首页» 前端 » react » 三、React高级部分:调试工具、PropTypes与DefaultProps、虚拟DOM、Diff算法、ref的使用、生命周期函数、Charles、动画

三、React高级部分:调试工具、PropTypes与DefaultProps、虚拟DOM、Diff算法、ref的使用、生命周期函数、Charles、动画

日期:2019年05月29日 阅读次数:3089 分类:react

一、React developer tools安装及使用

在chrome浏览器中安装React developer tools插件

安装React developer tools插件

安装React developer tools插件

打开Chrome网上应用店需要翻墙。
如果不翻墙,可以自己下载谷歌react的crx版的插件,点击更多工具,扩展程序,然后将crx文件拖入界面中就可以了。(待验证)
安装React developer tools插件

安装成功后,浏览器右上角会出现React图标。F12打开工发者工具,右侧也会出现一个React选择。打开后会有组件结构、Props、State等信息。

浏览器右上角如果为红色,则说明现在打开的是React开发的项目,而且在开发环境运行。
浏览器右上角如果为黑色,则说明现在打开的是React开发的线上项目。
浏览器右上角如果为灰色,则说明现在打开的不是React开发的项目。

安装React developer tools插件

二、PropTypes 与 DefaultProps 的应用

PropTypes设置prop的类型,DefaultProps设置默认值。

  1. 先在组件中引用PropTypes
import PropTypes from 'prop-types';
  1. 使用propTypes对属性进行强校验,验证格式是否正确(number,string,func等),isRequired是必须要传的意思,不能不传值。
TodoItem.propTypes = {
  test: PropTypes.string.isRequired,
  // 类型是数字或者是字符串
  content: PropTypes.oneOfType([PropTypes.number, PropTypes,string]),
  deleteItem: PropTypes.func,
  index: PropTypes.number
}
  1. defaultProps设置属性的默认值
TodoItem.defaultProps = {
  test: 'hello world'
}

官方文档说明

三、props,state 与 render 函数的关系

  1. 当组件的state或props发生改变时,render就会重新执行

  2. 当父组件的render函数被运行时,它的子组件的render都会被重新执行

四、React 中的虚拟DOM

接下来我们来尝试分析应该如何渲染数据模板
todoList为例:当input框内容改变时,页面重新渲染。

方法一

  1. state 数据
  2. JSX 模版
  3. 数据 + 模版 结合 生成真实的DOM,来显示页面。
  4. state 发生改变
  5. 数据 + 模版 结合 再次生成真实的DOM,替换原始的DOM。

缺陷:
1. 第一次生成了一个完整的DOM片段
2. 第二次又生成一个完整的DOM片段
3. 第二次的替换第一次的,非常耗性能。

总之:操作DOM都比较耗性能。

方法二

  1. state 数据
  2. JSX 模版
  3. 数据 + 模版 结合 生成真实的DOM,来显示页面内容
  4. state 数据发生变化。
  5. 数据 + 模版 结合,生成真实的DOM,并不直接替换原始DOM。
  6. 新的DOM(documentFragement 文档碎片)和原始的DOM做比对,找差异
  7. 找出input框发生了变化
  8. 只用新的DOM 中的input (差异)来替换老DOM 中的input元素。

缺陷: 性能提升的并不是很明显。

方法三

  1. state 数据
  2. JSX 模版
  3. 数据 + 模版 结合 生成真实的DOM,来显示页面内容。例如代码结构如下:
<div><span> hello world </span></div>
  1. 生成虚拟DOM (虚拟DOM其实就是一个js对象,用它来描述真实的DOM)
['div', {id: 'abc'}, [ 'span', {}, 'hello world' ] ]
  1. state 数据发生变化
  2. 数据 + 模版 生成新的虚拟DOM(极大的提升来性能)
['div', {id: 'abc'}, [ 'span', {}, 'bye bye' ] ]
  1. 比较原始的虚拟DOM 和新的虚拟DOM,找到区别是span中的内容发生来改变(极大的提高来性能)
  2. 直接操作DOM,改变span中的内容
    1. 因减少了DOM操作,所以极大提升了性能。
    2. 因虚拟DOM本质是JS对象,所以作对比的时候性能消耗很少,也提升了性能。

虚拟DOM说明:虚拟DOM是一个JS数组,有三项,格式如下

[DOM元素节点名称, 节点属性(属性对象), 子项(又可以是一个虚拟DOM 或 内容)]

五、深入了解虚拟DOM

上节我们分析了虚拟DOM的渲染,找出了最优的解决方案–方法三。但实际情况中应该是如下顺序。(3、4步骤是反过来的,应该先生成虚拟DOM,再生成真实的DOM)

  1. state 数据
  2. JSX 模版

  3. 数据 + 模版 结合 生成虚拟DOM (虚拟DOM其实就是一个js对象,用它来描述真实的DOM)

