Johnny Wang BlogJohnny Wang Blog
Articles
Project
Live2d
johnnywang/book
Articles
Project
Live2d
johnnywang/book
  • About Me

    • 關於 Johnny
    • 文章清單
  • Javascript

    • View Transitions - 認識並使用原生 document 轉場效果
    • 用 Web Container 打造自己的線上 NodeJS 開發環境
    • 來試用看看原生 Web Popover API
    • 在電腦裡搞一個 RWKV AI 小助手
    • 遊戲 App 素材解包學習紀錄
    • 動手自己做一個 ChatGPT UI 工具吧
    • Create a React Server Components Project without NextJS - 製作一個不依賴 NextJS 的 React Server Components 專案
    • 如何在 Vuepress 裡快樂寫 React
    • 從 Mock Service Worker 源碼中學習
    • 擺脫 Node modules 地獄,擁抱 Yarn Plug'n'Play(PnP)
    • 快速上手 NextJS v13 - 基礎觀念 AppRouter 篇
    • 快速上手 NextJS v13 - Data Fetching, Caching, Revalidating 篇
    • 用 Socket.io 搭配 Matterjs 製作一款 Real-Time Canvas 聊天室(文長慎入)
    • 如何只用一支 CDN 及 4行設定,讓瀏覽器讀懂 Typescript, React, Vue
    • 用 tsup 快速建立 Typescript 開發環境
    • 開發 Email EDM 你可以更輕鬆
    • 關於我的 Side project - Maju Web Editor
    • Web URL 中神奇的 createObjectURL method
    • Awesome Vite 一個由社群維護的 template 集大成
    • Vue、React 使用心得分享(文長慎入)
    • 一起動手用 Socket.io 和 Peerjs 打造 WebRTC 即時視訊
    • Temporal Typescript SDK 學習筆記
    • React Web3 Storage
    • React useState 取得最新值
    • Babel 7 Decorator 的神奇小問題
    • 用 Koa + Vite + Pinia 打造基礎 SSR 環境
    • Live2d 官方範例改寫
    • 如何把 Video 畫在 Canvas 上
    • 史上最簡單的 Webpack 5 教學
    • 在瀏覽器中直接 import Vue SFC 開發起來
    • 手寫一個可中斷的 delay promise
    • NodeJS 輕量開發框架 Expressjs 與 Koa2 的區別
    • 用 2D 物理引擎 Matterjs 製作經典馬力歐 1-1
    • 原生 Javascript 的類型標註工具 JSDoc
    • Pinia - Vuex 的後繼者
    • 用 Nodejs 寫個 FTP command line 工具
    • Vue3 Server Render 手把手帶你搭建
    • 你真的懂 Event Loop 嗎
    • Babel7 基本介紹與使用
    • Vue/Vitejs 部分源碼解析
    • Vuejs 依賴追蹤 2020 版
      • Vue3.0 簡單介紹
      • 物件追蹤 - reactive
      • 陣列處理 - Array
      • 黑魔法 - effect
      • Computed
    • Vuejs 依賴追蹤 2019 版
    • Js literal 模板編譯
    • 用 JavaScript 鎖定用戶調整畫面比例
    • Date.now() 與 new Date()
    • Promise 相關知識
    • Web Component 學習筆記
    • TypeScript 基礎篇
    • TypeScript 進階篇
    • MVC, MVVM, MVI 軟體設計架構
    • MVVM 簡單模擬框架實作
  • CSS & Sass

    • 2024 CSS 年度報告筆記
    • 2021 CSS 年度報告筆記
    • 如何不用 setTimeout 幫 display: none 的 DOM 加動畫
    • 純 CSS 實作星球環繞動畫效果
    • 差點錯過的 Tailwindcss 入門學習筆記
    • Tailwindcss 進階學習筆記
    • Sass, SCSS Built-In-Modules 內建模組與函數
    • SCSS Parent Selector
    • CSS 畫面鎖橫屏時,滾動的問題!!
    • Safari 使用 animation 時動態產生 rem 的坑
    • CSS: mix-blend-mode 屬性混合圖層動畫
  • Memo

    • Javascript

      • Yarn Plug'n'Play (PnP) 的 VSCode 設定方式
      • 如何用 Web API 讀取剪貼簿內容
      • CSP (Content Security Policy) 是什麼?
      • Astro 入門學習筆記
      • 如何解決 Astro 套用主題切換時,畫面抖動瞬閃問題
      • Rspack - 以 Rust 打造的快速構建工具
      • Typescript v5 Decorator 學習筆記
      • Sequelize 筆記
      • ES2022 學習筆記
      • Crypto 密碼加密方法
      • 實作 Test 的注意事項
      • Jest 測試工具 - 基礎篇
      • 正則表達式
      • Rxjs 學習筆記
      • Gulp 學習筆記
      • Clean Code Javascript 學習
      • JS 實戰開發特殊小技巧
    • React

      • React forwardRef 使用筆記
      • 如何在 Nextjs 中使用 middleware set cookie
      • React Styled-Components 基礎篇
      • React Styled-Components 進階篇
      • React Unit Test
      • React Testing Library
      • React Emotion Basic
      • React Styled-JSX
      • React 18 - Concurrent Features Memo
      • useEffect 的高階封裝範例
      • React useContext & useReducer 搭配
      • React Router Config 筆記
    • Vue

      • Cypress Vue
      • Vue 單元測試學習筆記
      • 學習 Vuetify 的一些筆記
      • Vuex 學習筆記
    • React Native

      • React Native - basic
      • Expo Basic
      • Expo Router
      • Expo Build
      • Issues
    • GraphQL

      • GraphQL 學習筆記 - 基礎篇
      • GraphQL 學習筆記 - 進階篇
      • Apollo Client 學習使用筆記
      • GraphQL Memo
    • Parse

      • Parse Javascript 文檔閱讀筆記
      • Parse User Object 章節
      • Parse Session Object 章節
      • Parse Schema Object 章節
      • Parse Cloud Code 章節
    • CSS

      • Sass, SCSS 基礎筆記
    • Docker

      • Docker 基礎技術
      • Dockerfile 技術篇
      • Docker-compose 技術篇 - 3.7
      • Kubernetes 學習筆記
      • ArgoCD 學習筆記
      • Podman 學習筆記
      • Colima 安裝使用筆記
    • Git Learning

      • Git 版本控制
      • Git 基礎使用 - init, clone, add, commit, status, log
      • Git 分支
      • Git Merge 分支合併指令
      • Git Stash 保存紀錄
      • Git 基礎復原指令 - restore
      • Remote 遠端協同工作 - remote, fetch, pull, push
      • Git Tag 標籤 - tag
      • Git 設定 - config
      • Git 檔案比對與差異 - diff
      • Git Rebase 定義分支的參考基準
      • Subtree 子樹
      • Git Reflog 指令紀錄
      • Git Filter Branch
      • Git Crypt 使用筆記
      • Git 客製化工具
    • Bash

      • Bash 基礎概念
      • Variable 變數
      • Script 腳本撰寫
      • Condition 流程控制
      • Operation 算數表達式
      • Function 函數
      • Command Line Params 指令列參數處理
    • Patterns

      • Introduce
      • Design Patterns

        • Singleton Pattern
        • Compound Pattern
        • Proxy Pattern
        • Hooks Pattern
        • Observer Pattern
        • Mediator/Middleware Pattern
        • HOC Pattern
        • Factory Pattern
        • Render Props Pattern
        • Flyweight Pattern
        • Container/Presentational Component
        • Prototype Pattern
        • Mixin Pattern
        • Provider Component
        • Command Component
        • Module Pattern
      • Render Patterns

        • Rendering Patterns 渲染模式介紹
    • FE 性能優化

      • 前端性能優化
      • 靜態資源優化
      • 頁面渲染架構優化
      • Server 與 Network 優化
      • 開發流程、監控體系優化
    • Ollama 筆記
    • Nginx JS module 基本使用筆記
    • FlowiseAI 使用介紹筆記
    • Github Copilot 使用筆記
    • Traefik Memo
    • API First 學習筆記
    • What is AC - Acceptance Criteria 驗收條件
    • Mermaid 學習筆記
    • 串接 Sonarcube 筆記
    • Youtube Data API 基礎使用筆記
    • FB API 學習筆記
    • VSCode 好用快捷鍵
    • 在 Simulator 上開發測試
    • 問題修正紀錄
  • References

    • 全端開發學習資源
    • 實用工具
    • 網頁前端課程目錄
  • Daily

    • 中醫學習

      • 中醫自學方向
      • 黃帝內經-素問
      • 基礎理論

        • 中藥治病
        • 中藥產地與採收
        • 陰陽五行
        • 精氣血津液
        • 整體觀與臟象
        • 病因病機
        • 辨證論治
        • 四診八綱
        • 五運六氣
        • 經絡
      • 診斷學

        • 辯證
        • 望診
        • 聞診
        • 問診
        • 脈診簡史
        • 切診
        • 常見脈象與臨床意義
        • 經脈辨證
        • 中醫的六邪在疾病初期、後期分別有那些症狀
      • 中藥學

        • 中藥基礎理論
        • 常見中藥材
        • 解表藥(30)
        • 清熱藥(82)
        • 瀉下藥(16)
        • 利水渗湿药(8)
        • 化痰止咳平喘药(15)
        • 止血藥(8)
        • 開竅-平肝-安神藥(11)
        • 化濕藥(5)
        • 祛風濕藥(12)
        • 理氣藥(13)
        • 活血祛瘀藥(7)
        • 消食藥(4)
        • 驅蟲藥(6)
        • 祛寒药(6)
        • 補虛藥(43)
        • 收斂藥(13)
      • 方劑學

        • 總論
        • 解表劑
        • 瀉下劑
        • 清熱劑
        • 理血劑
        • 去暑劑
        • 去濕劑
      • 針灸學

        • 特定穴
      • 其他

        • 內證觀察筆記 上篇【內證漫談】
        • 內證觀察筆記 中篇【太極器官、五藏】
        • 內證觀察筆記 下篇【十二正經】
        • 磁力對生理的影響
    • 易經學習

      • 易經入門
    • 道德經學習

      • 道德經學習重點筆記
      • 第一章 道可道,非常道
      • 第二章 夫唯弗居,是以不去
      • 第三章 不尚賢,使民不爭
      • 第四章 道沖而用之或不盈,淵兮似萬物之宗
      • 第五章 天地不仁,以萬物為芻狗,多聞數窮,不如守中
      • 第六章 谷神不死,是謂玄牝
      • 第七章 天長地久。天地所以能長且久者,以其不自生,故能長生。
      • 第八章 上善若水。水善利萬物而不爭,處眾人之所惡,故幾於道
      • 第九章 功成身退,天之道也。
      • 第十章 載營魄抱一,能無離乎﹖
      • 第十一章 有之以為利,無之以為用
      • 第十二章 五色令人目盲,五音令人耳聾,五味令人口爽,馳騁畋獵令人心發狂,難得之貨令人行妨。
      • 第十三章 寵辱若驚,貴大患若身。愛以身為天下,若可托天下
      • 第十四章 是謂無狀之狀,無物之象,是謂惚恍。迎之不見其首,隨之不見其後。執古之道,以御今之有。
      • 第十五章 保此道者,不欲盈。夫唯不盈,故能蔽而新成。
      • 第十六章 致虛極,守靜篤。萬物並作,吾以觀復。
      • 第十七章 太上,下知有之。悠兮其貴言,功成事遂,百姓皆謂我自然。
      • 第十八章 大道廢,有仁義;智慧出,有大偽;
      • 第十九章 絕聖棄智,民利百倍。見素抱樸,少私寡欲
      • 第二十章 絕學無憂。眾人皆有餘,而我獨若遺。
      • 第二十一章 道之為物,惟恍惟惚。惚兮恍兮,其中有象;恍兮惚兮,其中有物。
      • 第二十二章 曲則全,枉則直,窪則盈,敝則新,少則得,多則惑。是以聖人抱一為天下式
      • 第二十三章 希言自然。天地尚不能久,而況於人乎?信不足焉,有不信焉。
      • 第二十四章 企者不立,跨者不行
      • 第二十五章 有物混成,先天地生。人法地,地法天,天法道,道法自然。
      • 第二十六章 重為輕根,靜為躁君。輕則失本,躁則失君。
      • 第二十七章 善行無轍跡,善言無瑕謫。故善人者,不善人之師;不善人者,善人之資。不貴其師,不愛其資,雖智大迷,是謂要妙。
      • 第二十八章 知雄守雌,為天下谿,知榮守辱,為天下谷
      • 第二十九章 天下神器,不可為也,是以聖人去甚,去奢,去泰。
      • 第三十章 以道佐人主者,不以兵強天下
      • 第三十一章 夫兵者,不祥之器,物或惡之,故有道者不處。
      • 第三十二章 道常無名,樸雖小,天下莫能臣也。
      • 第三十三章 知人者智,自知者明。
      • 第三十四章 大道氾兮,其可左右。以其終不自為大,故能成其大。
      • 第三十五章 執大象,天下往。往而不害,安平太。樂與餌,過客止。
      • 第三十六章 將欲歙之,必固張之;將欲弱之,必固強之
      • 第三十七章 道常無為而無不為
      • 第三十八章 上德不德,是以有德;下德不失德,是以無德
      • 第三十九章 昔之得一者,天得一以清。不欲琭琭如玉,珞珞如石。
      • 第四十章 反者道之動,弱者道之用。天下萬物生於有,有生於無
      • 第四十一章 上士聞道,勤而行之;大方無隅,大器晚成,大音希聲,大象無形,道隱無名。
      • 第四十二章 道生一,一生二,二生三,三生萬物。
      • 第四十三章 天下之至柔,馳騁天下之至堅
      • 第四十四章 名與身孰親﹖身與貨孰多﹖得與亡孰病﹖
      • 第四十五章 大成若缺,其用不弊。大盈若沖,其用不窮。
      • 第四十六章 天下有道,卻走馬以糞。天下無道,戎馬生於郊。
      • 第四十七章 不出戶,知天下;不窺牖,見天道。
      • 第四十八章 為學日益,為道日損。損之又損,以至於無為。
      • 第四十九章 聖人無常心,以百姓心為心。
      • 第五十章 出生入死。生之徒,十有三;死之徒,十有三;人之生,動之死地,亦十有三。
      • 第五十一章 道生之,德畜之,物形之,勢成之。
      • 第五十二章 天下有始,以為天下母。以知其子,既知其子,復守其母,沒身不殆。
      • 第五十三章 使我介然有知,行於大道,唯施是畏。大道甚夷,而人好徑。
      • 第五十四章 善建者不拔,善抱者不脫,子孫以祭祀不輟。
      • 第五十五章 含德之厚,比於赤子。知和曰常,知常曰明。益生曰祥。心使氣曰強。
      • 第五十六章 知者不言,言者不知。
      • 第五十七章 以正治國,以奇用兵,以無事取天下。
      • 第五十八章 其政悶悶,其民淳淳;其政察察,其民缺缺。禍兮福之所倚,福兮禍之所伏。
      • 第五十九章 治人事天,莫若嗇。夫唯嗇,是謂早服
      • 第六十章 治大國,若烹小鮮。夫兩不相傷,故德交歸焉。
      • 第六十一章 大國者下流,天下之交。天下之牝,牝常以靜勝牡,以靜為下。
      • 第六十二章 道者萬物之奧。雖有拱璧以先駟馬,不如坐進此道。
      • 第六十三章 為無為,事無事,味無味。圖難於其易,為大於其細;
      • 第六十四章 合抱之木,生於毫末;九層之臺,起於累土;千里之行,始於足下。為者敗之,執者失之。
      • 第六十五章 古之善為道者,非以明民,將以愚之。
      • 第六十六章 是以欲上民,必以言下之。欲先民,必以身後之。
      • 第六十七章 天下皆謂我道大,似不肖。夫唯大,故似不肖。
      • 第六十八章 善為士者不武,善戰者不怒,善勝敵者不與
      • 第六十九章 吾不敢為主而為客,不敢進寸而退尺。禍莫大於輕敵,輕敵幾喪吾寶。
      • 第七十章 吾言甚易知,甚易行。天下莫能知,莫能行。
      • 第七十一章 知不知上,不知知病。夫唯病病,是以不病。
      • 第七十二章 民不畏威,則大威至。無狎其所居,無厭其所生。夫唯不厭,是以不厭。
      • 第七十三章 天之道,不爭而善勝,不言而善應,不召而自來,繟然而善謀。
      • 第七十四章 民不畏死,奈何以死懼之?若使民常畏死,而為奇者,吾得執而殺之,孰敢?
      • 第七十五章 民之饑,以其上食稅之多,是以饑。
      • 第七十六章 人之生也柔弱,其死也堅強。萬物草木之生也柔脆,其死也枯槁。
      • 第七十七章 天之道,其猶張弓與﹖高者抑之,下者舉之;有餘者損之,不足者補之。
      • 第七十八章 天下莫柔弱於水,而攻堅強者莫之能勝,以其無以易之。
      • 第七十九章 和大怨,必有餘怨,安可以為善﹖
      • 第八十章 小國寡民。使有什伯之器而不用,使民重死而不遠徙。
      • 第八十一章 信言不美,美言不信。天之道,利而不害;聖人之道,為而不爭。
    • 人類真相推廣協會

      • 人類真相介紹
      • 人生大挑戰-閱讀筆記
      • 書籍-人生大挑戰

        • 序文
        • 童年的回憶
        • 賺外快的童年
        • 養家的童年
        • 少年時期的回憶
        • 粉紅睡衣女鬼的祕密
        • 麵線、甘蔗和賽鴿
        • 我在黑社會的日子
        • 『台北一條龍』
        • 賭徒‧妻子‧盤仔人
        • 人鬼之戰—正邪不分的恐怖
        • 開展創業石銅雕畫的日子
        • 渡畜牲者‧瞎掰鬼與邪靈
        • 無所不在的陷阱—邪靈的詭計
        • 鬼屋‧符令‧大揭祕
        • 妖魔鬼怪大變身—邪靈與動物
        • 乩童與宮廟的祕密
        • 活鬼纏身的恐怖亂象
        • 前面親兄弟、後面無情義的假面
        • 前面手牽手、後面下毒手的險境
        • 人心險惡、五馬分屍所逼自殺的真相
        • 自殺後的奇遇—人死後的世界
        • 我自殺後的奇遇—「陰間地府處」與「陰府大本營」
        • 我被趕出家門、面臨眾叛親離的苦境
        • 其他
    • 英文學習

      • 自我介紹
      • 簡單寒暄
      • 指路
      • 買賣
      • 轉接電話
      • 通勤
      • 訂位
      • 數字
    • 金剛經 時間不存在?自我是虛擬?
    • 20250319 外婆過世
    • 說服的藝術:避免陷阱,建立共識
    • 禪修一定要去除「如何」嗎?
    • 不對稱的回報
    • 避免慢性壓力
    • 關鍵溝通表達力
    • 快樂的秘訣
    • 生活在現代社會,維持高品質思考的重要性
    • 心理學家的面相術:解讀情緒的密碼
    • 什麼是真相,真相是什麼
    • 身為一位歷史觀察者(文長慎入)
    • 2022 Leisure Learn
    • 兩年八個月,我從 Garena 畢業了
    • DeFi
    • 我的終生大事,交往到結婚
    • 轉職前端工程師 3 年多的回顧
    • MacOS + Iterm2 + Oh My Zsh
    • 我喜歡的名言佳句
    • Web Interview Preparation

