編者按:咱們會不時邀請工程師談談有意思的技術細節,但願知其因此然能讓你們在面試有更出色表現。
也給面試官提供更多思路。前端
雖然目前的技術棧已由 Vue 轉到了 React,但從以前使用 Vue 開發的多個項目實際經從來看仍是很是愉悅的,Vue 文檔清晰規範,api 設計簡潔高效,對前端開發人員友好,上手快,甚至我的認爲在不少場景使用 Vue 比 React 開發效率更高,以前也有斷斷續續研讀過 Vue 的源碼,但一直沒有梳理總結,因此在此作一些技術概括同時也加深本身對 Vue 的理解,那麼今天要寫的即是 Vue 中最經常使用到的 API 之一 computed
的實現原理。vue
話很少說,一個最基本的例子以下:面試
<div id="app">
<p>{{fullName}}</p>
</div>
複製代碼
new Vue({
data: {
firstName: 'Xiao',
lastName: 'Ming'
},
computed: {
fullName: function () {
return this.firstName + ' ' + this.lastName
}
}
})
複製代碼
Vue 中咱們不須要在 template 裏面直接計算 {{this.firstName + ' ' + this.lastName}}
,由於在模版中放入太多聲明式的邏輯會讓模板自己太重,尤爲當在頁面中使用大量複雜的邏輯表達式處理數據時,會對頁面的可維護性形成很大的影響,而 computed
的設計初衷也正是用於解決此類問題。api
watch
固然不少時候咱們使用 computed
時每每會與 Vue 中另外一個 API 也就是偵聽器 watch
相比較,由於在某些方面它們是一致的,都是以 Vue 的依賴追蹤機制爲基礎,當某個依賴數據發生變化時,全部依賴這個數據的相關數據或函數都會自動發生變化或調用。數組
雖然計算屬性在大多數狀況下更合適,但有時也須要一個自定義的偵聽器。這就是爲何 Vue 經過
watch
選項提供了一個更通用的方法來響應數據的變化。當須要在數據變化時執行異步或開銷較大的操做時,這個方式是最有用的。緩存
從 Vue 官方文檔對 watch
的解釋咱們能夠了解到,使用 watch
選項容許咱們執行異步操做(訪問一個 API)或高消耗性能的操做,限制咱們執行該操做的頻率,並在咱們獲得最終結果前,設置中間狀態,而這些都是計算屬性沒法作到的。微信
下面還另外總結了幾點關於 computed
和 watch
的差別:app
computed
是計算一個新的屬性,並將該屬性掛載到 vm(Vue 實例)上,而 watch
是監聽已經存在且已掛載到 vm
上的數據,因此用 watch
一樣能夠監聽 computed
計算屬性的變化(其它還有 data
、props
)computed
本質是一個惰性求值的觀察者,具備緩存性,只有當依賴變化後,第一次訪問 computed
屬性,纔會計算新的值,而 watch
則是當數據發生變化便會調用執行函數computed
適用一個數據被多個數據影響,而 watch
適用一個數據影響多個數據;以上咱們瞭解了 computed
和 watch
之間的一些差別和使用場景的區別,固然某些時候二者並無那麼明確嚴格的限制,最後仍是要具體到不一樣的業務進行分析。異步
言歸正傳,回到文章的主題 computed
身上,爲了更深層次地瞭解計算屬性的內在機制,接下來就讓咱們一步步探索 Vue 源碼中關於它的實現原理吧。函數
在分析 computed
源碼以前咱們先得對 Vue 的響應式系統有一個基本的瞭解,Vue 稱其爲非侵入性的響應式系統,數據模型僅僅是普通的 JavaScript 對象,而當你修改它們時,視圖便會進行自動更新。
當你把一個普通的 JavaScript 對象傳給 Vue 實例的
data
選項時,Vue 將遍歷此對象全部的屬性,並使用Object.defineProperty
把這些屬性所有轉爲getter/setter
,這些getter/setter
對用戶來講是不可見的,可是在內部它們讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化,每一個組件實例都有相應的watcher
實例對象,它會在組件渲染的過程當中把屬性記錄爲依賴,以後當依賴項的setter
被調用時,會通知watcher
從新計算,從而導致它關聯的組件得以更新。
Vue 響應系統,其核心有三點:observe
、watcher
、dep
:
observe
:遍歷 data
中的屬性,使用 Object.defineProperty 的 get/set
方法對其進行數據劫持;dep
:每一個屬性擁有本身的消息訂閱器 dep
,用於存放全部訂閱了該屬性的觀察者對象;watcher
:觀察者(對象),經過 dep
實現對響應屬性的監聽,監聽到結果後,主動觸發本身的回調進行響應。對響應式系統有一個初步瞭解後,咱們再來分析計算屬性。 首先咱們找到計算屬性的初始化是在 src/core/instance/state.js
文件中的 initState
函數中完成的
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
// computed初始化
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
複製代碼
調用了 initComputed
函數(其先後也分別初始化了 initData
和 initWatch
)並傳入兩個參數 vm
實例和 opt.computed
開發者定義的 computed
選項,轉到 initComputed
函數:
const computedWatcherOptions = { computed: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
'Getter is missing for computed property "${key}".',
vm
)
}
if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn('The computed property "${key}" is already defined in data.', vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn('The computed property "${key}" is already defined as a prop.', vm)
}
}
}
}
複製代碼
從這段代碼開始咱們觀察這幾部分:
獲取計算屬性的定義 userDef
和 getter
求值函數
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
複製代碼
定義一個計算屬性有兩種寫法,一種是直接跟一個函數,另外一種是添加 set
和 get
方法的對象形式,因此這裏首先獲取計算屬性的定義 userDef
,再根據 userDef
的類型獲取相應的 getter
求值函數。
計算屬性的觀察者 watcher
和消息訂閱器 dep
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
複製代碼
這裏的 watchers
也就是 vm._computedWatchers
對象的引用,存放了每一個計算屬性的觀察者 watcher
實例(注:後文中提到的「計算屬性的觀察者」、「訂閱者」和 watcher
均指代同一個意思但注意和 Watcher
構造函數區分),Watcher
構造函數在實例化時傳入了 4 個參數:vm
實例、getter
求值函數、noop
空函數、computedWatcherOptions
常量對象(在這裏提供給 Watcher
一個標識 {computed:true}
項,代表這是一個計算屬性而不是非計算屬性的觀察者,咱們來到 Watcher
構造函數的定義:
class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
if (options) {
this.computed = !!options.computed
}
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
} finally {
popTarget()
}
return value
}
update () {
if (this.computed) {
if (this.dep.subs.length === 0) {
this.dirty = true
} else {
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}
}
複製代碼
爲了簡潔突出重點,這裏我手動去掉了咱們暫時不須要關心的代碼片斷。 觀察 Watcher
的 constructor
,結合剛纔講到的 new Watcher
傳入的第四個參數 {computed:true}
知道,對於計算屬性而言 watcher
會執行 if
條件成立的代碼 this.dep = new Dep()
,而 dep
也就是建立了該屬性的消息訂閱器。
export default class Dep {
static target: ?Watcher;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep.target = null
複製代碼
Dep
一樣精簡了部分代碼,咱們觀察 Watcher
和 Dep
的關係,用一句話總結
watcher
中實例化了dep
並向dep.subs
中添加了訂閱者,dep
經過notify
遍歷了dep.subs
通知每一個watcher
更新。
defineComputed
定義計算屬性
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn('The computed property "${key}" is already defined in data.', vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn('The computed property "${key}" is already defined as a prop.', vm)
}
}
複製代碼
由於 computed
屬性是直接掛載到實例對象中的,因此在定義以前須要判斷對象中是否已經存在重名的屬性,defineComputed
傳入了三個參數:vm
實例、計算屬性的 key
以及 userDef
計算屬性的定義(對象或函數)。 而後繼續找到 defineComputed
定義處:
export function defineComputed ( target: any, key: string, userDef: Object | Function ) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop
sharedPropertyDefinition.set = userDef.set
? userDef.set
: noop
}
if (process.env.NODE_ENV !== 'production' &&
sharedPropertyDefinition.set === noop) {
sharedPropertyDefinition.set = function () {
warn(
'Computed property "${key}" was assigned to but it has no setter.',
this
)
}
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製代碼
在這段代碼的最後調用了原生 Object.defineProperty
方法,其中傳入的第三個參數是屬性描述符sharedPropertyDefinition
,初始化爲:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
複製代碼
隨後根據 Object.defineProperty
前面的代碼能夠看到 sharedPropertyDefinition
的 get/set
方法在通過 userDef
和 shouldCache
等多重判斷後被重寫,當非服務端渲染時,sharedPropertyDefinition
的 get
函數也就是 createComputedGetter(key)
的結果,咱們找到 createComputedGetter
函數調用結果並最終改寫 sharedPropertyDefinition
大體呈現以下:
sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
watcher.depend()
return watcher.evaluate()
}
},
set: userDef.set || noop
}
複製代碼
當計算屬性被調用時便會執行 get
訪問函數,從而關聯上觀察者對象 watcher
而後執行 wather.depend()
收集依賴和 watcher.evaluate()
計算求值。
computed
和 data
會分別創建各自的響應系統,Observer
遍歷 data
中每一個屬性設置 get/set
數據攔截computed
會調用 initComputed
函數
watcher
實例,並在內實例化一個 Dep
消息訂閱器用做後續收集依賴(好比渲染函數的 watcher
或者其餘觀察該計算屬性變化的 watcher
)Object.defineProperty
的get
訪問器函數watcher.depend()
方法向自身的消息訂閱器 dep
的 subs
中添加其餘屬性的 watcher
watcher
的 evaluate
方法(進而調用 watcher
的 get
方法)讓自身成爲其餘 watcher
的消息訂閱器的訂閱者,首先將 watcher
賦給 Dep.target
,而後執行 getter
求值函數,當訪問求值函數裏面的屬性(好比來自 data
、props
或其餘 computed
)時,會一樣觸發它們的 get
訪問器函數從而將該計算屬性的 watcher
添加到求值函數中屬性的 watcher
的消息訂閱器 dep
中,當這些操做完成,最後關閉 Dep.target
賦爲 null
並返回求值函數結果。set
攔截函數,而後調用自身消息訂閱器 dep
的 notify
方法,遍歷當前 dep
中保存着全部訂閱者 wathcer
的 subs
數組,並逐個調用 watcher
的 update
方法,完成響應更新。文 / 亦然
一枚嚮往詩與遠方的 coder
編 / 熒聲
本文已由做者受權發佈,版權屬於創宇前端。歡迎註明出處轉載本文。本文連接:knownsec-fed.com/2018-09-12-…
想要訂閱更多來自知道創宇開發一線的分享,請搜索關注咱們的微信公衆號:創宇前端(KnownsecFED)。歡迎留言討論,咱們會盡量回復。
感謝您的閱讀。