['div', {id: 'abc'}, [ 'span', {}, 'hello world' ] ]
  1. 用虚拟DOM的结构 来生成真实的DOM,来显示页面内容。例如代码结构如下:
<div><span> hello world </span></div>
  1. state 数据发生变化
  2. 数据 + 模版 生成新的虚拟DOM(极大的提升来性能)
['div', {id: 'abc'}, [ 'span', {}, 'bye bye' ] ]
  1. 比较原始的虚拟DOM 和新的虚拟DOM,找到区别是span中的内容发生来改变(极大的提高来性能)
  2. 直接操作DOM,改变span中的内容

JSX的解析过程

JSX -> React.createElement -> 虚拟 DOM(JS 对象) -> 真实的 DOM

JSX的写法

return <div><span>item</span></div>

React.createElement的写法

return React.createElement('div', {}, React.createElemtent('span', {}, 'item'))

虚拟DOMO的优点

  1. 性能提升了
    > 1. 因减少了DOM操作,所以极大提升了性能。
    > 2. 因虚拟DOM本质是JS对象,所以作对比的时候性能消耗很少,也提升了性能。
  2. 它使得跨端应用得以实现。React Native
    > 因为虚拟DOM是个JS对象,所以在浏览器上可以用这个对象渲染成真实的DOM,同样也可以用这个对象在原生程序中渲染成相应的组件。

六、虚拟DOM中的Diff算法

React 中虚拟DOM(js对象) 的 diff 算法

  1. diff是difference(差异)的简写,意思就是对比原始虚拟DOM 和新的虚拟DOM直接的差异。

  2. 数据发生改变的时候才会产生diff 算法。(state 或者 props 发生改变的时候,也就是调用 setState的时候)

    • (1) 其实props的改变,也是因为父级state的改变。也是因为调用了setState方法。
    • (2) setState方法是异步的。因为如果短时间内调用多次setState就可以全并成一个setState,只做一次diff比对,也只渲染一次DOM,从而提高性能。

    diff算法对比

  3. 虚拟DOM是同级比对的,如果比对有差异,下面子级的节点就不再作比对,就会把这层以下的所有的DOM全部替换为新的虚拟DOM。
    diff算法对比

  4. 虚拟DOM在循环的时候,要给定一个Key值。而且要是唯一的,不要使用index(索引)作为Key值。

    • (1) 如果没有Key值,虚拟DOM比对的时候只能做循环比对,比较耗性能。如果有Key值则可以根据Key快速找出对应项。
      没有KEY和有KEY的区别
    • (2) 最好不要使用index(索引)作为Key值,因为index不稳定。原因如下:
// 如果用index 去对应key值。
// 原始状态key值对应如下:
a: 0, b: 1, c: 2

// 如果进行删除操作之后,如删掉a。那么这个时候数据就变成了b c 
// 现在key值对应如下:
b: 0 c: 1,

// 这样子每项对应的Key值就完全变掉了,那这个key值就是失去来存在的意议。

七、React中ref的使用

ref是refrence(引用)的缩写。Refs 提供了一种方式,允许我们访问 DOM 节点。

  1. ref可以帮助我们获取DOM元素。如下:想获取input的DOM节点直接使用this.input。
<input ref={(inputRef) => {this.input = inputRef}} />
  1. setState是异步函数,所有和DOM操作一起使用时会出现一些bug。如:DOM获取的内容不是最新内容。
  2. setState和DOM操作一起使用,应该把DOM操作放在第二个参数(回调函数)中。
setState( () => ({}),  () => {} )
// 第一个参数是设置新的state,第二个参数是setState完成后的回调函数。
// 如果想获取state更新后的DOM节点,就必须写在回调函数里。

完整例子如下:

render() {
  return (
    <Fragment>
      <div>
        <label htmlFor="insertArea">输入内容</label>
        <input
          id="insertArea"
          className="input"
          value={this.state.inputValue}
          onChange={this.handleInputChange}
          ref={(inputRef) => {this.input = inputRef}}
        />
        // 使用以上ref,则可以直接使用this.input存储input的DOM节点的引用
        <button onClick={this.handleBtnClick}>提交</button>
      </div>
      <ul ref={(ulRef) => this.ul = ulRef}>
        {this.getTodoItem()}
      </ul>
    </Fragment>
  )
}

getTodoItem() {
  return this.state.list.map((item, index) => {
    return (
      <TodoItem
        key={item}
        content={tiem}
        index={index}
        deleteItem={this.handleItemDelete}
      />
    )
  })
}

