buxuku / react-handwritten

手写实现React
0 stars 1 forks source link

3.事件处理,合成事件 #3

Open buxuku opened 3 years ago

buxuku commented 3 years ago

在这之前,我们已经能够在DOM上绑定一些原生的属性了,比如id,style这些.但还没有实现交互行为,比如点击事件之类的,给DOM绑定事件,我们一般采用给DOM添加onxxx这样的事件,或者通过事件委托addEventListener这样的方式.

不过在React中,它自己实现了一套事件机制,就是所谓的合成事件,它解决了不同浏览器对于事件处理的各种兼容性问题,同时把所有的事件都绑定到了Document上面.因此其它DOM上面没有了事件绑定,从而也减少了内存的开销.

React以队列的方式,会从触发的元素向父元素依次进行回溯,来调用所有需要执行的事件函数.

那在实现它的时候,我们大概的思路就是,在事件注册的时候,把事件都绑定到Document上面去,然后在事件触发的时候,通过冒泡的形式来触发对应的事件即可.

事件注册

react-dom/index.js文件里面的方法中,我们增加对事件类型的处理

function renderAttributes(dom, attributes = {}){
    for(let key in attributes){
        const value = attributes[key];
        if(!value || key === 'children') continue; // 属性无值不处理,children也单独处理
        if(key === 'style'){
            for(let attr in value){
                dom.style[attr] = value[attr];
            }
+       } else if(key.startsWith('on')){
+           addEvent(dom, key.toLocaleLowerCase(), value)
        } else {
            dom[key] = value;
        }
    }
    return dom;
}

然后新建一个react-dom/event.js文件,先实现我们的事件注册方法,因为同一个DOM可能会绑定多个不同类型的事件,我们给DOM上增加一个store属性来缓存绑定的事件.相当于维护一个事件池.并对该事件类型在Document上面进行绑定.当用户进行对应的操作的时候,我们就能从event对象里面的target来获取到我们的目标元素.这也就是好比我们对一个ul包裹的li列表,我们要对li进行事件绑定的话, 无须对每一个li都去绑定事件,只需要在ul上面进行事件绑定即可,因为我们可以通过target来获取到到用户到底是点击的哪一个li的.

/**
 * 事件注册,所有的事件都绑定到Document上面
 * @param dom
 * @param key
 * @param handler
 */
export function addEvent(dom, eventType, handler){
    dom.store = dom.store || {};
    dom.store[eventType] = handler; // 给dom增加一个store属性来缓存当前dom上面绑定的所有事件.
    if(!document[eventType]){ // 同一类型的事件在document上面只需要绑定一次
        document[eventType] = dispatchEvent;
    }
}

事件派发

在目标元素触发事件的时候,我们需要把事件不断冒泡上去,我们需要继续来实现上一步未写的dispatchEvent方法

/**
 * 事件派发
 * @param event
 */
function dispatchEvent(event){
    let { target, type } = event;
    let eventType = `on${type}`;
    let syntheticEvent = createSyntheticEvent(event);
    // 模拟事件冒泡,一直向父级元素进行冒泡
    while(target){
        let { store } = target;
        let handler = store && store[eventType];
        handler && handler.call(target, syntheticEvent);
        target=target.parentNode;
    }
}

/**
 * 处理浏览器的一些兼容性处理
 * @param event
 * @returns {{}}
 */
function createSyntheticEvent(event){
    let syntheticEvent = {};
    for(let key in event){
        syntheticEvent[key] = event[key];
    }
    return syntheticEvent;
}

这里对syntheticEvent没有去完整实现,它里面处理了浏览器的各自兼容性.同时因为React是自己维护的一套事件队列,所以可以看到上面我们在实现冒泡的时候,是通过模拟的方式来实现的.这样目标元素就可以一层层触发上去.

react-dom/index.js中导入这个addEvent方法,同时修改src/index.js文件如下

import React from './react';
import ReactDOM from './react-dom';

class Hello extends React.Component {
    handleClick = (type) => {
        console.log('clicked', type);
    }

    render() {
        return <h1 id="title" className='title' onClick={() => this.handleClick('h1')}>hello <span
            style={{color: 'red'}} onClick={() => this.handleClick('span')}>{this.props.name}</span></h1>
    }
}

ReactDOM.render(
    <Hello name='world'/>, document.getElementById('root')
);

运行发现,点击h1span标签里面的内容都能正常触发事件了,点击span也能正常触发事件的冒泡了.