記錄Computed源碼分析

NO IMAGE

之前看了很多文章通過源碼分析computed的原理,藉此機會把自己對源碼所理解的,所以這篇記錄大多數都是對源碼批註的形式寫下來的,當做自己的學習筆記。寫的不好還請擔待。

瞭解Computed(計算屬性)

vue中有個常用的方法Computed就是計算屬性
平時我們在獲取後臺返回的數據後可能會對某些數據進行加工直接放在模板中太多的邏輯會讓模板過重且難以維護。例如:

<div id="example">
{{ message.split('').reverse().join('') }}
</div>

在這個地方,模板不再是簡單的聲明式邏輯。你必須看一段時間才能意識到,這裡是想要顯示變量 message 的翻轉字符串。當你想要在模板中多次引用此處的翻轉字符串時,就會更加難以處理。

所以,對於任何複雜邏輯,你都可以使用計算屬性。

var vm = new Vue({
el: '#example',
data: {
message: 'Hello'
},
computed: {
// 計算屬性的 getter
reversedMessage: function () {
// `this` 指向 vm 實例
return this.message.split('').reverse().join('')
}
}
})

computed介紹

在vue中的computed聲明瞭計算屬性,初始化vue對象的時候會直接掛載到vue實例上,可以用this.訪問,同時計算屬性有一個特點,他會緩存數據。當計算屬性中所依賴的數據沒有變化的時候計算屬性不會重新計算return的值。當計算屬性中依賴的數據項有某一項變化的時候他會重新計算並返回新值。

computed與methods區別

由於計算屬性會緩存所以調用computed不會重複的get數據並且計算,但是調用methods中的方法,每調用一次就會重新執行一次,所以使用computed的性能會高於methods

computed實現原理

剛剛簡單的介紹了computed,那麼他是如何實現的?我們通過源碼來探究一下。
(如果不瞭解vue響應式原理的,請移步這裡
vue初始化的時候分別對data,computed,watcher 執行相應的方法。源碼地址

export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
---省去其他無用代碼---
//這裡對computed執行了initComputed();
if (opts.computed) initComputed(vm, opts.computed)
}
const computedWatcherOptions = { lazy: true }
下面是initComputed方法===============
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
-----省略無用代碼
for (const key in computed) {
//遍歷了computed對每一個都進行了處理
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
//這裡new Watcher,調用屬性的時候,會調用get,dep回收集相應的訂閱者
watchers[key] = new Watcher( vm, getter || noop, noop,computedWatcherOptions)
}
//判斷聲明的key不在vue實例中存在
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
-----省略無用代碼
}
}
}

這裡初始化computed執行了initComputed方法,遍歷computed 對每個key執行了defineComputed()生成了訂閱者Watcher
同時傳入了四個參數,我們看下getter,和computeeWatcherOptions,當computed裡面寫的是函數的時候,這個getter就是聲明的函數,如果是對象,那麼就取get方法,

computed:{
sum(){
return this.a+this.b;
},
add:{
get:function(){
return:this.a+this.b;
}
}
}

computedWatcherOptions是個對象{lazy:true},這個參數會就是控制計算屬性懶加載的。我們接下來去看下 defineComputed(vm, key, userDef)這個方法做了什麼。
defineComputed源碼如下:

//這裡聲明瞭一個屬性描述符,
//這裡會用到sharedPropertyDefinition: Object.defineProperty(target, key, sharedPropertyDefinition)
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
//這裡判斷是不是服務端渲染,通常不是ssr
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
//不是ssr執行了createComputedGetter(key),創建了新的get方法賦值給了sharedPropertyDefinition.get
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
//這裡也執行了createComputedGetter()創建了個getter方法。
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
----刪除無用的------------------
Object.defineProperty(target, key, sharedPropertyDefinition)
//最後對computed的key添加屬性描述符,重寫了get和set
//當我們調用computed的時候調用了get方法收集了相應的依賴
}
==========createComputedGetter源碼============
//createComputedGetter會返回一個新get的函數
//當再代碼中調用computed的時候就會調用get,
//Object.defineProperty(target, key, sharedPropertyDefinition)這裡sharedPropertyDefinition的get就是這個函數返回的computedGetter
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
//當在代碼中使用computed後這裡判斷是不是初始化時候添加過的watcher
if (watcher) {
//當watcher.dirty是true的時候調用了evaluate(),我們去下面看下evaluate
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
//收集使用此計算屬性的訂閱者
watcher.depend()
}
//這裡返回計算後的值。
return watcher.value
}
}
}
//上面說到初始化computed的時候會new Watcher()還傳入了{lazy:true}
//所以當前這個computed的watcher.dirty=lazy=true
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
vm._watchers.push(this)
// options
if (options) {
this.lazy = !!options.lazy
} 
this.getter = expOrFn
//這裡this.dirty=true
this.dirty = this.lazy // for lazy watchers
evaluate () {
//調用了evaluate()方法。調用了get方法,緩存了自己
//watcher實例中的value就是計算後的值,然後把dirty變成false.
//所以這個計算屬性實例訪問計算後得值就是watcher.value
this.value = this.get()
this.dirty = false
}
//get方法緩存了自己,調用了傳過來的getter方法返回了計算後的值
//這個getter方法就是在計算屬性中聲明的函數
<!--computed:{-->
<!--    sum(){-->
<!--        return this.a+this.b-->
<!--    }-->
<!-- }-->
//這個sum函數使用了 data中聲明的a變量,和b變量這裡就就會調用這兩個變量的屬性描述符中的get方法,這兩個變量就會把
當前的計算屬性這個訂閱者收集起來。然後返回了計算後的值,這個watcher實例中的value就是計算後的值
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
return value
}

當計算屬性中依賴的數據變化的時候就會觸發set方法,通知更新,update中會把this.dirty設置為true,當再次調用computed的時候就會重新計算返回新的值.

 //當計算屬性依賴的數據變化的時候就會觸發set方法通知訂閱者更新,就會調用update
//因為this.lazy是初始化計算屬性才傳入進來的
//這裡判斷this.lazy是true改變this.dirty=true;
//必須是計算屬性中用到的數據變化得時候才會改變this.dirty=true;
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
//所以當重新觸發計算屬性的get方法後
還會執行這裡
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
//當在代碼中使用computed後這裡判斷是不是初始化時候添加過的watcher
if (watcher) {
//當watcher.dirty是true的時候才去計算
//為false的時候就直接返回原來的值
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
//收集使用此計算屬性的訂閱者
watcher.depend()
}
//這裡返回計算後的值。
return watcher.value
}
}

小結

我們在computed裡聲明瞭多個函數或者是對象,如果是函數則取本身作為getter函數,如果是對象則取對象中的get函數作為getter

computed:{
sum(){
return this.a+this.b
},
add(){
return this.aaa+this.bbb
},
name:{
get:function(){
return this.aa+this.bb
}
}
}

初始化computed的時候遍歷所有的computed,對每個key都創建了個watcher,傳入了一個特殊的參數lazy:true,然後調用了自己的getter方法這樣就收集了這個計算屬性依賴的所有data;那麼所依賴的data會收集這個訂閱者(當前計算屬性)
初始化的同時會針對computed中的key添加屬性描述符創建了獨有的get方法,當調用計算屬性的時候,這個get判斷dirty是否為真,為真的時候代表這個計算屬性需要重新計算,如果為false則直接返回value。當依賴的data 變化的時候回觸發數據的set方法調用update()通知更新,會把dirty設置成true,所以computed 就會重新計算這個值。

寫的不好、歡迎指正

相關文章

我的2019年終總結(浴火重生)|年度徵文

前端性能優化圖片懶加載(防抖、節流)

告訴你如何關閉騰訊廣告定向投放

一位18屆前端玩家的年終總結|年度徵文