This commit is contained in:
Evan You 2016-04-10 22:47:28 -04:00
commit a879ec06ef
35 changed files with 3671 additions and 0 deletions

10
.babelrc Normal file
View File

@ -0,0 +1,10 @@
{
"env": {
"development": {
"presets": ["es2015", "stage-2"]
},
"production": {
"presets": ["es2015-rollup", "stage-2"]
}
}
}

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.DS_Store
node_modules
npm-debug.log
explorations
TODOs.md

124
build/build.js Normal file
View File

@ -0,0 +1,124 @@
var fs = require('fs')
var zlib = require('zlib')
var rollup = require('rollup')
var uglify = require('uglify-js')
var babel = require('rollup-plugin-babel')
var node = require('rollup-plugin-node-resolve')
var commonjs = require('rollup-plugin-commonjs')
var replace = require('rollup-plugin-replace')
var version = process.env.VERSION || require('../package.json').version
var banner =
'/*!\n' +
' * Vue.js v' + version + '\n' +
' * (c) ' + new Date().getFullYear() + ' Evan You\n' +
' * Released under the MIT License.\n' +
' */'
// update main file
var main = fs
.readFileSync('src/index.js', 'utf-8')
.replace(/Vue\.version = '[\d\.]+'/, "Vue.version = '" + version + "'")
fs.writeFileSync('src/index.js', main)
var plugins = [
node(),
commonjs({
include: 'node_modules/**'
}),
babel({
exclude: 'node_modules/**'
})
]
// CommonJS build.
// this is used as the "main" field in package.json
// and used by bundlers like Webpack and Browserify.
rollup.rollup({
entry: 'src/index.js',
plugins: plugins
})
.then(function (bundle) {
return write('dist/vue.common.js', bundle.generate({
format: 'cjs',
banner: banner
}).code)
})
// Standalone Dev Build
.then(function () {
return rollup.rollup({
entry: 'src/index.js',
plugins: [
replace({
'process.env.NODE_ENV': "'development'"
})
].concat(plugins)
})
.then(function (bundle) {
return write('dist/vue.js', bundle.generate({
format: 'umd',
banner: banner,
moduleName: 'Vue'
}).code)
})
})
.then(function () {
// Standalone Production Build
return rollup.rollup({
entry: 'src/index.js',
plugins: [
replace({
'process.env.NODE_ENV': "'production'"
})
].concat(plugins)
})
.then(function (bundle) {
var code = bundle.generate({
format: 'umd',
moduleName: 'Vue'
}).code
var minified = banner + '\n' + uglify.minify(code, {
fromString: true,
output: {
ascii_only: true
}
}).code
return write('dist/vue.min.js', minified)
})
.then(zip)
})
.catch(logError)
function write (dest, code) {
return new Promise(function (resolve, reject) {
fs.writeFile(dest, code, function (err) {
if (err) return reject(err)
console.log(blue(dest) + ' ' + getSize(code))
resolve()
})
})
}
function zip () {
return new Promise(function (resolve, reject) {
fs.readFile('dist/vue.min.js', function (err, buf) {
if (err) return reject(err)
zlib.gzip(buf, function (err, buf) {
if (err) return reject(err)
write('dist/vue.min.js.gz', buf).then(resolve)
})
})
})
}
function getSize (code) {
return (code.length / 1024).toFixed(2) + 'kb'
}
function logError (e) {
console.log(e)
}
function blue (str) {
return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m'
}

38
package.json Normal file
View File

@ -0,0 +1,38 @@
{
"name": "vue-lite",
"version": "1.0.0",
"description": "Lighter-weight Vue on virtual dom",
"main": "index.js",
"scripts": {
"dev": "webpack --watch",
"test": "mocha",
"build": "NODE_ENV=production node build/build.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/vuejs/vue-lite.git"
},
"keywords": [
"vue"
],
"author": "Evan You",
"license": "MIT",
"bugs": {
"url": "https://github.com/vuejs/vue-lite/issues"
},
"homepage": "https://github.com/vuejs/vue-lite#readme",
"devDependencies": {
"babel-core": "^6.0.0",
"babel-loader": "^6.0.0",
"babel-preset-es2015": "^6.0.0",
"babel-preset-es2015-rollup": "^1.1.1",
"babel-preset-stage-2": "^6.0.0",
"rollup": "^0.25.8",
"rollup-plugin-babel": "^2.4.0",
"rollup-plugin-commonjs": "^2.2.1",
"rollup-plugin-node-resolve": "^1.5.0",
"rollup-plugin-replace": "^1.1.0",
"uglify-js": "^2.6.2",
"webpack": "^1.12.14"
}
}

129
src/compiler/codegen.js Normal file
View File

@ -0,0 +1,129 @@
import { parseText } from './text-parser'
const bindRE = /^:|^v-bind:/
const onRE = /^@|^v-on:/
const mustUsePropsRE = /^(value|selected|checked|muted)$/
export function generate (ast) {
const code = genElement(ast)
return new Function (`with (this) { return ${code}}`)
}
function genElement (el, key) {
let exp
if (exp = getAttr(el, 'v-for')) {
return genFor(el, exp)
} else if (exp = getAttr(el, 'v-if')) {
return genIf(el, exp)
} else if (el.tag === 'template') {
return genChildren(el)
} else {
return `__h__('${ el.tag }', ${ genData(el, key) }, ${ genChildren(el) })`
}
}
function genIf (el, exp) {
return `(${ exp }) ? ${ genElement(el) } : ''`
}
function genFor (el, exp) {
const inMatch = exp.match(/([a-zA-Z_][\w]*)\s+(?:in|of)\s+(.*)/)
if (!inMatch) {
throw new Error('Invalid v-for expression: '+ exp)
}
const alias = inMatch[1].trim()
exp = inMatch[2].trim()
const key = el.attrsMap['track-by'] || 'undefined'
return `(${ exp }).map(function (${ alias }, $index) {return ${ genElement(el, key) }})`
}
function genData (el, key) {
if (!el.attrs.length) {
return '{}'
}
let data = key ? `{key:${ key },` : `{`
if (el.attrsMap[':class'] || el.attrsMap['class']) {
data += `class: _renderClass(${ el.attrsMap[':class'] }, "${ el.attrsMap['class'] || '' }"),`
}
let attrs = `attrs:{`
let props = `props:{`
let hasAttrs = false
let hasProps = false
for (let i = 0, l = el.attrs.length; i < l; i++) {
let attr = el.attrs[i]
let name = attr.name
if (bindRE.test(name)) {
name = name.replace(bindRE, '')
if (name === 'class') {
continue
} else if (name === 'style') {
data += `style: ${ attr.value },`
} else if (mustUsePropsRE.test(name)) {
hasProps = true
props += `"${ name }": (${ attr.value }),`
} else {
hasAttrs = true
attrs += `"${ name }": (${ attr.value }),`
}
} else if (onRE.test(name)) {
name = name.replace(onRE, '')
// TODO
} else if (name !== 'class') {
hasAttrs = true
attrs += `"${ name }": (${ JSON.stringify(attr.value) }),`
}
}
if (hasAttrs) {
data += attrs.slice(0, -1) + '},'
}
if (hasProps) {
data += props.slice(0, -1) + '},'
}
return data.replace(/,$/, '') + '}'
}
function genChildren (el) {
if (!el.children.length) {
return 'undefined'
}
return '__flatten__([' + el.children.map(genNode).join(',') + '])'
}
function genNode (node) {
if (node.tag) {
return genElement(node)
} else {
return genText(node)
}
}
function genText (text) {
if (text === ' ') {
return '" "'
} else {
const exp = parseText(text)
if (exp) {
return 'String(' + escapeNewlines(exp) + ')'
} else {
return escapeNewlines(JSON.stringify(text))
}
}
}
function escapeNewlines (str) {
return str.replace(/\n/g, '\\n')
}
function getAttr (el, attr) {
let val
if (val = el.attrsMap[attr]) {
el.attrsMap[attr] = null
for (let i = 0, l = el.attrs.length; i < l; i++) {
if (el.attrs[i].name === attr) {
el.attrs.splice(i, 1)
break
}
}
}
return val
}

389
src/compiler/html-parser.js Normal file
View File

