diff --git a/__tests__/react-dom/ReactFunctionComponent-test.js b/__tests__/react-dom/ReactFunctionComponent-test.js new file mode 100644 index 0000000..055f36c --- /dev/null +++ b/__tests__/react-dom/ReactFunctionComponent-test.js @@ -0,0 +1,325 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +let React; +let ReactDOM; +let ReactTestUtils; + +function FunctionComponent(props) { + return
{props.name}
; +} + +describe('ReactFunctionComponent', () => { + beforeEach(() => { + jest.resetModules(); + React = require('../../dist/react') + ReactDOM = require('../../dist/react-dom') + ReactTestUtils = require('../utils/test-utils') + }); + + it('should render stateless component', () => { + const el = document.createElement('div'); + // console.log(FunctionComponent.toString()) + ReactDOM.createRoot(el).render(); + expect(el.textContent).toBe('A'); + }); + + // it('should update stateless component', () => { + // function Parent(props) { + // return ; + // } + + // const el = document.createElement('div'); + // ReactDOM.createRoot(el).render(); + // expect(el.textContent).toBe('A'); + + // ReactDOM.createRoot(el).render(); + // expect(el.textContent).toBe('B'); + // }); + + it('should not throw when stateless component returns undefined', () => { + function NotAComponent() { + } + + expect(function () { + ReactTestUtils.renderIntoDocument( +
+ +
+ ); + }).not.toThrowError(); + }); + + // it('should throw on string refs in pure functions', () => { + // function Child() { + // return
; + // } + + // expect(function() { + // ReactTestUtils.renderIntoDocument(); + // }).toThrowError( + // __LOG__ + // ? 'Function components cannot have string refs. We recommend using useRef() instead.' + // : // It happens because we don't save _owner in production for + // // function components. + // 'Element ref was specified as a string (me) but no owner was set. This could happen for one of' + + // ' the following reasons:\n' + + // '1. You may be adding a ref to a function component\n' + + // "2. You may be adding a ref to a component that was not created inside a component's render method\n" + + // '3. You have multiple copies of React loaded\n' + + // 'See https://reactjs.org/link/refs-must-have-owner for more information.', + // ); + // }); + + // it('should warn when given a string ref', () => { + // function Indirection(props) { + // return
{props.children}
; + // } + + // class ParentUsingStringRef extends React.Component { + // render() { + // return ( + // + // + // + // ); + // } + // } + + // expect(() => + // ReactTestUtils.renderIntoDocument(), + // ).toErrorDev( + // 'Warning: Function components cannot be given refs. ' + + // 'Attempts to access this ref will fail. ' + + // 'Did you mean to use React.forwardRef()?\n\n' + + // 'Check the render method ' + + // 'of `ParentUsingStringRef`.\n' + + // ' in FunctionComponent (at **)\n' + + // ' in div (at **)\n' + + // ' in Indirection (at **)\n' + + // ' in ParentUsingStringRef (at **)', + // ); + + // // No additional warnings should be logged + // ReactTestUtils.renderIntoDocument(); + // }); + + // it('should warn when given a function ref', () => { + // function Indirection(props) { + // return
{props.children}
; + // } + + // class ParentUsingFunctionRef extends React.Component { + // render() { + // return ( + // + // { + // expect(arg).toBe(null); + // }} + // /> + // + // ); + // } + // } + + // expect(() => + // ReactTestUtils.renderIntoDocument(), + // ).toErrorDev( + // 'Warning: Function components cannot be given refs. ' + + // 'Attempts to access this ref will fail. ' + + // 'Did you mean to use React.forwardRef()?\n\n' + + // 'Check the render method ' + + // 'of `ParentUsingFunctionRef`.\n' + + // ' in FunctionComponent (at **)\n' + + // ' in div (at **)\n' + + // ' in Indirection (at **)\n' + + // ' in ParentUsingFunctionRef (at **)', + // ); + + // // No additional warnings should be logged + // ReactTestUtils.renderIntoDocument(); + // }); + + // it('deduplicates ref warnings based on element or owner', () => { + // // When owner uses JSX, we can use exact line location to dedupe warnings + // class AnonymousParentUsingJSX extends React.Component { + // render() { + // return {}} />; + // } + // } + // Object.defineProperty(AnonymousParentUsingJSX, 'name', {value: undefined}); + + // let instance1; + + // expect(() => { + // instance1 = ReactTestUtils.renderIntoDocument( + // , + // ); + // }).toErrorDev('Warning: Function components cannot be given refs.'); + // // Should be deduped (offending element is on the same line): + // instance1.forceUpdate(); + // // Should also be deduped (offending element is on the same line): + // ReactTestUtils.renderIntoDocument(); + + // // When owner doesn't use JSX, and is anonymous, we warn once per internal instance. + // class AnonymousParentNotUsingJSX extends React.Component { + // render() { + // return React.createElement(FunctionComponent, { + // name: 'A', + // ref: () => {}, + // }); + // } + // } + // Object.defineProperty(AnonymousParentNotUsingJSX, 'name', { + // value: undefined, + // }); + + // let instance2; + // expect(() => { + // instance2 = ReactTestUtils.renderIntoDocument( + // , + // ); + // }).toErrorDev('Warning: Function components cannot be given refs.'); + // // Should be deduped (same internal instance, no additional warnings) + // instance2.forceUpdate(); + // // Could not be differentiated (since owner is anonymous and no source location) + // ReactTestUtils.renderIntoDocument(); + + // // When owner doesn't use JSX, but is named, we warn once per owner name + // class NamedParentNotUsingJSX extends React.Component { + // render() { + // return React.createElement(FunctionComponent, { + // name: 'A', + // ref: () => {}, + // }); + // } + // } + // let instance3; + // expect(() => { + // instance3 = ReactTestUtils.renderIntoDocument(); + // }).toErrorDev('Warning: Function components cannot be given refs.'); + // // Should be deduped (same owner name, no additional warnings): + // instance3.forceUpdate(); + // // Should also be deduped (same owner name, no additional warnings): + // ReactTestUtils.renderIntoDocument(); + // }); + + // // This guards against a regression caused by clearing the current debug fiber. + // // https://github.com/facebook/react/issues/10831 + // it('should warn when giving a function ref with context', () => { + // function Child() { + // return null; + // } + // Child.contextTypes = { + // foo: PropTypes.string, + // }; + + // class Parent extends React.Component { + // static childContextTypes = { + // foo: PropTypes.string, + // }; + // getChildContext() { + // return { + // foo: 'bar', + // }; + // } + // render() { + // return ; + // } + // } + + // expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + // 'Warning: Function components cannot be given refs. ' + + // 'Attempts to access this ref will fail. ' + + // 'Did you mean to use React.forwardRef()?\n\n' + + // 'Check the render method ' + + // 'of `Parent`.\n' + + // ' in Child (at **)\n' + + // ' in Parent (at **)', + // ); + // }); + + // it('should provide a null ref', () => { + // function Child() { + // return
; + // } + + // const comp = ReactTestUtils.renderIntoDocument(); + // expect(comp).toBe(null); + // }); + + // it('should use correct name in key warning', () => { + // function Child() { + // return
{[]}
; + // } + + // expect(() => ReactTestUtils.renderIntoDocument()).toErrorDev( + // 'Each child in a list should have a unique "key" prop.\n\n' + + // 'Check the render method of `Child`.', + // ); + // }); + + // // TODO: change this test after we deprecate default props support + // // for function components + + // it('should receive context', () => { + // class Parent extends React.Component { + // static childContextTypes = { + // lang: PropTypes.string, + // }; + + // getChildContext() { + // return {lang: 'en'}; + // } + + // render() { + // return ; + // } + // } + + // function Child(props, context) { + // return
{context.lang}
; + // } + // Child.contextTypes = {lang: PropTypes.string}; + + // const el = document.createElement('div'); + // ReactDOM.render(, el); + // expect(el.textContent).toBe('en'); + // }); + + it('should work with arrow functions', () => { + let Child = function () { + return
; + }; + // Will create a new bound function without a prototype, much like a native + // arrow function. + Child = Child.bind(this); + + expect(() => ReactTestUtils.renderIntoDocument()).not.toThrow(); + }); + + it('should allow simple functions to return null', () => { + const Child = function () { + return null; + }; + expect(() => ReactTestUtils.renderIntoDocument()).not.toThrow(); + }); + + it('should allow simple functions to return false', () => { + function Child() { + return false; + } + + expect(() => ReactTestUtils.renderIntoDocument()).not.toThrow(); + }); +}); diff --git a/__tests__/react/ReactElement-test.js b/__tests__/react/ReactElement-test.js index b0ba1b3..967c837 100644 --- a/__tests__/react/ReactElement-test.js +++ b/__tests__/react/ReactElement-test.js @@ -14,269 +14,271 @@ let ReactDOM let ReactTestUtils describe('ReactElement', () => { - let ComponentFC - let originalSymbol - - beforeEach(() => { - jest.resetModules() - - // Delete the native Symbol if we have one to ensure we test the - // unpolyfilled environment. - originalSymbol = global.Symbol - global.Symbol = undefined - - React = require('../../dist/react') - ReactDOM = require('../../dist/react-dom') - ReactTestUtils = require('../utils/test-utils') - - // NOTE: We're explicitly not using JSX here. This is intended to test - // classic JS without JSX. - ComponentFC = () => { - return React.createElement('div') - } - }) - - afterEach(() => { - global.Symbol = originalSymbol - }) - - it('uses the fallback value when in an environment without Symbol', () => { - expect((
).$$typeof).toBe('react.element') - }) - - it('returns a complete element according to spec', () => { - const element = React.createElement(ComponentFC) - expect(element.type).toBe(ComponentFC) - expect(element.key).toBe(null) - expect(element.ref).toBe(null) - - expect(element.props).toEqual({}) - }) - - it('allows a string to be passed as the type', () => { - const element = React.createElement('div') - expect(element.type).toBe('div') - expect(element.key).toBe(null) - expect(element.ref).toBe(null) - expect(element.props).toEqual({}) - }) - - it('returns an immutable element', () => { - const element = React.createElement(ComponentFC) - expect(() => (element.type = 'div')).not.toThrow() - }) - - it('does not reuse the original config object', () => { - const config = {foo: 1} - const element = React.createElement(ComponentFC, config) - expect(element.props.foo).toBe(1) - config.foo = 2 - expect(element.props.foo).toBe(1) - }) - - it('does not fail if config has no prototype', () => { - const config = Object.create(null, {foo: {value: 1, enumerable: true}}) - const element = React.createElement(ComponentFC, config) - expect(element.props.foo).toBe(1) - }) - - it('extracts key and ref from the config', () => { - const element = React.createElement(ComponentFC, { - key: '12', - ref: '34', - foo: '56', + let ComponentFC + let originalSymbol + + beforeEach(() => { + jest.resetModules() + + // Delete the native Symbol if we have one to ensure we test the + // unpolyfilled environment. + originalSymbol = global.Symbol + global.Symbol = undefined + + React = require('../../dist/react') + ReactDOM = require('../../dist/react-dom') + ReactTestUtils = require('../utils/test-utils') + + // NOTE: We're explicitly not using JSX here. This is intended to test + // classic JS without JSX. + ComponentFC = () => { + return React.createElement('div') + } }) - expect(element.type).toBe(ComponentFC) - expect(element.key).toBe('12') - expect(element.ref).toBe('34') - expect(element.props).toEqual({foo: '56'}) - }) - - it('extracts null key and ref', () => { - const element = React.createElement(ComponentFC, { - key: null, - ref: null, - foo: '12', + + afterEach(() => { + global.Symbol = originalSymbol + }) + + it('uses the fallback value when in an environment without Symbol', () => { + expect((
).$$typeof).toBe('react.element') + }) + + it('returns a complete element according to spec', () => { + const element = React.createElement(ComponentFC) + expect(element.type).toBe(ComponentFC) + expect(element.key).toBe(null) + expect(element.ref).toBe(null) + + expect(element.props).toEqual({}) + }) + + it('allows a string to be passed as the type', () => { + const element = React.createElement('div') + expect(element.type).toBe('div') + expect(element.key).toBe(null) + expect(element.ref).toBe(null) + expect(element.props).toEqual({}) + }) + + it('returns an immutable element', () => { + const element = React.createElement(ComponentFC) + expect(() => (element.type = 'div')).not.toThrow() }) - expect(element.type).toBe(ComponentFC) - expect(element.key).toBe('null') - expect(element.ref).toBe(null) - expect(element.props).toEqual({foo: '12'}) - }) - - it('ignores undefined key and ref', () => { - const props = { - foo: '56', - key: undefined, - ref: undefined, - } - const element = React.createElement(ComponentFC, props) - expect(element.type).toBe(ComponentFC) - expect(element.key).toBe(null) - expect(element.ref).toBe(null) - expect(element.props).toEqual({foo: '56'}) - }) - - it('ignores key and ref warning getters', () => { - const elementA = React.createElement('div') - const elementB = React.createElement('div', elementA.props) - expect(elementB.key).toBe(null) - expect(elementB.ref).toBe(null) - }) - - it('coerces the key to a string', () => { - const element = React.createElement(ComponentFC, { - key: 12, - foo: '56', + + it('does not reuse the original config object', () => { + const config = {foo: 1} + const element = React.createElement(ComponentFC, config) + expect(element.props.foo).toBe(1) + config.foo = 2 + expect(element.props.foo).toBe(1) }) - expect(element.type).toBe(ComponentFC) - expect(element.key).toBe('12') - expect(element.ref).toBe(null) - expect(element.props).toEqual({foo: '56'}) - }) - - // it('preserves the owner on the element', () => { - // let element; - - // function Wrapper() { - // element = React.createElement(ComponentFC); - // return element; - // } - - // const instance = ReactTestUtils.renderIntoDocument( - // React.createElement(Wrapper) - // ); - // expect(element._owner.stateNode).toBe(instance); - // }); - - // it('merges an additional argument onto the children prop', () => { - // const a = 1; - // const element = React.createElement( - // ComponentFC, - // { - // children: 'text' - // }, - // a - // ); - // expect(element.props.children).toBe(a); - // }); - - it('does not override children if no rest args are provided', () => { - const element = React.createElement(ComponentFC, { - children: 'text', + + it('does not fail if config has no prototype', () => { + const config = Object.create(null, {foo: {value: 1, enumerable: true}}) + const element = React.createElement(ComponentFC, config) + expect(element.props.foo).toBe(1) + }) + + it('extracts key and ref from the config', () => { + const element = React.createElement(ComponentFC, { + key: '12', + ref: '34', + foo: '56', + }) + expect(element.type).toBe(ComponentFC) + expect(element.key).toBe('12') + expect(element.ref).toBe('34') + expect(element.props).toEqual({foo: '56'}) + }) + + it('extracts null key and ref', () => { + const element = React.createElement(ComponentFC, { + key: null, + ref: null, + foo: '12', + }) + expect(element.type).toBe(ComponentFC) + expect(element.key).toBe('null') + expect(element.ref).toBe(null) + expect(element.props).toEqual({foo: '12'}) + }) + + it('ignores undefined key and ref', () => { + const props = { + foo: '56', + key: undefined, + ref: undefined, + } + const element = React.createElement(ComponentFC, props) + expect(element.type).toBe(ComponentFC) + expect(element.key).toBe(null) + expect(element.ref).toBe(null) + expect(element.props).toEqual({foo: '56'}) + }) + + it('ignores key and ref warning getters', () => { + const elementA = React.createElement('div') + const elementB = React.createElement('div', elementA.props) + expect(elementB.key).toBe(null) + expect(elementB.ref).toBe(null) + }) + + it('coerces the key to a string', () => { + const element = React.createElement(ComponentFC, { + key: 12, + foo: '56', + }) + expect(element.type).toBe(ComponentFC) + expect(element.key).toBe('12') + expect(element.ref).toBe(null) + expect(element.props).toEqual({foo: '56'}) + }) + + // it('preserves the owner on the element', () => { + // let element; + + // function Wrapper() { + // element = React.createElement(ComponentFC); + // return element; + // } + + // const instance = ReactTestUtils.renderIntoDocument( + // React.createElement(Wrapper) + // ); + // expect(element._owner.stateNode).toBe(instance); + // }); + + it('merges an additional argument onto the children prop', () => { + const a = 1; + const element = React.createElement( + ComponentFC, + { + children: 'text' + }, + a + ); + expect(element.props.children).toBe(a); + }); + + it('does not override children if no rest args are provided', () => { + const element = React.createElement(ComponentFC, { + children: 'text', + }) + expect(element.props.children).toBe('text') + }) + + it('overrides children if null is provided as an argument', () => { + const element = React.createElement( + ComponentFC, + { + children: 'text' + }, + null + ); + expect(element.props.children).toBe(null); + }); + + it('merges rest arguments onto the children prop in an array', () => { + const a = 1; + const b = 2; + const c = 3; + const element = React.createElement(ComponentFC, null, a, b, c); + expect(element.props.children).toEqual([1, 2, 3]); + }); + + // // NOTE: We're explicitly not using JSX here. This is intended to test + // // classic JS without JSX. + it('allows static methods to be called using the type property', () => { + function StaticMethodComponent() { + return React.createElement('div') + } + + StaticMethodComponent.someStaticMethod = () => 'someReturnValue' + + const element = React.createElement(StaticMethodComponent) + expect(element.type.someStaticMethod()).toBe('someReturnValue') + }) + + // // NOTE: We're explicitly not using JSX here. This is intended to test + // // classic JS without JSX. + it('identifies valid elements', () => { + function Component() { + return React.createElement('div') + } + + expect(React.isValidElement(React.createElement('div'))).toEqual(true) + expect(React.isValidElement(React.createElement(Component))).toEqual(true) + + expect(React.isValidElement(null)).toEqual(false) + expect(React.isValidElement(true)).toEqual(false) + expect(React.isValidElement({})).toEqual(false) + expect(React.isValidElement('string')).toEqual(false) + expect(React.isValidElement(Component)).toEqual(false) + expect(React.isValidElement({type: 'div', props: {}})).toEqual(false) + + const jsonElement = JSON.stringify(React.createElement('div')) + expect(React.isValidElement(JSON.parse(jsonElement))).toBe(true) + }) + + // // NOTE: We're explicitly not using JSX here. This is intended to test + // // classic JS without JSX. + it('is indistinguishable from a plain object', () => { + const element = React.createElement('div', {className: 'foo'}) + const object = {} + expect(element.constructor).toBe(object.constructor) + }) + + it('does not warn for NaN props', () => { + function Test() { + return
+ } + + const test = ReactTestUtils.renderIntoDocument() + expect(test.props.value).toBeNaN() + }) + + // // NOTE: We're explicitly not using JSX here. This is intended to test + // // classic JS without JSX. + it('identifies elements, but not JSON, if Symbols are supported', () => { + // Rudimentary polyfill + // Once all jest engines support Symbols natively we can swap this to test + // WITH native Symbols by default. + const REACT_ELEMENT_TYPE = function () { + } // fake Symbol + const OTHER_SYMBOL = function () { + } // another fake Symbol + global.Symbol = function (name) { + return OTHER_SYMBOL + } + global.Symbol.for = function (key) { + if (key === 'react.element') { + return REACT_ELEMENT_TYPE + } + return OTHER_SYMBOL + } + + jest.resetModules() + + React = require('../../dist/react') + + function Component() { + return React.createElement('div') + } + + expect(React.isValidElement(React.createElement('div'))).toEqual(true) + expect(React.isValidElement(React.createElement(Component))).toEqual(true) + + expect(React.isValidElement(null)).toEqual(false) + expect(React.isValidElement(true)).toEqual(false) + expect(React.isValidElement({})).toEqual(false) + expect(React.isValidElement('string')).toEqual(false) + + expect(React.isValidElement(Component)).toEqual(false) + expect(React.isValidElement({type: 'div', props: {}})).toEqual(false) + + const jsonElement = JSON.stringify(React.createElement('div')) + // ignore this test + // expect(React.isValidElement(JSON.parse(jsonElement))).toBe(false); }) - expect(element.props.children).toBe('text') - }) - - // it('overrides children if null is provided as an argument', () => { - // const element = React.createElement( - // ComponentFC, - // { - // children: 'text' - // }, - // null - // ); - // expect(element.props.children).toBe(null); - // }); - - // it('merges rest arguments onto the children prop in an array', () => { - // const a = 1; - // const b = 2; - // const c = 3; - // const element = React.createElement(ComponentFC, null, a, b, c); - // expect(element.props.children).toEqual([1, 2, 3]); - // }); - - // // NOTE: We're explicitly not using JSX here. This is intended to test - // // classic JS without JSX. - it('allows static methods to be called using the type property', () => { - function StaticMethodComponent() { - return React.createElement('div') - } - - StaticMethodComponent.someStaticMethod = () => 'someReturnValue' - - const element = React.createElement(StaticMethodComponent) - expect(element.type.someStaticMethod()).toBe('someReturnValue') - }) - - // // NOTE: We're explicitly not using JSX here. This is intended to test - // // classic JS without JSX. - it('identifies valid elements', () => { - function Component() { - return React.createElement('div') - } - - expect(React.isValidElement(React.createElement('div'))).toEqual(true) - expect(React.isValidElement(React.createElement(Component))).toEqual(true) - - expect(React.isValidElement(null)).toEqual(false) - expect(React.isValidElement(true)).toEqual(false) - expect(React.isValidElement({})).toEqual(false) - expect(React.isValidElement('string')).toEqual(false) - expect(React.isValidElement(Component)).toEqual(false) - expect(React.isValidElement({type: 'div', props: {}})).toEqual(false) - - const jsonElement = JSON.stringify(React.createElement('div')) - expect(React.isValidElement(JSON.parse(jsonElement))).toBe(true) - }) - - // // NOTE: We're explicitly not using JSX here. This is intended to test - // // classic JS without JSX. - it('is indistinguishable from a plain object', () => { - const element = React.createElement('div', {className: 'foo'}) - const object = {} - expect(element.constructor).toBe(object.constructor) - }) - - it('does not warn for NaN props', () => { - function Test() { - return
- } - - const test = ReactTestUtils.renderIntoDocument() - expect(test.props.value).toBeNaN() - }) - - // // NOTE: We're explicitly not using JSX here. This is intended to test - // // classic JS without JSX. - it('identifies elements, but not JSON, if Symbols are supported', () => { - // Rudimentary polyfill - // Once all jest engines support Symbols natively we can swap this to test - // WITH native Symbols by default. - const REACT_ELEMENT_TYPE = function () {} // fake Symbol - const OTHER_SYMBOL = function () {} // another fake Symbol - global.Symbol = function (name) { - return OTHER_SYMBOL - } - global.Symbol.for = function (key) { - if (key === 'react.element') { - return REACT_ELEMENT_TYPE - } - return OTHER_SYMBOL - } - - jest.resetModules() - - React = require('../../dist/react') - - function Component() { - return React.createElement('div') - } - - expect(React.isValidElement(React.createElement('div'))).toEqual(true) - expect(React.isValidElement(React.createElement(Component))).toEqual(true) - - expect(React.isValidElement(null)).toEqual(false) - expect(React.isValidElement(true)).toEqual(false) - expect(React.isValidElement({})).toEqual(false) - expect(React.isValidElement('string')).toEqual(false) - - expect(React.isValidElement(Component)).toEqual(false) - expect(React.isValidElement({type: 'div', props: {}})).toEqual(false) - - const jsonElement = JSON.stringify(React.createElement('div')) - // ignore this test - // expect(React.isValidElement(JSON.parse(jsonElement))).toBe(false); - }) }) diff --git a/examples/hello-world/src/App.tsx b/examples/hello-world/src/App.tsx index cbe2731..84830df 100644 --- a/examples/hello-world/src/App.tsx +++ b/examples/hello-world/src/App.tsx @@ -1,26 +1,33 @@ import {useState} from 'react' - function App() { const [num, updateNum] = useState(0); - const isOdd = num % 2; + const isOdd = num % 2 === 1; + + const before = [ +
  • 1
  • , +
  • 2
  • , +
  • 3
  • , +
  • 4
  • + ]; + const after = [ + //
  • 4
  • , +
  • 2
  • , +
  • 3
  • , +
  • 1
  • + ]; + const listToUse = isOdd ? after : before; + console.log(num, listToUse) return ( -

    { - e.stopPropagation() - console.log('click h3', e.currentTarget) - updateNum(prev => prev + 1); +
      { + updateNum(num => num + 1); }} > -
      { - console.log('click div', e.currentTarget) - }}> - {isOdd ?
      odd
      :

      even

      } -
      - -

    + {listToUse} + ); } diff --git a/package.json b/package.json index ba23706..055f53c 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ }, "scripts": { "build:dev": "ENV=dev node scripts/build.js", - "build:test": "node scripts/build.js --test", + "build:test": "ENV=test node scripts/build.js --test", "test": "npm run build:test && jest" }, "author": "", diff --git a/packages/react-dom/src/host_config.rs b/packages/react-dom/src/host_config.rs index 701c5bd..1e15926 100644 --- a/packages/react-dom/src/host_config.rs +++ b/packages/react-dom/src/host_config.rs @@ -8,7 +8,7 @@ use web_sys::{Node, window}; use react_reconciler::HostConfig; use shared::{log, type_of}; -use crate::synthetic_event::update_event_props; +use crate::synthetic_event::update_fiber_props; pub struct ReactDomHostConfig; @@ -35,9 +35,9 @@ impl HostConfig for ReactDomHostConfig { fn create_text_instance(&self, content: &JsValue) -> Rc { let window = window().expect("no global `window` exists"); let document = window.document().expect("should have a document on window"); - Rc::new(Node::from(document.create_text_node( - to_string(content).as_str() - ))) + Rc::new(Node::from( + document.create_text_node(to_string(content).as_str()), + )) } fn create_instance(&self, _type: String, props: Rc) -> Rc { @@ -45,13 +45,15 @@ impl HostConfig for ReactDomHostConfig { let document = window.document().expect("should have a document on window"); match document.create_element(_type.as_ref()) { Ok(element) => { - let element = update_event_props( + let element = update_fiber_props( element.clone(), &*props.clone().downcast::().unwrap(), ); Rc::new(Node::from(element)) } - Err(_) => todo!(), + Err(_) => { + panic!("Failed to create_instance {:?}", _type); + } } } @@ -60,9 +62,19 @@ impl HostConfig for ReactDomHostConfig { let c = child.clone().downcast::().unwrap(); match p.append_child(&c) { Ok(_) => { - log!("append_initial_child successfully {:?} {:?}", p, c); + log!( + "append_initial_child {:?} {:?}", + p, + if c.first_child().is_some() { + c.first_child().clone().unwrap().text_content() + } else { + c.text_content() + } + ); + } + Err(_) => { + log!("Failed to append_initial_child {:?} {:?}", p, c); } - Err(_) => todo!(), } } @@ -75,9 +87,11 @@ impl HostConfig for ReactDomHostConfig { let c = child.clone().downcast::().unwrap(); match p.remove_child(&c) { Ok(_) => { - log!("remove_child successfully {:?} {:?}", p, c); + log!("remove_child {:?} {:?}", p, c); + } + Err(e) => { + log!("Failed to remove_child {:?} {:?} {:?} ", e, p, c); } - Err(_) => todo!(), } } @@ -85,4 +99,40 @@ impl HostConfig for ReactDomHostConfig { let text_instance = text_instance.clone().downcast::().unwrap(); text_instance.set_node_value(Some(content.as_str())); } + + fn insert_child_to_container( + &self, + child: Rc, + container: Rc, + before: Rc, + ) { + let parent = container.clone().downcast::().unwrap(); + let before = before.clone().downcast::().unwrap(); + let child = child.clone().downcast::().unwrap(); + match parent.insert_before(&child, Some(&before)) { + Ok(_) => { + log!( + "insert_child_to_container {:?} {:?} {:?}", + parent, + if before.first_child().is_some() { + before.first_child().clone().unwrap().text_content() + } else { + before.text_content() + }, + if child.first_child().is_some() { + child.first_child().clone().unwrap().text_content() + } else { + child.text_content() + } + ); + } + Err(_) => { + log!( + "Failed to insert_child_to_container {:?} {:?}", + parent, + child + ); + } + } + } } diff --git a/packages/react-dom/src/lib.rs b/packages/react-dom/src/lib.rs index 9e8cca5..84b5cd4 100644 --- a/packages/react-dom/src/lib.rs +++ b/packages/react-dom/src/lib.rs @@ -11,8 +11,8 @@ use crate::utils::set_panic_hook; mod host_config; mod renderer; -mod utils; mod synthetic_event; +mod utils; #[wasm_bindgen(js_name = createRoot)] pub fn create_root(container: &JsValue) -> Renderer { diff --git a/packages/react-dom/src/renderer.rs b/packages/react-dom/src/renderer.rs index dbf697d..9e17638 100644 --- a/packages/react-dom/src/renderer.rs +++ b/packages/react-dom/src/renderer.rs @@ -1,8 +1,8 @@ use std::cell::RefCell; use std::rc::Rc; -use wasm_bindgen::JsValue; use wasm_bindgen::prelude::*; +use wasm_bindgen::JsValue; use react_reconciler::fiber::FiberRootNode; use react_reconciler::Reconciler; @@ -17,8 +17,16 @@ pub struct Renderer { } impl Renderer { - pub fn new(root: Rc>, reconciler: Reconciler, container: &JsValue) -> Self { - Self { root, reconciler, container: container.clone() } + pub fn new( + root: Rc>, + reconciler: Reconciler, + container: &JsValue, + ) -> Self { + Self { + root, + reconciler, + container: container.clone(), + } } } diff --git a/packages/react-dom/src/synthetic_event.rs b/packages/react-dom/src/synthetic_event.rs index 1803bef..b0854d0 100644 --- a/packages/react-dom/src/synthetic_event.rs +++ b/packages/react-dom/src/synthetic_event.rs @@ -24,7 +24,7 @@ impl Paths { } fn create_synthetic_event(e: Event) -> Event { - Reflect::set(&*e, &"__stopPropagation".into(), &JsValue::from_bool(false)); + Reflect::set(&*e, &"__stopPropagation".into(), &JsValue::from_bool(false)).expect("TODO: panic set __stopPropagation"); let e_cloned = e.clone(); let origin_stop_propagation = derive_from_js_value(&*e, "stopPropagation"); @@ -33,21 +33,21 @@ fn create_synthetic_event(e: Event) -> Event { &*e_cloned, &"__stopPropagation".into(), &JsValue::from_bool(true), - ); + ).expect("TODO: panic __stopPropagation"); if origin_stop_propagation.is_function() { let origin_stop_propagation = origin_stop_propagation.dyn_ref::().unwrap(); - origin_stop_propagation.call0(&JsValue::null()); + origin_stop_propagation.call0(&JsValue::null()).expect("TODO: panic origin_stop_propagation"); } }) as Box); let function = closure.as_ref().unchecked_ref::().clone(); closure.forget(); - Reflect::set(&*e.clone(), &"stopPropagation".into(), &function.into()); + Reflect::set(&*e.clone(), &"stopPropagation".into(), &function.into()).expect("TODO: panic set stopPropagation"); e } fn trigger_event_flow(paths: Vec, se: &Event) { for callback in paths { - callback.call1(&JsValue::null(), se); + callback.call1(&JsValue::null(), se).expect("TODO: panic call callback"); if derive_from_js_value(se, "__stopPropagation") .as_bool() .unwrap() @@ -142,7 +142,7 @@ pub fn init_event(container: JsValue, event_type: String) { on_click.forget(); } -pub fn update_event_props(node: Element, props: &JsValue) -> Element { +pub fn update_fiber_props(node: Element, props: &JsValue) -> Element { let js_value = derive_from_js_value(&node, ELEMENT_EVENT_PROPS_KEY); let element_event_props = if js_value.is_object() { js_value.dyn_into::().unwrap() @@ -163,11 +163,11 @@ pub fn update_event_props(node: Element, props: &JsValue) -> Element { .has_own_property(&callback_name.into()) { let callback = derive_from_js_value(props, callback_name); - Reflect::set(&element_event_props, &callback_name.into(), &callback); + Reflect::set(&element_event_props, &callback_name.into(), &callback).expect("TODO: panic set callback_name"); } } } - Reflect::set(&node, &ELEMENT_EVENT_PROPS_KEY.into(), &element_event_props); + Reflect::set(&node, &ELEMENT_EVENT_PROPS_KEY.into(), &element_event_props).expect("TODO: set ELEMENT_EVENT_PROPS_KEY"); node } diff --git a/packages/react-reconciler/src/begin_work.rs b/packages/react-reconciler/src/begin_work.rs index f865a2c..6560b28 100644 --- a/packages/react-reconciler/src/begin_work.rs +++ b/packages/react-reconciler/src/begin_work.rs @@ -52,7 +52,7 @@ fn update_host_root(work_in_progress: Rc>) -> Option>, pending_props: JsValue) -> Rc>, - should_track_effect: bool, + should_track_effects: bool, ) -> Rc> { - if should_track_effect && fiber.clone().borrow().alternate.is_none() { + if should_track_effects && fiber.clone().borrow().alternate.is_none() { let fiber = fiber.clone(); let mut fiber = fiber.borrow_mut(); fiber.flags |= Flags::Placement; @@ -33,31 +35,56 @@ fn place_single_child( fn delete_child( return_fiber: Rc>, child_to_delete: Rc>, - should_track_effect: bool, + should_track_effects: bool, ) { - if !should_track_effect { + if !should_track_effects { return; } - let deletions = { let return_fiber_borrowed = return_fiber.borrow(); return_fiber_borrowed.deletions.clone() }; - if deletions.is_none() { - return_fiber.borrow_mut().deletions = Some(vec![child_to_delete.clone()]); + if deletions.is_empty() { + return_fiber.borrow_mut().deletions = vec![child_to_delete.clone()]; return_fiber.borrow_mut().flags |= Flags::ChildDeletion; } else { - let mut del = return_fiber.borrow_mut().deletions.clone().unwrap(); + let mut del = &mut return_fiber.borrow_mut().deletions; del.push(child_to_delete.clone()); } } +fn delete_remaining_children( + return_fiber: Rc>, + current_first_child: Option>>, + should_track_effects: bool, +) { + if !should_track_effects { + return; + } + + let mut child_to_delete = current_first_child; + while child_to_delete.as_ref().is_some() { + delete_child( + return_fiber.clone(), + child_to_delete.clone().unwrap(), + should_track_effects, + ); + child_to_delete = child_to_delete + .clone() + .unwrap() + .clone() + .borrow() + .sibling + .clone(); + } +} + fn reconcile_single_element( return_fiber: Rc>, current_first_child: Option>>, element: Option, - should_track_effect: bool, + should_track_effects: bool, ) -> Rc> { if element.is_none() { panic!("reconcile_single_element, element is none") @@ -65,38 +92,42 @@ fn reconcile_single_element( let element = element.as_ref().unwrap(); let key = derive_from_js_value(&(*element).clone(), "key"); - if current_first_child.is_some() { - let current_first_child_cloned = current_first_child.clone().unwrap().clone(); + let mut current = current_first_child; + while current.is_some() { + let current_cloned = current.clone().unwrap().clone(); // Be careful, it is different with === // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness#same-value_equality_using_object.is - if Object::is(¤t_first_child_cloned.borrow().key, &key) { + if Object::is(¤t_cloned.borrow().key, &key) { if derive_from_js_value(&(*element).clone(), "$$typeof") != REACT_ELEMENT_TYPE { panic!("Undefined $$typeof"); } if Object::is( - ¤t_first_child_cloned.borrow()._type, + ¤t_cloned.borrow()._type, &derive_from_js_value(&(*element).clone(), "type"), ) { // type is the same, update props let existing = use_fiber( - current_first_child.clone().unwrap().clone(), + current_cloned.clone(), derive_from_js_value(&(*element).clone(), "props"), ); - existing.clone().borrow_mut()._return = Some(return_fiber); + existing.clone().borrow_mut()._return = Some(return_fiber.clone()); + delete_remaining_children( + return_fiber.clone(), + current.clone().unwrap().borrow().sibling.clone(), + should_track_effects, + ); return existing; } - delete_child( - return_fiber.clone(), - current_first_child.clone().unwrap().clone(), - should_track_effect, - ); + delete_remaining_children(return_fiber.clone(), current.clone(), should_track_effects); + break; } else { delete_child( return_fiber.clone(), - current_first_child.clone().unwrap().clone(), - should_track_effect, + current_cloned.clone(), + should_track_effects, ); + current = current_cloned.borrow().sibling.clone(); } } @@ -105,41 +136,205 @@ fn reconcile_single_element( Rc::new(RefCell::new(fiber)) } +fn create_props_with_content(content: JsValue) -> JsValue { + let props = Object::new(); + Reflect::set(&props, &JsValue::from("content"), &content).expect("props panic"); + props.into() +} + fn reconcile_single_text_node( return_fiber: Rc>, current_first_child: Option>>, content: Option, - should_track_effect: bool, + should_track_effects: bool, ) -> Rc> { - let props = Object::new(); - Reflect::set(&props, &JsValue::from("content"), &content.unwrap().clone()) - .expect("props panic"); + let props = create_props_with_content(content.unwrap()); + let mut current = current_first_child; + while current.is_some() { + let current_rc = current.clone().unwrap(); + if current_rc.borrow().tag == HostText { + let existing = use_fiber(current_rc.clone(), props.clone()); + existing.borrow_mut()._return = Some(return_fiber.clone()); + delete_remaining_children( + return_fiber.clone(), + current_rc.borrow().sibling.clone(), + should_track_effects, + ); + return existing; + } + delete_child( + return_fiber.clone(), + current_rc.clone(), + should_track_effects, + ); + current = current_rc.borrow().sibling.clone(); + } + + let mut created = FiberNode::new(WorkTag::HostText, props.clone(), JsValue::null()); + created._return = Some(return_fiber.clone()); + Rc::new(RefCell::new(created)) +} + +struct Key(JsValue); - if current_first_child.is_some() && current_first_child.as_ref().unwrap().borrow().tag == HostText { - let existing = use_fiber(current_first_child.as_ref().unwrap().clone(), (*props).clone()); - existing.borrow_mut()._return = Some(return_fiber.clone()); - return existing; +impl PartialEq for Key { + fn eq(&self, other: &Self) -> bool { + Object::is(&self.0, &other.0) } +} + +impl Eq for Key {} + +impl Hash for Key { + fn hash(&self, state: &mut H) { + if self.0.is_string() { + self.0.as_string().unwrap().hash(state) + } else if let Some(n) = self.0.as_f64() { + n.to_bits().hash(state) + } else if self.0.is_null() { + "null".hash(state) + } + } +} - if current_first_child.is_some() { - delete_child(return_fiber.clone(), current_first_child.clone().unwrap(), should_track_effect); +fn update_from_map( + return_fiber: Rc>, + existing_children: &mut HashMap>>, + index: u32, + element: &JsValue, + should_track_effects: bool, +) -> Rc> { + let key_to_use; + if type_of(element, "string") { + key_to_use = JsValue::from(index); + } else { + let key = derive_from_js_value(element, "key"); + key_to_use = match key.is_null() { + true => JsValue::from(index), + false => key.clone(), + } } + let before = existing_children.get(&Key(key_to_use.clone())).clone(); + if type_of(element, "string") { + let props = create_props_with_content(element.clone()); + if before.is_some() { + let before = (*before.clone().unwrap()).clone(); + existing_children.remove(&Key(key_to_use.clone())); + if before.borrow().tag == HostText { + return use_fiber(before.clone(), props.clone()); + } else { + delete_child(return_fiber, before, should_track_effects); + } + } + return Rc::new(RefCell::new(FiberNode::new( + WorkTag::HostText, + props.clone(), + JsValue::null(), + ))); + } else if type_of(element, "object") && !element.is_null() { + if derive_from_js_value(&(*element).clone(), "$$typeof") != REACT_ELEMENT_TYPE { + panic!("Undefined $$typeof"); + } + if before.is_some() { + let before = (*before.clone().unwrap()).clone(); + existing_children.remove(&Key(key_to_use.clone())); + if Object::is( + &before.borrow()._type, + &derive_from_js_value(&(*element).clone(), "type"), + ) { + return use_fiber(before.clone(), derive_from_js_value(element, "props")); + } else { + delete_child(return_fiber, before, should_track_effects); + } + } - let mut created = FiberNode::new(WorkTag::HostText, (*props).clone(), JsValue::null()); - created._return = Some(return_fiber.clone()); - Rc::new(RefCell::new(created)) + return Rc::new(RefCell::new(FiberNode::create_fiber_from_element(element))); + } + panic!("update_from_map unsupported"); } +fn reconcile_children_array( + return_fiber: Rc>, + current_first_child: Option>>, + new_child: &Array, + should_track_effects: bool, +) -> Option>> { + // 遍历到的最后一个可复用fiber在before中的index + let mut last_placed_index = 0; + // 创建的最后一个fiber + let mut last_new_fiber: Option>> = None; + // 创建的第一个fiber + let mut first_new_fiber: Option>> = None; + + let mut existing_children: HashMap>> = HashMap::new(); + let mut current = current_first_child; + while current.is_some() { + let current_rc = current.unwrap(); + let key_to_use = match current_rc.clone().borrow().key.is_null() { + true => JsValue::from(current_rc.borrow().index), + false => current_rc.borrow().key.clone(), + }; + existing_children.insert(Key(key_to_use), current_rc.clone()); + current = current_rc.borrow().sibling.clone(); + } + + let length = new_child.length(); + for i in 0..length { + let after = new_child.get(i); + let new_fiber = update_from_map( + return_fiber.clone(), + &mut existing_children, + i, + &after, + should_track_effects, + ); + { + new_fiber.borrow_mut().index = i; + new_fiber.borrow_mut()._return = Some(return_fiber.clone()); + } + + if last_new_fiber.is_none() { + last_new_fiber = Some(new_fiber.clone()); + first_new_fiber = Some(new_fiber.clone()); + } else { + last_new_fiber.clone().unwrap().clone().borrow_mut().sibling = Some(new_fiber.clone()); + last_new_fiber = Some(new_fiber.clone()); + } + + if !should_track_effects { + continue; + } + + let current = { new_fiber.borrow().alternate.clone() }; + if current.is_some() { + let old_index = current.clone().unwrap().borrow().index; + if old_index < last_placed_index { + new_fiber.borrow_mut().flags |= Flags::Placement; + continue; + } else { + last_placed_index = old_index; + } + } else { + new_fiber.borrow_mut().flags |= Flags::Placement; + } + } + + for (_, fiber) in existing_children { + delete_child(return_fiber.clone(), fiber, should_track_effects); + } + + first_new_fiber +} fn _reconcile_child_fibers( return_fiber: Rc>, current_first_child: Option>>, new_child: Option, - should_track_effect: bool, + should_track_effects: bool, ) -> Option>> { if new_child.is_some() { - let new_child = &new_child.unwrap(); + let new_child: &JsValue = &new_child.unwrap(); if type_of(new_child, "string") || type_of(new_child, "number") { return Some(place_single_child( @@ -147,10 +342,17 @@ fn _reconcile_child_fibers( return_fiber, current_first_child, Some(new_child.clone()), - should_track_effect, + should_track_effects, ), - should_track_effect, + should_track_effects, )); + } else if new_child.is_array() { + return reconcile_children_array( + return_fiber, + current_first_child, + new_child.dyn_ref::().unwrap(), + should_track_effects, + ); } else if new_child.is_object() { if let Some(_typeof) = derive_from_js_value(&new_child, "$$typeof").as_string() { if _typeof == REACT_ELEMENT_TYPE { @@ -159,9 +361,9 @@ fn _reconcile_child_fibers( return_fiber, current_first_child, Some(new_child.clone()), - should_track_effect, + should_track_effects, ), - should_track_effect, + should_track_effects, )); } } diff --git a/packages/react-reconciler/src/commit_work.rs b/packages/react-reconciler/src/commit_work.rs index 6f10bc8..53909dc 100644 --- a/packages/react-reconciler/src/commit_work.rs +++ b/packages/react-reconciler/src/commit_work.rs @@ -8,6 +8,7 @@ use crate::fiber::{FiberNode, StateNode}; use crate::fiber_flags::{Flags, get_mutation_mask}; use crate::HostConfig; use crate::work_tags::WorkTag; +use crate::work_tags::WorkTag::{HostComponent, HostRoot, HostText}; pub struct CommitWork { next_effect: Option>>, @@ -66,26 +67,28 @@ impl CommitWork { } fn commit_mutation_effects_on_fiber(&self, finished_work: Rc>) { - let flags = finished_work.clone().borrow().flags.clone(); + let flags = finished_work.borrow().flags.clone(); if flags.contains(Flags::Placement) { self.commit_placement(finished_work.clone()); - finished_work.clone().borrow_mut().flags -= Flags::Placement; + finished_work.borrow_mut().flags -= Flags::Placement; } if flags.contains(Flags::ChildDeletion) { - let deletions = finished_work.clone().borrow().deletions.clone(); - if deletions.is_some() { - let deletions = deletions.unwrap(); - for child_to_delete in deletions { - self.commit_deletion(child_to_delete); + { + let deletions = &finished_work.borrow().deletions; + if !deletions.is_empty() { + for child_to_delete in deletions { + self.commit_deletion(child_to_delete.clone()); + } } } - finished_work.clone().borrow_mut().flags -= Flags::ChildDeletion; + + finished_work.borrow_mut().flags -= Flags::ChildDeletion; } if flags.contains(Flags::Update) { self.commit_update(finished_work.clone()); - finished_work.clone().borrow_mut().flags -= Flags::Update; + finished_work.borrow_mut().flags -= Flags::Update; } } @@ -105,7 +108,8 @@ impl CommitWork { } fn commit_deletion(&self, child_to_delete: Rc>) { - let first_host_fiber: Rc>>>> = Rc::new(RefCell::new(None)); + let first_host_fiber: Rc>>>> = + Rc::new(RefCell::new(None)); self.commit_nested_unmounts(child_to_delete.clone(), |unmount_fiber| { let cloned = first_host_fiber.clone(); match unmount_fiber.borrow().tag { @@ -141,7 +145,6 @@ impl CommitWork { child_to_delete.clone().borrow_mut().child = None; } - fn commit_nested_unmounts(&self, root: Rc>, on_commit_unmount: F) where F: Fn(Rc>), @@ -175,6 +178,8 @@ impl CommitWork { node = node.clone().borrow()._return.clone().unwrap(); } + let node_cloned = node.clone(); + let _return = { node_cloned.borrow()._return.clone() }; node_cloned .borrow_mut() .sibling @@ -182,7 +187,7 @@ impl CommitWork { .unwrap() .clone() .borrow_mut() - ._return = node_cloned.borrow()._return.clone(); + ._return = _return; node = node_cloned.borrow().sibling.clone().unwrap(); } } @@ -193,11 +198,13 @@ impl CommitWork { return; } let parent_state_node = FiberNode::derive_state_node(host_parent.unwrap()); + let sibling = self.get_host_sibling(finished_work.clone()); if parent_state_node.is_some() { - self.append_placement_node_into_container( + self.insert_or_append_placement_node_into_container( finished_work.clone(), parent_state_node.unwrap(), + sibling, ); } } @@ -209,28 +216,46 @@ impl CommitWork { } } - fn append_placement_node_into_container( + fn insert_or_append_placement_node_into_container( &self, fiber: Rc>, parent: Rc, + before: Option>, ) { let fiber = fiber.clone(); let tag = fiber.borrow().tag.clone(); if tag == WorkTag::HostComponent || tag == WorkTag::HostText { let state_node = fiber.clone().borrow().state_node.clone().unwrap(); - self.host_config.append_child_to_container( - self.get_element_from_state_node(state_node), - parent.clone(), - ); + let state_node = self.get_element_from_state_node(state_node); + + if before.is_some() { + self.host_config.insert_child_to_container( + state_node, + parent, + before.clone().unwrap(), + ); + } else { + self.host_config + .append_child_to_container(state_node, parent.clone()); + } + return; } let child = fiber.borrow().child.clone(); if child.is_some() { - self.append_placement_node_into_container(child.clone().unwrap(), parent.clone()); + self.insert_or_append_placement_node_into_container( + child.clone().unwrap(), + parent.clone(), + before.clone(), + ); let mut sibling = child.unwrap().clone().borrow().sibling.clone(); while sibling.is_some() { - self.append_placement_node_into_container(sibling.clone().unwrap(), parent.clone()); + self.insert_or_append_placement_node_into_container( + sibling.clone().unwrap(), + parent.clone(), + before.clone(), + ); sibling = sibling.clone().unwrap().clone().borrow().sibling.clone(); } } @@ -249,4 +274,67 @@ impl CommitWork { None } + + /** + * 难点在于目标fiber的hostSibling可能并不是他的同级sibling + * 比如: 其中:function B() {return
    } 所以A的hostSibling实际是B的child + * 实际情况层级可能更深 + * 同时:一个fiber被标记Placement,那他就是不稳定的(他对应的DOM在本次commit阶段会移动),也不能作为hostSibling + */ + fn get_host_sibling(&self, fiber: Rc>) -> Option> { + let mut node = Some(fiber); + 'find_sibling: loop { + let node_rc = node.clone().unwrap(); + while node_rc.borrow().sibling.is_none() { + let parent = node_rc.borrow()._return.clone(); + let tag = parent.clone().unwrap().borrow().tag.clone(); + if parent.is_none() || tag == HostComponent || tag == HostRoot { + return None; + } + node = parent.clone(); + } + + let node_rc = node.clone().unwrap(); + let _return = { node_rc.borrow()._return.clone() }; + node_rc + .borrow_mut() + .sibling + .clone() + .unwrap() + .borrow_mut() + ._return = _return; + node = node_rc.borrow().sibling.clone(); + + let node_rc = node.clone().unwrap(); + let tag = node_rc.borrow().tag.clone(); + while tag != HostText && tag != HostComponent { + if node_rc.borrow().flags.contains(Flags::Placement) { + continue 'find_sibling; + } + if node_rc.borrow().child.is_none() { + continue 'find_sibling; + } else { + node_rc + .borrow_mut() + .child + .clone() + .unwrap() + .borrow_mut() + ._return = node.clone(); + node = node_rc.borrow().child.clone(); + } + } + if !node + .clone() + .unwrap() + .borrow() + .flags + .contains(Flags::Placement) + { + return Some(self.get_element_from_state_node( + node.clone().unwrap().borrow().state_node.clone().unwrap(), + )); + } + } + } } diff --git a/packages/react-reconciler/src/complete_work.rs b/packages/react-reconciler/src/complete_work.rs index 6890113..7d91888 100644 --- a/packages/react-reconciler/src/complete_work.rs +++ b/packages/react-reconciler/src/complete_work.rs @@ -157,15 +157,17 @@ impl CompleteWork { } WorkTag::HostText => { if current.is_some() && work_in_progress_cloned.borrow().state_node.is_some() { - let old_text = derive_from_js_value(¤t.clone().unwrap().clone().borrow().memoized_props, "content"); + let old_text = derive_from_js_value( + ¤t.clone().unwrap().clone().borrow().memoized_props, + "content", + ); let new_test = derive_from_js_value(&new_props, "content"); if !Object::is(&old_text, &new_test) { CompleteWork::mark_update(work_in_progress.clone()); } } else { let text_instance = self.host_config.create_text_instance( - &Reflect::get(&new_props, &JsValue::from_str("content")) - .unwrap() + &Reflect::get(&new_props, &JsValue::from_str("content")).unwrap(), ); work_in_progress.clone().borrow_mut().state_node = Some(Rc::new(StateNode::Element(text_instance.clone()))); diff --git a/packages/react-reconciler/src/fiber.rs b/packages/react-reconciler/src/fiber.rs index 5fc8db2..992956c 100644 --- a/packages/react-reconciler/src/fiber.rs +++ b/packages/react-reconciler/src/fiber.rs @@ -8,7 +8,7 @@ use std::rc::Rc; use wasm_bindgen::JsValue; use web_sys::js_sys::Reflect; -use shared::derive_from_js_value; +use shared::{derive_from_js_value, log, type_of}; use crate::fiber_flags::Flags; use crate::fiber_hooks::Hook; @@ -22,21 +22,20 @@ pub enum StateNode { } #[derive(Debug, Clone)] -pub(crate) enum MemoizedState { - JsValue(JsValue), +pub enum MemoizedState { + MemoizedJsValue(JsValue), Hook(Rc>), } impl MemoizedState { pub fn js_value(&self) -> Option { match self { - MemoizedState::JsValue(js_value) => Some(js_value.clone()), - MemoizedState::Hook(_) => None + MemoizedState::MemoizedJsValue(js_value) => Some(js_value.clone()), + MemoizedState::Hook(_) => None, } } } -#[derive(Debug)] pub struct FiberNode { pub index: u32, pub tag: WorkTag, @@ -53,7 +52,58 @@ pub struct FiberNode { pub subtree_flags: Flags, pub memoized_props: JsValue, pub memoized_state: Option, - pub deletions: Option>>>, + pub deletions: Vec>>, +} + +impl Debug for FiberNode { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Ok(match self.tag { + WorkTag::FunctionComponent => { + write!( + f, + "{:?}(flags:{:?}, subtreeFlags:{:?})", + self._type.as_ref(), + self.flags, + self.subtree_flags + ) + .expect("print error"); + } + WorkTag::HostRoot => { + write!( + f, + "{:?}(subtreeFlags:{:?})", + WorkTag::HostRoot, + self.subtree_flags + ) + .expect("print error"); + } + WorkTag::HostComponent => { + write!( + f, + "{:?}(key:{:?}, flags:{:?}, subtreeFlags:{:?})", + self._type, + self.key, + self.flags, + self.subtree_flags + ) + .expect("print error"); + } + WorkTag::HostText => { + write!( + f, + "{:?}(state_node:{:?}, flags:{:?})", + self.tag, + Reflect::get( + self.pending_props.as_ref(), + &JsValue::from_str("content"), + ) + .unwrap(), + self.flags + ) + .expect("print error"); + } + }) + } } impl FiberNode { @@ -74,7 +124,7 @@ impl FiberNode { memoized_state: None, flags: Flags::NoFlags, subtree_flags: Flags::NoFlags, - deletions: None, + deletions: vec![], } } @@ -86,9 +136,10 @@ impl FiberNode { let mut fiber_tag = WorkTag::FunctionComponent; if _type.is_string() { fiber_tag = WorkTag::HostComponent + } else if !type_of(&_type, "function") { + log!("Unsupported type {:?}", ele); } - let mut fiber = FiberNode::new(fiber_tag, props, key); fiber._type = _type; fiber @@ -117,7 +168,7 @@ impl FiberNode { }; return if w.is_none() { - let mut wip = { + let wip = { let c = c_rc.borrow(); let mut wip = FiberNode::new(c.tag.clone(), pending_props, c.key.clone()); wip._type = c._type.clone(); @@ -146,7 +197,7 @@ impl FiberNode { wip.pending_props = pending_props; wip.flags = Flags::NoFlags; wip.subtree_flags = Flags::NoFlags; - wip.deletions = None; + wip.deletions = vec![]; wip._type = c._type.clone(); wip.update_queue = c.update_queue.clone(); @@ -188,87 +239,47 @@ impl FiberRootNode { } } +struct QueueItem { + depth: u32, + node: Rc>, +} + +impl QueueItem { + fn new(node: Rc>, depth: u32) -> Self { + Self { + node, + depth, + } + } +} + impl Debug for FiberRootNode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let root = self.current.clone().borrow().alternate.clone(); Ok(if let Some(node) = root { let mut queue = VecDeque::new(); - queue.push_back(Rc::clone(&node)); + queue.push_back(QueueItem::new(Rc::clone(&node), 0)); - while let Some(current) = queue.pop_front() { + while let Some(QueueItem { node: current, depth }) = queue.pop_front() { let current_ref = current.borrow(); - match current_ref.tag { - WorkTag::FunctionComponent => { - let current_borrowed = current.borrow(); - write!( - f, - "{:?}(flags:{:?}, subtreeFlags:{:?})", - current_borrowed._type.as_ref(), - current_borrowed.flags, - current_borrowed.subtree_flags - ) - .expect("print error"); - } - WorkTag::HostRoot => { - write!( - f, - "{:?}(subtreeFlags:{:?})", - WorkTag::HostRoot, - current_ref.subtree_flags - ) - .expect("print error"); - } - WorkTag::HostComponent => { - let current_borrowed = current.borrow(); - write!( - f, - "{:?}(flags:{:?}, subtreeFlags:{:?})", - current_borrowed - ._type - .as_ref().as_string().unwrap(), - current_borrowed.flags, - current_borrowed.subtree_flags - ) - .expect("print error"); - } - WorkTag::HostText => { - let current_borrowed = current.borrow(); + write!(f, "{:?}", current_ref); - write!( - f, - "{:?}(state_node:{:?}, flags:{:?})", - current_borrowed.tag, - Reflect::get( - current_borrowed.pending_props.as_ref(), - &JsValue::from_str("content"), - ) - .unwrap(), - current_borrowed.flags - ) - .expect("print error"); - } - }; if let Some(ref child) = current_ref.child { - queue.push_back(Rc::clone(child)); + queue.push_back(QueueItem::new(Rc::clone(child), depth + 1)); let mut sibling = child.clone().borrow().sibling.clone(); while sibling.is_some() { - queue.push_back(Rc::clone(sibling.as_ref().unwrap())); + queue.push_back(QueueItem::new(Rc::clone(sibling.as_ref().unwrap()), depth + 1)); sibling = sibling.as_ref().unwrap().clone().borrow().sibling.clone(); } } - if let Some(next) = queue.front() { - let next_ref = next.borrow(); - if let (Some(current_parent), Some(next_parent)) = - (current_ref._return.as_ref(), next_ref._return.as_ref()) - { - if !Rc::ptr_eq(current_parent, next_parent) { - writeln!(f, "").expect("print error"); - writeln!(f, "------------------------------------") - .expect("print error"); - continue; - } + if let Some(QueueItem { node: next, depth: next_depth }) = queue.front() { + if *next_depth != depth { + writeln!(f, "").expect("print error"); + writeln!(f, "------------------------------------") + .expect("print error"); + continue; } if current_ref._return.is_some() { diff --git a/packages/react-reconciler/src/fiber_hooks.rs b/packages/react-reconciler/src/fiber_hooks.rs index 39eb62e..2929b24 100644 --- a/packages/react-reconciler/src/fiber_hooks.rs +++ b/packages/react-reconciler/src/fiber_hooks.rs @@ -141,29 +141,23 @@ fn update_work_in_progress_hook() -> Option>> { match current { None => None, - Some(current) => { - match current.clone().borrow().memoized_state.clone() { - Some(MemoizedState::Hook(memoized_state)) => Some(memoized_state.clone()), - _ => None, - } - } + Some(current) => match current.clone().borrow().memoized_state.clone() { + Some(MemoizedState::Hook(memoized_state)) => Some(memoized_state.clone()), + _ => None, + }, } } Some(current_hook) => current_hook.clone().borrow().next.clone(), }; next_work_in_progress_hook = match &WORK_IN_PROGRESS_HOOK { - None => { - match CURRENTLY_RENDERING_FIBER.clone() { - Some(current) => { - match current.clone().borrow().memoized_state.clone() { - Some(MemoizedState::Hook(memoized_state)) => Some(memoized_state.clone()), - _ => None, - } - } + None => match CURRENTLY_RENDERING_FIBER.clone() { + Some(current) => match current.clone().borrow().memoized_state.clone() { + Some(MemoizedState::Hook(memoized_state)) => Some(memoized_state.clone()), _ => None, - } - } + }, + _ => None, + }, Some(work_in_progress_hook) => work_in_progress_hook.clone().borrow().next.clone(), }; @@ -223,7 +217,7 @@ fn mount_state(initial_state: &JsValue) -> Result, JsValue> { memoized_state = initial_state.clone(); } hook.as_ref().unwrap().clone().borrow_mut().memoized_state = - Some(MemoizedState::JsValue(memoized_state.clone())); + Some(MemoizedState::MemoizedJsValue(memoized_state.clone())); unsafe { if CURRENTLY_RENDERING_FIBER.is_none() { @@ -234,15 +228,9 @@ fn mount_state(initial_state: &JsValue) -> Result, JsValue> { hook.as_ref().unwrap().clone().borrow_mut().update_queue = Some(queue.clone()); let q_rc = Rc::new(queue.clone()); let q_rc_cloned = q_rc.clone(); - let fiber = unsafe { - CURRENTLY_RENDERING_FIBER.clone().unwrap() - }; + let fiber = unsafe { CURRENTLY_RENDERING_FIBER.clone().unwrap() }; let closure = Closure::wrap(Box::new(move |action: &JsValue| { - dispatch_set_state( - fiber.clone(), - (*q_rc_cloned).clone(), - action, - ) + dispatch_set_state(fiber.clone(), (*q_rc_cloned).clone(), action) }) as Box); let function = closure.as_ref().unchecked_ref::().clone(); closure.forget(); @@ -252,7 +240,7 @@ fn mount_state(initial_state: &JsValue) -> Result, JsValue> { Ok(vec![memoized_state, function.into()]) } -fn update_state(initial_state: &JsValue) -> Result, JsValue> { +fn update_state(_: &JsValue) -> Result, JsValue> { let hook = update_work_in_progress_hook(); if hook.is_none() { @@ -274,13 +262,16 @@ fn update_state(initial_state: &JsValue) -> Result, JsValue> { log!("memoized_state {:?}", hook_cloned.borrow().memoized_state); Ok(vec![ - hook.clone().unwrap().clone() + hook.clone() + .unwrap() + .clone() .borrow() .memoized_state .clone() .unwrap() .js_value() - .unwrap().clone(), + .unwrap() + .clone(), queue.clone().unwrap().borrow().dispatch.clone().into(), ]) } diff --git a/packages/react-reconciler/src/lib.rs b/packages/react-reconciler/src/lib.rs index 7cc9ec5..3a7cb69 100644 --- a/packages/react-reconciler/src/lib.rs +++ b/packages/react-reconciler/src/lib.rs @@ -28,6 +28,13 @@ pub trait HostConfig { fn append_child_to_container(&self, child: Rc, parent: Rc); fn remove_child(&self, child: Rc, container: Rc); fn commit_text_update(&self, text_instance: Rc, content: String); + + fn insert_child_to_container( + &self, + child: Rc, + container: Rc, + before: Rc, + ); } pub struct Reconciler { @@ -39,7 +46,11 @@ impl Reconciler { Reconciler { host_config } } pub fn create_container(&self, container: Rc) -> Rc> { - let host_root_fiber = Rc::new(RefCell::new(FiberNode::new(WorkTag::HostRoot, JsValue::null(), JsValue::null()))); + let host_root_fiber = Rc::new(RefCell::new(FiberNode::new( + WorkTag::HostRoot, + JsValue::null(), + JsValue::null(), + ))); host_root_fiber.clone().borrow_mut().update_queue = Some(create_update_queue()); let root = Rc::new(RefCell::new(FiberRootNode::new( container.clone(), diff --git a/packages/react-reconciler/src/update_queue.rs b/packages/react-reconciler/src/update_queue.rs index 4279db7..e80a856 100644 --- a/packages/react-reconciler/src/update_queue.rs +++ b/packages/react-reconciler/src/update_queue.rs @@ -60,10 +60,12 @@ pub fn process_update_queue( Some(action) => { let f = action.dyn_ref::(); base_state = match f { - None => Some(MemoizedState::JsValue(action.clone())), + None => Some(MemoizedState::MemoizedJsValue(action.clone())), Some(f) => { - if let MemoizedState::JsValue(base_state) = base_state.as_ref().unwrap() { - Some(MemoizedState::JsValue( + if let MemoizedState::MemoizedJsValue(base_state) = + base_state.as_ref().unwrap() + { + Some(MemoizedState::MemoizedJsValue( f.call1(&JsValue::null(), base_state).unwrap(), )) } else { diff --git a/packages/react-reconciler/src/work_loop.rs b/packages/react-reconciler/src/work_loop.rs index f25edd1..050a6ea 100644 --- a/packages/react-reconciler/src/work_loop.rs +++ b/packages/react-reconciler/src/work_loop.rs @@ -166,9 +166,7 @@ impl WorkLoop { fn perform_unit_of_work(&self, fiber: Rc>) -> Result<(), JsValue> { let next = begin_work(fiber.clone())?; - let pending_props = { - fiber.clone().borrow().pending_props.clone() - }; + let pending_props = { fiber.clone().borrow().pending_props.clone() }; fiber.clone().borrow_mut().memoized_props = pending_props; if next.is_none() { self.complete_unit_of_work(fiber.clone()); @@ -199,7 +197,7 @@ impl WorkLoop { if sibling.is_some() { // self.work_in_progress = next.clone(); unsafe { - WORK_IN_PROGRESS = next.clone(); + WORK_IN_PROGRESS = sibling.clone(); } return; } @@ -207,7 +205,7 @@ impl WorkLoop { let _return = node.clone().unwrap().clone().borrow()._return.clone(); if _return.is_none() { - node = None; + // node = None; // self.work_in_progress = None; unsafe { WORK_IN_PROGRESS = None; diff --git a/packages/react/src/lib.rs b/packages/react/src/lib.rs index f23b90b..a79185b 100644 --- a/packages/react/src/lib.rs +++ b/packages/react/src/lib.rs @@ -1,7 +1,7 @@ -use js_sys::{JSON, Object, Reflect}; +use js_sys::{Array, JSON, Object, Reflect}; use wasm_bindgen::prelude::*; -use shared::REACT_ELEMENT_TYPE; +use shared::{derive_from_js_value, REACT_ELEMENT_TYPE}; use crate::current_dispatcher::CURRENT_DISPATCHER; @@ -68,13 +68,34 @@ pub fn jsx_dev(_type: &JsValue, config: &JsValue, key: &JsValue) -> JsValue { Reflect::set(&react_element, &"ref".into(), &_ref).expect("ref panic"); Reflect::set(&react_element, &"key".into(), &key).expect("key panic"); - react_element.into() } -#[wasm_bindgen(js_name = createElement)] -pub fn create_element(_type: &JsValue, config: &JsValue, key: &JsValue) -> JsValue { - jsx_dev(_type, config, key) +#[wasm_bindgen(js_name = createElement, variadic)] +pub fn create_element(_type: &JsValue, config: &JsValue, maybe_children: &JsValue) -> JsValue { + jsx(_type, config, maybe_children) +} + +#[wasm_bindgen(variadic)] +pub fn jsx(_type: &JsValue, config: &JsValue, maybe_children: &JsValue) -> JsValue { + let length = derive_from_js_value(maybe_children, "length"); + let obj = Object::new(); + let config = if config.is_object() { config } else { &*obj }; + match length.as_f64() { + None => {} + Some(length) => { + if length != 0.0 { + if length == 1.0 { + let children = maybe_children.dyn_ref::().unwrap(); + Reflect::set(&config, &"children".into(), &children.get(0)) + .expect("TODO: panic children"); + } else { + Reflect::set(&config, &"children".into(), maybe_children).expect("TODO: panic set children"); + } + } + } + }; + jsx_dev(_type, config, &JsValue::undefined()) } #[wasm_bindgen(js_name = isValidElement)] diff --git a/packages/shared/src/lib.rs b/packages/shared/src/lib.rs index 42acc61..190de33 100644 --- a/packages/shared/src/lib.rs +++ b/packages/shared/src/lib.rs @@ -24,7 +24,6 @@ pub fn is_dev() -> bool { env!("ENV") == "dev" } - pub fn type_of(val: &JsValue, _type: &str) -> bool { let t = if val.is_undefined() { "undefined".to_string() @@ -44,4 +43,4 @@ pub fn type_of(val: &JsValue, _type: &str) -> bool { "unknown".to_string() }; t == _type -} \ No newline at end of file +} diff --git a/readme.md b/readme.md index 69e1496..c6ff4d1 100644 --- a/readme.md +++ b/readme.md @@ -27,3 +27,5 @@ [从零实现 React v18,但 WASM 版 - [9] 使用 Jest 进行单元测试](https://www.paradeto.com/2024/04/23/big-react-wasm-9/) [从零实现 React v18,但 WASM 版 - [10] 实现单节点更新流程](https://www.paradeto.com/2024/04/26/big-react-wasm-10/) + +[从零实现 React v18,但 WASM 版 - [11] 实现事件系统](https://www.paradeto.com/2024/04/30/big-react-wasm-11/)