摘要
在上一篇中,我們實現了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.tagswitch (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)}
}