欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

關(guān)于Vue3中的響應(yīng)式原理

 更新時間:2022年09月07日 10:47:39   作者:SunsetFeng  
這篇文章主要介紹了關(guān)于Vue3中的響應(yīng)式原理,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教

一、簡介

本章內(nèi)容主要通過具體的簡單示例來分析Vue3是如何實現(xiàn)響應(yīng)式的。理解本章需要了解Vue3的響應(yīng)式對象。只注重原理設(shè)計層面,細節(jié)不做太多講解。

二、響應(yīng)核心

1.核心源碼

export class ReactiveEffect<T = any> {
  //是否激活
  active = true
  //依賴列表
  deps: Dep[] = []
  // can be attached after creation
  computed?: boolean
  //是否允許遞歸
  allowRecurse?: boolean
  onStop?: () => void
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope | null
  ) {
  	//將自身添加到一個全局的EffectScope容器中
    recordEffectScope(this, scope)
  }
  run() {
    if (!this.active) {
      //沒有激活,直接調(diào)用回調(diào)方法
      return this.fn()
    }
    //棧中不存在當前對象
    if (!effectStack.includes(this)) {
      try {
        //推入棧頂,并且置為全局激活對象
        effectStack.push((activeEffect = this))
        //開啟依賴收集開關(guān)
        enableTracking()
        //位操作符:用于優(yōu)化
        trackOpBit = 1 << ++effectTrackDepth
        //源碼中maxMarkerBits取30 猜測是因為整數(shù)位運算時是按照32位計算 當1<<31時為負值了,后續(xù)負值的位運算得不到預期結(jié)果 所以取的最大30
        if (effectTrackDepth <= maxMarkerBits) {
          //將當前依賴列表的所有依賴置為"已經(jīng)收集"
          initDepMarkers(this)
        } else {
          //不采用優(yōu)化模式,使用老流程,直接移除依賴的全部狀態(tài)
          cleanupEffect(this)
        }
        //調(diào)用回調(diào)
        return this.fn()
      } finally {
        if (effectTrackDepth <= maxMarkerBits) {
          //斷掉依賴關(guān)聯(lián)
          finalizeDepMarkers(this)
        }
        //重置位操作狀態(tài)
        trackOpBit = 1 << --effectTrackDepth
        //重置依賴收集狀態(tài)
        resetTracking()
        //棧頂出棧
        effectStack.pop()
        const n = effectStack.length
        activeEffect = n > 0 ? effectStack[n - 1] : undefined
      }
    }
  }
  stop() {
    if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

上述ReactiveEffect對象,其實需要關(guān)注的就是一個run方法,這個方法設(shè)計得十分巧妙,所有動態(tài)響應(yīng)的本質(zhì)其實都是通過調(diào)用run方法實現(xiàn)的。

比如如下代碼:

    let dummy
    const counter = reactive({ num: 0 })
    let innerfunc = () => dummy = counter.num;
    effect(innerfunc)
    //下面的賦值,最終會執(zhí)行innerfunc,所以dummy會變成7
    counter.num = 7

可能會有疑惑,上述代碼并沒有出現(xiàn)ReactiveEffect類型的對象,它其實是在effect方法中創(chuàng)建的,我們接下來分析下effect方法。

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  if ((fn as ReactiveEffectRunner).effect) {
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  //創(chuàng)建對象并傳參
  const _effect = new ReactiveEffect(fn)
  if (options) {
    extend(_effect, options)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) {
    //執(zhí)行
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  return runner
}

這個方法的簡單用法很簡單,就是創(chuàng)建一個ReactiveEffect類型對象,然后執(zhí)行run方法。

可能對于recordEffectScope方法有疑惑,其實這個方法和響應(yīng)式無關(guān),它的主要作用是將一個ReactiveEffect對象放入一個effectScope容器對象內(nèi),這個容器對象可以方便快捷的對容器內(nèi)所有的ReactiveEffect對象和其子effectScope調(diào)用stop方法。只關(guān)注響應(yīng)式的話可以不作考慮。

2.逐步分析上述示例代碼

    let dummy
    //步驟1:創(chuàng)建一個響應(yīng)式對象
    const counter = reactive({ num: 0 })
    let innerfunc = () => dummy = counter.num;
    //步驟2:調(diào)用effect方法
    effect(innerfunc)
    //步驟3:修改響應(yīng)式對象數(shù)據(jù)
    counter.num = 7

上述的測試代碼看似就3個步驟,其實內(nèi)部做的東西非常多,我們來跟蹤下運行流程。 

步驟1:這一步很簡單,就是單純的創(chuàng)建一個Proxy對象,此時counter對象變成響應(yīng)式的。 

步驟2:effect方法里面最終調(diào)用的是run方法,而run方法主要是將自身掛載到全局激活并入棧,此時調(diào)用回調(diào)方法。回調(diào)方法此時為上面innerfunc方法,調(diào)用這個方法會讀取counter.num屬性,讀取響應(yīng)式對象的屬性會調(diào)用代理攔截處理的get方法,在get方法里面,會收集依賴。此時將依賴存于棧頂?shù)哪莻€ReactiveEffect對象的deps屬性中。 

步驟3:當響應(yīng)式對象的屬性修改后,會觸發(fā)依賴更新,由于觸發(fā)更新的依賴列表里面存在effect方法里面創(chuàng)建的ReactiveEffect對象,所以會重新調(diào)用其run方法,在這兒也就會調(diào)用innerfunc方法。所以dummy屬性就會跟隨counter.num屬性的變化而變化

備注:上述三步驟中,提及了收集依賴和觸發(fā)依賴更新。接下來我們便看一下是如何收集依賴和觸發(fā)依賴更新的。

3.收集依賴和觸發(fā)依賴更新

(1).收集依賴

export function track(target: object, type: TrackOpTypes, key: unknown) {
  //是否允許收集
  if (!isTracking()) {
    return
  }
  //對象map
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  //依賴map
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }
  const eventInfo = __DEV__
    ? { effect: activeEffect, target, type, key }
    : undefined
  //收集依賴
  trackEffects(dep, eventInfo)
}
export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      //本一輪調(diào)用新收集的依賴
      dep.n |= trackOpBit // set newly tracked
      //是否應(yīng)該被收集
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }
  if (shouldTrack) {
    //收集依賴
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack(
        Object.assign(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo
        )
      )
    }
  }
}

