• 【vue设计与实现】组件的实现原理 3 - props与组件的被动更新


    虚拟DOM层面,组件的props与普通HTML标签的属性差别不大。假设我们有如下模板:

    <MyComponenent title="A Big Title" :other="val" />
    
    • 1

    这段模板对应的虚拟DOM是:

    const vnode = {
    	type: MyComponent,
    	props:{
    		title: 'A big Title',
    		other: this.val
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以看到,模板与虚拟DOM几乎是“同构”的。另外,在编写组件时,需要显式地指定组件会接收那些props数据,如下面代码所示:

    const MyComponent = {
    	name: 'MyComponent',
    	// 组件接收名为title的props,并且该props的类型为String
    	props:{
    		tite: String
    	},
    	render(){
    		return{
    			type: 'div',
    			children: `count is: ${this.title}` // 访问props数据
    		}
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    所以,对于一个组件,有两部分关于props的内容需要关心的是:

    1. 为组件传递的props数据,即组件的vnode.props对象
    2. 组件选项对象中定义的props选项,即MyComponent.props对象

    结合两个选项来解析出组件在渲染时需要用到的props数据,具体实现如下:

    function mountComponent(vnode, container, anchor){
    	const componentOptions = vnode.type
    	// 从组建选项对象中取出props定义,即propsOption
    	const {render, data, props: propsOption /* 其他省略 */ } = componentOptions
    	
    	beforeCreate && beforeCreate()
    	const state = reactive(data())
    	// 调用resolveProps函数解析出最终的props数据与attrs数据
    	const [props, attrs] = resolveProps(propsOption, vnode.props)
    	const instance = {
    		state,
    		// 将解析出的props数据包装为shallowReactive并定义到组件实例上
    		props: shallowReacive(props),
    		isMounted: false,
    		subTree: null
    	}
    	vnode.comoponent = instance
    
    	// 省略部分代码
    	
    }
    
    // resolveProps 函数用于解析组件props和attrs数据
    function resolveProps(options, propsData){
    	const props = {}
    	const attrs = {}
    	// 遍历为组件传递的props数据
    	for (const key in propsData){
    		if(key in options){
    			// 如果为组件传递的props数据在组件自身的props选项中有定义,则将其视为合法的props
    			props[key] = propsData[key]
    		}else{
    			// 否则将其作为attrs
    			attrs[key] = propsData[key]
    		}
    	}
    	
    	return [props,attrs]
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    我们将组件选项中定义的MyComponent.props对象和为组件传递的vnode.props对象想结合,最终解析出组件在渲染时需要使用的props和attrs数据。
    这里需要注意两点:

    1. 在Vue.js3中,没有定义在MyComponent.props选项中的props数据将存储到attrs对象中
    2. 上述实现中没有包含默认值、类型校验等内容的处理。当然这些实现起来也不难

    处理完props数据后,再来看关于props数据变化的问题。props本质上是父组件的数据,当props发生变化时,会触发父组件重新渲染,假设父组件的模板如下:

    <template>
    	<MyComponent :title="title" />
    </template>
    
    • 1
    • 2
    • 3

    其中,响应式数据title的初始值为字符串"A big title",因此首次渲染时,父组件的虚拟DOM为:

    const vnode = {
    	type: MyComponent,
    	props: {
    		title: 'A big title'
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当响应式数据title发生变化时,父组件的渲染函数会重新执行,产生的新虚拟DOM如下:

    const vnode = {
    	type: MyComponent,
    	props: {
    		title: 'A small title'
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    接着,父组件会进行自更新。在更新过程中,渲染器发现父组件的subTree包含组件类型的虚拟节点,所以会调用patchComponent函数完成子组件的更新,如下patch函数的代码所示:

    function patch(n1,n2,container,anchor){
    	if(n1 & n1.type !== n2.type){
    		unmount(n1)
    		n1 = null
    	}
    
    	const { type } = n2
    
    	if(typeof type === 'string'){
    		// 省略部分代码
    	}else if(type === Text){
    		// 省略部分代码
    	}else if(type === Fragment){
    		// 省略部分代码
    	}else if(typeof type === 'object'){
    		// vnode.type 的值是选项对象,作为组件来处理
    		if(!n1){
    			mountComponent(n2,container, anchor)
    		}else{
    			// 更新组件
    			patchComponent(n1,n2,anchor)
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    我们由父组件自更新所引起的子组件更新叫做子组件的被动更新
    当子组件发生被动更新时,需要做的是:

    1. 检测子组件是否真的需要更新,因为子组件的props可能不变
    2. 如果需要更新,则更新子组件的props,slots等内容

    patchComponent函数的具体实现如下:

    function patchComponent(n1,n2,anchor){
    	// 获取组件实例,即n1.component,同时让新的组件虚拟节点 n2.component也指向组件实例
    	// 将组件实例添加到新的组件vnode对象上,即n2.component = n1.component
    	const instance = (n2.component = n1.component) 
    	// 获取当前的props数据
    	const {props} = instance
    	// 调用hasPropsChanged检测为子组件传递的props是否发生变化,如果没有变化,则不需要更新
    	if(hasPropsChanged(n1.props, n2.props)){
    		// 调用 resolveProps函数重新获取props数据
    		const {nextProps} = resolveProps(n2.type.props, n2.props)
    		// 更新props
    		for(const k in nextProps){
    			props[k] = nextProps[k]
    		}
    		// 删除不存在的props
    		for(const k in props){
    			if(!(k in nextProps)) 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]
    		// 有不相等的props, 则说明有变化
    		if(nextProps[key] !== prevProps[key]) return true
    	}
    	return false
    
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    要注意的是

    1. 需要将组件实例添加到新的组件vnode对象上,即n2.component = n1.component,否则下次更新时将无法取得组件实例
    2. instance.props对象本身是浅响应的(即shallowReactive)。因此,在更新组件的props时,只需要设置instance.props对象下的属性值即可触发组件重新渲染。

    上面的实现中,没有处理attrs与slots的更新,attrs的更新本质上与更新props的原理相似。本质上,原理都是根据组件的props选项定义以及为组件传递的props数据来处理。

    由于props数据与组件自身的状态数据都需要暴露到渲染函数中,并使得渲染函数能够通过this访问它们,因此需要封装一个渲染上下文对象,如下面代码所示:

    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){ 
    				props[k] = v
    			}else{
    				console.error('不存在')
    			}
    		}
    	})
    	
    	// 生命周期函数调用时要绑定渲染上下文对象
    	created && created.call(renderContext)
    	//省略部分代码
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    上面这段代码,为组件实例创建了一个代理对象,该对象即渲染上下文对象。其作用是拦截数据状态的读取和设置操作,每当在渲染函数或生命周期钩子中通过this读取数据时,都会优先从组件的自身状态中读取,如果组件本身并没有对应的数据,则再从props数据中读取。最后将渲染上下文作为渲染函数以及生命周期钩子的this值即可

    补充知识
    对象的解构赋值
    如果变量名与属性名不一致,必须写成下面这样。

    let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
    baz // "aaa"
    
    • 1
    • 2
  • 相关阅读:
    Python 生成器与装饰器
    ubuntu下DHCP服务器搭建
    什么是jvm
    Hive3:数据的加载与导出
    java基于springboot的毕业生简历模板分享管理系统
    Feign
    基于SSM和Web实现的农作物生长监控系统设计与实现
    在windows上利用vmware17 搭建centos7 mini版本服务器
    Python 面向对象初步
    Markdown 教程之如何在 Markdown 文档中添加流程图、方程式和交互式图形
  • 原文地址:https://blog.csdn.net/loyd3/article/details/127613952