Property actions for reading Zepto source

Posted by taurus5_6 on Wed, 19 Jun 2019 18:05:40 +0200

This article is still a dom ain-related method, with a focus on how attributes are manipulated.

Read the Zepto Source Series article already on github, welcome star: reading-zepto

Source Version

The source code for reading this article is zepto1.2.0

Internal method

setAttribute

function setAttribute(node, name, value) {
  value == null ? node.removeAttribute(name) : node.setAttribute(name, value)
}

If the property value value value exists, the element's native method setAttribute is called to set the specified property value of the corresponding element, otherwise the removeAttribute is called to delete the specified property.

deserializeValue

// "true"  => true
// "false" => false
// "null"  => null
// "42"    => 42
// "42.5"  => 42.5
// "08"    => "08"
// JSON    => parse if valid
// String  => self
function deserializeValue(value) {
  try {
    return value ?
      value == "true" ||
      (value == "false" ? false :
       value == "null" ? null :
       +value + "" == value ? +value :
       /^[\[\{]/.test(value) ? $.parseJSON(value) :
       value) :
    value
  } catch (e) {
    return value
  }
}

The body of the function is a complex ternary expression, but what the function does is clearly noted in the notes.

try catch guarantees that the original value can still be returned in case of an error.

First break down this complex ternary expression:

Value? The value returned from a fairly complex expression: value

When a value exists, a fairly complex ternary expression operation is performed, otherwise the original value is returned.

Let's look at the operation when value == "true"

value == "true" | | (value from complex expression) 

This is actually an operation or an operation that does not perform the following expression when value == "true", but simply returns the value == "true", that is, returns true.

Let's look at the evaluation when value == false

Value == "false"? False: (value from other expressions)

Obviously, when value == "false", the value returned is false

Value == "null"? Null: (value from other expressions)

When value == "null", the return value is null

Let's look at the judgment of the number string:

 +value +'"== value? +value: (Value from other expressions)

This judgment is interesting.

+value implicitly converts value to numeric type,'42'to 42,'08' to 8, and abc to NaN.+ "" is the value converted to a number and then to a string.Then compare it with the original value using ==Note here that you are using ==, not ==.Needless to say, the expression on the left must be a string type. If the expression on the right is a string type and equals the value on the left, it means that the value is a numeric string and can be converted directly to a number using + value.However, a numeric string starting with 0, such as "08", is converted to "8" on the left, and the two strings are not equal, continuing with the following logic.

If the value is a number, the string on the left is converted to a number again and compared to the value. If the value is converted to a number on the left, it must be the value itself, so the expression holds and returns the same number.

/^[\[\{]/.test(value) ? $.parseJSON(value) : value

The long ternary expression was finally stripped to the bottom of the underwear.

/^[\[\{]/This is to detect whether the value begins with [or {, if so, as an object or array, and to deserialize the $.parseJSON method or return it as its original value.

In fact, this is not very precise. Strings that start with these two symbols may not be in object or array format at all. Serialization may be wrong, which is what try catch was responsible for at the beginning.

.html()

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)
},

The html method can either set or get a value, and the parameter html can be either a fixed value or a function.

The body of the html method is a ternary expression, and 0 in arguments is used to determine if the method has parameters. If it does not, it gets a value; otherwise, it sets a value.

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

Let's first look at Get Value, where 0 in this is a way to determine if the collection is empty, and if it is empty, it returns null; otherwise, it returns the innerHTML attribute value of the first element of the collection.

this.each(function(idx) {
  var originHtml = this.innerHTML
  $(this).empty().append(funcArg(this, html, idx, originHtml))
})

Once you know how to get the value, it's easy to set it, and it's important to note that when you set the value, the innerHTML value for each element in the collection is set to the given value.

Since the parameter html can be a fixed value or a function, the internal function funcArg is called to process the parameter first. For funcArg's analysis, see " Style actions for reading Zepto source code> .

The logic of setting is also simple, first emptying the content of the current element, calling the empty method, then calling the append method, inserting the given value into the current element.For an analysis of the append method, see " Operation DOM for reading Zepto source>

.text()

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)
},

The text method is used to get or set the textContent property of an element.

First look at the situation where no data is passed:

(0 in this ? this.pluck('textContent').join("") : null)

Call the pluck method to get the textContent property of each element and string the result collection.The difference between textContent and innerText is clear on MDN:

  • textContent takes the text of all elements, including script and style elements
  • innerText does not return the text of a hidden element
  • The innerText element is redrawn when it encounters a style

Specific reference MDN:Node.textContent

The html method in the logic for setting values is similar, but when newText == null, assign a value of'', otherwise it is converted to a string.I am a little confused about this conversion. When textContent is assigned, it is automatically converted to a string. Why do you want to convert it yourself once?Also, textContent directly assigned to null or undefined will automatically be converted to'', why do you want to do this yourself?

.attr()

attr: function(name, value) {
  var result
  return (typeof name == 'string' && !(1 in arguments)) ?
    (0 in this && this[0].nodeType == 1 && (result = this[0].getAttribute(name)) != null ? result : undefined) :
  this.each(function(idx) {
    if (this.nodeType !== 1) return
    if (isObject(name))
    for (key in name) setAttribute(this, key, name[key])
    else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name)))
      })
},

attr is used to get or set the attribute value of an element.The name parameter can be an object and is used to set multiple sets of attribute values.

Criteria for judgment:

typeof name == 'string' && !(1 in arguments)

The parameter name is a string, excluding the case where the name is an object, and the second parameter does not exist, in this case, to get a value.

(0 in this && this[0].nodeType == 1 && (result = this[0].getAttribute(name)) != null ? result : undefined)

There are several conditions to satisfy when getting attributes:

  1. Collection is not empty
  2. The nodeType of the first element of the collection is ELEMENT_NODE

Then call the element's native method, getAttribute, to get the attribute value corresponding to the first element. If the attribute value!=null, it returns the obtained attribute value, otherwise it returns undefined.

Let's look again at setting values:

this.each(function(idx) {
  if (this.nodeType !== 1) return
  if (isObject(name))
    for (key in name) setAttribute(this, key, name[key])
  else setAttribute(this, name, funcArg(this, value, idx, this.getAttribute(name)))
    })

If the nodeType of the element is not ELEMENT_NODE, return directly

When name is object, traverse the object and set the corresponding properties

Otherwise, set the value of the given property.

.removeAttr()

removeAttr: function(name) {
  return this.each(function() {
    this.nodeType === 1 && name.split(' ').forEach(function(attribute) {
      setAttribute(this, attribute)
    }, this)
  })
},

Delete the given property.Multiple attributes can be separated by spaces.

Instead, the setAttribute method is called, passing in only elements and attributes that need to be deleted, and setAttribute deletes the corresponding element attributes.

.prop()

propMap = {
  'tabindex': 'tabIndex',
  'readonly': 'readOnly',
  'for': 'htmlFor',
  'class': 'className',
  'maxlength': 'maxLength',
  'cellspacing': 'cellSpacing',
  'cellpadding': 'cellPadding',
  'rowspan': 'rowSpan',
  'colspan': 'colSpan',
  'usemap': 'useMap',
  'frameborder': 'frameBorder',
  'contenteditable': 'contentEditable'
}
prop: function(name, value) {
  name = propMap[name] || name
  return (1 in arguments) ?
    this.each(function(idx) {
    this[name] = funcArg(this, value, idx, this[name])
  }) :
  (this[0] && this[0][name])
},

Prop also sets or gets attributes for an element, but unlike attr, prop sets attributes inherent to the element itself, which attr uses to set custom attributes (and can also set intrinsic attributes).

propMap maps some special properties once.

When prop takes values and sets values, it directly manipulates attributes on element objects without calling methods such as setAttribute.

.removeProp()

removeProp: function(name) {
  name = propMap[name] || name
  return this.each(function() { delete this[name] })
},

Delete the fixed attribute of the element and call the delete method of the object.

.data()

capitalRE = /([A-Z])/g
data: function(name, value) {
  var attrName = 'data-' + name.replace(capitalRE, '-$1').toLowerCase()

  var data = (1 in arguments) ?
      this.attr(attrName, value) :
  this.attr(attrName)

  return data !== null ? deserializeValue(data) : undefined
},

Data calls the attr method internally, but prefixes the attribute name with data-which is also closer to the specification.

name.replace(capitalRE, '-$1').toLowerCase()

To explain this rule a little, capitalRE matches uppercase letters, and replace (capitalRE,'-$1') prefaces uppercase letters with a hyphen.The whole expression is actually converting name into data-camel-case.

return data !== null ? deserializeValue(data) : undefined

If the data is not strictly null, call deserializeValue serialization and return, otherwise return undefined.Why use strict null as a judgment?I don't know that either, because when I get a value, the attr method returns undefined for attributes that don't exist. Would it be better to use!= undefined?This way undefined doesn't need to walk the deserializeValue method at all.

.val()

val: function(value) {
  if (0 in arguments) {
    if (value == null) value = ""
    return this.each(function(idx) {
      this.value = funcArg(this, value, idx, this.value)
    })
  } else {
    return this[0] && (this[0].multiple ?
                       $(this[0]).find('option').filter(function() { return this.selected }).pluck('value') :
                       this[0].value)
  }
},

Gets or sets the value of a form element.

If the parameter is passed, or is the usual routine, the value property of the element is set.

Otherwise, get the value and see the logic for getting the value:

return this[0] && (this[0].multiple ? 
                   $(this[0]).find('option').filter(function() { return this.selected }).pluck('value') : 
                   this[0].value)

this[0].multiple determines whether to multiple-select for the drop-down list, and if so, finds all the selected options and returns the value of the selected option.The pluck method is used here to get attributes. For detailed analysis, see: Reading Zepto Source Collection Element Lookup>

Otherwise, return the value of the first element directly.

.offsetParent()

ootNodeRE = /^(?:body|html)$/i
offsetParent: function() {
  return this.map(function() {
    var parent = this.offsetParent || document.body
    while (parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static")
      parent = parent.offsetParent
    return parent
  })
}

Find the closest ancestor location element, that is, the ancestor element whose closest attribute position is set to relative, absolute, and fixed.

var parent = this.offsetParent || document.body

Gets the offsetParent property of the element, and if it does not exist, the default value is the body element.

parent && !rootNodeRE.test(parent.nodeName) && $(parent).css("position") == "static"

Determines whether the parent positioning element exists and is not the root element (that is, the body element or the html element), and is a relative positioning element before entering the loop, where the next offsetParent element is obtained.

This should be browser compatible, since offsetParent originally returned the most recent location element.

.offset()

offset: function(coordinates) {
  if (coordinates) return this.each(function(index) {
    var $this = $(this),
        coords = funcArg(this, coordinates, index, $this.offset()),
        parentOffset = $this.offsetParent().offset(),
        props = {
          top: coords.top - parentOffset.top,
          left: coords.left - parentOffset.left
        }

    if ($this.css('position') == 'static') props['position'] = 'relative'
    $this.css(props)
  })
  if (!this.length) return null
  if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0]))
    return { top: 0, left: 0 }
    var obj = this[0].getBoundingClientRect()
    return {
      left: obj.left + window.pageXOffset,
      top: obj.top + window.pageYOffset,
      width: Math.round(obj.width),
      height: Math.round(obj.height)
    }
},

