From 9b94390c7a337db9d46b45289500c66e8ae4e8dd Mon Sep 17 00:00:00 2001 From: MQuy Date: Wed, 30 Jan 2019 00:20:21 +0100 Subject: [PATCH] Support hooks Related prs + https://github.com/facebook/react/pull/13968 + https://github.com/facebook/react/pull/14569 --- README.md | 4 +- demo/app.js | 84 ++-------- demo/package.json | 4 +- demo/yarn.lock | 103 +++--------- qreact.js | 2 + src/Component.js | 2 + src/Fiber.js | 28 ++-- src/FiberBeginWork.js | 51 +++++- src/FiberHooks.js | 363 ++++++++++++++++++++++++++++++++++++++++++ src/FiberScheduler.js | 2 +- src/UpdateQueue.js | 8 +- 11 files changed, 465 insertions(+), 186 deletions(-) create mode 100644 src/FiberHooks.js diff --git a/README.md b/README.md index 2c5c431..b1ce476 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,6 @@ The dead simple implementation of React for the learning purpose on how React wo - [x] Global event handler - [x] Fiber - [x] Priority -- [ ] Hooks +- [x] Hooks - [ ] Suspend -- [ ] Synthetic event +- [ ] Synthetic event \ No newline at end of file diff --git a/demo/app.js b/demo/app.js index e8cc986..e5796e2 100644 --- a/demo/app.js +++ b/demo/app.js @@ -1,79 +1,19 @@ import * as React from "../qreact"; -class App extends React.Component { - constructor(props) { - super(props); - - this.state = { - stories: [ - { - id: 1, - name: "[Webpack] — Smart Loading Assets For Production", - url: - "https://hackernoon.com/webpack-smart-loading-assets-for-production-3571e0a29c2e", - }, - { - id: 2, - name: "V8 Engine Overview", - url: "https://medium.com/@MQuy90/v8-engine-overview-7c965731ced4", - }, - ], - }; - } - - render() { - const { stories } = this.state; - - return ( -
- -
- ); - } - - removeStory = story => () => { - const { stories } = this.state; - - const index = stories.findIndex(s => s.id == story.id); - stories.splice(index, 1); - - this.setState(stories); - }; -} - -class Story extends React.Component { - constructor(props) { - super(props); - - this.state = { likes: Math.ceil(Math.random() * 100) }; - } - render() { - const { story, onRemove } = this.props; - const { likes } = this.state; - - return ( -
  • - - {story.name} - -
  • - ); - } - - handleClick = () => { - this.setState({ - likes: this.state.likes + 1, - }); - }; +function Example() { + const [count, setCount] = React.useState(0); + + return ( +
    +

    You clicked {count} times

    + +
    + ); } React.render( - - - , +
    + +
    , document.getElementById("root"), ); diff --git a/demo/package.json b/demo/package.json index d062a0c..43f858d 100644 --- a/demo/package.json +++ b/demo/package.json @@ -19,7 +19,7 @@ "webpack-dev-server": "3.1.14" }, "dependencies": { - "react": "16.3.0", - "react-dom": "16.3.3" + "react": "16.8.0-alpha.1", + "react-dom": "16.8.0-alpha.1" } } diff --git a/demo/yarn.lock b/demo/yarn.lock index 3aa9c8d..928fbf1 100644 --- a/demo/yarn.lock +++ b/demo/yarn.lock @@ -949,11 +949,6 @@ array-unique@^0.3.2: resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -asap@~2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" - integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= - asn1.js@^4.0.0: version "4.9.2" resolved "https://registry.yarnpkg.com/asn1.js/-/asn1.js-4.9.2.tgz#8117ef4f7ed87cd8f89044b5bff97ac243a16c9a" @@ -1459,11 +1454,6 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40= -core-js@^1.0.0: - version "1.2.7" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" - integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= - core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -1804,13 +1794,6 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -encoding@^0.1.11: - version "0.1.12" - resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" - integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= - dependencies: - iconv-lite "~0.4.13" - end-of-stream@^1.0.0, end-of-stream@^1.1.0: version "1.4.1" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" @@ -2060,19 +2043,6 @@ faye-websocket@~0.11.1: dependencies: websocket-driver ">=0.5.1" -fbjs@^0.8.16: - version "0.8.17" - resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd" - integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90= - dependencies: - core-js "^1.0.0" - isomorphic-fetch "^2.1.1" - loose-envify "^1.0.0" - object-assign "^4.1.0" - promise "^7.1.1" - setimmediate "^1.0.5" - ua-parser-js "^0.7.18" - figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -2520,7 +2490,7 @@ iconv-lite@0.4.23: dependencies: safer-buffer ">= 2.1.2 < 3" -iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== @@ -2770,7 +2740,7 @@ is-regex@^1.0.4: dependencies: has "^1.0.1" -is-stream@^1.0.1, is-stream@^1.1.0: +is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= @@ -2817,14 +2787,6 @@ isobject@^3.0.0, isobject@^3.0.1: resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= -isomorphic-fetch@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" - integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk= - dependencies: - node-fetch "^1.0.1" - whatwg-fetch ">=0.10.0" - js-levenshtein@^1.1.3: version "1.1.6" resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d" @@ -3254,14 +3216,6 @@ no-case@^2.2.0: dependencies: lower-case "^1.1.1" -node-fetch@^1.0.1: - version "1.7.3" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" - integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== - dependencies: - encoding "^0.1.11" - is-stream "^1.0.1" - node-forge@0.7.5: version "0.7.5" resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.5.tgz#6c152c345ce11c52f465c2abd957e8639cd674df" @@ -3681,14 +3635,7 @@ promise-inflight@^1.0.1: resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= -promise@^7.1.1: - version "7.3.1" - resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" - integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg== - dependencies: - asap "~2.0.3" - -prop-types@^15.6.0: +prop-types@^15.6.2: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" integrity sha512-3pboPvLiWD7dkI3qf3KbUe6hKFKa52w+AE0VCqECtf+QHAKgOL37tTaNCnuX1nAAQ4ZhyP+kYVKf8rLmJ/feDQ== @@ -3812,25 +3759,25 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@16.3.3: - version "16.3.3" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.3.3.tgz#af4c2aef9f6a66251a46da50253c860a67ae66d9" - integrity sha512-ALCp7ZbSGkqRDtQoZozKVNgwXMxbxf/IGOUMC2A0yF6JHeZrS8e2cOotPT87Vf4b7PKCuUVKU4/RDEXxToA/yA== +react-dom@16.8.0-alpha.1: + version "16.8.0-alpha.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.0-alpha.1.tgz#dab73b8354ba2e498e3127d18e29d4546cea889e" + integrity sha512-tZCUM8BpnwUHJmLnUWP9c3vVZxnCqYotj7s4tx7umojG6BKv745KIBtuPTzt0EI0q50GMLEpmT/CPQ8iA61TwQ== dependencies: - fbjs "^0.8.16" loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.0" + prop-types "^15.6.2" + scheduler "^0.13.0-alpha.1" -react@16.3.0: - version "16.3.0" - resolved "https://registry.yarnpkg.com/react/-/react-16.3.0.tgz#fc5a01c68f91e9b38e92cf83f7b795ebdca8ddff" - integrity sha512-Qh35tNbwY8SLFELkN3PCLO16EARV+lgcmNkQnoZXfzAF1ASRpeucZYUwBlBzsRAzTb7KyfBaLQ4/K/DLC6MYeA== +react@16.8.0-alpha.1: + version "16.8.0-alpha.1" + resolved "https://registry.yarnpkg.com/react/-/react-16.8.0-alpha.1.tgz#c2b32689f3b466d3ce85a634dd9035f789d2cd97" + integrity sha512-vLwwnhM2dXrCsiQmcSxF2UdZVV5xsiXjK5Yetmy8dVqngJhQ3aw3YJhZN/YmyonxwdimH40wVqFQfsl4gSu2RA== dependencies: - fbjs "^0.8.16" loose-envify "^1.1.0" object-assign "^4.1.1" - prop-types "^15.6.0" + prop-types "^15.6.2" + scheduler "^0.13.0-alpha.1" "readable-stream@1 || 2", readable-stream@^2.0.0, readable-stream@^2.0.4, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@~2.3.6: version "2.3.6" @@ -4070,6 +4017,14 @@ sax@^1.2.4: resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== +scheduler@^0.13.0-alpha.1: + version "0.13.0-alpha.1" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.0-alpha.1.tgz#753977fb4fb35d8cdd559868a11e46b640955556" + integrity sha512-W0sH0848sVuPKg+I18vTYQyzVtA4X1lrVgSeXK6KnOPUltFdJcY5nkbTkjGUeS/E0x+eBsNYfSdhJtGjT95njw== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" + schema-utils@^0.4.4: version "0.4.7" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187" @@ -4183,7 +4138,7 @@ set-value@^2.0.0: is-plain-object "^2.0.3" split-string "^3.0.1" -setimmediate@^1.0.4, setimmediate@^1.0.5: +setimmediate@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU= @@ -4583,11 +4538,6 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -ua-parser-js@^0.7.18: - version "0.7.19" - resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b" - integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ== - uglify-js@3.4.x: version "3.4.9" resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" @@ -4887,11 +4837,6 @@ websocket-extensions@>=0.1.1: resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== -whatwg-fetch@>=0.10.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" - integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== - which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" diff --git a/qreact.js b/qreact.js index b97a1f1..08ee47d 100644 --- a/qreact.js +++ b/qreact.js @@ -2,6 +2,7 @@ import { render } from "./src/render"; import { createElement, REACT_ASYNC_MODE_TYPE } from "./src/createElement"; import { Component } from "./src/Component"; import { deferredUpdates } from "./src/FiberScheduler"; +import { useState } from "./src/FiberHooks"; const unstable_AsyncMode = REACT_ASYNC_MODE_TYPE; @@ -9,6 +10,7 @@ export { render, createElement, Component, + useState, deferredUpdates, unstable_AsyncMode, }; diff --git a/src/Component.js b/src/Component.js index 8c4a983..91025c6 100644 --- a/src/Component.js +++ b/src/Component.js @@ -14,6 +14,8 @@ export class Component { insertUpdateIntoFiber(fiber, update); scheduleWork(fiber, expirationTime); } + + isReactComponent = {}; } export const ReactInstanceMap = { diff --git a/src/Fiber.js b/src/Fiber.js index 6978745..c534dd7 100644 --- a/src/Fiber.js +++ b/src/Fiber.js @@ -5,6 +5,7 @@ import { HostText, ClassComponent, Mode, + FunctionalComponent, } from "./TypeOfWork"; import { REACT_ASYNC_MODE_TYPE, REACT_STRICT_MODE_TYPE } from "./createElement"; import { AsyncMode, StrictMode } from "./TypeOfMode"; @@ -95,7 +96,11 @@ export function createFiberFromElement(element, mode, expirationTime) { let fiberTag; if (typeof type === "function") { - fiberTag = ClassComponent; + if (shouldConstruct(type)) { + fiberTag = ClassComponent; + } else { + fiberTag = FunctionalComponent; + } } else if (typeof type === "string") { fiberTag = HostComponent; } else { @@ -109,7 +114,7 @@ export function createFiberFromElement(element, mode, expirationTime) { mode |= StrictMode; break; default: { - if (typeof type === "object" && type !== null) { + if (typeof type === "object" && type != null) { if (typeof type.tag === "number") { // Currently assumed to be a continuation and therefore is a // fiber already. @@ -144,20 +149,7 @@ export function createFiberFromText(content, mode, expirationTime) { return fiber; } -function createFiberFromElementType(type, key) { - let fiber; - if (typeof type === "function") { - fiber = new FiberNode(ClassComponent, key); - fiber.type = type; - } else if (typeof type === "string") { - fiber = new FiberNode(HostComponent, key); - fiber.type = type; - } else if ( - typeof type === "object" && - type != null && - typeof type.tag === "number" - ) { - fiber = type; - } - return fiber; +function shouldConstruct(Component) { + const prototype = Component.prototype; + return !!(prototype && prototype.isReactComponent); } diff --git a/src/FiberBeginWork.js b/src/FiberBeginWork.js index 3336d46..28fdb9b 100644 --- a/src/FiberBeginWork.js +++ b/src/FiberBeginWork.js @@ -14,11 +14,32 @@ import { HostText, Mode, } from "./TypeOfWork"; +import { prepareToUseHooks, finishHooks, bailoutHooks } from "./FiberHooks"; + +let didReceiveUpdate = false; export function beginWork(current, workInProgress, renderExpirationTime) { + const updateExpirationTime = workInProgress.expirationTime; + + if (current != null) { + if (current.memoizedProps !== workInProgress.pendingProps) { + didReceiveUpdate = true; + } else if (updateExpirationTime < renderExpirationTime) { + didReceiveUpdate = false; + } + } else { + didReceiveUpdate = false; + } + switch (workInProgress.tag) { case FunctionalComponent: - return updateFunctionalComponent(current, workInProgress); + const Component = workInProgress.type; + return updateFunctionalComponent( + current, + workInProgress, + Component, + renderExpirationTime, + ); case ClassComponent: return updateClassComponent( current, @@ -54,18 +75,28 @@ export function updateHostRoot(current, workInProgress, renderExpirationTime) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } -export function updateFunctionalComponent(current, workInProgress) { +export function updateFunctionalComponent( + current, + workInProgress, + Component, + renderExpirationTime, +) { let fn = workInProgress.type; let nextProps = workInProgress.pendingProps; - const memoizedProps = workInProgress.memoizedProps; + prepareToUseHooks(current, workInProgress, renderExpirationTime); + let nextChildren = fn(nextProps); + nextChildren = finishHooks(Component, nextProps, nextChildren); - if (nextProps == null || memoizedProps === nextProps) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); + if (current != null && !didReceiveUpdate) { + bailoutHooks(current, workInProgress, renderExpirationTime); + return bailoutOnAlreadyFinishedWork( + current, + workInProgress, + renderExpirationTime, + ); } - nextChildren = fn(nextProps); - workInProgress.effectTag |= PerformedWork; reconcileChildren(current, workInProgress, nextChildren); workInProgress.memoizedProps = nextProps; @@ -211,10 +242,14 @@ function reconcileChildren(current, workInProgress, nextChildren) { function updateMode(current, workInProgress) { const nextChildren = workInProgress.pendingProps.children; - if (nextChildren === null || workInProgress.memoizedProps === nextChildren) { + if (nextChildren == null || workInProgress.memoizedProps === nextChildren) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } reconcileChildren(current, workInProgress, nextChildren); workInProgress.memoizedProps = nextChildren; return workInProgress.child; } + +export function markWorkInProgressReceivedUpdate() { + didReceiveUpdate = true; +} diff --git a/src/FiberHooks.js b/src/FiberHooks.js new file mode 100644 index 0000000..dc649cd --- /dev/null +++ b/src/FiberHooks.js @@ -0,0 +1,363 @@ +import { + computeExpirationForFiber, + recalculateCurrentTime, + scheduleWork, +} from "./FiberScheduler"; +import { NoWork } from "./FiberExpirationTime"; +import { markWorkInProgressReceivedUpdate } from "./FiberBeginWork"; + +let renderExpirationTime = NoWork; +// The work-in-progress fiber. I've named it differently to distinguish it from +// the work-in-progress hook. +let currentlyRenderingFiber = null; + +// Hooks are stored as a linked list on the fiber's memoizedState field. The +// current hook list is the list that belongs to the current fiber. The +// work-in-progress hook list is a new list that will be added to the +// work-in-progress fiber. +let firstCurrentHook = null; +let currentHook = null; +let firstWorkInProgressHook = null; +let workInProgressHook = null; + +let remainingExpirationTime = NoWork; +let componentUpdateQueue = null; + +// Updates scheduled during render will trigger an immediate re-render at the +// end of the current pass. We can't store these updates on the normal queue, +// because if the work is aborted, they should be discarded. Because this is +// a relatively rare case, we also don't want to add an additional field to +// either the hook or queue object types. So we store them in a lazily create +// map of queue -> render-phase updates, which are discarded once the component +// completes without re-rendering. + +// Whether the work-in-progress hook is a re-rendered hook +let isReRender = false; +// Whether an update was scheduled during the currently executing render pass. +let didScheduleRenderPhaseUpdate = false; +// Lazily created map of render-phase updates +let renderPhaseUpdates = null; + +export function prepareToUseHooks( + current, + workInProgress, + nextRenderExpirationTime, +) { + renderExpirationTime = nextRenderExpirationTime; + currentlyRenderingFiber = workInProgress; + firstCurrentHook = current != null ? current.memoizedState : null; +} + +export function finishHooks(Component, props, children) { + // This must be called after every function component to prevent hooks from + // being used in classes. + + while (didScheduleRenderPhaseUpdate) { + // Updates were scheduled during the render phase. They are stored in + // the `renderPhaseUpdates` map. Call the component again, reusing the + // work-in-progress hooks and applying the additional updates on top. Keep + // restarting until no more updates are scheduled. + didScheduleRenderPhaseUpdate = false; + + // Start over from the beginning of the list + currentHook = null; + workInProgressHook = null; + componentUpdateQueue = null; + + children = Component(props); + } + renderPhaseUpdates = null; + + const renderedWork = currentlyRenderingFiber; + + renderedWork.memoizedState = firstWorkInProgressHook; + renderedWork.expirationTime = remainingExpirationTime; + renderedWork.updateQueue = componentUpdateQueue; + + renderExpirationTime = NoWork; + currentlyRenderingFiber = null; + + firstCurrentHook = null; + currentHook = null; + firstWorkInProgressHook = null; + workInProgressHook = null; + + remainingExpirationTime = NoWork; + componentUpdateQueue = null; + return children; +} + +function createHook() { + return { + memoizedState: null, + + baseState: null, + queue: null, + baseUpdate: null, + + next: null, + }; +} + +function cloneHook(hook) { + return { + memoizedState: hook.memoizedState, + + baseState: hook.baseState, + queue: hook.queue, + baseUpdate: hook.baseUpdate, + + next: null, + }; +} + +function createWorkInProgressHook() { + if (workInProgressHook == null) { + // This is the first hook in the list + if (firstWorkInProgressHook == null) { + isReRender = false; + currentHook = firstCurrentHook; + if (currentHook == null) { + // This is a newly mounted hook + workInProgressHook = createHook(); + } else { + // Clone the current hook. + workInProgressHook = cloneHook(currentHook); + } + firstWorkInProgressHook = workInProgressHook; + } else { + // There's already a work-in-progress. Reuse it. + isReRender = true; + currentHook = firstCurrentHook; + workInProgressHook = firstWorkInProgressHook; + } + } else { + if (workInProgressHook.next == null) { + isReRender = false; + let hook; + if (currentHook == null) { + // This is a newly mounted hook + hook = createHook(); + } else { + currentHook = currentHook.next; + if (currentHook == null) { + // This is a newly mounted hook + hook = createHook(); + } else { + // Clone the current hook. + hook = cloneHook(currentHook); + } + } + // Append to the end of the list + workInProgressHook = workInProgressHook.next = hook; + } else { + // There's already a work-in-progress. Reuse it. + isReRender = true; + workInProgressHook = workInProgressHook.next; + currentHook = currentHook != null ? currentHook.next : null; + } + } + return workInProgressHook; +} + +function basicStateReducer(state, action) { + return typeof action === "function" ? action(state) : action; +} + +export function useState(initialState) { + return useReducer(basicStateReducer, initialState); +} + +export function useReducer(reducer, initialState, initialAction) { + workInProgressHook = createWorkInProgressHook(); + let queue = workInProgressHook.queue; + if (queue != null) { + // Already have a queue, so this is an update. + if (isReRender) { + // This is a re-render. Apply the new render phase updates to the previous + // work-in-progress hook. + const dispatch = queue.dispatch; + if (renderPhaseUpdates != null) { + // Render phase updates are stored in a map of queue -> linked list + const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue); + if (firstRenderPhaseUpdate !== undefined) { + renderPhaseUpdates.delete(queue); + let newState = workInProgressHook.memoizedState; + let update = firstRenderPhaseUpdate; + do { + // Process this render phase update. We don't have to check the + // priority because it will always be the same as the current + // render's. + const action = update.action; + newState = reducer(newState, action); + update = update.next; + } while (update != null); + + workInProgressHook.memoizedState = newState; + + // Don't persist the state accumlated from the render phase updates to + // the base state unless the queue is empty. + // TODO: Not sure if this is the desired semantics, but it's what we + // do for gDSFP. I can't remember why. + if (workInProgressHook.baseUpdate === queue.last) { + workInProgressHook.baseState = newState; + } + + return [newState, dispatch]; + } + } + return [workInProgressHook.memoizedState, dispatch]; + } + + // The last update in the entire queue + const last = queue.last; + // The last update that is part of the base state. + const baseUpdate = workInProgressHook.baseUpdate; + + // Find the first unprocessed update. + let first; + if (baseUpdate != null) { + if (last != null) { + // For the first update, the queue is a circular linked list where + // `queue.last.next = queue.first`. Once the first update commits, and + // the `baseUpdate` is no longer empty, we can unravel the list. + last.next = null; + } + first = baseUpdate.next; + } else { + first = last != null ? last.next : null; + } + if (first != null) { + let newState = workInProgressHook.baseState; + let newBaseState = null; + let newBaseUpdate = null; + let prevUpdate = baseUpdate; + let update = first; + let didSkip = false; + do { + const updateExpirationTime = update.expirationTime; + if (updateExpirationTime < renderExpirationTime) { + // Priority is insufficient. Skip this update. If this is the first + // skipped update, the previous update/state is the new base + // update/state. + if (!didSkip) { + didSkip = true; + newBaseUpdate = prevUpdate; + newBaseState = newState; + } + // Update the remaining priority in the queue. + if (updateExpirationTime > remainingExpirationTime) { + remainingExpirationTime = updateExpirationTime; + } + } else { + // Process this update. + const action = update.action; + newState = reducer(newState, action); + } + prevUpdate = update; + update = update.next; + } while (update != null && update !== first); + + if (!didSkip) { + newBaseUpdate = prevUpdate; + newBaseState = newState; + } + + workInProgressHook.memoizedState = newState; + workInProgressHook.baseUpdate = newBaseUpdate; + workInProgressHook.baseState = newBaseState; + + // Mark that the fiber performed work, but only if the new state is + // different from the current state. + if (newState !== currentHook.memoizedState) { + markWorkInProgressReceivedUpdate(); + } + } + + const dispatch = queue.dispatch; + return [workInProgressHook.memoizedState, dispatch]; + } + + // There's no existing queue, so this is the initial render. + if (reducer === basicStateReducer) { + // Special case for `useState`. + if (typeof initialState === "function") { + initialState = initialState(); + } + } else if (initialAction !== undefined && initialAction != null) { + initialState = reducer(initialState, initialAction); + } + workInProgressHook.memoizedState = workInProgressHook.baseState = initialState; + queue = workInProgressHook.queue = { + last: null, + dispatch: null, + }; + const dispatch = (queue.dispatch = dispatchAction.bind( + null, + currentlyRenderingFiber, + queue, + )); + return [workInProgressHook.memoizedState, dispatch]; +} + +function dispatchAction(fiber, queue, action) { + const alternate = fiber.alternate; + if ( + fiber === currentlyRenderingFiber || + (alternate != null && alternate === currentlyRenderingFiber) + ) { + // This is a render phase update. Stash it in a lazily-created map of + // queue -> linked list of updates. After this render pass, we'll restart + // and apply the stashed updates on top of the work-in-progress hook. + didScheduleRenderPhaseUpdate = true; + const update = { + expirationTime: renderExpirationTime, + action, + next: null, + }; + if (renderPhaseUpdates == null) { + renderPhaseUpdates = new Map(); + } + const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue); + if (firstRenderPhaseUpdate === undefined) { + renderPhaseUpdates.set(queue, update); + } else { + // Append the update to the end of the list. + let lastRenderPhaseUpdate = firstRenderPhaseUpdate; + while (lastRenderPhaseUpdate.next != null) { + lastRenderPhaseUpdate = lastRenderPhaseUpdate.next; + } + lastRenderPhaseUpdate.next = update; + } + } else { + const currentTime = recalculateCurrentTime(); + const expirationTime = computeExpirationForFiber(currentTime, fiber); + const update = { + expirationTime, + action, + next: null, + }; + // Append the update to the end of the list. + const last = queue.last; + if (last == null) { + // This is the first update. Create a circular list. + update.next = update; + } else { + const first = last.next; + if (first != null) { + // Still circular. + update.next = first; + } + last.next = update; + } + queue.last = update; + scheduleWork(fiber, expirationTime); + } +} + +export function bailoutHooks(current, workInProgress, expirationTime) { + workInProgress.updateQueue = current.updateQueue; + if (current.expirationTime <= expirationTime) { + current.expirationTime = NoWork; + } +} diff --git a/src/FiberScheduler.js b/src/FiberScheduler.js index f39e703..2c55528 100644 --- a/src/FiberScheduler.js +++ b/src/FiberScheduler.js @@ -348,7 +348,7 @@ function computeInteractiveExpiration(currentTime) { return computeExpirationBucket(currentTime, expirationMs, bucketSizeMs); } -function recalculateCurrentTime() { +export function recalculateCurrentTime() { mostRecentCurrentTime = msToExpirationTime(performance.now()); return mostRecentCurrentTime; } diff --git a/src/UpdateQueue.js b/src/UpdateQueue.js index 0cd5bb3..9c985fc 100644 --- a/src/UpdateQueue.js +++ b/src/UpdateQueue.js @@ -10,21 +10,21 @@ export function insertUpdateIntoFiber(fiber, update) { } let queue2 = null; - if (alternateFiber !== null) { + if (alternateFiber != null) { queue2 = alternateFiber.updateQueue; - if (queue2 === null) { + if (queue2 == null) { queue2 = alternateFiber.updateQueue = createUpdateQueue(); } } // If there's only one queue, add the update to that queue and exit. - if (queue2 === null) { + if (queue2 == null) { insertUpdateIntoQueue(queue1, update); return; } // If either queue is empty, we need to add to both queues. - if (queue1.last === null || queue2.last === null) { + if (queue1.last == null || queue2.last == null) { insertUpdateIntoQueue(queue1, update); insertUpdateIntoQueue(queue2, update); return;