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

相关文章
相关标签/搜索