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

用文字來描述:
- 初始化計算屬性,為
vm添加fullName屬性,并設(shè)置其get方法 - 首次渲染頁面,
stack中存儲了渲染watcher。由于頁面中用到了fullName屬性,所以在渲染時會觸發(fā)fullName的get方法 fullName執(zhí)行get會通過依賴屬性firstName和lastName來求值,computed watcher會進入stack中- 此時又會觸發(fā)
firstName和lastName的get方法,收集computed watcher fullName求值方法執(zhí)行完成,computed watcher出棧,Dep.target為渲染watcher- 此時為
fullName對應(yīng)的computed watcher中的dep(也就是firstName和lastName對應(yīng)的dep)收集渲染watcher - 完成
fullName的取值過程,此時firstName和lastName的dep中分別收集的watcher為[computed watcher, render watcher]
假設(shè)我們更新了依賴,會通知收集的watcher進行更新:
vm.firstName = 'F'
在firstName屬性更新后,會觸發(fā)其對應(yīng)的set方法,執(zhí)行dep中收集的computed watcher和render watcher:
computed watcher: 將this.dirty設(shè)置為true,fullName之后取值時需要重新執(zhí)行用戶傳入的getter函數(shù)render watcher: 通知視圖更新,獲取fullName的最新值
到這里我們實現(xiàn)的computed屬性便能正常工作了!
總結(jié)
本文從一個簡單的計算屬性例子開始,一步步實現(xiàn)了計算屬性。并且針對這個例子,詳細分析了頁面渲染時的整個代碼執(zhí)行邏輯。希望小伙伴們在讀完本文后,能夠從源碼的角度,分析自己代碼中對應(yīng)計算屬性相關(guān)代碼的執(zhí)行流程,體會一下Vue 的computed屬性到底幫我們做了些什么。
到此這篇關(guān)于手寫實現(xiàn)Vue計算屬性的文章就介紹到這了,更多相關(guān)Vue計算屬性內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue配置marked鏈接添加target="_blank"的方法
這篇文章主要介紹了Vue配置marked鏈接添加target="_blank"的方法,文中給大家提到了vue實現(xiàn)類似target="_blank"打開新窗口的代碼,感興趣的朋友參考下吧2019-07-07
詳解win7 cmd執(zhí)行vue不是內(nèi)部命令的解決方法
這篇文章主要介紹了詳解win7 cmd執(zhí)行vue不是內(nèi)部命令的解決方法的相關(guān)資料,這里提供了解決問題的詳細步驟,具有一定的參考價值,需要的朋友可以參考下2017-07-07
Vue實例中生命周期created和mounted的區(qū)別詳解
這篇文章主要給大家介紹了關(guān)于Vue實例中生命周期created和mounted區(qū)別的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面跟著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-08-08

