Javascript

React16 了解源码系列(一)

本文主要是介绍React16 了解源码系列(一),对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

前置知识

使用react也有将近一年了,在使用的过程中,我相信你也会像我一样,存在很多疑惑的点; 举些例子,比如为什么react有些生命钩子会执行多次,而有些只会安全的执行一次?react 16 大版本更新的fiber到底是个什么东西?诸如此类的问题,我也百思不得其解;所以我踏上了探究源码之路;

总所周知,react源码不是一般的多,直接阅读react源码,真的是劝退... 在搜集react源码资料的时候,发现比较全和新的资料也很少,偶然一次机会看到奇舞团大佬按照react源码思路自己debug造了一个;在学习他的源码时候,我也私下和他交流了很多,真的是听君一席话,胜读十年书呀;2333....(还是自己太菜了,非常感谢大佬解答我的问题)

言归正传,这里强烈推荐他电子书,源码系列文章是基于最新的16.13.1解析的; 虽然没有更完,但是写得相当精彩,反正我是看了还想看那种。(有点崔更了,哈哈)

本文是根据最新的16.13.1进行解析,目的是把整体的源码流程看懂个大概,并不会深入到很细节的东西; 也就是说把react的整体更新流程弄明白,可以帮助你更好的去探究最终的源码细节,如果有不正确的地方,还望大佬们指正; 虽说现在更新到了16.13.1版本了,但是整体的架构依然没有变,这里我推荐几个必读的资料,很精彩;

  1. Lin Clark - A Cartoon Intro to Fiber - React Conf 2017
  2. 这可能是最通俗的 React Fiber(时间分片) 打开方式
  3. Deep In React 之浅谈 React Fiber 架构(一)

React16架构

在了解react架构之前,我们还需要了解一下浏览器渲染原理,主流的浏览器刷新频率为60Hz,即每(1000ms / 60Hz)16.6ms浏览器刷新一次。我们知道,JS是可以操作DOM的,所以JS脚本执行和浏览器布局、绘制是处于同一线程(渲染线程)。 也就是浏览器在一帧的时间内要完成以下工作

  1. JS脚本执行
  2. 样式布局
  3. 样式绘制 当JS执行时间过长,超出了16.6ms,这次刷新就没有时间执行样式布局和样式绘制了。这就是造成卡顿的原因

在16大版本之前,也就是React15架构只分为两层,Reconciler(协调器,可不中断)+ Renderer(渲染器,不可中断);也就是说协调阶段,同步(递归更新完)更新的;这很容易造成JS执行时间过长,超出了16.6ms,也就是说一旦开始更新,就不可中断,一口气做完。会造成卡顿,这样的用户体验非常差;

react团队发现,让用户操作感觉不到卡顿,操作以外的有延迟,卡顿一下,用户是完全可以接受的;JS执行时间过长,所以react更改了架构;React16架构可以分为三层:

  1. Scheduler(调度器,可中断)—— 调度任务的优先级,高优任务优先进入Reconciler
  2. Reconciler(协调器,可中断)—— 负责找出变化的组件
  3. Renderer(渲染器,不可中断)—— 负责将变化的组件渲染到页面上
  • 这样的三层架构,个人觉得有以下几个优点
  1. 像计算机网路协议一样,每一层专注干一件事情(单一职责),这样架构的应用,生命周期都相对的长;TCP/IP协议不是活了几十年了嘛。QAQ
  2. 可扩展性和灵活性很强;给开发者保留了很多底层抽象的可能;(antd 就是一个例子)
  3. 熟悉react框架以后,转其他框架相对轻松,因为react是最早出现的主流框架。(该懂的应该都懂)
  • 当然也会有一些非常明显的缺点
  1. 学习成本的提高,像新出的hook,和未来即将稳定的Concurrent 模式,都存在一定的学习成本;
  2. react并没有做很多优化工作,比如在编译阶段,像vue这样的框架就做了相应的优化;不过这也是框架和库的区别;因为react的定位始终是库,react核心开发人员dan自己也说过,未来的发展不会把react变成框架;

初始化阶段

