【React】React源码梳理笔记(十二)

前言

  • 上一篇是初次渲染,本篇是更新逻辑。本篇理解上不一定对,因为这部分相当复杂,我找的不同资料的人理解各不相同,后续有新理解另外发文。
  • 司徒正美fiber理解
  • 司徒正美关于alternate的解释

更新逻辑——元素渲染实现

  • 前面初次渲染时,我们已经得到了一个完整的fiber树,并且树上链接着副作用链表。

  • 而react的更新逻辑,也是走workloop,由于基于idleCallback,这里简化起见,写成了一个类似死循环的模式,相当于每当浏览器idle时执行下函数,但因为是idle时,所以用户不觉得卡。就有点像我们监听了scroll,结果只要一滚动就会不断触发函数一样。当然,里面应该还有若干优化。本篇先排除那些优化,实现简易逻辑。

  • 由于是死循环,所以每次执行时都会根据新的虚拟dom来生成新的fiber树,其中自然也会有复用逻辑之类,新生成的fiber树里同样也会有副作用链表,最后根据副作用链表来commit,渲染出最新的页面。

  • 首先使用元素渲染来实现更新操作:

import React from 'react';
import ReactDOM from 'react-dom';
let style={border:'3px solid red',marigin:'5px'}
let element = (
  <div id="A1" style={style}>
  A1
    <div id="B1" style={style}>
    B1
      <div id="C1" style={style}>C1</div>
      <div id="C2" style={style}>C2</div>
    </div>
    <div id="B2" style={style}>B2</div>
  </div>
)
ReactDOM.render(
  element,
  document.getElementById('root')
);
let render2=document.getElementById('render2')
render2.addEventListener('click',()=>{
  let element2=(
    <div id="A12" style={style}>
  A1
    <div id="B12" style={style}>
    B1
      <div id="C12" style={style}>C12</div>
      <div id="C22" style={style}>C22</div>
    </div>
    <div id="B22" style={style}>B22</div>
    <div id="B33" style={style}>B33</div>
  </div>
  )
  ReactDOM.render(
    element2,
    document.getElementById('root')
  );
})
let render3=document.getElementById('render3')
render3.addEventListener('click',()=>{
  let element3=(
    <div id="A12" style={style}>
  A3
    <div id="B12" style={style}>
    B3      <div id="C12" style={style}>33</div>
      <div id="C22" style={style}>C22</div>
    </div>
    
  </div>
  )
  ReactDOM.render(
    element3,
    document.getElementById('root')
  );
})
  • 页面上加入2个button,id如上,按下render2或者render3,渲染页面有增有减。
  • 当触发更新时,同样走的scheduleRoot,但是上次的渲染的结尾,我们将workInprogressRoot赋给了currentRoot,所以,如果进入这个函数,并且currentRoot有值,那么就是更新逻辑。
  • 首先传入的肯定是个根节点,然后让其alternate指向前面已经生成的那个完整的fiber树。
let nextUnitOfWork=null
let workInProgressRoot=null
let currentRoot=null
let deletions=[]//删除不在effectlist里
export function scheduleRoot(rootfiber){
    if(currentRoot){//如果有值,说明时是更新逻辑
        rootfiber.alternate=currentRoot
    }
    workInProgressRoot=rootfiber//work是根
    nextUnitOfWork=workInProgressRoot//相当于每个工作单元,workloop里面会不断变化
}
  • 然后就有意思了,后面不是会对新的虚拟dom制作fiber树嘛,在每次遍历孩子节点时候,如果相同位置节点类型一样,将新的子节点的alternate指向老的fiber树对应的节点。这样新生成的一颗树每个节点的alternate会指向对应的老的节点。当然会有增删改之类导致2棵树不完全一样,能对上就对上,对不上则直接是新的,没有alternate。
  • 另外删除节点需要挑出来在commit时最先处理。
