深入react源码!react事件模型是如何实现的❓

写在前面

本文致力于实现一个最简的事件模型,代码均已上传至github,期待star!✨:

github.com/kongyich/ti…

本文是系列文章,阅读的连续性非常重要!!

手写mini-react!超万字实现mount首次渲染流程🎉🎉

进击的hooks!实现react中的hooks架构和useState 🚀🚀

更新!更新!实现react更新及diff流程

食用前指南!本文涉及到react的源码知识,需要对react有基础的知识功底,建议没有接触过react的同学先去官网学习一下基础知识,再看本系列最佳!

事件流

在浏览器中,javascript​与HTML​之间的交互是通过事件去实现的。在一整个页面的DOM结构中可能存在许许多多的事件,点击的click​事件、加载的load​事件、鼠标指针悬浮的mouseover​事件等等。那么当用户在触发某个事件的时候,绑定在DOM中的事件是怎么执行的呢?

例如我们有如下DOM结构:

<div> 
    <p>Click me!</p> 
</div>

这就会涉及事件流的概念,事件流描述的是从页面中接收事件的顺序。事件发生后会在目标节点和根节点之间按照特定的顺序传播,路径经过的节点都会接收到事件。

事件捕获

假设我们在节点p上触发了click​事件,此时会产生两种事件传递顺序。网景提出一种事件流名为事件捕获(event capturing)。事件会从最外层开始发生,直到用户产生行为的元素。

针对我们上面的DOM结构,他的传递传递顺序如下:

document -> html -> body -> div -> p

最后到达用户触发的元素p,此时也被称为事件目标阶段。

事件冒泡

在到达时间目标阶段后,微软提出了名为事件冒泡(event bubbling) 的事件流,也就是事件会从最内层的元素开始发生,一直向上传播,直到document​对象。

针对我们上面的DOM结构,他的传递传递顺序如下:

p -> div -> body -> html -> document

深入react源码!react事件模型是如何实现的❓

1-4是捕获过程,4-5是目标阶段,5-8是冒泡阶段。

addEventListener

DOM2级事件中规定的事件流同时支持了事件捕获阶段和事件冒泡阶段,而作为开发者,我们可以选择事件处理函数在哪一个阶段被调用。

addEventListener​方法用来为一个特定的元素绑定一个事件处理函数,是JavaScript​中的常用方法。addEventListener​有三个参数:

element.addEventListener(event, function, useCapture)
参数 描述
event 必须。字符串,指定事件名。

所有 HTML DOM 事件,可以查看完整的HTML DOM Event 对象参考手册

function必须。指定要事件触发时执行的函数。

当事件对象会作为第一个参数传入函数。 事件对象的类型取决于特定的事件。

useCapture可选。布尔值,指定事件是否在捕获或冒泡阶段执行。

可能值:true – 事件在捕获阶段执行(即在事件捕获阶段调用处理函数)false- 默认。事件在冒泡阶段执行(即表示在事件冒泡的阶段调用事件处理函数)

事件代理(委托)

利用事件流的特性,我们可以实现一种叫做事件代理的方法。

我们有一些li元素,当点击每个 li 元素时,输出该元素的颜色:

<ul class="color_list">     
    <li>red</li>     
    <li>orange</li>     
    <li>yellow</li>     
    <li>green</li>     
    <li>blue</li>     
    <li>purple</li>   
</ul> 
<div class="box"></div>

<style>
.color_list{         
    display: flex;         
    display: -webkit-flex;     
}     
.color_list li{         
    width: 100px;         
    height: 100px;         
    list-style: none;         
    text-align: center;         
    line-height: 100px;     
} 
//每个li加上对应的颜色,此处省略 
.box{         
    width: 600px;         
    height: 150px;         
    background-color: #cccccc;         
    line-height: 150px;         
    text-align: center;     
} 
</style>

我们想要在点击每个 li 标签时,输出li当中的颜色(innerHTML)。常规做法是遍历每个 li ,然后在每个 li 上绑定一个点击事件:

var color_list=document.querySelector(".color_list");           
var colors=color_list.getElementsByTagName("li");           
var box=document.querySelector(".box");           
for(var n=0;n<colors.length;n++){               
    colors[n].addEventListener("click",function(){                   
        console.log(this.innerHTML)                   
        box.innerHTML="该颜色为 "+this.innerHTML;               
    })           
}

这种做法在 li 较少的时候可以使用,但如果有一万个 li ,那就会导致性能降低。

这时候使用事件代理,利用事件流的特性,只绑定一个事件处理函数就可以完成:

function colorChange(e){             
    var e=e||window.event;//兼容性的处理        
    if(e.target.nodeName.toLowerCase()==="li"){                 
        box.innerHTML="该颜色为 "+e.target.innerHTML;             
    }                         
}         
color_list.addEventListener("click",colorChange,false)

由于事件冒泡机制,点击了 li 后会冒泡到 ul ,此时就会触发绑定在 ul 上的点击事件,再利用 target 找到事件实际发生的元素,就可以达到预期的效果。

使用事件代理的好处不仅在于将多个事件处理函数减为一个,而且对于不同的元素可以有不同的处理方法。假如上述列表元素当中添加了其他的元素节点(如:a、span等),我们不必再一次循环给每一个元素绑定事件,直接修改事件代理的事件处理函数即可。

React的事件模型

我们在使用react的jsx来编写代码时,以点击事件为例,通常是这样定义的:

function App() {
	return (
		<div onClick={add}></div>
	)
}

可以看到在jsx中定义的onClick​明显不是DOM原生的事件名,这其实是react 植根于浏览器事件模型实现的一套事件系统。

react会将所有dom事件都绑定到document​上面,而不是某一个元素上,统一的使用事件监听,并在冒泡阶段处理事件,所有当在挂载或者销毁组件时,只需要在统一的事件监听上增加或者删除对象,当事件被触发时,我们的组件会生成一个合成事件,然后传递到document​中,document​会通过dispatchEvent​回调函数依次执行dispatchListener​中同类型事件监听函数。


接下来开始定义在react初始化时对事件进行挂载,还记得在初始化那一篇文章中,整个应用的入口函数:

ReactDOM.createRoot(document.getElementById('root')).render(
	<App />
);

在入口函数createRoot​ 中,container​ 参数则是整个应用的根DOM挂载点,在开始执行render​函数时,为根DOM节点挂载自定义的click​事件。

export function createRoot(container) {
	const root = createContainer(container);

	return {
		render(element) {
			// 挂载click事件
			initEvent(container, 'click');
			// ...省略
		}
	};
}

eventType​只被允许传入我们支持的事件,首先定义一个事件列表:

const validEventTypeList = ['click'];
export function initEvent(container, eventType) {
	// 验证是否支持指定的事件类型
	if (!validEventTypeList.includes(eventType)) {
		console.warn('当前不支持', eventType, '事件');
		return;
	}
	// 绑定事件
	container.addEventListener(eventType, (e) => {
		dispatchEvent(container, eventType, e);
	});
}

为根节点绑定事件,当事件被触发时,调用dispatchEvent​ 回调函数依次执行dispatchListener​中同类型事件监听函数。

既然要实现整个DOM的事件模型,自然也要实现原生DOM的整个事件流程,包括事件捕获及事件冒泡。

所以在dispatchEvent​函数中需要完成的步骤如下:

  • ​​收集沿途的事件
  • ​​构造合成事件​
  • 执行事件捕获列表
  • 执行事件冒泡列表
function dispatchEvent(container, eventType, e) {
	const targetElement = e.target;

	if (targetElement === null) {
		console.warn('事件不存在target', e);
		return;
	}

	// 1. 收集沿途的事件
	const { bubble, capture } = collectPaths(
		targetElement,
		container,
		eventType
	);
	// 2. 构造合成事件
	const se = createSyntheticEvent(e);

	// 3. 遍历captue
	triggerEventFlow(capture, se);
	// 阻止冒泡事件执行
	if (!se.__stopPropagation) {
		// 4. 遍历bubble
		triggerEventFlow(bubble, se);
	}
}

收集沿途的事件

在一个事件被触发时,首先就要收集目标DOM节点及其所有父节点定义的所有事件,根据目标事件对象,查找所有父级的onClick​和onClickCapture​ 事件,react默认的事件流方式是冒泡,如果想要在捕获阶段触发,可以添加onClickCapture​来定义事件。

首先创建一个函数来映射事件名:

function getEventCallbackNameFromEventType(
	eventType
) {
	return {
		click: ['onClickCapture', 'onClick']
	}[eventType];
}

getEventCallbackNameFromEventType​函数通过接收一个DOM事件名来获取对应的捕获及冒泡的事件名列表。


为了方便获取节点中的属性,也就是根据事件名称获取对应节点的事件处理函数,我们使用一个自定义属性来保存所有的节点属性,并在初始化以及更新流程保存。

export const elementPropsKey = '__props';
// 接收节点和属性
export function updateFiberProps(node, props) {
	node[elementPropsKey] = props;
}

render​流程中是处理props​初始化以及更新的一个合适的时机。在completeWork​ 函数的执行时对节点的props​进行保存:

export const completeWork = (wip: FiberNode) => {
	const newProps = wip.pendingProps;
	const current = wip.alternate;

	switch (wip.tag) {
		case HostComponent:
			if (current !== null && wip.stateNode) {
				// update 更新流程
				// 1. props是否变化 {onClick: xx} {onClick: xxx}
				updateFiberProps(wip.stateNode, newProps);
			} else {
				// mount 初始化流程
				// 1. 构建DOM
				const instance = createInstance(wip.type, newProps);
				// ...省略
			}
	
			return null;
		case HostText:
			// ...省略
			return null;
		case HostRoot:
			// ...省略
		case FunctionComponent:
			// ...省略
		default:
			if (__DEV__) {
				console.warn('未处理的completeWork情况', wip);
			}
			break;
	}
};
  • 初始化

初始化时直接在创建fiber​节点的真实DOM时保存props​。

export const createInstance = (type, props) => {
	// 处理props
	const element = document.createElement(type);
	// 初始化时保存props
	updateFiberProps(element, props);
	return element;
};
  • 更新

节点的更新阶段直接将新的props​重新保存。

updateFiberProps(wip.stateNode, newProps);

在收集事件的过程中使用节点的parentNode​ 属性向上查找,直到根节点。

在这里有一个需要注意的地方,我们定义了两个数组,分别收集捕获与冒泡事件的处理函数,但是我们在逐层收集时,捕获事件使用unshift​(前插),而冒泡事件使用push​(后增)。这样做的原因是,在两个数组收集完毕后两者的顺序完全不同:

capture: [父 --> 子]
bubble: [子 --> 父]

两者的顺序刚好相反,capture​父节点的事件处理函数在前,子节点的在后,bubble​子节点的事件处理函数在前,父节点的在后。

深入react源码!react事件模型是如何实现的❓

在后续遍历这两个数组执行事件回调函数的时候,执行顺序正好与DOM事件流的触发顺序一致。先由父到子逐层捕获,再由子到父逐层冒泡。

function collectPaths(
	targetElement,
	container,
	eventType
) {
	// 定义事件收集数组
	const paths = {
		capture: [],
		bubble: []
	};

	while (targetElement && targetElement !== container) {
		// 收集
		const elementProps = targetElement[elementPropsKey];
		if (elementProps) {
			// 获取事件映射
			const callbackNameList = getEventCallbackNameFromEventType(eventType);
			if (callbackNameList) {
				callbackNameList.forEach((callbackName, i) => {
					// 获取事件回调函数
					const eventCallback = elementProps[callbackName];
					if (eventCallback) {
						if (i === 0) {
							// capture
							paths.capture.unshift(eventCallback);
						} else {
							// bubble
							paths.bubble.push(eventCallback);
						}
					}
				});
			}
		}
		targetElement = targetElement.parentNode;
	}
	return paths;
}

构造合成事件

构造合成事件可以使我们更加灵活的添加一些额外的处理逻辑。通过劫持事件对象一些原生的方法,添加额外的逻辑。

下面这段逻辑中我们劫持了事件原生的阻止冒泡的方法stopPropagation​,使用自定义属性__stopPropagation​ 来控制是否组织部冒泡,然后重写了stopPropagation​方法。

function createSyntheticEvent(e: Event) {
	const syntheticEvent = e;
	// 初始化__stopPropagation
	syntheticEvent.__stopPropagation = false;
	// 保存原有的stopPropagation
	const originStopPropagation = e.stopPropagation;
	// 自定义stopPropagation执行,执行原有的stopPropagation方法
	syntheticEvent.stopPropagation = () => {
		syntheticEvent.__stopPropagation = true;
		if (originStopPropagation) {
			originStopPropagation();
		}
	};
	return syntheticEvent;
}

执行事件流

执行事件流就是依次遍历我们定义的capture​数组和bubble​数组。当然,如果设置了我们的自定义属性__stopPropagation​为true​,那么将会组织冒泡(阻止bubble​数组的遍历执行)。

function triggerEventFlow(paths, se) {
	for (let i = 0; i < paths.length; i++) {
		const callback = paths[i];
		// 执行事件处理函数
		callback.call(null, se);

		if (se.__stopPropagation) {
			break;
		}
	}
}

写在最后 ⛳

未来可能会更新实现mini-reactantd源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳

原文链接:https://juejin.cn/post/7327138554756890676 作者:小黄瓜没有刺

(0)
上一篇 2024年1月23日 下午4:01
下一篇 2024年1月24日 上午10:05

相关推荐

发表回复

登录后才能评论