这是一个系列文章,请关注 vue@2.6.11 源码分析 专栏
new Vue(...)
// src/core/instance/index.js
function Vue (options) {
this._init(options);
}
- 组件实例的初始化(事件、方法、属性、数据等等)
- 组件的渲染(
vm._init
中会去调用vm.$mount
方法,个人感觉这个方法调用放到构造函数中更合适,逻辑更清晰些)。实际上在子组件的渲染逻辑中,这个两步就是分开的,如下:const componentVNodeHooks = { init (vnode: VNodeWithData, hydrating: boolean): ?boolean { if (/** keepAlive场景 **/) { //... 忽略 } else { // 1. createComponentInstanceForVnode内部会去调用子组件构造函数(Vue.extend返回,后面会说到) // 构造函数会直接调用 vm._init 进行组件实例的初始化 const child = vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance) // 2. 开始生成虚拟DOM,并将虚拟DOM同步到界面上 child.$mount(hydrating ? vnode.elm : undefined, hydrating) } }, }
因此:我将整体流程划分为两个阶段:
下面我们先看下组件实例化过程中的初始化工作都有哪些,见下面vm._init
方法分析。
vm._init: 组件实例初始化(渲染之前的准备工作)
// src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
vm._uid = uid++
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)
}
vm._renderProxy = vm
vm._self = vm // expose real self
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')
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
首先是收集当前组件实例的options
并保存到 vm.$options
(主要目的是复用公共选项,公共选项保存在祖先构造函数中,如Vue.options),这里区分了两种场景:组件、new Vue()
- 组件:针对组件的初始化,走
initInternalComponent
export function initInternalComponent (vm: Component, options: InternalComponentOptions) { const opts = vm.$options = Object.create(vm.constructor.options) // doing this because it's faster than dynamic enumeration. const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } }
-
通过
原型继承
(Object.create
)基于组件构造函数 选项创建一个新的对象保存到当前组件实例vm.$options
中,目的就是复用构造函数中的选项;而后从入参中的options
中取出父组件实例
(Vue实例:parent
),和父vnode
(即parentVnode
),保存到vm.$options
中;父vnode
实际上一个placeholder vnode
即一个占位节点,并不会渲染到界面中,作为父子组件的纽带,这个placeholder vnode
中有一个属性componentOptions
保存了父组件传递给属性数据,监听的事件,和该vnode的孩子节点。 -
如下demo中的
<todo-item>
会被转为一个虚拟节点就是这里的placeholder vnode
,另外children
是指如下案例的<span/>
部分。todo-item
是在作为父组件内容出现的,当然也是在渲染父组件之创建父组件虚拟节点阶段创建的,此时刚好把这些信息记录给这个placeholder vnode
,当渲染todo-item
的实际内容时,就会从placeholder vnode
获取这些信息。<div> <todo-item @click='clickHandle' :propsA = 'A'> <span></span> </todo-item> </div>
new Vue(options)
:由于创建的时根组件不需要上面componentOptions
信息的合并,只需要将祖先构造函数选项和当前实例选项合并即可。注意,复现构造函数选项通过resolveConstructorOptions
来收集和更新。由于组件构造函数选项在Vue.extend(options)
中已经解析过,这里就必要次重复解析。注意:Vue中组件的构造函数是通过Vue.extend
方法基于原型继承创建的,当然是返回一个构造函数,后面会说到。
然后是events
、lifecycle
,inject/provede
、state
(methods
、props
、data
、computed
、watch
)的初始化
然后是vm.$mount
:new Vue
场景会提供el
参数,从而会执行vm.$mount
最后除了几个initXxx
方法,还有两个生命周期的触发:beforeCreate
、created
下面具体看看各种(实例vm相关的信息)初始化
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
}
设置 $parent
| $root
| $children
| $refs
:建立父子组件vue实例关系链:$parent
、$children
,这个过程忽略中间的抽象组件实例。$options.abstract,看到内置组件Keep-Alive
是抽象的 ❎。
添加组件生命周期相关的的标识:_inactive、_directInactive、_isMounted、_isDestroyed、_isBeingDestroyed
添加_watcher
属性,这个属性指向的Watcher实例和组件渲染有关
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) // src/core/vdom/helpers/update-listeners.js
}
}
每个Vue实例上都有$on
、$off
和事件相关的方法(见上一小节),相当于每个组件实例都有事件能力。调用updateComponentListeners
将新事件绑定到vm上,移除旧的事件。
vm.$options._parentListeners来自上面分析的initInternalComponent
方法,事件实际来自placeholder vnode
.componentOptions
initRender
给组件实例挂载创建虚拟DOM的方法(等价于snabbdom中的h
函数,见snabbdom@3.5.1 源码分析 - 第二节)
export function initRender (vm: Component) {
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
const renderContext = parentVnode && parentVnode.context
//... slot相关,暂时忽略,可能会单独出一节
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
// $attrs & $listeners are exposed for easier HOC creation.
// they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data
defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}
_c
:对于模板编译生成(不管是动态生成还是 vue-loade + webpack 生成,编译核心方法是 compileToFunctions)的render函数在模板编译阶段已经规范化过,不用再次被规范化,所以看到最后一个参数alwaysNormalize = false。
$createElement
:对于开发者自己提供的 render 函数需要被规范化,因此最后一个参数 alwaysNormalize = true
import Vue from 'vue'
import App from './App.vue'
var app = new Vue({
el: '#app',
render: h => h(App) // 这里的 h 是 createElement 方法
})
另外看到针对HOC
场景,定义响应式数据:vm.$attrs
和 vm.$listeners
。
initInjections、initProvide
不重要,遗留,有兴趣的同学可以自己看下,逻辑简单。
initState
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)
} //...
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
相关属性的响应式依赖过程:props
-> data
-> computed
-> watch
,因此需要按照这个顺序依次添加响应式能力。methods正常情况下应该无所谓,但是看到注释中提到methods名称不能和props重复,因此先props后methods。
比如 data中可能会到props中的数据,computed可能会用到data中的数据。watch可能会依赖前三者。
因此这里的 执行顺序 很重要。
initProps
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = vm.$options._propKeys = []
const isRoot = !vm.$parent
// root instance props should be converted
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
defineReactive(props, key, value)
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
toggleObserving(false)
:有一个专门的提交 avoid conversion when setting props- 这里只是不需要将父组件传递的
属性值
变成响应式,只是将当前属性
变成响应式。 - defineReactive -> observe,observe会根据 shouldObserve 变量判断是否将对象变成响应式
- defineReactive有入参shallow可以递归属性值,为什么不直接使用这个方式呢,因为如果属性值是原本就是响应式的话,子组件还是想继续监听的(针对属性set/del这种变化,因为是childOb,见defineReacive)。
- defineReactive and observe
- 属性值非响应式对象,暂时只能想起provide/inject情况,即父组件传递的属性值来自inject(猜测,尚未验证)
v2.cn.vuejs.org/v2/api/#pro…
提示:provide
和inject
绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。
- 这里只是不需要将父组件传递的
- 调用
validateProp
:如果父组件没有传递属性值,则设置默认值等,并会将默认值变成响应式对象 - 将
props
的各属性变成响应式,由于上面第一步,不会主动将父组件传递的属性值变成响应式,想一想这是合理的,管理好自己的数据就好,没必要动父组件的,否则反而可能引起问题。 - 代理 vm.xxx => vm._props.xxx,见
proxy
方法实现,这也是为什么我们this.a
可以直接访问属性的原因,等价于访问this._props.a
export 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) }
initMethods
function initMethods (vm: Component, methods: Object) {
for (const key in methods) {
//... 开发环境同名判断,不重要
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
将方法赋值给vm实例,注意绑定了this
指向
initData
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
if (!isPlainObject(data)) {
data = {}
}
// proxy data on instance
const keys = Object.keys(data)
let i = keys.length
while (i--) {
const key = keys[i]
proxy(vm, `_data`, key)
}
// observe data
observe(data, true /* asRootData */)
}
首先是调用getData
获取数据,注意genData
的实现,防止被上层的watcher
订阅,手动pushTarget
空值,这样就不会被上层的watcher
订阅到。因为这里只是获取data
的默认值,此时并需要和任何watcher
建立任何依赖关系。
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} // catch ...
} finally {
popTarget()
}
}
然后是将data
变成响应式数据。
initComputed
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
// create internal watcher for the computed property.
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
首先是遍历computed
,针对每个属性去new Watcher(...)
,注意这里的有个lazy
的选项即getter并不会立即执行,此时并不会真正的去收集依赖。这里另外的细节是,computed
定义的getter
中可以使用props\data
中的数据,由于props\data
在这之前已经是响应式数据,因此即使立即收集依赖也不会有问题,可以正确建立双向关系(观察者和props\data
)。
然后调用defineComputed
,分析如下
export function defineComputed (target: any, key: string, userDef: Object | Function) {
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get ? createComputedGetter(key): noop
sharedPropertyDefinition.set = userDef.set || noop
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
调用 createComputedGetter
返回一个闭包函数(持有key的引用)作为新的getter;
然后将该key和属性描述符(getter/setter)添加到vm上,读取vm[key]
这个属性值时会执行上面返回的 computedGetter
(什么时候会调用到呢,可能是mounted生命周期,可能是点击事件回调,可能是页面渲染需要的数据)
computedGetter
分析如下
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
如果发现数据是脏的,则重新估算该值(这也是之前lazy的价值,延迟执行,真正需要时再去执行)
// Watcher.js
evaluate () {
this.value = this.get()
this.dirty = false
}
computedGetter
中的watcher.depend()
的作用:为了让当前组件的渲染watcher
(后面会说到new Watcher(updateComponent)
)也订阅到相同的依赖。
场景1: mountComponet
→ new Watcher
创建一个渲染组件能力的watcher
即这里的Dep.target
,而上面computedGetter
中的watcher
是computed
属性相关的watcher
- 如果在
render
函数中用到了computed
中的数据,则会读取,那么会执行computedGetter
,先是执行watcher.evaluate
获取computed
属性的值(执行属性的handler
,这个过程读取响应式数据),这里帮助watcher
(computed属性关联的)收集依赖,因此依赖的数据变化时,会重新执行handler
计算新值。 - 由于
evaluate
执行过程中的Dep.target
指向的watcher
是computed
属性关联的(因为watcher.get
中会去pushTarget,popTarget
原因),而此时渲染Watcher
并没有和这部分的相应的数据建立关系。因此在执行完evaluate
后(popTarget
),此时的Dep.target
指向渲染watcher
,需要手动去收集一次依赖即computedGetter
中的watcher.depend()
。 - 小结:这里有两个
watcher
,都应该向对computed[key]
依赖的响应式数据添加订阅,但是由于框架的设计时同时只有一个watcher
可以作为观察者,因此内部的watcher
完成后,需要手动触发外层watcher
向内层wathcher
订阅的依赖订阅一次。(目前只有这一个地方这么用 , 渲染会在nextTick
中执行,不会产生性能问题,也行得通;否则应该让外层的watcher
向内层的watcher
订阅,不过内层的watcher
同时需要扮演依赖即Dep的角色。)/** * Depend on all deps collected by this watcher. */ Watcher.prototype.depend = function depend () { var i = this.deps.length; while (i--) { this.deps[i].depend(); } };
场景2: 如果没有在渲染阶段用到这个computed
属性,则不会引起该组件的重新渲染,显然是合理的。因为界面不不需要这些信息。
initWatch
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher (vm: Component, expOrFn: string | Function, handler: any, options?: Object) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
逻辑很显然,不赘述。
Vue.prototype.$watch
Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options?: Object): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} //...
}
return function unwatchFn () {
watcher.teardown()
}
}
添加了对options.immediate
支持、返回watcher
卸载能力方法。
vm.$mount:创建虚拟DOM + 虚拟DOM同步到界面
我们使用的是具有运行时模板编译的版本,意味着我们在new Vue()
参数中的template
会被构建为render
函数,这是“在线编译”的过程,它是调用 compileToFunction
方法实现的。
在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要
render
方法,无论我们是用单文件 .vue 方式开发组件,还是写了el
或者template
属性,最终都会转换成render
方法。
// src/platforms/web/entry-runtime-with-compiler.js
import { compileToFunctions } from './compiler/index'
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el?: string | Element, hydrating?: boolean): Component {
el = el && query(el)
if (el === document.body || el === document.documentElement) {
// Vue 不能挂载在 body、html 这样的根节点上
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)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
const { render, staticRenderFns } = compileToFunctions(template, {...}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
首先是提供运行时模板编译能力 ,即将template
转为render
函数
1. Vue不能挂载在 `body`、`html` 这样的根节点上
2. `template`可以是…,总之需要转为`html` 模板
1. dom id 如 “#app”,获取其 innerHTML
2. 又或者是DOM节点,获取其 innerHTML
3. 如果没有提供,则获取`el` 的outerHTML
4. 或者直接是 HTML字符串模板,如上述 demo
3. 调用 `compileToFunctions`方法将`template`转为`render`函数
然后是调用运行时版本的$mount
函数,定义在 src/platforms/web/runtime/index.js 中(web版本的运行时构建入口)
// src/platforms/web/runtime/index.js
import { mountComponent } from 'core/instance/lifecycle'
Vue.prototype.$mount = function(el?:string| Element, hydrating?:boolean): Component {
el = el && inBrowser ? query(el) :undefined
return mountComponent(this, el, hydrating)
}
mountComponent:vm._render + vm._update
export function mountComponent (vm: Component, el: ?Element, hydrating?: boolean): Component {
vm.$el = el
callHook(vm, 'beforeMount')
let updateComponent
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
两件事情
第一件:挂载和更新 DOM,
new Watcher()
:提供了监听数据变化的基础能力,那重点来了,上面data,props,computed 等都变成了响应式数据,如果在当watcher中读取了这些数据,就会建立双向关系,当这些数据变化时则会通知这里的watcher,自然就会用最新的数据更新界面。new Watcher(vm, getter , ...) // 第一次触发getter 就是 挂载 // 第二次触发getter 就是 更新
updateComponent
:_render
: 创建虚拟DOM树,传递给_update;_update
: 将_render后关于虚拟节点树的结果挂载或者更新到界面上(生成或者更新DOM
第二件:生命周期回调的触发:beforeMount
、beforeUpdate
、mounted
总结
组件实例options选项的合并,分别有哪几类
- 组件:组件选项,Vue选项,内部选项
- new Vue:实例选项、Vue选项
这一节主要是在说组件渲染之前的准备过程,各种数据的初始化,事件注册等等。 下节重点说下组件渲染真正渲染的过程(虚拟DOM的创建和虚拟DOM同步到界面的逻辑)
updateComponent = () => {
vm._update(vm._render(), hydrating) // hydrating: ssr相关 忽略
}
new Watcher(vm, updateComponent, noop, {
before () { /*...*/ }
}, true /* isRenderWatcher */)