摘要:當(dāng)某個屬性發(fā)生變化,觸發(fā)攔截函數(shù),然后調(diào)用自身消息訂閱器的方法,遍歷當(dāng)前中保存著所有訂閱者的數(shù)組,并逐個調(diào)用的方法,完成響應(yīng)更新。
編者按:我們會不時邀請工程師談?wù)動幸馑嫉募夹g(shù)細(xì)節(jié),希望知其所以然能讓大家在面試有更出色表現(xiàn)。也給面試官提供更多思路。
雖然目前的技術(shù)棧已由 Vue 轉(zhuǎn)到了 React,但從之前使用 Vue 開發(fā)的多個項目實際經(jīng)歷來看還是非常愉悅的,Vue 文檔清晰規(guī)范,api 設(shè)計簡潔高效,對前端開發(fā)人員友好,上手快,甚至個人認(rèn)為在很多場景使用 Vue 比 React 開發(fā)效率更高,之前也有斷斷續(xù)續(xù)研讀過 Vue 的源碼,但一直沒有梳理總結(jié),所以在此做一些技術(shù)歸納同時也加深自己對 Vue 的理解,那么今天要寫的便是 Vue 中最常用到的 API 之一 computed 的實現(xiàn)原理。
基本介紹話不多說,一個最基本的例子如下:
{{fullName}}
new Vue({ data: { firstName: "Xiao", lastName: "Ming" }, computed: { fullName: function () { return this.firstName + " " + this.lastName } } })
Vue 中我們不需要在 template 里面直接計算 {{this.firstName + " " + this.lastName}},因為在模版中放入太多聲明式的邏輯會讓模板本身過重,尤其當(dāng)在頁面中使用大量復(fù)雜的邏輯表達(dá)式處理數(shù)據(jù)時,會對頁面的可維護(hù)性造成很大的影響,而 computed 的設(shè)計初衷也正是用于解決此類問題。
對比偵聽器 watch當(dāng)然很多時候我們使用 computed 時往往會與 Vue 中另一個 API 也就是偵聽器 watch 相比較,因為在某些方面它們是一致的,都是以 Vue 的依賴追蹤機(jī)制為基礎(chǔ),當(dāng)某個依賴數(shù)據(jù)發(fā)生變化時,所有依賴這個數(shù)據(jù)的相關(guān)數(shù)據(jù)或函數(shù)都會自動發(fā)生變化或調(diào)用。
雖然計算屬性在大多數(shù)情況下更合適,但有時也需要一個自定義的偵聽器。這就是為什么 Vue 通過 watch 選項提供了一個更通用的方法來響應(yīng)數(shù)據(jù)的變化。當(dāng)需要在數(shù)據(jù)變化時執(zhí)行異步或開銷較大的操作時,這個方式是最有用的。
從 Vue 官方文檔對 watch 的解釋我們可以了解到,使用 watch 選項允許我們執(zhí)行異步操作(訪問一個 API)或高消耗性能的操作,限制我們執(zhí)行該操作的頻率,并在我們得到最終結(jié)果前,設(shè)置中間狀態(tài),而這些都是計算屬性無法做到的。
下面還另外總結(jié)了幾點關(guān)于 computed 和 watch 的差異:
computed 是計算一個新的屬性,并將該屬性掛載到 vm(Vue 實例)上,而 watch 是監(jiān)聽已經(jīng)存在且已掛載到 vm 上的數(shù)據(jù),所以用 watch 同樣可以監(jiān)聽 computed 計算屬性的變化(其它還有 data、props)
computed 本質(zhì)是一個惰性求值的觀察者,具有緩存性,只有當(dāng)依賴變化后,第一次訪問 computed 屬性,才會計算新的值,而 watch 則是當(dāng)數(shù)據(jù)發(fā)生變化便會調(diào)用執(zhí)行函數(shù)
從使用場景上說,computed 適用一個數(shù)據(jù)被多個數(shù)據(jù)影響,而 watch 適用一個數(shù)據(jù)影響多個數(shù)據(jù);
以上我們了解了 computed 和 watch 之間的一些差異和使用場景的區(qū)別,當(dāng)然某些時候兩者并沒有那么明確嚴(yán)格的限制,最后還是要具體到不同的業(yè)務(wù)進(jìn)行分析。
原理分析言歸正傳,回到文章的主題 computed 身上,為了更深層次地了解計算屬性的內(nèi)在機(jī)制,接下來就讓我們一步步探索 Vue 源碼中關(guān)于它的實現(xiàn)原理吧。
在分析 computed 源碼之前我們先得對 Vue 的響應(yīng)式系統(tǒng)有一個基本的了解,Vue 稱其為非侵入性的響應(yīng)式系統(tǒng),數(shù)據(jù)模型僅僅是普通的 JavaScript 對象,而當(dāng)你修改它們時,視圖便會進(jìn)行自動更新。
當(dāng)你把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項時,Vue 將遍歷此對象所有的屬性,并使用 Object.defineProperty 把這些屬性全部轉(zhuǎn)為 getter/setter,這些 getter/setter 對用戶來說是不可見的,但是在內(nèi)部它們讓 Vue 追蹤依賴,在屬性被訪問和修改時通知變化,每個組件實例都有相應(yīng)的 watcher 實例對象,它會在組件渲染的過程中把屬性記錄為依賴,之后當(dāng)依賴項的 setter 被調(diào)用時,會通知 watcher 重新計算,從而致使它關(guān)聯(lián)的組件得以更新。
Vue 響應(yīng)系統(tǒng),其核心有三點:observe、watcher、dep:
observe:遍歷 data 中的屬性,使用 Object.defineProperty 的 get/set 方法對其進(jìn)行數(shù)據(jù)劫持;
dep:每個屬性擁有自己的消息訂閱器 dep,用于存放所有訂閱了該屬性的觀察者對象;
watcher:觀察者(對象),通過 dep 實現(xiàn)對響應(yīng)屬性的監(jiān)聽,監(jiān)聽到結(jié)果后,主動觸發(fā)自己的回調(diào)進(jìn)行響應(yīng)。
對響應(yīng)式系統(tǒng)有一個初步了解后,我們再來分析計算屬性。
首先我們找到計算屬性的初始化是在 src/core/instance/state.js 文件中的 initState 函數(shù)中完成的
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) } }
調(diào)用了 initComputed 函數(shù)(其前后也分別初始化了 initData 和 initWatch )并傳入兩個參數(shù) vm 實例和 opt.computed 開發(fā)者定義的 computed 選項,轉(zhuǎn)到 initComputed 函數(shù):
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 求值函數(shù)
const userDef = computed[key] const getter = typeof userDef === "function" ? userDef : userDef.get
定義一個計算屬性有兩種寫法,一種是直接跟一個函數(shù),另一種是添加 set 和 get 方法的對象形式,所以這里首先獲取計算屬性的定義 userDef,再根據(jù) userDef 的類型獲取相應(yīng)的 getter 求值函數(shù)。
計算屬性的觀察者 watcher 和消息訂閱器 dep
watchers[key] = new Watcher( vm, getter || noop, noop, computedWatcherOptions )
這里的 watchers 也就是 vm._computedWatchers 對象的引用,存放了每個計算屬性的觀察者 watcher 實例(注:后文中提到的“計算屬性的觀察者”、“訂閱者”和 watcher 均指代同一個意思但注意和 Watcher 構(gòu)造函數(shù)區(qū)分),Watcher 構(gòu)造函數(shù)在實例化時傳入了 4 個參數(shù):vm 實例、getter 求值函數(shù)、noop 空函數(shù)、computedWatcherOptions 常量對象(在這里提供給 Watcher 一個標(biāo)識 {computed:true} 項,表明這是一個計算屬性而不是非計算屬性的觀察者,我們來到 Watcher 構(gòu)造函數(shù)的定義:
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() } } }
為了簡潔突出重點,這里我手動去掉了我們暫時不需要關(guān)心的代碼片段。
觀察 Watcher 的 constructor ,結(jié)合剛才講到的 new Watcher 傳入的第四個參數(shù) {computed:true} 知道,對于計算屬性而言 watcher 會執(zhí)行 if 條件成立的代碼 this.dep = new Dep(),而 dep 也就是創(chuàng)建了該屬性的消息訂閱器。
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 的關(guān)系,用一句話總結(jié)
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 屬性是直接掛載到實例對象中的,所以在定義之前需要判斷對象中是否已經(jīng)存在重名的屬性,defineComputed 傳入了三個參數(shù):vm 實例、計算屬性的 key 以及 userDef 計算屬性的定義(對象或函數(shù))。
然后繼續(xù)找到 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) }
在這段代碼的最后調(diào)用了原生 Object.defineProperty 方法,其中傳入的第三個參數(shù)是屬性描述符sharedPropertyDefinition,初始化為:
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop }
隨后根據(jù) Object.defineProperty 前面的代碼可以看到 sharedPropertyDefinition 的 get/set 方法在經(jīng)過 userDef 和 shouldCache 等多重判斷后被重寫,當(dāng)非服務(wù)端渲染時,sharedPropertyDefinition 的 get 函數(shù)也就是 createComputedGetter(key) 的結(jié)果,我們找到 createComputedGetter 函數(shù)調(diào)用結(jié)果并最終改寫 sharedPropertyDefinition 大致呈現(xiàn)如下:
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 }
當(dāng)計算屬性被調(diào)用時便會執(zhí)行 get 訪問函數(shù),從而關(guān)聯(lián)上觀察者對象 watcher 然后執(zhí)行 wather.depend() 收集依賴和 watcher.evaluate() 計算求值。
分析完所有步驟,我們再來總結(jié)下整個流程:當(dāng)組件初始化的時候,computed 和 data 會分別建立各自的響應(yīng)系統(tǒng),Observer 遍歷 data 中每個屬性設(shè)置 get/set 數(shù)據(jù)攔截
初始化 computed 會調(diào)用 initComputed 函數(shù)
注冊一個 watcher 實例,并在內(nèi)實例化一個 Dep 消息訂閱器用作后續(xù)收集依賴(比如渲染函數(shù)的 watcher 或者其他觀察該計算屬性變化的 watcher )
調(diào)用計算屬性時會觸發(fā)其Object.defineProperty的get訪問器函數(shù)
調(diào)用 watcher.depend() 方法向自身的消息訂閱器 dep 的 subs 中添加其他屬性的 watcher
調(diào)用 watcher 的 evaluate 方法(進(jìn)而調(diào)用 watcher 的 get 方法)讓自身成為其他 watcher 的消息訂閱器的訂閱者,首先將 watcher 賦給 Dep.target,然后執(zhí)行 getter 求值函數(shù),當(dāng)訪問求值函數(shù)里面的屬性(比如來自 data、props 或其他 computed)時,會同樣觸發(fā)它們的 get 訪問器函數(shù)從而將該計算屬性的 watcher 添加到求值函數(shù)中屬性的 watcher 的消息訂閱器 dep 中,當(dāng)這些操作完成,最后關(guān)閉 Dep.target 賦為 null 并返回求值函數(shù)結(jié)果。
當(dāng)某個屬性發(fā)生變化,觸發(fā) set 攔截函數(shù),然后調(diào)用自身消息訂閱器 dep 的 notify 方法,遍歷當(dāng)前 dep 中保存著所有訂閱者 wathcer 的 subs 數(shù)組,并逐個調(diào)用 watcher 的 update 方法,完成響應(yīng)更新。
文 / 亦然
一枚向往詩與遠(yuǎn)方的 coder編 / 熒聲
本文已由作者授權(quán)發(fā)布,版權(quán)屬于創(chuàng)宇前端。歡迎注明出處轉(zhuǎn)載本文。本文鏈接:https://knownsec-fed.com/2018...
想要訂閱更多來自知道創(chuàng)宇開發(fā)一線的分享,請搜索關(guān)注我們的微信公眾號:創(chuàng)宇前端(KnownsecFED)。歡迎留言討論,我們會盡可能回復(fù)。
感謝您的閱讀。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/97739.html
摘要:雖然計算屬性在大多數(shù)情況下更合適,但有時也需要一個自定義的偵聽器。當(dāng)某個屬性發(fā)生變化,觸發(fā)攔截函數(shù),然后調(diào)用自身消息訂閱器的方法,遍歷當(dāng)前中保存著所有訂閱者的數(shù)組,并逐個調(diào)用的方法,完成響應(yīng)更新。 雖然目前的技術(shù)棧已由Vue轉(zhuǎn)到了React,但從之前使用Vue開發(fā)的多個項目實際經(jīng)歷來看還是非常愉悅的,Vue文檔清晰規(guī)范,api設(shè)計簡潔高效,對前端開發(fā)人員友好,上手快,甚至個人認(rèn)為在很多...
摘要:前言一直混跡社區(qū)突然發(fā)現(xiàn)自己收藏了不少好文但是管理起來有點混亂所以將前端主流技術(shù)做了一個書簽整理不求最多最全但求最實用。 前言 一直混跡社區(qū),突然發(fā)現(xiàn)自己收藏了不少好文但是管理起來有點混亂; 所以將前端主流技術(shù)做了一個書簽整理,不求最多最全,但求最實用。 書簽源碼 書簽導(dǎo)入瀏覽器效果截圖showImg(https://segmentfault.com/img/bVbg41b?w=107...
摘要:畢業(yè)之后就在一直合肥小公司工作,沒有老司機(jī)沒有技術(shù)氛圍,在技術(shù)的道路上我只能獨自摸索。于是乎,我果斷辭職,在新年開工之際來到杭州,這里的互聯(lián)網(wǎng)公司應(yīng)該是合肥的幾十倍吧。。。。 畢業(yè)之后就在一直合肥小公司工作,沒有老司機(jī)、沒有技術(shù)氛圍,在技術(shù)的道路上我只能獨自摸索。老板也只會畫餅充饑,前途一片迷茫看不到任何希望。于是乎,我果斷辭職,在新年開工之際來到杭州,這里的互聯(lián)網(wǎng)公司應(yīng)該是合肥的幾十...
摘要:哪吒社區(qū)技能樹打卡打卡貼函數(shù)式接口簡介領(lǐng)域優(yōu)質(zhì)創(chuàng)作者哪吒公眾號作者架構(gòu)師奮斗者掃描主頁左側(cè)二維碼,加入群聊,一起學(xué)習(xí)一起進(jìn)步歡迎點贊收藏留言前情提要無意間聽到領(lǐng)導(dǎo)們的談話,現(xiàn)在公司的現(xiàn)狀是碼農(nóng)太多,但能獨立帶隊的人太少,簡而言之,不缺干 ? 哪吒社區(qū)Java技能樹打卡?【打卡貼 day2...
摘要:聲明的變量不得改變值,這意味著,一旦聲明變量,就必須立即初始化,不能留到以后賦值。 雖然今年沒有換工作的打算 但為了跟上時代的腳步 還是忍不住整理了一份最新前端知識點 知識點匯總 1.HTML HTML5新特性,語義化瀏覽器的標(biāo)準(zhǔn)模式和怪異模式xhtml和html的區(qū)別使用data-的好處meta標(biāo)簽canvasHTML廢棄的標(biāo)簽IE6 bug,和一些定位寫法css js放置位置和原因...
閱讀 2647·2019-08-30 15:52
閱讀 3594·2019-08-29 17:02
閱讀 1843·2019-08-29 13:00
閱讀 918·2019-08-29 11:07
閱讀 3235·2019-08-27 10:53
閱讀 1767·2019-08-26 13:43
閱讀 1011·2019-08-26 10:22
閱讀 1322·2019-08-23 18:06