【Vue3源码】从源码层面观察vue的变化

2,120 阅读14分钟

写在前面

Vue3发布已经有段时间了,相比Vue2做了特别多的优化。但具体好在哪里呢,除了开发者用得爽之外,框架底层优化需要我们通过研究源码才能有切身体会。

本文主要是通过源码层面来对比 Vue3Vue2,思考与总结新的 Vue3 做了哪些优化,这些优化好在哪里。

ps:点击传送门前往 Vue2 的源码整理

注:文章中有些标题是带下划线的蓝色方法名,这些方法都对应设置了超链接,点击即可跳转到源码中对应文件的位置

1. 初始化

Vue3 相对 Vue2 是一次重构,改用多模块架构,分为 compilerreactivityruntime三大模块

  • compiler-core
    • compiler-dom
  • runtime-core
    • runtime-dom
  • reactivity

将来在做 自定义渲染 时只需要基于 compilerruntime 两个 core 模块进行扩展即可

于是就引申出了一个概念:renderer,意为渲染器,是应用的入口,来自 runtime 模块

runtime 模块下我们可以扩展任意平台的渲染规则,目前我们要研究的web 平台入口是runtime-dom

于是初始化方式也发生了改变,通过Vue实例上的createApp方法创建页面应用,所以初始化阶段我们从createApp入手

1.1. Vue 的初始化

1.1.1. createApp

  • 作用

    • 获取渲染器renderer),通过渲染器createApp创建app 对象
    • 扩展mount方法
  • 核心源码

    const createApp = ((...args) => {
      const app = ensureRenderer().createApp(...args);
    
      const { mount } = app;
      app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
        const container = normalizeContainer(containerOrSelector);
        if (!container) return;
    
        // 挂载前先清空dom的内容
        container.innerHTML = '';
        // 调用原本的mount方法进行挂载
        const proxy = mount(container);
    
        return proxy;
      };
    
      return app;
    }) as CreateAppFunction<Element>;
    
    function ensureRenderer() {
      // 返回单例renderer
      return renderer || (renderer = createRenderer<Node, Element>(rendererOptions));
    }
    

    目前的createApp基于runtime-dom,目的是通过ensureRenderer创建一个基于web 平台渲染器,之所以加上一个“目前的”,是因为还会有runtime-corecreateApp,后面会具体整理。

    ps:rendererOptionsweb 平台特有的操作dom属性的方法。

1.1.2. createRenderer

  • 作用

    • 通过参数options创建平台客户端渲染器
  • 核心源码

    function createRenderer<HostNode = RendererNode, HostElement = RendererElement>(
      options: RendererOptions<HostNode, HostElement>
    ) {
      return baseCreateRenderer<HostNode, HostElement>(options);
    }
    

    这里再调用一次baseCreateRenderer是为了创建客户端渲染器,可以在当前文件下面看到还有另外一个方法createHydrationRenderer,他也调用了baseCreateRenderer,这是创建服务端渲染器

1.1.3. baseCreateRenderer

  • 作用

    • 根据平台操作参数返回真正的平台渲染器
  • 核心源码

    function baseCreateRenderer(
      options: RendererOptions,
      createHydrationFns?: typeof createHydrationFunctions
    ): any {
    
      // 取出平台特有的方法,这里太占空间,暂时不贴了,主要是insert、remove、patchProp等等
      const {...} = options
    
      // 接下来是很多组件渲染和diff的方法
      const patch = (n1, n2, container, ...) => {...}
      const processElement = (n1, n2, container) => {...}
      const mountElement = (vnode, container, ...) => {...}
      const mountChildren = (children, container, ...) => {...}
      ...
    
      const render: RootRenderFunction = (vnode, container) => {
        if (vnode == null) {
          if (container._vnode) {
            unmount(container._vnode, null, null, true);
          }
        } else {
          // 初始化和更新都走这里,类似vue2的__patch__
          patch(container._vnode || null, vnode, container);
        }
        container._vnode = vnode;
      };
    
      return {
        render,
        hydrate,
        createApp: createAppAPI(render, hydrate),
      };
    }
    

    渲染器包括renderhydratecreateApp三个方法,

    这一步非常重要,或许将来基于Vue3跨平台开发都会类似这种形式。

    通过参数options解构出基于平台操作 dom属性 的方法,用来创建真正的渲染更新函数。其中需要关注的是patch,因为他不止负责渲染更新,将来的初始化组件也通过这个入口进入 ⭐。

    其中render方法类似于vue2vm._update,负责初始化更新

    由于baseCreateRenderer是一个长达 1800 多行的方法,初始化时只关注最终返回的渲染器即可,

    最后的createApp由工厂函数createAppAPI创建

1.1.4. createAppAPI

  • 作用

    • 通过参数renderhydrate创建平台createApp方法,createApp用于创建真正的app(Vue) 实例
  • 核心源码

    function createAppAPI<HostElement>(
      render: RootRenderFunction,
      hydrate?: RootHydrateFunction
    ): CreateAppFunction<HostElement> {
      return function createApp(rootComponent, rootProps = null) {
        const app = {
          use(plugin: Plugin, ...options: any[]) {
            plugin.install(app, ...options);
            return app;
          },
    
          mixin(mixin: ComponentOptions) {
            context.mixins.push(mixin);
            return app;
          },
    
          component(name: string, component?: Component): any {
            context.components[name] = component;
            return app;
          },
    
          directive(name: string, directive?: Directive) {
            context.directives[name] = directive;
            return app;
          },
          mount(rootContainer: HostElement, isHydrate?: boolean): any {
            ...
          },
          unmount() {
            if (isMounted) {
              render(null, app._container);
            }
          },
          provide(key, value) {
            context.provides[key as string] = value;
            return app;
          },
        };
        return app;
      };
    }
    

    还记得吗,刚才有提醒,有两个createApp方法

    • runtime-core模块中:创建真正的app(Vue)实例
    • runtime-dom模块中:通过runtime-core模块创建web 平台的渲染器,利用渲染器拿到实例

    createApp内部定义了许多实例上的方法,usemixincomponentdirectivemountunmountprovide。 熟悉Vue2 的小伙伴可能会发现了,原来的静态方法现在都变成了实例方法,而且几乎每个方法返回app对象,因此可以链式调用,like this

    const app = createApp({
      setup() {
        const state = reactive({
          count: 0,
        });
        return { state };
      },
    })
      .use(store)
      .directive(transfer)
      .mixin(cacheStore)
      .mount('#app');
    

    这一步结束后渲染器createApp方法也有了,ensureRenderer将整个渲染器返回给runtime-dom模块。然后通过渲染器创建app 实例,对实例mount方法进行扩展,接下来进入实例渲染阶段

    mount渲染阶段的入口
    核心源码如下:

    mount(rootContainer: HostElement, isHydrate?: boolean): any {
      if (!isMounted) {
        // 初始化虚拟dom树
        const vnode = createVNode(rootComponent as ConcreteComponent, rootProps);
        if (isHydrate && hydrate) {
          // 服务端渲染
          hydrate(vnode as VNode<Node, Element>, rootContainer as any);
        } else {
          // 客户端渲染
          render(vnode, rootContainer);
        }
        return vnode.component!.proxy;
      }
    }
    

    app 实例未经扩展的mount方法相当于Vue2updateComponent,做了两件事情:

    1. 获取虚拟 dom

    2. 通过render方法将虚拟 dom转成真实 dom

      • 核心源码

        const render: RootRenderFunction = (vnode, container) => {
          if (vnode == null) {
            if (container._vnode) {
              unmount(container._vnode, null, null, true);
            }
          } else {
            patch(container._vnode || null, vnode, container);
          }
          flushPostFlushCbs();
          container._vnode = vnode;
        };
        

        render方法特别像Vue2vm._update,是初始化渲染组件更新的入口,均调用patch方法,
        由于首次渲染不存在旧的虚拟 dom,因此n1null

