# 深入理解vue响应式原理

# 写在前面

TIP

一切黑魔法的背后都是1+1

众所周知, 目前每个前端开发都会接触过一到两个框架. 或许是 react, 或许是 vue, 又或许是 angular.我们在使用这些框架的同时又有没有想过, 在这些看似很魔幻的功能背后到底隐藏着什么东西, 它又是如何运作的? 今天我们就针对 vue 最核心的响应式原理做一次深入的剖析.

那么什么是响应式呢? 我们知道在 react 中, 如果你想更新你的视图, 那么你需要在改变状态之后手动去调用 react 提供的 setState 方法. 而在 vue 里面, 我们修改 data 之后并不需要我们手动去调用什么方法通知 vue 去更新视图, 因为 vue 能够感知到某个状态被改变了, 从而主动的去更新视图, 对于开发者而言, 这可能更能让人接受, 而主动感知并更新视图的这个过程, 其实就是响应式.

继续用大白话来解释就是下面这个过程:

  1. vue 在初始化的过程中会对某些特定的数据进行劫持. 什么叫做劫持呢, 就是你对这个数据的任何读取, 赋值操作都会经过 vue 才能成功. 例如我们声明的 data, 当我们读取 data 里面的数据时, 必须经过 vue, 然后 vue 再给我们返回想要的数据, 去给 data 里面的数据赋值时, 也需要经过 vue, 让 vue 去帮助我们赋值. 整个劫持操作在 vue3 里面通过 Proxy 去实现, 我们后面会介绍到它, 现在大家只需要知道 vue 会这么干就行了.

  2. 那我们什么时候需要更新数据? 我们在 template 里面用到数据的时候, 一定会去读取 data 里面的值, 这个时候这个信息就被 vue 捕获到了, 同时, 这时候 vue 还能知道你是在哪个 template 里面用的这个值, 并将其一并记录下来.

  3. 当我们去改变 data 里面的值时, vue 又知道了, 同时他还记录了这个值对应的 template, 所以自然就会去主动更新你的视图, 完成整个更新流程.

例如下面这段代码:

<template>
  <div>{{msg}}</div>
  <button @click="change">change</button>
<template/>

<script>
  export default {
    data() {
      return {
        msg: 'hello world'
      }
    },

    methods: {
      change() {
        this.data.msg = 'hello world!!!!'
      }
    }
  }
</script>

当这段代码被解析并初次渲染时, vue 会经过下面这几步:

当我们更新 data 的时候, 是下面的流程:

这就是 vue 的响应式过程. vue3.0 已经把响应式相关的函数都抽到了 @vue/reactivity 中, 官网文档 (opens new window)在此. vue 里面所有和响应式相关的操作都通过这些函数来完成, 下面我们将顺着官网的介绍, 一个个对他们进行更加详细的介绍, 大家可以将该文档作为官网的补充文档进行阅读. 同时也推荐大家对于 vue3Reactivity API 有一定了解之后再来看这篇文章, 收获会更多.

# 响应式基础 API

# reactive

该方法就是返回一个对象的响应式副本, 他的响应式转换是深层的——它影响所有嵌套 property.在基于 ES2015 Proxy 的实现中, 返回的 proxy 是不等于原始对象的.

这个就是我们之前所说的 vue 劫持 data 的方法. 它的内部是通过 Proxy 实现的, 具体用法可以参考MDN (opens new window). 在这里我们可以简单写一个demo.

const obj = {name: 'klx'};
const proxyObj = new Proxy(obj, {
  get(target, key, receiver) {
    console.log("get: ", target, key, receiver);
    return Reflect.get(target, key, receiver);
  },

  set(target, key, value, receiver) {
    console.log("set: ", target, key, value, receiver);
    return Reflect.set(target, key, value, receiver);
  }
})

proxyObj.name = 'hello world';
// set:  { name: 'klx' } name hello world { name: 'klx' }

console.log(proxyObj.name);
// get:  { name: 'hello world' } name { name: 'hello world' }
// hello world

通过传递 getset 方法给 Proxy, 我们就能劫持到 obj 对象的读取和赋值操作, 进而能够在这些阶段做一些我们想做的事情.

我们可以看到这里面还使用了一个 Reflect, 这又是个啥玩意? 我们还是来看看MDN (opens new window). 在 proxy 中使用它来操作 target 主要是为了获取正确的上下文引用. 举个例子:

const parent = {
  _name: "parent name",
  get name() {
    return this._name;
  }
};

const parentProxy = new Proxy(parent, {
  get(target, prop, receiver) {
    // 正确写法
    //return Reflect.get(target, prop, receiver);

    // 错误写法
    return Reflect.get(target, prop);
  }
});

const child = {
  _name: "child name"
};

Object.setPrototypeOf(child, parentProxy);

console.log(child.name);
// parent name
// 想要拿到 child name, 当时却拿到了 parent name

在这个例子中, 我们的本意是想拿到 child name, 但是最后却拿到了 parent name, 这是因为 this 对象指向了 parent, 但是我们调用的时候明明是 child. 这个时候就出现了 this 指向错误的问题, 好在 Reflect.get 方法接受一个 receiver 参数用来修改 getter 调用时候的 this 指向, 所以这样我们才能拿到我们真正想要的值.

除此之外, 还有几个问题需要大家注意一下:

1. proxy 只能代理对象

Proxy 能够代理的对象仅限于 Object(Array, Map, Set, WeakMap, WeakMap等), 所以对于 NumberString 等类型, 它就无能为力了. 比如下面这段代码就会报错:

new Proxy(1, {});
// TypeError: Cannot create proxy with a non-object as target or handler

而我们在使用 vue 的过程中肯定会出现一些非对象类型, 那么这个时候会怎么处理? 凭直觉来说, 肯定是再做一层包装, 将其包装成对象再进行代理. 实际上 vue 也是这么干的, 这一块内容我们会在介绍Ref的时候提到.

2. 解构会使该 proxy 失去响应性

我们看这个例子:

const obj = new Proxy({name: 'klx'}, {
  get(target, key) {
    console.log("get", target, key);
    return target[key];
  }
});

obj.name;
// get { name: 'klx' } name

