1. 程式人生 > 遊戲 >冒險解密遊戲《勇者鬥么蛾》發售 支援簡體中文

冒險解密遊戲《勇者鬥么蛾》發售 支援簡體中文

Bilibili - 尤雨溪Vue原始碼分析

掘金 - 尤雨溪國外教程:親手帶你寫個簡易版的Vue!

掘金 - [Vue官方教程筆記]- 尤雨溪手寫mini-vue

目錄

響應性概念

響應性:當狀態更新,系統會自動更新關聯狀態;在Web場景下,指的是不斷變化的狀態反映到DOM上的變化。

例如實現一個功能,使得變數b的值總是變數a的值的10倍。如果我們擁有一個magic function onAChanged(),即當a的值改變之後,自動呼叫該函式,則可以實現類似的功能。

// 當a的值改變, 呼叫該回調函式
onAChanged(() => {
    document.querySelector('.cell.b1').textContent = a * 10;
    // 上面的程式碼可以抽象為:
    // view = render(state);  // 當狀態改變, 渲染對應的DOM元素
});

在React中,實現方式類似:

let update;
const onStateChanged = _update => {
    update = _update;
};

// 必須使用setState更新狀態
const setState = newState => {
    state = newState;
    update();
};

在Angular中,使用髒值檢測實現,攔截例如點選等時間,檢查資料是否被更新。

在Vue中,使用ES5的Object.defineProperty()方法,重寫物件所有屬性的gettersetter方法。

getter和setter

通過ES5的Object.defineProperty()方法,監聽屬性值的變更,注意:

  • 下面的做法相當於,redefine物件objkey屬性,所以要求configurable不能為false
  • 所以,當再次呼叫convert(stu)時,會報錯
function convert(obj) {
    // 通過forEach監聽obj物件的每個屬性
    Object.keys(obj).forEach(key => {
        let internalValue = obj[key];  // 通過閉包儲存原來的值
        Object.defineProperty(obj, key, {
            configurable: false,  // 該屬性不能被redefine
            get() {
                console.log(`getting key "${key}": ${internalValue}`);
                return internalValue;
            },
            set(newValue) {
                console.log(`setting key "${key}" to: ${newValue}`);
                internalValue = newValue;
            }
        });
    });
}

let stu = { name: 'Lee', age: 20 };
convert(stu);
console.log(stu);  // { name: [Getter/Setter], age: [Getter/Setter] }

let age = stu.age;  // getting key "age": 20
stu.age = 50;  // setting key "age" to: 50

// 由於上面已經將新的屬性值設定為configurable: false, 所以不能進行redefine
// convert(stu);  // TypeError: Cannot redefine property: name

可以看到,當使用convert()方法轉換stu物件之後,每當讀取/修改物件的屬性時,都會收到提醒。

依賴追蹤

我們期望實現一個Dep類,它可以使用depend()方法收集依賴項,當所依賴項發生改變時,使用notify()方法觸發依賴項的執行。

const dep = new Dep();

// 自動執行, 收集依賴項
autorun(() => {
  dep.depend();  // 收集依賴項
  console.log('updated'); 
})

dep.notify();  // 通知以上收集的依賴項: 所依賴的變數updated

實現一個真正的Dep類:

let activeEffect;  // 當前受依賴項影響的函式
class Dep {
    subscribers = new Set();  // 所有受依賴項影響的函式
    
    depend() {  // 收集當前受依賴項影響的函式, 加入subscribers
        if (activeEffect) {
            this.subscribers.add(activeEffect);
        }
    }

    notify() {  // 通知所有受依賴項影響的函式: 依賴項已經被改變, 需要執行函式
        this.subscribers.forEach(effect => effect());
    }
}

// 建立起effect函式與其依賴項的訂閱關係: 當依賴項被改變, 執行effect()
function watchEffect(effect) {
    activeEffect = effect;
    effect();
}

如果我們要使用Dep類,很明顯,依賴項不能是普通的物件,而是需要設定過getter和setter的物件,該物件屬性的getter和setter需要完成的額外功能是:

  • get():當值被讀取時,使該值對應的dep收集依賴
  • set():當值被修改時,通知受該值依賴的函式執行

例如,我們實現上面提到的功能,變數b的值總是變數a的值的10倍:

/* use Dep */
const dep = new Dep();

let a = 0, b = 0;

const state = {};  // 以後需要使用state操作a, 從而實現對a的資料劫持
Object.defineProperty(state, 'a', {
    get() {
        dep.depend();
        return a;
    },
    set(val) {
        if (a !== val) {
            a = val;
            dep.notify();
        }
    }
});

// effect: () => {b = state.a * 10;}
// 首先在watchEffect函式中, 由於執行了effect(), 所以對state.a進行了讀取, dep.depend()新增訂閱
// 於是, 每當state.a的值改變, dep.notify()執行受state.a依賴的effect()
watchEffect(() => {
    b = state.a * 10;
});

console.log(state.a);  // 0
console.log(b);  // 0

state.a = 10;
console.log(b);  // 100

迷你觀察者

我們將上面的convert()函式和Dep類進行結合,就得到了一個迷你觀察者:

function observe(raw) {
    // 1. 遍歷物件的所有key
    Object.keys(raw).forEach(key => {
        // 2. 為每個key建立一個dep物件
        const dep = new Dep();

        // 3. 重寫物件的key屬性
        let realVal = raw[key];
        Object.defineProperty(raw, key, {
            get() {
                dep.depend();  // 4. 讀取key屬性時, 建立依賴
                return realVal;
            },
            set(newVal) {
                realVal = newVal;
                dep.notify();  // 4. key屬性改變時, 通知被依賴的effect
            }
        });
    });
    return raw;
}

我們再實現上面的功能,就會更加簡潔,不用手動重寫getter和setter了:

let obj = {a: 1};  // 依賴項a
let b = 0;
observe(obj);

watchEffect(() => {
    b = obj.a * 10;
});

console.log(b);  // 10 (這是由於在watchEffect()中以及執行過effect()一次了)
obj.a = 20;
console.log(b);  // 200