Vue源码解读(一)
前言
接触 vue 已经有一段时间了,在此期间也开发了好几个 vue 项目,虽然说现在用 vue 开发使用是没问题的,但是如果想要玩转它还是需要读一下 vue 源码,去了解一下它的底层实现原理。下面就分享一下我看 vue 源码的一些感触,然后记录一下我对源码的理解,欢迎指正,纯属个人笔记,大牛勿喷!
vue 官网的 vue 的生命周期图:
源码学习目录
本文所剖析的 Vue.js 源码版本是以我现在项目使用为例,版本号为 v2.6.10 ,其代码目录如下:
首先从项目的 main.js 文件开始:
new Vue({ el: '#app', router, store, ddd: { name: 'yasin' }, render(h) { return h(App) } })
目前已经到达了生命周期的(new Vue())
我们点开 vue 的源码(/node_modules/vue/src/core/index.js),找到构造函数:
import Vue from './instance/index' import { initGlobalAPI } from './global-api/index' import { isServerRendering } from 'core/util/env' import { FunctionalRenderContext } from 'core/vdom/create-functional-component' initGlobalAPI(Vue) Object.defineProperty(Vue.prototype, '$isServer', { get: isServerRendering }) Object.defineProperty(Vue.prototype, '$ssrContext', { get () { /* istanbul ignore next */ return this.$vnode && this.$vnode.ssrContext } }) // expose FunctionalRenderContext for ssr runtime helper installation Object.defineProperty(Vue, 'FunctionalRenderContext', { value: FunctionalRenderContext }) Vue.version = '__VERSION__' export default Vue
入口文件中定义了一些服务端渲染的东西,我们不关心
初始阶段 new Vue
初始化阶段所做的第一件事就是new Vue()
创建一个 Vue 实例,那么new Vue()
的内部都干了什么呢? 我们知道,new 关键字在 JS 中表示从一个类中实例化出一个对象来,由此可见, Vue 实际上是一个类。所以new Vue()
实际上是执行了 Vue 类的构造函数,那么我们来看一下 Vue 类是如何定义的,Vue 类的定义是在源码的/node_modules/vue/src/core/instance/index.js:
function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) }
可以看到,Vue 类的定义非常简单,其构造函数核心就一行代码:
this._init(options)
调用原型上的_init(options)
方法并把用户所写的选项options
传入。那这个_init
方法是从哪来的呢?在 Vue 类定义的下面还有几行代码,其中之一就是:
initMixin(Vue)
这一行代码执行了 initMixin 函数,那 initMixin 函数又是从哪儿来的呢?该函数定义位于源码的 src/core/instance/init.js 中,如下:
export function initMixin (Vue: Class) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` 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 { 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) } } }
可以看到,在initMixin
函数内部就只干了一件事,那就是给Vue
类的原型上绑定_init
方法,同时 _init 方法的定义也在该函数内部。现在我们知道了,new Vue()会执行 Vue 类的构造函数,构造函数内部会执行 _init 方法,所以new Vue()
所干的事情其实就是 _init 方法所干的事情,那么我们着重来分析下 _init 方法都干了哪些事情。
首先,把 Vue 实例赋值给变量vm
,并且把用户传递的options
选项与当前构造函数的 options 属性及其父级构造函数的 options 属性进行合并(关于属性如何合并的问题下面会介绍),得到一个新的options
选项赋值给$options
属性,并将$options
属性挂载到 Vue 实例上,如下:
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
接着,通过调用一些初始化函数来为 Vue 实例初始化一些属性,事件,响应式数据等,如下:
initLifecycle(vm) // 初始化生命周期 initEvents(vm) // 初始化事件 initRender(vm) // 初始化渲染 callHook(vm, 'beforeCreate') // 调用生命周期钩子函数 initInjections(vm) //初始化 injections initState(vm) // 初始化 props,methods,data,computed,watch initProvide(vm) // 初始化 provide callHook(vm, 'created') // 调用生命周期钩子函数
可以看到,除了调用初始化函数来进行相关数据的初始化之外,还在合适的时机调用了 callHook 函数来触发生命周期的钩子,关于 callHook 函数是如何触发生命周期的钩子会在下面介绍,我们先继续往下看:
if (vm.$options.el) { vm.$mount(vm.$options.el) }
在所有的初始化工作都完成以后,最后,会判断用户是否传入了el
选项,如果传入了则调用$mount
函数进入模板编译与挂载阶段,如果没有传入 el 选项,则不进入下一个生命周期阶段,需要用户手动执行vm.$mount
方法才进入下一个生命周期阶段。
以上就是new Vue()
所做的所有事情,可以看到,整个初始化阶段都是在new Vue()
里完成的,关于new Vue()
里调用的一些初始化函数具体是如何进行初始化的,我们将在接下来逐一介绍。下面我们先来看看上文中遗留的属性合并及 callHook 函数是如何触发生命周期的钩子的问题。
合并属性
在上文中,_init
方法里首先会调用mergeOptions
函数来进行属性合并,如下:
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
它实际上就是把 resolveConstructorOptions(vm.constructor)
的返回值和 options
做合并,resolveConstructorOptions
的实现先不考虑,可简单理解为返回 vm.constructor.options
,相当于 Vue.options
,那么这个 Vue.options
又是什么呢,其实在 initGlobalAPI(Vue)
的时候定义了这个值,代码在 src/core/global-api/index.js 中:
export function initGlobalAPI (Vue: GlobalAPI) { // ... Vue.options = Object.create(null) ASSET_TYPES.forEach(type => { Vue.options[type + 's'] = Object.create(null) }) extend(Vue.options.components, builtInComponents) // ... }
首先通过 Vue.options = Object.create(null)
创建一个空对象,然后遍历 ASSET_TYPES
,ASSET_TYPES
的定义在 src/shared/constants.js 中:
export const ASSET_TYPES = [ 'component', 'directive', 'filter' ]
所以上面遍历 ASSET_TYPES 后的代码相当于:
Vue.options.components = {} Vue.options.directives = {} Vue.options.filters = {}
最后通过 extend(Vue.options.components, builtInComponents)
把一些内置组件扩展到 Vue.options.components
上,Vue 的内置组件目前 有<keep-alive>
、<transition>
和<transition-group>
组件,这也就是为什么我们在其它组件中使用这些组件不需要注册的原因。
那么回到 mergeOptions
这个函数,它的定义在 src/core/util/options.js
中:
/** * Merge two option objects into a new one. * Core utility used in both instantiation and inheritance. */ export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object { if (process.env.NODE_ENV !== 'production') { checkComponents(child) } if (typeof child === 'function') { child = child.options } normalizeProps(child, vm) normalizeInject(child, vm) normalizeDirectives(child) // Apply extends and mixins on the child options, // but only if it is a raw options object that isn't // the result of another mergeOptions call. // Only merged options has the _base property. if (!child._base) { if (child.extends) { parent = mergeOptions(parent, child.extends, vm) } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], 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 }
可以看出,mergeOptions 函数的 主要功能是把 parent 和 child 这两个对象根据一些合并策略,合并成一个新对象并返回。首先递归把 extends 和 mixins 合并到 parent 上,
if (!child._base) { if (child.extends) { parent = mergeOptions(parent, child.extends, vm) } if (child.mixins) { for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } } }
然后创建一个空对象options
,遍历parent
,把parent
中的每一项通过调用mergeField
函数合并到空对象 options 里,
const options = {} let key for (key in parent) { mergeField(key) }
接着再遍历 child,把存在于 child 里但又不在 parent 中 的属性继续调用 mergeField 函数合并到空对象 options 里,
for (key in child) { if (!hasOwn(parent, key)) { mergeField(key) } }
最后,options 就是最终合并后得到的结果,将其返回。
这里值得一提的是 mergeField
函数,它不是简单的把属性从一个对象里复制到另外一个对象里,而是根据被合并的不同的选项有着不同的合并策略。例如,对于 data 有 data 的合并策略,即该文件中的strats.data
函数;对于watch
有 watch 的合并策略,即该文件中的strats.watch
函数等等。这就是设计模式中非常典型的策略模式。
关于这些合并策略都很简单,这里我不一一展开介绍,仅介绍生命周期钩子函数的合并策略,因为我们后面会用到。生命周期钩子函数的合并策略如下:
/** * Hooks and props are merged as arrays. */ function mergeHook (parentVal,childVal): { return childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal } LIFECYCLE_HOOKS.forEach(hook => { strats[hook] = mergeHook })
这其中的 LIFECYCLE_HOOKS
的定义在 src/shared/constants.js
中:
export const LIFECYCLE_HOOKS = [ 'beforeCreate', 'created', 'beforeMount', 'mounted', 'beforeUpdate', 'updated', 'beforeDestroy', 'destroyed', 'activated', 'deactivated', 'errorCaptured' ]
这里定义了所有钩子函数名称,所以对于钩子函数的合并策略都是 mergeHook
函数。mergeHook
函数的实现用了一个多层嵌套的三元运算符,如果嵌套太深不好理解的话我们可以将其展开,如下:
function mergeHook (parentVal,childVal): { if (childVal) { if (parentVal) { return parentVal.concat(childVal) } else { if (Array.isArray(childVal)) { return childVal } else { return [childVal] } } } else { return parentVal } }
从展开后的代码中可以看到,它的合并策略是这样子的:如果 childVal
不存在,就返回 parentVal
;否则再判断是否存在 parentVal
,如果存在就把 childVal
添加到 parentVal
后返回新数组;否则返回 childVal
的数组。所以回到 mergeOptions
函数,一旦 parent
和 child
都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。
那么问题来了,为什么要把相同的钩子函数转换成数组呢?这是因为Vue
允许用户使用Vue.mixin
方法(关于该方法会在后面会介绍)向实例混入自定义行为,Vue
的一些插件通常都是这么做的。所以当Vue.mixin
和用户在实例化Vue
时,如果设置了同一个钩子函数,那么在触发钩子函数时,就需要同时触发这个两个函数,所以转换成数组就是为了能在同一个生命周期钩子列表中保存多个钩子函数。
callHook 函数如何触发钩子函数
关于callHook
函数如何触发钩子函数的问题,我们只需看一下该函数的实现源码即可,该函数的源码位于src/core/instance/lifecycle.js
中,如下:
export function callHook (vm: Component, hook: string) { // #7573 disable dep collection when invoking lifecycle hooks pushTarget() const handlers = vm.$options[hook] 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) } popTarget() }
可以看到,callHook
函数逻辑非常简单。首先从实例的$options
中获取到需要触发的钩子名称所对应的钩子函数数组handlers
,我们说过,每个生命周期钩子名称都对应了一个钩子函数数组。然后遍历该数组,将数组中的每个钩子函数都执行一遍。
初始阶段 initLifecycle
以上内容中,我们介绍了生命周期初始化阶段的整体工作流程,以及在该阶段都做了哪些事情。我们知道了,在该阶段会调用一些初始化函数,对Vue
实例的属性、数据等进行初始化工作。那这些初始化函数都初始化了哪些东西以及都怎么初始化的呢?接下来我们就把这些初始化函数一一展开介绍,先介绍第一个初始化函数initLifecycle
。
initLifecycle
函数的定义位于源码的src/core/instance/lifecycle.js
中,其代码如下:
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 }
可以看到,initLifecycle
函数的代码量并不多,逻辑也不复杂。其主要是给Vue
实例上挂载了一些属性并设置了默认值,值得一提的是挂载$parent
属性和$root
属性, 下面我们就来逐个分析。
首先是给实例上挂载$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
从代码中可以看到,逻辑是这样子的:如果当前组件不是抽象组件并且存在父级,那么就通过while
循环来向上循环,如果当前组件的父级是抽象组件并且也存在父级,那就继续向上查找当前组件父级的父级,直到找到第一个不是抽象类型的父级时,将其赋值vm.$parent
,同时把该实例自身添加进找到的父级的$children
属性中。这样就确保了在子组件的$parent
属性上能访问到父组件实例,在父组件的$children
属性上也能访问子组件的实例。
接着是给实例上挂载$root
属性,如下:
vm.$root = parent ? parent.$root : vm
实例的$root
属性表示当前实例的根实例,挂载该属性时,首先会判断如果当前实例存在父级,那么当前实例的根实例$root
属性就是其父级的根实例$root
属性,如果不存在,那么根实例$root
属性就是它自己。这很好理解,举个例子:假如有一个人,他如果有父亲,那么他父亲的祖先肯定也是他的祖先,同理,他的儿子的祖先也肯定是他的祖先,我们不需要真正的一层一层的向上递归查找到他祖先本人,只需要知道他父亲的祖先是谁然后告诉他即可。如果他没有父亲,那说明他自己就是祖先,那么他后面的儿子、孙子的$root
属性就是他自己了。
这就是一个自上到下将根实例的$root
属性依次传递给每一个子实例的过程。
最后,再初始化了一些其它属性,因为都是简单的赋初始值,这里就不再一一介绍,等后面内容涉及到的时候再介绍。
vm.$children = [] vm.$refs = {} vm._watcher = null vm._inactive = null vm._directInactive = false vm._isMounted = false vm._isDestroyed = false vm._isBeingDestroyed = false
初始化阶段 initEvents
从函数名字上来看,这个初始化函数是初始化实例的事件系统。我们知道,在Vue
中,当我们在父组件中使用子组件时可以给子组件上注册一些事件,这些事件即包括使用v-on
或@
注册的自定义事件,也包括注册的浏览器原生事件(需要加 .native
修饰符),如下:
<child @select="selectHandler" @click.native="clickHandler"></child>
不管是什么事件,当子组件(即实例)在初始化的时候都需要进行一定的初始化,那么接下来就来看看实例上的事件都是如何进行初始化的。
解析事件
我们先从解析事件开始说起,回顾之前的模板编译解析中,当遇到开始标签的时候,除了会解析开始标签,还会调用processAttrs
方法解析标签中的属性,processAttrs
方法位于源码的 src/compiler/parser/index.js
中, 如下:
export const onRE = /^@|^v-on:/ export const dirRE = process.env.VBIND_PROP_SHORTHAND ? /^v-|^@|^:|^\.|^#/ : /^v-|^@|^:|^#/ function processAttrs (el) { const list = el.attrsList let i, l, name, rawName, value, modifiers, syncGen, isDynamic for (i = 0, l = list.length; i < l; i++) { name = rawName = list[i].name value = list[i].value if (dirRE.test(name)) { // mark element as dynamic el.hasBindings = true // modifiers modifiers = parseModifiers(name.replace(dirRE, '')) // support .foo shorthand syntax for the .prop modifier if (process.env.VBIND_PROP_SHORTHAND && propBindRE.test(name)) { (modifiers || (modifiers = {})).prop = true name = `.` + name.slice(1).replace(modifierRE, '') } else if (modifiers) { name = name.replace(modifierRE, '') } if (bindRE.test(name)) { // v-bind name = name.replace(bindRE, '') value = parseFilters(value) isDynamic = dynamicArgRE.test(name) if (isDynamic) { name = name.slice(1, -1) } if ( process.env.NODE_ENV !== 'production' && value.trim().length === 0 ) { warn( `The value for a v-bind expression cannot be empty. Found in "v-bind:${name}"` ) } if (modifiers) { if (modifiers.prop && !isDynamic) { name = camelize(name) if (name === 'innerHtml') name = 'innerHTML' } if (modifiers.camel && !isDynamic) { name = camelize(name) } if (modifiers.sync) { syncGen = genAssignmentCode(value, `$event`) if (!isDynamic) { addHandler( el, `update:${camelize(name)}`, syncGen, null, false, warn, list[i] ) if (hyphenate(name) !== camelize(name)) { addHandler( el, `update:${hyphenate(name)}`, syncGen, null, false, warn, list[i] ) } } else { // handler w/ dynamic event name addHandler( el, `"update:"+(${name})`, syncGen, null, false, warn, list[i], true // dynamic ) } } } if ((modifiers && modifiers.prop) || ( !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name) )) { addProp(el, name, value, list[i], isDynamic) } else { addAttr(el, name, value, list[i], isDynamic) } } else if (onRE.test(name)) { // v-on name = name.replace(onRE, '') isDynamic = dynamicArgRE.test(name) if (isDynamic) { name = name.slice(1, -1) } addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic) } else { // normal directives name = name.replace(dirRE, '') // parse arg const argMatch = name.match(argRE) let arg = argMatch && argMatch[1] isDynamic = false if (arg) { name = name.slice(0, -(arg.length + 1)) if (dynamicArgRE.test(arg)) { arg = arg.slice(1, -1) isDynamic = true } } addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]) if (process.env.NODE_ENV !== 'production' && name === 'model') { checkForAliasModel(el, value) } } } else { // literal attribute if (process.env.NODE_ENV !== 'production') { const res = parseText(value, delimiters) if (res) { warn( `${name}="${value}": ` + 'Interpolation inside attributes has been removed. ' + 'Use v-bind or the colon shorthand instead. For example, ' + 'instead of <div id="{{ val }}">, use <div :id="val">.', list[i] ) } } addAttr(el, name, JSON.stringify(value), list[i]) // #6887 firefox doesn't update muted state if set via attribute // even immediately after element creation if (!el.component && name === 'muted' && platformMustUseProp(el.tag, el.attrsMap.type, name)) { addProp(el, name, 'true', list[i]) } } } }
又是一长串代码,还是一句话:“不要慌,问题不大!! 我们找关键点”
从上述代码中可以看到,在对标签属性进行解析时,判断如果属性是指令,首先通过 parseModifiers 解析出属性的修饰符,然后判断如果是事件的指令,则执行 addHandler(el, name, value, modifiers, false, warn, list[i], isDynamic)
方法, 该方法定义在 src/compiler/helpers.js
中,如下:
export function addHandler ( el: ASTElement, name: string, value: string, modifiers: ?ASTModifiers, important?: boolean, warn?: ?Function, range?: Range, dynamic?: boolean ) { modifiers = modifiers || emptyObject ... // check capture modifier 判断是否有 capture 修饰符 if (modifiers.capture) { delete modifiers.capture name = prependModifierMarker('!', name, dynamic) // 给事件名前加'!'用以标记 capture 修饰符 } // 判断是否有 once 修饰符 if (modifiers.once) { delete modifiers.once name = prependModifierMarker('~', name, dynamic)// 给事件名前加'~'用以标记 once 修饰符 } /* istanbul ignore if 判断是否有 passive 修饰符 */ if (modifiers.passive) { delete modifiers.passive name = prependModifierMarker('&', name, dynamic) // 给事件名前加'&'用以标记 passive 修饰符 } let events if (modifiers.native) { delete modifiers.native events = el.nativeEvents || (el.nativeEvents = {}) } else { events = el.events || (el.events = {}) } const newHandler: any = rangeSetItem({ value: value.trim(), dynamic }, range) if (modifiers !== emptyObject) { newHandler.modifiers = modifiers } const handlers = events[name] /* istanbul ignore if */ if (Array.isArray(handlers)) { important ? handlers.unshift(newHandler) : handlers.push(newHandler) } else if (handlers) { events[name] = important ? [newHandler, handlers] : [handlers, newHandler] } else { events[name] = newHandler } el.plain = false }
在addHandler
函数里做了 3 件事情,首先根据 modifier
修饰符对事件名 name
做处理,接着根据 modifier.native
判断事件是一个浏览器原生事件还是自定义事件,分别对应 el.nativeEvents
和 el.events
,最后按照 name
对事件做归类,并把回调函数的字符串保留到对应的事件中。
在上面的例子中,父组件的 child
节点生成的 el.events
和 el.nativeEvents
如下:
el.events = { select: { value: 'selectHandler' } } el.nativeEvents = { click: { value: 'clickHandler' } }
然后在模板编译的代码生成阶段,会在 genData
函数中根据 AST
元素节点上的 events
和 nativeEvents
生成_c(tagName,data,children)
函数中所需要的 data
数据,它的定义在 src/compiler/codegen/index.js
中:
export function genData (el state) { let data = '{' // ... if (el.events) { data += `${genHandlers(el.events, false)},` } if (el.nativeEvents) { data += `${genHandlers(el.nativeEvents, true)},` } // ... return data }
生成的 data 数据如下:
{ // ... on: {"select": selectHandler}, nativeOn: {"click": function($event) { return clickHandler($event) } } // ... }
可以看到,最开始的模板中标签上注册的事件最终会被解析成用于创建元素型VNode
的_c(tagName,data,children)
函数中data
数据中的两个对象,自定义事件对象on
,浏览器原生事件nativeOn
。
模板编译的最终目的是创建render
函数供挂载的时候调用生成虚拟DOM
,那么在挂载阶段, 如果被挂载的节点是一个组件节点,则通过 createComponent
函数创建一个组件 vnode
,该函数位于源码的 src/core/vdom/create-component.js
中, 如下:
export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array | void { // ... const listeners = data.on data.on = data.nativeOn // ... const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) return vnode }
可以看到,把 自定义事件data.on
赋值给了 listeners
,把浏览器原生事件 data.nativeOn
赋值给了 data.on
,这说明所有的原生浏览器事件处理是在当前父组件环境中处理的。而对于自定义事件,会把 listeners
作为 vnode
的 componentOptions
传入,放在子组件初始化阶段中处理, 在子组件的初始化的时候, 拿到了父组件传入的 listeners
,然后在执行 initEvents
的过程中,会处理这个 listeners
。
所以铺垫了这么多,结论来了:父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化;而浏览器原生事件是在父组件中处理。
换句话说:实例初始化阶段调用的初始化事件函数initEvents
实际上初始化的是父组件在模板中使用 v-on 或@注册的监听子组件内触发的事件。
initEvents 函数分析
了解了以上过程之后,我们终于进入了正题,开始分析initEvents
函数,该函数位于源码的src/instance/events.js
中,如下:
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) } }
可以看到,initEvents
函数逻辑非常简单,首先在vm
上新增_events
属性并将其赋值为空对象,用来存储事件。
vm._events = Object.create(null)
接着,获取父组件注册的事件赋给listeners
,如果listeners
不为空,则调用updateComponentListeners
函数,将父组件向子组件注册的事件注册到子组件的实例中,如下:
const listeners = vm.$options._parentListeners if (listeners) { updateComponentListeners(vm, listeners) }
这个updateComponentListeners
函数是什么呢?该函数定义如下:
export function updateComponentListeners ( vm: Component, listeners: Object, oldListeners: ?Object ) { target = vm updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm) target = undefined } function add (event, fn) { target.$on(event, fn) } function remove (event, fn) { target.$off(event, fn) }
可以看到,updateComponentListeners
函数其实也没有干什么,只是调用了updateListeners
函数,并把listeners
以及add
和remove
这两个函数传入。我们继续跟进,看看updateListeners
函数干了些什么,updateListeners
函数位于源码的src/vdom/helpers/update-listeners.js
中,如下:
export function updateListeners ( on: Object, oldOn: Object, add: Function, remove: Function, createOnceHandler: Function, vm: Component ) { let name, def, cur, old, event for (name in on) { def = cur = on[name] old = oldOn[name] event = normalizeEvent(name) /* istanbul ignore if */ if (__WEEX__ && isPlainObject(def)) { cur = def.handler event.params = def.params } if (isUndef(cur)) { process.env.NODE_ENV !== 'production' && warn( `Invalid handler for event "${event.name}": got ` + String(cur), vm ) } else if (isUndef(old)) { if (isUndef(cur.fns)) { cur = on[name] = createFnInvoker(cur, vm) } if (isTrue(event.once)) { cur = on[name] = createOnceHandler(event.name, cur, event.capture) } add(event.name, cur, event.capture, event.passive, event.params) } else if (cur !== old) { old.fns = cur on[name] = old } } for (name in oldOn) { if (isUndef(on[name])) { event = normalizeEvent(name) remove(event.name, oldOn[name], event.capture) } } }
可以看到,该函数的作用是对比listeners
和oldListeners
的不同,并调用参数中提供的add
和remove
进行相应的注册事件和卸载事件。其思想是:如果listeners
对象中存在某个key
(即事件名)而oldListeners
中不存在,则说明这个事件是需要新增的;反之,如果oldListeners
对象中存在某个key
(即事件名)而listeners
中不存在,则说明这个事件是需要从事件系统中卸载的;
该函数接收 6 个参数,分别是on
、oldOn
、add
、remove
、createOnceHandler
、vm
,其中on
对应listeners
,oldOn
对应oldListeners
。
首先对on
进行遍历, 获得每一个事件名,然后调用 normalizeEvent
函数(关于该函数下面会介绍)处理, 处理完事件名后, 判断事件名对应的值是否存在,如果不存在则抛出警告,如下:
for (name in on) { def = cur = on[name] old = oldOn[name] event = normalizeEvent(name) if (isUndef(cur)) { process.env.NODE_ENV !== 'production' && warn( `Invalid handler for event "${event.name}": got ` + String(cur), vm ) } }
如果存在,则继续判断该事件名在oldOn
中是否存在,如果不存在,则调用add
注册事件,如下
if (isUndef(old)) { if (isUndef(cur.fns)) { cur = on[name] = createFnInvoker(cur, vm) } add(event.name, cur, event.capture, event.passive, event.params) }
这里定义了 createFnInvoker
方法并返回invoker
函数:
export function createFnInvoker (fns: Function | Array, vm: ?Component): Function { function invoker () { const fns = invoker.fns if (Array.isArray(fns)) { const cloned = fns.slice() for (let i = 0; i < cloned.length; i++) { invokeWithErrorHandling(cloned[i], null, arguments, vm, `v-on handler`) } } else { // return handler return value for single handlers return invokeWithErrorHandling(fns, null, arguments, vm, `v-on handler`) } } invoker.fns = fns return invoker }
由于一个事件可能会对应多个回调函数,所以这里做了数组的判断,多个回调函数就依次调用。注意最后的赋值逻辑, invoker.fns = fns
,每一次执行 invoker
函数都是从 invoker.fns
里取执行的回调函数,回到 updateListeners
,当我们第二次执行该函数的时候,判断如果 cur !== old
,那么只需要更改 old.fns = cur
把之前绑定的 involer.fns
赋值为新的回调函数即可,并且 通过 on[name] = old
保留引用关系,这样就保证了事件回调只添加一次,之后仅仅去修改它的回调函数的引用。
if (cur !== old) { old.fns = cur on[name] = old }
最后遍历 oldOn
, 获得每一个事件名,判断如果事件名在on
中不存在,则表示该事件是需要从事件系统中卸载的事件,则调用 remove
方法卸载该事件。
以上就是updateListeners
函数的所有逻辑,那么上面还遗留了一个normalizeEvent
函数是干什么用的呢?还记得我们在解析事件的时候,当事件上有修饰符的时候,我们会根据不同的修饰符给事件名前面添加不同的符号以作标识,其实这个normalizeEvent
函数就是个反向操作,根据事件名前面的不同标识反向解析出该事件所带的何种修饰符,其代码如下:
const normalizeEvent = cached((name: string): { name: string, once: boolean, capture: boolean, passive: boolean, handler?: Function, params?: Array } => { const passive = name.charAt(0) === '&' name = passive ? name.slice(1) : name const once = name.charAt(0) === '~' name = once ? name.slice(1) : name const capture = name.charAt(0) === '!' name = capture ? name.slice(1) : name return { name, once, capture, passive } })
可以看到,就是判断事件名的第一个字符是何种标识进而判断出事件带有何种修饰符,最终将真实事件名及所带的修饰符返回。
小结
上面介绍了生命周期初始化阶段所调用的第二个初始化函数——initEvents
。该函数是用来初始化实例的事件系统的。
我们先从模板编译时对组件标签上的事件解析入手分析,我们知道了,父组件既可以给子组件上绑定自定义事件,也可以绑定浏览器原生事件。这两种事件有着不同的处理时机,浏览器原生事件是由父组件处理,而自定义事件是在子组件初始化的时候由父组件传给子组件,再由子组件注册到实例的事件系统中。
也就是说:初始化事件函数 initEvents 实际上初始化的是父组件在模板中使用 v-on 或@注册的监听子组件内触发的事件。
最后分析了initEvents
函数的具体实现过程,该函数内部首先在实例上新增了_events
属性并将其赋值为空对象,用来存储事件。接着通过调用updateComponentListeners
函数,将父组件向子组件注册的事件注册到子组件实例中的_events
对象里。
码云笔记 » Vue源码解读(一)