vue3 响应式原理
如有谬误,欢送指出~
更多学习笔记申请戳:https://github.com/6fa/WebKno…
1. 响应式外围
如果上面的例子中,想让 sum 变为响应式变量:
let num1 = 1;
let num2 = 2;
let sum = num1 + num2;
num1 = 10
console.log(sum) //sum 仍旧是 3,非响应式
则要实现的局部有:
- 数据劫持:要晓得 num1、num2 何时发生变化
- 依赖收集:晓得 sum 依赖哪些数据,例子中 sum 依赖了 num1、num2,则要建设它们的依赖关系
- 派发更新:当依赖的数据 num1、num2 产生扭转时,要告诉响应对象 sum 从新运算
vue3 通过 Proxy 拦挡数据的读取和设置(数据劫持),当数据读取时,通过 track 函数触发依赖的收集;当数据被设置时,通过 trigger 函数去派发更新。
那么 vue3 如何应用响应式呢?
- vue3 既能够通过 data 函数返回一个响应式对象,也能够通过 ref、reactive 来创立响应式变量。应用 reactive 等时,即在外部对数据用 Proxy 进行了包装。
- 应用 computed、watch、视图渲染函数等时,能够看作申明了一个依赖响应式数据的回调,这个回调会被传入 effect(副作用函数),当依赖的数据扭转时,回调被从新调用,从而 computed 等失去更新。
要实现简略版的响应式,其大抵构造为:
// 创立响应式变量,拦挡数据的 get 和 set
function reactive(obj){}
//effect 函数包裹那些 依赖响应式数据的函数 cb
//cb 依赖的数据更新时,从新执行 effect
function effect(cb){}
// 依赖收集,建设响应式数据和 effect 的映射关系
function track(target, property){}
// 触发更新,依据依赖关系,执行 effect 函数
function trigger(target, property){}
应用:
let obj = reactive({
num1: 10,
num2: 20
})
let sum = 0
effect(()=>{sum = obj.num1 + obj.num2})
console.log(sum) //30
obj.num1 = 100
console.log(sum) // 应该为 120
2.Proxy & Reflect 的根本应用
实现响应式变量的创立前,须要晓得 Proxy 和 Reflect 的根本应用。
JS 很难对单个局部变量进行跟踪,然而能够跟踪对象的属性变动:vue3 应用的 ES6 的 Proxy 和 Reflect。
-
Proxy 拦挡对象的读取、设置等操作,而后进行操作解决。然而不会间接操作源对象,而是通过对象的代理对象。
//Proxy 用法 //Proxy 对象由 target(指标对象)、handler(指定代理对象行为的对象)组成 let target = { a:1, b:2 } let handler = { //receiver 指调用该行为的对象,通常是 Proxy 实例自身 get(target, propKey, receiver){return target[propKey] //getter 甚至能够不返回数据 }, set(target, propKey, value, receiver){target[propKey] = value } } let proxy = new Proxy(target, handler) console.log(proxy.a) //1 proxy.a = 3 console.log(proxy.a) //3
-
Reflect 能间接调用对象的外部办法,和 Proxy 一样有获取、设置等操作。
//Reflect 用法 let target = {get a(){return this.num1 + this.num2}, set a(val){return this.num1 = val} } //receiver 为可选参数 let receiver = { num1:10, num2:20 } //Reflect.get(target, propKey, receiver) // 相当于间接操作 target 的 get a(){} Reflect.get(target, 'a', receiver) //30 this 绑定到了 receiver //Reflect.set(target, propKey, value, receiver) Reflect.set(target, 'a', 100, receiver) //100
-
Reflect 的作用次要是解决 this 的绑定问题,将 this 绑定到 proxy 对象而不是指标对象:比方 Reflect.get(target,property,receiver)获取属性时,如果 property 指定了 getter,getter 的 this 将绑定到 receiver 对象。
//Proxy 的问题 const obj = { a: 10, get double(){return this.a*2} } const proxyobj = new Proxy(obj,{get(target, propKey, receiver){return target[propKey] } }) let obj2 = {__proto__: proxyobj} obj2.a = 20 obj2.double // 期望值为 40,理论是 20,因为 double 的 getter 里的 this 绑定到了 obj // 应用 Reflect 解决 this 绑定问题 const obj = { a: 10, get double(){return this.a*2} } const proxyobj = new Proxy(obj,{get(target, propKey, receiver){ // 这里的 receiver 是 obj2 return Reflect.get(target, propKey, receiver) } }) let obj2 = {__proto__: proxyobj} obj2.a = 20 obj2.double //40, 通过 Refelct 的 receiver,get double() 里的 this 绑定到了 obj2
3.Reactive 函数的实现
创立响应式变量 reactive 函数的实现,次要靠外部实例化一个 Proxy 对象:
function reactive(obj){
const handler = { // 拦挡数据的 get、set 进行解决
get(){},
set(){}
}
const proxyObj = new Proxy(obj,handler)
return proxyObj // 返回代理对象实例
}
在获取数据时,就要开始进行数据的依赖收集(交给 track 函数去实现);在设置数据时,要触发更新(交给 trigger 函数去实现):
function reactive(obj){
const handler = {get(target, propKey, receiver){const val = Reflect.get(...arguments) // 读取数据
track(target, propKey) // 依赖收集
return val
},
set(target, propKey, newVal, receiver){const success = Reflect.set(...arguments) // 设置数据,返回 true or false
trigger(target, propKey) // 触发更新
return success
}
}
const proxyObj = new Proxy(obj,handler)
return proxyObj // 返回代理对象实例
}
然而下面仅对一层的对象起作用,对于属性还是对象的多层嵌套对象不起作用,须要手动递归实现响应:
function reactive(obj){
const handler = {get(target, propKey, receiver){const val = Reflect.get(...arguments)
track(target, propKey)
if(typeof val === 'object'){return reactive(val) // 新增
}
return val
}
}
...
}
4. 副作用函数
在实现下面的 track 和 trigger 前,还要理解副作用函数。副作用函数 effect 用来跟踪正在运行的函数,比方 watch、computed,外面的代码会被传入 effect,当 watch、computed 外面依赖的其余数据变动时,从新运行外面的代码。
以 vue3 的 computed 为例子:
const num = ref(10) //num 是响应式
const double = computed(()=>num*2)
能够把 computed 外面的内容看作依赖了响应式数据的更新函数(上面简称更新函数),且 computed 返回一个 ref 援用,则在 computed 函数外部,会有大略相似于上面的操作:
computed(cb){const result = ref()
effect(()=>result.value = cb())
return result
}
更新函数被当作 effect 函数的回调:
// 以后运行的副作用函数
let activeEffect = null
const effect = (cb)=>{
activeEffect = cb
// 运行响应式函数
cb()
activeEffect = null
}
effect 函数执行了更新函数,则会读取它依赖的数据,后面咱们曾经为这些数据设置了 proxy 代理,就在此时实现了依赖收集(建设更新函数与依赖的数据的映射关系,当数据发生变化,会通过该映射关系找到依赖该数据的更新函数,再次执行)。
以上面例子来阐明:
let num = reactive({value: 10})
let double = 0
effect(()=>{double = num.value*2}) //double 为响应式
let triple = num.value*3 //triple 不是响应式
//1. num 被 reactive 包装成响应式变量,会对它属性的获取、设置进行拦挡
//2. 运行副作用函数 effect,将以后运行的副作用函数 activeEffect 指向 effect 的回调,即更新函数
//3. 执行 activeEffect(更新函数)//4. 更新函数外部会读取 num.value, 触发 proxy 的依赖收集 track 函数
//6. track 外面会将 num.value 与更新函数建设映射关系
// 要建设映射关系是因为,当 num.value 扭转时,trigger 须要查找出全副依赖 num.value 的更新函数
// 而后全副从新运行,从而 double 被从新赋值
//7. double 被赋值
//8. 把 activeEffect 从新指向 null
//8. 运行到 triple,即便读取了 num.value,然而此时 activeEffect 为 null
// 不会建设 num.value 与 activeEffect 的映射关系,所以 num.value 扭转时不会更新到 triple
5.Track & Trigger 的实现
track(target, property):
次要将 target.property 与更新函数记录在一起,造成映射关系,这样就晓得依赖 target.property 都有哪些更新函数
trigger(target, property):
从映射关系中找到依赖 target.property 的更新函数,从新运行它们
依赖一个 target.property 的更新函数能够有很多,用 Set 构造去贮存它们:
property1: Set [cb1,cb2,cb3...]
将这个 set 构造称为 dep,每个响应式属性都要有一个 dep,能够用 Map 构造贮存:
Map {property1: Set [cb1,cb2,cb3...],
property2: Set [cb1,cb2,cb3...],
property3: Set [cb1,cb2,cb3...],
......
}
将这个 Map 构造称为 depsMap。然而这只是同一个对象里的属性,如果有多个对象呢?
因而须要又包裹一层,用另一个 Map(称为 targetMap 构造)包裹每个对象的 Map,然而 targetMap 用 WeakMap 构造,WeakMap 的属性刚好只能是对象:
WeakMap {
obj1: Map {property1:Set [cb1,cb2,cb3...],
...
},
obj2: Map {property1:Set [cb1,cb2,cb3...],
...
},
...
}
当数据被读取时,track 函数正是通过这个构造实现响应式属性和依赖它的更新函数的映射:
const targetMap = new WeakMap()
function track(target, property){if(!activeEffect)return
// 如果 activeEffect 为 null 则返回
// 只有运行 effect() 时 activeEffect 才有值
let depsMap = targetMap.get(target)
// 如果 target 对象还没对应的 depsMap 则新建
if(!depsMap){targetMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(property)
// 如果属性还没对应的 dep 则新建
if(!dep){depsMap.set(property, dep = new Set())
}
dep.add(activeEffect) // 增加属性对应的 effect 进映射构造
}
当数据被设置时,trigger 函数通过映射构造取出数据对应的所有更新函数并执行:
function trigger(target, property){const depsMap = targetMap.get(target)
if(!depsMap) return
const dep = depsMap.get(property)
if(!dep) return
//dep 是 Set 构造,有 forEach 办法
dep.forEach((effect)=>{effect()
})
}
6. 整合 & 应用
整合下面的代码:
//reactive.js
//effect 函数的实现
let activeEffect = null
function effect(cb){
activeEffect = cb
cb()
activeEffect = null
}
// 创立响应式变量函数
function reactive(obj){
const handler = {get(target, propKey, receiver){const val = Reflect.get(...arguments)// 读取数据
track(target, propKey) // 依赖收集
if(typeof val === 'object'){return reactive(val)
}
return val
},
set(target, propKey, newVal, receiver){const success = Reflect.set(...arguments)// 设置数据, 返回 true/false
trigger(target, propKey) // 触发更新
return success
}
}
const proxyObj = new Proxy(obj,handler)
return proxyObj
}
// 依赖收集函数
const targetMap = new WeakMap() // 贮存映射关系的构造
function track(target, property){if(!activeEffect)return
let depsMap = targetMap.get(target)
// 如果 target 对象还没对应的 depsMap 则新建
if(!depsMap){targetMap.set(target, depsMap = new Map())
}
let dep = depsMap.get(property)
// 如果属性还没对应的 dep 则新建
if(!dep){depsMap.set(property, dep = new Set())
}
dep.add(activeEffect) // 增加属性对应的 effect 进映射构造
}
// 派发更新函数
function trigger(target, property){const depsMap = targetMap.get(target)
if(!depsMap) return
const dep = depsMap.get(property)
if(!dep) return
dep.forEach((effect)=>{effect()
})
}
测试应用:
let obj = reactive({
num1: 10,
num2: 20,
son:{num3:20},
})
let sum = 0
effect(()=>{sum = obj.num1 + obj.num2 + obj.son.num3})
console.log(sum) //50
obj.num1 = 100
console.log(sum) //130 可知 sum 为响应式
参考:
Vue3 响应式原理及实现
Vue3 响应式原理 + 手写 reactive
Vue3 响应式原理与 reactive、effect、computed 实现
ES6 Reflect 与 Proxy