Vue响应式原理-如何监听Array的变化?

回忆

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

Observer的伪代码中咱们模拟了以下代码:java

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里还作了什么事。git

Array的变化如何监听?

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

虽然咱们能够监听到索引的改变。数组

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用了另外一个方案来处理。app

首先咱们的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 中对这个数组问题的解决方案很是的简单粗暴,就是对可以改变数组的方法作了一些手脚。post

咱们知道,改变数组的方法有不少,举个例子好比说push方法吧。push存在Array.prototype上的,若是咱们能

能拦截到原型上的push方法,是否是就能够作一些事情呢?

Object.defineProperty

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

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

value

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

writable

当且仅当该属性的writabletrue时,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

Vuearray.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); // 获取参数

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

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源码解读系列篇

Github博客 欢迎交流~

相关文章
相关标签/搜索