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、參考文獻

相關文章
相關標籤/搜索