0
点赞
收藏
分享

微信扫一扫

Js 中 Proxy + Reflect 及 Vue 响应式原理从零实现

Vue 响应式原理从零实现, Proxy + Reflect

上篇讲了关于 es6 引入的两个数据类型 Set / WeakSet 还有 Map / WeakMap , 区别在于这俩能用对象作为 key 以及不断发展中的 ECMA 不断会添加新特性, 说明 js 这个语言基本是要统治 web 了.

本篇也是基于 es6+ 的一些重点特性来从分析框架角度, 通过对 vue 响应式原理分析, 从而引出这背后的 Proxy 和 Reflect 这俩强大对象的组合应用.

同时, 也尝试跟着大佬去手写 vue3 响应式系统, 感受一下编程思维的真正乐趣了.

声明, 以下的所有代码逻辑都是搬运 b 站大佬 coderwhy 的公开视频整理哈, 学习娱乐为主的

监听对象的操作

会有这样的一个需求: 有一个对象, 我们希望能监听这个对象中的属性被访问或者被修改的过程.

通过之前学的对象属性描述符中, 存储属性描述符就能做, 即 Object.defineProperty(obj, key, get/set)`

// 监听对象操作方式

const obj = {
  _name: 'youge',
  age: 18 
}

// 通过属性描述符可以监听属性
Object.defineProperty(obj, 'name', {
  // 监听
  get: function() {
    console.log('监听到 obj.name 被访问啦~')
    return this._name
  },

  set() { console.log('监听到 obj.name 被设置啦~')}
})

// 监听 name 属性的获取和赋值操作
console.log(obj.name) // get
obj.name = 'cj' // set

也可以监听所有的 key .

// 监听所有可枚举属性

Object.keys(obj).forEach(key => {
  let value = obj[key]
  Object.defineProperty(obj, key, {
    get() {
      console.log(`监听到 obj 对象的 ${key} 属性被访问了`)
      return value
    }, 

    set: function(newValue) {
      console.log(`监听到 obj 对象的 ${key} 属性被设置为: ${newValue}`)
      value = newValue
    }
  })
})

// 监听 name 属性的获取和赋值操作 
console.log(obj.name)

obj.name = 'cj' 
obj.age = 20

console.log(obj.name )

监听到 obj 对象的 name 属性被访问了
youge
监听到 obj 对象的 name 属性被设置为: cj
监听到 obj 对象的 age 属性被设置为: 20
监听到 obj 对象的 name 属性被访问了
cj

这样做会存在的问题:

  • Object.defineProperty() 的设置初衷不是为了去监听一个对象属性变化, 虽然也能, 但不推荐
  • 它不能做更深程度的的监听, 如 新增属性, 删除属性等 是做不到的

vue2 就是用它来做响应式的, 虽然挫了点, 但还是能实现基本功能的

Proxy 类

在 es6 中, 新增了一个 Proxy类, 从名字就可以看出他是用于帮我们创建一个 代理 对象的.

  • 希望监听某对象 obj 的相关操作, 则可以 先创建一个代理对象 obj'
  • 对对象的所有操作, 都可通过代理对象 obj' 完成, 最后再同步到原来的对象上.

// Proxy 基本使用

const obj = {
  name: 'youge',
  age: 18
}

// 创建 obj 的代理对象 objProxy
// 参数一是被代理对象, 参数2是一个捕获器对象
const objProxy = new Proxy(obj, {})

// 通过代理对象就可以操作了, 完美替身
console.log(objProxy.name) // youge 
console.log(objProxy.age)  // 18

// 通过代理对象修改了属性, 原对象也会跟着被修改
objProxy.name = 'cj'
obj.age = () => console.log('run')
// 新增也可以
objProxy.run = function () {}


console.log(obj)

// 完美替身呀!
youge
18
{
  name: 'cj',
  age: [Function (anonymous)],
  run: [Function (anonymous)]

那如果要和上面一样监听属性的变化, 则需要传第二个捕获器参数对象 handler 了.

// Proxy 基本使用

const obj = {
  name: 'youge',
  age: 18
}

// 通过代理对象, 重写捕获器的逻辑就实现精准监控啦 

const objProxy = new Proxy(obj, {
  // 获取属性值时的捕获器
  get: function(target, key, reciever) {
    console.log(`监听到 obj 对象的 ${key} 属性被 访问:`, target[key])
    return target[key]
  },

  // 设置属性值时的捕获器
  set: function(target, key, newValue, reciever) {
    console.log(`监听到 obj 对象的 ${key} 属性被 设置:`, newValue)
    target[key] = newValue
  }

})

// 监听到 obj 对象的 name 属性被 访问: youge
objProxy.name 

// 监听到 obj 对象的 age 属性被 设置: 30
objProxy.age = 30

// { name: 'youge', age: 30 }
console.log(obj)

这里的 get, set 称为 handler 捕获器, 它俩都是函数类型有三, 四个参数:

  • target: 目标对象, 代理对象
  • prop: 属性名
  • value: 新属性值, get() 无此参数
  • receiver: 调用的代理对象

Proxy 的所有捕获器

这些东西就能精确对对象的每一步操作进行精准的控制啦.

捕捉器

捕捉场景

get()

属性读取

set()

属性设置

has()

in操作符

deleteProperty()

delete 操作符

apply()

函数调用

construct()

new 操作

defineProperty()

捕捉 Object.defineProperty()

ownKeys()

捕捉 Object.getOwnPropertyNames()

getOwnPropertyDescriptor()

捕捉 Object.getOwnPropertyDescriptor()

isExtensible()

捕捉 Object.isExtensible()

preventExtensions()

捕捉 Object.preventExtensions()

getPrototypeOf()

捕捉 Object.getPrototypeOf()

setPrototypeOf()

捕捉 Object.setPrototypeOf()

这不得把对象进行 360 度无死角监控嘛.

// Proxy 捕获器

const obj = { name: 'youge', age: 18 }

const objProxy = new Proxy(obj, {
  // 监控: 获取属性 
  get: function(target, key) {console.log(key, '属性被访问')},

  // 监控: 设置属性值
  set(target, key, newValue) {console.log(key, '属性值设置')},

  // 监控: in 操作符
  has: (target, key) => console.log(key, '属性被遍历'),

  // 监控: delete 删除属性
  deleteProperty: (target, key) => console.log(key, '属性被删除'),

  // 监控: 查看对象原型
  getPrototypeOf(target) {
    console.log('查看对象原型')
    return {}
  }

  // ...
})


// 只监控, 不修改哈
objProxy.name // name 属性被访问
objProxy.age = 30 // age 属性值设置
"name" in objProxy // name 属性被查询到
delete objProxy.name // name 属性被删除
Object.getPrototypeOf(objProxy) // 查看对象原型

函数也是特殊对象, 也能被监控普通调用, new 调用等.

// 函数也是对象, 也能被代理监控

function foo() { console.log('foo func')}

const fooProxy = new Proxy(foo, {
  // 监控: 函数被调用
  apply: function(target, thisArg, argArray) {
    console.log('foo 函数被调用啦~')
    // 让原函数去执行调用
    target.apply(thisArg, argArray)
  }, 

  // 监控: 对象被创建 (构造函数 / 类)
  construct: function(target, argArray, newTarget) {
    console.log('函数被 new 出一个对象啦~')
    return new target(...argArray)
  }

})

// foo 函数被调用啦~
// foo func
fooProxy()

// 函数被 new 出一个对象啦~
// foo func
// foo {}
const obj = new fooProxy()
console.log(obj)

Reflect 对象

在 es6 中新增了一个 Reflect 的 API, 它是一个 内置对象, 字面意思是反射. 它提供了 很多操作 js 对象的方法, 类似 Object,用于更安全、更规范地操作对象

它通常和 Proxy 配合使用,是现代元编程(meta-programming)的重要组成部分. 早期 ECMA 并未考虑到对于对象本身的操作应该如何规范, 因此很多 API 都直接放在了 Object 之上, 导致有点混乱, Object 又是一个构造函数, 给它又不太合适, 还有更多的像 in , delete 等操作, 都不知道放哪, 因此新增了 Reflect 对象来实现:

  • 统一对象操作的 API
  • 让 Proxy 的默认行为更容易被调用
  • 替代分散在全局或 Object 上的零散方法

一句话概括就是: Reflect 对象用来替换 Object 本不该承担的部分, 并统一规范所有对象的元编程接口.

Reflect 方法

对应操作

说明

Reflect.get(target, key, receiver)

读取属性

类似 target[key]

Reflect.set(target, key, value, receiver)

设置属性

类似 target[key] = value

Reflect.has(target, key)

检查属性是否存在

类似 'key' in target

Reflect.deleteProperty(target, key)

删除属性

类似 delete target[key]

Reflect.defineProperty(target, key, descriptor)

定义属性

类似 Object.defineProperty()

Reflect.getPrototypeOf(target)

获取原型

类似 Object.getPrototypeOf()

Reflect.setPrototypeOf(target,proto)

设置原型

类似 Object.setPrototypeOf()

Reflect.isExtensible(target)

判断是否可扩展

类似 Object.isExtensible()

Reflect.preventExtensions(target)

阻止扩展

类似 Object.preventExtensions()

Reflect.ownKeys(target)

获取所有自有键

类似 Reflect.ownKeys(target)(推荐)

Reflect.apply(func, thisArg, args)

调用函数

类似 Function.prototype.apply()

Reflect.construct(ctor, args)

构造实例

类似 new ctor(...args

Proxy 如果不结合 Reflect 使用, 则还是相当于操作原对象, 感觉代理多此一举.

// Proxy 时不用 Reflect 的问题

const obj = {name: 'youge',age: 18}

const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    // 这里其实还是对原来对象操作, 代理半天多此一举
    return target[key]
  }
})

那 Proxy + Reflect 就很丝滑:

// Proxy + Reflect  
const obj = { name: 'youge',age: 18 }

const objProxy = new Proxy(obj, {
  
  get: function(target, key, receiver) {
    console.log(`监听到 ${key} 属性被访问啦~`)
    // return target[key]
    return Reflect.get(target, key)
  }, 

  set: function(target, key, newValue, receiver) {
    console.log(`监听到 ${key} 属性被设置啦~`)
    // target[key] = newValue
    const ok = Reflect.set(target, key, newValue)
    // 一定能知道操作成功或者失败结果
    return ok ? true : false
  }

})

// { name: 'youge', age: 30 }
objProxy.name  

// 监听到 age 属性被设置啦~
objProxy.age = 30

// 原对象也改了: { name: 'youge', age: 30 }
console.log(obj)

一个显著的好处是, Reflect.set(target, key, newValue) 会返回一个 bool 值, 而原来的 target[key] 方式并不知道设置值是否成功还是失败. 比如, 这个对象进行了 Object.freeze() , 那就搞不清楚了.因此统一接口规范非常重要呀.

receiver 参数的作用

指定 getter 或 setter 内部的 this 指向

// receiver 参数的作用

const obj = {
  // 不让外部直接访问, 需通过 get/set 方法
  _name: 'youge',

  get name() {
    return this._name
  }, 

  set name(newValue) {
    this._name = newValue
  }

}

// obj.name = 'cj' // 通过 set -> this._name
// console.log(obj.name) // 通过 get -> this._name

// 现在想要对这个 setter / getter 做监听
const objProxy = new Proxy(obj, {
  get: function(target, key) {
    console.log('get 方法被访问---', key)
    return Reflect.get(target, key)
  }, 

  set: function(target, key, newValue) {
    Reflect.set(target, key, newValue)
  }
})

objProxy.name = 'yaya'

// objProxy.name -> Reflect.get(target, key) -> name -> this._name 
// 理论上这个 name, this._name 应该要被 拦截 2次才对,_name 逃逸了

// 这里的 this 默认是 obj 对象而非代理对象
// 那这个代理就失败了, 因为它被绕过去啦!

// 解决之道, 就是要改变 this._name 的 this 指向, 改为 代理对象
// receiver 参数就是干这个活的

console.log(objProxy.name)

这个 receiver 参数就是代理对象, 传进来就能改变 this.__name 的 this 指向为代理对象, 从而被完整监听.

// 现在想要对这个 setter / getter 做监听
const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    // receiver 就是代理对象
    console.log('get 方法被访问---', key)
    return Reflect.get(target, key, receiver)
  }

// objProxy.name -> Reflect.get(...) -> name -> this._name 
// 确保 this._name 不被逃逸

get 方法被访问--- name
get 方法被访问--- _name
yaya

因此, 对普通对象的读写, receiver 参数可选; 有 getter / setter 则必须传, 确保 this 指向准确; Proxy 嵌套 Proxy 的情况是, receiver 必须原, 防止 this 绑定丢失啦.

Reflect 中的 construct

用于以构造函数的方式创建一个对象实例,相当于使用 new 操作符,但它是函数式调用,更灵活、更可控.

参数

类型

必选

说明

constructor

Function


要调用的构造函数(如 Person

argsList

Array


传给构造函数的参数列表(如 ['Alice', 25]

newTarget

Function


指定 new.target 的值(用于控制原型链),默认等于 constructor

// Reflect 中的 construct 作用, 类似 new

function Student(name, age) {
  this.name = name 
  this.age = age 
}

function Teacher() {}

const stu = new Student('youge', 18)
console.log(stu) // 类型是 Student
console.log(stu.__proto__ === Student.prototype) // true 

// 现有个变态的需求, 要用 Student new 的对象, 让类型变为 Teacher

// 之前的方式是通过在 Student 里面调用 Super(name, age), 但如果被私有就不允许调
// 那就可以使用 Reflect 了.
const obj = Reflect.construct(Student, ['yaya', 20], Teacher)
console.log(obj) // Teacher { name: 'yaya', age: undefined }

相当于是 new 的函数版本, 暂时我用的不算太多, 就先不研究了.

总之:

Reflect

一组静态方法,用于操作对象

主要用途

配合 Proxy 实现默认行为

优势

返回值统一、支持 receiver、API 更规范

推荐用法

Proxy 的 trap 中优先使用 Reflect

常见组合

Proxy + Reflect = 强大的对象拦截与控制

总结就是: Proxy 用来拦截, Reflect 用来放行.

响应式

响应式”(Reactive)通常指的是一种编程范式,其核心思想是:当数据发生变化时,依赖于该数据的其他部分(如视图、计算属性等)能够自动、及时地更新,而无需开发者手动去操作和通知。 通常有两个关键机制:

  • 数据劫持 / 代理
  • 依赖收集与派发更新

// 响应式是什么

let m = 10

// 一段代码
console.log(m)
console.log(m * 2)
console.log(m ** 2)

// 需求: 当 m 发生变化时, 这一段代码可以自动执行
// 即上面的这坨代码, 实时响应 m 的变化

// 直接改肯定是不行的
// m = 20  

// 将这坨封装一个函数
function foo() {
  console.log(m)
  console.log(m * 2)
  console.log(m ** 2)
}

m = 20 
// m 发生变化, 与它相关的另外一坨代码自动执行
foo()

// 更多场景是, 对象的响应式
const obj = {name: 'youge', age: 18}

// 监听 name 改变时, 自动执行
console.log(obj.name)

// 监听 age 改变时, 自动执行
console.log(obj.age)

01-响应式函数封装

当监听的数据变化时, 执行的代码可能不止一行, 因此很自然想到将这些待执行的都放到一个函数当中. 那这样问题就变成了, 当监听的数据发生变化时, 自动去执行某一个函数.

// 设计响应式函数

const obj = {name: 'youge', age: 18}

// 响应式函数
function foo() {
  const newName = obj.name 
  console.log('foo func 响应式')
}

// 普通函数
function bar()  {
  console.log('这个函数不需要响应式')
}

// 怎么去识别, 那些函数要被执行, 哪些不执行
// 创建一个函数去收集好要需要被响应的函数就好了 (存数组)
obj.name = 'cj'

// 封装一个响应式的函数
let reactiveFns = [] // 数组的元素是一个函数

function watchFn(fn) {
  reactiveFns.push(fn)
}

// 上面的 foo 要响应式, 就给它加进数组去
watchFn(foo)

// 假设 baz 的匿名函数也要响应式 
watchFn(() => console.log('baz func 响应式'))

// 则监听到 obj.name 变化之后, 遍历数组, 执行响应式函数
reactiveFns.forEach(fn => {
  fn()
  // foo func 响应式
  // baz func 响应式
})

就先假定已实现数据的监听, 现在来组织管理, 哪个函数要被依赖, 及后续如何自动执行

  • 定义一个数组, reactiveFns = [fn1, fn2, fn3...] 里面每个元素用来存储将要被响应的函数;
  • 定义一个函数, watchFn(fn), 参数是一个函数, 传进去则会添加到上面的 reactiveFns 数组中;
  • 响应时, 遍历 reactiveFns 数组, 执行里面的每个函数调用即可

02-依赖收集由数组改为类

当前收集依赖的方式是放到一个数组中来保存, 但这样会遇到大数据量管理的问题:

  • 实际中要监听大量对象
  • 要监听大量的属性, 且会对应大量的响应式函数
  • 不适合在全局维护一大堆数组来保存这些响应式函数

因此要设计一个类, 用于管理某一个对象的某一个属性的所有响应式函数.

// 依赖收集类

// 每个属性都能对应一个依赖对象
class Depend {
  // 初始化存储依赖函数的数组
  constructor() {
    this.reactiveFns = []
  }

  // 添加依赖函数
  addDepend(reactivefn) {
    this.reactiveFns.push(reactivefn)
  }

  // 通知去遍历执行, 依赖函数数组里面的, 依赖函数
  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

// 给某个属性, 创建一个 依赖对象
const depend = new Depend()

// 将依赖函数都收集起来
depend.addDepend(() => console.log('ok, 依赖函数 fn1 执行'))
depend.addDepend(() => console.log('ok, 依赖函数 fn2 执行'))

// 当监听完属性变化后, 派发依赖函数执行任务
depend.notify()

// ok, 依赖函数 fn1 执行
// ok, 依赖函数 fn2 执行

这样就能实现针对某个具体属性, 对应创建一个依赖的类,

  • 它的 addDepend() 方法, 去负责收集好所有与这个属性相关的依赖函数
  • 它的 notify() 方法, 去负责派发执行所有依赖函数调用的任务

03-用 Proxy 监听对象的变化

当前完成依赖收集类之后, 派发任务还是手动的, 即现在要来解决, 自动监听对象的属性, 以实现自动派发:

// 当监听完属性变化后, 派发依赖函数执行任务
depend.notify() // 要自动化

  • 通过 Object.defineProperty() 即 vue2 的方式
  • 通过 new Proxy() 即 vue3 的方式

不用看当然选 Proxy, 当时 vue2 不选它的原因是, 那会儿 Proxy 都还没有出现呢, 只能是属性描述符的方式.

// 对象属性自动监听 + 自动派发

class Depend {
  constructor() {
    this.reactiveFns = []
  }
  addDepend(reactivefn) {
    this.reactiveFns.push(reactivefn)
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

var depend = new Depend()
depend.addDepend(() => console.log('依赖函数 fn1 执行'))
depend.addDepend(() => console.log('依赖函数 fn2 执行'))


// 自动监听对象变化
const obj = { name: 'youge', age: 18 }

const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    // 监听, 属性被访问, 就进行 deep 派发就好了
    depend.notify()
    return Reflect.get(target, key, receiver)
  }, 

  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    // 监听, 设置属性过后也派发
    depend.notify()
  }

})

// 代理对象变化, 自动派发

objProxy.name  
objProxy.age = 30

// 依赖函数 fn1 执行
// 依赖函数 fn2 执行

// 依赖函数 fn1 执行
// 依赖函数 fn2 执行

关键一步就是在监听对象属性变化的时候, 就进行 deep.notify() 派发执行所有依赖函数的变化. 那上的例子, 代理对象的 get, set 各被监听一次, 然后每次都要全部更新一遍依赖函数.

04-依赖收集按对象/属性重设数据结构

当前实现的对象监听自动派发是没有实现按属性管理的, 即当前任意一个属性变化, 都会进行全量派发.

objProxy.name     // name 监听会派发
objProxy.age = 30 // age  监听也会派发

这种方式是不行的, 我们应该针对不同属性, 来指定不同的派发规则, 而非全部更新所有依赖.

obj1 对象
name -> obj1.nameDepend
age -> obj1.ageDepend

obj2 对象
name -> obj2.nameDepend
age -> obj2.ageDepend

因此要重新设计一下数据结构, 先按对象划分, 再按对象.属性 进行划分, 即可用 映射 关系来描述:

首先, 对于单个对象 obj 来说, 它的每个属性都对应一个 depend 对象, 都要存起来, 则用 Map 结构即可

// 为对象的属性, 和 其依赖对象 做映射 Map 

const objMap = new Map()

objMap.set("name", nameDepend)
objMap.set("age", ageDepend)

// 这样要获取某个属性对应的 depened 对象就容易了
objMap.get('name') -> nameDepend

// 同时能实现值更新 name 的依赖 
nameDepend.notify()

其次, 对于多个不同的对象, 如 obj, info 等, 再用一个 WeakMap 给他们 "包" 起来, 形成一个整体

const objsMap = new WeakMap()

// 要监控两个对象, obj, info 
objsMap.set(obj,  objMap)
objsMap.set(info, infoMap)

// 这样, obj.name 则是能确定唯一的
const obj_name_depend_obj = objsMap.get(obj).get("name")
obj_name_depend_obj.notify()

就这里的数据结构用到了两个 map, 是一个层级嵌套关系:

  • Map1: 用来存储单个对象及其属性的对应 deep 对象:
  • objMap =
  • Map2: 用来存储所有要被监听对象

这个嵌套的 ma 数据结构结合 deep 对象的设计, 真的是妙呀 !

还是补充一下, 这里用 Map 而不用普通对象的原因是, 要用对象作为 key, 普通对象是不行的. 然后对象之间的整体管理用 WeakMap 是用其 弱引用 特性, 方便垃圾回收

于是这样的话, 原来的直接调用 depend 就不行了.

// 不能直接 deep 派发, 得先确定是哪个对象 的 哪个属性
set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
  
    // 不能这样写啦!
    depend.notify()
  }

于是我们再来封装一个获取某个对象, 对应的某个属性, 的 depend 对象函数.

// 获取 depend 对象, 根据传入的对象和属性
const objsMap = new WeakMap()

function getDepend(target, key) {
  // 从对象池里获取对象
  let objMap = objsMap.get(target)
  // 如果没有的话就新建, 并添加到 objsMap 大池中
  if (!objMap) {
    objMap = new Map()
    objsMap.set(target, objMap)
  }

  // 根据 key 去获取属性及对应的 depend 对象
  let depend = objMap.get(key)
  // 如果该属性没有依赖, 则初始化, 并添加到 objMap 中
  if (!depend) {
    depend = new Depend()
    objMap.set(key, depend)
  }

  return depend
}

理解这个 getDepend() 函数的关键就是要理解上面的这个 "对象和对象间, 对象和其属性" 设计的 map 关系的数据结构, 无非就是添加一下初始化而已当没有值的时候.

则对应的响应式部分也对象修改为, 要通过确定是某个对象, 某个属性之后, 再进行派发.

// 自动监听对象变化
const obj = { name: 'youge', age: 18 }

const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    // depend.notify()
    const depend = getDepend(target, key)
    depend.notify()
    
    return Reflect.get(target, key, receiver)
  }, 

  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    //depend.notify() 要根据当前对象下的某属性去派发
    const depend = getDepend(target, key)
    
    console.log(depend.reactiveFns) // []
    depend.notify()
  }

})

05-正确地收集依赖

当前我们收集依赖的方式是直接收集, 只要调用该方法就会收集一次

// 不能将所有的 fn 都一股脑的添加到依赖中
const depend = new Depend()

function watchFn(fn) {
  depend.addDepend(fn)
}

const obj = { name: 'youge', age: 18 }

// 必须要区分 watchFn(fn) 中的 fn 是对应的哪个对象的哪个属性
watchFn(() => console.log(objProxy.name, 'name 被访问需要执行'))

watchFn(() => console.log(objProxy.age, 'age 发生变化需要执行--1'))

  • 不知道当前是 哪个 obj 对象 的 哪个 key 对应的 depend 对象, 需要收集依赖
  • 只能针对一个单独的 depend 对象来收集依赖

因此要重新优化这个 watchFn() 函数:

function watchFn(fn) {
  // depend.addDepend(fn)
  // 1. 先找到这个 fn 对应的对象属性的 depend 对象
  // objProxy.age -> objAgeDepend

  // 调用的时候, 就会走 const depend = getDepend(target, key)
  // 这时候给 depend 添加上函数就好了 depend.addDepend()
  fn() 
}

当调用的时候, 以添加的这个为例:

watchFn(() => console.log(objProxy.name, 'name 被访问需要执行'))

加入的时候会被执行一次, 然后 objProxy.name 又会触发 get 监控, 则此时的 depend 就是正确的对象.

即正确的依赖收集时机, 就是在 get 捕获器的时候.

get: function(target, key, receiver) {
    // 此时的 depend 就是当前的属性对象
    const depend = getDepend(target, key)
    // 则将此依赖函数添加进来, 则为最佳时机
    depend.addDepend(fn) // 这 fn 拿不到
  
    depend.notify()
    return Reflect.get(target, key, receiver)

这里的 fn 就是 这个 () => console.log(objProxy.name, 'name 被访问需要执行') 函数, 但并不能直接拿到, 该怎么办呢?

function watchFn(fn) {
  fn()
}

// 需要将 上面的 fn 函数传到 下面的 fn 位置
// 不在一个作用域哈
depend.addDepend(fn)

可以这样搞个小技巧, 定义一个全局变量, 作为 中转站, 在 fn 执行前将它赋值给 全局变量, 这样在 depend 的函数作用域, 肯定是可以访问到全局变量的. 就拿到啦

// 全局当前活跃的响应式函数依赖(函数)

let globalActiveReactiveFn = null 

function watchFn(fn) {
  // 在执行函数前, 将 fn 赋值给 全局变量, 给别的地方引用
  globalActiveReactiveFn = fn 
  fn() 
  globalActiveReactiveFn = null 
}

get: function(target, key, receiver) {
    const depend = getDepend(target, key)
    // 添加依赖, 注意不要在 get 派发更新, 否则会死循环
    if (globalActiveReactiveFn) {
      depend.addDepend(globalActiveReactiveFn)
    }
    return Reflect.get(target, key, receiver)
  },

顺带修复了一下之前的重点 BUG, 在 get 中不要进行 depend.notify(), 这样会造成死循环的.

  • get 里面进行依赖收集, 千万不要进行 notify 呀
  • set 里面进行数据更新之后的, 依赖更新 (最好先验证是是否真的改变值)

// 给对象添加依赖
watchFn(() => console.log(objProxy.name, 'name 被访问需要执行'))

watchFn(() => console.log(objProxy.age, 'age 发生变化需要执行--1'))

watchFn(() => console.log(objProxy.age, 'age 发生变化需要执行--2'))

// 自动收集
watchFn(() => console.log(objProxy.name, '新函数'))

console.log('-------------- 改变 obj 的 属性值')


objProxy.name = 'cj'

youge name 被访问需要执行
18 age 发生变化需要执行--1
18 age 发生变化需要执行--2
youge 新函数
-------------- 改变 obj 的 属性值

cj name 被访问
cj 新函数

06-对 Depend 类重构

当前实现方式其实还有一些问题, 比如添加依赖的而时候, 必须要在 Proxy 中手动去添加, 全局变量的.

get: function(target, key, receiver) {
    const depend = getDepend(target, key)
    // 添加当前对象属性变化的依赖
    if (globalActiveReactiveFn) {
      depend.addDepend(globalActiveReactiveFn)
    }
    return Reflect.get(target, key, receiver)
  },

就这个 globalActiveReactiveFn 来说, Prox 其实可以不需要知道这个变量存在的. 它其实这样这样:

depend.adddepend() // 收集依赖即可, 不希望添加 fn 的操作显式放 get 中

还有一个问题是, 如果依赖中有两次用到 key , 如下面的 两次 .name, 则会被重复收集, 重复调用了.

// 给对象添加依赖
// 函数中用到 2次 key, 则会被收集两次
watchFn(() => {
  console.log(objProxy.name, '-------')
  console.log(objProxy.name, '+++++++')
})

console.log('-------------------- 属性改变')


objProxy.name = 'cj'

youge -------
youge +++++++
-------------------- 属性改变
cj -------
cj +++++++
cj -------
cj +++++++

那解决知道其实就是用 集合 Set, 将原来保存依赖的函数数据换成 Set 就好了. 这样去完成了 Deep 类的重构.

// 全局当前活跃的响应式函数依赖(函数)
let globalActiveReactiveFn = null 

// 重构 Deep 类
// 优化1: 重构 addDeep() 方法来收集依赖
// 优化2: 采用 Set 来替换 数组保存依赖函数

class Depend {
  constructor() {
    // 用 Set 来存储依赖函数, 减少重复收集后, 重复派发
    this.reactiveFns = new Set()
  }

  addDepend() {
    if(globalActiveReactiveFn) {
      this.reactiveFns.add(globalActiveReactiveFn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

07-再封装 Proxy, 实现将对象变成响应式

当前的响应式操作, 就只能是对 obj 对象进行响应式, 因为我们的监听写死了 obj

const obj = { name: 'youge', age: 18 }

const objProxy = new Proxy(obj, {
  get: function(target, key, receiver) {
    const depend = getDepend(target, key)
    // 自动收集依赖
    depend.addDepend()

    return Reflect.get(target, key, receiver)
  }, 

  set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    //depend.notify() 要根据当前对象下的某属性去派发
    const depend = getDepend(target, key)
    depend.notify()
  }

})

当前只能针对 obj 对象, 要像适用于所有对象, 即将这个过程, 再封装为一个通用函数即可.

// 封装为响应式函数

function reactive(obj) {
  return new Proxy(obj, {
    get: function(target, key, receiver) {
      const depend = getDepend(target, key)
      // get 收集依赖任务, 根据传入的对象和属性
      depend.addDepend()
      return Reflect.get(target, key, receiver)
    }, 
  
    set: function(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      // set 派发依赖执行任务, 根据传入的对象和属性
      const depend = getDepend(target, key)
      depend.notify()
    }
  })
}



// 将对象变为响应式对象
const obj = { name: 'youge', age: 18 }
const objProxy = reactive(obj)

// 给对象添加依赖
watchFn(() => {
  console.log(objProxy.name, '-------')
  console.log(objProxy.name, '+++++++')
})

console.log('-------------------- 属性改变')
objProxy.name = 'cj'

// 创建其他响应式对象 
const foo = reactive({
  city: '长安镇',
  height: 1.8
})

// 添加一些依赖 
watchFn(() => console.log(foo.city, 'foo 的依赖'))
foo.city = '杭州市'

youge -------
youge +++++++
-------------------- 属性改变
cj -------
cj +++++++
长安镇 foo 的依赖
cj@mini test % node "/Users/cj/Desktop/test/cj.js"
youge -------
youge +++++++
-------------------- 属性改变
cj -------
cj +++++++
长安镇 foo 的依赖
杭州市 foo 的依赖

vue3 + vue2 响应式小结

综上, 就基本实现了 vue3 响应式的工作原理, 一步步推导出来. 这一步步做下来, 只要思路清晰, 还是很好理解的.

// vue3 响应式原理

// 全局当前活跃的响应式函数依赖(函数)
let globalActiveReactiveFn = null 

class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }

  addDepend() {
    if(globalActiveReactiveFn) {
      this.reactiveFns.add(globalActiveReactiveFn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

// 获取 depend 对象, 根据传入的对象和属性
const objsMap = new WeakMap()

function getDepend(target, key) {
  // 从对象池里获取对象
  let objMap = objsMap.get(target)
  // 如果没有的话就新建, 并添加到 objsMap 大池中
  if (!objMap) {
    objMap = new Map()
    objsMap.set(target, objMap)
  }

  // 根据 key 去获取属性及对应的 depend 对象
  let depend = objMap.get(key)
  // 如果该属性没有依赖, 则初始化, 并添加到 objMap 中
  if (!depend) {
    depend = new Depend()
    objMap.set(key, depend)
  }
  return depend
}

function watchFn(fn) {
  globalActiveReactiveFn = fn 
  fn() 
  globalActiveReactiveFn = null 
}

// 封装通用的响应式函数
function reactive(obj) {
  return new Proxy(obj, {
    get: function(target, key, receiver) {
      const depend = getDepend(target, key)
      // get 收集依赖任务, 根据传入的对象和属性
      depend.addDepend()
      return Reflect.get(target, key, receiver)
    }, 
  
    set: function(target, key, newValue, receiver) {
      // 判断是否真的变化, 避免 obj.a = obj.a 的无意义更新
      const oldValue = target[key]
      if (oldValue === newValue) return true 

      const result = Reflect.set(target, key, newValue, receiver)
      // set 派发依赖执行任务, 根据传入的对象和属性
      const depend = getDepend(target, key)
      depend.notify()
      return result
    }
  })
}

// 测试
const obj = reactive({name: 'youge', age: 18})

watchFn(() => console.log(obj.age, ' age 监控管理'))
obj.age = 30

18  age 监控管理
30  age 监控管理

最后来补充一下 vue2 的响应式原理也是差不多的, 区别在于监听对象用的是 Object.defineProperty()

// vue2 版, 其他不变

function reactive2(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    // 监控属性, get 收集依赖
    Object.defineProperty(obj, key, {
      get: function() {
        const depend = getDepend(obj, key)
        depend.addDepend()
        return value 
      }, 
      // 监控属性, set 派发依赖执行任务
      set: function(newValue) {
        // 避免新旧值相同, 做无效的更新派发
        if (newValue === value) return true 
        
        const depend = getDepend(obj, key)
        value = newValue
        // 更新值后, 派发更新依赖任务
        depend.notify()
      }
    })
  })

  return obj
}

至此, 关于 Proxy + Reflect 和 vue2, vue3 的响应式实现原理就差不多了, 还是需要再去仔细回味整个实现过程呀.

耐心和恒心, 总会获得回报的.



举报

相关推荐

0 条评论