百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

「React进阶系列」史上最全React事件机制详解

zzlvtu 2024-09-04 23:02 4 浏览

框架总览


  • DOM事件流的三个阶段
  • 关于React事件的疑问
  • React事件绑定机制
  • React事件和原生事件有什么区别
  • React事件和原生事件的执行顺序,可以混用吗
  • React事件如何解决跨浏览器兼容
  • React stopPropagation 与 stopImmediatePropagation
  • 从React的事件机制源码看整个流程
  • 基本流程
  • 事件注册
  • 事件触发
  • 总结
  • 站在巨人肩上

DOM事件流的三个阶段

1、事件捕获阶段

当某个事件触发时,文档根节点最先接受到事件,然后根据DOM树结构向具体绑定事件的元素传递。该阶段为父元素截获事件提供了机会。

事件传递路径为:

window —> document —> boy —> div—> text

2、目标阶段

具体元素已经捕获事件。之后事件开始向根节点冒泡。

3、事件冒泡阶段

该阶段的开始即是事件的开始,根据DOM树结构由具体触发事件的元素向根节点传递。

事件传递路径:

text—> div —> body —> document —> window

使用addEventListener函数在事件流的的不同阶段监听事件。

DOMEle.addEventListener(‘事件名称’,handleFn,Boolean);

此处第三个参数Boolean即代表监听事件的阶段;

为true时,在在捕获阶段监听事件,执行逻辑处理;

为false时,在冒泡阶段监听事件,执行逻辑处理。

关于React事件的疑问

1.React事件绑定机制

考虑到浏览器的兼容性和性能问题,React 基于 Virtual DOM 实现了一个SyntheticEvent(合成事件)层,我们所定义的事件处理器会接收到一个SyntheticEvent对象的实例。与原生事件直接在元素上注册的方式不同的是,react的合成事件不会直接绑定到目标dom节点上,用事件委托机制,以队列的方式,从触发事件的组件向父组件回溯直到document节点,因此React组件上声明的事件最终绑定到了document 上。用一个统一的监听器去监听,这个监听器上保存着目标节点与事件对象的映射,当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象;当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做的好处:

1.减少内存消耗,提升性能,不需要注册那么多的事件了,一种事件类型只在 document 上注册一次

2.统一规范,解决 ie 事件兼容问题,简化事件逻辑

3.对开发者友好

React Event的主要四个文件是 ReactBrowerEventEmitter.js(负责节点绑定的回调函数,该回调函数执行过程中构建合成事件对象,获取组件实例的绑定回调并执行,若有state变更,则重绘组件),ReactEventListener.js(负责事件注册和事件分发), ReactEventEmitter(负责事件的执行),EventPluginHub.js(负责事件的存储)和ReactEventEmitterMixin.js(负责事件的合成)。

2. React事件和原生事件有什么区别

带着问题用以下用代码来展示两者的区别:

  1. 点击button,最后的输出顺序是什么?
  2. B,G 处的type都是啥?
export default class Test extends React.Component {

    componentDidMount() {

        document.querySelector('#btn').addEventListener('click', (e) => {

            console.log('A inner listener')

            setTimeout(() => {

                console.log('B inner listener timer', e.type)

            })

        })

        document.body.addEventListener('click', (e) => {

            console.log('C document listener')

        })

        window.addEventListener('click', (e) => {

            console.log('D window listener')

        })

    }

    outClick(e) {

        setTimeout(() => {

            console.log('E out timer', e.type)

        })

        console.log('F out e', e.type)

    }

    innerClick = (e) => {

        console.log('G inner e',e.type)

        e.stopPropagation()

    }

    render() {

        return (

            <div onClick={this.outClick}>

                <button id="btn" onClick={this.innerClick}>点我</button>

            </div>

        )

    }

}
1. 最后的输出顺序为 A C G B

2. B处的type为click,而G处的type为null
响应过程(对应第一问)

我们参照上题,详细说一下事件的响应过程:

由于我们写的几个监听事件addEventListener,都没有给第三个参数,默认值为false,所以在事件捕获阶段,原生的监听事件没有响应,react合成事件只实现了事件冒泡。所以在捕获阶段没有事件响应。

接着到了事件绑定的阶段,button上挂载了原生事件,于是输出"A",setTimeout中的"B"则进入EVENT LOOP。在上一段中,我们提到react的合成事件是挂载到document上,所以“G”没有输出。

之后进入冒泡阶段,到了div上,与上条同理,不会响应outClick,继续向上冒泡。

之后冒泡到了document上,先响应挂载到document的原生事件,输出"c"。之后接着由里向外响应合成事件队列,即输出"G",由于innerClick函数内设置了e.stopPropagation()。所以阻止了冒泡,父元素的事件响应函数没有执行。React合成事件执行e.stopPropagation()不会影响document层级之前的原生事件冒泡。但是会影响document之后的原生事件。所以没有执行body的事件响应函数。之后再处理EVENT LOOP上的事件,输出'B''.

事件池(对应第二问)

在react中,合成事件被调用后,合成事件对象会被重用,所有属性被置为null

event.constructor.release(event);

所以题目中outClick中通过异步方式访问e.type是取不到任何值的,如果需要保留属性,可以调用event.persist()事件,会保留引用。

总结

(1)命名规范不同

React事件的属性名是采用驼峰形式的,事件处理函数是一个函数;

原生事件通过addEventListener给事件添加事件处理函数

(2)React事件只支持事件冒泡。原生事件通过配置第三个参数,true为事件捕获,false为事件冒泡

(3)事件挂载目标不同

React事件统一挂载到document上;

原生事件挂载到具体的DOM上

(4)this指向不同

原生事件:

1.如果onevent事件属性定义的时候将this作为参数,在函数中获取到该参数是DOM对象。用该方法可以获取当前DOM。

2在方法中直接访问this, this指向当前函数所在的作用域。或者说调用函数的对象。

React事件:

React中this指向一般都期望指向当前组件,如果不绑定this,this一般等于undefined。