上述代碼是收集依賴的核心代碼??催^我響應(yīng)式對象文章的話,應(yīng)該會注意到,在涉及“讀”相關(guān)操作時,就會調(diào)用track方法來收集依賴。此時就是調(diào)用的上述track方法。track方法很簡單,主要是找到對應(yīng)的依賴列表,如果沒有就創(chuàng)建一個依賴列表。

trackEffects里面先只需要關(guān)注收集依賴的邏輯,可以很明顯的看到,里面就是一個依賴的雙向添加。至于上面的那些邏輯,最主要的目的是防止重復添加依賴,我會在后面的優(yōu)化環(huán)節(jié)詳細講。

依賴模型存儲模型大致如下:

在這里插入圖片描述

(2).觸發(fā)依賴更新

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  //獲取依賴map
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    // never been tracked
    return
  }
  let deps: (Dep | undefined)[] = []
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    deps = [...depsMap.values()]
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        deps.push(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      deps.push(depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          deps.push(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          deps.push(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }
  const eventInfo = __DEV__
    ? { target, type, key, newValue, oldValue, oldTarget }
    : undefined
  if (deps.length === 1) {
    if (deps[0]) {
      if (__DEV__) {
        triggerEffects(deps[0], eventInfo)
      } else {
        triggerEffects(deps[0])
      }
    }
  } else {
    const effects: ReactiveEffect[] = []
    for (const dep of deps) {
      if (dep) {
        effects.push(...dep)
      }
    }
    if (__DEV__) {
      triggerEffects(createDep(effects), eventInfo)
    } else {
      triggerEffects(createDep(effects))
    }
  }
}
export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  for (const effect of isArray(dep) ? dep : [...dep]) {
    if (effect !== activeEffect || effect.allowRecurse) {
      if (__DEV__ && effect.onTrigger) {
        effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
      }
      if (effect.scheduler) {
        effect.scheduler()
      } else {
        //只需要關(guān)注這兒
        effect.run()
      }
    }
  }
}

trigger方法在響應(yīng)式對象的"寫"操作中調(diào)用,這個方法整體上只是根據(jù)不同的依賴更新類型,將依賴添加進一個依賴數(shù)組里面,最終通過triggerEffects方法更新這個依賴數(shù)組里面的依賴。

在triggerEffects方法里面,暫時只需要關(guān)注effect.run即可,此時調(diào)用的是ReactiveEffect關(guān)聯(lián)的那個回調(diào)方法,這時候也就正確的響應(yīng)式變化了。

至于effect.scheduler,我會在后續(xù)的計算屬性篇章中講到,這個方法是給計算屬性用的。

三、V3.2的響應(yīng)式優(yōu)化

上述篇幅只講述了一個整體的響應(yīng)式變化原理,接下來介紹一下V3.2帶來的響應(yīng)式性能優(yōu)化。我們先看一下Dep類型的定義

export type Dep = Set<ReactiveEffect> & TrackedMarkers
/**
 * wasTracked and newTracked maintain the status for several levels of effect
 * tracking recursion. One bit per level is used to define whether the dependency
 * was/is tracked.
 */
type TrackedMarkers = {
  /**
   * wasTracked
   */
  w: number
  /**
   * newTracked
   */
  n: number
}

可以看到,依賴列表不是一個簡簡單單的Set集合,它還存在2個用于輔助的屬性。我們創(chuàng)建依賴也是通過createDep方法,實現(xiàn)如下:

export const createDep = (effects?: ReactiveEffect[]): Dep => {
  const dep = new Set<ReactiveEffect>(effects) as Dep
  dep.w = 0
  dep.n = 0
  return dep
}

我在這兒先說明一下這2個屬性的作用。w屬性用于判斷依賴是否已經(jīng)被收集,n屬性用于判斷依賴在本次調(diào)用中是否用到。可能現(xiàn)在還對此有疑惑,我用以下一個簡單示例來解釋。

let status = true;
let dummy
const depA = reactive({ num: 0 })
const depB = reactive({ num: 10 })
let innerfunc = () => {
	dummy = depA.num
	if(status){
		dummy += depB.num
		status = false
	}
	console.log(dummy);
}
effect(innerfunc)
depA.num = 7
depB.num = 20
//輸出為 10 7

我們來分析以下上述代碼的流程,首先調(diào)用effect方法,會執(zhí)行一次關(guān)聯(lián)的innerfunc,此時讀取了depA和depB的num屬性,所以此時ReactiveEffect對象里面的deps屬性存在2個依賴,并且輸出10。當修改depA.num屬性時,會觸發(fā)run方法,此時關(guān)注以下代碼:

        if (effectTrackDepth <= maxMarkerBits) {
          //將當前依賴列表的所有依賴置為"已經(jīng)收集"
          initDepMarkers(this)
        } else {
          //不采用優(yōu)化模式,使用老流程,直接移除依賴的全部狀態(tài)
          cleanupEffect(this)
        }

因為調(diào)用effect方法時,收集過一次依賴,所以initDepMarkers方法將所有的依賴都標記為已經(jīng)收集。在run方法最后,會調(diào)用innerfunc方法。在innerfunc方法中,這一次調(diào)用中又會去收集依賴,此時關(guān)注trackEffects中的以下代碼:

  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      //本一輪調(diào)用新收集的依賴
      dep.n |= trackOpBit // set newly tracked
      //是否應(yīng)該被收集
      shouldTrack = !wasTracked(dep)
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

在run方法中,有一個同樣的判斷effectTrackDepth <= maxMarkerBits,這個判斷是用于控制是否優(yōu)化的,后面會講為什么會存在這個判斷以及為什么maxMarkerBits的取值為30

在這個收集邏輯中,會將本次回調(diào)中第一次使用到的依賴置為"新增依賴",我們在看innerfunc,此時只會使用到depA,不會使用到depB,因此之前存在的關(guān)于depB對象的依賴在本次調(diào)用中沒有用到。

shouldTrack屬性表示依賴是否應(yīng)該被收集,如果沒有收集,則被收集。此時innerfunc里面輸出的dummy為7。

接下來關(guān)注run里面的以下代碼:

  if (effectTrackDepth <= maxMarkerBits) {
    //斷掉依賴關(guān)聯(lián)
    finalizeDepMarkers(this)
  }
  //重置位操作狀態(tài)
  trackOpBit = 1 << --effectTrackDepth
  //重置依賴收集狀態(tài)
  resetTracking()
  //棧頂出棧
  effectStack.pop()
  const n = effectStack.length
  activeEffect = n > 0 ? effectStack[n - 1] : undefined

上述代碼存在于run方法里面的finally關(guān)鍵字內(nèi),當innerfunc執(zhí)行完后,里面就會執(zhí)行這里。首先便會根據(jù)判斷通過finalizeDepMarkers方法去斷掉依賴關(guān)聯(lián)。

我們看以下方法的實現(xiàn):

export const initDepMarkers = ({ deps }: ReactiveEffect) => {
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].w |= trackOpBit // set was tracked
    }
  }
}
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
  const { deps } = effect
  if (deps.length) {
    let ptr = 0
    for (let i = 0; i < deps.length; i++) {
      const dep = deps[i]
      if (wasTracked(dep) && !newTracked(dep)) {
        dep.delete(effect)
      } else {
        deps[ptr++] = dep
      }
      // clear bits
      dep.w &= ~trackOpBit
      dep.n &= ~trackOpBit
    }
    deps.length = ptr
  }
}

這2個方法巧妙的通過位運算將調(diào)用分層。一開始將存在的依賴打上收集標簽,如果在本層中沒有使用到,則斷掉依賴關(guān)聯(lián)。當設(shè)置depB.num = 20時,首先會找到依賴列表,由于依賴列表中已經(jīng)不存在ReactiveEffect對象了,所以不會觸發(fā)依賴更新,此時不會有新的輸出。

這兒是一個優(yōu)化,斷掉不必要的關(guān)聯(lián)依賴,減少方法的調(diào)用。但我們在寫類似代碼時必須非常小心,由于斷掉了依賴關(guān)聯(lián),有可能會因為寫法不規(guī)范導致響應(yīng)失效的情況。

接下來解釋為什么要使用位運算,以及保留不走位運算的邏輯。

關(guān)注以下代碼:

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
  }
}

當每次觸發(fā)依賴更新時,如果都調(diào)用以上方法,會涉及大量的集合刪減操作。

我沒看過Set集合的實現(xiàn),但無非就是數(shù)組或者鏈表。如果使用數(shù)組,增刪操作會導致數(shù)組擴容或者移位,頻繁操作會耗費大量性能,如果是鏈表,也要經(jīng)過一次查找,大量的調(diào)用是會消耗性能的。

那么為什么又要保留這個方法呢,這是因為js引擎在進行整數(shù)位運算時幾乎都是按32位運算的,1 << 31后為負值,得不到預期結(jié)果,因此保留原邏輯。但其實這個邏輯幾乎不可能調(diào)到,如果真調(diào)用到這個原始邏輯,我只能說得檢查一下代碼是否規(guī)范了。

四、后話

關(guān)于響應(yīng)式就介紹到這兒,個人理解,希望能給大家一個參考,也希望大家多多支持腳本之家。

相關(guān)文章

  • vue面包屑組件的封裝方法

    vue面包屑組件的封裝方法

    這篇文章主要為大家詳細介紹了vue面包屑組件的封裝方法,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2021-09-09
  • vue2.0使用Sortable.js實現(xiàn)的拖拽功能示例

    vue2.0使用Sortable.js實現(xiàn)的拖拽功能示例

    本篇文章主要介紹了vue2.0使用Sortable.js實現(xiàn)的拖拽功能示例,具有一定的參考價值,感興趣的小伙伴們可以參考一下。
    2017-02-02
  • vue2更改data里的變量不生效時,深層更改data里的變量問題

    vue2更改data里的變量不生效時,深層更改data里的變量問題

    這篇文章主要介紹了vue2更改data里的變量不生效時,深層更改data里的變量問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-03-03
  • JavaScript實現(xiàn)簡單的圖片切換功能(實例代碼)

    JavaScript實現(xiàn)簡單的圖片切換功能(實例代碼)

    這篇文章主要介紹了JavaScript實現(xiàn)簡單的圖片切換功能,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧
    2020-04-04
  • 如何在在Vue3中使用markdown 編輯器組件

    如何在在Vue3中使用markdown 編輯器組件

    vue3發(fā)布正式版不久,生態(tài)還沒完全發(fā)展起來,目前支持vue3的開源markdown編輯器組件基本上也寥寥無幾,向大家推薦一個很好用的v-md-editor 組件,組件功能很強大,文檔也比較詳細。該文章只介紹組件的常用功能,更多高級的功能可以參考官方文檔。
    2021-05-05
  • vue3封裝ECharts組件詳解

    vue3封裝ECharts組件詳解

    前端開發(fā)需要經(jīng)常使用ECharts圖表渲染數(shù)據(jù)信息,在一個項目中我們經(jīng)常需要使用多個圖表,選擇封裝ECharts組件復用的方式可以減少代碼量,增加開發(fā)效率。感興趣的可以閱讀一下本文
    2023-04-04
  • vue實現(xiàn)拖拽交換位置

    vue實現(xiàn)拖拽交換位置

    這篇文章主要為大家詳細介紹了vue實現(xiàn)拖拽交換位置,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-04-04
  • 淺析vue-router原理

    淺析vue-router原理

    這篇文章主要圍繞Vue的SPA單頁面設(shè)計展開。SPA(single page application):單一頁面應(yīng)用程序,有且只有一個完整的頁面,對vue router原理感興趣的朋友跟隨小編一起看看吧
    2018-10-10
  • Element-Ui組件 NavMenu 導航菜單的具體使用

    Element-Ui組件 NavMenu 導航菜單的具體使用

    這篇文章主要介紹了Element-Ui組件 NavMenu 導航菜單的具體使用,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧
    2019-10-10
  • Vue項目中使用addRoutes出現(xiàn)問題的解決方法

    Vue項目中使用addRoutes出現(xiàn)問題的解決方法

    大家應(yīng)該都知道可以通過vue-router官方提供的一個api-->addRoutes可以實現(xiàn)路由添加的功能,事實上就也就實現(xiàn)了用戶權(quán)限,這篇文章主要給大家介紹了關(guān)于Vue項目中使用addRoutes出現(xiàn)問題的解決方法,需要的朋友可以參考下
    2021-08-08

最新評論