Evan You 11 лет назад
Родитель
Сommit
a0173e235c

+ 2 - 3
component.json

@@ -19,6 +19,8 @@
     "src/batcher.js",
     "src/binding.js",
     "src/cache.js",
+    "src/compile/compile.js",
+    "src/compile/transclude.js",
     "src/config.js",
     "src/directive.js",
     "src/directives/attr.js",
@@ -42,11 +44,8 @@
     "src/filters/array-filters.js",
     "src/filters/index.js",
     "src/instance/bindings.js",
-    "src/instance/compile.js",
-    "src/instance/element.js",
     "src/instance/events.js",
     "src/instance/init.js",
-    "src/instance/misc.js",
     "src/instance/scope.js",
     "src/observe/array-augmentations.js",
     "src/observe/object-augmentations.js",

+ 28 - 9
src/api/lifecycle.js

@@ -1,4 +1,6 @@
 var _ = require('../util')
+var compile = require('../compile/compile')
+var transclude = require('../compile/transclude')
 
 /**
  * Set instance target element and kick off the compilation
@@ -16,21 +18,38 @@ exports.$mount = function (el) {
     return
   }
   this._callHook('beforeCompile')
-  this._initElement(el)
-  this._compile()
-  this._isCompiled = true
+  var options = this.$options
+  if (options._linker) {
+    // pre-compiled linker. this means the element has
+    // been trancluded and compiled. just link it.
+    this._initElement(el)
+    options._linker(this, el)
+  } else {
+    el = transclude(el, options)
+    this._initElement(el)
+    var linker = compile(el, options)
+    linker(this, el)
+  }
   this._callHook('compiled')
-  this.$once('hook:attached', function () {
-    this._isAttached = true
-    this._isReady = true
-    this._callHook('ready')
-    this._initDOMHooks()
-  })
   if (_.inDoc(this.$el)) {
     this._callHook('attached')
+    ready.call(this)
+  } else {
+    this._emitter.once('hook:attached', ready)
   }
 }
 
+/**
+ * Mark an instance as ready.
+ */
+
+function ready () {
+  this._isAttached = true
+  this._isReady = true
+  this._callHook('ready')
+  this._initDOMHooks()
+}
+
 /**
  * Teardown an instance, unobserves the data, unbind all the
  * directives, turn off all the event listeners, etc.

+ 6 - 34
src/compile/compile.js

@@ -21,7 +21,6 @@ function noop () {}
  */
 
 var compile = module.exports = function (el, options) {
-  el = transclude(el, options)
   var nodeLinkFn = el instanceof DocumentFragment
     ? null
     : compileNode(el, options)
@@ -183,7 +182,7 @@ function makeTextNodeLinkFn (tokens, frag) {
     var fragClone = frag.cloneNode(true)
     var childNodes = _.toArray(fragClone.childNodes)
     var dirs = vm._directives
-    var token, value, node, type
+    var token, value, node
     for (var i = 0, l = tokens.length; i < l; i++) {
       token = tokens[i]
       value = token.value
@@ -218,15 +217,14 @@ function makeTextNodeLinkFn (tokens, frag) {
 
 function compileNodeList (nodeList, options) {
   var linkFns = []
-  var nodeLinkFn, childLinkFn
+  var nodeLinkFn, childLinkFn, node
   for (var i = 0, l = nodeList.length; i < l; i++) {
-    // always refer to nodeList[i] because it might be
-    // replaced during tranclusion
-    nodeLinkFn = compileNode(nodeList[i], options)
+    node = nodeList[i]
+    nodeLinkFn = compileNode(node, options)
     childLinkFn =
       (!nodeLinkFn || !nodeLinkFn.terminal) &&
-      nodeList[i].hasChildNodes()
-        ? compileNodeList(nodeList[i].childNodes, options)
+      node.hasChildNodes()
+        ? compileNodeList(node.childNodes, options)
         : null
     linkFns.push(nodeLinkFn, childLinkFn)
   }
@@ -380,32 +378,6 @@ function checkTerminalDirectives (el, options) {
 function makeTeriminalLinkFn (el, dirName, value, options) {
   var descriptor = dirParser.parse(value)[0]
   var def = options.directives[dirName]
-  // we can transclude and compile the child block here
-  // only when there's no dynamic component involved.
-  var dynamicComponent = false
-  var componentId, subOptions
-  if (dirName === 'repeat') {
-    componentId = el.getAttribute(config.prefix + 'component')
-  } else if (dirName === 'component') {
-    componentId = value
-  }
-  if (componentId) {
-    if (textParser.parse(componentId)) {
-      dynamicComponent = true
-    } else {
-      var Ctor = options.components[componentId]
-      _.assertAsset(Ctor, 'component', componentId)
-      if (Ctor) {
-        subOptions = Ctor.options
-      }
-    }
-  }
-  if (!dynamicComponent) {
-    subOptions = subOptions
-      ? mergeOptions(options, subOptions)
-      : _.Vue.options
-    descriptor.linkFn = compile(el, subOptions)
-  }
   return function terminalLinkFn (vm, el) {
     vm._directives.push(
       new Direcitve(dirName, el, vm, descriptor, def)

+ 7 - 1
src/compile/transclude.js

@@ -13,7 +13,7 @@ var templateParser = require('../parse/template')
  * @return {Element|DocumentFragment}
  */
 
-module.exports = function transclude (el, options) {
+var transclude = module.exports = function (el, options) {
   if (typeof el === 'string') {
     var selector = el
     el = document.querySelector(el)
@@ -23,6 +23,12 @@ module.exports = function transclude (el, options) {
   }
   if (el instanceof DocumentFragment) {
     return transcludeBlock(el)
+  } else if (el.tagName === 'TEMPLATE') {
+    if (el.content instanceof DocumentFragment) {
+      return transclude(el.content)
+    } else {
+      return transclude(templateParser.parse(el))
+    }
   } else if (options.template) {
     return transcludeTemplate(el, options)
   } else {

+ 3 - 1
src/directive.js

@@ -17,10 +17,11 @@ var expParser = require('./parse/expression')
  *                 - {String} [arg]
  *                 - {Array<Object>} [filters]
  * @param {Object} def
+ * @param {Function} [linker]
  * @constructor
  */
 
-function Directive (name, el, vm, descriptor, def) {
+function Directive (name, el, vm, descriptor, def, linker) {
   // public
   this.name = name
   this.el = el
@@ -28,6 +29,7 @@ function Directive (name, el, vm, descriptor, def) {
   // copy descriptor props
   _.extend(this, descriptor)
   // private
+  this._linker = linker
   this._locked = false
   this._bound = false
   // init

+ 1 - 1
src/directives/cloak.js

@@ -4,7 +4,7 @@ module.exports = {
 
   bind: function () {
     var el = this.el
-    this.vm.$once('hook:compiled', function () {
+    this.vm._emitter.once('hook:compiled', function () {
       el.removeAttribute(config.prefix + 'cloak')
     })
   }

+ 2 - 1
src/directives/component.js

@@ -37,7 +37,7 @@ module.exports = {
       }
     } else {
       _.warn(
-        'v-component ' + this.expression + ' cannot be ' +
+        'v-component="' + this.expression + '" cannot be ' +
         'used on an already mounted instance.'
       )
     }
@@ -135,6 +135,7 @@ module.exports = {
     if (this.Ctor && !this.childVM) {
       this.childVM = this.vm._addChild({
         el: this.el.cloneNode(true),
+        _linker: this._linker,
         parent: this.vm
       }, this.Ctor)
       if (this.keepAlive) {

+ 3 - 1
src/directives/if.js

@@ -13,14 +13,16 @@ module.exports = {
         this.el = templateParser.parse(el)
       }
     } else {
+      this.invalid = true
       _.warn(
-        'v-if ' + this.expression + ' cannot be ' +
+        'v-if="' + this.expression + '" cannot be ' +
         'used on an already mounted instance.'
       )
     }
   },
 
   update: function (value) {
+    if (this.invalid) return
     if (value) {
       if (!this.inserted) {
         if (!this.childVM) {

+ 0 - 296
src/instance/compile.js

@@ -1,296 +0,0 @@
-var _ = require('../util')
-var config = require('../config')
-var Direcitve = require('../directive')
-var textParser = require('../parse/text')
-var dirParser = require('../parse/directive')
-var templateParser = require('../parse/template')
-
-/**
- * The main entrance to the compilation process.
- * Calling this function requires the instance's `$el` to
- * be already set up, and it should be called only once
- * during an instance's entire lifecycle.
- */
-
-exports._compile = function () {
-  if (this._isBlock) {
-    _.toArray(this._blockFragment.childNodes)
-      .forEach(this._compileNode, this)
-  } else {
-    this._compileNode(this.$el)
-  }
-}
-
-/**
- * Generic compile function. Determines the actual compile
- * function to use based on the nodeType.
- *
- * @param {Node} node
- */
-
-exports._compileNode = function (node) {
-  var type = node.nodeType
-  if (type === 1 && node.tagName !== 'SCRIPT') {
-    this._compileElement(node)
-  } else if (type === 3 && config.interpolate) {
-    this._compileTextNode(node)
-  }
-}
-
-/**
- * Compile an Element.
- *
- * @param {Element} node
- */
-
-exports._compileElement = function (node) {
-  var tag = node.tagName.toLowerCase()
-  // textarea is pretty annoying
-  // because its value creates childNodes which
-  // we don't want to compile.
-  if (tag === 'textarea' && node.value) {
-      node.value = this.$interpolate(node.value)
-  }
-  var hasAttributes = node.hasAttributes()
-  // check priority directives
-  if (hasAttributes) {
-    if (this._checkPriorityDirs(node)) {
-      return
-    }
-  }
-  // check tag components
-  if (
-    tag.indexOf('-') > 0 &&
-    this.$options.components[tag]
-  ) {
-    this._bindDirective('component', tag, node)
-    return
-  }
-  // compile normal directives
-  if (hasAttributes) {
-    this._compileAttrs(node)
-  }
-  // recursively compile childNodes
-  if (node.hasChildNodes()) {
-    _.toArray(node.childNodes)
-      .forEach(this._compileNode, this)
-  }
-}
-
-/**
- * Compile attribtues on an Element.
- * Collect directives, sort them by priority,
- * then bind them. Also check normal directives for
- * param attributes and interpolation bindings.
- *
- * @param {Element} node
- */
-
-exports._compileAttrs = function (node) {
-  var attrs = _.toArray(node.attributes)
-  var i = attrs.length
-  // var registry = this.$options.directives
-  var dirs = []
-  var attr, attrName, dir, dirName, dirDef
-  while (i--) {
-    attr = attrs[i]
-    attrName = attr.name
-    if (attrName.indexOf(config.prefix) === 0) {
-      dirName = attrName.slice(config.prefix.length)
-      dirDef = this.$options.directives[dirName]
-      _.assertAsset(dirDef, 'directive', dirName)
-      if (dirDef) {
-        if (dirName !== 'cloak') {
-          node.removeAttribute(attrName)
-        }
-        dirs.push({
-          name: dirName,
-          value: attr.value,
-          def: dirDef
-        })
-      }
-    } else if (config.interpolate) {
-      this._bindAttr(node, attr)
-    }
-  }
-  // sort the directives by priority, low to high
-  dirs.sort(directiveComparator)
-  i = dirs.length
-  while (i--) {
-    dir = dirs[i]
-    this._bindDirective(dir.name, dir.value, node, dir.def)
-  }
-}
-
-/**
- * Directive priority sort comparator
- *
- * @param {Object} a
- * @param {Object} b
- */
-
-function directiveComparator (a, b) {
-  a = a.def.priority || 0
-  b = b.def.priority || 0
-  return a > b ? 1 : -1
-}
-
-/**
- * Compile a TextNode.
- * Possible interpolations include:
- *
- * - normal text binding,    e.g. {{text}}
- * - unescaped html binding, e.g. {{{html}}}
- * - one-time text binding,  e.g. {{*text}}
- * - one-time html binding,  e.g. {{{*html}}}
- *
- * @param {TextNode} node
- */
-
-exports._compileTextNode = function (node) {
-  var tokens = textParser.parse(node.nodeValue)
-  if (!tokens) {
-    return
-  }
-  var el, token, value
-  for (var i = 0, l = tokens.length; i < l; i++) {
-    token = tokens[i]
-    if (token.tag) {
-      if (token.oneTime) {
-        value = this.$get(token.value)
-        el = token.html
-          ? templateParser.parse(value, true)
-          : document.createTextNode(value)
-        _.before(el, node)
-      } else {
-        value = token.value
-        if (token.html) {
-          el = document.createComment('v-html')
-          _.before(el, node)
-          this._bindDirective('html', value, el)
-        } else if (token.partial) {
-          el = document.createComment('v-partial')
-          _.before(el, node)
-          this._bindDirective('partial', value, el)
-        } else {
-          el = document.createTextNode('')
-          _.before(el, node)
-          this._bindDirective('text', value, el)
-        }
-      }
-    } else {
-      el = document.createTextNode(token.value)
-      _.before(el, node)
-    }
-  }
-  _.remove(node)
-}
-
-/**
- * Check for priority directives that would potentially
- * skip other directives. Each priority directive, once
- * detected, will cause the compilation to skip the rest
- * and let that directive handle the rest. This allows,
- * for example, "v-repeat" to handle how it should handle
- * the situation when "v-component" is also present, and
- * "v-component" won't have to worry about that.
- *
- * @param {Element} node
- * @return {Boolean}
- */
-
-var priorityDirs = [
-  'repeat',
-  'component',
-  'if'
-]
-
-exports._checkPriorityDirs = function (node) {
-  if (_.attr(node, 'pre') !== null) {
-    return true
-  }
-  var value, dir
-  /* jshint boss: true */
-  for (var i = 0; i < 3; i++) {
-    dir = priorityDirs[i]
-    if (value = _.attr(node, dir)) {
-      this._bindDirective(dir, value, node)
-      return true
-    }
-  }
-}
-
-/**
- * Bind a directive.
- *
- * @param {String} name
- * @param {String} value
- * @param {Element} node
- * @param {Object} [def]
- */
-
-exports._bindDirective = function (name, value, node, def) {
-  var descriptors = dirParser.parse(value)
-  var dirs = this._directives
-  def = def || this.$options.directives[name]
-  for (var i = 0, l = descriptors.length; i < l; i++) {
-    dirs.push(
-      new Direcitve(name, node, this, descriptors[i], def)
-    )
-  }
-}
-
-/**
- * Check a normal attribute for bindings.
- * A normal attribute could potentially be:
- *
- * - a param attribute (only on root nodes)
- * - an interpolated attribute, e.g. attr="{{data}}"
- */
-
-exports._bindAttr = function (node, attr) {
-  var name = attr.name
-  var value = attr.value
-  // check if this is a param attribute.
-  var params = this.$options.paramAttributes
-  var isParam =
-    node === this.$el && // only check on root node
-    params &&
-    params.indexOf(name) > -1
-  if (isParam) {
-    node.removeAttribute(name)
-  }
-  // parse attribute value
-  var tokens = textParser.parse(value)
-  if (!tokens) {
-    if (isParam) {
-      this.$set(name, value)
-    }
-    return
-  }
-  // only 1 binding tag allowed
-  if (tokens.length > 1) {
-    _.warn(
-      'Invalid attribute binding: "' +
-      name + '="' + value + '"' +
-      '\nDon\'t mix binding tags with plain text ' +
-      'in attribute bindings.'
-    )
-    return
-  }
-  // param attributes are bound as v-with
-  var dirName = isParam
-    ? 'with'
-    : 'attr'
-  // wrap namespaced attribute so it won't mess up
-  // the directive parser
-  var arg = name.indexOf(':') > 0
-    ? "'" + name + "'"
-    : name
-  // bind
-  this._bindDirective(
-    dirName,
-    arg + ':' + tokens[0].value,
-    node
-  )
-}

+ 0 - 187
src/instance/element.js

@@ -1,187 +0,0 @@
-var _ = require('../util')
-var templateParser = require('../parse/template')
-
-/**
- * Setup the instance's element before compilation.
- * 1. Setup $el
- * 2. Process the template option
- * 3. Resolve <content> insertion points
- *
- * @param {Node|String} el
- */
-
-exports._initElement = function (el) {
-  if (typeof el === 'string') {
-    var selector = el
-    el = document.querySelector(el)
-    if (!el) {
-      _.warn('Cannot find element: ' + selector)
-    }
-  }
-  if (el instanceof DocumentFragment) {
-    this._initBlock(el)
-    this._initContent(el)
-  } else {
-    this.$el = el
-    this._initTemplate()
-  }
-  this.$el.__vue__ = this
-}
-
-/**
- * Initialize a block instance that manages a group of
- * nodes instead of one element. The group is denoted by
- * a starting node and an ending node.
- *
- * @param {DocumentFragment} frag
- */
-
-exports._initBlock = function (frag) {
-  this._isBlock = true
-  this.$el =
-  this._blockStart =
-    document.createComment('v-block-start')
-  this._blockEnd =
-    document.createComment('v-block-end')
-  _.prepend(this._blockStart, frag)
-  frag.appendChild(this._blockEnd)
-  this._blockFragment = frag
-}
-
-/**
- * Process the template option.
- * If the replace option is true this will swap the $el.
- */
-
-exports._initTemplate = function () {
-  var el = this.$el
-  var options = this.$options
-  var template = options.template
-  if (template) {
-    var frag = templateParser.parse(template, true)
-    if (!frag) {
-      _.warn('Invalid template option: ' + template)
-    } else {
-      // collect raw content. this wipes out $el.
-      this._collectRawContent()
-      if (options.replace) {
-        // replace
-        if (frag.childNodes.length > 1) {
-          // the template contains multiple nodes
-          // in this case the original `el` is simply
-          // a placeholder.
-          this._initBlock(frag)
-          this._initContent(_.toArray(frag.children))
-        } else {
-          // 1 to 1 replace, we need to copy all the
-          // attributes from the original el to the replacer
-          this.$el = frag.firstChild
-          _.copyAttributes(el, this.$el)
-          this._initContent(this.$el)
-        }
-        if (el.parentNode) {
-          this.$before(el)
-          _.remove(el)
-        }
-      } else {
-        // simply insert.
-        el.appendChild(frag)
-      }
-    }
-  }
-}
-
-/**
- * Collect raw content inside $el before they are
- * replaced by template content.
- */
-
-exports._collectRawContent = function () {
-  var el = this.$el
-  var child
-  if (el.hasChildNodes()) {
-    this._rawContent = document.createElement('div')
-    /* jshint boss: true */
-    while (child = el.firstChild) {
-      this._rawContent.appendChild(child)
-    }
-  }
-}
-
-/**
- * Resolve <content> insertion points mimicking the behavior
- * of the Shadow DOM spec:
- *
- *   http://w3c.github.io/webcomponents/spec/shadow/#insertion-points
- *
- * @param {Element|DocumentFragment} el
- */
-
-exports._initContent = function (el) {
-  var outlets = getOutlets(el)
-  var raw = this._rawContent
-  var i = outlets.length
-  var outlet, select, j, main
-  if (i) {
-    // first pass, collect corresponding content
-    // for each outlet.
-    while (i--) {
-      outlet = outlets[i]
-      if (raw) {
-        select = outlet.getAttribute('select')
-        if (select) { // select content
-          outlet.content = _.toArray(
-            raw.querySelectorAll(select)
-          )
-        } else { // default content
-          main = outlet
-        }
-      } else { // fallback content
-        outlet.content = _.toArray(outlet.childNodes)
-      }
-    }
-    // second pass, actually insert the contents
-    for (i = 0, j = outlets.length; i < j; i++) {
-      outlet = outlets[i]
-      if (outlet === main) continue
-      insertContentAt(outlet, outlet.content)
-    }
-    // finally insert the main content
-    if (raw && main) {
-      insertContentAt(main, _.toArray(raw.childNodes))
-    }
-  }
-  this._rawContent = null
-}
-
-/**
- * Get <content> outlets from the element/list
- *
- * @param {Element|Array} el
- * @return {Array}
- */
-
-var concat = [].concat
-function getOutlets (el) {
-  return _.isArray(el)
-    ? concat.apply([], el.map(getOutlets))
-    : _.toArray(el.querySelectorAll('content'))
-}
-
-/**
- * Insert an array of nodes at outlet,
- * then remove the outlet.
- *
- * @param {Element} outlet
- * @param {Array} contents
- */
-
-function insertContentAt (outlet, contents) {
-  // not using util DOM methods here because
-  // parentNode can be cached
-  var parent = outlet.parentNode
-  for (var i = 0, j = contents.length; i < j; i++) {
-    parent.insertBefore(contents[i], outlet)
-  }
-  parent.removeChild(outlet)
-}

+ 6 - 4
src/instance/events.js

@@ -8,6 +8,7 @@ var inDoc = require('../util').inDoc
 
 exports._initEvents = function () {
   var options = this.$options
+  var emitter = this._emitter
   var events = options.events
   var methods = options.methods
   if (events) {
@@ -18,7 +19,7 @@ exports._initEvents = function () {
         var handler = typeof handlers[i] === 'string'
           ? methods && methods[handlers[i]]
           : handlers[i]
-        this.$on(e, handler)
+        emitter.on(e, handler)
       }
     }
   }
@@ -29,7 +30,8 @@ exports._initEvents = function () {
  */
 
 exports._initDOMHooks = function () {
-  this.$on('hook:attached', function () {
+  var emitter = this._emitter
+  emitter.on('hook:attached', function () {
     this._isAttached = true
     var children = this._children
     if (!children) return
@@ -40,7 +42,7 @@ exports._initDOMHooks = function () {
       }
     }
   })
-  this.$on('hook:detached', function () {
+  emitter.on('hook:detached', function () {
     this._isAttached = false
     var children = this._children
     if (!children) return
@@ -66,5 +68,5 @@ exports._callHook = function (hook) {
       handlers[i].call(this)
     }
   }
-  this.$emit('hook:' + hook)
+  this._emitter.emit('hook:' + hook)
 }

+ 19 - 0
src/instance/init.js

@@ -70,4 +70,23 @@ exports._init = function (options) {
   if (options.el) {
     this.$mount(options.el)
   }
+}
+
+/**
+ * Initialize instance element. Called in the public
+ * $mount() method.
+ *
+ * @param {Element} el
+ */
+
+exports._initElement = function (el) {
+  if (el instanceof DocumentFragment) {
+    this._isBlock = true
+    this.$el = this._blockStart = el.firstChild
+    this._blockEnd = el.lastChild
+    this._blockFragment = el
+  } else {
+    this.$el = el
+  }
+  this.$el.__vue__ = this
 }

+ 1 - 0
src/parse/directive.js

@@ -27,6 +27,7 @@ var arg
  */
 
 function pushDir () {
+  dir.raw = str.slice(begin, i).trim()
   if (dir.expression === undefined) {
     dir.expression = str.slice(argIndex, i).trim()
   } else if (lastFilterIndex !== begin) {

+ 0 - 1
src/parse/path.js

@@ -1,6 +1,5 @@
 var Cache = require('../cache')
 var pathCache = new Cache(1000)
-var Observer = require('../observe/observer')
 var identRE = /^[$_a-zA-Z]+[\w$]*$/
 
 /**

+ 2 - 2
src/util/merge-option.js

@@ -152,8 +152,8 @@ module.exports = function mergeOptions (parent, child, vm) {
       (key === 'el' || key === 'data' || key === 'parent')
     ) {
       _.warn(
-        'The "' + key + '" option can only be used as an' +
-        'instantiation option and will be ignored in' +
+        'The "' + key + '" option can only be used as an ' +
+        'instantiation option and will be ignored in ' +
         'Vue.extend().'
       )
       return

+ 0 - 2
src/vue.js

@@ -70,8 +70,6 @@ extend(p, require('./instance/init'))
 extend(p, require('./instance/events'))
 extend(p, require('./instance/scope'))
 extend(p, require('./instance/bindings'))
-extend(p, require('./instance/element'))
-extend(p, require('./instance/compile'))
 
 /**
  * Mixin public API methods