资讯专栏INFORMATION COLUMN

结合源码彻底理解 react事件机制原理 04 - 事件执行

marser / 1470人阅读

摘要:文章涉及到的源码是基于版本,虽然不是最新版本但是也不会影响我们对事件机制的整体把握和理解。总结本文主要是从整体流程上介绍了下事件触发的过程。

前言

这是 react 事件机制的第四节-事件执行,一起研究下在这个过程中主要经过了哪些关键步骤,本文也是react 事件机制的完结篇,希望本文可以让你对 react 事件执行的原理有一定的理解。

文章涉及到的源码是基于 react15.6.1版本,虽然不是最新版本但是也不会影响我们对 react 事件机制的整体把握和理解。

回顾

先简单的回顾下上一文,事件注册的结果是是把所有的事件回调保存到了一个对象中

那么在事件触发的过程中上面这个对象有什么用处呢?

其实就是用来查找事件回调。

内容大纲

按照我的理解,事件触发过程总结为主要下面几个步骤

1.进入统一的事件分发函数(dispatchEvent)

2.结合原生事件找到当前节点对应的ReactDOMComponent对象

3.进行事件的合成

3.1根据当前事件类型生成指定的合成对象

3.2封装原生事件和冒泡机制

3.3查找当前节点以及他的所有父级

3.4在listenerBank查找事件回调并合成到 event(合成事件结束)

4.批量处理合成事件内的回调事件(事件触发完成 end)

说再多不如配个图

举个栗子

在说具体的流程前,先看一个栗子,后面的分析也是基于这个栗子

handleFatherClick=(e)=>{
        console.log("father click");
    }

    handleChildClick=(e)=>{
        console.log("child click");
    }

    render(){
        return 
father
child
}

看到这个熟悉的代码,我们就已经知道了执行结果。

当我点击 child div 的时候,会同时触发father的事件。

1、进入统一的事件分发函数 (dispatchEvent)

当我点击child div 的时候,这个时候浏览器会捕获到这个事件,然后经过冒泡,事件被冒泡到 document 上,交给统一事件处理函数 dispatchEvent 进行处理。(上一文中我们已经说过 document 上已经注册了一个统一的事件处理函数 dispatchEvent)

2、结合原生事件找到当前节点对应的ReactDOMComponent对象

在原生事件对象内已经保留了对应的ReactDOMComponent实例,应该是在挂载阶段就已经保存了

看下ReactDOMComponent实例的内容

3、开始进行事件合成

事件的合成,冒泡的处理以及事件回调的查找都是在合成阶段完成的。

3.1 根据当前事件类型找到对应的合成类,然后进行合成对象的生成

//进行事件合成,根据事件类型获得指定的合成类
var SimpleEventPlugin = {
    eventTypes: eventTypes,
    extractEvents: function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
        var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
        //代码已省略....
        var EventConstructor;

        switch (topLevelType) {
            //代码已省略....
            case "topClick"://【这里有一个不解的地方】 topLevelType = topClick,执行到这里了,但是这里没有做任何操作
                if (nativeEvent.button === 2) {
                    return null;
                }
            //代码已省略....
            case "topContextMenu"://而是会执行到这里,获取到鼠标合成类
                EventConstructor = SyntheticMouseEvent;
                break;


            case "topAnimationEnd":
            case "topAnimationIteration":
            case "topAnimationStart":
                EventConstructor = SyntheticAnimationEvent;//动画类合成事件
                break;

            case "topWheel":
                EventConstructor = SyntheticWheelEvent;//鼠标滚轮类合成事件
                break;

            case "topCopy":
            case "topCut":
            case "topPaste":
                EventConstructor = SyntheticClipboardEvent;
                break;
        }

        var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
        EventPropagators.accumulateTwoPhaseDispatches(event);
        return event;//最终会返回合成的事件对象
    }

3.2 封装原生事件和冒泡机制

在这一步会把原生事件对象挂到合成对象的自身,同时增加事件的默认行为处理和冒泡机制

/**
 * 
 * @param {obj} dispatchConfig 一个配置对象 包含当前的事件依赖 ["topClick"],冒泡和捕获事件对应的名称 bubbled: "onClick",captured: "onClickCapture"
 * @param {obj} targetInst 组件实例ReactDomComponent
 * @param {obj} nativeEvent 原生事件对象
 * @param {obj} nativeEventTarget  事件源 e.target = div.child
 */
function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) {

    this.dispatchConfig = dispatchConfig;
    this._targetInst = targetInst;
    this.nativeEvent = nativeEvent;//将原生对象保存到 this.nativeEvent
    //此处代码略.....
    var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;

    //处理事件的默认行为
    if (defaultPrevented) {
        this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
    } else {
        this.isDefaultPrevented = emptyFunction.thatReturnsFalse;
    }


    //处理事件冒泡 ,thatReturnsFalse 默认返回 false,就是不阻止冒泡
    this.isPropagationStopped = emptyFunction.thatReturnsFalse;
    return this;
}

