From ec9d728f3d0cb58d0732d44fdecf5659bd0cb9f0 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 10 Oct 2016 17:29:48 +0100 Subject: [PATCH] Add Implementation Notes and amend Codebase Overview (#7830) * Add more to Codebase Overview * WIP * Start a reconciler overview * Add a few more sections * todo * WIP * Finish it * Whitespace * Minor tweaks * Minor tweaks --- _data/nav_contributing.yml | 2 + contributing/codebase-overview.md | 156 ++++- contributing/how-to-contribute.md | 16 +- contributing/implementation-notes.md | 894 +++++++++++++++++++++++++ img/docs/implementation-notes-tree.png | Bin 0 -> 59918 bytes 5 files changed, 1059 insertions(+), 9 deletions(-) create mode 100644 contributing/implementation-notes.md create mode 100644 img/docs/implementation-notes-tree.png diff --git a/_data/nav_contributing.yml b/_data/nav_contributing.yml index 22024d08..1e1f02fd 100644 --- a/_data/nav_contributing.yml +++ b/_data/nav_contributing.yml @@ -4,5 +4,7 @@ title: How to Contribute - id: codebase-overview title: Codebase Overview + - id: implementation-notes + title: Implementation Notes - id: design-principles title: Design Principles diff --git a/contributing/codebase-overview.md b/contributing/codebase-overview.md index 235d5c15..28ba41cb 100644 --- a/contributing/codebase-overview.md +++ b/contributing/codebase-overview.md @@ -4,7 +4,7 @@ title: Codebase Overview layout: contributing permalink: contributing/codebase-overview.html prev: how-to-contribute.html -next: design-principles.html +next: implementation-notes.html --- This section will give you an overview of the React codebase organization, its conventions, and the implementation. @@ -48,7 +48,7 @@ React itself was extracted from Facebook codebase and uses Haste for historical When we compile React for npm, a script copies all the modules into [a single flat directory called `lib`](https://unpkg.com/react@15/lib/) and prepends all `require()` paths with `./`. This way Node, Browserify, Webpack, and other tools can understand React build output without being aware of Haste. -**If you're reading React source on GitHub and want to quickly jump to any file, press "t".** +**If you're reading React source on GitHub and want to jump to a file, press "t".** This is a GitHub shortcut for searching the current repo for fuzzy filename matches. Start typing the name of the file you are looking for, and it will show up as the first match. @@ -78,6 +78,22 @@ We don't have a top-level directory for unit tests. Instead, we put them into a For example, a test for [`setInnerHTML.js`](https://github.com/facebook/react/blob/87724bd87506325fcaf2648c70fc1f43411a87be/src/renderers/dom/client/utils/setInnerHTML.js) is located in [`__tests__/setInnerHTML-test.js`](https://github.com/facebook/react/blob/87724bd87506325fcaf2648c70fc1f43411a87be/src/renderers/dom/client/utils/__tests__/setInnerHTML-test.js) right next to it. +### Shared Code + +Even though Haste allows to import any module from anywhere in the repository, we follow a convention to avoid cyclic dependencies and other unpleasant surprises. By convention, a file may only import files in the same folder or in subfolders below. + +For example, files inside [`src/renderers/dom/client`](https://github.com/facebook/react/tree/master/src/renderers/dom/client) may import other files in the same folder ot below. + +However they can't import modules from [`src/renderers/dom/server`](https://github.com/facebook/react/tree/master/src/renderers/dom/server) because it is not a child directory of [`src/renderers/dom/client`](https://github.com/facebook/react/tree/master/src/renderers/dom/client). + +There is an exception to this rule. Sometimes we *do* need to share functionality between two groups of modules. In this case we hoist it up to a folder called `shared` inside their closest common ancestor folder. + +For example, code shared between [`src/renderers/dom/client`](https://github.com/facebook/react/tree/master/src/renderers/dom/client) and [`src/renderers/dom/server`](https://github.com/facebook/react/tree/master/src/renderers/dom/server) lives in [`src/renderers/dom/shared`](https://github.com/facebook/react/tree/master/src/renderers/dom/shared). + +By the same logic, if [`src/renderers/dom/client`](https://github.com/facebook/react/tree/master/src/renderers/dom/client) needs to share a utility with something in [`src/renderers/native`](https://github.com/facebook/react/tree/master/src/renderers/native), this utility would be in [`src/renderers/shared`](https://github.com/facebook/react/tree/master/src/renderers/shared). + +This convention is not enforced but we check for it during a pull request review. + ### Warnings and Invariants React codebase uses the `warning` module to display warnings: @@ -141,6 +157,136 @@ if (__DEV__) { } ``` +### JSDoc + +Some of the internal and public methods are annotated with [JSDoc annotations](http://usejsdoc.org/): + +```js +/** + * Updates this component by updating the text content. + * + * @param {ReactText} nextText The next text content + * @param {ReactReconcileTransaction} transaction + * @internal + */ +receiveComponent: function(nextText, transaction) { + // ... +}, +``` + +We try to keep existing annotations up-to-date but we don't enforce them. We don't use JSDoc in the newly written code, and instead use Flow to document and enforce types. + +### Flow + +We recently started introducing [Flow](https://flowtype.org/) checks to the codebase. Files marked with the `@flow` annotation in the license header comment are being typechecked. + +We accept pull requests [adding Flow annotations to existing code](https://github.com/facebook/react/pull/7600/files). Flow annotations look like this: + +```js +ReactRef.detachRefs = function( + instance: ReactInstance, + element: ReactElement | string | number | null | false, +): void { + // ... +} +``` + +When possible, new code should use Flow annotations. +You can run `npm run flow` locally to check your code with Flow. + +### Classes and Mixins + +React was originally written in ES5. We have since enabled ES2015 features with [Babel](http://babeljs.io/), including classes. However most of React code is still written in ES5. + +In particular, you might see the following pattern quite often: + +```js +// Constructor +function ReactDOMComponent(element) { + this._currentElement = element; +} + +// Methods +ReactDOMComponent.Mixin = { + mountComponent: function() { + // ... + } +}; + +// Put methods on the prototype +Object.assign( + ReactDOMComponent.prototype, + ReactDOMComponent.Mixin +); + +module.exports = ReactDOMComponent; +``` + +The `Mixin` in this code has no relation to React `mixins` feature. It is just a way of grouping a few methods under an object. Those methods may later get attached to some other class. We use this pattern in a few places although we try to avoid it in the new code. + +Equivalent code in ES2015 would like this: + +```js +class ReactDOMComponent { + constructor(element) { + this._currentElement = element; + } + + mountComponent() { + // ... + } +} + +module.exports = ReactDOMComponent; +``` + +Sometimes we [convert old code to ES2015 classes](https://github.com/facebook/react/pull/7647/files). However this is not very important to us because there is an [ongoing effort](#fiber-reconciler) to replace the React reconciler implementation with a less object-oriented approach which wouldn't use classes at all. + +### Dynamic Injection + +React uses dynamic injection in some modules. While it is always explicit, it is still unfortunate because it hinders understanding of the code. The main reason it exists is because React originally only supported DOM as a target. React Native started as a React fork. We had to add dynamic injection to React to let React Native override some behaviors. + +You may see modules declaring their dynamic dependencies like this: + +```js +// Dynamically injected +var textComponentClass = null; + +// Relies on dynamically injected value +function createInstanceForText(text) { + return new textComponentClass(text); +} + +var ReactHostComponent = { + createInstanceForText, + + // Provides an opportunity for dynamic injection + injection: { + injectTextComponentClass: function(componentClass) { + textComponentClass = componentClass; + }, + }, +}; + +module.exports = ReactHostComponent; +``` + +The `injection` field is not handled specially in any way. But by convention, it means that this module wants to have some (presumably platform-specific) dependencies injected into it at runtime. + +In React DOM, [`ReactDefaultInjection`](https://github.com/facebook/react/blob/4f345e021a6bd9105f09f3aee6d8762eaa9db3ec/src/renderers/dom/shared/ReactDefaultInjection.js) injects a DOM-specific implementation: + +```js +ReactHostComponent.injection.injectTextComponentClass(ReactDOMTextComponent); +``` + +In React Native, [`ReactNativeDefaultInjection`](https://github.com/facebook/react/blob/4f345e021a6bd9105f09f3aee6d8762eaa9db3ec/src/renderers/native/ReactNativeDefaultInjection.js) injects its own implementation: + +```js +ReactHostComponent.injection.injectTextComponentClass(ReactNativeTextComponent); +``` + +There are multiple injection points in the codebase. In the future, we intend to get rid of the dynamic injection mechanism and wire up all the pieces statically during the build. + ### Multiple Packages React is a [monorepo](http://danluu.com/monorepo/). Its repository contains multiple separate packages so that their changes can be coordinated together, and documentation and issues live in one place. @@ -227,6 +373,10 @@ During an update, the stack reconciler "drills down" through composite component It is important to understand that the stack reconciler always processes the component tree synchronously in a single pass. While individual tree branches may [bail out of reconciliation](/react/docs/advanced-performance.html#avoiding-reconciling-the-dom), the stack reconciler can't pause, and so it is suboptimal when the updates are deep and the available CPU time is limited. +#### Learn More + +**The [next section](/react/contributing/implementation-notes.html) describes the current implementation in more details.** + ### Fiber Reconciler The "fiber" reconciler is a new effort aiming to resolve the problems inherent in the stack reconciler and fix a few long-standing issues. @@ -259,4 +409,4 @@ Additionally, we provide a standalone build called `react-with-addons.js` which ### What Next? -Learn the [design principles](/react/contributing/design-principles.html) guiding development of React in the next section. +Read the [next section](/react/contributing/implementation-notes.html) to learn about the current implementation of reconciler in more detail. diff --git a/contributing/how-to-contribute.md b/contributing/how-to-contribute.md index fecb96ef..61055e5b 100644 --- a/contributing/how-to-contribute.md +++ b/contributing/how-to-contribute.md @@ -80,7 +80,8 @@ The core team is monitoring for pull requests. We will review your pull request 3. If you've changed APIs, update the documentation. 4. Ensure the test suite passes (`npm test`). 5. Make sure your code lints (`npm run lint`). -6. If you haven't already, complete the CLA. +6. Run the [Flow](https://flowtype.org/) typechecks (`npm run flow`). +7. If you haven't already, complete the CLA. ### Contributor License Agreement (CLA) @@ -104,6 +105,7 @@ Then, you can run several commands: * `npm test` runs the complete test suite. * `npm test -- --watch` runs an interactive test watcher. * `npm test ` runs tests with matching filenames. +* `npm run flow` runs the [Flow](https://flowtype.org/) typechecks. * `npm run build` creates a `build` folder with all the packages. We recommend running `npm test` (or its variations above) to make sure you don't introduce any regressions as you work on your change. However it can be handy to try your build of React in a real project. @@ -142,16 +144,18 @@ However, there are still some styles that the linter cannot pick up. If you are * Write "attractive" code * Do not use the optional parameters of `setTimeout` and `setInterval` -### License +### Introductory Video -By contributing to React, you agree that your contributions will be licensed under its BSD license. +You may be interested in watching [this short video](https://www.youtube.com/watch?v=wUpPsEcGsg8) (26 mins) which gives an introduction on how to contribute to React. ### Meeting Notes React team meets once a week to discuss the development of React, future plans, and priorities. You can find the meeting notes in a [dedicated repository](https://github.com/reactjs/core-notes/). -### What Next? +### License -You may be interested in watching [this short video](https://www.youtube.com/watch?v=wUpPsEcGsg8) (26 mins) which gives an introduction on how to contribute to React. +By contributing to React, you agree that your contributions will be licensed under its BSD license. + +### What Next? -Read the next sections to learn more about [understanding the codebase](/react/contributing/codebase-overview.html), and the [design principles](/react/contributing/design-principles.html) guiding the development of React. +Read the [next section](/react/contributing/codebase-overview.html) to learn how the codebase is organized. diff --git a/contributing/implementation-notes.md b/contributing/implementation-notes.md new file mode 100644 index 00000000..504c3974 --- /dev/null +++ b/contributing/implementation-notes.md @@ -0,0 +1,894 @@ +--- +id: implementation-notes +title: Implementation Notes +layout: contributing +permalink: contributing/implementation-notes.html +prev: codebase-overview.html +next: design-principles.html +--- + +This section is a collection of implementation notes for the [stack reconciler](/react/contributing/codebase-overview.html#stack-reconciler). + +It is very technical and assumes a strong understanding of React public API as well as how it's divided into core, renderers, and the reconciler. If you're not very familiar with the React codebase, read [the codebase overview](/react/contributing/codebase-overview.html) first. + +The stack reconciler is powering all the React production code today. It is located in [`src/renderers/shared/stack/reconciler`](https://github.com/facebook/react/tree/master/src/renderers/shared/stack) and is used by both React DOM and React Native. + +### Video: Building React from Scratch + +[Paul O'Shannessy](https://twitter.com/zpao) gave a talk about [building React from scratch](https://www.youtube.com/watch?v=_MAD4Oly9yg) that largely inspired this document. + +Both this document and his talk are simplifications of the real codebase so you might get a better understanding by getting familiar with both of them. + +### Overview + +The reconciler itself doesn't have a public API. [Renderers](/react/contributing/codebase-overview.html#stack-renderers) like React DOM and React Native use it to efficiently update the user interface according to the React components written by the user. + +### Mounting as a Recursive Process + +Let's consider the first time you mount a component: + +```js +ReactDOM.render(, rootEl); +``` + +React DOM will pass `` along to the reconciler. Remember that `` is a React element, that is, a description of *what* to render. You can think about it as a plain object: + +```js +console.log(); +// { type: App, props: {} } +``` + +The reconciler will check if `App` is a class or a function. + +If `App` is a function, the reconciler will call `App(props)` to get the rendered element. + +If `App` is a class, the reconciler will instantiate an `App` with `new App(props)`, call the `componentWillMount()` lifecycle method, and then will call the `render()` method to get the rendered element. + +Either way, the reconciler will learn the element `App` "rendered to". + +This process is recursive. `App` may render to a ``, `Greeting` may render to a `