React學(xué)習(xí)(7)-React中的事件處理

前言

props與state都是用于組件存儲數(shù)據(jù)的一js對象,前者是對外暴露數(shù)據(jù)接口,后者是對內(nèi)組件的狀態(tài),它們決定了UI界面顯示形態(tài),而若想要用戶與界面有些交互動作

也就是web瀏覽器通知應(yīng)用程序發(fā)生了什么事情,例如:鼠標(biāo)點(diǎn)擊,移動,鍵盤按下等頁面發(fā)生相應(yīng)的反饋,它是用戶與文檔或者瀏覽器窗口中發(fā)生的一些特定的交互瞬間. 這個時候就需要用事件實(shí)現(xiàn)了

在原生JS操作DOM中,往往有內(nèi)聯(lián)方式(

  • 在HTML中直接事件綁定
<p onclick="alert('關(guān)注微信itclanCoder公眾號')"></p>
  • 直接綁定
對象.事件類型 = 匿名函數(shù),obj.onclick = function(){})
  • 事件委托監(jiān)聽方式
     對象.addEventListener('事件類型,不帶on', 回調(diào)函數(shù)))

對DOM對象進(jìn)行事件監(jiān)聽處理

而在React中事件處理和內(nèi)聯(lián)方式相似,但是卻有些不同,如何確保函數(shù)可以訪問組件的屬性?

如何傳遞參數(shù)給事件處理器回調(diào)? 怎樣阻止函數(shù)被調(diào)用太快或者太多次?有什么解決辦法?

頻繁操作DOM會造成瀏覽器的卡頓,響應(yīng)不及時,引起瀏覽器的重繪重排,從而加重了瀏覽器的壓力

頻繁的調(diào)用后臺接口,好好的接口被你玩壞,造成頁面空白,崩潰,容易被后端同學(xué)提刀來見

既要提升用戶體驗(yàn),又要減少服務(wù)器端的開銷

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

React中的事件

在React中事件的綁定是直接寫在JSX元素上的,不需要通過addEventListener事件委托的方式進(jìn)行監(jiān)聽
寫法上:

  • 在JSX元素上添加事件,通過on*EventType這種內(nèi)聯(lián)方式添加,命名采用小駝峰式(camelCase)的形式,而不是純小寫(原生HTML中對DOM元素綁定事件,事件類型是小寫的),無需調(diào)用addEventListener進(jìn)行事件監(jiān)聽,也無需考慮兼容性,React已經(jīng)封裝好了一些的事件類型屬性(ps:onClick,onMouseMove,onChange,onFocus)等
  • 使用JSX語法時,需要傳入一個函數(shù)作為事件處理函數(shù),而不是一個字符串,也就是props值應(yīng)該是一個函數(shù)類型數(shù)據(jù),事件函數(shù)方法外面得用一個雙大括號包裹起來
  • on*EventType的事件類型屬性,只能用作在普通的原生html標(biāo)簽上(例如:div,input,a,p等,例如:<div onClick={ 事件處理函數(shù) }></div>),無法直接用在自定義組件標(biāo)簽上,也就是:<Button onClick={事件處理方法}></Button>,這樣寫是不起作用的
  • 不能通過返回false的方式阻止默認(rèn)行為,必須顯示使用preventDefault,如下所示
function handleClick(event){
  event.preventDefault();    
}

event(事件)對象

事件是web瀏覽器通知應(yīng)用程序發(fā)生的什么事情,例如:鼠標(biāo)點(diǎn)擊,移動,鍵盤按下等

它并不是javascript對象,但是由事件觸發(fā)的事件處理函數(shù)接收攜帶的事件對象參數(shù)(event),它會記錄這個事件的一些詳細(xì)的具體信息

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

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

event會記錄該事件對象的信息,如下圖所示


事件對象信息.png

當(dāng)給DOM元素綁定了事件處理函數(shù)的時候,該函數(shù)會自動的傳入一個event對象,這個對象和普通的瀏覽器的對象記錄了當(dāng)前事件的屬性和方法

在React中,event對象并不是瀏覽器提供的,你可以將它理解為React的事件對象,由React將原生瀏覽器的event對象進(jìn)行了封裝,對外提供一公共的API接口,無需考慮各個瀏覽器的兼容性

與原生瀏覽器處理事件的冒泡(event.stopPropatation),阻止默認(rèn)行為(event.preventDefault)使用一樣

this綁定性能比較

在上一節(jié)中已經(jīng)對this的綁定進(jìn)行了學(xué)習(xí),在一次拿出來,說明它的重要性

通常在對JSX元素綁定事件監(jiān)聽處理函數(shù)時,針對this的綁定,將事件處理函數(shù)綁定到當(dāng)前組件的實(shí)例上:以獲取到父組件傳來的props

以下幾種方式可以確保函數(shù)可以訪問組件屬性

  • 在構(gòu)造函數(shù)中綁定
    在constructor中進(jìn)行this壞境的綁定,初始化事件監(jiān)聽處理函數(shù)
