实现react教程

    仅实现下棋交互功能 https://codepen.io/gaearon/pen/VbbVLg?editors=0010

    main.html

    1. <!DOCTYPE html>
    2. <html>
    3. <head>
    4. <meta charset="utf-8">
    5. <meta name="viewport" content="width=device-width">
    6. <title>JS Bin</title>
    7. <style>
    8. body {
    9. font: 14px "Century Gothic", Futura, sans-serif;
    10. margin: 20px;
    11. }
    12. ol, ul {
    13. padding-left: 30px;
    14. }
    15. .board-row:after {
    16. clear: both;
    17. content: "";
    18. display: table;
    19. }
    20. .status {
    21. margin-bottom: 10px;
    22. }
    23. .square {
    24. background: #fff;
    25. border: 1px solid #999;
    26. float: left;
    27. font-size: 24px;
    28. font-weight: bold;
    29. line-height: 34px;
    30. height: 34px;
    31. margin-right: -1px;
    32. margin-top: -1px;
    33. padding: 0;
    34. text-align: center;
    35. width: 34px;
    36. }
    37. .square:focus {
    38. outline: none;
    39. }
    40. .kbd-navigation .square:focus {
    41. background: #ddd;
    42. }
    43. .game {
    44. display: flex;
    45. flex-direction: row;
    46. }
    47. .game-info {
    48. margin-left: 20px;
    49. }
    50. </style>
    51. </head>
    52. <body>
    53. <div id="root"></div>
    54. <script src="./dist/main.js">
    55. </script>
    56. </body>
    57. </html>

    tic.js

    1. import {render, Component, createElement} from './toy-react.js'
    2. // window.a = <div id="app" class="head"></div>
    3. class Square extends Component {
    4. constructor(props) {
    5. super(props);
    6. this.state = {
    7. value: null,
    8. };
    9. }
    10. render() {
    11. return (
    12. <button
    13. className="square"
    14. onClick={() => this.setState({value: 'X'})}
    15. >
    16. {this.state.value}
    17. </button>
    18. );
    19. }
    20. }
    21. class Board extends Component {
    22. renderSquare(i) {
    23. return <Square />;
    24. }
    25. render() {
    26. const status = 'Next player: X';
    27. return (
    28. <div>
    29. <div className="status">{status}</div>
    30. <div className="board-row">
    31. {this.renderSquare(0)}
    32. {this.renderSquare(1)}
    33. {this.renderSquare(2)}
    34. </div>
    35. <div className="board-row">
    36. {this.renderSquare(3)}
    37. {this.renderSquare(4)}
    38. {this.renderSquare(5)}
    39. </div>
    40. <div className="board-row">
    41. {this.renderSquare(6)}
    42. {this.renderSquare(7)}
    43. {this.renderSquare(8)}
    44. </div>
    45. </div>
    46. );
    47. }
    48. }
    49. class Game extends Component {
    50. render() {
    51. return (
    52. <div className="game">
    53. <div className="game-board">
    54. <Board />
    55. </div>
    56. <div className="game-info">
    57. <div>{/* status */}</div>
    58. <ol>{/* TODO */}</ol>
    59. </div>
    60. </div>
    61. );
    62. }
    63. }
    64. // ========================================
    65. render(
    66. <Game />,
    67. document.getElementById('root')
    68. );

    toy-react.js

    1. const RENDER_TO_DOM = Symbol('render to dom')
    2. export class Component {
    3. constructor() {
    4. this.props = Object.create(null);
    5. this.children = [];
    6. this._root = null;
    7. this._range = null;
    8. }
    9. setAttribute(name, value) {
    10. if (name == 'onClick') {
    11. console.log(name)
    12. }
    13. this.props[name] = value
    14. }
    15. appendChild(component) {
    16. this.children.push(component)
    17. }
    18. [RENDER_TO_DOM](range) {
    19. this._range = range
    20. this.render()[RENDER_TO_DOM](range)
    21. }
    22. rerender() {
    23. // this._range.deleteContents()
    24. // this[RENDER_TO_DOM](this._range)
    25. // 先插入再删除
    26. let oldRange = this._range;
    27. let range = document.createRange();
    28. range.setStart(oldRange.startContainer, oldRange.startOffset)
    29. range.setEnd(oldRange.startContainer, oldRange.startOffset)
    30. this[RENDER_TO_DOM](range)
    31. oldRange.setStart(range.endContainer, range.endOffset)
    32. oldRange.deleteContents()
    33. }
    34. setState (newState) {
    35. // console.log(newState)
    36. // 没有state 或者 不是对象, 短路
    37. if (this.state == null || typeof this.state != "object") {
    38. this.setState = newState
    39. this.rerender()
    40. return;
    41. }
    42. // 深拷贝
    43. let merge = (oldState, newState) => {
    44. for (let p in newState) {
    45. if (oldState[p] == null || typeof oldState[p] != "object") {
    46. oldState[p] = newState[p]
    47. } else {
    48. merge(oldState[p], newState[p])
    49. }
    50. }
    51. }
    52. merge(this.state, newState)
    53. this.rerender()
    54. }
    55. // get root() {
    56. // if (!this._root) {
    57. // this._root = this.render().root
    58. // }
    59. // return this._root
    60. // }
    61. }
    62. class ElementWrapper {
    63. constructor(type) {
    64. this.root = document.createElement(type)
    65. }
    66. setAttribute(name, value) {
    67. // 过滤事件, 如onClick
    68. if (name.match(/^on([\s\S]+)$/)) {
    69. // console.log(RegExp.$1)
    70. // 绑定事件, Click转click
    71. this.root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value)
    72. } else if (name == 'className'){
    73. this.setAttribute('class', value)
    74. } else {
    75. this.root.setAttribute(name, value)
    76. }
    77. this.root.setAttribute(name, value)
    78. }
    79. appendChild(component) {
    80. // this.root.appendChild(component.root)
    81. let range = document.createRange()
    82. range.setStart(this.root, this.root.childNodes.length)
    83. range.setEnd(this.root, this.root.childNodes.length)
    84. component[RENDER_TO_DOM](range)
    85. }
    86. [RENDER_TO_DOM](range){
    87. range.deleteContents()
    88. range.insertNode(this.root)
    89. }
    90. }
    91. class TextWrapper {
    92. constructor(content) {
    93. this.root = document.createTextNode(content)
    94. }
    95. [RENDER_TO_DOM](range){
    96. range.deleteContents()
    97. range.insertNode(this.root)
    98. }
    99. }
    100. export function createElement(tagName, attributes, ...rest) {
    101. let element
    102. if (typeof tagName == 'string') {
    103. element = new ElementWrapper(tagName)
    104. } else {
    105. element = new tagName
    106. }
    107. if (typeof attributes === 'object' && attributes instanceof Object) {
    108. for (const key in attributes) {
    109. element.setAttribute(key, attributes[key])
    110. }
    111. }
    112. let insertChildren = (children) => {
    113. // console.log(children)
    114. for(const child of children) {
    115. if (typeof child == 'string') {
    116. child = new TextWrapper(child)
    117. }
    118. if (child === null) {
    119. continue;
    120. }
    121. if ((typeof child == 'object') && (child instanceof Array)) {
    122. insertChildren(child)
    123. } else {
    124. element.appendChild(child)
    125. }
    126. }
    127. }
    128. insertChildren(rest)
    129. return element
    130. }
    131. export function render(component, parentElement) {
    132. // parentElement.appendChild(component.root)
    133. let range = document.createRange()
    134. range.setStart(parentElement, 0)
    135. range.setEnd(parentElement, parentElement.childNodes.length)
    136. component[RENDER_TO_DOM](range)
    137. }

    终极交互 https://codepen.io/gaearon/pen/gWWZgR?editors=0010

    不支持函数组件, 需要修改为类组件

    tic.final.js

    1. import {render, Component, createElement} from './toy-react.js'
    2. class Square extends Component {
    3. render() {
    4. return (
    5. <button className="square" onClick={this.props.onClick}>
    6. {this.props.value}
    7. </button>
    8. );
    9. }
    10. }
    11. class Board extends Component {
    12. renderSquare(i) {
    13. return (
    14. <Square
    15. value={this.props.squares[i]}
    16. onClick={() => this.props.onClick(i)}
    17. />
    18. );
    19. }
    20. render() {
    21. return (
    22. <div>
    23. <div className="board-row">
    24. {this.renderSquare(0)}
    25. {this.renderSquare(1)}
    26. {this.renderSquare(2)}
    27. </div>
    28. <div className="board-row">
    29. {this.renderSquare(3)}
    30. {this.renderSquare(4)}
    31. {this.renderSquare(5)}
    32. </div>
    33. <div className="board-row">
    34. {this.renderSquare(6)}
    35. {this.renderSquare(7)}
    36. {this.renderSquare(8)}
    37. </div>
    38. </div>
    39. );
    40. }
    41. }
    42. class Game extends Component {
    43. constructor(props) {
    44. super(props);
    45. this.state = {
    46. history: [
    47. {
    48. squares: Array(9).fill(null)
    49. }
    50. ],
    51. stepNumber: 0,
    52. xIsNext: true
    53. };
    54. }
    55. handleClick(i) {
    56. const history = this.state.history.slice(0, this.state.stepNumber + 1);
    57. const current = history[history.length - 1];
    58. const squares = current.squares.slice();
    59. if (calculateWinner(squares) || squares[i]) {
    60. return;
    61. }
    62. squares[i] = this.state.xIsNext ? "X" : "O";
    63. this.setState({
    64. history: history.concat([
    65. {
    66. squares: squares
    67. }
    68. ]),
    69. stepNumber: history.length,
    70. xIsNext: !this.state.xIsNext
    71. });
    72. }
    73. jumpTo(step) {
    74. this.setState({
    75. stepNumber: step,
    76. xIsNext: (step % 2) === 0
    77. });
    78. }
    79. render() {
    80. const history = this.state.history;
    81. const current = history[this.state.stepNumber];
    82. const winner = calculateWinner(current.squares);
    83. const moves = history.map((step, move) => {
    84. const desc = move ?
    85. 'Go to move #' + move :
    86. 'Go to game start';
    87. return (
    88. <li key={move}>
    89. <button onClick={() => this.jumpTo(move)}>{desc}</button>
    90. </li>
    91. );
    92. });
    93. let status;
    94. if (winner) {
    95. status = "Winner: " + winner;
    96. } else {
    97. status = "Next player: " + (this.state.xIsNext ? "X" : "O");
    98. }
    99. return (
    100. <div className="game">
    101. <div className="game-board">
    102. <Board
    103. squares={current.squares}
    104. onClick={i => this.handleClick(i)}
    105. />
    106. </div>
    107. <div className="game-info">
    108. <div>{status}</div>
    109. <ol>{moves}</ol>
    110. </div>
    111. </div>
    112. );
    113. }
    114. }
    115. // ========================================
    116. render(<Game />, document.getElementById("root"));
    117. function calculateWinner(squares) {
    118. const lines = [
    119. [0, 1, 2],
    120. [3, 4, 5],
    121. [6, 7, 8],
    122. [0, 3, 6],
    123. [1, 4, 7],
    124. [2, 5, 8],
    125. [0, 4, 8],
    126. [2, 4, 6]
    127. ];
    128. for (let i = 0; i < lines.length; i++) {
    129. const [a, b, c] = lines[i];
    130. if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
    131. return squares[a];
    132. }
    133. }
    134. return null;
    135. }