React专题:事件

事件

转载于 Github
作者博客 博客入口

用户需要与UI产生交互,所以UI需要一个反应机制,用户执行特定操作,就触发特定的回调函数,开发者再把这个机制挂载到DOM元素上。

DOM事件开发者再熟悉不过了,没了它页面就是死的。

那么React的事件机制有什么特殊吗?

不夸张的说,React是一个UI虚拟机一样的存在,在被挂载到页面上之前,UI在React的全权掌控下。React会干出什么来谁也说不准。

让我们来看看React对DOM事件机制做了什么手脚。

事件委托

事件委托我们都知道,因为有冒泡机制,开发者可以在父级元素监听事件,通过逻辑判断使得只有子元素的事件才会触发监听回调,这样就实现了子元素的事件监听委托给父元素。

在前端刀耕火种时期,事件委托解决了两个痛点。

  • 处理庞大的列表时,无需为每个列表项绑定事件监听。
  • 动态挂载的元素无需作额外的事件监听处理。
    可以看到,事件委托的红利主要是性能提升,大量重复的事件监听可以交由一个事件监听统一分发。

这样的好处,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;
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值