@gyyin
2019-12-28T23:36:34.000000Z
字数 11649
阅读 349
慕课专栏
React 进入 v16 版本后有了翻天覆地的变化,不仅是 api 上面,甚至连底层渲染都做了很大的改动,今天来聊一聊 React v16 的那些新特性。
React v16 是从2017年9月26日发布了第一个版本,截止到今天(2019年12月23日)目前 React 已经到了 16.12.0 版本,可以在 GitHub 上查看版本更新信息:releases
React 16 依赖于 Map 和 Set 以及 requestAnimationFrame。如果你还在使用没有原生提供这些功能的旧版浏览器和设备(例如 <IE11
),则可能需要包含 polyfill。
render() {
return [
<li>111</li>
<li>222</li>
<li>333</li>
]
}
render() {
return 'hello, world';
}
如果错误在构造函数、生命周期或者渲染的时候抛出。那么该组件会被卸载,不会影响到其他组件。
如果 class 组件定义了生命周期方法 static getDerivedStateFromError()
或 componentDidCatch()
中的任何一个(或两者),它就成为了 Error boundaries。通过生命周期更新 state 可让组件捕获树中未处理的 JavaScript 错误并展示降级 UI。
// static getDerivedStateFromError
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可以显降级 UI
return { hasError: true };
}
render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的降级 UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// componentDidCatch
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error) {
// 更新 state 使下一次渲染可以显降级 UI
this.setState({ hasError: true })
}
render() {
if (this.state.hasError) {
// 你可以渲染任何自定义的降级 UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
createPortal 允许你将组件挂载到任意的 DOM 节点下面,这样就可以实现将当前的组件挂载到父组件 DOM 层次之外的地方。
举个例子,比如之前所有的组件都被挂载了 ReactDOM.render
的第二个参数节点下面,如果想要实现一个模态框,不得不通过 CSS 定位的方式让蒙层覆盖住当前页面。
const Modal = (props) => {
const styles = {
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: 'gray'
}
return (
<div style={styles}>{props.children}</div>
)
}
// 调用
<Modal>
<h1>hello, world</h1>
</Modal>
但是如果父元素设置了 oveflow: hidden
,那么就不得不突破父元素的容器。虽然之前 React 提供了 unstable_renderSubtreeIntoContainer
方法,但这是个不稳定的 api。因此在 React v16.0 提供了 createPortal 这个特性,允许你将组件挂载到任意 DOM 之下。
createPortal 存在于 react-dom 这个库中,并非在 React 中。
在 html 文件中预留出一个 DOM 节点,和 ReactDOM 插入的节点同级。
<body>
<div id="app"></div> // 用于 ReactDOM.render 插入
<div id="modal"></div>
</body>
将 Modal 组件中的内容插入到提前创建好的 #modal
节点里面,这样就可以做到脱离原来应用的父容器 #app
。
class Modal extends React.Component {
constructor(props) {
super(props);
this.root = document.querySelector("#modal");
this.wrap = document.createElement("div");
}
componentDidMount() {
this.root.appendChild(this.wrap);
}
componentWillUnmount() {
this.root.removeChild(this.wrap);
}
render() {
return ReactDOM.createPortal(
this.props.children,
this.wrap
)
}
}
v16.0 对服务端渲染进行了一些新的变动,提供了新的 ReactDOM.hydrate
方法用于在服务端环境下代替 ReactDOM.render
,原来的 ReactDOM.render
用于客户端渲染。但 ReactDOM.render
依然可以使用,这个改动是向下兼容的。
v16.0 还提供了新的 renderToNodeStream
方法。renderToString
方法会将生成字符串到页面渲染一气呵成,如果组件比较大,同步的过程就会花费很多时间,带来的用户体验不够好。
// using Express
import { renderToString } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
res.write("<div id='content'>");
res.write(renderToString(<MyPage/>));
res.write("</div></body></html>");
res.end();
});
而 renderToNodeStream
允许使用可读流的形式将 string 传给 response 对象,一般在 Express、Koa、Nest 等 NodeJS 框架中,response 对象都是一个可写流。这样就不需要等待 html 字符串全部渲染成功后再返回给客户端,可以做到边读边渲染,用户也不需要等待很长时间后才看到页面显示出来。
// using Express
import { renderToNodeStream } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
res.write("<div id='content'>");
const stream = renderToNodeStream(<MyPage/>);
stream.pipe(res, { end: false });
stream.on('end', () => {
res.write("</div></body></html>");
res.end();
});
});
<B />
来替换 <A />
的时候,B.componentWillMount
总是在 A.componentWillUnmount
之前执行,而以前在某些场景下执行顺序是相反的。类似前面直接返回数组,React v16.2 提供了 Fragment 组件,它允许直接将返回元素,而不需要用父节点来包裹。
const App = () => (
<React.Fragment>
<h1>hello</h1>
<h1>world</h1>
</React.Fragment>
)
除此之外,React 还提供了语法糖 <></>
来快速使用 React.Fragment。
const App = () => (
<React.Fragment>
<h1>hello</h1>
<h1>world</h1>
</React.Fragment>
)
react Context 提供了一种新的组件通信方式,用于解决子孙组件通信需要多次传递 props(简直就是套娃啊)。
我们先来看一下老的 Context api 的用法。
一般需要你在祖先组件里面提供 Context,声明一个 Context 对象类型,并且实现 getChildContext 方法,返回一个 Context 对象。
class Parent extends Component {
// 声明 Context 对象类型
static childContextTypes = {
name: PropTypes.string,
eat: PropTypes.func
}
// 返回 Context 对象
getChildContext () {
return {
name: 'xiaoming',
eat: () => 'eat'
}
}
render () {
return <Child />
}
}
然后子孙组件接收到这个 Context 之后,需要通过 static contextTypes
声明需要接收的 Context 对象类型和属性之后,直接通过 this.context
来访问。
class Child extends Component {
static contextTypes = {
name: PropTypes.string,
eat: PropTypes.func
}
render() {
const {
name,
eat
} = this.context;
// ...
}
}
而在 React v16.3 中,对 Context 这个 api 进行了重新设计。具体就是增加了 createContext
这个方法,可以来创建一个 Context 对象,这个对象包含了 Provider
和 Consumer
两个组件,可以理解为“生产者”和“消费者”。
createContext 接收一个默认值,返回一个 Context 对象。
const ColorContext = React.createContext('red');
当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider
中读取到当前的 context 值。
只有当组件所处的树中没有匹配到 Provider
时,其 defaultValue
参数才会生效。这有助于在不使用 Provider
包装组件的情况下对组件进行测试。
注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。
<MyContext.Provider value={/* 某个值 */}>
每一个 Context 对象都会返回一个 Provider
组件,它允许消费组件来订阅 context 的变化。
Provider
接收一个 value
属性,传递给消费组件。一个 Provider
可以和多个消费组件有对应关系。同时,多个 Provider
也可以嵌套使用,里层的会覆盖外层的数据。
当 Provider
的 value
值发生变化时,它内部的所有消费组件都会重新渲染。Provider
及其内部 consumer
组件都不受制于 shouldComponentUpdate
函数,因此当 consumer
组件在其祖先组件退出更新的情况下也能更新。
<MyContext.Consumer>
{value => /* 基于 context 值进行渲染*/}
</MyContext.Consumer>
每一个 Context 对象也都会返回一个 Consumer
组件,这个组件也可以订阅到 context 变更。
Consumer
组件需要接收一个函数当做子元素(function as a child),现在一般称为 render props
。
这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值。
如果没有对应的 Provider,value 参数就是传递给 createContext() 的 defaultValue。
除了使用 Consumer
组件来获取到 context 的值,还可以继续通过设置 static contextType
来在组件中通过 this.context
的形式拿到 context 值。
和老的 contextType 区别在于,现在的 contextType 应该是一个 context 对象。
const ThemeContext = React.createContext('light');
class Child extends Component {
static contextType = ThemeContext;
render() {
const value = this.context;
// ...
}
}
Context 还允许你使用多个 context 嵌套。以 React 官网这个为例:
// Theme context,默认的 theme 是 “light” 值
const ThemeContext = React.createContext('light');
// 用户登录 context
const UserContext = React.createContext({
name: 'Guest',
});
class App extends React.Component {
render() {
const {signedInUser, theme} = this.props;
// 提供初始 context 值的 App 组件,可以嵌套多个 Provider
return (
<ThemeContext.Provider value={theme}>
<UserContext.Provider value={signedInUser}>
<Layout />
</UserContext.Provider>
</ThemeContext.Provider>
);
}
}
function Layout() {
return (
<div>
<Sidebar />
<Content />
</div>
);
}
// 一个组件可能会消费多个 context,多个 render props 嵌套
function Content() {
return (
<ThemeContext.Consumer>
{theme => (
<UserContext.Consumer>
{user => (
<ProfilePage user={user} theme={theme} />
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
);
}
hooks 是 React v16.8 引入的特性,它允许你在函数组件中使用 state 和某些特性。React 官方认为,类组件会让人难以理解,比如为什么要在事件函数里面重新绑定 this?在在多数情况下,也不可能将类组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。
React Hooks 可以解决组件代码逻辑复用的问题,通过编写自定义 Hook 就能将一些通用逻辑封装起来。也可以解决多个状态嵌套的问题,比如我们上面的多个 context 对象嵌套,也可以编写自定义 Hook 将其平铺开。
useState 接收一个值或者一个函数,它返回一个数组,数组包括传入的值、修改值的方法。
const App = () => {
const [count, setCount] = useState(0);
return <h1 onClick={() => setCount(count + 1)}>{count}<h1>
}
每次点击当前这个 h1,就会调用 setCount 方法来将 count 增加1,同时这个组件将会重新渲染。
你可能会问那每次渲染不就重新执行 useState 了吗?那 count 岂不是又变成了0?事实上不是这样的,React 会记录执行 useState 时候的下标,在下次渲染的时候会取前一次缓存起来的值,而非重新初始化。
正是因为如此,所以 React 官方建议只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook。
当 useState 接收一个函数的时候,函数返回值就是当前的 state 值。
const App = () => {
const [count, setCount] = useState(() => 0);
return <h1 onClick={() => setCount(count + 1)}>{count}<h1>
}
由于函数组件只是一个函数,无法处理具有副作用(网络请求、DOM 操作等等)的场景,所以常常需要用到生命周期的钩子,这就意味着要用类组件。React 提供了 useEffect 方法来在函数组件中解决这个问题。useEffect 接收一个可能有副作用代码的函数。
以一个绑定 scroll 事件的组件为例,scroll 事件就是副作用,在普通函数组件中是无法处理这种副作用的,更何况 scroll 事件还需要手动清除。
class App extends Component {
onScroll() {}
componentDidMount() {
window.addEventListener('scroll', this.onScroll);
}
componentWillUnmout() {
window.removeEventListener('scroll', this.onScroll);
}
}
但是如果使用了 useEffect,这个代码会变得异常简单。
const App = () => {
useEffect(() => {
const onScroll = () => {};
window.addEventListener('scroll', onScroll);
return () => {
window.removeEventListener('scroll', onScroll);
}
}, [])
}
来一起理解一下 useEffect,它接收两个参数,第一个参数是一个函数,第二个参数是一个数组(也可以不传)。
如果第二个参数什么也不传,那么第一个参数就会在组件每一次渲染之后执行,而其返回函数则会在组件卸载前执行,就类似于 componentDidMount、componentDidUpdate 和 componentWillUnmout。
如果第二个参数传了一个空数组,那么第一个参数只会在组件第一次渲染之后执行,返回函数则也是在卸载前执行,就类似于 componentDidMount 和 componentWillUnmout。
除此之外,如果第二个参数传的是一个有值的数组,那么这个函数就只会在这个值改变后才执行。如果这个值没有变化,不管组件渲染多少次,这个函数都不会执行。
const App = (props) => {
useEffect(() => {
console.log(props.name); // 每次传入新的 name 时就会打印
}, [props.name])
}
这种场景下的 useEffect 非常像 vue 里面的 watch,我们也可以基于此实现一个 watch。
const watch = (value, cb) => {
useEffect(cb, [value])
}
watch(props.name, () => console.log(props.name))
前面说过,对于通用的逻辑来说,可以编写自定义的 Hook 来提供给组件使用。一般自定义 Hook 命名都是以 use 开头,不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 规则。
接下来就带你手把手实现一个 Hook。假设有这么一种场景,我的页面中有很多类似弹层的组件,如果点击弹层之外的区域,就需要关闭这个弹层,但是如果点击这个弹层,就不应该关闭。
所以我们需要对整个页面绑定点击事件,但需要排除当前这个组件的 DOM,这个功能就可以写成自定义 Hook 来复用。
const useClickAway = (ref, cb) => {
useEffect(() => {
const clickHandler = (e) => {
if (!e.target.contains(ref)) {
cb();
}
}
document.body.addEventListener('click', clickHandler);
return () => {
document.body.removeEventListener('click', clickHandler);
}
}, [])
}
使用的时候,只要传入组件的 ref 和 关闭方法就行了。
const App = () => {
const [open, setOpen] = useState(true);
const ref = createRef();
useClickAway(ref, () => setOpen(false));
return open ? <Modal ref={ref} /> : null;
}
在 React v16.3 之前,如果想要获取到 Ref,一般有这么两种方法。
直接规定一个 ref 的名字:
class App extends Component {
componentDidMount() {
const ref = this.refs.container
}
render() {
return <div ref="container"></div>
}
}
或者传给 ref 属性一个函数,来动态设置 Ref。
class App extends Component {
container = null;
componentDidMount() {
const ref = this.container
}
render() {
return <div ref={r => this.container = r}></div>
}
}
React v16.3 提供了新的 createRef 和 forwardRef 来操作 Ref。
React.createRef
可以获取到 Ref 对象。
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.inputRef = React.createRef();
}
render() {
return <input type="text" ref={this.inputRef} />;
}
componentDidMount() {
this.inputRef.current.focus();
}
}
React.forwardRef 会创建一个 React 组件,这个组件能够将其接受的 ref 属性转发给子组件。被 React.forwardRef 包裹的函数组件,第二个参数就是 Ref 对象。
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 你可以直接获取到 button 的 ref
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
forwardRef 一般用于以下两种场景:
1、转发 Ref 到 DOM 组件
一般来说,我们在使用组件的时候,不关心其内部实现细节,防止组件过度依赖其他组件的 DOM 结构。
但是在例如封装的 input 组件中,常常会需要管理输入框的焦点,这就需要能够获取到 input 的 ref。
2、在高阶组件中转发 Ref
如果我的多个组件都使用了某个高阶组件,但又需要获取到 Ref 对象,该怎么办?ref 属性并不能通过 props 来传下去,这样只能通过 forwardRef 来转发这个高阶组件的 ref。
具体实现思路就是,在组件外创建一个 Ref 对象,将这个对象通过 forwardRef 转发给这个高阶组件,高阶组件内部将这个 ref 当做 props 传给被包裹的组件的 ref 属性,这样就获取到了隐藏在高阶组件中的被包裹组件的 ref。
function logProps(Component) {
class LogProps extends React.Component {
render() {
const {forwardedRef, ...rest} = this.props;
// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// 注意 React.forwardRef 回调的第二个参数 “ref”。
// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
// 然后它就可以被挂载到被 LogProps 包裹的子组件上。
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
React v16.6 中新增了 lazy 和 Suspense 两个属性,主要用于代码分割。
React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。
const OtherComponent = React.lazy(() => import('./OtherComponent'));
React.lazy 接受一个函数,这个函数需要使用动态 import。它返回一个 Promise,这个 Promise 需要 resolve 一个默认导出的 React 组件。
因此,如果你的 OtherComponent 如果不是默认导出的话,就只能使用一个中间文件来重新导出为默认模块。
这个代码会在第一次渲染的时候再导入 OtherComponent 这个文件。
lazy 组件需要包裹在 Suspense 组件中渲染,这样允许我们在等待 lazy 组件加载的时候进行一些占位处理,比如在加载成功之前,给其设置一个占位的 Loading 组件。
fallback 是一个 ReactElement,会在加载成功前渲染。
const OtherComponent = React.lazy(() => import('./OtherComponent'));
function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}
注意:React.lazy 和 Suspense 还不支持服务端渲染。如果你想要在使用服务端渲染的应用中使用,推荐使用 Loadable Components 这个库。
memo 等效于 PureComponent,后者是用于类组件,前者被提供用来函数组件。memo 可以帮助对 props 进行浅比较,减少一些不必要的 render。
const App = React.memo((props) => {
return <h1>{props.name}</h1>
})