做了那么久 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
phpinterface 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 来自 ensureRenderer
,ensureRenderer
是经过几次处理的结果,最终会看到 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 都相同即认为同一节点。
前四步很好理解:
- 从头部开始 diff,找到相同的 i 向后移动一位,继续 diff。如果发现不同节点,立即跳出循环,进行下一步。
- 从尾部向前 diff,相同则 e1(老队列尾部指针),e2(新队列尾部指针)同时向前移动,遇到不同跳出。
- 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 则为最后一个新节点位置。遍历创建全部新节点。
- 和上一种同理,i > e2,代表老队列有多余节点,全部移除即可。
- 进入这种情况,意味着,两个队列都没遍历完,中间部分需要继续 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--
}
}
}