欢迎围观Vue3中Proxy吊打Object.defineProperty()的全过程

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

刚好昨天快下班的时候浅浅了解了一下vue3的新特性。

其中比较经典的一点就是,vue2中的数据劫持是用 Object.defineProperty 做的,而在vue3中,尤大摒弃了 Object.defineProperty 而改用proxy来做数据劫持。

说实话在接触到vue3之前我连vue2的数据劫持都不太了解,所以今天就来个double kill吧!

image.png

深入响应式原理

在了解一件事物之前,决定怎么了解这件事物的方式很重要。

我们知道Object.defineProperty()是被vue2用来做数据劫持的,那么就涉及到getter和setter,所以我的思路是从vue2的官方文档下手,在深入响应式原理这一节里进行了详细地介绍。

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter

简单来说,Object.defineProperty()在这里就是用来监视数据的访问和变化并拦截下来。只有在Vue把此对象的所有property转为getter/setter时,你才有可能获得响应式数据。

然后你会问,为什么我说的是“才有可能获得响应式数据”?

因为在这里我们需要特别注意的一点是,由于 JavaScript 的限制,Vue 不能检测 数组和对象的变化。

对于对象

官方文档中给出的栗子浅显易懂,我不做多余的赘述

var vm = new Vue({
  data:{
    a:1
  }
})

// `vm.a` 是响应式的

vm.b = 2
// `vm.b` 是非响应式的

第一点,文档中提到

对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。

怎么理解这句话呢?

其实很简单,它不允许我们再添加像data这样的根级别响应式property,只能添加像 a:1 层级的property。

但有意思的是,我们可以巧妙地通过间接的方式,看似添加二层级的property实则也添加了根部的一个property。

Vue.set(vm.someObject, 'b', 2)

这里还需要注意的一点是,当我们为已有的对象添加新的property的时候,是不会触发更新的。

比如这样

Object.assign(this.someObject, { a: 1, b: 2 })//触发更新失败

image.png

在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象才能触发更新,像这样重新赋值

this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

image.png

对于数组

Vue不能检测一下数组的变动

  1. 利用索引直接设置一个数组项
  2. 修改数组长度

来个一目了然的烤栗子(≧∇≦)ノ

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

在这里我多说一句,其实官网文档真的写的很好,本文一方面是记录自己对这块儿知识的理解,另一方面是想尽量用更简介的描述来输出给其他可能没有耐心阅读官方文档的小伙伴们。

但我仍然对原汁原味的官方文档抱有虔诚的态度,还是希望大家能够去阅读它,也欢迎大家对本文指出不足和勘误。

image.png

Object.defineProperty()

代理与反射

vue2中的Object.defineProperty()和vue3中的proxy本质上起到的作用其实都是代理。

那么我们就先来捋一捋什么是反射和代理?

反射和代理就是一种拦截并向基本操作嵌入额外行为的能力。

这也是数据劫持的本体行为。

代理是目标对象的抽象。也就是说,它可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接地被操作,也可以通过代理来操作。

看个简单的例子来理解代理基操

const target = {
    id:'target'
};//目标对象
const handler = {};//代理对象
const proxy = new Proxy(target,handler);

console.log(target.id)//target
console.log(proxy.id)//target

显而易见,通过Proxy代理把目标对象上的属性映射到了代理对象身上。

Object.defineProperty()与Proxy的区别

Object.defineProperty()

defineProperty()捕获器会在Object.defineProperty()中被调用

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

defineProperty()捕获器处理程序参数

  1. obj:要在其上定义属性的对象
  2. prop:要定义或修改的属性的名称或Symbol
  3. descriptor:定义或修改的属性的描述符
Object.defineProperty(obj, prop, descriptor)

Proxy

Proxy 主要用于改变对象的默认访问行为,实际上是在访问对象之前增加一层拦截,在任何对对象的访问行为都会通过这层拦截

  1. target: 目标对象
  2. handler: 配置对象,用来定义拦截的行为
  3. proxy: Proxy构造器的实例

体现出来的功能有以下几点:

  • 拦截功能
  • 提供对象访问
  • 可以重写属性或者构造函数(关键是这一点!敲黑板!!)

区别

  1. Proxy 是对整个对象的代理,而 Object.defineProperty 只能代理某个属性
