刺破 Vue 的心臟之 響應式原始碼分析

2021-09-11 10:00:36 字數 3977 閱讀 5421

之前發刺破 vue 的心臟之——詳解 render function code的時候,承諾過會對 vue 的核心過程的各個部分通過原始碼解析的方式進行抽絲剝繭的探索,今天就來進入第二部分響應式原理部分的原始碼解析,承諾兌現得有些晚,求輕拍

還是之前的套路,在讀原始碼之前,先分析原理

上圖來自 vue 官網

深入響應式原理,建議先看看,這裡主要說說我的理解:在初始化的時候,首先通過object.defineproperty改寫 getter/setter 為 data 注入觀察者能力,在資料被呼叫的時候,getter 函式觸發,呼叫方(會為呼叫方建立乙個 watcher)將會被加入到資料的訂閱者序列,當資料被改寫的時候,setter 函式觸發,變更將會通知到訂閱者(watcher)序列中,並由 watcher 觸發 re-render,後續的事情就是通過render function code生成虛擬 dom,進行 diff 比對,將不同反應到真實的 dom 中讀原始碼是一件枯燥的事情,帶著問題去找答案,要更容易讀得進去

...

複製**

為了減少干擾,例子已經剝離得只剩下兩個關鍵的要素,資料屬性 name,以及呼叫了該屬性的計算屬性 cname,這其中 cname 跟 name 就是訂閱者跟被訂閱者的關係。我們現在需要帶著這樣的疑問去閱讀原始碼,cname 是如何成為 name 的訂閱者的,並且當 name 發生了變更的時候,如何通知到 cname 更新自己的資料

響應式處理的原始碼在 src/core/observer 目錄下,見名之意,這使用了觀察者模式,先不用著急進入這個目錄,在 vue 例項初始化的時候,會執行到 src/core/instance/state.js 中相關的狀態初始化邏輯,先到這個檔案來看看:

export

function initstate (vm: component) else , true /* asrootdata */)

} // 初始化計算屬性

if (opts.computed) initcomputed(vm, opts.computed)

...}複製**

我們所關注的初始化資料和初始化計算屬性在這裡都會被執行到,先來分析下 initdata, 沿著方法跟下去,發現最終 initdata 要做的事情是:

// observe data

observe(data, true /* asrootdata */)複製**

呼叫 observe 方法為 data 注入觀察者能力,這個時候我們可以正式進入 observer/index.js 檔案了,在這個檔案我們可以找到 observe 方法的定義,跟著方法讀下去,找到下一步的關鍵訊號:

ob = new observer(value)複製**
這一步通過傳入的 data,建立乙個 observer 例項,再跟到 observer 的建構函式中會發現,建構函式會為 data 的各個元素(當 data 為陣列的時候)或者各個屬性(當 data 為物件的時候)遞迴的建立 observer 物件,最終起作用的方法是 definereactive

export

function definereactive (

obj: object,

key: string,

val: any,

customsetter?: function

) // cater for pre-defined getter/setters

const getter = property && property.get

const setter = property && property.set

let childob = observe(val)

object.defineproperty(obj, key,

if (array.isarray(value))

}return value

},set: function reactivesetter (newval)

/* eslint-enable no-self-compare */

if (process.env.node_env !== 'production' && customsetter)

if (setter) else

childob = observe(newval)

// 當資料發生變更的時候,通知訂閱方進行資料變更

dep.notify()}})

}複製**

**貼得有點多,但著實不是為了湊字數,因為在這裡部分省略可能會帶來一些疑惑,就沒有進行縮減,見諒。這裡主要是通過 object.defineproperty 方法,重寫資料的 set 和 get 方法,當資料被呼叫時,set 方法會將當前的 watcher dep.target 也就是當前的呼叫方加入到該資料的訂閱者序列中,當資料變更,set 方法發通知到所有訂閱者,讓大家重新計算。這其中定義在 observer/dep.js 檔案中的 dep 定義了資料訂閱者的訂閱、取消訂閱等行為,在這裡就不貼**了。

回憶一下我們例項中的 name 和計算屬性 cname,當 cname 的方法執行的時候,name 被呼叫,就會觸發它的 get 方法,這個時候 cname 所對應的 watcher (computed 初始化的時候會為每個計算屬性建立乙個 watcher)。當 name 發生了變更,set 方法被觸發,cname 所對應的 watcher 作為訂閱者就會被通知到,從而重新計算 cname 的值

回到src/core/instance/state.js檔案的計算屬性初始化邏輯 initcomputed,這個方法不負眾望的為計算屬性建立了 watcher 物件

// create internal watcher for the computed property.

watchers[key] = new watcher(vm, getter, noop, computedwatcheroptions)複製**

於是乎我們的視線需要轉移到 observer/watcher.js 中,watcher 的建構函式中最為關鍵的是,this.get 方法的呼叫

this.value = this.lazy

? undefined

: this.get()複製**

在 this.get 方法中有兩步尤為關鍵(對於計算屬性來說,會進行延遲計算,這就是 this.lazy 標誌的意義所在):

get

() catch (e) finally

return value

}複製**

pushtarget(this) 將 watcher 置為 dep.target,當所依賴的資料的 get 方法被呼叫的時候,就可以根據 dep.target 把當前的 watcher 加入到訂閱者序列中。這麼做的目的是,當 watcher 依賴於多個資料的時候,可以共享 dep.target

執行 this.getter.call(vm, vm) 方法計算值,例如計算屬性 cname 的 getter 就是它的定義函式function()。此時依賴方的 get 方法被觸發,整個流程就能串起來,說得通了

對於計算屬性,還有乙個細節,需要將視線再轉移到 initcomputed 中:

export

function definecomputed (target: any, key: string, userdef: object | function) 複製**

它所呼叫的 definecomputed 方法會為計算屬性在當前的元件例項中建立乙個同名的屬性,這也就是為何計算屬性的定義上是方法,但是在實際的使用當中卻是屬性的原因。只有在它所依賴的資料更新的時候,資料通過 set 方法通知到它,它才會重新計算並把值賦給這個新建的**屬性。計算屬性高效就高效在這裡

Vue響應式原始碼分析

初始化資料initdata時呼叫obsreve函式。observe函式主要做了3件事 如果不是物件則啥都不做退出。物件已經是響應式的了,有 ob 了,直接返回這個依賴收集器。物件還不是響應式的,執行new observer observer中主要做了3件事 新建乙個new dep 依賴收集器,定義到...

Vue 響應式原理 原始碼

整個函式結束,相當於初始化所有屬性和vue內建事件 如 emit 並且使所有屬性變為響應式。初始化所有option api 對其中的使用者自定義資料data 進行 observe 此函式用來新建乙個類observer的例項,類observer的constructor中用walk 函式進行遍歷每個屬性...

Vue原始碼剖析(二)響應式

主要是雙向繫結和依賴收集 object.defineproperty,vue.js就是基於它實現 響應式系統 的。我們知道要實現雙向繫結需要給物件通過object.defineproperty新增getter和setter方法。那我們是怎麼來設定vue的呢,我們可以實現乙個observer函式,我們...