Vue計算屬性
1、提到計算屬性,我們馬上就會想到它的一個特性:快取,Vue 檔案也如是說:
計算屬性是基于它們的回應式依賴進行快取的/
2,很榮幸給大家分享,我是一名08年出道的前端高級老鳥,大家如果想跟我交流學習經驗,可以進我的扣扣裙 519293536 有問題我都會盡力幫大家,喜歡中高級問題,小白勿進哦
接下來正題:
那么計算屬性如何快取的呢?計算屬性的觀察者是如何進行依賴收集的呢,接下來深入原理看一下,
本文需要對基礎的回應式原理和幾個關鍵角色 Observer、Dep、Watcher 等有一定的了解,可參考 Observer、Dep、Watcher 傻傻搞不清楚
以一個簡單的例子開始:
<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!
Vue 初始化
還是從 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)
// ...
}
復制代碼
computed 初始化
看一下 initComputed :
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)
}
}
復制代碼
1. 定義 _computedWatchers
首先定義一個 watchers 空物件,同時掛在 vm._computedWatchers 上,用來存放該 vm 實體的所有 computedWatcher,
2. 實體化 computedWatcher
遍歷 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 = https://www.cnblogs.com/chengxuyuanaa/p/this.lazy
? undefined
: this.get()
}
}
復制代碼
執行建構式時指定 lazy dirty 為 true,最后執行 watcher.value = https://www.cnblogs.com/chengxuyuanaa/p/undefined 并未執行 get 方法進行求值,什么時候求值呢?后面就知道了,
回到上邊,為計算屬性創建內部 watcher 之后的 watchers 物件是這樣的:
{
text: Watcher {
lazy: true,
dirty: true,
deps: [],
getter: function () {
return `Hello, ${this.name}!`
},
value: undefined, // 直接賦值為 undefined ,
}
}
復制代碼
3. 定義計算屬性的 getter
看一下 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 實體的所有 computedWatcher - 遍歷 computed 選項并實體化 watcher,不求值,直接將 watcher.value = https://www.cnblogs.com/chengxuyuanaa/p/undefined
- 通過
defineComputed定義計算屬性的 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"]),
]);
}
復制代碼
1. 觸發計算屬性的 getter
這時會觸發計算屬性的 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
}
}
復制代碼
2. 求值 watcher.evaluate()
取出 vm._computedWatchers 中對應的 watcher ,此時 watcher.dirty 為 true,執行 watcher.evaluate() ,
// watcher.evaluate
evaluate() {
this.value = https://www.cnblogs.com/chengxuyuanaa/p/this.get()
this.dirty = false
}
復制代碼
這個函式做了兩件事:
- 執行 get 進行求值,這里就解答了上面何時求值的問題;
- 將 dirty 置為 false ,
先看求值:
// watcher.get
get() {
pushTarget(this) // Dep.target 置為當前 computedWatcher
let value
const vm = this.vm
try {
// 觸發回應資料的依賴收集
value = https://www.cnblogs.com/chengxuyuanaa/p/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!",
}
復制代碼
3. watcher.depend()
此時計算屬性的 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 ,
1. 觸發 computedWatcher 更新
// watcher.update
update() {
// computedWatcher 的 lazy 為 true
if (this.lazy) {
this.dirty = true
}
// ...
}
復制代碼
只做了一件事,就是將 dirty 置為 true,表示該計算屬性“臟”了,需要重新計算,什么時候重新求值呢,往下看,
2. 觸發渲染 watcher 更新
// 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}!`
}
復制代碼
至此,更新結束,
如何快取
通過上邊的程序分析,可以做出如下總結:
- 首次渲染時實體化 computedWatcher 并定義屬性
dirty: false,在 render 程序中求值并進行依賴收集; - 當 computedWatcher 訂閱的回應式資料也就是
name改變時,觸發 computedWatcher 的更新,修改dirty: true; - render 函式執行時讀取計算屬性
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 時就不會重新求值,這樣就起到了快取的效果,
后記
1、我是一名08年出道的前端高級老鳥,大家如果想跟我交流學習經驗,可以進我的扣扣裙 519293536 有問題我都會盡力幫大家,喜歡中高級問題,小白勿進哦
2、整個程序下來腦子還是有點吃力的,但多來幾遍就會逐漸理解 computed 實作的巧妙之處,其實快取的原理很簡單,就是一個標志位而已~??
注:文中代碼做了刪減,結合原始碼斷點捋捋更佳,
本文的文字及圖片來源于網路加上自己的想法,僅供學習、交流使用,不具有任何商業用途,著作權歸原作者所有,如有問題請及時聯系我們以作處理
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/69807.html
標籤:JavaScript