下面是增加的默认行为和冒泡机制的处理方法,其实就是改变了当前合成对象的属性值, 调用了方法后属性值为 true,就会阻止默认行为或者冒泡。

来看下代码

//在合成类原型上增加preventDefault和stopPropagation方法
_assign(SyntheticEvent.prototype, {
    preventDefault: function preventDefault() {
        // ....略

        this.isDefaultPrevented = emptyFunction.thatReturnsTrue;
    },
    stopPropagation: function stopPropagation() {
        //....略

        this.isPropagationStopped = emptyFunction.thatReturnsTrue;
    }
);

看下 emptyFunction 代码就明白了

3.3 根据当前节点实例查找他的所有父级实例存入path

/**
 * 
 * @param {obj} inst 当前节点实例
 * @param {function} fn 处理方法
 * @param {obj} arg 合成事件对象
 */
function traverseTwoPhase(inst, fn, arg) {
    var path = [];//存放所有实例 ReactDOMComponent

    while (inst) {
        path.push(inst);
        inst = inst._hostParent;//层级关系
    }

    var i;

    for (i = path.length; i-- > 0;) {
        fn(path[i], "captured", arg);//处理捕获 ,反向处理数组
    }

    for (i = 0; i < path.length; i++) {
        fn(path[i], "bubbled", arg);//处理冒泡,从0开始处理,我们直接看冒泡
    }
}

看下 path 长啥样

3.4 在listenerBank查找事件回调并合成到 event(事件合成结束)

紧接着上面代码

 fn(path[i], "bubbled", arg);

上面的代码会调用下面这个方法,在listenerBank中查找到事件回调,并存入合成事件对象。

/**EventPropagators.js
 * 查找事件回调后,把实例和回调保存到合成对象内
 * @param {obj} inst 组件实例
 * @param {string} phase 事件类型
 * @param {obj} event 合成事件对象
 */
function accumulateDirectionalDispatches(inst, phase, event) {
    var listener = listenerAtPhase(inst, event, phase);
    if (listener) {//如果找到了事件回调,则保存起来 (保存在了合成事件对象内)
        event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);//把事件回调进行合并返回一个新数组
        event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);//把组件实例进行合并返回一个新数组
    }
}

/**
 * EventPropagators.js
 * 中间调用方法 拿到实例的回调方法
 * @param {obj} inst  实例
 * @param {obj} event 合成事件对象
 * @param {string} propagationPhase 名称,捕获capture还是冒泡bubbled
 */
function listenerAtPhase(inst, event, propagationPhase) {
    var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
    return getListener(inst, registrationName);
}

/**EventPluginHub.js
 * 拿到实例的回调方法
 * @param {obj} inst 组件实例
 * @param {string} registrationName Name of listener (e.g. `onClick`).
 * @return {?function} 返回回调方法
 */
getListener: function getListener(inst, registrationName) {
    var bankForRegistrationName = listenerBank[registrationName];

    if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
        return null;
    }

    var key = getDictionaryKey(inst);
    return bankForRegistrationName && bankForRegistrationName[key];
}

这里要高亮一下

为什么能够查找到的呢?
因为 inst (组件实例)里有_rootNodeID,所以也就有了对应关系


到这里事件合成对象生成完成,所有的事件回调已保存到了合成对象中。

4、 批量处理合成事件对象内的回调方法(事件触发完成 end)

第3步生成完 合成事件对象后,调用栈回到了我们起初执行的方法内

//在这里执行事件的回调
runEventQueueInBatch(events);

到下面这一步中间省略了一些代码,只贴出主要的代码,

下面方法会循环处理 合成事件内的回调方法,同时判断是否禁止事件冒泡。

贴上最后的执行回调方法的代码

/**
 * 
 * @param {obj} event 合成事件对象
 * @param {boolean} simulated false
 * @param {fn} listener 事件回调
 * @param {obj} inst 组件实例
 */
function executeDispatch(event, simulated, listener, inst) {
    var type = event.type || "unknown-event";
    event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);

    if (simulated) {//调试环境的值为 false,按说生产环境是 true 
        //方法的内容请往下看
        ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
    } else {
        //方法的内容请往下看
        ReactErrorUtils.invokeGuardedCallback(type, listener, event);
    }

    event.currentTarget = null;
}

/** ReactErrorUtils.js
 * @param {String} name of the guard to use for logging or debugging
 * @param {Function} func The function to invoke
 * @param {*} a First argument
 * @param {*} b Second argument
 */
var caughtError = null;
function invokeGuardedCallback(name, func, a) {
    try {
        func(a);//直接执行回调方法
    } catch (x) {
        if (caughtError === null) {
            caughtError = x;
        }
    }
}

