Chendi WuMediaProjectsBlog
Back to blog

前端面试

前端面试知识体系整理:CSS、页面布局、渲染与性能、JS 运行机制、原型链、Virtual DOM、HTTP、通信、安全、算法等专题笔记。

2021-05-18
NotesInterview

前言

image.png

前言

做一个总结,总结自己的知识体系。

话不多数,上车🚌

指南

diagram.jpg

面试考察

考察基础知识与知识广度

image.png

image.png

image.png

思考

年底与年初哪个属于跳槽的最佳时期?

入职前...

拿到offer时,你最好就开始思考预测未来3-5年,你的工作与生活,该怎么走

参考

学习

  • 背景:学的很累
  • 原则:先少后多、先精后广
  • 方法:平衡学习、工作、生活时间与利用社区的力量

成长

  • 定义:技术、做事
  • 方法:自驱动、敢于挑战、承担责任
  • 自测:技术架构、独立做事、彼此成就

晋升

  • 目标:加薪、晋升
  • 方法:少说、多做、任劳任怨、有效的沟通、拒绝平庸、全面发展
  • 心态:先付出在回报、个人服从集体、学会定位和补位、足够耐心

总结

面试题更多时候是一个引子,更多是想通过面试题去思考题目背后带来的对某一模块的深入学习和探讨。

CSS

CSS 盒模型

基本概念

标准模型

面试-CSS盒模型-标准模型.png 从上图可以看到标准 W3C 盒子模型的范围包括 margin、border、padding、content,并且 content 部分不包含其他部分。

IE 模型

面试-CSS盒模型-IE盒子模型.png 从上图可以看到 IE 盒子模型的范围也包括 margin、border、padding、content,和标准 W3C 盒子模型不同的是:IE 盒子模型的 content 部分包含了 border 和 pading。

CSS 如何设置这两种模型

<!-- 标准模型 -->
box-sizing:conent-box;
<!-- IE盒模型 -->
box-sizing:border-box;

浏览器默认方式为:box-sizing:conent-box

JS 如何设置获取盒子模型对应的宽和高

dom.style.width / height

dom.currentStyle.width / height(ie支持)

window.getComputedStyle(dom).width / height

dom.getBoundingClientRect().width / height

实例题(根据盒模型解释边距重叠)

面试-CSS盒模型-边距重叠1.png 如上图:有两个元素其中子元素高度为 100px,子元素与父元素的上边距为 10px,求父元素的实际高度? 答案: 说 100px 对,说 110 也对,为什么捏?这个要看父元素的盒模型要怎么设置的

<style>
  html,
  * {
    padding: 0;
    margin: 0;
  }
  #sec {
    background: #f00;
  }
  .child {
    height: 100px;
    margin-top: 10px;
    background: yellow;
  }
</style>
<body>
  <section id="sec">
    <article class="child"></article>
  </section>
</body>

运行效果:

面试-CSS盒模型-运行1.png 上图证明 100px 是对的 元素加个 overflow:hidden

面试-CSS盒模型-运行2.png 你会发现这时高度为 110px, 这时大家可能会疑问,为什么给父级元素设置一个 overflow:hidden 以后,它的高度就成 110 呢,这块的基本原理是啥呢,咋就这样呢?说这个之前,先引用一个知识点: 上面代码是父子元素边距重叠,那么还有两种情况边距重叠就是,一种是兄弟元素,就是两个 div 挨着,每个都上边距或者下边距,那么重叠的原则就是取最大值。 来回答上面问题:给父级加了 overflow:hidden,其实就是给父级元素创建一个 BFC(块级格式化上下文)。

BFC

基本概念

Block Formatting Context, 块级格式化上下文,一个独立的块级渲染区域,该区域拥有一套渲染规格来约束块级盒子的布局,且与区域外部无关。

BFC 的原理

  • BFC 这个元素的垂直的边距会发生重叠
  • BFC 的区域不会与浮动元素的 float 重叠
  • 独立的容器,内外元素互不影响
  • 计算 BFC 高度,浮动元素也参与计算

如何创建 BFC

  • float 不为 none 的时候
  • position 不为 static 或者 relative 的时候
  • display 与 table 相关的时候
  • overflow 为 auto, hidden 的时候

BFC 使用场景

  • BFC 垂直方向边距重叠
<section id="margin">
  <style>
    #margin {
      background: pink;
      overflow: hidden;
    }
    #margin > p {
      margin: 5px auto 25px;
      background: red;
    }
  </style>
  <p>1</p>
  <div style="overflow:hidden">
    <p>2</p>
  </div>
  <p>3</p>
</section>
  • BFC 不与 float 重叠
<section id="layout">
  <style media="screen">
    #layout {
      background: red;
    }
    #layout .left {
      float: left;
      width: 100px;
      height: 100px;
      background: pink;
    }
    #layout .right {
      height: 110px;
      background: #ccc;
      overflow: auto;
    }
  </style>
  <div class="left"></div>
  <div class="right"></div>
</section>
  • BFC 子元素即使是 float 也会参与计算
<section id="float">
  <style media="screen">
    #float {
      background: red;
      overflow: auto;
      /*float: left;*/
    }
    #float .float {
      float: left;
      font-size: 30px;
    }
  </style>
  <div class="float">我是浮动元素</div>
</section>

link 标签和 import 标签的区别

link 属于 html 标签,而@import 是 css 提供的 页面被加载时,link 会同时被加载,而@import 引用的 css 会等到页面加载结束后加载。 link 是 html 标签,因此没有兼容性,而@import 只有 IE5 以上才能识别。 link 方式样式的权重高于@import 的。

选择器,优先级

id 选择器,class 选择器,标签选择器,伪元素选择器,伪类选择器等

同一元素引用了多个样式时,排在后面的样式属性的优先级高;

样式选择器的类型不同时,优先级顺序为:id 选择器 > class 选择器 > 标签选择器;

标签之间存在层级包含关系时,后代元素会继承祖先元素的样式。如果后代元素定义了与祖先元素相同的样式,则祖先元素的相同的样式属性会被覆盖。继承的样式的优先级比较低,至少比标签选择器的优先级低;

带有!important 标记的样式属性的优先级最高;

样式表的来源不同时,优先级顺序为:内联样式> 内部样式 > 外部样式 > 浏览器用户自定义样式 > 浏览器默认样式

页面布局

三栏布局

<section class="layout float">
  <style media="screen">
    .layout.float .left {
      float: left;
      width: 300px;
      background: red;
    }
    .layout.float .center {
      background: yellow;
    }
    .layout.float .right {
      float: right;
      width: 300px;
      background: blue;
    }
  </style>
  <h1>三栏布局</h1>
  <article class="left-right-center">
    <div class="left"></div>
    <div class="right"></div>
    <div class="center">
      <h2>浮动解决方案</h2>
      1.这是三栏布局的浮动解决方案; 2.这是三栏布局的浮动解决方案;
      3.这是三栏布局的浮动解决方案; 4.这是三栏布局的浮动解决方案;
      5.这是三栏布局的浮动解决方案; 6.这是三栏布局的浮动解决方案;
    </div>
  </article>
</section>
<section class="layout absolute">
  <style>
    .layout.absolute .left-center-right > div {
      position: absolute;
    }
    .layout.absolute .left {
      left: 0;
      width: 300px;
      background: red;
    }
    .layout.absolute .center {
      left: 300px;
      right: 300px;
      background: yellow;
    }
    .layout.absolute .right {
      right: 0;
      width: 300px;
      background: blue;
    }
  </style>
  <h1>三栏布局</h1>
  <article class="left-center-right">
    <div class="left"></div>
    <div class="center">
      <h2>绝对定位解决方案</h2>
      1.这是三栏布局的浮动解决方案; 2.这是三栏布局的浮动解决方案;
      3.这是三栏布局的浮动解决方案; 4.这是三栏布局的浮动解决方案;
      5.这是三栏布局的浮动解决方案; 6.这是三栏布局的浮动解决方案;
    </div>
    <div class="right"></div>
  </article>
</section>
<section class="layout flexbox">
  <style>
    .layout.flexbox {
      margin-top: 110px;
    }
    .layout.flexbox .left-center-right {
      display: flex;
    }
    .layout.flexbox .left {
      width: 300px;
      background: red;
    }
    .layout.flexbox .center {
      flex: 1;
      background: yellow;
    }
    .layout.flexbox .right {
      width: 300px;
      background: blue;
    }
  </style>
  <h1>三栏布局</h1>
  <article class="left-center-right">
    <div class="left"></div>
    <div class="center">
      <h2>flexbox解决方案</h2>
      1.这是三栏布局的浮动解决方案; 2.这是三栏布局的浮动解决方案;
      3.这是三栏布局的浮动解决方案; 4.这是三栏布局的浮动解决方案;
      5.这是三栏布局的浮动解决方案; 6.这是三栏布局的浮动解决方案;
    </div>
    <div class="right"></div>
  </article>
</section>
<section class="layout table">
  <style>
    .layout.table .left-center-right {
      width: 100%;
      height: 100px;
      display: table;
    }
    .layout.table .left-center-right > div {
      display: table-cell;
    }
    .layout.table .left {
      width: 300px;
      background: red;
    }
    .layout.table .center {
      background: yellow;
    }
    .layout.table .right {
      width: 300px;
      background: blue;
    }
  </style>
  <h1>三栏布局</h1>
  <article class="left-center-right">
    <div class="left"></div>
    <div class="center">
      <h2>表格布局解决方案</h2>
      1.这是三栏布局的浮动解决方案; 2.这是三栏布局的浮动解决方案;
      3.这是三栏布局的浮动解决方案; 4.这是三栏布局的浮动解决方案;
      5.这是三栏布局的浮动解决方案; 6.这是三栏布局的浮动解决方案;
    </div>
    <div class="right"></div>
  </article>
</section>
<section class="layout grid">
  <style>
    .layout.grid .left-center-right {
      width: 100%;
      display: grid;
      grid-template-rows: 100px;
      grid-template-columns: 300px auto 300px;
    }
    .layout.grid .left-center-right > div {
    }
    .layout.grid .left {
      width: 300px;
      background: red;
    }
    .layout.grid .center {
      background: yellow;
    }
    .layout.grid .right {
      background: blue;
    }
  </style>
  <h1>三栏布局</h1>
  <article class="left-center-right">
    <div class="left"></div>
    <div class="center">
      <h2>网格布局解决方案</h2>
      1.这是三栏布局的浮动解决方案; 2.这是三栏布局的浮动解决方案;
      3.这是三栏布局的浮动解决方案; 4.这是三栏布局的浮动解决方案;
      5.这是三栏布局的浮动解决方案; 6.这是三栏布局的浮动解决方案;
    </div>
    <div class="right"></div>
  </article>
</section>

未知宽高元素水平垂直居中

<div class="father">
  <div class="son"></div>
</div>

//第一种
<style>
  .father {
    position: relative;
    width: 500px;
    height: 500px;
    background-color: red;
  }
  .son {
    position: absolute;
    margin: auto;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    width: 300px;
    height: 300px;
    background-color: blue;
  }
</style>

//第二种
<style>
  .father {
    position: relative;
  }
  .son {
    margin: auto;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  }
</style>

//第三种
<style>
  .father {
    display: -webkit-flex;
    align-items: center;
    justify-content: center;
  }
  .son {
    margin: auto;
  }
</style>

//第四种,用表格布局
<div class="pe">
  <div class="he">
    <div class="she"></div>
  </div>
</div>
<style>
  .pe {
    display: table;
    width: 50%;
    height: 30%;
    position: absolute;
  }
  .he {
    display: table-cell;
    vertical-align: middle;
    text-align: center;
  }
  .she {
    width: 50%;
    height: 30%;
    display: inline-block;
  }
</style>

//第五种,行内块元素
<div class="wrap">
  <b class="vamp"></b>
  <div class="test">行内块元素</div>
  <div>
    <style>
      .vamp{
        width:0;
        height:100%;
        vertical-align:middle;
      }
      .test{
        background:"blue";
        display:inline-block;
      }
    </style>
  </div>
</div>

渲染机制

DOCTYPE

DOCTYPE 及 作用

DOCTYPE 是 document type(文档类型的)的简写 作用:

告诉浏览器需要通过哪一种规范(文档类型定义,DTD)解析文档(比如HTML或XHTML规范) DTD 告诉浏览器,我是什么文档类型。浏览器根据这个来判断用什么浏览引擎来解析它渲染它 DOCTYPE 就是直接告诉浏览器什么是 DTD。告诉浏览器包含哪个 DTD,也就是哪个文档类型

常见的 DOCTYPE 声明

<!-- HTML 5 -->
<!doctype html>

<!-- HTML 4.01 Strict -->
<!-- 这个 DTD 包含所有 HTML 元素和属性,但不包括展示性的和弃用的元素(比如 font) -->
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">

<!-- HTML 4.01 Transitional -->
<!-- 这个 DTD 包含所有 HTML 元素和属性,包括展示性的和弃用的元素(比如 font) -->
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

面试注意:

一个 strict,一个 loose。不用记怎么写。 > 知道 4.01 有两个模式,一个严格模式,一个传统模式。 > 知道 HTML5 怎么写。能说出严格和宽松的区别。 > 如果严格模式,使用了弃用的元素,页面是是有问题的。宽松模式没有问题。

**

浏览器渲染过程

抛出一个老生常谈的面试问题:从输入 URL 到页面加载完成,发生了什么? 首先我们需要通过 DNS域名解析,将 URL 解析为对应的 IP 地址,然后与这个 IP 地址确定的那台服务器建立起 TCP 网络连接,随后我们向服务端抛出我们的 HTTP 请求,服务端处理完我们的请求之后,把目标数据放在 HTTP 响应里返回给客户端,拿到响应数据的浏览器就可以开始走一个渲染的流程。渲染完毕,页面便呈现给了用户,并时刻等待响应用户的操作。

面试大白话解释:

输入 url,会有一个 dns 解析,然后发到服务器,服务器再响应,响应过来,到了客户端,浏览器这边就开始渲染过程

** 过程片段分为:

  • DNS解析
  • TCP连接
  • HTTP请求
  • service处理,HTTP响应返回
  • 浏览器拿到数据,解析响应内容,展示数据内容

diagram2.jpg

渲染过程很复杂,但是不会说的那么细。几句话说清就好了。

浏览器拿到 HTML 和 css 之后要干什么?

HTML 经过 HTML parser 转化成 DOM tree,css 按着 css 规则和 css 解释器,转成 CSSOM Tree,两棵树之间一整合,就是 attachment,形成一个 render tree,告诉浏览器渲染的树的结构就出来了。render tree 不包含 html 具体内容,也不知道具体位置是什么。比如有一个 div,layout 之前,不知道画在具体什么位置。这时候通过 layout 就可以精确计算到要显示的这些 dom 真正的宽高,位置颜色,最后开始 paint,画图,把内容呈现出来。最后 display,在浏览器上能看到页面效果。

CSSOM 树和 DOM 树连接在一起形成一个 render tree,渲染树用来计算可见元素的布局并且作为将像素渲染到屏幕上的过程的输入。

DOM 树和 CSSOM 树连接在一起形成 render tree .

render tree 只包含了用于渲染页面的节点布局计算了每一个对象的准确的位置以及大小绘画是最后一步,绘画要求利用 render tree 来将像素显示到屏幕上第一步是结合 DOM 树和 CSSOM 树形成“render tree”,渲染树用来描述所有可见的 DOM 内容,并且将 CSSOM 样式信息附加到节点上。

