问题描述
当你用vue-cli创建一个工程后,会看到index-html文件里有一个div,id叫app
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app">app</div>
<!-- built files will be auto injected -->
</body>
</html>
此时App.vue文件中也有一个div,id也叫app
<template>
<div id="app">
hello world!
</div>
</template>
但是根据规定,id应该具有唯一性,那么问题来了,我们在main.js里面挂载的那个#app是哪个?最终渲染到页面上的那个#app又是哪一个呢?
new Vue({
router,
render: (h: any) => h(App),
}).$mount('#app');
想要知道这些问题的答案,我们可以做一个简单的实验,比如把这两个文件中的某一个id改掉,看一下页面中最终渲染的是哪个,看一下哪个文件中的id改变会导致执行$mount('#app')报错,可以很轻松的得出结论。
但是本着知其然,知其所以然的态度,我们打开源码验证一下猜想
验证猜想
为了方便查找到我们要看的源码,所以选择在浏览器打断点调试。那么断点打在哪里比较合适呢,有两种思路
- 将断点打在app.js的new Vue()这行代码,顺着vue初始化的流程去追,追到页面dom节点渲染出来时,就可以往回找,通过打新的断点不断往下追,但是因为vue项目非常庞大,这种方法追踪起来效率很低,很容易迷失。(别问我怎么知道的)
- 第二种方法需要对vue源码项目的目录结构有一定了解,因为这种大型项目的文件分组都是有一定规律的。vue将所有dom更新的方法都放在了patch中,所以我们打开patch.js。这里需要做一个设想,声明了两个#app,最后渲染出来只有一个,那么肯定移除了另一个。所以我们要在这个文件中找一下有没有关于移除dom的方法,果然找到了removeNode,于是我们将断点打在这个方法上,刷新页面,果然进入了这个断点,这也验证了我刚才的猜想
function removeNode (el) {
const parent = nodeOps.parentNode(el)
// element may have already been removed due to v-html / v-text
if (isDef(parent)) {
nodeOps.removeChild(parent, el)
}
}
我们将鼠标悬浮在入参el上,可以看到,要被移除的元素是index.html里的#app,而且此时App组件中的内容已经渲染到了页面上,所以另一个#app很大概率是被append到body里面的,如果想分析另一个元素挂载过程,同样可以在patch中找到相应的方法打上断点直接调试

通过调用堆栈,我们追到了patch方法,这个方法就是大名鼎鼎的diff算法,可以看到,在调用patch的地方,index中的#app原生dom节点作为oldNode传入,vnode则使用已经处理过的App中的#app虚拟dom




结论
讲到这里,结论基本上已经出来了,那就是在index.html和App.vue中存在两个#app,经过$mount挂载后,最终渲染在body中的是App.vue中的那个#app,index.html中的#app则会被移除。
引申
不知道大家有没有想过作者为什么要这么设计呢?乍一看有点多此一举的感觉。我说一下我的想法。
- 我们现在的spa项目越做越大,即使做了懒加载,但是首屏以然需要加载很多基础环境地方js包,所以避免不了需要等待,那么这个时候,如果我们在index.html的#app里面写一个loading动画,那么在实际的App.vue内容没有被渲染出来,我们就能在页面上看到一个loading状态,能够显著的提升用户体验。当App.vue中的内容渲染完后,之前的节点就会被移除,loading动画自然也就消失了。
- 第二点其实还是关于页面等待体验优化的地方,那就是骨架屏。现在的很多h5页面都采用了骨架屏,不管方案如何,最终骨架屏在正式的内容加载出来后都应该自己消失的,那么写在这里也很合适、
- 至于其他的点,我暂时还没想出来,欢迎大家评论补充
另外,有了完整的调用堆栈信息,想要深挖$mount过程就很容易了