Vue2.x源码学习笔记-从一个小例子查看vm实例生命周期

学习任何一门框架,都不可能一股脑儿的从入口代码从上到下,把代码看完,

这样其实是很枯燥的,我想也很少有人这么干,或者这么干着干着可能干不下去了。

因为肯定很无聊。

我们先从一个最最简单的小例子,来查看new Vue(options)实例,这个过程发生了什么。

vm实例上的属性又如何添加上去的,又如何渲染到浏览器页面上的。

关于vue的数据依赖和虚拟dom都是重点,必然会在以后的帖子记录。

这篇帖子就根据下例子,看看实例化一个vm实例做了啥吧。

先把小例子贴出来:

      <div id="app">
<p>这是<span>静态内容</span></p>
<p>{{message}}</p>
</div>
<script src="../../dist/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
message: 'hi vue!'
}
})
console.log(vm)
</script>

根据上篇介绍了vue的调式笔记,那我们快快进入源码吧

Vue2.x源码学习笔记-从一个小例子查看vm实例生命周期

Vue2.x源码学习笔记-从一个小例子查看vm实例生命周期

根据vue构造函数那篇笔记,我们知道了Vue原型上有哪些方法,_init方法就是其中一个方法

我们看到_init就把实例要做的事情都做完了,当然其中有的语句所做的事,太多了。我们先一点一点开see see吧。

看图不好玩,我把源码取出 来 好好瞧瞧

export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid vm实例唯一标识
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 _isComponent是内部创建子组件时才会添加为true的属性
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 {
// mergeOptions 方法 合并构造器及构造器父级上定义的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(`${vm._name} init`, startTag, endTag)
} if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}

1. 给实例添加了唯一标识uid

2.性能统计相关,先忽略

3. 给实例添加_isVue,监听对象变化时用于过滤vm

4. 选项对象如果有_isComponent,就初始化内部组件,_isComponent是内部创建子组件时才会添加为true的属性

5. 小例子会走分支,mergeOptions 方法 合并构造器及构造器父级上定义的options,resolveConstructorOptions方法后面笔记会详解,

mergeOptions方法接受3个参数。我们先简单看下resolveConstructorOptions方法的定义

export function resolveConstructorOptions (Ctor: Class<Component>) {
let options = Ctor.options
// 有super属性,说明Ctor是通过Vue.extend()方法创建的子类
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// super option changed,
// need to resolve new options.
Ctor.superOptions = superOptions
// check if there are any late-modified/attached options (#4976)
const modifiedOptions = resolveModifiedOptions(Ctor)
// update base extend options
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
}
return options
}

可以看出Ctor.options其实就是Vue构造函数自身,在Vue构造函数静态属性那篇笔记,Vue是拥有options属性的,且有截图,等下会再截图看下,

接着在该函数中有个if语句,我们小例子会跳过的,直接返回options。因为有super属性,说明Ctor是通过Vue.extend()方法创建的子类。那么

options是啥呢,如下图,

Vue2.x源码学习笔记-从一个小例子查看vm实例生命周期

回到_init方法中,mergeOptions方法的第二个参数就是我们传入的options,第三个参数就是vm实例,把参数一起截个图吧,好回忆

Vue2.x源码学习笔记-从一个小例子查看vm实例生命周期

mergeOptions是Vue中处理属性的合并策略的地方, 先看下它的定义

export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (process.env.NODE_ENV !== 'production') {
// 如果有options.components,则判断是否组件名是否合法
checkComponents(child)
}
// 格式化child的props
normalizeProps(child)
// 格式化child的directives
normalizeDirectives(child)
const extendsFrom = child.extends
if (extendsFrom) {
parent = typeof extendsFrom === 'function'
? mergeOptions(parent, extendsFrom.options, vm)
: mergeOptions(parent, extendsFrom, vm)
}
if (child.mixins) {
for (let i = 0, l = child.mixins.length; i < l; i++) {
let mixin = child.mixins[i]
if (mixin.prototype instanceof Vue) {
mixin = mixin.options
}
parent = mergeOptions(parent, mixin, vm)
}
}
const options = {}
let key
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options

该函数主要返回一个数据合并后的options,
我们的小例子比较简单, so关于判断是否组件名是否合法,
格式化child的props, 格式化child的directives
extends, mixins 先跳过。
我们直接看怎么把属性合并到options = {}这个对象上的
首先遍历parent对象,然后通过mergeField函数,把components,
directives, filters, _base属性先添加到options对象上,值为
strats对象上的静态方法。
然后遍历child对象,把el, data属性也添加到options = {} 这个对象上
值为strats对象上对应的静态方法。
那我们先看看strats这个对象上有哪些静态方法,源码如下(src/util/options.js)