Vuejs 依賴追蹤 2020 版

Share On:

FacebookLINEMessengerTelegram

過了一年,使用了下最新 vue-next 後覺得很神奇,剛好看到大神的實踐文章,動手實作後又有了新的想法,本篇是參考新版 Vue3 所製作,並以舊的 Vue2 defineProperty 來練習實作相似功能。原文出處是使用新的 Proxy 實作。(概念十分類似)

Vue3.0 簡單介紹

開始之前先簡單講一下 Vue3.0,主要新增了許多底層 api 供使用者操作,用以構建更加複雜化與巨大的代碼,提升整體代碼的復用性之外,更大幅提升了對於 Typescript 的支持,甚至連效能方面對比於 Vue2 都呈指數型成長,程式碼的動靜差異部分越大,效能提升的越明顯。

其中新版的最主要響應式相關的 api 就是 reactive, computed, ref 等等概念,透過 reactive 可以建立一個類似於 Vue2 data 的響應式物件,還沒有摸過 Vue-next 的朋友們請先前往這裡看看,對於下面要講的東西會比較好理解喔~

那麼開始吧!

物件追蹤 - reactive

首先實踐下 reactive 的部分:

// 儲存已轉換的響應式對象
const proxyCache = new WeakMap();
// 儲存 effect 掛載之暫存
const effectCache = [];
// track 紀錄
const trackMap = new WeakMap();

