关于前端:前端经典面试题有答案

39次阅读

共计 29693 个字符,预计需要花费 75 分钟才能阅读完成。

作用域

  • 作用域:作用域是定义变量的区域,它有一套拜访变量的规定,这套规定来治理浏览器引擎如何在以后作用域以及嵌套的作用域中依据变量(标识符)进行变量查找
  • 作用域链:作用域链的作用是保障对执行环境有权拜访的所有变量和函数的有序拜访,通过作用域链,咱们能够拜访到外层环境的变量和 函数。

作用域链的实质上是一个指向变量对象的指针列表。变量对象是一个蕴含了执行环境中所有变量和函数的对象。作用域链的前 端始终都是以后执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最初一个对象。

  • 当咱们查找一个变量时,如果以后执行环境中没有找到,咱们能够沿着作用域链向后查找
  • 作用域链的创立过程跟执行上下文的建设无关 ….

作用域能够了解为变量的可拜访性,总共分为三种类型,别离为:

  • 全局作用域
  • 函数作用域
  • 块级作用域,ES6 中的 letconst 就能够产生该作用域

其实看完后面的闭包、this 这部分外部的话,应该根本能理解作用域的一些利用。

一旦咱们将这些作用域嵌套起来,就变成了另外一个重要的知识点「作用域链」,也就是 JS 到底是如何拜访须要的变量或者函数的。

  • 首先作用域链是在定义时就被确定下来的,和箭头函数里的 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
  • 从这段代码中咱们能够看到,globalName 这个变量无论在什么中央都是能够被拜访到的,所以它就是全局变量。而在 getName 函数中作为局部变量的 name 变量是不具备这种能力的
  • 当然全局作用域有相应的毛病,咱们定义很多全局变量的时候,会容易引起变量命名的抵触,所以在定义变量的时候应该留神作用域的问题。

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 并没有定义

什么是物理像素,逻辑像素和像素密度,为什么在挪动端开发时须要用到 @3x, @2x 这种图片?

以 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); }
}

Canvas 和 SVG 的区别

(1)SVG: SVG 可缩放矢量图形(Scalable Vector Graphics)是基于可扩大标记语言 XML 形容的 2D 图形的语言,SVG 基于 XML 就意味着 SVG DOM 中的每个元素都是可用的,能够为某个元素附加 Javascript 事件处理器。在 SVG 中,每个被绘制的图形均被视为对象。如果 SVG 对象的属性发生变化,那么浏览器可能主动重现图形。

其特点如下:

  • 不依赖分辨率
  • 反对事件处理器
  • 最适宜带有大型渲染区域的应用程序(比方谷歌地图)
  • 复杂度高会减慢渲染速度(任何适度应用 DOM 的利用都不快)
  • 不适宜游戏利用

(2)Canvas: Canvas 是画布,通过 Javascript 来绘制 2D 图形,是逐像素进行渲染的。其地位产生扭转,就会从新进行绘制。

其特点如下:

  • 依赖分辨率
  • 不反对事件处理器
  • 弱的文本渲染能力
  • 可能以 .png 或 .jpg 格局保留后果图像
  • 最适宜图像密集型的游戏,其中的许多对象会被频繁重绘

注:矢量图,也称为面向对象的图像或绘图图像,在数学上定义为一系列由线连贯的点。矢量文件中的图形元素称为对象。每个对象都是一个自成一体的实体,它具备色彩、形态、轮廓、大小和屏幕地位等属性。

对 this 对象的了解

this 是执行上下文中的一个属性,它指向最初一次调用这个办法的对象。在理论开发中,this 的指向能够通过四种调用模式来判断。

  • 第一种是 函数调用模式,当一个函数不是一个对象的属性时,间接作为函数来调用时,this 指向全局对象。
  • 第二种是 办法调用模式,如果一个函数作为一个对象的办法来调用时,this 指向这个对象。
  • 第三种是 结构器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
  • 第四种是 apply、call 和 bind 调用模式,这三个办法都能够显示的指定调用函数的 this 指向。其中 apply 办法接管两个参数:一个是 this 绑定的对象,一个是参数数组。call 办法接管的参数,第一个是 this 绑定的对象,前面的其余参数是传入函数执行的参数。也就是说,在应用 call() 办法时,传递给函数的参数必须一一列举进去。bind 办法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了应用 new 时会被扭转,其余状况下都不会扭转。

