Vue響應式系統:從原理到核心API全解析

響應式原理

響應式機制的主要功能就是,可以把普通的JavaScript對象封裝成為響應式對象,攔截數據的讀取和設置操作,實現依賴數據的自動化更新。

Q: 如何才能讓JavaScript對象變成響應式對象?

首先需要認識響應式數據和副作用函數,副作用函數指的是產生副作用的函數。

通過一個demo函數來理解一下:

let val = 1;
function effect() {val = 2;// 修改全局變量,產生副作用
}
effect()console.log(val)// 2

當console打印時,會觸發字段的讀取操作,副作用函數effect執行時,會觸發設置操作。當我們能攔截一個對象的讀取和設置操作時,那事情就變得簡單了。

在Vue2時,通過Object.defineProperty函數實現,Vue3采用Proxy來實現。

根據如上思路,采用Proxy實現方式如下:

// 存儲副作用函數的桶
const bucket = new Set();const data = { text: "summer" }
// 對原始數據的代理
const obj = new Proxy(data, {// 攔截讀取操作get(target, key) {//將副作用函數effect添加到存儲副作用函數的桶中bucket.add(effect)return target[key]},// 攔截設置操作set(target, key, newVal) {target[key] = newVal;// 把副作用函數從桶里取出并執行bucket.forEach(fn => fn())return true}
})

副作用函數可以是任意名字,上面的代碼是幫助我們理解響應式數據的基本實現和工作原理。

從上面的代碼片段中可以看出,一個響應系統的工作流程如下:

當讀取操作發生時,將副作用函數收集到“桶”中;當設置操作發生時,從“桶”中取出副作用函數并執行。

核心API和響應式工具函數

reactive:

reactive是通過ES6中的Proxy特性實現的屬性攔截,所以在reactive函數中我們直接返回new Proxy即可:

export function reactive(target) {if (typeof target!=='object') {console.warn(`reactive  ${target} 必須是一個對象`);return target}return new Proxy(target, mutableHandlers);
}
mutableHandlers

mutableHandlers要做的事就是配置Proxy的攔截函數,這里我們只攔截get和set操作,進入到baseHandlers.ts中。

使用createGetter和createSetter來創建get和set函數,mutableHandlers就是配置了get和set的對象返回。

get直接返回讀取的數據,這里的Reflect.get和target[key]實現的結果是一致的;并且返回值是對象的話,還會嵌套執行reactive,并且調用track函數收集依賴。set調用trigger函數,執行track收集的依賴。

