详解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实现给图片加链接
Aug 15 Javascript
jquery实现按Enter键触发事件示例
Sep 10 Javascript
jquery自动切换tabs选项卡的具体实现
Dec 24 Javascript
loading动画特效小结
Jan 22 Javascript
jquery编写日期选择器
Mar 16 Javascript
Javascript循环删除数组中元素的几种方法示例
May 18 Javascript
使用puppeteer破解极验的滑动验证码
Feb 24 Javascript
vue中的自定义分页插件组件的示例
Aug 18 Javascript
JS+HTML5 Canvas实现简单的写字板功能示例
Aug 30 Javascript
JS通过ajax + 多列布局 + 自动加载实现瀑布流效果
May 30 Javascript
微信小程序本地存储实现每日签到、连续签到功能
Oct 09 Javascript
JavaScript对象字面量和构造函数原理与用法详解
Apr 18 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
《星际争霸重制版》兵种对比图鉴
2020/03/02 星际争霸
php地址引用(php地址引用的效率问题)
2012/03/23 PHP
PHP错误处理函数register_shutdown_function使用示例
2017/07/03 PHP
laravel框架模型和数据库基础操作实例详解
2020/01/25 PHP
基于SVG的web页面图形绘制API介绍及编程演示
2013/06/28 Javascript
jQuery中dequeue()方法用法实例
2014/12/29 Javascript
Jquery对新插入的节点 绑定Click事件失效的解决方法
2016/06/02 Javascript
JavaScript事件学习小结(一)事件流
2016/06/09 Javascript
JS中的hasOwnProperty()、propertyIsEnumerable()和isPrototypeOf()
2016/08/11 Javascript
JavaScript 字符串常用操作小结(非常实用)
2016/11/30 Javascript
vue webpack重写cookie路径的方法
2019/07/10 Javascript
js使用文件流下载csv文件的实现方法
2019/07/15 Javascript
vue总线机制(bus)知识点详解
2020/05/10 Javascript
javascript canvas时钟模拟器
2020/07/13 Javascript
再也不怕 JavaScript 报错了,怎么看怎么处理都在这儿
2020/12/09 Javascript
[55:32]2018DOTA2亚洲邀请赛 4.4 淘汰赛 EG vs LGD 第二场
2018/04/05 DOTA
python的id()函数解密过程
2012/12/25 Python
Python爬虫实现简单的爬取有道翻译功能示例
2018/07/13 Python
python将pandas datarame保存为txt文件的实例
2019/02/12 Python
PyQt5 多窗口连接实例
2019/06/19 Python
Python进度条的制作代码实例
2019/08/31 Python
pycharm激活码有效到2020年11月底
2020/09/18 Python
解决Pycharm 导入其他文件夹源码的2种方法
2020/02/12 Python
Python3爬虫发送请求的知识点实例
2020/07/30 Python
HTML5 input元素类型:email及url介绍
2013/08/13 HTML / CSS
介绍下Java中==和equals的区别
2013/09/01 面试题
设计部经理的岗位职责
2013/11/16 职场文书
2014年公司植树节活动方案
2014/03/04 职场文书
护理人员的自我评价分享
2014/03/15 职场文书
幼儿园的门卫岗位职责
2014/04/10 职场文书
运动会铅球比赛加油稿
2014/09/26 职场文书
员工离职证明范本
2015/06/12 职场文书
护士岗位竞聘书
2015/09/15 职场文书
2015年幼儿园师德师风建设工作总结
2015/10/23 职场文书
JUnit5常用注解的使用
2021/07/02 Java/Android
Python借助with语句实现代码段只执行有限次
2022/03/23 Python