乐趣区

关于react.js:ToyReact-项目总结

webpack 配置

  • optimization: {minimize: false}:打包后的会把每一个文件放在 eval 中执行,通过 sourceURL 的形式在浏览器中关上它的时候变成一个独自的文件

    eval('console.log("1");\n\n//# sourceURL=webpack:///./main.js?');
  • @babel/preset-env 把高版本的 es 语法翻译成低版本的 es 语法
  • @babel/plugin-transform-react-jsxjs 中能够应用 jsx 语法

    let a = <MyComponent name="a" />;
    // 被翻译成
    var a = createElement(MyComponent, {name: "a",});
  • pragma:文本替换,如果不加的话默认是 React.crateElement,这里要本人实现一个 React 所以这里要替换成 ToyReact.createElement
module.exports = {
  entry: {main: "./main.js",},
  mode: "development",
  optimization: {minimize: false,},
  module: {
    rules: [
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader",
          options: {presets: ["@babel/preset-env"],
            plugins: [
              [
                "@babel/plugin-transform-react-jsx",
                {pragma: "ToyReact.createElement"},
              ],
            ],
          },
        },
      },
    ],
  },
};

ToyReact 根本应用办法

源码

main.js 是我的项目的入口文件,引入 ToyReact,申明一个组件 MyComponent

// main.js
import {ToyReact} from "ToyReact";

class MyComponent {}

let a = <MyComponent name="a" />;

ToyReact.js 是外围代码,这样就造成了一个最简略的 react

// ToyReact.js
export let ToyReact = {createElement() {console.log(arguments);
  },
};

createElement 有两个参数:

  • type:组件名 MyComponent(上图)

    • 如果 jsx 标签是 小写 的,那么这里的第一个参数是个 字符串 不是变量(下图图)
  • attribute:组件上的属性

用 ToyReact 实现 dom

创立 dom 节点

// main.js
import {ToyReact} from "ToyReact";

let a = (
  <div name="a" id="ids">
    <span></span>
    <span></span>
    <span></span>
  </div>
);

createElement 须要返回一个 dom 元素,否则最初的 div 接管到的 children 都是 null

// ToyReact.js
export let ToyReact = {createElement(type, attributes, ...children) {console.log(arguments);
    return document.createElement(type);
  },
};

前三次创立了三个 span,第四次就把这三个 span 作为 children 传递进来。

所以这时候

  • type 就是 div
  • attribute 就是 nameid
  • children 就是三个 span

挂载 dom 节点

// main.js
import {ToyReact} from "ToyReact";

let a = (
  <div name="a" id="ids">
    <span>Hello </span>
    <span>ToyReact </span>
    <span>!!!</span>
  </div>
);

document.body.appendChild(a);

span 外面的文本节点,传递进来后变成了一个字符串,所以在 appendChild 时须要创立一个文本节点。(下图)

// ToyReact.js
export let ToyReact = {createElement(type, attributes, ...children) {let element = document.createElement(type);

    for (let name in attributes) {element.setAttribute(name, attributes[name]);
    }

    for (let child of children) {if (typeof child === "string") child = document.createTextNode(child);
      element.appendChild(child);
    }
    return element;
  },
};

实现 ToyReact.render

react 中,挂载节点不是间接应用 document.body.appendChild 而是应用了 render 的办法,它为什么要这样做呢?次要起因是 jsx 是一个混合内容,组件和 HTML 是并存的,如果不是 HTML 的元素的话,无奈应用 document.body.appendChild

// main.js
import {ToyReact, Component} from "ToyReact";

class MyComponent extends Component {render() {return <div>cool</div>;}
}

let a = <MyComponent name="a" id="ids"></MyComponent>;

ToyReact.render(a, document.body);

这外面给 document.createElement 包裹一层 wrapper,把创立 dom 的过程对立操作。这里的 Wrapper 分为 ElementWrapperTextWrapperElementWrapper 用于创立元素节点,TextWrapper 用于创立文本节点。

ElementWrapperappendChild 接管的参数是虚构 dom

同时形象出 Component,实现 mountTosetAttribute 办法,让 MyComponent 继承。

class ElementWrapper {constructor(type) {this.root = document.createElement(type);
  }

  setAttribute(name, value) {this.root.setAttribute(name, value);
  }

  appendChild(vchild) {vchild.mountTo(this.root);
  }

  mountTo(parent) {parent.appendChild(this.root);
  }
}

class TextWrapper {constructor(content) {this.root = document.createTextNode(content);
  }

  mountTo(parent) {parent.appendChild(this.root);
  }
}

export class Component {constructor() {this.children = [];
  }

