手写简易版React框架

手写简易版React框架

1.基础环境的搭建

1.1首先将自己配置好的webpack环境搭好,目录结构如下:

webpack基本配置

1.2 React最基本的功能就是渲染jsx语法,其中用到了babel-loader,我们这里在webpack配置文件里已经配置好了babel-loader。然后新建一个文件夹名为my-react,在里面建立一个index.js写我们自己的react。新建一个react-dom文件夹,在里面新建一个index.js写我们自己的react-dom。

babel的作用: 首先把相关的代码转换->调用React.createElement()方法,调用的时候会把转换后的结果以参数的新式传递给该方法

在这里插入图片描述

2.编写createElement方法,便于解析jsx语法

2.1在入口文件index.js中简单写一段jsx语法的代码,并做打印。

import React from './my-react/index'
import ReactDOM from './react-dom/index'

const elem = (
  <div>hello</div>
)

console.log(elem);

2.2 在my-react中编写createElement方法

打印时,发现控制台报错:Uncaught TypeError: _index2.default.createElement is not a function。

经查询:React.createElement方法来自于React框架,作用就是返回对应的虚拟DOM。

在my-react中写createElement方法,并将我们自己编写的React实例对象导出。

//my-react/index.js
function createElement(tag,props,...children) {
  return new Element(tag,props,children)
}

class Element {
  constructor(tag,props,children) {
    this.tag = tag
    this.props = props
    this.children = children
  }
}

const React = {
  createElement
}

export default React

3.编写ReactDOM.render方法用来渲染传入的对象,并挂载到DOM节点上。

3.1普通文本的渲染处理

/**
 * //根据传入的虚拟DOM,返回真实DOM节点
 * @param {虚拟DOM} vdom 
 */
function createDom(vdom) {
  if (vdom == 'undefined') return
  //普通文本的处理
  if (typeof vdom == 'string' || typeof vdom == 'number') {
    return document.createTextNode(vdom)
  }
}

/**
 * 
 * @param {虚拟DOM} vdom 
 * @param {容器} container 
 */
function render(vdom,container) {
  //根据虚拟DOM转换为真实DOM
  const dom = createDom(vdom)
  //将真实DOM添加到容器DOM中
  container.appendChild(dom)
}

const ReactDOM = {
  render
}

export default ReactDOM

3.2 jsx虚拟DOM渲染处理

/**
 * //根据传入的虚拟DOM,返回真实DOM节点
 * @param {虚拟DOM} vdom 
 */
function createDom(vdom) {
  if (vdom == 'undefined') return
  //普通文本的处理
  if (typeof vdom == 'string' || typeof vdom == 'number') {
    return document.createTextNode(vdom)
  }
  //jsx对象处理
  else if (typeof vdom.tag == 'string') {
    const dom = document.createElement(vdom.tag)
    if (vdom.props) {
      //给dom添加属性
      for (let key in vdom.props) {
        setProperty(dom,key,vdom.props[key])
      }
    }
    //递归处理子节点
    if (vdom.children && vdom.children.length > 0) {
      vdom.children.forEach(item => render(item,dom))
    }
    return dom
  }
}

/**
 * 
 * @param {属性名} key 
 * @param {属性值} value 
 * @param {DOM节点} dom 
 */
function setProperty(dom,key,value) {
  //事件的处理 如果属性名以on开头则是事件,再将事件的key全变小写
  key.startsWith('on') && (key = key.toLowerCase())
  //样式的处理
  if (key == 'style' && value) {
    if (typeof value == 'string') {
      //如果value是字符串
      dom.style.cssText = value
    } else if (typeof value == 'object') {
      //如果value是对象
      for (let attr in value) {
        dom.style[attr] = value[attr]
      }
    }
  } else {
    //样式以外的处理
    dom[key] = value
  }
}

/**
 * 
 * @param {虚拟DOM} vdom 
 * @param {容器} container 
 */
