深入理解-JavaScript-回调函数

作者:Nilesh Sanyal翻译:疯狂的技术宅 原文:https://dzone.com/articles/ja... 未经允许严禁转载 JavaScript回调函数是成为一名成功的 JavaScript 开发人员必须要了解的一个重要概念。但是我相信,在阅读本文之后,你将能够克服以前使用回调方法遇到的所有障碍。 在开始之前,首先要确保我们对函数的理解是扎实的。 快速回顾:JavaScript 函数什么是函数?函数是在其中有一组代码的逻辑构件,用来执行特定任务。实际上为了易于调试和维护,函数允许以更有组织的方式去编写代码。函数还允许代码重用。 你只需定义一次函数,然后在需要时去调用它,而不必一次又一次地编写相同的代码。 声明一个函数现在,让我们看看如何在 javascript 中声明一个函数。 使用函数的构造函数: 在这种方法中,函数是在“函数”的构造函数的帮助下创建的。从技术上讲,这种方法比使用函数表达式语法和函数声明语句语法去声明函数的方法效率要低。使用函数表达式: 通常这种方法与变量分配相同。简而言之,函数主体被视为一个表达式,并且该表达式被分配给一个变量。使用这种语法定义的函数可以是命名函数或匿名函数。没有名称的函数被称为匿名函数。匿名函数是自调用的,这意味着它会自动调用起自身。这种行为也称为立即调用的函数表达式(IIFE)。 使用函数声明: 这种方法是 JavaScript 中常用的老派方法。在关键字“function”之后,你必须指定函数的名称。之后,如果函数接受多个参数或参数,也需要提及它们。虽然这部分是完全可选的。在函数体中,函数必须将一个值返回给调用方。遇到 return 语句后,该函数将会停止执行。在函数内部,参数将会充当局部变量。 同样,在函数内部声明的变量是该函数的局部变量。局部变量只能在该函数内访问,因此具有相同名称的变量可以轻松地用于不同的函数。 调用一个函数在下列任何一种情况下,将调用之前声明的函数: 发生事件时,例如,用户单击按钮,或者用户从下拉列表中选择某些选项等等。从 javascript 代码中调用该函数时。该函数可以自动调用,我们已经在匿名函数表达式中进行了讨论。() 运算符调用该函数。 什么是回调函数?按照 MDN 的描述:回调函数是作为参数传给另一个函数的函数,然后通过在外部函数内部调用该回调函数以完成某种操作。 让我用人话解释一下,回调函数是一个函数,将会在另一个函数完成执行后立即执行。回调函数是一个作为参数传给另一个 JavaScript 函数的函数。这个回调函数会在传给的函数内部执行。 在 JavaScript 中函数被看作是一类对象。对于一类对象,我们的意思是指数字、函数或变量可以与语言中的其他实体相同。作为一类对象,可以将函数作为变量传给其他函数,也可以从其他函数中返回这些函数。 可以执行这种操作的函数被称为高阶函数。回调函数实际上是一种模式。 “模式”一词表示解决软件开发中常见问题的某种行之有效的方法。最好将回调函数作为回调模式去使用。 为什么我们需要回调客户端 JavaScript 在浏览器中运行,并且浏览器的主进程是单线程事件循环。如果我们尝试在单线程事件循环中执行长时间运行的操作,则会阻止该过程。从技术上讲这是不好的,因为过程在等待操作完成时会停止处理其他事件。 例如,alert 语句被视为浏览器中 javascript 中的阻止代码之一。如果运行 alert,则在关闭 alert 对话框窗口之前,你将无法在浏览器中进行任何交互。为了防止阻塞长时间运行的操作,我们使用了回调。 让我们深入研究一下,以便使你准确了解在哪种情况下使用回调。 在上面的代码片段中,首先执行 getMessage()函数,然后执行 displayMessage() 。两者都在浏览器的控制台窗口中显示了一条消息,并且都立即执行。 在某些情况下,一些代码不会立即执行。例如,如果我们假设 getMessage() 函数执行 API 调用,则必须将请求发送到服务器并等待响应。这时我们应该如何处理呢? 如何使用回调函数我认为与其告诉你 JavaScript 回调函数的语法,不如在前面的例子中实现回调函数更好。修改后的代码段显示在下面的截图中。 为了使用回调函数,我们需要执行某种无法立即显示结果的任务。为了模拟这种行为,我们用 JavaScript 的 setTimeout() 函数。该函数会暂停两秒钟,然后在控制台窗口中显示消息“ Hi,there”。 ...

November 4, 2019 · 2 min · jiezi

JavaScript异步编程

前言从我们一开始学习JavaScript的时候就听到过一段话:JS是单线程的,天生异步,适合IO密集型,不适合CPU密集型。但是,多数JavaScript开发者从来没有认真思考过自己程序中的异步到底是怎么出现的,以及为什么会出现,也没有探索过处理异步的其他方法。到目前为止,还有很多人坚持认为回调函数就完全够用了。 但是,随着JavaScript面临的需求越来越多,它可以运行在浏览器、服务器、甚至是嵌入式设备上,为了满足这些需求,JavaScript的规模和复杂性也在持续增长,使用回调函数来管理异步也越来越让人痛苦,这一切,都需要更强大、更合理的异步方法,通过这篇文章,我想对目前已有JavaScript异步的处理方式做一个总结,同时试着去解释为什么会出现这些技术,让大家对JavaScript异步编程有一个更宏观的理解,让知识变得更体系化一些。 正文Step1 - 回调函数回调函数大家肯定都不陌生,从我们写一段最简单的定时器开始: setTimeout(function () { console.log('Time out');}, 1000);定时器里面的匿名函数就是一个回调函数,因为在JS中函数是一等公民,所以它可以像其他变量一样作为参数进行传递。这样看来,通过回调函数来处理异步挺好的,写着也顺手,为什么要用别的方法呢? 我们来看这样一个需求: 上面是微信小程序的登录时序图,我们的需求和它类似但又有些差别,想要获取一段业务数据,整个过程分为3步: 调用秘钥接口,获取key携带key调用登录接口,获取token和userId携带token和userId调用业务接口,获取数据可能上述步骤和实际业务中的有些出入,但是却可以用来说明问题,请大家谅解。 我们写一段代码来实现上述需求: let key, token, userId;$.ajax({ type: 'get', url: 'http://localhost:3000/apiKey', success: function (data) { key = data; $.ajax({ type: 'get', url: 'http://localhost:3000/getToken', data: { key: key }, success: function (data) { token = data.token; userId = data.userId; $.ajax({ type: 'get', url: 'http://localhost:3000/getData', data: { token: token, userId: userId }, success: function (data) { console.log('业务数据:', data); }, error: function (err) { console.log(err); } }); }, error: function (err) { console.log(err); } }); }, error: function (err) { console.log(err); }});可以看到,整段代码充满了回调嵌套,代码不仅在纵向扩展,横向也在扩展。我相信,对于任何人来说,调试起来都会很困难,我们不得不从一个函数跳到下一个,再跳到下一个,在整个代码中跳来跳去以查看流程,而最终的结果藏在整段代码的中间位置。真实的JavaScript程序代码可能要混乱的多,使得这种追踪难度会成倍增加。这就是我们常说的回调地狱(Callback Hell)。 ...

July 2, 2019 · 4 min · jiezi

JavaScript异步流程控制

JavaScript特性JavaScript属于单线程语言,即在同一时间,只能执行一个任务。在执行任务时,所有任务需要排队,前一个任务结束,才会执行后一个任务。当我们向后台发送一个请求时,主线程读取 “向后台发送请求” 这个事件并执行之后,到获取后台返回的数据这一过程会有段时间间隔,这时CPU处于空闲阶段,直到获取数据后再继续执行后面的任务,这就降低了用户体验度,使得页面加载变慢。于是,所有任务可以分成两种:同步任务和异步任务。同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务:不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制,这个过程会不断重复。“任务队列"是一个事件的队列(也可以理解成消息的队列),IO设备完成一项任务,就在"任务队列"中添加一个事件,表示相关的异步任务可以进入"执行栈"了。主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)。JavaScript异步实现的5种方式1. callback(回调函数)回调函数,也被称为高阶函数,是一个被作为参数传递给另一个函数并在该函数中被调用的函数。看一个在JQuery中简单普遍的例子:// 注意: click方法是一个函数而不是变量$("#button”).click(function() { alert(“Button Clicked”);}); 可以看到,上述例子将一个函数作为参数传递给了click方法,click方法会调用该函数,这是JavaScript中回调函数的典型用法,它在jQuery中广泛被使用。它不会立即执行,因为我们没有在后面加( ),而是在点击事件发生时才会执行。比如,我们要下载一个gif,但是不希望在下载的时候阻断其他程序,可以实现如下:downloadPhoto(‘http://coolcats.com/cat.gif', handlePhoto)function handlePhoto (error, photo) { if (error) { console.error(‘Download error!’, error); } else { console.log(‘Download finished’, photo); }}console.log(‘Download started’)首先声明handlePhoto函数,然后调用downloadPhoto函数并传递handlePhoto作为其回调函数,最后打印出“Download started”。请注意,handlePhoto尚未被调用,它只是被创建并作为回调传入downloadPhoto。但直到downloadPhoto完成其任务后才能运行,这可能需要很长时间,具体取决于Internet连接的速度,所以运行代码后,会先打印出Download started。这个例子是为了说明两个重要的概念:handlePhoto回调只是稍后存储一些事情的一种方式;事情发生的顺序不是从顶部到底部读取,而是基于事情完成时跳转;1. callback hell(回调地狱)var fs = require(‘fs’);/** * 如果三个异步api操作的话 无法保证他们的执行顺序 * 我们在每个操作后用回调函数就可以保证执行顺序 */ fs.readFile(’./data1.json’, ‘utf8’, function(err, data){ if (err) { throw err; } else { console.log(data); fs.readFile(’./data2.json’, ‘utf8’, function(err, data){ if (err) { throw err; } else { console.log(data) fs.readFile(’./data3.json’, ‘utf8’, function(err, data){ if (err) { throw err; } else { console.log(data); } }) } }) }})有没有看到这些以"})“结尾的金字塔结构?由于回调函数是异步的,在上面的代码中每一层的回调函数都需要依赖上一层的回调执行完,所以形成了层层嵌套的关系最终形成类似上面的回调地狱。2. 代码层面解决回调地狱1. 保持代码简短var form = document.querySelector(‘form’)form.onsubmit = function formSubmit (submitEvent) { var name = document.querySelector(‘input’).value request({ uri: “http://example.com/upload", body: name, method: “POST” }, function postResponse (err, response, body) { var statusMessage = document.querySelector(’.status’) if (err) return statusMessage.value = err statusMessage.value = body })}可以看到,上面的代码给两个函数加了描述性功能名称,使代码更容易阅读,当发生异常时,你将获得引用实际函数名称而不是“匿名”的堆栈跟踪。现在我们可以将这些功能移到我们程序的顶层:document.querySelector(‘form’).onsubmit = formSubmit;function formSubmit (submitEvent) { var name = document.querySelector(‘input’).value; request({ uri: “http://example.com/upload", body: name, method: “POST” }, postResponse);} function postResponse (err, response, body) { var statusMessage = document.querySelector(’.status’); if (err) return statusMessage.value = err; statusMessage.value = body;}重新整改代码结构之后,可以清晰的看到这段函数的功能。2. 模块化从上面取出样板代码,并将其分成几个文件,将其转换为模块。这是一个名为formuploader.js的新文件,它包含了之前的两个函数:module.exports.submit = formSubmit;function formSubmit (submitEvent) { var name = document.querySelector(‘input’).value; request({ uri: “http://example.com/upload", body: name, method: “POST” }, postResponse)}function postResponse (err, response, body) { var statusMessage = document.querySelector(’.status’); if (err) return statusMessage.value = err; statusMessage.value = body;}把它们exports后,在应用程序中引入并使用,这就使得代码更加简洁易懂了:var formUploader = require(‘formuploader’);document.querySelector(‘form’).onsubmit = formUploader.submit;3. error first处理每一处错误,并且回调的第一个参数始终保留用于错误:var fs = require(‘fs’) fs.readFile(’/Does/not/exist’, handleFile); function handleFile (error, file) { if (error) return console.error(‘Uhoh, there was an error’, error); // otherwise, continue on and use file in your code; }有第一个参数是错误是一个简单的惯例,鼓励你记住处理你的错误。如果它是第二个参数,会更容易忽略错误。除了上述代码层面的解决方法,还可以使用以下更高级的方法,也是另外4种实现异步的方法。但是请记住,回调是JavaScript的基本组成部分(因为它们只是函数),在学习更先进的语言特性之前学习如何读写它们,因为它们都依赖于对回调。2. 发布订阅模式订阅者把自己想订阅的事件注册到调度中心,当该事件触发时候,发布者发布该事件到调度中心(顺带上下文),由调度中心统一调度订阅者注册到调度中心的处理代码。比如有个界面是实时显示天气,它就订阅天气事件(注册到调度中心,包括处理程序),当天气变化时(定时获取数据),就作为发布者发布天气信息到调度中心,调度中心就调度订阅者的天气处理程序。简单来说,发布订阅模式,有一个事件池,用来给你订阅(注册)事件,当你订阅的事件发生时就会通知你,然后你就可以去处理此事件。使用发布订阅模式,来修改Ajax:xhr.onreadystatechange = function () {//监听事件 if (this.readyState === 4) { if (this.status === 200) { switch (dataType) { case ‘json’: { Event.emit(‘data ‘+method,JSON.parse(this.responseText)); //触发事件 break; } case ’text’: { Event.emit(‘data ‘+method,this.responseText); break; } case ‘xml’: { Event.emit(‘data ‘+method,this.responseXML); break; } default: { break; } } } }}3. PromiseES6将Promise写进了语言标准,统一了用法,原生提供了Promise对象。Promise,简单说就是一个容器,里面保存着一个异步操作的结果。从语法上说,Promise是一个对象,从它可以获取异步操作的消息。Promise有3种状态:pending(进行中)、fulfilled(成功)、rejected(失败)。Promise很重要的两个特点:状态不受外界影响;只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。一旦状态改变,就不会再变,任何时候都可以得到这个结果;Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为resolved(已定型)。1. 基本用法const p = new Promise((resolve,reject) => { // resolve在异步操作成功时调用 resolve(‘success’); // reject在异步操作失败时调用 reject(’error’);});p.then(result => { console.log(result);});p.catch(result => { console.log(result);})ES6规定,Promise对象是一个构造函数,用来生成Promise实例。new一个Promise实例时,这个对象的起始状态就是Pending状态,再根据resolve或reject返回Fulfilled状态 / Rejected状态。2. Promise.prototype.then( )前面可以看到,Promise实例具有then方法,所以then方法是定义在原型对象Promise.prototype上的,它的作用是为Promise实例添加状态改变时的回调函数。then方法返回的是一个新的Promise实例,因此then可以采用链式写法:getJSON("/posts.json”).then(function(json) { return json.post;}).then(function(post) { // …});3. Promise.prototype.catch( )Promise.prototype.catch方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。getJSON(’/posts.json’).then(function(posts) { // …}).catch(function(error) { // 处理 getJSON 和 前一个回调函数运行时发生的错误 console.log(‘发生错误!’, error);});4. Promise.all( )Promise.all方法用于将多个Promise实例,包装成一个新的Promise实例。const p = Promise.all([p1, p2, p3]);上面代码中,p的状态由p1、p2、p3决定,分成两种情况:只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。5. Promise.race( )Promise.race方法同样是将多个Promise实例,包装成一个新的Promise实例。不同的是,race()接受的对象中,哪个对象返回快就返回哪个对象,如果指定时间内没有获得结果,就将Promise的状态变为reject。const p = Promise.race([ fetch(’/resource-that-may-take-a-while’), new Promise(function (resolve, reject) { setTimeout(() => reject(new Error(‘request timeout’)), 5000) })]);p.then(console.log).catch(console.error);上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。6. Promise.resolve( )Promise.resolve(‘foo’)// 等价于new Promise(resolve => resolve(‘foo’))7. Promise.reject( )const p = Promise.reject(‘出错了’);// 等同于const p = new Promise((resolve, reject) => reject(‘出错了’))p.then(null, function (s) { console.log(s)});// 出错了下面是一个用Promise对象实现的Ajax操作的例子:const getJSON = function(url) { const promise = new Promise(function(resolve, reject){ const handler = function() { if (this.readyState !== 4) { return; } if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } }; const client = new XMLHttpRequest(); client.open(“GET”, url); client.onreadystatechange = handler; client.responseType = “json”; client.setRequestHeader(“Accept”, “application/json”); client.send(); }); return promise;};getJSON("/posts.json”).then(function(json) { console.log(‘Contents: ’ + json);}, function(error) { console.error(‘出错了’, error);});8. callbackify & promisifyNode 8提供了两个工具函数util.promisify、util.callbackify用于在回调函数和Promise之间做方便的切换,我们也可以用JavaScript代码来实现一下。1. promisify:把callback转化为promisefunction promisify(fn_callback) { //接收一个有回调函数的函数,回调函数一般在最后一个参数 if(typeof fn_callback !== ‘function’) throw new Error(‘The argument must be of type Function.’); //返回一个函数 return function (…args) { //返回Promise对象 return new Promise((resolve, reject) => { try { if(args.length > fn_callback.length) reject(new Error(‘arguments too much.’)); fn_callback.call(this,…args,function (…args) { //nodejs的回调,第一个参数为err, Error对象 args[0] && args[0] instanceof Error && reject(args[0]); //除去undefined,null参数 args = args.filter(v => v !== undefined && v !== null); resolve(args); }.bind(this)); //保证this还是原来的this } catch (e) { reject(e) } }) }}2. callbackify:promise转换为callbackfunction callbackify(fn_promise) { if(typeof fn_promise !== ‘function’) throw new Error(‘The argument must be of type Function.’); return function (…args) { //返回一个函数 最后一个参数是回调 let callback = args.pop(); if(typeof callback !== ‘function’) throw new Error(‘The last argument must be of type Function.’); if(fn_promise() instanceof Promise){ fn_promise(args).then(data => { //回调执行 callback(null,data) }).catch(err => { //回调执行 callback(err,null) }) }else{ throw new Error(‘function must be return a Promise object’); } }}个人而言,最好直接把代码改成promise形式的,而不是对已有的callback加上这个中间层,因为其实改动的成本差不多。但总有各种各样的情况,比如,你的回调函数已经有很多地方使用了,牵一发而动全身,这时这个中间层还是比较有用的。4. generator(生成器)函数Generator函数是ES6提供的一种异步编程解决方案,通过yield标识位和next()方法调用,实现函数的分段执行。1. next( )方法先从下面的例子看一下Generator函数是怎么定义和运行的。function gen() { yield “hello”; yield “generator”; return;}gen(); // 没有输出结果var g = gen();console.log(g.next()); // { value: ‘hello’, done: false }console.log(g.next()); // { value: ‘generator’, done: false }console.log(g.next()); // { value: ‘undefined’, done: true }从上面可以看到,Generator函数定义时要带,在直接执行gen()时,没有像普通的函数一样,输出结果,而是通过调用next()方法得到了结果。这个例子中我们引入了yield关键字,分析下这个执行过程:创建了g对象,指向gen的句柄第一次调用next(),执行到yield hello,暂缓执行,并返回了hello第二次调用next(),继续上一次的执行,执行到yield generator,暂缓执行,并返回了generator第三次调用next(),直接执行return,并返回done:true,表明结束。经过上面的分析,yield实际就是暂缓执行的标示,每执行一次next(),相当于指针移动到下一个yield位置。next()方法返回的结果是个对象,对象里面的value是运行结果,done表示是否运行完成。2. throw( )方法throw()方法在函数体外抛出一个错误,然后在函数体内捕获。function *gen1() { try{ yield; } catch(e) { console.log(‘内部捕获’) }}let g1 = gen1();g1.next();g1.throw(new Error());3. return( )方法return()方法返回给定值,并终结生成器,在return后面的yield不会再被执行。function *gen2(){ yield 1; yield 2; yield 3;}let g2 = gen2();g2.next(); // { value:1, done:false }g2.return(); // { value:undefined, done:true }g2.next(); // { value:undefined, done:true }5. Promise + async & await在ES2017中,提供了async / await两个关键字来实现异步,是异步编程的最高境界,就是根本不用关心它是否是异步,很多人认为它是异步编程的终极解决方案。async / await寄生于Promise,本质上还是基于Generator函数,可以说是Generator函数的语法糖,async用于申明一个function是异步的,而await可以认为是async wait的简写,等待一个异步方法执行完成。async function demo() { let result = await Promise.resolve(123); console.log(result);}demo();async函数返回的是一个Promise对象,在上述例子中,表示demo是一个async函数,await只能用在async函数里面,表示等待Promise返回结果后,再继续执行,await后面应该跟着Promise对象(当然,跟着其他返回值也没关系,只是会立即执行,这样就没有意义了)。Promise虽然一方面解决了callback的回调地狱,但是相对的把回调 “纵向发展” 了,形成了一个回调链:function sleep(wait) { return new Promise((res,rej) => { setTimeout(() => { res(wait); },wait); });}/let p1 = sleep(100);let p2 = sleep(200);let p =/sleep(100).then(result => { return sleep(result + 100);}).then(result02 => { return sleep(result02 + 100);}).then(result03 => { console.log(result03);})将上述代码改成async/await写法:async function demo() { let result01 = await sleep(100); //上一个await执行之后才会执行下一句 let result02 = await sleep(result01 + 100); let result03 = await sleep(result02 + 100); // console.log(result03); return result03;}demo().then(result => { console.log(result);});因为async返回的也是promise对象,所以用then接收就行了。如果是reject状态,可以用try-catch捕捉:let p = new Promise((resolve,reject) => { setTimeout(() => { reject(’error’); },1000);});async function demo(params) { try { let result = await p; } catch(e) { console.log(e); }}demo();这是基本的错误处理,但是当内部出现一些错误时,和Promise有点类似,demo()函数不会报错,还是需要catch回调捕捉,这就是内部的错误被 “静默” 处理了。let p = new Promise((resolve,reject) => { setTimeout(() => { reject(’error’); },1000);});async function demo(params) { // try { let result = name; // } catch(e) { // console.log(e); // }}demo().catch((err) => { console.log(err);})最后,总结一下JavaScript实现异步的5种方式的优缺点:回调函数:写起来方便,但是过多的回调会产生回调地狱,代码横向扩展,不易于维护和理解。发布订阅模式:方便管理和修改事件,不同的事件对应不同的回调,但是容易产生一些命名冲突的问题,事件到处触发,可能代码可读性不好。Promise对象:通过then方法来替代掉回调,解决了回调产生的参数不容易确定的问题,但是相对的把回调 “纵向发展” 了,形成了一个回调链。Generator函数:确实很好的解决了JavaScript中异步的问题,但是得依赖执行器函数。async/await:这可能是javascript中,解决异步的最好的方式了,让异步代码写起来跟同步代码一样,可读性和维护性都上来了。 ...

April 9, 2019 · 4 min · jiezi

前端异步解决方案-1(callback,事件监听);

今天在学习koa2的时候发现自己对async的使用方法和原理都不是很熟悉,连带着发现自己对前端异步的解决方案并了解并不如何深刻,所以干脆把前端现有的异步解决方案都复习了一遍。今天先把将用callback和事件监听思想解决异步问题的代码贴出来,明天再补充代码的解析及其他解决方案;CallBackfunction f1(f2) { setTimeout(function () { let b = 10; f2(b) }, 1000)}function f2(data) { console.log(“callback_type_demo”, data)}f1(f2);事件监听//实现事件监听let DOM = function () { //被监听的事件的集合 this.eventList = {}; //绑定事件监听的方法 this.on = function (eventName, fun) { if (typeof eventName !== “string”) { console.error(“监听的事件名应为字符串”); return; } if (typeof fun !== “function”) { console.error(“被触发的必须是事件”); return; } this.eventList[eventName] = this.eventList[eventName] || []; this.eventList[eventName].push(fun); }; //移除事件监听的方法 this.removeOn = function (eventName, fun) { let onList = this.eventList[eventName]; if (onList) { for (let i = 0; i < onList.length; i++) { if (onList[i] === fun) { onList.splice(i, 1); return } } } }; //触发事件监听的方法 this.trigger = function (eventName, param) { let onList = this.eventList[eventName]; if (onList) { for (let i = 0; i < onList.length; i++) { onListi } } };};let dom1 = new DOM();let dom2 = new DOM();let f2 = function () { setTimeout(function () { dom1.trigger(‘done’, 20) }, 100)};let f3 = function (data) { console.log(data)};dom1.on(“done”, f3);f2();setTimeout(function () { console.log(“removeOn”); dom1.removeOn(“done”, f3); f2();}, 200);dom1.trigger(“done”, “123”);dom2.trigger(“done”, “123”);由于时间关系今天不多做解析,明天再来解析我的代码和运行结果 ...

April 4, 2019 · 1 min · jiezi

JavaScript的调用栈、回调队列和事件循环

译者按这篇文章可以看做是对Philip Roberts 2014年在JSConf演讲的《What the heck is the event loop anyway?》的一个总结。建议先看Philip Roberts的这个演讲然后再阅读本篇文章。这哥们儿的演讲语言幽默风趣,内容通俗易懂,非常值得一看。在这个视频中,Philip Roberts将JavaScript的调用栈、回调队列和事件循环的内容讲的很清晰。所以你可以随意的跳过这篇文章,花上一个半小时去看视频。当然如果你愿意读一下我的这篇文章那也不是不可以。什么是JavaScript什么是JavaScript呢?列举一些关键词就是:他是单线程的、非阻塞的、异步的并发语言他有一个调用栈,一个事件循环,一个回调队列,还有一些api和别的东西如果你像我一样(或者像Philip Roberts)对此懵逼的话,这些话本身并没不意味着什么。那我们就来剖析一下。JavaScript运行时JavaScript运行时(像V8引擎)拥有一个堆(内存分配用的)和栈(执行上下文)。但是他没有setTimeout、DOM等。这些是浏览器提供的Web APIs。我们了解的JavaScript浏览器中的JavaScript拥有:一个像V8引擎一样的运行时(提供堆栈)浏览器提供的Web APIs,例如:DOM、AJAX和setTimeout一个为各种事件回调准备的回调队列,例如:onClick、onLoad、onDone一个事件循环什么是调用栈JavaScript是单线程的,意味着他有一个单独的调用栈,意味着他一次能做一件事。调用栈基本上就是一个记录程序执行位置的数据结构。如果程序进入了一个函数,那就往这个栈里面塞些东西。如果程序从一个函数中return了,那就从栈顶弹出一些东西。当我们的程序报错的时候,我们会在控制台看到调用栈信息。报错的时候我们可以看到栈的状态(被调用的那个函数的)。阻塞这涉及到一个重要的问题:程序运行的很慢的时候发生了什么?换句话说,就是程序阻塞了。阻塞并没有严格的定义。实际上就是程序执行慢。执行console.log不慢,但是一个从1到1,000,000,000的while循环,图像处理或者网络请求这些操作的执行就比较费时了。这些执行慢的东西堆在一起就发生了阻塞。因为JavaScript是单线程的,我们发起一个网络请求就不得不一直等到他结束。这在浏览器中就是个问题–当我们等这个请求的时候,浏览器就发生了阻塞(我们不能做点击、提交表单等操作)。解决这个问题的方法就是使用异步回调。并发,看到这个词的时候我们会发现上面有一个地方说的不对JavaScript一次只能做一件事情的说法是不对的。正确的说法应该是:JavaScript的运行时一次只能做一件事。他不能一边发ajax请求一边运行别的代码,也不能在执行别的代码时候运行一个定时器。但是我们可以并发的做这些事。因为浏览器不仅仅是一个运行时(还记得上面那个渣渣画质的图吗?)。调用栈可以往Web APIs里面放东西,Web APIs可以在事件结束的时候把回调函数放进回调队列,然后是事件循环。最终我们进入事件循环,这是这个过程中最简单的部分,他有一个非常简单的工作:看看调用栈,瞅瞅回调队列,如果调用栈空闲了,就把回调队列中的第一个函数取出来丢进调用栈让他执行(这就回到了JavaScript的地盘,回到了V8的内部)。整个串起来Philip搞了一个的碉堡的工具来可视化这个过程,这玩意儿叫Loupe。这是一个能够把JavaScript运行时可视化的工具。我们用它来看一个简单的例子:在一个异步的setTimeout回调中用console.log在控制台打些log出来。整个过程到底都发生了什么呢?我们来看一下:执行进入console.log(‘Hi’);函数,因此这个函数被丢进了调用栈里。console.log(‘Hi’);函数return了,因此他就被弹出了栈顶。执行进入setTimeout函数,因此这个函数被丢进了调用栈里。setTimeout是Web APIs的一部分,因此Web APIs处理了他,并且等了2秒继续执行脚本,进入console.log(‘EvenyBody’)函数,把他也丢进调用栈。console.log(‘EvenyBody’)函数return了,所以把他从栈顶弹出去2秒的定时已经完成了,所以就把对应的回调函数放到回调队列里。事件循环检查调用栈是否为空,如果非空的话,他就等着。因为调用栈现在是空的,所以把回调队列中的回调函数丢进调用栈。console.log(‘There’)函数返回了,因此把他从栈顶弹出去(译者按:原文为console.log(‘Everybody’),应为书写错误)。有趣的一点是:setTimeout(function(…), 0)的情况。setTimeout为0的时候这个过程看起来可能不明显,除非考虑到调用栈的执行环境和事件循环的情况。基本上都会推迟到调用栈为空才执行。考虑UI渲染的性能的情况为了回到了我们日常处理的UI层,我们需要考虑渲染问题。浏览器受到我们在JavaScript中所做操作的影响,他可能每隔16.6ms重绘一次屏幕(60帧/秒)。但是调用栈还有代码在执行的话,他实际上是没法做重绘的。就像Philip说的一样:当大家说不要"阻塞事件循环"的时候,他们实际上是说:不要把耗费时间长的代码放进调用栈,因为你要这么搞的话,浏览器就不能做他该做的事了,比如说给你搞一个漂亮流畅的UI。Philip Roberts “What the Heck Is the Event Loop Anyway”举个例子,滚动的处理函数触发多了会让UI变得卡顿。顺便说一句,这是我听过的对防抖最清楚的解释了,这就是你要做到的“不要阻塞事件循环”(那就是我们只在滚动处理函数被触发x次后才执行那些耗时的操作)。结语总之,这就是《What the heck is the event loop anyway?》的答案。Philip的演讲很好的帮我理解了什么是JavaScript,什么不是,哪个部分是运行时,哪个部分是浏览器的和我们该怎样有效的使用事件循环。好好看看这个视频吧。

February 18, 2019 · 1 min · jiezi