From 03d4c6aff89c7872425bf521e36de73d89f48996 Mon Sep 17 00:00:00 2001 From: youxingzhi Date: Mon, 6 May 2024 14:23:30 +0800 Subject: [PATCH 1/8] blog-12: add jsx for react --- .../react-dom/ReactFunctionComponent-test.js | 325 ++++++++++++++++++ package.json | 2 +- packages/react-dom/src/host_config.rs | 4 +- packages/react-dom/src/synthetic_event.rs | 2 +- packages/react/src/lib.rs | 38 +- 5 files changed, 361 insertions(+), 10 deletions(-) create mode 100644 __tests__/react-dom/ReactFunctionComponent-test.js diff --git a/__tests__/react-dom/ReactFunctionComponent-test.js b/__tests__/react-dom/ReactFunctionComponent-test.js new file mode 100644 index 0000000..494dd18 --- /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.only('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/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..ed2fa97 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; @@ -45,7 +45,7 @@ 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(), ); diff --git a/packages/react-dom/src/synthetic_event.rs b/packages/react-dom/src/synthetic_event.rs index 1803bef..79ef9c5 100644 --- a/packages/react-dom/src/synthetic_event.rs +++ b/packages/react-dom/src/synthetic_event.rs @@ -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() diff --git a/packages/react/src/lib.rs b/packages/react/src/lib.rs index f23b90b..8a2ce34 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, log, REACT_ELEMENT_TYPE}; use crate::current_dispatcher::CURRENT_DISPATCHER; @@ -68,15 +68,41 @@ 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"); - + log!("react_element2 {:?}", react_element); 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(); + log!("children {:?}", children.get(0)); + + Reflect::set(&config, &"children".into(), &children.get(0)).expect("TODO: panic children"); + } else { + Reflect::set(&config, &"children".into(), maybe_children); + } + } + } + }; + log!("react_element1 config {:?}", config); + + jsx_dev(_type, config, &JsValue::undefined()) +} + + #[wasm_bindgen(js_name = isValidElement)] pub fn is_valid_element(object: &JsValue) -> bool { object.is_object() From 2219b3883a6b9553816a15d83e0ce1e9baf10b6a Mon Sep 17 00:00:00 2001 From: youxingzhi Date: Mon, 6 May 2024 14:44:51 +0800 Subject: [PATCH 2/8] blog-12: add jsx for react --- packages/react/src/lib.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/react/src/lib.rs b/packages/react/src/lib.rs index 8a2ce34..c01ab54 100644 --- a/packages/react/src/lib.rs +++ b/packages/react/src/lib.rs @@ -1,7 +1,7 @@ use js_sys::{Array, JSON, Object, Reflect}; use wasm_bindgen::prelude::*; -use shared::{derive_from_js_value, log, REACT_ELEMENT_TYPE}; +use shared::{derive_from_js_value, REACT_ELEMENT_TYPE}; use crate::current_dispatcher::CURRENT_DISPATCHER; @@ -68,7 +68,6 @@ 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"); - log!("react_element2 {:?}", react_element); react_element.into() } @@ -88,8 +87,6 @@ pub fn jsx(_type: &JsValue, config: &JsValue, maybe_children: &JsValue) -> JsVal if length != 0.0 { if length == 1.0 { let children = maybe_children.dyn_ref::().unwrap(); - log!("children {:?}", children.get(0)); - Reflect::set(&config, &"children".into(), &children.get(0)).expect("TODO: panic children"); } else { Reflect::set(&config, &"children".into(), maybe_children); @@ -97,8 +94,6 @@ pub fn jsx(_type: &JsValue, config: &JsValue, maybe_children: &JsValue) -> JsVal } } }; - log!("react_element1 config {:?}", config); - jsx_dev(_type, config, &JsValue::undefined()) } From b29d7cbc32213c111d823a333a3ef7a1cd7534a8 Mon Sep 17 00:00:00 2001 From: youxingzhi Date: Mon, 6 May 2024 20:39:56 +0800 Subject: [PATCH 3/8] blog-12: reconcile array --- .../react-dom/ReactFunctionComponent-test.js | 2 +- __tests__/react/ReactElement-test.js | 524 +++++++++--------- examples/hello-world/src/App.tsx | 2 +- packages/react-dom/src/host_config.rs | 43 +- packages/react-dom/src/lib.rs | 2 +- packages/react-dom/src/renderer.rs | 14 +- packages/react-dom/src/synthetic_event.rs | 14 +- packages/react-reconciler/src/begin_work.rs | 2 +- packages/react-reconciler/src/child_fiber.rs | 270 +++++++-- packages/react-reconciler/src/commit_work.rs | 115 +++- .../react-reconciler/src/complete_work.rs | 8 +- packages/react-reconciler/src/fiber.rs | 19 +- packages/react-reconciler/src/fiber_hooks.rs | 47 +- packages/react-reconciler/src/lib.rs | 13 +- packages/react-reconciler/src/update_queue.rs | 8 +- packages/react-reconciler/src/work_loop.rs | 8 +- packages/react/src/lib.rs | 6 +- packages/shared/src/lib.rs | 3 +- 18 files changed, 707 insertions(+), 393 deletions(-) diff --git a/__tests__/react-dom/ReactFunctionComponent-test.js b/__tests__/react-dom/ReactFunctionComponent-test.js index 494dd18..055f36c 100644 --- a/__tests__/react-dom/ReactFunctionComponent-test.js +++ b/__tests__/react-dom/ReactFunctionComponent-test.js @@ -25,7 +25,7 @@ describe('ReactFunctionComponent', () => { ReactTestUtils = require('../utils/test-utils') }); - it.only('should render stateless component', () => { + it('should render stateless component', () => { const el = document.createElement('div'); // console.log(FunctionComponent.toString()) ReactDOM.createRoot(el).render(); 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..b1112ea 100644 --- a/examples/hello-world/src/App.tsx +++ b/examples/hello-world/src/App.tsx @@ -9,7 +9,7 @@ function App() { return (

{ - e.stopPropagation() + // e.stopPropagation() console.log('click h3', e.currentTarget) updateNum(prev => prev + 1); }} diff --git a/packages/react-dom/src/host_config.rs b/packages/react-dom/src/host_config.rs index ed2fa97..b3cdaae 100644 --- a/packages/react-dom/src/host_config.rs +++ b/packages/react-dom/src/host_config.rs @@ -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 { @@ -51,7 +51,9 @@ impl HostConfig for ReactDomHostConfig { ); Rc::new(Node::from(element)) } - Err(_) => todo!(), + Err(_) => { + panic!("Failed to create_instance {:?}", _type); + } } } @@ -59,10 +61,10 @@ impl HostConfig for ReactDomHostConfig { let p = parent.clone().downcast::().unwrap(); let c = child.clone().downcast::().unwrap(); match p.append_child(&c) { - Ok(_) => { - log!("append_initial_child successfully {:?} {:?}", p, c); + Ok(_) => {} + Err(_) => { + log!("Failed to append_initial_child {:?} {:?}", p, c); } - Err(_) => todo!(), } } @@ -74,10 +76,10 @@ impl HostConfig for ReactDomHostConfig { let p = container.clone().downcast::().unwrap(); let c = child.clone().downcast::().unwrap(); match p.remove_child(&c) { - Ok(_) => { - log!("remove_child successfully {:?} {:?}", p, c); + Ok(_) => {} + Err(e) => { + log!("Failed to remove_child {:?} {:?} {:?} ", e, p, c); } - Err(_) => todo!(), } } @@ -85,4 +87,25 @@ 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(&before, Some(&child)) { + Ok(_) => {} + 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 79ef9c5..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() @@ -163,11 +163,11 @@ pub fn update_fiber_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,13 +34,12 @@ 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() @@ -53,11 +53,37 @@ fn delete_child( } } +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 +91,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(), + 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 +135,192 @@ 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"); - - 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; + 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()); + return existing; + } + delete_child( + return_fiber.clone(), + current_rc.clone(), + should_track_effects, + ); + current = current_rc.borrow().sibling.clone(); } - if current_first_child.is_some() { - delete_child(return_fiber.clone(), current_first_child.clone().unwrap(), should_track_effect); + let mut created = FiberNode::new(WorkTag::HostText, props.clone(), JsValue::null()); + created._return = Some(return_fiber.clone()); + Rc::new(RefCell::new(created)) +} + +fn update_from_map( + return_fiber: Rc>, + mut existing_children: HashMap>>, + index: u32, + element: &JsValue, + should_track_effects: bool, +) -> Rc> { + let key_to_use; + if type_of(element, "string") { + key_to_use = index.to_string(); + } else { + let key = derive_from_js_value(element, "key"); + key_to_use = match key.is_null() { + true => index.to_string(), + false => match key.as_string() { + None => { + log!( + "update_from_map, key is not string {:?}", + derive_from_js_value(element, "key") + ); + "".to_string() + } + Some(k) => k, + }, + } } + let before = existing_children.get(&key_to_use).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_to_use); + 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"); + } - let mut created = FiberNode::new(WorkTag::HostText, (*props).clone(), JsValue::null()); - created._return = Some(return_fiber.clone()); - Rc::new(RefCell::new(created)) + if before.is_some() { + let before = (*before.clone().unwrap()).clone(); + existing_children.remove(&key_to_use); + 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); + } + } + + 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 => current_rc.borrow().index.to_string(), + false => current_rc + .borrow() + .key + .as_string() + .expect("key is not string"), + }; + existing_children.insert(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(), + existing_children.clone(), + 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 +328,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 +347,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..bddfb0c 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>>, @@ -105,7 +106,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 +143,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 +176,10 @@ 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,12 @@ 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(), + None, ); } } @@ -209,28 +215,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 +273,71 @@ 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 _return = { + node_rc.borrow()._return.clone() + }; + node.clone() + .unwrap() + .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..115b1b7 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,16 +22,16 @@ 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, } } } @@ -86,9 +86,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 +118,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(); @@ -224,9 +225,7 @@ impl Debug for FiberRootNode { write!( f, "{:?}(flags:{:?}, subtreeFlags:{:?})", - current_borrowed - ._type - .as_ref().as_string().unwrap(), + current_borrowed._type.as_ref().as_string().unwrap(), current_borrowed.flags, current_borrowed.subtree_flags ) 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 c01ab54..a79185b 100644 --- a/packages/react/src/lib.rs +++ b/packages/react/src/lib.rs @@ -87,9 +87,10 @@ pub fn jsx(_type: &JsValue, config: &JsValue, maybe_children: &JsValue) -> JsVal 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"); + Reflect::set(&config, &"children".into(), &children.get(0)) + .expect("TODO: panic children"); } else { - Reflect::set(&config, &"children".into(), maybe_children); + Reflect::set(&config, &"children".into(), maybe_children).expect("TODO: panic set children"); } } } @@ -97,7 +98,6 @@ pub fn jsx(_type: &JsValue, config: &JsValue, maybe_children: &JsValue) -> JsVal jsx_dev(_type, config, &JsValue::undefined()) } - #[wasm_bindgen(js_name = isValidElement)] pub fn is_valid_element(object: &JsValue) -> bool { object.is_object() 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 +} From fc1d08ac44e2c44081b322e67cd7369356a3617f Mon Sep 17 00:00:00 2001 From: youxingzhi Date: Mon, 6 May 2024 21:00:28 +0800 Subject: [PATCH 4/8] blog-12: reconcile array, fix bugs --- examples/hello-world/src/App.tsx | 33 ++++++++++++-------- packages/react-reconciler/src/child_fiber.rs | 7 ++++- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/examples/hello-world/src/App.tsx b/examples/hello-world/src/App.tsx index b1112ea..d1be32a 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 before = [ +
  • 1
  • , +
  • 2
  • , +
  • 3
  • , +
  • 4
  • + ]; + const after = [ +
  • 4
  • , +
  • 2
  • , +
  • 3
  • , +
  • 1
  • + ]; + + const listToUse = isOdd ? before : after; + return ( -

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

      even

      } -
      - -

    + {listToUse} + ); } diff --git a/packages/react-reconciler/src/child_fiber.rs b/packages/react-reconciler/src/child_fiber.rs index 82825b9..c3fb602 100644 --- a/packages/react-reconciler/src/child_fiber.rs +++ b/packages/react-reconciler/src/child_fiber.rs @@ -113,7 +113,7 @@ fn reconcile_single_element( existing.clone().borrow_mut()._return = Some(return_fiber.clone()); delete_remaining_children( return_fiber.clone(), - current.clone(), + current.clone().unwrap().borrow().sibling.clone(), should_track_effects, ); return existing; @@ -154,6 +154,11 @@ fn reconcile_single_text_node( 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( From 7dbc2bbf5221a1499bafeaa171c4b7a6577dd9b7 Mon Sep 17 00:00:00 2001 From: youxingzhi Date: Tue, 7 May 2024 15:15:15 +0800 Subject: [PATCH 5/8] blog-12: reconcile array, fix bugs, use JsValue as key of HashMap --- examples/hello-world/src/App.tsx | 8 +-- packages/react-dom/src/host_config.rs | 29 +++++++++- packages/react-reconciler/src/child_fiber.rs | 61 +++++++++++--------- packages/react-reconciler/src/commit_work.rs | 21 +++---- packages/react-reconciler/src/fiber.rs | 41 ++++++++----- 5 files changed, 99 insertions(+), 61 deletions(-) diff --git a/examples/hello-world/src/App.tsx b/examples/hello-world/src/App.tsx index d1be32a..83965ba 100644 --- a/examples/hello-world/src/App.tsx +++ b/examples/hello-world/src/App.tsx @@ -3,7 +3,7 @@ import {useState} from 'react' function App() { const [num, updateNum] = useState(0); - const isOdd = num % 2; + const isOdd = num % 2 === 1; const before = [
  • 1
  • , @@ -18,12 +18,12 @@ function App() {
  • 1
  • ]; - const listToUse = isOdd ? before : after; - + const listToUse = isOdd ? after : before; + console.log(num, listToUse) return (
      { - updateNum(num + 1); + updateNum(num => num + 1); }} > {listToUse} diff --git a/packages/react-dom/src/host_config.rs b/packages/react-dom/src/host_config.rs index b3cdaae..428c6b6 100644 --- a/packages/react-dom/src/host_config.rs +++ b/packages/react-dom/src/host_config.rs @@ -61,7 +61,17 @@ impl HostConfig for ReactDomHostConfig { let p = parent.clone().downcast::().unwrap(); let c = child.clone().downcast::().unwrap(); match p.append_child(&c) { - Ok(_) => {} + Ok(_) => { + 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); } @@ -98,7 +108,22 @@ impl HostConfig for ReactDomHostConfig { let before = before.clone().downcast::().unwrap(); let child = child.clone().downcast::().unwrap(); match parent.insert_before(&before, Some(&child)) { - Ok(_) => {} + 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 {:?} {:?}", diff --git a/packages/react-reconciler/src/child_fiber.rs b/packages/react-reconciler/src/child_fiber.rs index c3fb602..1a6ad36 100644 --- a/packages/react-reconciler/src/child_fiber.rs +++ b/packages/react-reconciler/src/child_fiber.rs @@ -1,5 +1,6 @@ use std::cell::RefCell; use std::collections::HashMap; +use std::hash::{Hash, Hasher}; use std::rc::Rc; use wasm_bindgen::{JsCast, JsValue}; @@ -174,39 +175,51 @@ fn reconcile_single_text_node( Rc::new(RefCell::new(created)) } +struct Key(JsValue); + +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) + } + } +} + fn update_from_map( return_fiber: Rc>, - mut existing_children: HashMap>>, + 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 = index.to_string(); + key_to_use = JsValue::from(index); } else { let key = derive_from_js_value(element, "key"); key_to_use = match key.is_null() { - true => index.to_string(), - false => match key.as_string() { - None => { - log!( - "update_from_map, key is not string {:?}", - derive_from_js_value(element, "key") - ); - "".to_string() - } - Some(k) => k, - }, + true => JsValue::from(index), + false => key.clone(), } } - - let before = existing_children.get(&key_to_use).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_to_use); + existing_children.remove(&Key(key_to_use.clone())); if before.borrow().tag == HostText { return use_fiber(before.clone(), props.clone()); } else { @@ -225,7 +238,7 @@ fn update_from_map( if before.is_some() { let before = (*before.clone().unwrap()).clone(); - existing_children.remove(&key_to_use); + existing_children.remove(&Key(key_to_use.clone())); if Object::is( &before.borrow()._type, &derive_from_js_value(&(*element).clone(), "type"), @@ -254,19 +267,15 @@ fn reconcile_children_array( // 创建的第一个fiber let mut first_new_fiber: Option>> = None; - let mut existing_children: HashMap>> = HashMap::new(); + 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 => current_rc.borrow().index.to_string(), - false => current_rc - .borrow() - .key - .as_string() - .expect("key is not string"), + true => JsValue::from(current_rc.borrow().index), + false => current_rc.borrow().key.clone(), }; - existing_children.insert(key_to_use, current_rc.clone()); + existing_children.insert(Key(key_to_use), current_rc.clone()); current = current_rc.borrow().sibling.clone(); } @@ -275,7 +284,7 @@ fn reconcile_children_array( let after = new_child.get(i); let new_fiber = update_from_map( return_fiber.clone(), - existing_children.clone(), + &mut existing_children, i, &after, should_track_effects, diff --git a/packages/react-reconciler/src/commit_work.rs b/packages/react-reconciler/src/commit_work.rs index bddfb0c..eea1d48 100644 --- a/packages/react-reconciler/src/commit_work.rs +++ b/packages/react-reconciler/src/commit_work.rs @@ -177,9 +177,7 @@ impl CommitWork { } let node_cloned = node.clone(); - let _return = { - node_cloned.borrow()._return.clone() - }; + let _return = { node_cloned.borrow()._return.clone() }; node_cloned .borrow_mut() .sibling @@ -198,12 +196,13 @@ impl CommitWork { return; } let parent_state_node = FiberNode::derive_state_node(host_parent.unwrap()); - // let sibling = self.get_host_sibling(finished_work.clone()); + let sibling = self.get_host_sibling(finished_work.clone()); + if parent_state_node.is_some() { self.insert_or_append_placement_node_into_container( finished_work.clone(), parent_state_node.unwrap(), - None, + sibling, ); } } @@ -293,12 +292,9 @@ impl CommitWork { node = parent.clone(); } - - let _return = { - node_rc.borrow()._return.clone() - }; - node.clone() - .unwrap() + let node_rc = node.clone().unwrap(); + let _return = { node_rc.borrow()._return.clone() }; + node_rc .borrow_mut() .sibling .clone() @@ -326,8 +322,7 @@ impl CommitWork { node = node_rc.borrow().child.clone(); } } - - if node + if !node .clone() .unwrap() .borrow() diff --git a/packages/react-reconciler/src/fiber.rs b/packages/react-reconciler/src/fiber.rs index 115b1b7..be49e1c 100644 --- a/packages/react-reconciler/src/fiber.rs +++ b/packages/react-reconciler/src/fiber.rs @@ -189,14 +189,28 @@ 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 { @@ -249,27 +263,22 @@ impl Debug for FiberRootNode { } }; 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() { write!(f, ",").expect("print error"); } else { From d30c3406e4d03cfbe6d30a7222cada64d8eba5d2 Mon Sep 17 00:00:00 2001 From: youxingzhi Date: Tue, 7 May 2024 15:45:42 +0800 Subject: [PATCH 6/8] blog-12: update readme.md --- readme.md | 2 ++ 1 file changed, 2 insertions(+) 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/) From e1d86e853acab154a2b98f1e73508f5b5a65dcef Mon Sep 17 00:00:00 2001 From: youxingzhi Date: Tue, 7 May 2024 17:07:14 +0800 Subject: [PATCH 7/8] blog-12: fix insert_before bugs --- examples/hello-world/src/App.tsx | 2 +- packages/react-dom/src/host_config.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hello-world/src/App.tsx b/examples/hello-world/src/App.tsx index 83965ba..f1cee88 100644 --- a/examples/hello-world/src/App.tsx +++ b/examples/hello-world/src/App.tsx @@ -9,7 +9,7 @@ function App() {
    • 1
    • ,
    • 2
    • ,
    • 3
    • , -
    • 4
    • + //
    • 4
    • ]; const after = [
    • 4
    • , diff --git a/packages/react-dom/src/host_config.rs b/packages/react-dom/src/host_config.rs index 428c6b6..fe2f4c4 100644 --- a/packages/react-dom/src/host_config.rs +++ b/packages/react-dom/src/host_config.rs @@ -107,7 +107,7 @@ impl HostConfig for ReactDomHostConfig { let parent = container.clone().downcast::().unwrap(); let before = before.clone().downcast::().unwrap(); let child = child.clone().downcast::().unwrap(); - match parent.insert_before(&before, Some(&child)) { + match parent.insert_before(&child, Some(&before)) { Ok(_) => { log!( "insert_child_to_container {:?} {:?} {:?}", From 1dc9ec5feca4833912d0f5a555f8f044722e0db8 Mon Sep 17 00:00:00 2001 From: youxingzhi Date: Tue, 7 May 2024 18:02:56 +0800 Subject: [PATCH 8/8] blog-12: fix deletions bugs --- examples/hello-world/src/App.tsx | 4 +- packages/react-dom/src/host_config.rs | 4 +- packages/react-reconciler/src/child_fiber.rs | 6 +- packages/react-reconciler/src/commit_work.rs | 20 ++-- packages/react-reconciler/src/fiber.rs | 109 ++++++++++--------- 5 files changed, 75 insertions(+), 68 deletions(-) diff --git a/examples/hello-world/src/App.tsx b/examples/hello-world/src/App.tsx index f1cee88..84830df 100644 --- a/examples/hello-world/src/App.tsx +++ b/examples/hello-world/src/App.tsx @@ -9,10 +9,10 @@ function App() {
    • 1
    • ,
    • 2
    • ,
    • 3
    • , - //
    • 4
    • +
    • 4
    • ]; const after = [ -
    • 4
    • , + //
    • 4
    • ,
    • 2
    • ,
    • 3
    • ,
    • 1
    • diff --git a/packages/react-dom/src/host_config.rs b/packages/react-dom/src/host_config.rs index fe2f4c4..1e15926 100644 --- a/packages/react-dom/src/host_config.rs +++ b/packages/react-dom/src/host_config.rs @@ -86,7 +86,9 @@ impl HostConfig for ReactDomHostConfig { let p = container.clone().downcast::().unwrap(); let c = child.clone().downcast::().unwrap(); match p.remove_child(&c) { - Ok(_) => {} + Ok(_) => { + log!("remove_child {:?} {:?}", p, c); + } Err(e) => { log!("Failed to remove_child {:?} {:?} {:?} ", e, p, c); } diff --git a/packages/react-reconciler/src/child_fiber.rs b/packages/react-reconciler/src/child_fiber.rs index 1a6ad36..4c28126 100644 --- a/packages/react-reconciler/src/child_fiber.rs +++ b/packages/react-reconciler/src/child_fiber.rs @@ -45,11 +45,11 @@ fn delete_child( 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()); } } diff --git a/packages/react-reconciler/src/commit_work.rs b/packages/react-reconciler/src/commit_work.rs index eea1d48..53909dc 100644 --- a/packages/react-reconciler/src/commit_work.rs +++ b/packages/react-reconciler/src/commit_work.rs @@ -67,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; } } diff --git a/packages/react-reconciler/src/fiber.rs b/packages/react-reconciler/src/fiber.rs index be49e1c..992956c 100644 --- a/packages/react-reconciler/src/fiber.rs +++ b/packages/react-reconciler/src/fiber.rs @@ -36,7 +36,6 @@ impl MemoizedState { } } -#[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![], } } @@ -147,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(); @@ -213,55 +263,8 @@ impl Debug for FiberRootNode { 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(QueueItem::new(Rc::clone(child), depth + 1)); let mut sibling = child.clone().borrow().sibling.clone(); @@ -278,7 +281,7 @@ impl Debug for FiberRootNode { .expect("print error"); continue; } - + if current_ref._return.is_some() { write!(f, ",").expect("print error"); } else {