【Vue原始碼學習】依賴收集

前端南玖發表於2022-01-29

前面我們學習了vue的響應式原理,我們知道了vue2底層是通過Object.defineProperty來實現資料響應式的,但是單有這個還不夠,我們在data中定義的資料可能沒有用於模版渲染,修改這些資料同樣會出發setter導致重新渲染,所以vue在這裡做了優化,通過收集依賴來判斷哪些資料的變更需要觸發檢視更新。

前言

如果這篇文章有幫助到你,❤️關注+點贊❤️鼓勵一下作者,文章公眾號首發,關注 前端南玖 第一時間獲取最新的文章~

我們先來考慮兩個問題:

  • 1.我們如何知道哪裡用了data裡面的資料?
  • 2.資料變更了,如何通知render更新檢視?

在檢視渲染過程中,被使用的資料需要被記錄下來,並且只針對這些資料的變化觸發檢視更新

這就需要做依賴收集,需要為屬性建立 dep 用來收集渲染 watcher

我們可以來看下官方介紹圖,這裡的collect as Dependency就是原始碼中的dep.depend()依賴收集,Notify就是原始碼中的dep.notify()通知訂閱者

響應式原理.png

依賴收集中的各個類

Vue原始碼中負責依賴收集的類有三個:

  • Observer:可觀測類,將陣列/物件轉成可觀測資料,每個Observer的例項成員中都有一個Dep的例項(上一篇文章實現過這個類)

  • Dep:觀察目標類,每一個資料都會有一個Dep類例項,它內部有個subs佇列,subs就是subscribers的意思,儲存著依賴本資料的觀察者,當本資料變更時,呼叫dep.notify()通知觀察者

  • Watcher:觀察者類,進行觀察者函式的包裝處理。如render()函式,會被進行包裝成一個Watcher例項

依賴就是Watcher,只有Watcher觸發的getter才會收集依賴,哪個Watcher觸發了getter,就把哪個watcher收集到Dep中。Dep使用釋出訂閱模式,當資料發生變化時,會迴圈依賴列表,把所有的watcher都通知一遍,這裡我自己畫了一張更清晰的圖:

vue響應式原理.png

Observer類

這個類我們上一期已經實現過了,這一期我們主要增加的是defineReactive在劫持資料gētter時進行依賴收集,劫持資料setter時進行通知依賴更新,這裡就是Vue收集依賴的入口

class Observer {
     constructor(v){
         // 每一個Observer例項身上都有一個Dep例項
         this.dep = new Dep()
        // 如果資料層次過多,需要遞迴去解析物件中的屬性,依次增加set和get方法
        def(v,'__ob__',this)  //給資料掛上__ob__屬性,表明已觀測
        if(Array.isArray(v)) {
            // 把重寫的陣列方法重新掛在陣列原型上
            v.__proto__ = arrayMethods
            // 如果陣列裡放的是物件,再進行監測
            this.observerArray(v)
        }else{
            // 非陣列就直接呼叫defineReactive將資料定義成響應式物件
            this.walk(v)
        }
        
     }
     observerArray(value) {
         for(let i=0; i<value.length;i++) {
             observe(value[i])
         }
     }
     walk(data) {
         let keys = Object.keys(data); //獲取物件key
         keys.forEach(key => {
            defineReactive(data,key,data[key]) // 定義響應式物件
         })
     }
 }

 function  defineReactive(data,key,value){
     const dep = new Dep() //例項化dep,用於收集依賴,通知訂閱者更新
     observe(value) // 遞迴實現深度監測,注意效能
     Object.defineProperty(data,key,{
         configurable:true,
         enumerable:true,
         get(){
             //獲取值
             // 如果現在處於依賴的手機階段
             if(Dep.target) {
                 dep.depend()
             }
            //  依賴收集
            return value
         },
         set(newV) {
             //設定值
            if(newV === value) return
            observe(newV) //繼續劫持newV,使用者有可能設定的新值還是一個物件
            value = newV
            console.log('值變化了:',value)
            // 釋出訂閱模式,通知
            dep.notify()
            // cb() //訂閱者收到訊息回撥
         }
     })
 }

Observer類的例項掛在__ob__屬性上,提供後期資料觀察時使用,例項化Dep類例項,並且將物件/陣列作為value屬性儲存下來 - 如果value是個物件,就執行walk()過程,遍歷物件把每一項資料都變為可觀測資料(呼叫defineReactive方法處理) - 如果value是個陣列,就執行observeArray()過程,遞迴地對陣列元素呼叫observe()

Dep類(訂閱者)

Dep類的角色是一個訂閱者,它主要作用是用來存放Watcher觀察者物件,每一個資料都有一個Dep類例項,在一個專案中會有多個觀察者,但由於JavaScript是單執行緒的,所以在同一時刻,只能有一個觀察者在執行,此刻正在執行的那個觀察者所對應的Watcher例項就會賦值給Dep.target這個變數,從而只要訪問Dep.target就能知道當前的觀察者是誰。

var uid = 0
export default class Dep {
    constructor() {
        this.id = uid++
        this.subs = [] // subscribes訂閱者,儲存訂閱者,這裡放的是Watcher的例項
    }

    //收集觀察者
    addSub(watcher) {
        this.subs.push(watcher)
    }
    // 新增依賴
    depend() {
        // 自己指定的全域性位置,全域性唯一
      //自己指定的全域性位置,全域性唯一,例項化Watcher時會賦值Dep.target = Watcher例項
        if(Dep.target) {
            this.addSub(Dep.target)
        }
    }
    //通知觀察者去更新
    notify() {
        console.log('通知觀察者更新~')
        const subs = this.subs.slice() // 複製一份
        subs.forEach(w=>w.update())
    }
}

Dep實際上就是對Watcher的管理,Dep脫離Watcher單獨存在是沒有意義的。

  • Dep是一個釋出者,可以訂閱多個觀察者,依賴收集之後Dep中會有一個subs存放一個或多個觀察者,在資料變更的時候通知所有的watcher
  • DepObserver的關係就是Observer監聽整個data,遍歷data的每個屬性給每個屬性繫結defineReactive方法劫持gettersetter, 在getter的時候往Dep類裡塞依賴(dep.depend),在setter的時候通知所有watcher進行update(dep.notify)

Watcher類(觀察者)

Watcher類的角色是觀察者,它關心的是資料,在資料變更之後獲得通知,通過回撥函式進行更新。

由上面的Dep可知,Watcher需要實現以下兩個功能:

  • dep.depend()的時候往subs裡面新增自己
  • dep.notify()的時候呼叫watcher.update(),進行更新檢視

同時要注意的是,watcher有三種:render watcher、 computed watcher、user watcher(就是vue方法中的那個watch)

var uid = 0
import {parsePath} from "../util/index"
import Dep from "./dep"
export default class Watcher{
    constructor(vm,expr,cb,options){
        this.vm = vm // 元件例項
        this.expr = expr // 需要觀察的表示式
        this.cb = cb // 當被觀察的表示式發生變化時的回撥函式
        this.id = uid++ // 觀察者例項物件的唯一標識
        this.options = options // 觀察者選項
        this.getter = parsePath(expr)
        this.value = this.get()
    }

    get(){
        // 依賴收集,把全域性的Dep.target設定為Watcher本身
        Dep.target = this
        const obj = this.vm
        let val
        // 只要能找就一直找
        try{
            val = this.getter(obj)
        } finally{
            // 依賴收集完需要將Dep.target設為null,防止後面重複新增依賴。
            Dep.target = null
        }
        return val
        
    }
    // 當依賴發生變化時,觸發更新
    update() {
        this.run()
    }
    run() {
        this.getAndInvoke(this.cb)
    }
    getAndInvoke(cb) {
        let val = this.get()

        if(val !== this.value || typeof val == 'object') {
            const oldVal = this.value
            this.value = val
            cb.call(this.target,val, oldVal)
        }
    }
}

要注意的是,watcher中有個sync屬性,絕大多數情況下,watcher並不是同步更新的,而是採用非同步更新的方式,也就是呼叫queueWatcher(this)推送到觀察者佇列當中,待nextTick的時候進行呼叫。

這裡的parsePath函式比較有意思,它是一個高階函式,用於把表示式解析成getter,也就是取值,我們可以試著寫寫看:

export function parsePath (str) {
   const segments = str.split('.') // 先將表示式以.切割成一個資料
  // 它會返回一個函式
  	return obj = > {
      for(let i=0; i< segments.length; i++) {
        if(!obj) return
        // 遍歷表示式取出最終值
        obj = obj[segments[i]]
      }
      return obj
    }
}

Dep與Watcher的關係

watcher 中例項化了 dep 並向 dep.subs 中新增了訂閱者, dep 通過 notify 遍歷了 dep.subs 通知每個 watcher 更新。

總結

依賴收集

  1. initState 時,對 computed 屬性初始化時,觸發 computed watcher 依賴收集
  2. initState 時,對偵聽屬性初始化時,觸發 user watcher 依賴收集(這裡就是我們常寫的那個watch)
  3. render()時,觸發 render watcher 依賴收集
  4. re-render 時,render()再次執行,會移除所有 subs 中的 watcer 的訂閱,重新賦值。
observe->walk->defineReactive->get->dep.depend()->
watcher.addDep(new Dep()) -> 
watcher.newDeps.push(dep) -> 
dep.addSub(new Watcher()) -> 
dep.subs.push(watcher)

派發更新

  1. 元件中對響應的資料進行了修改,觸發defineReactive中的 setter 的邏輯
  2. 然後呼叫 dep.notify()
  3. 最後遍歷所有的 subs(Watcher 例項),呼叫每一個 watcherupdate 方法。
set -> 
dep.notify() -> 
subs[i].update() -> 
watcher.run() || queueWatcher(this) -> 
watcher.get() || watcher.cb -> 
watcher.getter() -> 
vm._update() -> 
vm.__patch__()

推薦閱讀

原文首發地址點這裡,歡迎大家關注公眾號 「前端南玖」

我是南玖,我們下一期見!!!

相關文章