function isObject(v) {
  return v !== null && typeof v === 'object';
}

function reactive(target) {
  return createReactive(target);
}

function createReactive(target) {
  // 檢查是否為可遍歷物件
  if (!isObject(target)) return target;
  // 檢查是否已是響應式物件
  let observed = proxyCache.get(target);
  if (observed) return target;
  // 遍歷對象
  Object.keys(target).forEach((key) => {
    defineReactive(target, key, target[key]);
  });
  proxyCache.set(target, true);
  return target;
}

function defineReactive(target, key, val) {
  // 遍歷子屬性
  createReactive(val);
  Object.defineProperty(target, key, {
    get() {
      console.log('Get', key);
      return val;
    },
    set(newVal) {
      val = newVal;
      console.log('Set', key);
      // 遍歷新值
      createReactive(val);
    },
  });
}

到這邊我們已經可以用 reactive 來建立一個響應式對象

const data = reactive({
  name: 'Johnny',
});

console.log(data.name);
// Get name
// Johnny
data.name = 'Kevin';
// Set name

陣列處理 - Array

熟悉 Vue2 的朋友應該都知道,陣列在某些情況下修改會無法被追蹤到,這是因為我們無法在一開始知道後續會新增或刪除什麼序的緣故,這裡我們改寫對象數組的 push 方法以及加入一個 set 方法實現

