组件更新:完整的 DOM diff 流程
- 组件渲染过程,就是把各种类型的
vnode
渲染成真是DOM
- 组件是由模板、组件描述对象和数据构成的
- 数据的变化会影响组件的变化
- 组件的渲染过程中创建了一个带副作用的渲染函数,当数据变化的时候就会执行这个渲染函数来触发组件的更新
副作用渲染函数更新组件的过程
- 带副作用渲染函数
setupRenderEffect
的实现
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// 创建响应式的副作用渲染函数
instance.update = effect(function componentEffect() {
if (!instance.isMounted) {
// 渲染组件
}
else {
// 更新组件
let { next, vnode } = instance
// next 表示新的组件 vnode
if (next) {
// 更新组件 vnode 节点信息
updateComponentPreRender(instance, next, optimized)
}
else {
next = vnode
}
// 渲染新的子树 vnode
const nextTree = renderComponentRoot(instance)
// 缓存旧的子树 vnode
const prevTree = instance.subTree
// 更新子树 vnode
instance.subTree = nextTree
// 组件更新核心逻辑,根据新旧子树 vnode 做 patch
patch(prevTree, nextTree,
// 如果在 teleport 组件中父节点可能已经改变,所以容器直接找旧树 DOM 元素的父节点
hostParentNode(prevTree.el),
// 参考节点在 fragment 的情况可能改变,所以直接找旧树 DOM 元素的下一个节点
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG)
// 缓存更新后的 DOM 节点
next.el = nextTree.el
}
}, prodEffectOptions)
}
- 主要做了三件事
- 1、更新组件
vnode
节点- 判断组件实例中是否有新的组件
vnode
(用next
表示) - 有则更新组件
vnode
, - 没有则
next
指向之前的组件vnode
- 判断组件实例中是否有新的组件
- 2、渲染新的子树
vnode
- 根据最新数据进行渲染
- 3、根据新旧子树
vnode
执行patch
逻辑- 用来找出新旧子树
vnode
的不同,并找到一种合适的方式更新DOM
- 用来找出新旧子树
核心逻辑:patch 流程
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
// 如果存在新旧节点, 且新旧节点类型不同,则销毁旧节点
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
// n1 设置为 null 保证后续都走 mount 逻辑
n1 = null
}
const { type, shapeFlag } = n2
switch (type) {
case Text:
// 处理文本节点
break
case Comment:
// 处理注释节点
break
case Static:
// 处理静态节点
break
case Fragment:
// 处理 Fragment 元素
break
default:
if (shapeFlag & 1 /* ELEMENT */) {
// 处理普通 DOM 元素
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 6 /* COMPONENT */) {
// 处理组件
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 64 /* TELEPORT */) {
// 处理 TELEPORT
}
else if (shapeFlag & 128 /* SUSPENSE */) {
// 处理 SUSPENSE
}
}
}
function isSameVNodeType (n1, n2) {
// n1 和 n2 节点的 type 和 key 都相同,才是相同节点
return n1.type === n2.type && n1.key === n2.key
}
- 首先判断新旧节点是否是相同的
vnode
类型- 如果不同,那么最简单的操作就是删除旧的 div 节点,再去挂载新的 ul 节点
- 如果是相同的
vnode
类型,就需要走diff
更新流程了
处理组件
更新过程也是一个树的深度优先遍历过程,当前要更新的vnode为组件类型,执行processComponent
处理
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
if (n1 == null) {
// 挂载组件
}
else {
// 更新子组件
updateComponent(n1, n2, parentComponent, optimized)
}
}
const updateComponent = (n1, n2, parentComponent, optimized) => {
const instance = (n2.component = n1.component)
// 根据新旧子组件 vnode 判断是否需要更新子组件
if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
// 新的子组件 vnode 赋值给 instance.next
instance.next = n2
// 子组件也可能因为数据变化被添加到更新队列里了,移除它们防止对一个子组件重复更新
invalidateJob(instance.update)
// 执行子组件的副作用渲染函数
instance.update()
}
else {
// 不需要更新,只复制属性
n2.component = n1.component
n2.el = n1.el
}
}
- 通过
updateComponent
方法更新子组件 - 先通过
shouldUpdateComponent
,根据新旧子组件vnode
来判断是否需要更新子组件,主要是通过检测和对比组件vnode
中的props
、chidren
、dirs
、transiton
等属性,来决定子组件是否需要更新 - 注意,组件的数据变化只会影响当前组件的更新,也会检查子组件是否需要更新,并通过某种机制避免子组件重复更新。
- 如果组件需要更新
- 执行
invalidateJob(instance.update)
,避免子组件由于自身数据变化导致的重复更新 - 然后又执行了子组件的副作用渲染函数
instance.update
来主动触发子组件的更新。
- 执行
- 结合副作用渲染函数,如何更新组件
...
// 更新组件
let { next, vnode } = instance
// next 表示新的组件 vnode
if (next) {
// 更新组件 vnode 节点信息
updateComponentPreRender(instance, next, optimized)
}
else {
next = vnode
}
const updateComponentPreRender = (instance, nextVNode, optimized) => {
// 新组件 vnode 的 component 属性指向组件实例
nextVNode.component = instance
// 旧组件 vnode 的 props 属性
const prevProps = instance.vnode.props
// 组件实例的 vnode 属性指向新的组件 vnode
instance.vnode = nextVNode
// 清空 next 属性,为了下一次重新渲染准备
instance.next = null
// 更新 props
updateProps(instance, nextVNode.props, prevProps, optimized)
// 更新 插槽
updateSlots(instance, nextVNode.children)
}
...
- 我们在更新组件的
DOM
前,需要先更新组件vnode
节点信息- 包括更改组件实例的
vnode
指针 - 更新
props
和更新插槽等一系列操作
- 包括更改组件实例的
- 因为组件在稍后执行
renderComponentRoot
时会重新渲染新的子树vnode
,它依赖了更新后的组件vnode
中的props
和slots
等数据
一个组件重新渲染可能会有两种场景
- 1、组件本身的数据变化,这种情况下
next
是null
- 2、父组件在更新的过程中,遇到子组件节点,先判断子组件是否需要更新,如果需要则主动执行子组件的重新渲染方法,这种情况下
next
就是新的子组件vnode
processComponent 的本质
- 判断子组件是否需要更新
- 如果需要则递归执行子组件的副作用渲染函数来更新
- 否则仅仅更新一些
vnode
的属性,并让子组件实例保留对组件vnode
的引用 - 用于子组件自身数据变化引起组件重新渲染的时候,在渲染函数内部可以拿到新的组件
vnode
处理普通元素类型
当前要更新的vnode为普通元素,则执行processElement
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
isSVG = isSVG || n2.type === 'svg'
if (n1 == null) {
// 挂载元素
}
else {
// 更新元素
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}
const patchElement = (n1, n2, parentComponent, parentSuspense, isSVG, optimized) => {
const el = (n2.el = n1.el)
const oldProps = (n1 && n1.props) || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
// 更新 props
patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG)
const areChildrenSVG = isSVG && n2.type !== 'foreignObject'
// 更新子节点
patchChildren(n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG)
}
- 主要做两件事情
- 1、更新 props
-
patchProps
函数就是在更新DOM
节点的class
、style
、event
以及其它的一些DOM
属性
-
- 2、更新子节点,执行
patchChildren
函数
const patchChildren = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized = false) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
const { shapeFlag } = n2
// 子节点有 3 种可能情况:文本、数组、空
if (shapeFlag & 8 /* TEXT_CHILDREN */) {
if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {
// 数组 -> 文本,则删除之前的子节点
unmountChildren(c1, parentComponent, parentSuspense)
}
if (c2 !== c1) {
// 文本对比不同,则替换为新文本
hostSetElementText(container, c2)
}
}
else {
if (prevShapeFlag & 16 /* ARRAY_CHILDREN */) {
// 之前的子节点是数组
if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
// 新的子节点仍然是数组,则做完整地 diff
patchKeyedChildren(c1, c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
// 数组 -> 空,则仅仅删除之前的子节点
unmountChildren(c1, parentComponent, parentSuspense, true)
}
}
else {
// 之前的子节点是文本节点或者为空
// 新的子节点是数组或者为空
if (prevShapeFlag & 8 /* TEXT_CHILDREN */) {
// 如果之前子节点是文本,则把它清空
hostSetElementText(container, '')
}
if (shapeFlag & 16 /* ARRAY_CHILDREN */) {
// 如果新的子节点是数组,则挂载新子节点
mountChildren(c2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
}
}
}
- 一个元素的子节点
vnode
可能会有三种情况:纯文本、vnode
数组和空
新旧对比,总共有九种情况
- 1、旧子节点是纯文本
- 如果新子节点也是纯文本,那么做简单地文本替换即可;
- 如果新子节点是空,那么删除旧子节点即可;
- 如果新子节点是
vnode
数组,那么先把旧子节点的文本清空,再去旧子节点的父容器下添加多个新子节点
- 2、旧子节点是空
- 如果新子节点是纯文本,那么在旧子节点的父容器下添加新文本节点即可;
- 如果新子节点也是空,那么什么都不需要做;
- 如果新子节点是
vnode
数组,那么直接去旧子节点的父容器下添加多个新子节点即可
- 3、旧子节点是
vnode
数组- 如果新子节点是纯文本,那么先删除旧子节点,再去旧子节点的父容器下添加新文本节点;
- 如果新子节点是空,那么删除旧子节点即可;
- 如果新子节点也是
vnode
数组,那么就需要做完整的diff
新旧子节点了,这是最复杂的情况,内部运用了核心 diff 算法。
核心 diff 算法
步骤1、同步头部节点
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 3, e2 = 4
// (a b) c d
// (a b) e c d
while (i <= e1 && i <= e2) {
const n1 = c1[i]
const n2 = c2[i]
if (isSameVNodeType(n1, n2)) {
// 相同的节点,递归执行 patch 更新节点
patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
break
}
i++
}
}
- 我们需要维护几个变量:头部的索引
i
、旧子节点的尾部索引e1
和新子节点的尾部索引e2
。 - 从头部开始,依次对比新节点和旧节点
- 如果它们相同的则执行
patch
更新节点 - 如果不同或者索引
i
大于索引e1
或者e2
,则同步过程结束
步骤2、同步尾部节点
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 3, e2 = 4
// (a b) c d
// (a b) e c d
// 2. 从尾部开始同步
// i = 2, e1 = 3, e2 = 4
// (a b) (c d)
// (a b) e (c d)
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = c2[e2]
if (isSameVNodeType(n1, n2)) {
patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
break
}
e1--
e2--
}
}
- 同步尾部节点就是从尾部开始
- 依次对比新节点和旧节点
- 如果相同的则执行
patch
更新节点; - 如果不同或者索引
i
大于索引e1
或者e2
,则同步过程结束
步骤3、添加新的节点
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 3, e2 = 4
// (a b) c d
// (a b) e c d
// ...
// 2. 从尾部开始同步
// i = 2, e1 = 3, e2 = 4
// (a b) (c d)
// (a b) e (c d)
// 3. 挂载剩余的新节点
// i = 2, e1 = 1, e2 = 2
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor
while (i <= e2) {
// 挂载新节点
patch(null, c2[i], container, anchor, parentComponent, parentSuspense, isSVG)
i++
}
}
}
}
- 如果索引
i
大于尾部索引e1
且i
小于e2
- 那么从索引
i
开始到索引e2
之间,我们直接挂载新子树这部分的节点
步骤4、删除多余节点
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 4, e2 = 3
// (a b) c d e
// (a b) d e
// ...
// 2. 从尾部开始同步
// i = 2, e1 = 4, e2 = 3
// (a b) c (d e)
// (a b) (d e)
// 3. 普通序列挂载剩余的新节点
// i = 2, e1 = 2, e2 = 1
// 不满足
if (i > e1) {
}
// 4. 普通序列删除多余的旧节点
// i = 2, e1 = 2, e2 = 1
else if (i > e2) {
while (i <= e1) {
// 删除节点
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
}
- 如果索引
i
大于尾部索引e2
- 那么从索引
i
开始到索引e1
之间,我们直接删除旧子树这部分的节点。
处理未知子序列
移动子节点
var prev = [1, 2, 3, 4, 5, 6]
var next = [1, 3, 2, 6, 4, 5]
- 问题转变为:求解最长递增子序列
- 在查找过程中需要对比新旧子序列,那么我们就要遍历某个序列,如果在遍历旧子序列的过程中需要判断某个节点是否在新子序列中存在,这就需要双重循环,而双重循环的复杂度是
O(n2)
,为了优化这个复杂度,我们可以用一种空间换时间的思路,建立索引图,把时间复杂度降低到O(n)
。
建立索引图
- 通常我们在开发过程中, 会给
v-for
生成的列表中的每一项分配唯一key
作为项的唯一ID
- 这个
key
在diff
过程中起到很关键的作用。对于新旧子序列中的节点,我们认为key
相同的就是同一个节点,直接执行patch
更新即可。 - 根据 key 建立新子序列的索引图
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 7, e2 = 7
// (a b) c d e f g h
// (a b) e c d i g h
// 2. 从尾部开始同步
// i = 2, e1 = 7, e2 = 7
// (a b) c d e f (g h)
// (a b) e c d i (g h)
// 3. 普通序列挂载剩余的新节点, 不满足
// 4. 普通序列删除多余的旧节点,不满足
// i = 2, e1 = 4, e2 = 5
// 旧子序列开始索引,从 i 开始记录
const s1 = i
// 新子序列开始索引,从 i 开始记录
const s2 = i //
// 5.1 根据 key 建立新子序列的索引图
const keyToNewIndexMap = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = c2[i]
keyToNewIndexMap.set(nextChild.key, i)
}
}
- 得到
{e:2,c:3,d:4,i:5}
更新和移除旧节点
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 7, e2 = 7
// (a b) c d e f g h
// (a b) e c d i g h
// 2. 从尾部开始同步
// i = 2, e1 = 7, e2 = 7
// (a b) c d e f (g h)
// (a b) e c d i (g h)
// 3. 普通序列挂载剩余的新节点,不满足
// 4. 普通序列删除多余的旧节点,不满足
// i = 2, e1 = 4, e2 = 5
// 旧子序列开始索引,从 i 开始记录
const s1 = i
// 新子序列开始索引,从 i 开始记录
const s2 = i
// 5.1 根据 key 建立新子序列的索引图
// 5.2 正序遍历旧子序列,找到匹配的节点更新,删除不在新子序列中的节点,判断是否有移动节点
// 新子序列已更新节点的数量
let patched = 0
// 新子序列待更新节点的数量,等于新子序列的长度
const toBePatched = e2 - s2 + 1
// 是否存在要移动的节点
let moved = false
// 用于跟踪判断是否有节点移动
let maxNewIndexSoFar = 0
// 这个数组存储新子序列中的元素在旧子序列节点的索引,用于确定最长递增子序列
const newIndexToOldIndexMap = new Array(toBePatched)
// 初始化数组,每个元素的值都是 0
// 0 是一个特殊的值,如果遍历完了仍有元素的值为 0,则说明这个新节点没有对应的旧节点
for (i = 0; i < toBePatched; i++)
newIndexToOldIndexMap[i] = 0
// 正序遍历旧子序列
for (i = s1; i <= e1; i++) {
// 拿到每一个旧子序列节点
const prevChild = c1[i]
if (patched >= toBePatched) {
// 所有新的子序列节点都已经更新,剩余的节点删除
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
// 查找旧子序列中的节点在新子序列中的索引
let newIndex = keyToNewIndexMap.get(prevChild.key)
if (newIndex === undefined) {
// 找不到说明旧子序列已经不存在于新子序列中,则删除该节点
unmount(prevChild, parentComponent, parentSuspense, true)
}
else {
// 更新新子序列中的元素在旧子序列中的索引,这里加 1 偏移,是为了避免 i 为 0 的特殊情况,影响对后续最长递增子序列的求解
newIndexToOldIndexMap[newIndex - s2] = i + 1
// maxNewIndexSoFar 始终存储的是上次求值的 newIndex,如果不是一直递增,则说明有移动
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
}
else {
moved = true
}
// 更新新旧子序列中匹配的节点
patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, optimized)
patched++
}
}
}
-
patched
为正在更新的索引,toBePatched
为需要更新的长度,newIndexToOldIndexMap
的数组,来存储新子序列节点的索引和旧子序列节点的索引之间的映射关系 -
patched > toBePatched
,表明所有新的子序列节点都已经更新,剩余的节点删除 -
keyToNewIndexMap.get(prevChild.key)
获取不到,表明不在新序列里,删除 - 能找到,放入newIndexToOldIndexMap中,(此时为
index+1
,0
有特殊作用)
移动和挂载新节点
const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
let i = 0
const l2 = c2.length
// 旧子节点的尾部索引
let e1 = c1.length - 1
// 新子节点的尾部索引
let e2 = l2 - 1
// 1. 从头部开始同步
// i = 0, e1 = 6, e2 = 7
// (a b) c d e f g
// (a b) e c d h f g
// 2. 从尾部开始同步
// i = 2, e1 = 6, e2 = 7
// (a b) c (d e)
// (a b) (d e)
// 3. 普通序列挂载剩余的新节点, 不满足
// 4. 普通序列删除多余的节点,不满足
// i = 2, e1 = 4, e2 = 5
// 旧子节点开始索引,从 i 开始记录
const s1 = i
// 新子节点开始索引,从 i 开始记录
const s2 = i //
// 5.1 根据 key 建立新子序列的索引图
// 5.2 正序遍历旧子序列,找到匹配的节点更新,删除不在新子序列中的节点,判断是否有移动节点
// 5.3 移动和挂载新节点
// 仅当节点移动时生成最长递增子序列
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
let j = increasingNewIndexSequence.length - 1
// 倒序遍历以便我们可以使用最后更新的节点作为锚点
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex]
// 锚点指向上一个更新的节点,如果 nextIndex 超过新子节点的长度,则指向 parentAnchor
const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// 挂载新的子节点
patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG)
}
else if (moved) {
// 没有最长递增子序列(reverse 的场景)或者当前的节点索引不在最长递增子序列中,需要移动
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, 2)
}
else {
// 倒序递增子序列
j--
}
}
}
}
-
moved
为true
就通过getSequence(newIndexToOldIndexMap)
计算最长递增子序列 - 采用倒序的方式遍历新子序列,因为倒序遍历可以方便我们使用最后更新的节点作为锚点。
- 锚点指向上一个更新的节点,然后判断
newIndexToOldIndexMap[i]
是否为0
,如果是则表示这是新节点,就需要挂载它 - 接着判断是否存在节点移动的情况,如果存在的话则看节点的索引是不是在最长递增子序列中,如果在则倒序最长递增子序列,否则把它移动到锚点的前面
最长递增子序列
- 动态规划的思想,算法的时间复杂度是
O(n2)
-
vuejs
采用贪心 + 二分查找,总时间复杂度是O(nlogn)
arr:[2, 1, 5, 3, 6, 4, 8, 9, 7]
=> [1, 3, 4, 8, 9]
- 思路
- 对数组遍历
- 当 i 元素大于 i - 1 的元素时,添加 i 元素并更新最长子序列
- 否则往前查找直到找到一个比 i 小的元素,然后插在该元素后面并更新对应的最长递增子序列。
function getSequence (arr) {
const p = arr.slice()
const result = [0]
let i, j, u, v, c
const len = arr.length
for (i = 0; i < len; i++) {
const arrI = arr[i]
if (arrI !== 0) {
j = result[result.length - 1]
if (arr[j] < arrI) {
// 存储在 result 更新前的最后一个索引的值
p[i] = j
result.push(i)
continue
}
u = 0
v = result.length - 1
// 二分搜索,查找比 arrI 小的节点,更新 result 的值
while (u < v) {
c = ((u + v) / 2) | 0
if (arr[result[c]] < arrI) {
u = c + 1
}
else {
v = c
}
}
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1]
}
result[u] = i
}
}
}
u = result.length
v = result[u - 1]
// 回溯数组 p,找到最终的索引
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}