非原始值的响应式方案 - Object

理解 Proxy 和 Reflect

vue.js 3 的响应式数据是基于 Proxy 实现的,因此我们也有必要了解 Proxy 以及与之关联的 Reflect。

使用 Proxy 创建一个对象,可以实现对其他对象的代理。Proxy 只能代理对象,无法代理非对象值,例如字符串、布尔值等。

所以代理,指的是对一个对象基本语义的代理。它允许我们拦截并重新定义对一个对象的基本操作。

类似读取、设置属性值的操作,就属于基本语义的操作,即基本操作。

let obj = { foo: 1 }; obj.foo; // 读取属性 foo 的值 obj.foo++; // 读取和设置属性 foo 的值

既然是基本操作,就可以使用 Proxy 拦截。

const p = new Proxy(obj, { get () {}, set () {} })

Proxy 构造函数接收两个参数。第一个参数是被代理的对象,第二个参数也是一个对象,这个对象是一组夹子(trap)。其中 get 函数用来拦截读取操作,set 函数用来拦截设置操作。

在 JavaScript 世界里,万物皆对象。例如一个函数也是一个对象,所以调用函数也是对一个对象的基本操作。因此,我们可以用 Proxy 拦截函数的调用操作,这里我们使用 apply 拦截函数的调用。

const fn = name => { console.log('我是: ', name); } const p2 = new Proxy(fn, { apply (target, thisArg, argArray) { target.call(thisArg, ...argArray); } }); p2('heora'); // 我是: heora

Proxy 只能够拦截对一个对象的基本操作。调用对象下的方法属于非基本操作,我们叫它复合操作。

obj.fn();

调用一个对象下的方法, 是由两个基本语义组成的。第一个基本语义是 get,即先通过 get 操作得到 obj.fn 属性。第二个语义是函数调用,即通过 get 得到 obj.fn 的值后在调用它,也就是我们上面说到的 apply。理解 Proxy 只能代理对象的基本语义很重要。当我们实现对数组或 Map、Set 等数据类型的代理时,都利用了 Proxy 的这个特点。

了解了 Proxy,我们再来讨论 Reflect。Reflect 是一个全局对象,它有很多方法。

Reflect.get(); Reflect.set(); Reflect.apply(); // ...

Reflect 下的方法与 Proxy 拦截器方法名字相同,任何在 Proxy 的拦截器中找到的方法, 都可以在 Reflect 中找到同名函数,那么这些函数的作用是什么?以 Reflect.get 函数来说,它的功能就是提供访问一个对象属性的默认行为。

const obj = { foo: 1 }; console.log(obj.foo); // 直接读取 console.log(Reflect.get(obj, 'foo')); // 通过 Reflect.get 读取

Refelct.* 方法与响应式数据的实现密切相关。

const data = { foo: 1 }; const obj = new Proxy(data, { get (target, key) { track(target, key); return target[key]; }, set (target, key, newVal) { target[key] = newVal; trigger(target, key); } });

这是我们实现响应式数据最基本的代码。在 get 和 set 拦截函数中直接使用原始对象 target 来完成对属性的读取和设置操作。

这段代码其实还有问题。

const data = { foo: 1, get bar () { return this.foo } }; // ... effect(() => { console.log(obj.bar); }); obj.foo++;

bar 属性是一个访问器属性,它返回了 this.foo 属性的值。我们在 effect 副作用函数中通过代理对象 p 访问 bar 属性。

当 effect 注册的副作用函数执行时,会读取 obj.bar 属性值,它发现 obj.bar 是一个访问器属性,因此会执行 getter 函数。由于在 getter 函数中通过 this.foo 读取了 foo 属性值,因此我们认为副作用函数与属性 foo 之间也会建立联系。当我们修改 obj.foo 时应该能够触发响应,使得副作用函数重新执行。实际并非如此,当我们修改 obj.foo 值时,副作用函数并不会重新执行。

问题出在 bar 属性的访问起函数 getter 里。

const data = { foo: 1, get bar () { return this.foo } };

在 get 拦截函数内,通过 target[key] 返回属性值。其中 target 是原始值 data,key 就是字符串 bar,所以 target[key] 相当于 data.bar。因此当我们使用 obj.bar 访问 bar 属性时,它的 getter 函数内的 this 指向的其实是原始对象 data,这说明最终访问的其实是 data.foo。在副作用函数内通过原始对象访问它的某个属性是不会建立响应联系的。

