From b69ea00f5c9a3f134e0096602db7c50cc3f89ba8 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 28 May 2019 13:27:31 +0800 Subject: [PATCH] wip: minimal component implementation --- packages/runtime-core/src/component.ts | 45 ++++++++ packages/runtime-core/src/createRenderer.ts | 111 +++++++++++++++----- packages/runtime-core/src/vnode.ts | 28 ++++- packages/runtime-dom/src/rendererOptions.ts | 2 + 4 files changed, 158 insertions(+), 28 deletions(-) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 9f6723024..e6d9ecb87 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -1 +1,46 @@ +import { VNode, normalizeVNode } from './vnode' + export class Component {} + +export function renderComponentRoot(instance: any): VNode { + return normalizeVNode(instance.render(instance.vnode.props)) +} + +export function shouldUpdateComponent( + prevVNode: VNode, + nextVNode: VNode +): boolean { + const { props: prevProps } = prevVNode + const { props: nextProps } = nextVNode + + // TODO handle slots + // If has different slots content, or has non-compiled slots, + // the child needs to be force updated. + // if ( + // prevChildFlags !== nextChildFlags || + // (nextChildFlags & ChildrenFlags.DYNAMIC_SLOTS) > 0 + // ) { + // return true + // } + + if (prevProps === nextProps) { + return false + } + if (prevProps === null) { + return nextProps !== null + } + if (nextProps === null) { + return prevProps !== null + } + const nextKeys = Object.keys(nextProps) + if (nextKeys.length !== Object.keys(prevProps).length) { + return true + } + for (let i = 0; i < nextKeys.length; i++) { + const key = nextKeys[i] + if (nextProps[key] !== prevProps[key]) { + return true + } + } + return false +} diff --git a/packages/runtime-core/src/createRenderer.ts b/packages/runtime-core/src/createRenderer.ts index 2ce239e3d..11a490aac 100644 --- a/packages/runtime-core/src/createRenderer.ts +++ b/packages/runtime-core/src/createRenderer.ts @@ -13,12 +13,14 @@ import { Text, Fragment, Empty, - createVNode, + normalizeVNode, VNode, VNodeChildren } from './vnode.js' import { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' +import { effect } from '@vue/observer' import { isString, isFunction, isArray } from '@vue/shared' +import { renderComponentRoot, shouldUpdateComponent } from './component.js' const emptyArr: any[] = [] const emptyObj: { [key: string]: any } = {} @@ -46,6 +48,7 @@ export interface RendererOptions { createComment(text: string): HostNode setText(node: HostNode, text: string): void setElementText(node: HostNode, text: string): void + parentNode(node: HostNode): HostNode | null nextSibling(node: HostNode): HostNode | null } @@ -59,6 +62,7 @@ export function createRenderer(options: RendererOptions) { createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, + parentNode: hostParentNode, nextSibling: hostNextSibling } = options @@ -71,7 +75,7 @@ export function createRenderer(options: RendererOptions) { ) { // patching & not same type, unmount old tree if (n1 != null && !isSameType(n1, n2)) { - anchor = hostNextSibling(n1.anchor || n1.el) + anchor = getNextHostNode(n1) unmount(n1, true) n1 = null } @@ -160,27 +164,11 @@ export function createRenderer(options: RendererOptions) { start: number = 0 ) { for (let i = start; i < children.length; i++) { - const child = (children[i] = normalizeChild(children[i])) + const child = (children[i] = normalizeVNode(children[i])) patch(null, child, container, anchor) } } - function normalizeChild(child: any): VNode { - if (child == null) { - // empty placeholder - return createVNode(Empty) - } else if (isArray(child)) { - // fragment - return createVNode(Fragment, null, child) - } else if (typeof child === 'object') { - // already vnode - return child as VNode - } else { - // primitive types - return createVNode(Text, null, child + '') - } - } - function patchElement(n1: VNode, n2: VNode, optimized?: boolean) { const el = (n2.el = n1.el) const { patchFlag, dynamicChildren } = n2 @@ -328,7 +316,65 @@ export function createRenderer(options: RendererOptions) { n2: VNode, container: HostNode, anchor?: HostNode - ) {} + ) { + if (n1 == null) { + mountComponent(n2, container, anchor) + } else { + updateComponent(n1.component, n2, container, anchor) + } + } + + function mountComponent( + vnode: VNode, + container: HostNode, + anchor?: HostNode + ) { + const instance = (vnode.component = { + vnode: null, + subTree: null, + updateHandle: null, + render: vnode.type + } as any) + + instance.updateHandle = effect( + () => { + if (!instance.vnode) { + // initial mount + instance.vnode = vnode + const subTree = (instance.subTree = renderComponentRoot(instance)) + patch(null, subTree, container, anchor) + vnode.el = subTree.el + } else { + updateComponent(instance, vnode) + } + }, + { + scheduler: e => e() // TODO use proper scheduler + } + ) as any + } + + function updateComponent( + instance: any, + next: VNode, + container?: HostNode, + anchor?: HostNode + ) { + const prev = instance.vnode + instance.vnode = next + next.component = instance + if (shouldUpdateComponent(prev, next)) { + const prevTree = instance.subTree + const nextTree = (instance.subTree = renderComponentRoot(instance)) + patch( + prevTree, + nextTree, + container || hostParentNode(prevTree.el), + anchor || getNextHostNode(prevTree) + ) + next.el = nextTree.el + } + } function patchChildren( n1: VNode | null, @@ -405,7 +451,7 @@ export function createRenderer(options: RendererOptions) { const commonLength = Math.min(oldLength, newLength) let i for (i = 0; i < commonLength; i++) { - const nextChild = (c2[i] = normalizeChild(c2[i])) + const nextChild = (c2[i] = normalizeVNode(c2[i])) patch(c1[i], nextChild, container, null, optimized) } if (oldLength > newLength) { @@ -435,7 +481,7 @@ export function createRenderer(options: RendererOptions) { // (a b) d e while (i <= e1 && i <= e2) { const n1 = c1[i] - const n2 = (c2[i] = normalizeChild(c2[i])) + const n2 = (c2[i] = normalizeVNode(c2[i])) if (isSameType(n1, n2)) { patch(n1, n2, container, parentAnchor, optimized) } else { @@ -449,7 +495,7 @@ export function createRenderer(options: RendererOptions) { // d e (b c) while (i <= e1 && i <= e2) { const n1 = c1[e1] - const n2 = (c2[e2] = normalizeChild(c2[e2])) + const n2 = (c2[e2] = normalizeVNode(c2[e2])) if (isSameType(n1, n2)) { patch(n1, n2, container, parentAnchor, optimized) } else { @@ -471,7 +517,7 @@ export function createRenderer(options: RendererOptions) { const nextPos = e2 + 1 const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor while (i <= e2) { - patch(null, (c2[i] = normalizeChild(c2[i])), container, anchor) + patch(null, (c2[i] = normalizeVNode(c2[i])), container, anchor) i++ } } @@ -502,7 +548,7 @@ export function createRenderer(options: RendererOptions) { // 5.1 build key:index map for newChildren const keyToNewIndexMap: Map = new Map() for (i = s2; i <= e2; i++) { - const nextChild = (c2[i] = normalizeChild(c2[i])) + const nextChild = (c2[i] = normalizeVNode(c2[i])) if (nextChild.key != null) { // TODO warn duplicate keys keyToNewIndexMap.set(nextChild.key, i) @@ -588,6 +634,10 @@ export function createRenderer(options: RendererOptions) { } function move(vnode: VNode, container: HostNode, anchor: HostNode) { + if (vnode.component != null) { + move(vnode.component.subTree, container, anchor) + return + } if (vnode.type === Fragment) { hostInsert(vnode.el, container, anchor) const children = vnode.children as VNode[] @@ -601,6 +651,11 @@ export function createRenderer(options: RendererOptions) { } function unmount(vnode: VNode, doRemove?: boolean) { + if (vnode.component != null) { + // TODO teardown component + unmount(vnode.component.subTree, doRemove) + return + } const shouldRemoveChildren = vnode.type === Fragment && doRemove if (vnode.dynamicChildren != null) { unmountChildren(vnode.dynamicChildren, shouldRemoveChildren) @@ -623,6 +678,12 @@ export function createRenderer(options: RendererOptions) { } } + function getNextHostNode(vnode: VNode): HostNode { + return vnode.component === null + ? hostNextSibling(vnode.anchor || vnode.el) + : getNextHostNode(vnode.component.subTree) + } + return function render(vnode: VNode, dom: HostNode): VNode { patch(dom._vnode, vnode, dom) return (dom._vnode = vnode) diff --git a/packages/runtime-core/src/vnode.ts b/packages/runtime-core/src/vnode.ts index 4bac668a9..e32fc00e3 100644 --- a/packages/runtime-core/src/vnode.ts +++ b/packages/runtime-core/src/vnode.ts @@ -1,4 +1,4 @@ -import { isFunction } from '@vue/shared' +import { isArray, isFunction } from '@vue/shared' export const Fragment = Symbol('Fragment') export const Text = Symbol('Text') @@ -15,12 +15,17 @@ export type VNodeChild = VNode | string | number | null export interface VNodeChildren extends Array {} export interface VNode { - el: any - anchor: any // fragment anchor type: VNodeTypes props: { [key: string]: any } | null key: string | number | null children: string | VNodeChildren | null + component: any + + // DOM + el: any + anchor: any // fragment anchor + + // optimization only patchFlag: number | null dynamicProps: string[] | null dynamicChildren: VNode[] | null @@ -68,6 +73,7 @@ export function createVNode( props, key: props && props.key, children, + component: null, el: null, anchor: null, patchFlag, @@ -91,3 +97,19 @@ export function cloneVNode(vnode: VNode): VNode { // TODO return vnode } + +export function normalizeVNode(child: any): VNode { + if (child == null) { + // empty placeholder + return createVNode(Empty) + } else if (isArray(child)) { + // fragment + return createVNode(Fragment, null, child) + } else if (typeof child === 'object') { + // already vnode + return child as VNode + } else { + // primitive types + return createVNode(Text, null, child + '') + } +} diff --git a/packages/runtime-dom/src/rendererOptions.ts b/packages/runtime-dom/src/rendererOptions.ts index a1c5b3dd2..7ac294385 100644 --- a/packages/runtime-dom/src/rendererOptions.ts +++ b/packages/runtime-dom/src/rendererOptions.ts @@ -37,5 +37,7 @@ export const DOMRendererOptions: RendererOptions = { el.textContent = text }, + parentNode: (node: Node): Node | null => node.parentNode, + nextSibling: (node: Node): Node | null => node.nextSibling }