logo
blog
readme
Back to Blog
Back to Blog
Back to Blog

‌

‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌
‌

© 2025 linzhe. All rights reserved.

TODO:编辑

[vue] innerHTML patch edge case with h

我看你提交了一些VUE相关的pr,能说说解决了什么问题吗? 这是一个面试题,但我没准备,所以我要引导到这个pr,因为这个pr稍微有代表性一点,有一点复杂度

具体pr链接

bug playground

首先这是一个edge case就是用户在使用h函数才会发生, 如果是sfc是不会出现的,因为他会把这个分支创建不同的key,从而避免这个问题

ts
// 复现
import { ref, h } from 'vue'
const state = ref(false)
// state.value = true 后
// expect(Comp domStr).toBe('<div><del>baz</del></div>')

// 但是实际情况 Comp domStr = '<div></div>'
setTimeout(() => {
  state.value = true
}, 1000)
const Comp = {
  render: () => {
    if (state.value) {
      return h('div', [h('del', null, 'baz')])
    } else {
      return h('div', { innerHTML: 'baz' })
    }
  },
}
export default Comp

需求:

  • 分支1通过innerHTML设置内容
  • 分支2通过h函数的children设置内容

但这两个分支没有设置key,并且是同一个类型的标签,导致在patch时,vue认为这两个元素是同一个元素,vue只需要 patch element。

在没有修改这个bug之前 vue首先会patch 这两个vnode的children,因为n1没有children,而n2有children,所以他会挂载n2的children, 也就是<del>baz</del>,接着再进行patchProps,但是由于n1的props是{innerHTML: 'baz'}, n2的props是null,所以vue会将n1的props清空,也就是把这个divDomElement.innerHTML = '', 但是这个时候已经patchChildren了,<del>baz</del>已经被挂载到divDomElement上了,所以最后的结果就是<div></div>,而不是预期的<div><del>baz</del></div>。

ts
// n1 {type: 'div', props: {innerHTML: 'baz'}, children: null, el: divDomElement}
// n2 {type: 'div', props: null, children: [{type: 'del', props: null, children: 'baz'}], el: divDomElement }
patchChildren(n1, n2 /*...*/)

patchProps(
  el /*divDomElement*/,
  n2,
  oldProps /*{innerHTML: 'baz'}*/,
  newProps /*null=>{}*/
  /*...*/
)

怎么解决这个问题呢?

就是在 patch element 时,如果有这种情况。就先把dom的innerHTML清空,然后再进行patchChildren和patchProps。

ts
// ADD: 在patchElement中处理innerHTML和textContent的情况
if (
  (oldProps.innerHTML && newProps.innerHTML == null) ||
  (oldProps.textContent && newProps.textContent == null)
) {
  hostSetElementText(el, '')
}
...
patchChildren()
patchProps()

并且在patchProps时,不要直接设置el.innerHTML = value而是跳过,因为我们已经在patchChildren之前处理了。

ts
// patchProps -> patchDOMProp
if (key === 'innerHTML' || key === 'textContent') {
  // 修改后
  // null value case is handled in renderer patchElement before patching
  // children
  if (value == null) return
  el[key] = value
  return
}