1.1.5. patch

  • 作用

    • 根据虚拟 dom的类型 进行 组件的初始化与更新,最终将虚拟 dom转成真实 dom,(ps:组件包括浏览器 host 组件和自定义组件,下文相同)
  • 核心源码

    const patch: PatchFn = (
        n1,// 旧的虚拟dom
        n2,// 新的虚拟dom
        container,
        anchor = null,
        parentComponent = null,
        parentSuspense = null,
        isSVG = false,
        optimized = false
      ) => {
        // 新节点的类型
        const { type, ref, shapeFlag } = n2
        switch (type) {
          case Text:
            ...
            break
          case Comment:
            ...
            break
          case Static:
            ...
            break
          case Fragment:
            ...
            break
          default:
            if (shapeFlag & ShapeFlags.ELEMENT) {
              ...
            } else if (shapeFlag & ShapeFlags.COMPONENT) {
              processComponent(
                n1,
                n2,
                container,
                anchor,
                parentComponent,
                parentSuspense,
                isSVG,
                optimized
              )
            }
        }
      }
    

    之所以说组件的初始化与更新,是因为Vue3patch不同于Vue2__patch____patch__只负责渲染,因此我们可以说是组件的渲染,但Vue3patch在渲染阶段最终触发的函数不仅包括组件的渲染,期间还包括组件初始化阶段

    由于初始化时传入的新的虚拟 dom(n2是开发者调用createApp的参数,所以被判定是一个对象类型,所以会作为自定义组件处理,因此接下来执行processComponent方法

    核心源码如下:

    const processComponent = (
      n1: VNode | null,
      n2: VNode,
      container: RendererElement,
      anchor: RendererNode | null,
      parentComponent: ComponentInternalInstance | null,
      parentSuspense: SuspenseBoundary | null,
      isSVG: boolean,
      optimized: boolean
    ) => {
      if (n1 == null) {
        if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
          ...
        } else {
          // 初始化渲染
          mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
        }
      } else {
        // 组件更新
        updateComponent(n1, n2, optimized);
      }
    };
    

    由于首次渲染传进来的旧的虚拟 domnull,所以执行mountComponent方法

1.1.6. mountComponent

  • 作用

    • 初始化自定义组件
      • 创建组件实例
      • 安装组件(即组件初始化)
      • 安装副作用:完成渲染,定义更新函数
  • 核心源码

    const mountComponent: MountComponentFn = (
      initialVNode,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized
    ) => {
      // 1. 创建组件实例
      const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
        initialVNode,
        parentComponent,
        parentSuspense
      ));
    
      // inject renderer internals for keepAlive
      if (isKeepAlive(initialVNode)) {
        (instance.ctx as KeepAliveContext).renderer = internals;
      }
    
      // 2. 安装组件(即组件初始化)
      setupComponent(instance);
    
      // setup() is async. This component relies on async logic to be resolved
      // before proceeding
      if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
        parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect);
    
        // Give it a placeholder if this is not hydration
        // TODO handle self-defined fallback
        if (!initialVNode.el) {
          const placeholder = (instance.subTree = createVNode(Comment));
          processCommentNode(null, placeholder, container!, anchor);
        }
        return;
      }
    
      // 3. 安装副作用:完成渲染,定义更新函数
      setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
    };
    

    组件的初始化包括:

    1. createComponentInstance: 创建组件实例
    2. setupComponent:安装组件(组件初始化)。(类似于Vue2初始化执行的vm._init方法)
    3. setupRenderEffect:安装渲染函数的副作用(effect),完成组件渲染,并定义组件的更新函数。(effect替代了Vue2Watcher

1.2. 组件初始化

1.2.1. setupComponent

  • 作用

    • 安装组件(组件初始化)
      • mergeOptions
      • 定义实例的属性、事件、处理插槽,
      • 通过setupStatefulComponent方法完成数据响应式
  • 核心源码

    function setupComponent(instance: ComponentInternalInstance, isSSR = false) {
      isInSSRComponentSetup = isSSR;
    
      const { props, children, shapeFlag } = instance.vnode;
      const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT;
      // 初始化props
      initProps(instance, props, isStateful, isSSR);
      // 初始化插槽
      initSlots(instance, children);
    
      // 数据响应式
      const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined;
    
      isInSSRComponentSetup = false;
      return setupResult;
    }
    

    其中setupStatefulComponent负责数据响应式

(1) setupStatefulComponent

  • 作用

    • 完成数据响应式
  • 核心源码

    function setupStatefulComponent(instance: ComponentInternalInstance, isSSR: boolean) {
      // createApp的配置对象
      const Component = instance.type as ComponentOptions;
    
      // 0. create render proxy property access cache
      instance.accessCache = Object.create(null);
    
      // 1. render函数的上下文
      instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
    
      // 2. 处理setup函数
      const { setup } = Component;
      if (setup) {
        const setupContext = (instance.setupContext =
          setup.length > 1 ? createSetupContext(instance) : null);
    
        currentInstance = instance;
        pauseTracking();
        const setupResult = callWithErrorHandling(setup, instance, ErrorCodes.SETUP_FUNCTION, [
          __DEV__ ? shallowReadonly(instance.props) : instance.props,
          setupContext,
        ]);
        resetTracking();
        currentInstance = null;
    
        if (isPromise(setupResult)) {
          if (isSSR) {
            ...
          } else if (__FEATURE_SUSPENSE__) {
            // async setup returned Promise.
            // bail here and wait for re-entry.
            instance.asyncDep = setupResult;
          }
        } else {
          // 最终也会执行finishComponentSetup
          handleSetupResult(instance, setupResult, isSSR);
        }
      } else {
        finishComponentSetup(instance, isSSR);
      }
    }
    

    没有设置setup会执行handleSetupResult,最终依然会调用finishComponentSetup方法

(2) finishComponentSetup

  • 作用

    • 确保instance上有render 函数
    • 兼容 Vue2 options API 方式数据响应式功能
  • 核心源码

    function finishComponentSetup(instance: ComponentInternalInstance, isSSR: boolean) {
      const Component = instance.type as ComponentOptions;
    
      // template / render function normalization
      if (__NODE_JS__ && isSSR) {
        ...
      } else if (!instance.render) {
        // could be set from setup()
        if (compile && Component.template && !Component.render) {
          Component.render = compile(Component.template, {
            isCustomElement: instance.appContext.config.isCustomElement,
            delimiters: Component.delimiters,
          });
        }
    
        instance.render = (Component.render || NOOP) as InternalRenderFunction;
    
        // for runtime-compiled render functions using `with` blocks, the render
        // proxy used needs a different `has` handler which is more performant and
        // also only allows a whitelist of globals to fallthrough.
        if (instance.render._rc) {
          instance.withProxy = new Proxy(instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers);
        }
      }
    
      // applyOptions兼容Vue2的options API
      if (__FEATURE_OPTIONS_API__) {
        currentInstance = instance;
        pauseTracking();
        applyOptions(instance, Component);
        resetTracking();
        currentInstance = null;
      }
    }
    

1.2.2. setupRenderEffect

  • 作用

    • 安装渲染函数的副作用
    • 完成组件渲染
  • 核心源码

    const setupRenderEffect: SetupRenderEffectFn = (
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    ) => {
      // create reactive effect for rendering
      instance.update = effect(
        function componentEffect() {
          if (!instance.isMounted) {
            let vnodeHook: VNodeHook | null | undefined;
            const { el, props } = initialVNode;
            const { bm, m, parent } = instance;
    
            // 1.首先获取当前组件的虚拟dom
            const subTree = (instance.subTree = renderComponentRoot(instance));
    
            if (el && hydrateNode) {
              ...
            } else {
              // 初始化:递归执行patch
              patch(...)
            }
          } else {
            // updateComponent
            patch(...)
          }
        },
        __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions
      );
    };
    

    我对副作用的理解:如果定义了一个响应式数据,和他相关的副作用函数在数据发生变化时会重新执行

    函数内部通过instance.isMounted判断是初始化渲染或者是组件更新

    于是我们发现,effect替代了Vue2Watcher

1.3. 流程梳理

  1. Vue3的入口是createApp方法,createApp通过ensureRender获取renderer对象,调用renderer.createApp方法返回app 对象,然后扩展$mount方法

  2. ensureRender保证renderer是一个单例,通过createRenderer 调用 baseCreateRenderer完成创建

  3. baseCreateRenderer是真正创建renderer的方法,renderer包括renderhydratecreateApp,其中createApp方法通过调用createAppAPI创建

  4. createAppAPI是一个工厂函数,返回一个真正的createApp方法,createApp内部创建了**Vue(app)**的实例方法并返回

  5. 如果开发者调用了mount方法,将继续执行mount方法,从renderpatch,最终执行processComponent,在这里完成数据响应式真实 dom的挂载,至此,初始化阶段结束

1.4. 思考与总结

  1. 渲染器(renderer)是一个对象,包含三部分

    • render
    • hydrate
    • createApp
  2. 全局方法为何调整到实例上?

    • 避免实例之间污染
    • tree-shaking
    • 语义化
  3. 初始化阶段相比较Vue2的变化

    • 新增渲染器的概念,一切方法都由渲染器提供
    • Vue2通过创建对象的方式创建应用;而Vue3取消了对象的概念,改用方法返回实例,实例的方法可以链式调用
    • 根组件自定义组件
    • 自定义组件完成组件实例的创建初始化安装渲染/更新函数三件事情

2. 数据响应式与 effect

Vue2中,数据响应式存在以下几个小缺陷:

  1. 对于动态添加或删除的 key 需要额外的**api(Vue.set/Vue.delete)**解决
  2. 数组响应式需要单独一套逻辑处理
  3. 初始化时深层递归,效率相对低一些
  4. 无法监听新的数据结构 MapSet

Vue3 通过重构 响应式原理解决了上述问题,不仅如此,速度更是提升一倍内存占用减少1/2,那么一起来探究竟吧~

重构内容大致如下:

  1. proxy 代替 Object.defineProperty

  2. 数据观察

  3. 优化原本的发布订阅模型,去除 ObserverWatcherDep,改用简洁的 reactiveeffecttargetMap

    • track: 用于追踪依赖
    • trigger: 用于触发依赖
    • targetMap: 相当于发布订阅中心,以树结构管理对象、key 与依赖之间的关系

2.1. 从源码探究过程

定义Vue3响应式数据的核心方法是reactiveref

由于Vue3依然兼容Vue2,所以原本的options API可以继续使用,经过debug调试后发现最终在resolveData方法中执行了reactive;还有我在调试ref时发现也是借助了reactive,所以可以认为reactiveVue3 数据响应式入口

在研究reactive之前,我们先看一下响应式数据的几种枚举类型

2.1.1. ReactiveFlag

export const enum ReactiveFlags {
  SKIP = '__v_skip' /*        */, // 表示不需要被代理
  IS_REACTIVE = '__v_isReactive', // 响应式对象的标志,类似Vue2的__ob__
  IS_READONLY = '__v_isReadonly', // 只读对象的标志,不允许被修改
  RAW = '__v_raw' /*          */, // 原始类型
}

特别需要注意的是IS_REACTIVERAW

2.1.2. reactive

  • 作用
    • 创建响应式对象
  • 核心源码
    function reactive(target: object) {
      // if trying to observe a readonly proxy, return the readonly version.
      if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
        return target;
      }
      return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers);
    }
    
    除了只读对象,其他都允许执行响应式处理

