抽丝剥茧带你复习vue源码(2023年面试版本)

10,009 阅读23分钟

image.png 提示:本文包含vue3版本的源码分析。写本文花了笔者将近1个月时间,建议小白收藏本文,有需要再拿出来看

为啥需要阅读vue源码

count.jpg

  1. 岗位需要。从这个HC可以看出,前端技术专家需要熟读源码
  2. 开发框架,需要开发框架或者库时,参考成熟的前端框架实现是有必要的
  3. 代码质量,vue作为一个优秀的开源库,学习它的设计思想和设计模式可以帮助我们写质量更高、性能更优的代码

Vue3新特性

组合式API(composition API)

为什么使用composition API

  • options API的组件,比如在A组件中定义了B/C组件的data,methods,生命周期方法,computed,各个逻辑分散在组件的不同区域,代码难以复用,使用composition API解决了这个问题,可以做到高内聚、低耦合,代码可复用性和可维护性更好
  • vue2逻辑复用使用的是mixins,当一个组件引用多个mixin时,想知道query来源于哪个mixin,需要在每个引用的mixin中寻找一遍方法,即数据来源不清晰;且多个mixin中定义的属性和方法会存在命名冲突问题;composition API解决了上面的问题
  • 更好的类型推断,对Typescript更友好
  • composition API看不到this的使用,解决了this指向不明的问题
mixins:[TabMixin,TableQueryMixin,GrayBackgroundMixin,BaseQueryMixin],
methods:{
  query(){
    ...
  }
}  

teleport

Teleport类似于React的Portal,可以将组件挂载在任何DOM节点上

<button @click="openToast">打开toast</button>
<!--挂载在id为dialog的节点上-->
<teleport to="#dialog">
  <div v-if="visible" class="toast-container">
    <div class="toast-msg">我是一个toast</div>
  </div>
</teleport>

Fragment

Fragment组件支持多个根节点,作用:减少标签层级,减少内存占用

<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

diff算法优化

createRenderer

生命周期变更

beforeDestory->beforeUnmount

destroyed->unmounted

更多变更详见vue3迁移指南

vue3性能提升是通过哪些方面实现的

  1. 响应式系统升级,从vue2的Object.defineProperty变为了vue3的proxy,原因:
  • proxy性能优于Object.defineProperty

此处存疑,实际上proxy性能比Object.defineProperty要差,那么为啥vue3还要用proxy?参考 thecodebarbarian.com/thoughts-on…

proxy本质是对某个属性的劫持,proxy可以监听数据的新增和删除,而Object.defineProperty做不到,只能监听属性的变化

  • 可以监听数组的索引和length属性
  • 可以监听动态属性的添加
  • 可以监听删除属性
  1. 编译优化,主要有:
  • diff算法优化

vue3相比vue2增加了静态标记,静态标记的作用是会标志为一个flag,下次发生变化的时候直接找该处进行比较;已经标记为静态节点的元素不会参与diff比较

// 静态类型枚举
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
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
  NEED_PATCH = 1 << 9,   // 512
  DYNAMIC_SLOTS = 1 << 10,  // 动态 solt
  HOISTED = -1,  // 特殊标志是负整数表示永远不会用作 diff
  BAIL = -2 // 一个特殊的标志,指代差异算法
}
  • 静态提升、树结构打平

vue3对不参与更新的元素做静态提升,只会被创建一次,在渲染时直接复用。 作用:减少重复节点的创建,节省内存开销

  • 事件监听缓存
  • SSR优化

更新类型标记和树结构打平都大大提升了 Vue SSR 激活的性能表现:

  • 单个元素的激活可以基于相应 vnode 的更新类型标记走更快的捷径。
  • 在激活时只有区块节点和其动态子节点需要被遍历,这在模板层面上实现更高效的部分激活。
  1. 源码体积优化,移除了一些不常用的API,再就是tree shaking,任何一个函数,比如ref、reactive,只有在用到的时候才进行打包,无用模块都被摇树优化,减少了打包代码体积
import {computed,ref} from "vue"
export default defineComponent({
  setup(props,context){
    const age = ref(18)
    let state = reactive({name:"lyllovelemon"})
    const readOnlyAge = computed(()=>age.value++)
    return {
      age,
      state,
      readOnlyAge
    }
  }
})  

虚拟DOM

什么是虚拟DOM?如何实现虚拟DOM

React和Vue都使用了虚拟DOM技术,虚拟DOM是对真实DOM的一层抽象,它是一颗js对象树,用对象的属性描述节点,最后通过渲染器(renderer)将虚拟DOM渲染为真实DOM。

VNode不依赖某一个平台,它可以是浏览器平台,也可以是node平台或者weex平台,这也为前后端同构提供了可能

为什么要使用虚拟DOM

一个真实的dom元素,包含的属性和方法是很多的。浏览器在渲染DOM时性能开销很大,频繁渲染DOM最直接的结果就是页面卡顿。

// 在控制台输出document
document    

而且浏览器每次收到DOM更新流程时会从头到尾执行一遍更新流程。当你在一次操作时,需要更新10个DOM节点,浏览器没这么智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程。

而通过VNode,同样更新10个DOM节点,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,避免大量的无谓计算。

浏览器执行js运算的速度 > 渲染DOM元素的速度

在vue框架中,渲染流程为:

数据改变 -> 虚拟DOM ->操作真实DOM-> 视图更新

虚拟DOM是怎么变为真实DOM的

每一次DOM更新流程,Vue会用patch函数,对新老节点进行判断,执行创建/销毁节点。通过patchVnode函数和diff算法(参考了snabbdom),新旧Vnode diff比较,只更新有差异的部分

在页面首次渲染的时候会调用patch创建新的VNode,不会进行深层次的比较

每个组件对应一个Watcher实例,当数据变化,会触发setter通过notify通知Watcher,对应的Watcher会通知更新并执行更新函数,它会执行render函数获取新的虚拟DOM,然后执行patch比较新旧Vnode,得到有差异的部分;最后根据有差异的部分更新视图

Diff算法执行过程

vue2版本

  1. 只比较同一层级,不跨层比较

这样的优点是减少了比较次数,算法的事件复杂度降低

  1. 比较标签名

如果同一层级的标签名type不同,直接删除老的VNode

  1. 比较key

如果标签名和key相同,代表新旧VNode是同一个节点

Diff算法核心:patch,sameVNode,patchVnode,updateChildren

patch源码位置(vue2):vue/blob/main/src/core/vdom/patch.ts

主要逻辑:

  1. vnode存在,oldVNode不存在,新增VNode节点
  2. vnode不存在,oldVNode存在,删除oldVNode节点
  3. 两个都存在,通过sameVnode函数判断是不是同一个节点,如果是,通过patchVnode进行后续对比;不是则把VNode挂载在oldVNode的父元素下,如果组件的根节点被替换,就遍历父节点删除旧节点;如果是服务端渲染就通过hydrating把oldVNode和真实DOM结合
//vue2版本: vue/blob/main/src/core/vdom/patch.ts    
function patchVnode(
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly?: any
) {
// 如果新旧节点一致,不做处理直接返回
if (oldVnode === vnode) {
  return
}

if (isDef(vnode.elm) && isDef(ownerArray)) {
  // clone reused vnode
  vnode = ownerArray[index] = cloneVNode(vnode)
}
// 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化
const elm = (vnode.elm = oldVnode.elm)
// 异步占位符
if (isTrue(oldVnode.isAsyncPlaceholder)) {
  if (isDef(vnode.asyncFactory.resolved)) {
    hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
  } else {
    vnode.isAsyncPlaceholder = true
  }
  return
}

// 如果新旧节点都是静态节点且新旧节点key相同(同一个节点)
// 当vnode是克隆节点且是v-once节点,只需要把oldVnode的组件实例赋给vnode节点
if (
  isTrue(vnode.isStatic) &&
  isTrue(oldVnode.isStatic) &&
  vnode.key === oldVnode.key &&
  (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
  vnode.componentInstance = oldVnode.componentInstance
  return
}

let i
const data = vnode.data
if (isDef(data) && isDef((i = data.hook)) && isDef((i = i.prepatch))) {
  i(oldVnode, vnode)
}

const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
  // patch比较新旧节点,更新有差异的部分
  for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
  if (isDef((i = data.hook)) && isDef((i = i.update))) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
  if (isDef(oldCh) && isDef(ch)) {
    if (oldCh !== ch)
      // 新旧节点都有子节点,则处理比较更新子节点
      updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
  } else if (isDef(ch)) {
    if (__DEV__) {
      // 新节点有子节点且在开发环境,检查重复的key
      checkDuplicateKeys(ch)
    }
    // 旧节点有文本属性,设置为空文本元素
    if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
    addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
  }
  // 旧的子节点存在而新的子节点不存在,删除旧节点
  else if (isDef(oldCh)) {
    removeVnodes(oldCh, 0, oldCh.length - 1)
  } else if (isDef(oldVnode.text)) {
    nodeOps.setTextContent(elm, '')
  }
} 
// 新旧节点文本内容不相同,直接插入新文本内容
else if (oldVnode.text !== vnode.text) {
  nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
  if (isDef((i = data.hook)) && isDef((i = i.postpatch))) i(oldVnode, vnode)
}
}

patchVnode主要做了几个判断:

  • 新节点是否是文本节点,如果是,则直接更新dom的文本内容为新节点的文本内容
  • 新节点和旧节点如果都有子节点,则处理比较更新子节点
  • 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新DOM,并且添加进父节点
  • 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把DOM 删除

