preface

Learn from zepto.js how to manually trigger the start of a DOM event 😀😀😀

The front end has become so popular in recent years, with vue, React and Angular frameworks emerging all over the place. If we don’t know two-way data binding and what the virtual DOM is, we might be despised. Behind the fervor is often endless fickleness. Learning these advanced and popular class libraries or frameworks can help us go faster, but settling down to return to the basics and laying a solid foundation can help us go more steadily and farther.

Recently has been looking at zepto source code, hope to learn it to master some framework design skills, but also for a long time no longer pick up JS base to review and consolidate again. If you are interested in this series, please click on watch and follow the latest developments. This article mainly wants to talk about the implementation principle of adding event on and removing event off in zepto’s event module (event.js), and the details involved will be explained in detail in the middle.

If you want to see the full translated version of Event.js, please click here

The original address

The warehouse address

Said in the previous

In the era of Vue and React, and slash-and-burn, when Even Angular didn’t touch it, jQuery or Zepto were our tools and blades, allowing us to easily develop beautiful and compatible web pages. We admired and admired the convenience brought by the author, immersed in it and unable to pull ourselves out of it.

But with Zepto for so long you know how to code like this


$('.list').on('click'.'li'.function (e) {
  console.log($(this).html())
})Copy the code

How is event delegation implemented? Why is “this” li in your dot?

Normally we might write it like this.


$('.list li').bind('click'.function () $({})'.list').delegate('li'.'click'.function () $({})'.list li').live('click'.function () $({})'.list li').click(function () {})Copy the code

It’s a little bit too many ways to write it, maybe you have another way to write it, so

on

bind

delegate

live

click()

What are the differences between the forms of these added events, and how do they relate to each other?

I believe you have encountered similar questions in the interview process (read this article, you can find the answer 😯)?

Next we explore the principle of its internal implementation step by step from the perspective of the source code.

Everything fromonstart

The reason why I chose to start with the way on adds events is that almost all other writing methods are derived from ON, and if you understand how ON is implemented, the rest is pretty much the same.

I drew a picture I’d been working on for a long time

Zepto (zepto, zepto, zepto, zepto, zepto, zepto, zepto, zepto)

$.fn.on = function (event, selector, data, callback, one) {
  / / the first paragraph
  var autoRemove, delegator, $this = this
  if(event && ! isString(event)) { $.each(event,function (type, fn) {$this.on(type, selector, data, fn, one)
    })
    return $this
  }

  / / the second paragraph
  if(! isString(selector) && ! isFunction(callback) && callback ! = =false)
    callback = data, data = selector, selector = undefined
  if (callback === undefined || data === false)
    callback = data, data = undefined

  if (callback === false) callback = returnFalse

  // The above is for different call forms, do good parameter handling

  / / the third paragraph
  return $this.each(function (_, element) {
    // Handle the case where the event takes effect only once
    if (one) autoRemove = function (e) {
      remove(element, e.type, callback)
      return callback.apply(this.arguments)}// Add event delegate handlers

    if (selector) delegator = function (e) {
      var evt, match = $(e.target).closest(selector, element).get(0)
      if(match && match ! == element) { evt = $.extend(createProxy(e), {currentTarget: match, liveFired: element })
        return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments.1)))}}// Use the add inner function to actually register events for the selected element

    add(element, event, callback, data, selector, delegator || autoRemove)
  })
}Copy the code

It’s not easy to look at this big pile of code directly, so let’s read it in sections.

The first paragraph

var autoRemove, delegator, $this = this
  if(event && ! isString(event)) { $.each(event,function (type, fn) {$this.on(type, selector, data, fn, one)
    })
    return $this
  }Copy the code

The main purpose of this code is to handle the following form of call.

$('.list li').on({
  click: function () {
    console.log($(this).html())
  },
  mouseover: function () {$(this).css('backgroundColor'.'red')},mouseout: function () {$(this).css('backgroundColor'.'green')}})Copy the code

This is a notation that we usually write a little bit less, but it is supported. Zepto, on the other hand, loops through the ON method with key as the event name and val as the event handler.

Before we start the second code reading, let’s review some of the common ways to register events using ON

