详解Vue 如何监听Array的变化


Posted in Javascript onJune 06, 2019

回忆

在上一篇Vue响应式原理-理解Observer、Dep、Watcher简单讲解了Observer、Dep、Watcher三者的关系。

在Observer的伪代码中我们模拟了如下代码:

class Observer {
 constructor() {
  // 响应式绑定数据通过方法
  observe(this.data);
 }
}

export function observe (data) {
 const keys = Object.keys(data);
 for (let i = 0; i < keys.length; i++) {
  // 将data中我们定义的每个属性进行响应式绑定
  defineReactive(obj, keys[i]);
 }
}

export function defineReactive () {
 // ...省略 Object.defineProperty get-set
}

今天我们就进一步了解Observer里还做了什么事。

Array的变化如何监听?

data 中的数据如果是一个数组怎么办?我们发现Object.defineProperty对数组进行响应式化是有缺陷的。

虽然我们可以监听到索引的改变。

function defineReactive (obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: () => {
      console.log('我被读了,我要不要做点什么好?');
      return val;
    },
    set: newVal => {
      if (val === newVal) {
        return;
      }
      val = newVal;
      console.log("数据被改变了,我要渲染到页面上去!");
    }
  })
}

let data = [1];

// 对数组key进行监听
defineReactive(data, 0, 1);
console.log(data[0]); // 我被读了,我要不要做点什么好?
data[0] = 2; // 数据被改变了,我要渲染到页面上去!

但是defineProperty不能检测到数组长度的变化,准确的说是通过改变length而增加的长度不能监测到。这种情况无法触发任何改变。

data.length = 0; // 控制台没有任何输出

而且监听数组所有索引的的代价也比较高,综合一些其他因素,Vue用了另一个方案来处理。

首先我们的observe需要改造一下,单独加一个数组的处理。

// 将data中我们定义的每个属性进行响应式绑定
export function observe (data) {
  const keys = Object.keys(data);
  for (let i = 0; i < keys.length; i++) {
    // 如果是数组
    if (Array.isArray(keys[i])) {
      observeArray(keys[i]);
    } else {
      // 如果是对象
      defineReactive(obj, keys[i]);
    }
  }
}

// 数组的处理
export function observeArray () {
  // ...省略
}

那接下来我们就应该考虑下Array变化如何监听?

Vue 中对这个数组问题的解决方案非常的简单粗暴,就是对能够改变数组的方法做了一些手脚。

我们知道,改变数组的方法有很多,举个例子比如说push方法吧。push存在Array.prototype上的,如果我们能
能拦截到原型上的push方法,是不是就可以做一些事情呢?

Object.defineProperty

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。存取描述符是由getter-setter函数对描述的属性,也就是我们用来给对象做响应式绑定的。Object.defineProperty-MDN

虽然我们无法使用Object.defineProperty将数组进行响应式的处理,也就是getter-setter,但是还有其他的功能可以供我们使用。就是数据描述符,数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。

value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。

writable

当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。

因此我们只要把原型上的方法,进行value的重新赋值。

如下代码,在重新赋值的过程中,我们可以获取到方法名和所有参数。

function def (obj, key) {
  Object.defineProperty(obj, key, {
    writable: true,
    enumerable: true,
    configurable: true,
    value: function(...args) {
      console.log('key', key);
      console.log('args', args); 
    }
  });
}

// 重写的数组方法
let obj = {
  push() {}
}

// 数组方法的绑定
def(obj, 'push');

obj.push([1, 2], 7, 'hello!');
// 控制台输出 key push
// 控制台输出 args [Array(2), 7, "hello!"]

通过如上代码我们就可以知道,用户使用了数组上原型的方法以及参数我们都可以拦截到,这个拦截的过程就可以做一些变化的通知。

Vue监听Array三步曲

接下来,就看看Vue是如何实现的吧~

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

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

