此次分析的源码版本为3.0.5
前置知识:
- Set:类似于数组,但是成员的值都是唯一的(注:两个{}是不等的,两个NaN是相等的)、
- WeakSet:类似Set,但是WeakSet的成员只能是对象,而不能是其他类型的值,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,WeakSet 不可遍历。
- Map:它类似于对象,也是键值对的集合,区别于对象的的是对象的键只能是字符串,而Map的键可以是任何类型。
- WeakMap:WeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。WeakMap的键名所指向的对象,不计入垃圾回收机制。
- Proxy:目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
- Reflect:ES6 为了操作对象而提供的新 API。Reflect对象的方法与Proxy对象的方法一一对应
- 设计模式——发布订阅模式。
Object.defineProperty
讲vue3.0的响应式之前,先回顾一下vue2.0的响应式实现过程:通过Object.defineProperty api 劫持对象的属性的get和set操作,在get过程进行以来收集,在set过程进行派发通知更新视图,响应式 API 和组件更新的关系如图所示:
熟悉设计模式的一定很清楚发布订阅模式,这个过程通过Dep(发布者类)和Watcher(订阅者类)实现。
- Dep 负责收集所有相关的的订阅者 Watcher ,具体谁不用管,具体有多少也不用管,只需要根据 target 指向的计算去收集订阅其消息的 Watcher 即可,然后做好消息发布 notify 即可。
- Watcher 负责订阅 Dep ,并在订阅的时候让 Dep 进行收集,接收到 Dep 发布的消息时,做好其 update 操作即可。
用一张图可以直观地看清这个流程。
在对vue2.0响应式原理了解了之后,我们知道 Object.defineProperty API 的一些缺点:
- 不能监听对象属性新增和删除;
- 初始化阶段递归执行 Object.defineProperty 带来的性能负担。
Reactive API
Vue.js 3.0 为了解决 Object.defineProperty 的这些缺陷,使用 Proxy API 重写了响应式部分,并独立维护和发布整个 reactivity 库。
reactive
function reactive(target) {
// if trying to observe a readonly proxy, return the readonly version.
if (target && target["__v_isReadonly" /* IS_READONLY */]) {
return target;
}
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers);
}
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) {
// 目标必须是对象或数组类型
if (!isObject(target)) {
{
console.warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
// target 已经是 Proxy 对象,直接返回
// 有个例外,如果是 readonly 作用于一个响应式对象,则继续
if (target["__v_raw" /* RAW */] &&
!(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
return target;
}
// target 已经有对应的 Proxy 了
const proxyMap = isReadonly ? readonlyMap : reactiveMap;
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
// 只有在白名单里的数据类型才能变成响应式
const targetType = getTargetType(target);
if (targetType === 0 /* INVALID */) {
return target;
}
// 利用 Proxy 创建响应式
const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
// 给原始数据打个标识,说明它已经变成响应式,并且有对应的 Proxy 了
proxyMap.set(target, proxy);
return proxy;
}
可以看到,reactive 内部通过 createReactiveObject 函数把 target 变成了一个响应式对象。
在这个过程中,createReactiveObject 函数主要做了以下几件事情。
- 函数首先判断 target 是不是数组或者对象类型,如果不是则直接返回。所以原始数据 target 必须是对象或者数组。
- 如果对一个已经是响应式的对象再次执行 reactive,还应该返回这个响应式对象,
- 如果对同一个原始数据多次执行 reactive ,那么会返回相同的响应式对象。
- 使用 getTargetType 函数对 target对象做一进步限制
function getTargetType(value) {
return value["__v_skip" /* SKIP */] || !Object.isExtensible(value)
? 0 /* INVALID */
: targetTypeMap(toRawType(value));
}
比如,带有 __v_skip 属性的对象、不能扩展的对象实例是不能变成响应式的。
- 通过 Proxy API 劫持 target 对象,把它变成响应式。需要注意的是,这里 Proxy 对应的处理器对象会根据数据类型的不同而不同,我们先分析基本数据类型的 Proxy 处理器对象,reactive 函数传入的 baseHandlers 值是 mutableHandlers。
- 给原始数据打个标识。
Vue.js 3.0 的 reactive API 就是通过 Proxy 劫持数据,而且由于 Proxy 劫持的是整个对象,所以我们可以检测到任何对对象的修改,弥补了 Object.defineProperty API 的不足。
Proxy 处理器对象 mutableHandlers 的实现:
const mutableHandlers = {
get, // 访问对象属性会触发 get 函数;
set, // 设置对象属性会触发 set 函数;
deleteProperty, // 删除对象属性会触发 deleteProperty 函数;
has, // in 操作符会触发 has 函数;
ownKeys // 通过 Object.getOwnPropertyNames 访问对象属性名会触发 ownKeys 函数。
};
依赖收集
get 函数
依赖收集发生在数据访问的阶段,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性被访问的时候就会执行 get 函数,定位到get函数,其实它是执行 createGetter 函数的返回值。
createGetter
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
if (key === "__v_isReactive" /* IS_REACTIVE */) {
// 代理 observed.__v_isReactive
return !isReadonly;
}
else if (key === "__v_isReadonly" /* IS_READONLY */) {
// 代理 observed.__v_isReadonly
return isReadonly;
}
else if (key === "__v_raw" /* RAW */ &&
receiver === (isReadonly ? readonlyMap : reactiveMap).get(target)) {
// 代理 observed.__v_raw
return target;
}
// 处理数组的响应式
const targetIsArray = isArray(target);
// arrayInstrumentations包含对数组一些方法修改的函数
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
// 求值
const res = Reflect.get(target, key, receiver);
// 内置 Symbol key 不需要依赖收集
const keyIsSymbol = isSymbol(key);
if (keyIsSymbol
? builtInSymbols.has(key)
: key === `__proto__` || key === `__v_isRef`) {
return res;
}
// 依赖收集
if (!isReadonly) {
track(target, "get" /* GET */, key);
}
if (shallow) {
return res;
}
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key);
return shouldUnwrap ? res.value : res;
}
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 isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
get 函数主要做了四件事情:
首先对特殊的 key 做了代理,接着通过 Reflect.get 方法求值,如果 target 是数组且 key 命中了 arrayInstrumentations,则执行对应的函数。
arrayInstrumentations
const arrayInstrumentations = {};
['includes', 'indexOf', 'lastIndexOf'].forEach(key => {
const method = Array.prototype[key];
arrayInstrumentations[key] = function (...args) {
// toRaw 可以把响应式对象转成原始数据
const arr = toRaw(this);
for (let i = 0, l = this.length; i < l; i++) {
// 依赖收集,跟踪数组每个元素的变化。
track(arr, "get" /* GET */, i + '');
}
// 先尝试用参数本身,可能是响应式数据
const res = method.apply(arr, args);
if (res === -1 || res === false) {
// 如果失败,再尝试把参数转成原始数据
return method.apply(arr, args.map(toRaw));
}
else {
return res;
}
};
});
当 target 是一个数组的时候,我们去访问 target.includes、target.indexOf 或者 target.lastIndexOf 就会执行 arrayInstrumentations 代理的函数,除了调用数组本身的方法求值外,还对数组每个元素做了依赖收集。因为一旦数组的元素被修改,数组的这几个 API 的返回结果都可能发生变化,所以我们需要跟踪数组每个元素的变化。
回到 get 函数,第三步就是通过 Reflect.get 求值,然后会执行 track 函数收集依赖。
函数最后会对计算的值 res 进行判断,如果它也是数组或对象,则递归执行 reactive 把 res 变成响应式对象。
这么做是因为 Proxy 劫持的是对象本身,并不能劫持子对象的变化,这点和 Object.defineProperty API 一致。但是 Object.defineProperty 是在初始化阶段,即定义劫持对象的时候就已经递归执行了,而 Proxy 是在对象属性被访问的时候才递归执行下一步 reactive,这其实是一种延时定义子对象响应式的实现,在性能上会有较大的提升。
track 函数
整个 get 函数最核心的部分其实是执行 track 函数收集依赖,
track
// 是否应该收集依赖
let shouldTrack = true
// 当前激活的 effect
let activeEffect
// 原始数据对象 map
const targetMap = new WeakMap()
function track(target, type, key) {
if (!shouldTrack || activeEffect === undefined) {
return;
}
let depsMap = targetMap.get(target);
if (!depsMap) {
// 每个 target 对应一个 depsMap
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if (!dep) {
// 每个 key 对应一个 dep 集合
depsMap.set(key, (dep = new Set()));
}
if (!dep.has(activeEffect)) {
// 收集当前激活的 effect 作为依赖
dep.add(activeEffect);
// 当前激活的 effect 收集 dep 集合作为依赖
activeEffect.deps.push(dep);
if ( activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
});
}
}
}
分析这个函数前,需要先想想收集的依赖是什么,我们的目的是实现响应式,就是当数据变化的时候可以自动做一些事情,比如执行某些函数,所以我们收集的依赖就是数据变化后执行的副作用函数。
再来看实现,我们把 target 作为原始的数据,key 作为访问的属性。我们创建了全局的 targetMap 作为原始数据对象的 Map,它的键是 target,值是 depsMap,作为依赖的 Map;这个 depsMap 的键是 target 的 key,值是 dep 集合,dep 集合中存储的是依赖的副作用函数。为了方便理解,可以通过下图表示它们之间的关系:
所以每次 track ,就是把当前激活的副作用函数 activeEffect 作为依赖,然后收集到 target 相关的 depsMap 对应 key 下的依赖集合 dep 中。
派发通知
set 函数
派发通知发生在数据更新的阶段 ,由于我们用 Proxy API 劫持了数据对象,所以当这个响应式对象属性更新的时候就会执行 set 函数。我们来看一下 set 函数的实现,它是执行 createSetter 函数的返回值:
createSetter
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
const oldValue = target[key];
if (!shallow) {
// toRaw 可以把响应式对象转成原始数据
value = toRaw(value);
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value;
return true;
}
}
// 判断target是否存在这个key
const hadKey = isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
// 如果目标的原型链也是一个 proxy,通过 Reflect.set 修改原型链上的属性会再次触发 setter,这种情况下就没必要触发两次 trigger 了
if (target === toRaw(receiver)) {
if (!hadKey) {
// 处理新增key
trigger(target, "add" /* ADD */, key, value);
}
else if (hasChanged(value, oldValue)) {
// 处理更新key
trigger(target, "set" /* SET */, key, value, oldValue);
}
}
return result;
};
}
从代码里看,set主要做了两件事:
- 通过Reflect.set求值
- 通过trigger函数派发通知,并依据 key 是否存在于 target 上来确定通知类型,即新增还是修改。
trigger函数
整个 set 函数最核心的部分就是执行 trigger 函数派发通知
trigger
function trigger(target, type, key, newValue, oldValue, oldTarget) {
// 取对应target的副作用函数Map
const depsMap = targetMap.get(target);
if (!depsMap) {
// never been tracked
return;
}
// 创建运行的 effects 集合
const effects = new Set();
// 添加 effects 的函数
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.options.allowRecurse) {
effects.add(effect);
}
});
}
};
if (type === "clear" /* CLEAR */) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(add);
}
else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newValue) {
add(dep);
}
});
}
else {
// SET | ADD | DELETE 操作之一,添加对应的 effects
if (key !== void 0) {
add(depsMap.get(key));
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case "add" /* ADD */:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY));
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}
else if (isIntegerKey(key)) {
// new index added to array -> length changes
add(depsMap.get('length'));
}
break;
case "delete" /* DELETE */:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY));
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY));
}
}
break;
case "set" /* SET */:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY));
}
break;
}
}
const run = (effect) => {
if (effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
});
}
// 调度执行
if (effect.options.scheduler) {
effect.options.scheduler(effect);
}
else {
// 直接运行
effect();
}
};
// 遍历执行 effects
effects.forEach(run);
}
trigger 函数的实现也很简单,主要做了四件事情:
- 通过 targetMap 拿到 target 对应的依赖集合 depsMap;
- 创建运行的 effects 集合;
- 根据 key 从 depsMap 中找到对应的 effects 添加到 effects 集合;
- 遍历 effects 执行相关的副作用函数。
所以每次 trigger 函数就是根据 target 和 key ,从 targetMap 中找到相关的所有副作用函数遍历执行一遍。
副作用函数收集过程
effect
// 副作用函数栈
const effectStack = [];
// 当前激活的 effect
let activeEffect;
function effect(fn, options = EMPTY_OBJ) {
if (isEffect(fn)) {
// 如果 fn 已经是一个 effect 函数了,则指向原始函数
fn = fn.raw;
}
// 创建一个 wrapper,它是一个响应式的副作用的函数
const effect = createReactiveEffect(fn, options);
// lazy 配置,计算属性会用到,非 lazy 则直接执行一次
if (!options.lazy) {
effect();
}
return effect;
}
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
// 非激活状态,则判断如果非调度执行,则直接执行原始函数。
if (!effect.active) {
return options.scheduler ? undefined : fn();
}
if (!effectStack.includes(effect)) {
// 清空 effect 引用的依赖
cleanup(effect);
try {
// 开启全局 shouldTrack,允许依赖收集
enableTracking();
// 压栈
effectStack.push(effect);
activeEffect = effect;
// 执行原始函数
return fn();
}
finally {
// 出栈
effectStack.pop();
// 恢复 shouldTrack 开启之前的状态
resetTracking();
// 指向栈最后一个 effect
activeEffect = effectStack[effectStack.length - 1];
}
}
};
effect.id = uid++;
// 标识是一个 effect 函数
effect._isEffect = true;
// effect 自身的状态
effect.active = true;
// 包装的原始函数
effect.raw = fn;
// effect 对应的依赖,双向指针,依赖包含对 effect 的引用,effect 也包含对依赖的引用
effect.deps = [];
// effect 的相关配置
effect.options = options;
return effect;
}
effect 内部通过执行 createReactiveEffect 函数去创建一个新的 effect 函数,为了和外部的 effect 函数区分,我们把它称作 reactiveEffect 函数。这个 reactiveEffect
函数就是响应式的副作用函数,当执行 trigger 过程派发通知的时候,执行的 effect 就是它。
- 首先它会判断 effect 的状态是否是 active,这其实是一种控制手段,允许在非 active 状态且非调度执行情况,则直接执行原始函数 fn 并返回。
- 这个过程维护了一个副作用的函数栈effectStack,判断 effectStack 中是否包含 effect,如果没有就把 effect 压入栈内。
考虑到key属性的副作用可能是一个嵌套副作用函数,所以这里用一个栈结构来维护副作用函数栈。
- 在入栈前会执行 cleanup 函数清空 reactiveEffect 函数对应的依赖 。在执行 track 函数的时候,除了收集当前激活的 effect 作为依赖,还通过 activeEffect.deps.push(dep) 把 dep 作为 activeEffect 的依赖,这样在 cleanup 的时候我们就可以找到 effect 对应的 dep 了,然后把 effect 从这些 dep 中删除。
cleanup 函数的代码如下所示:
function cleanup(effect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
至此,我们从 reactive API 入手了解了整个响应式对象的实现原理。
通过这篇文章你可以掌握:
- 明白响应式 API 的实现原理,
- 什么时候收集依赖,什么时候派发更新,
- 副作用函数的作用和设计原理。
- 知道 reactive、readonly、ref 三种 API 的区别和各自的使用场景。
最后我们通过一张图来看一下整个响应式 API 实现和组件更新的关系:
它和前面 Vue.js 2.x 的响应式原理图很接近,其实 Vue.js 3.0 在响应式的实现思路和 Vue.js 2.x 差别并不大,主要就是 劫持数据的方式改成用 Proxy 实现 , 以及收集的依赖由 watcher 实例变成了组件副作用渲染函数 。