1.理解props与组件的被动更新
1.理解组件props传参
再虚拟DOM的层面,组件的props和普调的HTML标签属性差距不大,假设我们的虚拟DOM代码如下
js
const vnode = {
type: MyComponent,
props: {
title: 'A big Title',
other: this.val
}
}
在编写组件时,我们需要显式地指定组件会接收哪些 props 数据,如下面的代码所示:
js
const MyComponent = {
name: 'MyComponent',
// 组件接收名为 title 的 props,并且该 props 的类型为 String
props: {
title: String
},
render() {
return {
type: 'div',
children: `count is: ${this.title}` // 访问 props 数据
}
}
所以,对于一个组件来说,有两部分关于 props 的内容我们需要关心:
- 为组件传递的 props 数据,即组件的 vnode.props 对象;
- 组件选项对象中定义的 props 选项,即 MyComponent.props 对象。
2.再mountComponent中解析props
我们需要结合这两个选项来解析出组件在渲染时需要用到的 props 数据,具体实现如下:
js
function mountComponent(vnode, container, anchor) {
//省略其他代码
const componentOptions = vnode.type;
const { render, data, props: propsOptions } = componentOptions;
// 调用resolveProps解析出最终的props数据和attrs数据
const [props, attrs] = resolvePorps(propsOptions, vnode.props)
const instance = {
state,
// 将解析出的 props 数据包装为 shallowReactive 并定义到组件实例上
props: shallowReactive(props),
isMounted: false,
subTree: null
}
vnode.component = instance
}
function resolvePorps(options, propsData) {
const props = {}
const attrs = {}
for (const key in propsData) {
if (key in options) {
props[key] = propsData[key]
} else {
attrs[key] = propsData[key]
}
}
return [props, attrs]
}
再上面代码中,我们最终解析出来需要使用的props和attrs数据。props本质上是父组件的数据,当props发生变化的时候,会触发父组件的重新渲染
3.父组件自更新时,渲染子节点
当响应式的数据title发生变化的时候,父组件会重新渲染执行,当父组件进行子更新的时候。再更新过程中,渲染器发现父组件subTree包含组件类型的虚拟节点 ,所以会调用patchComponent
函数完成子组件的更新,如下面的patch
函数
js
function patch(n1, n2, container, anchor) {
if (typeof type === 'object') {
// vnode.type 的值是选项对象,作为组件来处理
if (!n1) {
mountComponent(n2, container, anchor)
} else {
// 更新组件
patchComponent(n1, n2, anchor)
}
}
}
其中,patchComponent
函数用来完成子组件的更新,我们吧由父组件子更新引起的子组件更新叫做子组件的被动更新。当子组件发生更新时,我们只需要做以下两个事情
- 判断子组件是否真的需要更新,因为props可能是不变的
- 如果需要更新,那就更新子组件的props。slots等内容
js
function patchComponent(n1, n2,) {
const instance = (n2.component = n1.component)
//获取当前的props数据
const { props } = instance;
//判断porps是否发生了变化
if (hasPropsChanged(n1.props, n2.props)) {
const [nextProps] = resolvePorps(n2.type.props, n2.props)
//更新props
for (const k in nextProps) {
props[k] = nextProps[k]
}
//删除不存在的props
for (const k in props) {
if (!(k in props)) delete props[k]
}
}
}
function hasPropsChanged(prevProps, nextProps) {
const nextKeys = Object.keys(nextProps)
//如果新旧的props数量变了,则说明有变化
if (nextKeys.length !== Object.keys(prevProps).length) {
return true
}
for (let i = 0; i < nextKeys.length; i++) {
const key = nextKeys[i]
if (nextProps[key] !== prevProps[key]) {
return true
}
}
return false
}
- 需要将组件实例添加到新的组件 vnode 对象上,n2.component=n1.component否则下次更新时将无法取 得组件实例;
- instance.props 对象本身是浅响应的(即 shallowReactive)。因此在更新组件的 props 时,只需要设置 instance.props 对象下的属性值即可触发组件重新渲染。
4.封装渲染上下文
由于 props 数据与组件自身的状态数据都需要暴露到渲染函数中,并使得渲染函数能够通过 this 访问它们,因此我们需要封装一个渲染上下文对象,如下面的代码所示:
js
function mountComponent(vnode, container, anchor) {
// 省略部分代码
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null
}
vnode.component = instance
// 创建渲染上下文对象,本质上是组件实例的代理
const renderContext = new Proxy(instance, {
get(t, k, r) {
// 取得组件自身状态与 props 数据
const { state, props } = t
// 先尝试读取自身状态数据
if (state && k in state) {
return state[k]
} else if (k in props) { // 如果组件自身没有该数据,则尝试从props 中读取
return props[k]
} else {
console.error('不存在')
}
},
set (t, k, v, r) {
const { state, props } = t
if (state && k in state) {
state[k] = v
} else if (k in props) {
console.warn(`Attempting to mutate prop "${k}". Props
are readonly.`)
} else {
console.error('不存在')
}
}
})
// 生命周期函数调用时要绑定渲染上下文对象
created && created.call(renderContext)
}
再上述代码中,我们创建了一个代理对象,它的意义在于拦截数据状态的读取和设置操作,每当再渲染函数或生命周期钩子中通过this来读取数据,都会优化从组件自身的状态中读取,如果组件本身没有对应的数据,则再从props数据中读取
2.setup函数的作用和实现
1.setup的基础定义
定义:setup主要用于配合组合式API,用于建立组合逻辑,创建响应数据,创建通用函数,注册生命周期钩子等能力。再组件的整个生命周期中,setup函数只会被挂载时执行一次,他的返回值有两种情况
1.返回一个函数,该函数作为组件的render函数
js
const Comp = {
setup() {
// setup 函数可以返回一个函数,该函数将作为组件的渲染函数
return () => {
return { type: 'div', children: 'hello' }
}
}
}
2.返回一个对象,该对象中包含的数据暴露给模版使用
js
const Comp = {
setup() {
const count = ref(0)
// 返回一个对象,对象中的数据会暴露到渲染函数中
return {
count
}
},
render() {
// 通过 this 可以访问 setup 暴露出来的响应式数据
return { type: 'div', children: `count is: ${this.count}` }
}
}
可以看到,setup 函数暴露的数据可以在渲染函数中通过 this 来访问。
另外,setup 函数接收两个参数。第一个参数是 props 数据对象,第二个参数也是一对象,通常称为 setupContext,如下面的代码所示:
js
const Comp = {
props: {
foo: String
},
setup(props, setupContext) {
props.foo // 访问传入的 props 数据
// setupContext 中包含与组件接口相关的重要数据
const { slots, emit, attrs, expose } = setupContext
// ...
}
}
从上面的代码可以看出,我们可以通过 setup 函数的第一个参数 取得外部为组件传递的 props 数据对象。同时,setup 函数还接收第 二个参数 setupContext 对象,其中保存着与组件接口相关的数据和方法,如下所示。
2.setup的基本实现
我们了解setup的基础定义后,那我们就围绕上述能力来尝试实现setup组件选项,如下:
1.从vnode中取出参数,调用setup,setup执行,传入props和setupContext
js
function mountComponent(vnode, container, anchor) {
const componentOptions = vnode.type;
let { render, data, setup } = componentOptions;
const state = data ? reactive(data()) : null; // 处理data选项
const [props, attrs] = resolvePorps(propsOptions, vnode.props)
const instance = {
state,
props: shallowReactive(props), // 处理props选项
isMounted: false,
subTree: null
}
const setupContext = { attrs }
const setupResult = setup(shallowReadonly(instance.props), setupContext) // 处理setup选项
let setupState = null;
if(typeof setupResult === 'function'){
if(render) { console.log('setup函数返回渲染函数,render函数被忽略')}
render = setupResult
}else{
setupState = setupResult
}
//省略后面
}
2.处理setup中的值,支持setup
js
function mountComponent(vnode, container, anchor) {
//省略前面
vnode.component = instance
const renderContext = new Proxy(instance,{
get(t,k,r){
const {state,props} = t
if(state && k in state){
return state[k]
}else if(k in props){
return props[k]
}else if(setupState && k in setupState){
return setupState[k]
}else{
console.warn('不存在该属性')
}
},
set(t,k,v,r){
const {state,props} = t
if(state && k in state){
state[k] = v
}else if(k in props){
console.warn('不能修改props')
}else if(setupState && k in setupState){
setupState[k] = v
}else{
console.warn('不存在该属性')
}
}
})
}
3.setup总结
- 我们通过检测setup函数的返回值类型来决定应该如何处理它,如果它的返回值是函数,则直接将其作为组件的渲染函数。
- 渲染上下文renderContext应该争取的处理setupState,因为setup函数返回的数据状态也应该暴露到渲染环境