阅读Vuejs设计与实现(第五章)

290 阅读7分钟

第五章 非原始值的响应式方案

5.1 理解Proxy 和 Reflect

  • 我们需要了解Proxy的用法,以及知道Proxy只能代理对象,基本数据类型无法代理。
  • 点击了解这两个API用法Proxy - Reflect
  • 本节只要带我们了解了Proxy以及Reflect作用,主要使用Reflect来配合Proxy来实现一些功能。
  • proxy就不细讲了,Reflect重点说一下。
// Reflect 主要作用

const obj = {foo: 1}
// 直接读取 
console.log(obj.foo) // 1

// 使用 Reflect.get 来读取
console.log(Reflect.get(obj,'foo'), { foo : 2 }) // 输出 2而非1
  • 根据上述代码,我们可以看到 Reflect传入第三个参数,相当于this。
  • 使用场景: 分析。我们在某些时候,对调用proxy代理的对象中的方法,最好使用 Reflect方法,否则有时this可能不是我们预想的那样
const obj = {foo:1}
const p = new Proxy(obj,{....})

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

effect(() => {
  console.log(p.bar)
})

// 调用
p.foo++
  • 上述代码,我们通过调用p.foo++,按理说会触发副作用函数执行。实则不然。

  • 原因: 上一章实现的get,通过对 target[key] 返回属性值。其中 target 是原始对象obj,而key就是字符串'bar',所以 target[key] 相当于 obj.bar。因此,当我们使用 p.bar访问属性时,它的getter函数内的this会指向的其实是原始对象 obj,这说明我们访问的是 obj.foo。注意:所以这样在副作用函数内通过原始对象访问的它 的属性是不会建立响应式联系的。

  • 解决问题: 使用 Reflect.get 函数

const p = new Proxy(obj,{
  // 拦截读取操作,接受第三个参数  receiver
  get(target,key,receiver){
    track(target,key)
    // 使用 Reflect.get 返回读取的属性值
    return Reflect.get(target,key,receiver)
  },
  // 省略
})
  • 上述代码,当我们使用代理对象 p访问 bar属性时候,那么receiver 则是p。那么问题就迎刃而解了。

5.2 JavaScript对象及Proxy的工作原理

  • 对象

    • 常规对象
      • 必须满足11个方法,ECMA规范10.1.x
      • 对于内部方法 Call,必须使用 ECMA 规范 10.2.1 给出的定义
      • 对于内部方法 Construct,必须使用 ECMA 规范 10.2.2给出的定义实现
    • 异质对象:非常规方法调用对象
  • 对象的语义实际上由对象的内部方法来决定的,书中写到包括[[GET]]在内,一个对象必须部署11个必要的方法,以及额外的两个必要方法 Call 和 Construct

  • 其实代理对象和普通对象没有太大区别。区别主要在于Get的实现,如果在创建代理对象没有执行对应的拦截函数get(),那当我们访问属性的时候,则是直接访问原始对象的属性,也叫透明代理。

  • Proxy 点击查看Proxy对象内部的方法

  • Call 和 Construct 这两个内部方法只要当代理的对象时函数和构造函数时才会部署。

  • 拦截删除属性的操作

const obj = {foo :1}
const p = new Proxy(obj,{
  deleteProperty(target,key){
    return Reflect.deleteProperty(target,key)
  }
})

console.log(p.foo) // 1
delete p.foo
constole.log(p.foo) // 未定义

5.3 如何代理 Object

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

const p = new Proxy(obj, {
  get(target, key ,receiver){
    // 建立联系
    track(target,key)
    // 返回属性值
    return Reflect.get(target,key,receiver)
  }
})
  • 通过阅读ECMA文档,找到API的具体说明,再结合下表来实现对对象的某个方法代理的确认。
const obj = { foo : 1}
const p = new Proxy(obj, {
  has(target,key){
    track(target,key)
    return Reflect.has(target,key)
  }
})
  • 这样,当我们在副作用函数中通过in操作符操作响应式数据时,就能够建立依赖关系
effect(() => {
	'foo' in p //将会建立依赖关系
})
  • 拦截for... 循环: 根据规范可以找到对应其 EnumerateObjectProperties(obj),这是一个抽象方法,该方法返回一个迭代器对象。

  • 下面代码是满足该抽闲该方法的示例实现

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){
    // 将副作用函数与 ITERATE_KEY关联
    track(target,ITERATE_KEY)
    return Reflect.ownKeys(target)
  }
})
  • 上述代码,拦截ownKeys操作即可间接拦截for... in循环。选择使用 ITERATE_KEY 作为追踪的key,是因为 ownKeys 函数,只能拿到目标对象,不能像get或set拿到具体操作的key。因为在读写属性值时,总能知道当前正在操作是哪个属性,所以需要在该属性与副作用函数之间建立联系即可。

  • ownKey获取属于自己的键值,不与任何具体的键绑定。这样,我们使用唯一key来标识。

  • 因此我们需要在触发响应的时候也对他进行触发。

