【React】react源碼梳理筆記(三)

前言

  • 本篇來了解setState之謎。
  • react最常見的面試題就是setState到底是同步還是異步?看完這篇就知道了。

渲染類組件和函數組件

  • 接上篇,上篇只是基本把事件給做了,下面需要渲染類組件和函數組件。
  • 現在一共有3種形式的元素:
    單獨的react元素
    類組件
    函數組件
  • 而單獨的react元素前面已經是可以渲染出來了。在babel轉譯前表現形式就是<div>xxx</div>這樣。
  • 爲了便於理解,這裏使用轉譯後的形式。
class ClassComponent extends React.Component{
  render(){
    return React.createElement('div',{id:'counter'},'hello')
  }
}
function FunctionCounter(){
  return React.createElement('div',{id:'counter'},'hello')
}
let element1 = React.createElement('div',{id:'counter'},'hello')
let element2 = React.createElement(ClassComponent,{id:'counter'},'hello')
let element3 = React.createElement(FunctionCounter,{id:'counter'},'hello')
  • 下面渲染element2和element3。
  • 類組件函數在第一篇已經寫了,我還畫了個圖,其中有個setState會調用updater的方法。它有2個方法,一個是setState,一個是forceUpdate。都是調用的updater上的方法。
  • 但虛擬dom上創建就有點不一樣了。沒用fiber前還是得在ReactElement裏判斷。先做出幾種類型:
export const REACT_ELEMENT_TYPE = Symbol.for('react.element')
export const REACT_TEXT_TYPE =Symbol.for('TEXT');
export const FUNCTION_COMPONENT=Symbol.for('FUNCTION_COMPONENT')
export const CLASS_COMPONENT=Symbol.for('CLASS_COMPONENT')
  • 前面ReactElement裏是全都加的是React_ELEMENT_TYPE類型。這次做個判斷。
const ReactElement = function(type, key, ref, owner,props) {
    let $$typeof
    if(typeof type==='function'&&type.prototype.isReactComponent){
      $$typeof = CLASS_COMPONENT
    }else if(typeof type==='function'){
      $$typeof =FUNCTION_COMPONENT
    }else{
      $$typeof = REACT_ELEMENT_TYPE
    }
    const element = {
      // 通過symbol創建標識,沒有symbol給個數字
      $$typeof,
    //剩餘屬性附上
      type: type,
      key: key,
      ref: ref,
      _owner: owner,
      props: props,
    };
    return element;
};
  • 然後需要改創建真實dom的方法:
function createFunctionDOM(element){
    let {type,props}=element
    let renderElement = type(props)
    let newDom =createDOM(renderElement)
    return newDom
}
function createClassComponetDOM(element){
    let {type,props}=element
    let componentInstance =new  type(props)
    let renderElement = componentInstance.render()
    let newDom =createDOM(renderElement)
    return newDom
}
export  function createDOM(element){
    let {$$typeof}=element
    let dom =null
    if( !$$typeof ){//字符串 
        dom = document.createTextNode(element)
    }else if($$typeof === REACT_ELEMENT_TYPE){
        dom = createNativeDOM(element)
    }else if($$typeof === FUNCTION_COMPONENT){
        dom = createFunctionDOM(element)
    }else if($$typeof === CLASS_COMPONENT){
        dom = createClassComponetDOM(element)
    }
    return dom 
}
  • 可以看見函數組件直接取返回值,拿返回值調createDom,類組件new出一個實例,然後調用render拿返回值,再傳給createDom。
  • 這樣就完成了渲染函數組件和類組件。

實現setState

  • 一般setState說的是類組件那個,函數組件那個是用hooks另外說。
  • 看一下原版使用:
import React from 'react';
import ReactDOM from 'react-dom';
class Counter extends React.Component{
  constructor(props){
    super(props)
    this.state={number:0}
  }
  handleClick=()=>{
    this.setState({number:this.state.number+1})
    console.log(this.state.number)
    this.setState({number:this.state.number+1})
    console.log(this.state.number)
    setTimeout(() => {
      this.setState({number:this.state.number+1})
      console.log(this.state.number)
      this.setState({number:this.state.number+1})
      console.log(this.state.number)
    });
  }
  render(){
    return <button onClick={this.handleClick}>+</button>
  }
}
ReactDOM.render(
  <Counter></Counter>,
  document.getElementById('root')
);
  • 這樣點擊一下按鈕會打印0023。其實主要是react裏面有個批量更新的玩意。會在事件流程裏開啓批量更新,然後在事件對象完成後關閉批量更新。現在來實現下。
  • 在組件中調用setState實際上就是調繼承的component的prototype的setstate方法。前面照源碼抄來的是這樣:
Component.prototype.setState = function(partialState, callback) {
    this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
Component.prototype.forceUpdate = function(callback) {
    this.updater.enqueueForceUpdate(this, callback, 'forceUpdate');
};
  • 所以這個調用的是this.updater,但是源碼裏Component的updater是傳來的,所以先改成自己做的。同時將方法也改簡略點。
export function Component(props, context) {
    this.props = props;
    this.context = context;
    this.refs = emptyObject;
    this.updater = new Updater(this) 
}
  
Component.prototype.isReactComponent = {};

Component.prototype.setState = function(partialState) {
    this.updater.enqueueSetState(partialState);
};
Component.prototype.forceUpdate = function() {
    console.log('forceupdate')
};
  • 這裏就把updater改成new出來,然後把實例傳進去。一個實例即對應一個updater。
  • 下面是updater,以及一個全局的updateQueue。
export let updateQueue={
    updaters:[],
    ispending:false,//true批量更新模式
    add(updater){
        this.updaters.push(updater)//放數組不更新
    },
    batchUpdate(){//只有有人調用此方法才更新
        let {updaters}=this
        this.ispending =true
        let updater = updaters.pop()
        while (updater) {
            updater.updeteComponent();
            updater = updaters.pop()
        }
        this.ispending=false
    }
}
function isFunction(obj){
    return typeof obj === 'function'
}
class Updater{
    constructor(componentInstance){
        this.componentInstance =componentInstance
        this.penddingState = []//如果是批量更新模式,需要存數組裏一起更新
        this.nextProps=null
    }
    enqueueSetState(partialState){
        this.penddingState.push(partialState)//存進數組
        this.emitUpdate()//進行判斷
    }
    emitUpdate(nextProps){
        this.nextProps=nextProps//有新狀態
        if(nextProps||!updateQueue.ispending){//如果是非批量更新模式
            this.updeteComponent()//直接更新
        }else{
            updateQueue.add(this)//添加進批量更新隊列
        }
    }
    updeteComponent(){
        let {componentInstance,penddingState,nextProps}=this
        if(nextProps||penddingState.length>0){//判斷有新屬性或者更新隊列裏有狀態
            shouldUpdate(componentInstance,nextProps,this.getState())
        }
    }
    getState(){//獲取新state
        let {componentInstance,penddingState}=this
        let {state}=componentInstance
        if(penddingState.length>0){//需要更新的state依次拿出來進行合併
            penddingState.forEach(nextState => {//nextstate就是setState裏內容
                if(isFunction(nextState)){//如果是函數
                    state=nextState.call(componentInstance,state)//得到最新state
                }else{
                    state={...state,...nextState}//得到新state
                }
            });
        }
        penddingState.length=0//最後讓數組置0
        return state
    }
}

function shouldUpdate(componentInstance,nextProps,nextState){//nextState就是最新state
    componentInstance.props =nextProps
    componentInstance.state = nextState//讓其有新屬性
    if(componentInstance.shouldComponentUpdate&&//生命週期裏那個存在並且給了false 不渲染
        !componentInstance.shouldComponentUpdate(nextProps,nextState)){
            return false
    }
    componentInstance.forceUpdate()
}
  • 簡單說是這樣,有個全局的一個對象裏面有個隊列,以及一個代表這個對象狀態的標誌ispending。它有個add方法就是往隊列里加Updater,有個批量更新方法就是把隊列裏Updater拿出來執行Updater的立即更新方法。
  • 而Updater,它有個隊列,這個隊列是存新狀態的,當有新狀態,第一件事就是存到Updater這個隊列裏。然後再進行一個判斷,是放到queue裏進行批量更新還是直接進行更新?
  • 放到queue裏的就會等待某地方調用queue的batchUpdate方法進行批量更新。而直接進行更新就直接自己進行調用更新。
  • 在更新方法裏,通過getState拿到最新的狀態,傳遞給shouldUpdate配合其生命週期控制渲染。如果需要渲染,就走forceUpdate這個方法。這時,真正的操作dom纔會來。
  • 爲了後面方便進行domdiff(react在fiber前是domdiff,fiber沒有domdiff),需要前面創建虛擬dom稍微修改一下,讓字符串也包裹成一個虛擬dom。這樣便於方便比較。同時將真實dom也掛載到虛擬dom上。(這段準備操作很像vue的domdiff)。
    if (childrenLength === 1) {//一個孩子就給children / /只有字符串
        if(typeof children === 'string')children ={$$typeof:REACT_TEXT_TYPE,key:null,type:children,ref:null,props:null}
        props.children = children;
    } else if (childrenLength > 1) {
        const childArray = Array(childrenLength);
        for (let i = 0; i < childrenLength; i++) {
            if(typeof arguments[i + 2] === 'string')arguments[i + 2]= {$$typeof:REACT_TEXT_TYPE,key:null,type:children,ref:null,props:null}
            childArray[i] = arguments[i + 2];
        }
        props.children = childArray;//多個就變成數組
    }
  • 字符串在遍歷時候就可以發現,直接包裹成文本類型的虛擬dom。
  • 然後修改創建真實dom那:
function createClassComponetDOM(element){
    let {type,props}=element
    let componentInstance =new  type(props)
    let renderElement = componentInstance.render()
    componentInstance.renderElement=renderElement //實例上可以拿到虛擬dom
    let newDom =createDOM(renderElement)
    return newDom
}
export  function createDOM(element){
    let {$$typeof}=element
    let dom =null
    if($$typeof === REACT_TEXT_TYPE ){//字符串 
        dom = document.createTextNode(element.type)
    }else if($$typeof === REACT_ELEMENT_TYPE){
        dom = createNativeDOM(element)
    }else if($$typeof === FUNCTION_COMPONENT){
        dom = createFunctionDOM(element)
    }else if($$typeof === CLASS_COMPONENT){
        dom = createClassComponetDOM(element)
    }
    element.dom = dom //虛擬dom上可以拿到真實Dom
    return dom 
}
  • 另外在事件發生時,我們需要開啓批量更新,結束時關閉批量更新並調用queue的批量更新:
function dispatchEvent(event){
    let {type,target}=event 
    let eventType ='on'+type
    syntheticEvent = getSyntheticEvent(event)
    updateQueue.ispending=true//進入批量更新模式
    while (target) {
        let {eventStore}=target//dom上有打下的標記
        let listener = eventStore&&eventStore[eventType]
        if(listener){
            listener.call(target,syntheticEvent)
        }
        target=target.parentNode//
    }
    for(let key in syntheticEvent){
       if(key!=='persist')syntheticEvent[key]=null
    }
    updateQueue.ispending=false//執行完false
    updateQueue.batchUpdate()//批量更新
}
  • 這樣就完成了,可以打印試一下,跟原版一模一樣,都是0023。
  • 所以說,在點擊按鈕時,其實是開啓了批量更新模式,因爲事件對象先進dispatchEvent函數,然後再運行用戶的setState方法,這樣用戶的狀態會放進updater隊列並存儲到queue裏,等待批量更新完成後再將其關閉,這個過程是個while循環,如果說同步還是異步?這裏有2種情況,一種批量更新情況,應該算是同步,因爲整個流程是一個同步過程,但是你後面console.log取不到。相當於這樣的代碼:
function a(){
}
console.log(a.yname)
a.yname='yehuozhili'
  • 這代碼是同步還是異步?肯定同步啊,但是console.log放前面去了而已。
  • 另一種情況是非批量更新情況,這種情況更是同步的情況。相當於這樣的代碼:
function a(){
}
a.yname='yehuozhili'
console.log(a.yname)
  • 最後把渲染邏輯寫一下,剩下的下篇說。
  • 可以先在button上加個id等於this.state.number來觀察渲染情況。
  • 前面componentInstance.forceUpdate就調用了渲染,完成這個邏輯:
Component.prototype.forceUpdate = function() {
   let {renderElement}=this//拿到虛擬dom
   if(this.componentWillUpdate){
       this.componenentWillUpdate()
   }
   let newRenderElement =this.render()//拿到新狀態下的組件虛擬dom結果
   let currentElement =compareTwoElement(renderElement,newRenderElement)//比較新老虛擬dom
   this.renderElement = currentElement
   if(this.componentDidUpdate){
       this.componentDidUpdate()
   }
};
function compareTwoElement(oldelement,newelement){
    let currentDom = oldelement.dom 
    let currentElement = oldelement
    if(newelement===null){//空節點直接換
        currentDom.parentNode.removeChild(currentDom)
        currentDom= null
        currentElement=null
    }else if(oldelement.type!== newelement.type){//新舊類型不一樣
        let newDom = createDOM(newelement)
        currentDom.parentNode.replaceChild(newDom,currentDom)
        currentElement=newelement //把當前虛擬dom換成新的
    }else{ //暫時先這麼寫,這裏要domdiff
        let newDom = createDOM(newelement)
        currentDom.parentNode.replaceChild(newDom,currentDom)
        currentElement=newelement
    }
    return currentElement
}
  • 其中通過組件實例拿到實例上掛載的虛擬Dom,進入compare函數去比較新老虛擬dom。而虛擬dom上的dom屬性正好掛載了真實Dom,所以也可以操作dom。
  • 這個新的虛擬dom,其實是執行了實例render的結果。所以更新會走一次render。
  • 最後那個else,先這麼寫,下次再寫domdiff。
  • 其實這個有點對應VUE的patch,不過patch是邊比對邊patch。
function  patch(oldVnode,newVnode) {
    //如果類型不一樣,直接替換
    if(newVnode.type!==oldVnode.type){
        return oldVnode.domElement.parentNode.replaceChild(creatRealDom(newVnode),oldVnode.domElement)
    }
    //節點類型一樣,文本賦值,第三個屬性不是兒子就是文本,是文本返回
    if(newVnode.text!==undefined){
        return oldVnode.domElement.textContent=newVnode.text
    }
    //節點類型一樣,更新屬性
    let domElement = newVnode.domElement = oldVnode.domElement//先讓newvnode上能操作dom
    updateAttr(newVnode,oldVnode.props)
    //節點類型一樣,查找兒子
    let oldChildren = oldVnode.children
    let newChildren = newVnode.children
    //分三種情況考慮
    //老的有兒子,新的沒兒子,刪除老的
    if(oldChildren.length>0 && newChildren.length>0){
        updateChildren(domElement,newChildren,oldChildren)
    }else if(oldChildren.length>0){//如果不是2個都大於0 那麼就是老的有兒子新的沒兒子
        domElement.innerHTML=''//改內部html即可刪除兒子
    }else if(newChildren.length>0){//新的有老的沒有
        for(let i=0;i<newChildren.length;i++){
            domElement.appendChild(creatRealDom(newChildren[i]))
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章