为了形成渲染树,浏览器大致做的事情有:从 DOM 树根节点开始,遍历每一个可见的节点一些节点是完全不可见的(比如 script 标签,meta 标签等),这些节点会被忽略,因为他们不会影响渲染的输出一些节点是通过 CSS 样式隐藏了,这些节点同样被忽略——例如上例中的 span 节点在 render tree 中被忽略,因为 span 样式是 display:none;对每一个可见的节点,找到合适的匹配的 CSSOM 规则,并且应用样式显示可见节点(节点包括内容和被计算的样式)

让我们快速的浏览下浏览器所做的事情:

处理 HTML 标签建立 DOM 树

处理 CSS 标签建立 CSSOM 树

连接 CSSOM 树和 DOM 树形成一个 render 树

在 render 树上运行布局来计算每个节点的形状

在屏幕上画每一个节点

虽然我们的页面很简单,但是它进行了大量的工作!

render-tree-construction.png

重绘与重排

重绘(repaint 或 redraw)

定义:当盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来之后,浏览器便把这些原色都按照各自的特性绘制一遍,将内容呈现在页面上。重绘是指一个元素外观的改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。 这个过程称之为重绘。

触发重绘的条件:

  1. DOM 改动
  2. CSS 改动

重排(重构/回流/reflow)

定义:当渲染树中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建, 这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。

触发 Reflow:

  1. 当你增加、删除、修改 DOM 节点时,会导致 Reflow 或 Repaint
  2. 当你移动 DOM 的位置,或是搞个动画的时候
  3. 当你修改 CSS 样式的时候
  4. 当你 Resize 窗口的时候(移动端没有这个问题),或者滚动的时候
  5. 当你修改网页的默认字体时

注意: table 及其内部元素可能需要多次计算才能确定好其在渲染树中节点的属性值,比同等元素要多花两倍时间,这就是我们尽量避免使用 table 布局页面的原因之一。

重绘和重排的关系

在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程称为重绘。所以,重排必定会引发重绘,但重绘不一定会引发重排。

如何尽量减小重绘?

重绘无法避免,但是最大程度减小。比如把用户输入计算条件,把输入隐藏掉,计算结果显示出来。两个交互,用户输入,然后输入隐藏,结果展示。这个过程,呈现内容不一样了。怎么做,少画一些东西。就是要把最后的结果,放在一个节点,就算有很多结果,也不要一个一个添加,而是放在一个片段里面,然后一次添加片段。也就是重绘一次。不然你加一个节点,重新重绘一次。 简单来说,能批量操作就批量操作。

Links

渲染树构建、布局及绘制:https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction

页面性能

提升页面性能的方法有哪些?

  1. 资源压缩合并,减少 HTTP 请求 或者 避免同一时间的过多次数请求
  2. 非核心代码异步加载 -> 异步加载的方式 -> 异步加载的区别
  3. 利用浏览器缓存 -> 缓存的分类 -> 缓存的原理
  4. 利用 CDN
  5. 预解析 DNS

资源压缩与合并

主要包括这些方面:压缩代码HTML/CSS/JS,压缩图片与其他资源,Tree-Sharking消除无用代码,以上Webpack构建项目可以搞定

资源压缩可以从文件中去掉多余的字符,比如回车、空格。你在编辑器中写代码的时候,会使用缩进和注释,这些方法无疑会让你的代码简洁而且易读,但它们也会在文档中添加多余的字节

html 压缩

html 代码压缩就是压缩这些在文本文件中有意义,但是在 HTML 中不显示的字符,包括空格,制表符,换行符等,还有一些其他意义的字符,如 HTML 注释也可以被压缩。

使用在线网站进行压缩(开发过程中一般不用)

nodejs 提供了 html-minifier 工具

后端模板引擎渲染压缩

css 压缩

css 代码压缩简单来说就是无效代码删除和 css 语义合并

使用在线网站进行压缩(开发过程中一般不用)

使用 html-minifier 工具

使用 clean-css 对 css 压缩

js 的压缩和混乱

js 的压缩和混乱主要包括以下这几部分:

  • 无效字符的删除
  • 剔除注释
  • 代码语义的缩减和优化
  • 代码保护(代码逻辑变得混乱,降低代码的可读性,这点很重要)
使用在线网站进行压缩(开发过程中一般不用)

使用 html-minifier 工具

使用 uglifyjs2 对 js 进行压缩

避免同时间段请求过多

  • CSS 实现雪碧图:使用background-position共享一张图
  • 图片懒加载:监听滚动后offsetTop,  使用src 替换 src(真实路径)
  • 列表懒加载(分批加载):监听滚动后offsetTop, 发送请求加载下一页的数据
  • 路由懒加载
  • 代码分包分块加载(webpack)
  • 预加载技术
  • 小程序分包、预下载
  • 等

异步加载

动态脚本创建

在还没定义 defer 和 async 前,异步加载的方式是动态创建 script,通过 window.方法确保页面加载完毕再将 script 标签插入到 DOM 中。具体代码如下

function loadJS(url, callback) {
  let script = document.createElement('script')
  script.type = 'text/javascript'
  // IE
  if (script.readyState) {
    script.onreadystatechange = function () {
      if (script.readyState == 'loaded' || script.readyState == 'complete') {
        script.onreadystatechange = null
        if (typeof callback === 'function') {
          callback()
        }
      }
    }
  } else {
    script.onload = function () {
      if (typeof callback === 'function') {
        callback()
      }
    }
  }
  script.src = url
  document.getElementsByTagName('head')[0].appendChild(script)
}
window.onload = function () {
  loadJS('js/index.js', () => {})
}

defer

  • 兼容所有浏览器
  • defer 属性规定是否对脚本执行进行延迟,直到页面加载为止
  • 如果是多个脚本,该方法可以确保所有设置了 defer 属性的脚本按顺序执行
  • 如果脚本不会改变文档的内容,可将 defer 属性加入到 script 标签中,以便加快处理文档的速度
<script defer type="text/javascript" src="js/index.js"></script>

async

  • async 属性是 HTML5 新增属性,需要 Chrome、FireFox、IE9+浏览器支持
  • async 属性规定一旦脚本可用,则会异步执行
  • async 属性仅适用于外部脚本
  • 如果是多个脚本,该方法不能保证脚本按顺序执行
<script async type="text/javascript" src="js/index.js"></script>

注意

  • defer 是在 HTML 解析完之后才会执行,如果是多个,按照加载的顺序依次执行
  • async 是在加载完之后立即执行,如果是多个,执行顺序和加载顺序无关

浏览器缓存

浏览器缓存机制有四个方面,它们按照获取资源时请求的优先级依次排列如下:

  1. Memory Cache
  2. Service Worker Cache
  3. HTTP Cache
  4. Push Cache

HTTP 缓存是我们日常开发中最为熟悉的一种缓存机制。它又分为强缓存和协商缓存。 优先级较高的是强缓存,在命中强缓存失败的情况下,才会走协商缓存。

强缓存

不会向服务器发送请求,直接从缓存中读取资源,HTTP 状态码为 200

强缓存是利用 http 头中的 Expires 和 Cache-Control 两个字段来控制的。强缓存中,当请求再次发出时,浏览器会根据其中的 expires 和 cache-control 判断目标资源是否“命中”强缓存,若命中则直接从缓存中获取资源,不会再与服务端发生通信。

image.png

expires
expires: Sat, 29 Aug 2020 08:43:05 GMT

当services端返回响应时,在 **Response Header** 中讲过期时间写入 **expires**** 字段中。 如上图所示:**expires** **存储里的过期时间 **expires: Sat, 29 Aug 2020 08:43:05 GMT\*\*,当浏览器再次加载对应资源文件时,如果在这个过期时间内,则命中强缓存。反之重新获取。

**expires**** **问题描述:

它最大的问题在于对 本地时间的依赖。 如果服务端和客户端的时间设置可能不同(例如时区),或用户手动调整客户端时间导致时间同步,那么 **expires**** **将无法达到我们的预期效果。

Cache-Control

Cache-Control 可在请求报文与响应报文中设置

public表时响应可以被客户端和代理服务器缓存
private表示响应只可以被客户端缓存
max-age=xxx缓存xx秒后就过期,需要重新请求
s-maxage=xxx覆盖max-age,作用同上,只在代理服务器生效
no-store不缓存如何响应
no-cache资源被缓存,但是立即失效,下次会发起验证资源是否过期
max-stale=xxxxx秒内,即使缓存过期,也使用该缓存
min-fresh=xxx希望在xx秒内获取最新的响应
max-age
cache-control: max-age=600

如上图所示:**cache-control** 通过 **max-age** 来控制资源的有效期,value 值为时间长度,秒为单位,用数值表示。则代表在这个请求正确返回时间(浏览器也会记录下来)的 600 秒内再次加载资源,就会命中强缓存。完美地规避了时间戳带来的潜在问题。

s-maxage

细节注意: s-maxage 仅在代理服务器中生效,客户端中我们只考虑 max-age

cache-control: max-age=600, s-maxage=3600

s-maxage 就是用于表示 cache 服务器上(比如 cache CDN)的缓存的有效时间的,并只对 public 缓存有效 s-maxage 优先级高于 max-age,当两者同时出现时,优先考虑 s-maxage 如果 s-maxage 未过期,则向代理服务器请求其缓存内容

public:所有内容都将被缓存(客户端和代理服务器都可缓存) private:所有内容只有客户端可以缓存,Cache-Control的默认取值

简单概括: 两者同时存在的话,Cache-Control 优先级高于 Expires **expires** 与 **cache-control** 两者区别就在于 **expires** 是 **http1.0** 的产物,**cache-control** 是 **http1.1** 的产物。 在某些不支持 **http1.1** 的环境下,**expires** 就会发挥用处。所以 **expires** 其实是过时的产物,现阶段它的存在是一种向下兼容。 因此,**cache-control** 可以视作是 **expires** 的完全替代方案。

思考:

强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容较客户端是否已经发生了更新呢?此时我们需要协商缓存策略。

协商缓存

协商缓存依赖于服务端与浏览器之间的通信

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,服务器会根据这个请求的 request header 的一些参数来判断是否命中协商缓存,如果命中,**直接返回 304 状态码,内容为空,**并带上新的 response header 通知浏览器从缓存中读取资源;另外协商缓存需要与 cache-control 共同使用。

Last-Modified 和 If-Modified-Since
  • 当第一次请求资源时,服务器将资源传递给客户端时,会将资源最后更改的时间以 Last-Modified 的形式加在实体首部上一起返回给客户端;
  • 随后接下每次请求时,都会带上 If-Modified-Since 字段的时间戳字段,值为上一次 response 返回给它的 Last-Modified 值;
  • 服务器接收到这个时间戳后,会比对该时间戳和资源在服务器上的最后修改时间是否一致,从而判断资源是否发生了变化。如果发生了变化,就会返回一个完整的响应内容,并在 Response Headers 中添加新的 Last-Modified 值;否则,返回如上图的 304 响应,Response Headers 不会再添加 Last-Modified 字段。
Last-Modified: Sat, 29 Aug 2020 08:43:05 GMT If-Modified-Since: Sat, 29 Aug 2020 08:43:05
GMT

image.png 但 Last-Modified 存在一些缺点:

  • 某些服务端不能获取精确的修改时间
  • 文件修改时间改了,但文件内容却没有变

既然根据文件修改时间来决定是否缓存尚有不足,能否可以直接根据文件内容是否修改来决定缓存策略?如下介绍

ETag 和 If-None-Match

Etag 是上一次加载资源时,服务器返回的 response header,是对该资源的一种唯一标识,只要资源有变化,Etag 就会重新生成。浏览器在下一次加载资源向服务器发送请求时,会将上一次返回的 Etag 值放到 request header 里的 If-None-Match 里,服务器只需要比较客户端传来的 If-None-Match 跟自己服务器上该资源的 ETag 是否一致,就能很好地判断资源相对客户端而言是否被修改过了。如果服务器发现 ETag 匹配不上,那么直接以常规 GET 200 回包形式将新的资源(当然也包括了新的 ETag)发给客户端;如果 ETag 是一致的,则直接返回 304 知会客户端直接使用本地缓存即可。 ETag 和 If-None-Match

