vue2/src/compiler.js

659 lines
20 KiB
JavaScript

var Emitter = require('./emitter'),
Observer = require('./observer'),
config = require('./config'),
utils = require('./utils'),
Binding = require('./binding'),
Directive = require('./directive'),
TextParser = require('./text-parser'),
DepsParser = require('./deps-parser'),
ExpParser = require('./exp-parser'),
transition = require('./transition'),
// cache deps ob
depsOb = DepsParser.observer,
// cache methods
slice = Array.prototype.slice,
log = utils.log,
makeHash = utils.hash,
def = utils.defProtected,
hasOwn = Object.prototype.hasOwnProperty
/**
* The DOM compiler
* scans a DOM node and compile bindings for a ViewModel
*/
function Compiler (vm, options) {
var compiler = this
// indicate that we are intiating this instance
// so we should not run any transitions
compiler.init = true
// process and extend options
options = compiler.options = options || makeHash()
utils.processOptions(options)
utils.extend(compiler, options.compilerOptions)
// initialize element
var el = compiler.setupElement(options)
log('\nnew VM instance:', el.tagName, '\n')
// copy scope properties to vm
var scope = options.scope
if (scope) utils.extend(vm, scope, true)
compiler.vm = vm
def(vm, '$', makeHash())
def(vm, '$el', el)
def(vm, '$compiler', compiler)
// keep track of directives and expressions
// so they can be unbound during destroy()
compiler.dirs = []
compiler.exps = []
compiler.childCompilers = [] // keep track of child compilers
compiler.emitter = new Emitter() // the emitter used for nested VM communication
// Store things during parsing to be processed afterwards,
// because we want to have created all bindings before
// observing values / parsing dependencies.
var observables = compiler.observables = [],
computed = compiler.computed = []
// prototypal inheritance of bindings
var parent = compiler.parentCompiler
compiler.bindings = parent
? Object.create(parent.bindings)
: makeHash()
compiler.rootCompiler = parent
? getRoot(parent)
: compiler
// set parent VM
// and register child id on parent
var childId = utils.attr(el, 'component-id')
if (parent) {
parent.childCompilers.push(compiler)
def(vm, '$parent', parent.vm)
if (childId) {
compiler.childId = childId
parent.vm.$[childId] = vm
}
}
// setup observer
compiler.setupObserver()
// pre compile / created hook
var created = options.beforeCompile || options.created
if (created) {
created.call(vm, options)
}
// create bindings for things already in scope
var key, keyPrefix
for (key in vm) {
keyPrefix = key.charAt(0)
if (keyPrefix !== '$' && keyPrefix !== '_') {
compiler.createBinding(key)
}
}
// for repeated items, create an index binding
// which should be inenumerable but configurable
if (compiler.repeat) {
vm.$index = compiler.repeatIndex
def(vm, '$collection', compiler.repeatCollection)
compiler.createBinding('$index')
}
// now parse the DOM, during which we will create necessary bindings
// and bind the parsed directives
compiler.compile(el, true)
// observe root values so that they emit events when
// their nested values change (for an Object)
// or when they mutate (for an Array)
var i = observables.length, binding
while (i--) {
binding = observables[i]
Observer.observe(binding.value, binding.key, compiler.observer)
}
// extract dependencies for computed properties
if (computed.length) DepsParser.parse(computed)
// done!
compiler.init = false
// post compile / ready hook
var ready = options.afterCompile || options.ready
if (ready) {
ready.call(vm, options)
}
}
var CompilerProto = Compiler.prototype
/**
* Initialize the VM/Compiler's element.
* Fill it in with the template if necessary.
*/
CompilerProto.setupElement = function (options) {
// create the node first
var el = this.el = typeof options.el === 'string'
? document.querySelector(options.el)
: options.el || document.createElement(options.tagName || 'div')
var template = options.template
if (template) {
// replace option: use the first node in
// the template directly
if (options.replace && template.childNodes.length === 1) {
var replacer = template.childNodes[0].cloneNode(true)
if (el.parentNode) {
el.parentNode.insertBefore(replacer, el)
el.parentNode.removeChild(el)
}
el = replacer
} else {
el.innerHTML = ''
el.appendChild(template.cloneNode(true))
}
}
// apply element options
if (options.id) el.id = options.id
if (options.className) el.className = options.className
var attrs = options.attributes
if (attrs) {
for (var attr in attrs) {
el.setAttribute(attr, attrs[attr])
}
}
return el
}
/**
* Setup observer.
* The observer listens for get/set/mutate events on all VM
* values/objects and trigger corresponding binding updates.
*/
CompilerProto.setupObserver = function () {
var compiler = this,
bindings = compiler.bindings,
observer = compiler.observer = new Emitter()
// a hash to hold event proxies for each root level key
// so they can be referenced and removed later
observer.proxies = makeHash()
// add own listeners which trigger binding updates
observer
.on('get', function (key) {
check(key)
depsOb.emit('get', bindings[key])
})
.on('set', function (key, val) {
observer.emit('change:' + key, val)
check(key)
bindings[key].update(val)
})
.on('mutate', function (key, val, mutation) {
observer.emit('change:' + key, val, mutation)
check(key)
bindings[key].pub()
})
function check (key) {
if (!bindings[key]) {
compiler.createBinding(key)
}
}
}
/**
* Compile a DOM node (recursive)
*/
CompilerProto.compile = function (node, root) {
var compiler = this,
nodeType = node.nodeType,
tagName = node.tagName
if (nodeType === 1 && tagName !== 'SCRIPT') { // a normal node
// skip anything with v-pre
if (utils.attr(node, 'pre') !== null) return
// special attributes to check
var repeatExp,
componentExp,
partialId,
directive
// It is important that we access these attributes
// procedurally because the order matters.
//
// `utils.attr` removes the attribute once it gets the
// value, so we should not access them all at once.
// v-repeat has the highest priority
// and we need to preserve all other attributes for it.
/* jshint boss: true */
if (repeatExp = utils.attr(node, 'repeat')) {
// repeat block cannot have v-id at the same time.
directive = Directive.parse(config.attrs.repeat, repeatExp, compiler, node)
if (directive) {
compiler.bindDirective(directive)
}
// v-component has 2nd highest priority
} else if (!root && (componentExp = utils.attr(node, 'component'))) {
directive = Directive.parse(config.attrs.component, componentExp, compiler, node)
if (directive) {
// component directive is a bit different from the others.
// when it has no argument, it should be treated as a
// simple directive with its key as the argument.
if (componentExp.indexOf(':') === -1) {
directive.isSimple = true
directive.arg = directive.key
}
compiler.bindDirective(directive)
}
} else {
// check transition property
node.vue_trans = utils.attr(node, 'transition')
// replace innerHTML with partial
partialId = utils.attr(node, 'partial')
if (partialId) {
var partial = compiler.getOption('partials', partialId)
if (partial) {
node.innerHTML = ''
node.appendChild(partial.cloneNode(true))
}
}
// finally, only normal directives left!
compiler.compileNode(node)
}
} else if (nodeType === 3) { // text node
compiler.compileTextNode(node)
}
}
/**
* Compile a normal node
*/
CompilerProto.compileNode = function (node) {
var i, j, attrs = node.attributes
// parse if has attributes
if (attrs && attrs.length) {
var attr, valid, exps, exp
// loop through all attributes
i = attrs.length
while (i--) {
attr = attrs[i]
valid = false
exps = Directive.split(attr.value)
// loop through clauses (separated by ",")
// inside each attribute
j = exps.length
while (j--) {
exp = exps[j]
var directive = Directive.parse(attr.name, exp, this, node)
if (directive) {
valid = true
this.bindDirective(directive)
}
}
if (valid) node.removeAttribute(attr.name)
}
}
// recursively compile childNodes
if (node.childNodes.length) {
var nodes = slice.call(node.childNodes)
for (i = 0, j = nodes.length; i < j; i++) {
this.compile(nodes[i])
}
}
}
/**
* Compile a text node
*/
CompilerProto.compileTextNode = function (node) {
var tokens = TextParser.parse(node.nodeValue)
if (!tokens) return
var dirname = config.attrs.text,
el, token, directive
for (var i = 0, l = tokens.length; i < l; i++) {
token = tokens[i]
if (token.key) { // a binding
if (token.key.charAt(0) === '>') { // a partial
var partialId = token.key.slice(1).trim(),
partial = this.getOption('partials', partialId)
if (partial) {
el = partial.cloneNode(true)
this.compileNode(el)
}
} else { // a binding
el = document.createTextNode('')
directive = Directive.parse(dirname, token.key, this, el)
if (directive) {
this.bindDirective(directive)
}
}
} else { // a plain string
el = document.createTextNode(token)
}
node.parentNode.insertBefore(el, node)
}
node.parentNode.removeChild(node)
}
/**
* Add a directive instance to the correct binding & viewmodel
*/
CompilerProto.bindDirective = function (directive) {
// keep track of it so we can unbind() later
this.dirs.push(directive)
// for a simple directive, simply call its bind() or _update()
// and we're done.
if (directive.isSimple) {
if (directive.bind) directive.bind()
return
}
// otherwise, we got more work to do...
var binding,
compiler = this,
key = directive.key,
baseKey = key.split('.')[0],
ownerCompiler = traceOwnerCompiler(directive, compiler)
if (directive.isExp) {
// expression bindings are always created on current compiler
binding = compiler.createBinding(key, true, directive.isFn)
} else if (ownerCompiler.vm.hasOwnProperty(baseKey)) {
// If the directive's owner compiler's VM has the key,
// it belongs there. Create the binding if it's not already
// created, and return it.
binding = hasOwn.call(ownerCompiler.bindings, key)
? ownerCompiler.bindings[key]
: ownerCompiler.createBinding(key)
} else {
// due to prototypal inheritance of bindings, if a key doesn't exist
// on the owner compiler's VM, then it doesn't exist in the whole
// prototype chain. In this case we create the new binding at the root level.
binding = ownerCompiler.bindings[key] || compiler.rootCompiler.createBinding(key)
}
binding.instances.push(directive)
directive.binding = binding
// invoke bind hook if exists
if (directive.bind) {
directive.bind()
}
// set initial value
var value = binding.value
if (value !== undefined) {
if (binding.isComputed) {
directive.refresh(value)
} else {
directive.update(value, true)
}
}
}
/**
* Create binding and attach getter/setter for a key to the viewmodel object
*/
CompilerProto.createBinding = function (key, isExp, isFn) {
var compiler = this,
bindings = compiler.bindings,
binding = new Binding(compiler, key, isExp, isFn)
if (isExp) {
// a complex expression binding
// we need to generate an anonymous computed property for it
var getter = ExpParser.parse(key, compiler)
if (getter) {
log(' created expression binding: ' + key)
binding.value = isFn
? getter
: { $get: getter }
compiler.markComputed(binding)
compiler.exps.push(binding)
}
} else {
log(' created binding: ' + key)
bindings[key] = binding
// make sure the key exists in the object so it can be observed
// by the Observer!
if (binding.root) {
// this is a root level binding. we need to define getter/setters for it.
compiler.define(key, binding)
} else {
Observer.ensurePath(compiler.vm, key)
var parentKey = key.slice(0, key.lastIndexOf('.'))
if (!hasOwn.call(bindings, parentKey)) {
// this is a nested value binding, but the binding for its parent
// has not been created yet. We better create that one too.
compiler.createBinding(parentKey)
}
}
}
return binding
}
/**
* Defines the getter/setter for a root-level binding on the VM
* and observe the initial value
*/
CompilerProto.define = function (key, binding) {
log(' defined root binding: ' + key)
var compiler = this,
vm = compiler.vm,
ob = compiler.observer,
value = binding.value = vm[key], // save the value before redefinening it
type = utils.typeOf(value)
if (type === 'Object' && value.$get) {
// computed property
compiler.markComputed(binding)
} else if (type === 'Object' || type === 'Array') {
// observe objects later, because there might be more keys
// to be added to it during Observer.ensurePath().
// we also want to emit all the set events after all values
// are available.
compiler.observables.push(binding)
}
Object.defineProperty(vm, key, {
enumerable: true,
get: function () {
var value = binding.value
if (depsOb.active && (!binding.isComputed && (!value || !value.__observer__)) ||
Array.isArray(value)) {
// only emit non-computed, non-observed (primitive) values, or Arrays.
// because these are the cleanest dependencies
ob.emit('get', key)
}
return binding.isComputed
? value.$get()
: value
},
set: function (newVal) {
var value = binding.value
if (binding.isComputed) {
if (value.$set) {
value.$set(newVal)
}
} else if (newVal !== value) {
// unwatch the old value
Observer.unobserve(value, key, ob)
// set new value
binding.value = newVal
ob.emit('set', key, newVal)
Observer.ensurePaths(key, newVal, compiler.bindings)
// now watch the new value, which in turn emits 'set'
// for all its nested values
Observer.observe(newVal, key, ob)
}
}
})
}
/**
* Process a computed property binding
*/
CompilerProto.markComputed = function (binding) {
var value = binding.value,
vm = this.vm
binding.isComputed = true
// bind the accessors to the vm
if (binding.isFn) {
binding.value = utils.bind(value, vm)
} else {
value.$get = utils.bind(value.$get, vm)
if (value.$set) {
value.$set = utils.bind(value.$set, vm)
}
}
// keep track for dep parsing later
this.computed.push(binding)
}
/**
* Retrive an option from the compiler
*/
CompilerProto.getOption = function (type, id) {
var opts = this.options
return (opts[type] && opts[type][id]) || (utils[type] && utils[type][id])
}
/**
* Unbind and remove element
*/
CompilerProto.destroy = function () {
var compiler = this,
i, key, dir, instances, binding,
vm = compiler.vm,
el = compiler.el,
directives = compiler.dirs,
exps = compiler.exps,
bindings = compiler.bindings,
beforeDestroy = compiler.options.beforeDestroy,
afterDestroy = compiler.options.afterDestroy
// call user teardown first
if (beforeDestroy) {
beforeDestroy.call(vm)
}
// unwatch
compiler.observer.off()
compiler.emitter.off()
// unbind all direcitves
i = directives.length
while (i--) {
dir = directives[i]
// if this directive is an instance of an external binding
// e.g. a directive that refers to a variable on the parent VM
// we need to remove it from that binding's instances
if (!dir.isSimple && dir.binding.compiler !== compiler) {
instances = dir.binding.instances
if (instances) instances.splice(instances.indexOf(dir), 1)
}
dir.unbind()
}
// unbind all expressions (anonymous bindings)
i = exps.length
while (i--) {
exps[i].unbind()
}
// unbind/unobserve all own bindings
for (key in bindings) {
if (hasOwn.call(bindings, key)) {
binding = bindings[key]
if (binding.root) {
Observer.unobserve(binding.value, binding.key, compiler.observer)
}
binding.unbind()
}
}
// remove self from parentCompiler
var parent = compiler.parentCompiler,
childId = compiler.childId
if (parent) {
parent.childCompilers.splice(parent.childCompilers.indexOf(compiler), 1)
if (childId) {
delete parent.vm.$[childId]
}
}
// finally remove dom element
if (el === document.body) {
el.innerHTML = ''
} else if (el.parentNode) {
transition(el, -1, function () {
el.parentNode.removeChild(el)
}, this)
}
// post teardown hook
if (afterDestroy) {
afterDestroy.call(vm)
}
}
// Helpers --------------------------------------------------------------------
/**
* determine which viewmodel a key belongs to based on nesting symbols
*/
function traceOwnerCompiler (key, compiler) {
if (key.nesting) {
var levels = key.nesting
while (compiler.parentCompiler && levels--) {
compiler = compiler.parentCompiler
}
} else if (key.root) {
while (compiler.parentCompiler) {
compiler = compiler.parentCompiler
}
}
return compiler
}
/**
* shorthand for getting root compiler
*/
function getRoot (compiler) {
return traceOwnerCompiler({ root: true }, compiler)
}
module.exports = Compiler