Vue原理解析——从Vue对象到DOM
我们都知道Vue的基本使用是从new一个Vue对象开始,挂载到真实DOM元素并实现数据的响应式绑定。那么在这个过程中究竟发生了什么呢?
Vue对象的初始化
Vue的构造类如下:
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)
}
可以看到除了环境检查以外,构造函数只调用了初始化函数。再进入到这个函数中看
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-init:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
/*一个防止vm实例自身被观察的标志位*/
vm._isVue = true
// 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
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
/*初始化生命周期*/
initLifecycle(vm)
/*初始化事件*/
initEvents(vm)
/*初始化render*/
initRender(vm)
/*调用beforeCreate钩子函数并且触发beforeCreate钩子事件*/
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
/*初始化props、methods、data、computed与watch*/
initState(vm)
initProvide(vm) // resolve provide after data/props
/*调用created钩子函数并且触发created钩子事件*/
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
/*格式化组件名*/
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
/*挂载组件*/
vm.$mount(vm.$options.el)
}
}
在初始化过程中,会依次进行一些Vue对象特性的注册,例如生命周期,响应式数据等。可以看到在数据初始化完毕后会调用created钩子函数,然后通过$mount方法去将Vue对象挂载到真实DOM元素上。
模板编译
$mount方法就是触发组件的DOM渲染的开端了。根据源码可知渲染模式有3种:自定义render函数,template和el,也就是分别对应我们常用的3种挂载写法了,在这一步的作用是解析HTML模板,转化为可以生成对应DOM的render函数。
需要知道的是虽然写法有3种,但无论使用哪种调用方式最终都要生成一个render函数。区别在于,通过template和el编译的模板是通过AST解析优化得到的,属于声明式渲染,理解容易但是灵活度比较低。而自定义的render函数相当于人为的用逻辑创建模板,灵活性高,但可读性比较差。具体使用哪一种,要根据业务需求决定。
VNode
在模板编译过程中,我们得到了一个可以生成对应DOM结构的render函数,但这个render函数并不是生成真实的DOM并插入到HTML中。要知道每当数据变化时都会重新调用render函数生成最新的DOM结构,而这样的JS操作DOM进行重绘的操作是非常消耗性能的。所以这个render函数生成的是以JS对象为节点的虚拟DOM树,每次修改数据时DOM的变化都在这颗虚拟DOM上执行,这就是VNode了。
Patch
每一次更新VNode后,会通过名为patch的方法对新旧VNode使用diff算法寻找差异,根据不同状态对VNode进行合理的修改,这部分算法由于篇幅原因在今后的文章中再专门总结。
映射
到这一步Vue的核心功能部分已经完成了,可以看到由于使用了VNode,只要在支持JS的环境下都可以使用Vue,那么根据环境的不同,将VNode映射到真实DOM的操作也不一样。Vue根据不同的环境做了一层适配层,例如浏览器环境和移动端的weex环境都会通过这层适配层对VNode提供相同接口去映射真实DOM。
小结
回头梳理一下,整体流程并不复杂,但沿着整个流程对我们学习Vue的相关知识非常有帮助,最后再梳理一下核心部分:
- new Vue初始化Vue核心功能。
- $mount挂载生成render函数。
- 数据变化时render函数生成新的VNode对象。
- 通过diff算法比对VNode并更新。
- 根据适配层将VNode变化映射到真实DOM。
-- EOF --