[建議收藏] 你想知道的Vue3核心源碼這裏都有

寫做不易,未經做者容許禁止以任何形式轉載!
若是以爲文章不錯,歡迎關注、點贊和分享!
持續分享技術博文,關注微信公衆號 👉🏻 前端LeBronhtml

Effect和Reactive

effect做爲Vue響應式原理中的核心,在Computed、Watch、Reactive中都有出現前端

主要和Reactive(Proxy)、track、trigger等函數配合實現收集依賴,觸發依賴更新vue

  • Effect
    • 反作用依賴函數
  • Track
    • 依賴收集
  • Trigger
    • 依賴觸發

Effect

effect能夠被理解爲一個反作用函數,被當作依賴收集,在響應式數據更新後被觸發。node

Vue的響應式API例如Computed、Watch都有用到effect來實現react

  • 先來看看入口函數
    • 入口函數主要是一些邏輯處理,核心邏輯位於createReactiveEffect
function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> {
  // 若是已是effect,則重置
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // 建立effect
  const effect = createReactiveEffect(fn, options)
  // 若是不是惰性執行,先執行一次
  if (!options.lazy) {
    effect()
  }
  return effect
}
複製代碼
  • createReactiveEffect
const effectStack: ReactiveEffect[] = []

function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect<T> {
  const effect = function reactiveEffect(): unknown {
    // 沒有激活,說明調用了effect stop函數
    if (!effect.active) {
      // 無調度者則直接返回,不然執行fn
      return options.scheduler ? undefined : fn()
    }
    // 判斷EffectStack中有沒有effect,有則不處理
    if (!effectStack.includes(effect)) {
      // 清除effect
      cleanup(effect)
      try {
        /* * 開始從新收集依賴 * 壓入stack * 將effect設置爲activeEffect * */
        enableTracking()
        effectStack.push(effect)
        activeEffect = effect
        return fn()
      } finally {
        /* * 完成後將effect彈出 * 重置依賴 * 重置activeEffect * */
        effectStack.pop()
        resetTracking()
        activeEffect = effectStack[effectStack.length - 1]
      }
    }
  } as ReactiveEffect
  effect.id = uid++ // 自增id,effect惟一標識
  effect.allowRecurse = !!options.allowRecurse
  effect._isEffect = true // 是不是effect
  effect.active = true  // 是否激活
  effect.raw = fn   // 掛載原始對象
  effect.deps = []  // 當前effect的dep數組
  effect.options = options  // 傳入的options
  return effect
}

// 每次effect運行都會從新收集依賴,deps是effect的依賴數組,須要所有清空
function cleanup(effect: ReactiveEffect) {
  const { deps } = effect
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      deps[i].delete(effect)
    }
    deps.length = 0
  }
}
複製代碼

Track

Track這個函數常出如今reactive的getter函數中,用於依賴收集git

源碼詳解見註釋github

function track(target: object, type: TrackOpTypes, key: unknown) {
  // activeEffect爲空表示沒有依賴
  if (!shouldTrack || activeEffect === undefined) {
    return
  }

  // targetMap依賴管理Map,用於收集依賴
  // 檢查targetMap中有沒有target,沒有則新建
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }

  // dep用來收集依賴函數,當監聽的key值發生變化,觸發dep中的依賴函數更新
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
    activeEffect.deps.push(dep)
    // 開發環境會觸發onTrack,僅用於調試
    if (__DEV__ && activeEffect.options.onTrack) {
      activeEffect.options.onTrack({
        effect: activeEffect,
        target,
        type,
        key
      })
    }
  }
}
複製代碼

Trigger

Trigger常出如今reactive中的setter函數中,用於觸發依賴更新web