function render(vdom,container) {
  //根据虚拟DOM转换为真实DOM
  const dom = createDom(vdom)
  //将真实DOM添加到容器DOM中
  container.appendChild(dom)
}

const ReactDOM = {
  render
}

export default ReactDOM

3.3渲染组件处理

我们将组件分为函数组件和类组件,首先我们写出如何渲染类组件的方法。在index.js入口文件写一个类组件:

//index.js
import React, { Component } from './my-react/index'
import ReactDOM from './react-dom/index'

// function App1(props) {
//   return (
//     <div className="App1">
//       <h1>App1 function组件</h1>
//     </div>
//   )
// }

class App2 extends Component {
  constructor(props){
    super(props)
  }
  render() {
    return (
      <div>
        <p>App2 class组件</p>
      </div>
    )
  }
}

// const elem = (
//   <div className="App" style={{border: '1px solid #ccc'}}>
//     <h1 className="title" style="color:red;" onClick={()=>alert(111)}>hello</h1>
//   </div>
// )

ReactDOM.render(<App2/>, document.getElementById('root'))

类组件需要继承自Component类,所以我们去my-react中创建一个Component类,并导出。

//my-react/index.js
export class Element {
  constructor(tag,props,children) {
    this.tag = tag
    this.props = props
    this.children = children
  }
}

class Component {
  constructor(props = {}) {
    this.props = props,
    this.state = {}
  }
}

function createElement(tag,props,...children) {
  return new Element(tag,props,children)
}

const React = {
  createElement,
  Component
}

export default React
export { Component }

此时,仍然无法渲染这个组件,因为我们在ReactDOM.render方法中还没有对如何渲染类组件做相应的处理。此时在react-dom的index.js中来处理类组件相关的方法。

//react-dom/index.js

/**
 * //根据传入的虚拟DOM,返回真实DOM节点
 * @param {虚拟DOM} vdom 
 */
function createDom(vdom) {
  if (vdom == 'undefined') return
  //普通文本的处理
  ...
  //jsx对象处理
  ...
  //组件的处理
  else if (typeof vdom.tag == 'function') {
    //创建组件的实例
    const instance = createComponentInstance(vdom.tag,vdom.props)
    //生成实例对应的DOM节点
    createDomForComponentInstance(instance)
    return instance.dom
  }
}

/**
 * 
 * @param {属性名} key 
 * @param {属性值} value 
 * @param {DOM节点} dom 
 */
function setProperty(DOM,key,value) {
  //事件的处理 如果属性名以on开头则是事件,再将事件的key全变小写
  ...
  //样式的处理
  ...
}

function createComponentInstance(comp,props) {
  let instance = null
  //类组件 直接用new生成一个组件实例
  instance = new comp(props)
  return instance
}
/**
 * 
 * @param {组件实例} instance 
 */
function createDomForComponentInstance(instance) {
  //获取到虚拟DOM并挂载到实例上 因为类组件的render方法中return的就是jsx对象
  //所以直接调用render方法获取获取虚拟DOM
  instance.vdom = instance.render()
  //生成真实的DOM节点,并且也挂载到实例上
  instance.dom = createDom(instance.vdom)
}

/**
 * 
 * @param {虚拟DOM} vdom 
 * @param {容器} container 
 */
function render(vdom,container) {
  //根据虚拟DOM转换为真实DOM
  const dom = createDom(vdom)
  //将真实DOM添加到容器DOM中
  container.appendChild(dom)
}

const ReactDOM = {
  render
}

export default ReactDOM

对类组件处理完成以后,再对函数组件进行相应的处理。函数组件处理的方式与类组件有所不同。

我们将函数组件传到ReactDOM.render方法中,应该对函数组件和类组件做不同的区分,然后分别处理。

//入口文件 index.js
import React, { Component } from './my-react/index'
import ReactDOM from './react-dom/index'

