• 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

  • 相关阅读:
    System Idle Process SYSTEM占用CPU
    apache和nginx的rewrite的区别
    解决file_get_contents failed to open stream: HTTP request failed! 错误
    个人总结大型高并发高负载网站的系统架构(转)
    代码的抽象三原则
    mysqldump导入某库某表的数据
    mysql中insert into和replace into以及insert ignore用法区别
    【原创】学习日记4:nginx负载均衡(二)2012.01.08
    【原创】学习日记1:redis安装以及lnmp环境搭建2012.01.06
    mysql优化 mysql.ini优化
  • 原文地址:https://www.cnblogs.com/huayang1995/p/15905991.html
Copyright © 2020-2023  润新知