React16 了解源码系列(一)

前置知识

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

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

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

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

  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脚本执行和浏览器布局、绘制是处于同一线程(渲染线程)。 也就是浏览器在一帧的时间内要完成如下工做github

  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;
  }
}
复制代码

非首次渲染更新流程

内容未完待续 markdown

总结

待更新

相关文章
相关标签/搜索