希沃ENOW大前端javascript
公司官網:CVTE(廣州視源股份)html
團隊:CVTE旗下將來教育希沃軟件平臺中心enow團隊前端
本文做者:vue
關於vue響應式的文章其實已經挺多了,不過大多都在淺嘗輒止,基本就是簡單介紹一下Object.defineProperty
,覆蓋一下setter
作個小demo
就算解決,好一點的會幫你引入observe、watcher、dep
的概念,以及加入對Array
的特殊處理,因此本篇除了上述之外,更多的重心將放在setter
引起render
的機制與流程上,而後結合這個這個響應式機制解析vue
中的watch
和computed
語法實現java
文章分爲兩部分,第一部分會簡單介紹vue實例構建流程,第二部分則深刻探究響應式實現。react
建議對照源碼閱讀文章,由於不少本文不少地方會直接指出文件路徑,同時將省略部分代碼而直述功能express
版本信息:數組
直入主題markdown
真正的vue
實例在core/instance/index
中能夠找到數據結構
function Vue (options) {
....
this._init(options) // 這個方法在initMixin中定義
}
initMixin(Vue) // 掛載_init()
stateMixin(Vue) // 掛載狀態處理方法(掛載data,methods等)
eventsMixin(Vue) // 掛載 事件 的方法($on,$off等)
lifecycleMixin(Vue) // 掛載 生命週期方法(update,destory)
renderMixin(Vue) // 掛載與渲染有關的方法($nextTick,_render)
複製代碼
每一個方法能夠按照代碼邏輯來看,實現對應功能,這裏拿initMixin
舉例
篇幅有限,全部此處僅解釋initMixin邏輯,剩餘幾個方法你們能夠本身探索哦
initMixin
中僅僅掛載了_init()
方法,在_init
中,初始化了整個vue
的狀態:
function _init(option) {
...
vm._uid = uid++ // 即component id
...
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm)
callHook(vm, 'created')
...
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 開始掛載
}
}
複製代碼
這裏咱們能夠看到幾個beforeCreate
,created
和Mount
關鍵字,大概就可以猜到vu
e實例的部分生命週期方法就是在這裏進行了掛載,再結合 vue官方文檔的圖示
關於初始化整個vue
的狀態,能夠舉例來講,例如initLifecycle
中就賦值了parent,children
,以及一些isMounted,isDestroy
的標識符。initRender
中就將attrs,listeners
響應化,等等,諸如此類。
從initMixin=>initState=>initData
,即可以看到掛載props,methods,data,computed,watch
了,
能夠看到,此處先掛載了
props,methods
,而後是data
的順序,其實再往下探究邏輯就能夠知道,若是存在變量重名,優先級是props>methods>data
的,這也就解釋了爲何初始化的順序是這樣安排的
在initData
中,先是獲取了data
數據,判斷props,methods
變量重名問題,而後是走了一個代理,將變量名代理到vue
實例上,這樣的話你的vue
實例中,使用this.x
指向就能夠訪問到this.data.x,
這類代理也用在了props
和methods
中
在
initData
獲取數據中能夠看到一個判斷typeof data === 'function' ? getData(data, vm) : data || {}
, 支持兩種方式獲取,實際上若是是本身寫這樣一個邏輯是會藏有隱患的,若是你的data是直接使用對象,而js的複雜數據類型是地址引用,這意味着,你實例化了兩個vue
對象,實際上他們的data引用地址是同一個地址,對其中一個vue data
的修改會觸發另外一個vue數據的變更,帶來的問題是巨大的
export function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製代碼
這個邏輯處理的設計也是很是巧妙,他覆蓋了實例中對該key的訪問,使用setter
和getter
將實際訪問指向了this.data[key]
。
這裏能夠說一下computed的邏輯,實際上也是取巧使用了本來用於data的響應式邏輯,其實看到上面貼出來的proxy代碼,大概就能猜到,既然proxy可以改變一個變量讀取的指向,那麼他也能創造一個虛假變量的指向,這個創造出來的這個變量實際上就是computed所使用的變量,將每次computed函數賦給getter,再加上響應式處理,就徹底實現了computed,
走到最後,就是observe(data)
,也就是開始處理vue數據的雙向綁定
不一樣於react
的單向數據流,vue
使用的雙向綁定,單向數據流能夠理解爲當源頭的數據發生變更則觸發行爲,固然這個變更是主動的,即你須要setState
才能觸發,而雙向綁定則能夠抽象爲,每個數據旁邊都有一個監護人(一種處理邏輯),當數據發生變化,這個監護人就會響應行爲,這個流程是被動發生的,只要該數據發生變更,就會經過監護人觸發行爲。
若是你以前有過了解,大概就會知道,js每一個數據的變更都是經過Object
原型鏈中的setter
去改變值,而若是你在他改變值以前,去通知監護人,就可以實現上述的邏輯,這一點不少博客文章都寫的很是清楚了。
接着第一部分的initData
知道最後observe(data)
,這裏開始正式處理響應式。
前面一直提到,經過Object
的原型鏈改變對象的默認行爲:getter
和setter
,首先咱們須要知道,在js
中,讀取一個對象的值並非直接讀取,而是經過Object的原型鏈上的默認行爲getter拿到對應的值,而改變這種行爲其實是經過Object.defineProperty
,來從新定義一個對象的getter
和setter
,在/src/core/observer/index.js
中咱們能夠看一個defineReactive
方法,他就是vue
用來實現這種行爲的方法,也是這個響應式的核心
function defineReactive(obj, key, val, ... ) {
// 此處須要保留getter、setter是由於,開發者可能本身基於defineProperty已經作過一層覆蓋,
// 而響應式又會覆蓋一次,因此爲了保留開發者本身的行爲,此處須要兼容原有的getter、setter
const getter = property && property.get // 拿到默認的getter、setter行爲
const setter = property && property.set
Object.defineProperty(obj, key, {
enumerable: true, // 是否能夠被枚舉出來(例如Object.keys(),for in)
configurable: true, // 是否能夠被配置,是否能夠被刪除
get: function() {
const value = getter ? getter.call(obj) : val
...
return value
}
set: function(newVal) {
...
setter.call(obj, newVal)
}
})
}
複製代碼
首先,咱們猜測一下,雙向綁定的行爲,數據可以響應行爲的變化,而行爲又可以操做數據的改變,雖然有部分教程會讓你站在數據的角度去理解這種行爲,實際上,咱們站在行爲的角度上去理解是更加方便的。
咱們將一種行爲定義爲一個Watcher
,他有多是一個vue
文件的template
中的dom
節點渲染行爲,也有多是computed
的計算值行爲,總之,咱們從行爲的角度出發,一個行爲的發生,會伴隨着對變量的讀取(回想一下咱們在vue
文件中的template
寫html
標籤時,老是會使用{{obj.xxx}}
來讀取某個變量並渲染),咱們想要實現,變量的改變也會帶動這個行爲的從新渲染,是否是咱們只須要在首次行爲發生的週期內,在讀取某個變量時,在這個變量內記錄這個Watcher
,這樣的話,下次變量的改變時,我只要觸發我以前記錄過的Watcher
就好了。因此,咱們只須要在一個Watcher
發生時,將其掛載到一個公共變量上,這樣在讀取一個值的時候,記錄這個公共變量,就可以實現上述操做。
這裏先不解釋Dep的做用,能夠將其抽象理解爲一個被掛載在數據上的數組,每次這個數據被一個watch讀取時,就會將這個watch記錄下來
既然說到將一種行爲定義爲一個watcher
,那麼能夠在/src/core/observer/watcher.js
中看到Watcher
的實體類,而咱們以前一直所說的「行爲」,實際上就是構造器的第二個參數expOrFn
,能夠有表達式或者函數讀取的兩種模式
class Watcher {
constructor ( vm: Component, // vue實例 expOrFn: string | Function, // 行爲 cb: Function, // 爲watch服務 options?: ?Object, isRenderWatcher?: boolean // 判斷是否爲渲染watcher, )
}
複製代碼
接着來看一種最典型的watcher行爲,在/src/core/instance/lifecycle.js
中的moundComponent
方法中,能夠看到一個實例化watcher
的方法
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
複製代碼
能夠看到,他將updateComponent
(能夠抽象爲渲染行爲)傳給Watcher
,而在Watcher
的實例化中,將會執行此方法,固然在執行以前,pushTarget(this)
,將這個watcher掛載到公共變量上然後開始執行渲染行爲,
class Watch {
constructor(...) {
....
if (typeof expOrFn === 'function') {
this.getter = expOrFn
}
this.get();
}
get() {
pushTarget(this) // 掛載行爲至公共Target
value = this.getter.call(vm, vm) // 開始執行行爲,之因此會有返回值是爲了computed服務
popTarget() // 取消掛載,避免下次讀取變量時又會綁定此行爲
}
}
複製代碼
此時,若是此行爲讀取了某個響應式變量,那麼該變量的getter
將會存儲公共變量target
,當行爲完成後就會取消行爲的掛載,這個時候咱們再回過頭來看前面的defineReactive
的邏輯
function defineReactive(obj, key) {
const dep = new Dep(); // 每一個數據都有一個本身的存儲列表
const getter = property && property.get
const setter = property && property.set
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) { // 判斷公共變量中是否掛載了行爲(watcher)
dep.depend() // 將行爲(watcher)加入dep(即此變量的存儲行爲列表)
...
}
return value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return // 判斷變量沒有變化,則直接返回(後二者判斷則是由於NaN!==NaN的特性)
}
if (setter) {
setter.call(obj, newVal) // 開始
} else {
val = newVal
}
dep.notify() // 通知本身這個數據的存儲列表,數據發生改變,須要從新執行行爲(watcher)
}
});
}
複製代碼
這個時候就很清晰明瞭了,這就是不少博客文章所說的依賴收集,變量在get時經過公共變量Target
收集依賴(也就是本文所說的行爲),在set
時,即變量數據發生改變時,觸發更新notify
;
前文有大體介紹computed
的實現,實際上在介紹完Wacher以後就能夠來詳細介紹了,計算屬性computed
並無實際的變量,他經過原型鏈覆蓋創造了一個變量指向(src/core/instance/state.js
的initComputed
),回憶一下computed的兩種寫法
'fullName': function() {
return this.firstName + this.secondeName;
}
'fullName': {
get: function () {...},
set: function() {...},
}
複製代碼
咱們再來看一下initComputed
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
// 對照着computed的兩種寫法,就能理解爲何這裏有這樣的判斷,
const getter = typeof userDef === 'function' ? userDef : userDef.get
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
{ lazy: true }
)
defineComputed(vm, key, userDef) // 經過defineProperty來創造一個掛載在vm上key(fullName)的指向
}
}
複製代碼
能夠看到,他將computed
的getter
方法,做爲Watcher
的行爲傳遞了進去,這樣在執行getter
時,能夠將此行爲綁定至過程當中所讀取到的變量(firstName
),如此,再下次firstName發生改變時,就會觸發此Watcher
,從新運行getter方法,獲得一個新的fullName
的值(還記得前文class Watch
中的value = this.getter.call(vm, vm)
嗎?這個返回值就是computed
的返回值),這樣就實現了computed
的邏輯
watch
的用法,是監聽某個變量,當該變量發生變化時,執行特定的邏輯,
上文提到的兩種Watcher
行爲都是函數行爲,可是Watcher
的行爲是支持函數或者表達式的(expOrFn
),因此此處的exp(expression)
這裏就是能夠提現到的,咱們只須要在變量發生變化時,執行watch
定義的邏輯便可,
還記得前文代碼defineReactive
中 set
方法通知依賴更新(dep.notify()
),雖然前文一直爲了方便理解,將Dep描述爲一種抽象的列表結構,僅用於依賴收集,但實際上他是一個單獨的數據結構,
let uid = 0;
class Dep {
constructor() {
this.id = uid ++;
this.subs = []; // 真正用於收集依賴的數據
}
depend () { // 依賴收集
if (Dep.target) {
Dep.target.addDep(this)
}
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
notify() { // 變量值發生變化,通知更新
// 遍歷全部收集的依賴,注意觸發更新,
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
...
}
Dep.target = null; // 這就是一直說的,用於掛載Watcher行爲的公共變量
function pushTarget(target){ Dep.target = target };
function popTarget() { Dep.target = null };
複製代碼
實際上這裏的靜態變量
target
以及pushTarget、popTarget
是通過簡化的,由於渲染並非一個單一的行爲,他是層層嵌套的行爲,因此在綁定響應式時,也是須要區分該變量究竟是要綁定至哪一個行爲(不然每一個變量都綁定最頂層的行爲,一個變量的變化,將會引起整個頁面的update
),所以真正的target是還有一個stack
棧結構,用於掛載多個嵌套的行爲
能夠看到,每次變量更新,都會觸發watcher.update
,那麼對於watch
監聽的回調,就能夠放到在update
中調用
class Watch {
constructor(vm, expOrFn, cb, ...) {
this.cb = cb // 這個cb就是watch監聽的回調
}
update() {
this.run()
}
run() {
...
this.cb.call(this.vm, ...)
}
}
複製代碼
至此,關於watch
監聽的實現邏輯大體就是如此
關於依賴收集,實際上並非在get變量時,直接將
watcher
綁定至Dep
中,能夠看到Dep.depend()
,他先通知行爲(watcher
),叫他先綁定本身,而後watcher綁定完dep以後,纔會回過頭,告知Dep
要addSub()
,這裏的邏輯像是一個圈
因此如今咱們回過頭來看,前文說了,每一個數據都有一個「監護人」,來記錄此數據所綁定的行爲,那麼這個「監護人」到底在哪裏呢? 能夠看到/src/core/observer/index.js
的class Observer
中,
class Observer {
constructor(val) {
...
def(value, '__ob__', this) // 對value定義__ob__屬性,掛載此object
...
}
}
複製代碼
對於每一份須要響應式處理的數據,都會掛載一個Observer
實例,其內subs就是用於記錄綁定此數據的Watcher
,同時也能夠看到,這份數據的get、set
方法已是被重寫過了,也就是前文的defineReactive
中的覆蓋行爲。
其實對於Array
的響應式是須要特殊處理的,由於他除了set、get
以外,還會對數組進行增減操做(splice
等),而這些操做是set沒法捕捉的,因此覆蓋get、set
顯然沒法實現數組的響應式,而vue
中採用的是直接覆蓋數組的原型鏈中會對數據自己改變的方法(push、shift、splice
等),/src/core/observer/array.js
整個文件就是對數據的特殊處理 最新的vue3
中,使用了ES6
的proxy
特性來替代這種覆蓋set、get
實現響應式行爲,這種模式同時也可以處理Array
。
vue
的源碼固然沒有如此簡單,不少東西文章都沒有涉及到,譬如說,經過上面的邏輯其實你能夠發現,dep
和watcher
實際上是互相引用的,而js
的垃圾回收是檢測變量引用的機制,因此若是是簡單的複製上文的邏輯,最終的這部分的內存實際上是沒法被回收的,須要你手動清除,固然vue
中也作了這樣的處理(每一個vm
下其實有一個watcherList
,用於記錄這個示例中全部使用到的watcher
,再vm.destroy
時,經過遍歷watcherList
,再銷燬每個watcher
,而watcher
中又會本身銷燬Dep
),可是限於篇幅緣由沒法詳細介紹了。