react原理

142 阅读5分钟

react源码中全局变量

// 下一个要处理的fiber节点
let nextUnitOfWork = null;
// 正在构建的fiber树
let workInProgressRoot = null; // rootFiber 应用的根
// 已经完成构建的fiber树
let currentRoot = null;
// 需要删除的dom的fiber
let deletions = [];

// 正在构建的fiber节点
let workInProgressFiber = null;
// 函数组件中正在执行的第几个hook
// useState不能放if中的原因就在这里
let hookIndex = 0;

fiber结构

graph TD
root --current--> container
container --child--> app
app --child--> fiber1
fiber1 --simbling--> fiber2
fiber2 --simbling--> fiber3

fiber1 -.-> app
fiber2 -.-> app
fiber3 -.-> app
app -.-> container

container --stateNode--> root
root --dom--> div#app

fiber树长什么样

当子节点为文本时

  • 如果只用一个子节点
  • 并且该子节点是文本节点
  • 那么不会创建fiber节点
<div>
  <h1>React</h1>
</div>;

fiber 树🌲

graph LR
fiber:div --child--> fiber:h1

当没有文本子节点时

  • 当只有一个子节点
  • 并且该子节点不是文本节点
  • 那么会创建fiber节点
<div>
  <h1>
      <i class='icon icon-home' />
  </h1>
</div>;
graph LR
fiber:div --child--> fiber:h1 --child--> fiber:i

当有多个子节点

  • 当有多个子节点
  • 就算子节点为文本, 依然会创建fiber
  <h2>
    <span>what</span>
    the
    <p>fk</p>
  </h2>
graph LR
fiber:h2 --child--> fiber:span --simbling-->fiber:txt --simbling--> fiber:p

root和container的关系

graph TD
root --current--> container
container --stateNode--> root
root --containerInfo--> 真实根节点div#root
container --child--> app

函数组件的特点

  • 对应的虚拟dom没有 children: vdom.children=null
  • 对应的 fiber 没有dom实例 fiber.stateNode = null
  • 函数组件的return 一定是个单节点
  • 函数组件的child 一定没有simbling
  • 函数组件的child 一定不是fnFiber

schedule

翻译: schedule: 进度表

双缓存

function render(fiber: Fiber) {
  // 第一次渲染
  // fiber.type是'div'
  if (!Fiber.curRoot) {
    Fiber.wipRoot = fiber
  }
  // 第二次渲染
  if (Fiber.curRoot && !Fiber.curRoot.alternate) {
    Fiber.wipRoot = {
      ...fiber,
      alternate: fiber
    }
  }
  // 第三次渲染(双缓存)
  if (Fiber.curRoot && Fiber.curRoot.alternate) {

    // 修改Fiber.curRoot.alternate, 将其变得和fiber一样
    Fiber.curRoot.alternate.type = fiber.type
    Fiber.curRoot.alternate.props = fiber.props

    Fiber.wipRoot = Fiber.curRoot.alternate
    Fiber.wipRoot.alternate = fiber
  }

  Fiber.unitWork = Fiber.wipRoot

}

增量渲染

什么是增量渲染:

Fiber增量渲染是指把一个渲染任务分解为多个渲染任务,而后将其分散到多个帧里面,实现任务的 "可中断"、"可恢复",并给不同的任务赋予不同的"优先级",最终达成更加顺滑的用户体验。

// (浏览器每渲染一帧) 并且 (浏览器有空), 就执行一次performUnitOfWork
function workLoop(deadline) {
 
  let shouldYield = false; // shouldYield === false 表示有空
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork); // 拿到下一个fiber,如果浏览器有空闲就执行
    shouldYield = deadline.timeRemaining() < 1;
  }

  if (!nextUnitOfWork && wipRoot) {
    commitRoot(); // 每一次提交root都会将wipRoot设置为null
    console.log("commitRoot")
  }
  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);// 约等于 workLoop()

举一反三

  • 往页面中添加30万个div
  • 利用时间切片性能优化
<div id="app">
    <button class="btn">add</button>
</div>
const app = document.querySelector('#app')
const btn: any = document.querySelector('.btn')

btn.onclick = () => {
  const add = (index: number) => {
    const div: any = document.createElement('div')
    div.innerText = index
    app.appendChild(div)
  }
  // 执行 add 三十万次
  performChunk(300000, add)
}

// 默认往requestIdleCallback队列添加异步任务
function performChunk(times: number, add: Function) {
  let i = 0;

  function unitWork(idle) {
    const hasTime = () => idle.timeRemaining() > 0

    while(hasTime() && i<=times) {
      add(`这是第${i}个`)
      i++
    }

    if(i<=times) {
      requestIdleCallback(unitWork)
    }
  }

  requestIdleCallback(unitWork)
  
}

reconcile

翻译: reconcile: 使一致

reconcile的工作内容:

  • 创建子fiber(diff算法发生在这里)
  • 把父fiber和子fiber串起来
graph TD
fiber --child--> fiber1
fiber1 --simbling--> fiber2
fiber2 --simbling--> fiber3

fiber1 -.-> fiber
fiber2 -.-> fiber
fiber3 -.-> fiber

优先级

  • fiber = performUnitOfWork(fiber)
  • performUnitOfWork:
    • 1: 优先返回child
    • 2: 没有chidl就返回sibling
    • 3: 既没有child, 也没有sibling,
      • 则返fiber.parent.sibling
      • 或者fiber.parent.parent.sibling
      • 或者fiber.parent.parent.parent.sibling
      • 直到找到为止
      • 如果fiber.parent是rootFiber, rootFiber.parent不存在 => 循环结束
