目錄
一、effect用法
1、基本用法
2、lazy屬性為true
3、options中包含onTrack
二、原始碼分析
1、effect方法的實作
1、trigger依赖更新
2、triggerEffects(deps[0], eventInfo)
3、triggerEffect(effect, debuggerEventExtraInfo)
首頁 web前端 Vue.js Vue3響應式核心之effect怎麼使用

Vue3響應式核心之effect怎麼使用

May 10, 2023 am 11:19 AM
vue3 effect

通常我們是不會直接使用effect的,因為effect是一個底層的API,在我們使用Vue3的時候Vue預設會幫我們呼叫effect。 effect翻譯為作用,意思是使其發生作用,而這個使其的其就是我們傳入的函數,所以effect的作用就是讓我們傳入的函數發生作用,也就是執行這個函數。執行過程簡圖如下:

Vue3響應式核心之effect怎麼使用

接下來先透過範例了解effect的基本用法,然後再去了解原理。

一、effect用法

1、基本用法

const obj = reactive({count: 1})

const runner = effect(() => {
  console.log(obj.count)
})

obj.count++
登入後複製

結果會先列印1, 然後在obj.count 之後列印2。

流程簡圖如下:

Vue3響應式核心之effect怎麼使用

執行#effect(fun)

// 先执行
fun()  // 打印出1

const runner = new ReactiveEffect(fn)

return runner

runner: {
  run() {
    this.fun() //执行fun
  },
  stop() {

  }
}
登入後複製

console.log (obj.count)track依賴收集結構如下:

Vue3響應式核心之effect怎麼使用

#obj.count 觸發依賴,執行runner.run(), 實際運行的是

() => {
  console.log(obj.count)
}
登入後複製

所以又印出2

2、lazy屬性為true

此值為true 時,只有在第一次手動呼叫runner 後,依賴數據變更時,才會自動執行effect 的回調,可以理解為effect 的是手動調用runner 後才首次執行

const obj = reactive({count: 1})

const runner = effect(() => {
  console.log(obj.count)
}, {
  lazy: true
})
runner()
obj.count++
登入後複製

只會打印出2

原因是effect源碼中有如下邏輯:

Vue3響應式核心之effect怎麼使用

3、options中包含onTrack

let events = []
const onTrack = (e) => {
  events.push(e)
}
const obj = reactive({ foo: 1, bar: 2 })
const runner = effect(
  () => {
    console.log(obj.foo)
  },
  { onTrack }
)
console.log('runner', runner)
obj.foo++
console.log("events", events)
登入後複製

看下events的列印結果:

Vue3響應式核心之effect怎麼使用

[
  {
    effect: runner,  // effect 函数的返回值
    target: toRaw(obj),  // 表示的是哪个响应式数据发生了变化
    type: TrackOpTypes.GET,  // 表示此次记录操作的类型。 get 表示获取值
    key: 'foo'
 }
]
登入後複製

二、原始碼分析

1、effect方法的實作

// packages/reactivity/src/effect.ts
export interface ReactiveEffectOptions extends DebuggerOptions {
  lazy?: boolean
  scheduler?: EffectScheduler
  scope?: EffectScope
  allowRecurse?: boolean
  onStop?: () => void
}

export function effect<T = any>(
  fn: () => T, // 副作用函数
  options?: ReactiveEffectOptions // 结构如上
): ReactiveEffectRunner {
  // 如果 fn 对象上有 effect 属性
  if ((fn as ReactiveEffectRunner).effect) {
    // 那么就将 fn 替换为 fn.effect.fn
    fn = (fn as ReactiveEffectRunner).effect.fn
  }
  // 创建一个响应式副作用函数
  const _effect = new ReactiveEffect(fn)
  if (options) {
    // 将配置项合并到响应式副作用函数上
    extend(_effect, options)
    // 如果配置项中有 scope 属性(该属性的作用是指定副作用函数的作用域)
    if (options.scope) recordEffectScope(_effect, options.scope)
  }
  if (!options || !options.lazy) { // options.lazy 不为true
    _effect.run() // 执行响应式副作用函数 首次执行fn()
  }
  // _effect.run作用域绑定到_effect
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  // 将响应式副作用函数赋值给 runner.effect
  runner.effect = _effect
  return runner
}
登入後複製

