首頁 > 軟體

Vue2 響應式系統之陣列

2022-04-13 04:00:16

本文接Vue2響應式系統 Vue2 響應式系統之分支切換  ,響應式系統之巢狀響應式系統之深度響應 還沒有看過的小夥伴需要看一下。

1、場景

import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
    list: ["hello"],
};
observe(data);
const updateComponent = () => {
    for (const item of data.list) {
        console.log(item);
    }
};

new Watcher(updateComponent);
data.list = ["hello", "liang"];

先可以一分鐘思考下會輸出什麼。

雖然 的值是陣列,但我們是對 進行整體賦值,所以依舊會觸發 的 ,觸發 進行重新執行,輸出如下:listdata.listdata.listsetWatcher

2、場景 2

import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
    list: ["hello"],
};
observe(data);
const updateComponent = () => {
    for (const item of data.list) {
        console.log(item);
    }
};
new Watcher(updateComponent);

data.list.push("liang");

先可以一分鐘思考下會輸出什麼。

這次是呼叫 方法,但我們對 方法什麼都沒做,因此就不會觸發 了。pushpushWatcher

3、方案

為了讓 還有陣列的其他方法也生效,我們需要去重寫它們,通過push代理模式 我們可以將陣列的原方法先儲存起來,然後執行,並且加上自己額外的操作。

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

/*
export function def(obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true,
    });
}
*/
import { def } from "./util";

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

const methodsToPatch = [
    "push",
    "pop",
    "shift",
    "unshift",
    "splice",
    "sort",
    "reverse",
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
    // cache original method
    const original = arrayProto[method];
    def(arrayMethods, method, function mutator(...args) {
        const result = original.apply(this, args);
        /*****************這裡相當於呼叫了物件 set 需要通知 watcher ************************/
        // 待補充
        /**************************************************************************** */
        return result;
    });
});

當呼叫了陣列的 或者其他方法,就相當於我們之前重寫屬性的 ,上邊待補充的地方需要做的就是通知 中的 。pushsetdepWatcher

export function defineReactive(obj, key, val, shallow) {
    const property = Object.getOwnPropertyDescriptor(obj, key);
    // 讀取使用者可能自己定義了的 get、set
    const getter = property && property.get;
    const setter = property && property.set;
    // val 沒有傳進來話進行手動賦值
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key];
    }
    const dep = new Dep(); // 持有一個 Dep 物件,用來儲存所有依賴於該變數的 Watcher

    let childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val;
            if (Dep.target) {
                dep.depend();
            }
            return value;
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val;

            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            dep.notify();
        },
    });
}

如上邊的程式碼,之前的 是通過閉包,每一個屬性都有一個各自的 ,負責收集 和通知 。depdepWatcherWatcher

那麼對於陣列的話,我們的 放到哪裡比較簡單呢?dep

回憶一下現在的結構。

const data = {
    list: ["hello"],
};
observe(data);

const updateComponent = () => {
    for (const item of data.list) {
        console.log(item);
    }
};
new Watcher(updateComponent);

上邊的程式碼執行過後會是下圖的結構。

list 屬性在閉包中擁有了 屬性,通過 ,收集到了包含 函數的 。Depnew WatcherupdateCompnentWatcher

同時因為 的 是陣列,也就是物件,通過上篇 listvalue["hello"]響應式系統之深度響應 (opens new window)我們知道,它也會去呼叫 函數。Observer

那麼,我是不是在 中也加一個 就可以了。ObserverDep

這樣當我們呼叫陣列方法去修改 的值的時候,去通知 中的 就可以了。['hello']ObserverDep

3、收集依賴程式碼實現

按照上邊的思路,完善一下 類。Observer

export class Observer {
    constructor(value) {
        /******新增 *************************/
        this.dep = new Dep();
        /************************************/
      	this.walk(value);
    }

    /**
     * 遍歷物件所有的屬性,呼叫 defineReactive
     * 攔截物件屬性的 get 和 set 方法
     */
    walk(obj) {
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i]);
        }
    }
}

然後在 中,當前 中的 也去收集依賴。getOberverdep

