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

未完,待续。。。。

Logo

前往低代码交流专区

更多推荐