feat(react): change updateNode to batch operation & perf event binding

This commit is contained in:
zoomchan-cxj
2022-05-27 23:43:30 +08:00
parent aed4a15d85
commit fdae46aed8
7 changed files with 163 additions and 168 deletions
@@ -425,7 +425,9 @@ class ListView extends React.Component<ListViewProps, ListViewState> {
}
const appearEventList = [onAppear, onDisappear, onWillAppear, onWillDisappear];
nativeProps.exposureEventEnabled = appearEventList.some(func => typeof func === 'function');
nativeProps.numberOfRows = itemList.length;
if (Device.platform.OS === 'ios') {
nativeProps.numberOfRows = itemList.length;
}
(nativeProps as ListViewProps).initialListSize = initialListSize;
(nativeProps as ListViewProps).style = {
overflow: 'scroll',
+130 -115
View File
@@ -24,7 +24,6 @@ import Animation from '../modules/animation';
import AnimationSet from '../modules/animation-set';
import { colorParse, colorArrayParse, Color } from '../color';
import { updateChild, updateWithChildren, endBatch } from '../renderer/render';
import { Device } from '../native';
import {
unicodeToChar,
tryConvertNumber,
@@ -52,6 +51,10 @@ interface DirectionMap {
[propName: string]: string;
}
interface AttributeUpdateOption {
isBatchUpdate?: boolean
}
interface DegreeUnit {
[propName: string]: string;
}
@@ -215,7 +218,7 @@ function parseTextShadowOffset(styleKey: string, styleValue: number, style: any)
* get final key sent to native
* @param key
*/
function getEventPropKey(key: string) {
function getEventName(key: string) {
if (isCaptureEvent(key)) {
key = key.replace('Capture', '');
}
@@ -230,6 +233,7 @@ class ElementNode extends ViewNode {
public id = '';
public style: HippyTypes.Style = {};
public attributes: Attributes = {};
public events: object = {};
public constructor(tagName: string) {
super();
@@ -292,7 +296,6 @@ class ElementNode extends ViewNode {
};
});
} else if (typeof style === 'object' && style) {
// TODO: Merge transform
mergedStyles = {
...mergedStyles,
...style,
@@ -367,8 +370,126 @@ class ElementNode extends ViewNode {
});
}
/* istanbul ignore next */
public setAttribute(key: string, value: any) {
// set node attributes in a batch
public setAttributes(attributeQueue: [][] = []) {
if (Array.isArray(attributeQueue) && attributeQueue.length > 0) {
attributeQueue.forEach((attributeList) => {
if (Array.isArray(attributeList)) {
const [key, value]: (string | any)[] = attributeList;
this.setAttribute(key, value, { isBatchUpdate: true });
}
});
updateChild(this);
}
}
parseAnimationStyleProp(style) {
// Set useAnimation if animation exist in style
let useAnimation = false;
Object.keys(style).some((declare) => {
const styleVal = (style as any)[declare];
if (styleVal && Array.isArray(styleVal) && declare === 'transform') {
for (let i = 0; i < styleVal.length; i += 1) {
const transform = styleVal[i];
/* eslint-disable-next-line no-restricted-syntax, guard-for-in */
for (const transformKey in (transform as any)) {
const transformValue = (transform as any)[transformKey];
if (typeof transformValue === 'object'
&& transformValue !== null
&& Number.isInteger(transformValue.animationId)) {
useAnimation = true;
return true;
}
}
}
}
if (typeof styleVal === 'object'
&& styleVal !== null
&& Number.isInteger((styleVal as Animation).animationId)) {
useAnimation = true;
return true;
}
return false;
});
if (useAnimation) {
this.attributes.useAnimation = true;
} else if (typeof this.attributes.useAnimation === 'boolean') {
this.attributes.useAnimation = undefined;
}
}
parseAttributeProp(key: string, value: any): boolean {
const caseList = [
{
match: () => ['id'].indexOf(key) >= 0,
action: () => {
if (value === this.id) {
return true;
}
this.id = value;
// update current node and child nodes
updateWithChildren(this);
return true;
},
},
{
match: () => ['value', 'defaultValue', 'placeholder'].indexOf(key) >= 0,
action: () => {
this.attributes[key] = unicodeToChar(value);
return false;
},
},
{
match: () => ['text'].indexOf(key) >= 0,
action: () => {
this.attributes[key] = value;
return false;
},
},
{
match: () => ['style'].indexOf(key) >= 0,
action: () => {
if (typeof value !== 'object' || value === undefined || value === null) {
return true;
}
this.setStyleAttribute(value);
return false;
},
},
{
match: () => true,
action: () => {
if (typeof value === 'function') {
const eventName = getEventName(key);
this.attributes[eventName] = true;
this.events[`${eventName}`] = true;
} else {
this.attributes[key] = value;
const eventName = getEventName(key);
if (this.events[`${eventName}`] === true
&& typeof value !== 'function') {
this.attributes[eventName] = false;
delete this.events[`${eventName}`];
}
}
return false;
},
},
];
let isNeedReturn = false;
caseList.some((conditionObj: { match: Function, action: Function }) => {
if (conditionObj.match()) {
isNeedReturn = conditionObj.action();
return true;
}
return false;
});
return isNeedReturn;
}
// set node attribute
public setAttribute(key: string, value: any, options: AttributeUpdateOption = {}) {
try {
// detect expandable attrs for boolean values
// See https://vuejs.org/v2/guide/components-props.html#Passing-a-Boolean
@@ -376,119 +497,13 @@ class ElementNode extends ViewNode {
value = true;
}
if (key === undefined) {
updateChild(this);
!options.isBatchUpdate && updateChild(this);
return;
}
const caseList = [
{
match: () => ['id'].indexOf(key) >= 0,
action: () => {
if (value === this.id) {
return true;
}
this.id = value;
// update current node and child nodes
updateWithChildren(this);
return true;
},
},
{
match: () => ['value', 'defaultValue', 'placeholder'].indexOf(key) >= 0,
action: () => {
this.attributes[key] = unicodeToChar(value);
return false;
},
},
{
match: () => ['text'].indexOf(key) >= 0,
action: () => {
this.attributes[key] = value;
return false;
},
},
{
match: () => ['numberOfRows'].indexOf(key) >= 0,
action: () => {
this.attributes[key] = value;
return Device.platform.OS !== 'ios';
},
},
{
match: () => ['style'].indexOf(key) >= 0,
action: () => {
if (typeof value !== 'object' || value === undefined || value === null) {
return true;
}
this.setStyleAttribute(value);
return false;
},
},
{
match: () => true,
action: () => {
if (typeof value === 'function') {
const processedKey = getEventPropKey(key);
this.attributes[processedKey] = true;
this.attributes[`__bind__${processedKey}`] = true;
} else {
this.attributes[key] = value;
const processedKey = getEventPropKey(key);
if (this.attributes[`__bind__${processedKey}`] === true
&& typeof value !== 'function') {
this.attributes[processedKey] = false;
this.attributes[`__bind__${processedKey}`] = false;
}
}
return false;
},
},
];
let isNeedReturn = false;
caseList.some((conditionObj: { match: Function, action: Function }) => {
if (conditionObj.match()) {
isNeedReturn = conditionObj.action();
return true;
}
return false;
});
const isNeedReturn = this.parseAttributeProp(key, value);
if (isNeedReturn) return;
// Set useAnimation if animation exist in style
let useAnimation = false;
Object.keys(this.style).some((declare) => {
const style = (this.style as any)[declare];
if (style && Array.isArray(style) && declare === 'transform') {
for (let i = 0; i < style.length; i += 1) {
const transform = style[i];
/* eslint-disable-next-line no-restricted-syntax, guard-for-in */
for (const transformKey in (transform as any)) {
const transformValue = (transform as any)[transformKey];
if (typeof transformValue === 'object'
&& transformValue !== null
&& Number.isInteger(transformValue.animationId)) {
useAnimation = true;
return transformValue;
}
}
}
}
if (typeof style === 'object'
&& style !== null
&& Number.isInteger((style as Animation).animationId)) {
useAnimation = true;
return style;
}
return false;
});
if (useAnimation) {
this.attributes.useAnimation = true;
} else if (typeof this.attributes.useAnimation === 'boolean') {
this.attributes.useAnimation = undefined;
}
updateChild(this);
this.parseAnimationStyleProp(this.style);
!options.isBatchUpdate && updateChild(this);
} catch (e) {
// noop
}
@@ -58,9 +58,10 @@ function commitUpdate(
workInProgress: any,
): void {
preCacheFiberNode(workInProgress, instance.nodeId);
const updatePayloadPropList: string[] = Object.keys(updatePayload);
if (updatePayloadPropList.length === 0) return;
updatePayloadPropList.forEach(propKey => instance.setAttribute(propKey, updatePayload[propKey]));
const updatePayloadList = Object.keys(updatePayload || {});
if (updatePayloadList.length === 0) return;
const attributeQueue = updatePayloadList.map(propKey => [propKey, updatePayload[propKey]]);
instance.setAttributes(attributeQueue);
}
// this is the hook when commitMutationEffects begin
@@ -79,49 +80,44 @@ function prepareUpdate(
oldProps: Props,
newProps: Props,
): UpdatePayload {
const updatePayload: {
[key: string]: any;
} = {};
let hasFunctionProp = false;
Object.keys(newProps).forEach((key: string) => {
const updatePayload: { [key: string]: any; } = {};
Object.keys(oldProps).forEach((key) => {
const oldPropValue = oldProps[key];
const newPropValue = newProps[key];
if (
(oldPropValue !== undefined && oldPropValue !== null)
&& (newPropValue === undefined || newPropValue === null)
) {
// if oldPros existed and newPros removed(undefined/null), indicated this prop deleted.
updatePayload[key] = newPropValue;
}
});
Object.keys(newProps).forEach((key) => {
const oldPropValue = oldProps[key];
const newPropValue = newProps[key];
switch (key) {
case 'children': {
if (oldPropValue !== newPropValue
&& (typeof newPropValue === 'number'
|| typeof newPropValue === 'string'
)) {
&& (typeof newPropValue === 'number'
|| typeof newPropValue === 'string'
)) {
updatePayload[key] = newPropValue;
}
break;
}
default: {
if (typeof oldPropValue === 'function'
&& typeof newPropValue === 'function'
&& !isEqual(oldPropValue, newPropValue)
) {
hasFunctionProp = true;
} else if (!isEqual(oldPropValue, newPropValue)) {
if (
(newPropValue !== undefined && newPropValue !== null)
&& (oldPropValue === undefined || oldPropValue === null)) {
updatePayload[key] = newPropValue;
} else if (typeof newPropValue !== 'function' && !isEqual(oldPropValue, newPropValue)) {
// if newProps existed and oldProps removed(undefined/null)
// or newProps only updated if not function property
updatePayload[key] = newPropValue;
}
}
}
});
const isUpdatePayloadEmpty = Object.keys(updatePayload).length === 0;
if (isUpdatePayloadEmpty && hasFunctionProp) {
/**
* if updatePayload only has function property,
* return empty object to trigger commitUpdate for updating fiberNode Cache
*/
return {};
}
if (isUpdatePayloadEmpty) {
/**
* prepareUpdate returning null would not trigger commitUpdate
*/
return null;
}
return updatePayload;
}
@@ -153,12 +153,6 @@ function getTargetNodeAttributes(targetNode: Element) {
id: targetNode.id,
...targetNodeAttributes,
};
// delete special __bind__event attribute, which is used in C DOM
Object.keys(attributes).forEach((key) => {
if (key.indexOf('__bind__') === 0 && typeof attributes[key] === 'boolean') {
delete attributes[key];
}
});
delete attributes.text;
delete attributes.value;
return attributes;
@@ -269,7 +263,6 @@ function insertChild(parentNode: ViewNode, childNode: ViewNode, atIndex = -1) {
type: NODE_OPERATION_TYPES.createNode,
nodes: translated,
});
// endBatch();
}
}
@@ -289,7 +282,6 @@ function removeChild(parentNode: ViewNode, childNode: ViewNode | null, index: nu
type: NODE_OPERATION_TYPES.deleteNode,
nodes: deleteNodeIds,
});
// endBatch();
}
function updateChild(parentNode: Element) {
@@ -304,7 +296,6 @@ function updateChild(parentNode: Element) {
nodes: [translated],
});
}
// endBatch();
}
function updateWithChildren(parentNode: ViewNode) {
@@ -317,7 +308,6 @@ function updateWithChildren(parentNode: ViewNode) {
type: NODE_OPERATION_TYPES.updateNode,
nodes: translated,
});
// endBatch();
}
export {
@@ -319,6 +319,9 @@ class ElementNode extends ViewNode {
default:
this.attributes[key] = value;
}
if (typeof this.filterAttribute === 'function') {
this.filterAttribute(this.attributes);
}
updateChild(this);
} catch (err) {
// Throw error in development mode
@@ -245,7 +245,6 @@ test('renderToNative test with children --debug mode', (t) => {
},
multiline: true,
onTyping: true,
__bind__onTyping: true,
keyboardType: 'url',
numberOfLines: 10,
underlineColorAndroid: 0,
@@ -362,7 +361,6 @@ test('renderToNative test with children --production mode', (t) => {
props: {
multiline: true,
onTyping: true,
__bind__onTyping: true,
keyboardType: 'url',
numberOfLines: 10,
underlineColorAndroid: 0,
@@ -305,12 +305,6 @@ function getTargetNodeAttributes(targetNode) {
class: classInfo,
...targetNodeAttributes,
};
// delete special __bind__event attribute, which is used in C DOM
Object.keys(attributes).forEach((key) => {
if (key.indexOf('__bind__') === 0 && typeof attributes[key] === 'boolean') {
delete attributes[key];
}
});
delete attributes.text;
delete attributes.value;
return attributes;
@@ -355,18 +349,15 @@ function renderToNative(rootViewId, targetNode) {
const nativeEventName = eventNamesMap[vueEventName];
if (nativeEventName) {
events[nativeEventName] = true;
events[`__bind__${nativeEventName}`] = true;
} else {
const name = `on${capitalizeFirstLetter(vueEventName)}`;
events[name] = true;
events[`__bind__${name}`] = true;
}
});
} else {
vueEventNames.forEach((vueEventName) => {
const name = `on${capitalizeFirstLetter(vueEventName)}`;
events[name] = true;
events[`__bind__${name}`] = true;
});
}
}