Gets or sets the offset of an element from the document.

Let's start with Getting Values:

if (!this.length) return null
if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0]))
  return { top: 0, left: 0 }
var obj = this[0].getBoundingClientRect()
return {
  left: obj.left + window.pageXOffset,
  top: obj.top + window.pageYOffset,
  width: Math.round(obj.width),
  height: Math.round(obj.height)
}

Returns null if the collection does not exist

if (document.documentElement !== this[0] && !$.contains(document.documentElement, this[0]))
  return { top: 0, left: 0 }

Returns {top: 0, left: 0} if the first element in the collection is not an html element object (document.documentElement!== this [0]) and is not a child of the html element

Next, call getBoundingClientRect to get the width and height values of the element, as well as the left and top values in the upper left corner of the relative window.See the documentation for details: Element.getBoundingClientRect()

Because getBoundingClientRect gets the location relative to the window, you need to add an offset outside the window, that is, window.pageXOffset or window.pageYOffset.

Let's look at the settings:

if (coordinates) return this.each(function(index) {
  var $this = $(this),
      coords = funcArg(this, coordinates, index, $this.offset()),
      parentOffset = $this.offsetParent().offset(),
      props = {
        top: coords.top - parentOffset.top,
        left: coords.left - parentOffset.left
      }

  if ($this.css('position') == 'static') props['position'] = 'relative'
  $this.css(props)
})

