前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >【深入vue】为什么Vue3.0不再使用defineProperty实现数据监听?(修订版)

【深入vue】为什么Vue3.0不再使用defineProperty实现数据监听?(修订版)

作者头像
桃翁
发布于 2020-02-26 02:16:13
发布于 2020-02-26 02:16:13
2.5K02
代码可运行
举报
文章被收录于专栏:前端桃园前端桃园
运行总次数:2
代码可运行

导读

vue3.0中,响应式数据部分弃用了Object.defineProperty,使用Proxy来代替它。本文将主要通过以下方面来分析为什么vue选择弃用Object.defineProperty。

1. Object.defineProperty真的无法监测数组下标的变化吗?

2. 分析vue2.x中对数组Observe部分源码

3. 对比Object.defineProperty和Proxy

一 无法监控到数组下标的变化?

在一些技术博客上看到过这样一种说法,认为 Object.defineProperty 有一个缺陷是无法监听数组变化:

无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。所以vue才设置了7个变异数组(push、pop、shift、unshift、splice、sort、reverse)的 hack 方法来解决问题。

Object.defineProperty的第一个缺陷,无法监听数组变化。 然而Vue的文档提到了Vue是可以检测到数组变化的,但是只有以下八种方法,vm.items[indexOfItem] = newValue这种是无法检测的。

这种说法是有问题的,事实上,Object.defineProperty 本身是可以监控到数组下标的变化的,只是在 Vue 的实现中,从性能/体验的性价比考虑,放弃了这个特性。

下面我们通过一个例子来为 Object.defineProperty 正名:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
function defineReactive(data, key, value) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
     get: function defineGet() {
      console.log(`get key: ${key} value: ${value}`)
      return value
    },
     set: function defineSet(newVal) {
      console.log(`set key: ${key} value: ${newVal}`)
      value = newVal
    }
  })
}

function observe(data) {
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key])
  })
}

let arr = [1, 2, 3]
observe(arr)

上面代码对数组arr的每个属性通过 Object.defineProperty 进行劫持,下面我们对数组arr进行操作,看看哪些行为会触发数组的 getter 和 setter 方法。

1. 通过下标获取某个元素和修改某个元素的值

可以看到,通过下标获取某个元素会触发 getter 方法, 设置某个值会触发 setter 方法。

接下来,我们再试一下数组的一些操作方法,看看是否会触发。

2. 数组的 push 方法

push 并未触发 setter 和 getter 方法,数组的下标可以看做是对象中的 key ,这里push 之后相当于增加了下索引为3的元素,但是并未对新的下标进行 observe ,所以不会触发。

3.数组的 unshift 方法

我擦,发生了什么?

unshift 操作会导致原来索引为0,1,2,3的值发生变化,这就需要将原来索引为0,1,2,3的值取出来,然后重新赋值,所以取值的过程触发了 getter ,赋值时触发了 setter 。

下面我们尝试通过索引获取一下对应的元素:

只有索引为0,1,2的属性才会触发 getter 。

这里我们可以对比对象来看,arr数组初始值为[1, 2, 3],即只对索引为0,1,2执行了 observe 方法,所以无论后来数组的长度发生怎样的变化,依然只有索引为0,1,2的元素发生变化才会触发,其他的新增索引,就相当于对象中新增的属性,需要再手动 observe 才可以。

4. 数组的 pop 方法

当移除的元素为引用为2的元素时,会触发 getter 。

删除了索引为2的元素后,再去修改或获取它的值时,不会再触发 setter 和 getter 。

这和对象的处理是同样的,数组的索引被删除后,就相当于对象的属性被删除一样,不会再去触发 observe。

到这里,我们可以简单的总结一下结论。

Object.defineProperty 在数组中的表现和在对象中的表现是一致的,数组的索引就可以看做是对象中的 key。

1. 通过索引访问或设置对应元素的值时,可以触发 getter 和 setter 方法