// 改寫數組 push 方法
function setArrayPush(arr, callback = () => {}) {
  Object.defineProperty(arr, 'push', {
    enumerable: false, // 隱藏屬性
    configurable: false,
    writable: false,
    value: function() {
      let n = this.length;
      for (let i = 0, l = arguments.length; i < l; i++, n++) {          
        this[n] = arguments[i];
        callback(this, n);
      }
      return n;
    }
  })
}

// set
// 其實就是剛剛上面的 defineReactive,僅進行接口友善化
function set(target, key, value) {
  return defineReactive(target, key, value);
}

然後稍微修改下我們的 createReactive

function createReactive(target) {
  if (!isObject(target)) return target;
  let observed = proxyCache.get(target);
  if (observed) return target;
  // 對數組對象修改 push 方法
  if (Array.isArray(target)) {
    setArrayPush(target, function(arr, index) {
      defineReactive(arr, index, arr[index]);
    });
  }
  Object.keys(target).forEach((key) => {
    defineReactive(target, key, target[key]);
  });
  proxyCache.set(target, true);
  return target;
}

如此一來我們就可以追蹤到數組對象在 push 後新增的屬性摟~

const arr = reactive([1, 2, 3]);
arr.push(4);
data[3] = 1;
// Set 3

set(arr, 3, 4);
arr[3] = 1;
// Set 3