// This is probably the one we use most
on(type, function(e){... })// You can pre-add data, and then use e.data in the callback function to use the added data
on(type, data, function(e){... })// Event proxy form
on(type, [selector], function(e){... })// Of course, the event proxy form can also be pre-added data
on(type, [selector], data, function(e){... })// It is also possible to make events only work once

on(type, [selector], data, function (e) {... },true)Copy the code

There are other ways to write it, but the common ones are probably these, and the second piece of code handles the parameters so that subsequent events are added correctly.

The second paragraph

// Selector is not a string, and callback is not a function
if(! isString(selector) && ! isFunction(callback) && callback ! = =false)
    callback = data, data = selector, selector = undefined
    // Data is not passed or functions are passed
  if (callback === undefined || data === false)
    callback = data, data = undefined
    // Callback can pass false and convert it to returnFalse
  if (callback === false) callback = returnFalseCopy the code

The three if statements handle parameter handling well for a variety of uses. While you may not be able to see how this works directly, try plugging in each use to see how compatible it is.

Let’s move on to the third paragraph

This function does two very important things

  1. Handles scenarios where one is passed true and the event fires only once
  2. The selector is passed in for event proxy handler development

Let’s see how it works piece by piece.

if (one) autoRemove = function (e) {
  remove(element, e.type, callback)
  return callback.apply(this.arguments)}Copy the code

The internal use of a remove function, not parse, as long as you know that it is the function to remove the event, when the event is removed, and then execute the callback function passed in. To achieve the effect of only one call.

So how is event broker implemented?

If you think about how you normally write event brokers, you usually take advantage of the nature of event bubbling (or, of course, event capturing) by delegating events from child elements to ancestor elements, which not only makes events more dynamic, but also reduces the total number of events and improves performance.

For example

We delegate events that would have been added to Li to the parent element UL.

<ul class="list">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>Copy the code
let $list = document.querySelector('.list')

$list.addEventListener('click'.function (e) {
  e = e || window.event
  let target = e.target || e.srcElement
  if (target.tagName.toLowerCase() === 'li') {
    target.style.background = 'red'}},false)Copy the code

Click to see the effect

Back to paragraph 3

 if (selector) delegator = function (e) {
    According to the closest function, the closest element matches the selector criterion first
    var evt, match = $(e.target).closest(selector, element).get(0)
    // The closest matching selector found cannot be an element
    if(match && match ! == element) {// Then extend the match node and element node to the event object
      evt = $.extend(createProxy(e), { currentTarget: match, liveFired: element })
      // Finally, the callback function is executed
      return (autoRemove || callback).apply(match, [evt].concat(slice.call(arguments.1)))}}Copy the code

The basic principles for implementing event broker in Zepto are: Start with the current target element e.target and look up to the first element that matches the selector rule, then extend the event object, add some attributes, and finally use the found match element as the internal this scope of the callback, and pass the extended event object as the first argument to the callback.

Closest thing to know. For details on how to use the API, please click here if you are not familiar with it

Speaking of which, events have not been added yet! Where do you add it? The last line of the on function is to go into the event add.


add(element, event, callback, data, selector, delegator || autoRemove)Copy the code

Once the parameters are processed, it’s time to actually add events to the element

Inside Zepto, the real place to add events to elements is in the add function.

function add(element, events, fn, data, selector, delegator, capture) {
  var id = zid(element), 
      set = (handlers[id] || (handlers[id] = []))

  events.split(/\s/).forEach(function (event) {
    if (event == 'ready') return $(document).ready(fn)
    var handler = parse(event)
    handler.fn = fn
    handler.sel = selector
    // emulate mouseenter, mouseleave
    if (handler.e in hover) fn = function (e) {
      var related = e.relatedTarget
      if(! related || (related ! = =this && !$.contains(this, related)))
        return handler.fn.apply(this.arguments)
    }

    handler.del = delegator
    var callback = delegator || fn
    handler.proxy = function (e) {
      e = compatible(e)
      if (e.isImmediatePropagationStopped()) return
      e.data = data
      var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
      if (result === false) e.preventDefault(), e.stopPropagation()
      return result
    }

    handler.i = set.length
    set.push(handler)

    if ('addEventListener' in element)
      element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
  })
}Copy the code

