likes
comments
collection
share

Vue3源码-整体渲染流程浅析

作者站长头像
站长
· 阅读数 12

本文基于Vue 3.2.30版本源码进行分析

为了增加可读性,会对源码进行删减、调整顺序、改变部分分支条件的操作,文中所有源码均可视作为伪代码

由于ts版本代码携带参数过多,不利于展示,大部分伪代码会取编译后的js代码,而不是原来的ts代码

文章内容

本文仅针对有状态组件、同步逻辑进行分析,不分析异步相关逻辑和函数组件等内容,相关源码展示也会剔除异步相关逻辑和函数组件等内容

  • 流程图展示Vue3首次渲染整体流程
  • 以流程图为基础,进行首次渲染整体流程的源码分析
  • 流程图展示Vue3组件更新整体流程
  • 以流程图为基础,进行组件更新整体流程的源码分析

由于本文篇幅较长,按照流程图看源码分析效果更佳

创建vnode

render()函数

  • 使用createBaseVNode进行普通元素节点的vnode创建
  • 使用createVNode进行Component组件节点的vnode创建

<template></template>内容最终会被编译成为render()函数,render()函数中执行createBaseVNode(_createElementVNode)/createVNode(_createVNode)返回渲染结果,在下面流程分析中

  1. 在一开始的app.mount会触发App.vuecreateVNode()函数获取vnode数据,进行patch()
  2. 在处理组件内部数据时,会触发renderComponentRoot()中调用render()函数(从而进行createBaseVNode/createVNode)获取组件内部的vnode数据,然后进行patch()
function render(_ctx, _cache) {
  with (_ctx) {
    const {createElementVNode: _createElementVNode, resolveComponent: _resolveComponent, createVNode: _createVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock} = _Vue;
    const _component_InnerComponent = _resolveComponent("InnerComponent");
    const _component_second_component = _resolveComponent("second-component");
    return (
      _openBlock(),
      _createElementBlock("div", _hoisted_1, [
        _createElementVNode("div", _hoisted_2, [_hoisted_3, _hoisted_4, _hoisted_5, _createVNode(_component_InnerComponent)]),
        _createVNode(
          _component_second_component,
          {
            "stat-fat": {label: "3333"},
            test: "333",
          },
          null,
          8 /* PROPS */,
          ["stat-fat"]
        ),
      ])
    );
  }
}

首次渲染整体流程图

Vue3源码-整体渲染流程浅析

首次渲染整体流程-源码概要分析

const createApp = ((...args) => {
    const { createApp } = ensureRenderer(); // 第1部分
    const app = createApp(...args); // 第2部分
    //...
    const { mount } = app;
    app.mount = (containerOrSelector) => { // 第3部分
        //...
        const proxy = mount(container, false, container instanceof SVGElement);//第4部分
        //...
        return proxy;
    };
    return app;
});

第1部分: ensureRenderer() 创建渲染器,获取createApp

由下面的代码块可以知道,会将render()函数传入createAppAPI(render)进行createApp()方法的创建

function ensureRenderer() {
    return (renderer || (renderer = createRenderer(rendererOptions)));
}
function createRenderer(options) {
    return baseCreateRenderer(options);
}
function baseCreateRenderer(options, createHydrationFns) {
    //...初始化很多方法
    const render = (vnode, container, isSVG) => { };
    return {
        render,
        hydrate,
        createApp: createAppAPI(render, hydrate)
    };
}

第2部分: 使用createApp()创建app对象

从下面的代码块可以知道,本质是将baseCreateRenderer()产生的render函数传入作为app.mount()中执行的一环,然后返回创建的app对象

function createAppAPI(render, hydrate) {
    return function createApp(rootComponent, rootProps = null) {
        const context = createAppContext();
        // ...
        let isMounted = false;
        const app = (context.app = {
            _uid: uid++,
            _component: rootComponent,
            _props: rootProps,
            _context: context,
            // ...省略
            mount(rootContainer, isHydrate, isSVG) {
              // ...省略
              vnode.appContext = context;
              render(vnode, rootContainer, isSVG);
              // ...省略
            }
            //...省略多种方法
        });
        return app;
    }
}

其中createAppContext()返回的一个初始化的对象,从下面的代码块可以看到全局变量app.config.globalProperties

export function createAppContext(): AppContext {
  return {
    app: null as any,
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      errorHandler: undefined,
      warnHandler: undefined,
      compilerOptions: {}
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null),
    optionsCache: new WeakMap(),
    propsCache: new WeakMap(),
    emitsCache: new WeakMap()
  }
}

第3部分: 重写app.mount,暴露出来该方法,在外部显示调用

由于createApp重名两次,为了避免认错,将初始化示例代码再写一次

根据初始化示例代码,在createApp()中创建了app对象,并且进行了app.mount()方法的书写,从下面示例代码可以知道,最终会调用app.mount("#app")

import App from "./App.vue";
const app = createApp(App);
app.mount("#app");

const createApp = ((...args) => {
    const { createApp } = ensureRenderer(); // 第1部分
    const app = createApp(...args); // 第2部分
    //...
    const { mount } = app;
    app.mount = (containerOrSelector) => { // 第3部分
        //...
        const proxy = mount(container, false, container instanceof SVGElement);//第4部分
        //...
        return proxy;
    };
    return app;
});

重写app.mount("#app")详细源码分析

  • containerOrSelector可以为字符串/DOM,使用normalizeContainer()进行整理
  • 判断如果没有render函数template内容,则使用innerHTML作为组件模板内容
  • 挂载前清空innerHTML内容
  • 执行原始的mount()方法,返回执行后的结果
app.mount = (containerOrSelector) => { // 第3部分
    const container = normalizeContainer(containerOrSelector);
    if (!container)
        return;
    const component = app._component;
    if (!isFunction(component) && !component.render && !component.template) {
        //在 DOM 模板中可能会执行 JS 表达式
        component.template = container.innerHTML;
    }
    container.innerHTML = '';
    const proxy = mount(container, false, container instanceof SVGElement);//第4部分
    //...
    return proxy;
};

function normalizeContainer(container) {
    if (isString(container)) {
        const res = document.querySelector(container);
        return res;
    }
    return container;
}

第4部分: 触发原始的app.mount(dom)执行(重写app.mount执行过程中)

整体源码

