一、先看下Hello World的模板和vue代码
<!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">
<title>Hello world</title>
</head>
<body>
<div id="app">
<span>{{descText}}</span>
</div>
</body>
<script src="../../dist/vue.js"></script>
<script src="./app.js"></script>
</html>
new Vue({
el: '#app',
data() {
return {
descText: 'Hello World'
}
},
beforeCreate () {
console.log('beforeCreate----this.descText', this.descText);
},
created() {
console.log('created----this.descText', this.descText);
},
beforeMount () {
console.log('beforeMount----this');
},
mounted() {
console.log('mounted----this');
},
methods: {
changeDesc() {
this.descText = 'Hello LLS'
}
}
})
运行之后结果如下:
那你知道Vue是怎么把Hello World渲染出来的吗? 接下来从Vue的源码的角度来分析一下执行过程。
先看下过程流程图,下图中右侧打了对钩的部分是本篇所涉及的,顺序是从上到下,其实这个图和Vue官网的图有点区别。
二、为何在beforeCreate之前不能获取实例属性的data和methods?
要想回答这个问题,就必须搞清楚在调用生命周期beforeCreate之前Vue到底做了哪些事情?
从上面的图可以看到beforeCreate阶段做了3个事情,初始化实例对象initLifecycle()、初始化父组件事件相关属性initEvents(vm)、初始化渲染相关initRender(vm)。
1、 初始化实例对象initLifecycle(vm)
先看下`initLifecycle`的部分源码
```js
function initLifecycle() {
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
```
可以看到initLifecycle主要是把实例属性初始化,比如把监听器_watcher置空、是否挂载完成_isMounted设置为false等。
2、 initEvents(vm)初始化父组件事件相关属性,当有父组件的方法绑定在子组件时候,供子组件调用。
initEvents的代码如下
```js
function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events 初始化父组件附加的事件
// 在经过合并options 阶段后,
// 子组件就可以从 vm.$options._parentListeners读取到父组件传过来的自定义事件。
// 通过updateListeners方法, 它的作用是借助$on, $emit方法, 完成父子组件事件的通信
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
```
3、 initRender(vm)渲染相关,初始化了一些渲染需要用的属性和方法以及slot相关。
initRender函数的关键代码如下
function initRender (vm: Component) {
vm.$slots = resolveSlots(options._renderChildren, renderContext)
vm.$scopedSlots = emptyObject
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// normalization is always applied for the public version, used in
// user-written render functions.
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
initRender做了以下事情
-
将组件的插槽编译成虚拟节点 DOM 树, 以列表的形式挂载到
vm实例,初始化作用域插槽为空对象; -
将模板的编译函数
_c和$createElement属性(把模板编译成虚拟 DOM 树)挂载到vm; -
最后把父组件传递过来的 listeners生成响应式的。
4、调用生命周期钩子beforeCreate callHook(vm, 'beforeCreate')
先看下callHook代码
function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget()
const handlers = vm.$options[hook] //相当于vm.$options['beforeCreate']
const info = `${hook} hook`
if (handlers) { //handlers是个数组,当使用了minx的时候,就可能会有多个生命周期钩子函数,['全局minxin的生命周期钩子函数', '局部minxin的生命周期钩子函数', '组件的生命周期钩子函数'],并且会按照以上顺序循环执行
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info)
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook)
}
popTarget()
}
invokeWithErrorHandling(handlers[i], vm, null, vm, info)实际上执行的就是handler.apply(context, args) 或者 handler.call(context),外加了异常处理。
function invokeWithErrorHandling (
handler: Function,
context: any,
args: null | any[],
vm: any,
info: string
) {
let res
try {
res = args ? handler.apply(context, args) : handler.call(context)
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
// issue #9511
// avoid catch triggering multiple times when nested calls
res._handled = true
}
} catch (e) {
handleError(e, vm, info)
}
return res
}
现在beforeCreate生命周期已经执行完了,我们看到此时还没对props、data、methods、computed、watch初始化,所以我们是无法获取data属性和调用method方法。
二、在生命周期beforeCreate之后和生命周期created之前做了啥
1、 将inject属性响应式 initInjections(vm)
function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm)
if (result) {
toggleObserving(false)
Object.keys(result).forEach(key => {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, result[key], () => {
warn(
`Avoid mutating an injected value directly since the changes will be ` +
`overwritten whenever the provided component re-renders. ` +
`injection being mutated: "${key}"`,
vm
)
})
} else {
defineReactive(vm, key, result[key])
}
})
toggleObserving(true)
}
}
2、 执行initState(vm)初始化&响应式props、data、methods、computed、watch属性
先看下initState(vm)的源码
function initState (vm: Component) {
vm._watchers = []
;
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) //props相关
if (opts.methods) initMethods(vm, opts.methods) //methods相关
if (opts.data) { //data相关
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed) //computed相关
if (opts.watch && opts.watch !== nativeWatch) { //watch相关
initWatch(vm, opts.watch)
}
}
(1) initProps
- 校验
props属性是否合法, - 通过
defineReactive(props, key, value)设置props响应式 - 在实例对象
vm上设置props的每个key值,方便通过this[key]读取
(2) initMethods
- 校验
props属性是否合法, - 将
method挂载到实例对象vm上,并设置method的环境变量为vm
initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
//省略的代码是校验方法是否是function、方法名是否在vm存在相同的实例属性
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
(3) initData
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
//遍历data,并对data的属性变化添加检测
//Object.defineProperty(target, key, sharedPropertyDefinition) 将data的属性添加到vm上,并添加响应式检测
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data
observe(data, true /* asRootData */)
}
- 首先判断data是对象还是函数,获取响应的data
- 校验
data属性是否合法,不能和props属性、methods方法名冲突 - 通过
defineReactive(props, key, value)设置data响应式 - 通过
proxy(vm,_data, key)在实例对象vm上设置props的每个key值,方便通过this[key]读取
function proxy (target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter () {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
(4)、 初始化compute属性 initComputed
- 设置
compute属性到vm上 - 设置监听,
compute属性的依赖
(5)、 初始化watch
- 将
watch的hanler添加到vm上 - 通过
vm.$watch(expOrFn, handler, options)监控watch属性的变化并触发handler
(6)、初始化provide
3、 调用生命周期钩子created
由于在调用created生命周期钩子之前已经完成了props、data、methods、计算属性、watch等的初始化,因此我们在created的生命周期钩子是可以对以上进行引用的。
三、生命周期created之后beforeMount之前做了啥
我们看到调用created生命周期之后执行了
vm.$mount(vm.$options.el), 这就是渲染代码的入口
const mount = Vue.prototype.$mount //缓存初始化时候原型上的$mount
Vue.prototype.$mount = function ( //重新定义原型上的$mount, 上面的vm.$mount(vm.$options.el)执行的$mount就是这里
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el) // document.querySelector(), vue模板
/* istanbul ignore if 校验 */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
// 如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法
// 在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,
// 无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法
if (!options.render) {
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
template = getOuterHTML(el)//获取到HTML的字符串
}
if (template) { //编译,生成render
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render //render表达式
options.staticRenderFns = staticRenderFns
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 调用runtime/index.js中的mount,继续后面的流程
return mount.call(this, el, hydrating)
}
以上代码做了两个事情
- 首先判断是否有
render函数Vue渲染函数,有的话执行后面的渲染逻辑mount.call(this, el, hydrating)我们看下Hello World的模板写成渲染函数是什么样子,render: function (createElement) { return createElement( 'div', // 标签名称 { attrs: { id: 'foo' } }, [ createElement('span', this.descText) ] ) }
其实createElement就是created生命周期的initRender里面在vm上增加的_c和$createElement属性
- 没有
render函数的话就去获取模板
先看下options属性上是否有template属性,如果有的话将其作为模板
如果options上没有template属性,会通过template = getOuterHTML(el)获取作为模板
- 接着将模板转成
render函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render //render表达式
- callHook(vm, 'beforeMount')
执行beforeMount钩子函数
其实从created到beforeMount阶段,主要是获取模板并将模板编译成render函数。
四、Mounted阶段
1、生成Vnode
vm._render()就是将render函数生成vNode,Hello World模板生成的vNode如下图所示,如果想对vNode深入了解,可以找资料学习。
vnode = render.call(vm._renderProxy, vm.$createElement)
生成的vNode如下:
2、patch阶段
patch阶段做的事情就是将虚拟DOM(vNode)树生成真实Dom并挂载到页面 。这个过程是从父节点开始遍历vNode树获取每个vNode节点,并将vNode节点生成真正的DOM,如果有children节点的话,继续遍历生成真实的DOM节点,并挂载到父vNode已经生成的DOM节点上,最后将根节点挂载到页面上,这样就完成了vNode到真实DOM的生成过程。
其实vNode节点生成真实DOM节点的过程还是用了JavaScript提供的原始DOM操作方法
该例子的vNode渲染成DOM的过程如下:
1、首先渲染最外层的节点,生成<div id="app"></div>
2、渲染孩子节点children, <span>Hello World</span>
3、将孩子节点<span>Hello World</span>插入到父节点<div id="app"></div>
4、将最外层的节点插入到body
3、 最后执行生命周期mounted。
五、总结
通过上面的介绍,可以看到Vue将模板渲染成真实DOM的过程有下面几个阶段:
- 初始化实例变量,把实例对象的属性值初始化;
- 初始化父组件相关事件;
- 初始化实例渲染相关方法;
- 执行生命周期钩子函数
beforeCreate; - 初始化
injection、provide、props、methods、data、computed、watcher等属性并实现响应式; - 执行生命周期钩子函数
created; - 如果没有
render函数,则获取Vue模板,可能来自template属性或者通过el选项指定的挂载元素中提取出的 HTML 模板; - 将
template转成render函数; - 执行生命周期钩子函数
beforeMount; - 将
render函数生成vNode(虚拟DOM); - 通过遍历
vNode(虚拟DOM),生成真实DOM并挂载到页面; - 执行生命周期钩子函数
mounted。
以上就是Vue实现渲染的大致过程,后面将会介绍watcher的原理。