@ -0,0 +1,389 @@
/**
* Convert HTML string to AST
*
* @param {String} html
* @return {Object}
*/
export function parse (html) {
let root
let currentParent
let stack = []
HTMLParser(html, {
html5: true,
start (tag, attrs, unary) {
let element = {
tag,
attrs,
attrsMap: makeAttrsMap(attrs),
parent: currentParent,
children: []
}
if (!root) {
root = element
}
if (currentParent) {
currentParent.children.push(element)
}
if (!unary) {
currentParent = element
stack.push(element)
}
},
end () {
stack.length -= 1
currentParent = stack[stack.length - 1]
},
chars (text) {
text = currentParent.tag === 'pre'
? text
: text.trim() ? text : ' '
currentParent.children.push(text)
},
comment () {
// noop
}
})
return root
}
function makeAttrsMap (attrs) {
const map = {}
for (let i = 0, l = attrs.length; i < l; i++) {
map[attrs[i].name] = attrs[i].value
}
return map
}
/*!
* HTML Parser By John Resig (ejohn.org)
* Modified by Juriy "kangax" Zaytsev
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*/
/*
* // Use like so:
* HTMLParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*/
/* global ActiveXObject, DOMDocument */
function makeMap(values) {
values = values.split(/,/)
var map = {}
values.forEach(function(value) {
map[value] = 1
})
return function(value) {
return map[value.toLowerCase()] === 1
}
}
// Regular Expressions for parsing tags and attributes
var singleAttrIdentifier = /([^\s"'<>\/=]+)/,
singleAttrAssign = /=/,
singleAttrAssigns = [singleAttrAssign],
singleAttrValues = [
// attr value double quotes
/"([^"]*)"+/.source,
// attr value, single quotes
/'([^']*)'+/.source,
// attr value, no quotes
/([^\s"'=<>`]+)/.source
],
qnameCapture = (function() {
// could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
// but for Vue templates we can enforce a simple charset
var ncname = '[a-zA-Z_][\\w\\-\\.]*'
return '((?:' + ncname + '\\:)?' + ncname + ')'
})(),
startTagOpen = new RegExp('^<' + qnameCapture),
startTagClose = /^\s*(\/?)>/,
endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>'),
doctype = /^<!DOCTYPE [^>]+>/i
var IS_REGEX_CAPTURING_BROKEN = false
'x'.replace(/x(.)?/g, function(m, g) {
IS_REGEX_CAPTURING_BROKEN = g === ''
})
// Empty Elements
var empty = makeMap('area,base,basefont,br,col,embed,frame,hr,img,input,isindex,keygen,link,meta,param,source,track,wbr')
// Inline Elements
var inline = makeMap('a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,noscript,object,q,s,samp,script,select,small,span,strike,strong,sub,sup,svg,textarea,tt,u,var')
// Elements that you can, intentionally, leave open
// (and which close themselves)
var closeSelf = makeMap('colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source')
// Attributes that have their values filled in disabled='disabled'
var fillAttrs = makeMap('checked,compact,declare,defer,disabled,ismap,multiple,nohref,noresize,noshade,nowrap,readonly,selected')
// Special Elements (can contain anything)
var special = makeMap('script,style')
// HTML5 tags https://html.spec.whatwg.org/multipage/indices.html#elements-3
// Phrasing Content https://html.spec.whatwg.org/multipage/dom.html#phrasing-content
var nonPhrasing = makeMap('address,article,aside,base,blockquote,body,caption,col,colgroup,dd,details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,title,tr,track')
var reCache = {}
function attrForHandler(handler) {
var pattern = singleAttrIdentifier.source +
'(?:\\s*(' + joinSingleAttrAssigns(handler) + ')' +
'\\s*(?:' + singleAttrValues.join('|') + '))?'
return new RegExp('^\\s*' + pattern)
}
function joinSingleAttrAssigns(handler) {
return singleAttrAssigns.map(function(assign) {
return '(?:' + assign.source + ')'
}).join('|')
}
export default function HTMLParser(html, handler) {
var stack = [], lastTag
var attribute = attrForHandler(handler)
var last, prevTag, nextTag
while (html) {
last = html
// Make sure we're not in a script or style element
if (!lastTag || !special(lastTag)) {
var textEnd = html.indexOf('<')
if (textEnd === 0) {
// Comment:
if (/^<!--/.test(html)) {
var commentEnd = html.indexOf('-->')
if (commentEnd >= 0) {
if (handler.comment) {
handler.comment(html.substring(4, commentEnd))
}
html = html.substring(commentEnd + 3)
prevTag = ''
continue
}
}
// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
if (/^<!\[/.test(html)) {
var conditionalEnd = html.indexOf(']>')
if (conditionalEnd >= 0) {
if (handler.comment) {
handler.comment(html.substring(2, conditionalEnd + 1), true /* non-standard */)
}
html = html.substring(conditionalEnd + 2)
prevTag = ''
continue
}
}
// Doctype:
var doctypeMatch = html.match(doctype)
if (doctypeMatch) {
if (handler.doctype) {
handler.doctype(doctypeMatch[0])
}
html = html.substring(doctypeMatch[0].length)
prevTag = ''
continue
}
// End tag:
var endTagMatch = html.match(endTag)
if (endTagMatch) {
html = html.substring(endTagMatch[0].length)
endTagMatch[0].replace(endTag, parseEndTag)
prevTag = '/' + endTagMatch[1].toLowerCase()
continue
}
// Start tag:
var startTagMatch = parseStartTag(html)
if (startTagMatch) {
html = startTagMatch.rest
handleStartTag(startTagMatch)
prevTag = startTagMatch.tagName.toLowerCase()
continue
}
}
var text
if (textEnd >= 0) {
text = html.substring(0, textEnd)
html = html.substring(textEnd)
}
else {
text = html
html = ''
}
// next tag
var nextTagMatch = parseStartTag(html)
if (nextTagMatch) {
nextTag = nextTagMatch.tagName
}
else {
nextTagMatch = html.match(endTag)
if (nextTagMatch) {
nextTag = '/' + nextTagMatch[1]
}
else {
nextTag = ''
}
}
if (handler.chars) {
handler.chars(text, prevTag, nextTag)
}
prevTag = ''
}
else {
var stackedTag = lastTag.toLowerCase()
var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)</' + stackedTag + '[^>]*>', 'i'))
html = html.replace(reStackedTag, function(all, text) {
if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
text = text
.replace(/<!--([\s\S]*?)-->/g, '$1')
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
}
if (handler.chars) {
handler.chars(text)
}
return ''
})
parseEndTag('</' + stackedTag + '>', stackedTag)
}
if (html === last) {
throw new Error('Parse Error: ' + html)
}
}
if (!handler.partialMarkup) {
// Clean up any remaining tags
parseEndTag()
}
function parseStartTag(input) {
var start = input.match(startTagOpen)
if (start) {
var match = {
tagName: start[1],
attrs: []
}
input = input.slice(start[0].length)
var end, attr
while (!(end = input.match(startTagClose)) && (attr = input.match(attribute))) {
input = input.slice(attr[0].length)
match.attrs.push(attr)
}
if (end) {
match.unarySlash = end[1]
match.rest = input.slice(end[0].length)
return match
}
}
}
function handleStartTag(match) {
var tagName = match.tagName
var unarySlash = match.unarySlash
if (handler.html5 && lastTag === 'p' && nonPhrasing(tagName)) {
parseEndTag('', lastTag)
}
if (!handler.html5) {
while (lastTag && inline(lastTag)) {
parseEndTag('', lastTag)
}
}
if (closeSelf(tagName) && lastTag === tagName) {
parseEndTag('', tagName)
}
var unary = empty(tagName) || tagName === 'html' && lastTag === 'head' || !!unarySlash
var attrs = match.attrs.map(function(args) {
// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
if (args[3] === '') { delete args[3] }
if (args[4] === '') { delete args[4] }
if (args[5] === '') { delete args[5] }
}
return {
name: args[1],
value: args[3] || args[4] || (args[5] && fillAttrs(args[5]) ? name : '')
}
})
if (!unary) {
stack.push({ tag: tagName, attrs: attrs })
lastTag = tagName
unarySlash = ''
}
if (handler.start) {
handler.start(tagName, attrs, unary, unarySlash)
}
}
function parseEndTag(tag, tagName) {
var pos
// Find the closest opened tag of the same type
if (tagName) {
var needle = tagName.toLowerCase()
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].tag.toLowerCase() === needle) {
break
}
}
}
// If no tag name is provided, clean shop
else {
pos = 0
}
if (pos >= 0) {
// Close all the open elements, up the stack
for (var i = stack.length - 1; i >= pos; i--) {
if (handler.end) {
handler.end(stack[i].tag, stack[i].attrs, i > pos || !tag)
}
}
// Remove the open elements from the stack
stack.length = pos
lastTag = pos && stack[pos - 1].tag
}
else if (tagName.toLowerCase() === 'br') {
if (handler.start) {
handler.start(tagName, [], true, '')
}
}
else if (tagName.toLowerCase() === 'p') {
if (handler.start) {
handler.start(tagName, [], false, '', true)
}
if (handler.end) {
handler.end(tagName, [])
}
}
}
}

10
src/compiler/index.js Normal file
View File

@ -0,0 +1,10 @@
import { parse } from './html-parser'
import { generate } from './codegen'
const cache = Object.create(null)
export function compile (html) {
html = html.trim()
const hit = cache[html]
return hit || (cache[html] = generate(parse(html)))
}

View File

@ -0,0 +1,27 @@
const tagRE = /\{\{((?:.|\\n)+?)\}\}/g
export function parseText (text) {
if (!tagRE.test(text)) {
return null
}
var tokens = []
var lastIndex = tagRE.lastIndex = 0
var match, index, value
/* eslint-disable no-cond-assign */
while (match = tagRE.exec(text)) {
/* eslint-enable no-cond-assign */
index = match.index
// push text token
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)))
}
// tag token
value = match[1]
tokens.push('(' + match[1].trim() + ')')
lastIndex = index + match[0].length
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return tokens.join('+')
}

79
src/config.js Normal file
View File

@ -0,0 +1,79 @@
export default {
/**
* Whether to print debug messages.
* Also enables stack trace for warnings.
*
* @type {Boolean}
*/
debug: false,
/**
* Whether to suppress warnings.
*
* @type {Boolean}
*/
silent: false,
/**
* Whether to use async rendering.
*/
async: true,
/**
* Whether to warn against errors caught when evaluating
* expressions.
*/
warnExpressionErrors: true,
/**
* Whether to allow devtools inspection.
* Disabled by default in production builds.
*/
devtools: process.env.NODE_ENV !== 'production',
/**
* Internal flag to indicate the delimiters have been
* changed.
*
* @type {Boolean}
*/
_delimitersChanged: true,
/**
* List of asset types that a component can own.
*
* @type {Array}
*/
_assetTypes: [
'component',
'directive',
'elementDirective',
'filter',
'transition',
'partial'
],
/**
* prop binding modes
*/
_propBindingModes: {
ONE_WAY: 0,
TWO_WAY: 1,
ONE_TIME: 2
},
/**
* Max circular updates allowed in a batcher flush cycle.
*/
_maxUpdateCount: 100
}