// 重写app.mount: const createApp = ((...args) => {})里面逻辑
const { mount } = app; 
app.mount = (containerOrSelector) => { // 第3部分
    const proxy = mount(container, false, container instanceof SVGElement);//第4部分
    return proxy;
};
// 第4部分: 原始的app.mount:
function createAppAPI(render, hydrate) {
    return function createApp(rootComponent, rootProps = null) {
        const context = createAppContext();
        const app = (context.app = {
            mount(rootContainer, isHydrate, isSVG) {
                if (!isMounted) {
                    const vnode = createVNode(rootComponent, rootProps);
                    vnode.appContext = context;
                    render(vnode, rootContainer, isSVG);
                    isMounted = true;
                    app._container = rootContainer;
                    rootContainer.__vue_app__ = app;
                    return getExposeProxy(vnode.component) || vnode.component.proxy;
                }
            }
        });
        return app
    }
}

原始的app.mount的核心代码:render()(非<template>转化后的render

从上面整体代码可以知道,主要还是触发了render()方法,这里的render()方法是createApp()时创建的app时创建的内部方法!!!!跟一开始创建vnoderender()方法是不同的方法!!!!!

  • 如果要渲染的vnode节点为空,则触发销毁逻辑
  • 如果要渲染的vnode节点不为空,则进行创建/更新逻辑:patch()
  • 然后触发Post优先级的队列执行
  • 最后将vnode赋值给container._vnode
render(vnode, rootContainer, isSVG);

// function baseCreateRenderer(options, createHydrationFns) {}
function baseCreateRenderer(options, createHydrationFns) {
    const render = (vnode, container, isSVG) => {
        if (vnode == null) {
            if (container._vnode) {
                unmount(container._vnode, null, null, true);
            }
        }
        else {
            // 也是定义在baseCreateRenderer()中
            patch(container._vnode || null, vnode, container, null, null, null, isSVG);
        }
        flushPostFlushCbs();
        container._vnode = vnode;
    };
}

原始的app.mount的返回值:getExposeProxy(vnode.component)

处理instance.exposed的一些情况,进行ref解码/处理key=$的情况,进行代理返回

function getExposeProxy(instance) {
    // ...改写
    if (!instance.exposed) {
        return;
    }
    if (instance.exposeProxy) {
        return instance.exposeProxy;
    } else {
        instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
            get(target, key) {
                if (key in target) {
                    return target[key];
                }
                else if (key in publicPropertiesMap) {
                    return publicPropertiesMap[key](instance);
                }
            }
        });
    }
}

function markRaw(value) {
    def(value, "__v_skip" /* SKIP */, true);
    return value;
}
function proxyRefs(objectWithRefs) {
    // shallowUnwrapHandlers进行Proxy的拦截,然后进行ref的解码,没有其它逻辑
    return isReactive(objectWithRefs)
        ? objectWithRefs
        : new Proxy(objectWithRefs, shallowUnwrapHandlers);
}
const publicPropertiesMap = extend(Object.create(null), {
    $: i => i,
    $el: i => i.vnode.el,
    $data: i => i.data,
    $props: i => (shallowReadonly(i.props)),
    $attrs: i => (shallowReadonly(i.attrs)),
    $slots: i => (shallowReadonly(i.slots)),
    $refs: i => (shallowReadonly(i.refs)),
    $parent: i => getPublicInstance(i.parent),
    $root: i => getPublicInstance(i.root),
    $emit: i => i.emit,
    $options: i => (resolveMergedOptions(i)),
    $forceUpdate: i => () => queueJob(i.update),
    $nextTick: i => nextTick.bind(i.proxy),
    $watch: i => (instanceWatch.bind(i))
});

首次渲染-App.vue组件的patch()流程分析

初始化createApp后的app.mount("#el")中的render()会触发patch()方法,进行vnode的渲染,当前也有很多地方也会触发patch()方法,该方法主要执行的流程为:

  • 如果新旧节点相同,则直接返回
  • 如果新旧节点不是同种类型,则直接销毁旧节点,并且将n1置为null,保证后面执行逻辑正确
  • 根据新vnode节点的类型,进行不同方法的调用,如文本调用processText(),如组件调用processComponent()
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = isHmrUpdating ? false : !!n2.dynamicChildren) => {
    if (n1 === n2) {
        return;
    }
    // patching & not same type, unmount old tree
    if (n1 && !isSameVNodeType(n1, n2)) {
        anchor = getNextHostNode(n1);
        unmount(n1, parentComponent, parentSuspense, true);
        n1 = null;
    }
    if (n2.patchFlag === -2 /* BAIL */) {
        optimized = false;
        n2.dynamicChildren = null;
    }
    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: // 处理Frgment元素
            processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
            break;
        default:
            if (shapeFlag & 1 /* ELEMENT */) {//处理普通DOM元素
                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 */) {//处理<teleport>
                type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals);
            }
            else if (shapeFlag & 128 /* SUSPENSE */) {//处理suspense
                type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals);
            }
            else {
                warn$1('Invalid VNode type:', type, `(${typeof type})`);
            }
    }
    // set ref
    if (ref != null && parentComponent) {
        setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2);
    }
};

初始化触发组件处理processComponent()

由于是初始化createApp,因此此时n1=null,触发mountComponent()逻辑

updateComponent()是数据变化后的组件更新逻辑,后面再分析

const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
    n2.slotScopeIds = slotScopeIds;
    if (n1 == null) {
        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);
    }
};

mountComponent()核心代码概述

mountComponent函数流程主要为:

  • 创建组件实例
  • 设置组件实例
  • 设置并运行带有effect的渲染函数
const mountComponent = (...args) => {
    // 创建组件实例
    initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense)
    const instance = initialVNode.component;
    //设置组件实例
    setupComponent(instance);
    //设置并运行带有effect的渲染函数
    setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
}

首次渲染-mountComponent()核心代码详细分析

createComponentInstance创建组件实例

初始化组件实例,逻辑非常简单,只是构建一个ComponentInternalInstance对象,包括非常多的属性

function createComponentInstance(vnode, parent, suspense) {
    const type = vnode.type;
    //...省略
    const instance: ComponentInternalInstance = {
        //...省略非常多的配置参数 
        vnode, // Vnode representing this component in its parent's vdom tree
        type,
        parent, // ComponentInternalInstance
        root // ComponentInternalInstance
    }
    instance.root = parent ? parent.root : instance
    instance.emit = emit.bind(null, instance)
    //...省略
    return instance
}

