详解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 相关文章推荐
JQuery Easyui Tree的oncheck事件实现代码
May 28 Javascript
12306验证码破解思路分享
Mar 25 Javascript
如何使用HTML5地理位置定位功能
Apr 27 Javascript
浅谈JavaScript 中有关时间对象的方法
Aug 15 Javascript
AngularJs concepts详解及示例代码
Sep 01 Javascript
jQuery+CSS3实现仿花瓣网固定顶部位置带悬浮效果的导航菜单
Sep 21 Javascript
快速实现jQuery多级菜单效果
Feb 01 Javascript
基于JavaScript实现多级菜单效果
Jul 25 Javascript
JavaScript登录验证基础教程
Nov 01 Javascript
echarts统计x轴区间的数值实例代码详解
Jul 07 Javascript
微信头像地址失效踩坑记附带解决方案
Sep 23 Javascript
JS面向对象编程实现的拖拽功能案例详解
Mar 03 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
ThinkPHP独立分组使用的注意事项
2014/11/25 PHP
php将数组存储为文本文件方法汇总
2015/10/28 PHP
PHP preg_match实现正则表达式匹配功能【输出是否匹配及匹配值】
2017/07/19 PHP
北京奥运官方网站幻灯切换效果flash版打包下载
2008/01/30 Javascript
jQuery点击后一组图片左右滑动的实现代码
2012/08/16 Javascript
关于IE中getElementsByClassName不能用的问题解决方法
2013/08/26 Javascript
jquery实现兼容浏览器的图片上传本地预览功能
2013/10/14 Javascript
Javascript 函数parseInt()转换时出现bug问题
2014/05/20 Javascript
javascript实现限制上传文件大小
2015/02/06 Javascript
JavaScript实现的圆形浮动标签云效果实例
2015/08/06 Javascript
JS基于ocanvas插件实现的简单画板效果代码(附demo源码下载)
2016/04/05 Javascript
基于jquery实现轮播特效
2016/04/22 Javascript
javascript css红色经典选项卡效果实现代码
2016/05/17 Javascript
Dropzone.js实现文件拖拽上传功能(附源码下载)
2016/11/22 Javascript
微信小程序 获取当前地理位置和经纬度实例代码
2016/12/05 Javascript
微信小程序 rich-text的使用方法
2017/08/04 Javascript
JavaScript常见继承模式实例小结
2019/01/11 Javascript
jquery制作的移动端购物车效果完整示例
2020/02/24 jQuery
解析Python中while true的使用
2015/10/13 Python
在Mac OS上使用mod_wsgi连接Python与Apache服务器
2015/12/24 Python
python获取酷狗音乐top500的下载地址 MP3格式
2018/04/17 Python
在PyCharm下使用 ipython 交互式编程的方法
2019/01/17 Python
python读取目录下所有的jpg文件,并显示第一张图片的示例
2019/06/13 Python
python实现ip地址查询经纬度定位详解
2019/08/30 Python
Python模块_PyLibTiff读取tif文件的实例
2020/01/13 Python
20行Python代码实现一款永久免费PDF编辑工具的实现
2020/08/27 Python
python中字符串的编码与解码详析
2020/12/03 Python
pytorch 实现L2和L1正则化regularization的操作
2021/03/03 Python
软件测试笔试题
2012/10/25 面试题
校园门卫岗位职责
2013/12/09 职场文书
年终奖发放方案
2014/06/02 职场文书
工作求职自荐信
2014/06/13 职场文书
元旦主持词开场白
2015/05/29 职场文书
经典爱情感言
2015/08/03 职场文书
某某幼儿园的教育教学管理调研分析报告
2019/11/29 职场文书
CSS变量实现主题切换的方法
2021/06/23 HTML / CSS