const { name } = obj;
// get { name: 'klx' } name

name
// nothing

现在我们再使用 name 是不会触发 get 函数的. 当然, 如果 name 本身就是一个具有响应性的对象, 那么它本身仍然具有响应性, 但是读取 name 的时候不会触发初始对象的响应性. 比如下面这个例子:

const message = new Proxy({name: 'klx'}, {
  get(target, key) {
    console.log("message get: ", target, key);
    return target[key];
  }
});

const obj = new Proxy({proxyObj: message}, {
  get(target, key) {
    console.log("obj get: ", target, key);
    return target[key];
  }
});

obj.proxyObj;
// obj get:  { proxyObj: { name: 'klx' } } proxyObj

const { proxyObj } = obj;
// obj get:  { proxyObj: { name: 'klx' } } proxyObj

proxyObj;
// nothing

proxyObj.name;
// message get:  { name: 'klx' } name

我们把 proxyObj 对象解构出来之后, 读取它本身的name属性仍然会触发它自己的 get 函数, 但是我们再读取 proxyObj 的时候已经无法触发 obj 对象的 get 函数了.

3. proxy 本身不会进行递归代理

这句话是什么意思呢, 也就是说如果我们有一个对象, 它只会进行一层浅代理, 并不会继续对对象中的对象进行代理, 正如我们上一个例子中, 如果想让 proxyObj 也是一个代理的话, 那么我们需要进行额外的操作, 也就是传给proxyObj的对象就是一个 proxy 对象.

而从官网的介绍我们明显可以看到, reactive 是会对进行一个深层次的转换, 具体怎么实现也非常简单.

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);

      if(isObject(res)) {
        return reactive(res);
      }else {
        return res;
      }
    }
  })
}

这样就能保证对 obj 里面所有的对象都进行代理.

至此, 对于这个 api 我们就介绍的差不多了, 可以看到我们大部分的篇幅都在介绍 proxy, 因为 reactive 其实就是 proxy, 所以现在我们知道, 通过这个api返回后的对象就是一个经过了 proxy 代理的对象.

# effect

接受一个传入的函数, 并响应式追踪其依赖, 在依赖发生变更的时候会重新运行该函数. 官网上并没有提及这个函数, 因为这是比较底层的一个函数, 我们日常使用可以用watchEffect来代替, 我们后面在说watchcomputed的时候会提到.

单独使用一个reactive其实并没有什么意义, 因为拿到这个proxy对象之后, 我们并不能干什么, 重要的是在proxyset/get函数内部它干了什么. 我们在之前的介绍中说到, vue 获取到读取data的事件时, 会将data.msg和模版进行绑定, 这个过程其实就是在get中做的. 而这个模版其实就可以理解为传给effect的函数. 话不多说, 我们先来一个简单的例子看看:

const { effect, reactive } = Vue;

const obj = reactive({ msg: 'hello world' });

effect(() => {
  console.log(obj.msg); 
  // 首次运行会输出 hello world
});

setTimeout(() => {
  obj.msg = 'hello world!!!'
  // obj的值发生改变之后会触发 effect 的函数再次运行, 再次输出 hello world!!!
}, 1000);

可以看到, 首次运行输出了hello world, 当我们改变obj.msg的时候, 再次触发了该函数, 输出了hello world!!!. 所以这是不是和data.msg的值改变之后, vue能够重新更新视图很像? 那么这个 effect 函数是如何实现的呢? 我们可以将其拆分一下, 当我们做到下面这几个步骤的时候, 就能实现这个effect了.

  1. 获取到某个响应式数据被读取了
  2. 知道此时这个响应式数据和什么东西要进行绑定
  3. 响应式数据在发生某些更改的时候重新触发绑定的函数

那么在这里第一步我们已经知道怎么做了, 我们在get函数中就能知道某个响应式数据被改变了. 我们如何知道此时应该将它和什么东西绑定在一起? 其实在这里我们要做的就是要将其和传给effect的函数绑定在一起. 我们将这个函数称为副作用, 也就是某个值修改后应该运行的副作用函数. 我们可以用一个activeEffect来记录下当前的副作用函数, 这样在触发get的时候我们就能知道该和什么东西绑定了, 下面我们简单实现一下effect函数.

let activeEffect = null;
const effectMap = new Map();

function effect(f) {
  activeEffect = f;
  f();
  activeEffect = null;
}

// _type 在我们这里是无用参数
function track(obj, _type, key) {
  // 收集副作用, 以 obj 为对象
  let depsMap = effectMap.get(obj);

  if(!depsMap) {
    effectMap.set(obj, depsMap = new Map());
  }

  // 副作用属于该对象的哪个属性
  let dep = depsMap.get(key);

  if(!dep) {
    depsMap.set(key, dep = []);
  }

  // 如果当前有活跃的副作用, 将其收集起来
  if(activeEffect) {
    dep.push(activeEffect);
  }
}

// _type 在我们这里是无用参数
function trigger(obj, _type, key) {
  // 触发副作用
  const depsMap = effectMap.get(obj);

  if(!depsMap) {
    return;
  }

  // 触发哪个 key 的副作用
  const dep = depsMap.get(key);

  if(!dep) {
    return;
  }

  // 运行 key 里面的副作用
  dep.forEach(effect => {
    effect();
  });
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, null, key);
      return Reflect.get(target, key, receiver);
    },

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

      trigger(target, null, key);

      return res;
    }
  })
}

const obj = reactive({msg: 'hello world'});

effect(() => {
  console.log("effect1", obj.msg);
  // effect1 hello world
})

effect(() => {
  console.log("effect2", obj.msg);
  // effect2 hello world
})

obj.msg = 'hello world!!!!'
// effect1 hello world!!!!
// effect2 hello world!!!!

注意, 我们在getset中干了什么呢, 我们维护了这样的一个列表:

+---------------------------+
| |
| target -> key -> effects |
| |
+---------------------------+

当触发了get函数的时候我们就能知道这个key和什么副作用挂钩, 当前活跃的副作用是在effect内部去赋值的.

当某个数据的set函数被触发的时候, 我们就能通过targetkey找到应该触发的副作用, 再依次运行就好了.

但是值得注意的是这样的实现只是一个简单的demo, 有很多东西都没有考虑到, 比如我们不能简单的通过key来对应这个列表, 因为对于ArrayMap这些对象来说, key并不难一一对应. 比如说我在effect里面读取了一个arr[2], 然后我运行了arr.clear(), 此时有可能并没有2key, 但是显然此时应该触发副作用. 所以在vue真正实现中, 有两个对象

export const enum TrackOpTypes {
  GET = 'get',
  HAS = 'has',
  ITERATE = 'iterate'
}

export const enum TriggerOpTypes {
  SET = 'set',
  ADD = 'add',
  DELETE = 'delete',
  CLEAR = 'clear'
}

其中TrackOpTypes标示在使用数据的时候可能触发的操作, TriggerOpTypes标示在修改数据时可能触发的操作, vue的内部实现会根据 key+type 来进行匹配, 判断执行相应的副作用.

实际上是这样的映射关系:

+--------------------------------------+
| |
| target -> OpTypes + key -> effects |
| |
+--------------------------------------+

具体的逻辑大家可以参看源码, 但是我们只要知道有一个这样的映射关系就行了. 同时, 在生产环境中, 还需要考虑递归、调度的问题, 这里我们就不过多展开了, 有兴趣的同学可以和我私聊或者自己参看源码.

这样, 我们就实现了一个简单的effect函数, 在我们改变某个响应式数据的时候, 能够触发对应的副作用函数. 实际上我们可以看到最核心的就是tracktrigger函数(这两个函数非常重要, 后面会一直提到), vue把他们封装在了reactive内部, 我们是无感知的调用, 那么如果我们不使用响应式对象, 直接手动触发这两个函数能否触发effect的副作用函数呢?话不多说, 我们直接来实战演练一下:

<script setup>
  import { effect, trigger, track } from "@vue/reactivity";

  const TrackOpTypes = {
    GET: "get",
    HAS: "has",
    ITERATE: "iterate",
  };

  const TriggerOpTypes = {
    SET: "set",
    ADD: "add",
    DELETE: "delete",
    CLEAR: "clear",
  };

  // 触发trigger函数, 用 type + key 找副作用
  const triggerEvent = () => trigger(obj, TriggerOpTypes.SET, "msg");

  const obj = { msg: "hello world" };

  effect(() => {
    // 捕获当前副作用函数, 用 type + key 映射
    track(obj, TrackOpTypes.GET, "msg");
    alert(obj.msg)
  });

  obj.msg = "hello world!!!!!";
  triggerEvent();
</script>

<template>
  <button @click="triggerEvent">
    点我触发副作用
  </button>
</template>

链接 (opens new window)在此.

可以看到, 我们根本没有使用响应式对象, 但是也能触发副作用. 这就是因为响应式对象的内部其实也是通过tracktrigger来触发, 我们只是把这个工作给提到外面来进行了而已.

# readonly

接受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理.只读代理是深层的: 任何被访问的嵌套property也是只读的.

一个只读的对象, 那不就是一个只有 get 的代理吗? 所以我们内部只要这样处理一下就好了:

function readonly(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);

      return isObject(res) ? readonly(res) : res;      
    },
    set(target, key) {
      console.warn(
        `Set operation on key "${String(key)}" failed: target is readonly.`,
        target
      )

      return true
    },
  })
}

这样, 当我们调用 set 方法的时候, 就会抛出一个错误提示用户这是一个只读属性.

# isReadonly

检查对象是否是由readonly创建的只读代理.

这个函数的用法很简单明了, 我也不用过多介绍什么了. 那么具体如何实现这个函数呢, 我最直观的想法就是在使用readonly函数的时候在目标对象上挂一个属性用来标识这是一个readonly对象, vue 内部并没有这么做. 在内部实现中, 有一个enum表示标示了各种类型

export const enum ReactiveFlags {
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  IS_SHALLOW = '__v_isShallow',
  RAW = '__v_raw'
}

其中IS_READONLY就表示只读类型, 那么怎么用呢. 不管是在用reactive还是readonly创建副本的时候, get函数中都会有一个这样的操作:

function readonly() {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if(key === ReactiveFlags.IS_READONLY) {
        return true;
      }

      return Reflect.get(target, key, receiver);
    }
  })
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      if(key === ReactiveFlags.IS_READONLY) {
        return false;
      }

      return Reflect.get(target, key, receiver);
    }
  })
}

当我们用ReactiveFlags.IS_READONLYkey去访问object的时候, 会返回true或者false表示这是一个什么变量, 那么很显然这两个函数中有很多重合的部分, 我们可以优化一下写成下面这样:

function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    if(key === ReactiveFlags.IS_READONLY) {
      return isReadonly;
    }

    return Reflect.get(target, key, receiver);
  }
}

function readonly() {
  return new Proxy(obj, {
    get: createGetter(true),
  })
}

function reactive(obj) {
  return new Proxy(obj, {
    get: createGetter(false),
  })
}

那么readonly的实现就非常简单了:

function readonly(obj) {
  return obj[ReactiveFlags.IS_READONLY];
}

我们可以直接实际演练一下, 不使用isReadonly函数, 直接使用ReactiveFlags来获取相应的值:

<script setup>
import { readonly } from 'vue';
  
const ReactiveFlags = {
  SKIP: '__v_skip',
  IS_REACTIVE: '__v_isReactive',
  IS_READONLY: '__v_isReadonly',
  IS_SHALLOW: '__v_isShallow',
  RAW: '__v_raw'
}

const msg = readonly({msg: 'hello world'});
  
const isReadonly = msg[ReactiveFlags.IS_READONLY];
</script>

<template>
  <div>
    isReadonly: {{isReadonly}}
  </div>
</template>

这里isReadonly会返回一个true, 也可以直接点击链接 (opens new window)自己尝试一下.

# toRaw

返回reactivereadonly代理的原始对象.这是一个“逃生舱”, 可用于临时读取数据而无需承担代理访问/跟踪的开销, 也可用于写入数据而避免触发更改.不建议保留对原始对象的持久引用.请谨慎使用.

我们可以看到之前的Flags里面有一个值就是RAW, 而我们在get函数中拿到的target其实就是原始对象, 所以怎么做大家也非常清楚了:

function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    if(key === IS_READONLY) {
      return isReadonly;
    }else if(key === ReactiveFlags.RAW) {
      return target;
    }

    return Reflect.get(target, key, receiver);
  }
}

# isReactive

检查对象是否是由reactive创建的响应式代理. 如果该代理是readonly创建的, 但包裹了由reactive创建的另一个代理, 它也会返回true. 同样的, 它的实现也非常简单.

export function isReactive(obj) {
  if (isReadonly(obj)) {
    return isReactive(obj[ReactiveFlags.RAW])
  }

  return obj[ReactiveFlags.IS_REACTIVE];
}

# isProxy

检查对象是否是由reactivereadonly创建的 proxy.

我们之前已经介绍过了isReactiveisReadonly, 那么显然:

function isProxy(obj) {
  return isReactive(obj) || isReadonly(obj);
}

# markRaw

标记一个对象, 使其永远不会转换为proxy.返回对象本身.

在之前的FLAGS中有一个属性叫SKIP, 这个属性就是标示某个对象跳过代理. 所以我们只需要为对象加上这个属性就行了.

function markRaw(obj) {
  Object.defineProperty(obj, ReactiveFlags.SKIP, {
    configurable: true,
    enumerable: false,
    value: true,
  })

  return obj;
}

function reactive(obj) {
  if(obj[ReactiveFlags.SKIP])) {
    return obj;
  }

  return new Proxy(obj, {
    get(target, key, receiver) {
      if(key === IS_READONLY) {
        return false;
      }

      return Reflect.get(target, key, receiver);
    }
  })
}

那么很显然, 我们可以直接试试不使用markRaw方法, 直接给对象加一个skip属性会怎么样:

<script setup>
  import { reactive, isProxy } from 'vue';

  const ReactiveFlags = {
    SKIP: '__v_skip',
    IS_REACTIVE: '__v_isReactive',
    IS_READONLY: '__v_isReadonly',
    IS_SHALLOW: '__v_isShallow',
    RAW: '__v_raw'
  }

  const obj1 = reactive({msg: 'hello world'});

  const isObj1Proxy = isProxy(obj1); 

  const obj2 = reactive((() => {
    const obj = {msg: 'hello world'};
    obj[ReactiveFlags.SKIP] = true;

    return obj;
  })());

  const isObj2Proxy = isProxy(obj2); 
</script>

<template>
  <div>
    obj1 is proxy: {{isObj1Proxy}}
  </div>
  <div>
    obj2 is proxy: {{isObj2Proxy}}
  </div>
</template>

isObj1Proxytrue, isObj2Proxyfalse, 具体链接 (opens new window)在此

# shallowReactive

创建一个响应式代理, 它跟踪其自身property的响应性, 但不执行嵌套对象的深层响应式转换 (暴露原始值).

我们最开始就说过, proxy本身是不会对深层对象进行代理的, 而我们去实现对深层对象的代理是通过递归实现的. 那么很显然我们只需要在递归的时候加一个判断条件就能实现这个api.

function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    if(key === IS_READONLY) {
      return isReadonly;
    }else if(key === ReactiveFlags.RAW) {
      return target;
    }

    const res = Reflect.get(target, key, receiver);

    if(shallow) {
      return res;
    }

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

function shallowReactive(obj) {
  return new Proxy(obj, {
    get: createGetter(false, true),
  });
}

这里我们是把get函数的创建给单独抽了出来, 像之前那样, 通过传参数的方式去控制创建各种get.

# shallowReadonly

创建一个proxy, 使其自身的property为只读, 但不执行嵌套对象的深度只读转换 (暴露原始值).

同理, 我们可以和shallowReactive公用一个createGetter函数:

function shallowReadonly(obj) {
  return new Proxy(obj, {
    get: createGetter(true, true),
  });
}

只需要将传给createGetter的第一个参数从false改为true就行了.

# Refs

# ref

接受一个内部值并返回一个响应式且可变的ref对象. ref对象仅有一个.value property, 指向该内部值. 如果将对象分配为ref值, 则它将被reactive函数处理为深层的响应式对象.

我们在介绍reactive的时候就说过, proxy只能代理对象, 那么对于 StringNumber这些类型改如何处理呢? vue提供了ref api可以用来处理这些值, 从介绍来看, 我们可以用.value来获取对应的值, 从这个行为也能看出内部一定是一个对象, 所以就相当于vue帮助我们包装了一个对象.

下面的问题就是如何让这个对象具有响应性. 不知道大家是否还记得我们之前在介绍effect的时候提到了两个函数tracktrigger, 同时, 我们还通过这两个函数让一个非响应式对象具有了响应性, 忘了的同学可以点击链接 (opens new window)回顾一下.

所以接下来的问题就简单了, 我们就借助这两个函数让我们封装的对象具有响应性.

const TrackOpTypes = {
  GET: 'get',
  HAS: 'has',
  ITERATE: 'iterate'
}

const TriggerOpTypes = {
  SET: 'set',
  ADD: 'add',
  DELETE: 'delete',
  CLEAR: 'clear'
}


class Ref {
  constructor(value) {
    this._value = value;
    this.__v_isRef = true;
  }

  get value() {
    track(this, TrackOpTypes.GET, 'value');

    return this._value;
  }

  set value(newValue) {
    if(newValue != value) {
      value = newValue;
      trigger(this, TriggerOpTypes.SET, 'value');
    }
  }
}

function ref(value) {
  return new Ref(value);
}

我们用之前实现过的 effect 来验证一下, 看看是否具有响应性:

const TrackOpTypes = {
  GET: 'get',
  HAS: 'has',
  ITERATE: 'iterate'
}

const TriggerOpTypes = {
  SET: 'set',
  ADD: 'add',
  DELETE: 'delete',
  CLEAR: 'clear'
}

let activeEffect = null;
const effectMap = new Map();

function effect(f) {
  activeEffect = f;
  f();
  activeEffect = null;
}