React事件需要手动为其绑定this具体原因可以参考文章: 为什么需要在 React 类组件中为事件处理程序绑定 this

(5)事件对象不同

原生js中事件对象是原生事件对象,它存在浏览器兼容性,需要用户自己处理各浏览器兼容问题;

ReactJS中的事件对象是React将原生事件对象(event)进行了跨浏览器包装过的合成事件(SyntheticEvent)。

为了性能考虑,执行完后,合成事件的事件属性将不能再访问

React事件和原生事件的执行顺序,可以混用吗

由上面的代码我们可以理解:

react的所有事件都挂载在document中

当真实dom触发后冒泡到document后才会对react事件进行处理

所以原生的事件会先执行

然后执行react合成事件

最后执行真正在document上挂载的事件

不要将合成事件与原生事件混用。执行React事对象件的e.stopPropagation()可以阻止React事件冒泡。但是不能阻止原生事件冒泡;反之,在原生事件中的阻止冒泡行为,却可以阻止 React 合成事件的传播。因为无法将事件冒泡到document上导致的

React事件如何解决跨浏览器兼容

react事件在给document注册事件的时候也是对兼容性做了处理。

上面这个代码就是给document注册事件,内部其实也是做了对ie浏览器的兼容做了处理。

其实react内部还处理了很多,比如react合成事件:

React 根据 W3C 规范 定义了这个合成事件,所以你不需要担心跨浏览器的兼容性问题。

事件处理程序将传递 SyntheticEvent 的实例,这是一个跨浏览器原生事件包装器。 它具有与浏览器原生事件相同的接口,包括stopPropagation() 和 preventDefault() ,在所有浏览器中他们工作方式都相同。

每个SyntheticEvent对象都具有以下属性:

| 属性名 | 类型 | 描述 |

| ---- | ---- | ---- |

| bubbles | boolean | 事件是否可冒泡|

| cancelable| boolean | 事件是否可拥有取消的默认动作|

| currentTarget | DOMEventTarget| 事件监听器触发该事件的元素(绑定事件的元素)|

| defaultPrevented | boolean| 当前事件是否调用了 event.preventDefault()方法|

| eventPhase| number | 事件传播的所处阶段[0:Event.NONE-没有事件被处理,1:Event.CAPTURING_PHASE - 捕获阶段,2:被目标元素处理,3:冒泡阶段(Event.bubbles为true时才会发生)]|

| isTrusted| boolean| 触发是否来自于用户行为,false为脚本触发|

| nativeEvent | DOMEvent| 浏览器原生事件|

| preventDefault()| void | 阻止事件的默认行为|

| isDefaultPrevented() | boolean | 返回的事件对象上是否调用了preventDefault()方法|

| stopPropagation()| void| 阻止冒泡|

| isPropagationStopped() | boolean | 返回的事件对象上是否调用了stopPropagation()方法|

| target | DOMEventTarget| 触发事件的元素|

| timeStamp | number| 事件生成的日期和时间|

| type | string | 当前 Event 对象表示的事件的名称,是注册事件的句柄,如,click、mouseover…etc.|

React合成的SyntheticEvent采用了事件池,这样做可以大大节省内存,而不会频繁的创建和销毁事件对象。

另外,不管在什么浏览器环境下,浏览器会将该事件类型统一创建为合成事件,从而达到了浏览器兼容的目的。

React stopPropagation 与 stopImmediatePropagation

React 合成事件与原生事件执行顺序图:

从图中我们可以得到一下结论:

(1)DOM 事件冒泡到document上才会触发React的合成事件,所以React 合成事件对象的e.stopPropagation,只能阻止 React 模拟的事件冒泡,并不能阻止真实的 DOM 事件冒泡

(2)DOM 事件的阻止冒泡也可以阻止合成事件原因是DOM 事件的阻止冒泡使事件不会传播到document上

(3)当合成事件和DOM 事件 都绑定在document上的时候,React的处理是合成事件应该是先放进去的所以会先触发,在这种情况下,原生事件对象的 stopImmediatePropagation能做到阻止进一步触发document DOM事件

stopImmediatePropagation :如果有多个相同类型事件的事件监听函数绑定到同一个元素,则当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation()方法,则剩下的监听函数将不会被执行。


从React的事件机制源码看整个流程

基本流程

在 react源码的 react-dom/src/events/ReactBrowserEventEmitter.js文件的开头,有这么一大段注释:

/**

 * Summary of `ReactBrowserEventEmitter` event handling:

 *

 *  - Top-level delegation is used to ......

 * ......

 *

 * +------------+    .

 * |    DOM     |    .

 * +------------+    .

 *       |           .

 *       v           .

 * +------------+    .

 * | ReactEvent |    .

 * |  Listener  |    .

 * +------------+    .                         +-----------+

 *       |           .               +--------+|SimpleEvent|

 *       |           .               |         |Plugin     |

 * +-----|------+    .               v         +-----------+

 * |     |      |    .    +--------------+                    +------------+

 * |     +-----------.--->|EventPluginHub|                    |    Event   |

 * |            |    .    |              |     +-----------+  | Propagators|

 * | ReactEvent |    .    |              |     |TapEvent   |  |------------|

 * |  Emitter   |    .    |              |<---+|Plugin     |  |other plugin|

 * |            |    .    |              |     +-----------+  |  utilities |

 * |     +-----------.--->|              |                    +------------+

 * |     |      |    .    +--------------+

 * +-----|------+    .                ^        +-----------+

 *       |           .                |        |Enter/Leave|

 *       +           .                +-------+|Plugin     |

 * +-------------+   .                         +-----------+

 * | application |   .

 * |-------------|   .

 * |             |   .

 * |             |   .

 * +-------------+   .

 *                   .

 *    React Core     .  General Purpose Event Plugin System

 */

这段注释是在大概描述 React的事件机制,也就是这个文件中的代码要做的一些事情,大概意思就是说事件委托是很常用的一种浏览器事件优化策略,于是 React就接管了这件事情,并且还贴心地消除了浏览器间的差异,赋予开发者跨浏览器的开发体验,主要是使用 EventPluginHub这个东西来负责调度事件的存储,合成事件并以对象池的方式实现创建和销毁。

