2万5千字大厂面经 | 掘金技术征文

如下面试题来自腾讯、阿里、网易、饿了么、美团、拼多多、百度等等大厂综合起来常考的题目。javascript

如何写一个漂亮的简历

简历不是一份记流水帐的东西,而是让用人方了解你的亮点的。css

平时有在作一些修改简历的收费服务,也算看过蛮多简历了。不少简历都有以下特征html

  • 喜欢说本身的特长、优势,用人方真的不关注你的性格是否阳光等等
  • 我的技能可以占半页的篇幅,并且长得也都差很少
  • 项目经验流水帐,好比我会用这个 API 实现了某某功能
  • 简历页数过多,真心看不下去

以上相似简历能够说用人方也看了无数份,彻底抓不到你的亮点。除非你呆过大厂或者教育背景不错或者技术栈符合人家要求了,不然基本就是看运气约面试了。前端

如下是我常常给别人修改简历的意见:vue

简历页数控制在 2 页如下java

  • 技术名词注意大小写
  • 突出我的亮点,扩充内容。好比在项目中如何找到 Bug,解决 Bug 的过程;好比如何发现的性能问题,如何解决性能问题,最终提高了多少性能;好比为什么如此选型,目的是什么,较其余有什么优势等等。整体思路就是不写流水帐,突出你在项目中具备不错的解决问题的能力和独立思考的能力。
  • 斟酌熟悉、精通等字眼,不要给本身挖坑
  • 确保每个写上去的技术点本身都能说出点什么,杜绝面试官问你一个技术点,你只能答出会用 API 这种减分的状况

作到以上内容,而后在投递简历的过程当中加上一份求职信,对你的求职之路相信能帮上不少忙。node

JS 相关

谈谈变量提高?

当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。react

接下来让咱们看一个老生常谈的例子,varc++

b() // call b
console.log(a) // undefined

var a = 'Hello world'

function b() {
    console.log('call b')
}
复制代码

想必以上的输出你们确定都已经明白了,这是由于函数和变量提高的缘由。一般提高的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于你们理解。可是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是建立的阶段,JS 解释器会找出须要提高的变量和函数,而且给他们提早在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明而且赋值为 undefined,因此在第二个阶段,也就是代码执行阶段,咱们能够直接提早使用。git

在提高的过程当中,相同的函数会覆盖上一个函数,而且函数优先于变量提高

b() // call b second

function b() {
    console.log('call b fist')
}
function b() {
    console.log('call b second')
}
var b = 'Hello world'
复制代码

var 会产生不少错误,因此在 ES6中引入了 letlet 不能在声明前使用,可是这并非常说的 let 不会提高,let 提高了,在第一阶段内存也已经为他开辟好了空间,可是由于这个声明的特性致使了并不能在声明前使用。

bind、call、apply 区别

首先说下前二者的区别。

callapply 都是为了解决改变 this 的指向。做用都是相同的,只是传参的方式不一样。

除了第一个参数外,call 能够接收一个参数列表,apply 只接受一个参数数组。

let a = {
    value: 1
}
function getValue(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])
复制代码

bind 和其余两个方法做用也是一致的,只是该方法会返回一个函数。而且咱们能够经过 bind 实现柯里化。

如何实现一个 bind 函数

对于实现如下几个函数,能够从几个方面思考

  • 不传入第一个参数,那么默认为 window
  • 改变了 this 指向,让新的对象能够执行该函数。那么思路是否能够变成给新的对象添加一个函数,而后在执行完之后删除?
Function.prototype.myBind = function (context) {
  if (typeof this !== 'function') {
    throw new TypeError('Error')
  }
  var _this = this
  var args = [...arguments].slice(1)
  // 返回一个函数
  return function F() {
    // 由于返回了一个函数,咱们能够 new F(),因此须要判断
    if (this instanceof F) {
      return new _this(...args, ...arguments)
    }
    return _this.apply(context, args.concat(...arguments))
  }
}
复制代码

如何实现一个 call 函数

Function.prototype.myCall = function (context) {
  var context = context || window
  // 给 context 添加一个属性
  // getValue.call(a, 'yck', '24') => a.fn = getValue
  context.fn = this
  // 将 context 后面的参数取出来
  var args = [...arguments].slice(1)
  // getValue.call(a, 'yck', '24') => a.fn('yck', '24')
  var result = context.fn(...args)
  // 删除 fn
  delete context.fn
  return result
}
复制代码

如何实现一个 apply 函数

Function.prototype.myApply = function (context) {
  var context = context || window
  context.fn = this

  var result
  // 须要判断是否存储第二个参数
  // 若是存在,就将第二个参数展开
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn()
  }

  delete context.fn
  return result
}
复制代码

简单说下原型链?

prototype

每一个函数都有 prototype 属性,除了 Function.prototype.bind(),该属性指向原型。

每一个对象都有 __proto__ 属性,指向了建立该对象的构造函数的原型。其实这个属性指向了 [[prototype]],可是 [[prototype]] 是内部属性,咱们并不能访问到,因此使用 _proto_ 来访问。

对象能够经过 __proto__ 来寻找不属于该对象的属性,__proto__ 将对象链接起来组成了原型链。

若是你想更进一步的了解原型,能够仔细阅读 深度解析原型中的各个难点

怎么判断对象类型?

  • 能够经过 Object.prototype.toString.call(xx)。这样咱们就能够得到相似 [object Type] 的字符串。
  • instanceof 能够正确的判断对象的类型,由于内部机制是经过判断对象的原型链中是否是能找到类型的 prototype

箭头函数的特色

function a() {
    return () => {
        return () => {
        	console.log(this)
        }
    }
}
console.log(a()()())
复制代码

箭头函数实际上是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this。在这个例子中,由于调用 a 符合前面代码中的第一个状况,因此 thiswindow。而且 this 一旦绑定了上下文,就不会被任何代码改变。

This

this 是不少人会混淆的概念,可是其实他一点都不难,你只须要记住几个规则就能够了。

function foo() {
	console.log(this.a)
}
var a = 1
foo()

var obj = {
	a: 2,
	foo: foo
}
obj.foo()

// 以上二者状况 `this` 只依赖于调用函数前的对象,优先级是第二个状况大于第一个状况

// 如下状况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)

// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new
复制代码

async、await 优缺点

async 和 await 相比直接使用 Promise 来讲,优点在于处理 then 的调用链,可以更清晰准确的写出代码。缺点在于滥用 await 可能会致使性能问题,由于 await 会阻塞代码,也许以后的异步代码并不依赖于前者,但仍然须要等待前者完成,致使代码失去了并发性。

下面来看一个使用 await 的代码。

var a = 0
var b = async () => {
  a = a + await 10
  console.log('2', a) // -> '2' 10
  a = (await 10) + a
  console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1
复制代码

对于以上代码你可能会有疑惑,这里说明下原理

  • 首先函数 b 先执行,在执行到 await 10 以前变量 a 仍是 0,由于在 await 内部实现了 generatorsgenerators 会保留堆栈中东西,因此这时候 a = 0 被保存了下来
  • 由于 await 是异步操做,遇到await就会当即返回一个pending状态的Promise对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,因此会先执行 console.log('1', a)
  • 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  • 而后后面就是常规执行代码了

generator 原理

Generator 是 ES6 中新增的语法,和 Promise 同样,均可以用来异步编程

// 使用 * 表示这是一个 Generator 函数
// 内部能够经过 yield 暂停代码
// 经过调用 next 恢复执行
function* test() {
  let a = 1 + 2;
  yield 2;
  yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
复制代码

从以上代码能够发现,加上 * 的函数执行后拥有了 next 函数,也就是说函数执行后返回了一个对象。每次调用 next 函数能够继续执行被暂停的代码。如下是 Generator 函数的简单实现

// cb 也就是编译过的 test 函数
function generator(cb) {
  return (function() {
    var object = {
      next: 0,
      stop: function() {}
    };

    return {
      next: function() {
        var ret = cb(object);
        if (ret === undefined) return { value: undefined, done: true };
        return {
          value: ret,
          done: false
        };
      }
    };
  })();
}
// 若是你使用 babel 编译后能够发现 test 函数变成了这样
function test() {
  var a;
  return generator(function(_context) {
    while (1) {
      switch ((_context.prev = _context.next)) {
        // 能够发现经过 yield 将代码分割成几块
        // 每次执行 next 函数就执行一块代码
        // 而且代表下次须要执行哪块代码
        case 0:
          a = 1 + 2;
          _context.next = 4;
          return 2;
        case 4:
          _context.next = 6;
          return 3;
		// 执行完毕
        case 6:
        case "end":
          return _context.stop();
      }
    }
  });
}
复制代码

Promise

Promise 是 ES6 新增的语法,解决了回调地狱的问题。

能够把 Promise 当作一个状态机。初始是 pending 状态,能够经过函数 resolvereject ,将状态转变为 resolved 或者 rejected 状态,状态一旦改变就不能再次变化。

then 函数会返回一个 Promise 实例,而且该返回值是一个新的实例而不是以前的实例。由于 Promise 规范规定除了 pending 状态,其余状态是不能够改变的,若是返回的是一个相同实例的话,多个 then 调用就失去意义了。

对于 then 来讲,本质上能够把它当作是 flatMap

如何实现一个 Promise

// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会当即执行
function MyPromise(fn) {
  let _this = this;
  _this.currentState = PENDING;
  _this.value = undefined;
  // 用于保存 then 中的回调,只有当 promise
  // 状态为 pending 时才会缓存,而且每一个实例至多缓存一个
  _this.resolvedCallbacks = [];
  _this.rejectedCallbacks = [];

  _this.resolve = function (value) {
    if (value instanceof MyPromise) {
      // 若是 value 是个 Promise,递归执行
      return value.then(_this.resolve, _this.reject)
    }
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = RESOLVED;
        _this.value = value;
        _this.resolvedCallbacks.forEach(cb => cb());
      }
    })
  };

  _this.reject = function (reason) {
    setTimeout(() => { // 异步执行,保证执行顺序
      if (_this.currentState === PENDING) {
        _this.currentState = REJECTED;
        _this.value = reason;
        _this.rejectedCallbacks.forEach(cb => cb());
      }
    })
  }
  // 用于解决如下问题
  // new Promise(() => throw Error('error))
  try {
    fn(_this.resolve, _this.reject);
  } catch (e) {
    _this.reject(e);
  }
}

MyPromise.prototype.then = function (onResolved, onRejected) {
  var self = this;
  // 规范 2.2.7,then 必须返回一个新的 promise
  var promise2;
  // 规范 2.2.onResolved 和 onRejected 都为可选参数
  // 若是类型不是函数须要忽略,同时也实现了透传
  // Promise.resolve(4).then().then((value) => console.log(value))
  onResolved = typeof onResolved === 'function' ? onResolved : v => v;
  onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;

  if (self.currentState === RESOLVED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
      // 因此用了 setTimeout 包裹下
      setTimeout(function () {
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === REJECTED) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      setTimeout(function () {
        // 异步执行onRejected
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (reason) {
          reject(reason);
        }
      });
    }));
  }

  if (self.currentState === PENDING) {
    return (promise2 = new MyPromise(function (resolve, reject) {
      self.resolvedCallbacks.push(function () {
        // 考虑到可能会有报错,因此使用 try/catch 包裹
        try {
          var x = onResolved(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });

      self.rejectedCallbacks.push(function () {
        try {
          var x = onRejected(self.value);
          resolutionProcedure(promise2, x, resolve, reject);
        } catch (r) {
          reject(r);
        }
      });
    }));
  }
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
  // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
  if (promise2 === x) {
    return reject(new TypeError("Error"));
  }
  // 规范 2.3.2
  // 若是 x 为 Promise,状态为 pending 须要继续等待不然执行
  if (x instanceof MyPromise) {
    if (x.currentState === PENDING) {
      x.then(function (value) {
        // 再次调用该函数是为了确认 x resolve 的
        // 参数是什么类型,若是是基本类型就再次 resolve
        // 把值传给下个 then
        resolutionProcedure(promise2, value, resolve, reject);
      }, reject);
    } else {
      x.then(resolve, reject);
    }
    return;
  }
  // 规范 2.3.3.3.3
  // reject 或者 resolve 其中一个执行过得话,忽略其余的
  let called = false;
  // 规范 2.3.3,判断 x 是否为对象或者函数
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 规范 2.3.3.2,若是不能取出 then,就 reject
    try {
      // 规范 2.3.3.1
      let then = x.then;
      // 若是 then 是函数,调用 x.then
      if (typeof then === "function") {
        // 规范 2.3.3.3
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            // 规范 2.3.3.3.1
            resolutionProcedure(promise2, y, resolve, reject);
          },
          e => {
            if (called) return;
            called = true;
            reject(e);
          }
        );
      } else {
        // 规范 2.3.3.4
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // 规范 2.3.4,x 为基本类型
    resolve(x);
  }
}
复制代码

== 和 ===区别,什么状况用 ==

上图中的 toPrimitive 就是对象转基本类型。

这里来解析一道题目 [] == ![] // -> true ,下面是这个表达式为什么为 true 的步骤

// [] 转成 true,而后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true
复制代码

=== 用于判断二者类型和值是否相同。 在开发中,对于后端返回的 code,能够经过 == 去判断。

垃圾回收

V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。所以,V8 将内存(堆)分为新生代和老生代两部分。

新生代算法

新生代中的对象通常存活时间较短,使用 Scavenge GC 算法。

在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,一定有一个空间是使用的,另外一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,若是有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。

老生代算法

老生代中的对象通常存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。

在讲算法前,先来讲下什么状况下对象会出如今老生代空间中:

  • 新生代中的对象是否已经经历过一次 Scavenge 算法,若是经历过的话,会将对象重新生代空间移到老生代空间中。
  • To 空间的对象占比大小超过 25 %。在这种状况下,为了避免影响到内存分配,会将对象重新生代空间移到老生代空间中。

老生代中的空间很复杂,有以下几个空间

enum AllocationSpace {
  // TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不变的对象空间
  NEW_SPACE,   // 新生代用于 GC 复制算法的空间
  OLD_SPACE,   // 老生代常驻对象空间
  CODE_SPACE,  // 老生代代码对象空间
  MAP_SPACE,   // 老生代 map 对象
  LO_SPACE,    // 老生代大空间对象
  NEW_LO_SPACE,  // 新生代大空间对象

  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};
复制代码

在老生代中,如下状况会先启动标记清除算法:

  • 某一个空间没有分块的时候
  • 空间中被对象超过必定限制
  • 空间不能保证新生代中的对象移动到老生代中

在这个阶段中,会遍历堆中全部的对象,而后标记活的对象,在标记完成后,销毁全部没有被标记的对象。在标记大型对内存时,可能须要几百毫秒才能完成一次标记。这就会致使一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工做分解为更小的模块,可让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿状况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可让 GC 扫描和标记对象时,同时容许 JS 运行,你能够点击 该博客 详细阅读。

清除对象后会形成堆内存出现碎片的状况,当碎片超过必定限制后会启动压缩算法。在压缩过程当中,将活的对象像一端移动,直到全部对象都移动完成而后清理掉不须要的内存。

闭包

闭包的定义很简单:函数 A 返回了一个函数 B,而且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。

function A() {
  let a = 1
  function B() {
      console.log(a)
  }
  return B
}
复制代码

你是否会疑惑,为何函数 A 已经弹出调用栈了,为何函数 B 还能引用到函数 A 中的变量。由于函数 A 中的变量这时候是存储在堆上的。如今的 JS 引擎能够经过逃逸分析辨别出哪些变量须要存储在堆上,哪些须要存储在栈上。

经典面试题,循环中使用闭包解决 var 定义函数的问题

for ( var i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}
复制代码

首先由于 setTimeout 是个异步函数,全部会先把循环所有执行完毕,这时候 i 就是 6 了,因此会输出一堆 6。

解决办法两种,第一种使用闭包

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j);
    }, j * 1000);
  })(i);
}
复制代码

第二种就是使用 setTimeout 的第三个参数

for ( var i=1; i<=5; i++) {
	setTimeout( function timer(j) {
		console.log( j );
	}, i*1000, i);
}
复制代码

第三种就是使用 let 定义 i

for ( let i=1; i<=5; i++) {
	setTimeout( function timer() {
		console.log( i );
	}, i*1000 );
}
复制代码

由于对于 let 来讲,他会建立一个块级做用域,至关于

{ // 造成块级做用域
  let i = 0
  {
    let ii = i
    setTimeout( function timer() {
        console.log( ii );
    }, i*1000 );
  }
  i++
  {
    let ii = i
  }
  i++
  {
    let ii = i
  }
  ...
}
复制代码

基本数据类型和引⽤类型在存储上的差异

前者存储在栈上,后者存储在堆上

浏览器 Eventloop 和 Node 中的有什么区别

众所周知 JS 是门非阻塞单线程语言,由于在最初 JS 就是为了和浏览器交互而诞生的。若是 JS 是门多线程的语言话,咱们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另外一个线程中删除节点),固然能够引入读写锁解决这个问题。

JS 在执行的过程当中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。若是遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出须要执行的代码并放入执行栈中执行,因此本质上来讲 JS 中的异步仍是同步行为。

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

console.log('script end');
复制代码

以上代码虽然 setTimeout 延时为 0,其实仍是异步。这是由于 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增长。因此 setTimeout 仍是会在 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.nextTickpromiseObject.observeMutationObserver

宏任务包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

不少人有个误区,认为微任务快于宏任务,实际上是错误的。由于宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。

因此正确的一次 Event loop 顺序是这样的

  1. 执行同步代码,这属于宏任务
  2. 执行栈为空,查询是否有微任务须要执行
  3. 执行全部微任务
  4. 必要的话渲染 UI
  5. 而后开始下一轮 Event loop,执行宏任务中的异步代码

经过上述的 Event loop 顺序可知,若是宏任务中的异步代码有大量的计算而且须要操做 DOM 的话,为了更快的 界面响应,咱们能够把操做 DOM 放入微任务中。

Node 中的 Event loop

Node 中的 Event loop 和浏览器中的不相同。

Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
复制代码

timer

timers 阶段会执行 setTimeoutsetInterval

一个 timer 指定的时间并非准确时间,而是在达到这个时间后尽快执行回调,可能会由于系统正在执行别的事务而延迟。

下限的时间有一个范围:[1, 2147483647] ,若是设定的时间不在这个范围,将被设置为1。

**I/O **

I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调

idle, prepare

idle, prepare 阶段内部实现

poll

poll 阶段很重要,这一阶段中,系统会作两件事情

  1. 执行到点的定时器
  2. 执行 poll 队列中的事件

而且当 poll 中没有定时器的状况下,会发现如下两件事情

  • 若是 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制
  • 若是 poll 队列为空,会有两件事发生
    • 若是有 setImmediate 须要执行,poll 阶段会中止而且进入到 check 阶段执行 setImmediate
    • 若是没有 setImmediate 须要执行,会等待回调被加入到队列中并当即执行回调

若是有别的定时器须要被执行,会回到 timer 阶段执行回调。

check

check 阶段执行 setImmediate

close callbacks

close callbacks 阶段执行 close 事件

而且在 Node 中,有些状况下的定时器执行顺序是随机的

setTimeout(() => {
    console.log('setTimeout');
}, 0);
setImmediate(() => {
    console.log('setImmediate');
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 由于可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 不然会执行 setTimeout
复制代码

固然在这种状况下,执行顺序是相同的

var fs = require('fs')

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
});
// 由于 readFile 的回调在 poll 中执行
// 发现有 setImmediate ,因此会当即跳到 check 阶段执行回调
// 再去 timer 阶段执行 setTimeout
// 因此以上输出必定是 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);

process.nextTick(() => {
  console.log("nextTick");
});
// nextTick, timer1, promise1
复制代码

setTimeout 倒计时偏差

JS 是单线程的,因此 setTimeout 的偏差实际上是没法被彻底解决的,缘由有不少,多是回调中的,有多是浏览器中的各类事件致使。这也是为何页面开久了,定时器会不许的缘由,固然咱们能够经过必定的办法去减小这个偏差。

如下是一个相对准备的倒计时实现

var period = 60 * 1000 * 60 * 2
var startTime = new Date().getTime();
var count = 0
var end = new Date().getTime() + period
var interval = 1000
var currentInterval = interval

function loop() {
  count++
  var offset = new Date().getTime() - (startTime + count * interval); // 代码执行所消耗的时间
  var diff = end - new Date().getTime()
  var h = Math.floor(diff / (60 * 1000 * 60))
  var hdiff = diff % (60 * 1000 * 60)
  var m = Math.floor(hdiff / (60 * 1000))
  var mdiff = hdiff % (60 * 1000)
  var s = mdiff / (1000)
  var sCeil = Math.ceil(s)
  var sFloor = Math.floor(s)
  currentInterval = interval - offset // 获得下一次循环所消耗的时间
  console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval) // 打印 时 分 秒 代码执行时间 下次循环间隔

  setTimeout(loop, currentInterval)
}

setTimeout(loop, currentInterval)
复制代码

防抖

你是否在平常开发中遇到一个问题,在滚动事件中须要作个复杂计算或者实现一个按钮的防二次点击操做。

