preface

Dom, also known as the Document Object Model, is an API for HTML and XML that represents a hierarchical tree of nodes. Browsers provide many ways to manipulate the DOM natively, allowing you to find, copy, replace, and delete the DOM. But Zepto encapsulates it again, giving us a more convenient way to operate. Take a look at the figure below, where we explore how Zepto implements each of these by deleting elements, inserting elements, copying elements, wrapping elements and replacing elements.

DOM manipulation

The original link

Github project address

Remove elements

remove

When a parent exists, removes elements from the current collection from its parent.

remove: function () {
  return this.each(function () {
    if (this.parentNode ! =null)
      this.parentNode.removeChild(this)})}Copy the code

Iterate over the elements in the current collection, using removeChild to remove the element when its parent exists.

detach

The function is the same as remove, remove elements.

$.fn.detach = $.fn.removeCopy the code

You can see that the $prototype adds a method detach that points to the remove function.

empty

Empties the DOM content of each element in the object collection

empty: function () {
  return this.each(function () { this.innerHTML = ' '})},Copy the code

Iterate over the elements in the current collection, and set the innerHTML attribute of the element to empty. This completes the purpose of cleaning up the DOM content.

Insert elements

There are many apis for inserting elements, so let’s review some of them and compare their differences.

append, prepend, after, before


<ul class="box">
  <li>1</li>
</ul>Copy the code
let $box = $('.box')
let insertDom = '<li>i am child</li>'

// append appendTo
// $box.append(insertDom)
// $(insertDom).appendTo($box)

/* 
      
  • 1
  • i am child
*/
// prepend prependTo // $box.prepend(insertDom) // $(insertDom).prependTo($box) /*
  • i am child
  • 1
