wip: minimal component implementation

This commit is contained in:
Evan You 2019-05-28 13:27:31 +08:00
parent 28a0c50357
commit b69ea00f5c
4 changed files with 158 additions and 28 deletions

View File

@ -1 +1,46 @@
import { VNode, normalizeVNode } from './vnode'
export class Component {} 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
}

View File

@ -13,12 +13,14 @@ import {
Text, Text,
Fragment, Fragment,
Empty, Empty,
createVNode, normalizeVNode,
VNode, VNode,
VNodeChildren VNodeChildren
} from './vnode.js' } from './vnode.js'
import { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' import { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags'
import { effect } from '@vue/observer'
import { isString, isFunction, isArray } from '@vue/shared' import { isString, isFunction, isArray } from '@vue/shared'
import { renderComponentRoot, shouldUpdateComponent } from './component.js'
const emptyArr: any[] = [] const emptyArr: any[] = []
const emptyObj: { [key: string]: any } = {} const emptyObj: { [key: string]: any } = {}
@ -46,6 +48,7 @@ export interface RendererOptions {
createComment(text: string): HostNode createComment(text: string): HostNode
setText(node: HostNode, text: string): void setText(node: HostNode, text: string): void
setElementText(node: HostNode, text: string): void setElementText(node: HostNode, text: string): void
parentNode(node: HostNode): HostNode | null
nextSibling(node: HostNode): HostNode | null nextSibling(node: HostNode): HostNode | null
} }
@ -59,6 +62,7 @@ export function createRenderer(options: RendererOptions) {
createComment: hostCreateComment, createComment: hostCreateComment,
setText: hostSetText, setText: hostSetText,
setElementText: hostSetElementText, setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling nextSibling: hostNextSibling
} = options } = options
@ -71,7 +75,7 @@ export function createRenderer(options: RendererOptions) {
) { ) {
// patching & not same type, unmount old tree // patching & not same type, unmount old tree
if (n1 != null && !isSameType(n1, n2)) { if (n1 != null && !isSameType(n1, n2)) {
anchor = hostNextSibling(n1.anchor || n1.el) anchor = getNextHostNode(n1)
unmount(n1, true) unmount(n1, true)
n1 = null n1 = null
} }
@ -160,27 +164,11 @@ export function createRenderer(options: RendererOptions) {
start: number = 0 start: number = 0
) { ) {
for (let i = start; i < children.length; i++) { 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) 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) { function patchElement(n1: VNode, n2: VNode, optimized?: boolean) {
const el = (n2.el = n1.el) const el = (n2.el = n1.el)
const { patchFlag, dynamicChildren } = n2 const { patchFlag, dynamicChildren } = n2
@ -328,7 +316,65 @@ export function createRenderer(options: RendererOptions) {
n2: VNode, n2: VNode,
container: HostNode, container: HostNode,
anchor?: 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( function patchChildren(
n1: VNode | null, n1: VNode | null,
@ -405,7 +451,7 @@ export function createRenderer(options: RendererOptions) {
const commonLength = Math.min(oldLength, newLength) const commonLength = Math.min(oldLength, newLength)
let i let i
for (i = 0; i < commonLength; 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) patch(c1[i], nextChild, container, null, optimized)
} }
if (oldLength > newLength) { if (oldLength > newLength) {
@ -435,7 +481,7 @@ export function createRenderer(options: RendererOptions) {
// (a b) d e // (a b) d e
while (i <= e1 && i <= e2) { while (i <= e1 && i <= e2) {
const n1 = c1[i] const n1 = c1[i]
const n2 = (c2[i] = normalizeChild(c2[i])) const n2 = (c2[i] = normalizeVNode(c2[i]))
if (isSameType(n1, n2)) { if (isSameType(n1, n2)) {
patch(n1, n2, container, parentAnchor, optimized) patch(n1, n2, container, parentAnchor, optimized)
} else { } else {
@ -449,7 +495,7 @@ export function createRenderer(options: RendererOptions) {
// d e (b c) // d e (b c)
while (i <= e1 && i <= e2) { while (i <= e1 && i <= e2) {
const n1 = c1[e1] const n1 = c1[e1]
const n2 = (c2[e2] = normalizeChild(c2[e2])) const n2 = (c2[e2] = normalizeVNode(c2[e2]))
if (isSameType(n1, n2)) { if (isSameType(n1, n2)) {
patch(n1, n2, container, parentAnchor, optimized) patch(n1, n2, container, parentAnchor, optimized)
} else { } else {
@ -471,7 +517,7 @@ export function createRenderer(options: RendererOptions) {
const nextPos = e2 + 1 const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) { while (i <= e2) {
patch(null, (c2[i] = normalizeChild(c2[i])), container, anchor) patch(null, (c2[i] = normalizeVNode(c2[i])), container, anchor)
i++ i++
} }
} }
@ -502,7 +548,7 @@ export function createRenderer(options: RendererOptions) {
// 5.1 build key:index map for newChildren // 5.1 build key:index map for newChildren
const keyToNewIndexMap: Map<any, number> = new Map() const keyToNewIndexMap: Map<any, number> = new Map()
for (i = s2; i <= e2; i++) { for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = normalizeChild(c2[i])) const nextChild = (c2[i] = normalizeVNode(c2[i]))
if (nextChild.key != null) { if (nextChild.key != null) {
// TODO warn duplicate keys // TODO warn duplicate keys
keyToNewIndexMap.set(nextChild.key, i) keyToNewIndexMap.set(nextChild.key, i)
@ -588,6 +634,10 @@ export function createRenderer(options: RendererOptions) {
} }
function move(vnode: VNode, container: HostNode, anchor: HostNode) { function move(vnode: VNode, container: HostNode, anchor: HostNode) {
if (vnode.component != null) {
move(vnode.component.subTree, container, anchor)
return
}
if (vnode.type === Fragment) { if (vnode.type === Fragment) {
hostInsert(vnode.el, container, anchor) hostInsert(vnode.el, container, anchor)
const children = vnode.children as VNode[] const children = vnode.children as VNode[]
@ -601,6 +651,11 @@ export function createRenderer(options: RendererOptions) {
} }
function unmount(vnode: VNode, doRemove?: boolean) { 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 const shouldRemoveChildren = vnode.type === Fragment && doRemove
if (vnode.dynamicChildren != null) { if (vnode.dynamicChildren != null) {
unmountChildren(vnode.dynamicChildren, shouldRemoveChildren) 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 { return function render(vnode: VNode, dom: HostNode): VNode {
patch(dom._vnode, vnode, dom) patch(dom._vnode, vnode, dom)
return (dom._vnode = vnode) return (dom._vnode = vnode)

View File

@ -1,4 +1,4 @@
import { isFunction } from '@vue/shared' import { isArray, isFunction } from '@vue/shared'
export const Fragment = Symbol('Fragment') export const Fragment = Symbol('Fragment')
export const Text = Symbol('Text') export const Text = Symbol('Text')
@ -15,12 +15,17 @@ export type VNodeChild = VNode | string | number | null
export interface VNodeChildren extends Array<VNodeChildren | VNodeChild> {} export interface VNodeChildren extends Array<VNodeChildren | VNodeChild> {}
export interface VNode { export interface VNode {
el: any
anchor: any // fragment anchor
type: VNodeTypes type: VNodeTypes
props: { [key: string]: any } | null props: { [key: string]: any } | null
key: string | number | null key: string | number | null
children: string | VNodeChildren | null children: string | VNodeChildren | null
component: any
// DOM
el: any
anchor: any // fragment anchor
// optimization only
patchFlag: number | null patchFlag: number | null
dynamicProps: string[] | null dynamicProps: string[] | null
dynamicChildren: VNode[] | null dynamicChildren: VNode[] | null
@ -68,6 +73,7 @@ export function createVNode(
props, props,
key: props && props.key, key: props && props.key,
children, children,
component: null,
el: null, el: null,
anchor: null, anchor: null,
patchFlag, patchFlag,
@ -91,3 +97,19 @@ export function cloneVNode(vnode: VNode): VNode {
// TODO // TODO
return vnode 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 + '')
}
}

View File

@ -37,5 +37,7 @@ export const DOMRendererOptions: RendererOptions = {
el.textContent = text el.textContent = text
}, },
parentNode: (node: Node): Node | null => node.parentNode,
nextSibling: (node: Node): Node | null => node.nextSibling nextSibling: (node: Node): Node | null => node.nextSibling
} }