首頁  >  文章  >  web前端  >  Vue中computed的實作原理是什麼?

Vue中computed的實作原理是什麼?

不言
不言原創
2018-09-14 16:10:063860瀏覽
這篇文章帶給大家的內容是關於Vue中computed的實作原理是什麼?有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

雖然目前的技術堆疊已由Vue 轉到了React,但從之前使用Vue 開發的多個專案實際經歷來看還是非常愉悅的,Vue 文檔清晰規範,api 設計簡潔高效,對前端開發人員友好,上手快,甚至個人認為在很多場景使用Vue 比React 開發效率更高,之前也有斷斷續續研讀過Vue 的源碼,但一直沒有梳理總結,所以在此做一些技術歸納同時也加深自己對Vue 的理解,那麼今天要寫的便是Vue 中最常用到的API 之一computed 的實作原理。

基本介紹

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

<div>
    <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}},因為在模版中放入太多宣告式的邏輯會讓模板本身過重,尤其當在頁面中使用大量複雜的邏輯表達式處理資料時,會對頁面的可維護性造成很大的影響,而computed 的設計初衷也正是用來解決這類問題。

對比偵聽器watch

當然很多時候我們使用computed 時往往會與Vue 中另一個API 也就是偵聽器watch 比較,因為在某些方面它們是一致的,都是以Vue 的依賴追蹤機制為基礎,當某個依賴資料發生變化時,所有依賴這個資料的相關資料或函數都會自動發生變化或調用。

雖然計算屬性在大多數情況下更合適,但有時也需要一個自訂的偵聽器。這就是為什麼 Vue 透過 watch 選項提供了一個更通用的方法來回應資料的變化。當需要在資料變更時執行非同步或開銷較大的操作時,這個方式是最有用的。

從Vue 官方文件對watch 的解釋我們可以了解到,使用 watch  選項允許我們執行非同步操作(存取一個API)或高消耗效能的操作,限制我們執行該操作的頻率,並在我們得到最終結果之前,設定中間狀態,而這些都是計算屬性無法做到的。

下面也總結了幾點關於computedwatch 的差異:

  1. ##computed 是計算一個新的屬性,並將該屬性掛載到vm(Vue 實例)上,而watch 是監聽已經存在且已掛載到vm 上的數據,所以用watch 同樣可以監聽computed 計算屬性的變化(其它還有dataprops

  2. computed 基本上是一個惰性求值的觀察者,具有快取性,只有當依賴變更後,第一次存取 computed  屬性,才會計算新的值,而watch 則是當資料發生變化便會呼叫執行函數

  3. 從使用場景上說,

    computed 適用一個資料被多個資料影響,而watch 適用一個資料影響多個資料;

  4. ##以上我們了解了
computed

watch 之間的一些差異和使用場景的區別,當然某些時候兩者並沒有那麼明確嚴格的限制,最後還是要具體到不同的業務進行分析。 原理分析

言歸正傳,回到文章的主題

computed

身上,為了更深層地了解計算屬性的內在機制,接下來就讓我們一步一步探索Vue源碼中關於它的實作原理吧。 在分析

computed

原始碼之前我們先得對Vue 的響應式系統有一個基本的了解,Vue 稱其為非侵入性的響應式系統,資料模型僅僅是普通的JavaScript 對象,而當你修改它們時,視圖就會進行自動更新。

Vue中computed的實作原理是什麼?當你把一個普通的JavaScript 物件傳給Vue 實例的data

選項時,Vue 將遍歷此物件所有的屬性,並且使用Object.defineProperty 把這些屬性全部轉為getter/setter,這些getter/setter 對使用者來說是不可見的,但是在內部它們讓Vue 追蹤依賴,在屬性被存取和修改時通知變化,每個元件實例都有對應的watcher 實例對象,它會在元件渲染的過程中把屬性記錄為依賴,之後當依賴項的setter 被呼叫時,會通知watcher 重新計算,從而致使它關聯的元件得以更新。 Vue 回應系統,其核心有三點:observe

watcherdep<ol class=" list-paddingleft-2"> <li><p><code>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 函数(其前后也分别初始化了 initDatainitWatch )并传入两个参数 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)
          }
        }
      }
    }

    从这段代码开始我们观察这几部分:

    1. 获取计算属性的定义 userDefgetter 求值函数

      const userDef = computed[key]
      const getter = typeof userDef === 'function' ? userDef : userDef.get

      定义一个计算属性有两种写法,一种是直接跟一个函数,另一种是添加 setget 方法的对象形式,所以这里首先获取计算属性的定义 userDef,再根据 userDef 的类型获取相应的 getter 求值函数。

    2. 计算属性的观察者 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()
          }
        }
      }

      为了简洁突出重点,这里我手动去掉了我们暂时不需要关心的代码片段。
      观察 Watcherconstructor ,结合刚才讲到的 new Watcher 传入的第四个参数 {computed:true} 知道,对于计算属性而言 watcher 会执行 if 条件成立的代码 this.dep = new Dep(),而 dep 也就是创建了该属性的消息订阅器。

      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 <p><code>Dep</code> 同样精简了部分代码,我们观察 <code>Watcher</code> 和 <code>Dep</code> 的关系,用一句话总结</p>
      <blockquote>
      <code>watcher</code> 中实例化了 <code>dep</code> 并向 <code>dep.subs</code> 中添加了订阅者,<code>dep</code> 通过 <code>notify</code> 遍历了 <code>dep.subs</code> 通知每个 <code>watcher</code> 更新。</blockquote></watcher>
    3. 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 前面的代码可以看到 sharedPropertyDefinitionget/set 方法在经过 userDef 和  shouldCache 等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinitionget 函数也就是 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() 计算求值。

    分析完所有步驟,我們再來總結下整個流程:

    1. 當元件初始化的時候,computed data 會分別建立各自的回應系統,Observer 遍歷data 中每個屬性設定get/set 資料攔截

      1. #初始化computed 會呼叫initComputed 函數

      2. ##註冊一個

        watcher 實例,並在內實例化一個Dep 訊息訂閱器​​用作後續收集依賴(例如渲染函數的watcher

        或其他觀察該計算屬性變化的
      3. watcher
      4. 呼叫計算屬性時會觸發其Object.definePropertyget存取器函數

      5. watcher.depend() 方法將其他屬性的watcher

      ##呼叫
    2. watcher

      evaluate 方法(進而呼叫watcherget 方法)讓自己成為其他 watcher 的訊息訂閱器​​的訂閱者,首先將watcher 賦給Dep.target,然後執行getter 求值函數,當存取求值函數裡面的屬性(例如來自data

      props
    3. 或其他
    computed

    )時,會同樣觸發它們的

    get

    存取器函數從而將該計算屬性的watcher 加到求值函數中屬性的
    watcher

    的訊息訂閱器​​

    dep 中,當這些操作完成,最後關閉 Dep.target

    賦為

    null

    並傳回求值函數結果。 ###############當某個屬性發生變化,觸發###set### 攔截函數,然後呼叫自身訊息訂閱器​​###dep### 的### notify### 方法,遍歷目前###dep### 中保存著所有訂閱者###wathcer### 的###subs### 數組,並逐一呼叫###watcher### 的 # ##update### 方法,完成回應更新。 ############相關推薦:#########javascript - 刷票器的實作原則###############thinkphp 控制器中display()步驟實作的原理############

    以上是Vue中computed的實作原理是什麼?的詳細內容。更多資訊請關注PHP中文網其他相關文章!

    陳述:
    本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn