上篇讲了关于 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 = 20console.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] = newValueconst 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 作用, 类似 newfunction 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.ageDependobj2 对象
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 的响应式实现原理就差不多了, 还是需要再去仔细回味整个实现过程呀.
