mirror of
https://github.com/Tencent/Hippy.git
synced 2026-10-28 13:38:10 +08:00
feat(react): change updateNode to batch operation & perf event binding
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user