源碼詳解見註釋算法

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
  }

  // 使用Set保存須要觸發的effect,避免重複
  const effects = new Set<ReactiveEffect>()
  // 定義依賴添加函數
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || effect.allowRecurse) {
          effects.add(effect)
        }
      })
    }
  }

  // 將depsMap中的依賴添加到effects中
  // 只爲了理解和原理的話 各個分支不用細看
  if (type === TriggerOpTypes.CLEAR) {
    // collection being cleared
    // trigger all effects for target
    depsMap.forEach(add)
  } else if (key === 'length' && isArray(target)) {
    depsMap.forEach((dep, key) => {
      if (key === 'length' || key >= (newValue as number)) {
        add(dep)
      }
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      add(depsMap.get(key))
    }

    // also run for iteration key on ADD | DELETE | Map.SET
    switch (type) {
      case TriggerOpTypes.ADD:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        } else if (isIntegerKey(key)) {
          // new index added to array -> length changes
          add(depsMap.get('length'))
        }
        break
      case TriggerOpTypes.DELETE:
        if (!isArray(target)) {
          add(depsMap.get(ITERATE_KEY))
          if (isMap(target)) {
            add(depsMap.get(MAP_KEY_ITERATE_KEY))
          }
        }
        break
      case TriggerOpTypes.SET:
        if (isMap(target)) {
          add(depsMap.get(ITERATE_KEY))
        }
        break
    }
  }

  // 封裝effects執行函數
  const run = (effect: ReactiveEffect) => {
    if (__DEV__ && effect.options.onTrigger) {
      effect.options.onTrigger({
        effect,
        target,
        key,
        type,
        newValue,
        oldValue,
        oldTarget
      })
    }
    // 若是存在scheduler則調用
    if (effect.options.scheduler) {
      effect.options.scheduler(effect)
    } else {
      effect()
    }
  }

  // 觸發effects中的全部依賴函數
  effects.forEach(run)
}
複製代碼

Reactive

瞭解了Track用於依賴收集,Trigger用於依賴觸發,那麼他們的調用時機是何時呢?來看看Reactive的源碼就清楚了,源碼詳解見註釋。vuex

注:源碼結構較爲複雜(封裝),爲便於理解原理,如下爲簡化源碼。

  • 總結來講
    • 在getter時進行依賴收集
    • 在setter時觸發依賴更新
function reactive(target:object){
    return new Proxy(target,{
        get(target: Target, key: string | symbol, receiver: object){
            const res = Reflect.get(target, key, receiver)
            track(target, TrackOpTypes.GET, key)
            return res
        }
        set(target: object, key: string | symbol, value: unknown, receiver: object){
            let oldValue = (target as any)[key]
            const result = Reflect.set(target, key, value, receiver)
            // trigger(target, TriggerOpTypes.ADD, key, value)
            trigger(target, TriggerOpTypes.SET, key, value, oldValue)
            return result
        }
    })
}
複製代碼

Computed

Computed是Vue中經常使用且好用的一個屬性,這個屬性的值在依賴改變後同步進行改變,在依賴未改變時使用緩存的值。

  • Vue2
    • 在Vue2中Computed的實現經過嵌套watcher,實現響應式數據的依賴收集,間接鏈式觸發依賴更新。
  • Vue3中出現了effect,從新實現了Computed屬性
    • effect能夠被理解爲反作用函數,被當作依賴收集,在響應式數據更新後被觸發。

Show me the Code

  • 讀完這段computed函數會發現,這裏只是作了簡要的getter和setter的賦值處理
    • computed支持兩種寫法
      • 函數
      • getter、setter
function computed<T>( getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T> ) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  if (isFunction(getterOrOptions)) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  return new ComputedRefImpl(
    getter,
    setter,
    isFunction(getterOrOptions) || !getterOrOptions.set
  ) as any
}
複製代碼
  • 核心邏輯都在ComputedRefImpl中,咱們接着往下看
    • 經過dirty變量標記數據是否爲舊數據
    • 在響應式數據更新後將dirty賦值爲true
    • 在下一次get時,dirty爲true時進行從新計算,並將dirty賦值爲false
