vue

247 阅读49分钟

Vue3.x

响应式原理

juejin.cn/post/685889…

1.为什么要用proxy,改用proxy之后的利与弊
  • vue3.0消除了当前 Vue 2 系列中基于 Object.defineProperty 所存在的一些局限,这些局限包括:1 对属性的添加、删除动作的监测; 2 对数组基于下标的修改、对于 .length 修改的监测; 3 对 Map、Set、WeakMap 和 WeakSet 的支持;

  • vue3.0将放弃对低版本浏览器的兼容(兼容版本ie11以上)

  • vue3.0 响应式用到的捕获器handler.has() -> in 操作符 的捕捉器。 handler.get() -> 属性读取 操作的捕捉器。handler.set() -> 属性设置* 操作的捕捉器。handler.deleteProperty() -> delete 操作符 的捕捉器。 handler.ownKeys() -> Object.keys(proxy)for...in...循环Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器; - 5种捕获器有名字相同的Reflect方法对应,不管 Proxy 怎么修改默认行为,都可以通过 Reflect 获取默认行为。如Reflect.set() 会返回一个是否修改成功的布尔值,直接赋值 target[key] = newValue,而不返回 true 就会报错。

2.Ref实现
  • 3.2 版本由原先的 track 函数改成了 trackRefValue,把依赖保存到 ref 对象的 dep 属性中则省去了这一系列的判断和设置,从而优化性能。由于直接从 ref 属性中就拿到了它所有的依赖且遍历执行,不需要执行 trigger 函数一些额外的逻辑,因此在性能上也得到了提升。
function ref(value) {
  return createRef(value)
}

const convert = (val) => isObject(val) ? reactive(val) : val

function createRef(rawValue, shallow = false) {
  if (isRef(rawValue)) {
    // 如果传入的就是一个 ref,那么返回自身即可,处理嵌套 ref 的情况。
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl {
  constructor(_rawValue, _shallow = false) {
    this._rawValue = _rawValue
    this._shallow = _shallow
    this.__v_isRef = true
    // 非 shallow 的情况,如果它的值是对象或者数组,则递归响应式
    this._value = _shallow ? _rawValue : convert(_rawValue)
  }
  get value() {
    // 给 value 属性添加 getter,并做依赖收集
    track(toRaw(this), 'get' /* GET */, 'value')
    return this._value
  }
  set value(newVal) {
    // 给 value 属性添加 setter
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal
      this._value = this._shallow ? newVal : convert(newVal)
      // 派发通知
      trigger(toRaw(this), 'set' /* SET */, 'value', newVal)
    }
  }
}
2.reactive实现
export function reactive(target: object) {
  if (readonlyToRaw.has(target)) {
    return target
  }
  return createReactiveObject(
    target,                   /* 目标对象 */
    rawToReactive,            /* { [targetObject] : obseved  }   */
    reactiveToRaw,            /* { [obseved] : targetObject }  */
    mutableHandlers,          /* 处理 基本数据类型 和 引用数据类型 */
    mutableCollectionHandlers /* 用于处理 Set, Map, WeakMap, WeakSet 类型 */
  )
}

const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
function createReactiveObject(
  target: unknown,
  toProxy: WeakMap<any, any>,
  toRaw: WeakMap<any, any>,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  /* 判断目标对象是否被effect */
  /* observed 为经过 new Proxy代理的函数 */
  let observed = toProxy.get(target) /* { [target] : obseved  } */
  if (observed !== void 0) { /* 如果目标对象已经被响应式处理,那么直接返回proxy的observed对象 */
    return observed
  }
  if (toRaw.has(target)) { /* { [observed] : target  } */
    return target
  }
  /* 如果目标对象是 Set, Map, WeakMap, WeakSet 类型,那么 hander函数是 collectionHandlers 否侧目标函数是baseHandlers */
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
   /* TODO: 创建响应式对象  */
  observed = new Proxy(target, handlers)
  /* target 和 observed 建立关联 */
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  /* 返回observed对象 */
  return observed
}

image.png

3.effect->新的渲染watcher
  • vue3.0用effect副作用钩子来代替vue2.0watcher。
  • mountComponent 初始化mountComponent 整个mountComponent的主要分为了三步,我们这里分别介绍一下每个步骤干了什么: ① 第一步: 创建component 实例 。 ② 第二步:初始化组件,建立proxy ,根据字符窜模版得到render函数。生命周期钩子函数处理等等 ③ 第三步:建立一个渲染effect,里面包装了真正的渲染方法componentEffect,添加一些effect初始化属性立即执行effect将当前渲染effect赋值给activeEffect,进行依赖收集
  // 初始化组件
  const mountComponent: MountComponentFn = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    /* 第一步: 创建component 实例   */
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ))

    /* 第二步 : 初始化 初始化组件,建立proxy , 根据字符窜模版得到render函数 */
    setupComponent(instance)
    /* 第三步:建立一个渲染effect,执行effect,将当前渲染effect赋值给activeEffect,进行依赖收集*/
    setupRenderEffect(
      instance,     // 组件实例
      initialVNode, //vnode  
      container,    // 容器元素
      anchor,
      parentSuspense,
      isSVG,
      optimized
    )   
  }
  • setupRenderEffect 构建渲染effect,创建一个effect,并把它赋值给组件实例的update方法,作为渲染更新视图用
const setupRenderEffect: SetupRenderEffectFn = (
   instance,
   initialVNode,
   container,
   anchor,
   parentSuspense,
   isSVG,
   optimized
 ) => {
   /* 创建一个渲染 effect */
   instance.update = effect(function componentEffect() {
     //...省去的内容后面会讲到
   },{ scheduler: queueJob })
 }
  • effect做了些什么

vue3.2版本相比于之前每次执行 effect 函数都需要先清空依赖,再添加依赖的过程,现在的实现会在每次执行 effect 包裹的函数前标记依赖的状态,过程中对于已经收集的依赖不会重复收集,执行完 effect 函数还会移除掉已被收集但是新的一轮依赖收集中没有被收集的依赖。 track中生成的depdep.n:n 是 newTracked 的缩写,表示是否是最新收集的(是否当前层)dep.w:w 是 wasTracked 的缩写,表示是否已经被收集,避免重复收集

createReactiveEffect的作用主要是配置了一些初始化的参数,然后包装了之前传进来的fn,重要的一点是把当前的effect赋值给了activeEffect,这一点非常重要,和收集依赖有着直接的关系

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  const effect = createReactiveEffect(fn, options)
  /* 如果不是懒加载 立即执行 effect函数 */
  if (!options.lazy) {
    effect()
  }
  return effect
}

function createReactiveEffect<T = any>(
  fn: (...args: any[]) => T, /**回调函数 */
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    try {
        enableTracking()
        effectStack.push(effect) //往effect数组中里放入当前 effect
        activeEffect = effect //TODO: effect 赋值给当前的 activeEffect
        return fn(...args) //TODO:    fn 为effect传进来 componentEffect
      } finally {
        effectStack.pop() //完成依赖收集后从effect数组删掉这个 effect
        resetTracking() 
        /* 将activeEffect还原到之前的effect */
        activeEffect = effectStack[effectStack.length - 1]
    }
  } as ReactiveEffect
  /* 配置一下初始化参数 */
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = [] /* TODO:用于收集相关依赖 */
  effect.options = options
  return effect
}

4.track(get)依赖收集
  • vue3.0深度响应式我们也只能在获取上一级get之后才能触发下一级的深度响应式
setup(){
 const state = reactive({ a:{ b:{} } })
 return {
     state
 }
}

