@es-react/es-react
Version:
Hippy react framework
471 lines (442 loc) • 13.6 kB
text/typescript
/*
* Tencent is pleased to support the open source community by making
* Hippy available.
*
* Copyright (C) 2017-2019 THL A29 Limited, a Tencent company.
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable no-param-reassign */
import ViewNode from '../dom/view-node';
import Element from '../dom/element-node';
import {
getRootViewId,
getRootContainer,
translateToNativeEventName,
eventHandlerType,
nativeEventMap,
} from '../utils/node';
import { deepCopy, isDev, isTraceEnabled, trace, warn } from '../utils';
import {HippyTypes} from '../types'
const componentName = ['%c[native]%c', 'color: red', 'color: auto'];
interface BatchType {
[key: string]: symbol;
}
const NODE_OPERATION_TYPES: BatchType = {
createNode: Symbol('createNode'),
updateNode: Symbol('updateNode'),
deleteNode: Symbol('deleteNode'),
moveNode: Symbol('moveNode'),
};
interface BatchChunk {
type: symbol,
nodes: HippyTypes.TranslatedNodes[],
eventNodes: HippyTypes.EventNode[],
printedNodes: HippyTypes.PrintedNode[],
}
let batchIdle = true;
let batchNodes: BatchChunk[] = [];
/**
* Convert an ordered node array into multiple fragments
*/
function chunkNodes(batchNodes: BatchChunk[]) {
const result: BatchChunk[] = [];
for (let i = 0; i < batchNodes.length; i += 1) {
const chunk: BatchChunk = batchNodes[i];
const { type, nodes, eventNodes, printedNodes } = chunk;
const lastChunk = result[result.length - 1];
if (!lastChunk || lastChunk.type !== type) {
result.push({
type,
nodes,
eventNodes,
printedNodes,
});
} else {
lastChunk.nodes = lastChunk.nodes.concat(nodes);
lastChunk.eventNodes = lastChunk.eventNodes.concat(eventNodes);
lastChunk.printedNodes = lastChunk.printedNodes.concat(printedNodes);
}
}
return result;
}
function isNativeGesture(name) {
return !!nativeEventMap[name];
}
function handleEventListeners(eventNodes: HippyTypes.EventNode[] = [], sceneBuilder: any) {
eventNodes.forEach((eventNode) => {
if (eventNode) {
const { id, eventList } = eventNode;
eventList.forEach((eventAttribute) => {
const { name, type, listener, isCapture } = eventAttribute;
let nativeEventName;
if (isNativeGesture(name)) {
nativeEventName = nativeEventMap[name];
} else {
nativeEventName = translateToNativeEventName(name);
}
if (type === eventHandlerType.REMOVE) {
sceneBuilder.removeEventListener(id, nativeEventName, listener);
}
if (type === eventHandlerType.ADD) {
sceneBuilder.addEventListener(id, nativeEventName, listener, isCapture);
}
});
}
});
}
/**
* print nodes operation log
* @param {HippyTypes.PrintedNode[]} printedNodes
* @param {string} nodeType
*/
function printNodesOperation(printedNodes: HippyTypes.PrintedNode[], nodeType: string): void {
if (isTraceEnabled()) {
trace(...componentName, nodeType, printedNodes);
}
}
/**
* batch Updates from js to native
* @param {number} rootViewId
*/
function batchUpdate(rootViewId: number): void {
const chunks = chunkNodes(batchNodes);
const sceneBuilder = new global.Hippy.SceneBuilder(rootViewId);
chunks.forEach((chunk) => {
switch (chunk.type) {
case NODE_OPERATION_TYPES.createNode:
printNodesOperation(chunk.printedNodes, 'createNode');
sceneBuilder.create(chunk.nodes);
handleEventListeners(chunk.eventNodes, sceneBuilder);
break;
case NODE_OPERATION_TYPES.updateNode:
printNodesOperation(chunk.printedNodes, 'updateNode');
sceneBuilder.update(chunk.nodes);
handleEventListeners(chunk.eventNodes, sceneBuilder);
break;
case NODE_OPERATION_TYPES.deleteNode:
printNodesOperation(chunk.printedNodes, 'deleteNode');
sceneBuilder.delete(chunk.nodes);
break;
case NODE_OPERATION_TYPES.moveNode:
printNodesOperation(chunk.printedNodes, 'moveNode');
sceneBuilder.move(chunk.nodes);
break;
default:
}
});
sceneBuilder.build();
}
/**
* endBatch - end batch update
* @param {boolean} isHookUsed - whether used commitEffects hook
*/
function endBatch(isHookUsed = false): void {
if (!batchIdle) return;
batchIdle = false;
if (batchNodes.length === 0) {
batchIdle = true;
return;
}
const rootViewId = getRootViewId();
// if commitEffectsHook used, call batchUpdate synchronously
if (isHookUsed) {
batchUpdate(rootViewId);
batchNodes = [];
batchIdle = true;
} else {
Promise.resolve().then(() => {
batchUpdate(rootViewId);
batchNodes = [];
batchIdle = true;
});
}
}
/**
* Translate to native props from attributes and meta
*/
function getNativeProps(node: Element) {
const { children, ...otherProps } = node.attributes;
return otherProps;
}
/**
* Get target node attributes, used to chrome devTool tag attribute show while debugging
*/
function getTargetNodeAttributes(targetNode: Element) {
try {
const targetNodeAttributes = deepCopy(targetNode.attributes);
const { id, nodeId } = targetNode;
const attributes = {
id,
hippyNodeId: `${nodeId}`,
...targetNodeAttributes,
};
delete attributes.text;
delete attributes.value;
return attributes;
} catch (e) {
warn('getTargetNodeAttributes error:', e);
return {};
}
}
/**
* getEventNode - translate event attributes to event node.
* @param targetNode
*/
function getEventNode(targetNode): HippyTypes.EventNode {
let eventNode: HippyTypes.EventNode = undefined;
const eventsAttributes = targetNode.events;
if (eventsAttributes) {
const eventList: HippyTypes.EventAttribute[] = [];
Object.keys(eventsAttributes)
.forEach((key) => {
const { name, type, isCapture, listener } = eventsAttributes[key];
if (!targetNode.isListenerHandled(key, type)) {
targetNode.setListenerHandledType(key, type);
eventList.push({
name,
type,
isCapture,
listener,
});
}
});
eventNode = {
id: targetNode.nodeId,
eventList,
};
}
return eventNode;
}
type renderToNativeReturnedVal = [
translatedNode?: HippyTypes.TranslatedNodes,
eventNode?: HippyTypes.EventNode,
printedNode?: HippyTypes.PrintedNode,
];
/**
* Render Element to native
*/
function renderToNative(
rootViewId: number,
targetNode: Element,
refInfo: HippyTypes.ReferenceInfo = {},
): renderToNativeReturnedVal {
if (!targetNode.nativeName) {
warn('Component need to define the native name', targetNode);
return [];
}
if (targetNode.meta.skipAddToDom) {
return [];
}
if (!targetNode.meta.component) {
throw new Error(`Specific tag is not supported yet: ${targetNode.tagName}`);
}
// Translate to native node
const nativeNode: HippyTypes.NativeNode = {
id: targetNode.nodeId,
pId: (targetNode.parentNode?.nodeId) || rootViewId,
name: targetNode.nativeName,
props: {
...getNativeProps(targetNode),
style: targetNode.style,
},
tagName: targetNode.tagName,
};
const eventNode = getEventNode(targetNode);
let printedNode: HippyTypes.PrintedNode = undefined;
if (isDev()) {
// generate printedNode for debugging
const listenerProp = {};
if (eventNode && Array.isArray(eventNode.eventList)) {
eventNode.eventList.forEach((eventListItem) => {
const { name, listener, type } = eventListItem;
type === eventHandlerType.ADD && Object.assign(listenerProp, { [name]: listener });
});
}
// Add nativeNode attributes info for debugging
nativeNode.props!.attributes = getTargetNodeAttributes(targetNode);
Object.assign(printedNode = {}, nativeNode, refInfo);
printedNode.listeners = listenerProp;
}
// convert to translatedNode
const translatedNode: HippyTypes.TranslatedNodes = [nativeNode, refInfo];
return [translatedNode, eventNode, printedNode];
}
type renderToNativeWithChildrenReturnedVal = [
nativeLanguages: HippyTypes.TranslatedNodes[],
eventLanguages: HippyTypes.EventNode[],
printedLanguages: HippyTypes.PrintedNode[]
];
/**
* Render Element with children to native
* @param {number} rootViewId - rootView id
* @param {ViewNode} node - current node
* @param {Function} [callback] - function called on each traversing process
* @param {HippyTypes.ReferenceInfo} [refInfo] - reference information
* @returns [nativeLanguages: HippyTypes.NativeNode[], eventLanguages: HippyTypes.EventNode[]]
*/
function renderToNativeWithChildren(
rootViewId: number,
node: ViewNode,
callback?: Function,
refInfo: HippyTypes.ReferenceInfo = {},
): renderToNativeWithChildrenReturnedVal {
const nativeLanguages: HippyTypes.TranslatedNodes[] = [];
const eventLanguages: HippyTypes.EventNode[] = [];
const printedLanguages: HippyTypes.PrintedNode[] = [];
node.traverseChildren((targetNode: Element, refInfo: HippyTypes.ReferenceInfo) => {
const [nativeNode, eventNode, printedNode] = renderToNative(rootViewId, targetNode, refInfo);
if (nativeNode) {
nativeLanguages.push(nativeNode);
}
if (eventNode) {
eventLanguages.push(eventNode);
}
if (printedNode) {
printedLanguages.push(printedNode);
}
if (typeof callback === 'function') {
callback(targetNode);
}
}, refInfo);
return [nativeLanguages, eventLanguages, printedLanguages];
}
function isLayout(node: ViewNode) {
const container = getRootContainer();
if (!container) {
return false;
}
// Determine node is a Document instance
return node instanceof container.containerInfo.constructor;
}
function insertChild(parentNode: ViewNode, childNode: ViewNode, refInfo: HippyTypes.ReferenceInfo = {}) {
if (!parentNode || !childNode) {
return;
}
if (childNode.meta.skipAddToDom) {
return;
}
const rootViewId = getRootViewId();
const renderRootNodeCondition = isLayout(parentNode) && !parentNode.isMounted;
const renderOtherNodeCondition = parentNode.isMounted && !childNode.isMounted;
// Render the root node or other nodes
if (renderRootNodeCondition || renderOtherNodeCondition) {
const [nativeLanguages, eventLanguages, printedLanguages] = renderToNativeWithChildren(
rootViewId,
childNode,
(node: ViewNode) => {
if (!node.isMounted) {
node.isMounted = true;
}
},
refInfo,
);
batchNodes.push({
type: NODE_OPERATION_TYPES.createNode,
nodes: nativeLanguages,
eventNodes: eventLanguages,
printedNodes: printedLanguages,
});
}
}
function removeChild(parentNode: ViewNode, childNode: ViewNode | null) {
if (!childNode || childNode.meta.skipAddToDom) {
return;
}
childNode.isMounted = false;
const rootViewId = getRootViewId();
const nativeNode = {
id: childNode.nodeId,
pId: childNode.parentNode ? childNode.parentNode.nodeId : rootViewId,
};
const deleteNodeIds: HippyTypes.TranslatedNodes[] = [
[
nativeNode,
{},
],
];
const printedNodes = isDev() ? [nativeNode] : [];
batchNodes.push({
printedNodes,
type: NODE_OPERATION_TYPES.deleteNode,
nodes: deleteNodeIds,
eventNodes: [],
});
}
function moveChild(parentNode: ViewNode, childNode: ViewNode, refInfo: HippyTypes.ReferenceInfo = {}) {
if (!parentNode || !childNode) {
return;
}
if (childNode.meta.skipAddToDom) {
return;
}
const rootViewId = getRootViewId();
const nativeNode = {
id: childNode.nodeId,
pId: childNode.parentNode ? childNode.parentNode.nodeId : rootViewId,
};
const moveNodeIds: HippyTypes.TranslatedNodes[] = [
[
nativeNode,
refInfo,
],
];
const printedNodes = isDev() ? [{ ...nativeNode, ...refInfo }] : [];
batchNodes.push({
printedNodes,
type: NODE_OPERATION_TYPES.moveNode,
nodes: moveNodeIds,
eventNodes: [],
});
}
function updateChild(parentNode: Element) {
if (!parentNode.isMounted) {
return;
}
const rootViewId = getRootViewId();
const [nativeNode, eventNode, printedNode] = renderToNative(rootViewId, parentNode);
if (nativeNode) {
batchNodes.push({
type: NODE_OPERATION_TYPES.updateNode,
nodes: [nativeNode],
eventNodes: [eventNode],
printedNodes: isDev() ? [printedNode] : [],
});
}
}
function updateWithChildren(parentNode: ViewNode) {
if (!parentNode.isMounted) {
return;
}
const rootViewId = getRootViewId();
const [nativeLanguages, eventLanguages, printedLanguages] = renderToNativeWithChildren(rootViewId, parentNode) || {};
if (nativeLanguages) {
batchNodes.push({
type: NODE_OPERATION_TYPES.updateNode,
nodes: nativeLanguages,
eventNodes: eventLanguages,
printedNodes: printedLanguages,
});
}
}
export {
endBatch,
renderToNative,
renderToNativeWithChildren,
insertChild,
removeChild,
updateChild,
moveChild,
updateWithChildren,
};