作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前 端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
作用域可以理解为变量的可访问性,总共分为三种类型,分别为:
let
、const
就可以产生该作用域其实看完前面的闭包、this
这部分内部的话,应该基本能了解作用域的一些应用。
一旦我们将这些作用域嵌套起来,就变成了另外一个重要的知识点「作用域链」,也就是 JS 到底是如何访问需要的变量或者函数的。
[[Scopes]]
图中的 [[Scopes]]
是个数组,作用域的一层层往上寻找就等同于遍历 [[Scopes]]
。
1. 全局作用域
全局变量是挂载在 window 对象下的变量,所以在网页中的任何位置你都可以使用并且访问到这个全局变量
var globalName = 'global';
function getName() {
console.log(globalName) // global
var name = 'inner'
console.log(name) // inner
}
getName();
console.log(name); //
console.log(globalName); //global
function setName(){
vName = 'setName';
}
setName();
console.log(vName); // setName
2. 函数作用域
函数中定义的变量叫作函数变量,这个时候只能在函数内部才能访问到它,所以它的作用域也就是函数的内部,称为函数作用域
function getName () {
var name = 'inner';
console.log(name); //inner
}
getName();
console.log(name);
除了这个函数内部,其他地方都是不能访问到它的。同时,当这个函数被执行完之后,这个局部变量也相应会被销毁。所以你会看到在 getName 函数外面的 name 是访问不到的
3. 块级作用域
ES6 中新增了块级作用域,最直接的表现就是新增的 let 关键词,使用 let 关键词定义的变量只能在块级作用域中被访问,有“暂时性死区”的特点,也就是说这个变量在定义之前是不能被使用的。
在 JS 编码过程中 if 语句
及 for
语句后面 {...}
这里面所包括的,就是块级作用域
console.log(a) //a is not defined
if(true){
let a = '123';
console.log(a); // 123
}
console.log(a) //a is not defined
从这段代码可以看出,变量 a 是在
if 语句{...}
中由let 关键词
进行定义的变量,所以它的作用域是 if 语句括号中的那部分,而在外面进行访问 a 变量是会报错的,因为这里不是它的作用域。所以在 if 代码块的前后输出 a 这个变量的结果,控制台会显示 a 并没有定义
以 iPhone XS 为例,当写 CSS 代码时,针对于单位 px,其宽度为 414px & 896px,也就是说当赋予一个 DIV元素宽度为 414px,这个 DIV 就会填满手机的宽度;
而如果有一把尺子来实际测量这部手机的物理像素,实际为 1242*2688 物理像素;经过计算可知,1242/414=3,也就是说,在单边上,一个逻辑像素=3个物理像素,就说这个屏幕的像素密度为 3,也就是常说的 3 倍屏。
对于图片来说,为了保证其不失真,1 个图片像素至少要对应一个物理像素,假如原始图片是 500300 像素,那么在 3 倍屏上就要放一个 1500900 像素的图片才能保证 1 个物理像素至少对应一个图片像素,才能不失真。 当然,也可以针对所有屏幕,都只提供最高清图片。虽然低密度屏幕用不到那么多图片像素,而且会因为下载多余的像素造成带宽浪费和下载延迟,但从结果上说能保证图片在所有屏幕上都不会失真。
还可以使用 CSS 媒体查询来判断不同的像素密度,从而选择不同的图片:
my-image { background: (low.png); }
@media only screen and (min-device-pixel-ratio: 1.5) {
#my-image { background: (high.png); }
}
(1)SVG: SVG可缩放矢量图形(Scalable Vector Graphics)是基于可扩展标记语言XML描述的2D图形的语言,SVG基于XML就意味着SVG DOM中的每个元素都是可用的,可以为某个元素附加Javascript事件处理器。在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象的属性发生变化,那么浏览器能够自动重现图形。
其特点如下:
(2)Canvas: Canvas是画布,通过Javascript来绘制2D图形,是逐像素进行渲染的。其位置发生改变,就会重新进行绘制。
其特点如下:
注:矢量图,也称为面向对象的图像或绘图图像,在数学上定义为一系列由线连接的点。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具有颜色、形状、轮廓、大小和屏幕位置等属性。
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
预处理器, 如:less
,sass
,stylus
,用来预编译sass
或者less
,增加了css
代码的复用性。层级,mixin
, 变量,循环, 函数等对编写以及开发UI组件都极为方便。
后处理器, 如: postCss
,通常是在完成的样式表中根据css
规范处理css
,让其更加有效。目前最常做的是给css
属性添加浏览器私有前缀,实现跨浏览器兼容性的问题。
css
预处理器为css
增加一些编程特性,无需考虑浏览器的兼容问题,可以在CSS
中使用变量,简单的逻辑程序,函数等在编程语言中的一些基本的性能,可以让css
更加的简洁,增加适应性以及可读性,可维护性等。
其它css
预处理器语言:Sass(Scss)
, Less
, Stylus
, Turbine
, Swithch css
, CSS Cacheer
, DT Css
。
使用原因:
CSS
代码,可以应用到老项目中现代计算机中数据都是以二进制的形式存储的,即0、1两种状态,计算机对二进制数据进行的运算加减乘除等都是叫位运算,即将符号位共同参与运算的运算。
常见的位运算有以下几种:
| 运算符 | 描述 | 运算规则 |
| --- | --- | --- | --- |
| &
| 与 | 两个位都为1时,结果才为1 |
| |
| 或 | 两个位都为0时,结果才为0 |
| ^
| 异或 | 两个位相同为0,相异为1 |
| ~
| 取反 | 0变1,1变0 |
| <<
| 左移 | 各二进制位全部左移若干位,高位丢弃,低位补0 |
| >>
| 右移 | 各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃 |
定义: 参加运算的两个数据按二进制位进行“与”运算。 运算规则:
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
总结:两位同时为1,结果才为1,否则结果为0。
例如:3&5 即:
0000 0011
0000 0101
= 0000 0001
因此 3&5 的值为1。
注意:负数按补码形式参加按位与运算。
用途:
(1)判断奇偶
只要根据最未位是0还是1来决定,为0就是偶数,为1就是奇数。因此可以用if ((i & 1) == 0)
代替if (i % 2 == 0)
来判断a是不是偶数。
(2)清零
如果想将一个单元清零,即使其全部二进制位为0,只要与一个各位都为零的数值相与,结果为零。
定义: 参加运算的两个对象按二进制位进行“或”运算。
运算规则:
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
总结:参加运算的两个对象只要有一个为1,其值为1。
例如:3|5即:
0000 0011
0000 0101
= 0000 0111
因此,3|5的值为7。
注意:负数按补码形式参加按位或运算。
定义: 参加运算的两个数据按二进制位进行“异或”运算。
运算规则:
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
总结:参加运算的两个对象,如果两个相应位相同为0,相异为1。
例如:3|5即:
0000 0011
0000 0101
= 0000 0110
因此,3^5的值为6。
异或运算的性质:
(a^b)^c == a^(b^c)
(a + b)^c == a^b + b^c
x^x=0,x^0=x
a^b^b=a^0=a
;定义: 参加运算的一个数据按二进制进行“取反”运算。
运算规则:
~ 1 = 0~ 0 = 1
总结:对一个二进制数按位取反,即将0变1,1变0。
例如:~6 即:
0000 0110= 1111 1001
在计算机中,正数用原码表示,负数使用补码存储,首先看最高位,最高位1表示负数,0表示正数。此计算机二进制码为负数,最高位为符号位。
当发现按位取反为负数时,就直接取其补码,变为十进制:
0000 0110 = 1111 1001反码:1000 0110补码:1000 0111
因此,~6的值为-7。
定义: 将一个运算对象的各二进制位全部左移若干位,左边的二进制位丢弃,右边补0。
设 a=1010 1110,a = a<< 2 将a的二进制位左移2位、右补0,即得a=1011 1000。
若左移时舍弃的高位不包含1,则每左移一位,相当于该数乘以2。
定义: 将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。
例如:a=a>>2 将a的二进制位右移2位,左补0 或者 左补1得看被移数是正还是负。
操作数每右移一位,相当于该数除以2。
上面提到了补码、反码等知识,这里就补充一下。
计算机中的有符号数有三种表示方法,即原码、反码和补码。三种表示方法均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位,三种表示方法各不相同。
(1)原码
原码就是一个数的二进制数。例如:10的原码为0000 1010
(2)反码
例如:-10
原码:1000 1010
反码:1111 0101
(3)补码
例如:-10
原码:1000 1010
反码:1111 0101
补码:1111 0110
参考 前端进阶面试题详细解答
全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在
全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。
标准内置对象的分类:
(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。例如 Infinity、NaN、undefined、null 字面量
(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。例如 eval()、parseFloat()、parseInt() 等
(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。例如 Object、Function、Boolean、Symbol、Error 等
(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。例如 Number、Math、Date
(5)字符串,用来表示和操作字符串的对象。例如 String、RegExp
(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array
(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。
例如 Map、Set、WeakMap、WeakSet
(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。
例如 SIMD 等
(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。例如 JSON 等
(10)控制抽象对象
例如 Promise、Generator 等
(11)反射。例如 Reflect、Proxy
(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。例如 Intl、Intl.Collator 等
(13)WebAssembly
(14)其他。例如 arguments
总结: js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。
<meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no" />
// width 设置viewport宽度,为一个正整数,或字符串‘device-width’
// device-width 设备宽度
// height 设置viewport高度,一般设置了宽度,会自动解析出高度,可以不用设置
// initial-scale 默认缩放比例(初始缩放比例),为一个数字,可以带小数
// minimum-scale 允许用户最小缩放比例,为一个数字,可以带小数
// maximum-scale 允许用户最大缩放比例,为一个数字,可以带小数
// user-scalable 是否允许手动缩放
1px
被 渲染成 2px
问题局部处理
meta
标签中的 viewport
属性 ,initial-scale
设置为 1
rem
按照设计稿标准走,外加利用transfrome
的scale(0.5)
缩小一倍即可;全局处理
mate
标签中的 viewport
属性 ,initial-scale
设置为 0.5
rem
按照设计稿标准走即可1. connect用法
作用:连接
React
组件与Redux store
connect([mapStateToProps], [mapDispatchToProps], [mergeProps],[options])
// 这个函数允许我们将 store 中的数据作为 props 绑定到组件上
const mapStateToProps = (state) => {
return {
count: state.count
}
}
Redux
的 store
,我们从中摘取了 count
属性。你不必将 state
中的数据原封不动地传入组件,可以根据 state
中的数据,动态地输出组件需要的(最小)属性ownProps
,是组件自己的 props
当
state
变化,或者ownProps
变化的时候,mapStateToProps
都会被调用,计算出一个新的stateProps
,(在与ownProps merge
后)更新给组件
mapDispatchToProps(dispatch, ownProps): dispatchProps
connect
的第二个参数是mapDispatchToProps
,它的功能是,将action
作为props
绑定到组件上,也会成为MyComp
的 `props
2. 原理解析
首先
connect
之所以会成功,是因为Provider
组件
Provider
的子组件Redux
的store
作为props
,通过context
对象传递给子孙组件上的connect
connect做了些什么
它真正连接
Redux
和React
,它包在我们的容器组件的外一层,它接收上面Provider
提供的store
里面的state
和dispatch
,传给一个构造函数,返回一个对象,以属性形式传给我们的容器组件
3. 源码
connect
是一个高阶函数,首先传入mapStateToProps
、mapDispatchToProps
,然后返回一个生产Component
的函数(wrapWithConnect
),然后再将真正的Component
作为参数传入wrapWithConnect
,这样就生产出一个经过包裹的Connect
组件,该组件具有如下特点
props.store
获取祖先Component
的store props
包括stateProps
、dispatchProps
、parentProps
,合并在一起得到nextState
,作为props
传给真正的Component
componentDidMount
时,添加事件this.store.subscribe(this.handleChange)
,实现页面交互shouldComponentUpdate
时判断是否有避免进行渲染,提升页面性能,并得到nextState
componentWillUnmount
时移除注册的事件this.handleChange
// 主要逻辑
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
return function wrapWithConnect(WrappedComponent) {
class Connect extends Component {
constructor(props, context) {
// 从祖先Component处获得store
this.store = props.store || context.store
this.stateProps = computeStateProps(this.store, props)
this.dispatchProps = computeDispatchProps(this.store, props)
this.state = { storeState: null }
// 对stateProps、dispatchProps、parentProps进行合并
this.updateState()
}
shouldComponentUpdate(nextProps, nextState) {
// 进行判断,当数据发生改变时,Component重新渲染
if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
this.updateState(nextProps)
return true
}
}
componentDidMount() {
// 改变Component的state
this.store.subscribe(() = {
this.setState({
storeState: this.store.getState()
})
})
}
render() {
// 生成包裹组件Connect
return (
<WrappedComponent {...this.nextState} />
)
}
}
Connect.contextTypes = {
store: storeShape
}
return Connect;
}
}
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
obj.__proto__ === Array.prototype;
Array.isArrray(obj);
obj instanceof Array
Array.prototype.isPrototypeOf(obj)
<!Doctype html>
有何作用? 严格模式与混杂模式如何区分?它们有何意义?文档声明的作用: 文档声明是为了告诉浏览器,当前HTML
文档使用什么版本的HTML
来写的,这样浏览器才能按照声明的版本来正确的解析。
的作用:<!doctype html>
的作用就是让浏览器进入标准模式,使用最新的 HTML5
标准来解析渲染页面;如果不写,浏览器就会进入混杂模式,我们需要避免此类情况发生。
严格模式与混杂模式的区分:
W3C
标准解析代码;区分:网页中的DTD
,直接影响到使用的是严格模式还是浏览模式,可以说DTD
的使用与这两种方式的区别息息相关。
DOCTYPE
,那么它一般以严格模式呈现(严格 DTD ——严格模式);DTD
和 URI
的 DOCTYPE
,也以严格模式呈现,但有过渡 DTD
而没有 URI
(统一资源标识符,就是声明最后的地址)会导致页面以混杂模式呈现(有 URI 的过渡 DTD ——严格模式;没有 URI 的过渡 DTD ——混杂模式);DOCTYPE
不存在或形式不正确会导致文档以混杂模式呈现(DTD不存在或者格式不正确——混杂模式);HTML5
没有 DTD
,因此也就没有严格模式与混杂模式的区别,HTML5
有相对宽松的 法,实现时,已经尽可能大的实现了向后兼容(HTML5 没有严格和混杂之分)。总之,严格模式让各个浏览器统一执行一套规范兼容模式保证了旧网站的正常运行。
script
来执行(宏任务)promise
click
事件...不会立即执行,需要等待当前代码全部执行完毕script
执行完毕后,会清空所有的微任务例子
自动执行的情况 会输出 listener1 listener2 task1 task2
如果手动点击click 会一个宏任务取出来一个个执行,先执行click的宏任务,取出微任务去执行。会输出 listener1 task1 listener2 task2
console.log(1)
async function asyncFunc(){
console.log(2)
// await xx ==> promise.resolve(()=>{console.log(3)}).then()
// console.log(3) 放到promise.resolve或立即执行
await console.log(3)
// 相当于把console.log(4)放到了then promise.resolve(()=>{console.log(3)}).then(()=>{
// console.log(4)
// })
// 微任务谁先注册谁先执行
console.log(4)
}
setTimeout(()=>{console.log(5)})
const promise = new Promise((resolve,reject)=>{
console.log(6)
resolve(7)
})
promise.then(d=>{console.log(d)})
asyncFunc()
console.log(8)
// 输出 1 6 2 3 8 7 4 5
1. 浏览器事件循环
涉及面试题:异步代码执行顺序?解释一下什么是
Event Loop
?JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变
js代码执行过程中会有很多任务,这些任务总的分成两类:
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。,我们用导图来说明:
我们解释一下这张图:
那主线程执行栈何时为空呢?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数
以上就是js运行的整体流程
面试中该如何回答呢? 下面是我个人推荐的回答:
setTimeout(function() {
console.log(1)
}, 0);
new Promise(function(resolve, reject) {
console.log(2);
resolve()
}).then(function() {
console.log(3)
});
process.nextTick(function () {
console.log(4)
})
console.log(5)
setTimeout
,将setTimeout的回调函数丢到宏任务队列中,在往下执行new Promise
立即执行,输出2,then的回调函数丢到微任务队列中,再继续执行,遇到process.nextTick
,同样将回调函数扔到微任务队列,再继续执行,输出5,当所有同步任务执行完成后看有没有可以执行的微任务,发现有then函数和nextTick
两个微任务,先执行哪个呢?process.nextTick
指定的异步任务总是发生在所有异步任务之前,因此先执行process.nextTick输出4然后执行then函数输出3,第一轮执行结束。
JS
在执行的过程中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。如果遇到异步的代码,会被挂起并加入到Task
(有多种task
) 队列中。一旦执行栈为空,Event
Loop
就会从Task
队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说JS
中的异步还是同步行为
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
console.log('script end');
不同的任务源会被分配到不同的
Task
队列中,任务源可以分为 微任务(microtask
) 和 宏任务(macrotask
)。在ES6
规范中,microtask
称为jobs
,macrotask
称为task
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
new Promise((resolve) => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout
以上代码虽然
setTimeout
写在Promise
之前,但是因为Promise
属于微任务而setTimeout
属于宏任务
微任务
process.nextTick
promise
Object.observe
MutationObserver
宏任务
script
setTimeout
setInterval
setImmediate
I/O
网络请求完成、文件读写完成事件UI rendering
宏任务中包括了
script
,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务
所以正确的一次 Event loop 顺序是这样的
Event loop
,执行宏任务中的异步代码通过上述的
Event loop
顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作DOM
的话,为了更快的响应界面响应,我们可以把操作DOM
放入微任务中
总结起来就是:
一次 Eventloop 循环会处理一个宏任务和所有这次循环中产生的微任务
。
2. Node 中的 Event loop
当 Node.js 开始启动时,会初始化一个 Eventloop,处理输入的代码脚本,这些脚本会进行 API 异步调用,
process.nextTick()
方法会开始处理事件循环。下面就是 Node.js 官网提供的Eventloop
事件循环参考流程
Node
中的 Event loop
和浏览器中的不相同。Node
的 Event loop
分为6
个阶段,它们会按照顺序反复运行process.nextTick
node中的微任务,当前执行栈的底部,优先级比promise
要高整个流程分为六个阶段,当这六个阶段执行完一次之后,才可以算得上执行了一次 Eventloop 的循环过程。我们来分别看下这六个阶段都做了哪些事情。
setTimeout
和 setInterval
的回调函数,简单理解就是由这两个函数启动的回调函数。poll
阶段是一个重要且复杂的阶段,几乎所有 I/O
相关的回调,都在这个阶段执行(除了setTimeout
、setInterval
、setImmediate
以及一些因为 exception
意外关闭产生的回调)。检索新的 I/O 事件,执行与 I/O 相关的回调
,其他情况 Node.js 将在适当的时候在此阻塞。这也是最复杂的一个阶段,所有的事件循环以及回调处理都在这个阶段执行。这个阶段的主要流程如下图所示。setImmediate()
回调函数在这里执行,setImmediate
并不是立马执行,而是当事件循环 poll 中没有新的事件处理时就执行该部分
,如下代码所示。const fs = require('fs');
setTimeout(() => { // 新的事件循环的起点
console.log('1');
}, 0);
setImmediate( () => {
console.log('setImmediate 1');
});
/// fs.readFile 将会在 poll 阶段执行
fs.readFile('./test.conf', {encoding: 'utf-8'}, (err, data) => {
if (err) throw err;
console.log('read file success');
});
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
console.log('poll callback');
});
// 首次事件循环执行
console.log('2');
在这一代码中有一个非常奇特的地方,就是 setImmediate
会在 setTimeout
之后输出。有以下几点原因:
setTimeout
如果不设置时间或者设置时间为0
,则会默认为1ms
主流程执行完成后,超过1ms
时,会将setTimeout
回调函数逻辑插入到待执行回调函数poll
队列中; 由于当前poll
队列中存在可执行回调函数,因此需要先执行完,待完全执行完成后,才会执行check:setImmediate
。因此这也验证了这句话,
先执行回调函数,再执行 setImmediate
socket.on('close', ...)
除了把 Eventloop 的宏任务细分到不同阶段外。node 还引入了一个新的任务队列
Process.nextTick()
可以认为,Process.nextTick()
会在上述各个阶段结束时,在进入下一个阶段之前立即执行
(优先级甚至超过 microtask
队列)
事件循环的主要包含微任务和宏任务。具体是怎么进行循环的呢
process.nextTick
和 Promise
。微任务在事件循环中优先级是最高的
,因此在同一个事件循环中有其他任务存在时,优先执行微任务队列。并且process.nextTick 和 Promise
也存在优先级,process.nextTick
高于 Promise
setTimeout
、setInterval
、setImmediate
和 I/O
。宏任务在微任务执行之后执行,因此在同一个事件循环周期内,如果既存在微任务队列又存在宏任务队列,那么优先将微任务队列清空,再执行宏任务队列我们可以看到有一个核心的主线程,它的执行阶段主要处理三个核心逻辑。
const fs = require('fs');
// 首次事件循环执行
console.log('start');
/// 将会在新的事件循环中的阶段执行
fs.readFile('./test.conf', {encoding: 'utf-8'}, (err, data) => {
if (err) throw err;
console.log('read file success');
});
setTimeout(() => { // 新的事件循环的起点
console.log('setTimeout');
}, 0);
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
console.log('Promise callback');
});
/// 执行 process.nextTick
process.nextTick(() => {
console.log('nextTick callback');
});
// 首次事件循环执行
console.log('end');
分析下上面代码的执行过程
Promise.resolve 和 process.nextTick
,宏任务队列包含:fs.readFile 和 setTimeout
;process.nextTick 再执行 Promise.resolve
,所以先输出 nextTick callback
再输出 Promise callback
;宏任务插入先后顺序执行 setTimeout 再执行 fs.readFile
,这里需要注意,先执行 setTimeout
由于其回调时间较短,因此回调也先执行,并非是 setTimeout
先执行所以才先执行回调函数,但是它执行需要时间肯定大于 1ms
,所以虽然 fs.readFile
先于setTimeout
执行,但是 setTimeout
执行更快,所以先输出 setTimeout
,最后输出 read file success
。// 输出结果
start
end
nextTick callback
Promise callback
setTimeout
read file success
当微任务和宏任务又产生新的微任务和宏任务时,又应该如何处理呢?如下代码所示:
const fs = require('fs');
setTimeout(() => { // 新的事件循环的起点
console.log('1');
fs.readFile('./config/test.conf', {encoding: 'utf-8'}, (err, data) => {
if (err) throw err;
console.log('read file sync success');
});
}, 0);
/// 回调将会在新的事件循环之前
fs.readFile('./config/test.conf', {encoding: 'utf-8'}, (err, data) => {
if (err) throw err;
console.log('read file success');
});
/// 该部分将会在首次事件循环中执行
Promise.resolve().then(()=>{
console.log('poll callback');
});
// 首次事件循环执行
console.log('2');
在上面代码中,有 2 个宏任务和 1 个微任务,宏任务是 setTimeout 和 fs.readFile
,微任务是 Promise.resolve
。
poll callback
。fs.readFile 和 setTimeout
,由于 fs.readFile
优先级高,先执行 fs.readFile
。但是处理时间长于 1ms
,因此会先执行 setTimeout
的回调函数,输出 1
。这个阶段在执行过程中又会产生新的宏任务 fs.readFile
,因此又将该 fs.readFile 插入宏任务队列
fs.readFile
,因此执行该宏任务,并等待处理完成后的回调,输出 read file sync success
。// 结果
2
poll callback
1
read file success
read file sync success
Process.nextick() 和 Vue 的 nextick
Node.js
和浏览器端宏任务队列的另一个很重要的不同点是,浏览器端任务队列每轮事件循环仅出队一个回调函数接着去执行微任务队列;而Node.js
端只要轮到执行某个宏任务队列,则会执行完队列中所有的当前任务,但是当前轮次新添加到队尾的任务则会等到下一轮次才会执行。
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout
上面介绍的都是
macrotask
的执行情况,microtask
会在以上每个阶段完成后立即执行
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
// 以上代码在浏览器和 node 中打印情况是不同的
// 浏览器中一定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2
Node
中的process.nextTick
会先于其他microtask
执行
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1");
});
}, 0);
// poll阶段执行
fs.readFile('./test',()=>{
// 在poll阶段里面 如果有setImmediate优先执行,setTimeout处于事件循环顶端 poll下面就是setImmediate
setTimeout(()=>console.log('setTimeout'),0)
setImmediate(()=>console.log('setImmediate'),0)
})
process.nextTick(() => {
console.log("nextTick");
});
// nextTick, timer1, promise1,setImmediate,setTimeout
对于
microtask
来说,它会在以上每个阶段完成前清空microtask
队列,下图中的Tick
就代表了microtask
谁来启动这个循环过程,循环条件是什么?
当 Node.js 启动后,会初始化事件循环,处理已提供的输入脚本,它可能会先调用一些异步的 API、调度定时器,或者
process.nextTick()
,然后再开始处理事件循环。因此可以这样理解,Node.js 进程启动后,就发起了一个新的事件循环,也就是事件循环的起点。
总结来说,Node.js 事件循环的发起点有 4 个:
Node.js
启动后;setTimeout
回调函数;setInterval
回调函数;I/O
后的回调函数。无限循环有没有终点
当所有的微任务和宏任务都清空的时候,虽然当前没有任务可执行了,但是也并不能代表循环结束了。因为可能存在当前还未回调的异步 I/O,所以这个循环是没有终点的,只要进程在,并且有新的任务存在,就会去执行
Node.js 是单线程的还是多线程的?
主线程是单线程执行的
,但是 Node.js存在多线程执行
,多线程包括setTimeout 和异步 I/O 事件
。其实 Node.js 还存在其他的线程,包括垃圾回收、内存优化
等
EventLoop 对渲染的影响
requestIdlecallback 和 requestAnimationFrame
,这两个函数在我们之前的内容中没有讲过,但是当你开始考虑它们在 Eventloop 的生命周期的哪一步触发,或者这两个方法的回调会在微任务队列还是宏任务队列执行的时候,才发现好像没有想象中那么简单。这两个方法其实也并不属于 JS 的原生方法,而是浏览器宿主环境提供的方法,因为它们牵扯到另一个问题:渲染。requestAnimationFrame
的出现却把这两件事情给关联起来requestAnimationFrame
我们可以在下次渲染之前执行回调函数。那下次渲染具体是哪个时间点呢?渲染和 Eventloop 有什么关系呢?Eventloop
的末尾,判断当前页面是否处于渲染时机,就是重新渲染
16.6ms
,因为就算渲染了屏幕上也看不到。当然浏览器也不能保证一定会每 16.6ms 会渲染一次,因为还会受到处理器的性能、JavaScript 执行效率等其他因素影响。requestAnimationFrame
,这个 API 保证在下次浏览器渲染之前一定会被调用,实际上我们完全可以把它看成是一个高级版的 setInterval
。它们都是在一段时间后执行回调,但是前者的间隔时间是由浏览器自己不断调整的,而后者只能由用户指定。这样的特性也决定了 requestAnimationFrame
更适合用来做针对每一帧来修改的动画效果requestAnimationFrame
不是 Eventloop
里的宏任务,或者说它并不在 Eventloop
的生命周期里,只是浏览器又开放的一个在渲染之前发生的新的 hook。另外需要注意的是微任务的认知概念也需要更新,在执行 animation callback 时也有可能产生微任务(比如 promise 的 callback),会放到 animation queue 处理完后再执行。所以微任务并不是像之前说的那样在每一轮 Eventloop 后处理,而是在 JS 的函数调用栈清空后处理但是 requestIdlecallback
却是一个更好理解的概念。当宏任务队列中没有任务可以处理时,浏览器可能存在“空闲状态”。这段空闲时间可以被 requestIdlecallback
利用起来执行一些优先级不高、不必立即执行的任务,如下图所示:
// (1)匹配 16 进制颜色值
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
// (2)匹配日期,如 yyyy-mm-dd 格式
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
// (3)匹配 qq 号
var regex = /^[1-9][0-9]{4,10}$/g;
// (4)手机号码正则
var regex = /^1[34578]\d{9}$/g;
// (5)用户名正则
var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
DNS 服务器中以资源记录的形式存储信息,每一个 DNS 响应报文一般包含多条资源记录。一条资源记录的具体的格式为
(Name,Value,Type,TTL)
其中 TTL 是资源记录的生存时间,它定义了资源记录能够被其他的 DNS 服务器缓存多长时间。
常用的一共有四种 Type 的值,分别是 A、NS、CNAME 和 MX ,不同 Type 的值,对应资源记录代表的意义不同:
TCP 和 UDP都是传输层协议,他们都属于TCP/IP协议族:
(1)UDP
UDP的全称是用户数据报协议,在网络中它与TCP协议一样用于处理数据包,是一种无连接的协议。在OSI模型中,在传输层,处于IP协议的上一层。UDP有不提供数据包分组、组装和不能对数据包进行排序的缺点,也就是说,当报文发送之后,是无法得知其是否安全完整到达的。
它的特点如下:
1)面向无连接
首先 UDP 是不需要和 TCP一样在发送数据前进行三次握手建立连接的,想发数据就可以开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。
具体来说就是:
2)有单播,多播,广播的功能
UDP 不止支持一对一的传输方式,同样支持一对多,多对多,多对一的方式,也就是说 UDP 提供了单播,多播,广播的功能。
3)面向报文
发送方的UDP对应用程序交下来的报文,在添加首部后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因此,应用程序必须选择合适大小的报文
4)不可靠性
首先不可靠性体现在无连接上,通信都不需要建立连接,想发就发,这样的情况肯定不可靠。
并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关心对方是否已经正确接收到数据了。
再者网络环境时好时坏,但是 UDP 因为没有拥塞控制,一直会以恒定的速度发送数据。即使网络条件不好,也不会对发送速率进行调整。这样实现的弊端就是在网络条件不好的情况下可能会导致丢包,但是优点也很明显,在某些实时性要求高的场景(比如电话会议)就需要使用 UDP 而不是 TCP。
5)头部开销小,传输数据报文时是很高效的。
UDP 头部包含了以下几个数据:
因此 UDP 的头部开销小,只有8字节,相比 TCP 的至少20字节要少得多,在传输数据报文时是很高效的。
(2)TCP TCP的全称是传输控制协议是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 是面向连接的、可靠的流协议(流就是指不间断的数据结构)。
它有以下几个特点:
1)面向连接
面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。
2)仅支持单播传输
每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。
3)面向字节流
TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。
4)可靠传输
对于可靠传输,判断丢包、误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
5)提供拥塞控制
当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞。
6)提供全双工通信
TCP允许通信双方的应用程序在任何时候都能发送数据,因为TCP连接的两端都设有缓存,用来临时存放双向通信的数据。当然,TCP可以立即发送一个数据段,也可以缓存一段时间以便一次发送更多的数据段(最大的数据段大小取决于MSS)
不同情况的调用,
this
指向分别如何。顺带可以提一下es6
中箭头函数没有this
,arguments
,super
等,这些只依赖包含箭头函数最接近的函数我们先来看几个函数调用的场景
function foo() {
console.log(this.a)
}
var a = 1
foo()
const obj = {
a: 2,
foo: foo
}
obj.foo()
const c = new foo()
foo
来说,不管 foo
函数被放在了什么地方,this
一定是window
obj.foo()
来说,我们只需要记住,谁调用了函数,谁就是 this
,所以在这个场景下 foo
函数中的 this
就是 obj
对象new
的方式来说,this
被永远绑定在了 c
上面,不会被任何方式改变 this
说完了以上几种情况,其实很多代码中的
this
应该就没什么问题了,下面让我们看看箭头函数中的this
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
this
的,箭头函数中的 this
只取决包裹箭头函数的第一个普通函数的 this
。在这个例子中,因为包裹箭头函数的第一个普通函数是 a
,所以此时的 this
是 window
。另外对箭头函数使用 bind
这类函数是无效的。bind
这些改变上下文的 API
了,对于这些函数来说,this
取决于第一个参数,如果第一个参数为空,那么就是 window
。bind
,不知道大家是否考虑过,如果对一个函数进行多次 bind
,那么上下文会是什么呢?let a = {}
let fn = function () { console.log(this) }
fn.bind().bind(a)() // => ?
如果你认为输出结果是
a
,那么你就错了,其实我们可以把上述代码转换成另一种形式
// fn.bind().bind(a) 等于
let fn2 = function fn1() {
return function() {
return fn.apply()
}.apply(a)
}
fn2()
可以从上述代码中发现,不管我们给函数
bind
几次,fn
中的this
永远由第一次bind
决定,所以结果永远是window
let a = { name: 'poetries' }
function foo() {
console.log(this.name)
}
foo.bind(a)() // => 'poetries'
以上就是
this
的规则了,但是可能会发生多个规则同时出现的情况,这时候不同的规则之间会根据优先级最高的来决定this
最终指向哪里。首先,
new
的方式优先级最高,接下来是bind
这些函数,然后是obj.foo()
这种调用方式,最后是foo
这种调用方式,同时,箭头函数的this
一旦被绑定,就不会再被任何方式所改变。
函数执行改变this
this
。因此要明白
this
指向,其实就是要搞清楚 函数的运行环境,说人话就是,谁调用了函数。例如
obj.fn()
,便是 obj
调用了函数,既函数中的 this === obj
fn()
,这里可以看成 window.fn()
,因此 this === window
但这种机制并不完全能满足我们的业务需求,因此提供了三种方式可以手动修改
this
的指向:
call: fn.call(target, 1, 2)
apply: fn.apply(target, [1, 2])
bind: fn.bind(target)(1,2)
1. DNS域名解析
DNS的域名查找,在客户端和浏览器,本地DNS之间的查询方式是递归查询;在本地DNS服务器与根域及其子域之间的查询方式是迭代查询;
在客户端输入 URL 后,会有一个递归查找的过程,从浏览器缓存中查找->本地的hosts文件查找->找本地DNS解析器缓存查找->本地DNS服务器查找
,这个过程中任何一步找到了都会结束查找流程。
如果本地DNS服务器无法查询到,则根据本地DNS服务器设置的转发器进行查询。若未用转发模式,则迭代查找过程如下图:
结合起来的过程,可以用一个图表示:
在查找过程中,有以下优化点:
浏览器缓存,系统缓存,路由器缓存,IPS服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存
。2. 建立TCP连接
首先,判断是不是https的,如果是,则HTTPS其实是HTTP + SSL / TLS 两部分组成,也就是在HTTP上又加了一层处理加密信息的模块。服务端和客户端的信息传输都会通过TLS进行加密,所以传输的数据都是加密后的数据
进行三次握手,建立TCP连接。
SSL握手过程
完成了之后,客户端和服务器端就可以开始传送数据
发送HTTP请求,服务器处理请求,返回响应结果
TCP连接建立后,浏览器就可以利用
HTTP/HTTPS
协议向服务器发送请求了。服务器接受到请求,就解析请求头,如果头部有缓存相关信息如if-none-match与if-modified-since
,则验证缓存是否有效,若有效则返回状态码为304
,若无效则重新返回资源,状态码为200
这里有发生的一个过程是HTTP缓存,是一个常考的考点,大致过程如图:
3. 关闭TCP连接
4. 浏览器渲染
按照渲染的时间顺序,流水线可分为如下几个子阶段:构建 DOM 树、样式计算、布局阶段、分层、栅格化和显示。如图:
styleSheets
,计算出 DOM
节点的样式。构建 DOM 树
样式计算
渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
CSS 样式来源主要有 3 种,分别是通过 link 引用的外部 CSS 文件、style标签内的 CSS、元素的 style 属性内嵌的 CSS。
页面布局
布局过程,即
排除 script、meta 等功能化、非视觉节点
,排除display: none
的节点,计算元素的位置信息,确定元素的位置,构建一棵只包含可见元素布局树。如图:
其中,这个过程需要注意的是回流和重绘
生成分层树
页面中有很多复杂的效果,如一些复杂的 3D 变换、页面滚动,或者使用 z-indexing 做 z 轴排序等,为了更加方便地实现这些效果,渲染引擎还需要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)
栅格化
合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图
通常一个页面可能很大,但是用户只能看到其中的一部分,我们把用户可以看到的这个部分叫做视口(viewport)。在有些情况下,有的图层可以很大,比如有的页面你使用滚动条要滚动好久才能滚动到底部,但是通过视口,用户只能看到页面的很小一部分,所以在这种情况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。
显示
最后,合成线程发送绘制图块命令给浏览器进程。浏览器进程根据指令生成页面,并显示到显示器上,渲染过程完成。
v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。
这个算法分为三步:
新生代对象晋升到老生代有两个条件:
老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎片,不便于后面的内存分配。所以了解决内存碎片的问题引入了标记压缩法。
由于在进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响。 为了解决这个问题 V8 引入了增量标记的方法,将一次停顿进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行
HTTP
协议传输;User Agent
;Apache
和 Nginx
;CDN
位于浏览器和服务器之间,主要起到缓存加速的作用;User Agent
,是自动访问网络资源的程序。TCP/IP
是网络世界最常用的协议,HTTP
通常运行在 TCP/IP
提供的可靠传输基础上DNS
域名是 IP
地址的等价替代,需要用域名解析实现到 IP
地址的映射;URI
是用来标记互联网上资源的一个名字,由“协议名 + 主机名 + 路径”构成,俗称 URL;HTTPS
相当于“HTTP+SSL/TLS+TCP/IP
”,为 HTTP
套了一个安全的外壳;HTTP
传输过程中的“中转站”,可以实现缓存加速、负载均衡等功能mode
hash
history
跳转
this.$router.push()
<router-link to=""></router-link>
占位
<router-view></router-view>
vue-router源码实现
VueRouter
类和install
方法router-view
用于显示匹配组件内容,router-link
用于跳转url
变化:监听hashchange
或popstate
事件url
:创建一个响应式的属性current
,当它改变时获取对应组件并显示// 我们的插件:
// 1.实现一个Router类并挂载期实例
// 2.实现两个全局组件router-link和router-view
let Vue;
class VueRouter {
// 核心任务:
// 1.监听url变化
constructor(options) {
this.$options = options;
// 缓存path和route映射关系
// 这样找组件更快
this.routeMap = {}
this.$options.routes.forEach(route => {
this.routeMap[route.path] = route
})
// 数据响应式
// 定义一个响应式的current,则如果他变了,那么使用它的组件会rerender
Vue.util.defineReactive(this, 'current', '')
// 请确保onHashChange中this指向当前实例
window.addEventListener('hashchange', this.onHashChange.bind(this))
window.addEventListener('load', this.onHashChange.bind(this))
}
onHashChange() {
// console.log(window.location.hash);
this.current = window.location.hash.slice(1) || '/'
}
}
// 插件需要实现install方法
// 接收一个参数,Vue构造函数,主要用于数据响应式
VueRouter.install = function (_Vue) {
// 保存Vue构造函数在VueRouter中使用
Vue = _Vue
// 任务1:使用混入来做router挂载这件事情
Vue.mixin({
beforeCreate() {
// 只有根实例才有router选项
if (this.$options.router) {
Vue.prototype.$router = this.$options.router
}
}
})
// 任务2:实现两个全局组件
// router-link: 生成一个a标签,在url后面添 // <router-link to="/about">aaa</router-link>
Vue.component('router-link', {
props: {
to: {
type: String,
required: true
},
},
render(h) {
// h(tag, props, children)
return h('a',
{ attrs: { href: '#' + this.to } },
this.$slots.default
)
// 使用jsx
// return <a href={'#'+this.to}>{this.$slots.default}</a>
}
})
Vue.component('router-view', {
render(h) {
// 根据current获取组件并render
// current怎么获取?
// console.log('render',this.$router.current);
// 获取要渲染的组件
let component = null
const { routeMap, current } = this.$router
if (routeMap[current]) {
component = routeMap[current].component
}
return h(component)
}
})
}
export default VueRouter
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。