React学习(7)-React中的事件处理

前言

props与state都是用于组件存储数据的一js对象,前者是对外暴露数据接口,后者是对内组件的状态,它们决定了UI界面显示形态,而若想要用户与界面有些交互动作

也就是web浏览器通知应用程序发生了什么事情,例如:鼠标点击,移动,键盘按下等页面发生相应的反馈,它是用户与文档或者浏览器窗口中发生的一些特定的交互瞬间. 这个时候就需要用事件实现了

在原生JS操作DOM中,往往有内联方式(

  • 在HTML中直接事件绑定
<p onclick="alert('关注微信itclanCoder公众号')"></p>
  • 直接绑定
对象.事件类型 = 匿名函数,obj.onclick = function(){})
  • 事件委托监听方式
     对象.addEventListener('事件类型,不带on', 回调函数))

对DOM对象进行事件监听处理

而在React中事件处理和内联方式相似,但是却有些不同,如何确保函数可以访问组件的属性?

如何传递参数给事件处理器回调? 怎样阻止函数被调用太快或者太多次?有什么解决办法?

频繁操作DOM会造成浏览器的卡顿,响应不及时,引起浏览器的重绘重排,从而加重了浏览器的压力

频繁的调用后台接口,好好的接口被你玩坏,造成页面空白,崩溃,容易被后端同学提刀来见

既要提升用户体验,又要减少服务器端的开销

那么本篇就是你想要知道的

React中的事件

在React中事件的绑定是直接写在JSX元素上的,不需要通过addEventListener事件委托的方式进行监听
写法上:

  • 在JSX元素上添加事件,通过on*EventType这种内联方式添加,命名采用小驼峰式(camelCase)的形式,而不是纯小写(原生HTML中对DOM元素绑定事件,事件类型是小写的),无需调用addEventListener进行事件监听,也无需考虑兼容性,React已经封装好了一些的事件类型属性(ps:onClick,onMouseMove,onChange,onFocus)等
  • 使用JSX语法时,需要传入一个函数作为事件处理函数,而不是一个字符串,也就是props值应该是一个函数类型数据,事件函数方法外面得用一个双大括号包裹起来
  • on*EventType的事件类型属性,只能用作在普通的原生html标签上(例如:div,input,a,p等,例如:<div onClick={ 事件处理函数 }></div>),无法直接用在自定义组件标签上,也就是:<Button onClick={事件处理方法}></Button>,这样写是不起作用的
  • 不能通过返回false的方式阻止默认行为,必须显示使用preventDefault,如下所示
function handleClick(event){
  event.preventDefault();    
}

event(事件)对象

事件是web浏览器通知应用程序发生的什么事情,例如:鼠标点击,移动,键盘按下等

它并不是javascript对象,但是由事件触发的事件处理函数接收携带的事件对象参数(event),它会记录这个事件的一些详细的具体信息

 <a href="#" onClick = { this.handleLink} >链接</a>

handleLink(event){
    event.preventDefault();
    console.log(event);
  }

event会记录该事件对象的信息,如下图所示


事件对象信息.png

当给DOM元素绑定了事件处理函数的时候,该函数会自动的传入一个event对象,这个对象和普通的浏览器的对象记录了当前事件的属性和方法

在React中,event对象并不是浏览器提供的,你可以将它理解为React的事件对象,由React将原生浏览器的event对象进行了封装,对外提供一公共的API接口,无需考虑各个浏览器的兼容性

与原生浏览器处理事件的冒泡(event.stopPropatation),阻止默认行为(event.preventDefault)使用一样

this绑定性能比较

在上一节中已经对this的绑定进行了学习,在一次拿出来,说明它的重要性

通常在对JSX元素绑定事件监听处理函数时,针对this的绑定,将事件处理函数绑定到当前组件的实例上:以获取到父组件传来的props

以下几种方式可以确保函数可以访问组件属性

  • 在构造函数中绑定
    在constructor中进行this坏境的绑定,初始化事件监听处理函数
class Button extends Component{
    constructor(props){
        super(props);
        // 在构造器函数中进行this坏境的绑定
        this.handleBtnClick = this.handleBtnClick.bind(this);
    }
    
    render(){
        return (
            <div>
               <button onClick={ this.handleBtnClick }>按钮</button>
            </div>
        );
    }

handleBtnClick(){
    alert(this);
}
} 

