这是一个系列文章,请关注 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()
- 组件:针对组件的初始化,走
initInternalComponentexport 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.aexport 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 */)