在初始化的时候,只有a的一层级建立了响应式,b并没有建立响应式,而当我们用state.a的时候,才会真正的将b也做响应式处理,也就是说我们访问了上一级属性后,下一代属性才会真正意义上建立响应式,这样做好处是, 1 初始化的时候不用递归去处理对象,造成了不必要的性能开销。 2 有一些没有用上的state,这里就不需要在深层次响应式处理。

  • 渲染effect函数componentEffect如何触发get

首先执行renderEffect ,赋值给activeEffect ,调用renderComponentRoot方法形成树结构,这里要注意的是,我们在最初mountComponent的setupComponent方法中,已经通过编译方法compile编译了template模版的内容,state.a state.b等抽象语法树,最终返回的render函数在这个阶段会被触发,在render函数中在模版中的表达式 state.a state.b 点语法会被替换成data中真实的属性。这时候就进行了真正的依赖收集,触发了get方法。接下来就是触发生命周期 beforeMount ,然后对整个树结构重新patch,patch完毕后,调用mounted钩子

function componentEffect() {
    if (!instance.isMounted) {
        let vnodeHook: VNodeHook | null | undefined
        const { el, props } = initialVNode
        const { bm, m, a, parent } = instance
        /* TODO: 触发instance.render函数,形成树结构 */
        const subTree = (instance.subTree = renderComponentRoot(instance))
        if (bm) {
          //触发 beforeMount声明周期钩子
          invokeArrayFns(bm)
        }
        patch(
            null,
            subTree,
            container,
            anchor,
            instance,
            parentSuspense,
            isSVG
        )
        /* 触发声明周期 mounted钩子 */
        if (m) {
          queuePostRenderEffect(m, parentSuspense)
        }
        instance.isMounted = true
      } else {
        // 更新组件逻辑
        // ......
      }
}

  • track->依赖收集器

里面主要引入了两个概念 targetMapdepsMap targetMap 键值对 proxy : depsMap proxy : 为reactive代理后的 Observer对象 。 depsMap :为存放依赖dep的 map 映射。

depsMap 键值对:key : deps key 为当前get访问的属性名, deps 存放effect的set数据类型。

我们知道track作用大致是,首先根据 proxy对象,获取存放deps的depsMap,然后通过访问的属性名key获取对应的dep,然后将当前激活的activeEffect存入当前dep收集依赖。

/* target 对象本身 ,key属性值  type 为 'GET' */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  /* 当打印或者获取属性的时候 console.log(this.a) 是没有activeEffect的 当前返回值为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()))
  }
   /* 当前activeEffect */
  if (!dep.has(activeEffect)) {
    /* dep添加 activeEffect */
    dep.add(activeEffect)
    /* 每个 activeEffect的deps 存放当前的dep */
    activeEffect.deps.push(dep)
  }
}
5.trigger(set)派发更新
  • 首先从targetMap中,根据当前proxy找到与之对应的depsMap。 ② 根据key找到depsMap中对应的deps,然后通过add方法分离出对应的effect回调函数和computed回调函数。 ③ 依次执行computedRunners 和 effects 队列里面的回调函数,如果发现需要调度处理,放进scheduler事件调度。此时的effect队列中有我们上述负责渲染的renderEffect,还有通过effectAPI建立的effect,以及通过watch形成的effect。我们这里只考虑到渲染effect。至于后面的情况会在接下来的文章中和大家一起分享
/* 根据value值的改变,从effect和computer拿出对应的callback ,然后依次执行 */
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)
  /* 没有经过依赖收集的 ,直接返回 */
  if (!depsMap) {
    return
  }
  const effects = new Set<ReactiveEffect>()        /* effect钩子队列 */
  const computedRunners = new Set<ReactiveEffect>() /* 计算属性队列 */
  const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect || !shouldTrack) {
          if (effect.options.computed) { /* 处理computed逻辑 */
            computedRunners.add(effect)  /* 储存对应的dep */
          } else {
            effects.add(effect)  /* 储存对应的dep */
          }
        }
      })
    }
  }

  add(depsMap.get(key))

  const run = (effect: ReactiveEffect) => {
    if (effect.options.scheduler) { /* 放进 scheduler 调度*/
      effect.options.scheduler(effect)
    } else {
      effect() /* 不存在调度情况,直接执行effect */
    }
  }

  //TODO: 必须首先运行计算属性的更新,以便计算的getter
  //在任何依赖于它们的正常更新effect运行之前,都可能失效。

  computedRunners.forEach(run) /* 依次执行computedRunners 回调*/
  effects.forEach(run) /* 依次执行 effect 回调( TODO: 里面包括渲染effect )*/
}
6.响应式总结
  • 1.初始化阶段: 初始化阶段通过组件初始化方法形成对应的proxy对象,然后形成一个负责渲染的effect。
  • 2.get依赖收集阶段:通过解析template,替换真实data属性,来触发get,然后通过stack方法,通过proxy对象和key形成对应的deps,将负责渲染的effect存入deps。(这个过程还有其他的effect,比如watchEffect存入deps中 )。
  • 3.set派发更新阶段:当我们 this[key] = value 改变属性的时候,首先通过trigger方法,通过proxy对象和key找到对应的deps,然后给deps分类分成computedRunners和effect,然后依次执行,如果需要调度的,直接放入调度。

watch与computed原理

juejin.cn/post/686919…

1.watch与watchEffect
  • watch源码
export function watch<T = any>(
  source: WatchSource<T> | WatchSource<T>[],  /* getter方法  */
  cb: WatchCallback<T>,                       /* hander回调函数 */
  options?: WatchOptions                      /* watchOptions */
): StopHandle { 
  return doWatch(source, cb, options)
}
  • watchEffect源码
export function watchEffect(
  effect: WatchEffect,         /* watch effect */ 
  options?: BaseWatchOptions   /* watchOptions */
): StopHandle {
  return doWatch(effect, null, options)
}
  • doWatch源码
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): StopHandle {
  /* 此时的 instance 是当前正在初始化操作的 instance  */
  const instance = currentInstance
  let getter: () => any
  if (isArray(source)) { /*  判断source 为数组 ,此时是watch情况 */
    getter = () =>
      source.map(
        s =>
          isRef(s)
            ? s.value
            : callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
      )
  /* 判断ref情况 ,此时watch api情况*/
  } else if (isRef(source)) {
    getter = () => source.value
   /* 正常watch情况,处理getter () => state.count */
  } else if (cb) { 
    getter = () =>
      callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
  } else {
    /*  watchEffect 情况 */
    getter = () => {
      if (instance && instance.isUnmounted) {
        return
      }
      if (cleanup) {
        cleanup()
      }
      return callWithErrorHandling(
        source,
        instance,
        ErrorCodes.WATCH_CALLBACK,
        [onInvalidate]
      )
    }
  }
   /* 处理深度监听逻辑 */
  if (cb && deep) {
    const baseGetter = getter
    /* 将当前 */
    getter = () => traverse(baseGetter())
  }

  let cleanup: () => void
  /* 清除当前watchEffect */
  const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
    cleanup = runner.options.onStop = () => {
      callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
    }
  }
  
  let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE

  const applyCb = cb
    ? () => {
        if (instance && instance.isUnmounted) {
          return
        }
        const newValue = runner()
        if (deep || hasChanged(newValue, oldValue)) {
          if (cleanup) {
            cleanup()
          }
          callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
            newValue,
            oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue,
            onInvalidate
          ])
          oldValue = newValue
        }
      }
    : void 0
  /* TODO:  scheduler事件调度*/
  let scheduler: (job: () => any) => void
  if (flush === 'sync') { /* 同步执行 */
    scheduler = invoke
  } else if (flush === 'pre') { /* 在组件更新之前执行 */
    scheduler = job => {
      if (!instance || instance.isMounted) {
        queueJob(job)
      } else {
        job()
      }
    }
  } else {  /* 正常情况 */
    scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
  }
  const runner = effect(getter, {
    lazy: true, /* 此时 lazy 为true ,当前watchEffect不会立即执行 */
    computed: true,
    onTrack,
    onTrigger,
    scheduler: applyCb ? () => scheduler(applyCb) : scheduler
  })

  recordInstanceBoundEffect(runner)
  /* 执行watcherEffect函数 */
  if (applyCb) {
    if (immediate) {
      applyCb()
    } else {
      oldValue = runner()
    }
  } else {
    runner()
  }
  /* 返回函数 ,用终止当前的watchEffect */
  return () => {
    stop(runner)
    if (instance) {
      remove(instance.effects!, runner)
    }
  }
}
  • 1 封装getter方法

