From 6b505dcd23ac757a5f31bf6696e41410065b7131 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 13 Feb 2020 17:47:00 -0500 Subject: [PATCH] wip(ssr): basic element hydration --- .../__snapshots__/scopeId.spec.ts.snap | 4 +- .../compiler-core/__tests__/scopeId.spec.ts | 10 +- .../__snapshots__/hoistStatic.spec.ts.snap | 24 ++-- .../__snapshots__/vFor.spec.ts.snap | 2 +- .../src/transforms/hoistStatic.ts | 3 +- .../src/transforms/transformElement.ts | 34 +++-- .../__snapshots__/vShow.spec.ts.snap | 2 +- packages/runtime-core/src/renderer.ts | 119 +++++++++++++++++- packages/runtime-core/src/vnode.ts | 7 +- packages/runtime-dom/src/index.ts | 7 +- packages/shared/src/patchFlags.ts | 33 +++-- 11 files changed, 202 insertions(+), 43 deletions(-) diff --git a/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap b/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap index 8a149785..47e28dc2 100644 --- a/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap +++ b/packages/compiler-core/__tests__/__snapshots__/scopeId.spec.ts.snap @@ -5,8 +5,8 @@ exports[`scopeId compiler support should push scopeId for hoisted nodes 1`] = ` const _withId = _withScopeId(\\"test\\") _pushScopeId(\\"test\\") -const _hoisted_1 = _createVNode(\\"div\\", null, \\"hello\\", -1) -const _hoisted_2 = _createVNode(\\"div\\", null, \\"world\\", -1) +const _hoisted_1 = _createVNode(\\"div\\", null, \\"hello\\", -2 /* HOISTED */) +const _hoisted_2 = _createVNode(\\"div\\", null, \\"world\\", -2 /* HOISTED */) _popScopeId() export const render = _withId(function render(_ctx, _cache) { diff --git a/packages/compiler-core/__tests__/scopeId.spec.ts b/packages/compiler-core/__tests__/scopeId.spec.ts index 000b9373..9b098a12 100644 --- a/packages/compiler-core/__tests__/scopeId.spec.ts +++ b/packages/compiler-core/__tests__/scopeId.spec.ts @@ -4,6 +4,8 @@ import { PUSH_SCOPE_ID, POP_SCOPE_ID } from '../src/runtimeHelpers' +import { PatchFlags } from '@vue/shared' +import { genFlagText } from './testUtils' describe('scopeId compiler support', () => { test('should only work in module mode', () => { @@ -81,8 +83,12 @@ describe('scopeId compiler support', () => { expect(code).toMatch( [ `_pushScopeId("test")`, - `const _hoisted_1 = _createVNode("div", null, "hello", -1)`, - `const _hoisted_2 = _createVNode("div", null, "world", -1)`, + `const _hoisted_1 = _createVNode("div", null, "hello", ${genFlagText( + PatchFlags.HOISTED + )})`, + `const _hoisted_2 = _createVNode("div", null, "world", ${genFlagText( + PatchFlags.HOISTED + )})`, `_popScopeId()` ].join('\n') ) diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap index 1667d09e..5912db0a 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/hoistStatic.spec.ts.snap @@ -4,7 +4,7 @@ exports[`compiler: hoistStatic transform hoist element with static key 1`] = ` "const _Vue = Vue const { createVNode: _createVNode } = _Vue -const _hoisted_1 = _createVNode(\\"div\\", { key: \\"foo\\" }, null, -1) +const _hoisted_1 = _createVNode(\\"div\\", { key: \\"foo\\" }, null, -2 /* HOISTED */) return function render(_ctx, _cache) { with (this) { @@ -24,7 +24,7 @@ const { createVNode: _createVNode } = _Vue const _hoisted_1 = _createVNode(\\"p\\", null, [ _createVNode(\\"span\\"), _createVNode(\\"span\\") -], -1) +], -2 /* HOISTED */) return function render(_ctx, _cache) { with (this) { @@ -43,7 +43,7 @@ const { createVNode: _createVNode, createCommentVNode: _createCommentVNode } = _ const _hoisted_1 = _createVNode(\\"div\\", null, [ _createCommentVNode(\\"comment\\") -], -1) +], -2 /* HOISTED */) return function render(_ctx, _cache) { with (this) { @@ -60,8 +60,8 @@ exports[`compiler: hoistStatic transform hoist siblings with common non-hoistabl "const _Vue = Vue const { createVNode: _createVNode } = _Vue -const _hoisted_1 = _createVNode(\\"span\\", null, null, -1) -const _hoisted_2 = _createVNode(\\"div\\", null, null, -1) +const _hoisted_1 = _createVNode(\\"span\\", null, null, -2 /* HOISTED */) +const _hoisted_2 = _createVNode(\\"div\\", null, null, -2 /* HOISTED */) return function render(_ctx, _cache) { with (this) { @@ -79,7 +79,7 @@ exports[`compiler: hoistStatic transform hoist simple element 1`] = ` "const _Vue = Vue const { createVNode: _createVNode } = _Vue -const _hoisted_1 = _createVNode(\\"span\\", { class: \\"inline\\" }, \\"hello\\", -1) +const _hoisted_1 = _createVNode(\\"span\\", { class: \\"inline\\" }, \\"hello\\", -2 /* HOISTED */) return function render(_ctx, _cache) { with (this) { @@ -105,7 +105,7 @@ return function render(_ctx, _cache) { const _directive_foo = _resolveDirective(\\"foo\\") return (_openBlock(), _createBlock(\\"div\\", null, [ - _withDirectives(_createVNode(\\"div\\", _hoisted_1, null, 32 /* NEED_PATCH */), [ + _withDirectives(_createVNode(\\"div\\", _hoisted_1, null, -1 /* NEED_PATCH */), [ [_directive_foo] ]) ])) @@ -172,7 +172,7 @@ exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static t "const _Vue = Vue const { createVNode: _createVNode } = _Vue -const _hoisted_1 = _createVNode(\\"span\\", null, \\"foo \\" + _toDisplayString(1) + \\" \\" + _toDisplayString(true), -1) +const _hoisted_1 = _createVNode(\\"span\\", null, \\"foo \\" + _toDisplayString(1) + \\" \\" + _toDisplayString(true), -2 /* HOISTED */) return function render(_ctx, _cache) { with (this) { @@ -189,7 +189,7 @@ exports[`compiler: hoistStatic transform prefixIdentifiers hoist nested static t "const _Vue = Vue const { createVNode: _createVNode } = _Vue -const _hoisted_1 = _createVNode(\\"span\\", { foo: 0 }, _toDisplayString(1), -1) +const _hoisted_1 = _createVNode(\\"span\\", { foo: 0 }, _toDisplayString(1), -2 /* HOISTED */) return function render(_ctx, _cache) { with (this) { @@ -323,7 +323,7 @@ return function render(_ctx, _cache) { const { createVNode: _createVNode, openBlock: _openBlock, createBlock: _createBlock } = _Vue return (_openBlock(), _createBlock(\\"div\\", null, [ - _createVNode(\\"div\\", { ref: foo }, null, 32 /* NEED_PATCH */) + _createVNode(\\"div\\", { ref: foo }, null, -1 /* NEED_PATCH */) ])) } }" @@ -346,7 +346,7 @@ exports[`compiler: hoistStatic transform should hoist v-for children if static 1 const { createVNode: _createVNode } = _Vue const _hoisted_1 = { id: \\"foo\\" } -const _hoisted_2 = _createVNode(\\"span\\", null, null, -1) +const _hoisted_2 = _createVNode(\\"span\\", null, null, -2 /* HOISTED */) return function render(_ctx, _cache) { with (this) { @@ -371,7 +371,7 @@ const _hoisted_1 = { key: 0, id: \\"foo\\" } -const _hoisted_2 = _createVNode(\\"span\\", null, null, -1) +const _hoisted_2 = _createVNode(\\"span\\", null, null, -2 /* HOISTED */) return function render(_ctx, _cache) { with (this) { diff --git a/packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap b/packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap index 9b721ef8..0d549e38 100644 --- a/packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap +++ b/packages/compiler-core/__tests__/transforms/__snapshots__/vFor.spec.ts.snap @@ -142,7 +142,7 @@ return function render(_ctx, _cache) { const _directive_foo = _resolveDirective(\\"foo\\") return (_openBlock(true), _createBlock(_Fragment, null, _renderList(list, (i) => { - return _withDirectives((_openBlock(), _createBlock(\\"div\\", null, null, 32 /* NEED_PATCH */)), [ + return _withDirectives((_openBlock(), _createBlock(\\"div\\", null, null, -1 /* NEED_PATCH */)), [ [_directive_foo] ]) }), 256 /* UNKEYED_FRAGMENT */)) diff --git a/packages/compiler-core/src/transforms/hoistStatic.ts b/packages/compiler-core/src/transforms/hoistStatic.ts index 06d9eaf7..da44a1ec 100644 --- a/packages/compiler-core/src/transforms/hoistStatic.ts +++ b/packages/compiler-core/src/transforms/hoistStatic.ts @@ -52,7 +52,8 @@ function walk( ) { if (!doNotHoistNode && isStaticNode(child, resultCache)) { // whole tree is static - ;(child.codegenNode as VNodeCall).patchFlag = PatchFlags.HOISTED + `` + ;(child.codegenNode as VNodeCall).patchFlag = + PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``) const hoisted = context.transformHoist ? context.transformHoist(child, context) : child.codegenNode! diff --git a/packages/compiler-core/src/transforms/transformElement.ts b/packages/compiler-core/src/transforms/transformElement.ts index 2d8eead6..2e481254 100644 --- a/packages/compiler-core/src/transforms/transformElement.ts +++ b/packages/compiler-core/src/transforms/transformElement.ts @@ -20,7 +20,7 @@ import { DirectiveArguments, createVNodeCall } from '../ast' -import { PatchFlags, PatchFlagNames, isSymbol } from '@vue/shared' +import { PatchFlags, PatchFlagNames, isSymbol, isOn } from '@vue/shared' import { createCompilerError, ErrorCodes } from '../errors' import { RESOLVE_DIRECTIVE, @@ -159,12 +159,18 @@ export const transformElement: NodeTransform = (node, context) => { // patchFlag & dynamicPropNames if (patchFlag !== 0) { if (__DEV__) { - const flagNames = Object.keys(PatchFlagNames) - .map(Number) - .filter(n => n > 0 && patchFlag & n) - .map(n => PatchFlagNames[n]) - .join(`, `) - vnodePatchFlag = patchFlag + ` /* ${flagNames} */` + if (patchFlag < 0) { + // special flags (negative and mutually exclusive) + vnodePatchFlag = patchFlag + ` /* ${PatchFlagNames[patchFlag]} */` + } else { + // bitwise flags + const flagNames = Object.keys(PatchFlagNames) + .map(Number) + .filter(n => n > 0 && patchFlag & n) + .map(n => PatchFlagNames[n]) + .join(`, `) + vnodePatchFlag = patchFlag + ` /* ${flagNames} */` + } } else { vnodePatchFlag = String(patchFlag) } @@ -256,20 +262,27 @@ export function buildProps( let hasRef = false let hasClassBinding = false let hasStyleBinding = false + let hasHydrationEventBinding = false let hasDynamicKeys = false const dynamicPropNames: string[] = [] const analyzePatchFlag = ({ key, value }: Property) => { if (key.type === NodeTypes.SIMPLE_EXPRESSION && key.isStatic) { + const name = key.content + if (!isComponent && isOn(name) && name.toLowerCase() !== 'onclick') { + // This flag is for hydrating event handlers only. We omit the flag for + // click handlers becaues hydration gives click dedicated fast path. + hasHydrationEventBinding = true + } if ( value.type === NodeTypes.JS_CACHE_EXPRESSION || ((value.type === NodeTypes.SIMPLE_EXPRESSION || value.type === NodeTypes.COMPOUND_EXPRESSION) && isStaticNode(value)) ) { + // skip if the prop is a cached handler or has constant value return } - const name = key.content if (name === 'ref') { hasRef = true } else if (name === 'class') { @@ -430,9 +443,12 @@ export function buildProps( if (dynamicPropNames.length) { patchFlag |= PatchFlags.PROPS } + if (hasHydrationEventBinding) { + patchFlag |= PatchFlags.HYDRATE_EVENTS + } } if (patchFlag === 0 && (hasRef || runtimeDirectives.length > 0)) { - patchFlag |= PatchFlags.NEED_PATCH + patchFlag = PatchFlags.NEED_PATCH } return { diff --git a/packages/compiler-dom/__tests__/transforms/__snapshots__/vShow.spec.ts.snap b/packages/compiler-dom/__tests__/transforms/__snapshots__/vShow.spec.ts.snap index c392948c..4bf54571 100644 --- a/packages/compiler-dom/__tests__/transforms/__snapshots__/vShow.spec.ts.snap +++ b/packages/compiler-dom/__tests__/transforms/__snapshots__/vShow.spec.ts.snap @@ -7,7 +7,7 @@ return function render(_ctx, _cache) { with (this) { const { vShow: _vShow, createVNode: _createVNode, withDirectives: _withDirectives, openBlock: _openBlock, createBlock: _createBlock } = _Vue - return _withDirectives((_openBlock(), _createBlock(\\"div\\", null, null, 32 /* NEED_PATCH */)), [ + return _withDirectives((_openBlock(), _createBlock(\\"div\\", null, null, -1 /* NEED_PATCH */)), [ [_vShow, a] ]) } diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 01b66c48..3c0828df 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -30,7 +30,8 @@ import { isReservedProp, isFunction, PatchFlags, - NOOP + NOOP, + isOn } from '@vue/shared' import { queueJob, @@ -188,6 +189,7 @@ export function createRenderer< options: RendererOptions ): { render: RootRenderFunction + hydrate: RootRenderFunction createApp: CreateAppFunction } { type HostVNode = VNode @@ -426,8 +428,9 @@ export function createRenderer< // props if (props != null) { for (const key in props) { - if (isReservedProp(key)) continue - hostPatchProp(el, key, props[key], null, isSVG) + if (!isReservedProp(key)) { + hostPatchProp(el, key, props[key], null, isSVG) + } } if (props.onVnodeBeforeMount != null) { invokeDirectiveHook(props.onVnodeBeforeMount, parentComponent, vnode) @@ -1813,8 +1816,118 @@ export function createRenderer< container._vnode = vnode } + function hydrate(vnode: HostVNode, container: any) { + hydrateNode(container.firstChild, vnode, container) + flushPostFlushCbs() + } + + // TODO handle mismatches + function hydrateNode( + node: any, + vnode: HostVNode, + container: any, + parentComponent: ComponentInternalInstance | null = null + ): any { + const { type, shapeFlag } = vnode + switch (type) { + case Text: + case Comment: + case Static: + vnode.el = node + return node.nextSibling + case Fragment: + vnode.el = node + const anchor = (vnode.anchor = hydrateChildren( + node.nextSibling, + vnode.children as HostVNode[], + container, + parentComponent + )) + return anchor.nextSibling + case Portal: + // TODO + break + default: + if (shapeFlag & ShapeFlags.ELEMENT) { + return hydrateElement(node, vnode, parentComponent) + } else if (shapeFlag & ShapeFlags.COMPONENT) { + // TODO + } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { + // TODO + } else if (__DEV__) { + warn('Invalid HostVNode type:', type, `(${typeof type})`) + } + } + } + + function hydrateElement( + el: any, + vnode: HostVNode, + parentComponent: ComponentInternalInstance | null + ) { + vnode.el = el + const { props, patchFlag } = vnode + // skip props & children if this is hoisted static nodes + if (patchFlag !== PatchFlags.HOISTED) { + // props + if (props !== null) { + if ( + patchFlag & PatchFlags.FULL_PROPS || + patchFlag & PatchFlags.HYDRATE_EVENTS + ) { + for (const key in props) { + if (!isReservedProp(key) && isOn(key)) { + hostPatchProp(el, key, props[key], null) + } + } + } else if (props.onClick != null) { + // Fast path for click listeners (which is most often) to avoid + // iterating through props. + hostPatchProp(el, 'onClick', props.onClick, null) + } + // vnode mounted hook + const { onVnodeMounted } = props + if (onVnodeMounted != null) { + queuePostFlushCb(() => { + invokeDirectiveHook(onVnodeMounted, parentComponent, vnode, null) + }) + } + } + // children + if ( + vnode.shapeFlag & ShapeFlags.ARRAY_CHILDREN && + // skip if element has innerHTML / textContent + !(props !== null && (props.innerHTML || props.textContent)) + ) { + hydrateChildren( + el.firstChild, + vnode.children as HostVNode[], + el, + parentComponent + ) + } + } + return el.nextSibling + } + + function hydrateChildren( + node: any, + vnodes: HostVNode[], + container: any, + parentComponent: ComponentInternalInstance | null = null + ) { + for (let i = 0; i < vnodes.length; i++) { + // TODO can skip normalizeVNode in optimized mode + // (need hint on rendered markup?) + const vnode = (vnodes[i] = normalizeVNode(vnodes[i])) + node = hydrateNode(node, vnode, container, parentComponent) + } + return node + } + return { render, + hydrate, createApp: createAppAPI(render) } } diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 475323b9..61d953a0 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -6,7 +6,8 @@ import { EMPTY_ARR, extend, normalizeClass, - normalizeStyle + normalizeStyle, + PatchFlags } from '@vue/shared' import { ComponentInternalInstance, @@ -277,7 +278,11 @@ export function createVNode( if ( shouldTrack > 0 && currentBlock !== null && + // the EVENTS flag is only for hydration and if it is the only flag, the + // vnode should not be considered dynamic. + patchFlag !== PatchFlags.HYDRATE_EVENTS && (patchFlag > 0 || + patchFlag === PatchFlags.NEED_PATCH || shapeFlag & ShapeFlags.SUSPENSE || shapeFlag & ShapeFlags.STATEFUL_COMPONENT || shapeFlag & ShapeFlags.FUNCTIONAL_COMPONENT) diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 9c9c1b59..8f4ec06f 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -9,13 +9,18 @@ import { patchProp } from './patchProp' // Importing from the compiler, will be tree-shaken in prod import { isFunction, isString, isHTMLTag, isSVGTag } from '@vue/shared' -const { render: baseRender, createApp: baseCreateApp } = createRenderer({ +const { + render: baseRender, + hydrate: baseHydrate, + createApp: baseCreateApp +} = createRenderer({ patchProp, ...nodeOps }) // use explicit type casts here to avoid import() calls in rolled-up d.ts export const render = baseRender as RootRenderFunction +export const hydrate = baseHydrate as RootRenderFunction export const createApp: CreateAppFunction = (...args) => { const app = baseCreateApp(...args) diff --git a/packages/shared/src/patchFlags.ts b/packages/shared/src/patchFlags.ts index 86dff111..d0f841b7 100644 --- a/packages/shared/src/patchFlags.ts +++ b/packages/shared/src/patchFlags.ts @@ -40,12 +40,9 @@ export const enum PatchFlags { // exclusive with CLASS, STYLE and PROPS. FULL_PROPS = 1 << 4, - // Indicates an element that only needs non-props patching, e.g. ref or - // directives (onVnodeXXX hooks). It simply marks the vnode as "need patch", - // since every patched vnode checks for refs and onVnodeXXX hooks. - // This flag is never directly matched against, it simply serves as a non-zero - // value. - NEED_PATCH = 1 << 5, + // Indicates an element with event listeners (which need to be attached + // during hydration) + HYDRATE_EVENTS = 1 << 5, // Indicates a fragment whose children order doesn't change. STABLE_FRAGMENT = 1 << 6, @@ -61,14 +58,28 @@ export const enum PatchFlags { // Components with this flag are always force updated. DYNAMIC_SLOTS = 1 << 9, - // A special flag that indicates a hoisted, static vnode. - HOISTED = -1, + // SPECIAL FLAGS ------------------------------------------------------------- + + // Special flags are negative integers. They are never matched against using + // bitwise operators (bitwise matching should only happen in branches where + // patchFlag > 0), and are mutually exclusive. When checking for a speical + // flag, simply check patchFlag === FLAG. + + // Indicates an element that only needs non-props patching, e.g. ref or + // directives (onVnodeXXX hooks). since every patched vnode checks for refs + // and onVnodeXXX hooks, itt simply marks the vnode so that a parent block + // will track it. + NEED_PATCH = -1, + + // Indicates a hoisted static vnode. This is a hint for hydration to skip + // the entire sub tree since static content never needs to be updated. + HOISTED = -2, // A special flag that indicates that the diffing algorithm should bail out // of optimized mode. This is only on block fragments created by renderSlot() // when encountering non-compiler generated slots (i.e. manually written // render functions, which should always be fully diffed) - BAIL = -2 + BAIL = -3 } // runtime object for public consumption @@ -91,11 +102,13 @@ export const PatchFlagNames = { [PatchFlags.CLASS]: `CLASS`, [PatchFlags.STYLE]: `STYLE`, [PatchFlags.PROPS]: `PROPS`, - [PatchFlags.NEED_PATCH]: `NEED_PATCH`, [PatchFlags.FULL_PROPS]: `FULL_PROPS`, + [PatchFlags.HYDRATE_EVENTS]: `HYDRATE_EVENTS`, [PatchFlags.STABLE_FRAGMENT]: `STABLE_FRAGMENT`, [PatchFlags.KEYED_FRAGMENT]: `KEYED_FRAGMENT`, [PatchFlags.UNKEYED_FRAGMENT]: `UNKEYED_FRAGMENT`, [PatchFlags.DYNAMIC_SLOTS]: `DYNAMIC_SLOTS`, + [PatchFlags.NEED_PATCH]: `NEED_PATCH`, + [PatchFlags.HOISTED]: `HOISTED`, [PatchFlags.BAIL]: `BAIL` }