本文是『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
对象建立了一个合成事件对象。它能解决什么问题呢?
通常来讲,当元素被卸载,元素绑定的事件监听器也要清除。要否则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;
复制代码
鬼知道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的花样。
简单粗暴对吧。再怎么狸猫换太子,我都绑的死死的。
不过呢,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专题一览