vue響應式原理細節分享

在講解之前,我們先了解一下數據響應式是什么?所謂數據響應式就是建立響應式數據依賴(調用了響應式數據的操作)之間的關系,當響應式數據發生變化時,可以通知那些使用了這些響應式數據的依賴操作進行相關更新操作,可以是DOM更新,也可以是執行一些回調函數。從Vue2到Vue3都使用了響應式,那么它們之間有什么區別?

  • Vue2響應式:基于Object.defineProperty()實現的。
  • Vue3響應式:基于Proxy實現的。

那么它們之間有什么區別?為什么Vue3會選擇Proxy替代defineProperty?我們先看看下面兩個例子:

defineReactive(data,key,val){Object.defineProperty(data,key,{enumerable:true,configurable:true,get:function(){console.log(`對象屬性:${key}訪問defineReactive的get!`)return val;},set:function(newVal){if(val===newVal){return;}val = newVal;console.log(`對象屬性:${key}訪問defineReactive的set!`)}})
}
let obj = {};
this.defineReactive(obj,'name','sapper');
// 修改obj的name屬性
obj.name = '工兵';
console.log('obj',obj.name);
// 為obj添加age屬性
obj.age = 12;
console.log('obj',obj);
console.log('obj.age',obj.age);
// 為obj添加數組屬性
obj.hobby = ['游戲', '原神'];
obj.hobby[0] = '王者';
console.log('obj.hobby',obj.hobby);// 為obj添加對象屬性
obj.student = {school:'大學'};
obj.student.school = '學院';
console.log('obj.student.school',obj.student.school);

在這里插入圖片描述

從上圖可以看出使用defineProperty定義了包含name屬性的對象obj,然后添加age屬性、添加hobby屬性(數組)、添加student屬性并分別訪問,都沒有觸發obj對象中的get、set方法。也就是說defineProperty定義對象不能監聽添加額外屬性或修改額外添加的屬性的變化,我們再看看這樣一個例子:

let obj = {};
// 初始化就添加hobby
this.defineReactive(obj,'hobby',['游戲', '原神']);
// 改變數組下標0的值
obj.hobby[0] = '王者';
console.log('obj.hobby',obj.hobby);

在這里插入圖片描述

假如我們一開始就為obj添加hobby屬性,我們發現修改數組下標0的值,并沒有觸發obj里的set方法,也就是說defineProperty定義對象不能監聽根據自身數組下標修改數組元素的變化,注意地,如果是直接用defineProperty定義數組元素是可以監聽的,但是對于數組比較大的時候就很犧牲性能,尤神考慮到性能就沒有使用這種方法。那么我們繼續看一下Proxy代理的對象例子:

// proxy實現
let targetProxy = {name:'sapper'};
let objProxy = new Proxy(targetProxy,{get(target,key){console.log(`對象屬性:${key}訪問Proxy的get!`)return target[key];},set(target,key,newVal){if(target[key]===newVal){return;}console.log(`對象屬性:${key}訪問Proxy的set!`)target[key]=newVal;return target[key];}
})
// 修改objProxy的name屬性
objProxy.name = '工兵';
console.log('objProxy.name',objProxy.name);
// 為objProxy添加age屬性
objProxy.age = 12;
console.log('objProxy.age',objProxy.age);
// 為objProxy添加hobby屬性
objProxy.hobby = ['游戲', '原神'];
objProxy.hobby[0] = '王者';
console.log('objProxy.hobby',objProxy.hobby);
// 為objProxy添加對象屬性
objProxy.student = {school:'大學'};
objProxy.student.school = '學院';
console.log('objProxy.student.school',objProxy.student.school);

在這里插入圖片描述

從上圖是不是發現了Proxy與defineProperty的明顯區別之處了,Proxy能支持對象添加或修改觸發get、set方法,不管對象內部有什么屬性。所以

  • Object.defineProperty():defineProperty定義對象不能監聽添加額外屬性修改額外添加的屬性的變化;defineProperty定義對象不能監聽根據自身數組下標修改數組元素的變化。我們看看Vue里的用法例子:
data() {return {name: 'sapper',student: {name: 'sapper',hobby: ['原神', '天涯明月刀'],},};
},
methods: {deleteName() {delete this.student.name;console.log('刪除了name', this.student);},addItem() {this.student.age = 21;console.log('添加了this.student的屬性', this.student);},updateArr() {this.student.hobby[0] = '王者';console.log('更新了this.student的hobby', this.student);},
}