要理解react的更新流程,我觉得最好的方式是画流程图,结合一点源码注释;不然在学习源码的过程会非常的混乱;先看react的初始化阶段。

  1. reactDOM.render,还记得应用挂载的时候么?
  • 应用挂载时候的入口
  // ReactDOM.render(<App name="Hello"/>, document.querySelector('#app'));
  const ReactDOM = {
  render(element, container) {
    // 创建 FiberRoot
    const root = container._reactRootContainer = new ReactRoot(container);
    // 首次渲染不需要批量更新
    DOMRenderer.unbatchedUpdates(() => {
      //调用 FiberRoot 的render方法开始渲染
      root.render(element);
    })
  }
复制代码
  1. FiberRoot 数据结构一探究竟
  • 这里我就不想贴一整大段代码,只把关键的属性列出来
  • FiberNode 里面的数据结构先不管,我们只需要知道它是记录组件(class/fc/element)的状态和信息
  • 最关键的是 current 属性,即是 RootFiber,也就是说 FiberNode.current = RootFiber;
export default class ReactRoot {
  constructor(container) {
    // RootFiber tag === 3
    this.current = new FiberNode(3, null, null);
    // 初始化rootFiber的updateQueue
    initializeUpdateQueue(this.current);
    // RootFiber指向FiberRoot
    this.current.stateNode = this;
    // 应用挂载的根DOM节点
    this.containerInfo = container;
    // root下已经render完毕的fiber
    this.finishedWork = null;
  }
}
复制代码
  1. unbatchedUpdates,这里涉及到一个react的批量更新问题;
  • 在 react 中,如果我在一个 classComponent 组件内的点击事件多次调用 this.setState
  • 主动batchedUpdates, 会输出1,2,3
  • 事件处理函数自带batchedUpdates,相当于使用定时器的效果,会输出0,0,0
  • 这是因为,react认为,在很短的时间内触发的更新,其实是没有必要的,会自动的加上事件合成 batchedUpdates
  • 当然,首次更新是非批量更新的,所以才会调用 unbatchedUpdates 方法;
  handleClick = () => {
    // 主动`unbatchedUpdates`
    // setTimeout(() => {
    //   this.countNumber()
    // }, 0)

    // setTimeout中没有`batchedUpdates`
    setTimeout(() => {
      batchedUpdates(() => this.countNumber())
    }, 0)

    // 事件处理函数自带`batchedUpdates`,相当于上面的情况
    // this.countNumber()
  }

  countNumber() {
    const num = this.state.number
    this.setState({
      number: num + 1,
    })
    console.log(this.state.number)
    this.setState({
      number: num + 2,
    })
    console.log(this.state.number)
    this.setState({
      number: num + 3,
    })
    console.log(this.state.number)
  }
复制代码
  1. 紧接着调用 FiberRoot.render
  • expirationTime 过期时间,代表着本次更新(update)的优先级;
  • 这里得注意,React16.13.1 的 expirationTime 和 16.7 的过期时间是相反的,在16.7中,值越小,优先级越大;
  • 在创建好更新以后,就进入了react调度阶段;
export default class ReactRoot {
  constructor(container) {
    // TODO...
  }   
  render(element) {
    // RootFiber  
    const current = this.current;
    // 申请当前的创建更新时间
    const currentTime = DOMRenderer.requestCurrentTimeForUpdate();
    // expirationTime 过期时间,可以代表着本次更新任务的优先级;
    // 不同事件触发的update会产生不同priority
    // 不同priority使fiber获得不同的expirationTime
    const expirationTime = DOMRenderer.computeExpirationForFiber(currentTime, current);
    // 创建更新
    const update = createUpdate(expirationTime);
    // fiber.tag为HostRoot类型,payload为对应要渲染的ReactComponents(APP 组件)
    update.payload = {element};
    enqueueUpdate(current, update);
    // 首次渲染会走这里,再次更新就直接创建更新对象然后开始调度
    return DOMRenderer.scheduleUpdateOnFiber(current, expirationTime);
  }
}
复制代码

首次渲染更新流程

老样子,我们还是直接先上流程图,根据流程再来看代码和注释;在阅读react源码的时候,是相当枯燥的,我们需要一点耐心慢慢解刨;

  1. scheduleUpdateOnFiber
  • 我们只处理异步任务,所以不需要通过expirationTime检查是否是异步
// 从当前fiber递归上去到root,再从root开始work更新
export function scheduleUpdateOnFiber(fiber, expirationTime) {
  // 注意是值越大,权限越大,和16.7相反了;
  // 向上冒泡更新,同时更新的过期时间(expirationTime)和子节点的过期时间 (childExpirationTime)
  // 这样做的原因是让整个fiber树上更新的最高优先级冒泡到root节点,进行更新  
  const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
  // root == FiberRoot
  if (!root) {
    return;
  }
  // 开始安排调度安排调度
  ensureRootIsScheduled(root);
}
复制代码
  1. ensureRootIsScheduled 开始安排调度
  • 这个阶段相对来说是非常复杂的,但是总的来说它做了以下几件事:
  • 将root加入schedule,root上每次只能存在一个scheduled的任务
  • 每次创建update后都会调用这个函数,需要考虑如下情况:
  • 1.root上有过期任务,需要以ImmediatePriority(同步不中断)立刻调度该任务
  • 2.root上已有schedule但还未到时间执行的任务,比较新旧任务expirationTime和优先级处理
  • 3.root上还没有已有schedule的任务,则开始该任务的render阶段
function ensureRootIsScheduled(root) {
  // 这个变量记录过期未执行的fiber的expirationTime
  const lastExpiredTime = root.lastExpiredTime;
  if (lastExpiredTime !== NoWork) {
   // ....TODO   
  }
  // 寻找root(FiberRoot)本次更新的过期时间
  const expirationTime = getNextRootExpirationTimeToWorkOn(root);
  const existingCallbackNode = root.callbackNode;
  // 本次更新的过期时间其实是没有任务 
  if (expirationTime === NoWork) {
    // 又存在当前正在进行的异步任务,同步执行掉
    if (existingCallbackNode) {
      root.callbackNode = null;
      root.callbackExpirationTime = NoWork;
      root.callbackPriority = Scheduler.NoPriority;
    }
    return;
  }

  // 从当前时间和expirationTime推断任务优先级
  const currentTime = requestCurrentTimeForUpdate();
  const priorityLevel = inferPriorityFromExpirationTime(currentTime, expirationTime);

  if (existingCallbackNode) {
    // 该root上已存在schedule的root
    const existingCallbackNodePriority = root.callbackPriority;
    const existingCallbackExpirationTime = root.callbackExpirationTime;
    if (existingCallbackExpirationTime === expirationTime && existingCallbackNodePriority >= priorityLevel) {
      // 该root已经存在的任务expirationTime和新udpate产生的expirationTime一致
      // 这代表他们可能是同一个事件触发产生的update
      // 且已经存在的任务优先级更高,则可以取消这次update的render
      return;
    }
    // 否则代表新udpate产生的优先级更高,取消之前的schedule,重新开始一次新的
    Scheduler.cancelCallback(existingCallbackNode);
  }

  root.callbackExpirationTime = expirationTime;
  root.callbackPriority = priorityLevel;
  // 保存Scheduler保存的当前正在进行的异步任务
  let callbackNode;
  // 过期任何和同步任务一样,不中断,一口气更新完;
  if (expirationTime === Sync) {
    callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  } else {
    // 正常的异步任务和Concurrent首次渲染走走这里 
    callbackNode = Scheduler.scheduleCallback(
      priorityLevel, 
      performConcurrentWorkOnRoot.bind(null, root),
      // 根据expirationTime,为任务计算一个timeout
      // timeout会影响任务执行优先级
      {timeout: expirationTimeToMs(expirationTime) - Scheduler.now()}
    )
  }
  root.callbackNode = callbackNode;
}
复制代码
  1. performSyncWorkOnRoot
  • 这是不通过scheduler的同步任务render阶段的入口
  • 注意render阶段其实就是Reconcile协调阶段, diff算法就是在这个阶段做的;
function  performSyncWorkOnRoot(root) {
  const lastExpiredTime = root.lastExpiredTime;
  const expirationTime = lastExpiredTime !== NoWork ? lastExpiredTime : Sync;
  //先暂时忽略这个函数   
  flushPassiveEffects();
  if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
    // 创建WIP树进行创建更新,如果WIP树还存在,说明需要打断这个任务
    prepareFreshStack(root, expirationTime);
  }
  //根据WIP树进行更新   
  if (workInProgress) {
    const prevExecutionContext = executionContext;
    executionContext |= RenderContext;
    do {
      // 进入同步的workLoop渲染大循环
      workLoopSync();
      break;
    } while (true)
    // render阶段结束,进入commit阶段,commit阶段不可中断
    commitRoot(root);
    // 重新安排调度, 以免又执行不到过期了的任务;
    ensureRootIsScheduled(root);
  }
  return null;
}
复制代码
  1. workLoopSync
  • 同步模式,不需要考虑任务是否需要中断, 这也是为什么渲染阶段可以同步的原因;