黑魔法 - effect

Vue3 中最新出現的一個黑魔法函式 effect,內部調用的所有依賴會自動被追蹤,並且在改變時執行內部代碼塊。實現如下:

function effect(fn) {
  const effect = createEffect(fn);
  // 調用一次,添加依賴
  effect();
  return effect;
}

function createEffect(fn) {
  const effect = function() {
    // 把自己 (effect) 傳進去掛載
    return runEffect(effect, fn);
  };
  return effect;
}

function runEffect(effect, fn) {
  try {
    // 掛載 effect
    effectCache.push(effect);
    // 添加 effect 到所有依賴中(藉由 reactive 對象屬性的 getter 進行 track)
    return fn();
  } finally {
    // 卸載 effect
    effectCache.pop(effect);
  }
}

接著要來實現 getter 掛載 effect 的 track 方法

function track(target, key) {
  const effect = effectCache[effectCache.length - 1];
  // 檢查是否有掛載中的 effect
  if (effect) {
    let depsMap = trackMap.get(target);
    if (!depsMap) {
      trackMap.set(target, depsMap = new Map());
    }
    let dep = depsMap.get(key);
    if (!dep) {
      depsMap.set(key, dep = new Set());
    }
    if (!dep.has(effect)) {
      dep.add(effect);
    }
  }
}