首先watch会根据source不同的类型,来形成getter方法。

为什么要得到getter方法? 原因很简单,在接下来形成执行effect函数的时候,getter方法会执行,可以读取proxy处理的data属性 或者是ref属性,触发proxy对象getter拦截器,收集依赖。

  • 2 形成applyCb监听回调

此时如果是composition api中 watch调用的doWatch方法,会有cb回调函数 ,如果有cb,会在下一次getter方法执行后,形成新的newValue,然后执行回调函数,也就是watch的监听函数

  • 3 effect处理,得到runner

将第一步形成的getter传递给effect处理 ,此时生成runner方法 ,首先此时的runner方法经过 createReactiveEffect 创造出的一个effect函数 这里可以称作 watcheffect,effect中deps用来收集依赖 ,watch的监听函数通过scheduler处理传递给当前的effect,getter方法作为fn 传递给当前effect,当依赖项发生变化的时候,首先执行fn即getter方法。

  • 4 执行runner

接下来执行 runner 方法 ,在runner方法的执行过程中 ,会做几件重要的事

一 把当前的 effect 作为activeEffect.

二 执行getter方法收集依赖,此时收集的依赖会,存放到当前effect的deps中.

三 当前属性的 deps 存放当前的 effect.

  • 5 依赖跟踪

当deps中依赖项改变的时候,会出发proxy属性 set方法 ,然后会遍历属性deps ,执行判断当前effect上有没有scheduler ,在watch处理流程中,是存在scheduler。那么会 执行上一章节中set逻辑中的trigger逻辑。

 effect.options.scheduler(effect)

而此时的scheduler,有两种情况

 applyCb ? () => scheduler(applyCb) : scheduler

① 当我们用composition-api 中 watchEffect 是不存在 applyCb回调函数的,此时执行 scheduler(effect) ,会在调度中执行当前effect,也就是watchEffect。

② 当我们用composition-api 中 watch,此时会执行 scheduler(applyCb) ,那么当前的 applyCb 回调函数(我们这里可以理解watch监听函数)会被传进scheduler执行,而不是当前的watchEffect本身。

image.png

2.computed
  • watch侧重点是对数据更新所产生的依赖追踪,而computer侧重点是对数据的缓存处理引用,这就是watch和computed本质的区别
  • 无论是vue3.0 特有的Composition API,还是 vue2.0的options形式,最后走的逻辑都是computed