function workLoopSync() {
  while (workInProgress) {
    workInProgress = performUnitOfWork(workInProgress);
  }
}
复制代码
  1. performUnitOfWork
  • 开始执行每个单元的渲染工作,执行到WIP树为空,也就是说没有更新了;
function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate;
  // beginWork会返回fiber.child,不存在next意味着深度优先遍历已经遍历到某个子树的最深层叶子节点
  // beginWork 为render阶段的主要工作之一,主要做了如下事:
  // 根据update更新 state
  // 根据update更新 props
  // 根据update更新 effectTag
  let next = beginWork(current, unitOfWork, renderExpirationTime);
  // beginWork完成 fiber的diff,可以更新momoizedProps
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  if (!next) {
    // completeUnitOfWork 主要做了如下事:
    // 1.为 beginWork阶段生成的fiber生成对应DOM,并产生DOM树
    // let next = completeWork(current, workInProgress);
    // 2. 将child fiber的expirationTime冒泡到父级
    // 这样在父级就能直到子孙中优先级最高到expirationTime
    // resetChildExpirationTime(workInProgress);
    // 3. 组装圣诞树链条 effect list
    next = completeUnitOfWork(unitOfWork);
  }
  return next;
}
复制代码
  • 对着代码我们再来看个图,你就明白了;work阶段结束了,也就代表着渲染阶段已结束
  1. commitRoot 提交阶段
  • 提交阶段相对简单,因为是同步执行的,不可中断
