一文详解Vue3中简单diff算法的实现

目录
  • 简单Diff算法
  • 减少DOM操作
    • 例子
    • 结论
    • 实现
  • DOM复用与key的作用
    • 例子
    • 虚拟节点的key
    • 实现
  • 找到需要移动的元素
    • 探索节点顺序关系
    • 实现
  • 如何移动元素
    • 例子
    • 实现
  • 添加新元素
    • 例子
    • 实现
  • 移除不存在的元素
    • 例子
    • 实现
  • 总结

简单Diff算法

核心Diff只关心新旧虚拟节点都存在一组子节点的情况

减少DOM操作

例子

// 旧节点
const oldVNode = {
  type: 'div',
  children: [
    { type: 'p', children: '1' },
    { type: 'p', children: '2' },
    { type: 'p', children: '3' }
  ]
}
// 新节点
const newVNode = {
  type: 'div',
  children: [
    { type: 'p', children: '4' },
    { type: 'p', children: '5' },
    { type: 'p', children: '6' }
  ]
}

如果直接去操作DOM,那么上面的更新需要6次DOM操作,卸载所有旧子节点,挂载所有新子节点。

但是观察上面新旧vNode的子节点可以发现:

  • 更新前后所有子节点都是 p 标签,即标签元素步变
  • 只有p标签的子节点发生变化了

所以最理想的更新方式是直接更新这个p标签的文本节点的内容,这样只需要一次DOM操作,即可完成一个p标签的更新。更新完所有节点只需要3次DOM操作就可以完成全部节点的更新。

上面的做法可以减少DOM操作次数,但问题也很明显,只有节点数量相同这个做法才能正常工作。但新旧两组子节点数量未必相同。

新的一组子节点数量少于旧的一组子节点的数量时,意味着有节点在更新后应该被卸载。(图二)

新的一组子节点数量多余旧的一组子节点的数量时,意味着有节点在更新后应该被新增并挂载。(图三)

结论

通过上面分析得出,进行新旧两组子节点的更新时,不应该总是遍历旧的一组子节点或新的一组子节点,而是应该遍历其中较短的一组。这样才能尽可能多的调用patch进行更新。接着对比新旧两组子节点的长度,如果新的一组子节点更长,说明有新节点需要挂载,否则说明有旧的子节点需要卸载。

实现

function easyDiff (n1, n2, container) {
  // 取出新旧子节点列表
  const oldChildren = n1.children
  const newChildren = n2.children
  // 获取新旧子节点列表的长度
  const oldLen = oldChildren.length
  const newLen = newChildren.length
  // 取得较小的一个(可以理解为两组子节点的公共长度)
  const commonLength = Math.min(oldLen, newLen)
  // 遍历 commonLength 次
  for (let i = 0; i < commonLength; i++) {
    patch(oldChildren[i], newChildren[i], container)
  }
  // 如果 newLen > oldLen,说明有新子节点需要挂载
  if (newLen > oldLen) {
    for (let i = commonLength; i < newLen; i++) {
      patch(null, newChildren[i], container)
    }
  }
  // 如果 oldLen > newLen,说明有旧节点需要卸载
  if (oldLen > newLen) {
    for (let i = commonLength; i < oldLen; i++) {
      unmount(oldChildren[i])
    }
  }
}

DOM复用与key的作用

例子

上面通过减少DOM操作次数提升了更新性能,但还存在可优化空间

const KEY = {
  oldVNode: [
    { type: 'p' },
    { type: 'div' },
    { type: 'span' }
  ],
  newVNode: [
    { type: 'span' },
    { type: 'p' },
    { type: 'div' }
  ]
}

针对这个例子,如果还使用上面的算法,则需要6次DOM操作。

调用 patch 在 p标签和span标签之间打补丁,由于不是相同标签,所以p标签被卸载,然后挂载span标签,需要两步操作,div - p,span - div同理。