当在JSX上进行事件监听绑定的时候,对于JSX回调函数中的this,由于Es6中的class的方法默认不会绑定this,如果你不进行this的坏境绑定,忘记绑定事件处理函数,并把它传给事件方法(上面是onClick),那么this的值是undefined

解决这个问题:一种是如上面的在构造器函数中进行this坏境的绑定,这种方式是React官方推荐的,也是性能比较好的

第二种方式是直接在JSX上,Reander中通过bind进行this的绑定

 <button onClick={ this.handleBtnClick.bind(this) }>按钮</button>

使用这种bind直接的绑定,每次渲染组件,都会创建一个新的函数,一般而言,这种写法也没什么问题,但是如果该回调函数作为prop值传入子组件时,这些组件就会进行额外的重新渲染,会影响性能,这与使用箭头函数同样存在这样的问题

解决办法:

  • 在构造器函数中进行绑定,如上所示:
  • 利用class fields(类字段)语法
class Button extends Component{
   
    // 类字段的形式进行绑定,函数表达式
    handleClick = () => {
        alert("学习React基础内容");
    }
    render(){
        return (
            <div>
               <button onClick={ this.handleBtnClick }>按钮</button>
            </div>
        );
    }
} 

如果不用类字段语法,可以在回调中使用箭头函数,这与它是等价的

class Button extends Component{
   
   
    handleClick()
        alert("学习React基础内容");
    }
    render(){
        return (
            <div>
               <button onClick={ () => { this.handleBtnClick } }>按钮</button>
            </div>
        );
    }
} 

此方法与直接在Render函数中使用bind绑定this坏境一样存在性能问题,当该事件处理函数作为prop传入子组件,必定会引起Render函数的渲染

所以出于性能的考虑,将this的绑定放在constructr函数中或者用类字段的语法来解决这种性能瓶颈问题

向事件处理程序中传递参数

在循环操作列表中,有时候要实现某些操作,我们需要向事件处理函数传递一些额外的参数,比如说:索引,要删除哪一行的ID
通过以下两种方式都可以向事件处理函数传递参数

<button onClick = { this.handleBtnDelete.bind(this,id)}>删除</butto/n>
或者
<button onClick = { (e) => this.handleDelete(id, e) }>删除</button>

如下以一个删除list的例子,效果如下,代码所示


向事件处理函数中传入参数.gif
import React, { Fragment, Component } from 'react';      
import ReactDOM from 'react-dom';

class List extends Component {
  constructor(props){
    super(props);

    const { list } = this.props;
    this.state = {
      list: list
    }
    
  }

  render(){
    const { list } = this.state;
    return (
      <Fragment>
          <ul>
              {
                // list.map((item, index) => <li onClick={ this.handleDelete.bind(this, index)} key={index}>{ item }</li>)
                list.map((item, index) => <li onClick={ (e) => this.handleDelete(index, e)} key={index}>{ item }</li>)
              }
          </ul>
      </Fragment>
    );
  }

  handleDelete(index, e){
    console.log(e)
     // 拷贝state的一个副本,不要直接的去更改state,在React中,不允许对state做任何改变
     const list = [...this.state.list]; 
     list.splice(index,1);

     this.setState(() => ({
         list: list 
     }))
  }


} 


const listData = ["itclanCoder", "川川", "chrome", "Firefox", "IE"]


const container = document.getElementById('root');

ReactDOM.render(<List list={ listData }  />, container);

在上面代码中,分别在render函数中绑定(Function.proptype.bind)和利用箭头函数包裹事件处理器,向事件监听处理函数传递参数,都是等价的

<button onClick = { this.handleBtnClick(this, id)}></button>
// 等价于
<button onClick = { () => this.handleBtnClick(id) }></button>

若使用箭头函数,React的事件对象会被作为第二个参数传递,而且也必须显示的传递进去

而通过bind的方式,事件对象以及更多的参数将会被隐式的传递进去

在render函数中直接的通过bind方法的绑定,会在每次组件渲染时都会创建一个新的函数,它会影响性能:最好是放在constructor函数中进行this坏境的绑定,因为constructor函数只会执行一次

constructor(props){
    super(props);
    // 事件监听处理函数,this坏境的绑定
    this.handleDelete = this.handleDelete.bind(this);
}

解决事件处理函数每次被重复渲染的问题

在Es5中,当调用一个函数时,函数名往往要加上一个圆括号,而在JSX 中给React元素绑定事件处理函数时,一个不小心,就习惯给加上了的