2.1.3. createReactiveObject

  • 作用

    • 不重复地创建响应式对象
  • 核心源码

    function createReactiveObject(
      target: Target,
      isReadonly: boolean,
      baseHandlers: ProxyHandler<any>,
      collectionHandlers: ProxyHandler<any>
    ) {
      // 1. 如果是 只读属性或者代理,则直接返回
      if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) {
        return target;
      }
      // 2. 如果已经是响应式对象了,则从 缓存 中直接返回
      const proxyMap = isReadonly ? readonlyMap : reactiveMap;
      const existingProxy = proxyMap.get(target);
      if (existingProxy) {
        return existingProxy;
      }
      // 3. 创建响应式对象
      const proxy = new Proxy(
        target,
        targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
      );
      // 4. 存入 缓存
      proxyMap.set(target, proxy);
      return proxy;
    }
    

    类型不是 ObjectArrayMapSetWeakmapWeakset 均不作操作。

    并且根据 Set、Map普通对象 区分使用 handler

    如果是 普通对象(包含数组) 则使用baseHandlers,也就是mutableHandlers

2.1.4. mutableHandlers

  • 作用
    • 定义响应式的拦截方法
      • getter触发依赖收集和定义子元素的响应式
      • setter触发依赖更新
  • 核心源码
const get = function get(target: Target, key: string | symbol, receiver: object) {
  // 已经是响应式对象、只读等边缘情况的处理
  ...
  // 1. 数组
  const targetIsArray = isArray(target);

  if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
    return Reflect.get(arrayInstrumentations, key, receiver);
  }

  // 2. 对象
  const res = Reflect.get(target, key, receiver);

  // 3. 依赖追踪
  track(target, TrackOpTypes.GET, key);

  // 4. 如果是对象,则继续观察
  if (isObject(res)) {
    // Convert returned value into a proxy as well. we do the isObject check
    // here to avoid invalid value warning. Also need to lazy access readonly
    // and reactive here to avoid circular dependency.
    return reactive(res);
  }

  // 5. 返回
  return res;
};

const set = function set(
  target: object,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  const oldValue = (target as any)[key];
  // 边缘情况的判断
  ...

  const hadKey =
    isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key);

  const result = Reflect.set(target, key, value, receiver);

  // 如果目标是原型链中的内容则不要触发依赖更新
  if (target === toRaw(receiver)) {
    // 依赖更新
    if (!hadKey) {
      trigger(target, TriggerOpTypes.ADD, key, value);
    } else if (hasChanged(value, oldValue)) {
      trigger(target, TriggerOpTypes.SET, key, value, oldValue);
    }
  }
  return result;
};

function deleteProperty(target: object, key: string | symbol): boolean {
  const hadKey = hasOwn(target, key);
  const oldValue = (target as any)[key];
  const result = Reflect.deleteProperty(target, key);
  if (result && hadKey) {
    trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
  }
  return result;
}

// in 操作符的捕捉器。
function has(target: object, key: string | symbol): boolean {
  const result = Reflect.has(target, key);
  if (!isSymbol(key) || !builtInSymbols.has(key)) {
    track(target, TrackOpTypes.HAS, key);
  }
  return result;
}

// Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
function ownKeys(target: object): (string | number | symbol)[] {
  track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY);
  return Reflect.ownKeys(target);
}

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys,
};
  • getter
    • 触发track实现依赖收集
    • 向下继续观察
  • setter
    • 触发trigger负责触发依赖

proxy中有三种方法可以拦截getter,分别是gethasownKeys,有两种方法可以拦截setter,分别是setdeleteProperty。 与Vue2不同的是,Vue3数据侦听改用懒执行方式,即只有调用了getter方法才会继续向下侦听,有效减少了首次执行的时间

2.1.5. targetMap

  • 定义

    const targetMap = new WeakMap<any, KeyToDepMap>();
    
  • 作用

    发布订阅中心,是一个Map 结构,以树结构管理 各个对象对象的 keykey 对应的 effect的关系大概是这样

    type targetMap = {
      [key: Object]: {
        [key: string]: Set<ReactiveEffect>;
      };
    };
    

2.1.6. activeEffect

  • 定义

    let activeEffect: ReactiveEffect | undefined;
    
  • 作用

    是一个全局变量,用于临时保存正在执行副作用函数,本质上是一个副作用函数。有点像Vue2Dep.target

2.1.7. track

  • 作用

    • 收集依赖
  • 核心源码

    export function track(target: object, type: TrackOpTypes, key: unknown) {
      // 源码中有暂停收集和继续收集的两个方法,这里是没有暂停的标志
      // 全局变量activeEffect是在instance.update或用户手动设置的副作用函数
      if (!shouldTrack || activeEffect === undefined) {
        return;
      }
      // 从发布订阅中心取出对象所有的key
      let depsMap = targetMap.get(target);
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
      }
      // 去除对象key的所有依赖effect
      let dep = depsMap.get(key);
      if (!dep) {
        depsMap.set(key, (dep = new Set()));
      }
      if (!dep.has(activeEffect)) {
        dep.add(activeEffect);
        activeEffect.deps.push(dep);
      }
    }
    

    收集依赖是同样是双向操作

    targetMap收集副作用函数副作用函数也需要引用当前key依赖的所有副作用函数,用于将来重新收集依赖使用。为什么重新收集依赖在下文会详细说明

