Skip to content

Commit

Permalink
feat: functional component support for compiled templates
Browse files Browse the repository at this point in the history
  • Loading branch information
yyx990803 committed Oct 11, 2017
1 parent 68bdbf5 commit ea0d227
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 53 deletions.
30 changes: 30 additions & 0 deletions src/core/instance/render-helpers/index.js
Original file line number Diff line number Diff line change
@@ -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
}
13 changes: 10 additions & 3 deletions src/core/instance/render-helpers/render-static.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,14 @@ export function renderStatic (
index: number,
isInFor?: boolean
): VNode | Array<VNode> {
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) {
Expand All @@ -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
}
Expand Down
45 changes: 7 additions & 38 deletions src/core/instance/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -65,6 +50,9 @@ export function initRender (vm: Component) {
}

export function renderMixin (Vue: Class<Component>) {
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)

Vue.prototype.$nextTick = function (fn: Function) {
return nextTick(fn, this)
}
Expand Down Expand Up @@ -135,23 +123,4 @@ export function renderMixin (Vue: Class<Component>) {
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
}
52 changes: 40 additions & 12 deletions src/core/vdom/create-functional-component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Component>,
propsData: ?Object,
data: VNodeData,
context: Component,
contextVm: Component,
children: ?Array<VNode>
): 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)
Expand All @@ -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
}
Expand Down
75 changes: 75 additions & 0 deletions test/unit/features/options/functional.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: `
<div>
<child :msg="msg">
<span>{{ msg }}</span>
<div slot="slot2">Second slot</div>
<template slot="scoped" slot-scope="scope">{{ scope.msg }}</template>
</child>
</div>
`,
methods: {
fn () {
this.msg = 'bye'
}
}
}).$mount()

function assertMarkup () {
expect(parent.$el.innerHTML).toBe(
`<div>` +
`<h2 class="red">${parent.msg}</h2>` +
`<span>${parent.msg}</span> ` +
`<div>Second slot</div>` +
parent.msg +
// static
`<div>Some <span>text</span></div>` +
`<div class="clickable">click me</div>` +
`</div>`
)
}

assertMarkup()
triggerEvent(parent.$el.querySelector('.clickable'), 'click')
waitForUpdate(assertMarkup).then(done)
})
})

1 comment on commit ea0d227

@eshell
Copy link

@eshell eshell commented on ea0d227 Oct 11, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lol. The second you posted this, I just converted my dumb components to jsx based functional components ;)

Please sign in to comment.