这些需求均可以经过函数防抖动来实现。尤为是第一个需求,若是在频繁的事件回调中作复杂计算,颇有可能致使页面卡顿,不如将屡次计算合并为一次计算,只在一个精确点作操做。

PS:防抖和节流的做用都是防止函数屡次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的状况下只会调用一次,而节流的 状况会每隔必定时间(参数wait)调用函数。

咱们先来看一个袖珍版的防抖理解一下防抖的实现:

// func是用户传入须要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
  // 缓存一个定时器id
  let timer = 0
  // 这里返回的函数是每次用户实际调用的防抖函数
  // 若是已经设定过定时器了就清空上一次的定时器
  // 开始一个新的定时器,延迟执行用户传入的方法
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      func.apply(this, args)
    }, wait)
  }
}
// 不难看出若是用户调用该函数的间隔小于wait的状况下,上一次的时间还未到就被清除了,并不会执行函数
复制代码

这是一个简单版的防抖,可是有缺陷,这个防抖只能在最后调用。通常的防抖会有immediate选项,表示是否当即调用。这二者的区别,举个栗子来讲:

  • 例如在搜索引擎搜索问题的时候,咱们固然是但愿用户输入完最后一个字才调用查询接口,这个时候适用延迟执行的防抖函数,它老是在一连串(间隔小于wait的)函数触发以后调用。
  • 例如用户给interviewMap点star的时候,咱们但愿用户点第一下的时候就去调用接口,而且成功以后改变star按钮的样子,用户就能够立马获得反馈是否star成功了,这个状况适用当即执行的防抖函数,它老是在第一次调用,而且下一次调用必须与前一次调用的时间间隔大于wait才会触发。

下面咱们来实现一个带有当即执行选项的防抖函数

// 这个是用来获取当前时间戳的
function now() {
  return +new Date()
}
/** * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行 * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {boolean} immediate 设置为ture时,是否当即调用函数 * @return {function} 返回客户调用函数 */
function debounce (func, wait = 50, immediate = true) {
  let timer, context, args
  
  // 延迟执行函数
  const later = () => setTimeout(() => {
    // 延迟函数执行完毕,清空缓存的定时器序号
    timer = null
    // 延迟执行的状况下,函数会在延迟函数中执行
    // 使用到以前缓存的参数和上下文
    if (!immediate) {
      func.apply(context, args)
      context = args = null
    }
  }, wait)

  // 这里返回的函数是每次实际调用的函数
  return function(...params) {
    // 若是没有建立延迟执行函数(later),就建立一个
    if (!timer) {
      timer = later()
      // 若是是当即执行,调用函数
      // 不然缓存参数和调用上下文
      if (immediate) {
        func.apply(this, params)
      } else {
        context = this
        args = params
      }
    // 若是已有延迟执行函数(later),调用的时候清除原来的并从新设定一个
    // 这样作延迟函数会从新计时
    } else {
      clearTimeout(timer)
      timer = later()
    }
  }
}
复制代码

总体函数实现的不难,总结一下。

  • 对于按钮防点击来讲的实现:若是函数是当即执行的,就当即调用,若是函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都从新计时。一旦你点累了,定时器时间到,定时器重置为 null,就能够再次点击了。
  • 对于延时执行函数来讲的实现:清除定时器ID,若是是延迟调用就调用函数

数组降维

[1, [2], 3].flatMap((v) => v + 1)
// -> [2, 3, 4]
复制代码

若是想将一个多维数组完全的降维,能够这样实现

const flattenDeep = (arr) => Array.isArray(arr)
  ? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
  : [arr]

flattenDeep([1, [[2], [3, [4]], 5]])
复制代码

深拷贝

这个问题一般能够经过 JSON.parse(JSON.stringify(object)) 来解决。

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
复制代码

可是该方法也是有局限性的:

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 不能解决循环引用的对象
let obj = {
  a: 1,
  b: {
    c: 2,
    d: 3,
  },
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)
复制代码

若是你有这么一个循环引用对象,你会发现你不能经过该方法深拷贝

在遇到函数、 undefined 或者 symbol 的时候,该对象也不能正常的序列化

let a = {
    age: undefined,
    sex: Symbol('male'),
    jobs: function() {},
    name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}
复制代码

你会发如今上述状况中,该方法会忽略掉函数和 undefined

可是在一般状况下,复杂数据都是能够序列化的,因此这个函数能够解决大部分问题,而且该函数是内置函数中处理深拷贝性能最快的。固然若是你的数据中含有以上三种状况下,能够使用 lodash 的深拷贝函数

若是你所需拷贝的对象含有内置类型而且不包含函数,能够使用 MessageChannel

function structuralClone(obj) {
  return new Promise(resolve => {
    const {port1, port2} = new MessageChannel();
    port2.onmessage = ev => resolve(ev.data);
    port1.postMessage(obj);
  });
}

var obj = {a: 1, b: {
    c: b
}}
// 注意该方法是异步的
// 能够处理 undefined 和循环引用对象
(async () => {
  const clone = await structuralClone(obj)
})()
复制代码

typeof 于 instanceof 区别

typeof 对于基本类型,除了 null 均可以显示正确的类型

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 没有声明,可是还会显示 undefined
复制代码

typeof 对于对象,除了函数都会显示 object

typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
复制代码

对于 null 来讲,虽然它是基本类型,可是会显示 object,这是一个存在好久了的 Bug

typeof null // 'object'
复制代码

PS:为何会出现这种状况呢?由于在 JS 的最第一版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头表明是对象,然而 null 表示为全零,因此将它错误的判断为 object 。虽然如今的内部类型判断代码已经改变了,可是对于这个 Bug 倒是一直流传下来。

instanceof 能够正确的判断对象的类型,由于内部机制是经过判断对象的原型链中是否是能找到类型的 prototype

咱们也能够试着实现一下 instanceof

function instanceof(left, right) {
    // 得到类型的原型
    let prototype = right.prototype
    // 得到对象的原型
    left = left.__proto__
    // 判断对象的类型是否等于类型的原型
    while (true) {
    	if (left === null)
    		return false
    	if (prototype === left)
    		return true
    	left = left.__proto__
    }
}
复制代码

浏览器相关

cookie和localSrorage、session、indexDB 的区别

特性 cookie localStorage sessionStorage indexDB
数据生命周期 通常由服务器生成,能够设置过时时间 除非被清理,不然一直存在 页面关闭就清理 除非被清理,不然一直存在
数据存储大小 4K 5M 5M 无限
与服务端通讯 每次都会携带在 header 中,对于请求性能影响 不参与 不参与 不参与

从上表能够看到,cookie 已经不建议用于存储。若是没有大量数据存储需求的话,能够使用 localStoragesessionStorage 。对于不怎么改变的数据尽可能使用 localStorage 存储,不然能够用 sessionStorage 存储。

对于 cookie,咱们还须要注意安全性。

属性 做用
value 若是用于保存用户登陆态,应该将该值加密,不能使用明文的用户标识
http-only 不能经过 JS 访问 Cookie,减小 XSS 攻击
secure 只能在协议为 HTTPS 的请求中携带
same-site 规定浏览器不能在跨域请求中携带 Cookie,减小 CSRF 攻击

怎么判断页面是否加载完成?

Load 事件触发表明页面中的 DOM,CSS,JS,图片已经所有加载完毕。

DOMContentLoaded 事件触发表明初始的 HTML 被彻底加载和解析,不须要等待 CSS,JS,图片加载。

如何解决跨域

由于浏览器出于安全考虑,有同源策略。也就是说,若是协议、域名或者端口有一个不一样就是跨域,Ajax 请求会失败。

咱们能够经过如下几种经常使用方法解决跨域的问题

JSONP

JSONP 的原理很简单,就是利用 <script> 标签没有跨域限制的漏洞。经过 <script> 标签指向一个须要访问的地址并提供一个回调函数来接收数据当须要通信时。

<script src="http://domain/api?param1=a&param2=b&callback=jsonp"></script>
<script>
    function jsonp(data) {
    	console.log(data)
	}
</script>    
复制代码

JSONP 使用简单且兼容性不错,可是只限于 get 请求。

在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就须要本身封装一个 JSONP,如下是简单实现

function jsonp(url, jsonpCallback, success) {
  let script = document.createElement("script");
  script.src = url;
  script.async = true;
  script.type = "text/javascript";
  window[jsonpCallback] = function(data) {
    success && success(data);
  };
  document.body.appendChild(script);
}
jsonp(
  "http://xxx",
  "callback",
  function(value) {
    console.log(value);
  }
);
复制代码

CORS

CORS须要浏览器和后端同时支持。IE 8 和 9 须要经过 XDomainRequest 来实现。

浏览器会自动进行 CORS 通讯,实现CORS通讯的关键是后端。只要后端实现了 CORS,就实现了跨域。

服务端设置 Access-Control-Allow-Origin 就能够开启 CORS。 该属性表示哪些域名能够访问资源,若是设置通配符则表示全部网站均可以访问资源。

document.domain

该方式只能用于二级域名相同的状况下,好比 a.test.comb.test.com 适用于该方式。

只须要给页面添加 document.domain = 'test.com' 表示二级域名都相同就能够实现跨域

postMessage

这种方式一般用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另外一个页面判断来源并接收消息

// 发送消息端
window.parent.postMessage('message', 'http://test.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
    var origin = event.origin || event.originalEvent.origin; 
    if (origin === 'http://test.com') {
        console.log('验证经过')
    }
});
复制代码

什么是事件代理

若是一个节点中的子节点是动态生成的,那么子节点须要注册事件的话应该注册在父节点上

<ul id="ul">
	<li>1</li>
    <li>2</li>
	<li>3</li>
	<li>4</li>
	<li>5</li>
</ul>
<script> let ul = document.querySelector('#ul') ul.addEventListener('click', (event) => { console.log(event.target); }) </script>
复制代码

事件代理的方式相对于直接给目标注册事件来讲,有如下优势

  • 节省内存
  • 不须要给子节点注销事件

Service worker

Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也能够在网络可用时做为浏览器和网络间的代理。它们旨在(除其余以外)使得可以建立有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采起适当的动做。他们还容许访问推送通知和后台同步API。

目前该技术一般用来作缓存文件,提升首屏速度,能够试着来实现这个功能。

