一不小心又来了一个线上问题。搜了网上的资料,有一些其他的情况,目前我遇到的是与v-if
相关的,仅记录此例。
场景
同一个 div 上,既绑定了 v-else
又绑定了一个自定义指令。
- 组件刚挂载时,
v-if
不满足,显示该 div - 该 div 执行
mounted
钩子函数时,自定义指令会把该元素移除 - 过了一会,
v-if
条件满足,然后触发 vue 的派发更新,此时 vue 内部抛出异常
测试文件:
html
<script src="https://cdn.bootcdn.net/ajax/libs/vue/3.3.4/vue.global.js"></script>
<div id="app">
<div v-if="hasData">hasData</div>
<div v-else v-permission-or="['admin']">else</div>
</div>
<script>
const { ref, nextTick } = window.Vue
// 用户的权限只有 dev
const myPermissions = ['dev']
// mock 请求
const request = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve()
}, 500)
})
}
Vue.createApp({
directives: {
'permission-or': {
mounted(el, binding, vnode, prevVnode) {
if (!myPermissions.includes(binding.value)) {
// 用户是没有权限的,所以必然会执行此处
el.remove()
// 移除后,其父节点为 null
console.log('el.parentNode', el.parentNode)
}
}
}
},
setup() {
const hasData = ref(false)
request().then(() => {
hasData.value = true
})
return { hasData }
}
}).mount('#app')
</script>
源码分析
首先依赖收集部分就不赘述了,在依赖更新时,会触发effect
:
componentUpdateFun
函数中,会调用patch
函数:
注意这里传入的第三个参数,实际取的是该节点的父元素: 取到的值是什么呢?是null
,因为文档中已经不存在该节点了。
继续看,到patch
函数内,首先会判断新旧元素是否相同,是则直接 return。这里显然不满足,于是会继续判断isSameVNodeType
,该方法的作用是判断是否为同一个节点,我们这里也不是同一个,是要从第二个 div 变为第一个 div,所以isSameVNodeType(n1, n2)
为 false,这个if
会进去,然后会把旧节点n1
设为 null
. 继续往下,有个 switch
,在这里面会进入default
里的第一个if
判断中,执行processElement
方法,这里传入的container
就是外面传进来的父元素null
:
这个函数就很简单了,判断旧节点是否为null
,注意到刚才已经被修改为null
了,因此会进入到if
内:
然后到mountElement
方法中,会执行hostInsert
方法:
这个方法的定义处,与之前看到的patch
第三个参数的取值,是同一个文件:
问题就在这里了,一路传下来的container
为null
,到insert
方法中,传入了第二个参数parent
,于是浏览器报错:
arduino
Uncaught (in promise) TypeError: Cannot read properties of null (reading 'insertBefore')
这个错误是 Promise 抛出的,一开始我还查不到日志,因为没有写全局捕获方法。不过即使捕获,也只是提供一下日志罢了,反正 vue 的代码不能继续执行的,所以页面更新也还是异常的。
------为什么是 Promise 抛出的?
因为vue3的依赖更新,本来就是通过 Promise 实现的。就像那道经典题------"nextTick"的原理......
算了,不扯了吧,我也不懂了......