const get = createGetter();
const set = createSetter();function createGetter(shallow = false) {return function get(target, key, receiver) {const res = Reflect.get(target, key, isRef(target) ? target : receiver);if (!isReadonly) {track(target, "get", key);}if(isObject(res)) {// 值也是對象的話,需要嵌套調用reactivereturn isReadonly ? readonly(res) : reactive(res)}return res;}
}function createSetter() {return function set(target, key, value, receiver) {const result = Reflect.set(target, key, value, isRef(target) ? target : receiver);if (target === toRaw(receiver)) {if (!hadKey) {trigger(target, 'add', key, value)} else if (hasChanged(value, oldValue)) {trigger(target, 'set', key, value, oldValue)}}return result}
}export const mutableHandlers = {get, set
}
track

在track函數中,我們可以使用一個巨大的targetMap去存儲依賴關系。map的key是我們要代理的target對象,值還是一個depsMap,存儲每一個key依賴的函數,每一個key都可以依賴多個effect。代碼如下:

const targetMap = new WeakMap();export function track(target,type, key) {// 沒有 activeEffect,直接returnif(!activeEffect) return;let depsMap = targetMap.get(target);if(depsMap) {targetMap.set(target, (depsMap = new Map()))}let dep = depsMap.get(key);if(!dep) {depsMap.set(key, (dep = new Set()))}deps.add(activeEffect)
}
trigger

根據targetMap的實現機制,trigger函數實現的思路就是從targetMap中,根據target和key找到對應的依賴函數集合deps,然后遍歷deps執行依賴函數。代碼如下:

export function trigger(target,type, key) {const depsMap = targetMap.get(target);if(!depsMap) {// 沒找到依賴return;}const deps = depsMap.get(key)if(!deps) return;deps.forEach((effectFn) => {// 判斷副作用函數是否存在調度器if(effectFn.scheduler) {effectFn.scheduler()} else {effectFn()}})
}
effect

我們把傳遞進來的fn函數通過effectFn函數包裹執行,在effectFn函數內部,把函數賦值給全局變量activeEffect;然后執行fn()的時候,就會觸發響應式對象的get函數,get函數內部就會把activeEffect存儲到依賴中,完成依賴的收集。

export function effect(fn, options =  {}) {// effect嵌套通過隊列管理const effectFn = () => {try {activeEffect = effectFn;return fn();} finally {activeEffect = null;}}if(!options.lazy) {// 沒有配置lazy,直接執行effectFn()}effectFn.scheduler = options.scheduler;return effectFn;
}

effect傳遞的函數,可以通過lazy和scheduler來控制函數的執行時機,默認是同步執行。

scheduler

使用數組管理傳遞的執行任務,最后使用Promise.resolve只執行最后一次,這也是Vue中watchEffect函數的大致原理。

const obj = reactive({ count: 1});
effect(() => {console.log(obj.count)
}, {scheduler: queueJob
});
// 調度器實現
const queue: Function[] = [];
let isFlushing = false;
function queueJob(job: () => void) {if(!isFlushing) {isFlushing = true;Promise.resolve().then(() => {let fn;while(fn = queue.shift()) {fn()}})}
}

ref

ref的執行邏輯比reactive要簡單一些,不需要使用Proxy代理語法,直接使用對象語法中的getter和setter配置,監聽value屬性即可。對象的get value方法,使用track函數去收集依賴,set value方法中使用trigger函數去觸發函數的執行。

export function ref(val) {if(isRef(val)) {return val;}
}export function isRef(val) {return !!(val && val.__isRef)
}// 利用面向對象的getter和setter進行track和trigger
class RefImpl {constructor(val, isShallow: boolean) {this._rawValue = isShallow ? value : toRaw(value)this._value = isShallow ? value : toReactive(value)this.__v_isShallow = isShallow}get value() {track(this, 'value')return this._value;}set value(newValue) {const oldValue = this._rawValue;const useDirectValue =this.__v_isShallow ||isShallow(newValue) ||isReadonly(newValue);newValue = useDirectValue ? newValue : toRaw(newValue);if(hasChanged(newValue,oldValue)) {this._rawValue = newValue;this._value = useDirectValue ? newValue : toReactive(newValue);trigger(this, "value")}}
}export const hasChanged = (value: any, oldValue: any): boolean =>!Object.is(value, oldValue)

ref也可以包裹復雜的數據結構,內部會直接調用reactive來實現,這也解決了日常對ref和reactive使用時機的疑惑,現在可以全部都用ref函數,ref內部會幫我們調用reactive。

computed

computed計算屬性也是一種特殊的effect函數。在computed函數,我們攔截了computed的value屬性,并且定制了effect的lazy和scheduler配置,computed注冊的函數就不會直接執行,而是要通過scheduler函數中對_dirty屬性決定是否執行。

export function computed(getterOrOptions) {let getter, setter;if(isFunction(getterOrOptions)) {getter = getterOrOptions;setter = () => {console.warn('computed value is readonly')}} else {getter = getterOrOptions.get;setter = getterOrOptions.set;}return new ComputedRefImpl(getter, setter);
}class ComputedRefImpl {constructor(getter, setter) {this._setter = setter;this._val = undefined;this._dirty = true;// computed就是一個特殊的effect,設置lazy和執行時機this.effect = effect(getter, {lazy: true,scheduler:() => {if(!this._dirty) {this._dirty = true;trigger(this, 'value')}}})}get value() {track(this, 'value')if(this._dirty) {this._dirty = false;this._val = this.effect()}return this._val;}set value(val) {this._setter(val)}
}

watch

watch本質就是觀測一個響應式數據,當數據發生變化時通知并執行相應的回調函數;實現本質上就是利用了effect以及options.scheduler選項。

function watch(source, cb) {let getter;if(typeof source === 'function') {getter = source;} else {getter = () => traverse(source)}let oldValue, newValue;const effectFn = effect(// 調用traverse遞歸地讀取() => getter, {lazy: true,scheduler() {// 執行副作用函數,獲取新值newValue = effectFn();// 將舊值和新值作為回調函數的參數cb(newValue, oldValue);// 更新舊值oldValue = newValue;}})// 手動調用副作用函數,獲取到舊值oldValue = effectFn();
}function traverse(value, seen = new Set()) {// 如果要讀取的數據是原始值,或者已經被讀取過,那么就不做處理if(!isObject(value) || value === null || seen.has(value)) return;seen.add(value)if (isArray(value)) {for (let i = 0; i < value.length; i++) {traverse(value[i], seen)}} else if (isSet(value) || isMap(value)) {value.forEach((v: any) => {traverse(v, seen)})} else if (isPlainObject(value)) {for (const key in value) {traverse(value[key], seen)}for (const key of Object.getOwnPropertySymbols(value)) {if (Object.prototype.propertyIsEnumerable.call(value, key)) {traverse(value[key as any], seen)}}}return value;
}

總結

在探索Vue響應式系統的旅程中,我們深入剖析了其底層邏輯與關鍵實現。響應式原理作為基石,通過數據劫持(Vue2依托Object.defineProperty, Vue3借助更強大的Proxy),搭配依賴收集與派發更新機制,構建起數據與試圖自動同步的橋梁。

核心API與工具函數則是響應式能力的具體延伸:reactive借助mutableHandlers等完成對象響應式轉換,track精準收集依賴、trigger觸發更新,effect與scheduler協同管控副作用執行;ref適配基本類型與對象的響應式需求,computed基于依賴緩存實現高效計算,watch靈活監聽數據變化。這些內容共同編織出Vue響應式系統的完整生態,理解它們,方能在Vue開發中精準駕馭數據驅動視圖的精髓,應對復雜場景時游刃有余,為打造高效、可維護的Vue應用筑牢基礎。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/bicheng/87910.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/87910.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/87910.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

水下目標檢測:突破與創新

水下目標檢測技術背景 水下環境帶來獨特挑戰&#xff1a;光線衰減導致對比度降低&#xff0c;散射引發圖像模糊&#xff0c;色偏使顏色失真。動態水流造成目標形變&#xff0c;小目標&#xff08;如1010像素海膽&#xff09;檢測困難。聲吶與光學數據融合可提升精度&#xff0…

高通SG882G平臺(移遠):2、使用docker鏡像編譯

其實之前已經編譯過了。今日搜索時發現&#xff0c;只有當時解決問題的匯總&#xff0c;沒有操作步驟。于是記錄下來。 建議使用Ubuntu20 LTS。 安裝docker $ sudo apt update $ sudo apt install docker.io $ sudo docker -v Docker version 27.5.1, build 27.5.1-0ubuntu3…

輕松上手:使用Nginx實現高效負載均衡

接上一篇《輕松上手&#xff1a;Nginx服務器反向代理配置指南》后&#xff0c;我們來探討一下如何使用Nginx實現高效負載均衡。 在當今高并發、大流量的互聯網環境下&#xff0c;單臺服務器早已無法滿足業務需求。想象一下&#xff1a;一次電商平臺的秒殺活動、一個熱門應用的…

身份證號碼+姓名認證接口-身份證二要素核驗

身份證號實名認證服務接口采用身份證號碼、姓名二要素核驗的方式&#xff0c;能夠快速確認用戶身份。無論是新用戶注冊&#xff0c;還是老用戶重要操作的身份復核&#xff0c;只需輸入姓名及身份證號&#xff0c;瞬間即可得到 “一致” 或 “不一致” 的核驗結果。這一過程高效…

自動駕駛基本概念

目錄 自動駕駛汽車&#xff08;Autonomous Vehicles &#xff09; 單車智能 車聯網 智能網聯&#xff08;單車智能車聯網&#xff09; 自動駕駛關鍵技術 環境感知與定位 車輛運動感知 車輛運動感知 路徑規劃與決策 自動駕駛發展歷程 自動駕駛應用場景 自動駕駛路測…

提示詞框架(10)--COAST

目前&#xff0c;有很多提示詞框架都叫COAST&#xff0c;但是每個的解釋都不同&#xff0c;出現很了很多解釋和演化版本&#xff0c;不要在意這些小事&#xff0c;我們都是殊途同歸--讓AI更好的完成任務COAST框架&#xff0c;比較適合需要詳細背景和技術支持的任務&#xff0c;…

基于selenium實現大麥網自動搶票腳本教程

閑來無事&#xff0c;打開大麥網發現現在大多數演唱票都需要手機端才能搶票&#xff0c;僅有很少一部分支持pc端用網頁去搶票&#xff0c;但正所謂&#xff1a;道高一尺&#xff0c;魔高一丈&#xff0c;解決這個反爬問題&#xff0c;我們可以采用Airtest連接仿真機來模擬手機端…

2048小游戲實現

2048小游戲實現 將創建一個完整的2048小游戲&#xff0c;包含游戲核心邏輯和美觀的用戶界面。設計思路 4x4網格布局響應式設計&#xff0c;適配不同設備分數顯示和最高分記錄鍵盤控制&#xff08;方向鍵&#xff09;和觸摸滑動支持游戲狀態提示&#xff08;勝利/失敗&#xff0…

Windows VMWare Centos Docker部署Springboot + mybatis + MySql應用

前置文章 Windows VMWare Centos環境下安裝Docker并配置MySqlhttps://blog.csdn.net/u013224722/article/details/148928081 Windows VMWare Centos Docker部署Springboot應用https://blog.csdn.net/u013224722/article/details/148958480 Windows VMWare Centos Docker部署…

【科普】Cygwin與wsl與ssh連接ubuntu有什么區別?DIY機器人工房

Cygwin、WSL&#xff08;Windows Subsystem for Linux&#xff09;和通過 SSH 連接 Ubuntu 是三種在 Windows 環境下與類 Unix/Linux 系統交互的工具&#xff0c;但它們的本質、運行環境、功能范圍有顯著區別。以下從核心定義、關鍵差異和適用場景三個維度詳細說明&#xff1a;…

Web前端數據可視化:ECharts高效數據展示完全指南

Web前端數據可視化&#xff1a;ECharts高效數據展示完全指南 當產品經理拿著一堆密密麻麻的Excel數據走向你時&#xff0c;你知道又到了"化腐朽為神奇"的時刻。數據可視化不僅僅是把數字變成圖表那么簡單&#xff0c;它是將復雜信息轉化為直觀洞察的藝術。 在過去兩…

# IS-IS 協議 | LSP 傳輸與鏈路狀態數據庫同步機制

略作整理&#xff0c;待校。 SRM 和 SSN 標志的作用 SRM 標志 功能&#xff1a;SRM 標志用于跟蹤路由器從一個接口向鄰居發送鏈路狀態協議數據單元&#xff08;LSP&#xff09;的狀態。作用&#xff1a;確保 LSP 的正確傳輸和狀態跟蹤。 SSN 標志 廣播網絡 功能&#xff1…

Windows DOS CMD 100

1. systeminfo&#xff1a;顯示系統詳細信息&#xff08;安裝日期/補丁/內存等&#xff09; 2. sfc /scannow&#xff1a;掃描并修復系統文件損壞 [管理員] 3. chkdsk /f&#xff1a;檢查磁盤錯誤并修復&#xff08;需重啟&#xff09; [管理員] 4. cleanmgr&#xff1a;啟動…

HTML初學者第三天

<1>文檔類型聲明標簽——<!DOCTYPE><!DOCTYPE>文檔聲明&#xff0c;作用是告訴瀏覽器使用哪種HTML版本來顯示網頁。<!DOCTYPE html>這句代碼的意思是&#xff1a;當前頁面采用的是HTML5版本來顯示網頁。注意&#xff1a;-<!DOCTYPE>聲明位于文檔…

學車筆記6

“不踩離合利用發動機制動”是指在駕駛過程中&#xff0c;駕駛員抬起油門踏板&#xff0c;但不踩下離合器踏板&#xff0c;利用發動機自身的阻力來減緩車輛速度的一種制動方式。具體介紹如下&#xff1a; #### 原理 - **動力傳遞反向**&#xff1a;正常情況下&#xff0c;發動…

人體坐姿檢測系統項目教程(YOLO11+PyTorch+可視化)

&#x1f4a1;本文主要內容&#xff1a;本項目基于YOLO11深度學習目標檢測算法&#xff0c;設計并實現了一個人體坐姿檢測系統。系統能夠自動識別圖像或視頻中的多種坐姿類型&#xff08;如&#xff1a;正常坐姿、不良坐姿等&#xff09;&#xff0c;為健康監測、智能教室、辦公…

服務網格可觀測性深度實踐與創新優化

主題&#xff1a;突破服務網格監控瓶頸——基于eBPF的無侵入式全鏈路可觀測性實踐 技術領域&#xff1a;云原生/微服務/服務網格&#xff08;Service Mesh&#xff09; 一、問題背景&#xff1a;傳統服務網格監控的痛點 在Istio、Linkerd等服務網格架構中&#xff0c;可觀測…

微信小程序41~50

1.列表渲染-進階用法 如果要對默認的變量名和下標進行修改&#xff0c;可以使用wx:for-item和wx:for-index wx:for-item可以指定數組當前元素的變量名 wx:for-index可以指定數組當前下標的變量名將wx:for用在標簽上&#xff0c;以渲染一個包含多個節點的結構快 并不是一個組件…

向量數據庫-Milvus快速入門

Milvus 概述 向量是神經網絡模型的輸出數據格式&#xff0c;可以有效地對信息進行編碼&#xff0c;在知識庫、語義搜索、檢索增強生成&#xff08;RAG&#xff09;等人工智能應用中發揮著舉足輕重的作用。 Milvus 是一個開源的向量數據庫&#xff0c;適合各種規模的人…

uniapp的光標跟隨和打字機效果

1、準備好容器文字的顯示textRef&#xff0c;以及光標的顯示 &#xff0c;使用transform-translate對光標進行移動到文字后面<template><view class"container" ref"contentRef"><u-parse :content"nodeText" ref"textRef&q…