背景:
代码:
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没有渲染出来
理想情况:
异常情况:
分析下案发情况,一开始页面加载好,向后端请求wikiTree数据
-
所以一开始alert是显示的,接着触发hasPermission发现没有权限,
el.parentNode.removeChild(el)将该节点移除。 -
数据到达,alert节点v-if变成false,移除节点,接着treeNode开始循环渲染。
问题分析
Vue 的 diff算法使用双指针比较,这里还原下分析的情况,可以搜索下,掘金很多文章
注意新的第二个节点是 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,指针前移
创建节点
创建的时候,使用insert函数,将其插入到 原来的 alert 的前面,但是原来的alert已经被我们从 el.parentNode.removeChild(el) 父节点中移除了
检查插入目标位置的parent的和目标节点前面的那个dom的parent是否一致,如果不一致,将会跳过插入的操作,但是这个检查好像有的时候会通过,会触发后续的报错。
因为原来的alert已经被我们非法移除了,但是vdom不知道,它试图将新的dom插入在其之前,于是导致了节点没有渲染
旧VNODE: alert
新VNODE: treeNode
第四轮: 创建这个treeNode,同理将会使用 insert 插入到alert之前,但是alert并没有在父节点上,所以被跳过,于是treeNode也没有渲染
旧VNODE: alert
新VNODE:
第五轮: 移除原来的alert
结论
推荐不要非法移除vue创建的节点,会影响其dom diff
el.parentNode && (el.style.display = 'none')
但是这样的结论不够神奇,根据前面的原因分析,有没有神奇的方案解决这个bug呢!
给这个节点套个DIV即可,这样就不会影响到节点的插入了。