react面试题ref概念⼈⼈能读懂的react源码解析(⼤⼚⾼薪必备)
⼈⼈都能读懂的react源码解析(⼤⼚⾼薪必备)
视频讲解
视频课程的⽬的是为了快速掌握react源码运⾏的过程和react中的scheduler、reconciler、renderer、fiber等,并且详细debug源码和分析,过程更清晰。
视频讲解(⾼效学习):
课程结构:
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
在正式开始之前需要了解⼀下前置知识,现在不太清楚没关系,这些内容会在后⾯的章节中出现并且详细介绍,这⼀章的⽬标是了解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操作或部分⽣命周期
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还没执⾏完,那就要暂停它的执⾏,等下⼀帧继续执⾏,把执⾏权交回给浏览器去绘制。
对⽐下开启和未开启concurrent mode的区别,开启之后,构建Fiber的任务的执⾏不会⼀直处于阻塞状态,⽽是分成了⼀个个的task
未开启concurrent
开启concurrent
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>
)
}
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章详细讲解
如果p和h1节点更新了则effectList如下,从rootFiber->h1->p,,顺便说下fiberRoot是整个项⽬的根节点,只存在⼀个,rootFiber是应⽤的根节点,可能存在多个,例如多个der(<App />, ElementById("root"));创建多个应⽤节点
Renderer(commit阶段中)
Renderer是在commit阶段⼯作的,commit阶段会遍历render阶段形成的effectList,并执⾏真实dom节点的操作和⼀些⽣命周期,不同平台对应的Renderer不同,例如浏览器对应的就是react-dom。
commit阶段发⽣在commitRoot函数中,该函数主要遍历effectList,分别⽤三个函数来处理effectList上的节点,这三个函数是commitBeforeMutationEffects、commitMutationEffects、commitLayoutEffects,他们主要做的事情如下,后⾯会详细讲解,现在在⼤脑⾥有⼀个结构就⾏
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取出来的都是离现在时间最近也就是优先级最⾼的那个任务,然后优先执⾏它。
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编译之后,最后会被编译成ateElement,这就是为什么jsx⽂件要声明import React from 'react'的原因,你可以在站点查看jsx被编译后的结果
处理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如:ateElement、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节点怎样调试源码
依赖安装: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
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论