【React 16】模拟实现4

React | 2020-06-15 23:17:03 583次 2次

上一篇实现了初始渲染,并构建出一颗 fiber 树和一条 effectList 链表,本篇实现数据的更新流程和双缓冲机制的应用。涉及的操作有更新、插入、删除节点,在 performUnitOfWork 中执行 beginWork,进入 reconcileChildren 中进行单个 fiber 节点进行比较判断操作的类型,为 fiber 打上一个 effectTag 标记,这样到了 completeUnitOfWork 阶段创建的 effectList 链表中就把所有的节点串起来并且记录上节点的变化、删除等。

在第一次更新整个过程中每一个 fiber 节点都会被创建一个 alternate 指向旧的 fiber 节点,第二次更新时如果节点类型相同会复用上一次更新后的 fiber alternate,并将此时的节点 alternate 指向上一次更新后的节点,这样做的目的就是一直相互复用对象,大量节点的情况下避免了垃圾回收机制不断触发,节省一定的 cpu 开销。

微信截图_20200615214248.png

实现更新的操作:

import React from './react';
import ReactDOM from './react-dom';
let style = { border: '3px solid red', margin: '5px' };
let element = (
  <div id="A1" style={style}>
    <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')
);

//更新操作 2s后更新一次
let element2 = (
  <div id="A1-2" style={style}>
    A3-2
    <div id="B1-2" style={style}>
      B1-2
      <div id="C1-2" style={style}>C1-2</div>
      <div id="C2-2" style={style}>C2-2</div>
    </div>
    <div id="B2-2" style={style}>B2-2</div>
  </div>
)
setTimeout(() => {
  ReactDOM.render(
    element2,
    document.getElementById('root')
  );
}, 2000)

//更新操作 4s后再更新一次
let element3 = (
  <div id="A1-3" style={style}>
    A3-3
    <div id="B1-3" style={style}>
      B1-3
      <div id="C1-3" style={style}>C1-3</div>
      <div id="C2-3" style={style}>C2-3</div>
    </div>
    <div id="B2-3" style={style}>B2-3</div>
  </div>
)

setTimeout(() => {
  ReactDOM.render(
    element3,
    document.getElementById('root')
  );
}, 6000)

依据上一篇文章中的代码继续补充:

import { 
    TAG_ROOT, ELEMENT_TEXT, TAG_TEXT, TAG_HOST, PLACEMENT, DELETION, UPDATE 
} from "./constants";
import { setProps } from './utils';

let nextUnitOfWork = null;//下一个工作单元
let workInProgressRoot = null;//正在渲染的根ROOT fiber
let currentRoot = null;//渲染成功之后当前根ROOTFiber
let deletions = [];//删除的节点我们并不放在effect list里,所以需要单独记录并执行
export function scheduleRoot(rootFiber) {
    if (currentRoot && currentRoot.alternate) {//第二次之后的更新
        workInProgressRoot = currentRoot.alternate;//第一次渲染出来的那个fiber tree
        workInProgressRoot.alternate = currentRoot;//让这个树的替身指向的当前的currentRoot
        if (rootFiber) workInProgressRoot.props = rootFiber.props;//让它的props更新成新的props
    } else if (currentRoot) {//说明至少已经渲染过一次了 第一次更新
        if (rootFiber) {
            rootFiber.alternate = currentRoot;
            workInProgressRoot = rootFiber;
        } else {
            workInProgressRoot = {
                ...currentRoot,
                alternate: currentRoot
            }
        }
    } else {//如果说是第一次渲染
        workInProgressRoot = rootFiber;
    }
    workInProgressRoot.firstEffect = 
        workInProgressRoot.lastEffect = 
        workInProgressRoot.nextEffect = null;
    nextUnitOfWork = workInProgressRoot;
}
function performUnitOfWork(currentFiber) {
    beginWork(currentFiber);//开
    if (currentFiber.child) {
        return currentFiber.child;
    }
    while (currentFiber) {
        completeUnitOfWork(currentFiber);//没有儿子让自己完成
        if (currentFiber.sibling) {//看有没有弟弟
            return currentFiber.sibling;//有弟弟返回弟弟
        }
        currentFiber = currentFiber.return;//找父亲然后让父亲完成
    }
}

