首頁 > 軟體

vue3.0響應式函數原理詳細

2022-02-14 13:00:17

前言:

Vue3重寫了響應式系統,和Vue2相比底層採用Proxy物件實現,在初始化的時候不需要遍歷所有的屬性再把屬性通過defineProperty轉換成get和set。另外如果有多層屬性巢狀的話只有存取某個屬性的時候才會遞迴處理下一級的屬性所以Vue3中響應式系統的效能要比Vue2好。

接下來我們自己實現Vue3響應式系統的核心函數(reactive/ref/toRefs/computed/effect/track/trigger)來學習一下響應式原理。

首先我們使用Proxy來實現響應式中的第一個函數reactive

1.reactive

reactive接收一個引數,首先要判斷這個引數是否是一個物件,如果不是直接返回,reactive只能將物件轉換成響應式物件,這是和ref不同的地方。

接著會建立攔截器物件handler, 其中抱哈get,set,deleteProperty等攔截方法,最後建立並返回Proxy物件。

// 判斷是否是一個物件
const isObject = val => val !== null && typeof val === 'object'
// 如果是物件則呼叫reactive
const convert= target => isObject(target) ? reactive(target) : target
// 判斷物件是否存在key屬性
const haOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target, key) => haOwnProperty.call(target, key)

export function reactive (target) {
    if (!isObject(target)) {
        // 如果不是物件直接返回
        return target
    }

    const handler = {
        get (target, key, receiver) {
            // 收集依賴
            const result = Reflect.get(target, key, receiver)
            // 如果屬性是物件則需要遞迴處理
            return convert(result)
        },
        set (target, key, value, receiver) {
            const oldValue = Reflect.get(target, key, receiver)
            let result = true;
            // 需要判斷當前傳入的新值和oldValue是否相等,如果不相等再去覆蓋舊值,並且觸發更新
            if (oldValue !== value) {
                result = Reflect.set(target, key, value, receiver)
                // 觸發更新...
            }
            // set方法需要返回布林值
            return result;
        },
        deleteProperty (target, key) {
            // 首先要判斷當前target中是否有自己的key屬性
            // 如果存在key屬性,並且刪除要觸發更新
            const hasKey = hasOwn(target, key)
            const result = Reflect.deleteProperty(target, key)
            if (hasKey && result) {
                // 觸發更新...
            }
            return result;
        }
    }
    return new Proxy(target, handler)
}

至此reactive函數就寫完了,接著我們來編寫一下收集依賴的過程。

在依賴收集的過程會建立三個集合,分別是targetMap,depsMap以及dep

其中targetMap是用來記錄目標物件和字典他使用的是weakMap,key是目標物件,targetMap的值是depsMap, 型別是Map,這裡面的key是目標物件的屬性名稱,值是一個Set集合,集合中儲存的元素是Effect函數。因為可以多次呼叫同一個Effect在Effect存取同一個屬性,這個時候這個屬性會收集多次依賴對應多個Effect函數。

一個屬性可以對應多個Effect函數,觸發更新的時候可以通過屬性找到對應的Effect函數,進行執行。

我們這裡分別來實現一下effecttrack兩個函數。

effect函數接收一個函數作為引數,我們首先在外面定一個變數儲存callback, 這樣track函數就可以存取到callback了。

let activeEffect = null;
export function effect (callback) {
    activeEffect = callback;
    // 存取響應式物件屬性,收集依賴
    callback();
    // 依賴收集結束要置null
    activeEffect = null;
}

track函數接收兩個引數目標物件和屬性, 他的內部要將target儲存到targetMap中。需要先定義一個這樣的Map。

let targetMap = new WeakMap()

export function track (target, key) {
    // 判斷activeEffect是否存在
    if (!activeEffect) {
        return;
    }
    // depsMap儲存物件和effect的對應關係
    let depsMap = targetMap.get(target)
    // 如果不存在則建立一個map儲存到targetMap中
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    // 根據屬性查詢對應的dep物件
    let dep = depsMap.get(key)
    // dep是一個集合,用於儲存屬性所對應的effect函數
    if (!dep) {
        // 如果不存在,則建立一個新的集合新增到depsMap中
        depsMap.set(key, (dep = new Set()))
    }
    dep.add(activeEffect)
}

