乐趣区

关于react.js:人人都能读懂的react源码解析大厂高薪必备2react心智模型来来来让大脑有react思维吧

人人都能读懂的 react 源码解析(大厂高薪必备)

2.react 心智模型(来来来, 让大脑有 react 思维吧)

视频解说

​ 视频课程的目标是为了疾速把握 react 源码运行的过程和 react 中的 scheduler、reconciler、renderer、fiber 等,并且具体 debug 源码和剖析,过程更清晰。

视频解说(高效学习): 点击学习

课程构造:

  1. <u> 开篇(据说你还在艰巨的啃 react 源码)</u>
  2. <u>react 心智模型(来来来, 让大脑有 react 思维吧)</u>
  3. <u>Fiber(我是在内存中的 dom)</u>
  4. <u> 从 legacy 或 concurrent 开始(从入口开始, 而后让咱们奔向将来)</u>
  5. <u>state 更新流程(setState 里到底产生了什么)</u>
  6. <u>render 阶段(厉害了, 我有创立 Fiber 的技能)</u>
  7. <u>commit 阶段(据说 renderer 帮咱们打好标记了, 映射实在节点吧)</u>
  8. <u>diff 算法(妈妈再也不放心我的 diff 面试了)</u>
  9. <u>hooks 源码(想晓得 Function Component 是怎么保留状态的嘛)</u>
  10. <u>scheduler&lane 模型(来看看工作是暂停、持续和插队的)</u>
  11. <u>concurrent mode(并发模式是什么样的)</u>
  12. <u> 手写迷你 react(短小精悍就是我)</u>

​ 在正式开始之前须要理解一下前置常识,当初不太分明没关系,这些内容会在前面的章节中呈现并且具体介绍,这一章的指标是理解 react 源码中存在的模型(数据结构或者思维)

react 架构

​ react 的外围能够用 ui=fn(state)来示意,更具体能够用

const state = reconcile(update);
const UI = commit(state);

​ react 源码能够分为如下几个模块:

  • Scheduler(调度器):排序优先级,让优先级高的工作先进行 reconcile
  • Reconciler(协调器):找出哪些节点产生了扭转,并打上不同的 Tag
  • Renderer(渲染器):将 Reconciler 中打好标签的节点渲染到视图上

    ​ Scheduler 的作用是调度工作,react15 没有 Scheduler 这部分,所以所有工作没有优先级,也不能中断,只能同步执行。

    ​ Reconciler 产生在 render 阶段,render 阶段会别离为节点执行 beginWork 和 completeWork(前面会讲),或者计算 state,比照节点的差别,为节点赋值相应的 effectTag(对应 dom 节点的增删改)

    ​ Renderer 产生在 commit 阶段,commit 阶段遍历 effectList 执行对应的 dom 操作或局部生命周期

    <img src=”https://gitee.com/xiaochen1024/assets/raw/master/assets/_10.png” style=”

    max-width: 100%;
    margin: 0 auto;
    display: block;

    “>

<img src=”https://gitee.com/xiaochen1024/assets/raw/master/assets/_25.png” style=”

max-width: 100%;
margin: 0 auto;
display: block;

“>

react17 的呈现是为了解决什么

​ react 之前的版本在 reconcile 的过程中是同步执行的,而计算简单组件的差别可能是一个耗时操作,加之 js 的执行是单线程的,设施性能不同,页面就可能会呈现卡顿的景象。此外利用所处的网络情况也不同,也须要应答不同网络状态下获取数据的响应,所以为了解决这两类(cpu、io)问题,react17 带了全新的 concurrent mode,它是一类性能的合集(如 fiber、schduler、lane、suspense),其目标是为了进步利用的响应速度,使利用不在那么卡顿,其外围是实现了一套异步可中断、带优先级的更新。

​ 那么 react17 怎么实现异步可中断的更新呢,咱们晓得个别浏览器的 fps 是 60Hz,也就是每 16.6ms 会刷新一次,而 js 执行线程和 GUI 也就是浏览器的绘制是互斥的,因为 js 能够操作 dom,影响最初出现的后果,所以如果 js 执行的工夫过长,会导致浏览器没工夫绘制 dom,造成卡顿。react17 会在每一帧调配一个工夫(工夫片)给 js 执行,如果在这个工夫内 js 还没执行完,那就要暂停它的执行,等下一帧继续执行,把执行权交回给浏览器去绘制。

