【浅入合成事件】从源码角度寻找为什么React 类组件需要bind this

888 阅读4分钟

「这是我参与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都在严格模式下执行。

MDN上查看具体说明

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 thisrender会多次执行bind🌟
render中箭头函数render会多次生成新的函数🌟🌟
constructor中bind this繁琐,有时候不一定需要constructor🌟🌟🌟
定义类方法时箭头函数推荐🌟🌟🌟🌟

React中是如何调用事件回调的

上面解释了为什么在Javascript的调用过程中this会丢失掉,那么是否在JSX到绑定事件执行也是因为这样的调用才丢失掉this的?

从JSX到fiber

我们梳理下JSX到事件监听,到事件函数被调用的过程,看testThis是如何从JSX中绑定到被执行

  1. 首先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
    })
  }
}
  1. React.createElement -> 转为react.element对象

  2. 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)
  • 最后生成dispatchQueueprocessDispatchQueue调用执行

processDispatchQueue会配合processDispatchQueueItemsInOrder模拟冒泡或捕捉执行listener, 最后都是通过invokeGuardedCallback最终调用callCallback执行,但是由于invokeGuardedCallback未传contextcallCallback,所以导致最终func.apply(context, funcArgs)undefined

image.png

思考

回顾下查看源码过程,其实在插件事件系统(dispatchEventForPluginEventSystem)的时候是可以拿的到组件的实例的, 如果在这里保存下实例,继续往下传递是不是就可以解决执行callCallbackapply contextundefined的问题呢?

总结

React执行事件回调总结: 在触发onClick时,会执行React在根元素的监听,从而调用插件事件系统开始根据不同事件去fiber上收集对应的props事件回调将其放入listeners数组中,组成合成事件队列dispatchQueue,再模拟冒泡去执行listeners中的事件回调。

this丢失总结: 在收集listenertestThis事件作为回调被转存一次,导致this丢失,又在apply执行listener时候绑定的是undefined最终导致testThis中的thisundefined