很容易发现新旧两组子节点只是顺序不同。所以最优的处理方式是,通过DOM的移动来完成子节点的更新,这比不断执行卸载和挂载性能好得多。但是要通过移动DOM来完成更新,必须要保证新旧两组子节点的确存在可复用的节点。(如果新的子节点没有在旧的子节点中出现,则无法通过移动节点的方式完成更新操作。)

用上面的例子来说,怎么确定新的一组节点中的第三个节点 { type: 'div' } 与旧的一组子节点中的第二个节点相同呢?可以通过vNode.type判断,但这种方式并不可靠。

  oldChildren: [
    { type: 'p', children: '1' },
    { type: 'p', children: '2' },
    { type: 'p', children: '3' }
  ],
  newChildren: [
    { type: 'p', children: '3' },
    { type: 'p', children: '1' },
    { type: 'p', children: '2' }
  ]

观察上面节点,可以发现,这个案例可以通过移动DOM的方式来完成更新,但是vNode.type的值都相同,导致无法确定新旧节点中的对应关系,就不能确定怎么移动DOM完成更新。

虚拟节点的key

因此,需要引入额外的 key 作为vNode的标识。

const KEY = {
  oldChildren: [
    { type: 'p', children: '1', key: '1' },
    { type: 'p', children: '2', key: '2' },
    { type: 'p', children: '3', key: '3' }
  ],
  newChildren: [
    { type: 'p', children: '3', key: '3' },
    { type: 'p', children: '1', key: '1' },
    { type: 'p', children: '2', key: '2' }
  ]
}

key 属性就像虚拟DOM的 身份证号,只要两个虚拟节点的type和key属性都相同,那么就可以认为它们是相同的;即可以进行DOM的复用。

但是DOM可复用并不意味着不需要更新

oldVNode: { type: 'p', children: 'text - 1', key: '1' }
newVNode: { type: 'p', children: 'text - 2', key: '1' }

两个节点有相同的key可type,但它们的文本内容不同,还是需要通过patch进行打补丁操作。

实现

function easyDiffV2 (n1, n2, container) {
  // 取出新旧子节点列表
  const oldChildren = n1.children
  const newChildren = n2.children
  // 遍历新的children
  for (let i = 0; i < newChildren.length; i++) {
    const newVNode = newChildren[i]
    for (let j = 0; j < oldChildren.length; j++) {
      const oldVNode = oldChildren[i]
      // 如果找到可复用的两个节点
      if (newVNode.key === oldVNode.key) {
        // 对可复用的两个节点打补丁
        patch(oldVNode, newVNode, container)
        // 一个新节点处理完后开始下一个新节点
        break
      }
    }
  }
}

外层循环遍历新的一组子节点,内层循环遍历旧的一组子节点。内层循环中对比新旧子节点的key值,在旧的子节点中找到可以复用的节点;一旦找到则调用 patch 打补丁。

找到需要移动的元素

现在已经可以通过key找到可复用的节点了,接下来要做的是判断一个节点是否需要移动

探索节点顺序关系

节点顺序不变 - 查找过程:

第一步:取新的一组子节点中的第一个节点 p - 1,它的key为1,在旧的一组子节点中找到具有相同key值的可复用节点,能够找到,并且该节点在旧的一组子节点中索引为0;p - 2、p = 3同理。

  • key 为 1 的节点在 旧节点列表中的索引为0
  • key 为 2 的节点在 旧节点列表中的索引为1
  • key 为 3 的节点在 旧节点列表中的索引为2

每一次查找可复用节点都会记录该可复用节点在旧的一组子节点中的位置索引,如果按照先后顺序排列,则可以得到一个序列:0、1、2,是一个递增序列。

节点顺序变化 - 查找过程

第一步:取新的一组子节点中的第一个节点 p - 3,它的key为3,在旧的一组子节点中找到具有相同key值的可复用节点,能够找到,并且该节点在旧的一组子节点中索引为2;

第二步:取新的一组子节点中的第一个节点 p - 1,它的key为1,在旧的一组子节点中找到具有相同key值的可复用节点,能够找到,并且该节点在旧的一组子节点中索引为0;