这四种形式,应用结构器调用模式的优先级最高,而后是 apply、call 和 bind 调用模式,而后是办法调用模式,而后是函数调用模式。

CSS 预处理器 / 后处理器是什么?为什么要应用它们?

预处理器, 如:lesssassstylus,用来预编译 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,左边抛弃

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,只有与一个各位都为零的数值相与,后果为零。

2. 按位或运算符(|)

定义: 加入运算的两个对象按二进制位进行“或”运算。

运算规定:

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。
留神:正数按补码模式加入按位或运算。

3. 异或运算符(^)

定义: 加入运算的两个数据按二进制位进行“异或”运算。

运算规定:

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^x=0,x^0=x
  • 自反性: a^b^b=a^0=a;

4. 取反运算符 (~)

定义: 加入运算的一个数据按二进制进行“取反”运算。

运算规定:

~ 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。

5. 左移运算符(<<)

定义: 将一个运算对象的各二进制位全副左移若干位,右边的二进制位抛弃,左边补 0。
设 a=1010 1110,a = a<< 2 将 a 的二进制位左移 2 位、右补 0,即得 a =1011 1000。
若左移时舍弃的高位不蕴含 1,则每左移一位,相当于该数乘以 2。

6. 右移运算符(>>)

定义: 将一个数的各二进制位全副右移若干位,负数左补 0,正数左补 1,左边抛弃。
例如:a=a>>2 将 a 的二进制位右移 2 位,左补 0 或者 左补 1 得看被移数是正还是负。
操作数每右移一位,相当于该数除以 2。

7. 原码、补码、反码

下面提到了补码、反码等常识,这里就补充一下。
计算机中的 有符号数 有三种示意办法,即原码、反码和补码。三种示意办法均有符号位和数值位两局部,符号位都是用 0 示意“正”,用 1 示意“负”,而数值位,三种示意办法各不相同。

(1)原码

原码就是一个数的二进制数。例如:10 的原码为 0000 1010

(2)反码

  • 负数的反码与原码雷同,如:10 反码为 0000 1010
  • 正数的反码为除符号位,按位取反,即 0 变 1,1 变 0。

例如:-10

原码:1000 1010
反码:1111 0101

(3)补码

  • 负数的补码与原码雷同,如:10 补码为 0000 1010
  • 正数的补码是原码除符号位外的所有位取反即 0 变 1,1 变 0,而后加 1,也就是反码加 1。

例如:-10

原码:1000 1010
反码:1111 0101
补码:1111 0110

参考 前端进阶面试题具体解答

JavaScript 有哪些内置对象