class Button extends Component{
    constructor(props){
        super(props);
        // 在構(gòu)造器函數(shù)中進(jìn)行this壞境的綁定
        this.handleBtnClick = this.handleBtnClick.bind(this);
    }
    
    render(){
        return (
            <div>
               <button onClick={ this.handleBtnClick }>按鈕</button>
            </div>
        );
    }

handleBtnClick(){
    alert(this);
}
} 

當(dāng)在JSX上進(jìn)行事件監(jiān)聽綁定的時候,對于JSX回調(diào)函數(shù)中的this户誓,由于Es6中的class的方法默認(rèn)不會綁定this,如果你不進(jìn)行this的壞境綁定,忘記綁定事件處理函數(shù),并把它傳給事件方法(上面是onClick),那么this的值是undefined

解決這個問題:一種是如上面的在構(gòu)造器函數(shù)中進(jìn)行this壞境的綁定,這種方式是React官方推薦的,也是性能比較好的

第二種方式是直接在JSX上,Reander中通過bind進(jìn)行this的綁定

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

使用這種bind直接的綁定,每次渲染組件,都會創(chuàng)建一個新的函數(shù),一般而言,這種寫法也沒什么問題,但是如果該回調(diào)函數(shù)作為prop值傳入子組件時,這些組件就會進(jìn)行額外的重新渲染术裸,會影響性能,這與使用箭頭函數(shù)同樣存在這樣的問題

解決辦法:

  • 在構(gòu)造器函數(shù)中進(jìn)行綁定,如上所示:
  • 利用class fields(類字段)語法
class Button extends Component{
   
    // 類字段的形式進(jìn)行綁定,函數(shù)表達(dá)式
    handleClick = () => {
        alert("學(xué)習(xí)React基礎(chǔ)內(nèi)容");
    }
    render(){
        return (
            <div>
               <button onClick={ this.handleBtnClick }>按鈕</button>
            </div>
        );
    }
} 

如果不用類字段語法,可以在回調(diào)中使用箭頭函數(shù),這與它是等價的

class Button extends Component{
   
   
    handleClick()
        alert("學(xué)習(xí)React基礎(chǔ)內(nèi)容");
    }
    render(){
        return (
            <div>
               <button onClick={ () => { this.handleBtnClick } }>按鈕</button>
            </div>
        );
    }
} 

此方法與直接在Render函數(shù)中使用bind綁定this壞境一樣存在性能問題,當(dāng)該事件處理函數(shù)作為prop傳入子組件,必定會引起Render函數(shù)的渲染

所以出于性能的考慮,將this的綁定放在constructr函數(shù)中或者用類字段的語法來解決這種性能瓶頸問題

向事件處理程序中傳遞參數(shù)

在循環(huán)操作列表中,有時候要實(shí)現(xiàn)某些操作,我們需要向事件處理函數(shù)傳遞一些額外的參數(shù),比如說:索引,要刪除哪一行的ID
通過以下兩種方式都可以向事件處理函數(shù)傳遞參數(shù)

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

如下以一個刪除list的例子,效果如下,代碼所示


向事件處理函數(shù)中傳入?yún)?shù).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函數(shù)中綁定(Function.proptype.bind)和利用箭頭函數(shù)包裹事件處理器,向事件監(jiān)聽處理函數(shù)傳遞參數(shù),都是等價的

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

若使用箭頭函數(shù),React的事件對象會被作為第二個參數(shù)傳遞,而且也必須顯示的傳遞進(jìn)去

而通過bind的方式,事件對象以及更多的參數(shù)將會被隱式的傳遞進(jìn)去

在render函數(shù)中直接的通過bind方法的綁定,會在每次組件渲染時都會創(chuàng)建一個新的函數(shù),它會影響性能:最好是放在constructor函數(shù)中進(jìn)行this壞境的綁定,因?yàn)閏onstructor函數(shù)只會執(zhí)行一次

constructor(props){
    super(props);
    // 事件監(jiān)聽處理函數(shù),this壞境的綁定
    this.handleDelete = this.handleDelete.bind(this);
}

解決事件處理函數(shù)每次被重復(fù)渲染的問題

在Es5中,當(dāng)調(diào)用一個函數(shù)時,函數(shù)名往往要加上一個圓括號,而在JSX 中給React元素綁定事件處理函數(shù)時,一個不小心,就習(xí)慣給加上了的

這就會造成,每次組件渲染時,這個函數(shù)都會被調(diào)用,會引起不必要的render函數(shù)渲染,將會引起性能問題

應(yīng)當(dāng)確保在傳遞一個函數(shù)給組件時,沒有立即調(diào)用這個函數(shù),如下所示

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

正確的做法是,應(yīng)該傳遞該事件函數(shù)本身(不加括號),如下所示

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

下面介紹本節(jié)的重點(diǎn),聽過函數(shù)節(jié)流,防抖,但不一定就真的就懂了

如何阻止函數(shù)調(diào)用太快(函數(shù)節(jié)流)或者太多次(函數(shù)防抖)

有時候,當(dāng)用戶頻繁的與UI界面操作交互時,例如:窗口調(diào)整(觸發(fā)resize),頁面滾動,上拉加載(觸發(fā)scroll),表單的按鈕提交,商城搶購瘋狂的點(diǎn)擊(觸發(fā)mousedown),而實(shí)時的搜索(keyup,input),拖拽等

當(dāng)你頻繁的觸發(fā)用戶界面時,會不停的觸發(fā)事件處理函數(shù),換而言之,當(dāng)出現(xiàn)連續(xù)點(diǎn)擊,上拉加載,實(shí)時搜索,對DOM元素頻繁操作,請求資源加載等耗性能的操作,可能導(dǎo)致界面卡頓,瀏覽器奔潰,頁面空白等情況

而解決這一問題的,正是函數(shù)節(jié)流與函數(shù)防抖
函數(shù)節(jié)流
定義: 節(jié)約(減少)觸發(fā)事件處理函數(shù)的頻率,連續(xù)每隔一定的時間觸發(fā)執(zhí)行的函數(shù),它是優(yōu)化高頻率執(zhí)行一段js代碼的一種手段

特點(diǎn): 不管事件觸發(fā)有多頻繁,都會保證在規(guī)定的間隔時間內(nèi)真正的執(zhí)行一次事件處理函數(shù)

應(yīng)用場景: 常用于鼠標(biāo)連續(xù)多次點(diǎn)擊click事件,鼠標(biāo)移動mousemove,拖拽,窗口尺寸改動(resize),鼠標(biāo)滾輪頁面上拉(onScroll),上拉刷新懶加載

原理: 通過判斷是否達(dá)到一定的時間來觸發(fā)函數(shù),若沒有規(guī)定時間則使用計(jì)時器進(jìn)行延遲,而下一次事件則會重新設(shè)定計(jì)時器,它是間隔時間執(zhí)行

通常與用戶界面高頻的操作有:

  • 鼠標(biāo)滾輪頁面上拉(onScroll),下拉刷新懶加載
  • 窗口尺寸改動(onresize)
  • 拖拽

若是高頻操作,若不進(jìn)行一定的處理,必然會造成多次數(shù)據(jù)的請求,服務(wù)器的壓力,這樣代碼的性能是非常低效的,影響性能,降低這種頻繁操作的一個重要的手段,就是降低頻率,通過節(jié)流控制,也就是讓核心功能代碼在一定的時間,隔多長時間內(nèi)執(zhí)行一次

節(jié)流就是保證一段時間內(nèi)只執(zhí)行一次核心代碼

你可以聯(lián)想生活中節(jié)約用水(三峽大壩設(shè)置很多水閘)的例子:

高頻事件就像是一個大開的水龍頭,水流源源不斷的大量流出,就像代碼在不斷的執(zhí)行,若不加以控制,就會造成資源的一種浪費(fèi)

對應(yīng)頁面中的,若是表單中連續(xù)點(diǎn)擊提交按鈕,監(jiān)聽滾動事件,連續(xù)下拉加載等請求服務(wù)器的資源

要節(jié)流,擰緊水龍頭,要它的流水頻率降低,每隔一段時間滴一滴水的,從而節(jié)省資源

在代碼中的體現(xiàn)就是:設(shè)置一定時器,讓核心功能代碼,隔間段的去執(zhí)行

下面是一個鼠標(biāo)滾輪,節(jié)流操作實(shí)現(xiàn):類似連續(xù)操作的兢仰,都是如此,連續(xù)點(diǎn)擊按鈕,上拉加載
節(jié)流方式一:時間戳+定時器

/* throttle1函數(shù),節(jié)流實(shí)現(xiàn)方式1:時間戳+定時器
         *  @params method,duration 第一個參數(shù)為事件觸發(fā)時的真正要執(zhí)行的函數(shù)
         *  第二個參數(shù)duration表示為定義的間隔時間
         *
         *  原理:通過判斷是否達(dá)到一定的時間來觸發(fā)函數(shù),若沒有規(guī)定時間則使用計(jì)時器進(jìn)行延遲,而下一次事件則會重新設(shè)定計(jì)時器,它是間隔時間執(zhí)行,不管事件觸發(fā)有多頻繁,都會保證在規(guī)定內(nèi)的事件一定會執(zhí)行一次真正事件處理函數(shù)
         *
         * */
         function throttle1(method, duration) {
            var timer = null;
            var prevTime = new Date();   // 之前的時間
            return function() {
                var that = this,
                    currentTime = new Date(),          // 獲取系統(tǒng)當(dāng)前時間
                    resTime = currentTime - prevTime;  // 時間戳
                // 打印本次當(dāng)前的世間和上次世間間隔的時間差
                console.log("時間差", resTime);
                // 當(dāng)前距離上次執(zhí)行時間小于設(shè)置的時間間隔
                if(resTime < duration) {
                    // 清除上次的定時器,取消上次調(diào)用的隊(duì)列任務(wù),重新設(shè)置定時器熊响。這樣就可以保證500毫秒秒內(nèi)函數(shù)只會被觸發(fā)一次,達(dá)到了函數(shù)節(jié)流的目的
                    clearTimeout(timer);
                    timer = setTimeout(function(){
                        prevTime = currentTime;
                        method.apply(that);
                    }, duration)
                }else { // 當(dāng)前距離上次執(zhí)行的時間大于等于設(shè)置的時間時,直接執(zhí)行函數(shù)
                    // 記錄執(zhí)行方法的時間
                    prevTime = currentTime;
                    method.apply(that);
                }
                
            }
         }
         
        // 事件觸發(fā)的方法(函數(shù)),函數(shù)節(jié)流1
         function handleJieLiu1(){
            console.log("節(jié)流方式1");
         }   
         
          var handleJieLiu1 = throttle1(handleJieLiu1, 500);
          document.addEventListener('mousewheel', handleJieLiu1);

節(jié)流方式二:

 /*
         * throttle2函數(shù)節(jié)流實(shí)現(xiàn)方式2:重置一個開關(guān)變量+定時器
         * @params method,duration形參數(shù)與上面的含義一致
         * @return 返回的是一個事件處理函數(shù)
         *
         * 在throttle2執(zhí)行時定義了runFlag的初始值,通過閉包返回一個匿名函數(shù)作為事件處理函數(shù),
         *
         * 在返回的函數(shù)內(nèi)部判斷runFlag的狀態(tài)并確定執(zhí)行真正的函數(shù)method還是跳出,每次執(zhí)行method后會更改runFlag的狀態(tài),通過定時器在durtion該規(guī)定的間隔時間內(nèi)重置runFlag鎖的狀態(tài)
         * 
          */
         function throttle2(method, duration){
            // 當(dāng)前時間間隔內(nèi)是否有方法執(zhí)行,設(shè)置一個開關(guān)標(biāo)識
            var runFlag = false;
            // 返回一個事件處理函數(shù)
            return function(e) {
                // 判斷當(dāng)前是否有方法執(zhí)行,有則什么都不做,若為true,則跳出
                if(runFlag){
                    return false;
                }
                // 開始執(zhí)行
                runFlag = true;
                // 添加定時器,在到達(dá)時間間隔時重置鎖的狀態(tài)
                setTimeout(function(){
                    method(e);
                    // 執(zhí)行完畢后,聲明當(dāng)前沒有正在執(zhí)行的方法,方便下一個時間調(diào)用
                    runFlag = false;
                }, duration)
            }
         } 
         // 事件觸發(fā)的方法(函數(shù)),函數(shù)節(jié)流2
         function handleJieLiu2(){
            console.log("節(jié)流方式2");
        }
        var handleJieLiu2 = throttle2(handleJieLiu2, 500);
        document.addEventListener('mousewheel', handleJieLiu2);

上面兩種實(shí)現(xiàn)函數(shù)節(jié)流的方式都可以達(dá)到防止用戶頻繁操作而引起重復(fù)請求資源的

具體效果如下所示:


函數(shù)的節(jié)流.gif

從上面的效果示例當(dāng)中,當(dāng)鼠標(biāo)滾輪不斷滾動時,事件處理函數(shù)的執(zhí)行順序不一樣

當(dāng)給一個大范圍的時間內(nèi),比如:1小時內(nèi),每幾分鐘執(zhí)行一次,超過一小時不在執(zhí)行,推薦使用第一種函數(shù)節(jié)流的方式

如果僅僅要求間隔一定時間執(zhí)行一次,推薦使用第二種函數(shù)節(jié)流的方式
函數(shù)防抖

定義:防止抖動,防止重復(fù)的觸發(fā),頻繁操作,核心在于诗赌,延遲事件處理函數(shù)的執(zhí)行,一定時間間隔內(nèi)只執(zhí)行最后一次操作,例如:表單多次提交,推薦使用防抖

換句話說,也就是當(dāng)連續(xù)觸發(fā)事件時并沒有執(zhí)行事件處理函數(shù),只有在某一階段連續(xù)觸發(fā)的最后一次才執(zhí)行,它遵循兩個條件

  • 必須要等待一段時間
  • 上一次觸發(fā)的時間間隔要大于設(shè)定值才執(zhí)行

特點(diǎn): 某段時間內(nèi)只執(zhí)行一次
在生活中,你可以想象公交司機(jī)等人上車后,才出站一樣

應(yīng)用場景: 常應(yīng)用于輸入框事件keydown,keyup,搜索聯(lián)想查詢,只有在用戶停止鍵盤輸入時,才發(fā)送Ajax請求

原理: 它是維護(hù)一個計(jì)時器,規(guī)定在duration(延遲)時間后出過事事件處理函數(shù),但是在duration時間內(nèi)再次觸發(fā)的話,都會清除當(dāng)前的timer重新計(jì)時,這樣一來,只有最后一次操作事件處理函數(shù)才被真正的觸發(fā)

具體代碼如下所示:

 /*
         *  函數(shù)防抖
         *  例如:假定時間間隔時500ms,頻繁不同的操作5s,且每兩次執(zhí)行時間小于等于間隔500ms
         *  那么最后只執(zhí)行了1次汗茄,也就是每一次執(zhí)行時都結(jié)束上一次的執(zhí)行
         *  @params method,duration,與上面一致
         *
         *  原理:它是維護(hù)一個計(jì)時器,規(guī)定在duration時間后出發(fā)時間處理函數(shù),但是在duration時間內(nèi)再次出發(fā)的化,都會清除當(dāng)前的timer重新計(jì)時,這樣一來,只有最后一次操作事件處理函數(shù)才被真正的觸發(fā)
         *
         * 一般用于輸入框事件,常用場景就是表單的搜索或者聯(lián)想查詢,如果不使用防抖會連續(xù)發(fā)送請求,增加服務(wù)器的壓力,使用防抖后,會在用戶輸入要查詢的關(guān)鍵詞后才發(fā)送請求,百度搜索就是這么實(shí)現(xiàn)的
         *
         * 
          */
         function debounce(method, duration) {
            var timer = null;
            return function(){
                var that = this,
                    args = arguments;
                // 在本次調(diào)用之間的一個間隔時間內(nèi)若有方法在執(zhí)行,則終止該方法的執(zhí)行
                if(timer) {
                    clearTimeout(timer);
                }
                // 開始執(zhí)行本次調(diào)用
                timer = setTimeout(function(){
                    method.apply(that,args);
                }, duration)

            }

         }
         // 事件觸發(fā)的方法(函數(shù)),防抖
        function handleFangDou(){
            console.log("函數(shù)的防抖",new Date());
        }
        var handleFangDou = debounce(handleFangDou, 500);
        var oInput = document.querySelector("#input"); // 獲取input元素
        oInput.addEventListener('keyup',handleFangDou);

具體效果如下所示:


函數(shù)的防抖.gif

如上輸入框效果所示,每當(dāng)輸入框輸入后,鍵盤彈起時,執(zhí)行事件處理函數(shù),而不應(yīng)該是鍵入內(nèi)容時都觸發(fā)一次事件處理函數(shù)

同理,搜索引擎,表單聯(lián)想查詢功能時,不是根據(jù)用戶鍵入的字母,數(shù)字,內(nèi)容同時進(jìn)行Ajax數(shù)據(jù)請求的,如果每鍵入一個字母都觸發(fā)一次數(shù)據(jù)請求,那就耗性能了的

應(yīng)當(dāng)是用戶停止輸入的時候才去觸發(fā)查詢請求,這個時候就用到函數(shù)防抖了的

表單的多次提交,百度搜索等都是用的防抖實(shí)現(xiàn)的
小結(jié):

共同點(diǎn): 都是解決頻繁操作觸發(fā)事件處理函數(shù),引起頁面卡頓,不流暢等性能問題,都是通過設(shè)置延時計(jì)時器邏輯來提升性能,以減少http請求次數(shù),節(jié)約請求資源
不同點(diǎn):函數(shù)節(jié)流,間隔時間內(nèi)執(zhí)行事件處理函數(shù),而函數(shù)防抖,一定時間間隔內(nèi)只執(zhí)行最后一次操作

那么在React中,又是如何實(shí)現(xiàn)函數(shù)的節(jié)流,函數(shù)的防抖的?

在React中借用了一個loadsh.throttle的庫實(shí)現(xiàn)函數(shù)的節(jié)流

首先你要在命令行終端下通過npm或者cnpm安裝這個庫

cnpm i -S lodash.throttle

然后在你編寫的React組件內(nèi)引入,調(diào)用一個throttle的函數(shù),這個throttle接收兩個參數(shù),第一個參數(shù)是要觸發(fā)的事件處理函數(shù),第二個是延遲的時間,隔多少秒調(diào)用一次

下面是函數(shù)的節(jié)流代碼,給定時間內(nèi)被調(diào)用不能超過一次,對點(diǎn)擊click事件處理器,使每秒鐘只能調(diào)用一次

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);  // 將觸發(fā)事件處理函數(shù)作為第一個參數(shù)傳入,第二個參數(shù)為間隔的時間,這里是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: `加載按鈕觸發(fā)了: ${ 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庫函數(shù)節(jié)流.gif

如果你不使用lodash.throttled第三方庫實(shí)現(xiàn)函數(shù)的節(jié)流,同樣,自己單獨(dú)封裝一個throttled實(shí)現(xiàn)函數(shù)節(jié)流也是可以的铭若,例如:

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);  // 將觸發(fā)事件處理函數(shù)作為第一個參數(shù)傳入,第二個參數(shù)為間隔的時間,這里是1秒
    
  }

  
  render() {

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

  handleLoadTime(){
    // this.setState((prevState) => ({
    //    tip: `加載按鈕觸發(fā)了: ${prevState.trigerTimes}次`,
    //    trigerTimes: prevState.trigerTimes+1
    // }))
    // 等價于下面的
    this.setState({
      tip: `加載按鈕觸發(fā)了: ${ this.state.trigerTimes }次`,
      trigerTimes: this.state.trigerTimes+1
    })
  }
// 事件處理函數(shù)
  handleClick() {
    this.handleLoadTime();
  }

  // 核心函數(shù)節(jié)流代碼實(shí)現(xiàn)
  throttle(method, duration){
          // 當(dāng)前時間間隔內(nèi)是否有方法執(zhí)行,設(shè)置一個開關(guān)標(biāo)識
          var runFlag = false;
          // 返回一個事件處理函數(shù)
          return function(e) {
            // 判斷當(dāng)前是否有方法執(zhí)行,有則什么都不做,若為true,則跳出
            if(runFlag){
              return false;
            }
            // 開始執(zhí)行
            runFlag = true;
            // 添加定時器,在到達(dá)時間間隔時重置鎖的狀態(tài)
            setTimeout(function(){
              method(e);
              // 執(zhí)行完畢后,聲明當(dāng)前沒有正在執(zhí)行的方法,方便下一個時間調(diào)用
              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函數(shù)以及不封裝throttle函數(shù),你會發(fā)現(xiàn),當(dāng)你點(diǎn)擊按鈕時,你連續(xù)點(diǎn)多少次,它會不斷的觸發(fā)事件處理函數(shù),如果是一個表單提交按鈕,使用函數(shù)的節(jié)流就很好的優(yōu)化了代碼了

不加函數(shù)節(jié)流的效果:如下所示:


不加節(jié)流函數(shù)的效果.gif

假如這是一個表單的提交按鈕,你點(diǎn)擊多少次,就向服務(wù)器請求多少次,這顯然是有問題的洪碳,如果你用函數(shù)的節(jié)流就很好解決這個問題

上面說完了React的函數(shù)節(jié)流,那么函數(shù)防抖又怎么實(shí)現(xiàn)呢?同樣,React可以借助一個第三方庫loadsh.debounce來實(shí)現(xiàn)

你仍然先要在終端下通過npm或者cnpm或yarn的方式安裝第三方庫

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

有沒有安裝上,可以在根目錄下查看pageckage.json中的dependencies依賴?yán)锩嬗袥]有l(wèi)oadsh.debounce

下面看一個輸入框,校驗(yàn)手機(jī)號的例子:
下面是沒有使用函數(shù)防抖
示例代碼如下所示:

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) // 對用戶輸入進(jìn)行判斷
    }
    
  }
  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: `手機(jī)號符合規(guī)則!`,
        trigerTimes: 0
      })
    } else {
      this.setState({
        tip: `手機(jī)號有誤, 觸發(fā)了:${trigerTimes}次`,
        trigerTimes: trigerTimes + 1
      })
    }

    // 這里發(fā)送Ajax請求
  }

  render() {
    return (
      <Fragment>
        <div><input  onChange = { this.handleChange } onKeyUp={ this.handleKeyUp} placeholder="請輸入手機(jī)號" /></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);
函數(shù)未防抖-手機(jī)輸入框.gif

未使用防抖,每次鍵盤keyup彈起一次,就會觸發(fā)一次,用戶未輸入完成就提示輸入有誤,這種體驗(yàn)不是很好
換而言之,如果每次鍵盤彈起時,都發(fā)送Ajax請求,這種思路本是沒錯的,但是若是間隔時間很短,連續(xù)輸入,總是頻繁的發(fā)送Ajax請求,那就造成頁面卡頓,服務(wù)器端的壓力了
示例效果如下所示:


函數(shù)防抖,使用debounce函數(shù)防抖.gif

下面是使用了debounce函數(shù)進(jìn)行函數(shù)防抖
示例代碼如下所示

import React, { Fragment, Component } from 'react';      
import ReactDOM from 'react-dom';
//import throttle from 'lodash.throttle'; // 函數(shù)節(jié)流
import debounce from 'lodash.debounce'; // 函數(shù)防抖

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) // 對用戶輸入進(jìn)行判斷
    }
    
  }
  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: `手機(jī)號符合規(guī)則!`,
        trigerTimes: 0
      })
    } else {
      this.setState({
        tip: `手機(jī)號有誤, 觸發(fā)了:${trigerTimes}次`,
        trigerTimes: trigerTimes + 1
      })
    }

    // 這里發(fā)送Ajax請求
  }

  render() {
    return (
      <Fragment>
        <div><input  onChange = { this.handleChange } onKeyUp={ this.handleKeyUp} placeholder="請輸入手機(jī)號" /></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);

當(dāng)然你不使用lodash.debounce這個庫提供的debounce函數(shù)進(jìn)行防抖,自己用原生的方法封裝一個debounce也是可以叼屠,上面有介紹的
代碼如下所示:你只需把對事件處理函數(shù)this壞境處的deboucunce更下一下即可,其他代碼跟以前一樣

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

注意此時debounce函數(shù)是放在這個searchBox組件內(nèi)的,如果該debounce函數(shù)放在組件外部,直接用function聲明式定義的瞳腌,直接調(diào)用debouce函數(shù)名即可,要稍稍注意下區(qū)別,對于這種常用的函數(shù),可以單獨(dú)把它封裝到一個文件里去也是可以的

收集成自己常用庫當(dāng)中,避免這種防抖函數(shù)分散在各個文件,到處都是的,函數(shù)節(jié)流也是如此
以下是debounce防抖函數(shù)的封裝

// 自己封裝一個debounce函數(shù)用于防抖
  debounce(method, duration) {
          var timer = null;
         /*return function(){
            var that = this,
                args = arguments;
            // 在本次調(diào)用之間的一個間隔時間內(nèi)若有方法在執(zhí)行,則終止該方法的執(zhí)行
            if(timer) {
              clearTimeout(timer);
            }
            // 開始執(zhí)行本次調(diào)用
            timer = setTimeout(function(){
              method.apply(that,args);
            }, duration)

          }*/
          // 上面的return匿名函數(shù)可以用Es6的箭頭函數(shù),以下寫法與上面等價,最簡潔的寫法,但是沒有上面的代碼好理解
          return (...args) => {
            clearTimeout(timer);
            timer = setTimeout(() =>   method(...args), duration)
          }

  }

當(dāng)然對于上面的代碼,還是可以優(yōu)化一下的,對于回調(diào)函數(shù),在Es6中,常用于箭頭函數(shù)來處理,這樣會省去不少麻煩
例如:this的指向問題
如下所示:debouce函數(shù)最簡易的封裝

你也可以把上面的定時器初始值放在debouce函數(shù)作為第三個形參數(shù)設(shè)置,也是可以的

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

  }

如果自己封裝throttledebounce函數(shù),可以單獨(dú)封裝到一個文件對外暴露就可以了,在需要用它們的地方,通過import引入即可,在代碼中直接調(diào)用就可以
在根目錄下(以你自己的為準(zhǔn))創(chuàng)建一個throttle.js
通過export default 暴露出去

/*
*  @authors 川川 (itclancode@163.com)
 * @ID suibichuanji
 * @date    2019-08-24 19:08:17
 * @weChatNum 微信公眾號:itclancoder
   @desc 封裝節(jié)流函數(shù)
*  @param method,duration:method事件處理函數(shù),duration:間隔的時間
*  @return 匿名函數(shù)
*  原理: 通過判斷是否達(dá)到一定的時間來觸發(fā)函數(shù),
*  若沒有規(guī)定時間則使用計(jì)時器進(jìn)行延遲,而下一次事件則會重新設(shè)定計(jì)時器
*  它是間隔時間執(zhí)行,不管事件觸發(fā)有多頻繁
*  都會保證在規(guī)定內(nèi)的事件一定會執(zhí)行一次真正事件處理函數(shù)
* 
 */
function throttle(method, duration) {
    var timer = null;
    var prevTime = new Date(); // 之前的時間
    return function() {
      var that = this,
        currentTime = new Date(), // 獲取系統(tǒng)當(dāng)前時間
        resTime = currentTime - prevTime; // 時間戳
      // 打印本次當(dāng)前的世間和上次世間間隔的時間差
      console.log("時間差", resTime);
      // 當(dāng)前距離上次執(zhí)行時間小于設(shè)置的時間間隔
      if (resTime < duration) {
        // 清除上次的定時器,取消上次調(diào)用的隊(duì)列任務(wù),重新設(shè)置定時器镜雨。這樣就可以保證500毫秒秒內(nèi)函數(shù)只會被觸發(fā)一次嫂侍,達(dá)到了函數(shù)節(jié)流的目的
        clearTimeout(timer);
        timer = setTimeout(function() {
          prevTime = currentTime;
          method.apply(that);
        }, duration)
      } else { // 當(dāng)前距離上次執(zhí)行的時間大于等于設(shè)置的時間時,直接執(zhí)行函數(shù)
        // 記錄執(zhí)行方法的時間
        prevTime = currentTime;
        method.apply(that);
      }

    }
  }
  export default throttle;

然后在需要使用函數(shù)節(jié)流文件中引入

import throttle from './throttle';

// 在組件的constructor內(nèi)初始化,this壞境綁定處進(jìn)行調(diào)用
this.handleClickThrottled = throttle(this.handleClick, 1000);

同理,若是自己封裝debounce函數(shù)的防抖,把它單獨(dú)的抽離出去封裝成一個函數(shù),通過export 對外暴露,供其他地方調(diào)用

/**
 * 
 * @authors 川川 (itclancode@163.com)
 * @ID suibichuanji
 * @date    2019-08-24 19:08:17
 * @weChatNum 微信公眾號:itclancoder
 * @version $Id$
 * @description  函數(shù)防抖
 * @param { method, duration} [method是事件處理函數(shù),duration是延遲時間]
 * 原理
 * 原理:它是維護(hù)一個計(jì)時器,規(guī)定在duration時間后出發(fā)時間處理函數(shù)
 * 但是在duration時間內(nèi)再次出發(fā)的化,都會清除當(dāng)前的timer重新計(jì)時
 * 這樣一來,只有最后一次操作事件處理函數(shù)才被真正的觸發(fā)
 *
 * 一般用于輸入框事件,常用場景就是表單的搜索或者聯(lián)想查詢,
 * 如果不使用防抖會連續(xù)發(fā)送請求,增加服務(wù)器的壓力
 * 使用防抖后,會在用戶輸入要查詢的關(guān)鍵詞后才發(fā)送請求,百度搜索就是這么實(shí)現(xiàn)的
 */
function  debounce(method, duration) {
          var timer = null;
         return function(){
            var that = this,
                args = arguments;
            // 在本次調(diào)用之間的一個間隔時間內(nèi)若有方法在執(zhí)行,則終止該方法的執(zhí)行
            if(timer) {
              clearTimeout(timer);
            }
            // 開始執(zhí)行本次調(diào)用
            timer = setTimeout(function(){
              method.apply(that,args);
            }, duration)

          }

  }

  export default debounce;

小結(jié):

React中如何節(jié)流和防抖

  • 引用lodash.throttle第三方庫的throttle函數(shù)
  • 自己封裝throttle函數(shù)用于節(jié)流
  • 引用lodash.debounce迪桑庫的debounce函數(shù)
  • 自己封裝debounce函數(shù)用于防抖

總結(jié)

整篇文章主要從介紹React事件開始,event(事件)對象,this綁定性能比較,向事件處理程序中傳遞參數(shù),到最后的如何阻止函數(shù)調(diào)用太快(函數(shù)節(jié)流,兩種方式)或者太多次(函數(shù)防抖),分別用原生JS以及React中的第三方庫實(shí)現(xiàn)

對于函數(shù)的節(jié)流與防抖是前端提升性能的手段,雖然就幾行代碼,但是面試時,常問不衰,讓你手寫,很多時候,拍拍胸脯,不借助搜索,你還真不一定能寫得出來

在實(shí)際的開發(fā)中,函數(shù)的節(jié)流與函數(shù)防抖也是比較頻繁的,可見它的重要性不言而喻


itclancoder二維碼.jpg
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末荚坞,一起剝皮案震驚了整個濱河市挑宠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颓影,老刑警劉巖各淀,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異诡挂,居然都是意外死亡碎浇,警方通過查閱死者的電腦和手機(jī)临谱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奴璃,“玉大人悉默,你說我怎么就攤上這事∧缃。” “怎么了麦牺?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長鞭缭。 經(jīng)常有香客問我剖膳,道長,這世上最難降的妖魔是什么岭辣? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任吱晒,我火速辦了婚禮,結(jié)果婚禮上沦童,老公的妹妹穿的比我還像新娘仑濒。我一直安慰自己,他們只是感情好偷遗,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布墩瞳。 她就那樣靜靜地躺著,像睡著了一般氏豌。 火紅的嫁衣襯著肌膚如雪喉酌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天泵喘,我揣著相機(jī)與錄音泪电,去河邊找鬼。 笑死纪铺,一個胖子當(dāng)著我的面吹牛相速,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鲜锚,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼突诬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了芜繁?” 一聲冷哼從身側(cè)響起攒霹,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎浆洗,沒想到半個月后催束,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡伏社,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年抠刺,在試婚紗的時候發(fā)現(xiàn)自己被綠了塔淤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡速妖,死狀恐怖高蜂,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情罕容,我是刑警寧澤备恤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站锦秒,受9級特大地震影響露泊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜旅择,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一惭笑、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧生真,春花似錦沉噩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至长已,卻和暖如春畜眨,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背痰哨。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工胶果, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留匾嘱,地道東北人斤斧。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像霎烙,于是被迫代替她去往敵國和親撬讽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 作為一個合格的開發(fā)者悬垃,不要只滿足于編寫了可以運(yùn)行的代碼游昼。而要了解代碼背后的工作原理;不要只滿足于自己的程序...
    六個周閱讀 8,428評論 1 33
  • 前言 組件中的state具體是什么?怎么更改state的數(shù)據(jù)? setState函數(shù)分別接收對象以及函數(shù)有什么區(qū)別...
    itclanCoder閱讀 871評論 0 0
  • 前言 為了進(jìn)一步的了解React的工作過程,已經(jīng)曉得了怎么編寫React組件,知道了React的數(shù)據(jù)流,那么是時候...
    itclanCoder閱讀 802評論 0 1
  • 原教程內(nèi)容詳見精益 React 學(xué)習(xí)指南尝蠕,這只是我在學(xué)習(xí)過程中的一些閱讀筆記烘豌,個人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,813評論 1 18
  • 前端開發(fā)面試題 面試題目: 根據(jù)你的等級和職位的變化看彼,入門級到專家級廊佩,廣度和深度都會有所增加囚聚。 題目類型: 理論知...
    怡寶丶閱讀 2,572評論 0 7