js异步编程及页面解析过程

一、首先介绍js单线程
1、js为什么是单线程(就一个线程主线程),而不去做成多线程
因js作为浏览器脚本语言,只要的用途和功能是与用户互动,以及操作DOM。(假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?会带来很复杂的同步问题)
2、什么是单线程:同一时间只能做一件事情。
3、任务队列(js单线程规定同一时间只能做一件事情,但我们的任务往往有很多个,所以就有任务队列一说):特点,单线程中所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。
坏处:如果前一个任务耗时很长,后面的任务必须等到前一个任务执行完才可以,这样性能极差,例如通过ajax请求网络数据。
解决:将任务分成同步任务及异步任务

  1. 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;
  2. 异步任务:先不进入主线程(不用占用时间让后面的任务等待)、而进入”任务队列”(task queue)的任务,只有”任务队列”通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

执行步骤及规则:
(1)文档流中所有同步任务一次进入主线程上执行,形成一个执行栈(依次顺序执行)
(2)异步任务进入到"任务队列"(task queue),只要异步任务执行完有了结果就在任务队列中放置一个事件(事件队列),可以将任务队列理解为事件队列。满足先进先出(先完成的先被主线程读取)
(3)当主线程执行完毕后,会去读取任务队列中的事件,进入主线程执行。(主线程会检查执行时间,事件只有到了规定的时间,才能返回主线程,例如定时器)
⚠️ 只要主线程空了,就会去读取”任务队列”,这就是JavaScript的运行机制。这个过程会不断重复。

⚠️:定时器的执行规则:也是异步的,他会在任务队列的尾部添加一个事件,也就是当主线程和任务队列”现有的事件都处理完,才会得到执行。

任务队列又区分:一个任务源的 task 必须放到同一个任务队列,从不同源来的则被添加到不同队列。setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。(任务源有宏任务和微任务)

在这里插入图片描述
执行一个宏任务(栈中没有就从事件队列中获取),主线程及读取都是宏任务
执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)
简单总结::先执行进入到主线程的所有同步的宏任务,当执行完后从事件队列中读取当前所有的微任务,依次到主线程进行执行,当执行完后,再执行宏任务(主线程有直接执行,无在任务队列的宏任务队列中继续执行)
依次循坏执行。
在这里插入图片描述

二、js异步操作
1、js 很多dom操作,例如click事件等
2、回调函数(es5前一直用它来实现异步):这个作为参数的函数就是回调函数,其作用是在需要的时候方便调用这段(回调函数)代码
简单来说:在直接调用函数A()时,把另一个函数B()作为参数,传入函数A()里面,以此来通过函数A()间接调用函数B()。

//  B函数被作为参数传递到A函数里,在A函数执行完后再执行B。而不是立马执行
// 简单回调函数
var test = function(abc){
    abc('HelloWorld'); // 实参
};
test( (words) => {console.log(words);});  调用test的 参数函数就是abc函数,abc调用的参数'HelloWorld'就是调用函数的实参
// 项目中常见写法
function A(callback){
    console.log("I am A");
    callback();  //调用该函数
}

function B(){
   console.log("I am B");
}

A(B);
// 嵌套回调函数
var test = function(abc){
    abc(   (def) => {console.log(def);}    ); // 回调函数的参数abc又是一个回调函数
}
test(      (aFunction) => {aFunction('HelloWorld')}      ); // test参数是abc函数,而aFunction是 

// 同步回调和异步回调的区别和知识点
// 1、同步回调:函数都是同步的,没有涉及点击事件、定时器、promise等
// 2、异步回调
function f1(callback){
  setTimeout(function () {
    // f1的任务代码
    callback();
  }, 1000);
}

f1(f2); //

f3();



3、定时器

setTimeout(()=>{
// 写执行逻辑
},3000)

4、promise (本身内部是一个同步函数,resolve与reject是异步的)
5、async await等

// 定时器异步任务
console.log('script start')	//1. 打印 script start
setTimeout(function(){ // 宏任务队列中
    console.log('settimeout')	// 4. 打印 settimeout
})	// 2. 调用 setTimeout 函数,并定义其完成后执行的回调函数(里面的打印函数),它是异步的挂起,当执行完后放入任务队列的宏任务队列中待读取到主线程执行回掉
console.log('script end')	//3. 打印 script start
// 输出顺序:script start->script end->settimeout
// promise异步函数,promise本身是同步的立即执行函数,只不过它里面的resove()及reject()是异步的,及对应执行then及catch中的内容是异步的
console.log('script start')  // 1、打印script start
let promise1 = new Promise(function (resolve) {
    console.log('promise1')     // 2、打印promise1(因为promise本身是同步的)
    resolve() // 5、它对应的then中内容挂起,当执行完后放到微任务队列中等待主线程调用
    console.log('promise1 end') // 3、打印promise1 end(因为promise本身是同步的)
}).then(function () {
    console.log('promise2')
})
setTimeout(function(){
    console.log('settimeout')  // 6、挂起,放到任务队列的宏任务队列中,待执行(主线程中的同步宏任务执行完后,微任务中取,依次将微任务执行完,再取宏任务,宏任务是执行一个渲染一个)
})
console.log('script end')  // 4、打印script end
// 输出顺序: script start->promise1->promise1 end->script end->promise2->settimeout