到了这一步发现递增的顺序被打破了。节点 p - 1 在旧的一组children 的索引为0,它小于 p - 3 在旧children中的索引2.这说明节点 p - 1 在旧children中排在 p - 3前面,但在新的children中,它排在节点 p - 3后面。因此得出:节点p - 1对应的真实DOM需要移动

第三步:取新的一组子节点中的第一个节点 p - 2,它的key为2,在旧的一组子节点中找到具有相同key值的可复用节点,能够找到,并且该节点在旧的一组子节点中索引为1;

节点 p - 2 在旧的一组children 的索引为0,它小于 p - 3 在旧children中的索引2.这说明节点 p - 2 在旧children中排在 p - 3前面,但在新的children中,它排在节点 p - 3后面。因此得出:**节点p - 2对应的真实DOM需要移动

可以将节点 p - 3 在旧children中的索引定义为:在旧children中寻找具有相同key值节点的过程中,遇到的最大索引值

如果后续寻找过程中,存在比当前遇到的最大索引值还要小的节点,则意味着该节点需要移动。

实现

function easyBigIndex (n1, n2, container) {
  // 取出新旧子节点列表
  const oldChildren = n1.children
  const newChildren = n2.children
  // 用来存储寻找过程中遇到的最大索引值
  let lastIndex = 0
  for (let i = 0; i < newChildren.length; i++) {
    const newVNode = newChildren[i]
    for (let j = 0; j < oldChildren; j++) {
      const oldVNode = oldChildren[j]
      if (newVNode.key === oldVNode.key) {
        patch(oldVNode, newVNode, container)
        if (j < lastIndex) {
          // 需要移动
        } else {
          // 更新lastIndex的值(lastIndex要保持当前已查找的索引中的最大值)
          lastIndex = j
        }
        break
      }
    }
  }
}

如何移动元素

移动节点指的是,移动一个虚拟节点所对应的真实DOM节点,并不是移动虚拟节点本身。既然移动的是真实DOM节点,就需要取得它的引用,其对应的真实DOM节点会存储到它的vNode.el属性中

例子

引用上面的案例:

取新的一组子节点中的第一个节点 p - 3,它的key 为3,在旧的虚拟节点列表中找到具有相同 key 值的可复用节点。发现能够找到,并且该节点在旧的一组子节点中的素引为2。此时变量 lastIndex 的值为 0,索引2 不小于0,所以节点 p - 3对应的真实DOM 不需要移动,但需要更新变量 lastIndex 的值为 2。

第二步:取新的一组子节点中第二个节点 p - 1,它的key 为1,在旧的一组子节点中找到具有相同 key 值的可复用节点。发现能够找到,并且该节点在日的一组子节点中的索引为0。此时变量 lastIndex 的值为 2,索引0小于 2,所以节点p-1对应的真实 DOM需要移动

到了这一步,我们发现,节点p - 1对应的真实 DOM 需要移动,但应该移动到哪里呢?新children 的顺序其实就是更新后真实 DOM 节点应有的顺序。所以节点 p-1在新 children 中的位置就代表了真实 DOM 更新后的位置。由于节点 p - 1在新 children 中排在节点p - 3后面,所以我们应该把节点p - 1所对应的真实 DOM移动到节点p - 3所对应的真实 DOM 后面。这样操作之后,此时真实 DOM 的顺序为 p-2、p-3、p-1。

第三步:取新的一组子节点中第三个节点 p-2,它的key 为2。尝试在旧的一组子节点中找到具有相同 key 值的可复用节点。发现能够找到,并且该节点在旧的一组子节点中的素引为1。此时变量 lastIndex 的值为 2,索引1小于2,所以节点p-2对应的真实 DOM需要移动

第二步操作完成后 新 / 旧 / 虚拟 节点之间的对应关系

实现

