源码系列:Vue3深入浅出(一)

avatar
@智云健康

作者: 徐จุ๊บ,未经授权禁止转载。

前言

Vue.js 3.0 (以下简称Vue3),正式发布在 2020 年 09 月 18 日,Vue3 Beta版本更是早在今年四月份就发布。相信有些同学早已上手体验一番。或者早已看过官方文档,英文不好的同学阔以点击这里,这是个中文文档。😂😂😂😂😂😂

想来大家肯定会对 Vue2 源码,或者是核心功能的实现有一定的了解,本文就是以我学习 Vue2 的方式,来和大家一同交流、学习 Vue3 的源码,目前版本是3.0.2。如果有不对、遗漏之处,还望指正、补充。


项目目录

目录结构

先回顾下 Vue2 源码目录

├── src
  ├── compiler    #  编译相关的模块
  ├── core        #  vue核心代码
  ├── platforms   #  平台相关
  ├── server      #  服务端渲染相关
  ├── sfc         #  vue单文件组件
  ├── shared      #  内容公用方法

再看看 Vue3 源码内的目录结构

├──packages
  ├── compiler-core       
  ├── compiler-dom        
  ├── compiler-sfc       
  ├── compiler-ssr     
  ├── reactivity         
  ├── runtime-core       
  ├── runtime-dom         
  ├── vue
  ├── shared
  ├── ...            

可以选一些来讲一下这些模块功能

  • compiler-core: 与平台无关的编译模块,例如基础的 baseCompile 编译模版文件, baseParse生成AST
  • compiler-dom: 基于compiler-core,专为浏览器的编译模块,可以看到它基于baseCompile,baseParse,重写了complie、parse
  • compiler-sfc: 用来编译vue单文件组件
  • compiler-ssr: 服务端渲染相关的
  • reactivity: vue独立的响应式模块
  • runtime-core: 也是与平台无关的基础模块,有vue的各类API,虚拟dom的渲染器
  • runtime-dom: 基于runtime-core,针对浏览器的运行时
  • vue: 引入导出 runtime-core,还有编译方法

可以看到 Vue3 模块拆分的清晰,模块相对独立,我们可以单独引用 reactivity 这个模块,也可以引用 compiler-sfc 在我们自己开发的 plugin 中去使用它,例如 vue-loader , vite 都有使用。

monorepo

这是因为Vue3采用 monorepo 是管理项目代码的方式。不同于 Vue2 代码管理,它是在一个 repo 中管理多个package,每个 package 都有自己的类型声明、单元测试。 package 又可以独立发布,总体来说更便于维护、发版和阅读的。

从入口开始

创建 Vue 实例

回顾下 Vue2 创建一个 b Vue 实例

import Vue from 'vue';
import App from './App.vue';

const vm = new Vue({
  /* 选项 */
}).$mount('#app');

Vue3 中创建一个应用实例

import { createApp } from 'vue';
import App from './App.vue';

const app = Vue.createApp({
  /* 选项 */
});

const vm = app.mount('#app');

可以看到两者差别在于,Vue2 中每个 Vue 应用都是通过 new 一个 Vue 构造函数创建一个新的 Vue 实例。而在 Vue3 中呢,每个 Vue 应用都是通过用 createApp 函数创建一个新的应用实例开始的。

本质是虽然没有区别,但却有这其他方面的积极作用,接着上面的例子,假如我们需要注册一个全局组件,或者更改全局的配置。

/* Vue2 的做法 */
Vue.component('my-component', {
  /* 选项 */
});
Vue.directive('my-directive', {});
Vue.mixin({
  /* ... */
});

/* Vue3 的做法 */
const app = Vue.createApp({
  /* 选项 */
});
app.component('my-component' /* 组件 */); // 每个方法都是可链式的
app.directive('my-directive' /* 指令 */);
app.mixin();

Vue2 中使用 Vue 的 API、配置可以全局改变 Vue,是很方便,但对于同一页面通过同一个 Vue 构造函数实例多个 app 情况很不友好。例如

const app1 = new Vue({}).$mount('#app1');
const app2 = new Vue({}).$mount('#app2');

Vue3 只会改变当前配置的应用实例,有效的避免了这个问题。

