Half a month ago, I saw an article about eventEmitter. After reading it, I was inspired to write one myself. When I went to bed at night, IT occurred to me that I could also try to implement Emitter in VUE. And so the story began.

1. Implement an eventEmiiter

1.1. Overall structure

Let’s start with a picture. Let’s seeEventEmitterWhat functions need to be implemented

You can see that the contents of an EventEmitter class aren’t messy. According to this picture, we can be roughly divided into the following modules.

  1. Initial properties of EventEmitter

    • _events // Stores all listeners
    • _maxListeners setMaxListeners getMaxListeners
  2. AddEventListener module

    • addListener
    • prependListener
    • once
    • prependOnceListener
    • on
  3. Emit module

    • emitNone
    • emitOne emitTwo emitThree
    • emitMany
    • Error event
  4. RemoveEventListener module

    • removeListener
    • removeAllListeners
  5. listeners,eventNames

    • Listeners // Get all events under a listener
    • EventNames // Gets what listeners there are
  6. Utility functions and compatibility functions

    • spliceOne
    • arrayClone
    • objectCreatePolyfill
    • objectKeysPolyfill
    • functionBindPolyfill

Basically, in this order, you can write a basic eventEmitter class.

Recommend you can first try to write their own to write, this way wait to see the source code of the mature library can get more harvest.

Then go to the Internet to find a mature library source for comparison, indeed found some problems need to improve 😳.

Click here. Once you’re done, take a look at the source code for the EventEmitter class.

  • In order to save lines of code, we use Array to store both single events and multiple events. As a library, the dozens of lines of code saved are more important than performance
  • Without considering emit several parameters, processing of different cases can help improve performance
  • No consideration is given to limiting the maximum number of bindings a class can have. Because if the number is more than one, easy to cause memory leaks.
  • The function lacks arguments. Lack of defensive code

1.2. Briefly analyze part of the code

Specific code will not be analyzed 😂 😂, a little on the main line to explain it. Because the source code is not complex, sink heart spend half an hour can certainly all understand.

The author starts by creating a _events object that will later access our listener. It then sets the maximum event allowed by a listener to avoid the possibility of memory leaks.

function EventEmitter() { if (! this._events || ! Object.prototype.hasOwnProperty.call(this, '_events')) { this._events = objectCreate(null); this._eventsCount = 0; } this._maxListeners = this._maxListeners || undefined; }Copy the code

Then add the event. In a real scenario, we would get what listener type we need to add and the corresponding method listener in the HTML

function _addListener(target, type, listener, prepend) { var m; var events; var existing; if (typeof listener ! == 'function') throw new TypeError('"listener" argument must be a function'); events = target._events; if (! events) { events = target._events = objectCreate(null); target._eventsCount = 0; } else { if (events.newListener) { target.emit('newListener', type, listener.listener ? listener.listener : listener); events = target._events; } existing = events[type]; } if (! existing) { existing = events[type] = listener; ++target._eventsCount; } else { if (typeof existing === 'function') { existing = events[type] = prepend ? [listener, existing] : [existing, listener]; } else { if (prepend) { existing.unshift(listener); } else { existing.push(listener); } } } return target; }Copy the code

After we add the event, we call it through emit

EventEmitter.prototype.emit = function emit(type) { var er, handler, len, args, i, events; events = this._events; if (events) doError = (doError && events.error == null); else if (! doError) return false; handler = events[type]; if (! handler) return false; if (isFn) handler.call(self); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self); } } return true; };Copy the code

This is the main line of code, more details I really recommend you all look at the source. Because those 400 + lines of code really aren’t that complicated. Instead, there are a lot of details in the middle of the source can be worth savoring.

1.2.1, Why use Object.create(null)

We can see that many libraries on the web (such as Vue) use object.create (null) to create objects instead of {} to create new objects. Why is this 🤔?

Object. Create () is an API that I don’t want to introduce

We can start by printing on the Chrome consoleObject.create({})What the created object looks like:

You can see that the newly created Object inherits all the methods in the Object prototype chain.

We can look at usage againObject.create(null)Created object:

Shows No properties without any properties.

The difference is clear, we’ve got a very pure object. So the question comes, what are the benefits of such objects 🤔?

The first thing we need to know is that both var a = {} and object.create ({}) return objects that inherit from Object prototypes, and prototypes can be modified. However, if another library or developer modifs the Object prototype on a page, then you will inherit the modified prototype method, which may not be desirable.