function easyMove (n1, n2, container) {
  // 取出新旧子节点列表
  const oldChildren = n1.children
  const newChildren = n2.children
  // 用来存储寻找过程中遇到的最大索引值
  let lastIndex = 0
  for (let i = 0; i < newChildren.length; i++) {
    const newVNode = newChildren[i]
    for (let j = 0; j < oldChildren; j++) {
      const oldVNode = oldChildren[j]
      if (newVNode.key === oldVNode.key) {
        patch(oldVNode, newVNode, container)
        if (j < lastIndex) {
          // 需要移动
          // 获取当前vNode的前一个vNode
          const prevVNode = newChildren[i - 1]
          // 如果 prevVNode 不存在,说明当前vNode是第一个节点,它不需要移动
          if (prevVNode) {
            // 由于要将newVNode对用的真实DOM移动到prevVNode对应的真实DOM后面,
            // 所以需要获取prevVNode对应的真实节点的下一个兄弟节点,并将其作为锚点
            const anchor = prevVNode.el.nextSibling
            // 调用insert将newVNode对应真实DOM插入到锚点元素前面
            // insert 是通过 el.insertBefore 插入元素的
            insert(newVNode.el, container, anchor)
          }
        } else {
          // 更新lastIndex的值(lastIndex要保持当前已查找的索引中的最大值)
          lastIndex = j
        }
        break
      }
    }
  }
}

添加新元素

例子

在新的一组子节点中,多出来一个 p - 4,它的key值为4,该节点在旧的一组字节点中不存在,因此应该将其视为新增节点。对于新增节点,更新时应该正确地将其挂载:

  • 找到新增节点
  • 将新增节点挂载到正确位置

第一步:取新的一组子节点中第一个节点p - 3,它的key值为3,在旧的一组子节及中找到可复用的节点。发现能找到,并且该节点在旧的一组子节点中的索引值为2。此时,变量lastIndex的值为0,所以节点 p - 3 对应的真实 DOM 不需要移动,但是需要将变量 lastIndex 的值更新为 2。

第二步:取新的一组子节点中第一个节点p - 1,它的key值为1,在旧的一组子节及中找到可复用的节点。发现能找到,并且该节点在旧的一组子节点中的索引值为1。此时变量lastIndex的值为2,所以节点 p - 1对应的真实DOM需要移动,并且应该移动到节点 p - 3对应的真实DOM后面。

第三步:取新的一组子节点中第一个节点p - 4,它的key值为4,在旧的一组子节及中找到可复用的节点。没有key值为4的节点,因此渲染器会把节点 p - 4 看作新增节点并挂载它。应该挂载到什么地方呢?观察p - 4在新的一组子节点中的位置。由于 p - 4出现在节点 p - 1后面,所以应该把 p - 4 挂载到节点 p - 1 对应的真实DOM后面。

第四步:取新的一组子节点中第一个节点p - 2,它的key值为2,在旧的一组子节及中找到可复用的节点。发现能找到,并且该节点在旧的一组子节点中的索引值为1。此时,变量lastIndex的值为2,索引值1小于lastIndex的值2,所以节点 p - 2对应的真实DOM需要移动,并且应该移动到节点 p - 4对应的真实DOM后面。

第二步操作完成后的节点对应关系

第三步操作完成后的节点对应关系

实现

