From 1c59723cd1fb5c503ce7617d36f820290117f1ce Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Sun, 8 Aug 2021 12:22:48 +0300 Subject: [PATCH] Faster and more correct variant which does not recreate full DOM tree on every run On the other hand, it has to disable Object.freeze in development builds because React freezes props in development builds and we want to modify them directly --- SSRComponent.js | 123 +++++++++++++++++++++++++++++++---------------- virtualRender.js | 54 +++++++++++++++++++-- 2 files changed, 131 insertions(+), 46 deletions(-) diff --git a/SSRComponent.js b/SSRComponent.js index 0b047c1..dd33563 100644 --- a/SSRComponent.js +++ b/SSRComponent.js @@ -1,10 +1,8 @@ // Component capable of saving & restoring state during render // (c) Vitaliy Filippov 2019+ -// Version: 2021-08-07 +// Version: 2021-08-08 // License: Dual-license MPL 1.1+ or GNU LGPL 3.0+ -// NOTE: Child components of the same class should have unique `key`s - import React from 'react'; export default class SSRComponent extends React.PureComponent @@ -39,13 +37,9 @@ export default class SSRComponent extends React.PureComponent if (store.children) { store.children = { ...store.children }; - for (const className in store.children) + for (const key in store.children) { - store.children[className] = { ...store.children[className] }; - for (const key in store.children[className]) - { - store.children[className][key] = SSRComponent.serializeStore(store.children[className][key]); - } + store.children[key] = SSRComponent.serializeStore(store.children[key]); } } return store; @@ -63,36 +57,15 @@ export default class SSRComponent extends React.PureComponent passStore(children) { - return React.Children.map(children, (child) => + walkTree(children, (child, key) => { if (child && (child.type instanceof Object) && (child.type.prototype instanceof SSRComponent)) { - const className = child.type.name; - this.props.store.idx[className] ||= { num: 0, keys: {} }; - let key; - if (child.props.key) - key = ':'+child.props.key; - else - { - this.props.store.idx[className].num++; - key = '['+this.props.store.idx[className].num; - } - this.props.store.idx[className].keys[key] = true; + this.props.store.idx[key] = true; let chstore = (this.props.store.children ||= {}); - chstore = (chstore[className] ||= {}); chstore = (chstore[key] ||= {}); - return React.cloneElement(child, { - store: chstore, - children: child.props && child.props.children ? this.passStore(child.props.children) : undefined, - }); + child.props.store = chstore; } - else if (child && child.props && child.props.children) - { - return React.cloneElement(child, { - children: this.passStore(child.props.children), - }); - } - return child; }); } @@ -103,19 +76,28 @@ export default class SSRComponent extends React.PureComponent this.props.store.instance = this; this.props.store.idx = {}; } - let children = this.doRender(); + let children; + if (process.env.NODE_ENV === 'production') + { + children = this.doRender(); + } + else + { + // Monkey-patch Object.freeze so fucking React can't freeze props + const freeze = Object.freeze; + Object.freeze = function() {}; + children = this.doRender(); + Object.freeze = freeze; + } if (this.props.store) { - children = this.passStore(children); + this.passStore(children); // Clear unused keys - for (let className in this.props.store.idx) + for (let key in this.props.store.children) { - for (let key in this.props.store.children[className]) + if (!this.props.store.idx[key]) { - if (!this.props.store.idx[className].keys[key]) - { - delete this.props.store.children[className][key]; - } + delete this.props.store.children[key]; } } delete this.props.store.idx; @@ -127,3 +109,62 @@ export default class SSRComponent extends React.PureComponent { } } + +// Preact puts children directly on element, and React via props +const getChildren = element => + element.props && element.props.children ? element.props.children : element.children ? element.children : undefined; + +// Preact uses "nodeName", React uses "type" +const getType = element => element.type || element.nodeName; + +const isReactElement = element => !!getType(element); + +// Recurse a React Element tree, running the provided visitor against each element. +// Similar to React.Children.map(), but tracks keys. +function walkTree(tree, visitor, path = '') +{ + if (!tree) + { + return; + } + if (tree instanceof Array) + { + // Process array, remembering keys and indices within series of elements of the same type + let index = 0, series = 0; + let lastType = null, lastHasKey = false; + for (let item of tree) + { + if (item) + { + let type = getType(item); + let key = item.key; + if (lastType == type && lastHasKey == (key != null)) + index++; + else + { + series++; + index = 0; + lastType = type; + lastHasKey = (key != null); + } + if (key == null) + key = ':'+series+':'+index; + else + key = '['+key+']'; + let typeName; + if (type && type.name) + typeName = type.name; + else if (typeof type == 'symbol') + typeName = type.toString(); + else + typeName = type; + walkTree(item, visitor, path+'/'+typeName+key); + } + } + } + if (isReactElement(tree)) + { + visitor(tree, path); + walkTree(getChildren(tree), visitor, path); + } +} diff --git a/virtualRender.js b/virtualRender.js index 30677bb..2e2eb1b 100644 --- a/virtualRender.js +++ b/virtualRender.js @@ -84,17 +84,25 @@ function walkTree(tree, visitor, context, options, path = '') { // Process array, remembering keys and indices within series of elements of the same type let res = []; - let index = 0; - let lastType = null; + let index = 0, series = 0; + let lastType = null, lastHasKey = false; for (let item of tree) { let type = item && getType(item); - if (lastType == type) + let key = item && getProps(item)?.key; + if (lastType == type && lastHasKey == (key != null)) index++; else + { + series++; index = 0; - lastType = type; - let key = item && getProps(item)?.key || index; + lastType = type; + lastHasKey = (key != null); + } + if (key == null) + key = ':'+series+':'+index; + else + key = '['+key+']'; let typeName; if (type && type.name) typeName = type.name; @@ -106,6 +114,42 @@ function walkTree(tree, visitor, context, options, path = '') } return res; } + if (tree instanceof Array) + { + // Process array, remembering keys and indices within series of elements of the same type + let index = 0, series = 0; + let lastType = null, lastHasKey = false; + for (let item of tree) + { + if (item) + { + let type = getType(item); + let key = item.key; + if (lastType == type && lastHasKey == (key != null)) + index++; + else + { + series++; + index = 0; + lastType = type; + lastHasKey = (key != null); + } + if (key == null) + key = ':'+series+':'+index; + else + key = '['+key+']'; + let typeName; + if (type && type.name) + typeName = type.name; + else if (typeof type == 'symbol') + typeName = type.toString(); + else + typeName = type; + res.push(walkTree(item, visitor, context, options, path+'/'+typeName+'['+key+']')); + } + } + return res; + } if (tree.type) { const _context = tree.type._context || (tree.type.Provider && tree.type.Provider._context);