手寫實(shí)現(xiàn)Vue計(jì)算屬性
前言
官網(wǎng)對(duì)計(jì)算屬性的介紹在這里:傳送門
計(jì)算屬性是Vue中很常用的一個(gè)配置項(xiàng),我們先用一個(gè)簡(jiǎn)單的例子來(lái)講解它的功能:
<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ù),會(huì)最終處理為vm.fullName的getter函數(shù)。所以vm.fullName = this.firstName + this.lastName = 'FooBar'。
計(jì)算屬性有以下特點(diǎn):
- 計(jì)算屬性可以簡(jiǎn)化模板中的表達(dá)式,用戶可以書(shū)寫更加簡(jiǎn)潔易讀的
template Vue為計(jì)算屬性提供了緩存功能,只有當(dāng)它依賴的屬性(例子中的this.firstName和this.lastName)發(fā)生變化時(shí),才會(huì)重新執(zhí)行屬性對(duì)應(yīng)的getter函數(shù),否則會(huì)將之前計(jì)算好的值返回。
正是由于computed的緩存功能,使得用戶在使用時(shí)會(huì)優(yōu)先考慮它,而不是使用watch、methods屬性。
在了解了計(jì)算屬性的用法后,我們通過(guò)代碼來(lái)一步步實(shí)現(xiàn)computed,并讓它完成上邊的例子。
初始化計(jì)算屬性
初始化computed的邏輯會(huì)書(shū)寫在scr/state.js中:
function initState (vm) {
const options = vm.$options;
// some code ...
if (options.computed) {
initComputed(vm);
}
}在initComputed中,可以通過(guò)vm.$options.computed拿到所有定義的計(jì)算屬性。對(duì)于每個(gè)計(jì)算屬性,需要對(duì)其做如下處理:
- 實(shí)例化計(jì)算屬性對(duì)應(yīng)的
Watcher - 取到計(jì)算屬性的
key,通過(guò)Object.defineProperty為vm實(shí)例添加key屬性,并設(shè)置它的get/set方法
function initComputed (vm) {
const { computed } = vm.$options;
// 將計(jì)算屬性watcher存儲(chǔ)到vm._computedWatchers屬性中,之后方法直接通過(guò)實(shí)例vm來(lái)獲取
const watchers = vm._computedWatchers = {};
for (const key in computed) {
if (computed.hasOwnProperty(key)) {
const userDef = computed[key];
// 計(jì)算屬性key的值有可能是對(duì)象,在對(duì)象中會(huì)設(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è)對(duì)象:
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ù)中,我們會(huì)根據(jù)計(jì)算屬性的類型來(lái)確定是否為其定義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);
// 如果是對(duì)象,用戶會(huì)傳入set方法
sharedPropertyDefinition.set = userDef.set;
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}
// 創(chuàng)建Object.defineProperty的get函數(shù)
function createComputedGetter (key) {
return function () {
// 通過(guò)之前保存的_computedWatchers來(lái)取到對(duì)應(yīng)的計(jì)算屬性watcher
const watcher = this._computedWatchers[key];
if (watcher.dirty) {
// 只有在dirty為true的時(shí)候才會(huì)重新執(zhí)行計(jì)算屬性
watcher.evaluate();
if (Dep.target) {
// 此時(shí),如果棧中有渲染watcher,會(huì)為當(dāng)前計(jì)算屬性watcher中收集的所有dep再收集渲染watcher
// 在watcher收集的dep對(duì)應(yīng)的屬性(this.firstName,this.lastName)更新后,通知視圖更新,從而更新頁(yè)面中的計(jì)算屬性
watcher.depend();
}
}
return watcher.value;
};
}在對(duì)計(jì)算屬性取值時(shí),首先會(huì)調(diào)用它在vm.fullName上定義的get方法,也就是上邊的createComputedGetter執(zhí)行后返回的函數(shù)。在函數(shù)內(nèi)部,只有當(dāng)watcher.dirty為true 時(shí),才會(huì)執(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不會(huì)執(zhí)行,用到的時(shí)候才會(huì)執(zhí)行
this.value = this.lazy ? undefined : this.get();
}
// 執(zhí)行傳入的getter函數(shù)進(jìn)行求值,將其賦值給this.value
// 求值完畢后,將dirty置為false,下次將不會(huì)再重新執(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í)會(huì)再次執(zhí)行evaluate來(lái)執(zhí)行this.get()方法
this.dirty = true;
} else {
queueWatcher(this);
}
}
// some code ...
}watcher.evaluate中的邏輯便是執(zhí)行我們?cè)诙x計(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) 是否存在,如果存在會(huì)為依賴屬性收集對(duì)應(yīng)的渲染watcher。這樣在依賴屬性更新時(shí),便會(huì)通過(guò)渲染watcher來(lái)通知視圖更新,獲取到最新的計(jì)算屬性。
依賴屬性更新
以文章開(kāi)始時(shí)的demo為例,首次執(zhí)行時(shí)的邏輯如下圖:

用文字來(lái)描述:
- 初始化計(jì)算屬性,為
vm添加fullName屬性,并設(shè)置其get方法 - 首次渲染頁(yè)面,
stack中存儲(chǔ)了渲染watcher。由于頁(yè)面中用到了fullName屬性,所以在渲染時(shí)會(huì)觸發(fā)fullName的get方法 fullName執(zhí)行get會(huì)通過(guò)依賴屬性firstName和lastName來(lái)求值,computed watcher會(huì)進(jìn)入stack中- 此時(shí)又會(huì)觸發(fā)
firstName和lastName的get方法,收集computed watcher fullName求值方法執(zhí)行完成,computed watcher出棧,Dep.target為渲染watcher- 此時(shí)為
fullName對(duì)應(yīng)的computed watcher中的dep(也就是firstName和lastName對(duì)應(yīng)的dep)收集渲染watcher - 完成
fullName的取值過(guò)程,此時(shí)firstName和lastName的dep中分別收集的watcher為[computed watcher, render watcher]
假設(shè)我們更新了依賴,會(huì)通知收集的watcher進(jìn)行更新:
vm.firstName = 'F'
在firstName屬性更新后,會(huì)觸發(fā)其對(duì)應(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è)簡(jiǎn)單的計(jì)算屬性例子開(kāi)始,一步步實(shí)現(xiàn)了計(jì)算屬性。并且針對(duì)這個(gè)例子,詳細(xì)分析了頁(yè)面渲染時(shí)的整個(gè)代碼執(zhí)行邏輯。希望小伙伴們?cè)谧x完本文后,能夠從源碼的角度,分析自己代碼中對(duì)應(yīng)計(jì)算屬性相關(guān)代碼的執(zhí)行流程,體會(huì)一下Vue 的computed屬性到底幫我們做了些什么。
到此這篇關(guān)于手寫實(shí)現(xiàn)Vue計(jì)算屬性的文章就介紹到這了,更多相關(guān)Vue計(jì)算屬性內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue項(xiàng)目實(shí)現(xiàn)圖片懶加載的簡(jiǎn)單步驟
懶加載的好處在于減少服務(wù)器的壓力,在網(wǎng)絡(luò)比較慢的情況下,可以提前給這張圖片添加一個(gè)占位圖片,提高用戶的體驗(yàn),這篇文章主要給大家介紹了關(guān)于vue項(xiàng)目實(shí)現(xiàn)圖片懶加載的相關(guān)資料,需要的朋友可以參考下2022-09-09
vue項(xiàng)目如何實(shí)現(xiàn)Echarts在label中獲取點(diǎn)擊事件
這篇文章主要介紹了vue項(xiàng)目如何實(shí)現(xiàn)Echarts在label中獲取點(diǎn)擊事件,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10
富文本編輯器vue2-editor實(shí)現(xiàn)全屏功能
這篇文章主要介紹了富文本編輯器vue2-editor實(shí)現(xiàn)全屏功能,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值 ,需要的朋友可以參考下2019-05-05
拿來(lái)就用vue-gird-layout組件封裝示例
這篇文章主要介紹了vue-gird-layout組件封裝示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
不依任何賴第三方,單純用vue實(shí)現(xiàn)Tree 樹(shù)形控件的案例
這篇文章主要介紹了不依任何賴第三方,單純用vue實(shí)現(xiàn)Tree 樹(shù)形控件的案例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-09-09
vue props對(duì)象validator自定義函數(shù)實(shí)例
今天小編就為大家分享一篇vue props對(duì)象validator自定義函數(shù)實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11
一篇文章搞懂Vue3中如何使用ref獲取元素節(jié)點(diǎn)
過(guò)去在Vue2中,我們采用ref來(lái)獲取標(biāo)簽的信息,用以替代傳統(tǒng) js 中的 DOM 行為,下面這篇文章主要給大家介紹了關(guān)于如何通過(guò)一篇文章搞懂Vue3中如何使用ref獲取元素節(jié)點(diǎn)的相關(guān)資料,需要的朋友可以參考下2022-11-11

