共计 10655 个字符,预计需要花费 27 分钟才能阅读完成。
浅析 Vue3 中的响应式原理
请在此前阅读 Vue Composition API 内容,熟悉一下 api。
实现响应式的方式
- defineProperty
- Proxy
众所周知在 Vue3 中使用了 ES6 提供的Proxy
API 取代了之前 defineProperty 来实现对数据的侦测.
为啥使用 Proxy?
我们知道在 Vue2 对于监测数组的变化是通过重写了数组的原型来完成的,这么做的原因是:
- 不会对数组每个元素都监听,提升了性能.(
arr[index] = newValue
是不会触发试图更新的, 这点不是因为 defineProperty 的局限性,而是出于性能考量的) - defineProperty 不能检测到数组长度的变化,准确的说是通过改变 length 而增加的长度不能监测到 (
arr.length = newLength
也不会)。
同样对于对象,由于 defineProperty 的局限性,Vue2 是不能检测对象属性的添加或删除的。
function defineReactive(data, key, val) { | |
Object.defineProperty(data, key, { | |
enumerable: true, | |
configurable: true, | |
get() {console.log(`get key: ${key} val: ${val}`) | |
return val | |
}, | |
set(newVal) {console.log(`set key: ${key} val: ${newVal}`) | |
val = newVal | |
} | |
}) | |
} | |
function observe(data) {Object.keys(data).forEach((key)=> {defineReactive(data, key, data[key]) | |
}) | |
} | |
let test = [1, 2, 3] | |
observe(test) | |
console.log(test[1]) // get key: 1 val: 2 | |
test[1] = 2 // set key: 1 val: 2 | |
test.length = 1 // 操作是完成的, 但是没有触发 set | |
console.log(test.length) // 输出 1,但是也没有触发 get |
实现响应式前的一些小细节
相对于 defineProperty,Proxy 无疑更加强大,可以代理数组,并且提供了多种属性访问的方法 traps(get,set,has,deleteProperty 等等)。
let data = [1,2,3] | |
let p = new Proxy(data, {get(target, key, receiver) { | |
// target 目标对象,这里即 data | |
console.log('get value:', key) | |
return target[key] | |
}, | |
set(target, key, value, receiver) { | |
// receiver 最初被调用的对象。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是 proxy 本身)。// 比如,假设有一段代码执行 obj.name = "jen",obj 不是一个 proxy 且自身不含 name 属性,但它的原型链上有一个 proxy,那么那个 proxy 的 set 拦截函数会被调用,此时 obj 会作为 receiver 参数传进来。console.log('set value:', key, value) | |
target[key] = value | |
return true // 在严格模式下,若 set 方法返回 false,则会抛出一个 TypeError 异常。} | |
}) | |
p.length = 4 // set value: length 4 | |
console.log(data) // [1, 2, 3, empty] |
但是对于数组的一次操作可能会触发多次 get/set, 主要原因自然是改变数组的内部 key 的数量了(即对数组进行插入删除之类的操作), 导致的连锁反应。
// data : [1,2,3] | |
p.push(1) | |
// get value: push | |
// get value: length | |
// set value: 3 1 | |
// set value: length 4 | |
// data : [1,2,3] | |
p.shift() | |
// get value: shift | |
// get value: length | |
// get value: 0 | |
// get value: 1 | |
// set value: 0 2 | |
// get value: 2 | |
// set value: 1 3 | |
// set value: length 2 |
同时 Proxy 是仅代理一层的,对于深层对象,也是需要开发者自行实现的,此外对于对象的添加是可以 set
traps 侦测到的,删除则需要使用 deleteProperty
traps。
let data = {a:1,b:{c:'c'},d:[1,2,3]} | |
let p = new Proxy(data, {.... 同上}) | |
console.log(p.a) | |
console.log(p.b.c) | |
console.log(p.d) | |
console.log(p.d[0]) | |
p.e = 'e' // 这里可以看到给对象添加一个属性也依然可以侦测到变化 | |
// get value: a | |
// 1 | |
// get value: b | |
// c | |
// get value: d | |
// [1, 2, 3] | |
// get value: d | |
// 1 | |
// set value: e e |
还有一件事,对于一些简单的 get/set 操作,我们在 traps 中使用 target[key]
,target[key] = value
是可以达到我们的需求的,但是对于一些复杂的如 delet,in 这类操作这样实现就不够优雅了,而 ES6 提供了 ReflectAPI, 且与 Proxy 的 traps 一一对应, 用来代替 Object 的默认行为。
所以我们先将之前的代码改造一下:
let p = new Proxy(data, {get(target, key, receiver) {console.log('get value:', key) | |
const res = Reflect.get(target, key, receiver) | |
return res | |
}, | |
set(target, key, value, receiver) {console.log('set value:', key, value) | |
// 如果赋值成功,则返回 true | |
const res = Reflect.set(target, key, value, receiver) | |
return res | |
} | |
}) |
实现响应式
明确目标
我们最终实现的效果应该如下一样:
const a = reactive({b: 0}) | |
effect(() => {console.log(a.b) | |
}) //effect 在传入时就会自动执行一次 | |
a.b ++ // 这时会打印 1 |
实现思路
熟悉 Vue2 的同学都知道 Vue2 的响应式是在 get 中收集依赖, 在 set 中触发依赖,Vue3 想必也不例外, 按照这个思路我们的实现步骤如下:
- 在触发 get 时收集 effect 函数传入的回调,这里我们称这个回调为 ReactiveEffect
- 在 set、deleteProperty… 时触发所有的 ReactiveEffect
下面我们看下具体的实现步骤
reactive 的简单实现
第一步,我们先来简单实现一个可以对对象增删改查侦测的函数
在 set 的实现中,我们将对象的 set 分为两类:新增 key 和更改 key 的 value。通过 hasOwnProperty 判断这个对象是否含有这个属性,不存在存在则是添加属性,存在则判断新 value 和旧 value 是否相同,不同才需要触发 log 执行。
这里的 reactive 函数我们记为 V1 版本。
const hasOwn = (val,key)=>{const res = Object.prototype.hasOwnProperty.call(val, key) | |
console.log(val,key,res) | |
return res | |
} | |
function reactive(data){ | |
return new Proxy(data, {get(target, key, receiver) {console.log('get value:', key) | |
const res = Reflect.get(target, key, receiver) | |
return res | |
}, | |
set(target, key, value, receiver) {const hadKey = hasOwn(target, key) | |
const oldValue = target[key] | |
const res = Reflect.set(target, key, value, receiver) | |
if (!hadKey) {console.log('set value:ADD', key, value) | |
} else if (value !== oldValue) {console.log('set value:SET', key, value) | |
} | |
return res | |
}, | |
deleteProperty(target, key){const hadKey = hasOwn(target, key) | |
const oldValue = target[key] | |
const res = Reflect.deleteProperty(target, key) | |
if (hadKey) {console.log('set value:DELETE', key) | |
} | |
return res | |
} | |
}) | |
} |
依赖收集
在 Vue3 中针对所有的被监听的对象,存在一张关系表 targetMap,key 为 target,value 为另一张关系表 depsMap。
depsMap 的 key 为 target 的每个 key,value 为由 effect 函数传入的参数的 Set 集。
type Dep = Set<ReactiveEffect> | |
type KeyToDepMap = Map<string | symbol, Dep> | |
const targetMap = new WeakMap<any, KeyToDepMap>() | |
// 大概结构如下所示 | |
// target | depsMap | |
// obj | key | Dep | |
// k1 | effect1,effect2... | |
// k2 | effect3,effect4... | |
// obj2 | key | Dep | |
// k1 | effect1,effect2... | |
// k2 | effect3,effect4... | |
// |
同时我们还需要收集 effect 函数的回调 ReactiveEffect, 当 ReactiveEffect 内有已被监听的对象 get 触发 get 时,便需要一个存储 ReactiveEffect 的地方。这里使用一个数组记录:
const activeReactiveEffectStack: ReactiveEffect[] = []
effect 函数实现如下:
function run(effect,fn,args){ | |
try {activeReactiveEffectStack.push(effect) | |
return fn(...args) // 执行 fn 以收集依赖 | |
} finally {activeReactiveEffectStack.pop() | |
} | |
} | |
function effect(fn,lazy=false){const effect1 = function (...args){return run(effect1, fn, args) | |
} | |
if (!lazy){effect1() | |
} | |
return effect1 | |
} |
track 跟踪器,由于 get 时的依赖收集:
function track(target,type,key){const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] | |
if (effect) {let depsMap = targetMap.get(target) | |
if (depsMap === void 0) {targetMap.set(target, (depsMap = new Map())) | |
} | |
let dep = depsMap.get(key) | |
if (dep === void 0) {depsMap.set(key, (dep = new Set())) | |
} | |
if (!dep.has(effect)) {dep.add(effect) | |
} | |
} | |
} |
trigger 触发器,key 变化 (set,delete…) 时触发, 获取这个 key 所对应的所有的 ReactiveEffect, 然后执行:
function trigger(target,type,key){console.log(`set value:${type}`, key) | |
const depsMap = targetMap.get(target) | |
if (depsMap === void 0) {return} | |
// 获取已存在的 Dep Set 执行 | |
const dep = depsMap.get(key) | |
if (dep !== void 0) { | |
dep.forEach(effect => {effect() | |
}) | |
} | |
} |
reactive 函数如下:
function reactive(target){ | |
const observed = new Proxy(target, {get(target, key, receiver) {const res = Reflect.get(target, key, receiver) | |
track(target,"GET",key) | |
return res | |
}, | |
set(target, key, value, receiver) {const hadKey = hasOwn(target, key) | |
const oldValue = target[key] | |
const res = Reflect.set(target, key, value, receiver) | |
if (!hadKey) {trigger(target,"ADD",key) | |
} else if (value !== oldValue) {trigger(target,"SET",key) | |
} | |
return res | |
}, | |
deleteProperty(target, key){const hadKey = hasOwn(target, key) | |
const oldValue = target[key] | |
const res = Reflect.deleteProperty(target, key) | |
if (hadKey) {console.log('set value:DELETE', key) | |
} | |
return res | |
} | |
}) | |
if (!targetMap.has(target)) {targetMap.set(target, new Map()) | |
} | |
return observed | |
} |
深层监听
通常我们都会想到通过递归的方式, 对每个 key 判读啊是否为对象来进行监听,在 Vue3 中:
function get(target, key, receiver) {const res = Reflect.get(target, key, receiver) | |
track(target, "GET", key) | |
return isObject(res) ? reactive(res) : res | |
} |
Vue3 中,这里做了性能的优化,做了一层 lazy access 的操作,这样只有在访问到深层的对象时才会去做代理。
注意
此时我们所有的 ReactiveEffect 都是和 key 绑定的, 也就是说, 在 ReactiveEffect 函数中, 我们必须 get 一次确定的某个 key, 否则在 set 时是没有 ReactiveEffect 可以触发的, 举个列子:
let data = {a:1,b:{c:'c'}} | |
let p = reactive(data) | |
effect(()=>{console.log(p)}) | |
p.a = 3 |
上面这种情况是不会打印 p 的.
let data = {a:1,b:{c:'c'}} | |
let p = reactive(data) | |
effect(()=>{console.log(p.a)}) | |
p.a = 3 // 3 |
这种情况才会执行()=>{console.log(p.a)
, 打印 3
数组类型的问题
但对于数组进行一些操作时,执行起来会有一点小不同, 我们来使用 V1 版本的 reactive 函数来监听一个数组p = reactive([1,2,3])
, 并分别对 p 进行操作看看结果:
p.push(1) | |
// set value:ADD 3 1 | |
p.unshift(1) | |
// set value:ADD 3 3 | |
// set value:SET 2 2 | |
// set value:SET 1 1 | |
p.splice(0,0,2) | |
// set value:ADD 3 3 | |
// set value:SET 2 2 | |
// set value:SET 1 1 | |
// set value:SET 0 2 | |
p[3] = 4 | |
// set value:ADD 3 4 | |
-------- | |
p,pop() | |
// set value:DELETE 2 | |
// set value:SET length 2 | |
p.shift() | |
// set value:SET 0 2 | |
// set value:SET 1 3 | |
// set value:DELETE 2 | |
// set value:SET length 2 | |
delete p[0] | |
// set value:DELETE 0 | |
// 这里 p 的 length 依然是三 |
可以发现当我们对数组添加元素时,对于 length 的 SET 并不会触发(), 而删除元素时才会触发 length 的 SET, 同时对数组的一次操作触发了多次 log。
这里在我们对数组添加操作时就会出现一个问题,我们使用p.push(1)
, 操作的 index 是 3,上面的列子我们知道在 effect 函数中我们必须 get 这个 3,才会把 ReactiveEffect 给绑定上去, 但那时候是很没有 3 这个 index 的, 所以就会导致没有办法执行 ReactiveEffect。
Vue3 中的处理是在 trigger 中添加一段代码:
function trigger(target,type,key){console.log(`set value:${type}`, key) | |
const depsMap = targetMap.get(target) | |
if (depsMap === void 0) {return} | |
const effects = new Set() | |
if (key !== void 0) {const depSet = depsMap.get(key) | |
if (depSet !== void 0) { | |
depSet.forEach(effect => {effects.add(effect) | |
}) | |
} | |
} | |
// 就是这里啦,(这里做了一些更改) | |
if (type === "ADD" || type === "DELETE") {if(Array.isArray(target)){ | |
const iterationKey = 'length' | |
const depSet = depsMap.get(iterationKey) | |
if (depSet !== void 0) { | |
depSet.forEach(effect => {effects.add(effect) | |
}) | |
} | |
} | |
} | |
// 获取已存在的 Dep Set 执行 | |
effects.forEach(effect=>effect()) | |
} |
当监听的 target 为数组时, 操作为 ADD 或者 DELETE 时, 触发的 ReactiveEffect 为绑在数组 length 上的, 看下面一段代码:
let data = {foo: 'foo', ary: [1, 2, 3] } | |
let r = reactive(data) | |
effect(()=>console.log(r.ary.length)) | |
r.ary.unshift(1) // 4 |
验证
我们来拿 Vue3 的代码执行一下看一下是否和我们的一样;
yarn build 之后引入 reactivity.global.js
const {reactive, effect} = VueObserver | |
let data = {foo: 'foo', ary: [1, 2, 3] } | |
let r = reactive(data) | |
effect(()=>console.log(r.ary.length)) | |
r.ary.unshift(1) // 4 | |
-------------- | |
const {reactive, effect} = VueObserver | |
let data = {foo: 'foo', ary: [1, 2, 3] } | |
let r = reactive(data) | |
effect(()=>console.log(r.ary)) | |
r.ary.unshift(1) // 没有打印 | |
------- | |
const {reactive, effect} = VueObserver | |
let data = {foo: 'foo', ary: [1, 2, 3] } | |
let r = reactive(data) | |
effect(()=>console.log(r)) | |
r.foo = 1 // 啥也没打印 | |
-------- | |
const {reactive, effect} = VueObserver | |
let data = {foo: 'foo', ary: [1, 2, 3] } | |
let r = reactive(data) | |
effect(()=>console.log(r.foo)) | |
r.foo = 1 // 1 | |
------ | |
const {reactive,effect} = VueObserver | |
let data = {foo: 'foo', ary: [1, 2, 3] } | |
let r = reactive(data) | |
effect(()=>console.log(r.ary.join())) | |
r.ary.unshift(1) | |
// 1,2,3 | |
// 1,2,3,3 | |
// 1,2,2,3 | |
// 1,1,2,3 | |
// 多次打印,证明多次触发 |
可以发现我们的代码和 Vue3 表现是一致的。
测试的代码都在这个里面➡️代码
总结
到此我们应该算是对 Vue3 中的响应式有一个了解了, 第一次写文章哈,如有错误的地方还望雅正
完整的代码
const activeReactiveEffectStack = [] | |
const targetMap = new WeakMap() | |
const isObject = (val) => val !== null && typeof val === 'object' | |
const hasOwn = (val,key)=>{const res = Object.prototype.hasOwnProperty.call(val, key) | |
//console.log(val,key,res) | |
return res | |
} | |
function run(effect,fn,args){ | |
try {activeReactiveEffectStack.push(effect) | |
return fn(...args) // 执行 fn 以收集依赖 | |
} finally {activeReactiveEffectStack.pop() | |
} | |
} | |
function effect(fn,lazy=false){const effect1 = function (...args){return run(effect1, fn, args) | |
} | |
if (!lazy){effect1() | |
} | |
return effect1 | |
} | |
function track(target,type,key){const effect = activeReactiveEffectStack[activeReactiveEffectStack.length - 1] | |
if (effect) {let depsMap = targetMap.get(target) | |
if (depsMap === void 0) {targetMap.set(target, (depsMap = new Map())) | |
} | |
let dep = depsMap.get(key) | |
if (dep === void 0) {depsMap.set(key, (dep = new Set())) | |
} | |
if (!dep.has(effect)) {console.log(key,effect) | |
dep.add(effect) | |
} | |
} | |
} | |
function trigger(target,type,key){console.log(`set value:${type}`, key) | |
const depsMap = targetMap.get(target) | |
if (depsMap === void 0) {return} | |
const effects = new Set() | |
if (key !== void 0) {const depSet = depsMap.get(key) | |
if (depSet !== void 0) { | |
depSet.forEach(effect => {effects.add(effect) | |
}) | |
} | |
} | |
if (type === "ADD" || type === "DELETE") {if(Array.isArray(target)){ | |
const iterationKey = 'length' | |
const depSet = depsMap.get(iterationKey) | |
if (depSet !== void 0) { | |
depSet.forEach(effect => {effects.add(effect) | |
}) | |
} | |
} | |
} | |
// 获取已存在的 Dep Set 执行 | |
effects.forEach(effect=>effect()) | |
} | |
function reactive(target){ | |
const observed = new Proxy(target, {get(target, key, receiver) {const res = Reflect.get(target, key, receiver) | |
track(target,"GET",key) | |
return isObject(res) ? reactive(res): res | |
}, | |
set(target, key, value, receiver) {const hadKey = hasOwn(target, key) | |
const oldValue = target[key] | |
const res = Reflect.set(target, key, value, receiver) | |
if (!hadKey) {trigger(target,"ADD",key) | |
} else if (value !== oldValue) {trigger(target,"SET",key) | |
} | |
return res | |
}, | |
deleteProperty(target, key){const hadKey = hasOwn(target, key) | |
const oldValue = target[key] | |
const res = Reflect.deleteProperty(target, key) | |
if (hadKey) {console.log('set value:DELETE', key) | |
} | |
return res | |
} | |
}) | |
if (!targetMap.has(target)) {targetMap.set(target, new Map()) | |
} | |
return observed | |
} |