这就会造成,每次组件渲染时,这个函数都会被调用,会引起不必要的render函数渲染,将会引起性能问题

应当确保在传递一个函数给组件时,没有立即调用这个函数,如下所示

render(){
    return (
       <button onClick = { this.handleClick()}>button</button>
    );
}

正确的做法是,应该传递该事件函数本身(不加括号),如下所示

render(){
   <button onClick = { this.handleClick }>button</button> 
}

下面介绍本节的重点,听过函数节流,防抖,但不一定就真的就懂了

如何阻止函数调用太快(函数节流)或者太多次(函数防抖)

有时候,当用户频繁的与UI界面操作交互时,例如:窗口调整(触发resize),页面滚动,上拉加载(触发scroll),表单的按钮提交,商城抢购疯狂的点击(触发mousedown),而实时的搜索(keyup,input),拖拽等

当你频繁的触发用户界面时,会不停的触发事件处理函数,换而言之,当出现连续点击,上拉加载,实时搜索,对DOM元素频繁操作,请求资源加载等耗性能的操作,可能导致界面卡顿,浏览器奔溃,页面空白等情况

而解决这一问题的,正是函数节流与函数防抖
函数节流
定义: 节约(减少)触发事件处理函数的频率,连续每隔一定的时间触发执行的函数,它是优化高频率执行一段js代码的一种手段

特点: 不管事件触发有多频繁,都会保证在规定的间隔时间内真正的执行一次事件处理函数

应用场景: 常用于鼠标连续多次点击click事件,鼠标移动mousemove,拖拽,窗口尺寸改动(resize),鼠标滚轮页面上拉(onScroll),上拉刷新懒加载

原理: 通过判断是否达到一定的时间来触发函数,若没有规定时间则使用计时器进行延迟,而下一次事件则会重新设定计时器,它是间隔时间执行

通常与用户界面高频的操作有:

  • 鼠标滚轮页面上拉(onScroll),下拉刷新懒加载
  • 窗口尺寸改动(onresize)
  • 拖拽

若是高频操作,若不进行一定的处理,必然会造成多次数据的请求,服务器的压力,这样代码的性能是非常低效的,影响性能,降低这种频繁操作的一个重要的手段,就是降低频率,通过节流控制,也就是让核心功能代码在一定的时间,隔多长时间内执行一次

节流就是保证一段时间内只执行一次核心代码

你可以联想生活中节约用水(三峡大坝设置很多水闸)的例子:

高频事件就像是一个大开的水龙头,水流源源不断的大量流出,就像代码在不断的执行,若不加以控制,就会造成资源的一种浪费

对应页面中的,若是表单中连续点击提交按钮,监听滚动事件,连续下拉加载等请求服务器的资源

要节流,拧紧水龙头,要它的流水频率降低,每隔一段时间滴一滴水的,从而节省资源

在代码中的体现就是:设置一定时器,让核心功能代码,隔间段的去执行

下面是一个鼠标滚轮,节流操作实现:类似连续操作的,都是如此,连续点击按钮,上拉加载
节流方式一:时间戳+定时器

/* throttle1函数,节流实现方式1:时间戳+定时器
         *  @params method,duration 第一个参数为事件触发时的真正要执行的函数
         *  第二个参数duration表示为定义的间隔时间
         *
         *  原理:通过判断是否达到一定的时间来触发函数,若没有规定时间则使用计时器进行延迟,而下一次事件则会重新设定计时器,它是间隔时间执行,不管事件触发有多频繁,都会保证在规定内的事件一定会执行一次真正事件处理函数
         *
         * */
         function throttle1(method, duration) {
            var timer = null;
            var prevTime = new Date();   // 之前的时间
            return function() {
                var that = this,
                    currentTime = new Date(),          // 获取系统当前时间
                    resTime = currentTime - prevTime;  // 时间戳
                // 打印本次当前的世间和上次世间间隔的时间差
                console.log("时间差", resTime);
                // 当前距离上次执行时间小于设置的时间间隔
                if(resTime < duration) {
                    // 清除上次的定时器,取消上次调用的队列任务,重新设置定时器。这样就可以保证500毫秒秒内函数只会被触发一次,达到了函数节流的目的
                    clearTimeout(timer);
                    timer = setTimeout(function(){
                        prevTime = currentTime;
                        method.apply(that);
                    }, duration)
                }else { // 当前距离上次执行的时间大于等于设置的时间时,直接执行函数
                    // 记录执行方法的时间
                    prevTime = currentTime;
                    method.apply(that);
                }
                
            }
         }
         
        // 事件触发的方法(函数),函数节流1
         function handleJieLiu1(){
            console.log("节流方式1");
         }   
         
          var handleJieLiu1 = throttle1(handleJieLiu1, 500);
          document.addEventListener('mousewheel', handleJieLiu1);