function easyMount (n1, n2, container) {
  // 取出新旧子节点列表
  const oldChildren = n1.children
  const newChildren = n2.children
  // 用来存储寻找过程中遇到的最大索引值
  let lastIndex = 0
  for (let i = 0; i < newChildren.length; i++) {
    const newVNode = newChildren[i]

    // 定义变量 find,代表是否在旧的一组子节点中找到可复用的节点,初始值为false - 没找到
    let find = false
    for (let j = 0; j < oldChildren; j++) {
      const oldVNode = oldChildren[j]
      if (newVNode.key === oldVNode.key) {
        // 一旦找到可复用的节点,将变量find设置为true
        find = true
        patch(oldVNode, newVNode, container)
        if (j < lastIndex) {
          // 需要移动
          // 获取当前vNode的前一个vNode
          const prevVNode = newChildren[i - 1]
          // 如果 prevVNode 不存在,说明当前vNode是第一个节点,它不需要移动
          if (prevVNode) {
            // 由于要将newVNode对用的真实DOM移动到prevVNode对应的真实DOM后面,
            // 所以需要获取prevVNode对应的真实节点的下一个兄弟节点,并将其作为锚点
            const anchor = prevVNode.el.nextSibling
            // 调用insert将newVNode对应真实DOM插入到锚点元素前面
            // insert 是通过 el.insertBefore 插入元素的
            insert(newVNode.el, container, anchor)
          }
        } else {
          // 更新lastIndex的值(lastIndex要保持当前已查找的索引中的最大值)
          lastIndex = j
        }
        break
      }
    }
    // 这里find如果还是false,说明当前newVNode没有在旧的一组子节点中找到可复用的节点
    // 也就是说当前 newVNode 是新增节点,需要挂载
    if (!find) {
      // 为了将节点挂载到正确位置,需要先获取锚点元素
      // 首先获取当前newVNode的前一个vNode节点
      const prevVNode = newChildren[i - 1]
      let anchor = null
      if (prevVNode) {
        // 如果有前一个vNode节点,则使用它的下一个兄弟节点作为锚点元素
        anchor = prevVNode.el.nextSibling
      } else {
        // 如果没有前一个vNode节点,说明即将挂载的新节点是第一个子节点
        // 这是使用容器元素的firstChild作为锚点
        anchor = container.firstChild
      }
      // 挂载 newVNode
      patch(null, newVNode, container, anchor)
    }
  }
}

移除不存在的元素

例子

在新的一组节点中,节点 p - 2 不存在了,说明该节点被删除,渲染器应该能找到那些需要删除的节点并正确地将其删除。

找到需要删除的节点 - 步骤:

  • 第一步:取新的一组子节点中的第一个节点p - 3,它的key 值为3,在旧的一组子节点中寻找可复用的节点。发现能够找到,并且该节点在旧的一组子节点中的索引值为2。此时变量 lastIndex 的值为0,索引2不小于lastIndex 的值0,所以节点p - 3对应的真实 DOM 不需要移动,但需要更新变量 lastIndex 的值为 2。
  • 第二步:取新的一组子节点中的第二个节点 p - 1,它的key 值为1。尝试在旧的一组子节点中寻找可复用的节点。发现能够找到,并且该节点在旧的一组子节点中的索引值为0。此时变量 lastIndex 的值为2,索引0小于 lastIndex 的值 2, 所以节点p-1对应的真实 DOM需要移动,并且应该移动到节点p-3对应的真实 DOM 后面经过这一步的移动操作后,发现 p - 3和p - 1都有了对应的真实DOM节点。
  • 至此更新结束,但 p - 2 对应的真实DOM仍然存在,所以需要增加额外的逻辑来删除遗留节点。当基本的更新结束时,需要遍历旧的一组子节点,然后去新的一组子节点中寻找具有相同key值的节点。如果找不到,说明应该删除该节点。

p - 2与任何newVNode没有对应关系

实现