function reconcilieChildren(currentFiber,newChildren){
    let newChildIndex=0
    let oldFiber = currentFiber.alternate&&currentFiber.alternate.child//这个指的是老的子节点
    let prevSibiling
    while(newChildIndex<newChildren.length||oldFiber){//while遍历孩子构建子节点 这里或者代表把新老节点取最大值(可能新的删了),当然oldfiber有的话只有1个
        let newChild = newChildren[newChildIndex]
        const sameType=oldFiber&&newChild&&oldFiber.type===newChild.type//新老节点一样
        let newFiber
        let tag
        if(newChild&&newChild.type===ELEMENT_TEXT){
            tag=TAG_TEXT//字符串单独处理
        }else if(newChild&&typeof newChild.type==='string'){
            tag=TAG_HOST//原生节点
        }
        if(sameType){
            newFiber={//复用
                tag:oldFiber.tag,
                type:oldFiber.type,
                props:newChild.props,
                stateNode:oldFiber.stateNode,
                return:currentFiber,
                alternate: oldFiber,//让新的fiber的alternate指向老的fiber节点
                effectTag:UPDATE,//更新操作
                nextEffect:null,//链表指向
            }
        }else{
            if(newChild){//不同且有新孩子,创建它
                newFiber={
                    tag,
                    type:newChild.type,
                    props:newChild.props,
                    stateNode:null,
                    return:currentFiber,
                    effectTag:PLACEMENT,//增加操作
                    nextEffect:null,//链表指向
                }
            }
            if(oldFiber){//有新fiber了就要删除老的
                oldFiber.effectTag=DELETION
                deletions.push(oldFiber)
            }
        }
        if(oldFiber){
            oldFiber=oldFiber.sibling//因为newchild是虚拟dom,是个数组,老节点是fiber树,所以往后移动时需要走兄弟
        }
        if(newFiber){
            if(newChildIndex===0){//第一个儿子
                currentFiber.child=newFiber
            }else{
                prevSibiling.sibling=newFiber
            }
            prevSibiling=newFiber
        }
        newChildIndex++
    }
}
  • 创建完毕后同样生成副作用链表:
function performUnitOfWork(currentFiber){   //这个就是前面deep函数样例的逻辑
    beginWork(currentFiber)//转化为fiber
    if(currentFiber.child){
        return currentFiber.child
    }
    while(currentFiber){
        completeUnitOfWork(currentFiber)
        if(currentFiber.sibling){
            return currentFiber.sibling
        }
        currentFiber=currentFiber.return
    }
}
  • 生成完毕后,通过workloop进入commit:
function workLoop(deadline){
    let shouldYield =false
    while(nextUnitOfWork&&!shouldYield){
        nextUnitOfWork=performUnitOfWork(nextUnitOfWork)
        shouldYield=deadline.timeRemaining()<=0
    }
    if (!nextUnitOfWork && workInProgressRoot) {//如果时间片到期后还有任务没有完成,就需要请求浏览器再次调度
        console.log('over');
        commitRoot();
    }
    requestIdleCallback(workLoop, { timeout: 500 });
}

function commitRoot(){
    deletions.forEach(commitWork)
    let currentFiber
    if(workInProgressRoot){
        currentFiber=workInProgressRoot.firstEffect
    }
    while(currentFiber){
        commitWork(currentFiber)
        currentFiber=currentFiber.nextEffect //单链表,一个个effect做
    }
    deletions.length=0//更新完清空
    currentRoot=workInProgressRoot//渲染完成的给currentroot
    workInProgressRoot=null
}
function commitWork(currentFiber){
    if(!currentFiber)return 
    let returnFiber=currentFiber.return
    let returnDom=returnFiber.stateNode//拿到真实dom
    if(currentFiber.effectTag===PLACEMENT){
        returnDom.appendChild(currentFiber.stateNode)
    }else if(currentFiber.effectTag===DELETION){
        returnDom.removeChild(currentFiber.stateNode)
    }else if(currentFiber.effectTag===UPDATE){
        if(currentFiber.type===ELEMENT_TEXT){
            if(currentFiber.alternate.props.text!==currentFiber.props.text){//上一个不等于本次
                currentFiber.stateNode.textContent=currentFiber.props.text
            }
        }else{//文本进行比对,否则更新
            updateDOM(currentFiber.stateNode,currentFiber.alternate.props,currentFiber.props)
        }
    }
    currentFiber.effectTag=null
}
  • commit里增加了更新逻辑,通过副作用链上的标志判断是更新,然后根据传来的fiber,进行相应的增删改。
  • 这样整个效果就完成了。

更新逻辑——支持类组件

  • 前面是元素渲染,下面支持类组件更新。
  • 这里类组件以及更新模式就简易写了,详细了解的请看第三篇。
  • 同样做个updateQueue,这玩意相当于上次写的updater不过简化了,纯粹就是一个加链表,一个执行清空链表操作。
export class UpdateQueue {
    constructor() {
        this.firstUpdate = null;
        this.lastUpdate = null;
    }
    enqueueUpdate(update) {
        if (this.lastUpdate === null) {
            this.firstUpdate = this.lastUpdate = update;
        } else {
            this.lastUpdate.nextUpdate = update;
            this.lastUpdate = update;
        }
    }
    forceUpdate(state) {
        let currentUpdate = this.firstUpdate;
        while (currentUpdate) {
            let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(state) : currentUpdate.payload;
            state = { ...state, ...nextState };
            currentUpdate = currentUpdate.nextUpdate;
        }
        this.firstUpdate = this.lastUpdate = null;
        return state;
    }
}
class Component {
    constructor(props) {
        this.props = props;
    }
    setState(payload) {
        let update = new Update(payload);
        this.internalFiber.updateQueue.enqueueUpdate(update);//更新的放链表
        scheduleRoot();//从根节点开始调度
    }
}
Component.prototype.isReactComponent = {};
  • 修改调度,使其rootfiber可以传或者不传,不传就拿当前树。
export function scheduleRoot(rootfiber){
    if(currentRoot){//如果有值,说明时是第一次更新逻辑
        if(rootfiber){
            rootfiber.alternate=currentRoot
            workInProgressRoot=rootfiber//work是根
        }else{
            workInProgressRoot = {
                ...currentRoot,
                alternate: currentRoot
            }
        }
        
    }else{//初次
        workInProgressRoot=rootfiber
    }
    workInProgressRoot.firstEffect = workInProgressRoot.lastEffect = workInProgressRoot.nextEffect = null;
    nextUnitOfWork=workInProgressRoot//相当于每个工作单元,workloop里面会不断变化
}
  • 修改beginwork逻辑,加一个类组件处理:
function beginWork(currentFiber){//如果原生dom,需要创建真实dom元素。current会变
    if(currentFiber.tag===TAG_ROOT){//根不需要建真实dom
        updateHostRoot(currentFiber)
    }else if(currentFiber.tag===TAG_TEXT){
        updateHostText(currentFiber)
    }else if(currentFiber.tag===TAG_HOST){
        updateHost(currentFiber)
    }else if(currentFiber.tag===TAG_CLASS){
        updateClassComponent(currentFiber)
    }
}

function updateClassComponent(currentFiber){
    if(!currentFiber.stateNode){
        currentFiber.stateNode=new currentFiber.type(currentFiber.props)
        currentFiber.stateNode.internalFiber=currentFiber
        currentFiber.updateQueue=new UpdateQueue()
    }
    currentFiber.stateNode.state = currentFiber.updateQueue
    .forceUpdate(currentFiber.stateNode.state);//这个传入的是老状态,链表里那个是最新的
    let newElement = currentFiber.stateNode.render();
    const newChildren = [newElement];
    reconcilieChildren(currentFiber, newChildren);
}
  • 调度部分加上判断,如果碰到类组件,打上class的tag,每个新fiber都搞个更新链
function reconcilieChildren(currentFiber,newChildren){
    let newChildIndex=0
    let oldFiber = currentFiber.alternate&&currentFiber.alternate.child//这个指的是老的子节点
    let prevSibiling
    while(newChildIndex<newChildren.length||oldFiber){//while遍历孩子构建子节点 这里或者代表把新老节点取最大值(可能新的删了),当然oldfiber有的话只有1个
        let newChild = newChildren[newChildIndex]
        const sameType=oldFiber&&newChild&&oldFiber.type===newChild.type//新老节点一样
        let newFiber
        let tag
        if(newChild && typeof newChild.type==='function' &&newChild.type.prototype.isReactComponent){
            tag =TAG_CLASS
        }else if(newChild&&newChild.type===ELEMENT_TEXT){
            tag=TAG_TEXT//字符串单独处理
        }else if(newChild&&typeof newChild.type==='string'){
            tag=TAG_HOST//原生节点
        }      
        if(sameType){//复用逻辑
                newFiber={//复用上一次的
                    tag:oldFiber.tag,
                    type:oldFiber.type,
                    props:newChild.props,
                    stateNode:oldFiber.stateNode,
                    return:currentFiber,
                    alternate: oldFiber,//让新的fiber的alternate指向老的fiber节点
                    effectTag:UPDATE,//更新操作
                    nextEffect:null,//链表指向
                    updateQueue:oldFiber.updateQueue||new UpdateQueue()
                }
        }else{
            if(newChild){//不同且有新孩子,创建它
                newFiber={
                    tag,
                    type:newChild.type,
                    props:newChild.props,
                    stateNode:null,
                    return:currentFiber,
                    effectTag:PLACEMENT,//增加操作
                    nextEffect:null,//链表指向
                    updateQueue:new UpdateQueue()
                }
            }
            if(oldFiber){//有新fiber了就要删除老的
                oldFiber.effectTag=DELETION
                deletions.push(oldFiber)
            }
        }
        if(oldFiber){
            oldFiber=oldFiber.sibling//因为newchild是虚拟dom,是个数组,老节点是fiber树,所以往后移动时需要走兄弟
        }
        if(newFiber){
            if(newChildIndex===0){//第一个儿子
                currentFiber.child=newFiber
            }else{
                prevSibiling.sibling=newFiber
            }
            prevSibiling=newFiber
        }
        newChildIndex++
    }
}
  • commit处进行修改,因为stateNode挂的是实例,所以挂载点在实例的父节点上。删除同理。
function commitWork(currentFiber){
    if(!currentFiber)return 
    let returnFiber=currentFiber.return
    while (returnFiber.tag !== TAG_HOST &&
        returnFiber.tag !== TAG_ROOT &&
        returnFiber.tag !== TAG_TEXT) {
        returnFiber = returnFiber.return;
    }
    let returnDom=returnFiber.stateNode//拿到真实dom
    if(currentFiber.effectTag===PLACEMENT){
        let nextFiber = currentFiber;
        // 如果要挂载的节点不是DOM节点,比如说是类组件Fiber,一直找第一个儿子,直到找到一个真实DOM节点为止
        while (nextFiber.tag !== TAG_HOST && nextFiber.tag !== TAG_TEXT) {
            nextFiber = currentFiber.child;
        }
        returnDom.appendChild(nextFiber.stateNode);
    }else if(currentFiber.effectTag===DELETION){
        commitDeletion(currentFiber,returnDom)
        //returnDom.removeChild(currentFiber.stateNode)
    }else if(currentFiber.effectTag===UPDATE){
        if(currentFiber.type===ELEMENT_TEXT){
            if(currentFiber.alternate.props.text!==currentFiber.props.text){//上一个不等于本次
                currentFiber.stateNode.textContent=currentFiber.props.text
            }
        }else{//文本进行比对,否则更新
            updateDOM(currentFiber.stateNode,currentFiber.alternate.props,currentFiber.props)
        }
    }
    currentFiber.effectTag=null
}
function commitDeletion(currentFiber, domReturn) {
    if (currentFiber.tag == TAG_HOST || currentFiber.tag == TAG_TEXT) {
        domReturn.removeChild(currentFiber.stateNode);
    } else {
        commitDeletion(currentFiber.child, domReturn)
    }
}
  • 这样就完成了。简单说就是类组件setState就是走调度方法同时调了个更新链表进行更新。每个实例都有自己的更新链。当调用时就把更新链清空。新的状态和样式重新过一遍调度和提交渲染到页面上。
  • 剩下的下次写。

你可能感兴趣的:(React)