v-permission 对 vue diff的影响

403 阅读2分钟

背景:

代码:

const hasPermission = Vue.directive('hasPermission', {
  inserted(el, binding, vnode) {
    const actionName = binding.arg
    const userPermissions = store.getters.userPermissions
    if (userPermissions && !userPermissions.includes(actionName)) {
      ;(el.parentNode && el.parentNode.removeChild(el)) ||
        (el.style.display = 'none')
    }
  },
})

大部分网络上都能查到类似的代码,用于检查用户是否具有某个权限来显示这个dom

PS: 为了方便理解,加入了2个div

<iframe>test1</iframe>
<a-alert
  v-if="wikiTree.length===0"
  v-hasPermission:App\Model\Wiki\Wikis:post
  message="当前没有文档,请点击上方蓝色加号按钮创建"
/>
<tree-node
  v-for="node in wikiTree"
  :tree-node="node">
</tree-node>
<div>test2</div>

比如这里,当用户没有post权限的时候,它会移除掉这个dom

案发场景

当用户没有wikis:post 权限的时候就会发生,用devtools检查wikiTree已经有数据了,但是treeNode没有渲染出来

理想情况:

image.png

异常情况:

image.png

分析下案发情况,一开始页面加载好,向后端请求wikiTree数据

  1. 所以一开始alert是显示的,接着触发hasPermission发现没有权限,el.parentNode.removeChild(el) 将该节点移除。

  2. 数据到达,alert节点v-if变成false,移除节点,接着treeNode开始循环渲染。

问题分析

Vue 的 diff算法使用双指针比较,这里还原下分析的情况,可以搜索下,掘金很多文章

image.png

注意新的第二个节点是 alert空的(v-if 为false,变为空的VNODE)

VNODE: iframe alert div-test2

新VNODE: iframe alert空的 treeNode div-test2

第一轮: iframe iframe 相同,patch。指针后移
VNODE: alert div-test2

新VNODE: alert空的 treeNode div-test2

第二轮: div-test2 div-test2 相同,patch 接着跳过
VNODE: alert

新VNODE: alert空的 treeNode

第三轮: 创建这个空的vnode,空的alert,指针前移

创建节点

image.png

创建的时候,使用insert函数,将其插入到 原来的 alert 的前面,但是原来的alert已经被我们从 el.parentNode.removeChild(el) 父节点中移除了

image.png

检查插入目标位置的parent的和目标节点前面的那个dom的parent是否一致,如果不一致,将会跳过插入的操作,但是这个检查好像有的时候会通过,会触发后续的报错。

因为原来的alert已经被我们非法移除了,但是vdom不知道,它试图将新的dom插入在其之前,于是导致了节点没有渲染

image.png

VNODE: alert

新VNODE: treeNode

第四轮: 创建这个treeNode,同理将会使用 insert 插入到alert之前,但是alert并没有在父节点上,所以被跳过,于是treeNode也没有渲染
VNODE: alert

新VNODE: 

第五轮: 移除原来的alert

结论

推荐不要非法移除vue创建的节点,会影响其dom diff

el.parentNode && (el.style.display = 'none')

但是这样的结论不够神奇,根据前面的原因分析,有没有神奇的方案解决这个bug呢!

image.png

给这个节点套个DIV即可,这样就不会影响到节点的插入了。