From c8e91ab24ee7eb7f4d75af893f9bb8b873a14aa3 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 25 Apr 2016 22:23:16 -0400 Subject: [PATCH] client-side hydration --- src/core/vdom/create-component.js | 6 ++- src/core/vdom/patch.js | 85 +++++++++++++++++++++++++++---- src/platforms/web/util/attrs.js | 2 +- 3 files changed, 81 insertions(+), 12 deletions(-) diff --git a/src/core/vdom/create-component.js b/src/core/vdom/create-component.js index 7310bae3..52944d35 100644 --- a/src/core/vdom/create-component.js +++ b/src/core/vdom/create-component.js @@ -74,9 +74,11 @@ function init (vnode) { _parentListeners: listeners, _renderChildren: children }) - // the child sets the parent vnode's elm when mounted + // if this is a server-rendered mount, + // the vnode would already have an element. + // otherwise the child sets the parent vnode's elm when mounted // and when updated. - child.$mount() + child.$mount(vnode.elm) vnode.child = child } diff --git a/src/core/vdom/patch.js b/src/core/vdom/patch.js index b0ba1129..ae016b01 100644 --- a/src/core/vdom/patch.js +++ b/src/core/vdom/patch.js @@ -8,7 +8,7 @@ import VNode from './vnode' import { isPrimitive, warn } from '../util/index' const emptyNode = VNode('', {}, []) -const hooks = ['create', 'update', 'remove', 'destroy', 'pre', 'post'] +const hooks = ['create', 'update', 'remove', 'destroy'] function isUndef (s) { return s === undefined @@ -283,10 +283,64 @@ export function createPatchFunction (backend) { } } + function invokeInsertHook (queue) { + for (i = 0; i < queue.length; ++i) { + queue[i].data.hook.insert(queue[i]) + } + } + + function hydrate (elm, vnode, insertedVnodeQueue) { + if (process.env.NODE_ENV !== 'production') { + if (!assertNodeMatch(elm, vnode)) { + return false + } + } + vnode.elm = elm + const { tag, data, children } = vnode + if (isDef(data)) { + if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode) + if (isDef(i = vnode.child)) { + // child component. it should have hydrated its own tree. + invokeCreateHooks(vnode, insertedVnodeQueue) + return true + } + } + if (isDef(tag)) { + if (isDef(children)) { + const childNodes = elm.childNodes + for (let i = 0; i < children.length; i++) { + let success = hydrate(childNodes[i], children[i], insertedVnodeQueue) + if (!success) { + return false + } + } + } + if (isDef(data)) { + invokeCreateHooks(vnode, insertedVnodeQueue) + } + } + return true + } + + function assertNodeMatch (node, vnode) { + if (vnode.tag) { + if (vnode.tag.indexOf('vue-component' === 0)) { + return true + } else { + return vnode.tag === node.tagName.toLowerCase() && ( + vnode.children + ? vnode.children.length === node.childNodes.length + : node.childNodes.length === 0 + ) + } + } else { + return vnode.text === node.textContent + } + } + return function patch (oldVnode, vnode) { - var i, elm, parent + var elm, parent var insertedVnodeQueue = [] - for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]() if (!oldVnode) { // empty mount, create new root element @@ -295,9 +349,25 @@ export function createPatchFunction (backend) { if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue) } else { - // mounting to a real element, just replace it - // TODO: pick up and reuse existing elements (hydration) if (isUndef(oldVnode.tag)) { + // mounting to a real element + // check if this is server-rendered content and if we can perform + // a successful hydration. + if (oldVnode.hasAttribute('server-rendered')) { + oldVnode.removeAttribute('server-rendered') + if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { + invokeInsertHook(insertedVnodeQueue) + return oldVnode + } else if (process.env.NODE_ENV !== 'production') { + warn( + 'The client-side rendered virtual DOM tree is not matching ' + + 'server-rendered content. Bailing hydration and performing ' + + 'full client-side render.' + ) + } + } + // either not server-rendered, or hydration failed. + // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) } elm = oldVnode.elm @@ -312,10 +382,7 @@ export function createPatchFunction (backend) { } } - for (i = 0; i < insertedVnodeQueue.length; ++i) { - insertedVnodeQueue[i].data.hook.insert(insertedVnodeQueue[i]) - } - for (i = 0; i < cbs.post.length; ++i) cbs.post[i]() + invokeInsertHook(insertedVnodeQueue) return vnode.elm } } diff --git a/src/platforms/web/util/attrs.js b/src/platforms/web/util/attrs.js index 06c2506e..e3ebab55 100644 --- a/src/platforms/web/util/attrs.js +++ b/src/platforms/web/util/attrs.js @@ -18,7 +18,7 @@ export const propsToAttrMap = { acceptCharset: 'accept-charset', className: 'class', htmlFor: 'for', - httpEquiv: 'http-equiv', + httpEquiv: 'http-equiv' } export const xlinkNS = 'http://www.w3.org/1999/xlink'