React内部事件系统实现可以分为两个阶段:事件注册、事件触发,涉及的主要类如下:

ReactEventListener:负责事件注册和事件分发。React将DOM事件全都注册到document节点上,事件分发主要调用dispatchEvent进行,从事件触发组件开始,向父元素遍历。

ReactEventEmitter:负责每个组件上事件的执行。

EventPluginHub:负责回调函数的存储

JSX中声明一个React事件,比如:

render() {

  return (

    <button onClick={this.handleClick}>点击</button>

  )

}

用户点击button按钮触发click事件后,DOM将event传给ReactEventListener,触发document上注册的事件处理函数,执行ReactEventListener.dispatchEvent(event)将事件分发到当前组件及以上的父组件。然后ReactEventEmitter对每个组件进行事件的执行,先构造React合成事件,然后以队列的方式调用JSX中声明的callback。

备注:以下代码逻辑大部分写在注释里面

事件注册

这是 react 事件机制的第1步 - 事件注册,在这里你将了解react事件的注册过程,以及在这个过程中主要经过了哪些关键步骤,同时结合源码进行验证和增强理解。

在这里并不会说非常细节的内容,而是把大概的流程和原理性的内容进行介绍,做到对整体流程有个认知和理解。

大致流程

react 事件注册过程其实主要做了2件事:事件注册、事件存储。

a. 事件注册 - 组件挂载阶段,根据组件内的声明的事件类型-onclick,onchange 等,给 document 上添加事件 -addEventListener,并指定统一的事件处理程序 dispatchEvent。

b. 事件存储 - 就是把 react 组件内的所有事件统一的存放到一个对象里,缓存起来,为了在触发事件的时候可以查找到对应的方法去执行。

关键步骤

上面大致说了事件注册需要完成的两个目标,那完成目标的过程需要经过哪些关键处理呢?

首先 react 拿到将要挂载的组件的虚拟 dom(其实就是 react element 对象),然后处理react dom 的 props ,判断属性内是否有声明为事件的属性,比如onClick,onChange,这个时候得到事件类型 click,change 和对应的事件处理程序 fn,然后执行后面3步

a. 完成事件注册

b. 将react dom ,事件类型,处理函数 fn 放入数组存储

c. 组件挂载完成后,处理 b 步骤生成的数组,经过遍历把事件处理函数存储到listenerBank(一个对象)中

源码解析

1.从 jsx 说起

看个最熟悉的代码,也是我们日常的写法

    //此处代码省略

    handleFatherClick=()=>{

    }

    handleChildClick=()=>{

    }

    render(){

        return <div className="box">

                    <div className="father" onClick={this.handleFatherClick}>

                        <div className="child" onClick={this.handleChildClick}>child </div>

                    </div>

               </div>

    }

经过 babel 编译后,可以看到最终调用的方法是react.createElement,而且声明的事件类型和回调就是个props。

react.createElement('div',

  {

    className:'box',

  },

  react.createElement('div',

    {

      className:'father',

      onClick: this.handleFatherClick

    },

    react.createElement('div',

      {

        className:'child',

        onClick: this.handleChildClick,

      },

      'child'

    );

  );

);

react.createElement执行的结果会返回一个所谓的虚拟 dom (react element object)

处理组件props,拿到事件类型和回调 fn

ReactDOMComponent在进行组件加载(mountComponent)、更新(updateComponent)的时候,需要对props进行处理(_updateDOMProperties):

_updateDOMProperties: function (lastProps, nextProps, transaction) {

    ... // 前面代码太长,省略一部分

    else if (registrationNameModules.hasOwnProperty(propKey)) {

        // 如果是props这个对象直接声明的属性,而不是从原型链中继承而来的,则处理它

        // nextProp表示要创建或者更新的属性,而lastProp则表示上一次的属性

        // 对于mountComponent,lastProp为null。updateComponent二者都不为null。unmountComponent则nextProp为null

        if (nextProp) {

          // mountComponent和updateComponent中,enqueuePutListener注册事件

          enqueuePutListener(this, propKey, nextProp, transaction);

        } else if (lastProp) {

          // unmountComponent中,删除注册的listener,防止内存泄漏

          deleteListener(this, propKey);

        }

    }

}

可以看下 registrationNameModules 的内容,就不细说了,他就是一个内置的常量。

事件注册和事件的存储
1.事件注册

接着上面的代码执行到了这个方法

enqueuePutListener(this, propKey, nextProp, transaction);

在这个方法里会进行事件的注册以及事件的存储,包括冒泡和捕获的处理

// inst: React Component对象

// registrationName: React合成事件名,如onClick

// listener: React事件回调方法,如onClick=callback中的callback

// transaction: mountComponent或updateComponent所处的事务流中,React都是基于事务流的

function enqueuePutListener(inst, registrationName, listener, transaction) {

  if (transaction instanceof ReactServerRenderingTransaction) {

    return;

  }

  var containerInfo = inst._hostContainerInfo;

  var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;

  // 找到document

  var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;

  // 注册事件,将事件注册到document上

  listenTo(registrationName, doc);

  // 存储事件,放入事务队列中

  transaction.getReactMountReady().enqueue(putListener, {

    inst: inst,

    registrationName: registrationName,

    listener: listener

  });

}

可以看到这个函数一共做了三件事:

1.根据当前的组件实例获取到最高父级-也就是document;

2.然后执行方法 listenTo - 也是最关键的一个方法,进行事件注册。

  1. 最后执行transaction.getReactMountReady().enqueue,将react dom 实例,事件类型,处理函数 fn 组成一个对象放入数组存储。等待组件挂载后依次为数组里面每一项执行putListener。为数组每一项生成一个映射关系,把这个关系保存在了一个 map里,也就是一个对象(键值对),然后在事件触发的时候去根据当前的组件id和事件类型查找到对应的事件fn。 从ReactBrowserEventEmitter.listenTo;在ReactBrowserEventEmitter