<img src=”https://gitee.com/xiaochen1024/assets/raw/master/assets/_24.png” style=”

max-width: 100%;
margin: 0 auto;
display: block;

“>

​ 比照下开启和未开启 concurrent mode 的区别,开启之后,构建 Fiber 的工作的执行不会始终处于阻塞状态,而是分成了一个个的 task

未开启 concurrent

<img src=”https://gitee.com/xiaochen1024/assets/raw/master/assets/_0.png” style=”

max-width: 100%;
margin: 0 auto;
display: block;

“>

开启 concurrent

<img src=”https://gitee.com/xiaochen1024/assets/raw/master/assets/_29.png” style=”

max-width: 100%;
margin: 0 auto;
display: block;

“>

Fiber 双缓存

​ Fiber(Virtual dom)是内存中用来形容 dom 阶段的对象

​ 在它下面保留了包含这个节点的属性、类型、dom 等,Fiber 通过 child、sibling、return(指向父节点)来造成 Fiber 树,

​ 还保留了更新状态时用于计算 state 的 updateQueue,updateQueue 是一种链表构造,下面可能存在多个未计算的 update,update 也是一种数据结构,下面蕴含了更新的数据、优先级等,

​ 除了这些之外,下面还有和副作用无关的信息。

​ 双缓存是指存在两颗 Fiber 树,current Fiber 树形容了以后出现的 dom 树,workInProgress Fiber 是正在更新的 Fiber 树,这两颗 Fiber 树都是在内存中运行的,在 workInProgress Fiber 构建实现之后会将它作为 current Fiber 利用到 dom 上

​ 在 mount 时(首次渲染),会依据 jsx 对象(Class Component 或的 render 函数者 Function Component 的返回值),构建 Fiber 对象,造成 Fiber 树,而后这颗 Fiber 树会作为 current Fiber 利用到实在 dom 上,在 update(状态更新时如 setState)的时候,会依据状态变更后的 jsx 对象和 current Fiber 做比照造成新的 workInProgress Fiber,而后 workInProgress Fiber 切换成 current Fiber 利用到实在 dom 就达到了更新的目标,而这一切都是在内存中产生的,从而缩小了对 dom 好性能的操作。

​ 例如上面代码的 Fiber 双缓存构造如下,在第 5 章会具体解说

function App() {
  return (
    <div>
      xiao
      <p>chen</p>
    </div>
  )
}

ReactDOM.render(<App />, document.getElementById("root"));

<img src=”https://gitee.com/xiaochen1024/assets/raw/master/assets/_17.png” style=”

max-width: 100%;
margin: 0 auto;
display: block;

“>

Reconciler(render 阶段中)

​ 协调器是在 render 阶段工作的,简略一句话概括就是 Reconciler 会创立或者更新 Fiber 节点。在 mount 的时候会依据 jsx 生成 Fiber 对象,在 update 的时候会依据最新的 state 造成的 jsx 对象和 current Fiber 树比照构建 workInProgress Fiber 树,这个比照的过程就是 diff 算法。reconcile 时会在这些 Fiber 上打上 Tag 标签,在 commit 阶段把这些标签利用到实在 dom 上,这些标签代表节点的增删改,如

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*              */ 0b0000000001000;

​ render 阶段遍历 Fiber 树相似 dfs 的过程,‘捕捉’阶段产生在 beginWork 函数中,该函数做的次要工作是创立 Fiber 节点,计算 state 和 diff 算法,‘冒泡’阶段产生在 completeWork 中,该函数次要是做一些收尾工作,例如解决节点的 props、和造成一条 effectList 的链表,该链表是被标记了更新的节点造成的链表

​ 深度优先遍历过程如下,图中的数字是程序,return 指向父节点,第 8 章具体解说

<img src=”https://gitee.com/xiaochen1024/assets/raw/master/assets/_5.png” style=”

max-width: 100%;
margin: 0 auto;
display: block;

“>

​ 如果 p 和 h1 节点更新了则 effectList 如下,从 rootFiber->h1->p,,顺便说下 fiberRoot 是整个我的项目的根节点,只存在一个,rootFiber 是利用的根节点,可能存在多个,例如多个 ReactDOM.render(<App />, document.getElementById("root")); 创立多个利用节点

<img src=”https://gitee.com/xiaochen1024/assets/raw/master/assets/_12.png” style=”

max-width: 100%;
margin: 0 auto;
display: block;

“>

Renderer(commit 阶段中)

​ Renderer 是在 commit 阶段工作的,commit 阶段会遍历 render 阶段造成的 effectList,并执行实在 dom 节点的操作和一些生命周期,不同平台对应的 Renderer 不同,例如浏览器对应的就是 react-dom。

​ commit 阶段产生在 commitRoot 函数中,该函数次要遍历 effectList,别离用三个函数来解决 effectList 上的节点,这三个函数是 commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects,他们次要做的事件如下,前面会具体解说,当初在大脑里有一个构造就行

<img src=”https://gitee.com/xiaochen1024/assets/raw/master/assets/_13.png” style=”

max-width: 100%;
margin: 0 auto;
display: block;

“>

diff 算法

​ diff 算法产生在 render 阶段的 reconcileChildFibers 函数中,diff 算法分为单节点的 diff 和多节点的 diff(例如一个节点中蕴含多个子节点就属于多节点的 diff),单节点会依据节点的 key 和 type,props 等来判断节点是复用还是间接新创建节点,多节点 diff 会波及节点的增删和节点地位的变动,具体见第 10 章。

Scheduler

​ 咱们晓得了要实现异步可中断的更新,须要浏览器指定一个工夫,如果没有工夫残余了就须要暂停工作,requestIdleCallback 貌似是个不错的抉择,然而它存在兼容和触发不稳固的起因,react17 中采纳 MessageChannel 来实现。

function workLoopConcurrent() {while (workInProgress !== null && !shouldYield()) {//shouldYield 判断是否暂停工作
    workInProgress = performUnitOfWork(workInProgress); 
  }
}

​ 在 Scheduler 中的每的每个工作的优先级应用过期工夫示意的,如果一个工作的过期工夫离当初很近,阐明它马上就要过期了,优先级很高,如果过期工夫很长,那它的优先级就低,没有过期的工作寄存在 timerQueue 中,过期的工作寄存在 taskQueue 中,timerQueue 和 timerQueue 都是小顶堆,所以 peek 取出来的都是离当初工夫最近也就是优先级最高的那个工作,而后优先执行它。

<img src=”https://gitee.com/xiaochen1024/assets/raw/master/assets/_23.png” style=”

max-width: 100%;
margin: 0 auto;
display: block;

“>

Lane

​ react 之前的版本用 expirationTime 属性代表优先级,该优先级和 IO 不能很好的搭配工作(io 的优先级高于 cpu 的优先级),当初有了更加细粒度的优先级示意办法 Lane,Lane 用二进制位示意优先级,二进制中的 1 示意地位,同一个二进制数能够有多个雷同优先级的位,这就能够示意‘批’的概念,而且二进制不便计算。

​ 这好较量车较量,在较量开始的时候会调配一个赛道,较量开始之后大家都会抢内圈的赛道(react 中就是抢优先级高的 Lane),较量的序幕,最初一名赛车如果落后了很多,它也会跑到内圈的赛道,最初达到目的地(对应 react 中就是饥饿问题,低优先级的工作如果被高优先级的工作始终打断,到了它的过期工夫,它也会变成高优先级)

​ Lane 的二进制位如下,从上往下,优先级递加

export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = /*                 */ 0b0000000000000000000000000000010;

export const InputDiscreteHydrationLane: Lane = /*      */ 0b0000000000000000000000000000100;
const InputDiscreteLanes: Lanes = /*                    */ 0b0000000000000000000000000011000;

const InputContinuousHydrationLane: Lane = /*           */ 0b0000000000000000000000000100000;
const InputContinuousLanes: Lanes = /*                  */ 0b0000000000000000000000011000000;

export const DefaultHydrationLane: Lane = /*            */ 0b0000000000000000000000100000000;
export const DefaultLanes: Lanes = /*                   */ 0b0000000000000000000111000000000;

const TransitionHydrationLane: Lane = /*                */ 0b0000000000000000001000000000000;
const TransitionLanes: Lanes = /*                       */ 0b0000000001111111110000000000000;

const RetryLanes: Lanes = /*                            */ 0b0000011110000000000000000000000;

export const SomeRetryLane: Lanes = /*                  */ 0b0000010000000000000000000000000;

export const SelectiveHydrationLane: Lane = /*          */ 0b0000100000000000000000000000000;

const NonIdleLanes = /*                                 */ 0b0000111111111111111111111111111;

export const IdleHydrationLane: Lane = /*               */ 0b0001000000000000000000000000000;
const IdleLanes: Lanes = /*                             */ 0b0110000000000000000000000000000;

export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;

jsx

​ jsx 是 ClassComponent 的 render 函数或者 FunctionComponent 的返回值,能够用来示意组件的内容,在通过 babel 编译之后,最初会被编译成 React.createElement,这就是为什么 jsx 文件要申明import React from 'react' 的起因,你能够在 babel 编译 jsx 站点查看 jsx 被编译后的后果

React.createElement的源码中做了如下几件事

  • 解决 config,把除了保留属性外的其余 config 赋值给 props
  • 把 children 解决后赋值给 props.children
  • 解决 defaultProps
  • 调用 ReactElement 返回一个 jsx 对象
export function createElement(type, config, children) {
  let propName;

  const props = {};

  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  if (config != null) {
    // 解决 config,把除了保留属性外的其余 config 赋值给 props
    //...
  }

  const childrenLength = arguments.length - 2;
  // 把 children 解决后赋值给 props.children
  //...

  // 解决 defaultProps
  //...

  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );
}

const ReactElement = function(type, key, ref, self, source, owner, props) {
  const element = {
    $$typeof: REACT_ELEMENT_TYPE,// 示意是 ReactElement 类型

    type: type,//class 或 function
    key: key,//key
    ref: ref,//useRef 的 ref 对象
    props: props,//props
    _owner: owner,
  };

  return element;
};

​ $$typeof 示意的是组件的类型,例如在源码中有一个查看是否是非法 Element 的函数,就是根 object.$$typeof === REACT_ELEMENT_TYPE 来判断的

export function isValidElement(object) {
  return (
    typeof object === 'object' &&
    object !== null &&
    object.$$typeof === REACT_ELEMENT_TYPE
  );
}

​ 如果组件是 ClassComponent 则 type 是 class 自身,如果组件是 FunctionComponent 创立的,则 type 是这个 function,源码中用 ClassComponent.prototype.isReactComponent 来区别二者。留神 class 或者 function 创立的组件肯定要首字母大写,不而后被当成一般节点,type 就是字符串。

​ jsx 对象上没有优先级、状态、effectTag 等标记,这些标记在 Fiber 对象上,在 mount 时 Fiber 依据 jsx 对象来构建,在 update 是依据最新状态的 jsx 和 current Fiber 比照,造成新的 workInProgress Fiber,最初 workInProgress Fiber 切换成 current Fiber

源码目录构造

源码中次要包含如下局部

  • fixtures:为代码贡献者提供的测试 React
  • packages:次要局部,蕴含 Scheduler,reconciler 等
  • scripts:react 构建相干

上面来看下 packages 次要蕴含的模块

  • react:外围 Api 如:React.createElement、React.Component 都在这
  • 和平台相干 render 相干的文件夹:

    react-art:如 canvas svg 的渲染
    react-dom:浏览器环境
    react-native-renderer:原生相干
    react-noop-renderer:调试或者 fiber 用

  • 试验性的包

    react-server: ssr 相干

    react-fetch: 申请相干

    react-interactions: 和事件如点击事件相干

    react-reconciler: 构建节点

  • shared:蕴含公共办法和变量
  • 辅助包:

    react-is : 判断类型

    react-client: 流相干

    react-fetch: 数据申请相干

    react-refresh: 热加载相干

  • scheduler:调度器相干
  • React-reconciler:在 render 阶段用它来构建 fiber 节点

怎么调试源码

​ 本课程应用的 react 版本是 17.0.1,通过上面几步就能够调试源码了,当然你能够用现成的蕴含本课程所有 demo 的我的项目来调试,倡议应用曾经构建好的我的项目,地址:https://github.com/xiaochen10…

  • clone 源码:git clone https://github.com/facebook/react.git
  • 依赖装置:npm install or yarn
  • build 源码:npm build react,react-dom,scheduler --type=NODE
  • 为源码建设软链:

    cd build/node_modules/react
    npm link
    cd build/node_modules/react-dom
    npm link
  • create-react-app 创立我的项目

    npx create-react-app demo
    npm link react react-dom

<img src=”https://gitee.com/xiaochen1024/assets/raw/master/assets/_9.jpg” style=”

  max-width: 100%;
  margin: 0 auto;
  display: block;

“>

退出移动版