面试-页面性能-ETag和If-None-Match.png 两者之间对比: 首先在精确度上,Etag 要优于 Last-Modified。Last-Modified 的时间单位是秒,如果某个文件在 1 秒内改变了多次,那么他们的 Last-Modified 其实并没有体现出来修改,但是 Etag 每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的 Last-Modified 也有可能不一致。 第二在性能上,Etag 要逊于 Last-Modified,毕竟 Last-Modified 只需要记录时间,而 Etag 需要服务器通过算法来计算出一个 hash 值。 第三在优先级上,服务器校验优先考虑 Etag 缓存的机制 强制缓存优先于协商缓存进行,若强制缓存(Expires 和 Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since 和 Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回 304,继续使用缓存。主要过程如下:

3174701-bb7148a4431ccda1.png 缓存的机制 用户行为对浏览器缓存的影响

  1. 地址栏访问,链接跳转是正常用户行为,将会触发浏览器缓存机制;
  2. F5 刷新,浏览器会设置 max-age=0,跳过强缓存判断,会进行协商缓存判断;
  3. ctrl+F5 刷新,跳过强缓存和协商缓存,直接从服务器拉取资源。

性能分析 - Performance API

市面上实现对网页性能监控工具提供,主要也是依靠 Performance API

侧重点查看方法使用

  1. Performance.timing - 对象包含延迟相关的性能信息
  2. Performance.getEntries() - 基于给定的 filter 返回一个 [PerformanceEntry](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceEntry) 对象的列表。
  3. Performance.getEntriesByType() - 基于给定的 entry type 返回一个 [PerformanceEntry](https://developer.mozilla.org/zh-CN/docs/Web/API/PerformanceEntry) 对象的列表。
  4. Performance.now() - 返回一个表示从性能测量时刻开始经过的毫秒数 [DOMHighResTimeStamp](https://developer.mozilla.org/zh-CN/docs/Web/API/DOMHighResTimeStamp)。

timing已弃用

function performancePrintTiming() {
  /**
   * timing 已经弃用
   * 接口 Performance 的只读属性  timeOrigin 返回一个表示 the performance measurement 开始时间的高精度 timestamp
   */
  if (!window?.performance?.timing) {
    throw new Error('Browsers do not support timing')
  }
  const {
    connectEnd,
    connectStart,
    domComplete,
    domContentLoadedEventEnd,
    domContentLoadedEventStart,
    domInteractive,
    domLoading,
    domainLookupEnd,
    domainLookupStart,
    fetchStart,
    loadEventEnd,
    loadEventStart,
    navigationStart,
    redirectEnd,
    redirectStart,
    requestStart,
    responseEnd,
    responseStart,
    secureConnectionStart,
    unloadEventEnd,
    unloadEventStart,
  } = window.performance.timing

  console.log('准备新页面时间耗时: ', fetchStart - navigationStart)
  console.log('redirect 重定向耗时: ', redirectEnd - redirectStart)
  console.log('Appcache 耗时: ', domainLookupStart - fetchStart)
  console.log('unload 前文档耗时: ', unloadEventEnd - unloadEventStart)
  console.log('DNS 查询耗时: ', domainLookupEnd - domainLookupStart)
  console.log('TCP 连接耗时: ', connectEnd - connectStart)
  console.log('request 请求耗时: ', responseEnd - requestStart)
  console.log('请求完毕至 DOM 加载: ', domInteractive - responseEnd)
  console.log('解析 DOM 树耗时: ', domComplete - domInteractive)
  console.log('load事件耗时: ', loadEventEnd - loadEventStart)
  console.log('加载时间耗时: ', loadEventEnd - navigationStart)
  console.log('白屏时间: ', responseStart - navigationStart)
}

getEntriesByType

function performancePrint() {
  if (!window?.performance?.getEntriesByType) {
    throw new Error('Browsers do not support getEntriesByType')
  }
  const {
    connectEnd,
    connectStart,
    decodedBodySize,
    domComplete,
    domContentLoadedEventEnd,
    domContentLoadedEventStart,
    domInteractive,
    domainLookupEnd,
    domainLookupStart,
    duration,
    encodedBodySize,
    entryType,
    fetchStart,
    initiatorType,
    loadEventEnd,
    loadEventStart,
    name,
    nextHopProtocol,
    redirectCount,
    redirectEnd,
    redirectStart,
    requestStart,
    responseEnd,
    responseStart,
    secureConnectionStart,
    serverTiming = [],
    startTime,
    transferSize,
    type,
    unloadEventEnd,
    unloadEventStart,
    workerStart,
    workerTiming = [],
  } = window.performance.getEntriesByType('navigation')[0]

  console.log('准备新页面时间耗时: ', fetchStart - startTime)
  console.log('redirect 重定向耗时: ', redirectEnd - redirectStart)
  console.log('Appcache 耗时: ', domainLookupStart - fetchStart)
  console.log('unload 前文档耗时: ', unloadEventEnd - unloadEventStart)
  console.log('DNS 查询耗时: ', domainLookupEnd - domainLookupStart)
  console.log('TCP 连接耗时: ', connectEnd - connectStart)
  console.log('request 请求耗时: ', responseEnd - responseStart)
  console.log('请求完毕至 DOM 加载: ', domInteractive - responseEnd)
  console.log('解析 DOM 树耗时: ', domComplete - domInteractive)
  console.log('load事件耗时: ', loadEventEnd - loadEventStart)
  console.log('加载时间耗时: ', loadEventEnd - startTime)
  console.log('白屏时间: ', responseStart - startTime)
}

image.png window.performance.timing 时间戳与页面整个加载流程中的关键时间节点有着一一对应的关系

getEntries

可以查询所有资源的耗时,也可以根据资源类型来查看某种类型的资源耗时,PerformanceResourceTiming 对象

image.png

其他

SSR 服务器渲染:解决 SPA 框架带来的 JS 动态渲染页面带来的延迟和白屏问题

参考:

  • Performance API MDN docs
  • Performance API W3 docs
  • 彻底理解浏览器的缓存机制
  • 前端性能优化原理与实践
  • 设计一个无懈可击的浏览器缓存方案:关于思路,细节,ServiceWorker,以及HTTP/2

DOM 事件

  1. 基本概念:DOM 事件的级别
  2. DOM 事件模型
  3. DOM 事件流
  4. 描述 DOM 事件捕获的具体流
  5. Event 对象的常见应用
  6. 自定义事件
  7. 移动端触摸事件

事件级别

  DOM0 element.=function(){}

  DOM2 element.addEventListener('click', function(){}, false)    //冒泡false和捕获true

  DOM3 element.addEventListener('keyup', function(){}, false)

事件流

面试-DOM事件-事件流.png

描述 DOM 事件捕获的具体流程

面试-DOM事件-事件捕获流程.png

window.addEventListener(
  'click',
  function (e) {
    console.log('window captrue')
  },
  true
)

document.addEventListener(
  'click',
  function (e) {
    console.log('document captrue')
  },
  true
)

document.documentElement.addEventListener(
  'click',
  function (e) {
    console.log('html captrue')
  },
  true
)
document.body.addEventListener(
  'click',
  function (e) {
    console.log('body captrue')
  },
  true
)

ev.addEventListener(
  'click',
  function (e) {
    console.log('ev captrue')
  },
  true
)

Event 对象的常见应用

event.preventDefault() // 阻止默认事件(a标签)
event.stopPropagation() // 阻止冒泡
event.stoplmmediatePropagation() //绑定两个事件触发A事件阻止B事件发生
event.currentTarget //返回绑定事件的元素
event.target //返回触发事件的元素

自定义事件(code)

var eve = new Event('custome')
ev.addEventListener('custome', function () {
  console.log('custome')
})
events.addEventListener(
  'click',
  function () {
    window.dispatchEvent(eve)
  },
  true
)

移动端触摸事件

  1. touchstart:当手指触碰到屏幕的时候触发
  2. touchmove:当手指在屏幕上滑动的时候触发
  3. touchend:当手指离开屏幕的时候时候触发
  4. touchcancel 事件:当系统停止跟踪触摸的时候触发(少用)

touches:表示当前跟踪的触摸操作的 touch 对象的数组。

targetTouches:特定于事件目标的 Touch 对象的数组。

changeTouches:表示自上次触摸以来发生了什么改变的 Touch 对象的数组。

每个 touch 对象包含的属性:

  1. clientX:触摸目标在视口中的 x 坐标。
  2. clientY:触摸目标在视口中的 y 坐标。
  3. identifier:标识触摸的唯一 ID。
  4. pageX:触摸目标在页面中的 x 坐标。
  5. pageY:触摸目标在页面中的 y 坐标。
  6. screenX:触摸目标在屏幕中的 x 坐标。
  7. screenY:触摸目标在屏幕中的 y 坐标。
  8. target:触目的 DOM 节点目标。

冒泡

当点击一个元素触发事件时. 事件会先从元素的最外层父元素一层一层进入到触发的元素, 然后在从触发元素一层一层返回到最外层父元素, 从最外层一层一层进入的阶段叫事件捕获阶段, 从最里层一层一层往外的阶段叫事件冒泡

事件委托

事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件

好处:给重复的节点添加相同操作,减少 dom 交互,提高性能

实现思路:给父组件添加事件,通过事件冒泡,排查元素是否为指定元素,并进行系列操作

JS 运行机制

console.log(1)
setTimeout(function () {
  console.log(2)
}, 0)
console.log(3)
// 1 3 2

console.log('A')
while (1) {}
console.log('B')
// A
// while同步,永远不会执行到B(相当于一个死循环)、

console.log('A')
setTimeout(function () {
  console.log('B')
}, 0)
while (1) {}
// A
// 因为setTimeout是一个异步队列,在同步任务完成之前,任何的异步队列是不会被响应的

for (var i = 0; i < 4; i++) {
  setTimeout(function () {
    console.log(i)
  }, 1000)
}
// 4 4 4 4
// settimeout是异步执行,1000ms后往任务队列里面添加一个任务,只有主线上的全部执行完,才会执行任务队列里的任务,当主线执行完成后,i是4,所以此时再去执行任务队列里的任务时,i全部是4了。
// 对于打印4次是:每一次for循环的时候,settimeout都执行一次,但是里面的函数没有被执行,而是被放到了任务队列里面,等待执行,for循环了4次,就放了4次,当主线程执行完成后,才进入任务队列里面执行

JS 的单线程的概念

概念

JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。

一定时间内只能执行一项任务,不能执行多项任务,为了要执行的代码,就有一个 javascript 任务队列。基于这一概念,JS 执行任务时分为两种模式:同步和异步。

“同步模式”是指后一个任务必须等待前一个任务完成后再执行,前一个任务加载时会阻塞后面程序的进行;“异步模式”不一定按顺序执行任务,所以不会阻塞程序的运行。

注意

Web Worker标准的提出,为了利用多核CPU的计算能力,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以没有改变JavaScript单线程的本质。

大白话总结

同一时间只能做一件事情

任务队列

JS 任务队列中,有两类任务队列:宏任务队列(macro tasks)和微任务队列(micro tasks)。宏任务队列可以有多个,微任务队列只有一个。那么什么任务,会分到哪个队列呢?

  • 宏任务:script脚本执行(全局任务), setTimeout, setInterval, setImmediate定时事件, I/O操作, UI rendering渲染等。
  • 微任务:node中process.nextTick, Promise回调, Object.observer, Dom变化监听MutationObserver.

diagram3.jpg JS 运行整体流程

解释一下上图:

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入 Event Table 并注册函数。
  • 当指定的事情完成时,Event Table 会将这个函数移入 Event Queue。
  • 主线程内的任务执行完毕为空,会去 Event Queue 读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的 Event Loop (事件循环)。

那主线程执行栈何时为空呢?js引擎存在 monitoring process 进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去 Event Queue 那里检查是否有等待被调用的函数。

Event Loop

浏览器的 Event Loop 遵循的是 HTML5 标准,NodeJs 的 Event Loop 遵循的是 libuv

浏览器

  1. 取一个宏任务来执行。执行完毕后,下一步
  2. 取一个微任务来执行,执行完毕后,再取一个微任务来执行。直到微任务队列为空,执行下一步
  3. 更新 UI 渲染

Event Loop 会无限循环执行上面 3 步,这就是 Event Loop 的主要控制逻辑。其中,第 3 步(更新 UI 渲染)会根据浏览器的逻辑,决定要不要马上执行更新。毕竟更新 UI 成本大,所以,一般都会比较长的时间间隔,执行一次更新。

从执行步骤来看,我们发现微任务,受到了特殊待遇!我们代码开始执行都是从 script(全局任务)开始,所以,一旦我们的全局任务(属于宏任务)执行完,就马上执行完整个微任务队列。

代码举例
Promise.resolve().then(() => {
  console.log(1)
})

// 宏任务
setTimeout(() => {
  console.log(2)
}, 0)

var s = new Date()
while (new Date() - s < 50); // 阻塞50ms

Promise.resolve().then(() => {
  console.log(3)
})

new Promise(function (resolve, reject) {
  console.log(4)
  resolve()
}).then(function () {
  console.log(5)
})

process.nextTick(function () {
  console.log(6)
})

console.log(7)
代码解释

第一轮:主线程开始执行,遇到 Promise.then() 的回调函数丢到微任务队列中,再继续执行,遇到setTimeout ,将 setTimeout 的回调函数丢到宏任务队列中,(加 50ms 的阻塞,是因为 **setTimeout** 的 **delayTime** 最少是 4ms. 为了避免认为 **setTimeout** 是因为 4ms 的延迟而后面才被执行的,我们加了 50ms 阻塞),再继续执行,遇到 Promise.then() 的回调函数丢到微任务队列中,再继续执行,在往下执行 new Promise 立即执行,输出4,then 的回调函数丢到微任务队列中,再继续执行,遇到process.nextTick,同样将回调函数扔到为任务队列,再继续执行,输出7; 当所有同步任务执行完成后看有没有可以执行的微任务,发现有 then 函数和 nextTick 两个微任务,先执行哪个呢?process.nextTick 指定的异步任务总是发生在所有异步任务之前,因此先执行process.nextTick 输出6然后执行第一个 then 函数输出1,同理输出3和输出5,第一轮执行结束; 第二轮:从宏任务队列开始,发现 setTimeout 回调,输出2执行完毕。

因此结果是: 4 7 6 1 3 5 2

面试回答
  • 首先 js 是单线程运行的,在代码执行时,通过任务进入执行栈并判断任务类型来保证代码的有序执行。
  • 在执行同步代码的时候,如果遇到了异步事件,挂起该任务,继续执行执行栈中的其他任务
  • 当同步事件执行完毕后,将异步事件对应的回调加入到与当前执行栈中不同的另一个任务队列中等待执行。
  • 任务队列可以分为宏任务队列和微任务队列,如果当前执行栈中的事件执行完毕后,判断微任务队列中是否有任务可以执行,如果有就将微任务队首的事件压入栈中执行。
  • 当微任务队列中的任务都执行完成后再去判断宏任务对列中的任务。

NodeJs

  1. 初始化 Event Loop
  2. 执行您的主代码。这里同样,遇到异步处理,就会分配给对应的队列。直到主代码执行完毕。
  3. 执行主代码中出现的所有微任务:先执行完所有 nextTick(),然后在执行其它所有微任务。
  4. 开始 Event Loop

NodeJs 的 Event Loop 分 6 个阶段执行:

     ┌───────────────────────────┐
  ┌─>│           timers          │
  │  └─────────────┬─────────────┘
  │  ┌─────────────┴─────────────┐
  │  │     pending callbacks     │
  │  └─────────────┬─────────────┘
  │  ┌─────────────┴─────────────┐
  │  │       idle, prepare       │
  │  └─────────────┬─────────────┘      ┌───────────────┐
  │  ┌─────────────┴─────────────┐      │   incoming:   │
  │  │           poll            │<─────┤  connections, │
  │  └─────────────┬─────────────┘      │   data, etc.  │
  │  ┌─────────────┴─────────────┐      └───────────────┘
  │  │           check           │
  │  └─────────────┬─────────────┘
  │  ┌─────────────┴─────────────┐
  └──┤      close callbacks      │
     └───────────────────────────┘

以上的 6 个阶段,具体处理的任务如下:

  • **timers: **这个阶段执行 setTimeout() 和 setInterval() 设定的回调
  • pending callbacks(I/O callbacks):会执行除了timer,close,setImmediate之外的事件回调
  • idle, prepare: 仅系统内部使用
  • poll: 轮训,不断检查有没有新的 I/O callback 事件,在适当的条件下会阻塞在这个阶段,主要分为如下两个步骤:
    • 先查看 poll queue 中是否有事件,有任务就按先进先出的顺序依次执行回调
    • 当 queue 为空时,会检查是否有 setImmediate() 的 callback,如果有就进入 check 阶段执行这些 callback。但同时也会检查是否有到期的 timer,如果有,就把这些到期的 timer 的 callback按照调用顺序放到 timer queue 中,之后循环会进入 timer 阶段执行 queue 中的 callback。
  • check: 执行 setImmediate() 设定的回调
  • close callbacks: 执行比如 socket.on('close', ...) 的回调

每个阶段执行完毕后,都会执行所有微任务(先 nextTick,后其它),然后再进入下一个阶段。

理解 process.nextTick() 您可能已经注意到 process.nextTick() 在关系图中没有显示,即使它是异步 API 的一部分。这是因为 process.nextTick() 在技术上不是事件循环的一部分。相反,无论事件循环的当前阶段如何,都将在当前操作完成后处理 nextTickQueue。这里的一个操作被视作为一个从 C++ 底层处理开始过渡,并且处理需要执行的 JavaScript 代码。

回顾上面关系图,任何时候在给定的阶段中调用 process.nextTick(),所有传递到 process.nextTick() 的回调将在事件循环继续之前得到解决。这可能会造成一些糟糕的情况, 因为它允许您通过进行递归 process.nextTick() 来“饿死”您的 I/O 调用,阻止事件循环到达 轮询 阶段。

Links

  • JavaScript 异步、栈、事件循环、任务队列
  • JavaScript:彻底理解同步、异步和事件循环(Event Loop)
  • Event loops
  • NodeJs 的 Event Loop 官方文档
  • JavaScript 运行机制详解:再谈 Event Loop - 阮一峰的网络日志

原型链

  • 创建对象的几种方法
  • 原型、构造函数、实例、原型链
  • instanceof 的原理
  • new 运算符

创建对象的几种方法

// 第一种方式:字面量
var o1 = { name: 'o1' }
var o2 = new Object({ name: 'o2' })

// 第二种方式:构造函数
var M = function (name) {
  this.name = name
}
var o3 = new M('o3')

// 第三种方式:Object.create
var p = { name: 'p' }
var o4 = Object.create(p)

console.log(o1) // { name: 'o1' }
console.log(o2) // { name: 'o2' }
console.log(o3) // M { name: 'o3' }
console.log(o4) // {}

原型、构造函数、实例、原型链

面试-原型链.png

对象是函数创建的,而函数却又是一种对象。也是属性的集合,可以对函数进行自定义属性。 每个函数都有一个属性叫做 prototype。 这个 prototype 的属性值是一个对象,默认的只有一个叫做 constructor 的属性,指向这个函数本身。

var Fn = function () {
  this.name = 'WuChenDi'
  this.old = 23
}
var f1 = new Fn()
console.log(f1) // Fn { name: 'WuChenDi', old: 23 }
console.log(Fn)
console.log(Fn.prototype)
console.log(f1.__proto__ === Fn.prototype) // true
console.log(Fn.prototype.constructor === Fn) // true

面试-原型链-原型与原型链.png

从上图可以看到对象的原型,指向构造函数的 prototype 属性。 f1.proto指向构造函数的 prototype 属性,就是对象的原型。所以 f1.proto === Fn.prototype 为 true prototype 的属性值是一个对象,默认的只有一个叫做 constructor 的属性,指向这个函数本身。 proto就是指向构造函数的 prototype 属性,Fn.prototype 就是对象的原型 即,每个由构造函数创建的对象都有一个proto属性,指向创建该对象的函数的 prototype。

var 对象名 = new 函数名()
对象名.**proto**=== 函数名.prototype

instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

面试-原型链-instanceof.png

// 第一种方式:字面量
var o1 = { name: 'o1' }
var o2 = new Object({ name: 'o2' })

// 第二种方式:构造函数
var M = function (name) {
  this.name = name
}
var o3 = new M('o3')

// 第三种方式:Object.create
var p = { name: 'p' }
var o4 = Object.create(p)

console.log(o3 instanceof M) // true
console.log(o3 instanceof Object) // true
console.log(o3.__proto__ === M.prototype) // true
console.log(M.prototype.__proto__ === Object.prototype) // true
console.log(o3.__proto__.constructor === M) // true

new 运算符

  • 一个新对象被创建。他继承自foo.prototype
  • 构造函数 foo 被执行。执行的时候,相应的传参会被传入,同时上下文 (this) 会被指定为这个新实例。 new foo 等同于 new foo(), 只能用在不传递如何参数的情况
  • 如果构造函数返回一个 "对象" ,那么这个对象会取代整个 new 出来的结果。如果构造函数没有返回对象,那么 new 出来的结果为步骤1创建的对象
// 第一种方式:字面量
var o1 = { name: 'o1' }
var o2 = new Object({ name: 'o2' })

// 第二种方式:构造函数
var M = function (name) {
  this.name = name
}
var o3 = new M('o3')

// 第三种方式:Object.create
var p = { name: 'p' }
var o4 = Object.create(p)

M.prototype.say = function () {
  console.log('say hi')
}

var o5 = new M('o5')

var new2 = function (func) {
  var o = Object.create(func.prototype)
  var k = func.call(o)
  if (typeof k === 'object') {
    return k
  } else {
    return o
  }
}

console.log((o6 = new2(M))) // M { name: undefined }
console.log(o6 instanceof M) // true
console.log(o6 instanceof Object) // true
console.log(o6.__proto__.constructor === M) // true

M.prototype.walk = function () {
  console.log('walk')
}

o6.walk() // walk
o3.walk() // walk

常问

JS 的 new 操作符做了哪些事情?

new 操作符新建了一个空对象,这个对象原型指向构造函数的 prototype,执行构造函数后返回这个对象。

面向对象

类与实例

类的声明

// 类的声明(es5)
var Animal = function () {
  this.name = 'Animal'
}

// 类的声明(es6)
class Animal2 {
  constructor() {
    this.name = 'Animal2'
  }
}

生成实例

// 类的声明(es5)
var Animal = function () {
  this.name = 'Animal'
}

// 类的声明(es6)
class Animal2 {
  constructor() {
    this.name = 'Animal2'
  }
}

// 实例化
console.log(new Animal()) // Animal { name: 'Animal' }
console.log(new Animal2()) // Animal2 { name: 'Animal2' }

继承实现

本质就是原型链

概念

说到继承的概念,首先要说一个经典的例子。 先定义一个类(Class)叫汽车,汽车的属性包括颜色、轮胎、品牌、速度、排气量等,由汽车这个类可以派生出“轿车”和“货车”两个类,那么可以在汽车的基础属性上,为轿车添加一个后备厢、给货车添加一个大货箱。这样轿车和货车就是不一样的,但是二者都属于汽车这个类,这样从这个例子中就能详细说明汽车、轿车以及卡车之间的继承关系。 继承可以使得子类别具有父类的各种方法和属性,比如上面的例子中“轿车” 和 “货车” 分别继承了汽车的属性,而不需要再次在“轿车”中定义汽车已经有的属性。在“轿车”继承“汽车”的同时,也可以重新定义汽车的某些属性,并重写或覆盖某些属性和方法,使其获得与“汽车”这个父类不同的属性和方法。 继承的基本概念就初步介绍这些,下面我们就来看看 JavaScript 中都有哪些实现继承的方法。

继承的实现方式

构造函数继承

特点: 子类实例共享父类引用属性的问题 创建子类实例时,可以向父类传递参数 可以实现多继承(call多个父类对象)

缺点: 实例并不是父类的实例,只是子类的实例 只能继承父类的实例属性和方法,不能继承原型属性/方法 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

function Parent1() {
  this.name = 'parent1'
}
Parent1.prototype.say = function () {}

function Child1() {
  Parent1.call(this)
  this.type = 'child1'
}
console.log(new Child1(), new Child1().say())
原型链继承

特点: 解决子类实例共享父类引用属性的问题 创建子类实例时,可以向父类传递参数 可以实现多继承(call多个父类对象)(不完美,没有父类方法)

缺点: 实例并不是父类的实例,只是子类的实例 只能继承父类的实例属性和方法,不能继承原型属性/方法 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

function Parent2() {
  this.name = 'parent2'
  this.play = [1, 2, 3]
}

function Child2() {
  this.type = 'child2'
}
Child2.prototype = new Parent2()

var s1 = new Child2()
var s2 = new Child2()
console.log(s1.play, s2.play) // [ 1, 2, 3 ] [ 1, 2, 3 ]
s1.play.push(4)
// TODO: 原型链中的原型对象共用
console.log(s1.play, s2.play) // [ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ]
console.log(s1.__proto__ === s2.__proto__) // true
组合方式继承

特点: 解决原型链实现继承的缺点

缺点: 父级构造函数执行了两次,影响性能

function Parent3() {
  this.name = 'parent3'
  this.play = [1, 2, 3]
}

function Child3() {
  Parent3.call(this)
  this.type = 'child3'
}
Child3.prototype = new Parent3()
var s3 = new Child3()
var s4 = new Child3()
s3.play.push(4)
console.log(s3.play, s4.play) // [ 1, 2, 3, 4 ] [ 1, 2, 3 ]
组合继承 - 优化1

特点: 父类执行一次(子类实例化执行),Child4.prototype = Parent4.prototype解释(因为都是对象,都是引用类型,不会执行父级构造函数)

缺点: 如何区分一个对象是由它的子类实例化的,还是父类实例化的(s5是Child4还是Parent4直接实例化的)

function Parent4() {
  this.name = 'parent4'
  this.play = [1, 2, 3]
}

function Child4() {
  Parent4.call(this)
  this.type = 'child4'
}
Child4.prototype = Parent4.prototype
var s5 = new Child4()
var s6 = new Child4()
console.log(s5, s6) // Parent4 { name: 'parent4', play: [ 1, 2, 3 ], type: 'child4' } Parent4 { name: 'parent4', play: [ 1, 2, 3 ], type: 'child4' }

console.log(s5 instanceof Child4, s5 instanceof Parent4) // true true
console.log(s5.constructor) // [λ: Parent4]
组合继承 - 优化2

Child5.prototype = Object.create(Parent5.prototype)解释:Object.create来创建一个中间对象,把两个原型对象区分开,这个中间对象还具备一个特性,它的原型对象是父类的原型对象,这样就可以连起来,通过给Child5的原型对象的constructor做修改,就可以正常区分开父类和子类的构造函数

function Parent5() {
  this.name = 'parent5'
  this.play = [1, 2, 3]
}

function Child5() {
  Parent5.call(this)
  this.type = 'child5'
}
Child5.prototype = Object.create(Parent5.prototype)
Child5.prototype.constructor = Child5

var s7 = new Child5()
console.log(s7 instanceof Child5, s7 instanceof Parent5) // true true
console.log(s7.constructor) // [λ: Child5]

数据结构与 GC

数据类型

原始值类型(值类型/基本数据类型)

  • Number
  • String
  • Boolean
  • Null
  • Undefined
  • Object
  • Symbol
  • BigInt

对象类型(引用数据类型)

  • 标准普通对象:object
  • 标准特殊对象:Array、RegExp、Date、Math、Error……
  • 非标准特殊对象:Number、String、Boolean……
  • 可调用/执行对象"函数":function

类型转化

显式类型强制转换是指当开发人员通过编写适当的代码用于在类型之间进行转换

Number(value)

隐式类型转换是指在对不同类型的值使用运算符时,值可以在类型之间自动的转换

1 == null

在 JS 中只有 3 种类型的转换

  • 转化为 Number 类型:Number() / parseFloat() / parseInt()
  • 转化为 String 类型:String() / toString()
  • 转化为 Boolean 类型: Boolean()

堆/栈

  • JS 就是动态语言,因为在声明变量之前并不需要确认其数据类型,所以 JS 的变量是没有数据类型的,值才有数据类型,变量可以随时持有任何类型的数据。
  • JS 数据类型可分为两大类——基本类型和引用类型。
    • 基本类型的数据是存放在栈中,引用类型的数据是存放在堆中的。
    • 堆中的数据是通过引用和变量关联起来的。
    • 闭包除外,JS 闭包中的变量值并不保存中栈内存中,而是保存在堆内存中。

垃圾回收

什么是垃圾回收?

手动回收和自动回收

通常情况下,垃圾数据回收分为手动回收和自动回收两种策略。

手动回收

例如以前学 C 的时候,何时分配内存、何时销毁内存都是由代码控制的,如下:

image.png

自动回收

自动垃圾回收的策略,如 JavaScript,产生的垃圾数据是由垃圾回收器来释放的,并不需要手动通过代码来释放。下面记录几个小疑惑。

  • **对于栈中的垃圾回收,**JS 引擎通过向下移动 ESP(extended stack pointer当前执行状态针)指针(记录调用栈当前执行状态的指针),来销毁该函数保存在栈中的执行上下文(变量环境、词法环境、this、outer),这里是不需要通过 V8 的垃圾回收机制的,效率非常高。
  • **对于堆中的垃圾回收,**主要通过副垃圾回收器(新生代)和主垃圾回收器(老生代)负责的,副垃圾回收器采用 scavenge 算法将区域分为对象区域和空闲区域,通过两个区域的反转让新生代区域无限使用下去。主垃圾回收器采用Mark-Sweep(Mark-Compact Incremental Marking 解决不同场景下问题的算法改进)算法进行空间回收的。无论是主副垃圾回收器的策略都是标记-清除-整理三个大的步骤。另外还有新生代的晋升策略(两次未清除的),大对象直接分配在老生代。
新生代和老生代
新生代

算法:Scavenge 算法

Note

原理:

  1. 把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域。
  2. 新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。
  3. 先对对象区域中的垃圾做标记,标记完成之后,把这些存活的对象复制到空闲区域中
  4. 完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。

对象晋升策略:经过两次垃圾回收依然还存活的对象,会被移动到老生区中。

老生代

算法:标记 - 清除(Mark-Sweep)算法

Note

原理:

  1. 标记:标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
  2. 清除:将垃圾数据进行清除。
  3. 碎片:对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片。而碎片过多会导致大对象无法分配到足够的连续内存。 :::

算法:标记 - 整理(Mark-Compact)算法

Note

原理:

  1. 标记:和标记 - 清除的标记过程一样,从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素标记为活动对象。
  2. 整理:让所有存活的对象都向内存的一端移动
  3. 清除:清理掉端边界以外的内存 :::

优化算法:增量标记(Incremental Marking)算法

Note

原理:

  1. 为了降低老生代的垃圾回收而造成的卡顿
  2. V8把一个完整的垃圾回收任务拆分为很多小的任务
  3. 让垃圾回收标记和 JavaScript 应用逻辑交替进行 :::

判断内存泄漏

  • 可能长时间运行页面卡顿,感官上猜测可能会有内存泄漏,通过DynaTrace(IE)、Performance(Chrome)等工具收集数据。
  • 在页面进行各种交互,在一定的时间内(越长越好),生成统计数据,观察内存变化(timeline),如变化规律为周期且平稳的,则不存在泄漏可能。但是变化趋势是向上,就存在内部泄漏的可能!
  • 但有个小问题,在统计图表发现heap、document、Nodes、Listeners等。这个变化趋势是由什么控制的?--- js堆内存吗

工作中避免内存泄漏

  • 确定不使用的临时变量置为 null
  • es6 普及场景下少使用闭包也是一种方法

闭包

闭包就是能够读取其他函数内部变量的函数,或者子函数在外调用,子函数所在的父函数的作用域不会被释放。 当在一个函数内定义另外一个函数就会产生闭包。

作用

为什么要用: 匿名自执行函数:我们知道所有的变量,如果不加上 var 关键字,则默认的会添加到全 局对象的属性上去,这样的临时变量加入全局对象有很多坏处,比如:别的函数可能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链 上遍历的)。除了每次使用变量都是用 var 关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,可以用闭包。

结果缓存:我们开发中会碰到很多情况,设想我们有一个处理过程很耗时的函数对象, 每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。

Links

浏览器工作原理与实践:https://time.geekbang.org/column/article/131233

Virtual DOM

Virtual DOM 就是用 JS 对象去 模拟 DOM 结构,它是真实 DOM 的抽象,只保留一些有用的信息,更轻量地描述 DOM 树的结构;新旧 vnode 对比,得出最小的更新范围,最后更新 DOM;数据驱动视图的模式下,有效控制 DOM 操作。

简单虚拟 DOM 结构

DOM 节点

<div id="app" class="container">
  <p>我是p标签</p>
  <ul style="background-color: red">
    <li>我是a标签</li>
  </ul>
</div>

js模拟 vnode 节点

{
	tag: "div",
	props: {
		className: "container",
		id: "app",
	},
	children: [
		{
			tag: "p",
			children: "我是p标签",
		},
		{
			tag: "ul",
			props: { style: "background-color: red" },
			children: [
				{
					tag: "li",
					children: "我是a标签",
				},
			],
		},
	],
};

snabbdom使用

案例代码:https://github.com/WuChenDi/Front-End/tree/master/12-snabbdom/virtual-dom

index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>snabbdom</title>
  </head>
  <body>
    <div id="app">app</div>
    <div id="container">container</div>
    <button id="btn-change">change</button>
    <script src="./src/index.js"></script>
  </body>
</html>

index.js

// index.js
{
  import { init, h } from 'snabbdom'

  let patch = init([])

  let vnode = h('div', 'Hello World')
  let app = document.querySelector('#app')
  console.log(app)
  let oldVnode = patch(app, vnode)

  vnode = h('div#dd', 'Hello cdd')
  patch(oldVnode, vnode)
}

{
  import { init, h } from 'snabbdom'
  let patch = init([])

  let vnode = h('div#container', [
    h('h1', '我是h1标签'),
    h('ul', [h('li', '我是li标签')]),
  ])

  let app = document.querySelector('#app')
  patch(app, vnode)

  setTimeout(() => {
    var newVnode = h('div#container', [h('h2', '我是h2标签')])
    patch(vnode, newVnode)
  }, 2000)

  // setTimeout(() => {
  // 	var newVnode = h("div#container", [h("h2", "我是h2标签")]);
  // 	patch(vnode, h("!"));
  // }, 2000);
}

{
  import { init, h } from 'snabbdom'

  // 定义 patch
  const patch = init([
    snabbdom_class,
    snabbdom_props,
    snabbdom_style,
    snabbdom_eventlisteners,
  ])

  const container = document.getElementById('container')

  // 生成 vnode
  const vnode = h('ul#list', {}, [h('li.item', {}, 'Item 1'), h('li.item', {}, 'Item 2')])

  patch(container, vnode)

  document.getElementById('btn-change').addEventListener('click', () => {
    // 生成 newVnode
    const newVnode = h('ul#list', {}, [
      h('li.item', {}, 'Item 1'),
      h('li.item', {}, 'Item B'),
      h('li.item', {}, 'Item 3'),
    ])
    patch(vnode, newVnode)
  })
}

diff算法

步骤

用 js 对象来描述 dom 树结构,然后用这个 js 对象来创建一棵真正的 dom 树,插入到文档中;当状态更新时,将新的 js 对象和旧的 js 对象进行比较,得到两个对象之间的差异;最后将差异应用到真正的 dom 上。

复杂度

树 diff 的时间复杂度 O(n^3)

第一次需要对 oldVnode 遍历一次tree 然后第二次对 vnode 遍历tree 最后两者排序

优化时间复杂度到 O(n)

只比较同一层级,不跨级比较 tag 不相同,则直接删掉重建,不再深度比较 tag 与 key,两者都相同,则认为是相同节点,不再深度比较

diagram1.jpg

diagram1.jpg

snabbdom源码分析

https://github.com/snabbdom/snabbdom > https://github.com/coconilu/Blog/issues/152

案例分析

import {
  init,
  classModule,
  propsModule,
  styleModule,
  eventListenersModule,
  h,
} from 'snabbdom'

const patch = init([
  // Init patch function with chosen modules
  classModule, // makes it easy to toggle classes
  propsModule, // for setting properties on DOM elements
  styleModule, // handles styling on elements with support for animations
  eventListenersModule, // attaches event listeners
])

const container = document.getElementById('container')

const vnode = h('div#container.two.classes', { on: { click: someFn } }, [
  h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),
  ' and this is just normal text',
  h('a', { props: { href: '/foo' } }, "I'll take you places!"),
])
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode)

