React 进阶(2)


2019-6-5 React

React 的高阶基础知识(2)

1、react 的优化

1.1 通过 build 压缩

在 webpack 打包时的优化

1.2 通过 chrome 的 promence 分析工具

在要分析的网页后加上字符串 ?react_perf,可以使用 promence 分析工具,可以具体定位到耗时长的方法和组件上。

1.3react 自己的 scu 优化

也就是通过 shouldCOmponentUpdata() 这个方法通过自建的 tree 避免重复渲染

1558655604271

  1. 第一种自己调节性能 shouldComponentUpdata

  2. 使用 PureComponent , 是 component 的一个子类其实 是在 shouldComponentUpdata 添加了一个判断。是否所有的 props 和 state 都有变化 如果没有变化则不要重新渲染.

    这个是属性比较是浅比较,只是比较内存地址。

  3. 重新设置值的时候推荐使用更换数组或者对象的方式,比如对象拷贝,和数组解构重新赋值

  4. 在使用浅比较的时候对于数组内部和对象内部可能感知不到,如果改成深比较可能造成浪费内存因为每次制造都是一个新状态对象,==Immutable==解决深浅拷贝问题,官方推荐使用这种方式。

    Immutable 可以通过共享部分对象来创建新的对象,对象被创建就不能被修改数据,对任何 Immutable 的任何操作包括添加==更改和删除==都会产生一个==新的 Immutable 对象==. 他的原理是实现了 ==persistent (持久化数据解构)==也就是使用旧数据创建新的数据的时候保证==旧的数据不可变==。

    为了避免深拷贝把所有节点都复制造成的性能损耗,Immutable 使用了 structural sharing(节点共享),即为如果只有修改这个节点和受他影响的父节点,其他节点进行共享,也就是节点共享

    import {map,fromJs} from 'Immutable'
    let m1 =Map({a:1,b:2,c:3,d:{aa:1}})
    clog(m1[a])//1 //原数据解构
    let m2 = m1.set('a','11')
    clog(m2.a)//2 新数据解构
    clog(m1.a)//1 原数据解构
    clog(m2===m1)//false 新内存地址
    clog(m1.d===m2.d)//true 说明d的内存地址相同 这个节点共享了
    //如果更改D 下面的aa则
    let m3 = m1.set(['d','aa'],'22222')//欲修改的子节点在数组后面  是按从前向后排列的
    //fromJS 把原生的JS对象转换成一个Immutable 数据解构对象
    let m4 = fromJS({a:1,b:2,c:3,d:{aa:1}})
    let m5 = m4.setIn([d,aa],'333')//添加 多级
    clog(m4.getIn(['d','aa']))//获取多级的方法
    let m6 = m4.updateIn(['d','aa'],value=>value+1)//更新
    let m6 = m4.updateIn(['d'],value=>value(hhhh)=22233)//更新
    let m7 =m4.mergeDeep({d:{bb:222}})//深度合并
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  5. 上面的库很大而且需要重新特定的方法,所有后面出现了 seamless-Immutable 这个库可以无缝的使用 js 大小只有 2K 这个库只支持 array 和 object

     let immutable = require('seamless-immutable')
    let m1 = immutable({a:1,b:2,{info:{name:'zhangsan'}}})
    let m2 = m1.merge({a:2,b:3,c:6)
    clog(m1.info.name)//使用js输出对象的方式使用即可
    clog(m2.b)
    
    1
    2
    3
    4
    5

1.4 dom diff 原理

React 遍历树形节点的时间复杂度是 o(n)

diff策略

  1. Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计
  2. 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构
  3. 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分

也就是说

  1. 如果组件的根节点发生了改变那么销毁原组件进行重新渲染。这个是从节点层面来说

    例如 div 变成了 ul 那么会执行 Unmount 进行组件销毁然后渲染新的组件 remount

  2. 如果 DOM 元素相同的时候,会检查属性保留相同的底层 DOM 节点,只更新改变的属性(是否是属性改变),例如

    <div className="hahah" id="111">
        <div className="hehehe" id="111">
           //只会更新className
    
    1
    2
    3

    可以如果节点改变就会触发dirty component 而删除整个节点和子节点

  3. 组件的实例不会改变会保留原来 state 和 props 寻找更新节点进行 Update 触发 Update 循环,接下来 Diff 算法对上一次的结果和新结果进行递归。

  4. 当节点处于同一层级时,diff 提供了 3 种节点操作,分别为 INSERT_MARKUP (插入)、MOVE_EXISTING (移动)和 REMOVE_NODE (删除)。react 会通过 Key 来进行比较原生节点是否进行变化. 来决定是使用哪种操作。

1.5 React16 以后 的Fiber架构

改变了之前react的组件渲染机制,新的架构使原来同步渲染的组件现在可以异步化,可中途中断渲染,执行更高优先级的任务,释放浏览器主线程。

react16源码(Fiber架构) 作者 : Colorful_coco

2、高阶组件

HOC(Higher Order Component,高阶组件)

摘自 深入理解 React 高阶组件 原文地址:https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#.g1lnzrw5d 翻译地址:https://zhuanlan.zhihu.com/p/24776678

实现HOC 的两种方式 :Props Proxy (PP)(代理Props) and Inheritance Inversion (II)(继承转化)。

使用 Props Proxy 可以做什么?

  • 操作 props
  • 通过 Refs 访问到组件实例
  • 提取 state
  • 用其他元素包裹 WrappedComponent

你可以用 Inheritance Inversion 做什么?

  • 渲染劫持(Render Highjacking)
  • 操作 state
  • Inheritance Inversion 的高阶组件不一定会解析完整子树

2.1 对于下面的抽取的逻辑

class App extends React.Component {
  render() {
    return (
      <div>
        <form>
          <Username />
          <Number />
        </form>
      </div>
    )
  }
}
class Username extends React.Component {
  render() {
    return (
      <label>
        用户名:
        <input name="username" ref={input => (this.username = input)} type="text" onChange={this.handleChange} />
        <br />
      </label>
    )
  }
  componentDidMount() {
    this.username.value = localStorage.getItem('username') || ''//1
  }
  handleChange = e => {
    localStorage.setItem('username', e.target.value)//2
  }
}
class Number extends React.Component {
  render() {
    return (
      <label>
        手机:
        <input name="number" ref={input => (this.number = input)} type="text" onChange={this.handleChange} />
        <br />
      </label>
    )
  }
  componentDidMount() {
    this.number.value = localStorage.getItem('number') || ''//1
  }
  handleChange = e => {
    localStorage.setItem('number', e.target.value)//2
  }
}

export default App
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

对于代码 1 2 位置处的逻辑可以抽取出来复用的 可以抽取成一个高阶组件

传递进去一个旧的组件,返回一个新的组件

//高阶组件时一个函数
//其实相当于把旧的组件放回回去 只不过多了几个封装过的参数
import React,{Component} from 'React'
export default function(oldcomponent,name,placeholder){
    //这个函数就是高阶组件  其中component 这个参数时必须的代表的时被封装的旧的组件.
    //后面的参数都时用来进行标记的参数
    class NewComponent extends Component{
     constructor(){
        super()
        this.state={
            data:''
        }
    }
      componentDidMount() {
        this.setState({data:localStorage.getItem(name) || placeholder})
      }
      save = e => {
        localStorage.setItem(name, e.target.value)//2
      }
        render(){
            return <oldcomponent data={this.state.data} save={this.save}>
                //保存方法 和属性
        }
    }
    return NewComponent
}


import local from './local'

class Username extends Component{
    render(){
        return (
        <label>
            <input defaultValue={this.props.data} onChange={this.props.save}/>
            </label>
        )
    }
}
export default local(<Username/> , 'number','请输入用户名!')
//这样来使用  把组件和参数传递进去

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

2.2 其他逻辑抽取方式

现在希望加载数据的时候先从 local 中抽取一个 key,然后从接口中抽取这个 KEY 的值,进行显示,也就是说现在显示的过程分成两部,第一步从 local 中获取第一部分 key 然后从后端通过 key 来获取响应的数据.

//多测组件的封装
//localstorage 封装
import React, {
  Component
} from 'react'

export default function (Oldcomponent, name, placeholder) {
  class NewComponent extends Component {
    constructor(props) {
      super(props)
      this.state = {
        data: ''
      }
    }
    componentWillMount() {
      this.setState({
        data: localStorage.getItem(name) || placeholder
      })

    }
    save = e => {
      localStorage.setItem(name, e.target.value)
    }
    render() {
      return <Oldcomponent
      data = {
        this.state.data
      }
      save = {
        this.save
      }
      />
    }
  }
  return NewComponent
}
// ajax组件封装
import React, { Component } from 'react'

export default function(Oldcomponent) {
  class NewComponent extends Component {
    constructor(props) {
      super(props)
      this.state = {
        data: ''
      }
    }
    componentWillMount() {
      const a = {
        xx: 'aaaaaaa',
        yy1: '23123123'
      }
      console.log('this.props', this.props)
      console.log('this.props.data :', this.props.data)
      this.setState({
        data: a[this.props.data]
      })
    }
    render() {
      return <Oldcomponent data={this.state.data} save={this.props.save} />
    }
  }
  return NewComponent
}
//app组件的封装
    import local from './components/localStorage.js'
import ajax from './components/ajax.js'

class App extends Component {
  render() {
    return (
      <div>
        <label>
          name: <input type="text" defaultValue={this.props.data} onChange={this.props.save} />
        </label>
      </div>
    )
  }
}
let Username = ajax(App)
Username = local(Username, 'username', 'username')
export default Username
//封装的时候从内往外封装
//使用从外向内执行   最外层  localstorage  然后找到数据后  ajax  然后 时 app组件
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

2.3装饰器模式

当高阶组件使用较多的时候上面的方法传参的方式不例与写读 ,因此使用装饰器模式来写高阶组件

  1. 安装react-rewired 包 安装包之后可以在不eject的情况下替换webpack.config.js文件
//创建文件 config-overrides.js文件
// 通过下面的方式覆盖配置
module.exports=(config)=>{
    //在这里配置
    return config
}
1
2
3
4
5
6
  1. 如果要更方便的配置 需要使用customize-cra 这个包 具体使用:

    const{override,addDecoratorslegacy}=require('customize-cra')
    module.export=override(
    addDecoratorslegacy()
    )
    
    1
    2
    3
    4
  2. 配合使用

# 安装
yarn add customize-cra react-app-rewired @babel/plugin-proposal-decorators
1
2
//config-overrides.js 中
const { override, addDecoratorsLegacy } = require('customize-cra');
module.exports = override(
 addDecoratorsLegacy()
);
//在 package.json 中
"scripts": {
     "start": "react-app-rewired start",
     "build": "react-app-rewired build",
     "test": "react-app-rewired test",
     "eject": "react-app-rewired eject"
}
1
2
3
4
5
6
7
8
9
10
11
12

这样来使用,而其他的都已经配置好开包即用

import {connnect} from./gaojiezujianbao’ //高阶组件包
@connnect
class App extends COmponent(
    render(){
        return (
            <h1>装饰器模式的高阶组件的演示
                </h1>
        }
)
export default App
// 上面就相当于  export default connnetc(App)
1
2
3
4
5
6
7
8
9
10
11

3、片段 fragments

可以把子元素列表放在一个分组中,并且不会再 DOM 中添加额外的节点

//react中只能把返回的节点放在一个顶级的节点中间,也就是说组件必须有一个包裹节点
//那么如果提取的时类似  一个数组的li的时候就会出现问题
//如果给LI 包裹一个DIV 那么DOM 解构就会乱掉,如果不包裹就会报错出现问题  类似于下面的解构
class MessageListItem extends Component {
  render() {
    return (
        {this.props.messages.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
    )
  }
}
class Message extends Component {
  constructor() {
    super()
    this.state = {
      message: [1, 2, 3]
    }
  }
  render() {
    return (
      <div>
        <ul>
          <MessageListItem messages={this.state.message} />
        </ul>
      </div>
    )
  }
}
export default Message
//这个时候就可以使用react.Fragment  代码片段为LIST增加一个虚拟的包裹元素
class MessageListItem extends Component {
  render() {
    return (
        <React.Fragment>
        {this.props.messages.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
        </React.Fragment>
    )
  }
}
//或者写做
class MessageListItem extends Component {
  render() {
    return (
        <>//
        {this.props.messages.map((item, index) => (
          <li key={index}>{item}</li>
        ))}
        </>
    )
  }
}
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
50
51
52
53
54

4 插槽

ReactDOM.createPortal 提供了一种把子节点渲染到父组件 DOM 结构之外的 DOM 节点

ReactDOM.creatPortal(child, container)
//child 时任何可以渲染的react子元素
//container时一个DOM 元素
class Message extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      show: false
    }
  }
  render() {
    return (
      <div>
        <button onClick={this.show}>显示</button>
        {this.state.show ? (
          <Button>
            <div
              style={{
                width: '200px',
                height: '200px',
                backgroundColor: 'red',
                position: 'absolute',
                left: '50%',
                top: '50%'
              }}
            >
              错误提示
            </div>
          </Button>
        ) : null}
      </div>
    )
  }
  show = () => {
    this.setState({ show: !this.state.show })
  }
}
class Button extends Component {
  constructor() {
    super()
    this.container = document.querySelector('#Model-root')
    //root 和 #Model-root 是平级节点
  }
  render() {
    return ReactDOM.createPortal(this.props.children, this.container)
  }
}

export default Message
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

5 错误边界(Error boundary)

一个小组件的错误不应该影响所有的组件,影响页面的显示.

//定义一个察觉错误的组件
class ErrorBoundary extends Component {
  constructor() {
    super()
    this.state = {
      hasError: false
    }
  }
  componentDidCatch(hasError) {
    //错误察觉  这个钩子函数是捕获错误 也就是相当于try  catch
    this.setState({ hasError })
  }

  render() {
    if (this.state.hasError) {
      //错误显示
      return <div class="error">'出错了'</div>
    }
    return this.props.children //渲染子组件  this.props.children  拿到子节点
  }
}

function Todo() {
  //渲染的子组件
  return <div>{null.toString()}</div>
}
class App extends Component {
  render() {
    return (
      <div>
        //使用ErrorBoundary察觉错误如果没出错渲染子组件 出错了渲染自己
        <ErrorBoundary>
          <Todo />
        </ErrorBoundary>
      </div>
    )
  }
}

export default App
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

再正常情况下组件树的错误提示会有冒泡的概念,错误如果没被 try catch 捕获到 会冒泡到顶层.

6、组件的 createClass 创建方法

16版本以后已经舍弃

7、组件的混入 mixin

由于createClass 在16版本之后已经取消掉,mixin 混入本身带来大量的依赖,所以react官方不推荐使用,实质上mixin已经被舍弃,而mixin 本身实质上是扩展收集功能,也就是把多个功能方法的合成。