vue2.0 source code analysis, initevent (component event initialization)

Using events in vue

// Example 1: 
// parent receive
this.$on('test', function (msg) {
  console.log(msg)  // hi
})

// children outgoing
this.$emit('test', 'hi')

// Example 2: 
// The parent component listens to the outgoing events of the child component
<my-component v-hook:created="dosomething"></my-component>

// Example 3:
// Destroy listening after a listening callback
<my-component v-on:click.once="dosomething"></my-component>

The above code is the standard event listening of vue, which is generally used for parent-child component communication.
In the vue source code, how to realize this function?

During vue initialization, events are initialized after the initialization lifecycle.
All information about component events will be collected and recorded here

Note: this chapter does not analyze the binding of dom events. The source code is in the render process for dom events, so it will be explained later in render

initialization

First, let's look at the initialization method

/*Initialization event*/
export function initEvents(vm: Component) {
	/*Create one on vm_ Events object, which is used to store events.*/
	vm._events = Object.create(null);
	/*This bool flag bit indicates whether there is a hook, without using the hash table method to find whether there is a hook. This can reduce unnecessary overhead and optimize performance.*/
	// For example: v-hook:created="dosomething"
	vm._hasHookEvent = false;
	// init parent attached events
	/*Event that initializes the parent component attach*/
	const listeners = vm.$options._parentListeners;
	if (listeners) {
		updateComponentListeners(vm, listeners);
	}
}

Object.create(null): create a prototype pointing to a null object. Compared with new object, it has the advantage of {} that the created object prototype does not have various attribute methods attached to the object, which can reduce side effects, MDN Object.create()

parentListeners: this variable will be added in options before the parent component initializes the child component to represent the listening events of the parent component

Add listening and cancel listening for events

In events JS, add and remove functions are defined to add and cancel listening, and these two functions are just simple calls to use $on, $off and $once of the prototype method to achieve the effect of listening

let target: Component;

/*When there is once, register a method that will only trigger once. When there is no once, register an event method*/
function add(event, fn, once) {
	if (once) {
		target.$once(event, fn);
	} else {
		target.$on(event, fn);
	}
}

/*Destroy an event method*/
function remove(event, fn) {
	target.$off(event, fn);
}

/*Update listening events of components*/
export function updateComponentListeners(vm: Component, listeners: Object, oldListeners: ?Object) {
	target = vm;
	// listeners parent on event
	// oldListeners is an old On event, which is null during initialization
	updateListeners(listeners, oldListeners || {}, add, remove, vm);
}

$on, $off, $emit, $once prototype method

In events In the. JS file, four methods of $on, $off, $emit and $once are mounted in the prototype of vue. The above methods of adding and canceling listening are running three of these four functions to add and cancel listening.

The codes of the following methods are relatively simple. There are comments in them, so they will not be mentioned separately.

methodRole in vue
$onListen for events of subcomponents
$onceListen to the events of sub components, trigger once and cancel listening
$offCancel listening event
$emitNotify the parent of listening events
/*Method of adding operation events to Vue prototype*/
export function eventsMixin(Vue: Class<Component>) {
	// If the parent component listens to the life cycle event of the child component like this: v-hook:created
	const hookRE = /^hook:/;

	/*Binding event methods on vm instances*/
	Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
		const vm: Component = this;

		/*If it is an array, recurse $on to bind methods to each member*/
		if (Array.isArray(event)) {
			for (let i = 0, l = event.length; i < l; i++) {
				this.$on(event[i], fn);
			}
		} else {
			// Add the listening callback to the corresponding event array
			(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
			/*Here, when registering events, the bool value, that is, a flag bit, is marked to indicate the existence of a hook, and there is no need to find out whether there is a hook through the hash table method. This can reduce unnecessary overhead and optimize performance.*/
			if (hookRE.test(event)) {
				vm._hasHookEvent = true;
			}
		}
		return vm;
	};

	/*Register an event method that executes only once*/
	// $once is actually an encapsulation of the \ $on method. It generates an on method and cancels listening after it is executed once.
	Vue.prototype.$once = function (event: string, fn: Function): Component {
		const vm: Component = this;
		function on() {
			/*Destroy the event at the first execution*/
			vm.$off(event, on);
			/*Method of performing registration*/
			fn.apply(vm, arguments);
		}
		on.fn = fn;
		vm.$on(event, on);
		return vm;
	};

	/*Log off an event. If no parameters are passed, all events will be logged off. If only the event name is passed, all methods under the event will be logged off*/
	Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
		const vm: Component = this;
		// all
		/*If no parameters are passed, all events will be logged off*/
		if (!arguments.length) {
			vm._events = Object.create(null);
			return vm;
		}
		// array of events
		/*If event is an array, the event is logged out recursively*/
		if (Array.isArray(event)) {
			for (let i = 0, l = event.length; i < l; i++) {
				// Recursively call this$ off
				this.$off(event[i], fn);
			}
			return vm;
		}
		// specific event
		const cbs = vm._events[event];
		/*If the event itself does not exist, it is returned directly*/
		if (!cbs) {
			return vm;
		}
		/*If only the event parameter is passed, all methods under the event method will be unregistered*/
		if (arguments.length === 1) {
			vm._events[event] = null;
			return vm;
		}
		// specific handler
		/*Traverse to find the corresponding method and delete it*/
		let cb;
		let i = cbs.length;
		while (i--) {
			cb = cbs[i];

			// If found, delete the corresponding function
			if (cb === fn || cb.fn === fn) {
				cbs.splice(i, 1);
				break;
			}

		}
		return vm;
	};

	/*Trigger an event 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(`Event "${lowerCaseEvent}" is emitted in component ` + `${formatComponentName(vm)} but the handler is registered for "${event}". ` + `Note that HTML attributes are case-insensitive and you cannot use ` + `v-on to listen to camelCase events when using in-DOM templates. ` + `You should probably use "${hyphenate(event)}" instead of "${event}".`);
		// 	}
		// }
		
		// _ events stores the listening of the parent component
		let cbs = vm._events[event];
		if (cbs) {
			/*Converts an object of a class array to an array*/
			cbs = cbs.length > 1 ? toArray(cbs) : cbs;
			const args = toArray(arguments, 1);
			/*Traversal execution*/
			for (let i = 0, l = cbs.length; i < l; i++) {
				cbs[i].apply(vm, args);
			}
		}
		return vm;
	};
}

