Object代理 本节着手实现响应式数据。前面我们使用get去拦截对属性的读取操作。但在响应系统中,读取是一个很宽泛的概念,有很多操作都涉及到读取这一行为。下面列出了对一个普通 对象的所有可能的读取操作:
访问属性:obj.foo。  
判断对象或原型上是否存在给定的 key:key in obj。  
使用 for...in 循环遍历对象:for (const key in obj) {}。 
 
对于属性读取(obj.foo),我们知道这可以通过get实现,而对于in和for in应该如何拦截呢?
这是我们在上一节里完成的部分基本代码,在此基础上实现后续功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 const  bucket = new  WeakMap ();let  activeEffect = null ;const  effectStack = [];function  effect (fn,options = {} ){    const  effectFn  = ( ) => {         cleanup (effectFn)                activeEffect = effectFn;           effectStack.push (effectFn);           const  res = fn ();          effectStack.pop ();            activeEffect = effectStack[effectStack.length  - 1 ];          return  res;      }     effectFn.options  = options;      effectFn.deps  = []      if (!options.lazy ){          effectFn ()     }     return  effectFn  } function  track (target,key ){    if (!activeEffect){         return      }     let  depsMap = bucket.get (target)     if (!depsMap){         bucket.set (target,(depsMap = new  Map ()))     }     let  deps = depsMap.get (key)     if (!deps){         depsMap.set (key,(deps = new  Set ()))     }     deps.add (activeEffect)        activeEffect.deps .push (deps) } function  trigger (target,key ){    const  depsMap = bucket.get (target)     if (!depsMap){         return      }     const  depsEffects = depsMap.get (key)     const  effectsToRun = new  Set ()      depsEffects && depsEffects.forEach (effectFn  =>  {                  if (effectFn !== activeEffect){             effectsToRun.add (effectFn)         }     })     effectsToRun&&effectsToRun.forEach (fn  =>  {                           if (fn.options .scheduler ){             fn.options .scheduler (fn)          }else {             fn ()          }     }); } function  cleanup (effectFn ){    for (let  i =0 ;i<effectFn.deps .length ;i++){          const  deps = effectFn.deps [i]          deps.delete (effectFn)      }     effectFn.deps .length  = 0  } 
 
in操作符 使用in操作符检查对象上是否具有给定的key 属于读取操作。
以下是原书中对in操作符拦截的思路:
in 操作符的运算结果是通过调用抽象方法HasProperty 得到的, 而抽象方法HasProperty的返回值是通过 调用对象的内部方法 [[HasProperty]] 得到的,它对应的拦截函数名叫 has,因此我们可以通过 has拦截函数实现对 in 操作符的代理:
1 2 3 4 5 6 7 8 9 10 11 12 const  obj = {foo :1 ,bar :2 }const  p =new  Proxy (obj,{    has (target,key ){         track (target,key)         return  Reflect .has (target,key)     } }) effect (()=> {    'foo'  in  p  }) 
 
这样,当我们在副作用函数中通过 in 操作符操作响应式数据时, 就能够建立依赖关系。
for…in循环 以下是原书中for in循环的拦截思路:
可以看到EnumerateObjectProperties是一个generator 函数,接收一个参数 obj。实际上,obj 就是被 for…in 循环遍历的对象,其关键点在于使用 Reflect.ownKeys(obj)来获取只属于对象自身拥有的键。 
也就是说,我们可以使用 ownKeys 拦截函数来拦截 for...in里发生的Reflect.ownKeys操作,即可间接拦截 for...in循环:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const  obj = {foo :1 }const  ITERATE_KEY  = Symbol ()const  p =new  Proxy (obj,{    ownKeys (target ){         track (target,ITERATE_KEY )         return  Reflect .ownKeys (target)     } }) effect (()=> {    for (let  key in  p){          console .log ('key' ,key)     } }) 
 
ownKeys用来获取一个对象的所有属于自己的键值,参数只能拿到目标对象target。在调用track时,为了传入for...in操作对应的key,我们需要自己设置一个key的标识ITERATE_KEY。在触发响应trigger时,我们同样传入这个标识。
这里有一个问题,当我们设置一个新属性(比如p.bar = 3)时,会触发 set 拦截函数执行。此时 set 拦截函数接收到和传给trigger 函数的 key 就是字符串 ‘bar’。于此同时,由于添加了新属性,for in的循环执行也会从原来的一次变成两次,这对副作用函数的执行有影响。但根据前文,for…in 循环是在副作用函数与 ITERATE_KEY 之间建立联系,和 ‘bar’ 没有关系,导致我们无法触发ITERATE_KEY的副作用函数。 
问题的关键就是触发此副作用函数,所以解决方法也就出来了,在添加属性操作时,我们同样执行ITERATE_KEY的副作用函数就可以了。于是我们修改trigger函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 function  trigger (target,key ){    const  depsMap = bucket.get (target)     if (!depsMap){         return      }     const  depsEffects = depsMap.get (key)     const  iterateEffects = depsMap.get (ITERATE_KEY )      const  effectsToRun = new  Set ()     depsEffects && depsEffects.forEach (effectFn  =>  {         if (effectFn !== activeEffect){             effectsToRun.add (effectFn)         }     })          iterateEffects && iterateEffects.forEach (effectFn  =>  {         if (effectFn !== activeEffect){             effectsToRun.add (effectFn)         }     })     effectsToRun&&effectsToRun.forEach (fn  =>  {         if (fn.options .scheduler ){             fn.options .scheduler (fn)         }else {             fn ()         }     }); } 
 
但这样又有了新问题,我们添加属性 时需要执行ITERATE_KEY相关联的副作用函数,但在修改属性 时,我们是不需要执行其副作用函数的。但由于我们都是通过set拦截的,要想区分这两种,我们需要修改我们的拦截函数:
1 2 3 4 5 6 7 8 9 const  p =new  Proxy (obj,{    set (target,key,newVal,receiver ){                  const  type = Object .prototype  .hasOwnProperty .call (target,key)?'SET' :'ADD'          const  res = Reflect .set (target,key,newVal,receiver)         trigger (target,key,type)         return  res     } }) 
 
我们给trigger传入第三个参数type用于区分操作类型,并且只有当操作类型 type 为 ‘ADD’ 时,才会触发与 ITERATE_KEY 相关联的副作用函数重新执行,避免了不必要的性能损耗:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function  trigger (target,key,type ){    const  depsMap = bucket.get (target)     if (!depsMap){         return      }     const  depsEffects = depsMap.get (key)     const  effectsToRun = new  Set ()     depsEffects && depsEffects.forEach (effectFn  =>  {         if (effectFn !== activeEffect){             effectsToRun.add (effectFn)         }     })           if (type === 'ADD' ){         const  iterateEffects = depsMap.get (ITERATE_KEY )          iterateEffects && iterateEffects.forEach (effectFn  =>  {             if (effectFn !== activeEffect){                 effectsToRun.add (effectFn)             }         })     }     effectsToRun&&effectsToRun.forEach (fn  =>  {         if (fn.options .scheduler ){             fn.options .scheduler (fn)         }else {             fn ()         }     }); } 
 
delete操作符 关于对象的代理,还剩下最后一项工作需要做,即删除属性操作的代理:delete p.foo
以下是原书的描述:
delete 操作符的行为依赖 [[Delete]]内部方法,这个内部方法通过使用deleteProperty拦截。我们先检查是否存在此属性,再通过Reflect.deleteProperty进行属性删除,最后调用trigger函数重新执行副作用函数:
1 2 3 4 5 6 7 8 9 10 11 12 const  p =new  Proxy (obj,{    deleteProperty (target,key ){                  const  hadKey = Object .prototype  .hasOwnProperty .call (target,key)                  const  res = Reflect .deleteProperty (target,key)         if (res && hadKey){                          trigger (target,key,'DELETE' )         }     } }) 
 
由于delete删除了对象,减少了for in的循环次数,我们也需要触发ITERATE_KEY 相关联的副作用函数重新执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function  trigger (target,key,type ){    const  depsMap = bucket.get (target)     if (!depsMap){         return      }     const  depsEffects = depsMap.get (key)          const  effectsToRun = new  Set ()     depsEffects && depsEffects.forEach (effectFn  =>  {         if (effectFn !== activeEffect){             effectsToRun.add (effectFn)         }     })           if (type === 'ADD' ||type === 'DELETE' ){         const  iterateEffects = depsMap.get (ITERATE_KEY )          iterateEffects && iterateEffects.forEach (effectFn  =>  {             if (effectFn !== activeEffect){                 effectsToRun.add (effectFn)             }         })     }     effectsToRun&&effectsToRun.forEach (fn  =>  {         if (fn.options .scheduler ){             fn.options .scheduler (fn)          }else {             fn ()         }     }); } 
 