const newVnode = h('div#container.two.classes', { on: { click: anotherEventHandler } }, [
  h(
    'span',
    { style: { fontWeight: 'normal', fontStyle: 'italic' } },
    'This is now italic type'
  ),
  ' and this is still just normal text',
  h('a', { props: { href: '/bar' } }, "I'll take you places!"),
])
// Second `patch` invocation
patch(vnode, newVnode) // Snabbdom efficiently updates the old view to the new state
大白话解释

首先 snabbdom 模块提供一个 init 方法,调用 init 方法会返回一个 patch 函数,第一个参数 oldVNode 或 DOM,第二个参数是新的 VNode 节点,调用 patch 函数会对 DOM 进行更新。通过调用 h 函数来创建 VNode。初始化 patch(container, vnode),container作为应用根节点,更新patch(vnode, newVnode)。

hook 钩子
NameTriggered when触发时机Arguments to callback
prethe patch process beginspatch 开始之前none
inita vnode has been added已经创建了一个 vnodevnode
createa DOM element has been created based on a vnode已经基于 vnode 创建了一个 DOM,但尚未挂载emptyVnode, vnode
insertan element has been inserted into the DOM创建的 DOM 被挂载了vnode
prepatchan element is about to be patched一个元素即将被 patcholdVnode, vnode
updatean element is being updated元素正在被更新oldVnode, vnode
postpatchan element has been patched元素已经 patch 完毕oldVnode, vnode
destroyan element is directly or indirectly being removed一个元素被直接或间接地移除了。间接移除的情况是指被移除元素的子元素vnode
removean element is directly being removed from the DOM一个元素被直接移除了(卸载)vnode, removeCallback
postthe patch process is donepatch 结束none