3
src/index.js Normal file
View File

@ -0,0 +1,3 @@
import Vue from './instance/index'
export default Vue

1
src/index.umd.js Normal file
View File

@ -0,0 +1 @@
module.exports = require('./index')['default']

85
src/instance/index.js Normal file
View File

@ -0,0 +1,85 @@
import { compile } from '../compiler/index'
import { observe } from '../observer/index'
import Watcher from '../observer/watcher'
import { h, patch } from '../vdom/index'
import { nextTick, isReserved, getOuterHTML } from '../util/index'
export default class Component {
constructor (options) {
this.$options = options
this._data = options.data
const el = this._el = document.querySelector(options.el)
const render = compile(getOuterHTML(el))
this._el.innerHTML = ''
Object.keys(options.data).forEach(key => this._proxy(key))
if (options.methods) {
Object.keys(options.methods).forEach(key => {
this[key] = options.methods[key].bind(this)
})
}
this._ob = observe(options.data)
this._watchers = []
this._watcher = new Watcher(this, render, this._update)
this._update(this._watcher.value)
}
_update (vtree) {
if (!this._tree) {
patch(this._el, vtree)
} else {
patch(this._tree, vtree)
}
this._tree = vtree
}
_renderClass (dynamic, cls) {
dynamic = dynamic
? typeof dynamic === 'string'
? dynamic
: Object.keys(dynamic).filter(key => dynamic[key]).join(' ')
: ''
return cls
? cls + (dynamic ? ' ' + dynamic : '')
: dynamic
}
__flatten__ (arr) {
var res = []
for (var i = 0, l = arr.length; i < l; i++) {
var e = arr[i]
if (Array.isArray(e)) {
for (var j = 0, k = e.length; j < k; j++) {
if (e[j]) {
res.push(e[j])
}
}
} else if (e) {
res.push(e)
}
}
return res
}
_proxy (key) {
if (!isReserved(key)) {
// need to store ref to self here
// because these getter/setters might
// be called by child scopes via
// prototype inheritance.
var self = this
Object.defineProperty(self, key, {
configurable: true,
enumerable: true,
get: function proxyGetter () {
return self._data[key]
},
set: function proxySetter (val) {
self._data[key] = val
}
})
}
}
}
Component.prototype.__h__ = h
Component.nextTick = nextTick

88
src/observer/array.js Normal file
View File

@ -0,0 +1,88 @@
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
/**
* Intercept mutating methods and emit events
*/
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
// cache original method
var original = arrayProto[method]
def(arrayMethods, method, function mutator () {
// avoid leaking arguments:
// http://jsperf.com/closure-with-arguments
var i = arguments.length
var args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
var result = original.apply(this, args)
var ob = this.__ob__
var inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
/**
* Swap the element at the given index with a new value
* and emits corresponding event.
*
* @param {Number} index
* @param {*} val
* @return {*} - replaced element
*/
def(
arrayProto,
'$set',
function $set (index, val) {
if (index >= this.length) {
this.length = Number(index) + 1
}
return this.splice(index, 1, val)[0]
}
)
/**
* Convenience method to remove the element at given index or target element reference.
*
* @param {*} item
*/
def(
arrayProto,
'$remove',
function $remove (item) {
/* istanbul ignore if */
if (!this.length) return
var index = this.indexOf(item)
if (index > -1) {
return this.splice(index, 1)
}
}
)

107
src/observer/batcher.js Normal file
View File

@ -0,0 +1,107 @@
import config from '../config'
import {
warn,
nextTick,
devtools
} from '../util/index'
// we have two separate queues: one for directive updates
// and one for user watcher registered via $watch().
// we want to guarantee directive updates to be called
// before user watchers so that when user watchers are
// triggered, the DOM would have already been in updated
// state.
var queueIndex
var queue = []
var userQueue = []
var has = {}
var circular = {}
var waiting = false
var internalQueueDepleted = false
/**
* Reset the batcher's state.
*/
function resetBatcherState () {
queue = []
userQueue = []
has = {}
circular = {}
waiting = internalQueueDepleted = false
}
/**
* Flush both queues and run the watchers.
*/
function flushBatcherQueue () {
runBatcherQueue(queue)
internalQueueDepleted = true
runBatcherQueue(userQueue)
resetBatcherState()
}
/**
* Run the watchers in a single queue.
*
* @param {Array} queue
*/
function runBatcherQueue (queue) {
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (queueIndex = 0; queueIndex < queue.length; queueIndex++) {
var watcher = queue[queueIndex]
var id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > config._maxUpdateCount) {
warn(
'You may have an infinite update loop for watcher ' +
'with expression "' + watcher.expression + '"',
watcher.vm
)
break
}
}
}
}
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*
* @param {Watcher} watcher
* properties:
* - {Number} id
* - {Function} run
*/
export function pushWatcher (watcher) {
var id = watcher.id
if (has[id] == null) {
if (internalQueueDepleted && !watcher.user) {
// an internal watcher triggered by a user watcher...
// let's run it immediately after current user watcher is done.
userQueue.splice(queueIndex + 1, 0, watcher)
} else {
// push watcher into appropriate queue
var q = watcher.user
? userQueue
: queue
has[id] = q.length
q.push(watcher)
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushBatcherQueue)
}
}
}
}

58
src/observer/dep.js Normal file
View File

@ -0,0 +1,58 @@
let uid = 0
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*
* @constructor
*/
export default function Dep () {
this.id = uid++
this.subs = []
}
// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null
/**
* Add a directive subscriber.
*
* @param {Directive} sub
*/
Dep.prototype.addSub = function (sub) {
this.subs.push(sub)
}
/**
* Remove a directive subscriber.
*
* @param {Directive} sub
*/
Dep.prototype.removeSub = function (sub) {
this.subs.$remove(sub)
}
/**
* Add self as a dependency to the target watcher.
*/
Dep.prototype.depend = function () {
Dep.target.addDep(this)
}
/**
* Notify all subscribers of a new value.
*/
Dep.prototype.notify = function () {
// stablize the subscriber list first
var subs = this.subs.slice()
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}

240
src/observer/index.js Normal file
View File