function App1(props) {
  return (
    <div className="App1">
      <h1>App1 function组件</h1>
    </div>
  )
}

ReactDOM.render(<App1/>, document.getElementById('root'))

在处理函数组件的时候,它与处理类组件的不同在于,无法直接用new关键字创建一个实例。

所以在生成组件实例的createComponentInstance方法中,我们通过组件的原型对象上是否有render方法来判断传过来的组件是类组件还是函数组件。

原型对象上有render方法则是类组件,直接用new关键字创建一个实例

否则是函数组件,我们引入my-react中的Element类,通过new Element()生成一个Element类实例。该实例constructor指向自身,并添加一个render方法,返回的是调用自身的结果,即jsx对象。方便调用render方法创建虚拟DOM。

//react-dom/index.js

/**
 * //根据传入的虚拟DOM,返回真实DOM节点
 * @param {虚拟DOM} vdom 
 */
function createDom(vdom) {
  if (vdom == 'undefined') return
  //普通文本的处理
  ...
  //jsx对象处理
  ...
  //组件的处理
  else if (typeof vdom.tag == 'function') {
  	//类组件和函数组件的tag属性都等于'function'
  	//所以都能进入到这个分支里
	//不同之处在创建组件的实例方法里
	
    //创建组件的实例
    const instance = createComponentInstance(vdom.tag,vdom.props)
    //生成实例对应的DOM节点
    createDomForComponentInstance(instance)
    return instance.dom
  }
}

/**
 * 
 * @param {属性名} key 
 * @param {属性值} value 
 * @param {DOM节点} dom 
 */
function setProperty(dom,key,value) {
  //事件的处理 如果属性名以on开头则是事件,再将事件的key全变小写
  ...
  //样式的处理
  ...
}

function createComponentInstance(comp,props) {
  let instance = null
  if (comp.prototype.render) {
    //组件的原型对象上有render方法,则是类组件
    //类组件 直接用new生成一个组件实例
    instance = new comp(props)
    
  } else {
    //是函数组件
    instance = new Element(comp)
    instance.constructor = comp
    instance.render = function (props) {
      return comp(props)
    }
  }
  return instance
}
/**
 * 
 * @param {组件实例} instance 
 */
function createDomForComponentInstance(instance) {
  //获取到虚拟DOM并挂载到实例上 因为类组件的render方法中return的就是jsx对象
  //所以直接调用render方法获取获取虚拟DOM
  instance.vdom = instance.render()
  //生成真实的DOM节点,并且也挂载到实例上
  instance.dom = createDom(instance.vdom)
}

/**
 * 
 * @param {虚拟DOM} vdom 
 * @param {容器} container 
 */
function render(vdom,container) {
  //根据虚拟DOM转换为真实DOM
  const dom = createDom(vdom)
  //将真实DOM添加到容器DOM中
  container.appendChild(dom)
}

const ReactDOM = {
  render
}

export default ReactDOM

4.编写组件的更新以及this.setState方法

4.1简单编写this.setState方法

首先对之前的代码稍做测试,我们添加一个state,然后添加一个点击事件,结果是没有问题的。

//index.js
import React, { Component } from './my-react/index'
import ReactDOM from './react-dom/index'

class App2 extends Component {
  constructor(props){
    super(props)
    this.state = {
      num: 0
    }
  }
  handelClick() {
    console.log(111);
  }
  render() {
    return (
      <div>
        <p>App2 class组件======{this.state.num}</p>
        <button onClick={this.handelClick.bind(this)}>按钮</button>
      </div>
    )
  }
}

ReactDOM.render(<App2/>, document.getElementById('root'))

在这里插入图片描述
然后在点击事件里更新数据,调用this.setState方法,但是我们还没有该方法。

思考一下,该方法因为所有的组件都能调用,所以应该写在Component类里,这样所有的组件都有了这个方法。

//index.js
import React, { Component } from './my-react/index'
import ReactDOM from './react-dom/index'

