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为响应式
参考:
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。