从【js事件执行机制】开始顺藤摸瓜...

前言:

聊到js,不能忽略的是js的单线程特性;而java历史悠久的语言确实多线程的,为什么js跟Java不一样呢?我的理解是java主要是作为一个服务端来共享数据以及处理来自多个用户的指令;当指令变多变密,如果不使用多线程,那么对CPU资源的占用以及用户等待指令回复的时间就造成巨大的不方便。多线程同步完成多项任务,提高资源使用效率来提高系统的效率,这样才更合理;而反过来,js是浏览器的脚本语言,每个用户端显示的页面信息可能会因为操作不同在同时展示的不一样,主要需要保证某个用户页面的dom元素的相关操作不要冲突即可;想一下,如果一个线程要修改按钮的内部字体,而另外一个要删除这个dom;两者如何执行呢?
上学的时候,我总是分不清“进程”、“线程”两个词,在这里一起记录一下:
进程:cpu分配资源最小单位,是能拥有资源和独立运行的最小单位;
线程:cpu最小调度单位,线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程
进程就像一个公司,公司相互独立,每个公司有多个员工可以使用,每个员工单独或一起完成任务,共享公司的空间
浏览器是多进程的:每一个tab页就是一个进程,每个进程有UI渲染线程、js引擎线程,http请求线程等;
上述参考链接:https://juejin.im/post/5eccc77c6fb9a047ab2c13c4#heading-0

一、js事件执行机制Event Loop

(面试的时候总会被问到事件循环机制,在这里总结一下自己的理解)

js的单线程决定了必须要把事件安排好执行顺序才能保证正常工作;其设计者估计经过多次思考实践设计吧,将任务分为同步和异步任务;
同步任务,就是按照执行顺序在主线程排队,一步OK下一步排上;
异步任务,不在同步那里排队,到一个任务队列里,等主线程上的同步执行完毕,将任务队列里的第一个提到主线程执行。
而事件循环就是为了将异步任务排好,毕竟有时候一个事件内部会调用另一个异步任务;
异步任务又分了两种:microtask微任务¯otask宏任务;
结合以上,列一个js执行顺序:
1. 首先,执行同步&立即执行任务;(这里需要标注一个new Promise()是立即执行的,这个是从阮大神的es6那里看来的,至于为什么,还不太能解释,可能就像什么是microtask和macrotask一样天生的吧!)
2. 其次,执行微任务,如:thenable函数、catchable函数、MutationObserver(html5新特性,会在指定的DOM发生变化时被调用)、Object.observe、process.nextTick(nodejs)
3. 最后,执行宏任务 ,如:定时相关的setTimeOut等、事件回调、http回调,UI交互等

了解了上述的既可以开始解从网上百度到的面试题了(嘿嘿嘿,来体验逐渐融会贯通,继而醍醐灌顶的感觉吧)

1. 题目1:
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');
})
setTimeout(function() {
    console.log('8');
    new Promise(function(resolve) {
        console.log('9');
        resolve();
    }).then(function() {
        console.log('10');
    })
})
new Promise(function(resolve) {
    console.log('11');
    resolve();
}).then(function() {
    console.log('12');
})
console.log('13');
1. 从上至下第一遍,console和new Promise都是立即执行的,所以先打印:1、5、11、13;
2. 有微任务thenable函数和宏任务定时器,先微后宏;thenable:6、12;setTimeOut: 2、3、7、8、9;
3. 此时任务队列里只剩下微任务thenable函数,依次执行:4、10;
2. 题目2:
async  function  async1 ()  {
    console.log('async1 start');
    await async2();
    console.log('async1 end')
}
async  function  async2 ()  {
    console.log('async2')
    let p1 = Promise.resolve('promise then');
    p1.then(function(value) {
        console.log(value);
    });
    console.log('async2 after promise')
}
async  function  async3 ()  {
    console.log('async3 start');
    await async4();
    console.log('async3 end')
}
async  function  async4 ()  {
    console.log('async4')
}
console.log('script start');
setTimeout(function ()  {
    console.log('setTimeout')
},  0);
async1();
async3();

new  Promise(function (resolve)  {
    console.log('promise1');
    resolve()
}).then(function ()  {
    console.log('promise2')
});

console.log('script end')
此处,使用了async这个工具,async在调用处开始执行,且特点是将同步进行同步操作、异步执行异步操作,简单的说会直接先调用一下该函数,函数内部按正常顺序执行,会对正常顺序后面的进行一次await执行等待
1. 从上至下第一遍,console和new Promise都是立即执行的,结合async的特性,所以先打印:script start、async1 start、async2、async2 after promise(因为这个promise是一个转换的promise对象,立即执行但是无打印)、async3 start、 async4、promise1、script end;
2. 此时有微任务thenable函数&宏任务定时器,先微后宏;thenable:promise then、async1 end、async3 end、promise2;setTimeOut: setTimeout

另外,外物知原理。为什么设计的时候要如此设计微任务和宏任务?

