Parcourir la source

SSR: eliminate closure in render.js implementation

eliminate closure

fix multiple render instances and memory leakage

fix eslint

magically fix flow typing

add multiple render stream test
Herrington Darkholme il y a 9 ans
Parent
commit
2ac581bb7f

+ 3 - 1
flow/options.js

@@ -57,7 +57,9 @@ declare type ComponentOptions = {
   _propKeys?: Array<string>;
   _parentVnode?: VNode;
   _parentListeners?: ?Object;
-  _renderChildren?: ?VNodeChildren
+  _renderChildren?: ?VNodeChildren;
+  _componentTag: ?string;
+  _scopeId: ?string;
 }
 
 declare type PropOptions = {

+ 21 - 0
flow/ssr.js

@@ -0,0 +1,21 @@
+declare type ComponentWithCacheContext = {
+  type: 'ComponentWithCache';
+  bufferIndex: number;
+  buffer: Array<string>;
+  key: string;
+}
+
+declare type ElementContext = {
+  type: 'Element';
+  children: Array<VNode>;
+  rendered: number;
+  endTag: string;
+  total: number;
+}
+
+declare type ComponentContext = {
+  type: 'Component';
+  prevActive: Component;
+}
+
+declare type RenderState = ComponentContext | ComponentWithCacheContext | ElementContext

+ 1 - 1
package.json

@@ -67,7 +67,7 @@
     "eslint-loader": "^1.3.0",
     "eslint-plugin-flowtype": "^2.16.0",
     "eslint-plugin-vue": "^1.0.0",
-    "flow-bin": "^0.32.0",
+    "flow-bin": "^0.33.0",
     "he": "^1.1.0",
     "http-server": "^0.9.0",
     "jasmine": "2.4.x",

+ 7 - 9
src/compiler/codegen/index.js

@@ -53,9 +53,10 @@ function genElement (el: ASTElement): string {
     // component or element
     let code
     if (el.component) {
-      code = genComponent(el)
+      code = genComponent(el.component, el)
     } else {
-      const data = genData(el)
+      const data = el.plain ? undefined : genData(el)
+
       const children = el.inlineTemplate ? null : genChildren(el)
       code = `_h('${el.tag}'${
         data ? `,${data}` : '' // data
@@ -95,11 +96,7 @@ function genFor (el: any): string {
     '})'
 }
 
-function genData (el: ASTElement): string | void {
-  if (el.plain) {
-    return
-  }
-
+function genData (el: ASTElement): string {
   let data = '{'
 
   // directives first.
@@ -229,9 +226,10 @@ function genSlot (el: ASTElement): string {
     : `_t(${slotName})`
 }
 
-function genComponent (el: any): string {
+// componentName is el.component, take it as argument to shun flow's pessimistic refinement
+function genComponent (componentName, el): string {
   const children = el.inlineTemplate ? null : genChildren(el)
-  return `_h(${el.component},${genData(el)}${
+  return `_h(${componentName},${genData(el)}${
     children ? `,${children}` : ''
   })`
 }

+ 1 - 1
src/core/util/props.js

@@ -126,7 +126,7 @@ function assertProp (
  */
 function assertType (value: any, type: Function): {
   valid: boolean,
-  expectedType: string
+  expectedType: ?string
 } {
   let valid
   let expectedType = getType(type)

+ 1 - 3
src/server/render-stream.js

@@ -13,9 +13,8 @@ import { createWriteFunction } from './write'
 
 export default class RenderStream extends stream.Readable {
   buffer: string;
-  render: Function;
+  render: (write: Function, done: Function) => void;
   expectedSize: number;
-  stackDepth: number;
   write: Function;
   next: Function;
   end: Function;
@@ -26,7 +25,6 @@ export default class RenderStream extends stream.Readable {
     this.buffer = ''
     this.render = render
     this.expectedSize = 0
-    this.stackDepth = 0
 
     this.write = createWriteFunction((text, next) => {
       const n = this.expectedSize

+ 186 - 155
src/server/render.js

@@ -3,6 +3,7 @@
 import { escape } from 'he'
 import { compileToFunctions } from 'web/compiler/index'
 import { createComponentInstanceForVnode } from 'core/vdom/create-component'
+import { noop } from 'shared/util'
 
 let warned = Object.create(null)
 const warnOnce = msg => {
@@ -43,103 +44,184 @@ const normalizeRender = vm => {
   }
 }
 
-export function createRenderFunction (
-  modules: Array<Function>,
-  directives: Object,
-  isUnaryTag: Function,
-  cache: any
-) {
-  if (cache && (!cache.get || !cache.set)) {
-    throw new Error('renderer cache must implement at least get & set.')
+function renderNode (node, isRoot, context) {
+  const { write, next } = context
+  if (node.componentOptions) {
+    // check cache hit
+    const Ctor = node.componentOptions.Ctor
+    const getKey = Ctor.options.serverCacheKey
+    const name = Ctor.options.name
+    const cache = context.cache
+    if (getKey && cache && name) {
+      const key = name + '::' + getKey(node.componentOptions.propsData)
+      const { has, get } = context
+      if (has) {
+        has(key, hit => {
+          if (hit && get) {
+            get(key, res => write(res, next))
+          } else {
+            renderComponentWithCache(node, isRoot, key, context)
+          }
+        })
+      } else if (get) {
+        get(key, res => {
+          if (res) {
+            write(res, next)
+          } else {
+            renderComponentWithCache(node, isRoot, key, context)
+          }
+        })
+      }
+    } else {
+      if (getKey && !cache) {
+        warnOnce(
+          `[vue-server-renderer] Component ${
+            Ctor.options.name || '(anonymous)'
+          } implemented serverCacheKey, ` +
+          'but no cache was provided to the renderer.'
+        )
+      }
+      if (getKey && !name) {
+        warnOnce(
+          `[vue-server-renderer] Components that implement "serverCacheKey" ` +
+          `must also define a unique "name" option.`
+        )
+      }
+      renderComponent(node, isRoot, context)
+    }
+  } else {
+    if (node.tag) {
+      renderElement(node, isRoot, context)
+    } else if (node.isComment) {
+      write(`<!--${node.text}-->`, next)
+    } else {
+      write(node.raw ? node.text : escape(String(node.text)), next)
+    }
   }
+}
 
-  const get = cache && normalizeAsync(cache, 'get')
-  const has = cache && normalizeAsync(cache, 'has')
+function renderComponent (node, isRoot, context) {
+  const prevActive = context.activeInstance
+  const child = context.activeInstance = createComponentInstanceForVnode(node, context.activeInstance)
+  normalizeRender(child)
+  const childNode = child._render()
+  childNode.parent = node
+  context.renderStates.push({
+    type: 'Component',
+    prevActive
+  })
+  renderNode(childNode, isRoot, context)
+}
 
-  // used to track and apply scope ids
-  let activeInstance: any
+function renderComponentWithCache (node, isRoot, key, context) {
+  const write = context.write
+  write.caching = true
+  const buffer = write.cacheBuffer
+  const bufferIndex = buffer.push('') - 1
+  context.renderStates.push({
+    type: 'ComponentWithCache',
+    buffer, bufferIndex, key
+  })
+  renderComponent(node, isRoot, context)
+}
 
-  function renderNode (
-    node: VNode,
-    write: Function,
-    next: Function,
-    isRoot: boolean
-  ) {
-    if (node.componentOptions) {
-      // check cache hit
-      const Ctor = node.componentOptions.Ctor
-      const getKey = Ctor.options.serverCacheKey
-      const name = Ctor.options.name
-      if (getKey && cache && name) {
-        const key = name + '::' + getKey(node.componentOptions.propsData)
-        if (has) {
-          has(key, hit => {
-            if (hit && get) {
-              get(key, res => write(res, next))
-            } else {
-              renderComponentWithCache(node, write, next, isRoot, cache, key)
-            }
-          })
-        } else if (get) {
-          get(key, res => {
-            if (res) {
-              write(res, next)
-            } else {
-              renderComponentWithCache(node, write, next, isRoot, cache, key)
-            }
-          })
-        }
-      } else {
-        if (getKey && !cache) {
-          warnOnce(
-            `[vue-server-renderer] Component ${
-              Ctor.options.name || '(anonymous)'
-            } implemented serverCacheKey, ` +
-            'but no cache was provided to the renderer.'
-          )
-        }
-        if (getKey && !name) {
-          warnOnce(
-            `[vue-server-renderer] Components that implement "serverCacheKey" ` +
-            `must also define a unique "name" option.`
-          )
+function renderElement (el, isRoot, context) {
+  if (isRoot) {
+    if (!el.data) el.data = {}
+    if (!el.data.attrs) el.data.attrs = {}
+    el.data.attrs['server-rendered'] = 'true'
+  }
+  const startTag = renderStartingTag(el, context)
+  const endTag = `</${el.tag}>`
+  const { write, next } = context
+  if (context.isUnaryTag(el.tag)) {
+    write(startTag, next)
+  } else if (!el.children || !el.children.length) {
+    write(startTag + endTag, next)
+  } else {
+    const children: Array<VNode> = el.children
+    context.renderStates.push({
+      type: 'Element',
+      rendered: 0,
+      total: children.length,
+      endTag, children
+    })
+    write(startTag, next)
+  }
+}
+
+function renderStartingTag (node: VNode, context) {
+  let markup = `<${node.tag}`
+  const { directives, modules } = context
+  if (node.data) {
+    // check directives
+    const dirs = node.data.directives
+    if (dirs) {
+      for (let i = 0; i < dirs.length; i++) {
+        const dirRenderer = directives[dirs[i].name]
+        if (dirRenderer) {
+          // directives mutate the node's data
+          // which then gets rendered by modules
+          dirRenderer(node, dirs[i])
         }
-        renderComponent(node, write, next, isRoot)
       }
-    } else {
-      if (node.tag) {
-        renderElement(node, write, next, isRoot)
-      } else if (node.isComment) {
-        write(`<!--${node.text}-->`, next)
-      } else {
-        write(node.raw ? node.text : escape(String(node.text)), next)
+    }
+    // apply other modules
+    for (let i = 0; i < modules.length; i++) {
+      const res = modules[i](node)
+      if (res) {
+        markup += res
       }
     }
   }
-
-  function renderComponent (node, write, next, isRoot) {
-    const prevActive = activeInstance
-    const child = activeInstance = createComponentInstanceForVnode(node, activeInstance)
-    normalizeRender(child)
-    const childNode = child._render()
-    childNode.parent = node
-    renderNode(childNode, write, () => {
-      activeInstance = prevActive
-      next()
-    }, isRoot)
+  // attach scoped CSS ID
+  let scopeId
+  const activeInstance = context.activeInstance
+  if (activeInstance &&
+      activeInstance !== node.context &&
+      (scopeId = activeInstance.$options._scopeId)) {
+    markup += ` ${scopeId}`
   }
+  while (node) {
+    if ((scopeId = node.context.$options._scopeId)) {
+      markup += ` ${scopeId}`
+    }
+    node = node.parent
+  }
+  return markup + '>'
+}
 
-  function renderComponentWithCache (node, write, next, isRoot, cache, key) {
-    write.caching = true
-    const buffer = write.cacheBuffer
-    const bufferIndex = buffer.push('') - 1
-    renderComponent(node, write, () => {
+const nextFactory = context => function next () {
+  const lastState = context.renderStates.pop()
+  if (!lastState) {
+    context.done()
+    // cleanup context, avoid leakage
+    context = (null: any)
+    return
+  }
+  switch (lastState.type) {
+    case 'Component':
+      context.activeInstance = lastState.prevActive
+      next()
+      break
+    case 'Element':
+      const { children, total } = lastState
+      const rendered = lastState.rendered++
+      if (rendered < total) {
+        context.renderStates.push(lastState)
+        renderNode(children[rendered], false, context)
+      } else {
+        context.write(lastState.endTag, next)
+      }
+      break
+    case 'ComponentWithCache':
+      const { buffer, bufferIndex, key } = lastState
       const result = buffer[bufferIndex]
-      cache.set(key, result)
+      context.cache.set(key, result)
       if (bufferIndex === 0) {
         // this is a top-level cached component,
         // exit caching mode.
-        write.caching = false
+        context.write.caching = false
       } else {
         // parent component is also being cached,
         // merge self into parent's result
@@ -147,81 +229,22 @@ export function createRenderFunction (
       }
       buffer.length = bufferIndex
       next()
-    }, isRoot)
+      break
   }
+}
 
-  function renderElement (el, write, next, isRoot) {
-    if (isRoot) {
-      if (!el.data) el.data = {}
-      if (!el.data.attrs) el.data.attrs = {}
-      el.data.attrs['server-rendered'] = 'true'
-    }
-    const startTag = renderStartingTag(el)
-    const endTag = `</${el.tag}>`
-    if (isUnaryTag(el.tag)) {
-      write(startTag, next)
-    } else if (!el.children || !el.children.length) {
-      write(startTag + endTag, next)
-    } else {
-      const children: Array<VNode> = el.children || []
-      write(startTag, () => {
-        const total = children.length
-        let rendered = 0
-
-        function renderChild (child: VNode) {
-          renderNode(child, write, () => {
-            rendered++
-            if (rendered < total) {
-              renderChild(children[rendered])
-            } else {
-              write(endTag, next)
-            }
-          }, false)
-        }
-
-        renderChild(children[0])
-      })
-    }
+export function createRenderFunction (
+  modules: Array<Function>,
+  directives: Object,
+  isUnaryTag: Function,
+  cache: any
+) {
+  if (cache && (!cache.get || !cache.set)) {
+    throw new Error('renderer cache must implement at least get & set.')
   }
 
-  function renderStartingTag (node: VNode) {
-    let markup = `<${node.tag}`
-    if (node.data) {
-      // check directives
-      const dirs = node.data.directives
-      if (dirs) {
-        for (let i = 0; i < dirs.length; i++) {
-          const dirRenderer = directives[dirs[i].name]
-          if (dirRenderer) {
-            // directives mutate the node's data
-            // which then gets rendered by modules
-            dirRenderer(node, dirs[i])
-          }
-        }
-      }
-      // apply other modules
-      for (let i = 0; i < modules.length; i++) {
-        const res = modules[i](node)
-        if (res) {
-          markup += res
-        }
-      }
-    }
-    // attach scoped CSS ID
-    let scopeId
-    if (activeInstance &&
-        activeInstance !== node.context &&
-        (scopeId = activeInstance.$options._scopeId)) {
-      markup += ` ${scopeId}`
-    }
-    while (node) {
-      if ((scopeId = node.context.$options._scopeId)) {
-        markup += ` ${scopeId}`
-      }
-      node = node.parent
-    }
-    return markup + '>'
-  }
+  const get = cache && normalizeAsync(cache, 'get')
+  const has = cache && normalizeAsync(cache, 'has')
 
   return function render (
     component: Component,
@@ -229,8 +252,16 @@ export function createRenderFunction (
     done: Function
   ) {
     warned = Object.create(null)
-    activeInstance = component
+    const context = {
+      activeInstance: component,
+      renderStates: [],
+      next: noop, // for flow
+      write, done,
+      isUnaryTag, modules, directives,
+      cache, get, has
+    }
+    context.next = nextFactory(context)
     normalizeRender(component)
-    renderNode(component._render(), write, done, true)
+    renderNode(component._render(), true, context)
   }
 }

+ 1 - 1
src/server/write.js

@@ -3,7 +3,7 @@
 const MAX_STACK_DEPTH = 1000
 
 export function createWriteFunction (
-  write: Function,
+  write: (text: string, next: Function) => ?boolean,
   onError: Function
 ): Function {
   let stackDepth = 0

+ 24 - 0
test/ssr/ssr-stream.spec.js

@@ -76,4 +76,28 @@ describe('SSR: renderToStream', () => {
     })
     stream.on('data', _ => _)
   })
+
+  it('should not mingle two components', done => {
+    const padding = (new Array(20000)).join('x')
+    const component1 = new Vue({
+      template: `<div>${padding}<div></div></div>`,
+      _scopeId: '_component1'
+    })
+    const component2 = new Vue({
+      template: `<div></div>`,
+      _scopeId: '_component2'
+    })
+    var stream1 = renderToStream(component1)
+    var stream2 = renderToStream(component2)
+    var res = ''
+    stream1.on('data', (text) => {
+      res += text.toString('utf-8').replace(/x/g, '')
+    })
+    stream1.on('end', () => {
+      expect(res).not.toContain('_component2')
+      done()
+    })
+    stream1.read(1)
+    stream2.read(1)
+  })
 })