React原理分析及总结

React

源码参考:github链接(https://github.com/facebook/react)

官网文档:文档链接(https://reactjs.org/docs/getting-started.html)

创建react项目的脚手架:

create react app(webpack封装,用于react项目的快速开发)

umi(结合了路由功能)

源码分析: 博客链接

相关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是否全部完成

        • 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

3)FunctionComponent -> useState

const [state, setState] = useState(0)

<h1 onClick={() => {setState(state + 1)}}>
<h1 onClick={() => {setState(() => state+ 1);}}>
 
useState分为第一次render和之后的update两种场景:

第一次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相关技术栈

  1. react-redux
  2. react-router
  3. react-router-dom

react相关组件库

  1. MUI
  2. AntD
  3. react-hook-form