琛若阁 發表於 2019-10-6 00:19:00

react高级特性

<ul>
<li>代码分割</li>
<li>Context</li>
<li>错误边界</li>
<li>Fragments</li>
<li>Portals</li>
<li>forwardRef</li>
<li>小结</li>
</ul>

<p>用了那么久的react, 竟不知道到原来react有那么多高级特性. 假期没事干, 试用了下一些react高级特性. 下为试用记录.</p>
<h2 id="代码分割">代码分割</h2>
<p>将一个庞大的单页应用打包成一个庞大的js, 首屏加载可能会非常糟糕, 这时可能会考虑做代码分割, 即根据模块或者路由分开打包js, 异步按需加载组件.</p>
<p>借助<code>webpack</code>和一些异步组件库(比如react-loadable, 也可以自己实现异步组件)就能很方便的实现这一点. 比如像下面这样:</p>
<pre><code class="language-javascript">// router.js
import React from 'react';
import Loadable from 'react-loadable';

const Loading = () =&gt; &lt;div&gt;Loading...&lt;/div&gt;;

///////////////页面路由配置////////////////

const Routers = {
    // 首页
    '/': Loadable({
      loader: () =&gt; import(/* webpackChunkName: "index" */'./pages/Index.jsx'),
      loading: Loading,
      }),
    // 首页
    '/index': Loadable({
      loader: () =&gt; import(/* webpackChunkName: "index" */'./pages/Index.jsx'),
      loading: Loading,
    }),
    '/404': Loadable({
      loader: () =&gt; import(/* webpackChunkName: "404" */'./pages/404/index'),
      loading: Loading,
    })
}

export default Routers;

// App.js
import React, { Component } from 'react';
import { BrowserRouter as Router, Route, Link, Switch } from "react-router-dom";

import Routers from './router';

class App extends Component {
componentDidMount() {

}
render() {
    return (
      &lt;Router&gt;
          &lt;Switch&gt;
            &lt;Route path="/" exact component={Routers["/"]} /&gt;
            &lt;Route path="/index" exact component={Routers["/index"]} /&gt;
            &lt;Route component={Routers['/404']} /&gt;
          &lt;/Switch&gt;
      &lt;/Router&gt;
    );
}
}

export default App;
</code></pre>
<p>我们直接使用<code>Loadable</code>创建异步组件, 在合适的时候使用, <code>webpack</code>会帮我做好代码分割, <code>Loadable</code>可以帮我们维护好异步组件的状态, 并且能够支持定义加载中的组件. 上边demo完整版参见web-test.</p>
<p>其实, react已经原生提供了异步组件的支持, 其使用和<code>Loadable</code>大体相同, 但是看起来会更加优雅.</p>
<pre><code class="language-javascript">import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import React, { Suspense, lazy } from 'react';

const Home = lazy(() =&gt; import(/* webpackChunkName: "home" */'./pages/Home'));
const About = lazy(() =&gt; import(/* webpackChunkName: "about" */'./pages/About'));

const App = () =&gt; (
&lt;Router&gt;
    &lt;Suspense fallback={&lt;div&gt;Loading...&lt;/div&gt;}&gt;
      &lt;Switch&gt;
      &lt;Route exact path="/" component={Home} /&gt;
      &lt;Route path="/about" component={About} /&gt;
      &lt;/Switch&gt;
    &lt;/Suspense&gt;
&lt;/Router&gt;
);

export default App;
</code></pre>
<p>这里我们使用<code>React.lazy</code>方法创建异步组件, 和<code>Loadable</code>类似, 也是使用了<code>import</code>方法, webpack会帮我们处理好这个import. 不同的是他并不支持定义loading, loading的自定义可以使用<code>Suspense</code>组件. 在其fallback中可以创建自定义的loading组件. 这个demo的完整版可参考react-demo.</p>
<h2 id="context">Context</h2>
<p>第一次接触Context是看redux源码发现的, Context特性是redux实现的核心之一. Context可以让很深的props的传递变得简单优雅, 不再需要逐级传递.</p>
<p>假设有如下组件, D组件需要拿A组件中数据, 可能需要从A通过props 传到B, 从B传到C, 从C 在通过props传到D. 非常麻烦.</p>
<pre><code class="language-javascript">&lt;A&gt;
&lt;B&gt;
    &lt;C&gt;
      &lt;D&gt;
      &lt;/D&gt;
    &lt;/C&gt;
