手摸手帶你理解Vue的Computed原理

来源:https://www.cnblogs.com/chanwahfung/archive/2020/06/26/13193897.html
-Advertisement-
Play Games

前言 computed 在 Vue 中是很常用的屬性配置,它能夠隨著依賴屬性的變化而變化,為我們帶來很大便利。那麼本文就來帶大家全面理解 computed 的內部原理以及工作流程。 在這之前,希望你能夠對響應式原理有一些理解,因為 computed 是基於響應式原理進行工作。如果你對響應式原理還不是 ...


前言

computedVue 中是很常用的屬性配置,它能夠隨著依賴屬性的變化而變化,為我們帶來很大便利。那麼本文就來帶大家全面理解 computed 的內部原理以及工作流程。

在這之前,希望你能夠對響應式原理有一些理解,因為 computed 是基於響應式原理進行工作。如果你對響應式原理還不是很瞭解,可以閱讀我的上一篇文章:手摸手帶你理解Vue響應式原理

computed 用法

想要理解原理,最基本就是要知道如何使用,這對於後面的理解有一定的幫助。

第一種,函數聲明:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 計算屬性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 實例
      return this.message.split('').reverse().join('')
    }
  }
})

第二種,對象聲明:

computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}

溫馨提示:computed 內使用的 data 屬性,下文統稱為“依賴屬性”

工作流程

先來瞭解下 computed 的大概流程,看看計算屬性的核心點是什麼。

入口文件:

// 源碼位置:/src/core/instance/index.js
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

_init:

// 源碼位置:/src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // mergeOptions 對 mixin 選項和傳入的 options 選項進行合併
      // 這裡的 $options 可以理解為 new Vue 時傳入的對象
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    // 初始化數據
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

initState:

// 源碼位置:/src/core/instance/state.js 
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:

// 源碼位置:/src/core/instance/state.js 
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  // 1
  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]
    // 2
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if (!isSSR) {
      // create internal watcher for the computed property.
      // 3
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        { lazy: true }
      )
    }

    // 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)) {
      // 4
      defineComputed(vm, key, userDef)
    }
  }
}
  1. 實例上定義 _computedWatchers 對象,用於存儲“計算屬性Watcher
  2. 獲取計算屬性的 getter,需要判斷是函數聲明還是對象聲明
  3. 創建“計算屬性Watcher”,getter 作為參數傳入,它會在依賴屬性更新時進行調用,並對計算屬性重新取值。需要註意 Watcherlazy 配置,這是實現緩存的標識
  4. defineComputed 對計算屬性進行數據劫持

defineComputed:

// 源碼位置:/src/core/instance/state.js 
const noop = function() {}
// 1
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 判斷是否為服務端渲染
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    // 2
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    // 3
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // 4
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
  1. sharedPropertyDefinition 是計算屬性初始的屬性描述對象
  2. 計算屬性使用函數聲明時,設置屬性描述對象的 getset
  3. 計算屬性使用對象聲明時,設置屬性描述對象的 getset
  4. 對計算屬性進行數據劫持,sharedPropertyDefinition 作為第三個給參數傳入

客戶端渲染使用 createComputedGetter 創建 get,服務端渲染使用 createGetterInvoker 創建 get。它們兩者有很大的不同,服務端渲染不會對計算屬性緩存,而是直接求值:

function createGetterInvoker(fn) {
  return function computedGetter () {
    return fn.call(this, this)
  }
}

但我們平常更多的是討論客戶端渲染,下麵看看 createComputedGetter 的實現。

createComputedGetter:

// 源碼位置:/src/core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    // 1
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      // 2
      if (watcher.dirty) {
        watcher.evaluate()
      }
      // 3
      if (Dep.target) {
        watcher.depend()
      }
      // 4
      return watcher.value
    }
  }
}

這裡就是計算屬性的實現核心,computedGetter 也就是計算屬性進行數據劫持時觸發的 get

  1. 在上面的 initComputed 函數中,“計算屬性Watcher”就存儲在實例的_computedWatchers上,這裡取出對應的“計算屬性Watcher
  2. watcher.dirty 是實現計算屬性緩存的觸發點,watcher.evaluate 對計算屬性重新求值
  3. 依賴屬性收集“渲染Watcher
  4. 計算屬性求值後會將值存儲在 value 中,get 返回計算屬性的值

計算屬性緩存及更新

緩存

下麵我們來將 createComputedGetter 拆分,分析它們單獨的工作流程。這是緩存的觸發點:

if (watcher.dirty) {
  watcher.evaluate()
}

接下來看看 Watcher 相關實現:

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    // dirty 初始值等同於 lazy
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
}

還記得創建“計算屬性Watcher”,配置的 lazy 為 true。dirty 的初始值等同於 lazy。所以在初始化頁面渲染,對計算屬性取值時,會執行一次 watcher.evaluate

evaluate() {
  this.value = this.get()
  this.dirty = false
}

求值後將值賦給 this.value,上面 createComputedGetter 內的 watcher.value 就是在這裡更新。接著 dirty 置為 false,如果依賴屬性沒有變化,下一次取值時,是不會執行 watcher.evaluate 的, 而是直接就返回 watcher.value,這樣就實現了緩存機制。

更新

依賴屬性在更新時,會調用 dep.notify:

notify() {
  this.subs.forEach(watcher => watcher.update())
}

然後執行 watcher.update:

update() {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

由於“計算屬性Watcher”的 lazy 為 true,這裡 dirty 會置為 true。等到頁面渲染對計算屬性取值時,執行 watcher.evaluate 重新求值,計算屬性隨之更新。

依賴屬性收集依賴

收集計算屬性Watcher

初始化時,頁面渲染會將“渲染Watcher”入棧,並掛載到Dep.target

在頁面渲染過程中遇到計算屬性,因此執行 watcher.evaluate 的邏輯,內部調用 this.get:

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm) // 計算屬性求值
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    popTarget()
    this.cleanupDeps()
  }
  return value
}
Dep.target = null
let stack = []  // 存儲 watcher 的棧

export function pushTarget(watcher) {
  stack.push(watcher)
  Dep.target = watcher
} 

export function popTarget(){
  stack.pop()
  Dep.target = stack[stack.length - 1]
}

pushTarget 輪到“計算屬性Watcher”入棧,並掛載到Dep.target,此時棧中為 [渲染Watcher, 計算屬性Watcher]

this.getter 對計算屬性求值,在獲取依賴屬性時,觸發依賴屬性的 數據劫持get,執行 dep.depend 收集依賴(“計算屬性Watcher”)

收集渲染Watcher

this.getter 求值完成後popTragte,“計算屬性Watcher”出棧,Dep.target 設置為“渲染Watcher”,此時的 Dep.target 是“渲染Watcher

if (Dep.target) {
  watcher.depend()
}

watcher.depend 收集依賴:

depend() {
  let i = this.deps.length
  while (i--) {
    this.deps[i].depend()
  }
}

deps 記憶體儲的是依賴屬性的 dep,這一步是依賴屬性收集依賴(“渲染Watcher”)

經過上面兩次收集依賴後,依賴屬性的 subs 存儲兩個 Watcher,[計算屬性Watcher,渲染Watcher]

為什麼依賴屬性要收集渲染Watcher

我在初次閱讀源碼時,很奇怪的是依賴屬性收集到“計算屬性Watcher”不就好了嗎?為什麼依賴屬性還要收集“渲染Watcher”?

第一種場景:模板里同時用到依賴屬性和計算屬性

<template>
  <div>{{msg}} {{msg1}}</div>
</template>

export default {
  data(){
    return {
      msg: 'hello'
    }
  },
  computed:{
    msg1(){
      return this.msg + ' world'      
    }
  }
}

