关于前端:vue3初始化挂载组件流程

23次阅读

共计 18767 个字符,预计需要花费 47 分钟才能阅读完成。

本文次要依据 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 函数外面,次要就做了:

  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 上,外面都是咱们常见的 $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 外面的函数会执行一次。而后就到外面的函数了。

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

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 组件挂载流程

写的不好的中央欢送批评指正

博客链接

正文完
 0