2.1.8. trigger

  • 作用

    • 触发依赖
  • 核心源码

    export function trigger(
      target: object,
      type: TriggerOpTypes,
      key?: unknown,
      newValue?: unknown,
      oldValue?: unknown
    ) {
      const depsMap = targetMap.get(target);
      if (!depsMap) {
        // never been tracked
        return;
      }
    
      const effects = new Set<ReactiveEffect>();
      // 添加副作用函数
      const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
        if (effectsToAdd) {
          effectsToAdd.forEach(effect => {
            if (effect !== activeEffect || effect.allowRecurse) {
              effects.add(effect);
            }
          });
        }
      };
    
    
      // 根据type和key找到需要处理的depsMap,这里处理边缘情况,最终执行ADD、DELETE、SET将depsMap中的内容添加到effects中
      ...
    
      // 首次渲染和异步更新
      const run = (effect: ReactiveEffect) => {
        if (effect.options.scheduler) {
          effect.options.scheduler(effect);
        } else {
          effect();
        }
      };
    
      // 遍历副作用函数并执行
      effects.forEach(run);
    }
    

    原本这里的代码有很多,但取出核心逻辑立刻就变简洁了。

    创建组件更新函数时会通过effect会传入第二个参数,其中包含 scheduler,将来在这里的 的 run 方法中会被使用

    核心内容就是根据 key触发依赖类型(ADDDELETESET 执行add方法,将依赖的副作用函数放到effects中批量执行

2.1.9. 副作用(effect)

我对副作用的理解是:如果定义了一个响应式数据,和他相关的副作用函数在数据发生变化时都会重新执行

debug过程中,发现watchEffectwatch也是通过doWatch方法最终调用effect,所以我们可以认为effect创建副作用函数的入口

(1)effectStack

  • 定义

    const effectStack: ReactiveEffect[] = [];
    
  • 作用

    这是一个栈(其实是数组)结构,存储多个副作用函数activeEffect,用于处理effect嵌套的场景(这个后面详细说明)。

(2)effect

  • 作用

    • 创建副作用函数,执行时触发getter完成依赖收集
  • 核心源码

    export function effect<T = any>(
      fn: () => T,
      options: ReactiveEffectOptions = EMPTY_OBJ
    ): ReactiveEffect<T> {
      if (isEffect(fn)) {
        fn = fn.raw;
      }
      const effect = createReactiveEffect(fn, options);
      if (!options.lazy) {
        effect();
      }
      return effect;
    }
    
    function createReactiveEffect<T = any>(
      fn: () => T,
      options: ReactiveEffectOptions
    ): ReactiveEffect<T> {
      const effect = function reactiveEffect(): unknown {
        if (!effect.active) {
          return options.scheduler ? undefined : fn();
        }
        if (!effectStack.includes(effect)) {
          cleanup(effect);
          try {
            enableTracking();
            effectStack.push(effect);
            activeEffect = effect;
            return fn();
          } finally {
            effectStack.pop();
            resetTracking();
            activeEffect = effectStack[effectStack.length - 1];
          }
        }
      } as ReactiveEffect;
      // 为effect添加很多属性
      ...
      return effect;
    }
    

    真正的副作用函数createReactiveEffect方法中创建,首先将副作用函数本身添加到effectStack栈顶,然后将赋值给activeEffect,紧接着执行fnfn触发响应式数据getter方法进行依赖收集,将activeEffect添加到 targetMap;当key发生改变时会触发依赖,从targetMap取出对应的副作用函数并执行,这便是一个副作用函数作为一个依赖收集触发流程

    或许你也会疑惑,activeEffect用于临时存储当前副作用函数我明白,但是为什么还要保存到effectStack这个栈结构中呢?

    后来查阅Vue3的社区才发现effect被设计为可以嵌套使用,这里的栈结构就是为了处理嵌套场景

    的特点是先进后出,也就是后进入的副作用函数率先执行,然后出栈,保证执行的顺序是从外到内

    这和react hook极不相同,可能是我用多了react的原因,嵌套这一点我暂时还是无法接受的,不过尤大既然这样设计自然有他的想法,所依作为一个react developer没有太多发言权,还是等Vue3用多了再做评价吧~

(3)副作用为什么需要重新收集依赖

或许在读响应式原理依赖收集的源码时你也会疑惑,为什么每次触发getter都进入track呢,而且还有重新收集依赖的过程

其实这是一种边缘情况的处理,副作用函数中的某些变量有可能会在条件中读取,因此存在动态依赖的行为。

没怎么懂对吗,这里用语言描述确实晦涩了点,不如用一小段代码体会一下,

watchEffect(() => {
  if (state.role === ROOT) {
    notice(state.user);
  }
});

state.user在条件中读取,当满足条件时,当前副作用函数state.user的依赖;当不满足条件时,state.user需要清除这个依赖。这样描述是否会清晰一些呢~

2.2. 流程概述

2.2.1. 数据响应式

  1. 初始化时创建响应式对象,建立 gettersetter 拦截,getter负责收集依赖,setter负责触发依赖
  2. 渲染时调用组件级effect方法,将组件的render 函数赋值给全局变量activeEffect并执行,render 函数触发对应keygetter函数,完成依赖收集
  3. 当用户再次触发了keysetter方法,从targetMap中取出对应的依赖函数,然后执行trigger方法触发依赖 完成更新

2.2.2. effect

目前看源码所了解到**触发effect**的方式有这些:instance.updatewatchwatchEffectcomputed

  1. 当执行到effect时,首先调用createReactiveEffect创建一个真正的副作用函数
  2. 如果是computed则等待响应式数据的getter触发副作用函数执行,反之则在创建过程中执行,最终都会触发keygetter函数,完成依赖收集
  3. 考虑到嵌套问题,将副作用函数放入effectStack中进行管理,每次执行然后出栈,保证副作用函数的执行顺序从外到内
  4. 另外要考虑动态依赖的边缘情况,所以需要重新收集依赖

2.3. 思考与总结

  1. Vue3的数据响应式那么多优点,有缺点吗?

    新的数据响应式方案不仅效率高,还可以完成13 个 api 的拦截,但缺点是不兼容低版本浏览器proxy,尤其是IE,不过都1202年了,还有人用IE嘛。。。 哈哈哈开玩笑~

  2. 为什么要用Reflect

    我的理解是,ReflectProxy相辅相成,只要proxy对象上有的方法reflect也拥有。而使用Reflect其实是一种是安全措施,保证操作的是原对象

  3. 为什么需要互相引用?

    这一点和Vue2很像,Vue2depWatcher也是互相引用,当删除 key时会解除二者的引用关系

    Vue3同样考虑到这一点,删除 key时需要解除副作用函数targetMap 中 key 的依赖函数的 关系

  4. effect 嵌套问题

    react 的函数式组件之所以不能嵌套使用 hook,是因为 react 的设计理念和 vue 不同,react 函数式组件每次 render 都作为函数自上而下执行,通过链表管理每个 hook 的状态,这就导致如果在条件或嵌套中使用 hook,会出现 hook 混乱的结果。但 vue 只是通过触发依赖更新组件,没有重新 render 一说,所以可以嵌套使用也是合理的,只是看开发者是否习惯这种思想的转换了。

  5. 关于重新收集依赖

    React副作用让开发者自己决定函数的执行依赖于哪些值,而Vue3替我们做了这件事情,于是开发者便不再有负担,只需要使用即可,不过有时候使用不当可能会造成多次重新收集依赖的过程,但也无伤大雅。

3. 异步更新

还记得吗,在组件但初始化阶段执行了setupRenderEffect,通过effectinstance.update赋值了更新函数,

instance.update = effect(
    function componentEffect() {
        if (!instance.isMounted) {
            // 初始化:递归执行patch
            patch(...)
        } else {
            // updateComponent
            patch(...)
        }
    },
    __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions
);

第二个参数中一定有个关键属性{scheduler: queueJob}

回顾effect函数,副作用函数的执行都通过run方法调用

核心源码如下

const run = (effect: ReactiveEffect) => {
  if (effect.options.scheduler) {
    effect.options.scheduler(effect);
  } else {
    effect();
  }
};

可以看到存在scheduler时,通过scheduler,也就是通过instance.updatequeueJob方法执行异步更新

3.1. 从源码探究原理

3.1.1. queueJob

  • 作用
    • 不重复地为queue添加任务
    • 调用queueFlush
  • 核心源码
    export function queueJob(job: SchedulerJob) {
      if (
        (!queue.length ||
          !queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) &&
        job !== currentPreFlushParentJob
      ) {
        queue.push(job);
        queueFlush();
      }
    }
    

3.1.2. queueFlush

  • 作用

    • 不重复地执行异步任务
  • 核心源码

    function queueFlush() {
      if (!isFlushing && !isFlushPending) {
        isFlushPending = true;
        currentFlushPromise = resolvedPromise.then(flushJobs);
      }
    }
    

    熟悉Vue2 源码的小伙伴可能会发现,Vue3异步任务变得简洁了许多,

    其中,真正的异步任务完全变成了promise,即基于浏览器的微任务队列来实现异步任务

    const resolvedPromise: Promise<any> = Promise.resolve();
    

3.1.3. nextTick

  • 作用
    • dom 更新完成后 执行自定义方法
  • 核心源码
    export function nextTick(this: ComponentPublicInstance | void, fn?: () => void): Promise<void> {
      const p = currentFlushPromise || resolvedPromise;
      return fn ? p.then(this ? fn.bind(this) : fn) : p;
    }
    
    currentFlushPromisePromise 对象,通过then方法会继续向微任务队列添加方法

3.2. 流程梳理

  • 组件初始化时在setupRenderEffect方法中为instance.update赋值更新函数
  • 当触发setter函数时会执行trigger,取出effect 函数通过 queueJob 执行
  • queueJob将任务加入到queue中,然后执行queueFlush方法
  • queueFlush是真正的异步任务,会不重复地向微任务队列添加任务
  • 当前的同步任务执行完毕后,浏览器会一次性刷新微任务队列,从而完成异步更新

3.3. 思考与总结

  1. Vue3 的异步任务相比 Vue2 变得很简洁,不再兼容低版本浏览器
  2. 真正的异步任务是 Promise 对象的 then 方法

4. patch

在研究 patch 之前,首先我们需要了解下Vue3的编译器优化,因为这直接改变了VNode的结构,为Vue3 patching 算法极高性能做好了基础。

4.1. 编译器带来的优化

  • 4.1.1. 静态节点提升

    将节点分为动态节点和静态节点,静态节点的作用域提升到 render 函数同级,like this,

    const _hoisted_1 = /*#__PURE__*/ _createTextVNode(' 一个文本节点 ');
    const _hoisted_2 = /*#__PURE__*/ _createVNode('div', null, 'good job!', -1 /* HOISTED */);
    
    return function render(_ctx, _cache) {
      with (_ctx) {
        return _openBlock(), _createBlock('div', null, [_hoisted_1, _hoisted_2]);
      }
    };
    

    其中_hoisted_1_hoisted_2就是被提升的静态节点,只在首次渲染时创建,

    在后续更新的时候会绕过这些静态节点

  • 4.1.2. 补丁标记 和 动态属性记录

    当我们在 template 中使用动态属性会被记录下来,like this,

    <child-comp :title="title" :foo="foo" msg="hello"/>
    

    会被 render 函数记录下来

    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      const _component_child_comp = _resolveComponent('child-comp');
    
      return (
        _openBlock(),
        _createBlock(
          _component_child_comp,
          {
            title: _ctx.title,
            foo: $setup.foo,
            msg: 'hello',
          },
          null,
          8 /* PROPS */,
          ['title', 'foo']
        )
      );
    }
    

    可以看到最后的两个参数,8PatchFlags中的一个类型,本质上是一个二进制数字,通过按位与运算可以做组合条件,在这里8表示当前组件有动态变化的props;第二个参数表示动态变化的是哪些props

    在后续diff props的时候只difftitlefoo

  • 4.1.3. block

    如果当前节点下有动态变化的内容则作为一个 block 存储,

    所以在Vue3中,你应常看到的 render 函数会是这种形式

    export function render(_ctx, _cache) {
      return _openBlock(), _createBlock('div', null, [_hoisted_1, _hoisted_2]);
    }
    

    _openBlock 打开区块_createBlock 创建 block,所有的动态变化的子节点会存储到 dynamicChildren中,

    将来diff children的时候只 diff dynamicChildren

  • 4.1.4. 缓存事件处理程序

    如果使用了内联函数,会被缓存到_cache中,在下次更新时直接从_cache中取出来使用,不再重复创建函数,避免前后引用不同导致的不必要渲染

    like this,

    <child-comp @click="toggle(index)"/>
    

    会被编译成

    export function render(_ctx, _cache, $props, $setup, $data, $options) {
      const _component_child_comp = _resolveComponent('child-comp');
    
      return (
        _openBlock(),
        _createBlock(_component_child_comp, {
          onClick: _cache[1] || (_cache[1] = $event => _ctx.toggle(_ctx.index)),
        })
      );
    }
    

    对于内联函数,如果组件发生了重新渲染,前后两个函数的引用不同可能会导致重复更新,这一点在 react 中会很常见,使用 react 时我们会通过useCallback进行优化。但Vue3替我们做了这一点。

    接下来来看一下,patching 算法是如何处理的

