Vue响应式原理 - 关于Array的特别处理

以前写过一篇响应式原理-如何监听Array的变化,最近准备给团队同事分享,发现以前看的太粗糙了,所以决定再写一篇详细版~javascript

1、如何监听数组索引的变化?

(1)案例分析

相信初学Vue的同窗必定踩过这个坑,改变数组的索引,没有触发视图更新。 好比下面这个案例:html

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
复制代码

以上案例摘抄Vue官方文档 - 数组更新检测vue

(2)解决方式

Vue官方文档也有给出,使用Vue.set便可达到触发视图更新的效果。java

// Vue.set
Vue.set(vm.items, indexOfItem, newValue);
复制代码

(3)Vue为什么不能监听索引的变化?

Vue官方给出了解释,不能检测。git

因为 JavaScript 的限制,Vue 不能检测如下数组的变更: 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValuegithub

那缘由是什么?我在学习的过程当中发现不少文章都在断章取义,Vue官方给出了解释是【Vue不能检测】,而不少文章写出的是【Object.defineProperty不能检测】。segmentfault

但实际上Object.defineProperty是能够检测到数组索引的变化的。以下案例:数组

let data = [1, 2];
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("数据被改变了,我要渲染到页面上去!");
        }
    })
}

defineReactive(data, 0, 1);
console.log(data[0]);
data[0] = 5;
复制代码

你们能够本身在控制台中尝试一下,答案很是明显了。浏览器

`Object.defineProperty`检测数组索引的变化

Vue只是没有使用这个方式去监听数组索引的变化,由于尤大认为性能消耗太大,因而在性能和用户体验之间作了取舍。 详细可见这边文章Vue为何不能检测数组变更bash

好了,终于揭开了谜底,为何Vue为何不能检测数组变更,由于不作哈哈。

可是咱们开发者确定是有这个需求的,解决方式就是以下,使用Vue.set

// Vue.set
Vue.set(vm.items, indexOfItem, newValue);
复制代码

原理?很是明显,在初始的过程当中没有循环对全部数组索引监听,可是开发者须要监听哪一个索引。Vue.set就帮你监听哪一个,核心仍是Object.defineProperty。只是尽量的避免了无用的数组索引监听。

2、如何监听数组内容的增长或减小?

(1)技能限制

Object.defineProperty虽然能检测索引的变化,但的确是监听不到数组的增长或删除的。能够阅读 Vue官方文档 - 对象变动检测注意事项 进行了解。

这个时候Vue是怎么作的呢?

(2)巧妙解决

数组拦截

Vue的解决方案,就是重写了数组的原型,更准确的表达是拦截了数组的原型。

首先选择了7个可以改变数组自身的几个方法。其次看下案例吧:

// 得到原型上的方法
const arrayProto = Array.prototype;

// 建立一个新对象,使用现有的对象来提供新建立的对象的__proto__
const arrayMethods = Object.create(arrayProto); 

// 作一些拦截的操做
Object.defineProperty(arrayMethods, 'push', {
    value(...args) {
        console.log('用户传进来的参数', args);

        // 真正的push 保证数据如用户指望
        arrayProto.push.apply(this, args);
    },
    enumerable: true,
    writable: true,
    configurable: true,
});

let list = [1];

list.__proto__ = arrayMethods; // 重置原型

list.push(2, 3);

console.log('用户获得的list:', list);

复制代码

为何叫拦截,咱们在重写案例中的push方法时,还须要使用真正的push,这样才能保证数组如用户所指望的push进去。

能够看到如下效果,咱们既能监听到用户传进来的参数,也就是监听到这个数组变化了,还能保证数组如用户所指望的push进去。

结果

为何使用arrayMethods继承真正的原型,由于这样才不会污染全局的Array.prototype,由于咱们要监听的数组只有vm.data中的。

(3)源码分析

