vue的響應式原理:依賴追蹤

路澤宇發表於2023-12-05

在明白原理之前,我們有很多表面現象、使用場景需要記憶。明白了原理後,你會發現它們已經不需要記了,因為從原理出發,你自己都能把它們推匯出來,一切是那麼的自然而然。感覺就是:這還用記嗎?很明顯嘛!

之前我對vue的響應式原理,只是一知半解,導致開發中經常會出現疑問,比如:為什麼有的資料它不響應?模板中用到的methods方法什麼時候會執行?什麼時候模板會重新渲染?渲染的過程是什麼等等。所有的這些開發過程中的疑惑,都是因為不瞭解底層原理造成的。

今天我們就來一起捋一下,vue的響應式原理。當然,只是入門級的,可以幫助和我一樣不瞭解原理的同學,大佬勿噴。

 

一、資料劫持:getter和setter

在vue的data初始化階段,vue會遞迴地遍歷data的每一個屬性,把它們處理成響應式資料。這是一個深層次的遍歷,也就是說data的屬性如果是一個物件,這個物件的屬性也是響應式的,不管巢狀幾層。

具體來說,vue對每一個屬性執行Object.defineProperty(),把每一個屬性轉換為getter和setter,以此實現對屬性取值、賦值的劫持,稱為資料劫持。

 

二、watcher和dep

我們知道,模板中會用到data的資料,計算屬性也是如此,它們會各用一個列表來儲存自己用到了哪些data資料,稱為依賴列表。

而data的屬性,則可能會被模板以及多個計算屬性用到,它也會用一個列表來儲存哪些模板或計算屬性用到了自己,也叫依賴列表。

模板和計算屬性,透過watcher物件來做這件事,依賴列表存放在watcher的一個陣列裡。每一個vue例項,有一個watcher,稱為渲染watcher。每一個計算屬性,各自有一個wathcer,稱為計算屬性watcher。

data的屬性,透過dep物件來做這件事,依賴列表存放在dep的一個陣列裡。data的每一個屬性,都有一個dep物件。

watcher的這個陣列,成員是dep物件。dep的這個陣列,成員是watcher物件。

也就是說,透過維護對方的列表,模板和計算屬性,知道我用到了哪些屬性。data的屬性,也知道哪些模板和計算屬性用到了我。

 

三、依賴收集

在模板第一次渲染、計算屬性第一次被使用時,它們所依賴屬性的getter會觸發,然後就把這個模板或計算屬性的watcher新增到該屬性的依賴列表裡(dep物件的陣列)。

同時,這些屬性的dep物件,也會被新增到模板或計算屬性的依賴列表裡(watcher物件的陣列)。

這個過程是雙向的。我曾經疑問為什麼需要在watcher裡維護依賴列表?因為看上去,屬性更新時,通知它的依賴列表裡的每一個watcher,讓它們去更新,這個模型似乎就可以了。

原來,有時我們是需要模板主動更新的,比如$forceUpdate函式,這時透過watcher的依賴列表,就可以檢視這些依賴有沒有更新,如果都沒有更新,就無需重新渲染,提高了效能。

 

四、依賴更新

在一個屬性發生變化時,這個屬性的setter被觸發,它會通知依賴列表裡的每一個watcher,讓它們去更新。

渲染watcher接到通知,會重新渲染頁面。計算屬性watcher接到通知,會進行重新計算。

實際的模型比這要複雜。元件的更新過程是非同步的,當被通知重新渲染時,不會立即觸發,而是將元件標記為“待更新”。Vue 使用一個非同步佇列來批次處理這些更新,以提高效能。這意味著在同一事件迴圈中,多次改變資料只會導致一次元件更新和重新渲染。

同樣,通知計算屬性重新計算,也不會立即觸發,而是把計算屬性標記為“待更新”,直到該計算屬性下一次被使用時(比如重新渲染),才會重新計算。

 

五、原理之上的應用

明白了原理,我們可以弄清楚很多問題,比如:

(1)vue中的哪些資料是響應式的?

props、data、computed:前兩個我們好理解,這裡需要注意的是計算屬性。思考下面一個問題:

模板中用到一個計算屬性,那麼它的渲染watcher的依賴列表裡,是這個計算屬性,還是這個計算屬性所依賴的data屬性?

答案是:這個計算屬性。這是因為,計算屬性本身也是響應式的,同樣會被Object.defineProperty處理。計算屬性的效果就是一層快取,它不僅會被模板用到,還可能被其他計算屬性用到。在這個案例中,當計算屬性依賴的data改變時,會先觸發計算屬性的重新計算,只有計算後的值和原來不同,模板才會重新渲染,反之,就無需重新渲染。

另外,$route和$store.state也是響應式的,原理和其他的一樣。意味著,如果模板中用到了它倆,它倆改變時模板是會重新渲染的(計算屬性也一樣,會重新計算)。

(2)我們知道,一個模板中會用到各種資料:data屬性、計算屬性、表示式、methods中的方法、全域性的自定義函式。那麼當模板重新渲染時,它們各自會怎麼樣呢?

計算屬性:只有計算屬性的依賴發生變化時,它才會在重新渲染時重新計算。前者會把計算屬性標記為“待更新”,重新計算則會等到下一次被使用(比如重新渲染)時才會進行。

表示式、methods中的方法、全域性的自定義函式:每次重新渲染都會重新計算,因為它們的值不會被快取,所以要儘可能多的使用計算屬性。

(3)什麼會觸發元件的重新渲染?

元件只有在模板依賴的資料發生變化時,才會重新渲染。那些模板中沒用到的資料,改變並不會讓模板重新渲染。並且,這種依賴是屬性級別的,也就是說,模板中用到了data中的一個物件,但這個物件的改變不一定導致重新渲染,因為改變的屬性不一定是模板用到的那個。

父元件和子元件,它們的渲染也沒有必然的聯絡。子元件的data發生變化,不會導致父元件重新渲染,因為父元件不會用到子元件的資料。父元件的data發生變化,也只有它自己,和用到該資料的子元件會重新渲染。不過要注意,如果父元件是銷燬了重新建立,那麼子元件也只能跟著銷燬重新建立。另外,如果父元件對子元件使用了v-if、v-for(搭配key使用)、key,那麼子元件很可能會隨著它們的變化而銷燬重建。

(4)為什麼vue無法監聽物件和陣列的某些操作?

明白了vue2的響應式原理,也就理解了為什麼,vue無法監聽到物件屬性的新增和刪除,因為vue2只能劫持物件屬性的取值和賦值。想給響應物件新增屬性,應該使用Vue.set()或者this.$set()。

陣列的限制是,無法監聽到透過索引直接賦值和修改陣列的長度。我暫時無法解釋,不過我的方法時統一用splice方法來替代。

 

本人水平非常有限,寫作主要是為了把自己學過的東西捋清楚。如有錯誤,還請指正,感激不盡。