class ComputedRefImpl<T> {
  private _value!: T
  private _dirty = true

  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true;
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  constructor( getter: ComputedGetter<T>, private readonly _setter: ComputedSetter<T>, isReadonly: boolean ) {
    this.effect = effect(getter, {
      lazy: true,
      // 響應式數據更新後將dirty賦值爲true
      // 下次執行getter判斷dirty爲true即從新計算computed值
      scheduler: () => {
        if (!this._dirty) {
          this._dirty = true
          // 派發全部引用當前計算屬性的反作用函數effect 
          trigger(toRaw(this), TriggerOpTypes.SET, 'value')
        }
      }
    })

    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    // 當響應式數據更新後dirty爲true
    // 從新計算數據後,將dirty賦值爲false
    if (self._dirty) {
      self._value = this.effect()
      self._dirty = false
    }
    // 依賴收集
    track(self, TrackOpTypes.GET, 'value')
    
    // 返回計算後的值
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}
複製代碼

Watch

Watch主要用於對某個變量的監聽,並作相應的處理

Vue3中不只重構了watch,還多了一個WatchEffect API

  • Watch

用於對某個變量的監聽,同時能夠經過callBack拿到新值和舊值

watch(state, (state, prevState)=>{})
複製代碼
  • WatchEffect

每次更新都會執行,自動收集使用到的依賴

沒法獲取到新值和舊值,可手動中止監聽

onInvalidate(fn)傳入的回調會在 watchEffect 從新運行或者 watchEffect 中止的時候執行

const stop = watchEffect((onInvalidate)=>{
    // ...
    onInvalidate(()=>{
        // ...
    })
})    
// 手動中止監聽
stop()
複製代碼

watch和watchEffect的不一樣點

  • watch惰性執行,watchEffect每次代碼加載都會執行
  • watch可指定監聽變量,watchEffect自動依賴收集
  • watch可獲取新舊值,watchEffect不行
  • watchEffect有onInvalidate功能,watch沒有
  • watch只可監聽ref、reactive等對象,watchEffect只可監聽具體屬性

Source Code

Show me the Code

