寫文章不容易,點個讚唄兄弟 專一 Vue 源碼分享,文章分爲白話版和 源碼版,白話版助於理解工做原理,源碼版助於瞭解內部詳情,讓咱們一塊兒學習吧 研究基於 Vue版本 【2.5.17】緩存
若是你以爲排版難看,請點擊 下面連接 或者 拉到 下面關注公衆號也能夠吧bash
今天要記錄 computed 的源碼,有時候想,理解不就行了嗎,爲何要記錄一遍源碼。如今終於想通了學習
過了一段時間以後,你就會忘記你的所謂理解是怎麼來的優化
「哎,爲何會這麼作,關係爲何是這樣,我c....」ui
因而,記錄並簡化源碼,就有助咱們迅速找到根源,解決咱們的疑惑,還能增強咱們的理解this
好吧lua
嗯,這篇文章很長很詳細哦,作好閱讀的準備,唔該spa
咱們重點說明,幾個問題的源碼實現prototype
一、computed 的 月老身份的來源
二、computed 怎麼計算
三、computed 的緩存是怎麼作的
四、computed 何時初始化
五、computed 是怎麼能夠直接使用實例訪問到的
複製代碼
問題不會按順序解析,由於這些問題會互相關聯,在探索源碼的過程當中,你天然會獲得答案
首先,從這個問題開始咱們今天的探索之旅,請看源碼
function Vue(){
... 其餘處理
initState(this)
...解析模板,生成DOM 插入頁面
}
function initState(vm) {
var opts = vm.$options;
if (opts.computed) {
initComputed(vm, opts.computed);
}
.....
}
複製代碼
沒錯,當你調用 Vue 建立實例過程當中,會去處理各類選項,其中包括處理 computed
處理 computed 的方法是 initComputed,下面就呈上 源碼
function initComputed(vm, computed) {
var watchers = vm._computedWatchers =
Object.create(null);
for (var key in computed) {
var userDef = computed[key];
var getter =
typeof userDef === 'function' ?
userDef: userDef.get;
// 每一個 computed 都建立一個 watcher
// watcher 用來存儲計算值,判斷是否須要從新計算
watchers[key] =
new Watcher(vm, getter, {
lazy: true
});
// 判斷是否有重名的屬性
if (! (key in vm)) {
defineComputed(vm, key, userDef);
}
}
}
複製代碼
initComputed 這段代碼作了幾件事
一、每一個 computed 配發 watcher
二、defineComputed 處理
三、收集全部 computed 的 watcher
好的,這三件事,一件一件說哈
computed 到底和 watcher 有什麼貓膩呢?
一、保存 computed 計算函數
二、保存計算結果
三、控制緩存計算結果是否有效
複製代碼
看下 Watcher 源碼構造函數
function Watcher(vm, expOrFn, options) {
this.dirty = this.lazy = options.lazy;
this.getter = expOrFn;
this.value = this.lazy ? undefined: this.get();
};
複製代碼
從這段源碼中,咱們再看 computed 傳了什麼參數
new Watcher(vm, getter, { lazy: true })
複製代碼
因而,咱們就知道了上面說的三個貓膩是怎麼回事
一、保存設置的 getter。
把用戶設置的 computed-getter,存放到 watcher.getter 中,用於後面的計算
二、watcher.value 存放計算結果,可是這裏有個條件,由於 lazy 的緣由,不會新建實例並立刻讀取值
這裏能夠算是 Vue 的一個優化,只有你再讀取 computed,再開始計算,而不是初始化就開始計算值了
雖然沒有一開始計算,可是計算 value 仍是這個 watcher.get 這個方法,來看下源碼(已省略部分代碼,下面講其餘問題,會更詳細展現出來)
這個方法,其實就是執行 保存的 getter 函數,從而獲得計算值,灰常簡單
Watcher.prototype.get = function() {
// getter 就是 watcher 回調
var value = this.getter.call(vm, vm);
return value
};
複製代碼
三、computed 新建 watcher 的時候,傳入 lazy
沒錯,做用是把計算結果緩存起來,而不是每次使用都要從新計算
而這裏呢,還把 lazy 賦值給了 dirty,爲何呢?
由於 lazy 表示一種固定描述,不可改變,表示這個 watcher 須要緩存
而 dirty 表示緩存是否可用,若是爲 true,表示緩存髒了,須要從新計算,不然不用
dirty 默認是 false 的,而 lazy 賦值給 dirty,就是給一個初始值,表示 你控制緩存的任務開始了
因此記住,【dirty】 是真正的控制緩存的關鍵,而 lazy 只是起到一個開啓的做用
具體,怎麼控制緩存,下面會說
請看源碼
function defineComputed(
target, key, userDef
) {
// 設置 set 爲默認值,避免 computed 並無設置 set
var set = function(){}
// 若是用戶設置了set,就使用用戶的set
if (userDef.set) set = userDef.set
Object.defineProperty(target, key, {
// 包裝get 函數,主要用於判斷計算緩存結果是否有效
get:createComputedGetter(key),
set:set
});
}
複製代碼
源碼已經被我簡短不少,可是意思是不變的
一、使用 Object.defineProperty 在 實例上computed 屬性,因此能夠直接訪問
二、set 函數默認是空函數,若是用戶設置,則使用用戶設置
三、createComputedGetter 包裝返回 get 函數
重點就在第三點,爲何重要?
兩大問題都在這裏獲得解決,【月老牽線問題+緩存控制問題】
立刻呈上 createComputedGetter 源碼
function createComputedGetter(key) {
return function() {
// 獲取到相應 key 的 computed-watcher
var watcher = this._computedWatchers[key];
// 若是 computed 依賴的數據變化,dirty 會變成true,
從而從新計算,而後更新緩存值 watcher.value
if (watcher.dirty) {
watcher.evaluate();
}
// 這裏是 月老computed 牽線的重點,讓雙方創建關係
if (Dep.target) {
watcher.depend();
}
return watcher.value
}
}
複製代碼
下面這段代碼做用就是緩存控制,請往下看
if (watcher.dirty) {
watcher.evaluate()
}
複製代碼
一、watcher.evaluate 用來從新計算,更新緩存值,並重置 dirty 爲false,表示緩存已更新
下面是源碼
Watcher.prototype.evaluate = function() {
this.value = this.get();
// 執行完更新函數以後,當即重置標誌位
this.dirty = false;
};
複製代碼
二、只有 dirty 爲 true 的時候,纔會執行 evaluate
全部說經過 控制 dirty 從而控制緩存,可是怎麼控制dirty 呢?
先說一個設定,computed數據A 引用了 data數據B,即A 依賴 B,因此B 會收集到 A 的 watcher
當 B 改變的時候,會通知 A 進行更新,即調用 A-watcher.update,看下源碼
Watcher.prototype.update = function() {
if (this.lazy) this.dirty = true;
....還有其餘無關操做,已被省略
};
複製代碼
當通知 computed 更新的時候,就只是 把 dirty 設置爲 true,從而 讀取 comptued 時,便會調用 evalute 從新計算
月老牽線的意思,在白話版中也說清楚了,這裏簡單說一下
現有 頁面-P,computed- C,data- D
一、P 引用了 C,C 引用了 D
二、理論上 D 改變時, C 就會改變,C 則通知 P 更新。
三、實際上 C 讓 D 和 P 創建聯繫,讓 D 改變時直接通知 P
複製代碼
沒錯,就是下面這段代碼搞的鬼
if (Dep.target) {
watcher.depend();
}
複製代碼
你別看這段代碼短啊,涉及的內容真很多啊且須要點腦筋的,看源碼分分鐘繞不過來,真的服尤大怎麼寫出來的
來看看 watcher.depend 的源碼
Watcher.prototype.depend = function() {
var i = this.deps.length;
while (i--) {
// this.deps[i].depend();
dep.addSub(Dep.target)
}
};
複製代碼
這段的做用就是!(依然使用上面的例子 PCD 代號來講明)
讓 D 的依賴收集器收集到 Dep.target,而 Dep.target 當前是什麼?
沒錯,就是 頁面 的 watcher!
因此這裏,D 就會收集到 頁面的 watcher 了,因此就會直接通知 頁面 watcher
看累了嗎.....
你會問了,爲何 Dep.target 是 頁面 watcher?
你好,這裏內容就有點多了有點繁雜了,坐好了兄die
先送你一份源碼大禮,收好了
Watcher.prototype.get = function() {
// 改變 Dep.target
pushTarget()
// getter 就是 watcher 回調
var value = this.getter.call(this.vm, this.vm);
// 恢復前一個 watcher
popTarget()
return value
};
Dep.target = null;
var targetStack = [];
function pushTarget(_target) {
// 把上一個 Dep.target 緩存起來,便於後面恢復
if (Dep.target) {
targetStack.push(Dep.target);
}
Dep.target = _target;
}
function popTarget() {
Dep.target = targetStack.pop();
}
複製代碼
註解幾個詞
一、頁面 watcher.getter 保存 頁面更新函數,computed watcher.getter 保存 計算getter
二、watcher.get 用於執行 watcher.getter 並 設置 Dep.target
三、Dep.target 會有緩存
下面開始 月老牽線的 詳細流程
一、頁面更新,讀取 computed 的時候,Dep.target 會設置爲 頁面 watcher。
二、computed 被讀取,createComputedGetter 包裝的函數觸發,第一次會進行計算
computed-watcher.evaluted 被調用,進而 computed-watcher.get 被調用,Dep.target 被設置爲 computed-watcher,舊值 頁面 watcher 被緩存起來。
三、computed 計算會讀取 data,此時 data 就收集到 computed-watcher
同時 computed-watcher 也會保存到 data 的依賴收集器 dep(用於下一步)。
computed 計算完畢,釋放Dep.target,而且Dep.target 恢復上一個watcher(頁面watcher)
四、手動 watcher.depend, 讓 data 再收集一次 Dep.target,因而 data 又收集到 恢復了的頁面watcher
再額外記一個data改變後續流程
綜上,此時 data 的依賴收集器=【computed-watcher,頁面watcher】
data 改變,正序遍歷通知,computed 先更新,頁面再更新,因此,頁面才能讀取到最新的 computed 值
三、收集全部 computed 的 watcher
從源碼中,你能夠看出爲每一個computed 新建watcher 以後,會所有收集到一個對象中,並掛到實例上
爲何收集起來,我暫時的想法是
爲了在 createComputedGetter 獲取到對應的 watcher
其實能夠經過傳遞 watcher ,可是這裏的作法是傳遞 key,而後使用key 去找到對應 watcher
哎喲,個人媽,終於寫完了,瞧瞧多少字,7000多,寫得我
場外:尼瑪大部分是源碼好嗎
okok,行行行