写点什么

Vue 是怎样监听数组的变化的?

作者:bb_xiaxia1998
  • 2022-10-17
    浙江
  • 本文字数:2563 字

    阅读完需:约 1 分钟

上周五跟着一个师姐面试一个三年工作经验的前端开发,我在一边谨慎的观摩。想着曾经我也被别人面试过,如今面试别人,感觉其实心情是一样的。


前言


  • 工作三年的 Vue 使用者应该懂什么?

  • 为何工作几年的基础越来越弱?

  • 工作如何挤出时间学习?

一道面试题

其实我们并不是要你把答案都记下来,而是把其中的思想学习到。就像你接触一个新的领域 react,你也一样可以把基本思想提炼出来。


面试题: 👂Vue 是如何对数据进行监听的?


这其实是老生常谈的问题,但凡你有一点基础知识,你也能答出一二。师姐跟我说,其实问题不只是问题本身,而是跟这个知识顺带出来的体系。📝


01 对象数据是怎么被监听的


在 vue2.x 版本中,数据监听是用过 Object.defineProperty 这个 API 来实现的,我们可以来看一个例子🌰


var text = 'vue';const data = {};Object.defineProperty(data, 'text', {    get() {        return text;    },    set(newVal) {        text = newVal;    }});data.text // 'vue'data.text = 'react' // 'react'
复制代码


当我们访问或设置对象的属性的时候,都会触发相对应的函数,然后在这个函数里返回或设置属性的值。我们当然可以在触发函数的时候做我们自己想做的事情,这也就是“劫持”操作。


在 Vue 中其实就是通过Object.defineProperty来劫持对象属性的setter和getter操作,并创建一个监听器,当数据发生变化的时候发出通知。


var data = {    name:'hello',    age:18}
Object.keys(data).forEach(function(key){ Object.defineProperty(data,key,{ enumerable:true, // 是否能在for...in循环中遍历出来或在Object.keys中列举出来。 configurable:true, // false,不可修改、删除目标属性或修改属性性以下特性 get:function(){ console.log('获取数据'); }, set:function(){ console.log('监听到数据发生了变化'); } })});data.name //控制台会打印出 “获取数据”data.name = 'world' //控制台会打印出 "监听到数据发生了变化"
复制代码


02 数组数据是怎么被监听的


我们知道,上面是对对象的数据进行监听的,我们不能对数组进行数据的“劫持”。那么 Vue 是怎么做的呢?


import { def } from '../util/index'
const arrayProto = Array.prototypeexport const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']

methodsToPatch.forEach(function (method) { // 缓存原来的方法 const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { const result = original.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) // notify change ob.dep.notify() return result })})
复制代码


看来 Vue 能对数组进行监听的原因是,把数组的方法重写了。总结起来就是这几步:


01 先获取原生 Array 的原型方法,因为拦截后还是需要原生的方法帮我们实现数组的变化。


02 对 Array 的原型方法使用 Object.defineProperty 做一些拦截操作。


03 把需要被拦截的 Array 类型的数据原型指向改造后原型。


参考:前端vue面试题详细解答

Vue 为什么不能检测数组变动

并不是说 JS 不能支持响应式数组,其实 JS 是没有这种限制的。


数组在 JS 中常被当作栈,队列,集合等数据结构的实现方式,会有批量的数据以待遍历。并且 runtime 对对象与数组的优化也有所不同。


所以对数组的处理需要特化出来以提高性能。


Vue 中是通过对每个键设置 getter/setter 来实现响应式的,开发者使用数组,目的往往是遍历,此时调用 getter 开销太大了,所以 Vue 不在数组每个键上设置,而是在数组上定义 __ob__ ,并且替换了 push 等等能够影响原数组的原型方法。


为此也有人去 GitHub 问了尤大,他的回答也是说因为性能问题而没有采用这种方式监听数组。github.com/vuejs/vue/i…


源码位置:src/core/observer/index.js


constructor (value: any) {    this.value = value    this.dep = new Dep()    this.vmCount = 0    def(value, '__ob__', this)    if (Array.isArray(value)) {      if (hasProto) {        protoAugment(value, arrayMethods)      } else {        copyAugment(value, arrayMethods, arrayKeys)      }      this.observeArray(value)    } else {      this.walk(value)    }  }
/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } }
复制代码


通过源码我们可以知道,Vue 没有对数组每个键设置响应式的过程,而是直接对值进行递归设置响应式。

$set 为啥能检测数组变动

还是去源码瞅一眼,看 vue 是怎么对数组进行处理的。


源码位置:dist/vue.runtime.esm.js


function set (target, key, val) {  //...   if (Array.isArray(target) && isValidArrayIndex(key)) {    target.length = Math.max(target.length, key);    target.splice(key, 1, val);    return val  }  if (key in target && !(key in Object.prototype)) {    target[key] = val;    return val  }  //...   defineReactive$$1(ob.value, key, val);  ob.dep.notify();  return val}
复制代码




  • 如果 target 是一个数组且索引有效,就设置 length 的属性。

  • 通过 splice 方法把 value 设置到 target 数组指定位置。

  • 设置的时候,vue 会拦截到 target 发生变化,然后把新增的 value 也变成响应式

  • 最后返回 value


这就是 vue 重写数组方法的原因,利用数组这些方法触发使得每一项的 value 都是响应式的。

回答思想

正如之前所说,面试一道题目不在乎表面你回答多么准确,实际上在乎的是其中的来龙去脉。我们由浅入深,一步步解密其中的原理,这才是学习的思想。


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
Vue是怎样监听数组的变化的?_Vue_bb_xiaxia1998_InfoQ写作社区