再查看updateChildren方法

  function updateChildren(
    parentElm,
    oldCh,
    newCh,
    insertedVnodeQueue,
    removeOnly
  ) {
    // 定义了四个指针:旧前、新前、旧后、新后
    let oldStartIdx = 0
    let newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    // 旧前节点
    let oldStartVnode = oldCh[0]
    // 旧后节点
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    // 新前节点
    let newStartVnode = newCh[0]
    // 新后节点
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx, idxInOld, vnodeToMove, refElm

 
    const canMove = !removeOnly

    if (__DEV__) {
      checkDuplicateKeys(newCh)
    }

    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      // 首先应该不是判断四种命中,而是略过已经加了undefined标记的项
      if (isUndef(oldStartVnode)) {
        oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
      } else if (isUndef(oldEndVnode)) {
        oldEndVnode = oldCh[--oldEndIdx]
      } 
      // 1.旧前节点与新前节点命中
      else if (sameVnode(oldStartVnode, newStartVnode)) {
        // 精细化比较两个节点 oldStartVnode现在和newStartVnode一样了
        patchVnode(
          oldStartVnode,
          newStartVnode,
          insertedVnodeQueue,
          newCh,
          newStartIdx
        )
        // 移动指针,改变指针指向的节点,这表示这两个节点都处理(比较)完了
        oldStartVnode = oldCh[++oldStartIdx]
        newStartVnode = newCh[++newStartIdx]
      } 
      // 2.旧后与新后命中
      else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(
          oldEndVnode,
          newEndVnode,
          insertedVnodeQueue,
          newCh,
          newEndIdx
        )
        // 移动两个尾指针
        oldEndVnode = oldCh[--oldEndIdx]
        newEndVnode = newCh[--newEndIdx]
      } 
      // 3.旧前与新后命中
      else if (sameVnode(oldStartVnode, newEndVnode)) {
        // Vnode moved right
        patchVnode(
          oldStartVnode,
          newEndVnode,
          insertedVnodeQueue,
          newCh,
          newEndIdx
        )
       // 当新后与旧前命中的时候,此时要移动节点。移动 新后(旧前) 指向的这个节点到老节点的旧后的后面
      // 移动节点:只要插入一个已经在DOM树上 的节点,就会被移动
        canMove &&
          nodeOps.insertBefore(
            parentElm,
            oldStartVnode.elm,
            nodeOps.nextSibling(oldEndVnode.elm)
          )
        oldStartVnode = oldCh[++oldStartIdx]
        newEndVnode = newCh[--newEndIdx]
      } 
      // 4.旧后与新前命中
      else if (sameVnode(oldEndVnode, newStartVnode)) {
        // Vnode moved left
        patchVnode(
          oldEndVnode,
          newStartVnode,
          insertedVnodeQueue,
          newCh,
          newStartIdx
        )
       //当新前与旧后命中的时候,此时要移动节点。移动新前(旧后)指向的这个节点到老节点的 旧前的前面
      // 移动节点:只要插入一个已经在DOM树上的节点,就会被移动
        canMove &&
          nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
        oldEndVnode = oldCh[--oldEndIdx]
        newStartVnode = newCh[++newStartIdx]
      } else {
        if (isUndef(oldKeyToIdx))
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
        idxInOld = isDef(newStartVnode.key)
          ? oldKeyToIdx[newStartVnode.key]
          : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
        if (isUndef(idxInOld)) {
          // New element
          createElm(
            newStartVnode,
            insertedVnodeQueue,
            parentElm,
            oldStartVnode.elm,
            false,
            newCh,
            newStartIdx
          )
        } else {
          vnodeToMove = oldCh[idxInOld]
          if (sameVnode(vnodeToMove, newStartVnode)) {
            patchVnode(
              vnodeToMove,
              newStartVnode,
              insertedVnodeQueue,
              newCh,
              newStartIdx
            )
            oldCh[idxInOld] = undefined
            canMove &&
              nodeOps.insertBefore(
                parentElm,
                vnodeToMove.elm,
                oldStartVnode.elm
              )
          } else {
            // 四种都没有匹配到,key相同但元素不同,创建新元素
            createElm(
              newStartVnode,
              insertedVnodeQueue,
              parentElm,
              oldStartVnode.elm,
              false,
              newCh,
              newStartIdx
            )
          }
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
    // 指针移动结束,旧的开始指针>旧的结束指针,代表产生了新元素
    if (oldStartIdx > oldEndIdx) {
      refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
      // 节点新增
      addVnodes(
        parentElm,
        refElm,
        newCh,
        newStartIdx,
        newEndIdx,
        insertedVnodeQueue
      )
    } 
    // 遍历结束,新的开始指针 > 新的结束指针,代表有元素需要被删除
    else if (newStartIdx > newEndIdx) {
      // 节点删除
      removeVnodes(oldCh, oldStartIdx, oldEndIdx)
    }
  }

vue3版本的diff

  • vue2是全量diff,vue3是静态标记+非全量diff,节点被打上静态标记就不需要参与diff
  • 事件缓存,可以理解为事件为静态的,初始化后就会在更新时从缓存中查找事件
  • vue3使用最长递增子序列,主要在patchKeyedChildren函数里
  1. 头和头比
  2. 尾和尾比
  3. 基于最长递增子序列进行新增/更新(移动)/删除
  • 老的 children:[ a, b, c, d, e, f, g ]
  • 新的 children:[ 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 ],-1 是老数组里没有的就说明是新增
  4. 然后再拿取出数组里的最长递增子序列,也就是 [ 2, 3, 4 ] 对应的节点 [ c, d, e ]
  5. 然后只需要把其他剩余的节点,基于 [ c, d, e ] 的位置进行移动/新增/删除就可以了
//packages/runtime-core/src/renderer.ts     
  const patchKeyedChildren = (
    c1: VNode[],
    c2: VNodeArrayChildren,
    container: RendererElement,
    parentAnchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    let i = 0
    const l2 = c2.length
    let e1 = c1.length - 1 // prev ending index
    let e2 = l2 - 1 // next ending index

    // 1. sync from start
    // (a b) c
    // (a b) d e
    while (i <= e1 && i <= e2) {
      const n1 = c1[i]
      const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        break
      }
      i++
    }

    // 2. sync from end
    // a (b c)
    // d e (b c)
    while (i <= e1 && i <= e2) {
      const n1 = c1[e1]
      const n2 = (c2[e2] = optimized
        ? cloneIfMounted(c2[e2] as VNode)
        : normalizeVNode(c2[e2]))
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        break
      }
      e1--
      e2--
    }

    // 3. common sequence + mount
    // (a b)
    // (a b) c
    // i = 2, e1 = 1, e2 = 2
    // (a b)
    // c (a b)
    // i = 0, e1 = -1, e2 = 0
    if (i > e1) {
      if (i <= e2) {
        const nextPos = e2 + 1
        const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
        while (i <= e2) {
          patch(
            null,
            (c2[i] = optimized
              ? cloneIfMounted(c2[i] as VNode)
              : normalizeVNode(c2[i])),
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          i++
        }
      }
    }

    // 4. common sequence + unmount
    // (a b) c
    // (a b)
    // i = 2, e1 = 2, e2 = 1
    // a (b c)
    // (b c)
    // i = 0, e1 = 0, e2 = -1
    else if (i > e2) {
      while (i <= e1) {
        unmount(c1[i], parentComponent, parentSuspense, true)
        i++
      }
    }

    // 5. unknown sequence
    // [i ... e1 + 1]: a b [c d e] f g
    // [i ... e2 + 1]: a b [e d c h] f g
    // i = 2, e1 = 4, e2 = 5
    else {
      const s1 = i // prev starting index
      const s2 = i // next starting index

      // 5.1 build key:index map for newChildren
      const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
      for (i = s2; i <= e2; i++) {
        const nextChild = (c2[i] = optimized
          ? cloneIfMounted(c2[i] as VNode)
          : normalizeVNode(c2[i]))
        if (nextChild.key != null) {
          if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
            warn(
              `Duplicate keys found during update:`,
              JSON.stringify(nextChild.key),
              `Make sure keys are unique.`
            )
          }
          keyToNewIndexMap.set(nextChild.key, i)
        }
      }

      // 5.2 loop through old children left to be patched and try to patch
      // matching nodes & remove nodes that are no longer present
      let j
      let patched = 0
      const toBePatched = e2 - s2 + 1
      let moved = false
      // used to track whether any node has moved
      let maxNewIndexSoFar = 0
      // works as Map<newIndex, oldIndex>
      // Note that oldIndex is offset by +1
      // and oldIndex = 0 is a special value indicating the new node has
      // no corresponding old node.
      // used for determining longest stable subsequence
      const newIndexToOldIndexMap = new Array(toBePatched)
      for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0

      for (i = s1; i <= e1; i++) {
        const prevChild = c1[i]
        if (patched >= toBePatched) {
          // all new children have been patched so this can only be a removal
          unmount(prevChild, parentComponent, parentSuspense, true)
          continue
        }
        let newIndex
        if (prevChild.key != null) {
          newIndex = keyToNewIndexMap.get(prevChild.key)
        } else {
          // key-less node, try to locate a key-less node of the same type
          for (j = s2; j <= e2; j++) {
            if (
              newIndexToOldIndexMap[j - s2] === 0 &&
              isSameVNodeType(prevChild, c2[j] as VNode)
            ) {
              newIndex = j
              break
            }
          }
        }
        if (newIndex === undefined) {
          unmount(prevChild, parentComponent, parentSuspense, true)
        } else {
          newIndexToOldIndexMap[newIndex - s2] = i + 1
          if (newIndex >= maxNewIndexSoFar) {
            maxNewIndexSoFar = newIndex
          } else {
            moved = true
          }
          patch(
            prevChild,
            c2[newIndex] as VNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          patched++
        }
      }

      // 5.3 move and mount
      // generate longest stable subsequence only when nodes have moved
      const increasingNewIndexSequence = moved
        ? getSequence(newIndexToOldIndexMap)
        : EMPTY_ARR
      j = increasingNewIndexSequence.length - 1
      // looping backwards so that we can use last patched node as anchor
      for (i = toBePatched - 1; i >= 0; i--) {
        const nextIndex = s2 + i
        const nextChild = c2[nextIndex] as VNode
        const anchor =
          nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
        if (newIndexToOldIndexMap[i] === 0) {
          // mount new
          patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (moved) {
          // move if:
          // There is no stable subsequence (e.g. a reverse)
          // OR current node is not among the stable sequence
          if (j < 0 || i !== increasingNewIndexSequence[j]) {
            move(nextChild, container, anchor, MoveType.REORDER)
          } else {
            j--
          }
        }
      }
    }
  }

组件是怎么渲染成DOM的

template->render函数->虚拟DOM-> 真实DOM

源码解析

 // vue2版本源码位置:src/core/vdom/vnode.js
export default class VNode {
  tag: string | void; /*当前节点的标签名*/
  data: VNodeData | void;  /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
  children: ?Array<VNode>; /*当前节点的子节点,是一个数组*/
  text: string | void; /*当前节点的文本*/
  elm: Node | void; /*当前虚拟节点对应的真实dom节点*/
  ns: string | void; /*当前节点的命名空间*/
  context: Component | void; /*编译作用域*/
  key: string | number | void;/*节点的key属性,被当作节点的标志,用以优化*/
  functionalContext: Component | void;/*函数化组件作用域*/
  componentOptions: VNodeComponentOptions | void;/*组件的option选项*/
  componentInstance: Component | void; /*当前节点对应的组件的实例*/
  parent: VNode | void; /*当前节点的父节点*/
  raw: boolean; /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
  isStatic: boolean; /*是否静态节点*/
  isRootInsert: boolean;/*是否作为根节点插入*/
  isComment: boolean; /*是否为注释节点*/
  isCloned: boolean; /*是否为克隆节点*/
  isOnce: boolean;/*是否有v-once指令*/
  constructor (
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions)
  {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.functionalContext = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false}
  }
  get child (): Component | void {
      return this.componentInstance
  }
}

举个例子,当前我有一颗VNode树,结构如下:

{
    tag: 'div'
    data: {
        class: 'root'
    },
    children: [
        {
            tag: 'span',
            data: {
                class: 'demo'
            }
            text: 'hello,lyllovelemon'
        }
    ]
}

渲染后变为:

<div class="root">
    <span class="demo">hello,lyllovelemon</span>
</div>
 //vue3版本源码位置:core/packages/runtime-core/src/vnode.ts(3.2.37版本)   
...
export interface VNode<
  HostNode = RendererNode,
  HostElement = RendererElement,
  ExtraProps = { [key: string]: any }
  > {
  // vnode节点标记,判断是否是vnode节点
  __v_isVNode: true
  // 响应式flag标记
  [ReactiveFlags.SKIP]: true
  // 节点类型:vnode节点
  type: VNodeTypes
  // 节点props属性
  props: (VNodeProps & ExtraProps) | null
  // 节点的key属性
  key: string | number | symbol | null
  // 节点的ref属性
  ref: VNodeNormalizedRef | null
  /**
   * SFC only. This is assigned on vnode creation using currentScopeId
   * which is set alongside currentRenderingInstance.
   */
  scopeId: string | null
  /**
   * SFC only. This is assigned to:
   * - Slot fragment vnodes with :slotted SFC styles.
   * - Component vnodes (during patch/hydration) so that its root node can
   *   inherit the component's slotScopeIds
   * @internal
   */
  slotScopeIds: string[] | null

  children: VNodeNormalizedChildren // 当前节点的子节点

  component: ComponentInternalInstance | null   // 当前节点的组件实例
  dirs: DirectiveBinding[] | null
  transition: TransitionHooks<HostElement> | null


  el: HostNode | null // DOM
  anchor: HostNode | null // fragment锚点
  target: HostElement | null // teleport目标元素
  targetAnchor: HostNode | null // teleport目标锚点
  staticCount: number // 静态vnode节点个数
  suspense: SuspenseBoundary | null// suspense
  ssContent: VNode | null
  ssFallback: VNode | null

  // 仅用于optimization
  shapeFlag: number
  patchFlag: number
  dynamicProps: string[] | null // 动态props
  dynamicChildren: VNode[] | null// 动态子节点,需要进行patch

  appContext: AppContext | null // 仅用于应用根节点
  memo?: any[] // v-memo
  isCompatRoot?: true // 仅用于compact
  ce?: (instance: ComponentInternalInstance) => void // 内部自定义元素的拦截hook
}

创建虚拟DOM

function _createVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,// 节点类型
  props: (Data & VNodeProps) | null = null, // 节点props属性
  children: unknown = null, // 子元素
  patchFlag: number = 0, // patch标志
  dynamicProps: string[] | null = null, // 动态props属性
  isBlockNode = false // 是否为区块节点,区块节点指内部结构稳定的节点
): VNode {
  if (!type || type === NULL_DYNAMIC_COMPONENT) {
    if (__DEV__ && !type) {
      warn(`Invalid vnode type when creating vnode: ${type}.`)
    }
    type = Comment // 注释节点
  }

  if (isVNode(type)) {
    // 创建一个克隆的虚拟DOM,接收3个参数,分别为节点类型、节点props属性、是否合并ref属性
    const cloned = cloneVNode(type, props, true /* mergeRef: true */)
    if (children) {
      normalizeChildren(cloned, children)// 存在子节点,则建立当前虚拟节点与子节点的联系
    }
    // 不为区块节点且当前的节点存在
    if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
      if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
        currentBlock[currentBlock.indexOf(type)] = cloned
      } else {
        currentBlock.push(cloned)
      }
    }
    cloned.patchFlag |= PatchFlags.BAIL
    return cloned
  }

  // class component normalization.
  if (isClassComponent(type)) {
    type = type.__vccOpts
  }

  // 2.x async/functional component compat
  if (__COMPAT__) {type = convertLegacyComponent(type, currentRenderingInstance) }
  // class & style normalization.
  if (props) {
    // 用于对象的响应式或代理, we need to clone it to enable mutation.
    props = guardReactiveProps(props)!
    let { class: klass, style } = props
    if (klass && !isString(klass)) {
      props.class = normalizeClass(klass) // 标准化class,每一项class都转为字符串处理
    }
    if (isObject(style)) {
      // reactive state objects need to be cloned since they are likely to be
      // mutated
      if (isProxy(style) && !isArray(style)) {
        style = extend({}, style)
      }
      props.style = normalizeStyle(style)// 标准化style
    }
  }

  // encode the vnode type information into a bitmap
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
    ? ShapeFlags.SUSPENSE
    : isTeleport(type)
    ? ShapeFlags.TELEPORT
    : isObject(type)
    ? ShapeFlags.STATEFUL_COMPONENT
    : isFunction(type)
    ? ShapeFlags.FUNCTIONAL_COMPONENT
    : 0

  if (__DEV__ && shapeFlag & ShapeFlags.STATEFUL_COMPONENT && isProxy(type)) {
    type = toRaw(type)
    warn(
      `Vue received a Component which was made a reactive object. This can ` +
        `lead to unnecessary performance overhead, and should be avoided by ` +
        `marking the component with `markRaw` or using `shallowRef` ` +
        `instead of `ref`.`,
      `\nComponent that was made reactive: `,
      type
    )
  }

  return createBaseVNode(
    type,
    props,
    children,
    patchFlag,
    dynamicProps,
    shapeFlag,
    isBlockNode,
    true
  )

树结构打平

这里我们引入一个概念“区块”,内部结构是稳定的一个部分可被称之为一个区块。在这个用例中,整个模板只有一个区块,因为这里没有用到任何结构性指令 (比如 v-if 或者 v-for)。

每一个块都会追踪其所有带更新类型标记的后代节点 (不只是直接子节点),举例来说:

<div> <!-- root block -->
  <div>...</div>         <!-- 不会追踪 -->
  <div :id="id"></div>   <!-- 要追踪 -->
  <div>                  <!-- 不会追踪 -->
    <div>{{ bar }}</div> <!-- 要追踪 -->
  </div>
</div>

编译的结果会打平为一个数组,仅包含所有动态的后代节点dynamicChildren

div (block root)
- div 带有 :id 绑定
- div 带有 {{ bar }} 绑定

当组件需要重渲染时,只需要遍历这个打平的树而不是整棵树,这个过程叫树结构打平,大大减少了虚拟DOM需要遍历的节点数量,模板中任何静态的部分都会被跳过

依赖收集与响应式原理

什么是响应式

数据变化驱动视图更新就叫响应式

vue2的响应式是通过Object.defineProperty实现的,有以下几个问题:

  1. 不能深层监听对象的变化
  2. 不能获取数组下标和length
let val='lyllovelemon'
Object.defineProperty(obj,'name',{
  configurable:true,// 属性是否可配置,默认为false,为true时对应属性可被删除和修改,并且可以通过Object.defineProperty修改
  enumerable:true,// 属性是否可枚举,默认为false,为true是属性可被for...in或者Object.keys遍历
  writable:true,// 属性是否可写,默认为false,为true时属性值才可改变 
  value:'lyllovelemon',// 属性的初始值,可以是任何有效的JavaScript值(数值、对象、函数等),默认为undefined
  get(){
    return val
  },// 属性读取
  set(newVal){
    val = newVal
  }// 属性写入
})
console.log(obj.name)// 'lyllovelemon',表示属性的value已生效
obj.name='lyl'
console.log(obj.name)// 'lyl',getter和setter都已生效

vue3使用ES6的proxy实现响应式,proxy是支持数组的,因此解决了无法获取数组下标和length的问题;对于深层监听也不需要使用递归解决,当get判断值为对象时,将对象响应式处理即可

原理实现

vue3源码位置:packages/reactivity/src/reactive.ts

export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
  // 监听元素为只读的,不做处理返回
  if (isReadonly(target)) {
    return target
  }
  // 否则通过createReactiveObject处理
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}

响应式实际是在createReactiveObject函数中处理的,该函数主要做了几件事:

  • 判断监听
  • 实例化proxy对象
function createReactiveObject(
  target: Target, // 监听的元素
  isReadonly: boolean, // 是否只读
  baseHandlers: ProxyHandler<any>, // proxy处理函数
  collectionHandlers: ProxyHandler<any>,// 收集变化函数
  proxyMap: WeakMap<Target, any> // proxyMap 
) {
  // 监听元素不是对象则返回该元素
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // 监听元素是原生且不是只读元素,是响应式的则返回该元素
  if (
    target[ReactiveFlags.RAW] &&
    !(isReadonly && target[ReactiveFlags.IS_REACTIVE])
  ) {
    return target
  }
  // 监听元素已经被Proxy处理过了,从proxyMap中查找该元素并返回
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  // 获取元素类型
  const targetType = getTargetType(target)
  // 监听元素为基本数据类型则返回该元素
  if (targetType === TargetType.INVALID) {
    return target
  }
  // new实例化proxy对象
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  // 将监听元素加入到proxyMap并返回监听元素
  proxyMap.set(target, proxy)
  return proxy
}

getTargetType函数主要用于处理元素类型

function getTargetType(value: Target) {
  return value[ReactiveFlags.SKIP] || !Object.isExtensible(value)
    ? TargetType.INVALID
    : targetTypeMap(toRawType(value))
}
//TargetType常量定义,基本数据类型为INVALID,object/array为COMMON
// Map/Set/WeakMap/WeakSet为COLLECTION
const enum TargetType {
  INVALID = 0,
  COMMON = 1,
  COLLECTION = 2
}

function targetTypeMap(rawType: string) {
  switch (rawType) {
    case 'Object':
    case 'Array':
      return TargetType.COMMON
    case 'Map':
    case 'Set':
    case 'WeakMap':
    case 'WeakSet':
      return TargetType.COLLECTION
    default:
      return TargetType.INVALID
  }
}

事件机制原理

Vue事件机制API

vue提供了四个事件机制API,分别是:on,on,off,emit,emit,once

初始化事件

vue2版本源码位置:vue/blob/main/src/core/instance/events.ts

核心方法为initEvents,主要作用:

  • 初始化events和hasHookEvent变量
  • 父组件绑定的事件存在就调用updateComponentListeners,这个函数里调用updateListeners,接收新旧事件对象作为参数,遍历新事件对象,标准化事件,事件绑定了once就调用对应逻辑;旧事件没有新事件有,创建事件对象,旧事件有新事件没有,删除对应旧事件;新旧事件不一致使用新事件
export function initEvents(vm: Component) {
  // 给vm实例绑定_events属性,值为null
  vm._events = Object.create(null)
  // 给vm实例绑定_hasHookEvent实例,标志是否存在钩子
  vm._hasHookEvent = false
  // 初始化父组件绑定的事件
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

$on

on用于自定义一个事件,通过on用于自定义一个事件,通过emit触发

// 接收2个参数,分别为事件名(事件数组),需要执行的函数
Vue.prototype.$on = function (
    event: string | Array<string>,
    fn: Function
  ): Component {
    const vm: Component = this
    // 如果events是数组,遍历执行,给每一项事件绑定fn函数
    if (isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      // events是单个,建立一个空数组,将fn函数push进数组
      ;(vm._events[event] || (vm._events[event] = [])).push(fn)
      // hook性能优化专用
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

$off

$off用于移除自定义事件

//接收2个可选参数,分别为事件名(事件数组),需要执行的函数
Vue.prototype.$off = function (
    event?: string | Array<string>,
    fn?: Function
  ): Component {
    const vm: Component = this
    // off函数未传参,将事件置空,返回vm实例
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // events是数组,遍历每一项解除函数绑定,返回vm实例
    if (isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    const cbs = vm._events[event!]
    if (!cbs) {
      return vm
    }
    // fn未传参,清空events数组
    if (!fn) {
      vm._events[event!] = null
      return vm
    }
    // 特别处理cbs,遍历每一项移除
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

$emit

$emit触发自定义事件

 Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (__DEV__) {
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
            `${formatComponentName(
              vm
            )} but the handler is registered for "${event}". ` +
            `Note that HTML attributes are case-insensitive and you cannot use ` +
            `v-on to listen to camelCase events when using in-DOM templates. ` +
            `You should probably use "${hyphenate(
              event
            )}" instead of "${event}".`
        )
      }
    }
    // 从vm的_events属性中获取cbs
    let cbs = vm._events[event]
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      // cbs类数组转数组
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      // 遍历cbs每一项调用invokeWithErrorHandling,向上冒泡捕获错误
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }

$once

$once自定义一个只执行一次的事件,执行过后自动移除该事件

// 接收两个参数,分别为事件名和需要执行的函数
Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    // 内部定义on方法,做2件事:
    // 1.在第1次执行的时候将事件销毁
   // 2. 将实例绑定到fn函数
    function on() {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

事件缓存

vue3在vue2的基础上增加了事件监听缓存

模板编译原理

template函数怎么编译成render函数的

vue2版本创建vue实例时,组件通过 _init方法进行初始化, 这个方法主要用于初始化data,method,生命周期等属性,最后调用$mount进行实例挂载

 //vue2.7.10源码地址:src/core/instance/init.ts
export function initMixin(Vue: typeof Component) {
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to mark this as a Vue instance without having to do instanceof
    // check
    vm._isVue = true
    // avoid instances from being observed
    vm.__v_skip = true
    // effect scope
    vm._scope = new EffectScope(true /* detached */)
    vm._scope._vm = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options as any)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (__DEV__ && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    if (vm.$options.el) {
      vm.$mount(vm.$options.el) // 实例挂载
    }
  }
}                                        

查看$mount方法,render函数不存在,将template通过compilerToFunctions方法编译成得到AST,render和staticRenderFns(vue的编译优化,静态节点不需要patch),render函数在运行时会返回虚拟DOM 核心方法就是compileToFunctions

//src/platforms/web/runtime-with-compiler.ts
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    __DEV__ &&
      warn(
        `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
      )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (__DEV__ && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (__DEV__) {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // @ts-expect-error
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (__DEV__ && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(
        template,
        {
          outputSourceRange: __DEV__,
          shouldDecodeNewlines,
          shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments
        },
        this
      )
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (__DEV__ && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

compileToFunction是在createCompiler方法中被调用的,createCompiler调用了parse进行模板编译

//src/platforms/web/compiler/index.ts
const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

//src/platforms/web/compiler/index.ts
export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)//1.parse模板解析
  if (options.optimize !== false) {
    optimize(ast, options)// 2.optimize 优化
  }
  const code = generate(ast, options)//3.generate 代码生成
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

template -> AST -> render

  1. parse方法主要用于处理template,通过正则表达式解析template模板中的指令、class、style等数据,生成AST
  2. optimize主要用于标记static静态节点和静态根节点,这是vue编译时进行的优化,优点是只有在挂载的时候生成,静态节点不会参与后续的diff,也就是说视图更新时,会跳过静态节点的比较,减少了节点的比较次数,diff性能得到了提升
  3. generate,主要用于将AST转换为render function字符串,得到render字符串和staticRenderFns字符串
export function generate(
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast
    ? ast.tag === 'script'
      ? 'null'
      : genElement(ast, state)
    : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

Transition实现原理

Transition组件基本使用

Transition组件是vue提供的内置组件,用于制作状态变化的动画

会在组件进入或离开DOM时应用动画

<Transition name="fade" mode="out-in">
  <p v-if="show">hello,lyllovelemon</p>
</Transition>
  
<style  scoped>
 .fade-enter-active,.fade-leave-active{
     transition:opacity 0.5s ease;
 }
 .fade-enter-from,.fade-leave-to{
     opacity: 0;
 }
</style>    

Transition组件插槽只支持单个元素,当插槽内容时组件时,必须确保组件只有一个根元素,否则会报错 expects exactly one child element or component.

TransitionGroup

用于v-for列表元素、组件的插入、移除、改变添加动画效果

<template>
        <button @click="addItem">任意位置添加一项</button>
        <button @click="reset">重置</button>
        <button @click="shuffleItem">打乱</button>
        <TransitionGroup name="list" tag="ul">
            <li v-for="item in list" :key="item">{{item}}
                <button @click="remove(item)">删除</button>
            </li>
        </TransitionGroup>
</template>
<script>
    import {ref,defineComponent} from 'vue'
    import {shuffle} from 'lodash-es'
    export default defineComponent({
        setup(){
            const getInitialItems=()=>[1,2,3,4,5]
            const list = ref(getInitialItems())
            let id = list.value.length + 1
            const addItem=()=>{
                const i = Math.round(Math.random()*list.value.length)
                list.value.splice(i,0,id++)
            }
            const reset=()=>{
                list.value=getInitialItems()
            }
            const shuffleItem=()=>{
                list.value = shuffle(list.value)
            }
            const remove=(item)=>{
                const index = list.value.indexOf(item)
                index>-1 && list.value.splice(index,1)
            }
            return{
                list,
                shuffleItem,
                addItem,
                reset,
                remove
            }
        }
    })

</script>
<style  scoped>
  // 声明过渡效果
 .list-move,
 .list-enter-active,
 .list-leave-active {
    transition: all 0.5s ease;
 }
 // 声明进入和离开的状态 
 .list-enter-from,
 .list-leave-to {
    opacity: 0;
    transform: translateX(30px);
 }
// 确保离开的项被移出了布局流,以便正确计算移动时的动画效果  
.list-leave-active{
    position: absolute;
}
</style>

Transition组件实现原理

源码位置(vue3):packages/runtime-dom/src/components/Transition.ts

原理也很简单,Transition既然是vue组件,当然也会经历虚拟DOM转换为真实DOM。Transtion组件的特殊之处是为渲染子节点的VNode添加key属性,然后在子节点的VNode下添加transition属性,表示这是个transition组件渲染的VNode,然后在转换为真实DOM的时候特殊处理

// 导出Transition组件常量,它是一个函数组件类型
// 接收参数分别为props,slots
// 返回一个h函数
export const Transition: FunctionalComponent<TransitionProps> = (
  props,
  { slots }
) => h(BaseTransition, resolveTransitionProps(props), slots)

// TransitionProps是一个接口类型
export interface TransitionProps extends BaseTransitionProps<Element> {
  // 过渡效果命名,用于基于css的过渡效果
  name?: string
  // 类型,TRANSITION或者ANIMATION
  type?: typeof TRANSITION | typeof ANIMATION
  // 是否支持css过渡效果
  css?: boolean
  // 动画持续时长,接收数值类型,单位毫秒;也可以单独定义进入/离开动画的持续时长
  duration?: number | { enter: number; leave: number }
  // 自定义transition classes,相比vue2的6个增加了3个
  enterFromClass?: string
  enterActiveClass?: string
  enterToClass?: string
  appearFromClass?: string
  appearActiveClass?: string
  appearToClass?: string
  leaveFromClass?: string
  leaveActiveClass?: string
  leaveToClass?: string
}

查看TransitionProps继承的BaseTransitionProps,这个接口主要定义了mode和一些JavaScript钩子

// packages/runtime-core/src/components/BaseTransition.ts    
export interface BaseTransitionProps<HostElement = RendererElement> {
  // 过渡模式,支持in-out/out-in/default
  mode?: 'in-out' | 'out-in' | 'default'
  // 是否首次渲染
  appear?: boolean
  // 是否通过自定义指令(v-show等)控制
  // If true, indicates this is a transition that doesn't actually insert/remove
  // the element, but toggles the show / hidden status instead.
  // The transition hooks are injected, but will be skipped by the renderer.
  // Instead, a custom directive can control the transition by calling the
  // injected hooks (e.g. v-show).
  persisted?: boolean

  // 进入DOM的JavaScript钩子,可以通过@before-enter="xxx"形式调用 
  onBeforeEnter?: Hook<(el: HostElement) => void>
  onEnter?: Hook<(el: HostElement, done: () => void) => void>
  onAfterEnter?: Hook<(el: HostElement) => void>
  onEnterCancelled?: Hook<(el: HostElement) => void>
  // leave Javascript钩子
  onBeforeLeave?: Hook<(el: HostElement) => void>
  onLeave?: Hook<(el: HostElement, done: () => void) => void>
  onAfterLeave?: Hook<(el: HostElement) => void>
  onLeaveCancelled?: Hook<(el: HostElement) => void> // only fired in persisted mode
  // appear Javascript钩子
  onBeforeAppear?: Hook<(el: HostElement) => void>
  onAppear?: Hook<(el: HostElement, done: () => void) => void>
  onAfterAppear?: Hook<(el: HostElement) => void>
  onAppearCancelled?: Hook<(el: HostElement) => void>
}

由此Transition组件的props已经清晰,再回到transition对应的源码位置,核心方法为resolveTransitionProps

// Transition返回了一个h函数,接收参数分别是Transition的props,需要执行的函数,插槽
export const Transition: FunctionalComponent<TransitionProps> = (
  props,
  { slots }
) => h(BaseTransition, resolveTransitionProps(props), slots)

export function resolveTransitionProps(
  rawProps: TransitionProps
): BaseTransitionProps<Element> {
  const baseProps: BaseTransitionProps<Element> = {}
  // 遍历Transition组件的每一项props,没有定义在DOMTransitionPropsValidators中的属性一律转换为any类型
  for (const key in rawProps) {
    if (!(key in DOMTransitionPropsValidators)) {
      ;(baseProps as any)[key] = (rawProps as any)[key]
    }
  }
  // css属性为false,直接返回baseProps
  if (rawProps.css === false) {
    return baseProps
  }

  // 解构一些基本的属性,包括9个自定义class,name未赋值定义为v
  const {
    name = 'v',
    type,
    duration,
    enterFromClass = `${name}-enter-from`,
    enterActiveClass = `${name}-enter-active`,
    enterToClass = `${name}-enter-to`,
    appearFromClass = enterFromClass,
    appearActiveClass = enterActiveClass,
    appearToClass = enterToClass,
    leaveFromClass = `${name}-leave-from`,
    leaveActiveClass = `${name}-leave-active`,
    leaveToClass = `${name}-leave-to`
  } = rawProps

  // 分别处理enter,appear,leave三种情况的class
  const legacyClassEnabled =
    __COMPAT__ &&
    compatUtils.isCompatEnabled(DeprecationTypes.TRANSITION_CLASSES, null)
  let legacyEnterFromClass: string
  let legacyAppearFromClass: string
  let legacyLeaveFromClass: string
  if (__COMPAT__ && legacyClassEnabled) {
    const toLegacyClass = (cls: string) => cls.replace(/-from$/, '')
    if (!rawProps.enterFromClass) {
      legacyEnterFromClass = toLegacyClass(enterFromClass)
    }
    if (!rawProps.appearFromClass) {
      legacyAppearFromClass = toLegacyClass(appearFromClass)
    }
    if (!rawProps.leaveFromClass) {
      legacyLeaveFromClass = toLegacyClass(leaveFromClass)
    }
  }

  // 标准化动画持续时长,拿到进入/离开DOM持续时长
  const durations = normalizeDuration(duration)
  const enterDuration = durations && durations[0]
  const leaveDuration = durations && durations[1]
  // 解构拿到JavaScript钩子方法
  const {
    onBeforeEnter,
    onEnter,
    onEnterCancelled,
    onLeave,
    onLeaveCancelled,
    onBeforeAppear = onBeforeEnter,
    onAppear = onEnter,
    onAppearCancelled = onEnterCancelled
  } = baseProps

  // 处理进入动画的完成,移除对应的enter to class和enter active class
  const finishEnter = (el: Element, isAppear: boolean, done?: () => void) => {
    removeTransitionClass(el, isAppear ? appearToClass : enterToClass)
    removeTransitionClass(el, isAppear ? appearActiveClass : enterActiveClass)
    done && done()
  }

  // 处理离开动画的完成,移除对应的leave from class,leave to class,leave active class
  const finishLeave = (
    el: Element & { _isLeaving?: boolean },
    done?: () => void
  ) => {
    el._isLeaving = false
    removeTransitionClass(el, leaveFromClass)
    removeTransitionClass(el, leaveToClass)
    removeTransitionClass(el, leaveActiveClass)
    done && done()
  }

  const makeEnterHook = (isAppear: boolean) => {
    return (el: Element, done: () => void) => {
      const hook = isAppear ? onAppear : onEnter
      const resolve = () => finishEnter(el, isAppear, done)
      callHook(hook, [el, resolve])
      // 封装了requestAnimationFrame方法
      nextFrame(() => {
        removeTransitionClass(el, isAppear ? appearFromClass : enterFromClass)
        if (__COMPAT__ && legacyClassEnabled) {
          removeTransitionClass(
            el,
            isAppear ? legacyAppearFromClass : legacyEnterFromClass
          )
        }
        addTransitionClass(el, isAppear ? appearToClass : enterToClass)
        if (!hasExplicitCallback(hook)) {
          whenTransitionEnds(el, type, enterDuration, resolve)
        }
      })
    }
  }

  return extend(baseProps, {
    onBeforeEnter(el) {
      callHook(onBeforeEnter, [el])
      addTransitionClass(el, enterFromClass)
      if (__COMPAT__ && legacyClassEnabled) {
        addTransitionClass(el, legacyEnterFromClass)
      }
      addTransitionClass(el, enterActiveClass)
    },
    onBeforeAppear(el) {
      callHook(onBeforeAppear, [el])
      addTransitionClass(el, appearFromClass)
      if (__COMPAT__ && legacyClassEnabled) {
        addTransitionClass(el, legacyAppearFromClass)
      }
      addTransitionClass(el, appearActiveClass)
    },
    onEnter: makeEnterHook(false),
    onAppear: makeEnterHook(true),
    onLeave(el: Element & { _isLeaving?: boolean }, done) {
      el._isLeaving = true
      const resolve = () => finishLeave(el, done)
      addTransitionClass(el, leaveFromClass)
      if (__COMPAT__ && legacyClassEnabled) {
        addTransitionClass(el, legacyLeaveFromClass)
      }
      // force reflow so *-leave-from classes immediately take effect (#2593)
      forceReflow()
      addTransitionClass(el, leaveActiveClass)
      nextFrame(() => {
        if (!el._isLeaving) {
          // cancelled
          return
        }
        removeTransitionClass(el, leaveFromClass)
        if (__COMPAT__ && legacyClassEnabled) {
          removeTransitionClass(el, legacyLeaveFromClass)
        }
        addTransitionClass(el, leaveToClass)
        if (!hasExplicitCallback(onLeave)) {
          whenTransitionEnds(el, type, leaveDuration, resolve)
        }
      })
      callHook(onLeave, [el, resolve])
    },
    onEnterCancelled(el) {
      finishEnter(el, false)
      callHook(onEnterCancelled, [el])
    },
    onAppearCancelled(el) {
      finishEnter(el, true)
      callHook(onAppearCancelled, [el])
    },
    onLeaveCancelled(el) {
      finishLeave(el)
      callHook(onLeaveCancelled, [el])
    }
  } as BaseTransitionProps<Element>)
}

keepAlive原理

基本使用

keepAlive是一个内置组件,主要用于组件缓存,它包裹的组件在切换后不会被销毁,而是保留在内存中,避免重复渲染DOM。include/exclude用于包含/排除组件,max用于限制最大缓存实例个数,它使用的是LRU算法

LRU缓存(最大最少使用缓存):缓存的实例个数超过最大数量,最久没被访问的缓存实例将被销毁,以便为新实例腾出空间

缓存实例的生命周期

onActivated:组件被挂载时调用

onDeactivated:组件被卸载时调用

<script setup>
  import {onActivated,onDeactivated} from "vue"
  onActivated(()=>{
   // 调用时机为首次挂载
  // 以及每次从缓存中被重新插入时
  })
  onDeactivated(()=>{
    // 在从 DOM 上移除、进入缓存
  // 以及组件卸载时调用
  })
</script>	

keepAlive实现原理

// vue3版本源码位置:packages/runtime-core/src/components/KeepAlive.ts
// MatchPattern类,支持传入字符串、正则表达式或者字符串数组、正则数组
type MatchPattern = string | RegExp | (string | RegExp)[]

// 定义KeepAliveProps接口,接收三个参数
export interface KeepAliveProps {
  // include缓存白名单实例,可选参数
  include?: MatchPattern
  // exclude缓存黑名单实例,可选参数
  exclude?: MatchPattern
  // 最大可缓存个数,可选参数,支持传入数字或字符串
  max?: number | string
}
type CacheKey = string | number | symbol | ConcreteComponent
// 定义Cache类,用于缓存虚拟DOM,Map类型
type Cache = Map<CacheKey, VNode>
// 定义Keys类,用于缓存虚拟DOM对应的key,Set类型
type Keys = Set<CacheKey>

// KeepAliveContext接口
export interface KeepAliveContext extends ComponentRenderContext {
  // 渲染器实例
  renderer: RendererInternals
  // 组件创建实例方法
  activate: (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    isSVG: boolean,
    optimized: boolean
  ) => void
  // 组件销毁实例方法
  deactivate: (vnode: VNode) => void
}

const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
  // 标志用于摇树优化
  __isKeepAlive: true,
  // 定义的props,前面已说过不再重复
  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
  },

  // 在setup生命周期执行
  setup(props: KeepAliveProps, { slots }: SetupContext) {
    // 获取当前正在渲染的实例
    const instance = getCurrentInstance()!
    // 取实例的上下文作为共享上下文(摇树优化用)
    const sharedContext = instance.ctx as KeepAliveContext
    // 如果内部渲染实例未注册,表明是服务端渲染,在keepAlive内部渲染子组件
    if (__SSR__ && !sharedContext.renderer) {
      return () => {
        const children = slots.default && slots.default()
        return children && children.length === 1 ? children[0] : children
      }
    }
    // cache用于缓存虚拟DOM,keys缓存虚拟DOM对应的key
    const cache: Cache = new Map()
    const keys: Keys = new Set()
    // 当前渲染的虚拟DOM,初始化时为null
    let current: VNode | null = null

    //开发环境且开启了devtools,实例增加__v_cache属性使用缓存
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      ;(instance as any).__v_cache = cache
    }
    // 获取实例的suspense属性
    const parentSuspense = instance.suspense

    // 从共享上下文中解构出patch,move,unmount,createElement方法
    const {
      renderer: {
        p: patch,
        m: move,
        um: _unmount,
        o: { createElement }
      }
    } = sharedContext
    // 创建div元素
    const storageContainer = createElement('div')

    // 共享上下文的activate属性定义组件被创建的方法
    sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
      // 从虚拟DOM的component属性获取实例
      const instance = vnode.component!
      move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
      // 调用patch方法进行新旧VNode比较
      patch(
        instance.vnode,
        vnode,
        container,
        anchor,
        instance,
        parentSuspense,
        isSVG,
        vnode.slotScopeIds,
        optimized
      )
      queuePostRenderEffect(() => {
        instance.isDeactivated = false
        if (instance.a) {
          invokeArrayFns(instance.a)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeMounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
      }, parentSuspense)

      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        // 更新组件树
        devtoolsComponentAdded(instance)
      }
    }
// 共享上下文的deactivate属性定义组件被销毁的方法
    sharedContext.deactivate = (vnode: VNode) => {
      const instance = vnode.component!
      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      queuePostRenderEffect(() => {
        if (instance.da) {
          invokeArrayFns(instance.da)
        }
        const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
        if (vnodeHook) {
          invokeVNodeHook(vnodeHook, instance.parent, vnode)
        }
        instance.isDeactivated = true
      }, parentSuspense)

      if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
        // 更新组件树
        devtoolsComponentAdded(instance)
      }
    }

    function unmount(vnode: VNode) {
      // 重置shapeFlag用于unmounted
      resetShapeFlag(vnode)
      _unmount(vnode, instance, parentSuspense, true)
    }

    function pruneCache(filter?: (name: string) => boolean) {
      // 遍历已缓存的虚拟DOM map
      cache.forEach((vnode, key) => {
        // 获取当前组件名
        const name = getComponentName(vnode.type as ConcreteComponent)
        if (name && (!filter || !filter(name))) {
          pruneCacheEntry(key)
        }
      })
    }

    function pruneCacheEntry(key: CacheKey) {
      // 从缓存的虚拟DOM map中取出key对应的VNode
      const cached = cache.get(key) as VNode
      // 当前没有被渲染的实例(旧的VNode需要被删除)或者
      //缓存VNode的type不等于当前实例的type(新旧VNode不相等)
      if (!current || cached.type !== current.type) {
        // unmount卸载VNode
        unmount(cached)
      } else if (current) {
        // 当前激活实例不再使用keep-alive,重置flag
        resetShapeFlag(current)
      }
      // 从cache和keys中删除对应实例
      cache.delete(key)
      keys.delete(key)
    }

    // watch include/exclude属性变更
    watch(
      () => [props.include, props.exclude],
      ([include, exclude]) => {
        // include命中则增加
        include && pruneCache(name => matches(include, name))
        // exclude命中则删除
        exclude && pruneCache(name => !matches(exclude, name))
      },
      // prune post-render after `current` has been updated
      { flush: 'post', deep: true }
    )

    // 缓存渲染后的子树
    let pendingCacheKey: CacheKey | null = null
    const cacheSubtree = () => {
      // fix #1621, the pendingCacheKey could be 0
      if (pendingCacheKey != null) {
        cache.set(pendingCacheKey, getInnerChild(instance.subTree))
      }
    }
    onMounted(cacheSubtree)
    onUpdated(cacheSubtree)

    onBeforeUnmount(() => {
      cache.forEach(cached => {
        // 解构获取subTree,suspense
        const { subTree, suspense } = instance
        // 获取子树的child vnode
        const vnode = getInnerChild(subTree)
        if (cached.type === vnode.type) {
          // 重置标志
          resetShapeFlag(vnode)
          // 触发deactivated hook
          const da = vnode.component!.da
          da && queuePostRenderEffect(da, suspense)
          return
        }
        unmount(cached)
      })
    })

    return () => {
      pendingCacheKey = null
      // 没有插槽则返回
      if (!slots.default) {
        return null
      }

      const children = slots.default()
      // 获取keepAlive第一个子节点的VNode
      const rawVNode = children[0]
      // keepAlive内部只允许包裹一个组件
      if (children.length > 1) {
        if (__DEV__) {
          warn(`KeepAlive should contain exactly one component child.`)
        }
        current = null
        return children
      } else if (
        // 包裹的组件不是VNode,也不是有状态的组件或者Suspense,直接返回当前子组件
        !isVNode(rawVNode) ||
        (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
          !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
      ) {
        current = null
        return rawVNode
      }
      // 获取包裹组件的子节点VNode
      let vnode = getInnerChild(rawVNode)
      // 获取vnode的type属性,它是ConcreteComponent
      const comp = vnode.type as ConcreteComponent
      // 异步组件需要基于已加载的内部组件进行命名检查
      const name = getComponentName(
        isAsyncWrapper(vnode)
          ? (vnode.type as ComponentOptions).__asyncResolved || {}
          : comp
      )
      // 解构include,exclude,max
      const { include, exclude, max } = props

      // 定义了include属性且命名不匹配
      // 或定义了exclude属性且命名匹配
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        // 当前节点为VNode,返回keepAlive第一层子组件
        current = vnode
        return rawVNode
      }
      // vnode没有key属性,key为comp,否则取vnode.key
      const key = vnode.key == null ? comp : vnode.key
      // 根据key获取缓存对应的虚拟DOM
      const cachedVNode = cache.get(key)

      if (vnode.el) {
        // 克隆vnode节点
        vnode = cloneVNode(vnode)
        // 是Suspense组件,给原生VNode赋值ssContent属性,值为vnode
        if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
          rawVNode.ssContent = vnode
        }
      }
      // 在beforeMount/beforeUpdate钩子中缓存到instance.subTree
      pendingCacheKey = key

      if (cachedVNode) {
        // 已缓存的vNode存在就把el和component赋值到vnode
        vnode.el = cachedVNode.el
        vnode.component = cachedVNode.component
        if (vnode.transition) {
          // 递归更新子树的transition hook
          setTransitionHooks(vnode, vnode.transition!)
        }
        // 避免vnode作为新元素挂载
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
        // keys set中先删后加,确保是最新的key
        keys.delete(key)
        keys.add(key)
      } else {
        // 新增key
        keys.add(key)
        // max属性存在且当前已缓存的keys容量大于max
        if (max && keys.size > parseInt(max as string, 10)) {
          // 使用LRU更新key
          pruneCacheEntry(keys.values().next().value)
        }
      }
      // 避免vnode被unmounted
      vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE

      current = vnode
      // 是否为Suspense组件,是返回原生Vnode,否返回vnode
      return isSuspense(rawVNode.type) ? rawVNode : vnode
    }
  }
}
export const isSuspense = (type: any): boolean => type.__isSuspense

keepAlive是在哪个生命周期被调用的

vue2

  • created阶段,初始化cache、keys,cache用于缓存虚拟DOM,是一个map集合。keys用于缓存组件的key集合,是一个Set
  • mounted阶段,监听include,exclude的变化,执行相应操作
  • destroyed阶段,删除所有缓存相关实例

vue3 和vue2的过程类似,只不过生命周期钩子变更了
created->onCreated, mounted->OnMount, destroyed->onUnmount

Supsense原理与异步

基本使用

suspense是vue的一个内置组件,用于处理异步依赖,它有两个插槽, #default#fallback,每个插槽只允许一个直接的子节点

vue应用中组件加载时间过长就可以使用这个组件

<Suspense>
  <!--1.初始渲染时,渲染#default插槽的内容,如果在这个过程遇到异步依赖,会进入挂起状态-->
  <Container />
  <!--2.在挂起状态时,展示的是#fallback插槽的内容--> 
  <!--3.当所有异步依赖完成后,suspense进入完成状态,展示#default插槽的内容,当异步加载失败时,会展示#fallback插槽的内容-->  
  <template #fallback>
    loading...
  </template>
</Suspense>

Suspense实现原理

//vue3源码位置:packages/runtime-core/src/components/Suspense.ts

Teleport是怎么实现选择性挂载的

Teleport是vue的一个内置组件,类似于React的Portal,它可以让组件渲染在父组件以外的DOM上,主要支持to和disabled两个参数

to 必选,Teleport目标挂载的DOM元素

disabled 可选,用于禁用Teleport的功能,插槽内容不会移动到任何位置

使用场景:弹窗

 // index.vue
<template>
    <div class="outer">
        <h3>Tooltip with Vue3 Teleport</h3>
        <button id="show-modal" @click="show">显示</button>
        <Teleport to="body">
            <modal :show="showModal" @close="hide">
                <template #header>
                    <h3>custom modal</h3>
                </template>
            </modal>
        </Teleport>
    </div>
</template>

<script setup>
   import Modal from './Modal.vue'
   import {ref} from 'vue'

   const showModal = ref(false)
   const show = ()=>{
       showModal.value = true
   }
   const hide=()=>{
       showModal.value = false
   }

</script>

// Modal.vue
<template>
<Transition name="modal">
    <div v-if="show" class="modal-mask">
       <div class="modal-wrapper">
           <div class="modal-container">
               <div class="modal-header">
               <slot name="header">default header</slot>
               </div>
               <div class="modal-body">
                   <slot name="body">default body</slot>
               </div>
               <div class="modal-footer">
                   <slot name="footer">default footer</slot>
                   <button class="modal-default-button" @click="$emit('close')">x</button>
               </div>
           </div>
       </div>
    </div>
</Transition>

</template>

<script setup>
    import {defineProps} from 'vue'
    const props=defineProps({
        show:Boolean
    })
</script>

<style scoped>
    .modal-mask {
        position: fixed;
        z-index: 9998;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background-color: rgba(0, 0, 0, 0.5);
        display: table;
        transition: opacity 0.3s ease;
    }

    .modal-wrapper {
        display: table-cell;
        vertical-align: middle;
    }

    .modal-container {
        width: 300px;
        margin: 0 auto;
        padding: 20px 30px;
        background-color: #fff;
        border-radius: 2px;
        box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
        transition: all 0.3s ease;
    }

    .modal-header h3 {
        margin-top: 0;
        color: #42b983;
    }

    .modal-body {
        margin: 20px 0;
    }

    .modal-default-button {
        float: right;
    }
    .modal-enter-from {
        opacity: 0;
    }

    .modal-leave-to {
        opacity: 0;
    }

    .modal-enter-from .modal-container,
    .modal-leave-to .modal-container {
        -webkit-transform: scale(1.1);
        transform: scale(1.1);
    }
</style>

注意,Teleport的to目标必须已经存在于DOM中,在挂载Teleport前,to目标的元素必须挂载完成,否则会报错

实现原理

主要是通过document的querySelector实现的,Teleport实质是一个组件,必然会经历组件的初始化、挂载与更新

  1. Teleport组件通过process初始化,查看是否禁用teleport,禁用则在初始化容器时渲染,处理HMR
  2. 通过resolveTarget拿到要渲染teleport的容器节点,目标不存在就报错返回,存在打注释定位标记
  3. 进入挂载阶段,确认内部是否是array children结构,是否配置了disabled参数,配置了就解析在默认容器中,没有配置就解析在to 指定的容器中,初始化结束
  4. 更新阶段,核心函数moveTeleport,能触发Teleport更新只有两种情况:
  • 修改to或者disabled的值
  • 组件内部的内容发生更新

根据这两种情况进行不同的处理,更新结束

moveTeleport 用于更新

修改to或者disabled的值分为以下情况:

  1. disabled从false为true,会将to指定的容器移动到默认的容器
  2. disabled从true为false,会将默认容器移动到to指定的容器
  3. to指定的容器改变,向重新传递一个选择器,重新解析,移动到新的选择器位置
function moveTeleport(
  vnode: VNode,
  container: RendererElement,
  parentAnchor: RendererNode | null,
  { o: { insert }, m: move }: RendererInternals,
  moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER
) {
  // 确认新的目标容器,目标容器变了,将Teleport插入新的目标容器
  if (moveType === TeleportMoveTypes.TARGET_CHANGE) {
    insert(vnode.targetAnchor!, container, parentAnchor)
  }
  const { el, anchor, shapeFlag, children, props } = vnode
  const isReorder = moveType === TeleportMoveTypes.REORDER
  // 重新排序
  if (isReorder) {
    insert(el!, container, parentAnchor)
  }
  // 没有重排序或者disabled属性设置为true
  if (!isReorder || isTeleportDisabled(props)) {
    // 遍历vnode子节点,将子节点移动到
    if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      for (let i = 0; i < (children as VNode[]).length; i++) {
        move(
          (children as VNode[])[i],
          container,
          parentAnchor,
          MoveType.REORDER
        )
      }
    }
  }
  // 需要重新排序
  if (isReorder) {
    insert(anchor!, container, parentAnchor)
  }
}

nextTick实现原理

以下代码点击按钮以后输出是什么

<template>
  <div class="container">
    <div ref="test">{{msg}}</div>
    <button @click="handleClick">点击</button>
  </div>
</template>
<script>
  export default {
    data(){
      return{
        msg:'Hello,lyllovelemon'
      }
    },
    methods:{
      handleClick(){
        this.msg='Hello,lyl'
        console.log(this.$refs.test.innerHTML)
      }
    }
  }
</script>	

输出结果是Hello,lyllovelemon,这是为什么呢

异步更新DOM策略

Vue的DOM更新是异步的,当一个响应式数据变化时,会在Watcher的setter函数中通知闭包中的Dep,Dep会调用它管理的所有Watcher对象,触发update方法

//vue2.7.10 src/core/observer/watcher.ts
update() {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run() // 同步执行run方法,直接渲染视图
  } else {
    queueWatcher(this) // 异步执行queueWatcher,加到观察者队列中,在下一个tick调用
  }
}

我们再看看queueWatcher的具体实现

// src/core/observer/scheduler.ts
// 接收一个Watcher实例作为参数
export function queueWatcher(watcher: Watcher) {
  const id = watcher.id // 获取watcher的id
  if (has[id] != null) { // 观察者队列中存在就跳过
    return
  }
  
  if (watcher === Dep.target && watcher.noRecurse) { // noRecurse为true
    return
  }

  has[id] = true // 加入到has哈希表,用于下次校验
  if (!flushing) {
    queue.push(watcher) // 没有flush到加到队列
  } else {
    // flush过了, 根据id从watcher队列中删除
    // if already past its id, it will be run next immediately.
    let i = queue.length - 1
    while (i > index && queue[i].id > watcher.id) {
      i--
    }
    queue.splice(i + 1, 0, watcher)
  }
  // queue the flush
  if (!waiting) {
    waiting = true // waiting标志为true,表示不是立即更新视图,而是进入等待

    if (__DEV__ && !config.async) {
      flushSchedulerQueue() // 开发环境且同步
      return
    }
    // 在nextTick中执行flushSchedulerQueue方法
    nextTick(flushSchedulerQueue)
  }
}

queueWatcher中可以看到视图更新不是立即进行,而是将watcher对象push到一个队列,此时处于waiting状态,watch对象会不停的加入到队列中,等到下一个tick时,这些对象才会被遍历取出,更新视图。下一个tick就是nextTick

Vue为什么不使用同步更新DOM

<template>
  <div class="container">
    <div>{{count}}</div>
    <button @click="handleClick">点击</button>
  </div>
</template>
<script>
  export default {
    data(){
      return{
        count:0
      }
    },
    methods:{
      handleClick(){
        for(let i=0;i<1000;i++){
           this.count++
        }
      }
    }
  }
</script>

如上图,点击按钮时,执行了1000次 count的++,当DOM更新是同步时,就会触发1000次DOM更新刷新视图,这是很耗性能的,Vue使用异步更新DOM策略,将count++的操作放到队列中,在下一个tick时统一执行。同一个id的Watcher不会重复加到queue中,所以最终更新视图直接将count从0加到1000,保证视图更新DOM是从当前栈执行完后的下一个tick调用,大大优化了性能

nextTick原理

vue2.5以前的版本:nextTick实质是产生一个回调函数加入到task(宏任务)或者microtask(微任务),当前栈执行完后调用该回调函数,起到了异步触发的作用

vue2.5以后全部使用微任务实现,原因是使用宏任务会产生一些问题

 // src/core/util/next-tick.ts
export let isUsingMicroTask = false

const callbacks: Array<Function> = []
let pending = false
function flushCallbacks() { //清空回调函数
  pending = false  // pending为false,表示等待结束,准备执行
  const copies = callbacks.slice(0) // 浅拷贝回调函数
  callbacks.length = 0 // 将回调函数清空
  for (let i = 0; i < copies.length; i++) { // 拷贝后的数组遍历执行回调
    copies[i]()
  }
}

let timerFunc // 延迟执行函数

/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 支持promise,用promise实现
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
 
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]')
) {
  // 不支持promise但是支持MutationObserver,用MutationObserver实现
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // 以上不支持但支持setImmediate,用setImmediate实现
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // 以上都不支持,最后用setTimeout实现
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  // cb存到callbacks中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e: any) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true // pending是一个状态标志,保证timerFunc在下一个tick之前只执行一次
    timerFunc()//timerFunc
  }
  // $flow-disable-line
  // 支持promise就用promise实现
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}   

timerFunc函数做了什么:先后按照promise、MutationObserver、setImmediate、setTimeout实现,promise、MutationObserver都是微任务的实现

为什么优先使用微任务

由于浏览器的事件循环机制,引擎在每一个宏任务执行完毕,从队列中取下一个宏任务执行之前,会将这个宏任务下的微任务队列拿出来依次执行,因此微任务的执行时间早于宏任务。每个task执行完后都会触发UI的重新渲染,在microTask中完成数据更新,当前task结束就能拿到最新的UI了,如果再新建一个task,UI渲染就会进行两次

为什么优先使用Promise而不是MutationObserver

MutationObserver虽然浏览器兼容性更好,但是在iOS7,Android 4.4的touch事件上会有问题

setImmediate为什么比setTimeout好

setImmediate可以保证调用后立即执行,setTimeout需要和系统时间保持一致,最快也要4ms以后才能执行。但是setTimeout的浏览器兼容性更好,setImmediate只支持IE浏览器

watch函数实现原理

watch原理

对watch的每一个属性创建watcher,watcher在初始化时会将监听的目标值缓存到watcher.value中,触发data[key]的get方法,被对应的dep进行依赖收集,当data[key]发生改变时触发set方法,执行dep.notify方法,

通知所有收集的依赖,触发收集的watcher的watch,执行watch.cb,也就是watch中的监听函数

computed原理

computed是响应式的, 给computed设置get和set会和Object.defineProperty关联起来(vue2),vue3会和proxy关联

computed是有缓存的,主要通过dirty控制

dirty是一个脏数据标志位,为false时表示读取computed使用缓存,为true时表示读取computed会执行get函数重新计算

    export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions
) {
  let getter: ComputedGetter<T> // 收集getter
  let setter: ComputedSetter<T> // 收集setter

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

  // 是服务端渲染就赋值null,客户端渲染实例化watcher
  const watcher = isServerRendering()
    ? null
    : new Watcher(currentInstance, getter, noop, { lazy: true })

  if (__DEV__ && watcher && debugOptions) { // 用于debug
    watcher.onTrack = debugOptions.onTrack
    watcher.onTrigger = debugOptions.onTrigger
  }

  const ref = {
    // some libs rely on the presence effect for checking computed refs
    // from normal refs, but the implementation doesn't matter
    effect: watcher,
    get value() {
      if (watcher) {
        if (watcher.dirty) { // dirty标志位,表示数据需要更新
          watcher.evaluate() // 执行watcher的evaluate方法
        }
        if (Dep.target) {
          if (__DEV__ && Dep.target.onTrack) {
            Dep.target.onTrack({
              effect: Dep.target,
              target: ref,
              type: TrackOpTypes.GET,
              key: 'value'
            })
          }
          watcher.depend()
        }
        return watcher.value
      } else {
        return getter()
      }
    },
    set value(newVal) {
      setter(newVal)
    }
  } as any

  def(ref, RefFlag, true)
  def(ref, ReactiveFlags.IS_READONLY, onlyGetter)

  return ref
}

性能优化

cn.vuejs.org/guide/best-…

性能优化主要在以下几个方面:页面加载速度优化打包体积优化,打包速度优化,浏览器安全

页面加载速度优化

  1. 使用正确的架构

SPA/SSR/SSG

  1. v-for循环中正确的使用key

key需要在循环列表中保持唯一,不要使用数组下标作为key,key用于高效的更新diff

  1. 保持props稳定

如下图代码,它使用了id和activeId两个prop来保证它是否是当前活跃的一项,代码存在什么问题呢?

<ListItem
  v-for="item in list"
  :id="item.id"
  :active-id="activeId" />

当activeId更新时,ListItem的每一项都会进行更新,造成了不必要的重复渲染,我们需要改成只有活跃状态发生改变的项才需要更新,将上面的代码改写。对于大多数的组件来说,activeId 改变时,它们的 active prop 都会保持不变,因此它们无需再更新。总结一下,就是让传给子组件的 props 尽量保持稳定。

<ListItem
  v-for="item in list"
  :id="item.id"
  :active="item.id === activeId" />

4. 正确使用v-oncev-memo 5. 使用shallowRef和shallowReactive绕开深度响应

Vue3的响应式默认是深度的,优点在于便于进行数据的状态管理,缺点是数据量大时,性能负担加重,因为每个属性访问都会进行依赖的深度追踪。

解决办法:使用shallowRef和shallowReactive绕开深度响应

const shallowArray = shallowRef([
  /* 巨大的列表,里面包含深层的对象 */
])

// 这不会触发更新...
shallowArray.value.push(newObject)
// 这才会触发更新
shallowArray.value = [...shallowArray.value, newObject]

// 这不会触发更新...
shallowArray.value[0].foo = 1
// 这才会触发更新
shallowArray.value = [  {    ...shallowArray.value[0],
    foo: 1
  },
  ...shallowArray.value.slice(1)
]

6. 长列表渲染:虚拟列表

长列表渲染的最优方案是虚拟列表,其次还有time slice和css方案。渲染的列表项成千上万时,对应的DOM元素也会递增,但是我们并不需要一次性将所有数据渲染出来,虚拟列表的原理就是创建一个可视化区域,让可视化区域的数据渲染出来

  1. 避免内存泄漏

事件绑定后需要取消绑定,定时器也需要定时销毁

created(){
  window.addEventListener('click',this.onClick,false)
  setTimeout(timer,2000)
}
beforeUnmount(){
  window.removeEventListener('click',this.onClick,false)
  clearTimeout(timer,2000)
}  

8. 预加载与懒加载

图片,路由都可以进行懒加载

  1. v-if和v-show使用
  2. computed和watch使用
  3. 使用keep-alive缓存DOM
  4. 节流/防抖
  5. 浏览器缓存与本地缓存
  6. CSS优化

GPU加速

减少回流和重绘

打包速度优化

  1. 开启多进程打包
  2. 多线程打包
  3. CDN

打包体积优化

以打包工具webpack举例

  1. 开启gzip压缩
  2. 压缩HTML
  3. 压缩CSS
  4. 压缩JavaScript
  5. 图片压缩
  6. 尽量选择ES模式的依赖

选择lodash-es优于lodash,它对于tree-shaking更友好

  1. 使用Html-webpack-bundle-analyaser分析包体积

这个插件可用于分析包体积,查看最大的包,按需进行体积优化

  1. 代码分割

使用打包工具将整个包拆分为多个较小的包,可以按需加载或者并行加载,通过适当的代码分割,页面加载时需要的功能可以立即下载,而额外的块只在需要时才加载,从而提高性能。

// defineAsyncComponent可用于按需加载组件
import {defineAsyncComponent} from "vue"
// Container是一个异步组件,按需加载    
const Container=defineAsyncComponent(()=>import("./container.vue"))  

7. 开启tree-shaking 8. 第三方库按需引入

以element为例,项目中并没有用到所有的组件时,可以进行按需引入,可以有效减少第三方包体积

  1. 使用合适的sourceMap

开发环境最优sourceMap:cheap-module-eval-source-map

生产环境推荐souceMap:source-map

浏览器安全

  1. 避免CSRF
  2. 避免XSS
  3. 安全沙箱

写在最后

源码分析花的时间很长,白天要工作(目前工作比较饱和),只能晚上写,前前后后花了快1个月时间,如果这篇文章对你有帮助的话,不妨给博主姐姐点赞收藏评论

thumb.webp