Toyreact project summary

Time:2020-11-22

Webpack configuration

  • optimization: { minimize: false}: each file will be placed in theevalBysourceURLThe way to open it in the browser becomes a separate file

    eval('console.log("1");\n\n//# sourceURL=webpack:///./main.js?');
  • @babel/preset-envPut the higher versionesThe grammar is translated into a lower versionesgrammar
  • @babel/plugin-transform-react-jsxstayjsYou can usejsxgrammar

    let a = <MyComponent name="a" />;
    //Translated into
    var a = createElement(MyComponent, {
      name: "a",
    });
  • pragma: text replacement, if not added, the default isReact.crateElementWe need to implement one hereReactSo here we’re going to replace it withToyReact.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" },
              ],
            ],
          },
        },
      },
    ],
  },
};

Basic usage of toyreact

Source code

main.jsIt is the entry file of the projectToyReact, declare a componentMyComponent

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

class MyComponent {}

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

ToyReact.jsIs the core code, which forms the simplestreact

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

createElementThere are two parameters:

  • type: component nameMyComponent(above)

    • IfjsxThe label isa lowercase letterSo the first parameter here is acharacter stringNot a variable (figure below)
  • attribute: properties on component

Toyreact project summary

Toyreact project summary

Using toyreact to implement DOM

Creating DOM nodes

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

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

createElementYou need to return onedomElement, otherwise the lastdivReceivedchildrenAll of themnull

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

The first three created threespanI’ll take these three for the fourth timespanAschildrenPass it in.

So at this time

  • typenamelydiv
  • attributenamelynameandid
  • childrenThere are threespan

Toyreact project summary

Mounting DOM nodes

// 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);

spanThe text node inside becomes a string after it is passed inappendChildYou need to create a text node. (below)

// 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 project summary

realization ToyReact.render

stayreactThe mount node is not used directlydocument.body.appendChildIt’s using therenderWhy does it do this? The main reason is thatjsxIs a mix of content, components andHTMLThey coexist, if notHTMLCannot be useddocument.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);

It’s heredocument.createElementWrap one layerwrapper, createdomUnified operation of the process. thereWrapperDivided intoElementWrapperandTextWrapperElementWrapperUsed to create element nodes,TextWrapperUse to create a text node.

ElementWrapperOfappendChildThe received parameter is virtualdom

At the same time, abstract outComponent, implementationmountToandsetAttributeMethod, letMyComponentInheritance.

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);
  },
};

Expand children automatically

When I write here, there will be a problemthis.childrenIt’s not going to unfold automaticallychildLook

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

staymountToWhen we receive an array, we want to expand the array and implement an arrayinseartChildren

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
}

Implement set state, props, events

Source code

A single rendering has been completed, but it must not be rendered at one time. It will not be moved again later. It may need to be re rendered in some way.

theremain.jsIt is relatively complicated. Originally, this example is to teach us how toreactNow let’s go back and use this example to verify whether the framework support is supported

// 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);

If you run this program with the framework written above, you will find an error,propsnon-existent

Implement props

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

Implement onclick

Use regular to match and bind to elements.

stayreactBasically notremoveBecauseremoveAnd then,reactOfrenderWill be destroyed.reactOnly the wholedomTree destruction rebind an event.

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);
  }
}

Implement set state

stateandpropsIt doesn’t make any difference. It’s a normal variable, butsetStateBycomponentProvides a magic function.

setStateIt is about onemergeOperation,setStateIt will be replacedstateAfter the replacementreactWill make a new onerenderOperation of.

this.state = { value: null };

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

setStateMaybe the process is firstmergestayupdateDom

mergeImplementation: Yes, it isstateTraversal. If it is an object, it will be called recursively. If it is a basic type, it will be updated directlyoldStateCorresponding value.

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();
  }
}

to updatedomUse it hererangeTo operate.

Use it hererangeOfapiyes:

  • setStartandsetEndTwo parameters are received:nodeandoffset

    • nodeIf it’s a text node,offsetIs the text inside;nodeIf it’s an ordinary oneelementoffsetIt is the number of its internal child nodes
  • setStartBeforeandsetStartAfterReceive a node
  • deleteContentsemptyrangecontent
  • insertNodestayrangeThe actual location of the insertion node

Operation process:domThe node is not mounted directly, but israngeTo mount

  1. Element node creationrangeThen callmountToTo mount, the text node is called directlymountToTo mount
  2. ToyReact.renderWill be called to createrange, callComponentOfmountToTo mount
  3. ComponentOfmountToThe call will call its ownupdateto updatedom
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);
  },
};

updateThe core ofrangeDelete all the cores in, get new onesvdom, and then call themountTo

updateCreated aplaceholderThe function is to occupy space. Because it’s in usedeleteContentsAfter that,offsetChanges can occur, leading to errors.

mountToYou can addwillMountanddidMount

updateYou can addwillMountanddidMount

It’s written in heredomThe work has been basically completed, the following is the implementationdomBecome virtualdom

Implementation of toyreact virtual DOM

Source code

Use toyreact to implement this example

// 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[c]) {
      return squares[a];
    }
  }
  return null;
}

Put the DOM operation into mountto to complete it

takeElementWrapperOfappendChildandsetAttributeThe logic ofmountToTo do it,updateOnly the virtual operationdom

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);
  }
}

thenupdateYou can simplify the code

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

Implement update

stayupdateThe diff algorithm is to compare the differences between the old and new trees.

Then, when comparing the differences between the two trees, we should compare:

  • type
  • props
  • children

typeandpropsThe comparison is very well understood,childrenIt’s a little bit troublesome, becausechildrenThere are different operations for deleting and adding

Tips

  1. This algorithm is not suitable for dealing withhistoryThe situation
  2. The function is not handled here, because each time it returns a new function, which results in the old and new functionsdomIt must be different, according toreactTo implement, there should be a global event broker.
  3. childrenThe addition, deletion and displacement were not handled

realization,updateThere are three functions:

  1. isSameNodeCompare whether the old and new nodes are the same. If the same, returntrueOtherwise returnfalse

    1. typeIs it the same
    2. propsIs it the same
    3. propsIs the number the same
  2. isSameTreeCompare the old with the newdomWhether it is the same, if the same, returntrueOtherwise returnfalse

    1. Are nodes the same
    2. Is the number of child nodes the same
    3. Is each child node the same
  3. replaceto updatedomDifferences in trees

    1. Is the root node the same
    2. Different nodes, callmountToto updatedom
    3. If the nodes are the same, it is better than the child node (recursive call)
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
}

To achieve local follow-up, we need toupdatePut logic intoElementWrapperamong

Project tutorial: toyreact

Recommended Today

Explain module, import and export in JavaScript

Author: Tania rascia Crazy technology house Original text:https://www.taniarascia.com/j… In the era of the Internet, websites are mainly developed with HTML and CSS. If you load JavaScript into a page, it usually provides effects and interactions in the form of small fragments. Generally, all JavaScript code is written in a file and loaded into a filescriptTag. […]