The first few lines are inherent patterns, so don't expand any more. Look at this paragraph:

parentOffset = $this.offsetParent().offset()

What is the use of getting the offset value of the nearest positioned element?

props = {
  top: coords.top - parentOffset.top,
  left: coords.left - parentOffset.left
}
if ($this.css('position') == 'static') props['position'] = 'relative'
  $this.css(props)

As we can see, setting an offset actually sets the left and top values of the element.If the parent element has a positioning element, the left and top values are relative to the first parent positioning element.

Therefore, you need to subtract the top and left values of the offset of the first parent positioning element from the corresponding incoming coords.top and coords.left.

If the position value of the current element is static, set the value to relative, and offset the current element from itself to calculate the difference between the left and top values.

.position()

position: function() {
  if (!this.length) return

  var elem = this[0],
    offsetParent = this.offsetParent(),
    offset = this.offset(),
    parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset()
  offset.top -= parseFloat($(elem).css('margin-top')) || 0
  offset.left -= parseFloat($(elem).css('margin-left')) || 0
  parentOffset.top += parseFloat($(offsetParent[0]).css('border-top-width')) || 0
  parentOffset.left += parseFloat($(offsetParent[0]).css('border-left-width')) || 0
  return {
    top: offset.top - parentOffset.top,
    left: offset.left - parentOffset.left
  }
},