// promise.all的使用
// Promise.all() 方法接收一个promise的iterable类型(注:Array,Map,Set都属于ES6的iterable类型)的输入,并且只返回一个Promise实例, 那个输入的所有promise的resolve回调的结果是一个数组。(它是当所有输入的promise的resolve回调都结束或者没有promise执行了)。它的reject回调执行是,只要任何一个输入的promise的reject回调执行或者输入不合法的promise就会立即抛出错误,并且reject的是第一个抛出的错误信息
// 使用方法1,将结果成一个数组分别再进行操作
Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});
// expected output: Array [3, 42, "foo"] // 各个promise1 resolve的结果组成的数组

// 使用方法2 单独拿返回结果值,不在then中拿值
Promise.all([
      store.fetchPlanList(), // 自己里面写了单独的逻辑
      store.getReplaceLogisticsList(),
    ]).then(() => {
     // 写一些其他刷新或逻辑,因为此时接口已经拿到值了
    });
fetchPlanList() {
    return Request.get('/valeera/api/orderPlan/plan/list/v1', { onlyOnce: false })
    .then(({ data = [] }) => { this.tmpPlanList = data; });
  }
getReplaceLogisticsList() {
    Request.post('/valeera/check/api/dataQuery/esData/replace_logistics1')
      .then((res) => {
        this.replaceLogisticsList = res.data;
      });
  }
  // promise.all用于所有已完成或第一个失败,一般用户接口相互依赖都得返回后再处理一些逻辑的操作(或者直接用回调,也是可以实现依赖关系请求。或者在一个请求成功里再调用另一个)
  // Promise.all 当且仅当传入的可迭代对象为空时为同步
// async/await 同步代码的形式实现了异步
 // async/await 异步操作。async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再执行函数体内后面的语句。可以理解为,是让出了线程,跳出了 async 函数体。   async 函数的返回值是 Promise 实例对象
 // 例如  async的返回可用于promise的方法处理
 async function queryData(id){
    const ret = await axios.get('/data');
    return ret;
}
queryData.then(ret => {
    console.log(ret);
})

async function async1(){ // 刚进来此函数还未被调用
   console.log('async1 start');  // 2、打印async1 start
    await async2(); // 调用async2去执行,但它后面的内容直到async2()执行完才被执行,已成异步了,已回归到主线程了()只是让出此async线程
    console.log('async1 end') // 5、打印async1 end
}
async function async2(){ // 刚进来此函数还未被调用
    console.log('async2')   // 3、打印async2
}

console.log('script start'); // 1、打印script start
async1();
console.log('script end') // 4、打印script end

// 输出顺序:script start->async1 start->async2->script end->async1 end
// 特点遇await即遇到异步操作,将异步操作放入队列,让出主线程,继续让主线程执行之后的同步任务,当挂起的回掉被主线程执行后再执行await后面的内容

// 综合题
//请写出输出内容
async function async1() {
    console.log('async1 start'); // 2、打印async1 start
    await async2(); // 让出主线程
    console.log('async1 end'); // 6、挂起到微任务队列中 
}
async function async2() {
	console.log('async2'); // 3、打印async2
}

console.log('script start');  // 1、打印script start

setTimeout(function() {
    console.log('setTimeout'); // 8、挂起到任务队列中,执行完后再宏任务队列中添加事件,待主线程调用
}, 0)

async1();

new Promise(function(resolve) {
    console.log('promise1'); // 4、打印promise1
    resolve();
}).then(function() {
    console.log('promise2'); // 7、放入微任务队列中
});
console.log('script end'); // 5、打印script end

/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/