  • 這裏能夠看到watch和watchEffet的核心邏輯都封裝到了doWatch中
// watch
export function watch<T = any, Immediate extends Readonly<boolean> = false>( source: T | WatchSource<T>, cb: any, options?: WatchOptions<Immediate> ): WatchStopHandle {
  if (__DEV__ && !isFunction(cb)) {
    warn(
      `\`watch(fn, options?)\` signature has been moved to a separate API. ` +
        `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
        `supports \`watch(source, cb, options?) signature.`
    )
  }
  return doWatch(source as any, cb, options)
}

export function watchEffect( effect: WatchEffect, options?: WatchOptionsBase ): WatchStopHandle {
  return doWatch(effect, null, options)
}
複製代碼
  • doWatch

如下爲刪減版源碼,理解核心原理便可

詳情見註釋

function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ, instance = currentInstance ): WatchStopHandle {

  let getter: () => any
  let forceTrigger = false
  let isMultiSource = false

  // 對不一樣的狀況作getter賦值
  if (isRef(source)) {
    // ref經過.value獲取
    getter = () => (source as Ref).value
    forceTrigger = !!(source as Ref)._shallow
  } else if (isReactive(source)) {
    // reactive直接獲取
    getter = () => source
    deep = true
  } else if (isArray(source)) {
    // 若是是數組,作遍歷處理
    isMultiSource = true
    forceTrigger = source.some(isReactive)
    getter = () =>
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return traverse(s)
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER, [
            instance && (instance.proxy as any)
          ])
        } else {
          __DEV__ && warnInvalidSource(s)
        }
      })
  } else if (isFunction(source)) {
    // 若是是函數的狀況
    // 有cb則爲watch,沒有則爲watchEffect
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER, [
          instance && (instance.proxy as any)
        ])
    } else {
      // no cb -> simple effect
      getter = () => {
        if (instance && instance.isUnmounted) {
          return
        }
        if (cleanup) {
          cleanup()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onInvalidate]
        )
      }
    }
  } else {
    // 異常狀況
    getter = NOOP
    // 拋出異常
    __DEV__ && warnInvalidSource(source)
  }

  // 深度監聽邏輯處理
  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }
  
  let cleanup: () => void
  let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
    cleanup = runner.options.onStop = () => {
      callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
    }
  }

  // 記錄oldValue,並經過runner獲取newValue
  // callback的封裝處理爲job
  let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
  const job: SchedulerJob = () => {
    if (!runner.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      const newValue = runner()
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) =>
              hasChanged(v, (oldValue as any[])[i])
            )
          : hasChanged(newValue, oldValue)) ||
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
        // cleanup before running cb again
        if (cleanup) {
          cleanup()
        }
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
          onInvalidate
        ])
        oldValue = newValue
      }
    } else {
      // watchEffect
      runner()
    }
  }

  // important: mark the job as a watcher callback so that scheduler knows
  // it is allowed to self-trigger (#1727)
  job.allowRecurse = !!cb


  // 經過讀取配置,處理job的觸發時機
  // 並再次將job的執行封裝到scheduler中
  let scheduler: ReactiveEffectOptions['scheduler']
  if (flush === 'sync') { // 同步執行
    scheduler = job
  } else if (flush === 'post') { // 更新後執行
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    // 更新前執行
    scheduler = () => {
      if (!instance || instance.isMounted) {
        queuePreFlushCb(job)
      } else {
        // with 'pre' option, the first call must happen before
        // the component is mounted so it is called synchronously.
        job()
      }
    }
  }

  // 使用effect反作用處理依賴收集,在依賴更新後調用scheduler(其中封裝了callback的執行)
  const runner = effect(getter, {
    lazy: true,
    onTrack,
    onTrigger,
    scheduler
  })

  // 收集依賴
  recordInstanceBoundEffect(runner, instance)

  // 讀取配置,進行watch初始化
  // 是否有cb
  if (cb) {
    // 是否馬上執行
    if (immediate) {
      job()
    } else {
      oldValue = runner()
    }
  } else if (flush === 'post') {
    // 是否更新後執行
    queuePostRenderEffect(runner, instance && instance.suspense)
  } else {
    runner()
  }

  // 返回手動中止函數
  return () => {
    stop(runner)
    if (instance) {
      remove(instance.effects!, runner)
    }
  }
}
複製代碼

Mixin

Mixin意爲混合,是公共邏輯封裝利器。

原理比較簡單,那就是合併。

  • 合併分爲對象的合併和生命週期的合併
    • 對象,mergeOption
      • 類型Object.assign的合併,會出現覆蓋現象
    • 生命週期,mergeHook
      • 合併會將兩個生命週期放入一個隊列,依次調用
  • mergeOptions
function mergeOptions( to: any, from: any, instance?: ComponentInternalInstance | null, strats = instance && instance.appContext.config.optionMergeStrategies ) {
  if (__COMPAT__ && isFunction(from)) {
    from = from.options
  }

  const { mixins, extends: extendsOptions } = from

  extendsOptions && mergeOptions(to, extendsOptions, instance, strats)
  mixins &&
    mixins.forEach((m: ComponentOptionsMixin) =>
      mergeOptions(to, m, instance, strats)
    )
    
   // 對mixin中的對象進行遍歷
  for (const key in from) {
    // 若是存在則進行覆蓋處理
    if (strats && hasOwn(strats, key)) {
      to[key] = strats[key](to[key], from[key], instance && instance.proxy, key)
    } else {
    // 若是不存在則直接賦值
      to[key] = from[key]
    }
  }
  return to
}
複製代碼
  • mergeHook