var ReactErrorUtils = {
    invokeGuardedCallback: invokeGuardedCallback,
    invokeGuardedCallbackWithCatch: invokeGuardedCallback,
    rethrowCaughtError: function rethrowCaughtError() {
        if (caughtError) {
            var error = caughtError;
            caughtError = null;
            throw error;
        }
    }
};

if (process.env.NODE_ENV !== "production") {//非生产环境会通过自定义事件去触发回调
    if (typeof window !== "undefined" && typeof window.dispatchEvent === "function" && typeof document !== "undefined" && typeof document.createEvent === "function") {
        var fakeNode = document.createElement("react");

        ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
            var boundFunc = func.bind(null, a);
            var evtType = "react-" + name;
            fakeNode.addEventListener(evtType, boundFunc, false);
            var evt = document.createEvent("Event");
            evt.initEvent(evtType, false, false);
            fakeNode.dispatchEvent(evt);
            fakeNode.removeEventListener(evtType, boundFunc, false);
        };
    }
}

最后react 通过生成了一个临时节点fakeNode,然后为这个临时元素绑定事件处理程序,然后创建自定义事件 Event,通过fakeNode.dispatchEvent方法来触发事件,并且触发完毕之后立即移除监听事件。

到这里事件回调已经执行完成,但是也有些疑问,为什么在非生产环境需要通过自定义事件来执行回调方法。可以看下上面的代码在非生产环境对ReactErrorUtils.invokeGuardedCallback 方法进行了重写。

5、总结

本文主要是从整体流程上介绍了下 react 事件触发的过程。

主要流程有:

进入统一的事件分发函数(dispatchEvent)

结合原生事件找到当前节点对应的ReactDOMComponent对象

进行事件的合成

3.1 根据当前事件类型生成指定的合成对象

3.2 封装原生事件和冒泡机制

3.3 查找当前节点以及他的所有父级

3.4 在listenerBank查找事件回调并合成到 event(事件合成结束)

4.批量处理合成事件内的回调事件(事件触发完成 end)

其中并没有深入到源码的细节,包括事务处理、合成的细节等,另外梳理过程中自己也有一些疑惑的地方,对源码有兴趣的小伙儿可以深入研究下,当然还是希望本文能够带给你一些启发,若文章有表述不清或有问题的地方欢迎留言交流。

更多精彩内容欢迎关注我的公众号 - 前端张大胖

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/104429.html

相关文章

  • 结合源码彻底理解 react事件机制原理 01 - 对事件机制的初步理解和验证

    摘要:前言这是事件机制的第一篇,主要内容有表象理解,验证,意义和思考。因为合成事件的触发是基于浏览器的事件机制来实现的,通过冒泡机制冒泡到最顶层元素,然后再由统一去处理。合成事件的阻止冒泡不会影响原生事件。 showImg(https://segmentfault.com/img/bVbtvP2?w=800&h=420); 前言 这是 react 事件机制的第一篇,主要内容有:表象理解,验证...

    muddyway 评论0 收藏0
  • 结合源码彻底理解 react事件机制原理 03 - 事件注册

    摘要:文章涉及到的源码是基于版本,虽然不是最新版本但是也不会影响我们对事件机制的整体把握和理解。到这里事件注册就完事儿了。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 这是 react 事件机制的第三节 - 事件注册,通过本文你将了解react 事件的注册过程,以及在这个过程中主要经过了哪些关键步骤,同时结合源...

    chaosx110 评论0 收藏0
  • 结合源码彻底理解 react事件机制原理 02 - 对于合成的理解

    摘要:前言这是事件机制系列文章的第二篇对于合成的理解,咱们就来说说合成这个名词。在给注册事件的时候也是对兼容性做了处理。总结以上就是我对于合成这个名词的理解,其实内部还处理了很多,我只是略微简单的举了几个栗子。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 这是react事件机制系列文章的第二篇-对于合成的理解,...

    nihao 评论0 收藏0
  • 前端小册 - 结合源码彻底理解 react 事件机制

    摘要:对事件机制的初步理解和验证对于合成的理解事件注册机制事件执行本文基于进行分析,虽然不是最新版本但是也不会影响我们对事件机制的整体把握和理解。最后希望通过本文可以让你对事件机制有更清晰的认识和理解。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 写这个文章也算是实现19年的一个 flag,研究一个知识点并且把...

    YJNldm 评论0 收藏0
  • 优秀文章收藏(慢慢消化)持续更新~

    摘要:整理收藏一些优秀的文章及大佬博客留着慢慢学习原文协作规范中文技术文档协作规范阮一峰编程风格凹凸实验室前端代码规范风格指南这一次,彻底弄懂执行机制一次弄懂彻底解决此类面试问题浏览器与的事件循环有何区别笔试题事件循环机制异步编程理解的异步 better-learning 整理收藏一些优秀的文章及大佬博客留着慢慢学习 原文:https://www.ahwgs.cn/youxiuwenzhan...

    JeOam 评论0 收藏0

发表评论

0条评论

marser

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<