一、组件的渲染
一个有状态组件就是一个选项对象,如下所示MyComponent是一个组件:
const MyComponent = {
name:'MyComponent',
data(){
return{
foo:'hello world'
}
},
//一个组件必须包括一个渲染函数,返回值为虚拟dom
render(){
return{
type:'div',
children:`foo的值是:${this.foo}` //在渲染函数内使用组件状态
}
}
}
从渲染器的内部实现来看,一个组件是一个特殊类型的虚拟dom节点,type属性就是组件的选项对象:
const vnode = {
type:MyComponent
}
渲染器的patch函数会对虚拟dom的type类型进行判断,如果是对象的话,就当成组件来处理,执行挂载组件mountComponent和更新组件patchComponent的操作。在挂载组件函数中,要调用reactive函数将data函数返回的状态包装为响应式数据,并且要将渲染函数包装为副作用函数,将render函数的this指向响应式数据state,这样在渲染函数内就能通过this拿到组件状态,并且当组件自身的响应式数据发生变化,组件就会重新执行渲染函数。
function mountComponent(vnode,container,anchor){
const componentOptions = vnode.type
const {render,data} = componentOptions
const state = reactive(data())
effect(()=>{
const subTree = render.call(state,state)
patch(null,subTree,container,anchor)
})
}
现在effect的执行是同步的,多次修改响应式数据,将会导致渲染函数执行多次。为副作用函数指定一个调度器,当副作用函数需要重新执行时,不立即执行它,而是将它缓存到微任务队列,等执行栈清空后,再将其从微任务队列中取出并执行,这样无论对响应式数据进行多少次修改,副作用函数都会只重新执行一次。
这样做还存在的问题是现在patch函数的第一个参数是null,相当于每次发生更新都会进行全新的挂载。因此需要组件实例,更新的时候用新的subTree与上一次组件渲染的subTree打补丁就行。
二、组件实例
为了解决上述问题,在挂载组件函数中定义组件实例,并将实例设置到vnode上:
const instance = {
//组件自身的状态数据,即data
state,
//用来表示组件是否已经被挂载
isMounted:false,
//组件所渲染的内容
subTree:null
}
三、组件的props
对于一个组件来说,有两部分关于props的内容:
1、为组件传递的props数据,即组件的vnode.props对象
2、组件选项对象中定义的props选项,即MyComponent.props对象
props本质上是父组件传过来的数据,当props发生变化时,会触发父组件重新渲染,更新过程中,渲染器发现父组件的subTree包含组件类型的虚拟节点,就会调用子组件更新函数,这就叫子组件的被动更新。
四、组件事件
在组件中用emit来发射自定义事件,在父组件中使用该组件,就可以监听由emit函数发射的自定义事件:
<MyComponent @change="handler" />
const CompVNode = {
type:MyComponent,
props:{
onChange:handler
}
}
自定义事件change被编译成名为onchange的属性,并存储在props数据对象中。
五、setup函数
组件的setup函数是vue3新增的组件选项,为了解决vue 中 data、computed、methods、watch 等内容非常多以后,同一业务逻辑的 data 中的数据和 methods 中的方法在 vue 文件中“相隔甚远”的问题。setup函数返回一个对象,该对象中包含的数据将暴露给模版使用。
六、异步组件
使用 Vue3 的 defineAsyncComponent 特性可以延迟加载组件,会在服务器需要时加载。使用动态导入语句import()来加载组件,会返回一个promise实例。defineAsyncComponent接收的参数包括异步组件加载器、超时时长、出错时要渲染的组件、延迟时间、loading组件。常用于让多个组件使用同一个挂载点,并动态切换。
这个函数的实现原理是设定时长为延迟时间的定时器,超过这个时间还没加载完就把loading置为true,渲染loading组件;如果加载超时,就渲染error组件,如果加载成功,渲染该异步组件;无论加载成功或失败,最后都要将loading置回false。
七、内建组件-KeepAlive
用KeepAlive包裹的内部组件可以对渲染的dom进行缓存,当组件切换时,不会重新卸载挂载dom,优化了性能。应用场景:当表单填写至一半跳转到别的页面,再跳转回来时刚刚填写的内容依然存在。
实现原理:
1、组件本身不会渲染额外的内容,因为它的渲染函数最终只返回被keepalive包裹的组件。默认插槽就是被包裹的组件,因此在setup函数中可以拿到。
2、使用map对象来实现对组件的缓存,键是vnode.type即组件选项对象,值是用于描述组件的vnode对象,缓存了这个对象就相当于缓存了组件实例。
3、KeepAlive组件的实例上会被添加两个内部函数_deactivate和_activate,如果有缓存的内容,在渲染器中就要调用_activate函数来激活它,激活的本质就是将组件的内容从隐藏容器搬回来,在卸载组件的操作中调用_deactivate函数使其失活。
4、KeepAlive组件支持三个 Props,分别是 include、exclude 和 max。如果子组件名称不匹配 include 的 vnode ,以及子组件名称匹配 exclude 的 vnode 都不应该被缓存。max表示能缓存组件的最大数量,缓存策略就是每次要将当前访问的组件作为最新组件,当缓存数量超过max时,删除最久没有访问过的组件。
八、内建组件-Teleport
用Teleport包裹的组件可以被指定挂载到to属性指定的父节点下,但逻辑仍保持原来的逻辑。应用场景:一个全屏模式的组件,用Teleport包裹能使它的样式不受父组件的影响。
实现原理:在patch函数中,Teleport组件的渲染逻辑被分离出来,这个组件的创建主要分为三步:1、在主视图里插入注释节点或者空白文本节点;2、获取目标元素节点;3、调用mount方法创建子节点往目标元素插入子节点。这个组件的更新操作包括更新子节点,处理 disabled 属性变化的情况,处理 to 属性变化的情况。
九、内建组件-Transition
用Transition包裹的组件可以为其添加入场/出场的过渡效果,Transition组件本身不会渲染任何额外的内容,它通过默认插槽读取过渡元素,并渲染需要过渡的元素。
实现原理:在过渡元素的vnode对象上添加与dom元素过渡相关的钩子函数。比如beforeenter钩子会在创建dom后,挂载dom前调用,在这个钩子函数中设置初始状态,添加enter-from和enter-active类;enter钩子在挂载dom后执行,在这个钩子函数中要在下一帧(类似于requestanimation效果,如果在同一帧那么就直接不绘制enter-from类的效果了)移除enter-from类,添加enter-to类,并监听transitioned事件,在过渡效果结束后删除enter-to和enter-active。leave钩子在卸载元素时调用,将卸载操作封装为一个函数,先调用leave钩子,再执行卸载操作。在leave钩子中先设置离场过渡的初始状态,添加leave-from类和leave-active类,在下一帧移除leave-from,添加leave-to,并监听transitioned事件,在过渡效果结束后删除leave-to和leave-active。