全局的对象(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 对象。

viewport

<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依照设计稿规范走,外加利用transfromescale(0.5) 放大一倍即可;

全局解决

  • mate标签中的 viewport属性,initial-scale 设置为 0.5
  • rem 依照设计稿规范走即可

connect 组件原理剖析

1. connect 用法

作用:连贯 React 组件与 Redux store

connect([mapStateToProps], [mapDispatchToProps], [mergeProps],[options])
// 这个函数容许咱们将 store 中的数据作为 props 绑定到组件上
const mapStateToProps = (state) => {
  return {count: state.count}
}
  • 这个函数的第一个参数就是 Reduxstore,咱们从中摘取了 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 的子组件
  • 接管 Reduxstore作为 props,通过context 对象传递给子孙组件上的connect

connect 做了些什么

它真正连贯 ReduxReact,它包在咱们的容器组件的外一层,它接管下面 Provider提供的 store 外面的 statedispatch,传给一个构造函数,返回一个对象,以属性模式传给咱们的容器组件

3. 源码

connect是一个高阶函数,首先传入 mapStateToPropsmapDispatchToProps,而后返回一个生产Component 的函数 (wrapWithConnect),而后再将真正的Component 作为参数传入 wrapWithConnect,这样就生产出一个通过包裹的Connect 组件,该组件具备如下特点

  • 通过 props.store 获取先人 Componentstore props包含 statePropsdispatchPropsparentProps, 合并在一起失去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()做判断
Object.prototype.toString.call(obj).slice(8,-1) === 'Array';
  • 通过原型链做判断
obj.__proto__ === Array.prototype;
  • 通过 ES6 的 Array.isArray()做判断
Array.isArrray(obj);
  • 通过 instanceof 做判断
obj instanceof Array
  • 通过 Array.prototype.isPrototypeOf
Array.prototype.isPrototypeOf(obj)

文档申明(Doctype)和 <!Doctype html> 有何作用? 严格模式与混淆模式如何辨别?它们有何意义?

文档申明的作用: 文档申明是为了通知浏览器,以后 HTML 文档应用什么版本的 HTML 来写的,这样浏览器能力依照申明的版本来正确的解析。

的作用:<!doctype html> 的作用就是让浏览器进入规范模式,应用最新的 HTML5 规范来解析渲染页面;如果不写,浏览器就会进入混淆模式,咱们须要防止此类情况产生。

严格模式与混淆模式的辨别:

  • 严格模式 :又称为规范模式,指浏览器依照W3C 规范解析代码;
  • 混淆模式:又称怪异模式、兼容模式,是指浏览器用本人的形式解析代码。混淆模式通常模仿老式浏览器的行为,以避免老站点无奈工作;

辨别 :网页中的DTD,间接影响到应用的是严格模式还是浏览模式,能够说DTD 的应用与这两种形式的区别非亲非故。

  • 如果文档蕴含严格的 DOCTYPE,那么它个别以严格模式出现( 严格 DTD ——严格模式);
  • 蕴含过渡 DTDURIDOCTYPE,也以严格模式出现,但有过渡 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 代码执行过程中会有很多工作,这些工作总的分成两类:

  • 同步工作
  • 异步工作

当咱们关上网站时,网页的渲染过程就是一大堆同步工作,比方页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的工作,就是异步工作。,咱们用导图来阐明:

咱们解释一下这张图:

  • 同步和异步工作别离进入不同的执行 ” 场合 ”,同步的进入主线程,异步的进入 Event Table 并注册函数。
  • 当指定的事件实现时,Event Table 会将这个函数移入 Event Queue。
  • 主线程内的工作执行结束为空,会去 Event Queue 读取对应的函数,进入主线程执行。
  • 上述过程会一直反复,也就是常说的 Event Loop(事件循环)。

那主线程执行栈何时为空呢?js 引擎存在 monitoring process 过程,会继续一直的查看主线程执行栈是否为空,一旦为空,就会去 Event Queue 那里查看是否有期待被调用的函数

以上就是 js 运行的整体流程

面试中该如何答复呢?上面是我集体举荐的答复:

  • 首先 js 是单线程运行的,在代码执行的时候,通过将不同函数的执行上下文压入执行栈中来保障代码的有序执行
  • 在执行同步代码的时候,如果遇到了异步事件,js 引擎并不会始终期待其返回后果,而是会将这个事件挂起,继续执行执行栈中的其余工作
  • 当同步事件执行结束后,再将异步事件对应的回调退出到与以后执行栈中不同的另一个工作队列中期待执行
  • 工作队列能够分为宏工作对列和微工作对列,当以后执行栈中的事件执行结束后,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,第一轮执行完结。
  • 第二轮:从宏工作队列开始,发现 setTimeout 回调,输入 1 执行结束,因而后果是 25431

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 称为 jobsmacrotask 称为 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 程序是这样的

  • 执行同步代码,这属于宏工作
  • 执行栈为空,查问是否有微工作须要执行
  • 执行所有微工作
  • 必要的话渲染 UI
  • 而后开始下一轮 Event loop,执行宏工作中的异步代码

通过上述的 Event loop 程序可知,如果宏工作中的异步代码有大量的计算并且须要操作 DOM 的话,为了更快的响应界面响应,咱们能够把操作 DOM 放入微工作中

  • JavaScript 引擎首先从宏工作队列(macrotask queue)中取出第一个工作
  • 执行结束后,再将微工作(microtask queue)中的所有工作取出,依照程序别离全副执行(这里包含不仅指开始执行时队列里的微工作),如果在这一步过程中产生新的微工作,也须要执行;
  • 而后再从宏工作队列中取下一个,执行结束后,再次将 microtask queue 中的全副取出,周而复始,直到两个 queue 中的工作都取完。

总结起来就是:一次 Eventloop 循环会解决一个宏工作和所有这次循环中产生的微工作

2. Node 中的 Event loop

当 Node.js 开始启动时,会初始化一个 Eventloop,解决输出的代码脚本,这些脚本会进行 API 异步调用,process.nextTick() 办法会开始处理事件循环。上面就是 Node.js 官网提供的 Eventloop 事件循环参考流程

  • Node 中的 Event loop 和浏览器中的不雷同。
  • NodeEvent loop 分为 6 个阶段,它们会依照程序重复运行
  • 每次执行执行一个宏工作后会清空微工作(执行程序和浏览器统一,在 node11 版本以上)
  • process.nextTick node 中的微工作,以后执行栈的底部,优先级比 promise 要高

整个流程分为六个阶段,当这六个阶段执行完一次之后,才能够算得上执行了一次 Eventloop 的循环过程。咱们来别离看下这六个阶段都做了哪些事件。

  • Timers 阶段 :这个阶段执行 setTimeoutsetInterval 的回调函数,简略了解就是由这两个函数启动的回调函数。
  • I/O callbacks 阶段:这个阶段次要执行零碎级别的回调函数,比方 TCP 连贯失败的回调。
  • idle,prepare 阶段:仅零碎外部应用,你只须要晓得有这 2 个阶段就能够。
  • poll 阶段 poll 阶段是一个重要且简单的阶段,简直所有 I/O 相干的回调,都在这个阶段执行(除了setTimeoutsetIntervalsetImmediate 以及一些因为 exception 意外敞开产生的回调)。 检索新的 I/O 事件,执行与 I/O 相干的回调,其余状况 Node.js 将在适当的时候在此阻塞。这也是最简单的一个阶段,所有的事件循环以及回调解决都在这个阶段执行。这个阶段的次要流程如下图所示。
  • check 阶段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

  • close callbacks 阶段:执行一些敞开的回调函数,如 socket.on('close', ...)

除了把 Eventloop 的宏工作细分到不同阶段外。node 还引入了一个新的工作队列 Process.nextTick()

能够认为,Process.nextTick() 会在上述各个阶段完结时,在 进入下一个阶段之前立刻执行(优先级甚至超过 microtask 队列)

事件循环的次要蕴含微工作和宏工作。具体是怎么进行循环的呢

  • 微工作 :在 Node.js 中微工作蕴含 2 种——process.nextTickPromise 微工作在事件循环中优先级是最高的 ,因而在同一个事件循环中有其余工作存在时,优先执行微工作队列。并且process.nextTick 和 Promise 也存在优先级,process.nextTick 高于 Promise
  • 宏工作:在 Node.js 中宏工作蕴含 4 种——setTimeoutsetIntervalsetImmediateI/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');

剖析下下面代码的执行过程

  • 第一个事件循环主线程发动,因而先执行同步代码,所以先输入 start,而后输入 end
  • 第一个事件循环主线程发动,因而先执行同步代码,所以先输入 start,而后输入 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

  • 整个过程优先执行主线程的第一个事件循环过程,所以先执行同步逻辑,先输入 2。
  • 接下来执行微工作,输入 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 的原生办法,而是浏览器宿主环境提供的办法,因为它们牵扯到另一个问题:渲染。
  • 咱们晓得浏览器作为一个简单的利用是多线程工作的,除了运行 JS 的线程外,还有渲染线程、定时器触发线程、HTTP 申请线程,等等。JS 线程能够读取并且批改 DOM,而渲染线程也须要读取 DOM,这是一个典型的多线程竞争临界资源的问题。所以浏览器就把这两个线程设计成互斥的,即同时只能有一个线程在执行
  • 渲染本来就不应该呈现在 Eventloop 相干的常识体系里,然而因为 Eventloop 显然是在探讨 JS 如何运行的问题,而渲染则是浏览器另外一个线程的工作。然而 requestAnimationFrame的呈现却把这两件事件给关联起来
  • 通过调用 requestAnimationFrame 咱们能够在下次渲染之前执行回调函数。那下次渲染具体是哪个工夫点呢?渲染和 Eventloop 有什么关系呢?

    • 简略来说,就是在每一次 Eventloop 的开端,判断以后页面是否处于渲染机会,就是从新渲染
  • 有屏幕的硬件限度,比方 60Hz 刷新率,简而言之就是 1 秒刷新了 60 次,16.6ms 刷新一次。这个时候浏览器的渲染间隔时间就没必要小于 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 服务器中以资源记录的模式存储信息,每一个 DNS 响应报文个别蕴含多条资源记录。一条资源记录的具体的格局为

(Name,Value,Type,TTL)

其中 TTL 是资源记录的生存工夫,它定义了资源记录可能被其余的 DNS 服务器缓存多长时间。

罕用的一共有四种 Type 的值,别离是 A、NS、CNAME 和 MX,不同 Type 的值,对应资源记录代表的意义不同:

  • 如果 Type = A,则 Name 是主机名,Value 是主机名对应的 IP 地址。因而一条记录为 A 的资源记录,提供了标 准的主机名到 IP 地址的映射。
  • 如果 Type = NS,则 Name 是个域名,Value 是负责该域名的 DNS 服务器的主机名。这个记录次要用于 DNS 链式 查问时,返回下一级须要查问的 DNS 服务器的信息。
  • 如果 Type = CNAME,则 Name 为别名,Value 为该主机的标准主机名。该条记录用于向查问的主机返回一个主机名 对应的标准主机名,从而通知查问主机去查问这个主机名的 IP 地址。主机别名次要是为了通过给一些简单的主机名提供 一个便于记忆的简略的别名。
  • 如果 Type = MX,则 Name 为一个邮件服务器的别名,Value 为邮件服务器的标准主机名。它的作用和 CNAME 是一 样的,都是为了解决标准主机名不利于记忆的毛病。

TCP 和 UDP 的概念及特点

TCP 和 UDP 都是传输层协定,他们都属于 TCP/IP 协定族:

(1)UDP

UDP 的全称是 用户数据报协定,在网络中它与 TCP 协定一样用于解决数据包,是一种无连贯的协定。在 OSI 模型中,在传输层,处于 IP 协定的上一层。UDP 有不提供数据包分组、组装和不能对数据包进行排序的毛病,也就是说,当报文发送之后,是无奈得悉其是否平安残缺达到的。

它的特点如下:

1)面向无连贯

