上一篇按照核心文件的结构,从宏观角度了解了Vue对象的构造过程,这里再从微观角度看看new Vue()的过程中框架都做了些什么。
首先来个引入vue的demo。在demo中js:
var demo = new Vue({
el: '#demo',
data() {
return {
text: 'hello world!'
}
}
})
_init方法
当我们在new一个vue对象的时候,初始化一个实例,在instance/init中的Vue构造函数,代码执行了this._init(options),那我们就从_init入手。
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
// 浏览器环境&支持window.performance&非生产环境&配置了performance
if (process.env.NODE_ENV !== 'production'
&& config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
// 相当于 window.performance.mark(startTag)
mark(startTag)
}
// a flag to avoid this being observed
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 {
// 将options进行合并
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)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production'
&& config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
简单看一下,首先吧当前实例给VM:
- 在当前实例中,添加_uid,_isVue属性。
- 当非生产环境时,用window.performance标记vue初始化的开始。
- 由于我们的demo中,没有手动处理_isComponent,所以这里会进入到else分支,将Vue.options与传入options进行合并。
- 为当前实例添加_renderProxy,_self属性。
- 初始化生命周期,initLifecycle
- 初始化事件,initEvents
- 初始化render,initRender
- 调用生命周期中的beforeCreate
- 初始化注入值 initInjections
- 初始化状态 initState
- 初始化Provide initProvide
- 调用生命周期中的 created
- 非生产环境下,标识初始化结束,为当前实例增加_name属性
- 根据options传入的el,调用当前实例的$mount
这是实例化时_init方法,接下来结合我们的demo,来细细的看下每一步产生的影响,以及具体调用的方法。
mergeOptions
看到给到的三个参数
{
resolveConstructorOptions(vm.constructor),
options || {},
vm
}
记得在runtime对Vue的变更之后,options变成了:
Vue.options = {
components: {
KeepAlive: { name: "keep-alive" …}
Transition: {name: "transition", props: {…} …}
TransitionGroup: {props: {…}, beforeMount: ƒ, …}
},
directives: {
model: { componentUpdated: ƒ …}
show: { bind: ƒ, update: ƒ, unbind: ƒ }
},
filters: {},
_base: ƒ Vue
}
- 首先将this.constructor传入resolveConstructorOptions中,因为我们的demo中没有进行继承操作,所以在resolveConstructorOptions方法中,没有进入if,直接返回得到的结果,就是在runtime中进行处理后的options选项。而options就是我们在调用new Vue({})时,传入的options。此时,mergeOptions方法变为:
vm.$options = mergeOptions(
{
components: {
KeepAlive: { name: "keep-alive" …}
Transition: {name: "transition", props: {…} …}
TransitionGroup: {props: {…}, beforeMount: ƒ, …}
},
directives: {
model: { componentUpdated: ƒ …}
show: { bind: ƒ, update: ƒ, unbind: ƒ }
},
filters: {},
_base: ƒ Vue
},
{
el: '#demo',
data: ƒ data()
},
vm
)
demo经过mergeOptions之后,变为了如下:
在merge完options后,会判断如果是非生产环境时,会进入initProxy方法。
initProxy(代理)
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
vm._self = vm
不是很明白,大概就是这里在非生产环境时,对config.keyCodes的一些关键字做了禁止赋值操作。
initLifecycle
初始化生命周期相关属性:
function initLifecycle (vm) {
const options = vm.$options
// 省去部分与本次demo无关代码
...
vm.$parent = undefined
vm.$root = vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
关于生命周期钩子函数:生命周期钩子
vm.$mount
还记得在给vue原型添加方法和属性的时候有Vue.prototype. mount就是覆盖Vue.prototype. m o u n t 就 是 覆 盖 V u e . p r o t o t y p e . mount
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
const options = this.$options
if (!options.render) {
let template = getOuterHTML(el)
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
这里在覆盖 mount之前,先将原有的 m o u n t 之 前 , 先 将 原 有 的 mount保留至变量mount中,整个覆盖后的方法是将template转为render函数挂载至vm的options,然后调用调用原有的mount。所以还记得mount来自于哪嘛?那就继续吧runtime/index,方法很简单,调用了生命周期中mountComponent。
function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
callHook(vm, 'beforeMount')
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
new watcher()
vue中每个组件实例都有相应的 watcher 实例对象,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
打开src/core/observer/watcher.js,让我们看看Watcher的构造函数吧。为了清楚的看到Watcher的流程。依旧只保留方法我们需要关注的东西:
constructor (vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
vm._watcher = this
vm._watchers.push(this)
this.getter = expOrFn
this.value = this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
popTarget()
this.cleanupDeps()
return value
}
- 在Watcher的构造函数中,本次传入的updateComponent作为Wather的getter。
- 在get方法调用时,又通过pushTarget方法,将当前Watcher赋值给Dep.target
- 调用getter,相当于调用vm._update,先调用vm._render,而这时vm._render,此时会将已经准备好的render函数进调用。
- render函数中又用到了this.text,所以又会调用text的get方法,从而触发了dep.depend()
dep.depend()会调回Watcher的addDep,这时Watcher记录了当前dep实例。 - 继续调用dep.addSub(this),dep又记录了当前Watcher实例,将当前的Watcher存入dep.subs中。
- 这里顺带提一下本次demo还没有使用的,也就是当this.text发生改变时,会触发Observer中的set方法,从而触发dep.notify()方法来进行update操作。
就这样,Vue的数据响应系统,通过Observer、Watcher、Dep完美的串在了一起。其中在mount里template转换为render,render实际调用时,会经历_render, $createElement, patch, 方法,有兴趣可以自己浏览下’src/core/vdom/’目录下的文件,来了解vue针对虚拟dom的使用。关于vdom:
Vue原理解析之Virtual Dom
终!