我对虚拟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"
const btn1 = () => { VNodeOne.service() }
const btn2 = () => { VNodeOne.service() } </script>
|
VNodeOne下的index.vue代码:
1 2 3 4 5
| <template> <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(){ const vm = createVNode(VNodeOne); render(vm,document.body); } }
|
VNodeTwo下的index.vue代码:
1 2 3 4
| <template> <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(){ const vm = createVNode(VNodeTwo); 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> <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> 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(){ const container = document.createElement('div') const vm = createVNode(VNodeTwo); 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>
<div>我是VNode1</div> <div> <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。