// TODO: // - app context // - component // - lifecycle // - refs // - reused nodes // - hydration import { Text, Fragment, Empty, createVNode } from './h.js' import { TEXT, CLASS, STYLE, PROPS, KEYED, UNKEYED } from './patchFlags' const emptyArr: any[] = [] const emptyObj = {} const isSameType = (n1, n2) => n1.type === n2.type && n1.key === n2.key export function createRenderer(hostConfig) { const { insert, remove, patchProp: hostPatchProp, createElement: hostCreateElement, createText: hostCreateText, createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, nextSibling: hostNextSibling } = hostConfig function patch(n1, n2, container, anchor, optimized) { // patching & not same type, unmount old tree if (n1 != null && !isSameType(n1, n2)) { anchor = hostNextSibling(n1.el) unmount(n1, true) n1 = null } const { type } = n2 if (type === Text) { processText(n1, n2, container, anchor) } else if (type === Empty) { processEmptyNode(n1, n2, container, anchor) } else if (type === Fragment) { processFragment(n1, n2, container, anchor, optimized) } else if (typeof type === 'function') { // TODO Component } else { processElement(n1, n2, container, anchor, optimized) } } function processText(n1, n2, container, anchor) { if (n1 == null) { insert((n2.el = hostCreateText(n2.children)), container, anchor) } else { const el = (n2.el = n1.el) if (n2.children !== n1.children) { hostSetText(el, n2, children) } } } function processEmptyNode(n1, n2, container, anchor) { if (n1 == null) { insert((n2.el = hostCreateComment('')), container, anchor) } else { n2.el = n1.el } } function processElement(n1, n2, container, anchor, optimized) { // mount if (n1 == null) { mountElement(n2, container, anchor) } else { patchElement(n1, n2, container, optimized) } } function mountElement(vnode, container, anchor) { const el = (vnode.el = hostCreateElement(vnode.type)) if (vnode.props != null) { for (const key in vnode.props) { hostPatchProp(el, key, vnode.props[key], null) } } if (typeof vnode.children === 'string') { hostSetElementText(el, vnode.children) } else { mountChildren(vnode.children, el) } insert(el, container, anchor) } function mountChildren(children, container, anchor, start = 0) { for (let i = start; i < children.length; i++) { const child = (children[i] = normalizeChild(children[i])) patch(null, child, container, anchor) } } function normalizeChild(child) { // empty placeholder if (child == null) { return createVNode(Empty) } else if (typeof child === 'string' || typeof child === 'number') { return createVNode(Text, null, child + '') } else if (Array.isArray(child)) { return createVNode(Fragment, null, child) } else { return child } } function patchElement(n1, n2, container, optimized) { const el = (n2.el = n1.el) const { patchFlag, dynamicChildren } = n2 const oldProps = (n1 && n1.props) || emptyObj const newProps = n2.props || emptyObj if (patchFlag != null) { // the presence of a patchFlag means this element's render code was // generated by the compiler and can take the fast path. // in this path old node and new node are guaranteed to have the same shape // (i.e. at the exact same position in the source template) // class // this flag is matched when the element has dynamic class bindings. if (patchFlag & CLASS) { // TODO handle full class API, potentially optimize at compilation stage? if (oldProps.class !== newProps.class) { el.className = newProps.class } } // style // this flag is matched when the element has dynamic style bindings // TODO separate static and dynamic styles? if (patchFlag & STYLE) { setStyles(el.style, oldProps.style, newProps.style) } // props // This flag is matched when the element has dynamic prop/attr bindings // other than class and style. The keys of dynamic prop/attrs are saved for // faster iteration. // Note dynamic keys like :[foo]="bar" will cause this optimization to // bail out and go through a full diff because we need to unset the old key if (patchFlag & PROPS) { const propsToUpdate = n2.dynamicProps for (let i = 0; i < propsToUpdate.length; i++) { const key = propsToUpdate[i] const prev = oldProps[key] const next = newProps[key] if (prev !== next) { hostPatchProp(el, key, next, prev) } } } // text // This flag is matched when the element has only dynamic text children. // this flag is terminal (i.e. skips children diffing). if (patchFlag & TEXT) { if (n1.children !== n2.children) { hostSetElementText(el, n2.children) } return // terminal } } else if (!optimized) { // unoptimized, full diff patchProps(el, oldProps, newProps) } if (dynamicChildren != null) { // children fast path const olddynamicChildren = n1.dynamicChildren for (let i = 0; i < dynamicChildren.length; i++) { patch(olddynamicChildren[i], dynamicChildren[i], el, null, true) } } else if (!optimized) { // full diff patchChildren(n1, n2, el) } } function patchProps(el, oldProps, newProps) { if (oldProps !== newProps) { for (const key in newProps) { const next = newProps[key] const prev = oldProps[key] if (next !== prev) { hostPatchProp(el, key, next, prev) } } if (oldProps !== emptyObj) { for (const key in oldProps) { if (!(key in newProps)) { hostPatchProp(el, key, null, null) } } } } } function processFragment(n1, n2, container, anchor, optimized) { const fragmentAnchor = (n2.el = n1 ? n1.el : document.createComment('')) if (n1 == null) { insert(fragmentAnchor, container, anchor) mountChildren(n2.children, container, fragmentAnchor) } else { patchChildren(n1, n2, container, fragmentAnchor, optimized) } } function patchChildren(n1, n2, container, anchor, optimized) { const c1 = n1 && n1.children const c2 = n2.children // fast path const { patchFlag } = n2 if (patchFlag != null) { if (patchFlag & KEYED) { // this could be either fully-keyed or mixed (some keyed some not) patchKeyedChildren(c1, c2, container, anchor, optimized) return } else if (patchFlag & UNKEYED) { // unkeyed patchUnkeyedChildren(c1, c2, container, anchor, optimized) return } } if (typeof c2 === 'string') { // text children fast path if (Array.isArray(c1)) { unmountChildren(c1, false) } hostSetElementText(container, c2) } else { if (typeof c1 === 'string') { hostSetElementText('') mountChildren(c2, container, anchor) } else { // two arrays, cannot assume anything, do full diff patchKeyedChildren(c1, c2, container, anchor, optimized) } } } function patchUnkeyedChildren(c1, c2, container, anchor, optimized) { c1 = c1 || emptyArr c2 = c2 || emptyArr const oldLength = c1.length const newLength = c2.length const commonLength = Math.min(oldLength, newLength) let i for (i = 0; i < commonLength; i++) { const nextChild = (c2[i] = normalizeChild(c2[i])) patch(c1[i], nextChild, container, null, optimized) } if (oldLength > newLength) { // remove old unmountChildren(c1, commonLength, true) } else { // mount new mountChildren(c2, container, anchor, commonLength) } } // can be all-keyed or mixed function patchKeyedChildren(c1, c2, container, anchor, optimized) { // TODO patchUnkeyedChildren(c1, c2, container, anchor, optimized) } function unmount(vnode, doRemove) { if (doRemove) { if (vnode.type === Fragment) { unmountChildren(vnode.children, 0, doRemove) } remove(vnode.el) } if (Array.isArray(vnode.children)) { unmountChildren(vnode.children) } } function unmountChildren(children, start = 0, doRemove) { for (let i = start; i < children.length; i++) { unmount(children[i], doRemove) } } return function render(vnode, dom) { patch(dom._vnode, vnode, dom) return (dom._vnode = vnode) } }