reactive响应触发 我们从规范的角度详细介绍了如何代理对象,在这个过程中,处理了很多边界条件。但想要合理地触发响应,还有许多工作要做。从这里开始我们逐步实现并完善reactive对各种类型的对象的代理。
值无变化取消响应 首先第一个问题,当setter设置的值没有发生变化时, 不应该触发响应。
为了满足需求,我 们需要修改set 拦截函数的代码,在调用 trigger 函数触发响应之 前,需要检查值是否真的发生了变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 const  p =new  Proxy (obj,{    set (target,key,newVal,receiver ){                  const  oldVal = target[key]          const  type = Object .prototype  .hasOwnProperty .call (target,key)?'SET' :'ADD'          const  res = Reflect .set (target,key,newVal,receiver)         if (oldVal !== newVal){                          trigger (target,key,type)         }         return  res     },     ) 
 
我们在 set 拦截函数内首先获取旧值 oldVal,接着比较新值与旧值,只有当它们不全等的时候才触发响应。但还有一个问题,对于NaN来说,全等比较的结果是false:
1 2 NaN  === NaN  NaN  !== NaN  
 
因此我们需要对NaN这个特例进行条件判断:
1 2 3 4 5 6 7 8 9 10 11 set (target,key,newVal,receiver ){    const  oldVal = target[key]     const  type = Object .prototype  .hasOwnProperty .call (target,key)?'SET' :'ADD'      const  res = Reflect .set (target,key,newVal,receiver)               if (oldVal !== newVal&&(oldVal === oldVal || newVal === newVal)){         trigger (target,key,type)     }     return  res }, 
 
这样我们就解决了 NaN 的问题。
从原型上继承属性 接下来我们讨论一种从原型上继承属性的情况,从现在起我们将先前的proxy封装为一个reactive函数,它接收一个对象作为参数,返回为其创建的响应式数据:
1 2 3 4 5 6 function  reactive (obj ){    return  new  Proxy (obj,{         getter...         setter...     }) } 
 
我们用reactive函数设置一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const  proto = {foo :1 }const  data = {}const  parent = reactive (proto)const  child = reactive (data)Object .setPrototypeOf (child,parent)effect (()=> {    console .log ('foo' ,child.foo ) }) child.foo  = 2  child.foo  = 3  
 
我们定义了空对象data 和对象 proto,分别为二者创建了对应的响应式数据 child 和 parent,并且使用 Object.setPrototypeOf 将 parent 设置为 child 的原型。接着访问child.foo属性,由于child本身没有设置foo属性,因此这个foo值是从原型parent上继承来的,控制台输出foo 1(没有设置原型会输出undefined)。
但当我们赋值child.foo = 2时,控制台输出了两次,也就是说这里的副作用函数执行了两次,产生了一次不必要的更新。
以下是原书中对原因的分析:
总结一下,当我们添加副作用函数时,console.log('foo',child.foo)触发了child的getter,但由于child本身没有这个属性,系统就会取它的原型parent并调用原型的getter,副作用函数同时被child和parent收集了。同样的,当我们设置child.foo = 2时,触发了child的setter,但由于child本身没有这个属性,系统就会取它的原型parent并调用原型的setter,于是就会导致副作用函数被再次执行。
于是解决方法就明确了,只要屏蔽其中一次就可以了。我们选择将parent的副作用函数执行屏蔽,在setter里区分这两次更新。
我们来看两次setter有何不同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 set (target, key, value, receiver ) {          } set (target, key, value, receiver ) {          } 
 
在 JavaScript 的 Proxy 机制中,receiver 参数代表 实际触发属性操作的对象 ,即最初发起操作的代理对象。我们可以看到,child和parent的receiver都是 child,而target 则是变化的。对child来说receiver就是target的代理对象,但parent却不是。所以解决办法就有了:判断 receiver 是否是 target 的代理对象,只有当 receiver 是 target 的代理对象时才触发更新。
我们为getter添加一个能力,使代理对象在访问一个特定属性raw时,可以得到它的原始对象:
1 2 3 4 5 6 7 8 9 10 11 12 get (target,key,receiver ){         if (key === 'raw' ){         return  target     }     track (target,key)     return  Reflect .get (target,key,receiver) },      child.raw  === data  parent.raw  === proto  
 
这样我们就可以通过访问raw得到其原始对象。于是我们就可以在setter里进行判断了:
1 2 3 4 5 6 7 8 9 10 11 12 set (target,key,newVal,receiver ){            const  oldVal = target[key]             const  type = Object .prototype  .hasOwnProperty .call (target,key)?'SET' :'ADD'              const  res = Reflect .set (target,key,newVal,receiver)                          if (receiver.raw  === target){                 if (oldVal !== newVal&&(oldVal === oldVal || newVal === newVal)){                     trigger (target,key,type)                 }             }             return  res         }, 
 
这样我们就成功屏蔽掉原型引起的更新了,从而避免不必要的更新操作。
浅响应与深相应 目前我们实现的reactive是浅响应的,简单来说,我们的响应式对象的属性如果包含了另一个对象,修改此对象并不能触发响应:
1 2 3 4 5 6 7 const  obj = reactive ({ foo : { bar : 1  } })effect (()=> {    console .log ('foo' ,obj.foo )  }) obj.foo .bar  = 4   
 
我们来看一下此时的reactive实现:
1 2 3 4 5 6 7 8 get (target,key,receiver ){    if (key === 'raw' ){         return  target     }     track (target,key)          return  Reflect .get (target,key,receiver) }, 
 
当我们读取obj.foo.bar时,首先要通过getter访问obj.foo,但这里通过Reflect.get 得到的结果是一个普通对象{bar:1},并不是响应式对象。因此要实现对象属性的响应,我们需要对Reflect.get的返回值进行一次封装: 
1 2 3 4 5 6 7 8 9 10 11 12 13 get (target,key,receiver ){    if (key === 'raw' ){         return  target     }     track (target,key)          const  res = Reflect .get (target,key,receiver)          if (typeof  res === 'object'  && res !== null ){         return  reactive (res)      }     return  res }, 
 
这样我们访问对象属性时,通过递归调用reactive将其包装成响应式数据,就能触发副作用函数重新执行了。reactive成功实现了深响应。
整理以上代码,使用createReactive函数替代reactive的代理内容,并使用shallowReactive浅响应(只响应对象的第一层属性),与原本的reactive深响应区分开:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function  createReactive (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 (typeof  res === 'object'  && res !== null ){                 return  reactive (res)              }             return  res         },         ...,     	...     }) } function  reactive (obj ){    return  createReactive (obj) } function  shallowReactive (obj ){    return  createReactive (obj,true ) } 
 
