十日谈 :React API

欢迎阅读我的React源码学习笔记

JSX到JS的转换

<div></div>
// 实际上是
react.createElement("div",null)

在babelplay ground中可以查看转换结果
附上地址:
babelplay ground

React.createElement(
  type,
  [props],
  [...children]
)

第一个参数是必填,传入的是似HTML标签名称,eg: ul, li
第二个参数是选填,表示的是属性,使用key value的形式,eg: className=“name” => {className: name}
第三个参数是选填, 子节点,eg: 要显示的文本内容

注意:当bable转译JSX时,会区分第一个参数的首字母大小写,如果是组件,那么会转译为大写,如果是小写,bable会将其作为一个标签转译。所以组件名称要严格使用首字母大写以免引发混淆!

reactElement

注意:看源码首先考虑从入口文件开始,这样方便直观查找各类API来自哪里,又是如何export出来的,我们在根目录找到src文件夹即可看到源码,入口文件是react.js
例如react.js文件:

export {
  Children,
  createMutableSource,
  createRef,
  Component,
  PureComponent,
  createContext,
  forwardRef,
  lazy,
  memo,
  useCallback,
  useContext,
  useEffect,
  useImperativeHandle,
  useDebugValue,
  useInsertionEffect,
  useLayoutEffect,
  useMemo,
  useMutableSource,
  useSyncExternalStore,
  useReducer,
  useRef,
  useState,
  REACT_FRAGMENT_TYPE as Fragment,
  REACT_PROFILER_TYPE as Profiler,
  REACT_STRICT_MODE_TYPE as StrictMode,
  REACT_DEBUG_TRACING_MODE_TYPE as unstable_DebugTracingMode,
  REACT_SUSPENSE_TYPE as Suspense,
  createElement,
  cloneElement,
  isValidElement,
  ReactVersion as version,
  ReactSharedInternals as __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,
  // Deprecated behind disableCreateFactory
  createFactory,
  // Concurrent Mode
  useTransition,
  startTransition,
  useDeferredValue,
  REACT_SUSPENSE_LIST_TYPE as SuspenseList,
  REACT_LEGACY_HIDDEN_TYPE as unstable_LegacyHidden,
  REACT_OFFSCREEN_TYPE as unstable_Offscreen,
  getCacheSignal as unstable_getCacheSignal,
  getCacheForType as unstable_getCacheForType,
  useCacheRefresh as unstable_useCacheRefresh,
  REACT_CACHE_TYPE as unstable_Cache,
  // enableScopeAPI
  REACT_SCOPE_TYPE as unstable_Scope,
  useId,
  act,
};

找到这个文件最后export的API,这个对象所有eport出来的API都是React提供给我们使用的,可以看到里面包含许多我们经常使用的API比如PureComponent、lazy、Children、useEffect。

creatElement方法

接下来我们讲之前提到的createElement这个API

function createElement(type, config, children) {...}
// type 就是我们的节点类型,原生节点类型是一个字符串,如果是组件那么就是是一个component,这里有两种,也是我们比较熟知的class component和function component。
// config 就是我们写在标签里面所有的attrs,他们是以{key: value}的形式存放在对象中
// children 就是标签中间的内容部分,也可以包含一个子标签
function createElement(type, config, children) {
    //...
	for (propName in config) {
      if (
        hasOwnProperty.call(config, propName) &&
        !RESERVED_PROPS.hasOwnProperty(propName)
      ) {
        props[propName] = config[propName];
      }
    }
    //...
}

这里我们看到的是一个对config 的处理,如果不是内嵌的props就把它放进一个新建的props对象中。
从源码可以看出虽然创建的时候都是通过config传入的,但是key和ref不会跟其他config中的变量一起被处理,而是单独作为变量出现在ReactElement上。

function createElement(type, config, children) {
  //...
  const childrenLength = arguments.length - 2;
  if (childrenLength === 1) {
    props.children = children;
  } else if (childrenLength > 1) {
    const childArray = Array(childrenLength);
    for (let i = 0; i < childrenLength; i++) {
      childArray[i] = arguments[i + 2];
    }
    if (__DEV__) {
      if (Object.freeze) {
        Object.freeze(childArray);
      }
    }
    props.children = childArray;
    //用props.children存放数组
  }
  //...
}

children是多个的所以要通过arguments将多的参数全部获取,除去type和config。

// createelement返回的是一个ReactElement方法
  return ReactElement(
    type,
    key,
    ref,
    self,
    source,
    ReactCurrentOwner.current,
    props,
  );

ReactElement方法

const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
    // This tag allows us to uniquely identify this as a React Element
    $$typeof: REACT_ELEMENT_TYPE,

    // Built-in properties that belong on the element
    type: type,
    key: key,
    ref: ref,
    props: props,

    // Record the component responsible for creating this element.
    _owner: owner,
  };
	//...
	return element;
}

列举了输入的参数和输出的对象,了解清楚这几个,那么这个函数的框架就变得清晰了。

ReactElement只是一个用来承载信息的容器,他会告诉后续的操作这个节点的以下信息:

type类型,用于判断如何创建节点 key和ref这些特殊信息 props新的属性内容
$$typeof用于确定是否属于ReactElement
这些信息对于后期构建应用的树结构是非常重要的,而React通过提供这种类型的数据,来脱离平台的限制

Component & pureComponent

首先从入口文件的import里面去寻找这两个类

import {Component, PureComponent} from './ReactBaseClasses';

Component

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // 如果一个组件有string refs,我们稍后会分配一个不同的对象。
  this.refs = emptyObject;
  // 我们初始化了default updater,但真正的更新在render时注入。
  this.updater = updater || ReactNoopUpdateQueue;
}

我们来看看很重要的setState原型方法

Component.prototype.setState = function(partialState, callback) {
  if (
    typeof partialState !== 'object' &&
    typeof partialState !== 'function' &&
    partialState != null
  ) {
    throw new 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');
  // 可以看出,setState实际上只是调用了传入参数上的一个方法,具体这个方法的用处后续再谈
};

其实我本以为Component会是一个庞大的API,有很多定义在其中,但是看了才发现,他只是承载了一些定义和调用,没有涉及任何生命周期相关内容。

pureComponent

function PureComponent(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}
// 以上部分我们可以看出实际上和Component是没有区别的

const pureComponentPrototype = (PureComponent.prototype = new ComponentDummy());
pureComponentPrototype.constructor = PureComponent;
// Avoid an extra prototype jump for these methods.
Object.assign(pureComponentPrototype, Component.prototype);

//到此为止我们可以发现实际上以上代码是对Component的一个继承,继承了Component中的原型方法
pureComponentPrototype.isPureReactComponent = true;
//只有这最后一句有一些区别,这个isPureReactComponent 实际上就是一个主动的标识,这种方式在我们业务开发中也很常见

顺便说一下:React中对比一个ClassComponent是否需要更新,只有两个地方。一是看有没有shouldComponentUpdate方法,二就是这里的PureComponent判断

createRef & ref

首先回顾一下ref的核心功能

① React提供的这个ref属性,表示为对组件真正实例的引用,其实就是ReactDOM.render()返回的组件实例;
ReactDOM.render()渲染组件时返回的是组件实例;
渲染dom元素时,返回是具体的dom节点。
②ref可以挂载到组件上也可以是dom元素上;
挂到组件(class声明的组件)上的ref表示对组件实例的引用。不能在函数式组件上使用 ref 属性,因为它们没有实例:
挂载到dom元素上时表示具体的dom元素节点

createRef

我们来看看他的源码,也非常简单,用同样方式找到

import type {RefObject} from 'shared/ReactTypes';

// an immutable object with a single mutable value
export function createRef(): RefObject {
  const refObject = {
    current: null,
  };
  if (__DEV__) {
    Object.seal(refObject);
  }
  return refObject;
}

这里我们了解一下:

Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要原来是可写的就可以改变。

这里就有个问题,createRef得到这个对象,在后期如何使用如何挂载这个属性呢

forword-ref

forwardRef是用来解决HOC组件传递ref的问题的,所谓HOC就是Higher Order Component,比如使用redux的时候,我们用connect来给组件绑定需要的state,这其中其实就是给我们的组件在外部包了一层组件,然后通过…props的方式把外部的props传入到实际组件。当我们使用ref的时候,实际上得到的是包装后的组件,这样就不是我们真正的意图了。所以我们可以使用forword-ref这个api。

const TargetComponent = React.forwardRef((props, ref) => (
  <TargetComponent ref={ref} />
))

context

我们就直接看新版本下的context
先看用法:

const { Provider, Consumer } = React.createContext('defaultValue')
//Provider发布方  Consumer 订阅方
const ProviderComp = (props) => (
  <Provider value={'realValue'}>
    {props.children}
  </Provider>
)

const ConsumerComp = () => (
  <Consumer>
    {(value) => <p>{value}</p>}
  </Consumber>
)

老API在性能方面远远低于API,单次传递会影响途径的所有的子组件重新render,从而加大开销,如果需要项目优化,有使用Context的代码可以考虑进行替换
源码:

  const context: ReactContext<T> = {
    $$typeof: REACT_CONTEXT_TYPE,
    // As a workaround to support multiple concurrent renderers, we categorize
    // some renderers as primary and others as secondary. We only expect
    // there to be two concurrent renderers at most: React Native (primary) and
    // Fabric (secondary); React DOM (primary) and React ART (secondary).
    // Secondary renderers store their context values on separate fields.
    _currentValue: defaultValue,
    _currentValue2: defaultValue,
    // Used to track how many concurrent renderers this context currently
    // supports within in a single renderer. Such as parallel server rendering.
    _threadCount: 0,
    // These are circular
    Provider: (null: any),
    Consumer: (null: any),
  };
  context.Provider = {
    $$typeof: REACT_PROVIDER_TYPE,
    _context: context,
  };
    if (__DEV__) {
    //......
    context.Consumer = Consumer;
  } else {
    context.Consumer = context;
  }

我们可以看到$$typeof和我们用createElement生成不同,_currentValue用于记录最新context的值,在context.Consumer一旦指向context自身的时候,就会的到_currentValue记录的值。具体更新过程后续分析。
我们需要注意,这里的$$typeof是存放在先前提到的element对象中的type的值中的,而不是去改变element本身的.

ConcurrentMode

react17开始支持concurrent mode,这种模式的根本目的是为了让应用保持cpu和io的快速响应,它是一组新功能,包括Fiber、Scheduler、Lane,可以根据用户硬件性能和网络状况调整应用的响应速度,核心就是为了实现异步可中断的更新。concurrent mode也是未来react主要迭代的方向。
先进行了解,后续更新调度的时候再去深挖。
实际上这个组件源码中只是一个Symbol也就是 REACT_CONCURRENT_MODE_TYPE,但是这个Symbol影响了一些判断,改变常规调度机制,使得响应异步可控。

Suspense和lazy

先来看看这个API的功能是什么:
​ Suspense可以在请求数据的时候显示pending状态,请求成功后展示数据,原因是因为Suspense中组件的优先级很低,而离屏的fallback组件优先级高,当Suspense中组件resolve之后就会重新调度一次render阶段
注意,如果一个<suspense>标签中包含多个组件,那么会等待这些组件共同加载完毕才会render。

我们看一下源码

  REACT_SUSPENSE_TYPE as Suspense,

我们发现它任然是个Symbol,但是lazy并不是个Symbol

import {lazy} from './ReactLazy';
export function lazy<T>(
  ctor: () => Thenable<{default: T, ...}>,
): LazyComponent<T, Payload<T>> {
  const payload: Payload<T> = {
    // We use these fields to store the result.
    _status: Uninitialized,
    _result: ctor,
  };

  const lazyType: LazyComponent<T, Payload<T>> = {
    $$typeof: REACT_LAZY_TYPE,
    _payload: payload,
    _init: lazyInitializer,
  };

  if (__DEV__) {
  //.....
  }

  return lazyType;
}

我们可以看到它的入参是一个方法,_status记录Thenable对象(一般来说是个promise)的状态,比如pendding状态时就是-1。_result记录Thenable对象resolve所返回对象。

hooks

首先,我们要清楚hooks是应用于函数式组件的额,函数式组件相比于类式组件区别在于函数式组件没有this,而且不能使用生命周期方法,所以需要其它方式来弥补。
useState用来替换setsState
useEffect用来替换生命周期方法。
我们在
react.development.js文件中可以找找到声明React的部分代码:

var React = {...}

先尝试看一下useState

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

我们可以看到返回值返回的是resolveDispatcher()方法返回值的useState方法,那么我们继续看一下什么是resolveDispatcher()。

function resolveDispatcher() {
  var dispatcher = ReactCurrentDispatcher.current;
  !(dispatcher !== null) ? invariant(false, 'Hooks can only be called inside the body of a function component. (https://fb.me/react-invalid-hook-call)') : void 0;
  return dispatcher;
}

我们发现ReactCurrentDispatcher.current是真正被返回出来的,那么这个值,又是在什么时候被赋值的呢,可以提前带着疑问点一下,ReactCurrentDispatcher.current是在react-dom渲染过程中进行赋值的。
而ReactCurrentDispatcher 实际上是如下实例

var ReactCurrentDispatcher = {
  /**
   * @internal
   * @type {ReactComponent}
   */
  current: null
};

我们发现

function useState(initialState) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

function useReducer(reducer, initialArg, init) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useReducer(reducer, initialArg, init);
}