补充 页面解析过程
客户端(浏览器)发起页面请求,主机对 地址中的DNS 域名进行解析,找到对应的 IP 地址,请求发送到 服务端,服务器根据请求内容发送响应给客户端,客户端收到响应,将内容渲染成网页。
域名解析的具体过程:
(1)浏览器首先看浏览器的缓存中是否缓存了域名对应的IP地址,若存在,直接使用建立tcp连接发送http进行前后端交互拿页面渲染的内容。若不存在则向本地的域名服务器发送请求进行查询
(2)本地域名服务器上的解析:本地域名服务器收到请求后首先会查看本地的DNS缓存,若果存在相应的缓存则返回给查询主机。否则继续向根域名服务器发送一个域名解析请求。
(3)根域名服务器上的解析:当根域名服务器接收到本地域名服务器发送的域名解析请求后,根域名服务器返回顶级域名(例如com)的一个或多个IP地址。(根域名服务器只负责顶级域名的解析)
(4)顶级域名服务器上的解析:当本地域名服务器收到顶级域名的ip地址后,进一步com的域名服务器发送一个域名解析请求,最后顶级域名服务器返回 可以解析域名域名服务器地址,再次发送请求完成解析
(6)本地域名解析服务器上完成解析:当本地域名服务器收到完成域名的ip地址后,放到本地域名服务器的缓存中,最后返回ip地址给查询主机
(7)主机得到ip地址后设置TTL(指域名解析记录的留存时间),建立之际与服务器的连接tcl,发送http请求,进行交互

(8)当拿到服务器的响应数据后,浏览器进行页面的渲染:

浏览器渲染页面的过程:浏览器构建dom树并将绘制在屏幕上
1)解析html生成dom树
2)解析css生成css dom树
3)dom树和css树生成render树
4)根据render树进行布局
5)根据布局进行页面绘制
⚠️: 浏览器进行dom渲染的过程是占用主线程的,所以当遇到非异步的script记载js时会阻塞进程,所以写代码时进行做到一部分加载js和图片等的懒加载,减少用户等待页面加载出来的时间。

浏览器渲染进程包含1、解析HTML文件和CSS文件,加载图片等资源文件,渲染成用户看到的页面;2、执行解析js文件脚本代码。
整个过程浏览器会开启多个线程协作完成,包括:GUI渲染线程,JS引擎线程,事件触发线程,定时器触发线程,异步HTTP请求线程。
其中GUI渲染线程和JS引擎线程是相互排斥的,因为JS引擎线程在执行的时候有可能会发生重绘和回流。

HTTP报文格式:请求报文和响应报文都是由三部分组成

// 1、http请求报文格式:一般包括 请求行  请求头  主体
GET /somedir/page.html HTTP/1.1  (请求方法、url、http版本号)
Host: www.someschool.edu
Connection: close
User-agent: Mozilla/5.0
Accept-language: fr

// 2、http响应报文的格式:一般包括 状态行(申明http的版本号、状态码及描述) 首部行(额外信息) 实体主体(传输的数据)
HTTP/1.1 200 OK
Connection: close
Date: Tue, 09 Aug 2011 15:44:04 GMT
Server: Apache/2.2.3 (CentOS)
Last-Modified: Tue, 09 Aug 2011 15:11:03 GMT
Content-Length: 6821
Content-Type: text/html

(data data data data data ...)

面试问题:页面抖动的原因

// 回答1 页面抖动即页面轮播或下拉,可以在css的底部设置(是css控制页面的高度导致的,实质就是滚动条显示不显示的问题)
html, body {height:100%;overflow:auto;margin: 0;}
或
html,body{ overflow-y:scroll;} 
html,body{ overflow:scroll; min-height:101%;} 
html{ overflow:-moz-scrollbars-vertical;} 

// 回答2 可以使用css3中的计算(原因:页面居中使用margin: 0 auto,页面出现滚动条时改变了页面的宽度)
// 在居中定宽主体的父级加入一下css3代码
.wrap-outer {
    margin-left: calc(100vw - 100%);
}
100vw相对于浏览器的window.innerWidth,是浏览器的内部宽度,其中滚动条也计算在内,而100%是可用宽度,不包含滚动条,所以表达式calc(100vw - 100%)就是计算出滚动条的宽度大小,所以当出现浏览器滚动条的时候,将页面往左边挤,margin-left的设置又将内容往右边挤了相同的宽度,这就导致了虽然页面没有变化,但是我们居中元素的margin值改变了

// 图片抖动问题的原因及解决办法
// 可能原因:图片资源是通过异步外部加载图片的,这样图片实在dom树渲染后调用的,此时dom树已经知道各个节点的布局与位置,在DOM结构渲染完成时,img图片尚未加载,此时我们并不知道图片的宽高比,高度会默认为0。当图片加载完成后,已经知道图片的信息了,会重新计算,并进行重绘,所以会出现抖动现象。

异步问题深入学习网址:
1、https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/33
2、https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/156
3、https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/7

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值