只读与浅只读 我们希望一些数据是只读的。只读本质上也是对数据对象的代理,同样可以使用上文的createReactive函数实现。我们添加第三个参数isReadonly,并修改代理内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 function  createReactive (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)?'SET' :'ADD'              const  res = Reflect .set (target,key,newVal,receiver)             if (receiver.raw  === target){                 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,'DELETE' )             }         }     }          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 (typeof  res === 'object'  && res !== null ){                 return  reactive (res)             }             return  res         },                                  ),      } function  readonly (obj ){    return  createReactive (obj,false ,true ) } 
 
如上代码所示,我们使用createReactive创建代理对象时,通过第三个参数isReadonly指定是否创建一个只读的代理对象,并修改set和 deleteProperty 拦截函数,一旦数据是只读的,则会自动拦截修改操作,提示这是非法操作。由于数据是只读的无法修改,因此不会触发set的trigger函数,也就不需要事先搜集副作用函数依赖了。我们在get拦截函数里进行判断,只读时取消track函数的触发。
基于以上操作,我们就可以实现readonly函数,通过调用createReactive实现只读。
和先前的浅响应与深相应一样,只读也有类似的问题。以上的readonly只实现了浅只读。我们需要对get拦截进行类似的数据封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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 (typeof  res === 'object'  && res !== null ){                  return  isReadonly ? readonly (res) : reactive (res)     }     return  res },           function  readonly (obj ){    return  createReactive (obj,false ,true ) } function  shallowReadonly (obj ){    return  createReactive (obj,true ,true ) } 
 
