vue2/src/compiler.js

554 lines
17 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'),
slice = Array.prototype.slice,
vmAttr,
eachAttr
/*
* The DOM compiler
* scans a DOM node and compile bindings for a ViewModel
*/
function Compiler (vm, options) {
// need to refresh this everytime we compile
eachAttr = config.prefix + '-each'
vmAttr = config.prefix + '-viewmodel'
options = this.options = options || {}
// initialize element
var el = typeof options.el === 'string'
? document.querySelector(options.el)
: options.el || document.createElement(options.tagName || 'div')
// 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])
}
}
// initialize template
var template = options.template
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
var templateNode = document.querySelector(template)
if (templateNode) {
el.innerHTML = templateNode.innerHTML
}
} else {
el.innerHTML = template
}
} else if (options.templateFragment) {
el.innerHTML = ''
el.appendChild(options.templateFragment.cloneNode(true))
}
utils.log('\nnew VM instance: ', el, '\n')
// copy data to vm
var data = options.data
if (data) utils.extend(vm, data)
// set stuff on the ViewModel
vm.$el = el
vm.$compiler = this
vm.$parent = options.parentCompiler && options.parentCompiler.vm
// now for the compiler itself...
this.vm = vm
this.el = el
this.directives = []
// anonymous expression bindings that needs to be unbound during destroy()
this.expressions = []
// Store things during parsing to be processed afterwards,
// because we want to have created all bindings before
// observing values / parsing dependencies.
var observables = this.observables = []
var computed = this.computed = [] // computed props to parse deps from
var ctxBindings = this.contextBindings = [] // computed props with dynamic context
// prototypal inheritance of bindings
var parent = this.parentCompiler
this.bindings = parent
? Object.create(parent.bindings)
: {}
this.rootCompiler = parent
? getRoot(parent)
: this
// setup observer
this.setupObserver()
// call user init. this will capture some initial values.
if (options.init) {
options.init.apply(vm, options.args || [])
}
// create bindings for keys set on the vm by the user
for (var key in vm) {
if (key.charAt(0) !== '$') {
this.createBinding(key)
}
}
// now parse the DOM, during which we will create necessary bindings
// and bind the parsed directives
this.compileNode(this.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, this.observer)
}
// extract dependencies for computed properties
if (computed.length) DepsParser.parse(computed)
// extract dependencies for computed properties with dynamic context
if (ctxBindings.length) this.bindContexts(ctxBindings)
// unset these no longer needed stuff
this.observables = this.computed = this.contextBindings = this.arrays = null
}
var CompilerProto = Compiler.prototype
/*
* Setup observer.
* The observer listens for get/set/mutate events on all VM
* values/objects and trigger corresponding binding updates.
*/
CompilerProto.setupObserver = function () {
var bindings = this.bindings,
observer = this.observer = new Emitter(),
depsOb = DepsParser.observer
// a hash to hold event proxies for each root level key
// so they can be referenced and removed later
observer.proxies = {}
// add own listeners which trigger binding updates
observer
.on('get', function (key) {
if (bindings[key] && depsOb.isObserving) {
depsOb.emit('get', bindings[key])
}
})
.on('set', function (key, val) {
observer.emit('change:' + key, val)
if (bindings[key]) bindings[key].update(val)
})
.on('mutate', function (key, val, mutation) {
observer.emit('change:' + key, val, mutation)
if (bindings[key]) bindings[key].pub()
})
}
/*
* Compile a DOM node (recursive)
*/
CompilerProto.compileNode = function (node, root) {
var compiler = this, i, j
if (node.nodeType === 3) { // text node
compiler.compileTextNode(node)
} else if (node.nodeType === 1) {
var eachExp = node.getAttribute(eachAttr),
vmExp = node.getAttribute(vmAttr),
directive
if (eachExp) { // each block
directive = Directive.parse(eachAttr, eachExp, compiler, node)
if (directive) {
compiler.bindDirective(directive)
}
} else if (vmExp && !root) { // nested ViewModels
node.removeAttribute(vmAttr)
var ChildVM = utils.getVM(vmExp)
if (ChildVM) {
new ChildVM({
el: node,
child: true,
parentCompiler: compiler
})
}
} else { // normal node
// parse if has attributes
if (node.attributes && node.attributes.length) {
var attrs = slice.call(node.attributes),
attr, valid, exps, exp
i = attrs.length
while (i--) {
attr = attrs[i]
if (attr.name === vmAttr) continue
valid = false
exps = attr.value.split(',')
j = exps.length
while (j--) {
exp = exps[j]
directive = Directive.parse(attr.name, exp, compiler, node)
if (directive) {
valid = true
compiler.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.compileNode(nodes[i])
}
}
}
}
}
/*
* Compile a text node
*/
CompilerProto.compileTextNode = function (node) {
var tokens = TextParser.parse(node.nodeValue)
if (!tokens) return
var compiler = this,
dirname = config.prefix + '-text',
el, token, directive
for (var i = 0, l = tokens.length; i < l; i++) {
token = tokens[i]
el = document.createTextNode('')
if (token.key) {
directive = Directive.parse(dirname, token.key, compiler, el)
if (directive) {
compiler.bindDirective(directive)
}
} else {
el.nodeValue = token
}
node.parentNode.insertBefore(el, node)
}
node.parentNode.removeChild(node)
}
/*
* Add a directive instance to the correct binding & viewmodel
*/
CompilerProto.bindDirective = function (directive) {
this.directives.push(directive)
var key = directive.key,
baseKey = key.split('.')[0],
compiler = traceOwnerCompiler(directive, this)
var binding
if (directive.isExp) {
binding = this.createBinding(key, true)
} else if (compiler.vm.hasOwnProperty(baseKey)) {
// if the value is present in the target VM, we create the binding on its compiler
binding = compiler.bindings.hasOwnProperty(key)
? compiler.bindings[key]
: compiler.createBinding(key)
} else {
// due to prototypal inheritance of bindings, if a key doesn't exist here,
// it doesn't exist in the whole prototype chain. Therefore in that case
// we create the new binding at the root level.
binding = compiler.bindings[key] || this.rootCompiler.createBinding(key)
}
binding.instances.push(directive)
directive.binding = binding
// for newly inserted sub-VMs (each items), need to bind deps
// because they didn't get processed when the parent compiler
// was binding dependencies.
var i, dep, deps = binding.contextDeps
if (deps) {
i = deps.length
while (i--) {
dep = this.bindings[deps[i]]
dep.subs.push(directive)
}
}
var value = binding.value
// invoke bind hook if exists
if (directive.bind) {
directive.bind(value)
}
// set initial value
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) {
var bindings = this.bindings,
binding = new Binding(this, key, isExp)
if (isExp) {
// a complex expression binding
// we need to generate an anonymous computed property for it
var result = ExpParser.parse(key)
if (result) {
utils.log(' created anonymous binding: ' + key)
binding.value = { get: result.getter }
this.markComputed(binding)
this.expressions.push(binding)
// need to create the bindings for keys
// that do not exist yet
var i = result.vars.length, v
while (i--) {
v = result.vars[i]
if (!bindings[v]) {
this.rootCompiler.createBinding(v)
}
}
} else {
utils.warn(' invalid expression: ' + key)
}
} else {
utils.log(' created binding: ' + key)
bindings[key] = binding
// make sure the key exists in the object so it can be observed
// by the Observer!
this.ensurePath(key)
if (binding.root) {
// this is a root level binding. we need to define getter/setters for it.
this.define(key, binding)
} else {
var parentKey = key.slice(0, key.lastIndexOf('.'))
if (!bindings.hasOwnProperty(parentKey)) {
// this is a nested value binding, but the binding for its parent
// has not been created yet. We better create that one too.
this.createBinding(parentKey)
}
}
}
return binding
}
/*
* Sometimes when a binding is found in the template, the value might
* have not been set on the VM yet. To ensure computed properties and
* dependency extraction can work, we have to create a dummy value for
* any given path.
*/
CompilerProto.ensurePath = function (key) {
var path = key.split('.'), sec, obj = this.vm
for (var i = 0, d = path.length - 1; i < d; i++) {
sec = path[i]
if (!obj[sec]) obj[sec] = {}
obj = obj[sec]
}
if (utils.typeOf(obj) === 'Object') {
sec = path[i]
if (!(sec in obj)) obj[sec] = undefined
}
}
/*
* Defines the getter/setter for a root-level binding on the VM
* and observe the initial value
*/
CompilerProto.define = function (key, binding) {
utils.log(' defined root binding: ' + key)
var compiler = this,
vm = this.vm,
ob = this.observer,
value = binding.value = vm[key], // save the value before redefinening it
type = utils.typeOf(value)
if (type === 'Object' && value.get) {
// computed property
this.markComputed(binding)
} else if (type === 'Object' || type === 'Array') {
// observe objects later, becase there might be more keys
// to be added to it. we also want to emit all the set events
// after all values are available.
this.observables.push(binding)
}
Object.defineProperty(vm, key, {
enumerable: true,
get: function () {
var value = binding.value
if ((!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({
el: compiler.el,
vm: vm,
item: compiler.each
? vm[compiler.eachPrefix]
: null
})
: 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)
// 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
// keep a copy of the raw getter
// for extracting contextual dependencies
binding.rawGet = value.get
// bind the accessors to the vm
value.get = value.get.bind(vm)
if (value.set) value.set = value.set.bind(vm)
// keep track for dep parsing later
this.computed.push(binding)
}
/*
* Process subscriptions for computed properties that has
* dynamic context dependencies
*/
CompilerProto.bindContexts = function (bindings) {
var i = bindings.length, j, k, binding, depKey, dep, ins
while (i--) {
binding = bindings[i]
j = binding.contextDeps.length
while (j--) {
depKey = binding.contextDeps[j]
k = binding.instances.length
while (k--) {
ins = binding.instances[k]
dep = ins.compiler.bindings[depKey]
dep.subs.push(ins)
}
}
}
}
/*
* Unbind and remove element
*/
CompilerProto.destroy = function () {
utils.log('compiler destroyed: ', this.vm.$el)
// unwatch
this.observer.off()
var i, key, dir, inss, binding,
directives = this.directives,
exps = this.expressions,
bindings = this.bindings,
el = this.el
// remove all directives that are instances of external bindings
i = directives.length
while (i--) {
dir = directives[i]
if (dir.binding.compiler !== this) {
inss = dir.binding.instances
if (inss) inss.splice(inss.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 (bindings.hasOwnProperty(key)) {
binding = bindings[key]
if (binding.root) {
Observer.unobserve(binding.value, binding.key, this.observer)
}
binding.unbind()
}
}
// remove el
if (el === document.body) {
el.innerHTML = ''
} else if (el.parentNode) {
el.parentNode.removeChild(el)
}
}
// 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