.js文件下找到listenTo方法,可以发现它主要解决了不同浏览器间捕获和冒泡不兼容的问题。click,mousewheel等事件调用trapBubbledEvent来注册冒泡事件;scroll,focus等事件调用trapCapturedEvent来注册捕获事件。

listenTo : function (registrationName, contentDocumentHandle) {

    var mountAt = contentDocumentHandle;

    var isListening = getListeningForDocument(mountAt);

    var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];

    for (var i = 0; i < dependencies.length; i++) {

      var dependency = dependencies[i];

      if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {

        if (dependency === 'topWheel') {

          if (isEventSupported('wheel')) {

            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'wheel', mountAt);

          } else if (isEventSupported('mousewheel')) {

            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'mousewheel', mountAt);

          } else {

            // Firefox needs to capture a different mouse scroll event.

            // @see http://www.quirksmode.org/dom/events/tests/scroll.html

            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topWheel', 'DOMMouseScroll', mountAt);

          }

        } else if (dependency === 'topScroll') {

          if (isEventSupported('scroll', true)) {

            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topScroll', 'scroll', mountAt);

          } else {

            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topScroll', 'scroll', ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE);

          }

        } else if (dependency === 'topFocus' || dependency === 'topBlur') {

          if (isEventSupported('focus', true)) {

            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topFocus', 'focus', mountAt);

            ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent('topBlur', 'blur', mountAt);

          } else if (isEventSupported('focusin')) {

            // IE has `focusin` and `focusout` events which bubble.

            // @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html

            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topFocus', 'focusin', mountAt);

            ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent('topBlur', 'focusout', mountAt);

          }

          // to make sure blur and focus event listeners are only attached once

          isListening.topBlur = true;

          isListening.topFocus = true;

        } else if (topEventMapping.hasOwnProperty(dependency)) {

          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);

        }

        isListening[dependency] = true;

      }

    }

  }

最后执行EventListener.listen(冒泡)或者EventListener.capture(捕获),来看看将事件绑定到冒泡阶段的具体代码:

// 三个参数为 topEvent、原生 DOM Event、Document(挂载节点)

trapBubbledEvent: function (topLevelType, handlerBaseName, element) {

    if (!element) {

        return null;

    }

    return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));

}

// 三个参数为 Document(挂载节点)、原生 DOM Event、事件绑定函数

listen: function listen(target, eventType, callback) {

  if (target.addEventListener) {

    target.addEventListener(eventType, callback, false);

    // 返回一个解绑的函数

    return {

        remove: function remove() {

            target.removeEventListener(eventType, callback, false);

        }

    }

  }

  if (target.attachEvent) {

    target.attachEvent('on' + eventType, callback);

    // 返回一个解绑的函数

    return {

        remove: function remove() {

            target.detachEvent('on' + eventType, callback);

        }

    }

  }

}

也可以看到注册事件的时候也对 ie 浏览器做了兼容。

上面没有看到 dispatchEvent 的定义,其实上面代码中的callback统一为dispatchEvent。dispatchEvent将在之后讲。

到这里事件注册就完事儿了。

事件存储

开始事件的存储,在 react 里所有事件的触发都是通过 dispatchEvent方法统一进行派发的,而不是在注册的时候直接注册声明的回调,来看下事件如何存储的 。

还是上面的源码:

function enqueuePutListener(inst, registrationName, listener, transaction) {

  var containerInfo = inst._hostContainerInfo;

  var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;

  var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;

  listenTo(registrationName, doc);//这个方法上面已说完

  //这里涉及到了事务,事物会在以后的章节再介绍,主要看事件注册

  //下面的代码是将putListener放入数组,当组件挂载完后会依次执行数组的回调。也就是putListener会依次执行

  transaction.getReactMountReady().enqueue(putListener, {

    inst: inst,//组件实例

    registrationName: registrationName,//事件类型 click

    listener: listener //事件回调 fn

  });

}

大致的流程就是执行完listenTo(事件注册),执行transaction.getReactMountReady().enqueue,将react dom 实例,事件类型,处理函数 fn 组成一个对象放入数组存储。等待组件挂载后依次为数组里面每一项执行putListener。为数组每一项生成一个映射关系,把这个关系保存在了一个 对象(键值对)里,这个对象叫做 listenerBank,如下图。然后在事件触发的时候去根据当前的组件id和事件类型查找到对应的事件fn。

事件存储由EventPluginHub来负责,EventPluginHub在react事件系统的核心文件renderers/shared/event/EventPluginHub.js中定义,感兴趣的同学可以去看看源码~~

var EventPluginHub = {

 injection,

 putListener,

 getListener,

 deleteListener,

 deleteAllListeners,

 extractEvents, // 当顶层事件被触发,该方法中会传入原生事件,生成合成事件

 enqueueEvents,// 合成事件进入事件队列

 processEventQueue, // 调度事件队列上的所有合成事件

}

事件存储的入口在我们上面讲到的putListener方法,如下

function putListener() {

  var listenerToPut = this;

  EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);

}

实际调用的是EventPluginHub.js中的putListener方法,该方法在组件挂载时执行。EventPluginHub.js主要负责事件的存储、合成事件以对象池的方式实现创建和销毁。

/**

   * EventPluginHub用来存储React事件, 将listener存储到`listenerBank[registrationName][key]`

   *

   * @param {object} inst: 事件源

   * @param {string} listener的名字,比如onClick

   * @param {function} listener的callback

   */

  //

  var listenerBank = {};

  putListener: function (inst, registrationName, listener) {

    // 用来标识注册了事件,比如onClick的React对象。key的格式为'.nodeId', 只用知道它可以标示哪个React对象就可以了

    var key = getDictionaryKey(inst);

    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});

    // 将listener事件回调方法存入listenerBank[registrationName][key]中,比如listenerBank['onclick'][nodeId]

    // 所有React组件对象定义的所有React事件都会存储在listenerBank中

    bankForRegistrationName[key] = listener;

    //onSelect和onClick注册了两个事件回调插件, 用于walkAround某些浏览器兼容bug,不用care

    var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];

    if (PluginModule && PluginModule.didPutListener) {

      PluginModule.didPutListener(inst, registrationName, listener);

    }

  },

var getDictionaryKey = function (inst) {

  return '.' + inst._rootNodeID;

};

listenerBank其实就是一个二级 map,这样的结构更方便事件的查找。

这里的组件 id 就是组件的唯一标识,然后和fn进行关联,在触发阶段就可以找到相关的事件回调。

看到这个结构是不是很熟悉呢?就是我们平常使用的 object.

到这里大致的流程已经说完,是不是感觉有点明白又不大明白。

没关系,再来个详细的图,重新理解下。

事件触发

在事件注册阶段,最终所有的事件和事件类型都会保存到listenerBank中。

那么在事件触发的过程中上面这个对象有什么用处呢?

其实就是用来查找事件回调

事件触发过程总结为主要分为3个步骤:事件分发、生成合成事件、批量执行事件回调

1.进入统一的事件分发函数(dispatchEvent)

2.结合原生事件找到当前节点对应的ReactDOMComponent对象

3.开始事件的合成

3.1 根据当前事件类型生成指定的合成对象

3.2 封装原生事件和冒泡机制

3.3 查找当前元素以及他所有父级

3.4 在listenerBank查找事件回调并合成到 event(合成事件结束)

4.批量处理合成事件内的回调事件(事件触发完成 end)

举个栗子

在说具体的流程前,先看一个栗子,后面的分析也是基于这个栗子

handleFatherClick=(e)=>{

        console.log('father click');

    }

    handleChildClick=(e)=>{

        console.log('child click');

    }

    render(){

        return <div className="box">

                    <div className="father" onClick={this.handleFatherClick}> father

                        <div className="child" onClick={this.handleChildClick}>child </div>

                    </div>

               </div>

    }

看到这个熟悉的代码,我们就已经知道了执行结果。

当我点击 child div 的时候,会同时触发father的事件。

1. 事件分发

当事件触发时,注册在document上的回调函数会被触发。事件触发的入口函数是ReactEventListener.dispatchEvent负责分发已经注册的回调函数。在这个函数中会调用batchingStrategy 的 batchUpdate 方法实现批量处理更新。batchUpdate以transaction形式调用,批量处理更新。

// topLevelType:带top的事件名,如topClick。不用纠结为什么带一个top字段,知道它是事件名就OK了

// nativeEvent: 用户触发click等事件时,浏览器传递的原生事件

dispatchEvent: function (topLevelType, nativeEvent) {

    // disable了则直接不回调相关方法

    if (!ReactEventListener._enabled) {

      return;

    }

     // bookKeeping的初始化使用了react在源码中用到的对象池的方法来避免多余的垃圾回收。

     // bookKeeping的作用看ta的定义就知道了,就是一个用来保存过程中会使用到的变量的对象。

    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);

    try {

      // 放入批处理队列中,React事件流也是一个消息队列的方式

      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);

    } finally {

      TopLevelCallbackBookKeeping.release(bookKeeping);

    }

}

TopLevelCallbackBookKeeping是一个类,该类对象用于记录topLevelType,nativeEvent和用于存储所有的祖先节点数组ancestors(当前是空的,只有分发时才会遍历并存储所有的祖先节点) 。

那么传入batchedUpdates 内部的回调函数handleTopLevelImpl是什么呢???它其实就是事件分发的核心部分。

// document进行事件分发,这样具体的React组件才能得到响应。因为DOM事件是绑定到document上的

function handleTopLevelImpl(bookKeeping) {

  // 获取发生原生的事件的e.target

  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);

  // // 获取原生事件的target说在的组件,它是虚拟DOM

  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);

  // 执行事件回调前,先由当前组件向上遍历它的所有父组件。得到ancestors这个数组。

  // 因为事件回调中可能会改变Virtual DOM结构,所以要先遍历好组件层级

  var ancestor = targetInst;

  do {

    bookKeeping.ancestors.push(ancestor);  

    // 这里的findParent曾经给我带来误导,我以为去找当前元素所有的父节点,但其实不是的,

    // 我们知道一般情况下,我们的组件最后会被包裹在<div id='root'></div>的标签里

    // 一般是没有组件再去嵌套它的,所以通常返回null

    ancestor = ancestor && findParent(ancestor);

  } while (ancestor);

  // 从当前组件向父组件遍历,依次执行注册的回调方法. 我们遍历构造ancestors数组时,是从当前组件向父组件回溯的,故此处事件回调也是这个顺序

  // 这个顺序就是冒泡的顺序

  for (var i = 0; i < bookKeeping.ancestors.length; i++) {

    targetInst = bookKeeping.ancestors[i];

    ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));

  }

}

function findParent(inst) {

  while (inst._hostParent) {

    inst = inst._hostParent;

  }

  var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst);

  var container = rootNode.parentNode;

  return ReactDOMComponentTree.getClosestInstanceFromNode(container);

}

从上面的事件分发中可见,React自身实现了一套冒泡机制。从触发事件的对象开始,向父元素回溯,依次调用它们注册的事件callback。

看下ReactDOMComponent实例的内容

事件处理由_handleTopLevel完成。它其实是调用ReactBrowserEventEmitter.handleTopLevel() ,如下

// React事件调用的入口。DOM事件绑定在了document原生对象上,每次事件触发,都会调用到handleTopLevel

  handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {

    // 采用对象池的方式构造出合成事件。不同的eventType的合成事件可能不同

    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);

    // 批处理队列中的events

    runEventQueueInBatch(events);

  }

handleTopLevel方法是事件callback调用的核心。它主要做两件事情,一方面利用浏览器回传的原生事件构造出React合成事件,另一方面采用队列的方式处理events。先看如何构造合成事件。


2. 事件合成

合成事件时一个跨浏览器原生事件包装器,具有与浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault() ,除了事件在所有浏览器中他们工作方式都相同。现在来看一看React是如何实现合成事件的。

EventPluginHub.js中extractEvents方法:

  extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {

    var events;

    var plugins = EventPluginRegistry.plugins;

    for (var i = 0; i < plugins.length; i++) {

      // Not every plugin in the ordering may be loaded at runtime.

      var possiblePlugin = plugins[i];

      if (possiblePlugin) {

        var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);

        if (extractedEvents) {

          events = accumulateInto(events, extractedEvents);

        }

      }

    }

    return events;

  }
插件中的extractevents

注意不要将EventPluginHub.extractevents和possiblePlugin.extractEvents搞混了

以点击事件click的生成插件SimpleEventPlugin为例:

//进行事件合成,根据事件类型获得指定的合成类

var SimpleEventPlugin = {

    eventTypes: eventTypes,

    extractEvents: function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) {

        var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];

        //代码已省略....

        var EventConstructor;

        switch (topLevelType) {

            //代码已省略....

            case 'topClick'://【这里有一个不解的地方】 topLevelType = topClick,执行到这里了,但是这里没有做任何操作

                if (nativeEvent.button === 2) {

                    return null;

                }

            //代码已省略....

            case 'topContextMenu'://而是会执行到这里,获取到鼠标合成类

                EventConstructor = SyntheticMouseEvent;

                break;

            case 'topAnimationEnd':

            case 'topAnimationIteration':

            case 'topAnimationStart':

                EventConstructor = SyntheticAnimationEvent;//动画类合成事件

                break;

            case 'topWheel':

                EventConstructor = SyntheticWheelEvent;//鼠标滚轮类合成事件

                break;

            case 'topCopy':

            case 'topCut':

            case 'topPaste':

                EventConstructor = SyntheticClipboardEvent;

                break;

        }

        // 合成事件对象都是以pool方式创建和销毁的,这提高了React的性能,同时也意味着一旦事件执行结束 

        // 该合成事件对象会被销毁。因此不能通过异步方式获取该事件

        var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);

        EventPropagators.accumulateTwoPhaseDispatches(event);

        return event;//最终会返回合成的事件对象

    }

EventPluginHub.extractEvents()方法是通过执行各个插件的extractEvents方法来创建合成事件。possiblePlugin.extractEvents()根据事件的Type创建不同插件的合成事件,accumulateInto()负责将所有插件的合成事件存入到events数组中,形成当前事件的合成事件集合。EventPluginRegistry.plugins默认包含五种plugin,他们是在EventPluginHub初始化阶段注入进去的,分别是 SimpleEventPlugin、EnterLeaveEventPlugin、ChangeEventPlugin、SelectEventPlugin和BeforeInputEventPlugin。根据不同的事件类型采用不同的事件合成方法,这些事件合成方法有SyntheticAnimationEvent.js、SyntheticFocusEvent.js、SyntheticKeyboardEvent.js、SyntheticMouseEvent.js、SyntheticTouchEvent.js、SyntheticUIEvent.js、SyntheticWheelEvent.js等等一共13种合成方法。

上面提到调用EventPropagators.accumulateTwoPhaseDispatches(event)从EventPluginHub中获取回调函数,存储到合成事件的_dispatchListeners属性中。如下:

// EventPropagators.js

function accumulateTwoPhaseDispatches(events) {

  forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);

}

// forEachAccumulated 函数在接下来会讲到

function accumulateTwoPhaseDispatchesSingle(event) {

  if (event && event.dispatchConfig.phasedRegistrationNames) {

    EventPluginUtils.traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);

  }

}
把所有父级元素绑定的相关事件按照捕获->冒泡的顺序存push到合成事件对象的_dispatchListeners属性中。该属性为一个数组。
/**

 * 

 * @param {obj} inst 当前节点实例

 * @param {function} fn 处理方法

 * @param {obj} arg 合成事件对象

 */

function traverseTwoPhase(inst, fn, arg) {

    var path = [];//存放所有实例 ReactDOMComponent

    while (inst) {

        path.push(inst);

        inst = inst._hostParent;//层级关系

    }

    var i;

    for (i = path.length; i-- > 0;) {

        fn(path[i], 'captured', arg);//处理捕获 ,反向处理数组

    }

    for (i = 0; i < path.length; i++) {

        fn(path[i], 'bubbled', arg);//处理冒泡,从0开始处理,我们直接看冒泡

    }

}

看下 path 长啥样

紧接着如何在listenerBank中查找事件回调并合成到合成对象的_dispatchListeners中呢。

紧接着上面代码

fn(path[i], 'bubbled', arg);

上面的代码会调用下面这个方法,在listenerBank中查找到事件回调,并存入合成事件对象。

/**EventPropagators.js

 * 查找事件回调后,把实例和回调保存到合成对象内

 * @param {obj} inst 组件实例

 * @param {string} phase 事件类型

 * @param {obj} event 合成事件对象

 */

function accumulateDirectionalDispatches(inst, phase, event) {

    var listener = listenerAtPhase(inst, event, phase);

    if (listener) {//如果找到了事件回调,则保存起来 (保存在了合成事件对象内)

        event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);//把事件回调进行合并返回一个新数组

        event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);//把组件实例进行合并返回一个新数组

    }

}

/**

 * EventPropagators.js

 * 中间调用方法 拿到实例的回调方法

 * @param {obj} inst  实例

 * @param {obj} event 合成事件对象

 * @param {string} propagationPhase 名称,捕获capture还是冒泡bubbled

 */

function listenerAtPhase(inst, event, propagationPhase) {

    var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];

    return getListener(inst, registrationName);

}

/**EventPluginHub.js

 * 拿到实例的回调方法

 * @param {obj} inst 组件实例

 * @param {string} registrationName Name of listener (e.g. `onClick`).

 * @return {?function} 返回回调方法

 */

getListener: function getListener(inst, registrationName) {

    var bankForRegistrationName = listenerBank[registrationName];

    if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {

        return null;

    }

    var key = getDictionaryKey(inst);

    return bankForRegistrationName && bankForRegistrationName[key];

}

为什么能够查找到的呢?

因为 inst (组件实例)里有_rootNodeID,所以也就有了对应关系。比如通过上面函数getDictionaryKey获取到触发事件的DOM组件的_rootNodeId属性,然后根据callback = listenerBank[eventType][_rootNodeId]可以获取该组件的回调函数。

到这里事件合成对象生成完成,所有的事件回调已保存到了合成对象中。

3.批量执行事件回调

事件合成之后就需要执行事件回调函数,React以批量处理事件队列的方式执行事件的。它的入口函数是在ReactEventEmitterMixin.js中的runEventQueueInBatch方法:

var eventQueue = null;

function runEventQueueInBatch(events) {

  EventPluginHub.enqueueEvents(events);

  EventPluginHub.processEventQueue(false);

}

//EventPluginHub.enqueueEvents()方法是将合成事件写入队列,EventPluginHub.processEventQueue()方法是执行队列中的事件。

// EventPluginHub.enqueueEvents()方法的实现逻辑很简单,使用accumulateInto方法将events存入eventQueue队列中。

 enqueueEvents: function (events) {

    if (events) {

      eventQueue = accumulateInto(eventQueue, events);

    }

  }

function accumulateInto(current, next) {

  if (current == null) {

    return next;

  }

  // 将next添加到current中,返回一个包含他们两个的新数组

  // 如果next是数组,current不是数组,采用push方法,否则采用concat方法

  // 如果next不是数组,则返回一个current和next构成的新数组

  if (Array.isArray(current)) {

    if (Array.isArray(next)) {

      current.push.apply(current, next);

      return current;

    }

    current.push(next);

    return current;

  }

  if (Array.isArray(next)) {

    return [current].concat(next);

  }

  return [current, next];

}

// EventPluginHub.processEventQueue()方法的实现逻辑分为:

processEventQueue: function (simulated) {

// 现将eventQueue 设置为null,以便在处理过程中让新合成事件入队列

    var processingEventQueue = eventQueue;

    eventQueue = null;

    if (simulated) {

      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);

    } else {

      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);

    }

       // This would be a good time to rethrow if any of the event handlers threw.

    ReactErrorUtils.rethrowCaughtError();

  }

(1)获取合成事件队列,其中可能包含之前没处理完的合成事件

(2)遍历合成事件队列,

function forEachAccumulated(arr, cb, scope) {

  if (Array.isArray(arr)) {

    arr.forEach(cb, scope);

  } else if (arr) {

    cb.call(scope, arr);

  }

}

事件队列的回调函数为executeDispatchesAndReleaseSimulated,负责事件的分发和事件遍历结束后是否释放合成事件对象。

var executeDispatchesAndReleaseSimulated = function (e) {

  return executeDispatchesAndRelease(e, true);

};

/**/

var executeDispatchesAndRelease = function (event, simulated) {

  if (event) {

/*按存储顺序分发事件,先进先出*/

    EventPluginUtils.executeDispatchesInOrder(event, simulated);

/*判断是否释放事件*/

    if (!event.isPersistent()) {

      event.constructor.release(event);

    }

  }

};

var executeDispatchesAndReleaseTopLevel = function (e) {

  return executeDispatchesAndRelease(e, false);

};

var executeDispatchesAndRelease = function (event, simulated) {

  if (event) {

    //进行事件分发

    EventPluginUtils.executeDispatchesInOrder(event, simulated);

    if (!event.isPersistent()) {

       // 处理完,则release掉event对象,采用对象池方式,减少GC

      // React帮我们处理了合成事件的回收机制,不需要我们关心。但要注意,如果使用了DOM原生事件,则要自己回收

      event.constructor.release(event);

    }

  }

};

// EventPluginUtils.js

// 事件处理的核心

function executeDispatchesInOrder(event, simulated) {

  var dispatchListeners = event._dispatchListeners;

  var dispatchInstances = event._dispatchInstances;

 if (Array.isArray(dispatchListeners)) {

    // 如果有多个listener,则遍历执行数组中event

    for (var i = 0; i < dispatchListeners.length; i++) {

      // 如果isPropagationStopped设成true了,则停止事件传播,退出循环。

      if (event.isPropagationStopped()) {

        break;

      }

      // 执行event的分发,从当前触发事件元素向父元素遍历

      // event为浏览器上传的原生事件

      // dispatchListeners[i]为JSX中声明的事件callback

      // dispatchInstances[i]为对应的React Component 

      executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);

    }

  } else if (dispatchListeners) {

    // 如果只有一个listener,则直接执行事件分发

    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);

  }

  // 处理完event,重置变量。因为使用的对象池,故必须重置,这样才能被别人复用

  event._dispatchListeners = null;

  event._dispatchInstances = null;

}

通过executeDispatchesInOrder函数可知,dispatch 合成事件分为两个步骤:

  • 通过_dispatchListeners里得到所有绑定的回调函数,在通过_dispatchInstances的绑定回调函数的虚拟dom元素
  • 循环执行_dispatchListeners里所有的回调函数,这里有一个特殊情况,也是react阻止冒泡的原理

当回调函数里使用了stopPropagation会使得数组后面的回调函数不能执行,这样就做到了阻止事件冒泡

目前还是还有看到执行事件的代码,在接着看:

/**

 * 

 * @param {obj} event 合成事件对象

 * @param {boolean} simulated false

 * @param {fn} listener 事件回调

 * @param {obj} inst 组件实例

 */

function executeDispatch(event, simulated, listener, inst) {

    var type = event.type || 'unknown-event';

    event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);

    if (simulated) {//调试环境的值为 false,按说生产环境是 true 

        //方法的内容请往下看

        ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);

    } else {

        //方法的内容请往下看

        ReactErrorUtils.invokeGuardedCallback(type, listener, event);

    }

    event.currentTarget = null;

}

/** ReactErrorUtils.js

 * @param {String} name of the guard to use for logging or debugging

 * @param {Function} func The function to invoke

 * @param {*} a First argument

 * @param {*} b Second argument

 */

var caughtError = null;

function invokeGuardedCallback(name, func, a) {

    try {

        func(a);//直接执行回调方法

    } catch (x) {

        if (caughtError === null) {

            caughtError = x;

        }

    }

}

var ReactErrorUtils = {

    invokeGuardedCallback: invokeGuardedCallback,

    invokeGuardedCallbackWithCatch: invokeGuardedCallback,

    rethrowCaughtError: function rethrowCaughtError() {

        if (caughtError) {

            var error = caughtError;

            caughtError = null;

            throw error;

        }

    }

};

if (process.env.NODE_ENV !== 'production') {//非生产环境会通过自定义事件去触发回调

    if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function') {

        var fakeNode = document.createElement('react');

        ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {

            var boundFunc = func.bind(null, a);

            var evtType = 'react-' + name;

            fakeNode.addEventListener(evtType, boundFunc, false);

            var evt = document.createEvent('Event');

            evt.initEvent(evtType, false, false);

            fakeNode.dispatchEvent(evt);

            fakeNode.removeEventListener(evtType, boundFunc, false);

        };

    }

}

最后react 通过生成了一个临时节点fakeNode,然后为这个临时元素绑定事件处理程序,然后创建自定义事件 Event,通过fakeNode.dispatchEvent方法来触发事件,并且触发完毕之后立即移除监听事件。


总结

React的事件系统主要分为3个步骤:

一是事件绑定,ReactBrowserEventEmitter的trapBubbledEvent等方法为节点或文档绑定事件;

二是事件监听,ReactEventListener.dispatchEvent将把该回调函数分发给事件对象的_dispatchListener属性;调用ReactBrowserEventEmitter.ReactEventListener方法以监听节点事件。

三是事件分发与触发,对触发的事件进行分发,并创建合成事件对象,在回调中用构建合成事件对象并执行合成事件对的象绑定回调。

站在巨人肩上

一看就晕的React事件机制

揭秘React形成合成事件的过程

【长文慎入】一文吃透 react 事件机制原理

React 事件系统

React事件机制 - 源码概览(上)

react 事件池

React 合成事件和原生事件的区别

React的事件机制

React event实现原理

为什么需要在 React 类组件中为事件处理程序绑定 this

React合成事件系统

【译】了解React源代码-UI更新(DOM树)9

相关推荐

什么是DPDK?DPDK的原理及学习学习路线总结

一、什么是DPDK  对于用户来说,它可能是一个性能出色的包数据处理加速软件库;对于开发者来说,它可能是一个实践包处理新想法的创新工场;对于性能调优者来说,它可能又是一个绝佳的成果分享平台。 ...

每天进步一点:两分钟解决kvm下windows虚拟机鼠标不跟随

跟随昨天文章做测试的朋友们应该和我一样遇到了vnc连接windows鼠标不跟随的问题,经过一番查找有两种解决办法:1.编辑配置文件命令virshedittest或者直接vi/etc/libvir...

PC虚拟化主流:KVM、XEN、OpenVZ详解

目前,PC的虚拟化逐渐成为互联网发展的大趋势,我们知道,KVM、XEN、OpenVZ是虚拟化的三种方式,今天我们就来探讨这三种虚拟化的优势和劣势。1、pc虚拟化——KVMKVM是完整的硬件虚拟化,可在...

Windows上使用QEMU创建aarch64(ARM64)虚拟机

前言随着国产化的推进,现在采用ARM、MIPS的机器越来越多,作为开发、运维人员要调测软件总不能每种架构的机器都去买一台吧?主要像博主这样的穷B,实在也是承受不起。。需要的工具...

高度致敬Windows!开源优麒麟20.04 LTS发布:支持5年

优麒麟团队宣布,优麒麟(UbuntuKylin)开源操作系统20.04LTS正式版已经发布,代号FocalFossa,全球同步发布的还有Ubuntu20.04、Lubuntu20.04、Xub...

极空间虚拟机上线了,一学就会!小白保姆级使用教程

友情提示本文涉及内容较多,篇幅在4500字左右,为了对小白用户更加友好,图片示例多达60张。整个文章部分为三个阶段,准备-初探-实战。其中实战部分包含Windows系统,ikuai软路由系统,iSto...

Windows Subsystem for Linux现以应用形式上架Microsoft Store

微软今天宣布WindowsSubsystemforLinux(WSL)作为一款应用上架Windows11端的MicrosoftStore。也就是说,现在WSL以应用的方式通过...

Windows Server 2019 Core 虚拟机系统镜像制作

WindowsServer2019Core简介WindowsServer2019是微软于2018年11月13日发布的新一代WindowsServer服务器操作系统,基于Win10180...

微软商店中的WSL预览版现已可用!Windows 11用户狂喜

...

在NAS上安装Win10,24小时待命的云电脑达成√

#头条创作挑战赛#引子...

免费开源虚拟机VirtualBox 7.0.12发布:修复TPM和黑屏问题

IT之家10月18日消息,甲骨文近日发布了VirtualBox7.0.12维护版本更新,重点修复此前版本中用户反馈和官方发现的BUG,改善了对LinuxKernel6.4/6.5...

KVM Cloud 虚拟机管理系统安装部署

KVMCloud介绍KVMCloud是一款基于KVM实现的适用于小微企业的虚拟机管理系统,支持如下功能:基于KVM的VM基础功能(创建、启动、停止、重装、webVNC等功能)使用NFS作为磁盘...

个人KVM 虚拟化学习笔记(kvm虚拟化管理平台)

一、KVM原理二、KVM基础功能2.1CPU2.2内存2.3存储2.4网络三、KVM高级功能...

kvm虚拟化之ESXi到KVM之v2v迁移(esxi虚拟机迁移到另一个esxi)

1.ESXi到KVM之v2v情况说明(1).配置任务列表:1)VMwareESXi虚拟平台下linux系统迁移到KVM虚拟平台。2)VMwareESXi虚拟平台下windows系统迁移到KVM虚拟平台...

unraid下虚拟机安装Windows(vmware安装unraid)

unraid下虚拟机安装Windows使用unraid也有一段时间了,主要是做数据备份,以及docker容器的安装测试,今天有空测试一下VMS虚拟机的使用,用在unraid上安装windows7操作系...