Write an example in a CSDN web console, unexpectedly this problem occurs

If we create a clean object ourselves at the beginning of each library, we can rewrite the object’s prototype methods for reuse and inheritance without affecting or being affected by others.

1.2.2 functions that are more efficient than native Splice

I saw such a code in the source code, the author personally commented that 1.5 times faster than the original.

// About 1.5x faster than the two-arg version of Array#splice(). Function spliceOne(list, index) {for (var I = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) list[i] = list[k]; list.pop(); }Copy the code

I know Splice is slow, but the author says 1.5 times faster and I’m going to try it out for myself.

The flaw is that the longer the array, the longer it takes; The closer the subscript gets to the start, the longer it takes. So I tested it 100 times with arrays of different lengths and different indices.

Var arr = []; var arr = []; for(let i = 0; i < 50000; i++){arr.push(i)} console.time(); SpliceOne (arr,1) // arr.splice(1,1) // arr.splice(49995,1) // spliceOne(arr,49995) console.timeEnd() // If the data length is 5, subscript 1, Splice efficiency is 33% faster // For data length 500, subscript 1 splice efficiency is 75% faster // for data length 50000, subscript 1 Splice efficiency is 95% faster // for data length 5, subscript 4, SpliceOne 20% faster // If data length is 500, subscript 45, spliceOne 50% faster // If data length is 50000, subscript 49995, spliceOne 50% fasterCopy the code

Since the source code is for Node.js, we don’t know if splice has been optimized internally by the browser. The authors’ method does, in some cases, do it faster, and it’s impressive. 👍 👍 👍 🤕

1.2.3 multiple EMIT methods

Source, the author specifically for emit wrote a different approach, different number of parameters have emitNone, emitOne, emitTwp, emitThree, emitMany.

If I were to write it my way, it would be at most emitNone and emitMany. But the authors should have minimized the code for loops for greater efficiency. This is where people like me, who don’t write libraries, tend to be insensitive. The dozen or so lines of code saved after compression are less important than the performance loss.

2. Simply implement EventEmitter in Vue

After writing EventEmitter, it still felt very monotonous. Then I went to sleep thinking, can I just write this class into vUE? With the actual scene, you will know what you can do and what the problem is. Otherwise, there will be no progress without a theory.

There have been many articles on the web explaining how VUE implements bidirectional binding. In fact, bi-directional data binding is not the only thing that is implemented during HTML compilation, but also the addition of event listeners. But there are few articles online about surveillance.

2.1. Try to implement EventEmitter in VUE

In my initial thinking, I would compile the HTML to get all the attributes and figure out which attributes are bound events and which are data bound.

<template> <div id="app" :data="data" @click="test" @change="test2"> </div> </> { test(){alert(123)}, test2(){console.log(456)} } } var onRE = /^@|^v-on:/; function compile(node) { var reg = /\{\{(.*)\}\}/; If (node.nodeType === = 1){var attr = Node.attributes; for(var i = 0; i < attr.length; i ++){ console.log(attr[i]) } } } compile(window.app) </script>Copy the code

So I wrote the first piece of code, hoping to rely on the native Node method attributes to get all the attributes on the DOM element

Each attribute attr[I] is an object attr of the form @click=test. Although it behaves like a string, it is a NamedNodeMap. Don’t know how to use 😂 😂 😂

After looking for information on the Internet, I found out how he got the key and value.

var onRE = /^@/; function compile(node) { var reg = /\{\{(.*)\}\}/; If (node.nodeType === = 1){var attr = Node.attributes; for(var i = 0; i < attr.length; i ++){ if(onRE.test(attr[i].nodeName)){ var value = attr[i].nodeValue; } } } } compile(window.app)Copy the code

However, the article stated that this attribute is no longer recommended in the DOM4 provision 😢 Playdays (▔, ▔)ㄏ. Want to give up, or obediently went to see how vue source code is implemented.

2.2. Vue source code implements EventEmitter

Thinking that VUE must also compile HTML first, I went straight to the HTML-parse module in the source code.

Vue first defines a parseHTML method that passes in the HTML template to be compiled, which is our template. An attribute regular expression is then used to match all attributes in the template string, and an array attrs containing all attributes is returned.

Vue then iterates over the resulting array attrs, which is v-for? Or change? SRC and so on. When an event such as @click or V-on :click is obtained, an event listener is added using the addHandler method. We can then use emit in development.

