自己动手搭建一个React框架——toyReact


我们之前在博客上有提到 react的教程中tic-tac-toe游戏的案例,那么在篇博客中,我们主要要实现的是搭建自己的toyReact框架,将tic-tac-toe在toyReact框架中跑起来。
这里是代码的github仓库: https://github.com/feddiyao/toyReact

环境配置

包安装:

npm install --save-dev webpack webpack-cli
npm install --save-dev babel-loader @babel/core @babel/preset-env
npm install @babel/plugin-transform-react-jsx --save-dev

webpack的config文件:

module.exports = { 
    entry: {
        main: './main.js'
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                use: {
                    loader: 'babel-loader',
                    options: {
                        presets: ['@babel/preset-env'],
                        plugins: ['@babel/plugin-transform-react-jsx']
                    }
                }
            }
        ]
    },
    mode: "development",
    optimization: {
        minimize: false
    }

}

其中modeoptimization的设置增加了页面的可读性,此时执行npx webpack 输出的main.js文件中只有相关代码:

eval("\n\n//# sourceURL=webpack:///./main.js?");

即当我们在浏览器中打开一个main.js文件时,会映射到一个单独的文件,webpack将给我们提供一个易于阅读和调试的版本。

babel是把 JavaScript 中 es2015/2016/2017/2046 的新语法转化为 es5,让低端运行环境(如浏览器和 node )能够认识并执行。
presets设定实现的效果:
当我们在main.js里面放入测试代码:

for (let i of [1, 2, 3]) {
    console.log(i);
}

webpack的输出man.js文件中相关代码为:

eval("for (var _i = 0, _arr = [1, 2, 3]; _i < _arr.length; _i++) {\n  var i = _arr[_i];\n  console.log(i);\n}\n\n//# sourceURL=webpack:///./main.js?");

JSX 原理和关键实现

上面的环境配置中 @babel/plugin-transform-react-jsx是babel专门用来处理jsx的,将jsx转换为react函数
看一下plugins设定实现的效果:
当我们在man.js中放入测试代码:

let a = <div/>

webpack的输出man.js文件中相关代码为:

var a = /*#__PURE__*/React.createElement("div", null);

当我们把plugins进行如下的配置:

plugins: [['@babel/plugin-transform-react-jsx', {pragma: 'createElement'}]]

webpack的输出man.js文件中相关代码为:

var a = createElement("div", null);

可以做一个比较,即在输出时把React.createElement替换为了createElement
多个子节点进行测试:

let a = <div id="a" class="c">
    <div></div>
    <div></div>
    <div></div>
    <div></div>
</div>

webpack的输出man.js文件中相关代码:

var a = createElement("div", {
  id: "a",
  "class": "c"
}, createElement("div", null), createElement("div", null), createElement("div", null), createElement("div", null));

第一个参数为div,第二个参数为属性列表,从第三个参数开始就是子节点。
所以我们来初步实现一下createElement:

function createElement(tagName, attributes, ...children) {
    let e = document.createElement(tagName);
    for (let p in attributes) {
        e.setAttribute(p, attributes[p]);
    }
    for (let child of children) {
        e.appendChild(child);
    }
    return e;
}

此时若我们在工作台打印一下a,则输出为:
在这里插入图片描述
在子节点中加入文本的处理:

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

可以在浏览器中调用以下代码来进行节点的挂载:

document.body.appendChild(a);

接下来考虑MyComponent类型的节点:
首先在createElement方法中加入类型的判断:

export function createElement(type, attributes, ...children) {
    let e;
    if (typeof type === "string") {
        e = document.createElement(type);
    } else {
        e = new type;
    }
    for (let p in attributes) {
        e.setAttribute(p, attributes[p]);
    }
    for (let child of children) {
        if(typeof child === "string") {
            child = document.createTextNode(child)
        }
        e.appendChild(child);
    }
    return e;
}

运行代码,控制台打印错误:

Uncaught TypeError: e.setAttribute is not a function

很显然,问题是由我们自定义的节点带来的,让我们对代码做进一步的拆分。
首先,我们需要对不同类型的节点进行不同的处理:
定义ElementWrapper和TextWrapper

