Jelajahi Sumber

wip: refactor compiler to skip normalization when possible

wip fix

wip fix

wip fix
Evan You 9 tahun lalu
induk
melakukan
79e1058799

+ 2 - 0
flow/component.js

@@ -88,6 +88,8 @@ declare interface Component {
   _o: (vnode: VNode | Array<VNode>, index: number, key: string) => VNode | VNodeChildren;
   _o: (vnode: VNode | Array<VNode>, index: number, key: string) => VNode | VNodeChildren;
   // toString
   // toString
   _s: (value: any) => string;
   _s: (value: any) => string;
+  // text to VNode
+  _v: (value: string | number) => VNode;
   // toNumber
   // toNumber
   _n: (value: string) => number | string;
   _n: (value: string) => number | string;
   // empty vnode
   // empty vnode

+ 31 - 6
src/compiler/codegen/index.js

@@ -61,7 +61,7 @@ function genElement (el: ASTElement): string {
     } else {
     } else {
       const data = el.plain ? undefined : genData(el)
       const data = el.plain ? undefined : genData(el)
 
 
-      const children = el.inlineTemplate ? null : genChildren(el)
+      const children = el.inlineTemplate ? null : genChildren(el, true)
       code = `_h('${el.tag}'${
       code = `_h('${el.tag}'${
         data ? `,${data}` : '' // data
         data ? `,${data}` : '' // data
       }${
       }${
@@ -276,12 +276,36 @@ function genScopedSlot (key: string, el: ASTElement) {
   }}`
   }}`
 }
 }
 
 
-function genChildren (el: ASTElement): string | void {
-  if (el.children.length) {
-    return '[' + el.children.map(genNode).join(',') + ']'
+function genChildren (el: ASTElement, checkSkip?: boolean): string | void {
+  const children = el.children
+  if (children.length) {
+    const el: any = children[0]
+    // optimize single v-for
+    if (children.length === 1 &&
+        el.for &&
+        el.tag !== 'template' &&
+        el.tag !== 'slot') {
+      return genElement(el)
+    }
+    return `[${children.map(genNode).join(',')}]${
+      checkSkip
+        ? canSkipNormalization(children) ? '' : ',true'
+        : ''
+    }`
   }
   }
 }
 }
 
 
+function canSkipNormalization (children): boolean {
+  for (let i = 0; i < children.length; i++) {
+    const el: any = children[i]
+    if (el.for || el.tag === 'template' || el.tag === 'slot' ||
+      (el.if && el.ifConditions.some(c => c.tag === 'template'))) {
+      return false
+    }
+  }
+  return true
+}
+
 function genNode (node: ASTNode) {
 function genNode (node: ASTNode) {
   if (node.type === 1) {
   if (node.type === 1) {
     return genElement(node)
     return genElement(node)
@@ -291,9 +315,10 @@ function genNode (node: ASTNode) {
 }
 }
 
 
 function genText (text: ASTText | ASTExpression): string {
 function genText (text: ASTText | ASTExpression): string {
-  return text.type === 2
+  return `_v(${text.type === 2
     ? text.expression // no need for () because already wrapped in _s()
     ? text.expression // no need for () because already wrapped in _s()
     : transformSpecialNewlines(JSON.stringify(text.text))
     : transformSpecialNewlines(JSON.stringify(text.text))
+  })`
 }
 }
 
 
 function genSlot (el: ASTElement): string {
 function genSlot (el: ASTElement): string {
@@ -310,7 +335,7 @@ function genSlot (el: ASTElement): string {
 
 
 // componentName is el.component, take it as argument to shun flow's pessimistic refinement
 // componentName is el.component, take it as argument to shun flow's pessimistic refinement
 function genComponent (componentName, el): string {
 function genComponent (componentName, el): string {
-  const children = el.inlineTemplate ? null : genChildren(el)
+  const children = el.inlineTemplate ? null : genChildren(el, true)
   return `_h(${componentName},${genData(el)}${
   return `_h(${componentName},${genData(el)}${
     children ? `,${children}` : ''
     children ? `,${children}` : ''
   })`
   })`

+ 23 - 7
src/core/instance/render.js

@@ -1,11 +1,24 @@
 /* @flow */
 /* @flow */
 
 
 import config from '../config'
 import config from '../config'
-import VNode, { createEmptyVNode, cloneVNode, cloneVNodes } from '../vdom/vnode'
 import { normalizeChildren } from '../vdom/helpers/index'
 import { normalizeChildren } from '../vdom/helpers/index'
+import VNode, {
+  cloneVNode,
+  cloneVNodes,
+  createTextVNode,
+  createEmptyVNode
+} from '../vdom/vnode'
 import {
 import {
-  warn, formatComponentName, bind, isObject, toObject,
-  nextTick, resolveAsset, _toString, toNumber, looseEqual, looseIndexOf
+  warn,
+  isObject,
+  toObject,
+  nextTick,
+  toNumber,
+  _toString,
+  looseEqual,
+  looseIndexOf,
+  resolveAsset,
+  formatComponentName
 } from '../util/index'
 } from '../util/index'
 
 
 import { createElement } from '../vdom/create-element'
 import { createElement } from '../vdom/create-element'
@@ -18,9 +31,12 @@ export function initRender (vm: Component) {
   const renderContext = parentVnode && parentVnode.context
   const renderContext = parentVnode && parentVnode.context
   vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext)
   vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext)
   vm.$scopedSlots = {}
   vm.$scopedSlots = {}
-  // bind the public createElement fn to this instance
+  // bind the createElement fn to this instance
   // so that we get proper render context inside it.
   // so that we get proper render context inside it.
-  vm.$createElement = bind(createElement, vm)
+  // args order: tag, data, children, needNormalization
+  // the needNormalization flag is flipped and defaults to true for the public version.
+  vm._h = (a, b, c, d) => createElement(vm, a, b, c, d, false)
+  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
   if (vm.$options.el) {
   if (vm.$options.el) {
     vm.$mount(vm.$options.el)
     vm.$mount(vm.$options.el)
   }
   }
@@ -89,10 +105,10 @@ export function renderMixin (Vue: Class<Component>) {
     return vnode
     return vnode
   }
   }
 
 
-  // shorthands used in render functions
-  Vue.prototype._h = createElement
   // toString for mustaches
   // toString for mustaches
   Vue.prototype._s = _toString
   Vue.prototype._s = _toString
+  // convert text to vnode
+  Vue.prototype._v = createTextVNode
   // number conversion
   // number conversion
   Vue.prototype._n = toNumber
   Vue.prototype._n = toNumber
   // empty vnode
   // empty vnode

+ 12 - 14
src/core/vdom/create-component.js

@@ -6,7 +6,7 @@ import { resolveConstructorOptions } from '../instance/init'
 import { activeInstance, callHook } from '../instance/lifecycle'
 import { activeInstance, callHook } from '../instance/lifecycle'
 import { resolveSlots } from '../instance/render'
 import { resolveSlots } from '../instance/render'
 import { createElement } from './create-element'
 import { createElement } from './create-element'
-import { warn, isObject, hasOwn, hyphenate, validateProp, bind } from '../util/index'
+import { warn, isObject, hasOwn, hyphenate, validateProp } from '../util/index'
 
 
 const hooks = { init, prepatch, insert, destroy }
 const hooks = { init, prepatch, insert, destroy }
 const hooksToMerge = Object.keys(hooks)
 const hooksToMerge = Object.keys(hooks)
@@ -105,19 +105,17 @@ function createFunctionalComponent (
       props[key] = validateProp(key, propOptions, propsData)
       props[key] = validateProp(key, propOptions, propsData)
     }
     }
   }
   }
-  const vnode = Ctor.options.render.call(
-    null,
-    // ensure the createElement function in functional components
-    // gets a unique context - this is necessary for correct named slot check
-    bind(createElement, { _self: Object.create(context) }),
-    {
-      props,
-      data,
-      parent: context,
-      children: normalizeChildren(children),
-      slots: () => resolveSlots(children, context)
-    }
-  )
+  // 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)
+  const vnode = Ctor.options.render.call(null, h, {
+    props,
+    data,
+    parent: context,
+    children: normalizeChildren(children),
+    slots: () => resolveSlots(children, context)
+  })
   if (vnode instanceof VNode) {
   if (vnode instanceof VNode) {
     vnode.functionalContext = context
     vnode.functionalContext = context
     if (data.slot) {
     if (data.slot) {

+ 18 - 13
src/core/vdom/create-element.js

@@ -4,36 +4,41 @@ import VNode, { createEmptyVNode } from './vnode'
 import config from '../config'
 import config from '../config'
 import { createComponent } from './create-component'
 import { createComponent } from './create-component'
 import { normalizeChildren } from './helpers/index'
 import { normalizeChildren } from './helpers/index'
-import { warn, resolveAsset } from '../util/index'
+import { warn, resolveAsset, isPrimitive } from '../util/index'
 
 
 // wrapper function for providing a more flexible interface
 // wrapper function for providing a more flexible interface
 // without getting yelled at by flow
 // without getting yelled at by flow
 export function createElement (
 export function createElement (
+  context: Component,
   tag: any,
   tag: any,
   data: any,
   data: any,
-  children: any
-): VNode | void {
-  if (data && (Array.isArray(data) || typeof data !== 'object')) {
+  children: any,
+  needNormalization: any,
+  flipNormalization: boolean
+): VNode {
+  if (Array.isArray(data) || isPrimitive(data)) {
+    needNormalization = children
     children = data
     children = data
     data = undefined
     data = undefined
   }
   }
-  // make sure to use real instance instead of proxy as context
-  return _createElement(this._self, tag, data, children)
+  if (flipNormalization) needNormalization = !needNormalization
+  return _createElement(context, tag, data, children, needNormalization)
 }
 }
 
 
 export function _createElement (
 export function _createElement (
   context: Component,
   context: Component,
   tag?: string | Class<Component> | Function | Object,
   tag?: string | Class<Component> | Function | Object,
   data?: VNodeData,
   data?: VNodeData,
-  children?: VNodeChildren | void
-): VNode | void {
+  children?: any,
+  needNormalization?: boolean
+): VNode {
   if (data && data.__ob__) {
   if (data && data.__ob__) {
     process.env.NODE_ENV !== 'production' && warn(
     process.env.NODE_ENV !== 'production' && warn(
       `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
       `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
       'Always create fresh vnode data objects in each render!',
       'Always create fresh vnode data objects in each render!',
       context
       context
     )
     )
-    return
+    return createEmptyVNode()
   }
   }
   if (!tag) {
   if (!tag) {
     // in case of component :is set to falsy value
     // in case of component :is set to falsy value
@@ -52,24 +57,24 @@ export function _createElement (
     if (config.isReservedTag(tag)) {
     if (config.isReservedTag(tag)) {
       // platform built-in elements
       // platform built-in elements
       return new VNode(
       return new VNode(
-        tag, data, normalizeChildren(children, ns),
+        tag, data, needNormalization ? normalizeChildren(children, ns) : children,
         undefined, undefined, ns, context
         undefined, undefined, ns, context
       )
       )
     } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
     } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
       // component
       // component
-      return createComponent(Ctor, data, context, children, tag)
+      return createComponent(Ctor, data, context, children, tag) || createEmptyVNode()
     } else {
     } else {
       // unknown or unlisted namespaced elements
       // unknown or unlisted namespaced elements
       // check at runtime because it may get assigned a namespace when its
       // check at runtime because it may get assigned a namespace when its
       // parent normalizes children
       // parent normalizes children
       const childNs = tag === 'foreignObject' ? 'xhtml' : ns
       const childNs = tag === 'foreignObject' ? 'xhtml' : ns
       return new VNode(
       return new VNode(
-        tag, data, normalizeChildren(children, childNs),
+        tag, data, needNormalization ? normalizeChildren(children, childNs) : children,
         undefined, undefined, ns, context
         undefined, undefined, ns, context
       )
       )
     }
     }
   } else {
   } else {
     // direct component options / constructor
     // direct component options / constructor
-    return createComponent(tag, data, context, children)
+    return createComponent(tag, data, context, children) || createEmptyVNode()
   }
   }
 }
 }

+ 2 - 8
src/core/vdom/helpers/normalize-children.js

@@ -1,7 +1,7 @@
 /* @flow */
 /* @flow */
 
 
 import { isPrimitive } from 'core/util/index'
 import { isPrimitive } from 'core/util/index'
-import VNode from 'core/vdom/vnode'
+import VNode, { createTextVNode } from 'core/vdom/vnode'
 
 
 export function normalizeChildren (children: any, ns: ?string): Array<VNode> | void {
 export function normalizeChildren (children: any, ns: ?string): Array<VNode> | void {
   return isPrimitive(children)
   return isPrimitive(children)
@@ -30,9 +30,7 @@ function normalizeArrayChildren (children: any, ns: ?string, nestedIndex?: strin
       }
       }
     } else {
     } else {
       if (c.text && last && last.text) {
       if (c.text && last && last.text) {
-        if (!last.isCloned) {
-          last.text += c.text
-        }
+        res[res.length - 1] = createTextVNode(last.text + c.text)
       } else {
       } else {
         // inherit parent namespace
         // inherit parent namespace
         if (ns) {
         if (ns) {
@@ -49,10 +47,6 @@ function normalizeArrayChildren (children: any, ns: ?string, nestedIndex?: strin
   return res
   return res
 }
 }
 
 
-function createTextVNode (val) {
-  return new VNode(undefined, undefined, undefined, String(val))
-}
-
 function applyNS (vnode, ns) {
 function applyNS (vnode, ns) {
   if (vnode.tag && !vnode.ns) {
   if (vnode.tag && !vnode.ns) {
     vnode.ns = ns
     vnode.ns = ns

+ 4 - 0
src/core/vdom/vnode.js

@@ -58,6 +58,10 @@ export const createEmptyVNode = () => {
   return node
   return node
 }
 }
 
 
+export function createTextVNode (val: string | number) {
+  return new VNode(undefined, undefined, undefined, String(val))
+}
+
 // optimized shallow clone
 // optimized shallow clone
 // used for static nodes and slot nodes because they may be reused across
 // used for static nodes and slot nodes because they may be reused across
 // multiple renders, cloning them avoids errors when DOM manipulations rely
 // multiple renders, cloning them avoids errors when DOM manipulations rely

+ 2 - 2
test/unit/features/component/component-async.spec.js

@@ -17,7 +17,7 @@ describe('Component async', () => {
         }
         }
       }
       }
     }).$mount()
     }).$mount()
-    expect(vm.$el.innerHTML).toBe('')
+    expect(vm.$el.innerHTML).toBe('<!---->')
     expect(vm.$children.length).toBe(0)
     expect(vm.$children.length).toBe(0)
     function next () {
     function next () {
       expect(vm.$el.innerHTML).toBe('<div>hi</div>')
       expect(vm.$el.innerHTML).toBe('<div>hi</div>')
@@ -151,7 +151,7 @@ describe('Component async', () => {
         }
         }
       }
       }
     }).$mount()
     }).$mount()
-    expect(vm.$el.innerHTML).toBe('')
+    expect(vm.$el.innerHTML).toBe('<!---->')
     expect(vm.$children.length).toBe(0)
     expect(vm.$children.length).toBe(0)
     function next () {
     function next () {
       expect(vm.$el.innerHTML).toBe('<div>hi</div>')
       expect(vm.$el.innerHTML).toBe('<div>hi</div>')

+ 9 - 11
test/unit/modules/vdom/create-element.spec.js

@@ -1,14 +1,12 @@
 import Vue from 'vue'
 import Vue from 'vue'
-import { createElement } from 'core/vdom/create-element'
 import { createEmptyVNode } from 'core/vdom/vnode'
 import { createEmptyVNode } from 'core/vdom/vnode'
-import { bind } from 'shared/util'
 
 
 describe('create-element', () => {
 describe('create-element', () => {
   it('render vnode with basic reserved tag using createElement', () => {
   it('render vnode with basic reserved tag using createElement', () => {
     const vm = new Vue({
     const vm = new Vue({
       data: { msg: 'hello world' }
       data: { msg: 'hello world' }
     })
     })
-    const h = bind(createElement, vm)
+    const h = vm.$createElement
     const vnode = h('p', {})
     const vnode = h('p', {})
     expect(vnode.tag).toBe('p')
     expect(vnode.tag).toBe('p')
     expect(vnode.data).toEqual({})
     expect(vnode.data).toEqual({})
@@ -28,7 +26,7 @@ describe('create-element', () => {
         }
         }
       }
       }
     })
     })
-    const h = bind(createElement, vm)
+    const h = vm.$createElement
     const vnode = h('my-component', { props: { msg: vm.message }})
     const vnode = h('my-component', { props: { msg: vm.message }})
     expect(vnode.tag).toMatch(/vue-component-[0-9]+/)
     expect(vnode.tag).toMatch(/vue-component-[0-9]+/)
     expect(vnode.componentOptions.propsData).toEqual({ msg: vm.message })
     expect(vnode.componentOptions.propsData).toEqual({ msg: vm.message })
@@ -43,7 +41,7 @@ describe('create-element', () => {
     const vm = new Vue({
     const vm = new Vue({
       data: { msg: 'hello world' }
       data: { msg: 'hello world' }
     })
     })
-    const h = bind(createElement, vm)
+    const h = vm.$createElement
     const tag = 'custom-tag'
     const tag = 'custom-tag'
     const vnode = h(tag, {})
     const vnode = h(tag, {})
     expect(vnode.tag).toBe('custom-tag')
     expect(vnode.tag).toBe('custom-tag')
@@ -60,7 +58,7 @@ describe('create-element', () => {
     const vm = new Vue({
     const vm = new Vue({
       data: { msg: 'hello world' }
       data: { msg: 'hello world' }
     })
     })
-    const h = bind(createElement, vm)
+    const h = vm.$createElement
     const vnode = h(null, {})
     const vnode = h(null, {})
     expect(vnode).toEqual(createEmptyVNode())
     expect(vnode).toEqual(createEmptyVNode())
   })
   })
@@ -69,7 +67,7 @@ describe('create-element', () => {
     const vm = new Vue({
     const vm = new Vue({
       data: { msg: 'hello world' }
       data: { msg: 'hello world' }
     })
     })
-    const h = bind(createElement, vm)
+    const h = vm.$createElement
     const vnode = h(Vue.extend({ // Component class
     const vnode = h(Vue.extend({ // Component class
       props: ['msg']
       props: ['msg']
     }), { props: { msg: vm.message }})
     }), { props: { msg: vm.message }})
@@ -84,7 +82,7 @@ describe('create-element', () => {
 
 
   it('render vnode with createElement with children', () => {
   it('render vnode with createElement with children', () => {
     const vm = new Vue({})
     const vm = new Vue({})
-    const h = bind(createElement, vm)
+    const h = vm.$createElement
     const vnode = h('p', void 0, [h('br'), 'hello world', h('br')])
     const vnode = h('p', void 0, [h('br'), 'hello world', h('br')])
     expect(vnode.children[0].tag).toBe('br')
     expect(vnode.children[0].tag).toBe('br')
     expect(vnode.children[1].text).toBe('hello world')
     expect(vnode.children[1].text).toBe('hello world')
@@ -93,7 +91,7 @@ describe('create-element', () => {
 
 
   it('render vnode with children, omitting data', () => {
   it('render vnode with children, omitting data', () => {
     const vm = new Vue({})
     const vm = new Vue({})
-    const h = bind(createElement, vm)
+    const h = vm.$createElement
     const vnode = h('p', [h('br'), 'hello world', h('br')])
     const vnode = h('p', [h('br'), 'hello world', h('br')])
     expect(vnode.children[0].tag).toBe('br')
     expect(vnode.children[0].tag).toBe('br')
     expect(vnode.children[1].text).toBe('hello world')
     expect(vnode.children[1].text).toBe('hello world')
@@ -102,7 +100,7 @@ describe('create-element', () => {
 
 
   it('render svg elements with correct namespace', () => {
   it('render svg elements with correct namespace', () => {
     const vm = new Vue({})
     const vm = new Vue({})
-    const h = bind(createElement, vm)
+    const h = vm.$createElement
     const vnode = h('svg', [h('a', [h('foo', [h('bar')])])])
     const vnode = h('svg', [h('a', [h('foo', [h('bar')])])])
     expect(vnode.ns).toBe('svg')
     expect(vnode.ns).toBe('svg')
     // should apply ns to children recursively
     // should apply ns to children recursively
@@ -113,7 +111,7 @@ describe('create-element', () => {
 
 
   it('render MathML elements with correct namespace', () => {
   it('render MathML elements with correct namespace', () => {
     const vm = new Vue({})
     const vm = new Vue({})
-    const h = bind(createElement, vm)
+    const h = vm.$createElement
     const vnode = h('math', [h('matrix')])
     const vnode = h('math', [h('matrix')])
     expect(vnode.ns).toBe('math')
     expect(vnode.ns).toBe('math')
     // should apply ns to children
     // should apply ns to children