├── .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 | ![update-process](assets/update-process.jpg) 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 | ![mount-process](assets/mount-process.jpg) 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 | ![update-process](assets/update-process.jpg) 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 | --------------------------------------------------------------------------------