核心程式碼:

建立一個響應式副作用函數const _effect = new ReactiveEffect( fn),其運行結果如下:

Vue3響應式核心之effect怎麼使用

非lazy狀態執行響應式副作用函數_effect.run()

if (!options || !options.lazy) { // options.lazy 不为true
  _effect.run() // 执行响应式副作用函数 首次执行fn()
}
登入後複製

_effect.run作用域綁定到_effect

// _effect.run作用域绑定到_effect
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
登入後複製

傳回副作用函數runner

##2、ReactiveEffect函數原始碼

export class ReactiveEffect<T = any> {
  active = true
  deps: Dep[] = [] // 响应式依赖项的集合
  parent: ReactiveEffect | undefined = undefined

  /**
   * Can be attached after creation
   * @internal
   */
  computed?: ComputedRefImpl<T>
  /**
   * @internal
   */
  allowRecurse?: boolean
  /**
   * @internal
   */
  private deferStop?: 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
  ) {
    // 记录当前 ReactiveEffect 对象的作用域
    recordEffectScope(this, scope)
  }

  run() {
    // 如果当前 ReactiveEffect 对象不处于活动状态,直接返回 fn 的执行结果
    if (!this.active) {
      return this.fn()
    }
    // 寻找当前 ReactiveEffect 对象的最顶层的父级作用域
    let parent: ReactiveEffect | undefined = activeEffect
    let lastShouldTrack = shouldTrack // 是否要跟踪
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    try {
      // 记录父级作用域为当前活动的 ReactiveEffect 对象
      this.parent = activeEffect
      activeEffect = this  // 将当前活动的 ReactiveEffect 对象设置为 “自己”
      shouldTrack = true // 将 shouldTrack 设置为 true (表示是否需要收集依赖)
      // effectTrackDepth 用于标识当前的 effect 调用栈的深度,执行一次 effect 就会将 effectTrackDepth 加 1
      trackOpBit = 1 << ++effectTrackDepth

      if (effectTrackDepth <= maxMarkerBits) {
        // 初始依赖追踪标记
        initDepMarkers(this)
      } else {
        // 清除依赖追踪标记
        cleanupEffect(this)
      }
      // 返回副作用函数执行结果
      return this.fn()
    } finally {
      // 如果 effect调用栈的深度 没有超过阈值
      if (effectTrackDepth <= maxMarkerBits) {
        // 确定最终的依赖追踪标记
        finalizeDepMarkers(this)
      }
      // 执行完毕会将 effectTrackDepth 减 1
      trackOpBit = 1 << --effectTrackDepth
      // 执行完毕,将当前活动的 ReactiveEffect 对象设置为 “父级作用域”
      activeEffect = this.parent
      // 将 shouldTrack 设置为上一个值
      shouldTrack = lastShouldTrack
      // 将父级作用域设置为 undefined
      this.parent = undefined
      // 延时停止,这个标志是在 stop 方法中设置的
      if (this.deferStop) {
        this.stop()
      }
    }
  }

  stop() {
    // stopped while running itself - defer the cleanup
    // 如果当前 活动的 ReactiveEffect 对象是 “自己”
    // 延迟停止,需要执行完当前的副作用函数之后再停止
    if (activeEffect === this) {
      // 在 run 方法中会判断 deferStop 的值,如果为 true,就会执行 stop 方法
      this.deferStop = true
    } else if (this.active) {// 如果当前 ReactiveEffect 对象处于活动状态
      cleanupEffect(this) // 清除所有的依赖追踪标记
      if (this.onStop) { 
        this.onStop()
      }
      this.active = false // 将 active 设置为 false
    }
  }
}
登入後複製

  • run方法的作用就是執行副作用函數,在執行副作用函數的過程中,會收集依賴;

  • stop方法的作用就是停止目前的ReactiveEffect對象,停止之後,就不會再收集依賴了;

  • activeEffect和this並不是每次都相等的,因為activeEffect會跟著呼叫棧的深度而變化,而this則是固定的;

