JavaScript:浏览器进程/线程 与 JS异步发展(回调-发布订阅-观察者-promise)

298 阅读15分钟

浏览器运行机制

浏览器是多进程的,JS是单线程的。

一个进程可以包含多个线程,进程是CPU资源分配的单位,线程是CPU调度的单位。

浏览器的主要进程有4个

  1. Browser进程:浏览器的主进程(负责协调、主控),只有一个。

负责浏览器界面显示,与用户交互。如前进,后退
负责各个页面的管理,创建和销毁其他进程
将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
网络资源的管理,下载等

  1. 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
  2. GPU进程:最多一个,用于3D绘制等
  3. Renderer进程(浏览器渲染进程/浏览器内核):默认每个Tab页面一个。内部是多线程的。

页面渲染,脚本执行,事件处理等

浏览器渲染进程主要包括五个线程

  1. GUI渲染线程(页面渲染):解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制。需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
  2. JS引擎线程(脚本执行):JS引擎线程负责解析Javascript脚本,运行代码。一直等待着任务队列中任务的到来,然后加以处理。
  3. 事件触发器线程:JS引擎执行代码块如setTimeOut时/鼠标点击/AJAX异步请求等,会将对应任务添加到事件线程中,当对应的事件符合触发条件被触发时,该线程会把事件添加到事件队列的末尾,等待JS引擎的处理

比如setTimeout(func1,1000)就是一个定时器事件,将func1这个任务放到定时器事件的线程中,1s后,满足定时器事件被触发的条件了,就把定时器事件挂在事件队列的末尾

  1. 定时触发器线程:setIntervalsetTimeout所在线程,浏览器定时计数器并不是由JavaScript引擎计数的。W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms
  2. 浏览器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原型上有两个方法:attachsetState
  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);
})