function completeUnitOfWork(currentFiber) {//第一个完成的A1(TEXT)
    let returnFiber = currentFiber.return;//A1
    if (returnFiber) {
        ////这一段是把自己儿子的effect 链挂到父亲身上
        if (!returnFiber.firstEffect) {
            returnFiber.firstEffect = currentFiber.firstEffect;
        }
        if (currentFiber.lastEffect) {
            if (returnFiber.lastEffect) {
                returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
            }
            returnFiber.lastEffect = currentFiber.lastEffect;
        }
        //把自己挂到父亲 身上
        const effectTag = currentFiber.effectTag;
        if (effectTag) {// 自己有副作用 A1 first last=A1(Text)
            if (returnFiber.lastEffect) {
                returnFiber.lastEffect.nextEffect = currentFiber;
            } else {
                returnFiber.firstEffect = currentFiber;
            }
            returnFiber.lastEffect = currentFiber;
        }
    }
}
/**
 * beginWork开始
 * completeUnitOfWork完成单元
 * 1.创建真实DOM元素
 * 2.创建子fiber  
 */
function beginWork(currentFiber) {
    if (currentFiber.tag === TAG_ROOT) {//根fiber
        updateHostRoot(currentFiber);
    } else if (currentFiber.tag === TAG_TEXT) {//文本fiber
        updateHostText(currentFiber);
    } else if (currentFiber.tag === TAG_HOST) {//原生DOM节点 stateNode dom
        updateHost(currentFiber);
    }
}