class App2 extends Component {
  constructor(props){
    super(props)
    this.state = {
      num: 0
    }
  }
  handelClick() {
    this.setState({
      num: this.state.num + 1
    })
  }
  render() {
    return (
      <div>
        <p>App2 class组件======{this.state.num}</p>
        <button onClick={this.handelClick.bind(this)}>按钮</button>
      </div>
    )
  }
}

ReactDOM.render(<App2/>, document.getElementById('root'))
//my-react/index.js
export class Element {
  ...
}

class Component {
  constructor(props = {}) {
    this.props = props,
    this.state = {}
  }
  
  setState(updatedState) {
  	//updatedState 是传入的需要更新的数据
  	
    //合并对象  将新的state合并到旧的state上
    Object.assign(this.state,updatedState)
    console.log(this.state);//{num: 1}
    //再调用render方法重新生成虚拟DOM
    const newVdom = this.render()
    //根据虚拟DOM生成DOM
    const newDom = createDom(newVdom)
    //替换DOM节点  当调用过createDom方法后,组件实例上就已经挂载了DOM节点
    if (this.dom.parentNode) {
      this.dom.parentNode.replaceChild(newDom, this.dom)
    }
    //将最新的DOM节点挂载到实例上
    this.dom = newDom
  }
}
...
...

export default React
export { Component }

此时我们实现了更新数据,但是和官方React不同的是,React是用diff算法,根据虚拟DOM进行只更新需要更新的数据。

而我们现在是将整个DOM节点进行了更新。

4.2通过diff算法找出新旧虚拟DOM 之间的区别,然后只更新需要更新的DOM。

在react-dom文件夹里新建一个diff.js、patch.js和patches-type.js.

具体的diff算法对两棵树结构进行深度优先遍历,找出不同。这里可直接复制来使用。

//react-dom/diff.js
import { Element } from '../my-react'
import { PATCHES_TYPE } from './patches-type'
const diffHelper = {
    Index: 0,
    isTextNode: (eleObj) => {
        return !(eleObj instanceof Element);
    },
    diffAttr: (oldAttr, newAttr) => {
        let patches = {}
        for (let key in oldAttr) {
            if (oldAttr[key] !== newAttr[key]) {
                // 可能产生了更改 或者 新属性为undefined,也就是该属性被删除
                patches[key] = newAttr[key];
            }
        }

        for (let key in newAttr) {
            // 新增属性
            if (!oldAttr.hasOwnProperty(key)) {
                patches[key] = newAttr[key];
            }
        }

        return patches;
    },
    diffChildren: (oldChild, newChild, patches) => {
        if (newChild.length > oldChild.length) {
            // 有新节点产生
            patches[diffHelper.Index] = patches[diffHelper.Index] || [];
            patches[diffHelper.Index].push({
                type: PATCHES_TYPE.ADD,
                nodeList: newChild.slice(oldChild.length)
            });
        }
        oldChild.forEach((children, index) => {
            dfsWalk(children, newChild[index], ++diffHelper.Index, patches);
        });
    },
    dfsChildren: (oldChild) => {
        if (!diffHelper.isTextNode(oldChild)) {
            oldChild.children.forEach(children => {
                ++diffHelper.Index;
                diffHelper.dfsChildren(children);
            });
        }
    }
}



export function diff(oldTree, newTree) {
    // 当前节点的标志 每次调用Diff,从0重新计数
    diffHelper.Index = 0;
    let patches = {};

    // 进行深度优先遍历
    dfsWalk(oldTree, newTree, diffHelper.Index, patches);

    // 返回补丁对象
    return patches;
}

function dfsWalk(oldNode, newNode, index, patches) {
    let currentPatches = [];
    if (!newNode) {
        // 如果不存在新节点,发生了移除,产生一个关于 Remove 的 patch 补丁
        currentPatches.push({
            type: PATCHES_TYPE.REMOVE
        });

        // 删除了但依旧要遍历旧树的节点确保 Index 正确
        diffHelper.dfsChildren(oldNode);
    } else if (diffHelper.isTextNode(oldNode) && diffHelper.isTextNode(newNode)) {
        // 都是纯文本节点 如果内容不同,产生一个关于 textContent 的 patch
        if (oldNode !== newNode) {
            currentPatches.push({
                type: PATCHES_TYPE.TEXT,
                text: newNode
            });
        }
    } else if (oldNode.tag === newNode.tag) {
        // 如果节点类型相同,比较属性差异,如若属性不同,产生一个关于属性的 patch 补丁
        let attrs = diffHelper.diffAttr(oldNode.props, newNode.props);

        // 有attr差异
        if (Object.keys(attrs).length > 0) {
            currentPatches.push({
                type: PATCHES_TYPE.ATTRS,
                attrs: attrs
            });
        }

        // 如果存在孩子节点,处理孩子节点
        diffHelper.diffChildren(oldNode.children, newNode.children, patches);
    } else {
        // 如果节点类型不同,说明发生了替换
        currentPatches.push({
            type: PATCHES_TYPE.REPLACE,
            node: newNode
        });
        // 替换了但依旧要遍历旧树的节点确保 Index 正确
        diffHelper.dfsChildren(oldNode);
    }

    // 如果当前节点存在补丁,则将该补丁信息填入传入的patches对象中
    if (currentPatches.length) {
        patches[index] = patches[index] ? patches[index].concat(currentPatches) : currentPatches;
    }
}
//react-dom/patch.js
import { Element } from '../my-react'
import { setProperty, createDom } from './index'
import { PATCHES_TYPE } from './patches-type'

export function patch(node, patches) {
    let patchHelper = {
        Index: 0
    }
    dfsPatch(node, patches, patchHelper);
}

function dfsPatch(node, patches, patchHelper) {
    let currentPatch = patches[patchHelper.Index];
    node.childNodes.forEach(child => {
        patchHelper.Index++
        dfsPatch(child, patches, patchHelper);
    });
    if (currentPatch) {
        doPatch(node, currentPatch);
    }
}

function doPatch(node, patches) {
    patches.forEach(patch => {
        switch (patch.type) {
            case PATCHES_TYPE.ATTRS:
                for (let key in patch.attrs) {
                    if (patch.attrs[key] !== undefined) {
                        setProperty(node, key, patch.attrs[key]);
                    } else {
                        node.removeAttribute(key);
                    }
                }
                break;
            case PATCHES_TYPE.TEXT:
                node.textContent = patch.text;
                break;
            case PATCHES_TYPE.REPLACE:
                let newNode = patch.node instanceof Element ? createDom(patch.node) : document.createTextNode(patch.node);
                node.parentNode.replaceChild(newNode, node);
                break;
            case PATCHES_TYPE.REMOVE:
                node.parentNode.removeChild(node);
                break;
            case PATCHES_TYPE.ADD:
                patch.nodeList.forEach(newNode => {
                    let n = newNode instanceof Element ? createDom(newNode) : document.createTextNode(newNode);
                    node.appendChild(n);
                });
                break;
            default:
                break;
        }
    })
}
//react-dom/patches-type.js
export const PATCHES_TYPE = {
  ATTRS: 'ATTRS',
  REPLACE: 'REPLACE',
  TEXT: 'TEXT',
  REMOVE: 'REMOVE',
  ADD: 'ADD'
}

把diff算法相关的文件引入完成以后,我们对my-react中的setState方法进行一个修改。

//my-react/index.js
import { createDom } from '../react-dom/index'
import { diff } from '../react-dom/diff'
import { patch } from '../react-dom/patch'

export class Element {
  constructor(tag,props,children) {
    this.tag = tag
    this.props = props
    this.children = children
  }
}

class Component {
  constructor(props = {}) {
    this.props = props,
    this.state = {}
  }
  /**
   * 
   * @param {传入的需要更新的数据} updatedState 
   */
  setState(updatedState) {
    //合并对象  将新的state合并到旧的state上
    Object.assign(this.state,updatedState)
    //再调用render方法重新生成新的虚拟DOM
    const newVdom = this.render()
    //根据diff算法找出新旧虚拟DOM的区别
    const patches = diff(this.vdom,newVdom)
    //根据不同,更新DOM节点
    patch(this.dom,patches)
    //将最新的虚拟DOM挂载到实例上
    this.vdom = newVdom
  }
}

function createElement(tag,props,...children) {
  return new Element(tag,props,children)
}

const React = {
  createElement,
  Component
}

export default React
export { Component }

至此,我们已经完成了简易的setState的方法。

但是经测试,发现一个错误
在这里插入图片描述
这是因为我们在使用与diff算法相关方法时,使用到了react-dom里的setProperty方法,但是我们在定义该方法时,并没有导出。所以我们进入react-dom/index.js中,将该方法导出。

//react-dom/index.js
...
export function createDom(vdom) {
...
}
...
//将此方法导出
export function setProperty(dom,key,value) {
...
}

再次测试,没有任何问题,而且更新也是根据diff算法进行局部的更新。

5.生命周期

我们先看一下react的生命周期函数
在这里插入图片描述
其中,最常用的几个生命周期函数为constructor、render、componentDidMount、componentDidUpdated。

前两个函数,我们在之前写组件相关的代码时已经有写过,并且在相应的时间进行了调用。

因为我们写的只是简易版的react,所以其余的不常用的我们暂时不写。现在只需要写componentDidMount和componentDidUppdated这两个函数。

5.1 componentDidMount

首先分析,componentDidMount只在组件挂载完成后,执行一次。之后如果数据发生更新,则不再执行。

所以我们应该在react-dom中编写该方法。componentDidMount是在组件挂载完成以后执行,所以我们找到在组件实例中挂载虚拟DOM的方法。在此处添加componentDidMount方法。

//react-dom/index.js
import { Element } from '../my-react/index'

/**
 * //根据传入的虚拟DOM,返回真实DOM节点
 * @param {虚拟DOM} vdom 
 */
export function createDom(vdom) {
  ...
  //组件的处理
  else if (typeof vdom.tag == 'function') {
    //创建组件的实例
    const instance = createComponentInstance(vdom.tag,vdom.props)
    //生成实例对应的DOM节点
    createDomForComponentInstance(instance)
    return instance.dom
  }
}

/**
 * 
 * @param {属性名} key 
 * @param {属性值} value 
 * @param {DOM节点} dom 
 */
export function setProperty(dom,key,value) {
 ...
}

function createComponentInstance(comp,props) {
  let instance = null
  if (comp.prototype.render) {
    //组件的原型对象上有render方法,则是类组件
    //类组件 直接用new生成一个组件实例
    instance = new comp(props)
    
  } else {
    //是函数组件
    instance = new Element(comp)
    instance.constructor = comp
    instance.render = function (props) {
      return comp(props)
    }
  }
  return instance
}
/**
 * 
 * @param {组件实例} instance 
 */
function createDomForComponentInstance(instance) {
  //获取到虚拟DOM并挂载到实例上 因为类组件的render方法中return的就是jsx对象
  //所以直接调用render方法获取获取虚拟DOM
  instance.vdom = instance.render()

  //如果实例上没有挂载过DOM,则是第一次创建
  //之后再发生更新,则不会进入到该判断分支,就不会执行componentDidMount方法
  if (!instance.dom) {
    typeof instance.componentDidMount == 'function' && instance.componentDidMount()
  }

  //生成真实的DOM节点,并且也挂载到实例上
  instance.dom = createDom(instance.vdom)
}

/**
 * 
 * @param {虚拟DOM} vdom 
 * @param {容器} container 
 */
function render(vdom,container) {
  //根据虚拟DOM转换为真实DOM
  const dom = createDom(vdom)
  //将真实DOM添加到容器DOM中
  container.appendChild(dom)
}

const ReactDOM = {
  render
}

export default ReactDOM

5.2 componentDidUpdated

分析该方法,在每次组件更新完成后都会执行。所以我们找到my-react的index.js中,在setState方法的最后添加componentDidUpdated方法。

//my-react/index.js
import { createDom } from '../react-dom/index'
import { diff } from '../react-dom/diff'
import { patch } from '../react-dom/patch'

export class Element {
  constructor(tag,props,children) {
    this.tag = tag
    this.props = props
    this.children = children
  }
}

class Component {
  constructor(props = {}) {
    this.props = props,
    this.state = {}
  }
  /**
   * 
   * @param {传入的需要更新的数据} updatedState 
   */
  setState(updatedState) {
    //合并对象  将新的state合并到旧的state上
    Object.assign(this.state,updatedState)
    //再调用render方法重新生成新的虚拟DOM
    const newVdom = this.render()
    //根据diff算法找出新旧虚拟DOM的区别
    const patches = diff(this.vdom,newVdom)
    //根据不同,更新DOM节点
    patch(this.dom,patches)
    this.vdom = newVdom
    //生命周期函数componentDidUpdated
    typeof this.componentDidUpdated == 'function' && this.componentDidUpdated()
  }
}

function createElement(tag,props,...children) {
  return new Element(tag,props,children)
}

const React = {
  createElement,
  Component
}

export default React
export { Component }

6. 完善setState方法

目前我们写的setState方法已经能完成普通的更新操作。但是还有两个地方需要改进。

首先,用官方的react做一个小demo。

组件的初始数据为: num: 0, score: 100
点击按钮,调用两次setState,分别将num和score加1,并打印this.state

import React, { Component } from 'react'
import ReactDOM from 'react-dom'

class App2 extends Component {
  constructor(props){
    super(props)
    this.state = {
      num: 0,
      score: 100
    }
  }
  handelClick() {
    this.setState({
      num: this.state.num + 1
    })
    this.setState({
      score: this.state.score + 1
    })
    console.log(this.state.num);
  }
  componentDidUpdate() {
    console.log('componentDidUpdate');
  }
  render() {
    return (
      <div>
        <p>{this.state.num}===={this.state.score}</p>
        <button onClick={this.handelClick.bind(this)}>按钮</button>
      </div>
    )
  }
}

ReactDOM.render(<App2/>, document.getElementById('root'))

由下图结果,我们发现了两个问题。

一、在点击事件中,先setState再打印this.state。打印出的是更新之前的旧数据。证明setState方法是异步的。而我们自己写的setState方法是同步的。

二、我们调用了两次setState方法,但是componentDidUpdated方法只执行了一次。证明React将两次更新操作合并处理了,只进行了一次更新,就把我们想要更新的两个数据都成功更新了。而我们自己写的setState则没有做这样的处理。
在这里插入图片描述

6.1 利用任务队列完善setState方法

我们利用任务队列的思想,当组件中调用setState方法时,我们先不直接进行更新操作,而是将要更新的数据和要更新的组件做为一个大的对象,放到一个任务队列中。

当多次调用setState方法,则一直进行入队操作。当进入队列完毕,将所有要更新的数据做一个合并,再统一进行一次更新操作。

首先,修改my-react的index.js中的setState方法,将之前的更新逻辑注掉。

在setState方法中,每被调用一次,我们就调用enqueue方法。

//my-react/index.js
import { createDom } from '../react-dom/index'
import { diff } from '../react-dom/diff'
import { patch } from '../react-dom/patch'
import { enqueue } from './queue'

export class Element {
  ...
}

class Component {
  ...
  /**
   * 
   * @param {传入的需要更新的数据} updatedState 
   */
  setState(updatedState) {
    /*
    //合并对象  将新的state合并到旧的state上
    Object.assign(this.state,updatedState)
    //再调用render方法重新生成新的虚拟DOM
    const newVdom = this.render()
    //根据diff算法找出新旧虚拟DOM的区别
    const patches = diff(this.vdom,newVdom)
    //根据不同,更新DOM节点
    patch(this.dom,patches)
    this.vdom = newVdom
    //生命周期函数componentDidUpdated
    typeof this.componentDidUpdated == 'function' && this.componentDidUpdated()
    */

    //进入更新任务队列
    enqueue(updatedState, this)
  }
  update() {
    //调用render方法重新生成新的虚拟DOM
    const newVdom = this.render()
    //根据diff算法找出新旧虚拟DOM的区别
    const patches = diff(this.vdom,newVdom)
    //根据不同,更新DOM节点
    patch(this.dom,patches)
    this.vdom = newVdom
    //生命周期函数componentDidUpdated
    typeof this.componentDidUpdated == 'function' && this.componentDidUpdated()
  }
  
}

function createElement(tag,props,...children) {
  ...
}

const React = {
  createElement,
  Component
}

export default React
export { Component }

在my-react中新建一个queue.js

//my-react/queue.js

//保存需要更新的数据和组件的对象的队列
const stateQueue = []
//保存需要更新的组件的队列
const compQueue = []

//入列方法
export function enqueue(updatedState, comp) {
  //如果任务队列为0, 进行出列操作
  if (stateQueue.length == 0) {
    //当第一次进入,必定会进入到该判断分支里
    //但是因为是异步,所以不会执行
    //当所有同步执行完成后,也就是所有需要更新的数据都入列了
    //再执行出列操作,并对所有要更新的数据做一个合并

    //异步的调用出列函数
    setTimeout(flush,0)
  }
  stateQueue.push({
    updatedState,
    comp
  })
  //判断组件队列中是否已经有该组件
  const hasComp = compQueue.some(item => item == comp)
  //如果组件队列中没有,才push进去
  if (!hasComp) {
    compQueue.push(comp)
  }
}

//出列函数
function flush() {
  let item, comp
  //循环出列 并合并对象
  while (item = stateQueue.shift()) {
    const { updatedState, comp } = item
    //合并对象 将所有需要更新的数据都合并到组件实例的state属性上
    Object.assign(comp.state, updatedState)
  }
  //拿到需要更新的组件
  while (comp = compQueue.shift()) {
    //调用组件自身的update方法,更新数据及虚拟DOM
    comp.update()
  }
}

总结

至此,已经完成了react框架大部分基本的功能。

总结一下,首先,我们在reactDOM.render方法中,根据传入的不同的类型,生成不同的DOM节点,并对传入的属性做递归处理,挂载在容器DOM中,渲染不同的结果。重要的就是对jsx对象的渲染,在这里用到了babel,因为babel能解析jsx语法,通过调用createElement方法生成虚拟DOM对象。

由此,我们我们才能渲染组件。对组件我们又分别对函数组件和类组件做了不同的处理。最后都是将生成的虚拟DOM和真实DOM挂载到组件的实例上,以便于后期diff算法进行计算更新。

然后我们对更新方法进行了处理。利用到了diff算法进行虚拟DOM的比对,最后只更新需要更新的部分。并分别在组件挂载完毕后和组件更新完毕后添加了componentDidMount和componentDidUpdated这两个生命周期函数。

最后完善了setState方法。我们利用任务队列的思想,将每次需要更新的数据放到任务队列中,之后再进行对象合并,将所有需要更新的数据,合并到组件实例的state中,做一个统一的更新操作。这样,同时多次调用setState时,只需进行一次更新操作,就能把所有要更新的数据全部更新。在进行出列操作时,利用定时器setTimeout,来将setState方法变成了异步方法。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 游动-白 设计师:上身试试 返回首页