源码分析

https://github.com/WuChenDi/Front-End/blob/master/12-snabbdom/3.0.1

vnode
export interface VNode {
  sel: string | undefined // selector
  data: VNodeData | undefined
  children: Array<VNode | string> | undefined // 子节点
  elm: Node | undefined // element, 存储 HTMLELement
  text: string | undefined // 文本节点
  key: Key | undefined // 节点 key
}

export interface VNodeData {
  props?: Props // 节点属性
  attrs?: Attrs // 节点 attribute 属性
  class?: Classes // class 类名
  style?: VNodeStyle // style 样式
  dataset?: Dataset // html 自定义属性 data-
  on?: On // 事件
  attachData?: AttachData
  hook?: Hooks // 钩子
  key?: Key
  ns?: string // for SVGs
  fn?: () => VNode // for thunks
  args?: any[] // for thunks
  is?: string // for custom elements v1
  [key: string]: any // for any other 3rd party module
}
h 函数

h 渲染函数实际上只用于创建 vnode,并没有将 vnode 绘制到文档中。作为接口,h 渲染函数本身也只包含参数多态的处理,由于存在多种参数情况,所以首先会对参数进行格式化,在对 children 处理,如果是 svg 元素,调用 addNS 函数处理,最后返回 vnode 。

export function h(sel: string): VNode
export function h(sel: string, data: VNodeData | null): VNode
export function h(sel: string, children: VNodeChildren): VNode
export function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export function h(sel: any, b?: any, c?: any): VNode {
  let data: VNodeData = {}
  let children: any
  let text: any
  let i: number
  // 参数格式化
  if (c !== undefined) {
    if (b !== null) {
      data = b
    }
    if (is.array(c)) {
      children = c
    } else if (is.primitive(c)) {
      text = c
    } else if (c && c.sel) {
      children = [c]
    }
  } else if (b !== undefined && b !== null) {
    if (is.array(b)) {
      children = b
    } else if (is.primitive(b)) {
      text = b
    } else if (b && b.sel) {
      children = [b]
    } else {
      data = b
    }
  }
  // 如果存在 children,将不是 vnode 的项转成 vnode
  if (children !== undefined) {
    for (i = 0; i < children.length; ++i) {
      if (is.primitive(children[i]))
        children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
    }
  }
  // svg 元素添加 namespace
  if (
    sel[0] === 's' &&
    sel[1] === 'v' &&
    sel[2] === 'g' &&
    (sel.length === 3 || sel[3] === '.' || sel[3] === '#')
  ) {
    addNS(data, children, sel)
  }
  // 返回 vnode
  return vnode(sel, data, children, text, undefined)
}
sameVnode 函数

sameVnode 函数的主要逻辑是用来判断 vnode 节点是否为相同

function sameVnode(vnode1: VNode, vnode2: VNode): boolean {
  // key 和 sel 都相等
  // 如果 key 没有传入 => undefined === undefined // true
  // 不传 key 情况分析:不在循环体里面,像直接定义的情况,直接通过 tag/sel 来比较
  const isSameKey = vnode1.key === vnode2.key
  const isSameIs = vnode1.data?.is === vnode2.data?.is
  const isSameSel = vnode1.sel === vnode2.sel

  return isSameSel && isSameKey && isSameIs
}
createKeyToOldIdx 函数

createKeyToOldIdx 函数 返回 map 将所有定义了 key 的 oldVNode 在数组中的 index 作为键值,key作为键名存储起来,然后赋给 oldKeyToIdx(updateChildren 方法使用)

function createKeyToOldIdx(
  children: VNode[],
  beginIdx: number,
  endIdx: number
): KeyToIndexMap {
  const map: KeyToIndexMap = {}
  for (let i = beginIdx; i <= endIdx; ++i) {
    const key = children[i]?.key
    if (key !== undefined) {
      map[key as string] = i
    }
  }
  return map
}
createElm 函数
大白话

createElm 函数的主要逻辑在于创建真实的 DOM 节点 vnode.elm。首先调用 init hook ,然后创建 DOM 树分为如下三种情形:

  • vnode.sel === '!' 表示当前元素是注释节点,调用 createComment 创建注释节点,挂载到 vnode.elm;
  • vnode.sel !== 'undefined',对当前选择器进行解析(tag、id,class等),调用 createElementNS 或 createElement 生成 elm,接着调用 create hook,如果存在 children,遍历所有子节点并递归调用 createElm 创建 dom,通过 appendChild 挂载到当前的 elm 上,不存在 children 但存在 text,便使用 createTextNode 来创建文本,再次调用 create hook 与 insert hook 存储起来等 dom 插入后才会调用,这里用个数组来保存能避免调用时再次对 vnode 树做遍历;
  • 以上两种都不满足时,表示当前只是 text,调用 createTextNode 来创建文本,然后挂载到 vnode.elm;
流程图

diagram1.jpg

coding
function createElm(vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
  let i: any
  let data = vnode.data
  if (data !== undefined) {
    // 调用 init hook
    const init = data.hook?.init
    if (isDef(init)) {
      init(vnode)
      data = vnode.data
    }
  }
  const children = vnode.children
  const sel = vnode.sel
  // 注释节点
  if (sel === '!') {
    if (isUndef(vnode.text)) {
      vnode.text = ''
    }
    // 创建注释节点
    vnode.elm = api.createComment(vnode.text!)
  } else if (sel !== undefined) {
    // Parse selector
    const hashIdx = sel.indexOf('#')
    const dotIdx = sel.indexOf('.', hashIdx)
    const hash = hashIdx > 0 ? hashIdx : sel.length
    const dot = dotIdx > 0 ? dotIdx : sel.length
    const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
    const elm = (vnode.elm =
      isDef(data) && isDef((i = data.ns))
        ? api.createElementNS(i, tag, data)
        : api.createElement(tag, data))

    if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
    if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))

    // 调用 create hook
    for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)

    // 挂载子节点(递归创建节点)
    if (is.array(children)) {
      for (i = 0; i < children.length; ++i) {
        const ch = children[i]
        if (ch != null) {
          api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
        }
      }
    } else if (is.primitive(vnode.text)) {
      api.appendChild(elm, api.createTextNode(vnode.text))
    }
    const hook = vnode.data!.hook
    if (isDef(hook)) {
      // 调用 create hook
      hook.create?.(emptyNode, vnode)
      if (hook.insert) {
        // insert hook 存储起来 等 dom 插入后才会调用,这里用个数组来保存能避免调用时再次对 vnode 树做遍历
        insertedVnodeQueue.push(vnode)
      }
    }
  } else {
    // 文本节点
    vnode.elm = api.createTextNode(vnode.text!)
  }
  return vnode.elm
}
patchVnode 函数
大白话

patchVnode 函数的主要逻辑在于更新节点。首先调用 vnode 上的 prepatch hook,如果当前的两个 vnode 完全相同,直接返回。然后分如下两大类情形:

  • vnode.text === undefined
    • 新旧节点都存在 children,执行 updateChildren 更新
    • 存在新 children,不存在旧 children,如果旧 text 存在,则先清空在调用 addVnodes 添加children
    • 不存在新 children,存在旧 children,调用 removeVnodes 移除旧节点的 children
    • oldVnode 存在 text,调用 setTextContent 置空
  • vnode.text !== undefined,移除 oldVnode 对应的 dom 节点,并使用 setTextContent 更新 vnode.elm
流程图

diagram1.jpg

coding
function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
  // 执行 prepatch hook
  const hook = vnode.data?.hook
  hook?.prepatch?.(oldVnode, vnode)
  // 设置 vnode.elm
  const elm = (vnode.elm = oldVnode.elm)!
  // 旧 children
  const oldCh = oldVnode.children as VNode[]
  // 新 children
  const ch = vnode.children as VNode[]
  // 如果 oldVnode 和 vnode 是完全相同,说明无需更新,直接返回。
  // 极少这种情况,除非人为测试
  if (oldVnode === vnode) return
  // hook 相关
  if (vnode.data !== undefined) {
    // 调用 update hook
    for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)

    // 调用 vnode update hook
    vnode.data.hook?.update?.(oldVnode, vnode)
  }
  // 新test(vnode.text) === undefined (vnode.children != undefined) / (vnode.children 一般有值)
  // text 与 children 不可能共存,但是都为 undefined 成立
  if (isUndef(vnode.text)) {
    // 新旧都有 children
    if (isDef(oldCh) && isDef(ch)) {
      // 新旧节点都存在 children 就执行 updateChildren
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
    }
    // 存在新 children,不存在旧 children(有可能存在旧 text)
    else if (isDef(ch)) {
      // 如果旧 text 存在值,先置空
      if (isDef(oldVnode.text)) api.setTextContent(elm, '')
      // 添加 children
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    }
    // 存在旧 children,不存在新 children(有可能存在旧 text)
    else if (isDef(oldCh)) {
      // 移除旧节点的 children
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      // 旧节点存在 text 置空
      api.setTextContent(elm, '')
    }
  }
  // else: vnode.text !== undefined (vnode.children 无值)
  else if (oldVnode.text !== vnode.text) {
    // 移除旧 children
    if (isDef(oldCh)) {
      // 新节点删除了 children ,删除老的 DOM 元素
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    }
    // 文本节点更新
    api.setTextContent(elm, vnode.text!)
  }
  // 调用 postpatch hook
  hook?.postpatch?.(oldVnode, vnode)
}
updateChildren 函数
大白话
流程图
coding
function updateChildren(
  parentElm: Node,
  oldCh: VNode[],
  newCh: VNode[],
  insertedVnodeQueue: VNodeQueue
) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx: KeyToIndexMap | undefined
  let idxInOld: number
  let elmToMove: VNode
  let before: any

  // 遍历 oldCh newCh,对节点进行比较和更新
  // 每轮比较最多处理一个节点,算法复杂度 O(n)
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (oldStartVnode == null) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
    } else if (oldEndVnode == null) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (newStartVnode == null) {
      newStartVnode = newCh[++newStartIdx]
    } else if (newEndVnode == null) {
      newEndVnode = newCh[--newEndIdx]
    }
    // 以旧 vnode 首节点和新 vnode 首节点对比
    else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    }
    // 以旧 vnode 尾节点和新 vnode 尾节点对比
    else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    }
    // 以旧 vnode 首节点和新 vnode 尾节点对比
    else if (sameVnode(oldStartVnode, newEndVnode)) {
      // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    }
    // 以旧 vnode 尾节点和新 vnode 新节点对比
    else if (sameVnode(oldEndVnode, newStartVnode)) {
      // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
      api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    }
    // 以上4中都未命中
    else {
      if (oldKeyToIdx === undefined) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      }
      // 拿新节点 key , 能否对应上 oldCh 中的某个节点的 key
      idxInOld = oldKeyToIdx[newStartVnode.key as string]

      // 对应不上
      if (isUndef(idxInOld)) {
        // New element
        api.insertBefore(
          parentElm,
          createElm(newStartVnode, insertedVnodeQueue),
          oldStartVnode.elm!
        )
      }
      // 对应上了
      else {
        // 拿到对应上 key 的节点
        elmToMove = oldCh[idxInOld]
        // sel 是否相等(sameVnode 条件)
        // sel 不相等,key相等
        if (elmToMove.sel !== newStartVnode.sel) {
          // New element
          api.insertBefore(
            parentElm,
            createElm(newStartVnode, insertedVnodeQueue),
            oldStartVnode.elm!
          )
        }
        // sel 相等,key 相等
        else {
          patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
          oldCh[idxInOld] = undefined as any
          api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!)
        }
      }
      // 更新之后,调整指针
      newStartVnode = newCh[++newStartIdx]
    }
  }
  // oldCh 已经全部处理完成,而 newCh 还有新的节点,需要对剩下的每个项都创建新的 dom
  if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) {
    if (oldStartIdx > oldEndIdx) {
      before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm
      addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
    } else {
      // newCh 已经全部处理完成,而 oldCh 还有旧的节点,需要将多余的节点移除
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
  }
}

