React專題:事件

本文是『horseshoe·React專題』系列文章之一,後續會有更多專題推出javascript

來個人 GitHub repo 閱讀完整的專題文章前端

來個人 我的博客 得到無與倫比的閱讀體驗java

用戶須要與UI產生交互,因此UI須要一個反應機制,用戶執行特定操做,就觸發特定的回調函數,開發者再把這個機制掛載到DOM元素上。react

DOM事件開發者再熟悉不過了,沒了它頁面就是死的。git

那麼React的事件機制有什麼特殊嗎?github

不誇張的說,React是一個UI虛擬機同樣的存在,在被掛載到頁面上以前,UI在React的全權掌控下。React會幹出什麼來誰也說不許。瀏覽器

讓咱們來看看React對DOM事件機制作了什麼手腳。babel

事件委託

事件委託咱們都知道,由於有冒泡機制,開發者能夠在父級元素監聽事件,經過邏輯判斷使得只有子元素的事件纔會觸發監聽回調,這樣就實現了子元素的事件監聽委託給父元素。異步

在前端刀耕火種時期,事件委託解決了兩個痛點。函數

  • 處理龐大的列表時,無需爲每一個列表項綁定事件監聽。
  • 動態掛載的元素無需做額外的事件監聽處理。

能夠看到,事件委託的紅利主要是性能提高,大量重複的事件監聽能夠交由一個事件監聽統一分發。

這樣的好處,React會不要?

不過,React作的更完全。

一個React應用只有一個事件監聽器,這個監聽器掛載在document上。你沒聽錯,就是這麼粗暴。全部的事件都由這個監聽器統一分發。

組件掛載和更新時,會將綁定的事件分門別類的放進一個叫作EventPluginHub的事件池裏。事件觸發時,根據事件產生的Event對象找到觸發事件的組件,再經過組件標識和事件類型從事件池裏找到對應的事件監聽回調,而後就是打個響指。

原生DOM事件系統會爲每一個事件生成一個Event對象,你去打印出來看看,這玩意有多少屬性。因此React一不作二不休,基於Event對象建立了一個合成事件對象。它能解決什麼問題呢?

  • 它能實現跨瀏覽器的表現一致性,由於React作了不少兼容性的處理。兼容性問題是前端的毒瘤啊。
  • 若是事件屢次觸發,合成事件對象會被複用,提升性能。

通常來講,當元素被卸載,元素綁定的事件監聽器也要清除。要否則JavaScript放個removeEventListener接口出來幹什麼?

由於React實現了對事件的統一管理,因此這些髒活累活都自動幫你幹了,你不須要手動清除JSX上綁定的事件監聽器。這同時也能夠提升性能,由於開發者多半會忘記清除。固然原生事件React就無能爲力了。

說到原生事件,React合成事件與原生事件是什麼關係呢?

答案是沒有關係,互不干擾。

如下例子,即使阻止了冒泡,點擊按鈕依然會同時觸發document事件。放心,不是兼容性的問題。合成事件擁有獨立的冒泡機制,它只能阻止頂層的合成事件。

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <button onClick={this.handleClick}>Click</button>
        );
    }
    
    componentDidMount() {
        document.addEventListener('click', (event) => console.log(event));
    }
    
    handleClick = (event) => {
        event.stopPropagation();
        console.log(event);
    }
}

export default App;
複製代碼

React知道你事多,因此在合成事件對象下面保存了原生事件對象nativeEvent,以備不時之需。

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <button onClick={this.handleClick}>Click</button>
        );
    }
    
    componentDidMount() {
        document.addEventListener('click', (event) => console.log(event));
    }
    
    handleClick = (event) => {
        event.nativeEvent.stopPropagation();
        console.log(event);
    }
}

export default App;
複製代碼

哈?你說仍是不行?

莫不是你計算機壞了,聽我一句勸,砸了吧。

別慌別慌,這裏還有一個知識點:

原生事件對象裏除了stopPropagation以外還有stopImmediatePropagation(是否是歷來沒用過),有什麼區別?