首先 UDP 是不须要和 TCP 一样在发送数据前进行三次握手建设连贯的,想发数据就能够开始发送了。并且也只是数据报文的搬运工,不会对数据报文进行任何拆分和拼接操作。

具体来说就是:

  • 在发送端,应用层将数据传递给传输层的 UDP 协定,UDP 只会给数据减少一个 UDP 头标识下是 UDP 协定,而后就传递给网络层了
  • 在接收端,网络层将数据传递给传输层,UDP 只去除 IP 报文头就传递给应用层,不会任何拼接操作

2)有单播,多播,播送的性能

UDP 不止反对一对一的传输方式,同样反对一对多,多对多,多对一的形式,也就是说 UDP 提供了单播,多播,播送的性能。

3)面向报文

发送方的 UDP 对应用程序交下来的报文,在增加首部后就向下交付 IP 层。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。因而,应用程序必须抉择适合大小的报文

4)不可靠性

首先不可靠性体现在无连贯上,通信都不须要建设连贯,想发就发,这样的状况必定不牢靠。

并且收到什么数据就传递什么数据,并且也不会备份数据,发送数据也不会关怀对方是否曾经正确接管到数据了。

再者网络环境时好时坏,然而 UDP 因为没有拥塞管制,始终会以恒定的速度发送数据。即便网络条件不好,也不会对发送速率进行调整。这样实现的弊病就是在网络条件不好的状况下可能会导致丢包,然而长处也很显著,在某些实时性要求高的场景(比方电话会议)就须要应用 UDP 而不是 TCP。