源码入口

接下来就从import { createApp } from 'vue'; 这句开始源码的学习。

在 源码里 vue 模块,我们可以看到,vue 这个模块其实大致就做了引入导出runtime-dom,complier。继续在 runtime-dom 中寻找 createApp

export const createApp = ((...args) => {
  // 可以看到真正的createApp 方法是在渲染器属性上的
  const app = ensureRenderer().createApp(...args)
  // ...
  const { mount } = app
  app.mount = (containerOrSelector: Element | string): any => {
    // ...
  }

  return app
}) as CreateAppFunction<Element>

/**
 * ensureRenderer 这里是为了执行createApp时,才给renderer渲染器赋值,也是优化的一点。
 * 只导入reactive, 没有执行createApp,不会执行 createRenderer,
 * 那么打包时 tree-shaking 可以摇掉 runtime-core 这个模块。
 * */
function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}

runtime-dom/index.ts 里可以发现对 createApp 的定义,里面的实现大致分为三步步,创建 app 应用实例,改写mount方法, 返回 app 应用实例。

接着我们去导出这个方法的 runtime-core 模块中看看 createRenderer,并找到真正的 createApp

// 1
export { createRenderer } from './renderer'

// 2
export function createRenderer (options) {
  return baseCreateRenderer(options)
}

// 3
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
) {
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    forcePatchProp: hostForcePatchProp,
    createElement: hostCreateElement,
    createText: hostCreateText,
    createComment: hostCreateComment,
    setText: hostSetText,
    setElementText: hostSetElementText,
    parentNode: hostParentNode,
    nextSibling: hostNextSibling,
    setScopeId: hostSetScopeId = NOOP,
    cloneNode: hostCloneNode,
    insertStaticContent: hostInsertStaticContent
  } = options

  // ...

  return {
    render,
    hydrate,
    createApp: createAppAPI(render, hydrate)
  }
}

// 4
export function createAppAPI(render, hydrate) {
  return function createApp(rootComponent, rootProps = null) {
    // ...
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,
      version,

      use (plugin) {
        // ...
        return app
      },
      mixin(mixin: ComponentOptions) {
        // ...
        return app
      },
      mount(rootContainer: HostElement, isHydrate?: boolean): any {
        if (!isMounted) {
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )

          vnode.appContext = context

          if (isHydrate && hydrate) {
            // ...
          } else {
            render(vnode, rootContainer)
          }
          isMounted = true
          app._container = rootContainer
          ;(rootContainer as any).__vue_app__ = app

          return vnode.component!.proxy
        }
      },
      // ...
    }
  }
  • 注:上面代码我省略了很多与创建 pp 实例无关的代码,我觉得理清思路看源码比逐行去阅读要清晰一些。

辗转四次我们终于在 runtime-core/apiCreateApp 里找到 createApp 方法, 以及 app 实例。 可以看到 app 实例 和 Vue2 里 Vue 构造函数上的 API 基本一致的。app 实例上例如 usecomponent等最后都会返回 app 实例,支持链式写法。

从 createApp 方法的调用到 app 实例创建这个过程,其实我们大致可以看到runtime-dom 这个模块怎么基于 runtime-core 构建对于浏览器的虚拟 dom 渲染器。

/**
 * 在 runtime-dom 里,调用 runtime-core 的 createRenderer 方法
 * 并传入 rendererOptions,这个 rendererOptions 里面其实包含着浏览器的DOM API,props
 * 例如 createElement、insertBefore 等,大家可以去 runtime-dom/nodeOps.ts 里面看看。
 * **/
function ensureRenderer() {
  return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}

/**
 * 传入不同环境的 endererOptions,就可以生成不同环境的render
 * **/
function baseCreateRenderer(
  options: RendererOptions,
  createHydrationFns?: typeof createHydrationFunctions
) {
  // 这些变量最终都是为 redner 里 patch 服务的
  const {
    insert: hostInsert,
    remove: hostRemove,
    patchProp: hostPatchProp,
    createElement: hostCreateElement,
    // ...
  } = options

  // 此处生成对应浏览器环境的 render
  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
  }

  return {
    render,
    hydrate,
    // 以参数形式 传入 createApp 中, 最终供 app实例里的 mount 使用。
    createApp: createAppAPI(render, hydrate)
  }

