【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]))
        }
    }
}
发布了178 篇原创文章 · 获赞 11 · 访问量 4万+

猜你喜欢

转载自blog.csdn.net/yehuozhili/article/details/105486459