function performUnitOfWork(currentFiber) {
  beginWork(currentFiber);   // 这里构建fiber树,就是把父fiber和子fiber串起来
  if (currentFiber.child) {
    return currentFiber.child;
  }
  while (currentFiber) {
    completeUnitOfWork(currentFiber); // 这里构建effect链
    if (currentFiber.sibling) {
      return currentFiber.sibling; 
    }
    currentFiber = currentFiber.return; 
  }
}

递: beginWork

  • 链接 child sibling return
  • diff 算法: 给不能复用的节打标记
function beginWork(currentFiber) {
  // 如果 currentFiber 是根fiber
  if (currentFiber.tag === TAG_ROOT) {
    updateHostRoot(currentFiber);
  } else if (currentFiber.tag === TAG_HOST) {
    updateHost(currentFiber); // 原生DOM节点
  } else if (currentFiber.tag === TAG_FUNCTION_COMPONENT) { //处理函数组件
    updateFunctionComponent(currentFiber);
  }
}

处理根fiber

// 更新根组件
function updateHostRoot(currentFiber) {
  //更新根rootFiber
  let newChildren = currentFiber.props.children;
  // 调和 子节点
  reconcileChildren(currentFiber, newChildren);
}

处理函数组件的fiber

let wipFiber = null;// 正在构建的fiber节点
let hookIndex = null; // 确保useState一一对应

function updateFunctionComponent(fiber) {
  wipFiber = fiber; // 保存下当前正在处理的fiber,这样做的目的是可以任务中断再开始后可以快速恢复之前的状态
  hookIndex = 0;    // useState每执行一次, hookIndex+1
  wipFiber.hooks = []; // useState每执行一次, wipFiber.hooks.push(hook)
  const children = [fiber.type(fiber.props)]; // 如果fiber是函数组件,那就执行type方法,把props传过去,返回值又是一个jsx转createElement
  reconcileChildren(fiber, children); // 继续调用协调器来处理父fiber和子fiber之间的关系
}

处理原生节点的fiber

function updateHostComponent(fiber) {
  if (!fiber.dom) { 
    fiber.dom = createDom(fiber);
  }
  reconcileChildren( // 如果fiber的dom有东西,就调用协调器处理该dom的子节点,当前例子子节点是一个函数(组件)
    fiber,
    (fiber.props && fiber.props.children) ? fiber.props.children : []
  );
}

reconcileChildren

// 通过虚拟dom elements 生成 子fiber
// 将父fiber和子fiber串起来 
// diff算法也在这里进行
function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;

  while (index < elements.length || oldFiber != null) { // 如果
    const element = elements[index];
    let newFiber = null;
    // 这里是简单diff, 就是从左往右无脑对比, 没有key
    const sameType = oldFiber && element && element.type == oldFiber.type;

    if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE", // 如果更新的子dom的类型和oldFiber的一样,那就是触发了组件更新
      };
    }
    if (element && !sameType) {
      newFiber = { // 如果第一次创建也就是mount的话,sameType肯定null,必然构建一个PLACEMENT的tag,而不是UPDATE或其他
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT",
      };
    }
    // oldFiber存在而element不存在->需要删除
    if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION";
      deletions.push(oldFiber); // 
    }
    // 更新oldFiber准备好下一轮比较
    if (oldFiber) {
      oldFiber = oldFiber.sibling;
    }

    if (index === 0) {
      wipFiber.child = newFiber; // 所有的element都会被包装成fiber,然后wipFiber的child指向当前newFiber
    } else if (element) {
      prevSibling.sibling = newFiber;
    }

    prevSibling = newFiber;
    index++;
  }
}

归: completeUnitOfWork

  • mount阶段: 创建真实dom
  • 给可以复用的节点打上更新标记
  • flags冒泡
    • 以前用的effectList
    • 使用subTreeFlags的好处: 能确定某一个fiber的子孙有没有副作用
    • 只要subTreeFlags不等于0, 它的子孙节点中就有副作用
let NoFlags = ob00000000
let Update  = ob00000001
...

let subFlags = NoFlags
subFlags |= child.subFlags
subFlags |= child.flags
fiber.subFlags = subFlags

diff算法

let lastPlacedIndex

示例

  • 考虑一个列表从 [A, B, C] 变为 [C, A, B] 的情况:
  • 处理 C 时,它的 oldIndex = 2,lastPlacedIndex = 0,因为 2 >= 0,不需要移动,且更新 lastPlacedIndex = 2。
  • 处理 A 时,它的 oldIndex = 0,lastPlacedIndex = 2,因为 0 < 2,需要移动(标记为 Placement)。
  • 处理 B 时,它的 oldIndex = 1,lastPlacedIndex = 2,因为 1 < 2,需要移动(标记为 Placement)。
  • 最终只有 A 和 B 需要移动,而 C 不需要移动,从而减少了 DOM 操作。

flag |= Placement

  • 发生在beginWork阶段
    • fiber.alternate 不存在时标记📌
    • fiber 发生移动时标记📌

flag |= Update

  • 发生在 complete阶段
    • fiber.stateNode 可以复用时标记📌

commit

  • 找出fiber tree 上面有副作用的节点
  • 根据flags进行: 新增, 更新, 位移
  • flags不为0就有副作用
  • subFlags为0表示子孙节点没有副作用, 不必往下遍历了
  • subFlags的值是: 所有子孙节点逻辑或操作
00000001
00000000
00000000
00000000
--------
00000001 // 二进制位运算: 只要有一个的值不为0,结果都不可能为0