【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

为什么 Vue3.0 不再使用 defineProperty 实现数据监听?

  • 2019-11-29
  • 本文字数:4969 字

    阅读完需:约 16 分钟

为什么Vue3.0不再使用defineProperty实现数据监听?

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

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

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

  3. 对比Object.definePropertyProxy

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

在一些技术博客上,我看到过这样一种说法,认为 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正名:


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 进行操作,看看哪些行为会触发数组的 gettersetter 方法。


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



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


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


2. 数组的 push 方法



push 并未触发 settergetter方法,数组的下标可以看做是对象中的 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 的元素后,再去修改或获取它的值时,不会再触发 settergetter


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


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


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


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

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

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


所以,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 中, 下面是这部分源码的分析。


/* * 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 ,下面是核心代码。


/** * 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.fooproxy['foo']

  • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['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,但 gettersetter 基本不会再有针对性优化。


5. Proxy 兼容性差



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


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

总结

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

  2. Object.definePropertyProxy 本质差别是,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


本文转载自微信公众号:前端小苑


2019-11-29 13:444033

评论

发布
暂无评论
发现更多内容

采用Excel作为可视化设计器的开源规则引擎 NopRule

canonical

低代码 规则引擎 可视化开发 可逆计算 Nop平台

CnosDB 签约京清能源,助力分布式 光伏发电解决监测系统难题

CnosDB

开源 时序数据库 CnosDB 京清能源

ARTS 打卡第四周

请务必优秀

文盘Rust -- 给程序加个日志 | 京东云技术团队

京东科技开发者

京东云 企业号9月PK榜

第四周ARTS打卡

犇犇

ARTS 打卡计划

ARTS 打卡第 4 周

atom

macos轻量级思维导图软件 ClickCharts最新激活版

mac大玩家j

思维导图 Mac软件 思维导图软件

解锁社交媒体的未来:SocialFi 的承诺

区块链软件开发推广运营

交易所开发 数字藏品开发 合约交易所开发 NFT开发 区块链开发DAPP开发

蓝易云:提升网站性能:Nginx五种高效负载均衡策略详解!

百度搜索:蓝易云

nginx 云计算 Linux 运维 云服务器

mac电脑数据转换 EasyDataTransform激活最新版

胖墩儿不胖y

数据转换 Mac软件 数据处理软件

Spring 中 Bean 的生命周期

小万哥

Java spring 程序员 springboot SpringCloud

区块链项目:白皮书+PPT海报设计,热度视频/MG动画,出海包装/宣发,经济模型设计

区块链软件开发推广运营

数字藏品开发 dapp开发 区块链开发 链游开发 NFT开发

9月23-24日·上海线下·CSM认证周末班【提前报名特惠】“全球金牌课程”CST导师亲授

ShineScrum捷行

Rich Bowen: 无论你在创造什么,最终交付的是信任。

亚马逊云科技 (Amazon Web Services)

开源 亚马逊云科技

当红语言模型利器:深度解析向量数据库技术及其应用

Baihai IDP

人工智能 AI 向量数据库 白海科技 大语言模型

手机+卫星的科技狂想

脑极体

手机 卫星

张勇卸任阿里云董事长与CEO,该职务将由集团CEO吴泳铭兼任

B Impact

破局DevOps|8大北极星指标指引研发效能方向

laofo

DevOps 研发效能 研发度量 效能度量 研发效能度量

探索GreatADM:如何快速定义监控

GreatSQL

ARTS 打卡第 4 周

Geek_wu

ARTS 打卡计划 左耳朵耗子

数据库深分页介绍及优化方案 | 京东云技术团队

京东科技开发者

京东云 企业号9月PK榜

【Y 码力】WAL 与性能

YMatrix 超融合数据库

性能提升 WAL 超融合数据库 故障恢复 YMatrix

数据驱动决策:以经验与洞察引领工作智慧

叶小鍵

DPText-DETR: 基于动态点query的场景文本检测,更高更快更鲁棒 | 京东探索研究院

京东科技开发者

京东云 企业号9月PK榜

持续部署:提高敏捷加速软件交付(内含教程)

SEAL安全

ci 持续部署 CD 软件交付 企业号9月PK榜

谷沁清益生菌清口含片,守护口腔健康的第一道防线

联营汇聚

ARTS 打卡第 4 周

AI帅辉

ARTS 打卡计划 学习分享

元宇宙优势与劣势、机遇与挑战分析-元宇宙解决方案

3DCAT实时渲染

元宇宙解决方案

蓝易云:Tomcat 部署及优化详细教程!

百度搜索:蓝易云

Java tomcat Linux Web

为什么Vue3.0不再使用defineProperty实现数据监听?_大前端_我叫于是乎_InfoQ精选文章