第三步:把需要被拦截的 Array 类型的数据原型指向改造后原型。

我们将代码进行下改造,拦截的过程中还是要将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变,然后我们再去做视图的更新等操作。

const arrayProto = Array.prototype // 获取Array的原型

function def (obj, key) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    value: function(...args) {
      console.log(key); // 控制台输出 push
      console.log(args); // 控制台输出 [Array(2), 7, "hello!"]
      
      // 获取原生的方法
      let original = arrayProto[key];
      // 将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变
      const result = original.apply(this, args);

      // do something 比如通知Vue视图进行更新
      console.log('我的数据被改变了,视图该更新啦');
      this.text = 'hello Vue';
      return result;
    }
  });
}

// 新的原型
let obj = {
  push() {}
}

// 重写赋值
def(obj, 'push');

let arr = [0];

// 原型的指向重写
arr.__proto__ = obj;

// 执行push
arr.push([1, 2], 7, 'hello!');
console.log(arr);

被改变后的arr。

详解Vue 如何监听Array的变化

Vue源码解析

array.js

Vue在array.js中重写了methodsToPatch中七个方法,并将重写后的原型暴露出去。

// Object.defineProperty的封装
import { def } from '../util/index'

// 获得原型上的方法
const arrayProto = Array.prototype

// Vue拦截的方法
const methodsToPatch = [
 'push',
 'pop',
 'shift',
 'unshift',
 'splice',
 'sort',
 'reverse'
];

// 将上面的方法重写
methodsToPatch.forEach(function (method) {
  def(arrayMethods, method, function mutator (...args) {
    console.log('method', method); // 获取方法
    console.log('args', args); // 获取参数

   // ...功能如上述,监听到某个方法执行后,做一些对应的操作
    // 1、将开发者的参数传给原生的方法,保证数组按照开发者的想法被改变
    // 2、视图更新等
  })
})

export const arrayMethods = Object.create(arrayProto);

observer

在进行数据observer绑定的时候,我们先判断是否hasProto,如果存在__proto__,就直接将value 的 __proto__指向重写过后的原型。如果不能使用 __proto__,貌似有些浏览器厂商没有实现。那就直接循环 arrayMethods把它身上的这些方法直接装到 value 身上好了。毕竟调用某个方法是先去自身查找,当自身找不到这关方法的时候,才去原型上查找。

// 判断是否有__proto__,因为部分浏览器是没有__proto__
const hasProto = '__proto__' in {}
// 重写后的原型
import { arrayMethods } from './array'
// 方法名
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);

// 数组的处理
export function observeArray (value) {
  // 如果有__proto__,直接覆盖        
  if (hasProto) {
    protoAugment(value, arrayMethods);
  } else {
    // 没有__proto__就把方法加到属性自身上
    copyAugment(value, arrayMethods, )
  }
}

// 原型的赋值
function protoAugment (target, src) {
  target.__proto__ = src;
}

// 复制
function copyAugment (target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key]);
  }
}

通过上面的代码我们发现,没有直接修改 Array.prototype,而是直接把 arrayMenthods 赋值给 value 的 __proto__ 。因为这样不会污染全局的Array, arrayMenthods 只对 data中的Array 生效。

总结

因为监听的数组带来的代价和一些问题,Vue使用了重写原型的方案代替。拦截了数组的一些方法,在这个过程中再去做通知变化等操作。

