发布于 

我对虚拟DOM的一些思考

最近编写vue插件时我遇到了一个问题:通过引入函数调用,创建虚拟DOM,并且把它转成真实DOM且挂载到body容器中,但当我需要再次把某个组件通过同样的方式挂载到body容器中,(这里暂把第一次创建的虚拟DOM称作旧VNode,第二次创建的称作新VNode)。

原本我是想两个VNode都存在body中,且新VNode排在旧VNode的后面,但在执行新VNode的挂载时却发现body容器中只剩下新VNode,旧VNode不在body中了。这又是什么情况???

下面我来写一个小案例,复现当时的情况吧!

在本案例中,页面有两个按钮,点击按钮1会调用函数,该函数会创建VNode1,点击按钮2会创建VNode2

App.vue下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div class="container">
<button @click="btn1">点我创建VNode1</button>
<button @click="btn2">点我创建VNode2</button>
</div>
<script setup>
import VNodeOne from "./components/VNodeOne"
import VNodeTwo from "./components/VNodeTwo"
//点击创建VNode1按钮函数
const btn1 = () => {
VNodeOne.service()
}
//点击创建VNode1按钮函数
const btn2 = () => {
VNodeOne.service()
}
</script>

VNodeOne下的index.vue代码:

1
2
3
4
5
<template>
<!-- 这是VNode1 -->
<div >我是VNode1</div>
</template>

VNodeOne下的index.js代码:

1
2
3
4
5
6
7
8
9
10
11
12
import VNodeOne from "./index.vue"
import { createVNode, render } from 'vue'

export default {
service(){
//为组件创建虚拟DOM
const vm = createVNode(VNodeOne);
//把虚拟DOM渲染成真实DOM,并挂载到body容器中
render(vm,document.body);

}
}

VNodeTwo下的index.vue代码:

1
2
3
4
<template>
<!-- 这是VNode2 -->
<div>我是VNode2</div>
</template>

VNodeTwo下的index.js代码:

1
2
3
4
5
6
7
8
9
10
11
import VNodeTwo from "./index.vue"
import { createVNode, render } from 'vue'

export default {
service(){
//为组件创建虚拟DOM
const vm = createVNode(VNodeTwo);
//把虚拟DOM渲染成真实DOM,并挂载到body容器中
render(vm,document.body);
}
}

下面我点击按钮1,body下的结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
<body>
<div id="app" data-v-app="">
<div class="container">
<button>点我创建VNode1</button>
<button>点我创建VNode2</button>
</div>
</div>
<script type="module" src="/src/main.js?t=1679131845507"></script>

<!-- 这是VNode1 -->
<div>我是VNode1</div>
</body>

事情按照我计划那样,VNdoe1被渲染到body容器中,那现在点击按钮2,看看会发生什么?

1
2
3
4
5
6
7
8
9
10
11
12
<body>
<div id="app" data-v-app="">
<div class="container">
<button>点我创建VNode1</button>
<button>点我创建VNode2</button>
</div>
</div>
<script type="module" src="/src/main.js?t=1679132407262"></script>

<!-- 这是VNode2 -->xian
<div>我是VNode2</div>
</body>

新生成的DOM结构中,我们可以看到body结构中的VNode1不存在了,只剩下VNode2,也可以这样说,在body容器中,新的VNode取代了旧VNode。但是这不符合预期,我们想要的是把VNode2加在VNode1后面,这又该怎么办?

办法总比困难多,只需将VNodeTwo.js代码修改一下即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
import VNodeTwo from "./index.vue"
import { createVNode, render } from 'vue'

export default {
service(){
//先创建一个div,真实DOM
const container = document.createElement('div')
const vm = createVNode(VNodeTwo);
//将虚拟DOM渲染并挂载到这个div中,然后再把它挂载到body下
render(vm,container);
document.body.appendChild(container)
}
}

现在我们依次点击btn1,btn2,再看看body结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<body>
<div id="app" data-v-app="">
<div class="container">
<button>点我创建VNode1</button>
<button>点我创建VNode2</button>
</div>
</div>
<script type="module" src="/src/main.js?t=1679133237946"></script>


<!-- 这是btn1 -->
<div>我是VNode1</div>
<div>
<!-- 这是btn222 -->
<div>我是VNode2</div>
</div>
</body>

这时VNode2就排在VNode1后面了(虽然多了一层div包裹),总算把这个问题解决了。

重点来了

那为什么VNode直接渲染到body中,再次触发时新的VNode会替代旧的VNode,

而把VNode渲染到div(已存在)中,再把div挂载到body时, 再次触发时旧的VNode没被新的div替换,而是新div加在旧的VNode后面呢?

这个问题,怎么回答?

按照我的理解,我是这么认为的:

​ 点击btn1时,即第一次触发render(vm,document.body),将VNode渲染并挂载到body容器中,再次触发的时候,如果该VNode是同一个VNode且内容不变的情况下,vue不执行任何操作,也就是说DOM结构不变,如果是执行render(newVm,document.body),那么newVm会替代旧vm渲染到body中,这样当我们点击btn2时,发生VNode2取代VNode1就可以解释为:VNode1为旧VNode,VNode2为新VNode,它们两个内容不一样,所以发生取代现象(这种说法不严谨,仅用于本文理解)。

​ 在把VNodeTwo.js代码修改后,把VNode2渲染到新创建的div(已存在)中,再把div挂载到body时, 再次触发时旧的VNode1没被新的div替换,而是新div加在旧的VNode后面,是因为vue会对这些由虚拟dom转成真实dom的数据进行管理,同一个容器下的虚拟dom发生变化时,会将新的虚拟dom替换旧的(当然这里还涉及了diff算法对比) 而在body下添加真实的dom(div),它就不受vue的管理,但vue可以管理这个容器下的虚拟dom。