export class Observer {
    constructor (value: any) {
        // 若是是数组
        if (Array.isArray(value)) {
            // 若是原型上有__proto__属性, 主要是浏览器判断兼容
            if (hasProto) {
                // 直接覆盖响应式对象的原型
                protoAugment(value, arrayMethods)
            } else {
                // 直接拷贝到对象的属性上,由于访问一个对象的方法时,先找他自身是否有,而后才去原型上找
                copyAugment(value, arrayMethods, arrayKeys)
            }
        } else {
          // 若是是对象
          this.walk(value);
        }
    }
}
复制代码

以上能够看到Observer对数组的特别处理。

(4)数组是如何收集依赖、派发更新的?

咱们知道对象是在getter中收集依赖,setter中派发更新。 那简单回忆下:

function defineReactive (obj, key, val) {
    // 生成一个Dep实例
    let dep = new Dep();
    Object.defineProperty(obj, key, {
        get: () => {
            // 依赖收集
            dep.depend();
        },
        set: () => {
            // 派发更新
            dep.notify();
        },
    })
}
复制代码

为了保证data中每一个数据有着一对一的dep,这里应用了闭包,保证每一个dep实例不会被销毁。那么问题来了,dep是一个局部变量呀~ 而监听数组变化,须要在数组拦截器中进行派发更新。那就访问不到这个dep了,就没法知道具体要通知哪些Watcher了!

那Vue是怎么作的呢?既然这个访问不到,那就再来一个dep吧。

export class Observer {
    constructor (value: any) {
        this.value = value // data属性
        this.dep = new Dep() // 挂载dep实例
        // 为数据定义了一个 __ob__ 属性,这个属性的值就是当前 Observer 实例对象
        def(value, '__ob__', this) // 把当前Observer实例挂在到data的__ob__上
    }
}
复制代码

Vue初始化的过程当中,给data中的每一个数据都挂载了当前的Observer实例,又在这个实例上挂载了dep。这样就能保证咱们在数组拦截器中访问到dep了。以下:

Object.defineProperty(arrayMethods, 'push', {
    value(...args) {
        console.log('用户传进来的参数', args);

        // 真正的push 保证数据如用户指望
        arrayProto.push.apply(this, args);
        
        // this指向当前这个数组,在初始化的时候被赋值__ob__
        console.log(this.__ob__.dep)
    },
    enumerable: true,
    writable: true,
    configurable: true,
});
复制代码

如今咱们即可以在拦截器中执行dep.notify()啦。

那如何收集依赖呢?

// 获取当前data上的 observe实例,也就是__ob__
let childOb = !shallow && observe(val);

function defineReactive (obj, key, val) {
    // 生成一个Dep实例
    let dep = new Dep();
    Object.defineProperty(obj, key, {
        get: () => {
            if (Dep.target) {
                // 依赖收集
                dep.depend();
                
                // 二次收集
                if (childOb.dep) {
                    // 再收集一次依赖
                    childOb.dep.depend();
                }
            }
            return val;
        },
    })
}
复制代码

如今要存放2个dep,那天然是要在getter中收集2次的,childOb其实就是observe中返回的__ob__。不用在乎细节,自行查看源码就知道啦~

(5)总结

总结一下,针对数组在getter中收集依赖,在拦截器中触发更新

数组

3、其余思考

(1)思考:还有哪里能够用到__ob__?

  1. 判断某个数组是否已Observer过,避免重复执行。

  2. Vue.setVue.del,都是须要访问dep的。

(2)数组赋值算改变长度吗?

由于Object.defineProperty不能检测数组的长度变化,例如:vm.items.length = newLength

var vm = new Vue({
  data: {
    items: ['a']
  }
})
// 从新赋值,改变长度
vm.items = ['a, 'b', 'c']
复制代码

vm.items = ['a, 'b', 'c']这种状况,Vue是如何监听的?这种状况其实监听的是对象vmitems属性,和数组实际上是不要紧的。由于以前发现有人误解,这里简单的提示一下~

4、总结

本文主要仍是讲原理及思路,并不会涉及到不少代码,毕竟源码总会变。同时还要保证本身的js基础扎实,阅读源码才不会吃力哦~ 我就是很吃力的那种😭

若是你以为对你有帮助,就点个赞吧~

已完成:

Vue源码解读系列篇

Github博客 欢迎交流~

5、参考文献

相关文章
相关标签/搜索