摘要:雖然計算屬性在大多數(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)為在很多場景使用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),而這些都是計算屬性無法做到的。
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。
分析完以上步驟,我們再來梳理下整個流程:
當(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)更新。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/97697.html
摘要:當(dāng)某個屬性發(fā)生變化,觸發(fā)攔截函數(shù),然后調(diào)用自身消息訂閱器的方法,遍歷當(dāng)前中保存著所有訂閱者的數(shù)組,并逐個調(diào)用的方法,完成響應(yīng)更新。 編者按:我們會不時邀請工程師談?wù)動幸馑嫉募夹g(shù)細(xì)節(jié),希望知其所以然能讓大家在面試有更出色表現(xiàn)。也給面試官提供更多思路。 showImg(https://segmentfault.com/img/bVbgYyU?w=1200&h=600); 雖然目前的技術(shù)...
摘要:畢業(yè)之后就在一直合肥小公司工作,沒有老司機(jī)沒有技術(shù)氛圍,在技術(shù)的道路上我只能獨自摸索。于是乎,我果斷辭職,在新年開工之際來到杭州,這里的互聯(lián)網(wǎng)公司應(yīng)該是合肥的幾十倍吧。。。。 畢業(yè)之后就在一直合肥小公司工作,沒有老司機(jī)、沒有技術(shù)氛圍,在技術(shù)的道路上我只能獨自摸索。老板也只會畫餅充饑,前途一片迷??床坏饺魏蜗M?。于是乎,我果斷辭職,在新年開工之際來到杭州,這里的互聯(lián)網(wǎng)公司應(yīng)該是合肥的幾十...
摘要:前言最近在學(xué)習(xí)計算屬性的源碼,發(fā)現(xiàn)和普通的響應(yīng)式變量內(nèi)部的實現(xiàn)還有一些不同,特地寫了這篇博客,記錄下自己學(xué)習(xí)的成果文中的源碼截圖只保留核心邏輯完整源碼地址可能需要了解一些響應(yīng)式的原理版本計算屬性的概念一般的計算屬性值是一個函數(shù),這個函數(shù)showImg(https://user-gold-cdn.xitu.io/2019/5/6/16a8b98f1361f6f6); 前言 最近在學(xué)習(xí)Vue計...
摘要:前言一直混跡社區(qū)突然發(fā)現(xiàn)自己收藏了不少好文但是管理起來有點混亂所以將前端主流技術(shù)做了一個書簽整理不求最多最全但求最實用。 前言 一直混跡社區(qū),突然發(fā)現(xiàn)自己收藏了不少好文但是管理起來有點混亂; 所以將前端主流技術(shù)做了一個書簽整理,不求最多最全,但求最實用。 書簽源碼 書簽導(dǎo)入瀏覽器效果截圖showImg(https://segmentfault.com/img/bVbg41b?w=107...
摘要:如果沒有緩存,我們將不可避免的多次執(zhí)行的現(xiàn)在我們要開始講解,是如何判斷是否使用緩存的首先計算后,會把計算得到的值保存到一個變量中。當(dāng)使用緩存時,就直接返回這個變量。 寫文章不容易,點個贊唄兄弟專注 Vue 源碼分享,文章分為白話版和 源碼版,白話版助于理解工作原理,源碼版助于了解內(nèi)部詳情,讓我們一起學(xué)習(xí)吧研究基于 Vue版本 【2.5.17】 如果你覺得排版難看,請點擊 下面鏈接 或...
閱讀 1452·2023-04-25 19:00
閱讀 4150·2021-11-17 17:00
閱讀 1764·2021-11-11 16:55
閱讀 1523·2021-10-14 09:43
閱讀 3120·2021-09-30 09:58
閱讀 856·2021-09-02 15:11
閱讀 2127·2019-08-30 12:56
閱讀 1405·2019-08-30 11:12