From ea0d227d2ddfa5fc5e1112acf9cd485b4eae62cb Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 10 Oct 2017 22:33:16 -0400 Subject: [PATCH] feat: functional component support for compiled templates --- src/core/instance/render-helpers/index.js | 30 ++++++++ .../instance/render-helpers/render-static.js | 13 +++- src/core/instance/render.js | 45 ++--------- src/core/vdom/create-functional-component.js | 52 ++++++++++--- test/unit/features/options/functional.spec.js | 75 +++++++++++++++++++ 5 files changed, 162 insertions(+), 53 deletions(-) create mode 100644 src/core/instance/render-helpers/index.js diff --git a/src/core/instance/render-helpers/index.js b/src/core/instance/render-helpers/index.js new file mode 100644 index 00000000000..38414a9ab53 --- /dev/null +++ b/src/core/instance/render-helpers/index.js @@ -0,0 +1,30 @@ +/* @flow */ + +import { toNumber, toString, looseEqual, looseIndexOf } from 'shared/util' +import { createTextVNode, createEmptyVNode } from 'core/vdom/vnode' +import { renderList } from './render-list' +import { renderSlot } from './render-slot' +import { resolveFilter } from './resolve-filter' +import { checkKeyCodes } from './check-keycodes' +import { bindObjectProps } from './bind-object-props' +import { renderStatic, markOnce } from './render-static' +import { bindObjectListeners } from './bind-object-listeners' +import { resolveScopedSlots } from './resolve-slots' + +export function installRenderHelpers (target: any) { + target._o = markOnce + target._n = toNumber + target._s = toString + target._l = renderList + target._t = renderSlot + target._q = looseEqual + target._i = looseIndexOf + target._m = renderStatic + target._f = resolveFilter + target._k = checkKeyCodes + target._b = bindObjectProps + target._v = createTextVNode + target._e = createEmptyVNode + target._u = resolveScopedSlots + target._g = bindObjectListeners +} diff --git a/src/core/instance/render-helpers/render-static.js b/src/core/instance/render-helpers/render-static.js index 9ee977eb96b..552536fec0e 100644 --- a/src/core/instance/render-helpers/render-static.js +++ b/src/core/instance/render-helpers/render-static.js @@ -9,7 +9,14 @@ export function renderStatic ( index: number, isInFor?: boolean ): VNode | Array { - let tree = this._staticTrees[index] + // static trees can be rendered once and cached on the contructor options + // so every instance shares the same trees + let options = this.constructor.options + if (this.$options.staticRenderFns !== options.staticRenderFns) { + options = this.$options + } + const trees = options._staticTrees || (options._staticTrees = []) + let tree = trees[index] // if has already-rendered static tree and not inside v-for, // we can reuse the same tree by doing a shallow clone. if (tree && !isInFor) { @@ -18,8 +25,8 @@ export function renderStatic ( : cloneVNode(tree) } // otherwise, render a fresh tree. - tree = this._staticTrees[index] = - this.$options.staticRenderFns[index].call(this._renderProxy) + tree = trees[index] = + options.staticRenderFns[index].call(this._renderProxy, null, this) markStatic(tree, `__static__${index}`, false) return tree } diff --git a/src/core/instance/render.js b/src/core/instance/render.js index b492316a0df..21a21c9c754 100644 --- a/src/core/instance/render.js +++ b/src/core/instance/render.js @@ -3,33 +3,18 @@ import { warn, nextTick, - toNumber, - toString, - looseEqual, emptyObject, handleError, - looseIndexOf, defineReactive } from '../util/index' -import VNode, { - cloneVNodes, - createTextVNode, - createEmptyVNode -} from '../vdom/vnode' +import { createElement } from '../vdom/create-element' +import { installRenderHelpers } from './render-helpers/index' +import { resolveSlots } from './render-helpers/resolve-slots' +import VNode, { cloneVNodes, createEmptyVNode } from '../vdom/vnode' import { isUpdatingChildComponent } from './lifecycle' -import { createElement } from '../vdom/create-element' -import { renderList } from './render-helpers/render-list' -import { renderSlot } from './render-helpers/render-slot' -import { resolveFilter } from './render-helpers/resolve-filter' -import { checkKeyCodes } from './render-helpers/check-keycodes' -import { bindObjectProps } from './render-helpers/bind-object-props' -import { renderStatic, markOnce } from './render-helpers/render-static' -import { bindObjectListeners } from './render-helpers/bind-object-listeners' -import { resolveSlots, resolveScopedSlots } from './render-helpers/resolve-slots' - export function initRender (vm: Component) { vm._vnode = null // the root of the child tree vm._staticTrees = null @@ -65,6 +50,9 @@ export function initRender (vm: Component) { } export function renderMixin (Vue: Class) { + // install runtime convenience helpers + installRenderHelpers(Vue.prototype) + Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this) } @@ -135,23 +123,4 @@ export function renderMixin (Vue: Class) { vnode.parent = _parentVnode return vnode } - - // internal render helpers. - // these are exposed on the instance prototype to reduce generated render - // code size. - Vue.prototype._o = markOnce - Vue.prototype._n = toNumber - Vue.prototype._s = toString - Vue.prototype._l = renderList - Vue.prototype._t = renderSlot - Vue.prototype._q = looseEqual - Vue.prototype._i = looseIndexOf - Vue.prototype._m = renderStatic - Vue.prototype._f = resolveFilter - Vue.prototype._k = checkKeyCodes - Vue.prototype._b = bindObjectProps - Vue.prototype._v = createTextVNode - Vue.prototype._e = createEmptyVNode - Vue.prototype._u = resolveScopedSlots - Vue.prototype._g = bindObjectListeners } diff --git a/src/core/vdom/create-functional-component.js b/src/core/vdom/create-functional-component.js index f13589370b3..74c36ea7b90 100644 --- a/src/core/vdom/create-functional-component.js +++ b/src/core/vdom/create-functional-component.js @@ -4,6 +4,7 @@ import VNode from './vnode' import { createElement } from './create-element' import { resolveInject } from '../instance/inject' import { resolveSlots } from '../instance/render-helpers/resolve-slots' +import { installRenderHelpers } from '../instance/render-helpers/index' import { isDef, @@ -12,15 +13,43 @@ import { validateProp } from '../util/index' +function FunctionalRenderContext ( + data, + props, + children, + parent, + Ctor +) { + const options = Ctor.options + this.data = data + this.props = props + this.children = children + this.parent = parent + this.listeners = data.on || emptyObject + this.injections = resolveInject(options.inject, parent) + this.slots = () => resolveSlots(children, parent) + // support for compiled functional template + if (options._compiled) { + this.constructor = Ctor + this.$options = options + this._c = parent._c + this.$slots = this.slots() + this.$scopedSlots = data.scopedSlots || emptyObject + } +} + +installRenderHelpers(FunctionalRenderContext.prototype) + export function createFunctionalComponent ( Ctor: Class, propsData: ?Object, data: VNodeData, - context: Component, + contextVm: Component, children: ?Array ): VNode | void { + const options = Ctor.options const props = {} - const propOptions = Ctor.options.props + const propOptions = options.props if (isDef(propOptions)) { for (const key in propOptions) { props[key] = validateProp(key, propOptions, propsData || emptyObject) @@ -31,20 +60,19 @@ export function createFunctionalComponent ( } // ensure the createElement function in functional components // gets a unique context - this is necessary for correct named slot check - const _context = Object.create(context) - const h = (a, b, c, d) => createElement(_context, a, b, c, d, true) - const vnode = Ctor.options.render.call(null, h, { + const _contextVm = Object.create(contextVm) + const h = (a, b, c, d) => createElement(_contextVm, a, b, c, d, true) + const renderContext = new FunctionalRenderContext( data, props, children, - parent: context, - listeners: data.on || emptyObject, - injections: resolveInject(Ctor.options.inject, context), - slots: () => resolveSlots(children, context) - }) + contextVm, + Ctor + ) + const vnode = options.render.call(null, h, renderContext) if (vnode instanceof VNode) { - vnode.functionalContext = context - vnode.functionalOptions = Ctor.options + vnode.functionalContext = contextVm + vnode.functionalOptions = options if (data.slot) { (vnode.data || (vnode.data = {})).slot = data.slot } diff --git a/test/unit/features/options/functional.spec.js b/test/unit/features/options/functional.spec.js index 9bdff840aca..ecbd0be3c55 100644 --- a/test/unit/features/options/functional.spec.js +++ b/test/unit/features/options/functional.spec.js @@ -185,4 +185,79 @@ describe('Options functional', () => { const vnode = h('child') expect(vnode).toEqual(createEmptyVNode()) }) + + it('should work with render fns compiled from template', done => { + // code generated via vue-template-es2015-compiler + var render = function (_h, _vm) { + var _c = _vm._c + return _c( + 'div', + [ + _c('h2', { staticClass: 'red' }, [_vm._v(_vm._s(_vm.props.msg))]), + _vm._t('default'), + _vm._t('slot2'), + _vm._t('scoped', null, { msg: _vm.props.msg }), + _vm._m(0), + _c('div', { staticClass: 'clickable', on: { click: _vm.parent.fn }}, [ + _vm._v('click me') + ]) + ], + 2 + ) + } + var staticRenderFns = [ + function (_h, _vm) { + var _c = _vm._c + return _c('div', [_vm._v('Some '), _c('span', [_vm._v('text')])]) + } + ] + + const child = { + functional: true, + _compiled: true, + render, + staticRenderFns + } + + const parent = new Vue({ + components: { + child + }, + data: { + msg: 'hello' + }, + template: ` +
+ + {{ msg }} +
Second slot
+ +
+
+ `, + methods: { + fn () { + this.msg = 'bye' + } + } + }).$mount() + + function assertMarkup () { + expect(parent.$el.innerHTML).toBe( + `
` + + `

${parent.msg}

` + + `${parent.msg} ` + + `
Second slot
` + + parent.msg + + // static + `
Some text
` + + `
click me
` + + `
` + ) + } + + assertMarkup() + triggerEvent(parent.$el.querySelector('.clickable'), 'click') + waitForUpdate(assertMarkup).then(done) + }) })