2. 通过 push 或 unshift 会增加索引,对于新增加的属性,需要再手动初始化才能被observe。

3. 通过 pop 或 shift 删除元素,会删除并更新索引,也会触发 setter 和 getter 方法。

所以,Object.defineProperty 是有监控数组下标变化的能力的,只是vue2.x放弃了这个特性。

二 vue对数组的observe做了哪些处理?

vue的 Observer 类定义在 core/observer/index.js 中

可以看到,vue的 Observer 对数组做了单独的处理。

hasProto 是判断数组的实例是否有 __proto__ 属性,如果有 __proto__ 属性就会执行 protoAugment 方法,将 arrayMethods 重写到原型上。 hasProto 定义如下。

arrayMethods 是对数组的方法进行重写,定义在 core/observer/array.js 中, 下面是这部分源码的分析。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from '../util/index'

// 复制数组构造函数的原型,Array.prototype也是一个数组。
const arrayProto = Array.prototype
// 创建对象,对象的__proto__指向arrayProto,所以arrayMethods的__proto__包含数组的所有方法。
export const arrayMethods = Object.create(arrayProto)

// 下面的数组是要进行重写的方法
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
// 遍历methodsToPatch数组,对其中的方法进行重写
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  // def方法定义在lang.js文件中,是通过object.defineProperty对属性进行重新定义。
  // 即在arrayMethods中找到我们要重写的方法,对其进行重新定义
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      // 上面已经分析过,对于push,unshift会新增索引,所以需要手动observe
      case 'push':
      case 'unshift':
        inserted = args
        break
      // splice方法,如果传入了第三个参数,也会有新增索引,所以也需要手动observe
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // push,unshift,splice三个方法触发后,在这里手动observe,其他方法的变更会在当前的索引上进行更新,所以不需要再执行ob.observeArray
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

三 Object.defineProperty VS Proxy

上面已经知道 Object.defineProperty 对数组和对象的表现是一致的,那么它和 Proxy 对比存在哪些优缺点呢?

1. Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。

由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作

2. Object.defineProperty对新增属性需要手动进行Observe。

由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用 Object.defineProperty 进行劫持。

也正是因为这个原因,使用vue给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。

下面看一下vue的 set 方法是如何实现的,set方法定义在 core/observer/index.js ,下面是核心代码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
export function set (target: Array<any> | Object, key: any, val: any): any {
  // 如果target是数组,且key是有效的数组索引,会调用数组的splice方法,
  // 我们上面说过,数组的splice方法会被重写,重写的方法中会手动Observe
  // 所以vue的set方法,对于数组,就是直接调用重写splice方法
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // 对于对象,如果key本来就是对象中的属性,直接修改值就可以触发更新
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // vue的响应式对象中都会添加了__ob__属性,所以可以根据是否有__ob__属性判断是否为响应式对象
  const ob = (target: any).__ob__
  // 如果不是响应式对象,直接赋值
  if (!ob) {
    target[key] = val
    return val
  }
  // 调用defineReactive给数据添加了 getter 和 setter,
  // 所以vue的set方法,对于响应式的对象,就会调用defineReactive重新定义响应式对象,defineReactive 函数
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

在 set 方法中,对 target 是数组和对象做了分别的处理,target 是数组时,会调用重写过的 splice 方法进行手动 Observe 。

对于对象,如果 key 本来就是对象的属性,则直接修改值触发更新,否则调用 defineReactive 方法重新定义响应式对象。

如果采用 proxy 实现,Proxy 通过 set(target, propKey, value, receiver) 拦截对象属性的设置,是可以拦截到对象的新增属性的。

不止如此,Proxy 对数组的方法也可以监测到,不需要像上面vue2.x源码中那样进行 hack。

完美!!!

3. Proxy支持13种拦截操作,这是defineProperty所不具有的

  • get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。
  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
  • has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
  • deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
  • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。

4. 新标准性能红利

Proxy 作为新标准,长远来看,JS引擎会继续优化 Proxy,但 getter 和 setter 基本不会再有针对性优化。

5. Proxy兼容性差

可以看到,Proxy对于IE浏览器来说简直是灾难。

并且目前并没有一个完整支持 Proxy 所有拦截方法的Polyfill方案,有一个google编写的 proxy-polyfill (https://github.com/GoogleChrome/proxy-polyfill)也只支持了 get,set,apply,construct 四种拦截,可以支持到IE9+和Safari 6+。

四 总结

1. Object.defineProperty 对数组和对象的表现一直,并非不能监控数组下标的变化,vue2.x中无法通过数组索引来实现响应式数据的自动更新是vue本身的设计导致的,不是 defineProperty 的锅。

2. Object.defineProperty 和 Proxy 本质差别是,defineProperty 只能对属性进行劫持,所以出现了需要递归遍历,新增属性需要手动 Observe 的问题。

3. Proxy 作为新标准,浏览器厂商势必会对其进行持续优化,但它的兼容性也是块硬伤,并且目前还没有完整的polyfill方案。

参考

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

https://segmentfault.com/a/1190000015783546

https://zhuanlan.zhihu.com/p/35080324

http://es6.ruanyifeng.com/#docs/proxy

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-02-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端桃园 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
为什么采用Proxy重构响应系统 | Vue3源码系列
修改的是程序默认形为,形同于在编程语言层面上做修改,属于元编程(meta programming)
陈大鱼头
2020/09/30
1K0
Vue2剥丝抽茧-响应式系统之数组
接 Vue2剥丝抽茧-响应式系统、Vue2剥丝抽茧-响应式系统之分支切换,响应式系统之嵌套 、响应式系统之深度响应 还没有看过的同学需要看一下。
windliang
2022/08/20
1740
Vue2剥丝抽茧-响应式系统之数组
Vue0.11版本源码阅读系列二:数据观察
上篇介绍了创建vue实例时大概做了一些什么事情,其中有一项是初始化数据,本篇来看一下数据观察具体是怎么做的。
街角小林
2022/06/15
3480
Vue2剥丝抽茧-响应式系统之数组2
接 Vue2剥丝抽茧-响应式系统、Vue2剥丝抽茧-响应式系统之分支切换,响应式系统之嵌套 、响应式系统之深度响应 、Vue2剥丝抽茧-响应式系统之数组 还没有看过的同学需要看一下。
windliang
2022/08/20
2440
Vue2剥丝抽茧-响应式系统之数组2
浅谈 JavaScript 数据双向绑定[Proxy/defineProperty]
从 JavaScript 的数据双向绑定(defineProperty、Proxy)开始,谈谈 Vue2 中的数组监听问题。
老猫-Leo
2023/12/11
4330
浅谈 JavaScript 数据双向绑定[Proxy/defineProperty]
Vue是怎样监听数组的变化的?
其实我们并不是要你把答案都记下来,而是把其中的思想学习到。就像你接触一个新的领域react,你也一样可以把基本思想提炼出来。
bb_xiaxia1998
2022/10/05
5150
Vue是怎样监听数组的变化的?
其实我们并不是要你把答案都记下来,而是把其中的思想学习到。就像你接触一个新的领域react,你也一样可以把基本思想提炼出来。
bb_xiaxia1998
2022/10/17
4490
vue源码分析-响应式系统(三)
在之前介绍数据代理章节,我们已经详细介绍过Vue数据代理的技术是利用了Object.defineProperty,Object.defineProperty让我们可以方便的利用存取描述符中的getter/setter来进行数据的监听,在get,set钩子中分别做不同的操作,达到数据拦截的目的。然而Object.defineProperty的get,set方法只能检测到对象属性的变化,对于数组的变化(例如插入删除数组元素等操作),Object.defineProperty却无法达到目的,这也是利用Object.defineProperty进行数据监控的缺陷,虽然es6中的proxy可以完美解决这一问题,但毕竟有兼容性问题,所以我们还需要研究Vue在Object.defineProperty的基础上如何对数组进行监听检测。
yyzzabc123
2022/10/17
4170
Vue中数组变动监听
Vue的通过数据劫持的方式实现数据的双向绑定,即使用Object.defineProperty()来实现对属性的劫持,但是Object.defineProperty()中的setter是无法直接实现数组中值的改变的劫持行为的,想要实现对于数组下标直接访问的劫持需要使用索引对每一个值进行劫持,但是在Vue中考虑性能问题并未采用这种方式,所以需要特殊处理数组的变动。
WindRunnerMax
2020/09/16
6050
深入浅出Vue响应式原理
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。----官方文档 本文将针对响应式原理做一个详细介绍,并且带你实现一个基础版的响应式系统。本文的代码请猛戳Github博客
浪里行舟
2019/07/11
9710
深入浅出Vue响应式原理
每日一题之Vue数据劫持原理是什么?
定义: 数据劫持,指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。
bb_xiaxia1998
2022/10/06
5210
vue实战-深入响应式数据原理
在我们执行new Vue创建实例时,会调用如下构造函数,在该函数内部调用this._init(options)。
yyds2026
2022/10/18
5080
Vue 核心之数据劫持
Angular、Regular、Vue、React等等可以实现数据绑定,再也不需要手动进行DOM操作了,它们实现的原理也基本上是脏检查或数据劫持。 本文就以Vue框架出发,学习Object.defineProperty来实现数据劫持。
前端小tips
2021/11/25
3550
Vue 核心之数据劫持
VUE源码解读之响应式系统及Watcher的调度实现
鉴于目前 vue3 还没有正式发布,而且 vue2 里面的一些实现思想还是很有参考价值的,于是这篇原理性讲解还是 vue2 的,希望对你有启发~
winty
2020/04/01
9570
34道Vue面试题系列:Vue中如何检测数组变化?
本次解析本套高级前端的Vue面试题的第三问,Vue中是如何检测数组变化的,如果对这一问也有所不熟悉的,请一起学习吧。
用户1462769
2020/03/31
2.8K0
34道Vue面试题系列:Vue中如何检测数组变化?
12道vue高频原理面试题,你能答出几道?
Observer : 它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新
null仔
2020/03/02
1.5K0
手摸手带你理解Vue响应式原理
响应式原理作为 Vue 的核心,使用数据劫持实现数据驱动视图。在面试中是经常考查的知识点,也是面试加分项。
WahFung
2020/08/24
9520
深入理解vue2.x中Object.defineproperty()和vue3.x中Proxy
vue2.x中数据的双向绑定主要通过Object.defineproperty()方法实现,data中的数据改变通过Object.defineProperty()对属性设置set属性,获取通过get属性,Object.defineProperty的作用就是劫持一个对象的属性,通常我们对属性的getter和setter方法进行劫持,在对象的属性发生变化时进行特定的操作。而vue3.x主要是通过proxy实现, proxy在目标对象的外层搭建一层拦截,外界对目标对象的某些操作,必须通过这层拦截。
can4hou6joeng4
2023/11/29
2260
2. 「vue@2.6.11 源码分析」数据驱动视图(响应式)
vue 最核心的卖点是数据驱动和组件。浏览器原生提供的交互是通过dom api来修改dom元素,由于浏览器兼容性问题后面的框架如jquery对原生的api进行了一层的封装以屏蔽浏览器的差异性,但并未作出实质的改变。想想这个过程,通常是数据发生变化,js根据变化的情况进行判断而后操作dom。dom变动的本质实际根本上实际是由数据驱动,我在第一家公司数字政通(egova)首次接触了的此类框架knockout。
tinyant
2023/02/24
5480
2. 「vue@2.6.11 源码分析」数据驱动视图(响应式)
面试官:Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?
定义:Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
@超人
2021/02/26
6510
面试官:Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?
相关推荐
为什么采用Proxy重构响应系统 | Vue3源码系列
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验