export function defineReactive(obj, key, val, shallow) {
    const property = Object.getOwnPropertyDescriptor(obj, key);
    // 讀取使用者可能自己定義了的 get、set
    const getter = property && property.get;
    const setter = property && property.set;
    // val 沒有傳進來話進行手動賦值
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key];
    }
    const dep = new Dep(); // 持有一個 Dep 物件,用來儲存所有依賴於該變數的 Watcher

    let childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val;
            if (Dep.target) {
                dep.depend();
                /******新增 *************************/
                if (childOb) {
                    // 當前 value 是陣列,去收集依賴
                    if (Array.isArray(value)) {
                        childOb.dep.depend();
                    }
                }
                /************************************/
            }
            return value;
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val;

            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            dep.notify();
        },
    });
}

4、通知依賴程式碼實現

我們已經重寫了 方法,但直接覆蓋全域性的 方法肯定是不好的,我們可以在 類中去操作,如果當前 是陣列,就去攔截它的 方法。arrayarrrayObservervaluearray

這裡就回到 的原型鏈上了,我們可以通過瀏覽器自帶的 ,將當前物件的原型指向我們重寫過的方法即可。js__proto__

考慮相容性的問題,如果 不存在,我們直接將重寫過的方法複製給當前物件即可。__proto__

import { arrayMethods } from './array' // 上邊重寫的所有陣列方法
/* export const hasProto = "__proto__" in {}; */
export class Observer {
    constructor(value) {
        this.dep = new Dep();
      	/******新增 *************************/
        if (Array.isArray(value)) {
            if (hasProto) {
                protoAugment(value, arrayMethods);
            } else {
                copyAugment(value, arrayMethods, arrayKeys);
            }
        /************************************/
        } else {
            this.walk(value);
        }
    }

    /**
     * 遍歷物件所有的屬性,呼叫 defineReactive
     * 攔截物件屬性的 get 和 set 方法
     */
    walk(obj) {
        const keys = Object.keys(obj);
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i]);
        }
    }
}
/**
 * Augment a target Object or Array by intercepting
 * the prototype chain using __proto__
 */
function protoAugment(target, src) {
    /* eslint-disable no-proto */
    target.__proto__ = src;
    /* eslint-enable no-proto */
}

/**
 * Augment a target Object or Array by defining
 * hidden properties.
 */
/* istanbul ignore next */
function copyAugment(target, src, keys) {
    for (let i = 0, l = keys.length; i < l; i++) {
        const key = keys[i];
        def(target, key, src[key]);
    }
}

還需要考慮一點,陣列方法中我們只能拿到 值,那麼怎麼拿到 對應的 呢。valuevalueObserver

我們只需要在 類中,增加一個屬性來指向自身即可。Observe

export class Observer {
    constructor(value) {
        this.dep = new Dep();
        /******新增 *************************/
        def(value, '__ob__', this)
        /************************************/
        if (Array.isArray(value)) {
            if (hasProto) {
                protoAugment(value, arrayMethods);
            } else {
                copyAugment(value, arrayMethods, arrayKeys);
            }
        } else {
            this.walk(value);
        }
    }
  	...
}

回到最開始重寫的 方法中,只需要從 中拿到 去通知 即可。array__ob__DepWatcher

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from "./util";

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

const methodsToPatch = [
    "push",
    "pop",
    "shift",
    "unshift",
    "splice",
    "sort",
    "reverse",
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
    // cache original method
    const original = arrayProto[method];
    def(arrayMethods, method, function mutator(...args) {
        const result = original.apply(this, args);
        /*****************這裡相當於呼叫了物件 set 需要通知 watcher ************************/
        const ob = this.__ob__;
        // notify change
        ob.dep.notify();
        /**************************************************************************** */
        return result;
    });
});

5、測試

import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
    list: ["hello"],
};
observe(data);
const updateComponent = () => {
    for (const item of data.list) {
        console.log(item);
    }
};

new Watcher(updateComponent);
data.list.push("liang");

這樣當呼叫 方法的時候,就會觸發相應的 來執行 函數了。pushWatcherupdateComponent

當前的依賴就變成了下邊的樣子:

6、總結

對於陣列的響應式我們解決了三個問題,依賴放在哪裡、收集依賴和通知依賴。

我們來和普通物件屬性進行一下對比。

 到此這篇關於Vue2 響應式系統之陣列的文章就介紹到這了,更多相關Vue2 陣列內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com