在這里插入圖片描述

從圖中確實可以修改data里的屬性,但是不能及時渲染,所以Vue2提供了兩個屬性方法解決了這個問題:Vue.$setVue.$delete

注意不能直接this._ data.age這樣去添加age屬性,也是不支持的。

this.$delete(this.student, 'name');// 刪除student對象屬性name
this.$set(this.student, 'age', '21');// 添加student對象屬性age
this.$set(this.student.hobby, 0, '王者');// 更新student對象屬性hobby數組

在這里插入圖片描述

  • Proxy:解決了上面兩個弊端,proxy可以實現:
  • 可以直接監聽對象而非對象屬性,可以監聽對象添加額外屬性的變化;
const user = {name:'張三'}
const obj = new Proxy(user,{
get:function (target,key){console.log("get run");return target[key];
},
set:function (target,key,val){console.log("set run");target[key]=val;return true;
}
})
obj.age = 22;
console.log(obj); // 監聽對象添加額外屬性打印set run!  
  • 可以直接監聽數組的變化
const obj = new Proxy([2,1],{
get:function (target,key){console.log("get run");return target[key];
},
set:function (target,key,val){console.log("set run");target[key]=val;return true;
}
})
obj[0] = 3;
console.log(obj); // 監聽到了數組元素的變化打印set run!  
  • Proxy 返回的是一個新對象,而 Object.defineProperty 只能遍歷對象屬性直接修改。
  • 支持多達13 種攔截方法,不限于 apply、ownKeys、deleteProperty、has 等等是Object.defineProperty 不具備的。

總的來說,Vue3響應式使用Proxy解決了Vue2的響應式的詬病,從原理上說,它們所做的事情都是一樣的,依賴收集依賴更新

Vue2響應式原理

這里基于Vue2.6.14版本進行分析

Vue2響應式:通過Object.defineProperty()對每個屬性進行監聽,當對屬性進行讀取的時候就會觸發getter,對屬性修改的時候就會觸發setter。首先我們都知道Vue實例中有data屬性定義響應式數據,它是一個對象。我們看看下面例子的data:

data(){return {name: 'Sapper',hobby: ['游戲', '原神'],obj: {name: '張三',student: {major: '軟件工程',class: '1班',}}}
}

在這里插入圖片描述

從上圖我們可以看到,data中的每一個屬性都會帶 __ob__ 屬性,它是一個Observer對象,其實Vue2中響應式的關鍵就是這個對象,在data中的每一個屬性都會帶get、set方法,而Vue源碼中其實把get、set分別定義為reactiveGetter、reactiveSetter,這些東西怎么添加進去的。Vue2又是怎么數據變化同時實時渲染頁面?先看看下面的圖:

在這里插入圖片描述

給data屬性創建Observer實例:通過初注冊響應式函數initState中調用了initData函數實現為data創建Observer實例。

在這里插入圖片描述

function initData(vm: Component) {// 獲取組件中聲明的data屬性let data: any = vm.$options.data// 對new Vue實例下聲明、組件中聲明兩種情況的處理data = vm._data = isFunction(data) ? getData(data, vm) : data || {}...// observe dataconst ob = observe(data) // 為data屬性創建Observer實例ob && ob.vmCount++
}

通過Observer實例把data中所有屬性轉換成getter/setter形式來實現響應性:對data屬性分為兩種情況處理:對象屬性處理(defineReactive實現)和數組屬性處理

注意地,由于Vue實例的data永遠都是一個對象,所以data里面包含的數組類型只有對象屬性數組屬性

在這里插入圖片描述

在getter收集依賴,在setter中觸發依賴:當讀取data中的數據時,會在get方法中收集依賴,當修改data中的數據時,會在set方法中通知依賴更新。defineReactive方法中主要是做四件事情:創建Dep實例給對象屬性添加get/set方法收集依賴通知依賴更新

在這里插入圖片描述

從上面我們知道了dep.depend()實現了依賴收集,dep.notify()實現了通知依賴更新,那么Dep類究竟做了什么?我們先看看下面的圖:

在這里插入圖片描述

