Zeptoソースコードを読む:DOM操作

この記事は依然として dom 関連のメソッドについて扱い、主に dom を操作するメソッドを重点的に紹介します。

Zeptoソースコードを読むシリーズはGitHub上で公開されていますので、ご興味のある方はぜひStarをお願いします:reading-zepto

ソースコードのバージョン

本記事で読んだソースコードは zepto1.2.0 です。

remove

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

現在の要素集合から要素を削除します。

親ノードが存在する場合、親ノードの removeChild メソッドを使用して要素を削除します。

類似メソッドジェネレータ

zepto における afterprependbeforeappendinsertAfterinsertBeforeappendToprependTo はすべてこの類似メソッドジェネレータによって生成されます。

コンテナの定義

adjacencyOperators = ['after', 'prepend', 'before', 'append']

まず、類似操作の配列を定義します。配列には afterprependbeforeappend のみ含まれており、後でこれらのメソッドを生成した後、insertAfterinsertBeforeappendToprependTo はそれぞれ対応するメソッドを呼び出すことになります。

補助関数traverseNode

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

この関数は再帰的に node の子ノードを走査し、ノードをコールバック関数 fun に渡します。この補助関数は後で使われます。

コアソースコード

adjacencyOperators.forEach(function(operator, operatorIndex) {
  var inside = operatorIndex % 2 //=> prepend, append

  $.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)
          }
        })
          })
    })
  }

呼び出し方法

解析前に、これらのメソッドの使い方を見てみましょう:

after(content)
prepend(content)
before(content)
append(content)

パラメータ content はHTML文字列、DOMノード、またはノードの配列を受け取ることができます。after は各要素の後に content を挿入し、before は各要素の前に挿入します。prepend は各要素の最初に、append は各要素の最後に content を挿入します。beforeafter は要素の外部に挿入され、prependappend は要素の内部に挿入されることに注意してください。

引数 content をnodeノード配列に変換

var inside = operatorIndex % 2 //=> prepend, append

adjacencyOperators を走査し、対応するメソッド名 operator とそのインデックス operatorIndex を取得します。

inside 変数を定義します。インデックスが偶数の場合、insidetrue になり、つまり operatorprepend または append の場合、insidetrue となります。これは content が要素内部に挿入されるか外部に挿入されるかを区別するために使われます。

$.fn[operator]$.fn オブジェクトにプロパティ(メソッド名)を設定します。

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

argType 変数は引数の型を保存し、つまり content の型を保持します。nodescontent から変換された node ノード配列です。

$.map を使って arguments を処理しています。なぜ arguments[0] を使うのではなく $.map を使うのか?それは配列をフラット化できるからです。詳細は『Zeptoのユーティリティ関数』を参照してください。

まず内部関数 type を使って引数の型を取得します。これは『Zepto内部メソッド』で既に分析されています。

引数 content(すなわち arg)の型が配列の場合、arg を走査します。要素に nodeType プロパティがある場合は、node ノードであると判断し、それを arr に追加します。それ以外の場合は、zepto オブジェクト($.zepto.isZ で判定、『Zeptoの$』で分析済み)の場合、get メソッドを呼び出して配列を連結します。それ以外はHTML文字列として zepto.fragment で処理し、結果を連結します。『Zeptoの$』で zepto.fragment についても分析されています。

引数の型が object(すなわち zepto オブジェクト)または null の場合はそのまま返します。

それ以外はHTML文字列として zepto.fragment で処理します。

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

また parent 変数を定義し、content を挿入する親ノードを保持します。要素数が 1 より大きい場合、copyByClonetrue になります。この変数の役割は後で説明します。

nodes の数が 1 より小さい場合、つまり挿入するノードがない場合は、以降の処理を行わず、this を返してチェーン可能にします。

insertBeforeを使ってすべての操作をシミュレート

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

要素集合を each で走査します。

parent = inside ? target : target.parentNode

もし node ノードをターゲット要素 target の内部に挿入する必要がある場合、parent はターゲット要素 target に設定し、そうでなければ親要素に設定します。

target = operatorIndex == 0 ? target.nextSibling :
  operatorIndex == 1 ? target.firstChild :
  operatorIndex == 2 ? target :
  null

このコードはすべての操作を dom ネイティブメソッド insertBefore でシミュレートします。例えば operatorIndex == 0(つまり after)の場合、node ノードはターゲット要素 target の次の兄弟要素の前に挿入されます。operatorIndex == 1(つまり prepend)の場合、node ノードはターゲット要素の最初の子要素の前に挿入されます。これは before に対応します。そして insertBefore の第二引数が null の場合、node は子要素の末尾に挿入され、これは append に対応します。詳細はNode.insertBefore()を参照してください。

