34道Vue面试题系列:Vue中如何检测数组变化?

发表于 3年以前  | 总阅读数:1219 次

前言

本次解析本套高级前端的Vue面试题的第三问,Vue中是如何检测数组变化的,如果对这一问也有所不熟悉的,请一起学习吧。


上一文中,我们提到了Vue2.0和3.0的响应式原理,但是没有深入细讲,在本文会进行深入的分析Vue在2.0版本和3.0版本里,分别是如何检测各种数据类型的值变化,从而做到页面响应式的,并且搞清楚为何数组类型的变化要特殊处理,最后也将Vue从2.x升级到3.x的过程中为何要采用了不同的数据监测原理的原因也一探究竟。

从一段基础代码入手

下面这段代码非常简单,编写过Vue的同学都能看懂它在干什么,但是你能准确的说出这段代码在第一秒,第二秒,第三秒页面上分别有什么变化吗?

<!DOCTYPE html>
<html>
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<body>
<div id="app">
  <div>{{ list }}</div>
</div>
<script>
new Vue({
  el: '#app',
  data: {
    list: [],
  },
  mounted() {
     setTimeout(()=>{
     this.list[0] = 3
   }, 1000)
     setTimeout(()=>{
         this.list.length = 5
     }, 2000)
     setTimeout(()=>{
         this.$set(this.list, this.list)
     }, 3000)
  }
})
</script>
</body>
</html>

大家最好能动手拷贝上面的代码,本地新建HTML文件保存后打开调试查看,我这里直接说一下结果。当执行这段代码后,页面在第一秒和第二秒无变化,直到第三秒时候才会发生变化,思考一下第一秒和第二秒改变了list的值,为什么Vue的双向绑定在这里失效了呢?围绕这个问题下面开始一步一步看看Vue的数据变化监听实现机制。

Vue2.0的数据变化监听

这里由浅入深的去看,先从要监听普通数据类型看起。

1、检测属性为基本数据类型

监听普通数据类型,即要监听的对象属性的值为非对象的五种基本类型变化,这里不直接看源码,每一步都自己手动的去实现,更加便于理解。

<!DOCTYPE html>
<html>
  <div>
    name: <input id="name" />
  </div>
</html>
<script>
// 监听Model下的name属性,当name属性有变化时要引起页面id=name的响应变化
const model = {
  name: 'vue',
};
// 利用Object.defineProperty创建一个监听器
function observe(obj) {
  let val = obj.name;
  Object.defineProperty(obj, 'name', {
    get() {
      return val;
    },
    set(newVal) {
      // 当有新值设置时,执行setter
      console.log(`name变化:从${val}到${newVal}`);
      // 解析到页面
      compile(newVal);
      val = newVal;
    }
  })
}
// 解析器,将变化的数据响应到页面上
function compile(val) {
  document.querySelector('#name').value = val;
}
// 调用监听器,对model开始监听
observe(model);
</script>

在控制台调试过程。

上面的代码在调试的时候,我先查看了model.name初始值后,进行了重新设置,可以引起setter函数的触发执行,从而页面达到响应式效果。

但是当给name属性赋值为对象类型后,再给新对象里插入key1一个属性后,接着改变这个key1的值,这时候页面并不能得到响应式触发。

所以上面的observe的实现中,当name是普通数据类型的时候监听没有问题,而要监听的内容是对象的变化里的时候,上面的写法就有问题了。

下面看看监听对象类型属性observe函数要怎么实现。

2、检测属性为对象类型

从上面的例子里,检测属性值为对象时,不能满足监听需求,接下来进一步改造observe监听函数,解决思路很简单,如果是对象,只需再一次将当前对象下的所有普通类型的监听变化即可,如果该对象下还有对象属性,继续监听就可以了,如果你对递归很熟,马上就知道该如何解决这个问题。

<!DOCTYPE html>
<html>
  <div>
    name: <input id="name" />
    val: <input id="val" />
    list: <input id="list" />
  </div>
</html>
<script>
// 监听Model下的name属性,当name属性有变化时要引起页面id=name的响应变化
const model = {
  name: 'vue',
  data: {
    val: 1
  },
  list: [1]
};
// 监听函数
function observe(obj) {
  // 遍历所有属性,各自监听
  Object.keys(obj).map(key => {
    // 将object属性特殊处理
    if (typeof obj[key] === 'object') {
      // 是对象属性的再次监听
      observe(obj[key]);
    } else {
      // 非对象属性的做监听
      defineReactive(obj, key, obj[key]);
    }
  })
}
// 利用Object.defineProperty做对象属性的做监听
function defineReactive(obj, key, val) {
  Object.defineProperty(obj, key, {
    get() {
      return val;
    },
    set(newVal) {
      // 当有新值设置时,执行setter
      console.log(`${key}变化:从${val}到${newVal}`);
      if (Array.isArray(obj)) {
        document.querySelector(`#list`).value = newVal;
      } else {
        document.querySelector(`#${key}`).value = newVal;
      }
      val = newVal;
      // 新增的属性再次进行监听
      observe(newVal);
    }
  })
}
// 监听model下的所有属性
observe(model);
</script>

在控制台调试过程。

在上面的实际操作中,我先改变了属性name的值,触发了setter,页面收到响应,再次改变了model.data这个对象下的val属性,页面也得到响应式变化,这说明我们在之前是想observe监听不到对象属性变化的问题在上面的改造下得到了解决。

接下来要注意,在最后我改变了数组属性list下的第一个下标里的值为5,页面也得到了监听结果,但是我改变了第二个下标后,没有触发setter,接着特意去改变list的length,或者push都没有触发数组的setter,页面没有变化响应。

这里抛出两个问题:

a、我修改了数组list的第二个下标的值,并且调用length、push改变数组list后页面也没有响应到变化,是怎么回事?

b、回到文章开始示例的那一段Vue代码里的实现,我改变了Vue的data下list的下标属性值,页面是没有响应变化的,但是这里我改了list的内的值从1到5,页面响应了,这又是怎么回事?

请带着a、b两个问题继续往下看。

3、检测属性为数组对象类型

这里分析一下a问题修改数组下标的值和调用length、push方法改变数组时不触发监听器的setter函数的原因。我之前看到很多文章写Object.defineProperty不能监听到数组内的值变化,真的是这样么?

请看下面的例子,这里不绑定页面,只观察Object.defineProperty监听的数组元素,是否能监听到变化。

从上面代码里,首先监听了model数组里所有的属性,然后通过各种数组的方法来修改当前数组,得出以下几个结论。

1、直接修改数组中已有的元素是可以被监听的。

2、数组的操作方法如果是操作已经存在的被监听的元素也是可以触发setter被监听的。

3、只有push、length、pop一些特殊的方法确实不能触发setter,这跟方法的内部实现与Object.defineProperty的setter钩子的触发实现有关系,是语言层面的原因。

4、改变超过数组长度的下标的值时,值变化是不能监听到的。这个其实很好理解,不存在的属性当然是不能监听到,因为绑定监听操作在之前已经执行过了,后添加的元素属性在绑定当时都还没有存在,当然没有办法提前去监听它了。

所以综上,Object.defineProperty不能监听到数组内的值变化的说法是错误的,同时也得出了a问题的答案,语言层面不支持用Object.defineProperty监听不存在的数组元素,并且通过一些能造成数组的方法造成数组改变也不能监听到。

4、探究Vue源码,看数组的监听如何实现

对于b问题,则需要去看看Vue的源码里,为何Object.defineProperty明明能监听到数组值的变化,而它却没有实现呢?

这里分享一下我看源码的技巧,如果直接打开github一行一行看看源码是很懵逼的,我这里是直接用Vue-cli在本地生成一个Vue项目,然后在安装的node_modules下的Vue包里进行断点查看的,大家可以尝试下。

测试代码很简单,如下;

import Vue from './node_modules/_vue@2.6.11@vue/dist/vue.runtime.common.dev'
// 实例化Vue,启动起来后直接
new Vue({
  data () {
    return {
      list: [1, 3]
    }
  },
})

解释一下这一块儿的源码,下面的hasProto的源码是看是否有原型存在,arrayMethods是被重写的数组方法,代码流程是如果有原型,直接修改原型上的push,pop,shift,unshift,splice, sort,reverse七个方法,如果没有原型的情况下,走copyAugment去新增这七个属性后赋值这七个方法,并没有监听。

/**
   * Observe a list of Array items.
   */
observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    // 监听数组元素
    observe(items[i])
  }
}

最后就是this.observeArray函数了,它的内部实现非常简单,它对数组元素进行了监听,什么意思呢,就是改变数组里的元素不能监听到,但是数组内的值是对象类型的,修改它依旧能得到监听响应,如改变list[0].val可以得到监听,但是改变list[0]不能,但是依旧没有对数组本身的变化进行监听。