infernojs源码分析

be continued

diff 过程

  • patch
  • patchVnode
  • addVnode
  • removeVnode
  • updateChildren
  • diff算法极致的性能优化做出的改变

vnode.key的作用是什么?

key 是用来判断两个 VNode 是否相同,参与到 diff 过程的一些判断中。key 情况分如下两种情况:

  • 当 key 相同时,调用 patchVnode 函数 更新节点,首先在旧列表中找到对应的 DOM 节点,然后执行移动操作;
  • 当 key 不相同时,会调用 createElm 函数 创建新节点,然后如果存在父节点,便将其插入到 dom 上,然后移除旧的 dom 节点来完成更新;

换句话说,key 用来在一个兄弟节点列表中进行唯一标记,这样在构建新节点的时候,会根据 key 去查找已经存在的节点,而不是就地复用,同时移除 key 不存在的节点。

Vue 与 React实现对比

Vue v2+ 中的 patch 机制就是基于 snabbdom 类库实现的(v3+ 切换到 infernojs)。 React v16+ 在推行 Fiber 之后,摒弃了递归 diff 的做法,但 diff 的核心思想是类似的。

Vue2和Vue3和React三者的diff算法有什么

Vue2:双端比较 Vue3:最长递增子序列 React:仅右移

HTTP 协议

  • HTTP 协议的主要特点
  • HTTP 报文的组成部分
  • HTTP 方法
  • POST 和 GET 的区别
  • HTTP 状态码
  • 什么是持久连接
  • 什么是管线化
  • HTTP 与 HTTPS

HTTP 协议的主要特点

  • 简单快速
  • 灵活
  • 无连接
  • 无状态

HTTP 报文的组成部分

面试-HTTP协议类-报文.png

HTTP 方法

* GET             获取资源
* POST            传输资源
* PUT             更新资源
* DELETE          删除资源
* HEAD            获得报文首部

POST 和 GET 的区别(1、3、5、6、8)

* GET在浏览器回退时是无害的,而POST会再次提交请求
* GET产生的URL地址可以被收藏,而POST不可以
* GET请求会被浏览器主动缓存,而POST不会,除非手动设置
* GET请求只能进行url编码,而POST支持多种编码方式
* GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留
* GET请求在URL中传送的参数是有长度限制的,而POST没有限制·对参数的数据类型,GET只接受ASCIl字符,而POST没有限制
* GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息
* GET参数通过URL传递,POST放在Request body中

HTTP 状态码

* 1xx:指示信息-表示请求已接收,继续处理
* 2xx:成功-表示请求已被成功接收
* 3xx:重定向-要完成请求必须进行更进一步的操作
* 4xx:客户端错误-请求有语法错误或请求无法实现
* 5xx:服务器错误-服务器未能实现合法的请求

* 200 OK:客户端请求成功
* 206 Partial Content:客户发送了一个带有Range头的GET请求,服务器完成了它
* 301 Moved Permanently:所请求的页面已经转移至新的url
* 302 Found:所请求的页面已经临时转移至新的url
* 304 Not Modified:客户端有缓冲的文档并发出了一个条件性的请求,服务器告诉客户,原来缓冲的文档还可以继续使用
* 400 Bad Request:客户端请求有语法错误,不能被服务器所理解
* 401 Unatuthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用
* 403 Forbidden:对被请求页面的访问被禁止
* 404 Not Found:请求资源不存在
* 500 Internal Server Error:服务器发生不可预期的错误原来缓冲的文档还可以继续使用
* 503 Server Unavailable:请求未完成,服务器临时过载或当机,一段时间后可能恢复正常

持久连接

HTTP 协议采用“请求-应答”模式,当使用普通模式,即非 Keep-Alive 模式时,每个请求/应答客户和服务器都要新建一个连接,完成之后立即断开连接(HTTP 协议为无连接的协议)

当使用 Keep-Alive 模式(又称持久连接、连接重用)时,Keep-Alive 功能使客户端到服务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive 功能避免了建立或者重新建立连接

管线化

在使用持久连接的情况下,某个连接上消息的传递类似于: 请求 1->响应 1->请求 2->响应 2->请求 3->响应 3 某个连接上的消息变成了类似这样: 请求 1>请求 2->请求 3->响应 1->响应 2->响应 3

* 管线化机制通过持久连接完成,仅HTTP/1.1支持此技术
* 只有GET和HEAD请求可以进行管线化,而POST则有所限制
* 初次创建连接时不应启动管线机制,因为对方(服务器)不一定支持HTTP/1.1版本的协议
* 管线化不会影响响应到来的顺序,如上面的例子所示,响应返回的顺序并未改变
* HTTP/1.1要求服务器端支持管线化,但并不要求服务器端也对响应进行管线化处理,只是要求对于管线化的请求不失败即可
* 由于上面提到的服务器端问题,开启管线化很可能并不会带来大幅度的性能提升,而且很多服务器端和代理程序对管线化的支持并不好,因此现代浏览器如Chrome和Firefox默认并未开启管线化支持

HTTP 与 HTTPS

* HTTP的URL为http://开头,HTTPS的URL为https://开头
* HTTP标准端口80,HTTPS标准端口是443
* 在OSI网络模型中,HTTP工作于应用层,HTTPS工作于传输层
* HTTP是不安全的,而HTTPS是安全的
* HTTP无法加密,而HTTPS对传输的数据进行加密
* HTTP无需证书,HTTPS需要CA机构wosign颁发的SSL证书

cookies和session的区别

  1. 存储位置不同:cookie的数据信息存放在客户端浏览器上,session的数据信息存放在服务器上。
  2. 存储容量不同:单个cookie保存的数据<=4KB,一个站点最多保存20个Cookie,而对于session来说并没有上限,但出于对服务器端的性能考虑,session内不要存放过多的东西,并且设置session删除机制。
  3. 存储方式不同:cookie中只能保管ASCII字符串,并需要通过编码方式存储为Unicode字符或者二进制数据。session中能够存储任何类型的数据,包括且不限于string,integer,list,map等。
  4. 隐私策略不同:cookie对客户端是可见的,别有用心的人可以分析存放在本地的cookie并进行cookie欺骗,所以它是不安全的,而session存储在服务器上,对客户端是透明的,不存在敏感信息泄漏的风险。
  5. 有效期上不同:开发可以通过设置cookie的属性,达到使cookie长期有效的效果。session依赖于名为JSESSIONID的cookie,而cookie JSESSIONID的过期时间默认为-1,只需关闭窗口该session就会失效,因而session不能达到长期有效的效果。
  6. 服务器压力不同:cookie保管在客户端,不占用服务器资源。对于并发用户十分多的网站,cookie是很好的选择。session是保管在服务器端的,每个用户都会产生一个session。假如并发访问的用户十分多,会产生十分多的session,耗费大量的内存。
  7. 跨域支持上不同:cookie支持跨域名访问。session不支持跨域名访问。

WebSocket和HTTP

相同点

  1. 都是一样基于TCP的,都是可靠性传输协议。
  2. 都是应用层协议。

不同点

  1. WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息。HTTP是单向的。
  2. WebSocket是需要握手进行建立连接的。

通信

  • 什么是同源策略及限制
  • 前后端如何通信
  • 如何创建 Ajax
  • 跨域通信的几种方式

什么是同源策略及限制

同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。

  • cookie,localstorage 和 IndexDB 无法读取
  • DOM 无法获取
  • Ajax 请求不能发送

前后端如何通信

  • Ajax
  • WebSocket
  • CORS

如何创建 Ajax(code)

  • XMLHttpRequest 对象的工作流程
  • 兼容性处理
  • 事件的触发条件
  • 事件的触发顺序

跨域通信的几种方式(code)

  1. 通过 JSONP 跨域
  2. CORS
  3. document.domain + iframe 跨域
  4. location.hash + iframe
  5. window.name + iframe 跨域
  6. postMessage
  7. nginx 代理跨域
  8. nodejs 中间件代理跨域
  9. WebSocket 协议跨域
  10. webpack proxy
jsonp

优缺点:

  1. JSONP 是服务器与客户端跨源通信的常用方法。最大特点就是简单适用,老式浏览器全部支持,服务器改造非常小。
  2. 只能实现 get 一种请求、不安全 容易遭到 xss 攻击
<script src="http://www.abc.com/?data=name&callback=jsonp" charset="utf-8"></script>
<script type="text/javascript">
  // jsonp({
  //   data: {
  //
  //   },
  // });
</script>
CORS

CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。 普通跨域请求:只服务端设置 Access-Control-Allow-Origin 即可,前端无须设置,若要带 cookie 请求:前后端都需要设置。由于同源策略的限制,所读取的 cookie 为跨域请求接口所在域的 cookie,而非当前页 优缺点:

  1. 目前,所有浏览器都支持该功能(IE8+:IE8/9 需要使用 XDomainRequest 对象来支持 CORS)),CORS 也已经成为主流的跨域解决方案。
  2. 整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
  3. CORS 与 JSONP 的使用目的相同,但是比 JSONP 更强大。JSONP 只支持 GET 请求,CORS 支持所有类型的 HTTP 请求。JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。
// url(必选),options(可选)
fetch('/some/url/', {
  method: 'get',
})
  .then(function (response) {})
  .catch(function (err) {
    // 出错了,等价于 then 的第二个参数,但这样更好用更直观
  })
document.domain + iframe 跨域

此方案仅限主域相同,子域不同的跨域应用场景(网页一级域名相同,只是二级域名不同)。 实现原理:两个页面都通过 js 强制设置 document.domain 为基础主域,就实现了同域。

<!-- 父窗口:(www.a.com/a.html) -->
<iframe id="iframe" src="http://child.a.com/b.html"></iframe>
<script>
  document.domain = 'a.com'
  let user = 'admin'
</script>

<!-- 子窗口:(child.a.com/b.html) -->
<script>
  document.domain = 'a.com'
  // 获取父窗口中变量
  alert('get js data from parent ---> ' + window.parent.user)
</script>
location.hash

利用 hash,场景是当前页面 A 通过 iframe 或 frame 嵌入了跨域的页面 B

// 在A中伪代码如下:
let Biframe = document.getElementsByTagName('iframe')
Biframe.src += '#data'

// 在B中的伪代码如下
window.onhashchange = function () {
  let data = window.location.hash
}
window.name + iframe 跨域

window.name 这个属性不是一个简单的全局属性 --- 只要在一个 window 下,无论 url 怎么变化,只要设置好了 window.name,那么后续就一直都不会改变,同理,在 iframe 中,即使 url 在变化,iframe 中的 window.name 也是一个固定的值

// 1.  a.html:(www.a.com/a.html)

const iframeNameProxy = (url, callback) => {
  let iframe = document.createElement('iframe')
  let state = 0

  // 加载跨域页面
  iframe.src = url

  // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
  iframe.onload = function () {
    if (state === 1) {
      // 第2次onload(同域c页)成功后,读取同域window.name中数据
      callback(iframe.contentWindow.name)
      destoryFrame()
    } else if (state === 0) {
      // 第1次onload(跨域页)成功后,切换到同域代理页面
      iframe.contentWindow.location = 'http://www.a.com/c.html'
      state = 1
    }
  }

  document.body.appendChild(iframe)

  // 获取数据以后销毁这个iframe,释放内存;同时也保证了安全(不被其他域frame js访问)
  function destoryFrame() {
    iframe.contentWindow.document.write('')
    iframe.contentWindow.close()
    document.body.removeChild(iframe)
  }
}

// 请求跨域b页面数据
iframeNameProxy('http://www.b.com/b.html', (data) => {
  alert(data)
})

// 2.  c.html:(www.a.com/c.html)
// src设置为当前域的c.html,中间代理页,与a.html同域,内容为空即可

// 3.  b.html:(www.b.com/b.html)
const person = {
  name: 'WuChenDi',
  age: 23,
}
window.name = JSON.stringify(person)
postMessage

窗口 A(http:A.com)向跨域的窗口 B(http:B.com)发送信息

// 窗口A(http:A.com)向跨域的窗口B(http:B.com)发送信息
Bwindow.postMessage('data', 'http://B.com')
// 在窗口B中监听
Awindow.addEventListener(
  'message',
  function (event) {
    console.log(event.origin)
    console.log(event.source)
    console.log(event.data)
  },
  false
)
nginx 代理跨域
server {
    listen       80;
    server_name  localhost;

    # 匹配以/apis/开头的请求
    location ^~ /apis/ {
        proxy_pass http://www.xxx.com/; #反向代理
        proxy_cookie_domain http://www.xxx.com/;
    }

    location / {
        root   html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   html;
    }
}

WebSocket

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。 使用 ws://(非加密)和wss://(加密)作为协议前缀。 该协议不实行同源政策,只要服务器支持,就可以通过它进行跨源通信。

webpack的proxy是如何解决跨域的?

其实这里就像是反向代理一样。我们在使用webpack开发项目的时候,webpack的dev-server模块会启动一个服务器,这个服务器不止帮我们做了自动更新,同时也可以做到反向代理。就是我们把请求发送给webpack-dev-server, 然后webpack-dev-server再去请求后端服务器,服务之间的请求是没有跨域问题的,只要后端返回了webpack-dev-server就能拿到,然后再返回给前端。

链接参考:

  • http://www.ruanyifeng.com/blog/2016/04/cors.html
  • https://juejin.im/post/5c9c38e2e51d452db7007f66#heading-7

安全

XSS

概念

XSS (Cross Site Scripting),即跨站脚本攻击,是一种常见于 Web 应用中的计算机安全漏洞。恶意攻击者往 Web 页面里嵌入恶意的客户端脚本,当用户浏览此网页时,脚本就会在用户的浏览器上执行,进而达到攻击者的目的。比如获取用户的 Cookie、导航到恶意网站、携带木马等。

常见攻击方法
  1. 绕过 XSS-Filter,利用<>标签注入 Html/JavaScript 代码;
  2. 利用 HTML 标签的属性值进行 xss 攻击。例如:<img src="javascript:alert('xss')" />;(当然并不是所有的 Web 浏览器都支持 JavaScript 伪协议,所以此类 XSS 攻击具有一定的局限性)
  3. 空格、回车和 Tab。如果 XSS Filter 仅仅将敏感的输入字符列入黑名单,比如 JavaScript,用户可以利用空格、回车和 Tab 键来绕过过滤,例如:<img src="javas cript:alert(/xss/)" />;
  4. 利用事件来执行跨站脚本。例如:<img src="#" onerror="alert(1)" />,当 src 错误的视乎就会执行 onerror 事件;
  5. 利用 CSS 跨站。例如:body {backgrund-image: url("javascript:alert('xss')" )};
  6. 利用字符编码,透过这种技巧,不仅能让 XSS 代码绕过服务端的过滤,还能更好地隐藏 Shellcode;(JS 支持 unicode、eacapes、十六进制、十进制等编码形式)
  7. 拆分跨站法,将 XSS 攻击的代码拆分开来,适用于应用程序没有过滤 XSS 关键字符(如<、>)却对输入字符长度有限制的情况下;
  8. DOM 型的 XSS 主要是由客户端的脚本通过 DOM 动态地输出数据到页面上,它不依赖于提交数据到服务器,而是从客户端获得 DOM 中的数据在本地执行。容易导致 DOM 型的 XSS 的输入源包括:Document.URL、Location(.pathname|.href|.search|.hash)、Document.referrer、Window.name、Document.cookie、localStorage/globalStorage;
