上一章 Vue2異步更新和nextTick原理,我們介紹了 JavaScript 執行機制是什么?nextTick原始碼是如何實作的?以及Vue是如何異步更新渲染的?
本章目標
- 計算屬性是如何實作的?
- 計算屬性快取原理 - 帶有dirty屬性的watcher
- 洋蔥模型的應用
初始化
在 Vue初始化實體的程序中,如果用戶 options選項中存在計算屬性時,則初始化計算屬性
// 初始化狀態
export function initState(vm) {
const opts = vm.$options // 獲取所有的選項
// 初始化資料
if (opts.data) { initData(vm) }
// 初始化計算屬性
if (opts.computed) { initComputed(vm) }
}
我們給每個計算屬性都創建了一個 Watcher實體,標識為lazy:true
, 在初始化watcher時不會立即執行 get方法(計算屬性方法)
并將計算屬性watcher 都保存到了 Vue實體上,讓我們可以在后續的 getter方法中通過 vm獲取到當前的計算屬性watcher
然后使用Object.defineProperty
去劫持計算屬性
// 初始化計算屬性
function initComputed(vm) {
const computed = vm.$options.computed
const watchers = (vm._computedWatchers = {}) // 將每個計算屬性對應的watcher 都保存到 vm上
for (let key in computed) {
let userDef = computed[key]
// 兼容不同寫法 函式方式 和 物件getter/setter方式
let fn = typeof userDef === 'function' ? userDef : userDef.get
// 給每個計算屬性都創建一個 watcher,并標識為 lazy,不會立即執行 get-fn
watchers[key] = new Watcher(vm, fn, { lazy: true })
// 劫持計算屬性getter/setter
defineComputed(vm, key, userDef)
}
}
// 劫持計算屬性
function defineComputed(target, key, userDef) {
const setter = userDef.set || (() => {})
Object.defineProperty(target, key, {
get: createComputedGetter(key),
set: setter,
})
}
當我們劫持到計算屬性被訪問時,根據 dirty 值去決定是否更新 watcher快取值
然后讓自己依賴的屬性(準確來說是訂閱的所有dep)都去收集上層watcher,即 Dep.target(可能是計算屬性watcher,也可能是渲染watcher)
// 劫持計算屬性的訪問
function createComputedGetter(key) {
return function () {
const watcher = this._computedWatchers[key] // this就是 defineProperty 劫持的targer,獲取到計算屬性對應的watcher
// 如果是臟的,就去執行用戶傳入的函式
if (watcher.dirty) {
watcher.evaluate() // 重新求值后 dirty變為false,下次就不求值了,走快取值
}
// 當前計算屬性watcher 出堆疊后,還有渲染watcher 或者其他計算屬性watcher,我們應該讓當前計算屬性watcher 訂閱的 dep,也去收集上一層的watcher 即Dep.target(可能是計算屬性watcher,也可能是渲染watcher)
if (Dep.target) {
watcher.depend()
}
// 回傳watcher上的值
return watcher.value
}
Dep
Dep.target
:當前渲染的 watcher,靜態變數stack
:存放 watcher 的堆疊, 利用 pushTarget、popTarget 這兩個方法做入堆疊出堆疊操作
// 當前渲染的 watcher
Dep.target = null
// 存放 watcher 的堆疊
let stack = []
// 當前 watcher 入堆疊, Dep.target 指向 當前 watcher
export function pushTarget(watcher) {
stack.push(watcher)
Dep.target = watcher
}
// 堆疊中最后一個 watcher 出堆疊,Dep.target指向堆疊中 最后一個 watcher,若堆疊為空,則為 undefined
export function popTarget() {
stack.pop()
Dep.target = stack[stack.length - 1]
}
計算屬性Watcher
在初始化Vue實體時,我們會給每個計算屬性都創建一個對應watcher(我們稱之為計算屬性watcher,除此之外還有 渲染watcher 和 偵聽器watcher ),他有一個 value 屬性用于快取計算屬性方法的回傳值,
默認標識 lazy: true,懶的,代表計算屬性watcher,創建時不會立即執行 get方法
默認標識 dirty: true,臟的,當我們劫持到計算屬性訪問時,如果是臟的,我們會通過watcher.evaluate
重新計算 watcher 的 value值 并將其標識為干凈的;如果是干凈的,則直接取 watcher 快取值
depend 方法,會讓計算屬性watcher 訂閱的dep去收集上層watcher,可能是渲染watcher,也可能是計算屬性watcher(計算屬性嵌套的情況),實作洋蔥模型的核心方法
update 方法,當計算屬性依賴的物件發生變化時,會觸發dep.notify
派發更新 并 呼叫 update 方法,只需更新 dirty 為 true即可,我們會在后續的渲染watcher 更新時,劫持到計算屬性的訪問操作,并通過 watcher.evaluate
重新計算其 value值
class Watcher {
constructor(vm, fn, options) {
// 計算屬性watcher 用到的屬性
this.vm = vm
this.lazy = options.lazy // 懶的,不會立即執行get方法
this.dirty = this.lazy // 臟的,決定重新讀取get回傳值 還是 讀取快取值
this.value = https://www.cnblogs.com/burc/archive/2023/04/19/this.lazy ? undefined : this.get() // 存盤 get回傳值
}
// 重新渲染
update() {
console.log('watcher-update')
if (this.lazy) {
// 計算屬性依賴的值發生改變,觸發 setter 通知 watcher 更新,將計算屬性watcher 標識為臟值即可
// 后面還會觸發渲染watcher,會走 evaluate 重新讀取回傳值
this.dirty = true
} else {
queueWatcher(this) // 把當前的watcher 暫存起來,異步佇列渲染
// this.get(); // 重新渲染
}
}
// 計算屬性watcher為臟時,執行 evaluate,并將其標識為干凈的
evaluate() {
this.value = https://www.cnblogs.com/burc/archive/2023/04/19/this.get() // 重新獲取到用戶函式的回傳值
this.dirty = false
}
// 用于洋蔥模型中計算屬性watcher 訂閱的dep去 depend收集上層watcher 即Dep.target(可能是計算屬性watcher,也可能是渲染watcher)
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
快取原理
計算屬性是基于它們的回應式依賴進行快取的,只在相關回應式依賴發生改變時它們才會重新求值, 快取原理如下:
在初始化計算屬性時,我們使用Object.defineProperty
劫持了計算屬性,并做了一些 getter/setter操作
計算屬性watcher 有一個 dirty臟值屬性,默認為 true
當我們劫持到計算屬性被訪問時,如果 dirty 為 true,則執行 evaluate 更新 watcher 的 value值 并 將 dirty 標識為 false;如果為 false,則直接取 watcher 的快取值
當計算屬性依賴的屬性變化時,會通知 watcher 呼叫 update方法,此時我們將 dirty 標識為 true,這樣再次取值時會重新進行計算
洋蔥模型
在初始化Vue實體時,我們會給每個計算屬性都創建一個對應的懶的watcher,不會立即呼叫計算屬性方法
當我們訪問計算屬性時,會通過watcher.evaluate()
讓其直接依賴的屬性去收集當前的計算屬性watcher,并且還會通過watcher.depend()
讓其訂閱的所有 dep都去收集上層watcher,可能是渲染watcher,也可能是計算屬性watcher(如果存在計算屬性嵌套計算屬性的話),這樣依賴的屬性發生變化也可以讓視圖進行更新
讓我們一起來分析下計算屬性嵌套的例子
<p>{{fullName}}</p>
computed: {
fullAge() {
return '今年' + this.age
},
fullName() {
console.log('run')
return this.firstName + ' ' + this.lastName + ' ' + this.fullAge
},
}
- 初始化組件時,渲染watcher 入堆疊
stack:[渲染watcher]
- 當執行 render方法并初次訪問 fullName時,執行
computed watcher1.evaluate()
,watcher1
入堆疊stack:[渲染watcher, watcher1]
- 當執行
watcher1
的 get方法時,其直接依賴的 firstName 和 lastName 會去收集當前的 watcher1;然后又訪問 fullAge 并執行computed watcher2.evaluate()
,watcher2
入堆疊watcher1:[firstName, lastName]
stack:[渲染watcher, watcher1, watcher2]
- 執行
watcher2
的 get方法時,其直接依賴的 age 會去收集當前的 watcher2watcher2:[age]
watcher2
出堆疊,并執行watcher2.depend()
,讓watcher2
訂閱的 dep再去收集當前watcher1stack:[渲染watcher, watcher1]
watcher1:[firstName, lastName, age]
watcher1
出堆疊,執行watcher1.depend()
,讓watcher1
訂閱的 dep再去收集當前的渲染watcherstack:[渲染watcher]
渲染watcher:[firstName, lastName, age]
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/550579.html
標籤:其他
上一篇:http1.1與http2.0