React 高级内容

一. React developer tools安装及使用

这是React项目的调试工具,方便我们在开发过程中对React项目进行调试。

安装步骤:

1. 去github下载react-devtools压缩包,也可以通过git bash下载:

 git clone https://github.com/facebook/react-devtools.git

2. 下载成功后,进入react-devtools文件夹,通过git bash安装依赖,安装时间会比较长:

npm --registry https://registry.npm.taobao.org install

3. 安装成功后,我们便可以打包一份扩展程序出来:

npm run build:extension:chrome

4. 进入react-devtools\shells\chrome\build目录下,将里面的unpacked文件夹拖进chrome扩展程序里面即可。

注意:此方法不太好使,建议使用别的方法。


二. PropTypes与DefaultProps的应用

1. PropTypes,子组件可以对父组件传过来的属性或者方法做一个类型校验,不符合时会警告。

使用方法:

在TodoItem组件中引入PropTypes

在class的后面使用PropTypes来对父组件传过来的值做类型校验:

当传过来的值类型不符合时,控制台会警告。

知识点:

当我们需要某个属性必须传的时候,我们可以这么写:

TodoItem.propTypes = {
  test: PropTypes.string.isRequired,  //此处要求test时必须传,且是字符串类型
  content: PropTypes.string,
  deleteItem: PropTypes.func,
  index: PropTypes.number
}

 

2. DefaultProps, 默认值,当我们子组件要求父组件传某个属性,但是父组件却没有传,我们可以自己定义一个默认值。

使用方法:

还有一些关于props的功能,可以自行查阅react官网。

 


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

1. React中数据与页面之间互相联动的底层运行机理。

当组件的state或者props发生改变的时候,render函数就会重新执行。

  • 拿我们前面做的TodoList为例。当我们往input框输入内容的时候,onchange事件就会被执行,而onchange事件对应的方法就会改变state,一旦state发生改变,那么render函数就会重新地被执行,那么页面就会重新渲染,渲染的数据也是最新的。我们可以通过在render函数中console.log()内容进行验证,可以发现每次输入都会打印。
  • props改变的场景:当我们父组件向子组件传值的时候,子组件通过props接收。父组件向子组件传递的值发生了改变,那么props也就发生了改变,相应地,子组件中的render函数就会重新执行。还有一种情况子组件的render函数也会被重新执行,当父组件的render函数重新执行的时候,由于子组件也是在父组件的render函数中被调用的,所以也会导致子组件被重新渲染,也就是子组件的render函数被重新执行。

 


四. React中的虚拟DOM

假如我们需要自己实现一个React数据重新渲染的功能,我们需要做如下准备:

1、初始方案

之所以上面的准备耗性能,是因为我们上面每次都是把整个dom重新生成,而不是只生成数据改变了的那部分dom,这消耗了性能,另一方面替换掉完整的dom,这也会消耗性能。

2、第一次改进

上面的改进方案并不直接替换全部的原始dom,这节约了一部分性能。但是新的dom和原始dom进行比对又消耗了一部分性能,所以性能的提升并不明显。

 

3、React提出了虚拟DOM的方案

上面便是React的虚拟DOM,在这里需要知道一个点:js创建或者操作对象比创建或者操作真实的dom节省了很大的性能,这就是React提出虚拟DOM的原因。

 


五. 深入了解虚拟DOM

1. 在实际过程中, React的虚拟dom和上面有一些区别,上面是为了方便用户理解。实际上是这样的:

 

2. 我们项目中render函数里面的标签内容不是真实的dom,它只是模板,当render函数执行的时候模板与数据相结合,生成虚拟的dom。

过程: JSX模板-------> createElement-----> 虚拟dom(JS对象)------->真实的dom

原理:

render() {
  return (
    <div>item</div>
  )
}

上面的jsx语法执行以后实际上是调用了下面的方法

render() {
  return React.createElement('div', {}, 'item');
}

这两次代码在页面中的效果是一样的,只不过第二次的代码调用了更底层的实现方法,没有使用jsx语法。实际开发中之所以不采用createElement方法,是因为jsx语法更简洁,更方便我们编写。Vue框架中的虚拟dom机制和React是一致的。

 

3. 虚拟DOM的优点。

  • 性能提升了
  • 它使得跨端应用得以实现。React Native

React Native之所以能做app,依靠的就是虚拟DOM。当我们运行在浏览器端的时候,虚拟DOM就会生成真实的DOM,当我们运行在原生应用平台的时候,虚拟DOM就会生成相应的原生应用组件。

 


六. 虚拟DOM中的Diff算法