节流方式二:

 /*
         * throttle2函数节流实现方式2:重置一个开关变量+定时器
         * @params method,duration形参数与上面的含义一致
         * @return 返回的是一个事件处理函数
         *
         * 在throttle2执行时定义了runFlag的初始值,通过闭包返回一个匿名函数作为事件处理函数,
         *
         * 在返回的函数内部判断runFlag的状态并确定执行真正的函数method还是跳出,每次执行method后会更改runFlag的状态,通过定时器在durtion该规定的间隔时间内重置runFlag锁的状态
         * 
          */
         function throttle2(method, duration){
            // 当前时间间隔内是否有方法执行,设置一个开关标识
            var runFlag = false;
            // 返回一个事件处理函数
            return function(e) {
                // 判断当前是否有方法执行,有则什么都不做,若为true,则跳出
                if(runFlag){
                    return false;
                }
                // 开始执行
                runFlag = true;
                // 添加定时器,在到达时间间隔时重置锁的状态
                setTimeout(function(){
                    method(e);
                    // 执行完毕后,声明当前没有正在执行的方法,方便下一个时间调用
                    runFlag = false;
                }, duration)
            }
         } 
         // 事件触发的方法(函数),函数节流2
         function handleJieLiu2(){
            console.log("节流方式2");
        }
        var handleJieLiu2 = throttle2(handleJieLiu2, 500);
        document.addEventListener('mousewheel', handleJieLiu2);

上面两种实现函数节流的方式都可以达到防止用户频繁操作而引起重复请求资源的

具体效果如下所示:


函数的节流.gif

从上面的效果示例当中,当鼠标滚轮不断滚动时,事件处理函数的执行顺序不一样

当给一个大范围的时间内,比如:1小时内,每几分钟执行一次,超过一小时不在执行,推荐使用第一种函数节流的方式

如果仅仅要求间隔一定时间执行一次,推荐使用第二种函数节流的方式
函数防抖

定义:防止抖动,防止重复的触发,频繁操作,核心在于,延迟事件处理函数的执行,一定时间间隔内只执行最后一次操作,例如:表单多次提交,推荐使用防抖

换句话说,也就是当连续触发事件时并没有执行事件处理函数,只有在某一阶段连续触发的最后一次才执行,它遵循两个条件

  • 必须要等待一段时间
  • 上一次触发的时间间隔要大于设定值才执行

特点: 某段时间内只执行一次
在生活中,你可以想象公交司机等人上车后,才出站一样

应用场景: 常应用于输入框事件keydown,keyup,搜索联想查询,只有在用户停止键盘输入时,才发送Ajax请求

原理: 它是维护一个计时器,规定在duration(延迟)时间后出过事事件处理函数,但是在duration时间内再次触发的话,都会清除当前的timer重新计时,这样一来,只有最后一次操作事件处理函数才被真正的触发

具体代码如下所示:

 /*
         *  函数防抖
         *  例如:假定时间间隔时500ms,频繁不同的操作5s,且每两次执行时间小于等于间隔500ms
         *  那么最后只执行了1次,也就是每一次执行时都结束上一次的执行
         *  @params method,duration,与上面一致
         *
         *  原理:它是维护一个计时器,规定在duration时间后出发时间处理函数,但是在duration时间内再次出发的化,都会清除当前的timer重新计时,这样一来,只有最后一次操作事件处理函数才被真正的触发
         *
         * 一般用于输入框事件,常用场景就是表单的搜索或者联想查询,如果不使用防抖会连续发送请求,增加服务器的压力,使用防抖后,会在用户输入要查询的关键词后才发送请求,百度搜索就是这么实现的
         *
         * 
          */
         function debounce(method, duration) {
            var timer = null;
            return function(){
                var that = this,
                    args = arguments;
                // 在本次调用之间的一个间隔时间内若有方法在执行,则终止该方法的执行
                if(timer) {
                    clearTimeout(timer);
                }
                // 开始执行本次调用
                timer = setTimeout(function(){
                    method.apply(that,args);
                }, duration)

            }

         }
         // 事件触发的方法(函数),防抖
        function handleFangDou(){
            console.log("函数的防抖",new Date());
        }
        var handleFangDou = debounce(handleFangDou, 500);
        var oInput = document.querySelector("#input"); // 获取input元素
        oInput.addEventListener('keyup',handleFangDou);

具体效果如下所示:


函数的防抖.gif

如上输入框效果所示,每当输入框输入后,键盘弹起时,执行事件处理函数,而不应该是键入内容时都触发一次事件处理函数

同理,搜索引擎,表单联想查询功能时,不是根据用户键入的字母,数字,内容同时进行Ajax数据请求的,如果每键入一个字母都触发一次数据请求,那就耗性能了的

应当是用户停止输入的时候才去触发查询请求,这个时候就用到函数防抖了的

表单的多次提交,百度搜索等都是用的防抖实现的
小结:

共同点: 都是解决频繁操作触发事件处理函数,引起页面卡顿,不流畅等性能问题,都是通过设置延时计时器逻辑来提升性能,以减少http请求次数,节约请求资源
不同点:函数节流,间隔时间内执行事件处理函数,而函数防抖,一定时间间隔内只执行最后一次操作

那么在React中,又是如何实现函数的节流,函数的防抖的?

在React中借用了一个loadsh.throttle的库实现函数的节流

首先你要在命令行终端下通过npm或者cnpm安装这个库

cnpm i -S lodash.throttle

然后在你编写的React组件内引入,调用一个throttle的函数,这个throttle接收两个参数,第一个参数是要触发的事件处理函数,第二个是延迟的时间,隔多少秒调用一次

下面是函数的节流代码,给定时间内被调用不能超过一次,对点击click事件处理器,使每秒钟只能调用一次

import React, { Fragment, Component } from 'react';      
import ReactDOM from 'react-dom';
import throttle from 'lodash.throttle'; // 引入lodash.throttle库

class LoadMoreButton extends Component {
  constructor(props) {
    super(props);
    
    this.state = {
      tip: '',
      trigerTimes: 1
    }

    this.handleClick = this.handleClick.bind(this);
    this.handleClickThrottled = throttle(this.handleClick, 1000);  // 将触发事件处理函数作为第一个参数传入,第二个参数为间隔的时间,这里是1秒
  }

  componentWillUnmount() {
    this.handleClickThrottled.cancel();
  }

  render() {
    return(
      <Fragment>
          <div><button onClick={ this.handleClickThrottled }>Load More</button></div>
          <div>{ this.state.tip }</div>
      </Fragment>
        
    ) 
  }

  handleClick() {
    this.setState({
      tip: `加载按钮触发了: ${ this.state.trigerTimes }次`,
      trigerTimes: this.state.trigerTimes+1
    })
  }
}

class Load extends Component {
  constructor(props){
    super(props);

  }

  render(){
    return (
        <Fragment>
            <LoadMoreButton   />
        </Fragment>
    );
  }

}
const container = document.getElementById('root');

ReactDOM.render(<Load  />, container);

效果如下所示

React中的loadsh.throttle库函数节流.gif

如果你不使用lodash.throttled第三方库实现函数的节流,同样,自己单独封装一个throttled实现函数节流也是可以的,例如:

import React, { Fragment, Component } from 'react';      
import ReactDOM from 'react-dom';


class LoadMoreButton extends Component {
  constructor(props) {
    super(props);
  
    this.state = {
      tip: "",
      trigerTimes: 1
    }
    
    this.handleLoadTime = this.handleLoadTime.bind(this);
    this.handleClick = this.handleClick.bind(this);
    this.handleClickThrottled = this.throttle(this.handleClick, 1000);  // 将触发事件处理函数作为第一个参数传入,第二个参数为间隔的时间,这里是1秒
    
  }

  
  render() {

    return(
        <Fragment>
              <div><button  onClick={ this.handleClickThrottled }>Load More</button></div>
              <div>{ this.state.tip }</div>
        </Fragment>
    ) 
  }

