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