十日谈 :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属性返回一个新数组