百度大家都是猜的;咱也来猜猜:个人觉得,正常的执行流程是从上至下一次走;而有些是立即执行的效果,可以马上得到结果;但是有的如定时器、回调函数是需要时间等待的,很难预料什么时候会OK,这样就需要分开两组,这样各走各的;宏任务先按顺序执行一圈,部分宏任务之后总会引出新的微任务函数,在每圈之后查看是否有微任务执行的话,就能尽快的把要结束的原有宏任务结束掉;既排了队,又不至于叠加等很久。
另外,网上有人解释可能还有数据执行污染的情况,虽然不是很理解正常为什么会对数据操作多次,但能理解有特殊情况。

二、promise&co&async同步异步模块

里外煎透promise,配菜co模块&async

三、vm.$nextTick和process.nextTick

提到nextTick,不知道有没有同时想到vm.$nextTick和process.nextTick;不辜负我们对它们的名字联想,这两个的核心思想还是一致的,对比一下核心源码看看,此处参考:

https://juejin.im/post/5e67a802e51d4526f76ecb9a
https://juejin.im/post/5e8edae5e51d4546b35655b2#heading-0
两位的文章都还是不错滴

// 1、vm.$nextTick
// 该函数的作用就是延迟 cb 到当前调用栈执行完成之后执行
export function nextTick (cb?: Function, ctx?: Object) {
  // 传入的回调函数会在callbacks中存起来
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // pending是一个状态标记,保证timerFunc在下一个tick之前只执行一次
  if (!pending) {
    pending = true
    /**
    * timerFunc 实现的就是根据当前环境判断使用哪种方式实现
    * 就是按照 Promise.then和 MutationObserver以及setImmediate的优先级来判断,支持哪个就用哪个,如果执行环境不支持,会采用setTimeout(fn, 0)代替;
    */
    timerFunc()
  }
  // 当nextTick不传参数的时候,提供一个Promise化的调用
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}


// 2、process.nextTick
process.nextTick(callback[, ...args])
从函数定义上讲,参数均为cb和obj参数值;执行思路均包含:
1. 会将cb添加到一个nextTick的队列
2. 队列会在下一次event loop开始执行前依次出队(队列先入先出)
具体的细节不同,还是要慢慢体会;不过先来简单说一下好处:对vue来说,是很明显的异步更新,为了避免修改重复多次事件调用,只需要更改最后一次即可,避免消耗过多性能;对node来说,是解决一些回调函数的执行处理,还需要探究。

四、看了一些源码之后,发现最深层的封装常用到js的原型继承以及js的call、apply、bind

看了一些源码之后,发现最深层的封装常用到js的“call、apply、bind”以及“原型继承”,如上述vm.$nextTick里有这样一句cb.call(ctx),另外这也是容易弄混的面试考点,且用对了有很好的效果。

4.1 call、apply、bind

提起这三个,可就又引出一个点"js的this指向",似乎进了一个连环坑,哈哈哈哈,那到都到这儿了,就攒攒力气一个个爬出去呗!

4.1.1 js的this指向

在没写出这个标题,脑子里就出现这么个意思“this指向的是其运行时所在的环境,一般指向window”;然而,坑之所以是个坑,那就代表没这么容易上岸!这句话主要指的是【非严格模式】下【普通函数调用】的this指向,如:

function testThis(){
	console.log(this)
}
testThis(); // 打印window对象

然而,【严格模式】下,在【JavaScript高级程序设计】中如是说:“在严格模式下,未指定环境对象而调用函数,则this值不会转为window。除非明确把函数添加到某个对象或者调用appy()或call(),否则this值将是undefined”,所以是这样滴:

'use strict'
function testThis(){
	console.log(this)
}
testThis();  // undefined
前人总结经验,this调用区分需要区分这三种情况(地址:https://blog.csdn.net/yangwei234/article/details/84451165)
1. 方法调用模式

就是函数被当做一个对象的方法调用,使用【对象】.【方法】的方式,此时this指向被绑定的对象

var a = 1;
var obj = {
    a: 2,
    fn: function(){
        console.log(this.a);
    }
}
obj.fn() // 2

这里,大佬提醒注意document的绑定事件是属于方法调用模式,所以内部是dom对象,这样也确实很方便我们操作dom,合理便利。举得例子是这样的

document.addEventListener('click', function(e){
    console.log(this);  // document
    setTimeout(function(){
        console.log(this); // window
    }, 200);
}, false);  

而setTimeout为什么是window呢?也常有人问它为什么会丢失this呢?关于这个问题我也说不透,主要是从这里看来的setTimeOut,有这么几句话,__由setTimeout()调用的代码运行在与所在函数完全分离的执行环境上。这会导致,这些代码中包含的 this 关键字在非严格模式会指向 window (或全局)对象,严格模式下为 undefined,这和所期望的this的值是不一样的。备注:即使是在严格模式下,setTimeout()的回调函数里面的this仍然默认指向window对象, 并不是undefined。__读书少了,不知道是不是事件循环机制的道理,待研究。

'use strict'
setTimeout(() => {
	console.log(this);
},200);
// 打印 window

参考文章的大佬帮助理解,setTimeout内的函数属于回调函数,可以这么理解,f1.call(null, f2),所以this指向window。

2. 普通函数调用

就是最终是函数调用,举了三个例子:
最普通的函数调用、函数嵌套、把函数赋值后调用

function fn(){
    console.log(this); //window
}
fn();


function fn1(){
    function fn2(){
        console.log(this); //window
    }
    fn2();
}
fn1();


var obj = {
    fn: function(){
        console.log(this);
    }
}
var fn1 = obj.fn;
fn1();//1

最后一种需要解释解释,因为这解释了我之前见过的这么一道题

var foo = {bar:function(){console.log(this)}}; (false || foo.bar)()

最后this指向的是window。obj.fn虽然是一个对象的函数,但是被赋值给fn1之后,fn1就是一个单纯的普通函数等待着被调用。所以这里在【非严格模式】下,无this或者说this为undefined和null的时候,它就是window;而【严格模式】,依然是undefined

'use strict'
var a = 1;
var obj = {
    a: 2,
    fn: function(){
        console.log(this);
    }
}
var fn1 = obj.fn;
fn1(); // undefined

所以这里__(false || foo.bar)()__可以理解为,前面的或语句是相当于定义了一个变量,变量被foo.bar赋值,调用则根据【非严格模式】指向window;【严格模式】,依然是undefined;

3. 构造器调用模式

new一个函数时,背地里会创建一个连接到 prototype 成员的新对象,同时 this 会被绑定到那个新对象上。

function Person(name, age){
    // 这里的this都是指向实例
    this.name  = name;
    this.age = age;
    this.sayAge = function(){
        console.log(this.age);
    }
}

var per = new Person('yw', 2);
per.sayAge(); // 2

关于这个的详细解释,嘿嘿嘿,估计要说说原型链了,后面补充

另外需要注意几点:
  1. js的箭头函数的this;众所周知,箭头函数解决了原来__var self = this;或 var this=this__这个苦笑不得的写法,这是因为__箭头函数的this是继承山下文来的,内部没有this_,至于怎么做到的,可能就跟一开始怎么做到this继承那么绕一样不可泄露吧,至今还未探索到。
  2. 闭包里的this,似乎也是一般指向window的,其实也不然;通过上面的例子,我们可以看this是根据调用不同以及是否使用了转this的函数来变化的;所以要到调用使用的地方去看this的指向;也不知道是不是可以夸一句灵活,还是埋怨一句变态,哈哈哈
  3. 如果要改变setTimeout里面this指向,用箭头函数或者bind即可;如下:
var num = 0;
function Obj (){
    this.num = 1,
    this.getNumLater = function(){
        setTimeout(() => {
            console.log(this.num);
        }, 1000)    //箭头函数中的this总是指向外层调用者,也就是Obj
        setTimeout(function(){
            console.log(this.num);
        }.bind(this), 1000)    //利用bind()将this绑定到这个函数上
    }
}

4.1.2 call、apply、bind

这三个函数都是为了改变this的指向;

类型 接收参数 第一个参数 其余参数 返回 立即执行
call 多个 this指向,默认window或undefined 有多个即多个依次
apply 两个 this指向,默认window或undefined arguments数组形式多个
bind 两个 明显的this指向 arguments数组形式多个 返回一个函数,等待调用 否,函数调用
call与apply的区别主要是除第一个参数之后的参数形式;bind返回的是一个改变了this的函数,且bind绑定的时候的参数是会预先传给返回的方法,调用方法的时候会默认带着这几个参数,其他的再依次补充,少为undefined,多不收。
call、apply都是立即执行,bind返回函数可调用多次;可能也有点授人以鱼不如授人以渔的区别吧~~
function fn(a, b, c){
    console.log(a, b, c);
}
var fn1 = fn.bind(null, 'yangwei');
fn('A', 'B', 'C');       // A B C
fn1('A', 'B', 'C');     // yangwei A B
fn1('B', 'C');           // yangwei B C
fn.call(null, 'yw');   // yw undefined undefined    call 是把第二个及以后的参数作为 fn 方法的实参传进去,而 fn1 方法的实参则是在 bind 中参数中的基础上再往后排

在es6箭头函数下,call与apply失效;-- 待实践
node环境中无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this都指向空对象{} – 待实践

使用语法翻译:从this指向开始,找call、apply、bind前面的是操作,后面是参数。
var total = [].push.apply(arr1, arr2); —> this指向arr1,第二个参数是个数组对象,需要操作的是数据里每一个值;那么就是arr1 push arr2的每一项,就相当于concat;

4.2 原型继承

第一次觉得“原型链和继承”看着是那么那么顺眼…

你可能感兴趣的:(js原理,js执行机制)