class ElementWrapper {
    constructor(type) {
        this.root = document.createElement(type);
    }
    setAttribute(name, value) {
        this.root.setAttribute(name, value)
    }
    appendChild(component) {
        this.root.appendChild(component.root)
    }
}

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

对于自定义的component,我们则进行如下的处理:

export class Component {
    constructor() {
        this.props = Object.create(null);
        this.children = [];
        this._root = null;
    }
    setAttribute(name, value) {
        this.props[name] = value;
    }
    appendChild(component) {
        this.children.push(component);
    }
    get root() {
        if (!this._root) {
            this._root = this.render().root;
        }
        return this._root;
    }
}

这里注意到root方法,在这里,若没有root则执行一次render函数,会循环调用render函数,直到找到root为止。
我们让MyComponent继承Component:

class MyComponent extends Component {
    render() {
        return <div>
            <h1>my component</h1>
            {this.children}
        </div>
    }
}

此时createElement尚未进行多层子节点嵌套的处理,因此需要改进createElement函数:

export function createElement(type, attributes, ...children) {
    let e;
    if (typeof type === "string") {
        e = new ElementWrapper(type);
    } else {
        e = new type;
    }
    for (let p in attributes) {
        e.setAttribute(p, attributes[p]);
    }

    let insertChildren = (children) => {
        for (let child of children) {
            if(typeof child === "string") {
                child = new TextWrapper(child)
            }
            if(typeof child == "object" && (child instanceof Array)) {
                insertChildren(child);
            } else {
                e.appendChild(child);
            }
        }
    }
    insertChildren(children);

    return e;
}

至此,react对jsx生成实节点的基本处理就完成了。

为Toy-React添加生命周期

让组件拥有重新渲染的能力

我们都知道react将组件的状态放在了state当中,那么在toy-react实现的过程中,难的不是为组件添加state属性,难的是如何处理setState方法。我们首先来为组件添加state属性:

class MyComponent extends Component {
    constructor() {
        super();
        this.state = {
            a: 1,
            b: 2
        }
    }
    render() {
        return <div>
            <h1>my component</h1>
            <span>{this.state.a.toString()}</span>
            {this.children}
        </div>
    }
}

修改MyComponent实现的代码,我们能很轻易地获取到组件的state内容,并将它渲染到页面上。
那么接下来,我们就迎来了难点如何去实现state的更新,组件在更新的操做一定是在render中实现的。我们上一阶段的代码,是在root中实现对render的调用的,我们在Component类中添加了get root的方法:

 get root() {
        if (!this._root) {
            this._root = this.render().root;
        }
        return this._root;
    }

但是现在我们没有办法达到通过root实现更新的目的,所以在Component类中我们加入新的方法Render_To_DOM,这里会用到range,不熟悉range的小伙伴可以参考相关的MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Range,我们接着往下走:

const RENDER_TO_DOM = Symbol("render to dom");
[RENDER_TO_DOM](range){
        this.render()[RENDER_TO_DOM](range);
    }

同样的,为 ElementWrapperTextWrapper也添加Render_To_Dom 方法:

 [RENDER_TO_DOM](range){
        range.deleteContents();
        range.insertNode(this.root);
    }

更改render方法的内容:

export function render(component, parentElement) {
    let range = document.createRange();
    range.setStart(parentElement, 0);
    range.setEnd(parentElement, parentElement.childNodes.length);
    range.deleteContents(); 
    component[RENDER_TO_DOM](range);
}

ElementWrapperappendChild也进行更改

appendChild(component) {
    let range = document.createRange();
    range.setStart(this.root, this.root.childNodes.length);
    range.setEnd(this.root, this.root.childNodes.length);
    component[RENDER_TO_DOM](range);
}

这样我们就拥有了重新渲染的能力,附上更改后的全部代码:

const RENDER_TO_DOM = Symbol("render to dom");

class ElementWrapper {
    constructor(type) {
        this.root = document.createElement(type);
    }
    setAttribute(name, value) {
        this.root.setAttribute(name, value)
    }
    appendChild(component) {
        let range = document.createRange();
        range.setStart(this.root, this.root.childNodes.length);
        range.setEnd(this.root, this.root.childNodes.length);
        component[RENDER_TO_DOM](range);
    }
    [RENDER_TO_DOM](range){
        range.deleteContents();
        range.insertNode(this.root);
    }
}

