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