我们继续看看 runtime-core/apiCreateAppcreateApp 方法

export function createAppAPI(render, hydrate) {
  return function createApp(rootComponent, rootProps = null) {
    // ...
    const app: App = (context.app = {
      // ...
      /**
       *  我们在项目里创建 app实例,再 mount 到某一节点
       *  最终会执行到这里,App 组件作为 rootComponent, render是浏览器环境的渲染器。
       * **/
      mount(rootContainer: HostElement, isHydrate?: boolean): any {
        if (!isMounted) {
          const vnode = createVNode(
            rootComponent as ConcreteComponent,
            rootProps
          )
          vnode.appContext = context

          if (isHydrate && hydrate) {
            // ...
          } else {
            render(vnode, rootContainer)
          }
          isMounted = true
          app._container = rootContainer
          ;(rootContainer as any).__vue_app__ = app

          return vnode.component!.proxy
        }
      }
      // ...
    }
  }

当然,我们在项目里.mount('#app') 并不是直接执行 app 实例里的 mount,这里 mount 方法 是 runtime-core 里与平台无关。事实上 runtime-dom 有重写过 mount,亦是针对浏览器环境。

但是这块,这篇文章不会继续说下去,因为 mount 过程大致为创建 Vnode,渲染,生成真实 DOM,其中有用到 Vue3 中新的响应式,所以我们可以先看看这个可以独立使用,不会有其他包袱的模块 reactivity。 (给自己挖个坑,后面会说的 -。-|)


Vue3 的响应式

Proxy

我们知道在 Vue2 里内部通过 Object.defineProperty API 劫持数据的变化,深度遍历 data 函数里的对象,给对象里每一个属性设置 gettersetter

触发 getter 会通过 Dep 类做依赖收集操作,收集当前 Dep.target, 也就是 watcher

触发 setter,会做派发更新操作,执行 dep.notify 通知收集到的各类 watcher 更新,如 computed watcheruser watcher渲染 watcher


Vue3Proxy 重构了响应式部分,effect 副作用函数 代替了 watcher

Proxyget handle 里 执行track() 用来跟踪收集依赖(收集 activeEffect,也就是 effect ),

set handle 里执行 trigger() 用来触发响应(执行收集的 effect)

独立的响应式

之前提过很多次,reactivity 是可以独立使用,例如我们在 node 中使用。

// index.js
const { effect, reactive } = require('@vue/reactivity');
// reactive 定义响应式数据,也就是用proxy 设置 get、set handle
const obj = reactive({ num: 1 });

// effect 定义副作用函数
effect(() => {
  console.log(obj.num);
});

// 修改num, trigger 触发响应,执行 effect
setInterval(() => {
  ++obj.num;
}, 1000);

node index.js, 运行这段脚本就可以看到控制台会一直递增打印。

reactive

那接下来我们就跟着这段代码来看看 @vue/reactivity 里的 reactive, effect吧。后面还是只关注我们目前所关心的,这样个方向的主流程走完之后再梳理其余方法。

先简单介绍写 ReactiveFlags 这个枚举,因为后面也会用到

export const enum ReactiveFlags {
  SKIP = '__v_skip',  // 这个属性值为 true 的对象 都会被跳过代理
  IS_REACTIVE = '__v_isReactive', // 获取是否是响应式
  IS_READONLY = '__v_isReadonly', // 是否是只读的
  RAW = '__v_raw' // 这个属性会应用到原始对象
}

进入查看 reactive 方法的正题。

export function reactive(target: object) {
  // 如果是只读的响应式数据,直接会返回本身哈
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target, // 对象
    false,  // 是否只读
    mutableHandlers, // proxy handle
    mutableCollectionHandlers  // 集合数据的 proxy handle
  )
}

function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  if (!isObject(target)) {
    // 不是对象 直接返回
    return target
  }

  // 如果已经是响应式对象则直接返回, 除非是 readonly 作用在这个响应式对象上
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }

  // 一个缓存map,key是 target对象, value是响应式对象
  // 如果这个对象已经创建过响应式对象,则从 缓存map中读出返回
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }

  // 带有 skip 标记 、被冻结等至不可扩展的,类型不是object array map set weakmap weakset 都在白名单之外,不创建代理
  const targetType = getTargetType(target)
  if (targetType === TargetType.INVALID) {
    return target
  }

  // 使用 proxy 来创建响应式对象
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 存入 缓存map 中
  proxyMap.set(target, proxy)
  return proxy
}

