[建议收藏] 你想知道的Vue3核心源码这里都有

发表于 2年以前  | 总阅读数:585 次

Effect和Reactive

effect作为Vue响应式原理中的核心,在Computed、Watch、Reactive中都有出现

主要和Reactive(Proxy)、track、trigger等函数配合实现收集依赖,触发依赖更新

  • Effect

  • 副作用依赖函数

  • Track

  • 依赖收集

  • Trigger

  • 依赖触发

Effect

effect可以被理解为一个副作用函数,被当做依赖收集,在响应式数据更新后被触发。

Vue的响应式API例如Computed、Watch都有用到effect来实现

  • 先来看看入口函数
  • 入口函数主要是一些逻辑处理,核心逻辑位于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函数中,用于依赖收集

源码详解见注释

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函数中,用于触发依赖更新

源码详解见注释

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的源码就清楚了,源码详解见注释。

注:源码结构较为复杂(封装),为便于理解原理,以下为简化源码。

  • 总结来说
  • 在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的赋值处理

  • 函数

  • getter、setter

  • computed支持两种写法

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 const 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。

在使用的时候,如果能在缓存中找到这个方法,那么它将直接被使用。

如果找不到,那么将这个方法注入缓存。

总之,就是把方法给缓存了。

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/71fWC3Tox0DTLOL-Nm-wQg

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:8月以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:8月以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:8月以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:8月以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:8月以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:8月以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:8月以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:8月以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:8月以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:8月以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:8月以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:8月以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:8月以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:8月以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:8月以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:8月以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:8月以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:8月以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:8月以前  |  398次阅读  |  详细内容 »
 目录