精品欧美一区二区三区在线观看 _久久久久国色av免费观看性色_国产精品久久在线观看_亚洲第一综合网站_91精品又粗又猛又爽_小泽玛利亚一区二区免费_91亚洲精品国偷拍自产在线观看 _久久精品视频在线播放_美女精品久久久_欧美日韩国产成人在线

「React進階」一文吃透react事件原理

開發 前端
今天我們來一起探討一下React事件原理,這篇文章,我盡量用通俗簡潔的方式,把React事件系統講的明明白白。

[[396223]]

本文轉載自微信公眾號「前端Sharing」,作者前端Sharing。轉載本文請聯系前端Sharing公眾號。

一 前言

今天我們來一起探討一下React事件原理,這篇文章,我盡量用通俗簡潔的方式,把React事件系統講的明明白白。

我們講的react版本是16.13.1 , v17之后react對于事件系統會有相關的改版,文章后半部分會提及。

老規矩,在正式講解react之前,我們先想想這幾個問題(如果我是面試官,你會怎么回答?):

1 我們寫的事件是綁定在dom上么,如果不是綁定在哪里?

2 為什么我們的事件不能綁定給組件?

3 為什么我們的事件手動綁定this(不是箭頭函數的情況)

4 為什么不能用 return false來阻止事件的默認行為?

5 react怎么通過dom元素,找到與之對應的 fiber對象的?

6 onClick是在冒泡階段綁定的?那么onClickCapture就是在事件捕獲階段綁定的嗎?

必要的知識概念

在弄清楚react事件之前,有幾個概念我們必須弄清楚,因為只有弄明白這幾個概念,在事件觸發階段,我們才能更好的理解react處理事件本質。

我們寫在JSX事件終將變成什么?

我們先寫一段含有點擊事件的react JSX語法,看一下它最終會變成什么樣子?

  1. class Index extends React.Component{ 
  2.     handerClick= (value) => console.log(value)  
  3.     render(){ 
  4.         return <div> 
  5.             <button onClick={ this.handerClick } > 按鈕點擊 </button> 
  6.         </div> 
  7.     } 

經過babel轉換成React.createElement形式,如下:

babel.jpg

最終轉成fiber對象形式如下:

fiber.jpg

fiber對象上的memoizedProps 和 pendingProps保存了我們的事件。

什么是合成事件?

通過上一步我們看到了,我們聲明事件保存的位置。但是事件有沒有被真正的注冊呢?我們接下來看一下:

我們看一下當前這個元素<button>上有沒有綁定這個事件監聽器呢?

button_event.jpg

button上綁定的事件

我們可以看到 ,button上綁定了兩個事件,一個是document上的事件監聽器,另外一個是button,但是事件處理函數handle,并不是我們的handerClick事件,而是noop。

noop是什么呢?我們接著來看。

原來noop就指向一個空函數。

noop.jpg

然后我們看document綁定的事件

document.jpg

可以看到click事件被綁定在document上了。

接下來我們再搞搞事情??????,在demo項目中加上一個input輸入框,并綁定一個onChange事件。睜大眼睛看看接下來會發生什么?

  1. class Index extends React.Component{ 
  2.     componentDidMount(){ 
  3.         console.log(this) 
  4.     } 
  5.     handerClick= (value) => console.log(value)  
  6.     handerChange=(value) => console.log(value) 
  7.     render(){ 
  8.         return <div style={{ marginTop:'50px' }} > 
  9.             <button onClick={ this.handerClick } > 按鈕點擊 </button> 
  10.             <input  placeholder="請輸入內容" onChange={ this.handerChange }  /> 
  11.         </div> 
  12.     } 

我們先看一下input dom元素上綁定的事件

然后我們看一下document上綁定的事件

8E1D3BDB-ACFB-4E49-A5FF-CF990C47A60E.jpg

我們發現,我們給<input>綁定的onChange,并沒有直接綁定在input上,而是統一綁定在了document上,然后我們onChange被處理成很多事件監聽器,比如blur , change , input , keydown , keyup 等。

綜上我們可以得出結論:

①我們在 jsx 中綁定的事件(demo中的handerClick,handerChange),根本就沒有注冊到真實的dom上。是綁定在document上統一管理的。

②真實的dom上的click事件被單獨處理,已經被react底層替換成空函數。

③我們在react綁定的事件,比如onChange,在document上,可能有多個事件與之對應。

④ react并不是一開始,把所有的事件都綁定在document上,而是采取了一種按需綁定,比如發現了onClick事件,再去綁定document click事件。

那么什么是react事件合成呢?

在react中,我們綁定的事件onClick等,并不是原生事件,而是由原生事件合成的React事件,比如 click事件合成為onClick事件。比如blur , change , input , keydown , keyup等 , 合成為onChange。

那么react采取這種事件合成的模式呢?

一方面,將事件綁定在document統一管理,防止很多事件直接綁定在原生的dom元素上。造成一些不可控的情況

另一方面, React 想實現一個全瀏覽器的框架, 為了實現這種目標就需要提供全瀏覽器一致性的事件系統,以此抹平不同瀏覽器的差異。

接下來的文章中,會介紹react是怎么做事件合成的。

dom元素對應的fiber Tag對象

我們知道了react怎么儲存了我們的事件函數和事件合成因果。接下來我想讓大家記住一種類型的 fiber 對象,因為后面會用到,這對后續的理解很有幫助。

我們先來看一個代碼片段:

  1. <div>  
  2.   <div> hello , my name is alien </div> 
  3. </div> 

 

 

 

看<div> hello , my name is alien </div> 對應的 fiber類型。tag = 5

然后我們去react源碼中找到這種類的fiber類型。
  1. /react-reconciler/src/ReactWorkTagsq.js 
  1. export const HostComponent = 5; // 元素節點 

好的 ,我們暫且把 HostComponent 和 HostText記錄??下來。接下來回歸正題,我們先來看看react事件合成機制。

二 事件初始化-事件合成,插件機制

接下來,我們來看一看react這么處理事件合成的。首先我們從上面我們知道,react并不是一次性把所有事件都綁定進去,而是如果發現項目中有onClick,才綁定click事件,發現有onChange事件,才綁定blur , change , input , keydown , keyup等。所以為了把原理搞的清清楚楚,筆者把事件原理分成三部分來搞定:

1 react對事件是如何合成的。

2 react事件是怎么綁定的。

3 react事件觸發流程。

事件合成-事件插件

1 必要概念

我們先來看來幾個常量關系,這對于我們吃透react事件原理很有幫助。在解析來的講解中,我也會講到這幾個對象如何來的,具體有什么作用。

①namesToPlugins

第一個概念:namesToPlugins 裝事件名 -> 事件模塊插件的映射,namesToPlugins最終的樣子如下:

  1. const namesToPlugins = { 
  2.     SimpleEventPlugin, 
  3.     EnterLeaveEventPlugin, 
  4.     ChangeEventPlugin, 
  5.     SelectEventPlugin, 
  6.     BeforeInputEventPlugin, 

SimpleEventPlugin等是處理各個事件函數的插件,比如一次點擊事件,就會找到SimpleEventPlugin對應的處理函數。我們先記錄下它,至于具體有什么作用,接下來會講到。

②plugins

plugins,這個對象就是上面注冊的所有插件列表,初始化為空。

  1. const  plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...]; 

③registrationNameModules

registrationNameModules記錄了React合成的事件-對應的事件插件的關系,在React中,處理props中事件的時候,會根據不同的事件名稱,找到對應的事件插件,然后統一綁定在document上。對于沒有出現過的事件,就不會綁定,我們接下來會講到。registrationNameModules大致的樣子如下所示。

  1.     onBlur: SimpleEventPlugin, 
  2.     onClick: SimpleEventPlugin, 
  3.     onClickCapture: SimpleEventPlugin, 
  4.     onChange: ChangeEventPlugin, 
  5.     onChangeCapture: ChangeEventPlugin, 
  6.     onMouseEnter: EnterLeaveEventPlugin, 
  7.     onMouseLeave: EnterLeaveEventPlugin, 
  8.     ... 

④事件插件

那么我們首先就要搞清楚,SimpleEventPlugin,EnterLeaveEventPlugin每個插件都是什么?我們拿SimpleEventPlugin為例,看一下它究竟是什么樣子?

  1. const SimpleEventPlugin = { 
  2.     eventTypes:{  
  3.         'click':{ /* 處理點擊事件  */ 
  4.             phasedRegistrationNames:{ 
  5.                 bubbled: 'onClick',       // 對應的事件冒泡 - onClick  
  6.                 captured:'onClickCapture' //對應事件捕獲階段 - onClickCapture 
  7.             }, 
  8.             dependencies: ['click'], //事件依賴 
  9.             ... 
  10.         }, 
  11.         'blur':{ /* 處理失去焦點事件 */ }, 
  12.         ... 
  13.     } 
  14.     extractEvents:function(topLevelType,targetInst,){ /* eventTypes 里面的事件對應的統一事件處理函數,接下來會重點講到 */ } 

首先事件插件是一個對象,有兩個屬性,第一個extractEvents作為事件統一處理函數,第二個eventTypes是一個對象,對象保存了原生事件名和對應的配置項dispatchConfig的映射關系。由于v16React的事件是統一綁定在document上的,React用獨特的事件名稱比如onClick和onClickCapture,來說明我們給綁定的函數到底是在冒泡事件階段,還是捕獲事件階段執行。

⑤ registrationNameDependencies

  1. onBlur: ['blur'], 
  2. onClick: ['click'], 
  3. onClickCapture: ['click'], 
  4. onChange: ['blur''change''click''focus''input''keydown''keyup''selectionchange'], 
  5. onMouseEnter: ['mouseout''mouseover'], 
  6. onMouseLeave: ['mouseout''mouseover'], 
  7. ... 

2 事件初始化

對于事件合成,v16.13.1版本react采用了初始化注冊方式。

  1. react-dom/src/client/ReactDOMClientInjection.js 
  1. /* 第一步:注冊事件:  */ 
  2. injectEventPluginsByName({ 
  3.     SimpleEventPlugin: SimpleEventPlugin, 
  4.     EnterLeaveEventPlugin: EnterLeaveEventPlugin, 
  5.     ChangeEventPlugin: ChangeEventPlugin, 
  6.     SelectEventPlugin: SelectEventPlugin, 
  7.     BeforeInputEventPlugin: BeforeInputEventPlugin, 
  8. }); 

injectEventPluginsByName 這個函數具體有什么用呢,它在react底層是默認執行的。我們來簡化這個函數,看它到底是干什么的。

  1. legacy-event/EventPluginRegistry.js 
  1. /* 注冊事件插件 */ 
  2. export function injectEventPluginsByName(injectedNamesToPlugins){ 
  3.      for (const pluginName in injectedNamesToPlugins) { 
  4.          namesToPlugins[pluginName] = injectedNamesToPlugins[pluginName] 
  5.      } 
  6.      recomputePluginOrdering() 

injectEventPluginsByName做的事情很簡單,形成上述的namesToPlugins,然后執行recomputePluginOrdering,我們接下來看一下recomputePluginOrdering做了寫什么?

  1. const eventPluginOrder = [ 'SimpleEventPlugin' , 'EnterLeaveEventPlugin','ChangeEventPlugin','SelectEventPlugin' , 'BeforeInputEventPlugin' ] 
  2.  
  3. function recomputePluginOrdering(){ 
  4.     for (const pluginName in namesToPlugins) { 
  5.         /* 找到對應的事件處理插件,比如 SimpleEventPlugin  */ 
  6.         const pluginModule = namesToPlugins[pluginName]; 
  7.         const pluginIndex = eventPluginOrder.indexOf(pluginName); 
  8.         /* 填充 plugins 數組  */ 
  9.         plugins[pluginIndex] = pluginModule; 
  10.     } 
  11.  
  12.     const publishedEvents = pluginModule.eventTypes; 
  13.     for (const eventName in publishedEvents) { 
  14.        // publishedEvents[eventName] -> eventConfig , pluginModule -> 事件插件 , eventName -> 事件名稱 
  15.         publishEventForPlugin(publishedEvents[eventName],pluginModule,eventName,) 
  16.     }  
  17. 】 

recomputePluginOrdering,作用很明確了,形成上面說的那個plugins,數組。然后就是重點的函數publishEventForPlugin。

  1. /* 
  2.   dispatchConfig -> 原生事件對應配置項 { phasedRegistrationNames :{  冒泡 捕獲  } ,   } 
  3.   pluginModule -> 事件插件 比如SimpleEventPlugin   
  4.   eventName -> 原生事件名稱。 
  5. */ 
  6. function publishEventForPlugin (dispatchConfig,pluginModule,eventName){ 
  7.     eventNameDispatchConfigs[eventName] = dispatchConfig; 
  8.     /* 事件 */ 
  9.     const phasedRegistrationNames = dispatchConfig.phasedRegistrationNames; 
  10.     if (phasedRegistrationNames) { 
  11.     for (const phaseName in phasedRegistrationNames) { 
  12.         if (phasedRegistrationNames.hasOwnProperty(phaseName)) { 
  13.             // phasedRegistrationName React事件名 比如 onClick / onClickCapture 
  14.             const phasedRegistrationName = phasedRegistrationNames[phaseName]; 
  15.             // 填充形成 registrationNameModules React 合成事件 -> React 處理事件插件映射關系 
  16.             registrationNameModules[phasedRegistrationName] = pluginModule; 
  17.             // 填充形成 registrationNameDependencies React 合成事件 -> 原生事件 映射關系 
  18.             registrationNameDependencies[phasedRegistrationName] = pluginModule.eventTypes[eventName].dependencies; 
  19.         } 
  20.     } 
  21.     return true
  22.     } 

publishEventForPlugin 作用形成上述的 registrationNameModules 和 registrationNameDependencies 對象中的映射關系。

3 事件合成總結

到這里整個初始化階段已經完事了,我來總結一下初始化事件合成都做了些什么。這個階段主要形成了上述的幾個重要對象,構建初始化React合成事件和原生事件的對應關系,合成事件和對應的事件處理插件關系。接下來就是事件綁定階段。

三 事件綁定-從一次點擊事件開始

事件綁定流程

如果我們在一個組件中這么寫一個點擊事件,React會一步步如何處理。

1 diffProperties 處理React合成事件

  1. <div> 
  2.   <button onClick={ this.handerClick }  className="button" >點擊</button> 
  3. </div> 

 

 

第一步,首先通過上面的講解,我們綁定給hostComponent種類的fiber(如上的button元素),會 button 對應的fiber上,以memoizedProps 和 pendingProps形成保存。

  1. button 對應 fiber 
  2. memoizedProps = { 
  3.    onClick:function handerClick(){}, 
  4.    className:'button' 

結構圖如下所示:

58E6A4AF-1902-42BC-9D11-B47234037E01.jpg

第二步,React在調合子節點后,進入diff階段,如果判斷是HostComponent(dom元素)類型的fiber,會用diff props函數diffProperties單獨處理。

  1. react-dom/src/client/ReactDOMComponent.js 
  1. function diffProperties(){ 
  2.     /* 判斷當前的 propKey 是不是 React合成事件 */ 
  3.     if(registrationNameModules.hasOwnProperty(propKey)){ 
  4.          /* 這里多個函數簡化了,如果是合成事件, 傳入成事件名稱 onClick ,向document注冊事件  */ 
  5.          legacyListenToEvent(registrationName, document); 
  6.     } 

diffProperties函數在 diff props 如果發現是合成事件(onClick) 就會調用legacyListenToEvent函數。注冊事件監聽器。

2 legacyListenToEvent 注冊事件監聽器

  1. react-dom/src/events/DOMLegacyEventPluginSystem.js 
  1. //  registrationName -> onClick 事件 
  2. //  mountAt -> document or container 
  3. function legacyListenToEvent(registrationName,mountAt){ 
  4.    const dependencies = registrationNameDependencies[registrationName]; // 根據 onClick 獲取  onClick 依賴的事件數組 [ 'click' ]。 
  5.     for (let i = 0; i < dependencies.length; i++) { 
  6.     const dependency = dependencies[i]; 
  7.     //這個經過多個函數簡化,如果是 click 基礎事件,會走 legacyTrapBubbledEvent ,而且都是按照冒泡處理 
  8.      legacyTrapBubbledEvent(dependency, mountAt); 
  9.   } 

legacyTrapBubbledEvent 就是執行將綁定真正的dom事件的函數 legacyTrapBubbledEvent(冒泡處理)。

  1. function legacyTrapBubbledEvent(topLevelType,element){ 
  2.    addTrappedEventListener(element,topLevelType,PLUGIN_EVENT_SYSTEM,false

第三步:在legacyListenToEvent函數中,先找到 React 合成事件對應的原生事件集合,比如 onClick -> ['click'] , onChange -> [blur , change , input , keydown , keyup],然后遍歷依賴項的數組,綁定事件,這就解釋了,為什么我們在剛開始的demo中,只給元素綁定了一個onChange事件,結果在document上出現很多事件監聽器的原因,就是在這個函數上處理的。

我們上面已經透露了React是采用事件綁定,React 對于 click 等基礎事件,會默認按照事件冒泡階段的事件處理,不過這也不絕對的,比如一些事件的處理,有些特殊的事件是按照事件捕獲處理的。

  1. case TOP_SCROLL: {                                // scroll 事件 
  2.     legacyTrapCapturedEvent(TOP_SCROLL, mountAt); // legacyTrapCapturedEvent 事件捕獲處理。 
  3.     break; 
  4. case TOP_FOCUS: // focus 事件 
  5. case TOP_BLUR:  // blur 事件 
  6. legacyTrapCapturedEvent(TOP_FOCUS, mountAt); 
  7. legacyTrapCapturedEvent(TOP_BLUR, mountAt); 
  8. break; 

3 綁定 dispatchEvent,進行事件監聽

如上述的scroll事件,focus 事件 ,blur事件等,是默認按照事件捕獲邏輯處理。接下來就是最重要關鍵的一步。React是如何綁定事件到document?事件處理函數函數又是什么?問題都指向了上述的addTrappedEventListener,讓我們來揭開它的面紗。

  1. /* 
  2.   targetContainer -> document 
  3.   topLevelType ->  click 
  4.   capture = false 
  5. */ 
  6. function addTrappedEventListener(targetContainer,topLevelType,eventSystemFlags,capture){ 
  7.    const listener = dispatchEvent.bind(null,topLevelType,eventSystemFlags,targetContainer)  
  8.    if(capture){ 
  9.        // 事件捕獲階段處理函數。 
  10.    }else
  11.        /* TODO: 重要, 這里進行真正的事件綁定。*/ 
  12.       targetContainer.addEventListener(topLevelType,listener,false) // document.addEventListener('click',listener,false
  13.    } 

第四步:這個函數內容雖然不多,但是卻非常重要,首先綁定我們的事件統一處理函數 dispatchEvent,綁定幾個默認參數,事件類型 topLevelType demo中的click ,還有綁定的容器doucment。然后真正的事件綁定,添加事件監聽器addEventListener。 事件綁定階段完畢。

4 事件綁定過程總結

我們來做一下事件綁定階段的總結。

① 在React,diff DOM元素類型的fiber的props的時候, 如果發現是React合成事件,比如onClick,會按照事件系統邏輯單獨處理。

② 根據React合成事件類型,找到對應的原生事件的類型,然后調用判斷原生事件類型,大部分事件都按照冒泡邏輯處理,少數事件會按照捕獲邏輯處理(比如scroll事件)。

③ 調用 addTrappedEventListener 進行真正的事件綁定,綁定在document上,dispatchEvent 為統一的事件處理函數。

④ 有一點值得注意: 只有上述那幾個特殊事件比如 scorll,focus,blur等是在事件捕獲階段發生的,其他的都是在事件冒泡階段發生的,無論是onClick還是onClickCapture都是發生在冒泡階段,至于 React 本身怎么處理捕獲邏輯的。我們接下來會講到。

四 事件觸發-一次點擊事件,在react底層系統會發生什么?

  1. <div> 
  2.   <button onClick={ this.handerClick }  className="button" >點擊</button> 
  3. </div> 

 

 

還是上面這段代碼片段,當點擊一下按鈕,在 React 底層會發生什么呢?接下來,讓我共同探索事件觸發的奧秘。

事件觸發處理函數 dispatchEvent

我們在事件綁定階段講過,React事件注冊時候,統一的監聽器dispatchEvent,也就是當我們點擊按鈕之后,首先執行的是dispatchEvent函數,因為dispatchEvent前三個參數已經被bind了進去,所以真正的事件源對象event,被默認綁定成第四個參數。

  1. react-dom/src/events/ReactDOMEventListener.js 
  1. function dispatchEvent(topLevelType,eventSystemFlags,targetContainer,nativeEvent){ 
  2.     /* 嘗試調度事件 */ 
  3.     const blockedOn = attemptToDispatchEvent( topLevelType,eventSystemFlags, targetContainer, nativeEvent); 

在這個階段主要做了這幾件事:

① 首先根據真實的事件源對象,找到 e.target 真實的 dom 元素。

② 然后根據dom元素,找到與它對應的 fiber 對象targetInst,在我們 demo 中,找到 button 按鈕對應的 fiber。

③ 然后正式進去legacy模式的事件處理系統,也就是我們目前用的React模式都是legacy模式下的,在這個模式下,批量更新原理,即將拉開帷幕。

這里有一點問題,React怎么樣通過原生的dom元素,找到對應的fiber的呢? ,也就是說 getClosestInstanceFromNode 原理是什么?

答案是首先 getClosestInstanceFromNode 可以找到當前傳入的 dom 對應的最近的元素類型的 fiber 對象。React 在初始化真實 dom 的時候,用一個隨機的 key internalInstanceKey 指針指向了當前dom對應的fiber對象,fiber對象用stateNode指向了當前的dom元素。

  1. // 聲明隨機key 
  2. var internalInstanceKey = '__reactInternalInstance$' + randomKey; 
  3.  
  4. // 使用隨機key  
  5. function getClosestInstanceFromNode(targetNode){ 
  6.   // targetNode -dom  targetInst -> 與之對應的fiber對象 
  7.   var targetInst = targetNode[internalInstanceKey]; 

在谷歌調試器上看

fiber_dom.jpg

兩者關系圖

dom_fiber.jpg

legacy 事件處理系統與批量更新

  1. react-dom/src/events/DOMLegacyEventPluginSystem.js 
  1. /* topLevelType - click事件 | eventSystemFlags = 1 | nativeEvent = 事件源對象  | targetInst = 元素對應的fiber對象  */ 
  2. function dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst){ 
  3.     /* 從React 事件池中取出一個,將 topLevelType ,targetInst 等屬性賦予給事件  */ 
  4.     const bookKeeping = getTopLevelCallbackBookKeeping(topLevelType,nativeEvent,targetInst,eventSystemFlags); 
  5.     try { /* 執行批量更新 handleTopLevel 為事件處理的主要函數 */ 
  6.     batchedEventUpdates(handleTopLevel, bookKeeping); 
  7.   } finally { 
  8.     /* 釋放事件池 */   
  9.     releaseTopLevelCallbackBookKeeping(bookKeeping); 
  10.   } 

對于v16事件池,我們接下來會講到,首先 batchedEventUpdates為批量更新的主要函數。我們先來看看batchedEventUpdates

  1. react-dom/src/events/ReactDOMUpdateBatching.js 
  1. export function batchedEventUpdates(fn,a){ 
  2.     isBatchingEventUpdates = true
  3.     try{ 
  4.        fn(a) // handleTopLevel(bookKeeping) 
  5.     }finally{ 
  6.         isBatchingEventUpdates = false 
  7.     } 

批量更新簡化成如上的樣子,從上面我們可以看到,React通過開關isBatchingEventUpdates來控制是否啟用批量更新。fn(a),事件上調用的是 handleTopLevel(bookKeeping) ,由于js是單線程的,我們真正在組件中寫的事件處理函數,比如demo 的 handerClick實際執行是在handleTopLevel(bookKeeping)中執行的。所以如果我們在handerClick里面觸發setState,那么就能讀取到isBatchingEventUpdates = true這就是React的合成事件為什么具有批量更新的功能了。比如我們這么寫

  1. state={number:0} 
  2. handerClick = () =>{ 
  3.     this.setState({number: this.state.number + 1   }) 
  4.     console.log(this.state.number) //0 
  5.     this.setState({number: this.state.number + 1   }) 
  6.     console.log(this.state.number) //0 
  7.     setTimeout(()=>{ 
  8.         this.setState({number: this.state.number + 1   }) 
  9.         console.log(this.state.number) //2 
  10.         this.setState({number: this.state.number + 1   }) 
  11.         console.log(this.state.number)// 3 
  12.     }) 

如上述所示,第一個setState和第二個setState在批量更新條件之內執行,所以打印不會是最新的值,但是如果是發生在setTimeout中,由于eventLoop 放在了下一次事件循環中執行,此時 batchedEventUpdates 中已經執行完isBatchingEventUpdates = false,所以批量更新被打破,我們就可以直接訪問到最新變化的值了。

接下來我們有兩點沒有梳理:

  • 一是React事件池概念
  • 二是最后的線索是執行handleTopLevel(bookKeeping),那么handleTopLevel到底做了寫什么。

執行事件插件函數

上面說到整個事件系統,最后指向函數 handleTopLevel(bookKeeping) 那么 handleTopLevel 到底做了什么事情?

  1. // 流程簡化后 
  2. // topLevelType - click   
  3. // targetInst - button Fiber 
  4. // nativeEvent 
  5. function handleTopLevel(bookKeeping){ 
  6.     const { topLevelType,targetInst,nativeEvent,eventTarget, eventSystemFlags} = bookKeeping 
  7.     for(let i=0; i < plugins.length;i++ ){ 
  8.         const possiblePlugin = plugins[i]; 
  9.         /* 找到對應的事件插件,形成對應的合成event,形成事件執行隊列  */ 
  10.         const  extractedEvents = possiblePlugin.extractEvents(topLevelType,targetInst,nativeEvent,eventTarget,eventSystemFlags)   
  11.     } 
  12.     if (extractedEvents) { 
  13.         events = accumulateInto(events, extractedEvents); 
  14.     } 
  15.     /* 執行事件處理函數 */ 
  16.     runEventsInBatch(events); 

我把整個流程簡化,只保留了核心的流程,handleTopLevel最后的處理邏輯就是執行我們說的事件處理插件(SimpleEventPlugin)中的處理函數extractEvents,比如我們demo中的點擊事件 onClick 最終走的就是 SimpleEventPlugin 中的 extractEvents 函數,那么React為什么這么做呢? 我們知道我們React是采取事件合成,事件統一綁定,并且我們寫在組件中的事件處理函數( handerClick ),也不是真正的執行函數dispatchAciton,那么我們在handerClick的事件對象 event,也是React單獨合成處理的,里面單獨封裝了比如 stopPropagation和preventDefault等方法,這樣的好處是,我們不需要跨瀏覽器單獨處理兼容問題,交給React底層統一處理。

extractEvents 形成事件對象event 和 事件處理函數隊列

重點來了!重點來了!重點來了!,extractEvents 可以作為整個事件系統核心函數,我們先回到最初的demo,如果我們這么寫,那么四個回調函數,那么點擊按鈕,四個事件是如何處理的呢。首先如果點擊按鈕,最終走的就是extractEvents函數,一探究竟這個函數。

  1. legacy-events/SyntheticEvent.js 
  1. const  SimpleEventPlugin = { 
  2.     extractEvents:function(topLevelType,targetInst,nativeEvent,nativeEventTarget){ 
  3.         const dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType); 
  4.         if (!dispatchConfig) { 
  5.             return null
  6.         } 
  7.         switch(topLevelType){ 
  8.             default
  9.             EventConstructor = SyntheticEvent; 
  10.             break; 
  11.         } 
  12.         /* 產生事件源對象 */ 
  13.         const event = EventConstructor.getPooled(dispatchConfig,targetInst,nativeEvent,nativeEventTarget) 
  14.         const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames; 
  15.         const dispatchListeners = []; 
  16.         const {bubbled, captured} = phasedRegistrationNames; /* onClick / onClickCapture */ 
  17.         const dispatchInstances = []; 
  18.         /* 從事件源開始逐漸向上,查找dom元素類型HostComponent對應的fiber ,收集上面的React合成事件,onClick / onClickCapture  */ 
  19.          while (instance !== null) { 
  20.               const {stateNode, tag} = instance; 
  21.               if (tag === HostComponent && stateNode !== null) { /* DOM 元素 */ 
  22.                    const currentTarget = stateNode; 
  23.                    if (captured !== null) { /* 事件捕獲 */ 
  24.                         /* 在事件捕獲階段,真正的事件處理函數 */ 
  25.                         const captureListener = getListener(instance, captured); 
  26.                         if (captureListener != null) { 
  27.                         /* 對應發生在事件捕獲階段的處理函數,邏輯是將執行函數unshift添加到隊列的最前面 */ 
  28.                             dispatchListeners.unshift(captureListener); 
  29.                             dispatchInstances.unshift(instance); 
  30.                             dispatchCurrentTargets.unshift(currentTarget); 
  31.                         } 
  32.                     } 
  33.                     if (bubbled !== null) { /* 事件冒泡 */ 
  34.                         /* 事件冒泡階段,真正的事件處理函數,邏輯是將執行函數push到執行隊列的最后面 */ 
  35.                         const bubbleListener = getListener(instance, bubbled); 
  36.                         if (bubbleListener != null) { 
  37.                             dispatchListeners.push(bubbleListener); 
  38.                             dispatchInstances.push(instance); 
  39.                             dispatchCurrentTargets.push(currentTarget); 
  40.                         } 
  41.                     } 
  42.               } 
  43.               instance = instance.return
  44.          } 
  45.           if (dispatchListeners.length > 0) { 
  46.               /* 將函數執行隊列,掛到事件對象event上 */ 
  47.             event._dispatchListeners = dispatchListeners; 
  48.             event._dispatchInstances = dispatchInstances; 
  49.             event._dispatchCurrentTargets = dispatchCurrentTargets; 
  50.          } 
  51.         return event 
  52.     } 

事件插件系統的核心extractEvents主要做的事是:

① 首先形成React事件獨有的合成事件源對象,這個對象,保存了整個事件的信息。將作為參數傳遞給真正的事件處理函數(handerClick)。

② 然后聲明事件執行隊列 ,按照冒泡和捕獲邏輯,從事件源開始逐漸向上,查找dom元素類型HostComponent對應的fiber ,收集上面的 React 合成事件,例如 onClick / onClickCapture ,對于冒泡階段的事件(onClick),將 push 到執行隊列后面 , 對于捕獲階段的事件(onClickCapture),將 unShift到執行隊列的前面。

③ 最后將事件執行隊列,保存到React事件源對象上。等待執行。

舉個例子比如如下

  1. handerClick = () => console.log(1) 
  2. handerClick1 = () => console.log(2) 
  3. handerClick2 = () => console.log(3)  
  4. handerClick3= () => console.log(4) 
  5. render(){ 
  6.     return <div onClick={ this.handerClick2 } onClickCapture={this.handerClick3}  >  
  7.         <button onClick={ this.handerClick }  onClickCapture={ this.handerClick1  }  className="button" >點擊</button> 
  8.     </div> 

看到這里我們應該知道上述函數打印順序為什么了吧,首先遍歷 button 對應的fiber,首先遇到了 onClickCapture ,將 handerClick1 放到了數組最前面,然后又把onClick對應handerClick的放到數組的最后面,形成的結構是[ handerClick1 , handerClick ] , 然后向上遍歷,遇到了div對應fiber,將onClickCapture對應的handerClick3放在了數組前面,將onClick對應的 handerClick2 放在了數組后面,形成的結構 [ handerClick3,handerClick1 , handerClick,handerClick2 ] ,所以執行的順序 // 4 2 1 3,就是這么簡單,完美!

FDEBA681-2E03-420B-A838-5907439837A9.jpg

事件觸發

有的同學可能好奇React的事件源對象是什么樣的,以上面代碼中SyntheticEvent為例子我們一起來看看:

  1. legacy-events/SyntheticEvent.js/ 
  1. function SyntheticEvent( dispatchConfig,targetInst,nativeEvent,nativeEventTarget){ 
  2.   this.dispatchConfig = dispatchConfig; 
  3.   this._targetInst = targetInst; 
  4.   this.nativeEvent = nativeEvent; 
  5.   this._dispatchListeners = null
  6.   this._dispatchInstances = null
  7.   this._dispatchCurrentTargets = null
  8.   this.isPropagationStopped = () => false; /* 初始化,返回為false  */ 
  9.  
  10. SyntheticEvent.prototype={ 
  11.     stopPropagation(){ this.isPropagationStopped = () => true;  }, /* React單獨處理,阻止事件冒泡函數 */ 
  12.     preventDefault(){ },  /* React單獨處理,阻止事件捕獲函數  */ 
  13.     ... 

在 handerClick 中打印 e :

B9180401-93FF-4EF0-A2FB-C2FA43B29550.jpg

既然事件執行隊列和事件源對象都形成了,接下來就是最后一步事件觸發了。上面大家有沒有注意到一個函數runEventsInBatch,所有事件綁定函數,就是在這里觸發的。讓我們一起看看。

  1. legacy-events/EventBatching.js 
  1. function runEventsInBatch(){ 
  2.     const dispatchListeners = event._dispatchListeners; 
  3.     const dispatchInstances = event._dispatchInstances; 
  4.     if (Array.isArray(dispatchListeners)) { 
  5.     for (let i = 0; i < dispatchListeners.length; i++) { 
  6.       if (event.isPropagationStopped()) { /* 判斷是否已經阻止事件冒泡 */ 
  7.         break; 
  8.       } 
  9.        
  10.       dispatchListeners[i](event) 
  11.     } 
  12.   } 
  13.   /* 執行完函數,置空兩字段 */ 
  14.   event._dispatchListeners = null
  15.   event._dispatchInstances = null

dispatchListeners[i](event)就是執行我們的事件處理函數比如handerClick,從這里我們知道,我們在事件處理函數中,返回 false ,并不會阻止瀏覽器默認行為。

  1. handerClick(){ //并不能阻止瀏覽器默認行為。 
  2.     return false 

應該改成這樣:

  1. handerClick(e){ 
  2.     e.preventDefault() 

另一方面React對于阻止冒泡,就是通過isPropagationStopped,判斷是否已經阻止事件冒泡。如果我們在事件函數執行隊列中,某一會函數中,調用e.stopPropagation(),就會賦值給isPropagationStopped=()=>true,當再執行 e.isPropagationStopped()就會返回 true ,接下來事件處理函數,就不會執行了。

其他概念-事件池

  1. handerClick = (e) => { 
  2.    console.log(e.target) // button  
  3.    setTimeout(()=>{ 
  4.        console.log(e.target) // null 
  5.    },0) 

對于一次點擊事件的處理函數,在正常的函數執行上下文中打印e.target就指向了dom元素,但是在setTimeout中打印卻是null,如果這不是React事件系統,兩次打印的應該是一樣的,但是為什么兩次打印不一樣呢?因為在React采取了一個事件池的概念,每次我們用的事件源對象,在事件函數執行之后,可以通過releaseTopLevelCallbackBookKeeping等方法將事件源對象釋放到事件池中,這樣的好處每次我們不必再創建事件源對象,可以從事件池中取出一個事件源對象進行復用,在事件處理函數執行完畢后,會釋放事件源到事件池中,清空屬性,這就是setTimeout中打印為什么是null的原因了。

事件觸發總結

我把事件觸發階段做的事總結一下:

①首先通過統一的事件處理函數 dispatchEvent,進行批量更新batchUpdate。

②然后執行事件對應的處理插件中的extractEvents,合成事件源對象,每次React會從事件源開始,從上遍歷類型為 hostComponent即 dom類型的fiber,判斷props中是否有當前事件比如onClick,最終形成一個事件執行隊列,React就是用這個隊列,來模擬事件捕獲->事件源->事件冒泡這一過程。

③最后通過runEventsInBatch執行事件隊列,如果發現阻止冒泡,那么break跳出循環,最后重置事件源,放回到事件池中,完成整個流程。

evnent_click.jpg

五 關于react v17版本的事件系統

React v17 整體改動不是很大,但是事件系統的改動卻不小,首先上述的很多執行函數,在v17版本不復存在了。我來簡單描述一下v17事件系統的改版。

1 事件統一綁定container上,ReactDOM.render(app, container);而不是document上,這樣好處是有利于微前端的,微前端一個前端系統中可能有多個應用,如果繼續采取全部綁定在document上,那么可能多應用下會出現問題。

react_17_delegation.png

2 對齊原生瀏覽器事件

React 17中終于支持了原生捕獲事件的支持, 對齊了瀏覽器原生標準。同時 onScroll 事件不再進行事件冒泡。onFocus 和 onBlur 使用原生 focusin, focusout 合成。

3 取消事件池React 17取消事件池復用,也就解決了上述在setTimeout打印,找不到e.target的問題。

六 總結

本文從事件合成,事件綁定,事件觸發三個方面詳細介紹了React事件系統原理,希望大家能通過這篇文章更加深入了解v16 React 事件系統,如果有疑問和不足之處,也希望大家能在評論區指出。

 

責任編輯:武曉燕 來源: 前端Sharing
相關推薦

2021-10-15 14:28:30

React 組件渲染

2021-08-30 19:04:29

jsIO

2025-08-08 01:11:00

React組件通信

2025-04-09 05:22:00

2025-11-03 01:00:00

2024-09-18 13:57:15

2021-06-07 08:41:59

React異步組件

2025-06-27 06:00:00

智能體AgentReAct

2024-08-09 08:41:14

2025-02-03 07:00:00

Java接口工具

2023-08-27 21:29:43

JVMFullGC調優

2023-09-08 08:20:46

ThreadLoca多線程工具

2022-08-15 17:34:22

react-routv6

2024-08-26 08:58:50

2025-06-05 03:11:00

2020-02-21 14:35:57

JavaScript繼承前端

2024-01-09 08:24:47

JMM核心線程

2021-01-13 05:21:59

參數

2021-12-16 14:45:09

https架構服務端

2024-07-12 14:46:20

點贊
收藏

51CTO技術棧公眾號

国产精品激情偷乱一区二区∴| 欧美综合国产| 日韩av网址在线观看| 91看片就是不一样| 黄色网在线免费观看| 国产超碰在线一区| 国产精品777| 青青草手机在线视频| 伊人久久大香线蕉无限次| 欧美精品一二三区| 91视频最新入口| gogo在线高清视频| 国产色一区二区| 国产福利久久精品| 亚洲天堂aaa| 99精品国产在热久久婷婷| 日韩视频―中文字幕| 亚洲永久无码7777kkk| 国模大尺度视频一区二区| 日韩欧美在线播放| www.av91| 麻豆视频免费在线观看| 久久亚区不卡日本| 国产精品yjizz| 中文字幕 自拍偷拍| 一本色道精品久久一区二区三区 | 亚洲一区欧美一区| 少妇精品久久久久久久久久| 少妇精品视频一区二区| 国产精品一区二区久激情瑜伽| 茄子视频成人在线| 九九热国产视频| 亚洲人metart人体| 中文字幕久久亚洲| 亚洲黄色免费视频| 亚洲激情播播| 日韩av在线免费| 国产av一区二区三区传媒| 国产精品高清一区二区| 欧美三级三级三级爽爽爽| 妺妺窝人体色www在线小说| 狂野欧美性猛交xxxxx视频| 国产精品久久久久一区| 日韩欧美视频一区二区| 九色网友自拍视频手机在线| 99久久精品一区| 国产成人一区二区三区免费看| 国产女人高潮毛片| 极品少妇xxxx精品少妇| 国产免费一区二区三区香蕉精| 伊人成年综合网| 日韩电影免费在线看| 国产精品2018| 中国a一片一级一片| 日本 国产 欧美色综合| 国产精品久久久久久久天堂| 中国黄色一级视频| 久久成人免费网| 成人av色在线观看| a天堂视频在线| 国产成人精品免费网站| 国产精品一区二区免费| 日韩中文字幕免费观看| wwww国产精品欧美| 色综合久久av| 欧美videos极品另类| 自拍偷拍欧美精品| 国产成人在线小视频| 国内激情视频在线观看| 欧美日韩综合视频| 波多野结衣作品集| 亚洲精品第一| 日韩三级免费观看| 久久久国产精品无码| 亚洲精品国模| www.欧美精品一二三区| 免费在线观看国产精品| 亚洲一区二区三区四区五区午夜 | 成年人观看网站| 日日夜夜天天综合| 在线电影国产精品| 污片免费在线观看| 欧美日韩在线二区| 欧美第一黄色网| 欧美黑人一区二区| 狠狠色丁香久久婷婷综合_中 | 国产a久久精品一区二区三区| 一区二区欧美久久| 麻豆亚洲av熟女国产一区二| 亚洲一区中文| 成人综合网网址| 亚洲av成人无码久久精品老人| 欧美激情在线一区二区| 日韩亚洲欧美一区二区| 三上悠亚亚洲一区| 日韩欧美一区电影| 熟女高潮一区二区三区| 综合久久99| 日av在线播放中文不卡| 国产成人精品无码高潮| 久久精品一区八戒影视| 亚洲一区 在线播放| 欧美极度另类| 欧美电影精品一区二区| 99精品全国免费观看| 亚洲欧洲日本mm| 国产精品美乳一区二区免费| 后进极品白嫩翘臀在线视频| 国产精品欧美久久久久无广告| av无码久久久久久不卡网站| 欧美视频第一| 亚洲毛茸茸少妇高潮呻吟| 欧美成人精品一区二区免费看片| 老妇喷水一区二区三区| 国产精品毛片一区视频| 日韩在线观看www| 狠狠久久五月精品中文字幕| 久草福利在线观看| 日韩欧美视频在线播放| 欧美一区二区色| 成人免费视频国产| 亚洲靠逼com| 亚洲欧洲日本精品| 国产91久久精品一区二区| 国内精品久久久| 国内精品国产成人国产三级| 国产精品久久久爽爽爽麻豆色哟哟| 国产91对白刺激露脸在线观看| 都市激情亚洲| 欧美激情视频一区二区三区不卡| 中文字幕激情视频| 国产欧美日韩精品在线| 精品视频无码一区二区三区| 希岛爱理av免费一区二区| 97免费视频在线播放| 亚洲精品国产suv一区| 亚洲免费观看在线视频| 欧美一级小视频| 欧美疯狂party性派对| 国产精品网站视频| 成全电影播放在线观看国语| 一本久道中文字幕精品亚洲嫩| 日本一区二区在线免费观看| 日韩视频不卡| 久久人人九九| 欧美成人h版| 亚洲色图偷窥自拍| 啪啪小视频网站| 国产精品无遮挡| 三级视频中文字幕| 久久精品国产68国产精品亚洲| 国产精品美女主播| 国产在线观看a| 91精品国产色综合久久久蜜香臀| 日韩影院一区二区| 国产成人精品影院| 日韩中文字幕在线免费| 欧美wwwwww| 国产成人一区二区三区小说 | 国产精品白浆| 97色在线视频| 欧美日韩激情视频一区二区三区| 日韩欧美综合在线视频| 美女100%露胸无遮挡| 美女视频黄 久久| 99re6这里有精品热视频| 99re8这里有精品热视频免费| 欧美精品久久久久久久免费观看 | 99久久精品国产毛片| 日韩欧美国产免费| 欧美色图一区| 成人a视频在线观看| 任你弄在线视频免费观看| 亚洲精品97久久| 波多野结衣在线观看视频| 国产精品国产三级国产三级人妇| 26uuu国产| 久久久久91| 最新欧美日韩亚洲| 国产精品nxnn| 国产精品色视频| 中文字幕伦理免费在线视频 | 欧美日免费三级在线| www.超碰在线观看| 91一区二区三区在线观看| 三级视频中文字幕| 亚洲精品看片| 亚洲天堂电影网| 加勒比色老久久爱综合网| 国产精品第2页| 日本一级理论片在线大全| 精品亚洲国产成av人片传媒 | 中文字幕一区二区三区在线乱码 | 中文字字幕在线中文| 国产精品嫩草久久久久| 国产女主播在线播放| 蜜桃一区二区三区在线| 少妇一晚三次一区二区三区| 九九在线精品| 国产91视觉| 91av一区| 7m精品福利视频导航| 国产剧情在线| 国产亚洲精品日韩| 色丁香婷婷综合久久| 91麻豆精品国产91久久久资源速度 | 亚洲男女一区二区三区| 亚洲无人区码一码二码三码的含义| 国产精品自产自拍| 蜜桃免费在线视频| 国产欧美精品久久| 国产欧美123| 久久电影院7| 视频一区二区三区免费观看| 成人在线超碰| 亚洲专区在线视频| 成人av色网站| 国产成人免费av| 在线人成日本视频| 欧美激情视频一区二区三区不卡| 日本福利在线| 亚洲最新视频在线| 欧洲一区av| 日韩av网站导航| 天堂在线观看免费视频| 日韩欧美国产一区二区三区| 亚洲一二区视频| 在线欧美一区二区| 激情视频网站在线观看| 天天操天天综合网| 日韩精品久久久久久久| 亚洲一区二区三区中文字幕 | 懂色av影视一区二区三区| 久久久综合久久| 一区二区三区日韩精品| 国产稀缺精品盗摄盗拍| 国产精品灌醉下药二区| 欧美亚洲色综久久精品国产| 国产无人区一区二区三区| 欧美成人午夜精品免费| 久久香蕉国产线看观看99| 色欲av无码一区二区三区| 2023国产精品| 黄色aaa视频| 久久精品亚洲乱码伦伦中文| 成人精品999| 国产日韩成人精品| 一级二级黄色片| 国产精品美女久久久久aⅴ国产馆 国产精品美女久久久久av爽李琼 国产精品美女久久久久高潮 | 亚洲一卡二卡三卡| 天天色天天射综合网| 精品91一区二区三区| 欧美国产三级| www.xxx麻豆| 国产日韩欧美三级| 久久久久久久久久久久久国产精品| 六月丁香综合| www.久久91| 国产激情一区二区三区四区 | 欧美国产视频在线| 成人免费视频入口| 亚洲欧美视频在线观看视频| 九九热视频精品| 天天色天天操综合| 正在播放木下凛凛xv99| 91精品国产综合久久精品app| 精品久久久久久亚洲综合网站| 精品国产免费一区二区三区香蕉| 婷婷丁香花五月天| 亚洲网在线观看| 免费网站成人| 久久男人资源视频| 日本不卡一二三| 成人精品一区二区三区电影黑人| 在线一区二区三区视频| 久久综合婷婷综合| 四虎成人精品永久免费av九九| 少妇久久久久久被弄到高潮| 男女av一区三区二区色多| а 天堂 在线| 不卡的av在线| 天天干天天操天天拍| 亚洲一区二区精品视频| 久久久久99精品成人片我成大片| 欧美日韩一本到| 亚洲欧美另类视频| 最近免费中文字幕视频2019| 欧美亚洲系列| 国产精品久久视频| 国产精品x8x8一区二区| 亚洲一区二区在线免费观看| 亚洲国产精品一区| 91极品视频在线观看| 不卡电影免费在线播放一区| 黄色免费一级视频| 亚洲成人午夜影院| 国产又黄又猛又爽| 亚洲欧美日韩爽爽影院| 中文在线观看免费| 国产成人精品优优av| swag国产精品一区二区| 亚洲高清123| 亚洲欧美日韩国产| av在线天堂网| 亚洲欧美综合在线精品| 欧美一区二区三区不卡视频| 精品少妇一区二区三区日产乱码| 成人h小游戏| 欧美亚洲日本黄色| 136导航精品福利| 伊人情人网综合| 久久国产99| 北京富婆泄欲对白| 亚洲人成网站精品片在线观看| 日日夜夜操视频| 亚洲黄色www网站| 自拍亚洲图区| 91网站在线免费观看| 黄色不卡一区| 成人黄色片视频| 成人免费视频视频| 青青草成人免费| 3d动漫精品啪啪一区二区竹菊| 国产乱视频在线观看| 国产69久久精品成人| 国产一区二区三区亚洲| 久久免费一级片| 国产精品中文字幕日韩精品| 国产真实乱在线更新| 欧美日韩国产高清一区| 成av人电影在线观看| 国产精品久久久久久久久久久不卡 | 日韩欧美亚洲视频| 亚洲成人性视频| 欧美hdxxxx| 97久久精品午夜一区二区| 91精品天堂福利在线观看| 欧美伦理片在线观看| 欧美经典一区二区三区| 免费黄色av片| 尤物yw午夜国产精品视频| 欧美日韩不卡| 日韩精品一区二区三区丰满 | 污免费在线观看| 亚洲日穴在线视频| a天堂在线观看视频| 色综合久久中文字幕综合网小说| 成人综合日日夜夜| 青青视频免费在线| 国产不卡免费视频| 国产精品成人免费一区二区视频| 欧美精品一区二区三区在线| 2020国产在线| 久久久久久久久一区二区| 国产日韩亚洲| 亚洲码无人客一区二区三区| 欧美亚一区二区| 麻豆网在线观看| 国产成人av一区二区三区| 亚洲一区成人| 九一在线免费观看| 欧美一区二区成人6969| 美足av综合网| 精品一区国产| 巨乳诱惑日韩免费av| 久久久久久久久久97| 日韩一区二区麻豆国产| 俺来俺也去www色在线观看| 91精品麻豆日日躁夜夜躁| 久久黄色美女电影| 成人欧美一区二区三区视频| 国产欧美亚洲一区| 久久午夜精品视频| 日韩无一区二区| 亚洲精品**中文毛片| 亚洲一区二区三区欧美| 国产成人精品亚洲777人妖| 久久亚洲精品国产| 少妇高潮久久77777| 66精品视频在线观看| 久久国产色av免费观看| 亚洲美女免费视频| 欧美视频免费一区二区三区| 国产中文字幕亚洲| 亚洲高清av| 激情高潮到大叫狂喷水| 亚洲国产成人精品久久久国产成人一区| 亚洲最大网站| 在线观看18视频网站| 久久只精品国产| 99热这里只有精品3| 欧洲精品毛片网站| 在线成人直播| 一级黄色性视频| 欧美成人一区二区三区在线观看| 日韩新的三级电影| 国产日本在线播放| 国产精品免费视频网站| 天天干在线观看| 91亚洲精品在线|