三、依賴收集相關

1、如何觸發依賴收集

在副作用函數中,

obj. count就會觸發依賴收集

const runner = effect(() => {
  console.log(obj.count) 
})
登入後複製

觸發的入口在get攔截器裡面

function createGetter(isReadonly = false, shallow = false) {
  // 闭包返回 get 拦截器方法
  return function get(target: Target, key: string | symbol, receiver: object) {
    // ...
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key)
    }
    // ...
  }
登入後複製

2、track原始碼

const targetMap = new WeakMap();
/**
 * 收集依赖
 * @param target target 触发依赖的对象,例子中的obj
 * @param type 操作类型 比如obj.count就是get
 * @param key 指向对象的key, 比如obj.count就是count
 */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (shouldTrack && activeEffect) { // 是否应该依赖收集 & 当前的new ReactiveEffect()即指向的就是当前正在执行的副作用函数

    // 如果 targetMap 中没有 target,就会创建一个 Map
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }
    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, (dep = createDep())) // createDep 生成dep = { w:0, n: 0}
    }

    const eventInfo = __DEV__
      ? { effect: activeEffect, target, type, key }
      : undefined

    trackEffects(dep, eventInfo)
  }
}
登入後複製

shouldTrack在上面也講過,它的作用是控制是否收集依賴;

activeEffect就是我們剛剛講的ReactiveEffect對象,它指向的就是當前正在執行的副作用函數;

track方法的作用就是收集依賴,它的實作非常簡單,就是在targetMap中記錄下target和key;

targetMap是一個WeakMap,它的鍵是target,值是一個Map,這個Map的鍵是key,值是一個Set;

targetMap的結構偽代碼如下:

targetMap = {
  target: { 
    key: dep
  },
  // 比如:
  obj: { 
    count: {
       w: 0, 
       n: 0
    }
  }
}
登入後複製

Vue3響應式核心之effect怎麼使用

以上是最原始的depMap

##dev環境為增加響應式偵錯會增加

eventInfo

<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class='brush:php;toolbar:false;'>const eventInfo = __DEV__ ? { effect: activeEffect, target, type, key } : undefined</pre><div class="contentsignin">登入後複製</div></div>eventInfo結構如下:

Vue3響應式核心之effect怎麼使用

#trackEffects(dep, eventInfo)

#如果dep 中沒有當前的ReactiveEffect 對象,就會加進去, 作用就把對象的屬性操作與副作用函數建立關聯,接下來看

trackEffects

3、trackEffects(dep, eventInfo)原始碼解讀

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  let shouldTrack = false
  if (effectTrackDepth <= maxMarkerBits) {
    if (!newTracked(dep)) {
      // 执行之前 dep = Set(0) {w: 0, n: 0}

      // 执行之后 dep = Set(0) {w: 0, n: 2}
      dep.n |= trackOpBit // set newly tracked
  
      shouldTrack = !wasTracked(dep) 
    }
  } else {
    // Full cleanup mode.
    shouldTrack = !dep.has(activeEffect!)
  }

  if (shouldTrack) {
    // 将activeEffect添加到dep
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
    if (__DEV__ && activeEffect!.onTrack) { // onTrack逻辑
      activeEffect!.onTrack(
        extend(
          {
            effect: activeEffect!
          },
          debuggerEventExtraInfo!
        )
      )
    }
  }
}
登入後複製

dep.add(activeEffect!)