4.2. patch

  • 作用

    • 组件渲染和更新的入口
    • 通过调用processXXX执行对应类型的渲染/更新函数
  • 核心源码

    const patch: PatchFn = (
        n1,
        n2,
        container,
        anchor = null,
        parentComponent = null,
        parentSuspense = null,
        isSVG = false,
        optimized = false
      ) => {
        // 若包含patchFlag,则开启优化
        if (n2.patchFlag === PatchFlags.BAIL) {
          optimized = false
          n2.dynamicChildren = null
        }
    
        const { type, ref, shapeFlag } = n2
        // 根据VNode的type决定使用哪种patching算法
        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 if (__DEV__) {
              patchStaticNode(n1, n2, container, isSVG)
            }
            break
          case Fragment:
            processFragment(...)
            break
          default:
            if (shapeFlag & ShapeFlags.ELEMENT) {
              processElement(...)
            } else if (shapeFlag & ShapeFlags.COMPONENT) {
              processComponent(...)
            } else if (shapeFlag & ShapeFlags.TELEPORT) {
              ;(type as typeof TeleportImpl).process(...)
            } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
              ;(type as typeof SuspenseImpl).process(...)
            } else if (__DEV__) {
              warn('Invalid VNode type:', type, `(${typeof type})`)
            }
        }
    
        // set ref
        if (ref != null && parentComponent) {
          setRef(ref, n1 && n1.ref, parentSuspense, n2)
        }
      }
    

    除了TextCommentStaticFragment会通过 type 处理,其他情况都是由 shapeFlag 来决定使用哪种 patching 算法

    这几种算法基本大同小异,本文选取processElement进行分析