class TextWrapper {
    constructor(content) {
        this.root = document.createTextNode(content);
    }
    [RENDER_TO_DOM](range){
        this.render()[RENDER_TO_DOM](range);
    }
    [RENDER_TO_DOM](range){
        range.deleteContents();
        range.insertNode(this.root);
    }
}

export class Component {
    constructor() {
        this.props = Object.create(null);
        this.children = [];
        this._root = null;
    }
    setAttribute(name, value) {
        this.props[name] = value;
    }
    appendChild(component) {
        this.children.push(component);
    }
    [RENDER_TO_DOM](range){
        this.render()[RENDER_TO_DOM](range);
    }
}

export function createElement(type, attributes, ...children) {
    let e;
    if (typeof type === "string") {
        e = new ElementWrapper(type);
    } else {
        e = new type;
    }
    for (let p in attributes) {
        e.setAttribute(p, attributes[p]);
    }

    let insertChildren = (children) => {
        for (let child of children) {
            if(typeof child === "string") {
                child = new TextWrapper(child)
            }
            if(typeof child == "object" && (child instanceof Array)) {
                insertChildren(child);
            } else {
                e.appendChild(child);
            }
        }
    }
    insertChildren(children);

    return e;
}

export function render(component, parentElement) {
    let range = document.createRange();
    range.setStart(parentElement, 0);
    range.setEnd(parentElement, parentElement.childNodes.length);
    range.deleteContents(); 
    component[RENDER_TO_DOM](range);
}

组件进行重新渲染功能实现

首先如果是重新绘制的话,我们需要将刚才的range做一个存储,以component为例:

export class Component {
    constructor() {
        this.props = Object.create(null);
        this.children = [];
        this._root = null;
        this._range = null;
    }
    setAttribute(name, value) {
        this.props[name] = value;
    }
    appendChild(component) {
        this.children.push(component);
    }
    [RENDER_TO_DOM](range){
        this._range = range;
        this.render()[RENDER_TO_DOM](range);
    }
    rerender() {
        this._range.deleteContents();
        this[RENDER_TO_DOM](this._range);
    }
}

将range保存在this._range当中,并且定义了它的rerender方法,更改main.js文件进行测试,在render中加入一个点击事件:

 render() {
        return <div>
            <h1>my component</h1>
            <button onclick={()=> {this.state.a ++; this.rerender();}}>add</button>
            <span>{this.state.a.toString()}</span>
        </div>
    }

此时需要更改ElementWrapper对点击事件进行单独处理,修改ElementWrappersetAttribute函数

 setAttribute(name, value) {
    if(name.match(/^on([\s\S]+)$/)) {
         this.root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value);
     } else {
         this.root.setAttribute(name, value);
     }
 }

此时点击button 即可完成页面的重新渲染。

setState的api实现

我们知道在react中,setState会把旧的state和新的state合并起来,不会把旧的完全替换掉。而且rerender是不需要手动去调用的,当我们调用setState方法后,会自动触发重新的渲染。
下面来看setState的方法:

setState(newState) {
    if (this.state === null || typeof this.state !== "object") {
        this.state = newState;
        this.rerender();
        return;
    }
    let merge = (oldState, newState) => {
        for (let p in newState) {
            //js著名的坑,因为null的typeof也是object类型的
            if(oldState[p] === null || typeof oldState[p] !== "object") {
                oldState[p] = newState[p];
            } else {
                merge(oldState[p], newState[p]);
            }
        }

    }
    merge(this.state, newState);
    this.rerender();
}

对应的进行测试,修改main中的render方法:

 render() {
  return <div>
        <h1>my component</h1>
        <button onclick={()=> {this.setSate({a: this.state.a + 1})}}>add</button>
        <span>{this.state.a.toString()}</span>
        <span>{this.state.a.toString()}</span>
    </div>
}

这样就完成了setState的渲染。

Tic-Tac-Toe试运行