如果dep 中沒有目前的ReactiveEffect 對象,就會加進去

Vue3響應式核心之effect怎麼使用

Vue3響應式核心之effect怎麼使用

最終產生的depTarget結構如下:###############四、觸發依賴######例如範例中程式碼###obj.count ###就會觸發set攔截,觸發依賴更新###
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    //...
    const result = Reflect.set(target, key, value, receiver)
    // don&#39;t trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value) // 触发ADD依赖更新
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue) //触发SET依赖更新
      }
    }
    //...
  }
登入後複製

1、trigger依赖更新

// 路径:packages/reactivity/src/effect.ts
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
  const depsMap = targetMap.get(target) // 获取depsMap, targetMap是在track中创建的依赖
  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 === &#39;length&#39; && isArray(target)) {
    const newLength = Number(newValue)
    depsMap.forEach((dep, key) => {
      if (key === &#39;length&#39; || key >= newLength) {
        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(&#39;length&#39;))
        }
        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))
    }
  }
}
登入後複製

const depsMap = targetMap.get(target) 获取 targetMap 中的 depsMap targetMap结构如下:

Vue3響應式核心之effect怎麼使用

执行以上语句之后的depsMap结构如下:

Vue3響應式核心之effect怎麼使用

将 depsMap 中 key 对应的 ReactiveEffect 对象添加到 deps 中deps.push(depsMap.get(key))之后的deps结构如下:

Vue3響應式核心之effect怎麼使用

triggerEffects(deps[0], eventInfo)

  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])
      }
    }
  }
登入後複製

trigger函数的作用就是触发依赖,当我们修改数据的时候,就会触发依赖,然后执行依赖中的副作用函数。

在这里的实现其实并没有执行,主要是收集一些需要执行的副作用函数,然后在丢给triggerEffects函数去执行,接下来看看triggerEffects函数。

2、triggerEffects(deps[0], eventInfo)

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}
登入後複製

主要步骤

const effects = isArray(dep) ? dep : [...dep]获取effects

Vue3響應式核心之effect怎麼使用

triggerEffect(effect, debuggerEventExtraInfo)执行effect,接下来看看源码

3、triggerEffect(effect, debuggerEventExtraInfo)

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
     // 如果 effect.onTrigger 存在,就会执行,只有开发模式下才会执行
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    // 如果 effect 是一个调度器,就会执行 scheduler
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      // 其它情况执行 effect.run()
      effect.run()
    }
  }
}
登入後複製

effect.run()就是执行副作用函数

以上是Vue3響應式核心之effect怎麼使用的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

vue3專案中怎麼使用tinymce vue3專案中怎麼使用tinymce May 19, 2023 pm 08:40 PM

tinymce是一個功能齊全的富文本編輯器插件,但在vue中引入tinymce並不像別的Vue富文本插件一樣那麼順利,tinymce本身並不適配Vue,還需要引入@tinymce/tinymce-vue,並且它是國外的富文本插件,沒有透過中文版本,需要在其官網下載翻譯包(可能需要翻牆)。 1.安裝相關依賴npminstalltinymce-Snpminstall@tinymce/tinymce-vue-S2、下載中文包3.引入皮膚和漢化包在項目public資料夾下新建tinymce資料夾,將下載的

vue3+vite:src使用require動態匯入圖片報錯怎麼解決 vue3+vite:src使用require動態匯入圖片報錯怎麼解決 May 21, 2023 pm 03:16 PM

vue3+vite:src使用require動態導入圖片報錯和解決方法vue3+vite動態的導入多張圖片vue3如果使用的是typescript開發,就會出現require引入圖片報錯,requireisnotdefined不能像使用vue2這樣imgUrl:require(' …/assets/test.png')導入,是因為typescript不支援require所以用import導入,下面介紹如何解決:使用awaitimport

Vue3如何實作刷新頁面局部內容 Vue3如何實作刷新頁面局部內容 May 26, 2023 pm 05:31 PM

想要實現頁面的局部刷新,我們只需要實現局部元件(dom)的重新渲染。在Vue中,想要實現這效果最簡單的方式方法就是使用v-if指令。在Vue2中我們除了使用v-if指令讓局部dom的重新渲染,也可以新建一個空白元件,需要刷新局部頁面時跳轉至這個空白元件頁面,然後在空白元件內的beforeRouteEnter守衛中又跳轉回原來的頁面。如下圖所示,如何在Vue3.X中實現點擊刷新按鈕實現紅框範圍內的dom重新加載,並展示對應的加載狀態。由於Vue3.X中scriptsetup語法中組件內守衛只有o

Vue3怎麼解析markdown並實現程式碼高亮顯示 Vue3怎麼解析markdown並實現程式碼高亮顯示 May 20, 2023 pm 04:16 PM

Vue實作部落格前端,需要實作markdown的解析,如果有程式碼則需要實作程式碼的高亮。 Vue的markdown解析函式庫很多,如markdown-it、vue-markdown-loader、marked、vue-markdown等。這些庫都大同小異。這裡選用的是marked,程式碼高亮的函式庫選用的是highlight.js。具體實現步驟如下:一、安裝依賴庫在vue專案下開啟命令窗口,並輸入以下命令npminstallmarked-save//marked用於將markdown轉換成htmlnpmins

怎麼使用vue3+ts+axios+pinia實現無感刷新 怎麼使用vue3+ts+axios+pinia實現無感刷新 May 25, 2023 pm 03:37 PM

vue3+ts+axios+pinia實作無感刷新1.先在專案中下載aiXos和pinianpmipinia--savenpminstallaxios--save2.封裝axios請求-----下載js-cookienpmiJS-cookie-s//引入aixosimporttype{AxiosRequestConfigig ,AxiosResponse}from"axios";importaxiosfrom'axios';import{ElMess

Vue3復用元件怎麼使用 Vue3復用元件怎麼使用 May 20, 2023 pm 07:25 PM

前言無論是vue還是react,當我們遇到多處重複程式碼的時候,我們都會想著如何重複使用這些程式碼,而不是一個檔案裡充斥著一堆冗餘程式碼。實際上,vue和react都可以透過抽組件的方式來達到復用,但如果遇到一些很小的程式碼片段,你又不想抽到另外一個檔案的情況下,相比而言,react可以在相同文件裡面宣告對應的小元件,或透過renderfunction來實現,如:constDemo:FC=({msg})=>{returndemomsgis{msg}}constApp:FC=()=>{return(

Vue3中怎麼實現選取頭像並裁剪 Vue3中怎麼實現選取頭像並裁剪 May 29, 2023 am 10:22 AM

最終效果安裝VueCropper組件yarnaddvue-cropper@next上面的安裝值針對Vue3的,如果時Vue2或想使用其他的方式引用,請訪問它的npm官方地址:官方教程。在元件中引用使用時也很簡單,只需要引入對應的元件和它的樣式文件,我這裡沒有在全域引用,只在我的元件檔案中引入import{userInfoByRequest}from'../js/api' import{VueCropper}from'vue-cropper&

Vue3中如何使用defineCustomElement定義元件 Vue3中如何使用defineCustomElement定義元件 May 28, 2023 am 11:29 AM

使用Vue建構自訂元素WebComponents是一組web原生API的統稱,允許開發者建立可重複使用的自訂元素(customelements)。自訂元素的主要好處是,它們可以在使用任何框架,甚至在不使用框架的場景下使用。當你面向的最終用戶可能使用了不同的前端技術棧,或者當你希望將最終的應用與它使用的組件實現細節解耦時,它們會是理想的選擇。 Vue和WebComponents是互補的技術,Vue為使用和創建自訂元素提供了出色的支援。你可以將自訂元素整合到現有的Vue應用中,或使用Vue來構

See all articles