手写Vue3初始化流程
分享:我在情感上的愚钝就像是门窗紧闭的屋子,虽然爱情的脚步在屋前走过去又走过来,我也听到了,可是我觉得那是路过的脚步,那是走向别人的脚步。直到有一天,这个脚步停留在这里,然后门铃响了。——《第七天》
前言
又是学习Vue
的一天,也是想知道createApp
做了什么的一天,演变成想知道Vue
整个初始化的过程的一天。
入口
const App = {
render() {
// ui
return h(
"div",
{
id: "root",
class: ["red", "blue"],
},
[h("p", { class: "red" }, "hi"), h("p", { class: "blue" }, "mini-vue")]
);
},
setup() {
// composition API
return {
msg: "VueVueVue",
};
},
};
const rootContainer = document.querySelector("#app");
createApp(App).mount(rootContainer);
这里我们能看见createApp
, mount
和 h
,但是不知道它内部都做了什么,这就来写一个初始化的流程出来。
- 可以看到
createApp
接收一个对象,返回一个对象并且有一个mount
函数 mount
函数接收一个根元素
createApp & mount
function createApp(rootComponent) {
return {
mount(rootContainer) {
// component 转换成 vnode
const vnode = createVNode(rootComponent);
// 所有逻辑操作,都基于 vnode 做处理
render(vnode, rootContainer);
},
};
}
- 在
createApp
内,只做一件事,就是 return 一个对象带有 mount 方法的对象 - 在
mount
中,会先将组件转换成虚拟DOM
,然后之后所有的操作都会基于虚拟DOM
执行,主要逻辑就是render
函数,它将根组件挂载到根元素上
render
function render(vnode, container) {
// patch
patch(vnode, container);
}
在 render
中只调用了patch
,这是因为挂载的时候,不仅仅是组件,还有element和text,所以patch
方法就是用来判断是哪种类型的vnode
需要做处理
patch
function patch(vnode, container) {
// 处理element processElement
// console.log(vnode.type);
if (typeof vnode.type === "string") {
processElement(vnode, container);
} else if (isObject(vnode.type)) {
// 处理组件 processComponent
processComponent(vnode, container);
}
}
当Type
为对象时候,走处理组件逻辑(processComponent
),为字符串的时候,走处理元素的处理逻辑(processElement
)
processComponent
function processComponent(vnode: any, container: any) {
// 挂载component
mountComponent(vnode, container);
}
内部调用了mountComponent
方法,用来对组件进行处理
mountComponent
function mountComponent(vnode: any, container) {
// 创建组件实例,这个实例对象会存储一些组件上的属性 如:props,slots
const instance = createComponentInstance(vnode);
// 处理组件
setupComponent(instance);
setupRenderEffect(instance, container);
}
在这个方法中,首先会创建一个组件实例,然后后面对组件的处理都是基于这个实例
createComponentInstance
function createComponentInstance(vnode) {
const component = {
vnode,
type: vnode.type,
};
return component;
}
在createComponentInstance
中,对组件做了一个实例化操作,本质就是重新包装了一下vnode
,方便后续处理
setupComponent
function setupComponent(instance) {
/*
初始化:
1. initProps
2. initSlots()
3. 调用setup
*/
setupStatefulComponent(instance);
}
setupComponent
方法是初始化的核心,也就是调用所有初始化API的地方,这里就只看初始化节点(ps:其他props,slots暂不考虑)
setupStatefulComponent
function setupStatefulComponent(instance: any) {
// {vnode: {type: App,props, children}}
// const App = {setup(){}}
// 拿到组件
const Component = instance.type;
const { setup } = Component;
if (setup) {
// function(render) or object(data)
const setupResult = setup();
handleSetupResult(instance, setupResult);
}
}
在setupStatefulComponent
中就是为了拿到组件的setup
函数的返回值(即h函数的返回值 vnode
),使用handleSetupResult
函数将这个vnode
挂载到instance
上
handleSetupResult
function handleSetupResult(instance, setupResult: any) {
// function or object
if (typeof setupResult === "object") {
instance.setupState = setupResult;
}
finishComponentSetup(instance);
}
将最后操作完成后的组件实例instance
传入finishComponentSetup
中
finishComponent
function finishComponentSetup(instance: any) {
const Component = instance.type;
// 判断用户是否提供了render函数
instance.render = Component.render;
}
当用户提供了render
函数的时候,就使用用户提供的
setupRenderEffect
function setupRenderEffect(instance: any, container) {
// 虚拟节点树
const subTree = instance.render();
patch(subTree, container);
}
当所有操作完成后,通过render
拿到所有需要挂载的虚拟节点树
,通过patch
递归操作,因为每一次挂载节点都需要判断节点类型,类型不同,不同处理
processElement
function processElement(vnode, container) {
// init -> update
mountElement(vnode, container);
}
这里处理与组件处理类似
mountElement
function mountElement(vnode: any, container: any) {
const { type, props, children } = vnode;
const el = document.createElement(type);
// children -> string or array
if (typeof children === "string") {
el.textContent = children;
} else if (Array.isArray(children)) {
// vnode,挂载children在父元素上
mountChildren(vnode, el);
}
// props -> object
for (const key in props) {
if (Object.prototype.hasOwnProperty.call(props, key)) {
const val = props[key];
el.setAttribute(key, val);
}
}
el.setAttribute("id", "root");
container.append(el);
}
这里首先根据标签类型,创建dom元素,将children
挂载到这个dom元素
上,且将props
属性名称与值,添加到这个dom元素
上,这里当children
是数组类型时,特殊处理
mountChildren
function mountChildren(vnode, container) {
vnode.children.forEach((v) => {
patch(v, container);
});
}
这里的核心逻辑就是patch
方法,每一次需要挂载节点的时候,都需要知道当前要挂载节点的类型(组件,元素,文本),因为不同节点类型,不同处理
总结
以上大概就是一个Vue初始化的全过程,我们可以知道:
- 所有的操作都是基于
虚拟DOM
做处理 - 最核心就是
patch
函数,主要作用就是用来,分发不同类型节点给对应的API做处理 - Vue将每个函数都抽离的很细致,做到了每个函数只做一件事,遵循职责单一原则,值得学习
转载自:https://juejin.cn/post/7206654718674829369