React 16.8 在2019年上线发布
React
先看React 源码API 知识结构图

React 源码
- react : 核心Api如:React.createElement、React.Component都在这
- 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-refresh: 热加载相关
- React-reconciler:在render阶段用它来构建fiber节点
react 的设计理念
异步可中断
代数效应 Algebraic Effects
除了cpu的瓶颈问题,还有一类问题是和副作用相关的问题,比如获取数据、文件操作等。不同设备性能和网络状况都不一样,react怎样去处理这些副作用,让我们在编码时最佳实践,运行应用时表现一致呢,这就需要react有分离副作用的能力,为什么要分离副作用呢,因为要解耦,这就是代数效应
在获取数据前展示loading,数据获取之后取消loading,假设我们的设备性能和网络状况都很好,数据很快就获取到了,那我们还有必要在一开始的时候展示loading吗?如何才能有更好的用户体验呢
function getPrice(id) {
return fetch(`xxx.com?id=${productId}`).then((res)=>{
return res.price
})
}
async function getTotalPirce(id1, id2) {
const p1 = await getPrice(id1);
const p2 = await getPrice(id2);
return p1 + p2;
}
async function run(){
await getTotalPrice('001', '002');
}
getPrice是一个异步获取数据的方法,我们可以用async+await的方式获取数据,但是这会导致调用getTotalPrice的run方法也会变成异步函数,这就是async的传染性,所以没法分离副作用
function getPrice(id) {
const price = perform id;
return price;
}
function getTotalPirce(id1, id2) {
const p1 = getPrice(id1);
const p2 = getPrice(id2);
return p1 + p2;
}
try {
getTotalPrice('001', '002');
} handle (productId) {
fetch(`xxx.com?id=${productId}`).then((res)=>{
resume with res.price
})
}
现在改成下面这段代码,其中perform和handle是虚构的语法,当代码执行到perform的时候会暂停当前函数的执行,并且被handle捕获,handle函数体内会拿到productId参数获取数据之后resume价格price,resume会回到之前perform暂停的地方并且返回price,这就完全把副作用分离到了getTotalPirce和getPrice之外。
这里的关键流程是perform暂停函数的执行,handle获取函数执行权,resume交出函数执行权
代数效应的另一个例子
function getName(user) {
let name = user.name;
if (name === null) {
throw new Error('A girl has no name');
}
return name;
}
function makeFriends(user1, user2) {
user1.friendNames.add(getName(user2));
user2.friendNames.add(getName(user1));
}
const arya = { name: null };
const gendry = { name: 'Gendry' };
try {
makeFriends(arya, gendry);
} catch (err) {
console.log("Oops, that didn't work out: ", err);
}
我们在 getName 中 throw 一个错误,但它穿过 makeFriends,“冒泡”到了最近的 catch 块。这是 try / catch 的一个重要属性。处于中间的东西不需要关心自身的错误处理。 在上面的例子中,一旦命中错误,后面的代码就不能继续执行了。当我们进入 catch 块,就无法再回到原来的代码继续执行, 通过代数效应的话,我们可以奇迹般地“回到”原来的地方然后做一些改变:我们从 user.name 缺失中恢复
function getName(user) {
let name = user.name;
if (name === null) {
//
// 引擎在调用堆栈中寻找最近的 try / handle 效应处理
name = perform 'ask_name'; // 可以传给 perform 任何值。在这里,传入的是一个字符串,但也可以是一个对象或者任意其他数据类型
}
return name;
}
function makeFriends(user1, user2) {
user1.friendNames.add(getName(user2));
user2.friendNames.add(getName(user1));
}
const arya = { name: null };
const gendry = { name: 'Gendry' };
try {
makeFriends(arya, gendry);
// 效应告诉我们如何处理名字参数缺失的情况
} handle (effect) {
if (effect === 'ask_name') {
resume with 'Arya Stark';
}
}
React 源码架构
react的核心可以用ui=fn(state)来表示,更详细可以用
const state = reconcile(update);
const UI = commit(state);
- Scheduler(调度器): 排序优先级,让优先级高的任务先进行reconcile
- Reconciler(协调器): 找出哪些节点发生了改变,并打上不同的Flags(旧版本react叫Tag)
- Renderer(渲染器): 将Reconciler中打好标签的节点渲染到视图上

