IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    React 复习笔记(上) · 看不见我的美 · 是你瞎了眼

    馬腊咯稽发表于 2020-02-25 00:00:00
    love 0
    React 复习笔记(上)

    JSX 与 React 元素

    JSX 是一个 JavaScript 的语法扩展;在编译之后,JSX 会被 Babel 转为函数调用,对其取值后得到 JS 对象。JSX 使用 camelCase 来定义属性的名称,而不使用 HTML 属性名称的命名约定。

    对于 React,JSX 仅仅是 React.createElement(component, props, …children) 函数的语法糖;包含在开始和结束标签之间的 JSX 表达式内容将作为特定属性 props.children 传递给外层组件。

    与浏览器的 DOM 元素不同,React 元素是不可变对象;一旦被创建,就无法更改它的子元素或者属性;一个元素就像电影的单帧:它代表了某个特定时刻的 UI。React 元素是创建开销极小的普通对象;ReactDOM 会将元素和它的子元素与它们之前的状态进行比较,通过 Diff 算法使 DOM 与 React 元素保持一致。

    组件生命周期

    当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:

    • constructor()
    • static getDerivedStateFromProps()
    • render()
    • componentDidMount()

    当组件的 props 或 state 发生变化时会触发更新;其生命周期调用顺序如下:

    • static getDerivedStateFromProps()
    • shouldComponentUpdate()
    • render()
    • getSnapshotBeforeUpdate()
    • componentDidUpdate()

    当组件从 DOM 中移除时,其生命周期调用顺序如下:

    • componentWillUnmount()

    当渲染过程、生命周期、或子组件的构造函数中抛出错误时,其生命周期调用顺序如下:

    • static getDerivedStateFromError()
    • componentDidCatch()

    错误边界

    错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JS 错误,并且会渲染出备用 UI,而不是渲染那些崩溃了的子组件树;如果一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界;自 React 16 起,任何未被错误边界捕获的错误将会导致整个 React 组件树被卸载。

    只有 class 组件才可以成为错误边界组件,如果一个 class 组件中定义了 static getDerivedStateFromError 或 componentDidCatch 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。当抛出错误后,请使用 static getDerivedStateFromError 渲染备用 UI,使用 componentDidCatch 打印错误信息。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    
    class ErrorBoundary extends React.Component {
     constructor(props) {
     super(props);
     this.state = { hasError: false };
     }
     static getDerivedStateFromError(error) {
     // 更新 state,渲染能够显示降级后的 UI
     return { hasError: true };
     }
     componentDidCatch(error, errorInfo) {
     // 将错误日志上报给服务器
     logErrorToMyService(error, errorInfo);
     }
     render() {
     if (this.state.hasError) {
     // 自定义降级后的 UI 并渲染
     return <h1>出错啦...</h1>;
     }
     return this.props.children;
     }
    }
    // 然后你可以将它作为一个常规组件去使用
    <ErrorBoundary>
     <MyWidget />
    </ErrorBoundary>;
    

    错误边界无法捕获以下场景中产生的错误:

    1. 事件处理;
    2. 异步代码,例如 setTimeout 或 requestAnimationFrame 回调函数;
    3. 服务端渲染;
    4. 它自身抛出来的错误(并非它的子组件)。

    Refs

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    class AutoFocusTextInput extends React.Component {
     constructor(props) {
     super(props);
     this.textInput = React.createRef();
     }
     componentDidMount() {
     this.textInput.current.focusTextInput();
     }
     render() {
     return <CustomTextInput ref={this.textInput} />;
     }
    }
    

    refs 主要用于:

    1. 管理焦点,文本选择或媒体播放;
    2. 触发强制动画;
    3. 集成第三方 DOM 库。

    ref 的值根据节点的类型而有所不同:

    1. 当 ref 被传递给 render 中的元素时,对该节点的引用可以在 ref 的 current 属性中被访问;
    2. 当 ref 属性用于 HTML 元素时,构造函数中使用 React.createRef() 创建的 ref 接收底层 DOM 元素作为其 current 属性;
    3. 当 ref 属性用于自定义 class 组件时,ref 对象接收组件的挂载实例作为其 current 属性。

    React.createContext

    创建一个 Context 对象,每个 Context 对象都会返回一个 Provider 组件,它允许消费组件订阅 Context 的变化;当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 Context 值。

    Provider 接收一个 value 属性,传递给消费组件;一个 Provider 可以和多个消费组件有对应关系;多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

    当 Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染;Provider 及其内部 Consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 Consumer 组件在其祖先组件退出更新的情况下也能更新。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    const ThemeContext = React.createContext('light'); // 默认值 light
    const App = () => {
     /**
     * 使用一个 Provider 来将当前的 theme 传递给以下的组件树
     * 无论多深,任何组件都能读取这个值
     * 在这个例子中,我们将“dark”作为当前的值传递下去
     */
     return (
     <ThemeContext.Provider value="dark">
     <Toolbar />
     </ThemeContext.Provider>
     );
    };
    // 中间的组件再也不必指明往下传递 theme 了
    const Toolbar = () => (
     <div>
     <ThemedButton />
     </div>
    );
    // 指定 contextType 读取当前的 ThemeContext,React 会往上找到最近的 ThemeContext.Provider,然后使用它的值
    const ThemedButton = () => <Button theme={this.context} />;
    ThemedButton.contextType = ThemeContext;
    

    Context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。

    1
    2
    3
    4
    
    const MyContext = React.createContext(/* some value */);
    MyContext.displayName = 'MyDisplayName';
    <MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
    <MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
    

    消费多个 Context。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    
    const ThemeContext = React.createContext('light');
    const UserContext = React.createContext({
     name: 'Guest'
    });
    const App = ({ signedInUser, theme }) => {
     // 提供初始 context 值的 App 组件
     return (
     <ThemeContext.Provider value={theme}>
     <UserContext.Provider value={signedInUser}>
     <Layout />
     </UserContext.Provider>
     </ThemeContext.Provider>
     );
    };
    const Layout = () => (
     <div>
     <Sidebar />
     <Content />
     </div>
    );
    const Content = () => (
     <ThemeContext.Consumer>
     {theme => (
     <UserContext.Consumer>
     {user => <ProfilePage user={user} theme={theme} />}
     </UserContext.Consumer>
     )}
     </ThemeContext.Consumer>
    );
    

    ReactDOM.createPortal

    将子节点渲染到父组件以外的 DOM 节点,AntDesign 的弹窗就是使用 ReactDOM.createPortal 进行渲染的;尽管 portal 可以被放置在 DOM 树中的任何地方,但其行为和普通的 React 子节点行为一致,portal 仍存在于 React 树,且与 DOM 中的位置无关;一个从 portal 内部触发的事件会一直冒泡至包含 React 树的祖先,即便这些元素并不是 DOM 中的祖先。

    ReactDOM.createPortal 返回的 VitrualDOM 对象会有一个叫做 containerInfo 的属性指向挂载的 DOM 节点;React 内部正是通过判断 containerInfo 属性是否存在来决定 VitrualDOM 的挂载方式的。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    
    // 在 DOM 中有两个容器是兄弟级
    const appRoot = document.getElementById('app-root');
    const modalRoot = document.getElementById('modal-root');
    const Child = () => {
     // 这个按钮的点击事件会冒泡到父元素
     return (
     <div className="modal">
     <button>Click</button>
     </div>
     );
    };
    class Modal extends React.Component {
     constructor(props) {
     super(props);
     this.el = document.createElement('div');
     }
     componentDidMount() {
     modalRoot.appendChild(this.el);
     }
     componentWillUnmount() {
     modalRoot.removeChild(this.el);
     }
     render() {
     return ReactDOM.createPortal(this.props.children, this.el);
     }
    }
    class Parent extends React.Component {
     constructor(props) {
     super(props);
     this.state = { clicks: 0 };
     }
     handleClick = () => {
     // 当子元素里的按钮被点击时,这个将会被触发更新父元素的 state,即使这个按钮在 DOM 中不是直接关联的后代
     this.setState(state => ({
     clicks: state.clicks + 1
     }));
     };
     render() {
     return (
     <div onClick={this.handleClick}>
     <p>{this.state.clicks}</p>
     <Modal>
     <Child />
     </Modal>
     </div>
     );
     }
    }
    ReactDOM.render(<Parent />, appRoot);
    

    React.forwardRef

    React.forwardRef 用于 refs 转发,将 ref 自动地通过组件传递到其一子组件;React.forwardRef 会创建一个 React 组件,这个组件能够将其接受的 ref 属性转发到其组件树下的另一个组件中。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    
    /**
     * 1. 通过调用 React.createRef 创建了一个 ref 并将其赋值给 btn 变量
     * 2. 通过指定 ref 为 JSX 属性,将其向下传递给 <FancyButton ref={btn}>
     * 3. React 传递 ref 给 forwardRef 内函数 (props, ref) => ...,作为其第二个参数
     * 4. 向下转发该 ref 参数到 <button ref={ref}>,将其指定为 JSX 属性
     * 5. 当 ref 挂载完成,ref.current 将指向 <button> DOM 节点
     */
    const FancyButton = React.forwardRef((props, ref) => (
     <button ref={ref} className="FancyButton">
     {props.children}
     </button>
    ));
    const App = () => {
     const btn = React.createRef();
     // 可以获取底层 DOM 节点 button 的 ref
     return (
     <>
     <FancyButton ref={btn}>点我</FancyButton>
     </>
     );
    };
    

    React.lazy & Suspense

    React.lazy 接受一个函数,这个函数需要动态调用 import 并返回一个 Promise,该 Promise 需要 resolve 一个 default export 的 React 组件;是的,React.lazy 目前只支持默认导出。应该在 Suspense 组件中渲染 React.lazy 组件,如此使得在等待加载组件时做优雅降级。

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    // 基于路由的代码分割
    const Home = React.lazy(() => import('./pages/Home'));
    const About = React.lazy(() => import('./pages/About'));
    const App = () => (
     <Router>
     <Suspense fallback={<div>Loading...</div>}>
     <Switch>
     <Route exact path="/" component={Home} />
     <Route path="/about" component={About} />
     </Switch>
     </Suspense>
     </Router>
    );
    

    高阶组件

    高阶组件是参数为组件,返回值为新组件的函数;组件是将 props 转换为 UI,而高阶组件是将组件转换为另一个组件;高阶组件有两种:

    • 新组件包裹旧组件:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    
    function withEnhancerOne(WrappedComponent) {
     class EnhancerComponent extends Component {
     // ...
     render() {
     if (boolean) {
     return null;
     }
     // 将 HOC 接受到的 props 传入被包裹的组件,还传入了一些新的 props
     return (
     <>
     <div style={{ fontSize: 16 }}>
     <WrappedComponent {...this.props} {...otherProps} />
     </div>
     </>
     );
     }
     }
     EnhancerComponent.displayName = `EC-(${
     WrappedComponent.displayName || WrappedComponent.name || 'Component'
     })`;
     return EnhancerComponent;
    }
    
    • 新组件继承旧组件:
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    function withEnhancerTwo(WrappedComponent) {
     return class EnhancerComponent extends WrappedComponent {
     // 可以通过 super 访问到 WrappedComponent 中的各种变量和方法
     render() {
     if (boolean) {
     return null;
     }
     return (
     <>
     <div>EnhancerComponent</div>
     {super.render()}
     </>
     );
     }
     };
    }
    

    组合模式

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    
    const TabItem = ({ active, children, onClick }) => {
     const style = {
     border: active ? '1px solid red' : 0,
     color: active ? 'red' : 'green'
     };
     return (
     <h3 style={style} onClick={onClick}>
     {children}
     </h3>
     );
    };
    class Tabs extends Component {
     state = {
     activeIndex: 0
     };
     render() {
     const childrenReplacement = React.children
     .toArray(this.props.children)
     .map((child, index) => {
     if (React.isValidElement(child)) {
     /**
     * 要想扩展传给子节点的 props,只能通过 React.cloneElement
     * 直接修改 props 对象是不行滴
     */
     return React.cloneElement(child, {
     active: this.state.activeIndex === index,
     onClick: () => this.setState({ activeIndex: index })
     });
     }
     })
     .filter(({ props }) => !props.disabled);
     // 不渲染原有的 children,改为渲染 childrenReplacement
     return <>{childrenReplacement}</>;
     }
    }
    <Tabs>
     <TabItem>One</TabItem>
     <TabItem disabled>Two</TabItem>
     <TabItem>Three</TabItem>
    </Tabs>;
    


沪ICP备19023445号-2号
友情链接