5)头部开销小,传输数据报文时是很高效的。

UDP 头部蕴含了以下几个数据:

  • 两个十六位的端口号,别离为源端口(可选字段)和指标端口
  • 整个数据报文的长度
  • 整个数据报文的测验和(IPv4 可选字段),该字段用于发现头部信息和数据中的谬误

因而 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

不同状况的调用,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,所以此时的 thiswindow。另外对箭头函数应用 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

  • 因为 JS 的设计原理: 在函数中,能够援用运行环境中的变量。因而就须要一个机制来让咱们能够在函数体外部获取以后的运行环境,这便是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)

从输出 URL 到页面展现过程

1. DNS 域名解析

  • 根 DNS 服务器:返回顶级域 DNS 服务器的 IP 地址
  • 顶级域 DNS 服务器:返回权威 DNS 服务器的 IP 地址
  • 权威 DNS 服务器:返回相应主机的 IP 地址

DNS 的域名查找,在客户端和浏览器,本地 DNS 之间的查问形式是递归查问;在本地 DNS 服务器与根域及其子域之间的查问形式是迭代查问;

在客户端输出 URL 后,会有一个递归查找的过程,从 浏览器缓存中查找 -> 本地的 hosts 文件查找 -> 找本地 DNS 解析器缓存查找 -> 本地 DNS 服务器查找,这个过程中任何一步找到了都会完结查找流程。

如果本地 DNS 服务器无奈查问到,则依据本地 DNS 服务器设置的转发器进行查问。若未用转发模式,则迭代查找过程如下图:

联合起来的过程,能够用一个图示意:

在查找过程中,有以下优化点:

  • DNS 存在着多级缓存,从离浏览器的间隔排序的话,有以下几种: 浏览器缓存,零碎缓存,路由器缓存,IPS 服务器缓存,根域名服务器缓存,顶级域名服务器缓存,主域名服务器缓存
  • 在域名和 IP 的映射过程中,给了利用基于域名做负载平衡的机会,能够是简略的负载平衡,也能够依据地址和运营商做全局的负载平衡。

2. 建设 TCP 连贯

首先,判断是不是 https 的,如果是,则 HTTPS 其实是 HTTP + SSL / TLS 两局部组成,也就是在 HTTP 上又加了一层解决加密信息的模块。服务端和客户端的信息传输都会通过 TLS 进行加密,所以传输的数据都是加密后的数据

进行三次握手,建设 TCP 连贯。

  • 第一次握手:建设连贯。客户端发送连贯申请报文段
  • 第二次握手:服务器收到 SYN 报文段。服务器收到客户端的 SYN 报文段,须要对这个 SYN 报文段进行确认
  • 第三次握手:客户端收到服务器的 SYN+ACK 报文段,向服务器发送 ACK 报文段

