Vue原始碼學習(九):響應式前置:實現物件的依賴收集(dep和watcher)

養肥胖虎發表於2023-10-08

好傢伙,這是目前為止最繞的一章,也是十分抽象的一章

由於實在太過抽象,我只能用一個不那麼抽象的例項去說服我自己

 

完整程式碼已開源https://github.com/Fattiger4399/analytic-vue.git

1.我們要做什麼?

來看這個例子,

index.html

setTimeout(() => {
            vm.msg = "張三"
            vm._updata(vm._render())
        },2000)

 

在這個例子中,我們能夠看到hello在msg的值變化後,渲染的值也發生了改變

 

現在,我們要做的事情是

setTimeout(() => {
            vm.msg = "張三"
            // vm._updata(vm._render())
        },2000)
vm._updata(vm._render())將這行程式碼去掉,也能實現相同的效果
原本我要手動更新,現在我引入一個監視者watcher幫我盯著這個資料,
當資料發生改變,自動更新檢視
實現"自動更新"
 
 

2.程式碼實現

程式碼已開源https://github.com/Fattiger4399/analytic-vue.git

專案:

 (紅框為本篇涉及到的檔案)

 

2.1.dep.js

class Dep{
    constructor(){
        this.subs = []
    }
    //收集watcher
    depend(){
        this.subs.push(Dep.target)
    }
    //更新watcher
    notify(){
        this.subs.forEach(watcher =>{
            watcher.updata()
        })
    }
}

//新增watcher
Dep.target = null
export function pushTarget(watcher){
    Dep.target = watcher
}
export function popTarget(){
    Dep.target = null
}
export default Dep

Dep 類中有 subs 陣列,用於存放依賴(即 Watcher 物件)。

depend() 方法用於收集依賴,將當前的 Watcher 物件新增到 subs 陣列中。

notify() 方法用於更新依賴,遍歷 subs 陣列,呼叫各個 Watcher 物件的 update() 方法

 

2.2.index.js中資料劫持部分

//對物件中的屬性進行劫持
function defineReactive(data, key, value) {
    observer(value) //深度代理
    let dep = new Dep() //給每一個物件新增dep
    Object.defineProperty(data, key, {
        get() {
            // console.log('獲取')
            if(Dep.target){
                dep.depend()
            }
            console.log(dep)
            return value
        },
        set(newValue) {
            // console.log('設定')
            if (newValue == value) {
                return;
            }
            observer(newValue)
            value = newValue
            dep.notify()
        }
    })

}

 

2.3.watcher.js

//(1) 透過這個類watcher 實現更新
import { pushTarget,popTarget } from "./dep"
let id = 0
class watcher {
    //cb表示回撥函式,options表示標識
    constructor(vm, updataComponent, cb, options) {
        //(1)將
        this.vm = vm
        this.exprOrfn = updataComponent
        this.cb = cb
        this.options = options
        this.id = id++
        //判斷
        if (typeof updataComponent === 'function') {
            this.getter = updataComponent
        }
        //更新檢視
        this.get()
    }
    //初次渲染
    get() {
        console.log(this,'||this is this')
        pushTarget(this) //給dep 新增  watcher
        this.getter()
        popTarget() //給dep 去除 watcher
    }
    //更新
    updata() {
        this.getter()
    }
}

export default watcher

//收集依賴 vue dep watcher data:{name,msg}
//dep:dep 和 data 中的屬性是一一對應
//watcher:監視的資料有多少個,就對應有多少個watcher
//dep與watcher: 一對多 dep.name = [w1,w2]


//實現物件的收集依賴

初次渲染時,呼叫 get() 方法,會先呼叫 pushTarget() 方法將當前 Watcher 新增到 Dep 中,

然後呼叫 getter() 方法進行渲染,最後呼叫 popTarget() 方法去除該 Watcher。

而在更新時,直接呼叫 update() 方法,也會呼叫 getter() 方法進行更新。

 

2.4.lifecycle.js

 

import {
    patch
} from "./vnode/patch"

import watcher from "./observe/watcher"

export function mounetComponent(vm, el) {
    //原始碼
    callHook(vm, "beforeMounted")
    //(1)vm._render() 將 render函式變成vnode
    //(2)vm.updata()將vnode變成真實dom
    let updataComponent = () => {
        vm._updata(vm._render())
    }
    new watcher(vm, updataComponent,()=>{},true)
    callHook(vm, "mounted")
}

export function lifecycleMixin(Vue) {
    Vue.prototype._updata = function (vnode) {
        // console.log(vnode)
        let vm = this
        //兩個引數 ()
        vm.$el = patch(vm.$el, vnode)
        // console.log(vm.$el, "||this is vm.$el")
    }
}

//(1) render()函式 =>vnode =>真實dom 

//生命週期呼叫
export function callHook(vm, hook) {
    // console.log(vm.options,"||this is vm.options")
    // console.log(hook,"||this is hook")
    // console.log(vm.$options,"||this is vm.$options")
    const handlers = vm.$options[hook]
    if (handlers) {
        for (let i = 0; i < handlers.length; i++) {
            handlers[i].call(this) //改變生命週期中的指向 
        }
    }
}

在掛載方法中新建watcher例項

 

 

於是,到這裡,我們已經實現了與上述等去掉vm._updata(vm._render())前的效果

 

 

接下來是個人的一些思考

3.一些思考

3.1.vue的響應式原理和dep依賴收集的關係?

