Vue中計算屬性computed的實現(xiàn)原理

基本介紹

話不多說毅往,一個最基本的例子如下:

<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}}侯嘀,因為在模版中放入太多聲明式的邏輯會讓模板本身過重诗茎,尤其當(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é)了幾點(diǎn)關(guān)于computed和watch的差異:

  1. computed是計算一個新的屬性,并將該屬性掛載到vm(Vue實例)上,而watch是監(jiān)聽已經(jīng)存在且已掛載到vm上的數(shù)據(jù)然想,所以用watch同樣可以監(jiān)聽computed計算屬性的變化(其它還有data狠半、props)
  2. computed本質(zhì)是一個惰性求值的觀察者已日,具有緩存性堂鲜,只有當(dāng)依賴變化后,第一次訪問 computed 屬性建椰,才會計算新的值湿诊,而watch則是當(dāng)數(shù)據(jù)發(fā)生變化便會調(diào)用執(zhí)行函數(shù)
  3. 從使用場景上說,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)肖爵,其核心有三點(diǎn):observe卢鹦、watcher、dep:

  1. observe:遍歷data中的屬性劝堪,使用 Object.definePropertyget/set方法對其進(jìn)行數(shù)據(jù)劫持
  2. dep:每個屬性擁有自己的消息訂閱器dep冀自,用于存放所有訂閱了該屬性的觀察者對象
  3. 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)
      }
    }
  }
}

從這段代碼開始我們觀察這幾部分:

  1. 獲取計算屬性的定義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ù)绣张。

  1. 計算屬性的觀察者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()
    }
  }
}

為了簡潔突出重點(diǎn)嗦明,這里我手動去掉了我們暫時不需要關(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<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()
    }
  }
}

// 當(dāng)首次計算 computed 屬性的值時诗良,Dep 將會在計算期間對依賴進(jìn)行收集
Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
  // 在一次依賴收集期間汹桦,如果有其他依賴收集任務(wù)開始(比如:當(dāng)前 computed 計算屬性嵌套其他 computed 計算屬性),
  // 那么將會把當(dāng)前 target 暫存到 targetStack鉴裹,先進(jìn)行其他 target 的依賴收集舞骆,
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  // 當(dāng)嵌套的依賴收集任務(wù)完成后,將 target 恢復(fù)為上一層的 Watcher壹罚,并繼續(xù)做依賴收集
  Dep.target = targetStack.pop()
}

dep同樣精簡了部分代碼葛作,我們觀察Watcher和dep的關(guān)系,用一句話總結(jié)

watcher中實例化了dep并向dep.subs中添加了訂閱者猖凛,dep通過notify遍歷了dep.subs通知每個watcher更新赂蠢。

  1. 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í)行g(shù)et訪問函數(shù),從而關(guān)聯(lián)上觀察者對象watcher遇绞。

分析完以上步驟键袱,我們再來梳理下整個流程:

  1. 當(dāng)組件初始化的時候,computeddata會分別建立各自的響應(yīng)系統(tǒng)摹闽,Observer遍歷data中每個屬性設(shè)置get/set數(shù)據(jù)攔截
  2. 初始化computed會調(diào)用initComputed函數(shù)
  • 注冊一個watcher實例蹄咖,并在內(nèi)實例化一個Dep消息訂閱器用作后續(xù)收集依賴(比如渲染函數(shù)的watcher或者其他觀察該計算屬性變化的watcher)
  • 調(diào)用計算屬性時會觸發(fā)其Object.defineProperty的get訪問器函數(shù)
  • 調(diào)用watcher.depend()方法向自身的消息訂閱器depsubs中添加其他屬性的watcher
  • 調(diào)用watcherevaluate方法(進(jìn)而調(diào)用watcherget方法)讓自身成為其他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é)果。
  1. 當(dāng)某個屬性發(fā)生變化务蝠,觸發(fā)set攔截函數(shù)拍谐,然后調(diào)用自身消息訂閱器depnotify方法,遍歷當(dāng)前dep中保存著所有訂閱者wathcersubs數(shù)組馏段,并逐個調(diào)用watcherupdate方法轩拨,完成響應(yīng)更新。

總結(jié)

1.初始化 data 和 computed院喜,分別代理其 set 和 get 方法亡蓉,對 data 中的所有屬性生成唯一的 dep 實例

2.對 computed 中的 屬性生成唯一的 watcher,并保存在 vm._computedWatchers 中

3.訪問計算屬性時喷舀,設(shè)置 Dep.target 指向 計算屬性的 watcher砍濒,調(diào)用該屬性具體方法

4.方法中訪問 data 的屬性,即會調(diào)用 data 屬性的 get 方法硫麻,將 data 屬性的 dep 加入到 計算屬性的 watcher 爸邢, 同時該 dep 中的 subs 添加這個 watcher

5.設(shè)置 data 的這個屬性時,調(diào)用該屬性代理的 set 方法拿愧,觸發(fā) dep 的 notify 方法

6.因為時 computed 屬性杠河,只是將 watcher 中的 dirty 設(shè)置為 true

7.最后,訪問計算屬性的 get 方法時浇辜,得知該屬性的 watcher.dirty 為 true券敌,則調(diào)用 watcher.evaluate() 方法獲取新的值

綜合以上:也可以解釋了為什么有些時候當(dāng)computed沒有被訪問(或者沒有被模板依賴),當(dāng)修改了this.data值后柳洋,通過vue-tools發(fā)現(xiàn)其computed中的值沒有變化的原因待诅,因為沒有觸發(fā)到其get方法。

參考文章:
https://segmentfault.com/a/1190000010408657
https://segmentfault.com/a/1190000016368913?utm_source=tag-newest

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末熊镣,一起剝皮案震驚了整個濱河市卑雁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绪囱,老刑警劉巖测蹲,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異毕箍,居然都是意外死亡弛房,警方通過查閱死者的電腦和手機(jī)道盏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門而柑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來文捶,“玉大人,你說我怎么就攤上這事媒咳〈馀牛” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵涩澡,是天一觀的道長顽耳。 經(jīng)常有香客問我,道長妙同,這世上最難降的妖魔是什么射富? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮粥帚,結(jié)果婚禮上胰耗,老公的妹妹穿的比我還像新娘。我一直安慰自己芒涡,他們只是感情好柴灯,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著费尽,像睡著了一般赠群。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上旱幼,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天查描,我揣著相機(jī)與錄音,去河邊找鬼速警。 笑死叹誉,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的闷旧。 我是一名探鬼主播长豁,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼忙灼!你這毒婦竟也來了匠襟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤该园,失蹤者是張志新(化名)和其女友劉穎酸舍,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體里初,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡啃勉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了双妨。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淮阐。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡叮阅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出泣特,到底是詐尸還是另有隱情浩姥,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布状您,位于F島的核電站勒叠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏膏孟。R本人自食惡果不足惜眯分,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柒桑。 院中可真熱鬧颗搂,春花似錦、人聲如沸幕垦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽先改。三九已至疚察,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間仇奶,已是汗流浹背貌嫡。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留该溯,地道東北人岛抄。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓,卻偏偏與公主長得像狈茉,于是被迫代替她去往敵國和親夫椭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 計算屬性適合用在模版渲染當(dāng)中氯庆,某個值是依賴了其他響應(yīng)式對象甚至是計算屬性計算而來的蹭秋。 偵聽屬性適用在觀測某個值的變...
    LoveBugs_King閱讀 1,536評論 0 0
  • 這方面的文章很多,但是我感覺很多寫的比較抽象堤撵,本文會通過舉例更詳細(xì)的解釋仁讨。(此文面向的Vue新手們,如果你是個大牛...
    Ivy_2016閱讀 15,393評論 8 64
  • 前言 Vue.js 的核心包括一套“響應(yīng)式系統(tǒng)”实昨。 “響應(yīng)式”洞豁,是指當(dāng)數(shù)據(jù)改變后,Vue 會通知到使用該數(shù)據(jù)的代碼...
    NARUTO_86閱讀 37,508評論 8 86
  • 回憶 watch的過程就是訂閱數(shù)據(jù),數(shù)據(jù)更新時執(zhí)行回調(diào)函數(shù)丈挟。關(guān)于渲染闰挡,渲染W(wǎng)atcher本身就訂閱了數(shù)據(jù)變化,us...
    LoveBugs_King閱讀 743評論 0 0
  • Vue 依賴收集原理分析 Vue實例在初始化時礁哄,可以接受以下幾類數(shù)據(jù): 模板 初始化數(shù)據(jù) 傳遞給組件的屬性值 co...
    wuww閱讀 6,874評論 3 19