從圖中我們得明確一點,誰使用了變化的數據,也就是說哪個依賴使用了變化的數據,其實就是Dep.taget,它就是我們需要收集的依賴,是一個Watcher實例對象,其實Watcher對象有點類似watch監聽器,我們先看一個例子:


vm.$watch('a.b.c',function(newVal,oldVal)){....}

怎么監聽多層嵌套的對象,其實就是通過.分割為對象,循環數組一層層去讀數據,最后一后拿到的就是想要對的數據。

export function parsePath (path){const segment = path.split('.');return function(obj){...for(let i=0;i<segment.length;i++){if(!obj) return;obj = obj[segment[i]]}return obj}
}

當嵌套對象a.b.c屬性發生變化時,就會觸發第二個參數中的函數。也就是說a.b.c就是變化的數據,當它的值發生變化時,通知Watcher,接著Watcher觸發第二個參數執行回調函數。我們看看Watcher類源碼,是不是發現了cb其實就與watch的第二參數有異曲同工之妙。

export default class Watcher implements DepTarget {vm?: Component | nullcb: Functiondeps: Array<Dep>...constructor(vm: Component | null,expOrFn: string | (() => any),cb: Function,...) {...this.getter = parsePath(expOrFn)// 解析嵌套對象...}get() { // 讀取數據...return value}addDep(dep: Dep) {...dep.addSub(this)//添加依賴...}cleanupDeps() {// 刪除依賴...dep.removeSub(this)...}update() {// 通知依賴更新this.run()...}run() {...this.cb.call(this.vm, value, oldValue)}...depend() { // 收集依賴let i = this.deps.lengthwhile (i--) {this.deps[i].depend()}}...
}

實現對數組的監聽:從最開始的例子,我們了解對象以及嵌套對象的監聽,但是Object.defineProperty是用來監聽對象指定屬性的變化,不支持數組監聽,那么數組又是怎么監聽?我們上面說了data中的數據被賦予響應性都是在Observer中實現的,那么監聽的實現也是在Observer對象中實現的,先對數組的特定方法做自定義處理,為了攔截數組元素通知依賴更新,然后才通過observeArray函數遍歷創建Observer實例,主要分為兩種情況:

// 源碼Observer類中對數組處理的部分代碼
if (Array.isArray(value)) {if (hasProto) {protoAugment(value, arrayMethods)} else {copyAugment(value, arrayMethods, arrayKeys)}this.observeArray(value)
}
  • 當瀏覽器支持__ proto __ 對象:強制賦值當前arrayMethods給target的__ proto __ 對象,直接給當前target數組帶上自定義封裝的數組方法,從而實現監聽數組變化。其實arrayMethods處理后就是下面這樣一個對象:

    在這里插入圖片描述

    protoAugment(value, arrayMethods)function protoAugment (target, src: Object) {target.__proto__ = src
    }
    