簡單粗暴放進Set,調用時依次調用

function mergeHook( to: Function[] | Function | undefined, from: Function | Function[] ) {
  return Array.from(new Set([...toArray(to), ...toArray(from)]))
}
複製代碼

Vuex4

Vuex是在Vue中經常使用的狀態管理庫,在Vue3發佈後,這個狀態管理庫也隨之發出了適配Vue3的Vuex4

快速過Vuex3.x原理

  • 爲何每一個組件均可以經過

    this.$store訪問到store數據?

    • 在beforeCreate時,經過mixin的方式注入了store
  • 爲何Vuex中的數據都是響應式的

    • 建立store的時候調用的是new Vue,建立了一個Vue實例,至關於借用了Vue的響應式。
  • mapXxxx是怎麼獲取到store中的數據和方法的

    • mapXxxx只是一個語法糖,底層實現也是從$store中獲取而後返回到computed / methods中。

總的來看,能夠把Vue3.x理解爲一個每一個組件都注入了的mixin?

Vuex4原理探究

去除冗餘代碼看本質

createStore

  • 從createStore開始看起
    • 能夠發現Vuex4中的state是經過reactive API去建立的響應式數據,Vuex3中是經過new Vue實例
    • dispatch、commit的實現基本是封裝了一層執行,不用過於關心
export function createStore (options) {
    return new Store(options)
}
class Store{
    constructor (options = {}){
        // 省略若干代碼...
        this._modules = new ModuleCollection(options)
        const state = this._modules.root.state
        resetStoreState(this, state)
        
        // bind commit and dispatch to self
        const store = this
        const { dispatch, commit } = this
        this.dispatch = function boundDispatch (type, payload) {
          return dispatch.call(store, type, payload)
        }    
        this.commit = function boundCommit (type, payload, options) {
          return commit.call(store, type, payload, options)
        }
        // 省略若干代碼...
    }
}
function resetStoreState (store, state, hot) {
    // 省略若干代碼...
    store._state = reactive({
        data: state
    })
    // 省略若干代碼...
}
複製代碼

install

  • Vuex是以插件的形式在Vue中使用的,在createApp時調用install安裝
