最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React-从源码开始(一)

    正文概述 掘金(SiroSongs)   2021-04-30   514

    作为一名前端小白,会想起第一次debug React组件时的手足无措,在惊叹这个框架的奇妙之余也更加觊觎其中的道理。

    万物伊始——react准备工作

    每个react的web应用想要展示到页面上的第一步都需要执行渲染函数ReactDOM.createRoot(document.getElementById('root')).render(<App />) ,而它都做了些什么呢?

    首先我们看到的是ReactDOM调用的createRoot方法,它在react内部调用createRoot方法返回一个React容器对象

    export function createRoot(
      container: Container,
      options?: RootOptions,
    ): RootType {
      return new ReactDOMRoot(container, options);
    }
    

    参数中的container,也就是我们通过document.getElementById('root')获取的dom对象,options这个参数,它主要用在服务端渲染流程中,所以现在为空。

    再看,这个函数中返回的是ReactDOMRoot构造函数生成的对象,而ReactDOMRoot又是干什么呢?

    function ReactDOMRoot(container: Container, options: void | RootOptions) {
      this._internalRoot = createRootImpl(container, ConcurrentRoot, options);
    }
    

    它为我们创建的React容器对象的_internalRoot属性通过createRootImpl函数的返回值赋值,他又是做什么的呢。

    function createRootImpl(
      container: Container,
      tag: RootTag,
      options: void | RootOptions,
    ) {
      ...
      // 创建FiberRoot对象
      const root = createContainer(container, tag, hydrate, hydrationCallbacks);
      // 标记一下FiberRoot对象
      markContainerAsRoot(root.current, container);
      // 获取整个应用的挂在的那个元素
      const rootContainerElement =
        container.nodeType === COMMENT_NODE ? container.parentNode : container;
      // 注册事件代理
      listenToAllSupportedEvents(rootContainerElement);
      ...
      return root;
    }
    

    首先我们看到这个函数的参数由之前的两个变成了三个,中间的tag参数是用来标志react的工作模式。自从React16之后, react引入了Fiber,工作模式也由之前的一种变成两种,随之改变的是在react应用挂载时执行的渲染函数也由一种变成了三种。

    ReactDOM.render(<App />, document.getElementById('root'))
    ReactDOM.createBlockingRoot(document.getElementById('root')).render(<App />)
    ReactDOM.createRoot(document.getElementById('root')).render(<App />)
    

    第一种还是原始默认,其工作方式也是同步方式,而剩下两种则是使用新版的并行工作方式。 而它们之间就是通过刚才提到的tag参数进行区分的,而createBlockingRootcreateRoot之间借用官方的解释:

    export type RootTag = 0 | 1 | 2;
    
    export const LegacyRoot = 0;
    export const BlockingRoot = 1;
    export const ConcurrentRoot = 2;
    

    进入这个函数,最主要的就是做了两件事

    1. 生成FiberRoot对象。
    2. 注册事件代理。

    我们先看第一步,通过调用createContainer函数生成root并最终返回这个root,这也就是_internalRoot的值。

    export function createContainer(
      containerInfo: Container,
      tag: RootTag,
      hydrate: boolean,
      hydrationCallbacks: null | SuspenseHydrationCallbacks,
    ): OpaqueRoot {
      // 创建并返回FiberRoot
      return createFiberRoot(containerInfo, tag, hydrate, hydrationCallbacks);
    }
    

    createContainer中又调用了createFiberRoot

    export function createFiberRoot(
      containerInfo: any,
      tag: RootTag,
      hydrate: boolean,
      hydrationCallbacks: null | SuspenseHydrationCallbacks,
    ): FiberRoot {
      // 实例化一个FiberRoot对象
      const root: FiberRoot = (new FiberRootNode(containerInfo, tag, hydrate): any);
      ...
      // 创建一个当前组件树的根节点HostRootFiber
      const uninitializedFiber = createHostRootFiber(tag);
      // 将这个HostRootFiber对象作为FiberRoot对象current属性的引用
      root.current = uninitializedFiber;
      // 这个FiberRoot对象的stateNode属性指向容器对象
      uninitializedFiber.stateNode = root;
      // 初始化更新队列
      initializeUpdateQueue(uninitializedFiber);
      return root;
    }
    

    首先,调用FiberRootNode函数实例化root对象,这个函数会返回一个RootNode对象,赋值给root并最终也返回它。

    而之后调用createHostRootFiber创建HostRootFiber对象,赋值给uninitializedFiber 这里的createHostRootFiber函数最终返回的是一个Fiber对象。而传给createHostRootFiber的参数是当前工作模式的标志。

    此时将uninitializedFiber赋值到root.current属性上,作为当前的组件树的根节点。

    接下来初始化更新队列,调用initializeUpdateQueue函数

    export function initializeUpdateQueue<State>(fiber: Fiber): void {
      const queue: UpdateQueue<State> = {
        baseState: fiber.memoizedState,
        firstBaseUpdate: null,
        lastBaseUpdate: null,
        shared: {
          pending: null,
        },
        effects: null,
      };
      // 为fiber节点也就是当前组件树的根节点的updateQueue属性赋初始值
      fiber.updateQueue = queue;
    }
    

    最后返回root,返回并结束我们在createRootImpl中的第一步生成FiberRoot对象。

    接下来执行listenToAllSupportedEvents,参数为依然是通过document.getElementById('root')获取的dom对象

    const listeningMarker =
      '_reactListening' +
      Math.random()
        .toString(36)
        .slice(2);
    
    export function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
      // 当(rootContainerElement: any)[listeningMarker]不为true时向下执行
      if ((rootContainerElement: any)[listeningMarker]) {
        // 优化:在相同的portal容器或者根节点只迭代一次
        // 一旦移除这个标记,我们也许还可以删除一些用于懒加载的簿记图。
        return;
      }
      // 设置(rootContainerElement: any)[listeningMarker]为true,确保不再重复执行。
      (rootContainerElement: any)[listeningMarker] = true;
      // 为所有的原生事件添加监听
      allNativeEvents.forEach(domEventName => {
        // 判断当前原生事件是否在不需要代理监听的事件列表里,如果不在则执行
        if (!nonDelegatedEvents.has(domEventName)) {
          listenToNativeEvent(
            domEventName,
            false,
            ((rootContainerElement: any): Element),
            null,
          );
        }
        listenToNativeEvent(
          domEventName,
          true,
          ((rootContainerElement: any): Element),
          null,
        );
      });
    }
    
    export function listenToNativeEvent(
      domEventName: DOMEventName,
      isCapturePhaseListener: boolean,
      rootContainerElement: EventTarget,
      targetElement: Element | null,
      eventSystemFlags?: EventSystemFlags = 0,
    ): void {
      let target = rootContainerElement;
      // selectionchange需要附加到文档上,否则它将无法捕获仅直接在文档上触发的传入事件。
      if (
        domEventName === 'selectionchange' &&
        (rootContainerElement: any).nodeType !== DOCUMENT_NODE
      ) {
        target = (rootContainerElement: any).ownerDocument;
      }
      // If the event can be delegated (or is capture phase), we can
      // register it to the root container. Otherwise, we should
      // register the event to the target element and mark it as
      // a non-delegated event.
      if (
        targetElement !== null &&
        !isCapturePhaseListener &&
        nonDelegatedEvents.has(domEventName)
      ) {
        // 对于所有未委派的事件,除了滚动之外,还将其事件侦听器附加到其事件触发的各个元
        // 素上。 这意味着我们可以跳过此步骤,因为以前已经添加了事件侦听器。 但是,我们对滚
        // 动事件进行了特殊处理,因为现实是任何元素都可以滚动。
        if (domEventName !== 'scroll') {
          return;
        }
        eventSystemFlags |= IS_NON_DELEGATED;
        target = targetElement;
      }
      // 创建或者获取事件代理集合
      const listenerSet = getEventListenerSet(target);
      // 创建代理事件名称并标记这个事件是捕获还是冒泡
      const listenerSetKey = getListenerSetKey(
        domEventName,
        isCapturePhaseListener,
      );
      // 如果是新增事件或者更新事件
      if (!listenerSet.has(listenerSetKey)) {
        if (isCapturePhaseListener) {
          eventSystemFlags |= IS_CAPTURE_PHASE;
        }
        // 添加绑定事件
        addTrappedEventListener(
          target,
          domEventName,
          eventSystemFlags,
          isCapturePhaseListener,
        );
        // 将新增或更新事件添加到事件集合中
        listenerSet.add(listenerSetKey);
      }
    }
    

    至此,我们完成了万里长征的第一步,react在渲染更新前的准备工作

    1. 初始化根节点
    2. 初始化事件代理

    万事开头难,然后中间难,最后会更难。每天挪一小步,光就会出现。


    起源地下载网 » React-从源码开始(一)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元