React 高级使用

函数组件

函数组件和 class 组件的区别

函数组件:

  • 纯函数,输入 props,输出 JSX
  • 没有实例,没有生命周期,没有 state
  • 不能扩展其他方法
// class 组件
Class List extends React.Component {
	constructor(props) {
		super(props);
	}

	render() {
		const { list } = this.props;

		return <ul>
			{ 
				list.map((item, index) => {
					return <li key={item.id}>
						<span>{item.title}</span>
					</li>;
				})
			}
		</ul>;
	}
}
// 函数组件
function List(props) {
	const { list } = this.props;

	return <ul>
		{ 
			list.map((item, index) => {
				return <li key={item.id}>
					<span>{item.title}</span>
				</li>;
			})
		}
	</ul>;
}

受控和非受控组件

受控组件

  • 表单的值受 state 控制。
  • value 指向 stateonChange 事件监听,使用 setState 修改值。

非受控组件

  • ref
  • defaultValuedefaultChecked
  • 手动操作 dom 元素
import React from 'react';

class App extends React.Component {
    constructor(props) {
        super(props);
        
        this.state = {
            name: 'zhangsan',
            flag: true,
        };
        
        this.nameInputRef = React.createRef(); // 创建 ref
        this.fileInputRef = React.createRef();
    }
    render() {
        // // input defaultValue
        // return <div>
        //     {/* 使用 defaultValue 而不是 value ,使用 ref */}
        //     <input defaultValue={this.state.name} ref={this.nameInputRef}/>
        //     {/* state 并不会随着改变 */}
        //     <span>state.name: {this.state.name}</span>
        //     <br/>
        //     <button onClick={this.alertName}>alert name</button>
        // </div>;

        // // checkbox defaultChecked
        // return <div>
        //     <input
        //         type="checkbox"
        //         defaultChecked={this.state.flag}
        //     />
        // </div>;

        // file
        return <div>
            <input type="file" ref={this.fileInputRef}/>
            <button onClick={this.alertFile}>alert file</button>
        </div>;

    }
    
    alertName = () => {
        const elem = this.nameInputRef.current; // 通过 ref 获取 DOM 节点
        alert(elem.value); // 不是 this.state.name
    }
    
    alertFile = () => {
        const elem = this.fileInputRef.current; // 通过 ref 获取 DOM 节点
        alert(elem.files[0].name);
    }
}

export default App;

非受控组件使用场景

必须手动操作 DOM 元素,setState 实现不了

  • 比如:文件上传 <input type="file" />
  • 某些富文本编辑器,需要传入 DOM 元素

Portals

让组件渲染到父组件之外。

背景

组件默认会按既定层次嵌套渲染,如何让组件渲染到父组件之外?

基本使用

import React from 'react';
import ReactDOM from 'react-dom';
import './style.css';

class App extends React.Component {
    constructor(props) {
        super(props);
        
        this.state = {
        };
    }
    
    render() {
        // // 正常渲染
        // return <div className="modal">
        //     {this.props.children} {/* vue slot */}
        // </div>;

        // 使用 Portals 渲染到 body 上。
        // fixed 元素要放在 body 上,有更好的浏览器兼容性。
        return ReactDOM.createPortal(
            <div className="modal">{this.props.children}</div>,
            document.body // DOM 节点
        );
    }
}

export default App;

使用场景

  • overflow: hidden
  • 父组件 z-index 值太小
  • fixed 需要放在 body 第一层

context

背景

  • 公共信息(语言、主题)如何传递给每个组件?
  • props 太繁琐
  • redux 小题大做

vueprovide / inject 功能类似。

基本使用

import React from 'react';

// 创建 Context 填入默认值(任何一个 js 变量)
const ThemeContext = React.createContext('light');

// 底层组件 - 函数是组件
function ThemeLink (props) {
    // const theme = this.context; // 会报错。函数式组件没有实例,即没有 this

    // 函数式组件可以使用 Consumer
    return <ThemeContext.Consumer>
        { value => <p>link's theme is {value}</p> }
    </ThemeContext.Consumer>;
}

// 底层组件 - class 组件
class ThemedButton extends React.Component {
    // 指定 contextType 读取当前的 theme context。
    // static contextType = ThemeContext; // 也可以用 ThemedButton.contextType = ThemeContext;
    
    render() {
    	// React 会往上找到最近的 theme Provider,然后使用它的值。
        const theme = this.context; 
        
        return <div>
            <p>button's theme is {theme}</p>
        </div>;
    }
}

// 指定 contextType 读取当前的 theme context。
ThemedButton.contextType = ThemeContext; 

// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
    return (
        <div>
            <ThemedButton />
            <ThemeLink />
        </div>
    );
}

class App extends React.Component {
    constructor(props) {
        super(props);
        
        this.state = {
            theme: 'light'
        };
    }
    
    render() {
        return <ThemeContext.Provider value={this.state.theme}>
            <Toolbar />
            <hr/>
            <button onClick={this.changeTheme}>change theme</button>
        </ThemeContext.Provider>;
    }
    
    changeTheme = () => {
        this.setState({
            theme: this.state.theme === 'light' ? 'dark' : 'light'
        });
    }
}

export default App;

异步组件(懒加载)

  • import()
  • React.lazy
  • React.Suspense

通过 React.lazy 引入异步组件,由于异步加载的时候可能会出现等待,所以可以使用 React.Suspense 来定义一个 loading

import React from 'react';

const ContextDemo = React.lazy(() => import('./ContextDemo'));

class App extends React.Component {
    constructor(props) {
        super(props);
    }
    
    render() {
        return <div>
            <p>引入一个动态组件</p>
            <hr />
            <React.Suspense fallback={<div>Loading...</div>}>
                <ContextDemo/>
            </React.Suspense>
        </div>;

        // 1. 强制刷新,可看到 loading 
        // 2. 看 network 的 js 加载
    }
}

export default App;

性能优化

shouldComponentUpdate(简称 SCU)

React 中父组件更新,子组件无条件更新。

基本用法

shouldComponentUpdate(nextProps, nextState) {
	if (nextState.count !== this.state.count) {
		return true; // 可以渲染
	}

	return false; // 不重复渲染
}

默认返回 true

  • React 中父组件更新,则子组件无条件更新。
import React from 'react';

class Input extends React.Component {
    constructor(props) {
        super(props);
        
        this.state = {
            title: ''
        };
    }
    
    render() {
        return <div>
            <input value={this.state.title} onChange={this.onTitleChange}/>
            <button onClick={this.onSubmit}>提交</button>
        </div>;
    }
    
    onTitleChange = (e) => {
        this.setState({
            title: e.target.value
        });
    }
    
    onSubmit = () => {
        const { submitTitle } = this.props;
        
        submitTitle(this.state.title); // 'abc'

        this.setState({
            title: ''
        });
    }
}

class List extends React.Component {
    constructor(props) {
        super(props);
    }
    
    render() {
        const { list } = this.props

        return <ul>{list.map((item, index) => {
            return <li key={item.id}>
                <span>{item.title}</span>
            </li>;
        })}</ul>;
    }
}

class Footer extends React.Component {
    constructor(props) {
        super(props);
    }
    
    render() {
        return <p>
            {this.props.text}
            {this.props.length}
        </p>;
    }
    
    componentDidUpdate() {
        console.log('footer did update');
    }
    
    shouldComponentUpdate(nextProps, nextState) {
        if (nextProps.text !== this.props.text
            || nextProps.length !== this.props.length) {
            return true; // 可以渲染
        }
        
        return false; // 不重复渲染
    }

    // React 默认:父组件有更新,子组件则无条件也更新!!!
    // 性能优化对于 React 更加重要!
    // SCU 一定要每次都用吗?—— 需要的时候才优化
}

class TodoListDemo extends React.Component {
    constructor(props) {
        super(props);
        
        // 状态(数据)提升
        this.state = {
            list: [
                {
                    id: 'id-1',
                    title: '标题1'
                },
                {
                    id: 'id-2',
                    title: '标题2'
                },
                {
                    id: 'id-3',
                    title: '标题3'
                }
            ],
            footerInfo: '底部文字'
        };
    }
    
    render() {
        return <div>
            <Input submitTitle={this.onSubmitTitle}/>
            <List list={this.state.list}/>
            <Footer text={this.state.footerInfo} length={this.state.list.length}/>
        </div>;
    }
    
    onSubmitTitle = (title) => {
        this.setState({
            list: this.state.list.concat({
                id: `id-${Date.now()}`,
                title
            })
        });
    }
}

export default TodoListDemo;

SCU 一定要配合不可变值

import React from 'react';
import _ from 'lodash';

class Input extends React.Component {
    constructor(props) {
        super(props);
        
        this.state = {
            title: ''
        };
    }
    
    render() {
        return <div>
            <input value={this.state.title} onChange={this.onTitleChange}/>
            <button onClick={this.onSubmit}>提交</button>
        </div>;
    }
    
    onTitleChange = (e) => {
        this.setState({
            title: e.target.value
        });
    }
    
    onSubmit = () => {
        const { submitTitle } = this.props;
        
        submitTitle(this.state.title);

        this.setState({
            title: ''
        });
    }
}

class List extends React.Component {
    constructor(props) {
        super(props);
    }
    
    render() {
        const { list } = this.props;

        return <ul>{list.map((item, index) => {
            return <li key={item.id}>
                <span>{item.title}</span>
            </li>
        })}</ul>;
    }

    // 增加 shouldComponentUpdate
    shouldComponentUpdate(nextProps, nextState) {
        // _.isEqual 做对象或者数组的深度比较(一次性递归到底)
        if (_.isEqual(nextProps.list, this.props.list)) {
            // 相等,则不重复渲染
            return false;
        }
        
        return true; // 不相等,则渲染
    }
}

class TodoListDemo extends React.Component {
    constructor(props) {
        super(props);
        
        this.state = {
            list: [
                {
                    id: 'id-1',
                    title: '标题1'
                },
                {
                    id: 'id-2',
                    title: '标题2'
                },
                {
                    id: 'id-3',
                    title: '标题3'
                }
            ]
        };
    }
    
    render() {
        return <div>
            <Input submitTitle={this.onSubmitTitle}/>
            <List list={this.state.list}/>
        </div>;
    }
    
    onSubmitTitle = (title) => {
        // 正确的用法
        this.setState({
            list: this.state.list.concat({
                id: `id-${Date.now()}`,
                title
            })
        });

        // // 为了演示 SCU ,故意写的错误用法
        // this.state.list.push({
        //     id: `id-${Date.now()}`,
        //     title
        // });
        // this.setState({
        //     list: this.state.list
        // });
    }
}

export default TodoListDemo;

纯组件(PureComponent 和 React.memo)

  • PureComponentSCU 实现了浅比较;用于 class 组件
  • React.memo:函数组件中的 PureComponent
  • 浅比较已适用大部分情况,尽量不要做深度比较(比较耗费性能)
function MyComponent(props) {
	// 使用 props 渲染
}

function areEqual(prevProps, nextProps) {
	// 如果把 nextProps 传入 render 方法的返回结果与 将 prevProps 传入 render 方法的返回结果 一致则返回 true,否则返回 false
}

export default React.memo(MyComponent, areEqual);

不可变值 immutable.js

  • 彻底拥抱“不可变值”
  • 基于共享数据(不是深拷贝),速度好

缺点:

  • 有一定的迁移和学习成本。按需使用
const map1 = Immutable.Map({ a: 1, b: 2, c: 3 });
const map2 = map1.set('b', 50);

map1.get('b'); // 2
map2.get('b'); // 50

关于组件公共逻辑的抽离

  • 高阶组件 HOC
  • Render Props

高阶组件 HOC

高阶组件基本用法

  • 高阶组件的本质是一个工厂函数(相当于是给组件包裹了一层父组件,然后将属性通过 props 透传给子组件)
  • 主要解决业务逻辑复用的问题
// 高阶组件不是一种功能,而是一种模式
const HOCFactory = Component => {
	class HOC extends Rect.component {
		// 在此定义多个组件的公共逻辑
		render() {
			// 返回拼装的结果
			return <Component {...this.props} />;
		}
	}

	return HOC;
}

const EnhancedComponent1 = HOCFactory(WrappedComponent1);
const WnhancedComponent2 = HOCFactory(WrappedComponent2);

举例:鼠标滑过获取位置抽离为高阶组件

import React from 'react';

// 高阶组件
const withMouse = (Component) => {
    class withMouseComponent extends React.Component {
        constructor(props) {
            super(props);
            
            this.state = { 
            	x: 0, 
            	y: 0 
            };
        }
  
        handleMouseMove = (event) => {
            this.setState({
                x: event.clientX,
                y: event.clientY
            });
        }
  
        render() {
            return (
                <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
                    {/* 1. 透传所有 props 2. 增加 mouse 属性 */}
                    <Component {...this.props} mouse={this.state}/>
                </div>
            );
        }
    }
    
    return withMouseComponent;
}

const App = (props) => {
    const a = props.a;
    const { x, y } = props.mouse; // 接收 mouse 属性
    
    return (
        <div style={{ height: '500px' }}>
            <h1>The mouse position is ({x}, {y})</h1>
            <p>{a}</p>
        </div>
    )
}

export default withMouse(App); // 返回高阶函数

redux 的 connect 是高阶组件

import { connenct } from 'react-redux';

// connect 是高阶组件
const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList);

export default VisibleTodoList;
export const connect = (mapStateToProps, mapDispatchToProps) => {
	return WarappedComponent => {
		class Connect extends Component {
			constructor() {
				super();

				this.state = {
					allprops: {}
				};
			}

			// 省略...

			render() {
				return <WrappedComponent {...this.state.allProps} />
			}
		}
		
		return Connect;
	};
}

Vue 如何实现高阶组件

在工厂函数中,实例化一个 vue 组件,在这个新实例里面实现公用逻辑,然后在 render 函数中传入 Component,并且带上 props 值。

const HOCFactory = Component => {
    const instance = new Vue({
        // 实现公共组件逻辑...
        created () {},
        mounted () {},
        data () {},
        
        // 实现公共组件逻辑...
        
        render (h) => h(Component, {
            props: {
                // 传入props
            }
        }) 
    });
    
    return instance;
}

const EnhancedComponent1 = HOCFactory(Component1);
const EnhancedComponent2 = HOCFactory(Component2);

Render Props

通过一个函数将 class 组件的 state 作为 props 传递给纯函数组件。

基本使用

// Render Props 的核心思想
// 通过一个函数将 class 组件的 state 作为 props 传递给纯函数组件
class Factory extends React.component {
	constructor() {
		this.state = {
			// 多个组件的公共逻辑的数据
		};
	}

	// 修改 state
	render() {
			// 通过 render 函数将 state 传递给纯函数组件
			return <div>{this.props.render(this.state)}</div>;
	}
}

const App = () => {
	return <Factory render={
			// render 是一个函数组件
			(props) => <p>{props.a} {props.b} ...</p/>
		} />;
};

举例:用 Render Props 实现鼠标滑过获取位置的逻辑复用

import React from 'react';
import PropTypes from 'prop-types';

class Mouse extends React.Component {
    constructor(props) {
        super(props);
        
        this.state = { 
        	x: 0, 
        	y: 0 
        };
    }
  
    handleMouseMove = (event) => {
      this.setState({
        x: event.clientX,
        y: event.clientY
      });
    }
  
    render() {
      return (
        <div style={{ height: '500px' }} onMouseMove={this.handleMouseMove}>
            {/* 将当前 state 作为 props ,传递给 render (render 是一个函数组件) */}
            {this.props.render(this.state)}
        </div>
      );
    }
}

Mouse.propTypes = {
    render: PropTypes.func.isRequired // 必须接收一个 render 属性,而且是函数
};

const App = (props) => {
    return (<div style={{ height: '500px' }}>
        <p>{props.a}</p>
        <Mouse render={
            /* render 是一个函数组件 */
            ({ x, y }) => <h1>The mouse position is ({x}, {y})</h1>
        }/>
        
    </div>);
};

/**
 * 即,定义了 Mouse 组件,只有获取 x y 的能力。
 * 至于 Mouse 组件如何渲染,App 说了算,通过 render prop 的方式告诉 Mouse 。
 */

export default App;

高阶组件 和 Render Props 的区别

  • HOC:模式简单,但会增加组件层级
  • Render Props:代码简洁,学习成本较高

FQA

  1. 函数组件和 class 组件的区别。
    函数组件:
    - 纯函数,输入 props,输出 JSX
    - 没有实例,没有生命周期,没有 state
    - 不能扩展其他方法
  2. 受控组件和非受控组件的区别。
    受控组件:
    - valuestate 控制,通过 onChnage 监听事件,setState 修改值
    非受控组件:
    - value 不受 state 控制,通过 defaultValue 指定默认值,通过 ref 获取 dom 节点
  3. Portals 是什么?
    • 将组件渲染到父组件之外
  4. React 为什么要加 shouldComponentUpdate 这个可选的生命周期,为什么不直接在 React 中作对比?
    • 因为大部分情况下,没这个必要。如果项目不是太复杂,用不用 SCU 效果都一样,那就别用。所以给开发者一个可定制的权利,如果项目很复杂,可以选择使用。
  5. react 如何实现组件公共逻辑的抽离?
    • 高阶组件
      • HOC 就是一个函数中定义 一个组件, 组件做了公用的逻辑, 并且接受一个组件作为参数,返回这个参数组件,并且把公用组件的 state 传给参数组件
    • Render Props
      • render props 就是给公用组件添加一个属性, 属性值是个函数,函数在公用组件中被调用,且传入了需要的参数
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值