const strats = config.optionMergeStrategies

if (process.env.NODE_ENV !== 'production') {
strats.el = strats.propsData = function (parent, child, vm, key) {
/**/
}
} strats.data = function (
parentVal: any,
childVal: any,
vm?: Component
): ?Function {
/**/
} function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
} config._lifecycleHooks.forEach(hook => {
strats[hook] = mergeHook
}) function mergeAssets (parentVal: ?Object, childVal: ?Object): Object {
/**/
} config._assetTypes.forEach(function (type) {
strats[type + 's'] = mergeAssets
}) strats.watch = function (parentVal: ?Object, childVal: ?Object): ?Object {
/* istanbul ignore if */
/**/
} strats.props =
strats.methods =
strats.computed = function (parentVal: ?Object, childVal: ?Object): ?Object {
if (!childVal) return Object.create(parentVal || null)
if (!parentVal) return childVal
/**/
} const defaultStrat = function (parentVal: any, childVal: any): any {
return childVal === undefined
? parentVal
: childVal
}

以上是缩减版的代码,其实看下截图,会一目了然

Vue2.x源码学习笔记-从一个小例子查看vm实例生命周期

可以看到其实就是我们new Vue(options) 中的options对象中的可选参数。我们小例子只传了el, data,

我们看看通过mergeOptions方法合并后的options长的什么鸟样,如图:

Vue2.x源码学习笔记-从一个小例子查看vm实例生命周期

其实小例子只是走个过程,没必要把所有函数代码弄懂,先把大体流程走完,后续代码在一一分析。

6. 回到vm_init()方法中,接着走initProxy(vm)这个语句,这个语句其实就是给vm实例添加了一个_renderProxy属性,值为为一个Proxy代理对象,生产环境就是vm自身。

接下来的每个语句都有好多代码啊,我们一个个 look see see

7. initLifecycle 方法的定义

export function initLifecycle (vm: Component) {
const options = vm.$options // locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
} 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
}

初始化生命周期,该函数只是给vm添加了
$parent, $root, $children, $refs, _watcher,
_inactive, _directInactive, _isMounted, _isDestroyed
_isBeingDestroyed属性。

options.abstract用于判断是否是抽象组件,
组件的父子关系建立会跳过抽象组件,抽象组件比如keep-alive、transition等。
所有的子组件$root都指向*组件。

8. initEvents方法的定义

export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}

该方法初始化事件相关的属性,给vm实例添加了_events, _hasHookEvent属性
_parentListeners是父组件中绑定在自定义标签上的事件,供子组件处理。

9. initRender方法的定义

export function initRender (vm: Component) {
vm.$vnode = null // the placeholder node in parent tree
vm._vnode = null // the root of the child tree
vm._staticTrees = null
const parentVnode = vm.$options._parentVnode
const renderContext = parentVnode && parentVnode.context
vm.$slots = resolveSlots(vm.$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)
}

这里先给vm添加了$vnode, _vnode, _staticTrees, $slots, $scopedSlots, _c, $createElement
属性或者方法(添加了一些虚拟dom、slot等相关的属性和方法)

10. 调用beforeCreate钩子

11. 调用initInjections(vm)方法,我们小例子比较简单,不会进入if语句中

export function initInjections (vm: Component) {
const inject: any = vm.$options.inject
if (inject) {
// inject is :any because flow is not smart enough to figure out cached
// isArray here
const isArray = Array.isArray(inject)
const keys = isArray
? inject
: hasSymbol
? Reflect.ownKeys(inject)
: Object.keys(inject) for (let i = 0; i < keys.length; i++) {
const key = keys[i]
const provideKey = isArray ? key : inject[key]
let source = vm
while (source) {
if (source._provided && provideKey in source._provided) {
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
defineReactive(vm, key, source._provided[provideKey], () => {
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, source._provided[provideKey])
}
break
}
source = source.$parent
}
}
}
}

将父组件provide中定义的值,通过inject注入到子组件,且这些属性不会被观察

12. initState(vm)

export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch) initWatch(vm, opts.watch)
}

该方法主要就是操作数据了,props、methods、data、computed、watch,
从这里开始就涉及到了Observer、Dep和Watcher,下个笔记再记录

13. initProvide(vm)

 export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}

也不会进分支,先略过

14. 调用created钩子函数。

可以看到在created钩子函数调用前, 基本就是对传入数据的格式化、数据的双向绑定、以及一些属性的初始化。

现在来看下实例的属性和方法

Vue2.x源码学习笔记-从一个小例子查看vm实例生命周期

