Vue源码解析:Vue生命周期之从生到死(二)
我们知道在实例初始化的时候,会调用一些初始化函数。对Vue的实例属性,数据等进行初始化操作。第一个初始化函数:initLifecycle。//src/core/instance/lifecycle.jsexport function initLifecycle (vm: Component) {const options = vm.$options// locate first non-abstr
我们知道在实例初始化的时候,会调用一些初始化函数。对Vue的实例属性,数据等进行初始化操作。
第一个初始化函数: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
}
代码不多,但是做了俩个比较重要的事情: 那就是挂载了$parent和$root属性。
首先是$parent的挂载:逻辑是这样的,如果当前组件不是抽象组件并且存在父级,那么就通过while
循环来向上循环,如果当前组件的父级是抽象组件并且也存在父级,那就继续向上查找当前组件父级的父级,直到找到第一个不是抽象类型的父级时,将其赋值vm.$parent
,同时把该实例自身添加进找到的父级的$children
属性中。这样就确保了在子组件的$parent
属性上能访问到父组件实例,在父组件的$children
属性上也能访问子组件的实例。
其次是$root的挂载:逻辑是这样的,首先会判断如果当前实例存在父级,那么当前实例的根实例$root
属性就是其父级的根实例$root
属性,如果不存在,那么根实例$root
属性就是它自己。
最后就是初始化一些其他的属性。
第二个初始化函数:initEvents
initEvents是vue初始化实例的事件系统。我们知道,当出现父子组件的时候,我们可以在子组件上注册v-on或@这样的自定义事件,也可以注册浏览器原生的事件,那么子组件在初始化实例的时候,是如何解析这些事件的呢?
我们知道,在vue模版解析,当解析到标签的时候,不仅仅要解析开始标签,还有一个比较重要的事情,就是调用processAttrs方法来解析属性:
// src/compiler/parser/index.js
export const onRE = /^@|^v-on:/
export const dirRE = /^v-|^@|^:/
function processAttrs (el) {
const list = el.attrsList
let i, l, name, value, modifiers
for (i = 0, l = list.length; i < l; i++) {
name = list[i].name
value = list[i].value
if (dirRE.test(name)) {
// 解析修饰符
modifiers = parseModifiers(name)
if (modifiers) {
name = name.replace(modifierRE, '')
}
if (onRE.test(name)) { // v-on
name = name.replace(onRE, '')
addHandler(el, name, value, modifiers, false, warn)
}
}
}
}
判断如果属性是指令,首先通过 parseModifiers
解析出属性的修饰符,然后判断如果是事件的指令,则执行 addHandler(el, name, value, modifiers, false, warn)
方法。
// src/compiler/helpers.js
export function addHandler (el,name,value,modifiers) {
modifiers = modifiers || emptyObject
// check capture modifier 判断是否有capture修饰符
if (modifiers.capture) {
delete modifiers.capture
name = '!' + name // 给事件名前加'!'用以标记capture修饰符
}
// 判断是否有once修饰符
if (modifiers.once) {
delete modifiers.once
name = '~' + name // 给事件名前加'~'用以标记once修饰符
}
// 判断是否有passive修饰符
if (modifiers.passive) {
delete modifiers.passive
name = '&' + name // 给事件名前加'&'用以标记passive修饰符
}
let events
if (modifiers.native) {
delete modifiers.native
events = el.nativeEvents || (el.nativeEvents = {})
} else {
events = el.events || (el.events = {})
}
const newHandler: any = {
value: value.trim()
}
if (modifiers !== emptyObject) {
newHandler.modifiers = modifiers
}
const handlers = events[name]
if (Array.isArray(handlers)) {
handlers.push(newHandler)
} else if (handlers) {
events[name] = [handlers, newHandler]
} else {
events[name] = newHandler
}
el.plain = false
}
addHandler做了三件事:
- 根据modifier修饰符对name做处理;
- 根据modifier.native判断事件是一个原生事件还是自定义事件,分别对应el.nativeEvents和el.events;
- 最后按照name归类,把回掉函数的字符串保留在对应的事件中
然后在模板编译的代码生成阶段,会在 genData
函数中根据 AST
元素节点上的 events
和 nativeEvents
生成_c(tagName,data,children)
函数中所需要的 data
数据。
export function genData (el state) {
let data = '{'
// ...
if (el.events) {
data += `${genHandlers(el.events, false,state.warn)},`
}
if (el.nativeEvents) {
data += `${genHandlers(el.nativeEvents, true, state.warn)},`
}
// ...
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<VNode> | 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函数到底干了什么?
// src/instance/events.js
export function initEvents (vm: Component) {
vm._events = Object.create(null)
// init parent attached events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
从上面的代码中可以看出,其实很简单,在vue的实例上创建了一个_events对象。然后将获取父组件的注册事件赋给listeners,如过listeners存在,那么就调用updateComponentListeners().
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, vm)
target = undefined
}
function add (event, fn, once) {
if (once) {
target.$once(event, fn)
} else {
target.$on(event, fn)
}
}
function remove (event, fn) {
target.$off(event, fn)
}
updateComponentListers调用了updatelisterners函数。
// src/vdom/helpers/update-listeners.js
export function updateListeners (
on: Object,
oldOn: Object,
add: Function,
remove: Function,
vm: Component
) {
let name, def, cur, old, event
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
)
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur)
}
add(event.name, cur, event.once, 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、旧的listeners,添加和删除函数,以及vue实例。首先对新的listeners进行遍历,获得每一个事件。调用normalizeEvent()函数处理。处理完事件名后, 判断事件名对应的值是否存在,如果不存在则抛出警告。如果存在,则继续判断该事件名在oldOn
中是否存在,如果不存在,则调用add
注册事件。这里定义了一个createFnInvoker函数。
export function createFnInvoker (fns) {
function invoker () {
const fns = invoker.fns
if (Array.isArray(fns)) {
const cloned = fns.slice()
for (let i = 0; i < cloned.length; i++) {
cloned[i].apply(null, arguments)
}
} else {
// return handler return value for single handlers
return fns.apply(null, arguments)
}
}
invoker.fns = fns
return invoker
}
由于一个事件可能会对应多个回调函数,所以这里做了数组的判断,多个回调函数就依次调用。注意最后的赋值逻辑, invoker.fns = fns
,每一次执行 invoker
函数都是从 invoker.fns
里取执行的回调函数,回到 updateListeners
,当我们第二次执行该函数的时候,判断如果 cur !== old
,那么只需要更改 old.fns = cur
把之前绑定的 involer.fns
赋值为新的回调函数即可,并且 通过 on[name] = old
保留引用关系,这样就保证了事件回调只添加一次,之后仅仅去修改它的回调函数的引用。最后遍历 oldOn
, 获得每一个事件名,判断如果事件名在on
中不存在,则表示该事件是需要从事件系统中卸载的事件,则调用 remove
方法卸载该事件。
当事件上有修饰符的时候,我们会根据不同的修饰符给事件名前面添加不同的符号以作标识,其实这个normalizeEvent
函数就是个反向操作,根据事件名前面的不同标识反向解析出该事件所带的何种修饰符。
const normalizeEvent = cached((name: string): {
name: string,
once: boolean,
capture: boolean,
passive: boolean,
handler?: Function,
params?: Array<any>
} => {
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实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件。
未完,待续。。。。
更多推荐
所有评论(0)