&lt;/B&gt;
&lt;/A&gt;
</code></pre>
<p>看一下通过Context特性如何实现.</p>
<pre><code class="language-javascript">// MyContext.js
import React from 'react';

const MyContext = React.createContext("我是来自A的默认值");
export default MyContext;

// A.js
import React from 'react';
import B from './B';
import MyContext from './MyContext';
export default class A extends React.Component {
    constructor(props) {
      super(props);
    }
    render() {
      return (
            &lt;div&gt;
                &lt;MyContext.Provider value={'我是来自A的数据'}&gt;
                  &lt;B /&gt;
                &lt;/MyContext.Provider&gt;
            &lt;/div&gt;
      )
    }
}

// B.js
import React from 'react';
import C from './C';
class B extends React.Component {
    render() {
      return (
            &lt;div&gt;
                &lt;h3&gt;我是B组件&lt;/h3&gt;
                &lt;C /&gt;
            &lt;/div&gt;
      );
    }
}
export default B;

// C.js
import React from 'react';
import MyContext from './MyContext';
import D from './D';

function C() {
    return (
      &lt;MyContext.Consumer&gt;
            {
                (value) =&gt; (
                  &lt;div&gt;
                        &lt;h3&gt;我是C组件&lt;/h3&gt;
                        &lt;div&gt;我是来自A的数据: {value}&lt;/div&gt;
                        &lt;D /&gt;
                  &lt;/div&gt;
                )
            }
      &lt;/MyContext.Consumer&gt;
    )
}

export default C;

// D.js
import React from 'react';
import MyContext from './MyContext';

class D extends React.Component {
    render() {
      let context = this.context;
      return (
            &lt;div&gt;
                &lt;h3&gt;我是D组件&lt;/h3&gt;
                &lt;div&gt;我拿到了A中传递过来的数据&lt;/div&gt;
                {context}
            &lt;/div&gt;
      );
    }
}

D.contextType = MyContext;

export default D;
</code></pre>
<p>可以看到在C组件和D组件没有通过任何props传递就拿到了A中的数据.这个demo的完整版可参考react-demo. 这个例子可能看起来直接将需要共享的变量放到全局就可以了, 但是放到全局的当他变更后没法setState重新渲染, 而Context中的数据可以通过setState引起重新渲染.</p>
<p>从上边的Demo来看, Context的使用非常简单</p>
<ol>
<li>使用React.createContext()创建Context</li>
<li>在父组件使用Context.Provider传值</li>
<li>在子组件消费</li>
</ol>
<ul>
<li>对于class组件可以生命静态变量contextType消费, 见D</li>
<li>对于函数是组件, 可以用Context.Consumer来消费, 见C</li>
</ul>
<h2 id="使用-proptypes-进行类型检查">使用 PropTypes 进行类型检查</h2>
<p>一个被人调用的组件可以通过PropTypes对props参数类型进行校验, 将类型问题及早通知给调用方. 通过给组件指定静态属性propTypes并结合prop-types库可以很方便实现. prop-types需要单独安装.</p>
<p>如下是prop-types提供的一些校验器, 来自react中文文档</p>
<pre><code class="language-javascript">import PropTypes from 'prop-types';

MyComponent.propTypes = {
// 你可以将属性声明为 JS 原生类型,默认情况下
// 这些属性都是可选的。
optionalArray: PropTypes.array,
optionalBool: PropTypes.bool,
optionalFunc: PropTypes.func,
optionalNumber: PropTypes.number,
optionalObject: PropTypes.object,
optionalString: PropTypes.string,
optionalSymbol: PropTypes.symbol,

// 任何可被渲染的元素(包括数字、字符串、元素或数组)
// (或 Fragment) 也包含这些类型。
optionalNode: PropTypes.node,

// 一个 React 元素。
optionalElement: PropTypes.element,

// 一个 React 元素类型(即,MyComponent)。
optionalElementType: PropTypes.elementType,

// 你也可以声明 prop 为类的实例,这里使用
// JS 的 instanceof 操作符。
optionalMessage: PropTypes.instanceOf(Message),

// 你可以让你的 prop 只能是特定的值,指定它为
// 枚举类型。
optionalEnum: PropTypes.oneOf(['News', 'Photos']),

// 一个对象可以是几种类型中的任意一个类型
optionalUnion: PropTypes.oneOfType([
    PropTypes.string,
    PropTypes.number,
    PropTypes.instanceOf(Message)
]),

// 可以指定一个数组由某一类型的元素组成
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),

