-
Notifications
You must be signed in to change notification settings - Fork 128
Description
Vue3响应式系统设计理念
先说点题外话,Vue3 正式发布之后,我面试的时候如果发现对方简历写有“精通Vue”之类的话,一般都会问一问Vue3相对Vue2有哪些变化,很多人只能答上来一点就是响应式从 defineProperty变成了 proxy。其实Vue3是一次比较大的重构,变化的地方非常多,举几个重要的点:
- mono repo 设计:源码分成了十几个比较独立的模块,减少了代码耦合,部分模块可以独立使用也可以换成其他的实现版本
- 编译时优化:在编译阶段对模板中动态内容和静态内容进行打标,并对动态内容进行分块(block),在运行时仅更新分块之后的动态部分,大幅提升更新效率
- 组合式API:全新的组合式API,提供类似 React Hooks的全新语法
上面的题外话有助于理解接下来的内容。回到本文议题,在Vue3中响应式模块其实有两个层次的变化:
- 内部实现从
definePropery变成了proxy,规避了一些老版本难以解决的bug reactivity变成了一个功能完善的有明确API定义的独立模块,而不是耦合在Vue源码中,这就意味着我们可以在任何项目中引入reactivity不需要依赖Vue框架
既然是一个独立的模块,那么它就会有自己完整的设立思路。响应式系统的设计思路是什么?总结为一句话就是:在目标发生变化时,执行对应的函数。顺着这个思路,我们就可以定义出响应式系统的几个要素:
- 需要定义被监听的目标和执行的函数,我们统一把这两个对象称为
target和effectFn - 需要建立
target和effectFn之间的关联,当target变化之后自动执行effectFn
怎么实现上面两个要素呢? Vue3中是这么实现的:
- 提供
reactive函数用来声明需要被监听的目标,提供effect函数用来声明需要执行的函数 - 内部实现了一套依赖收集和触发机制来建立
target和effectFn的关联关系,主要是提供track和trigger两个函数分别收集和触发依赖。

42行代码实现
reactive函数将一个 target变成响应式的,原理是通过 proxy进行了代理,这里为了说明原理,我们只实现对 Object类型的代理,代码如下:
const reactive = function (target) {
return new Proxy(target, {
get(target, key, reciever) {
track(target, key);
return Reflect.get(target, key, reciever);
},
set(target, key, value, reciever) {
Reflect.set(target, key, value, reciever);
trigger(target, key);
}
});
}用法如下所示:
const p = reactive({name: 'luxun'}); // p 就变成响应式的了;reactive的基本原理是在 get的时候收集依赖,在 set 的时候触发依赖。这种依赖收集方式非常巧妙,使得我们代码中不必额外写依赖声明,只要读取了值就会自动收集依赖。
track和 trigger函数分别是用来记录依赖和触发依赖的,配合 effect记录当前函数,实现如下:
let activeEffect = undefined;
const targetMap = new WeakMap();
const effect = function (fn) {
activeEffect = fn;
fn();
}
const track = function (target, key) {
const dep = targetMap.get(target) || new Map();
const funcs = dep.get(key) || new Set();
funcs.add(activeEffect);
dep.set(key, funcs);
targetMap.set(target, dep);
}
const trigger = function (target, key) {
const dep = targetMap.get(target);
if (dep && dep.get(key)) {
dep.get(key).forEach(fn => fn());
}
}用法如下:
const p = reactive({name: 'luxun'});
effect(() => console.log(p.name));
p.name = 'zhangsan'; // 触发effect执行上面 22行实现代码,有三个要点(面试考点)需要注意。
第一个要点是 effect实现。
effect作用就是记录并执行传入的 fn,这里为什么可以用一个全局变量来记录呢? 因为JS的代码执行是单线程的(不考虑worker),effect函数不可能并行执行,因此这样记录没有问题。而且这不是为了实现简单随便写的,官方实现也是一个全局变量。effect中执行 fn()时,会触发对p的读取操作,此时就会调用track函数记录依赖。
第二个要点是三个数据结构 WeakMap,Map 和 Set。
targetMap为什么不用 Map或者 Object呢?主要是两个原因:
WeakMap可以用任意的JS类型作为key,这里我们需要用target对象作为keyWeakMap对key的应用是弱引用,不会影响垃圾回收。
那dep为什么可以用Map呢? 因为dep整体会被作为垃圾回收,通过key持有引用不会影响垃圾回收,而且key一定是一个字符串。
为什么funcs用Set而不用数组呢?因为Set是自动去重的。
第三个要点 是targetMap的结构,依赖信息是如何记录的。结构是这样的: targetMap[target][key] = new Set(fn1, fn2, fn3);
为什么要用 Reflect
还有一个非常需要注意的点,是对 Reflect 的使用。大家考虑下这两行代码有什么区别?
Reflect.get(target, key, reciever); // 通过Reflect取值
target[key]; // 通过key取值假设我们把 Reflect.get换成 target[key]会有什么问题吗? 要回答这个问题,先看看MDN上的定义:
The static
Reflect.get()method works like getting a property from an object (target[propertyKey]) as a function.
按照说明,似乎是没有区别的,不过关键是第三个参数Reciever:
receiverOptional:
The value ofthisprovided for the call totargetif agetteris encountered. When used withProxy,it can be an object that inherits from target.
这个参数可以指定取值时的this? 大家肯定会奇怪取值的时候哪来的this呢? 我们把前面的例子改一下就知道了:
const raw = {
firstName: 'Lu',
lastName: 'xun',
get name() {
return this.firstName + this.lastName;
}
}
const person = reactive(raw);当取值 name的时候,这不就有 this了吗?此时如果我们通过 target[key]取值,相当于通过 raw.name进行了取值,那么其中的 this就指向了raw 而不是 person。这样就会有问题了,因为只有 person.firstName才会进入 getter收集依赖, raw.firstName 并不会触发依赖收集。
结论就是: Reflect 是为了解决 this 指向问题,如果用 target[key]会导致 this指向原始值而无法收集到依赖。
和官方实现有什么区别?
前文的42行玩具实现其实已经揭示了核心逻辑,当显然不能和官方2000行代码实现相媲美。那么官方的这么多代码额外做了哪些工作呢?总结一下:
- 对原始类型、嵌套类型、数组、Map、Set, Symbol、只读等不同类型数据的处理
2.effect执行的时候支持lazy模式,支持自定义调度器 - 官方还提供了
Ref,Computed等API - 完善的TS类型定义
- 异常处理,DEV模式
上文中的完整的42行代码参见这里:https://github.com/lihongxun945/42lines-vue3-reactivity/blob/master/reactivity.js