SSL 握手过程

  • 第一阶段 建设平安能力 包含协定版本 会话 Id 明码构件 压缩办法和初始随机数
  • 第二阶段 服务器发送证书 密钥替换数据和证书申请,最初发送申请 - 相应阶段的完结信号
  • 第三阶段 如果有证书申请客户端发送此证书 之后客户端发送密钥替换数据 也能够发送证书验证音讯
  • 第四阶段 变更明码构件和完结握手协定

实现了之后,客户端和服务器端就能够开始传送数据

发送 HTTP 申请,服务器解决申请,返回响应后果

TCP 连贯建设后,浏览器就能够利用 HTTP/HTTPS 协定向服务器发送申请了。服务器承受到申请,就解析申请头,如果头部有缓存相干信息如if-none-match 与 if-modified-since,则验证缓存是否无效,若无效则返回状态码为304,若有效则从新返回资源,状态码为200

这里有产生的一个过程是 HTTP 缓存,是一个常考的考点,大抵过程如图:

3. 敞开 TCP 连贯

4. 浏览器渲染

依照渲染的工夫程序,流水线可分为如下几个子阶段:构建 DOM 树、款式计算、布局阶段、分层、栅格化和显示。如图:

  • 渲染过程将 HTML 内容转换为可能读懂 DOM 树结构。
  • 渲染引擎将 CSS 样式表转化为浏览器能够了解的 styleSheets,计算出 DOM 节点的款式。
  • 创立布局树,并计算元素的布局信息。
  • 对布局树进行分层,并生成分层树。
  • 为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分图块,并栅格化将图块转换成位图。
  • 合成线程发送绘制图块命令给浏览器过程。浏览器过程依据指令生成页面,并显示到显示器上。

构建 DOM 树

  • 转码(Bytes -> Characters)—— 读取接管到的 HTML 二进制数据,按指定编码格局将字节转换为 HTML 字符串
  • Tokens 化(Characters -> Tokens)—— 解析 HTML,将 HTML 字符串转换为构造清晰的 Tokens,每个 Token 都有非凡的含意同时有本人的一套规定
  • 构建 Nodes(Tokens -> Nodes)—— 每个 Node 都增加特定的属性(或属性拜访器),通过指针可能确定 Node 的父、子、兄弟关系和所属 treeScope(例如:iframe 的 treeScope 与外层页面的 treeScope 不同)
  • 构建 DOM 树(Nodes -> DOM Tree)—— 最重要的工作是建设起每个结点的父子兄弟关系

款式计算

渲染引擎将 CSS 样式表转化为浏览器能够了解的 styleSheets,计算出 DOM 节点的款式。

CSS 款式起源次要有 3 种,别离是通过 link 援用的内部 CSS 文件、style 标签内的 CSS、元素的 style 属性内嵌的 CSS。

页面布局

布局过程,即 排除 script、meta 等功能化、非视觉节点,排除 display: none 的节点,计算元素的地位信息,确定元素的地位,构建一棵只蕴含可见元素布局树。如图:

其中,这个过程须要留神的是回流和重绘

生成分层树

页面中有很多简单的成果,如一些简单的 3D 变换、页面滚动,或者应用 z-indexing 做 z 轴排序等,为了更加不便地实现这些成果,渲染引擎还须要为特定的节点生成专用的图层,并生成一棵对应的图层树(LayerTree)

栅格化

合成线程会依照视口左近的图块来优先生成位图,理论生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图

通常一个页面可能很大,然而用户只能看到其中的一部分,咱们把用户能够看到的这个局部叫做视口(viewport)。在有些状况下,有的图层能够很大,比方有的页面你应用滚动条要滚动良久能力滚动到底部,然而通过视口,用户只能看到页面的很小一部分,所以在这种状况下,要绘制出所有图层内容的话,就会产生太大的开销,而且也没有必要。

显示

最初,合成线程发送绘制图块命令给浏览器过程。浏览器过程依据指令生成页面,并显示到显示器上,渲染过程实现。