防御措施

原则不相信客户输入的数据 注意: 攻击代码不一定在 <script></script> 中

  1. 使用 XSS Filter

输入过滤,对用户提交的数据进行有效性验证,仅接受指定长度范围内并符合我们期望格式的的内容提交,阻止或者忽略除此外的其他任何数据。比如:电话号码必须是数字和中划线组成,而且要设定长度上限。过滤一些些常见的敏感字符,例如:< > ' " & # \ javascript expression "onclick=" "onfocus"过滤或移除特殊的 Html 标签。 例如: <script>,<iframe>,&lt;for <, &gt; for >, &quot for;过滤 JavaScript 事件的标签,例如 onclick=,onfocus 等等。 输出编码,当需要将一个字符串输出到 Web 网页时,同时又不确定这个字符串中是否包括 XSS 特殊字符(如 < > &' " 等),为了确保输出内容的完整性和正确性,可以使用编码 HTMLEncode 进行处理。

  1. DOM 型的 XSS 攻击防御

把变量输出到页面时要做好相关的编码转义工作,如要输出到 <script>中,可以进行 JS 编码;要输出到 HTML 内容或属性,则进行 HTML 编码处理。根据不同的语境采用不同的编码处理方式。

  1. HttpOnly Cookie

将重要的 cookie 标记为 http only, 这样的话当浏览器向 Web 服务器发起请求的时就会带上 cookie 字段,但是在脚本中却不能访问这个 cookie,这样就避免了 XSS 攻击利用 JavaScript 的 document.cookie 获取 cookie

转义字符
function escapeHtml(str) {
  if (!str) return
  str = str.replace(/&/g, '&amp;')
  str = str.replace(/</g, '&lt;')
  str = str.replace(/>/g, '&gt;')
  str = str.replace(/"/g, '&quto;')
  str = str.replace(/'/g, '&#39;')
  str = str.replace(/`/g, '&#96;')
  str = str.replace(/\//g, '&#x2F;')
  return str
}

// use
escapeHtml('<script>alert(1)</script>')
// &lt;script&gt;alert(1)&lt;&#x2F;script&gt;

但是对于显示富文本来说,显然不能通过上面的办法来转义所有字符,因为这样会把需要的格式也过滤掉。对于这种情况,通常采用白名单过滤的办法,当然也可以通过黑名单过滤,但是考虑到需要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式。 推荐使用 js-xss

CSRF

概念

CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者 Session Riding,通常缩写为 CSRF 或者 XSRF,是一种对网站的恶意利用。

攻击原理

diagram1.jpg

CSRF 攻击的主要目的是让用户在不知情的情况下攻击自己已登录的一个系统,类似于钓鱼。如用户当前已经登录了邮箱,或 bbs,同时用户又在使用另外一个,已经被你控制的站点,我们姑且叫它钓鱼网站。这个网站上面可能因为某个图片吸引你,你去点击一下,此时可能就会触发一个 js 的点击事件,构造一个 bbs 发帖的请求,去往你的 bbs 发帖,由于当前你的浏览器状态已经是登陆状态,所以 session 登陆 cookie 信息都会跟正常的请求一样,纯天然的利用当前的登陆状态,让用户在不知情的情况下,帮你发帖或干其他事情。

防御措施
面试大白话
  1. 通过 referer、token 或者 验证码 来检测用户提交
  2. 尽量不要在页面的链接中暴露用户隐私信息
  3. 对于用户修改删除等操作最好都使用 post 操作
  4. 避免全站通用的 cookie,严格设置 cookie 的域
详细
验证码
原理:
CSRF攻击过程中,用户在不知情的情况下构造了网络请求,添加验证码后,强制用户必须与应用进行交互

*  优点:简洁而有效
*  缺点:网站不能给所有的操作都加上验证码

Referer Check
原理:
* 利用HTTP头中的Referer判断请求来源是否合法
* Referer首部包含了当前请求页面的来源页面的地址,一般情况下Referer的来源页就是发起请求的那个页面,如果是在iframe中发起的请求,那么对应的页面URL就是iframe的src

*  优点:简单易操作(只需要在最后给所有安全敏感的请求统一添加一个拦截器来检查Referer的值就行)
*  缺点:服务器并非什么时候都能取到Referer
        1.很多出于保护用户隐私的考虑,限制了Referer的发送。
        2.比如从HTTPS跳转到HTTP,出于安全的考虑,浏览器不会发送Referer

Anti CSRF Token
原理:把参数加密,或者使用一些随机数,从而让攻击者无法猜测到参数值,也就无法构造请求的 URL,也就无法发起 CSRF 攻击。

例子(增加token):
*  比如一个删除操作的URL是:`https://host/v1/user/crowd/delete?id=0530`
*  保持原参数不变,新增一个参数Token,Token值是随机的,不可预测
*  `https://host/v1/user/crowd/delete?id=0530&token=${getToken()}`

*  优点:比检查Referer方法更安全,并且不涉及用户隐私
*  缺点:
        加密
        1. 加密后的URL非常难读,对用户非常不友好
        2. 加密的参数每次都在改变,导致用户无法对页面进行搜索
        3. 普通参数也会被加密或哈希,将会给DBA工作带来很大的困扰,因为数据分析常常需要用到参数的明文

        token
        1. 对所有的请求都添加Token比较困难(解决方案:可以使用代理请求方法处理)

CSP

中间人攻击

点击劫持

概念

点击劫持是一种视觉上的欺骗手段。攻击者使用一个透明的、不可见的iframe,覆盖在一个网页上,然后诱使用户在网页上进行操作,此时用户将在不知情的情况下点击透明的iframe页面。通过调整iframe页面的位置,可以诱使用户恰好点击在iframe页面的一些功能性按钮上。

防御措施
1.X-Frame-Options HTTP响应头是用来给浏览器指示允许一个页面能否在`<frame>、<iframe>、<object>`中展现的标记

#### 有三个可选的值
1.  DENY:浏览器会拒绝当前页面加载任何frame页面(即使是相同域名的页面也不允许)
2.  SAMEORIGIN:允许加载frame页面,但是frame页面的地址只能为同源域名下的页面
3.  ALLOW-FROM:可以加载指定来源的frame页面(可以定义frame页面的地址)

2.禁止iframe的嵌套
if (window.top.location !== window.loaction) {
	window.top.location === window.self.location;
}

其他

异常与监控

前端错误的分类

系统异常

系统异常比较少,相关可能为浏览器奔溃

网络异常

  • XMLHttpRequest 请求异常
  • Fetch 请求异常
  • 静态资源加载异常

应用异常

  • Error:错误的基类,其他错误都继承自该类型。
  • EvalError : 与 eval() 有关的错误。
  • RangeError : 表示这个值不在允许值集或范围内。
  • ReferenceError : 表示发现一个无效的引用。
  • SyntaxError : 表示发生了解析错误。
  • TypeError :当其它类型错误都不符合时,TypeError 用于指示一个不成功的操作。
  • URIError :表示用于处理 URI 的函数(encodeURI 或 decodeURl)使用方式与其定义的不兼容。
  • 异常参考:Top 10 JavaScript errors from 1000+ projects

异常捕获

try/catch/finally

try {
  var a = 1
  var b = a + c
} catch (error) {
  // 捕获处理
  console.log(error) // ReferenceError: c is not defined
  logger.error('catch', error)
} finally {
  throw new Error('error')
}

思考: 如果 catch 块和 finally 块都抛出异常,catch 块的异常是否能抛出? 当该 finally 块引发异常时,它将有效地隐藏从该块引发的异常,并将 catch 最终引发该异常。因此,重要的是要么在捕获时记录异常,要么确保 finally 块本身不会引发异常。

window.onerror

请注意 window.error 无法捕获静态资源异常和 JS 代码错误。

/**
 * @param {String}  message    错误信息
 * @param {String}  source     错误文件路径
 * @param {Number}  lineno     错误行号
 * @param {Number}  colno      错误列号
 * @param {Object}  error      Error对象(对象)
 */
window.onerror = function (message, source, lineno, colno, error) {
  console.log(`捕获到异常:${(message, source, lineno, colno, error)}`)
  logger.error('oneror', JSON.stringify({ message, source, lineno, colno, error }))
}

静态资源加载 异常

<script>
  function errorHandler(error) {
    console.log(`捕获到静态资源加载异常: ${error}`)
    logger.error('onerror', JSON.stringify(error))
  }
</script>

<script src="http://cdn.xxx.com/js/test.js" onerror="errorHandler(this)"></script>

<link
  rel="stylesheet"
  href="http://cdn.xxx.com/styles/index.css"
  onerror="errorHandler(this)"
/>

Promise 异常

window.addEventListener('unhandledrejection', (event) => {
  console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`)
  logger.error('promise', JSON.stringify(event))

  event.preventDefault()
})

// 或
window.onunhandledrejection = (event) => {
  console.warn(`UNHANDLED PROMISE REJECTION: ${event.reason}`)
  logger.error('promise', JSON.stringify(event))

  event.preventDefault()
}

Vue

/**
 * @name Vue 异常上报
 * @param {{message,name,script,line,column,stack}} err error 对象
 * @param {String} vm 抛出异常的 Vue 实例对象
 * @param {String} info Vue 特定的错误信息,比如错误所在的生命周期钩子
 */
Vue.config.errorHandler = (err, vm, info) => {
  console.log('vue errorHandler', { err, vm, info })

  logger.error('vue error', JSON.stringify({ err, vm, info }))
}

React

import React from 'react'
import { Modal } from 'antd'
import './styles.scss'

export default class ErrorBoundary extends React.Component {
  state = { hasError: false, error: null, tipText: null }

  componentDidCatch(error, info) {
    console.error(error, info)
    logger.error('react error', JSON.stringify({ error, info }))

    if (
      error &&
      (error.toString().indexOf('ChunkLoadError') === 0 ||
        error.toString().indexOf('Error: Loading CSS ') === 0)
    ) {
      // 监测到Webpack异步模块加载失败
      Modal.confirm({
        title: '检测到网站可能有更新,需要刷新页面',
        okText: '刷新',
        cancelText: '关闭',
        maskClosable: false,
        onOk: () => {
          location.reload()
        },
      })
      this.setState({ hasError: true, tipText: '网站可能有更新,请刷新页面' })
    } else {
      this.setState({ hasError: true, error, tipText: null })
    }
  }

  render() {
    if (this.state.tipText) {
      return (
        <div className='page-error page-standard'>
          <h2 className='title'>{this.state.tipText}</h2>
        </div>
      )
    } else if (this.state.hasError) {
      return (
        <div className='page-error page-standard'>
          <h2 className='title'>抱歉,页面出错</h2>
          <h5 className='tip'>请尝试刷新页面,或联系技术人员,以下是错误信息:</h5>
          <div className='error-message'>
            {this.state.error ? this.state.error.toString() : '错误:未知错误'}
          </div>
        </div>
      )
    }

    return this.props.children
  }
}

// 使用案例:启动入口页面包裹
import React from 'react'
import { render } from 'react-dom'
import ErrorBoundary from 'components/ErrorBoundary'

const appRoot = document.getElementById('root')
appRoot.setAttribute('notranslate', true)

render(<ErrorBoundary>{/* code... */}</ErrorBoundary>, appRoot)

延伸: 跨域的 js 运行错误可以捕获吗,错误提示什么,应该怎么处理? 跨域之后 window.onerror 是无法捕获异常信息的,所以统一返回 Script error,解决方案

  1. 在 script 标签增加 crossorigin="anonymous" 属性
  2. 设置 Access-Control-Allow-Origin: *

监控平台搭建

这个应该每个公司都有自己的监控平台,这里不列举过多。

如果数据量过大,架构需要对行为记录存储的设计与成本进行考虑。如容器存储与存放时间等

数据监控

  • PV:即页面浏览量或点击量
  • UV:指访问某个站点或点击某条新闻的不同IP地址的人数
  • 页面停留时长
  • 用户与数据来源
  • 触发行为

性能监控

  • 不同环境下机型与系统下的首屏加载时间
  • DNS、TCP、request、页面渲染、load、加载、白屏等耗时时间

异常监控

JavaScript与样式异常

埋点

数据上报可以在延伸扩展:即时,批量,主动等上报方式,可根据业务优先级来决定

代码埋点与数据上报

diagram1.jpg

算法

LeetCode

LeetCode

排序

排序

堆栈,队列,链表

  • 堆栈,队列,链表:https://juejin.im/entry/58759e79128fe1006b48cdfd

递归

  • 递归:https://segmentfault.com/a/1190000009857470

波兰式和逆波兰式

  • 理论:http://www.cnblogs.com/chenying99/p/3675876.html
  • 源码:https://github.com/Tairraos/rpn.js/blob/master/rpn.js

面试题目总结

2020

手写继承
{
  {
    // js 实现继承
    function Animal() {
      this.eat = function () {
        console.log('animal can eat ...')
      }
    }
    function Dog() {
      this.break = function () {
        console.log('dog can break')
      }
    }
    Dog.prototype = new Animal()
    let es5 = new Dog()
    es5.eat() // animal can eat ...
    es5.break() // dog can break
  }
  {
    // class 实现 继承
    class Animal {
      constructor(name) {
        this.name = name
      }
      eat() {
        console.log('animal can wat')
      }
    }
    class Dog extends Animal {
      constructor(name) {
        super(name)
      }
      break() {
        console.log('dog can break')
      }
    }
    let es6 = new Dog('Zekee')
    es6.eat() // animal can wat
    es6.break() // dog can break
  }
}

instanceof 实现原理
{
  // instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。
  {
    function new_instance_of(leftVaule, rightVaule) {
      let rightProto = rightVaule.prototype
      leftVaule = leftVaule.__proto__
      while (true) {
        if (leftVaule === null) return false
        if (leftVaule === rightProto) return true
        leftVaule = leftVaule.__proto__
      }
    }
  }
  {
    const new_instance_of = function (leftVaule, rightVaule) {
      let proto = Object.getPrototypeOf(leftVaule)
      while (true) {
        if (proto == null) return false
        if (proto === rightVaule.prototype) return true
        proto = Object.getPrototypeOf(proto)
      }
    }
  }
  // 总结:使用 typeof 来判断基本数据类型是 ok 的,不过需要注意当用 typeof 来判断 null 类型时的问题,如果想要判断一个对象的具体类型可以考虑用 instanceof,但是 instanceof 也可能判断不准确,比如一个数组,他可以被 instanceof 判断为 Object。所以我们要想比较准确的判断对象实例的类型时,可以采取 Object.prototype.toString.call() 方法
}

promise 限制并发数
{
  class LimitPromise {
    constructor(max) {
      // 异步任务“并发”上限
      this._max = max
      // 当前正在执行的任务数量
      this._count = 0
      // 等待执行的任务队列
      this._taskQueue = []
    }

    /**
     * 调用器,将异步任务函数和它的参数传入
     * @param caller 异步任务函数,它必须是async函数或者返回Promise的函数
     * @param args 异步任务函数的参数列表
     * @returns {Promise<unknown>} 返回一个新的Promise
     */
    call(caller, ...args) {
      return new Promise((resolve, reject) => {
        const task = this._createTask(caller, args, resolve, reject)
        if (this._count >= this._max) {
          // console.log('count >= max, push a task to queue')
          this._taskQueue.push(task)
        } else {
          task()
        }
      })
    }

    /**
     * 创建一个任务
     * @param caller 实际执行的函数
     * @param args 执行函数的参数
     * @param resolve
     * @param reject
     * @returns {Function} 返回一个任务函数
     * @private
     */
    _createTask(caller, args, resolve, reject) {
      return () => {
        // 实际上是在这里调用了异步任务,并将异步任务的返回(resolve和reject)抛给了上层
        caller(...args)
          .then(resolve)
          .catch(reject)
          .finally(() => {
            // 任务队列的消费区,利用Promise的finally方法,在异步任务结束后,取出下一个任务执行
            this._count--
            if (this._taskQueue.length) {
              // console.log('a task run over, pop a task to run')
              let task = this._taskQueue.shift()
              task()
            } else {
              // console.log('task count = ', count)
            }
          })
        this._count++
        // console.log('task run , task count = ', count)
      }
    }
  }
  // 调用器:就是把真正的执行函数和参数传入,创建返回一个新的Promise,而这个新Promise的什么时候返回,取决于这个异步任务何时被调度。Promise内部主要就是创建一个任务,判断任务是执行还是入队。
  // 创建任务:实际上就是返回了一个函数,将真正的执行函数放在里面执行。这里利用了Promise的finally方法,在finally中判断是否执行下一个任务,实现任务队列连续消费的地方就是这里。
}

箭头函数跟普通函数的区别

函数扩展

flex 1 全写
/* 等价于 */
#app {
  flex-grow: 1;
  flex-shrink: 1;
  flex-basis: 0%;
}
/* 分别代表了所定义flex盒子的拉伸因子、收缩规则、基础宽度。 */
vue 双向绑定原理

vue学习记录

https 实现原理(越详细越好)
GraphQL 如何优化请求速度
GraphQL为什么会提升性能

GraphQL能够根据页面展示需求请求所需要的数据,不会有冗余的数据,传输效率上会更高

浏览器渲染页面过程
  • 从耗时的角度,浏览器请求、加载、渲染一个页面,时间花在下面五件事情上:
  • DNS 查询
  • TCP 连接
  • HTTP 请求即响应
  • 服务器响应
  • 客户端渲染

渲染机制

如何性能优化
  • DNS解析时间: domainLookupEnd - domainLookupStart
  • TCP建立连接时间: connectEnd - connectStart
  • 白屏时间: responseStart - navigationStart
  • dom渲染完成时间: domContentLoadedEventEnd - navigationStart
  • 页面onload时间: loadEventEnd - navigationStart
CDN 优化有哪些
缓存有哪些,区别是什么
  • http 缓存 (强缓存和协商缓存)
  • 浏览器缓存 ( Cookie, LocalStorage, SessionStorage )

页面性能类

手写 bind、reduce
{
  {
    // bind
    // 箭头函数的 this 永远指向它所在的作用域
    // 函数作为构造函数用 new 关键字调用时,不应该改变其 this 指向,因为 new绑定 的优先级高于 显示绑定 和 硬绑定
    {
      var mybind = function (thisArg) {
        if (typeof this !== "function") {
          throw TypeError("绑定必须在函数上调用");
        }
        // 拿到参数,为了传给调用者
        const args = Array.prototype.slice.call(arguments, 1),
        // 保存 this
        var that = this;
        // 构建一个干净的函数,用于保存原函数的原型
        var nop = function () { };
        // 绑定的函数
        var bound = function () {
          // this instanceof nop, 判断是否使用 new 来调用 bound
          // 如果是 new 来调用的话,this的指向就是其实例,
          // 如果不是 new 调用的话,就改变 this 指向到指定的对象 o
          return that.apply(
            this instanceof nop ? this : thisArg,
            args.concat(Array.prototype.slice.call(arguments))
          );
        };

        // 箭头函数处理:由于箭头函数没有 prototype,箭头函数this永远指向它所在的作用域
        if (this.prototype) {
          nop.prototype = this.prototype;
        }
        // 修改绑定函数的原型指向
        bound.prototype = new nop();

        return bound;
      };
    }
    // 测试
    const bar = function () {
      console.log(this.name, arguments);
    };
    bar.prototype.name = "bar";
    const foo = { name: "foo" };
    const bound = bar.mybind(foo, 22, 33, 44);
    new bound(); // bar, [22, 33, 44]
    bound(); // foo, [22, 33, 44]
  }
}

防抖节流
/**
 * @description 函数防抖
 * @param {Function} method 延时调用函数
 * @param {Number} wait 延迟时长
 * @param {Boolean} immediate 立即执行选项
 */
export const Ddebounce = <T extends (...args: any[]) => any>(
  method: T,
  wait: number,
  immediate: boolean
) => {
  if (typeof method !== 'function') {
    throw new TypeError('Expected a function')
  }
  let timeout: ReturnType<typeof setTimeout> | null = null
  // Ddebounce函数为返回值
  // 使用Async/Await处理异步,如果函数异步执行,等待setTimeout执行完,拿到原函数返回值后将其返回
  // args为返回函数调用时传入的参数,传给method
  let Ddebounce = async (...args: Parameters<T>): Promise<ReturnType<T>> => {
    return new Promise((resolve) => {
      // 用于记录原函数执行结果
      let result: ReturnType<T>
      // 将method执行时this的指向设为 debounce 返回的函数被调用时的this指向
      let context = this
      // 如果存在定时器则将其清除
      if (timeout) {
        clearTimeout(timeout)
      }
      // 立即执行需要两个条件,一是immediate为true,二是timeout未被赋值或被置为null
      if (immediate) {
        // 如果定时器不存在,则立即执行,并设置一个定时器,wait毫秒后将定时器置为null
        // 这样确保立即执行后wait毫秒内不会被再次触发
        let callNow = !timeout
        timeout = setTimeout(() => {
          timeout = null
        }, wait)
        // 如果满足上述两个条件,则立即执行并记录其执行结果
        if (callNow) {
          result = method.apply(context, args)
          resolve(result)
        }
      } else {
        // 如果immediate为false,则等待函数执行并记录其执行结果
        // 并将Promise状态置为fullfilled,以使函数继续执行
        timeout = setTimeout(() => {
          // args是一个数组,所以使用fn.apply
          // 也可写作method.call(context, ...args)
          result = method.apply(context, args)
          resolve(result)
        }, wait)
      }
    })
  }

  // 在返回的 Ddebounce 函数上添加取消方法
  // Ddebounce.cancel = function () {
  //   clearTimeout(timeout)
  //   timeout = null
  // }
  return Ddebounce
}
/**
 * 节流
 * @param func 回调函数
 * @param delay 延迟时间
 */
export const Dthrottle = <T extends (...args: any[]) => void>(func: T, delay: number) => {
  let timer: ReturnType<typeof setTimeout> | null = null

  return function (this: ThisParameterType<T>, ...args: Parameters<T>) {
    const context = this

    if (!timer) {
      timer = setTimeout(function () {
        func.apply(context, args)
        timer = null
      }, delay)
    }
  }
}
遍历树,求树的最大层数。求某层最多的节点数
node 跟浏览器的 event loop 区别

JS运行机制

node 进程之间如何通讯
node 开启进程的方法有哪些,区别是什么

Node.js 通过提供 cluster、child_process API 创建子进程的方式来赋予Node.js “多线程”能力。但是这种创建进程的方式会牺牲共享内存,并且数据通信必须通过json进行传输。(有一定的局限性和性能问题) 基于此 Node.js V10.5.0 提供了 worker_threads,它比 child_process 或 cluster更轻量级。 与child_process 或 cluster 不同,worker_threads 可以共享内存,通过传输 ArrayBuffer 实例或共享 SharedArrayBuffer 实例来实现。

node 如何部署的

pm2,docker容器化等

node check 阶段做了什么,触发了什么事件
node 如何处理错误的

大多数异步API发生错误,采用callback方式来处理异常,其中callback的第一个参数就是err,如果第一个参数为null,而非err的话,则正确执行后面指令,反之为error的话,就会处理相应错误。

前端模块化的理解
  • 功能分治,利维护
  • 复用,利开发
隐式转换
{
  {
    // ToString (注:ToString不是对象的toString方法,而是指其他类型的值转换为字符串类型的操作)
    // null:转为"null"
    // undefined:转为"undefined"
    // 布尔类型:true和false分别被转为"true"和"false"
    // 数字类型:转为数字的字符串形式,如10转为"10", 1e21转为"1e+21"
    // 数组:转为字符串是将所有元素按照","连接起来,相当于调用数组的Array.prototype.join()方法,如[1, 2, 3]转为"1,2,3",空数组[]转为空字符串,数组中的null或undefined,会被当做空字符串处理
    // 普通对象:转为字符串相当于直接使用Object.prototype.toString(),返回"[object Object]"
    String(null) // 'null'
    String(undefined) // 'undefined'
    String(true) // 'true'
    String(10) // '10'
    String(1e21) // '1e+21'
    String([1, 2, 3]) // '1,2,3'
    String([]) // ''
    String([null]) // ''
    String([1, undefined, 3]) // '1,,3'
    String({}) // '[object Objecr]'
  }
  {
    // ToNumber (指其他类型转换为数字类型的操作)
    // null: 转为0
    // undefined:转为NaN
    // 字符串:如果是纯数字形式,则转为对应的数字,空字符转为0, 否则一律按转换失败处理,转为NaN
    // 布尔型:true和false被转为1和0
    // 数组:数组首先会被转为原始类型,也就是ToPrimitive,然后在根据转换后的原始类型按照上面的规则处理,关于ToPrimitive,会在下文中讲到
    // 对象:同数组的处理
    Number(null) // 0
    Number(undefined) // NaN
    Number('10') // 10
    Number('10a') // NaN
    Number('') // 0
    Number(true) // 1
    Number(false) // 0
    Number([]) // 0
    Number(['1']) // 1
    Number({}) // NaN
  }
  {
    // ToBoolean (指其他类型转换为布尔类型的操作)
    // js中的假值只有false、null、undefined、空字符、0和NaN,其它值转为布尔型都为true
    Boolean(null) // false
    Boolean(undefined) // false
    Boolean('') // flase
    Boolean(NaN) // flase
    Boolean(0) // flase
    Boolean([]) // true
    Boolean({}) // true
    Boolean(Infinity) // true
  }
  {
    // ToPrimitive (指对象类型类型(如:对象、数组)转换为原始类型的操作)
    // 注:对于不同类型的对象来说,ToPrimitive的规则有所不同,比如Date对象会先调用toString
    // 当对象类型需要被转为原始类型时,它会先查找对象的valueOf方法,如果valueOf方法返回原始类型的值,则ToPrimitive的结果就是这个值
    // 如果valueOf不存在或者valueOf方法返回的不是原始类型的值,就会尝试调用对象的toString方法,也就是会遵循对象的ToString规则,然后使用toString的返回值作为ToPrimitive的结果
    Number([]) // 0
    Number(['10']) //10

    const obj1 = {
      valueOf() {
        return 100
      },
      toString() {
        return 101
      },
    }
    Number(obj1) // 100

    const obj2 = {
      toString() {
        return 102
      },
    }
    Number(obj2) // 102

    const obj3 = {
      toString() {
        return {}
      },
    }
    Number(obj3) // TypeError

    // 对象类型在ToNumber时会先ToPrimitive,再根据转换后的原始类型ToNumber
    // Number([]), 空数组会先调用valueOf,但返回的是数组本身,不是原始类型,所以会继续调用toString,得到空字符串,相当于Number(''),所以转换后的结果为"0"
    // 同理,Number(['10'])相当于Number('10'),得到结果10
    // obj1的valueOf方法返回原始类型100,所以ToPrimitive的结果为100
    // obj2没有valueOf,但存在toString,并且返回一个原始类型,所以Number(obj2)结果为102
    // obj3的toString方法返回的不是一个原始类型,无法ToPrimitive,所以会抛出错误
  }
  {
    // ==
    // 只要布尔类型参与比较,该布尔类型的值首先会被转换为数字类型
    // 根据布尔类型的ToNumber规则,true转为1,false转为0
    {
      false == 0 // true
      true == 1 // true
      true == 2 // false
    }
    // 数字类型和字符串类型的相等比较
    // 当数字类型和字符串类型做相等比较时,字符串类型会被转换为数字类型
    // 根据字符串的ToNumber规则,如果是纯数字形式的字符串,则转为对应的数字,空字符转为0, 否则一律按转换失败处理,转为NaN(NaN和任何值都不相等,包括本身)
    {
      0 == '' // true
      1 == '1' // true
      1e21 == '1e21' // true
      Infinity == 'Infinity' // true
      true == '1' // true
      false == '0' // true
      false == '' // true
    }
    // 对象类型和原始类型的相等比较
    // 当对象类型和原始类型做相等比较时,对象类型会依照ToPrimitive规则转换为原始类型
    {
      '[object Object]' == {} // true
      '1,2,3' == [1, 2, 3] // true
    }
    // null、undefined和其他类型的比较
    // null和undefined宽松相等的结果为true,其次null和undefined都是假值
    // false转为0,然后呢? 没有然后了,ECMAScript规范中规定null和undefined之间互相宽松相等(==),并且也与其自身相等,但和其他所有的值都不宽松相等(==)。
    {
      null == false // false
      undefined == false // false
      null == undefined // true
    }
    // {
    //   [] == ![] // true
    //   [] == 0 // true
    //   [2] == 2 // true
    //   ['0'] == false // true
    //   '0' == false // true
    //   [] == false // true
    //   [null] == 0 // true
    //   null == 0 // false
    //   [null] == false // true
    //   null == false // false
    //   [undefined] == false // true
    //   undefined == false // false
    // }
    {
      // 定义一个变量a,使得下面的表达式结果为true; a == 1 && a == 2 && a == 3
      {
        const a = {
          // 定义一个属性来做累加
          inx: 1,
          valueOf() {
            return this.inx++
          },
        }
        console.log(a == 1 && a == 2 && a == 3) // true
      }
      {
        const a = {
          // 定义一个属性来做累加
          inx: 1,
          toString() {
            return this.inx++
          },
        }
        console.log(a == 1 && a == 2 && a == 3) // true
      }
    }
  }
}
数字在计算机怎么储存的
webpack 优化
webpack 的 require 是如何查找依赖的
webpack 如何实现动态加载
webpack 插件原理,如何写一个插件
给你一个项目,从头开始你怎么考虑

前言

工作流做了哪些事情
如何提升效率与性能

页面性能类

未来的规划是什么
跨域有哪些

通信类

安全类

安全类

变量提升 let const var 区别
  • let不允许在相同作用域内重复声明同一个变量,即同一个作用域内不允许出现名称相同的变量。
  • const用于声明常量,一旦声明,必须立即赋值,且以后不可更改。
  • const命令两个注意点:
    • 1.const 声明之后必须马上赋值,否则会报错
    • 2.const 简单类型一旦声明就不能再更改,复杂类型(数组、对象等)指针指向的地址不能更改,内部数据可以更改。
    • 解释:对象是引用类型,这里的返回值是对象的指针,指向这个对象存储的这个指针,这个指针是不变的,但是,对象本身是可以变的
链表与数组的区别
链表如何遍历

链表

script 标签中 async 跟 defer 的区别
  • 注:网上说 defer不会阻碍页面渲染,但自己测试iOS与safari会白屏,其效果跟script标签一样。

页面性能类

时针与分针计算 夹角问题(Tencent)
function angle(h, m) {
  if (h < 24 && m < 60) {
    // 时针一小时30度,一分钟0.5度
    const a = (h % 12) * 30 + m * 0.5
    // 分针一分钟6度
    const b = m * 6
    return Math.abs(a - b)
  }
}

console.log(angle(9, 0)) // 270

2021

Copyright (c) 2023-PRESENT | wudi