@ -0,0 +1,240 @@
import Dep from './dep'
import { arrayMethods } from './array'
import {
def,
isArray,
isPlainObject,
hasProto,
hasOwn
} from '../util/index'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
/**
* By default, when a reactive property is set, the new value is
* also converted to become reactive. However in certain cases, e.g.
* v-for scope alias and props, we don't want to force conversion
* because the value may be a nested value under a frozen data structure.
*
* So whenever we want to set a reactive property without forcing
* conversion on the new value, we wrap that call inside this function.
*/
let shouldConvert = true
export function withoutConversion (fn) {
shouldConvert = false
fn()
shouldConvert = true
}
/**
* Observer class that are attached to each observed
* object. Once attached, the observer converts target
* object's property keys into getter/setters that
* collect dependencies and dispatches updates.
*
* @param {Array|Object} value
* @constructor
*/
export function Observer (value) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
// Instance methods
/**
* Walk through each property and convert them into
* getter/setters. This method should only be called when
* value type is Object.
*
* @param {Object} obj
*/
Observer.prototype.walk = function (obj) {
var keys = Object.keys(obj)
for (var i = 0, l = keys.length; i < l; i++) {
this.convert(keys[i], obj[keys[i]])
}
}
/**
* Observe a list of Array items.
*
* @param {Array} items
*/
Observer.prototype.observeArray = function (items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
/**
* Convert a property into getter/setter so we can emit
* the events when the property is accessed/changed.
*
* @param {String} key
* @param {*} val
*/
Observer.prototype.convert = function (key, val) {
defineReactive(this.value, key, val)
}
/**
* Add an owner vm, so that when $set/$delete mutations
* happen we can notify owner vms to proxy the keys and
* digest the watchers. This is only called when the object
* is observed as an instance's root $data.
*
* @param {Vue} vm
*/
Observer.prototype.addVm = function (vm) {
(this.vms || (this.vms = [])).push(vm)
}
/**
* Remove an owner vm. This is called when the object is
* swapped out as an instance's $data object.
*
* @param {Vue} vm
*/
Observer.prototype.removeVm = function (vm) {
this.vms.$remove(vm)
}
// helpers
/**
* Augment an target Object or Array by intercepting
* the prototype chain using __proto__
*
* @param {Object|Array} target
* @param {Object} src
*/
function protoAugment (target, src) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
/**
* Augment an target Object or Array by defining
* hidden properties.
*
* @param {Object|Array} target
* @param {Object} proto
*/
function copyAugment (target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i]
def(target, key, src[key])
}
}
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*
* @param {*} value
* @param {Vue} [vm]
* @return {Observer|undefined}
* @static
*/
export function observe (value, vm) {
if (!value || typeof value !== 'object') {
return
}
var ob
if (
hasOwn(value, '__ob__') &&
value.__ob__ instanceof Observer
) {
ob = value.__ob__
} else if (
shouldConvert &&
(isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (ob && vm) {
ob.addVm(vm)
}
return ob
}
/**
* Define a reactive property on an Object.
*
* @param {Object} obj
* @param {String} key
* @param {*} val
*/
export function defineReactive (obj, key, val) {
var dep = new Dep()
var property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get
var setter = property && property.set
var childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
if (isArray(value)) {
for (var e, i = 0, l = value.length; i < l; i++) {
e = value[i]
e && e.__ob__ && e.__ob__.dep.depend()
}
}
}
return value
},
set: function reactiveSetter (newVal) {
var value = getter ? getter.call(obj) : val
if (newVal === value) {
return
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
}

319
src/observer/watcher.js Normal file
View File

@ -0,0 +1,319 @@
import config from '../config'
import Dep from './dep'
import { pushWatcher } from './batcher'
import {
extend,
warn,
isArray,
isObject,
nextTick
} from '../util/index'
let uid = 0
/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*
* @param {Vue} vm
* @param {String|Function} expOrFn
* @param {Function} cb
* @param {Object} options
* - {Array} filters
* - {Boolean} twoWay
* - {Boolean} deep
* - {Boolean} user
* - {Boolean} sync
* - {Boolean} lazy
* - {Function} [preProcess]
* - {Function} [postProcess]
* @constructor
*/
export default function Watcher (vm, expOrFn, cb, options) {
// mix in options
if (options) {
extend(this, options)
}
var isFn = typeof expOrFn === 'function'
this.vm = vm
vm._watchers.push(this)
this.expression = expOrFn
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = Object.create(null)
this.newDepIds = null
this.prevError = null // for async error stacks
// parse expression for getter/setter
if (isFn) {
this.getter = expOrFn
this.setter = undefined
} else {
warn('vue-lite only supports watching functions.')
}
this.value = this.lazy
? undefined
: this.get()
// state for avoiding false triggers for deep and Array
// watchers during vm._digest()
this.queued = this.shallow = false
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
Watcher.prototype.get = function () {
this.beforeGet()
var scope = this.scope || this.vm
var value
try {
value = this.getter.call(scope, scope)
} catch (e) {
if (
process.env.NODE_ENV !== 'production' &&
config.warnExpressionErrors
) {
warn(
'Error when evaluating expression ' +
'"' + this.expression + '": ' + e.toString(),
this.vm
)
}
}
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
if (this.preProcess) {
value = this.preProcess(value)
}
if (this.filters) {
value = scope._applyFilters(value, null, this.filters, false)
}
if (this.postProcess) {
value = this.postProcess(value)
}
this.afterGet()
return value
}
/**
* Set the corresponding value with the setter.
*
* @param {*} value
*/
Watcher.prototype.set = function (value) {
var scope = this.scope || this.vm
if (this.filters) {
value = scope._applyFilters(
value, this.value, this.filters, true)
}
try {
this.setter.call(scope, scope, value)
} catch (e) {
if (
process.env.NODE_ENV !== 'production' &&
config.warnExpressionErrors
) {
warn(
'Error when evaluating setter ' +
'"' + this.expression + '": ' + e.toString(),
this.vm
)
}
}
}
/**
* Prepare for dependency collection.
*/
Watcher.prototype.beforeGet = function () {
Dep.target = this
this.newDepIds = Object.create(null)
this.newDeps.length = 0
}
/**
* Add a dependency to this directive.
*
* @param {Dep} dep
*/
Watcher.prototype.addDep = function (dep) {
var id = dep.id
if (!this.newDepIds[id]) {
this.newDepIds[id] = true
this.newDeps.push(dep)
if (!this.depIds[id]) {
dep.addSub(this)
}
}
}
/**
* Clean up for dependency collection.
*/
Watcher.prototype.afterGet = function () {
Dep.target = null
var i = this.deps.length
while (i--) {
var dep = this.deps[i]
if (!this.newDepIds[dep.id]) {
dep.removeSub(this)
}
}
this.depIds = this.newDepIds
var tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*
* @param {Boolean} shallow
*/
Watcher.prototype.update = function (shallow) {
if (this.lazy) {
this.dirty = true
} else if (this.sync || !config.async) {
this.run()
} else {
// if queued, only overwrite shallow with non-shallow,
// but not the other way around.
this.shallow = this.queued
? shallow
? this.shallow
: false
: !!shallow
this.queued = true
// record before-push error stack in debug mode
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.debug) {
this.prevError = new Error('[vue] async stack trace')
}
pushWatcher(this)
}
}
/**
* Batcher job interface.
* Will be called by the batcher.
*/
Watcher.prototype.run = function () {
if (this.active) {
var value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated; but only do so if this is a
// non-shallow update (caused by a vm digest).
((isObject(value) || this.deep) && !this.shallow)
) {
// set new value
var oldValue = this.value
this.value = value
// in debug + async mode, when a watcher callbacks
// throws, we also throw the saved before-push error
// so the full cross-tick stack trace is available.
var prevError = this.prevError
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' &&
config.debug && prevError) {
this.prevError = null
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
nextTick(function () {
throw prevError
}, 0)
throw e
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
this.queued = this.shallow = false
}
}
/**
* Evaluate the value of the watcher.
* This only gets called for lazy watchers.
*/
Watcher.prototype.evaluate = function () {
// avoid overwriting another watcher that is being
// collected.
var current = Dep.target
this.value = this.get()
this.dirty = false
Dep.target = current
}
/**
* Depend on all deps collected by this watcher.
*/
Watcher.prototype.depend = function () {
var i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
/**
* Remove self from all dependencies' subcriber list.
*/
Watcher.prototype.teardown = function () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed or is performing a v-for
// re-render (the watcher list is then filtered by v-for).
if (!this.vm._isBeingDestroyed && !this.vm._vForRemoving) {
this.vm._watchers.$remove(this)
}
var i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
this.vm = this.cb = this.value = null
}
}
/**
* Recrusively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*
* @param {*} val
*/
function traverse (val) {
var i, keys
if (isArray(val)) {
i = val.length
while (i--) traverse(val[i])
} else if (isObject(val)) {
keys = Object.keys(val)
i = keys.length
while (i--) traverse(val[keys[i]])
}
}

89
src/util/component.js Normal file
View File

@ -0,0 +1,89 @@
import { warn } from './debug'
import { resolveAsset } from './options'
import { getAttr, getBindAttr } from './dom'
export const commonTagRE = /^(div|p|span|img|a|b|i|br|ul|ol|li|h1|h2|h3|h4|h5|h6|code|pre|table|th|td|tr|form|label|input|select|option|nav|article|section|header|footer)$/i
export const reservedTagRE = /^(slot|partial|component)$/i
let isUnknownElement
if (process.env.NODE_ENV !== 'production') {
isUnknownElement = function (el, tag) {
if (tag.indexOf('-') > -1) {
// http://stackoverflow.com/a/28210364/1070244
return (
el.constructor === window.HTMLUnknownElement ||
el.constructor === window.HTMLElement
)
} else {
return (
/HTMLUnknownElement/.test(el.toString()) &&
// Chrome returns unknown for several HTML5 elements.
// https://code.google.com/p/chromium/issues/detail?id=540526
!/^(data|time|rtc|rb)$/.test(tag)
)
}
}
}
/**
* Check if an element is a component, if yes return its
* component id.
*
* @param {Element} el
* @param {Object} options
* @return {Object|undefined}
*/
export function checkComponentAttr (el, options) {
var tag = el.tagName.toLowerCase()
var hasAttrs = el.hasAttributes()
if (!commonTagRE.test(tag) && !reservedTagRE.test(tag)) {
if (resolveAsset(options, 'components', tag)) {
return { id: tag }
} else {
var is = hasAttrs && getIsBinding(el)
if (is) {
return is
} else if (process.env.NODE_ENV !== 'production') {
var expectedTag =
options._componentNameMap &&
options._componentNameMap[tag]
if (expectedTag) {
warn(
'Unknown custom element: <' + tag + '> - ' +
'did you mean <' + expectedTag + '>? ' +
'HTML is case-insensitive, remember to use kebab-case in templates.'
)
} else if (isUnknownElement(el, tag)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.'
)
}
}
}
} else if (hasAttrs) {
return getIsBinding(el)
}
}
/**
* Get "is" binding from an element.
*
* @param {Element} el
* @return {Object|undefined}
*/
function getIsBinding (el) {
// dynamic syntax
var exp = getAttr(el, 'is')
if (exp != null) {
return { id: exp }
} else {
exp = getBindAttr(el, 'is')
if (exp != null) {
return { id: exp, dynamic: true }
}
}
}

24
src/util/debug.js Normal file
View File

@ -0,0 +1,24 @@
import config from '../config'
import { hyphenate } from './lang'
let warn
let formatComponentName
if (process.env.NODE_ENV !== 'production') {
const hasConsole = typeof console !== 'undefined'
warn = (msg, vm) => {
if (hasConsole && (!config.silent)) {
console.error('[Vue warn]: ' + msg + (vm ? formatComponentName(vm) : ''))
}
}
formatComponentName = vm => {
var name = vm._isVue ? vm.$options.name : vm.name
return name
? ' (found in component: <' + hyphenate(name) + '>)'
: ''
}
}
export { warn }

388
src/util/dom.js Normal file
View File

@ -0,0 +1,388 @@
import config from '../config'
import { isIE9 } from './env'
import { warn } from './debug'
import { camelize } from './lang'
/**
* Query an element selector if it's not an element already.
*
* @param {String|Element} el
* @return {Element}
*/
export function query (el) {
if (typeof el === 'string') {
var selector = el
el = document.querySelector(el)
if (!el) {
process.env.NODE_ENV !== 'production' && warn(
'Cannot find element: ' + selector
)
}
}
return el
}
/**
* Check if a node is in the document.
* Note: document.documentElement.contains should work here
* but always returns false for comment nodes in phantomjs,
* making unit tests difficult. This is fixed by doing the
* contains() check on the node's parentNode instead of
* the node itself.
*
* @param {Node} node
* @return {Boolean}
*/
export function inDoc (node) {
var doc = document.documentElement
var parent = node && node.parentNode
return doc === node ||
doc === parent ||
!!(parent && parent.nodeType === 1 && (doc.contains(parent)))
}
/**
* Get and remove an attribute from a node.
*
* @param {Node} node
* @param {String} _attr
*/
export function getAttr (node, _attr) {
var val = node.getAttribute(_attr)
if (val !== null) {
node.removeAttribute(_attr)
}
return val
}
/**
* Get an attribute with colon or v-bind: prefix.
*
* @param {Node} node
* @param {String} name
* @return {String|null}
*/
export function getBindAttr (node, name) {
var val = getAttr(node, ':' + name)
if (val === null) {
val = getAttr(node, 'v-bind:' + name)
}
return val
}
/**
* Check the presence of a bind attribute.
*
* @param {Node} node
* @param {String} name
* @return {Boolean}
*/
export function hasBindAttr (node, name) {
return node.hasAttribute(name) ||
node.hasAttribute(':' + name) ||
node.hasAttribute('v-bind:' + name)
}
/**
* Insert el before target
*
* @param {Element} el
* @param {Element} target
*/
export function before (el, target) {
target.parentNode.insertBefore(el, target)
}
/**
* Insert el after target
*
* @param {Element} el
* @param {Element} target
*/
export function after (el, target) {
if (target.nextSibling) {
before(el, target.nextSibling)
} else {
target.parentNode.appendChild(el)
}
}
/**
* Remove el from DOM
*
* @param {Element} el
*/
export function remove (el) {
el.parentNode.removeChild(el)
}
/**
* Prepend el to target
*
* @param {Element} el
* @param {Element} target
*/
export function prepend (el, target) {
if (target.firstChild) {
before(el, target.firstChild)
} else {
target.appendChild(el)
}
}
/**
* Replace target with el
*
* @param {Element} target
* @param {Element} el
*/
export function replace (target, el) {
var parent = target.parentNode
if (parent) {
parent.replaceChild(el, target)
}
}
/**
* Add event listener shorthand.
*
* @param {Element} el
* @param {String} event
* @param {Function} cb
* @param {Boolean} [useCapture]
*/
export function on (el, event, cb, useCapture) {
el.addEventListener(event, cb, useCapture)
}
/**
* Remove event listener shorthand.
*
* @param {Element} el
* @param {String} event
* @param {Function} cb
*/
export function off (el, event, cb) {
el.removeEventListener(event, cb)
}
/**
* For IE9 compat: when both class and :class are present
* getAttribute('class') returns wrong value...
*
* @param {Element} el
* @return {String}
*/
function getClass (el) {
var classname = el.className
if (typeof classname === 'object') {
classname = classname.baseVal || ''
}
return classname
}
/**
* In IE9, setAttribute('class') will result in empty class
* if the element also has the :class attribute; However in
* PhantomJS, setting `className` does not work on SVG elements...
* So we have to do a conditional check here.
*
* @param {Element} el
* @param {String} cls
*/
export function setClass (el, cls) {
/* istanbul ignore if */
if (isIE9 && !/svg$/.test(el.namespaceURI)) {
el.className = cls
} else {
el.setAttribute('class', cls)
}
}
/**
* Add class with compatibility for IE & SVG
*
* @param {Element} el
* @param {String} cls
*/
export function addClass (el, cls) {
if (el.classList) {
el.classList.add(cls)
} else {
var cur = ' ' + getClass(el) + ' '
if (cur.indexOf(' ' + cls + ' ') < 0) {
setClass(el, (cur + cls).trim())
}
}
}
/**
* Remove class with compatibility for IE & SVG
*
* @param {Element} el
* @param {String} cls
*/
export function removeClass (el, cls) {
if (el.classList) {
el.classList.remove(cls)
} else {
var cur = ' ' + getClass(el) + ' '
var tar = ' ' + cls + ' '
while (cur.indexOf(tar) >= 0) {
cur = cur.replace(tar, ' ')
}
setClass(el, cur.trim())
}
if (!el.className) {
el.removeAttribute('class')
}
}
/**
* Extract raw content inside an element into a temporary
* container div
*
* @param {Element} el
* @param {Boolean} asFragment
* @return {Element|DocumentFragment}
*/
export function extractContent (el, asFragment) {
var child
var rawContent
/* istanbul ignore if */
if (isTemplate(el) && isFragment(el.content)) {
el = el.content
}
if (el.hasChildNodes()) {
trimNode(el)
rawContent = asFragment
? document.createDocumentFragment()
: document.createElement('div')
/* eslint-disable no-cond-assign */
while (child = el.firstChild) {
/* eslint-enable no-cond-assign */
rawContent.appendChild(child)
}
}
return rawContent
}
/**
* Trim possible empty head/tail text and comment
* nodes inside a parent.
*
* @param {Node} node
*/
export function trimNode (node) {
var child
/* eslint-disable no-sequences */
while (child = node.firstChild, isTrimmable(child)) {
node.removeChild(child)
}
while (child = node.lastChild, isTrimmable(child)) {
node.removeChild(child)
}
/* eslint-enable no-sequences */
}
function isTrimmable (node) {
return node && (
(node.nodeType === 3 && !node.data.trim()) ||
node.nodeType === 8
)
}
/**
* Check if an element is a template tag.
* Note if the template appears inside an SVG its tagName
* will be in lowercase.
*
* @param {Element} el
*/
export function isTemplate (el) {
return el.tagName &&
el.tagName.toLowerCase() === 'template'
}
/**
* Create an "anchor" for performing dom insertion/removals.
* This is used in a number of scenarios:
* - fragment instance
* - v-html
* - v-if
* - v-for
* - component
*
* @param {String} content
* @param {Boolean} persist - IE trashes empty textNodes on
* cloneNode(true), so in certain
* cases the anchor needs to be
* non-empty to be persisted in
* templates.
* @return {Comment|Text}
*/
export function createAnchor (content, persist) {
var anchor = config.debug
? document.createComment(content)
: document.createTextNode(persist ? ' ' : '')
anchor.__v_anchor = true
return anchor
}
/**
* Find a component ref attribute that starts with $.
*
* @param {Element} node
* @return {String|undefined}
*/
var refRE = /^v-ref:/
export function findRef (node) {
if (node.hasAttributes()) {
var attrs = node.attributes
for (var i = 0, l = attrs.length; i < l; i++) {
var name = attrs[i].name
if (refRE.test(name)) {
return camelize(name.replace(refRE, ''))
}
}
}
}
/**
* Get outerHTML of elements, taking care
* of SVG elements in IE as well.
*
* @param {Element} el
* @return {String}
*/
export function getOuterHTML (el) {
if (el.outerHTML) {
return el.outerHTML
} else {
var container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}

106
src/util/env.js Normal file
View File

@ -0,0 +1,106 @@
/* global MutationObserver */
// can we use __proto__?
export const hasProto = '__proto__' in {}
// Browser environment sniffing
export const inBrowser =
typeof window !== 'undefined' &&
Object.prototype.toString.call(window) !== '[object Object]'
// detect devtools
export const devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__
// UA sniffing for working around browser-specific quirks
const UA = inBrowser && window.navigator.userAgent.toLowerCase()
export const isIE9 = UA && UA.indexOf('msie 9.0') > 0
export const isAndroid = UA && UA.indexOf('android') > 0
let transitionProp
let transitionEndEvent
let animationProp
let animationEndEvent
// Transition property/event sniffing
if (inBrowser && !isIE9) {
const isWebkitTrans =
window.ontransitionend === undefined &&
window.onwebkittransitionend !== undefined
const isWebkitAnim =
window.onanimationend === undefined &&
window.onwebkitanimationend !== undefined
transitionProp = isWebkitTrans
? 'WebkitTransition'
: 'transition'
transitionEndEvent = isWebkitTrans
? 'webkitTransitionEnd'
: 'transitionend'
animationProp = isWebkitAnim
? 'WebkitAnimation'
: 'animation'
animationEndEvent = isWebkitAnim
? 'webkitAnimationEnd'
: 'animationend'
}
export {
transitionProp,
transitionEndEvent,
animationProp,
animationEndEvent
}
/**
* Defer a task to execute it asynchronously. Ideally this
* should be executed as a microtask, so we leverage
* MutationObserver if it's available, and fallback to
* setTimeout(0).
*
* @param {Function} cb
* @param {Object} ctx
*/
export const nextTick = (function () {
var callbacks = []
var pending = false
var timerFunc
function nextTickHandler () {
pending = false
var copies = callbacks.slice(0)
callbacks = []
for (var i = 0; i < copies.length; i++) {
copies[i]()
}
}
/* istanbul ignore if */
if (typeof MutationObserver !== 'undefined') {
var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(counter)
observer.observe(textNode, {
characterData: true
})
timerFunc = function () {
counter = (counter + 1) % 2
textNode.data = counter
}
} else {
// webpack attempts to inject a shim for setImmediate
// if it is used as a global, so we have to work around that to
// avoid bundling unnecessary code.
const context = inBrowser
? window
: typeof global !== 'undefined' ? global : {}
timerFunc = context.setImmediate || setTimeout
}
return function (cb, ctx) {
var func = ctx
? function () { cb.call(ctx) }
: cb
callbacks.push(func)
if (pending) return
pending = true
timerFunc(nextTickHandler, 0)
}
})()

7
src/util/index.js Normal file
View File

@ -0,0 +1,7 @@
export * from './lang'
export * from './env'
export * from './dom'
export * from './options'
export * from './component'
export * from './debug'
export { defineReactive } from '../observer/index'

407
src/util/lang.js Normal file
View File

@ -0,0 +1,407 @@
export function isPrimitive (s) {
return typeof s === 'string' || typeof s === 'number'
}
/**
* Set a property on an object. Adds the new property and
* triggers change notification if the property doesn't
* already exist.
*
* @param {Object} obj
* @param {String} key
* @param {*} val
* @public
*/
export function set (obj, key, val) {
if (hasOwn(obj, key)) {
obj[key] = val
return
}
if (obj._isVue) {
set(obj._data, key, val)
return
}
var ob = obj.__ob__
if (!ob) {
obj[key] = val
return
}
ob.convert(key, val)
ob.dep.notify()
if (ob.vms) {
var i = ob.vms.length
while (i--) {
var vm = ob.vms[i]
vm._proxy(key)
vm._digest()
}
}
return val
}
/**
* Delete a property and trigger change if necessary.
*
* @param {Object} obj
* @param {String} key
*/
export function del (obj, key) {
if (!hasOwn(obj, key)) {
return
}
delete obj[key]
var ob = obj.__ob__
if (!ob) {
return
}
ob.dep.notify()
if (ob.vms) {
var i = ob.vms.length
while (i--) {
var vm = ob.vms[i]
vm._unproxy(key)
vm._digest()
}
}
}
var hasOwnProperty = Object.prototype.hasOwnProperty
/**
* Check whether the object has the property.
*
* @param {Object} obj
* @param {String} key
* @return {Boolean}
*/
export function hasOwn (obj, key) {
return hasOwnProperty.call(obj, key)
}
/**
* Check if an expression is a literal value.
*
* @param {String} exp
* @return {Boolean}
*/
var literalValueRE = /^\s?(true|false|-?[\d\.]+|'[^']*'|"[^"]*")\s?$/
export function isLiteral (exp) {
return literalValueRE.test(exp)
}
/**
* Check if a string starts with $ or _
*
* @param {String} str
* @return {Boolean}
*/
export function isReserved (str) {
var c = (str + '').charCodeAt(0)
return c === 0x24 || c === 0x5F
}
/**
* Guard text output, make sure undefined outputs
* empty string
*
* @param {*} value
* @return {String}
*/
export function _toString (value) {
return value == null
? ''
: value.toString()
}
/**
* Check and convert possible numeric strings to numbers
* before setting back to data
*
* @param {*} value
* @return {*|Number}
*/
export function toNumber (value) {
if (typeof value !== 'string') {
return value
} else {
var parsed = Number(value)
return isNaN(parsed)
? value
: parsed
}
}
/**
* Convert string boolean literals into real booleans.
*
* @param {*} value
* @return {*|Boolean}
*/
export function toBoolean (value) {
return value === 'true'
? true
: value === 'false'
? false
: value
}
/**
* Strip quotes from a string
*
* @param {String} str
* @return {String | false}
*/
export function stripQuotes (str) {
var a = str.charCodeAt(0)
var b = str.charCodeAt(str.length - 1)
return a === b && (a === 0x22 || a === 0x27)
? str.slice(1, -1)
: str
}
/**
* Camelize a hyphen-delmited string.
*
* @param {String} str
* @return {String}
*/
var camelizeRE = /-(\w)/g
export function camelize (str) {
return str.replace(camelizeRE, toUpper)
}
function toUpper (_, c) {
return c ? c.toUpperCase() : ''
}
/**
* Hyphenate a camelCase string.
*
* @param {String} str
* @return {String}
*/
var hyphenateRE = /([a-z\d])([A-Z])/g
export function hyphenate (str) {
return str
.replace(hyphenateRE, '$1-$2')
.toLowerCase()
}
/**
* Converts hyphen/underscore/slash delimitered names into
* camelized classNames.
*
* e.g. my-component => MyComponent
* some_else => SomeElse
* some/comp => SomeComp
*
* @param {String} str
* @return {String}
*/
var classifyRE = /(?:^|[-_\/])(\w)/g
export function classify (str) {
return str.replace(classifyRE, toUpper)
}
/**
* Simple bind, faster than native
*
* @param {Function} fn
* @param {Object} ctx
* @return {Function}
*/
export function bind (fn, ctx) {
return function (a) {
var l = arguments.length
return l
? l > 1
? fn.apply(ctx, arguments)
: fn.call(ctx, a)
: fn.call(ctx)
}
}
/**
* Convert an Array-like object to a real Array.
*
* @param {Array-like} list
* @param {Number} [start] - start index
* @return {Array}
*/
export function toArray (list, start) {
start = start || 0
var i = list.length - start
var ret = new Array(i)
while (i--) {
ret[i] = list[i + start]
}
return ret
}
/**
* Mix properties into target object.
*
* @param {Object} to
* @param {Object} from
*/
export function extend (to, from) {
var keys = Object.keys(from)
var i = keys.length
while (i--) {
to[keys[i]] = from[keys[i]]
}
return to
}
/**
* Quick object check - this is primarily used to tell
* Objects from primitive values when we know the value
* is a JSON-compliant type.
*
* @param {*} obj
* @return {Boolean}
*/
export function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
/**
* Strict object type check. Only returns true
* for plain JavaScript objects.
*
* @param {*} obj
* @return {Boolean}
*/
var toString = Object.prototype.toString
var OBJECT_STRING = '[object Object]'
export function isPlainObject (obj) {
return toString.call(obj) === OBJECT_STRING
}
/**
* Array type check.
*
* @param {*} obj
* @return {Boolean}
*/
export const isArray = Array.isArray
/**
* Define a property.
*
* @param {Object} obj
* @param {String} key
* @param {*} val
* @param {Boolean} [enumerable]
*/
export function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
/**
* Debounce a function so it only gets called after the
* input stops arriving after the given wait period.
*
* @param {Function} func
* @param {Number} wait
* @return {Function} - the debounced function
*/
export function debounce (func, wait) {
var timeout, args, context, timestamp, result
var later = function () {
var last = Date.now() - timestamp
if (last < wait && last >= 0) {
timeout = setTimeout(later, wait - last)
} else {
timeout = null
result = func.apply(context, args)
if (!timeout) context = args = null
}
}
return function () {
context = this
args = arguments
timestamp = Date.now()
if (!timeout) {
timeout = setTimeout(later, wait)
}
return result
}
}
/**
* Manual indexOf because it's slightly faster than
* native.
*
* @param {Array} arr
* @param {*} obj
*/
export function indexOf (arr, obj) {
var i = arr.length
while (i--) {
if (arr[i] === obj) return i
}
return -1
}
/**
* Make a cancellable version of an async callback.
*
* @param {Function} fn
* @return {Function}
*/
export function cancellable (fn) {
var cb = function () {
if (!cb.cancelled) {
return fn.apply(this, arguments)
}
}
cb.cancel = function () {
cb.cancelled = true
}
return cb
}
/**
* Check if two values are loosely equal - that is,
* if they are plain objects, do they have the same shape?
*
* @param {*} a
* @param {*} b
* @return {Boolean}
*/
export function looseEqual (a, b) {
/* eslint-disable eqeqeq */
return a == b || (
isObject(a) && isObject(b)
? JSON.stringify(a) === JSON.stringify(b)
: false
)
/* eslint-enable eqeqeq */
}

381
src/util/options.js Normal file
View File

@ -0,0 +1,381 @@
import Vue from '../instance/index'
import config from '../config'
import {
extend,
set,
isObject,
isArray,
isPlainObject,
hasOwn,
camelize,
hyphenate
} from './lang'
import { warn } from './debug'
import { commonTagRE, reservedTagRE } from './component'
/**
* Option overwriting strategies are functions that handle
* how to merge a parent option value and a child option
* value into the final value.
*
* All strategy functions follow the same signature:
*
* @param {*} parentVal
* @param {*} childVal
* @param {Vue} [vm]
*/
var strats = config.optionMergeStrategies = Object.create(null)
/**
* Helper that recursively merges two data objects together.
*/
function mergeData (to, from) {
var key, toVal, fromVal
for (key in from) {
toVal = to[key]
fromVal = from[key]
if (!hasOwn(to, key)) {
set(to, key, fromVal)
} else if (isObject(toVal) && isObject(fromVal)) {
mergeData(toVal, fromVal)
}
}
return to
}
/**
* Data
*/
strats.data = function (parentVal, childVal, vm) {
if (!vm) {
// in a Vue.extend merge, both should be functions
if (!childVal) {
return parentVal
}
if (typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return parentVal
}
if (!parentVal) {
return childVal
}
// when parentVal & childVal are both present,
// we need to return a function that returns the
// merged result of both functions... no need to
// check if parentVal is a function here because
// it has to be a function to pass previous merges.
return function mergedDataFn () {
return mergeData(
childVal.call(this),
parentVal.call(this)
)
}
} else if (parentVal || childVal) {
return function mergedInstanceDataFn () {
// instance merge
var instanceData = typeof childVal === 'function'
? childVal.call(vm)
: childVal
var defaultData = typeof parentVal === 'function'
? parentVal.call(vm)
: undefined
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
/**
* El
*/
strats.el = function (parentVal, childVal, vm) {
if (!vm && childVal && typeof childVal !== 'function') {
process.env.NODE_ENV !== 'production' && warn(
'The "el" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
)
return
}
var ret = childVal || parentVal
// invoke the element factory if this is instance merge
return vm && typeof ret === 'function'
? ret.call(vm)
: ret
}
/**
* Hooks and param attributes are merged as arrays.
*/
strats.init =
strats.created =
strats.ready =
strats.attached =
strats.detached =
strats.beforeCompile =
strats.compiled =
strats.beforeDestroy =
strats.destroyed =
strats.activate = function (parentVal, childVal) {
return childVal
? parentVal
? parentVal.concat(childVal)
: isArray(childVal)
? childVal
: [childVal]
: parentVal
}
/**
* Assets
*
* When a vm is present (instance creation), we need to do
* a three-way merge between constructor options, instance
* options and parent options.
*/
function mergeAssets (parentVal, childVal) {
var res = Object.create(parentVal)
return childVal
? extend(res, guardArrayAssets(childVal))
: res
}
config._assetTypes.forEach(function (type) {
strats[type + 's'] = mergeAssets
})
/**
* Events & Watchers.
*
* Events & watchers hashes should not overwrite one
* another, so we merge them as arrays.
*/
strats.watch =
strats.events = function (parentVal, childVal) {
if (!childVal) return parentVal
if (!parentVal) return childVal
var ret = {}
extend(ret, parentVal)
for (var key in childVal) {
var parent = ret[key]
var child = childVal[key]
if (parent && !isArray(parent)) {
parent = [parent]
}
ret[key] = parent
? parent.concat(child)
: [child]
}
return ret
}
/**
* Other object hashes.
*/
strats.props =
strats.methods =
strats.computed = function (parentVal, childVal) {
if (!childVal) return parentVal
if (!parentVal) return childVal
var ret = Object.create(null)
extend(ret, parentVal)
extend(ret, childVal)
return ret
}
/**
* Default strategy.
*/
var defaultStrat = function (parentVal, childVal) {
return childVal === undefined
? parentVal
: childVal
}
/**
* Make sure component options get converted to actual
* constructors.
*
* @param {Object} options
*/
function guardComponents (options) {
if (options.components) {
var components = options.components =
guardArrayAssets(options.components)
var ids = Object.keys(components)
var def
if (process.env.NODE_ENV !== 'production') {
var map = options._componentNameMap = {}
}
for (var i = 0, l = ids.length; i < l; i++) {
var key = ids[i]
if (commonTagRE.test(key) || reservedTagRE.test(key)) {
process.env.NODE_ENV !== 'production' && warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + key
)
continue
}
// record a all lowercase <-> kebab-case mapping for
// possible custom element case error warning
if (process.env.NODE_ENV !== 'production') {
map[key.replace(/-/g, '').toLowerCase()] = hyphenate(key)
}
def = components[key]
if (isPlainObject(def)) {
components[key] = Vue.extend(def)
}
}
}
}
/**
* Ensure all props option syntax are normalized into the
* Object-based format.
*
* @param {Object} options
*/
function guardProps (options) {
var props = options.props
var i, val
if (isArray(props)) {
options.props = {}
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
options.props[val] = null
} else if (val.name) {
options.props[val.name] = val
}
}
} else if (isPlainObject(props)) {
var keys = Object.keys(props)
i = keys.length
while (i--) {
val = props[keys[i]]
if (typeof val === 'function') {
props[keys[i]] = { type: val }
}
}
}
}
/**
* Guard an Array-format assets option and converted it
* into the key-value Object format.
*
* @param {Object|Array} assets
* @return {Object}
*/
function guardArrayAssets (assets) {
if (isArray(assets)) {
var res = {}
var i = assets.length
var asset
while (i--) {
asset = assets[i]
var id = typeof asset === 'function'
? ((asset.options && asset.options.name) || asset.id)
: (asset.name || asset.id)
if (!id) {
process.env.NODE_ENV !== 'production' && warn(
'Array-syntax assets must provide a "name" or "id" field.'
)
} else {
res[id] = asset
}
}
return res
}
return assets
}
/**
* Merge two option objects into a new one.
* Core utility used in both instantiation and inheritance.
*
* @param {Object} parent
* @param {Object} child
* @param {Vue} [vm] - if vm is present, indicates this is
* an instantiation merge.
*/
export function mergeOptions (parent, child, vm) {
guardComponents(child)
guardProps(child)
var options = {}
var key
if (child.mixins) {
for (var i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
for (key in parent) {
mergeField(key)
}
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key)
}
}
function mergeField (key) {
var strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key)
}
return options
}
/**
* Resolve an asset.
* This function is used because child instances need access
* to assets defined in its ancestor chain.
*
* @param {Object} options
* @param {String} type
* @param {String} id
* @param {Boolean} warnMissing
* @return {Object|Function}
*/
export function resolveAsset (options, type, id, warnMissing) {
/* istanbul ignore if */
if (typeof id !== 'string') {
return
}
var assets = options[type]
var camelizedId
var res = assets[id] ||
// camelCase ID
assets[camelizedId = camelize(id)] ||
// Pascal Case ID
assets[camelizedId.charAt(0).toUpperCase() + camelizedId.slice(1)]
if (process.env.NODE_ENV !== 'production' && warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
)
}
return res
}

39
src/vdom/dom.js Normal file
View File

@ -0,0 +1,39 @@
export function createElement(tagName){
return document.createElement(tagName)
}
export function createElementNS(namespaceURI, qualifiedName){
return document.createElementNS(namespaceURI, qualifiedName)
}
export function createTextNode(text){
return document.createTextNode(text)
}
export function insertBefore(parentNode, newNode, referenceNode){
parentNode.insertBefore(newNode, referenceNode)
}
export function removeChild(node, child){
node.removeChild(child)
}
export function appendChild(node, child){
node.appendChild(child)
}
export function parentNode(node){
return node.parentElement
}
export function nextSibling(node){
return node.nextSibling
}
export function tagName(node){
return node.tagName
}
export function setTextContent(node, text){
node.textContent = text
}

33
src/vdom/h.js Normal file
View File

@ -0,0 +1,33 @@
import VNode from './vnode'
import { isPrimitive, isArray } from '../util/index'
function addNS(data, children) {
data.ns = 'http://www.w3.org/2000/svg'
if (children !== undefined) {
for (var i = 0; i < children.length; ++i) {
addNS(children[i].data, children[i].children)
}
}
}
export default function h (tag, b, c) {
var data = {}, children, text, i
if (arguments.length === 3) {
data = b
if (isArray(c)) { children = c }
else if (isPrimitive(c)) { text = c }
} else if (arguments.length === 2) {
if (isArray(b)) { children = b }
else if (isPrimitive(b)) { text = b }
else { data = b }
}
if (isArray(children)) {
for (i = 0; i < children.length; ++i) {
if (isPrimitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i])
}
}
if (tag === 'svg') {
addNS(data, children)
}
return VNode(tag, data, children, text, undefined)
}

