研究 runtime
一边 Vue
一边源码html
初看 Vue 是 Vue
源码是源码vue
再看 Vue 不是 Vue
源码不是源码node
再再看
Vue 是调用栈
源码也是调用栈git
—— By DOM哥github
Vue 运行时这一块是很是有意思的,不像 Vue 编译器那么枯燥,这里面有大量的实用技巧和设计思想能够学习。使用过 Vue 的小伙伴应该对 Vue 【响应的数据绑定】(也叫双向绑定)的印象很是深入,在修改了数据以后,视图就会实时获得相应更新,这无疑极大地减轻了开发者的负担,使得开发人员能够专一于处理业务逻辑和操做数据,也就是闻名遐迩的【数据驱动开发】。至于操做 DOM 更新视图这件苦脏累的活,Vue 已经帮你妥善处理完毕而且对你彻底透明(意思是它就像空气同样你彻底注意不到它,却又深度依赖它,离不开它)。算法
Vue 运行时模块主要是围绕 Vue 实例的生命周期展开的,它涵盖了 Vue 实例生命周期内所须要的所有设施,包括实例建立,响应的数据绑定,挂载到 DOM 节点以及数据变化时自动更新视图等关键部分。本篇也将沿着 Vue 实例的生命周期路线,结合运行时关键实现伪代码,一步步清晰地描绘出 Vue 运行时的空中鸟瞰图。浏览器
本段的部份内容参考自 Vue 官网的生命周期描述。框架
就像每一个人的生命周期有 幼年、童年、少年、青年、中年、老年,每一个 Vue 实例的生命周期也有 beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、activated、deactivated、beforeDestroy、destroyed 等多个阶段。dom
Vue 实例生命周期代码示例:ide
<div id='index'>{{msg}}</div>
复制代码
new Vue({
el: '#index',
data: {
msg: 'lifecycle',
},
beforeCreate(){ console.log('beforeCreate')},
created(){ console.log('created')},
beforeMount(){ console.log('beforeMount')},
mounted(){ console.log('mounted')},
})
// Console output:
// beforeCreate
// created
// beforeMount
// mounted
复制代码
每一个 Vue 实例在被建立时都要通过一系列的初始化过程,例如设置数据监听,编译 HTML 模板,将实例挂载到 DOM 等。在这个初始化的过程当中会在特定的地方运行一些叫作【生命周期钩子】的函数,这些钩子其实就是开发者能够自定义的回调函数,如上面传入的 created
函数就会在 Vue 实例 created 时被调用。
下面一张图能够很是清晰地说明 Vue 各个生命周期钩子的调用时机(图片来自 Vue 官网生命周期图示):
Vue 的生命周期图示
你不须要立马弄明白图上全部的东西,不过随着你的不断学习和使用,它的参考价值会愈来愈高。
众所周知 Vue 是经过 new Vue()
的方式进行使用的,也就是说 Vue 内部将本身封装成了一个类。然而 Vue 并无使用 ES6 最新的 class
方式进行实现,而是用了原来 prototype 那一套,这是让宝宝有些伤心的。闲话待会再叙,先看一下源码:
// vue/src/core/instance/index.js
function Vue (options) {
this._init(options)
}
复制代码
Vue 将初始化工做所有放在了 Vue.prototype._init()
方法里。去伪存真,_init
方法主代码以下:
// vue/src/core/instance/init.js
Vue.prototype._init = function (options) {
const vm = this
vm.$options = mergeOptions(options || {})
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
复制代码
initEvents
和 initRender
函数主要用来初始化 Vue 实例的一些容器字段,如今可暂时忽略它们。接下来重点来了,在 initState
函数中封装了实现【响应的数据绑定】的关键代码,虽然这不是 Vue 最流弊的部分,但倒是咱对 Vue 最好奇的地方,也是咱开始本源码系列的最初动力。在 initState
以前和以后分别调用了 Vue 的生命周期钩子函数 beforeCreate
和 created
,接下来看看 Vue 是如何实现响应的数据绑定的。
响应的数据绑定并非 Vue 首创的,而是 MVVVM 模式理论的一部分,它是 View 层和 ViewModel 层的链接方式。以下图所示:
MVVM 分层示意图
Vue 经过【观察者模式】实现了一套响应式系统。观察者模式(也叫发布/订阅模式)会将观察者和被观察的对象严格分离开,当被观察对象的状态发生变化时,全部依赖于它的观察者都将获得通知并自动刷新。举个栗子,用户界面能够做为一个观察者,业务数据是被观察者,用户界面观察业务数据的变化,当数据发生变化时,用户界面就会自动更新。
该模式必须包含两个角色:观察者和被观察对象。Vue 定义了一个 Watcher
类来建立观察者,定义了一个 Dep
类来建立被观察对象。 Dep 是 Dependent 的缩写,意思是做为观察者的依赖存在,也就是被观察对象。
首先看一下【观察者】 Watcher
的定义:
// vue/src/core/observer/watcher.js
import Dep from './dep'
export default class Watcher {
constructor(vm) {
this.vm = vm
this.newDeps = []
Dep.target = this
}
// 添加一个观察者,或者说注册一个依赖
addDep(dep) {
this.newDeps.push(dep)
// 在【观察者】收集【被观察者】的同时,【被观察者】也会收集【观察者】
// 这比如王八看绿豆对眼儿了,遂互存了电话号码,就有了后来的相识相知
dep.addSub(this)
}
// 在被观察对象状态发生变化时调用此方法
update() {
let {vm} = this
// 更新视图
vm._update(vm._render())
}
}
复制代码
每个【观察者】都会收集本身要观察的数据对象(Dep),当【被观察对象】发生变化时,【被观察对象】会通知【观察者】,【观察者】收到通知后执行 update
方法更新视图。
接下来看一下【被观察者】 Dep
:
export default class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知全部对本身有依赖的观察者
notify () {
const subs = this.subs
for (let i = 0; i < subs.length; i++) {
subs[i].update()
}
}
}
Dep.target = null
复制代码
每一个【被观察对象】一样会收集依赖本身的【观察者】,当本身发生变化时,就会通知(notify
)这些观察者 update
。
那么问题来了,这两个角色是如何收集对方的呢?又如何得知【被观察者】发生变化了呢? 这就用到了并不经常使用的 Object.defineProperty() 方法,经过在 JavaScript 对象每一个属性描述符的 setter
和 getter
里作文章,就能实时捕捉 JavaScript 对象的变化。
须要注意的是,Object.defineProperty()
是 JS 语言自己的一个 API 而不是 Vue 实现的,Object.defineProperty 是 ES5 中一个没法 shim 的特性,这也是为何 Vue 不支持 IE8 以及更低版本浏览器的缘由。若是想支持 IE8 以及更低版本浏览器怎么办呢?那就只有放弃 Vue,选择 Knockout。更好的解决方案就是直接让 IE8 以及更 low 的家伙见鬼去吧。不过基本上不用担忧这个问题了,由于据最新浏览器使用调查报告,IE8 以及更低版本浏览器的市场份额已经微不足道,直接忽略不计就好了。
既然 JS 已经支持在对象属性变化时添加自定义处理,Vue 须要作的事就是遍历传入的 data
选项,为 data
的每一个属性设置 setter
和 getter
。这就解决了如何得知【被观察者】发生了变化这个问题。
接下来讲说这二者是如何收集对方的。【观察者】和【被观察者】就比如单身男和单身女,得有人安排相亲才能创建起联系呵,Vue 就是这个牵线搭桥的媒婆。下面是相亲源码:
// vue/src/core/observer/index.js
import Dep from './dep'
export function observe (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
let key = keys[i], value = obj[key];
// 深度优先遍历
observe(value)
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 【观察者】收集【被观察者】
// 同时【被观察者】也会收集【观察者】
if (Dep.target) {
Dep.target.addDep(dep)
}
return value
},
set(newVal) {
value = newVal
// 【被观察者】通知【观察者】
dep.notify()
}
})
}
}
复制代码
能够看到,Vue 在遍历 data
对象时完成了【观察者】和【被观察对象】彼此之间的收集工做。而且在 data
的某字段发生变化时,相应的依赖就会通知【观察者】本身发生了变化,【观察者】就能够作出反应。
Vue 接下来就会在 initState()
中调用 observe(vm.$options.data)
,执行以后实例化 Vue 时传入的 data
对象就会成为响应式的,当你修改 data
对象的数据时(一般是根据用户操做执行对应的业务逻辑),【被观察者】就会通知已收集的全部【观察者】,观察者就会调用本身的 update
方法,从而更新视图。这基本上就是 Vue 所实现的响应的数据绑定的工做原理。
在构建完响应式系统以后,Vue 接下来会检查用户是否传入了 el
选项,由于 Vue 在将包含指令的 HTML 模板编译成最终的朴素的 HTML 以后会执行 DOM 替换操做,最终展现在页面上,若是没有 el
选项,Vue 就不知道要把产出的 HTML 放到哪里去展现。
挂载到 DOM 节点并不是替换一下 DOM 那么简单,它包括将模板编译成 render
函数,执行 render
函数生成虚拟DOM,计算出新旧虚拟DOM之间的最小变动,打补丁式地更新页面视图等几大步。
这个编译过程在前几篇的 Vue 编译器模块里已经讲得很清楚了,主要分为根据模板生成 AST,对 AST 进行优化,根据 AST 生成 render 函数这三步,这里再也不赘述,感兴趣的可前往查看。
【虚拟DOM】并不是 Vue 提出的概念,而是老早就被发掘出来的新型DOM操做方式,MVVM 框架在引入虚拟DOM以后如虎添翼。之因此叫作虚拟DOM,是相对于真实DOM而言的。直接操做DOM很慢,由于真实的DOM对象很重,操做真实DOM对象(HTMLElement)花销很大,并且操做完以后每每会引发浏览器对页面的重绘和重排。若是频繁的进行DOM操做,页面性能会急剧降低。因而聪明的 Jser 决定使用简单的 JS 对象格式来表示真实 DOM,也就是虚拟DOM。先执行对虚拟DOM的操做(这会执行的很快,由于是纯 JS 操做),最后对比操做先后的新旧虚拟DOM树,找出最小变动,一次性地应用到真实DOM上。虽然仍是要对真实DOM操做,但次数却大大减小,从而在更新视图的同时可有效保证页面性能。
Vue 的虚拟DOM系统是在开源虚拟DOM库 Snabbdom 的基础上作了适当的改进。
下面是 Vue 的 VNode 定义(正是一个个这样的 VNode 组成了一棵虚拟DOM树):
// vue/src/core/vdom/vnode.js
export default class VNode {
constructor (tag, data, children, text, elm) {
this.tag = tag
this.data = data
this.children = children
this.text = text
this.elm = elm // 此字段存放真实DOM
}
}
复制代码
在上一步执行 render
函数生成虚拟DOM后,接下来就须要对比新旧虚拟DOM之间的差别,从而得到DOM的最小变动。比较两棵DOM树的差别是虚拟DOM库最核心的部分,这也是所谓的 Virtual DOM 的 diff 算法。就像版本控制系统 Git 的 diff 能够计算出两次提交之间的变动,虚拟DOM的 diff 也能够计算出新旧虚拟DOM之间的差别。计算出来的差别称为一个 patch,也就是补丁。
若是是首次渲染,也就是页面刚加载进来第一次渲染,Vue 会用模板编译后的DOM替换掉传入的 el
元素。请注意这一点,对模板内DOM的操做(绑定事件,引用DOM等)应该始终放在 Vue 的 mounted
以后,不然全部处理都将丢失,由于模板会被替换掉。
若是是后续数据发生变化,Vue 就会用打补丁的方式更新视图,尽量重用现有DOM,将真实的DOM操做减到最少。
在上面【观察者】 Watcher
的定义中 update
方法里执行视图更新。所以 Vue 运行时的整个工做流程基本上是这样的:
用户调用 new Vue(options)
实例化 Vue,Vue 在 _init
方法中初始化相关字段和事件,最重要的,创建起响应式系统,Vue 实例的后续运行重度依赖于此响应式系统。Vue 会新建一个【观察者】,该观察者在建立时会执行 update
方法首次渲染视图,包含 Vue 指令的模板会被替换成编译后的朴素 HTML。Vue 会遍历传入的 data
选项,经过 Object.defineProperty
设置 setter
和 getter
将其变成【被观察对象】。当 data
的数据发生变化时,被观察对象就会通知观察者,观察者就会再次调用 update
方法打补丁式地更新视图。
本篇完,将在下一篇中开始深究运行时实现细节。
本系列会以每周一篇的速度持续更新,喜欢的小伙伴记得点关注哦