接著實現 setter 中的 trigger 方法,執行對應儲存的所以 effect

function trigger(target, key) {
  const depsMap = trackMap.get(target);
  // 沒有依賴則結束
  if (!depsMap) return;
  const effects = depsMap.get(key);
  if (effects) {
    effects.forEach(effect => effect());
  }
}

最後修改下我們的 defineReactive的 getter/setter

function defineReactive(target, key, val) {
  createReactive(val);
  Object.defineProperty(target, key, {
    get() {
      // 掛載 effect 到 target => key 的依賴
      track(target, key);
      return val;
    },
    set(newVal) {
      val = newVal;
      // 調用所有 key 的依賴 effects
      trigger(target, key);
      createReactive(val);
    },
  });
}

到這邊,我們可以用 effect 來建立響應式的代碼塊了,範例如下:

const data = reactive([1, 2, 3]);
effect(() => {
  console.log('Effect: ', data[0]);
})
data[0] = 3;
// Effect: 1
// Effect: 3

Computed

computed 的部分其實就是使用到上面完成的 effect 來實現,只是內部需要進行快取跟懶執行,也就是調用 n 次只取值一次,節省效能。

首先來完成 computed 主方法:

function computed(getter) {
  let dirty = true;
  const runner = effect(getter, {
    // lazy 使 effect 不在建立時立即執行
    lazy: true,
    reset() {
      // 依賴改變時,僅執行 reset,不直接取值,當下次調用時才會懶加載取值
      dirty = true;
    }
  });
  let value = null;
  return {
    get value() {
      // 檢查依賴是否改變,未改變則不再取值
      if (dirty) {
        value = runner();
        dirty = false;
      }
      return value;
    }
  };
}