两个虚拟DOM比对的方式我们就称为Diff算法。

1. 为什么React中要把setstate设置成为异步

比如说我们连续调用了三次setstate,React就会把三次合并成一次setstate,只去做一次虚拟DOM的比对,然后去更新一次dom,这样就可以省去额外的两次DOM比对带来的性能耗费。这么设置本质上是为了提高React底层的性能。

2. 为什么不建议用index来做key值

React中虚拟DOM比对的Diff算法是同级比对的,当数据发生改变了以后,又会生成一个新的虚拟DOM,两者进行比对,找到差异以后就会生成新真实的DOM。首先比对最顶层的虚拟DOM,假设他俩一致就会去比对第二层。如果他俩不一致,于是他就会把原始页面上的虚拟DOM,对应的下面的所有DOM,全部都删除掉。重新生成一遍节点下的所有的DOM,用重新生成的这些DOM,替换掉原始页面上的DOM。也就是它如果比对的这层虚拟DOM不一致,下面几层就不去比了,整个的就对原始的DOM进行替换了。这么比对有些可重复利用的dom还是会被销毁掉,消耗了一些性能,但是好处是算法简单,这样带来的好处就是比对的速度会非常的快,大大减少了比对算法的性能上的消耗。

我们平时给数组列表设置key值,使得两个虚拟DOM之间有名字,方便两两进行比对。而我们不推荐使用index值来作为key值,因为比如说第二次的虚拟DOM是我们删除了数组的某一项,此时一部分元素的index值就发生了改变,这样我们算法根据key值进行比对的就发生了错误。这就是为什么我们一般不推荐使用index来作为key值得原因。

 


七. React中ref的使用

1、希望content的类型可以是字符串或者是数组

 

2、 ref的使用

在React中除了可以使用e.target来获取对应的dom元素以外,还可以使用ref来获取dom元素。

1. 之前的TodoList中handleInputChange方法里面的e.target可以用ref替换。方法:

首先给render函数里面的input添加ref(注意:inputDom是一个参数,可以自己随便定义)

接着将handleInputChange方法里面的e.target替换即可。实际效果一样:

 

但是React是数据驱动的框架,我们不推荐使用ref直接操作dom,除非遇到很复杂的业务必须操作dom。

 

2、使用ref遇到的一个问题:别忘了setState是异步的。

我们希望在TodoList中实现一个功能,点击提交按钮,打印出ul里面div的长度。

首先:

接着:

我们通过控制台发现结果不对:每次打印都会少一个。

原因是React的setState方法是异步的,每次打印出来以后setState方法才可能会执行。解决方法:

使用setState方法的第二个参数,该参数里面的函数会等setState异步执行完成之后才会被执行。

结果:

 


八、React的生命周期钩子

React的生命周期主要经历了四个阶段:

Initialization: 初始化阶段

Mounting:组件挂载到页面阶段

Updating:组件内容更新阶段

Unmounting:把组件从页面去除阶段

1、初始化阶段:

        该阶段的生命周期函数在组件刚被创建的那个时刻就会调用。由于constructor这个函数并非React所独有,ES6中也存在,所以React并没有将它归为生命周期函数,但是它就是在该阶段调用的。在该阶段会定义state,会接收props。

2、挂载阶段:

        该阶段有三个生命周期函数被调用

  • componentWillMount   在组件即将第一次被挂载到页面的时刻自动执行
  • render  组件挂载到页面的时刻自动执行
  • componentDidMount   组件第一次被挂载到页面之后,自动地执行

我们分别在不同阶段打印相应的生命周期函数,结果如下:

我们在输入框输入文字,发现只有render函数被调用,这是因为另外两个只在第一次被挂载的时候会调用,之后就不会被调用了:

 

3、组件内容更新阶段

        有两种情况会进入该阶段,一种是props发生改变,另一种是state发生改变。从上面的图可以发现两者的区别是props比state开始多调用一个生命周期函数(因为归根结底props改变的原因是因为父组件的state改变)。

两者都有的:

  • shouldComponentUpdate    组件被更新之前,他会自动被执行。有三种情况

1. 

  shouldComponentUpdate() {
    console.log('shouldComponentUpdate');
  }

然后在输入框中输入,但是输入框却不现实文字,结果:

这是因为这个生命周期函数最后需要return一个布尔值。

 

2. return ture

  shouldComponentUpdate() {
    console.log('shouldComponentUpdate');
    return true;
  }

为了方便我们记忆,上面代码可以这么理解:应该让组件更新吗?return true的话就让组件更新,相应的输入框也能显示文字,组件更新也就能够调用rener函数了。结果页面:

 