trigger(target,ITERATE_KEY)
  • 使用set拦截函数来实现,对for..in的拦截
const p = new Proxy(obj,{
  // 拦截设置操作
  set(target,key,newVal){
    // 设置属性值
    const res = Reflect.set(target,key,newVal,receiver)
    // 把副作用函数从桶里取出并执行
    trigger(target,key)
    
    return res
  }
  
  // 省略其他拦截函数
})
  • 当p添加新属性bar,会触发set拦截函数执行。此时set拦截函数接收到的key是字符串‘bar’,因此最终调用trigger函数时也只是触发了与‘bar’相关联的副作用函数重新执行。
  • 但是for...in循环是在副作用函数与ITERATE_KEY之间建立联系,不过和‘bar’一点儿关系没有,这是尝试执行p.bar= 2时,并不能正确的触发响应。
  • 解决方法:当添加属性时,我们将哪些与 ITERATE_KEY相关联的副作用函数也取出来执行就可以了。
function trigger(target,key){
  const depsMap = buck.get(target)
  if(!depsMap){
    // 取得 与 key想关联的副作用函数
    const effects = depsMap.get(key)
    // 取得与 ITERATE_KEY 相关联的副作用函数	
    const iterateEffects = depsMap.get(ITERATE_KEY)
    
    const effectsToRun = new Set()
    // 将与 key相关联的副作用函数 添加到 effectToRun
    effects && effects.forEach((effectFn) => {
      if(effectFn != activeEffect){
        effectsToRun.add(effectFn)
			}
		})
    
    // 将与 ITERATE_KEY 想关联的副作用函数也添加到 effectsToRun
    iterateEffects && iterateEffects.forEach((effectFn) => {
      if(effectFn !== activeEffect){
        effectToRun.add(effectRun)
      }
		})
    
    effectToRun.forEach(effectFn => {
      if(effectFn.ioption.scheduler){
        effectFn.options.scheduler(effectFn)
      }else {
        effectFn()
			}
    })
	}
}
  • 当trigger函数执行,对具体操作key和 ITERATE_KEY想关联副作用都拿出来执行。
  • 问题:此时新增属性触发副作用函数没问题,不过修改已有属性则会出现问题。
const obj = {foo : 1}
const p = new Proxy(obj,{/*...*/})

effect(() => {
  // for .. in 循环
  for(const key in p ){
    console.log(key) // foo
  }
})

当我们修改 p.foo =2,这时修改属性不会对for ... in 循环产生影响。因为无论怎么修改一个属性,for... in 都只会循环一次。因此为了不需要触发副作用函数重新执行,则使用set来拦截。

const p = new Proxy(obj, {
  // 拦截设置操作
  set(target,key,newVal){
    // 如果属性不存在,则说明是在添加新属性,否则设置已有属性
    const type = Object.prototype.hasOwnProperty(target,key) ?  'SET' : 'ADD'
    
    // 设置属性值
    const res = Reflect.set(target,key,newValue,receiver)
    // 把副作用函数从桶里取出并执行
    trigger(target,key,type)
    
    return res
  }
  
  // 省略其他拦截函数
})
  • 上述代码,使用Object.prototype.hasOwnProperty检查操作的属性是否已经存在于目标对象上。根据存在与否,在设置操作类型SETADD,并且将 type传入trigger函数,并扩展trigger函数。
  • trigger 函数内,用type来区分当前操作类型,并且只有当typeADD时,才会触发ITERATE_KEY想关联的副作用函数重新执行,这样就避免了不必要的性能损耗。
  • 通常我们对操作的类型进行类型的枚举。
const TiggerType = {
  SET: 'SET',
  ADD: 'ADD'
}
  • 对象代理,最后一步工作需要做,即删除属性操作的代理: 使用deleteProperty来拦截。只有满足是自己对象和并且完成删除后,在调用trigger函数,传入新的操作类型DELETE。因此,当操作类型为DELETE,应该触发那些与ITERATE_KEY相关联的副作用函数重新执行。

继续更新中

END

个人博客汇总地址:github.com/codehzy/blo…