vue3响应式原理
如有谬误,欢送指出~
更多学习笔记申请戳:https://github.com/6fa/WebKno...
1.响应式外围
如果上面的例子中,想让sum变为响应式变量:
let num1 = 1;let num2 = 2;let sum = num1 + num2;num1 = 10console.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和setfunction reactive(obj){}//effect函数包裹那些 依赖响应式数据的函数cb//cb依赖的数据更新时,从新执行effectfunction effect(cb){}//依赖收集,建设响应式数据和effect的映射关系function track(target, property){}//触发更新,依据依赖关系,执行effect函数function trigger(target, property){}
应用:
let obj = reactive({ num1: 10, num2: 20})let sum = 0effect(()=>{ sum = obj.num1 + obj.num2})console.log(sum) //30obj.num1 = 100console.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) //1proxy.a = 3console.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 = 20obj2.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是obj2return Reflect.get(target, propKey, receiver)}})let obj2 = {__proto__: proxyobj}obj2.a = 20obj2.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 = nullconst effect = (cb)=>{ activeEffect = cb //运行响应式函数 cb() activeEffect = null}
effect函数执行了更新函数,则会读取它依赖的数据,后面咱们曾经为这些数据设置了proxy代理,就在此时实现了依赖收集(建设更新函数与依赖的数据的映射关系,当数据发生变化,会通过该映射关系找到依赖该数据的更新函数,再次执行)。
以上面例子来阐明:
let num = reactive({value: 10})let double = 0effect(()=>{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 = nullfunction 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 = 0effect(()=>{ sum = obj.num1 + obj.num2 + obj.son.num3})console.log(sum) //50obj.num1 = 100console.log(sum) //130 可知sum为响应式
参考:
Vue3响应式原理及实现
Vue3响应式原理+手写reactive
Vue3响应式原理与reactive、effect、computed实现
ES6 Reflect 与 Proxy