createContext
之前也有context
,相当于自动向下传递的props
,子树中的任意组件都可以从context
中按需取值(配合contextTypes
声明)
像props
一样,context
的作用也是自上而下传递数据,通常用于多语言配置、主题和数据缓存等场景,这些场景有几个特点:
同一份数据需要被多个组件访问
这些组件处于不同的嵌套层级
从数据传递的角度看,props
是一级数据共享,context
是子树共享。如果没有context
特性的话,就需要从数据源组件到数据消费者组件逐层显式传递数据(props
),一来麻烦,二来中间组件没必要知道这份数据,逐层传递造成了中间组件与数据消费者组件的紧耦合。而context
特性能够相对优雅地解决这两个问题,就像是props
机制的补丁
P.S.实际上,要解耦中间组件与数据消费者组件的话,还有另一种方法:把填好数据的组件通过props
传递下去,而不直接传递数据。这样中间组件就不需要知道数据消费者组件的内部细节(如依赖的数据)了,只知道这个位置将被插入某个组件(也就是组件组合,类似于Vue的slot
特性),这种思路有点IoC的意思,具体见Before You Use Context
createContext
API算是对context
特性的重新实现(可替代之前的context
):
const {Provider, Consumer} = React.createContext(defaultValue);
<Provider value={/* some value */}>
<Consumer>
{value => /* render something based on the context value */}
</Consumer>
P.S.旧的context
API在v16.x
仍然可用,但之后会被移除掉
只维护value
(没有key
),创建时给定默认值,通过Provider
组件写,通过Consumer
组件来读
一个Provider
可以对应多个Consumer
,内层Provider
能够重写外层Provider
的值(实际上Consumer
会从组件树中与之匹配的最近Provider
那里拿到值),Provider
的value
prop
发生变化时会通知所有后代Consumer
重新渲染(直接通知,不走shouldComponentUpdate
)
P.S.默认值比较有意思,如果Consumer
没有与之匹配的Provider
,就走defaultValue
。作用是在单测等场景,Consumer
可以不需要Provider
自己跑
P.S.比较新旧value
,确定是否发生了变化,走的是Object.is()浅对比逻辑(引用类型只比较引用)
内部实现
context
类型定义如下:
export type ReactContext<T> = {
$$typeof: Symbol | number,
Consumer: ReactContext<T>,
Provider: ReactProviderType<T>,
unstable_read: () => T,
_calculateChangedBits: ((a: T, b: T) => number) | null,
_currentValue: T,
_currentValue2: T,
_changedBits: number,
_changedBits2: number,
// DEV only
_currentRenderer?: Object | null,
_currentRenderer2?: Object | null,
};
export type ReactProviderType<T> = {
$$typeof: Symbol | number,
_context: ReactContext<T>,
};
看起来比较奇怪,带两份_currentValue
等值属性是为了支持多renderer
并发工作(使之互不影响):
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.
Consumer
和Provider
两个属性很有意思,存在循环引用:
context = {
Consumer: context,
Provider: {
_context: context
}
}
用来校验Consumer
和Provider
组件是否匹配:
// Check if the context matches.
dependency.context === context && (dependency.observedBits & changedBits) !== 0
createContext
实现如下:
export function createContext<T>(
defaultValue: T,
calculateChangedBits: ?(a: T, b: T) => number,
): ReactContext<T> {
if (calculateChangedBits === undefined) {
calculateChangedBits = null;
}
const context: ReactContext<T> = {
$$typeof: REACT_CONTEXT_TYPE,
_calculateChangedBits: calculateChangedBits,
_currentValue: defaultValue,
_currentValue2: defaultValue,
_changedBits: 0,
_changedBits2: 0,
// These are circular
Provider: (null: any),
Consumer: (null: any),
unstable_read: (null: any),
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context,
};
context.Consumer = context;
context.unstable_read = readContext.bind(null, context);
return context;
}
在渲染阶段把Provider
组件身上的value
prop
转移到context
对象上:
export function pushProvider(providerFiber: Fiber, changedBits: number): void {
const context: ReactContext<any> = providerFiber.type._context;
context._currentValue = providerFiber.pendingProps.value;
context._changedBits = changedBits;
}
Consumer
读取value
时建立依赖关系:
export function readContext<T>(
context: ReactContext<T>,
observedBits: void | number | boolean,
): T {
let contextItem = {
context: ((context: any): ReactContext<mixed>),
observedBits: resolvedObservedBits,
next: null,
};
if (lastContextDependency === null) {
// This is the first dependency in the list
currentlyRenderingFiber.firstContextDependency = lastContextDependency = contextItem;
} else {
// Append a new context item.
lastContextDependency = lastContextDependency.next = contextItem;
}
return isPrimaryRenderer ? context._currentValue : context._currentValue2;
}
fiber节点上带有依赖链表firstContextDependency
,Provider
的value
发生变化时通知所有依赖项,大致如下:
export function propagateContextChange(
workInProgress: Fiber,
context: ReactContext<mixed>,
changedBits: number,
renderExpirationTime: ExpirationTime,
): void {
// 遍历fiber子树,找出第一个与context匹配的Consumer或Provider
while (fiber !== null) {
// 遍历fiber节点的所有context依赖
do {
// 检查是否匹配
// 匹配的话,标记该fiber需要更新,等待调度
} while (dependency !== null);
}
}
P.S.具体实现细节见react/packages/react-reconciler/src/ReactFiberNewContext.js
此外还有两种组件,Provider
与Consumer
:
export type ReactProvider<T> = {
$$typeof: Symbol | number,
type: ReactProviderType<T>,
key: null | string,
ref: null,
props: {
value: T,
children?: ReactNodeList,
},
};
export type ReactConsumer<T> = {
$$typeof: Symbol | number,
type: ReactContext<T>,
key: null | string,
ref: null,
props: {
children: (value: T) => ReactNodeList,
unstable_observedBits?: number,
},
};
Consumer
看起来比较特殊,其props.children
是个value => ReactNodeList
的函数
createRef
之前版本中,ref
有2种形式:
字符串形式
函数形式
示例:
<div ref="mask"></div>
<div ref={(node) => this.maskNode = node}></div>
前者方便易用,后者更安全(unmount
时候会给null
掉,游离节点引发的内存风险降低不少)
此外,字符串ref
还有很多缺陷:
要兼容Closure Compiler高级模式的话,必须把
this.refs['myname']
标识为字符串(具体见Types in the Closure Type System)不允许单一实例有多个owner
动态字符串会妨碍VM优化
在异步批量渲染下存在问题,因为是同步处理的,需要始终保持一致
可以通过hook获取到兄弟
ref
,但破坏了组件的封装性不支持静态类型化,在类似TypeScript的(强类型)语言中,每次用到都必须显式转换
由子组件调用的回调中无法把
ref
绑定到正确的owner
上,例如<Child renderer={index => <div ref="test">{index}</div>} />
中的ref
会被挂在执行改回调的组件上,而不是当前owner
希望ref
能够传递,能有多个owner
,以及适应异步批处理场景……关于此话题的更多讨论,见Implement Better Refs API
第3种ref
不是字符串也不是函数,而是个对象(故称之为对象ref):
export function createRef(): RefObject {
const refObject = {
current: null,
};
return refObject;
}
也就是说:
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
这里给div
指定的ref
属性,实际上是个对象(身上有个current
属性),所以用法是这样:
const node = this.myRef.current;
const myComponent = this.myComponentRef.current;
就实现而言,与之前的字符串ref
相比,不过是包了一层对象而已。其类型定义如下:
export type RefObject = {|
current: any,
|};
P.S.其中|...|
的Flow类型定义表示禁止扩展(Object.seal()
)
RefObject
是仅含一个current
key
的对象,这样做有3个好处:
相对安全。与函数
ref
类似,unmount
时current
会被置为null
,一定程度上降低了内存风险适用于函数式组件。因为对象
ref
不与组件实例强关联(不要求创建实例,函数ref
也具有这个优势)可传递,也能有多个
owner
。这一点比函数ref
和字符串ref
都强大,反正只是个对象,多个组件持有也没关系,比其它两个灵活
P.S.之所以说“一定程度上”,是因为非要this.cachedNode = this.myRef.current
这么干的话,肯定是null
不掉的(包的这一层引用隔离,可以轻易突破)
P.S.虽然有了新的对象ref
,但并没有废弃前两个,3者目前的状态是:
对象
ref
:因可传递等特性,建议使用函数
ref
:因其灵活性而得以保留,建议使用字符串
ref
:不建议使用,并且在后续版本可能被移除掉
函数形式的ref
提供了更细粒度的控制(fine-grain control),包括ref
绑定、解绑的时机
P.S.对象ref
很大程度上是作为字符串ref
的替代品推出的,所以建议用对象,废弃字符串ref
forwardRef
大多数场景用不着,但在几个典型场景很关键:
触发深层
input
的focus
(如自动聚焦搜索框)计算元素宽高尺寸(如JS布局方案)
重新定位DOM元素(如
tooltip
)
从组件角度分为两类:
DOM包装组件
高阶组件(High Order Component)
上面提到的3个场景都属于DOM包装组件,比如MyInput
、MyDialog
、MyTooltip
,特点是对DOM节点的包装/增强。从使用角度看,与input、select
等原生DOM节点地位一样,能构成视图,并且可交互。而交互的支持依赖对原生DOM节点的控制,比如无论包多少层,想要focus
效果的话,最终还是要触发input
节点的对应行为,这种场景下,ref
传递就成了刚需
These components tend to be used throughout the application in a similar manner as a regular DOM button and input, and accessing their DOM nodes may be unavoidable for managing focus, selection, or animations.
P.S.实际应用中,甚至见到过类似this.refs.wapper.refs.node
的奇技淫巧,这实际上就是对ref
传递特性的强烈需求
而高阶组件一般是对组件功能的增强/扩展,因此天生就面临ref
传递的问题,包了一层之后ref
就不能直接访问了,但又没有太好的方式向下传递,所以一直是个问题(以不太优雅的方式维持ref
链)
不使用forwardRef
API的话,可以这样解决:
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.inputElement = React.createRef();
}
render() {
return (
<CustomTextInput inputRef={this.inputElement} />
);
}
}
(摘自gaearon/dom_ref_forwarding_alternatives_before_16.3.md)
姑且称之为别名ref prop传递,说白了就是通过props
向下传递一个ref
载体(this.inputElement
),到达目标节点后与之关联起来(ref={props.inputRef}
),类似于:
function CustomTextInput(props) {
return (
<div>
<input ref={node => props.refHost.node = node} />
</div>
);
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.refHost = {};
}
render() {
return (
<CustomTextInput refHost={this.refHost} />
);
}
}
forwardRef
API提供了一种比较优雅的解决方案:
let CustomTextInput = React.forwardRef((props, ref) => (
<div>
<input ref={ref} />
</div>
));
class Parent extends React.Component {
constructor(props) {
super(props);
this.inputRef = {};
}
render() {
return (
<CustomTextInput ref={this.inputRef} />
);
}
}
对比上面第一种替代方案,几乎一模一样,无非是把ref
作为独立参数,从而避免用不叫ref
的prop
传递ref
的尴尬
在高阶组件的场景,这样做:
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// Assign the custom prop "forwardedRef" as a ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// Note the second param "ref" provided by React.forwardRef.
// We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
// And it can then be attached to the Component.
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
因为React.forwardRef
接受一个render
函数,非常适合函数式组件,而对class
形式的组件不太友好,所以上例这样的高阶函数场景,实质上是通过forwardRef + 别名ref prop传递
来解决的
内部实现
与ref
载体的思路几乎没什么区别,甚至其内部实现也差不多
先看API入口:
function forwardRef<Props, ElementType: React$ElementType>(
render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
) {
return {
$$typeof: REACT_FORWARD_REF_TYPE,
render,
};
}
React.forwardRef
接受一个(props, ref) => React$Node
类型的render
函数作为参数,返回值是一种新的React$Node
(即合法ReactElement
,用来描述视图结构的对象),相当于给这参数传入的render
函数添上了类型标识
P.S.更多合法ReactElement
见react/packages/shared/isValidElementType.js
内部根据该类型标识区分出来之后,做一些额外处理,包括挂载、更新和卸载3部分:
// 挂载
function commitAttachRef(finishedWork: Fiber) {
const ref = finishedWork.ref;
if (ref !== null) {
const instance = finishedWork.stateNode;
ref.current = instance;
}
}
// 更新
function updateForwardRef(
current: Fiber | null,
workInProgress: Fiber,
renderExpirationTime: ExpirationTime,
) {
const render = workInProgress.type.render;
const nextProps = workInProgress.pendingProps;
const ref = workInProgress.ref;
let nextChildren = render(nextProps, ref);
reconcileChildren(
current,
workInProgress,
nextChildren,
renderExpirationTime,
);
return workInProgress.child;
}
// 卸载
function commitDetachRef(current: Fiber) {
const currentRef = current.ref;
if (currentRef !== null) {
currentRef.current = null;
}
}
(摘自react/packages/react-reconciler/src/ReactFiberBeginWork.js、react/packages/react-reconciler/src/ReactFiberCommitWork.js,清晰起见,不太重要的部分都删掉了)
挂载阶段实际上并不关心对象ref
的来源(无论层层传递过来的还是自己创建的都一样),更新也没什么特殊的,用新的props
和ref
去render
,卸载就是置null
,实现其实比较简单
StrictMode
StrictMode is a tool for highlighting potential problems in an application.
React.StrictMode
用来开启子树严格检查,是个内置组件:
import React from 'react';
function ExampleApplication() {
return (
<div>
<Header />
<React.StrictMode>
<div>
<ComponentOne />
<ComponentTwo />
</div>
</React.StrictMode>
<Footer />
</div>
);
}
有几个特点:
不渲染UI,像
Fragment
一样会为后代组件(即子树级)开启额外的检查和警告提示
仅在
development
环境有效,不影响production
版本
主要有4个作用:
识别具有
unsafe
生命周期的组件字符串
ref
警告检测非预期的副作用
检测旧的
context
context API
P.S.以后还会添加更多功能
unsafe
、字符串ref
、旧context
API检查的实际意义是保障API废弃决策可靠推进,尤其是涉及第三方依赖的场景,很难确认是否存在即将过时的API的使用,提供运行时检查能够有效提醒开发者去处理,例如:
而副作用检测对于Async Rendering特性是很有意义的,第一阶段涉及很多组件方法:
constructor
componentWillMount
componentWillReceiveProps
componentWillUpdate
getDerivedStateFromProps
shouldComponentUpdate
render
setState updater functions (the first argument)
也就是说,这些函数将来(开启异步渲染特性之后)可能会被调用多次,所以要求不含副作用(即idempotent,调用多次和调用一次产生的效果完全一样)。但问题是,副作用很难被检测到,StrictMode
也做不到,所以做了这样一件事情:
By intentionally double-invoking methods like the component constructor, strict mode makes patterns like this easier to spot.
具体地,故意多调1次这些函数:
class
组件的构造函数render
函数setState
传入的更新函数getDerivedStateFromProps
生命周期函数
算是多少有点帮助吧,既然无法帮助解决问题,那就想办法帮助暴露问题
I was cսrious if you ever thougһt of changing the strսcture of your website? Its very well written; I love ѡhat youve got to say.
But maybе you could a little more in the way of content so people c᧐uld conneϲt witgh it bettеr. Youve got an awdul lot of text foor only having one or two pictures. Maybe you coyld space it out better?