export function createAppAPI<HostElement>( render: RootRenderFunction, hydrate?: RootHydrateFunction ): CreateAppFunction<HostElement> {
  return function createApp(rootComponent, rootProps = null) {
  
    // 省略部分代碼....
    const app: App = (context.app = {
      _uid: uid++,
      _component: rootComponent as ConcreteComponent,
      _props: rootProps,
      _container: null,
      _context: context,

      version,
      
      // 省略部分代碼....

      use(plugin: Plugin, ...options: any[]) {
        if (installedPlugins.has(plugin)) {
          __DEV__ && warn(`Plugin has already been applied to target app.`)
        } else if (plugin && isFunction(plugin.install)) {
          installedPlugins.add(plugin)
          plugin.install(app, ...options)
        } else if (isFunction(plugin)) {
          installedPlugins.add(plugin)
          plugin(app, ...options)
        } else if (__DEV__) {
          warn(
            `A plugin must either be a function or an object with an "install" ` +
              `function.`
          )
        }
        return app
      },
      // 省略部分代碼 ....
   }
}
複製代碼
  • Store 類的install
    • 實現經過inject獲取
    • 實現this.$store獲取

下面接着看provide實現

install (app, injectKey) {
  // 實現經過inject獲取
  app.provide(injectKey || storeKey, this)
  // 實現this.$store獲取
  app.config.globalProperties.$store = this
}
複製代碼

app.provide實現

provide(key, value) {
  // 已存在則警告
  if (__DEV__ && (key as string | symbol) in context.provides) {
    warn(
      `App already provides property with key "${String(key)}". ` +
        `It will be overwritten with the new value.`
    )
  }

  // 將store放入context的provide中
  context.provides[key as string] = value
  return app
}

// context相關 context爲上下文對象
const context = createAppContext()
export function createAppContext(): AppContext {
  return {
    app: null as any,
    config: {
      isNativeTag: NO,
      performance: false,
      globalProperties: {},
      optionMergeStrategies: {},
      errorHandler: undefined,
      warnHandler: undefined,
      compilerOptions: {}
    },
    mixins: [],
    components: {},
    directives: {},
    provides: Object.create(null)
  }
}
複製代碼

Vue.useStore

  • 在Vue3 Composition API中使用Vuex
import { useStore } from 'vuex'

export default{
    setup(){
        const store = useStore();
    }
}
複製代碼
  • useStore的實現
function useStore (key = null) {
  return inject(key !== null ? key : storeKey)
}
複製代碼

Vue.inject

  • 經過provide時存入的key取出store
  • 有父級實例則取父級實例的provides,沒有則取根實例的provides
function inject( key: InjectionKey<any> | string, defaultValue?: unknown, treatDefaultAsFactory = false ) {
  const instance = currentInstance || currentRenderingInstance
  if (instance) {
    // 有父級實例則取父級實例的provides,沒有則取根實例的provides
    const provides =
      instance.parent == null
        ? instance.vnode.appContext && instance.vnode.appContext.provides
        : instance.parent.provides

    // 經過provide時存入的key取出store
    if (provides && (key as string | symbol) in provides) {
      return provides[key as string]
    // 省略一部分代碼......
  } 
}
複製代碼

Vue.provide

  • Vue的provide API也比較簡單,至關於直接經過key/value賦值
  • 當前實例provides和父級實例provides相同時,經過原型鏈創建鏈接
function provide<T>(key: InjectionKey<T> | string | number, value: T) {
  if (!currentInstance) {
    if (__DEV__) {
      warn(`provide() can only be used inside setup().`)
    }
  } else {
    let provides = currentInstance.provides
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides)
    }
    // TS doesn't allow symbol as index type
    provides[key as string] = value
  }
}
複製代碼

注入

  • 爲何每一個組件實例都有Store對象了?
    • 在建立組件實例的時候注入了provides
function createComponentInstance(vnode, parent, suspense) {
    const type = vnode.type;
    const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
    const instance = {
        parent,
        appContext,
        // ...
        provides: parent ? parent.provides : Object.create(appContext.provides),
        // ...
    }
    // ...
    return instance;
}
複製代碼

可從vue中引入provide、inject、getCurrentInstance等API進行庫開發 / 高階用法,這裏不過多贅述。

Diff算法優化

瞭解Vue3的Diff算法優化前,能夠先了解一下Vue2的Diff算法

本部分注重把算法講清楚,將不進行逐行源碼分析

  • Vue3中的主要優化點爲
    • 在updateChildren時雙端比較 -> 最長遞增子序列
    • 全量Diff -> 靜態標記 + 非全量Diff
    • 靜態提高

updateChildren

  • Vue2
    • 頭 - 頭比較
    • 尾 - 尾比較
    • 頭 - 尾比較
    • 尾 - 頭比較
  • Vue3
    • 頭 - 頭比較
    • 尾 - 尾比較
    • 基於最長遞增子序列進行移動 / 刪除 / 新增

舉個🌰

  • oldChild [a,b,c,d,e,f,g]
  • newChild [a,b,f,c,d,e,h,g]
  1. 首先進行頭 - 頭比較,比較到不同的節點時跳出循環
    • 獲得[a,b]
  2. 而後進行尾 - 尾比較,比較到不同的節點時跳出循環
    • 獲得[g]
  3. 剩餘[f,c,d,e,h]
    • 經過newIndexToOldIndexMap生成數組[5, 2, 3, 4, -1]
    • 得出最長遞增子序列[2, 3, 4]對應節點爲[c, d, e]
    • 剩餘的節點基於[c, d, e]進行移動 / 新增 / 刪除

最長遞增子序列 減小Dom元素的移動,達到最少的 dom 操做以減少開銷。

關於最長遞增子序列算法能夠看看最長遞增子序列

靜態標記

Vue2中對vdom進行全量Diff,Vue3中增長了靜態標記進行非全量Diff

對vnode打了像如下枚舉內的靜態標記

  • patchFlag
export enum PatchFlags{
  TEXT = 1 ,  //動態文本節點
  CLASS = 1 << 1, //2 動態class
  STYLE = 1 << 2, //4 動態style
  PROPS = 1 << 3, //8 動態屬性,但不包含類名和樣式
  FULL_PROPS = 1 << 4, //16 具備動態key屬性,當key改變時,需進行完整的diff比較
  HYDRATE_EVENTS = 1 << 5,//32 帶有監聽事件的節點
  STABLE_FRAGMENT = 1 << 6, //64 一個不會改變子節點順序的fragment
  KEYED_FRAGMENT = 1 << 7,  //128 帶有key屬性的fragment或部分子節點有key
  UNKEYEN_FRAGMENT = 1 << 8,   //256 子節點沒有key的fragment
  NEED_PATCH = 1 << 9,   //512 一個節點只會進行非props比較
  DYNAMIC_SLOTS = 1 << 10,//1024 動態slot
  HOISTED = -1,   //靜態節點 
  //指示在diff過程當中要退出優化模式
  BAIL = -2
}
複製代碼

舉個🌰

  • 模板長這樣
<div>
  <p>Hello World</p>
  <p>{{msg}}</p>
</div>
複製代碼
  • 生成vdom源碼

對msg變量進行了標記

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("p", null, "Hello World"),
    _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

// Check the console for the AST
複製代碼

總結

  • 對vnode進行標記,將須要動態更新和不須要動態更新的節點進行分類
  • 靜態節點僅需建立一次,渲染直接複用,不參與diff算法流程。

靜態提高

  • Vue2中不管是元素是否參與更新,每次都會從新建立

  • Vue3中對於不參與更新的元素,只會被建立一次,以後會在每次渲染時候被不停地複用

  • 之後每次進行render的時候,就不會重複建立這些靜態的內容,而是直接從一開始就建立好的常量中取就好了。

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

/* * 靜態提高前 */
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("p", null, "Xmo"),
    _createVNode("p", null, "Xmo"),
    _createVNode("p", null, "Xmo"),
    _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

/* * 靜態提高後 */
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "Xmo", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "Xmo", -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createVNode("p", null, "Xmo", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _hoisted_1,
    _hoisted_2,
    _hoisted_3,
    _createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

// Check the console for the AST
複製代碼

cacheHandlers 事件偵聽器緩存

  • 默認狀況下onClick會被視爲動態綁定,因此每次都會去追蹤它的變化

  • 可是由於是同一個函數,因此沒有追蹤變化,直接緩存起來複用便可。

// 模板
<div>
  <button @click="onClick">btn</button>
</div>


// 使用緩存前
// 這裏咱們尚未開啓事件監聽緩存,熟悉的靜態標記 8 /* PROPS */ 出現了,
// 它將標籤的 Props (屬性) 標記動態屬性。
// 若是咱們存在屬性不會改變,不但願這個屬性被標記爲動態,那麼就須要 cacheHandler 的出場了。
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", { onClick: _ctx.onClick }, "btn", 8 /* PROPS */, ["onClick"])
  ]))
}

// Check the console for the AST


// 使用緩存後
import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("button", {
      onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
    }, "btn")
  ]))
}

// Check the console for the AST
複製代碼

它的意思很明顯,onClick 方法被存入 cache。

在使用的時候,若是能在緩存中找到這個方法,那麼它將直接被使用。

若是找不到,那麼將這個方法注入緩存。

總之,就是把方法給緩存了。


相關文章
相關標籤/搜索