baseHandlers 里面劫持了哪些操作呢?

// reactivity/baseHandlers.ts => mutableHandlers
// 这里我们是选择了普通对象的handlers来看的
export const mutableHandlers: ProxyHandler<object> = {
  get, // 访问对象属性的handler
  set, // 设置对象属性的handler
  deleteProperty, //删除对象属性handler
  has, // 针对 in 操作符的handler
  ownKeys // 对象上getOwnPropertyNames、getOwnPropertySymbols、keys 等方法的handler
};

使用 Proxy 优势在于哪,我相信很多人就算没有看过源码,也都了解一些。例如Proxy 弥补了 Object.defineProperty 需要递归对象,给每一个属性设置 settergetter,没法劫持一些其他操作,数组也需要 hack,处理新增属性需要额外的方法,Map、Set 、weakMap 等数据结构无法响应式等不足。

总结

上述一些,是使用 Proxy 带来的优化,后面我们会看看代码实现里又做了哪些其他优化。 至此我们了解了例子中第一句, 先执行 reactive, 用proxy 代理了我们传入的原始对象,返回了一个这个proxy,就叫做响应式对象吧。 最后我们把返回的响应式对象赋值给 obj

effect

接着上面例子的顺序,再来看这一句

  effect(() => {
    console.log(obj.num);
  });

我们给 effect 方法里传入了一个函数 () => { console.log(obj.num); } , 函数里访问了响应式对象 obj 的 num 属性。那我们来看看 effect 的源码吧。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    // 1. 如果 fn 有 effect 函数标示,就指向原始函数,在下面 createReactiveEffect 里就可以看到raw、_isEffect的定义
    fn = fn.raw
  }
  // 2. 创建一个响应式副作用函数
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    // 3. 执行effect, 这个有没有像 computed watcher 的 lazy 属性 ,若为true就不立即执行,
    effect()
  }
  // 4. 返回包裹着 fn 的 effect 函数
  return effect
}

let uid = 0

