前些时间大概看了一下Vue的官方文档,也大概了解过 Vue 实现数据绑定的原理,但是还是想再深入得了解其具体的代码细节,便抽了些时间来阅读源码。看了个大概,另外自己水平有限,也只能把自己看懂的分享给大家。
Vue 的源码用了flow进行类型检测等工作,真正生产环境的代码是 flow编译而成的,所以在IDE中阅读代码时会出现很多代码错误的提示,如果只需要阅读而不对源码进行修改和单元测试,则不必过多考虑。需要的话可以去阅读一下flow这个工具的用法, 这里是官网: https://flow.org/
另外,源码中的语法大都是 ES6 的标准,还运用了很多Object.defineProperty来配置对象成员的 get 和 set 钩子。
构造过程 new Vue({...})
Vue的构造器在 src/core/instance/index.js 中,但是在加载Vue这个API的时会先对它进行初始化,代码位置在 src/core/index.js 中
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
Vue.version = '__VERSION__'
export default Vue
可以看到这里在 Vue 构造器 被导出之前运行了 initGlobalAPI() 来扩展Vue 构造器本身的属性及方法(javascript 作为一个基于原型的 OO 语言, 其中一个函数也是一个对象,因此是可以有自己的方法和属性的)。
initGlobalAPI() 在 src/core/global-api/index.js 中
再看 Vue 构造器
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
在构造器被运行之前,依然运行了一些函数来初始化Vue 原型的方法,这里很重要,但是在第一篇文章里就不详述了。
在构造 vue 对象时,运行了 _init(options) 函数来初始化,在src/core/instance/init.js 中
function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
......
// merge options
// 在之前对构造器本身及其原型进行了扩展,这里进行合并处理
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
......
vm._self = vm
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件及时间处理
initRender(vm) // 初始化render
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化 data, 的监视等
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
......
if (vm.$options.el) {
vm.$mount(vm.$options.el) // 挂载
}
}
}
在挂载之前先要对生命周期,模板渲染等进行初始化,比较重要的是建立对数据的监视来实现动态绑定以在数据更改时重新渲染VDOM 并更新DOM。
挂载时要进行模板的编译和DOM的生成,这一步之后,就顺利构造完成了。
VDOM
Vue 同 react 一样, 运用了自己的虚拟DOM来实现DOM的更新,大体可以理解为在数据发生变化时先建立一个虚拟的DOM,并且与之前的虚拟DOM进行比较,最后经过diff 之后找到不同并通过 patch 来更行真正的DOM。其优点在于避免了大量的DOM操作(因为DOM操作很费资源)。
这里不进行详细分析,但是推荐给大家一篇文章,里面介绍了诸多框架中数据监测的实现
http://teropa.info/blog/2015/03/02/change-and-its-detection-in-javascript-frameworks.html
模板编译
Vue 的模板编译大体需要经过一下几个步骤
- 由template编译成 ast render 函数(AST是指抽象语法树,具体就不说了,因为我也不会)
- 运行render 函数,编译成 vnode(虚拟DOM节点)
- 通过patch更行DOM
而具体的模板编译和patch都是相对比较复杂的算法,这里就先不细说(以后可能也细说不了,得看我个人造化了)
数据监视(数据绑定)
Vue 的数据绑定主要是通过get 和 set 钩子来实现的,而钩子函数究竟干了什么呢,其实在初始化的过程中建立了一系列的 Observer 对象来监视数据的变化,在 Observer 中又有 Watcher 对象,负责监视到数据变化之后的操作(不知道说的对不对,个人理解吧)。Observer 的建立过程中就设置了诸多get set 钩子。Watcher则是在挂载时建立(也有可能在其他地方也建立了但是还没有读到)。
结语
以上就是对Vue 2.5 源码的概述,由于个人水平有限,可能有很多错误,希望大家能提出,共同学习。