React學習(7)-React中的事件處理

  • 2019 年 12 月 10 日
  • 筆記

前言

props與state都是用於組件存儲數據的一js對象,前者是對外暴露數據介面,後者是對內組件的狀態,它們決定了UI介面顯示形態,而若想要用戶與介面有些交互動作

也就是web瀏覽器通知應用程式發生了什麼事情,例如:滑鼠點擊,移動,鍵盤按下等頁面發生相應的回饋,它是用戶與文檔或者瀏覽器窗口中發生的一些特定的交互瞬間. 這個時候就需要用事件實現了

在原生JS操作DOM中,往往有如下方式

  • 內聯方式(在HTML中直接事件綁定)
<p onclick="alert('關注微信itclanCoder公眾號')"></p>
  • 直接綁定(對象.事件類型 = 匿名函數
// DOM元素對象.事件類型 = 匿名函數  obj.onclick = function(){})
  • 事件委託監聽方式
//對象.addEventListener('事件類型,不帶on', 回調函數))對DOM對象進行事件監聽處理  document.addEventListener('click', function(){    alert("川川是個全宇宙最帥的小夥子");  })

而在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>

這樣寫是不起作用的,要想解決,也有方法,借用第三方庫,styled-component,這個我們在後續的內容當中單獨拿出來講的

  • 不能通過返回false的方式阻止默認行為,必須顯示使用preventDefault,如下所示
// 在React中無法通過return false阻止默認事件,下面是錯誤的寫法  function handleClick(){    // 邏輯程式碼    return 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會記錄該事件對象的資訊,如下圖所示

event所記錄的資訊

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

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

與原生瀏覽器處理事件的冒泡(event.stopProgatation()),阻止默認行為(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(){    console.log(this); // 會輸出Button組件  }  }

當在JSX上進行事件監聽綁定的時候,對於JSX回調函數中的this,由於Es6中的class的方法默認不會綁定this,如果你不進行this的壞境綁定,忘記綁定事件處理函數,並把它傳給事件方法(上面是onClick),那麼this的值是undefined

解決這個問題:

  • 一種是如上面的在構造器函數中進行this壞境的綁定,這種方式是React官方推薦的,也是性能比較好的
  • 第二種方式是直接在JSX上,Render中通過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的例子,效果如下,程式碼所示

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');  

在上面程式碼中,分別在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);

上面兩種實現函數節流的方式都可以達到防止用戶頻繁操作而引起重複請求資源的

具體效果如下所示

從上面的效果示例當中,當滑鼠滾輪不斷滾動時,事件處理函數的執行順序不一樣

當給一個大範圍的時間內,比如: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);

具體效果如下所示:

如上輸入框效果所示,每當輸入框輸入值後,當鍵盤彈起時,執行事件處理函數,而不應該是鍵入內容時都觸發一次事件處理函數

同理,搜索引擎,表單聯想查詢功能時,不是根據用戶鍵入的字母,數字,內容同時進行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);

效果如下所示

如果你不使用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函數,你會發現,當你點擊按鈕時,你連續點多少次,它會不斷的觸發事件處理函數,如果是一個表單提交按鈕,使用函數的節流就很好的優化了程式碼了

不加函數節流的效果:如下所示:

假如這是一個表單的提交按鈕,你點擊多少次,就向伺服器請求多少次,這顯然是有問題的,如果你用函數的節流就很好解決這個問題

上面說完了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);

未使用防抖時,每次鍵盤keyup彈起一次,就會觸發一次,用戶未輸入完成就提示輸入有誤,這種體驗不是很好 

換而言之,如果每次鍵盤彈起時,都發送Ajax請求,這種思路本是沒錯的,但是若是間隔時間很短,連續輸入,總是頻繁的發送Ajax請求,那就造成頁面卡頓,伺服器端的壓力了

正常的效果 示例效果如下所示:應該等鍵盤內容輸入完之後,才觸發事件處理函數

下面是使用了debounce函數進行函數防抖 示例程式碼如下所示

import React, { Fragment, Component } from 'react';  import ReactDOM from 'react-dom';  //import throttle from 'lodash.throttle'; // 函數節流  import debounce from 'lodash.debounce'; // 函數防抖,引入loadash.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) // 此處調用debounce函數,第一個參數為事件處理函數,第二個參數為延遲的時間間隔,這裡是1s  }    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)    }    }

如果自己封裝throttle和debounce函數,可以單獨封裝到一個文件對外暴露就可以了,在需要用它們的地方,通過import引入即可,在程式碼中直接調用就可以 在根目錄下(以你自己的為準)創建一個throttle.js 通過export default 暴露出去

/*  * @authors 川川 ([email protected])  * @ID suibichuanji  * @date 2019-08-31 21: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 川川 ([email protected])  * @ID suibichuanji  * @date 2019-08-31 21:10:17  * @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中的事件處理有了一定的理解和認識,光看仍然是迷迷迷糊的,似懂非懂,一手寫起來,就卡殼..文字講千百遍,不如程式碼擼一遍

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

對於函數的節流與防抖是前端提升性能的手段,雖然就幾行程式碼,但是面試時,常問不衰,讓你手寫,很多時候,拍拍胸脯,不藉助搜索引擎,還真不一定能立馬寫得出來

在實際的開發中,函數的節流與函數防抖也是用得比較頻繁的,可見它的重要性不言而喻