setupComponent设置组件实例

整体概述

  • 判断是否是有状态组件:const isStateful = isStatefulComponent(instance)
  • 初始化props,构建props和attrs属性
  • 初始化slots
  • 执行setupStatefulComponent()
function setupComponent(instance, isSSR = false) {
    const { props, children } = instance.vnode;
    // isStatefulComponent = instance.vnode.shapeFlag & 4
    const isStateful = isStatefulComponent(instance);
    initProps(instance, props, isStateful, isSSR);
    initSlots(instance, children);
    const setupResult = isStateful
        ? setupStatefulComponent(instance, isSSR)
        : undefined;
    return setupResult;
}

核心逻辑setupStatefulComponent

整体概述

初始化propsslots后调用setupStatefulComponent(),主要执行流程:

  • 创建渲染代理属性访问缓存
  • 创建渲染上下文代理,并且给它Object.defineProperty一个__v_skip属性,__v_skip可以阻止响应式追踪,监测到__v_skip立刻返回target
  • 处理setup()函数
    • 如果存在setup(),则执行setup函数,获取执行结果setupResult,根据返回结果的类型,返回方法(渲染函数)/返回对象(响应式数据/方法),构建instance.render方法/instance.setupState对象,然后再调用finishComponentSetup()方法处理
    • 如果不存在setup(),直接调用finishComponentSetup()方法处理
    • finishComponentSetup()方法是为了合并Options APIComposition API的生命周期以及各种injectmethodsdatacomputed等属性
function setupStatefulComponent(instance, isSSR) {
    const Component = instance.type;
    //...省略一些简单的校验逻辑
    // 0. 创建渲染代理属性访问缓存
    instance.accessCache = Object.create(null);
    // 1. 创建public实例 / 渲染代理,并且给它Object.defineProperty一个__v_skip属性
    // def(value, "__v_skip" /* SKIP */, true);
    instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
    const { setup } = Component;
    if (setup) {
    // 2. 处理setup()函数
      // ...进行一系列处理
    }
    else {
    // 3. 没有setup函数时,直接调用finishComponentSetup
        finishComponentSetup(instance, isSSR);
    }
}

第1步:instance.proxy创建渲染上下文

const PublicInstanceProxyHandlers = {
    get({ _: instance }, key) {},
    set({ _: instance }, key, value) {},
    has({ _: { data, setupState, accessCache, ctx, appContext, propsOptions } }, key) {}
};

由上面整体概述的代码可以知道,主要是将instance.ctx进行了new Proxy()的拦截,由于在new Proxy()后进行了__v_skip的赋值,因此这里只是单纯拦截了key=getkey=setkey=has方法,没有做任何的响应式处理,也阻止任何的响应式处理

get方法拦截(instance.proxy)

instance中获取多种数据,包括ctxsetupStatedatapropsaccessCachetypeappContext

  • ctx:用户自定义的数据,包含了所有可以在<template>中访问的数据,如果是Ref/Rective数据,拿到的是toRaw的数据
    • 包含Options API中的所有this.xxx数据,比如datamethods产生的this.xxx/this.xxx()
    • 包含Composition APIsetup(){return {xxx}}进行return的变量(原始Raw数据)
  • setupState:拿到在setup()中的数据,比如具备响应式Ref数据
  • dataOptions APIdata
  • propsOptions APIprops
  • accessCache:访问过的数据的缓存,对渲染上下文的每个属性访问都会调用此getter,这其中最昂贵的部分,是多个hasOwn()调用。访问普通对象,使用accessCache(with null prototype)缓存对象要快得多,每次访问过数据,就进行accessCode[key]=xxx的缓存
  • typevnode类型,有FragmentTextCommentStatic以及组件类型等等,可以获取到CSS Module数据
  • appContextcreateAppapp上下文,具有appContext.config.globalProperties等全局方法
get({ _: instance }, key) {
    const { ctx, setupState, data, props, accessCache, type, appContext } = instance;
}

accessCache中查找是否有符合条件的key,如果有,则拿出对应的数据类型,然后直接从对应的数据类型中获取,省去hasOwn()比较

if (key[0] !== '$') {
    const n = accessCache[key];
    if (n !== undefined) {
        switch (n) {
            case 1 /* SETUP */:
                return setupState[key];
            case 2 /* DATA */:
                return data[key];
            case 4 /* CONTEXT */:
                return ctx[key];
            case 3 /* PROPS */:
                return props[key];
            // default: just fallthrough
        }
    }
}

如果没有accessCache,则按照顺序:setupState>data>props>ctx查找是否有符合条件的key,并且跟新accessCache

if (key[0] !== '$') {
    const n = accessCache[key];
    if (n !== undefined) {
        // ...accessCode[key]存储对应的类型
    } else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
        accessCache[key] = 1 /* SETUP */;
        return setupState[key];
    } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
        accessCache[key] = 2 /* DATA */;
        return data[key];
    } else if (
        (normalizedProps = instance.propsOptions[0]) &&
        hasOwn(normalizedProps, key)) {
        accessCache[key] = 3 /* PROPS */;
        return props[key];
    } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
        accessCache[key] = 4 /* CONTEXT */;
        return ctx[key];
    }
    else if (shouldCacheAccess) {
        accessCache[key] = 0 /* OTHER */;
    }
}

如果setupState>data>props>ctx都找不到对应的key,则accessCache[key] = 0 /* OTHER */;此时key有两种情况,一种是key是以$作为开头的字符串,一种是确实不存在于setupState>data>props>ctx中这个时候会在publicPropertiesMap中寻找是否有符合条件的key

// const publicPropertiesMap = extend(Object.create(null), {
//     $: i => i,
//     $el: i => i.vnode.el,
//     $data: i => i.data,
//     $props: i => (shallowReadonly(i.props)),
//     $attrs: i => (shallowReadonly(i.attrs)),
//     $slots: i => (shallowReadonly(i.slots)),
//     $refs: i => (shallowReadonly(i.refs)),
//     $parent: i => getPublicInstance(i.parent),
//     $root: i => getPublicInstance(i.root),
//     $emit: i => i.emit,
//     $options: i => (resolveMergedOptions(i)),
//     $forceUpdate: i => () => queueJob(i.update),
//     $nextTick: i => nextTick.bind(i.proxy),
//     $watch: i => (instanceWatch.bind(i))
// });
const publicGetter = publicPropertiesMap[key];
if (publicGetter) {
    if (key === '$attrs') {
        track(instance, "get" /* GET */, key);
        markAttrsAccessed();
    }
    return publicGetter(instance);
}

