React事件機(jī)制及簡(jiǎn)單模擬

事件傳播和事件委托

添加事件處理程序的方式:element.addEventListener('click',(event)=>{}),event為事件對(duì)象。缺省是冒泡階段觸發(fā)事件處理程序,捕獲階段觸發(fā)事件處理程序:element.addEventListener('click',(event)=>{},true)

事件流的三個(gè)階段:捕獲階段,到達(dá)目標(biāo)階段,冒泡階段。捕獲階段是由外到內(nèi)傳播,先被document捕獲,然后逐漸傳播到事件目標(biāo),然后到冒泡階段,冒泡階段由內(nèi)到外傳播,逐漸傳播到document。

  <body>
    <div id="parent">
      <div id="child">test</div>
    </div>
    <script>
      const parent = document.getElementById("parent");
      const child = document.getElementById("child");
      document.addEventListener(
        "click",
        (event) => {
          console.log("document 捕獲");
        },
        true
      );
      parent.addEventListener(
        "click",
        (event) => {
          console.log("parent 捕獲");
        },
        true
      );
      child.addEventListener(
        "click",
        (event) => {
          console.log("child 捕獲");
        },
        true
      );
      document.addEventListener("click", (event) => {
        console.log("document 冒泡");
      });
      parent.addEventListener("click", (event) => {
        console.log("parent 冒泡");
      });
      child.addEventListener("click", (event) => {
        console.log("child 冒泡");
      });
    </script>
  </body>
控制臺(tái)輸出.png

事件委托是利用事件冒泡,使用一個(gè)事件處理程序管理一類事件。將事件處理程序不添加到具體節(jié)點(diǎn)上而是添加到祖先節(jié)點(diǎn),通過(guò)event.target區(qū)分觸發(fā)節(jié)點(diǎn)。比如一個(gè)ul列表中每一項(xiàng)都添加事件處理程序,可以把事件處理程序加在ul上,通過(guò)event.target區(qū)分是哪一項(xiàng)觸發(fā)的。

  <body>
    <ul id="list">
      <li id="item1">第 1 項(xiàng)</li>
      <li id="item2">第 2 項(xiàng)</li>
      <li id="item3">第 3 項(xiàng)</li>
    </ul>
    <script>
      const parent = document.getElementById("list");

      parent.addEventListener("click", (event) => {
        switch (event.target.id) {
          case "item1":
            console.log("第 1 項(xiàng)");
            break;
          case "item2":
            console.log("第 2 項(xiàng)");
            break;
          case "item3":
            console.log("第 3 項(xiàng)");
            break;
          default:
            break;
        }
      });
    </script>
  </body>

React事件機(jī)制

React采用將事件委托給document的方式添加事件處理程序document.addEventListener('click',dispatchEvent),事件處理程序會(huì)模擬真實(shí)的事件系統(tǒng)先調(diào)度捕獲階段的事件處理程序,然后執(zhí)行冒泡階段的事件處理程序。因此React 元素上的onClick在真實(shí)的Dom上并沒(méi)有綁定事件處理程序,在document冒泡階段才會(huì)執(zhí)行節(jié)點(diǎn)上的onClick,onClickCapture方法。

class App extends React.Component {
  parentRef = React.createRef();
  childRef = React.createRef();
  componentDidMount() {
    this.parentRef.current.addEventListener(
      "click",
      () => {
        console.log("父元素 原生 捕獲");
      },
      true
    );
    this.parentRef.current.addEventListener("click", () => {
      console.log("父元素 原生 冒泡");
    });
    this.childRef.current.addEventListener(
      "click",
      () => {
        console.log("子元素 原生 捕獲");
      },
      true
    );
    this.childRef.current.addEventListener("click", () => {
      console.log("子元素 原生 冒泡");
    });
    document.addEventListener(
      "click",
      () => {
        console.log("document 捕獲");
      },
      true
    );
    document.addEventListener("click", () => {
      console.log("document 冒泡");
    });
  }
  parentBubble = () => {
    console.log("父元素React 冒泡");
  };
  childBubble = () => {
    console.log("子元素React 冒泡");
  };
  parentCapture = () => {
    console.log("父元素React 捕獲");
  };
  childCapture = () => {
    console.log("子元素React 捕獲");
  };
  render() {
    return (
      <div
        ref={this.parentRef}
        onClick={this.parentBubble}
        onClickCapture={this.parentCapture}
      >
        <p
          ref={this.childRef}
          onClick={this.childBubble}
          onClickCapture={this.childCapture}
        >
          事件執(zhí)行順序
        </p>
      </div>
    );
  }
}
圖二:控制臺(tái)輸出.png

模擬實(shí)現(xiàn)React事件機(jī)制

React通過(guò)事件委托響應(yīng)事件,document.addEventListener("click", dispatchEvent),dispatchEvent應(yīng)遵循事件模型,先處理捕獲階段的事件監(jiān)聽(tīng),后處理冒泡階段的事件監(jiān)聽(tīng)。

  <body>
    <div id="parent">
      <div id="child">事件順序</div>
    </div>
    <script>
      const parent = document.getElementById("parent");
      const child = document.getElementById("child");

      function dispatchEvent(event) {
        const pathes = [];
        let path = event.target;
        while (path) {
          pathes.push(path);
          path = path.parentNode;
        }
        for (let index = pathes.length - 1; index >= 0; index--) {
          const path = pathes[index];
          path.onClickCapture && path.onClickCapture();
        }
        for (let index = 0; index <= pathes.length - 1; index++) {
          const path = pathes[index];
          path.onClick && path.onClick();
        }
      }
      
      document.addEventListener("click", dispatchEvent);
      parent.onClick = () => {
        console.log("parent React 冒泡");
      };
      parent.onClickCapture = () => {
        console.log("parent React 捕獲");
      };
      child.onClick = () => {
        console.log("child React 冒泡");
      };
      child.onClickCapture = () => {
        console.log("child React 捕獲");
      };
    </script>
  </body>

React 16事件機(jī)制存在的問(wèn)題

理想中應(yīng)遵循先捕獲后冒泡,但在添加原生事件處理程序和React事件處理程序是會(huì)交替出現(xiàn)捕獲、冒泡的情況,如上文圖二。因此官方不建議混合使用原生事件處理程序和React事件處理程序。

React 17改進(jìn)了事件機(jī)制,將事件不再委托給document而是委托給容器,并保證了捕獲、冒泡不交叉出現(xiàn)。

  <body>
    <div id="parent">
      <div id="child">事件順序</div>
    </div>
    <script>
      const parent = document.getElementById("parent");
      const child = document.getElementById("child");

      function dispatchEvent(event, isBubble) {
        const pathes = [];
        let path = event.target;
        while (path) {
          pathes.push(path);
          path = path.parentNode;
        }
        if (isBubble) {
          for (let index = 0; index <= pathes.length - 1; index++) {
            const path = pathes[index];
            path.onClick && path.onClick();
          }
        } else {
          for (let index = pathes.length - 1; index >= 0; index--) {
            const path = pathes[index];
            path.onClickCapture && path.onClickCapture();
          }
        }
      }

      document.body.addEventListener(
        "click",
        (event) => dispatchEvent(event, false),
        true
      );
      document.body.addEventListener("click", (event) =>
        dispatchEvent(event, true)
      );
      parent.onClick = () => {
        console.log("parent React 冒泡");
      };
      parent.onClickCapture = () => {
        console.log("parent React 捕獲");
      };
      child.onClick = () => {
        console.log("child React 冒泡");
      };
      child.onClickCapture = () => {
        console.log("child React 捕獲");
      };
    </script>
  </body>

混合原生和React事件處理程序示例:

  <body>
    <div id="parent">
      <div id="child">事件順序</div>
    </div>
    <script>
      const parent = document.getElementById("parent");
      const child = document.getElementById("child");

      function dispatchEvent(event, isBubble) {
        const pathes = [];
        let path = event.target;
        while (path) {
          pathes.push(path);
          path = path.parentNode;
        }
        if (isBubble) {
          for (let index = 0; index <= pathes.length - 1; index++) {
            const path = pathes[index];
            path.onClick && path.onClick();
          }
        } else {
          for (let index = pathes.length - 1; index >= 0; index--) {
            const path = pathes[index];
            path.onClickCapture && path.onClickCapture();
          }
        }
      }

      document.body.addEventListener(
        "click",
        (event) => dispatchEvent(event, false),
        true
      );
      document.body.addEventListener("click", (event) =>
        dispatchEvent(event, true)
      );
      parent.onClick = () => {
        console.log("parent React 冒泡");
      };
      parent.onClickCapture = () => {
        console.log("parent React 捕獲");
      };
      child.onClick = () => {
        console.log("child React 冒泡");
      };
      child.onClickCapture = () => {
        console.log("child React 捕獲");
      };

      parent.addEventListener(
        "click",
        (event) => console.log("parent 原生 捕獲"),
        true
      );
      parent.addEventListener("click", (event) =>
        console.log("parent 原生 冒泡")
      );
      child.addEventListener(
        "click",
        (event) => console.log("child 原生 捕獲"),
        true
      );
      child.addEventListener("click", (event) =>
        console.log("child 原生 冒泡")
      );
      document.addEventListener(
        "click",
        (event) => console.log("document 原生 捕獲"),
        true
      );
      document.addEventListener("click", (event) =>
        console.log("document 原生 冒泡")
      );
    </script>
  </body>
控制臺(tái)輸出.png

一個(gè)應(yīng)用

頁(yè)面有一個(gè)顯示按鈕,點(diǎn)擊顯示模態(tài)框,點(diǎn)擊模態(tài)框其它區(qū)域關(guān)閉模態(tài)框。

class Modal extends React.Component {
  state = { show: false };
  componentDidMount() {
    document.addEventListener("click", () => {
      this.setState({ show: false });
    });
  }

  handleClick = (event) => {
    this.setState({ show: true });
  };

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>顯示</button>
        {this.state.show && <div>this is Modal</div>}
      </div>
    );
  }
}

實(shí)際表現(xiàn):點(diǎn)擊按鈕卻不顯示模態(tài)框。因?yàn)镽eact事件handleClick在document事件監(jiān)聽(tīng)處理程序處理,即先執(zhí)行this.setState({ show: true });然后再執(zhí)行componentDidMount中的this.setState({ show: false });所以不顯示模態(tài)框。可以通過(guò)stopImediatePropagation阻止事件傳播來(lái)解決這個(gè)問(wèn)題。同一個(gè)節(jié)點(diǎn)有多個(gè)事件監(jiān)聽(tīng)器,上面的事件監(jiān)聽(tīng)器調(diào)用了stopImmediatePropagation,則后續(xù)的事件監(jiān)聽(tīng)器不執(zhí)行,事件也不會(huì)繼續(xù)傳播。

class Modal extends React.Component {
  state = { show: false };
  componentDidMount() {
    document.addEventListener("click", () => {
      this.setState({ show: false });
    });
  }

  handleClick = (event) => {
    this.setState({ show: true });
    event.nativeEvent.stopImmediatePropagation();
  };
  handleModalClick = (event) => {
    event.nativeEvent.stopImmediatePropagation();
  };

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>顯示</button>
        {this.state.show && (
          <div onClick={this.handleModalClick}>this is Modal</div>
        )}
      </div>
    );
  }
}

總結(jié)

  • React采用事件委托的方式在document上添加事件監(jiān)聽(tīng)器;
  • React中的事件對(duì)象是合成事件對(duì)象,抹平了系統(tǒng)的差異性;
  • 通過(guò)事件委托使用一個(gè)事件監(jiān)聽(tīng)器處理一類事件,代替使用多個(gè)事件監(jiān)聽(tīng)函數(shù)的方案,節(jié)省內(nèi)存和讀寫DOM的開(kāi)銷;
  • 源碼
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容