接著對應的修改下 effect 方法

function effect(fn, options = { lazy: false }) {
  const effect = createEffect(fn);
  // 非懶加載時進行調用
  if (!options.lazy) {
    effect();
  }
  // 掛載 reset 方法到 effect 上,如此可一併被存到依賴之中
  // 後續調用時就不直接調用 effect,而是使用 reset 打開取值的開關
  effect.reset = options.reset;
  return effect;
}

最後在 trigger 中進行判斷

function trigger(target, key) {
  const depsMap = trackMap.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  if (effects) {
    effects.forEach(effect => {
      // 當有 reset 時進行調用,不直接執行 effect
      if (effect.reset) {
        effect.reset();
      } else {
        // 正常調用取值
        effect();
      }
    });
  }
}

到這其實已經完成主要的 computed 功能了,範例如下:

const data = reactive({
  name: 'Johnny',
  age: 30
});

const info = computed(() => {
  console.log('Cache');
  return data.name + ' ' + data.age;
});
// 調用 n 次僅取值一次
console.log(info.value);
console.log(info.value);

// Cache
// Johnny 30
// Johnny 30

然後這邊就會發現一個問題,如果在 effect 之中使用到了 computed 會怎麼樣?

const data = reactive({
  name: 'Johnny',
  age: 30
});

