Переглянути джерело

fragment handle attach/detach

Evan You 10 роки тому
батько
коміт
9ac42746ea

+ 4 - 2
src/api/lifecycle.js

@@ -63,6 +63,8 @@ exports.$destroy = function (remove, deferCleanup) {
  * @return {Function}
  */
 
-exports.$compile = function (el, host) {
-  return compiler.compile(el, this.$options, true)(this, el, host)
+exports.$compile = function (el, host, scope, frag) {
+  return compiler.compile(el, this.$options, true)(
+    this, el, host, scope, frag
+  )
 }

+ 14 - 12
src/compiler/compile.js

@@ -52,16 +52,18 @@ exports.compile = function (el, options, partial) {
    * @param {Vue} vm
    * @param {Element|DocumentFragment} el
    * @param {Vue} [host] - host vm of transcluded content
+   * @param {Object} [scope] - v-for scope
+   * @param {Fragment} [frag] - link context fragment
    * @return {Function|undefined}
    */
 
-  return function compositeLinkFn (vm, el, host, scope) {
+  return function compositeLinkFn (vm, el, host, scope, frag) {
     // cache childNodes before linking parent, fix #657
     var childNodes = _.toArray(el.childNodes)
     // link
     var dirs = linkAndCapture(function compositeLinkCapturer () {
-      if (nodeLinkFn) nodeLinkFn(vm, el, host, scope)
-      if (childLinkFn) childLinkFn(vm, childNodes, host, scope)
+      if (nodeLinkFn) nodeLinkFn(vm, el, host, scope, frag)
+      if (childLinkFn) childLinkFn(vm, childNodes, host, scope, frag)
     }, vm)
     return makeUnlinkFn(vm, dirs)
   }
@@ -386,7 +388,7 @@ function compileNodeList (nodeList, options) {
  */
 
 function makeChildLinkFn (linkFns) {
-  return function childLinkFn (vm, nodes, host, scope) {
+  return function childLinkFn (vm, nodes, host, scope, frag) {
     var node, nodeLinkFn, childrenLinkFn
     for (var i = 0, n = 0, l = linkFns.length; i < l; n++) {
       node = nodes[n]
@@ -395,10 +397,10 @@ function makeChildLinkFn (linkFns) {
       // cache childNodes before linking parent, fix #657
       var childNodes = _.toArray(node.childNodes)
       if (nodeLinkFn) {
-        nodeLinkFn(vm, node, host, scope)
+        nodeLinkFn(vm, node, host, scope, frag)
       }
       if (childrenLinkFn) {
-        childrenLinkFn(vm, childNodes, host, scope)
+        childrenLinkFn(vm, childNodes, host, scope, frag)
       }
     }
   }
@@ -434,10 +436,10 @@ function checkElementDirectives (el, options) {
 function checkComponent (el, options, hasAttrs) {
   var componentId = _.checkComponent(el, options, hasAttrs)
   if (componentId) {
-    var componentLinkFn = function (vm, el, host, scope) {
+    var componentLinkFn = function (vm, el, host, scope, frag) {
       vm._bindDir('component', el, {
         expression: componentId
-      }, componentDef, host, scope)
+      }, componentDef, host, scope, frag)
     }
     componentLinkFn.terminal = true
     return componentLinkFn
@@ -488,8 +490,8 @@ function makeTerminalNodeLinkFn (el, dirName, value, options, def) {
   // no need to call resolveAsset since terminal directives
   // are always internal
   def = def || options.directives[dirName]
-  var fn = function terminalNodeLinkFn (vm, el, host, scope) {
-    vm._bindDir(dirName, el, descriptor, def, host, scope)
+  var fn = function terminalNodeLinkFn (vm, el, host, scope, frag) {
+    vm._bindDir(dirName, el, descriptor, def, host, scope, frag)
   }
   fn.terminal = true
   return fn
@@ -546,7 +548,7 @@ function compileDirectives (attrs, options) {
  */
 
 function makeNodeLinkFn (directives) {
-  return function nodeLinkFn (vm, el, host, scope) {
+  return function nodeLinkFn (vm, el, host, scope, frag) {
     // reverse apply because it's sorted low to high
     var i = directives.length
     var dir, j, k
@@ -559,7 +561,7 @@ function makeNodeLinkFn (directives) {
         k = dir.descriptors.length
         for (j = 0; j < k; j++) {
           vm._bindDir(dir.name, el,
-            dir.descriptors[j], dir.def, host, scope)
+            dir.descriptors[j], dir.def, host, scope, frag)
         }
       }
     }

+ 7 - 3
src/directive.js

@@ -18,11 +18,13 @@ var expParser = require('./parsers/expression')
  *                 - {String} [arg]
  *                 - {Array<Object>} [filters]
  * @param {Object} def - directive definition object
- * @param {Vue|undefined} host - transclusion host target
+ * @param {Vue} [host] - transclusion host component
+ * @param {Object} [scope] - v-for scope
+ * @param {Fragment} [frag] - owner fragment
  * @constructor
  */
 
-function Directive (name, el, vm, descriptor, def, host, scope) {
+function Directive (name, el, vm, descriptor, def, host, scope, frag) {
   // public
   this.name = name
   this.el = el
@@ -34,11 +36,13 @@ function Directive (name, el, vm, descriptor, def, host, scope) {
   this.filters = descriptor.filters
   // private
   this._descriptor = descriptor
-  this._host = host
   this._locked = false
   this._bound = false
   this._listeners = null
+  // link context
+  this._host = host
   this._scope = scope
+  this._frag = frag
   // init
   this._bind(def)
 }

+ 6 - 1
src/directives/component.js

@@ -209,7 +209,12 @@ module.exports = {
         // will be the intermediate scope created for this
         // repeat fragment. this is used for linking props
         // and container directives.
-        _scope: this._scope
+        _scope: this._scope,
+        // pass in the owner fragment of this component.
+        // this is necessary so that the fragment can keep
+        // track of its contained components in order to
+        // call attach/detach hooks for them.
+        _frag: this._frag
       }
       // extra options
       if (extraOptions) {

+ 3 - 3
src/directives/for.js

@@ -129,7 +129,7 @@ module.exports = {
       frag = oldFrags[i]
       if (!frag.reused) {
         this.deleteCachedFrag(frag)
-        frag.unlink()
+        frag.destroy()
         this.remove(frag, removalIndex++, totalRemoved, inDoc)
       }
     }
@@ -189,7 +189,7 @@ module.exports = {
       // avoid accidental fallback
       _.define(scope, '$key', null)
     }
-    var frag = this.factory.create(host, scope)
+    var frag = this.factory.create(host, scope, this._frag)
     frag.forId = this.id
     this.cacheFrag(value, frag, index, key)
     return frag
@@ -458,7 +458,7 @@ module.exports = {
       while (i--) {
         frag = this.frags[i]
         this.deleteCachedFrag(frag)
-        frag.unlink()
+        frag.destroy()
       }
     }
   }

+ 3 - 45
src/directives/if.js

@@ -30,62 +30,20 @@ module.exports = {
   },
 
   insert: function () {
-    this.frag = this.factory.create(this._host, this._scope)
+    this.frag = this.factory.create(this._host, this._scope, this._frag)
     this.frag.before(this.anchor)
-    // call attached for all the child components created
-    // during the compilation
-    if (_.inDoc(this.vm.$el)) {
-      var children = getContainedComponents(this.vm, this.frag.node, this.anchor)
-      if (children) children.forEach(callAttach)
-    }
   },
 
   remove: function () {
     if (!this.frag) return
-    // collect children beforehand
-    var children
-    if (_.inDoc(this.vm.$el)) {
-      children = getContainedComponents(this.vm, this.frag.node, this.anchor)
-    }
     this.frag.remove()
-    if (children) children.forEach(callDetach)
-    this.frag.unlink()
+    this.frag.destroy()
     this.frag = null
   },
 
   unbind: function () {
     if (this.frag) {
-      this.frag.unlink()
-    }
-  }
-}
-
-function getContainedComponents (vm, start, end) {
-  return vm.$children.length && vm.$children.filter(function (c) {
-    var cur = start
-    var next
-    while (next !== end) {
-      next = cur.nextSibling
-      if (
-        cur === c.$el ||
-        cur.contains && cur.contains(c.$el)
-      ) {
-        return true
-      }
-      cur = next
+      this.frag.destroy()
     }
-    return false
-  })
-}
-
-function callAttach (child) {
-  if (!child._isAttached) {
-    child._callHook('attached')
-  }
-}
-
-function callDetach (child) {
-  if (child._isAttached) {
-    child._callHook('detached')
   }
 }

+ 3 - 1
src/element-directives/content.js

@@ -70,7 +70,9 @@ module.exports = {
 
   compile: function (content, context, host) {
     if (content && context) {
-      this.unlink = context.$compile(content, host)
+      this.unlink = context.$compile(
+        content, host, this._scope, this._frag
+      )
     }
     if (content) {
       _.replace(this.el, content)

+ 3 - 2
src/fragment/factory.js

@@ -45,11 +45,12 @@ function FragmentFactory (vm, el) {
  *
  * @param {Vue} host
  * @param {Object} scope
+ * @param {Fragment} parentFrag
  */
 
-FragmentFactory.prototype.create = function (host, scope) {
+FragmentFactory.prototype.create = function (host, scope, parentFrag) {
   var frag = templateParser.clone(this.template)
-  return new Fragment(this.linker, this.vm, frag, host, scope)
+  return new Fragment(this.linker, this.vm, frag, host, scope, parentFrag)
 }
 
 module.exports = FragmentFactory

+ 51 - 3
src/fragment/fragment.js

@@ -12,11 +12,16 @@ var transition = require('../transition')
  * @param {Object} [scope]
  */
 
-function Fragment (linker, vm, frag, host, scope) {
-  this.unlink = linker(vm, frag, host, scope, this)
+function Fragment (linker, vm, frag, host, scope, parentFrag) {
+  this.children = []
+  this.childFrags = []
   this.scope = scope
   this.inserted = false
-  this.children = []
+  this.parentFrag = parentFrag
+  if (parentFrag) {
+    parentFrag.childFrags.push(this)
+  }
+  this.unlink = linker(vm, frag, host, scope, this)
   var single = this.single = frag.childNodes.length === 1
   if (single) {
     this.node = frag.childNodes[0]
@@ -32,6 +37,23 @@ function Fragment (linker, vm, frag, host, scope) {
   this.node.__vfrag__ = this
 }
 
+Fragment.prototype.callHook = function (hook) {
+  var i, l
+  for (i = 0, l = this.children.length; i < l; i++) {
+    hook(this.children[i])
+  }
+  for (i = 0, l = this.childFrags.length; i < l; i++) {
+    this.childFrags[i].callHook(hook)
+  }
+}
+
+Fragment.prototype.destroy = function () {
+  if (this.parentFrag) {
+    this.parentFrag.$remove(this)
+  }
+  this.unlink()
+}
+
 /**
  * Insert fragment before target, single node version
  *
@@ -45,6 +67,9 @@ function singleBefore (target, trans) {
     : _.before
   method(this.node, target, this.scope)
   this.inserted = true
+  if (_.inDoc(this.node)) {
+    this.callHook(attach)
+  }
 }
 
 /**
@@ -52,8 +77,12 @@ function singleBefore (target, trans) {
  */
 
 function singleRemove () {
+  var shouldCallRemove = _.inDoc(this.node)
   transition.remove(this.node, this.scope)
   this.inserted = false
+  if (shouldCallRemove) {
+    this.callHook(detach)
+  }
 }
 
 /**
@@ -75,6 +104,9 @@ function multiBefore (target, trans) {
   }
   _.before(this.end, target)
   this.inserted = true
+  if (_.inDoc(this.node)) {
+    this.callHook(attach)
+  }
 }
 
 /**
@@ -82,6 +114,7 @@ function multiBefore (target, trans) {
  */
 
 function multiRemove () {
+  var shouldCallRemove = _.inDoc(this.node)
   var parent = this.node.parentNode
   var node = this.node.nextSibling
   var nodes = this.nodes = []
@@ -96,6 +129,21 @@ function multiRemove () {
   parent.removeChild(this.node)
   parent.removeChild(this.end)
   this.inserted = false
+  if (shouldCallRemove) {
+    this.callHook(detach)
+  }
+}
+
+function attach (child) {
+  if (!child._isAttached) {
+    child._callHook('attached')
+  }
+}
+
+function detach (child) {
+  if (child._isAttached) {
+    child._callHook('detached')
+  }
 }
 
 module.exports = Fragment

+ 9 - 3
src/instance/compile.js

@@ -102,12 +102,14 @@ exports._initElement = function (el) {
  * @param {Node} node   - target node
  * @param {Object} desc - parsed directive descriptor
  * @param {Object} def  - directive definition object
- * @param {Vue|undefined} host - transclusion host component
+ * @param {Vue} [host] - transclusion host component
+ * @param {Object} [scope] - v-for scope
+ * @param {Fragment} [frag] - owner fragment
  */
 
-exports._bindDir = function (name, node, desc, def, host, scope) {
+exports._bindDir = function (name, node, desc, def, host, scope, frag) {
   this._directives.push(
-    new Directive(name, node, this, desc, def, host, scope)
+    new Directive(name, node, this, desc, def, host, scope, frag)
   )
 }
 
@@ -133,6 +135,10 @@ exports._destroy = function (remove, deferCleanup) {
   if (parent && !parent._isBeingDestroyed) {
     parent.$children.$remove(this)
   }
+  // remove self from owner fragment
+  if (this._frag) {
+    this._frag.children.$remove(this)
+  }
   // destroy all children.
   i = this.$children.length
   while (i--) {

+ 9 - 0
src/instance/init.js

@@ -59,6 +59,15 @@ exports._init = function (options) {
   // and container directives.
   this._scope = options._scope
 
+  // fragment:
+  // if this instance is compiled inside a Fragment, it
+  // needs to reigster itself as a child of that fragment
+  // for attach/detach to work properly.
+  this._frag = options._frag
+  if (this._frag) {
+    this._frag.children.push(this)
+  }
+
   // push self into parent / transclusion host
   if (this.$parent) {
     this.$parent.$children.push(this)

+ 5 - 5
test/unit/specs/compiler/compile_spec.js

@@ -59,10 +59,10 @@ if (_.inBrowser) {
       expect(typeof linker).toBe('function')
       linker(vm, el)
       expect(vm._bindDir.calls.count()).toBe(4)
-      expect(vm._bindDir).toHaveBeenCalledWith('a', el, descriptorB, defA, undefined, undefined)
-      expect(vm._bindDir).toHaveBeenCalledWith('a', el.firstChild, descriptorA, defA, undefined, undefined)
-      expect(vm._bindDir).toHaveBeenCalledWith('b', el.firstChild, descriptorB, defB, undefined, undefined)
-      expect(vm._bindDir).toHaveBeenCalledWith('b', el.lastChild, descriptorB, defB, undefined, undefined)
+      expect(vm._bindDir).toHaveBeenCalledWith('a', el, descriptorB, defA, undefined, undefined, undefined)
+      expect(vm._bindDir).toHaveBeenCalledWith('a', el.firstChild, descriptorA, defA, undefined, undefined, undefined)
+      expect(vm._bindDir).toHaveBeenCalledWith('b', el.firstChild, descriptorB, defB, undefined, undefined, undefined)
+      expect(vm._bindDir).toHaveBeenCalledWith('b', el.lastChild, descriptorB, defB, undefined, undefined, undefined)
       // check the priority sorting
       // the "b" on the firstNode should be called first!
       expect(vm._bindDir.calls.argsFor(1)[0]).toBe('b')
@@ -115,7 +115,7 @@ if (_.inBrowser) {
       // expect 1 call because terminal should return early and let
       // the directive handle the rest.
       expect(vm._bindDir.calls.count()).toBe(1)
-      expect(vm._bindDir).toHaveBeenCalledWith('repeat', el.firstChild, descriptor, def, undefined, undefined)
+      expect(vm._bindDir).toHaveBeenCalledWith('repeat', el.firstChild, descriptor, def, undefined, undefined, undefined)
     })
 
     it('custom element components', function () {