@gyyin
2019-12-28T15:36:34.000000Z
字数 11649
阅读 606
慕课专栏
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 getDerivedStateFromErrorclass ErrorBoundary extends React.Component {constructor(props) {super(props);this.state = { hasError: false };}static getDerivedStateFromError(error) {// 更新 state 使下一次渲染可以显降级 UIreturn { hasError: true };}render() {if (this.state.hasError) {// 你可以渲染任何自定义的降级 UIreturn <h1>Something went wrong.</h1>;}return this.props.children;}}// componentDidCatchclass ErrorBoundary extends React.Component {constructor(props) {super(props);this.state = { hasError: false };}componentDidCatch(error) {// 更新 state 使下一次渲染可以显降级 UIthis.setState({ hasError: true })}render() {if (this.state.hasError) {// 你可以渲染任何自定义的降级 UIreturn <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 Expressimport { 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 Expressimport { 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');// 用户登录 contextconst UserContext = React.createContext({name: 'Guest',});class App extends React.Component {render() {const {signedInUser, theme} = this.props;// 提供初始 context 值的 App 组件,可以嵌套多个 Providerreturn (<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 的 refconst 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” 定义为 refreturn <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>})