track是依賴收集的函數。需要在reactive函數的get方法中呼叫。

get (target, key, receiver) {
    // 收集依賴
    track(target, key)
    const result = Reflect.get(target, key, receiver)
    // 如果屬性是物件則需要遞迴處理
    return convert(result)
},

這樣整個依賴收集就完成了。接著就要實現觸發更新,對應的函數是trigger,這個過程和track的過程正好相反。

trigger函數接收兩個引數,分別是target和key。

export function trigger (target, key) {
    const depsMap = targetMap.get(target)
    // 如果沒有找到直接返回
    if (!depsMap) {
        return;
    }
    const dep = depsMap.get(key)
    if (dep) {
        dep.forEach(effect => {
            effect()
        })
    }
}

trigger函數要在reactive函數中的setdeleteProperty中觸發。

set (target, key, value, receiver) {
    const oldValue = Reflect.get(target, key, receiver)
    let result = true;
    // 需要判斷當前傳入的新值和oldValue是否相等,如果不相等再去覆蓋舊值,並且觸發更新
    if (oldValue !== value) {
        result = Reflect.set(target, key, value, receiver)
        // 觸發更新...
        trigger(target, key)
    }
    // set方法需要返回布林值
    return result;
},
deleteProperty (target, key) {
    // 首先要判斷當前target中是否有自己的key屬性
    // 如果存在key屬性,並且刪除要觸發更新
    const hasKey = hasOwn(target, key)
    const result = Reflect.deleteProperty(target, key)
    if (hasKey && result) {
        // 觸發更新...
        trigger(target, key)
    }
    return result;
}

2.ref

ref接收一個引數可以是原始值也可以是一個物件,如果傳入的是物件並且是ref建立的物件則直接返回,如果是普通物件則呼叫reactive來建立響應式物件,否則建立一個只有value屬性的響應式物件。

export function ref (raw) {
    // 判斷raw是否是ref建立的物件,如果是直接返回
    if (isObject(raw) && raw.__v__isRef) {
        return raw
    }

    // 之前已經定義過convert函數,如果引數是物件就會呼叫reactive函數建立響應式
    let value = convert(raw);

    const r = {
        __v__isRef: true,
        get value () {
            track(r, 'value')
            return value
        },
        set value (newValue) {
            // 判斷新值和舊值是否相等
            if (newValue !== value) {
                raw = newValue
                value = convert(raw)
                // 觸發更新
                trigger(r, 'value')
            }
        }
    }

    return r
}

3.toRefs

toRefs接收reactive函數返回的響應式物件,如果不是響應式物件則直接返回。將傳入物件的所有屬性轉換成一個類似ref返回的物件將準換後的屬性掛載到一個新的物件上返回。

export function toRefs (proxy) {
    // 如果是陣列建立一個相同長度的陣列,否則返回一個空物件
    const ret = proxy instanceof Array ? new Array(proxy.length) : {}

    for (const key in proxy) {
        ret[key] = toProxyRef(proxy, key)
    }

    return ret;
}

function toProxyRef (proxy, key) {
    const r = {
        __v__isRef: true,
        get value () { // 這裡已經是響應式物件了,所以不需要再收集依賴了
            return proxy[key]
        },
        set value (newValue) {
            proxy[key] = newValue
        }
    }
    return r
}

toRefs的作用其實是將reactive中的每個屬性都變成響應式的。reactive方法會建立一個響應式的物件,但是如果將reactive返回的物件進行解構使用就不再

是響應式了,toRefs的作用就是支援解構之後仍舊為響應式。

4.computed

接著再來模擬一下computed函數的內部實現

computed需要接收一個有返回值的函數作為引數,這個函數的返回值就是計算屬性的值,需要監聽函數內部響應式資料的變化,最後將函數執行的結果返回。

export function computed (getter) {
    const result = ref()

    effect(() => (result.value = getter()))

    return result
}

computed函數會通過effect監聽getter內部響應式資料的變化,因為在effect中執行getter的時候存取響應式資料的屬性會去收集依賴,當資料變化會重新執行effect函數,將getter的結果再儲存到result中。

到此這篇關於vue3.0響應式函數原理詳細的文章就介紹到這了,更多相關vue3.0響應式函數原理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


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