Returns the offset from the parent element.

offsetParent = this.offsetParent(),
offset = this.offset(),
parentOffset = rootNodeRE.test(offsetParent[0].nodeName) ? { top: 0, left: 0 } : offsetParent.offset()

Get the first positioned parent offsetParent and relative document offset parentOffset, respectively, and its own relative document offset offset.When getting the offset of each positioned parent element, first determine if the parent element is the root element, and if so, both left and top return 0.

offset.top -= parseFloat($(elem).css('margin-top')) || 0
offset.left -= parseFloat($(elem).css('margin-left')) || 0

The distance between two elements should not include the outer margin of the element, so the outer margin is subtracted.

parentOffset.top += parseFloat($(offsetParent[0]).css('border-top-width')) || 0
parentOffset.left += parseFloat($(offsetParent[0]).css('border-left-width')) || 0

Because position returns the distance from the context box of the first positioned element, the offset left and top values of the parent element need to be added to the border value (offset is the distance from the document to the outer margin).

return {
  top: offset.top - parentOffset.top,
  left: offset.left - parentOffset.left
}

Finally, subtract their offset from the document to get the offset between them.

.scrollTop()

scrollTop: function(value) {
  if (!this.length) return
  var hasScrollTop = 'scrollTop' in this[0]
  if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset
  return this.each(hasScrollTop ?
                   function() { this.scrollTop = value } :
                   function() { this.scrollTo(this.scrollX, value) })
},

Gets or sets the scrolling distance of an element on the vertical axis.

First look at getting the value:

var hasScrollTop = 'scrollTop' in this[0]
if (value === undefined) return hasScrollTop ? this[0].scrollTop : this[0].pageYOffset

If there is a scrollTop attribute, scrollTop is used to get the attribute directly, otherwise pageYOffset is used to get the distance of element Y axis out of screen, that is, the scrolling height.

return this.each(hasScrollTop ?
                   function() { this.scrollTop = value } :
                   function() { this.scrollTo(this.scrollX, value) })

Once you know the value, setting the value is also easy. If there is a scrollTop property, set the value of this property directly. Otherwise, call the scrollTo method, scrollX to get the scroll distance to the x-axis, and scroll the y-axis to the specified distance value.

.scrollLeft()

scrollLeft: function(value) {
  if (!this.length) return
  var hasScrollLeft = 'scrollLeft' in this[0]
  if (value === undefined) return hasScrollLeft ? this[0].scrollLeft : this[0].pageXOffset
  return this.each(hasScrollLeft ?
                   function() { this.scrollLeft = value } :
                   function() { this.scrollTo(value, this.scrollY) })
},

The principle of scrollLeft is the same as that of scrollTop, and the narrative is no longer expanded.

Series articles

  1. Read the code structure of the Zepto source
  2. Internal method for reading Zepto source
  3. Tool functions for reading Zepto sources
  4. Read the magic of Zepto Source$
  5. Collection operation for reading Zepto source
  6. Reading Zepto Source Collection Element Lookup
  7. Operation DOM for reading Zepto source
  8. Style actions for reading Zepto source code

Reference resources

License

Author: Diagonal other side

Topics: Javascript Attribute github JSON