手寫簡易版React框架

@young!發表於2020-11-25

手寫簡易版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方法變成了非同步方法。

相關文章