组件的插槽指组件会预留一个槽位,该槽位具体要渲染的内容由用户插入,如下模板所示:
<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数组,并逐个执行该数组内的生命周期钩子函数即可