function commitRoot(root) {
  const renderPriorityLevel = Scheduler.getCurrentPriorityLevel();
  // 包裹一层commitRoot,commit使用Scheduler调度
  Scheduler.runWithPriority(Scheduler.ImmediatePriority, commitRootImp.bind(null, root, renderPriorityLevel));
}

// commit阶段的入口,包括如下子阶段:
// before mutation阶段:遍历effect list,执行 DOM操作前触发的钩子
// mutation阶段:遍历effect list,执行effect
function commitRootImp(root) {
  do {
    // syncCallback会保存在一个内部数组中,在 flushPassiveEffects 中 同步执行完
    // 由于syncCallback的callback是 performSyncWorkOnRoot,可能产生新的 passive effect
    // 所以需要遍历直到rootWithPendingPassiveEffects为空
    flushPassiveEffects();
  } while (ReactFiberCommitWorkGlobalVariables.rootWithPendingPassiveEffects !== null)

  if (!finishedWork) {
    return null;
  }

  root.finishedWork = null;
  root.finishedExpirationTime = NoWork;

  // 重置Scheduler相关
  root.callbackNode = null;
  root.callbackExpirationTime = NoWork;
  root.callbackPriority = Scheduler.NoPriority;

  // 已经在commit阶段,finishedWork对应的expirationTime对应的任务的处理已经接近尾声
  // 让我们找找下一个需要处理的任务
  // 在 completeUnitOfWork中有childExpirationTime的冒泡逻辑
  // fiber树中高优先级的expirationTime会冒泡到顶上
  // 所以 childExpirationTime 代表整棵fiber树中下一个最高优先级的任务对应的expirationTime
  const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime(finishedWork);
  // 更新root的firstPendingTime,这代表下一个要进行的任务的expirationTime
  markRootFinishedAtTime(root, expirationTime, remainingExpirationTimeBeforeCommit);

  if (root === workInProgressRoot) {
    // 重置 workInProgress
    workInProgressRoot = null;
    workInProgress = null;
    renderExpirationTime = NoWork;
  }

  let firstEffect;
  if (root.effectTag) {
    // 由于根节点的effect list不含有自身的effect,所以当根节点本身存在effect时需要将其append 入 effect list
    if (finishedWork.lastEffect) {
      finishedWork.lastEffect.nextEffect = finishedWork;
      firstEffect = finishedWork.firstEffect;
    } else {
      firstEffect = finishedWork;
    }
  } else {
    // 根节点本身没有effect
    firstEffect = finishedWork.firstEffect;
  }
  let nextEffect;
  if (firstEffect) {
    // before mutation阶段
    const prevExecutionContext = executionContext;
    executionContext |= CommitContext;
    nextEffect = firstEffect;    
    do {
      try {
        nextEffect = commitBeforeMutationEffects(nextEffect);
      } catch(e) {
        console.warn('commit before error', e);
        nextEffect = nextEffect.nextEffect;
      }
    } while(nextEffect)

    // mutation阶段
    nextEffect = firstEffect;
    do {
      try {
        nextEffect = commitMutationEffects(root, nextEffect);
      } catch(e) {
        console.warn('commit mutaion error', e);
        nextEffect = nextEffect.nextEffect;
      }
    } while(nextEffect)

    // workInProgress tree 现在完成副作用的渲染变成current tree
    // 之所以在 mutation阶段后设置是为了componentWillUnmount触发时 current 仍然指向之前那棵树
    root.current = finishedWork;
    
    if (ReactFiberCommitWorkGlobalVariables.rootDoesHavePassiveEffects) {
      // 本次commit含有passiveEffect
      ReactFiberCommitWorkGlobalVariables.rootDoesHavePassiveEffects = false;
      ReactFiberCommitWorkGlobalVariables.rootWithPendingPassiveEffects = root;
      ReactFiberCommitWorkGlobalVariables.pendingPassiveEffectsExpirationTime = expirationTime;
      ReactFiberCommitWorkGlobalVariables.pendingPassiveEffectsRenderPriority = renderPriorityLevel;
    } else {
      // effectList已处理完,GC
      nextEffect = firstEffect;
      while (nextEffect) {
        const nextNextEffect = nextEffect.next;
        nextEffect.next = null;
        nextEffect = nextNextEffect;
      }
    }
    executionContext = prevExecutionContext;
  } else {
    // 无effect
    root.current = finishedWork;
  }
}
复制代码

非首次渲染更新流程

内容未完待续

总结

待更新

这篇关于React16 了解源码系列(一)的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!