Vue 源码学习笔记 part 1

做了那么久 Vue,试着看一看源码,我拉取的版本是 3.2.31。拉代码时候,记得 fork 一份到自己的仓库,这样可以随便修改。

根目录下看到了 pnpm-workspace.yaml,明显包管理工具使用的是 pnpm,不同于 element-plus,这里没有指定 packageManager

monorepo 的根目录一般用来配置一些通用内容,会设置 "private": true,不会发布。只想了解 Vue 核心,可以看 packages 目录下对应的包。

Vue 3 做了很多 tree-shaking 方面的优化,拆分的比较细致。阅读时多思考平常使用遇到的问题,带着目的去读会好一点。整个代码注释也很清晰,重点地方都有,理解起来还是挺方便的。不得不说,人家的代码写的真好。

打包

第一步了解下打包的相关操作。"build": "node scripts/build.js",打包用的是 node,也学习一下。

build.js 中引入了 utils.js 的两个变量:targets 和 fuzzyMatchTarget。

javascript 复制代码
// 读取 packages 下面的文件
const targets = (exports.targets = fs.readdirSync('packages').filter(f => {
  // 过滤不是目录的文件
  if (!fs.statSync(`packages/${f}`).isDirectory()) {
    return false
  }
  // 过滤私有库,和不用打包的库
  const pkg = require(`../packages/${f}/package.json`)
  if (pkg.private && !pkg.buildOptions) {
    return false
  }
  return true
}))

使用的都是同步的 fs 方法,当前的目录,就是运行代码的目录,也就是在根目录下。readdirSync 会将目录下的所有文件,文件夹都读取出来,需要过滤掉不是文件夹的。node 可以直接读取 JSON,根据对应 package 是否私有、有打包选项,这里做了一个过滤。

fuzzyMatchTarget 用来过滤打包情况,打包命令中有获取参数 const buildAllMatching = args.all || args.a,如果带 all 或者 a 参数是打包多个库的。没有涉及到 node API,就不写下来了。

回到 build.js,看主体实现:

scss 复制代码
async function run() {
  if (isRelease) {
    // remove build cache for release builds to avoid outdated enum values
    await fs.remove(path.resolve(__dirname, '../node_modules/.rts2_cache'))
  }
  if (!targets.length) {
    await buildAll(allTargets)
    checkAllSizes(allTargets)
  } else {
    await buildAll(fuzzyMatchTarget(targets, buildAllMatching))
    checkAllSizes(fuzzyMatchTarget(targets, buildAllMatching))
  }
}

这里使用了 fs.remove 清除缓存。remove 这个方法,不是 node 自身支持的,依赖于一个库 fs-extra,接下来是打包文件的过滤。

javascript 复制代码
async function buildAll(targets) {
  // require('os').cpus().length 获取 cpu 核心数
  await runParallel(require('os').cpus().length, targets, build)
}

os 是 node 中的一个模块,获取操作系统相关信息。通过获取 cpu 核心数,控制并发数。

scss 复制代码
async function runParallel(maxConcurrency, source, iteratorFn) {
  const ret = []
  const executing = []
  for (const item of source) {
    // 使用 Promise 完成异步任务,防止阻塞主进程
    const p = Promise.resolve().then(() => iteratorFn(item, source))
    ret.push(p)
    // 处理并发数
    if (maxConcurrency <= source.length) {
      // 异步任务完成后,移除对应任务
      const e = p.then(() => executing.splice(executing.indexOf(e), 1))
      executing.push(e)
      // 如果超出并发限制需要等待
      if (executing.length >= maxConcurrency) {
        await Promise.race(executing)
      }
    }
  }
  return Promise.all(ret)
}

这里给我一种豁然开朗的感觉,一直不清楚 Promise.race 的应用场景,终于看到了。Promise.race 会等待第一个异步任务完成,这样就可以在保证并发数的情况下,执行下一个操作。const e = p.then(() => executing.splice(executing.indexOf(e), 1)) 更是让我体会到异步编程,和同步上的区别。如果是同步操作的话,大概需要轮询,直到完成。这里放入了异步回调里面,非常简洁。也不是多么难的操作,业务写久了,异步用的不多。

打包使用的是 rollup,需要了解 rollup 的打包配置。Vue packages 中的包,入口文件引入的是打包后的文件,需要了解打包的入口文件。

一个基本的 rollup.config.js,结构是这样的:

css 复制代码
export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'cjs'
  }
};

其中 input 指定了入库文件,Vue 中是通过 config 中的一个方法 createConfig,来生成对应的 config(Vue 会区分 runtime-only 和 full-build,对应不同的打包配置),其中一行代码:

bash 复制代码
let entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts`

这里指定了入库文件,直接去对应的 package 中查找对应文件就行了。我看的都是完整版,也就是 index.ts 文件。

Core

一个基本的 Vue 项目,main.js 中大概会有下面这两行代码:

ini 复制代码
const app = createApp(App)
​
app.mount('#app')

所以接下来可以直接去看 createApp 方法。

createApp

typescript 复制代码
export const createApp = ((...args) => {
  const app = ensureRenderer().createApp(...args)
​
  // dev 下提供了两个校验
  // 一个判断是否是原生 tag
  // 另一个是 complier options 合法性的校验
  // 这两个方法挂在 app.config 下
  if (__DEV__) {
    injectNativeTagCheck(app)
    injectCompilerOptionsCheck(app)
  }
​
  const { mount } = app
  app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
    const container = normalizeContainer(containerOrSelector)
    if (!container) return
​
    const component = app._component
    // 没有模版使用 container 的 DOM 结构
    if (!isFunction(component) && !component.render && !component.template) {
      // __UNSAFE__
      // Reason: potential execution of JS expressions in in-DOM template.
      // The user must make sure the in-DOM template is trusted. If it's
      // rendered by the server, the template should not contain any user data.
      component.template = container.innerHTML
      // 2.x compat check
      if (__COMPAT__ && __DEV__) {
        for (let i = 0; i < container.attributes.length; i++) {
          const attr = container.attributes[i]
          if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
            compatUtils.warnDeprecation(
              DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
              null
            )
            break
          }
        }
      }
    }
​
    // clear content before mounting
    container.innerHTML = ''
    const proxy = mount(container, false, container instanceof SVGElement)
    if (container instanceof Element) {
      container.removeAttribute('v-cloak')
      container.setAttribute('data-v-app', '')
    }
    return proxy
  }
​
  return app
}) as CreateAppFunction<Element>

ensureRenderer 其实看名字也能想到,最终会创建 renderer。具体过程其实不用看那么细,我就挑了一些重点看了看。

这里主要还是 app.mount 方法,如果对照官网,会发现官网写的很详细。

下面是官网对 app.mount() 的解释:

将应用实例挂载在一个容器元素中。

  • 类型

    ts

    php 复制代码
    interface App {
      mount(rootContainer: Element | string): ComponentPublicInstance
    }
  • 详细信息

    参数可以是一个实际的 DOM 元素或一个 CSS 选择器 (使用第一个匹配到的元素)。返回根组件的实例。

    如果该组件有模板或定义了渲染函数,它将替换容器内所有现存的 DOM 节点。否则在运行时编译器可用的情况下,容器元素的 innerHTML 将被用作模板。

    在 SSR 激活模式下,它将激活容器内现有的 DOM 节点。如果出现了激活不匹配,那么现有的 DOM 节点将会被修改以匹配客户端的实际渲染结果。

    对于每个应用实例,mount() 仅能调用一次。

这里提到了 SSR,但是这部分我没有去看,后面也不会涉及到,感兴趣的可以自己去翻翻源码。

参数可以是一个 DOM 元素也可以是 CSS 选择器,需要处理,统一表现。const container = normalizeContainer(containerOrSelector),就是用来做这个的。

需要注意的就是几个不常用的点:

2.x compat check: Vue 2 兼容处理,需要注意的是 Vue 2 技术上并没有根组件的概念。有的只是顶层创建的一个 Vue 实例,所以你可以把 Vue 2 的根组件当作普通的组件看待,自然各种指令也都是可以使用的。Vue 3 相当于增加了一个 app 的概念,在根组件上只能使用 v-cloak 指令,别的指令不会生效,这里给了非兼容的提示。

v-cloak 处理: 这个指令大概也不是很常用。在模版编译完成后,会将容器内部结构清空,替换为编译后的内容。换句话说,编译完成之前,页面内容是写在 #app 中的原始内容。假设有这样一个结构:

css 复制代码
<div id="app">
  <button @click="count++">
    Count is: {{ count }}
  </button>
</div>

在编译完成前,buttom 中内容将会是 Count is: {{ count }}。正常情况,我们是不希望用户看到 {{ count }} 这样的为编译内容,这里就可以使用 v-cloak,配合样式将不希望用户看到的内容进行一些处理。编译完成后,就需要将这些内容展示出来。

patch

这一步,我就跳的比较多了。前面的代码中 app 来自 ensureRendererensureRenderer 是经过几次处理的结果,最终会看到 baseCreateRenderer 这个方法。整个方法很长

baseCreateRenderer 中有这么几行代码:

javascript 复制代码
const render: RootRenderFunction = (vnode, container, isSVG) => {
  if (vnode == null) {
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    // 首次进入已创建 app 实例,通过 patch 更新
    patch(container._vnode || null, vnode, container, null, null, null, isSVG)
  }
  flushPostFlushCbs()
  container._vnode = vnode
}

patch 就是对比更新 vnode 的方法,创建时候没有旧的 vnode,这里就使用的 null。patch 方法中间涉及到各种类型组件的处理。先把类型放出来看看(源码中注注释非常详细,这里节约地方,部分删除了):

ini 复制代码
export const enum PatchFlags {
  TEXT = 1,
  CLASS = 1 << 1,
  STYLE = 1 << 2,
  PROPS = 1 << 3,
  FULL_PROPS = 1 << 4,
  HYDRATE_EVENTS = 1 << 5,
  STABLE_FRAGMENT = 1 << 6,
  KEYED_FRAGMENT = 1 << 7,
  UNKEYED_FRAGMENT = 1 << 8,
  NEED_PATCH = 1 << 9,
  DYNAMIC_SLOTS = 1 << 10,
  DEV_ROOT_FRAGMENT = 1 << 11,
  /**
   * Indicates a hoisted static vnode. This is a hint for hydration to skip
   * the entire sub tree since static content never needs to be updated.
   */
  HOISTED = -1,
  /**
   * A special flag that indicates that the diffing algorithm should bail out
   * of optimized mode. For example, on block fragments created by renderSlot()
   * when encountering non-compiler generated slots (i.e. manually written
   * render functions, which should always be fully diffed)
   * OR manually cloneVNodes
   */
  BAIL = -2
}

这里使用位操作符,左移 (<<)。比如说 CLASS = 1 << 1,就是将 1 左移一位,也就是 0b0010。同一个组件,可能是多个类型的联合,用处看下图:

假设有一个类型是 Text | Class(后面简单表示为 T) 的组件,需要判断是否为 Text,只需计算 T & Text。枚举值除了标识位都为 0,也就是说结果只依赖于标识位。

patch 方法中都是同层比较,如果是多级计较,时间复杂度就会飙升,可能最后还不如不比较,直接修改效率高。读代码的时候也要清楚这一点,这里的处理还是有一些复杂的,很多方法来回调用,存在各种递归。记住自己只需要看完同层的逻辑就行了,下一级的操作并没有任何区别,别掉进递归出不来了。

typescript 复制代码
const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null, // vue 插入节点使用的 insertBefore,需要一个定位节点
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    slotScopeIds = null,
    optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
  ) => {
    // 没有变化直接返回
    if (n1 === n2) {
      return
    }
​
    // 判断新旧节点 type 和 key 是否都相同
    // 不相同卸载旧节点
    // 如果运行环境 dev,HMR 会强制 reload 
    if (n1 && !isSameVNodeType(n1, n2)) {
      anchor = getNextHostNode(n1)
      unmount(n1, parentComponent, parentSuspense, true)
      n1 = null
    }
    // 这里判断类型,默认 diff 模式是 optimized
    // 如果标记为 PatchFlags.BAIL(-2)
    // 则需要全部 diff
    if (n2.patchFlag === PatchFlags.BAIL) {
      // 例如本地运行,
      optimized = false
      n2.dynamicChildren = null
    }
​
    // 根据节点 type 进行处理
    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:  // 文本节点
        processText(n1, n2, container, anchor)
        break
      case Comment: // 注释节点
        processCommentNode(n1, n2, container, anchor)
        break
      case Static:  // 静态节点
        if (n1 == null) {
          // 原来没有就直接挂载
          mountStaticNode(n2, container, anchor, isSVG)
        } else if (__DEV__) {
          // 本地开发可能修改静态内容
          // 需要更新
          patchStaticNode(n1, n2, container, isSVG)
        }
        break
      case Fragment:   // 处理 Fragment 元素
        processFragment(
          n1,
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {  // 处理 ELEMENT
          processElement(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.COMPONENT) { // 处理 Vue 组件
          // 首次进入只渲染了根组件
          // 也会走这里
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) { // 处理 Teleport 组件
          ;(type as typeof TeleportImpl).process(
            n1 as TeleportVNode,
            n2 as TeleportVNode,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {  // 处理 Suspense 组件
          // Suspense 组件是一个比较新的特性
          // 如果父级组件依赖很多异步的子组件
          // 当子组件没有渲染完成,父组件可以知道这个状态进行处理
          // 类 Promise.all 的场景
          ;(type as typeof SuspenseImpl).process(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized,
            internals
          )
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }
​
    // set ref
    // 如果有绑定 DOM 进行处理
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
    }
  }

上面可以看到有针对不同方法的处理,这里就不细看了。其实有一定基础后去看源码,会加深自己的理解。在日常开发中,我们可能很少把 Vue 的每个特性都熟悉一遍。看源码时,每个地方的处理都会提醒你,哦,原来还有这个功能,或者这个功能还可以这样用啊。也有一些处理会让你觉得,这都是谁整的花活。

有一点还是说一下吧,Element 处理中,有这样的代码:

scss 复制代码
// mount children first, since some props may rely on child content
// being already rendered, e.g. `<select value>`
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
  hostSetElementText(el, vnode.children as string)
} 
​
// snip
​
// props
if (props) {
// snip
​
  /**
   * Special case for setting value on DOM elements:
   * - it can be order-sensitive (e.g. should be set *after* min/max, #2325, #4024)
   * - it needs to be forced (#1471)
   * #2353 proposes adding another renderer option to configure this, but
   * the properties affects are so finite it is worth special casing it
   * here to reduce the complexity. (Special casing it also should not
   * affect non-DOM renderers)
   */
  if ('value' in props) {
    hostPatchProp(el, 'value', null, props.value)
  }
}

这里注释也很清楚了,为什么要先创建子元素,因为有的属性依赖于子元素,例如 select 的 value。另外还有针对 value 属性做额外处理,value 元素需要晚于 max,min 设置。

diff

接下来就是 diff 操作了。上面 process 相关方法中,使用了 patchChildren 进行 children 的 diff。patchChildren 更具元素类型,区分了带 key 和 不带 key 的 diff,先看不带 key 的。

patchUnkeyedChildren:

typescript 复制代码
const patchUnkeyedChildren = (
  c1: VNode[],
  c2: VNodeArrayChildren,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) => {
  c1 = c1 || EMPTY_ARR
  c2 = c2 || EMPTY_ARR
  const oldLength = c1.length
  const newLength = c2.length
  // 取较短的
  const commonLength = Math.min(oldLength, newLength)
  let i
  for (i = 0; i < commonLength; i++) {
    const nextChild = (c2[i] = optimized
      ? cloneIfMounted(c2[i] as VNode)
      : normalizeVNode(c2[i]))
    patch(
      c1[i],
      nextChild,
      container,
      null,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized
    )
  }
  if (oldLength > newLength) {
    // remove old
    unmountChildren(
      c1,
      parentComponent,
      parentSuspense,
      true,
      false,
      commonLength
    )
  } else {
    // mount new
    mountChildren(
      c2,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      slotScopeIds,
      optimized,
      commonLength
    )
  }
}

这里面处理很简单,for 循环中 patch 对应位置元素。如果老节点多,就把剩余未 patch 的全部移除,如果新节点多,就创建新的元素。

patchKeyedChildren,带 key 的比较:

typescript 复制代码
// can be all-keyed or mixed
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--
        }
      }
    }
  }
}

isSameVNodeType 是判断新老节点是否相同的,如果节点的 key 和 type 都相同即认为同一节点。

前四步很好理解:

  1. 从头部开始 diff,找到相同的 i 向后移动一位,继续 diff。如果发现不同节点,立即跳出循环,进行下一步。
  2. 从尾部向前 diff,相同则 e1(老队列尾部指针),e2(新队列尾部指针)同时向前移动,遇到不同跳出。
  3. i > e1,证明老队列遍历完毕。由于 i++ 和 e--,都是判断后执行的。如果两个队列没有区别,diff 就会在这个条件下结束。如果 i <= e2 则代表新队列没有遍历完,例如,(a b) (d e) 和 (a b) c (d e),第一步结束,i = 2。接着是第二步,从尾部开始,d 相同,再往前移动一位,e1 = 1,e2 = 2,两节点不同第二步跳出。新队列节点多, i 刚好是第一个新节点的 index,e2 则为最后一个新节点位置。遍历创建全部新节点。
  4. 和上一种同理,i > e2,代表老队列有多余节点,全部移除即可。
  5. 进入这种情况,意味着,两个队列都没遍历完,中间部分需要继续 diff。

第 5 部分还是分开写吧。

首先解释变量作用:

s1,s2 代表新老队列开始位置。

patched,已比较完成的节点。

toBePatched,需要比较的节点数,从 s2 到 e2 包括 s2。

moved,是否需要移动节点。

maxNewIndexSoFar,当前移动的最靠后位置。

newIndexToOldIndexMap,记录新 index 对应的 老 index,数组下标为新的 index,值是老 index。

接下来每个部分单独解释:

kotlin 复制代码
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 // 新节点中对应 index
  if (prevChild.key != null) {
    // 有 key 直接查找
    newIndex = keyToNewIndexMap.get(prevChild.key)
  } else {
    // key-less node, try to locate a key-less node of the same type
    // 部分没有 key,查找对应 index
    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 {
    // 记录新 index 对应的 老 index
    // 由于默认值是 0,找到第一位,写入 0 就会有问题,统一加一处理
    newIndexToOldIndexMap[newIndex - s2] = i + 1 
    // maxNewIndexSoFar 也即是移动的最靠后的位置
    // 有这个不代表一定需要移动
    // old: a b c d
    // new: e g f a
    // 这种情况只需要移除不需要的老节点即可
    // move 只有在涉及到前后移动老节点才使用
    if (newIndex >= maxNewIndexSoFar) {
      maxNewIndexSoFar = newIndex
    } else {
      // 这里表示有向前移动操作
      moved = true
    }
    // snip
    // patch节点
    patched++
  }
}

重点是理解 maxNewIndexSoFar 和 move 的作用,向前移动有两种,直接移动和移除前面的节点,才会有 newIndex >= maxNewIndexSoFar 这个判断。

这一步结束,所有节点已经 patch 过了,对应的节点信息也记录完成,接下来需要移动节点。

首先,获取了 newIndexToOldIndexMap 中的最长递增子序列。操作的意义在于,尽量减少移动次数。Vue 插入节点使用的 insertBefore,插入顺序操作是从尾到头。获取最长递增子序列后,在这个序列中的节点可以保持不动。

csharp 复制代码
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--
    }
  }
}
相关推荐
cc蒲公英41 分钟前
Vue2+vue-office/excel 实现在线加载Excel文件预览
前端·vue.js·excel
森叶1 小时前
Electron-vue asar 局部打包优化处理方案——绕开每次npm run build 超级慢的打包问题
vue.js·electron·npm
小小竹子1 小时前
前端vue-实现富文本组件
前端·vue.js·富文本
青稞儿2 小时前
面试题高频之token无感刷新(vue3+node.js)
vue.js·node.js
程序员凡尘3 小时前
完美解决 Array 方法 (map/filter/reduce) 不按预期工作 的正确解决方法,亲测有效!!!
前端·javascript·vue.js
Bug缔造者8 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_9 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js
罗政9 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
阿树梢10 小时前
【Vue】VueRouter路由
前端·javascript·vue.js
随笔写11 小时前
vue使用关于speak-tss插件的详细介绍
前端·javascript·vue.js