handleInputChange(e) {
  // const value = e.target.value
  // 使用了ref后,this.input等价于e.target

  // 但是不建议使用ref这种方法,尽量使用以数据驱动的方式编写代码,不要操作DOM元素。而且这样子做有时还会出现一些bug。
  const value = this.input
  this.setState(() => ({
    inputValue: value
  }))
}

handleBtnClick() {
  /*this.setState((prevState) => {
    list: [...prevState.list, prevState.inputValue],
    inputValue: ''
  })
  // 这样子打印出的长度其实要比实际的少一个。
  // 看似setState已经执行,但是setState是个异步函数,会在console.log()之后执行。
  console.log(this.ul.querySelectorAll('div').length)*/

  // 正确的方式是在setState的第二个参数(回调函数)中打印长度。
  this.setState((prevState) => {
    list: [...prevState.list, prevState.inputValue],
    inputValue: ''
  }, () => {
    console.log(this.ul.querySelectorAll('div').length)
  })
}

handleItemDelete(index) {
  this.setState((prevState) => {
    const list = [...PrevState.list] // 重新赋值,而不是引用
    list.splice(index, 1)
    return {list}
  })
}

八、React 的生命周期函数

生命周期函数是指组件在某一个时刻自动执行的函数,每个组件中都有生命周期函数。

React的生命周期函数

生命周期流程

初始化数据
constructor:这只是一个阶段中执行的函数,不能算是生命周期函数。使用初始化state和props。

挂载阶段
* componentWillMount:组件被挂载到页面前执行(第一次渲染时会被执行)
* render:渲染页面,即:挂载 (第一次渲染时会被执行)
* componentDidMount:组件被挂载到页面后执行(第一次渲染时会被执行)

更新(数据发生变化时)阶段
* componentWillReceiveProps:从父组件传来的Props变化时执行。

组件要从父组件接受参数
只要父组件的render函数重新执行了,子组件的这个生命周期函数就会执行。可细化为以下两句话。
如果这个组件第一次存在于父组件中,不会执行。
如果这个组件之前已经存在于父组件中,才会执行。

  • shouldComponentUpdate:组件更新(state或props变化时)前执行。并返回一个布尔值,表示是否更新。
  • componentWillUpdate:组件更新(state或props变化时)前执行
    > 该函数在shouldComponentUpdate之后执行
    > 如果shouldComponentUpdate返回true才会执行,如果返回false则不执行,以下两个函数也是如此。
  • render:重新渲染页面
  • componentDidUpdate:组件更新(state或props变化时)完成之后执行

注销
* componentWillUnmount:即将注销组件前执行

九、React 生命周期函数的使用场景

  1. 所有的生命周期函数都可以不存在,但是render函数必须存在。因为React中的组件继承了Component组件的属性,这个组件中内置了其它生命周期函数,唯独没有内置render函数的默认实现。
  2. 父组件数据发生变化时,父组件的render函数会重新渲染,同时子组件也的render也会重新渲染。但是很多时候的这种子组件中render的渲染是不必要的,可以在子组件生命周期函数中加以判断,只有特定条件下子组件才更新。
shouldComponentUpdate(nextProps, nextState){
  if(nextProps.content !== this.props.content) {
    return true
  }else{
    return false
  }
}

性能优化:
1. 使用生命周期函数shouldComponentUpdate(nextProps,nextState)阻止不必要的子组件渲染,提高性能。
2. 作用域的修改:把尽量使用的操作放在constructor里面。比如:this.handleClick.bind(this),只会执行一次,避免子组件无谓渲染。
3. React 的底层setState,内置性能提升机制,异步函数,把多次数据改变结合一一次来做,降低虚拟DOM比对频率。
4、React底层用的是虚拟DOM的概念,同层比对,还有key 值这样的概念,提升虚拟DOM比对速度。

1、ajax请求只需求请求一次,所以适用放在只执行一次的函数中。如:componentWillMount或constructor中
2、如果放在render函数中,会形成死循环。
3、但在更高级的使用时(如:使用React Native时),放在componentWillMount中会出问题。
4、约定ajax请求最好是放在componentDidMount生命周期函数中,放这里肯定没有问题。

十、使用Charles实现本地数据mock

axios的安装和使用:
安装

npm install axios --save-dev

使用(放在componentDidMount生命周期函数中)

import axios from 'axios'
componentDidMount() {
  axios.get('./api/todolist')
    .then((res) => {
      this.setState(() => ({
        list: [...res.data]
      }))
    })
    .catch(() => {console.log("erro")})
}

Charles简介

Charles是一个HTTP代理/ HTTP监视器/反向代理,使开发人员能够查看其机器和Internet之间的所有HTTP和SSL / HTTPS流量。这包括请求,响应和HTTP标头(包含cookie和缓存信息)。