// index.js
if (navigator.serviceWorker) {
  navigator.serviceWorker
    .register("sw.js")
    .then(function(registration) {
      console.log("service worker 注册成功");
    })
    .catch(function(err) {
      console.log("servcie worker 注册失败");
    });
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener("install", e => {
  e.waitUntil(
    caches.open("my-cache").then(function(cache) {
      return cache.addAll(["./index.html", "./index.js"]);
    })
  );
});

// 拦截全部请求事件
// 若是缓存中已经有请求的数据就直接用缓存,不然去请求数据
self.addEventListener("fetch", e => {
  e.respondWith(
    caches.match(e.request).then(function(response) {
      if (response) {
        return response;
      }
      console.log("fetch source");
    })
  );
});
复制代码

打开页面,能够在开发者工具中的 Application 看到 Service Worker 已经启动了

浏览器缓存

缓存对于前端性能优化来讲是个很重要的点,良好的缓存策略能够下降资源的重复加载提升网页的总体加载速度。

一般浏览器缓存策略分为两种:强缓存和协商缓存。

强缓存

实现强缓存能够经过两种响应头实现:ExpiresCache-Control 。强缓存表示在缓存期间不须要请求,state code 为 200

Expires: Wed, 22 Oct 2018 08:41:00 GMT
复制代码

Expires 是 HTTP / 1.0 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过时,须要再次请求。而且 Expires 受限于本地时间,若是修改了本地时间,可能会形成缓存失效。

Cache-control: max-age=30
复制代码

Cache-Control 出现于 HTTP / 1.1,优先级高于 Expires 。该属性表示资源会在 30 秒后过时,须要再次请求。

协商缓存

若是缓存过时了,咱们就能够使用协商缓存来解决问题。协商缓存须要请求,若是缓存有效会返回 304。

协商缓存须要客户端和服务端共同实现,和强缓存同样,也有两种实现方式。

Last-Modified 和 If-Modified-Since

Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified 的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。

可是若是在本地打开缓存文件,就会形成 Last-Modified 被修改,因此在 HTTP / 1.1 出现了 ETag

ETag 和 If-None-Match

ETag 相似于文件指纹,If-None-Match 会将当前 ETag 发送给服务器,询问该资源 ETag 是否变更,有变更的话就将新的资源发送回来。而且 ETag 优先级比 Last-Modified 高。

选择合适的缓存策略

对于大部分的场景均可以使用强缓存配合协商缓存解决,可是在一些特殊的地方可能须要选择特殊的缓存策略

  • 对于某些不须要缓存的资源,能够使用 Cache-control: no-store ,表示该资源不须要缓存
  • 对于频繁变更的资源,能够使用 Cache-Control: no-cache 并配合 ETag 使用,表示该资源已被缓存,可是每次都会发送请求询问资源是否更新。
  • 对于代码文件来讲,一般使用 Cache-Control: max-age=31536000 并配合策略缓存使用,而后对文件进行指纹处理,一旦文件名变更就会马上下载新的文件。

浏览器性能问题

重绘(Repaint)和回流(Reflow)

重绘和回流是渲染步骤中的一小节,可是这两个步骤对于性能影响很大。

  • 重绘是当节点须要更改外观而不会影响布局的,好比改变 color 就叫称为重绘
  • 回流是布局或者几何属性须要改变就称为回流。

回流一定会发生重绘,重绘不必定会引起回流。回流所需的成本比重绘高的多,改变深层次的节点极可能致使父节点的一系列回流。

因此如下几个动做可能会致使性能问题:

  • 改变 window 大小
  • 改变字体
  • 添加或删除样式
  • 文字改变
  • 定位或者浮动
  • 盒模型

不少人不知道的是,重绘和回流其实和 Event loop 有关。

  1. 当 Event loop 执行完 Microtasks 后,会判断 document 是否须要更新。由于浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
  2. 而后判断是否有 resize 或者 scroll ,有的话会去触发事件,因此 resizescroll 事件也是至少 16ms 才会触发一次,而且自带节流功能。
  3. 判断是否触发了 media query
  4. 更新动画而且发送事件
  5. 判断是否有全屏操做事件
  6. 执行 requestAnimationFrame 回调
  7. 执行 IntersectionObserver 回调,该方法用于判断元素是否可见,能够用于懒加载上,可是兼容性很差
  8. 更新界面
  9. 以上就是一帧中可能会作的事情。若是在一帧中有空闲时间,就会去执行 requestIdleCallback 回调。

以上内容来自于 HTML 文档

减小重绘和回流
  • 使用 translate 替代 top

    <div class="test"></div>
    <style> .test { position: absolute; top: 10px; width: 100px; height: 100px; background: red; } </style>
    <script> setTimeout(() => { // 引发回流 document.querySelector('.test').style.top = '100px' }, 1000) </script>
    复制代码
  • 使用 visibility 替换 display: none ,由于前者只会引发重绘,后者会引起回流(改变了布局)

  • 把 DOM 离线后修改,好比:先把 DOM 给 display:none (有一次 Reflow),而后你修改100次,而后再把它显示出来

  • 不要把 DOM 结点的属性值放在一个循环里当成循环里的变量

    for(let i = 0; i < 1000; i++) {
        // 获取 offsetTop 会致使回流,由于须要去获取正确的值
        console.log(document.querySelector('.test').style.offsetTop)
    }
    复制代码
  • 不要使用 table 布局,可能很小的一个小改动会形成整个 table 的从新布局

  • 动画实现的速度的选择,动画速度越快,回流次数越多,也能够选择使用 requestAnimationFrame

  • CSS 选择符从右往左匹配查找,避免 DOM 深度过深

  • 将频繁运行的动画变为图层,图层可以阻止该节点回流影响别的元素。好比对于 video 标签,浏览器会自动将该节点变为图层。

图片优化

计算图片大小

对于一张 100 * 100 像素的图片来讲,图像上有 10000 个像素点,若是每一个像素的值是 RGBA 存储的话,那么也就是说每一个像素有 4 个通道,每一个通道 1 个字节(8 位 = 1个字节),因此该图片大小大概为 39KB(10000 * 1 * 4 / 1024)。

可是在实际项目中,一张图片可能并不须要使用那么多颜色去显示,咱们能够经过减小每一个像素的调色板来相应缩小图片的大小。

了解了如何计算图片大小的知识,那么对于如何优化图片,想必你们已经有 2 个思路了:

  • 减小像素点
  • 减小每一个像素点可以显示的颜色
图片加载优化
  1. 不用图片。不少时候会使用到不少修饰类图片,其实这类修饰图片彻底能够用 CSS 去代替。
  2. 对于移动端来讲,屏幕宽度就那么点,彻底没有必要去加载原图浪费带宽。通常图片都用 CDN 加载,能够计算出适配屏幕的宽度,而后去请求相应裁剪好的图片。
  3. 小图使用 base64 格式
  4. 将多个图标文件整合到一张图片中(雪碧图)
  5. 选择正确的图片格式:
    • 对于可以显示 WebP 格式的浏览器尽可能使用 WebP 格式。由于 WebP 格式具备更好的图像数据压缩算法,能带来更小的图片体积,并且拥有肉眼识别无差别的图像质量,缺点就是兼容性并很差
    • 小图使用 PNG,其实对于大部分图标这类图片,彻底能够使用 SVG 代替
    • 照片使用 JPEG

其余文件优化

  • CSS 文件放在 head
  • 服务端开启文件压缩功能
  • script 标签放在 body 底部,由于 JS 文件执行会阻塞渲染。固然也能够把 script 标签放在任意位置而后加上 defer ,表示该文件会并行下载,可是会放到 HTML 解析完成后顺序执行。对于没有任何依赖的 JS 文件能够加上 async ,表示加载和渲染后续文档元素的过程将和 JS 文件的加载与执行并行无序进行。
  • 执行 JS 代码过长会卡住渲染,对于须要不少时间计算的代码能够考虑使用 WebworkerWebworker 可让咱们另开一个线程执行脚本而不影响渲染。

CDN

静态资源尽可能使用 CDN 加载,因为浏览器对于单个域名有并发请求上限,能够考虑使用多个 CDN 域名。对于 CDN 加载静态资源须要注意 CDN 域名要与主站不一样,不然每次请求都会带上主站的 Cookie。

使用 Webpack 优化项目

  • 对于 Webpack4,打包项目使用 production 模式,这样会自动开启代码压缩
  • 使用 ES6 模块来开启 tree shaking,这个技术能够移除没有使用的代码
  • 优化图片,对于小图能够使用 base64 的方式写入文件中
  • 按照路由拆分代码,实现按需加载
  • 给打包出来的文件名添加哈希,实现浏览器缓存文件

Webpack

优化打包速度

  • 减小文件搜索范围
    • 好比经过别名
    • loader 的 test,include & exclude
  • Webpack4 默认压缩并行
  • Happypack 并发调用
  • babel 也能够缓存编译

Babel 原理

本质就是编译器,当代码转为字符串生成 AST,对 AST 进行转变最后再生成新的代码

  • 分为三步:词法分析生成 Token,语法分析生成 AST,遍历 AST,根据插件变换相应的节点,最后把 AST 转换为代码

如何实现一个插件

  • 调用插件 apply 函数传入 compiler 对象
  • 经过 compiler 对象监听事件

好比你想实现一个编译结束退出命令的插件

class BuildEndPlugin {
  apply (compiler) {
    const afterEmit = (compilation, cb) => {
      cb()
      setTimeout(function () {
        process.exit(0)
      }, 1000)
    }

    compiler.plugin('after-emit', afterEmit)
  }
}

module.exports = BuildEndPlugin
复制代码

框架

React 生命周期

在 V16 版本中引入了 Fiber 机制。这个机制必定程度上的影响了部分生命周期的调用,而且也引入了新的 2 个 API 来解决问题。

在以前的版本中,若是你拥有一个很复杂的复合组件,而后改动了最上层组件的 state,那么调用栈可能会很长

调用栈过长,再加上中间进行了复杂的操做,就可能致使长时间阻塞主线程,带来很差的用户体验。Fiber 就是为了解决该问题而生。

Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将以前的同步渲染改为了异步渲染,在不影响体验的状况下去分段计算更新。

对于如何区别优先级,React 有本身的一套逻辑。对于动画这种实时性很高的东西,也就是 16 ms 必须渲染一次保证不卡顿的状况下,React 会每 16 ms(之内) 暂停一下更新,返回来继续渲染动画。

对于异步渲染,如今渲染有两个阶段:reconciliationcommit 。前者过程是能够打断的,后者不能暂停,会一直更新界面直到完成。

Reconciliation 阶段

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

Commit 阶段

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

由于 reconciliation 阶段是能够被打断的,因此 reconciliation 阶段会执行的生命周期函数就可能会出现调用屡次的状况,从而引发 Bug。因此对于 reconciliation 阶段调用的几个函数,除了 shouldComponentUpdate 之外,其余都应该避免去使用,而且 V16 中也引入了新的 API 来解决这个问题。

getDerivedStateFromProps 用于替换 componentWillReceiveProps ,该函数会在初始化和 update 时被调用

class ExampleComponent extends React.Component {
  // Initialize state in constructor,
  // Or with a property initializer.
  state = {};

  static getDerivedStateFromProps(nextProps, prevState) {
    if (prevState.someMirroredValue !== nextProps.someValue) {
      return {
        derivedData: computeDerivedState(nextProps),
        someMirroredValue: nextProps.someValue
      };
    }

    // Return null to indicate no change to state.
    return null;
  }
}
复制代码

getSnapshotBeforeUpdate 用于替换 componentWillUpdate ,该函数会在 update 后 DOM 更新前被调用,用于读取最新的 DOM 数据。

V16 生命周期函数用法建议

class ExampleComponent extends React.Component {
  // 用于初始化 state
  constructor() {}
  // 用于替换 `componentWillReceiveProps` ,该函数会在初始化和 `update` 时被调用
  // 由于该函数是静态函数,因此取不到 `this`
  // 若是须要对比 `prevProps` 须要单独在 `state` 中维护
  static getDerivedStateFromProps(nextProps, prevState) {}
  // 判断是否须要更新组件,多用于组件性能优化
  shouldComponentUpdate(nextProps, nextState) {}
  // 组件挂载后调用
  // 能够在该函数中进行请求或者订阅
  componentDidMount() {}
  // 用于得到最新的 DOM 数据
  getSnapshotBeforeUpdate() {}
  // 组件即将销毁
  // 能够在此处移除订阅,定时器等等
  componentWillUnmount() {}
  // 组件销毁后调用
  componentDidUnMount() {}
  // 组件更新后调用
  componentDidUpdate() {}
  // 渲染组件函数
  render() {}
  // 如下函数不建议使用
  UNSAFE_componentWillMount() {}
  UNSAFE_componentWillUpdate(nextProps, nextState) {}
  UNSAFE_componentWillReceiveProps(nextProps) {}
}
复制代码

setState

setState 在 React 中是常用的一个 API,可是它存在一些问题,可能会致使犯错,核心缘由就是由于这个 API 是异步的。

首先 setState 的调用并不会立刻引发 state 的改变,而且若是你一次调用了多个 setState ,那么结果可能并不如你期待的同样。

handle() {
  // 初始化 `count` 为 0
  console.log(this.state.count) // -> 0
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  this.setState({ count: this.state.count + 1 })
  console.log(this.state.count) // -> 0
}
复制代码

第一,两次的打印都为 0,由于 setState 是个异步 API,只有同步代码运行完毕才会执行。setState 异步的缘由我认为在于,setState 可能会致使 DOM 的重绘,若是调用一次就立刻去进行重绘,那么调用屡次就会形成没必要要的性能损失。设计成异步的话,就能够将屡次调用放入一个队列中,在恰当的时候统一进行更新过程。

第二,虽然调用了三次 setState ,可是 count 的值仍是为 1。由于屡次调用会合并为一次,只有当更新结束后 state 才会改变,三次调用等同于以下代码

Object.assign(  
  {},
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
  { count: this.state.count + 1 },
)
复制代码

固然你也能够经过如下方式来实现调用三次 setState 使得 count 为 3

handle() {
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
  this.setState((prevState) => ({ count: prevState.count + 1 }))
}
复制代码

若是你想在每次调用 setState 后得到正确的 state ,能够经过以下代码实现

handle() {
    this.setState((prevState) => ({ count: prevState.count + 1 }), () => {
        console.log(this.state)
    })
}
复制代码

Vue的 nextTick 原理

nextTick 可让咱们在下次 DOM 更新循环结束以后执行延迟回调,用于得到更新后的 DOM。

在 Vue 2.4 以前都是使用的 microtasks,可是 microtasks 的优先级太高,在某些状况下可能会出现比事件冒泡更快的状况,但若是都使用 macrotasks 又可能会出现渲染的性能问题。因此在新版本中,会默认使用 microtasks,但在特殊状况下会使用 macrotasks,好比 v-on。

对于实现 macrotasks ,会先判断是否能使用 setImmediate ,不能的话降级为 MessageChannel ,以上都不行的话就使用 setTimeout

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (
  typeof MessageChannel !== 'undefined' &&
  (isNative(MessageChannel) ||
    // PhantomJS
    MessageChannel.toString() === '[object MessageChannelConstructor]')
) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
复制代码

nextTick 同时也支持 Promise 的使用,会判断是否实现了 Promise

export function nextTick(cb?: Function, ctx?: Object) {
  let _resolve
  // 将回调函数整合进一个数组中
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // 判断是否能够使用 Promise 
  // 能够的话给 _resolve 赋值
  // 这样回调函数就能以 promise 的方式调用
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}
复制代码

Vue 生命周期

生命周期函数就是组件在初始化或者数据更新时会触发的钩子函数。

在初始化时,会调用如下代码,生命周期就是经过 callHook 调用的

Vue.prototype._init = function(options) {
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate') // 拿不到 props data
    initInjections(vm) 
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')
}
复制代码

能够发如今以上代码中,beforeCreate 调用的时候,是获取不到 props 或者 data 中的数据的,由于这些数据的初始化都在 initState 中。

接下来会执行挂载函数

export function mountComponent {
    callHook(vm, 'beforeMount')
    // ...
    if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
    }
}
复制代码

beforeMount 就是在挂载前执行的,而后开始建立 VDOM 并替换成真实 DOM,最后执行 mounted 钩子。这里会有个判断逻辑,若是是外部 new Vue({}) 的话,不会存在 $vnode ,因此直接执行 mounted 钩子了。若是有子组件的话,会递归挂载子组件,只有当全部子组件所有挂载完毕,才会执行根组件的挂载钩子。

接下来是数据更新时会调用的钩子函数

function flushSchedulerQueue() {
  // ...
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before() // 调用 beforeUpdate
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' +
            (watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`),
          watcher.vm
        )
        break
      }
    }
  }
  callUpdatedHooks(updatedQueue)
}

function callUpdatedHooks(queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}
复制代码

上图还有两个生命周期没有说,分别为 activateddeactivated ,这两个钩子函数是 keep-alive 组件独有的。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 actived 钩子函数。

最后就是销毁组件的钩子函数了

Vue.prototype.$destroy = function() {
  // ...
  callHook(vm, 'beforeDestroy')
  vm._isBeingDestroyed = true
  // remove self from parent
  const parent = vm.$parent
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm)
  }
  // teardown watchers
  if (vm._watcher) {
    vm._watcher.teardown()
  }
  let i = vm._watchers.length
  while (i--) {
    vm._watchers[i].teardown()
  }
  // remove reference from data ob
  // frozen object may not have observer.
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--
  }
  // call the last hook...
  vm._isDestroyed = true
  // invoke destroy hooks on current rendered tree
  vm.__patch__(vm._vnode, null)
  // fire destroyed hook
  callHook(vm, 'destroyed')
  // turn off all instance listeners.
  vm.$off()
  // remove __vue__ reference
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  // release circular reference (#6759)
  if (vm.$vnode) {
    vm.$vnode.parent = null
  }
}
复制代码

在执行销毁操做前会调用 beforeDestroy 钩子函数,而后进行一系列的销毁操做,若是有子组件的话,也会递归销毁子组件,全部子组件都销毁完毕后才会执行根组件的 destroyed 钩子函数。

Vue 双向绑定

  • 在初始化 data props 时,递归对象,给每个属性双向绑定,对于数组而言,会拿到原型重写函数,实现手动派发更新。由于函数不能监听到数据的变更,和 proxy 比较一下。
  • 除了以上数组函数,经过索引改变数组数据或者给对象添加新属性也不能触发,须要使用自带的set 函数,这个函数内部也是手动派发更新
  • 在组件挂载时,会实例化渲染观察者,传入组件更新的回调。在实例化过程当中,会对模板中的值对象进行求值,触发依赖收集。在触发依赖以前,会保存当前的渲染观察者,用于组件含有子组件的时候,恢复父组件的观察者。触发依赖收集后,会清理掉不须要的依赖,性能优化,防止不须要的地方去重复渲染。
  • 改变值会触发依赖更新,会将收集到的全部依赖所有拿出来,放入 nextTick 中统一执行。执行过程当中,会先对观察者进行排序,渲染的最后执行。先执行 beforeupdate 钩子函数,而后执行观察者的回调。在执行回调的过程当中,可能 watch 会再次 push 进来,由于存在在回调中再次赋值,判断无限循环。

v-model原理

  • v:model 在模板编译的时候转换代码
  • v-model 本质是 :value 和 v-on,可是略微有点区别。在输入控件下,有两个事件监听,输入中文时只有当输出中文才触发数据赋值
  • v-model 和:bind 同时使用,前者优先级更高,若是 :value 会出现冲突
  • v-model 由于语法糖的缘由,还能够用于父子通讯

watch 和 computed 的区别和运用的场景

  • 前者是计算属性,依赖其余属性计算值。而且 computer 的值有缓存,只有当计算值变化才变化触发渲染。后者监听到值得变化就会执行回调
  • computer 就是简单计算一下,适用于渲染页面。watch 适合作一些复杂业务逻辑
  • 前者有依赖两个 watcher,computer watcher 和渲染 watcher。判断计算出的值变化后渲染 watcher 派发更新触发渲染

Vue 的父子通讯

  • 使用 v-model 实现父传子,子传父。由于 v-model 默认解析成 :value 和 :input
  • 父传子
    • 经过 props
    • 经过 $children 访问子组件数组,注意该数组乱序
    • 对于多级父传子,能够使用 v-bind={$attrs} ,经过对象的方式筛选出父组件中传入但子组件不须要的 props
    • $listens 包含了父做用域中的 (不含 .native 修饰器的) v-on 事件监听器。
  • 子传父
    • 父组件传递函数给子组件,子组件经过 $emit 触发
    • 修改父组件的 props
    • 经过 $parent 访问父组件
    • .sync
  • 平行组件
    • EventBus
  • Vuex 解决一切

路由原理

前端路由实现起来其实很简单,本质就是监听 URL 的变化,而后匹配路由规则,显示相应的页面,而且无须刷新。目前单页面使用的路由就只有两种实现方式

  • hash 模式
  • history 模式

www.test.com/#/ 就是 Hash URL,当 # 后面的哈希值发生变化时,不会向服务器请求数据,能够经过 hashchange 事件来监听到 URL 的变化,从而进行跳转页面。

History 模式是 HTML5 新推出的功能,比之 Hash URL 更加美观

MVVM

MVVM 由如下三个内容组成

  • View:界面
  • Model:数据模型
  • ViewModel:做为桥梁负责沟通 View 和 Model

在 JQuery 时期,若是须要刷新 UI 时,须要先取到对应的 DOM 再更新 UI,这样数据和业务的逻辑就和页面有强耦合。

在 MVVM 中,UI 是经过数据驱动的,数据一旦改变就会相应的刷新对应的 UI,UI 若是改变,也会改变对应的数据。这种方式就能够在业务处理中只关心数据的流转,而无需直接和页面打交道。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种状况下,View 和 Model 均可以独立出来,任何一方改变了也不必定须要改变另外一方,而且能够将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel。

在 MVVM 中,最核心的也就是数据双向绑定,例如 Angluar 的脏数据检测,Vue 中的数据劫持。

脏数据检测

当触发了指定事件后会进入脏数据检测,这时会调用 $digest 循环遍历全部的数据观察者,判断当前值是否和先前的值有区别,若是检测到变化的话,会调用 $watch 函数,而后再次调用 $digest 循环直到发现没有变化。循环至少为二次 ,至多为十次。

脏数据检测虽然存在低效的问题,可是不关心数据是经过什么方式改变的,均可以完成任务,可是这在 Vue 中的双向绑定是存在问题的。而且脏数据检测能够实现批量检测出更新的值,再去统一更新 UI,大大减小了操做 DOM 的次数。因此低效也是相对的,这就仁者见仁智者见智了。

数据劫持

Vue 内部使用了 Object.defineProperty() 来实现双向绑定,经过这个函数能够监听到 setget 的事件。

var data = { name: 'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value

function observe(obj) {
  // 判断类型
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key])
  })
}

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
    }
  })
}
复制代码

以上代码简单的实现了如何监听数据的 setget 的事件,可是仅仅如此是不够的,还须要在适当的时候给属性添加发布订阅

<div>
    {{name}}
</div>
复制代码

在解析如上模板代码时,遇到 {{name}} 就会给属性 name 添加发布订阅。

// 经过 Dep 解耦
class Dep {
  constructor() {
    this.subs = []
  }
  addSub(sub) {
    // sub 是 Watcher 实例
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update()
    })
  }
}
// 全局属性,经过该属性配置 Watcher
Dep.target = null

function update(value) {
  document.querySelector('div').innerText = value
}

class Watcher {
  constructor(obj, key, cb) {
    // 将 Dep.target 指向本身
    // 而后触发属性的 getter 添加监听
    // 最后将 Dep.target 置空
    Dep.target = this
    this.cb = cb
    this.obj = obj
    this.key = key
    this.value = obj[key]
    Dep.target = null
  }
  update() {
    // 得到新值
    this.value = this.obj[this.key]
    // 调用 update 方法更新 Dom
    this.cb(this.value)
  }
}
var data = { name: 'yck' }
observe(data)
// 模拟解析到 `{{name}}` 触发的操做
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy' 
复制代码

接下来,对 defineReactive 函数进行改造

function defineReactive(obj, key, val) {
  // 递归子属性
  observe(val)
  let dp = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      console.log('get value')
      // 将 Watcher 添加到订阅
      if (Dep.target) {
        dp.addSub(Dep.target)
      }
      return val
    },
    set: function reactiveSetter(newVal) {
      console.log('change value')
      val = newVal
      // 执行 watcher 的 update 方法
      dp.notify()
    }
  })
}
复制代码

以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加。

Proxy 与 Object.defineProperty 对比

Object.defineProperty 虽然已经可以实现双向绑定了,可是他仍是有缺陷的。

  1. 只能对属性进行数据劫持,因此须要深度遍历整个对象
  2. 对于数组不能监听到数据的变化

虽然 Vue 中确实能检测到数组数据的变化,可是实际上是使用了 hack 的办法,而且也是有缺陷的。

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// hack 如下几个函数
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // 得到原生函数
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    // 调用原生函数
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 触发更新
    ob.dep.notify()
    return result
  })
})
复制代码

反观 Proxy 就没以上的问题,原生支持监听数组变化,而且能够直接对整个对象进行拦截,因此 Vue 也将在下个大版本中使用 Proxy 替换 Object.defineProperty

let onWatch = (obj, setBind, getLogger) => {
  let handler = {
    get(target, property, receiver) {
      getLogger(target, property)
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      setBind(value);
      return Reflect.set(target, property, value);
    }
  };
  return new Proxy(obj, handler);
};

let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
  value = v
}, (target, property) => {
  console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
复制代码

虚拟 DOM

虚拟 DOM 涉及的内容不少,具体能够参考我以前 写的文章

路由鉴权

  • 登陆页和其余页面分开,登陆之后实例化 Vue 而且初始化须要的路由
  • 动态路由,经过 addRoute 实现

Vue 和 React 区别

  • Vue 表单支持双向绑定开发更方便
  • 改变数据方式不一样,setState 有使用坑
  • props Vue 可变,React 不可变
  • 判断是否须要更新 React 能够经过钩子函数判断,Vue 使用依赖追踪,修改了什么才渲染什么
  • React 16之后 有些钩子函数会执行屡次
  • React 须要使用 JSX,须要 Babel 编译。Vue 虽然能够使用模板,可是也能够经过直接编写 render 函数不须要编译就能运行。
  • 生态 React 相对较好

网络

TCP 3次握手

在 TCP 协议中,主动发起请求的一端为客户端,被动链接的一端称为服务端。无论是客户端仍是服务端,TCP 链接创建完后都能发送和接收数据,因此 TCP 也是一个全双工的协议。

起初,两端都为 CLOSED 状态。在通讯开始前,双方都会建立 TCB。 服务器建立完 TCB 后遍进入 LISTEN 状态,此时开始等待客户端发送数据。

第一次握手

客户端向服务端发送链接请求报文段。该报文段中包含自身的数据通信初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x 表示客户端的数据通讯初始序号。

第二次握手

服务端收到链接请求报文段后,若是赞成链接,则会发送一个应答,该应答中也会包含自身的数据通信初始序号,发送完成后便进入 SYN-RECEIVED 状态。

第三次握手

当客户端收到链接赞成的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时链接创建成功。

PS:第三次握手能够包含数据,经过 TCP 快速打开(TFO)技术。其实只要涉及到握手的协议,均可以使用相似 TFO 的方式,客户端和服务端存储相同 cookie,下次握手时发出 cookie 达到减小 RTT 的目的。

你是否有疑惑明明两次握手就能够创建起链接,为何还须要第三次应答?

由于这是为了防止失效的链接请求报文段被服务端接收,从而产生错误。

能够想象以下场景。客户端发送了一个链接请求 A,可是由于网络缘由形成了超时,这时 TCP 会启动超时重传的机制再次发送一个链接请求 B。此时请求顺利到达服务端,服务端应答完就创建了请求。若是链接请求 A 在两端关闭后终于抵达了服务端,那么这时服务端会认为客户端又须要创建 TCP 链接,从而应答了该请求并进入 ESTABLISHED 状态。此时客户端实际上是 CLOSED 状态,那么就会致使服务端一直等待,形成资源的浪费。

PS:在创建链接中,任意一端掉线,TCP 都会重发 SYN 包,通常会重试五次,在创建链接中可能会遇到 SYN FLOOD 攻击。遇到这种状况你能够选择调低重试次数或者干脆在不能处理的状况下拒绝请求。

TCP 拥塞控制

拥塞处理和流量控制不一样,后者是做用于接收方,保证接收方来得及接受数据。而前者是做用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的状况。

拥塞处理包括了四个算法,分别为:慢开始,拥塞避免,快速重传,快速恢复。

慢开始算法

慢开始算法,顾名思义,就是在传输开始时将发送窗口慢慢指数级扩大,从而避免一开始就传输大量数据致使网络拥塞。

慢开始算法步骤具体以下

  1. 链接初始设置拥塞窗口(Congestion Window) 为 1 MSS(一个分段的最大数据量)
  2. 每过一个 RTT 就将窗口大小乘二
  3. 指数级增加确定不能没有限制的,因此有一个阈值限制,当窗口大小大于阈值时就会启动拥塞避免算法。

拥塞避免算法

拥塞避免算法相比简单点,每过一个 RTT 窗口大小只加一,这样可以避免指数级增加致使网络拥塞,慢慢将大小调整到最佳值。

在传输过程当中可能定时器超时的状况,这时候 TCP 会认为网络拥塞了,会立刻进行如下步骤:

  • 将阈值设为当前拥塞窗口的一半
  • 将拥塞窗口设为 1 MSS
  • 启动拥塞避免算法

快速重传

快速重传通常和快恢复一块儿出现。一旦接收端收到的报文出现失序的状况,接收端只会回复最后一个顺序正确的报文序号(没有 Sack 的状况下)。若是收到三个重复的 ACK,无需等待定时器超时再重发而是启动快速重传。具体算法分为两种:

TCP Taho 实现以下

  • 将阈值设为当前拥塞窗口的一半
  • 将拥塞窗口设为 1 MSS
  • 从新开始慢开始算法

TCP Reno 实现以下

  • 拥塞窗口减半
  • 将阈值设为当前拥塞窗口
  • 进入快恢复阶段(重发对端须要的包,一旦收到一个新的 ACK 答复就退出该阶段)
  • 使用拥塞避免算法

TCP New Ren 改进后的快恢复

TCP New Reno 算法改进了以前 TCP Reno 算法的缺陷。在以前,快恢复中只要收到一个新的 ACK 包,就会退出快恢复。

TCP New Reno 中,TCP 发送方先记下三个重复 ACK 的分段的最大序号。

假如我有一个分段数据是 1 ~ 10 这十个序号的报文,其中丢失了序号为 3 和 7 的报文,那么该分段的最大序号就是 10。发送端只会收到 ACK 序号为 3 的应答。这时候重发序号为 3 的报文,接收方顺利接收并会发送 ACK 序号为 7 的应答。这时候 TCP 知道对端是有多个包未收到,会继续发送序号为 7 的报文,接收方顺利接收并会发送 ACK 序号为 11 的应答,这时发送端认为这个分段接收端已经顺利接收,接下来会退出快恢复阶段。

HTTPS 握手

HTTPS 仍是经过了 HTTP 来传输信息,可是信息经过 TLS 协议进行了加密。

TLS

TLS 协议位于传输层之上,应用层之下。首次进行 TLS 协议传输须要两个 RTT ,接下来能够经过 Session Resumption 减小到一个 RTT。

在 TLS 中使用了两种加密技术,分别为:对称加密和非对称加密。

对称加密

对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。

非对称加密

有公钥私钥之分,公钥全部人均可以知道,能够将数据用公钥加密,可是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道。

TLS 握手过程以下图:

  1. 客户端发送一个随机值,须要的协议和加密方式
  2. 服务端收到客户端的随机值,本身也产生一个随机值,并根据客户端需求的协议和加密方式来使用对应的方式,发送本身的证书(若是须要验证客户端证书须要说明)
  3. 客户端收到服务端的证书并验证是否有效,验证经过会再生成一个随机值,经过服务端证书的公钥去加密这个随机值并发送给服务端,若是服务端须要验证客户端证书的话会附带证书
  4. 服务端收到加密过的随机值并使用私钥解密得到第三个随机值,这时候两端都拥有了三个随机值,能够经过这三个随机值按照以前约定的加密方式生成密钥,接下来的通讯就能够经过该密钥来加密解密

经过以上步骤可知,在 TLS 握手阶段,两端使用非对称加密的方式来通讯,可是由于非对称加密损耗的性能比对称加密大,因此在正式传输数据时,两端使用对称加密的方式通讯。

PS:以上说明的都是 TLS 1.2 协议的握手状况,在 1.3 协议中,首次创建链接只须要一个 RTT,后面恢复链接不须要 RTT 了。

从输入 URL 到页面加载全过程

  1. 首先作 DNS 查询,若是这一步作了智能 DNS 解析的话,会提供访问速度最快的 IP 地址回来
  2. 接下来是 TCP 握手,应用层会下发数据给传输层,这里 TCP 协议会指明两端的端口号,而后下发给网络层。网络层中的 IP 协议会肯定 IP 地址,而且指示了数据传输中如何跳转路由器。而后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了
  3. TCP 握手结束后会进行 TLS 握手,而后就开始正式的传输数据
  4. 数据在进入服务端以前,可能还会先通过负责负载均衡的服务器,它的做用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个 HTML 文件
  5. 首先浏览器会判断状态码是什么,若是是 200 那就继续解析,若是 400 或 500 的话就会报错,若是 300 的话会进行重定向,这里会有个重定向计数器,避免过屡次的重定向,超过次数也会报错
  6. 浏览器开始解析文件,若是是 gzip 格式的话会先解压一下,而后经过文件的编码格式知道该如何去解码文件
  7. 文件解码成功后会正式开始渲染流程,先会根据 HTML 构建 DOM 树,有 CSS 的话会去构建 CSSOM 树。若是遇到 script 标签的话,会判断是否存在 async 或者 defer ,前者会并行进行下载并执行 JS,后者会先下载文件,而后等待 HTML 解析完成后顺序执行,若是以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里若是使用 HTTP 2.0 协议的话会极大的提升多图的下载效率。
  8. 初始的 HTML 被彻底加载和解析后会触发 DOMContentLoaded 事件
  9. CSSOM 树和 DOM 树构建完成后会开始生成 Render 树,这一步就是肯定页面元素的布局、样式等等诸多方面的东西
  10. 在生成 Render 树的过程当中,浏览器就开始调用 GPU 绘制,合成图层,将内容显示在屏幕上了

HTTP 经常使用返回码

2XX 成功

  • 200 OK,表示从客户端发来的请求在服务器端被正确处理
  • 204 No content,表示请求成功,但响应报文不含实体的主体部分
  • 205 Reset Content,表示请求成功,但响应报文不含实体的主体部分,可是与 204 响应不一样在于要求请求方重置内容
  • 206 Partial Content,进行范围请求

3XX 重定向

  • 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
  • 302 found,临时性重定向,表示资源临时被分配了新的 URL
  • 303 see other,表示资源存在着另外一个 URL,应使用 GET 方法获取资源
  • 304 not modified,表示服务器容许访问资源,但因发生请求未知足条件的状况
  • 307 temporary redirect,临时重定向,和302含义相似,可是指望客户端保持请求方法不变向新的地址发出请求

4XX 客户端错误

  • 400 bad request,请求报文存在语法错误
  • 401 unauthorized,表示发送的请求须要有经过 HTTP 认证的认证信息
  • 403 forbidden,表示对请求资源的访问被服务器拒绝
  • 404 not found,表示在服务器上没有找到请求的资源

5XX 服务器错误

  • 500 internal sever error,表示服务器端在执行请求时发生了错误
  • 501 Not Implemented,表示服务器不支持当前请求所须要的某个功能
  • 503 service unavailable,代表服务器暂时处于超负载或正在停机维护,没法处理请求

数据结构算法

常见排序

如下两个函数是排序中会用到的通用函数,就不一一写了

function checkArray(array) {
    if (!array || array.length <= 2) return
}
function swap(array, left, right) {
    let rightValue = array[right]
    array[right] = array[left]
    array[left] = rightValue
}
复制代码

冒泡排序

冒泡排序的原理以下,从第一个元素开始,把当前元素和下一个索引元素进行比较。若是当前元素大,那么就交换位置,重复操做直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操做,可是此时最后一个元素已是最大数了,因此不须要再比较最后一个元素,只须要比较到 length - 1 的位置。

如下是实现该算法的代码

function bubble(array) {
  checkArray(array);
  for (let i = array.length - 1; i > 0; i--) {
    // 从 0 到 `length - 1` 遍历
    for (let j = 0; j < i; j++) {
      if (array[j] > array[j + 1]) swap(array, j, j + 1)
    }
  }
  return array;
}
复制代码

该算法的操做次数是一个等差数列 n + (n - 1) + (n - 2) + 1 ,去掉常数项之后得出时间复杂度是 O(n * n)

插入排序

插入排序的原理以下。第一个元素默认是已排序元素,取出下一个元素和当前元素比较,若是当前元素大就交换位置。那么此时第一个元素就是当前的最小数,因此下次取出操做从第三个元素开始,向前对比,重复以前的操做。

如下是实现该算法的代码

function insertion(array) {
  checkArray(array);
  for (let i = 1; i < array.length; i++) {
    for (let j = i - 1; j >= 0 && array[j] > array[j + 1]; j--)
      swap(array, j, j + 1);
  }
  return array;
}
复制代码

该算法的操做次数是一个等差数列 n + (n - 1) + (n - 2) + 1 ,去掉常数项之后得出时间复杂度是 O(n * n)

选择排序

选择排序的原理以下。遍历数组,设置最小值的索引为 0,若是取出的值比当前最小值小,就替换最小值索引,遍历完成后,将第一个元素和最小值索引上的值交换。如上操做后,第一个元素就是数组中的最小值,下次遍历就能够从索引 1 开始重复上述操做。

如下是实现该算法的代码

function selection(array) {
  checkArray(array);
  for (let i = 0; i < array.length - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < array.length; j++) {
      minIndex = array[j] < array[minIndex] ? j : minIndex;
    }
    swap(array, i, minIndex);
  }
  return array;
}
复制代码

该算法的操做次数是一个等差数列 n + (n - 1) + (n - 2) + 1 ,去掉常数项之后得出时间复杂度是 O(n * n)

归并排序

归并排序的原理以下。递归的将数组两两分开直到最多包含两个元素,而后将数组排序合并,最终合并为排序好的数组。假设我有一组数组 [3, 1, 2, 8, 9, 7, 6],中间数索引是 3,先排序数组 [3, 1, 2, 8] 。在这个左边数组上,继续拆分直到变成数组包含两个元素(若是数组长度是奇数的话,会有一个拆分数组只包含一个元素)。而后排序数组 [3, 1][2, 8] ,而后再排序数组 [1, 3, 2, 8] ,这样左边数组就排序完成,而后按照以上思路排序右边数组,最后将数组 [1, 2, 3, 8][6, 7, 9] 排序。

如下是实现该算法的代码

function sort(array) {
  checkArray(array);
  mergeSort(array, 0, array.length - 1);
  return array;
}

function mergeSort(array, left, right) {
  // 左右索引相同说明已经只有一个数
  if (left === right) return;
  // 等同于 `left + (right - left) / 2`
  // 相比 `(left + right) / 2` 来讲更加安全,不会溢出
  // 使用位运算是由于位运算比四则运算快
  let mid = parseInt(left + ((right - left) >> 1));
  mergeSort(array, left, mid);
  mergeSort(array, mid + 1, right);

  let help = [];
  let i = 0;
  let p1 = left;
  let p2 = mid + 1;
  while (p1 <= mid && p2 <= right) {
    help[i++] = array[p1] < array[p2] ? array[p1++] : array[p2++];
  }
  while (p1 <= mid) {
    help[i++] = array[p1++];
  }
  while (p2 <= right) {
    help[i++] = array[p2++];
  }
  for (let i = 0; i < help.length; i++) {
    array[left + i] = help[i];
  }
  return array;
}
复制代码

以上算法使用了递归的思想。递归的本质就是压栈,每递归执行一次函数,就将该函数的信息(好比参数,内部的变量,执行到的行数)压栈,直到遇到终止条件,而后出栈并继续执行函数。对于以上递归函数的调用轨迹以下

mergeSort(data, 0, 6) // mid = 3
  mergeSort(data, 0, 3) // mid = 1
    mergeSort(data, 0, 1) // mid = 0
      mergeSort(data, 0, 0) // 遇到终止,回退到上一步
    mergeSort(data, 1, 1) // 遇到终止,回退到上一步
    // 排序 p1 = 0, p2 = mid + 1 = 1
    // 回退到 `mergeSort(data, 0, 3)` 执行下一个递归
  mergeSort(2, 3) // mid = 2
    mergeSort(3, 3) // 遇到终止,回退到上一步
  // 排序 p1 = 2, p2 = mid + 1 = 3
  // 回退到 `mergeSort(data, 0, 3)` 执行合并逻辑
  // 排序 p1 = 0, p2 = mid + 1 = 2
  // 执行完毕回退
  // 左边数组排序完毕,右边也是如上轨迹
复制代码

该算法的操做次数是能够这样计算:递归了两次,每次数据量是数组的一半,而且最后把整个数组迭代了一次,因此得出表达式 2T(N / 2) + T(N) (T 表明时间,N 表明数据量)。根据该表达式能够套用 该公式 得出时间复杂度为 O(N * logN)

快排

快排的原理以下。随机选取一个数组中的值做为基准值,从左至右取值与基准值对比大小。比基准值小的放数组左边,大的放右边,对比完成后将基准值和第一个比基准值大的值交换位置。而后将数组以基准值的位置分为两部分,继续递归以上操做。

如下是实现该算法的代码

function sort(array) {
  checkArray(array);
  quickSort(array, 0, array.length - 1);
  return array;
}

function quickSort(array, left, right) {
  if (left < right) {
    swap(array, , right)
    // 随机取值,而后和末尾交换,这样作比固定取一个位置的复杂度略低
    let indexs = part(array, parseInt(Math.random() * (right - left + 1)) + left, right);
    quickSort(array, left, indexs[0]);
    quickSort(array, indexs[1] + 1, right);
  }
}
function part(array, left, right) {
  let less = left - 1;
  let more = right;
  while (left < more) {
    if (array[left] < array[right]) {
      // 当前值比基准值小,`less` 和 `left` 都加一
	   ++less;
       ++left;
    } else if (array[left] > array[right]) {
      // 当前值比基准值大,将当前值和右边的值交换
      // 而且不改变 `left`,由于当前换过来的值尚未判断过大小
      swap(array, --more, left);
    } else {
      // 和基准值相同,只移动下标
      left++;
    }
  }
  // 将基准值和比基准值大的第一个值交换位置
  // 这样数组就变成 `[比基准值小, 基准值, 比基准值大]`
  swap(array, right, more);
  return [less, more];
}
复制代码

该算法的复杂度和归并排序是相同的,可是额外空间复杂度比归并排序少,只需 O(logN),而且相比归并排序来讲,所需的常数时间也更少。

面试题

Sort Colors:该题目来自 LeetCode,题目须要咱们将 [2,0,2,1,1,0] 排序成 [0,0,1,1,2,2] ,这个问题就能够使用三路快排的思想。

如下是代码实现

var sortColors = function(nums) {
  let left = -1;
  let right = nums.length;
  let i = 0;
  // 下标若是遇到 right,说明已经排序完成
  while (i < right) {
    if (nums[i] == 0) {
      swap(nums, i++, ++left);
    } else if (nums[i] == 1) {
      i++;
    } else {
      swap(nums, i, --right);
    }
  }
};
复制代码

Kth Largest Element in an Array:该题目来自 LeetCode,题目须要找出数组中第 K 大的元素,这问题也能够使用快排的思路。而且由于是找出第 K 大元素,因此在分离数组的过程当中,能够找出须要的元素在哪边,而后只须要排序相应的一边数组就好。

如下是代码实现

var findKthLargest = function(nums, k) {
  let l = 0
  let r = nums.length - 1
  // 得出第 K 大元素的索引位置
  k = nums.length - k
  while (l < r) {
    // 分离数组后得到比基准树大的第一个元素索引
    let index = part(nums, l, r)
    // 判断该索引和 k 的大小
    if (index < k) {
      l = index + 1
    } else if (index > k) {
      r = index - 1
    } else {
      break
    }
  }
  return nums[k]
};
function part(array, left, right) {
  let less = left - 1;
  let more = right;
  while (left < more) {
    if (array[left] < array[right]) {
	   ++less;
       ++left;
    } else if (array[left] > array[right]) {
      swap(array, --more, left);
    } else {
      left++;
    }
  }
  swap(array, right, more);
  return more;
}
复制代码

堆排序

堆排序利用了二叉堆的特性来作,二叉堆一般用数组表示,而且二叉堆是一颗彻底二叉树(全部叶节点(最底层的节点)都是从左往右顺序排序,而且其余层的节点都是满的)。二叉堆又分为大根堆与小根堆。

  • 大根堆是某个节点的全部子节点的值都比他小
  • 小根堆是某个节点的全部子节点的值都比他大

堆排序的原理就是组成一个大根堆或者小根堆。以小根堆为例,某个节点的左边子节点索引是 i * 2 + 1,右边是 i * 2 + 2,父节点是 (i - 1) /2

  1. 首先遍历数组,判断该节点的父节点是否比他小,若是小就交换位置并继续判断,直到他的父节点比他大
  2. 从新以上操做 1,直到数组首位是最大值
  3. 而后将首位和末尾交换位置并将数组长度减一,表示数组末尾已经是最大值,不须要再比较大小
  4. 对比左右节点哪一个大,而后记住大的节点的索引而且和父节点对比大小,若是子节点大就交换位置
  5. 重复以上操做 3 - 4 直到整个数组都是大根堆。

如下是实现该算法的代码

function heap(array) {
  checkArray(array);
  // 将最大值交换到首位
  for (let i = 0; i < array.length; i++) {
    heapInsert(array, i);
  }
  let size = array.length;
  // 交换首位和末尾
  swap(array, 0, --size);
  while (size > 0) {
    heapify(array, 0, size);
    swap(array, 0, --size);
  }
  return array;
}

function heapInsert(array, index) {
  // 若是当前节点比父节点大,就交换
  while (array[index] > array[parseInt((index - 1) / 2)]) {
    swap(array, index, parseInt((index - 1) / 2));
    // 将索引变成父节点
    index = parseInt((index - 1) / 2);
  }
}
function heapify(array, index, size) {
  let left = index * 2 + 1;
  while (left < size) {
    // 判断左右节点大小
    let largest =
      left + 1 < size && array[left] < array[left + 1] ? left + 1 : left;
    // 判断子节点和父节点大小
    largest = array[index] < array[largest] ? largest : index;
    if (largest === index) break;
    swap(array, index, largest);
    index = largest;
    left = index * 2 + 1;
  }
}
复制代码

以上代码实现了小根堆,若是须要实现大根堆,只须要把节点对比反一下就好。

该算法的复杂度是 O(logN)

系统自带排序实现

每一个语言的排序内部实现都是不一样的。

对于 JS 来讲,数组长度大于 10 会采用快排,不然使用插入排序 源码实现 。选择插入排序是由于虽然时间复杂度不好,可是在数据量很小的状况下和 O(N * logN)相差无几,然而插入排序须要的常数时间很小,因此相对别的排序来讲更快。

对于 Java 来讲,还会考虑内部的元素的类型。对于存储对象的数组来讲,会采用稳定性好的算法。稳定性的意思就是对于相同值来讲,相对顺序不能改变。

搜索二叉树

其余

介绍下设计模式

工厂模式

工厂模式分为好几种,这里就不一一讲解了,如下是一个简单工厂模式的例子

class Man {
  constructor(name) {
    this.name = name
  }
  alertName() {
    alert(this.name)
  }
}

class Factory {
  static create(name) {
    return new Man(name)
  }
}

Factory.create('yck').alertName()
复制代码

固然工厂模式并不只仅是用来 new 出实例

能够想象一个场景。假设有一份很复杂的代码须要用户去调用,可是用户并不关心这些复杂的代码,只须要你提供给我一个接口去调用,用户只负责传递须要的参数,至于这些参数怎么使用,内部有什么逻辑是不关心的,只须要你最后返回我一个实例。这个构造过程就是工厂。

工厂起到的做用就是隐藏了建立实例的复杂度,只须要提供一个接口,简单清晰。

在 Vue 源码中,你也能够看到工厂模式的使用,好比建立异步组件

export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {
    
    // 逻辑处理...
  
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )

  return vnode
}
复制代码

在上述代码中,咱们能够看到咱们只须要调用 createComponent 传入参数就能建立一个组件实例,可是建立这个实例是很复杂的一个过程,工厂帮助咱们隐藏了这个复杂的过程,只须要一句代码调用就能实现功能。

单例模式

单例模式很经常使用,好比全局缓存、全局状态管理等等这些只须要一个对象,就能够使用单例模式。

单例模式的核心就是保证全局只有一个对象能够访问。由于 JS 是门无类的语言,因此别的语言实现单例的方式并不能套入 JS 中,咱们只须要用一个变量确保实例只建立一次就行,如下是如何实现单例模式的例子

class Singleton {
  constructor() {}
}

Singleton.getInstance = (function() {
  let instance
  return function() {
    if (!instance) {
      instance = new Singleton()
    }
    return instance
  }
})()

let s1 = Singleton.getInstance()
let s2 = Singleton.getInstance()
console.log(s1 === s2) // true
复制代码

在 Vuex 源码中,你也能够看到单例模式的使用,虽然它的实现方式不大同样,经过一个外部变量来控制只安装一次 Vuex

let Vue // bind on install

export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    // ...
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
复制代码

适配器模式

适配器用来解决两个接口不兼容的状况,不须要改变已有的接口,经过包装一层的方式实现两个接口的正常协做。

如下是如何实现适配器模式的例子

class Plug {
  getName() {
    return '港版插头'
  }
}

class Target {
  constructor() {
    this.plug = new Plug()
  }
  getName() {
    return this.plug.getName() + ' 适配器转二脚插头'
  }
}

let target = new Target()
target.getName() // 港版插头 适配器转二脚插头
复制代码

在 Vue 中,咱们其实常用到适配器模式。好比父组件传递给子组件一个时间戳属性,组件内部须要将时间戳转为正常的日期显示,通常会使用 computed 来作转换这件事情,这个过程就使用到了适配器模式。

装饰模式

装饰模式不须要改变已有的接口,做用是给对象添加功能。就像咱们常常须要给手机戴个保护套防摔同样,不改变手机自身,给手机添加了保护套提供防摔功能。

如下是如何实现装饰模式的例子,使用了 ES7 中的装饰器语法

function readonly(target, key, descriptor) {
  descriptor.writable = false
  return descriptor
}

class Test {
  @readonly
  name = 'yck'
}

let t = new Test()

t.yck = '111' // 不可修改
复制代码

在 React 中,装饰模式其实随处可见

import { connect } from 'react-redux'
class MyComponent extends React.Component {
    // ...
}
export default connect(mapStateToProps)(MyComponent)
复制代码

代理模式

代理是为了控制对对象的访问,不让外部直接访问到对象。在现实生活中,也有不少代理的场景。好比你须要买一件国外的产品,这时候你能够经过代购来购买产品。

在实际代码中其实代理的场景不少,也就不举框架中的例子了,好比事件代理就用到了代理模式。

<ul id="ul">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
<script> let ul = document.querySelector('#ul') ul.addEventListener('click', (event) => { console.log(event.target); }) </script>
复制代码

由于存在太多的 li,不可能每一个都去绑定事件。这时候能够经过给父节点绑定一个事件,让父节点做为代理去拿到真实点击的节点。

发布-订阅模式

发布-订阅模式也叫作观察者模式。经过一对一或者一对多的依赖关系,当对象发生改变时,订阅方都会收到通知。在现实生活中,也有不少相似场景,好比我须要在购物网站上购买一个产品,可是发现该产品目前处于缺货状态,这时候我能够点击有货通知的按钮,让网站在产品有货的时候经过短信通知我。

在实际代码中其实发布-订阅模式也很常见,好比咱们点击一个按钮触发了点击事件就是使用了该模式

<ul id="ul"></ul>
<script>
    let ul = document.querySelector('#ul')
    ul.addEventListener('click', (event) => {
        console.log(event.target);
    })
</script>
复制代码

在 Vue 中,如何实现响应式也是使用了该模式。对于须要实现响应式的对象来讲,在 get 的时候会进行依赖收集,当改变了对象的属性时,就会触发派发更新。

若是你对于如何实现响应式还有疑问,能够阅读我以前的文章 深度解析 Vue 响应式原理

最后

若是你认为还有什么好的题目能够贡献,也能够在评论中提出

你可能会疑问我怎么写出 25K 的文字的,其实不少面试题均可以在个人万星项目中找到答案,如下是 项目地址

若是你想学习到更多的前端知识、面试技巧或者一些我我的的感悟,能够关注个人公众号一块儿学习

了解掘金秋招求职征文活动更多信息👉秋招求职时,写文就有好礼相送 | 掘金技术征文

相关文章
相关标签/搜索