const effectStack = [] // effect栈,记得 Vue2 里面 全局存 Watcher 的栈吗?
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)) {
      // 这是一个优化,清除的 deps 里所有 dep 里的 effect,配合后面 track 里给 effect deps重新加 dep,相当于清除掉不需要的依赖,后面会详细说到 
      cleanup(effect)
      try {
        // 开启允许收集 也就是设这个变量shouldTrack为 true
        enableTracking()
        // 以下是压栈, 设置activeEffect,执行原始函数
        effectStack.push(effect)
        activeEffect = effect
        return fn() // 这里执行原始函数,就是引用到我们在函数里写的 响应式的对象的值,触发的响应式对象的 get handler
      } finally {
        // 最终出栈,停止收集,activeEffect 回指上一个effect,这里是对有嵌套关系的effect有作用
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  // 下面是effect的相关属性
  effect.id = uid++
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = [] // effect 对 dep 的双向依赖
  effect.options = options
  return effect
}

function cleanup(effect: ReactiveEffect) {
  // deps 是一个 数组包着 Set集合的数据结构   [Set1(...), Set2(...), ...], 每一个 Set 就是,targetMap里面的dep
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}

总结

至此我们知道了,例子中执行 effect(fn),会创建、执行、并返回一个 effect 函数,执行这个 effect 函数时,开启收集开关,压入全局 effectStack 栈中,将全局 activeEffect 指针指向自己,并执行传入的 fn,这时候触发了 obj 的 get handler。执行完 fn 后,执行退栈, 停止收集,activeEffect 指向栈中到的上一个 effect。

get, 依赖收集

上面说到执行传入的 fn,这时候触发了 obj 的 get handler,那我们看看 get 的源码。

function createGetter(isReadonly = false, shallow = false) {
  return function get(target: Target, key: string | symbol, receiver: object) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return !isReadonly
    } else if (key === ReactiveFlags.IS_READONLY) {
      return isReadonly
    } else if (
      key === ReactiveFlags.RAW &&
      receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)
    ) {
      return target
    }
    // 还记得 ReactiveFlags 枚举里面的那些值吗, 都是通过上面那些判断来获取到相应的值,也因为都是私有属性,得到值直接返回即可,没必要往下走了

    const targetIsArray = isArray(target)
    // ['includes', 'indexOf', 'lastIndexOf'] 数组改变,这方法的结果可能也会发生改变,所以 get 里面做了特殊处理
    // 例如 执行arr.includes('xx') 时,会跟踪 arr 数组每一个下标
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver)
    }

    // 利用Reflect映射返回值
    const res = Reflect.get(target, key, receiver)

    if (
      isSymbol(key)
        ? builtInSymbols.has(key as symbol)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      // 判断原生方法等,直接返回,不track了
      return res
    }

    if (!isReadonly) {
      // track 依赖收集操作
      track(target, TrackOpTypes.GET, key)
    }

    if (shallow) {
      // 这是表示浅响应的,比如 shallowReactive 方法,后面也会简单介绍下
      return res
    }

    if (isRef(res)) {
      // 这里 ref 是 reactivily 里面另一个api,实现相对简单,可以对基本类型创建个响应式对象,例如 num = ref(0) 会创建一个 value为0的响应式对象, isRef 就是判断是不是 ref 值
      const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
      return shouldUnwrap ? res.value : res
    }

    if (isObject(res)) {
      // 可以看到哈,子对象也是需要递归的去劫持的,但这里相比 Vue2 有个优化的点,
      // Vue2里面如果属性仍是个对象 数组等,则立即遍历子对象去做劫持
      // 而 Vue3 则是在访问到这个属性,发现值是对象再去转为响应式对象

      return isReadonly ? readonly(res) : reactive(res)
    }

    return res
  }
}

上述呢是完整的 get handler了,可以发现get操作里面先是对 ReactiveFlags 这个枚举里面的值做了劫持,接着对数组里面特殊方法单独求值、处理,然后使用 Reflect 这个好搭档来求值,再 track 来做依赖收集相关的事情,最后返回结果。 那我们来看一看 track。

// ./effect.ts

// 先看两个使用到的变量
activeEffect // 类似于 Vue2 里的 Dep.target, 一个watcher。 这里表示当前激活的effect
shouldTrack // 判断当前是否应该收集 ,effect函数内部执行一开始设这个变量为true
targetMap // 以原始对象为健 ,值也是一个weakMap,map以属性名为key,effect集合为value
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!shouldTrack || activeEffect === undefined) {
    return
  }

  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // 以上这一堆判断,缓存,最终会形成 targetMap 样的数据结构
  /** 
   *  targetMap = {
   *    [target]: {
   *      [key]: new Set([ effect,... ])
   *    }
   *  }
  **/

  if (!dep.has(activeEffect)) { // effect 执行时,将 activeEffect 指向自己
    dep.add(activeEffect)
    // 当前激活的 effect 也会存储 dep集合,这其实是配合 effect 里面 cleanup 方法 清除不需要的依赖
    activeEffect.deps.push(dep)
  }
}

为什么说 activeEffect.deps.push(dep) 是配合 cleanup 清除不需要的依赖,来看下面例子

// 例2: 多了一个 count 属性
const obj = reactive({num: 1, count: 0});

effect(() => {
  if (obj.num === 1) {
    ++obj.num
    // 我们只有在 num 为 1 的是时候再去访问 count
    console.log('count', obj.count)
  }
  console.log('num', obj.num)
},);

setTimeout(() => {
  // 因为第一次访问到了 count, 所以 我们修改 count 会去执行 effect
  console.log('第一次修改 count')
  obj.count = 2
}, 1000);

setTimeout(() => {
  // 上一次执行effect的时候,cleanup清除了所有依赖,但因为 num 为 2
  // 函数内部不会访问到 count ,并未去 track count,也就不会有重新 activeEffect.deps.push(dep) 这个操作
  // 所以修改 count 并不会执行 effect
  console.log('第二次修改 count')
  obj.count = 3
}, 1000);

