vue3初始化挂载组件流程

1,902 阅读10分钟

本文主要根据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中的内容渲染到页面上,我们看看渲染结果,如下图:

image-20210524192812883

看上面的例子:我们可以知道,通过vue中的createApp(App)方法传入App组件,然后调用mount(root)方法去挂载到root上。到这里就会有些疑问❓

  • App组件经过vue编译后是什么样子的?
  • createApp(App)这个函数里面经过怎么样的处理然后返回mount方法?
  • mount方法是怎么把App组件挂载到root上的?
  • ...

先看看第一个问题,我们上面代码有打印console.log(App),具体看看App经过编译后是得到如下的一个对象:

image-20210524193854779

其中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中,其大致流程图如下:

image-20210525102552034

第一部分的.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函数里面,主要就做了:

  1. 通过const vnode = createVNode(rootComponent, rootProps);创建vnode。
  2. vnode上挂载appContext
  3. render(vnode, rootContainer, isSVG);vnoderootContainer(容器节点)作为参数传入render函数中去执行。
  4. 设置挂载标识isMounted = true;
  5. ...等等其他属性挂载。

到创建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,就是我们上面编译后的ApprootComponent大致格式如下(不清楚可以回头看看呢。)

rootComponent = {
  render() {},
  setup() {}
}

创建vnode的流程:

  1. 先是判断shapeFlag,这里type == rootComponent 是一个对象,就知道这时候shapeFlag = 4
  2. 创建一个vnode对象,其中type == rootComponent
  3. 然后normalizeChildren(vnode, children),这里没有children,跳过
  4. 返回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执行流程:

  1. 先看看我们的参数n1 = null(老的vnode), n2 = vnode (新的vnode),container = root,一开始是老的vnode的;
  2. 获取n2的type、shapeFlag,此时我们的type = { render, setup },shapeFlag = 4
  3. 经过switch...case判断,我们会走到else if (shapeFlag & 6 /* COMPONENT */)这个分支,因为4 & 6 = 4;
  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函数的作用主要有两个

  1. 当n1 == null 的时候,去调用函数mountComponent去挂载组件
  2. 当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, 然后调用两个函数setupComponentsetupRenderEffect

  • 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上,里面都是我们常见的elel、data、propsprops、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里面的函数会执行一次。然后就到里面的函数了。

  1. 先通过instance.isMounted判断是否已经挂载了。没有挂载过的就去执行挂载操作,挂载过的就执行更新操作
  2. 通过renderComponentRoot函数生成subTree
  3. 调用path进行递归挂载
  4. 更新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,然后pushblockStack,所以当前的

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);
};

mountElement挂载流程,显示创建元素,然后判断子元素是数组还是文本,如果孩子是文本就直接创建文本,插入到元素中,如果是数组,就调用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组件挂载流程图

createApp(App).mount('root').png

写的不好的地方欢迎批评指正

博客链接