// 可以指定一个对象由某一类型的值组成
optionalObjectOf: PropTypes.objectOf(PropTypes.number),

// 可以指定一个对象由特定的类型值组成
optionalObjectWithShape: PropTypes.shape({
    color: PropTypes.string,
    fontSize: PropTypes.number
}),

// An object with warnings on extra properties
optionalObjectWithStrictShape: PropTypes.exact({
    name: PropTypes.string,
    quantity: PropTypes.number
}),   

// 你可以在任何 PropTypes 属性后面加上 `isRequired` ,确保
// 这个 prop 没有被提供时,会打印警告信息。
requiredFunc: PropTypes.func.isRequired,

// 任意类型的数据
requiredAny: PropTypes.any.isRequired,

// 你可以指定一个自定义验证器。它在验证失败时应返回一个 Error 对象。
// 请不要使用 `console.warn` 或抛出异常,因为这在 `onOfType` 中不会起作用。
customProp: function(props, propName, componentName) {
    if (!/matchme/.test(props)) {
      return new Error(
      'Invalid prop `' + propName + '` supplied to' +
      ' `' + componentName + '`. Validation failed.'
      );
    }
},

// 你也可以提供一个自定义的 `arrayOf` 或 `objectOf` 验证器。
// 它应该在验证失败时返回一个 Error 对象。
// 验证器将验证数组或对象中的每个值。验证器的前两个参数
// 第一个是数组或对象本身
// 第二个是他们当前的键。
customArrayProp: PropTypes.arrayOf(function(propValue, key, componentName, location, propFullName) {
    if (!/matchme/.test(propValue)) {
      return new Error(
      'Invalid prop `' + propFullName + '` supplied to' +
      ' `' + componentName + '`. Validation failed.'
      );
    }
})
};
</code></pre>
<p>也可以给props指定默认值</p>
<pre><code class="language-javascript">class Greeting extends React.Component {
render() {
    return (
      &lt;h1&gt;Hello, {this.props.name}&lt;/h1&gt;
    );
}
}

// 指定 props 的默认值:
Greeting.defaultProps = {
name: 'Stranger'
};

// 渲染出 "Hello, Stranger":
ReactDOM.render(
&lt;Greeting /&gt;,
document.getElementById('example')
);
</code></pre>
<p>检验和默认值也可以这样写</p>
<pre><code class="language-javascript">class Greeting extends React.Component {
static defaultProps = {
    name: 'stranger'
}
static propTypes = {
    name: PropTypes.string,
}
render() {
    return (
      &lt;div&gt;Hello, {this.props.name}&lt;/div&gt;
    )
}
}
</code></pre>
<h2 id="错误边界">错误边界</h2>
<p>错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。</p>
<p>当子组件抛出错误时, 下边的两个生命周期会被触发, 可以在这里边处理错误, 显示降级UI, 向服务端上报错误.</p>
<p>错误边界组件核心生命周期如下</p>
<pre><code class="language-javascript">static getDerivedStateFromError()
componentDidCatch()
</code></pre>
<p>下面是个小demo</p>
<pre><code class="language-javascript">// index.js

import React from 'react';
import ErrorComponent from './ErrorComponent';

export default class Home extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
            hasError: false,
      }
    }

    static getDerivedStateFromError() {
      console.log('getDerivedStateFromError');
      return { hasError: true };
    }
    componentDidCatch (error, info) {
      console.log('componentDidCatch');
      console.log({
            error,
            info,
      })
    }
    render() {
      if (this.state.hasError) {
            return &lt;div&gt;发生了某种错误&lt;/div&gt;
      }
      return (
            &lt;div&gt;
                &lt;h3&gt;错误边界测试&lt;/h3&gt;
                &lt;ErrorComponent /&gt;
            &lt;/div&gt;
      )
    }
}

// ErrorComponent.js
import React from 'react';