Vue 的響應式原理是透過利用 Object.defineProperty() 方法攔截物件的訪問和修改,從而實現對資料的觀測。

Vue 在初始化時會遍歷 data 物件的屬性,為每個屬性建立一個 Dep(依賴)物件。

Dep 物件主要負責管理依賴收集和派發更新。

當讀取資料時,會觸發該屬性的 getter 函式,收集依賴。

Dep 物件會儲存當前正在執行的 Watcher(觀察者)物件,將其新增到自身的 subs(訂閱者)陣列中。

在修改資料時,會觸發該屬性的 setter 函式,通知 Dep 物件進行依賴的派發。

Dep 物件會遍歷 subs 陣列,呼叫每個 Watcher 物件的 update() 方法,進而觸發 Watcher 的更新操作。

Watcher 是 Vue 響應式系統的核心,它負責建立響應式資料與檢視之間的關係。

在渲染過程中,Vue 會將模板中涉及到的資料對應的 Watcher 例項化並新增到 Dep 的 subs 陣列中。

當資料發生變化,會觸發相應的 Watcher 進行更新操作,從而實現檢視的重新渲染。(所謂的訂閱釋出)

 

3.2.Dep作用:

 

  1. 收集依賴:Dep 例項內部有一個 subs 陣列,用於儲存依賴(Watcher)的引用。當一個觀察者(Watcher)初始化時,會透過呼叫對應資料的 getter 方法,觸發依賴收集的過程。在這個過程中,Dep.target(當前正在訪問的觀察者)會被新增到 Dep 例項的 subs 陣列中。這樣就建立了資料與觀察者之間的依賴關係。

  2. 觸發更新:當被觀察的資料發生變化時,它的 setter 方法會通知 Dep 例項。Dep 例項會遍歷 subs 陣列,依次呼叫每個依賴(Watcher)的 update 方法,通知觀察者進行更新操作。

  3. 依賴管理:Dep 例項透過 subs 陣列維護了一組觀察者(Watcher)的引用。當資料發生變化時,可以快速找到需要更新的觀察者,避免了不必要的觸發和更新。

  4. 多重依賴管理:在 Vue 中,一個觀察者(Watcher)可以依賴多個資料,一個資料也可以被多個觀察者依賴。透過 Dep 例項和 subs 陣列的組合,可以實現對多個資料和觀察者的管理和通知。

Dep 依賴收集的作用就是在 getter 函式中將 Watcher 物件新增到自己的 subs 陣列中,而 Watcher 透過 update() 方法來觸發檢視的更新,從而保持資料與檢視的同步。

 

3.3.為什麼要收集依賴?

收集依賴的目的是為了建立起資料與檢視之間的關聯關係,

當資料發生變化時,能夠準確地知道需要更新哪些檢視。

在 Vue 中,當資料物件的某個屬性被訪問時,會觸發該屬性的 getter 函式,並在這個時候收集依賴。

這個依賴就是 Watcher 物件,它會被新增到 Dep 物件的 subs 陣列中。這樣在資料發生變化時,就可以遍歷 subs 陣列,依次呼叫每個 Watcher 物件的 update() 方法來更新對應的檢視。

收集依賴的好處有以下幾點:

  1. 減少不必要的更新:只有當資料被訪問時,才會觸發依賴收集的過程。如果資料沒有被使用,那麼就不會收集對應的依賴。這樣可以避免不必要的檢視更新,提升效能。

  2. 精確追蹤依賴關係:透過依賴收集,可以準確地確定哪些資料被使用,以及被使用的地方。當某個資料發生變化時,只需要更新和這個資料相關聯的檢視,而不是所有的檢視。

  3. 動態的更新依賴關係:如果在資料使用過程中有新的 Watcher 註冊進來,依賴收集可以動態地將這個新的 Watcher 新增到對應的依賴中。這樣可以保證當資料變化時,新的 Watcher 也能夠得到通知。

總之,透過收集依賴,可以確保資料與檢視之間的同步更新,提高程式的效率和可靠性。

 

3.4.什麼是收集依賴

收集依賴是一個在觀察者模式中常見的概念,用於建立起觀察者和被觀察者之間的關聯關係

在 Vue.js 中的響應式系統中,當我們訪問一個響應式物件的屬性時,會觸發屬性的 getter 方法。而在 getter 方法中,會有一個收集依賴的過程。

具體來說,收集依賴的過程如下:

  1. 建立一個 Watcher 物件,用於表示當前正在進行資料觀察的例項。

  2. 在建立 Watcher 物件之前,會將這個 Watcher 物件先設定為“活動”的狀態,即將其賦值給一個特定的變數 Dep.target。

  3. 在 getter 方法中,會透過 Dep.target 去判斷當前正在進行依賴收集的 Watcher。如果存在 Dep.target,說明當前正在進行依賴收集的操作,那麼就將這個 Watcher 新增到相關的依賴列表中。

  4. 當依賴列表中收集完所有的 Watcher 後,清空 Dep.target,將其置為 null,表示依賴收集完成。

     

透過收集依賴,我們可以在資料發生變化時,快速找到需要更新的觀察者,從而實現資料與檢視的同步更新。收集依賴的過程是自動進行的,無需手動呼叫,由 Vue 內部的響應式系統自動管理。

收集依賴是實現 Vue 的響應式系統的重要環節之一,它保證了當響應式物件的屬性被訪問時能夠建立起資料與觀察者之間的關聯關係,從而實現了資料的動態更新。

 

相關文章