// _type 在我们这里是无用参数
function track(obj, _type, key) {
  // 收集副作用, 以 obj 为对象
  let depsMap = effectMap.get(obj);

  if(!depsMap) {
    effectMap.set(obj, depsMap = new Map());
  }

  // 副作用属于该对象的哪个属性
  let dep = depsMap.get(key);

  if(!dep) {
    depsMap.set(key, dep = []);
  }

  // 如果当前有活跃的副作用, 将其收集起来
  if(activeEffect) {
    dep.push(activeEffect);
  }
}

// _type 在我们这里是无用参数
function trigger(obj, _type, key) {
  // 触发副作用
  const depsMap = effectMap.get(obj);

  if(!depsMap) {
    return;
  }

  // 触发哪个 key 的副作用
  const dep = depsMap.get(key);

  if(!dep) {
    return;
  }

  // 运行 key 里面的副作用
  dep.forEach(effect => {
    effect();
  });
}


class Ref {
  constructor(value) {
    this.__v_isRef = true;
    this._value = value;
  }

  get value() {
    track(this, TrackOpTypes.GET, 'value');

    return this._value;
  }

  set value(newValue) {
    if(newValue != this._value) {
      this._value = newValue;
      trigger(this, TriggerOpTypes.SET, 'value');
    }
  }
}

function ref(value) {
  return new Ref(value);
}

const refObj = ref(1);

effect(() => {
  console.log(refObj.value);
  // 1
})

refObj.value++;
// 2

effect 首次运行的时候会输出 1, 当refvalue被改变的时候会再次触发副作用函数, 输出2, 这样我们就让一个普通对象具有了响应性. 至于第二句话: 如果将对象分配为ref值, 则它将被reactive函数处理为深层的响应式对象.

那么我们只要判断一下这个值是否时对象, 再调用reactive处理一下是不是就大功告成了.

# isRef

检查值是否为一个ref对象

注意我们之前ref的实现中, 里面有一个__v_isRef, 这个值就代表了这是个ref.

function isRef(ref) {
  return !!ref.__v_isRef;
}

# unref

如果参数是一个ref, 则返回内部值, 否则返回参数本身.这是 val = isRef(val) ? val.value : val 的语法糖函数.

这句话本身就说的非常清楚了, 这是为了方便我们使用value值, 毕竟如果是ref, 我们必须使用.value才能拿到真正的值.

# toRefs

将响应式对象转换为普通对象, 其中结果对象的每个property都是指向原始对象相应propertyref.

我们之前说过, proxy如果被解构之后会失去响应性, 那么如果我们想在解构之后还能保持响应性就必须做一些额外的处理. 我们仔细想想, 失去响应性的主要原因是这个变量被赋值给了另一个普通变量, 如果我们每次拿值的时候都从原始的响应式对象上拿, 这样我们就能始终保持响应性了.

class ObjectRefImpl {
  constructor(obj, key) {
    this._obj = obj;
    this._key = key;
  }

  get value() {
    const val = this._obj[this._key];

    return val;
  }

  set value(val) {
    this._obj[this._key] = val;
  }
}

function toRefs(obj) {
  const res = {};

  for(const key in obj) {
    res[key] = new ObjectRefImpl(obj, key);
  }

  return res;
}

这样, 我们每次使用解构后的值时, 都会从原始proxy对象上取值, 这样就会触发原始proxygetset, 从而保持响应性. 同样, 我们来测试一下:

let activeEffect = null;
const effectMap = new Map();

function effect(f) {
  activeEffect = f;
  f();
  activeEffect = null;
}

// _type 在我们这里是无用参数
function track(obj, _type, key) {
  // 收集副作用, 以 obj 为对象
  let depsMap = effectMap.get(obj);

  if(!depsMap) {
    effectMap.set(obj, depsMap = new Map());
  }

  // 副作用属于该对象的哪个属性
  let dep = depsMap.get(key);

  if(!dep) {
    depsMap.set(key, dep = []);
  }

  // 如果当前有活跃的副作用, 将其收集起来
  if(activeEffect) {
    dep.push(activeEffect);
  }
}

// _type 在我们这里是无用参数
function trigger(obj, _type, key) {
  // 触发副作用
  const depsMap = effectMap.get(obj);

  if(!depsMap) {
    return;
  }

  // 触发哪个 key 的副作用
  const dep = depsMap.get(key);

  if(!dep) {
    return;
  }

  // 运行 key 里面的副作用
  dep.forEach(effect => {
    effect();
  });
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, null, key);
      return Reflect.get(target, key, receiver);
    },

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

      trigger(target, null, key);

      return res;
    }
  })
}

class ObjectRefImpl {
  constructor(obj, key) {
    this._obj = obj;
    this._key = key;
    this.__v_isRef = true;
  }

  get value() {
    const val = this._obj[this._key];

    return val;
  }

  set value(val) {
    this._obj[this._key] = val;
  }
}

function toRefs(obj) {
  const res = {};

  for(const key in obj) {
    res[key] = new ObjectRefImpl(obj, key);
  }

  return res;
}

const obj = reactive({name: 'klx', age: 10});

const { name, age } = toRefs(obj);

effect(() => {
  console.log(name.value, age.value);
  // klx 10
})

age.value++;
// klx 11

可以看到, 当我们修改age的值时, 是能够触发副作用函数的, 大家也可以试试直接从obj里面解构, 再看看能不能触发effect的副作用.

# toRef

可以用来为源响应式对象上的某个property新创建一个ref. 然后, ref可以被传递, 它会保持对其源property的响应式连接.

可以看到, toReftoRefs非常像, 只是toRef是针对单个props. 而还有一个显著区别是, 如果obj上不存在某个props, 那么toRefs是不会进行处理的, 但是toRef还是会返回一个可用的ref. 因为我们之前是通过遍历objkey来进行处理的, 如果某个可选props没有, 那自然不会进行处理.

class ObjectRefImpl {
  constructor(obj, key) {
    this._obj = obj;
    this._key = key;
    this.__v_isRef = true;
  }

  get value() {
    const val = this._obj[this._key];

    return val;
  }

  set value(val) {
    this._obj[this._key] = val;
  }
}

function toRef(obj, key) {
  return new ObjectRefImpl(obj, key);
}

实现上也非常简单, 和toRefs基本上一样.

# customRef