export function computed<T>(
  options: WritableComputedOptions<T>
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  if (isFunction(getterOrOptions)) {  /* 处理只有get函数的逻辑 */
    getter = getterOrOptions
    setter = () => {}
  } else { /* 还有 getter 和 setter情况 */
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  let dirty = true
  let value: T
  let computed: ComputedRef<T>
  const runner = effect(getter, {
    lazy: true,
    computed: true,
    scheduler: () => {
      if (!dirty) {
        dirty = true /* 派发所有引用当前计算属性的副作用函数effect */
        trigger(computed, TriggerOpTypes.SET, 'value')
      }
    }
  })
  computed = {
    _isRef: true,
    effect: runner,
    get value() { 
      if (dirty) {
        /* 运行computer函数内容 */
        value = runner()
        dirty = false
      }/* 收集引入当前computer属性的依赖 */
      track(computed, TrackOpTypes.GET, 'value')
      return value
    },
    set value(newValue: T) {
      setter(newValue)
    }
  } as any
  return computed
}
  • 形成computedEffect: 首先根据当前参数类型判断当前计算属性,是单纯getter,还是可以修改属性的 setter 和 getter,将getter作为callback传入effect函数形成一个effect,我们这里姑且称之为computedEffect,computedEffec的调度函数中,是对当前computed里面引用的reactive或者ref变化,而追溯到引入自身计算属性的依赖追踪,然后形成并返回一个computed对象

  • 依赖收集:当我们引用computed属性的时候,会调用track方法进行依赖收集,会执行和响应式一样的流程,这里重要的是,当在收集本身computed对象依赖的同时,会调用runner()方法,runner()执行了getter方法,此时又收集了当前computed引用的reactive或者ref的依赖项,也就是说,为什么当computed中依赖项更新时候,当前的getter函数会执行,形成新的value

  • 派发更新:当reactive或者ref的依赖项更新的时候会触发set然后会触发runner函数的执行,runner函数执行会重新计算出新的value,runner函数执行会执行scheduler函数,scheduler里面会执行当前computed计算属性的依赖项,追踪到所有引用当前computer的依赖项,更新新的value

vue3.0diff算法

juejin.cn/post/686196…

1.diff算法的作用域
  • 在vue3.0源码中 ,patchElement用于处理element类型的vnode,processFragment用于处理Fragment类型的vnode,如果面对当前vnode存在有很多chidren的情况,那么需要分别遍历 patchChildren新的children Vnode和老的 children vnode,在patchChildren方法中根据Vnode是否有key分别执行patchKeyedChildren(diff算法)和patchUnkeyedChildren
2.diff算法作用
  • 如果没有用到diff算法,而是依次patch虚拟dom树,那么如上稍微修改dom顺序,就会在patch过程中没有一一对应的的新老vnode,所以老vnode的节点没有一个可以复用,这样就需要重新创造新的节点,浪费了性能开销,diff作用就是在patch子vnode过程中,找到与新vnode对应的老vnode,复用真实的dom节点,避免不必要的性能开销
3.diff实现

diff整体策略为:深度优先,同层比较。比较只会在同层级进行, 不会跨层级比较。比较的过程中,循环从两边向中间收拢。 image.png

  • ⑤ 不确定的元素 ( 这种情况说明没有patch完相同的vnode )

    • 遍历所有新节点把对应的key和索引,存入key -> index 的keyToNewIndexMap中。
    • newIndexToOldIndexMap 用来存放新节点索引和老节点索引的数组。 newIndexToOldIndexMap 数组的index是新vnode的索引 , value是老vnode的索引。
    • 开始遍历老的节点,判断有没有key, 如果存在key通过新节点的keyToNewIndexMap找到与新节点index,如果不存在key那么会遍历剩下来的新节点试图找到对应index。
    • 如果存在index证明有对应的老节点,那么直接复用老节点进行patch,没有找到与老节点对应的新节点,删除当前老节点。
    • newIndexToOldIndexMap中,存入旧节点的index,建立对应新老节点关系。
    • 用getSequence(newIndexToOldIndexMap)生成最长稳定序列(最长递增子序列),需要一个序列作为基础的参照序列,其他未在稳定序列的节点,进行移动。
    • 对于以下的原始序列 0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 最长递增子序列为 0, 2, 6, 9, 11, 15。
4.diff的改进
  • 编译阶段的优化:
    • 事件缓存:将事件缓存(如: @click),可以理解为变成静态的了
    • 静态提升:第一次创建静态节点时保存,后续直接复用
    • 添加静态标记:给节点添加静态标记,以优化 Diff 过程
  • Diff阶段的优化
    • Vue2 是全量 Diff,Vue3 是静态标记 + 非全量 Diff
    • 使用最长递增子序列优化了对比流程
4.key的正确用法
  • key的作用
    • 在v-for循环中,key的作用是:通过判断newVnode和OldVnode的key和type是否相等,从而复用与新节点对应的老节点,节约性能的开销。
  • 如何正确使用key
    • 用index做key的效果实际和没有用diff算法是一样的,改变列表的顺序就无法复用了(key相同,type不同)
    • 用index拼接其他值作为key,改变列表的顺序就无法复用了(生成的key不相同)
    • 正确用法 :用唯一值id做key

Vue

MVVM是什么?和MVC有何区别呢?
  • Model(模型):负责从数据库中取数据
  • View(视图):负责展示数据的地方
  • Controller(控制器):用户交互的地方,例如点击事件等等。思想:Controller将Model的数据展示在View上
  • VM:也就是View-Model,【视图】和【模型】之间数据的双向绑定。思想:实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,对应 View 层显示会自动改变。
为什么data是个函数并且返回一个对象呢?
  • data之所以是一个函数,是因为一个组件可能会多处调用,而每一次调用就会执行data函数并返回新的数据对象,这样,可以避免多处调用之间的数据污染
不需要响应式的数据应该怎么处理?
  • Object.freeze()
Vue常用的修饰符有哪些有什么应用场景

表单修饰符:lazy(光标离开标签的时候,才会将值赋予给value) trim number;

事件修饰符:stop prevent self(只当在 event.target 是当前元素自身时触发处理函数) once(绑定了事件以后只能触发一次,第二次就不会触发)capture(使事件触发从包含这个元素的顶层开始往下触发) passive(相当于给onscroll事件整了一个.lazy修饰符) native(让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件)

image.png

Vue中的过滤器了解吗?过滤器的应用场景有哪些?

image.png

v-if和v-show有何区别
  • v-if是通过控制dom元素的删除和生成来实现显隐,每一次显隐都会使组件重新跑一遍生命周期
  • v-show是通过控制dom元素的css样式来实现显隐,不会销毁
  • 频繁或者大数量显隐使用v-show,否则使用v-if
为什么v-if和v-for不建议用在同一标签?
  • 在Vue2中,v-for优先级是高于v-if的。v-forv-if同时存在,会先把所有元素都遍历出来,然后再一个个判断,渲染了无用的节点,增加操作。
  • 需要注意的是在vue3中则完全相反,v-if的优先级高于v-for,所以v-if执行时,它调用的变量还不存在,就会导致异常
  • 永远不要把 v-if 和 v-for 同时用在同一个元素上
为什么不建议用index做key,为什么不建议用随机数做key?
  • index:改变列表的顺序就无法复用(key相同,type不同)
  • 随机数:改变列表的顺序就无法复用(key不相同)
相同的路由组件如何重新渲染?
  • 改变组件的key即可
Vue组件为什么只能有一个根元素
  • vue2中组件确实只能有一个根,因为vdom是一颗单根树形结构,patch方法在遍历的时候从根节点开始遍历
  • vue3中之所以可以写多个根节点,是因为引入了Fragment的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个Fragment节点,把多个根节点作为它的children。
vue-loader是什么?它有什么作用
  • vue-loader是用于处理单文件组件(SFC,Single-File Component),vue-loader被执行时,它会对SFC中的template、script和style每个语言块用单独的loader链处理。vue-loader会调用@vue/compiler-sfc模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,最后将这些单独的块装配成最终的组件模块。
  • 必须交由 @vue/compiler-sfc 编译为标准的 JavaScript 和 CSS,一个编译后的 SFC 是一个标准的 JavaScript(ES) 模块
  • SFC 中的 <style> 标签一般会在开发时注入成原生的 <style> 标签以支持热更新,而生产环境下它们会被抽取、合并成单独的 CSS 文件。
  • Vite 提供 Vue SFC 支持的官方插件@vitejs/plugin-vue
// source.vue被vue-loader处理之后返回的代码// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
​
script.render = render
export default script
  • script lang="ts",webpack会展开成import script from 'babel-loader!vue-loader!source.vue?vue&type=script'
  • style scoped lang="scss",webpack会展开成import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
  • template lang="pug",webpack会展开成import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'需要用Vue 模板编译器编译template,从而得到render函数
审查元素时发现data-v-xxxxx
  • 这是在标记vue文件中style时使用scoped标记产生的,因为要保证各文件中的css不相互影响,给每个component都做了唯一的标记
  • vue 中的 scoped 属性的效果主要通过 PostCSS 转译实现的。PostCSS 给一个组件中的所有 DOM 添加了一个独一无二的动态属性,然后,给 CSS 选择器额外添加一个对应的属性选择器。
  • scoped内如何实现样式穿透的
    • ::v-deep 操作符( >>> 的别名),.a >>> .b { /* ... */ } 编译成.a[data-v-f3f3eg9] .b { /* ... */ }
    • 定义一个含有 scoped 属性的 style 标签之外,再定义一个不含有 scoped 属性的 style 标签
ref和reactive异同
  • ref接收内部值(inner value)返回响应式Ref对象,reactive返回响应式代理对象
  • reactive内部使用Proxy代理传入对象并拦截该对象各种操作(track,trigger),从而实现响应式。ref内部封装一个RefImpl类,并设置get value/set value,拦截用户对值的访问,从而实现响应式。
  • ref返回的响应式数据在JS中使用需要加上.value才能访问其值,在视图中使用会自动脱ref,不需要.value;ref可以接收对象或数组等非原始值,但内部依然是reactive实现响应式;reactive内部如果接收Ref对象会自动脱ref;使用展开运算符(...)展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象之后再展开。
watch和watchEffect异同
  • watchEffect在使用时,传入的函数会立刻执行一次。在同步执行过程期间,它会自动追踪响应数据作为依赖。可以消除手动维护依赖列表的负担。此外,如果你需要侦听一个嵌套数据结构中的几个属性,watchEffect() 可能会比深度侦听器更有效,因为它将只跟踪回调中被使用到的属性,而不是递归地跟踪所有的属性。
  • watchEffect 仅会在其同步执行期间,才追踪依赖。在使用异步回调时,只有在第一个 await 正常工作前访问到的属性才会被追踪。
  • watchEffect(effect)是一种特殊watch
  • watch默认情况下并不会执行回调函数,除非我们手动设置immediate选项。
  • watch更底层,可以接收多种数据源,包括用于依赖收集的getter函数,因此它完全可以实现watchEffect的功能,同时由于可以指定getter函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用watch。
  • watch和watchEffect默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。如果想在侦听器回调中能访问被 Vue 更新之后的 DOM,你需要指明 flush: 'post' 选项
  • 在 setup() 或 <script setup> 中用同步语句创建的侦听器,会自动绑定到宿主组件实例上,并且会在宿主组件卸载时自动停止。如果用异步回调创建一个侦听器,那么它不会绑定到当前组件上,你必须手动停止它,以防内存泄漏。要手动停止一个侦听器,请调用 watch 或 watchEffect 返回的函数
computed和watch有何区别?
  • computed() 方法期望接收一个 getter 函数,返回值为一个计算属性 ref。和其他一般的 ref 类似,你可以通过 ref.value 访问计算结果。计算属性 ref 也会在模板中自动解包,因此在模板表达式中引用时无需添加 .value计算属性值会基于其响应式依赖被缓存
  • computed特点:具有响应式的返回值;watch特点:侦测变化,执行回调;
  • computed是依赖已有的变量来计算一个目标变量,并且computed具有缓存机制,依赖值不变的情况下其会直接读取缓存进行复用,computed不能进行异步操作
  • watch是监听某一个变量的变化,并执行相应的回调函数,通常是一个变量的变化决定多个变量的变化,watch可以进行异步操作。watch可以传递对象,设置deep、immediate等选项。
组件通信常用方式
  • 父子组件,props/$emit/$parent/ref/$attrs
  • 兄弟组件,$parent/$root/eventbus(emit,emit,on,$off)/vuex
  • 跨层级关系,eventbus/vuex/provide+inject
provide和inject依赖注入
// 提供方
<script setup> 
  import { provide, ref } from 'vue'
  const location = ref(0)
  function updateLocation() { 
    location.value = 'South Pole' 
  }
  provide('location', { location, updateLocation }) 
</script>

// 注入方
<script setup> 
  import { inject } from 'vue' 
  const { location, updateLocation } = inject('location')
</script>
  • 一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。
  • 供的值是一个 ref,注入进来的会是该 ref 对象,这使得注入方组件能够通过 ref 对象保持了和供给方的响应性链接。
对象新属性无法更新视图,删除属性,arr[index] = xxx无法更新视图,为什么?
  • 原因:Object.defineProperty没有对对象的新属性和数组内的属性进行属性劫持。
  • Vue.set(obj, key, value);Vue.delete(obj, key);Vue.set(arr, index, value);
如果子组件改变props里的数据会发生什么
  • 如果修改的是基本类型,则会报错vue warning,prop 是只读的!
  • 当对象或数组作为props被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,而对 Vue 来说,禁止这样的改动,虽然可能生效,但有很大的性能损耗,比较得不偿失,并且父级数据会跟着变。改变引用类型地址也会报错。
自定义指令

image.png

image.png

<script setup> 
const vFocus = {
   // 在绑定元素的父组件及他自己的所有子节点都挂载完成后调用(binding传递给指令的值)
   mounted: (el, binding, vnode, prevVnode) => el.focus() 
   // 在绑定元素的父组件及他自己的所有子节点都更新后调用(binding传递给指令的值)
   updated: (el, binding, vnode, prevVnode) => {}
} 
</script> 

<template> <input v-focus /> </template>

// 将一个自定义指令全局注册到应用层级也是一种常见的做法
const app = createApp({}) 
// 使 v-focus 在所有组件中都可用 
app.directive('focus', { /* ... */ })
  • 在 <script setup> 中,任何以 v 开头的驼峰式命名的变量都可以被用作一个自定义指令,vFocus 即可以在模板中以 v-focus 的形式使用。
  • 在没有使用 <script setup> 的情况下,export default时自定义指令需要通过 directives 选项注册。
  • 将一个自定义指令全局注册,app.directive('focus', { /* ... */ });
  • 当在组件上使用自定义指令时,它会始终应用于组件的根节点,和透传attrs(传入当前组件的所有属性和v-on事件)类似,和 attribute 不同,指令不能通过 v-bind="$attrs" 来传递给一个不同的元素。
  • v-once是vue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。编译器发现元素上面有v-once时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算。
  • vue3.2之后,又增加了v-memo指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了。
Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做?
  • 页面权限,配置一个asyncRoutes数组,需要认证的页面在其路由的meta中添加一个roles字段,通过路由守卫要求用户登录后获取用户角色,根据角色过滤出路由表,通过router.addRoutes(asyncRoutes)方式动态添加路由即可。
  • 按钮权限实现一个指令,例如v-permission,将按钮要求角色通过值传给v-permission指令,在指令的moutned钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。
组合式函数
// fetch.js
import { ref, watchEffect, toValue } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)

  watchEffect(() => {
    // 在 fetch 之前重置状态
    data.value = null
    error.value = null
    // toValue() 将可能的 ref 或 getter 解包,规范化为值
    fetch(toValue(url))
      .then((res) => res.json())
      .then((json) => (data.value = json))
      .catch((err) => (error.value = err))
  })

  return { data, error }
}