function easyUnmount (n1, n2, container) {
  // 取出新旧子节点列表
  const oldChildren = n1.children
  const newChildren = n2.children
  // 用来存储寻找过程中遇到的最大索引值
  let lastIndex = 0
  for (let i = 0; i < newChildren.length; i++) {
    const newVNode = newChildren[i]

    // 定义变量 find,代表是否在旧的一组子节点中找到可复用的节点,初始值为false - 没找到
    let find = false
    for (let j = 0; j < oldChildren; j++) {
      const oldVNode = oldChildren[j]
      if (newVNode.key === oldVNode.key) {
        // 一旦找到可复用的节点,将变量find设置为true
        find = true
        patch(oldVNode, newVNode, container)
        if (j < lastIndex) {
          // 需要移动
          // 获取当前vNode的前一个vNode
          const prevVNode = newChildren[i - 1]
          // 如果 prevVNode 不存在,说明当前vNode是第一个节点,它不需要移动
          if (prevVNode) {
            // 由于要将newVNode对用的真实DOM移动到prevVNode对应的真实DOM后面,
            // 所以需要获取prevVNode对应的真实节点的下一个兄弟节点,并将其作为锚点
            const anchor = prevVNode.el.nextSibling
            // 调用insert将newVNode对应真实DOM插入到锚点元素前面
            // insert 是通过 el.insertBefore 插入元素的
            insert(newVNode.el, container, anchor)
          }
        } else {
          // 更新lastIndex的值(lastIndex要保持当前已查找的索引中的最大值)
          lastIndex = j
        }
        break
      }
    }
    // 这里find如果还是false,说明当前newVNode没有在旧的一组子节点中找到可复用的节点
    // 也就是说当前 newVNode 是新增节点,需要挂载
    if (!find) {
      // 为了将节点挂载到正确位置,需要先获取锚点元素
      // 首先获取当前newVNode的前一个vNode节点
      const prevVNode = newChildren[i - 1]
      let anchor = null
      if (prevVNode) {
        // 如果有前一个vNode节点,则使用它的下一个兄弟节点作为锚点元素
        anchor = prevVNode.el.nextSibling
      } else {
        // 如果没有前一个vNode节点,说明即将挂载的新节点是第一个子节点
        // 这是使用容器元素的firstChild作为锚点
        anchor = container.firstChild
      }
      // 挂载 newVNode
      patch(null, newVNode, anchor)
    }
  }

  // 更新操作完成后,遍历旧的一组子节点
  for (let i = 0; i < oldChildren.length; i++) {
    const oldVNode = oldChildren[i]
    // 拿旧子节点oldVNode去新的一组子节点中寻找具有相同key值的节点
    const has = newChildren.find(
      vNode => vNode.key === oldVNode.key
    )
    if (has) {
      // 如果没找到具有相同key值的节点,则说明需要删除该节点,调用unmount函数将其卸载
      unmount(oldVNode)
    }
  }
}

总结

遍历新旧子结点中较少的一组,逐个调用patch进行打补丁,然后比较新旧两组子节点的数量,如果新的一组子节点数量更多,说明有新节点需要挂载;否则说明在旧的一组子节点中,有节点需要卸载。

引入key属性,就像虚拟节点的身份证号,通过key找到可复用的节点,然后尽可能通过DOM移动操作来完成更新,避免过多地对DOM元素进行销毁和重建。

简单Diff算法地核心逻辑是,拿新的一组子节点中地节点去旧的一组子节点中寻找可复用地节点,如果找到了,则记录该节点地位置索引。在整个更新过程中,如果一个节点地索引值小于最大索引,则说明该节点对应地真实DOM元素需要移动。