异质对象的代理 在 JavaScript 中有两种对象:常规对象和异质对象。这两类对象内部犯法的逻辑大体是相同的,但后者会有一些特殊方法与处理。为了更好地实现reactive的对象代理,我们需要针对这些和常规对象有差异的对象进行进一步的处理。
原书针对数组和集合的代理展开了长篇幅的深入介绍,由于篇幅实在太长,暂时就跳过了,有空再单独更新重点部分。
代理数组 数组对象除了 [[DefineOwnProperty]] 这个内部方法之 外,其他内部方法的逻辑都与常规对象相同。因此,当实现对数组的代理时,用于代理普通对象的大部分代码可以继续使用:
1 2 3 4 5 6 7 const  arr = reactive ([1 ,2 ,3 ])effect (()=> {    console .log ('数组' ,arr[0 ]) }) arr[0 ] = 2   
 
当我们通过索引读取或 设置数组元素的值时,代理对象的 get/set 拦截函数也会执行,因此 我们不需要做任何额外的工作,就能够让数组索引的读取和设置操作是响应式的了。
但对数组的操作与对普通对象的操作仍然存在不同,下面总结了所有对数组元素或属性的“读取”操作。
通过索引访问数组元素值:arr[0]。  
访问数组的长度:arr.length。  
把数组作为对象,使用 for…in 循环遍历。  
使用 for…of 迭代遍历数组。  
数组的原型方法,如 concat/join/every/some/find/findIndex/includes 等,以及其他所有不改变原数组的原型方法。 
 
可以看到,对数组的读取操作要比普通对象丰富得多。我们再来看看对数组元素或属性的设置操作有哪些。 
通过索引修改数组元素值:arr[1] = 3。  
修改数组长度:arr.length = 0。  
数组的栈方法:push/pop/shift/unshift。  
修改原数组的原型方法:splice/fill/sort 等。 
 
除了通过数组索引修改数组元素值这种基本操作之外,数组本身 还有很多会修改原数组的原型方法。有些方法的操作语义是“读取”,而有些方法的操作语义是“设置”。 因此,当这些操作发生时,也应该正确地建立响应联系或触发响应。接下来,我们针对这些数组的操作挨个说起。
数组索引index与length 
遍历数组 
数组的查找 
数组的长度修改 
代理集合 Set & Map 本节介绍集合类型数据的响应式方案。集合类型包括 Map/Set 以及 WeakMap/WeakSet。使用 Proxy 代理集合类型的 数据不同于代理普通对象,因为集合类型数据的操作与普通对象存在 很大的不同。下面总结了 Set 和 Map 这两个数据类型的原型属性和方法。
Set 类型的原型属性和方法如下 :
size:返回集合中元素的数量。  
add(value):向集合中添加给定的值。  
clear():清空集合。  
delete(value):从集合中删除给定的值。  
has(value):判断集合中是否存在给定的值。  
keys():返回一个迭代器对象。可用于 for…of 循环,迭代 器对象产生的值为集合中的元素值。  
values():对于 Set 集合类型来说,keys() 与 values() 等 价。  
entries():返回一个迭代器对象。迭代过程中为集合中的每一 个元素产生一个数组值 [value, value]。  
forEach(callback[, thisArg]):forEach 函数会遍历集 合中的所有元素,并对每一个元素调用 callback 函数。 forEach 函数接收可选的第二个参数 thisArg,用于指定 callback 函数执行时的 this 值。 
 
Map 类型的原型属性和方法如下 :
size:返回 Map 数据中的键值对数量。  
clear():清空 Map。  
delete(key):删除指定 key 的键值对。  
has(key):判断 Map 中是否存在指定 key 的键值对。  
get(key):读取指定 key 对应的值。  
set(key, value):为 Map 设置新的键值对。  
keys():返回一个迭代器对象。迭代过程中会产生键值对的 key 值。  
values():返回一个迭代器对象。迭代过程中会产生键值对的 value 值。  
entries():返回一个迭代器对象。迭代过程中会产生由 [key, value] 组成的数组值。  
forEach(callback[, thisArg]):forEach 函数会遍历 Map 数据的所有键值对,并对每一个键值对调用 callback 函 数。forEach 函数接收可选的第二个参数 thisArg,用于指定 callback 函数执行时的 this 值。 
 
可以发现Map和Set还是挺类似的,区别主要在于添加和读取元素的方法:Set 类型使用 add(value) 方添加元素,而Map 类型使用 set(key, value) 方法设置键值对,并且Map类型可以使用 get(key) 方法读取相应的值。
如何代理 Set 和 Map 
建立响应联系 
避免污染原始数据 
处理 forEach 
迭代器方法 
values  与 keys  方法