var parentInDocument = $.contains(document.documentElement, parent)

$.contains メソッドを使って、親ノード parentdocument 内にあるかどうかを確認します。『Zeptoユーティリティ関数』で $.contains についてすでに分析されています。

nodeノード配列を要素に挿入

nodes.forEach(function(node) {
  if (copyByClone) node = node.cloneNode(true)
  else if (!parent) return $(node).remove()

  parent.insertBefore(node, target)
  ...
})

ノードを複製する必要がある場合(要素数が 1 より大きい場合)、node ノードの cloneNode メソッドを使って複製します。引数 true はノードの子要素と属性も複製することを意味します。なぜ要素数が 1 より大きいときにノードを複製する必要があるのでしょうか?なぜなら insertBefore はノードの参照を挿入するため、すべての要素に対して同じ参照を使うと、最後の要素のみにしか挿入されないからです。

親ノードが存在しない場合、node を削除して処理を終了します。

ノードを insertBefore メソッドで要素に挿入します。

scriptタグ内のスクリプトを処理

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

親要素が document 内にある場合、traverseNode を呼び出して node およびそのすべての子ノードを処理します。主に node またはその子ノードが外部スクリプトを指さない script タグかどうかを確認します。

el.nodeName != null && el.nodeName.toUpperCase() === 'SCRIPT'

これは script タグかどうかを判断するためのコードで、nodenodeName プロパティが script かどうかで判断します。

!el.type || el.type === 'text/javascript'

type 属性がないか、または type 属性が 'text/javascript' の場合。これは javascript だけを処理することを意味します。なぜなら type 属性が必ずしも text/javascript に設定されているとは限らないからです。設定されていないか、または text/javascript の場合のみ javascript として処理されます。詳細はMDNのscript要素を参照してください。

!el.src

かつ外部スクリプトが存在しない場合。

var target = el.ownerDocument ? el.ownerDocument.defaultView : window

ownerDocument プロパティが存在するか、つまり要素のルートノード(すなわち document オブジェクト)を取得し、その defaultView プロパティから関連する window オブジェクトを取得します。これは iframe 内の script を処理するためです。なぜなら iframe には独立した window オブジェクトがあるからです。存在しない場合はデフォルトの window オブジェクトを使用します。

target['eval'].call(target, el.innerHTML)

最後に、windoweval メソッドを呼び出して script 内のスクリプトを実行します。スクリプトは el.innerHTML で取得します。

なぜ script 要素を特別に処理する必要があるのでしょうか?セキュリティの観点から、insertBefore で挿入されたスクリプトは実行されないため、eval を使用して実行する必要があります。

insertAfter、prependTo、insertBefore、append-toメソッドの生成

まずこれらのメソッドの呼び出し方法を見てみましょう:

insertAfter(target)
insertBefore(target)
appendTo(target)
prependTo(target)

これらのメソッドは要素集合を指定されたターゲット要素 target 内に挿入します。これは afterbeforeappendprepend とは逆の操作です。

対応関係は以下の通りです:

after    => insertAfter
prepend  => prependTo
before   => insertBefore
append   => appendTo

そのため、対応するメソッドを呼び出してこれらのメソッドを生成できます。

$.fn[inside ? operator + 'To' : 'insert' + (operatorIndex ? 'Before' : 'After')] = function(html) {
  $(html)[operator](this)
  return this
}
inside ? operator + 'To' : 'insert' + (operatorIndex ? 'Before' : 'After')

これは実際にはメソッド名を生成します。もし prepend または append の場合、後ろに To を追加し、Before または After の場合、前に insert を追加します。

$(html)[operator](this)

単純に反転して対応するメソッドを呼び出します。

これにより、類似メソッドジェネレータは afterprependbeforeappendinsertAfterinsertBeforeappendToprependTo の8つのメソッドを効率的に生成します。

empty

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

empty はすべての要素の内容をクリアし、nodeinnerHTML プロパティを空文字列に設定します。

replaceWith

replaceWith: function(newContent) {
  return this.before(newContent).remove()
},

すべての要素を指定された内容 newContent に置き換えます。newContent の型は before の引数と同じです。

replaceWidth は最初に beforenewContent を挿入し、その後要素を削除することで置き換えを実現します。

wrapAll