  handleLoadTime(){
    // this.setState((prevState) => ({
    //    tip: `加载按钮触发了: ${prevState.trigerTimes}次`,
    //    trigerTimes: prevState.trigerTimes+1
    // }))
    // 等价于下面的
    this.setState({
      tip: `加载按钮触发了: ${ this.state.trigerTimes }次`,
      trigerTimes: this.state.trigerTimes+1
    })
  }
// 事件处理函数
  handleClick() {
    this.handleLoadTime();
  }

  // 核心函数节流代码实现
  throttle(method, duration){
          // 当前时间间隔内是否有方法执行,设置一个开关标识
          var runFlag = false;
          // 返回一个事件处理函数
          return function(e) {
            // 判断当前是否有方法执行,有则什么都不做,若为true,则跳出
            if(runFlag){
              return false;
            }
            // 开始执行
            runFlag = true;
            // 添加定时器,在到达时间间隔时重置锁的状态
            setTimeout(function(){
              method(e);
              // 执行完毕后,声明当前没有正在执行的方法,方便下一个时间调用
              runFlag = false;
            }, duration)
          }
  } 
}

class Load extends Component {
  constructor(props){
    super(props);
   
  }

  render(){
    return (
        <Fragment>
            <LoadMoreButton />
        </Fragment>
    );
  }
}

const container = document.getElementById('root');

ReactDOM.render(<Load  />, container);

你可以试着不加第三方库lodash.throttled中的throtte函数以及不封装throttle函数,你会发现,当你点击按钮时,你连续点多少次,它会不断的触发事件处理函数,如果是一个表单提交按钮,使用函数的节流就很好的优化了代码了

不加函数节流的效果:如下所示:


不加节流函数的效果.gif

假如这是一个表单的提交按钮,你点击多少次,就向服务器请求多少次,这显然是有问题的,如果你用函数的节流就很好解决这个问题

上面说完了React的函数节流,那么函数防抖又怎么实现呢?同样,React可以借助一个第三方库loadsh.debounce来实现

你仍然先要在终端下通过npm或者cnpm或yarn的方式安装第三方库

npm i -S loadsh.debounce
或者
cnpm install -S loadsh.debounce

有没有安装上,可以在根目录下查看pageckage.json中的dependencies依赖里面有没有loadsh.debounce

下面看一个输入框,校验手机号的例子:
下面是没有使用函数防抖
示例代码如下所示:

import React, { Fragment, Component } from 'react';      
import ReactDOM from 'react-dom';


class SearchBox extends Component{
  constructor(props){
    super(props)
    this.state = {
      tip: null,
      trigerTimes: 1
    }
    
    this.handleChange = this.handleChange.bind(this);
  }

  handleChange(e){
    if(e.target.value){
      this.setState({
          tip: null        
      })
    }
    
  }

  handleKeyUp = (e) => {
    if(e.target.value){
      this.isPhoneLegal(e.target.value) // 对用户输入进行判断
    }
    
  }
  isPhoneLegal = (phone) => {
    const phoneRegexp = /^1([38]\d|5[0-35-9]|7[3678])\d{8}$/
    const { trigerTimes } = this.state
    if(phoneRegexp.test(phone)) {
      this.setState({
        tip: `手机号符合规则!`,
        trigerTimes: 0
      })
    } else {
      this.setState({
        tip: `手机号有误, 触发了:${trigerTimes}次`,
        trigerTimes: trigerTimes + 1
      })
    }

    // 这里发送Ajax请求
  }

  render() {
    return (
      <Fragment>
        <div><input  onChange = { this.handleChange } onKeyUp={ this.handleKeyUp} placeholder="请输入手机号" /></div>
        <div >
          {this.state.tip}
        </div>
      </Fragment>
    )
  }

}

class Search extends Component{
  render(){
    return (
      <Fragment>
          <SearchBox  />
      </Fragment>
    );
  }
}

const container = document.getElementById('root');

ReactDOM.render(<Search />, container);
函数未防抖-手机输入框.gif

未使用防抖,每次键盘keyup弹起一次,就会触发一次,用户未输入完成就提示输入有误,这种体验不是很好
换而言之,如果每次键盘弹起时,都发送Ajax请求,这种思路本是没错的,但是若是间隔时间很短,连续输入,总是频繁的发送Ajax请求,那就造成页面卡顿,服务器端的压力了
示例效果如下所示:


函数防抖,使用debounce函数防抖.gif

下面是使用了debounce函数进行函数防抖
示例代码如下所示

