乐趣区

关于vue.js:Vue3响应式原理

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

退出移动版