详解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 相关文章推荐
兼容IE和FF的js脚本代码小结(比较常用)
Dec 06 Javascript
js history对象简单实现返回和前进
Oct 30 Javascript
js 与 php 通过json数据进行通讯示例
Mar 26 Javascript
js函数参数设置默认值的一种变通实现方法
May 26 Javascript
JS动态添加iframe的代码
Sep 14 Javascript
盘点javascript 正则表达式中 中括号的【坑】
Mar 16 Javascript
Vue分页组件实例代码
Apr 17 Javascript
详解vuex中mapState,mapGetters,mapMutations,mapActions的作用
Apr 13 Javascript
jQuery删除/清空指定元素的所有子节点实例代码
Jul 04 jQuery
JavaScript 继承 封装 多态实现及原理详解
Jul 29 Javascript
three.js 如何制作魔方
Jul 31 Javascript
通过实例解析js可枚举属性与不可枚举属性
Dec 02 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.ini中文版
2006/10/09 PHP
QQ登录 PHP OAuth示例代码
2011/07/20 PHP
php使用GD创建保持宽高比缩略图的方法
2015/04/17 PHP
浅谈php(codeigniter)安全性注意事项
2017/04/06 PHP
php 截取中英文混合字符串的方法
2018/05/31 PHP
Laravel 创建指定表 migrate的例子
2019/10/09 PHP
浅谈laravel-admin form中的数据,在提交后,保存前,获取并进行编辑
2019/10/21 PHP
dtree 网页树状菜单及传递对象集合到js内,动态生成节点
2012/04/14 Javascript
图片放大镜jquery.jqzoom.js使用实例附放大镜图标
2014/06/19 Javascript
Javascript学习指南
2014/12/01 Javascript
jquery配合.NET实现点击指定绑定数据并且能够一键下载
2016/10/28 Javascript
javascript跨域请求包装函数与用法示例
2016/11/03 Javascript
js判断一个字符串是以某个字符串开头的简单实例
2016/12/27 Javascript
使用JavaScript判断用户输入的是否为正整数(两种方法)
2017/02/05 Javascript
彻底学会Angular.js中的transclusion
2017/03/12 Javascript
vue2.0获取自定义属性的值
2017/03/28 Javascript
整理关于Bootstrap警示框的慕课笔记
2017/03/29 Javascript
ES6函数实现排它两种写法解析
2020/05/13 Javascript
python thread 并发且顺序运行示例
2009/04/09 Python
Python中for循环详解
2014/01/17 Python
python虚拟环境virtualenv的使用教程
2017/10/20 Python
python自动发邮件库yagmail的示例代码
2018/02/23 Python
vue.js实现输入框输入值内容实时响应变化示例
2018/07/07 Python
python获取百度热榜链接的实例方法
2020/08/25 Python
德国baby-markt婴儿用品瑞士网站:baby-markt.ch
2017/06/09 全球购物
英国女性时尚品牌:Apricot
2018/12/04 全球购物
戴森英国官网:Dyson英国
2019/05/07 全球购物
台湾演唱会订票网站:StubHub台湾
2019/06/11 全球购物
长青弘远的面试题
2012/06/09 面试题
社区七一党员活动方案
2014/01/25 职场文书
继承公证书
2014/04/09 职场文书
国际金融专业自荐信
2014/07/05 职场文书
公路局群众路线教育实践活动第一阶段工作汇报
2014/10/25 职场文书
2017元旦晚会开幕词
2016/03/03 职场文书
比较几种Redis集群方案
2021/06/21 Redis
Apache Pulsar集群搭建部署详细过程
2022/02/12 Servers