Closed Hilshire closed 4 years ago
HOC
有些难解释,因为这不是 api ,而是 react
的一种设计模式。无论是名字还是用法,HOC 都和高阶函数有些相似。如果你对高阶函数有足够的了解,并且经常在项目中使用的话,想必会对HOC有这更好的理解。
好在 HOC 是一个一旦掌握了就不会忘记的技能。实际上,我更愿意称呼它为某种『习惯』或者『范式』,它本质提供了一种代码抽象的手段。
使用HOC要注意以下几点:
约定:最大化可组合性 这个约定解释起来就有点麻烦...
// React Redux 的 `connect` 函数
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
这个函数大家应该都不陌生,HOC 应该符合 Component => Component
这样的函数签名。这让我们可以向上面一样组合HOC。对于一些情况,可以用 compose
处理
// 而不是这样...
const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
// ... 你可以编写组合工具函数
// compose(f, g, h) 等同于 (...args) => f(g(h(...args)))
const enhance = compose(
// 这些都是单参数的 HOC
withRouter,
connect(commentSelector)
)
const EnhancedComponent = enhance(WrappedComponent)
约定:包装显示名称以便轻松调试 显示名称应该为 WithSubscription(CommentList):
function withSubscription(WrappedComponent) {
class WithSubscription extends React.Component {/* ... */}
WithSubscription.displayName =
`WithSubscription(${getDisplayName(WrappedComponent)})`;
return WithSubscription;
}
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
render() {
// 每次调用 render 函数都会创建一个新的 EnhancedComponent
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// 这将导致子树每次渲染都会进行卸载,和重新挂载的操作!此外还会丢失状态
return <EnhancedComponent />;
}
很长的一章,然而其实没什么内容。这些就够了:
class SomePlugin extends React.Component {
componentDidMount() {
this.$el = $(this.el);
this.$el.somePlugin();
}
componentWillUnmount() {
this.$el.somePlugin('destroy');
}
render() {
return <div ref={el => this.el = el} />;
}
}
比较杂,不重要的部分省略了。
React.createElement(...)
<MyComponents.DatePicker color="blue" />
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = { photo: PhotoStory, video: VideoStory };
function Story(props) { // 错误!JSX 类型不能是一个表达式。 return <components[props.storyType] story={props.story} />; }
```jsx
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// 正确!JSX 类型可以是大写字母开头的变量。
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
Props 默认值为 “True” 不建议不传 value 给 prop
属性展开
如果你知道在什么情况下你的组件不需要更新,你可以在 shouldComponentUpdate 中返回 false 来跳过整个渲染过程。其包括该组件的 render 调用以及之后的操作。
在大部分情况下,你可以继承 React.PureComponent 以代替手写 shouldComponentUpdate()。它用当前与之前 props 和 state 的浅比较覆写了 shouldComponentUpdate() 的实现。
疑问: pureComponent 为什么可以加快性能?react 会在什么时候更新组件?
Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。 值得注意的是,这种情况下,组件在组件树中的位置是不变的。我认为这种不对应可能会造成困惑。 react 团队显然也知道这一点。文档上有专门的一段用来写这种情况下的冒泡行为。及时DOM再不同的地方,事件依然会冒泡到父组件上。
Profiler 测量渲染一个 React 应用多久渲染一次以及渲染一次的“代价”。 它的目的是识别出应用中渲染较慢的部分,或是可以使用类似 memoization 优化的部分,并从相关优化中获益。
Profiler 包含两个 prop:一个是 id,另一个是 onRender
function onRenderCallback(
id, // 发生提交的 Profiler 树的 “id”
phase, // "mount" (如果组件树刚加载) 或者 "update" (如果它重渲染了)之一
actualDuration, // 本次更新 committed 花费的渲染时间
baseDuration, // 估计不使用 memoization 的情况下渲染整颗子树需要的时间
startTime, // 本次更新中 React 开始渲染的时间
commitTime, // 本次更新中 React committed 的时间
interactions // 属于本次更新的 interactions 的集合
) {
// 合计或记录渲染时间。。。
}
略
略
名字起的很高大上,内容实际也很高大上,但是总有点文不对题的感觉——往下面一翻,就发现突然开始讲解起diff算法了。于是又理所当然地解释了一遍 react
是如何把 O(n3) 的复杂度变成 O(n) 的。而这种算法可以实现,源自两个前提:
- 两个不同类型的元素会产生出不同的树;
- 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定;
而上述前提成立的理由,fb给出的答案是:
在实践中,我们发现以上假设在几乎所有实用的场景下都成立。
在我看来,这个答案是很让人不安的。当然,抓着这一点不放颇有吹毛求疵之嫌。
比对同类型的组件元素:
当一个组件更新时,组件实例保持不变,这样 state 在跨越不同的渲染时保持一致。React 将更新该组件实例的 props 以跟最新的元素保持一致,并且调用该实例的
componentWillReceiveProps()
和componentWillUpdate()
方法。
注:在 react16 中,componentWillReceiveProps
已经不再推荐使用。
在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。 在子元素列表末尾新增元素时,更变开销比较小 但是,在元素列表头部新增元素,开销会变大。所以,
react
变更每一个元素来实现在头部新增元素。当然,这样的开销也是不可接受的。这也就是为什么react
的列表都要求一个key
使用数组下标作为 key
:
这是可行的。但是要小心两种情况:
这个策略在元素不进行重新排序时比较合适,但一旦有顺序修改,diff 就会变得慢。
当基于下标的组件进行重新排序时,组件 state 可能会遇到一些问题。由于组件实例是基于它们的 key 来决定是否更新以及复用,如果 key 是一个下标,那么修改顺序时会修改当前的 key,导致非受控组件的 state(比如输入框)可能相互篡改导致无法预期的变动
结论很明显:能不用下标就不用下标,尤其是使用后端数据进行渲染的时候。
请谨记协调算法是一个实现细节。React 可以在每个 action 之后对整个应用进行重新渲染,得到的最终结果也会是一样的。在此情境下,重新渲染表示在所有组件内调用 render 方法,这不代表 React 会卸载或装载它们。React 只会基于以上提到的规则来决定如何进行差异的合并。
我们定期探索优化算法,让常见用例更高效地执行。在当前的实现中,可以理解为一棵子树能在其兄弟之间移动,但不能移动到其他位置。在这种情况下,算法会重新渲染整棵子树。
由于 React 依赖探索的算法,因此当以下假设没有得到满足,性能会有所损耗。
- 该算法不会尝试匹配不同组件类型的子树。如果你发现你在两种不同类型的组件中切换,但输出非常相似的内容,建议把它们改成同一类型。在实践中,我们没有遇到这类问题。
- Key 应该具有稳定,可预测,以及列表内唯一的特质。不稳定的 key(比如通过 Math.random() 生成的)会导致许多组件实例和 DOM 节点被不必要地重新创建,这可能导致性能下降和子组件中的状态丢失。
这一章节的开头,作者不厌其烦地声明不要滥用 ref
,以及 ref
应该被用在什么场景。事实证明,ref
确实很容易被滥用,毕竟人都是懒惰的,而中国被巨量的需求压榨的码农实在是太多了。
文档列出的,应该使用 ref
的情况有以下三种:
管理焦点,文本选择或媒体播放。 触发强制动画。 集成第三方 DOM 库。
不得不说这是一个非常严苛的要求。而这个要求的立足点在于,大部分情况下,不使用 ref
获取 DOM
的场景,其实都可以用状态提升解决。换句话说,如果有场景需要使用 ref
获得一个组件,那么很有可能是程序中某个地方出了问题,也就是我们俗称的『坏味道』。
不过,在实际开发中,实际上很难完全避免这一点。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
const node = this.myRef.current;
ref 的值根据节点的类型而有所不同:
当 ref 属性用于 HTML 元素时,构造函数中使用
React.createRef()
创建的 ref 接收底层 DOM 元素作为其current
属性。 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其current
属性。 你不能在函数组件上使用 ref 属性,因为他们没有实例。ref 会在 componentDidMount 或 componentDidUpdate 生命周期钩子触发前更新。
这个就有意思了:除了属性之外,我们也可以传递一个回调。
它能助你更精细地控制何时 refs 被设置和解除。 在
js
当中,函数的威力不言自明。然而这种灵活性当然也提供了 cheat 的可能 我不想粘贴实例里的那一坨代码,所以只附上我觉得重要的部分:... this.textInput = null;
this.setTextInputRef = element => {
this.textInput = element;
};
this.focusTextInput = () => {
// 使用原生 DOM API 使 text 输入框获得焦点
if (this.textInput) this.textInput.focus();
};
...
文档中展示的小小魔术:
```jsx
function CustomTextInput(props) {
return (
<div>
<input ref={props.inputRef} />
</div>
);
}
class Parent extends React.Component {
render() {
return (
<CustomTextInput
inputRef={el => this.inputElement = el}
/>
);
}
}
这是非常重要的一章。但是这一章其实没有新的API,而是阐述了一种 react
使用范式。这一范式是为了解决所谓的『横切关注点』问题,简单来说是为了处理多组件之间的逻辑复用问题。
提到这个首先想到的应该是 this.props.children
和 HOC
: 这两个都是 react
为了解决这个问题而提供的武器。实际上我想不出有什么是这两个解决不了,而需要 render props
解决的问题。如果我有一些页面需要复用,我会考虑用 children prop
;如果我只是一些逻辑需要复用,那我就使用 HOC
。那么问题来了,使用 Render Props
的场景是什么呢?
如过让我来选择,我会在同时需要复用视图和逻辑的时候使用它——实际上,这个说法也很狭隘,因为这三者其实是不冲突的。
我会用单独的一个 comment
来放这一节,不过其实没有什么东西想写。这个观念上的章节还是直接看文档更加合适一点。具体的代码文档用三行就展示了出来,尽管下面跟了很长篇幅的例子。
<DataProvider render={data => (
<h1>Hello {data.target}</h1>
)}/>
对了,还有一个需要注意的地方: 将 Render Props 与 React.PureComponent 一起使用时要小心
如果你在 render 方法里创建函数,那么使用 render prop 会抵消使用 React.PureComponent 带来的优势。因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render prop 将会生成一个新的值。
为了绕过这一问题,有时你可以定义一个 prop 作为实例方法,类似这样:
略。 为什么这部分内容会出现在高阶指南里?我不是说它简单什么的,而是说这个难道不应该专门找个地方放着吗?
react
有自己的严格模式,像这样开启:
import React from 'react';
function ExampleApplication() {
return (
<div>
<Header />
<React.StrictMode>
<div>
<ComponentOne />
<ComponentTwo />
</div>
</React.StrictMode>
<Footer />
</div>
);
}
开启之后可以帮你检查是不是用了过时的api之类的问题,不是什么有趣的内容。不过有一个地方很棒:
渲染阶段的生命周期包括以下 class 组件方法:
constructor componentWillMount (or UNSAFE_componentWillMount) componentWillReceiveProps (or UNSAFE_componentWillReceiveProps) componentWillUpdate (or UNSAFE_componentWillUpdate) getDerivedStateFromProps shouldComponentUpdate render setState 更新函数(第一个参数) 因为上述方法可能会被多次调用,所以不要在它们内部编写副作用相关的代码,这点非常重要。忽略此规则可能会导致各种问题的产生,包括内存泄漏和或出现无效的应用程序状态。不幸的是,这些问题很难被发现,因为它们通常具有非确定性。
如果我没有记错(这常常会发生),这好像是文档里第一次提到生命周期函数的副作用问题,而且还是在这样的一节里面。我觉得这不太应该。当然,这个可能没我想的那么重要,比较有副作用也不影响实现业务。可是如果这样,之前的一些范式也不影响实现具体业务啊。
对我而言这部分内容比严格模式来的有趣多了。
用到去查就是了。
但是这里面包含了一块内容来说明如何设定默认值:
您可以通过配置特定的 defaultProps 属性来定义 props 的默认值
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
// 指定 props 的默认值:
Greeting.defaultProps = {
name: 'Stranger'
};
// 渲染出 "Hello, Stranger":
ReactDOM.render(
<Greeting />,
document.getElementById('example')
);
不是,我寻思着前面讲 props
的时候也没提到这个呀?能不能不要把这种内容塞在这种犄角旮旯地方啊?
文档淡化了非受控组件的部分,可以看出 fb 不是很想让人去使用这一种技术,以至于都不太想让人知道这是一种什么东西。
不过,我认为受控组件和非受控组件是非常重要的内容。对这一部分的理解可以看出一个开发对组件化开发的理解。而非受控组件很容易引发bug,因为数据流在这里被截断了。理解两者的区别可以让你对这种情况变得敏感,虽然进行过一段时间开发之后,人们往往会习惯写受控组件(这也是 fb 希望看到的结果)。
在一个受控组件中,表单数据由 DOM 管理,这意味着你不需要为 DOM 写 change 回调。这会减轻我们的代码量,而且某种意义上因为更贴近传统开发模式,有的时候更方便集成一些东西——它的好处仅此而已。
此外 react 为这些表单提供了 defaultValue 属性用来给予默认值。
这一块虽然内容很少,我依然用一个 comment 来放它。这是我的个人偏好。
略。我想不出任何需要同时使用两者的场景。
我选择单独用一个comment放这一部分,因为这是高级指引的最后一部分了🤣
大部分情况下,框架提供的API都比普通业务需要的API多。所以,即使是工作事件很长的开发,也会有不了解的API,这也是我不太喜欢一直问人冷门API的人的原因。
当然,要是什么都不知道那肯定是不行的。
只列出我关心的,且文档较少提到的api
React.memo
React.memo 为高阶组件。它与 React.PureComponent 非常相似,但只适用于函数组件,而不适用 class 组件。
const MyComponent = React.memo(function MyComponent(props) { /* 使用 props 渲染 */ });
默认情况下其只会对复杂对象做浅层对比,如果你想要控制对比过程,那么请将自定义的比较函数通过第二个参数传入来实现。 此方法仅作为性能优化的方式而存在。但请不要依赖它来“阻止”渲染,因为这会产生 bug。
cloneElement()
不算很有用
React.cloneElement(
element,
[props],
[...children]
)
isValidElement()
React.Children
React.Children.map(children, function[(thisArg)])
服务端渲染相关,略
建议看文档
react
对事件进行了处理,你获得的实际是 SyntheticEvent
对象。
SyntheticEvent
实例将被传递给你的事件处理函数,它是浏览器的原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括stopPropagation()
和preventDefault()
。 如果需要获得原生事件对象,可以访问nativeEvent
属性。事件池
SyntheticEvent 是合并而来。这意味着 SyntheticEvent 对象可能会被重用,而且在事件回调函数被调用后,所有的属性都会无效。出于性能考虑,你不能通过异步访问事件。
这句话基本上等于在说:『react
事件有自己的机制,但我不打算在这里详说,你要是想了解可以自己去研究。』
注意:
如果你想异步访问事件属性,你需在事件上调用
event.persist()
,此方法会从池中移除合成事件,允许用户代码保留对事件的引用。
之后时一个长长的事件列表,略过不表
略
我知道 test
很重要啦,实际上每个人都知道 test
很重要,但是目前我还没在视图相关业务中看到过测试。
略。没有新东西。
这也是一块非常难写的部分。文档中有一半是 hook 的理念,还有一半是 hook 的用法。前者不好写,后者没必要。更过分的是,一读文档,立刻发现有一些东西暗藏在那些激动人心的叙述里面,文档如同冬天的湖面上冷澈的薄冰。
从前面的高级指引,我们可以很轻松的看出来,组件之间逻辑的复用是前段组件化编程的痛点。尽管提供了 HOC 和 render props , react
对这样的解决方案还是不太满意。另一方面,react团队
对函数式编程的热爱深入骨髓,完善函数组件的动力显然非常充足。
这一点是由 JS 的特性决定的。与其说 react
喜欢函数,其实不如说 JS 喜欢函数。对 JS 而言,逻辑复用最方便的方法永远是函数。那么扩展函数组件就成为了理所当然的选择。
同时,它带来了生命周期函数的震荡。当我们用函数的思维来思考的时候,副作用的概念是那么的理所当然,而 MVVM 下任何副作用似乎都可以归结为视图的变化,这样想来 useEffert
的存在就显得非常理所当然,仿佛它本来就应该在那里似的。当我们用这种方式思考的时候,生命周期函数就被踢到了一边,一切都是状态和副作用。
创建自定义hook极其简单,而自定义hook本质上也只是使用了 use
开头的函数而已。本质上是对逻辑的封装。
// 文档中的自定义hook
import { useState, useEffect } from 'react';
function useFriendStatus(friendID) {
const [isOnline, setIsOnline] = useState(null);
useEffect(() => {
function handleStatusChange(status) {
setIsOnline(status.isOnline);
}
ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
return () => {
ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
};
});
return isOnline;
}
这样看来,事情似乎变得简单了。样板代码变得很少,逻辑复用也很灵活。即使JS的函数意味着最大程度的灵活性,react
显然也要做一些事情去整合这种写法的状态。一个只提供状态,不渲染任何视图的函数组件,它内部的 state
和使用它的组件到底是什么关系?它有一个自己的副本吗?还是说相当于在父函数内调用了 useState
?更别说官方给出的限制了:
只在最顶层使用 Hook 不要在循环,条件或嵌套函数中调用 Hook
只在 React 函数中调用 Hook
这些镣铐或多或少向你暗示有某些暗坑潜藏在冰面之下,这种情况下的 hook
只能做为一种进阶用法存在。可能使用 hook
写出来的项目确实更解耦,更好维护,但是如果有某个不谙世事的新人乱搞一气,可能事情就会变得麻烦起来——不过,什么样的代码不是这样呢?
事情不只是这样简单而已。再往下看看,会发现 react
还提供了 useConetxt
和 useRenducer
两个 hook
,useContext
很正常,这是一个必要的功能,useReducer
则不同,无论怎么看都觉得是在冲 redux
开炮。尽管目前只是一个简单的玩具,但是 redux
的必要性又进一步降低了。react
也许不会提供数据管理功能,但是可以提供部分能力,压缩需要数据管理的情况。毕竟redux
实在不是什么轻量级的框架。
无障碍
跳过
代码分割
import
时,webpack
将自动进行分割;React.lazy()
和Suspense
配合动态分割,可以让我们在动态加载的时候,显示更友好的内容 用法:用React.lazy()
包裹import
,与Suspense
组合,同时在Suspense
的fallback
里写入加载时的内容。const OtherComponent = React.lazy(() => import('./OtherComponent')); const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
function MyComponent() { return (
); }
Context.Consumer
Context.displayName 用于
devtools
的显示错误边界
组件渲染抛出异常的时候,显示准备好的错误信息。 事件内的异常不会在这里处理(不过如果事件导致组件异常,会怎么样呢?)
static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { hasError: true }; }
componentDidCatch(error, errorInfo) { // 你同样可以将错误日志上报给服务器 logErrorToMyService(error, errorInfo); }
render() { if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染 return
Something went wrong.
; }} }
Fragments
很 cool ,不是吗?