VIP用户交流群:462197261 收藏本站北冥有鱼 互联网前沿资源第一站 助力全行业互联网+
在线客服:78895949
tonglan
  • 当前位置:
  • 详解Vue 如何监听Array的变化

    回忆

    在上一篇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.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源码简化后的,为了方便大家理解。思想理解了,源码就容易看懂了。

    以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持北冥有鱼。

    您可能感兴趣的文章:

    • vue App.vue中的公共组件改变值触发其他组件或.vue页面监听
    • 解决Vue.js父组件$on无法监听子组件$emit触发事件的问题
    • 使用vue.js在页面内组件监听scroll事件的方法
    • vue-cli监听组件加载完成的方法
    • vue.js项目 el-input 组件 监听回车键实现搜索功能示例
    • vue使用$emit时,父组件无法监听到子组件的事件实例
    • vue2.x 父组件监听子组件事件并传回信息的方法
    • 详解vuex 中的 state 在组件中如何监听
    • vuejs2.0实现分页组件使用$emit进行事件监听数据传递的方法

    广而告之:
    热门推荐:
    ionic实现下拉刷新载入数据功能

    本文实例为大家分享了ionic下拉刷新载入数据的具体代码,供大家参考,具体内容如下 <!DOCTYPE html> <html ng-app="myApp"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initi···

    使用vue构建一个上传图片表单

    这篇博客也不知道起个什么名字比较好,毕竟刚开始学习vue,很多还不是很熟悉,一直在慢慢摸索中;之前也习惯了用jQuery写js代码,思维逻辑也要有个转换的过程。 1. 转变思维 使用vue编写代码,首先要做的就是转换思维,vue是一个数据驱动的框架,我们只需要操作数据,数据变化···

    PHP FTP操作类代码( 上传、拷贝、移动、删除文件/创建目录)

    复制代码 代码如下:<?php/*** 作用:FTP操作类( 拷贝、移动、删除文件/创建目录 )* 时间:2006/5/9* 作者:欣然随风* QQ:276624915*/class class_ftp{    public $off; // 返回操作状态(成功/失败)    public $conn_id; // FTP连接&nb···

    php用户注册信息验证正则表达式

    下面这个正则验证用户名的方法原则是这样的用户名必须是由字母带数字带定划线组成了,下面一起来看看例子吧. 1.检查用户名是否符合规定“两位以上的字母,数字,或者下划线”,代码如下: /** * 检查用户名是否符合规定 * * @param STRING $username 要检查的用户名 * @retu···

    CodeIgniter多语言实现方法详解

    本文实例分析了CodeIgniter多语言实现方法。分享给大家供大家参考,具体如下: CI应用目录下有一个language语言包目录,用来配置多种不同的语言。语言的配置位于config文件中,配置格式如下: 复制代码 代码如下:$config['language'] = 'english'; 定义语言文件 language下有个···

    vue-cli 3 全局过滤器的实例代码详解

    在vue1.x版本里是自带里几个基础的过滤器的(虽然不怎么用到)。 到了vue2.x版本之后,内置到几个过滤器就被删掉了,如果需要用到过滤器,就要开发者自己去定义。 可以把过滤器定义在某个组件内部,但这样就成了局部过滤器,在别的组件里是无法使用的。所以通常不会这么做。 ···

    发挥语言的威力--融合PHP与ASP

    本人在项目开发中,开始一直用php,在享受着php的灵活的功能时,同时也遇到了一些问题,如php的ODBC接口连接"远程oracle"问题,就是报告你无m骺幏ń馕鯰NS服务名,经过一番捉摸,现在还是没有找到问题的答案,燃眉之急! 情急之中,想出一妙计,众君请看: 我···

    HTML网页制作教程 谨慎使用iframe标记

    使用 iframe 可以轻易的调用其他网站的页面,但应谨慎使用。它比创建其他 DOM 元素(包括 style 和 script)多耗费数十甚至数百倍的性能。增加100个不同元素的时间对比显示 iframe 是多么耗费性能:   使用 iframe 的页面通常没有这么多 iframe,所以创建 DOM 的时间不用···

    JS 使用 window对象的print方法实现分页打印功能

    最近做项目用到了web在线打印功能,经研究使用了JS自身支持的Window对象的打印方法,此种方法兼容性比较好,在IE和火狐浏览器下使用都没有问题。  1.但是网上好多案例都不支持分页功能,最后通过CSS的page-break-after:always样式解决分页问题,以下代码纯个人编写,有需···

    Vue filter格式化时间戳时间成标准日期格式的方法

    调用实例:yyyy-MM-dd或者yyyy-MM-dd hh:mm:ss进行格式 <div>{{data | dataFormat('yyyy-MM-dd hh:mm:ss')}}</div> 代码: import Vue from 'vue' Vue.filter('dataFormat', function (value, fmt) { let getDate = new Date(value); let o = { 'M+': getDate···