关于事件:技术白皮书第五章信息抽取技术的未来发展趋势和面临的挑战

5.信息抽取技术的将来发展趋势和面临的挑战5.1 NER技术的将来发展趋势和面临的挑战论文《 Survey on Deep Learning for Named Entity Recognition》总结了NER技术面临的挑战和将来倒退方向。随着建模语言的提高和理论利用的需要,NER会失去钻研人员更多的关注。另一方面,NER通常被视为上游应用程序的预处理组件。这意味着特定的NER工作由上游应用程序的需要定义,例如,命名实体的类型以及是否须要检测嵌套实体。 以下是NER钻研的以下进一步摸索方向。1.细粒度NER和边界检测。尽管许多现有钻研(《Neural architectures for named entity recognition》、《End-to-end sequence labeling via bidirectional lstm-cnns-crf》、《Robust lexical features for improved neural network named-entity recognition》)都集中在个别畛域的粗粒度NER上,但论文冀望在特定畛域对细粒度NER进行更多钻研,以反对各种理论的word利用(《Software-specific named entity recognition in software engineering social content》)。细粒度NER面临的挑战是命名实体类型的显著减少,以及容许一个命名实体具备多个命名实体类型所带来的复杂性。这须要从新拜访常见的NER办法,其中实体边界和类型同时被检测,例如,通过应用B-I-E-S-(实体类型)和O作为解码标签。值得思考的是,将命名实体边界检测定义为一项专用工作,以检测命名实体边界,同时疏忽命名实体类型。 边界检测和命名实体类型分类的解耦实现了边界检测的通用和鲁棒的解决方案,这些解决方案能够在不同畛域共享,并为命名实体类型分类提供了专用的畛域特定办法。正确的实体边界还能无效地缓解实体链接到知识库中的谬误流传。曾经有一些钻研,认为实体边界检测是NER中的两头步骤(即子工作)。据论文所知,目前还没有专门针对实体边界检测的工作来提供一个鲁棒的识别器。论文期待着在将来这一钻研方向的冲破。 2.联结NER和实体链接。实体链接(EL)也称为命名实体规范化或消歧,旨在参考知识库为文本中提到的实体调配惟一身份,例如通用畛域的维基百科和生物医学畛域的对立医学语言零碎(UMLS)。大多数现有工作将NER和EL独自作为流水线(pipeline)设置中的两个独立工作来解决。论文认为,胜利链接的实体(例如,通过知识库中的相干实体)所携带的语义显著丰盛。也就是说,链接实体有助于胜利检测实体边界和正确分类实体类型。值得摸索联结执行NER和EL,甚至实体边界检测、实体类型分类和实体链接的办法,以便每个子工作都能从其余子工作的局部输入中受害,并缩小流水线(pipeline)设置中不可避免的谬误流传。 3.有辅助资源的非正式文本上基于DL的NER基于非正式文本或用户生成内容的DL-NER的性能依然很低。这须要在这方面进行更多的钻研。特地是,论文留神到,NER的性能显著受害于辅助资源的可用性,例如用户语言中的地位名称词典。尽管没有提供强有力的证据表明,波及地名词典,作为额定的特色能够导致NER在通用畛域的性能晋升,但论文认为辅助资源往往是必要的,以更好地理解用户生成的内容。问题是如何为用户生成的内容或特定畛域的文本上的NER工作获取匹配的辅助资源,以及如何无效地将辅助资源合并到基于深度学习的NER中。 4.基于DL的NER的可伸缩性。使神经网络模型更具可伸缩性依然是一个挑战。此外,当数据量减少时,依然须要优化参数指数增长的解决方案(《A review on deep learning for recommender systems: challenges and remedies》)。一些基于DL的NER模型以微小的计算能力为代价获得了良好的性能。例如,ELMo示意用3×1024维向量示意每个单词,模型在32个GPU上训练了5周(《Contextual string embeddings for sequence labeling》)。Google BERT示意在64个云TPU上进行训练。然而,如果终端用户无法访问弱小的计算资源,他们就无奈对这些模型进行微调。开发均衡模型复杂性和可伸缩性的办法将是一个有前途的方向。另一方面,模型压缩和剪枝技术也能够用来缩小模型学习所需的空间和计算工夫。 5.NER的深度迁徙学习。许多以实体为核心的应用程序求助于现成的NER零碎来辨认命名实体。然而,因为语言特色的差别以及正文的差别,在一个数据集上训练的模型可能无奈在其余文本上很好地工作。只管有一些钻研将深度迁徙学习利用于NER,但这个问题尚未失去充沛探讨。将来应致力于如何通过摸索以下钻研问题,无效地将常识从一个畛域转移到另一个畛域:(a)开发一个可能跨不同畛域工作的鲁棒识别器;(b) 摸索NER工作中的zero-shot, one-shot 和 few-shot learning;(c) 提供解决跨域设置中的域不匹配和标签不匹配的解决方案。 6.一个易于应用的工具包,用于基于DL的NER。最近,Röder等人开发了GERBIL(《GERBIL - benchmarking named entity recognition and linking consistently》),它为钻研人员、最终用户和开发人员提供了易于应用的界面,用于对实体正文工具进行基准测试,目标是确保可反复和可架构的试验。然而,它不波及最新的基于DL的技术。Ott介绍了FAIRSEQ(《fairseq: A fast, extensible toolkit for sequence modeling》),这是一个疾速、可扩大的序列建模工具包,特地是用于机器翻译和文本形容。Dernoncourt等人实现了一个名为NeuroNER的框架(《NeuroNER: an easy-to-use program for named-entity recognition based on neural networks》),它只依赖于循环神经网络的一个变体。近年来,许多深度学习框架(例如TensorFlow、PyTorch和Keras)被设计为通过高级编程接口为设计、训练和验证深度神经网络提供构建模块。论文构想,一个易于应用的NER工具包能够领导开发人员应用一些标准化模块来实现它:数据处理、输出示意、上下文编码器、标记解码器和有效性度量。论文置信,专家和非专家都能够从这些工具包中受害。 ...

August 25, 2022 · 1 min · jiezi

利用JavaScript自定义事件完成组件间的数据通信

我们知道,在JavaScript中,原生DOM事件在开发中是很有用的(与用户交互的重要方式),但是操作原生DOM事件其实有两大缺点:性能低、依赖于浏览器(NodeJs、小程序等不可用)。那么这个时候,就需要我们进行自定义事件去处理某些特定的业务。认识Event对象及元素的dispatchEvent方法在JavaScript中,所有事件的父对象就是Event对象,也就是说像我们平时所有的点击(click)、触摸(touch)、鼠标等事件对象都继承自Event。理解这一点是很重要的。先来简单看一个事件的场景。 场景一、页面上有两个按钮a、b,当点击按钮b的时候,调用按钮a的点击事件。简单布局代码如下: <button id="btn1">a</button><button id="btn2">b</button>程序员A的做法,分别获取这两个按钮,然后给b按钮添加点击事件后,调用按钮a的click方法。代码如下: <button id="btn1" onclick="alert('I am a button named a')">a</button><button id="btn2">b</button><script> let btn1 = document.querySelector('#btn1'); let btn2 = document.querySelector('#btn2'); btn2.onclick = function(){ btn1.onclick(); }</script>程序员B的做法,分别获取这两个按钮,然后给b按钮添加点击事件后,在回调函数中在添加按钮a的点击事件。代码如下: <button id="btn1">a</button><button id="btn2">b</button><script> let btn1 = document.querySelector('#btn1'); let btn2 = document.querySelector('#btn2'); btn2.onclick = function(){ btn1.addEventListener('click',function(){ alert('I am a button named a') },false) }</script>看到这里,你认为谁的做法是正确的?显然程序员A的做法是对的(就目前的要求来看),但有缺陷,如果按钮a的事件是通过addEventListener方法去注册监听的,就不起作用了。那么该怎样做才会比较好?这就需要我们的Event对象和元素的dispatchEvent方法了。改进代码如下: <button id="btn1">a</button><button id="btn2">b</button><script> let btn1 = document.querySelector('#btn1'); let btn2 = document.querySelector('#btn2'); btn1.addEventListener('click',function(){ alert('I am a button named a') },false) btn2.onclick = function(){ let ev = new Event('click'); btn1.dispatchEvent(ev); }</script>其中: ...

October 2, 2019 · 2 min · jiezi

jascript事件循环机制

      事件循环机制控制了javascript代码的执行顺序。我们都知道javascript是单线程,这个线程中拥有唯一的一个事件循环。(新标准web workker有多线程的概念。)而事件循环机制主要以来调用栈来处理执行顺序,依靠任务队列来执行代码的执行。队列的概念可以参考https://segmentfault.com/a/11...      在一个线程中,调用栈是唯一的,但是任务队列可以是多个,并且分为macro-task(宏任务)及micro-task(微任务)两种类型。      这里需要区分一个概念任务及任务源。setTimeout及Promise是任务源。他们指定具体的执行任务进入任务队列。只有回调中的函数才会进入任务队列。就像setTimeout它其实是丽姬执行的,只是它的回调函数才会延迟执行。promise也是,本身是立即执行的,但是then才会在“未来”执行。      javascript的执行顺序是从整体代码开始做循环,之后全局上下文进入函数调用栈。直到调用栈清空。整体代码所处的macro-task执行完成,轮到micro-task任务执行。一直循环直到所有的任务执行完成。      当然,不同的任务源的任务会进入不同的任务队列。      具体的可以参考一下代码。      1.事件循环从macro-task开始,整体代码开始执行。整体代码script进入macro-task,并且执行代码main进入调用函数调用栈。遇到第12行的打印输出start      2.继续执行,遇到13行的setTimeout,它是宏任务源。便将其分发到对应的队列中。接着遇到16行的promise。promise.resolve会进入函数调用栈直接执行,因此打印promise1,接着将p.then1和p.then分发到对应的微任务队列中。继续执行代码,遇到第24行的打印便输出end。大致图示如下图。      3.script执行完毕,即第一个宏任务执行完毕,开始执行微任务。现在微任务只有一个队列,里面有p1.then1,p1.then2。队列是先进先出,因此先执行p1.then1,p1.then1进入函数调用栈,输出then1。      4. p1.then1执行完毕之后,出栈。但是此时的正在进行的微任务还未执行完完毕,会继续执行p1.then2,p1.then2进入函数调用栈,输出then2。此时,微任务正在进行的队列已经执行完毕。      5.当微任务执行完毕之后,第一轮循环结束,进入第二轮循环,继续执行宏任务,此时setTimeout执行,进入函数调用栈,输出setTimeout1。      6.此时,宏任务队列和微任务队列中都没有任务了。代码执行完毕,就不会有任何输出了。       我们上述的代码只涉及到一个宏任务及微任务队列的情况。但如果情况更加复杂会有什么样的表现呢?大家可以看看下面的代码。根据上面的原理试着自己分析下结果~      1.还是跟以前的例子一样,事件循环从macro-task开始,整体代码开始执行。输出start。setTimeout1,setTimeout2依次进入新的宏任务队列。p3.resolve执行,输出promise31,promise31。并将setTimeout3放入新的宏任务队列。因为setTimeout3不是整体代码中定义的,而是在promise中定义的,需要重新开启一个宏任务队列。然后p3.then1,p3.then2分别进入微任务队列。p3.resolve出栈后,整体代码继续执行,这里就不重新画图了,输出end。      2.整体代码已执行完成,循环进入微任务。此时p3.then1进入函数调用栈。输出then31。遇到新的定时,将set4放入宏任务队列。遇到新的promise,继续将p4.resolve入栈。输出promise41,promise42。遇到新的定时,将set5放入宏任务队列。此时需要注意的是,在微任务中继续有promise。此时的promise.then不再进入微任务队列,而是直接执行。因此输出then41。      3.微任务队列还未执行完毕,继续执行p3.then2。直接输出then32。此时微任务队列已经执行完毕,进入下一轮循环。      4.新的循环开始。队列是先进先出,因此在宏任务当前队列中,set1先执行,进入函数调用栈。输出setTimeout1。遇到新的promise,继续将p1.resolve入栈。输出promise1。还是跟上看一样,在宏任务中继续有promise。此时的promise.then不再进入微任务队列,而是直接执行。直接输出then1。      5.setTimeout1执行完毕,正在执行的宏任务队列还有任务,继续执行setTimeout2。setTimeout2进入函数调用栈。跟setTimeout1的分析一样,陆续输出setTimeout2,promise2,then2。      6.当前宏任务执行完毕,微任务内没有可执行的队列。继续下一轮循环。执行set3。输出setTimeout3。遇到新的promsie,还是跟上面的分析一样,输出promise5,then5。因为微任务一直没有可执行的队列。宏任务内的队列依次执行,输出setTimeout4,setTimeout5。

June 15, 2019 · 1 min · jiezi

如何正确使用Node.js事件

本文首发微信公众号:jingchengyideng欢迎关注,每天都给你推送新鲜的前端技术文章翻译:疯狂的技术宅原文:https://medium.freecodecamp.o…事件驱动的编程变得流行之前,在程序内部进行通信的标准方法非常简单:如果一个组件想要向另外一个发送消息,只是显式地调用了那个组件上的方法。但是在 react 中用的却是事件驱动而不是调用。事件的好处这种方法能够使组件更加分离。在我们继续写程序时,会识别整个过程中的事件,在正确的时间触发它们,并为每个事件附加一个或多个事件监听器,这使得功能扩展变得更加容易。我们可以为特定事件添加更多的 listener,而不必修改现有的侦听器或触发事件的应用程序部分。我们所谈论的是观察者模式。设计一个事件驱动的体系结构对事件进行识别非常重要,我们不希望最终必须从系统中删除或替换现有事件,因为这可能会迫使我们删除或修改附加到事件上的众多侦听器。我的一般原则是仅在业务逻辑单元完成执行时才考虑触发事件。假如你想在用户注册后发送一堆不同的电子邮件。注册过程本身可能会涉及许多复杂的步骤和查询,但从商业角度来看,这只是其中的一个步骤。每个要发送的电子邮件也是单独的步骤。因此,一旦注册完成马上就发布事件是很有意义的。于是我们附加了多个监听器,每个监听器负责发送一种类型的电子邮件。Node的异步事件驱动架构具有一些被称为“emitters”的对象。它们发出命名事件,这些事件会调用被称为“listener”的函数。发出事件的所有对象都是 EventEmitter 类的实例。使用它,我们可以创建自己的事件:一个例子让我们使用内置的 events 模块(我建议你查看这个文档:https://nodejs.org/api/events…)以获取对 EventEmitter 的访问权限。const EventEmitter = require(’events’);const myEmitter = new EventEmitter();module.exports = myEmitter;这是我们的服务器端程序的一部分,它负责接收HTTP请求,保存新用户并发出事件:const myEmitter = require(’./my_emitter’);// Perform the registration steps// Pass the new user object as the message passed through by this event.myEmitter.emit(‘user-registered’, user);附加一个监听器的单独模块:const myEmitter = require(’./my_emitter’);myEmitter.on(‘user-registered’, (user) => { // Send an email or whatever.});将策略与实现分开是一种非常好的做法。在这种情况下,策略意味着哪些 listener 订阅了哪些事件。实现意味着 listener 自己。const myEmitter = require(’./my_emitter’);const sendEmailOnRegistration = require(’./send_email_on_registration’);const someOtherListener = require(’./some_other_listener’);myEmitter.on(‘user-registered’, sendEmailOnRegistration);myEmitter.on(‘user-registered’, someOtherListener);module.exports = (user) => { // Send a welcome email or whatever.}这种分离使 listener 也可以被重复使用,它可以被附加到发送相同消息的其他事件上(用户对象)。同样重要的是 当多个 listener 被附加到单个事件时,它们将按照附加的顺序同步执行。因此 someOtherListener 将在 sendEmailOnRegistration 完成执行后运行。但是,如果你希望自己的 listener 以异步方式运行,只需用 setImmediate 包装它们的实现,如下所示:module.exports = (user) => { setImmediate(() => { // Send a welcome email or whatever. });}让你的 Listeners 保持简洁在写 listener 时要坚持单一责任原则。一个 listener 应该只做一件事并把事情做好。例如:要避免在 listener 中编写太多的条件并根据事件传来的数据(消息)去决定做什么。在这种情况下使用不同的事件会更加合适:const myEmitter = require(’./my_emitter’);// Perform the registration steps// The application should react differently if the new user has been activated instantly.if (user.activated) { myEmitter.emit(‘user-registered:activated’, user); } else { myEmitter.emit(‘user-registered’, user);}const myEmitter = require(’./my_emitter’);const sendEmailOnRegistration = require(’./send_email_on_registration’);const someOtherListener = require(’./some_other_listener’);const doSomethingEntirelyDifferent = require(’./do_something_entirely_different’);myEmitter.on(‘user-registered’, sendEmailOnRegistration);myEmitter.on(‘user-registered’, someOtherListener);myEmitter.on(‘user-registered:activated’, doSomethingEntirelyDifferent);view raw必要时明确分离 Listener在前面的例子中,我们的 listener 是完全独立的函数。但是在 listener 与对象关联的情况下(这时是一种方法),必须手动将其从已订阅的事件中分离出来。否则对象将永远不会被垃圾回收,因为对象( listener )的一部分将会继续被外部对象( emitter )引用,所以存在内存泄漏的可能。例如,如果我们正在开发一个聊天程序,并且希望当新消息到达用户进入的聊天室时,显示通知的功能应该位于该用户对象本身的内部,我们可能会这样做:class ChatUser { displayNewMessageNotification(newMessage) { // Push an alert message or something. } // chatroom is an instance of EventEmitter. connectToChatroom(chatroom) { chatroom.on(‘message-received’, this.displayNewMessageNotification); } disconnectFromChatroom(chatroom) { chatroom.removeListener(‘message-received’, this.displayNewMessageNotification); }}当用户关闭他的标签或暂时断开互联网连接时,我们可能希望在服务器端发起一个回调,通知其他用户有人刚刚下线。当然在这时为脱机用户调用 displayNewMessageNotification 没有任何意义。除非我们删除它,否则它将继续被用于调用新消息。如果不这样做,除了不必要的调用之外,用户对象也会被永久地保留在内存中。因此在用户脱机时应该在服务器端回调中调用 disconnectFromChatroom。注意事项如果不小心,即便是松散耦合的事件驱动架构也会导致复杂性的增加,可能会导致在系统中跟踪依赖关系变得很困难。如果我们从侦听器内部发出事件,程序会特别容易出现这类问题。这可能会触发意外的事件链。本文首发微信公众号:jingchengyideng欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章欢迎继续阅读本专栏其它高赞文章:12个令人惊叹的CSS实验项目世界顶级公司的前端面试都问些什么CSS Flexbox 可视化手册过节很无聊?还是用 JavaScript 写一个脑力小游戏吧!从设计者的角度看 ReactCSS粘性定位是怎样工作的一步步教你用HTML5 SVG实现动画效果程序员30岁前月薪达不到30K,该何去何从7个开放式的前端面试题React 教程:快速上手指南 ...

April 4, 2019 · 1 min · jiezi

React 事件冒泡

在React中,我们可以在创建element的时候,传入事件和处理函数,这些事件会被做为合成事件来处理,当然,有些时候,我们也需要定义原生事件,比如给document绑定事件。有些情况下,就需要通过阻止事件冒泡来实现预期的交互效果。下面是几个简单的demoDemo比如有如下的代码:import React from ‘react’class Demo1 extends React.Component{ onClickInner(e){ console.log(‘inner div’) } onClickOuter(e){ console.log(‘outer div’) } render(){ return <div onClick={this.onClickOuter}> <div onClick={this.onClickInner}>inner div</div> </div> }}当我们点击 inner div时,控制台输出结果:inner divouter div这两个事件都是合成事件,在点击时,两个事件会依次冒泡到document,由统一的事件监听器处理。如果希望阻止onClickOuter 触发,可以在onClickInner内调用e.stopPropagation()。需要注意的是,这里的e是合成事件实例,调用stopPropagation 也只能阻止合成事件的冒泡。假如我们将onClickOuter 通过原生事件来绑定:class App extends React.Component { onClickInner(e) { e.stopPropagation(); console.log(“inner div”); } onClickOuter(e) { console.log(“outer div”); } componentDidMount() { this.outer.onclick = this.onClickOuter;// 通过DOM 0级绑定 } render() { return ( <div ref={ref => (this.outer = ref)}> <div id=‘inner’ onClick={this.onClickInner}>123</div> </div> ); }}虽然在onClickInner内调用了 e.stopPropagation, 但是原生事件还是会通过冒泡来触发,而且会先于onClickInner, 控制台输出:outer divinner div这是因为onClickInner合成事件被触发的时候,说明点击事件已经通过冒泡传递到了document,在这个过程中,便会经过外层的div,进而触发该原生事件。这也说明了,合成事件的stopPropagation只能阻止合成事件的冒泡。即使我们在这里通过e.nativeEvent获取到原生事件并调用stopPropagation,也无济于事,因为上面已经说了,在该合成事件被触发的时候,已经冒泡到了document.那么我们该通过什么方式来阻止原生事件onClickOuter被触发呢:既然在onClickInner处理不了,只能在onClickOuter内处理了:onClickOuter(e) {// 这里e是原生事件 if(e.target && e.target.id === ‘inner’){ return ; } console.log(“outer div”);}如果我们将原生事件绑定在了document上:class App extends React.Component { constructor(props) { super(props); // this.bindDocument(); } onClickInner(e) { console.log(“inner div”); } componentDidMount() { this.bindDocument(); } bindDocument() { document.addEventListener(“click”, function(e) { console.log(“document”); }); } render() { return ( <div id=“inner” onClick={this.onClickInner}> 123 </div> ); }}上面代码中,在组件挂载完毕后,再给document绑定click事件,这时候,React合成事件已经注册完成,当点击时,document上的click事件会依据绑定顺序的先后依次执行,所以控制台会输出:inner divdocument如果希望阻止后绑定的事件触发,可以在onClickInner内调用stopImmediatePropagation:如果有多个相同类型事件的事件监听函数绑定到同一个元素,当该类型的事件触发时,它们会按照被添加的顺序执行。如果其中某个监听函数执行了 event.stopImmediatePropagation() 方法,则当前元素剩下的监听函数将不会被执行。 ...

April 1, 2019 · 1 min · jiezi

重探浏览器事件(浅析事件编程化)

前言在平常开发过程中,就算不使用现在主流的框架也至少得使用个Jquery,这些工具帮我们统一不同浏览器平台之间的差异和细节,可以将注意力集中到开发上来.不过有意思的一点是,在看完高程的N年后我居然连event对象中的target和currentTarget属性的区别都忘记了.先提几个引子:你能说出event.currentTarget和event.target的区别吗?如果可以那么event.srcElement和事件监听函数中的this呢如何使用编程的方式来触发事件,而不借助浏览器默认触发方式?如何创建一个我们自己的Event对象,然后自定义我们的事件?实现上方的内容的同时该如何兼容IE浏览器?如果这几个内容你都熟悉了,那么这篇文章不会给你带来太多的帮助.在正文开始之前先来浏览一个表格,来看一下不同浏览器之间Event对象的属性有何不同:<button id=“button”>click me then change word</button> var button = document.getElementById(‘button’); button.addEventListener(‘click’,function (event) { console.log(event); });在下方的表格中我们记录了不同浏览器之间click点击后event可用的属性列表(删除了控制台输出的原型和函数引用):firefox67chrome72edge44.17763.1.0ie11ie9altKeyaltKeyaltKeyaltKeyaltKeybubblesbubblesbubblesAT_TARGETAT_TARGETbuttonbuttonbuttonbubblesbubblesbuttonsbuttonsbuttonsBUBBLING_PHASEBUBBLING_PHASEcancelBubblecancelBubblecancelablebuttonbuttoncancelablecancelablecancelBubblebuttonsbuttonsclientXclientXclientXcancelablecancelableclientYclientYclientYcancelBubblecancelBubblecomposedcomposedctrlKeyCAPTURING_PHASECAPTURING_PHASEctrlKeyctrlKeycurrentTargetclientXclientXcurrentTargetcurrentTargetdefaultPreventedclientYclientYdefaultPreventeddefaultPreventeddetailconstructorconstructordetaildetaileventPhasectrlKeyctrlKeyeventPhaseeventPhasefromElementcurrentTargetcurrentTargetexplicitOriginalTargetfromElementheightdefaultPreventeddefaultPreventedisTrustedisTrustedisPrimarydetaildetaillayerXlayerXisTrusteddeviceSessionIdeventPhaselayerYlayerYlayerXeventPhasefromElementmetaKeymetaKeylayerYfromElementisTrustedmovementXmovementXmetaKeyheightlayerXmovementYmovementYmovementXhwTimestamplayerYmozInputSourceoffsetXmovementYisPrimarymetaKeymozPressureoffsetYoffsetXisTrustedoffsetXoffsetXpageXoffsetYlayerXoffsetYoffsetYpageYpageXlayerYpageXoriginalTargetpathpageYmetaKeypageYpageXrelatedTargetpointerIdoffsetXrelatedTargetpageYreturnValuepointerTypeoffsetYscreenXrangeOffsetscreenXpressurepageXscreenYrangeParentscreenYrelatedTargetpageYshiftKeyregionshiftKeyreturnValuepointerIdsrcElementrelatedTargetsourceCapabilitiesscreenXpointerTypetargetreturnValuesrcElementscreenYpressuretimeStampscreenXtargetshiftKeyrelatedTargettoElementscreenYtimeStampsrcElementrotationtypeshiftKeytoElementtargetscreenXviewsrcElementtypetiltXscreenYwhichtargetviewtiltYshiftKeyxtimeStampwhichtimeStampsrcElementytypextoElementtarget viewytwisttiltX which typetiltY x viewtimeStamp y whichtoElement widthtype xview ywhich width x y 通过这个表格我们可以观察Event对象在不同浏览器之间结构是不同的,出人意料的是即使是在现代浏览器中事件对象也存在着差异.当然这篇文章可不是将所有的Event属性都将一遍,要知道不同的事件的Event对象结构是不同的.吐槽:本来是打算提供IE8的但是ie8不能使用addEventListener来监听事件懒得去搞ie那套数据了.currentTarget,target,srcElement,thiscurrentTarget一句话:哪个元素上监听的事件,event.currentTarget返回的就是这个对象的本身的引用.如果你的一个事件监听函数被注册到了多个DOM元素上,利用这个属性你就可以判断是谁触发的事件.this回调函数中的this === event.currentTarget.button.addEventListener(‘click’,function (event) { console.log(event.currentTarget === this); // true});targetevent.target和上面的三者不同,这里面涉及到了DOM中的一个基本知识事件冒泡和事件拦截.关于这两点我相信大家都已经了解了,即使不了解网上介绍的文章也有一大堆.我们用事件冒泡来举例,并且改写我们之前的那个例子: <div id=“wrap”> <button id=“button”>test click</button> </div> var wrap = document.getElementById(‘wrap’), button = document.getElementById(‘button’); // 注意我们监听的是wrap的click事件,而不是button的click事件 wrap.addEventListener(‘click’,function (event) { // event.target指向的是按钮,因为我们点击的是按钮 console.log(event.target === button && event.target === event.srcElement); // true // 当我们点击按钮触发的事件冒泡到了wrap,所以触发了wrap的click事件, // 此时currentTarget指向的是wrap console.log(wrap===this && wrap === event.currentTarget); // true // 直接打印event然后控制台中查看currentTaget会返回null // 你可以将他赋值到一个变量在打印输出这个变量 // see https://github.com/vuejs/vue/issues/6867#issuecomment-338195468 })在这个例子中,我们点击页面中的按钮,然后再按钮的包裹div中接收到了button冒泡上来的事件,这其中:this 和 currentTarget指向的都是添加了监听器的对象这里就是wraptarget 和 srcElement指向的是触发了事件的元素事件委托也是event.target最常见的用途之一:// Make a listvar ul = document.createElement(‘ul’);document.body.appendChild(ul);var li1 = document.createElement(’li’);var li2 = document.createElement(’li’);ul.appendChild(li1);ul.appendChild(li2);function hide(e){ // e.target 引用着 <li> 元素 // 不像 e.currentTarget 引用着其父级的 <ul> 元素. e.target.style.visibility = ‘hidden’;}// 添加监听事件到列表,当每个 <li> 被点击的时候都会触发。ul.addEventListener(‘click’, hide, false);https://developer.mozilla.org…srcElement简单理解event.srcElement === event.target.Event.srcElement 是标准的 Event.target 属性的一个别名。它只对老版本的IE浏览器有效。https://developer.mozilla.org…参考之前的表格后看来这个属性还没有被干掉,在目前最新的浏览器上它依然存在,不过已经不建议使用,除非你需要向下兼容.完整的事件编程EventTarget接口当我们在使用如下的方法的时候:elem.addEventListenerelem.removeEventListenerelem.dispatchEvent实际上是在使用EventTarget接口上的功能.例如我们可以创建一个新的EventTarget对象来添加事件监听: const a = new EventTarget; a.addEventListener(‘click’,()=>{ })但是这没有任何意义,因为这里没有事件被触发.我们仅仅是添加了事件监听器而已.为了达到我们目的通过编程的方式来执行完整的事件流程我们还需要完成如下的几步:继承EventTarget而不是直接使用EventTarget的实例,在事件监听函数中传递Event对象找个地方来触发这个事件首先我们来继承EventTarget对象:继承EventTarget在浏览器中大部分可以添加删除事件的对象都继承了EventTarget对象.你可以在控制台选择一个HTML元素一路查找原型链得到.但是他们进过重重继承,都有自己的独特属性和事件类型甚至是不同的构造函数.为了和已有的事件进行区分我们这里需要对EventTarget进行继承: // — 包装EventTarget开始 function MyEventTarget() { var target = document.createTextNode(null); this.addEventListener = target.addEventListener.bind(target); this.removeEventListener = target.removeEventListener.bind(target); this.dispatchEvent = target.dispatchEvent.bind(target); } MyEventTarget.prototype = EventTarget.prototype; // — 包装EventTarget结束 // — 创建我们继承EventTarget的构造函数 function myElem() { } myElem.prototype = new MyEventTarget; myElem.prototype.constructor = myElem; // 创建实例 const instance = new myElem();instance.addEventListener(‘click’,()=>{ // 现在我们实例可以监听事件了}); console.log(instance);继承的过程看似非常复杂,尤其是包装EventTarget显得多此一举.但是搞定EventTarget的继承确实花了我大量的时间去寻找解决方案.你完全可以编写自己的继承方式来去继承EventTarget,不过你会发现这其中的坑非常深.简单来说,EventTarget在JavaScript中真的就是一个接口,虽然是以函数的形式存在,但是它不是构造函数(这点在Chrome64 和firefox59后进行了修改).总之通过原型链继承的EventTarget统统无法工作,如果使用ES6的类式继承在现代浏览器中(Chrome64和firefox59后)都可以进行继承.详细参考:https://stackoverflow.com/que…创建我们的Event对象获取一个event:document.getElementById(‘button’).addEventListener(‘click’,(event)=>{ // event console.log(event); })如果你在浏览器中运行这段代码并且在控制台中查看,你会发现变量event的名称MouseEvent,如果你沿着原型链向上你会发现继承的是UIEvent再次向上查看则是真正的Event.事件触发中传递的第一个参数我们通常叫它event,所有的event对象都基于Event,但是这不意味着这种关系的窗户纸就只有一层,click事件中的event和Event之间就隔着一个UIEvent.通常随着event继承的层数越多,event对象身上的属性也会越来越多.现在我们来创建一个标准的Event对象:// 使用全局的Eventnew Event(’test’,{ // 事件类型 bubbles:false, // 是否冒泡 默认false cancelable:false,// 是否可以被取消 默认false });https://developer.mozilla.org…如果你在浏览器中观察这个对象,你会发现事件上常见的属性诸如:event.targetevent.currentTargetevent.preventDefault()都在这个new Event()返回的对象中,由于其他类型的事件都继承自Event这也解释了为什么事件对象中总是有这些属性.和继承EventTarget一样,使用Event的过程也同样艰难,总的来说使用Event的难点在于它有两套API:第一套比较新的API提供了现代的接口,也就是之前例子中的方式.在创建一个已有的事件的时候,你只需要使用全局的构造函数就可以,例如:new MouseEvent(’test’,/对应MouseEvent的参数选项/),但是缺点就是不支持IE浏览器.第二套API支持IE浏览器,但是使用过程比较繁琐使用Event.createEvent(/事件类型/)创建对应事件类型的Event对象,使用Event.initEvent()来初始化事件,并且提供对应事件类型的参数,如果你创建一个MouseEvent类型的事件InitEvent方法最多需要15个参数.这种情况下使用new MouseEvent()传入对象配置的形式就简单多了.一篇值得参考的文章,使用createEvent apihttps://www.cnblogs.com/ggz19…此外不同种类的事件,都有自己的全局构造函数,不同类型的构造函数的第二个参数中的选项也是不同的.其他的构造函数请参考这里.触发我们的事件触发事件就显得简单多了,我们需要使用EventTarget.dispatchEvent方法.在我们之前创建的实例上进行事件的触发: function MyEventTarget() { var target = document.createTextNode(null); this.addEventListener = target.addEventListener.bind(target); this.removeEventListener = target.removeEventListener.bind(target); this.dispatchEvent = target.dispatchEvent.bind(target); } MyEventTarget.prototype = EventTarget.prototype; function myElem() { } myElem.prototype = new MyEventTarget; myElem.prototype.constructor = myElem; const instance = new myElem(); instance.addEventListener(’test’, (event) => { console.log(event); // 监听事件并且打印实例 }); const myEvent = new Event(’test’); // 创建Event实例 instance.dispatchEvent(myEvent); // 触发事件当你调用dispatchEvent的时候,EventTarget会按照对应事件注册的顺序来同步执行这些事件监听器.如果在事件监听器中调用了event.preventDefault,那么dispatchEvent就返回false反之返回true(前提是cancleable为true).详细参考https://developer.mozilla.org…编程式的事件触发我们在页面中来一次具体的实战,首先建立如下的HTML结构:<div id=“wrap”> <button id=“button”>test click</button></div>我们在#wrap中监听click事件,然后在#button触发click事件.这样我们可以练习一下Event中bubbles(允许冒泡)参数的使用,另外还可以测试click事件中的Event对象如果不是MouseEvent的实例那么监听器是否会被触发. const button = document.getElementById(‘button’), wrap = document.getElementById(‘wrap’); wrap.addEventListener(‘click’, (event) => { console.log(event); // 打印event对象 }); const myEvent1 = new Event(‘click’, { bubbles: false, // 不可以冒泡 }); const myEvent2 = new Event(‘click’, { bubbles: true, // 可以冒泡 }); button.dispatchEvent(myEvent1); // 这次没有打印出内容 button.dispatchEvent(myEvent2); // 这次打印出了内容结论很明确:dispatchEvent执行的时候只要是Event的实例且类型相同那么监听器就会被触发.bubbles参数可以控制该事件是否允许冒泡 ...

March 30, 2019 · 2 min · jiezi

【React深入】React事件机制

关于React事件的疑问1.为什么要手动绑定this2.React事件和原生事件有什么区别3.React事件和原生事件的执行顺序,可以混用吗4.React事件如何解决跨浏览器兼容5.什么是合成事件下面是我阅读过源码后,将所有的执行流程总结出来的流程图,不会贴代码,如果你想阅读代码看看具体是如何实现的,可以根据流程图去源码里寻找。事件注册组件装载 / 更新。通过lastProps、nextProps判断是否新增、删除事件分别调用事件注册、卸载方法。调用EventPluginHub的enqueuePutListener进行事件存储获取document对象。根据事件名称(如onClick、onCaptureClick)判断是进行冒泡还是捕获。判断是否存在addEventListener方法,否则使用attachEvent(兼容IE)。给document注册原生事件回调为dispatchEvent(统一的事件分发机制)。事件存储EventPluginHub负责管理React合成事件的callback,它将callback存储在listenerBank中,另外还存储了负责合成事件的Plugin。EventPluginHub的putListener方法是向存储容器中增加一个listener。获取绑定事件的元素的唯一标识key。将callback根据事件类型,元素的唯一标识key存储在listenerBank中。listenerBank的结构是:listenerBank[registrationName][key]。例如:{ onClick:{ nodeid1:()=>{…} nodeid2:()=>{…} }, onChange:{ nodeid3:()=>{…} nodeid4:()=>{…} }}事件触发 / 执行这里的事件执行利用了React的批处理机制,在前一篇的【React深入】setState执行机制中已经分析过,这里不再多加分析。触发document注册原生事件的回调dispatchEvent获取到触发这个事件最深一级的元素例如下面的代码:首先会获取到this.child <div onClick={this.parentClick} ref={ref => this.parent = ref}> <div onClick={this.childClick} ref={ref => this.child = ref}> test </div> </div>遍历这个元素的所有父元素,依次对每一级元素进行处理。构造合成事件。将每一级的合成事件存储在eventQueue事件队列中。遍历eventQueue。通过isPropagationStopped判断当前事件是否执行了阻止冒泡方法。如果阻止了冒泡,停止遍历,否则通过executeDispatch执行合成事件。释放处理完成的事件。react在自己的合成事件中重写了stopPropagation方法,将isPropagationStopped设置为true,然后在遍历每一级事件的过程中根据此遍历判断是否继续执行。这就是react自己实现的冒泡机制。合成事件调用EventPluginHub的extractEvents方法。循环所有类型的EventPlugin(用来处理不同事件的工具方法)。在每个EventPlugin中根据不同的事件类型,返回不同的事件池。在事件池中取出合成事件,如果事件池是空的,那么创建一个新的。根据元素nodeid(唯一标识key)和事件类型从listenerBink中取出回调函数返回带有合成事件参数的回调函数总流程将上面的四个流程串联起来。为什么要手动绑定this通过事件触发过程的分析,dispatchEvent调用了invokeGuardedCallback方法。function invokeGuardedCallback(name, func, a) { try { func(a); } catch (x) { if (caughtError === null) { caughtError = x; } }}可见,回调函数是直接调用调用的,并没有指定调用的组件,所以不进行手动绑定的情况下直接获取到的this是undefined。这里可以使用实验性的属性初始化语法 ,也就是直接在组件声明箭头函数。箭头函数不会创建自己的this,它只会从自己的作用域链的上一层继承this。因此这样我们在React事件中获取到的就是组件本身了。和原生事件有什么区别React 事件使用驼峰命名,而不是全部小写。通过 JSX , 你传递一个函数作为事件处理程序,而不是一个字符串。例如,HTML:<button onclick=“activateLasers()"> Activate Lasers</button>在 React 中略有不同:<button onClick={activateLasers}> Activate Lasers</button>另一个区别是,在 React 中你不能通过返回 false 来阻止默认行为。必须明确调用 preventDefault 。由上面执行机制我们可以得出:React自己实现了一套事件机制,自己模拟了事件冒泡和捕获的过程,采用了事件代理,批量更新等方法,并且抹平了各个浏览器的兼容性问题。React事件和原生事件的执行顺序 componentDidMount() { this.parent.addEventListener(‘click’, (e) => { console.log(‘dom parent’); }) this.child.addEventListener(‘click’, (e) => { console.log(‘dom child’); }) document.addEventListener(‘click’, (e) => { console.log(‘document’); }) } childClick = (e) => { console.log(‘react child’); } parentClick = (e) => { console.log(‘react parent’); } render() { return ( <div onClick={this.parentClick} ref={ref => this.parent = ref}> <div onClick={this.childClick} ref={ref => this.child = ref}> test </div> </div>) }执行结果:由上面的流程我们可以理解:react的所有事件都挂载在document中当真实dom触发后冒泡到document后才会对react事件进行处理所以原生的事件会先执行然后执行react合成事件最后执行真正在document上挂载的事件react事件和原生事件可以混用吗?react事件和原生事件最好不要混用。原生事件中如果执行了stopPropagation方法,则会导致其他react事件失效。因为所有元素的事件将无法冒泡到document上。由上面的执行机制不难得出,所有的react事件都将无法被注册。合成事件、浏览器兼容 function handleClick(e) { e.preventDefault(); console.log(‘The link was clicked.’); }这里, e 是一个合成的事件。 React 根据 W3C 规范 定义了这个合成事件,所以你不需要担心跨浏览器的兼容性问题。事件处理程序将传递 SyntheticEvent 的实例,这是一个跨浏览器原生事件包装器。 它具有与浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault() ,在所有浏览器中他们工作方式都相同。每个 SyntheticEvent 对象都具有以下属性:boolean bubblesboolean cancelableDOMEventTarget currentTargetboolean defaultPreventednumber eventPhaseboolean isTrustedDOMEvent nativeEventvoid preventDefault()boolean isDefaultPrevented()void stopPropagation()boolean isPropagationStopped()DOMEventTarget targetnumber timeStampstring typeReact合成的SyntheticEvent采用了事件池,这样做可以大大节省内存,而不会频繁的创建和销毁事件对象。另外,不管在什么浏览器环境下,浏览器会将该事件类型统一创建为合成事件,从而达到了浏览器兼容的目的。推荐阅读【React深入】setState的执行机制 ...

March 5, 2019 · 1 min · jiezi

Javascript 时间循环event loop

都知道javascript是单线程,那么问题来了,既然是单线程顺序执行,那怎么做到异步的呢?我们理解的单线程应该是这样的,排着一个个来,是同步执行。现实中js是这样的 setTimeout(function() { console.log(1); }); new Promise(function(resolve, reject) { console.log(2) resolve(3) }).then(function(val) { console.log(val); }) console.log(4) //执行结果为 2、4、3、1结果告诉我们,js是单线程没错,不过不是逐行同步执行。那我们就来解析一下既然有异步,那顺序是怎样的?这些执行顺序规则就是理解eventLoop的要点,继续往下。上图为我录制的chrome控制代码台执行顺序,虽然能看出执行顺序但我们还是懵逼的,我们不知道规则,不懂就要问。搜索了很多官方、个人博客得到了一堆词:js引擎、主线程、事件表、事件队列、宏任务、微任务,彻底懵逼。。。不急不急一个个来,我们进入刨根问底状态js引擎总结一句话就是解析优化代码 **制定执行规则 具体规则往下看主线程总结一句话执行js引擎优化并排列顺序后的代码事件表(event table)执行代码过程中,异步的回调,例如(setTimeout,ajax回调)注册回调事件到event table事件队列当事件回调结束,事件表(event table)会将事件移入到事件队列(event queue)宏任务和微任务宏任务包含的事件事件浏览器nodeI/O✅✅setTimeout✅✅setInterval✅✅setImmediate❌✅requestAnimationFrame✅❌微任务包含的事件事件浏览器nodeI/O✅✅process.nextTick❌✅MutationObserver✅❌Promise.then catch finally✅✅很多博客是这样说的:浏览器会不断从task队列中按顺序取task执行,每执行完一个task都会检查microtask队列是否为空(执行完一个task的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去task队列中取下一个task执行说实话不是太理解,那么我就以自己的方式去学习和理解为了更好的理解我们再看代码 console.log(‘1’); setTimeout(function() { console.log(‘2’); new Promise(function(resolve) { console.log(‘3’); resolve(); }).then(function() { console.log(‘4’) }) }) new Promise(function(resolve) { console.log(‘5’); resolve(); }).then(function() { console.log(‘6’) }) setTimeout(function() { console.log(‘7’); new Promise(function(resolve) { console.log(‘8’); resolve(); }).then(function() { console.log(‘9’) }) }) //执行结果:1、5、6、2、3、4、7、8、9有图为证我没骗你再来个动图我们具体看看浏览器的执行顺序首先js引擎,区分是直接执行(同步代码),再执行异步代码,如果是异步再区分是宏任务还是微任务,分别放入两个任务队列,然后开始执行,每执行完一个宏任务,扫一遍微任务队列并全部执行,此时形成一次eventLoop循环。以此规则不停的执行下去就是我们所听到的事件循环。我再补充一点,可以理解js引擎一开始把整个script当做一个宏任务,这样里边的就更容易理解了,开始就执行script宏任务,解析到宏任务里边又包含同步代码和异步代码(宏任务和微任务)依次执行顺序形成eventLoop。欢迎吐槽点赞评论!文章参考学习:https://www.jianshu.com/p/12b…https://juejin.im/post/59e85e...https://segmentfault.com/a/11…

January 15, 2019 · 1 min · jiezi

javascript事件节流和防抖

事件节流和防抖是为了解决开发过程中遇到性能问题,常见于onscroll、onresize,频繁点击button等事件节流设置一个时间间隔,时间间隔内只允许执行一次,好像客运站大巴,到点才会走。问题:多年前遇到过一个onresize问题,页面满屏布局,模块很多dom结构也相对复杂。所以在窗口频繁快速变化大小的时候页面反应异常卡顿。解决办法:说实话当初意识到是性能问题不过不知道怎么解决,搜索了很多相关问题,最后在https://stackoverflow.com找到…,遗憾的是具体的链接忘了。不过也是因为这个问题宋词爱上了这个网站,问题答案很靠谱。//问题解决的原理就是事件节流window.onresize = () => { console.log(‘resize’)}随便晃几下执行了150多次,这也就是卡顿的根源。解决这个问题我们需要减少执行次数。 let timer = null window.onresize = () => { console.log(timer) if (!timer) { timer = setTimeout(() => { callBack() timer = null }, 1000) } } function callBack() { console.log(‘resize’) }这样不管我们一秒内晃动多少次callBack只执行一次,问题解决接下在封装一下//封装前我们先思考一下,首先既然是封装那么事件不一定都是onersize、间隔时间得有用户设置、callBack得是用户写。**其实我们只关心callBack,和执行间隔时间,恰好事件都有回调 function callBack() { console.log(‘resize’) } function throttle(callBack, time) { let timer = null //timer状态要常驻内存,这里做了一个闭包 return function() { if (!timer) { timer = setTimeout(() => { callBack() timer = null }, time) } } } window.addEventListener(‘resize’, throttle(callBack, 1000), false)测试完美!防抖常用于验证码防刷,按钮频繁点击导致发起多次请求给服务端造成压力,代码策略是,一段时间内事件统一处理,防抖原理类似 趴活的黑车,永远喊得就差一位上车就走,等你上去他接着喊就差一位上车就走。。。 擦有点乱 看代码吧待续。。。您的吐槽or点赞是我的动力! ...

January 11, 2019 · 1 min · jiezi

以中间件,路由,跨进程事件的姿势使用WebSocket

通过参考koa中间件,socket.io远程事件调用,以一种新的姿势来使用WebSocket。浏览器端浏览器端使用WebSocket很简单// Create WebSocket connection.const socket = new WebSocket(‘ws://localhost:8080’);// Connection openedsocket.addEventListener(‘open’, function (event) { socket.send(‘Hello Server!’);});// Listen for messagessocket.addEventListener(‘message’, function (event) { console.log(‘Message from server ‘, event.data);});MDN关于WebSocket的介绍能注册的事件有onclose,onerror,onmessage,onopen。用的比较多的是onmessage,从服务器接受到数据后,会触发message事件。通过注册相应的事件处理函数,可以根据后端推送的数据做相应的操作。如果只是写个demo,单单输出后端推送的信息,如下使用即可:socket.addEventListener(‘message’, function (event) { console.log(‘Message from server ‘, event.data);});实际使用过程中,我们需要判断后端推送的数据然后执行相应的操作。比如聊天室应用中,需要判断消息是广播的还是私聊的或者群聊的,以及是纯文字信息还是图片等多媒体信息。这时message处理函数里可能就是一堆的if else。那么有没有什么别的优雅的姿势呢?答案就是中间件与事件,跨进程的事件的发布与订阅。在说远程事件发布订阅之前,需要先从中间件开始,因为后面实现的远程事件发布订阅是基于中间件的。中间件前面说了,在WebSocket实例上可以注册事件有onclose,onerror,onmessage,onopen。每一个事件的处理函数里可能需要做各种判断,特别是message事件。参考koa,可以将事件处理函数以中间件方式来进行使用,将不同的操作逻辑分发到不同的中间件中,比如聊天室应用中,聊天信息与系统信息(比如用户登录属于系统信息)是可以放到不同的中间件中处理的。koa提供use接口来注册中间件。我们针对不同的事件提供相应的中间件注册接口,并且对原生的WebSocket做封装。export default class EasySocket{ constructor(config) { this.url = config.url; this.openMiddleware = []; this.closeMiddleware = []; this.messageMiddleware = []; this.errorMiddleware = []; this.openFn = Promise.resolve(); this.closeFn = Promise.resolve(); this.messageFn = Promise.resolve(); this.errorFn = Promise.resolve(); } openUse(fn) { this.openMiddleware.push(fn); return this; } closeUse(fn) { this.closeMiddleware.push(fn); return this; } messageUse(fn) { this.messageMiddleware.push(fn); return this; } errorUse(fn) { this.errorMiddleware.push(fn); return this; }}通过xxxUse注册相应的中间件。 xxxMiddleware中就是相应的中间件。xxxFn 中间件通过compose处理后的结构再添加一个connect方法,处理相应的中间件并且实例化原生WebSocketconnect(url) { this.url = url || this.url; if (!this.url) { throw new Error(‘url is required!’); } try { this.socket = new WebSocket(this.url, ’echo-protocol’); } catch (e) { throw e; } this.openFn = compose(this.openMiddleware); this.socket.addEventListener(‘open’, (event) => { let context = { client: this, event }; this.openFn(context).catch(error => { console.log(error) }); }); this.closeFn = compose(this.closeMiddleware); this.socket.addEventListener(‘close’, (event) => { let context = { client: this, event }; this.closeFn(context).then(() => { }).catch(error => { console.log(error) }); }); this.messageFn = compose(this.messageMiddleware); this.socket.addEventListener(‘message’, (event) => { let res; try { res = JSON.parse(event.data); } catch (error) { res = event.data; } let context = { client: this, event, res }; this.messageFn(context).then(() => { }).catch(error => { console.log(error) }); }); this.errorFn = compose(this.errorMiddleware); this.socket.addEventListener(’error’, (event) => { let context = { client: this, event }; this.errorFn(context).then(() => { }).catch(error => { console.log(error) }); }); return this; }使用koa-compose模块处理中间件。注意context传入了哪些东西,后续定义中间件的时候都已使用。compose的作用可看这篇文章 傻瓜式解读koa中间件处理模块koa-compose然后就可以使用了:new EasySocket() .openUse((context, next) => { console.log(“open”); next(); }) .closeUse((context, next) => { console.log(“close”); next(); }) .errorUse((context, next) => { console.log(“error”, context.event); next(); }) .messageUse((context, next) => { //用户登录处理中间件 if (context.res.action === ‘userEnter’) { console.log(context.res.user.name+’ 进入聊天室’); } next(); }) .messageUse((context, next) => { //创建房间处理中间件 if (context.res.action === ‘createRoom’) { console.log(‘创建房间 ‘+context.res.room.anme); } next(); }) .connect(‘ws://localhost:8080’)可以看到,用户登录与创建房间的逻辑放到两个中间件中分开处理。不足之处就是每个中间件都要判断context.res.action,而这个context.res就是后端返回的数据。怎么消除这个频繁的if判断呢? 我们实现一个简单的消息处理路由。路由定义消息路由中间件messageRouteMiddleware.jsexport default (routes) => { return async (context, next) => { if (routes[context.req.action]) { await routescontext.req.action; } else { console.log(context.req) next(); } }}定义路由router.jsexport default { userEnter:function(context,next){ console.log(context.res.user.name+’ 进入聊天室’); next(); }, createRoom:function(context,next){ console.log(‘创建房间 ‘+context.res.room.anme); next(); }}使用:new EasySocket() .openUse((context, next) => { console.log(“open”); next(); }) .closeUse((context, next) => { console.log(“close”); next(); }) .errorUse((context, next) => { console.log(“error”, context.event); next(); }) .messageUse(messageRouteMiddleware(router))//使用消息路由中间件,并传入定义好的路由 .connect(‘ws://localhost:8080’)一切都变得美好了,感觉就像在使用koa。想一个问题,当接收到后端推送的消息时,我们需要做相应的DOM操作。比如路由里面定义的userEnter,我们可能需要在对应的函数里操作用户列表的DOM,追加新用户。这使用原生JS或JQ都是没有问题的,但是如果使用vue,react这些,因为是组件化的,用户列表可能就是一个组件,怎么访问到这个组件实例呢?(当然也可以访问vuex,redux的store,但是并不是所有组件的数据都是用store管理的)。我们需要一个运行时注册中间件的功能,然后在组件的相应的生命周期钩子里注册中间件并且传入组件实例运行时注册中间件,修改如下代码:messageUse(fn, runtime) { this.messageMiddleware.push(fn); if (runtime) { this.messageFn = compose(this.messageMiddleware); } return this; }修改 messageRouteMiddleware.jsexport default (routes,component) => { return async (context, next) => { if (routes[context.req.action]) { context.component=component;//将组件实例挂到context下 await routescontext.req.action; } else { console.log(context.req) next(); } }}类似vue mounted中使用mounted(){ let client = this.$wsClients.get(“im”);//获取指定EasySocket实例 client.messageUse(messageRouteMiddleware(router,this),true)//运行时注册中间件,并传入定义好的路由以及当前组件中的this}路由中通过 context.component 即可访问到当前组件。完美了吗?每次组件mounted 都注册一次中间件,问题很大。所以需要一个判断中间件是否已经注册的功能。也就是一个支持具名注册中间件的功能。这里就暂时不实现了,走另外一条路,也就是之前说到的远程事件的发布与订阅,我们也可以称之为跨进程事件。跨进程事件看一段socket.io的代码:Server (app.js)var app = require(‘http’).createServer(handler)var io = require(‘socket.io’)(app);var fs = require(‘fs’);app.listen(80);function handler (req, res) { fs.readFile(__dirname + ‘/index.html’, function (err, data) { if (err) { res.writeHead(500); return res.end(‘Error loading index.html’); } res.writeHead(200); res.end(data); });}io.on(‘connection’, function (socket) { socket.emit(’news’, { hello: ‘world’ }); socket.on(‘my other event’, function (data) { console.log(data); });});Client (index.html)<script src="/socket.io/socket.io.js"></script><script> var socket = io(‘http://localhost’); socket.on(’news’, function (data) { console.log(data); socket.emit(‘my other event’, { my: ‘data’ }); });</script>注意力转到这两部分:服务端 socket.emit(’news’, { hello: ‘world’ }); socket.on(‘my other event’, function (data) { console.log(data); });客户端 var socket = io(‘http://localhost’); socket.on(’news’, function (data) { console.log(data); socket.emit(‘my other event’, { my: ‘data’ }); });使用事件,客户端通过on订阅’news’事件,并且当触发‘new’事件的时候通过emit发布’my other event’事件。服务端在用户连接的时候发布’news’事件,并且订阅’my other event’事件。一般我们使用事件的时候,都是在同一个页面中on和emit。而socket.io的神奇之处就是同一事件的on和emit是分别在客户端和服务端,这就是跨进程的事件。那么,在某一端emit某个事件的时候,另一端如果on监听了此事件,是如何知道这个事件emit(发布)了呢?没有看socket.io源码之前,我设想应该是emit方法里做了某些事情。就像java或c#,实现rpc的时候,可以依据接口定义动态生成实现(也称为代理),动态实现的(代理)方法中,就会将当前方法名称以及参数通过相应协议进行序列化,然后通过http或者tcp等网络协议传输到RPC服务端,服务端进行反序列化,通过反射等技术调用本地实现,并返回执行结果给客户端。客户端拿到结果后,整个调用完成,就像调用本地方法一样实现了远程方法的调用。看了socket.io emit的代码实现后,思路也是大同小异,通过将当前emit的事件名和参数按一定规则组合成数据,然后将数据通过WebSocket的send方法发送出去。接收端按规则取到事件名和参数,然后本地触发emit。(注意远程emit和本地emit,socket.io中直接调用的是远程emit)。下面是实现代码,事件直接用的emitter模块,并且为了能自定义emit事件名和参数组合规则,以中间件的方式提供处理方法:export default class EasySocket extends Emitter{//继承Emitter constructor(config) { this.url = config.url; this.openMiddleware = []; this.closeMiddleware = []; this.messageMiddleware = []; this.errorMiddleware = []; this.remoteEmitMiddleware = [];//新增的部分 this.openFn = Promise.resolve(); this.closeFn = Promise.resolve(); this.messageFn = Promise.resolve(); this.errorFn = Promise.resolve(); this.remoteEmitFn = Promise.resolve();//新增的部分 } openUse(fn) { this.openMiddleware.push(fn); return this; } closeUse(fn) { this.closeMiddleware.push(fn); return this; } messageUse(fn) { this.messageMiddleware.push(fn); return this; } errorUse(fn) { this.errorMiddleware.push(fn); return this; } //新增的部分 remoteEmitUse(fn, runtime) { this.remoteEmitMiddleware.push(fn); if (runtime) { this.remoteEmitFn = compose(this.remoteEmitMiddleware); } return this; } connect(url) { … //新增部分 this.remoteEmitFn = compose(this.remoteEmitMiddleware); } //重写emit方法,支持本地调用以远程调用 emit(event, args, isLocal = false) { let arr = [event, args]; if (isLocal) { super.emit.apply(this, arr); return this; } let evt = { event: event, args: args } let remoteEmitContext = { client: this, event: evt }; this.remoteEmitFn(remoteEmitContext).catch(error => { console.log(error) }) return this; }}下面是一个简单的处理中间件:client.remoteEmitUse((context, next) => { let client = context.client; let event = context.event; if (client.socket.readyState !== 1) { alert(“连接已断开!”); } else { client.socket.send(JSON.stringify({ type: ’event’, event: event.event, args: event.args })); next(); } })意味着调用client.emit(‘chatMessage’,{ from:‘admin’, masg:“Hello WebSocket”});就会组合成数据{ type: ’event’, event: ‘chatMessage’, args: { from:‘admin’, masg:“Hello WebSocket” }}发送出去。服务端接受到这样的数据,可以做相应的数据处理(后面会使用nodejs实现类似的编程模式),也可以直接发送给别的客户端。客户受到类似的数据,可以写专门的中间件进行处理,比如:client.messageUse((context, next) => { if (context.res.type === ’event’) { context.client.emit(context.res.event, context.res.args, true);//注意这里的emit是本地emit。 } next();})如果本地订阅的chatMessage事件,回到函数就会被触发。在vue或react中使用,也会比之前使用路由的方式简单mounted() { let client = this.$wsClients.get(“im”); client.on(“chatMessage”, data => { let isSelf = data.from.id == this.user.id; let msg = { name: data.from.name, msg: data.msg, createdDate: data.createdDate, isSelf }; this.broadcastMessageList.push(msg); });}组件销毁的时候移除相应的事件订阅即可,或者清空所有事件订阅destroyed() { let client = this.$wsClients.get(“im”); client.removeAllListeners();}心跳重连核心代码直接从websocket-heartbeat-js copy过来的(用npm包,还得在它的基础上再包一层),相关文章 初探和实现websocket心跳重连。核心代码: heartCheck() { this.heartReset(); this.heartStart(); } heartStart() { this.pingTimeoutId = setTimeout(() => { //这里发送一个心跳,后端收到后,返回一个心跳消息 this.socket.send(this.pingMsg); //接收到心跳信息说明连接正常,会执行heartCheck(),重置心跳(清除下面定时器) this.pongTimeoutId = setTimeout(() => { //此定时器有运行的机会,说明发送ping后,设置的超时时间内未收到返回信息 this.socket.close();//不直接调用reconnect,避免旧WebSocket实例没有真正关闭,导致不可预料的问题 }, this.pongTimeout); }, this.pingTimeout); } heartReset() { clearTimeout(this.pingTimeoutId); clearTimeout(this.pongTimeoutId); }最后源码地址:easy-socket-browsernodejs实现的类似的编程模式(有空再细说):easy-socket-node实现的聊天室例子:online chat demo 聊天室前端源码:lazy-mock-im聊天室服务端源码:lazy-mock ...

November 5, 2018 · 4 min · jiezi