├── packages ├── my-react │ ├── .gitignore │ ├── src │ │ ├── dom │ │ │ ├── render.js │ │ │ ├── unmount.js │ │ │ ├── utils.js │ │ │ ├── mount.js │ │ │ └── event.js │ │ ├── index.js │ │ ├── element.js │ │ ├── component.js │ │ ├── hooks.js │ │ ├── patch.js │ │ └── vnode.js │ ├── .babelrc │ ├── webpack.config.js │ └── package.json └── example-simple │ ├── public │ ├── favicon.ico │ ├── manifest.json │ └── index.html │ ├── src │ ├── tests │ │ ├── reconcileTest.css │ │ ├── hooksTests │ │ │ ├── useStateTest.jsx │ │ │ ├── useStateTest2.jsx │ │ │ └── useStateTest3.jsx │ │ ├── renderPrimitiveTest.jsx │ │ ├── keyTest.jsx │ │ ├── diffTests │ │ │ ├── updateAttrs.jsx │ │ │ ├── reorderTextNodes2.jsx │ │ │ ├── reorderTextNodes.jsx │ │ │ ├── reorderTest.jsx │ │ │ ├── unkeyedTest.jsx │ │ │ ├── keyedTest.jsx │ │ │ ├── manuallyKeyedTest.jsx │ │ │ └── nestedKeyedTest.jsx │ │ ├── setStateTests │ │ │ ├── mutateState.jsx │ │ │ ├── setStateTest.jsx │ │ │ ├── multipleSetStateCalls.jsx │ │ │ ├── propTest.jsx │ │ │ ├── setStateWithChildren.jsx │ │ │ └── callbackAndSetState.jsx │ │ ├── unmountTest.jsx │ │ ├── didUpdateTest.jsx │ │ ├── lifecycleTest.jsx │ │ ├── refTests │ │ │ └── refTest.jsx │ │ ├── reduxTest.jsx │ │ ├── reactReduxTest.jsx │ │ ├── form │ │ │ └── inputTest.jsx │ │ ├── childrenTest.jsx │ │ └── reconcileTest.jsx │ ├── utils.js │ ├── Controller │ │ ├── Controller.css │ │ └── Controller.jsx │ ├── parseQuery.js │ ├── section.jsx │ ├── diffRendered.js │ ├── style.css │ ├── app.jsx │ ├── index.js │ └── allTests.js │ ├── config.js │ ├── README.md │ ├── .gitignore │ └── package.json ├── lerna.json ├── package.json ├── README.md └── .gitignore /packages/my-react/.gitignore: -------------------------------------------------------------------------------- 1 | dist -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.6.0", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true 5 | } 6 | -------------------------------------------------------------------------------- /packages/example-simple/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gaoxiaoliangz/react-lite/HEAD/packages/example-simple/public/favicon.ico -------------------------------------------------------------------------------- /packages/my-react/src/dom/render.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import mount from './mount' 3 | 4 | const render = (vNode, dom) => mount(vNode, dom) 5 | 6 | export default render 7 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/reconcileTest.css: -------------------------------------------------------------------------------- 1 | .reconcileTest .box { 2 | width: 20px; 3 | height: 20px; 4 | display: inline-block; 5 | margin-right: 10px; 6 | } 7 | -------------------------------------------------------------------------------- /packages/example-simple/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack: { 3 | development: config => config, 4 | production: config => config, 5 | devServer: (config, proxy, allowedHost) => config, 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/my-react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": [ 4 | "lodash", 5 | "@babel/plugin-proposal-class-properties", 6 | "@babel/plugin-proposal-object-rest-spread" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/example-simple/src/utils.js: -------------------------------------------------------------------------------- 1 | export function guid() { 2 | const s4 = () => { 3 | return Math.floor((1 + Math.random()) * 0x10000) 4 | .toString(16) 5 | .substring(1) 6 | } 7 | return s4() + s4() 8 | } 9 | -------------------------------------------------------------------------------- /packages/example-simple/README.md: -------------------------------------------------------------------------------- 1 | # README 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app) using [@gxl/react-scripts](https://github.com/gaoxiaoliangz/create-react-app/tree/master/packages/react-scripts). 4 | -------------------------------------------------------------------------------- /packages/example-simple/src/Controller/Controller.css: -------------------------------------------------------------------------------- 1 | .controller { 2 | position: fixed; 3 | right: 0; 4 | bottom: 0; 5 | padding: 30px; 6 | z-index: 999; 7 | } 8 | 9 | .controller select { 10 | display: inline-block; 11 | margin: 0 15px 0 5px; 12 | } 13 | -------------------------------------------------------------------------------- /packages/example-simple/src/parseQuery.js: -------------------------------------------------------------------------------- 1 | const parseQuery = queryStr => 2 | queryStr.split('&').reduce((obj, part) => { 3 | const [key, val] = part.split('=') 4 | return { 5 | ...obj, 6 | [key]: val, 7 | } 8 | }, {}) 9 | 10 | export default parseQuery 11 | -------------------------------------------------------------------------------- /packages/example-simple/src/section.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | const Section = ({ title, children }) => { 3 | return ( 4 |
5 |

{title}

6 | {children} 7 |
8 | ) 9 | } 10 | return Section 11 | } 12 | -------------------------------------------------------------------------------- /packages/my-react/src/dom/unmount.js: -------------------------------------------------------------------------------- 1 | import { FLAGS } from '../vnode' 2 | 3 | const unmount = vNode => { 4 | if (vNode.flag === FLAGS.CLASS && vNode.instance.componentWillUnmount) { 5 | vNode.instance.componentWillUnmount() 6 | } 7 | vNode.dom.parentNode.removeChild(vNode.dom) 8 | } 9 | 10 | export default unmount 11 | -------------------------------------------------------------------------------- /packages/my-react/src/index.js: -------------------------------------------------------------------------------- 1 | import render from './dom/render' 2 | import Component from './component' 3 | import { createElement } from './element' 4 | import { useState } from './hooks' 5 | 6 | export default { 7 | createElement, 8 | Component, 9 | useState, 10 | } 11 | 12 | export const ReactDOM = { 13 | render, 14 | } 15 | -------------------------------------------------------------------------------- /packages/my-react/src/element.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import { createVNode } from './vnode' 3 | 4 | export const createElement = (type, props, ...children) => { 5 | return createVNode(type, { 6 | ...props, 7 | ...(children.length !== 0 && { 8 | children: children.length === 1 ? children[0] : children, 9 | }), 10 | }) 11 | } 12 | -------------------------------------------------------------------------------- /packages/example-simple/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /packages/example-simple/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/hooksTests/useStateTest.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | const useState = React.useState 3 | const UseStateTest = () => { 4 | const [clicks, setClicks] = useState(0) 5 | const ele = ( 6 |
7 | clicks: {clicks} 8 | 9 |
10 | ) 11 | return ele 12 | } 13 | return UseStateTest 14 | } 15 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/renderPrimitiveTest.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | const RenderPrimitiveTest = () => { 3 | const list = [0, 1, undefined, null, true, false, []] 4 | return ( 5 |
6 | {list.map((item, idx) => { 7 | return ( 8 |

9 | {'' + item}: 10 | {item} 11 |

12 | ) 13 | })} 14 |
15 | ) 16 | } 17 | return RenderPrimitiveTest 18 | } 19 | -------------------------------------------------------------------------------- /packages/my-react/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: { 5 | react: './src/index.js', 6 | }, 7 | output: { 8 | path: path.join(__dirname, 'dist'), 9 | filename: '[name].js', 10 | libraryTarget: 'umd', 11 | }, 12 | mode: 'development', 13 | devtool: 'sourcemap', 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.(js|ts)$/, 18 | loader: require.resolve('babel-loader'), 19 | include: path.join(__dirname, 'src'), 20 | }, 21 | ], 22 | }, 23 | resolve: { 24 | extensions: ['.js', '.ts', '.json'], 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/keyTest.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export default React => { 4 | const Item = props => { 5 | return
item {props.label}
6 | } 7 | 8 | // class Item extends React.Component { 9 | // render() { 10 | // return
item {this.props.label}
11 | // } 12 | // } 13 | 14 | const KeyTest = () => { 15 | return ( 16 |
17 | {_.times(3, n => { 18 | return ( 19 | 23 | ) 24 | })} 25 |
26 | ) 27 | } 28 | 29 | return KeyTest 30 | } 31 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/hooksTests/useStateTest2.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | const useState = React.useState 3 | const UseStateTest2 = () => { 4 | const [clicks, setClicks] = useState(0) 5 | const [clicks2, setClicks2] = useState(0) 6 | const ele = ( 7 |
8 |
9 | clicks: {clicks} 10 | 11 |
12 |
13 | clicks2: {clicks2} 14 | 15 |
16 |
17 | ) 18 | return ele 19 | } 20 | return UseStateTest2 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "how-react-works", 3 | "description": "", 4 | "private": true, 5 | "scripts": { 6 | "start": "cd packages/example-simple && npm run start", 7 | "build": "cd packages/my-react && yarn build" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/gaoxiaoliangz/react-lite.git" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "bugs": { 16 | "url": "https://github.com/gaoxiaoliangz/react-lite/issues" 17 | }, 18 | "homepage": "https://github.com/gaoxiaoliangz/react-lite#readme", 19 | "devDependencies": { 20 | "lerna": "^3.4.3" 21 | }, 22 | "workspaces": [ 23 | "packages/*" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/diffTests/updateAttrs.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | return class UpdateAttrs extends React.Component { 3 | state = { 4 | flag: false, 5 | } 6 | 7 | render() { 8 | const { flag } = this.state 9 | return ( 10 |
11 | flag: {flag.toString()} 12 | 21 |
text
22 |
23 | ) 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/example-simple/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | example simple 13 | 14 | 15 |
16 |
17 |
18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/hooksTests/useStateTest3.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | const useState = React.useState 3 | const UseStateTest2 = () => { 4 | const [clicks, setClicks] = useState(0) 5 | const ele = ( 6 |
7 | clicks: {clicks} 8 | 9 |
10 | ) 11 | return ele 12 | } 13 | 14 | return class UseStateTest3 extends React.Component { 15 | state = {} 16 | render() { 17 | return ( 18 |
19 | 20 | 21 |
22 | ) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/my-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-react", 3 | "version": "0.6.0", 4 | "main": "src/index.js", 5 | "scripts": { 6 | "build": "webpack" 7 | }, 8 | "license": "MIT", 9 | "private": true, 10 | "devDependencies": { 11 | "@babel/core": "^7.1.2", 12 | "@babel/plugin-proposal-class-properties": "^7.1.0", 13 | "@babel/plugin-proposal-object-rest-spread": "^7.0.0", 14 | "@babel/preset-env": "^7.1.0", 15 | "babel-loader": "^8.0.4", 16 | "babel-plugin-lodash": "^3.3.4", 17 | "webpack": "^4.23.1", 18 | "webpack-cli": "^3.1.2" 19 | }, 20 | "dependencies": { 21 | "@types/lodash": "^4.14.117", 22 | "invariant": "^2.2.4", 23 | "lodash": "^4.17.11" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/my-react/src/component.js: -------------------------------------------------------------------------------- 1 | import patch from './patch' 2 | 3 | export default class Component { 4 | constructor(props) { 5 | this.props = props 6 | this.state = null 7 | } 8 | 9 | setState(state, cb) { 10 | setTimeout(() => { 11 | const prevState = this.state 12 | const prevProps = this.props 13 | this.state = { 14 | ...prevState, 15 | ...state, 16 | } 17 | const rendered = this.render() 18 | patch(rendered, this._vNode.rendered) 19 | this._vNode.rendered = rendered 20 | if (cb) cb() 21 | if (this.componentDidUpdate) { 22 | this.componentDidUpdate(prevProps, prevState) 23 | } 24 | }) 25 | } 26 | } 27 | 28 | Component.$IS_CLASS = true 29 | -------------------------------------------------------------------------------- /packages/example-simple/src/diffRendered.js: -------------------------------------------------------------------------------- 1 | const slice = (arr, start, end) => { 2 | return arr.slice(start < 0 ? 0 : start, end) 3 | } 4 | 5 | const diffRendered = (html1, html2) => { 6 | const chars1 = Array.from(html1) 7 | const chars2 = Array.from(html2) 8 | 9 | let differentIdx = -1 10 | 11 | for (let i = 0; i < chars1.length; i++) { 12 | if (chars1[i] !== chars2[i]) { 13 | differentIdx = i 14 | break 15 | } 16 | } 17 | 18 | if (differentIdx === -1) { 19 | return null 20 | } 21 | const range = 50 22 | 23 | return [ 24 | slice(chars1, differentIdx - range, differentIdx + range).join(''), 25 | slice(chars2, differentIdx - range, differentIdx + range).join(''), 26 | ] 27 | } 28 | 29 | export default diffRendered 30 | -------------------------------------------------------------------------------- /packages/my-react/src/dom/utils.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export function getAttrs(props) { 4 | return _.flow( 5 | _.curryRight(_.omitBy)((v, k) => ['children'].includes(k)), 6 | _.curryRight(_.mapKeys)((v, k) => { 7 | if (k === 'className') { 8 | return 'class' 9 | } 10 | return k 11 | }), 12 | )(props) 13 | } 14 | 15 | export function updateAttrs(dom, attrsObject) { 16 | _.forEach(attrsObject, (v, k) => { 17 | dom.setAttribute(k, v) 18 | }) 19 | } 20 | 21 | export function getNodeIndex(node) { 22 | const parent = node.parentNode 23 | for (let index = 0; parent.childNodes.length > index; index++) { 24 | if (node === parent.childNodes[index]) { 25 | return index 26 | } 27 | } 28 | return -1 29 | } 30 | -------------------------------------------------------------------------------- /packages/example-simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-simple", 3 | "version": "0.6.0", 4 | "private": true, 5 | "dependencies": { 6 | "react-scripts": "3.1.1", 7 | "inferno": "^6.1.4", 8 | "inferno-compat": "^6.1.4", 9 | "lodash": "^4.17.4", 10 | "my-react": "^0.6.0", 11 | "react": "16.7.0-alpha.0", 12 | "react-dom": "16.7.0-alpha.0", 13 | "react-redux": "^5.1.0", 14 | "redux": "^4.0.1" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": "react-app" 24 | }, 25 | "browserslist": [ 26 | ">0.2%", 27 | "not dead", 28 | "not ie <= 11", 29 | "not op_mini all" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/diffTests/reorderTextNodes2.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | class ReorderTextNodes2 extends React.Component { 3 | state = { 4 | showFirst: false, 5 | } 6 | 7 | updateClicks = clicks => { 8 | this.setState({ 9 | clicks, 10 | }) 11 | } 12 | 13 | render() { 14 | const { showFirst } = this.state 15 | return ( 16 |
17 |
18 | 27 |
28 | {showFirst ? ['0', '1', '2'] : ['1', '2']} 29 |
30 | ) 31 | } 32 | } 33 | return ReorderTextNodes2 34 | } 35 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/setStateTests/mutateState.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | return class MutateState extends React.Component { 3 | state = { 4 | clicks: 0, 5 | } 6 | 7 | render() { 8 | const { clicks } = this.state 9 | return ( 10 |
11 | clicks: {clicks} 12 | 22 | 29 |
30 | ) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-lite 2 | 3 | A simple implementation of react 4 | 5 | ## Why creating this repo 6 | 7 | I want to understand how react works, the best way to do it is to implement it my own way. 8 | 9 | The package is at very early stages of development, so only a handful of react features are supported. 10 | 11 | ## What's been included 12 | 13 | React 14 | 15 | - createElement 16 | 17 | Function component 18 | 19 | - hooks: useState 20 | 21 | Class component 22 | 23 | - setState 24 | - componentDidMount 25 | - componentDidUpdate 26 | - componentWillUnmount 27 | 28 | ReactDOM 29 | 30 | - render 31 | 32 | ## How to run 33 | 34 | ``` 35 | lerna bootstrap 36 | yarn build --watch 37 | yarn start 38 | ``` 39 | 40 | The demo runs two versions of react in two columns with identical components, which I've created for testing purposes, left side is my version, right side is react. 41 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/unmountTest.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | class Comp extends React.Component { 3 | componentDidMount() { 4 | console.log('mount') 5 | } 6 | 7 | componentWillUnmount() { 8 | console.log('unmount') 9 | } 10 | 11 | render() { 12 | return
Comp
13 | } 14 | } 15 | 16 | return class UnmountTest extends React.Component { 17 | state = { 18 | flag: true, 19 | } 20 | 21 | render() { 22 | const { flag } = this.state 23 | return ( 24 |
25 | show: {flag.toString()} 26 | 35 | {flag && } 36 |
37 | ) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/setStateTests/setStateTest.jsx: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant' 2 | 3 | export default React => { 4 | class SetStateTest extends React.Component { 5 | state = { 6 | clicks: 0, 7 | } 8 | 9 | updateClicks = clicks => { 10 | this.setState({ 11 | clicks, 12 | }) 13 | invariant(clicks !== this.state.clicks, 'setState should be async!') 14 | } 15 | 16 | handleAddClick = () => { 17 | this.updateClicks(this.state.clicks + 1) 18 | } 19 | 20 | render() { 21 | const { clicks } = this.state 22 | return ( 23 |
24 | clicks: {clicks} 25 | 26 | 29 |
30 | ) 31 | } 32 | } 33 | return SetStateTest 34 | } 35 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/diffTests/reorderTextNodes.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | class ReorderTextNodes extends React.Component { 3 | state = { 4 | showFirst: false, 5 | } 6 | 7 | updateClicks = clicks => { 8 | this.setState({ 9 | clicks, 10 | }) 11 | } 12 | 13 | render() { 14 | const { showFirst } = this.state 15 | return ( 16 |
17 |
18 | 27 |
28 |
29 | {showFirst && '0'} 30 | {'1'} 31 | {'2'} 32 |
33 |
34 | ) 35 | } 36 | } 37 | return ReorderTextNodes 38 | } 39 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/didUpdateTest.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | class Comp extends React.Component { 3 | componentDidUpdate(prevProps, prevState) { 4 | console.log('Comp', prevProps, prevState) 5 | } 6 | 7 | render() { 8 | return
Comp show: {this.props.flag.toString()}
9 | } 10 | } 11 | 12 | return class DidUpdateTest extends React.Component { 13 | state = { 14 | flag: true, 15 | } 16 | 17 | componentDidUpdate(prevProps, prevState) { 18 | console.log('DidUpdateTest', prevProps, prevState) 19 | } 20 | 21 | render() { 22 | const { flag } = this.state 23 | return ( 24 |
25 | 34 | 35 |
36 | ) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/lifecycleTest.jsx: -------------------------------------------------------------------------------- 1 | import invariant from 'invariant' 2 | 3 | export default React => { 4 | const order = [] 5 | class InnerDeep extends React.Component { 6 | componentDidMount() { 7 | order.push(0) 8 | } 9 | 10 | render() { 11 | return
inner deep
12 | } 13 | } 14 | 15 | class Inner extends React.Component { 16 | componentDidMount() { 17 | order.push(1) 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 |
inner
24 | 25 |
26 | ) 27 | } 28 | } 29 | 30 | class LifecycleTest extends React.Component { 31 | componentDidMount() { 32 | order.push(2) 33 | invariant(order.join(',') === '0,1,2', 'order is wrong!') 34 | } 35 | 36 | render() { 37 | return ( 38 |
39 |
lifecycle
40 | 41 |
42 | ) 43 | } 44 | } 45 | 46 | return LifecycleTest 47 | } 48 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/setStateTests/multipleSetStateCalls.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | return class MultipleSetStateCalls extends React.Component { 3 | state = { 4 | clicks: 0, 5 | clicks2: 0, 6 | } 7 | 8 | render() { 9 | const { clicks, clicks2 } = this.state 10 | return ( 11 |
12 | clicks: {clicks}, clicks2: {clicks2} 13 | 22 | 34 |
35 | ) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/diffTests/reorderTest.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | return class ReorderTest extends React.Component { 3 | state = { 4 | orderFlag: false, 5 | } 6 | 7 | render() { 8 | const { orderFlag } = this.state 9 | const items = [ 10 |
11 | 0 12 |
, 13 |
14 | 1 15 |
, 16 | ] 17 | return ( 18 |
19 |
20 | 29 |
30 |
31 | {(orderFlag ? [0, 1] : [1, 0]).map(order => { 32 | return items[order] 33 | })} 34 |
2
35 |
36 |
37 | ) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/example-simple/src/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | .root-wrap { 5 | overflow-y: auto; 6 | position: fixed; 7 | height: 100%; 8 | width: 100%; 9 | left: 0; 10 | top: 0; 11 | } 12 | body { 13 | } 14 | p { 15 | margin: 0; 16 | } 17 | .left, .right { 18 | min-height: calc(100%); 19 | } 20 | .left { 21 | float: left; 22 | } 23 | .right { 24 | float: right; 25 | } 26 | .sep { 27 | height: 2px; 28 | width: 100%; 29 | background: #333; 30 | } 31 | #root, 32 | #root2 { 33 | width: 50%; 34 | padding: 30px; 35 | } 36 | #root2 { 37 | border-left: 1px solid #ddd; 38 | } 39 | body { 40 | margin: 0; 41 | padding: 0; 42 | font-family: Arial, Helvetica, sans-serif; 43 | } 44 | .test-container { 45 | border-bottom: 1px solid #ddd; 46 | margin-bottom: 20px; 47 | padding-bottom: 20px; 48 | } 49 | .test-container h3 { 50 | color: #666; 51 | font-weight: normal; 52 | font-size: 12px; 53 | } 54 | .green { 55 | color: green; 56 | } 57 | .red { 58 | color: red; 59 | } 60 | .bg-green { 61 | background: green; 62 | } 63 | .bg-red { 64 | background: red; 65 | } -------------------------------------------------------------------------------- /packages/example-simple/src/tests/refTests/refTest.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export default React => { 4 | // const Item = props => { 5 | // return
item {props.label}
6 | // } 7 | 8 | class Item extends React.Component { 9 | render() { 10 | return
item {this.props.label}
11 | } 12 | } 13 | 14 | return class RefTest extends React.Component { 15 | render() { 16 | return ( 17 |
{ 19 | console.log('dom ref', ref) 20 | }} 21 | > 22 | 29 | {_.times(3, n => { 30 | return ( 31 | { 33 | console.log('item ref', ref) 34 | }} 35 | key={n} 36 | label={n} 37 | /> 38 | ) 39 | })} 40 |
41 | ) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | /dist 61 | .cache 62 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/diffTests/unkeyedTest.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | class UnkeyedTest extends React.Component { 3 | state = { 4 | showFirst: false, 5 | } 6 | 7 | componentDidMount() { 8 | this.item.setAttribute('class', 'green') 9 | } 10 | 11 | updateClicks = clicks => { 12 | this.setState({ 13 | clicks, 14 | }) 15 | } 16 | 17 | render() { 18 | const { showFirst } = this.state 19 | return ( 20 |
21 |
22 | 31 |
32 | {showFirst ? ( 33 |
34 |
0
35 |
1
36 |
2
37 |
38 | ) : ( 39 |
40 |
(this.item = ref)}>1
41 |
2
42 |
43 | )} 44 |
45 | ) 46 | } 47 | } 48 | return UnkeyedTest 49 | } 50 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/diffTests/keyedTest.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | class KeyedTest extends React.Component { 3 | state = { 4 | showFirst: false, 5 | } 6 | 7 | componentDidMount() { 8 | this.item.setAttribute('class', 'green') 9 | } 10 | 11 | updateClicks = clicks => { 12 | this.setState({ 13 | clicks, 14 | }) 15 | } 16 | 17 | render() { 18 | const { showFirst } = this.state 19 | const stuff = ( 20 |
21 | {showFirst ?
0
: null} 22 |
{ 24 | this.item = ref 25 | }} 26 | > 27 | 1 28 |
29 |
2
30 |
31 | ) 32 | return ( 33 |
34 |
35 | 44 |
45 | {stuff} 46 |
47 | ) 48 | } 49 | } 50 | return KeyedTest 51 | } 52 | -------------------------------------------------------------------------------- /packages/my-react/src/hooks.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import patch from './patch' 3 | 4 | let stateId = 0 5 | 6 | export const funcStates = new WeakMap() 7 | 8 | export const reactState = { 9 | currentVNode: null, 10 | isCreatingState: null, 11 | } 12 | 13 | export const useState = initialState => { 14 | const currentVNode = reactState.currentVNode 15 | let currentState = initialState 16 | let cursor 17 | let store = funcStates.get(currentVNode) 18 | if (!store) { 19 | cursor = 0 20 | store = { 21 | cursor, 22 | states: [initialState], 23 | stateId, 24 | } 25 | stateId++ 26 | funcStates.set(currentVNode, store) 27 | } else if (reactState.isCreatingState === true) { 28 | store.cursor++ 29 | cursor = store.cursor 30 | store.states.push(initialState) 31 | } else { 32 | store.cursor++ 33 | cursor = store.cursor 34 | currentState = store.states[cursor] 35 | } 36 | 37 | const setter = newState => { 38 | store.states[cursor] = newState 39 | reactState.currentVNode = currentVNode 40 | reactState.isCreatingState = false 41 | 42 | // reset cursor for the next render 43 | store.cursor = -1 44 | const rendered = currentVNode.type(currentVNode.props) 45 | patch(rendered, currentVNode.rendered) 46 | currentVNode.rendered = rendered 47 | } 48 | return [currentState, setter] 49 | } 50 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/reduxTest.jsx: -------------------------------------------------------------------------------- 1 | import { createStore, combineReducers } from 'redux' 2 | 3 | const updateCount = count => { 4 | return { 5 | type: 'update-count', 6 | payload: count, 7 | } 8 | } 9 | 10 | const reducers = combineReducers({ 11 | count: (state = 0, action) => { 12 | switch (action.type) { 13 | case 'update-count': 14 | return action.payload 15 | 16 | default: 17 | return state 18 | } 19 | }, 20 | }) 21 | 22 | export default React => { 23 | const store = createStore(reducers) 24 | class ReduxTest extends React.Component { 25 | componentDidMount() { 26 | store.subscribe(() => { 27 | this.setState({}) 28 | }) 29 | } 30 | 31 | render() { 32 | const currentCount = store.getState().count 33 | return ( 34 |
35 |
36 | 43 | {currentCount} 44 | 51 |
52 |
53 | ) 54 | } 55 | } 56 | return ReduxTest 57 | } 58 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/reactReduxTest.jsx: -------------------------------------------------------------------------------- 1 | import { Provider, connect } from 'react-redux' 2 | import { createStore, combineReducers } from 'redux' 3 | 4 | const updateCount = count => { 5 | return { 6 | type: 'update-count', 7 | payload: count, 8 | } 9 | } 10 | 11 | const reducers = combineReducers({ 12 | count: (state = 0, action) => { 13 | switch (action.type) { 14 | case 'update-count': 15 | return action.payload 16 | 17 | default: 18 | return state 19 | } 20 | }, 21 | }) 22 | 23 | export default React => { 24 | const store = createStore(reducers) 25 | 26 | const ReactReduxTest = ({ count, updateCount }) => { 27 | return ( 28 |
29 |
30 | 31 | {count} 32 | 33 |
34 |
35 | ) 36 | } 37 | 38 | const ReactReduxTest2 = connect( 39 | (state, ownProps) => { 40 | return { 41 | count: state.count, 42 | } 43 | }, 44 | { updateCount }, 45 | )(ReactReduxTest) 46 | 47 | class ReactReduxTestWrapper extends React.Component { 48 | render() { 49 | return ( 50 | 51 | 52 | 53 | ) 54 | } 55 | } 56 | 57 | return ReactReduxTestWrapper 58 | } 59 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/form/inputTest.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | return class InputTest extends React.Component { 3 | state = { 4 | input: '', 5 | } 6 | 7 | render() { 8 | const { input } = this.state 9 | return ( 10 |
11 | 20 | 27 |
28 | { 32 | this.setState({ 33 | input: e.target.value, 34 | }) 35 | }} 36 | onChange={e => { 37 | this.setState({ 38 | input: e.target.value, 39 | }) 40 | }} 41 | /> 42 |
43 |
44 | controlled input 45 | {}} 49 | onChange={e => {}} 50 | /> 51 |
52 |
53 | ) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/diffTests/manuallyKeyedTest.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export default React => { 4 | class ReconcileTest2 extends React.Component { 5 | state = { 6 | showFirst: false, 7 | } 8 | 9 | componentDidMount() { 10 | this.item.setAttribute('class', 'green') 11 | } 12 | 13 | updateClicks = clicks => { 14 | this.setState({ 15 | clicks, 16 | }) 17 | } 18 | 19 | render() { 20 | const { showFirst } = this.state 21 | return ( 22 |
23 |
24 | 33 |
34 | 54 |
55 | ) 56 | } 57 | } 58 | return ReconcileTest2 59 | } 60 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/setStateTests/propTest.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | class Click extends React.Component { 3 | state = {} 4 | 5 | render() { 6 | return ( 7 |
8 | Click (Class Component):{' '} 9 | 19 | {this.props.children} 20 |
21 | ) 22 | } 23 | } 24 | 25 | class Comp extends React.Component { 26 | state = {} 27 | 28 | render() { 29 | return ( 30 |
31 | Comp (Class Component), prop label:{' '} 32 | {this.props.label && {this.props.label}} 33 | {this.props.children} 34 |
35 | ) 36 | } 37 | } 38 | 39 | return class PropTest extends React.Component { 40 | state = { 41 | testCbClicks: 0, 42 | } 43 | 44 | render() { 45 | const { testCbClicks } = this.state 46 | return ( 47 |
48 | { 50 | this.setState({ 51 | testCbClicks: testCbClicks + 1, 52 | }) 53 | }} 54 | > 55 | 56 | 57 |
58 | ) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/diffTests/nestedKeyedTest.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | class NestedKeyedTest extends React.Component { 3 | state = { 4 | showFirst: false, 5 | } 6 | 7 | componentDidMount() { 8 | this.item.setAttribute('class', 'green') 9 | } 10 | 11 | updateClicks = clicks => { 12 | this.setState({ 13 | clicks, 14 | }) 15 | } 16 | 17 | render() { 18 | const { showFirst } = this.state 19 | const stuff = ( 20 |
21 | {showFirst ? ( 22 |
23 |
24 |
0
25 |
26 |
27 | ) : null} 28 |
29 |
30 |
{ 32 | this.item = ref 33 | }} 34 | > 35 | 1 36 |
37 |
38 |
39 |
40 |
41 |
2
42 |
43 |
44 |
45 | ) 46 | return ( 47 |
48 |
49 | 58 |
59 | {stuff} 60 |
61 | ) 62 | } 63 | } 64 | return NestedKeyedTest 65 | } 66 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/setStateTests/setStateWithChildren.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | class Click extends React.Component { 3 | state = {} 4 | 5 | render() { 6 | return ( 7 |
8 | Click (Class Component):{' '} 9 | 18 | {this.props.children} 19 |
20 | ) 21 | } 22 | } 23 | 24 | class Comp extends React.Component { 25 | state = {} 26 | 27 | render() { 28 | return ( 29 |
30 |
Comp (Class Component)
31 | {this.props.children} 32 |
33 | ) 34 | } 35 | } 36 | 37 | const Comp2 = () => { 38 | return
Comp2 (Function Component)
39 | } 40 | 41 | return class SetStateWithChildren extends React.Component { 42 | state = { 43 | clicks: 0, 44 | } 45 | 46 | render() { 47 | const { clicks } = this.state 48 | return ( 49 |
50 | { 52 | this.setState({ 53 | clicks: clicks + 1, 54 | }) 55 | }} 56 | > 57 |
clicks: {clicks}
58 | 59 | 60 | 61 | 62 |
63 |
64 | ) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/example-simple/src/app.jsx: -------------------------------------------------------------------------------- 1 | import section from './section' 2 | import testGroups from './allTests' 3 | 4 | export default (React, { onUpdate, activeGroup, activeTest }) => { 5 | const Section = section(React) 6 | 7 | class App extends React.Component { 8 | componentDidMount() { 9 | onUpdate() 10 | } 11 | 12 | componentDidUpdate() { 13 | onUpdate() 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 | {testGroups 20 | .filter( 21 | (group, groupIdx) => 22 | activeGroup === '' ? true : groupIdx === +activeGroup, 23 | ) 24 | .map((group, gIdx) => { 25 | return ( 26 |
27 |

{group.desc}

28 | {group.children 29 | .filter( 30 | (child, childIdx) => 31 | activeTest === '' ? true : childIdx === +activeTest, 32 | ) 33 | .map((child, idx) => { 34 | const TestComp = child.test(React) 35 | return ( 36 |
37 |
38 | 39 |
40 |
41 | ) 42 | })} 43 |
44 | ) 45 | })} 46 |
47 | ) 48 | } 49 | } 50 | return App 51 | } 52 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/setStateTests/callbackAndSetState.jsx: -------------------------------------------------------------------------------- 1 | export default React => { 2 | let first = true 3 | 4 | class Click extends React.Component { 5 | state = {} 6 | 7 | render() { 8 | return ( 9 |
10 | Click (Class Component):{' '} 11 | 21 | {this.props.children} 22 |
23 | ) 24 | } 25 | } 26 | 27 | class Comp extends React.Component { 28 | state = {} 29 | 30 | componentDidMount() { 31 | if (first) { 32 | this.comp.setAttribute('class', 'green') 33 | first = false 34 | } 35 | } 36 | 37 | render() { 38 | return ( 39 |
{ 41 | this.comp = ref 42 | }} 43 | className="Comp red" 44 | > 45 | Comp (Class Component), prop label:{' '} 46 | {this.props.label && {this.props.label}} 47 | {this.props.children} 48 |
49 | ) 50 | } 51 | } 52 | 53 | return class CallbackAndSetState extends React.Component { 54 | state = { 55 | testCbClicks: 0, 56 | } 57 | 58 | render() { 59 | const { testCbClicks } = this.state 60 | return ( 61 |
62 | { 64 | this.setState({ 65 | testCbClicks: testCbClicks + 1, 66 | }) 67 | }} 68 | > 69 | 70 | 71 |
72 | ) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/childrenTest.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | 3 | export default React => { 4 | const ChildrenTest = () => { 5 | const Block = ({ title, children }) => { 6 | return ( 7 |
8 |

{title}

9 | {children} 10 |
11 | ) 12 | } 13 | 14 | const theThirdBlock = ( 15 | 16 | {[1, 2, 3, [4, 5, [6, 7]]]} 17 | the child 18 | and the other 19 | 20 | ) 21 | 22 | const content = ( 23 |
24 | {React.createElement( 25 | 'div', 26 | {}, 27 | _.times(3, n => { 28 | return
{n} with times
29 | }), 30 | [ 31 |
0 manually
, 32 |
1 manually
, 33 |
2 manually
, 34 | [ 35 |
0 manually deep
, 36 |
1 manually deep
, 37 |
2 manually deep
, 38 | ], 39 | ], 40 |
0 manually in arg
, 41 |
1 manually in arg
, 42 |
2 manually in arg
, 43 | ..._.times(3, n => { 44 | return
{n} with times in arg
45 | }), 46 | [1, 2, 3], 47 | 'str', 48 | 'str2', 49 | ['str3', 'str4', ['str5', 'str6', ['str7']]] 50 | )} 51 |
52 | 53 | 54 | the child 55 | 56 | 57 | the child 58 | and the other 59 | 60 | {theThirdBlock} 61 |
62 |
63 | ) 64 | return content 65 | } 66 | return ChildrenTest 67 | } 68 | -------------------------------------------------------------------------------- /packages/example-simple/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import React1, { ReactDOM as ReactDOM1 } from 'my-react' 4 | import React2, { render as react2Render } from 'inferno-compat' 5 | import invariant from 'invariant' 6 | import app from './app' 7 | import './style.css' 8 | import diffRendered from './diffRendered' 9 | import Controller from './Controller/Controller' 10 | import parseQuery from './parseQuery' 11 | 12 | const reactMap = { 13 | react: { 14 | react: React, 15 | reactDom: ReactDOM, 16 | }, 17 | myReact: { 18 | react: React1, 19 | reactDom: ReactDOM1, 20 | }, 21 | inferno: { 22 | react: React2, 23 | reactDom: { 24 | render: react2Render, 25 | }, 26 | }, 27 | } 28 | 29 | const mount = ({ 30 | domId, 31 | whichReact = reactMap.react, 32 | onUpdate = () => {}, 33 | component, 34 | }) => { 35 | const { react, reactDom } = whichReact 36 | const queryObj = parseQuery(window.location.search.substr(1)) 37 | const App = 38 | component || 39 | app(react, { 40 | render: reactDom.render, 41 | onUpdate, 42 | activeGroup: queryObj.group, 43 | activeTest: queryObj.test, 44 | }) 45 | reactDom.render(react.createElement(App), document.getElementById(domId)) 46 | } 47 | 48 | const render = () => { 49 | // mount controller 50 | mount({ 51 | domId: 'controller', 52 | component: Controller, 53 | }) 54 | 55 | // mount my react 56 | mount({ 57 | domId: 'root', 58 | whichReact: reactMap.myReact, 59 | }) 60 | 61 | // mount facebook react 62 | mount({ 63 | domId: 'root2', 64 | whichReact: reactMap.react, 65 | onUpdate: () => { 66 | setTimeout(() => { 67 | const diff = diffRendered( 68 | document.getElementById('root').innerHTML, 69 | document.getElementById('root2').innerHTML, 70 | ) 71 | invariant(!diff, `renders differently!\n${(diff || []).join('\n')}`) 72 | }, 100) 73 | }, 74 | }) 75 | } 76 | 77 | render() 78 | window.__render = render 79 | -------------------------------------------------------------------------------- /packages/example-simple/src/tests/reconcileTest.jsx: -------------------------------------------------------------------------------- 1 | import _ from 'lodash' 2 | import './reconcileTest.css' 3 | 4 | export default React => { 5 | // const ItemA = () =>
itemA
6 | // const ItemB = () =>
itemB
7 | 8 | class ReconcileTest extends React.Component { 9 | state = { 10 | clicks: 0, 11 | orderFlag: true, 12 | hasLeadingEle: false, 13 | } 14 | 15 | componentDidMount() { 16 | this.flag.setAttribute('class', 'box bg-green') 17 | this.itemA.setAttribute('class', 'green') 18 | } 19 | 20 | updateClicks = clicks => { 21 | this.setState({ 22 | clicks, 23 | }) 24 | } 25 | 26 | reorder = () => { 27 | this.setState({ 28 | orderFlag: !this.state.orderFlag, 29 | }) 30 | } 31 | 32 | render() { 33 | const { clicks, orderFlag, hasLeadingEle } = this.state 34 | // const itemA = (this.itemA = ref)} /> 35 | // const itemB = 36 | 37 | const itemA =
(this.itemA = ref)}>itemA
38 | const itemB =
itemB
39 | // const itemA =
(this.itemA = ref)}>itemA
40 | // const itemB = itemB 41 | return ( 42 |
43 |
44 | 53 | 54 |
55 |
56 | {hasLeadingEle &&
leading ele
} 57 |
(this.flag = ref)} /> 58 | 59 | {clicks} 60 | 61 | {_.times(3, n => { 62 | return
{n}
63 | })} 64 | {orderFlag ? itemA : itemB} 65 | {!orderFlag ? itemA : itemB} 66 |
67 |
68 | ) 69 | } 70 | } 71 | return ReconcileTest 72 | } 73 | -------------------------------------------------------------------------------- /packages/example-simple/src/Controller/Controller.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import testGroups from '../allTests' 3 | import './Controller.css' 4 | import parseQuery from '../parseQuery' 5 | 6 | const withAll = arr => { 7 | return [{ desc: 'all' }, ...arr].map((item, idx) => ({ 8 | ...item, 9 | index: idx === 0 ? '' : idx - 1, 10 | })) 11 | } 12 | 13 | class Controller extends React.Component { 14 | state = { 15 | activeGroup: '', 16 | activeTest: '', 17 | } 18 | 19 | componentDidMount() { 20 | const queryObj = parseQuery(window.location.search.substr(1)) 21 | this.setState({ 22 | activeGroup: queryObj.group, 23 | activeTest: queryObj.test, 24 | }) 25 | } 26 | 27 | goto = () => { 28 | window.location.href = `/?group=${this.state.activeGroup}&test=${ 29 | this.state.activeTest 30 | }` 31 | } 32 | 33 | render() { 34 | const { activeGroup, activeTest } = this.state 35 | const tests = testGroups[activeGroup] 36 | ? testGroups[activeGroup].children 37 | : [] 38 | return ( 39 |
40 | 41 | 62 | {activeGroup !== '' && ( 63 | 64 | 65 | 84 | 85 | )} 86 |
87 | ) 88 | } 89 | } 90 | 91 | export default Controller 92 | -------------------------------------------------------------------------------- /packages/my-react/src/dom/mount.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import invariant from 'invariant' 3 | import { FLAGS } from '../vnode' 4 | import { updateAttrs } from './utils' 5 | import { addListeners } from './event' 6 | import { reactState, funcStates } from '../hooks'; 7 | 8 | const createDOMElement = vNode => { 9 | const { ref } = vNode 10 | const dom = document.createElement(vNode.type) 11 | updateAttrs(dom, vNode.attributes) 12 | addListeners(dom, vNode.listeners) 13 | vNode.children.forEach(child => { 14 | mount(child, dom) 15 | }) 16 | return dom 17 | } 18 | 19 | const createDOMTextElement = vNode => { 20 | const type = typeof vNode.textContent 21 | const textContent = 22 | type === 'number' || type === 'string' ? vNode.textContent.toString() : '' 23 | const dom = document.createTextNode(textContent) 24 | return dom 25 | } 26 | 27 | const mountChild = (child, parent) => { 28 | parent.appendChild(child) 29 | } 30 | 31 | // mount 32 | const mountClassComponent = (vNode, parentDOM) => { 33 | const instance = new vNode.type(vNode.props) 34 | vNode.instance = instance 35 | instance._vNode = vNode 36 | const rendered = instance.render() 37 | vNode.rendered = rendered 38 | const dom = mount(rendered, parentDOM) 39 | vNode.dom = dom 40 | if (instance.componentDidMount) { 41 | instance.componentDidMount() 42 | } 43 | // @todo: 这里处理 ref 的时机和 react 并不一样,不知道 react 是出于什么考虑 44 | if (vNode.ref) { 45 | vNode.ref(instance) 46 | } 47 | return dom 48 | } 49 | 50 | const mountFunctionComponent = (vNode, parentDOM) => { 51 | reactState.currentVNode = vNode 52 | reactState.isCreatingState = true 53 | const rendered = vNode.type(vNode.props) 54 | // reset cursor if function component uses hooks 55 | if (funcStates.get(vNode)) { 56 | funcStates.get(vNode).cursor = -1 57 | } 58 | vNode.rendered = rendered 59 | const dom = mount(rendered, parentDOM) 60 | vNode.dom = dom 61 | return dom 62 | } 63 | 64 | const mountText = (vNode, parentDOM) => { 65 | const textNode = createDOMTextElement(vNode) 66 | mountChild(textNode, parentDOM) 67 | vNode.dom = textNode 68 | return textNode 69 | } 70 | 71 | const mountElement = (vNode, parentDOM) => { 72 | const dom = createDOMElement(vNode) 73 | mountChild(dom, parentDOM) 74 | if (vNode.ref) { 75 | vNode.ref(dom) 76 | } 77 | vNode.dom = dom 78 | return dom 79 | } 80 | 81 | const mount = (vNode, parentDOM) => { 82 | const { flag } = vNode 83 | let dom 84 | 85 | switch (flag) { 86 | case FLAGS.CLASS: 87 | dom = mountClassComponent(vNode, parentDOM) 88 | break 89 | case FLAGS.FUNC: 90 | dom = mountFunctionComponent(vNode, parentDOM) 91 | break 92 | case FLAGS.ELEMENT: 93 | dom = mountElement(vNode, parentDOM) 94 | break 95 | case FLAGS.TEXT: 96 | dom = mountText(vNode, parentDOM) 97 | break 98 | default: 99 | throw new Error(`Unknown flag ${flag}`) 100 | } 101 | 102 | invariant(dom, 'mount dom null') 103 | return dom 104 | } 105 | 106 | export default mount 107 | -------------------------------------------------------------------------------- /packages/my-react/src/patch.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import _ from 'lodash' 3 | import invariant from 'invariant' 4 | import { FLAGS } from './vnode' 5 | import mount from './dom/mount' 6 | import unmount from './dom/unmount' 7 | import { updateAttrs, getNodeIndex } from './dom/utils' 8 | import { removeListeners, addListeners } from './dom/event' 9 | import { reactState, funcStates } from './hooks' 10 | 11 | const patchClassComponent = (vNode, prevVNode) => { 12 | invariant(prevVNode.dom !== null, 'patchClassComponent dom null') 13 | vNode.instance = prevVNode.instance 14 | const oldProps = vNode.instance.props 15 | vNode.instance.props = vNode.props 16 | // 因为这个问题排查了很久,一开始表现为 dom 为 null,事件触发两次 17 | vNode.instance._vNode = vNode 18 | const newRendered = prevVNode.instance.render() 19 | vNode.rendered = newRendered 20 | vNode.dom = prevVNode.dom 21 | 22 | const patchResult = patch(newRendered, prevVNode.rendered) 23 | if (vNode.instance.componentDidUpdate) { 24 | vNode.instance.componentDidUpdate(oldProps, vNode.instance.state) 25 | } 26 | // @todo: react 里面,在 update 阶段,一开始 ref 会是 null 27 | if (vNode.ref) { 28 | vNode.ref(vNode.instance) 29 | } 30 | return patchResult 31 | } 32 | 33 | const patchFunctionComponent = (vNode, prevVNode) => { 34 | const prevStore = funcStates.get(prevVNode) 35 | if (prevStore) { 36 | // reset cursor before rendering 37 | prevStore.cursor = -1 38 | funcStates.set(vNode, prevStore) 39 | funcStates.delete(prevVNode) 40 | } 41 | reactState.currentVNode = vNode 42 | reactState.isCreatingState = false 43 | const newRendered = vNode.type(vNode.props) 44 | // 需要手动更新 rendered 45 | // 其实可以写成 vNode.render() 然后自己更新内部状态,但那样太 OO 了 46 | vNode.rendered = newRendered 47 | vNode.dom = prevVNode.dom 48 | invariant(prevVNode.dom !== null, 'patchFunctionComponent dom null') 49 | return patch(newRendered, prevVNode.rendered) 50 | } 51 | 52 | const patchElement = (vNode, prevVNode) => { 53 | vNode.dom = prevVNode.dom 54 | if (!_.isEqual(vNode.attributes, prevVNode.attributes)) { 55 | updateAttrs(vNode.dom, vNode.attributes) 56 | } 57 | invariant(prevVNode.dom !== null, 'patchElement dom null') 58 | removeListeners(prevVNode.dom, prevVNode.listeners) 59 | addListeners(vNode.dom, vNode.listeners) 60 | patchChildren(vNode.children, prevVNode.children, prevVNode.dom) 61 | if (vNode.ref) { 62 | vNode.ref(vNode.dom) 63 | } 64 | } 65 | 66 | const patchTextElement = (vNode, prevVNode) => { 67 | // 这个之前居然放到判断里了,导致之前未更新的 text 节点在之后的更新里找不到 dom 68 | vNode.dom = prevVNode.dom 69 | if (vNode.textContent !== prevVNode.textContent) { 70 | const type = typeof vNode.textContent 71 | const textContent = 72 | type === 'number' || type === 'string' ? vNode.textContent.toString() : '' 73 | invariant(prevVNode.dom !== null, 'patchTextElement dom null') 74 | 75 | // 没有真正采用和 react 一样的实现,担心会引入各种 dom 为 null 的问题 76 | // 现在在 chrome 里面通过检查器至少看不到 "" 77 | if (textContent) { 78 | prevVNode.dom.textContent = textContent 79 | vNode.dom = prevVNode.dom 80 | } else { 81 | mount(vNode, prevVNode.dom.parentNode) 82 | unmount(prevVNode) 83 | } 84 | } 85 | } 86 | 87 | const patch = (vNode, prevVNode) => { 88 | if (vNode === prevVNode) { 89 | return 90 | } 91 | const { flag, type } = vNode 92 | 93 | if (prevVNode.flag !== flag || type !== prevVNode.type) { 94 | const parentDOM = prevVNode.dom.parentNode 95 | unmount(prevVNode) 96 | mount(vNode, parentDOM) 97 | return 98 | } 99 | 100 | switch (flag) { 101 | case FLAGS.CLASS: 102 | patchClassComponent(vNode, prevVNode) 103 | break 104 | case FLAGS.FUNC: 105 | patchFunctionComponent(vNode, prevVNode) 106 | break 107 | case FLAGS.ELEMENT: 108 | patchElement(vNode, prevVNode) 109 | case FLAGS.TEXT: 110 | patchTextElement(vNode, prevVNode) 111 | default: 112 | break 113 | } 114 | } 115 | 116 | export const patchChildren = (currentChildren, lastChildren, parentDOM) => { 117 | const lastChildInUse = [] 118 | let results = [] 119 | currentChildren.forEach((currentVNode, idx) => { 120 | const { key } = currentVNode 121 | if (lastChildren[idx] && key === lastChildren[idx].key) { 122 | patch(currentVNode, lastChildren[idx]) 123 | lastChildInUse.push(lastChildren[idx]) 124 | } else { 125 | const match = lastChildren.find(child => child.key === key) 126 | if (match) { 127 | lastChildInUse.push(match) 128 | patch(currentVNode, match) 129 | } else { 130 | mount(currentVNode, parentDOM) 131 | } 132 | } 133 | }) 134 | 135 | const lastChildNotInUse = lastChildren.filter( 136 | child => !lastChildInUse.includes(child), 137 | ) 138 | 139 | // reorder 140 | lastChildNotInUse.forEach(unmount) 141 | currentChildren.forEach((currentVNode, idx) => { 142 | const domIdx = getNodeIndex(currentVNode.dom) 143 | if (domIdx !== idx) { 144 | parentDOM.insertBefore(currentVNode.dom, parentDOM.childNodes[idx]) 145 | } 146 | }) 147 | 148 | return results 149 | } 150 | 151 | export default patch 152 | -------------------------------------------------------------------------------- /packages/my-react/src/dom/event.js: -------------------------------------------------------------------------------- 1 | export const addListeners = (dom, listeners = {}) => { 2 | _.forEach(listeners, ({ handler, useCapture }, key) => { 3 | dom.addEventListener(key, handler, useCapture) 4 | }) 5 | } 6 | 7 | export const removeListeners = (dom, listeners = {}) => { 8 | _.forEach(listeners, ({ handler, useCapture }, key) => { 9 | dom.removeEventListener(key, handler, useCapture) 10 | }) 11 | } 12 | 13 | export const REACT_EVENT_KEYS = [ 14 | // Clipboard Events 15 | 'onCopy', 16 | 'onCopyCapture', 17 | 'onCut', 18 | 'onCutCapture', 19 | 'onPaste', 20 | 'onPasteCapture', 21 | 22 | // Composition Events 23 | 'onCompositionEnd', 24 | 'onCompositionEndCapture', 25 | 'onCompositionStart', 26 | 'onCompositionStartCapture', 27 | 'onCompositionUpdate', 28 | 'onCompositionUpdateCapture', 29 | 30 | // Focus Events 31 | 'onFocus', 32 | 'onFocusCapture', 33 | 'onBlur', 34 | 'onBlurCapture', 35 | 36 | // Form Events 37 | 'onChange', 38 | 'onChangeCapture', 39 | 'onInput', 40 | 'onInputCapture', 41 | 'onReset', 42 | 'onResetCapture', 43 | 'onSubmit', 44 | 'onSubmitCapture', 45 | 'onInvalid', 46 | 'onInvalidCapture', 47 | 48 | // Image Events 49 | 'onLoad', 50 | 'onLoadCapture', 51 | 'onError', 52 | 'onErrorCapture', 53 | 54 | // Keyboard Events 55 | 'onKeyDown', 56 | 'onKeyDownCapture', 57 | 'onKeyPress', 58 | 'onKeyPressCapture', 59 | 'onKeyUp', 60 | 'onKeyUpCapture', 61 | 62 | // Media Events 63 | 'onAbort', 64 | 'onAbortCapture', 65 | 'onCanPlay', 66 | 'onCanPlayCapture', 67 | 'onCanPlayThrough', 68 | 'onCanPlayThroughCapture', 69 | 'onDurationChange', 70 | 'onDurationChangeCapture', 71 | 'onEmptied', 72 | 'onEmptiedCapture', 73 | 'onEncrypted', 74 | 'onEncryptedCapture', 75 | 'onEnded', 76 | 'onEndedCapture', 77 | 'onLoadedData', 78 | 'onLoadedDataCapture', 79 | 'onLoadedMetadata', 80 | 'onLoadedMetadataCapture', 81 | 'onLoadStart', 82 | 'onLoadStartCapture', 83 | 'onPause', 84 | 'onPauseCapture', 85 | 'onPlay', 86 | 'onPlayCapture', 87 | 'onPlaying', 88 | 'onPlayingCapture', 89 | 'onProgress', 90 | 'onProgressCapture', 91 | 'onRateChange', 92 | 'onRateChangeCapture', 93 | 'onSeeked', 94 | 'onSeekedCapture', 95 | 'onSeeking', 96 | 'onSeekingCapture', 97 | 'onStalled', 98 | 'onStalledCapture', 99 | 'onSuspend', 100 | 'onSuspendCapture', 101 | 'onTimeUpdate', 102 | 'onTimeUpdateCapture', 103 | 'onVolumeChange', 104 | 'onVolumeChangeCapture', 105 | 'onWaiting', 106 | 'onWaitingCapture', 107 | 108 | // MouseEvents 109 | 'onClick', 110 | 'onClickCapture', 111 | 'onContextMenu', 112 | 'onContextMenuCapture', 113 | 'onDoubleClick', 114 | 'onDoubleClickCapture', 115 | 'onDrag', 116 | 'onDragCapture', 117 | 'onDragEnd', 118 | 'onDragEndCapture', 119 | 'onDragEnter', 120 | 'onDragEnterCapture', 121 | 'onDragExit', 122 | 'onDragExitCapture', 123 | 'onDragLeave', 124 | 'onDragLeaveCapture', 125 | 'onDragOver', 126 | 'onDragOverCapture', 127 | 'onDragStart', 128 | 'onDragStartCapture', 129 | 'onDrop', 130 | 'onDropCapture', 131 | 'onMouseDown', 132 | 'onMouseDownCapture', 133 | 'onMouseEnter', 134 | 'onMouseLeave', 135 | 'onMouseMove', 136 | 'onMouseMoveCapture', 137 | 'onMouseOut', 138 | 'onMouseOutCapture', 139 | 'onMouseOver', 140 | 'onMouseOverCapture', 141 | 'onMouseUp', 142 | 'onMouseUpCapture', 143 | 144 | // Selection Events 145 | 'onSelect', 146 | 'onSelectCapture', 147 | 148 | // Touch Events 149 | 'onTouchCancel', 150 | 'onTouchCancelCapture', 151 | 'onTouchEnd', 152 | 'onTouchEndCapture', 153 | 'onTouchMove', 154 | 'onTouchMoveCapture', 155 | 'onTouchStart', 156 | 'onTouchStartCapture', 157 | 158 | // Pointer Events 159 | 'onPointerDown', 160 | 'onPointerDownCapture', 161 | 'onPointerMove', 162 | 'onPointerMoveCapture', 163 | 'onPointerUp', 164 | 'onPointerUpCapture', 165 | 'onPointerCancel', 166 | 'onPointerCancelCapture', 167 | 'onPointerEnter', 168 | 'onPointerEnterCapture', 169 | 'onPointerLeave', 170 | 'onPointerLeaveCapture', 171 | 'onPointerOver', 172 | 'onPointerOverCapture', 173 | 'onPointerOut', 174 | 'onPointerOutCapture', 175 | 'onGotPointerCapture', 176 | 'onGotPointerCaptureCapture', 177 | 'onLostPointerCapture', 178 | 'onLostPointerCaptureCapture', 179 | 180 | // UI Events 181 | 'onScroll', 182 | 'onScrollCapture', 183 | 184 | // Wheel Events 185 | 'onWheel', 186 | 'onWheelCapture', 187 | 188 | // Animation Events 189 | 'onAnimationStart', 190 | 'onAnimationStartCapture', 191 | 'onAnimationEnd', 192 | 'onAnimationEndCapture', 193 | 'onAnimationIteration', 194 | 'onAnimationIterationCapture', 195 | 196 | // Transition Events 197 | 'onTransitionEnd', 198 | 'onTransitionEndCapture', 199 | ] 200 | 201 | // react 并没有严格遵从 dom 的事件命名 202 | const reactEventMap = { 203 | doubleclick: 'dblclick', 204 | } 205 | 206 | export const REACT_EVENT_MAP = REACT_EVENT_KEYS.reduce((map, key) => { 207 | const useCapture = key.endsWith('Capture') 208 | const domEventKey = key 209 | .substring(2, !useCapture ? key.length : key.length - 7) 210 | .toLocaleLowerCase() 211 | return { 212 | ...map, 213 | [key]: { 214 | key: reactEventMap[domEventKey] || domEventKey, 215 | useCapture, 216 | }, 217 | } 218 | }, {}) 219 | -------------------------------------------------------------------------------- /packages/example-simple/src/allTests.js: -------------------------------------------------------------------------------- 1 | import renderPrimitiveTest from './tests/renderPrimitiveTest' 2 | import setStateTest from './tests/setStateTests/setStateTest' 3 | import childrenTest from './tests/childrenTest' 4 | import reconcileTest from './tests/reconcileTest' 5 | import reduxTest from './tests/reduxTest' 6 | import lifecycleTest from './tests/lifecycleTest' 7 | import keyedTest from './tests/diffTests/keyedTest' 8 | import manuallyKeyedTest from './tests/diffTests/manuallyKeyedTest' 9 | import reorderTextNodes from './tests/diffTests/reorderTextNodes' 10 | import unkeyedTest from './tests/diffTests/unkeyedTest' 11 | import reorderTextNodes2 from './tests/diffTests/reorderTextNodes2' 12 | import nestedKeyedTest from './tests/diffTests/nestedKeyedTest' 13 | import propTest from './tests/setStateTests/propTest' 14 | import setStateWithChildren from './tests/setStateTests/setStateWithChildren' 15 | import multipleSetStateCalls from './tests/setStateTests/multipleSetStateCalls' 16 | import callbackAndSetState from './tests/setStateTests/callbackAndSetState' 17 | import mutateState from './tests/setStateTests/mutateState' 18 | import reorderTest from './tests/diffTests/reorderTest' 19 | import updateAttrs from './tests/diffTests/updateAttrs' 20 | import unmountTest from './tests/unmountTest' 21 | import didUpdateTest from './tests/didUpdateTest' 22 | import inputTest from './tests/form/inputTest' 23 | import keyTest from './tests/keyTest' 24 | import refTest from './tests/refTests/refTest' 25 | import useStateTest from './tests/hooksTests/useStateTest' 26 | import useStateTest2 from './tests/hooksTests/useStateTest2' 27 | import useStateTest3 from './tests/hooksTests/useStateTest3' 28 | // import reactReduxTest from './tests/reactReduxTest' 29 | 30 | const testGroups = [ 31 | { 32 | desc: 'form tests', 33 | children: [ 34 | { 35 | test: inputTest, 36 | desc: 'input test', 37 | }, 38 | ], 39 | }, 40 | { 41 | desc: 'render tests', 42 | children: [ 43 | { 44 | test: renderPrimitiveTest, 45 | desc: 'render primitive', 46 | }, 47 | { 48 | test: childrenTest, 49 | desc: 'children test', 50 | }, 51 | { 52 | test: keyTest, 53 | desc: 'key test', 54 | }, 55 | ], 56 | }, 57 | { 58 | desc: 'hook tests', 59 | children: [ 60 | { 61 | test: useStateTest, 62 | desc: 'useState test', 63 | }, 64 | { 65 | test: useStateTest2, 66 | desc: 'useState test2', 67 | }, 68 | { 69 | test: useStateTest3, 70 | desc: 'useState test3', 71 | }, 72 | ], 73 | }, 74 | { 75 | desc: 'ref tests', 76 | children: [ 77 | { 78 | test: refTest, 79 | desc: 'ref test', 80 | }, 81 | ], 82 | }, 83 | { 84 | desc: 'setState tests', 85 | children: [ 86 | { 87 | test: setStateTest, 88 | desc: 'setStateTest basic', 89 | }, 90 | { 91 | test: propTest, 92 | desc: 'prop test', 93 | }, 94 | { 95 | test: setStateWithChildren, 96 | desc: 'setStateWithChildren', 97 | }, 98 | { 99 | test: multipleSetStateCalls, 100 | desc: 'multipleSetStateCalls', 101 | }, 102 | { 103 | test: callbackAndSetState, 104 | desc: 'callbackAndSetState', 105 | }, 106 | { 107 | test: mutateState, 108 | desc: 'mutateState', 109 | }, 110 | ], 111 | }, 112 | { 113 | desc: 'diff tests', 114 | children: [ 115 | { 116 | test: manuallyKeyedTest, 117 | desc: 'manuallyKeyedTest', 118 | }, 119 | { 120 | test: keyedTest, 121 | desc: 'keyedTest', 122 | }, 123 | { 124 | test: updateAttrs, 125 | desc: 'update attrs', 126 | }, 127 | { 128 | test: reorderTest, 129 | desc: 'reorder test', 130 | }, 131 | { 132 | test: nestedKeyedTest, 133 | desc: 'nestedKeyedTest', 134 | }, 135 | { 136 | test: unkeyedTest, 137 | desc: 'unkeyedTest', 138 | }, 139 | { 140 | test: reorderTextNodes, 141 | desc: 'reorderTextNodes', 142 | }, 143 | { 144 | test: reorderTextNodes2, 145 | desc: 'reorderTextNodes2', 146 | }, 147 | ], 148 | }, 149 | { 150 | desc: 'lifecycle tests', 151 | children: [ 152 | { 153 | test: lifecycleTest, 154 | desc: 'lifecycleTest', 155 | }, 156 | { 157 | test: unmountTest, 158 | desc: 'unmount test', 159 | }, 160 | { 161 | test: didUpdateTest, 162 | desc: 'didUpdateTest', 163 | }, 164 | ], 165 | }, 166 | { 167 | desc: 'misc', 168 | children: [ 169 | { 170 | test: reconcileTest, 171 | desc: 'reconcileTest', 172 | }, 173 | ], 174 | }, 175 | { 176 | desc: '3rd party integration tests', 177 | children: [ 178 | { 179 | test: reduxTest, 180 | desc: 'reduxTest', 181 | }, 182 | // Provider 需要 node_modules 里面的 react 依赖,这里采用的 183 | // 依赖注入的方式变得不可行 184 | // { 185 | // test: reactReduxTest, 186 | // desc: 'reactReduxTest', 187 | // }, 188 | ], 189 | }, 190 | ] 191 | 192 | export default testGroups 193 | -------------------------------------------------------------------------------- /packages/my-react/src/vnode.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import _ from 'lodash' 3 | import { getAttrs } from './dom/utils' 4 | import { REACT_EVENT_MAP, REACT_EVENT_KEYS } from './dom/event' 5 | 6 | export const FLAGS = { 7 | TEXT: 'text', 8 | CLASS: 'class', 9 | FUNC: 'func', 10 | ELEMENT: 'element', 11 | } 12 | 13 | const TEXT_VNODE_TYPE = Symbol('text_vnode_type') 14 | 15 | const isTextElement = element => { 16 | return !(element instanceof VNode) 17 | } 18 | 19 | let debugId = 0 20 | 21 | class VNode { 22 | /** 23 | * @param {{ type?, props?, textContent?, key?, flag }} config 24 | */ 25 | constructor({ type, props, key, textContent, flag }) { 26 | this._debugId = debugId 27 | this.type = type 28 | this.flag = flag 29 | this.props = props 30 | this.textContent = textContent 31 | this.key = key 32 | this.children = [] 33 | this.ref = null 34 | this.validated = false 35 | this.parent = null 36 | this.dom = null 37 | this.listeners = null 38 | this.attributes = null 39 | this.rendered = null 40 | this.instance = null 41 | this.state = null 42 | } 43 | } 44 | 45 | // generate unique keys & flatten children 46 | const processChildren = (children, keyPrefix = '__root_', parent) => { 47 | return (Array.isArray(children) ? children : [children]).reduce( 48 | (output, childElement, idx) => { 49 | if (Array.isArray(childElement)) { 50 | return output.concat( 51 | processChildren(childElement, `${keyPrefix}${idx}_>`, parent), 52 | ) 53 | } 54 | 55 | const generatedKey = '__gen_' + keyPrefix + idx 56 | 57 | if (isTextElement(childElement)) { 58 | const textVNode = createVNode(TEXT_VNODE_TYPE, { 59 | textContent: childElement, 60 | key: generatedKey, 61 | }) 62 | textVNode.parent = parent 63 | return [...output, textVNode] 64 | } 65 | 66 | childElement.parent = parent 67 | if (childElement.key === null) { 68 | childElement.key = generatedKey 69 | } else { 70 | childElement.key = keyPrefix + childElement.key 71 | } 72 | return [...output, childElement] 73 | }, 74 | [], 75 | ) 76 | } 77 | 78 | export const createVNode = (type, allProps = {}) => { 79 | let vNode 80 | const props = _.omit(allProps, ['key', 'ref', '__source', '__self']) 81 | if (typeof type === 'string') { 82 | vNode = createElementVNode(type, props) 83 | } else if (type.$IS_CLASS) { 84 | vNode = createClassVNode(type, props) 85 | } else if (typeof type === 'function') { 86 | vNode = createFunctionVNode(type, props) 87 | if (allProps.ref) { 88 | console.error( 89 | 'Function components cannot be given refs. Attempts to access this ref will fail.', 90 | ) 91 | } 92 | } else if (type === TEXT_VNODE_TYPE) { 93 | vNode = createTextVNode(props.textContent) 94 | } else { 95 | throw new Error(`Unknown type: ${type}`) 96 | } 97 | vNode.key = allProps.key === undefined ? null : allProps.key 98 | vNode.ref = allProps.ref || null 99 | 100 | if (vNode.key !== null) { 101 | Object.defineProperty(props, 'key', { 102 | configurable: false, 103 | enumerable: false, 104 | get() { 105 | console.error( 106 | '`key` is not a prop. Trying to access it will result in `undefined` being returned', 107 | ) 108 | }, 109 | }) 110 | } 111 | 112 | if (vNode.ref !== null) { 113 | Object.defineProperty(props, 'ref', { 114 | configurable: false, 115 | enumerable: false, 116 | get() { 117 | console.error( 118 | '`ref` is not a prop. Trying to access it will result in `undefined` being returned', 119 | ) 120 | }, 121 | }) 122 | } 123 | 124 | vNode.children = 125 | props.children === undefined 126 | ? [] 127 | : processChildren(props.children, undefined, vNode) 128 | 129 | // validate keys 130 | const keys = vNode.children.map(child => child.key) 131 | const uniqueKeys = _.union(keys) 132 | const print = arr => console.error(arr.sort().join(', ')) 133 | if (keys.length !== uniqueKeys.length) { 134 | console.error('key should be unique!') 135 | print(uniqueKeys) 136 | print(keys) 137 | } 138 | 139 | debugId++ 140 | return vNode 141 | } 142 | 143 | const createTextVNode = text => { 144 | const vNode = new VNode({ 145 | textContent: text, 146 | flag: FLAGS.TEXT, 147 | }) 148 | return vNode 149 | } 150 | 151 | const createFunctionVNode = (type, props) => { 152 | const vNode = new VNode({ 153 | type, 154 | props, 155 | flag: FLAGS.FUNC, 156 | }) 157 | return vNode 158 | } 159 | 160 | const createClassVNode = (type, props) => { 161 | const vNode = new VNode({ 162 | type, 163 | props, 164 | flag: FLAGS.CLASS, 165 | }) 166 | return vNode 167 | } 168 | 169 | const createElementVNode = (type, props) => { 170 | let finalProps = props 171 | let listeners = null 172 | let attributes = null 173 | const eventProps = _.pick(finalProps, REACT_EVENT_KEYS) 174 | if (!_.isEmpty(eventProps)) { 175 | listeners = _.reduce( 176 | eventProps, 177 | (listeners, handler, key) => { 178 | const match = REACT_EVENT_MAP[key] 179 | return { 180 | ...listeners, 181 | [match.key]: { 182 | handler, 183 | useCapture: match.useCapture, 184 | }, 185 | } 186 | }, 187 | {}, 188 | ) 189 | finalProps = _.omit(finalProps, _.keys(eventProps)) 190 | } 191 | attributes = getAttrs(finalProps) 192 | const vNode = new VNode({ 193 | type, 194 | props: finalProps, 195 | flag: FLAGS.ELEMENT, 196 | }) 197 | vNode.listeners = listeners 198 | vNode.attributes = attributes 199 | return vNode 200 | } 201 | --------------------------------------------------------------------------------