• 【vue设计与实现】组件的实现原理 5 - 插槽的工作原理与实现 & 注册生命周期


    组件的插槽指组件会预留一个槽位,该槽位具体要渲染的内容由用户插入,如下模板所示:

    <template>
    	<header><slot name="header" /></header>
    	<div>
    		<slot name="body" />  
    	</div>
    	<footer><slot name="footer" />  </footer>
    </template>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    当在父组件中使用组件时,可以根据插槽的名字来插入自定义的内容

    <MyComponent>
    	<template #header>
    		<h1>我是标题</h1>
    	</template>
    	<template #body>
    		<section>我是内容</section>
    	</template> 
    	<template #footer>
    		<p>我是注脚</p>
    	</template>
    </MyComponent>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    上面这段父组件的模板会被编译成如下渲染函数:

    // 父组件的渲染函数
    function render(){
    	return {
    		type: MyComponent,
    		// 组件的children 会被编译成一个对象
    		children:{
    			header(){
    				return {type:'h1',children:'我是标题'}
    			}body(){
    				return {type:'section',children:'我是内容'}
    			}footer(){
    				return {type:'p',children:'我是注脚'}
    			}}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    可以看到,组件模板中的插槽内容会被编译为插槽函数,而插槽函数的返回值就是具体的插槽内容
    组件MyComponent的模板则会被编译为如下渲染函数:

    // MyComponent 组件模板的编译结果
    function render(){
    	return [
    		{
    			type: 'header',
    			children:[this.$slots.header()]
    		},
    		{
    			type: 'body',
    			children:[this.$slots.body()]
    		},
    		{
    			type: 'footer',
    			children:[this.$slots.footer()]
    		},
    	]
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    可以看到,渲染插槽内容就是调用插槽函数并渲染由其返回的内容的过程
    在运行的实现上,插槽则依赖于setupContext中的slot对象,如下面代码所示

    function mountComponent(vnode, container, anchor){
    	// 省略部分代码
    	
    	// 直接使用编译好的vnode.children对象作为slots对象即可
    	const slots = vnode.children || {}
    
    	// 将slots对象添加到setupContext中
    	const setupContext = {attrs,emit,slots}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    为了在render函数内和生命周期钩子函数内能够通过this.$slots来访问插槽内容,还需要再renderContext中特殊对待$slots属性,如下代码所示

    function mountComponent(vnode, container, anchor){
    	// 省略部分代码
    
    	const slots = vnode.children || {}
    
    	const instance = {
    		state,
    		props: shallowReactive(props),
    		isMounted:false,
    		subTree: null,
    		// 将插槽添加到组件实例上
    		slots
    	} 
    
    	// 省略部分代码
    
    	const renderContext = new Proxy(instance,{
    		get(t,k,r){
    			const {state, props, slots} = t
    			// 当k的值为$slots时,直接返回组件实例是上的slots
    			if(k==='$slots') return slots
    		}
    	})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    注册生命周期

    在Vue.js3,有一部分组合式API是用来注册生命周期钩子函数,例如onMounted, onUpdated等,如下面的代码所示:

    import { onMounted } from 'vue'
    
    const MyComponent = {
    	setup(){
    		onMounted(()=>{
    			console.log('mounted 1')
    		})
    		// 可以注册多个
    		onMounted(()=>{
    			console.log('mounted 2')
    		})
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这些钩子函数会在组件被挂载之后再执行。
    这里的疑问在于,在A组件的setup函数中调用onMounted函数会将该钩子函数注册到A组件上,而在B组件的setup函数中调用onMounted函数会将该钩子函数注册到B组件上。要实现这个功能呢,需要维护一个变量currentInstance,用来存储当前组件实例,每当初始化组件并执行组件的setup函数之前,先将currentInstance设置为当前组件实例,再执行组件的setup函数,这样就可以通过currentInstance来获取当前正在被初始化的组件实例,从而将那些通过onMounted函数注册的钩子函数与组件实例进行关联。
    看下面代码:

    // 全局变量,存储当前正在被初始化的组件实例
    let currentInstance = null
    // 该方法接收组件实例为参数,并将改实例设置为currentInstance
    function setCurrentInstance(instance){
    	currentInstance = instance
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    有了这些之后,就可以着手修改mountComponent函数,如下面的代码所示

    function mountComponent(vnode, container, anchor){
    	// 省略部分代码
    
    	const slots = vnode.children || {}
    
    	const instance = {
    		state,
    		props: shallowReactive(props),
    		isMounted:false,
    		subTree: null,
    		slots,
    		// 在组件实例中添加mounted数组,用来存储通过onMounted函数注册的生命周期钩子函数
    		mounted: []
    	} 
    	
    	// 省略部分代码
    	// setup
    	const setupContext = {attrs, emit, slots}
    
    	// 在调用setup函数之前,设置当前组件实例
    	setCurrentInstance(instance)
    	// 执行setup函数
    	const setupResult = setup(shallowReadonly(instance.props),setupContext)
    	// 在setup函数执行完毕之后,重置当前组件实例
    	setCurrentInstance(null)
    
    	// 省略部分代码
    }
    
    • 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

    现在,组件实例的维护已经搞定,接下来就是onMounted函数本身的实现,如下面代码:

    function onMounted(fn){
    	if(currentInstance){
    		// 将其添加到instance.mounted数组中
    		currentInstance.mounted.push(fn)
    	}else{
    		console.error('onMounted 函数只能在setup中调用')
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    最后一步要做的是,在合适的时机调用这些注册到instance.mounted数组中的生命周期钩子函数,如下面代码所示:

    function mountComponent(vnode, container, anchor){
    	
    	effect(()=>{
    		const subTree = render.call(renderContext, renderContext)
    		if(!instance.isMounted){
    			// 省略部分代码
    	
    			// 遍历instance.mounted数组并逐个执行即可
    			instance.mounted && instance.mounted.forEach(hook => hook.call(renderContext))
    		}else{
    			// 省略部分代码
    		}
    		instance.subTree = subTree
    	},{
    		sheduler: queueJob
    	})
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    可以看到,只需要再合适的时机遍历instance.mounted数组,并逐个执行该数组内的生命周期钩子函数即可

  • 相关阅读:
    【22-23春】AI作业10-经典卷积网络
    『Linux升级路』基础开发工具——vim篇
    排查与解决死锁
    优化双重循环
    Redis缓存穿透-热点缓存并发重建-缓存与数据库双写不一致-缓存雪崩
    Ajax和JQuery
    计算机网络(自顶向下方法)-网络层
    支付宝小程序 头部导航背景颜色渐变
    Python Selenium 八大元素定位方法(上)
    神经压缩文本训练:提升大型语言模型效率的新方法
  • 原文地址:https://blog.csdn.net/loyd3/article/details/127682276