4.3. processElement

  • 作用

    • 组件首次渲染:调用mountElement
    • 组件更新:调用patchElement
  • 核心源码

    const processElement = (
      n1: VNode | null,
      n2: VNode,
      container: RendererElement,
      anchor: RendererNode | null,
      parentComponent: ComponentInternalInstance | null,
      parentSuspense: SuspenseBoundary | null,
      isSVG: boolean,
      optimized: boolean
    ) => {
      isSVG = isSVG || (n2.type as string) === 'svg'
      if (n1 == null) {
        mountElement(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      } else {
        patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
      }
    }
    

    n1表示旧的虚拟 domn2表示新的虚拟 dom。目前我们分析patching 算法,所以需要看patchElement方法。

4.4. patchElement

  • 作用

    • Element类型的 VNode 进行 patch
  • 核心源码

    const patchElement = (
        n1: VNode,
        n2: VNode,
        parentComponent: ComponentInternalInstance | null,
        parentSuspense: SuspenseBoundary | null,
        isSVG: boolean,
        optimized: boolean
      ) => {
        const el = (n2.el = n1.el!)
        let { patchFlag, dynamicChildren, dirs } = n2
        // #1426 take the old vnode's patch flag into account since user may clone a
        // compiler-generated vnode, which de-opts to FULL_PROPS
        patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS
        const oldProps = n1.props || EMPTY_OBJ
        const newProps = n2.props || EMPTY_OBJ
        let vnodeHook: VNodeHook | undefined | null
    
        // 1. diff props
        if (patchFlag > 0) {
          if (patchFlag & PatchFlags.FULL_PROPS) {
            // 动态key,需要全量diff
            ...
            // 最终调用的是patchProps
          } else {
            // class是动态属性的情况
            ...
            // 最终调用的是hostPatchProp
    
            // style是动态属性的情况
            ...
            // 最终调用的是hostPatchProp
    
            // 处理dynamicProps中的动态属性
            ...
            // 循环调用hostPatchProp
          }
    
          // 动态text
          ...
          // 最终调用hostSetElementText
        } else if (!optimized && dynamicChildren == null) {
          // 全量diff,即没有优化的情况
          ...
          // 最终调用的是patchProps
        }
    
        // 2. diff children
        if (dynamicChildren) {
          // 动态子节点
          patchBlockChildren(
            n1.dynamicChildren!,
            dynamicChildren,
            el,
            parentComponent,
            parentSuspense,
            areChildrenSVG
          )
        } else if (!optimized) {
          // 全量diff,即没有优化的情况
          patchChildren(
            n1,
            n2,
            el,
            null,
            parentComponent,
            parentSuspense,
            areChildrenSVG
          )
        }
      }
    

    代码量有点多,因为还包含没有优化的情况,没有优化时就和 Vue2 基本相同,

    借助patchFlag,可以通过hostPatchProp实现靶向更新

    借助dynamicChildren,可以通过patchBlockChildren实现按需 diff 子节点, 没有patchChildren时通过patchChildren全量 diff。

    hostPatchProp很简单,只是按照传入的参数进行更新,我们重点关注patchBlockChildrenpatchChildren

4.5. patchBlockChildren

  • 作用

    • 处理block级别的children
  • 核心源码

      const patchBlockChildren: PatchBlockChildrenFn = (
        oldChildren,
        newChildren,
        fallbackContainer,
        parentComponent,
        parentSuspense,
        isSVG
      ) => {
        for (let i = 0; i < newChildren.length; i++) {
          const oldVNode = oldChildren[i]
          const newVNode = newChildren[i]
          // Determine the container (parent element) for the patch.
          const container =
            // - In the case of a Fragment, we need to provide the actual parent
            // of the Fragment itself so it can move its children.
            oldVNode.type === Fragment ||
            // - In the case of different nodes, there is going to be a replacement
            // which also requires the correct parent container
            !isSameVNodeType(oldVNode, newVNode) ||
            // - In the case of a component, it could contain anything.
            oldVNode.shapeFlag & ShapeFlags.COMPONENT ||
            oldVNode.shapeFlag & ShapeFlags.TELEPORT
              ? hostParentNode(oldVNode.el!)!
              : // In other cases, the parent container is not actually used so we
                // just pass the block element here to avoid a DOM parentNode call.
                fallbackContainer
          patch(
            oldVNode,
            newVNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            true
          )
        }
      }
    

    遍历newChildren,即dynamicChildren,对同级的新旧VNode通过patch进行diff,以此类推不断缩小 children 的级别,调用patch

    到一定层级后调用patch就不再有优化选项,最终变成处理新旧两个 children

4.6. patchChildren

  • 作用

    • 选择子节点的patching 算法
  • 核心源码

    const patchChildren: PatchChildrenFn = (
        n1,
        n2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized = false
      ) => {
        const c1 = n1 && n1.children
        const prevShapeFlag = n1 ? n1.shapeFlag : 0
        const c2 = n2.children
    
        const { patchFlag, shapeFlag } = n2
        // fast path
        if (patchFlag > 0) {
          if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
            // 有key的children
            patchKeyedChildren(...)
            return
          } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
            // 没有key的children
            patchUnkeyedChildren(...)
            return
          }
        }
    
        // children has 3 possibilities: text, array or no children.
        // children 有三种可能:文本,数组,或没有children
        ...
      }
    

    PatchFlags.KEYED_FRAGMENTPatchFlags.UNKEYED_FRAGMENTchildren 是否包含 key 的依据,根据是否包含key选择 patchKeyedChildrenpatchUnkeyedChildren

    其中patchKeyedChildren是对带有 key 的 children 的处理方法。

4.7. patchKeyedChildren

  • 作用

    • diffkey 的子节点
  • 核心源码

    const patchKeyedChildren = (
        c1: VNode[],
        c2: VNodeArrayChildren,
        container: RendererElement,
        parentAnchor: RendererNode | null,
        parentComponent: ComponentInternalInstance | null,
        parentSuspense: SuspenseBoundary | null,
        isSVG: boolean,
        optimized: boolean
      ) => {
        let i = 0
        const l2 = c2.length
        let e1 = c1.length - 1 // prev ending index
        let e2 = l2 - 1 // next ending index
    
        // 1. 掐头
        // (a b) c
        // (a b) d e
        while (i <= e1 && i <= e2) {
          const n1 = c1[i]
          const n2 = (c2[i] = optimized
            ? cloneIfMounted(c2[i] as VNode)
            : normalizeVNode(c2[i]))
          if (isSameVNodeType(n1, n2)) {
            patch(...)
          } else {
            break
          }
          i++
        }
    
        // 2. 去尾
        // a (b c)
        // d e (b c)
        while (i <= e1 && i <= e2) {
          const n1 = c1[e1]
          const n2 = (c2[e2] = optimized
            ? cloneIfMounted(c2[e2] as VNode)
            : normalizeVNode(c2[e2]))
          if (isSameVNodeType(n1, n2)) {
            patch(...)
          } else {
            break
          }
          e1--
          e2--
        }
    
        // 3. 掐头去尾后,若新节点有剩余则新增,旧节点有剩余则删除
        // (a b)
        // (a b) c
        // i = 2, e1 = 1, e2 = 2
        // (a b)
        // c (a b)
        // i = 0, e1 = -1, e2 = 0
        if (i > e1) {
          if (i <= e2) {
            const nextPos = e2 + 1
            const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
            while (i <= e2) {
              patch(...)
              i++
            }
          }
        }
    
        // 4. 旧节点有剩余则删除
        // (a b) c
        // (a b)
        // i = 2, e1 = 2, e2 = 1
        // a (b c)
        // (b c)
        // i = 0, e1 = 0, e2 = -1
        else if (i > e2) {
          while (i <= e1) {
            unmount(c1[i], parentComponent, parentSuspense, true)
            i++
          }
        }
    
        // 5. 掐头去尾后,如果中间剩余的是不相同的节点,则diff
        // [i ... e1 + 1]: a b [c d e] f g
        // [i ... e2 + 1]: a b [e d c h] f g
        // i = 2, e1 = 4, e2 = 5
        else {
          ...
        }
      }
    

    该过程在源码中分为 5 步,但可以优化提取下,分成三大步

    1. 首先一次性找到所有相同的开头,该过程称为掐头
    2. 然后一次性找到所有相同的结尾,该过程称为去尾
    3. 掐头去尾后的处理,该过程称为扫尾
      • 3.1. 新旧节点有一个空了
        • 3.1. 若新数组有剩余就新增,
        • 3.2. 若旧数组有剩余就删除
      • 3.2. 新旧节点都有剩余
        • diff 两个新旧节点两个剩余的 children

    举个例子或许会更形象些

    1. 新增子节点的情况

          const oldChildren = [a, b, c, d];
          const newChildren = [a, e, f, b, c, d];
      
          // 1. 掐头
          const oldChildren = [b, c, d];
          const newChildren = [e, f, b, c, d];
      
          // 2. 去尾
          const oldChildren = [];
          const newChildren = [e, f];
      
          // 3. 掐头去尾后,新旧节点有一个空了,则批量增加[e, f]
          ...
      
    2. 更改部分子节点的情况

      const oldChildren = [a, g, h, b, c, d];
      const newChildren = [a, e, f, b, c, d];
      
      // 1. 掐头
      const oldChildren = [g, h, b, c, d];
      const newChildren = [e, f, b, c, d];
      
      // 2. 去尾
      const oldChildren = [g, h];
      const newChildren = [e, f];
      
      // 3. 掐头去尾后,新旧节点都没空,对[g, h]、[e, f]这两个children进行diff
      const oldChildren = [g, h];
      const newChildren = [e, f];
      

5. 思考、总结与补充

  1. Vue3 大量运用工厂模式

    像 createAppAPI、createRenderer 都是通过工厂模式返回一个真正的函数,这些方法基本都在源码的core 包里面,在这里的目的是为了更好地跨平台,工厂函数只提供核心处理方法,具体的平台方法由模块声明。可以更好地跨平台开发,避免再出现开发Weex要直接修改源码这种情况。

  2. 渲染器的概念及应用

    渲染器是一个对象,是 Vue3 源码中的一个核心概念,渲染器内部包含三个方法,renderhydratecreateApp

    我们如果要做自定义渲染可以通过创建自定义渲染器来实现。

  3. 编译器相关

    • 编译器做了哪些事情?

      通过parsetransformgeneratetemplate转成render 函数

    • 编译发生在哪个阶段?

      • 如果是webpack环境,则在开发环境下通过vue-loader完成编译工作。到了生产环境只保留vueruntime文件
      • 如果是携带了compilerruntime版本,则组件初始化执行mount方法时,最终调用setupComponent完成编译
    • 编译阶段做的优化

      • Block Tree
      • PatchFlags
      • 静态提升、静态 PROPS
      • Cache Event handler
      • 预字符串化
      • 如何实现靶向更新
  4. 位运算

    位运算是在数字底层(即表示数字的 32 个数位)进行运算的。由于位运算是低级的运算操作,所以速度往往也是最快的。

    可以很快地处理组合条件,比如项目中如果有多角色叠加权限的场景就可以使用位运算来解决。

  5. diff

    Vue3 真正的 diff 算法变化并不大,可以分成三大步骤

    • 掐头、
    • 去尾、
    • 扫尾
      • 批量增/删
      • diff

    真正让 Vue3 diff 快到飞起的原因是编译器做了很多优化。

  6. 关于数据响应式

    不仅仅是Object.definePropertyproxy的改变,还有一个变化是子数据懒观察

  7. composition-api 及 hook

    很像Reacthook,Vue3 的 hook 没有新旧值不同的心智负担,而且副作用函数可以嵌套使用,但存在一些是否应该解构的心智负担。

    composition-api相比 Vue2 的options API可以更好地复用逻辑,具体的奥秘可以在官网查看~,强烈推荐!!!