React中的事件

发布于 大漠

在大多的Web页面或Web应用程序中,构建UI仅仅是其中小小的一部分,其中有些U是纯静态的,但有些UI是会随着用户的操作带来相应变化的。比如,用户通过鼠标点击,键盘按下,视窗大小调整或一系列其他的手势和交互来触发。而这些我们往往称之为事件。在Web中处理事件都是交给JavaScript来完成,同样的,在React中也有相应的事件。在这一节开始,我们就来学习React中的事件。

你可能在DOM的世界中对事件的使用有一定的了解。如果没有,也不用担心,我们在开启探讨React的事件之前会对JavaScript中的事件做一个初步的了解。

JavaScript中的事件

Web页面或Web应用程序总是会提供一些控件给用户操作的。简单地说,用户做了一个操作,会发生什么。或者说,应用程序用什么方法让它们对已知的事件作出反应。

在开始之前,我们必须了解事件是什么?这个很简单,在Web中创建的所有东西都可以通过以下的语句来建模:

当(...)发生,做(...)?

在Web中我们可以用无数种不同的方法来填补这句话中的空白之处。第一个()表示发生了什么?第二个()描述相应的操作会发生什么?比如下面这些例子:

  • 当(页面加载)完成时,请(播放视频
  • 当(点击)发生时,请(提交表单
  • 当(鼠标释放)时,请(放大图片
  • 当(按下删除键)时,请将此(列表项删除
  • 当(触摸手势)发生时,请将(旧照片过滤掉
  • 当(文件下载)完时,请(更新进度条

这种模型适用于我们所有的编码。不过,事件只不过是一个信号。它表示刚刚发生了什么事。而这个事件是可以鼠标点击键盘按下触摸手势等。回到我们所说的事件模型中,模型的前半部分是我们所说的事件,后面部分是事件反应

用到我们生活中来的话,有点类似于我们发送了一个信号,等事物接到这个信号就会做出一个对应的响应。而且在JavaScript中,事件又是非常重要的。如果用专业术语来描述的话,其包含两个部分:

  • 事件侦听
  • 事件做出的反应

这两个步骤看起来非常简单,但是不要忘记这是在JavaScript中处理。如果我们稍微走错了一步,都会给我们的应用程序带来巨大的创伤。

事件侦听

在JavaScript中有三种方式可以为DOM元素注册事件处理函数(即给元素添加事件侦听)。

addEventListener

最常见的就是通过addEventListener来给目标元素添加事件侦听。比如:

myButton.addEventListener('click', function(){
    alert('Hello, World!')
}, false)

HTMl属性

我们可以在HTML元素中添加事件:

<!-- HTML -->
<button onclick="alert('Hello, World!')">Click</button>

DOM元素属性

我们也可以直接给一个DOM元素属性添加相应的事件:

myButton.onclick = function(e){alert('Hello, World!')}

通常情况下,我们都习惯于使用addEventListener来给目标元素(DOM元素)添加事件:

targetElement.addEventListener(eventName, eventHandler, false)

简单介绍一下其组成部分:

  • targetElement:要侦听事件的元素或对象。通常是一个DOM元素,也可以是documentwindow或任何专门用于触发事件的对象
  • eventName:事件名称,在JavaScript中有关于事件的名称列表可以点击这里查阅
  • eventHandler:事件处理程序,就是程序要做的事情(比如用户点击了按钮,会发生什么事情?)
  • 事件冒泡或捕获:这是最后一个参数,在JavaScript中指的是事件冒泡或捕获

放到一起之后,他可能像下面这样:

btnEle.addEventListener('click',showMessage('大漠'), false)

const showMessage = (name) => {
    alert(`Hello, ${name}~`)
}

事件捕获或冒泡

在JavaScript事件中还有一个很重要的概念,也比前面有关于事件的基础知识更为复杂,那就是事件捕获和事件冒泡。为了更好的帮助我们理解事件中有关于这两方面的概念,使用一个简单的示例来向大家阐述。

<!-- HTML -->
<!DOCTYPE html>
<html>
    <body id="theBody" class="item">
        <div id="one_a" class="item">
            <div id="two" class="item">
                <div id="three_a" class="item">
                    <button id="buttonOne" class="item">one</button>
                </div>
                <div id="three_b" class="item">
                    <button id="buttonTwo" class="item">two</button>
                    <button id="buttonThree" class="item">three</button>
                </div>
            </div>
        </div>
        <div id="one_b" class="item"></div>
    </body>
</html>

如果我们用DOM树来描述上面的HTML结构的话,大致像下图这样:

假设用户点击了buttonOne按钮,即触发了一个click事件。很多同学都会认为click事件是从目标元素buttonOne开始触发,事实上并非如此,click事件从文档的根开始(即window)。如果用图来描述的话,如下;

click事件从文档根(window)开始,然后按照DOM树的路径一级一级往下寻找,直到触发click事件的butttOne元素(也称为事件目标)停止:

正如上图所示,事件所经过的路径是直的,它会通知该路径上的每个元素。如果该路径上有与当前事件匹配的元素,那么就会调用对应的事件处理程序。一旦事件达到目标,它就不会停止。不同的是,事件会往上移。事件路径上的每个元素都会得到其存在的通知。出现的任何事件处理程序也将被调用。

需要注意的是,事件在哪个位置启动并不重要,因为事件总是从文档的根开始,向下直接到达目标,然后返回根。在这样的一个过程中,从根开始向下寻找事件目标的过程被称为事件捕获阶段:

反过来,事件从目标元素向文档根元素的过程被称为事件冒泡阶段:

也就是说,当一个事件被触发时,会得到两次通知。每次监听事件时,我们都会选择要监听哪个阶段的事件。这是一个细节,我们可以给addEventListener指定truefalse来进行设置:

  • true:表示在捕获阶段侦听事件
  • false:表示在冒泡阶段侦听事件

有的时候我们需要结束事件的生命周期。只需要在事件对象上使用stopPropagation方法即可:

const handleClick = (e) => {
    e.stopPropagation()
}

上面这些是JavaScript中事件的基础知识,如果你想了解更多有关于JavaScript事件相关的知识,可以阅读下面这些教程:

React中的事件

基于我们现有的JavaScript经验,你可能已经非常习惯使用事件。但是,在React中处理事件的方式和JavaScript有所不同。React没有直接针对DOM事件,而是将它们包装在自己的事件包装器中。在接下来的小节中,我将和大家一起探讨和学习React中事件相关的知识点。

创建事件

为了更好的和大家聊React中的事件,我们从一个简单的示例开始。该示例创建一个包含inputbutton的表单控件。input可以输出你想要的文本内容,当input输入一个值时,将触发一个事件(一般是onChange)事件。另外,用户点击按钮时,也会触发一个事件(一般是onClick)事件,该事件会调用一个函数,该函数会将文本框的内容(文本)反转。

该示例大致是这样工作的:

  • 一个input,可以让用户输入想要的内容
  • 当用户在input中输入值时,将会触发onChange事件,它会调用一个handleChange()函数,该函数用于设置input的新状态
  • 当用户点击“点击我”按钮时(button),会触发另一个事件,该事件会调用handleReverse()函数,将输入框的文本反转

该示例的代码大致如下:

const App = () => {
    const [inputVal, setInputVal] = React.useState('')
    const [reversedText, setReversedText] = React.useState('')
    
    const handleChange = e => setInputVal(e.currentTarget.value)
    
    const handleClick = e => {
        e.preventDefault()
        setReversedText(inputVal.split("").reverse().join(""))
    }
    
    return (
        <React.Fragment>
            <form>
                <div className="controle">
                    <input type="text" value={inputVal} onChange={handleChange} placeholder="输入你想要的内容" />
                    <button onClick={handleClick}><span>点击我</span></button>
                </div>
            </form> 
            <p>{reversedText}</p>
        </React.Fragment>  
    )
}

上面示例中是React创建事件方式之一。如果你是使用类来创建的组件,那么组件中的创建事件方法可以像下面这样:

class MyComponent extends React.Component {
    handleClick = () => {
        // ...
    }

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

如果使用React Hooks来创建组件的话,除了上例中useState()方法外,我们还可以使用useRef()方法:

// React Hooks中使用useState()创建事件
class MyComponent = () => {
    const handleClick = e => useState()

    return <button onClick={handleClick}>Click Me</button>
}

// React Hooks中使用useRef()创建事件
class MyComponent = () => {
    const handleChange = e => useRef()

    return <button onClick={handleClick.current}>Click Me</button>
}

但在React Hooks中使用上面这两种方式创建事件,对性能有所影响,为了解决这个问题,建议使用useCallback()钩子函数,比如:

const MyComponent = () => {
    const handleClick = useCallback((e) => {
        // ...
    }, [/* deps */)
}

useCallback只在必要时返回一个新函数(每当deps数组中的值发生更改时),因此OptimizedButtonComponent将不会重新渲染超过必要的值。

有关于React Hooks中useState()useRef()useCallback()等钩子函数更详细的介绍,我们将会在后面的内容中详细阐述。这里不做过多的阐述。

React中的事件处理

从上面的示例中可以看出来:

React元素的事件处理和DOM元素很相似。 你只需要定义一个响应函数,让它成为事件处理程序并侦听事件。React将负责在检测到监听的事件时执行事件处理程序。

这里最重要的区别是,在React中通过元素的props(属性)来设置事件处理程序,而不是HTML的DOM元素的事件处理程序属性(attribute)。正如前面学习React时所了解到的,React中render()函数中的标记实际上是虚拟DOM(ReactElements),而不是真实的HTML DOM元素。

现在我们知道怎么在React组件创建事件、引用事件。但它们之间还是有所差异:

  • React事件的命名采用小驼峰式(camelCase),而不是纯小写
  • 使用JSX语法时需要传入一个函数作为事件处理函数,而不是一个字符串
  • React元素props引用的事件不会被调用,而只传递对它的引用
  • 在React中不能通过return false的方式来阻止事件冒泡,得必须使用.preventDefault()

经过前面的内容,估计大家对React事件有了一点点了解,接下来,我们来花点时间看看,React中事件是怎么处理的。在React中,处理事件响应的方式有多种,比如:

使用匿名函数

在React的render()函数中,可以使用匿名函数来响应,比如:

class SayHi extends React.Component {
    render() {
        return <button onClick={()=>{alert('Hello React!')}}>Click Me</button>
    }
}

buttonclick事件响应一个匿名函数,这也是React中最常见的处理事件响应的方式之一。这种方式简单直接。在该示例中,我们采用了ES6的箭头函数,其实也可以换成不是箭头函数那种:

class SayHi extends React.Component {
    render() {
        return <button onClick={function(){alert('Hello React!')}}>Click Me</button>
    }
}

但这种方式很少使用,因为会涉及到函数中this的绑定问题。有关于React中事件函数中关于this绑定的问题,我们将放到后面一起来学习。

当然,使用匿名函数是简单直接,但事件响应逻辑比较复杂的话,匿名函数的代码量就会很大,会导致render函数变得臃肿,不容易直观地看出组件最终渲染出的元素结构。另外,每次render方法调用时,都会重新创建一个匿名函数对象,带来额外的性能开销,当组件的层级嵌套越深时,这种开销越大。

使用组件方法

其实文章开头的示例就是组件方法的一种,只不过使用的是React Hooks来构建的组件。在这里我们来使用class来构建一个计数器的组件。

class Counter extends React.Component {
    constructor(props) {
        super(props)
        this.state = {count: 0}
        this.increaseHandleClick = this.increaseHandleClick.bind(this)
        this.decrementHandleClick = this.decrementHandleClick.bind(this)
    }

    increaseHandleClick = () => {
        this.setState({
            count: this.state.count + 1
        })
    }

    decrementHandleClick = () => {
        this.setState({
            count: this.state.count - 1
        })
    }

    render() {
        return (
            <React.Fragment>
                <button onClick={this.increaseHandleClick}><span>+</span></button>
                <p>{ this.state.count }</p>
                <button onClick={this.decrementHandleClick}><span>-</span></button>
            </React.Fragment>
        )
    }
}

buttonclick事件会响应组件中的方法:+按钮响应的是increaseHandleClick方法,-按钮响应的是decrementHandleClick方法。这种方式好处是每次render()方法调用,不会重新创建一个新的事件响应函数,不会有额外的性能开销,但这种方式需要在构造函数中使用.bind()手动绑定this

使用属性初始化语法

在React中还可以使用ES7的属性初始化语法(Property Initializers):

class Counter extends React.Component {
    constructor(props) {
        super(props)
        this.state = {count: 0}
        // this.increaseHandleClick = this.increaseHandleClick.bind(this)
        // this.decrementHandleClick = this.decrementHandleClick.bind(this)
    }

    increaseHandleClick = () => {
        this.setState({
            count: this.state.count + 1
        })
    }

    decrementHandleClick = () => {
        this.setState({
            count: this.state.count - 1
        })
    }

    render() {
        return (
            <React.Fragment>
                <button onClick={this.increaseHandleClick}><span>+</span></button>
                <p>{ this.state.count }</p>
                <button onClick={this.decrementHandleClick}><span>-</span></button>
            </React.Fragment>
        )
    }
}

这种方式和组件函数方式非常类似,最大的不同是不需要在构造函数中手动使用.bind()来绑定this

使用React Hooks钩子函数

React Hooks出来之后,我们可以使用Hooks中的一些钩子函数来给事件做出相应的响应。比如,上面的计数器,如果我们使用Hooks的话,可以像下面这样来改造:

const Counter = ()  => {
    const [count, setCount] = React.useState(0)

    const increaseHandleClick = () => {
        setCount(count + 1)
    }

    const decrementHandleClick = () => {
        setCount(count - 1)
    }

    return (
        <React.Fragment>
            <button onClick={increaseHandleClick}><span>+</span></button>
            <p>{ count }</p>
            <button onClick={decrementHandleClick}><span>-</span></button>
        </React.Fragment>
    )
}

React事件如何绑定this

this关键词在JavaScript中也是非常重要的知识点,在开始学习React中事件如何绑定this之前,咱们先简单的回顾一下JavaScript中绑定this的几个规则。

在非严格模式下,this是全局对象,在严格模式下是undefined

在简单函数调用情况下,非严格模式下,this将默认为全局对象:

function foo() {
    console.log(this.bar)
}

foo() // ~> undefined

var bar = 'w3cplus'

foo() // ~> w3cplus

相同的场景如果在严格模式下(strict mode),this将是undefined。比如下面的例子:

'use strict'

function foo() {
    console.log(this.bar)
}

foo() // ~> Uncaught TypeError: Cannot read property 'bar' of undefined

// 下面的代码不会被执行
var bar = 'w3cplus'

foo()

另外,全局级别上使用letconst声明的变量并不存储在全局对象中,而是存储在一个不可访问的声明性环境记录中,因此我闪前面的例子在使用let时给出了一个不同的结果:

function foo() {
    console.log(this.bar)
}

foo() // ~> undefined

let bar = 'w3cplus'

foo() // ~> undefined

window.bar = 'w3cplus'

foo() // ~> w3cplus

this指向函数被调用的对象

这条规则将适用于你日常代码中的大多数情况,并适用于调用对象的方法:

let Person = {
    name: '大漠',
    age: 30,
    foo: function() {
        console.log(this.name)
    }
}

Person.foo() // ~> w3cplus

如果我们的对象只包含对函数的引用,我们会得到相同的结果:

function foo() {
    console.log(this.name)
}

let Person = {
    name: 'w3cplus',
    age: 30,
    foo: foo
}

Person.foo() // ~> w3cplus

我们可以显式地告诉JavaScript引擎,使用callapplybindthis指向为某个值。

callapply可用于调用具有特定值的函数:

function foo() {
    console.log(this.name)
}

let Person = {
    name: 'w3cplus',
    agge: 30
}

foo.call(Person) // ~> w3cplus

callapply都完成相同的任务,它们的第一个参数应该是this所指向的。只有在需要将其他参数传递给被调用的函数时,这种并异才会明显。使用call,附加的参数将作为普通的以逗号分隔的参数列表传递,而使用apply,则可以传入参数数组。

bind用于创建一个永久绑定到此值的新函数。在下面的示例中,创建了一个新函数,它的this永久的绑定到Person,并将foo重新分配给这个新的永久绑定函数:

function foo(){
    console.log(this.name)
}

let Person = {
    name: 'w3cplus',
    age: 30
}

foo = foo.bind(Person)

foo() // ~> w3cplus

使用new关键字构造一个新对象,并且this指向这个对象

当使用new关键字将函数作为构造函数调用时,this指向创建的新对象:

function foo(name) {
    this.name = name
}

let Person = new foo('w3cplus')

console.log(Person.name) // ~> w3cplus

如果使用箭头函数,那么this将保持与其父作用域相同的值。例如,在这里,this在箭头函数保持相同的值:

function foo(name) {
    this.name = name
    this.bar = () => {
        return this.name.toUpperCase()
    }
}

const myFoo = new foo('w3cplus')

console.log(myFoo.name) // ~> w3cplus

console.log(myFoo.bar()) // ~> W3CPLUS

上面的就是JavaScript中绑定this的四个规则。也是this的基本运用。如果你对JavaScript中的this不太了解,建议你花点时间阅读下面这些教程:

有了上面的基础和相应的铺垫,我们回到React组件中。来看React中类组件,即,使用class创建一个类:

class Counter extends React.Component {
    constructor(props) {
        super(props)
        this.state= {
            count: 0
        }
    }

    increaseHandleClick(e) {
        console.log(e)
    }

    decrementHandleClick(e) {
        console.log(e)
    }

    render() {
        return (
            <React.Fragment>
                <button onClick={this.increaseHandleClick}><span>+</span></button>
                <p>{ this.state.count }</p>
                <button onClick={this.decrementHandleClick}><span>-</span></button>
            </React.Fragment>
        )
    }
}

这个时候点击+-按钮时,控制器输出的结果如下:

我们将注意力回到JSX的语法中来,其中点击回调方法对函数进行了this的绑定。那么为什么这里还需要进行绑定呢?在React中,JSX只是为了React.createElement(component, props, ...children)方法提供的语法糖。比如:

<React.Fragment>
    <button onClick={this.increaseHandleClick}><span>+</span></button>
    <p>{ this.state.count }</p>
    <button onClick={this.decrementHandleClick}><span>-</span></button>
</React.Fragment>

编译出来的结果为:

React.createElement(React.Fragment, null, React.createElement("button", {
    onClick: (void 0).increaseHandleClick
}, React.createElement("span", null, "+")), 

React.createElement("p", null, (void 0).state.count), 

React.createElement("button", {
    onClick: (void 0).decrementHandleClick
}, React.createElement("span", null, "-")));

那么上面的Counter组件就变成如下形式:

class Counter extends React.Component {
    constructor(props) {
        super(props)
        this.state= {
            count: 0
        }
    }

    increaseHandleClick(e) {
        console.log(e)
    }

    decrementHandleClick(e) {
        console.log(e)
    }

    render() {
        // return (
        //     <React.Fragment>
        //         <button onClick={this.increaseHandleClick}><span>+</span></button>
        //         <p>{ this.state.count }</p>
        //         <button onClick={this.decrementHandleClick}><span>-</span></button>
        //     </React.Fragment>
        // )
    
        return React.createElement(
            React.Fragment, 
            null, 
            React.createElement(
                "button", 
                {onClick: this.increaseHandleClick}, 
                React.createElement(
                    "span", 
                    null, 
                    "+"
                )
            ), 
            React.createElement(
                "p", 
                null, 
                this.state.count
            ), 
            React.createElement(
                "button", 
                {onClick: this.decrementHandleClick}, 
                React.createElement(
                    "span", 
                    null, 
                    "-"
                )
            )
        );
    }
}

在按钮中,React.createElement的第二个参数,传入了一个对象,而且这个对象里面有属性的值是取this对象里面的属性。当这个对象放入React.createElement执行后,去取这个this.increaseHandleClick(在-按钮对应的是this.decrementHandleClick)属性时,this已经不是我们在书写的时候认为的绑定在Counter上了。但在ES6的class中,this绑定了undefined。这也就是React组件点击事件回调函数需要手动绑定this

而在React组件中,将this指向组件实例,也就是响应事件绑定this有多种主式,常见的主要有:

// 在`class`的构造器中手动通过`bind()`绑定`this`
class Counter extends React.Component {
    constructor(props) {
        super(props)
        this.state= {
            count: 0
        }
        this.increaseHandleClick = this.increaseHandleClick.bind(this)
        this.decrementHandleClick = this.decrementHandleClick.bind(this)
    }

    increaseHandleClick(e) {
        console.log(e)
        console.log(this.state.count)
    }

    decrementHandleClick(e) {
        console.log(e)
        console.log(this.state.count)
    }

    render() {
        return (
            <React.Fragment>
                <button onClick={this.increaseHandleClick}><span>+</span></button>
                <p>{ this.state.count }</p>
                <button onClick={this.decrementHandleClick}><span>-</span></button>
            </React.Fragment>
        )
    }
}

上面是一个典型的类组件,当在其他组件中调用或使用ReactDOM.render()方法将Counter渲染到界面上时会生成一个组件的实例。根据 this指向的基本规则 不难发现,示例中的this最终会指向 组件实例。组件实例生成的时候,构造器constructor会执行:

constructor(props) {
    // ...

    this.increaseHandleClick = this.increaseHandleClick.bind(this)
    this.decrementHandleClick = this.decrementHandleClick.bind(this)
}

就拿this.increaseHandleClick = this.increaseHandleClick.bind(this)来举例。该语句中的this指向新生成的实例,赋值语句右侧的表达式先查找this.increaseHandleClick()这个方法,由对象的属性查找机制(沿原型链由近及远查找)可知道此处会查找到 原型方法this.increaseHandleClick(), 接着执行.bind(this),此处的this指向新生成的实例,所以赋值语句右侧的表达式计算完成后,会生成一个指定了this的新方法,接着执行赋值操作,将新生成的函数赋值给实例的increaseHandleClick属性,由对象的赋值机制可知,此处的increaseHandleClick会直接作为实例属性生成。简单地说:

把原型方法increaseHandleClick()改变为实例方法increaseHandleClick(),并且强制指定这个方法中的this指向当前的实例

接下来看另外一种绑定this的方法。该方法和上面的方法有点类似,不同的只是绑定this的位置不一样。另外他们主要区别是:

  • 如果组件中的一个事件只会被使用一次,则使用接下来绑定this的方式
  • 如果一件事件处理函数会多次绑定,则使用构造函数中绑定的方式

比如:

// 在`render`函数中通过`bind()`绑定`this`
class Counter extends React.Component {
    constructor(props) {
        super(props)
        this.state= {
            count: 0
        }
        // this.increaseHandleClick = this.increaseHandleClick.bind(this)
        // this.decrementHandleClick = this.decrementHandleClick.bind(this)
    }

    increaseHandleClick(e) {
        console.log(e)
        console.log(this.state.count)
    }

    decrementHandleClick(e) {
        console.log(e)
        console.log(this.state.count)
    }

    render() {
        return (
            <React.Fragment>
                <button onClick={this.increaseHandleClick.bind(this)}><span>+</span></button>
                <p>{ this.state.count }</p>
                <button onClick={this.decrementHandleClick.bind(this)}><span>-</span></button>
            </React.Fragment>
        )
    }
}

另外,还可以使用ES7的属性初始化语法(Property Initializers),就不需要在构造器中手动通过bind()绑定this

// 使用ES7的属性初始化语法(Property Initializers
class Counter extends React.Component {
    constructor(props) {
        super(props)
        this.state= {
            count: 0
        }
    }

    increaseHandleClick= (e) => {
        console.log(e)
        console.log(this.state.count)
    }

    decrementHandleClick = (e) => {
        console.log(e)
        console.log(this.state.count)
    }

    render() {
        return (
            <React.Fragment>
                <button onClick={this.increaseHandleClick}><span>+</span></button>
                <p>{ this.state.count }</p>
                <button onClick={this.decrementHandleClick}><span>-</span></button>
            </React.Fragment>
        )
    }
}

上面的方法和下面的写法是等效的:

// 使用ES6
class Counter extends React.Component {
    constructor(props) {
        super(props)
        this.state= {
            count: 0
        }
        this.increaseHandleClick = (e) => {
            console.log(e)
            console.log(this.state.count)
        }

        this.decrementHandleClick = (e) => {
            console.log(e)
            console.log(this.state.count)
        }
    }


    render() {
        return (
            <React.Fragment>
                <button onClick={this.increaseHandleClick}><span>+</span></button>
                <p>{ this.state.count }</p>
                <button onClick={this.decrementHandleClick}><span>-</span></button>
            </React.Fragment>
        )
    }
}

在ES6中,还可以运用箭头函数内部this不可变性,即 箭头函数内部的this永远指向箭头函数被定义时外部最近的this。简单地说,能够自动绑定定义此函数作用域的this。所以可以通过这个特性来绑定this

// 使用箭头函数
class Counter extends React.Component {
    constructor(props) {
        super(props)
        this.state= {
            count: 0
        }
        
    }

    increaseHandleClick = (e) => {
        console.log(e)
        console.log(this.state.count)
    }

    decrementHandleClick = (e) => {
        console.log(e)
        console.log(this.state.count)
    }

    render() {
        return (
            <React.Fragment>
                <button onClick={this.increaseHandleClick}><span>+</span></button>
                <p>{ this.state.count }</p>
                <button onClick={this.decrementHandleClick}><span>-</span></button>
            </React.Fragment>
        )
    }
}

也可以在元素内使用箭头函数:

// 使用箭头函数
class Counter extends React.Component {
    constructor(props) {
        super(props)
        this.state= {
            count: 0
        }
        
    }

    increaseHandleClick(e){
        console.log(e)
        console.log(this.state.count)
    }

    decrementHandleClick(e){
        console.log(e)
        console.log(this.state.count)
    }

    render() {
        return (
            <React.Fragment>
                <button onClick={(e)=>{this.increaseHandleClick(e)}}><span>+</span></button>
                <p>{ this.state.count }</p>
                <button onClick={(e)=>{this.decrementHandleClick(e)}}><span>-</span></button>
            </React.Fragment>
        )
    }
}

React事件处理函数为什么要绑定this

在React组件上绑定事件监听器,主要是为了响应用户的交互动作。特定的交互动作触发事件时,监听事件处理程序(事件函数)中往往都需要操作组件某个状态的值,进而对用户的点击行为提供相应的响应反馈。而对于开发者来说,这个事件处理程序被触发的时候,就需要能够拿到这个组件专属的状态合集。

比如上面计算数器组件Counter,有两个按钮(+-按钮),当用户点击按钮时分别会触发increaseHandleClick()decrementHandleClick()事件函数,事件函数会将组件内部状态属性this.state.count的值进行 +1 (this.state.count+1)-1(this.state.count-1),因此在组件中强制绑定监听器函数的this指向当前实例。

class Counter extends React.Component {
    constructor(props) {
        super(props)
        this.state= {
            count: 0
        }
        this.increaseHandleClick = this.increaseHandleClick.bind(this)
        this.decrementHandleClick = this.decrementHandleClick.bind(this)
    }

    increaseHandleClick(e){
        this.setState({
            count: this.state.count + 1
        })
    }

    decrementHandleClick(e){
        this.setState({
            count: this.state.count - 1
        })
    }

    render() {
        return (
            <React.Fragment>
                <button onClick={this.increaseHandleClick}><span>+</span></button>
                <p>{ this.state.count }</p>
                <button onClick={this.decrementHandleClick}><span>-</span></button>
            </React.Fragment>
        )
    }
}

如果类组件中,没有绑定this的指向,我们想要访问count,我们可以尝试着在this.increaseHandleClick()(或this.decrementHandleClick())方法中这样做:

class Counter extends React.Component {
    state = {
        count: 0
    }

    increaseHandleClick(e) {
        console.log(e)
    }

    decrementHandleClick(e) {
        console.log(e)
    }

    render() {
        return (
            <React.Fragment>
                <button onClick={this.increaseHandleClick}><span>+</span></button>
                <p>{ this.state.count }</p>
                <button onClick={this.decrementHandleClick}><span>-</span></button>
            </React.Fragment>
        )
    }
}

当用户点击+(或-)按钮时,会看到一个错误信息:

在解决这个问题,需要知道React中如何绑定事件。我们知道,在JavaScript中this的更改取决于函数或方法的调用方式。而在React组件中,我们需要显式的绑定this(在React中有不同的方式可以显式绑定this)。比如说使用constructor()super()

class Counter extends React.Component {
    constructor(){
        super()
        this.increaseHandleClick = this.increaseHandleClick.bind(this)
        this.decrementHandleClick = this.decrementHandleClick.bind(this)
    }
    state = {
        count: 0
    }

    increaseHandleClick(e) {
        console.log(this.state.count)
    }

    decrementHandleClick(e) {
        console.log(this.state.count)
    }

    render() {
        return (
            <React.Fragment>
                <button onClick={this.increaseHandleClick}><span>+</span></button>
                <p>{ this.state.count }</p>
                <button onClick={this.decrementHandleClick}><span>-</span></button>
            </React.Fragment>
        )
    }
}

如果在构造器中初始化了state这个属性,那么原型方法执行时,this.state会直接获取实例的state属性;如果构造器中没有初始化state这个属性,说明组件没有自身状态。就算是调用原型方法也似乎没有什么影响。而我们显式的绑定this就是为了提前规避this指针丢失的问题

React事件流

事件流指从页面中接收事件的顺序,也可以理解为事件在页面中传播的顺序。

在DOM中事件流包括三个阶段:事件捕获处于目标阶段事件冒泡。W3C规范中有一张图形象的描述了Web页面中的事件流:

在React中也有事件流的概念,不过React的事件流其默认的传播方式是冒泡。比如说,我们有一个这样的组件:

class App  extends React.Component {
    constructor(props) {
        super(props)
        this.grandpaFn = this.grandpaFn.bind(this)
        this.parentFn = this.parentFn.bind(this)
        this.childrenFn = this.childrenFn.bind(this)
    }

    grandpaFn(e) {
        console.log('Grandpa Element')
    }

    parentFn(e) {
        console.log('Parent Element')
    }

    childrenFn(e){
        console.log('Children Element')
    }

    render() {
        return (
            <div className="grandpa" onClick={this.grandpaFn}>
                <div className="parent" onClick={this.parentFn}>
                    <div className="children" onClick={this.childrenFn}></div>
                </div>
            </div>
        )
    }
}

这是一个很简单的示例,就是三个div相互嵌套,即div.grandpa > div.parent > div.children。如果我们用DOM树来表达的话,大致像下面这样的(略去一些其他标签,比如headtitle等):

在这三个div中都绑定了click事件,分别绑定了grandpaFn()parentFn()childrenFn()

render() {
    return (
        <div className="grandpa" onClick={this.grandpaFn}>
            <div className="parent" onClick={this.parentFn}>
                <div className="children" onClick={this.childrenFn}></div>
            </div>
        </div>
    )
}

当用户点击div.children时(也就是示例中黑色圆圈区域),在浏览器的控制台中先后会去输出如下图所示的值:

前面提到过,在React的事件处理系统中,默认的事件流就是冒泡。如果说我们希望以捕获的方式来触发事件的话,可以使用onClickCapture来绑定事件,也就是在事件类型后面加一个后缀**Capture**:

render() {
    return (
        <div className="grandpa" onClickCapture={this.grandpaFn}>
          <div className="parent" onClickCapture={this.parentFn}>
            <div className="children" onClickCapture={this.childrenFn}></div>
          </div>
        </div>
    )
}

这个时候当用户点击示例中黑色圆圈区域的任何一点,

React的合成事件

React的事件和JavaScript事件有所不同,它是合成事件(Synethic event)。为了解决跨浏览器兼容性问题,React将浏览器原生事件(Browser Native Event)封装成合成事件(SyntheticEvent传入设置的事件处事理器中。事件合成,即是事件自定义。事件合成除了抹平浏览器之间的差异之外,还可以用来自定义高级事件,比如React的onChange事件,它为表单元素定义了统一的值变动事件。另外第三方也可以通过React的事件插件机制来合成自定义事件。

在React中,其合成事件执行的过程大致如下图所示:

大致会经过下面三个主要过程:

  • 事件注册:所有事件都会冒泡(注册)到document上,而且拥有统一的回调函数dispatchEvent来执行事件分发
  • 事件合成:从原生的nativeEvent对象生成合成事件对象,同一种事件类型只能生成一个合成事件,比如onClick,所有通过JSX绑定的onClick的回调函数都会按顺序(冒泡或捕获)会放到Event._dispatchListeners数组中,然后依次执行它
  • 事件派发:每次触发事件都会执行根节点上addEventListener注册的回调,也就是ReactEventListener.dispatchEvent方法,事件分发入口函数

在React底层,主要对合成事件做了两件事:事件委托自动绑定

前面也提到过,在React中,并不会把事件处理程序(事件函数)直接绑定到真实的DOM节点上,而是把所有事件绑定到结构的最外层,使用一个统一的事件监听器,这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升。

另外一点是,在React组件中,每个事件处理程序的this都会会指向该组件的实例,即自动绑定this为当前组件,有关于React事件中的this更详细的介绍,我们前面有做过介绍,这里不再做阐述。

如果拿React的合成事件与原生事件对比的话,他们之间还是有一定的区别:

  • 写法上的差异,React合成事件是驼峰写法,比如onClick,而原生事件是全小写,比如onclick
  • 执行的时机不同,React合成事件全部委托到document上,而原生事件绑定到DOM元素自身
  • 类型不同,React合成事件可以是任何类型,比如this.handleClick函数,而原生事件中只能是字符串

有了这个认识之后,我们再来看React中的事件委托,这样会更好理解一点。

React事件委托

React事件事实上是合成事件,在合成事件系统中,所有事件都是绑定在document上(冒泡到documnet)。React合成事件的冒泡并不是真的冒泡,而是节点的遍历。也就是说,我们在某个React元素上绑定了事件,并不会直接绑定到真实的DOM节点上,而是把所有事件绑定到结构的最外层,即document,这个时候document被称为统一的事件监听器,即,最后事件都会委托给document统一触发。

在JavaScript中,使用e.stopPropagation()来阻止捕获和冒泡阶段中当前事件的进一步传播;使用e.preventDefault()可以取消默认事件:

  • e.stopPropagation()e.stopPropagation()也是事件对象(Event)的一个方法,作用是阻止目标元素的冒泡事件,但是不会阻止默认行为
  • e.preventDefault()e.preventDefault()是事件对象(Event)的一个方法,作用是取消目标元素的默认行为。如果元素自身没有默认行为,调用该方法时就无效

另外,在JavaScript中还可以使用return false来阻止目标元素的默认行为。

在React中,React的合成事件(SyntheticEvent)和浏览器原生事件一样,也有e.stopPropagation()e.preventDefault(),不同的是在React中做了浏览器之间差异的抹平。

比如在上面的示例中,在div.children元素绑定的事件childrenFn()上使用e.stopPropagation()方法来阻止事件流的传播:

class App  extends React.Component {
    constructor(props) {
        super(props)
        this.grandpaFn = this.grandpaFn.bind(this)
        this.parentFn = this.parentFn.bind(this)
        this.childrenFn = this.childrenFn.bind(this)
    }

    grandpaFn(e) {
        console.log('Grandpa Element')
    }

    parentFn(e) {
        console.log('Parent Element')
    }

    childrenFn(e){
        console.log('Children Element')
        e.stopPropagation()
    }

    render() {
        return (
            <div className="grandpa" onClick={this.grandpaFn}>
                <div className="parent" onClick={this.parentFn}>
                    <div className="children" onClick={this.childrenFn}></div>
                </div>
            </div>
        )
    }
}

这个时候用户点击黑色区域(即div.children),在浏览器控制台上只会输出Children Element

这说明已经成功阻止了冒泡。其执行过程如下:

从上图我们可以看到,React阻止的事件流,并没有阻止真正DOM元素的事件触发,当用户点击黑色区域(div.children)时,真正的元素还是按照冒泡的方式,层层将事件交给上级元素进行处理,最后事件传播到document,触发合成事件,在合成事件中,div.children触发时,e.stopPropagation()被调用,合成事件中的事件被终止。因此,合成事件中的e.stopPropagation()无法阻止事件在真正元素上的传递,它只阻止合成事件中的事件流。相反,如果我们在div.children上绑定一个真正的事件,那么合成事件则会被终止。

回过头来看React合成事件执行的流程图:

从图中我们可以得知:

  • React元素上的事件冒泡到document上才会触发React的合成事件,所以React合成事件对象的e.stopPropagation()只能阻止React模拟的事件冒泡,并不能阻止真实的DOM事件冒泡
  • DOM事件的阻止冒泡也可以阻止合成事件原因是DOM事件的阻止冒泡使事件不会传播到document
  • 当合成事件和DOM事件都绑定在document上的时候,React的处理是合成事件顺序应该是先放进去的会先触发,在这种情况下,原生事件对象的stopImmediatePropagation能做到阻止进一步触发document的DOM事件

事实上,在React事件中,阻止事件冒泡有三种情况。

使用e.stopPropagation()阻止React合成事件间的冒泡

class App  extends React.Component {
    constructor(props) {
        super(props)
        this.grandpaFn = this.grandpaFn.bind(this)
        this.parentFn = this.parentFn.bind(this)
        this.childrenFn = this.childrenFn.bind(this)
    }

    grandpaFn(e) {
        console.log('Grandpa Element')
    }

    parentFn(e) {
        console.log('Parent Element')
    }

    childrenFn(e){
        // 阻止React合成事件间的冒泡
        e.stopPropagation()
        console.log('Children Element')
    }

    render() {
        return (
            <div className="grandpa" onClick={this.grandpaFn}>
                <div className="parent" onClick={this.parentFn}>
                    <div className="children" onClick={this.childrenFn}></div>
                </div>
            </div>
        )
    }
}

使用e.nativeEvent.stopImmediatePropagation()阻止React合成事件与最外层document上的事件间的冒泡

class App  extends React.Component {
    constructor(props) {
        super(props)
        this.grandpaFn = this.grandpaFn.bind(this)
        this.parentFn = this.parentFn.bind(this)
        this.childrenFn = this.childrenFn.bind(this)
    }

    grandpaFn(e) {
        console.log('Grandpa Element')
    }

    parentFn(e) {
        console.log('Parent Element')
    }

    childrenFn(e){
        // 阻止React合成事件与最外层document上的事件间的冒泡
        // e.nativeEvent.stopImmediatePropagation();
        console.log('Children Element')
    }

    componentDidMount() {
        document.addEventListener('click', () => {
            console.log('Docuemnt');
        })
    }

    render() {
        return (
            <div className="grandpa" onClick={this.grandpaFn}>
                <div className="parent" onClick={this.parentFn}>
                    <div className="children" onClick={this.childrenFn}></div>
                </div>
            </div>
        )
    }
}

childrenFn()中如果没有添加e.nativeEvent.stopImmediatePropagation(),用户点击黑色区域(div.children)时,事件冒泡过程会像下图这样:

如果在childrenFn()中显式添加e.nativeEvent.stopImmediatePropagation(),那么会阻止React合成事件与最外层document上的事件间的冒泡:

childrenFn(e){
    // 阻止React合成事件与最外层document上的事件间的冒泡
    // e.nativeEvent.stopImmediatePropagation();
    console.log('Children Element')
}

使用e.target来阻止React合成事件与除最外层document上的原生事件上的冒泡

class App  extends React.Component {
    constructor(props) {
        super(props)
        this.grandpaFn = this.grandpaFn.bind(this)
        this.parentFn = this.parentFn.bind(this)
        this.childrenFn = this.childrenFn.bind(this)
    }

    grandpaFn(e) {
        console.log('Grandpa Element')
    }

    parentFn(e) {
        console.log('Parent Element')
    }

    childrenFn(e){
        console.log('Children Element')
    }

    componentDidMount() {
        document.body.addEventListener('click',e => {
            // 通过e.target判断阻止冒泡
            if(e.target&&e.target.matches('div.children')){
                console.log(e.target)
            }

            console.log('Body Element');
        })
    }

    render() {
        return (
            <div className="grandpa" onClick={this.grandpaFn}>
                <div className="parent" onClick={this.parentFn}>
                    <div ref="update" className="children" onClick={this.childrenFn}></div>
                </div>
            </div>
        )
    }
}

React中事件对象

在React中,同样可以获取到事件发生时的事件对象。比如我们在上面的示例中,childrenFn()中输出事件对象:

childrenFn(e){
    console.log(e)
}

当用户点击黑色区域(即div.children),控制台会输出事件对象,如下图所示:

在React中,React合成事件中的事件对象,并不是原生的事件对象。只是我们同样可以通过它获取原生事件对象上的某些属性,比如说鼠标点击时的xy的坐标(即clientXclientY)。比如下面的示例:

childrenFn(e){
    console.log('Children Element')
    console.log(`鼠标点击时坐标值:(${e.clientX}, ${e.clientY})`)
}

用户点击黑色区域(记得仅是黑色区域,因为示例中的childrenFn()绑定在div.children上),点击不同位置会输出不同的坐标值:

而且在React中,事件对象在React合成事件中,只有一个,并且被全局共享。也就是说,当这次事件调用完成之后,这个事件对象会被清空,等待下一次的事件触发,这样一来,就无法在异步操作中获得React合成事件中的事件对象。比如,下面这个示例:

childrenFn(e){
    console.log('Children Element')
    setTimeout(()=>{
        console.log(`1s之后获得鼠标点击时坐标值:(${e.clientX}, ${e.clientY})`)
    },1000)
}

1s之后输出的e.clientXe.clientY的值都是null。我们在上面的基础上再调整一下示例代码:

childrenFn(e){
    console.log('Children Element')
    const {clientX, clientY} = e
    setTimeout(()=>{
        this.setState({
            x: clientX,
            y: clientY
        })
        console.log(`1s之后获得鼠标点击时坐标值:(${this.state.x}, ${this.state.y})`)
    },1000)
}

React事件中使用原生事件

在React中一般不建议我们将React合成事件和JavaScript的原生事件混合在一起使用,这样容易引起混淆。但是在某些需求中,我们又不得不在React的合成事件中使用原生事件。

在React事件中使用原生事件,会用到React的生命周期。这是因为原生事件绑定在真实DOM上,所以一般会在生命周期的componentDidMount中或ref回调函数中进行绑定操作,在componentWillUnmount中进行解绑操作。另外合成事件和DOM事件混合使用,其触发事件是有一定的顺序:

  • 先执行原生事件,事件冒泡至document再执行合成事件
  • 如果父子元素,触发顺序为:子元素原生事件 ~> 父元素原生事件 ~> 子元素合成事件 ~> 父元素合成事件

由于这部分涉及到React生命周期相关的知识,所以这里不做过多的探讨,待学习完React生命周期相关的知识之后,再回过头来学习这部分知识以及React组件生命周期事件。如果感兴趣的话,欢迎关注后续的相关更新。

小结

在这篇文章中,我们以JavaScript事件入手和大家一起探讨和学习了React中事件相关的基础知识,比如,如何在React中创建事件怎么处理事件、React事件中this的绑定以及React中的事件流事件委托事件对象相关的知识。事实上,这些只是React事件的基础知识,在React事件系统中还包括其他的相关知识,比如事件管理,生命周期事件,如何使用回调处理事件等。我们将在下一节中和大家一起探讨这方面的相关知识。如果你感兴趣的话,欢迎关注后续的相关更新。如果文章中有不对之处,欢迎路过大神拍正。