【Vue2.x源碼系列06】計算屬性computed原理

来源:https://www.cnblogs.com/burc/archive/2023/04/19/17330998.html
-Advertisement-
Play Games

本章目標:計算屬性是如何實現的?計算屬性緩存原理以及洋蔥模型的應用?在初始化Vue實例時,我們會給每個計算屬性都創建一個對應watcher,我們稱之為計算屬性watcher ...


上一章 Vue2非同步更新和nextTick原理,我們介紹了 JavaScript 執行機制是什麼?nextTick源碼是如何實現的?以及Vue是如何非同步更新渲染的?

本章目標

  • 計算屬性是如何實現的?
  • 計算屬性緩存原理 - 帶有dirty屬性的watcher
  • 洋蔥模型的應用

初始化

在 Vue初始化實例的過程中,如果用戶 options選項中存在計算屬性時,則初始化計算屬性

// 初始化狀態
export function initState(vm) {
  const opts = vm.$options // 獲取所有的選項

  // 初始化數據
  if (opts.data) { initData(vm) }

  // 初始化計算屬性
  if (opts.computed) { initComputed(vm) }
}

我們給每個計算屬性都創建了一個 Watcher實例,標識為lazy:true, 在初始化watcher時不會立即執行 get方法(計算屬性方法)

並將計算屬性watcher 都保存到了 Vue實例上,讓我們可以在後續的 getter方法中通過 vm獲取到當前的計算屬性watcher

然後使用Object.defineProperty去劫持計算屬性

// 初始化計算屬性
function initComputed(vm) {
  const computed = vm.$options.computed
  const watchers = (vm._computedWatchers = {}) // 將每個計算屬性對應的watcher 都保存到 vm上
  for (let key in computed) {
    let userDef = computed[key]

    // 相容不同寫法 函數方式 和 對象getter/setter方式
    let fn = typeof userDef === 'function' ? userDef : userDef.get

    // 給每個計算屬性都創建一個 watcher,並標識為 lazy,不會立即執行 get-fn
    watchers[key] = new Watcher(vm, fn, { lazy: true })

    // 劫持計算屬性getter/setter
    defineComputed(vm, key, userDef)
  }
}

// 劫持計算屬性
function defineComputed(target, key, userDef) {
  const setter = userDef.set || (() => {})

  Object.defineProperty(target, key, {
    get: createComputedGetter(key),
    set: setter,
  })
}

當我們劫持到計算屬性被訪問時,根據 dirty 值去決定是否更新 watcher緩存值

然後讓自己依賴的屬性(準確來說是訂閱的所有dep)都去收集上層watcher,即 Dep.target(可能是計算屬性watcher,也可能是渲染watcher)

// 劫持計算屬性的訪問
function createComputedGetter(key) {
  return function () {
    const watcher = this._computedWatchers[key] // this就是 defineProperty 劫持的targer。獲取到計算屬性對應的watcher

    // 如果是髒的,就去執行用戶傳入的函數
    if (watcher.dirty) {
      watcher.evaluate() // 重新求值後 dirty變為false,下次就不求值了,走緩存值
    }

    // 當前計算屬性watcher 出棧後,還有渲染watcher 或者其他計算屬性watcher,我們應該讓當前計算屬性watcher 訂閱的 dep,也去收集上一層的watcher 即Dep.target(可能是計算屬性watcher,也可能是渲染watcher)
    if (Dep.target) {
      watcher.depend()
    }

    // 返回watcher上的值
    return watcher.value
  }

Dep

  • Dep.target:當前渲染的 watcher,靜態變數
  • stack:存放 watcher 的棧。 利用 pushTarget、popTarget 這兩個方法做入棧出棧操作
// 當前渲染的 watcher
Dep.target = null

// 存放 watcher 的棧
let stack = []
// 當前 watcher 入棧, Dep.target 指向 當前 watcher
export function pushTarget(watcher) {
  stack.push(watcher)
  Dep.target = watcher
}
// 棧中最後一個 watcher 出棧,Dep.target指向棧中 最後一個 watcher,若棧為空,則為 undefined
export function popTarget() {
  stack.pop()
  Dep.target = stack[stack.length - 1]
}

計算屬性Watcher

在初始化Vue實例時,我們會給每個計算屬性都創建一個對應watcher(我們稱之為計算屬性watcher,除此之外還有 渲染watcher偵聽器watcher ),他有一個 value 屬性用於緩存計算屬性方法的返回值。

預設標識 lazy: true,懶的,代表計算屬性watcher,創建時不會立即執行 get方法

預設標識 dirty: true,髒的,當我們劫持到計算屬性訪問時,如果是髒的,我們會通過watcher.evaluate重新計算 watcher 的 value值 並將其標識為乾凈的;如果是乾凈的,則直接取 watcher 緩存值

depend 方法,會讓計算屬性watcher 訂閱的dep去收集上層watcher,可能是渲染watcher,也可能是計算屬性watcher(計算屬性嵌套的情況),實現洋蔥模型的核心方法

update 方法,當計算屬性依賴的對象發生變化時,會觸發dep.notify派發更新 並 調用 update 方法,只需更新 dirty 為 true即可。我們會在後續的渲染watcher 更新時,劫持到計算屬性的訪問操作,並通過 watcher.evaluate重新計算其 value值

class Watcher {
  constructor(vm, fn, options) {
    // 計算屬性watcher 用到的屬性
    this.vm = vm
    this.lazy = options.lazy // 懶的,不會立即執行get方法
    this.dirty = this.lazy // 髒的,決定重新讀取get返回值 還是 讀取緩存值

    this.value = this.lazy ? undefined : this.get() // 存儲 get返回值
  }

  // 重新渲染
  update() {
    console.log('watcher-update')
    if (this.lazy) {
      // 計算屬性依賴的值發生改變,觸發 setter 通知 watcher 更新,將計算屬性watcher 標識為臟值即可
      // 後面還會觸發渲染watcher,會走 evaluate 重新讀取返回值
      this.dirty = true
    } else {
      queueWatcher(this) // 把當前的watcher 暫存起來,非同步隊列渲染
      // this.get(); // 重新渲染
    }
  }

  // 計算屬性watcher為臟時,執行 evaluate,並將其標識為乾凈的
  evaluate() {
    this.value = this.get() // 重新獲取到用戶函數的返回值
    this.dirty = false
  }
  
  // 用於洋蔥模型中計算屬性watcher 訂閱的dep去 depend收集上層watcher 即Dep.target(可能是計算屬性watcher,也可能是渲染watcher)
  depend() {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

緩存原理

計算屬性是基於它們的響應式依賴進行緩存的。只在相關響應式依賴發生改變時它們才會重新求值。 緩存原理如下:

在初始化計算屬性時,我們使用Object.defineProperty劫持了計算屬性,並做了一些 getter/setter操作

計算屬性watcher 有一個 dirty臟值屬性,預設為 true

當我們劫持到計算屬性被訪問時,如果 dirty 為 true,則執行 evaluate 更新 watcher 的 value值 並 將 dirty 標識為 false;如果為 false,則直接取 watcher 的緩存值

當計算屬性依賴的屬性變化時,會通知 watcher 調用 update方法,此時我們將 dirty 標識為 true。這樣再次取值時會重新進行計算

洋蔥模型

在初始化Vue實例時,我們會給每個計算屬性都創建一個對應的懶的watcher,不會立即調用計算屬性方法

當我們訪問計算屬性時,會通過watcher.evaluate()讓其直接依賴的屬性去收集當前的計算屬性watcher,並且還會通過watcher.depend()讓其訂閱的所有 dep都去收集上層watcher,可能是渲染watcher,也可能是計算屬性watcher(如果存在計算屬性嵌套計算屬性的話)。這樣依賴的屬性發生變化也可以讓視圖進行更新

讓我們一起來分析下計算屬性嵌套的例子

<p>{{fullName}}</p>

computed: {
  fullAge() {
    return '今年' + this.age
  },
  fullName() {
    console.log('run')
    return this.firstName + ' ' + this.lastName  + ' ' + this.fullAge
  },
}
  1. 初始化組件時,渲染watcher 入棧
    • stack:[渲染watcher]
  2. 當執行 render方法並初次訪問 fullName時,執行computed watcher1.evaluate()watcher1入棧
    • stack:[渲染watcher, watcher1]
  3. 當執行watcher1的 get方法時,其直接依賴的 firstName 和 lastName 會去收集當前的 watcher1;然後又訪問 fullAge 並執行computed watcher2.evaluate()watcher2入棧
    • watcher1:[firstName, lastName]
    • stack:[渲染watcher, watcher1, watcher2]
  4. 執行watcher2的 get方法時,其直接依賴的 age 會去收集當前的 watcher2
    • watcher2:[age]
  5. watcher2出棧,並執行watcher2.depend(),讓watcher2訂閱的 dep再去收集當前watcher1
    • stack:[渲染watcher, watcher1]
    • watcher1:[firstName, lastName, age]
  6. watcher1出棧,執行watcher1.depend(),讓watcher1訂閱的 dep再去收集當前的渲染watcher
    • stack:[渲染watcher]
    • 渲染watcher:[firstName, lastName, age]
人間不正經生活手冊
您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 一. 問題描述 使用華為推送服務下發IM消息時,下發消息請求成功且code碼為80000000,但是手機總是收不到消息; 在華為推送自助分析(Beta)平臺查看發現,消息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類消息做了單個設備每日推送數量上限管理,具體 ...
  • 在前面的Blog例子中我們使用的是GraphQL, 雖然GraphQL的使用處於上升趨勢,但是Rest API還是使用的更廣泛一些. 所以還是決定回到傳統的rest api framework上來, Django rest framework的官網上給了一個很好用的QuickStart, 我參考Qu ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 大家平時在開發的時候有沒被new Date()折磨過?就是它的諸多怪異的設定讓你每每用的時候,都可能不小心踩坑。造成程式意外出錯,卻一下子找不到問題出處,那叫一個煩透了…… 下麵,我就列舉它的“四宗罪”及應用思考 可惡的四宗罪 1. Sa ...
  • 實現文字跑馬燈效果,首先用到 substring()截取 和 setInterval計時器 clearInterval()清除計時器 效果如下: 實現代碼如下: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta ...
  • JavaScript 操作符/運算符 在 JavaScript 中,有一些操作符可以使代碼更簡潔、易讀和高效。以下是一些常見的操作符: 1、可選鏈操作符(optional chaining operator) ?.是可選鏈操作符(optional chaining operator)。?. 可選鏈操 ...
  • 一、概述 rem是一個相對長度單位,它的單位長度取決於根標簽html的字體尺寸。rem即root em的意思,中文翻譯為根em。瀏覽器的文本尺寸一般預設為16px,即預設情況下: 1rem = 16px rem佈局原理:根據CSS媒體查詢功能,更改根標簽的字體尺寸,實現rem單位隨屏幕尺寸的變化,如 ...
  • 好家伙,我的包終於開發完啦 歡迎使用胖虎的飛機大戰包!! 為你的主頁添加色彩 這是一個有趣的網頁小游戲包,使用canvas和js開發 使用ES6模塊化開發 效果圖如下: (覺得圖片太sb的可以自己改) 代碼已開源!! Git: https://gitee.com/tang-and-han-dynas ...
  • 我們都知道,通常情況下我們使用 vue 大多都是用的 SFC(Signle File Component)單文件組件模式,即一個組件就是一個文件,但其實 Vue 也是支持使用 JSX 來編寫組件的。這裡不討論 SFC 和 JSX 的好壞,這個仁者見仁智者見智。本篇文章旨在帶領大家快速瞭解和使用 Vu ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...