effect(() => { data.foo; // data 是原始数据,不是代理对象,不能建立响应联系 });

我们可以使用 Reflect.get 函数解决这个问题。

const data = { foo: 1, get bar () { return this.foo } }; const obj = new Proxy(data, { get (target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, set (target, key, newVal) { target[key] = newVal; trigger(target, key); } }); effect(() => { console.log(obj.bar); }); obj.foo++;

receiver 用来处理定义在 prototype 上的 getter 。

我们在代理对象的 get 拦截函数接收第三个参数 receiver,它代表谁在读取属性。

obj.bar

当我们使用代理对象 obj 访问 bar 属性时,那么 receiver 就是 obj,你可以简单地理解为函数调用中的 this。我们使用 Reflect.get(target, key, receiver) 代替之前的 target[key],这里的关键点就是第三个参数。它会使访问器属性中 bar 的 getter 函数内的 this 指向代理对象 obj。

const data = { foo: 1, get bar () { // this 为 obj 对象 return this.foo } };

this 由原始对象 data 变成代理对象 obj。这会在副作用函数与响应式数据之间建立响应联系,从而达到依赖收集的效果。如果此时再对 obj.foo 进行自增操作,会发现已经可以触发副作用函数重新执行了。

对象及 Proxy 的工作原理

我们经常听说 “JavaScript 中一切皆对象”,那么到底什么是对象那?

根据 ECMAScript 规范,在 JavaScript 中有两种对象,一种叫做常规对象(ordinary object),另一种叫做异质对象(exotic object)。这两种对象包含了 JavaScript 世界中的所有对象,任何不属于常规对象的对象都是异质对象。

我们知道,在 JavaScript 中,函数其实也是对象。假设给出一个对象 obj,如何区分它是普通对象还是函数呢?实际上,在 JavaScript 中,对象的实际语义是由对象的内部方法(internal method)指定的。所谓内部方法,指的是当我们对一个对象进行操作时在引擎内部调用的方法,这些方法对于 JavaScript 使用者来说是不可见的。当我们访问对象属性时:

obj.foo

引擎内部会调用 [[Get]] 这个内部方法来读取属性值。在 ECMAScript 规范中使用 [[xxx]] 来代表内部方法或内部槽。当然,一个对象不仅部署了 [[Get]] 这个内部方法。

对象必要的内部方法

https://tc39.es/ecma262/#sec-invariants-of-the-essential-internal-methods

内部方法 签名 描述
[[GetPrototypeOf]] () => Object | Null 查明为该对象提供继承属性的对象,null 代表没有继承属性
[[SetPrototypeOf]] (Object |Null) => Boolean 将该对象与提供继承属性的另一个对象相关联。传递 null 表示没有继承属性,返回 true 表示操作成功完成,返回 false 表示操作失败
[[IsExtensible]] () => Boolean 查明是否允许向该对象添加其他属性
[[PreventExtensions]] () => Boolean 控制能否向该对象添加新属性。如果操作成功则返回 true,操作失败则返回 false
[[GetOwnProperty]] (propertyKey) => Undefined | PropertyDescriptor 返回该对象自身属性的描述符,其键为 propertyKey,如果不存在这样的属性,则返回 undefined
[[DefineOwnProperty]] (propertyKey, PropertyDescriptor) => Boolean 创建或更改自己的属性,其键为 propertyKey,以具有由 PropertyDescriptor 描述的状态。如果该属性已创建或更新,则返回 true;如果无法创建或更新该属性,则返回 false
[[HasProperty]] (propertyKey) => Boolean 返回一个布尔值,指示该对象是否已经拥有键为 propertyKey 的自己的或继承的属性
[[Get]] (propertyKey, Receiver) => any 从该对象返回键为 propertyKey 的属性的至。如果必须运行 ECMAScript 代码来检索属性值,则在运行代码时使用 Receiver 作为 this 值
[[Set]] (propertyKey, value, Receiver) => Boolean 将键值为 propertyKey 的属性的值设置为 value。如果必须运行 ECMAScript 代码来设置属性值,则在运行代码时使用 Receiver 作为 this 值。如果成功设置了属性值,则返回 true;如果无法设置,则返回 false
[[Delete]] (propertyKey) => Boolean 从该对象删除属于自身的键为 propertyKey 的属性。如果该属性未被删除并且仍然存在,则返回 false;如果该属性已被删除或不存在,则返回 true
[[OwnPropertyKeys]] () => List of propertyKey 返回一个 List,其元素都是对象自身的属性值

包括 [[Get]] 在内,一个对象必须部署 11 种必要的内部方法。除了上面的内部方法之外,还有两个额外的必要内部方法。

额外的必要内部方法

内部方法 签名 描述
[[Call]] (any, a list of any) => any 将运行的代码与 this 对象关联。由函数调用触发。该内部方法的参数是一个 this 值和参数列表
[[Construct]] (a list of any, Object) => Object 创建一个对象。通过 new 运算符或 super 调用触发。该内部方法的第一个参数是一个 List,该 List 的元素是构造函数调用或 Super 调用的参数,第二个参数是最初应用 new 运算符的对象。实现该内部方法的对象称为构造函数

如果一个对象需要作为函数调用,那么这个对象就必须部署内部方法 [[call]]。我们可以通过内部方法和内部槽来区分对象,例如函数对象会部署内部方法 [[Call]] ,而普通对象不会。

内部方法具有多态性。不同类型的对象可能部署了相同的内部方法,但是具有不同的逻辑。例如,普通对象和 Proxy 对象都部署了 [[Get]] 这个内部方法,但它们的逻辑是不同的,普通对象部署的 [[Get]] 内部方法的逻辑是由 ECMA 规范的 10.1.8 节定义的,而 Proxy 对象部署的 [[Get]] 内部方法的逻辑在 ECMA 规范的 10.5.8 节定义。

https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-get-p-receiver

https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-get-p-receiver

了解内部方法后,就可以解释什么是常规对象,什么是异质对象。

  • 对于对象必要的内部方法,必须使用 ECMA 规范 10.1.x 节给出的定义实现;
  • 对于内部方法 [[Call]],必须使用 ECMA 规范 10.2.1 节给出的定义实现;
  • 对于内部方法 [[Construct]] ,必须使用 ECMA 规范 10.2.2 节给出的定义实现。

所有不符合这三点要求的对象都是异质对象。例如,由于 Proxy 对象的内部方法 [[Get]] 没有使用 ECMA 规范 10.1.8 节给出的定义实现,所以 Proxy 是一个异质对象。

Proxy 是一个对象,它本身部署了上述必要的内部方法,当我们通过代理对象访问属性值时:

const p = new Proxy(obj, { /** ... */ }); p.foo

引擎会调用部署在在对象 p 上的内部方法 [[Get]] 。代理对象和普通对象没有太大区别。它们的区别在于对于内部方法 [[Get]] 的实现,这里就体现了内部方法的多态性,即不同的对象部署相同的内部方法,但它们的行为可能不同。具体体现在,如果在创建代理对象时没有指定对应的拦截函数,例如没有指定 get() 拦截函数,那么当我们通过代理对象访问属性值时,代理对象的内部方法 [[Get]] 会调用原始对象的内部方法 [[Get]] 来获取属性值,这其实就是代理透明性质。

创建代理对象时指定的拦截函数,实际上是用来自定义代理对象本身的内部方法和行为,而不是用来指定被代理对象的内部方法和行为的。下面列出了 Proxy 对象部署的所有内部方法以及用来自定义内部方法和行为的拦截函数名字。

Proxy 对象部署的内部方法

Proxy 对象部署的所有内部方法。

内部方法 处理器函数
[[GetPrototypeOf]] getPrototyeOf
[[SetPrototypeOf]] setPrototypeOf
[[IsExtensible]] isExtensible
[[PreventExtensions]] preventExtensions
[[GetOwnProperty]] getOwnPropertyDescriptor
[[DefineOwnProperty]] defineProperty
[[HasProperty]] has
[[Get]] get
[[Set]] set
[[Delete]] deleteProperty
[[OwnPropertyKeys]] ownKeys
[[Call]] apply
[[Construct]] construct

其中 [[Call]][[Construct]] 这两个内部方法只有当被代理的对象是函数和构造函数时才会部署。

我们要拦截删除属性操作时,可以使用 deleteProperty 拦截函数实现。

const data = { foo: 1 }; const obj = new Proxy(data, { deleteProperty (target, key) { return Reflect.deleteProperty(target, key); } }); console.log(obj.foo); delete obj.foo; console.log(obj.foo);

deleteProperty 实现的是大力对象 obj 的内部方法和行为,所以为了删除被代理对象上的属性值,我们需要使用 Reflect.deleteProperty(target, key) 来完成。

如何代理对象

之前我们使用 get 拦截函数去拦截对属性的读取操作。在响应系统中,“读取” 是一个很宽泛的概念,例如使用 in 操作符检查对象上是否具有给定的 key 也属于 “读取” 操作。

effect(() => {[ 'foo' on obj ]});

这本质是也是在进行 “读取” 操作。响应系统应该拦截一切读取操作,以便当数据变化时能够正确地触发响应。

  • 访问属性:obj.foo
  • 判断对象或原型上是否存在给定的 key:key in obj
  • 使用 for ... in 循环遍历对象: for (const key in obj) {}

接下来,我们逐步讨论如何拦截这些读取操作。对于属性的读取,例如 obj.foo,我们可以使用 get 拦截函数实现。

const obj = { foo: 1 }; const p = new Proxy(obj, { get (target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, set (target, key, newVal) { target[key] = newVal; trigger(target, key); }, });

对于 in 操作符,应该如何拦截?在 ECMA-262 规范的 13.10.1 节中,明确定义了 in 操作符的运行时逻辑。

https://tc39.es/ecma262/#sec-relational-operators-runtime-semantics-evaluation

RelationalExpression : RelationalExpression in ShiftExpression 1. Let lref be the result of evaluating RelationalExpression. 2. Let lval be ? GetValue(lref). 3. Let rref be the result of evaluating ShiftExpression. 4. Let rval be ? GetValue(rref). 5. If Type(rval) is not Object, throw a TypeError exception. 6. Return ? HasProperty(rval, ? ToPropertyKey(lval)).
  1. lref 的值为 RelationalExpression 的执行结果;
  2. lval 的值为 ? GetValue(lref) ;
  3. rref 的值为 ShiftExpression 的执行结果;
  4. rval 的值为 ? GetValue(rref) ;
  5. 如果 Type(rval) 不是对象,则抛出异常;
  6. 返回 ? HasProperty(rval, ? ToPropertyKey(lval)).

关键点在第 6 步,可以发现,in 操作符的运算结果是通过调用一个叫做 HasProperty 的抽象方法得到的。

HasProperty 抽象方法,可以在 ECMA-262 规范中的 7.3.12 找到。

https://tc39.es/ecma262/#sec-hasproperty

1. Return ? O.[[HasProperty]](P).

HasProperty 抽象方法的返回值是通过调用对象的内部方法 [[HasProperty]] 得到的。[[HasProperty]] 是对象必要的内部方法,它对应的拦截函数叫 has,因此我们可以通过 has 拦截函数实现对 in 操作符的处理。

const obj = { foo: 1 }; const p = new Proxy(obj, { get (target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, has (target, key) { track(target, key); return Reflect.has(target, key); }, set (target, key, newVal) { target[key] = newVal; trigger(target, key); }, });

这样我们在副作用函数中通过 in 操作符操作响应式数据时,就能够建立依赖关系。

接着来看如何拦截 for...in 循环。我们所有能够拦截的方法有 13 种,它们是一个对象的所有基本语义方法,也就是说,任何操作其实都是由这些基本语义方法及其组合实现的,for...in 循环也不例外。

https://tc39.es/ecma262/#sec-runtime-semantics-forinofheadevaluation

6. If iterationKind is enumerate, then a. If exprValue is undefined or null, then i. Return Completion Record { [[Type]]: break, [[Value]]: empty, [[Target]]: empty }. b. Let obj be ! ToObject(exprValue). c. Let iterator be EnumerateObjectProperties(obj). d. Let nextMethod be ! GetV(iterator, "next"). e. Return the Iterator Record { [[Iterator]]: iterator, [[NextMethod]]: nextMethod, [[Done]]: false }.

在 ECMA 262 规范的 14.7.5.6 节中定义了 for...in 头部的执行规则。

  1. 如果 iterationKind 是枚举(enumerate),则
    1. 如果 exprValue 是 undefined 或 null,返回 Completion Record { [[Type]]: break, [[Value]]: empty, [[Target]]: empty }
    2. obj 的值为 ! ToObject(exprValue)
    3. iterator 的值为 EnumerateObjectProperties(obj)
    4. nextMethod 的值为 ! GetV(iterator, "next")
    5. 返回 Iterator Record { [[Iterator]]: iterator, [[NextMethod]]: nextMethod, [[Done]]: false }

仔细观察这一子步骤:

让 iterator 的值为 EnumerateObjectProperties(obj)

其中的关键点在于 EnumerateObjectProperties(obj) 。这里的 EnumerateObjectProperties 是一个抽象方法,该方法返回一个迭代器对象,规范中的 14.7.5.9 给出了满足该抽象方法的示例实现。

https://tc39.es/ecma262/#sec-enumerate-object-properties

function* EnumerateObjectProperties(obj) { const visited = new Set(); for (const key of Reflect.ownKeys(obj)) { if (typeof key === "symbol") continue; const desc = Reflect.getOwnPropertyDescriptor(obj, key); if (desc) { visited.add(key); if (desc.enumerable) yield key; } } const proto = Reflect.getPrototypeOf(obj); if (proto === null) return; for (const protoKey of EnumerateObjectProperties(proto)) { if (!visited.has(protoKey)) yield protoKey; } }

可以看到,该方法是一个 generator 函数,接收一个参数 obj。实际上,obj 就是被 for...in 循环遍历的对象,其关键点在于使用 Reflect.ownKeys(obj) 来获取只属于对象自身拥有的键。我们可以使用 ownKeys 拦截函数来拦截 Reflect.ownKeys 操作。

const obj = { foo: 1 }; const ITERATE_KEY = Symbol(); const p = new Proxy(obj, { // ... ownKeys (target) { track(target, ITERATE_KEY); return Reflect.ownKeys(target); } });

拦截 ownKeys 操作即可间接拦截 for...in 循环。但是我们为什么要使用 ITERATE_KEY 作为追踪的 key?

ownKeys 拦截函数与 get/set 拦截函数不同,在 get/set 中,我们可以得到具体操作的 key,但是在 ownKeys 中,我们只能拿到目标对象 target。在读写属性值时,可以明确地知道当前正在操作哪一个属性,所以只需要在该属性与副作用函数之间建立联系即可。ownKeys 用来获取一个对象的所有属于自己的键值,这个操作明显不与任何具体的键进行绑定,因此我们只能构造唯一的 key 作为标识,即 ITERATE_KEY

既然追踪的是 ITERATE_KEY,在触发响应的时候也应该触发它才行:

trigger(target, ITERATE_KEY);

我们用一段代码来说明。假设副作用函数内有一段 for...in 循环。

const obj = { foo: 1 }; const ITERATE_KEY = Symbol(); const p = new Proxy(obj, { get (target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, set (target, key, newVal) { target[key] = newVal; trigger(target, key); }, ownKeys (target) { track(target, ITERATE_KEY); return Reflect.ownKeys(target); } }); effect(() => { for (const key in p) { console.log(key); } }); p.bar = 2;

由于对象 p 原本只有 foo 属性,因此 for…in 循环只会执行一次。现在为它添加了新的属性 bar,所以 for…in 循环就会由执行一次变成执行两次。也就是说,当为对象添加新属性时,会对 for…in 循环产生影响,所以需要触发与 ITERATE_KEY 相关联的副作用函数重新执行。但我们之前的 effect 实现还做不到这一点。

const p = new Proxy(obj, { get (target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, set (target, key, newVal) { const res = Reflect.set(target, key, newVal); trigger(target, key); return res; }, ownKeys (target) { track(target, ITERATE_KEY); return Reflect.ownKeys(target); } });

当为对象 p 添加新的 bar 属性时,会触发 set 拦截函数执行。此时 set 拦截函数接收到的 key 就是字符串 ‘bar’,因此最终调用 trigger 函数时也只是触发与 ‘bar’ 相关联的副作用函数重新执行。但根据前文的介绍,我们知道 for…in 循环是在副作用函数与 ITERATE_KEY 之间建立联系,这和 ‘bar’ 一点关系都没有,因此当我们尝试执行 p.bar = 2 操作时,并不会正确地触发响应。

弄清楚问题在哪,我们就可以解决这个问题了。当添加属性时,我们将那些与 ITERATE_KEY 相关联的副作用函数也取出来执行。

function trigger (target, key) { // 使用 target 从 bucket 中获取 depsMap,key -> effects const depsMap = bucket.get(target); if (!depsMap) return; // 根据 key 从 depsMap 中获取 effects const effects = depsMap.get(key); // 获取与 ITERATE_KEY 相关联的副作用函数(☆☆☆) const iterateEffects = depsMap.get(ITERATE_KEY); const effectsToRun = new Set(); // 将与 key 相关联的副作用函数添加到 effctesToRun effects && effects.forEach(effectFn => { // 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行 if (effectFn !== activeEffect) { effectsToRun.add(effectFn); } }) // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun(☆☆☆) iterateEffects && iterateEffects.forEach(effectFn => { if (effectFn !== activeEffect) { effectsToRun.add(effectFn); } }); // effects && effects.forEach(fn => fn()); 避免与 cleanup 产生死循环 effectsToRun.forEach(effectFn => { // 如果存在调度器,则调用该调度器,并将副作用函数作为参数传递 if (effectFn.options.scheduler) { effectFn.options.scheduler(effectFn); } else { effectFn(); } }); }

当 trigger 函数执行时,除了把那些直接与具体操作的 key 相关联的副作用取出来执行外,还要把那些与 ITERATE_KEY 相关联的副作用函数取出来执行。

const obj = { foo: 1 }; const p = new Proxy(obj, { get (target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, set (target, key, newVal) { const res = Reflect.set(target, key, newVal); trigger(target, key); return res; }, ownKeys (target) { track(target, ITERATE_KEY); return Reflect.ownKeys(target); } }); effect(() => { for (const key in p) { console.log(key); } }); p.bar = 2;

对于添加新的属性来说,这么做没有什么问题,但如果仅仅修改已有属性的值,就会存在问题。

p.foo = 2;

与添加属性不同,修改属性不会对 for...in 循环产生影响。因为无论怎么修改一个属性的值,对于 for...in 玄幻来说都只会循环一次。所以在这种情况下,我们不需要触发副作用函数重新执行,否则会造成不必要的性能开销。然而无论是添加新属性,还是修改已有的属性值,其基本语义都是 [[Set]],我们都是通过 set 拦截函数来实现拦截的。

const p = new Proxy(obj, { // ... set (target, key, newVal) { const res = Reflect.set(target, key, newVal); trigger(target, key); return res; }, // ... });

所以如果相解决上述问题,当设置属性操作发生时,就需要我们在 set 拦截函数内能够区分操作的类型,区分出是添加新属性还是设置已有属性。

const obj = { foo: 1 }; const hasOwnProperty = (target, key) => Object.prototype.hasOwnProperty.call(target, key); const p = new Proxy(obj, { get (target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, ownKeys (target) { track(target, ITERATE_KEY); return Reflect.ownKeys(target); }, set (target, key, newVal, receiver) { const type = hasOwnProperty(target, key) ? 'SET' : 'ADD'; const res = Reflect.set(target, key, newVal, receiver); trigger(target, key, type); return res; }, });

我们优先使用 Object.prototype.hasOwnProperty 检查当前操作的属性是否已经存在于目标对象上,如果存在,则说明当前操作类型为 ‘SET’,即修改属性值;否则认为当前操作类型为 ‘ADD’,即添加新属性。我们把类型作为第三个参数传递给 trigger 函数。

在 trigger 函数内就可以通过类型 type 来区分当前的操作类型,并且只有当操作类型 type 为 ‘ADD’ 时,才会触发 ITERATE_KEY 相关联的副作用函数重新执行,这样就避免了不需要的性能损耗。

function trigger (target, key, type) { // 使用 target 从 bucket 中获取 depsMap,key -> effects const depsMap = bucket.get(target); if (!depsMap) return; // 根据 key 从 depsMap 中获取 effects const effects = depsMap.get(key); const effectsToRun = new Set(); // 将与 key 相关联的副作用函数添加到 effctesToRun effects && effects.forEach(effectFn => { // 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行 if (effectFn !== activeEffect) { effectsToRun.add(effectFn); } }) if (type === 'ADD') { // 获取与 ITERATE_KEY 相关联的副作用函数 const iterateEffects = depsMap.get(ITERATE_KEY); // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun iterateEffects && iterateEffects.forEach(effectFn => { if (effectFn !== activeEffect) { effectsToRun.add(effectFn); } }); } // effects && effects.forEach(fn => fn()); 避免与 cleanup 产生死循环 effectsToRun.forEach(effectFn => { // 如果存在调度器,则调用该调度器,并将副作用函数作为参数传递 if (effectFn.options.scheduler) { effectFn.options.scheduler(effectFn); } else { effectFn(); } }); }

通常我们会将操作类型封装为一个枚举值。

const TRIGGER_TYPE = { SET: 'SET', ADD: 'ADD' };

这样无论是对后期代码维护,还是对代码的清晰度,都是非常有帮助的。

关于对象的代理,还剩下最后一部分,就是删除属性操作的代理。

delete p.foo;

如何代理 delete 操作符呢?规范的 13.5.1.2 节中明确定义了 delete 操作符的行为。

13.5.1.2 Runtime Semantics: Evaluation UnaryExpression : delete UnaryExpression 1. Let ref be the result of evaluating UnaryExpression. 2. ReturnIfAbrupt(ref). 3. If ref is not a Reference Record, return true. 4. If IsUnresolvableReference(ref) is true, then a. Assert: ref.[[Strict]] is false. b. Return true. 5. If IsPropertyReference(ref) is true, then a. Assert: IsPrivateReference(ref) is false. b. If IsSuperReference(ref) is true, throw a ReferenceError exception. c. Let baseObj be ? ToObject(ref.[[Base]]). d. Let deleteStatus be ? baseObj.[[Delete]](ref.[[ReferencedName]]). e. If deleteStatus is false and ref.[[Strict]] is true, throw a TypeError exception. f. Return deleteStatus. 6. Else, a. Let base be ref.[[Base]]. b. Assert: base is an Environment Record. c. Return ? base.DeleteBinding(ref.[[ReferencedName]]).
  1. 如果 IsPropertyReference(ref) 是 true,那么
    1. 断言:IsPrivateReference(ref) 是 false
    2. 如果 IsSuperReference(ref) 也是 true,则抛出 ReferenceError 异常
    3. baseObj 的值为 ? ToObject(ref.[[Base]])
    4. deleteStatus 的值为 ? baseObj.[[Delete]](ref.[[ReferencedName]])
    5. 如果 deleteStatus 的值为 false 并且 ref.[[Strict]] 的值是 true,则抛出 TypeError 异常
    6. 返回 deleteStatus

由第 5 步的 d 子步骤可知,delete 操作符的行为依赖 [[Delete]] 内部方法。该内部方法可以使用 deleteProperty 拦截。

const TRIGGER_TYPE = { SET: 'SET', ADD: 'ADD', DELETE: 'DELETE' }; const p = new Proxy(obj, { // ... deleteProperty (target, key) { // 检查被操作的属性是否是对象自己的属性 const hadKey = Object.prototype.hasOwnProperty.call(target, key); // 使用 Reflect.deleteProperty 删除属性 const res = Reflect.deleteProperty(target, key); if (res && hadKey) { // 只有当被删除属性时对象自身属性并且删除成功时,才出发更新 trigger(target, key, TRIGGER_TYPE.DELETE); } return res; } });

在调用 trigger 函数时,我们传递了新的操作类型 ‘DELETE’。由于删除操作会使得对象的建变少,它会影响 for...in 循环的次数,因此当操作类型为 ‘DELETE’ 时,我们也应该触发那些与 ITERATE_KEY 相关联的副作用函数重新执行。

function trigger (target, key, type) { // 使用 target 从 bucket 中获取 depsMap,key -> effects const depsMap = bucket.get(target); if (!depsMap) return; // 根据 key 从 depsMap 中获取 effects const effects = depsMap.get(key); const effectsToRun = new Set(); // 将与 key 相关联的副作用函数添加到 effctesToRun effects && effects.forEach(effectFn => { // 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行 if (effectFn !== activeEffect) { effectsToRun.add(effectFn); } }) // 操作类型为 ADD 或 DELETE 时,需要触发与 ITERATE_KEY 相关联的副作用函数执行 if (type === TRIGGER_TYPE.ADD || type === TRIGGER_TYPE.DELETE) { // 获取与 ITERATE_KEY 相关联的副作用函数 const iterateEffects = depsMap.get(ITERATE_KEY); // 将与 ITERATE_KEY 相关联的副作用函数也添加到 effectsToRun iterateEffects && iterateEffects.forEach(effectFn => { if (effectFn !== activeEffect) { effectsToRun.add(effectFn); } }); } // effects && effects.forEach(fn => fn()); 避免与 cleanup 产生死循环 effectsToRun.forEach(effectFn => { // 如果存在调度器,则调用该调度器,并将副作用函数作为参数传递 if (effectFn.options.scheduler) { effectFn.options.scheduler(effectFn); } else { effectFn(); } }); }

在这段代码中,我们添加 type === 'DELETE' 判断,使得删除属性操作能够触发与 ITERATE_KEY 相关联的副作用函数重新执行。

const { effect, track, trigger, ITERATE_KEY, TRIGGER_TYPE } = require('../shared/effect'); const obj = { foo: 1 }; const p = new Proxy(obj, { get (target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, ownKeys (target) { track(target, ITERATE_KEY); return Reflect.ownKeys(target); }, set (target, key, newVal, receiver) { const type = Object.prototype.hasOwnProperty.call(target, key) ? TRIGGER_TYPE.SET : TRIGGER_TYPE.ADD; const res = Reflect.set(target, key, newVal, receiver); trigger(target, key, type); return res; }, ownKeys (target) { track(target, ITERATE_KEY); return Reflect.ownKeys(target); }, deleteProperty (target, key) { const hadKey = Object.prototype.hasOwnProperty.call(target, key); const res = Reflect.deleteProperty(target, key); if (res && hadKey) { trigger(target, key, TRIGGER_TYPE.DELETE); } return res; } }); effect(() => { for (const key in p) { console.log(key); } }); // p.bar = 2; // p.foo = 2; delete p.foo;

合理地触发响应

我们从规范的角度介绍了如何代理对象,这个过程中,我们处理了很多边界条件。例如,我们需要明确知道操作的类型是 ‘ADD’ 还是 ‘SET’,或者是其他操作类型,从而正确地触发响应。但想要合理地触发响应,还有许多工作要做。

下面我们来看第一个问题,即当值没有发生变化时,不需要触发影响。

const obj = { foo: 1 }; const p = new Proxy(obj, { /* ... */ }); effect(() => { console.log(p.foo); }); p.foo = 1;

p.foo 的初始值为 1,当为 p.foo 设置新的值时,如果值没有发生变化,则不需要触发响应。为了满足需求,我们需要修改 set 拦截函数的代码,在调用 trigger 函数触发响应之前,需要检查值是否真的发生了变化。

const p = new Proxy(obj, { // ... set (target, key, newVal, receiver) { const oldVal = target[key]; const type = Object.prototype.hasOwnProperty.call(target, key) ? TRIGGER_TYPE.SET : TRIGGER_TYPE.ADD; const res = Reflect.set(target, key, newVal, receiver); if (oldVal !== newVal) { trigger(target, key, type); } return res; }, // ... });

我们在 set 拦截函数内首先获取旧值 oldVal,接着比较新值与旧值,只有当它们不全等的时候才触发响应。

但是仅仅进行全等比较是有缺陷的,体现在对 NaN 的处理上 。NaNNaN 进行全等比较总会得到 false。

const obj = { foo: NaN }; effect(() => { console.log(p.foo); }); p.foo = NaN; // NaN // NaN

为了解决这个问题,我们需要再加一个条件,新值和旧值不全等的情况下,保证它们都不是 NaN。这样就解决了 NaN 的问题。

const p = new Proxy(obj, { // ... set (target, key, newVal, receiver) { const oldVal = target[key]; const type = Object.prototype.hasOwnProperty.call(target, key) ? TRIGGER_TYPE.SET : TRIGGER_TYPE.ADD; const res = Reflect.set(target, key, newVal, receiver); // 不全等且都不是 NaN if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) { trigger(target, key, type); } return res; }, // ... });

接下来,我们讨论一种从原型上继承属性的情况。为了讲解方便,我们需要封装一个 reactive 函数,该函数接收一个对象作为参数,并返回为其创建的响应式数据。

const { track, trigger, ITERATE_KEY, TRIGGER_TYPE } = require('../shared/effect'); function reactive (obj) { return new Proxy(obj, { get (target, key, receiver) { track(target, key); return Reflect.get(target, key, receiver); }, ownKeys (target) { track(target, ITERATE_KEY); return Reflect.ownKeys(target); }, set (target, key, newVal, receiver) { const oldVal = target[key]; const type = Object.prototype.hasOwnProperty.call(target, key) ? TRIGGER_TYPE.SET : TRIGGER_TYPE.ADD; const res = Reflect.set(target, key, newVal, receiver); if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) { trigger(target, key, type); } return res; }, ownKeys (target) { track(target, ITERATE_KEY); return Reflect.ownKeys(target); }, deleteProperty (target, key) { const hadKey = Object.prototype.hasOwnProperty.call(target, key); const res = Reflect.deleteProperty(target, key); if (res && hadKey) { trigger(target, key, TRIGGER_TYPE.DELETE); } return res; } }); }

reactive 函数只是对 Proxy 进行了一层封装。我们可以基于 reactive 创建一个例子。

const obj = {}; const proto = { bar: 1 }; const child = reactive(obj); const parent = reactive(proto); Object.setPrototypeOf(child, parent); effect(() => { console.log(child.bar); }); child.bar = 2; // 会导致副作用函数重新执行两次 1 2 2

我们定义了空对象 obj 和对象 proto,分别为两者创建了对应的响应式数据 child 和 parent,并且使用 Object.setProtypeOf 方法将 parent 设置为 child 的原型。我们在副作用函数内访问 child.bar 的值。child 本身并没有 bar 属性,因此当访问 child.bar 时,值是从原型上继承而来的。但既然 child 是响应式数据,那么它与副作用函数之间就会建立联系,因此当我们执行 child.bar = 2 时,副作用函数会重新触发。但是执行代码你会发现,副作用函数不仅执行了,还执行了两次。

当在副作用函数中读取 child.bar 的值时,会触发 child 代理对象的 get 拦截函数。在拦截函数内使用 Reflect.get(target, key, receiver) 来得到最终结果。

Reflect.get(obj, 'bar', receiver);

这其实是实现通过 obj.bar 来访问属性值的默认行为。引擎内部是通过调用 obj 对象所部署的 [[Get]] 内部方法来得到最终结果的,因此我们有必要查看规范 10.1.8.1 节来了解 [[Get]] 内部方法的执行流程。

1. Let desc be ? O.[[GetOwnProperty]](P). 2. If desc is undefined, then a. Let parent be ? O.[[GetPrototypeOf]](). b. If parent is null, return undefined. c. Return ? parent.[[Get]](P, Receiver). 3. If IsDataDescriptor(desc) is true, return desc.[[Value]]. 4. Assert: IsAccessorDescriptor(desc) is true. 5. Let getter be desc.[[Get]]. 6. If getter is undefined, return undefined. 7. Return ? Call(getter, Receiver).
  1. 如果 desc 是 undefined,那么
    1. 让 parent 的值为 ? O.[[GetPrototypeOf]]()
    2. 如果 parent 是 null,则返回 undefined
    3. 返回 ? parent.[[Get]](P, Receiver)

如果对象自身不存在该属性,那么会获取对象的原型,并调用原型的 [[Get]] 方法得到最终结果。也就是说,当读取 child.bar 属性值时,由于 child 代理的对象 obj 自身没有 bar 属性,因此会获取对象 obj 的原型,也就是 parent 对象,所以最终得到的实际上 parent.bar 的值。parent 本身也是响应式对象,因此在副作用函数中访问 parent.bar 的值时,会导致副作用函数被收集,从而建立响应联系。child.barparent.bar 都与副作用函数建立了响应联系。

我们还需要看看设置操作发生时的具体执行流程。当执行 child.bar = 2 时,会调用 child 代理对象的 set 拦截函数。同样,在 set 拦截函数中,我们用 Reflect.set(target, key, newVal, receiver) 来完成默认的设置行为,即引擎会调用 obj 对象部署的 [[Set]] 内部方法,根据规范 10.1.9.2 节可知 [[Set]] 内部方法的执行流程。

1. If ownDesc is undefined, then a. Let parent be ? O.[[GetPrototypeOf]](). b. If parent is not null, then i. Return ? parent.[[Set]](P, V, Receiver). c. Else, i. Set ownDesc to the PropertyDescriptor { [[Value]]: undefined, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }.
  1. 如果 ownDesc 是 undefined,那么
    1. 让 parent 的值为 O.[[GetPrototypeOf]]()
    2. 如果 parent 不是 null,则
      1. 返回 ? parent.[[Set]](P, V, Receiver)
    3. 否则
      1. ownDesc 设置为 { [[Value]]: undefined, [[Writable]]: true, [[Enumerable]]: true, [[Configurable]]: true }

如果设置的属性不存在于对象上,那么会取得其原型,并调用原型的 [[Set]] 方法,也就是 parent 的 [[Set]] 内部方法。由于 parent 是代理对象,所以这就相当于执行它的 set 拦截函数。换句话说,虽然我们操作的是 child.bar,但这也会导致 parent 代理对象 set 拦截函数被执行。当读取 child.bar 的值时,副作用函数不仅会被 child.bar 收集,也会被 parent.bar 收集。所以当 parent 代理对象的 set 拦截函数执行时,就会触发副作用函数重新执行,那就是为什么修改 child.bar 的值会导致副作用函数重新执行两次。

其实解决思路也很简单,既然执行两次,那么只要屏蔽其中一次就可以。我们可以把由 parent.bar 触发的那次副作用函数的重新执行屏蔽。两次更新是由于 set 拦截函数被触发两次导致的,所以只要我们能够在 set 拦截函数内区分这两次更新就可以了。当我们设置 child.bar 的值时,会执行 child 代理对象的 set 拦截函数。

// child 的 set 拦截函数 set (target, key, value, receiver) { // target 是原始对象 obj // receiver 是代理对象 child }

此时的 target 是原始对象 obj,receiver 是代理对象 child,我们发现 receiver 其实就是 target 的代理对象。

但由于 obj 上不存在 bar 属性,所以会取得 obj 的原型 parent,并执行 parent 代理对象的 set 拦截函数:

// parent 的 set 拦截函数 set (target, key, value, receiver) { // target 是原始对象 proto // receiver 仍然是代理对象 child }

当 parent 代理对象的 set 拦截函数执行时,此时 target 是原始对象 proto,而 receiver 仍然是代理对象 child,而不再是 target 的代理对象。通过这个特点,我们可以看到 target 和 receiver 的区别。由于我们设置的是 child.bar 的值,所以无论是在什么情况下,receiver 都是 child,而 target 则是变化的。根据这个区别,我们很容易就可以想到解决办法,只需要判断 receiver 是否是 target 的代理对象即可。只有当 receiver 是 target 的代理对象时才触发更新,这样就能够屏蔽由原型引起的更新了。

所以接下来的问题就变成如何确定 receiver 是不是 target 的代理对象,这需要我们为 get 拦截函数添加一个能力。

function reactive (obj) { return new Proxy(obj, { get (target, key, receiver) { if (key === 'raw') { return target; } track(target, key); return Reflect.get(target, key, receiver); } }); }

我们增加了一段代码,它可以让代理对象通过 raw 属性读取原始数据。

console.log(child.raw === obj); // true console.log(parent.raw === proto); // true

有了它,我们就能够在 set 拦截函数中判断 receiver 是不是 target 的代理对象。

function reactive (obj) { return new Proxy(obj, { get (target, key, receiver) { if (key === 'raw') { return target; } track(target, key); return Reflect.get(target, key, receiver); }, set (target, key, newVal, receiver) { const oldVal = target[key]; const type = Object.prototype.hasOwnProperty.call(target, key) ? TRIGGER_TYPE.SET : TRIGGER_TYPE.ADD; const res = Reflect.set(target, key, newVal, receiver); // taget === receiver.raw 说明 receiver 是 target 的代理对象 if (target === receiver.raw) { if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) { trigger(target, key, type); } } return res; } }); }

我们新增了一个判断条件,只有当 receiver 是 target 的代理对象时才触发更新,这样就能屏蔽由原型引起的更新,从而避免不必要的更新操作。

浅响应与深响应

这一节我们介绍 reactive 与 shallowReactive 的区别,即深响应和浅响应的区别。实际上,我们目前所实现的 reactive 是浅响应的。

const obj = reactive({ foo: { bar: 1 } }); effect(() => { console.log(obj.foo.bar); }); obj.foo.bar = 2; // 修改 obj.foo.bar 的值,并不能触发响应

我们创建 obj 代理对象,该对象的 foo 属性值也是一个对象,即 { bar: 1 } 。在副作用函数内访问 obj.foo.bar 的值时,会发现后续对 obj.foo.bar 的修改不能触发副作用函数重新执行。

function reactive (obj) { return new Proxy(obj, { get (target, key, receiver) { if (key === 'raw') { return target; } track(target, key); return Reflect.get(target, key, receiver); } }); }

我们读取 obj.foo.bar 时,首先要读取 obj.foo 的值。这里我们直接用 Reflect.get 函数返回 obj.foo 的结果。由于通过 Reflect.get 得到 obj.foo 的结果是一个普通对象,即 { bar: 1 },它并不是一个响应式对象,所以在副作用函数中访问 obj.foo.bar 时,是不能建立响应联系的。要解决这个问题,我们需要对 Reflect.get 返回的结果做一层包装:

const isPlainObject = (data) => typeof data === 'object' && data !== null; function reactive (obj) { return new Proxy(obj, { get (target, key, receiver) { if (key === 'raw') { return target; } track(target, key); const res = Reflect.get(target, key, receiver); if (isPlainObject(res)) { return reactive(res); } return res; }, }); }

当读取属性值时,我们首先检测该值是否是对象,如果是对象,则递归地调用 reactive 函数将其包装成响应式数据并返回。这样当使用 obj.foo 读取 foo 属性值时,得到的就会是一个响应式数据,因此再通过 obj.foo.bar 读取 bar 属性值时,自然就会建立响应联系。这样,当修改 obj.foo.bar 的值时,就能够触发副作用函数重新执行了。

但是,并非所有情况下我们都希望深响应,这就催生了 shallowReactive,即浅响应。所谓浅响应,指的是只有对象的第一层属性是响应的。

const obj = shallowReactive({ foo: { bar: 1 } }); effect(() => { console.log(obj.foo.bar); }); obj.foo = { bar: 2 }; // 响应的,可以触发副作用函数并执行 obj.foo.bar = 3; // 不是响应的,不能触发副作用函数重新执行

我们使用 shallowReactive 函数创建了一个浅响应的代理对象 obj。可以发现,只有对象的第一层属性是响应的,第二层及更深层次的属性则不是响应的。

function crateReactive (obj, isShallow = false) { return new Proxy(obj, { get (target, key, receiver) { if (key === 'raw') { return target; } track(target, key); const res = Reflect.get(target, key, receiver); if (isShallow) { return res; } if (isPlainObject(res)) { return reactive(res); } return res; } }); } function reactive (obj) { return crateReactive(obj); } function shallowReactive (obj) { return crateReactive(obj, true); }

我们可以把对象创建的工作封装到一个新的函数 createReactive 中。该函数除了接收原始对象 obj 之外,还接收参数 isShallow,代表是否创建浅响应对象。默认情况下,isShallow 的值为 false,代表创建深响应对象。当读取属性操作发生时,在 get 拦截函数内如果发现是浅响应的,那么直接返回原始数据即可。

只读和浅只读

我们希望一些数据是只读的,当用户尝试修改只读数据时,会收到警告信息。这样就可以实现对数据的保护,例如组件接收到的 props 对象应该是一个只读数据。这时就需要接下来要讨论的 readonly 函数,它能够将一个数据变成只读的。

const obj = readonly({ foo: 1 }); obj.foo = 2;

只读本质上也是对数据对象的代理,我们同样可以使用 createReactive 函数来实现。我们可以为 createReactive 函数增加第三个参数 isReadonly

function crateReactive (obj, isShallow = false, isReadonly = false) { return new Proxy(obj, { // ... set (target, key, newVal, receiver) { if (isReadonly) { console.warn(`属性 ${ key } 是只读的`); return true; } const oldVal = target[key]; const type = Object.prototype.hasOwnProperty.call(target, key) ? TRIGGER_TYPE.SET : TRIGGER_TYPE.ADD; const res = Reflect.set(target, key, newVal, receiver); if (target === receiver.raw) { if (oldVal !== newVal && (oldVal === oldVal || newVal === newVal)) { trigger(target, key, type); } } return res; }, // ... deleteProperty (target, key) { if (isReadonly) { console.warn(`属性 ${ key } 是只读的`); return true; } const hadKey = Object.prototype.hasOwnProperty.call(target, key); const res = Reflect.deleteProperty(target, key); if (res && hadKey) { trigger(target, key, TRIGGER_TYPE.DELETE); } return res; } }); } function readonly (obj) { return crateReactive(obj, false, true); }

我们使用 createReactive 创建代理对象时,可以通过第三个参数指定是否创建一个只读的代理对象。同时,我们还修改了 get 拦截函数和 deleteProperty 拦截函数的实现,对于一个对象来说,只读意味着既不可以设置对象的属性值,也不可以删除对象的属性。在这个两个拦截函数中,我们分别添加了是否是只读的判断,一旦数据是只读的,则当这些操作发生时,会打印警告信息,提示用户这是一个非法操作。

其次,如果一个数据是只读的,那就意味着任何方式都无法修改它。因此,没有必要为制度数据建立响应联系。处于这个原因,当在副作用函数中读取一个只读属性的值时,不需要调用 track 函数追踪响应。

const obj = readonly({ foo: 1 }); effect(() => { console.log(obj.foo); // 可以读取值,但是不需要在副作用函数与数据之间建立响应关系 }); obj.foo = 2;

为了实现这个功能,我们需要修改 get 拦截函数的实现。

function crateReactive (obj, isShallow = false, isReadonly = false) { return new Proxy(obj, { get (target, key, receiver) { if (key === 'raw') { return target; } // 非只读才需要建立响应关系 if (!isReadonly) { track(target, key); } const res = Reflect.get(target, key, receiver); if (isShallow) { return res; } if (isPlainObject(res)) { return reactive(res); } return res; }, // ... }); }

在 get 拦截函数内检测 isReadonly 变量的值,判断是否是只读的,只有在非只读的情况下才会调用 track 函数建立响应关系。

我们目前实现的 readonly 函数更应该叫做 shallowReadonly ,因为它没有做到深只读。

const obj = readonly({ foo: { bar: 1 } }); effect(() => { console.log(obj.foo.bar); }); obj.foo.bar = 2; // 1 // 2

为了实现深只读,我们还应该在 get 函数内地递归地调用 readonly 将数据包装成只读的代理对象,并将其作为返回值返回。

function crateReactive (obj, isShallow = false, isReadonly = false) { return new Proxy(obj, { get (target, key, receiver) { if (key === 'raw') { return target; } if (!isReadonly) { track(target, key); } const res = Reflect.get(target, key, receiver); if (isShallow) { return res; } if (isPlainObject(res)) { return isReadonly ? readonly(res) : reactive(res); } return res; }, }); }

如上面的代码所示,我们在返回属性值之前,判断它是否是只读的,如果是只读的,则调用 readonly 函数对值进行包装,并把包装后的只读对象返回。

对于 shallowReadonly ,我们只需要修改 createReactive 的第二个参数即可。

function readonly (obj) { return crateReactive(obj, false, true); } function shallowReadonly (obj) { return crateReactive(obj, true, true); }

shallowReadonly 函数内调用 createReactive 函数创建代理对象时,将第二个参数 isShallow 设置为 true,这样就可以创建一个浅只读的代理对象了。