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.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.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
操作符,但它是函数式调用,更灵活、更可控.
参数 | 类型 | 必选 | 说明 |
| Function | ✅ | 要调用的构造函数(如 |
| Array | ✅ | 传给构造函数的参数列表(如 |
| Function | ❌ | 指定 |
// 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 的函数版本, 暂时我用的不算太多, 就先不研究了.
总之:
| 一组静态方法,用于操作对象 |
主要用途 | 配合 |
优势 | 返回值统一、支持 |
推荐用法 | 在 |
常见组合 |
|
总结就是: 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 的响应式实现原理就差不多了, 还是需要再去仔细回味整个实现过程呀.
耐心和恒心, 总会获得回报的.