stopImmediatePropagation不只會阻止頂層事件的冒泡,連自身元素綁定的其餘事件也會阻止。由於同一個元素能夠綁定多個事件,而事件觸發順序是根據綁定順序來的,只要使用了這個方法,它以後綁定的兄弟事件也別想蹦躂了。

那這跟React有什麼關係呢?

你忘了?上面講到,React有一套合成事件機制,全部事件都由document統一分發。

因此呀,別看這倆一個在button上,一個在document上,其實它們都是在document上觸發的。

這下理解了爲何要用stopImmediatePropagation阻止冒泡了吧,它們是曹丕和曹植啊。

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <button onClick={this.handleClick}>Click</button>
        );
    }
    
    componentDidMount() {
        document.addEventListener('click', (event) => console.log(event));
    }
    
    handleClick = (event) => {
        event.nativeEvent.stopImmediatePropagation();
        console.log(event);
    }
}

export default App;
複製代碼

不信再看一個衍生例子。

這回stopImmediatePropagation不只不能阻止body事件,body事件還會先於button觸發。鐵證,React全部事件都是由document統一分發的。

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <button onClick={this.handleClick}>Click</button>
        );
    }
    
    componentDidMount() {
        document.body.addEventListener('click', (event) => console.log(event));
    }
    
    handleClick = (event) => {
        event.nativeEvent.stopImmediatePropagation();
        console.log(event);
    }
}

export default App;
複製代碼

合成事件的異步處理

先來看例子,你們以爲最終state裏的value是什麼?

答案是程序崩潰。

別看了,沒有語法錯誤。

報錯信息裏說Cannot read property 'value' of null,說明target爲空,問題是target怎麼會爲空呢?

癥結就在於React追求極致的性能。在合成事件機制裏,一旦事件監聽回調被執行,合成事件對象就會被銷燬,而setState的回調是異步的,等它執行的時候合成事件對象早就被銷燬了。這就是target爲空的緣由。

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <button onClick={this.handleClick}>Click</button>
        );
    }
    
    handleClick = (event) => {
        this.setState((prevState) => ({ value: event.target.value }));
    }
}

export default App;
複製代碼

若是實在有這樣的需求,React也有錦囊妙計:event.persist()

這就是告訴React,你別回收了,我還要拿去釣妹子呢。

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <button onClick={this.handleClick}>Click</button>
        );
    }
    
    handleClick = (event) => {
        event.persist();
        this.setState((prevState) => ({ value: event.target.value }));
    }
}

export default App;
複製代碼

咱們再來看一種狀況。

臥槽,怎麼又能夠了?我啥也沒跟React說呀。

咱們都說setState是異步(或者說批量更新)的,那是說渲染異步,而賦值給value是同步的。

因此這個時候value是有值的。

那爲何回調形式的setState得不到值呢?回調嘛,你想嘛,是同步仍是異步。

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <button onClick={this.handleClick}>Click</button>
        );
    }
    
    handleClick = (event) => {
        this.setState({ value: event.target.value });
    }
}

export default App;
複製代碼

綁定this

鬼知道JavaScript裏的this幹倒了多少人。

其實,要弄清楚this的指向,只要找到調用者就好了。調用者,就是this的題眼。

爲何例子中的函數在非嚴格模式下指向window,而在嚴格模式下指向undefined呢?

由於在JavaScript刀耕火種時代,window既是窗口對象,也是全局對象。而全部的全局變量(包括函數)都掛載在window下面。

非嚴格模式下這個函數的調用者就是window。

後面人們以爲這樣太八路軍了,甚至有人以爲這是JavaScript最大的設計失誤。因此以後的嚴格模式、class類和ESModule的全局變量都再也不掛載到window上,反正能找補一點是一點。

因此嚴格模式下這個函數沒有調用者,或者叫神之調用,因此this指向undefined。

function something() {
    console.log(this);
}
複製代碼