const info = computed(() => {
  console.log('Cache');
  return data.name + ' ' + data.age;
});

effect(() => {
  console.log('Get value: ', info.value);
});

data.name = 'Johnson';
// 這裡只會執行一次 Cache

這裡主要是因為我們 effect 回傳的 runner 在調用後,會將自己掛載到對應的屬性依賴中,但這邊的 info 並不是一個 reactive 的物件,他也是由另一個 effect 建立,實際需要添加的依賴應該是原來建立 info 這個 computed 中的所有依賴,也因此我們在 computed 的 value getter 中,必須略過原 runner,保留新建立的 effect 在依賴暫存中,並通過原來 computed 的 getter,將新的 effect 直接儲存到原來 getter 內的依賴上。

修改 computed 如下

function computed(getter) {
  let initAttach = false; // 是否已首次取值
  let needCache = true; // 依賴值是否改變
  const runner = effect(getter, {
    lazy: true,
    computed: true,
    reset() {
      needCache = true;
    }
  });
  let value = null;
  return {
    get value() {
      // 取值時是否存在新的 effect,是的話表示此 computed 被用於該新的 effect 中
      const applyNewEffect = effectCache[effectCache.length-1];
      // 首次取值,runner 掛載當前 effect 到所有依賴中
      if (!initAttach) {
        runner()
        initAttach = true;
      // 後續取值,getter 掛載新的 effect 到所有依賴中(繞過 runner,避免掛載覆蓋)
      // 1. 掛載新 effect(將此 computed 對象用於其他 effect 中)
      // 2. 原依賴值改變(getter 內依賴值改變)
      }
      if (applyNewEffect || needCache) {
        value = getter();
        needCache = false;
      }
      return value;
    }
  };
}

對應改下 effect 跟 trigger

function effect(fn, options = { lazy: false }) {
  const effect = createEffect(fn);
  if (!options.lazy) {
    effect();
  }
  effect.computed = options.computed;
  effect.reset = options.reset;
  return effect;
}

function trigger(target, key) {
  const depsMap = trackMap.get(target);
  if (!depsMap) return;
  const effects = depsMap.get(key);
  if (effects) {
    effects.forEach(effect => {
      if (effect.computed) {
        effect.reset();
      } else {
        effect();
      }
    });
  }
}

幾番修改後,現在在 effect 使用到 computed 也完全沒問題摟~

簡單使用如下:

<div id="app"></div>
const data = reactive({
  name: 'Johnny',
  age: 30,
});

const info = computed(() => {
  return data.name + ' ' + data.age;
});

effect(() => {
  document.getElementById('app').innerHTML = info.value;
});

setTimeout(() => {
  data.name = 'Johnson';
}, 2000);

以上就是這次學習的一點紀錄,希望有幫助到大家理解 Vue 的核心響應式原理,也歡迎有興趣的大大們指教瞜~^^,以上代碼都有放到我的個人 github 中筆記,也歡迎去看看~@johnnywang/reactive

文章參考:

  1. 帶你了解 vue-next(Vue 3.0)之 爐火純青

Share On:

FacebookLINEMessengerTelegram
最近更新:: 2022/3/29 凌晨2:43
Contributors: Johnny Wang, johnnywang1994
Prev
Vue/Vitejs 部分源碼解析
Next
Vuejs 依賴追蹤 2019 版