20210329某直播公司总结

144 阅读30分钟

目录

  1. cookie的处理流程
  2. 说一下从url输入到返回请求的过程
  3. 说下数组去重的几种方式
  4. ts中interface可以继承吗
  5. ts中interface可以写多个吗
  6. ts的泛型有什么作用
  7. new的原理是什么
  8. 防抖和节流的区别是什么?防抖和节流的实现
  9. 发布订阅模式
  10. hash路由和history路由
  11. 如何正确的判断this
  12. 跨域的处理方式
  13. 类型检测的几种方式
  14. 项目中为何使用json化配置
  15. React的生命周期
  16. setState到底是异步还是同步
  17. promise all和rase有什么区别
  18. useRef
  19. 虚拟列表的实现
  20. 如何模拟class
  21. 事件轮询

1. cookie的处理流程

  1. 使用浏览器访问服务端页面;
  2. 服务端收到该客户端第一次请求后,会创建一个session,生产一个唯一sessionId;
  3. 同时在响应请求中设置cookie,属性名为jessionid;
  4. 客户端收到后会保存jessionid,再次请求时,会在header中设置,服务端可从请求头中获取;
  5. 服务端验证获取的sessionId是否存在,即可验证是否是同一用户;

2. 说一下从url输入到返回请求的过程

首先是DNS查询,如果这一步做了智能DNS解析的话,会提供访问速度最快的IP地址回来。

DNS的作用就是通过域名查询到具体的IP。

因为IP存在数字和英文的组合(IPv6),很不利于人类记忆,所以就出现了域名。你可以把域名看成是某个IP的别名,DNS就是去查询这个别名的真正名称是什么。

在TCP握手之前就已经进行了DNS查询,这个查询是操作系统自己做的。当你在浏览器中想访问www.baidu.com时,会进行一下操作:

  1. 操作系统会首先在本地缓存中查询IP
  2. 没有的话会去系统配置的DNS服务器中查询
  3. 如果这时候还没得话,会直接去DNS根服务器查询,这一步查询会找出负责com这个一级域名的服务器
  4. 然后去该服务器查询baidu这个二级域名
  5. 接下来三级域名的查询其实是我们配置的,你可以给www这个域名配置一个IP,然后还可以给别的三级域名配置一个IP

以上介绍的是DNS迭代查询,还有种是递归查询,区别就是前者是由客户端去做请求,后者是由系统配置的DNS服务器做请求,得到结果后将数据返回给客户端。

PS:DNS是基于UDP做的查询,大家也可以考虑下为什么之前不考虑使用TCP去实现。

接下来是TCP握手,应用层会下发数据给传输层,这里TCP协议会指明两端的端口号,然后下发给网络层。网络层中的IP协议会确定IP地址,并且指示了数据传输中如何跳转路由器。然后包会再被封装到数据链路层的数据帧结构中,最后就是物理层面的传输了。

在这一部分中,可以详细说下TCP的握手情况以及TCP的一些特性。

当TCP握手结束后就会进行TLS握手,然后就开始正式的传输数据。

在这一部分中,可以详细说下TLS的握手情况以及两种加密方式的内容。

数据在进入服务端之前,可能还会先经过负责负载均衡的服务器,它的作用就是将请求合理的分发到多台服务器上,这时假设服务端会响应一个HTML文件。

首先浏览器会判断状态码是什么,如果是200那就继续解析,如果400或500的话就会报错,如果300的话会进行重定向,这里会有个重定向计数器,避免过多次的重定向,超过次数也会报错。

浏览器开始解析文件,如果是gzip格式的话会先解压一下,然后通过文件的编码格式知道该如何去解码文件。

文件解码成功后会正式开始渲染流程,先会根据HTML构建DOM树,有CSS的话会去构建CSSOM树。如果遇到script标签的话,会判断是否存在async或者defer,前者会并行进行下载并执行JS,后者会先下载文件,然后等待HTML解析完成后顺序执行。

如果以上都没有,就会阻塞住渲染流程直到JS执行完毕。遇到文件下载的会去下载文件,这里如果使用HTTP/2协议的话会极大的提高多图的下载效率。

CSSOM树和DOM树构建完成后会开始生成Render树,这一步就是确定页面元素的布局、样式等等诸多方面的东西

在生成Render树的过程中,浏览器就开始调用GPU绘制,合成图层,将内容显示在屏幕上了。

3. 说下数组去重的几种方式

  1. 利用ES6 Set去重
function unique (arr) {
  return Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
 //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

不考虑兼容性,这种去重的方法代码最少。这种方法还无法去掉“{}”空对象,后面的高阶方法会添加去掉重复“{}”的方法。

  1. 利用for嵌套for,然后splice去重
function unique(arr) {
  for (var i = 0; i < arr.length; i++) {
    for (var j = i + 1; j < arr.length; j++) {
      if (arr[i] == arr[j]) {         //第一个等同于第二个,splice方法删除第二个
        arr.splice(j, 1);
        j--;
      }
    }
  }
  return arr;
}
var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr))
//[1, "true", 15, false, undefined, NaN, NaN, "NaN", "a", {…}, {…}]     //NaN和{}没有去重,两个null直接消失了

双层循环,外层循环元素,内层循环时比较值。值相同时,则删去这个值。

  1. 利用indexOf去重
function unique(arr) {
  if (!Array.isArray(arr)) {
    console.log('type error!')
    return
  }
  var array = [];
  for (var i = 0; i < arr.length; i++) {
    if (array.indexOf(arr[i]) === -1) {
      array.push(arr[i])
    }
  }
  return array;
}
var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr))
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}]  //NaN、{}没有去重

新建一个空的结果数组,for 循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。

  1. 利用sort()
function unique(arr) {
  if (!Array.isArray(arr)) {
    console.log('type error!')
    return;
  }
  arr = arr.sort()
  var arrry = [arr[0]];
  for (var i = 1; i < arr.length; i++) {
    if (arr[i] !== arr[i - 1]) {
      arrry.push(arr[i]);
    }
  }
  return arrry;
}
var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr))
// [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined]      //NaN、{}没有去重

利用sort()排序方法,然后根据排序后的结果进行遍历及相邻元素比对。

  1. 利用hasOwnProperty
function unique(arr) {
  var obj = {};
  return arr.filter(function(item, index, arr){
      return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
  })
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}]   //所有的都去重了

利用hasOwnProperty 判断是否存在对象属性

  1. 利用filter
function unique(arr) {
  return arr.filter(function (item, index, arr) {
    //当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
    return arr.indexOf(item, 0) === index;
  });
}
var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr))
//[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {…}, {…}]
  1. 利用递归去重
function unique(arr) {
  var array = arr;
  var len = array.length;

  array.sort(function (a, b) {   //排序后更加方便去重
    return a - b;
  })

  function loop(index) {
    if (index >= 1) {
      if (array[index] === array[index - 1]) {
        array.splice(index, 1);
      }
      loop(index - 1);    //递归loop,然后数组去重
    }
  }
  loop(len - 1);
  return array;
}
var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr));
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]
  1. 利用Map数据结构去重
function arrayNonRepeatfy(arr) {
  let map = new Map();
  let array = new Array();  // 数组用于返回结果
  for (let i = 0; i < arr.length; i++) {
    if (map.has(arr[i])) {  // 如果有该key值
      map.set(arr[i], true);
    } else {
      map.set(arr[i], false);   // 如果没有该key值
      array.push(arr[i]);
    }
  }
  return array;
}
var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr));
//[1, "a", "true", true, 15, false, 1, {…}, null, NaN, NaN, "NaN", 0, "a", {…}, undefined]
  1. 利用reduce+includes
function unique(arr) {
  return arr.reduce((prev, cur) => prev.includes(cur) ? prev : [...prev, cur], []);
}
var arr = [1, 1, 'true', 'true', true, true, 15, 15, false, false, undefined, undefined, null, null, NaN, NaN, 'NaN', 0, 0, 'a', 'a', {}, {}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]

4. ts中interface可以继承吗

可以的

5. ts中interface可以写多个吗

可以的,最后会合并

6. ts的泛型有什么作用

我给的答案是泛型决定了一个类型在不同的场景下能够在每个场景下从始至终的保持类型一致

7. new的原理是什么

  1. 创建一个新对象。
  2. 这个新对象会被执行[[原型]]连接。
  3. 将构造函数的作用域赋值给新对象,即this指向这个新对象.
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function myNew(func) {
  let target = {};
  target.__proto__ = func.prototype;
  let res = func.call(target);
  if (typeof (res) === 'object' || typeof (res) === 'function') {
    return res;
  }
  return target;
}

字面量创建对象,不会调用Object构造函数, 简洁且性能更好;

new Object()方式创建对象本质上是方法调用,涉及到在proto链中遍历该方法,当找到该方法后,又会生产方法调用必须的堆栈信息,方法调用结束后,还要释放该堆栈,性能不如字面量的方式。

通过对象字面量定义对象时,不会调用Object构造函数。

8. 防抖和节流的区别是什么?防抖和节流的实现。

浏览器的 resize、scroll、keypress、mousemove 等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。为了优化体验,需要对这类事件进行调用次数的限制。

  • 狂点一个按钮
  • 页面滚动
  • 入模糊匹配
  • 。。。。。。
防抖(debounce)

在某一次高频触发下,我们只识别一次(可以控制开始触发,还是最后一次触发)
详细:假设我们规定500MS触发多次算是高频,只要我们检测到是高频触发了,则在本次频繁操作下(哪怕你操作了10次)也是只触发一次...

<button id="submit">点击</button>
function () {
  console.log("hello world")
}

上面按钮每一次点击,都会触发打印,如果疯狂点击,那么下面就会疯狂打印,会降低性能。

实际开发1:使用按钮只会,移除事件绑定

<button id="submit">点击</button>
function handle() {
    submit.onclick = null;
    submit.disabled = true;
    console.log('OK');
    setTimeout(() => {
      submit.onclick = handle;
      submit.disabled = false;
    }, 1000);
}
submit.onclick = handle;

实际开发2:使用按钮只会,移除事件绑定

<button id="submit">点击</button>
let flag = false;
submit.onclick = function () {
  if (flag) return;
  flag = true;
  console.log('OK');
  setTimeout(() => {
    // 事情处理完
    flag = false;
  }, 1000);
};

封装一个函数防抖的方法

<button id="submit">点击</button>
function debounce(func, wait, immediate) {
  // 多个参数及传递默认的处理
  if (typeof func !== "function") throw new TypeError("func must be an function!");
  if (typeof wait === "undefined") wait = 500;
  if (typeof wait === "boolean") {
    immediate = wait;
    wait = 500;
  }
  if (typeof immediate !== "boolean") immediate = false;

  // 设定定时器返回值标识
  let timer = null;
  return function proxy(...params) {
    let self = this,
      now = immediate && !timer;

    clearTimeout(timer);
    timer = setTimeout(function () {
      timer = null;
      !immediate ? func.call(self, ...params) : null;
    }, wait);

    // 第一次触发就立即执行
    now ? func.call(self, ...params) : null;
  };
}

function handle(ev) {
  // 具体在点击的时候要处理的业务
  console.log('OK', this, ev);
}
submit.onclick = debounce(handle, true);
// submit.onclick = proxy;  疯狂点击的情况下,proxy会被疯狂执行,我们需要在proxy中根据频率管控handle的执行次数
// submit.onclick = handle; //handle->this:submit  传递一个事件对象

简洁版本的防抖

function myDebounce(fn, interval = 500) {
  let timeout = null;
  return function () {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      fn.apply(this, arguments);
    }, interval);
  };
}
function doSomething(){
    // onmousemove触发的事件回调函数
}
container.onmousemove = myDebounce(doSomething, 300);
节流(throttle)

在某一次高频触发下,我们不是只识别一次,按照我们设定的间隔时间(自己规定的频率),每到达这个频率都会触发一次
详细:假设我们规定频率是500MS,我们操作了10min,触发的次数=(10601000)/500

比如,滚动页面,触发了多次打印

body {
  height: 3000px;
  overflow-x: hidden;
  background: -webkit-linear-gradient(top left, lightblue, pink, orange);
}
window.onscroll = function () {
  // 默认情况下,页面滚动中:浏览器在最快的反应时间内(4~6MS),就会识别监听一次事件触发,把绑定的方法执行,这样导致方法执行的次数过多,造成不必要的资源浪费
  console.log('OK');
};

封装一个函数节流的方法

body {
  height: 3000px;
  overflow-x: hidden;
  background: -webkit-linear-gradient(top left, lightblue, pink, orange);
}
function throttle(func, wait) {
  if (typeof func !== "function") throw new TypeError("func must be an function!");
  if (typeof wait === "undefined") wait = 500;
  let timer = null,
    previous = 0; //记录上一次操作的时间
  return function proxy(...params) {
    let self = this,
      now = new Date(), //当前这次触发操作的时间
      remaining = wait - (now - previous);
    if (remaining <= 0) {
      // 两次间隔时间超过wait了,直接执行即可
      clearTimeout(timer);
      timer = null;
      previous = now;
      func.call(self, ...params);
    } else if (!timer) {
      // 两次触发的间隔时间没有超过wait,则设置定时器,让其等待remaining这么久之后执行一次「前提:没有设置过定时器」
      timer = setTimeout(function () {
        clearTimeout(timer);
        timer = null;
        previous = new Date();
        func.call(self, ...params);
      }, remaining);
    }
  };
}

function handle() {
  console.log('OK');
}
window.onscroll = throttle(handle, 500);

简洁版本的节流

function myThrottle(fn, interval = 500) {
  let run = true;
  return function () {
    if (!run) return;
    run = false;
    setTimeout(() => {
      fn.apply(this, arguments);
      run = true;
    }, interval);
  };
}

function doSomething(){
    // onmousemove触发的事件回调函数
}
container.onmousemove = myThrottle(doSomething, 300);

9. 发布订阅模式

我们可以把上面例子的几个核心概念提取一下,买家可以被认为是订阅者(Subscriber),售货员可以被认为是发布者(Publisher),售货员持有小本本(SubscriberMap),小本本上记录有买家订阅(subscribe)的不同鞋型(Type)的信息,当然也可以退订(unSubscribe),当鞋型有消息时售货员会给订阅了当前类型消息的订阅者发布(notify)消息。

主要有下面几个概念:

  1. Publisher:发布者,当消息发生时负责通知对应订阅者
  2. Subscriber:订阅者,当消息发生时被通知的对象
  3. SubscriberMap:持有不同type的数组,存储有所有订阅者的数组
  4. type:消息类型,订阅者可以订阅的不同消息类型
  5. subscribe:该方法为将订阅者添加到SubscriberMap中对应的数组中
  6. unSubscribe:该方法为在SubscriberMap中删除订阅者
  7. notify:该方法遍历通知SubscriberMap中对应type的每个订阅者

现在的结构如下图:

下面使用通用化的方法实现一下。

首先我们使用立即调用函数 IIFE(Immediately Invoked Function Expression) 方式来将不希望公开的 SubscriberMap 隐藏,然后可以将注册的订阅行为换为回调函数的形式,这样我们可以在消息通知时附带参数信息,在处理通知的时候也更灵活:

const Publisher = (function() {
  const _subsMap = {}   // 存储订阅者
  return {
    /* 消息订阅 */
    subscribe(type, cb) {
      if (_subsMap[type]) {
        if (!_subsMap[type].includes(cb))
          _subsMap[type].push(cb)
      } else _subsMap[type] = [cb]
    },
    /* 消息退订 */
    unsubscribe(type, cb) {
      if (!_subsMap[type] ||
        !_subsMap[type].includes(cb)) return
      const idx = _subsMap[type].indexOf(cb)
      _subsMap[type].splice(idx, 1)
    },
    /* 消息发布 */
    notify(type, ...payload) {
      if (!_subsMap[type]) return
      _subsMap[type].forEach(cb => cb(...payload))
    }
  }
})()

Publisher.subscribe('运动鞋', message => console.log('152xxx' + message))    // 订阅运动鞋
Publisher.subscribe('运动鞋', message => console.log('138yyy' + message))
Publisher.subscribe('帆布鞋', message => console.log('139zzz' + message))    // 订阅帆布鞋

Publisher.notify('运动鞋', ' 运动鞋到货了 ~')   // 打电话通知买家运动鞋消息
Publisher.notify('帆布鞋', ' 帆布鞋售罄了 T.T') // 打电话通知买家帆布鞋消息

// 输出:  152xxx 运动鞋到货了 ~
// 输出:  138yyy 运动鞋到货了 ~
// 输出:  139zzz 帆布鞋售罄了 T.T

上面是使用 IIFE 实现的,现在 ES6 如此流行,也可以使用 class 语法来改写一下:

class Publisher {
  constructor() {
    this._subsMap = {}
  }

  /* 消息订阅 */
  subscribe(type, cb) {
    if (this._subsMap[type]) {
      if (!this._subsMap[type].includes(cb))
        this._subsMap[type].push(cb)
    } else this._subsMap[type] = [cb]
  }

  /* 消息退订 */
  unsubscribe(type, cb) {
    if (!this._subsMap[type] ||
      !this._subsMap[type].includes(cb)) return
    const idx = this._subsMap[type].indexOf(cb)
    this._subsMap[type].splice(idx, 1)
  }

  /* 消息发布 */
  notify(type, ...payload) {
    if (!this._subsMap[type]) return
    this._subsMap[type].forEach(cb => cb(...payload))
  }
}

const adadis = new Publisher()

adadis.subscribe('运动鞋', message => console.log('152xxx' + message))    // 订阅运动鞋
adadis.subscribe('运动鞋', message => console.log('138yyy' + message))
adadis.subscribe('帆布鞋', message => console.log('139zzz' + message))    // 订阅帆布鞋

adadis.notify('运动鞋', ' 运动鞋到货了 ~')   // 打电话通知买家运动鞋消息
adadis.notify('帆布鞋', ' 帆布鞋售罄了 T.T') // 打电话通知买家帆布鞋消息

// 输出:  152xxx 运动鞋到货了 ~
// 输出:  138yyy 运动鞋到货了 ~
// 输出:  139zzz 帆布鞋售罄了 T.T

10. hash路由和history路由

Browser包含window对象,Navigator对象,Screen对象,History对象,Location对象等,其中window对象表示浏览器打开的窗口;Navigator对象包含浏览器的相关信息,其常用的属性有navigator.userAgent获取浏览器内核等信息;Screen对象包含客户端显示屏幕的信息,如screen.height或screen.width获取宽高;history对象包含访问过的URL,是window对象的一部分,有三个方法,back(),forward(),go(),调用这三个方法,浏览器会加载对应页面;location对象包含当前URL有关的信息,是window对象的一部分,常用的属性有location.hash返回URL的hash值,location.port返回当前使用的端口号。

  1. hash

ash即'#/xxx',是浏览器用来做页面定位的,例如a标签的描点功能,使用window.location.hash可以读取当前页面的hash值,也可以写入,hashchange事件可以响应hash的的改变,有两个特点:

vue利用了浏览器的hash特性实现了前端路由(vue-router hash模式)。

当用户访问wenyan.51easymaster.com 时,服务器返回一个html文件(以及打包好的js文件和css文件),当访问wenyan.51easymaster.com/#/WYHome 和wenyan.51easymaster.com/#/answerDet… 和 #/answerDetail)请求对应的JSON数据渲染对应页面。整个项目只有一个html文件,vue-router内部监听了hashchange事件响应hash值改变,根据hash值的不同渲染不同的视图。

  1. history

HTML5 History Interface中history对象新增了两个方法pushState()和popState()。

这两个方法应用于浏览器的历史记录栈,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会重载页面。

vue-router的history模式充分利用history.pushState API来完成URL跳转而无须重新加载页面。使用history模式,URL是wenyan.51easymaster.com/user/id这个样子…

11. 如何正确的判断this

this的绑定规则有四种:默认绑定,隐式绑定,显式绑定,new绑定。

  1. 函数是否在new中调用(new绑定),如果是,那么this绑定的是新创建的对象。
  2. 函数是否通过call,apply调用,或者使用了bind(即硬绑定),如果是,那么this绑定的就是指定的对象。
  3. 函数是否在某个上下文对象中调用(隐式绑定),如果是的话,this绑定的是那个上下文对象。一般是obj.foo()
  4. 如果以上都不是,那么使用默认绑定。如果在严格模式下,则绑定到undefined,否则绑定到全局对象。
  5. 如果把null或者undefined作为this的绑定对象传入call、apply或者bind, 这些值在调用时会被忽略,实际应用的是默认绑定规则。
  6. 箭头函数没有自己的this,它的this继承于上一层代码块的this。
var number = 5;
var obj = {
  number: 3,
  fn1: (function() {
    var number;
    this.number *= 2;
    number = number * 2;
    number = 3;
    return function() {
      var num = this.number;
      this.number *= 2;
      console.log(num);
      number *= 3;
      console.log(number);
    }
  })()
}
var fn1 = obj.fn1;
fn1.call(null);
obj.fn1();
console.log(window.number);

答案:10,9,3,27,20

12. 跨域的处理方式

跨域主要分3部分:

  1. 协议相同
  2. 域名相同
  3. 端口相同

只要有一个不同,那么就是跨域


  // 地址
  http://www.baidu.com

  协议:http://
  域名:www.baidu.com
  端口:8080(http) 443(https) 默认端口省略

  http://www.baidu.com/login.html // 同源
  http://www.baidu2.com/login.html // 不同源,域名不同
  http://www.baidu.com:81/login.html // 不同源,端口不同

同源的目的:

目的是为了保护用户信息的安全,防止恶意网站窃取数据,否则Cookie可以共享。有的网站一般会把一些重要信息存放在cookie或者LocalStorage中,这时如果别的网站能够获取获取到这个数据,可想而知,这样就没有什么安全可言了。

限制范围:

  • Cookie、LocalStorage和IndexDB 无法读取
  • DOM无法获得
  • AJAX 请求不能发送

主要这3种方式不行。

方案1:CORS

比较常见的就是nodejs配置CORS允许跨域。

  1. Access-Control-Allow-Origin
    • 字段必传,为*表示允许任意域名的请求。当有cookie需要传递时,需要指定域名。
  2. Access-Control-Allow-Credentials
    • 字段可选,默认为false,表示是否允许发送cookie。若允许,通知浏览器也要开启cookie值的传递。
  3. Access-Control-Expose-Headers
    • 字段可选。如果想要浏览器拿到getResponesHeader()其他字段,就在这里指定。
  4. Access-Control-Request-Method
    • 必须字段,非简单请求时设置的字段,例如PUT请求。
  5. Access-Control-Request-Headers
    • 指定额外的发送头信息,以逗号分割字符串。
module.exports = {
  //=>WEB服务端口号
  PORT: 3001,
  //=>CROS跨域相关信息
  CROS: {
    ALLOW_ORIGIN: 'http://127.0.0.1:5500',
    ALLOW_METHODS: 'PUT,POST,GET,DELETE,OPTIONS,HEAD',
    HEADERS: 'Content-Type,Content-Length,Authorization, Accept,X-Requested-With',
    CREDENTIALS: true
  }
};

app.use((req, res, next) => {
  const {
    ALLOW_ORIGIN,
    CREDENTIALS,
    HEADERS,
    ALLOW_METHODS
  } = CONFIG.CROS;
  res.header("Access-Control-Allow-Origin", ALLOW_ORIGIN);
  res.header("Access-Control-Allow-Credentials", CREDENTIALS);
  res.header("Access-Control-Allow-Headers", HEADERS);
  res.header("Access-Control-Allow-Methods", ALLOW_METHODS);
  req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS!') : next();
});
方案2:Proxy

现在主流三大框架,react,vue,argular都使用了webpack进行工程化。在本地开发最常见的就是proxy代理,解决跨域。

主要原理是:客户端像服务器请求数据。webpack-dev-server会再本地创建一个web服务,这个服务会和客户端同源。本地服务实际上是一个node服务,它作为一个中间层会帮客户端去像服务端请求数据,然后把数据返回给客户端。

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
  mode: 'production',
  entry: './src/main.js',
  output: {
    filename: 'main.[hash].min.js',
    path: path.resolve(__dirname, 'build')
  },
  devServer: {
    port: '3000',
    compress: true,
    open: true,
    hot: true,
    proxy: {
      '/': {
        target: 'http://127.0.0.1:3001',
        changeOrigin: true
      }
    }
  },
  // 配置WEBPACK的插件
  plugins: [
    new HtmlWebpackPlugin({
      template: `./public/index.html`,
      filename: `index.html`
    })
  ]
};
方案3:JSONP

主要原理:link,script这种是不会跨域的。所以,前端代码写一个script src = http://localhoost:80/list?callback=func,把这个链接发送给服务端。但是传递给服务端的函数必须是一个全局的函数。服务端接受到请求后,会把callback这个值,返回给客户端。客户端获取到服务端返回的指定格式字符串。发现其实就是本地的func全局函数执行,并且把数据传递给这个函数。

但是这种方式有一个弊端,那就是只能get请求,而且不安全,只要服务端支持,谁都可以调用。

下面手写一个JSONP的实现

function jsonp(url = "", callback) {
  let script;

  // 把传递的回调函数挂载到全局上
  let name = `jsonp${new Date().getTime()}`;
  window[name] = data => {
    // 从服务器获取到了结果
    document.body.removeChild(script);
    delete window[name];
    callback && callback(data);
  };

  // 处理URL
  url += `${url.includes('?') ? '&' : '?'}callback=${name}`;

  // 发送请求
  script = document.createElement('script');
  script.src = url;
  document.body.appendChild(script);
}

jsonp('http://127.0.0.1:1001/list?lx=1', result => {
  console.log(result);
});

jsonp('https://matchweb.sports.qq.com/matchUnion/cateColumns?from=pc', result => {
  console.log(result);
});
方案4:nginx反向代理

这是后端需要做的,其实我也不是很熟悉,大致配置方式。

server {
  listen 80;
  server_name 192.168.161.189;
  #charset koi8-r;
  #access_log logs/host.access.log main ;
  location  {
    proxy_pass http: // 192.168.161.189:8070;
    root html;
    index index.html index.html;
  }
}

什么是代理?

既然是代理跨域,那么代理(Proxy Server)就是一个很重要的点,这里的代理说的服务器代理,是一种很重要的服务器安全功能,也是一种很常见的设计模式,来隔绝不同的模块,解耦模块。

为什么代理是反理?

nginx就能够把用户的请求分发到空闲的服务器上,然后服务器返回自己的服务到负载均衡设备上,然后负载均衡的设备会讲服务器的服务返回给用户,所以我们并不知道为什么服务的是哪一台服务器发送出来的,这就很好的隐藏了服务器。有一句精辟的话是这么说的:“反向代理就是流量发散状,代理是流量汇聚状。”

方案5:POST MESSAGE

A.html

<iframe id="iframe" src="http://127.0.0.1:1002/B.html" frameborder="0" style="display: none;"></iframe>

iframe.onload = function () {
  iframe.contentWindow.postMessage('消息', 'http://127.0.0.1:1002/');
}

//=>监听B传递的信息
window.onmessage = function (ev) {
  console.log(ev.data);
}

B.html

window.onmessage = function (ev) {
  // console.log(ev.data);

  //=>ev.source:A
  ev.source.postMessage(ev.data + '@@@', '*');
}
方案6:基于iframe的跨域解决方案1——locaction.hash

原理:也是利用iframe可以在不同域中传值的特点,而location.hash正好可以携带参数,所以利用iframe作为这个不同域之间的桥梁。

A域名页面

var iframe = document.createElement('iframe')
iframe.src = 'http://www.B.com:80/hash.html'
document.body.appendChild(iframe)

window.onhashchange = function () {
  //处理hash
  console.log(location.hash)
}

B域名页面

var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    var res = JSON.parse(xhr.responseText)
    console.log(res.msg)
    parent.location.href = `http://www.A.com:80/a.html#msg=${res.msg}`
  }
}
xhr.open('GET', 'http://www.B.com:80/json', true)
xhr.send(null)

缺点

  1. iframe虽然能解决问题,但是安全风险还是比较重要的。
  2. hash传参处理起来比较麻烦。
方案7:基于iframe的跨域解决方案2——window.name

原理其实是和上面的方法一样,区别在于window.name能够传递2MB以上的数据。

A域名页面

var iframe = document.createElement('iframe')
iframe.src = 'http://www.B.com:80/name.html'
document.body.appendChild(iframe)
var times = 0
iframe.onload = function () {
  if (times === 1) {
    console.log(JSON.parse(iframe.contentWindow.name))
    destoryFrame()
  } else if (times === 0) {
    times = 1
  }
}

// 获取数据以后销毁这个iframe,释放内存;
function destoryFrame() {
  document.body.removeChild(iframe);
}

B域名页面

var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function () {
  if (xhr.readyState === 4 && xhr.status === 200) {
    window.name = xhr.responseText
    location.href = 'http://www.A.com:80/a.html'
  }
}
xhr.open('GET', 'http://www.B.com:80/json', true)
xhr.send(null)

等等其他处理方式

13. 类型检测的几种方式

  1. typeof
    • typeof(null) => "object"
    • typeof(数组/正则/日期/对象) => "object"
  2. instanceof
    • 检测当前实例是否属于这个类(也可以用来检测数据类型:对typeof的补充)
    • 不能用来处理基本数据类型(基本数据类型基于构造函数方式创建的实例是可以的)
    • 只要出现在实例的原型链上的类,检测结果都是TRUE(页面中可以手动更改原型链的指向,这样导致检测结果不一定准确)
  3. constructor
    • constructor也是一样可以被更改的(这个检测结果也不一定准确)
    • 基本数据类型也可以处理
  4. Object.prototype.toString.call([val])
    • 其它类的原型上的toString基本上都是转换为字符串的,只有Object原型上的是检测数据类型的返回结果 "[object 所属的类]"
    • Object.prototype.toString执行,它中的this是谁,就是检测谁的数据类型
    • Object.prototype.toString.call(xxx)
    • ({}).toString.call(xxx)
    • 最强大的检测数据类型方法(基本上没有弊端)

14. 项目中为何使用json化配置

工具可以动态生成页面,用json来表述这个页面元素是最直接的方式。

15. React的生命周期

React 16之后有三个⽣命周期被废弃(但并未删除)

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

官⽅计划在17版本完全删除这三个函数,只保留UNSAVE_前缀的三个函数,⽬的是为了向下兼容,但是对于开发者⽽⾔应该尽量避免使⽤他们,⽽是使⽤新增的⽣命周期函数替代它们。

⽬前React16.8+的⽣命周期分为三个阶段,分别是挂载阶段、更新阶段、卸载阶段。

挂载阶段:

  • constructor:构造函数,最先被执⾏,我们通常在构造函数⾥初始化state对象或者给⾃定义⽅法绑定this;
  • getDerivedStateFromProps:static getDerivedStateFromProps(nextProps, prevState),这是个静态⽅法,当我们接收到新的属性想去修改我们state, 可以使⽤getDerivedStateFromProps
  • render:render函数是纯函数,只返回需要渲染的东⻄,不应该包含其它的业务逻辑,可以返回原⽣的DOM、React组件、Fragment、Portals、字符串和数字、 Boolean和null等内容;
  • componentDidMount:组件装载之后调⽤,此时我们可以获取到DOM节点并操作,⽐如对canvas,svg的操作,服务器请求,订阅都可以写在这个⾥⾯,但是记得在componentWillUnmount中取消订阅;

更新阶段:

  • getDerivedStateFromProps: 此⽅法在更新个挂载阶段都可能会调⽤;
  • shouldComponentUpdate:shouldComponentUpdate(nextProps, nextState),有两个参数nextProps和nextState,表示新的属性和变化之后的state,返回⼀个布尔值,true表示会触发重新渲染,false表示不会触发重新渲染,默认返回true,我们通常利⽤此⽣命周期来优化React程序性能;
  • render:更新阶段也会触发此⽣命周期;
  • getSnapshotBeforeUpdate:getSnapshotBeforeUpdate(prevProps, prevState),这个⽅法在render之后,componentDidUpdate之前调⽤,有两个参数prevProps和prevState,表示之前的属性和之前的state,这个函数有⼀个返回值,会作为第三个参数传给componentDidUpdate,如果你不想要返回值,可以返回null,此⽣命周期必须与componentDidUpdate搭配使⽤;
  • componentDidUpdate:componentDidUpdate(prevProps, prevState, snapshot),该⽅法在getSnapshotBeforeUpdate⽅法之后被调⽤,有三个参数prevProps,prevState,snapshot,表示之前的props,之前的state,和snapshot。第三个参数是getSnapshotBeforeUpdate返回的,如果触发某些回调函数时需要⽤到DOM元素的状态,则将对⽐或计算的过程迁移⾄getSnapshotBeforeUpdate,然后在componentDidUpdate中统⼀触发回调或更新状态。

卸载阶段:

-componentWillUnmount:当我们的组件被卸载或者销毁了就会调⽤,我们可以在这个函数⾥去清除⼀些定时器,取消⽹络请求,清理⽆效的DOM元素等垃圾清理⼯作。

总结:

  • componentWillMount:在渲染之前执行,用于根组件中的 App 级配置;
  • componentDidMount:在第一次渲染之后执行,可以在这里做AJAX请求,DOM的操作或状态更新以及设置事件监听器;
  • componentWillReceiveProps:在初始化render的时候不会执行,它会在组件接受到新的状态(Props)时被触发,一般用于父组件状态更新时子组件的重新渲染
  • shouldComponentUpdate:确定是否更新组件。默认情况下,它返回true。如果确定在state或props更新后组件不需要在重新渲染,则可以返回false,这是一个提高性能的方法;
  • componentWillUpdate:在shouldComponentUpdate返回true确定要更新组件之前件之前执行;
  • componentDidUpdate:它主要用于更新DOM以响应props或state更改;
  • componentWillUnmount:它用于取消任何的网络请求,或删除与组件关联的所有事件监听器。

16. setState到底是异步还是同步

先给出答案: 有时表现出异步,有时表现出同步。

  • setState只在合成事件和钩⼦函数中是“异步”的,在原⽣事件和setTimeout中都是同步的;
  • setState的“异步”并不是说内部由异步代码实现,其实本身执⾏的过程和代码都是同步的,只是合成事件和钩⼦函数的调⽤顺序在更新之前,导致在合成事件和钩⼦函数中没法⽴⻢拿到更新后的值,形成了所谓的“异步”,当然可以通过第⼆个参数setState(partialState, callback)中的callback拿到更新后的结果;
  • setState的批量更新优化也是建⽴在“异步”(合成事件、钩⼦函数)之上的,在原⽣事件和setTimeout中不会批量更新,在“异步”中如果对同⼀个值进⾏多次 setState,setState的批量更新策略会对其进⾏覆盖,取最后⼀次的执⾏,如果是同时setState多个不同的值,在更新时会对其进⾏合并批量更新。

17. promise all和rase有什么区别

  1. promise.all:完成多个异步请求,当在页面中需要多多个异步请求完成后,在执行某个操作
  2. promise.rase: 多个异步请求,只要某个成功,就可以使用某个操作

18. useRef

  1. useRef是一个方法,且useRef返回一个可变的ref对象(对象!!!)
  2. initialValue被赋值给其返回值的.current对象
  3. 可以保存任何类型的值:dom、对象等任何可辨值
  4. ref对象与自建一个{current:''}对象的区别是:useRef会在每次渲染时返回同一个ref对象,即返回的ref对象在组件的整个生命周期内保持不变。自建对象每次渲染时都建立一个新的。
  5. ref对象的值发生改变之后,不会触发组件重新渲染。有一个窍门,把它的改边动作放到useState()之前。
  6. 本质上,useRef就是一个其.current属性保存着一个可变值‘盒子’。目前我用到的是pageRef和sortRef分别用来保存分页信息和排序信息。

在异步处理中的,setState的时候,无法拿到最新的值,可以使用useRef。

19. 虚拟列表的实现

为什么需要使用虚拟列表

假设我们的长列表需要展示10000条记录,我们同时将10000条记录渲染到页面中,先来看看需要花费多长时间:

<button id="button">button</button><br>
<ul id="container"></ul>  
document.getElementById('button').addEventListener('click',function(){
  // 记录任务开始时间
  let now = Date.now();
  // 插入一万条数据
  const total = 10000;
  // 获取容器
  let ul = document.getElementById('container');
  // 将数据插入容器中
  for (let i = 0; i < total; i++) {
    let li = document.createElement('li');
    li.innerText = ~~(Math.random() * total)
    ul.appendChild(li);
  }
  console.log('JS运行时间:',Date.now() - now);
  setTimeout(()=>{
    console.log('总运行时间:',Date.now() - now);
  },0)

  // print JS运行时间: 38
  // print 总运行时间: 957 
})

当我们点击按钮,会同时向页面中加入一万条记录,通过控制台的输出,我们可以粗略的统计到,JS的运行时间为38ms,但渲染完成后的总时间为957ms。

简单说明一下,为何两次console.log的结果时间差异巨大,并且是如何简单来统计JS运行时间和总渲染时间:

  • 在JS的Event Loop中,当JS引擎所管理的执行栈中的事件以及所有微任务事件全部执行完后,才会触发渲染线程对页面进行渲染
  • 第一个console.log的触发时间是在页面进行渲染之前,此时得到的间隔时间为JS运行所需要的时间
  • 第二个console.log是放到setTimeout中的,它的触发时间是在渲染完成,在下一次Event Loop中执行的

虚拟列表其实是按需显示的一种实现,即只对可见区域进行渲染,对非可见区域中的数据不渲染或部分渲染的技术,从而达到极高的渲染性能。

假设有1万条记录需要同时渲染,我们屏幕的可见区域的高度为500px,而列表项的高度为50px,则此时我们在屏幕中最多只能看到10个列表项,那么在首次渲染的时候,我们只需加载10条即可。

说完首次加载,再分析一下当滚动发生时,我们可以通过计算当前滚动值得知此时在屏幕可见区域应该显示的列表项。

假设滚动发生,滚动条距顶部的位置为150px,则我们可得知在可见区域内的列表项为第4项至`第13项。

实现:

虚拟列表的实现,实际上就是在首屏加载的时候,只加载可视区域内需要的列表项,当滚动发生时,动态通过计算获得可视区域内的列表项,并将非可视区域内存在的列表项删除。

  • 计算当前可视区域起始数据索引(startIndex)
  • 计算当前可视区域结束数据索引(endIndex)
  • 计算当前可视区域的数据,并渲染到页面中
  • 计算startIndex对应的数据在整个列表中的偏移位置startOffset并设置到列表上

由于只是对可视区域内的列表项进行渲染,所以为了保持列表容器的高度并可正常的触发滚动,将Html结构设计成如下结构:

<div class="infinite-list-container">
  <div class="infinite-list-phantom"></div>
  <div class="infinite-list">
    <!-- item-1 -->
    <!-- item-2 -->
    <!-- ...... -->
    <!-- item-n -->
  </div>
</div>
  • infinite-list-container 为可视区域的容器
  • infinite-list-phantom 为容器内的占位,高度为总列表高度,用于形成滚动条
  • infinite-list 为列表项的渲染区域

接着,监听infinite-list-container的scroll事件,获取滚动位置scrollTop

  • 假定可视区域高度固定,称之为screenHeight
  • 假定列表每项高度固定,称之为itemSize
  • 假定列表数据称之为listData
  • 假定当前滚动位置称之为scrollTop

则可推算出:

  • 列表总高度listHeight = listData.length * itemSize
  • 可显示的列表项数visibleCount = Math.ceil(screenHeight / itemSize)
  • 数据的起始索引startIndex = Math.floor(scrollTop / itemSize)
  • 数据的结束索引endIndex = startIndex + visibleCount
  • 列表显示数据为visibleData = listData.slice(startIndex,endIndex)

当滚动后,由于渲染区域相对于可视区域已经发生了偏移,此时我需要获取一个偏移量startOffset,通过样式控制将渲染区域偏移至可视区域中。

偏移量startOffset = scrollTop - (scrollTop % itemSize);

最终的简易代码如下:

<template>
  <div ref="list" class="infinite-list-container" @scroll="scrollEvent($event)">
    <div class="infinite-list-phantom" :style="{ height: listHeight + 'px' }"></div>
    <div class="infinite-list" :style="{ transform: getTransform }">
      <div ref="items"
        class="infinite-list-item"
        v-for="item in visibleData"
        :key="item.id"
        :style="{ height: itemSize + 'px',lineHeight: itemSize + 'px' }"
      >{{ item.value }}</div>
    </div >
  </div >
</template>
export default {
  name: 'VirtualList',
  props: {
    //所有列表数据
    listData: {
      type: Array,
      default: () => []
    },
    //每项高度
    itemSize: {
      type: Number,
      default: 200
    }
  },
  computed: {
    //列表总高度
    listHeight() {
      return this.listData.length * this.itemSize;
    },
    //可显示的列表项数
    visibleCount() {
      return Math.ceil(this.screenHeight / this.itemSize)
    },
    //偏移量对应的style
    getTransform() {
      return `translate3d(0,${this.startOffset}px,0)`;
    },
    //获取真实显示列表数据
    visibleData() {
      return this.listData.slice(this.start, Math.min(this.end, this.listData.length));
    }
  },
  mounted() {
    this.screenHeight = this.$el.clientHeight;
    this.start = 0;
    this.end = this.start + this.visibleCount;
  },
  data() {
    return {
      //可视区域高度
      screenHeight: 0,
      //偏移量
      startOffset: 0,
      //起始索引
      start: 0,
      //结束索引
      end: null,
    };
  },
  methods: {
    scrollEvent() {
      //当前滚动位置
      let scrollTop = this.$refs.list.scrollTop;
      //此时的开始索引
      this.start = Math.floor(scrollTop / this.itemSize);
      //此时的结束索引
      this.end = this.start + this.visibleCount;
      //此时的偏移量
      this.startOffset = scrollTop - (scrollTop % this.itemSize);
    }
  }
};

参考:juejin.cn/post/684490…

20. 如何模拟class

身后有阴影,那是因为面向太阳。

ES6中的class可以看作一个语法糖,es5其实都是可以做到的,新的class语法使其更符合面向对象编程而已。

区别:

  • 类必须使用new调用,否则会报错。ES5的构造函数是可以当做普通函数使用的
  • 类的内部所有定义的方法,都是不可枚举的
  • 类的静态方法也可以被子类继承
  • 可以继承原生的构造函数
  • ES5是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数
  • ES6允许继承原生构造函数定义子类,因为ES6是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承

类必须使用new调用,否则会报错

function _checkType(obj, constructor) {
  if (obj instanceof constructor) {
    throw new TypeError('Cannot call a class as a function')
  }
}

内部方法不可枚举

function definePorperties(target, descriptions) {
  for (let descriptor of descriptions) {
    descriptor.enumerable = descriptor.enumerable || false

    descriptor.configurable = true

    if ('value' in descriptor) {
      descriptor.writable = true
    }

    Object.defineProperty(target, descriptor.key, descriptor)
  }
}

/**
* 
* @param {*} constructor 表示对应的constructor对象
* @param {*} protoDesc  class内部定义的方法
* @param {*} staticDesc  class内部定义的静态方法
*/
function _creatClass(constructor, protoDesc, staticDesc) {
  protoDesc && definePorperties(constructor.prototype, protoDesc)
  staticDesc && definePorperties(constructor, staticDesc)
  return constructor
}

真正的创建class

function _checkType(obj, constructor) {
  if (!(obj instanceof constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}

function definePorperties(target, descriptions) {
  for (let descriptor of descriptions) {
    descriptor.enumerable = descriptor.enumerable || false

    descriptor.configurable = true

    if ('value' in descriptor) {
      descriptor.writable = true
    }

    Object.defineProperty(target, descriptor.key, descriptor)
  }
}

/**
* 
* @param {*} constructor 表示对应的constructor对象
* @param {*} protoDesc  class内部定义的方法
* @param {*} staticDesc  class内部定义的静态方法
*/
function _creatClass(constructor, protoDesc, staticDesc) {
  protoDesc && definePorperties(constructor.prototype, protoDesc)
  staticDesc && definePorperties(constructor, staticDesc)
  return constructor
}

const Foo = function () {
  function Foo(name) {
    _checkType(this, Foo) // 检查是否是new调用
    this.name = name
  }

  _creatClass(Foo, [ // class内部定义的方法
    {
      key: 'say',
      value: function () {
        console.log(this.name);
      }
    }
  ], [
    {
      key: 'say',
      value: function () {
        console.log('static say');
        console.log(this.name);
      }
    }
  ])
  return Foo
}()

这个时候直接调用Foo()

使用new 操作符之后

下面截图可以看出say方法是不可枚举的

实现继承

类的静态方法也可被子类继承

function _inherits(subClass, superClass) {
  if (typeof superClass !== 'function' && superClass !== null) {
    throw new TypeError('父类必须是函数且不为null')
  }

  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  })

  if (superClass) {
    Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass._proto_ = superClass
  }
}

使用父类的实例对象this

// 返回父类的this,若为null,返回自身
function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError('this has not initialised - super() has not been called ')
  }
  return call && (typeof call === 'object' || typeof call === 'function') ? call : self
}

创建子类class

const Child = function (_parent) {
  _inherits(child, _parent) // 继承父类原型上的属性和静态方法的继承
  function Child(name, age) {
    _checkType(this, Child)

    // 先使用父类的实例对象this,再返回
    const _this = _possibleConstructorReturn(this, (Child._proto_ || Object.getPrototypeOf(Child)).call(this, name))
    _this.age = age
    return _this
  }
  return Child
}(Foo)

执行截图,证明继承成功了

完整代码

function _checkType(obj, constructor) {
  if (!(obj instanceof constructor)) {
    throw new TypeError('Cannot call a class as a function')
  }
}

function definePorperties(target, descriptions) {
  for (let descriptor of descriptions) {
    descriptor.enumerable = descriptor.enumerable || false
    descriptor.configurable = true
    if ('value' in descriptor) {
      descriptor.writable = true
    }
    Object.defineProperty(target, descriptor.key, descriptor)
  }
}

/**
* 
* @param {*} constructor 表示对应的constructor对象
* @param {*} protoDesc  class内部定义的方法
* @param {*} staticDesc  class内部定义的静态方法
*/
function _creatClass(constructor, protoDesc, staticDesc) {
  protoDesc && definePorperties(constructor.prototype, protoDesc)
  staticDesc && definePorperties(constructor, staticDesc)
  return constructor
}

const Foo = function () {
  function Foo(name) {
    _checkType(this, Foo) // 检查是否是new调用
    this.name = name
  }
  _creatClass(Foo, [ // class内部定义的方法
    {
      key: 'say',
      value: function () {
        console.log(this.name);
      }
    }
  ], [
    {
      key: 'say',
      value: function () {
        console.log('static say');
        console.log(this.name);
      }
    }
  ])
  return Foo
}()

function _inherits(subClass, superClass) {
  if (typeof superClass !== 'function' && superClass !== null) {
    throw new TypeError('父类必须是函数且不为null')
  }
  subClass.prototype = Object.create(superClass && superClass.prototype, {
    constructor: {
      value: subClass,
      enumerable: false,
      writable: true,
      configurable: true
    }
  })
  if (superClass) {
    Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass._proto_ = superClass
  }
}

// 返回父类的this,若为null,返回自身
function _possibleConstructorReturn(self, call) {
  if (!self) {
    throw new ReferenceError('this has not initialised - super() has not been called ')
  }
  return call && (typeof call === 'object' || typeof call === 'function') ? call : self
}

const Child = function (_parent) {
  _inherits(Child, _parent) // 继承父类原型上的属性和静态方法的继承
  function Child(name, age) {
    _checkType(this, Child)
    // 先使用父类的实例对象this,再返回
    const _this = _possibleConstructorReturn(this, (Child._proto_ || Object.getPrototypeOf(Child)).call(this, name))
    _this.age = age
    return _this
  }
  return Child
}(Foo)

class中子类调用super()的区别

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

  • super作为函数调用时,代表父类的构造函数。ES6要求,子类的构造函数必须执行super函数。子类没有写constructor方法,js引擎默认,帮你执行constructor(){ super() }
  • super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。 ES6在继承中强制要求,必须在子类调用super,因为子类的this是由父类得来的。

参考:www.jianshu.com/p/29a100c45…

21. 事件轮询

先理解一些概念:

  • JS分为同步任务和异步任务
  • 同步任务都在JS引擎线程上执行,形成一个执行栈
  • 事件触发线程管理一个任务队列,异步任务触发条件达成,将回调事件放到任务队列中
  • 执行栈中所有同步任务执行完毕,此时JS引擎线程空闲,系统会读取任务队列,将可运行的异步任务回调事件添加到执行栈中,开始执行

因为JavaScript是单线程的。意味着,前一个任务结束,才会执行后一个任务。如果前面一个任务执行的时间很长,后面一个任务不得不等待,会形成卡死现象。

如果仅仅只是计算量太大了,也算了。但是很多时候,cpu是闲着的,比如io输入输出,ajax请求,难道不得不等待结果,再执行吗?

JavaScript语言的设计者意识到,这时主线程完全可以不管IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回了结果,再回过头,把挂起的任务继续执行下去。

于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

微任务的优先级永远高于宏任务,宏任务的优先级是按照谁先到执行时间,谁先执行。

宏任务:定时器,ajax,事件绑定

微任务:promise,async,await

统计代码执行时间
console.log(1);
console.time('AAA');
for (let i = 0; i < 99999999; i++) {
  if (i === 99999998) {
    console.log(2);
  }
}
console.timeEnd('AAA'); //=>time/timeEnd可以记录一段程序执行的时间(时间受电脑性能和执行时候的环境转态影响) "事后统计法"   300MS~400MS
console.log(3);
练习1:
console.log(1);
setTimeout(() => {
  console.log(2);
}, 1000);
console.log(3);
setTimeout(() => {
  console.log(4);
}, 0); //=>并不是立即执行,需要等待浏览器的最小反应时间 5~6MS
console.log(5);
for (let i = 0; i < 99999999; i++) {
  if (i === 99999998) {
    console.log(6);
  }
}
console.log(7);

答案:1 3 5 6 7 4 2

解析:先执行同步任务,然后执行宏任务。

| GUI线程 | 宏任务 | | --- | --- | --- | | console.log(1) | 1000毫秒后执行console.log(2) | | console.log(3) | 5-6毫秒执行console.log(4) | | console.log(5) | - | | console.log(6) | - | | console.log(7) | - |

练习2:
setTimeout(() => {
  console.log(1);
}, 20);
console.log(2);
setTimeout(() => {
  console.log(3);
}, 10);
console.log(4);
for (let i = 0; i < 90000000; i++) {
  // do soming  79MS
}
console.log(5);
setTimeout(() => {
  console.log(6);
}, 8);
console.log(7);
setTimeout(() => {
  console.log(8);
}, 15);
console.log(9);

答案:2 4 5 7 9 3 1 6 8

解析:先执行同步任务,然后执行宏任务。

| GUI线程 | 宏任务 | | --- | --- | --- | | console.log(2) | 任务1:20毫秒后执行console.log(1) | | console.log(4) | 任务2:10毫秒执行console.log(3) | | console.log(5) | 执行for循环后,已经过去了79ms,宏任务,1,2已经执行完成了,等待同步执行完成后执行 | | console.log(7) | 任务3:8毫秒执行console.log(6) | | console.log(9) | 任务4:15毫秒执行console.log(8) |

练习3:
for (var i = 0; i < 5; i++) {
  setTimeout(() => {
    console.log(i);
  }, 0);
}
// 第一次循环  向任务队列中插入一个定时器
// 第二次循环  向任务队列中插入一个定时器
// ...
// 第五次循环  向任务队列中插入一个定时器
// 循环结束 全局下的i=5  任务队列中有5个定时器  【GUI空闲】
//   定时器执行中遇到i 不是自己的,则找全局的,全局的i=5
//   =>5 * 5
练习4:
setTimeout(() => {
  console.log(1);
  while (1 === 1) {}
}, 10);
console.log(2);
for (let i = 0; i < 90000000; i++) {
  // do soming  79MS
}
// 循环结束  任务1已经到时间了
console.log(3);
setTimeout(() => {
  console.log(4);	
}, 5);
console.log(5);

答案:2 4 5 7 9 3 1 6 8

解析:先执行同步任务,然后执行宏任务,执行第一个宏任务后,遇到了死循环,第二个宏任务永远无法执行。

练习5:
async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2() {
  console.log('async2');
}
console.log('script start');
setTimeout(function () {
  console.log('setTimeout');
}, 0)
async1();
new Promise(function (resolve) {
  console.log('promise1');
  resolve();
}).then(function () {
  console.log('promise2');
});
console.log('script end');

答案:

  1. script start
  2. async1 start
  3. async2
  4. promise1
  5. script end
  6. async1 end
  7. promise2
  8. setTimeout

解析:先执行同步任务,然后执行微任务,最后执行宏任务。

GUI线程微任务宏任务
console.log('script start')任务1:console.log('async1 end')任务1:5-6毫秒后执行console.log('setTimeout')
console.log('async1 start')任务2:console.log('promise2')-
console.log('async2')--
console.log('promise1')--
console.log('script end')--
练习6:
function func1() {
  console.log('func1 start');
  return new Promise(resolve => {
    resolve('OK');
  });
}
function func2() {
  console.log('func2 start');
  return new Promise(resolve => {
    setTimeout(() => {
      resolve('OK');
    }, 10);
  });
}
console.log(1);
setTimeout(async () => {
  console.log(2);
  await func1();
  console.log(3);
}, 20);
for (let i = 0; i < 90000000; i++) { } //循环大约要进行80MS左右
console.log(4);
func1().then(result => {
  console.log(5);
});
func2().then(result => {
  console.log(6);
});
setTimeout(() => {
  console.log(7);
}, 0);
console.log(8);

答案:

  1. 1
  2. 4
  3. func1 start
  4. func2 start
  5. 8
  6. 5
  7. 7
  8. 2
  9. func1 start
  10. 3
  11. 6

解析:先执行同步任务,然后执行微任务,最后执行宏任务。

GUI线程微任务宏任务
console.log(1)任务1:console.log(5)任务1:20毫秒后执行async ()
console.log(4)任务2:console.log(3)任务2:20毫秒后执行resolve('OK')
console.log('func1 start')任务3:console.log(6)任务3:5-6毫秒后执行console.log(7)
console.log('func2 start')--
console.log(8)--