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

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

<template>
	<header><slot name="header" /></header>
	<div>
		<slot name="body" />  
	</div>
	<footer><slot name="footer" />  </footer>
</template>

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

<MyComponent>
	<template #header>
		<h1>我是标题</h1>
	</template>
	<template #body>
		<section>我是内容</section>
	</template> 
	<template #footer>
		<p>我是注脚</p>
	</template>
</MyComponent>

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

// 父组件的渲染函数
function render(){
	return {
		type: MyComponent,
		// 组件的children 会被编译成一个对象
		children:{
			header(){
				return {type:'h1',children:'我是标题'}
			}body(){
				return {type:'section',children:'我是内容'}
			}footer(){
				return {type:'p',children:'我是注脚'}
			}}
	}
}

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

// MyComponent 组件模板的编译结果
function render(){
	return [
		{
			type: 'header',
			children:[this.$slots.header()]
		},
		{
			type: 'body',
			children:[this.$slots.body()]
		},
		{
			type: 'footer',
			children:[this.$slots.footer()]
		},
	]

}

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

function mountComponent(vnode, container, anchor){
	// 省略部分代码
	
	// 直接使用编译好的vnode.children对象作为slots对象即可
	const slots = vnode.children || {}

	// 将slots对象添加到setupContext中
	const setupContext = {attrs,emit,slots}

}

为了在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
		}
	})
}

注册生命周期

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

import { onMounted } from 'vue'

const MyComponent = {
	setup(){
		onMounted(()=>{
			console.log('mounted 1')
		})
		// 可以注册多个
		onMounted(()=>{
			console.log('mounted 2')
		})
	}

}

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

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

有了这些之后,就可以着手修改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)

	// 省略部分代码
}

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

function onMounted(fn){
	if(currentInstance){
		// 将其添加到instance.mounted数组中
		currentInstance.mounted.push(fn)
	}else{
		console.error('onMounted 函数只能在setup中调用')
	}
}

最后一步要做的是,在合适的时机调用这些注册到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
	})
}

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值