事件循环
从promise、process.nextTick、setTimeout出发,谈谈Event Loop中的Job queue
简要介绍:谈谈promise.resove,setTimeout,setImmediate,process.nextTick在EvenLoop队列中的执行顺序
1.问题的引出 event loop都不陌生,是指主线程从“任务队列”中循环读取任务,比如
例1:
setTimeout(function(){console.log(1)},0); console.log(2) //输出2,1
在上述的例子中,我们明白首先执行主线程中的同步任务,当主线程任务执行完毕后,再从event loop中读取任务,因此先输出2,再输出1。
event loop读取任务的先后顺序,取决于任务队列(Job queue)中对于不同任务读取规则的限定。比如下面一个例子:
例2:
setTimeout(function () { console.log(3); }, 0); Promise.resolve().then(function () { console.log(2); }); console.log(1); //输出为 1 2 3
先输出1,没有问题,因为是同步任务在主线程中优先执行,这里的问题是setTimeout和Promise.then任务的执行优先级是如何定义的。
2 . Job queue中的执行顺序 在Job queue中的队列分为两种类型:macro-task和microTask。我们举例来看执行顺序的规定,我们设
macro-task队列包含任务: a1, a2 , a3 micro-task队列包含任务: b1, b2 , b3
执行顺序为,首先执行marco-task队列开头的任务,也就是 a1 任务,执行完毕后,在执行micro-task队列里的所有任务,也就是依次执行b1, b2 , b3,执行完后清空micro-task中的任务,接着执行marco-task中的第二个任务,依次循环。
了解完了macro-task和micro-task两种队列的执行顺序之后,我们接着来看,真实场景下这两种类型的队列里真正包含的任务(我们以node V8引擎为例),在node V8中,这两种类型的真实任务顺序如下所示:
macro-task(宏任务)队列真实包含任务: script(主程序代码),setTimeout, setInterval, setImmediate, I/O, UI rendering
micro-task(微任务)队列真实包含任务: process.nextTick, Promises, Object.observe, MutationObserver
由此我们得到的执行顺序应该为:
script(主程序代码)—>process.nextTick—>Promises…——>setTimeout——>setInterval——>setImmediate——> I/O——>UI rendering
在ES6中macro-task队列又称为ScriptJobs,而micro-task又称PromiseJobs
3 . 真实环境中执行顺序的举例
(1) setTimeout和promise
例3:
setTimeout(function () { console.log(3); }, 0); Promise.resolve().then(function () { console.log(2); }); console.log(1);
我们先以第1小节的例子为例,这里遵循的顺序为:
script(主程序代码)——>promise——>setTimeout 对应的输出依次为:1 ——>2————>3 (2) process.nextTick和promise、setTimeout
例子4:
setTimeout(function(){console.log(1)},0); new Promise(function(resolve,reject){ console.log(2); resolve(); }).then(function(){console.log(3) }).then(function(){console.log(4)}); process.nextTick(function(){console.log(5)}); console.log(6); //输出2,6,5,3,4,1
这个例子就比较复杂了,这里要注意的一点在定义promise的时候,promise构造部分是同步执行的,这样问题就迎刃而解了。
首先分析Job queue的执行顺序:
script(主程序代码)——>process.nextTick——>promise——>setTimeout
I) 主体部分: 定义promise的构造部分是同步的, 因此先输出2 ,主体部分再输出6(同步情况下,就是严格按照定义的先后顺序)
II)process.nextTick: 输出5
III)promise: 这里的promise部分,严格的说其实是promise.then部分,输出的是3,4
IV) setTimeout : 最后输出1
综合的执行顺序就是: 2——>6——>5——>3——>4——>1
(3)更复杂的例子
setTimeout(function(){console.log(1)},0); new Promise(function(resolve,reject){ console.log(2); setTimeout(function(){resolve()},0) }).then(function(){console.log(3) }).then(function(){console.log(4)}); process.nextTick(function(){console.log(5)}); console.log(6); //输出的是 2 6 5 1 3 4
这种情况跟我们(2)中的例子,区别在于promise的构造中,没有同步的resolve,因此promise.then在当前的执行队列中是不存在的,只有promise从pending转移到resolve,才会有then方法,而这个resolve是在一个setTimout时间中完成的,因此3,4最后输出。
typeof和instanceof
ECMAScript是松散类型的,一次需要一种手段来检测给定变量的数据类型,typeof操作符(注意不是函数哈!)就是负责提供这方面信息的
typeof 可以用于检测基本数据类型和引用数据类型。
语法格式如下:
typeof variable
返回6种String类型的结果:
- "undefined" - 如果这个值未定义
- "boolean" - 如果这个值是布尔值
- "string" - 如果这个值是字符串
- "number" - 如果这个值是数值
- "object" - 如果这个值是对象或null
- "function" - 如果这个值是函数 示例:
console.log(typeof 'hello'); // "string" console.log(typeof null); // "object" console.log(typeof (new Object())); // "object" console.log(typeof(function(){})); // "function"
typeof主要用于检测基本数据类型:数值、字符串、布尔值、undefined, 因为typeof用于检测引用类型值时,对于任何Object对象实例(包括null),typeof都返回"object"值,没办法区分是那种对象,对实际编码用处不大。
instanceof 用于判断一个变量是否某个对象的实例
在检测基本数据类型时typeof是非常得力的助手,但在检测引用类型的值时,这个操作符的用处不大,通常,我们并不是想知道某个值是对象,而是想知道它是什么类型的对象。此时我们可以使用ECMAScript提供的instanceof操作符。
语法格式如下:
1. result = variable instanceof constructor
返回布尔类型值:
- true - 如果变量(variable)是给定引用类型的实例,那么instanceof操作符会返回true
- false - 如果变量(variable)不是给定引用类型的实例,那么instanceof操作符会返回false 示例:
function Person(){} function Animal(){} var person1 = new Person(); var animal1 = new Animal(); console.log(person1 instanceof Person); // true console.log(animal1 instanceof Person); // false console.log(animal1 instanceof Object); // true console.log(1 instanceof Person); //false var oStr = new String("hello world"); console.log(typeof(oStr)); // object console.log(oStr instanceof String); console.log(oStr instanceof Object); // 判断 foo 是否是 Foo 类的实例 function Foo(){} var foo = new Foo(); console.log(foo instanceof Foo); // instanceof 在继承中关系中的用法 console.log('instanceof 在继承中关系中的用法'); function Aoo(){} function Foo(){} Foo.prototype = new Aoo(); var fo = new Foo(); console.log(fo instanceof Foo); console.log(fo instanceof Aoo)
根据规定,所有引用类型的值都是Object的实例。因此,在检测一个引用类型值和Object构造函数时,instanceof操作符会始终返回true。如果使用instanceof 操作符检测基本类型值时,该操作符会始终返回false,因为基本类型不是对象。
console.log(Object.prototype.toString.call(null)); // [object Null] undefined console.log(Object.prototype.toString.call([1,2,3])); //[object Array] undefined console.log(Object.prototype.toString.call({})); // [object Object]
常见的继承的几种方法
原型链继承
定义 利用原型让一个引用类型继承另外一个引用类型的属性和方法 代码
function SuperType(){ this.property = 'true'; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subProperty = 'false'; } SubType.prototype = new SuperType(); SubType.prototype.getSubValue = function(){ return this.subProperty; } var instance = new SubType(); alert(instance.getSuperValue());//true
- 优点 简单明了,容易实现,在父类新增原型属性和方法,子类都能访问到。
- 缺点 包含引用类型值的函数,所有的实例都指向同一个引用地址,修改一个,其他都会改变。不能像超类型的构造函数传递参数
构造函数继承 定义 在子类型构造函数的内部调用超类型的构造函数
代码
function SuperType(){ this.colors = ['red','yellow']; } function SubType(){ SuperType.call(this); } var instance1 = new SubType(); instance1.colors.push('black'); var instance2 = new SubType(); instance2.colors.push('white'); alert(instance1.colors);//'red','yellow','black' alert(instance2.colors);//'red','yellow','white'
- 优点 简单明了,直接继承了超类型构造函数的属性和方法
- 缺点 方法都在构造函数中定义,因此函数复用就无从谈起了,而且超类型中的原型的属性和方法,对子类型也是不可见的,结果所有的类型只能使用构造函数模式。
组合继承
定义 使用原型链实现多原型属性和方法的继承,使用构造函数实现实例的继承
代码
function SuperType(name){ this.name = name; this.colors = ['red','black']; } SuperType.prototype.sayName = function() { alert(this.name); } function SubType(name,age){ SuperType.call(this,name); this.age = age; } SubType.protptype = new SuperType(); SubType.protptype.sayAge = function(){ alert(this.age); }
- 优点 解决了构造函数和原型继承中的两个问题
- 缺点 无论什么时候,都会调用两次超类型的构造函数