wrapAll: function(structure) {
  if (this[0]) {
    $(this[0]).before(structure = $(structure))
    var children
    // drill down to the inmost element
    while ((children = structure.children()).length) structure = children.first()
    $(structure).append(this)
  }
  return this
},

要素集合のすべての要素を指定された構造 structure で囲みます。

要素が存在する場合、すなわち this[0] が存在する場合、後続の操作を行います。そうでない場合は this を返してチェーン可能にします。

before メソッドを使用して、指定された構造を最初の要素の前に挿入します。つまり、すべての要素の前に挿入します。

while ((children = structure.children()).length) structure = children.first()

structure の子要素を検索し、子要素が存在する場合は structure を最初の子要素に代入します。これにより、最も深い子要素までたどります。

要素集合のすべての要素を structure の末尾に挿入します。もし structure に子要素がある場合、最も深い最初の子要素の末尾に挿入します。これにより、すべての要素が structure 内に囲まれます。

wrap

wrap: function(structure) {
  var func = isFunction(structure)
  if (this[0] && !func)
    var dom = $(structure).get(0),
        clone = dom.parentNode || this.length > 1

    return this.each(function(index) {
      $(this).wrapAll(
        func ? structure.call(this, index) :
        clone ? dom.cloneNode(true) : dom
      )
    })
},

要素集合の各要素を指定された構造 structure で囲みます。この構造は単一要素またはネストされた要素、HTML要素、DOMノード、またはコールバック関数にすることができます。コールバック関数は現在の要素とそのインデックスを受け取り、条件に合う囲み構造を返します。

var func = isFunction(structure)

structure が関数かどうかを判断します。

if (this[0] && !func)
  var dom = $(structure).get(0),
      clone = dom.parentNode || this.length > 1

要素集合が空でなく、structure が関数でない場合、structure をノードに変換します。変換には $(structure).get(0) を使用し、結果を dom に代入します。もし domparentNode が存在するか、要素数が 1 より大きい場合、clonetrue になります。

return this.each(function(index) {
  $(this).wrapAll(
  func ? structure.call(this, index) :
  clone ? dom.cloneNode(true) : dom
  )
})

要素集合を走査し、wrapAll メソッドを呼び出します。もし structure が関数の場合、関数の戻り値を引数として渡します。

それ以外の場合、clonetrue の場合、dom(つまり囲み要素のコピー)を渡します。そうでない場合は dom を直接渡します。コピーを渡す理由は、前のジェネレータと同じです。これにより、元のノードの参照を避けるためです。もし domparentNode が存在する場合、dom は他のノードに属していることを意味し、直接使用すると構造が破壊されるためです。

wrapInner

wrapInner: function(structure) {
  var func = isFunction(structure)
  return this.each(function(index) {
    var self = $(this),
        contents = self.contents(),
        dom = func ? structure.call(this, index) : structure
    contents.length ? contents.wrapAll(dom) : self.append(dom)
  })
},

要素集合の各要素の内容を指定された構造 structure で囲みます。structure の引数型は wrap と同じです。

要素集合を走査し、contents メソッドで要素の内容を取得します。『Zeptoの要素探索』で contents メソッドについて分析されています。

もし structure が関数の場合、関数の戻り値を dom に代入します。それ以外の場合は、structure を直接 dom に代入します。

もし contents.length が存在する場合、つまり要素が空でない場合、wrapAll メソッドを呼び出して要素の内容を dom で囲みます。空要素の場合、直接 dom を要素の末尾に挿入し、内部に囲みを適用します。

unwrap

unwrap: function() {
  this.parent().each(function() {
    $(this).replaceWith($(this).children())
  })
  return this
},

要素集合のすべての要素の親要素を削除し、子要素は保持します。

実装方法は簡単で、現在の要素の親要素を走査し、親要素を子要素に置き換えます。

clone

clone: function() {
  return this.map(function() { return this.cloneNode(true) })
},

要素集合の各要素のコピーを作成し、コピーされた集合を返します。

要素集合を走査し、node のネイティブメソッド cloneNode を呼び出してコピーを作成します。注意すべきは、cloneNode は要素のデータやイベントハンドラーはコピーされないことです。

シリーズ記事

  1. Zeptoソースコードを読む:コード構造
  2. Zeptoソースコードを読む:内部メソッド
  3. Zeptoソースコードを読む:ユーティリティ関数
  4. Zeptoソースコードを読む:$の謎
  5. Zeptoソースコードを読む:集合操作
  6. Zeptoソースコードを読む:要素探索

参考

タグ: zepto dom js javascript front-end

7月1日 22:49 投稿