  setAttribute(name, value) {this[name] = value;
  }

  mountTo(parent) {let vdom = this.render();
    vdom.mountTo(parent);
  }

  appendChild(vchild) {this.children.push(vchild);
  }
}

export let ToyReact = {createElement(type, attributes, ...children) {
    let element;
    if (typeof type === "string") element = new ElementWrapper(type);
    else element = new type();

    for (let name in attributes) {element.setAttribute(name, attributes[name]);
    }

    if (typeof child === "string") child = new TextWrapper(child);
    element.appendChild(child);

    return element;
  },
  render(vdom, element) {vdom.mountTo(element);
  },
};

主动开展 children

写到这里的时候,会有一个问题,这外面的 this.children 不会主动开展,而不是把这货色当成一个 child 来看

class MyComponent extends Component {render() {
    return (
      <div>
        <span>Hello</span>
        <span>Hello</span>
        <div>{this.children}</div>
      </div>
    );
  }
}

mountTo 时接管到的是一个数组,咱们是心愿把数组开展,实现一个 inseartChildren

createElement(type, attributes, ...children) {
  let element
  if (typeof type === 'string')
    element = new ElementWrapper(type)
  else
    element = new type

  for (let name in attributes) {element.setAttribute(name, attributes[name])
  }

  let insertChildren = (children) => {for (let child of children) {if (typeof child === 'object' && child instanceof Array) {insertChildren(child)
      } else {if (!(child instanceof Component) &&
          !(child instanceof ElementWrapper) &&
          !(child instanceof TextWrapper))
          child = String(child)

        if (typeof child === 'string')
          child = new TextWrapper(child)
        element.appendChild(child)
      }
    }
  }

  insertChildren(children)

  return element
}

实现 setState,props,事件

源码

单次渲染,曾经实现了,但必定不能一次渲染,前面再也不去动它了,可能须要通过某种形式去从新渲染。

这里的 main.js 就绝对简单了,原本这个例子是教咱们如何把 react 跑起来,当初咱们反过来,用这个例子去验证框架支不反对

// mian.js
import {ToyReact, Component} from "./ToyReact";

class Square extends Component {constructor(props) {super(props);
    this.state = {value: null,};
  }

  render() {
    return (<button className="square" onClick={() => this.setState({value: "X"})}>
        {this.state.value ? this.state.value : ""}
      </button>
    );
  }
}

class Board extends Component {renderSquare(i) {return <Square value={i}></Square>;
  }

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

let a = <Board />;
ToyReact.render(a, document.body);

用下面写好的框架跑这个程序的话,会发现报了一个谬误,props 不存在

实现 props

class Component {constructor() {this.props = Object.create(null);
  }
  setAttribute(name, value) {this.props[name] = value;
  }
}

实现 onClick

用正则去匹配,绑定到元素上。

react 上,根本没有 remove 的状况,因为 remove 的话,reactrender 的合法性就会被毁坏掉。react 只能把整个 dom 树销毁从新绑定一个事件。

class ElementWrapper {setAttribute(name, value) {if (name.match(/^on([\s\S]+)$/)) {const eventName = RegExp.$1.replace(/^[\s\S]/, (s) => s.toLowerCase());
      this.root.addEventListener(eventName, value);
    }
    if (name === "className") name = "class";
    this.root.setAttribute(name, value);
  }
}

实现 setState

stateprops 没啥区别就是一个一般变量,但 setState 是由 component 提供的一个神奇的函数。

setState 实现的大略是一个 merge 操作,setState 会替换掉 state 中的内容,替换完之后 react 会做一个从新的 render 的操作。

this.state = {value: null};

this.setState({value: "x"});

setState 大略流程是先 mergeupdateDom

merge 实现:对传进来的 state 遍历,如果是一个对象就递归调用,如果是根本类型就间接更新 oldState 对应的值。

class Component {setState(state) {let merge = (oldState, newState) => {for (let p in newState) {if (typeof newState[p] === "object") {if (typeof oldState[p] !== "object") {oldState[p] = {};}
          merge(oldState[p], newState[p]);
        } else {oldState[p] = newState[p];
        }
      }
    };
    if (!this.state && state) this.state = {};
    merge(this.state, state);
    this.update();}
}

更新 dom 这里应用 range 来操作。

这里应用 rangeapi 有:

  • setStartsetEnd 接管两个参数:nodeoffset

    • node 如果是文本节点,offset 就是外面的文字;node 如果是一个一般的 elementoffset 就是它外部子节点的个数
  • setStartBeforesetStartAfter 接管一个节点
  • deleteContents 清空 range 内容
  • insertNoderange 的其实地位插入节点

运行流程:dom 节点不间接挂载,而是用 range 去挂载

  1. 元素节点创立 range 而后调用 mountTo 去挂载,文本节点间接调用 mountTo 去挂载
  2. ToyReact.render 将被调用,创立 range,调用 ComponentmountTo 去挂载
  3. ComponentmountTo 调用会将会调用本身的 update 更新 dom
class ElementWrapper {appendChild(vchild) {let range = document.createRange();
    if (this.root.children.length) {range.setStartAfter(this.root.lastChild);
      range.setEndAfter(this.root.lastChild);
    } else {range.setStart(this.root, 0);
      range.setEnd(this.root, 0);
    }
    vchild.mountTo(range);
  }

  mountTo(range) {range.deleteContents();
    range.insertNode(this.root);
  }
}

class TextWrapper {mountTo(range) {range.deleteContents();
    range.insertNode(this.root);
  }
}

export class Component {mountTo(range) {
    this.range = range;
    this.update();}

  update() {let placeholder = document.createComment("placeholder");
    let range = document.createRange();
    range.setStart(this.range.endContainer, this.range.endOffset);
    range.setEnd(this.range.endContainer, this.range.endOffset);
    range.insertNode(placeholder);
    this.range.deleteContents();
    let vdom = this.render();
    vdom.mountTo(this.range);
  }

  appendChild(vchild) {this.children.push(vchild);
  }
}

export let ToyReact = {render(vdom, element) {let range = document.createRange();
    if (element.children.length) {range.setStartAfter(element.lastChild);
      range.setEndAfter(element.lastChild);
    } else {range.setStart(element, 0);
      range.setEnd(element, 0);
    }
    vdom.mountTo(range);
  },
};

update 中的外围是把 range 中的外围全副删了,获取新的 vdom,而后在调用 mountTo

update 中创立了一个 placeholder 作用是用来占位。因为在应用 deleteContents 后,offset 会发生变化,导致出错。

mountTo 就能够加 willMountdidMount

update 就能够加 willMountdidMount

写到这外面,实 dom 的工作曾经根本实现了,上面就是把实 dom 变成虚构 dom

实现 ToyReact 虚构 Dom

源码

用 ToyReact 实现这个例子

// main.js
import {ToyReact, Component} from "./ToyReact";

class Square extends Component {render() {
    return (<button className="square" onClick={this.props.onClick}>
        {this.props.value}
      </button>
    );
  }
}

class Board extends Component {renderSquare(i) {
    return (
      <Square
        value={this.props.squares[i]}
        onClick={() => this.props.onClick(i)}
      />
    );
  }

  render() {
    return (
      <div>
        <div className="board-row">
          {this.renderSquare(0)}
          {this.renderSquare(1)}
          {this.renderSquare(2)}
        </div>
        <div className="board-row">
          {this.renderSquare(3)}
          {this.renderSquare(4)}
          {this.renderSquare(5)}
        </div>
        <div className="board-row">
          {this.renderSquare(6)}
          {this.renderSquare(7)}
          {this.renderSquare(8)}
        </div>
      </div>
    );
  }
}

class Game extends Component {constructor(props) {super(props);
    this.state = {
      history: [
        {squares: Array(9).fill(null),
        },
      ],
      stepNumber: 0,
      xIsNext: true,
    };
  }

  handleClick(i) {const history = this.state.history.slice(0, this.state.stepNumber + 1);
    const current = history[history.length - 1];
    const squares = current.squares.slice();
    if (calculateWinner(squares) || squares[i]) {return;}
    squares[i] = this.state.xIsNext ? "X" : "O";
    this.setState({
      history: history.concat([
        {squares: squares,},
      ]),
      stepNumber: history.length,
      xIsNext: !this.state.xIsNext,
    });
  }

  jumpTo(step) {
    this.setState({
      stepNumber: step,
      xIsNext: step % 2 === 0,
    });
  }

  render() {
    const history = this.state.history;
    const current = history[this.state.stepNumber];
    const winner = calculateWinner(current.squares);

    const moves = history.map((step, move) => {
      const desc = move ? "Go to move #" + move : "Go to game start";
      return (<li key={move}>
          <button onClick={() => this.jumpTo(move)}>{desc}</button>
        </li>
      );
    });

    let status;
    if (winner) {status = "Winner:" + winner;} else {status = "Next player:" + (this.state.xIsNext ? "X" : "O");
    }

    return (
      <div className="game">
        <div className="game-board">
          <Board
            squares={current.squares}
            onClick={(i) => this.handleClick(i)}
          />
        </div>
        {/*<div className="game-info">*/}
        {/*  <div>{status}</div>*/}
        {/*  <ol>{moves}</ol>*/}
        {/*</div>*/}
      </div>
    );
  }
}

// ========================================

ToyReact.render(<Game />, document.body);

function calculateWinner(squares) {
  const lines = [[0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares) {return squares[a];
    }
  }
  return null;
}

将 dom 操作放到 mountTo 中去实现

ElementWrapperappendChildsetAttribute 的逻辑都放在 mountTo 中去做,update 时就只操作虚构 dom

class ElementWrapper {constructor(type) {
    this.type = type;
    this.props = Object.create(null);
    this.children = [];}

  setAttribute(name, value) {this.props[name] = value;
  }

  appendChild(vchild) {this[childrenSymbol].push(vchild);
    this.children.push(vchild.vdom);
  }

  get vdom() {return this;}

  mountTo(range) {
    this.range = range;
    let placeholder = document.createComment("placeholder");
    let endRange = document.createRange();
    endRange.setStart(range.endContainer, range.endOffset);
    endRange.setEnd(range.endContainer, range.endOffset);
    endRange.insertNode(placeholder);

    range.deleteContents();

    let element = document.createElement(this.type);

    for (let name in this.props) {let value = this.props[name];
      if (name.match(/^on([\s\S]+)$/)) {const eventName = RegExp.$1.replace(/^[\s\S]/, (s) => s.toLowerCase());
        element.addEventListener(eventName, value);
      }

      if (name === "className") element.setAttribute("class", value);
      element.setAttribute(name, value);
    }

    for (let child of this.children) {let range = document.createRange();
      if (element.children.length) {range.setStartAfter(element.lastChild);
        range.setEndAfter(element.lastChild);
      } else {range.setStart(element, 0);
        range.setEnd(element, 0);
      }
      child.mountTo(range);
    }

    range.insertNode(element);
  }
}

而后 update 中就能够精简代码

update() {let vdom = this.render()
  vdom.mountTo(this.range)
}

实现 update

update 中能够获取到新旧两颗树,diff 算法就是比对这两颗树的差别。

那么咱们在比对这两个树的差别时,应该比对:

  • type
  • props
  • children

typeprops 比对都很有好了解,children 就有点麻烦,因为 children 有删除和减少不同的操作

Tips

  1. 这个算法不适宜解决有 history 的状况
  2. 函数这里没有解决,因为每次返回的都是一个新函数,造成了新旧 dom 必定不一样,依照 react 的实现,应该要有一个全局事件代理。
  3. children 没有解决新增、删除、移位等状况

实现,update 有三个函数:

  1. isSameNode 比对新旧节点是否雷同,如果雷同返回 true 否则返回 false

    1. type 是否雷同
    2. props 是否雷同
    3. props 个数是否雷同
  2. isSameTree 比对新旧 dom 是否雷同,如果雷同返回 true 否则返回 false

    1. 节点是否雷同
    2. 子节点数量是否雷同
    3. 每个子节点是否雷同
  3. replace 更新 dom 树的差别

    1. 根节点是否雷同
    2. 节点不雷同,调用 mountTo 更新 dom
    3. 如果节点雷同,就比对子节点(递归调用)
update() {
  let vdom = this.vdom
  if (this.oldVdom) {let isSameNode = (node1, node2) => {if (node1.type !== node2.type)
        return false
      for (let name in node1.props) {if (typeof node1.props[name] === "object" && typeof node2.props[name] === 'object' &&
          JSON.stringify(node1.props[name]) === JSON.stringify(node2.props[name]))
          continue
        if (node1.props[name] !== node2.props[name])
          return false
      }
      if (Object.keys(node1.props).length !== Object.keys(node2.props).length)
        return false

      return true
    }

    let isSameTree = (node1, node2) => {if (!isSameNode(node1, node2))
        return false
      if (node1.children.length !== node2.children.length)
        return false
      for (let i = 0; i < node1.children.length; i++) {if (!isSameTree(node1.children[i], node2.children[i]))
          return false
      }
      return true
    }
    let replace = (newTree, oldTree) => {if (isSameTree(newTree, oldTree)) return
      if (!isSameNode(newTree, oldTree)) {newTree.mountTo(oldTree.range)
      } else {for (let i = 0; i < newTree.children.length; i++) {replace(newTree.children[i], oldTree.children[i])
        }
      }
    }
    replace(vdom, this.oldVdom)
  } else {vdom.mountTo(this.range)
  }
  this.oldVdom = vdom
}

要实现部分跟进,须要把 update 的逻辑放进 ElementWrapper 当中

我的项目教程:ToyReact

退出移动版