回顧
先捋一下,之前我們實現的 Vue
類,主要有一下的功能:
- 屬性和方法的代理
proxy
- 監聽屬性
watcher
- 事件
對於比與現在的 Vue
中的資料處理,我們還有一些東西沒有實現:Computed
、props
、provied/inject
。
由於後兩者和子父元件有關,先放一放,我們先來實現 Computed
。
Computed
在官方文件中有這麼一句話:
計算屬性的結果會被快取,除非依賴的響應式屬性變化才會重新計算。
這也是計算屬性效能比使用方法來的好的原因所在。
ok 現在我們來實現它,我們先規定一下一個計算屬性的形式:
{
get: Function,
set: Function
}
複製程式碼
官方給了我們兩種形式來寫 Computed
,看了一眼原始碼,發現最終是處理成這種形式,所以我們先直接使用這種形式,之後再做統一化處理。
慣例我們通過測試程式碼來看我們要實現什麼功能:
let test = new Vue({
data() {
return {
firstName: 'aco',
lastName: 'Yang'
}
},
computed: {
computedValue: {
get() {
console.log('測試快取')
return this.firstName + ' ' + this.lastName
}
},
computedSet: {
get() {
return this.firstName + ' ' + this.lastName
},
set(value) {
let names = value.split(' ')
this.firstName = names[0]
this.lastName = names[1]
}
}
}
})
console.log(test.computedValue)
// 測試快取
// aco Yang
console.log(test.computedValue)
// acoYang (快取成功,並沒有呼叫 get 函式)
test.computedSet = 'accco Yang'
console.log(test.computedValue)
// 測試快取 (通過 set 使得依賴發生了變化)
// accco Yang
複製程式碼
我們可以發現:
- 計算屬性是代理到
Vue
例項上的一個屬性 - 第一次呼叫時,呼叫了
get
方法(有 ‘測試快取’ 輸出),而第二次沒有輸出 - 當依賴發生改變時,再次呼叫了
get
方法
解決
第一點很好解決,使用 Object.defineProperty
代理一下就 ok。
接下來看第二點和第三點,當依賴發生改變時,值就會變化,這點和我們之前實現 Watcher
很像,計算屬性的值就是 get
函式的返回值,在 Watcher
中我們同樣儲存了監聽的值(watcher.value
),而這個值是會根據依賴的變化而變化的(如果沒看過 Watcher
實現的同學,去看下 step3
和 step4
),所以計算屬性的 get
就是 Watcher
的 getter
。
那麼 Watcher
的 callback
是啥?其實這裡根本不需要 callback
,計算屬性僅僅需要當依賴發生變化時,儲存的值發生變化。
ok 瞭解之後我們來實現它,同樣的為了方便理解我寫成了一個類:
function noop() {
}
let uid = 0
export default class Computed {
constructor(key, option, ctx) {
// 這裡的 ctx 一般是 Vue 的例項
this.uid = uid++
this.key = key
this.option = option
this.ctx = ctx
this._init()
}
_init() {
let watcher = new Watcher(
this.ctx,
this.option.get || noop,
noop
)
// 將屬性代理到 Vue 例項下
Object.defineProperty(this.ctx, this.key, {
enumerable: true,
configurable: true,
set: this.option.set || noop,
get() {
return watcher.value
}
})
}
}
// Vue 的建構函式
export class Vue extends Event {
constructor(options) {
super()
this.uid = uid++
this._init(options)
}
_init(options) {
let vm = this
...
for (let key in options.computed) {
new Computed(vm, key, options.computed[key])
}
}
}
複製程式碼
我們實現了代理屬性 Object.defineProperty
和更新計算屬性的值,同時依賴沒變化時,也是不會觸發 Watcher
的更新,解決了以上的 3
個問題。
但是,試想一下,計算屬性真的需要實時去更新對應的值嗎?
首先我們知道,依賴的屬性發生了變化會導致計算屬性的變化,換句話說就是,當計算屬性發生變化了,data
下的屬性一定有一部分發生了變化,而 data
下屬性發生變化,會導致檢視的改變,所以計算屬性發生變化在去觸發檢視的變化是不必要的。
其次,我們不能確保計算屬性一定會用到。
而基於第一點,計算屬性是不必要去觸發檢視的變化的,所以計算屬性其實只要在獲取的時候更新對應的值即可。
Watcher 的髒檢查機制
根據我們上面的分析,而 Computed
是 Watcher
的一種實現,所以我們要實現一個不實時更新的 Watcher
。
在 Watcher
中我們實現值的更新是通過下面這段程式碼:
update() {
const value = this.getter.call(this.obj)
const oldValue = this.value
this.value = value
this.cb.call(this.obj, value, oldValue)
}
複製程式碼
當依賴更新的時候,會去觸發這個函式,這個函式變更了 Watcher
例項儲存的 value
,所以我們需要在這裡做出改變,先看下虛擬碼:
update() {
if(/* 判斷這個 Watcher 需不需要實時更新 */){
// doSomething
// 跳出 update
return
}
const value = this.getter.call(this.obj)
const oldValue = this.value
this.value = value
this.cb.call(this.obj, value, oldValue)
}
複製程式碼
這裡的判斷是需要我們一開始就告訴 Watcher
的,所以同樣的我們需要修改 Watcher
的建構函式
constructor(object, getter, callback, options) {
···
if (options) {
this.lazy = !!options.lazy
} else {
this.lazy = false
}
this.dirty = this.lazy
}
複製程式碼
我們給 Watcher
多傳遞一個 options
來傳遞一些配置資訊。這裡我們把不需要實時更新的 Watcher
叫做 lazy Watcher
。同時設定一個標誌(dirty
)來標誌這個 Watcher
是否需要更新,換個專業點的名稱是否需要進行髒檢查。
ok 接下來我們把上面的虛擬碼實現下:
update() {
// 如果是 lazy Watcher
if (this.lazy) {
// 需要進行髒檢查
this.dirty = true
return
}
const value = this.getter.call(this.obj)
const oldValue = this.value
this.value = value
this.cb.call(this.obj, value, oldValue)
}
複製程式碼
如果程式碼走到 update
也就說明這個 Watcher
的依賴發生了變化,同時這是個 lazy Watcher
,那這個 Watcher
就需要進行髒檢查。
但是,上面程式碼雖然標誌了這個 Watcher
,但是 value
並沒有發生變化,我們需要專門寫一個函式去觸發變化。
/**
* 髒檢查機制手動觸發更新函式
*/
evaluate() {
this.value = this.getter.call(this.obj)
// 髒檢查機制觸發後,重置 dirty
this.dirty = false
}
複製程式碼
ok 接著我們來修改 Computed
的實現:
class Computed {
constructor(ctx, key, option,) {
this.uid = uid++
this.key = key
this.option = option
this.ctx = ctx
this._init()
}
_init() {
let watcher = new Watcher(
this.ctx,
this.option.get || noop,
noop,
// 告訴 Wather 來一個 lazy Watcher
{lazy: true}
)
Object.defineProperty(this.ctx, this.key, {
enumerable: true,
configurable: true,
set: this.option.set || noop,
get() {
// 如果是 dirty watch 那就觸發髒檢查機制,更新值
if (watcher.dirty) {
watcher.evaluate()
}
return watcher.value
}
})
}
}
複製程式碼
ok 測試一下
let test = new Vue({
data() {
return {
firstName: 'aco',
lastName: 'Yang'
}
},
computed: {
computedValue: {
get() {
console.log('測試快取')
return this.firstName + ' ' + this.lastName
}
},
computedSet: {
get() {
return this.firstName + ' ' + this.lastName
},
set(value) {
let names = value.split(' ')
this.firstName = names[0]
this.lastName = names[1]
}
}
}
})
// 測試快取 (剛繫結 watcher 時會呼叫一次 get 進行依賴繫結)
console.log('-------------')
console.log(test.computedValue)
// 測試快取
// aco Yang
console.log(test.computedValue)
// acoYang (快取成功,並沒有呼叫 get 函式)
test.firstName = 'acco'
console.log(test.computedValue)
// 測試快取 (當依賴發生變化時,就會呼叫 get 函式)
// acco Yang
test.computedSet = 'accco Yang'
console.log(test.computedValue)
// 測試快取 (通過 set 使得依賴發生了變化)
// accco Yang
複製程式碼
到目前為止,單個 Vue
下的資料相關的內容就差不多了,在實現 props
、provied/inject
機制前,我們需要先實現父子元件,這也是下一步的內容。