UNPKG

hele

Version:
558 lines (546 loc) 18.6 kB
class Reference { constructor() { this.current = undefined; } } function isEqual(a, b) { if (a === b || a !== a && b !== b) { return true; } if ((!(a instanceof Object && b instanceof Object)) || String(a) !== String(b)) { return false; } for (const key in a) { if (!((key in b) && isEqual(a[key], b[key]))) { return false; } } for (const key in b) { if (!(key in a)) { return false; } } return true; } function _clrChd(node, deep = false) { [...node.childNodes].forEach(childNode => { if (deep) { _clrChd(childNode, true); } node.removeChild(childNode); }); } function _flatten(array) { const ans = new Array(); array.forEach(ele => { if (ele instanceof Array) { ele.forEach(e => { ans.push(e); }); } else { ans.push(ele); } }); return ans; } function _copy(original) { if (original instanceof Object) { // @ts-ignore return Object.create(original); } else { return original; } } const _isNorObj = (obj) => obj instanceof Object && String(obj) === '[object Object]'; const _eleMap = new Map(); const namespaces = new Map([ ['svg', 'http://www.w3.org/2000/svg'] ]); const _nsCtxName = '_xmlns'; function _toEle(element) { if (element instanceof HElement) { return element; } else { const type = typeof element; if (type === 'string') { return new HElement(null, { children: [element] }); } else if (type === 'number') { return new HElement(null, { children: [element.toString()] }); } else if (element instanceof Array) { return element.map(_toEle); } else { return null; } } } function _toNode(element) { if (element instanceof HElement) { return element.toNode(); } else if (element instanceof Array) { return _flatten(element.map(_toNode)); } else { return document.createTextNode(''); } } class HElement { constructor(type, props) { this.type = type; this.parent = undefined; this.node = undefined; this.context = undefined; this.props = (type && typeof type !== 'string' && type.prototype instanceof Component) ? Object.assign({}, type.defaultProps, props) : props; } toNode() { const { type, props } = this; let node; if (type === null) { node = document.createTextNode(props.children[0]); } else if (typeof type === 'string') { const context = _copy(this.context), xmlns = !props['no-xmlns'] && (props.xmlns || namespaces.get(type) || context[_nsCtxName]); node = xmlns ? document.createElementNS(xmlns, type) : document.createElement(type); if (xmlns) { context[_nsCtxName] = xmlns; } _crtNode(props, node, context); } else { const { element, component } = _getCom(type, props, _copy(this.context)), parsedElement = _toEle(element); // @ts-ignore if (type === Context) { this.context = component.state; } if (parsedElement) { const { context } = this; _flatten([parsedElement]).forEach(ele => { if (ele) { ele.parent = this; ele.context = _copy(context); } }); } if (component) { _eleMap.set(component, this); } try { node = parsedElement instanceof Array ? _flatten(parsedElement.map(_toNode)) : _toNode(parsedElement); if (component) { component.onDidMount(); } } catch (error) { node = document.createTextNode(''); if (component) { component.onCaughtError(error); } else { throw error; } } } return this.node = node; } } function _upCom(component) { const oldElement = _eleMap.get(component); if (oldElement) { const { node } = oldElement; if (node) { const { parent } = oldElement, nodes = _flatten([node]); nodes.forEach((n, i) => { const { parentNode } = n; _clrChd(n, true); if (parentNode) { if (i === 0) { const newElement = component.toElement(); if (newElement instanceof HElement) { newElement.parent = parent; newElement.context = oldElement.context; _eleMap.set(component, newElement); const newNode = newElement.toNode(), newNodes = _flatten([newNode]), fragment = document.createDocumentFragment(); if (parent) { if (parent.node instanceof Array) { nodes.forEach((n, i) => { const index = parent.node.indexOf(n); if (i === 0) { parent.node.splice(index, 1, ...newNodes); } else { parent.node.splice(index, 1); } }); } else { parent.node = newNode; } } newNodes.forEach(child => { fragment.appendChild(child); }); parentNode.replaceChild(fragment, n); } else { _eleMap.delete(component); parentNode.removeChild(n); } } else { parentNode.removeChild(n); } } }); } } } const _expired = new Set(); const defaultTickMethod = callback => { requestAnimationFrame(callback); }; const Ticker = { tickMethod: defaultTickMethod, maxUpdateTime: 12, maxClearTime: 3, _willTick: false, _tick() { if (!Ticker._willTick) { Ticker.tickMethod(() => { Ticker._willTick = false; const { maxUpdateTime, maxClearTime } = Ticker; let startTime = Date.now(); _expired.forEach(component => { if (Date.now() - startTime < maxUpdateTime) { _expired.delete(component); const { state, updateRequestCallbacks, _forceUp } = component, callbacks = updateRequestCallbacks.slice(0), callbackCount = callbacks.length; let newState = _copy(state), t; callbacks.forEach(callback => { t = callback(newState); if (t !== undefined) { newState = t; } }); component.updateRequestCallbacks = updateRequestCallbacks.slice(callbackCount); try { if (_forceUp || component.shouldUpdate(state, newState)) { component._forceUp = false; const snapshot = component.onWillUpdate(state); component.state = newState; _upCom(component); component.onDidUpdate(snapshot); } } catch (error) { component.onCaughtError(error); } } }); if (_expired.size) { Ticker._tick(); } startTime = Date.now(); let hasElementDeleted = true; while (hasElementDeleted && Date.now() - startTime < maxClearTime) { hasElementDeleted = false; _eleMap.forEach((element, component) => { const { node } = element; if (node) { const tempNode = (node instanceof Array ? node[0] : node); if (!tempNode || !tempNode.parentNode) { hasElementDeleted = true; try { component.onWillUnmount(); _eleMap.delete(component); component.onDidUnmount(); } catch (error) { component.onCaughtError(error); } } } }); } }); Ticker._willTick = true; } }, _mark(component) { _expired.add(component); Ticker._tick(); } }; class Component { constructor(props, context) { this.context = context; this.state = {}; this.refs = new Map(); this.updateRequestCallbacks = new Array(); this._forceUp = false; this.props = props; } toElement() { this.refs.forEach(ref => { ref.current = undefined; }); try { const result = this.render(), type = typeof result; if (type === 'string') { return new HElement(null, { children: [result] }); } else if (type === 'number') { return new HElement(null, { children: [result.toString()] }); } else { return result; } } catch (error) { this.onCaughtError(error); return null; } } onWillMount() { } onDidMount() { } shouldUpdate(oldState, newState) { return !isEqual(oldState, newState); } // @ts-ignore onWillUpdate(newState) { } onDidUpdate(snapShot) { } onWillUnmount() { } onDidUnmount() { } onCaughtError(error) { throw error; } createRef(name) { const { refs } = this; if (refs.has(name)) { return refs.get(name); } else { const ref = new Reference(); refs.set(name, ref); return ref; } } requestUpdate(callback) { this.updateRequestCallbacks.push(callback); Ticker._mark(this); return this; } update(newState) { return this.requestUpdate(state => { if (_isNorObj(newState) && _isNorObj(state)) { Object.assign(state, newState); } else { return newState; } }); } forceUpdate(newState) { this._forceUp = true; if (arguments.length) { // @ts-ignore this.update(newState); } else { Ticker._mark(this); } return this; } } Component.defaultProps = {}; const Fragment = props => _flatten(props.children); class Context extends Component { constructor(props, context) { super(props, context); this.state = Object.assign({}, context, props.value); } render() { return _flatten(this.props.children); } } function render(node, root, deepClear = true) { _clrChd(root, deepClear); Ticker._tick(); // @ts-ignore const { context } = root; _flatten([node]).forEach(element => { if (element instanceof HElement) { element.context = context; _flatten([element.toNode()]).forEach(child => { root.appendChild(child); }); } else { const type = typeof element; if (type === 'string') { root.appendChild(document.createTextNode(element)); } else if (type === 'number') { root.appendChild(document.createTextNode(element.toString())); } } }); } const specialNodePropProcessors = new Map([ ['children', (children, node) => { render(children, node, false); }], ['style', (style, node) => { if (!(node instanceof HTMLElement)) { return; } if (style instanceof Object) { for (const key in style) { // @ts-ignore node.style[key] = style[key]; } } else { // @ts-ignore node.style = style; } }], ['ref', (ref, node) => { ref.current = node; }], ['class', (classNames, node) => { if ('setAttribute' in node) { node.setAttribute('class', typeof classNames === 'string' ? classNames : classNames.filter(name => typeof name === 'string').join(' ')); } }], ['no-xmlns', () => { }] ]); const specialComponentPropProcessors = new Map([ ['ref', (ref, component) => { ref.current = component; }] ]); const specialFactoryPropProcessors = new Map([ ['ref', (ref) => { ref.current = undefined; }] ]); const eventPattern = /^on(\w+)$/i, captruePattern = /capture/i, nonpassivePattern = /nonpassive/i, oncePattern = /once/i; function _getEName(rawEvent, useCapture, nonpassive, once) { let t = 0; if (useCapture) { t += 7; } if (nonpassive) { t += 7; } if (once) { t += 4; } return t > 0 ? rawEvent.slice(0, -t) : rawEvent; } function _getEOpt(capture, nonpassive, once) { return (!nonpassive && !capture && !once) ? false : { capture, passive: !nonpassive, once }; } function _crtNode(props, node, context) { // @ts-ignore node.context = context; for (const key in props) { const value = props[key], processor = specialNodePropProcessors.get(key); if (processor) { processor(value, node); } else if (key.match(eventPattern)) { const rawEvent = RegExp.$1, capture = captruePattern.test(rawEvent), nonpassive = nonpassivePattern.test(rawEvent), once = oncePattern.test(rawEvent), event = _getEName(rawEvent, capture, nonpassive, once); node.addEventListener(event, value, _getEOpt(capture, nonpassive, once)); } else if (!(key in node) && ('setAttribute' in node)) { node.setAttribute(key, value); } else { try { // @ts-ignore node[key] = value; } catch (err) { node.setAttribute(key, value); } } } } function _getCom(componentGetter, props, context) { const result = { element: null, component: null }; if (componentGetter.prototype instanceof Component) { const component = result.component = new componentGetter(props, context); for (const key in props) { const processor = specialComponentPropProcessors.get(key); if (processor) { processor(props[key], component); } } try { component.onWillMount(); } catch (error) { component.onCaughtError(error); } result.element = component.toElement(); } else { for (const key in props) { const processor = specialFactoryPropProcessors.get(key); if (processor) { processor(props[key], undefined); } } const element = componentGetter(props, context); result.element = element; } return result; } function createElement(type, props, ...children) { return new HElement(type, { ...(props || {}), children: _flatten(children) }); } class Portal extends Component { constructor(props, context) { super(props, context); if (props.container) { this.container = props.container; } else { this.container = document.createElement('div'); document.body.appendChild(this.container); } } render() { return null; } onWillMount() { render(createElement(Context, { value: this.context }, this.props.children), this.container, this.props.deepClear); } } function createFactory(type) { return function ComponentFactory(props, ...children) { return createElement(type, props, ...children); }; } export { specialNodePropProcessors, specialComponentPropProcessors, specialFactoryPropProcessors, _getEName, _getEOpt, _crtNode, _getCom, isEqual, _clrChd, _flatten, _copy, _isNorObj, _upCom, _expired, defaultTickMethod, Ticker, Component, Fragment, Context, _eleMap, namespaces, _nsCtxName, _toEle, _toNode, HElement, Reference, render, Portal, createElement, createFactory };