export default class Home extends React.Component {
    state = {
      showError: false,
    }
    componentDidMount() {
    }
    click = () =&gt; {
      this.setState({
            showError: true,
      })
    }
    render() {
      if (this.state.showError) {
            throw new Error("抛出错误");
      }
      return (
            &lt;div onClick={this.click}&gt;我是产生错误的组件&lt;/div&gt;
      )
    }
}
</code></pre>
<p>我们可以在componentDidCatch(error, info) 获取错误信息, 错误信息error.message, 错误堆栈error.stack, 组件堆栈info.componentStack, 这些信息可以显示给用户, 也可以上报到服务器. 可以在getDerivedStateFromError返回state, 渲染降级组件.</p>
<h2 id="fragments">Fragments</h2>
<p>Fragments解决了一个组件不能返回多个元素的问题, 没有Fragments时一个组件没法返回多个元素, 所以我们经常用个div包一下, 结果是增加了一个多余的dom节点, 甚至产生不合法的dom, 比如下边这样的.</p>
<pre><code class="language-javascript">// 组件1
function Columns() {
    return (
      &lt;div&gt;
            &lt;td&gt;第一列&lt;/td&gt;
            &lt;td&gt;第二列&lt;/td&gt;
      &lt;/div&gt;
    )
}
// 组件2
function Table() {
return (
    &lt;table&gt;
      &lt;tr&gt;
      &lt;Columns/&gt;
      &lt;/tr&gt;
    &lt;/table&gt;
)
}
</code></pre>
<p>因为没法返回多个元素, 所以在Columns组件中使用了div包裹两个td, 然后在Table组件使用, 结果就产生了tr里边放td的错误结构. 使用Fragments特性可以很方便的解决这个问题. 如下. 只要用个<code>&lt;React.Fragment&gt;</code>包装就可以了, 也可以写成<code>&lt;&gt;something&lt;/&gt;</code>.</p>
<pre><code class="language-javascript">function Columns() {
    return (
      &lt;React.Fragment&gt;
            &lt;td&gt;第一列&lt;/td&gt;
            &lt;td&gt;第二列&lt;/td&gt;
      &lt;/React.Fragment&gt;
    )
}
</code></pre>
<h2 id="portals">Portals</h2>
<p>Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的方案. portal 的典型使用场景是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框.</p>
<p>如下是一个toast组件 demo, 完整版参考react-demo</p>
<pre><code class="language-javascript">// Toast.js
import React from 'react';
import ReactDOM from 'react-dom';
import './Toast.css';

export default class Toast extends React.Component {
constructor(props) {
    super(props);
    this.el = document.querySelector('body');
}

render() {
    return ReactDOM.createPortal(
      (
          &lt;div className="toast"&gt;
            &lt;div className="toast-inner"&gt;
                {this.props.text}
            &lt;/div&gt;
          &lt;/div&gt;
      ),
      this.el,
    );
}
}
// PortalTest.js
import React from 'react';
import Toast from './Toast';
export default class PortalTest extends React.Component {
    render() {
      return (
            &lt;div&gt;
                &lt;h1&gt;PortalTest&lt;/h1&gt;
                &lt;Toast text="toast提示"/&gt;
            &lt;/div&gt;
      )
    }
}
</code></pre>
<p>结果如图</p>
<p><img src="https://img2018.cnblogs.com/blog/1128201/201910/1128201-20191006001816015-248221883.png" alt="Portals测试结果" loading="lazy"></p>
<p>可以发现Toast这个组件不是在其父元素中, 而是跑到了我们期望的body里边. 这样不管父组件写overflow:hidden;还是其他都不会影响到这个toast.</p>
<h2 id="forwardref">forwardRef</h2>
<p>forwardRef是一种将ref转移到子组件的方式.</p>
<p>forwardRef 主要有两种使用场景</p>
<ul>
<li>希望对基础组件做一些封装, 但是希望基础组件的实例的方法能被调用</li>
<li>高阶组件中希望ref指向被包裹的组件而不是外层组件</li>
</ul>
<ol>
<li>关于第一种场景</li>
</ol>
<p>之前做ReactNative时有个FlatList组件, 希望对他封装一层, 但是又希望调用方可以使用ref或则FlatList的实例, 方便调用上边的方法. 这时就可以用forwardRef. 下面举的是 input的例子, 我们希望封装一下, 但让调用方仍然可以通过ref获取dom调用focus.</p>
<pre><code class="language-javascript">import React from 'react';

const LabelInput =
    React.forwardRef((props, ref) =&gt; {
      return &lt;div&gt;
            &lt;label&gt;{props.label}&lt;/label&gt;
            &lt;input ref={ref} className="input" style={{ border: '1px solid red' }} /&gt;
      &lt;/div&gt;
    })

