Skip to content

Commit

Permalink
Begin rendering with diff instead of diffChildren and other golf ⛳ (#…
Browse files Browse the repository at this point in the history
…1715)

Beginning the render with diff means that the initial Fragment will be rendered as a real Component making our parent._children pointer more useful (will have meaningful _dom pointers, will have forceUpdate function after initial render).
  • Loading branch information
andrewiggins committed Jun 18, 2019
2 parents f848a03 + e4bde0c commit f39f877
Show file tree
Hide file tree
Showing 7 changed files with 72 additions and 49 deletions.
6 changes: 5 additions & 1 deletion debug/src/debug.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function initDebug() {
const warnedComponents = { useEffect: {}, useLayoutEffect: {}, lazyPropTypes: {} };

options._catchError = (error, vnode) => {
let component = vnode._component;
let component = vnode && vnode._component;
if (component && typeof error.then === 'function') {
error = new Error('Missing Suspense. The throwing component was: ' + (component.displayName || component.name));
}
Expand All @@ -35,6 +35,8 @@ export function initDebug() {
};

options._diff = vnode => {
if (vnode == null) { return; }

let { type } = vnode;

if (type===undefined) {
Expand Down Expand Up @@ -136,6 +138,8 @@ export function initDebug() {
};

options.diffed = (vnode) => {
if (vnode == null) { return; }

if (vnode._component && vnode._component.__hooks) {
let hooks = vnode._component.__hooks;
hooks._list.forEach(hook => {
Expand Down
2 changes: 1 addition & 1 deletion debug/src/devtools/custom.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ export function getChildren(vnode) {
*/
export function isRoot(vnode) {
// Timings of root vnodes will never be set
return vnode.type===Fragment && vnode.endTime==-1;
return vnode.type===Fragment && vnode._parent === null;
}

/**
Expand Down
26 changes: 22 additions & 4 deletions debug/test/browser/debug.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,14 @@ describe('debug', () => {
expect(vnode.props.__self).to.be.undefined;
});

it('should throw an error when using a hook outside a render', () => {
// TODO: Fix this test. It only passed before because App was the first component
// into render so currentComponent in hooks/index.js wasn't set yet. However,
// any children under App wouldn't have thrown the error if they did what App
// did because currentComponent would be set to App.
// In other words, hooks never clear currentComponent so once it is set, it won't
// be unset
it.skip('should throw an error when using a hook outside a render', () => {
const Foo = props => props.children;
class App extends Component {
componentWillMount() {
useState();
Expand All @@ -115,12 +122,23 @@ describe('debug', () => {
return <p>test</p>;
}
}
const fn = () => act(() => render(<App />, scratch));
const fn = () => act(() => render(<Foo><App /></Foo>, scratch));
expect(fn).to.throw(/Hook can only be invoked from render/);
});

it('should throw an error when invoked outside of a component', () => {
const fn = () => act(() => useState());
// TODO: Fix this test. It only passed before because render was never called.
// Once render is called, currentComponent is set and never unset so calls to
// hooks outside of components would still work.
it.skip('should throw an error when invoked outside of a component', () => {
function Foo(props) {
useEffect(() => {}); // Pretend to use a hook
return props.children;
}

const fn = () => act(() => {
render(<Foo>Hello!</Foo>, scratch);
useState();
});
expect(fn).to.throw(/Hook can only be invoked from render/);
});

Expand Down
8 changes: 6 additions & 2 deletions debug/test/browser/devtools.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -455,8 +455,8 @@ describe('devtools', () => {
render(<div />, scratch);

expect(vnodeSpy, 'vnode').to.have.been.called;
expect(diffSpy, 'diff').to.have.been.calledOnce;
expect(diffedSpy, 'diffed').to.have.been.calledOnce;
expect(diffSpy, 'diff').to.have.been.calledTwice;
expect(diffedSpy, 'diffed').to.have.been.calledTwice;
expect(commitSpy, 'commit').to.have.been.calledOnce;

render(null, scratch);
Expand Down Expand Up @@ -592,6 +592,7 @@ describe('devtools', () => {
checkEventReferences(prev.concat(hook.log));

expect(serialize(hook.log)).to.deep.equal([
{ type: 'update', component: 'Fragment' },
{ type: 'rootCommitted', component: 'Fragment' }
]);
});
Expand All @@ -609,6 +610,7 @@ describe('devtools', () => {
expect(serialize(hook.log)).to.deep.equal([
{ type: 'unmount', component: '#text: Hello World' },
{ type: 'mount', component: 'span' },
{ type: 'update', component: 'Fragment' },
{ type: 'rootCommitted', component: 'Fragment' }
]);
});
Expand Down Expand Up @@ -722,6 +724,7 @@ describe('devtools', () => {
checkEventReferences(prev.concat(hook.log));

expect(serialize(hook.log)).to.deep.equal([
{ type: 'update', component: 'Fragment' },
{ type: 'rootCommitted', component: 'Fragment' }
]);
});
Expand All @@ -736,6 +739,7 @@ describe('devtools', () => {
{ type: 'unmount', component: 'span' },
{ type: 'unmount', component: '#text: Hello World' },
{ type: 'update', component: 'div' },
{ type: 'update', component: 'Fragment' },
{ type: 'rootCommitted', component: 'Fragment' }
]);
});
Expand Down
4 changes: 1 addition & 3 deletions src/diff/children.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,7 @@ export function diffChildren(parentDom, newParentVNode, oldParentVNode, context,
if (oldDom == EMPTY_OBJ) {
oldDom = null;
if (excessDomChildren!=null) {
for (i = 0; !oldDom && i < excessDomChildren.length; i++) {
oldDom = excessDomChildren[i];
}
oldDom = excessDomChildren[0];
}
else {
for (i = 0; !oldDom && i < oldChildrenLength; i++) {
Expand Down
65 changes: 32 additions & 33 deletions src/diff/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ import options from '../options';
* Fragments that have siblings. In most cases, it starts out as `oldChildren[0]._dom`.
*/
export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, force, oldDom) {
let c, tmp, isNew, oldProps, oldState, snapshot,
newType = newVNode.type, clearProcessingException;
let tmp, newType = newVNode.type;

// When passing through createElement it assigns the object
// constructor as undefined. This to prevent JSON-injection.
Expand All @@ -33,6 +32,8 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi

try {
outer: if (typeof newType==='function') {
let c, isNew, oldProps, oldState, snapshot, clearProcessingException;
let newProps = newVNode.props;

// Necessary for createContext api. Setting this property will pass
// the context value as `this.context` just for this component.
Expand All @@ -48,16 +49,16 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi
else {
// Instantiate the new component
if (newType.prototype && newType.prototype.render) {
newVNode._component = c = new newType(newVNode.props, cctx); // eslint-disable-line new-cap
newVNode._component = c = new newType(newProps, cctx); // eslint-disable-line new-cap
}
else {
newVNode._component = c = new Component(newVNode.props, cctx);
newVNode._component = c = new Component(newProps, cctx);
c.constructor = newType;
c.render = doRender;
}
if (provider) provider.sub(c);

c.props = newVNode.props;
c.props = newProps;
if (!c.state) c.state = {};
c.context = cctx;
c._context = context;
Expand All @@ -70,7 +71,7 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi
c._nextState = c.state;
}
if (newType.getDerivedStateFromProps!=null) {
assign(c._nextState==c.state ? (c._nextState = assign({}, c._nextState)) : c._nextState, newType.getDerivedStateFromProps(newVNode.props, c._nextState));
assign(c._nextState==c.state ? (c._nextState = assign({}, c._nextState)) : c._nextState, newType.getDerivedStateFromProps(newProps, c._nextState));
}

// Invoke pre-render lifecycle methods
Expand All @@ -80,11 +81,11 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi
}
else {
if (newType.getDerivedStateFromProps==null && force==null && c.componentWillReceiveProps!=null) {
c.componentWillReceiveProps(newVNode.props, cctx);
c.componentWillReceiveProps(newProps, cctx);
}

if (!force && c.shouldComponentUpdate!=null && c.shouldComponentUpdate(newVNode.props, c._nextState, cctx)===false) {
c.props = newVNode.props;
if (!force && c.shouldComponentUpdate!=null && c.shouldComponentUpdate(newProps, c._nextState, cctx)===false) {
c.props = newProps;
c.state = c._nextState;
c._dirty = false;
c._vnode = newVNode;
Expand All @@ -94,15 +95,15 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi
}

if (c.componentWillUpdate!=null) {
c.componentWillUpdate(newVNode.props, c._nextState, cctx);
c.componentWillUpdate(newProps, c._nextState, cctx);
}
}

oldProps = c.props;
oldState = c.state;

c.context = cctx;
c.props = newVNode.props;
c.props = newProps;
c.state = c._nextState;

if (tmp = options._render) tmp(newVNode);
Expand Down Expand Up @@ -141,15 +142,15 @@ export function diff(parentDom, newVNode, oldVNode, context, isSvg, excessDomChi
if (!isNew && oldProps!=null && c.componentDidUpdate!=null) {
c.componentDidUpdate(oldProps, oldState, snapshot);
}

if (clearProcessingException) {
c._pendingError = c._processingException = null;
}
}
else {
newVNode._dom = diffElementNodes(oldVNode._dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts);
}

if (clearProcessingException) {
c._pendingError = c._processingException = null;
}

if (tmp = options.diffed) tmp(newVNode);
}
catch (e) {
Expand Down Expand Up @@ -219,29 +220,27 @@ function diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChil
dom.data = newProps;
}
}
else {
if (excessDomChildren!=null && dom.childNodes!=null) {
else if (newVNode!==oldVNode) {
if (excessDomChildren!=null) {
excessDomChildren = EMPTY_ARR.slice.call(dom.childNodes);
}
if (newVNode!==oldVNode) {
let oldProps = oldVNode.props || EMPTY_OBJ;
let newProps = newVNode.props;

let oldHtml = oldProps.dangerouslySetInnerHTML;
let newHtml = newProps.dangerouslySetInnerHTML;
if ((newHtml || oldHtml) && excessDomChildren==null) {
// Avoid re-applying the same '__html' if it did not changed between re-render
if (!newHtml || !oldHtml || newHtml.__html!=oldHtml.__html) {
dom.innerHTML = newHtml && newHtml.__html || '';
}
}
if (newProps.multiple) {
dom.multiple = newProps.multiple;
}
oldProps = oldVNode.props || EMPTY_OBJ;

diffChildren(dom, newVNode, oldVNode, context, newVNode.type==='foreignObject' ? false : isSvg, excessDomChildren, mounts, EMPTY_OBJ);
diffProps(dom, newProps, oldProps, isSvg);
let oldHtml = oldProps.dangerouslySetInnerHTML;
let newHtml = newProps.dangerouslySetInnerHTML;
if ((newHtml || oldHtml) && excessDomChildren==null) {
// Avoid re-applying the same '__html' if it did not changed between re-render
if (!newHtml || !oldHtml || newHtml.__html!=oldHtml.__html) {
dom.innerHTML = newHtml && newHtml.__html || '';
}
}
if (newProps.multiple) {
dom.multiple = newProps.multiple;
}

diffChildren(dom, newVNode, oldVNode, context, newVNode.type==='foreignObject' ? false : isSvg, excessDomChildren, mounts, EMPTY_OBJ);
diffProps(dom, newProps, oldProps, isSvg);
}

return dom;
Expand Down
10 changes: 5 additions & 5 deletions src/render.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { EMPTY_OBJ, EMPTY_ARR } from './constants';
import { commitRoot } from './diff/index';
import { diffChildren } from './diff/children';
import { commitRoot, diff } from './diff/index';
import { createElement, Fragment } from './create-element';
import options from './options';

Expand All @@ -18,10 +17,10 @@ export function render(vnode, parentDom, replaceNode) {
vnode = createElement(Fragment, null, [vnode]);

let mounts = [];
diffChildren(
diff(
parentDom,
replaceNode ? vnode : (parentDom._children = vnode),
oldVNode,
oldVNode || EMPTY_OBJ,
EMPTY_OBJ,
parentDom.ownerSVGElement !== undefined,
replaceNode
Expand All @@ -30,7 +29,8 @@ export function render(vnode, parentDom, replaceNode) {
? null
: EMPTY_ARR.slice.call(parentDom.childNodes),
mounts,
replaceNode || EMPTY_OBJ
false,
replaceNode || EMPTY_OBJ,
);
commitRoot(mounts, vnode);
}
Expand Down

0 comments on commit f39f877

Please sign in to comment.