阅读 React 官方文档 Part-2
使用 React.lazy 需要注意存在条件判断时的 fallback
事件处理
你必须谨慎对待 JSX 回调函数中的 this
,在 JavaScript 中,class 的方法默认不会绑定 this
。如果你忘记绑定 this.handleClick
并把它传入了 onClick
,当你调用这个函数的时候 this
的值为 undefined
。
这并不是 React 特有的行为;这其实与 JavaScript 函数工作原理有关。通常情况下,如果你没有在方法后面添加 ()
,例如 onClick={this.handleClick}
,你应该为这个方法绑定 this
。
如果觉得使用 bind
很麻烦,这里有两种方式可以解决。你可以使用 public class fields 语法 to correctly bind callbacks:
1class LoggingButton extends React.Component {
2 // This syntax ensures `this` is bound within handleClick.
3 handleClick = () => {
4 console.log('this is:', this);
5 };
6
7 render() {
8 return (
9 <button onClick={this.handleClick}>
10 Click me
11 </button>
12 );
13 }
14}
如果你没有使用 class fields 语法,你可以在回调中使用箭头函数:
1class LoggingButton extends React.Component {
2 handleClick() {
3 console.log('this is:', this);
4 }
5
6 render() {
7 // 此语法确保 `handleClick` 内的 `this` 已被绑定。
8 return (
9 <button onClick={() => this.handleClick()}>
10 Click me
11 </button>
12 );
13 }
14}
此语法问题在于每次渲染 LoggingButton
时都会创建不同的回调函数。在大多数情况下,这没什么问题,但如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染。我们通常建议在构造器中绑定或使用 class fields 语法来避免这类性能问题。
使用 startTransition 避免切换时 Suspense fallback 的出现
任何组件都可能因渲染而暂停,甚至是已经展示给用户的组件。为了使屏幕内容始终一致,如果一个已经显示的组件暂停,React 必须隐藏它的树,直到最近的 <Suspense>
边界。然而,从用户的角度来看,这可能会使人很困惑。
参考这个标签切换的示例:
1import React, { Suspense } from 'react';
2import Tabs from './Tabs';
3import Glimmer from './Glimmer';
4
5const Comments = React.lazy(() => import('./Comments'));
6const Photos = React.lazy(() => import('./Photos'));
7
8function MyComponent() {
9 const [tab, setTab] = React.useState('photos');
10
11 function handleTabSelect(tab) {
12 setTab(tab);
13 };
14
15 return (
16 <div>
17 <Tabs onTabSelect={handleTabSelect} />
18 <Suspense fallback={<Glimmer />}>
19 {tab === 'photos' ? <Photos /> : <Comments />}
20 </Suspense>
21 </div>
22 );
23}
在这个示例中,如果标签从 'photos'
切换为 'comments'
,但 Comments
会暂停,用户会看到屏幕闪烁。这符合常理,因为用户不想看到 'photos'
,而 Comments
组件还没有准备好渲染其内容,而 React 为了保证用户体验的一致性,只能显示上面的 Glimmer
,别无选择。
然而,有时这种用户体验并不可取。特别是在准备新 UI 时,展示 “旧” 的 UI 会体验更好。你可以尝试使用新的 startTransition
API 来让 React 实现这一点:
1function handleTabSelect(tab) {
2 startTransition(() => {
3 setTab(tab);
4 });
5}
此处代码会告知 React,将标签切换为 'comments'
不会标记为紧急更新,而是标记为需要一些准备时间的 transition。然后 React 会保留旧的 UI 并进行交互,当它准备好时,会切换为 <Comments />
,具体请参阅 Transitions 以了解更多相关信息。
Context
1const MyContext = React.createContext(defaultValue);
创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider
中读取到当前的 context 值。
只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue
参数才会生效。此默认值有助于在不使用 Provider 包装组件的情况下对组件进行测试。注意:将 undefined
传递给 Provider 的 value 时,消费组件的 defaultValue
不会生效。
1<MyContext.Provider value={/* 某个值 */}>
每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化。
Provider 接收一个 value
属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
当 Provider 的 value
值发生变化时,它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件(包括 .contextType 和 useContext)的传播不受制于 shouldComponentUpdate
函数,因此当 consumer 组件在其祖先组件跳过更新的情况下也能更新。
通过新旧值检测来确定变化,使用了与 Object.is
相同的算法。
注意
当传递对象给
value
时,检测变化的方式会导致一些问题:详见注意事项。
HOC
当你将 HOC 应用于组件时,原始组件将使用容器组件进行包装。这意味着新组件没有原始组件的任何静态方法。
1// 定义静态函数
2WrappedComponent.staticMethod = function() {/*...*/}
3// 现在使用 HOC
4const EnhancedComponent = enhance(WrappedComponent);
5
6// 增强组件没有 staticMethod
7typeof EnhancedComponent.staticMethod === 'undefined' // true
为了解决这个问题,你可以在返回之前把这些方法拷贝到容器组件上:
1function enhance(WrappedComponent) {
2 class Enhance extends React.Component {/*...*/}
3 // 必须准确知道应该拷贝哪些方法 :(
4 Enhance.staticMethod = WrappedComponent.staticMethod;
5 return Enhance;
6}
但要这样做,你需要知道哪些方法应该被拷贝。你可以使用 hoist-non-react-statics 自动拷贝所有非 React 静态方法:
1import hoistNonReactStatic from 'hoist-non-react-statics';
2function enhance(WrappedComponent) {
3 class Enhance extends React.Component {/*...*/}
4 hoistNonReactStatic(Enhance, WrappedComponent);
5 return Enhance;
6}
除了导出组件,另一个可行的方案是再额外导出这个静态方法。
1// 使用这种方式代替...
2MyComponent.someFunction = someFunction;
3export default MyComponent;
4
5// ...单独导出该方法...
6export { someFunction };
7
8// ...并在要使用的组件中,import 它们
9import MyComponent, { someFunction } from './MyComponent.js';
Element
ReactElement
先不看其他的方法,我们首先来看一下ReactElement到底是什么样的。 ReactElement函数是一个工厂函数,创建新的react元素;不支持class模式,不要new它;instanceof也不奏效,可以检测$$typeof是不是Symbol.for(‘react.element’)。
首先创建一个element对象,参数里的type、key、ref、props、owner放进去,然后就可以把这个对象return了。
不过在开发模式下,我们在这个对象里面额外添加了_store作为验证标记。
再把_store、_self、_source赋值并且设置为不能重新定义属性,不能枚举,不能写入,最后调用Object.freeze使对象不可再改变。
1var ReactElement = function(type, key, ref, self, source, owner, props) {
2 var element = {
3 // This tag allow us to uniquely identify this as a React Element
4 $$typeof: REACT_ELEMENT_TYPE,
5
6 // Built-in properties that belong on the element
7 type: type,
8 key: key,
9 ref: ref,
10 props: props,
11
12 // Record the component responsible for creating this element.
13 _owner: owner,
14 };
15
16 if (__DEV__) {
17 // The validation flag is currently mutative. We put it on
18 // an external backing store so that we can freeze the whole object.
19 // This can be replaced with a WeakMap once they are implemented in
20 // commonly used development environments.
21 element._store = {};
22
23 // To make comparing ReactElements easier for testing purposes, we make
24 // the validation flag non-enumerable (where possible, which should
25 // include every environment we run tests in), so the test framework
26 // ignores it.
27 Object.defineProperty(element._store, 'validated', {
28 configurable: false,
29 enumerable: false,
30 writable: true,
31 value: false,
32 });
33 // self and source are DEV only properties.
34 Object.defineProperty(element, '_self', {
35 configurable: false,
36 enumerable: false,
37 writable: false,
38 value: self,
39 });
40 // Two elements created in two different places should be considered
41 // equal for testing purposes and therefore we hide it from enumeration.
42 Object.defineProperty(element, '_source', {
43 configurable: false,
44 enumerable: false,
45 writable: false,
46 value: source,
47 });
48 if (Object.freeze) {
49 Object.freeze(element.props);
50 Object.freeze(element);
51 }
52 }
53
54 return element;
55};
createElement
1createElement(type, config, children)
用jsx写的代码都会被转换成createElement,你无需直接调用它。 type是你要创建的元素的类型,可以是html的div或者span,也可以是其他的react组件,注意大小写。 从config中获取props、key、ref、self、source。 向props加入children,如果是一个就放一个对象,如果是多个就放入一个数组。 那如果type.defaultProps有默认的props时,并且对应的props里面的值是undefined,把默认值赋值到props中。 这时就可以直接return一个调用ReactElement的执行结果了。 在开发环境里,我们还会对key和ref进行校验。
1export function createElement(type, config, children) {
2 var propName;
3
4 // Reserved names are extracted
5 var props = {};
6
7 var key = null;
8 var ref = null;
9 var self = null;
10 var source = null;
11
12 if (config != null) {
13 if (hasValidRef(config)) {
14 ref = config.ref;
15 }
16 if (hasValidKey(config)) {
17 key = '' + config.key;
18 }
19
20 self = config.__self === undefined ? null : config.__self;
21 source = config.__source === undefined ? null : config.__source;
22 // Remaining properties are added to a new props object
23 for (propName in config) {
24 if (
25 hasOwnProperty.call(config, propName) &&
26 !RESERVED_PROPS.hasOwnProperty(propName)
27 ) {
28 props[propName] = config[propName];
29 }
30 }
31 }
32
33 // Children can be more than one argument, and those are transferred onto
34 // the newly allocated props object.
35 var childrenLength = arguments.length - 2;
36 if (childrenLength === 1) {
37 props.children = children;
38 } else if (childrenLength > 1) {
39 var childArray = Array(childrenLength);
40 for (var i = 0; i < childrenLength; i++) {
41 childArray[i] = arguments[i + 2];
42 }
43 if (__DEV__) {
44 if (Object.freeze) {
45 Object.freeze(childArray);
46 }
47 }
48 props.children = childArray;
49 }
50
51 // Resolve default props
52 if (type && type.defaultProps) {
53 var defaultProps = type.defaultProps;
54 for (propName in defaultProps) {
55 if (props[propName] === undefined) {
56 props[propName] = defaultProps[propName];
57 }
58 }
59 }
60 if (__DEV__) {
61 if (key || ref) {
62 if (
63 typeof props.$$typeof === 'undefined' ||
64 props.$$typeof !== REACT_ELEMENT_TYPE
65 ) {
66 var displayName =
67 typeof type === 'function'
68 ? type.displayName || type.name || 'Unknown'
69 : type;
70 if (key) {
71 defineKeyPropWarningGetter(props, displayName);
72 }
73 if (ref) {
74 defineRefPropWarningGetter(props, displayName);
75 }
76 }
77 }
78 }
79 return ReactElement(
80 type,
81 key,
82 ref,
83 self,
84 source,
85 ReactCurrentOwner.current,
86 props,
87 );
88}
createFactory
这个就是相当于createElement的第一个参数type给制定了,返回给你个createElement函数。
1export function createFactory(type) {
2 var factory = createElement.bind(null, type);
3 factory.type = type;
4 return factory;
5}
###cloneElement
1cloneElement(element, config, children)
返回一个克隆的新元素,拥有原始元素的props和新的props,原始元素的key和ref也会被保留。几乎等价于
1<element.type {...element.props} {...props}>{children}</element.type>
首先将原始元素的props复制一份; 再将key、ref、self、source、owner保留; 如果config中有ref、owner、key,使用config中的; 填充defaultProps,优先使用config,其次是原始元素的; children放到props里; 返回ReactElement。
1export function cloneElement(element, config, children) {
2 var propName;
3
4 // Original props are copied
5 var props = Object.assign({}, element.props);
6
7 // Reserved names are extracted
8 var key = element.key;
9 var ref = element.ref;
10 // Self is preserved since the owner is preserved.
11 var self = element._self;
12 // Source is preserved since cloneElement is unlikely to be targeted by a
13 // transpiler, and the original source is probably a better indicator of the
14 // true owner.
15 var source = element._source;
16
17 // Owner will be preserved, unless ref is overridden
18 var owner = element._owner;
19
20 if (config != null) {
21 if (hasValidRef(config)) {
22 // Silently steal the ref from the parent.
23 ref = config.ref;
24 owner = ReactCurrentOwner.current;
25 }
26 if (hasValidKey(config)) {
27 key = '' + config.key;
28 }
29
30 // Remaining properties override existing props
31 var defaultProps;
32 if (element.type && element.type.defaultProps) {
33 defaultProps = element.type.defaultProps;
34 }
35 for (propName in config) {
36 if (
37 hasOwnProperty.call(config, propName) &&
38 !RESERVED_PROPS.hasOwnProperty(propName)
39 ) {
40 if (config[propName] === undefined && defaultProps !== undefined) {
41 // Resolve default props
42 props[propName] = defaultProps[propName];
43 } else {
44 props[propName] = config[propName];
45 }
46 }
47 }
48 }
49
50 // Children can be more than one argument, and those are transferred onto
51 // the newly allocated props object.
52 var childrenLength = arguments.length - 2;
53 if (childrenLength === 1) {
54 props.children = children;
55 } else if (childrenLength > 1) {
56 var childArray = Array(childrenLength);
57 for (var i = 0; i < childrenLength; i++) {
58 childArray[i] = arguments[i + 2];
59 }
60 props.children = childArray;
61 }
62
63 return ReactElement(element.type, key, ref, self, source, owner, props);
64}
isValidElement
通过$$typeof判断一个对象是否是react元素。
1export function isValidElement(object) {
2 return (
3 typeof object === 'object' &&
4 object !== null &&
5 object.$$typeof === REACT_ELEMENT_TYPE
6 );
7}
ReactElementValidator
ReactElementValidator就是在开发环境下对ReactElement的方法多了一些校验。
createElementWithValidation
首先校验type是否是合法的:string、function、symbol、number。 校验了子节点的key,确保每个数组中的元素都有唯一的key。 校验了props是否符合设置的proptypes。
createFactoryWithValidation
把type设为不可枚举,并且在get的时候警告,不建议直接访问Factory.type
cloneElementWithValidation
校验了子节点的key;校验了proptypes。