function updateHost(currentFiber) {
    if (!currentFiber.stateNode) {//如果此fiber没有创建DOM节点
        currentFiber.stateNode = createDOM(currentFiber);
    }
    const newChildren = currentFiber.props.children;
    reconcileChildren(currentFiber, newChildren);
}
function createDOM(currentFiber) {
    if (currentFiber.tag === TAG_TEXT) {
        return document.createTextNode(currentFiber.props.text);
    } else if (currentFiber.tag === TAG_HOST) {// span div
        let stateNode = document.createElement(currentFiber.type);//div
        updateDOM(stateNode, {}, currentFiber.props);
        return stateNode;
    }
}
function updateDOM(stateNode, oldProps, newProps) {
    if (stateNode && stateNode.setAttribute)
        setProps(stateNode, oldProps, newProps);
}
function updateHostText(currentFiber) {
    if (!currentFiber.stateNode) {//如果此fiber没有创建DOM节点
        currentFiber.stateNode = createDOM(currentFiber);
    }
}
function updateHostRoot(currentFiber) {
    //先处理自己 如果是一个原生节点,创建真实DOM 2.创建子fiber 
    let newChildren = currentFiber.props.children;//[element=<div id="A1"]
    reconcileChildren(currentFiber, newChildren);
}
//newChildren是一个虚拟DOM的数组 把虚拟DOM转成Fiber节点
function reconcileChildren(currentFiber, newChildren) {//[A1]
    let newChildIndex = 0;//新子节点的索引
    //如果说currentFiber有alternate并且alternate有child属性
    let oldFiber = currentFiber.alternate && currentFiber.alternate.child;
    if (oldFiber) oldFiber.firstEffect = oldFiber.lastEffect = oldFiber.nextEffect = null;
    let prevSibling;//上一个新的子fiber
    //遍历我们的子虚拟DOM元素数组,为每个虚拟DOM元素创建子Fiber
    while (newChildIndex < newChildren.length || oldFiber) {
        let newChild = newChildren[newChildIndex];//取出虚拟DOM节点[A1]{type:'A1'}
        let newFiber;//新的Fiber
        const sameType = oldFiber && newChild && oldFiber.type === newChild.type;
        let tag;
        if (newChild && newChild.type == ELEMENT_TEXT) {
            tag = TAG_TEXT;//这是一个文本节点
        } else if (newChild && typeof newChild.type === 'string') {
            tag = TAG_HOST;//如果是type是字符串,那么这是一个原生DOM节点 "A1" div
        }//beginWork创建fiber 在completeUnitOfWork的时候收集effect
        if (sameType) {//说明老fiber和新虚拟DOM类型一样,可以复用老的DOM节点,更新即可
            if (oldFiber.alternate) {//说明至少已经更新一次了
                newFiber = oldFiber.alternate;//如果有上上次的fiber,就拿 过来作为这一次的fiber
                newFiber.props = newChild.props;
                newFiber.alternate = oldFiber;
                newFiber.effectTag = UPDATE;
                newFiber.nextEffect = null;
            } else {
                newFiber = {
                    tag: oldFiber.tag,//TAG_HOST
                    type: oldFiber.type,//div
                    props: newChild.props,//{id="A1" style={style}} 一定要用新的元素的props
                    stateNode: oldFiber.stateNode,//div还没有创建DOM元素
                    return: currentFiber,//父Fiber returnFiber
                    alternate: oldFiber,//让新的fiber的alternate指向老的fiber节点
                    effectTag: UPDATE,//副作用标识 render我们要会收集副作用 增加 删除 更新
                    nextEffect: null,//effect list 也是一个单链表
                }
            }
        } else {
            if (newChild) {//看看新的虚拟DOM是不是为null
                newFiber = {
                    tag,//TAG_HOST
                    type: newChild.type,//div
                    props: newChild.props,//{id="A1" style={style}}
                    stateNode: null,//div还没有创建DOM元素
                    return: currentFiber,//父Fiber returnFiber
                    effectTag: PLACEMENT,//副作用标识 render我们要会收集副作用 增加 删除 更新
                    nextEffect: null,//effect list 也是一个单链表
                }
            }
            if (oldFiber) {
                oldFiber.effectTag = DELETION;
                deletions.push(oldFiber);
            }
        }
        if (oldFiber) {
            oldFiber = oldFiber.sibling;//oldFiber指针往后移动一次
        }
        //最小的儿子是没有弟弟的
        if (newFiber) {
            if (newChildIndex == 0) {//如果当前索引为0,说明这是太子
                currentFiber.child = newFiber;
            } else {
                prevSibling.sibling = newFiber;//让太子的sibling弟弟指向二皇子
            }
            prevSibling = newFiber;
        }
        newChildIndex++;
    }
}
//循环执行工作 nextUnitWork
function workLoop(deadline) {
    let shouldYield = false;//是否要让出时间片或者说控制权
    while (nextUnitOfWork && !shouldYield) {
        nextUnitOfWork = performUnitOfWork(nextUnitOfWork);//执行完一个任务后
        shouldYield = deadline.timeRemaining() < 1;//没有时间的话就要让出控制权
    }
    if (!nextUnitOfWork && workInProgressRoot) {
        console.log('render阶段结束');
        commitRoot();
    }
    //不管有没有任务,都请求再次调度 每一帧都要执行一次workLoop
    requestIdleCallback(workLoop, { timeout: 500 });
}
function commitRoot() {
    deletions.forEach(commitWork);//执行effect list之前先把该删除的元素删除
    let currentFiber = workInProgressRoot.firstEffect;
    while (currentFiber) {
        commitWork(currentFiber);
        currentFiber = currentFiber.nextEffect;
    }
    deletions.length = 0;//提交之后要清空deletion数组
    currentRoot = workInProgressRoot;//把当前渲染成功的根fiber 赋给currentRoot
    workInProgressRoot = null;
}
function commitWork(currentFiber) {
    if (!currentFiber) return;
    let returnFiber = currentFiber.return;
    let domReturn = returnFiber.stateNode;
    if (currentFiber.effectTag === PLACEMENT) {//新增加节点
        domReturn.appendChild(currentFiber.stateNode);
    } else if (currentFiber.effectTag === DELETION) {//删除节点
        return domReturn.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;
}

requestIdleCallback(workLoop, { timeout: 500 });

下一篇开始类组件和 hooks 实现。

2人赞

分享到: