Vue3.0初探:Proxy VS defineProperty

原文地址:https://juejin.im/post/6885915715719823374

前言

2019.10.5日发布了Vue3.0,到了2020年4月21日晚,Vue作者尤雨溪在B站直播分享了Vue.js 3.0 Beta最新进展,估计Vue3.0正式版也快出来了。

Vue3.0 为了达到更快、更小、更易于维护、更贴近原生、对开发者更友好的目的,在很多方面进行了重构:

  1. 使用 Typescript
  2. 放弃 class 采用 function-based API
  3. 重构 complier
  4. 重构 virtual DOM
  5. 新的响应式机制

这次的分享就聊聊新的响应式机制,进入正文~

回顾Vue2.x的响应式机制
实现原理

相信用过Vue的基本上都知道Vue的响应式都是利用了Object.defineProperty()。MDN上的解释是:Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty()把这些属性全部转为getter/setter,在getter中做数据依赖收集处理,在setter中 监听数据的变化,并通知订阅当前数据的地方。

img

部分源码 src/core/observer/index.js#L156-L193, 版本为 2.6.11 如下:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive (  //defineReactive 的功能就是定义一个响应式对象,给对象动态添加 getter 和 sette
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)          //对象属性的定义
  if (property && property.configurable === false) {               // false就什么都不做
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {     //  walk的时候   对key求值赋给val
    val = obj[key]
  }
let childOb = !shallow && observe(val)
 // 对 data中的数据进行深度遍历,给对象的每个属性添加响应式
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {//访问的时候触发    并依赖收集
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
         // 进行依赖收集
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // 是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {//  修改触发   并派发更新
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 新的值需要重新进行observe,保证数据响应式 
      //Observer 是一个类,它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新,这里就不看它的源码了
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 gettersetter。最后利用 Object.defineProperty去给 obj的属性 key添加gettersetter

存在的问题
  • 检测不到对象属性的添加和删除:当你在对象上新加了一个属性newProperty,当前新加的这个属性并没有加入vue检测数据更新的机制(因为是在初始化之后添加的)。vue.$set是能让vue知道你添加了属性, 它会给你做处理,$set内部也是通过调用Object.defineProperty()去处理的

  • 针对数组只实现了 push,pop,shift,unshift,splice,sort,reverse 这七个方法的监听,对于item[indexOfItem] = newValue这种是无法检测的。通过数组下标改变值的时候,是不能触发视图更新的。(并不是说Object.defineProperty 不能监听数组下标的改变,举个例子)

    const arrData = [1,2,3,4,5];
    arrData.forEach((val,index)=>{
        Object.defineProperty(arrData,index,{
            set(newVal){
                console.log(`defineProperty set key: ${index} value: ${newVal}`)
            },
            get(){
                console.log(`defineProperty get key: ${index} value: ${val}`)
                return val;
            }
        })
    })
    //通过下标获取某个元素和修改某个元素的值
    //let index = arrData[1];
    //arrData[0] = "后";
    //数组的push
    //arrData.push(8);
    //数组的unshift
     arrData.unshift(0);
    
  • 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择。

初探Vue3.0的响应式机制
Proxy是什么?

什么是代理呢?Proxy是 ES6 中新增的一个特性。MDN上的解释是:Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

使用 Proxy 的核心优点是可以交由它来处理一些非核心逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)。 从而可以让对象只需关注于核心逻辑,达到关注点分离,降低对象复杂度等目的。

Proxy用法?
const p = new Proxy(target, handler);
//target:所要拦截的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)
//handler:一个对象,定义要拦截的行为
//p 是代理后的对象。当外界每次对 p 进行操作时,就会执行 handler 对象上的一些方法。

可以理解为在对象之前设置一个“拦截”,当该对象被访问的时候,都必须经过这层拦截。意味着你可以在这层拦截中进行各种操作。Proxy支持的拦截操作一共 13 种。比如你可以在这层拦截中对原对象进行处理,返回你想返回的数据结构。举个例子:

// 声明要响应式的对象,Proxy会自动代理
 const data = {
   name: "banggan",
   age: 26,
   info: {
     address: "北京" // 需要深度监听
   },
   nums: [10, 20, 30]
 }; 
const proxyData = new Proxy(data, {
   get(target,key,receive){ 
     // 只处理本身(非原型)的属性
     const ownKeys = Reflect.ownKeys(target)
     if(ownKeys.includes(key)){
       console.log('get',key) // 监听
     }
     const result = Reflect.get(target,key,receive)
     return result
   },
   set(target, key, val, reveive){
     // 重复的数据,不处理
     const oldVal = target[key]
     if(val == oldVal){
       return true
     }
     const result = Reflect.set(target, key, val,reveive)
     console.log('set', key, val)
     return result
   },
   deleteProperty(target, key){
     const result = Reflect.deleteProperty(target,key)
     console.log('delete property', key)
     console.log('result',result)
     return result
   }
 })
proxyData.name;
proxyData.age = '20';
proxyData.newPropKey = '新属性';
proxyData.info.tel = '88888888';
delete proxyData.name

上面代码可以看到,新增的属性,并不需要重新添加响应式处理,因为 Proxy 是对对象的操作,只要你访问对象,就会走到 Proxy 的逻辑中。

Reflect 是一个内置对象,它提供拦截 JavaScript 操作的方法,可简化的创建 Proxy。它提供了一组操作与修改对象的 API,以便在 Proxy 对目标进行操作。

Reflectproxy关系就很明了了,Proxy 提供拦截操作,Reflect 提供修改操作.

既然Prox可以代理所有对象,那ES6 的Map、Set、WeakSet、WeakMap呢?尝试一下:

let map = new Map([['company','58']])
let mapProxy = new Proxy(map, {
  get(target, key, receiver) {
    var value = Reflect.get(...arguments)
     console.log("取值:",...arguments)
    return typeof value == 'function' ? value.bind(target) : value
  }
})
mapProxy.get("company")
Proxy在Vue3.0的运用

Vue3.0 使用 Proxy 作为响应式数据实现的核心,用 Proxy 返回一个代理对象,通过代理对象来收集依赖和触发更新。

Reactive

createReactiveObject用于创建响应式代理对象:

  • 首先判断target是否是对象类型,如果不是对象,直接返回;
  • 然后判断目标对象是否已经是可观察的,如果是,直接返回已创建的响应式Proxy,toProxy就是rawToReactive这个WeakMap,用于映射响应式Proxy;
  • 然后判断目标对象是否已经是响应式Proxy,如果是,直接返回响应式Proxy,toRaw就是reactiveToRaw这个WeakMap,用于映射原始对象;
  • 然后创建响应式代理,对于SetMapWeakMapWeakSet的响应式对象handler与ObjectArray的响应式对象handler不同,需要分开处理;
  • 创建完立即更新rawToReactivereactiveToRaw映射;
ref

ref的作用是提供响应式包装对象, 为简单类型的值生成一个形为 { value: T } 的包装,这样在修改的时候就可以通过 count.value = 3 去触发响应式的更新了。

ref的底层就是reactiveref对象具有对应的 getter 和 setter ,getter总是返回经过convert转化后的响应式对象raw,并触发 Vue 的依赖收集,对ref对象赋值会调用settersetter调用会通知deps,通知依赖这一状态的对象更新,并重新更新rawraw被保存为新的响应式包装对象。

effect

Effect其核心在于响应式追踪变化,在创建响应式对象时,立即触发其getter一次,会使用track收集到其依赖,在响应式对象变更时,立即触发trigger,更新该响应式对象的依赖。

track用于收集依赖deps(依赖一般收集effect/computed/watch的回调函数):

  • track时,effectStack栈顶就是当前的effect,因为在调用原始监听函数前,执行了effectStack.push(effect),在调用完成最后,会执行effectStack.pop()出栈;
  • effect.activefalse时会导致effectStack.length === 0,这时不用收集依赖,在track函数调用开始时就做了此判断;

trigger用于通知deps,通知依赖这一状态的对象更新:

  • trigger内部会维护两个队列effectscomputedRunners,分别是普通属性和计算属性的依赖更新队列;
  • trigger调用时,Vue 会找到更新属性对应的依赖,然后将需要更新的effect放到执行队列里面,在完成了依赖查找之后,对effectscomputedRunners进行遍历,调用scheduleRun进行更新;

img

  • 初始化阶段

origin(array) 对象通过reactive.ts转化成响应式的 Proxy 对象 state

把函数 fn() 作为一个响应式的effect函数并立即执行一次。**由于在 fn() 里面有引用到 Proxy 对象的属性,所以这一步会触发对象的 getter,从而启动依赖收集。**这个effect函数也会被压入一个名effectStack的栈中,供后续依赖收集的时候使用。

  • 依赖收集阶段:

当上面的effect被立即执行,其内部的 fn() 触发了 Proxy 对象的 getter 的时候,启动依赖收集。创建targetMap依赖收集表。

targetMap 是一个 WeakMap,其 key 值是~~当前的 Proxy 对象 state,而 value 则是该对象所对应的 depsMap。

depsMap 是一个 Map,key 值为触发 getter 时的属性值(此处为 count),而 value 则是触发过该属性值所对应的各个 effect。

这样,{ target -> key -> dep } 的对应关系就建立起来了,依赖收集也就完成了。

  • 响应阶段

当修改对象的某个属性值的时候,会触发对应的 setter。

setter 里面的 trigger() 函数会从依赖收集表里找到当前属性对应的各个 dep,然后把它们推入到 effectscomputedEffects(计算属性)队列中,最后通过 scheduleRun()挨个执行里面的 effect。

总结
  • Proxy可以直接监听对象而非属性:Proxy直接可以劫持整个对象,并返回一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改。不管是操作便利程度还是底层功能上都远强于Object.defineProperty
  • Proxy可以直接监听数组变(push、shift、splice)。
  • Proxy可以监听set、map、weakSet、weakMap。
  • Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是Object.defineProperty不具备的。
  • Proxy的劣势就是兼容性问题,而且无法用polyfill磨平。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值