At different times of patch, some module hooks, such as create and UPDATE, are implemented. The properties, styles, events, and so on associated with DOM elements are set through the hook functions of these modules.
Also through these hooks initialized binding events, defined in SRC/platforms/web/runtime/modules/events. Js
export default {
create: updateDOMListeners,
update: updateDOMListeners
}
Copy the code
As you can see, Events throws two hooks, one for create and one for UPDATE. Both hooks execute the updateDOMListeners method.
Ordinary VNode
There are three places where the create hook function is executed during patch
- through
createElm
When the DOM element is createdcreate
Hook and pass in the current VNode;cbs.create[i](emptyNode, vnode)
- Secondly, during the update process, if the new and old root elements of the component are different, the component placeholder VNode will be updated after the component renders VNode update
elm
Property, which is then executedcreate
Hook and passes in the current component placeholder VNode - through
createComponent
After creating the component render VNode, executecreate
Hook and pass in the current component VNode;
Cbs. update[I](oldVnode, vnode)
In the case of events, its create and Update hooks execute the updateDOMListeners function
function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) {
if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) {
return
}
const on = vnode.data.on || {}
const oldOn = oldVnode.data.on || {}
target = vnode.elm
normalizeEvents(on)
updateListeners(on, oldOn, add, remove, createOnceHandler, vnode.context)
target = undefined
}
Copy the code
When the CREATE hook executes updateDOMListeners, oldVnode is always empty Vnode and only the second Vnode parameter is passed
Oldvnode.data. on and vnode.data.on are null. The normalizeEvents function is then called. NormalizeEvents are mainly related to v-Model processing. The updateListeners function is then executed
UpdateListeners functions defined in SRC/core/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)
if(isUndef(cur)) { process.env.NODE_ENV ! = ='production' && warn(
`Invalid handler for event "${event.name}": got ` + String(cur),
vm
)
} else if (isUndef(old)) { // Create for the first time
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) {// Trigger on update, if cur and old are different
old.fns = cur
on[name] = old
}
}
// Iterate through oldOn and delete events in oldOn if there is no event named name
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
// The purpose here is to remove the method corresponding to name
remove(event.name, oldOn[name], event.capture)
}
}
}
Copy the code
The updateListeners function iterates through all the events in the new VNode, gets the event name and calls 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) = = ='~' // Prefixed last, checked first
name = once ? name.slice(1) : name
const capture = name.charAt(0) = = ='! '
name = capture ? name.slice(1) : name
return {
name,
once,
capture,
passive
}
})
Copy the code
The normalizeEvent function identifies the event name with the once, capture, and passive modifiers. The final return is an object assigned to the event variable, which in updateListeners has a value of
{
name: 'click'.once: false.capture: false.passive: false
}
Copy the code
Back to updateListeners, for the first creation, the createFnInvoker function is called to create a callback function if the event defined has no FNS attribute
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm)
}
Copy the code
export function createFnInvoker (fns: Function | Array<Function>, vm: ? Component) :Function {
function invoker () {}
invoker.fns = fns
return invoker
}
Copy the code
The createFnInvoker function defines an invoker function and adds an attribute FNS to the invoker function. The attribute value is the callback function that defines the event. The invoker function is returned and assigned to on[name].
When updateListeners are executed the second time, determine if cur! FNS = cur, and on[name] = old, so that the involer. FNS callback is added only once. Then only the reference to its callback function is modified.
else if(cur ! == old) {// Trigger on update, if cur and old are different
old.fns = cur
on[name] = old
}
Copy the code
Going back to updateListeners, the passed Add function is called for the first creation and the DOM is bound with an event via addEventListener.
function add (
name: string,
handler: Function,
capture: boolean,
passive: boolean
) {
if (useMicrotaskFix) {
const attachedTimestamp = currentFlushTimestamp
const original = handler
handler = original._wrapper = function (e) {
if (
e.target === e.currentTarget ||
e.timeStamp >= attachedTimestamp ||
e.timeStamp <= 0|| e.target.ownerDocument ! = =document
) {
return original.apply(this.arguments)
}
}
}
target.addEventListener(
name,
handler,
supportsPassive
? { capture, passive }
: capture
)
}
Copy the code
Change the Vue version to 2.4.0, and you will find that each click, the A and B variables will increase, and the text will not change. The reason for this is that clicking on the parent element triggers the component update, and because the update is in a microtask, it takes precedence over the event bubbling mechanism. That is, when the component is updated, the event bubbling mechanism continues, and the div tag has a click event, which triggers the click event and causes the bug above. I’ll do a Demo at the end, if you’re interested
The solution to this is to first execute a callback to the current element if e.target === E.Currenttarget holds. On the other hand, it indicates that the current phase is the event bubble phase, and determine e.tidstamp >= attachedTimestamp (the time from when e.tidstamp opens the page to perform the event callback). If this is true, it indicates that no component was updated during this period, so the callback continues
AttachedTimestamp is a timestamp that is obtained when a component update is triggered. If the component is updated attachedTimestamp must be greater than the E.tamstamp.
After binding the event, the following logic is executed during the update phase
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name)
// The purpose here is to remove the method corresponding to name
remove(event.name, oldOn[name], event.capture)
}
}
Copy the code
All events of oldVnode are traversed. If the current event name is not included in the new event, the previously bound event is deleted by using remove
function remove (
name: string,
handler: Function, capture: boolean, _target? : HTMLElement) {
(_target || target).removeEventListener(
name,
handler._wrapper || handler,
capture
)
}
Copy the code
In updateListeners, if the. Once modifier is set, the createOnceHandler function passed in is executed
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture)
}
Copy the code
CreateOnceHandler returns a function. Inside the function is the callback defined by the call. If the return value is not NULL, the listener is unlistened
function createOnceHandler (event, handler, capture) {
const _target = target
return function onceHandler () {
const res = handler.apply(null.arguments)
if(res ! = =null) {
remove(event, onceHandler, capture, _target)
}
}
}
Copy the code
Trigger execution
In the case of a click, when the user clicks, a callback is triggered to execute the Invoker 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`)}}Copy the code
The invoker function first gets the FNS property and performs the defined callback through invokeWithErrorHandling
summary
The general process is as follows
In the patch process, create and Update hook functions will be triggered to iterate over all events and obtain event names and modifiers used. If this is the first time, an invoker method is created and a property FNS is bound to the method to store the defined callback function. The event is then bound through addEventListener. If it is an update process and the event name corresponds to a different callback function than the previous one, the value of the binding property FNS is changed. And when the callback is triggered, it gets the defined callback function from the FNS attribute; The advantage of this is that you don’t need to bind twice, you only need to bind once. Finally, event listening is cancelled if there is no corresponding event name in the new event.
Component placeholder VNode
Next look at the component placeholder VNode events; There are two types of events: native events and custom events
Native events
When creating a component VNode through createComponent, there is this piece of logic
export function createComponent (
Ctor: Class<Component> | Function | Object | void, data: ? VNodeData, context: Component, children: ?Array<VNode>, tag? : string) :VNode | Array<VNode> | void {
data = data || {}
// Get the custom event
const listeners = data.on
// Assign nativeOn to data.on
data.on = data.nativeOn
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? ` -${name}` : ' '}`,
data, undefined.undefined.undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
Copy the code
Add native events to the data on the, and custom events added to the component VNode componentOptions. Listeners; The next step is to enter the patch process, and the component VNode will execute createComponent to create the component instance. After the instance is created, modify the ELM property of the component VNode and execute all hook functions in CBs. create, and the rest of the logic is the same as the ordinary DOM event binding process
Custom event
The above code, custom events added to the component VNode componentOptions. Listeners; How do the create and update processes handle custom events, respectively
create
In the patch process, if VNode is a component VNode, the createComponent function is called. In the createComponent function, the init hook function is called. And init hook function invoked createComponentInstanceForVnode to create the component instances;
export function createComponentInstanceForVnode (
vnode: any, // we know it's MountedComponentVNode but flow doesn't
parent: any, // activeInstance in lifecycle state
) :Component {
// Add the _isComponent, _parentVnode, and parent properties to the options of the vUE instance
const options: InternalComponentOptions = {
_isComponent: true._parentVnode: vnode,
parent
}
return new vnode.componentOptions.Ctor(options)
}
Copy the code
The initEvents function is called during the creation of the instance
export function initEvents (vm: Component) {
vm._events = Object.create(null)
vm._hasHookEvent = false
// Get custom events
const listeners = vm.$options._parentListeners
if (listeners) {
updateComponentListeners(vm, listeners)
}
}
Copy the code
If there are custom events in the initEvents function, the updateComponentListeners function is called
update
During patch, if VNode is a component VNode, the prepatch hook function is called
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
const options = vnode.componentOptions
const child = vnode.componentInstance = oldVnode.componentInstance
updateChildComponent(
child,
options.propsData, // Updated props specifies the latest props of the child component
options.listeners, // Updated Listeners Custom events
vnode, // new parent vnode
options.children // new children)},Copy the code
UpdateChildComponent is called within the Prepatch hook function to update the custom event
export function updateChildComponent (
vm: Component, // Subcomponent instance
propsData: ?Object,
listeners: ?Object,
parentVnode: MountedComponentVNode, / / component vnode
renderChildren: ?Array<VNode>
) {
// ...
// update listeners
listeners = listeners || emptyObject
// Get the custom event from the last binding
const oldListeners = vm.$options._parentListeners
// Assign this custom event to _parentListeners
vm.$options._parentListeners = listeners
updateComponentListeners(vm, listeners, oldListeners)
// ...
}
Copy the code
The custom event from the last binding is fetched and then assigned to the _parentListeners. Call updateComponentListeners and pass in old and new custom events
updateComponentListeners
export function updateComponentListeners (
vm: Component,
listeners: Object,
oldListeners: ?Object
) {
target = vm
updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
target = undefined
}
Copy the code
UpdateComponentListeners call updateListeners; As mentioned above, when first created, an invoker method is created and an attribute FNS is added to the method to hold the defined callbacks. Then you call the passed add function.
The add function for custom events is to call the vue.prototype. $on method
Vue.prototype.$on = function (event: string | Array<string>, fn: Function) :Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true}}return vm
}
Copy the code
$on(event, fn), vm._events[event].push(fn); $on(event, fn), vm._events[event].push(fn); Note that vm._events[event] is an array
Go back to updateListeners and change the FNS attribute value if it is an update process and the old and new callbacks are different. Finally, all custom events for oldVnode are traversed, and if the same event name does not exist in the new VNode, the event is deleted using vm.$off(event, fn)
Vue.prototype.$off = function (event? : string |Array<string>, fn? :Function) :Component {
const vm: Component = this
// all
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
if(! cbs) {return vm
}
if(! fn) { vm._events[event] =null
return vm
}
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
// cb.fn === fn: see the implementation of $once
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break}}return vm
}
Copy the code
The vue.prototype. $off process is as follows
- If there is no parameter, clear it
vm._events
And return the instance - If the incoming
event
It’s an array. It calls each element of the arrayVue.prototype.$off
And return the instance - If the incoming
event
It’s a string, according toevent
Gets the callback function- If the callback function is not empty and is not passed
fn
Parameter, willevent
从vm._events
Clear,vm._events[event] = null
And return the instance - If it’s passed in
fn
Parameter, iterates over the callback function array, if passedfn
Parameter and callback function arrayfn
Property, the element is removed from the array
- If the callback function is not empty and is not passed
The other logic in updateListeners is that for custom events with the.once modifier, the vue.prototype. $once method is called
Vue.prototype.$once = function (event: string, fn: Function) :Component {
const vm: Component = this
// Wrap a layer to remove the corresponding method after execution
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
Copy the code
The vue.prototype. $once method wraps the FN in a layer. When the callback is triggered, the current FN is removed from vm._events and then executed.
The trigger
This is triggered by the this.$emit method
Vue.prototype.$emit = function (event: string) :Component {
const vm: Component = this
if(process.env.NODE_ENV ! = ='production') {
const lowerCaseEvent = event.toLowerCase()
if(lowerCaseEvent ! == event && vm._events[lowerCaseEvent]) { tip() } }let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments.1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
Copy the code
The vue.prototype. $emit method retrieves the corresponding callback from vm._events based on the event passed in and executes the callback
summary
Component VNode has two types of events: native events with. Native modifier and custom events
For native events: When creating a component VNode, add events with native modifiers to data.on; Next comes the patch process
- In the create phase, the Vue instance of the component VNode is created and the component is mounted. Perform the following operations after the mounting is complete
create
Hook function;create
The hook function gets the component root element (elm
), which will then create oneinvoker
Method and add a property value to the method to hold the defined callback function; At last,addEventListener
rightThe root element of the componentAdding an Event Listener - The update process is the call
update
The hook function, if there is no old event, creates oneinvoker
Method, and modify if the callback function for the old and new events is differentinvoker
methodsfns
Properties; Finally, cancel listening for events that are not in the new node.
For custom events:
- Create phase: Create for custom events when creating a component Vue instance
invoker
Method and set the methodfns
Properties; Collect these custom events tovm._events
; When callingthis.$emit
, fromvm._events
To get the corresponding callback function and trigger. - Update process: The custom events of the new VNode are traversed. If the old VNode does not have the same event name, the event name is created
invoker
Method and set the methodfns
Properties, whichinvoker
Methods byVue.prototype.$on
Added to thevm._events
; If there is a custom event with the same event name in the old VNode and the new callback is different, this is changedinvoker
methodsfns
Property and willinvoker
Method is assigned to a new event. And then finally throughVue.protorype.$off
Removes event names that are not in the new custom eventvm._events
Remove the
Demo
<! DOCTYPEhtml>
<html lang="en">
<head>
<title>Document</title>
<style>
.i {
width: 100px;
height: 100px;
background-color: #f00;
}
</style>
</head>
<body>
<div id="div1">
<div class="i">asdfasdfsadf</div>
</div>
<script>
const oDiv = document.querySelector('#div1')
const oI = document.querySelector('.i')
oDiv.addEventListener('click'.() = > {
console.log('I clicked div')
})
oI.addEventListener('click'.() = > {
console.log('Clicked on I'.)
new Promise((resolve) = > {
resolve()
}).then(() = > {
console.log('Microtask triggering')})})</script>
</body>
</html>
Copy the code