My god, is so long long long a big tuo, people hard not open, looking at the heart tired ah ah ah!! But don’t worry, as long as step by step to see, the final affirmation can understand.

There’s a sentence at the beginning

var id = zid(element)Copy the code

 function zid(element) {
    return element._zid || (element._zid = _zid++)
  }Copy the code

Zepto gives the element that adds the event a unique identifier, _zID, which starts at 1 and increments upwards. Subsequent event removal functions are associated with the element based on this ID.

// Code initial place definition
var handlers = {}, 


set = (handlers[id] || (handlers[id] = []))Copy the code

Handlers are the event buffers made up of the digits 0, 1, 2, 3… An event handler that holds each element. Let’s see what the handlers look like.

html

<ul class="list">
  <li>1</li>
  <li>2</li>
  <li>3</li>
</ul>Copy the code

javascript

$('.list').on('click'.'li'.' '.function (e) {
  console.log(e)
}, true)Copy the code

Handlers, which are themselves objects, each key(1, 2, 3…) (This key also corresponds to the _zID attribute on the element) holds an array, and each item in the array holds an object associated with the event type. So let’s see what each array of keys looks like

[{e: 'click'.// Event name
    fn: function () {}, // The callback function passed in by the user
    i: 0.// The index of the object in the array
    ns: 'qianlongo'.// Namespace
    proxy: function () {}, // The event handler that actually binds the DOM to the event is del or fn
    sel: '.qianlongo'.// The selector passed in when the event is brokered
    del: function () {} // The event proxy function
  },
  {
    e: 'mouseover'.// Event name
    fn: function () {}, // The callback function passed in by the user
    i: 1.// The index of the object in the array
    ns: 'qianlongo'.// Namespace
    proxy: function () {}, // The event handler that actually binds the DOM to the event is del or fn
    sel: '.qianlongo'.// The selector passed in when the event is brokered
    del: function () {} // The event proxy function}]Copy the code

This setting facilitates the removal of subsequent events. Draw a simple graph to see the mapping between the events added to the element and the Handlers.

Now that we understand the mapping between them, let’s go back to the source code and continue.

events.split(/\s/).forEach(function (event) {
  // xxx
})Copy the code

Removing some of the internal code logic for a moment, we see that it shards events and adds them in a loop, which is why we add events like the one below

$('li').on('click mouseover mouseout'.function () {})Copy the code

The next step is to focus on the inner details of the loop. Partial comments have been added

// If the forEach event is ready, call the ready method directly.
if (event == 'ready') return $(document).ready(fn)
// Get 'click.qianlongo' => {e: 'click', ns: 'qianlongo'}
var handler = parse(event)
// Mount the user-input callback function to handler
handler.fn = fn
// Mount the selector passed in by the user to handler (event broker is useful)
handler.sel = selector
// Simulate the mouseEnter and mouseleave events with mouseover and mouseout, respectively
/ / https://qianlongo.github.io/zepto-analysis/example/event/mouseEnter-mouseOver.html (mouseenter and mouseover why so entanglements?)
// emulate mouseenter, mouseleave
if (handler.e in hover) fn = function (e) {
  var related = e.relatedTarget
  if(! related || (related ! = =this && !$.contains(this, related)))
    return handler.fn.apply(this.arguments)
}
handler.del = delegator
// Note that event handlers are preferred for callbacks that require event handlers (after a layer of processing) and user input
var callback = delegator || fn
// Proxy is the actual bound event handler
// And overwrites the event object
// Add some methods and properties, and finally call the callback passed in by the user. If the callback returns false, it is considered necessary to prevent the default behavior and prevent bubbling
handler.proxy = function (e) {
  e = compatible(e)
  if (e.isImmediatePropagationStopped()) return
  e.data = data
  var result = callback.apply(element, e._args == undefined ? [e] : [e].concat(e._args))
  // If the callback returns false, it prevents bubbling and prevents browser default behavior
  if (result === false) e.preventDefault(), e.stopPropagation()
  return result
}
// Assign the index of this added handler in set to I
handler.i = set.length
// Save the handler, noting that multiple event handlers can be added to an element for the same event
set.push(handler)
// Last, of course, is the bind event
if ('addEventListener' in element)
  element.addEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))Copy the code

This is the end of the add event. Let’s go back to the question at the beginning of the article,

on

bind

delegate

live

click()

What are the differences between the forms of these added events, and how do they relate to each other? In fact, see their source code probably know the difference

// Bind events
$.fn.bind = function (event, data, callback) {
  return this.on(event, data, callback)
}

// a small range of bubble binding events
$.fn.delegate = function (selector, event, callback) {
  return this.on(event, selector, callback)
}

// Delegate the event bubble to the body
$.fn.live = function (event, callback) {$(document.body).delegate(this.selector, event, callback)
  return this
}

// Bind and trigger events in the mail mode
$('li').click(() => {})

; ('focusin focusout focus blur load resize scroll unload click dblclick ' +
  'mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave ' +
  'change select keydown keypress keyup error').split(' ').forEach(function (event) {
    $.fn[event] = function (callback) {
      return (0 in arguments)?// Inside the click() call, bind is still used
        this.bind(event, callback) :
        this.trigger(event)
    }
  })Copy the code

The bind and click() functions bind events directly to the element, live to the body element, and a delegate to the body element. On is the best function because of live.

A concrete implementation of event removal

The implementation of event removal depends on the implementation of event binding, which associates the actual registered event information with the DOM and puts it in the Handlers. How does this work? Let’s take it step by step.

Again, a rough flow chart for event removal

Off function

 $.fn.off = function (event, selector, callback) {
  var $this = this
  // {click: clickFn, mouseover: mouseoverFn}
  // The object is passed in, and the loop loops over the call itself
  if(event && ! isString(event)) { $.each(event,function (type, fn) {$this.off(type, selector, fn)
    })
    return $this
  }
  // ('click', fn)
  if(! isString(selector) && ! isFunction(callback) && callback ! = =false)
    callback = selector, selector = undefined

  if (callback === false) callback = returnFalse
  // Remove events bound to the element
  return $this.each(function () {
    remove(this, event, callback, selector)
  })
}Copy the code

Off function and on function is basically the same routine, do some basic parameter analysis, and then remove the specific work of the event to remove function implementation, so we mainly look at the remove function.

The remove function

 // Delete events, off, etc

function remove(element, events, fn, selector, capture) {
  // Get the flag ID that was added to the element when the event was added
  var id = zid(element)
  // Loop over the events to remove (so we can remove multiple events at once)
    ; (events || ' ').split(/\s/).forEach(function (event) {
      FindHandlers returns a collection of qualified event responses
      findHandlers(element, event, fn, selector).forEach(function (handler) {
        // [{}, {}, {}] Each element adds events that look like this structure
        // Remove the response functions that exist at handlers
        delete handlers[id][handler.i]
        // Actually remove the event bound to element and its event handler
        if ('removeEventListener' in element)
          element.removeEventListener(realEvent(handler.e), handler.proxy, eventCapture(handler, capture))
      })
    })
}Copy the code

Moving on, an important function is findHandlers

// Find handlers from the Handlers given element, event, etc.
// Mainly used for event removal (remove) and actively triggering events (triggerHandler)

function findHandlers(element, event, fn, selector) {
  // Parse the event to get the event name and namespace
  event = parse(event)
  if (event.ns) var matcher = matcherFor(event.ns)
  // Read the handler(array) added to the element and select it based on parameters such as event
  return (handlers[zid(element)] || []).filter(function (handler) {
    returnhandler && (! event.e || handler.e == event.e)// The event name must be the same&& (! event.ns || matcher.test(handler.ns))// Namespaces need to be the same&& (! fn || zid(handler.fn) === zid(fn))// The callback function needs to be the same.&& (! selector || handler.sel == selector)// Selectors need to be the same for event proxies})}Copy the code

Since the callback function during event registration is not the fn passed in by the user, but the customized proxy function, it is necessary to compare the FN passed in by the user with the FN stored in the handler.

At the end

Rory said a lot of wordy bar, I do not know whether to zepto in the event processing part clearly said in detail, welcome your comments.

If it helps you a little bit, click here and add a little star

If it helps you a little bit, click here and add a little star

If it helps you a little bit, click here and add a little star