摘要:當某個屬性發生變化,觸發攔截函數,然后調用自身消息訂閱器的方法,遍歷當前中保存著所有訂閱者的數組,并逐個調用的方法,完成響應更新。
編者按:我們會不時邀請工程師談談有意思的技術細節,希望知其所以然能讓大家在面試有更出色表現。也給面試官提供更多思路。
雖然目前的技術棧已由 Vue 轉到了 React,但從之前使用 Vue 開發的多個項目實際經歷來看還是非常愉悅的,Vue 文檔清晰規范,api 設計簡潔高效,對前端開發人員友好,上手快,甚至個人認為在很多場景使用 Vue 比 React 開發效率更高,之前也有斷斷續續研讀過 Vue 的源碼,但一直沒有梳理總結,所以在此做一些技術歸納同時也加深自己對 Vue 的理解,那么今天要寫的便是 Vue 中最常用到的 API 之一 computed 的實現原理。
基本介紹話不多說,一個最基本的例子如下:
{{fullName}}
new Vue({ data: { firstName: "Xiao", lastName: "Ming" }, computed: { fullName: function () { return this.firstName + " " + this.lastName } } })
Vue 中我們不需要在 template 里面直接計算 {{this.firstName + " " + this.lastName}},因為在模版中放入太多聲明式的邏輯會讓模板本身過重,尤其當在頁面中使用大量復雜的邏輯表達式處理數據時,會對頁面的可維護性造成很大的影響,而 computed 的設計初衷也正是用于解決此類問題。
對比偵聽器 watch當然很多時候我們使用 computed 時往往會與 Vue 中另一個 API 也就是偵聽器 watch 相比較,因為在某些方面它們是一致的,都是以 Vue 的依賴追蹤機制為基礎,當某個依賴數據發生變化時,所有依賴這個數據的相關數據或函數都會自動發生變化或調用。
雖然計算屬性在大多數情況下更合適,但有時也需要一個自定義的偵聽器。這就是為什么 Vue 通過 watch 選項提供了一個更通用的方法來響應數據的變化。當需要在數據變化時執行異步或開銷較大的操作時,這個方式是最有用的。
從 Vue 官方文檔對 watch 的解釋我們可以了解到,使用 watch 選項允許我們執行異步操作(訪問一個 API)或高消耗性能的操作,限制我們執行該操作的頻率,并在我們得到最終結果前,設置中間狀態,而這些都是計算屬性無法做到的。
下面還另外總結了幾點關于 computed 和 watch 的差異:
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; 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編 / 熒聲
本文已由作者授權發布,版權屬于創宇前端。歡迎注明出處轉載本文。本文鏈接:https://knownsec-fed.com/2018...
想要訂閱更多來自知道創宇開發一線的分享,請搜索關注我們的微信公眾號:創宇前端(KnownsecFED)。歡迎留言討論,我們會盡可能回復。
感謝您的閱讀。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/97739.html
摘要:雖然計算屬性在大多數情況下更合適,但有時也需要一個自定義的偵聽器。當某個屬性發生變化,觸發攔截函數,然后調用自身消息訂閱器的方法,遍歷當前中保存著所有訂閱者的數組,并逐個調用的方法,完成響應更新。 雖然目前的技術棧已由Vue轉到了React,但從之前使用Vue開發的多個項目實際經歷來看還是非常愉悅的,Vue文檔清晰規范,api設計簡潔高效,對前端開發人員友好,上手快,甚至個人認為在很多...
摘要:前言一直混跡社區突然發現自己收藏了不少好文但是管理起來有點混亂所以將前端主流技術做了一個書簽整理不求最多最全但求最實用。 前言 一直混跡社區,突然發現自己收藏了不少好文但是管理起來有點混亂; 所以將前端主流技術做了一個書簽整理,不求最多最全,但求最實用。 書簽源碼 書簽導入瀏覽器效果截圖showImg(https://segmentfault.com/img/bVbg41b?w=107...
摘要:畢業之后就在一直合肥小公司工作,沒有老司機沒有技術氛圍,在技術的道路上我只能獨自摸索。于是乎,我果斷辭職,在新年開工之際來到杭州,這里的互聯網公司應該是合肥的幾十倍吧。。。。 畢業之后就在一直合肥小公司工作,沒有老司機、沒有技術氛圍,在技術的道路上我只能獨自摸索。老板也只會畫餅充饑,前途一片迷茫看不到任何希望。于是乎,我果斷辭職,在新年開工之際來到杭州,這里的互聯網公司應該是合肥的幾十...
摘要:哪吒社區技能樹打卡打卡貼函數式接口簡介領域優質創作者哪吒公眾號作者架構師奮斗者掃描主頁左側二維碼,加入群聊,一起學習一起進步歡迎點贊收藏留言前情提要無意間聽到領導們的談話,現在公司的現狀是碼農太多,但能獨立帶隊的人太少,簡而言之,不缺干 ? 哪吒社區Java技能樹打卡?【打卡貼 day2...
摘要:聲明的變量不得改變值,這意味著,一旦聲明變量,就必須立即初始化,不能留到以后賦值。 雖然今年沒有換工作的打算 但為了跟上時代的腳步 還是忍不住整理了一份最新前端知識點 知識點匯總 1.HTML HTML5新特性,語義化瀏覽器的標準模式和怪異模式xhtml和html的區別使用data-的好處meta標簽canvasHTML廢棄的標簽IE6 bug,和一些定位寫法css js放置位置和原因...
閱讀 2649·2019-08-30 15:52
閱讀 3596·2019-08-29 17:02
閱讀 1844·2019-08-29 13:00
閱讀 922·2019-08-29 11:07
閱讀 3238·2019-08-27 10:53
閱讀 1770·2019-08-26 13:43
閱讀 1016·2019-08-26 10:22
閱讀 1332·2019-08-23 18:06