再看看arrayMethods是如何重写数组的操作方法的。

// 记录原始Array未重写之前的API原型方法
const arrayProto = Array.prototype
// 拷贝一份上面的原型出来
const arrayMethods = Object.create(arrayProto)
// 将要重写的方法
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ]
/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  def(arrayMethods, method, function mutator (...args) {
    // 原有的数组方法调用执行
    const result = arrayProto[method].apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    // 如果是插入的数据,将其再次监听起来
    if (inserted) ob.observeArray(inserted)
    // 触发订阅,像页面更新响应就在这里触发
    ob.dep.notify()
    return result
  })
})

从上面的源码里可以完整的看到了Vue2.x中重写数组方法的思路,重写之后的数组会在每次在执行数组的原始方法之后手动触发响应页面的效果。

看完源码后,问题a也水落石出了,Vue2.x中并没有实现将已存在的数组元素做监听,而是去监听造成数组变化的方法,触发这个方法的同时去调用挂载好的响应页面方法,达到页面响应式的效果。

但是也请注意并非所有的数组方法都重新写了一遍,只有push,pop,shift,unshift,splice, sort,reverse这七个。至于为什么不用Object.defineProperty去监听数组中已存在的元素变化。

作者尤雨溪的考虑是因为性能原因,给每一个数组元素绑定上监听,实际消耗很大,而受益并不大。

issue地址:https://github.com/vuejs/vue/issues/8562。

Vue3.0的数据变化监听

前一篇说了Vue3.0的监听采用的是ES6新的构造方法Proxy来代理原对象做变化检测,(对于Proxy不熟的同学可以翻看上一篇内容)而Proxy作为代理的存在,当异步触发Model里的数据变化时,必须经过Proxy这一层,在这一层则可以监听数组以及各种数据类型的变化,看看下面的例子。

简直完美,无论是数组下标赋值引起变化还是数组方法引起变化,都可以被监听到,而且既可以避开监听数组每个属性下造成的性能问题,还可以解决像pop、push方法,length方法改变数组时监听不到数组变化的问题。

接下来使用Proxy和Reflect实现Vue3.0下的双向绑定。

<!DOCTYPE html>
<html>
  <div>
    name: <input id="name" />
    val: <input id="val" />
    list: <input id="list" />
  </div>
</html>
<script>
let model = {
  name: 'vue',
  data: {
    val: 1,
  },
  list: [1]
}
function isObj (obj) {
  return typeof obj === 'object';
}
// 监控器
function observe(data) {
  // 将属性都做监控
  Object.keys(data).map(key => {
    if (isObj(data[key])) {
      // 对象类型的继续监听它的属性
      data[key] = observe(data[key]);
    }
  })
  return defineProxy(data);
}
// 生成Proxy代理
function defineProxy(obj) {
  return new Proxy(obj, {
    set(obj, key, val) {
      console.log(`属性${key}变化为${val}`);
      compile(obj, key, val);
      return Reflect.set(...arguments);
    }
  })
}
// 解析器,响应页面变化
function compile(obj, id, val) {
  if (Array.isArray(obj)) { // 数组变化
    document.querySelector('#list').value = model.list;
  } else {
    document.querySelector(`#${id}`).value = val;
  }
}
model= observe(model);
</script>

利用Proxy和Reflect实现之后,不用在考虑数组的操作是否触发setter,只要操作经过proxy代理层,各种操作都会被被捕获到,达到页面响应式的要求。

总结

在Vue2.x中数组变化监听的问题,其实不是Object.definePropertype方法监听不到,而是为了性能和收益比例综合考虑之下,改变了监听方式,从原本的直接监听结果变化这种思路变换到监听会导致结果变化的方法上,也就上面所提到的对数组的重写。

而Vue3.0中利用Proxy的方式则完美解决了2.0中出现的问题,所以以后面试中如果遇到Vue中对于数组监听的处理的时候,一定要分清楚是哪一个版本,本文完。

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/y-Xc4tkMC0ppRevgutWi0A

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:7月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:7月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:7月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:7月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:7月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:7月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:7月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:7月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:7月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:7月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:7月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:7月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:7月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:7月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:7月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:7月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:7月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:7月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:7月以前  |  398次阅读  |  详细内容 »
 目录