我们将tic-tac-toe的阶段性结果的代码粘贴到自己的代码上,尝试用写的toy-react来运行tic-tac-toe,此处为tic-tac-toy代码:https://codepen.io/gaearon/pen/VbbVLg?editors=0010
粘贴后main.html页面为:

<style>
body {
font: 14px "Century Gothic", Futura, sans-serif;
margin: 20px;
}

ol, ul {
padding-left: 30px;
}

.board-row:after {
clear: both;
content: "";
display: table;
}

.status {
margin-bottom: 10px;
}

.square {
background: #fff;
border: 1px solid #999;
float: left;
font-size: 24px;
font-weight: bold;
line-height: 34px;
height: 34px;
margin-right: -1px;
margin-top: -1px;
padding: 0;
text-align: center;
width: 34px;
}

.square:focus {
outline: none;
}

.kbd-navigation .square:focus {
background: #ddd;
}

.game {
display: flex;
flex-direction: row;
}

.game-info {
margin-left: 20px;
}
</style>
<body>
    <div id="root"></div>
</body>
<script src="main.js"></script>

main.js页面为:

import {createElement, Component, render} from "./toy-react.js"
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}
        </button>
      );
    }
  }
  
  class Board extends Component {
    renderSquare(i) {
      return <Square />;
    }
  
    render() {
      const status = 'Next player: X';
  
      return (
        <div>
          <div className="status">{status}</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 {
    render() {
      return (
        <div className="game">
          <div className="game-board">
            <Board />
          </div>
          <div className="game-info">
            <div>{/* status */}</div>
            <ol>{/* TODO */}</ol>
          </div>
        </div>
      );
    }
  }
  
  // ========================================
  
render(
    <Game />,
    document.getElementById('root')
);

页面运行后报错:

toy-react.js:46 Uncaught TypeError: Cannot read property 'Symbol(render to dom)' of null

所以需要优化toy-react.js中的代码,在createElementinsertChildren加入childnull的判断:

 let insertChildren = (children) => {
     for (let child of children) {
         if(typeof child === "string") {
             child = new TextWrapper(child)
         }
         if(child === null) {
             continue;
         }
         if(typeof child == "object" && (child instanceof Array)) {
             insertChildren(child);
         } else {
             e.appendChild(child);
         }
     }
 }

这样就可以运行了,但是有一个问题是页面的样式好像没有生效,检查以后发现是我们的toy-react中没有加入对className的处理:

 setAttribute(name, value) {
   if(name.match(/^on([\s\S]+)$/)) {
         //大小写敏感事件若采用驼峰命名则单独处理
         this.root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value);
     } else {
         if(name == "className") {
             this.root.setAttribute("class", value);
         } else {
             this.root.setAttribute(name, value);
         }
     }
 }

这样游戏就能正常地运行起来了。
此时会产生一个新的问题,当我们从左往右进行点击的时候,页面就会丢东西,这个问题是由range带来的。
我们看一下rerender的代码:

rerender() {
      this._range.deleteContents();
      this[RENDER_TO_DOM](this._range);
  }

当我们调用deleteContents时,会让range成为一个全空的range,这时会导致一个问题,就是相邻的range会把这个全空的range吞进去,我们再做插入的时候就会被后边的range包含进去。所以我们再rerender的时候要保证这个range是不空的,为了保证range不空,我们就要先插入,再删除:

  rerender() {
     //保存老的range,避免调用RENDER_TO_DOM方法插入后修改了this._range
     let oldRange = this._range;
     let range = document.createRange();
     range.setStart(_this.range.startContainer, _this.range.startOffset);
     range.setEnd(_this.range.startContainer, _this.range.startOffset);
     this[RENDER_TO_DOM](range);
     //将老的range挪到插入之后
     oldRange.setStart(range.endContainer, range.endOffset);
     oldRange.deleteContents();
 }

然后我们把最终的tic-tac-toe的代码放到页面中:
页面代码:https://codepen.io/gaearon/pen/gWWZgR?editors=0010
当然,代码要根据我们的实现做一个微调,挪动后的main.js:

import {createElement, Component, render} from "./toy-react.js"
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>
      );
    }
  }
  
  // ========================================
  
render(<Game />, document.getElementById("root"));
  
  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;
  }

成功运行。