3. return false

  shouldComponentUpdate() {
    console.log('shouldComponentUpdate');
    return false;
  }

return false组件不会更新,所以输入框不会显示我们输入的文字,但是控制台不会报错,因为我们return 了一个布尔值。结果页面:

 

  • componentWillUpdate    
  //组件被更新之前,它会自动执行。但是它在shouldComponentUpdate之后执行
  //如果shouldComponentUpdate返回true它才会执行
  //如果返回false,这个函数就不会被执行了
  componentWillUpdate() {
    console.log('componentWillUpdate');
  }

下面是return true的情况:

 

  • render
  • componentDidUpdate    组件更新完毕之后,它会被执行
  componentDidUpdate() {
    console.log('componentDidUpdate')
  }

 结果:

 

props独有的:

  • componentWillReceiveProps     前提是一个组件要从父组件接收参数。如果这个组件第一次存在于父组件中不会被执行,如果这个组件已经存在于父组件中了,才会被执行。

该生命周期函数是props独有的,它执行的前提是该组件必须有props参数,也就是父组件给该组件传值。所以我们把这个生命周期函数写在TodoItem这个子组件中。

效果演示:

TodoItem:

  componentWillReceiveProps() {
    console.log('child componentWillReceiveProps');
  }

结果:我们输入完毕第一次点击提交以后,不会打印出来,再次输入以后,此时父组件的render函数重新执行后,才会执行该生命周期函数,即打印出来。

 

4、把组件从页面去除阶段

  • componentWillUnmount    当这个组件即将被从页面剔除的时候执行

TodoItem:

  componentWillUnmount() {
    console.log('child componentWillUnmount');
  }

结果:当我们点击列表项时,该列表项被删除,此时该生命周期函数被执行

 


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

1. 注意: render这个生命周期函数必须存在。

这是因为React的组件是继承自Component这个组件的,该组件默认内置了所有的生命周期函数,唯独没有内置render这个生命周期函数的实现。

 

2. 利用生命周期函数提高性能

案例:

TodoItem中的render函数中添加console.log('child render');

结果:

实际中上面每次输入导致TodoItem这个子组件都会重新被渲染是耗性能的,我们可以利用shouldComponentUpdate这个生命周期函数来实现。

TodoItem:

  shouldComponentUpdate(nextProps, nextState) {
    //nextProps表示组件如果更新后的Props
    //nextState表示组件如果更新后的state
    if(nextProps.content !== this.props.content) {
      return true;
    } else {
      return false;
    }
  }

效果:

可以发现,此时当我们只输入没有提交的时候,TodoItem这个组件不会重新被渲染,提高了性能。

 

3. 在哪个生命周期函数中发送ajax请求?

不要放在render函数中,因为它在页面中经常会被反复地执行。推荐放在componentDidMount这个生命周期函数中。

 

 


10、使用 mockjs 实现本地数据 mock

1、课程讲的 charles 折腾了一下午也用不了,应该是版本更新后的问题。查找资料后使用 mockjs 成功实现了 

  • 安装 mockjs : npm install mockjs ;
  • 在 src 目录下建立 mock 文件夹,里面建立 mockdata.js 文件:
import Mock from 'mockjs';

Mock.mock('/todolist', {
  data: ['a', 'b', 'c']
})   //第一个参数是自己写的,到时候 axios 也写这个路径即可, 第二个参数放数据
  • TodoList.js 文件引入 mockdata.js , 然后添加代码:
  componentDidMount() {
    axios.get('/todolist')
      .then((res) => {
        const data = res.data.data;
        this.setState(() => ({
          list: [...data]
        }))
      })
      .catch(() => {console.log('error')});
  }

此时页面就渲染出模拟的数据了。


11、React中实现CSS过渡动画

删除之前的 TodoList 后的目录结构:

index.js:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(<App />, document.getElementById('root'));

App.js:

import React, { Component, Fragment } from 'react';
import './style.css';

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {
      show: true
    };
    this.handleToggle = this.handleToggle.bind(this);
  }
  render() {
    return (
      <Fragment>
        <div className={this.state.show ? 'show' : 'hide'}>hello world</div>
        <button onClick={this.handleToggle}>toggle</button>
      </Fragment>
    )
  }
  handleToggle() {
    this.setState({
      show: this.state.show ? false : true
    })
  }
}

export default App;

style.css:

.show {
  opacity: 1;
  transition: all 1s ease-in;   /*所有的属性都用 1s 的时间来切换*/
}