*/
// before insertBefore // $box.before(insertDom) // $(insertDom).insertBefore($box) /*
  • i am child
    • 1
    */
    // after insertAfter // $box.after(insertDom) // $(insertDom).insertAfter($box) /*
    • 1
  • i am child
  • */
    Copy the code

    Above is append, appendTo, the prepend prependTo, insertAfter, before, after, insertBefore eight methods of basic usage, and after using the dom structure. Let’s summarize their differences.

    First, each method can take an HTML string, a DOM node, or an array of nodes. Refer to thezeptojs_api

    append.appendTo.prepend.prependToBoth insert content inside the element, andafter.insertAfter.before.insertBeforeInserts content outside the element.

    append.appendToInsert content at the end of the element,prepend.prependToIs inserted at the initial position of the element,after.insertAfterIs to insert content after the element,before.insertBeforeInserts content in front of the element

    Next we’ll learn and read the core source code for implementing these eight methods

    adjacencyOperators = ['after'.'prepend'.'before'.'append']
    
    adjacencyOperators.forEach(function(operator, operatorIndex) {
      var inside = operatorIndex % 2
    
      $.fn[operator] = function() {
        // arguments can be nodes, arrays of nodes, Zepto objects and HTML strings
        var argType, nodes = $.map(arguments.function(arg) {
          var arr = []
          argType = type(arg)
          if (argType == "array") {
            arg.forEach(function(el) {
              if(el.nodeType ! = =undefined) return arr.push(el)
              else if ($.zepto.isZ(el)) return arr = arr.concat(el.get())
              arr = arr.concat(zepto.fragment(el))
            })
            return arr
          }
          return argType == "object" || arg == null ?
            arg : zepto.fragment(arg)
        }),
            parent, copyByClone = this.length > 1
        if (nodes.length < 1) return this
    
        return this.each(function(_, target) {
          parent = inside ? target : target.parentNode
    
          // convert all methods to a "before" operation
          target = operatorIndex == 0 ? target.nextSibling :
          operatorIndex == 1 ? target.firstChild :
          operatorIndex == 2 ? target :
          null
    
          var parentInDocument = $.contains(document.documentElement, parent)
    
          nodes.forEach(function(node) {
            if (copyByClone) node = node.cloneNode(true)
            else if(! parent)return $(node).remove()
    
            parent.insertBefore(node, target)
            if (parentInDocument) traverseNode(node, function(el) {
              if(el.nodeName ! =null && el.nodeName.toUpperCase() === 'SCRIPT'&& (! el.type || el.type ==='text/javascript') && !el.src) {
                var target = el.ownerDocument ? el.ownerDocument.defaultView : window
                target['eval'].call(target, el.innerHTML)
              }
            })
              })
        })
      }Copy the code

    Walk through the adjacencyOperators array to add corresponding methods to the $stereotype

    adjacencyOperators = ['after'.'prepend'.'before'.'append']
    
    adjacencyOperators.forEach(function(operator, operatorIndex) {
      // xxx
      $.fn[operator] = function() {
        // xxx
      }
      // xxx
    })Copy the code

    You can see that you can loop through the adjacencyOperators to add corresponding methods to the $prototype.

    Conversion node node

    
    var argType, nodes = $.map(arguments.function(arg) {
      var arr = []
      argType = type(arg)
      if (argType == "array") {
        arg.forEach(function(el) {
          if(el.nodeType ! = =undefined) return arr.push(el)
          else if ($.zepto.isZ(el)) return arr = arr.concat(el.get())
          arr = arr.concat(zepto.fragment(el))
        })
        return arr
      }
      return argType == "object" || arg == null ?
        arg : zepto.fragment(arg)
    })Copy the code

    example

    // 1 HTML string
    $box.append('<span>hello world</span>')
    // 2 DOM nodes
    $box.append(document.createElement('span'))
    // 3 Multiple parameters
    $box.append('<span>1</span>'.'<span>2</span>')
    / / 4 array
    $box.append(['<span>hello world</span>'.document.createElement('span')])Copy the code

    The content passed in can be an HTML string, a DOM node, or an array of nodes. There are types of possible cases that are handled here. The data type of each parameter is determined by the internal type function and stored in argType.

    When the parameter type is array (like 4 in the example above), the parameter is iterated, pushing the element into the array ARR if it has a nodeType attribute. If the element in the parameter is a Zepto object, the get method is called to merge the arR with the returned array of native elements.

    Fragment is returned when the argument type is Object or NULL, or if the argument type is not a string, it is processed by calling zepto.fragment(this function will be described in a later article) and returned.

    So far, we’ve seen how to convert the content passed in to the corresponding DOM node.

    Now what do we donodesThe DOM node created in the.

    
    parent, copyByClone = this.length > 1
    
    if (nodes.length < 1) return thisCopy the code

    Take a look at the parent and copyByClone variables, which are important and will be explained below. And if the length of the array to insert is less than 1, there is no need to continue down, just return this to do the chain operation.

    
    return this.each(function(_, target) {
      // xxx
      nodes.forEach(function(node) {
        // xxx 
        // Notice that all insertions are done through the insertBefore function
        parent.insertBefore(node, target)
        // xxx})})Copy the code

    The rest of the code is a two-layer nested loop, with the first layer iterating through the currently selected element set and the second layer iterating through the set of nodes that need to be inserted. The final insertion of the element is done through two loops, and it is important to note that both append and after methods are emulated by insertBefore.

    Identify the parent node and the target target node

    From the above analysis, we know that the insertion of a node is completed by insertBefore(insert a child node before a child node of the current node). Several important factors are

    parentNode.insertBefore(newNode, referenceNode)

    1. The parent node (parentNode)
    2. New nodes to be inserted (newNode)
    3. ReferenceNode referenceNode

    So it’s extremely important to determine the 1 and 3 above. How do you know for sure?

    
    return this.each(function(_, target) {
      parent = inside ? target : target.parentNode
    
      // convert all methods to a "before" operation
      target = operatorIndex == 0 ? target.nextSibling :
      operatorIndex == 1 ? target.firstChild :
      operatorIndex == 2 ? target :
      null
      // xxx
    })Copy the code

    Inside is what!! Let’s go back to the top

    adjacencyOperators = ['after'.'prepend'.'before'.'append']
    adjacencyOperators.forEach(function (operator, operatorIndex) {
      var inside = operatorIndex % 2
      // xxx
    })Copy the code

    So inside is 1 (true) when the $prototype is prepend and append, and 0 (false) when after and before.

    Since both prepend and Append add new nodes inside the selected element, parent is the target itself, but after and before add new nodes outside the selected element, making parent the parent of the currently selected element. So far the above three elements 1 have been identified, and 3(target) how to determine?

    target = operatorIndex == 0 ? target.nextSibling :
      operatorIndex == 1 ? target.firstChild :
      operatorIndex == 2 ? target :
      nullCopy the code
    1. If operatorIndex is 0 (after), node should be inserted after target and before target’s next sibling
    2. If the operatorIndex is 1, which is the prepend method, Node should be inserted before the first child of the target element
    3. If the operatorIndex is 2, the before method, the node node should be inserted before the target node
    4. Otherwise the operatorIndex is 4, that is, the append method, and node should be inserted at the end of the last child of target, insertBefore passing null, which corresponds to its function

    Now that the three elements are defined on page 3, let’s move on to the second loop.

    Inserts the new node into the specified position

    
    nodes.forEach(function(node) {
      if (copyByClone) node = node.cloneNode(true)
      else if(! parent)return $(node).remove()
    
      parent.insertBefore(node, target)
      // Handle script insertion
    })Copy the code

    There is a judgment before inserting the node into the specified location. If copyByClone is true, a copy of the new node will be inserted. Why do you do that? Let’s look at an example.

    
    <ul class="list">
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </ul>Copy the code
    
    let $list = document.querySelector('.list')
      let $listLi = document.querySelectorAll('.list li')
      let createEle = (tagName, text) = > {
        let ele = document.createElement(tagName)
        ele.innerHTML = text
        return ele
      }
      let $span1 = createEle('span'.'span1')
      let $span2 = createEle('span'.'span2')
    
      Array.from($listLi).forEach((target) = > {
        [$span1, $span2].forEach((node) = > {
          // node = node.cloneNode(true)
          $list.insertBefore(node, target)
        })
      })Copy the code

    Unregister the cloneNode part, we expect to insert two spans before each of the three Li’s, but what happens? Only the last node can be successfully preceded by two SPAN nodes. If newElement is already in the DOM tree, it will be removed from the DOM tree first. , so when we need to insert two similar nodes into multiple Li, we need to clone a new node and insert it again.

    Let’s go back to the source code.

    
    nodes.forEach(function(node) {
      if (copyByClone) node = node.cloneNode(true)
      else if(! parent)return $(node).remove()
    
      parent.insertBefore(node, target)
      // Handle script insertion
    })Copy the code

    If a node needs to be cloned (the number of selected elements is greater than 1), the new node should be cloned first. If the corresponding parent node is not found, the new node to be inserted will be deleted. Finally, the new node will be inserted by insertBefore method.

    At this point we seem to have completed the transition from

    Create a new node => Insert the new node to the specified location. The task seems to have been accomplished, but the revolution has not yet succeeded, and comrades still need to work hard. Now comes the last bit of code, dealing with the need to manually execute the js code contained in the inserted node when it is a script tag.

    
    
    var parentInDocument = $.contains(document.documentElement, parent)
    
    if (parentInDocument) traverseNode(node, function(el) {
      if(el.nodeName ! =null && el.nodeName.toUpperCase() === 'SCRIPT'&& (! el.type || el.type ==='text/javascript') && !el.src) {
        var target = el.ownerDocument ? el.ownerDocument.defaultView : window
        target['eval'].call(target, el.innerHTML)
      }
    })Copy the code

    Take a look at the traverseNode function code in advance

    function traverseNode(node, fun) {
      fun(node)
      for (var i = 0, len = node.childNodes.length; i < len; i++)
        traverseNode(node.childNodes[i], fun)
    }Copy the code

    The main purpose of this function is to call the fun function passed in as an argument to the node node. And recursively gives Fun the child nodes of node to handle.

    Let’s keep going.

    The $. Contains method is used to determine whether the parent is in the document, and the following conditions must be met before the subsequent operations are performed.

    1. The nodeName attribute exists
    2. NodeName is a script tag
    3. The type attribute is null or the type attribute is text/javascript
    4. The SRC attribute is empty (that is, no external script is specified)

    Identify the window object

    var target = el.ownerDocument ? el.ownerDocument.defaultView : windowCopy the code

    If the new node has ownerDocument MDN, the window object is defaultView MDN, otherwise the window object itself is used.

    Here we will mainly consider the node is the iframe type of elements, need to do the ternary processing.

    The final call is target[‘eval’].call(target, el.innerhtml) to execute the script.

    ‘after’, ‘prepend’, ‘before’, ‘append’ (😀, not easy)

    appendTo, prependTo, insertBefore, insertAfter

    Then we walked on, said in front of the insert have a lot of ways, including insertAfter, insertBefore, prependTo, appendTo implementation is based on the above several methods.

    // append => appendTo
    // prepend => prependTo
    // before => insertBefore
    // after => insertAfter
    
    $.fn[inside ? operator + 'To' : 'insert' + (operatorIndex ? 'Before' : 'After')] = function (html) {
      $(html)[operator](this)
      return this
    }Copy the code

    AppendTo and prependTo methods are added to the $prototype if append or prepend, and insertBefore and insertAfter methods are added to the $prototype if before or after. Because the two corresponding methods are essentially the same function, but slightly opposite in use, a simple reverse call is ok.

    html

    Gets or sets the HTML content of the element in the collection of objects. Returns innerHtml of the first element in the collection of objects when no content argument is given. When given the Content parameter, it replaces the content of each element in the collection of objects. Content can be all the types of Zeptojs_API described in Append

    example

    1.The HTML () ⇒ string2.HTML (content) ⇒ self3. html(function(index, oldHtml){... }) ⇒ selfCopy the code

    The source code to achieve

    html: function (html) {
      return 0 in arguments ?
        this.each(function (idx) {
          var originHtml = this.innerHTML
          $(this).empty().append(funcArg(this, html, idx, originHtml))
        }) :
        (0 in this ? this[0].innerHTML : null)}Copy the code

    When no HTML argument is passed, the innerHTML of the first element is read and returned. Otherwise, the innerHTML of the first element is returned

    (0 in this ? this[0].innerHTML : null)Copy the code

    When an HTML argument is passed. Iterating over the currently selected set of elements, saving the innerHTML of the current element into the originHtml variable, then emptying the innerHTML of the current element, and inserting the HTML returned by funcArg into the current element.

    function funcArg(context, arg, idx, payload) {
      return isFunction(arg) ? arg.call(context, idx, payload) : arg
    }Copy the code

    As you can see, funcArg will type the incoming ARG. If it is a function, it will pass the corresponding argument to the function and return the result of the function. If it is not a function, it will return the ARG directly.

    text

    Gets or sets the textual content of the elements in all object collections. Returns the text content of the first element in the current collection of objects (including the text content of child nodes) when no content argument is given. When given the Content parameter, it is used to replace the text content of all elements in the object collection. It is similar to HTML except that it cannot be used to get or set HTML. zeptojs_api

    text: function (text) {
      return 0 in arguments ?
        this.each(function (idx) {
          var newText = funcArg(this, text, idx, this.textContent)
          this.textContent = newText == null ? ' ' : ' ' + newText
        }) :
        (0 in this ? this.pluck('textContent').join("") : null)}Copy the code

    The text implementation is a little bit like HTML except that with no arguments, HTML takes the innercontent of the first element and HTMLText concatenates the textContent of all the current elements and returns it.

    Copy the element

    clone

    Copy all the elements in the collection by deep cloning. zeptojs_api

    
    clone: function () {
      return this.map(function () { return this.cloneNode(true)})}Copy the code

    CloneNode is used to iterate over the currently selected element set, and true is passed to indicate that deep cloning is required.

    Note that cloneNode methods do not copy Javascript properties added to DOM nodes, such as event handlers, etc. This method only copies properties, child nodes, and nothing else. There is a bug in IE that it assigns event handlers. So we recommend removing the event handler before assigning.

    Replace the element

    replaceWidth

    Replaces all matched elements with the given content. (including the element itself) zeptojs_API

    replaceWith: function(newContent) {
      return this.before(newContent).remove()
    }Copy the code

    The source code implementation is actually very simple in two steps, the first step is to call the before method we talked about earlier and insert the newContent in front of the element, the second step is to delete the currently selected element. And that, of course, serves the purpose of substitution.

    Package elements

    wrapAll

    Wrap a single structure around all matched elements. The structure can be a single element or several nested elements

    wrapAll: function (structure) {
      // If the selected element exists
      if (this[0]) {
        // The structure is inserted before the first selected element using the before method
        $(this[0]).before(structure = $(structure))
        var children
        // drill down to the inmost element
        // Get the first child of the structure at the deepest level
        while ((children = structure.children()).length) structure = children.first()
        // Add the current set of elements to the end of the structure using the append method
        $(structure).append(this)}// return this directly for subsequent chain operations
      return this
    }Copy the code

    The children function gets all the direct children of the collection of objects. The first function gets the first element of the current collection.

    And let’s look at the following two examples.

    
    <ul class="box">
      <li>1</li>
      <li>2</li>
    </ul>
    <div class="wrap">
    </div>
    <div class="wrap">
    </div>Copy the code
    $('.box').wrapAll('.wrap')Copy the code

    After executing the above code, the DOM structure becomes

    
    <div class="wrap">
      <ul class="box">
        <li>1</li>
        <li>2</li>
      </ul>
    </div>
    
    <div class="wrap">
      <ul class="box">
        <li>1</li>
        <li>2</li>
      </ul>
    </div>
    
    <ul class="box">
      <li>1</li>
      <li>2</li>
    </ul>Copy the code

    You can see that the original UL structure still exists, as if a copy of ul and its children were wrapped in the wrap.

    In another example, the only difference is that the base layer is nested in the wrap structure.

    
    <ul class="box">
        <li>1</li>
        <li>2</li>
    </ul>
    <div class="wrap">
      <div class="here"></div>
      <div></div>
    </div>
    <div class="wrap">
      <div class="here"></div>
      <div></div>
    </div>Copy the code

    But the dom result is $(‘.box’).wrapall (‘.wrap’).

    <div class="wrap">
      <div class="here">
        <ul class="box">
          <li>1</li>
          <li>2</li>
        </ul>
      </div>
      <div></div>
    </div>
    
    <div class="wrap">
      <div class="here"></div>
      <div></div>
    </div>Copy the code

    The ul structure has been moved to the first child of the first wrap, here. What are the specific reasons? You can go back and look at the core implementation of Append.

    wrap

    Wrap an HTML element around each matched element. The structure parameter can be a single element or several nested elements. It can also be an HTML string fragment or a DOM node. It can also be a callback function that generates a package element that returns the first two types of package fragments. zeptojs_api/#wrapAll

    wrap: function (structure) {
      var func = isFunction(structure)
      // The currently selected element is not empty, and structure is not a function
      if (this[0] && !func)
        // Assign the first element of the structure to the DOM element
        var dom = $(structure).get(0),
          Clone is true if the dom element parentNode exists or the number of elements currently selected is greater than 1
          clone = dom.parentNode || this.length > 1
      // Iterate over the currently selected element and call the wrapAll method
      return this.each(function (index) {$(this).wrapAll(
          // If structure is a function, pass the current element and corresponding index to the function
          func ? structure.call(this, index) :
            // If clone is true, the copied copy is used
            clone ? dom.cloneNode(true) : dom
        )
      })
    }Copy the code

    wrapInner

    Wrap the contents of each element in a separate structure

    wrapInner: function (structure) {
      // Determine whether structure is a function
      var func = isFunction(structure)
      // Iterate over the current set of elements
      return this.each(function (index) {
        // contents => get all the children of the current element (including element node and text node)
        var self = $(this), contents = self.contents(),
          // If structure is a function, the result of its execution is assigned to DOM; otherwise, it is directly assigned
          dom = func ? structure.call(this, index) : structure
          // If the child node of the current element is not empty, call wrapAll, otherwise just insert dom directly into the current element of self
        contents.length ? contents.wrapAll(dom) : self.append(dom)
      })
    }Copy the code

    Note that this function is a bit different from the previous wrapAll and wrap functions, which emphasize wrapping the contents of the current element (including element nodes and text nodes).

    unwrap

    Remove the immediate parent node of each element in the collection and leave their child elements in place

    unwrap: function () {
      // Get all the immediate parents of the current set of elements with parent()
      // Iterate over the collection of parent nodes
      this.parent().each(function () {
        // Replace the parent node with all the children of the parent node
        $(this).replaceWith($(this).children())
      })
      return this
    },Copy the code

    At the end

    Whoosh, finally finished, tired to death. You are welcome to point to questions in the text.

    reference

    Read Zepto source operation DOM

    Zepto source code analysis – Zepto module

    ownerDocument

    insertBefore

    innerHTML

    JavaScript Advanced Programming 3rd Edition

    The article records

    The form module

    1. Zepto source Code analysis form module (2017-10-01)

    Zepto module

    1. A collection of useful methods in Zepto (2017-08-26)
    2. Zepto Core Module tools and Methods (2017-08-30)
    3. Zepto: How to add, delete, alter, and query DOM

    The event module

    1. Why are mouseenter and Mouseover so intertwined? (2017-06-05)
    2. How to trigger DOM events manually (2017-06-07)
    3. Who says you just “know how to use “jQuery? (2017-06-08)

    The ajax module

    1. Jsonp (Principle and implementation details)