react17.x源码解析(1)——源码目录及react架构

react的源码目录如下,主要有三个文件夹:

  • fixtures:一些测试demo,方便react编码时的测试
  • packages: react的主要源码内容
  • script: 和react打包、编译、本地开发相关的命令

我们要探究的源码内容,都存放在packages文件夹下:

image

根据packages下面各个部分的功能,我将其划分为了几个模块:

核心 api
react的核心api都位于packages/react文件夹下,包括createElement、memo、context以及hooks等,凡是通过react包引入的api,都位于此文件夹下。

调度和协调
调度和协调是 react16 fiber出现后的核心功能,和他们相关的包如下:

  • scheduler:对任务进行调度,根据优先级排序
  • react-conciler:diff算法相关,对fiber进行副作用标记
    image

渲染
和渲染相关的内容包括以下几个目录:

  • react-art:canvas、svg等内容的渲染
  • react-dom:浏览器环境下的渲染,也是我们本系列中主要涉及讲解的渲染的包
  • react-native-renderer: 用于原生环境渲染相关
  • react-noop-renderer: 用于调试环境的渲染

辅助包

  • shared:定义了react的公共方法和变量
  • react-is:react中的类型判断

其他
其他的包和本次react源码探究的关联不是很多,不过多介绍。

react架构
react为了保证页面能够流畅渲染,react16之后的更新过程分为render和commit两个阶段。render阶段包括Scheduler(调度器)和Reconciler(协调器),commit阶段包括Renderer(渲染器):

image

触发更新
触发更新的方式主要有以下几种:ReactDOM.render(包括首次渲染)、setState、forUpdate、hooks中的useState以及ref的改变等引起的。

scheduler
当首次渲染或组件状态发生更新等情况时,此时页面就要发生渲染了。scheduler过程会对诸多的任务进行优先级排序,让浏览器的每一帧优先执行高优先级的任务(例如动画、用户点击输入事件等),从而防止react的更新任务太大影响到用户交互,保证了页面的流畅性。

reconciler
reconciler过程中,会开始根据优先级执行更新任务。这一过程主要是根据最新状态构建新的fiber树,与之前的fiber树进行diff对比,对fiber节点标记不同的副作用,对渲染过程中真实dom的增删改。

commit
在render阶段中,最终会生成一个effectList数组,记录了页面真实dom的新增、删除和替换等以及一些事件响应,commit会根据effectList对真实的页面进行更新,从而实现页面的改变。

jsx的转换
在React16版本及之前,应用程序通过@babel/preset-react将jsx语法转换为React.createElement的js代码,因此需要显示将React引入,才能正常调用createElement。
React17版本之后,官方与babel进行了合作,直接通过将react/jsx-runtime对jsx语法进行了新的转换而不依赖React.createElement,转换的结果便是可直接供ReactDOM.render使用的ReactElement对象。因此如果在React17版本后只是用jsx语法不使用其它的react提供的api,可以不引入React,应用程序依然能够正常运行。

React.createElement源码
虽然现在react17之后我们可以不再依赖React.createElement这个api了,但是实际场景中以及很多开源包中可能会有很多通过React.createElement手动创建元素的场景,所以推荐学习一下Reat.createElement源码。

React.createElement其接收三个或以上参数:

  • type:要创建的React元素类型,可以是标签名称字符串,如'div'或者'span'等;也可以说是React组件类型(class组件或者函数组件);或者是React fragment类型。
  • config:写在标签上的属性的集合,js对象格式,若标签上未添加任何属性则为null。
  • children:从第三个参数开始后的参数为当前创建的React元素的子节点,每个参数的类型,若是当前元素节点的textContent则为字符串类型;否则为新的React.createElement创建的元素。

函数中会对参数进行一系列的解析,源码如下,对源码相关的理解都用注释进行了标记:

export function createElement(type,config,children){
 let propName;
 //记录标签上的属性集合
 const props = {};
 let key = null;
 let ref = null;
 let self = null;
 let source = null;
 //config不为null时,说明标签上有属性,将属性添加到props中
 //其中,key和ref为react提供的特殊属性,不加入到props中,而是用key和ref单独记录
 if(config !=null){
 if(hasValidRef(config)){
 //有合法的ref时,则给ref赋值
 ref = config.ref;
 if(__DEV__){
 warnIfStringRefCannotBeAutoConverted(config);
 }
 }
 if(hasValidKey(config)){
 //有合法的key时,则给key赋值
 key = '' + config.key;
 }
 //self和source是开发环境下对代码在编译器中位置信息进行记录,用于开发环境下调试
 self = config.__self === undefined ? null : config.__self;
 source = config.__source === undefined ? null : config.__source;
 // 将config中除key、ref、__self、__source之外的属性添加到props中
 for(propName in config){
  if(
  hasOwnProperty.call(config,propName)&&
  !RESERVED_PROPS.hasOwnProperty(propName)
  ){
  props[propName] = config[propName];
  }
 }
 }
 //将子节点添加到props的children属性上
 const childrenLength = arguments.length -2;
 if(childrenLength===1){
 //共3个参数时表示只有一个子节点,直接将子节点赋值给props的children属性
 props.children = children;
 }else if (childrenLength > 1){
 //3个以上参数时表示有多个子节点,将子节点push到一个数组中然后将数组赋值给props的children
 const childArray = Array(childrenLength);
 for(let i=0;i<childrenLength;i++){
 childArray[i] = arguments[i+2];
 }
 //开发环境下冻结 childArray,防止被随意修改
 if(__DEV__){
  if(Object.freeze){
  Object.freeze(childArray);
  }
 }
 props.children = childArray;
 }
 //如果有defaultProps,对其遍历并且将用户在标签上未对其手动设置属性添加props中
 //此处针对class组件类型
 if(type && type.defaultProps){
  const defaultProps = type.defaultProps;
  for(propName in defaultProps){
   if(props[propName]===undefined){
    props[porpName] = defaultProps[propName];
   }
  }
 }
 // key 和 ref不挂载到prps上
 // 开发环境若想通过props.key 或者props.ref获取warning
 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);
	 }
   }
 }
 // 调用 ReactElement并返回
 return ReactElement(
 type,
 key,
 self,
 source,
 ReactCurrentOwner.current,
 props,
 )
}

由此可知,React.createElement 做的事情主要有:

  • 解析config参数中是否有合法的key、ref、__source和__self属性,若存在分别赋值给key、ref、source和self;将剩余的属性解析挂载到props上
  • 除type和config外后面的参数,挂载到props.children上
  • 针对类组件,如果type.defaultProps存在,遍历type.defaultProps的属性,如果props不存在该属性,则添加到props上
  • 将type、key、ref、self、props等信息,调用ReactElement等函数创建虚拟dom,ReactElement主要是在开发环境下通过Object.defineProperty将_store、_self、_source设置为不可枚举,提高element比较时的性能:
const ReactElement = function(type,key,ref,self,source,owner,props){
const element = {
//用于表示是否为ReactElement
&&typeof:REACT_ELEMENT_TYPE,

// 用于创建真实 dom 的相关信息
type:type,
key:key,
ref:ref,
props:props,

_owner:owner,
};
if(__DEV__){
element._store = {};
//开发环境下将_store、_self、_source设置为不可枚举,提高element的比较性能
  Object.defineProperty(element._store,'validated',{
  configurable:false,
  enumerable:false,
  writable:true,
  value:false,
  })
  Object.defineProperty(element, '_self', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: self,
    });

    Object.defineProperty(element, '_source', {
      configurable: false,
      enumerable: false,
      writable: false,
      value: source,
    });
    // 冻结 element 和 props,防止被手动修改
    if (Object.freeze) {
      Object.freeze(element.props);
      Object.freeze(element);
    }
  }

  return element;
};

所以通过流程图总结一下createElement所做的事情如下:
image

React.Component源码

function Component(props,context,updater){
	//接收props,context,updater进行初始化,挂载到this上
	this.props = props;
	this.context = context;
	this.refs = emptyObject;
	//updater 上挂载了isMounted、enqueueForceUpdate、enqueueSetState等触发器方法
	this.updater = updater || ReactNoopUpdateQueue;
}
//原型链上挂载 isReactComponent,在ReactDOM.render时用于和函数组件作区分
Component.prototype.isReactComponent = {};

//给类组件添加`this.setState`方法
Component.prototype.setState = function(partialState,callback){
//验证参数是否合法
 invariant(
 typeof partialState === 'object' || typeof partialState === 'function' || partialState == null
 );
 //添加至 enqueueSetState队列
 this.updater.enqueueSetState(this,partialState,callback,'setState');
};

// 给类组件添加 `this.forceUpdate`方法
Component.prototype.forceUpdate = function(callback){
//添加至 enqueueForceUpdate队列
this.updater.enqueueForceUpdate(this,callback,'forceUpdate');
}

从源码上可以得知,React.Component主要做了以下几件事情:

  • 将props,context,updater挂载到this上
  • 在Component原型链上添加isReactComponent对象,用于标记类组件
  • 在Component原型链上添加setState方法
  • 在Component原型链上添加forceUpdate方法

这样我们就理解了react类组件的super()作用,以及this.setState和this.forceUpdate的由来

总结
react17之后babel对jsx的转换就是比之前多了一步 React.createElement的动作:
image

通过babel及React.createElement,将jsx转换为了浏览器能识别的原生js语法,为react后续对状态改变、事件响应以及页面更新奠定了基础。

fiber节点结构

fiber是一种数据结构,每个fiber节点的内部,都保存了dom相关信息、fiber树相关的引用、要更新时的副作用等,我们可以看下源码中的fiber结构:

export type Fiber = {|
//作为静态数据结构,存储节点dom相关信息
tag:WorkTag,//组件的类型,取决于react的元素类型
key:null | string,
elementType:any,//元素类型
type:any,//定义与此fiber关联的功能或类。对于组件,他指向构造函数;对于DOM元素,他指定HTML tag
stateNode:any,//真实dom节点

// fiber链表树相关
return:Fiber|null,//父 fiber
child:Fiber|null,//第一个子fiber
sibling: Fiber | null,//下一个兄弟fiber
index:number,//在父fiber下面的子fiber中的下标

ref:
 |null
 |(((handle:mixed)=>void)&{_stringRef:?string,...})
 |RefObject,
 //工作单元,用于计算 state和props渲染
 pendingProps:any,//本次渲染需要使用props
 memoizedProps:any,//上次渲染使用的props
 updateQueue:mixed,//用于状态更新、回调函数、DOM更新的队列
 memoizedState:any,//上次渲染后的state状态
 dependencies:Dependencies | null, //contexts、events等依赖

 mode:TypeOfMode,
 
 //副作用相关
 flags:Flags,//记录更新时当前fiber的副作用(删除、更新、替换等)状态
 subtreeFlags:Flags, //当前子树的副作用状态
 deletions:Array<Fiber> | null, //要删除的子fiber
 nextEffet:Fiber | null,//下一个有副作用的fiber
 firstEffect:Fiber | null,//指向第一个有副作用的fiber
 lastEffectZ: Fiber | null,//指向最后一个有副作用的fiber
 
 //优先级相关
 lanes:Lanes,
 childLanes:Lanes,
 
 alternate:Fiber | null,//指向workInProgress fiber树中对应的节点
 
 actualDuration?:number,
 actualStartTime?:number,
 selfBaseDuration?:number,
 treeBaseDuration?:number,
 _debugID?:number,
 _debugSource?:Source | null,
 _debugOwner?:Fiber | null,
 _debugIsCurrentlyTiming?:boolean,
 _debugNeedsRemount?:bollean,
 _debugHookTypes?:Array<HooKType> | null,
|};

dom相关属性

fiber中和dom节点相关的信息主要关注tag、key、type、和stateNode。

tag

fiber中tag属性的ts类型为workType,用于标记不同的react组件类型,我们可以看一下源码中workType的枚举值;

//packages/react-reconciler/src/ReactWorkTags.js

export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent =2;
export const HostRoot = 3;
export const HostPortal = 4;
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const FundamentalComponent = 20;
export const ScopeComponent = 21;
export const Block = 22;
export const OffscreenComponent = 23;
export const LegacyHiddenComponent = 24;

在react协调时,beginWork和completeWork等流程时,都会根据tag类型的不同,去执行不同的函数处理fiber节点。

key和type
key和type两项用于react diff过程中确定fiber是否可以复用。
key为用户定义的唯一值。type定义与此fiber关联的功能或类。对于组件,他指向函数或者类本身;对于DOM元素,他指定HTML tag。

stateNode
stateNode用于记录当前fiber所对应的真实dom节点或者当前虚拟组件的实例,这么做的原因第一是为了实现Ref,第二是为了实现真实dom的跟踪。

链表树相关属性
我们看一下和fiber链表树构建相关的return、child和sibling几个字段:

  • return:指定父fiber,若没有父fiber则为null
  • child: 指向第一个子fiber,若没有任何子fiber则为null
  • sibling:指向下一个兄弟fiber,若没有下一个兄弟fiber则为null
    通过这几个字段,各个fiber节点构成了fiber链表树结构:
    image

副作用相关属性
首先理解一下react中的副作用,举一个生活中比较通俗的例子:我们感冒了本来吃点药就没事了,但是吃了药身体过敏了,而这个过敏就是副作用。react中,我们修改了state、props、ref等数据,除了数据改变之外,还引起dom的变化,这种render阶段不能完成的工作,我们称之为副作用。

flags
react中通过flags记录每个节点diff后需要变更的状态,例如dom的添加、替换、删除等。
image

Effect List
在render阶段,react会采用深度优化先遍历,对fiber数进行遍历,把每一个副作用的fiber筛选出来,最后构建生成一个只带副作用的Effect list 链表。和该链表相关的字段有firstEffect、nextEffect和lastEffect:
image

firstEffect指向第一个有副作用的fiber节点,lastEffect指向最后一个有副作用的节点,中间的节点全部通过nextEffect链接,最终形成Effect链表。

在commit阶段,React拿到Effect list 链表中的数据后,根据每一个fiber节点的flags类型,对相应的DOM进行更改。

其它
其它需要重点关注一下的属性还有lane和alternate。

lane
lane代表react要执行的fiber任务的优先级,通过这个字段,render阶段react确定应该优先将哪些任务提交到commit阶段去执行。

我们看一下源码中lane的枚举值:
image

同Flags的枚举值一样,Lanes也是用31位的二进制数表示,表示了31条赛道,位数越小的赛道,代表的优先级越高。
例如 InputDiscreteHydrationLane、InputDiscreteLanes、InputContinuousHydrationLane等用户交互引起的更新的优先级较高,DefaultLanes这种请求数据引起更新的优先级中等,而OffscreenLane、IdleLanes这种优先级较低。
优先级越低的任务,在render阶段越容易被打断,commit执行的时机越靠后。

alternate

当react的状态发生更新时,当前页面所对应的fiber树称为current Fiber,同时react会根据新的状态构建一颗新的fiber树,称为 workInProgress Fiber。current Fiber中每个fiber节点通过alternate字段,指向workInProgress Fiber中对应的fiber节点。同样workInProgress Fiber中的fiber节点的alternate字段也会指向current Fiber中对应的fiber节点。

参考:https://juejin.cn/post/7016512949330116645

posted @ 2022-02-17 18:59  举个栗子走天下  阅读(1109)  评论(0编辑  收藏  举报