function useRef(initialValue) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useRef(initialValue);
}

function useEffect(create, inputs) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useEffect(create, inputs);
}

function useLayoutEffect(create, inputs) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useLayoutEffect(create, inputs);
}

function useCallback(callback, inputs) {
  var dispatcher = resolveDispatcher();
  return dispatcher.useCallback(callback, inputs);
}

这些Hooks方法调用的都是dispatcher实例上的具体方法,所以我们作为了解首先先记住dispatcher,并在后续文章中逐步揭晓dispatcher。

children

React.children就是用来操作组件的子节点的。

const React = {
  Children: {
    map,
    forEach,
    count,
    toArray,
    only,
  },
  ....
 }

我们可以看到,这些属性和数组的方法是很类似的,这里主要提一下map这个属性

先看一个简单demo

import React from 'react'

function ChildrenDemo(props) {
  console.log(props.children)
  console.log(React.Children.map(props.children, c => [c, [c, c]]))
  return props.children
}

export default () => (
  <ChildrenDemo>
    <span>1</span>
    <span>2</span>
  </ChildrenDemo>
)

我们发现,这个map方法实际上是通过第二个参数的形式进行展开,传入的是节点,展开得到map返回的一个新数组,这个数组是一维不再嵌套的,比如demo中第二个参数是[c,[c,c]],而返回得到的是一个有六个元素的数组,大概是:
[children1,children1,children1,children2,children2,children2]
注意,forEach属性修改原数组,而map属性返回一个新数组

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值