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

人人都能读懂的react源码解析(大厂高薪必备) 2.react心智模型(来来来,让大脑有react思维吧) 视频讲解
? 视频课程的目的是为了快速掌握react源码运行的过程和react中的scheduler、reconciler、renderer、fiber等,并且详细debug源码和分析,过程更清晰。
视频讲解(高效学习):点击学习
课程结构:

  1. 开篇(听说你还在艰难的啃react源码)
  2. react心智模型(来来来,让大脑有react思维吧)
  3. Fiber(我是在内存中的dom)
  4. 从legacy或concurrent开始(从入口开始,然后让我们奔向未来)
  5. state更新流程(setState里到底发生了什么)
  6. render阶段(厉害了,我有创建Fiber的技能)
  7. commit阶段(听说renderer帮我们打好标记了,映射真实节点吧)
  8. diff算法(妈妈再也不担心我的diff面试了)
  9. hooks源码(想知道Function Component是怎样保存状态的嘛)
  10. scheduler&lane模型(来看看任务是暂停、继续和插队的)
  11. concurrent mode(并发模式是什么样的)
  12. 手写迷你react(短小精悍就是我)
?
? 在正式开始之前需要了解一下前置知识,现在不太清楚没关系,这些内容会在后面的章节中出现并且详细介绍,这一章的目标是了解react源码中存在的模型(数据结构或者思想)
react架构
? react的核心可以用ui=fn(state)来表示,更详细可以用
const state = reconcile(update); const UI = commit(state);

? react源码可以分为如下几个模块:
  • Scheduler(调度器): 排序优先级,让优先级高的任务先进行reconcile
  • Reconciler(协调器): 找出哪些节点发生了改变,并打上不同的Tag
  • 【人人都能读懂的react源码解析(大厂高薪必备)2.react心智模型(来来来,让大脑有react思维吧)】Renderer(渲染器): 将Reconciler中打好标签的节点渲染到视图上
    ? Scheduler的作用是调度任务,react15没有Scheduler这部分,所以所有任务没有优先级,也不能中断,只能同步执行。
    ? Reconciler发生在render阶段,render阶段会分别为节点执行beginWork和completeWork(后面会讲),或者计算state,对比节点的差异,为节点赋值相应的effectTag(对应dom节点的增删改)
    ? Renderer发生在commit阶段,commit阶段遍历effectList执行对应的dom操作或部分生命周期
    人人都能读懂的react源码解析(大厂高薪必备)2.react心智模型(来来来,让大脑有react思维吧)
    文章图片
    max-width: 100%; margin: 0 auto; display: block;

    ">
人人都能读懂的react源码解析(大厂高薪必备)2.react心智模型(来来来,让大脑有react思维吧)
文章图片
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还没执行完,那就要暂停它的执行,等下一帧继续执行,把执行权交回给浏览器去绘制。
人人都能读懂的react源码解析(大厂高薪必备)2.react心智模型(来来来,让大脑有react思维吧)
文章图片
max-width: 100%; margin: 0 auto; display: block;

">
? 对比下开启和未开启concurrent mode的区别,开启之后,构建Fiber的任务的执行不会一直处于阻塞状态,而是分成了一个个的task
未开启concurrent
人人都能读懂的react源码解析(大厂高薪必备)2.react心智模型(来来来,让大脑有react思维吧)
文章图片
max-width: 100%; margin: 0 auto; display: block;

">
开启concurrent
人人都能读懂的react源码解析(大厂高薪必备)2.react心智模型(来来来,让大脑有react思维吧)
文章图片
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 (xiaochen
) }ReactDOM.render(, document.getElementById("root"));

人人都能读懂的react源码解析(大厂高薪必备)2.react心智模型(来来来,让大脑有react思维吧)
文章图片
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章详细讲解
人人都能读懂的react源码解析(大厂高薪必备)2.react心智模型(来来来,让大脑有react思维吧)
文章图片
max-width: 100%; margin: 0 auto; display: block;

">
? 如果p和h1节点更新了则effectList如下,从rootFiber->h1->p,,顺便说下fiberRoot是整个项目的根节点,只存在一个,rootFiber是应用的根节点,可能存在多个,例如多个ReactDOM.render(, document.getElementById("root")); 创建多个应用节点
人人都能读懂的react源码解析(大厂高薪必备)2.react心智模型(来来来,让大脑有react思维吧)
文章图片
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,他们主要做的事情如下,后面会详细讲解,现在在大脑里有一个结构就行
人人都能读懂的react源码解析(大厂高薪必备)2.react心智模型(来来来,让大脑有react思维吧)
文章图片
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取出来的都是离现在时间最近也就是优先级最高的那个任务,然后优先执行它。
人人都能读懂的react源码解析(大厂高薪必备)2.react心智模型(来来来,让大脑有react思维吧)
文章图片
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

人人都能读懂的react源码解析(大厂高薪必备)2.react心智模型(来来来,让大脑有react思维吧)
文章图片
max-width: 100%; margin: 0 auto; display: block;

">

    推荐阅读