export default class Home extends React.Component {
    constructor(props) {
      super(props);
      this.ref = React.createRef();
    }

    focus = () =&gt; {
      try {
            this.ref.current.focus();
      } catch (e) {
            console.log(e);
      }
    }
    render() {
      return (
            &lt;div&gt;
                &lt;h1&gt;测试forwardRef&lt;/h1&gt;
                &lt;LabelInput ref={this.ref} label="手机号"/&gt;
                &lt;button onClick={this.focus}&gt;点击input可以获取焦点&lt;/button&gt;
            &lt;/div&gt;
      )
    }
}
</code></pre>
<p>在LabelInput组件里边将ref转到了input上, 从而外边的调用方可以直接掉focus方法. 如果不做转发, 那么ref将指向div, 再要找到里边的input就比较麻烦了, 而且破坏了组件的封装性.</p>
<ol start="2">
<li>关于第二种场景</li>
</ol>
<pre><code class="language-javascript">import React from 'react';

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;

            // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
            return &lt;Component ref={forwardedRef} {...rest} /&gt;;
      }
    }

    // 注意 React.forwardRef 回调的第二个参数 “ref”。
    // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
    // 然后它就可以被挂载到被 LogPros 包裹的子组件上。
    return React.forwardRef((props, ref) =&gt; {
      return &lt;LogProps {...props} forwardedRef={ref} /&gt;;
    });
}

class InnerComp extends React.Component {
    render() {
      return &lt;div id="InnerComp"&gt;
            被包裹的组件-text={this.props.text}
      &lt;/div&gt;
    }
}

const Comp = logProps(InnerComp);

export default class Home extends React.Component {
    constructor(props) {
      super(props);
      this.ref = React.createRef();
    }
    click = () =&gt; {
      console.log(this.ref.current);
    }
    render() {
      return (
            &lt;div&gt;
                &lt;h1&gt;测试forwardRef&lt;/h1&gt;
                &lt;Comp ref={this.ref} text="测试" /&gt;
                &lt;button onClick={this.click}&gt;点击打印ref&lt;/button&gt;
            &lt;/div&gt;
      )
    }
}
</code></pre>
<p>这里点击打印的是InnerComp组件, 如果去掉forwardRef则打印LogProps组件. 可见通过forwardRef可以成功将ref传递到被包裹的组件.</p>
<p><strong>注意</strong> 函数组件不能给ref, 只有class组件可以. 测试发现的.</p>
<h2 id="小结">小结</h2>
<table>
<thead>
<tr>
<th style="text-align: center">特性</th>
<th style="text-align: left">特性描述</th>
<th style="text-align: left">使用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: center">代码分割</td>
<td style="text-align: left">提供异步组件,实现拆包</td>
<td style="text-align: left">需要优化包体积时使用</td>
</tr>
<tr>
<td style="text-align: center">Context</td>
<td style="text-align: left">跨层级传递数据</td>
<td style="text-align: left">优化多层级传递props问题</td>
</tr>
<tr>
<td style="text-align: center">PropTypes 进行类型检查</td>
<td style="text-align: left">可以对props的类型加上校验器</td>
<td style="text-align: left">希望及早暴露props类型错误</td>
</tr>
<tr>
<td style="text-align: center">错误边界</td>
<td style="text-align: left">提供不过子组件错误和在错误返回指定state的生命周期</td>
<td style="text-align: left">希望在渲染错误时提供降级UI或上报错误</td>
</tr>
<tr>
<td style="text-align: center">Fragments</td>
<td style="text-align: left">提供在一个组件返回多个元素的能力</td>
<td style="text-align: left">希望在一个组件返回多个元素</td>
</tr>
<tr>
<td style="text-align: center">Portals</td>
<td style="text-align: left">提供将元素渲染到父元素之外的能力</td>
<td style="text-align: left">Toast, Modal等</td>
</tr>
<tr>
<td style="text-align: center">forwardRef</td>
<td style="text-align: left">转发传进来的ref</td>
<td style="text-align: left">希望将外部传递的ref转移到别的元素上,而不是自己</td>
</tr>
</tbody>
</table>


</div>
<div id="MySignature" role="contentinfo">
    文章来源: http://www.cnblogs.com/floor/,转载请注明,谢谢!<br><br>
来源:https://www.cnblogs.com/floor/p/11626242.html
頁: [1]
查看完整版本: react高级特性