注意:阅读本文可能需要一点点JavaScript以及Vue的使用基础
或许你早已听过Vue的大名,Vue的响应式的绑定、抽象语法树、虚拟DOM、diff算法等等,但实际上这些东西到底是什么,可能还抱有一点疑惑,此文是我在阅读书籍、学习Vue源码时根据自己的理解对Vue的一个大体脉络的一个概括,以及对以上提及的知识点的一些解释。属一家之言,起一个抛砖引玉的作用,如果有不对的地方,各位可以在评论区留言,我会尽快订正
(先贴一张Vue生命周期的图片,我们将结合一个Vue实例的生命周期来理解Vue都干了些什么)
接下来Vue的旅程正式开始:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue 测试实例</title>
<!-- 下面这里就是所有的vue源码 -->
<script src="https://unpkg.com/vue@2.6.14/dist/vue.min.js"></script>
</head>
<body>
<div id="app">
<p>{{ message }}</p>
</div>
<script>
new Vue({
el: '#app',
data: {
message: 'Hello Vue.js!'
}
})
</script>
</body>
</html>
这是应该是最简单的Vue的使用了,在以上这些代码在点击运行后,Vue这台庞大的机器便会开始“隆隆”的运转了。
一、创建阶段(beforeCreate、created):
created前
我们发现,在上面的html代码中,我们使用了"{{}}"双大括号语法模板语法,众所周知,这在html中可是没有这样的语法的。再看看new Vue时传入的el: '#app'
参数。是的,我们将整个id为app的div交给了的Vue来管理,我们就是使用Vue对这个id为app的div(专业点就是DOM)操作来操作去的,故vue的文档里也有说:Vue 的核心库只关注视图层 。
所以,vue在创建阶段(created前)就是将传入的div、data以及methods等做初始化。
也就是说,做点准备工作
二、挂载阶段(beforeMount、mounted):
created到beforeMount阶段
在created到beforeMount这个阶段发生了一件很不得了的事情——Vue将传入的这个id为app的div转化成了一个貌似高大上的东西,叫抽象语法树(AST)。可又为什么要转化成抽象语法树呢?
(哈哈,这个问题稍加思索一下便可得到答案,读者可稍微暂停一下,再往下继续看文章)
因为这样Vue才能理解我们在html写的双大括号语法模板语法,以及在标签里添加的v-bind,v-for,v-if,甚至是自定义标签等等乱七八糟的东西,将我们上面写的这个id叫app的div——
<div id="app">
<p>{{ message }}</p>
</div>
转化成AST之后就会像这个样子——
{
tag: "div",
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: undefined,
attrsList: [],
attrsMap: {},
children: [
{
tag: "p",
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: {
tag: "div",
type: 1,
staticRoot: false,
static: false,
plain: true,
parent: undefined,
attrsList: [],
attrsMap: {},
},
attrsList: [],
attrsMap: {},
children: [
{
type: 2,
text: {{message}},
static: false,
expression: "_s(message)"
}
]
}
]
}
我们可以看到,这个所谓的抽象语法树(AST)也就是一个js对象而已,在这个js对象里包含了所有需要用到的各种信息。
Vue将这么一转化,js操作这些变量不是轻轻松松,就可以将双大括号里的变量换成data里的数据啦。
beforeMount到mounted阶段
可浏览器根本读不懂这个AST啊,所以Vue在beforeMount到mounted这个阶段就是将AST再转化为html代码,也就是说,到mounted完成后
模板语法
<div id="app">
<p>{{ message }}</p>
</div>
已经变成了
<div id="app">
<p>Hello Vue.js!</p>
</div>
三、挂载完毕(beforeUpdate、updated):
到这,久闻大名的虚拟DOM终于出场了。
我们可以先看看虚拟DOM大概长什么样子——
{
tag:'div', // 元素标签
attrs:{ // 属性
id:'app'
}
children:[
{tag:'p', ...}
] // 子元素
}
(可以看到,也是一个js的对象,通过键和值描述节点,但可千万不能认为这和AST没两样,这是两个不同阶段的产物)
然后再想想我们为什么需要虚拟DOM——现代网页程序在运行时,状态会不停的发生变化,有可能是用户点击了按钮,有可能是发送了一次axios请求,这些行为都是异步的。每当状态发生某些改变时,就会需要重新渲染
难道每次发生变化了都把整个DOM删掉然后重新渲染一份出来吗?
我们看看真实DOM的体量——
一个div元素就这么多东西,这样做的代价无疑非常庞大的
故我们希望检测出来哪些DOM发生了变化,然后更新变化的DOM便能极大的优化性能,
在各大框架中都有自己的一套解决方案,在Angular.js中使用的是脏检查手法,这里不讨论。而react.js与Vue.js的手段便是虚拟DOM
下图就是虚拟DOM的具体流程
看了此图,也就能理解Vue生命周期里beforeUpdate与updated是什么时候触发的了
总之,虚拟DOM就起这么一个对比的作用,找出不同,然后有选择的替换渲染,达到优化性能的目的
四、销毁阶段(beforeDestroy、destroyed):
就是Vue实例不再被使用的销毁阶段,具体细节在一开始图片里已描述的非常详细,这里不再赘述
再讲讲Vue的响应式数据绑定
什么是响应式数据绑定?
响应式数据绑定就是在修改data里的数据后页面视图也随之改变,这可是在没有刷新页面的情况下做到的喔。
Vue是怎么做到的呢?
想要做到数据变化后修改视图,首先要做到的就是监听到哪变化了,
1.侦测变化:
深入学习过JavaScript的同学会知道:有两种方法可以侦测到js对象的变化,Object.defineProperty 和ES6的Proxy(如果有不清楚的同学,具体的使用方法这里就不讨论了,大概就是在Object.defineProperty 中当传入的对象被读取时会触发get回调,当传入的对象被修改时会触发set回调,ES6的Proxy就是js提供的一种源编程能力,可以直接检测到对象的变化)
2.通知变化
通过一系列的代码监听到都有哪些数据发生变化之后的下一步就是通知使用此数据的地方,希望使用此数据的地方使用新的数据,直接通知到DOM吗? Vue1.x的版本确实是这样做的,可这么精确是有代价的,依赖追踪在内存的开销会很大,因此Vue搞了一个叫Watcher的东西,当数据变化时就先通知Watcher,Watcher再通知使用此数据的地方,每一个Vue组件也就是Vue实例都会有一个Watcher,统一管理达到了优化的效果
PS:其实也挺简单的对吧,就是监听数据变化,然后通知到使用此数据的地方
但还有一个值得注意的地方,如果数组数据变化了的话,Object.defineProperty 和ES6的Proxy都是没有办法监听到的,所以数组的变化是通过在Array原型里构造一个拦截器,每当使用了6种数组方法的其中一个就能被拦截器监听到,实现监听数据改变的方法。
总结
在上述的每一步,Vue都做了非常多的工作,从将html代码转化成AST,到虚拟DOM的渲染,再到diff算法,又或是各种API的实现,源码都有很多有意思的地方,使用了函数柯里化优化速度,在抽象语法树里使用优化器,避免更改没有变化的DOM元素,还有更多的v-if, v-for的原理,过滤器等。多少了解一些可以很大程度的帮助你写出更优秀的Vue代码