垃圾回收

  • 对于在 JavaScript 中的字符串,对象,数组是没有固定大小的,只有当对他们进行动态分配存储时,解释器就会分配内存来存储这些数据,当 JavaScript 的解释器耗费完零碎中所有可用的内存时,就会造成零碎解体。
  • 内存透露,在某些状况下,不再应用到的变量所占用内存没有及时开释,导致程序运行中,内存越占越大,极其状况下能够导致系统解体,服务器宕机。
  • JavaScript 有本人的一套垃圾回收机制,JavaScript 的解释器能够检测到什么时候程序不再应用这个对象了(数据),就会把它所占用的内存开释掉。
  • 针对 JavaScript 的来及回收机制有以下两种办法(罕用):标记革除,援用计数
  • 标记革除

v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。

  • 新创建的对象或者只经验过一次的垃圾回收的对象被称为新生代。经验过屡次垃圾回收的对象被称为老生代。
  • 新生代被分为 From 和 To 两个空间,To 个别是闲置的。当 From 空间满了的时候会执行 Scavenge 算法进行垃圾回收。当咱们执行垃圾回收算法的时候应用逻辑将会进行,等垃圾回收完结后再继续执行。

这个算法分为三步:

  • 首先查看 From 空间的存活对象,如果对象存活则判断对象是否满足降职到老生代的条件,如果满足条件则降职到老生代。如果不满足条件则挪动 To 空间。
  • 如果对象不存活,则开释对象的空间。
  • 最初将 From 空间和 To 空间角色进行替换。

新生代对象降职到老生代有两个条件:

  • 第一个是判断是对象否曾经通过一次 Scavenge 回收。若经验过,则将对象从 From 空间复制到老生代中;若没有经验,则复制到 To 空间。
  • 第二个是 To 空间的内存应用占比是否超过限度。当对象从 From 空间复制到 To 空间时,若 To 空间应用超过 25%,则对象间接降职到老生代中。设置 25% 的起因次要是因为算法完结后,两个空间完结后会替换地位,如果 To 空间的内存太小,会影响后续的内存调配。

老生代采纳了标记革除法和标记压缩法。标记革除法首先会对内存中存活的对象进行标记,标记完结后革除掉那些没有标记的对象。因为标记革除后会造成很多的内存碎片,不便于前面的内存调配。所以了解决内存碎片的问题引入了标记压缩法。

因为在进行垃圾回收的时候会暂停利用的逻辑,对于新生代办法因为内存小,每次进展的工夫不会太长,但对于老生代来说每次垃圾回收的工夫长,进展会造成很大的影响。为了解决这个问题 V8 引入了增量标记的办法,将一次进展进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行

HTTP 世界全览

  • 互联网上绝大部分资源都应用 HTTP 协定传输;
  • 浏览器是 HTTP 协定里的申请方,即 User Agent
  • 服务器是 HTTP 协定里的应答方,罕用的有 ApacheNginx
  • CDN 位于浏览器和服务器之间,次要起到缓存减速的作用;
  • 爬虫是另一类 User Agent,是主动拜访网络资源的程序。
  • TCP/IP 是网络世界最罕用的协定,HTTP 通常运行在 TCP/IP 提供的牢靠传输根底上
  • DNS 域名是 IP 地址的等价代替,须要用域名解析实现到 IP 地址的映射;
  • URI 是用来标记互联网上资源的一个名字,由“协定名 + 主机名 + 门路”形成,俗称 URL;
  • HTTPS 相当于“HTTP+SSL/TLS+TCP/IP”,为 HTTP 套了一个平安的外壳;
  • 代理是 HTTP 传输过程中的“中转站”,能够实现缓存减速、负载平衡等性能

vue-router

mode

  • hash
  • history

跳转

  • this.$router.push()
  • <router-link to=""></router-link>

占位

<router-view></router-view>

vue-router 源码实现

  • 作为一个插件存在: 实现 VueRouter 类和 install 办法
  • 实现两个全局组件:router-view用于显示匹配组件内容,router-link用于跳转
  • 监控 url 变动: 监听 hashchangepopstate事件
  • 响应最新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

正文完
 0