likes
comments
collection
share

谈谈react的合成事件与JS事件传播机制

作者站长头像
站长
· 阅读数 32

JS事件传播机制

事件传播机制分为三个阶段

以下以click事件为例。

1. 捕获阶段

当我们触发目标节点的点击事件时,会从最外层元素向里层元素逐一查找,分析出路径来,此阶段称为捕获阶段

2. 目标阶段

目标节点的点击行为事件触发,此阶段称为目标阶段

3. 冒泡阶段

按照捕获阶段分析出来的路径,从里到外,把每个元素的点击行为事件逐一触发,此阶段称为冒泡阶段

图示:

谈谈react的合成事件与JS事件传播机制

事件委托

事件委托是利用事件的传播机制,实现的一套事件绑定处理方案。

例如:一个容器中有很多元素,都要在点击的时候做一些事情。

- 传统方案:获取需要操作的元素,然后逐一做事件绑定。
- 事件委托:只需要给容器做一个时间绑定「点击内部的任何元素,根据事件的冒泡传播机制,都会让容器的点击事件也触发;然后再根据事件源,做不同的事情就可以了。」
const body = document.body
body.addEventListener('click',(e)=>{
    let target = ev.target,
    id = target.id
    if(id==='wrapper'){
        // do something,when wrapper ele click
        return;
    }
     if(id==='outer'){
        // do something,when outer ele click
        return;
    }
     if(id==='inner'){
        // do something,when inner ele click
        return;
    }
    

})

事件委托的优势:

  1. 提高js代码的运行的性能,并且把处理的逻辑集中在了一起;
  2. 给动态绑定的元素做事件绑定;
  3. 某个元素下,存在多个元素有同样的操作。

限制:

  1. 操作的事件必须支持冒泡传播机制才可以使用事件委托,如:mouseenter、mouseleave 等事件没有冒泡传播机制。
  2. 若是单独的事件绑定,做了时间传播机制的阻止,那么事件委托中的操作也不会生效!!

react的合成事件

合成事件是围绕浏览器原生事件,充当跨浏览器包装器的对象;它们将不同浏览器的行为合并为一个 API,这样做是为了确保事件在不同浏览器中显示一致的属性!

在react中事件的处理并不是通过对当前元素基于addEventLinstener进行绑定的,而是通过事件委托处理的

基本语法

在JSX元素上,直接基于 onXxx={函数} 进行事件绑定! 浏览器标准事件,在React中大部分都支持。

import React, { Component } from 'react';
export default class App extends Component {
  state = {
    num: 0,
  };
  render() {
    let { num } = this.state;
    return (
      <div>
        {num}
        <br />
        <button
          onClick={(ev) => {
            // 合成事件对象 :SyntheticBaseEvent
            console.log(ev);
            this.setState({
              num: num + 1,
            });
          }}
        >
          处理
        </button>
      </div>
    );
  }
}


在类组件中需要注意的点:合成事件中的this和传参处理

在类组件中,我们要时刻保证,合成事件绑定的函数中,里面的this是当前类的实例! 也需要保障传参的正常!

import React, { Component } from 'react';
export default class App extends Component {
  handler1 = (ev) => {
    console.log(ev, this);
  };
  handler2 = (x, y, ev) => {
    console.log(x, y, ev, this);
  };
  render() {
    return (
      <div>
        <button onClick={this.handler1}>测试1</button>
        <button onClick={this.handler2.bind(null, 10, 20)}>测试2</button>
      </div>
    );
  }
}


合成事件的底层机制

总原则:基于事件委托实现。 在react中事件的处理并不是通过对当前元素基于addEventLinstener进行绑定的,而是通过事件委托处理的

react 17及以后,都是委托给#root容器,捕获和冒泡阶段都做了事件委托。

  1. 在组件渲染的时候,如果发现JSX元素中有onXxx/onXxxCapture 这样的属性,不会给当前元素直接做事件绑定,只是把绑定的方法赋给元素的相关属性!!
  2. 然后给#root这个容器做了事件绑定【捕获和冒泡都做了】。
1. 因为组件中所渲染的内容,最后都会插入到#root 容器中,这样点击页面中任何一个元素,最后都会把      #root的电机行为触发。
2. 而在给#root绑定的方法中,把之前给元素设置的onXxx/onXxxCapture 属性,在相应的阶段执行。

合成事件原理代码:

 const root = document.querySelector("#root"),
  outer = document.querySelector(".outer"),
  inner = document.querySelector(".inner");
/* 原理 */
const dispatchEvent = function dispatchEvent(ev, isCapture) {
  let path = ev.path,
    target = ev.target;
  if (isCapture) {
    [...path].reverse().forEach((elem) => {
      let handler = elem.onClickCapture;
      if (typeof handler === "function") handler(ev);
    });
    return;
  }
  path.forEach((elem) => {
    let handler = elem.onClick;
    if (typeof handler === "function") handler(ev);
  });
};

// 冒泡阶段的委托
root.addEventListener(
  "click",
  function (ev) {
    dispatchEvent(ev, false);
  },
  false
);
// 捕获阶段的委托
root.addEventListener(
  "click",
  function (ev) {
    dispatchEvent(ev, true);
  },
  true
);

例子展示

代码示例:

window.addEventListener("click", () => {
  console.log("window 冒泡阶段 原生");
});
window.addEventListener(
  "click",
  () => {
    console.log("window 捕获阶段 原生");
  },
  true
);

document.getElementsByTagName("html")[0].addEventListener("click", () => {
  console.log("html 冒泡阶段 原生");
});

document.getElementsByTagName("html")[0].addEventListener(
  "click",
  () => {
    console.log("html 捕获阶段 原生");
  },
  true
);

export default function HomePage() {
  useEffect(() => {
    document.getElementById("outer")?.addEventListener("click", () => {
      console.log("outer 冒泡阶段 原生");
    });
    document.getElementById("outer")?.addEventListener(
      "click",
      () => {
        console.log("outer 捕获阶段 原生");
      },
      true
    );
  }, []);

  return (
    <div
      className="wrapper"
      onClick={() => {
        console.log("wrapper 冒泡阶段");
      }}
      onClickCapture={() => {
        console.log("wrapper 捕获阶段");
      }}
    >
      <div
        className="outer"
        id="outer"
      >
        <div
          className="inner"
          onClick={() => {
            console.log("inner 冒泡阶段");
          }}
          onClickCapture={() => {
            console.log("inner 捕获阶段");
          }}
        >
          目标元素
        </div>
      </div>
    </div>
  );
}


结果输出: 谈谈react的合成事件与JS事件传播机制

react 16及以前

在16版本-中,合成事件的处理机制,是把事件委托给document元素,并且只做了冒泡阶段的委托;在委托的方法中,把onXxx/onXxxCapture合成事件属性进行执行。

合成事件的原理代码:

const outer = document.querySelector(".outer"),
  inner = document.querySelector(".inner");
/* 原理 */
const dispatchEvent = function dispatchEvent(ev) {
  let path = ev.path,
    target = ev.target;
  [...path].reverse().forEach((elem) => {
    let handler = elem.onClickCapture;
    if (typeof handler === "function") handler(ev);
  });
  path.forEach((elem) => {
    let handler = elem.onClick;
    if (typeof handler === "function") handler(ev);
  });
};
// 委托
document.addEventListener(
  "click",
  function (ev) {
    dispatchEvent(ev);
  },
  false
);


示例代码

window.addEventListener("click", () => {
  console.log("window 冒泡阶段 原生");
});
window.addEventListener(
  "click",
  () => {
    console.log("window 捕获阶段 原生");
  },
  true
);

document.getElementsByTagName("html")[0].addEventListener("click", () => {
  console.log("html 冒泡阶段 原生");
});

document.getElementsByTagName("html")[0].addEventListener(
  "click",
  () => {
    console.log("html 捕获阶段 原生");
  },
  true
);

function IndexPage() {
  useEffect(() => {
    document.getElementById("outer")?.addEventListener("click", () => {
      console.log("outer 冒泡阶段 原生");
    });
    document.getElementById("outer")?.addEventListener(
      "click",
      () => {
        console.log("outer 捕获阶段 原生");
      },
      true
    );
  }, []);

  return (
    <div
      className="wrapper"
      onClick={() => {
        console.log("wrapper 冒泡阶段");
      }}
      onClickCapture={() => {
        console.log("wrapper 捕获阶段");
      }}
    >
      <div className="outer" id="outer">
        <div
          className="inner"
          onClick={() => {
            console.log("inner 冒泡阶段");
          }}
          onClickCapture={() => {
            console.log("inner 捕获阶段");
          }}
        >
          目标元素
        </div>
      </div>
    </div>
  );
}


输出结果: 谈谈react的合成事件与JS事件传播机制

合成事件对象

合成事件对象SyntheticBaseEvent:我们在React合成事件触发的时候,也可以获取到事件对象,只不过此对象是合成事件对象「React内部经过特殊处理,把各个浏览器的事件对象统一化后,构建的一个事件对象」 合成事件对象中,也包含了浏览器内置事件对象中的一些属性和方法。

常用的基本都有:

  • clientX/clientY
  • pageX/pageY
  • target
  • type
  • preventDefault
  • stopPropagation
  • ...
  • .nativeEvent:基于这个属性,可以获取浏览器内置『原生』的事件对象

谈谈react的合成事件与JS事件传播机制

react合成事件对象和内置事件对象的不同处:

react合成事件对象:该有的都有。

  • 其中对象中nativeEvent对象中存放的是内置事件对象中的属性和方法;
  • 经过bind处理后,ev是最后一个实参。

谈谈react的合成事件与JS事件传播机制

react 16与react 17+ 的合成事件对象的区别:

react 16 中使用的对象缓存池的机制

在react16中react内部基于事件对象池做了一个缓存机制!!属性都是做了get set劫持,且属性值为null。

谈谈react的合成事件与JS事件传播机制

react 16 对象缓存池

当每一次事件触发的时候,如果传播到了委托的元素上【document】,在委托的方法中我们首先会对内置的事件对象做统一处理,生成合成事件对象!

为了防止每一次的触发都是重新创建出新的事件合成对象,设置了一个事件对象池【缓存池】,

  • 本次事件触发,获取到事件操作的相关信息,我们从事件 事件对象池 中获取存储的合成事件对象,把信息赋给相关的成员。
  • 等待本次操作结束,把合成对象中的成员信息都给清空掉,再放入到事件对象池中。这也就是为什么我们在16版本中通过异步方法获取合成事件对象的某个属性值时,得到的是null!!
 <div
      className="wrapper"
      onClick={(ev) => {
        console.log(ev.clientX, "同步获取ev.clientX");
        setTimeout(() => {
          console.log(ev.clientX, "异步获取ev.clientX");
        }, 500);
      }}
    >click me</div>

谈谈react的合成事件与JS事件传播机制

react 17+ 的合成事件对象:

react17及以后去掉了对象缓存池机制。异步也是可以获取到属性值的

谈谈react的合成事件与JS事件传播机制

转载自:https://juejin.cn/post/7282693176204034063
评论
请登录