import React, { Fragment, Component } from 'react';      
import ReactDOM from 'react-dom';
//import throttle from 'lodash.throttle'; // 函数节流
import debounce from 'lodash.debounce'; // 函数防抖

class SearchBox extends Component{
  constructor(props){
    super(props)
    this.state = {
      tip: null,
      trigerTimes: 1
    }
    this.handleChange = this.handleChange.bind(this);
    this.isPhoneLegal = debounce(this.isPhoneLegal, 1000)
  }

  componentWillUnmount(){
    this.isPhoneLegal.cancel();
  }

  handleChange(e){
    if(e.target.value){
      this.setState({
          tip: null        
      })
    }
    
  }

  handleKeyUp = (e) => {
    if(e.target.value){
      this.isPhoneLegal(e.target.value) // 对用户输入进行判断
    }
    
  }
  isPhoneLegal = (phone) => {
    const phoneRegexp = /^1([38]\d|5[0-35-9]|7[3678])\d{8}$/
    const { trigerTimes } = this.state
    if(phoneRegexp.test(phone)) {
      this.setState({
        tip: `手机号符合规则!`,
        trigerTimes: 0
      })
    } else {
      this.setState({
        tip: `手机号有误, 触发了:${trigerTimes}次`,
        trigerTimes: trigerTimes + 1
      })
    }

    // 这里发送Ajax请求
  }

  render() {
    return (
      <Fragment>
        <div><input  onChange = { this.handleChange } onKeyUp={ this.handleKeyUp} placeholder="请输入手机号" /></div>
        <div >
          {this.state.tip}
        </div>
      </Fragment>
    )
  }

}

class Search extends Component{
  render(){
    return (
      <Fragment>
          <SearchBox  />
      </Fragment>
    );
  }
}

const container = document.getElementById('root');

ReactDOM.render(<Search />, container);

当然你不使用lodash.debounce这个库提供的debounce函数进行防抖,自己用原生的方法封装一个debounce也是可以,上面有介绍的
代码如下所示:你只需把对事件处理函数this坏境处的deboucunce更下一下即可,其他代码跟以前一样

 this.isPhoneLegal = this.debounce(this.isPhoneLegal, 1000)

注意此时debounce函数是放在这个searchBox组件内的,如果该debounce函数放在组件外部,直接用function声明式定义的,直接调用debouce函数名即可,要稍稍注意下区别,对于这种常用的函数,可以单独把它封装到一个文件里去也是可以的

收集成自己常用库当中,避免这种防抖函数分散在各个文件,到处都是的,函数节流也是如此
以下是debounce防抖函数的封装

// 自己封装一个debounce函数用于防抖
  debounce(method, duration) {
          var timer = null;
         /*return function(){
            var that = this,
                args = arguments;
            // 在本次调用之间的一个间隔时间内若有方法在执行,则终止该方法的执行
            if(timer) {
              clearTimeout(timer);
            }
            // 开始执行本次调用
            timer = setTimeout(function(){
              method.apply(that,args);
            }, duration)

          }*/
          // 上面的return匿名函数可以用Es6的箭头函数,以下写法与上面等价,最简洁的写法,但是没有上面的代码好理解
          return (...args) => {
            clearTimeout(timer);
            timer = setTimeout(() =>   method(...args), duration)
          }

  }

当然对于上面的代码,还是可以优化一下的,对于回调函数,在Es6中,常用于箭头函数来处理,这样会省去不少麻烦
例如:this的指向问题
如下所示:debouce函数最简易的封装

你也可以把上面的定时器初始值放在debouce函数作为第三个形参数设置,也是可以的

debounce(method, duration, timer = null) {
          return (...args) => {
            clearTimeout(timer);
            timer = setTimeout(() => {
              method(...args)
            }, duration)
          }

  }

如果自己封装throttledebounce函数,可以单独封装到一个文件对外暴露就可以了,在需要用它们的地方,通过import引入即可,在代码中直接调用就可以
在根目录下(以你自己的为准)创建一个throttle.js
通过export default 暴露出去

/*
*  @authors 川川 (itclancode@163.com)
 * @ID suibichuanji
 * @date    2019-08-24 19:08:17
 * @weChatNum 微信公众号:itclancoder
   @desc 封装节流函数
*  @param method,duration:method事件处理函数,duration:间隔的时间
*  @return 匿名函数
*  原理: 通过判断是否达到一定的时间来触发函数,
*  若没有规定时间则使用计时器进行延迟,而下一次事件则会重新设定计时器
*  它是间隔时间执行,不管事件触发有多频繁
*  都会保证在规定内的事件一定会执行一次真正事件处理函数
* 
 */