Vue2.x源码学习笔记-从一个小例子查看vm实例生命周期

接下来看看怎么把html模板中的属性出来的

15. vm.$mount(vm.$options.el)

const mount = Vue.prototype.$mount
// 重写Vue构造函数原型上的$mount方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el) /* 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
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)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
} const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters
}, this)
options.render = render
options.staticRenderFns = staticRenderFns /* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}

该方法主要拿到template模板,然后通过compileToFunctions方法的返回值给vm实例
的$options添加render属性,值为一个匿名函数该匿名函数返回值为:
with(this){return _c('div',{attrs:{"id":"app"}},[_m(0),_v(" "),_c('p',[_v(_s(message))])])}
还添加了一个staticRenderFns属性,值为一个数组,数组元素为匿名函数:

anonymous() {
with(this){return _c('p',[_v("这是"),_c('span',[_v("静态内容")])])}
}

至于compileToFunctions函数先不拿出来看了,目前先知道它干了啥,就行了(至于实例的_c,_v,_m这些方法何时挂载上去的,前面笔记已经说过了)

之后调用 mount.call(this, el, hydrating) 方法

Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}

该方法又调用mountComponent(this, el, hydrating)

export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
callHook(vm, 'beforeMount') let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}` mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`${name} render`, startTag, endTag) mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`${name} patch`, startTag, endTag)
}
} else {
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
} vm._watcher = new Watcher(vm, updateComponent, noop)
hydrating = false // manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}

小例子在该方法大致流程是这么走的

因为vm.$options.render就是给匿名函数,所以不会走if分支,
然后调用beforeMount钩子函数
再然后定义一个updateComponent函数,这个函数怎么执行是个关键
然后给实例添加了一个_watcher属性,值为Watcher实例
然后如果vm.$vnode == null则把vm._isMounted变量置为true,然后调用mounted钩子函数
最后返回vm实例,可以链式调用。

触发updateCOMPONENT函数是new Watcher,先看看Watcher类的定义

constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
...
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: '' if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
} get () {
pushTarget(this)
let value
const vm = this.vm
if (this.user) {
try {
value = this.getter.call(vm, vm)
} catch (e) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
}
} else {
value = this.getter.call(vm, vm)
} if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}

在构造函数中,expOrFn也就是updateComponent赋值给this.getter,
并且在获取this.value的值时会调用this.get(),这里的this.lazy默认值是false,
在computed属性中创建的Watcher会传入true。

在this.get()中,会调用this.getter,所以上面的例子中,updateComponent方法会被调用,

updateComponent = () => {
vm._update(vm._render(), hydrating)
}

回到该函数,先执行实例的_render函数,该函数主要生成虚拟dom
然后执行实例的update方法

Vue.prototype._render = function (): VNode {
const vm: Component = this
const {
render,
staticRenderFns,
_parentVnode
} = vm.$options if (vm._isMounted) {
// clone slot nodes on re-renders
for (const key in vm.$slots) {
vm.$slots[key] = cloneVNodes(vm.$slots[key])
}
} vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject if (staticRenderFns && !vm._staticTrees) {
vm._staticTrees = []
}
// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render function`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
vnode = vm.$options.renderError
? vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
: vm._vnode
} else {
vnode = vm._vnode
}
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}

该函数主要是 vnode = render.call(vm._renderProxy, vm.$createElement)语句
函数调用过程中的this,是vm._renderProxy,是一个Proxy代理对象或vm本身。暂且把它当做vm本身。

_c是(a, b, c, d) => createElement(vm, a, b, c, d, false)。
createElement函数做了这些事:
a是要创建的标签名,这里是div。
接着b是data,也就是模板解析时,添加到div上的属性等。
c是子元素数组,所以这里又调用了_c来创建一个p标签。
_v是createTextVNode,也就是创建一个文本结点。
_s是_toString,也就是把message转换为字符串,在这里,因为有with(this),
所以message传入的就是我们data中定义的第一个vue实例。
所以,render函数返回的是一个VNode对象,也就是我们的虚拟dom对象。
它的返回值,将作为vm._update的第一个参数。

接着看下update方法

  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
activeInstance = prevActiveInstance
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}

从mountComponent中知道创建Watcher对象先于vm._isMounted = true。
所以这里的vm._isMounted还是false,不会调用beforeUpdate钩子函数。

下面会调用vm.__patch__,在这一步之前,
页面的dom还没有真正渲染。该方法包括真实dom的创建、虚拟dom的diff修改、dom的销毁等,
具体细节后续笔记在记录。

上一篇:20145205 《Java程序设计》实验报告三:敏捷开发与XP实践


下一篇:c#unicode,中文互转