以上就是一文详解Vue3中简单diff算法的实现的详细内容,更多关于Vue简单diff算法的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解vue的diff算法原理

    我的目标是写一个非常详细的关于diff的干货,所以本文有点长.也会用到大量的图片以及代码举例,目的让看这篇文章的朋友一定弄明白diff的边边角角. 先来了解几个点... 1. 当数据发生变化时,vue是怎么更新节点的? 要知道渲染真实DOM的开销是很大的,比如有时候我们修改了某个数据,如果直接渲染到真实dom上会引起整个dom树的重绘和重排,有没有可能我们只更新我们修改的那一小块dom而不要更新整个dom呢?diff算法能够帮助我们. 我们先根据真实DOM生成一颗 virtual DOM ,当

  • vue3 diff 算法示例

    目录 一.可能性(常见): 二.找规律 三.算法优化 最长的递增子序列 完整代码 一.可能性(常见): 1. 旧的:a b c新的:a b c d 2. 旧的:  a b c新的:d a b c 3. 旧的:a b c d新的:a b c 4. 旧的:d a b c新的:  a b c 5. 旧的:a b c d e i f g新的:a b e c d h f g 对应的真实虚拟节点(为方便理解,文中用字母代替): // vnode对象 const a = { type: 'div', // 标

  • 简单谈谈Vue中的diff算法

    目录 概述 虚拟Dom(virtual dom) 原理 实现过程 patch方法 sameVnode函数 patchVnode函数 updateChildren函数 结语 概述 diff算法,可以说是Vue的一个比较核心的内容,之前只会用Vue来进行一些开发,具体的核心的内容其实涉猎不多,最近正好看了下这方面的内容,简单聊下Vue2.0的diff算法的实现吧,具体从几个实现的函数来进行分析 虚拟Dom(virtual dom) virtual DOM是将真实的DOM的数据抽取出来,以对象的形式模

  • vue.js diff算法原理详细解析

    目录 diff算法的概念 虚拟Dom h函数 diff对比规则 patch patchVnode updateChildren 总结 diff算法的概念 diff算法可以看作是一种对比算法,对比的对象是新旧虚拟Dom.顾名思义,diff算法可以找到新旧虚拟Dom之间的差异,但diff算法中其实并不是只有对比虚拟Dom,还有根据对比后的结果更新真实Dom. 虚拟Dom 上面的概念我们提到了虚拟Dom,相信大家对这个名词并不陌生,下面为大家解释一下虚拟Dom的概念,以及diff算法中为什么要对比虚拟

  • Vue3组件更新中的DOM diff算法示例详解

    目录 同步头部节点 同步尾部节点 添加新的节点 删除多余节点 处理未知子序列 移动子节点 建立索引图 更新和移除旧节点 移动和挂载新节点 最长递增子序列 总结 总结 在vue的组件更新过程中,新子节点数组相对于旧子节点数组的变化,无非是通过更新.删除.添加和移动节点来完成,而核心 diff 算法,就是在已知旧子节点的 DOM 结构.vnode 和新子节点的 vnode 情况下,以较低的成本完成子节点的更新为目的,求解生成新子节点 DOM 的系列操作. 举例来说,假说我们有一个如下的列表 <ul>

  • Vue的diff算法原理你真的了解吗

    目录 思维导图 0.从常见问题引入 1.生成虚拟dom 1.h方法实现 2.render方法实现 3.再次渲染 2.diff算法 1.对常见的dom做优化 情况1:末尾追加一个元素(头和头相同) 情况2:队首添加一个节点(尾和尾) 情况3:翻转类型(头和尾) 情况4:暴力比对复用 对于key的探讨 1.为什么不能没有key 2.为什么key不能是index 3.diff的遍历方式 总结 思维导图 0. 从常见问题引入 虚拟dom是什么? 如何创建虚拟dom? 虚拟dom如何渲染成真是dom? 虚

  • 一文详解Vue3中简单diff算法的实现

    目录 简单Diff算法 减少DOM操作 例子 结论 实现 DOM复用与key的作用 例子 虚拟节点的key 实现 找到需要移动的元素 探索节点顺序关系 实现 如何移动元素 例子 实现 添加新元素 例子 实现 移除不存在的元素 例子 实现 总结 简单Diff算法 核心Diff只关心新旧虚拟节点都存在一组子节点的情况 减少DOM操作 例子 // 旧节点 const oldVNode = { type: 'div', children: [ { type: 'p', children: '1' },

  • 一文详解Vue3中使用ref获取元素节点

    目录 静态绑定 onMounted nextTick watchEffect watch v-for中使用 动态绑定 ref设置函数 通过getCurrentInstance方法 获取vue实例 前言: 本文介绍在vue3的setup中使用composition API获取元素节点的几种方法: 静态绑定 仅仅需要申明一个ref的引用,用来保存元素,在template中,不必bind引用(:ref="domRef"),只需要声明一个同名的ref属性(ref="domRef&qu

  • 详解Python中图像边缘检测算法的实现

    目录 写在前面 1.一阶微分算子 1.1 Prewitt算子 1.2 Sobel算子 2.二阶微分算子 2.1 Laplace算子 2.2 LoG算子 3.Canny边缘检测 写在前面 从本节开始,计算机视觉教程进入第三章节——图像特征提取.在本章,你会见到一张简简单单的图片中蕴含着这么多你没注意到的细节特征,而这些特征将会在今后更高级的应用中发挥着极其重要的作用.本文讲解基础特征之一——图像边缘. 本文采用面向对象设计,定义了一个边缘检测类EdgeDetect,使图像边缘检测算法的应用更简洁,

  • 详解Vue3中对VDOM的改进

    前言 vue-next 对virtual dom的patch更新做了一系列的优化,从编译时加入了 block 以减少 vdom 之间的对比次数,另外还有 hoisted 的操作减少了内存的开销.本文写给自己看,做个知识点记录,如有错误,还请不吝赐教. VDOM VDOM的概念简单来说就是用js对象来模拟真实DOM树.由于MV**的架构,真实DOM树应该随着数据(Vue2.x中的data)的改变而发生改变,这些改变可能是以下几个方面: v-if v-for 动态的props(如:class,@cl

  • 详解Vue3中Teleport的使用

    在本文中,我们将介绍: Teleport 的目的 Teleport 的例子 一些很有意思的代码交互 Teleport 的目的 首先是什么时候以及使用这个 Teleport 功能. 在开发较大的 Vue 项目时应该以可重用的逻辑去组织代码.但是当处理某些类型的组件(如模式.通知或工具提示)时,模板 HTML 的逻辑可能不会和我们希望渲染元素处于相同的文件中. 实际上在大多数情况下,与 Vue 的 DOM 完全分开处理相比,处理这些元素要容易得多.因为嵌套组件的位置.z-index 和样式等这些东西

  • 一文详解JS中的事件循环机制

    目录 前言 1.JavaScript是单线程的 2.同步和异步 3.事件循环 前言 我们知道JavaScript 是单线程的编程语言,只能同一时间内做一件事,按顺序来处理事件,但是在遇到异步事件的时候,js线程并没有阻塞,还会继续执行,这又是为什么呢?本文来总结一下js 的事件循环机制. 1.JavaScript是单线程的 JavaScript 是一种单线程的编程语言,只有一个调用栈,决定了它在同一时间只能做一件事.在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保证代码的有序执行.在

  • 一文详解Java中的类加载机制

    目录 一.前言 二.类加载的时机 2.1 类加载过程 2.2 什么时候类初始化 2.3 被动引用不会初始化 三.类加载的过程 3.1 加载 3.2 验证 3.3 准备 3.4 解析 3.5 初始化 四.父类和子类初始化过程中的执行顺序 五.类加载器 5.1 类与类加载器 5.2 双亲委派模型 5.3 破坏双亲委派模型 六.Java模块化系统 一.前言 Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最 终形成可以被虚拟机直接使用的Java类型,这个过程

  • 一文详解C++中运算符的使用

    目录 一.算术运算符 二.关系运算符 三.逻辑运算符 四.位运算符 五.赋值运算符 六.杂项运算符 一.算术运算符 运算符 描述 + 把两个操作数相加 - 从第一个操作数中减去第二个操作数 * 把两个操作数相乘 / 分子除以分母 % 取模运算符,整除后的余数 ++ 自增运算符,整数值增加 1 – 自减运算符,整数值减少 1 通过下面的例子可以让我们更好的理解C++中的运算符的意义与使用方法. #include <iostream> using namespace std; int main()

  • 一文详解Python中PO模式的设计与实现

    目录 什么是PO模式 PO 三层模式 PO 设计模式的优点 将改写的脚本转为PO设计模式 构建基础的 BasePage 层 构建首页的 Page 层(HomePage) 构建登录页的 Page 层(LoginPage) 构建 首页 - 订单 - 支付 流程的 Page 层(OrderPage) PO 设计模式下测试Case的改造 在使用 Python 进行编码的时候,会使用自身自带的编码设计格式,比如说最常见的单例模式,稍微抽象一些的抽象工厂模式等等… 在利用 Python 做自动化测试的时候,

  • 一文详解Vue3响应式原理

    目录 回顾 vue2.x 的响应式 vue3的响应式 Reflect 回顾 vue2.x 的响应式 实现原理: 对象类型:通过object.defineProperty()对属性的读取.修改进行拦截(数据劫持) 数组类型:通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹) Object.defineProperty(data,'count ",{ get(){}, set(){} }) 存在问题: 新增属性.删除属性,界面不会更新 直接通过下标修改数组,界面不会自动更新 但是

随机推荐