React
源码参考:github链接(https://github.com/facebook/react)
官网文档:文档链接(https://reactjs.org/docs/getting-started.html)
创建react项目的脚手架:
create react app(webpack封装,用于react项目的快速开发)
源码分析: 博客链接
相关UI库:
https://github.com/reactstrap/reactstrap
https://github.com/mui/material-ui
如果用create-react-app脚手架创建的react项目,我们可以看到:react,react-dom,react-script被默认加入了package.json的依赖包,而最新版默认导入了测试包testing-library。
对于react-script,默认帮我们写好了start,test,build,eject等命令帮助你处理编译,打包等和webpack相关的操作。
create-react-app:会下载最新的react-script, 然后它将所有设置委托给 react-scripts,当你运行 create-react-app 时,它始终使用最新版本的 react-scripts 创建项目,以便你自动获得新创建的应用程序中的所有新功能和改进。
react-script 源码参考
我们如果想自己写一个脚手架,可以参考他的写法。
{
"name": "react-test",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^13.2.1",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "5.0.0",
"web-vitals": "^2.1.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
原理及核心思想
react
函数式组件思想 当你 setstate
就会遍历 diff
当前组件所有的子节点子组件, 这种方式开销是很大的, 所以 react 16
采用了 fiber
链表代替之前的树,可以中断的,分片的在浏览器空闲时候执行
jsx是React.createElement的语法糖,jsx通过babel转化成React.createElement函数,React.createElement执行之后返回jsx对象,也叫virtual-dom,Fiber会根据jsx对象和current Fiber进行对比形成workInProgress Fiber
源码调试(不必理解细节,大致在心里有个链路)
// App.js
import React from "react";
export default class App extends React.Component {
render() {
return (<div>hello</div>)
}
}
// index.js
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
babel会将所有的jsx语法糖转成element对象,入口是index文件的ReactDOM.render,打个断点(react-dom包),可以看到传入的element已经变成对象了。
1)APP render调试如下:
- ReactDOM.render
- legacyRenderSubtreeIntoContainer
- legacyCreateRootFromDOMContainer // 生成根节点容器对象
- createLegacyRoot
- createRootImpl
- createContainer
- createFiberRoot // 创建fiberRoot节点
- initializeUpdateQueue // 创建updateQueue,并放在fiber节点上
- updateContainer
- createUpdate
- enqueueUpdate // 初始时创建环形链
- scheduleUpdateOnFiber
- performSyncWorkOnRoot
- renderRootSync
do { try { workLoopSync(); break; } catch (thrownValue) { handleError(root, thrownValue); } } while (true);
- workLoopSync
function workLoopSync() { // Already timed out, so perform work without checking if we need to yield. while (workInProgress !== null) { performUnitOfWork(workInProgress); } }
- performUnitOfWork // 执行工作单元(Fiber-链式结构)performUnitOfWork 负责对 Fiber 进行操作,并按照深度遍历的顺序返回下一个 Fiber。两层循环,一个从上往下,一个从下往上;
因为使用了链表结构,每个节点存了各种状态和数据,即使处理流程被中断了,我们随时可以从上次未处理完的 Fiber 继续遍历下去。- beginWork
- processUpdateQueue // 更新updateQueue里面的next指向
- reconcileChildren // 虚拟dom diff
- 通过next判断是否还有后续
- completeUnitOfWork
- 通过returnFiber判断当前unit是否全部完成
- beginWork
function performUnitOfWork(unitOfWork) { // The current, flushed, state of this fiber is the alternate. Ideally // nothing should rely on this, but relying on it here means that we don't // need an additional field on the work in progress. var current = unitOfWork.alternate; setCurrentFiber(unitOfWork); var next; if ( (unitOfWork.mode & ProfileMode) !== NoMode) { startProfilerTimer(unitOfWork); next = beginWork$1(current, unitOfWork, subtreeRenderLanes); stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true); } else { next = beginWork$1(current, unitOfWork, subtreeRenderLanes); } resetCurrentFiber(); unitOfWork.memoizedProps = unitOfWork.pendingProps; if (next === null) { // If this doesn't spawn new work, complete the current work. completeUnitOfWork(unitOfWork); } else { workInProgress = next; } ReactCurrentOwner$2.current = null; }
- commitRoot
2)ClassComponent -> setState调试如下
Component.prototype.setState = function (partialState, callback) {
if (!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) {
{
throw Error( "setState(...): takes an object of state variables to update or a function which returns an object of state variables." );
}
}
this.updater.enqueueSetState(this, partialState, callback, 'setState');
};
- enqueueSetState // 将当前更新的element转成fiber
- createUpdate
- enqueueUpdate
- scheduleUpdateOnFiber
- performSyncWorkOnRoot
- renderRootSync
- workLoopSync
- performUnitOfWork
- beginWork
- completeUnitOfWork
- performUnitOfWork
3)FunctionComponent -> useState
const [state, setState] = useState(0)
第一次render,大致的调用链如下:
- useState
- mountState
- mountWorkInProgressHook(初始化当前fiber的memoizedState)
- 将初始的state和内部封装的dispatchAction作为返回,
function dispatchAction(fiber, queue, action)当前的工作的fiber自动绑定到第一个fiber参数,hook的queue绑定到第二个queue参数,暴露一个action给用户自定义,这个action也就是我们上面那个setState的入参,可以是具体值,可以是一个方法。
当用户点击h1时,触发setState,也就是dispatchAction方法,把具体的action传入,在dispatchAction中封装update,继续触发scheduleUpdateOnFiber,之后调用一样。
useState: function (initialState) {
currentHookNameInDev = 'useState';
mountHookTypesDev();
var prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnMountInDEV;
try {
return mountState(initialState);
} finally {
ReactCurrentDispatcher$1.current = prevDispatcher;
}
},
function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState
};
var dispatch = queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber$1, queue);
return [hook.memoizedState, dispatch];
}
function mountWorkInProgressHook() {
var hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber$1.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
function dispatchAction(fiber, queue, action) {
{
if (typeof arguments[3] === 'function') {
error("State updates from the useState() and useReducer() Hooks don't support the " + 'second callback argument. To execute a side effect after ' + 'rendering, declare it in the component body with useEffect().');
}
}
var eventTime = requestEventTime();
var lane = requestUpdateLane(fiber);
var update = {
lane: lane,
action: action,
eagerReducer: null,
eagerState: null,
next: null
}; // Append the update to the end of the list.
var pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
var alternate = fiber.alternate;
if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) {
// This is a render phase update. Stash it in a lazily-created map of
// queue -> linked list of updates. After this render pass, we'll restart
// and apply the stashed updates on top of the work-in-progress hook.
didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
} else {
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
// The queue is currently empty, which means we can eagerly compute the
// next state before entering the render phase. If the new state is the
// same as the current state, we may be able to bail out entirely.
var lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
var prevDispatcher;
{
prevDispatcher = ReactCurrentDispatcher$1.current;
ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
}
try {
var currentState = queue.lastRenderedState;
var eagerState = lastRenderedReducer(currentState, action); // Stash the eagerly computed state, and the reducer used to compute
// it, on the update object. If the reducer hasn't changed by the
// time we enter the render phase, then the eager state can be used
// without calling the reducer again.
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (objectIs(eagerState, currentState)) {
// Fast path. We can bail out without scheduling React to re-render.
// It's still possible that we'll need to rebase this update later,
// if the component re-renders for a different reason and by that
// time the reducer has changed.
return;
}
} catch (error) {// Suppress the error. It will throw again in the render phase.
} finally {
{
ReactCurrentDispatcher$1.current = prevDispatcher;
}
}
}
}
{
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
warnIfNotScopedWithMatchingAct(fiber);
warnIfNotCurrentlyActingUpdatesInDev(fiber);
}
}
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
{
markStateUpdateScheduled(fiber, lane);
}
}
接下来的render的,进入useState会走如下逻辑
-
useState
- updateState
-
updateReducer
- 返回新的state和老的dispatcher
function updateReducer(reducer, initialArg, init) {
var hook = updateWorkInProgressHook();
var queue = hook.queue;
if (!(queue !== null)) {
{
throw Error( "Should have a queue. This is likely a bug in React. Please file an issue." );
}
}
queue.lastRenderedReducer = reducer;
var current = currentHook; // The last rebase update that is NOT part of the base state.
var baseQueue = current.baseQueue; // The last pending update that hasn't been processed yet.
var pendingQueue = queue.pending;
if (pendingQueue !== null) {
// We have new updates that haven't been processed yet.
// We'll add them to the base queue.
if (baseQueue !== null) {
// Merge the pending queue and the base queue.
var baseFirst = baseQueue.next;
var pendingFirst = pendingQueue.next;
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
{
if (current.baseQueue !== baseQueue) {
// Internal invariant that should never happen, but feasibly could in
// the future if we implement resuming, or some form of that.
error('Internal error: Expected work-in-progress queue to be a clone. ' + 'This is a bug in React.');
}
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
if (baseQueue !== null) {
// We have a queue to process.
var first = baseQueue.next;
var newState = current.baseState;
var newBaseState = null;
var newBaseQueueFirst = null;
var newBaseQueueLast = null;
var update = first;
do {
var updateLane = update.lane;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
// update/state.
var clone = {
lane: updateLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: null
};
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
} // Update the remaining priority in the queue.
// TODO: Don't need to accumulate this. Instead, we can remove
// renderLanes from the original lanes.
currentlyRenderingFiber$1.lanes = mergeLanes(currentlyRenderingFiber$1.lanes, updateLane);
markSkippedUpdateLanes(updateLane);
} else {
// This update does have sufficient priority.
if (newBaseQueueLast !== null) {
var _clone = {
// This update is going to be committed so we never want uncommit
// it. Using NoLane works because 0 is a subset of all bitmasks, so
// this will never be skipped by the check above.
lane: NoLane,
action: update.action,
eagerReducer: update.eagerReducer,
eagerState: update.eagerState,
next: null
};
newBaseQueueLast = newBaseQueueLast.next = _clone;
} // Process this update.
if (update.eagerReducer === reducer) {
// If this update was processed eagerly, and its reducer matches the
// current reducer, we can use the eagerly computed state.
newState = update.eagerState;
} else {
var action = update.action;
newState = reducer(newState, action);
}
}
update = update.next;
} while (update !== null && update !== first);
if (newBaseQueueLast === null) {
newBaseState = newState;
} else {
newBaseQueueLast.next = newBaseQueueFirst;
} // Mark that the fiber performed work, but only if the new state is
// different from the current state.
if (!objectIs(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
var dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
基本过程已经大致理解,接下来核心就是弄懂这个Fiber是怎么工作的。
Fiber的结构
是自定义的一种链式存储结构
export type Fiber = {
tag: WorkTag, // 标记不同的组件类型
Key: null | string, // 组件的key
elementType: any, // ReactElement.type,也就是我们调用`createElement`的第一个参数
type: any, // // 异步组件resolved之后返回的内容,一般是`function`或者`class`
stateNode: any, // // 保存组件的类实例、DOM节点或与Fiber节点关联的其他 React 元素类型的引用,跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
// 链表结构
return: Fiber | null, // 指向父节点,或者render该节点的组件
child: Fiber | null, // 指向第一个子节点
sibling: Fiber | null, // 指向下一个兄弟节点
index: number,
// 最后用于附加此节点的 ref
ref:
| null
| (((handle: mixed) => void) & {_stringRef: ?string, ...})
| RefObject,
// 保存状态和依赖
pendingProps: any, // 新的变动带来的新的props
memoizedProps: any, // 上一次渲染完成之后的props
memoizedState: any, // 上一次渲染的时候的state
dependencies: Dependencies | null,//一个列表,存放这个Fiber依赖的contexts, events
// effect 也用链表关联
flags: Flags,
subtreeFlags: Flags,
deletions: Array<Fiber> | null,
nextEffect: Fiber | null, // 单链表用来快速查找下一个 effect
firstEffect: Fiber | null, // 子树中第一个effect
lastEffect: Fiber | null, // 子树中最后一个side effect
lanes: Lanes,
childLanes: Lanes,
// 其他
mode: TypeOfMode, // 共存的模式表示这个子树是否默认是异步渲染的
updateQueue: UpdateQueue<any> | null, //该Fiber对应的组件产生的Update会存放在这个队列里面
expirationTime: ExpirationTime, // 代表任务在未来的哪个时间点应该被完成
childExpirationTime: ExpirationTime // 快速确定子树中是否有不在等待的超时的变化
alternate: Fiber | null, // WIP树中对应的fiber节点,渲染完成后会交换位置
}
链表结构 return, child, sibling
react相关技术栈
- react-redux
- react-router
- react-router-dom
react相关组件库