jsx or tsx
jsx是js语言的扩展,react通过babel词法解析,将jsx转换成React.createElement,React.createElement方法返回virtual-dom对象(内存中用来描述dom阶段的对象),所有jsx本质上就是React.createElement的语法糖,它能声明式的编写我们想要组件呈现出什么样的ui效果。在第5章jsx我们会详细介绍jsx解析之后的结果
Fiber 双缓存架构
Fiber对象上面保存了包括这个节点的属性、类型、dom等,Fiber通过child、sibling、return(指向父节点)来形成Fiber树,还保存了更新状态时用于计算state的updateQueue,updateQueue是一种链表结构,上面可能存在多个未计算的update,update也是一种数据结构,上面包含了更新的数据、优先级等,除了这些之外,上面还有和副作用有关的信息。 双缓存是指存在两颗Fiber树,current Fiber树描述了当前呈现的dom树,workInProgress Fiber是正在更新的Fiber树,这两颗Fiber树都是在内存中运行的,在workInProgress Fiber构建完成之后会将它作为current Fiber应用到dom上
function App() {
const [count, setCount] = useState(0);
return (
<>
<h1
onClick={() => {
// debugger;
setCount(() => count + 1);
}}
>
Hi <p title={count}>{count}</p> lanbery
</h1>
</>
)
}
ReactDOM.render(<App />, document.getElementById("root"));
在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好性能的操作
scheduler
Scheduler的作用是调度任务,react15没有Scheduler这部分,所以所有任务没有优先级,也不能中断,只能同步执行 要实现异步可中断的更新,需要浏览器指定一个时间,如果没有时间剩余了就需要暂停任务,requestIdleCallback貌似是个不错的选择,但是它存在兼容和触发不稳定的原因,react17中采用MessageChannel来实现
// React 17.0.2
export function runWithPriority<T>(
reactPriorityLevel: ReactPriorityLevel,
fn: () => T,
): T {
const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
return Scheduler_runWithPriority(priorityLevel, fn);
}
export function scheduleCallback(
reactPriorityLevel: ReactPriorityLevel,
callback: SchedulerCallback,
options: SchedulerCallbackOptions | void | null,
) {
const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
return Scheduler_scheduleCallback(priorityLevel, callback, options);
}
export function scheduleSyncCallback(callback: SchedulerCallback) {
// Push this callback into an internal queue. We'll flush these either in
// the next tick, or earlier if something calls `flushSyncCallbackQueue`.
if (syncQueue === null) {
syncQueue = [callback];
// Flush the queue in the next tick, at the earliest.
immediateQueueCallbackNode = Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushSyncCallbackQueueImpl,
);
} else {
// Push onto existing queue. Don't need to schedule a callback because
// we already scheduled one when we created the queue.
syncQueue.push(callback);
}
return fakeCallbackNode;
}
export function cancelCallback(callbackNode: mixed) {
if (callbackNode !== fakeCallbackNode) {
Scheduler_cancelCallback(callbackNode);
}
}
export function flushSyncCallbackQueue() {
if (immediateQueueCallbackNode !== null) {
const node = immediateQueueCallbackNode;
immediateQueueCallbackNode = null;
Scheduler_cancelCallback(node);
}
flushSyncCallbackQueueImpl();
}
function flushSyncCallbackQueueImpl() {
if (!isFlushingSyncQueue && syncQueue !== null) {
// Prevent re-entrancy.
isFlushingSyncQueue = true;
let i = 0;
if (decoupleUpdatePriorityFromScheduler) {
const previousLanePriority = getCurrentUpdateLanePriority();
try {
const isSync = true;
const queue = syncQueue;
setCurrentUpdateLanePriority(SyncLanePriority);
runWithPriority(ImmediatePriority, () => {
for (; i < queue.length; i++) {
let callback = queue[i];
do {
callback = callback(isSync);
} while (callback !== null);
}
});
syncQueue = null;
} catch (error) {
// If something throws, leave the remaining callbacks on the queue.
if (syncQueue !== null) {
syncQueue = syncQueue.slice(i + 1);
}
// Resume flushing in the next tick
Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushSyncCallbackQueue,
);
throw error;
} finally {
setCurrentUpdateLanePriority(previousLanePriority);
isFlushingSyncQueue = false;
}
} else {
try {
const isSync = true;
const queue = syncQueue;
runWithPriority(ImmediatePriority, () => {
for (; i < queue.length; i++) {
let callback = queue[i];
do {
callback = callback(isSync);
} while (callback !== null);
}
});
syncQueue = null;
} catch (error) {
// If something throws, leave the remaining callbacks on the queue.
if (syncQueue !== null) {
syncQueue = syncQueue.slice(i + 1);
}
// Resume flushing in the next tick
Scheduler_scheduleCallback(
Scheduler_ImmediatePriority,
flushSyncCallbackQueue,
);
throw error;
} finally {
isFlushingSyncQueue = false;
}
}
}
}
在Scheduler中的每个任务的优先级使用过期时间表示的,如果一个任务的过期时间离现在很近,说明它马上就要过期了,优先级很高,如果过期时间很长,那它的优先级就低,没有过期的任务存放在timerQueue中,过期的任务存放在taskQueue中,timerQueue和timerQueue都是小顶堆,所以peek取出来的都是离现在时间最近也就是优先级最高的那个任务,然后优先执行它

Lane 模型
react之前的版本用expirationTime属性代表优先级,该优先级和IO不能很好的搭配工作(io的优先级高于cpu的优先级),现在有了更加细粒度的优先级表示方法Lane,Lane用二进制位表示优先级,二进制中的1表示位置,同一个二进制数可以有多个相同优先级的位,这就可以表示‘批’的概念,而且二进制方便计算。
这好比赛车比赛,在比赛开始的时候会分配一个赛道,比赛开始之后大家都会抢内圈的赛道(react中就是抢优先级高的Lane),比赛的尾声,最后一名赛车如果落后了很多,它也会跑到内圈的赛道,最后到达目的地(对应react中就是饥饿问题,低优先级的任务如果被高优先级的任务一直打断,到了它的过期时间,它也会变成高优先级)
reconciler (render 阶段)
Reconciler发生在render阶段,render阶段会分别为节点执行beginWork和completeWork,或者计算state,对比节点的差异,为节点赋值相应的effectFlags(对应dom节点的增删改
在update的时候会根据最新的state形成的jsx对象和current Fiber树对比构建workInProgress Fiber树,这个对比的过程就是diff算法
diff算法发生在render阶段的reconcileChildFibers函数中,diff算法分为单节点的diff和多节点的diff(例如一个节点中包含多个子节点就属于多节点的diff),单节点会根据节点的key和type,props等来判断节点是复用还是直接新创建节点,多节点diff会涉及节点的增删和节点位置的变化
reconcile时会在这些Fiber上打上Flags标签,在commit阶段把这些标签应用到真实dom上,这些标签代表节点的增删改
function App() {
return (
<>
<h1>
<p>Hi</p> lanbery
</h1>
</>
)
}

renderer (commit 阶段)
Renderer发生在commit阶段,commit阶段遍历effectList执行对应的dom操作或部分生命周期。
Renderer是在commit阶段工作的,commit阶段会遍历render阶段形成的effectList,并执行真实dom节点的操作和一些生命周期,不同平台对应的Renderer不同,例如浏览器对应的就是react-dom。
commit阶段发生在commitRoot函数中,该函数主要遍历effectList,分别用三个函数来处理effectList上的节点,这三个函数是commitBeforeMutationEffects、 commitMutationEffects、commitLayoutEffects,他们主要做的事情如下,后面会详细讲解,现在在大脑里有一个结构就行
concurrent
它是一类功能的合集(如fiber、schduler、lane、suspense),其目的是为了提高应用的响应速度,使应用cpu密集型的更新不在那么卡顿,其核心是实现了一套异步可中断、带优先级的更新。
我们知道一般浏览器的fps是60Hz,也就是每16.6ms会刷新一次,而js执行线程和GUI也就是浏览器的绘制是互斥的,因为js可以操作dom,影响最后呈现的结果,所以如果js执行的时间过长,会导致浏览器没时间绘制dom,造成卡顿。react17会在每一帧分配一个时间(时间片)给js执行,如果在这个时间内js还没执行完,那就要暂停它的执行,等下一帧继续执行,把执行权交回给浏览器去绘制。
怎样调试react
jsx & 核心api
virtual-dom
jsx & createElement
/**
* Create and return a new ReactElement of the given type.
* See https://reactjs.org/docs/react-api.html#createelement
*/
export function createElement(type, config, children) {
let propName;
// Reserved names are extracted
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
if (hasValidRef(config)) {
ref = config.ref;
if (__DEV__) {
warnIfStringRefCannotBeAutoConverted(config);
}
}
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// Remaining properties are added to a new props object
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// Children can be more than one argument, and those are transferred onto
// the newly allocated props object.
const childrenLength = arguments.length - 2;
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
props.children = childArray;
}
// Resolve default props
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
if (__DEV__) {
if (key || ref) {
const displayName =
typeof type === 'function'
? type.displayName || type.name || 'Unknown'
: type;
if (key) {
defineKeyPropWarningGetter(props, displayName);
}
if (ref) {
defineRefPropWarningGetter(props, displayName);
}
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
render
// react-dom/client/ReactDOMLegacy.js
export function render(
element: React$Element<any>, //jsx对象
container: Container, // 挂载dom
callback: ?Function, // 回调
) {
invariant(
isValidContainer(container),
'Target container is not a DOM element.',
);
if (__DEV__) {
const isModernRoot =
isContainerMarkedAsRoot(container) &&
container._reactRootContainer === undefined;
if (isModernRoot) {
console.error(
'You are calling ReactDOM.render() on a container that was previously ' +
'passed to ReactDOM.createRoot(). This is not supported. ' +
'Did you mean to call root.render(element)?',
);
}
}
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
}
component
//ReactBaseClasses.js
function Component(props, context, updater) {
this.props = props;//props属性
this.context = context;//当前的context
this.refs = emptyObject;//ref挂载的对象
this.updater = updater || ReactNoopUpdateQueue;//更新的对像
}
Component.prototype.isReactComponent = {};//表示是classComponent
component函数中主要在当前实例上挂载了props、context、refs、updater等,所以在组件的实例上能拿到这些,而更新主要的承载结构就是updater, 主要关注isReactComponent,它用来表示这个组件是类组件
jsx是React.createElement的语法糖,jsx通过babel转化成React.createElement函数,React.createElement执行之后返回jsx对象,也叫virtual-dom,Fiber会根据jsx对象和current Fiber进行对比形成workInProgress Fiber
react启动的模式
-
legacy 模式: ReactDOM.render(
, rootNode)。这是当前 React app 使用的方式。当前没有计划删除本模式,但是这个模式可能不支持这些新功能。 legacy 模式在合成事件中有自动批处理的功能,但仅限于一个浏览器任务。非 React 事件想使用这个功能必须使用 unstable_batchedUpdates。在 blocking 模式和 concurrent 模式下,所有的 setState 在默认情况下都是批处理的。会在开发中发出警告 legacy模式是我们常用的,它构建dom的过程是同步的,所以在render的reconciler中,如果diff的过程特别耗时,那么导致的结果就是js一直阻塞高优先级的任务(例如用户的点击事件),表现为页面的卡顿,无法响应。 -
blocking 模式: ReactDOM.createBlockingRoot(rootNode).render(
)。目前正在实验中。作为迁移到 concurrent 模式的第一个步骤 -
concurrent 模式: ReactDOM.createRoot(rootNode).render(
)。目前在实验中,未来稳定之后,打算作为 React 的默认开发模式。这个模式开启了所有的新功能 concurrent Mode:它用时间片调度实现了异步可中断的任务,根据设备性能的不同,时间片的长度也不一样,在每个时间片中,如果任务到了过期时间,就会主动让出线程给高优先级的任务。
| 特性/Mode | legacy 模式 | blocking 模式 | concurrent 模式 |
|---|---|---|---|
| String Refs | ✔ | ✘ | ✘ |
| Legacy Context | ✔ | ✘ | ✘ |
| findDOMNode | ✔ | ✘ | ✘ |
| Suspense | ✔ | ✔ | ✔ |
| SuspenseList | ✔ | ✘ | ✘ |
| Suspense SSR + Hydration | ✘ | ✔ | ✔ |
| Progressive Hydration | ✘ | ✔ | ✔ |
| Selective Hydration | ✘ | ✘ | ✔ |
| Cooperative Multitasking | ✘ | ✘ | ✔ |
| Automatic batching of multiple stState | ✘ | ✔ | ✔ |
| Priority-base Rending | ✘ | ✘ | ✔ |
| Interruptible Prerendering | ✘ | ✘ | ✔ |
| useTransition | ✘ | ✘ | ✔ |
| useDeferredValue | ✘ | ✘ | ✔ |
| Suspense Reveal “Train” | ✘ | ✘ | ✔ |
主要执行流程