Of course, there are many operations in vUE. For example, this property array and tag are then passed into a createASTElement function to generate an AST tree rendering into the real DOM and so on. But that’s not what we need to talk about in this article

We will then follow the vUE process to implement the binding event. First we define our HTML content.

<template> <div id="app" :data="data" @click="test" @change="test2">test contents </div> </> <script> var vue = {data(){return  {data:1} } methods: { test(){alert(123)}, test2(){console.log(456)} } } </script>Copy the code

Before we start compiling, we prepare all the re’s we need to use and create an eventEmitter class

var attribute = /^\s*([^\s"'<>\/=]+)(? :\s*(=)\s*(? :"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))? /; var ncname = '[a-zA-Z_][\\w\\-\\.]*'; const qnameCapture = `((? :${ncname}\\:)? ${ncname})` const startTagOpen = new RegExp(`^<${qnameCapture}`) var startTagClose = /^\s*(\/?) > /; var onRE = /^@|^v-on:/; var eventEmitter = new EventEmitter() const app = document.getElementById("app")Copy the code

And then we start writing our compiler function. As mentioned earlier, we pass in the template and match out all the properties step by step based on the re.

Function compiler(HTML){HTML = trim(HTML) // Because template newlines have Spaces, Let index = html.match(startTagOpen)[0].length HTML = html. subString (index) const match = {attrs: [], attrList: [] } let end, attr; // If you have multiple DOM levels, vue has loops, but testing is not that complicated. (end = html.match(startTagClose)) && (attr = html.match(attribute))) { match.attrs.push(attr) index = attr.length html =  html.substring(attr[0].length) } return match }Copy the code

Explain the compilation process. Find the HTML to compile based on the re of the opening tag. It then intercepts the rest of the string except for the start tag

So let’s go ahead and evaluate the string. Depending on the attribute regular expression, determine if the HTML tag has any attributes and, if so, extract them from the string.

Keep looping through the string until you reach the closing tag /div>. Then finish compiling and return the array.

After compiling, we have obtained all the attributes in the template, but now the stored attributes are represented as a match array, not as a developer-friendly map. So what we’re going to do is work with the array that we got.

function processAttrs(match){ let l = match.attrs.length for (var i = 0; i < l; i++) { var args = match.attrs[i]; // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778 if (args[0].indexOf('""') === -1) { if  (args[3] === '') { delete args[3]; } if (args[4] === '') { delete args[4]; } if (args[5] === '') { delete args[5]; } } var value = args[3] || args[4] || args[5] || ''; match.attrList[i] = { name: args[1], value: value }; } return match }Copy the code

In this step, we have obtained an attrList and stored the attributes as a map. We then iterate over these properties to determine which methods need to be bound and which properties we don’t need. If it is a method that needs to be bound, we add an event listener through the addHandler function.

function processHandler(match){
	let attrList = match.attrList, l = attrList.length
	let name, value
	for(let i = 0; i < l; i ++){
		name = attrList[i].name
		value = attrList[i].value
		if(onRE.test(name)){
			name = name.replace(onRE, '');
			addHandle(vue, name, value, false);
		}
	}
}

function addHandle(target, name, value, prepend){
	let handler = target.methods[value]
	eventEmitter.addListener(name, handler, prepend)

	eventEmitter.emit("click")
}
Copy the code

This is the end of the process. The next step is to initialize the compilation every time you go to the page.

function parseHTML(html){
	const match = compiler(html)
	processAttrs(match)
	processHandler(match)
}

Copy the code

If you want to try to fire an event that we previously bound, in VUE it is the child that fires to the parent. You don’t have to do parent-child components here. We can directly call emit in JS to verify

eventEmitter.emit("click")
Copy the code

Game over 😊

The end of the article, daily summary. The code for implementing the entire eventEmitter isn’t complicated, especially given the simplicity of the source code, which can be easily understood after a few minutes. I didn’t take a closer look at what the implementation looks like in VUE, but I’m guessing it’s pretty similar.

Vue takes more time to extract attributes than vue does. I did not expect to finally refer to VUE. On the way to see it again, I also understand the whole process of VUE compiling HTML, and what each process implements.

In fact, look at the source can learn a lot of things, the most direct is to know how to achieve a function. In addition? In fact, it is more than that. Things like coding habits, things like how to write defensive code, things like how to write closing code, things like that. These are the joys of looking at the source code.

After watching it, mom won’t have to worry about relying on eventEmitter for interviews anymore