摘要
在上一篇中,我们实现了useState的hook,但由于没有实现事件机制,所以我们只能将setState挂载在window上。
而这一篇主要就是来实现事件系统,从而实现通过点击事件进行setState。
而在React中,虽然我们是将事件绑定在JSX上的某个元素上,但是其实最终的执行者是最外层的容器。
也就是说React利用了冒泡的机制,将所有事件都冒泡到了最外层容器上,从而创建合成事件,在对相应的事件执行。
所以在实现事件机制之前,我们先将准备好的JSX进行修改:
function App() {
const [name, setName] = useState('kusi','key');
const [age, setAge] = useState(20)
const click1 = () => {
console.log(name)
setName(name + '1')
}
const click2 = () => {
console.log(age)
setAge(age + 1)
}
return jsx("div", {
ref: "123",
onClick: click1,
children: jsx("span", {
children: name + age,
onClick: click2
})
});
}
1.实现initEvent方法
刚才我们说了,在React中,事件的执行者是最外层的容器,也就是说我们需要给最外层的容器绑定一个事件,用来初始化。
export const initEvent = (root, eventType) => {
root.addEventListener(eventType, (e) => {
dispatchEvent()
})
}
而我们可以在最开始的时候,调用initEvent。最开始也就是createContainer方法里面:
function createContainer(root) {
initEvent(root, 'click')
const hostRootFilber = new FilberNode(HostRoot, {}, '')
return new FilberRootNode(root, hostRootFilber)
}
这里我们先实现click事件。
2.给所有DOM绑定props
我们思考一下,对于所有的事件,一定是在对应组件的Props里面,而我们要在dom上拿到对应的事件,那么就要将props属性同步给dom。
而真实DOM是在completeWork阶段生成的,所以我们需要实现一个方法,用来给dom绑定props属性:
function addPropsToDOM(element, props) {
element['__props'] = props
}
在completeWork阶段,调用该方法:
export const completeWork = (filberNode) => {
const tag = filberNode.tag
switch (tag) {
case HostComponent: {
if(filberNode.stateNode !== null){
//更新
addPropsToDOM(filberNode.stateNode, filberNode.pendingProps)
}else{
completeHostComponent(filberNode)
}
break;
}
function completeHostComponent(filberNode) {
const type = filberNode.type;
const element = document.createElement(type);
addPropsToDOM(element, filberNode.pendingProps)
filberNode.stateNode = element;
const parent = filberNode.return;
if(parent && parent.stateNode && parent.tag === HostComponent) {
parent.stateNode.appendChild(element)
}
completeWork(filberNode.child)
}
此时可以打印看一下stateNode中的element,是否已经有__props属性了:
3.收集所有事件
现在所有的DOM已经有了对应的事件,现在我们需要将所有的事件收集起来:
收集的过程就是,当前点击的元素,到最外层容器录过的所有事件。
所以我们需要三个参数:当前点击的元素,容器,事件类型。
由于在React中,事件分为两种,比如onClick和onClickCapture。所以我们用两个集合来收集这两种事件。
function collectEvent(event, root, eventType) {
const bubble = [];
const capture = [];
while(event !== root){
const eventProps = event['__props'];
if(eventType === 'click'){
const click = eventProps['onClick'];
const clickCapture = eventProps['onClickCapture'];
if(click){
bubble.push(click);
}
if(clickCapture){
capture.unshift(clickCapture)
}
}
event = event.parentNode;
}
return {bubble, capture}
}
然后我们在dispatchEvent中进行调用:
function dispatchEvent(root, eventType, e) {
const {bubble, capture} = collectEvent(e.target, root, eventType)
console.log(bubble, capture);
}
我们看一下打印结果:
可以看到在bubble中,已经将方法保存下来了。
4.创建合成事件对象
我们现在已经收集了这么多方法,按理说也该去执行了。但是有一个问题, 我们创建了bubble和capture。只是用来模仿浏览器的冒泡和捕获,也就是并非是真正的冒泡捕获。
最终执行所有事件的还是root,所以我们要创建一个新的event,用来代替浏览器的event。
在这个方法中,我们用一个标志位__stopPropgation来决定是否冒泡。如果在外面调用“e.stopPropgation”,我们将这个标志位置位true。
function createSyntheticEvent(e) {
const syntheticEvent = e;
syntheticEvent.__stopPropgation = false;
const originStopPropgation = e.stopPropagation;
syntheticEvent.stopPropagation = () => {
syntheticEvent.__stopPropgation = true;
if( originStopPropgation ) {
originStopPropgation()
}
}
return syntheticEvent;
}
}
在dispatchEvent中进行调用:
function dispatchEvent(root, eventType, e) {
const {bubble, capture} = collectEvent(e.target, root, eventType)
console.log(bubble, capture);
const se = createSyntheticEvent(e)
}
4.事件调用
OK,现在我们要进行最后一步,对事件进行调用了。我们只需要对bubble和capture中的事件进行遍历调用即可,现在我们实现一个方法:
function triggerEvent(paths, se) {
for(let i=0; i< paths.length; i++) {
paths[i].call(null, se);
if(se.__stopPropgation) {
break;
}
}
}
然后再dispatchEvent中执行:
function dispatchEvent(root, eventType, e) {
const {bubble, capture} = collectEvent(e.target, root, eventType)
const se = createSyntheticEvent(e);
triggerEvent(capture,se);
if(!se.__stopPropgation) {
triggerEvent(bubble,se)
}
}