提到計算屬性,咱們立刻就會想到它的一個特性:緩存,Vue 文檔也如是說:javascript
計算屬性是基於它們的響應式依賴進行緩存的vue
那麼計算屬性如何緩存的呢,計算屬性的觀察者是如何進行依賴收集的呢,接下來深刻原理看一下。java
本文須要對基礎的響應式原理和幾個關鍵角色 Observer、Dep、Watcher
等有必定的瞭解,可參考 Observer、Dep、Watcher 傻傻搞不清楚node
以一個簡單的例子開始:緩存
<div id="app">
<h2>{{ this.text }}</h2>
<button @click="changeName">Change name</button>
</div>
const vm = new Vue({
el: '#app',
data() {
return {
name: 'xiaoming',
}
},
computed: {
text() {
return `Hello, ${this.name}!`
}
},
methods: {
changeName() {
this.name = 'onlyil'
},
},
})
複製代碼
初始展現 Hello, xiaoming!
,點擊按鈕後展現 Hello, onlyil!
app
仍是從 vue 初始化看起,從 new Vue()
開始,構造函數會執行 this._init
,在 _init
中會進行合併配置、初始化生命週期、事件、渲染等,最後執行 vm.$mount
進行掛載。異步
// src/core/instance/index.js
function Vue (options) {
// ...
this._init(options)
}
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
// 合併選項
// ...
// 一系列初始化
// ...
initState(vm)
// ...
// 掛載
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
複製代碼
計算屬性的初始化就在 initState
中:函數
// src/core/instance/state.js
export function initState (vm: Component) {
const opts = vm.$options
// ...
// 初始化 computed
if (opts.computed) initComputed(vm, opts.computed)
// ...
}
複製代碼
看一下 initComputed
:oop
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = Object.create(null)
// 遍歷 computed 選項,依次進行定義
for (const key in computed) {
const getter = computed[key]
// 爲計算屬性建立內部 watcher
watchers[key] = new Watcher(
vm,
getter || noop, // 計算屬性 text 函數
noop,
computedWatcherOptions // { lazy: true } ,指定 lazy 屬性,表示要實例化 computedWatcher
)
// 爲計算屬性定義 getter
defineComputed(vm, key, userDef)
}
}
複製代碼
首先定義一個 watchers
空對象,同時掛在 vm._computedWatchers
上,用來存放該 vm 實例的全部 computedWatcher。post
遍歷 computed 選項並實例化 watcher,參數中的 getter
就是上邊示例中計算屬性 text
對應的函數:
function () {
return `Hello, ${this.name}!`
}
複製代碼
參數中的 computedWatcherOptions
爲 { lazy: true }
,指定 lazy 屬性,表示要實例化的是 computedWatcher。
實例化 computedWatcher :
class Watcher {
constructor(vm, expOrFn, cb, options) {
// options 爲 { lazy: true }
if (options) {
// ...
this.lazy = !!options.lazy
// ...
}
this.dirty = this.lazy // for lazy watchers, 初始 dirty 爲 true
this.getter = expOrFn
// lazy 爲 true,不進行求值,直接返回 undefined
this.value = this.lazy
? undefined
: this.get()
}
}
複製代碼
執行構造函數時指定 lazy
dirty
爲 true,最後執行 watcher.value = undefined 並未執行 get
方法進行求值。何時求值呢?後面就知道了。
回到上邊,爲計算屬性建立內部 watcher 以後的 watchers 對象是這樣的:
{
text: Watcher {
lazy: true,
dirty: true,
deps: [],
getter: function () {
return `Hello, ${this.name}!`
},
value: undefined, // 直接賦值爲 undefined ,
}
}
複製代碼
看一下 defineComputed
作了什麼:
function defineComputed(target, key, userDef) {
Object.defineProperty(target, key, {
get: function () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
})
}
複製代碼
有沒有很熟悉,在定義響應式數據的 defineReactive
方法中也使用了 Object.defineProperty
方法來定義訪問器屬性。這裏在該 vm 實例上定義了 text 屬性,每當訪問到 this.text
時就會執行對應的 getter ,函數作了什麼暫時能夠不看。那何時會讀取到 this.text
呢?答案在下邊。
總結一下 computed 初始化過程:
vm._computedWatchers
用來存放該 vm 實例的全部 computedWatcherdefineComputed
定義計算屬性的 getter ,等待後邊讀取時觸發初始化完成後,會進入 mount 階段,在執行 render
生成 vnode 時會讀取到計算屬性 text
,本文示例的 render 函數是這樣:
function render() {
var h = arguments[0];
return h("div", [
h("h2", [this.text]), // 這裏讀取了計算屬性 text
h("button", {
"on": {
"click": this.changeName
}
}, ["changeName"]),
]);
}
複製代碼
這時會觸發計算屬性的 getter ,也就是上邊定義的訪問器屬性:
get: function () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 此時 dirty 爲 true ,進行求值
if (watcher.dirty) {
// 求值,對 data 進行依賴收集,使 computedWatcher 訂閱 data
// 這裏的 data 就是 "this.name"
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
複製代碼
取出 vm._computedWatchers
中對應的 watcher ,此時 watcher.dirty
爲 true,執行 watcher.evaluate()
。
// watcher.evaluate
evaluate() {
this.value = this.get()
this.dirty = false
}
複製代碼
這個函數作了兩件事:
先看求值:
// watcher.get
get() {
pushTarget(this) // Dep.target 置爲當前 computedWatcher
let value
const vm = this.vm
try {
// 觸發響應數據的依賴收集
value = this.getter.call(vm, vm)
} catch (e) {
// ...
} finally {
popTarget() // Dep.target 從新置爲渲染 watcher
}
return value
}
複製代碼
這裏須要知道一個前置內容,全局的 Dep.target
存的是當前正在求值的 watcher,它是用一個棧 targetStack
來維護的。當前是在渲染過程當中,因此此時棧是這樣:[ 渲染watcher ]。
function pushTarget(target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
複製代碼
首先 pushTarget(this)
, Dep.target
成爲當前 computedWatcher,此時棧是這樣的:[ 渲染watcher, computedWatcher ]。
而後執行 getter 觸發響應數據的依賴收集。再回顧一下 getter :
function () {
return `Hello, ${this.name}!`
}
複製代碼
很顯然執行這個函數會讀取 this.name
,name 的 dep 就會收集當前 computedWatcher ,當前 computedWatcher 就會訂閱 name 的變化(這裏不作詳細介紹,參考響應式原理的依賴收集)。
收集完成後執行 popTarget()
,Dep.target
從新成爲渲染 watcher ,此時的棧是這樣:[ 渲染watcher ]。
而後返回計算得出的值,再將 dirty 置爲 false 。此時 name 的 dep 是這樣的:
{
id: 3,
subs: [ computedWatcher ], // 收集了 computedWatcher
}
複製代碼
computedWatcher 是這樣的:
{
dirty: false, // 求值完成,dirty 置爲 false
deps: [ name 的 dep ], // 訂閱了 name
value: "Hello, xiaoming!",
}
複製代碼
此時計算屬性的 get 訪問執行到了這裏:
get: function () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 求值...
// 執行到了這裏
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
複製代碼
此時 Dep.target
是渲染 watcher ,因此會執行 watcher.depend()
。
// watcher.depend
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
複製代碼
能夠看到將 computedWatcher 訂閱的 deps 依次執行 dep.depend
,熟悉響應式原理的應該立刻就知道了,這是在對這些 dep 的響應式數據進行依賴收集,也就是對示例中的 name
進行依賴收集,收集的是誰呢?上面提到此時的 Dep.target
是渲染 watcher ,那麼總結下來,這一步作的是:
讓 computedWatcher 訂閱的響應式數據收集渲染 watcher
這一步操做以後 name
的 dep 是這樣的:
{
id: 3,
subs: [ computedWatcher, 渲染 watcher ], // 收集了渲染 watcher
}
複製代碼
最後,返回 watcher.value ,get 訪問結束,render
函數繼續往下走,以後渲染出最終頁面。
當點擊按鈕時,執行 this.name = 'onlyil'
,會觸發 name
的訪問器屬性 set ,執行 dep.notify()
,依次觸發它所收集的 watcher 的更新邏輯,也就是 [ computedWatcher, 渲染 watcher ]
的 update 。
// watcher.update
update() {
// computedWatcher 的 lazy 爲 true
if (this.lazy) {
this.dirty = true
}
// ...
}
複製代碼
只作了一件事,就是將 dirty 置爲 true,表示該計算屬性「髒」了,須要從新計算,何時從新求值呢,往下看。
// watcher.update
update() {
// ...
//
queueWatcher(this)
}
複製代碼
這裏就是加入異步更新隊列,最終又會執行到 render
函數來生成 vnode ,同首次渲染同樣,在 render
過程當中又會讀取到計算屬性 text
,再次觸發它的 getter :
get: function () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 此時 dirty 爲 true ,"髒"了
if (watcher.dirty) {
// 從新求值
watcher.evaluate()
}
// ...
return watcher.value
}
}
複製代碼
從新求值也就會從新執行咱們在 computed 選項定義的函數,頁面就展現了新值 Hello, onlyil!
function () {
return `Hello, ${this.name}!`
}
複製代碼
至此,更新結束。
經過上邊的過程分析,能夠作出以下總結:
dirty: false
,在 render 過程當中求值並進行依賴收集;name
改變時,觸發 computedWatcher 的更新,修改 dirty: true
;text
,發現 dirty
爲 true ,從新求值,頁面視圖更新。能夠發現一個關鍵點,computedWatcher 的更新只作了一件事:修改 dirty: true
,求值操做始終都在 render 過程當中。
如今咱們修改示例,新增一個數據 count
和方法 add
:
<div id="app">
<h2>{{ this.text }}</h2>
<h2>{{ this.count }}</h2>
<button @click="changeName">Change name</button>
<button @click="add">Add</button>
</div>
const vm = new Vue({
el: '#app',
data() {
return {
name: 'xiaoming',
count: 0,
}
},
computed: {
text() {
return `Hello, ${this.name}!`
}
},
methods: {
changeName() {
this.name = 'onlyil'
},
add() {
this.count += 1
},
},
})
複製代碼
點擊 Add 按鈕 count
會發生改變,那麼在重渲染時 computedWatcher 會從新求值嗎?
答案是不會,緩存的關鍵就在於此。回頭再看下文章開頭引用 Vue 文檔裏一句話:
計算屬性是基於它們的響應式依賴進行緩存的
結合上邊的分析是否是恍然大悟,計算屬性 text
的 getter 函數並無讀取 count
,因此它的 computedWatcher 不會訂閱 count
的變化,即 count
的 dep 也不會收集該 computedWatcher 。
因此當 count
改變時,不會觸發 computedWatcher 的更新,dirty
仍爲 false ,說明這個計算屬性不「髒」。那在以後的 render 過程當中讀取到計算屬性 text
時就不會從新求值,這樣就起到了緩存的效果。
整個過程下來腦子仍是有點吃力的,但多來幾遍就會逐漸理解 computed 實現的巧妙之處。其實緩存的原理很簡單,就是一個標誌位而已~🤣
注:文中代碼作了刪減,結合源碼斷點捋捋更佳。