.hide {
  opacity: 0;
  transition: all 1s ease-in;   /*所有的属性都用 1s 的时间来切换*/
}

启动项目就实现了一个切换显示与否的渐变效果。


12、React中使用CSS动画效果

之前代码不变,就改变 style.css 的效果:

.show {
  animation: show-item 2s ease-in forwards;
}

.hide {
  animation: hide-item 2s ease-in forwards;   /*forwards是让动画结束的时候保存最后一帧的样式,不加它的话就不会隐藏*/
}

@keyframes hide-item {
  0% {
    opacity: 1;
    color: red;
  }
  50% {
    opacity: .5;
    color: green;
  }
  100% {
    opacity: 0;
    color: blue;
  }
}
@keyframes show-item {
  0% {
    opacity: 0;
    color: red;
  }
  50% {
    opacity: .5;
    color: green;
  }
  100% {
    opacity: 1;
    color: blue;
  }
}

此时运行后就有了动画的效果。


13、使用react-transition-group实现动画(一)

简单的动画我们可以自己写,但是比较复杂的我们可以借助这个第三方模块 : react-transition-group

  • 首先进行安装 : 
    npm install react-transition-group --save
  • App.js :
import React, { Component, Fragment } from 'react';
import { CSSTransition } from 'react-transition-group';
import './style.css';

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}  //根据show的值来判断是出场动画还是入场动画
          timeout={300}  //动画的时间
          classNames='fade'  //给类添加前缀名
        >
          <div>hello world</div>
        </CSSTransition>
        <button onClick={this.handleToggle}>toggle</button>
      </Fragment>
    )
  }
  handleToggle() {
    this.setState({
      show: this.state.show ? false : true
    })
  }
}

export default App;
  • style.css :
/*执行入场动画的时候,也就是show从false变成true的时候。在入场动画执行的第一个时刻CSSTransition这个组件会往div上挂载一个样式,名字就是fade-enter*/
.fade-enter {
  opacity: 0;
}

/*入场动画执行的第二个时刻到入场动画完成之前的时刻,这段时间div上面一直会有fade-enter-active这个class*/
.fade-enter-active {
  opacity: 1;
  transition: opacity 1s ease-in;
}
/*当整个入场动画执行完以后,这个class就会被增加到div上面*/
.fade-enter-done {
  opacity: 1;
}

/*出场的第一个时刻,该class会被加上去*/
.fade-exit {
  opacity: 1;
}
/*执行出场动画的这段时间都会存在这个class*/
.fade-exit-active {
  opacity: 0;
  transition: opacity 1s ease-in;
}
/*整个出场动画完成之后*/
.fade-exit-done {
  opacity: 0;
}

此时我们就实现了和之前一样的动画效果。

虽然看起来我们实现一样的效果,使用这个模块来做更复杂,但是这个第三方的库有着很多特别实用的地方:

  • 给 CSSTransition 这个组件添加 unmountOnExit ,可以发现动画结束后 div 元素就不存在,动画开始它又出来了。
  • 这个模块还有很多钩子函数(类似生命周期函数),我们可以使用这些钩子函数实现特定的动画。
  render() {
    return (
      <Fragment>
        <CSSTransition
          in={this.state.show}  //根据show的值来判断是出场动画还是入场动画
          timeout={300}  //动画的时间
          classNames='fade'  //给类添加前缀名
          unmountOnExit
          onEntered={(el) => {el.style.color='red'}}  //el 指的就是内部的 div 元素,意思是当入场动画执行结束之后,给元素添加红色的字体颜色
        >
          <div>hello world</div>
        </CSSTransition>
        <button onClick={this.handleToggle}>toggle</button>
      </Fragment>
    )

添加配置:

appear={true}   //第一次展示的时候就有动画效果

一般这个组件都能满足我们 react 开发中需要的动画效果,到时候查文档即可。


14、使用react-transition-group实现动画(二)

1、实现多个元素的动画效  App.js:

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

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
                  key={index}
                  timeout={300}
                  classNames='fade'
                  unmountOnExit
                  onEntered={(el) => {el.style.color='red'}}
                  appear={true}
                >
                  <div>{item}</div>
                </CSSTransition>
              )
            })
          }
        </TransitionGroup>
        <button onClick={this.handleAddItem}>toggle</button>
      </Fragment>
    )
  }
  handleAddItem() {
    this.setState((prevState) => {
      return {
        list: [...prevState.list, 'item']
      }
    })
  }
}

export default App;

其实就是用 TransitionGroup 包起来, 内部每一项还是使用 CSSTransition 。

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章