vue3挂载流程
一、整体渲染流程
众所周知 vue 的渲染从其入口函数,即createApp(APP).mount('#app')开始,整体的大概流程如下所示:
// 1.创建app
app = createApp(APP)
app = ensureRenderer().createApp(APP)
createRenderer(rendererOptions).createApp(APP)
baseCreateRenderer(rendererOptions).createApp(APP) // 为了实现重载额外包了一层
createAppAPI(render)(App)
// 2. 执行app的挂载
app.mount('#app')
vnode = createVNode(APP)
render(vnode,container)
patch(null, vnode, container)
processComponent(null, vnode, container, null);
mountComponent(vnode, container, null);
instance = vnode.component = createComponentInstance(vnode,null)
setupComponent(instance)
--------------------------------------------------- setupComponent
initProps(instance, instance .vnode.props)
initSlots(instance, instance .vnode.children)
setupStatefulComponent(instance)
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
setupContext = createSetupContext(instance)
setupResult = setup && setup(shallowReadonly(instance.props), setupContext)
handleSetupResult(instance, setupResult)
instance.setupState = proxyRefs(setupResult)
new Proxy(setupResult, shallowUnwrapHandlers);
---------------------------------------------------
setupRenderEffect(instance, vnode, container)
instance.update = effect(componentUpdateFn, {
scheduler: () => {
queueJob(instance.update);
},
});二、createApp
createApp主要完成了如下三个功能:
export const createApp = (...args) => {
// 1. 创建渲染器 2. 创建app
const app = ensureRenderer().createApp(...args);
const { mount } = app;
// 3. 重写mount(添加初始化container等功能)
app.mount = () => {
// 省略部分代码
// 执行原始mount
mount();
};
return app; // 返回app
};
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions));
}- 对于 mount 的重写主要用于添加一些平台相关的额外功能,比如根据
#app获取到具体的 dom 元素等 ensureRenderer确保渲染器只会进行一次初始化,其中rendererOptions是一些 web 平台的 dom 操作集合对象,在分析渲染器时,我们知道 vue 为了实现跨平台,将其中一些依赖于特定平台的操作进行了封装。
在createRenderer定义了一大堆函数,但对于我们理解整体流程来说,最重要的是它返回的对象,因为可以看出 vue 通过调用其返回对象中的 createApp 方法,从而创建了 app 对象。
function baseCreateRenderer(options, createHydrationFns) {
// 省略部分代码
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate), // 创建app的方法。这里会调用render方法
};
}hydrate是服务端渲染使用的变量这里不用管,createAppAPI(render)主要完成的功能如下:
export function createAppAPI(render) {
return function createApp(rootComponent) {
const app = {
_component: rootComponent,
mount(rootContainer) {
const vnode = createVNode(rootComponent);
render(vnode, rootContainer);
},
};
return app;
};
}- 传入的 render 在执行
app.mount的时候调用 - 创建 app 实例并返回
二、mount
1. mountComponent
从上述代码可以看出原始的 mount 函数主要完成的功能为 创建 vnode 并使用 render 函数进行渲染 。其中 render 函数内部会调用patch函数,根据vnode.type进行不同的处理。初始渲染时执行的是mountComponent(vnode, container, null)(mountComponent),mountComponent 主要实现的功能如下:
function mountComponent(initialVNode, container, parentComponent) {
// 1. 先创建一个组件instance
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent
));
// 2. 给 instance 加工加工
setupComponent(instance);
// 3. 设置渲染副作用
setupRenderEffect(instance, initialVNode, container);
}setupComponent类似于组件实例的初始化,在 instance 上添加额外的属性setupRenderEffect类似于 vue2 中设置 render Watcher
2. setupComponent
function setupComponent(instance) {
const { props, children } = instance.vnode;
initProps(instance, props); // 解析props和attrs
initSlots(instance, children); // 初始化slots
setupStatefulComponent(instance);
}
function setupStatefulComponent(instance) {
// 1. 创建渲染上下文
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
// 2. 调用 setup
const Component = instance.type;
const { setup } = Component;
if (setup) {
// 设置当前 currentInstance 的值
setCurrentInstance(instance);
const setupContext = createSetupContext(instance); // { attrs, emit, slots, expose}
const setupResult = setup && setup(shallowReadonly(instance.props), setupContext);
setCurrentInstance(null);
// 3. 处理 setupResult
handleSetupResult(instance, setupResult);
} else {
// 给 instance 设置 render
// 包含模板/渲染函数规范化以及对2.x选项式的兼容
finishComponentSetup(instance);
}
}初始化 props 时根据组件内部声明的 props 选项将传入组件的属性挂载到
instance.props或instance.attrs父组件环境下在编译时会将使用子组件时传入的插槽编译为一个 children 对象,其中包含返回 vnode 的函数。在初始化 slots 时将 children 对象中的函数取出并挂载到
instance.slots渲染上下文指的是在我们在编写 template 模板中会使用的数据,实际上是对
instance.ctx对象的代理setupContext中使用的 expose 函数本质上依然是将传入的对象挂载到instance.exposed中handleSetupResult实现的功能是根据 setup 函数返回值的类型进行不同处理如果返回值是函数,将此函数作为 render 挂载到
instance.render如果返回值是对象,将此对象挂载到
instance.setupState,因为此对象中的数据在模板中也需要可以直接进行访问。(在渲染上下文中对其进行处理)之所以返回对象中 ref 类型的响应式数据在模板中可以直接使用而不用
xx.value,是因为在挂载时使用proxyRefs进行了解构。instance.setupState = proxyRefs(setupResult);
3. setupRenderEffect
function setupRenderEffect(instance, initialVNode, container) {
function componentUpdateFn() {
if (!instance.isMounted) {
// 组件初始化的时候会执行这里
// 调用 render 函数触发依赖收集,响应式的值变更后会再次触发这个函数
const proxyToUse = instance.proxy; // 渲染上下文
const subTree = (instance.subTree = normalizeVNode(
instance.render.call(proxyToUse, proxyToUse)
));
// 触发beforeMount钩子函数
// patch组件内实际的内容
patch(null, subTree, container, null, instance);
// 把 root element 赋值给 组件的vnode.el ,为后续调用 $el 的时候获取值
initialVNode.el = subTree.el;
instance.isMounted = true;
} else {
// 响应式的值变更后会执行此处逻辑
// 主要就是拿到新的 vnode ,然后和之前的 vnode 进行对比
// 拿到最新的 subTree
const { next, vnode } = instance;
// 如果有 next 的话, 说明需要更新组件的数据(props,slots 等)
// 先更新组件的数据,然后更新完成后,再继续对比当前组件的子元素
if (next) {
next.el = vnode.el;
updateComponentPreRender(instance, next);
}
const proxyToUse = instance.proxy;
const nextTree = normalizeVNode(instance.render.call(proxyToUse, proxyToUse));
// 替换之前的 subTree
const prevTree = instance.subTree;
instance.subTree = nextTree;
// 触发 beforeUpdated hook
// 用旧的 vnode 和新的 vnode 交给 patch 来处理
patch(prevTree, nextTree, prevTree.el, null, instance);
// 触发 updated hook
}
}
instance.update = effect(componentUpdateFn, {
scheduler: () => {
queueJob(instance.update);
},
});
}- 在 vue3.2 版本里面设置副作用函数使用的是
new ReactiveEffect,至于为什么不直接用 effect ,是因为需要一个 scope 参数来收集所有的 effect,而 effect 这个函数是对外暴露的 API ,设计上要保持简洁和稳定性,不适合频繁改变其参数和行为。所以会使用new ReactiveEffect,因为 它是一个内部类,具有更高的灵活性来适应内部需求,包括接受额外的参数。 - 执行
instance.render函数会生成组件实际内容的 vnode,然后调用patch将其挂载 - 当组件更新是因为内部数据变更引起的自更新时,next 的值为 null;而父组件传入子组件的 props 发生变化导致的被动更新时,next 的值为新的子组件 vnode。
- 当在组件标签上使用 ref 获取组件实例时,实际获取的是
exposeProxy,因此使用ref.value.xxx使用子组件的属性或者方法时,只能使用子组件通过 expose 暴露出来的方法和公共方法。(即$el,$props这些)
本质上和 vue2 区别不大,主要实现就两部分:
- 封装一个函数,其中先
render再patch- 将函数设置为副作用(vue2 中的 watcher)
设置副作用的时候函数会自动执行一次,将其与模板中所用到的响应式数据绑定
三、总结
总体来说整体流程与 vue2 区别不大,首先会实例化应用(createApp 和 new vue),然后创建组件 vnode 并执行挂载,挂载过程中会将包含render和patch的函数与响应式数据相关联,并在执行 patch 函数时挂载子节点和子组件。
