1. 什么是组件?
从用户的角度看,一个有状态组件的组件就是一个选项对象,但是从渲染器内部的实现看,一个组件是一个特殊类型的虚拟DOM节点。通过vnode.type 属性来存储标签名称。实际上,组件本身是对页面内容的封装,它用来描述页面内容的一部分,render函数用来描述组件所内容的接口。
const MyComponent = {
// 组件名称
name: "MyCompent",
render(){
// 返回虚拟dom
return{
type: "div", // Fragment 片段、Text 文本节点、string 普通元素、object 选项对象(组件)
children: `hello world`
}
}
}
如果type是选项对象(组件),调用mountComponent、patchCompent函数完成组件的挂载和更新,真正完成渲染任务的是 mountCompontent 函数。
function mountCompontent(vnode, container, anchor){
// 通过vnode获取组件的选项对象
const componentOptions = vnode.type;
// 获取组件的渲染函数
const { render } = componentOptions;
// 执行渲染函数,
const subTree = render();
// 调用patch挂载组件
patch(null, subTree, container, anchor);
}
2. 组件的状态与自更新是怎样的?
2.1 初始化组件状态
- 组件自身状态的初始化分两步,首先通过组件的选项对象获得data函数,调用teactive函数将data函数返回的状态包装为响应式对象;
- 在调用render函数时,将其this的指向设置为响应式数据state,同时将state作为render函数的第一个参数传递。
2.2 组件状态的自动更新
一但自身想响应式数据发生变化时,组件根据收集的依赖,重新执行渲染函数进行派发更新,但是因为effect函数是同步的,如果多次修改响应式数据的值,会导致渲染函数多次执行。这里设计了一个调度器缓存机制,当副作用函数需要重新执行时,优先缓冲到微任务队列中,等到执行栈清空后,再将它从微任务队列中取出来执行。
// 用set结构存储缓存队列
const queue = new Set();
// 是否正在刷新任务队列
let isFlushing = false;
// 立即执行resolve的promise实例
const p = Promise.resolve();
// 开始刷新
function queueExecute(job) {
// 加入微队列
queue.add(job);
// 如果还没有刷新队列,先刷新一遍
if(!isFlushing){
isFlushing = true;
p.then(() => {
try{
// 执行任务队列中的任务
queue.forEach(job => job());
} finally {
// 重置状态
isFlushing = false;
queue.clear = 0;
}
})
}
}
3. 组件的生命周期
这段代码中,首先从选项对象中取的注册到组件上的生命周期函数,然后在合适时机调用不同的钩子,这就是生命周期的基本原理。
function mountComponent(vnode, container, anchor){
const componentOptions = vnode.type;
// 从组件选项对象中获取组件的生命周期函数
const { render, data, beforeCreate, created, beforeMount, mounted, beforeUpdate, updated } = componentOptions;
// 1.调用 beforeCreate
beforeCreate && beforeCreate();
const state = reactive(data());
const instance = { state, isMounted: true, subTree: null }
vnode.component = instance;
// 2.调用 created
created && created.call(state);
effect(() => {
const subTree = render.call(state, state);
if(!instance.isMounted){
// 3.调用 beforeMounted
beforeMounted && beforeMounted();
patch(null, subTree, container, anchor);
instance.isMounted = true;
// 4.调用 mounted
mounted && mounted();
} else {
// 5.调用 brforeUpdate
beforeUpdate && beforeUpdate();
patch(instance.subTree, subTree, container, anchor);
// 6.调用 update
updated && updated.call(state)
}
instance.subTree = subTree;
}, { scheduler: queueExecute })
}
4.props与组件的被动更新
4.1 Props的传递与获取
在虚拟dom层面,组件的props与普通HTML标签的属性差别不大,为什么这么说?来看一段代码,比如这个组件:
<MyComponent title="提示" :value="val" />
对应的虚拟dom会解析成:
const vnode = {
type: Mycomponent,
props: {
title: "提示",
value: this.val
}
}
所以,对于一个组件来说,我们只需要关心两部分内容
- 为组件传递props数据,即vnode.props对象;
- 组件选项对象中定义的props选项,即 MyComponent.props对象; 将组件选项中定义的MyComponent.props 和 为组件传递的vnode.props相结合,最终解析出组件渲染时需要使用的props和attrs数据。 在Vue3中,没有定义在MyComponent.props 选项中的props数据将存储到attrs中。
4.2 Props数据变化与更新
props本质是父组件的数据,当props发生变化时,会触发父组件的重新渲染。在更新过程中,渲染器发现父组件的subTree包含组件类型的虚拟节点,会调用patchComponent函数完成子组件的更新。
function path(n1, n2, container, ancher){
if(n1 && n1.type !== n2.type){
unmount(n1);
n1 = null;
}
const { type } = n2;
if(typeof type === 'string'){
//……
} else if(typeof type === Text){
//……
} else if(typeof type === Fragment){
//……
} else if(typeif type === 'object'){
// vnode.type 的值是选项对象,作为组件来处理
if(!n1){
mountComponent(n2, container, anchor);
} else {
// 更新组件
patchComponent(n1, n2, anchor);
}
}
}
patchComponent函数用来完成子组件的更新,具体实现:
// 子组件的更新
function patchComponent(n1, n2, anchor){
// 获取组件实例
const instance = (n2.component = n1.component);
// 获取当前props数据
const { props } = instance;
// 调用 hasPropsCahnged 检测为子组件传递的props是否发生变化,如果没有变化,则不需要更新
if(hasPropsCahnged(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)){
delte props[k];
}
}
}
}
// 检测为子组件传递的props是否发生变化
function hasPropsCahnged(prevProps, nextProps){
const nextKeys = Object.keys(nextProps);
// 如果新旧 props 的数量变化了,则说明有变化
if(nextKeys.lengt !== Object.keys(prevProps).length){
return true;
}
// 遍历每一项看内部变化
for(let i = 0; i < nextKeys.length; i++){
const key = nextKeys[i];
// 有不相等的props,说明有变化
if(nextPeops[key] !== prevProps[key]){
return true;
}
}
return false;
}
以上是组件被动更新的核心逻辑,上面的处理中,并没有处理attrs和solts的更新,attrs的更新本质上和更新props相似;
总结
- 本文首先描述的是如何使用虚拟节点来描述组件,使用虚拟节点的vnode.type 属性来存储组件对象,渲染器根据虚拟节点的type属性类型判断是否为组件,如果是组件,则调用mountComponent和patchComponent来完成组件的挂载和更新。
- 接着,讲到了组件的自动更新,在组件挂载阶段,会为组件创建一个用于渲染内容的副作用函数。这个副作用函数会与组件自身的响应式数据建立联系,也就是依赖收集。当组件数据发生变化时,会触发副作用函数重新执行,也就是派发更新。
- 然后,介绍了组件本质上是一个对象,包含了组件运行过程中的全部状态,例如组件是否挂载,响应式数据,以及所渲染的内容。有了这些实例,在副作用函数内,根据组件实例上的标识,来决定应该进行全新的挂载还是打补丁。
- 最后,通过源码,实现了props与组件的被动更新,副作用自更新所引起的子组件的更新叫做子组件的被动更新,子组件的更新通过判断新旧数据的变化,来判断是否被更新。