<script setup> 
    import { ref, watchEffect, toValue } from 'vue'
    import { useFetch } from './fetch.js' 
    const url = ref('/initial-url') 
    const { data, error } = useFetch(url) 
    // 这将会重新触发fetch 
    url.value = '/new-url'
</script> 
  • 这个版本的 useFetch() 现在能接收静态 URL 字符串、ref 和 getter,使其更加灵活。watch effect 会立即运行,并且会跟踪 toValue(url) 期间访问的任何依赖项。如果没有跟踪到依赖项(例如 url 已经是字符串),则 effect 只会运行一次;否则,它将在跟踪到的任何依赖项更改时重新运行。
  • 每一个调用 useFetch() 的组件实例会创建其独有的 dataerror 状态拷贝,因此他们不会互相影响。如果你想要在组件之间共享状态,请阅读状态管理这一章。
  • 我们一直在组合式函数中使用 ref() 而不是 reactive()。我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性
  • 在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:onMounted()确保在组件挂载后才调用的生命周期钩子中执行 DOM 相关的副作用,确保在 onUnmounted() 时清理副作用。
  • 组合式函数只能在 <script setup> 或 setup() 钩子中被调用。在这些上下文中,它们也只能被同步调用。在某些情况下,你也可以在像 onMounted() 这样的生命周期钩子中调用它们。这些是 Vue 用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要,将生命周期钩子,1. 将计算属性和监听器注册到该组件实例上,以便在该组件被卸载时停止监听,避免内存泄漏。
  • <script setup> 是唯一在调用 await 之后仍可调用组合式函数的地方。编译器会在异步操作之后自动为你恢复当前的组件实例。
插件
// plugins/i18n.js
export default {
  install: (app, options) => {
  // 注入一个全局可用的 $translate() 方法
    app.config.globalProperties.$translate = (key) => {
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }
    // 插件中的 Provide / Inject
    app.provide('i18n', options)
  }
}
//
import { createApp } from 'vue' 
const app = createApp({})
import i18nPlugin from './plugins/i18n' 
app.use(i18nPlugin, {
    // options
    greetings: { hello: 'Bonjour!' } 
})
//
<h1>{{ $translate('greetings.hello') }}</h1>
  • 通过app.component()app.directive()注册一到多个全局组件或自定义指令。
  • 通过 app.provide()使一个资源可被注入进整个应用。
  • app.config.globalProperties中添加一些全局实例属性或方法
vue中如何扩展一个组件
  • 逻辑扩展有:mixins(数组可扩展多个对象)、extends(只能扩展单个对象,和混入发生冲突,该选项优先级较高);
  • 内容扩展有slots;插槽主要用于vue组件中的内容分发,要精确分发到不同位置可以使用具名插槽,如果要使用子组件中的数据可以使用作用域插槽。
  • 作为扩展,还可以说说vue3中新引入的composition api带来的变化
递归组件
<template>
  <li>
    <div> {{ model.name }}</div>
    <ul v-show="isOpen" v-if="isFolder">
      <!-- 注意这里:组件递归渲染了它自己 -->
      <TreeItem
        class="item"
        v-for="model in model.children"
        :model="model">
      </TreeItem>
    </ul>
  </li>
<script>
export default {
  name: 'TreeItem',
  // ...
}
</script>
异步组件
import { defineAsyncComponent } from 'vue'
// defineAsyncComponent定义异步组件
const AsyncComp = defineAsyncComponent(() => {
  // 加载函数返回Promise
  return new Promise((resolve, reject) => {
    // ...可以从服务器加载组件
    resolve(/* loaded component */)
  })
})
// 借助打包工具实现ES模块动态导入
const AsyncComp = defineAsyncComponent({
    loader: () => import('./components/MyComponent.vue'),
    loadingComponent: LoadingComponent, // 加载异步组件时使用的组件 
    delay: 200, // 展示加载组件前的延迟时间,默认为 200ms 
    errorComponent: ErrorComponent, // 加载失败后展示的组件 
    timeout: 3000
})
  • 不仅可以在路由切换时懒加载组件,还可以在页面组件中继续使用异步组件,从而实现更细的分割粒度。(首次加载页面组件不会加载异步组件,分开打包,分割应用为更小的块,并且在需要组件时再加载)
  • defineAsyncComponent定义了一个高阶组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容。