本文的一些代码均是Vue源码简化后的,为了方便大家理解。思想理解了,源码就容易看懂了。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
javascript DOM编程实例(智播客学习)
Nov 23 Javascript
$.format,jquery.format 使用说明
Jul 13 Javascript
javascript分页代码(当前页码居中)
Sep 20 Javascript
Extjs4.0设置Ext.data.Store传参的请求方式(默认为GET)
Apr 02 Javascript
js控制再次点击按钮之间的间隔时间可防止重复提交
Aug 01 Javascript
javascript如何写热点图
Dec 08 Javascript
用JS写的一个Ajax库(实例代码)
Aug 06 Javascript
Bootstrap作品展示站点实战项目2
Oct 14 Javascript
详解angularjs获取元素以及angular.element()用法
Jul 25 Javascript
vue中如何动态绑定图片,vue中通过data返回图片路径的方法
Feb 07 Javascript
JavaScript设计模式之单例模式原理与用法实例分析
Jul 26 Javascript
详解ES6 Fetch API HTTP请求实用指南
Nov 14 Javascript
js常见遍历操作小结
Jun 06 #Javascript
vue中v-show和v-if的异同及v-show用法
Jun 06 #Javascript
vue中的过滤器实例代码详解
Jun 06 #Javascript
Vue响应式原理Observer、Dep、Watcher理解
Jun 06 #Javascript
原生js通过一行代码实现简易轮播图
Jun 05 #Javascript
解决IOS端微信H5页面软键盘弹起后页面下方留白的问题
Jun 05 #Javascript
详解vue父子组件关于模态框状态的绑定方案
Jun 05 #Javascript
You might like
PHP中利用substr_replace将指定两位置之间的字符替换为*号
2011/01/27 PHP
ThinkPHP控制器间实现相互调用的方法
2014/10/31 PHP
PHP信号处理机制的操作代码讲解
2019/04/19 PHP
extjs中grid中嵌入动态combobox的应用
2011/01/01 Javascript
Js实现滚动变色的文字效果
2014/06/16 Javascript
js中setTimeout()与clearTimeout()用法实例浅析
2015/05/12 Javascript
学习javascript面向对象 实例讲解面向对象选项卡
2016/01/04 Javascript
jQuery flip插件实现的翻牌效果示例【附demo源码下载】
2016/09/20 Javascript
微信小程序 五星评价功能的实现
2017/03/09 Javascript
微信小程序手势操作之单触摸点与多触摸点
2017/03/10 Javascript
javascript 跨域问题以及解决办法
2017/07/17 Javascript
原生JS实现移动端web轮播图详解(结合Tween算法造轮子)
2017/09/10 Javascript
代码详解Vuejs响应式原理
2017/12/20 Javascript
Nuxt.js踩坑总结分享
2018/01/18 Javascript
vue2.0+vuex+localStorage代办事项应用实现详解
2018/05/31 Javascript
简单了解vue中的v-if和v-show的区别
2019/10/08 Javascript
vue循环数组改变点击文字的颜色
2019/10/14 Javascript
vue和小程序项目中使用iconfont的方法
2020/05/19 Javascript
python字典序问题实例
2014/09/26 Python
浅谈Python实现2种文件复制的方法
2018/01/19 Python
删除DataFrame中值全为NaN或者包含有NaN的列或行方法
2018/11/06 Python
Python简单处理坐标排序问题示例
2019/07/11 Python
详解CSS3的box-shadow属性制作边框阴影效果的方法
2016/05/10 HTML / CSS
CSS3 实现童年的纸飞机
2019/05/05 HTML / CSS
全球性的在线购物网站:Zapals
2017/03/22 全球购物
罗兰·穆雷官网:Roland Mouret
2018/09/28 全球购物
面向游戏玩家和书呆子的极客订阅盒:Loot Crate
2020/11/25 全球购物
存储过程和sql语句的优缺点
2014/07/02 面试题
高校师德师风自我剖析材料
2014/09/29 职场文书
2014年办公室主任工作总结
2014/11/12 职场文书
学校中秋节活动总结
2015/03/23 职场文书
白银帝国观后感
2015/06/17 职场文书
在校学生证明格式
2015/06/24 职场文书
民事调解协议书
2016/03/21 职场文书
5种方法告诉你如何使JavaScript 代码库更干净
2021/09/15 Javascript
JavaScript展开运算符和剩余运算符的区别详解
2022/02/18 Javascript