科普了一下this,咱們來看看this在React中有什麼幺蛾子。

(僞裝)咱們都知道,下面例子的this打印出來是undefined。

關鍵是爲何?

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <button onClick={this.handleClick}>Click</button>
        );
    }
    
    handleClick() {
        console.log(this);
    }
}

export default App;
複製代碼

先看一個別的例子。

obj.something是一個函數,action也是一個函數,區別在於調用者。一個函數一旦被從新賦值,它的調用者就可能發生變化。

const obj = {
    something: function() {
        console.log(this);
    },
};
obj.something(); // 打印obj

const action = obj.something;
action(); // (假設嚴格模式)打印undefined
複製代碼

再回到以前的例子,關鍵在這一句onClick={this.handleClick},注意回調已經被從新賦值了,無論未來它的調用者是誰,這時它已經和組件實例無關了。

而後,咱們要知道,React會把同一類型的事件push到一個隊列裏,一旦document監聽到這類事件,就會依次執行隊列裏的回調,直到冒泡被阻止。

就像這樣[handleDivClick, handleButtonClick],想象一下被這樣處理以後,執行時調用者是誰。

這就是上面例子打印出來是undefined的緣由。

其實React早期版本,程序會自動綁定this到組件實例,可是有人以爲這樣會使部分開發者覺得this指向組件實例就是理所應當的,因此取消了這一操做。

因而呢,開發者要手動綁定this。咱們來看看綁定this的花樣。

在JSX裏面直接綁定this

簡單粗暴對吧。再怎麼狸貓換太子,我都綁的死死的。

不過呢,bind的性能是堪憂的。並且你發現沒有,每一次從新render都會從新bind一次。

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <button onClick={this.handleClick.bind(this)}>Click</button>
        );
    }
    
    handleClick() {
        console.log(this);
    }
}

export default App;
複製代碼

包裹一層箭頭函數

箭頭函數會繼承父做用域的this,這裏的父做用域固然就是組件實例。

但是得額外包裹一層箭頭函數,並且每次觸發事件都會生成一個箭頭函數。

固然事件須要傳參的時候沒的說,必須得包裹一層箭頭函數。

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <button onClick={() => this.handleClick()}>Click</button>
        );
    }

    handleClick() {
        console.log(this);
    }
}

export default App;
複製代碼

在構造函數裏手動綁定

這也是React官方推薦的寫法。

此寫法的意思是:把一個綁定了this的回調賦值給實例的屬性。

缺點是若是事件比較多,構造函數裏會有點擁擠。

並且往深層處想,這個回調被掛載在了原型上,同時也被掛載在了實例上。重複掛載。

import React, { Component } from 'react';

class App extends Component {
    constructor(props) {
        super(props);
        this.handleClick = this.handleClick.bind(this);
    }
    
    render() {
        return (
            <button onClick={this.handleClick}>Click</button>
        );
    }
    
    handleClick() {
        console.log(this);
    }
}

export default App;
複製代碼

回調直接寫在實例上

這種寫法叫作屬性初始化器(Property Initializers)。目前還不是JavaScript正式的語法,不過babel能夠提早讓開發者使用。

首先說明,該寫法的關鍵不是直接寫在實例上,而是箭頭函數。由於箭頭函數會繼承父做用域的this,因此回調中的this指向組件實例。

不信你把箭頭函數改爲匿名函數試試。

那我能不能把箭頭函數寫在原型上呢?你甭管我用什麼辦法。

也是能夠的,只是有點麻煩。

屬性初始化器的寫法不會將回調重複掛載,不須要重複綁定,語法也至關優雅。

等成爲了JavaScript正式的語法,React官方必定會推薦這種寫法的。

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <button onClick={this.handleClick}>Click</button>
        );
    }
    
    handleClick = () => {
        console.log(this);
    }
}

export default App;
複製代碼

React專題一覽

什麼是UI

JSX

可變狀態

不可變屬性

生命週期

組件

事件

操做DOM

抽象UI

相關文章
相關標籤/搜索