Core updateListeners function

Call updateListeners in the updateComponentListeners function and import the add and remove methods.
Generally speaking, this function is used to compare old and new listeners, add new listener events and remove old listeners. If the old value (oldOn) is not available during initialization.
oldOn will pass in the value update comparison during the initialization and render of the life cycle. It will not be expanded here. Only the initialization process is discussed.

Here's a note: the value in on[name] is actually a function, the invoker function returned in the createFnInvoker method, and our bound events are saved in the invoker FNS.

Isundefe: judge whether the value is undefined or Null

/*Returns a function that executes the fns at the time of generation. If fns is an array, it is convenient to execute each of its items*/
export function createFnInvoker(fns: Function | Array<Function>): Function {
	function invoker() {
		const fns = invoker.fns;
		if (Array.isArray(fns)) {
			for (let i = 0; i < fns.length; i++) {
				fns[i].apply(null, arguments);
			}
		} else {
			// return handler return value for single handlers
			return fns.apply(null, arguments);
		}
	}
	invoker.fns = fns;
	return invoker;
}


/*Update listening events*/
export function updateListeners(on: Object, oldOn: Object, add: Function, remove: Function, vm: Component) {
	let name, cur, old, event;
	/*Traverse all methods of the new event*/
	for (name in on) {
		cur = on[name];
		old = oldOn[name];

		/*Get and remove the ~,!, and& Equal prefix*/
		event = normalizeEvent(name);
		/*isUndef Used to judge that the incoming object is not equal to undefined or null*/
		if (isUndef(cur)) {
			// Development warning code, negligible
			// process.env.NODE_ENV !== 'production' && warn(
			//   `Invalid handler for event "${event.name}": got ` + String(cur),
			//   vm
			// )
		} else if (isUndef(old)) {
			// Initialization because oldOn has nothing, go here
			if (isUndef(cur.fns)) {
				/*createFnInvoker Returns a function, which is used to execute fns during generation. If fns is an array, it is convenient to execute each of its items*/
				cur = on[name] = createFnInvoker(cur);
			}
			add(event.name, cur, event.once, event.capture, event.passive);
		} else if (cur !== old) {
			old.fns = cur;
			on[name] = old;
		}
	}
	
	/*Remove all old events*/
	for (name in oldOn) {
		if (isUndef(on[name])) {
			event = normalizeEvent(name);
			remove(event.name, oldOn[name], event.capture);
		}
	}
}


Supplement on normalizeEvent

In the updateListeners method, normalizeEvent is used to remove the event ~,, and. And return four attribute objects: name, once, capture and passive.

Here we take a look at the code of the auxiliary function file

In the event processing of the source code, it actually adopts ~& These three represent some meanings

Symbolmeaning
~Once, run once to cancel listening
&passive mode, does not block default events
!In capture mode, bubbling takes precedence

Therefore, the function of normalizeEvent is to return the real time name and the flags of these events.

In normalizeEvent, the cached method is called first. In fact, the cached function is an application of a high-order function. It returns a function, saves the incoming parameters and the results when the function is called for the first time, and can directly return the results when the function is called for the second time with the same parameters without running the function, which can save performance. This can also be done in details, You Daniu beer!

/**
 * Create a cached version of a pure function.
 */
 /*The result of fn(str) is obtained from str, but the result will be cached by the cache in the closure. The next time if it is the same str, it does not need to be recalculated by fn(str), but directly obtain the result*/
export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null)
  return (function cachedFn (str: string) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }: any)
}

const normalizeEvent = cached(
	(
		name: string
	): {
		name: string,
		once: boolean,
		capture: boolean,
		passive: boolean,
	} => {
		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,
		};
	}
);

The above is the process code related to initEvent. I have written my questions about the source code on it. If I don't understand other questions, I can leave a message or private letter, and I will add them to my blog.

Keywords: Javascript Vue Vue.js

Added by JCBarry on Sun, 23 Jan 2022 16:12:31 +0200