vue3.0快發(fā)布了轻局,也帶來了很多新的特性,如新的監(jiān)測設(shè)計挡毅,PWA蒜撮,TS支持等,本節(jié)一起了解下新的監(jiān)測原理跪呈。
舊的響應(yīng)式原理
vue2利用Object.defineProperty來劫持data數(shù)據(jù)的getter和setter操作段磨。這使得data在被訪問或賦值時,動態(tài)更新綁定的template模塊耗绿。
對象:會遞歸得去循環(huán)vue得每一個屬性苹支,(這也是浪費(fèi)性能的地方)會給每個屬性增加getter和setter,當(dāng)屬性發(fā)生變化的時候會更新視圖误阻。
數(shù)組:重寫了數(shù)組的方法债蜜,當(dāng)調(diào)用數(shù)組方法時會觸發(fā)更新琉用,也會對數(shù)組中的每一項(xiàng)進(jìn)行監(jiān)控。
缺點(diǎn):對象只監(jiān)控自帶的屬性策幼,新增的屬性不監(jiān)控邑时,也就不生效。若是后續(xù)需要這個自帶屬性特姐,就要再初始化的時候給它一個undefined值晶丘,后續(xù)再改這個值
數(shù)組的索引發(fā)生變化或者數(shù)組的長度發(fā)生變化不會觸發(fā)實(shí)體更新√坪可以監(jiān)控引用數(shù)組中引用類型值浅浮,若是一個普通值并不會監(jiān)控,例如:[1, 2, {a: 3}] ,只能監(jiān)控a
Proxy消除了之前 Vue2.x 中基于 Object.defineProperty 的實(shí)現(xiàn)所存在的這些限制:無法監(jiān)聽 屬性的添加和刪除捷枯、數(shù)組索引和長度的變更滚秩,并可以支持 Map、Set淮捆、WeakMap 和 WeakSet郁油!
Proxy及使用
MDN 上是這么描述的——Proxy
對象用于定義基本操作的自定義行為(如屬性查找,賦值攀痊,枚舉桐腌,函數(shù)調(diào)用等)。
其實(shí)就是在對目標(biāo)對象的操作之前提供了攔截苟径,可以對外界的操作進(jìn)行過濾和改寫案站,修改某些操作的默認(rèn)行為,這樣我們可以不直接操作對象本身棘街,而是通過操作對象的代理對象來間接來操作對象蟆盐,達(dá)到預(yù)期的目的~
一個例子:
let obj = {
name:{name:'hhh'},
arr: ['吃','喝','玩']
}
//proxy兼容性差 可以代理13種方法 get set
//defineProperty 只對特定 的屬性進(jìn)行攔截
let handler = {
get (target,key) { //target就是obj key就是要取obj里面的哪個屬性
console.log('收集依賴')
return target[key]
},
set (target,key,value) {
console.log('觸發(fā)更新')
target[key] = value
}
}
let proxy = new Proxy(obj,handler)
//通過代理后的對象取值和設(shè)置值
proxy.arr //收集依賴
proxy.name = '123' //觸發(fā)更新
定義了一個對象obj,通過代理后的對象(上面的proxy)來操作原對象遭殉。當(dāng)取值的時候會走get方法石挂,返回對應(yīng)的值,當(dāng)設(shè)置值的時候會走set方法恩沽,觸發(fā)更新誊稚。
但這是老的寫法翔始,新的寫法是使用Reflect罗心。
Reflect是內(nèi)置對象,為操作對象而提供的新API城瞎,將Object對象的屬于語言內(nèi)部的方法放到Reflect對象上渤闷,即從Reflect對象上拿Object對象內(nèi)部方法。 如果出錯將返回false
簡單改寫上面這個例子:
let handler = {
get (target,key) { //target就是obj key就是要取obj里面的哪個屬性
console.log('收集依賴')
// return target[key]
//Reflect 反射 這個方法里面包含了很多api
return Reflect.get(target,key)
},
set (target,key,value) {
console.log('觸發(fā)更新')
// target[key] = value //這種寫法設(shè)置時如果不成功也不會報錯 比如這個對象默認(rèn)不可配置
Reflect.set(target,key,value)
}
}
let proxy = new Proxy(obj,handler)
//通過代理后的對象取值和設(shè)置值
proxy.arr //收集依賴
proxy.name.name //收集依賴(只有一個)
proxy.name = '123' //觸發(fā)更新
效果依舊和上面一樣脖镀。
但是有一個問題飒箭,這個對象是多層對象,它并不會取到里面的那個name的值。
這是因?yàn)橹癘bject.defineProperty方法是一開始就會對這個多層對象進(jìn)行遞歸處理弦蹂,所以可以拿到肩碟,而Proxy不會。它是懶代理凸椿。如果對這個對象里面的值進(jìn)行代理就取不到值削祈。就像上面我們只對name進(jìn)行了代理,但并沒有對name.name進(jìn)行代理脑漫,所以他就取不到這個值髓抑,需要代理之后才能取到。
改進(jìn)如下:
let obj = {
name:{name:'hhh'},
arr: ['吃','喝','玩']
}
//proxy兼容性差 可以代理13種方法 get set
//defineProperty 只對特定 的屬性進(jìn)行攔截
let handler = {
get (target,key) { //target就是obj key就是要取obj里面的哪個屬性
console.log('收集依賴')
if(typeof target[key] === 'object' && target[key] !== null){
//遞歸代理优幸,只有取到對應(yīng)值的時候才會代理
return new Proxy(target[key],handler)
}
// return target[key]
//Reflect 反射 這個方法里面包含了很多api
return Reflect.get(target,key)
},
set (target,key,value) {
console.log('觸發(fā)更新')
// target[key] = value //這種寫法設(shè)置時如果不成功也不會報錯 比如這個對象默認(rèn)不可配置
Reflect.set(target,key,value)
}
}
let proxy = new Proxy(obj,handler)
proxy.arr //收集依賴
proxy.name.name //收集依賴(2個)
接下來看看數(shù)組的代理過程:
let obj = {
name:{name:'hhh'},
arr: ['吃','喝','玩']
}
//proxy兼容性差 可以代理13種方法 get set
//defineProperty 只對特定 的屬性進(jìn)行攔截
let handler = {
get (target,key) { //target就是obj key就是要取obj里面的哪個屬性
console.log('收集依賴')
if(typeof target[key] === 'object' && target[key] !== null){
//遞歸代理吨拍,只有取到對應(yīng)值的時候才會代理
return new Proxy(target[key],handler)
}
// return target[key]
//Reflect 反射 這個方法里面包含了很多api
return Reflect.get(target,key)
},
set (target,key,value) {
console.log('觸發(fā)更新')
// target[key] = value //這種寫法設(shè)置時如果不成功也不會報錯 比如這個對象默認(rèn)不可配置
return Reflect.set(target,key,value)
}
}
let proxy = new Proxy(obj,handler)
//通過代理后的對象取值和設(shè)置值
// proxy.name.name = '123' //設(shè)置值,取一次网杆,設(shè)置一次
proxy.arr.push(456)
//輸出
收集依賴 (proxy.arr)
收集依賴 (proxy.arr.push)
收集依賴 (proxy.arr.length)
觸發(fā)更新 寫入新值
觸發(fā)更新 長度改變
proxy.arr[0]=456
//輸出
收集依賴 (proxy.arr)
觸發(fā)更新 寫入新值
這里面它會走兩次觸發(fā)更新的操作羹饰,因?yàn)榈谝淮涡枰薷臄?shù)組的長度,第二次再把元素放進(jìn)數(shù)組里碳却。所以我們需要判斷一下它是新增操作還是修改操作
判斷新舊屬性:
set (target,key,value) {
let oldValue = target[key]
console.log(key, oldValue, value)
if(!oldValue){
console.log('新增屬性')
}else if(oldValue !== value){
console.log('修改屬性')
}
return Reflect.set(target,key,value)
}
首先拿到它的舊值严里,如果這個值不存在就是新增,如果存在但不相等就是修改操作
vue3的響應(yīng)式設(shè)計
Vue 3.0 的想法是引入靈感來自于 React Hook 的 Function-based API追城,作為主要的組件聲明方式刹碾。
意思就是所有組件的初始狀態(tài)、computed座柱、watch迷帜、methods 都要在一個叫做 setup 的方法中定義,拋棄(暫時會繼續(xù)兼容)原有的基于對象的組件聲明方式色洞。
組件裝載
vue3.0用effect副作用鉤子來代替vue2.0watcher戏锹。我們都知道在vue2.0中,有渲染watcher專門負(fù)責(zé)數(shù)據(jù)變化后的從新渲染視圖火诸。vue3.0改用effect來代替watcher達(dá)到同樣的效果锦针。
先簡單介紹一下mountComponent流程,后面的文章會詳細(xì)介紹mount階段的
// 初始化組件
const mountComponent: MountComponentFn = (
...
) => {
/* 第一步: 創(chuàng)建component 實(shí)例 */
const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
...
))
/* 第二步 : TODO:初始化 初始化組件,建立proxy , 根據(jù)字符竄模版得到 */
setupComponent(instance)
/* 第三步:建立一個渲染effect置蜀,執(zhí)行effect */
setupRenderEffect(
instance, // 組件實(shí)例
initialVNode, //vnode
container, // 容器元素
...
)
}
整個mountComponent的主要分為了三步奈搜,我們這里分別介紹一下每個步驟干了什么:
① 第一步: 創(chuàng)建component 實(shí)例 。
② 第二步:初始化組件,建立proxy ,根據(jù)字符竄模版得到render函數(shù)盯荤。生命周期鉤子函數(shù)處理等等
③ 第三步:建立一個渲染effect馋吗,執(zhí)行effect。
從如上方法中我們可以看到秋秤,在setupComponent已經(jīng)構(gòu)建了響應(yīng)式對象宏粤,但是還沒有初始化收集依賴脚翘。
setupRenderEffect 構(gòu)建渲染effect
const setupRenderEffect: SetupRenderEffectFn = (
...
) => {
/* 創(chuàng)建一個渲染 effect */
instance.update = effect(function componentEffect() {
//...省去的內(nèi)容后面會講到
},{ scheduler: queueJob })
}
setupRenderEffect的作用
① 創(chuàng)建一個effect,并把它賦值給組件實(shí)例的update方法绍哎,作為渲染更新視圖用来农。
② componentEffect作為回調(diào)函數(shù)形式傳遞給effect作為第一個參數(shù)
effect做了些什么
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
const effect = createReactiveEffect(fn, options)
/* 如果不是懶加載 立即執(zhí)行 effect函數(shù) */
if (!options.lazy) {
effect()
}
return effect
}
effect作用如下
① 首先調(diào)用。createReactiveEffect.
② 如果不是懶加載 立即執(zhí)行 由createReactiveEffect創(chuàng)建出來的ReactiveEffect函數(shù)崇堰。
接著看createReactiveEffect(這就是vue2.x中的Watcher)
function createReactiveEffect<T = any>(
fn: (...args: any[]) => T, /**回調(diào)函數(shù) */
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
try {
enableTracking()
effectStack.push(effect) //往effect數(shù)組中里放入當(dāng)前 effect
activeEffect = effect //TODO: effect 賦值給當(dāng)前的 activeEffect
return fn(...args) //TODO: fn 為effect傳進(jìn)來 componentEffect
} finally {
effectStack.pop() //完成依賴收集后從effect數(shù)組刪掉這個 effect
resetTracking()
/* 將activeEffect還原到之前的effect */
activeEffect = effectStack[effectStack.length - 1]
}
} as ReactiveEffect
/* 配置一下初始化參數(shù) */
effect.id = uid++
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = [] /* TODO:用于收集相關(guān)依賴 */
effect.options = options
return effect
}
createReactiveEffect的作用主要是配置了一些初始化的參數(shù)备图,然后包裝了之前傳進(jìn)來的fn重要的一點(diǎn)是把當(dāng)前的effect賦值給了activeEffect,這一點(diǎn)非常重要,和收集依賴有著直接的關(guān)系.
整個響應(yīng)式初始化階段進(jìn)行總結(jié)
① setupComponent創(chuàng)建組件赶袄,調(diào)用composition-api,處理options(構(gòu)建響應(yīng)式)得到Observer對象揽涮。
② 創(chuàng)建一個渲染effect,里面包裝了真正的渲染方法componentEffect饿肺,添加一些effect初始化屬性蒋困。
③ 然后立即執(zhí)行effect,然后將當(dāng)前渲染effect賦值給activeEffect
數(shù)據(jù)響應(yīng)
vue3新的響應(yīng)式書寫方式(老的也兼容)
setup() {
const state = {
count: 0,
double: computed(() => state.count * 2)
}
function increment() {
state.count++
}
onMounted(() => {
console.log(state.count)
})
watch(() => {
document.title = `count ${state.count}`
})
return {
state,
increment
}
}
感覺setup這塊就有點(diǎn)像 react hooks 理解成一個帶有數(shù)據(jù)的邏輯復(fù)用模塊敬辣,不再以vue組件為單位的代碼復(fù)用了
和React鉤子不同雪标,setup()函數(shù)僅被調(diào)用一次。
所以新的響應(yīng)書數(shù)據(jù)兩種聲明方式:
- Ref
前提:聲明一個類型 Ref
ref()函數(shù)源碼:
function ref(raw: unknown) {
if (isRef(raw)) {
return raw
}
// convert 內(nèi)容:判斷 raw是不是對象溉跃,是的話 調(diào)用reactive把raw響應(yīng)化
raw = convert(raw)
const r = {
_isRef: true,
get value() {
// track 理解為依賴收集
track(r, OperationTypes.GET, '')
return raw
},
set value(newVal) {
raw = convert(newVal)
// trigger 理解為觸發(fā)監(jiān)聽村刨,就是觸發(fā)頁面更新好了
trigger(r, OperationTypes.SET, '')
}
}
return r as Ref
}
上面的convert函數(shù)內(nèi)容為
const convert = val => isObject(val) ? reactive(val) : val
可以看得出 ref類型 只會包裝最外面一層,內(nèi)部的對象最終還是調(diào)用reactive撰茎,生成Proxy對象進(jìn)行響應(yīng)式代理嵌牺。
疑問:可能有人想問,為什么不都用proxy, 內(nèi)部對象都用proxy,最外層還要搞個 Ref類型龄糊,多此一舉嗎逆粹?
理由可能比較簡單,那就是proxy代理的都是對象炫惩,對于基本數(shù)據(jù)類型僻弹,函數(shù)傳遞或?qū)ο蠼Y(jié)構(gòu)是,會丟失原始數(shù)據(jù)的引用他嚷。
官方解釋:
However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:
- Reactive
源碼如下:
注:target一定是一個對象蹋绽,不然會報警告
function reactive(target) {
// 如果target是一個只讀響應(yīng)式數(shù)據(jù)
if (readonlyToRaw.has(target)) {
return target
}
// 如果是被用戶標(biāo)記的只讀數(shù)據(jù),那通過readonly函數(shù)去封裝
if (readonlyValues.has(target)) {
return readonly(target)
}
// go ----> step2
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers, // 注意傳遞
mutableCollectionHandlers
)
}
createReactiveObject函數(shù)如下:
function createReactiveObject(
target: unknown,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 判斷target不是對象就 警告 并退出
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 通過原始數(shù)據(jù) -> 響應(yīng)數(shù)據(jù)的映射筋蓖,獲取響應(yīng)數(shù)據(jù)
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// 如果原始數(shù)據(jù)本身就是個響應(yīng)數(shù)據(jù)了卸耘,直接返回自身
if (toRaw.has(target)) {
return target
}
// 如果是不可觀察的對象,則直接返回原對象
if (!canObserve(target)) {
return target
}
// 集合數(shù)據(jù)與(對象/數(shù)組) 兩種數(shù)據(jù)的代理處理方式不同
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// 聲明一個代理對象 ----> step3
observed = new Proxy(target, handlers)
// 兩個weakMap 存target observed
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
通過上面源碼創(chuàng)建proxy對象的大致流程是這樣的:
①首先判斷目標(biāo)對象有沒有被proxy響應(yīng)式代理過扭勉,如果是那么直接返回對象鹊奖。
②然后通過判斷目標(biāo)對象是否是[ Set, Map, WeakMap, WeakSet ]數(shù)據(jù)類型來選擇是用collectionHandlers , 還是baseHandlers->就是reactive傳進(jìn)來的mutableHandlers作為proxy的hander對象涂炎。
③最后通過真正使用new proxy來創(chuàng)建一個observed 忠聚,然后通過rawToReactive reactiveToRaw 保存 target和observed鍵值對。
攔截器
在baseHandles基類處理對象中可以看到對set/get的攔截處理
(我們以對象類型為例唱捣,集合類型的handlers稍復(fù)雜點(diǎn))
handlers如下两蟀,new Proxy(target, handles)的 handles就是下面這個對象
export const mutableHandlers = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
依賴收集
打開createGetter(false)看實(shí)現(xiàn)思路如下:
思路:當(dāng)我們代理get獲取到res時,判斷res 是否是對象震缭,如果是那么 繼續(xù)reactive(res)赂毯,可以說是一個遞歸
reactive(target) ->
createReactiveObject(target,handlers) ->
new Proxy(target, handlers) ->
createGetter(readonly) ->
get() -> res ->
isObject(res) ? reactive(res) : res
function createGetter(isReadonly: boolean) {
// isReadonly 用來區(qū)分是否是只讀響應(yīng)式數(shù)據(jù)
// receiver即是被創(chuàng)建出來的代理對象
return function get(target: object, key: string | symbol, receiver: object) {
// 獲取原始數(shù)據(jù)的響應(yīng)值
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
if (isRef(res)) {
return res.value
}
// 收集依賴
track(target, OperationTypes.GET, key)
// 這里判斷上面獲取的res 是否是對象,如果是對象 則調(diào)用reactive并且傳遞的是獲取到的res拣宰,
// 則形成了遞歸
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
在vue2.0的時候党涕。響應(yīng)式是在初始化的時候就深層次遞歸處理了
但是與vue2.0不同的是,即便是深度響應(yīng)式我們也只能在獲取上一級get之后才能觸發(fā)下一級的深度響應(yīng)式。
這樣做好處是:
- 初始化的時候不用遞歸去處理對象巡社,造成了不必要的性能開銷膛堤。
- 有一些沒有用上的state,這里就不需要在深層次響應(yīng)式處理晌该。
先來看看track源碼:
/* target 對象本身 肥荔,key屬性值 type 為 'GET' */
export function track(target: object, type: TrackOpTypes, key: unknown) {
/* 當(dāng)打印或者獲取屬性的時候 console.log(this.a) 是沒有activeEffect的 當(dāng)前返回值為0 */
let depsMap = targetMap.get(target)
if (!depsMap) {
/* target -map-> depsMap */
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
/* key : dep dep觀察者 */
depsMap.set(key, (dep = new Set()))
}
/* 當(dāng)前activeEffect */
if (!dep.has(activeEffect)) {
/* dep添加 activeEffect */
dep.add(activeEffect)
/* 每個 activeEffect的deps 存放當(dāng)前的dep */
activeEffect.deps.push(dep)
}
}
里面主要引入了兩個概念 targetMap和 depsMap
track作用大致是,首先根據(jù) proxy對象朝群,獲取存放deps的depsMap燕耿,然后通過訪問的屬性名key獲取對應(yīng)的dep,然后將當(dāng)前激活的effect存入當(dāng)前dep收集依賴。
主要作用
①找到與當(dāng)前proxy 和 key對應(yīng)的dep姜胖。
②dep與當(dāng)前activeEffect建立聯(lián)系誉帅,收集依賴。
為了方便理解右莱,targetMap 和 depsMap的關(guān)系堵第,下面我們用一個例子來說明:
例子:
<div id="app" >
<span>{{ state.a }}</span>
<span>{{ state.b }}</span>
<div>
<script>
const { createApp, reactive } = Vue
/* 子組件 */
const Children ={
template="<div> <span>{{ state.c }}</span> </div>",
setup(){
const state = reactive({
c:1
})
return {
state
}
}
}
/* 父組件 */
createApp({
component:{
Children
}
setup(){
const state = reactive({
a:1,
b:2
})
return {
state
}
}
})mount('#app')
渲染effect函數(shù)如何觸發(fā)get
前面說過,創(chuàng)建一個渲染renderEffect隧出,然后把賦值給activeEffect踏志,最后執(zhí)行renderEffect ,在這個期間是怎么做依賴收集的呢胀瞪,讓我們一起來看看,update函數(shù)中做了什么针余,我們回到之前講的componentEffect邏輯上來
function componentEffect() {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, a, parent } = instance
/* TODO: 觸發(fā)instance.render函數(shù),形成樹結(jié)構(gòu) */
const subTree = (instance.subTree = renderComponentRoot(instance))
if (bm) {
//觸發(fā) beforeMount聲明周期鉤子
invokeArrayFns(bm)
}
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
/* 觸發(fā)聲明周期 mounted鉤子 */
if (m) {
queuePostRenderEffect(m, parentSuspense)
}
instance.isMounted = true
} else {
// 更新組件邏輯
// ......
}
}
代碼大致首先會通過renderComponentRoot方法形成樹結(jié)構(gòu)凄诞,這里要注意的是圆雁,我們在最初mountComponent的setupComponent方法中,已經(jīng)通過編譯方法compile編譯了template模版的內(nèi)容帆谍,state.a state.b等抽象語法樹伪朽,最終返回的render函數(shù)在這個階段會被觸發(fā),在render函數(shù)中在模版中的表達(dá)式 state.a state.b 點(diǎn)語法會被替換成data中真實(shí)的屬性汛蝙,這時候就進(jìn)行了真正的依賴收集烈涮,觸發(fā)了get方法朴肺。接下來就是觸發(fā)生命周期 beforeMount ,然后對整個樹結(jié)構(gòu)重新patch,patch完畢后,調(diào)用mounted鉤子.
依賴收集流程總結(jié)
① 首先執(zhí)行renderEffect 坚洽,賦值給activeEffect 戈稿,調(diào)用renderComponentRoot方法,然后觸發(fā)render函數(shù)讶舰。
② 根據(jù)render函數(shù)鞍盗,解析經(jīng)過compile,語法樹處理過后的模版表達(dá)式跳昼,訪問真實(shí)的data屬性般甲,觸發(fā)get。
③ get方法首先經(jīng)過之前不同的reactive鹅颊,通過track方法進(jìn)行依賴收集敷存。
④ track方法通過當(dāng)前proxy對象target,和訪問的屬性名key來找到對應(yīng)的dep。
⑤ 將dep與當(dāng)前的activeEffect建立起聯(lián)系挪略。將activeEffect壓入dep數(shù)組中历帚,(此時的dep中已經(jīng)含有當(dāng)前組件的渲染effect,這就是響應(yīng)式的根本原因)如果我們觸發(fā)set,就能在數(shù)組中找到對應(yīng)的effect杠娱,依次執(zhí)行挽牢。
set 派發(fā)更新
set的一個主要作用去觸發(fā)監(jiān)聽,使試圖更新摊求,需要注意的是控制什么時候才是視圖需要真的更新
function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 拿到新值的原始數(shù)據(jù)
value = toRaw(value)
// 獲取舊值
const oldValue = (target as any)[key]
// 如果舊值是Ref類型禽拔,新值不是,那么直接更新值室叉,并返回
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// 如果是原始數(shù)據(jù)原型鏈上的數(shù)據(jù)操作睹栖,不做任何觸發(fā)監(jiān)聽函數(shù)的行為。
if (target === toRaw(receiver)) {
// 更新的兩種條件
// 1. 不存在key茧痕,即當(dāng)前操作是在新增屬性
// 2. 舊值和新值不等
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
}
return result
}
疑問:對于數(shù)據(jù)的set操作會出發(fā)多次traps?
這里有個前提了解:就是我們?nèi)粘P薷臄?shù)組野来,比如 let a = [1], a.push(2),
這個push操作踪旷,我們是實(shí)際上是對a做了2個屬性的修改曼氛,1,set length 1令野; 2. set value 2
所以我們的set traps會出發(fā)多次
思路:通過屬性值和value控制舀患,比如當(dāng) set key是 length的時候,我們可以判斷當(dāng)前數(shù)組 已經(jīng)有此屬性气破,所以不需要出發(fā)更新聊浅,當(dāng)新設(shè)置的值和老值一樣是也不需要更新。
疑問:set的源碼里面有 有一個 target === toRaw(receiver)條件下才繼續(xù)操作 trigger更新視圖?
這里就暴露出一個東西低匙,即存在 target !== toRaw(receiver)
Receiver: 最初被調(diào)用的對象旷痕。通常是 proxy 本身,但 handler 的 set 方法也有可能在原型鏈上或以其他方式被間接地調(diào)用(因此不一定是 proxy 本身)
其實(shí)源碼有注釋
// don’t trigger if target is something up in the prototype chain of original
即如果我們的操作是操作原始數(shù)據(jù)原型鏈上的數(shù)據(jù)操作努咐,target 就不等于 toRaw(receiver)
什么情況下 target !== toRaw(receiver)
例如:
const child = new Proxy(
{},
{ // 其他 traps 省略
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
console.log('child', receiver)
return true
}
}
)
const parent = new Proxy(
{ a: 10 },
{ // 其他 traps 省略
set(target, key, value, receiver) {
Reflect.set(target, key, value, receiver)
console.log('parent', receiver)
return true
}
}
)
Object.setPrototypeOf(child, parent) // child.__proto__ === parent true
child.a = 4
// 結(jié)果
// parent Proxy {a: 4}
// Proxy {a: 4}
從結(jié)果可以看出苦蒿,理論上 parent的set應(yīng)該不會觸發(fā)殴胧,但實(shí)際是觸發(fā)了渗稍,此時
target: {a: 10}
receiver: Proxy {a: 4}
// 在vue3中
toRaw(receiver): {a: 4}
set的流程大致是這樣的
① 首先通過toRaw判斷當(dāng)前的proxy對象和建立響應(yīng)式存入reactiveToRaw的proxy對象是否相等。
② 判斷target有沒有當(dāng)前key,如果存在的話团滥,改變屬性竿屹,執(zhí)行trigger(target, TriggerOpTypes.SET, key, value, oldValue)。
③ 如果當(dāng)前key不存在灸姊,說明是賦值新屬性拱燃,執(zhí)行trigger(target, TriggerOpTypes.ADD, key, value)
接著看下trigger
/* 根據(jù)value值的改變,從effect和computer拿出對應(yīng)的callback 力惯,然后依次執(zhí)行 */
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
/* 獲取depssMap */
const depsMap = targetMap.get(target)
/* 沒有經(jīng)過依賴收集的 碗誉,直接返回 */
if (!depsMap) {
return
}
const effects = new Set<ReactiveEffect>() /* effect鉤子隊(duì)列 */
const computedRunners = new Set<ReactiveEffect>() /* 計算屬性隊(duì)列 */
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || !shouldTrack) {
if (effect.options.computed) { /* 處理computed邏輯 */
computedRunners.add(effect) /* 儲存對應(yīng)的dep */
} else {
effects.add(effect) /* 儲存對應(yīng)的dep */
}
}
})
}
}
add(depsMap.get(key))
const run = (effect: ReactiveEffect) => {
if (effect.options.scheduler) { /* 放進(jìn) scheduler 調(diào)度*/
effect.options.scheduler(effect)
} else {
effect() /* 不存在調(diào)度情況,直接執(zhí)行effect */
}
}
//TODO: 必須首先運(yùn)行計算屬性的更新父晶,以便計算的getter
//在任何依賴于它們的正常更新effect運(yùn)行之前哮缺,都可能失效。
computedRunners.forEach(run) /* 依次執(zhí)行computedRunners 回調(diào)*/
effects.forEach(run) /* 依次執(zhí)行 effect 回調(diào)( TODO: 里面包括渲染effect )*/
}
trigger的核心邏輯
① 首先從targetMap中甲喝,根據(jù)當(dāng)前proxy找到與之對應(yīng)的depsMap尝苇。
② 根據(jù)key找到depsMap中對應(yīng)的deps,然后通過add方法分離出對應(yīng)的effect回調(diào)函數(shù)和computed回調(diào)函數(shù)埠胖。
③ 依次執(zhí)行computedRunners 和 effects 隊(duì)列里面的回調(diào)函數(shù)糠溜,如果發(fā)現(xiàn)需要調(diào)度處理,放進(jìn)scheduler事件調(diào)度