本文主要根据vue3源码去理解清楚vue3的组件挂载流程(最后附流程图),根据个人阅读源码去解释,vue的组件是怎么从.vue
单文件组件一步步插入到真实DOM中,并渲染到页面上。
例子说明
那下面的简单代码去做个例子说明,先看看vue3的写法。
App.vue
<template> <h1>Hello <span class="blue">{{ name }}</span></h1> </template> <script> import { defineComponent } from 'vue'; export default defineComponent({ setup() { return { name: 'world' } } }) </script> <style scoped> .blue { color: blue; } </style>
index.js
import { createApp } from "vue"; import App from "./App.vue"; const root = document.getElementById('app'); console.log(App); createApp(App).mount(root);
index.html
<body> <div id="app"></div> <script src="/bundle.js"></script> <-- index.js经过打包 变成 bundle.js是经过打包的js --> </body>
通过这三个文件vue3就可以把App.vue
中的内容渲染到页面上,我们看看渲染结果,如下图:
看上面的例子:我们可以知道,通过vue中的createApp(App)
方法传入App组件
,然后调用mount(root)
方法去挂载到root
上。到这里就会有些疑问❓
- App组件经过vue编译后是什么样子的?
- createApp(App)这个函数里面经过怎么样的处理然后返回mount方法?
- mount方法是怎么把App组件挂载到root上的?
- ...
先看看第一个问题,我们上面代码有打印console.log(App)
,具体看看App
经过编译后是得到如下的一个对象:
其中setup
就是我们组件定义的setup函数
,而render
的函数代码如下,
const _hoisted_1 = /*#__PURE__*/createTextVNode("Hello "); // 静态代码提升
const _hoisted_2 = { class: "blue" }; // 静态代码提升
const render = /*#__PURE__*/_withId((_ctx, _cache, $props, $setup, $data, $options) => {
return (openBlock(), createBlock("h1", null, [
_hoisted_1,
createVNode("span", _hoisted_2, toDisplayString(_ctx.name), 1 /* TEXT */)
]))
});
到这里就不难看出,App.vue
文件经过编译之后得到一个render
函数,详情请看在线编译。当然还有css
代码,编译后代码如下:
var css_248z = "\n.blue[data-v-7ba5bd90] {\r\n color: blue;\n}\r\n";
styleInject(css_248z);
styleInject
方法作用就是创建一个style
标签,style
标签的内容就是css
的内容,然后插入到head标签
中,简单代码如下,
function styleInject(css, ref) {
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
head.appendChild(style);
}
渲染流程
从上面的例子我们可以看出,其实vue就是把单文件组件,经过编译,然后挂载到页面上,css样式插入到head中,其大致流程图如下:
第一部分的.vue
文件是怎么编译成render函数,其详细过程和细节很多,这里就不会过多赘述。本文着重讲述组件是怎么挂载到页面上面来的。首先我们看看createApp(App).mount(root);
这一行代码里面的createApp
是怎么生成并且返回mount
的。
app对象生成
// 简化后的代码
const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args);
const { mount } = app; // 先保存app.mount方法
// 重写app.mount方法
app.mount = (containerOrSelector) => {
// 省略一些代码
};
return app;
});
createApp
函数不看细节,直接跳到最后一行代码,看返回一个app
对象,app
里面有一个app.mount
函数,外面就可以这么createApp().mount()
调用了。
其中的细节也很简单,createApp
里面通过ensureRenderer()
延迟创建渲染器,执行createApp(...args)
返回一个app
对象,对象里面有mount
函数,通切片编程的方式,重写了app.mount
的函数,最后返回app这个对象。
现在的疑问来到了app
是怎么生成的,app
对象里面都有什么,这就看ensureRenderer
这个函数
const forcePatchProp = (_, key) => key === 'value';
const patchProp = () => {
// 省略
};
const nodeOps = {
// 插入标签
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null);
},
// 移除标签
remove: child => {
const parent = child.parentNode;
if (parent) {
parent.removeChild(child);
}
},
// 创建标签
createElement: (tag, isSVG, is, props) => {
const el = doc.createElement(tag);
// 省略了svg标签创建代码
return el;
},
// 创建文本标签
createText: text => doc.createTextNode(text),
// ...还有很多
}
const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps);
function ensureRenderer() {
return renderer || (renderer = createRenderer(rendererOptions)); // 创建渲染器
}
这个函数很简单,就是直接返回 createRenderer(rendererOptions)
创建渲染器函数。
从这里可以看出,如果我们vue用不到渲染器相关的就不会调用ensureRenderer,只用到响应式的包的时候,这些代码就会被tree-shaking掉。
这里的参数rendererOptions
需要说明一下,这些都是跟dom
的增删改查操作相关的,说明都在注释里面。
继续深入createRenderer
,从上面的代码const app = ensureRenderer().createApp(...args);
可以知道createRendere
会返回一个对象,对象里面会有createApp
属性,下面是createRenderer
函数
// options: dom操作相关的api
function createRenderer(options) {
return baseCreateRenderer(options);
}
function baseCreateRenderer(options, createHydrationFns) {
const patch = () => {
// 省略
};
const processText = () => {
// 省略
};
const render = (vnode, container, isSVG) => {
// 省略
}
// 此处省略很多代码
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
};
}
看到上面的代码,知道了ensureRenderer();
中就是调用createAppAPI(render, hydrate)
返回的对象createApp
函数,这时候就来到了createAppAPI(render, hydrate)
了。
let uid$1 = 0;
function createAppAPI(render, hydrate) {
return function createApp(rootComponent, rootProps = null) {
const context = createAppContext(); // 创建app上下文对象
let isMounted = false; // 挂载标识
const app = (context.app = {
_uid: uid$1++,
_component: rootComponent, // 传入的<App />组件
// 省略了一些代码
use(plugin, ...options) {}, // 使用插件相关,省略
mixin(mixin) {}, // mixin 相关,省略
component(name, component) {}, // 组件挂载, 省略
directive(name, directive) {}, // 指令挂载
mount(rootContainer, isHydrate, isSVG) {
if (!isMounted) {
// 创建vnode
const vnode = createVNode(rootComponent, rootProps);
vnode.appContext = context;
// 省略了一些代码
// 渲染vnode
render(vnode, rootContainer, isSVG);
isMounted = true;
app._container = rootContainer;
rootContainer.__vue_app__ = app;
return vnode.component.proxy;
}
},
// 省略了一些代码
});
return app;
};
}
上面的createApp函数里面返回一个app 对象,对象里面就有组件相关的属性,包括插件相关、 mixin 相关、组件挂载、指令挂载到context上,还有一个值得注意的就是mount函数,和最开始createApp(App).mount(root);
不一样,还记得上面的里面是经过重写了,重写里面就会调用这里mount
函数了,代码如下
const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args);
const { mount } = app; // 先缓存mount
app.mount = (containerOrSelector) => {
// 获取到容器节点
const container = normalizeContainer(containerOrSelector);
if (!container) return;
const component = app._component;
if (!isFunction(component) && !component.render && !component.template) {
// 传入的App组件,如果不是函数组件,组件没有render 组件没有template,就用容器的innerHTML
component.template = container.innerHTML;
}
// 清空容器的内容
container.innerHTML = '';
// 把组件挂载到容器上
const proxy = mount(container, false, container instanceof SVGElement);
return proxy;
};
return app;
});
上面的那个重写mount
函数,里面做了一些事情,
- 处理传入的容器并生成节点;
- 判断传入的组件是不是函数组件,组件里面有没有
render
函数、组件里面有没有template
属性,没有就用容器的innerHTML
作为组件的template
; - 清空容器内容;
- 运行缓存的
mount
函数,实现挂载组件;
上面就是createApp(App).mount(root);
的大致运行流程。但是到这里仅仅知道是怎么生成app
的,render
函数是怎么生成vnode
的?,vnode
又是怎么挂载到页面的?下面我们继续看,mount
函数里面都做了什么?
mount组件挂载流程
从上文中,最后会调用mount
去挂载组件到页面上。我们着重看看createApp
函数中mount
函数做了什么?
function createAppAPI(render, hydrate) {
return function createApp(rootComponent, rootProps = null) {
const context = createAppContext(); // 创建app上下文对象
let isMounted = false; // 挂载标识
const app = (context.app = {
// 省略
mount(rootContainer, isHydrate, isSVG) {
if (!isMounted) {
// 创建vnode
const vnode = createVNode(rootComponent, rootProps);
vnode.appContext = context;
// 省略了一些代码
// 渲染vnode
render(vnode, rootContainer, isSVG);
isMounted = true;
app._container = rootContainer;
rootContainer.__vue_app__ = app;
return vnode.component.proxy;
}
},
// 省略了一些代码
});
return app;
};
}
mount函数里面,主要就做了:
- 通过
const vnode = createVNode(rootComponent, rootProps);
创建vnode。 - 在
vnode
上挂载appContext
。 render(vnode, rootContainer, isSVG);
将vnode
、rootContainer
(容器节点)作为参数传入render
函数中去执行。- 设置挂载标识
isMounted = true;
。 - ...等等其他属性挂载。
到创建vnode
的过程,里面最终会调用_createVNode
函数,传入rootComponent
(就是我们编译后的object),然后生成一个vnode
对象。
const createVNodeWithArgsTransform = (...args) => {
return _createVNode(...(args));
};
function _createVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, isBlockNode = false) {
// 省略
// 将vnode类型信息编码
const shapeFlag = isString(type)
? 1 /* ELEMENT */
: isSuspense(type)
? 128 /* SUSPENSE */
: isTeleport(type)
? 64 /* TELEPORT */
: isObject(type)
? 4 /* STATEFUL_COMPONENT */
: isFunction(type)
? 2 /* FUNCTIONAL_COMPONENT */
: 0;
const vnode = {
__v_isVNode: true,
["__v_skip" /* SKIP */]: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children: null,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null
};
normalizeChildren(vnode, children);
// 省略
if (!isBlockNode && currentBlock && (patchFlag > 0 || shapeFlag & 6 /* COMPONENT */) && patchFlag !== 32) {
currentBlock.push(vnode);
}
return vnode;
}
const createVNode = createVNodeWithArgsTransform;
上面的过程,需要注意rootComponent
,就是我们上面编译后的App
,rootComponent
大致格式如下(不清楚可以回头看看呢。)
rootComponent = {
render() {},
setup() {}
}
创建vnode
的流程:
- 先是判断
shapeFlag
,这里type == rootComponent
是一个对象,就知道这时候shapeFlag = 4
- 创建一个
vnode
对象,其中type == rootComponent
- 然后
normalizeChildren(vnode, children)
,这里没有children
,跳过 - 返回
vnode
通过创建createVNode
就可以得到一个vnode
对象,然后就是拿这个vnode
去渲染render(vnode, rootContainer, isSVG);
const render = (vnode, container, isSVG) => {
// vnode是空的
if (vnode == null) {
if (container._vnode) {
// 卸载老vnode
unmount(container._vnode, null, null, true);
}
} else {
// container._vnode 一开始是没有的,所以n1 = null
patch(container._vnode || null, vnode, container, null, null, null, isSVG);
}
flushPostFlushCbs();
container._vnode = vnode; // 节点上挂载老的vnode
};
大家看到,render
函数的执行,首先会判断传入的vnode
是不是为null
,如果是null
并且容器节点挂载老的vnode
,就需要卸载老vnode
,因为新的vnode
已经没有了,如果不是null
,执行patch
函数。这个流程很简单。下面直接看patch
函数:
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = false) => {
// 省略
const { type, ref, shapeFlag } = n2;
switch (type) {
case Text:
processText(n1, n2, container, anchor);
break;
case Comment:
processCommentNode(n1, n2, container, anchor);
break;
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG);
} else {
patchStaticNode(n1, n2, container, isSVG);
}
break;
case Fragment:
processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
break;
default:
if (shapeFlag & 1 /* ELEMENT */) {
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
} else if (shapeFlag & 6 /* COMPONENT */) {
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
} else if (shapeFlag & 64 /* TELEPORT */) {
type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals);
} else if (shapeFlag & 128 /* SUSPENSE */) {
type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals);
} else {
warn('Invalid VNode type:', type, `(${typeof type})`);
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2);
}
};
patch
执行流程:
- 先看看我们的参数n1 = null(老的vnode), n2 = vnode (新的vnode),container = root,一开始是老的vnode的;
- 获取n2的type、shapeFlag,此时我们的
type = { render, setup }
,shapeFlag = 4 - 经过switch...case判断,我们会走到
else if (shapeFlag & 6 /* COMPONENT */)
这个分支,因为4 & 6 = 4
; - 交给
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
去处理我们的组件。
下面直接看看processComponent
这个函数:
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
n2.slotScopeIds = slotScopeIds;
if (n1 == null) {
// keep-alive组件处理
if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized);
} else {
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
} else {
updateComponent(n1, n2, optimized);
}
};
这个processComponent
函数的作用主要有两个
- 当n1 == null 的时候,去调用函数
mountComponent
去挂载组件 - 当n1不为null,就有有新老vnode的时候,去调用
updateComponent
去更新组件
我们这里说挂载流程,就直接看mountComponent
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 第一步
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense));
// 省略
// 第二步
setupComponent(instance);
// 省略
// 第三步
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
};
去掉一些无关的代码之后,我们看到mountComponent
其实很简单,里面创建组件instance
, 然后调用两个函数setupComponent
、setupRenderEffect
。
- setupComponent:主要作用是做一些组件的初始化工作什么的
- setupRenderEffect: 就相当于vue2的渲染watcher一样
1、createComponentInstance
但是我们先看看组件是怎么通过createComponentInstance
创建实例的?
function createAppContext() {
return {
app: null,
config: {}, // 省略
mixins: [],
components: {},
directives: {},
provides: Object.create(null)
};
}
const emptyAppContext = createAppContext();
let uid$2 = 0;
function createComponentInstance(vnode, parent, suspense) {
const type = vnode.type;
// inherit parent app context - or - if root, adopt from root vnode
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
const instance = {
uid: uid$2++,
vnode,
type,
parent,
appContext,
root: null,
subTree: null,
update: null,
render: null
withProxy: null,
components: null,
// props default value
propsDefaults: EMPTY_OBJ,
// state
ctx: EMPTY_OBJ,
data: EMPTY_OBJ,
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
// 省略
};
{
instance.ctx = createRenderContext(instance); // 生成一个对象 { $el, $data, $props, .... }
}
instance.root = parent ? parent.root : instance;
instance.emit = emit.bind(null, instance);
return instance;
}
大家不要把这么多属性看蒙了,其实createComponentInstance
就是初始化一个instance
对象,然后返回出去这个instance
,就这么简单。
instance.ctx = createRenderContext(instance); 这个对象里面有很多初始化属性,在通过createRenderContext把很多属性都挂载到instance.ctx上,里面都是我们常见的$el、$data、$props、$attr、$emit、...,这跟我们初次渲染没啥关系,先不要看好了。
2、setupComponent
下一步就是把生成的instance
的对象,放到setupComponent
函数作为参数去运行。
function setupComponent(instance) {
const { props, children } = instance.vnode;
const isStateful = isStatefulComponent(instance); // instance.vnode.shapeFlag & 4
// 省略
const setupResult = isStateful
? setupStatefulComponent(instance)
: undefined;
return setupResult;
}
看setupComponent
做的功能就是,判断我们的vnode.shapeFlag
是不是状态组件,从上面得知,我们的vnode.shapeFlag == 4
,所以下一步就会去调用setupStatefulComponent(instance)
,然后返回值setupResult,最后 返回出去。在看看setupStatefulComponent
function setupStatefulComponent(instance, isSSR) {
const Component = instance.type;
// 省略
const { setup } = Component;
if (setup) {
// 省略
// 调用setup
const setupResult = callWithErrorHandling(setup, instance, 0, [shallowReadonly(instance.props), setupContext]);
// 省略
handleSetupResult(instance, setupResult, isSSR);
} else {
finishComponentSetup(instance, isSSR);
}
}
function callWithErrorHandling(fn, instance, type, args) {
let res;
try {
res = args ? fn(...args) : fn(); // 调用传进来的setup函数
} catch (err) {
handleError(err, instance, type);
}
return res;
}
function handleSetupResult(instance, setupResult, isSSR) {
// 省略
instance.setupState = proxyRefs(setupResult); // 相当于代理挂载操作 instance.setupState = setupResult
{
exposeSetupStateOnRenderContext(instance); // 相当于代理挂载操作 instance.ctx = setupResult
}
finishComponentSetup(instance, isSSR);
}
setupStatefulComponent
函数就是调用我们组件自定义的setup函数,返回一个setupResult对象,根据上面写的,setup返回的就是一个对象:
setupResult = {
name: 'world'
}
然后在运行handleSetupResult
,看到里面其实没做什么工作,就是调用finishComponentSetup(instance, isSSR);
function finishComponentSetup(instance, isSSR) {
const Component = instance.type;
if (!instance.render) {
instance.render = (Component.render || NOOP);
}
// support for 2.x options
{
currentInstance = instance;
pauseTracking();
applyOptions(instance, Component);
resetTracking();
currentInstance = null;
}
}
至此所有的setupComponent
流程都完成了,就是调用setup
函数,然后往instance
里面挂载很多属性代理。包括后面重要的instance.ctx
, 都代理了setupResult
。下面我们看第三步:
3、setupRenderEffect
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// create reactive effect for rendering
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
let vnodeHook;
const { el, props } = initialVNode;
const subTree = (instance.subTree = renderComponentRoot(instance));
// 省略
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
initialVNode.el = subTree.el;
// 省略
instance.isMounted = true;
// 生路
} else {
// 更新过程
}
}, createDevEffectOptions(instance) );
};
setupRenderEffect中的effect里面的函数会执行一次。然后就到里面的函数了。
- 先通过
instance.isMounted
判断是否已经挂载了。没有挂载过的就去执行挂载操作,挂载过的就执行更新操作 - 通过
renderComponentRoot
函数生成subTree - 调用path进行递归挂载
- 更新instance.isMounted标识
生成subTree
renderComponentRoot
是生成subTree的,其实里面就是执行我们的App
组件的render
函数。
function renderComponentRoot(instance) {
const { type: Component, vnode, proxy, withProxy, props, propsOptions: [propsOptions], slots, attrs, emit, render, renderCache, data, setupState, ctx } = instance;
let result;
try {
let fallthroughAttrs;
if (vnode.shapeFlag & 4) {
// 拿到instance.proxy
const proxyToUse = withProxy || proxy;
// 调用install.render函数,并改变this的指向
result = normalizeVNode(render.call(proxyToUse, proxyToUse, renderCache, props, setupState, data, ctx));
} else {
// 函数组件挂载
}
// 省略
} catch (err) {
blockStack.length = 0;
handleError(err, instance, 1 /* RENDER_FUNCTION */);
result = createVNode(Comment);
}
setCurrentRenderingInstance(prev);
return result;
}
renderComponentRoot
的主要功能就是调用install.render
函数,并改变this
的指向到instance.proxy
。从上面我们知道,
- instance.proxy有数据代理,就是访问instance.proxy.name === 'world'
result = normalizeVNode(render.call(proxyToUse, proxyToUse, renderCache, props, setupState, data, ctx));
这里的render
函数就是我们app编译过的redner函数,const _hoisted_1 = /*#__PURE__*/createTextVNode("Hello "); // 静态代码提升 const _hoisted_2 = { class: "blue" }; // 静态代码提升 const render = /*#__PURE__*/_withId((_ctx, _cache, $props, $setup, $data, $options) => { return (openBlock(), createBlock("h1", null, [ _hoisted_1, createVNode("span", _hoisted_2, toDisplayString(_ctx.name), 1 /* TEXT */) ])) });
然后执行render函数,会把instance的ctx、props、setupState等等参数传进去。我们看看render函数执行就是先执行一个openBlock()
const blockStack = []; // block栈
let currentBlock = null; // 当前的block
function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []));
}
function closeBlock() {
blockStack.pop();
currentBlock = blockStack[blockStack.length - 1] || null;
}
从上面可以看出,执行一个openBlock()
,就是新建一个数据,赋值到currentBlock
,然后push
到blockStack
,所以当前的
blockStack = [[]]
currentBlock = []
然后回执先执行createVNode("span", _hoisted_2, toDisplayString(_ctx.name), 1)
,这就是创建span节点的vnode,toDisplayString(_ctx.name)
就是取ctx.name的值,等价于toDisplayString(_ctx.name) === 'world'
,createVNode上面会有讲过,通过createVNode创建出来的对象大概就是
// 详细代码看前文有写
function createVNode () {
const vnode = {
appContext: null,
children: "world",
props: {class: "blue"},
type: "span",
// 省略很多
}
// 这里有判断,如果是动态block就push
// currentBlock.push(vnode);
}
// 执行过后
// currentBlock = [span-vnode]
// blockStack = [[span-vnode]]
然后就是执行 createBlock("h1", null, [_hoisted_1, span-vnode])
function createBlock(type, props, children, patchFlag, dynamicProps) {
const vnode = createVNode(type, props, children, patchFlag, dynamicProps, true);
// 保存动态的block,下次更新只更新这个,
vnode.dynamicChildren = currentBlock || EMPTY_ARR;
// 清空当前的block
closeBlock();
// 如果currentBlock还有的话就继续push到currentBlock
if (currentBlock) {
currentBlock.push(vnode);
}
return vnode;
}
createBlock也是调用createVNode,这样子就生成一个h1的vnode了,然后执行vnode.dynamicChildren = currentBlock,在清空block并返回vnode。如下大致如下
vnode = {
"type": "h1",
"children": [
{
"children": "Hello ",
"shapeFlag": 8,
"patchFlag": 0,
},
{
"type": "span",
"props": {
"class": "blue"
},
"children": "world",
"shapeFlag": 9,
"patchFlag": 1,
}
],
"shapeFlag": 17,
"patchFlag": 0,
"dynamicChildren": [
{
"type": "span",
"props": {
"class": "blue"
},
"children": "world",
"shapeFlag": 9,
"patchFlag": 1,
}
],
"appContext": null
}
patch subTree
然后调用patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
,又到patch,这会type=== 'h1',会调用patch里面的。
const { type, ref, shapeFlag } = n2;
if (shapeFlag & 1 /* ELEMENT */) {
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
// ---
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
isSVG = isSVG || n2.type === 'svg';
if (n1 == null) {
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
} else {
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
};
调用processElement会调用mountElement去挂载
const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
let el;
let vnodeHook;
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode;
{
// 创建h1元素
el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is, props);
// chilren是文本的
if (shapeFlag & 8) {
hostSetElementText(el, vnode.children);
} else if (shapeFlag & 16) { // chilren是数组的
// 递归挂载children
mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', slotScopeIds, optimized || !!vnode.dynamicChildren);
}
// 省略
// 把创建的h1挂载到root上
hostInsert(el, container, anchor);
};
mountElemen
t挂载流程,显示创建元素,然后判断子元素是数组还是文本,如果孩子是文本就直接创建文本,插入到元素中,如果是数组,就调用mountChildren函数,
const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, optimized, slotScopeIds, start = 0) => {
for (let i = start; i < children.length; i++) {
patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, optimized, slotScopeIds);
}
};
mountChildren
函数里面就循环所有的孩子,然后一个个调用patch
去插入。
最后插入递归path
调用完成之后生成的树节点el,会调用hostInsert(el, container, anchor);
插入到root中。然后渲染到页面中去。
vue3组件挂载流程
写的不好的地方欢迎批评指正
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。