插槽的使用以及原理
  • 具名插槽,v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header> image.png
  • 作用域插槽,插槽的内容可能想要同时使用父组件域内和子组件域内的数据
  • 原理在插槽函数slot调用时传入 props,MyComponent类比成函数调用,接收参数,v-slot="slotProps" 可以类比这里的函数签名,和函数的参数类似。

image.png

透传 Attributes
  • “透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyle 和 id
  • 当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。
  • 如果一个子组件的根元素已经有了 class 或 style attribute,它会和从父组件上继承的值合并
  • v-on事件监听器click 监听器会被添加到 <MyButton> 的根元素,即那个原生的 <button> 元素之上。当原生的 <button> 被点击,会触发父组件的 onClick 方法。同样的,如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。
  • 禁用 Attributes 继承,件选项中设置 inheritAttrs: false。透传进来的 attribute 可以在模板的表达式中直接用 $attrs,这个 $attrs 对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 classstylev-on 监听器等等;在 <script setup> 中使用 useAttrs() API 来访问一个组件的所有透传 attribute。
Vue实例挂载的过程
  • 调用createApp方法之后会返回一个app对象,紧接着我们会调用mount方法将节点挂载到页面上。mount方法内createVnode根据编译后的.vue文件生成对应的虚拟节。
  • createVNode主要是对传递的type做出判断,通过赋值shapeFlag来标明当前的虚拟节点的类型。如果props含有style或者class要进行标准化。(编译后的template,在调用createVNode的时候传递的props就已经是经过处理的了)
  • render函数用于将createVnode生成的虚拟节点挂载到用户传入的container中。如果当前DOM实例已经挂载过了,那么需要先卸载挂载的节点、调用patch执行挂载流程、最后执行Vue的前置和后置调度器缓存的函数`。
  • 介绍patch函数之前我们同样介绍一个重要的标识符PatchFlags---靶向更新标识。在编译阶段会判断当前的节点是否包含动态的props、动态style、动态class、fragment是否稳定、当key属性是动态的时候需要全量比较props等。这样就可以在更新阶段判断patchFlag来实现靶向更新。比较特殊的有HOISTED:-1表示静态节点不需要diff(HMR的时候还是需要,用户可能手动直接改变静态节点)BAIL表示应该结束patch。判断beforeVNodecurrentVNodetype与key是否相等,如果不等,表示节点需要被卸载。例如:<div></div> => <p></p>节点发生了改变,需要被卸载。这里需要注意的是:Vue的diff进行的是同层同节点比较,type和key将作为新旧节点是否是同一个的判断标准。(设置ref属性。同时在更新阶段ref也需要被更新。)
  • mountComponent主要用于挂载组件createComponentInstance根据传递的虚拟节点创建组件实例,调用setupComponent函数进行初始化组件实例的插槽props,如果是有状态组件还需要处理setup的返回值。最后调用setupRenderEffect绑定副作用更新函数
Vue的生命周期,讲一讲?
  • setup:执行顺序在 beforeCreate钩子函数之前,是最早执行的,在程序运行中,setup函数只执行一次,创建的是data和method。setup中没有this
  • beforeCreate:通常用于插件开发中执行一些初始化任务。将开发者定义的配置项和Vue内部的配置项进行合并,初始化组件的自定义事件,定义createElement函数/初始化插槽。
  • created:初始化inject,初始化所有数据(props -> methods -> data -> computed -> watch),初始化provide,且初始化完毕,可以访问各种数据,获取接口数据等
  • beforeMount:寻找是否有挂载的节点el(el优先级高),根据render函数准备开始渲染页面/实例化渲染watcher
  • mounted:dom已创建,可用于获取访问数据和dom元素;访问子组件等。
  • beforeUpdate:此时view层还未更新,可用于获取更新前各种状态
  • updated:完成view层的更新,更新后,所有状态已是最新
  • beforeunmount:实例被销毁前调用,可用于一些定时器或订阅的取消
  • unmounted:销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器

image.png

父子组件生命周期顺序
  • 加载渲染过程,父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
  • 子组件更新过程:父beforeUpdate->子beforeUpdate->子updated->父updated
  • 父组件更新过程:父 beforeUpdate -> 父 updated
  • 销毁过程:父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
双向绑定使用和原理
  • vue中双向绑定是一个指令v-model,可以绑定一个响应式数据到视图,同时视图中变化能改变该值。默认情况下相当于:value@input
  • 通常在表单项上使用v-model,还可以在自定义组件上使用,表示某个值的输入和输出控制。v-model用在自定义组件上时又会有很大不同,vue3中它类似于sync修饰符(:visible.sync="isVisible"),最终展开的结果是:modelValue属性和@update:modelValue事件;vue3中我们甚至可以用参数形式指定多个不同的绑定,例如v-model:foo和v-model:bar,非常强大!
<UserName 
    v-model:first-name.capitalize="first" 
    v-model:last-name.uppercase="last" 
/>

// <UserName />组件内部
<script setup> 
  const props = defineProps({ 
    firstName: String, 
    lastName: String, 
    firstNameModifiers: { default: () => ({}) }, 
    lastNameModifiers: { default: () => ({}) } 
   })       
   defineEmits(['update:firstName', 'update:lastName']) 
   console.log(props.firstNameModifiers, props.lastNameModifiers) 
   // { capitalize: true }{ uppercase: true} 
</script>

  • v-model是一个指令,它的神奇魔法实际上是vue的编译器完成的。我做过测试,包含v-model的模板,转换为渲染函数之后,实际上还是是value属性的绑定以及input事件监听,事件回调函数中会做相应变量更新操作。编译器根据表单元素的不同会展开不同的DOM属性和事件对,比如text类型的input和textarea会展开为value和input事件;checkbox和radio类型的input会展开为checked和change事件;select用value作为属性,用change作为事件。

image.png

Vue响应式是怎么实现的
  • 数据劫持+观察者模式
  • MVVM框架中要解决的一个核心问题是连接数据层和视图层,通过数据驱动应用,数据变化,视图更新,要做到这点的就需要对数据做响应式处理
  • 对象内部通过 defineReactive 方法,使用 Object.defineProperty 将属性进行劫持(只会劫持已经存在的属性),数组则是通过重写数组方法来实现。当页面使用对应属性时,每个属性都闭包中dep属性,存放他所依赖的 watcher(依赖收集),当属性变化后会通知自己对应的 watcher 去更新(派发更新)。
  • 但在实际使用中也存在一些缺点:比如初始化时的递归遍历会造成性能损失;新增或删除属性时需要用户使用Vue.set/delete这样特殊的api才能生效;对于es6中新产生的Map、Set这些数据结构不支持等问题。
  • 为了解决这些问题,vue3重新编写了这一部分的实现:利用ES6的Proxy代理要响应化的数据
  • Proxy 只会代理对象的第一层判断当前 Reflect.get 的返回值是否为 Object,如果是则再通过 reactive 方法做代理, 这样就实现了深度观测。
  • 运行时 vs. 编译时响应性
    • Vue 的响应式系统基本是基于运行时的。追踪和触发都是在浏览器中运行时进行的。运行时响应性的优点是,它可以在没有构建步骤的情况下工作,另一方面,这使得它受到了 JavaScript 语法的制约,导致需要使用一些例如 Vue ref 这样的值的容器
    • 一些框架,如 Svelte,选择通过编译时实现响应性来克服这种限制。它对代码进行分析和转换,以模拟响应性。该编译步骤允许框架改变 JavaScript 本身的语义——例如,隐式地注入执行依赖性分析的代码,以及围绕对本地定义的变量的访问进行作用触发。需要构建步骤编译。
    • 信号 (signal)信号是与 Vue 中的 ref 相同的响应性基础类型。它是一个在访问时跟踪依赖、在变更时触发副作用的值容器。这个概念经常与细粒度订阅和更新的渲染模型一起讨论。由于使用了虚拟 DOM,Vue 目前依靠编译器来实现类似的优化。然而,我们也在探索一种新的受 Solid 启发的编译策略 (Vapor Mode),它不依赖于虚拟 DOM,而是更多地利用 Vue 的内置响应性系统。
说说你对虚拟 DOM 的理解
  • 将真实元素节点抽象成VNode的一个 JavaScript 对象,只不过它是通过不同的属性去描述一个视图结构,保存在内存中
  • 有两份虚拟 DOM 树,运行时渲染器将会有比较地遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为更新 (patch),又被称为“比对”(diffing) 或“协调”(reconciliation)。
  • 有效减少直接操作dom的次数,从而减少页面重绘和回流。
  • 可以渲染成不同平台上的对应的内容。
  • 组件模板template会被编译器compiler编译为渲染函数,在接下来的挂载(mount)过程中会调用render函数,返回的对象就是虚拟dom。但它们还不是真正的dom,所以会在后续的patch过程中进一步转化为dom。
  • 虚拟 DOM 在 React 和大多数其他实现中都是纯运行时的:更新算法无法预知新的虚拟 DOM 树会是怎样,因此它总是需要遍历整棵树、比较每个 vnode 上 props 的区别来确保正确性。
  • 带编译时信息的虚拟 DOM。在 Vue 中,框架同时控制着编译器和运行时。这使得我们可以为紧密耦合的模板渲染器应用许多编译时优化。编译器可以静态分析模板并在生成的代码中留下标记,使得运行时尽可能地走捷径。
    • 静态提升,不带任何动态绑定的静态内容,Vue 编译器自动地会提升这部分 vnode 创建函数到这个模板的渲染函数之外并在每次渲染时都使用这份相同的 vnode,会完全跳过对它们的差异比对。
    • 更新类型标记,运行时渲染器也将会使用来检查这些标记,Vue 能够在更新带有动态绑定的元素时做最少的操作。
    • 树结构打平,编译的结果会被打平为一个数组,仅包含所有动态的后代节。

image.png

Vue的模板编译原理
  • 模板预编译是构建工具,运行时编译是@vue/compiler-sfc
  • parse:接受 template 原始模板,按着模板的节点和数据生成对应的 ast
  • optimize:遍历 ast 的每一个节点,标记静态节点,这样就知道哪部分不会变化,于是在页面需要更新时,通过 diff 减少去对比这部分DOM,提升性能(静态提升,更新类型标记位运算,树结构打平)
  • generate 把前两步生成完善的 ast,组成 render 字符串,然后将 render 字符串通过 new Function 的方式转换成渲染函数
  • 渲染Watcher对象会通过调用updateComponent,_render函数会返回一个新的Vnode节点,传入_update中与旧的VNode对象进行对比,经过一个patch的过程得到两个VNode节点的差异,最后将这些差异渲染到真实环境形成视图。
Vue 组件挂载时
  • 编译:Vue 模板被编译为渲染函数:即用来返回虚拟 DOM 树的函数。这一步骤可以通过构建步骤提前完成,也可以通过使用运行时编译器即时完成。
  • 挂载:运行时渲染器调用渲染函数,遍历返回的虚拟 DOM 树,并基于它创建实际的 DOM 节点。这一步会作为响应式副作用执行,因此它会追踪其中所用到的所有响应式依赖。
  • 更新:当一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。

image.png

Vue的computed和watch的原理
  • computed原理

    • vue内部defineComputed是把computed属性定义在vm实例上的。computed是有缓存的,所以创建watcher的时候,会传一个配置{ lazy: true },同时也可以区分这是computedWatcher,lazy (dirty)配置可以让 Watcher 不会立即执行。
    • computedWatcher的特殊之处在于渲染watcher只能作为依赖被收集到其他的dep筐子里,而computedWatcher实例上有属于自己的dep,它可以收集别的watcher作为自己的依赖。惰性求值,初始化的时候先不去运行getter。
    • 当读取计算属性时,vue 检查其对应的 Watcher 是否是脏值,如果是,则运行函数,计算依赖,并得到对应的值,保存在 Watcher 的 value 中,然后设置 dirty 为 false,然后返回。如果 dirty 为 false,则直接返回 watcher 的 value
    • 模版中使用computed在读取value时,Dep.target肯定此时是正在运行的渲染函数的watcher。先把当前正在运行的渲染函数的watcher作为依赖收集到computedWatcher内部的dep筐子里。
    • 把自身computedWatcher设置为 全局Dep.target,然后开始求值函数执行() => data.number + 1,途中遇到data.number的读取,这时又会触发'number'这个key的劫持get函数,这时全局的Dep.target是computedWatcher,data.number的dep依赖筐子里丢进去了computedWatcher
    • 当计算属性的依赖变化时,会先触发计算属性的 Watcher 执行,此时,它只需设置 dirty 为 true 即可,不做任何处理。由于依赖同时会收集到组件的 Watcher,因此组件会重新渲染,而重新渲染时又读取到了计算属性,由于计算属性目前已为 dirty,因此会重新运行 getter 进行运算
    • 此时如果更新data.number的话,会一级一级往上触发更新。会触发computedWatcherupdatecomputedWatcherdep里装着渲染watcher,所以只需要触发 this.dep.notify(),就会触发渲染watcher的update方法,从而更新视图。
  • watch的原理

    • 依然是利用Watcher类去实现,我们把用于watch的watcher叫做watchWatcher,传入的getter函数也就是() => data.msgWatcher在执行它之前还是一样会把自身(也就是watchWatcher)设为Dep.target,这时读到data.msg,就会把watchWatcher丢进data.msg的依赖筐子里。
    • 如果data.msg更新了,则就会触发watchWatcherupdate方法。update方法中,在调用this.get去更新值之前,先把旧值保存起来,然后把新值和旧值一起通过调用callback函数交给外部。
nextTick的原理及vue异步更新
  • juejin.cn/post/684490…
  • 响应式触发setter->Dep->Watcher->update->patch
  • 当某个响应式数据发生变化的时候,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有Watch对象。触发Watch对象的update实现。update根据,lazy,sync执行不同的更新。默认使用异步执行DOM更新,queueWatcher(this)把当前watcherpush进观察者队列,此时状态处于waiting的状态,会继续会有Watch对象被push进这个队列queue,等待下一个tick时,这些Watch对象才会被遍历取出,更新视图。同时id重复的Watcher不会被多次加入到queue中去。
  • nextTick签名如下:function nextTick(callback?: () => void): Promise<void>
  • nextTick的原理,收集副作用cb存放到callbacks中;然后执行timerFunc(pending是一个状态标记,保证timerFunc在下一个tick之前只执行一次);timerFunc会检测当前环境而不同实现,其实就是按照Promise.resolve,MutationObserver(DOM变化时触发回调),setTimeout优先级(前两个方法都会在microtask中执行)遍历执行callbacks;
  • nextTick的目的就是产生一个回调函数加入task或者microtask中,当前栈执行完以后(可能中间还有别的排在前面的函数)调用该回调函数,起到了异步触发(即下一个tick时触发)的目的。
  • flushSchedulerQueue是nextTick时的回调函数,主要目的是执行queue中Watcher的run函数,用来更新视图。给queue去重排序,组件更新的顺序是从父组件到子组件的顺序,一个组件的user watchers比render watcher先运行。
怎么缓存当前的组件?缓存后怎么更新?
  • juejin.cn/post/684490…
  • keep-alive包裹动态组件component时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM。由于component的is属性是个响应式数据,因此只要它变化,keep-alive的render函数就会重新执行。<KeepAlive> 默认会缓存内部的所有组件实例,结合属性include和exclude可以明确指定缓存哪些组件或排除缓存指定组件,它会根据组件的name选项进行匹配。vue3中结合vue-router时变化较大
<router-view v-slot="{ Component }">
  <keep-alive :max="10">
    <component :is="Component"></component>
  </keep-alive>
</router-view>
  • 指定了 max 后类似一个LRU 缓存:如果缓存的实例数量即将超过指定的那个最大数量,则最久没有被访问的缓存实例将被销毁
  • 缓存后如果要获取数据,1.每次进入路由的时候,都会执行beforeRouteEnter;2.在keep-alive缓存的组件插入到 DOM 中被激活的时候,都会执行onActivated钩子,不会重新调用组件的created等方法,需要用onActivated与onDeactivated这两个生命钩子来得知当前组件是否处于活动状态。(onActivated 在组件挂载时也会调用,并且 onDeactivated 在组件卸载时也会调用。)
  • keep-alive原理,created钩子会创建一个cache对象,destroyed钩子则在组件被销毁的时候清除cache缓存中的所有组件实例vnode.componentInstance.$destroy();
  • render,getFirstComponentChild(this.$slots.default)得到slot插槽中的第一个组件的VNode,获取组件名称或tag,name不在inlcude中或者在exlude中则直接返回vnode(没有缓存) 如果已经做过缓存了则vnode.componentInstance = this.cache[key].componentInstance(LRU),还未缓存过则进行缓存vnode,this.cache[key] = vnode。
  • watch,监视include以及exclude,在被修改的时候对cache进行修正
  • LRU-使用map数据结构(先删除再设置),每当get和set操作map数据时,先判断map.has(key),map.delete(key),map.set(key, temp),当map.size大于固定长度时,删除前面多余的this.map.delete(this.map.keys().next().value)
vue性能优化技巧
  • 代码分割是指构建工具将构建后的 JavaScript 包拆分为多个较小的,可以按需或并行加载的文件。像 Rollup (Vite 就是基于它之上开发的) 或者 webpack 这样的打包工具可以通过分析 ESM 动态导入的语法来自动进行代码分割。
  • 路由懒加载
    • component选项配置一个返回 Promise 组件的函数就可以定义懒加载路由{ path: '/users/:id', component: () => import('./views/UserDetails') },Vue Router只会在第一次进入页面时才会获取这个函数。
    • 结合注释() => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')可以做webpack代码分块,vite中结合rollupOptions定义分块
  • 异步组件import { defineAsyncComponent } from 'vue'
  • 服务端渲染 (SSR),Vue 也支持将组件在服务端直接渲染成 HTML 字符串,作为服务端响应返回给浏览器,最后在浏览器端将静态的 HTML“激活”(hydrate) 为能够交互的客户端应用
    • 更快的首屏加载数据获取过程在首次访问时在服务端完成,可能有更快的数据库连接。服务端渲染的 HTML 无需等到所有的 JavaScript 都下载并执行完成之后才显示,所以你的用户将会更快地看到完整渲染的页面。
    • 更好的 SEO:搜索引擎爬虫可以直接看到完全渲染的页面。(爬虫并不会等到内容异步加载完成再抓取。)
    • 服务端渲染的应用需要一个能让 Node.js 服务器运行的环境,在 Node.js 中渲染一个完整的应用要比仅仅托管静态文件更加占用 CPU 资源,因此如果你预期有高流量,请为相应的服务器负载做好准备,并采用合理的缓存策略。
  • 包体积与 Tree-shaking 优化
    • 如果你根本没有使用到内置的 <Transition> 组件,它将不会被打包进入最终的产物里。Tree-shaking 也可以移除你源代码中其他未使用到的模块。
    • 使用了构建步骤,模板会被预编译,因此我们无须在浏览器中载入 Vue 编译器。这在同样最小化加上 gzip 优化下会相对缩小 14kb 并避免运行时的编译开销。
    • 使用了构建步骤,应当尽量选择提供 ES 模块格式的依赖,它们对 tree-shaking 更友好。举例来说,选择 lodash-es 比 lodash 更好。
  • 更新优化
    • 在 Vue 之中,一个子组件只会在其至少一个 props 改变时才会更新,让传给子组件的 props 尽量保持稳定,减少更新次数。
    • v-once,仅渲染元素和组件一次,并跳过之后的更新。在随后的重新渲染,元素/组件及其所有子项将被当作静态内容并跳过渲染。这可以用来优化更新时的性能。
    • v-memo,从 3.2 起,指定依赖值数组,如果数组里的每个值都与最后一次的渲染相同,这个元素和组件 及其子项的所有更新都将被跳过。实际上,甚至虚拟 DOM 的 vnode 创建也将被跳过。v-memo 传入空依赖数组 (v-memo="[]") 将与 v-once 效果相同。
    • 当搭配 v-for 使用 v-memo,确保两者都绑定在同一个元素上。v-memo 不能用在 v-for 内部。
  • 通用优化
    • 减少大型不可变数据的响应性开销,Vue 的响应性系统默认是深度的。通过使用 shallowRef() 和 shallowReactive() 来绕开深度响应。浅层式 API 创建的状态只在其顶层是响应式的。(推荐使用 Immer
    • 避免不必要的组件抽象,组件实例比普通 DOM 节点要昂贵得多,而且为了逻辑抽象创建太多组件实例将会导致性能损失。
vue-router原理
  • router-link默认生成一个a标签,设置to属性定义跳转path。原理阻止a标签默认事件,调用的是navigate方法,avigate内部依然调用router.push()。
  • router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系。原理上是在router-view组件内部判断当前router-view处于嵌套层级的深度,讲这个深度作为匹配组件数组matched的索引,获取对应渲染组件,渲染之。(默认0,加1之后传给后代,同时根据深度获取匹配路由)

image.png

  • vue-router有3个模式,其中history和hash更为常用。hash模式使用和部署简单,但是不会被搜索引擎处理,seo有问题;history模式则建议用在大部分web项目上,但是它要求应用在部署时做特殊配置,服务器需要做回退处理,否则会出现刷新页面404的问题。

  • 路由守卫有三个级别:全局beforeEach,路由独享beforeEnter,组件级beforeRouteEnter。原理用户的任何导航行为都会走navigate方法,内部有个guards队列,runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入catch流程取消原本导航。

  • 从头开始实现一个简单的路由

<script setup>
import { ref, computed } from 'vue'
import Home from './Home.vue'
import About from './About.vue'
import NotFound from './NotFound.vue'
const routes = {
  '/': Home,
  '/about': About
}
const currentPath = ref(window.location.hash)
window.addEventListener('hashchange', () => {
  currentPath.value = window.location.hash
})
const currentView = computed(() => {
  return routes[currentPath.value.slice(1) || '/'] || NotFound
})
</script>
<template>
  <a href="#/">Home</a> |
  <a href="#/about">About</a> |
  <a href="#/non-existent-path">Broken Link</a>
  <component :is="currentView" />
</template>
状态管理pinia原理
  • 事实上,Vue 的响应性系统与组件层是解耦的,这使得它非常灵活。
  • 请注意这里点击的处理函数使用了 store.increment(),带上了圆括号作为内联表达式调用,因为它并不是组件的方法,并且必须要以正确的 this 上下文来调用。
// store.js
import { reactive } from 'vue'

export const store = reactive({
  count: 0,
  increment() {
    this.count++
  }
})

// ComponentB.vue
<script setup> 
    import { store } from './store.js' 
</script>
<template>
  <button @click="store.increment()">
    From B: {{ store.count }}
  </button>
</template>

Vue3.0 新特性