React Advanced 备忘

965 阅读9分钟

1 声明周期

整个生命周期的钩子函数流程如下图:

react-lifecycle

在这里重点讲一下shouldComponentUpdate,无论是组件的属性发生变化还是状态发生改变都会执行render方法,在render执行之前要先执行shouldComponentUpdate判断一下是否需要更新,并且这个方法我们经常用来优化我们的组件。

通过shouldComponentUpdate进行优化

首先我们想到的是PureComponent 可以帮我们做这件事,shouldComponentUpdate的原理如下:

shouldComponentUpdate(nextProps, nextState){
    // 如果组件的状态或者属性的地址没有发生变化的时候,该函数返回`false`以阻止重新渲染
    for(let prop in nextProps){
        if (nextProps[prop] !== this.props[prop]) {
            return true;
        }
    }
    for (let state in nextState) {
        if (nextState[state] !== this.state[state]) {
            return true;
        }
    }
    return false
}

但是shouldComponentUpdate也存在一些问题,如下图:

有了 Immutable(在第二节有介绍),妈妈再也不用担心的我学习了。 在shouldComponentUpdate中我们可以这样优化:

shouldComponentUpdate(nextProps, nextState){
    return !(is(newProps, this.props) && is(nextState, this.state));
}

setState的时候我们可以这样做:

let list = List(this.state.list);
list = list.push(value);
this.setState({
    list
});

这样的话,就可以实现既不占用太多的内存,同时还是两个不同的对象。

使用 Chrome 性能分析工具 分析组件性能

  • 我们可以通过添加 ?react_perf 查询字段加载你的应用(例如:http://localhost:3000/?react_perf)。
  • 打开 Chrome DevTools Performance 并点击 Record 。
  • 执行你想要分析的操作,不要超过20秒,否则 Chrome 可能会挂起。
  • 停止记录。
  • 在 User Timing 标签下,React事件将会分组列出

2 Immutable

1 Immutable数据结构的特点

  • 不可变(Immutable): 一个集合一旦创建,在其他时间是不可更改的。
  • 持久的(Persistent): 新的集合可以基于之前的结合创建并产生突变,例如:set。原来的集合在新集合创建之后仍然是可用的。
  • 结构共享(Structural Sharing): 新的集合尽可能通过之前集合相同的结构创建,最小程度地减少复制操作来提高性能。

下面的动态图可以帮助我们加深理解:

Immutable

2 Immutable类库

a) immutable.js

  • 内部实现了一套完整的 Persistent Data Structure,还有很多易用的数据类型。像 Collection、List、Map、Set、Record等。

e.g.

let {fromJS, Map} = require('immutable');

let a = {
    count: '1'
};

let a1 = Map(a);
let a2 = a1.set('count', '11');

console.log(a);
console.log(a1);
console.log(a2);

/*
{ count: '1' }
Map { "count": "1" }
Map { "count": "11" }
*/

let obj = {name: 'lily', info: {age: 8}};
let obj1 = fromJS(obj);
let obj2 = obj1.setIn(['info', 'age'], 9);
console.log(obj);
console.log(obj1);
console.log(obj2);

/*
{ name: 'lily', info: { age: 8 } }
Map { "name": "lily", "info": Map { "age": 8 } }
Map { "name": "lily", "info": Map { "age": 9 } }
*/

b) seamless-immutable

使用Object.defineProperty扩展了 JavaScript的Array和Object对象来实现,只支持 Array 和 Object 两种数据类型.

let Immutable = require('seamless-immutable');

let c = Immutable({info: {age: 8}});
let c1 = c.merge({info: {age: 9}});
console.log(c.info.age);// 8
console.log(c1.info.age);// 9

3 Immutabled的优势

a) 降低复杂度

let immutable = require('seamless-immutable');
let obj = immutable({ age: 8 });
handle(obj);
console.log(obj.age); // 8

function handle(obj) {
    obj.age = 100;
}

b) 节约内存

let { fromJS } = require('immutable');

let a1 = fromJS({
    name: 'Lily',
    info: {
        age: 8
    }
});
let a2 = a1.set('name', 'Tom');
// 对没有发生变化的部分进行数据共享
console.log(a1.get('info') === a2.get('info'));// true 

c) 方便回溯

3 常用API

  • MapList
// Map  - 原生object --> Map对象  (只会转换第一层)
// List - 原生array  --> List对象 (只会转换第一层)

let m1 = Map({
    name: 'Lily', info: {
        age: 8,
    }
});

let a1 = List([1, 2, 3]);

console.log(m1); // Map { "name": "Lily", "info": [object Object] }
console.log(a1); // List [ 1, 2, 3 ]
  • fromJS
// fromJS - 原生js --> immutable对象 (深度转换,会将内部嵌套的对象和数组全部转成immutable)

let m2 = fromJS({
    name: 'Lily', info: {
        age: 8,
    }
});

let a2 = fromJS([1, 2, 3]);

console.log(m2); // Map { "name": "Lily", "info": Map { "age": 8 } }
console.log(a2); // List [ 1, 2, 3 ]
  • immutableData.toJS immutableData.size immutableData.count
// immutableData.toJS() - immutable对象 --> 原生js  (深度转换,会将内部嵌套的Map和List全部转换成原生js)
// immutableData.size   - 查看List或者map大小
// immutableData.count() - 查看List或者map大小


let obj = {
    name: 'Lily',
    info: {
        age: 8,
    }
};
let m1 = fromJS(obj);
let m2 = fromJS(obj);


console.log(m1.size);    // 2
console.log(m1.count()); // 2

r = m1.toJS();

console.log(r);         // { name: 'Lily', info: { age: 8 } }
console.log(r == obj);  // false
  • 增删改查 (所有操作都会返回新的值,不会修改原来值)
// get()     getIn()
// set()     setIn()
// update()  updateIn()
// delete()  deleteIn()

let obj = {
    name: 'Lily',
    info: {
        age: 8
    }
};

let data = fromJS(obj);

let data1 = data.get('name');
console.log(data1); // Lily

let data2 = data.getIn(['info', 'age']);
console.log(data2); // 8

let data3 = data.set('name', 'Tom');
console.log(data3); // Map { "name": "Tom", "info": Map { "age": 8 } }

let data4 = data.setIn(['info', 'age'], 9);
console.log(data4); // Map { "name": "Lily", "info": Map { "age": 9 } }

let data5 = data.update('name', (value) => value + '123');
console.log(data5); // Map { "name": "Lily123", "info": Map { "age": 8 } }

let data6 = data.updateIn(['info', 'age'], (value) => value + 1);
console.log(data6); // Map { "name": "Lily", "info": Map { "age": 9 } }

let data7 = data.delete('name');
console.log(data7); // Map { "info": Map { "age": 8 } }

let data8 = data.deleteIn(['info', 'age']);
console.log(data8); // Map { "name": "Lily", "info": Map {} }
  • is() 判断两个immutable对象是否相等
let obj = {
    name: 'Lily',
    info: {
        age: 8
    }
};

let data1 = fromJS(obj);
let data2 = fromJS(obj);
console.log(is(data1, data2));// true

let data3 = data1.set('name', 'Tom');
console.log(is(data3, data2));// false
  • immutableData.merge() 对象合并
let obj1 = {
    a: 1,
    b: 2
};
let obj2 = {
    b: 9,
    c: 3
};


let data1 = fromJS(obj1);
let data2 = fromJS(obj2);
let data3 = data1.merge(data2);
console.log(data3);// Map { "a": 1, "b": 9, "c": 3 }

let data4 = data2.merge(data1);
console.log(data4);// Map { "b": 2, "c": 3, "a": 1 }

3 Reconciliation 一致性比较

当比较不同的两个树,React 首先比较两个根元素。根据根跟的类型不同,它有不同的行为

  • 当根元素类型不同时,React 将会销毁原先的树并重写构建新的树
  • 当比较两个相同类型的 React DOM 元素时,React 检查它们的属性(attributes),保留相同的底层 DOM 节点,只更新发生改变的属性(attributes)
  • 当一个组件更新的时候,组件实例保持不变,以便在渲染中保持state。React会更新组件实例的属性来匹配新的元素,并在元素实例上调用 componentWillReceiveProps() 和 componentWillUpdate()

在这里我们举一个根元素类型不同的栗子, 如下:

import React, {Component} from 'react';

class Counter extends Component {
    constructor() {
        super();
        this.flag = new Date().getTime();
    }
    
    componentWillMount() {
        console.log(this.flag + ' - componentWillMount');
    }
    
    render() {
        return (<div>count</div>)
    }
    
    componentDidMount() {
        console.log(this.flag + ' - componentDidMount');
    }
    
    componentWillUnmount() {
        console.log(this.flag + ' - componentWillUnmount');
    }
}


export default class Reconciliation extends Component {
    constructor() {
        super();
        this.state = {
            show: true
        };
    }
    
    handleChange = () => {
        this.setState({
            show: !this.state.show
        });
    }
    
    render() {
        return (
            <div>
                <button onClick={this.handleChange}>change</button>
                {
                    this.state.show
                        ?   (<div>
                                <Counter/>
                            </div>)
                        :   (<span>
                                <Counter/>
                             </span>)
                }
            </div>
        )
    }
}

控制台打印的顺序如下:

即新的实例开始加载后,旧的实例才开始销毁。

4 使用 PropTypes 进行类型检查

React 内置了类型检测的功能。要在组件中进行类型检测,你可以赋值 propTypes 属性

class Count extends Component{
    // 设置默认属性
    static defaultProps = {
        name: 'Tom'
    }
    // 进行类型检测
    static propTypes = {
        name: PropTypes.string,
        age: PropTypes.number,
        node: function(props, propName, componentName) {
            if (props[propName].length == 0){
                return new Error('debugger')
            }
        }
    }
}
  • .array 数组
  • .bool 布尔值
  • .func 函数
  • .number 数字
  • .object 对象
  • .string 字符串
  • .symbol 符号
  • .node 任何东西都可以被渲染:numbers, strings, elements,或者是包含这些类型的数组(或者是片段)。
  • .element React元素
  • .instanceOf(Message) 类的一个实例
  • .oneOf(['News', 'Photos']) 枚举值
  • .oneOfType([PropTypes.string,PropTypes.number,PropTypes.instanceOf(Message)]) 多种类型其中之一
  • .arrayOf(PropTypes.number) 某种类型的数组
  • .objectOf(PropTypes.number) 某种类型的对象
  • .shape({color: PropTypes.string,fontSize: PropTypes.number}) 特定形式的对象
  • .func.isRequired 可以使用 `isRequired' 链接上述任何一个,以确保在没有提供 prop 的情况下显示警告
  • .any.isRequired 任何数据类型的值 function(props, propName, componentName) { return new Error()} 自定义的验证器
  • .arrayOf(function(propValue, key, componentName, location, propFullName) { }

5 Context

在某些场景下,你想在整个组件树中传递数据,但却不想手动地在每一层传递属性。你可以直接在 React 中使用强大的”context” API解决上述问题

  • 用法
// 祖先组件
static childContextTypes = {
    color: PropTypes.string,
    changeColor: PropTypes.func
}

getChildContext(){
    return {
        color: this.state.color,
        changeColor: (color) => {
            this.setState({color: color})
        }
    }
}


// 孙子组件
static contextTypes = {// 孙子组件需要哪些就写哪些,不需要把所有的属性都继承过来
    color: PropTypes.string,
    changeColor: PropTypes.func
}

this.context // 在组件中可以使用 this.context 就可以获取到上下文的内容了 

  • e.g.
import React, {Component} from 'react';
import PropTypes from 'prop-types'

class Main extends Component {
    render() {
        return (
            <div>
                <Content/>
            </div>
        )
    }
}
class Content extends Component {
    static contextTypes = {
        color: PropTypes.string,
        changeColor: PropTypes.func
    }
    render() {
        return (
            <div>
                <span style={{color: this.context.color}}>content</span>
                <button onClick={() => this.context.changeColor('green')}>变绿</button>
                <button onClick={() => this.context.changeColor('red')}>变红</button>
            </div>
        )
    }
}

class Head extends Component {
    render() {
        return (
            <div>
                <Title/>
            </div>
        )
    }
}
class Title extends Component {
    static contextTypes = {
        color: PropTypes.string
    }
    render() {
        return (
            <div>
                <span style={{color: this.context.color}}>title</span>
            </div>
        )
    }
}

export default class Page extends Component {
    
    static childContextTypes = {
        color: PropTypes.string,
        changeColor: PropTypes.func
    }
    constructor(){
        super();
        this.state = {
            color: 'red'
        };
    }
    getChildContext(){
        let self = this;
        return {
            color: this.state.color,
            changeColor: (color) => {
                this.setState({color: color})
            }
        }
    }
    
    render() {
        return (
            <div>
                <Head/>
                <Main/>
            </div>
        )
    }
}

6 片段(React.Fragment)

  • React 中一个常见模式是为一个组件返回多个元素。 片段(fragments) 可以让你将子元素列表添加到一个分组中,并且不会在DOM中增加额外节点。
import React, {Component} from 'react';

class Item extends Component {
    render() {
        return (
            <React.Fragment>
                {
                    this.props.list.map((item, index) => (
                        <li key={index}>{item}</li>
                    ))
                }
            </React.Fragment>
        )
    }
}

export default class Fragment extends Component {
    constructor() {
        super();
        this.state = {
            list: [1, 2, 3]
        };
    }
    render() {
        return (
            <div>
                <Item list={this.state.list}/>
            </div>
        )
    }
}

生成的DOM结构如下图,React.Fragment并不会生成一个DOM节点

7 插槽(Portals)

Portals 提供了一种很好的方法,将子节点渲染到父组件 DOM 层次结构之外的 DOM 节点。

ReactDOM.createPortal(child, container)
// child     - 任何一个可渲染的React元素,比如:一个元素、字符串或者fragment
// container - 一个DOM元素,这个元素就是child将要放置的地方
import React, {Component} from 'react';
import ReactDOM from 'react-dom'
import './portals.css'

class Modal extends Component {
    constructor() {
        super();
        this.modal = document.querySelector('#modal-root');
    }
    
    render() {
        return ReactDOM.createPortal(this.props.children, this.modal);
    }
}


export default class Portals extends Component {
    constructor() {
        super();
        this.state = {
            show: false
        };
    }
    
    handleClick = () => {
        this.setState({show: !this.state.show});
    };
    
    render() {
        return (
            <div>
                <button onClick={this.handleClick}>show</button>
                {
                    this.state.show && <Modal>
                        <div id="modal" className="modal">
                            <div id="modal-content" className="modal-content">
                                <button onClick={this.handleClick}>X</button>
                            </div>
                        </div>
                    </Modal>
                }
            </div>
        )
    }
    
}

8 Error BoundariescomponentDidCatch

Error Boundaries

部分 UI 中的 JavaScript 错误不应该破坏整个应用程序。 为了解决 React 用户的这个问题,React 16引入了一个 “错误边界(Error Boundaries)” 的新概念。 它依赖一个新的生命周期函数即componentDidCatch(error, info)

componentDidCatch

这个函数类似于JS中的catch{ }代码块,但是只是用针对于组件的,可以捕获组件内的错误对象。

note

注意:Error Boundary只能捕获他自己树下的错误,不能捕获发生在自己身上的错误。如果一个错误没有被Error Boundary捕获到,这个错误就会冒泡到它的上层离它最近的error boundary,这个跟原生JS中catch {}代码块的工作原理很相似。

9 高阶组件(Higher-Order Components)

高阶组件就是一个函数,用来封装重复的逻辑。传入一个老组件,返回一个新组件。

const NewComponent = higherOrderComponent(OldComponent)
  • 举一个例子
import React, {Component} from 'react';
import ReactDOM from 'react-dom';

function ajax(OldComponent) {
    class NewComponent extends Component {
        constructor() {
            super();
            this.state = {value: ''};
        }
        
        componentDidMount() {
            fetch('./data.json').then(res => res.json()).then((userData) => {
                this.setState({
                    value: userData[this.props.keyName]
                })
            })
        }
        
        render() {
            return <OldComponent value={this.state.value}/>
        }
    }
    
    return NewComponent;
}

function local(OldComponent, name, placeHolder) {
    class NewComponent extends Component {
        constructor() {
            super();
            this.state = {keyName: ''};
        }
        
        componentDidMount() {
            this.setState({
                keyName: localStorage.getItem(name) || placeHolder
            })
        }
        
        render() {
            return <OldComponent keyName={this.state.keyName}/>
        }
    }
    
    return NewComponent;
}

class UserName extends Component {
    render() {
        return (
            <div>
                用户名 <input type="text" value={this.props.value}/>
                <br/>
            </div>
        )
    }
}

UserName = ajax(UserName);
UserName = local(UserName, 'username', '请输入您的用户名');

class Mobile extends Component {
    render() {
        return (
            <div>
                手机号 <input type="text" value={this.props.value}/>
                <br/>
            </div>
        )
    }
}

Mobile = ajax(Mobile);
Mobile = local(Mobile, 'mobile', '请输入您的手机号码');

class Index extends Component {
    render() {
        return (
            <div>
                <UserName/>
                <Mobile/>
            </div>
        )
    }
}

ReactDOM.render(<Index/>, document.querySelector('#app'));

本例子中高阶组件的逻辑图如下:

其中,高阶组件将值传递给组件的逻辑如下:

10 参考文章