setTimeout(() => {
  // num 每次都有访问到,所以正常触发响应式
  console.log('第三次修改 num')
  obj.num = 3
}, 1000);

从这个例子来说第一次执行 effect,是自动执行的,effect.deps 值就会是 [dep, dep],分别是 num 的 dep 集合,以及 count 的 dep 集合。

接着修改 count 触发执行 effect,先执行 cleanup,会清除 num和count dep 集合里的effect,

执行 fn 时候,因为只有访问 num,num 的 dep 又会加上 effect,但 count 的 dep 不会了。而且 effect.deps 只会 push num 的 dep。

所以之后修改 count 的时候不会 就不会在触发 effect了。

总结

最后回归主流程 ,在最开始的例子中我们在传入的 fn 中访问量 obj.num ,触发 get handler,它会通过 Reflect.get 得到值 1,接着 track 咱们的 num 属性,收集effect,最后 targetMap 的结构会如下

targetMap = {
  [obj]: {
    'num' : new Set([effect])
  }
}

set, 派发更新

上一节依赖收集流程以及走完。其中我们说到修改 num 会触发 effect 执行, 其实就是派发更新,也就是 执行 set handler。 来看看 set handler 源码

// ./baseHandlers.ts

function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // 1. 先取old值
    const oldValue = (target as any)[key]
    if (!shallow) {
      value = toRaw(value) // toRaw 就是取原始对象,这里如果value是响应式对象,则一直取到原始对象
      if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
        // 如果老的值是 Ref 响应对象,而更新的值不是,那更新老的响应式对象的 value 属性值即可,不需要执行这里的trigger
        oldValue.value = value
        return true
      }
    } else {
      // in shallow mode, objects are set as-is regardless of reactive or not
    }
    
    // 2. 判断当前 set 的 key 存不存在与 target上
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)

    // 3. Reflect.set 求值 
    const result = Reflect.set(target, key, value, receiver)

    // 这里原始数据上原型链上数据操作,Reflect.set修改后,会再进来,所以有了这判断
    if (target === toRaw(receiver)) {
       // 4. 通过有没有当前 key 是不是已存在来决定是 add 的 triger,还是 set 的 trigger,set会多一个oldvalue
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

set的逻辑看完了,注释里面大致标注为4步,来看看最后一步里的 trigger

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  // 从之前 track 里面存储的 targetMap 里取出对应 depsMap
  const depsMap = targetMap.get(target)

  if (!depsMap) {
    // 没有依赖,直接返回,不会触发后面effect的执行
    return
  }

  const effects = new Set<ReactiveEffect>()
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) { // 这是包含 effects 的 Set集合
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          // add 方法是要把 effect 统一 收集到 effects 这个集合里
          effects.add(effect)
        }
      })
    }
  }

  // ...
  add(depsMap.get(key)) // 把dep集合放到effects里
  // ...

  const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) {
      // 一个调度器,可以去做排序、去重、放入 nexttick 中异步执行
      // 这个调度器我们也是可以自定义的
      effect.options.scheduler(effect)
    } else {
      // 否则直接执行
      effect()
    }
  }

  // 开始执行
  effects.forEach(run)
}

总结

至此,我们了解派发更新这块基础的流程,按照最开始的例子, 我们修改 num ,触发 set handler,把 targetMap 里 num 对应的 dep 取出来。通过 add 方法把 dep 里的 effect 加入到 effects 这个大的集合里。最后执行 run 方法遍历执行 effects 里的 effect。

总结

到此为止,咱们学习 Vue3 开头算是结束了。因为初学,我们不能做到面面俱到,也不能理解每一句代码的作用,只能抓住最关心,最基础的流程。就像一颗树一样,我们了解完最基础的主干之后,才能继续探索其他分支。

例如这次重点分析响应式里还有很多其他API,RefshallowReactive,、readonly shallowReadonly等等,但是我觉得我们通过 reactive 了解完整个响应式的 副作用函数,依赖收集、派发更新基础后,再回头看这个API会轻松不少。

ps:这次我们了解了基础的响应式,可以再去学学 Vue3 渲染逻辑,composition API等,后面也会把其他模块学习的过程给写下来,有不对之处,还请指正,谢谢!