vue3響應(yīng)式--proxy

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ù)兩種聲明方式:

  1. 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:

  1. 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)度

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市直撤,隨后出現(xiàn)的幾起案子非竿,更是在濱河造成了極大的恐慌,老刑警劉巖谋竖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件红柱,死亡現(xiàn)場離奇詭異,居然都是意外死亡圈盔,警方通過查閱死者的電腦和手機(jī)豹芯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來驱敲,“玉大人铁蹈,你說我怎么就攤上這事。” “怎么了握牧?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵容诬,是天一觀的道長。 經(jīng)常有香客問我沿腰,道長览徒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任颂龙,我火速辦了婚禮习蓬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘措嵌。我一直安慰自己躲叼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布企巢。 她就那樣靜靜地躺著枫慷,像睡著了一般。 火紅的嫁衣襯著肌膚如雪浪规。 梳的紋絲不亂的頭發(fā)上或听,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機(jī)與錄音笋婿,去河邊找鬼誉裆。 笑死,一個胖子當(dāng)著我的面吹牛萌抵,可吹牛的內(nèi)容都是我干的找御。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼绍填,長吁一口氣:“原來是場噩夢啊……” “哼霎桅!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起讨永,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤滔驶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后卿闹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體揭糕,經(jīng)...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年锻霎,在試婚紗的時候發(fā)現(xiàn)自己被綠了著角。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡旋恼,死狀恐怖吏口,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤产徊,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布昂勒,位于F島的核電站,受9級特大地震影響舟铜,放射性物質(zhì)發(fā)生泄漏戈盈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一谆刨、第九天 我趴在偏房一處隱蔽的房頂上張望粉臊。 院中可真熱鬧是牢,春花似錦联喘、人聲如沸恰画。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至膝宁,卻和暖如春鸦难,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背员淫。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工合蔽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人介返。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓拴事,卻偏偏與公主長得像,于是被迫代替她去往敵國和親圣蝎。 傳聞我的和親對象是個殘疾皇子刃宵,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評論 2 355