Vue源码解析:Vue生命周期之从生到死(二)

    技术2022-08-11  84

    我们知道在实例初始化的时候,会调用一些初始化函数。对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或@注册的监听子组件内触发的事件。

    未完,待续。。。。

    Processed: 0.012, SQL: 9