17
src/vdom/index.js Normal file
View File

@ -0,0 +1,17 @@
import createPatchFunction from './patch'
import h from './h'
import _class from './modules/class'
import style from './modules/style'
import props from './modules/props'
import attrs from './modules/attrs'
import events from './modules/events'
const patch = createPatchFunction([
_class, // makes it easy to toggle classes
props,
style,
attrs,
events
])
export { patch, h }

42
src/vdom/modules/attrs.js Normal file
View File

@ -0,0 +1,42 @@
var booleanAttrs = ["allowfullscreen", "async", "autofocus", "autoplay", "checked", "compact", "controls", "declare",
"default", "defaultchecked", "defaultmuted", "defaultselected", "defer", "disabled", "draggable",
"enabled", "formnovalidate", "hidden", "indeterminate", "inert", "ismap", "itemscope", "loop", "multiple",
"muted", "nohref", "noresize", "noshade", "novalidate", "nowrap", "open", "pauseonexit", "readonly",
"required", "reversed", "scoped", "seamless", "selected", "sortable", "spellcheck", "translate",
"truespeed", "typemustmatch", "visible"]
var booleanAttrsDict = {}
for(var i=0, len = booleanAttrs.length; i < len; i++) {
booleanAttrsDict[booleanAttrs[i]] = true
}
function updateAttrs(oldVnode, vnode) {
var key, cur, old, elm = vnode.elm,
oldAttrs = oldVnode.data.attrs || {}, attrs = vnode.data.attrs || {}
// update modified attributes, add new attributes
for (key in attrs) {
cur = attrs[key]
old = oldAttrs[key]
if (old !== cur) {
// TODO: add support to namespaced attributes (setAttributeNS)
if(!cur && booleanAttrsDict[key])
elm.removeAttribute(key)
else
elm.setAttribute(key, cur)
}
}
//remove removed attributes
// use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value)
// the other option is to remove all attributes with value == undefined
for (key in oldAttrs) {
if (!(key in attrs)) {
elm.removeAttribute(key)
}
}
}
export default {
create: updateAttrs,
update: updateAttrs
}

12
src/vdom/modules/class.js Normal file
View File

@ -0,0 +1,12 @@
import { setClass } from '../../util/index'
function updateClass (oldVnode, vnode) {
if (vnode.data.class !== undefined) {
setClass(vnode.elm, vnode.data.class || '')
}
}
export default {
init: updateClass,
update: updateClass
}

View File

@ -0,0 +1,42 @@
function arrInvoker(arr) {
return function() {
// Special case when length is two, for performance
arr.length === 2 ? arr[0](arr[1]) : arr[0].apply(undefined, arr.slice(1))
}
}
function fnInvoker(o) {
return function(ev) { o.fn(ev) }
}
function updateEventListeners(oldVnode, vnode) {
var name, cur, old, elm = vnode.elm,
oldOn = oldVnode.data.on || {}, on = vnode.data.on
if (!on) return
for (name in on) {
cur = on[name]
old = oldOn[name]
if (old === undefined) {
if (Array.isArray(cur)) {
elm.addEventListener(name, arrInvoker(cur))
} else {
cur = {fn: cur}
on[name] = cur
elm.addEventListener(name, fnInvoker(cur))
}
} else if (Array.isArray(old)) {
// Deliberately modify old array since it's captured in closure created with `arrInvoker`
old.length = cur.length
for (var i = 0; i < old.length; ++i) old[i] = cur[i]
on[name] = old
} else {
old.fn = cur
on[name] = old
}
}
}
export default {
create: updateEventListeners,
update: updateEventListeners
}

21
src/vdom/modules/props.js Normal file
View File

@ -0,0 +1,21 @@
function updateProps(oldVnode, vnode) {
var key, cur, old, elm = vnode.elm,
oldProps = oldVnode.data.props || {}, props = vnode.data.props || {}
for (key in oldProps) {
if (!props[key]) {
delete elm[key]
}
}
for (key in props) {
cur = props[key]
old = oldProps[key]
if (old !== cur && (key !== 'value' || elm[key] !== cur)) {
elm[key] = cur
}
}
}
export default {
create: updateProps,
update: updateProps
}

73
src/vdom/modules/style.js Normal file
View File

@ -0,0 +1,73 @@
// TODO:
// - remove animation related bits
// - include prefix sniffing of v-bind:style
var raf = (typeof window !== 'undefined' && window.requestAnimationFrame) || setTimeout
var nextFrame = function(fn) { raf(function() { raf(fn) }) }
function setNextFrame(obj, prop, val) {
nextFrame(function() { obj[prop] = val })
}
function updateStyle(oldVnode, vnode) {
var cur, name, elm = vnode.elm,
oldStyle = oldVnode.data.style || {},
style = vnode.data.style || {},
oldHasDel = 'delayed' in oldStyle
for (name in oldStyle) {
if (!style[name]) {
elm.style[name] = ''
}
}
for (name in style) {
cur = style[name]
if (name === 'delayed') {
for (name in style.delayed) {
cur = style.delayed[name]
if (!oldHasDel || cur !== oldStyle.delayed[name]) {
setNextFrame(elm.style, name, cur)
}
}
} else if (name !== 'remove' && cur !== oldStyle[name]) {
elm.style[name] = cur
}
}
}
function applyDestroyStyle(vnode) {
var style, name, elm = vnode.elm, s = vnode.data.style
if (!s || !(style = s.destroy)) return
for (name in style) {
elm.style[name] = style[name]
}
}
function applyRemoveStyle(vnode, rm) {
var s = vnode.data.style
if (!s || !s.remove) {
rm()
return
}
var name, elm = vnode.elm, idx, i = 0, maxDur = 0,
compStyle, style = s.remove, amount = 0, applied = []
for (name in style) {
applied.push(name)
elm.style[name] = style[name]
}
compStyle = getComputedStyle(elm)
var props = compStyle['transition-property'].split(', ')
for (; i < props.length; ++i) {
if(applied.indexOf(props[i]) !== -1) amount++
}
elm.addEventListener('transitionend', function(ev) {
if (ev.target === elm) --amount
if (amount === 0) rm()
})
}
export default {
create: updateStyle,
update: updateStyle,
destroy: applyDestroyStyle,
remove: applyRemoveStyle
}

258
src/vdom/patch.js Normal file
View File

@ -0,0 +1,258 @@
import VNode from './vnode'
import * as dom from './dom'
import { isPrimitive } from '../util/index'
const emptyNode = VNode('', {}, [], undefined, undefined)
const hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post']
function isUndef (s) {
return s === undefined
}
function isDef (s) {
return s !== undefined
}
function sameVnode (vnode1, vnode2) {
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel
}
function createKeyToOldIdx (children, beginIdx, endIdx) {
var i, map = {}, key
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
export default function createPatchFunction (modules, api) {
var i, j, cbs = {}
if (isUndef(api)) api = dom
for (i = 0; i < hooks.length; ++i) {
cbs[hooks[i]] = []
for (j = 0; j < modules.length; ++j) {
if (modules[j][hooks[i]] !== undefined) cbs[hooks[i]].push(modules[j][hooks[i]])
}
}
function emptyNodeAt (elm) {
return VNode(api.tagName(elm).toLowerCase(), {}, [], undefined, elm)
}
function createRmCb (childElm, listeners) {
return function() {
if (--listeners === 0) {
var parent = api.parentNode(childElm)
api.removeChild(parent, childElm)
}
}
}
function createElm (vnode, insertedVnodeQueue) {
var i, thunk, data = vnode.data
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode)
if (isDef(i = data.vnode)) {
thunk = vnode
vnode = i
}
}
var elm, children = vnode.children, tag = vnode.sel
if (isDef(tag)) {
elm = vnode.elm = isDef(data) && isDef(i = data.ns)
? api.createElementNS(i, tag)
: api.createElement(tag)
if (Array.isArray(children)) {
for (i = 0; i < children.length; ++i) {
api.appendChild(elm, createElm(children[i], insertedVnodeQueue))
}
} else if (isPrimitive(vnode.text)) {
api.appendChild(elm, api.createTextNode(vnode.text))
}
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
i = vnode.data.hook // Reuse variable
if (isDef(i)) {
if (i.create) i.create(emptyNode, vnode)
if (i.insert) insertedVnodeQueue.push(vnode)
}
} else {
elm = vnode.elm = api.createTextNode(vnode.text)
}
if (isDef(thunk)) thunk.elm = vnode.elm
return vnode.elm
}
function addVnodes (parentElm, before, vnodes, startIdx, endIdx, insertedVnodeQueue) {
for (; startIdx <= endIdx; ++startIdx) {
api.insertBefore(parentElm, createElm(vnodes[startIdx], insertedVnodeQueue), before)
}
}
function invokeDestroyHook (vnode) {
var i, j, data = vnode.data
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
if (isDef(i = vnode.children)) {
for (j = 0; j < vnode.children.length; ++j) {
invokeDestroyHook(vnode.children[j])
}
}
if (isDef(i = data.vnode)) invokeDestroyHook(i)
}
}
function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
for (; startIdx <= endIdx; ++startIdx) {
var i, listeners, rm, ch = vnodes[startIdx]
if (isDef(ch)) {
if (isDef(ch.sel)) {
invokeDestroyHook(ch)
listeners = cbs.remove.length + 1
rm = createRmCb(ch.elm, listeners)
for (i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
if (isDef(i = ch.data) && isDef(i = i.hook) && isDef(i = i.remove)) {
i(ch, rm)
} else {
rm()
}
} else { // Text node
api.removeChild(parentElm, ch.elm)
}
}
}
}
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue) {
var oldStartIdx = 0, newStartIdx = 0
var oldEndIdx = oldCh.length - 1
var oldStartVnode = oldCh[0]
var oldEndVnode = oldCh[oldEndIdx]
var newEndIdx = newCh.length - 1
var newStartVnode = newCh[0]
var newEndVnode = newCh[newEndIdx]
var oldKeyToIdx, idxInOld, elmToMove, before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = oldKeyToIdx[newStartVnode.key]
if (isUndef(idxInOld)) { // New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
elmToMove = oldCh[idxInOld]
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
oldCh[idxInOld] = undefined
api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx+1].elm
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
function patchVnode (oldVnode, vnode, insertedVnodeQueue) {
var i, hook
if (isDef(i = vnode.data) && isDef(hook = i.hook) && isDef(i = hook.prepatch)) {
i(oldVnode, vnode)
}
if (isDef(i = oldVnode.data) && isDef(i = i.vnode)) oldVnode = i
if (isDef(i = vnode.data) && isDef(i = i.vnode)) {
patchVnode(oldVnode, i, insertedVnodeQueue)
vnode.elm = i.elm
return
}
var elm = vnode.elm = oldVnode.elm, oldCh = oldVnode.children, ch = vnode.children
if (oldVnode === vnode) return
if (!sameVnode(oldVnode, vnode)) {
var parentElm = api.parentNode(oldVnode.elm)
elm = createElm(vnode, insertedVnodeQueue)
api.insertBefore(parentElm, elm, oldVnode.elm)
removeVnodes(parentElm, [oldVnode], 0, 0)
return
}
if (isDef(vnode.data)) {
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
i = vnode.data.hook
if (isDef(i) && isDef(i = i.update)) i(oldVnode, vnode)
}
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
} else if (isDef(ch)) {
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
api.setTextContent(elm, vnode.text)
}
if (isDef(hook) && isDef(i = hook.postpatch)) {
i(oldVnode, vnode)
}
}
return function patch (oldVnode, vnode) {
var i, elm, parent
var insertedVnodeQueue = []
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
if (isUndef(oldVnode.sel)) {
oldVnode = emptyNodeAt(oldVnode)
}
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
elm = oldVnode.elm
parent = api.parentNode(elm)
createElm(vnode, insertedVnodeQueue)
if (parent !== null) {
api.insertBefore(parent, vnode.elm, api.nextSibling(elm))
removeVnodes(parent, [oldVnode], 0, 0)
}
}
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i])
}
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
return vnode
}
}

4
src/vdom/vnode.js Normal file
View File

@ -0,0 +1,4 @@
export default function VNode (sel, data, children, text, elm) {
const key = data === undefined ? undefined : data.key
return { sel, data, children, text, elm, key }
}

16
webpack.config.js Normal file
View File

@ -0,0 +1,16 @@
var path = require('path')
module.exports = {
entry: path.resolve(__dirname, 'src/index.umd.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'vue.js',
library: 'Vue',
libraryTarget: 'umd'
},
module: {
loaders: [
{ test: /\.js/, loader: 'babel', exclude: /node_modules/ }
]
}
}