活着,最有意义的事情,就是不遗余力地提升自己的认知,拓展自己的认知边界。
在搭建源码调试环境一节中,我们已经找到了Vue的构造函数,接下来开始探索Vue初始化的流程。
一个小测试
在精读源码之前,我们可以在一些重要的方法内打印一下日志,熟悉一下这些关键节点的执行顺序。(执行npm run dev后,源码变更后会自动生成新的vue.js,我们的测试html只需要刷新即可)
在初始化之前,Vue类的构建过程?
在此过程中,大部分都是原型方法和属性,意味着实例vm可以直接调用
注意事项:
1、以$为前缀的属性和方法,在调用_init原型方法的那一刻即可使用
2、以_为前缀的原型方法和属性,谨慎使用
3、本章旨在了解Vue为我们提供了哪些工具(用到时,深入研究,不必要在开始时花过多精力,后边遇到时会详细说明)
4、类方法和属性在new Vue()前后都可以使用,原型方法和属性只能在new Vue()后使用
定义构造函数
// src/core/instance/index.js
function Vue (options) {
//形式上很简单,就是一个_init方法
this._init(options)
}
挂载原型方法:_init
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) { }
挂载与state相关的原型属性和原型方法
// src/core/instance/state.js
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
//略
}
挂载与事件相关的原型方法
// src/core/instance/events.js
const hookRE = /^hook:/
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
Vue.prototype.$once = function (event: string, fn: Function): Component {}
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
Vue.prototype.$emit = function (event: string): Component {}
挂载与生命周期相关的原型方法
// src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
Vue.prototype.$forceUpdate = function () {}
Vue.prototype.$destroy = function () {}
挂载与渲染相关的原型方法
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)
Vue.prototype.$nextTick = function (fn: Function) {}
Vue.prototype._render = function (): VNode {}
挂载Vue类方法和类属性
// src/core/global-api/index.js
// config
const configDef = {}
configDef.get = () => config
Object.defineProperty(Vue, 'config', configDef)
Vue.util = {
warn,
extend,
mergeOptions,
defineReactive
}
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {
observe(obj)
return obj
}
Vue.options = Object.create(null)
ASSET_TYPES.forEach(type => {
Vue.options[type + 's'] = Object.create(null)
})
Vue.options._base = Vue
extend(Vue.options.components, builtInComponents)
initUse(Vue) //挂载类方法use,用于安装插件(特别特别重要)
initMixin(Vue) //挂载类方法mixin,用于全局混入(在Vue3中被新特性取代)
initExtend(Vue) //实现Vue.extend函数
initAssetRegisters(Vue)//实现Vue.component, Vue.directive, Vue.filter函数
挂载平台相关的属性,挂载原型方法$mount
// src/platforms/web/runtime/index.js
// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
console.log('挂载$mount方法')
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {}
拓展$mount方法
// src/platforms/web/entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount //保存之前定义的$mount方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
//执行拓展内容
return mount.call(this, el, hydrating) //执行最初定义的$mount方法
}
Vue的初始化过程(很重要哦!!!)
熟悉了初始化过程,就会对不同阶段挂载的实例属性了然于胸,了解Vue是如何处理options中的数据,将初始化流程抽象成一个模型,从此,当你看到用户编写的options选项,都可以在这个模型中演练。
前边我们提到过,Vue的构造函数中只调用了一个_init方法
执行_init方法
// src/core/instance/init.js
Vue.prototype._init = function (options?: Object) {
const vm: Component = this //此刻,Vue的实例已经创建,只是雏形,但Vue的所有原型方法可以调用
// a flag to avoid this being observed //(observe会在后面的响应式章节详细说明)
vm._isVue = true
// merge options
if (options && options._isComponent) {// 在后面的Vue组件章节会详细说明
// 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(//合并options
resolveConstructorOptions(vm.constructor),//主要处理包含继承关系的实例()
options || {},
vm
)
}
// expose real self
vm._self = vm
initLifecycle(vm) //初始化实例中与生命周期相关的属性
initEvents(vm) //处理父组件传递的事件和回调
initRender(vm) //初始化与渲染相关的实例属性
callHook(vm, 'beforeCreate') //调用beforeCreate钩子,即执行beforeCreate中的代码(用户编写)
initInjections(vm) // resolve injections before data/props 获取注入数据
initState(vm) //初始化props、methods、data、computed、watch
initProvide(vm) // resolve provide after data/props 提供数据注入
callHook(vm, 'created') //执行钩子created中的代码(用户编写)
if (vm.$options.el) {//DOM容器(通常是指定id的div)
vm.$mount(vm.$options.el) //将虚拟DOM转换成真实DOM,然后插入到DOM容器内
}
}
initLifecycle:初始化与生命周期相关的实例属性
export function initLifecycle (vm: Component) {
const options = vm.$options //将实例属性保存在本地,因为复杂数据保存在堆中,每次访问vm的实例属性,都会执行堆查找操作,通常多于两次访问一个变量时,将会保存在本地(即:用空间换时间的思想)
// 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 //通过上面的定位逻辑,vue组件实例的$parent指的是第一个非抽象父元素(因为抽象元素不会被渲染,即不会转换成真实的dom,由此看来,抽象元素只是作为中间过渡之用,你可以把它看成是工具)
vm.$root = parent ? parent.$root : vm //所有组件实例的$root都指向根组件实例
vm.$children = [] //通过$parent,$children的初始化来看,此时的执行顺序:先父组件,后子组件
vm.$refs = {} //通过初始化赋值,可以推断每一个vue组件的所有ref组合是一个对象
//与生命周期相关的初始化,为什么会涉及父元素,子元素的初始化?
//个人理解:从生到死,生皆有因(有父元素,特例:根元素),终必有果(有子元素,特例:叶子元素)
//1、在vue中有三中类型的watcher:render watcher, computed watcher, $watch watcher
// render watcher: 与渲染有关的
// computed watcher: 用户编写的computed options选项
// $watch watcher: 用户编写的$watch或watch选项
// 后续会有专门的章节专门讨论这三种watcher
//2、_watcher中保存的是render watcher,在响应式机制章节和异步更新机制章节中会有详细说明
//3、_watcher的初始化为什么属于生命周期的范畴,将_watcher理解为监护人(同“父子概念”一样理解)
//4、拓展:在vue1版本中,一个变量对应一个watcher,在vue2中,一个vue组件实例对应一个watcher
//5、在后面的initState中有一个实例属性_watchers,保存的是当前组件实例中所有的watcher
vm._watcher = null
vm._inactive = null //内置组件keep-alive的实例是否处于唤醒状态(休眠与唤醒的切换,也可以理解为生命周期的一部分)
vm._directInactive = false (暂未找到直接或间接的解释,后续补充,已备案)
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
initEvents(vm):处理父组件传递的事件和回调
// src/core/instance/events.js
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false //1、vue实例中是否包含钩子事件,比如created,beforeMount等等;2、在$on方法中有对钩子事件的处理;3、在CallHook中会详细说明
// init parent attached events
const listeners = vm.$options._parentListeners //只有非根组件中才会有_parentListeners属性,相关内容会在嵌套vue组件章节有详细说明
if (listeners) {
updateComponentListeners(vm, listeners) //暂略
}
}
initRender(vm):初始化与渲染相关的实例属性
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
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. 用户编写的渲染函数,渲染函数返回vnode,即虚拟dom
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)
}
CallHook(vm, 'beforeCreate'):执行beforeCreate钩子
执行options中,用户编写在beforeCreate中的代码
export function callHook (vm: Component, hook: string) {
// #7573 disable dep collection when invoking lifecycle hooks
pushTarget() //pushTarget的参数为undefined,导致Dep.target为undefined,依赖收集失败
const handlers = vm.$options[hook] //hook就是beforeCreate, created等等
const info = `${hook} hook`
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
invokeWithErrorHandling(handlers[i], vm, null, vm, info) //可以将此编码技巧用在项目中
}
}
if (vm._hasHookEvent) {
vm.$emit('hook:' + hook) //意味着我们可以手动执行钩子函数(例如:this.$emit('hook: mounted')),不过需要考虑依赖收集的影响(后续会讨论,已备案)
}
popTarget() //还原依赖收集的上下文
}
initInjections(vm): resolve injections before data/props 获取注入数据
export function initInjections (vm: Component) {
const result = resolveInject(vm.$options.inject, vm) //解析用户编写的inject
if (result) {
toggleObserving(false) //导致的结果:如果result[key]是对象或数组,则不会递归地进行observe
//也就是说,下面的defineReactive只会对inject的第一层属性进行响应式处理
Object.keys(result).forEach(key => {
defineReactive(vm, key, result[key]) //对属性的访问和设置拦截(此处利用的是ES5的API: defineProperty,Vue3中利用的是ES6的API: Proxy),将在响应式机制章节中详细说明
})
toggleObserving(true) //还原上下文
}
}
initState(vm):初始化props、methods、data、computed、watch(划重点啦!!!)
export function initState (vm: Component) {
vm._watchers = [] //保存的是当前vue实例中所有的watcher
const opts = vm.$options
if (opts.props) initProps(vm, opts.props) //初始化用户编写的props
if (opts.methods) initMethods(vm, opts.methods) //初始化用户编写的methods
if (opts.data) {
initData(vm) //初始化用户编写的data
} else {
observe(vm._data = {}, true /* asRootData */) //当无data选项时所执行的默认处理
}
if (opts.computed) initComputed(vm, opts.computed) //初始化computed选项
if (opts.watch && opts.watch !== nativeWatch) {
// Firefox has a "watch" function on Object.prototype...
// export const nativeWatch = ({}).watch
// ({})操作:将字面量转换成Object的实例,因此可以访问watch的原型方法
initWatch(vm, opts.watch) //初始化watch选项
}
}
initProps: 初始化props
此处概念比较多,propsData、 props、vm._props、 propsOptions,后续会结合实例来分析其区别,此处只做大概了解。
function initProps (vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {} //传递给子组件的props中属性对应的值
const props = vm._props = {} //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) //如果不是根组件,则只对props做shalow reactive(浅层响应式)
}
for (const key in propsOptions) {//propsOptions是用户编写在props中的代码
keys.push(key)
// validateProp 获取key对应的value,如果使用default默认值时,
// 对value做深层响应式处理,因为default的值是一个fresh copy(自己要亲自验证哦!!!)
const value = validateProp(key, propsOptions, propsData, vm)
defineReactive(props, key, value) // 此处做的是浅层响应式处理
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {//当vue实例上没有这个属性时,会进行代理处理,
// 这也是为什么可以通过this直接访问props中属性的原因。
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
}
initMethods:初始化methods
function initMethods (vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {//开发模式
if (typeof methods[key] !== 'function') {
// 意味着methods中的项必须是函数
}
if (props && hasOwn(props, key)) {
// 方法名不能是props中的属性名,由此看来,props的优先级比methods高
}
if ((key in vm) && isReserved(key)) {
// 如果key是vm的属性,同时首字符是'$'或'_',则报错
// isReserved:判断key的首字符是否是'$'或'_'
}
}
// 此处为methods中的函数绑定上下文,这也是methods中的函数能通过this访问vue实例的原因
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
}
}
initData: 初始化data
function initData (vm: Component) {
let data = vm.$options.data // 用户编写的data选项
// 如果data是函数,则会执行getData方法
// 在getData方法中,本质上就一行代码:return data.call(vm, vm),为data函数绑定的上下文
// 第二个参数告诉我们:data可以接受参数,使用这个参数和this本质上都是vue的实例vm
//注意事项:1、如果data不是函数,那么我们得到的就是data的引用地址,当这个组件被多次使用时,
// 一个实例中发生变化,其他实例对应的值也发生变化。
// (这也是我们在自定义组件中将data写成函数形式的原因,
// 因为执行函数后,每次得到的数据地址都不同
// (在函数栈帧中,参数的传入,数据的返回,都会))
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// isPlainObject: Object.prototype.toString.call(obj) === '[object Object]'
if (!isPlainObject(data)) {
data = {}
//报错提示
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {//开发模式
if (methods && hasOwn(methods, key)) {
//如果key在methods中已经使用,则会报错
}
}
if (props && hasOwn(props, key)) {
//开发模式下,data中使用的key在props中已经使用,则报错
} else if (!isReserved(key)) {
//如果key不是vue占用的标识符,则对_data中的属性进行代理,这也是可以用this访问data属性的原因
proxy(vm, `_data`, key)
}
}
// observe data 在此处对data进行深层响应式处理
// 通过上述key的校验后,再做响应式处理,可以避免一些徒劳
observe(data, true /* asRootData */)
}
initComputed:初始化computed 选项
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null) //挂载_computedWatchers属性
// computed properties are just getters during SSR
const isSSR = isServerRendering() //服务端渲染
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// create internal watcher for the computed property.
//为computed中的每一个key创建一个watcher,具体watcher的操作,在响应式机制章节详细说明
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)) {
//会将key对应的watcher缓存在vm的_computedWatchers中,具体详情,后面有专题讨论
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {//开发模式检测
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}
initWatch:初始化watch
createWatcher:本质上执行了vm.$watch(expOrFn, handler, options)
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {// 意味着一个key可以对应多个处理函数
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
initProvide(vm): 提供数据注入
为什么provide初始化滞后与inject,后续补充
export function initProvide (vm: Component) {
const provide = vm.$options.provide
if (provide) {
vm._provided = typeof provide === 'function'
? provide.call(vm)
: provide
}
}
CallHook(vm, 'created'): 执行created钩子中的代码
callHook的相关逻辑,参考上面的callHook(vm, 'beforeCreate')
执行挂载
执行$mount扩展
通过下面的代码可知:当用户代码中同时包含render,template,el时,它们的优先级依次为:render、template、el
const mount = Vue.prototype.$mount //保存原有的$mount,先执行拓展部分,然后再执行原有的$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) {
// 挂载容器el不能是body或documentElement
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) === '#') {
// 通过id获取template
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
//异常情况
return this
}
} else if (el) {
template = getOuterHTML(el)
}
if (template) {
// 将template转换成render
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
//执行最初定义的$mount
return mount.call(this, el, hydrating)
}
$mount方法中,首先获取挂载容器,然后执行mountComponent方法
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined //el真实存在,则获取真实的DOM元素
return mountComponent(this, el, hydrating)
}
// src/core/instance/lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el //$el赋值,用户最早在beforeMount钩子可以访问
if (!vm.$options.render) {//用户没有编写渲染函数,则进入该分支
//当用户编写的代码中,既没有render函数,也没有template模板,也没有正确的el,
// 在$mount拓展阶段就不会生成render,Vue就会提供一个默认的空节点渲染函数,挂载后就什么都不显示
vm.$options.render = createEmptyVNode
}
callHook(vm, 'beforeMount') //执行用户编写在beforeMount中的代码
let updateComponent
//该函数会在创建watcher过程中执行,要想清晰地看清调用过程,在该函数内部打断点,查看调用堆栈即可
updateComponent = () => {
// vm._render():获取组件对应的vnode(虚拟DOM)
// 将虚拟dom转换成真实DOM
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
// 创建一个render watcher,一个vue组件只有一个
// 创建watcher时,会调用传入的updateComponent,
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
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
}
在_update方法中,通过_vnode属性判断是否初次渲染,patch 其实就是patch方法,关于patch的详细逻辑,将在diff算法章节详细说明。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode //_vnode在下面赋值,因此初次渲染时,该字段为空
const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode //将将要渲染的虚拟DOM保存起来
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
// src/platforms/web/runtime/index.js
// install platform patch function
// Vue.prototype.__patch__ = inBrowser ? patch : noop
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
// 切换当前处于激活状态的vue实例
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {// prevEl是打补丁之前的$el,保存该值是因为打补丁后,$el会改变
// 此操作是为了解除原$el对组件实例的引用
prevEl.__vue__ = null
}
if (vm.$el) {//实现vm与$el的双向引用
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
// 关于$vnode的描述,后续补充
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.
}