From f2c8880041bd845b4af28ed20e2f67c2e2c8d6ff Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 14 Apr 2016 05:21:48 -0400 Subject: [PATCH] component wip --- src/compiler/codegen/index.js | 12 ++--- src/compiler/parser/index.js | 4 +- src/runtime/global-api.js | 9 ++-- src/runtime/instance/index.js | 12 +++-- src/runtime/instance/render.js | 46 +++++++++++++++----- src/runtime/instance/state.js | 5 +++ src/runtime/util/options.js | 19 ++++---- src/runtime/vdom/component.js | 45 +++++++++++++++++++ src/runtime/vdom/{h.js => create-element.js} | 9 +++- src/runtime/vdom/index.js | 4 +- src/runtime/vdom/patch.js | 32 ++++++++------ src/runtime/vdom/thunk.js | 29 ++++++++++++ 12 files changed, 170 insertions(+), 56 deletions(-) create mode 100644 src/runtime/vdom/component.js rename src/runtime/vdom/{h.js => create-element.js} (73%) create mode 100644 src/runtime/vdom/thunk.js diff --git a/src/compiler/codegen/index.js b/src/compiler/codegen/index.js index a2c53af10..5394d7df6 100644 --- a/src/compiler/codegen/index.js +++ b/src/compiler/codegen/index.js @@ -7,9 +7,9 @@ export function generate (ast) { } function genElement (el) { - if (el['for']) { + if (el.for) { return genFor(el) - } else if (el['if']) { + } else if (el.if) { return genIf(el) } else if (el.tag === 'template') { return genChildren(el) @@ -21,15 +21,15 @@ function genElement (el) { } function genIf (el) { - const exp = el['if'] - el['if'] = false // avoid recursion + const exp = el.if + el.if = false // avoid recursion return `(${exp}) ? ${genElement(el)} : null` } function genFor (el) { - const exp = el['for'] + const exp = el.for const alias = el.alias - el['for'] = false // avoid recursion + el.for = false // avoid recursion return `(${exp}) && (${exp}).map(function (${alias}, $index) {return ${genElement(el)}})` } diff --git a/src/compiler/parser/index.js b/src/compiler/parser/index.js index e823a727b..f972af7bc 100644 --- a/src/compiler/parser/index.js +++ b/src/compiler/parser/index.js @@ -134,7 +134,7 @@ function processFor (el) { console.error(`Invalid v-for expression: ${exp}`) } el.alias = inMatch[1].trim() - el['for'] = inMatch[2].trim() + el.for = inMatch[2].trim() if ((exp = getAndRemoveAttr(el, 'track-by'))) { el.key = exp === '$index' ? exp @@ -146,7 +146,7 @@ function processFor (el) { function processIf (el) { let exp = getAndRemoveAttr(el, 'v-if') if (exp) { - el['if'] = exp + el.if = exp } } diff --git a/src/runtime/global-api.js b/src/runtime/global-api.js index ed3832931..098e13665 100644 --- a/src/runtime/global-api.js +++ b/src/runtime/global-api.js @@ -1,6 +1,6 @@ import config from './config' import * as util from './util/index' -import h from './vdom/h' +import { createElement } from './vdom/index' import { set, del, @@ -15,7 +15,7 @@ import { } from './util/index' export function initGlobalAPI (Vue) { - Vue.h = h + Vue.h = Vue.createElement = createElement Vue.config = config Vue.util = util Vue.set = set @@ -98,12 +98,9 @@ export function initGlobalAPI (Vue) { */ function createClass (name) { - /* eslint-disable no-new-func */ return new Function( - 'return function ' + classify(name) + - ' (options) { this._init(options) }' + `return function ${classify(name)} (options) { this._init(options) }` )() - /* eslint-enable no-new-func */ } /** diff --git a/src/runtime/instance/index.js b/src/runtime/instance/index.js index 6b06f0d09..25f8ef761 100644 --- a/src/runtime/instance/index.js +++ b/src/runtime/instance/index.js @@ -7,6 +7,10 @@ import { nextTick, mergeOptions } from '../util/index' let uid = 0 export default function Vue (options) { + this._init(options) +} + +Vue.prototype._init = function (options) { // a uid this._uid = uid++ // a flag to avoid this being observed @@ -32,11 +36,11 @@ export default function Vue (options) { initRender(this) } +Vue.prototype.$nextTick = function (fn) { + nextTick(fn, this) +} + stateMixin(Vue) eventsMixin(Vue) lifecycleMixin(Vue) renderMixin(Vue) - -Vue.prototype.$nextTick = function (fn) { - nextTick(fn, this) -} diff --git a/src/runtime/instance/render.js b/src/runtime/instance/render.js index bfcf0887e..8d1d8336d 100644 --- a/src/runtime/instance/render.js +++ b/src/runtime/instance/render.js @@ -1,10 +1,12 @@ import Watcher from '../observer/watcher' -import { query, resolveAsset } from '../util/index' -import { h, patch } from '../vdom/index' +import { query, resolveAsset, hyphenate } from '../util/index' +import { createElement, patch } from '../vdom/index' import { callHook } from './lifecycle' export function initRender (vm) { + vm._vnode = null vm._mounted = false + // TODO: handle _renderData and _renderChildren const el = vm.$options.el if (el) { vm.$mount(el) @@ -13,33 +15,55 @@ export function initRender (vm) { export function renderMixin (Vue) { // shorthands used in render functions - Vue.prototype.__h__ = h + Vue.prototype.__h__ = createElement Vue.prototype.__d__ = function (id) { return resolveAsset(this.$options, 'directives', id, true) } - Vue.prototype._update = function (vtree) { + Vue.prototype._update = function (vnode) { callHook(this, 'beforeUpdate') - if (!this._tree) { - this.$el = patch(this.$el, vtree) + if (!this._vnode) { + this.$el = patch(this.$el, vnode) } else { - this.$el = patch(this._tree, vtree) + this.$el = patch(this._vnode, vnode) } - this._tree = vtree + this._vnode = vnode callHook(this, 'updated') } + Vue.prototype._tryUpdate = function (data, children) { + if (children) { + // TODO: handle content slots + this.$forceUpdate() + return + } + // check props + const props = this.$options.props + if (props && data.attrs) { + for (let key in props) { + let oldVal = this[key] + let newVal = data.attrs[key] || data.attrs[hyphenate(key)] + if (oldVal !== newVal) { + this.$forceUpdate() + } + } + } + } + Vue.prototype.$mount = function (el) { callHook(this, 'beforeMount') - this.$el = el ? query(el) : document.createElement('div') - this.$el.innerHTML = '' + this.$el = el && query(el) + if (this.$el) { + this.$el.innerHTML = '' + } this._watcher = new Watcher(this, this.$options.render, this._update) this._update(this._watcher.value) callHook(this, 'mounted') this._mounted = true + return this } Vue.prototype.$forceUpdate = function () { - this._watcher.run() + this._watcher.update() } } diff --git a/src/runtime/instance/state.js b/src/runtime/instance/state.js index 8ead82527..89f70f356 100644 --- a/src/runtime/instance/state.js +++ b/src/runtime/instance/state.js @@ -12,12 +12,17 @@ import { export function initState (vm) { vm._watchers = [] + initProps(vm) initData(vm) initComputed(vm) initMethods(vm) initWatch(vm) } +function initProps (vm) { + // TODO +} + function initData (vm) { var data = vm.$options.data data = vm._data = typeof data === 'function' diff --git a/src/runtime/util/options.js b/src/runtime/util/options.js index 990d59e12..89a6d620b 100644 --- a/src/runtime/util/options.js +++ b/src/runtime/util/options.js @@ -257,29 +257,30 @@ function guardComponents (options) { */ function guardProps (options) { - var props = options.props - var i, val + const res = {} + const props = options.props + let i, val if (isArray(props)) { - options.props = {} i = props.length while (i--) { val = props[i] if (typeof val === 'string') { - options.props[val] = null + res[camelize(val)] = null } else if (val.name) { - options.props[val.name] = val + res[camelize(val.name)] = val } } } else if (isPlainObject(props)) { - var keys = Object.keys(props) + const keys = Object.keys(props) i = keys.length while (i--) { val = props[keys[i]] - if (typeof val === 'function') { - props[keys[i]] = { type: val } - } + res[camelize(keys[i])] = typeof val === 'function' + ? { type: val } + : val } } + options.props = res } function guardDirectives (options) { diff --git a/src/runtime/vdom/component.js b/src/runtime/vdom/component.js new file mode 100644 index 000000000..24129d5b6 --- /dev/null +++ b/src/runtime/vdom/component.js @@ -0,0 +1,45 @@ +import Vue from '../instance/index' + +export default function Component (Ctor, data, children) { + if (typeof Ctor === 'object') { + Ctor = Vue.extend(Ctor) + } + // return a placeholder vnode + return { + sel: 'component', + data: { + hooks: { init, prepatch, destroy }, + Ctor, data, children + } + } +} + +function init (vnode) { + const data = vnode.data + const child = new data.Ctor({ + _renderData: data.data, + _renderChildren: data.children + }).$mount() + data.child = child + data.vnode = child._vnode +} + +function prepatch (oldVnode, vnode) { + const old = oldVnode.data + const cur = vnode.data + if (cur.Ctor !== old.Ctor) { + // component changed, teardown and create new + // TODO: keep-alive? + old.child.$destroy() + init(vnode) + } else { + // try re-render child. the child may optimize it + // and just does nothing. + old.child._tryUpdate(cur.data, cur.children) + cur.vnode = old.child._vnode + } +} + +function destroy (vnode) { + vnode.data.childComponent.$destroy() +} diff --git a/src/runtime/vdom/h.js b/src/runtime/vdom/create-element.js similarity index 73% rename from src/runtime/vdom/h.js rename to src/runtime/vdom/create-element.js index 42333df00..bd85b7d36 100644 --- a/src/runtime/vdom/h.js +++ b/src/runtime/vdom/create-element.js @@ -1,7 +1,8 @@ import VNode from './vnode' +import Component from './component' import { isPrimitive, isArray } from '../util/index' -export default function h (tag, data, children) { +export default function createElement (tag, data, children) { if (isArray(children)) { let _children = children children = [] @@ -22,5 +23,9 @@ export default function h (tag, data, children) { } } } - return VNode(tag, data, children, undefined, undefined) + if (typeof tag === 'string') { + return VNode(tag, data, children) + } else { + return Component(tag, data, children) + } } diff --git a/src/runtime/vdom/index.js b/src/runtime/vdom/index.js index 3cc788f17..54f1d2e9f 100644 --- a/src/runtime/vdom/index.js +++ b/src/runtime/vdom/index.js @@ -5,7 +5,7 @@ */ import createPatchFunction from './patch' -import h from './h' +import createElement from './create-element' import classes from './modules/class' import style from './modules/style' import props from './modules/props' @@ -22,4 +22,4 @@ const patch = createPatchFunction([ directives ]) -export { patch, h } +export { patch, createElement } diff --git a/src/runtime/vdom/patch.js b/src/runtime/vdom/patch.js index 5a8764a9a..6533f4e36 100644 --- a/src/runtime/vdom/patch.js +++ b/src/runtime/vdom/patch.js @@ -2,7 +2,7 @@ import VNode from './vnode' import * as dom from './dom' import { isPrimitive } from '../util/index' -const emptyNode = VNode('', {}, [], undefined, undefined) +const emptyNode = VNode('', {}, []) const hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post'] const svgNS = 'http://www.w3.org/2000/svg' @@ -241,21 +241,25 @@ export default function createPatchFunction (modules, api) { 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) - + if (!oldVnode) { createElm(vnode, insertedVnodeQueue) + } else { + if (isUndef(oldVnode.sel)) { + oldVnode = emptyNodeAt(oldVnode) + } - if (parent !== null) { - api.insertBefore(parent, vnode.elm, api.nextSibling(elm)) - removeVnodes(parent, [oldVnode], 0, 0) + 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) + } } } diff --git a/src/runtime/vdom/thunk.js b/src/runtime/vdom/thunk.js new file mode 100644 index 000000000..aeb394457 --- /dev/null +++ b/src/runtime/vdom/thunk.js @@ -0,0 +1,29 @@ +import createElement from './create-element' + +function init (thunk) { + var i, cur = thunk.data + cur.vnode = cur.fn.apply(undefined, cur.args) +} + +function prepatch (oldThunk, thunk) { + var i, old = oldThunk.data, cur = thunk.data + var oldArgs = old.args, args = cur.args + cur.vnode = old.vnode + if (old.fn !== cur.fn || oldArgs.length !== args.length) { + cur.vnode = cur.fn.apply(undefined, args) + return + } + for (i = 0; i < args.length; ++i) { + if (oldArgs[i] !== args[i]) { + cur.vnode = cur.fn.apply(undefined, args) + return + } + } +} + +export default function thunk (name, fn, args) { + return createElement('thunk' + name, { + hook: { init, prepatch }, + fn, args + }) +}