手寫實(shí)現(xiàn)Vue計(jì)算屬性
前言
官網(wǎng)對計(jì)算屬性的介紹在這里:傳送門
計(jì)算屬性是Vue
中很常用的一個(gè)配置項(xiàng),我們先用一個(gè)簡單的例子來講解它的功能:
<div id="app"> {{fullName}} </div> <script> const vm = new Vue({ data () { return { firstName: 'Foo', lastName: 'Bar' }; }, computed: { fullName () { return this.firstName + this.lastName; } } }); </script>
在例子中,計(jì)算屬性中定義的fullName
函數(shù),會最終處理為vm.fullName
的getter
函數(shù)。所以vm.fullName = this.firstName + this.lastName = 'FooBar'
。
計(jì)算屬性有以下特點(diǎn):
- 計(jì)算屬性可以簡化模板中的表達(dá)式,用戶可以書寫更加簡潔易讀的
template
Vue
為計(jì)算屬性提供了緩存功能,只有當(dāng)它依賴的屬性(例子中的this.firstName
和this.lastName
)發(fā)生變化時(shí),才會重新執(zhí)行屬性對應(yīng)的getter
函數(shù),否則會將之前計(jì)算好的值返回。
正是由于computed
的緩存功能,使得用戶在使用時(shí)會優(yōu)先考慮它,而不是使用watch
、methods
屬性。
在了解了計(jì)算屬性的用法后,我們通過代碼來一步步實(shí)現(xiàn)computed
,并讓它完成上邊的例子。
初始化計(jì)算屬性
初始化computed
的邏輯會書寫在scr/state.js
中:
function initState (vm) { const options = vm.$options; // some code ... if (options.computed) { initComputed(vm); } }
在initComputed
中,可以通過vm.$options.computed
拿到所有定義的計(jì)算屬性。對于每個(gè)計(jì)算屬性,需要對其做如下處理:
- 實(shí)例化計(jì)算屬性對應(yīng)的
Watcher
- 取到計(jì)算屬性的
key
,通過Object.defineProperty
為vm
實(shí)例添加key
屬性,并設(shè)置它的get/set
方法
function initComputed (vm) { const { computed } = vm.$options; // 將計(jì)算屬性watcher存儲到vm._computedWatchers屬性中,之后方法直接通過實(shí)例vm來獲取 const watchers = vm._computedWatchers = {}; for (const key in computed) { if (computed.hasOwnProperty(key)) { const userDef = computed[key]; // 計(jì)算屬性key的值有可能是對象,在對象中會設(shè)置它的get set 方法 const getter = typeof userDef === 'function' ? userDef : userDef.get; // 為每一個(gè)計(jì)算屬性創(chuàng)建一個(gè)watcher watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true }); // 將計(jì)算屬性的key添加到實(shí)例vm上 defineComputed(vm, key, userDef); } } }
計(jì)算屬性也可以傳入set
方法,用于設(shè)置值時(shí)處理的邏輯,此時(shí)計(jì)算屬性的value
是一個(gè)對象:
new Vue({ // ... computed: { fullName: { // getter get: function () { return this.firstName + ' ' + this.lastName }, // setter set: function (newValue) { var names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } } } } //... )
在defineComputed
函數(shù)中,我們會根據(jù)計(jì)算屬性的類型來確定是否為其定義set
方法:
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }; function defineComputed (target, key, userDef) { if (typeof userDef === 'function') { sharedPropertyDefinition.get = createComputedGetter(key); } else { sharedPropertyDefinition.get = createComputedGetter(key); // 如果是對象,用戶會傳入set方法 sharedPropertyDefinition.set = userDef.set; } Object.defineProperty(target, key, sharedPropertyDefinition); } // 創(chuàng)建Object.defineProperty的get函數(shù) function createComputedGetter (key) { return function () { // 通過之前保存的_computedWatchers來取到對應(yīng)的計(jì)算屬性watcher const watcher = this._computedWatchers[key]; if (watcher.dirty) { // 只有在dirty為true的時(shí)候才會重新執(zhí)行計(jì)算屬性 watcher.evaluate(); if (Dep.target) { // 此時(shí),如果棧中有渲染watcher,會為當(dāng)前計(jì)算屬性watcher中收集的所有dep再收集渲染watcher // 在watcher收集的dep對應(yīng)的屬性(this.firstName,this.lastName)更新后,通知視圖更新,從而更新頁面中的計(jì)算屬性 watcher.depend(); } } return watcher.value; }; }
在對計(jì)算屬性取值時(shí),首先會調(diào)用它在vm.fullName
上定義的get
方法,也就是上邊的createComputedGetter
執(zhí)行后返回的函數(shù)。在函數(shù)內(nèi)部,只有當(dāng)watcher.dirty
為true
時(shí),才會執(zhí)行watcher.evaluate
。
下面我們先看下Watcher
中關(guān)于計(jì)算屬性的代碼:
import { popTarget, pushTarget } from './dep'; import { nextTick } from '../shared/next-tick'; import { traverse } from './traverse'; let id = 0; class Watcher { constructor (vm, exprOrFn, cb, options = {}) { // some code ... // 設(shè)置dirty的初始值為false this.lazy = options.lazy; this.dirty = this.lazy; if (typeof exprOrFn === 'function') { this.getter = this.exprOrFn; } // some code ... // 初始化時(shí)計(jì)算屬性的getter不會執(zhí)行,用到的時(shí)候才會執(zhí)行 this.value = this.lazy ? undefined : this.get(); } // 執(zhí)行傳入的getter函數(shù)進(jìn)行求值,將其賦值給this.value // 求值完畢后,將dirty置為false,下次將不會再重新執(zhí)行求值函數(shù) evaluate () { this.value = this.get(); this.dirty = false; } // 為watcher中的dep,再收集渲染watcher depend () { this.deps.forEach(dep => dep.depend()); } get () { pushTarget(this); const value = this.getter.call(this.vm); if (this.deep) { traverse(value); } popTarget(); return value; } update () { if (this.lazy) { // 依賴的值更新后,只需要將this.dirty設(shè)置為true // 之后獲取計(jì)算屬性的值時(shí)會再次執(zhí)行evaluate來執(zhí)行this.get()方法 this.dirty = true; } else { queueWatcher(this); } } // some code ... }
watcher.evaluate
中的邏輯便是執(zhí)行我們在定義計(jì)算屬性時(shí)傳入的回調(diào)函數(shù)(getter
),將其返回值賦值給watcher.value
,并在取值完畢后,將watcher.dirty
置為false
。這樣再次取值時(shí)便直接將watcher.value
返回即可,而不用再執(zhí)行回調(diào)函數(shù)進(jìn)行重復(fù)計(jì)算。
當(dāng)計(jì)算屬性的依賴屬性(this.firstName
和this.lastName
)發(fā)生變化后,我們要更新視圖,讓計(jì)算屬性重新執(zhí)行getter
函數(shù)獲取到最新值。所以代碼中判斷Dep.target
(此時(shí)為渲染watcher
) 是否存在,如果存在會為依賴屬性收集對應(yīng)的渲染watcher
。這樣在依賴屬性更新時(shí),便會通過渲染watcher
來通知視圖更新,獲取到最新的計(jì)算屬性。
依賴屬性更新
以文章開始時(shí)的demo
為例,首次執(zhí)行時(shí)的邏輯如下圖:
用文字來描述:
- 初始化計(jì)算屬性,為
vm
添加fullName
屬性,并設(shè)置其get
方法 - 首次渲染頁面,
stack
中存儲了渲染watcher
。由于頁面中用到了fullName
屬性,所以在渲染時(shí)會觸發(fā)fullName
的get
方法 fullName
執(zhí)行get
會通過依賴屬性firstName
和lastName
來求值,computed watcher
會進(jìn)入stack
中- 此時(shí)又會觸發(fā)
firstName
和lastName
的get
方法,收集computed watcher
fullName
求值方法執(zhí)行完成,computed watcher
出棧,Dep.target
為渲染watcher
- 此時(shí)為
fullName
對應(yīng)的computed watcher
中的dep
(也就是firstName
和lastName
對應(yīng)的dep
)收集渲染watcher
- 完成
fullName
的取值過程,此時(shí)firstName
和lastName
的dep
中分別收集的watcher
為[computed watcher, render watcher]
假設(shè)我們更新了依賴,會通知收集的watcher
進(jìn)行更新:
vm.firstName = 'F'
在firstName
屬性更新后,會觸發(fā)其對應(yīng)的set
方法,執(zhí)行dep
中收集的computed watcher
和render watcher
:
computed watcher
: 將this.dirty
設(shè)置為true
,fullName
之后取值時(shí)需要重新執(zhí)行用戶傳入的getter
函數(shù)render watcher
: 通知視圖更新,獲取fullName
的最新值
到這里我們實(shí)現(xiàn)的computed
屬性便能正常工作了!
總結(jié)
本文從一個(gè)簡單的計(jì)算屬性例子開始,一步步實(shí)現(xiàn)了計(jì)算屬性。并且針對這個(gè)例子,詳細(xì)分析了頁面渲染時(shí)的整個(gè)代碼執(zhí)行邏輯。希望小伙伴們在讀完本文后,能夠從源碼的角度,分析自己代碼中對應(yīng)計(jì)算屬性相關(guān)代碼的執(zhí)行流程,體會一下Vue
的computed
屬性到底幫我們做了些什么。
到此這篇關(guān)于手寫實(shí)現(xiàn)Vue計(jì)算屬性的文章就介紹到這了,更多相關(guān)Vue計(jì)算屬性內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue項(xiàng)目實(shí)現(xiàn)圖片懶加載的簡單步驟
懶加載的好處在于減少服務(wù)器的壓力,在網(wǎng)絡(luò)比較慢的情況下,可以提前給這張圖片添加一個(gè)占位圖片,提高用戶的體驗(yàn),這篇文章主要給大家介紹了關(guān)于vue項(xiàng)目實(shí)現(xiàn)圖片懶加載的相關(guān)資料,需要的朋友可以參考下2022-09-09vue項(xiàng)目如何實(shí)現(xiàn)Echarts在label中獲取點(diǎn)擊事件
這篇文章主要介紹了vue項(xiàng)目如何實(shí)現(xiàn)Echarts在label中獲取點(diǎn)擊事件,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10富文本編輯器vue2-editor實(shí)現(xiàn)全屏功能
這篇文章主要介紹了富文本編輯器vue2-editor實(shí)現(xiàn)全屏功能,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-05-05不依任何賴第三方,單純用vue實(shí)現(xiàn)Tree 樹形控件的案例
這篇文章主要介紹了不依任何賴第三方,單純用vue實(shí)現(xiàn)Tree 樹形控件的案例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-09-09vue props對象validator自定義函數(shù)實(shí)例
今天小編就為大家分享一篇vue props對象validator自定義函數(shù)實(shí)例,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2019-11-11一篇文章搞懂Vue3中如何使用ref獲取元素節(jié)點(diǎn)
過去在Vue2中,我們采用ref來獲取標(biāo)簽的信息,用以替代傳統(tǒng) js 中的 DOM 行為,下面這篇文章主要給大家介紹了關(guān)于如何通過一篇文章搞懂Vue3中如何使用ref獲取元素節(jié)點(diǎn)的相關(guān)資料,需要的朋友可以參考下2022-11-11