//Proxy
var target = {
    a:1,
    b:{
        c:2,
        d:{e:3}
    }
};
var handler = {
    //捕获器
    get:function(trapTarget,prop,receiver){
        console.log('触发get:',prop)
        return Reflect.get(trapTarget,prop) // 反射API // 只要在代理上调用,所有捕获器都会拦截它们对应的反射API操作
    },
    set:function(trapTarget,key,value,receiver){
        console.log('触发set:',key,value)
        return Reflect.set(trapTarget,key,value,receiver)
    }
};
const proxy = new Proxy(target,handler);
// 访问
proxy.b.c;// 触发get: b
proxy.b.d.e;// 触发get: b //说明都不能够遍历到深层次的地方,只能代理最外层属性
console.log(proxy);//{ a: 1, b: { c: 2, d: { e: 3 } } }

// Object.defineProperty
const obj = {}
Object.defineProperty(obj,'name',{
    value:'张三'
})
console.log(obj.name) // '张三'
obj.name = '李四' // 给obj.name赋新值
console.log(obj.name) // 张三  //默认writable为false,即不可改
  1. 对象上新增属性和数组新增修改,Proxy可以监听到,Object.defineProperty不能(Vue2中)
  2. 若对象内部属性要全部递归代理,Proxy可以只在调用时递归,而Object.defineProperty需要一次性完成所有递归,性能比Proxy差

假如对象嵌套的层级比较深的话,每一次都需要循环遍历(采用递归代理)

  1. Proxy不兼容IE,Object.defineProperty不兼容IE8及以下
  2. 如果Object.defineProperty遍历到对象不存在的属性的时候,它是检测不到变化的

image.png

Vue2和Vue3代理基础架构对比

Vue2中的defineProperty基础架构

假设我们定义了一个defineReactive函数来实现代理映射的效果,里面包含了get和set方法

如果触发了get方法,那么直接映射源数据value

如果触发了set方法,那么先判断新的数据是否等于原来的数据,这样做是为了避免无效更新视图层,减少性能损耗

如果不等于源数据,那么就将newValue重新赋值给value

然后再更新视图层,这样就实现了最基本的响应式数据

const dinner = {
    meal:'tacos'
}

function defineReactive(target,key,value) {
    Object.defineProperty(target,key,{
        get(){
            return value
        },
        set(newValue){
            if(newValue !==value){
                value = newValue
                //更新视图层
            }
        }
    })
}

for (let key in dinner){
    defineReactive(dinner,key,dinner[key])
}

console.log('set之前',dinner.meal) //set之前 tacos

dinner.meal = 'changed'

console.log('set之后',dinner.meal) //set之后 changed

Vue3中的Proxy基础架构

由于Proxy的基本用法我们在上面已经详细介绍过了,这里的例子大家应该很好理解

const dinner = {
    meal:'tacos'
}

const handler = {
    //这里的key指的是访问的property
    get(target,key){
        return target[key]
    },
    set(target,key,value){
        target[key] = value
    }
}

const proxy = new Proxy(dinner,handler)

console.log('set之前',proxy.meal)//set之前 tacos

proxy.meal = 'changed'

console.log('set之后',proxy.meal)//set之后 changed

可以看出都能实现数据响应式变化,但是在这里,我们考虑到,如果遍历的是数组或者多层嵌套的话,更改一下defineProperty中的例子

把原对象变为

const dinner = {
    meal:'tacos',
    a:{
        b:[1,2,3],
        c:{
            d:'',
            e:''
        }
    }
}

那么在层级比较深并且包含数组的情况下,又该如何实现响应式呢?

此时我们需要定义一个observer来观测value的类型,再决定遍历的方式和次数

function observer(target){
    if(typeof target !== 'object'||target == null){
        return target
    }
    if(Array.isArray(target)){
        //拦截数组,给数组的方法进行了重写
        Object.setPrototypeOf(target,proto);
        //target.__proto__ = proto
        for (let i =0;i<target.length;i++){}
        observer(target[i]);
    }else{
        //是对象的话,就进行层层递归
        for (let key in target){
            defineReactive(target,key,target[key])
        }
    }
}
function defineReactive(target,key,value) {
    //递归遍历,继续拦截对象
    observer(value);
    Object.defineProperty((target,key,{
        get() {
            return value;
        },
        set(newValue) {
            if (newValue!==value){
                observer(newValue)
                // updateView 更新视图的方法
                value = newValue
            }
        }
    }))
}

这里其实已经能看出defineProperty的缺点了

在重写的defineReactive方法里,显而易见的性能损耗基本上都在observer上

而在Vue3中的Proxy可以很好地解决上面的问题

我是一直保持helloworld初心并且奔跑在前端路上的fighter~ 输出不易,欢迎关注点赞~

最后,希望我们都能在前端的路上越走越远啦~(\ \ ▽ \ \)

image.png

猜你喜欢

转载自juejin.im/post/7124973052659499038