如果publicPropertiesMap中没有符合条件的key,会继续向下寻找CSS Module

const publicGetter = publicPropertiesMap[key];
if (publicGetter) {
    // ...
}
else if ((cssModule = type.__cssModules) && (cssModule = cssModule[key])) {
    return cssModule;
}

如果CSS Module没有符合条件的key,会继续向下寻找ctx用户自定义的数据(上面的ctx寻找是针对非$开头的key,这里包括了$开头的key

const publicGetter = publicPropertiesMap[key];
if (publicGetter) {
    // ...
} else if ((cssModule = type.__cssModules) && (cssModule = cssModule[key])) {
    //...
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
    // user may set custom properties to `this` that start with `$`
    accessCache[key] = 4 /* CONTEXT */;
    return ctx[key];
}

如果ctx没有符合条件的key,继续往全局属性中查找

const publicGetter = publicPropertiesMap[key];
if (publicGetter) {
    // ...
} else if ((cssModule = type.__cssModules) && (cssModule = cssModule[key])) {
    //...
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
   // ...
} else if (((globalProperties = appContext.config.globalProperties),
    hasOwn(globalProperties, key)) {
    return globalProperties[key];
}
set方法拦截(instance.proxy)

get方法一样,按照setupState>data>props,判断是否存在该key,然后进行值的更新如果setupState>data>props,则直接进行ctx[key] = value的赋值

ctx[key]=value的赋值类似于在mounted(){this.test=222}=>那么会存储ctx[test]=222,这样的数据只能共享于组件自定义数据ctx props是readonly数据,无法set更改,直接阻止 Vue内部以$开头的属性也是不可以更改,直接阻止

set({ _: instance }, key, value) {
    const { data, setupState, ctx } = instance;
    if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
        setupState[key] = value;
    } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
        data[key] = value;
    } else if (hasOwn(instance.props, key)) {
        return false;
    }
    if (key[0] === '$' && key.slice(1) in instance) {
        return false;
    } else {
        ctx[key] = value;
    }
    return true;
}
has方法拦截(instance.proxy)

按照accessCache>data>setupState>props>ctx>publicPropertiesMap>globalProperties进行hasOwn的判断

has({ _: { data, setupState, accessCache, ctx, appContext, propsOptions } }, key) {
    let normalizedProps;
    return (!!accessCache[key] ||
        (data !== EMPTY_OBJ && hasOwn(data, key)) ||
        (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) ||
        ((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
        hasOwn(ctx, key) ||
        hasOwn(publicPropertiesMap, key) ||
        hasOwn(appContext.config.globalProperties, key));
}

第2步:setup存在时的处理

整体概述
const { setup } = Component;
if (setup) {
    const setupContext = (instance.setupContext =
        setup.length > 1 ? createSetupContext(instance) : null);
    setCurrentInstance(instance);
    pauseTracking();
    const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [shallowReadonly(instance.props), setupContext]);
    resetTracking();
    unsetCurrentInstance();
    if (isPromise(setupResult)) {
        setupResult.then(unsetCurrentInstance, unsetCurrentInstance);
        instance.asyncDep = setupResult;
    } else {
        handleSetupResult(instance, setupResult, isSSR);
    }
} else {
    //...
}
createSetupContext创建setup上下文

只有当setup.length>1,才会触发createSetupContext(instance),那是因为setupContext代表着setup的第二个参数,如下所示,setupContext=context,即setupContext={attrs, slots, emit, expose}

 const App = {
    setup(props, context) {
      // context就是setupContext
      // 此时setup.length=2
    }
 }

 const App1 = {
    setup(props) {
      // context就是setupContext
      // 此时setup.length=1
    }
 }

从下面createSetupContext代码块可以知道,本质还是拿instance.xxx的相关属性进行{attrs, slots, emit, expose}的拼凑

function createSetupContext(instance) {
    const expose = exposed => {
        instance.exposed = exposed || {};
    };
    let attrs;
    return {
        get attrs() {
            return attrs || (attrs = createAttrsProxy(instance))
        },
        slots: instance.slots,
        emit: instance.emit,
        expose
    }
}
执行setup函数,获取执行结果setupResult
  • 设置当前活跃的instance为当前的实例
  • shouldTrack=false,阻止响应式的依赖收集
  • callWithErrorHandling:使用统一的try{}catch{}执行setup()函数,并且传递shallowReadonly(instance.props)和上面初始化的setupContext={attrs, slots, emit, expose}
  • 回滚shouldTrack的状态
  • 回滚currentInstance的状态,置为null
// currentInstance = instance 设置当前活跃的instance
setCurrentInstance(instance);
// shouldTrack=false,阻止响应式的依赖收集
pauseTracking();
// res = args ? fn(...args) : fn() 执行setup()函数,传递对应的props和context
const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [shallowReadonly(instance.props), setupContext]);
// 恢复shouldTrack状态
resetTracking();
// 将当前活跃的instance置为null
unsetCurrentInstance();
处理setupResult-handleSetupResult()

从下面的代码块可以知道,最终调用的是handleSetupResult()方法

if (isPromise(setupResult)) {
    // setup返回的是一个Promise的情况,较为少见,暂时不分析这种情况
    setupResult.then(unsetCurrentInstance, unsetCurrentInstance);
    instance.asyncDep = setupResult;
} else {
    handleSetupResult(instance, setupResult, isSSR);
}
function handleSetupResult(instance, setupResult, isSSR) {
    if (isFunction(setupResult)) {
        instance.render = setupResult;
    } else if (isObject(setupResult)) {
        instance.setupState = proxyRefs(setupResult);
    }
    finishComponentSetup(instance, isSSR);
}

从上面代码可以知道,setup()执行完毕后可能返回一个function(),如下所示,setup 返回一个渲染函数,此时在渲染函数中可以直接使用在同一作用域下声明的响应式状态,handleSetupResult()中直接将返回的渲染函数赋值给instance.render

返回一个渲染函数将会阻止我们返回其他东西,我们想通过模板引用将这个组件的方法暴露给父组件可以通过调用 expose() 解决这个问题

