实现虚拟Dom

    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. let game = <Game />
    117. console.log(game);
    118. // render(game, document.getElementById("root"));
    119. function calculateWinner(squares) {
    120. const lines = [
    121. [0, 1, 2],
    122. [3, 4, 5],
    123. [6, 7, 8],
    124. [0, 3, 6],
    125. [1, 4, 7],
    126. [2, 5, 8],
    127. [0, 4, 8],
    128. [2, 4, 6]
    129. ];
    130. for (let i = 0; i < lines.length; i++) {
    131. const [a, b, c] = lines[i];
    132. if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
    133. return squares[a];
    134. }
    135. }
    136. return null;
    137. }

    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. this.props[name] = value
    11. }
    12. appendChild(component) {
    13. this.children.push(component)
    14. }
    15. get vdom() {
    16. return this.render().vdom;
    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 extends Component {
    63. constructor(type) {
    64. super(type)
    65. this.type = type
    66. this.root = document.createElement(type)
    67. }
    68. // setAttribute(name, value) {
    69. // // 过滤事件, 如onClick
    70. // if (name.match(/^on([\s\S]+)$/)) {
    71. // // console.log(RegExp.$1)
    72. // // 绑定事件, Click转click
    73. // this.root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value)
    74. // } else if (name == 'className'){
    75. // this.setAttribute('class', value)
    76. // } else {
    77. // this.root.setAttribute(name, value)
    78. // }
    79. // this.root.setAttribute(name, value)
    80. // }
    81. // appendChild(component) {
    82. // // this.root.appendChild(component.root)
    83. // let range = document.createRange()
    84. // range.setStart(this.root, this.root.childNodes.length)
    85. // range.setEnd(this.root, this.root.childNodes.length)
    86. // component[RENDER_TO_DOM](range)
    87. // }
    88. get vdom() {
    89. this.children = this.children.map(child => child.vdom);
    90. return this
    91. }
    92. [RENDER_TO_DOM](range){
    93. range.deleteContents()
    94. range.insertNode(this.root)
    95. }
    96. }
    97. class TextWrapper extends Component{
    98. constructor(content) {
    99. super(content)
    100. this.content = content
    101. this.root = document.createTextNode(content)
    102. }
    103. get vdom() {
    104. return this;
    105. }
    106. [RENDER_TO_DOM](range){
    107. range.deleteContents()
    108. range.insertNode(this.root)
    109. }
    110. }
    111. export function createElement(tagName, attributes, ...rest) {
    112. let element
    113. if (typeof tagName == 'string') {
    114. element = new ElementWrapper(tagName)
    115. } else {
    116. element = new tagName
    117. }
    118. if (typeof attributes === 'object' && attributes instanceof Object) {
    119. for (const key in attributes) {
    120. element.setAttribute(key, attributes[key])
    121. }
    122. }
    123. let insertChildren = (children) => {
    124. // console.log(children)
    125. for(const child of children) {
    126. if (typeof child == 'string') {
    127. child = new TextWrapper(child)
    128. }
    129. if (child === null) {
    130. continue;
    131. }
    132. if ((typeof child == 'object') && (child instanceof Array)) {
    133. insertChildren(child)
    134. } else {
    135. element.appendChild(child)
    136. }
    137. }
    138. }
    139. insertChildren(rest)
    140. return element
    141. }
    142. export function render(component, parentElement) {
    143. // parentElement.appendChild(component.root)
    144. let range = document.createRange()
    145. range.setStart(parentElement, 0)
    146. range.setEnd(parentElement, parentElement.childNodes.length)
    147. component[RENDER_TO_DOM](range)
    148. }