Charles 是在 Mac 下常用的网络封包截取工具,在做移动开发时,我们为了调试与服务器端的网络通讯协议,常常需要截取网络封包来分析。

Charles 通过将自己设置成系统的网络访问代理服务器,使得所有的网络访问请求都通过它来完成,从而实现了网络封包的截取和分析。

除了在做移动开发中调试端口外,Charles 也可以用于分析第三方应用的通讯协议。配合 Charles 的 SSL 功能,Charles 还可以分析 Https 协议。

Charles 主要的功能包括:

  1. 截取 Http 和 Https 网络封包。
  2. 支持重发网络请求,方便后端调试。
  3. 支持修改网络请求参数。
  4. 支持网络请求的截获并动态修改。
  5. 支持模拟慢速网络。

Charles官网
Charles 抓包使用教程

使用Charles实现本地数据mock

打开映射规则面板
使用Charles实现本地数据mock

填写映射配置信息(当请求配置的地址时,把本地选择的文件内容当作返回数据返回)
使用Charles实现本地数据mock

需要勾选这两者
使用Charles实现本地数据mock

十一、使用 react-transition-group 实现动画

安装react-transition-group模块

npm install react-transition-group --save

react-transition-group包含三个
* Transition
* CSSTransition
* TransitionGroup

使用CSSTransition实现单个组件的动画

import React, { Component, Fragment } from 'react'
import { CSSTransition } from 'react-transition-group';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      show: true
    }
    this.handleToggle = this.handleToggle.bind(this);
  }

  render() {
    return (
      <Fragment>
        <CSSTransition
          in={this.state.show} // 如果this.state.show从false变为true,则动画入场,反之out出场
          timeout={1000} // 动画执行1秒
          classNames="fade" // 自定义的class名
          unmountOnExit // 可选,当动画出场后在页面上移除包裹的dom节点
          //可选,动画入场之后的回调,el指被包裹的dom,让div内的字体颜色等于蓝色
          onEntered={(el) => {
            el.style.color='blue'
          }}
          //同理,动画出场之后的回调,也可以在这里来个setState啥的操作
          onExited={() => {
            // ……   
          }}
          appear={true} // 第一次入场就执行动画
        >
          <div>hello</div>
        </CSSTransition>
        <button onClick = {this.handleToggle}>toggle</button>
      </Fragment>
    )
  }

  handleToggle() {
    this.setState({
      show: this.state.show ? false : true;
    })
  }
}

对应的样式

/* enter是入场前的刹那(点击按钮),appear指页面第一次加载前的一刹那(自动) */
.fade-enter, .fade-appear{
  opacity: 0;
}
/* enter-active指入场后到入场结束的过程,appear-active则是页面第一次加载自动执行 */
.fade-enter-active, .fade-appear-active {
  opacity: 1;
  transition: opacity 200ms;
}
/* 入场动画执行完毕后,保持状态 */
.fade-enter-done {
  opacity: 1;
}

/* 同理,出场前的一刹那,以下就不详细解释了,一样的道理 */
.fade-exit {
  opacity: 1;
}
.fade-exit-active {
  opacity: 0;
  transition: opacity 200ms;
}
.fade-exit-done {
  opacity: 0;
}

使用TransitionGroup实现一组组件的动画

import React, { Component, Fragment } from 'react'
import { CSSTransition, TransitionGroup } from 'react-transition-group';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      list: []
    }
    this.handleAddItem = this.handleAddItem.bind(this);
  }

  render() {
    return (
      <Fragment>
        <TransitionGroup>
        {
          this.state.list.map((item, index) => {
            return (
              <CSSTransition
                timeout={1000}
                classNames="fade"
                unmountOnExit // 元素隐藏后DOM被移除
                onEntered={(el) => {el.style.color='blue'}} // 入场动画结束后执行
                appear={true} // 第一次入场就执行动画
                key={index}
              >
                <div>{item}</div>
              </CSSTransition>
            )
          })
        }
        </TransitionGroup>
        <button onClick = {this.handleAddItem}>Add</button>
      </Fragment>
    )
  }

  handleAddItem() {
    this.setState((prevState) => {
      return {
        list: [...prevState.list, 'iiemss']
      }
    })
  }
}

对应的样式

.fade-enter, .fade-appear{
  opacity: 0;
}
.fade-enter-active, .fade-appear-active {
  opacity: 1;
  transition: opacity 200ms;
}
.fade-enter-done {
  opacity: 1;
}

.fade-exit {
  opacity: 1;
}
.fade-exit-active {
  opacity: 0;
  transition: opacity 200ms;
}
.fade-exit-done {
  opacity: 0;
}

react-transition-group文档说明

文章标签: