浏览器运行机制
浏览器是多进程的,JS是单线程的。
一个进程可以包含多个线程,进程是CPU资源分配的单位,线程是CPU调度的单位。
浏览器的主要进程有4个
- Browser进程:浏览器的主进程(负责协调、主控),只有一个。
负责浏览器界面显示,与用户交互。如前进,后退
负责各个页面的管理,创建和销毁其他进程
将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
网络资源的管理,下载等
- 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
- GPU进程:最多一个,用于3D绘制等
- Renderer进程(浏览器渲染进程/浏览器内核):默认每个Tab页面一个。内部是多线程的。
页面渲染,脚本执行,事件处理等
浏览器渲染进程主要包括五个线程
- GUI渲染线程(页面渲染):解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制。需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- JS引擎线程(脚本执行):JS引擎线程负责解析Javascript脚本,运行代码。一直等待着任务队列中任务的到来,然后加以处理。
- 事件触发器线程:JS引擎执行代码块如setTimeOut时/鼠标点击/AJAX异步请求等,会将对应任务添加到事件线程中,当对应的事件符合触发条件被触发时,该线程会把事件添加到事件队列的末尾,等待JS引擎的处理
比如
setTimeout(func1,1000)就是一个定时器事件,将func1这个任务放到定时器事件的线程中,1s后,满足定时器事件被触发的条件了,就把定时器事件挂在事件队列的末尾
- 定时触发器线程:
setInterval与setTimeout所在线程,浏览器定时计数器并不是由JavaScript引擎计数的。W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms - 浏览器HTTP请求线程:在XMLHttpRequest在连接后是通过浏览器新开一个线程请求。将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行
其中,GUI渲染线程与JS引擎线程是互斥的
异步
什么是异步
异步可以理解为非顺序执行。 即代码的书写顺序并不代表代码的执行顺序。
如何实现异步:多线程+轮询
“异步”隐藏着两重含义:
1.任务被分割,不是被连续执行
2.一个时间点下不只一个任务正在被执行
两者并不矛盾,只是角度不同:
从单个任务角度来看,异步模式对它的影响就是:它不能连续地,被一个“人”完成,可能是A先做了开头,然后换成B去做,最后又是A。
从宏观的角度来看,异步模式就像是有好几个人,同时在做着自己手头上的事情,这样几个任务可以被同时执行。
这里的人代表的就是线程。即要做到异步,必须要多个线程配合。
而线程之间的配合必然涉及到线程间的通信,使用到的就是轮询。
异步的提出
异步模式会出现的原因是: JavaScript是一门单线程的语言,也就是说JavaScript引擎一个时间点下只能执行一个任务。 要让JavaScript引擎发挥最大的作用,就必须让它只做最重要的事情,把零碎的工作转给其他人。
异步的实现需要多个线程,而JavaScript引擎只能提供一个线程。
比如后文中出现的fs.readFile(读取文件)就是一个异步方法,在JavaScript引擎线程知道需要读取某个文件之后,它就把读取字节流这个繁琐且耗时多的任务给了别人(NodeJS线程池),自己去做其他事情,等读取完毕,JavaScript引擎线程再去执行fs.readFile中的回调方法。
如果让JavaScript引擎完成整个任务,在计算机使用者的角度,体验可能就是:文件打开化的时间好长,我不想等了,点右上角的×,页面又关不掉——卡死。因为关闭页面这个操作需要JavaScript引擎来处理,而它此时正在读取字节流,没有功夫来关闭页面。
异步模式提高执行效率,但是对于编程带来了困难。 JavaScript引擎只执行异步任务的一头一尾,那么尾部的代码写在什么地方便成了问题。不能把尾部任务直接并列写在头部任务后面,因为尾部任务需要中间任务的结果才能执行,我们不知道什么时候中间部分才会被执行完。
下面会分析JS中异步编程发展的模式,从最开始的回调函数,到发布订阅模式——观察者模式——ES6提出的Promise。
所有的这些方法提出都是为了把被分割的任务更优雅的书写,不用再像回调函数时代,把后一个函数夹在前一个函数内部。
被称为异步发展的最终级版本是ES7提出的async/await。但限于笔者水平,暂时不在本篇范围内。之后学习完会再补充。
回调函数
回调函数解决这个异步编程的第一个方案。
把尾部任务函数当成一个参数传入头部任务函数内,告诉JavaScript引擎,当中间部分任务有了结果再来执行这些代码。
这个方案只能处理比较简单的情景,如果任务二的执行依赖于任务一的结果,任务三的执行依赖于任务二的结果.......那么必须把任务二函数所有代码写进任务一内,当一个参数传给任务一,任务三函数所有代码写进任务二,当一个参数传给任务二......这样任务一这个函数就会非常庞大,因为里面还有任务二的逻辑,任务二里面还有任务三的逻辑。这种情况也被称为“回调地狱”。回调地狱不仅观感不佳,更麻烦的是找/修bug非常困难。
此外,还有两个问题:不能return,不能捕获错误。
let fs = require('fs');
function read(filename) {
fs.readFile(filename,'utf8',function(err,data){
// throw Error('出错了')
// 读取操作的结果只能写在回调函数内部
if(err){
console.log(err);
}else{
// console.log(data);
}
})
}
// try{
// read('./file.txt');
// }catch(e){
// console.log('err!',e);
// }
// 回调函数没有被执行,x是undefine
setTimeout(()=>{
let x = read('./file.txt');
console.log(x);
},1000)
发布订阅模式
发布订阅模式基于构造函数/类Event。
概念分析
事件(Event/Event Emitter):
可以做出回应的动作。点右上角的×,网页就关闭,这个点击,就是一个事件。
Event和Event Emitter:
Event是C++中的核心概念,依赖于libuv库,它负责怎么响应底层事件。比如打开文件事件,获取数据事件。
Event Emitter是JS中的核心概念,它可以是一个更高级的事件——一个Event被触发使得一个EventEmitter被触发;我们也可以自定想要的事件,做出适当的回应。Event Emitter是JavaScript中很重要的部分,很多模块都是继承于Event Emitter(原型是Event Emitter)。
#### 事件监听(Event Listener): 做出动作发生后的事情。还是点击×关闭网页,关闭网页就是事件监听。
Event Listener
一个事件可以有多个监听,事件发生后,这些监听会一个个发生。
自己实现了一个简单版的Events构造函数,学习发布订阅模式的原理。
function Events(){ this.events = {}; }让每一个Events实例都有一个私有属性events(保存事件监听)。
Events的prototype上有两个方法on对应订阅,emit对应发布
on(订阅)接收一个函数(和事件类别)作为参数,把这个函数放按类别到实例的events中
emit(发布)接收一个变量(和事件类别)作为参数,将这个变量作为events数组中每一个函数的参数,依次调用
// 注意:实例化后,this指向实例对象
function Event() {
this.events = {};
}
Event.prototype.on = function(type, listener) {
this.events[type] = this.events[type] || [];
this.events[type].push(listener)
}
Event.prototype.emit = function(type, r) {
if(this.events[type]) {
this.events[type].forEach(function(item){
item(r);
})
}
}
//实例化
var myEmt = new Event();
myEmt.on('openTheLid',function(){
console.log('I drink it up');
})
myEmt.on('openTheLid',function() {
console.log('I throw it to the bin');
})
myEmt.on('realizeTheLastDayOfVocation', function() {
console.log('I have to pack my lugguage!');
})
myEmt.on('realizeTheLastDayOfVocation', function() {
console.log('I go to the airport');
})
console.log("today is 2.15");
myEmt.emit('realizeTheLastDayOfVocation');//I have to pack my lugguage!+I go to the airport
我在实例对象myEmt上绑定了两个事件。
一个是“打开瓶盖”openTheLid事件,有“喝光它”'I drink it up'“丢掉瓶子”'I throw it to the bin'两个监听
一个是“假期结束”realizeTheLastDayOfVocation事件,有“整理行李”'I have to pack my lugguage!'“赶去机场”'I go to the airport'两个监听
这便实现了对这两个事件的发布订阅模式处理:
首先将这个任务的尾部逻辑写在“时间监听”部分内。触发事件再用简单的一句e.emit()'发布,之前订阅的逻辑便会被执行。Event对象通过内部逻辑使得on和emit相互联系,使用时可以将业务代码拆分书写。
现在myEmt对象是这样的:
myEmt = {
events:{
openTheLid:[fun(){},fun(){}],
realizeTheLastDayOfVocation:[fun(){},fun(){}],
}
}
优化
因为在引号中的字符串不会被检查,所以最好再建一个文件
//event.js
module.exports = {
events: {
OPEN: 'openTheLid',
LASTDAY: 'realizeTheLastDayOfVocation'
}
}
//eventdemo.js
var eventConfig = require('./event').events;
function Event() {
this.events = {};
}
Event.prototype.on = function(type, listener) {
this.events[type] = this.events[type] || [];
this.events[type].push(listener)
}
Event.prototype.emit = function(type) {
if(this.events[type]) {
this.events[type].forEach(function(item){
item();
})
}
}
//关键字会有自动补齐
var myEmt = new Event();
myEmt.on(eventConfig.OPEN,function(){
console.log('I drink it up');
})
myEmt.on(eventConfig.OPEN,function() {
console.log('I throw it to the bin');
})
myEmt.on(eventConfig.LASTDAY, function() {
console.log('I have to pack my lugguage!');
})
myEmt.on(eventConfig.LASTDAY, function() {
console.log('I go to the airport');
})
console.log("today is 2.15");
myEmt.emit(eventConfig.LASTDAY);
观察者模式
观察者模式基于Observer类。
每一个Observer实例都有一个私有属性state记录对象的状态,arr记录它的观察者。
Observer原型上有两个方法:attach和setState。
attach方法,用以将观察者存入arr数组
setState方法用以更新状态,并且使用先前存下的arr数组,通知给观察者
function Observer() {
this.state = 'UNHAPPY';
this.arr = [];
}
//将被观察者跟观察者联系起来
Observer.prototype.attach = function(s) {
this.arr.push(s);
}
Observer.prototype.setState = function(newState) {
this.state = newState;
this.arr.forEach((s)=>{
// 被观察者通知观察者变化情况
s.update(this.state);
})
}
function Subject(name,target) {
this.name = name;
this.target = target;
}
Subject.prototype.update = function(newState) {
console.log(this.name+'观察到了被观察者的'+newState+'变化');
}
let o = new Observer();
let s1 = new Subject('Brynn', o);
let s2 = new Subject('Boolean', o);
o.attach(s1);
o.attach(s2);
o.setState('HAPPY');
o.setState('INDIFFERENT');
//运行结果
Brynn观察到了被观察者的HAPPY变化
Boolean观察到了被观察者的HAPPY变化
Brynn观察到了被观察者的INDIFFERENT变化
Boolean观察到了被观察者的INDIFFERENT变化
Promise
Promise的实现基于Promise类,自己实现了一个简单的Promise,学习一下它的原理。
Promise实例都有以下私有属性:
status标识当前对象的状态,有pending,resolved,rejected三个值,只可以从pending态转化为其他两个状态;
value记录成功值,reason记录失败状态;
onResolvedCallbacks是成功状态的回调,onRejectCallbacks是失败状态的回调;
函数resolve将当前状态设为成功态,并且调用onResolvedCallbacks里面的方法;函数reject将当前状态设为失败态,并且调用onRejectCallbacks里面的方法
函数Promise接收一个函数作为参数,这个函数会被立刻执行,是同步的,并且在这个函数里面可以调用resolve和reject函数改变当前Promise状态
Promise原型上有then方法和catch方法:
then方法接受两个函数作为参数,一个是成功的回调,一个是失败的回调,如果当时是成功/失败,就立刻执行,否则放到成功/失败回调函数队列里
catch方法接受一个函数作为参数,作为失败的回调,本质是一个简化版的then
同一个实例可以多次调用then
成功失败分别处理
//Promise.js
function Promise(executor) {
let self = this;
self.status = 'pending';
self.value = undefined;
self.resson = undefined;
self.onResolvedCallbacks = [];
self.onRejectCallbacks = [];
function resolve(value){
if(self.status === 'pending'){
self.value = value;
self.status = 'resolved';
// 发布
self.onResolvedCallbacks.forEach(fn=>fn());
}
}
function reject(reason){
if(self.status === 'pending'){
self.reason = reason;
self.status = 'rejected';
// 发布
self.onRejectCallbacks.forEach(fn=>fn());
}
}
executor(resolve, reject);
}
Promise.prototype.then = function(onFulfilled, onRejected){
let self = this;
if(self.status === 'resolved'){
onFulfilled(self.value);
}
if(self.status === 'rejected'){
onRejected(self.reason);
}
if(self.status === 'pending') {
// 订阅
self.onResolvedCallbacks.push(function(){
onFulfilled(self.value)
});
self.onRejectCallbacks.push(function(){
onRejected(self.value)
});
}
}
module.exports = Promise;
Promise的处理方式很类似于发布订阅模式,但它可以对异步任务的成功和失败有不同的处理方式。
then的链式调用
// promise0320.js
// resolve、reject是库函数写好,用户调用
function Promise (executor){
let self = this;
self.value = undefined;
self.reason = undefined
self.status = 'pending';
self.onResolevedCallbacks = [];
self.onRejectedCallbacks = []; // 存放所有失败的回调
// 将promise设置为成功态
function resolve(value){
if(self.status === 'pending'){
self.value = value;
self.status = 'resolved'; // 成功态
self.onResolevedCallbacks.forEach(fn=>fn());
}
}
//将promise设置为失败态
function reject(reason){
if(self.status === 'pending'){
self.reason = reason;
self.status = 'rejected'; // 失败态
// 发布
self.onRejectedCallbacks.forEach(fn =>fn());
}
}
//为了处理在newPromise阶段就抛出错误的情况
try{
executor(resolve,reject);
}catch(e){
reject(e);
}
}
//onFulfilled、onRejected是用户写好,库函数的逻辑决定调哪个
Promise.prototype.then = function(onFulfilled,onRejected){
// promise then值的穿透,如果没有传onFulfilled或者onRejected,会有一个默认的函数
onFulfilled = typeof onFulfilled === 'function'?onFulfilled:value=>value;
onRejected = typeof onFulfilled === 'function'?onRejected:function(err){
throw err;
}
let self = this;
// 每次调用then都返回一个新的promise,实现promise的链式调用
let promise2 = new Promise(function(resolve,reject){
if(self.status == 'resolved'){
// 为了能取到promise2,因为promise2是new出来的,要new内部逻辑走完才存在promise2
setTimeout(()=>{
let x = onFulfilled(self.value);
//把then返回值包装成promise2
try{
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
}
if(self.status == 'rejected'){
setTimeout(()=>{
let x = onRejected(self.value);
try{
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
}
if(self.status == 'pending') {
self.onResolevedCallbacks.push(function(){
setTimeout(()=>{
let x = onFulfilled(self.value);
try{
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
},0)
})
self.onRejectedCallbacks.push(function(){
setTimeout(()=>{
let x = onRejected(self.reason);
try{
resolvePromise(promise2,x,resolve,reject)
}catch(e){
reject(e)
}
})
})
}
})
return promise2;
}
Promise.prototype.catch = function(errFn){
return this.then(null,errFn)
}
function resolvePromise(promise2,x,resolve,reject){ // 判断x 是不是promise
if(promise2 === x){ // 表示防止自己等待自己
reject(new TypeError('循环引用了'));
}
// 防止then返回的promise被多次调用,这是逻辑是防止别人的promise不规范
let called;
// then的返回的是一个对象
if((typeof x === 'object' && x !== null) || typeof x == 'function'){
try{
let then = x.then;
// then的返回的是一个promise对象,要拿到这个promise对象的结果
if(typeof then === 'function'){
then.call(x,(y)=>{
//这个判断逻辑是写给别人的promise,方式promise的状态被多次改变(既调用resolve又调用reject)
if(called) return;
called = true;
//为了处理resolve的内容又是一个new的promise的情况,递归调用到y一个普通值位置
resolvePromise(promise2,y,resolve,reject)
// resolve(y)//通常情况
},(r)=>{
if(called) return;
called = true;
reject(r)
})
// then的返回值是一个对象,把这个普通对象当初promise2的成功值
}else{
resolve(x)//
}
}catch(e){
if(called) return;
called = true;
reject(e)
}
// then的返回值是一个普通值,把这个普通值当初promise2的成功值
}else{
resolve(x);
}
}
// 可以直接生成一个成功态的promise
Promise.resolve = function(){
return new Promise((resolve,reject)=>{
resolve();
})
}
// 可以直接生成一个失败态的promise
Promise.reject = function(){
return new Promise((resolve,reject)=>{
reject();
})
}
module.exports = Promise;
调用
// promise0320app.js
let Promise = require('./promise0320');
let p1 = new Promise(function(resolve,reject){
resolve('promise1 success')
// throw new Error()
});
// console.log(p1);
let p2 = p1.then((data)=>{
console.log('promise1的成功回调')
console.log(data);
//返回一个promise
return new Promise((resolve,reject)=>{
setTimeout(()=>{
// reject('newPromise2 fail')
resolve(new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('finalPromise2 success')
},1000)
}))
},1000)
})
// 返回普通值,p2是成功态的
// return 'promise2 success'
// 抛出错误,p2是失败态的
// throw new Error()
// 循环引用,抛出异常,这个异常要在p2.then中才能捕捉,因为then的执行是异步的,立刻打印是pending
// return p2;
},(err)=>{
console.log('promise1的失败回调')
console.log(err)
throw 'promise2 fail'
});
let p3 = p2.then((data)=>{
console.log('promise2 的成功回调')
console.log(data)
return 'promise3 success'
},(err)=>{
console.log('promise2 的失败回调')
console.log(err)
return 'promise3 success'
})
let p = new Promise((resolve,reject)=>{
resolve('promise的穿透')
})
p.then().then((data)=>{
console.log(data)
})
到现在为止,promise可以实现链式调用,then的穿透。
链式调用的规则如下:
调用then后返回一个新的Promise,有确定的成功/失败态
捕错机制:默认找离自己最近的then的失败回调/catch
捕获到错误之后的then一般是走成功回调(因为每次then返回一个全新的promise,有自己的成功态和失败态return变成成功,抛错变成失败,跟上一个没有关系)
Promise后面的then走成功还是失败取决于Promise内部调用的是resolve还是reject,或者抛出错误
then后面的then走成功还是失败取决于then内部的return/throw
then内部return一个普通值,后面的then走成功的回调,并且将返回值作为value(在失败的then里return后面也是走成功的回调)
then内部return一个Promise,根据Promise结果决定走成功还是失败回调
then内部没有return,后面的then走成功回调的回调,但value是undefined(在失败的then里return后面也是走成功的回调)
延迟对象
使用延迟对象defer可以减少代码的嵌套,在promise外部使用resolve和reject。
let fs = require('fs');
Promise.defer = function(){
let dfd = {};
dfd.promise = new Promise((resolve,reject)=>{
dfd.resolve = resolve;
dfd.reject = reject;
})
return dfd;
}
// 没有defer延迟对象的写法
// function read(url){
// return new Promise((resolve,reject)=>{
// fs.readFile(url,'utf8',(err,data)=>{
// if(err) reject(err);
// resolve(data);
// })
// })
// }
function read(url){
let defer = Promise.defer();
// 可以在promise函数外部使用reject和resolve
fs.readFile(url,'utf8',(err,data)=>{
if(err) defer.reject(err);
defer.resolve(data);
})
return defer.promise;
}
read('./name.txt').then(data=>{
console.log(data);
})