分享错误想法是因为,我最近在思考一些不甚重要的问题。代码写多了,总想优化一下。遇到一些有趣好用的写法,自己常会用到工作中。没有考虑过原理,迷迷糊糊地用,出现了一些错误的认识。这里记录下,也用作自省。
之前看到一个有意思的写法,有关在 Vue 中获取 DOM 元素宽度的。
场景是这样的,页面有两个部分,头部和底部。头部栏又分为两部分,左侧名字,右侧内容提示区。需要适配不同屏幕宽度,期望是标题区域能展示全,就全展示出来,内容区域自动撑开。如果屏幕太小,内容区域有一个最小限制,优先保证内容区域展示 ,标题栏对应缩减。页面底部区域对齐头部宽度,完整两部分样式上的一致性。
举个例子,假设标题有两种可能,沙丘和你想活出怎样的人生。沙丘短很多,缩短标题宽度,能给内容增加很大一部分区域,在小屏幕下很可观。
提前写好百分比,很难做出这样的效果。我想到的办法就是,DOM 挂载后获取标题区初始宽度,再根据情况计算重新设置宽度。问题是,怎么获取标题区宽度。可以在 onMounted 中获取,这会导致 onMounted 中功能变复杂。后来我发现了一种有趣的写法,场景其实不重要啊,重点是这种写法。我举个例子:
html
<template>
<div class="wrapper" ref="wrapperRef">
<div ref="testRef">{{ textContent }}</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";
const textContent =
"隔一程山水,你是我不能回去的原乡,与我坐忘于光阴的两岸。彼处桃花盛开,绚烂漫天凄艳的红霞,你笑的清浅从容,而我却仍在这里守望。落英如雨,印证我佛拈花一笑的了然。爱,如此繁华,如此寂寥。";
const testRef = ref();
watch(testRef, (el: HTMLElement) => {
// 这里可以获取 DOM 信息
console.log(el.offsetWidth, "<---- in watch");
});
</script>
也就是说,可以通过 watch 侦听到 ref 绑定值的变化。testRef 默认是 undefined,绑定 DOM 元素后,会触发 watcher。随后可以获取 Element 属性,做一些初始化处理。这部分代码不必放在 onMounted 中,我觉得会使功能内聚更好一点。
对于这种用法,我慢慢产生了一些疑问。比如说我们创建了一个元素,写入 textContent,在没有 append 进 DOM 前,元素宽度是多少。还有就是 ref 是什么时候绑定的。一开始,我有一些想当然的看法,很棒,全错了。
我最开始的想法是这样的,元素在插入 DOM 结构前,是内容撑开宽度,内容多长,宽度多长。这个想法错的离谱,浏览器比我要聪明。都没有添加进 DOM,干嘛要费力计算样式呢。第二个错误就是,我认为 HTMLElement 创建后就会绑定 ref 了,也就是在 mount 进 DOM 前。还有一个错误,我其实能想明白,当时陷入思维误区了。总所周知,子组件先触发 onMounted,父组件再触发。逻辑上也很好理解,父组件包含子组件,理应子组件先完成创建并添加进父组件,父组件才可能构建完成。但是具体到真实的创建 DOM 操作上,Vue 用的是 Node.insertBefore()
,外层 Element 没有创建,内层元素没有创建的必要。可是当时,我从生命周期角度,想当然认为 HTMLElement 也是从内往外构建的。
OK,目前我有一些疑问,也写了一些代码去看这个流程:
html
<template>
<div class="wrapper" ref="wrapperRef">
<div ref="testRef">{{ textContent }}</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from "vue";
const textContent =
"隔一程山水,你是我不能回去的原乡,与我坐忘于光阴的两岸。彼处桃花盛开,绚烂漫天凄艳的红霞,你笑的清浅从容,而我却仍在这里守望。落英如雨,印证我佛拈花一笑的了然。爱,如此繁华,如此寂寥。";
const wrapperRef = ref();
const testRef = ref();
watch(testRef, (el: HTMLElement) => {
console.log(el.offsetWidth, "<---- in watch");
});
onMounted(() => {
const el = document.createElement("div");
el.textContent = textContent;
console.log(el.offsetWidth, "<----- create element");
console.log(wrapperRef.value.offsetWidth, "<-----mounted wrapper width");
wrapperRef.value.appendChild(el);
console.log(el.offsetWidth, "<----- append create element");
});
</script>
打印结果如下:
bash
0 '<----- create element'
index.vue:26 400 '<-----mounted wrapper width'
index.vue:30 400 '<----- append create element'
index.vue:16 400 '<---- in watch'
可以看到创建元素后,元素并没有默认宽度,添加进 DOM 后才有宽度。绑定元素触发的 Watcher 更新,实际上是在 onMounted 之后。其实还有一个隐藏的问题,既然 mounted 之后,添加进 DOM 中的元素有宽度,意味着此时组件的 wrapper 已经添加进真实 DOM 了。也就是同步真实 DOM 这个过程是由外至内的,但是这个不影响子组件先完成 mounted 的逻辑,毕竟子组件是父组件的一部分。虽然先生成父组件 DOM 外层的结构并添加进真实 DOM 中,但是子组件元素全部添加完成后,父组件可能还有元素没有完成添加进 DOM 的工作。
我知道上面说的很绕,总之看表现来推断,添加 HTMLElement 的操作,是从整个 DOM 结构由外到内,从上到下添加的。组件生命周期,是子组件先完成 mounted,父组件再完成。
具体的流程,是让我愈发地感兴趣了,接下来就是狠狠的啃源码。