  • 當瀏覽器不支持__ proto __ 對象:遍歷數組元素通過defineProperty定義為元素帶上自定義封裝的原生數組方法,由于自定義數組方法中做了攔截通知依賴更新,從而實現監聽數組的變化。

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
console.log(arrayKeys);// ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
copyAugment(value, arrayMethods, arrayKeys)function copyAugment (target: Object, src: Object, keys: Array<string>) {for (let i = 0, l = keys.length; i < l; i++) {const key = keys[i]def(target, key, src[key])// 遍歷數組元素通過為元素帶上}
}

對數組的Array原生方法做了自定義封裝的源碼如下,在自定義方法中攔截通知依賴更新。

在這里插入圖片描述

// 遍歷target實現創建Observer實例
observeArray (items: Array<any>) {for (let i = 0, l = items.length; i < l; i++) {observe(items[i])}
}
### Vue2響應式原理小結:- **給data創建Observer實例**- **Observer類實現對數據封裝getter、setter的響應性**- **針對數組類型數據,自定義封裝Array原生方法,在封裝過程中攔截執行通知依賴更新**- **真正通過Watcher通知依賴更新,通過run方法中的cb回調函數,實現類似watch偵聽器第二參數中監聽變化后的操作**。## Vue3響應式原理> 這里基于Vue3.2.41版本進行分析其實Vue3的響應原理與Vue2的響應原理都差不多,唯一不同的就是它們的實現方式,Vue3通過創建Proxy的實例對象而實現的,它們都是收集依賴、通知依賴更新。而Vue3中把依賴命名為副作用函數effect,也就是數據改變發生的副作用,我們先來看一下例子:```js
const house = {status:'未出租',price:1200,type:'一房一廳'};
const obj = new Proxy(house, {
get (target, key) {return target[key];
},
set (target, key, newVal) {target[key] = newVal;return true;
}
})
function effect () {
console.log('房子狀態:'+obj.status);
}effect () // 觸發了proxy對象的get方法
obj.status = '已出租!';
effect () 

通過Proxy創建一個代理對象,把house代理給obj,obj是代理對象,house是被代理對象。house對象中數據改變,由于effect函數讀取了對象屬性,所以當數據改變,也需要及時更新副作用函數effect。但是問題來了,假如對象中多個屬性的,依賴于數據變化的多個副作用函數,數據變化一次都需要執行一次,代碼寫起來就會很冗余,所以我們需要這樣處理:

const objSet = new Set();
const obj = new Proxy(house, {// 攔截讀取操作get (target, key) {objSet.add(effect) // 收集effectreturn target[key];},set (target, key, newVal) {target[key] = newVal;objSet.forEach(fn=>fn()) // 遍歷effectreturn true;}
})

把副作用函數都存到Set實例中,Set可以過濾重復數據,然后在獲取數據中收集副作用函數,在修改數據中遍歷執行副作用函數,這樣就簡化了代碼,不需要每次改變都要執行一次了,也就是修改一次數據及時更新effect。雖然上面已經實現了響應式的雛形了,但是還需要解決很多問題:

假如這個副作用函數是一個匿名函數,這時候需要怎么處理? 添加一個全局變量臨時存儲。

effect (()=>console.log('房子狀態:'+obj.status)) // 上面的例子會直接報not define// 添加一個全局變量activeEffect存儲依賴函數,這樣effect就不會依賴函數的名字了
let activeEffect;
function effect (fn) {activeEffect = fn;// 執行副作用函數fn()
}

假如讀取不存在的屬性的時候,副作用函數發生什么? 副作用函數會被重新執行,由于目標字段與副作用函數沒有建立明確的函數聯系。所以這就需要引入唯一key辨識每一個數據的副作用函數,以target(目標數據)、key(字段名)、effectFn(依賴)。看下圖:

setTimeout(() => {obj.notExit = '不存在的屬性';
}, 1000)

分三種情況分析副作用函數存儲數據唯一標識

  • 兩個副作用函數同時讀取同一個對象的屬性值

在這里插入圖片描述

  • 一個副作用函數中讀取了同一個對象不同屬性

在這里插入圖片描述

  • 不同副作用函數中讀取兩個不同對象的相同屬性

在這里插入圖片描述
所以為了解決這些不同情況的副作用保存問題,所以Vue3引入了Weak、Map、Set三個集合方法來保存對象屬性的相關副作用函數:
在這里插入圖片描述

   const weakMap = new WeakMap();let activeEffect;const track = ((target,key)=>{if(!activeEffect){return;}// 從weakMap中獲取當前target對象let depsMap = weakMap.get(target);if(!depsMap){weakMap.set(target,(depsMap=new Map()))}// 從Map中屬性key獲取當前對象指定屬性let deps = depsMap.get(key)if(!deps){// 副作用函數存儲depsMap.set(target,(deps=new Set()))}deps.add(activeEffect)  })const trigger = ((target,key)=>{// 從weakMap中獲取當前target對象const depsMap = weakMap.get(target);if(!depsMap) return;// 從Map中獲取指定key對象屬性的副作用函數集合const effects = depsMap.get(key);effects&&effects.forEach(fn=>fn())})

WeakMap與Map的區別是? 區別就是垃圾回收器是否回收的問題,WeakMap對象對key是弱引用,如果target對象沒有任何引用,可以被垃圾回收器回收,這就需要它了。相對于WeakMap,不管target是否引用,Map都不會被垃圾回收,容易造成內存泄露。我們看一下下面例子:

   const map = new Map();const weakMap = new WeakMap();(function(){const foo = {foo:1};const bar = {bar:2};map.set(foo,1);weakMap.set(bar,2);})() // 函數執行完,weakMap內的所有屬性都被垃圾回收器回收了setTimeout(() => {console.log(weakMap);// 刷新頁面發現weakMap里面沒有屬性了}, 2000)

假如在一個副作用函數中調用了對象的兩個屬性,但是有布爾值控制,按正常來說,副作用函數只能執行一次get獲取值的,但是我們現有的實現方法還實現不了,我們看看下面例子。

   const effectFn = (() => {const str = obj.status ? '' : obj.type;})const obj = new Proxy(house, {get(target, key) {console.log('get run!');// 打印了兩次...},set(target, key, newVal) {...}})

通過這個例子,我們是不是需要解決這個問題,也就是當每次副作用函數執行時,我們可以先把它從所有與之關聯的依賴集合中刪除。我們看看源碼例子:

   // 清空副作用函數依賴的集合function cleanupEffect(effect: ReactiveEffect) {const { deps } = effectif (deps.length) {for (let i = 0; i < deps.length; i++) {deps[i].delete(effect)}deps.length = 0}}

嵌套副作用函數處理:由于副作用函數可能是嵌套,比如副作用函數中effectFn1中有還有一個副作用函數effectFn2,以上面的方法對于嵌套函數的處理用全局變量 activeEffect 來存儲通過 effect 函數注冊的副作用函數,這意味著同一時刻 activeEffect 所存儲的副作用函數只能有一個。當副作用函數發生嵌套時,內層副作用函數的執行會覆蓋 activeEffect 的值,并且永遠不會恢復到原來的值。看了很多資料舉例用effect棧存儲,是的沒錯,當執行副作用函數的時候把它入棧,執行完畢后把它出棧。現在我們一起看一下源碼怎么處理的:

  • 按位跟蹤標記遞歸深度方式(優化方案):通過用二進制位標記當前嵌套深度的副作用函數是否記錄過,如果記錄過就,如果已經超過最大深度,因為采用降級方案,是全部刪除然后重新收集副作用函數的。
     let effectTrackDepth = 0 // 當前副作用函數遞歸深度export let trackOpBit = 1 // 在track函數中執行當前的嵌套副作用函數的標志位const maxMarkerBits = 30 // 最大遞歸深度支持30位,

為什么需要設置30位,因為31位會溢出。

// 每次執行 effect 副作用函數前,全局變量嵌套深度會自增1
trackOpBit = 1 << ++effectTrackDepth// 執行完副作用函數后會自減
trackOpBit = 1 << --effectTrackDepth;

為什么是左移一位,是因為第一位也就是說當前深度只是1,所以保持不變,不用管,從第二位開始。

  if (effectTrackDepth <= maxMarkerBits) {// 執行副作用函數之前,使用 `deps[i].w |= trackOpBit`對依賴dep[i]進行標記,追蹤依賴initDepMarkers(this)} else {// 降級方案:完全清理cleanupEffect(this)}

如何判斷當前依賴是否已記錄過,通過按位與判斷是否有位已經標識,有就大于0:

   //代表副作用函數執行前被 track 過export const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0//代表副作用函數執行后被 track 過export const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
  • 清理依賴:
    export const finalizeDepMarkers = (effect: ReactiveEffect) => {const { deps } = effectif (deps.length) {let ptr = 0for (let i = 0; i < deps.length; i++) {const dep = deps[i]// 有 was 標記但是沒有 new 標記,應當刪除if (wasTracked(dep) && !newTracked(dep)) {dep.delete(effect)} else {// 需要保留的依賴deps[ptr++] = dep}// 清空,把當前位值0,先按位非,再按位與dep.w &= ~trackOpBitdep.n &= ~trackOpBit}// 保留依賴的長度deps.length = ptr}}
  • 完全清理方式(降級方案):逐個清理掉當前依賴集合deps中每個依賴。

    function cleanupEffect(effect: ReactiveEffect) {
    const { deps } = effect
    if (deps.length) {for (let i = 0; i < deps.length; i++) {deps[i].delete(effect)}deps.length = 0
    }
    }
    

響應式可調度性scheduler:trigger 動作觸發副作用函數重新執行時,有能力決定副作用函數執行的時機、次數以及方式。

Vue3響應式的6個細節我們都了解了,我們可以對副作用工作流做一個全面總結如圖:

在這里插入圖片描述

Vue3響應式的關鍵在于兩個函數:track(收集依賴)和trigger(觸發依賴)。

   // target: 響應式代理對象, type: 訂閱類型(get、hase、iterate), key: 要獲取的target的鍵值export function track(target: object, type: TrackOpTypes, key: unknown) {// 如果允許追蹤, 并且當前有正在運行的副作用if (shouldTrack && activeEffect) {// 獲取當前target訂閱的副作用集合, 如果不存在, 則新建一個let depsMap = targetMap.get(target)if (!depsMap) {// 獲取對應屬性key訂閱的副作用, 如果不存在, 則新建一個targetMap.set(target, (depsMap = new Map()))}let dep = depsMap.get(key)if (!dep) {depsMap.set(key, (dep = createDep()))}...// 處理訂閱副作用trackEffects(dep, eventInfo)}}export function trackEffects(dep: Dep,debuggerEventExtraInfo?: DebuggerEventExtraInfo) {let shouldTrack = falseif (effectTrackDepth <= maxMarkerBits) { // 如果當前追蹤深度不超過最大深度(30), 則添加訂閱if (!newTracked(dep)) { // 如果未訂閱過, 則新建dep.n |= trackOpBit // 據當前的追蹤標識位設置依賴的new值shouldTrack = !wasTracked(dep) // 開啟訂閱追蹤}} else {shouldTrack = !dep.has(activeEffect!)}if (shouldTrack) {dep.add(activeEffect!) // 將當前正在運行副作用作為新訂閱者添加到該依賴中activeEffect!.deps.push(dep) // 緩存依賴到當前正在運行的副作用依賴數組...}}// 根據不同的type從depsMap取出,放入effects,隨后通過run方法將當前的`effect`執行export function trigger(target: object,type: TriggerOpTypes,key?: unknown,newValue?: unknown,oldValue?: unknown,oldTarget?: Map<unknown, unknown> | Set<unknown>) {const depsMap = targetMap.get(target) // 獲取響應式對象的副作用Map, 如果不存在說明未被追蹤, 則不需要處理if (!depsMap) {return}let deps: (Dep | undefined)[] = []// 如果是清除操作,那就要執行依賴原始數據的所有監聽方法。因為所有項都被清除了。if (type === TriggerOpTypes.CLEAR) { // clear// 如果是調用了集合的clear方法, 則要對其所有的副作用進行處理deps = [...depsMap.values()]} else if (key === 'length' && isArray(target)) {const newLength = Number(newValue)depsMap.forEach((dep, key) => {if (key === 'length' || key >= newLength) {deps.push(dep)}})} else { // set add delete// key不為void 0,則說明肯定是SET | ADD | DELETE這三種操作 // 然后將依賴這個key的所有監聽函數推到相應隊列中if (key !== void 0) {deps.push(depsMap.get(key))}switch (type) { // 根據不同type取出并存入depscase TriggerOpTypes.ADD:// 如果原始數據是數組,則key為length,否則為迭代行為標識符if (!isArray(target)) {deps.push(depsMap.get(ITERATE_KEY))if (isMap(target)) {deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))}} else if (isIntegerKey(key)) {deps.push(depsMap.get('length'))}breakcase TriggerOpTypes.DELETE:// 如果原始數據是數組,則key為length,否則為迭代行為標識符if (!isArray(target)) {deps.push(depsMap.get(ITERATE_KEY))if (isMap(target)) {deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))}}breakcase TriggerOpTypes.SET:if (isMap(target)) {deps.push(depsMap.get(ITERATE_KEY))}break}}...const effects: ReactiveEffect[] = []for (const dep of deps) {if (dep) {effects.push(...dep)}}// 遍歷effects元素執行run函數triggerEffects(createDep(effects))}}

Vue3響應式原理小結:

Vue3中的副作用函數其實就是Vue2的依賴

  • activeEffect解決匿名函數問題
  • WeakMap、Map、Set存儲對象屬性的相關副作用函數
  • 處理副作用函數時,假如有多個響應式屬性,控制只觸發生效的屬性或用到的屬性
  • 嵌套副作用函數,使用二進制位記錄嵌套副作用,通過控制二進制位是否清理嵌套副作用實現層級追蹤
  • track()實現依賴收集、層級依賴追蹤、依賴清理(解決嵌套副作用)
  • trigger()當某個依賴值發生變化時觸發的, 根據依賴值的變化類型, 會收集與依賴相關的不同副作用處理對象, 然后逐個觸發他們的 run 函數, 通過執行副作用函數獲得與依賴變化后對應的最新值

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

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

相關文章

前端:多服務端接口資源整合與zip打包下載

項目需求 前端項目開發中,有一個頁面需要去整合多個服務接口返回的數據資源,并且需要將這多個服務接口接口返回的數據進行資源壓縮,最終打包成zip壓縮包,并在客戶端完成下載。 基本需求梳理如下, 實現思路 這個需求點其實本質上還是傳統的“文件下載”功能需求,常見的例如…

Python使用defaultdict簡化值為list的字典

原始代碼&#xff1a; from typing import Dictrelated_objects_for_fetch: Dict[str, list] {}for key, value in [(k1, v1), (k1, v2), (k2, v2), (k3, v3), (k2, v2)]:if key not in related_objects_for_fetch:related_objects_for_fetch[key] []if value not in (value…

貪心問題(POJ1700/1017/1065)(C++)

一、貪心問題 貪心算法 貪心算法&#xff08;greedy algorithm&#xff09;&#xff0c;是用計算機來模擬一個「貪心」的人做出決策的過程。這個人十分貪婪&#xff0c;每一步行動總是按某種指標選取最優的操作。而且他目光短淺&#xff0c;總是只看眼前&#xff0c;并不考慮…

第三天:LINK3D核心原理講解【第1部分】

第三天:LINK3D核心原理講解 LINK3D學習筆記 目標 了解LINK3D velodyne64線激光雷達LINK3D質心點提取效果: 分布在車道與墻體的交界處。 課程內容 LINK3D論文精講LINK3D聚合關鍵點提取代碼講解LINK3D描述子匹配代碼講解除了ALOAM的線特征、面特征,還有其他點云特征嗎,是…

如何使用 Postgres 折疊您的堆棧 實現一切#postgresql認證

技術蔓延如何蔓延 假設您正在開發一款新產品或新功能。一開始&#xff0c;您的團隊會列出需要解決的技術問題。有些解決方案您將自行開發&#xff08;您的秘訣&#xff09;&#xff0c;而其他解決方案您將使用現有技術&#xff08;可能至少包括一個數據庫&#xff09;來解決。…

人工智能期末復習筆記(更新中)

分類問題 分類&#xff1a;根據已知樣本的某些特征&#xff0c;判斷一個新的樣本屬于哪種已知的樣本類 垃圾分類、圖像分類 怎么解決分類問題 分類和回歸的區別 1. 邏輯回歸分類 用于解決分類問題的一種模型。根據數據特征或屬性&#xff0c;計算其歸屬于某一類別 的概率P,…

ComfyUI局部重繪的四種方式 (附件工作流在最后)

前言 局部重繪需要在圖片中選擇重繪區域&#xff0c;點擊圖片右擊選擇Open in MaskEditor&#xff08;在蒙版編輯器中打開&#xff09;&#xff0c;用鼠標描繪出需要重繪的區域 方式一&#xff1a;重繪編碼器 這種方式重繪比較生硬&#xff0c;需要額外搭配使用才行 方式二&…

el-upload 上傳圖片及回顯照片和預覽圖片,文件流和http線上鏈接格式操作

<div v-for"(info, index) in zsjzqwhxqList.helicopterTourInfoList" :key"info.id" >編輯上傳圖片// oss返回線上地址http鏈接格式&#xff1a;<el-form-itemlabel"巡視結果照片":label-width"formLabelWidth"><el…

Cyber Weekly #13

賽博新聞 1、谷歌發布最強開源小模型Gemma-2 本周五&#xff08;6月28日&#xff09;凌晨&#xff0c;谷歌發布最強開源小模型Gemma-2&#xff0c;分別為9B&#xff08;90億&#xff09;和27B&#xff08;270億&#xff09;參數規模&#xff0c;其中9B 模型在多項基準測試中均…

潁川韓氏,來自戰國七雄韓國的豪族

潁川是戰國七雄韓國故土&#xff0c;韓國被秦國滅國后&#xff0c;王公貴族們除了堅決反秦的被殺了外&#xff0c;大部分都留存了下來。這些人在楚、漢反秦戰爭中&#xff0c;成為反秦統一戰線的重要力量&#xff0c;其中兩人先后被封為重新恢復的韓國的國王。 一個是橫陽君韓…

Windows系統下,將nginx注冊為本地服【親測可用】

在Windows系統下&#xff0c;將nginx注冊為本地服務并設置為開機自動運行&#xff0c;可以通過以下步驟進行&#xff1a; 下載Windows Service Wrapper&#xff1a; 訪問 WinSW 項目頁面&#xff0c;下載適用于Windows的版本。 準備nginx&#xff1a; 確保你已經下載并解壓了n…

共有5部手機,如果通過服務器讓1號手機執行打開 “閑魚.js“ 腳本

1.手機端Auto.js腳本&#xff1a; 每部手機需要在連接時發送一個唯一的標識符&#xff08;如設備ID&#xff09;&#xff0c;并接收服務器發送的指令以執行指定的腳本。 // Auto.js腳本連接WebSocket服務器并發送設備ID var WebSocket require(ws); var ws new WebSocket(w…

大模型上下文長度擴展中的檢索增強技術簡述

基于Transformer的語言模型在眾多自然語言處理任務上都取得了十分優異的成績&#xff0c;在一些任務上已經達到SOTA的效果。但是&#xff0c;經過預訓練后&#xff0c;模型能夠較好處理的序列長度就固定下來。而當前的眾多場景往往需要處理很長的上下文&#xff08;如&#xff…

CppTest單元測試框架(更新)

目錄 1 背景2 設計3 實現4 使用4.1 主函數4.2 使用方法 1 背景 前面文章單元測試之CppTest測試框架中講述利用宏ADD_SUITE將測試用例自動增加到測試框架中。但在使用中發現一個問題&#xff0c;就是通過宏ADD_SUITE增加多個測試Suite時&#xff0c;每次運行時都是所有測試Suit…

逆向開發環境準備

JDK安裝 AndroidStudio安裝 默認sdk路徑 C:\Users\Administrator\AppData\Local\Android\Sdk 將platform-tools所在的目錄添加到path C:\Users\Administrator\AppData\Local\Android\Sdk\platform-tools 主要目的是使用該目錄下的adb等命令 將tools所在的目錄添加到path C:\Us…

1-5題查詢 - 高頻 SQL 50 題基礎版

目錄 1. 相關知識點2. 例題2.1.可回收且低脂的產品2.2.尋找用戶推薦人2.3.大的國家2.4. 文章瀏覽 I2.5. 無效的推文 1. 相關知識點 sql判斷&#xff0c;不包含null&#xff0c;判斷不出來distinct是通過查詢的結果來去除重復記錄ASC升序計算字符長度 CHAR_LENGTH() 或 LENGTH(…

sqlmap注入詳解

免責聲明:本文僅做分享... 目錄 1.介紹 2.特點 3.下載 4.幫助文檔 5.常見命令 指定目標 請求 HTTP cookie頭 HTTP User-Agent頭 HTTP協議的證書認證 HTTP(S)代理 HTTP請求延遲 設定超時時間 設定重試超時 設定隨機改變的參數值 利用正則過濾目標網址 避免過多的…

Python-爬蟲案例

Python-爬蟲案例 代碼代碼 代碼 import requests import json import threading from queue import Queue import timeclass HeiMaTouTiao:def __init__(self):self.headers {"User-Agent": "Mozilla/5.0 (Windows NT 6.1; WOW64) ""AppleWebKit/53…

前端筆記-day11

文章目錄 01-空間-平移02-視距03-空間旋轉Z軸04-空間旋轉X軸05-空間旋轉Y軸06-立體呈現07-案例-3D導航08-空間縮放10-動畫實現步驟11-animation復合屬性12-animation拆分寫法13-案例-走馬燈14-案例-精靈動畫15-多組動畫16-全民出游全民出游.htmlindex.css 01-空間-平移 <!D…

基于Spring Boot的在線醫療咨詢平臺的設計與實現【附源碼】

基于Spring Boot的在線醫療咨詢平臺的設計與實現 Design and implementation of the computer hardware mall based on Spring Boot Candidate&#xff1a; Supervisor&#xff1a; April 20th, 2024 學位論文原創性聲明 本人鄭重聲明&#xff1a;所呈交的論文是本人在導師…