虚拟DOM原理和关键实现

在此之前我们已经实现了react的基本功能,但是它有一个致命的问题,没有实现虚拟DOM,这将导致我们每次进行的更新都是全量的。在这样的情况下,哪怕我们是一个很细小的操作,都会导致页面的整体的dom的更新,这是完全不可接受的,所以我们在这里加入了vdom的概念。

虚拟DOM树的构建

我们在ElementWrapper中加入方法:

get vdom(){
     return {
         type: this.type,
         props: this.props,
         children: this.children.map(child => child.vdom)
     }
 }

其中this.type可以在contructor当中进行获得,那么props和children呢?我们知道这两个其实是与ElementWrappersetAttributeappendChild相关的(一个是存this.props另一个是存this.children)我们就要对这两个模块的代码做一定的改造。而这个逻辑就是Component里面的逻辑,我们让ElementWrapper去extendsComponent
同理,对TextWrapper也这么做,在TextWrapper中加入vdom方法:

 get vdom() {
     return {
         type:"#text",
         content: this.content
     }
 }

实现Component的vdom方法:

    get vdom() {
       return this.render().vdom;
    }

修改main.js代码,把vdom做一个打印输出:

// render(<Game />, document.getElementById("root"));
let game = <Game/>;
console.log(game.vdom);

结果查看:
在这里插入图片描述
我们可以看到这个div是没有children的,我们必须要把ElementWrapper中的setAttributeappendChild方法注释掉,它们才能调用到Component中的方法。
注释掉以后再打印,可以弹道children和prop就出来了:
在这里插入图片描述
到这一步为止我们已经完成了虚拟dom树的构建。

虚拟DOM生成真实的DOM

因为我们是基于vdom去做一个更新的,所以ElementWrapper中的constructor方法中的root就不再需要了。接下来看获得vdom的方法,现在这个方法返回的是一个新的对象,这个返回其实是有问题的,如果这个对象上面没有方法,我们就没办法完成重绘,所以我们更改这个vdom的方法。
将vdom的get方法都改为return this;
这时vom的children变成了组件的children,我们为Component加入vchildren的方法:

get vchildren() {
      return this.children.map(child => child.vdom);
  }

接下来,在原来的ElementWrappesetAttributeappendChild方法做的事情,我们要放到RENDER_TO_DOM方法里面去完成,更改RENDER_TO_DOM方法:

   [RENDER_TO_DOM](range){
        range.deleteContents();

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

        //所有prop里面的内容要抄写到attribute上
         for(let name in this.props) {
             let value = this.props[name];
            if(name.match(/^on([\s\S]+)$/)) {
                //大小写敏感事件若采用驼峰命名则单独处理
                root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value);
            } else {
                if(name == "className") {
                    root.setAttribute("class", value);
                } else {
                    root.setAttribute(name, value);
                }
            }
         }

         //处理children
         for (let child of this.children) {
            let childRange = document.createRange();
            childRange.setStart(root, root.childNodes.length);
            childRange.setEnd(root, root.childNodes.length);
            child[RENDER_TO_DOM](childRange);
         }

        range.insertNode(root);
    }

到这一步我们已经实现了虚拟dom到实体dom的更新。

VDOM比对

接下来我们就开始进行重头戏,vdom树的比对了。
首先我们得rerender方法可以退休了,我们不再是实现重新渲染,而是更新操作。而vdom的比对,我们将放在Component里面去实现,首先看一下RENDER_TO_DOM的方法,因为我们要在里面完成节点的渲染,所以我们的比对也需要在里面进行:
我们创建update方法只会进行同位置的节点的比对:

 update() {
        let isSameNode = (oldNode, newNode) => {
            if (oldNode.type != newNode.type) 
                return false;
            for (let name in newNode.props) {
                if (newNode.props[name] !== oldNode.props[name]) {
                    return false;
                }
            }

            if (Object.keys(oldNode.props).length > Object.keys(newNode.props).length)
                return false;

            if (newNode.type === "#text") {
                if(newNode.content !== oldNode.content)
                    return false;
            }
            return true;
        }
        //递归访问vdom的内容
        let update = (oldNode, newNode) => {
            //type, props, children
            //#text content
            if(!isSameNode(oldNode, newNode)) {
                newNode[RENDER_TO_DOM](oldNode._range);
                return;
            }
            newNode._range = oldNode._range;

            //处理children的问题
            let newChildren = newNode.vchildren;
            let oldChildren = oldNode.vchildren;

            for (let i = 0; i < newChildren.length; i++) {
                let newChild = newChildren[i];
                let oldChild = oldChildren[i];
                if (i < oldChildren.length) {
                    update(oldChild, newChild);
                } else {
                    //TODO
                }
            }

        }
        let vdom = this.vdom;
        update(this._vdom, vdom);
        this._vdom = vdom;
    }

