【react】读源码笔记

JSX(转成虚拟dom)

JSX通过Babel编译时会被编译成React.createElement

// 编译前
<div className="content">
    <h3>Hello React</h3>
    <p>React is great</p>
</div>
// 编译后
React.createElement(
    'div',
    {
        className: 'content'
    },
    React.createElement('h3', null, 'Hello World'),
    React.createElement('p', null, 'React is greate')
)

React.createElement代表一个节点元素,第一个参数是节点的名称,第二个是节点的属性,后面的参数都是子节点。
我们可以自己在babeljs.is网站试验。React.createElement就是用来创建虚拟DOM的,返回的就是一个虚拟DOM对象。React再将虚拟DOM转换为真实DOM显示到页面中。

jsx在运行时会被Babel转换为React.createElement对象,React.createElement会被React转换成虚拟DOM对象,虚拟DOM对象会被React转换成真实DOM对象。

JSX语法的出现就是为了让React开发人员编写用户界面代码更加轻松。

createElement

createElement方法接收type, props, childrens三个参数。分别表示标签类型,标签属性和标签子元素。在这个方法中要返回一个虚拟DOM对象,在这个对象中有个type属性其实就是参数传入的值,接着是props和children。

/**
 * 创建 React Element
 * type      元素类型
 * config    配置属性
 * children  子元素
 * 1. 分离 props 属性和特殊属性
 * 2. 将子元素挂载到 props.children 中
 * 3. 为 props 属性赋默认值 (defaultProps)
 * 4. 创建并返回 ReactElement
 */
export function createElement(type, config, children) {
  /**
   * propName -> 属性名称
   * 用于后面的 for 循环
   */
  let propName;

  /**
   * 存储 React Element 中的普通元素属性 即不包含 key ref self source
   */
  const props = {};

  /**
   * 待提取属性
   * React 内部为了实现某些功能而存在的属性
   */
  let key = null;
  let ref = null;
  let self = null;
  let source = null;

  // 如果 config 不为 null
  if (config != null) {
    // 如果 config 对象中有合法的 ref 属性
    if (hasValidRef(config)) {
      // 将 config.ref 属性提取到 ref 变量中
      ref = config.ref;
      
    }
    // 如果在 config 对象中拥有合法的 key 属性
    if (hasValidKey(config)) {
      // 将 config.key 属性中的值提取到 key 变量中
      key = '' + config.key;
    }

    self = config.__self === undefined ? null : config.__self;
    source = config.__source === undefined ? null : config.__source;
    // 遍历 config 对象
    for (propName in config) {
      // 如果当前遍历到的属性是对象自身属性
      // 并且在 RESERVED_PROPS 对象中不存在该属性
    /**
    * const RESERVED_PROPS = {
      *     key: true,
      *     ref: true,
      *     _self: true,
      *     _source:true 
      *}
    */
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        // 将满足条件的属性添加到 props 对象中 (普通属性)
        props[propName] = config[propName];
      }
    }
  }

  /**
   * 将第三个及之后的参数挂载到 props.children 属性中
   * 如果子元素是多个 props.children 是数组
   * 如果子元素是一个 props.children 是对象
   */

  // 由于从第三个参数开始及以后都表示子元素
  // 所以减去前两个参数的结果就是子元素的数量
  const childrenLength = arguments.length - 2;
  // 如果子元素的数量是 1
  if (childrenLength === 1) {
    // 直接将子元素挂载到到 props.children 属性上
    // 此时 children 是对象类型
    props.children = children;
    // 如果子元素的数量大于 1
  } else if (childrenLength > 1) {
    // 创建数组, 数组中元素的数量等于子元素的数量
    const childArray = Array(childrenLength);
    // 开启循环 循环次匹配子元素的数量
    for (let i = 0; i < childrenLength; i++) {
      // 将子元素添加到 childArray 数组中
      // i + 2 的原因是实参集合的前两个参数不是子元素
      childArray[i] = arguments[i + 2];
    }
  
    
    // 将子元素数组挂载到 props.children 属性中
    props.children = childArray;
  }

  /**
   * 如果当前处理是组件
   * 看组件身上是否有 defaultProps 属性
   * 这个属性中存储的是 props 对象中属性的默认值
   * 遍历 defaultProps 对象 查看对应的 props 属性的值是否为 undefined
   * 如果为undefined 就将默认值赋值给对应的 props 属性值
   */

  // 将 type 属性值视为函数 查看其中是否具有 defaultProps 属性
  if (type && type.defaultProps) {
    // 将 type 函数下的 defaultProps 属性赋值给 defaultProps 变量
    const defaultProps = type.defaultProps;
    // 遍历 defaultProps 对象中的属性 将属性名称赋值给 propName 变量
    for (propName in defaultProps) {
      // 如果 props 对象中的该属性的值为 undefined
      if (props[propName] === undefined) {
        // 将 defaultProps 对象中的对应属性的值赋值给 props 对象中的对应属性的值
        props[propName] = defaultProps[propName];
      }
    }
  }

 

  // 如果处于开发环境
  
  }
  // 返回 ReactElement
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    // 在 Virtual DOM 中用于识别自定义组件
    ReactCurrentOwner.current,
    props,
  );
}

代码看似很多,其实逻辑非常清晰:

处理参数,对传进来的数据进行加工处理,比如提取config参数,处理props等
调用真正的创建虚拟DOM的APIReactElement创建ReactNode
数据加工部分分为三步:

  • 第一步,判断config有没有传,不为null就做处理
    判断ref、key,__self、__source这些是否存在或者有效,满足条件就分别赋值给前面新建的变量。
    遍历config,并将config自身的属性依次赋值给前面新建props。
  • 第二步,处理子元素。默认从第三个参数开始都是子元素。
    如果子元素只有一个,直接赋值给props.children。
    如果子元素有多个,转成数组后再赋值给props.children。
  • 第三步,处理默认属性defaultProps
    一个纯粹的标签也可以理解成一个最最最基础的组件,而组件支持 defaultProps,所以这一步判断有没有defaultProps,如果有同样遍历,并将值不为undefined的部分都拷贝到props对象上。

ReactElement

/**
 * ReactElement 就是虚拟节点的概念
 * @param {*} key 虚拟节点的唯一标识,后期可以进行优化
 * @param {*} type 虚拟节点类型,type可能是字符串('div', 'span'),也可能是一个function,function时为一个自定义组件
 * @param {*} props 虚拟节点的属性
 */
function ReactElement(type, key, ref, self, source, owner, props) {
   const element = {
    // 这个标签允许我们将其标识为唯一的React Element
    $$typeof: REACT_ELEMENT_TYPE,
    // 元素的内置属性
    type: type,
    key: key,
    ref: ref,
    props: props,
    // 记录负责创建此元素的组件。
    _owner: owner,
  };
  return element

}

这里提一下REACT_ELEMENT_TYPE,他的实现是:

export const REACT_ELEMENT_TYPE = Symbol.for(‘react.element’);

$$typeof定义为Symbol(react.element),而Symbol一大特性就是标识唯一性,即便两个看着一模一样的Symbol,它们也不会相等。而react之所以这样做,本质也是为了防止xss攻击,防止外部伪造虚拟dom结构。

JSX与Fiber节点

从上面的内容我们可以发现,JSX是一种描述当前组件内容的数据结构,他不包含组件schedule、reconcile、render所需的相关信息。

比如如下信息就不包括在JSX中:

  • 组件在更新中的优先级
  • 组件的state
  • 组件被打上的用于Renderer的标记
  • 这些内容都包含在Fiber节点中。

所以,在组件mount时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。

在update时,Reconciler将JSX与Fiber节点保存的数据对比,生成组件对应的Fiber节点,并根据对比结果为Fiber节点打上标记

架构

Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
Reconciler(协调器)—— 负责找出变化的组件
Renderer(渲染器)—— 负责将变化的组件渲染到页面上

概念

调度器(Scheduler)

既然我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。Scheduler还提供了多种调度优先级供任务设置。

协调器(Reconciler)

Reconciler内部采用了Fiber的架构,Reconciler与Renderer不再是交替工作。当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记,类似这样:

export const Placement = /*             */ 0b0000000000010;
export const Update = /*                */ 0b0000000000100;
export const PlacementAndUpdate = /*    */ 0b0000000000110;
export const Deletion = /*              */ 0b0000000001000;

整个Scheduler与Reconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。

渲染器(Renderer)

Renderer根据Reconciler为虚拟DOM打的标记,同步执行对应的DOM操作

在这里插入图片描述
在这里插入图片描述

渲染阶段render

function App() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button >click me</button>
      </div>
    )
}
ReactDOM.render(
  <App />,
  document.getElementById('root')
);
“递”阶段

首先从rootFiber开始向下深度优先遍历。为遍历到的每个Fiber节点调用beginWork方法 (opens new window)。

该方法会根据传入的Fiber节点创建子Fiber节点,并将这两个Fiber节点连接起来。

当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。

“归”阶段

在“归”阶段会调用completeWork (opens new window)处理Fiber节点。

当某个Fiber节点执行完completeWork,如果其存在兄弟Fiber节点(即fiber.sibling !== null),会进入其兄弟Fiber的“递”阶段。

如果不存在兄弟Fiber,会进入父级Fiber的“归”阶段。

“递”和“归”阶段会交错执行直到“归”到rootFiber。至此,render阶段的工作就结束了。

在这里插入图片描述
render阶段会依次执行:

第一步:FiberRoot beignwork
第二步:App Fiber beignWork
第三步:div Fiber beignWork
第四步:p Fiber beignWork
第五步:因为p节点之后文本子节点,react会进行优化,直接进行completeUnitOfWork阶段,完成后将workInProgress设置为button fiber
第六步:button Fiber beignWork
第七步:同第五步p节点的优化,button会直接completeUnitOfWork阶段
第八步:div completeUnitOfWork
第九步:App completeUnitOfWork
第十步:FiberRoot completeUnitOfWork

  • beginWork()为捕获阶段,此阶段会采取深度优先的方式遍历节点,并完成Fiber树创建以及diff算法。
  • completeUnitOfWork()为冒泡阶段,此阶段要完成生命周期(部分)的调用,形成effectlist等

React 源码之render捕获阶段

React 源码之render冒泡阶段

React 源码之commit阶段
在commit阶段是同步执行的,不可以被打断,也就是说所有的更新需要一次性完成。

在这里插入图片描述
来自 react 解析

协调:将虚拟DOM与真实DOM的状态进行同步,是一个使一致的过程

Diff:判断要删除、新建、移动的节点,是一个找不同的过程

Reconciler != Diff,但是一般说调和(协调)就是指的Diff算法,因为Diff算法确实是调和过程最具代表性的一环

双缓存Fiber树

什么是“双缓存
这种在内存中构建并直接替换的技术叫做双缓存 (opens new window)。

React使用“双缓存”来完成Fiber树的构建与替换——对应着DOM树的创建与更新

在React中最多会同时存在两棵Fiber树。当前屏幕上显示内容对应的Fiber树称为current Fiber树,正在内存中构建的Fiber树称为workInProgress Fiber树。

current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;

React应用的根节点通过使current指针在不同Fiber树的rootFiber间切换来完成current Fiber树指向的切换。

即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。

每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。

接下来我们以具体例子讲解mount时、update时的构建/替换流程。

mount时

首次执行ReactDOM.render会创建fiberRootNode(源码中叫fiberRoot)和rootFiber。其中fiberRootNode是整个应用的根节点,rootFiber是所在组件树的根节点。

之所以要区分fiberRootNode与rootFiber,是因为在应用中我们可以多次调用ReactDOM.render渲染不同的组件树,他们会拥有不同的rootFiber。但是整个应用的根节点只有一个,那就是fiberRootNode。

fiberRootNode的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber树

在这里插入图片描述

beginWork(递)

从传参看方法执行
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // ...省略函数体
}

其中传参:

  • current:当前组件对应的Fiber节点在上一次更新时的Fiber节点,即workInProgress.alternate
  • workInProgress:当前组件对应的Fiber节点
  • renderLanes:优先级相关,在讲解Scheduler时再讲解

我们知道,除rootFiber以外, 组件mount时,由于是首次渲染,是不存在当前组件对应的Fiber节点在上一次更新时的Fiber节点,即mount时current === null。

组件update时,由于之前已经mount过,所以current !== null。

所以我们可以通过current === null ?来区分组件是处于mount还是update。
在这里插入图片描述

基于此原因,beginWork的工作可以分为两部分:

mount:

mount时除fiberRootNode以外,current === null
根据workInProgress.tag判断需要创建的Fiber节点类型,并且创建相应的Fiber节点。我们可以从这里看到tag的所有类型。对于我们常见的组件类型,如FuctionComponent,ClassComponent,HostComponent,最终会进入reconcileChildren方法。

update

着另一条线路来看看update阶段。在该阶段,首先通过对传入的两个Fiber节点的type和props进行比较,判断该节点是否可以复用。若不可以复用,则把他当成新的Fiber节点看待,走类似mount阶段的流程,最后生成带effecTag的新的Fiber节点;若可以复用,则检查子树是否需要更新。若子树需要更新,则执行cloneChildFibers并且返回子节点;否则直接返回null.

reconcileChildren

从该函数名就能看出这是Reconciler模块的核心部分。它对于从不同路径来的Fiber节点做了不同的事情:

  • 对于mount组件,他会创建新的Fiber节点
  • 对于update组件,他会将当前组件于该节点上次更新时对应的Fiber节点(Diff算法)进行比较,将比较的结果生成新的Fiber节点
export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes
) {
  if (current === null) {
    // 对于mount的组件
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 对于update的组件
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}
effectTag(做具体标记)

我们知道,render阶段的工作是在内存中进行,当工作结束后会通知Renderer需要执行的DOM操作。要执行DOM操作的具体类型就保存在fiber.effectTag中

completeUnitOfWork(归)

和beginWork一样,它也是通过判断current === null?来判断mount阶段和update阶段。

update

当update时,Fiber节点已经存在对应DOM节点,所以不需要生成DOM节点。需要做的主要是处理props,比如:

  • onClick、onChange等回调函数的注册
  • 处理style prop
  • 处理DANGEROUSLY_SET_INNER_HTML prop
  • 处理children prop
  • 我们去掉一些当前不需要关注的功能(比如ref)。可以看到最主要的逻辑是调用updateHostComponent方法。

在updateHostComponent内部,被处理完的props会被赋值给workInProgress.updateQueue,并最终会在commit阶段被渲染在页面上。

mount
  • 为Fiber节点生成对应的DOM节点
  • 将子孙DOM节点插入刚生成的DOM节点中
  • 与update逻辑中的updateHostComponent类似的处理props的过程
effectList

?:作为DOM操作的依据,commit阶段需要找到所有有effectTag的Fiber节点并依次执行effectTag对应操作。如果再遍历一次Fiber树是及其低效的操作,那么怎么办呢?

在completeWork的上层函数completeUnitOfWork中,每个执行完completeWork且存在effectTag的Fiber节点,会被保存在一条被称为effectList的单向链表中。

effectList中第一个Fiber节点白存在fiber.firstEffect,最后一个元素保存在fiber.lastEffect中,在归的阶段,所有有effectTag的Fiber节点都会被追加到effectList中,最终形成以rootFiber.firstEffect为起点的单向链表。这样,在commit阶段只需要遍历effectList九能执行所有的effect了。

提交阶段commit

在rootFiber.firstEffect上保存了一条需要执行副作用的Fiber节点的单向链表effectList,这些Fiber节点的updateQueue中保存了变化的props。

这些副作用对应的DOM操作在commit阶段执行。

除此之外,一些生命周期钩子(比如componentDidXXX)、hook(比如useEffect)需要在commit阶段执行。

commit阶段的主要工作(即Renderer的工作流程)分为三部分:

  • before mutation阶段(执行DOM操作前)

  • mutation阶段(执行DOM操作)

  • layout阶段(执行DOM操作后)

before mutation阶段

我们知道了在before mutation阶段,会遍历effectList,依次执行:

  1. 处理DOM节点渲染/删除后的 autoFocus、blur逻辑

  2. 调用getSnapshotBeforeUpdate生命周期钩子

  3. 调度useEffect

mutation阶段

mutation阶段会遍历effectList,依次执行commitMutationEffects。该方法的主要工作为“根据effectTag调用不同的处理函数处理Fiber

layout阶段

该阶段之所以称为layout,因为该阶段的代码都是在DOM渲染完成(mutation阶段完成)后执行的。layout阶段会遍历effectList,依次执行commitLayoutEffects。该方法的主要工作为“根据effectTag调用不同的处理函数处理Fiber并更新ref

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值