vue3初始化掛載組件流程

本文主要根據vue3源碼去理解清楚vue3的組件掛載流程(最後附流程圖),根據我的閱讀源碼去解釋,vue的組件是怎麼從.vue單文件組件一步步插入到真實DOM中,並渲染到頁面上。css

例子說明

那下面的簡單代碼去作個例子說明,先看看vue3的寫法。html

  • App.vuevue

    <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.jsnode

    import { createApp } from "vue";
    import App from "./App.vue";
    const root = document.getElementById('app');
    console.log(App);
    createApp(App).mount(root);
  • index.htmlreact

    <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上。到這裏就會有些疑問❓api

  • 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代碼,編譯後代碼以下:app

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同樣

一、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、...,這跟咱們初次渲染沒啥關係,先不要看好了。

二、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。下面咱們看第三步:

三、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組件掛載流程

vue3掛載流程

寫的很差的地方歡迎批評指正

博客連接

相關文章
相關標籤/搜索