单独抽调replace方法进行range的更新

function replaceContent(range, node) {
    range.insertNode(node);
    range.setStartAfter(node);
    range.deleteContents();

    range.setStartBefore(node);
    range.setEndAfter(node);
}

TODO代码补全

 let tailRange = oldChildren[oldChildren.length - 1]._range;
            
for (let i = 0; i < newChildren.length; i++) {
      let newChild = newChildren[i];
      let oldChild = oldChildren[i];
      if (i < oldChildren.length) {
          update(oldChild, newChild);
      } else {
          //如果oldchildre的数量小于newchildren的数量,我们就要去执行插入
          let range = document.createRange();
          range.setStart(tailRange.endContainer, tailRange.endOffseet);
          range.setEnd(tailRange.endContainer, tailRange.endOffseet);
          newChild[RENDER_TO_DOM](range);
          tailRange = range;
      }
  }

最终代码

在完成一些小的bug的修复后,我们就实现了一个基本的toy-react代码:

const RENDER_TO_DOM = Symbol("render to dom");

export class Component {
    constructor() {
        this.props = Object.create(null);
        this.children = [];
        this._root = null;
        this._range = null;
    }
    setAttribute(name, value) {
        this.props[name] = value;
    }
    appendChild(component) {
        this.children.push(component);
    }
    get vdom() {
       return this.render().vdom;
    }
    [RENDER_TO_DOM](range){
        this._range = range;
        //赋值的vdom是一个getter,将会重新render得到一棵新的dom树
        this._vdom = this.vdom;
        this._vdom[RENDER_TO_DOM](range);
    }

    update() {
        let isSameNode = (oldNode, newNode) => {
            if (oldNode.type != newNode.type) 
                return false;
            for (let name in newNode.props) {
                if (newNode.props[name] !== oldNode.props[name]) {
                    return false;
                }
            }

            if (Object.keys(oldNode.props).length > Object.keys(newNode.props).length)
                return false;

            if (newNode.type === "#text") {
                if(newNode.content !== oldNode.content)
                    return false;
            }
            return true;
        }
        //递归访问vdom的内容
        let update = (oldNode, newNode) => {
            //type, props, children
            //#text content
            if(!isSameNode(oldNode, newNode)) {
                newNode[RENDER_TO_DOM](oldNode._range);
                return;
            }
            newNode._range = oldNode._range;

            //处理children的问题
            let newChildren = newNode.vchildren;
            let oldChildren = oldNode.vchildren;

            if(!newChildren || !newChildren.length) {
                return;
            }

            let tailRange = oldChildren[oldChildren.length - 1]._range;
            
            for (let i = 0; i < newChildren.length; i++) {
                let newChild = newChildren[i];
                let oldChild = oldChildren[i];
                if (i < oldChildren.length) {
                    update(oldChild, newChild);
                } else {
                    //如果oldchildre的数量小于newchildren的数量,我们就要去执行插入
                    let range = document.createRange();
                    range.setStart(tailRange.endContainer, tailRange.endOffseet);
                    range.setEnd(tailRange.endContainer, tailRange.endOffseet);
                    newChild[RENDER_TO_DOM](range);
                    tailRange = range;
                }
            }

        }
        let vdom = this.vdom;
        update(this._vdom, vdom);
        this._vdom = vdom;
    }
    // rerender() {
    //     //保存老的range,避免调用RENDER_TO_DOM方法插入后修改了this._range
    //     let oldRange = this._range;
    //     let range = document.createRange();
    //     range.setStart(this._range.startContainer, this._range.startOffset);
    //     range.setEnd(this._range.startContainer, this._range.startOffset);
    //     this[RENDER_TO_DOM](range);
    //     //将老的range挪到插入之后
    //     oldRange.setStart(range.endContainer, range.endOffset);
    //     oldRange.deleteContents();
    // }
    setState(newState) {
        if (this.state === null || typeof this.state !== "object") {
            this.state = newState;
            this.update();
            return;
        }
        let merge = (oldState, newState) => {
            for (let p in newState) {
                //js著名的坑,因为null的typeof也是object类型的
                if(oldState[p] === null || typeof oldState[p] !== "object") {
                    oldState[p] = newState[p];
                } else {
                    merge(oldState[p], newState[p]);
                }
            }

        }
        merge(this.state, newState);
        this.update();
    }
}

