├── .gitignore
├── blog
├── assets
│ ├── mount-process.jpg
│ └── update-process.jpg
├── README.md
├── book.json
├── guidance.md
├── mounting-contd.md
├── update.md
├── update-dom.md
├── mounting.md
└── update-contd.md
├── dilithium
├── src
│ ├── assert.js
│ ├── shouldUpdateComponent.js
│ ├── Element.js
│ ├── mount.js
│ ├── Reconciler.js
│ ├── operations.js
│ ├── instantiateComponent.js
│ ├── traverseAllChildren.js
│ ├── DOM.js
│ ├── Component.js
│ ├── ChildReconciler.js
│ ├── DOMComponent.js
│ └── MultiChild.js
├── index.js
├── package.json
└── webpack.config.js
├── demo
├── index.html
├── webpack.config.js
├── package.json
└── app.js
├── .travis.yml
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | build
3 | bundle.js
4 |
--------------------------------------------------------------------------------
/blog/assets/mount-process.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyan33/learn-react-source-code/HEAD/blog/assets/mount-process.jpg
--------------------------------------------------------------------------------
/blog/assets/update-process.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/cyan33/learn-react-source-code/HEAD/blog/assets/update-process.jpg
--------------------------------------------------------------------------------
/dilithium/src/assert.js:
--------------------------------------------------------------------------------
1 | function assert(val) {
2 | if (!Boolean(val)) {
3 | throw new Error('assertion failure')
4 | }
5 | }
6 |
7 | module.exports = assert
8 |
--------------------------------------------------------------------------------
/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Document
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/dilithium/index.js:
--------------------------------------------------------------------------------
1 | const Element = require('./src/Element')
2 | const Component = require('./src/Component')
3 | const Mount = require('./src/mount')
4 |
5 | module.exports = {
6 | createElement: Element.createElement,
7 | Component,
8 | render: Mount.render
9 | }
10 |
--------------------------------------------------------------------------------
/dilithium/src/shouldUpdateComponent.js:
--------------------------------------------------------------------------------
1 | function shouldUpdateComponent(prevElement, nextElement) {
2 | // if it's still the same type, we update the component
3 | // instead of unmount and mount from scratch
4 | return prevElement.type === nextElement.type
5 | }
6 |
7 | module.exports = shouldUpdateComponent
8 |
--------------------------------------------------------------------------------
/dilithium/src/Element.js:
--------------------------------------------------------------------------------
1 | function createElement(type, config, children) {
2 | const props = Object.assign({}, config)
3 | const childrenLength = [].slice.call(arguments).length - 2
4 |
5 | if (childrenLength > 1) {
6 | props.children = [].slice.call(arguments, 2)
7 | } else if (childrenLength === 1) {
8 | props.children = children
9 | }
10 |
11 | return {
12 | type,
13 | props
14 | }
15 | }
16 |
17 | module.exports = {
18 | createElement
19 | }
20 |
--------------------------------------------------------------------------------
/dilithium/src/mount.js:
--------------------------------------------------------------------------------
1 | const instantiateComponent = require('./instantiateComponent')
2 | const Reconciler = require('./Reconciler')
3 | const DOM = require('./DOM')
4 |
5 | function render(element, node) {
6 | // todo: add update
7 | mount(element, node)
8 | }
9 |
10 | function mount(element, node) {
11 | let component = instantiateComponent(element)
12 | let renderedNode = Reconciler.mountComponent(component)
13 |
14 | DOM.empty(node)
15 | DOM.appendChildren(node, renderedNode)
16 | }
17 |
18 | module.exports = {
19 | render,
20 | }
21 |
--------------------------------------------------------------------------------
/demo/webpack.config.js:
--------------------------------------------------------------------------------
1 | let webpack = require('webpack');
2 |
3 | module.exports = {
4 | entry: './app.js',
5 | output: {
6 | path: __dirname,
7 | filename: 'bundle.js',
8 | },
9 | "source-map": "source-map",
10 | module: {
11 | loaders: [
12 | {
13 | test: /\.js$/,
14 | loader: 'babel',
15 | query: {
16 | plugins: [
17 | ['transform-react-jsx', {pragma: 'Dilithium.createElement'}],
18 | 'transform-class-properties'
19 | ]
20 | }
21 | }
22 | ]
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/dilithium/src/Reconciler.js:
--------------------------------------------------------------------------------
1 | /**
2 | * component => node
3 | */
4 |
5 | function mountComponent(component) {
6 | return component.mountComponent()
7 | }
8 |
9 | function unmountComponent(component) {
10 | component.unmountComponent()
11 | }
12 |
13 | function receiveComponent(component, nextElement) {
14 | const prevElement = component._currentElement
15 | if (prevElement === nextElement) return
16 |
17 | component.updateComponent(component._currentElement, nextElement)
18 | }
19 |
20 | module.exports = {
21 | mountComponent,
22 | unmountComponent,
23 | receiveComponent
24 | }
25 |
--------------------------------------------------------------------------------
/dilithium/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dilithium",
3 | "version": "0.1.0",
4 | "main": "index.js",
5 | "scripts": {
6 | "build": "webpack --progress --colors",
7 | "watch": "webpack --progress --colors --watch"
8 | },
9 | "dependencies": {
10 | "babel-core": "^6.13.2",
11 | "babel-loader": "^6.2.5",
12 | "babel-plugin-transform-class-properties": "^6.11.5",
13 | "circular-dependency-plugin": "^1.1.0",
14 | "babel-plugin-transform-react-jsx": "^6.8.0",
15 | "webpack": "^1.13.2"
16 | },
17 | "babel": {
18 | "plugins": [
19 | "transform-class-properties"
20 | ]
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/dilithium/src/operations.js:
--------------------------------------------------------------------------------
1 | const UPDATE_TYPES = {
2 | INSERT: 1,
3 | MOVE: 2,
4 | REMOVE: 3
5 | }
6 |
7 | const OPERATIONS = {
8 | insert(node, afterNode) {
9 | return {
10 | type: UPDATE_TYPES.INSERT,
11 | content: node,
12 | afterNode: afterNode,
13 | }
14 | },
15 |
16 | move(component, afterNode) {
17 | return {
18 | type: UPDATE_TYPES.MOVE,
19 | fromIndex: component._mountIndex,
20 | afterNode: afterNode,
21 | }
22 | },
23 |
24 | remove(node) {
25 | return {
26 | type: UPDATE_TYPES.REMOVE,
27 | fromNode: node,
28 | }
29 | }
30 | }
31 |
32 | module.exports = {
33 | UPDATE_TYPES,
34 | OPERATIONS
35 | }
--------------------------------------------------------------------------------
/demo/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "dilithium-demo",
3 | "version": "v0.0.1",
4 | "scripts": {
5 | "build": "webpack --progress --colors",
6 | "watch": "webpack --progress --colors --watch"
7 | },
8 | "dependencies": {
9 | "babel-core": "^6.13.2",
10 | "babel-loader": "^6.2.5",
11 | "babel-plugin-transform-class-properties": "^6.11.5",
12 | "babel-plugin-transform-react-jsx": "^6.8.0",
13 | "webpack": "^1.13.2"
14 | },
15 | "babel": {
16 | "plugins": [
17 | [
18 | "transform-react-jsx",
19 | {
20 | "pragma": "Dilithium.createElement"
21 | }
22 | ],
23 | "transform-class-properties"
24 | ]
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/dilithium/webpack.config.js:
--------------------------------------------------------------------------------
1 | let webpack = require('webpack');
2 | let CircularDependencyPlugin = require('circular-dependency-plugin')
3 |
4 | module.exports = {
5 | entry: './index.js',
6 | output: {
7 | path: __dirname,
8 | filename: 'build/Dilithium.js',
9 | libraryTarget: 'var',
10 | library: 'Dilithium'
11 | },
12 | module: {
13 | loaders: [
14 | {
15 | test: /\.js$/,
16 | loader: 'babel',
17 | query: {
18 | plugins: [
19 | ['transform-react-jsx', {pragma: 'Dilithium.createElement'}],
20 | 'transform-class-properties'
21 | ]
22 | }
23 | }
24 | ]
25 | },
26 | plugins: [
27 | new webpack.optimize.OccurenceOrderPlugin(),
28 | new CircularDependencyPlugin(),
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/demo/app.js:
--------------------------------------------------------------------------------
1 | const Dilithium = require('../dilithium')
2 |
3 | class App extends Dilithium.Component {
4 | render() {
5 | return (
6 |
7 |
Heading 3
8 |
9 |
10 | )
11 | }
12 | }
13 |
14 | class SmallHeaderWithState extends Dilithium.Component {
15 | constructor() {
16 | super()
17 | this.state = { number: 0 }
18 | setInterval(() => {
19 | this.setState({
20 | number: this.state.number + 1
21 | })
22 | }, 1000)
23 | }
24 |
25 | render() {
26 | return (
27 |
28 |
SmallHeader
32 | { this.state.number }
33 |
34 | )
35 | }
36 | }
37 |
38 | Dilithium.render(, document.getElementById('root'))
39 |
--------------------------------------------------------------------------------
/dilithium/src/instantiateComponent.js:
--------------------------------------------------------------------------------
1 | const DOMComponent = require('./DOMComponent')
2 |
3 | /**
4 | * element => component
5 | */
6 |
7 | function instantiateComponent(element) {
8 | let componentInstance
9 |
10 | if (typeof element.type === 'function') {
11 | // todo: add functional component
12 | // only supports class component for now
13 | componentInstance = new element.type(element.props)
14 | componentInstance._construct(element)
15 | } else if (typeof element.type === 'string') {
16 | componentInstance = new DOMComponent(element)
17 | } else if (typeof element === 'string' || typeof element === 'number') {
18 | componentInstance = new DOMComponent({
19 | type: 'span',
20 | props: { children: element }
21 | })
22 | }
23 |
24 | return componentInstance
25 | }
26 |
27 | module.exports = instantiateComponent
28 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "8"
5 |
6 | # 缓存依赖
7 | cache:
8 | directories:
9 | - $HOME/.npm
10 |
11 | before_install:
12 | - export TZ='Asia/Shanghai' # 更改时区
13 |
14 | # 依赖安装
15 | install:
16 | - npm install -g gitbook-cli gitbook-summary
17 |
18 | # 构建脚本
19 | script:
20 | # 自定义输出目录 gitbook build src dest
21 | - cd ./blog
22 | - gitbook install
23 | - book sm
24 | - cat SUMMARY.md
25 | - gitbook build . ../blog-build
26 |
27 | # 分支白名单
28 | branches:
29 | only:
30 | - master # 只对 master 分支进行构建
31 |
32 | # GitHub Pages 部署
33 | deploy:
34 | provider: pages
35 | skip_cleanup: true
36 | # 在项目仪表盘的 Settings -> Environment Variables 中配置
37 | github_token: $GITHUB_TOKEN
38 | # 将 build 目录下的内容推送到默认的 gh-pages 分支上,并不会连带 build 目录一起
39 | local_dir: blog-build
40 | name: $GIT_NAME
41 | email: $GIT_EMAIL
42 | on:
43 | branch: master
44 |
--------------------------------------------------------------------------------
/dilithium/src/traverseAllChildren.js:
--------------------------------------------------------------------------------
1 | const SEPARATOR = '.'
2 | const SUBSEPARATOR = ':'
3 |
4 | function getComponentKey(component, index) {
5 | // This is where we would use the key prop to generate a unique id that
6 | // persists across moves. However we're skipping that so we'll just use the
7 | // index.
8 | return index.toString(36)
9 | }
10 |
11 | function traverseAllChildren(children, callback, traverseContext) {
12 | return traverseAllChildrenImpl(children, '', callback, traverseContext)
13 | }
14 |
15 | function traverseAllChildrenImpl(
16 | children,
17 | nameSoFar,
18 | callback,
19 | traverseContext
20 | ) {
21 | // single child
22 | if (
23 | typeof children === 'string' ||
24 | typeof children === 'number' ||
25 | !Array.isArray(children)
26 | ) {
27 | callback(
28 | traverseContext,
29 | children,
30 | nameSoFar + SEPARATOR + getComponentKey(children, 0)
31 | )
32 | return 1
33 | }
34 |
35 | let subtreeCount = 0
36 | const namePrefix = !nameSoFar ? SEPARATOR : nameSoFar + SUBSEPARATOR
37 |
38 | children.forEach((child, i) => {
39 | subtreeCount += traverseAllChildrenImpl(
40 | child,
41 | namePrefix + getComponentKey(child, i),
42 | callback,
43 | traverseContext
44 | )
45 | })
46 |
47 | return subtreeCount
48 | }
49 |
50 | module.exports = traverseAllChildren
51 |
--------------------------------------------------------------------------------
/dilithium/src/DOM.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A set of DOM helper functions
3 | */
4 |
5 | function empty(node) {
6 | [].slice.call(node, node.childNodes).forEach(node.removeChild, node)
7 | }
8 |
9 | function updateStyles(node, styleObj) {
10 | Object.keys(styleObj).forEach((styleName) => {
11 | node.style[styleName] = styleObj[styleName]
12 | })
13 | }
14 |
15 | function setProperty(node, attr, value) {
16 | if (attr === 'children') return
17 | node.setAttribute(attr, value)
18 | }
19 |
20 | function removeProperty(node, attr) {
21 | node.removeAttribute(attr);
22 | }
23 |
24 | function appendChildren(node, children) {
25 | if (Array.isArray(children)) {
26 | children.forEach((child) => node.appendChild(child))
27 | } else {
28 | node.appendChild(children)
29 | }
30 | }
31 |
32 | function removeChild(node, child) {
33 | node.removeChild(child)
34 | }
35 |
36 | function insertAfter(node, child, afterChild) {
37 | node.insertBefore(
38 | child,
39 | afterChild ? afterChild.nextSibling : node.firstChild
40 | )
41 | }
42 |
43 | function replaceNode(prevNode, newNode) {
44 | const parentNode = prevNode.parentNode
45 | empty(parentNode)
46 | parentNode.appendChild(newNode)
47 | }
48 |
49 | module.exports = {
50 | empty,
51 | setProperty,
52 | removeProperty,
53 | appendChildren,
54 | removeChild,
55 | insertAfter,
56 | updateStyles,
57 | replaceNode,
58 | }
59 |
--------------------------------------------------------------------------------
/blog/README.md:
--------------------------------------------------------------------------------
1 | # Learn React Source Code
2 |
3 | * [Day1 - Guidance](guidance.md)
4 | * [Day2 - Mounting](mounting.md)
5 | * [Day3 - Mounting - Contd](mounting-contd.md)
6 | * [Day4 - Updating - The Process Flow](update.md)
7 | * [Day5 - Updating - Diff](update-contd.md)
8 | * [Day6 - Updating - Real DOM Update](update-dom.md)
9 |
10 | ## What You'll Learn
11 |
12 | * React 是怎样将 JSX mount 成为真正的 DOM 节点的
13 | * React 是怎样用 Virtual DOM 的 Diff 算法更新 Element tree,然后映射到真正的 DOM 变化的
14 | * 什么是 Virtual DOM,它的优势是什么,以及它和 React 是怎样结合使用的
15 | * 对 React 的核心功能有一个更深入的理解
16 |
17 | ## What This Doesn't Cover
18 |
19 | 由于这是一个 React 的最小实现,它并没有实现 React 的全部功能,以下这些功能是这个代码库没有涵盖到的。(这个 list 在 Paul 2016 的演讲中被提及到)
20 |
21 | * `defaultProps`
22 | * `propTypes`
23 | * `keys`
24 | * `refs`
25 | * batching
26 | * events
27 | * createClass
28 | * warnings
29 | * browser
30 | * optimizations
31 | * rendering null
32 | * DOM updates
33 | * SVG
34 | * life cycle hooks
35 | * error boundaries
36 | * perf tooling and optimizing
37 | * `PureComponents`
38 | * functional components
39 |
40 | 但是当你读完整个博客和代码后,相信你已经会有对实现这其中的几个功能的一些初步思考。
41 |
42 | ## Run the Demo
43 |
44 | ```sh
45 | > cd ./demo
46 | > npm install
47 | > npm run watch
48 | ```
49 |
50 | Open the `index.html` manually.
51 |
52 | ## Disclaimers
53 |
54 | 1. Most code of Dilithium you've seen in this repo is originally written by [@zpao](https://github.com/zpao), at [building-react-from-scratch](https://github.com/zpao/building-react-from-scratch), but it's also slightly changed here. I'll keep digging some of the listed features and adding blog and source code on top of the current codebase.
55 |
56 | 2. The diffing algorithm used in the Dilithium is the stack reconcilliation, not the new fiber architecture.
57 |
58 | 3. The code snippets in the blogs are sometimes somewhat different from that in the codebase. This is for better readablity and a more smooth learning curve.
59 |
60 | ## Liscense
61 |
62 | MIT[@Chang](github.com/cyan33)
63 |
--------------------------------------------------------------------------------
/blog/book.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Learn React Source Code",
3 | "author": "cyan33",
4 | "links": {
5 | "sidebar": {
6 | "Learn React Source Code": "https://github.com/cyan33/learn-react-source-code"
7 | }
8 | },
9 | "plugins": [
10 | "-lunr",
11 | "-search",
12 | "search-plus",
13 | "splitter",
14 | "anchors",
15 | "anchor-navigation-ex",
16 | "copy-code-button",
17 | "expandable-chapters",
18 | "theme-darkblue",
19 | "theme-comscore",
20 | "tbfed-pagefooter",
21 | "prism",
22 | "-highlight",
23 | "github-buttons",
24 | "edit-link",
25 | "disqus"
26 | ],
27 | "pluginsConfig": {
28 | "tbfed-pagefooter": {
29 | "modify_label": "最近更新:",
30 | "modify_format": "YYYY-MM-DD HH:mm"
31 | },
32 | "prism": {
33 | "css": [
34 | "prismjs/themes/prism-tomorrow.css"
35 | ]
36 | },
37 | "github-buttons": {
38 | "buttons": [{
39 | "user": "cyan33",
40 | "repo": "learn-react-source-code",
41 | "type": "star",
42 | "size": "small",
43 | "count": true
44 | }]
45 | },
46 | "edit-link": {
47 | "base": "https://github.com/cyan33/learn-react-source-code/edit/master/blog",
48 | "label": "Edit This Page"
49 | },
50 | "sharing": {
51 | "facebook": false,
52 | "twitter": true,
53 | "weibo": true
54 | },
55 | "disqus": {
56 | "shortName": "gitbook-8"
57 | },
58 | "anchor-navigation-ex": {
59 | "multipleH1": false
60 | }
61 | },
62 | "ignores": [
63 | ".*",
64 | "build",
65 | "_book",
66 | "gitbook",
67 | "node_modules"
68 | ]
69 | }
70 |
--------------------------------------------------------------------------------
/blog/guidance.md:
--------------------------------------------------------------------------------
1 | # Guidance
2 |
3 | 要阅读 React 源码,你并不需要是一个非常有经验的 React 开发者。在学习了半个月之后,我个人认为,你甚至不需要会 React,理论上依旧也可以达到目的。但是,对 React 有一个良好的使用经验可以在你阅读源码的时候起到一个对照和辅助作用,这一点是十分有帮助的。
4 |
5 | 在阅读 React 源码之前,最重要的一个概念是搞明白这个问题:
6 |
7 | **在 React 里,Component, Element, Instance 之间有什么区别和联系?**
8 |
9 | Answer:
10 |
11 | - **Element** 是一个纯 JavaScript Object。React 正是使用它来描述整个 DOM 树的结构。对于每一个组件来说,render 函数返回的也正是一个 element,而不是真正的 DOM 节点。我们使用的 JSX,其实是 React.createElement 函数的语法糖,这个函数返回的是 Element。它的结构是:
12 |
13 | ```js
14 | {
15 | type: 'div',
16 | props: {
17 | className,
18 | children,
19 | }
20 | }
21 | ```
22 |
23 | 其中,children 的数据结构是 Element 的数组,或者单个 Element。由此,Element 的数据结构满足了递归的需要。
24 |
25 | - **Component** 是我们最常写的“组件”,有以下几种类型:
26 | - DOMComponent: `div, span, ul` 等
27 | - CompositeComponent: 复合组件,又分为 functional component 和 class component
28 | - TextComponent: number or string
29 |
30 | 由于我们在使用 React 实现组件化的时候,使用的有且只有 CompositeComponent,所以,我们的每一个组件,其实都是一个 Component。但是当 React 试图去 render 这些组件的时候,**会将 Element 转化成 Component,进而转化成真正的 DOM 节点**。
31 |
32 | - **Instance** 是 Component 的实例化之后的对象,我们在 Component 中使用的 `this` 就是 instance。这也是 `setState` 和诸多生命周期函数所在的地方。从这一点出发,可以把 Component 理解为 Class,instance是实例化后的结果。
33 |
34 | 在解决这个问题之后,遇到 React 代码里的函数名和参数中带有 "element", "component" 的,一定要自动条件反射到对应的概念,比如说 `instantiateComponent`, `mountComponent`,`createElement`,等等。
35 |
36 | 除此之外,如果你还没有信心直接开始阅读源码,建议(按次序)阅读以下三篇官方的 React Advanced Guide。对于理解 React 的架构和一些重要概念很有帮助。
37 |
38 | [JSX in Depth](https://reactjs.org/docs/jsx-in-depth.html)
39 |
40 | [Implementation Details](https://reactjs.org/docs/implementation-notes.html)
41 |
42 | [Reconciliation](https://reactjs.org/docs/reconciliation.html)
43 |
44 | ## Ask yourself Before Move On
45 |
46 | 在开始之前,为了检查一下自己的学习成果,不妨问一下自己这几个问题:
47 |
48 | - What is the difference between components, elements, instances in React
49 | - How does React use the element tree, instead of instances to compose the DOM structure
50 | - What is the advantage(s) of using the element tree
51 | - How does the React recursively work out a final DOM tree from a mixture of DOM components and React components during the render process
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [Learn React Source Code](https://dragonforker.github.io/learn-react-source-code)
2 |
3 | * [Day1 - Guidance](blog/guidance.md)
4 | * [Day2 - Mounting](blog/mounting.md)
5 | * [Day3 - Mounting - Contd](blog/mounting-contd.md)
6 | * [Day4 - Updating - The Process Flow](blog/update.md)
7 | * [Day5 - Updating - Diff](blog/update-contd.md)
8 | * [Day6 - Updating - Real DOM Update](blog/update-dom.md)
9 |
10 | [Read it Online](https://dragonforker.github.io/learn-react-source-code)
11 |
12 | ## What You'll Learn
13 |
14 | * React 是怎样将 JSX mount 成为真正的 DOM 节点的
15 | * React 是怎样用 Virtual DOM 的 Diff 算法更新 Element tree,然后映射到真正的 DOM 变化的
16 | * 什么是 Virtual DOM,它的优势是什么,以及它和 React 是怎样结合使用的
17 | * 对 React 的核心功能有一个更深入的理解
18 |
19 | ## What This Doesn't Cover
20 |
21 | 由于这是一个 React 的最小实现,它并没有实现 React 的全部功能,以下这些功能是这个代码库没有涵盖到的。(这个 list 在 Paul 2016 的演讲中被提及到)
22 |
23 | * `defaultProps`
24 | * `propTypes`
25 | * `keys`
26 | * `refs`
27 | * batching
28 | * events
29 | * createClass
30 | * warnings
31 | * browser
32 | * optimizations
33 | * rendering null
34 | * DOM updates
35 | * SVG
36 | * life cycle hooks
37 | * error boundaries
38 | * perf tooling and optimizing
39 | * `PureComponents`
40 | * functional components
41 |
42 | 但是当你读完整个博客和代码后,相信你已经会有对实现这其中的几个功能的一些初步思考。
43 |
44 | ## Run the Demo
45 |
46 | ```sh
47 | > cd ./demo
48 | > npm install
49 | > npm run watch
50 | ```
51 |
52 | Open the `index.html` manually.
53 |
54 | ## Disclaimers
55 |
56 | 1. Most code of Dilithium you've seen in this repo is originally written by [@zpao](https://github.com/zpao), at [building-react-from-scratch](https://github.com/zpao/building-react-from-scratch), but it's also slightly changed here. I'll keep digging some of the listed features and adding blog and source code on top of the current codebase.
57 |
58 | 2. The diffing algorithm used in the Dilithium is the stack reconcilliation, not the new fiber architecture.
59 |
60 | 3. The code snippets in the blogs are sometimes somewhat different from that in the codebase. This is for better readablity and a more smooth learning curve.
61 |
62 | ## Liscense
63 |
64 | MIT[@Chang](github.com/cyan33)
65 |
--------------------------------------------------------------------------------
/dilithium/src/Component.js:
--------------------------------------------------------------------------------
1 | const assert = require('./assert')
2 | const shouldUpdateComponent = require('./shouldUpdateComponent')
3 | const instantiateComponent = require('./instantiateComponent')
4 | const Reconciler = require('./Reconciler')
5 |
6 | class Component {
7 | constructor(props) {
8 | this.props = props
9 | this._renderedComponent = null
10 | this._renderedNode = null
11 | this._currentElement = null
12 | this._pendingState = null
13 | assert(this.render)
14 | }
15 |
16 | setState(partialState) {
17 | this._pendingState = Object.assign({}, this.state, partialState)
18 | this.performUpdateIfNecessary()
19 | }
20 |
21 | _construct(element) {
22 | this._currentElement = element
23 | }
24 |
25 | mountComponent() {
26 | // we simply assume the render method returns a single element
27 | let renderedElement = this.render()
28 |
29 | let renderedComponent = instantiateComponent(renderedElement)
30 | this._renderedComponent = renderedComponent
31 |
32 | let renderedNode = Reconciler.mountComponent(renderedComponent)
33 | this._renderedNode = renderedNode
34 |
35 | return renderedNode
36 | }
37 |
38 | unmountComponent() {
39 | if (!this._renderedComponent) return
40 |
41 | // call componentWillUnmount()
42 |
43 | // delegate the unmounting process to the rendered component
44 | Reconciler.unmountComponent(this._renderedComponent)
45 | }
46 |
47 | updateComponent(prevElement, nextElement) {
48 | if (prevElement !== nextElement) {
49 | // should get re-render because of the changes of props passed down from parents
50 | // react calls componentWillReceiveProps here
51 | }
52 |
53 | // re-bookmarking
54 | this._currentElement = nextElement
55 |
56 | this.props = nextElement.props
57 | this.state = this._pendingState
58 | this._pendingState = null
59 |
60 | let prevRenderedElement = this._renderedComponent._currentElement
61 | let nextRenderedElement = this.render()
62 |
63 | if (shouldUpdateComponent(prevRenderedElement, nextRenderedElement)) {
64 | Reconciler.receiveComponent(this._renderedComponent, nextRenderedElement)
65 | } else {
66 | // re-mount everything from this point
67 | Reconciler.unmountComponent(this._renderedComponent)
68 |
69 | const nextRenderedComponent = instantiateComponent(nextElement)
70 | this._renderedNode = Reconciler.mountComponent(nextRenderedComponent)
71 | DOM.replaceNode(this._renderedComponent._domNode, this._renderedNode)
72 | }
73 | }
74 |
75 | performUpdateIfNecessary() {
76 | // react uses a batch here, we are just gonna call it directly without delay
77 | this.updateComponent(this._currentElement, this._currentElement)
78 | }
79 | }
80 |
81 | module.exports = Component
82 |
--------------------------------------------------------------------------------
/dilithium/src/ChildReconciler.js:
--------------------------------------------------------------------------------
1 | const traverseAllChildren = require('./traverseAllChildren')
2 | const shouldUpdateComponent = require('./shouldUpdateComponent')
3 | const Reconciler = require('./Reconciler')
4 |
5 | function instantiateChild(childInstances, child, name) {
6 | // don't know wtf happened here, cannot resolve it at top level
7 | // hack it in
8 | const instantiateComponent = require('./instantiateComponent')
9 |
10 | if (!childInstances[name]) {
11 | childInstances[name] = instantiateComponent(child)
12 | }
13 | }
14 |
15 | function instantiateChildren(children) {
16 | let childInstances = {}
17 |
18 | traverseAllChildren(children, instantiateChild, childInstances)
19 |
20 | return childInstances
21 | }
22 |
23 | function unmountChildren(renderedChildren) {
24 | if (!renderedChildren) return
25 |
26 | Object.keys(renderedChildren).forEach(childKey => {
27 | Reconciler.unmountComponent(renderedChildren[childKey])
28 | })
29 | }
30 |
31 | function updateChildren(
32 | prevChildren, // instance tree
33 | nextChildren, // element tree
34 | mountNodes,
35 | removedNodes
36 | ) {
37 | // hack in the import function
38 | const instantiateComponent = require('./instantiateComponent')
39 |
40 | // we use the index of the tree to track the updates of the component, like `0.0`
41 | Object.keys(nextChildren).forEach((childKey) => {
42 | const prevChildComponent = prevChildren[childKey]
43 | const prevElement = prevChildComponent && prevChildComponent._currentElement
44 | const nextElement = nextChildren[childKey]
45 |
46 | // three scenarios:
47 | // 1: the prev element exists and is of the same type as the next element
48 | // 2: the prev element exists but not of the same type
49 | // 3: the prev element doesn't exist
50 |
51 | if (prevElement && shouldUpdateComponent(prevElement, nextElement)) {
52 | // this will do the recursive update of the sub tree
53 | // and this line is basically the actual update
54 | Reconciler.receiveComponent(prevChildComponent, nextElement)
55 | // and we do not need the new element
56 | // note that we are converting the `nextChildren` object from an
57 | // element tree to a component instance tree during all this process
58 | nextChildren[childKey] = prevChildComponent
59 | } else {
60 | // otherwise, we need to do the unmount and re-mount stuff
61 | if (prevChildComponent) {
62 | // only supports DOM node for now, should add composite component
63 | removedNodes[childKey] = prevChildComponent._domNode
64 | Reconciler.unmountComponent(prevChildComponent)
65 | }
66 |
67 | // instantiate the new child. (insert)
68 | const nextComponent = instantiateComponent(nextElement)
69 | nextChildren[childKey] = nextComponent
70 |
71 | mountNodes.push(Reconciler.mountComponent(nextComponent))
72 | }
73 | })
74 |
75 | // last but not least, remove the old children which no longer exist
76 | Object.keys(prevChildren).forEach((childKey) => {
77 | if (!nextChildren.hasOwnProperty(childKey)) {
78 | const prevChildComponent = prevChildren[childKey]
79 | removedNodes[childKey] = prevChildComponent
80 | Reconciler.unmountComponent(prevChildComponent)
81 | }
82 | })
83 | }
84 |
85 | module.exports = {
86 | instantiateChildren,
87 | unmountChildren,
88 | updateChildren,
89 | }
90 |
--------------------------------------------------------------------------------
/dilithium/src/DOMComponent.js:
--------------------------------------------------------------------------------
1 | const MultiChild = require('./MultiChild')
2 | const DOM = require('./DOM')
3 | const assert = require('./assert')
4 |
5 | class DOMComponent extends MultiChild {
6 | constructor(element) {
7 | super()
8 | this._currentElement = element
9 | this._domNode = null
10 | }
11 |
12 | mountComponent() {
13 | // create real dom nodes
14 | const node = document.createElement(this._currentElement.type)
15 | this._domNode = node
16 |
17 | this._updateNodeProperties({}, this._currentElement.props)
18 | this._createInitialDOMChildren(this._currentElement.props)
19 |
20 | return node
21 | }
22 |
23 | unmountComponent() {
24 | this.unmountChildren()
25 | }
26 |
27 | updateComponent(prevElement, nextElement) {
28 | this._currentElement = nextElement
29 | this._updateNodeProperties(prevElement.props, nextElement.props)
30 | this._updateDOMChildren(prevElement.props, nextElement.props)
31 | }
32 |
33 | _updateNodeProperties(prevProps, nextProps) {
34 | let styleUpdates = {}
35 |
36 | // Loop over previous props so we know what we need to remove
37 | Object.keys(prevProps).forEach((propName) => {
38 | if (propName === 'style') {
39 | Object.keys(prevProps['style']).forEach((styleName) => {
40 | styleUpdates[styleName] = ''
41 | })
42 | } else {
43 | DOM.removeProperty(this._domNode, propName)
44 | }
45 | })
46 |
47 | // update / add new attributes
48 | Object.keys(nextProps).forEach((propName) => {
49 | let prevValue = prevProps[propName]
50 | let nextValue = nextProps[propName]
51 |
52 | if (prevValue === nextValue) return
53 |
54 | if (propName === 'style') {
55 | Object.keys(nextProps['style']).forEach((styleName) => {
56 | // overwrite the existing styles
57 | styleUpdates[styleName] = nextProps.style[styleName]
58 | })
59 | } else {
60 | DOM.setProperty(this._domNode, propName, nextProps[propName])
61 | }
62 | })
63 |
64 | DOM.updateStyles(this._domNode, styleUpdates)
65 | }
66 |
67 | _createInitialDOMChildren(props) {
68 | // this is where we go into the children of the dom component and
69 | // recursively mount and append each of the childNode to the parent node
70 | if (
71 | typeof props.children === 'string' ||
72 | typeof props.children === 'number'
73 | ) {
74 | const textNode = document.createTextNode(props.children)
75 | this._domNode.appendChild(textNode)
76 | } else if (props.children) {
77 | // Single element or Array
78 | const childrenNodes = this.mountChildren(props.children)
79 | DOM.appendChildren(this._domNode, childrenNodes)
80 | }
81 | }
82 |
83 | _updateDOMChildren(prevProps, nextProps) {
84 | const prevType = typeof prevProps.children
85 | const nextType = typeof nextProps.children
86 | assert(prevType === nextType)
87 |
88 | // Childless node, skip
89 | if (nextType === 'undefined') return
90 |
91 | // Much like the initial step in mounting, handle text differently than elements.
92 | if (nextType === 'string' || nextType === 'number') {
93 | this._domNode.textContent = nextProps.children
94 | } else {
95 | this.updateChildren(nextProps.children)
96 | }
97 | }
98 | }
99 |
100 | module.exports = DOMComponent
101 |
--------------------------------------------------------------------------------
/dilithium/src/MultiChild.js:
--------------------------------------------------------------------------------
1 | const ChildReconciler = require('./ChildReconciler')
2 | const Reconciler = require('./Reconciler')
3 | const { UPDATE_TYPES, OPERATIONS } = require('./operations')
4 | const traverseAllChildren = require('./traverseAllChildren')
5 | const DOM = require('./DOM')
6 |
7 | function flattenChildren(children) {
8 | const flattenedChildren = {}
9 | traverseAllChildren(
10 | children,
11 | (context, child, name) => context[name] = child,
12 | flattenedChildren
13 | )
14 | return flattenedChildren
15 | }
16 |
17 | // this is responsible for the real updates of the diffing tree
18 | function processQueue(parentNode, updates) {
19 | updates.forEach(update => {
20 | switch (update.type) {
21 | case UPDATE_TYPES.INSERT:
22 | DOM.insertAfter(parentNode, update.content, update.afterNode)
23 | break
24 |
25 | case UPDATE_TYPES.MOVE:
26 | // this automatically removes and inserts the new child
27 | DOM.insertAfter(
28 | parentNode,
29 | update.content,
30 | update.afterNode
31 | )
32 | break
33 |
34 | case UPDATE_TYPES.REMOVE:
35 | DOM.removeChild(parentNode, update.fromNode)
36 | break
37 |
38 | default:
39 | assert(false)
40 | }
41 | })
42 | }
43 |
44 | class MultiChild {
45 | constructor() {
46 | this._renderedChildren = null
47 | }
48 |
49 | mountChildren(children) {
50 | // children elements => children nodes
51 | const childrenComponents = ChildReconciler.instantiateChildren(children)
52 | this._renderedChildren = childrenComponents
53 |
54 | /*
55 | {
56 | '.0.0': {_currentElement, ...}
57 | '.0.1': {_currentElement, ...}
58 | }
59 | */
60 |
61 | const childrenNodes = Object.keys(childrenComponents).map((childKey, i) => {
62 | const childComponent = childrenComponents[childKey]
63 |
64 | childComponent._mountIndex = i
65 |
66 | return Reconciler.mountComponent(childComponent)
67 | })
68 |
69 | return childrenNodes
70 | }
71 |
72 | unmountChildren() {
73 | ChildReconciler.unmountChildren(this._renderedChildren)
74 | }
75 |
76 | updateChildren(nextChildren) {
77 | // component tree
78 | let prevRenderedChildren = this._renderedChildren
79 | // element tree
80 | let nextRenderedChildren = flattenChildren(nextChildren)
81 |
82 | let mountNodes = []
83 | let removedNodes = {}
84 |
85 | ChildReconciler.updateChildren(
86 | prevRenderedChildren,
87 | nextRenderedChildren,
88 | mountNodes,
89 | removedNodes
90 | )
91 | // We'll compare the current set of children to the next set.
92 | // We need to determine what nodes are being moved around, which are being
93 | // inserted, and which are getting removed. Luckily, the removal list was
94 | // already determined by the ChildReconciler.
95 |
96 | // We'll generate a series of update operations here based on the
97 | // bookmarks that we've made just now
98 | let updates = []
99 |
100 | let lastIndex = 0
101 | let nextMountIndex = 0
102 | let lastPlacedNode = null
103 |
104 | Object.keys(nextRenderedChildren).forEach((childKey, nextIndex) => {
105 | let prevChild = prevRenderedChildren[childKey]
106 | let nextChild = nextRenderedChildren[childKey]
107 |
108 | // mark this as an update if they are identical
109 | if (prevChild === nextChild) {
110 | // We don't actually need to move if moving to a lower index.
111 | // Other operations will ensure the end result is correct.
112 | if (prevChild._mountIndex < lastIndex) {
113 | updates.push(OPERATIONS.move(nextChild, lastPlacedNode))
114 | }
115 |
116 | lastIndex = Math.max(prevChild._mountIndex, lastIndex)
117 | prevChild._mountIndex = nextIndex
118 | } else {
119 | // Otherwise we need to record an insertion.
120 | // First, if we have a prevChild then we know it's a removal.
121 | // We want to update lastIndex based on that.
122 | if (prevChild) {
123 | lastIndex = Math.max(prevChild._mountIndex, lastIndex)
124 | }
125 |
126 | nextChild._mountIndex = nextIndex
127 | updates.push(
128 | OPERATIONS.insert(
129 | mountNodes[nextMountIndex],
130 | lastPlacedNode
131 | )
132 | )
133 | nextMountIndex ++
134 | }
135 |
136 | // keep track of lastPlacedNode
137 | lastPlacedNode = nextChild._domNode
138 | })
139 |
140 | // enque the removal the non-exsiting nodes
141 | Object.keys(removedNodes).forEach((childKey) => {
142 | updates.push(
143 | OPERATIONS.remove(removedNodes[childKey])
144 | )
145 | })
146 |
147 | // do the actual updates
148 | processQueue(this._domNode, updates)
149 |
150 | // at this point, nextRenderedChildren has already become a component tree
151 | // rather than the original element tree
152 | this._renderedChildren = nextRenderedChildren
153 | }
154 | }
155 |
156 | module.exports = MultiChild
157 |
--------------------------------------------------------------------------------
/blog/mounting-contd.md:
--------------------------------------------------------------------------------
1 | # Mounting - Contd
2 |
3 | 在上一节中,我们了解到,React 所有的复合组件(包括 class component 和 functional component)的 mounting 全部 defer 到了 DOM Component 的 mounting 中。并且,在 DOM Component 中的 `mountComponent` 方法中,我们留下了两个问题。
4 |
5 | ```js
6 | mountComponent() {
7 | // create real dom nodes
8 | const node = document.createElement(this._currentElement.type)
9 | this._domNode = node
10 |
11 | this._updateNodeProperties({}, this._currentElement.props)
12 | this._createInitialDOMChildren(this._currentElement.props)
13 |
14 | return node
15 | }
16 | ```
17 |
18 | 第一,怎样将当前 element 的 `props` 属性映射到当前 DOM 节点的属性?
19 |
20 | 第二,React 是怎样递归 mount 子组件的?
21 |
22 | 本篇博客主要讲解这两个问题。
23 |
24 | ## `updateNodeProperties`
25 |
26 | 我们回顾一下 `props` 的来源和数据结构。首先,`props` 是从 JSX 中来的:
27 |
28 | ```jsx
29 |
38 | Hello World
39 |
40 | ```
41 |
42 | 编译后的结果是:
43 |
44 | ```js
45 | React.createElement(
46 | 'div',
47 | {
48 | className: 'container',
49 | style: {
50 | color: 'red',
51 | fontSize: '24px'
52 | }
53 | },
54 | 'Hello World'
55 | );
56 | ```
57 |
58 | 运行 `createElement` 后,最终返回值,也就是 Element,变成了这样的数据结构:
59 |
60 | ```js
61 | {
62 | type: 'div',
63 | props: {
64 | className: 'container',
65 | children: 'Hello World',
66 | style: {
67 | color: 'red',
68 | fontSize: '24px'
69 | }
70 | },
71 | }
72 | ```
73 |
74 | 可以看出,`props` 就是一个**不完全的**和HTML属性之间的映射。为什么说是**不完全**呢?有以下两个原因:
75 |
76 | 1. 有些属性并不是 DOM 属性,也不会被挂载在 DOM 上。比如 `children`。
77 | 2. `props` 的属性名和 HTML 的 property 并不存在一一对应的关系。比如说 `className` 对应的应该是 `class`。
78 |
79 | 除此之外,我们还应该考虑很重要的一点,那就是当组件更新,`props.style` 中的更新方式应该是怎样的呢?(这一部分本应放在 `updating` 再讲,但是为了整个函数的连贯性,我们在此一并讲完。)举个例子:
80 |
81 | 当一个组件的 `style` 由
82 |
83 | ```js
84 | {
85 | fontSize: '36px'
86 | }
87 | ```
88 |
89 | 变为
90 |
91 | ```js
92 | {
93 | color: 'red'
94 | }
95 | ```
96 |
97 | 的时候,我们不仅应该设置 `color: red`,而且应该讲之前的 `fontSize` 去除,恢复为默认值。
98 |
99 | 总而言之,用一句话概括 `updateNodeProperties` 的过程:**先重置之前的 props,再设置新的 props**
100 |
101 | 代码如下(为了简化整个过程,我们忽略了第二点):
102 |
103 | ```js
104 | function updateNodeProperties(prevProps, nextProps) {
105 | let styleUpdates = {}
106 |
107 | Object.keys(prevProps).forEach((propName) => {
108 | if (propName === 'style') {
109 | Object.keys(prevProps.style).forEach((styleName) => {
110 | styleUpdates[styleName] = ''
111 | })
112 | } else {
113 | DOM.removeProperty(this._domNode, propName)
114 | }
115 | })
116 |
117 | Object.keys(nextProps).forEach((propName) => {
118 | if (propName === 'style') {
119 | Object.keys(nextProps.style).forEach((styleName) => {
120 | styleUpdates[styleName] = nextProps.style[styleName]
121 | })
122 | } else {
123 | DOM.setProperty(this._domNode, propName, nextProps[propName])
124 | }
125 | })
126 |
127 | // update styles based on the `styleUpdates` object
128 | updateStyles(this._domNode, styleUpdates)
129 | }
130 |
131 | function updateStyles(node, style) {
132 | Object.keys(style).forEach((styleName) => {
133 | node.style[styleName] = style[styleName]
134 | })
135 | }
136 | ```
137 |
138 | ## `createInitialDOMChildren`
139 |
140 | 在设置好最外层 DOM 节点的属性后,剩下的任务是将遍历 `props.children` 并 mount 每一个子节点,并且 append 到当前的 DOM 节点上。在上节我们提到,借助于 Reconciller 的**多态**,我们统一了 React 各类组件的接口,其中之一就是 `mountComponent` 这个方法。不管是什么类型的组件,调用这个方法都会返回对应的真正的 DOM 节点。这样一来,`createInitialDOMChildren` 就很好实现了。
141 |
142 | 不考虑到之后的 update,我们的第一想法或许是这样的:
143 |
144 | ```js
145 | _createInitialDOMChildren(props) {
146 | if (
147 | typeof props.children === 'string' ||
148 | typeof props.children === 'number'
149 | ) {
150 | const textNode = document.createTextNode(props.children)
151 | this._domNode.appendChild(textNode)
152 | } else if (props.children) {
153 | const children = Array.isArray(props.children) ? props.children : [props.children]
154 | children.forEach((child, i) => {
155 | // element => component
156 | const childComponent = instantiateComponent(child)
157 | childComponent._mountIndex = i
158 | // component => DOM node
159 | const childNode = Reconciler.mountComponent(childComponent)
160 |
161 | DOM.appendChildren(this._domNode, childrenNodes)
162 | })
163 | }
164 | }
165 | ```
166 |
167 | 到此为止我们实现了 mounting 的操作。
168 |
169 | 让我们来想一下这样做的优劣。
170 |
171 | 优点是显而易见的,直观明了,没有多余的操作。但是缺点却非常致命,每次 mount 之后,我们并没有**保存对 mount 节点的信息**,这就使之后 Virtual DOM 的 Diff 实现变得无从下手。事实上,React 并不是简单地像上文这样 mount component,与此同时,还在这个过程中生成了一个 hash tree。
172 |
173 | `DOMComponent` 继承了 `MultiChild`,关于 mounting 和 update 的大部分复杂的操作都在在这个类里面,例如在这个过程中调用的 `mountChildren`。从源码中看出,与上面我们写的 `_createInitialChildren` 细微的差别是,源码中并没有简单的使用 `forEach` 直接遍历,而是使用了一个函数,叫做 `traverseAllChildren`,利用这个方法,在每次 mounting 和 update 的过程中,得以以一种附加 callback 的方式遍历所有子节点,并返回上文我们说的 hash tree。如果你有兴趣,可以阅读:
174 |
175 | [DOMComponent.js](../dilithium/src/DOMComponent.js)
176 | [MultiChild.js](../dilithium/src/MultiChild.js)
177 | [traverseAllChildren.js](../dilithium/src/traverseAllChildren.js)
178 |
179 | > 不用担心,在接下来的 update 中我们会讲到这几个函数和方法。
180 |
181 | 在下篇中我们会讲解由 React 是怎么实现 `setState`,以及其引发的一系列更新操作的。
182 |
--------------------------------------------------------------------------------
/blog/update.md:
--------------------------------------------------------------------------------
1 | # `setState` and Update
2 |
3 | React 中组件的更新大致有两种,第一种是由于单向数据流传到当前组件中 `props` 的变化导致,另一种是由于组件中 `setState` 引起的 local state 的变化导致。由于 `props` 的单项数据流始于最外层的组件中的 local state(如果不使用 Redux 等状态管理工具的话),我们不妨从 `setState` 入手,分析 React 是如何更新 DOM 的。
4 |
5 | 在继续下去之前,推荐阅读:
6 |
7 | [Reconciliation](https://reactjs.org/docs/reconciliation.html)
8 |
9 | 在 React 中,`setState` 是异步的,这是因为 React 对 `setState` 进行了 batch 操作,即将短时间内的几个 `setState` 合并为一个。为什么要这么做呢?因为计算由 `setState` 而引发的 DOM diff 是很费时的,batch 使整个流程从**读取、修改、读取、修改、读取、修改……**变成了**读取、读取、读取、修改**,减少了大量的计算操作。
10 |
11 | 为了简化,我们暂不考虑 batch 的实现。并且不考虑 `setState` 接受一个回调函数作为参数的情况。
12 |
13 | 首先回忆一下,`setState` 只能在 Class Component 中使用,这意味着这个方法位于 `Component` 这个文件中:
14 |
15 | ```js
16 | class Component {
17 | // ...
18 | setState(partialState) {
19 | this._pendingState = Object.assign({}, this.state, partialState)
20 | this.updateComponent(this._currentElement, this._currentElement)
21 | }
22 |
23 | updateComponent(prevElement, nextElement) {}
24 | }
25 | ```
26 |
27 | 为什么这里要调用 `updateComponent` 的两个参数都是 `currentElement` 呢?我们知道 React 用一个 element 来表示一个组件的 DOM 结构。并且在上文提到,组件的更新无非有两种,一种是组件的 `props` 发生变化,这会改变 Element 的数据,而 state 的改变却并不会改变 Element。所以这里 element 在 `setState` 的操作中是没有变化的。
28 |
29 | 知道了这一点后,我们也知道要在 `updateComponent` 中区分这两种情况了。
30 |
31 | ```js
32 | updateComponent(prevElement, nextElement) {
33 | if (prevElement !== nextElement) {
34 | // should get re-render because of the changes of props passed down from parents
35 | // react calls componentWillReceiveProps here
36 | }
37 |
38 | // re-bookmarking
39 | this._currentElement = nextElement
40 |
41 | this.props = nextElement.props
42 | this.state = this._pendingState
43 | this._pendingState = null
44 |
45 | const prevRenderedElement = this._renderedComponent._currentElement
46 | const nextRenderedElement = this.render()
47 |
48 | if (shouldUpdateComponent(prevRenderedElement, nextRenderedElement)) {
49 | Reconciler.receiveComponent(this._renderedComponent, nextElement)
50 | } else {
51 | // remount everything under this node
52 | Reconciler.unmountComponent(this._renderedComponent)
53 |
54 | const nextRenderedComponent = instantiateComponent(nextElement)
55 | this._renderedNode = Reconciler.mountComponent(nextRenderedComponent)
56 |
57 | DOM.replaceNode(this._renderedComponent._domNode, this._renderedNode)
58 | }
59 | }
60 | ```
61 |
62 | 在这段代码中,如上所述,我们首先通过判断 `prevElement` 和 `nextElement` 是否相等,来得出是 `props` 变化还是 `state` 变化导致的 re-render。如果 `Element` 发生变化,说明 `props` 发生了改变,React 此时也会调用 `componentWillReceiveProps` 这个生命周期函数。
63 |
64 | 接着,我们重新设置当前 component instance 的 `props` 和 `state`。由于 React 组件就是 `(props, state) => element` 的一个函数映射,所以此时我们通过 `render` 得出了新的 element。
65 |
66 | 接下来我们需要正式进入通过对边 `prevElement` 和 `nextElement` 进行 更新的环节。在 [Reconciliation](https://reactjs.org/docs/reconciliation.html) 中,我们了解到,现有的 [Tree Diff Algorithm](https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf) 的复杂度是 O(n^3),而 React 基于两个假设得出了一个 O(n) 的 Diff 算法,也就是我们所说的 Virtual DOM Diff Algorithm。
67 |
68 | 这两个假设是:
69 |
70 | 1. 不同类型的 Element 会生成不同的子树。例如 `div` 变化成 `ul`,或者复合组建由 `` 变为 ``
71 | 1. 通过 `key` 这个属性,React 可以得知在重绘中需要具体更新哪几个节点。
72 |
73 | 我们暂且不考虑 `key` 的实现,只考虑第一点。这样一来也很简单地实现了 `shouldUpdateComponent` 这个函数。(**注意区分 shouldUpdateComponent 和 shouldComponentUpdate 两个方法,前者用来判断组件 element 的 type 有没有变化,后者是 React 组件内部的生命周期函数**)
74 |
75 | ```js
76 | function shouldUpdateComponent(prevElement, nextElement) {
77 | return prevElement.type === nextElement.type
78 | }
79 | ```
80 |
81 | 当 `element` 类型发生改变时,React 选择重新绘制由此向下的所有节点。所以我们看到了 `else` 部分的代码:首先销毁当前 component instance,然后重新 instantiate,并 mount component。
82 |
83 | 当 `element` 类型没有改变时,我们需要**更新**相应的 DOM 节点,而不是重新 mount。我们记得之前在 mounting 中讲到,React 通过 Reconciler 实现了 `mountComponent` 接口的多态。这里我们再介绍一个新的方法,叫做 `receiveComponent`(但是这个命名并不是很好)。它的实现如下:
84 |
85 | ```js
86 | function receiveComponent(component, nextElement) {
87 | if (component._currentElement === nextElement) return
88 | component.updateComponent(component._currentElement, nextElement)
89 | }
90 | ```
91 |
92 | 实际上就是调用了对应组件内部的 `updateComponent` 这个方法。
93 |
94 | 需要额外注意的是,从最开始的 mounting,亦或是从 `setState` 开始的 updating,class component 内部的 `this._renderedComponent` 和 `this._currentElement` 是 **`render` 函数最外层的组件类型**,调用的 `updateComponent` 从 Class Component defer到了 DOM Component)。
95 |
96 | 举个例子:
97 |
98 | ```js
99 | class Counter extends React.Component {
100 | constructor() {
101 | super()
102 | this.state = { count: 0 }
103 | setInterval(() => {
104 | this.setState({ count: this.state.count + 1 })
105 | }, 1000)
106 | }
107 |
108 | render() {
109 | return (
110 |
111 | { this.state.count }
112 |
113 | )
114 | }
115 | }
116 | ```
117 |
118 | 那么整个更新的流程图应该是这样的:
119 |
120 | 
121 |
122 | 由此可以看出,和 mounting 一样,真正的 updating 也是发生在 `DOMComponent` 里。
123 |
124 | 那么我们进一步去看 `DOMComponent` 内部是怎么进行 update 的。
125 |
126 | ```js
127 | // DOMComponent.js
128 | updateComponent(prevElement, nextElement) {
129 | this._currentElement = nextElement
130 | this._updateNodeProperties(prevElement.props, nextElement.props)
131 | this._updateDOMChildren(prevElement.props, nextElement.props)
132 | }
133 | ```
134 |
135 | 非常简洁是不是?更新当前 DOM 节点的属性(上节已经讲过),然后递归更新子树。
136 |
137 | 但是到目前位置我们还并没有详细进入 `_updateDOMChildren` 这个函数的细节,而这正是 React Virtual DOM 的 Diff 算法的精华。
138 |
139 | 这一节我们着重分析 **React update 的整个流程**,下一节我们会分析这个函数带来的一系列操作,并开始分析 Diff 算法的内部细节。
140 |
--------------------------------------------------------------------------------
/blog/update-dom.md:
--------------------------------------------------------------------------------
1 | # Update the Real DOM
2 |
3 | 首先回顾一下上篇文章的进度。
4 |
5 | ```js
6 | updateChildren(nextChildren) {
7 | // component tree
8 | let prevRenderedChildren = this._renderedChildren
9 | // element tree
10 | let nextRenderedChildren = flattenChildren(nextChildren)
11 |
12 | let mountNodes = []
13 | let removedNodes = {}
14 |
15 | ChildReconciler.updateChildren(
16 | prevRenderedChildren,
17 | nextRenderedChildren,
18 | mountNodes,
19 | removedNodes
20 | )
21 |
22 | // ...
23 | }
24 | ```
25 |
26 | 我们通过 `ChildReconciler.updateChildren`,mutate 修改了 `nextRenderedChildren`, `mountNodes` 和 `removedNodes`。在后两个变量中,分别储存着 diff 后**需要新插入的元素和需要被移除的元素**。
27 |
28 | ## Create the `OPERATIONS` Object
29 |
30 | 接下来,我们要根据这两个变量,构建 `updates`。但是在此之前,我们先对 `updates` 的数据结构做好约定。由于最终我们要根据 `updates` 进行真正的 DOM 操作,所以其中必然包含了每个 DOM 操作需要的全部信息。所以在这里我们写了这样一个 helper function,用来生成每一个 DOM 操作的更新信息:
31 |
32 | ```js
33 | const UPDATE_TYPES = {
34 | INSERT: 1,
35 | MOVE: 2,
36 | REMOVE: 3
37 | }
38 |
39 | const OPERATIONS = {
40 | insert(node, afterNode) {
41 | return {
42 | type: UPDATE_TYPES.INSERT,
43 | content: node,
44 | afterNode: afterNode,
45 | }
46 | },
47 |
48 | move(component, afterNode) {
49 | return {
50 | type: UPDATE_TYPES.MOVE,
51 | fromIndex: component._mountIndex,
52 | afterNode: afterNode,
53 | }
54 | },
55 |
56 | remove(component, node) {
57 | return {
58 | type: UPDATE_TYPES.REMOVE,
59 | fromIndex: component._mountIndex,
60 | fromNode: node,
61 | }
62 | }
63 | }
64 | ```
65 |
66 | 由此就能像这样通过 `OPERATIONS` 这个对象内部的方法,生成一个atomic 的更新对象,并添加到 `updates` 中去。
67 |
68 | ```js
69 | updates.push(OPERATIONS.insert(node, afternode))
70 | ```
71 |
72 | ## Build the `updates`
73 |
74 | 到了真正要构建 `updates` 的时候了。
75 |
76 | ```js
77 | updateChildren(nextChildren) {
78 | // component tree
79 | let prevRenderedChildren = this._renderedChildren
80 | // element tree
81 | let nextRenderedChildren = flattenChildren(nextChildren)
82 |
83 | let mountNodes = []
84 | let removedNodes = {}
85 |
86 | ChildReconciler.updateChildren(
87 | prevRenderedChildren,
88 | nextRenderedChildren,
89 | mountNodes,
90 | removedNodes
91 | )
92 |
93 | // We'll compare the current set of children to the next set.
94 | // We need to determine what nodes are being moved around, which are being
95 | // inserted, and which are getting removed. Luckily, the removal list was
96 | // already determined by the ChildReconciler.
97 |
98 | // We'll generate a series of update operations here based on the
99 | // bookmarks that we've made just now
100 | let updates = []
101 |
102 | let lastIndex = 0
103 | let nextMountIndex = 0
104 | let lastPlacedNode = null
105 |
106 | Object.keys(nextRenderedChildren).forEach((childKey, nextIndex) => {
107 | let prevChild = prevRenderedChildren[childKey]
108 | let nextChild = nextRenderedChildren[childKey]
109 |
110 | // mark this as an update if they are identical
111 | if (prevChild === nextChild) {
112 | // We don't actually need to move if moving to a lower index.
113 | // Other operations will ensure the end result is correct.
114 | if (prevChild._mountIndex < lastIndex) {
115 | updates.push(OPERATIONS.move(nextChild, lastPlacedNode))
116 | }
117 |
118 | lastIndex = Math.max(prevChild._mountIndex, lastIndex)
119 | prevChild._mountIndex = nextIndex
120 | } else {
121 | // Otherwise we need to record an insertion.
122 | // First, if we have a prevChild then we know it's a removal.
123 | // We want to update lastIndex based on that.
124 | if (prevChild) {
125 | lastIndex = Math.max(prevChild._mountIndex, lastIndex)
126 | }
127 |
128 | nextChild._mountIndex = nextIndex
129 | updates.push(
130 | OPERATIONS.insert(
131 | mountNodes[nextMountIndex],
132 | lastPlacedNode
133 | )
134 | )
135 | nextMountIndex ++
136 | }
137 |
138 | // keep track of lastPlacedNode
139 | lastPlacedNode = nextChild._domNode
140 | })
141 |
142 | // enque the removal the non-exsiting nodes
143 | Object.keys(removedNodes).forEach((childKey) => {
144 | updates.push(
145 | OPERATIONS.remove(
146 | prevRenderedChildren[childKey],
147 | removedNodes[childKey]
148 | )
149 | )
150 | })
151 |
152 | // ...
153 | }
154 | ```
155 |
156 | 有几点需要解释一下:
157 |
158 | 1. 整个函数就是分别往 `updates` 里面 push 了三个东西:`insert`, `move` 和 `remove` 的一系列操作。
159 |
160 | 1. 首先我们看到,只有在 `prevChild._mountIndex < lastIndex` 也就是将更新过后的 `prevChild` 移动到一个*更高*的索引的时候,我们才 push 一个 move 操作。而当将其 move 到一个*更低*索引的时候,我们可以置之不顾。为什么?因为后续的操作会将一些节点删除或者移动,最终结果是该节点自动往更低索引处走了。
161 |
162 | 1. 用 `lastIndex` 来记录**上一个 `prevChild` 的 `mountIndex`**,这个变量的唯一用处是比对**当前的 `prevChild` 是在相对往哪里移动**,如果 `prevChild._mountIndex < lastIndex` 说明当前的节点在当前的 update 中应该往 **下(高索引处)** 移动。
163 |
164 | 1. 用 `lastPlacedNode` 来记录上一个被放置的节点。用来作为 `insertAfter` 的第三个参数。
165 |
166 | 1. 最后遍历 `removedNodes` 得出 `updates` 中的 `remove` 操作。
167 |
168 | 经过这样的操作之后,我们有了关于本次 patch 的所有相关信息,只需要遍历 `updates`,进行真正的 DOM 修改即可。
169 |
170 | 这个函数的最后两行:
171 |
172 | ```js
173 | // do the actual updates
174 | processQueue(this._domNode, updates)
175 | // re-bookmark
176 | this._renderedChildren = nextRenderedChildren
177 | ```
178 |
179 | 至于 `processQueue`,则是针对 updates 跑 DOM 操作:
180 |
181 | ```js
182 | function processQueue(parentNode, updates) {
183 | updates.forEach(update => {
184 | switch (update.type) {
185 | case UPDATE_TYPES.INSERT:
186 | DOM.insertAfter(parentNode, update.content, update.afterNode)
187 | break
188 |
189 | case UPDATE_TYPES.MOVE:
190 | // this automatically removes and inserts the new child
191 | DOM.insertAfter(
192 | parentNode,
193 | update.content,
194 | update.afterNode
195 | )
196 | break
197 |
198 | case UPDATE_TYPES.REMOVE:
199 | DOM.removeChild(parentNode, update.fromNode)
200 | break
201 |
202 | default:
203 | assert(false)
204 | }
205 | })
206 | }
207 | ```
208 |
209 | 对于三类操作,分别执行相应的 DOM 修改。
210 |
211 | 至此,我们已经分析完了所有的 Virtual DOM Diff 以及 update 的操作。
212 |
213 | 你可以回顾一下 README 中的 [What Will You Learn](../README.md#what-youll-learn) 来回顾复习一下这个系列博客的所学知识。
214 |
215 | ## 参考资料
216 |
217 | [Paul O Shannessy - Building React From Scratch](https://www.youtube.com/watch?v=_MAD4Oly9yg)
218 |
219 | [Building React from Scratch](https://github.com/zpao/building-react-from-scratch)
220 |
221 | [Tech Talk: What is the Virtual DOM?](https://www.youtube.com/watch?v=d7pyEDqBDeE)
222 |
223 | [Let's Build a Virtual DOM from Scratch](https://www.youtube.com/watch?v=l2Tu0NqH0qU)
224 |
--------------------------------------------------------------------------------
/blog/mounting.md:
--------------------------------------------------------------------------------
1 | # Day2 - Mounting
2 |
3 | 在做好一定的预备知识的学习后,本篇我们只研究一个问题:
4 |
5 | **React 是如何把 Component 中的 JSX 映射到页面上真正的 DOM 节点的。这个流程是怎样的?**。
6 |
7 | ## 面向测试编程
8 |
9 | 我们首先写一个小 demo,用于测试我们最终的代码:
10 |
11 | ```js
12 | const Dilithium = require('../dilithium')
13 |
14 | class App extends Dilithium.Component {
15 | render() {
16 | return (
17 |
18 |
19 |
Heading 1
20 |
21 | Heading 2
22 |
23 |
Heading 3
24 |
25 | )
26 | }
27 | }
28 |
29 | class SmallHeader extends Dilithium.Component {
30 | render() {
31 | return (
32 | SmallHeader
33 | )
34 | }
35 | }
36 |
37 | Dilithium.render(, document.getElementById('root'))
38 | ```
39 |
40 | 可以看出,基本用法是跟 React 一致的。
41 |
42 | 至于 `Dilithium.render` 函数,我们有如下的实现:
43 |
44 | ```js
45 | function render(element, node) {
46 | // todo: add update
47 | mount(element, node)
48 | }
49 | ```
50 |
51 | ## The Mounting Process Overview
52 |
53 | 在我们开始分析之前,首先给出这个答案的流程图:
54 |
55 | 
56 |
57 | 根据这个流程,我们给出 `mount` 的实现:
58 |
59 | ```js
60 | function mount(element, node) {
61 | const component = instantiateComponent(element)
62 | const renderedNode = component.mountComponent()
63 |
64 | // these are just helper functions of native DOM functions
65 | // you can check them out in dilithium/src/DOM.js
66 | DOM.empty(node)
67 | DOM.appendChildren(node, renderedNode)
68 | }
69 | ```
70 |
71 | 然后根据流程的各个环节逐步开始分析。
72 |
73 | ## JSX -> Element
74 |
75 | 在 React 中,我们使用的组件有两种,class component 或是 functional component. 对于 class component 来说,render 函数是组件内必不可少的;而对于 functional component,组件没有生命周期和 local state,组件函数返回值等同于 class component 中 render 函数的返回值。
76 |
77 | 无论是 render 函数的返回值,还是函数是组件的返回值,它们都是 JSX。JSX 是 `React.createElement(type, props, ...children)` 函数的语法糖,如果你还不熟悉,建议先阅读[JSX in Depth](https://reactjs.org/docs/jsx-in-depth.html),然后可以在 [Try it out](https://babeljs.io/repl/) 中试一下 JSX 和 `createElement` 的映射关系。
78 |
79 | 我们知道,JSX 只是调用了函数 `React.createElement`,并把对应的 JSX 结构映射到了 `createElement` 相应的参数中去。例如:
80 |
81 | ```html
82 |
83 | good
84 | Hello world
85 |
86 | ```
87 |
88 | 会被编译成:
89 |
90 | ```js
91 | React.createElement(
92 | "div",
93 | { className: "container" },
94 | "good",
95 | React.createElement(
96 | "span",
97 | null,
98 | "Hello world"
99 | )
100 | );
101 | ```
102 |
103 | 那么 `createElement` 这个函数又做了什么事情呢?
104 |
105 | ```js
106 | function createElement(type, config, children) {
107 | const props = Object.assign({}, config)
108 | const childrenLength = [].slice.call(arguments).length - 2
109 |
110 | if (childrenLength > 1) {
111 | props.children = [].slice.call(arguments, 2)
112 | } else if (childrenLength === 1) {
113 | props.children = children
114 | }
115 |
116 | return {
117 | type,
118 | props
119 | }
120 | }
121 | ```
122 |
123 | 一言以蔽之,`createElement` 就是将 `children` 合并进了当前 Element 对象,成为了其中的 `children` 属性。
124 |
125 | 这样,对于一个 JSX 结构,我们最终得到了一个数据结构如下的**纯 JS Object**,也就是 Element。
126 |
127 | ```js
128 | {
129 | type: string | function | class
130 | props: {
131 | children
132 | }
133 | }
134 | ```
135 |
136 | > Note:
137 | > 暂时不支持函数式组件
138 |
139 | ## Element -> Component
140 |
141 | 有了 Element 后,我们需要将 Element 中对应的组件类型(`type`)实例化,也就是 `instantiateComponent`。在前文提到,element type 有三种:
142 |
143 | 1. string, 例如 `"div", "ul"` 等原生 DOM 结构。
144 | 2. function, 函数式组件(暂不支持)
145 | 3. class component
146 |
147 | 但是我们也要考虑另一种情况,element 本身是一个字符串或数字(并没有被组件包裹)。这样,我们根据不同情况,分别生成不同的组件类型:
148 |
149 | ```js
150 | function instantiateComponent(element) {
151 | let componentInstance
152 |
153 | if (typeof element.type === 'function') {
154 | // todo: add functional component
155 | // only supports class component for now
156 | componentInstance = new element.type(element.props)
157 | componentInstance._construct(element)
158 | } else if (typeof element.type === 'string') {
159 | componentInstance = new DOMComponent(element)
160 | } else if (typeof element === 'string' || typeof element === 'number') {
161 | // to reduce overhead, we wrap the text with a span
162 | componentInstance = new DOMComponent({
163 | type: 'span',
164 | props: { children: element }
165 | })
166 | }
167 |
168 | return componentInstance
169 | }
170 | ```
171 |
172 | ## Component -> DOM Nodes
173 |
174 | 在讨论这点之前,我们先讨论一下“多态”(polymorphism)。这是 OOP 中很重要的一个概念,在 `instantiateComponent` 中,我们根据参数 `element` 类型的不同,调用了不同的方法,本质上是一种“函数多态”。而在这一环节,我们专门将多态抽离出来,构成一个 `Reconciller:
175 |
176 | ```js
177 | // Reconciller
178 |
179 | function mountComponent(component) {
180 | return component.mountComponent()
181 | }
182 | ```
183 |
184 | 同时,我们在不同类型中的 component 中,分别实现**同名**的方法(`mountComponent`):
185 |
186 | 在 Class Component 中,我们看到,`mountComponent` 和实际的 `mount` 流程非常相似,都是 element -> component -> node。这里由于 class component 本身没有对应的 DOM 映射,所以 mount 的过程 defer 到了下一层组件。
187 |
188 | ```js
189 | // Component
190 | class Component {
191 | constructor(props) {
192 | this.props = props
193 | this.currentElement = null
194 | this._renderedComponent = null
195 | this._renderedNode = null
196 | }
197 |
198 | _construct(element) {
199 | this.currentElement = element
200 | }
201 |
202 | mountComponent() {
203 | // we simply assume the render method returns a single element
204 | const renderedElement = this.render()
205 |
206 | const renderedComponent = instantiateComponent(renderedElement)
207 | this._renderedComponent = renderedComponent
208 |
209 | const renderedNode = Reconciler.mountComponent(renderedComponent)
210 | this._renderedNode = renderedNode
211 |
212 | return renderedNode
213 | }
214 | }
215 | ```
216 |
217 | ```js
218 | class DOMComponent {
219 | constructor(element) {
220 | this._currentElement = element
221 | this._domNode = null
222 | }
223 |
224 | mountComponent() {
225 | // create real dom nodes
226 | const node = document.createElement(this._currentElement.type)
227 | this._domNode = node
228 |
229 | this._updateNodeProperties({}, this._currentElement.props)
230 | this._createInitialDOMChildren(this._currentElement.props)
231 |
232 | return node
233 | }
234 | }
235 | ```
236 |
237 | 我们暂不分析 `_updateNodeProperties` 和 `_createInitialDOMChildren` 这两个函数方法的细节(留到下篇博客),从字面意思可以看出,这两个函数分别是将 `element.props` 挂载到真正的 DOM 节点上,以及递归 mount 子节点。最终返回当前这个 DOM 节点。
238 |
239 | 回顾一下 `mount` 函数:
240 |
241 | ```js
242 | function mount(element, node) {
243 | const component = instantiateComponent(element)
244 | const renderedNode = component.mountComponent()
245 |
246 | DOM.empty(node)
247 | DOM.appendChildren(node, renderedNode)
248 | }
249 | ```
250 |
251 | 到这里 `const renderedNode = component.mountComponent()`,我们已经拿到了真正的 DOM 节点,剩下的工作非常简单。首先清空 container 里的内容,然后将 renderedNode append 上去。
252 |
253 | 如下是两个 DOM helper function:
254 |
255 | ```js
256 | function empty(node) {
257 | [].slice.call(node.childNodes).forEach((child) => {
258 | node.removeChild(child)
259 | })
260 | }
261 |
262 | function appendChildren(node, children) {
263 | if (Array.isArray(children)) {
264 | children.forEach((child) => {
265 | node.appendChild(child)
266 | })
267 | } else {
268 | node.appendChild(children)
269 | }
270 | }
271 | ```
272 |
273 | 至此,我们已经走完了 mounting 整个的流程。完整的代码实现(仅 mounting 部分)在[这里](https://github.com/cyan33/learn-react-source-code/tree/mount)
274 |
275 | 在理解这个流程的时候,我个人认为有这几个个关键点,你也可以把它们作为检验你是否真正理解这个过程的几个题目。
276 |
277 | 1. Element, Component, Instance 的区别是什么
278 | 2. 四种不同的 Element 类型分别是怎样 mount 成真正的 DOM 节点的
279 | 3. Class Component 是怎样 defer mount 的
280 | 4. DOM Component 是怎样实现真正的 mount 的
281 |
282 | 但是,在最后的 DOM Component 中,我们有两个问题/函数还没有讲,分别是:
283 |
284 | 1. `updateNodeProperties`
285 | 2. `createInitialDOMChildren`
286 |
287 | 其中正是 `createInitialDOMChildren` 实现了 Element tree 的递归 mount。我们将在下一篇博客中完成最后这部分的分析。
288 |
--------------------------------------------------------------------------------
/blog/update-contd.md:
--------------------------------------------------------------------------------
1 | # Update - Contd
2 |
3 | 在上节中,我们顺着 `setState` 的流程一路走到了 DOM Component 的 `updateDOMChildren` 这个方法。接下来我们看看这个方法是怎么实现的:
4 |
5 | ```js
6 | _updateDOMChildren(prevProps, nextProps) {
7 | const prevType = typeof prevProps.children
8 | const nextType = typeof nextProps.children
9 |
10 | // Childless node, skip
11 | if (nextType === 'undefined') return
12 |
13 | // Much like the initial step in mounting, handle text differently than elements.
14 | if (nextType === 'string' || nextType === 'number') {
15 | this._domNode.textContent = nextProps.children
16 | } else {
17 | this.updateChildren(nextProps.children)
18 | }
19 | }
20 | ```
21 |
22 | 可以看到,这个方法和 DOM Component 中的 `createInitialDOMChildren` 十分类似,因为在更新时,我们也要考虑 `children` 类型发生变化的情况。如果变为 `string || number`,那么直接修改 `domNode` 的 `textContent` 就可以了。但是大部分情况下,我们需要更复杂的 diff 对比。
23 |
24 | 我们也观察到,这个函数调用了父类 `MultiChild` 的 `updateChildren` 方法。而这个方法可以说是 React Virtual DOM Diff 算法的入口。在继续分析下去之前,有必要牢记这个方法实参的数据结构:
25 |
26 | ```js
27 | children: ReactElement || Array
28 | ```
29 |
30 | ## 预备工作之 `traverseAllChildren`
31 |
32 | 在 Mount 部分中,我们谈到过,React 的 DOM Component 并不是简单的遍历子树并逐个 mount,而是通过 `traverseAllChildren` 生成了一个 hash tree,并保存到了 `this._renderedChildren` 这个属性中。
33 |
34 | 现在,我们首先来看看 `traverseAllChildren` 是怎么实现的。
35 |
36 | ```js
37 | const SEPARATOR = '.'
38 | const SUBSEPARATOR = ':'
39 |
40 | function getComponentKey(component, index) {
41 | // This is where we would use the key prop to generate a unique id that
42 | // persists across moves. However we're skipping that so we'll just use the
43 | // index.
44 | return index.toString(36)
45 | }
46 |
47 | function traverseAllChildren(children, callback, traverseContext) {
48 | return traverseAllChildrenImpl(children, '', callback, traverseContext)
49 | }
50 |
51 | function traverseAllChildrenImpl(
52 | children,
53 | nameSoFar,
54 | callback,
55 | traverseContext
56 | ) {
57 | if (
58 | typeof children === 'string' ||
59 | typeof children === 'number' ||
60 | !Array.isArray(children)
61 | ) {
62 | callback(
63 | traverseContext,
64 | children,
65 | nameSoFar + SEPARATOR + getComponentKey(children, 0)
66 | )
67 | return 1
68 | }
69 |
70 | let subtreeCount = 0
71 | const namePrefix = !nameSoFar ? SEPARATOR : namePrefix + SUBSEPARATOR
72 |
73 | children.forEach((child, i) => {
74 | subtreeCount += traverseAllChildrenImpl(
75 | child,
76 | namePrefix + getComponentKey(child, i),
77 | callback,
78 | traverseContext
79 | )
80 | })
81 |
82 | return subtreeCount
83 | }
84 | ```
85 |
86 | 这个函数乍一看比较复杂,但是花时间分析一下,就会发现其实还是很简洁的。在此之前我们先看一下这个函数是怎么被调用的。
87 |
88 | 比如说在 mounting 阶段我们记得有一个步骤是在 `mountChildren` 中调用 `instantiateChildren`,这个方法是这样的:
89 |
90 | ```js
91 | function instantiateChildren(children) {
92 | let childInstances = {}
93 |
94 | traverseAllChildren(
95 | children,
96 | (traverseContext, children, name) => traverseContext[name] = children,
97 | childInstances
98 | )
99 |
100 | return childInstances
101 | }
102 | ```
103 |
104 | 有几点需要注意:
105 |
106 | 1. 一般情况下我们不提倡使用 mutate 方法,但是在 `traverAllChildren` 这个函数里我们看出它直接利用 callback 修改了参数 `traverseContext`,也就是 `childInstances`。也正因为如此,我们生成了所谓的 hash tree。
107 | 1. `traverseAllChildren` 中的 `nameSoFar` 正是 hash tree 中的每个 Component 的 key。
108 | 1. 注意 `traverseAllChildren` 并不会无限地递归到 leaf node,而只是**一层**的遍历。只要当前 child 是 **单个元素(即使它是一个 wrapper)** 就不会再往里递归。
109 |
110 | [comment]: <> (Explain this in detail later)
111 |
112 | 通过 `instantiateChildren` 我们生成的 hash tree 的数据结构是类似这样的(我们将这个 tree 保存到了 `this._renderedChildren` 中):
113 |
114 | ```js
115 | {
116 | '.0.0': {_currentElement, ...}
117 | '.0.1': {_currentElement, ...}
118 | }
119 | ```
120 |
121 | ## Back to Update
122 |
123 | 现在我们回到 update,还记得我们上篇讲到的流程图吗?
124 |
125 | 
126 |
127 | 我们上次讲到了 `updateDOMChildren` 这个方法,现在我们继续向下分析。
128 |
129 | 首先我们知道,组件由 `setState` 更新的时候会带来各种各样的变化,这其中包括 element `props` 变化,也包括 element 本身内容变化,甚至 element 的类型变化。
130 |
131 | ```js
132 | updateDOMChildren(prevProps, nextProps) {
133 | const nextChildrenType = typeof nextProps.children
134 |
135 | // Childless node, skip
136 | if (nextType === 'undefined') return
137 |
138 | if (nextType === 'string' || nextType === 'number') {
139 | this._domNode.textContent = nextProps.children
140 | } else {
141 | this.updateChildren(nextProps.children)
142 | }
143 | }
144 | ```
145 |
146 | 如果只是单纯地节点内容发生变化,那么只需要修改 `textContent`。接下来我们重点看 `MultiChild` 中的 `updateChildren` 这个方法。
147 |
148 | ## Core
149 |
150 | 接下来的这部分是 React 的核心。敲黑板划重点了!
151 |
152 | 首先我们用几句话概括一下整个更新的过程。在 React 里,我们首先对比 `prevRenderedChildren` 和 `nextRenderedChildren`,也就是所谓的 diff 操作。通过 diff,我们得出**需要 insert 的新节点,需要 remove 的节点,和需要调整顺序的节点**。并把它们保存在数组或对象这样的数据结构里。最后,我们逐个遍历这些数据结构并生成一个数组叫做 `updates`,用来保存所有需要执行的操作描述。最后,我们遍历 `updates`,执行真正的 DOM 操作。
153 |
154 | ```js
155 | updateChildren(nextChildren) {
156 | // component tree
157 | let prevRenderedChildren = this._renderedChildren
158 | // element tree
159 | let nextRenderedChildren = flattenChildren(nextChildren)
160 |
161 | let mountNodes = []
162 | let removedNodes = {}
163 |
164 | ChildReconciler.updateChildren(
165 | prevRenderedChildren,
166 | nextRenderedChildren,
167 | mountNodes,
168 | removedNodes
169 | )
170 |
171 | // ...
172 | }
173 | ```
174 |
175 | `this._renderedChildren` 中保存着我们之前 mounting 中生成的 Component hash tree。但是 `nextChildren` 仍然是一个元素类型为 element 的数组。为了数据结构的一致,我们首先也需要对它进行 traverse 生成 hash tree:
176 |
177 | ```js
178 | function flattenChildren(children) {
179 | const flattenedChildren = {}
180 |
181 | traverseChildren(
182 | children,
183 | (flattenedChildren, child, name) => flattenedChildren[name] = child,
184 | flattenedChildren
185 | )
186 |
187 | return flattenedChildren
188 | }
189 | ```
190 |
191 | 注意到我们此处生成的 `nextRenderedChildren` 是一个 value 类型为 element 的 hash tree。而 `prevRenderedChildren` 的 value 类型为 component。
192 |
193 | 在“统一”了数据结构后,我们增加了一个中间件,`ChildReconciler`,专门用来处理 Children 的操作。接下来我们看一下其中的 `updateChildren` 这个方法。正如我们刚才说的吗,它的作用是得出**需要 insert 的新节点,需要 remove 的节点,和需要调整顺序的节点**。并把它们保存在数组或对象这样的数据结构里。
194 |
195 | ```js
196 | function updateChildren(
197 | prevChildren, // instance tree
198 | nextChildren, // element tree
199 | mountNodes,
200 | removedNodes
201 | ) {
202 | // we use the index of the tree to track the updates of the component, like `0.0`
203 | Object.keys(nextChildren).forEach((childKey) => {
204 | const prevChildComponent = prevChildren[childKey]
205 | const prevElement = prevChildComponent && prevChildComponent._currentElement
206 | const nextElement = nextChildren[childKey]
207 |
208 | // three scenarios:
209 | // 1: the prev element exists and is of the same type as the next element
210 | // 2: the prev element exists but not of the same type (type has changed)
211 | // 3: the prev element doesn't exist (insert a new element)
212 |
213 | if (prevElement && shouldUpdateComponent(prevElement, nextElement)) {
214 | // this will do the recursive update of the sub tree
215 | // and this line is basically the actual update
216 | Reconciler.receiveComponent(prevChildComponent, nextElement)
217 | // and we do not need the new element
218 | // note that we are converting the `nextChildren` object from an
219 | // element tree to a component instance tree during all this process
220 | nextChildren[childKey] = prevChildComponent
221 | } else {
222 | // otherwise, we need to do the unmount and re-mount stuff
223 | if (prevChildComponent) {
224 | // only supports DOM node for now, should add composite component
225 | removedNodes[childKey] = prevChildComponent._domNode
226 | Reconciler.unmountComponent(prevChildComponent)
227 | }
228 |
229 | // instantiate the new child. (insert)
230 | const nextComponent = instantiateComponent(nextElement)
231 | nextChildren[childKey] = nextComponent
232 |
233 | mountNodes.push(Reconciler.mountComponent(nextComponent))
234 | }
235 | })
236 |
237 | // last but not least, remove the old children which no longer exist
238 | Object.keys(prevChildren).forEach((childKey) => {
239 | if (!nextChildren.hasOwnProperty(childKey)) {
240 | const prevChildComponent = prevChildren[childKey]
241 | removedNodes[childKey] = prevChildComponent
242 | Reconciler.unmountComponent(prevChildComponent)
243 | }
244 | })
245 | }
246 | ```
247 |
248 | 仔细看一下,这个函数其实还是不复杂的。我们主要针对三种情况进行处理。在遍历 `nextChildren` 的时候,我们假定这个 hash tree 的每个 key 都存在对应的 `prevChild`。这三种情况分别是:
249 |
250 | 1. `prevElement` 存在且 和 `nextElement` 同样类型(`shouldUpdateComponent`)
251 | 1. `prevElement` 存在但是类型已经发生变化
252 | 1. `prevElement` 不存在,说明需要插入一个新的 `nextElement`
253 |
254 | 最后,由于我们遍历的是 `nextChildren`,接下来还需要遍历一下 `prevChildren`,如果 `prevElement` 的 key 不存在对应的 `nextElement`,说明这个节点在这次 update 中被删除了。我们将其加入 `removedNodes`。
255 |
256 | > 值得注意的是,`nextChildren` 从最初的 value 类型为 element 的 hash tree,通过 `nextChildren[childKey] = prevChildComponent` 亦或是 `instantiateComponent` 转化成了 value 类型为 component 的 hash tree。
257 |
258 | 就这样,通过 `ChildReconciler.updateChildren`,我们通过 diff 算法,得出了所有的需要 mount 的节点,和需要移除的节点,并分别储存在 `mountNodes` 和 `removedNodes` 里面。(注意这两个变量的数据结构是不一样的)
259 |
260 | 最后,不妨思考一个问题,**到底什么是 Virtual DOM?**
261 |
262 | 其实读完这两篇文章之后,你应该已经有答案了。无论是从一开始调用 `setState` 后组件内的 `updateComponent` 还是之后的 `updateChildren`,我们始终没有触碰到真正的 DOM 元素,而利用的是 React Element,或是我们之前生成的 Component hash tree。这也是为什么在这个系列博客的第一篇中,文章末尾提出的问题 `What is the advantage(s) of using the element tree`。因为操作 DOM 是很费资源和时间的,但是操作原生的 JS 对象就大大减少了消耗。
263 |
264 | 所以所谓的 Virtual DOM,无非是**在 mounting 和 update 的过程中,将真正的 DOM 结构映射到了原生的 JS 对象(element tree 或 component tree),从而大大提高了 diff 的效率。**
265 |
266 | 在下一节,也是最后一节中,我们会讲解怎样将 `mountNodes` 和 `removedNodes` 映射到 `updates`,并且遍历 `updates` 做真正的 DOM 更新。
267 |
--------------------------------------------------------------------------------