模板有用到依賴屬性,在頁面渲染對依賴屬性取值時,依賴屬性就存儲了“渲染Watcher”,所以 watcher.depend 這步是屬於重覆收集的,但 watcher 內部會去重。

這也是我為什麼會產生疑問的點,Vue 作為一個優秀的框架,這麼做肯定有它的道理。於是我想到了另一個場景能合理解釋 watcher.depend 的作用。

第二種場景:模板內只用到計算屬性

<template>
  <div>{{msg1}}</div>
</template>

export default {
  data(){
    return {
      msg: 'hello'
    }
  },
  computed:{
    msg1(){
      return this.msg + ' world'      
    }
  }
}

模板上沒有使用到依賴屬性,頁面渲染時,那麼依賴屬性是不會收集 “渲染Watcher”的。此時依賴屬性里只會有“計算屬性Watcher”,當依賴屬性被修改,只會觸發“計算屬性Watcher”的 update。而計算屬性的 update 里僅僅是將 dirty 設置為 true,並沒有立刻求值,那麼計算屬性也不會被更新。

所以需要收集“渲染Watcher”,在執行完“計算屬性Watcher”後,再執行“渲染Watcher”。頁面渲染對計算屬性取值,執行 watcher.evaluate 才會重新計算求值,頁面計算屬性更新。

總結

計算屬性原理和響應式原理都是大同小異的,同樣的是使用數據劫持以及依賴收集,不同的是計算屬性有做緩存優化,只有在依賴屬性變化時才會重新求值,其它情況都是直接返回緩存值。服務端不對計算屬性緩存。

計算屬性更新的前提需要“渲染Watcher”的配合,因此依賴屬性的 subs 中至少會存儲兩個 Watcher


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • MySQL 查詢所有存儲過程視圖 等等 INFORMATION_SCHEMA.TABLES INFORMATION_SCHEMA.TABLES是MySQL自帶的(被視作)一個信息資料庫,保存著MySQL伺服器所維護的其他資料庫的信息。INFORMATION_SCHEMA.TABLES其中有數個只讀表 ...
  • 1.case語句 case語句使用簡單的結構對數值做出選擇,更為重要的是,它還可以用來設置變數的值 --CASE語法格式: case input_name when 表達式 then 結果執行 …… end; 說明:首先設定一個變數的值,然後順序比較when關鍵字後面給出的值,若相等,則執行then ...
  • 目前跨端開發比較熱門的就是 React Native 和 Flutter 了,到底該選哪門技術似乎也快成了大前端圈的一個熱門話題。對於web前端來說,基於web生態的 React Native 應該是一個更加順暢而自然的選擇;但 Flutter 讓人動心的地方就是高性能和 跨端UI一致性。而Reac ...
  • 瞭解表單及其控制項 1.form:表單標簽 表單屬性: action:表單提交到哪裡 method:以什麼方式去提交到action指定的地址,有get和post,預設為get target: placeholder:表示在文本裡面預設顯示什麼文字 2.form裡面含有的控制項: input: selec ...
  • var start = "2020-6-26 20:36:00"; //開始時間 var now = new Date(); //當前時間 var ns = new Date(start).getTime() - now.getTime(); //毫秒差 //時間差 var todays = fun ...
  • /** * 多個關鍵詞列表高亮(word_list1,color1,word_list2,color2,...) * @param word_list 關鍵詞列表(例: ["關鍵詞a","關鍵詞b"],不區分大小寫) * @param color 顏色值(例: "#ff0000") * @retur ...
  • 普利姆演算法(加點法)求最小生成樹 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> ...
  • 我們在進行vue開發時會將公共的部分進行抽象,然後生成一個獨立的組件,這個組件可以被其它頁面引用,如果希望有交互的動作就設計到了值的傳遞,或者方法的回調等問題,這一次我們主要來說一下父組件和子組件的交互。 值的傳遞 子組件,通過定義了prods,然後可以實現從父組件向子組件的傳遞: <templat ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...