class ElementWrapper extends Component{
    constructor(type) {
        super(type)
        this.type = type;
    }
    // setAttribute(name, value) {
    //     if(name.match(/^on([\s\S]+)$/)) {
    //         //大小写敏感事件若采用驼峰命名则单独处理
    //         this.root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value);
    //     } else {
    //         if(name == "className") {
    //             this.root.setAttribute("class", value);
    //         } else {
    //             this.root.setAttribute(name, value);
    //         }
    //     }
    // }

    get vdom(){
        this.vchildren = this.children.map(child => child.vdom);
        return this;
        // {
        //     type: this.type,
        //     props: this.props,
        //     children: this.children.map(child => child.vdom)
        // }
    }
    // appendChild(component) {
    //     let range = document.createRange();
    //     range.setStart(this.root, this.root.childNodes.length);
    //     range.setEnd(this.root, this.root.childNodes.length);
    //     component[RENDER_TO_DOM](range);
    // }
    [RENDER_TO_DOM](range){
        this._range = range;

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

        //所有prop里面的内容要抄写到attribute上
         for(let name in this.props) {
             let value = this.props[name];
            if(name.match(/^on([\s\S]+)$/)) {
                //大小写敏感事件若采用驼峰命名则单独处理
                root.addEventListener(RegExp.$1.replace(/^[\s\S]/, c => c.toLowerCase()), value);
            } else {
                if(name == "className") {
                    root.setAttribute("class", value);
                } else {
                    root.setAttribute(name, value);
                }
            }
         }

         if(!this.vchildren) {
            this.vchildren = this.children.map(child => child.vdom);
         }

         //处理children
         for (let child of this.vchildren) {
            let childRange = document.createRange();
            childRange.setStart(root, root.childNodes.length);
            childRange.setEnd(root, root.childNodes.length);
            child[RENDER_TO_DOM](childRange);
         }

         replaceContent(range, root)
    }
}

class TextWrapper extends Component {
    constructor(content) {
        super(content);
        this.type = "#text";
        this.content = content;
    }
    get vdom() {
        return this;
        // {
        //     type:"#text",
        //     content: this.content
        // }
    }

    [RENDER_TO_DOM](range){
        this._range = range;
        let root = document.createTextNode(this.content)
        replaceContent(range, root)
    }
}

function replaceContent(range, node) {
    range.insertNode(node);
    range.setStartAfter(node);
    range.deleteContents();

    range.setStartBefore(node);
    range.setEndAfter(node);
}

export function createElement(type, attributes, ...children) {
    let e;
    if (typeof type === "string") {
        e = new ElementWrapper(type);
    } else {
        e = new type;
    }
    for (let p in attributes) {
        e.setAttribute(p, attributes[p]);
    }

    let insertChildren = (children) => {
        for (let child of children) {
            if(typeof child === "string") {
                child = new TextWrapper(child)
            }
            if(child === null) {
                continue;
            }
            if(typeof child == "object" && (child instanceof Array)) {
                insertChildren(child);
            } else {
                e.appendChild(child);
            }
        }
    }
    insertChildren(children);

    return e;
}

export function render(component, parentElement) {
    let range = document.createRange();
    range.setStart(parentElement, 0);
    range.setEnd(parentElement, parentElement.childNodes.length);
    range.deleteContents(); 
    component[RENDER_TO_DOM](range);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值