创建一个自定义的ref, 并对其依赖项跟踪和更新触发进行显式控制.它需要一个工厂函数, 该函数接收tracktrigger函数作为参数, 并且应该返回一个带有getset的对象.

customRef 主要是让开发者自己控制什么时候触发依赖跟踪和更新, 其实经过上面这么多的介绍, 我们不使用customRef也能用它提供的tracktrigger函数来实现一个自己的customRef. 这个函数主要是把内部的函数通过传参的形式暴露给了开发者, 让大家少一点心智负担. 其实工厂函数接受的tracktrigger就是我们之前一直在说的两个函数.

class CustomRef {
  constructor(factory) {
    const {get, set} = factory(
      () => track(this, TrackOpTypes.GET, 'value'), // 传递给开发者的 track 函数
      () => trigger(this, TriggerOpTypes.SET, 'value'), // 传递给开发者的 trigger 函数
    )

    this._get = get;
    this._set = set;
  }

  get value() {
    return this._get();
  }

  set value(val) {
    this._set(val);
  }
}

function customRef(factory) {
  return new CustomRef(factory);
}

这样, 开发者在调用tracktrigger的时候, 实际上还是通过OpTypes+key建立映射关系, 实现响应性. 但是现在就把调用的权利交给了开发者, 让他自己控制.

# shallowRef

创建一个跟踪自身.value变化的ref, 但不会使其值也变成响应式的.

其实我们上面实现的ref本身就没有对value再进行处理, vue本身会将其处理为响应式对象, 那么, 如果我们不想讲其变成响应式对象, 只要加一个判断条件是不是就行了?

isObject && isShallow ? reactive(val) : val;

就类似这样, 是不是很简单.

# triggerRef

手动执行与shallowRef关联的任何作用 (effect).

到这里我们已经非常熟悉tracktrigger函数了. 我们知道, 我们如果想触发副作用就必须触发trigger. 但是如果我们使用了shallowRef, 其下面的value是不会具有响应性的. 所以自然也不会触发trigger, 不会触发effect.

所以我们就可以手动去触发trigger, 让它触发副作用.

function triggerRef(ref) {
  trigger(ref, TriggerOpTypes.SET, 'value');
}

const ref = shallowRef({ age: 1 });

effect(() => {
    console.log(ref.value.age)
})

// 并不会触发响应式
ref.value.age++;
// 强制触发 effect 副作用
triggerRef(ref);

这样, 我们就能触发这个 Optypes+key 所绑定的副作用.

# Computed 与 watch

# computed

接受一个getter函数, 并根据getter的返回值返回一个不可变的响应式ref对象. 或者, 接受一个具有getset函数的对象, 用来创建可写的ref对象.

这个 computed 就是我们常说的计算属性, 他有一个非常重要的特点就是会缓存之前的结果. 只有当它所依赖的值发生改变的时候才会重新计算结果, 否则不会再执行之前的函数而是返回之前的结果. 同时它的计算是惰性的, 也就是说只有在你真正使用这个值的时候才会去计算. 如果你只声明但是没有使用, 这个getter函数是永远不会执行的. 我们一步步来实现一下 computed, 先不用考虑缓存、惰性求值之类的东西.

function computed(getter) {
  const refObj = ref(null);

  effect(() => {
    refObj.value = getter();
  });

  return refObj;
}

其实只有这么简单几行代码我们就能实现一个超级简单的computed. 在getter运行的时候内部使用到的响应式变量都会触发track, 这样就把effect副作用收集了起来. 当后面这些值的setter被触发之后, 就会重新触发副作用, 进而重新对refObj.value进行赋值, 所以我们就能拿到最新的值了. 我们来简单试一试

const TrackOpTypes = {
  GET: 'get',
  HAS: 'has',
  ITERATE: 'iterate'
}

const TriggerOpTypes = {
  SET: 'set',
  ADD: 'add',
  DELETE: 'delete',
  CLEAR: 'clear'
}

let activeEffect = null;
const effectMap = new Map();

function effect(f) {
  activeEffect = f;
  f();
  activeEffect = null;
}

// _type 在我们这里是无用参数
function track(obj, _type, key) {
  // 收集副作用, 以 obj 为对象
  let depsMap = effectMap.get(obj);

  if(!depsMap) {
    effectMap.set(obj, depsMap = new Map());
  }

  // 副作用属于该对象的哪个属性
  let dep = depsMap.get(key);

  if(!dep) {
    depsMap.set(key, dep = []);
  }

  // 如果当前有活跃的副作用, 将其收集起来
  if(activeEffect) {
    dep.push(activeEffect);
  }
}

// _type 在我们这里是无用参数
function trigger(obj, _type, key) {
  // 触发副作用
  const depsMap = effectMap.get(obj);

  if(!depsMap) {
    return;
  }

  // 触发哪个 key 的副作用
  const dep = depsMap.get(key);

  if(!dep) {
    return;
  }

  // 运行 key 里面的副作用
  dep.forEach(effect => {
    effect();
  });
}


class Ref {
  constructor(value) {
    this.__v_isRef = true;
    this._value = value;
  }

  get value() {
    track(this, TrackOpTypes.GET, 'value');

    return this._value;
  }

  set value(newValue) {
    if(newValue != this._value) {
      this._value = newValue;
      trigger(this, TriggerOpTypes.SET, 'value');
    }
  }
}

function ref(value) {
  return new Ref(value);
}

function computed(f) {
  const refObj = ref(null);

  effect(() => {
    refObj.value = f();
  });

  return refObj;
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, null, key);
      return Reflect.get(target, key, receiver);
    },

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

      trigger(target, null, key);

      return res;
    }
  })
}

const obj = reactive({name: 'klx', age: 20});

const age = computed(() => {
  return obj.age * 2;
});

console.log(age.value);
// 40

obj.age++;

console.log(age.value);
// 42

当我们改变obj.age的值之后, age的值也随之改变了, 符合预期. 那么接下来我们来看看如何避免重复计算以及惰性计算.

其实真正的effect函数有一个lazy的变量可以传递, 这样effect就不会立即执行. 执行完effect会返回一个run函数, 调用run函数的时候才会真正执行. 同时effect还能接受一个scheduler(调度器)的参数, effect里面的变量被改变触发副作用时, 并不运行传给effect的回调函数, 而是运行用户自定义的调度器, 也就是scheduler函数. 所以我们可以把effect进行一些改造:

function effect(f, options) {
  const { lazy = false, scheduler} = options;
  const run = () => {
    try {
      activeEffect = scheduler ? scheduler : f;
      return f();
    }finally {
      activeEffect = null;
    }
  };
  
  if(!lazy) {
    run();
  }

  return run;
}

那么显然, computed 其实就是一个lazyeffect:

function computed(f) {
  const run = effect(() => {
    return f();
  }, {
    lazy: true,
  });

  const refObj = {
    get value() {
      return run();  
    }
  };

  return refObj;
}

这样, 只有在我们真的使用了refObj.value的时候, 才会执行到get函数, 从而触发effect的执行. 大家可以自己修改一下代码尝试一下. 那么第二个问题就是如何避免多次重复执行. 可以看到, 现在的实现中, 无论我们的依赖有没有改变, run函数每次必定都会执行, 那么什么时候该执行什么时候不该执行? 首先第一次肯定需要跑一遍run函数我们才能拿到真正的结果. 但是在后面的过程中, 如果effect依赖的那些值都没有变化的话, 那我们就没有必要再次运行run函数了, 因为依赖的值不变得到的结果肯定还是一样的.

所以我们弄一个dirty的变量来判断effect所依赖的值是否发生了变化, 这个时候就用到了scheduler函数. 那么dirty什么情况下才应该是true?

  1. 首次默认值为true
  2. computed内部依赖的值发生了改变

而在依赖值改变的时候, 会触发effect的副作用函数, 所以我们在这个时机把dirty的值修改为true就好了.

function computed(f) {
  let dirty = true;
  let res = null;
  const run = effect(() => {
    return f();
  }, {
    lazy: true,
    scheduler: () => {
      dirty = true;
    }
  });

  const refObj = {
    get value() {
      if(dirty) {
        res = run();
        dirty = false;
      }
      return res;
    }
  };

  return refObj;
}

我们把结果保存起来, 在dirtyfalse的时候不再运行run函数, 这样就能实现值的缓存. 我们来完整跑一跑:

const TrackOpTypes = {
  GET: 'get',
  HAS: 'has',
  ITERATE: 'iterate'
}

const TriggerOpTypes = {
  SET: 'set',
  ADD: 'add',
  DELETE: 'delete',
  CLEAR: 'clear'
}

let activeEffect = null;
const effectMap = new Map();

function effect(f, options) {
  const { lazy = false, scheduler} = options;
  const run = () => {
    try {
      activeEffect = scheduler ? scheduler : f;
      return f();
    }finally {
      activeEffect = null;
    }
  };
  
  if(!lazy) {
    run();
  }

  return run;
}

// _type 在我们这里是无用参数
function track(obj, _type, key) {
  // 收集副作用, 以 obj 为对象
  let depsMap = effectMap.get(obj);

  if(!depsMap) {
    effectMap.set(obj, depsMap = new Map());
  }

  // 副作用属于该对象的哪个属性
  let dep = depsMap.get(key);

  if(!dep) {
    depsMap.set(key, dep = []);
  }

  // 如果当前有活跃的副作用, 将其收集起来
  if(activeEffect) {
    dep.push(activeEffect);
  }
}

// _type 在我们这里是无用参数
function trigger(obj, _type, key) {
  // 触发副作用
  const depsMap = effectMap.get(obj);

  if(!depsMap) {
    return;
  }

  // 触发哪个 key 的副作用
  const dep = depsMap.get(key);

  if(!dep) {
    return;
  }

  // 运行 key 里面的副作用
  dep.forEach(effect => {
    effect();
  });
}

function computed(f) {
  let dirty = true;
  let res = null;
  const run = effect(() => {
    return f();
  }, {
    lazy: true,
    scheduler: () => {
      dirty = true;
    }
  });

  const refObj = {
    get value() {
      if(dirty) {
        res = run();
        dirty = false;
      }
      return res;
    }
  };

  return refObj;
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, null, key);
      return Reflect.get(target, key, receiver);
    },

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

      trigger(target, null, key);

      return res;
    }
  })
}

const obj = reactive({name: 'klx', age: 20});

const age = computed(() => {
  console.log("computed 执行了")
  return obj.age * 2;
});

console.log(age.value);
// computed 执行了
// 40

console.log(age.value);
// 40

可以看到, 第二次使用age.value的时候已经不会执行computed的函数了. 其实还有一个问题就是, 我们现在这样写, 返回的对象是没有响应性的, computed实际上会返回一个ref, 是具有响应性的. 那么这个要怎么写了, 我已经不会写了, 大家帮我实现一下(是不是用tracktrigger就行了?).

# watchEffect

立即执行传入的一个函数, 同时响应式追踪其依赖, 并在其依赖变更时重新运行该函数.

从描述来看这个和我们之前介绍的effect函数非常像, 其实watchEffect底层依赖的就是effect, 但是它比effect做了更多的事情, 例如:

  1. 会根据实际情况自动stop. 例如组件被卸载的时候
  2. 会通过队列去调用副作用, 避免多次重复调用, 例如多次触发同一个变量的set, 最终只会执行一次副作用.
  3. 接受一个flush参数, 支持自定义副作用调用时机.

个人认为, 也正因为如此, 它并不是一个纯粹的响应式api, 所以它不随着@vue/reactivity一起暴露出来, 而是和watch函数一起放在了@vue/runtime-core中. 我们主要看看它的多次调用触发一次副作用如何实现? 还记得之前说过effect支持传入一个调度器的参数吗, 其实我们借助这个参数就能实现这个功能.

function watchEffect(f) {
  const queue = [];

  const run = effect(() => {
    return f();
  }, {
    scheduler: () => {
      if(!queue.includes(run)) {
        queue.push(run);

        Promise.resolve().then(() => {
          while(queue.length) {
            const f = queue.shift();
            f();
          }
        })
      }
    }
  })
}

我们维护一个队列, 将要执行的副作用函数放入其中, 每次执行之前先判断这个副作用函数是否在队列中, 再决定是否入栈. 最后利用js的事件循环将真正的执行放到微任务队列中去执行. 而要想实现flush参数, 其实也是在调度器里面做文章, 例如在flush等于sync的情况下就直接运行副作用函数, 从而让用户去控制副作用函数执行时机. 我们可以看看效果:

const TrackOpTypes = {
  GET: 'get',
  HAS: 'has',
  ITERATE: 'iterate'
}

const TriggerOpTypes = {
  SET: 'set',
  ADD: 'add',
  DELETE: 'delete',
  CLEAR: 'clear'
}

let activeEffect = null;
const effectMap = new Map();

function effect(f, options) {
  const { lazy = false, scheduler} = options;
  const run = () => {
    try {
      activeEffect = scheduler ? scheduler : f;
      return f();
    }finally {
      activeEffect = null;
    }
  };
  
  if(!lazy) {
    run();
  }

  return run;
}

// _type 在我们这里是无用参数
function track(obj, _type, key) {
  // 收集副作用, 以 obj 为对象
  let depsMap = effectMap.get(obj);

  if(!depsMap) {
    effectMap.set(obj, depsMap = new Map());
  }

  // 副作用属于该对象的哪个属性
  let dep = depsMap.get(key);

  if(!dep) {
    depsMap.set(key, dep = []);
  }

  // 如果当前有活跃的副作用, 将其收集起来
  if(activeEffect) {
    dep.push(activeEffect);
  }
}

// _type 在我们这里是无用参数
function trigger(obj, _type, key) {
  // 触发副作用
  const depsMap = effectMap.get(obj);

  if(!depsMap) {
    return;
  }

  // 触发哪个 key 的副作用
  const dep = depsMap.get(key);

  if(!dep) {
    return;
  }

  // 运行 key 里面的副作用
  dep.forEach(effect => {
    effect();
  });
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, null, key);
      return Reflect.get(target, key, receiver);
    },

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

      trigger(target, null, key);

      return res;
    }
  })
}

function watchEffect(f) {
  const queue = [];

  const run = effect(() => {
    return f();
  }, {
    scheduler: () => {
      if(!queue.includes(run)) {
        queue.push(run);

        Promise.resolve().then(() => {
          while(queue.length) {
            const f = queue.shift();
            f();
          }
        })
      }
    }
  })
}

const obj = reactive({age: 20});

watchEffect(() => {
  console.log(obj.age);
})
// 首次执行输出 20

obj.age++;
obj.age++;
obj.age++;
// 全部加完之后输出 23

这样就避免了无用的副作用运行. 所以我们在日常开发中使用watchEffect就行了, effect只是它底层依赖的api而已(毕竟官网都没说明的api).

# watchPostEffect

watchEffect的别名, 带有 flush: 'post' 选项, 等效于:

watchEffect(() => {}, {
  flush: 'post'
});

# watchSyncEffect

watchEffect的别名, 带有 flush: 'sync' 选项, 等效于:

watchEffect(() => {}, {
  flush: 'sync'
});

# watch

watch API 与选项式 API this.$watch (以及相应的 watch 选项) 完全等效.watch 需要侦听特定的数据源, 并在单独的回调函数中执行副作用.默认情况下, 它也是惰性的——即回调仅在侦听源发生变化时被调用.

这个函数支持监听一个ref对象, 监听reactive对象, 也支持传入一个函数, 监听内部依赖的变量, 同时也支持传递数组. 这个方法实现的思路其实也很简单, 将传入的监听内容封装为一个 getter 函数, 将其放到effect中去, 在副作用触发时通过调度器执行对应回调函数就行了. 例如这样:

function watch(reactiveObj, cb) {
  const get = () => {
    return reactiveObj
  }

  effect(() => {
    return get();
  }, {
    scheduler: () => {
      cb();
    }
  })
}

但是有一个地方需要注意, 这个函数有一个deep参数, 是用来控制是否监听对象内部值的变化. 因为当你传入一个reactive对象的时候, getter函数并没有使用到reactive内部的值, 自然不会track副作用. 那么你再修改reactive内部值的时候, 自然也不会被监听到.

const { reactive, watch } = Vue;

const obj = reactive({ name: 'klx', age: 10 });

watch(() => obj, () => {
  console.log("obj 被修改", obj);
});

// 正确写法
// watch(() => obj, () => {
//   console.log("obj 被修改", obj);
// }, {
//   deep: true,
// });

obj.age++;
// 并不会触发 watch

我们可能注意到这里是使用函数去返回了一个reactive, 如果你直接给watch传一个reactive会发现它是能够成功的监听到内部值的改变的, 这是因为当你单独传入reactive时, vue内部帮你把deep置为true了, 其他情况下默认值为false.

// ...
} else if (isReactive(source)) {
  getter = () => source
  deep = true
} else if (isArray(source))
// ...

而当你传入deep: true时, vue会自动通过traverse函数帮你遍历对象下的所有属性:

export function traverse(value: unknown, seen?: Set<unknown>) {
  if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
    return value
  }
  seen = seen || new Set()
  if (seen.has(value)) {
    return value
  }
  seen.add(value)
  if (isRef(value)) {
    traverse(value.value, seen)
  } else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen)
    }
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, seen)
    })
  } else if (isPlainObject(value)) {
    for (const key in value) {
      traverse((value as any)[key], seen)
    }
  }
  return value
}

这样, 就会触发所有propsget, 从而track到副作用, 实现深层监听.

# 结语

至此, 终于把官网上介绍的响应性 API 介绍完了, 其实还有一个 Effect 作用域 API, 这个API的源码其实也非常简单, 大家感兴趣的自己去看就好了.

这一系列文章只是为了让大家更加深入的了解vue实现响应式背后的原理, vue 的所有响应式操作都依赖这些基础api实现. 而文中所列出的代码也不是vue的真正实现, 都是一个最基础的能跑的demo, 非常多的情况是没有考虑进去的. 但是已经足够让大家对于它背后的原理有一定的了解. 如果想真正看看vue到底是如何处理各种递归、调度、优化依赖收集的部分, 还是建议大家自己去阅读一下源码.

# 参考资料

  1. github源码 (opens new window)
  2. 官网文档 (opens new window)
  3. vue core team 成员 HcySunYang 博客 (opens new window)