「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战」
在React类组件在JSX中使用函数时,通常需要将函数bind到当前this,我们知道这其实不是React的问题,而是在Javascript中默认绑定的原因。但是除了在JavaScript中如何调用函数,我更想知道在Reac事件系统中, JSX的绑定事件时如何被执行的,为什么React没有帮助使用者自动绑定组件实例。
问题回顾
在JSX绑定事件 -> 到事件触发,因为作为回调函数保存调用,导致退回默认绑定,this丢失
class TestComponent extends React.Component {
test() {
console.log(this) // undefined
}
render() {
return (
<button onClick={this.test}>test this</button>
)
}
}
分析原因
在JavaScript中的隐式绑定
普通函数调用
普通函数调用,在非严格模式下函数的this默认指向window对象,严格模式执行undefined
function testThis() {
console.log(this)
}
testThis() // window
"use strict";
function testThis() {
console.log(this)
}
testThis() // undefined
类方法调用
注意: 而类声明也是以严格模式执行的,包括构造函数,静态方法,原型方法,getter和setter都在严格模式下执行。
class TestThis {
name = 'testName'
test() {
console.log(this.name)
}
}
let test = new TestThis()
test.test() // testName
那么在JSX中是如何丢失掉this的?下面代码解释了this为何丢失掉
class TestThis {
name = 'testName'
test() {
console.log(this)
}
}
let testInstance = new TestThis()
let testFn = testInstance.test
testFn() // undefined
如何解决
| 解决方法 | 推荐指数 | 推荐指数 |
|---|---|---|
| render中bind this | render会多次执行bind | 🌟 |
| render中箭头函数 | render会多次生成新的函数 | 🌟🌟 |
| constructor中bind this | 繁琐,有时候不一定需要constructor | 🌟🌟🌟 |
| 定义类方法时箭头函数 | 推荐 | 🌟🌟🌟🌟 |
React中是如何调用事件回调的
上面解释了为什么在
Javascript的调用过程中this会丢失掉,那么是否在JSX到绑定事件执行也是因为这样的调用才丢失掉this的?
从JSX到fiber
我们梳理下JSX到事件监听,到事件函数被调用的过程,看
testThis是如何从JSX中绑定到被执行
- 首先
JSX-> 通过babel转为React.creatElement
class TestComponent extends Component {
testThis() {
console.log(this)
}
render() {
return (
<button onClick={this.testThis}>test this</button>
)
}
}
class Component {
testThis() {
console.log(this);
}
render() {
return React.createElement("button", {
onClick: this.testThis
})
}
}
-
React.createElement -> 转为react.element对象
-
react.element对象 -> Fiber对象
从触发到执行事件回调
React在会在首次completeWork阶段根据fiber创建真实DOM并将DOM上需要注册的事件统一注册到根元素上面。
上面我们知道我们声明的事件函数testThis在fiber上,接着我们来分析下点击按钮时React是如何冒泡执行testThis函数的。
首先我们点击按钮,肯定是执行上面绑定的listener, listener就是dispatchDiscreteEvent函数,用来处理离散事件,click这种的就属于离散事件,但是不管怎么样的事件最终都会调用dispatchEvent, dispatchEvent是用来分发事件的。
那么执行dispatchEvent后主要做了什么事情:
调用attemptToDispatchEvent
- 根据原生事件对象 nativeEvent 找到真实的触发dom 元素
export function attemptToDispatchEvent(
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
targetContainer: EventTarget,
nativeEvent: AnyNativeEvent,
) {
// 根据原生事件对象 nativeEvent 找到真实的触发dom 元素
const nativeEventTarget = getEventTarget(nativeEvent);
// 获取触发对应的fiber
// react生成真实dom时会生成随机指针指向对应fiberNode, 所以可以通过dom找到fiber
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
// 忽略部分代码...
// 最后调用事件处理插件事件系统
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer,
);
return null;
}
dispatchEventForPluginEventSystem会调用batchedUpdates批量执行dispatchEventsForPlugins, dispatchEventsForPlugins最终又调用extractEvents
extractevents主要做了什么事情
- 根据不同类型事件创建合成事件
- 收集各层级符合事件的listener(
fiber身上的testThis) - 最后生成
dispatchQueue给processDispatchQueue调用执行
processDispatchQueue会配合processDispatchQueueItemsInOrder模拟冒泡或捕捉执行listener, 最后都是通过invokeGuardedCallback最终调用callCallback执行,但是由于invokeGuardedCallback未传context给callCallback,所以导致最终func.apply(context, funcArgs)为undefined
思考
回顾下查看源码过程,其实在插件事件系统(dispatchEventForPluginEventSystem)的时候是可以拿的到组件的实例的, 如果在这里保存下实例,继续往下传递是不是就可以解决执行callCallback时apply context为undefined的问题呢?
总结
React执行事件回调总结: 在触发onClick时,会执行React在根元素的监听,从而调用插件事件系统开始根据不同事件去fiber上收集对应的props事件回调将其放入listeners数组中,组成合成事件队列dispatchQueue,再模拟冒泡去执行listeners中的事件回调。
this丢失总结: 在收集listener时testThis事件作为回调被转存一次,导致this丢失,又在apply执行listener时候绑定的是undefined最终导致testThis中的this为undefined