function throttle(method, duration) {
    var timer = null;
    var prevTime = new Date(); // 之前的时间
    return function() {
      var that = this,
        currentTime = new Date(), // 获取系统当前时间
        resTime = currentTime - prevTime; // 时间戳
      // 打印本次当前的世间和上次世间间隔的时间差
      console.log("时间差", resTime);
      // 当前距离上次执行时间小于设置的时间间隔
      if (resTime < duration) {
        // 清除上次的定时器,取消上次调用的队列任务,重新设置定时器。这样就可以保证500毫秒秒内函数只会被触发一次,达到了函数节流的目的
        clearTimeout(timer);
        timer = setTimeout(function() {
          prevTime = currentTime;
          method.apply(that);
        }, duration)
      } else { // 当前距离上次执行的时间大于等于设置的时间时,直接执行函数
        // 记录执行方法的时间
        prevTime = currentTime;
        method.apply(that);
      }

    }
  }
  export default throttle;

然后在需要使用函数节流文件中引入

import throttle from './throttle';

// 在组件的constructor内初始化,this坏境绑定处进行调用
this.handleClickThrottled = throttle(this.handleClick, 1000);

同理,若是自己封装debounce函数的防抖,把它单独的抽离出去封装成一个函数,通过export 对外暴露,供其他地方调用

/**
 * 
 * @authors 川川 (itclancode@163.com)
 * @ID suibichuanji
 * @date    2019-08-24 19:08:17
 * @weChatNum 微信公众号:itclancoder
 * @version $Id$
 * @description  函数防抖
 * @param { method, duration} [method是事件处理函数,duration是延迟时间]
 * 原理
 * 原理:它是维护一个计时器,规定在duration时间后出发时间处理函数
 * 但是在duration时间内再次出发的化,都会清除当前的timer重新计时
 * 这样一来,只有最后一次操作事件处理函数才被真正的触发
 *
 * 一般用于输入框事件,常用场景就是表单的搜索或者联想查询,
 * 如果不使用防抖会连续发送请求,增加服务器的压力
 * 使用防抖后,会在用户输入要查询的关键词后才发送请求,百度搜索就是这么实现的
 */
function  debounce(method, duration) {
          var timer = null;
         return function(){
            var that = this,
                args = arguments;
            // 在本次调用之间的一个间隔时间内若有方法在执行,则终止该方法的执行
            if(timer) {
              clearTimeout(timer);
            }
            // 开始执行本次调用
            timer = setTimeout(function(){
              method.apply(that,args);
            }, duration)

          }

  }

  export default debounce;

小结:

React中如何节流和防抖

  • 引用lodash.throttle第三方库的throttle函数
  • 自己封装throttle函数用于节流
  • 引用lodash.debounce迪桑库的debounce函数
  • 自己封装debounce函数用于防抖

总结

整篇文章主要从介绍React事件开始,event(事件)对象,this绑定性能比较,向事件处理程序中传递参数,到最后的如何阻止函数调用太快(函数节流,两种方式)或者太多次(函数防抖),分别用原生JS以及React中的第三方库实现

对于函数的节流与防抖是前端提升性能的手段,虽然就几行代码,但是面试时,常问不衰,让你手写,很多时候,拍拍胸脯,不借助搜索,你还真不一定能写得出来

在实际的开发中,函数的节流与函数防抖也是比较频繁的,可见它的重要性不言而喻


itclancoder二维码.jpg
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • 作为一个合格的开发者,不要只满足于编写了可以运行的代码。而要了解代码背后的工作原理;不要只满足于自己的程序...
    六个周阅读 8,360评论 1 33
  • 前言 组件中的state具体是什么?怎么更改state的数据? setState函数分别接收对象以及函数有什么区别...
    itclanCoder阅读 823评论 0 0
  • 前言 为了进一步的了解React的工作过程,已经晓得了怎么编写React组件,知道了React的数据流,那么是时候...
    itclanCoder阅读 758评论 0 1
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,770评论 1 18
  • 前端开发面试题 面试题目: 根据你的等级和职位的变化,入门级到专家级,广度和深度都会有所增加。 题目类型: 理论知...
    怡宝丶阅读 2,548评论 0 7