import { h, ref } from 'vue'
export default {
  setup(props, { expose }) {
    const count = ref(0)
    const increment = () => ++count.value
    expose({
      increment
    })
    return () => h('div', count.value)
  }
}
import { h, ref } from 'vue'
export default {
    setup(props, { expose }) {
        const count = ref(0)
        const increment = () => ++count.value
        return {
            count,
            increment
        }
    }
}

setup()可以直接返回一个object(如上面代码块所示),handleSetupResult()将返回的对象进行proxyRefs的封装,如下面代码块所示,本质就是进行new Proxy()的拦截,然后进行ref类型的判断以及对应的解构,除了解构之外,其它操作就是普通的getset方法

由于proxyRefs会自动解构,因为我们setup()返回的Ref类型数据,也不用手动写ref.value,直接写ref即可,会自动解构

function proxyRefs(objectWithRefs) {
    return isReactive(objectWithRefs)
        ? objectWithRefs
        : new Proxy(objectWithRefs, shallowUnwrapHandlers);
}
const shallowUnwrapHandlers = {
    // unref = isRef(ref) ? ref.value : ref;
    get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
    set: (target, key, value, receiver) => {
        const oldValue = target[key];
        if (isRef(oldValue) && !isRef(value)) {
            oldValue.value = value;
            return true;
        }
        else {
            return Reflect.set(target, key, value, receiver);
        }
    }
};
处理setupResult-finishComponentSetup()

根据setupResult数据的类型做出处理后,最终还是调用了finishComponentSetup()方法,与setup不存在时的处理调用的方法一致,因此这个阶段的分析直接放在下面一小节中

第3步:setup不存在时的处理-finishComponentSetup()

整体概述
function finishComponentSetup(instance, isSSR, skipOptions) {
    const Component = instance.type;
    if (!instance.render) {
        if (!isSSR && compile && !Component.render) {
            const template = Component.template;
            startMeasure(instance, `compile`);
            //...拼接finalCompilerOptions
            Component.render = compile(template, finalCompilerOptions);
            endMeasure(instance, `compile`);
        }
        instance.render = (Component.render || NOOP);
        if (installWithProxy) {
            //for runtime-compiled render functions using `with` blocks
            installWithProxy(instance);
        }
    }
    // support for 2.x options
    setCurrentInstance(instance);
    pauseTracking();
    applyOptions(instance);
    resetTracking();
    unsetCurrentInstance();
}
instance.render不存在,但是存在Component.template

一般开发.vue程序时,会在运行编译阶段通过vue-loader将组件的<template></template>转化为render()函数,以及转化对应的JavaScriptCSS,形成标准的渲染函数,因此我们一般都使用Runtime-only版本的Vue.js代码但是存在一种情况,我们需要通过某种途径获取模版内容(比如从网络请求获取对应的模板数据),然后借助Vue.jsRuntime+Compiler版本,动态转化模板为render()函数,如下面代码块所示,当instance.render=false并且Component.template存在时,动态转化形成instance.render

installWithProxy还不清楚是哪种情况的处理,先暂时搁置,后面遇到这种情况再回来补充完整细节

if (!instance.render) {
    if (!isSSR && compile && !Component.render) {
        const template = Component.template;
        startMeasure(instance, `compile`);
        //...拼接finalCompilerOptions
        Component.render = compile(template, finalCompilerOptions);
        endMeasure(instance, `compile`);
    }
    instance.render = (Component.render || NOOP);
    if (installWithProxy) {
        //for runtime-compiled render functions using `with` blocks
        installWithProxy(instance);
    }
}
function registerRuntimeCompiler(_compile) {
    compile = _compile;
    installWithProxy = i => {
        if (i.render._rc) {
            i.withProxy = new Proxy(i.ctx, RuntimeCompiledPublicInstanceProxyHandlers);
        }
    };
}
兼容Vue2的Options API模式-applyOptions()
  • 合并Options APIComposition API的相关属性,包括injectmethodsdatacomputedwatchprovide
  • 合并Options APIComposition API的生命周期,包括onBeforeMountonMounted等等很多生命周期
  • 处理exposerenderinheritAttrscomponentsdirectives

几个注意的点:

  1. 合并Options API的data时使用了reactive(data)
  2. beforeCreate生命周期触发后才进行Options APIComposition API的合并
  3. 合并完成injectmethodsdatacomputedwatchprovide后触发created生命周期
  4. 然后再处理其它生命周期以及exposerenderinheritAttrscomponentsdirectives
function applyOptions(instance) {
    const options = resolveMergedOptions(instance);
    const publicThis = instance.proxy;
    const ctx = instance.ctx;
    // beforeCreate
    if (options.beforeCreate) {
        callHook(options.beforeCreate, instance, "bc" /* BEFORE_CREATE */);
    }
    const {
        //...很多属性
    } = options;

    if (dataOptions) {
        const data = dataOptions.call(publicThis, publicThis);
        instance.data = reactive(data);
    }
    // created
    if (created) {
        callHook(created, instance, "c" /* CREATED */);
    }

    registerLifecycleHook(onBeforeMount, beforeMount);
    //.......
}

setupRenderEffect设置并运行带有effect的渲染函数

const mountComponent = (...args) => {
    // 创建组件实例
    initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense)
    const instance = initialVNode.component;
    //设置组件实例
    setupComponent(instance);
    //设置并运行带有effect的渲染函数
    setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
}

上面已经详细分析了createComponentInstance()setupComponent()的执行逻辑,这一节将讲解mountComponent的最后一个逻辑:setupRenderEffect()

setupRenderEffect整体概述

从下面的代码块可以知道,最终触发了update(),也就是触发effect.run.bind(effect),由后续的响应式系统文章可以知道最终执行的顺序为:queueJob(instance.update)->effect.run.bind(effect)->getter: componentUpdateFn(),此时instance.isMounted=false,进行renderComponentRoot和patch

const setupRenderEffect = (instance, ...args) => {
    const componentUpdateFn = () => {
        if (!instance.isMounted) {
            //...进行renderComponentRoot和patch
        }
    }
    const effect = (instance.effect = new ReactiveEffect(
        componentUpdateFn,
        () => queueJob(instance.update),
        instance.scope // track it in component's effect scope
    ));

    const update = (instance.update = effect.run.bind(effect));
    update.id = instance.uid;
    toggleRecurse(instance, true);

    update()
}

componentUpdateFn

componentUpdateFn剔除生命周期等执行代码以及更新组件的逻辑代码后,最终的核心代码可以精简到下面的代码块

const componentUpdateFn = () => {
    if (!instance.isMounted) {
        toggleRecurse(instance, false);
        // beforeMount hook处理
        toggleRecurse(instance, true);
        const subTree = (instance.subTree = renderComponentRoot(instance));
        patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
        initialVNode.el = subTree.el;
        instance.isMounted = true;
        initialVNode = container = anchor = null;
    }
}

renderComponentRoot(instance)生成subTree

从下面的代码块可以知道,最终调用的是render.call(),这里的render()<template>转化后的render函数

上面createApp()流程执行app.mount()所触发的render()ensureRenderer()创建的自定义渲染器内部建立的render()函数,不是<template>转化后的render函数

function renderComponentRoot(instance) {
    const {
        render
        //...
    } = instance
    const prev = setCurrentRenderingInstance(instance);
    // ...省略代码
    if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
        // 有状态组件
        const proxyToUse = withProxy || proxy;
        result = normalizeVNode(render.call(proxyToUse, proxyToUse, renderCache, props, setupState, data, ctx));
    } else {
        // functional组件
    }
    // ...省略代码
    setCurrentRenderingInstance(prev);
    return result;
}

通过render()函数的createVNode()等一些方法创建vnode数据后,进行normalizeVNode()的简单整理

function normalizeVNode(child) {
    // child就是render生成后的vnode数据
    if (child == null || typeof child === 'boolean') {
        // empty placeholder
        return createVNode(Comment);
    } else if (isArray(child)) {
        // fragment
        return createVNode(Fragment, null,
            // #3666, avoid reference pollution when reusing vnode
            child.slice());
    } else if (typeof child === 'object') {
        // already vnode, this should be the most common since compiled templates
        // always produce all-vnode children arrays
        return cloneIfMounted(child);
    } else {
        // strings and numbers
        return createVNode(Text, null, String(child));
    }
}
function cloneIfMounted(child) {
    return child.el === null || child.memo ? child : cloneVNode(child);
}

整理之后,result是组件最顶部元素的vnode数据,比如下面的代码块,顶部有两个div元素,因此会默认生成一个最外层的fragment元素

<template>
    <div></div>
    <div></div>
</template>

subTree = {
    type: "Symbol(Fragment)"
    children: [
        {
            type: "div",
            props: { id: 'item1' },
            el: "div#item1"// 实际上是DOM元素
        },
        {
            type: "div",
            props: { id: 'item1' },
            el: "div#item2" // 实际上是DOM元素
        }
    ]
}

patch(null, subTree, container)

如整体概述的代码所示,当执行renderComponentRoot()创建vnode数据后,此时vnode数据是以顶部元素的数据,然后再次触发patch()方法,此时跟ensureRenderer().createApp().mount()触发的patch()方法有两个点不一样,一个是vnode数据,此时的vnode数据类型是Symbol(Fragment),而不是Component数据类型,因此不会触发processComponent,会触发processFragment()

patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);

const patch = (n1, n2, container, ...args) => {
    //...
    const { type, ref, shapeFlag } = n2;
    switch (type) {
        case Text: // 处理文本
            processText(n1, n2, container, anchor);
            break;
        case Fragment: // 处理Frgment元素
            processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
            break;
        default:
            if (shapeFlag & 1 /* ELEMENT */) {//处理普通DOM元素
                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);
            }
    }
    //...
}
processFragment()

从下面的代码块可以知道,最终processFragment()触发的是mountChildren(),最终也是触发每一个children vnodepatch()

const processFragment = (n1, n2, container, anchor, ...xx) => {
    const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''));
    const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''));
    if (n1 == null) {
        hostInsert(fragmentStartAnchor, container, anchor);
        hostInsert(fragmentEndAnchor, container, anchor);
        mountChildren(n2.children, container, fragmentEndAnchor, ...xxx);
    }
};
const mountChildren = (children, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, start = 0) => {
    for (let i = start; i < children.length; i++) {
        const child = (children[i] = optimized
            ? cloneIfMounted(children[i])
            : normalizeVNode(children[i]));
        patch(null, child, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
    }
};

借助下面的示例,我们可以知道,children vnodetype=divvnode数据类型,因此patch()会触发processElement()代码

<template>
    <div></div>
    <div></div>
</template>

subTree = {
    type: "Symbol(Fragment)"
    children: [
        {
            type: "div",
            props: { id: 'item1' },
            el: "div#item1"// 实际上是DOM元素
        },
        {
            type: "div",
            props: { id: 'item1' },
            el: "div#item2" // 实际上是DOM元素
        }
    ]
}
processElement()

从下面的代码块可以知道,processElement()最终触发的是mountElement()

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

从下面的代码块可以知道,最终mountElement()触发核心流程是:

  • 使用hostCreateElement创建一个DOM 元素,比如创建一个div DOM元素
  • 根据类型,如果为<div>textContent</div>,即children是文本的时候,直接调用hostSetElementText,即setText
  • 根据类型,如果为<div><div></div></div>,即children还是其它DOM元素的时候,需要再调用mountChildren()->patch(children),然后根据type,再调用不同的处理函数,比如<div><div></div><div></div></div> = mountChildren()->processElement(children1)->processElement(children2)
  • 最终调用hostInsert进行元素的插入,比如App.vue,传递的container就是app.mount("#el")中的div#el,最终会将App.vue的最最外层创建的元素插入到div#el的子元素的末尾

insert:先子节点进行hostInsert -> 后父节点进行hostInsert -> 最终挂载到最外层的DOM上

const mountElement = (vnode, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
    let el;
    const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode;
    // ...
    el = vnode.el = hostCreateElement(vnode.type, isSVG, props && props.is, props);
    // mount children first, since some props may rely on child content
    // being already rendered, e.g. `<select value>`
    if (shapeFlag & 8 /* TEXT_CHILDREN */) {
        hostSetElementText(el, vnode.children);
    } else if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
        mountChildren(vnode.children, el, null, parentComponent, parentSuspense, isSVG && type !== 'foreignObject', slotScopeIds, optimized);
    }
    // ...
    hostInsert(el, container, anchor);
    // ...
};

组件更新-整体流程-触发父组件和子组件更新

示例

<script>
const SecondComponent = {
    template: '<button>SecondComponent: {{count}}</button>',
    props: {
        count: Number
    }
}
const InnerComponent = {
    template: '<span>InnerComponent: {{proxyCount}}</span><div>这是InnerComponent的第2个元素</div>',
    props: {
        count: Number
    }
}
const InnerComponent1 = {
    template: '<span>InnerComponent1: {{proxyCount}}</span>',
    props: {
        count: Number
    }
}
</script>
<script type="text/x-template" id="app">
<div id="app-wrapper">
    <div id="app-content1">
        <div>app-content1: app-content1{{proxyCount}}</div>
        <InnerComponent v-if="proxyCount>=1" :count="proxyCount"></InnerComponent>
        <InnerComponent1 v-else :count="proxyCount"></InnerComponent1>
    </div>
    <second-component :count="proxyCount"></second-component>
</div >
</script >
onMounted(() => {
    setTimeout(() => {
        console.clear();
        proxyCount.value = 8;
    }, 500);
    console.error("onMounted():" + proxyCount.value);
});

流程图

Vue3源码-整体渲染流程浅析

整体流程图源码分析

前置分析

渲染Effect的依赖收集

由上面首次渲染分析可以知道,我们在app.mount("#app")->内部render()->patch()->mountComponent()->setupRenderEffect()->为Component建立ReactiveEffect->手动调用建立ReactiveEffect时传入的getter:componentUpdateFn->触发vue-loader转化的render()->触发响应式数据Proxygetter,从而触发依赖收集

const componentUpdateFn = () => {
    if (!instance.isMounted) {
        // 首次渲染逻辑
        // 执行<template>转化的render()函数,触发响应式数据的getter,进行依赖收集
        const subTree = (instance.subTree = renderComponentRoot(instance));
        // 进行vnode数据的挂载
        patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
    } else {
        
    }
};
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn,
    () => queueJob(instance.update), instance.scope));
const update = (instance.update = effect.run.bind(effect));
update.id = instance.uid;
toggleRecurse(instance, true);
update();

渲染Effect的派发更新

当渲染Effect的响应式数据发生变化时,触发渲染Effect的重新执行,即触发componentUpdateFn()重新执行,此时instance.isMounted=true

const componentUpdateFn = () => {
    if (!instance.isMounted) {
        
    } else {
        // 更新组件的更新逻辑
    }
};

App.vue触发派发更新详细分析

主要触发的流程为

  • 此时next为空,因此不会执行updateComponentPreRender()
  • renderComponentRoot(): 执行vue-loader转化<template>后的render()函数,渲染新的组件根部vnode数据(携带children)
  • patch(prevTree, nextTree):进行新旧数据的具体比对更新逻辑
  • next.el=nextTree.el:缓存更新后的el

结合示例代码和上述流程,我们可以知道,App.vue会触发componentUpdateFn(),从而执行流程

  • next = vnode
  • const nextTree = renderComponentRoot(instance)
  • patch(prevTree, nextTree)

此时传入的nextTree是组件App.vue根部节点type=div#app-wrapperchildren=[{type:"div#app-content1"}, {type:"div#app-content2"}]vnode数据

const componentUpdateFn = () => {
    if (!instance.isMounted) {
        
    } else {
        // 更新组件的更新逻辑
        let {next, bu, u, parent, vnode} = instance;
        toggleRecurse(instance, false);
        if (next) {
            next.el = vnode.el;
            updateComponentPreRender(instance, next, optimized);
        } else {
            next = vnode;
        }
        toggleRecurse(instance, true);
        const nextTree = renderComponentRoot(instance);
        const prevTree = instance.subTree;
        instance.subTree = nextTree;
        patch(prevTree, nextTree,
            // parent may have changed if it's in a teleport
            hostParentNode(prevTree.el),
            // anchor may have changed if it's in a fragment
            getNextHostNode(prevTree), instance, parentSuspense, isSVG);

        next.el = nextTree.el;
    }
};

核心流程patch()分析

  • 当两个vnoden1.type === n2.type && n1.key === n2.key不成立时,isSameVNodeType=false,直接调用n1.umount(),然后设置n1=null,跟首次渲染的流程一致
  • 当两个vnode的类型相同,会触发更新逻辑,结合示例分析,由于nextTreetype=div,因此会触发patch()->processElement()的处理
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);

const patch = (n1, n2, container, ...args) => {
    if (n1 && !isSameVNodeType(n1, n2)) {
        anchor = getNextHostNode(n1);
        unmount(n1, parentComponent, parentSuspense, true);
        n1 = null;
    }
    const { type, ref, shapeFlag } = n2;
    switch (type) {
        case Text: // 处理文本
            processText(n1, n2, container, anchor);
            break;
        case Fragment: // 处理Frgment元素
            processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
            break;
        default:
            if (shapeFlag & 1 /* ELEMENT */) {//处理普通DOM元素
                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);
            }
    }
}
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
    isSVG = isSVG || n2.type === 'svg';
    if (n1 == null) {
        // ...首次渲染
    } else {
        patchElement(n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
    }
};

processElement()->patchElement()分析

由于patchFlag和Block涉及到的知识点较多,留在后续文章编译优化中讲解,这里示例调试代码,强制force full diff

patchElement()的流程主要为:

  • patchChildren():进行vnode children的对比更新处理
  • patchProps(): 更新当前vnodeprops数据,包括styleclassidon事件DOM Prop(.stop等等)等等可以挂载在普通HTML元素,比如<div>/Vue组件元素,比如<App>上面的属性
const patchElement = (n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
    // 暂时回避patchFlag,全部更新都是full diff
    parentComponent && toggleRecurse(parentComponent, true);
    // if (isHmrUpdating) {
    if (true) {
        // HMR updated, force full diff
        patchFlag = 0;
        optimized = false;
        dynamicChildren = null;
    }
    // full diff
    patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds, false);
    // unoptimized, full diff
    patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG);
};
patchChildren:更新children

暂时回避patchFlag相关逻辑,全部更新都是full diff

从下面代码块可以知道,patchChildren()直接9种条件进行列举处理

const patchChildren = (n1, n2, container, ...args) => {
    const {patchFlag, shapeFlag} = n2;
    // children has 3 possibilities: text, array or no children.
    if (shapeFlag & 8 /* TEXT_CHILDREN */) {
        // text children fast path
        if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {
            unmountChildren(c1, parentComponent, parentSuspense);
        }
        if (c2 !== c1) {
            hostSetElementText(container, c2);
        }
    } else {
        if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {
            // prev children was array
            if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
                // two arrays, cannot assume anything, do full diff
                patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
            } else {
                // no new children, just unmount old
                unmountChildren(c1, parentComponent, parentSuspense, true);
            }
        } else {
            // prev children was text OR null
            // new children is array OR null
            if (prevShapeFlag & 8 /* TEXT_CHILDREN */) {
                hostSetElementText(container, '');
            }
            // mount new if array
            if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
                mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
            }
        }
    }
};

条件1:旧children文本->新children文本
  • hostSetElementText:更新文本内容
条件2:旧children数组->新children文本
  • unmountChildren:先移除所有的旧vnodechildren数组
  • hostSetElementText:设置DOM的内部元素为新vnodechildren文本
条件3:旧children空->新children文本
  • hostSetElementText:设置DOM的内部元素为新vnodechildren文本
条件4:旧children文本->新children数组
  • hostSetElementText:清空文本内容
  • mountChildren:初始化数组vnode children
条件5:旧children数组->新children数组
条件6:旧children空->新children数组
  • mountChildren:直接初始化数组vnode children
条件7:旧children文本->新children
  • hostSetElementText:清空文本内容
条件8:旧children数组->新children
  • unmountChildren:先移除所有的旧vnodechildren数组
条件9:旧children空->新children
  • 什么都不做
patchProps:更新props、class等各种VNode数据

暂时回避patchFlag相关逻辑,全部更新都是full diff

  • 获取newProps对应的key,更新oldProps对应keyvalue
  • newProps不存在的key,代表已经废弃,删除oldProps已经废弃的key
  • 更新key=value的值
const patchProps = (el, vnode, oldProps, newProps, parentComponent, parentSuspense, isSVG) => {
    if (oldProps !== newProps) {
        for (const key in newProps) {
            // empty string is not valid prop
            if (isReservedProp(key))
                continue;
            const next = newProps[key];
            const prev = oldProps[key];
            // defer patching value
            if (next !== prev && key !== 'value') {
                hostPatchProp(el, key, prev, next, isSVG, vnode.children, parentComponent, parentSuspense, unmountChildren);
            }
        }
        if (oldProps !== EMPTY_OBJ) {
            for (const key in oldProps) {
                if (!isReservedProp(key) && !(key in newProps)) {
                    hostPatchProp(el, key, oldProps[key], null, isSVG, vnode.children, parentComponent, parentSuspense, unmountChildren);
                }
            }
        }
        if ('value' in newProps) {
            hostPatchProp(el, 'value', oldProps.value, newProps.value);
        }
    }
};

processComponent()->updateComponent()分析

由示例可知,最终会触发子Component的渲染更新,最终触发patch()->processComponent()

const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
    n2.slotScopeIds = slotScopeIds;
    if (n1 == null) {
        // ...首次渲染
    } else {
        updateComponent(n1, n2, optimized);
    }
};
const updateComponent = (n1, n2, optimized) => {
    const instance = (n2.component = n1.component);
    if (shouldUpdateComponent(n1, n2, optimized)) {
        // ...省略异步相关if分支逻辑
        instance.next = n2;
        // 监测是否已经执行,防止重复更新
        invalidateJob(instance.update);
        // instance.update is the reactive effect.
        instance.update();
    } else {
        // no update needed. just copy over properties
        n2.component = n1.component;
        n2.el = n1.el;
        instance.vnode = n2;
    }
};

上面代码块的流程为:

  • shouldUpdateComponent:比对两个vnode的propschildrendirstransition,从而返回是否需要更新的值
  • 如果不需要更新,则直接将覆盖属性即可
  • 如果需要更新
    • instance.next = n2
    • 监测当前组件的渲染effect是否已经在队列中,防止重复更新
    • 触发instance.update(),由下面mountComponent()相关源码分析可以知道,最终instance.update()=effect.run.bind(effect)->getter: componentUpdateFn()
const mountComponent = (...args) => {
    //......
    //设置并运行带有effect的渲染函数
    setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
}
const setupRenderEffect = (instance, ...args) => {
    const componentUpdateFn = () => {
        if (!instance.isMounted) {
            //...进行renderComponentRoot和patch
        } else {
            // updateComponent()->触发更新逻辑
        }
    }
    const effect = (instance.effect = new ReactiveEffect(
        componentUpdateFn,
        () => queueJob(instance.update),
        instance.scope // track it in component's effect scope
    ));

    const update = (instance.update = effect.run.bind(effect));
    update.id = instance.uid;
    toggleRecurse(instance, true);

    update()
}
componentUpdateFn: 渲染effect触发更新组件逻辑

从上面示例可以知道,我们最外层是App.vue,内部又有其它Component组件,比如InnerComponent,如下面代码块所示

<App>
  <InnerComponent></InnerComponent>
</App>

从上面App.vue触发派发更新详细分析-核心流程patch()分析,我们可以知道,响应式数据会触发App.vue渲染Effect重新执行,由于没有从processComponent()->updateComponent()进入,因此instance.next为空,而子组件InnerComponent是经过processComponent()->updateComponent()触发的effect.run()方法,因此instance.next不为空从下面的代码块可以知道,流程为:

  • updateComponentPreRender:更新新旧instance的各种数据,包括instance.vnodeinstance.propsinstance.slots
  • const nextTree = renderComponentRoot(instance):渲染新vnode的根部节点vnode数据
  • patch(prevTree, nextTree):传入新旧vnode的根部节点vnode数据进行patch()

此时传入的nextTree是子Component根部节点的vnode数据,然后触发patch(),然后根据type进行processElement/processComponent的递归深度处理,最终处理完成所有的结构的更新

const componentUpdateFn = () => {
    if (!instance.isMounted) {
        
    } else {
        // 更新组件的更新逻辑
        let {next, bu, u, parent, vnode} = instance;
        toggleRecurse(instance, false);
        if (next) {
            next.el = vnode.el;
            updateComponentPreRender(instance, next, optimized);
        } else {
            next = vnode;
        }
        toggleRecurse(instance, true);
        const nextTree = renderComponentRoot(instance);
        const prevTree = instance.subTree;
        instance.subTree = nextTree;
        patch(prevTree, nextTree,
            // parent may have changed if it's in a teleport
            hostParentNode(prevTree.el),
            // anchor may have changed if it's in a fragment
            getNextHostNode(prevTree), instance, parentSuspense, isSVG);

        next.el = nextTree.el;
    }
};