小记

274 阅读28分钟

网络相关

http 1.0 1.1 2.0 3.0 分别有什么区别

  1. 1.0存在问题
  • 浏览器限制同一个域名下请求的最大数量 (chrome是6个)
  • 存在队头阻塞问题 导致达到最大请求数量的时候 剩余资源要等其他资源请求完成之后才能发起请求
  1. 1.1升级
  • 更多缓存相关控制策略 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 新增Cache-Control字段替代expires
  • 默认长链接模式(长链接与短连接的区别是 建立tcp通道是否会断开)
  1. 2.0升级
  • 二进制分帧 以二进制流形式传输 一个完整的请求响应可以由一个或者多个帧组成
  • 多路复用 允许多路复用单一连接(一个tcp连接)发起多重请求 并采用分帧形式 分帧还是无序的
  • 同步压缩 部分字段做双端缓存(浏览器 服务器维护索引表)
  • 服务端push 当客户端请求了某些请求后 服务端可以主动推送其他资源
  1. 3.0升级
  • QUIC协议 是基于udp的协议 又吸取了tcp的优点:可靠
  • 多路复用 使用基于udp的 quic协议为底层 原生就实现多路复用

https

  • 需要证书访问
  • https传输的不再是明文内容 是经过加密的二进制流
  • https 是 http 协议的更加安全的版本,通过使用SSL/TLS进行加密传输的数据(ssl是安全层 tls是传输安全层)
  • http 默认的端口是 80和 https 默认端口是 443

深入:https加解密过程 juejin.cn/post/684490…

强缓存与协商缓存(针对于get请求 对静态资源的处理方式 post请求无)

  1. 强缓存: 缓存期间不需要重新请求 status code返回的是 200
  • 通过http header里面的Expires字段(1.0)与Cache-Control字段(1.1)控制 Cache-Control优先级更高
  • 一般Cache-Control 设置 max-age=30 值表示 缓存30秒后就过期 需要重新请求
  1. 协商缓存: 如果缓存过期 就需要发起请求验证资源是否有更新 如果资源没变服务端会返回304 并且更新浏览器缓存有效期
  • 通过http header里面的 Last-Modified 和 Etag 字段来控制 Last-Modified表示最后修改日期 但他有弊端 打开本地缓存文件这个字段会被修改 且此字段只能以秒计时 不可感知的时间内完成文件修改服务端会认为资源命中 所以 一般用Etag 类似于文件指纹

最佳实践: html协商缓存 静态文件(js css)强缓存 依赖于前端工程化中的文件指纹 hash值

网络安全

1. XSS攻击 跨站脚本攻击 cross-site scripting

攻击者在目标网站上注入恶意代码(如读取cooike seeeion 或者其他的敏感信息)以下是常见防御手段:

  • X-XSS-Protection http响应头字段
  • httpOnly 设置cookie不能被js获取
  • csp策略 内容安全策略 本质上是建立白名单规定哪些外部资源可以加载和执⾏
  • 对数据进行严格输出编码
  • 用户交互的输入校验
  • 验证码行为

2. CSRF攻击 即跨站请求伪造 一般是建立在xss攻击之上 首先要获取用户cookie

  • cookie设置 samesite属性 表示cookie不能随着跨域请求发送
  • 请求校验referer
  • token机制 服务器下发随机token 每次发起请求时将token带上 服务器验证token是否有效

3. 点击劫持 一般是通过iframe嵌套的方式 将iframe设置为透明 然后诱导用户点击

  • X-FRAME-OPTIONS 响应头 设置iframe嵌套规则 可以设置白名单

CSS

px 与物理像素的关系

此问题涉及到逻辑像素与物理像素之间的关系 在早期的时候 1px的像素渲染成1个实际像素点 但现在随着屏幕DPR(Device Pixel Ratio 设备像素比 即物理像素与逻辑像素的比值)不是1 如DPR为2 则1px为2个物理像素点
DPR计算为 手机屏幕分辨率如 750*xxx 一般的逻辑像素宽为375(js中使用screen.width获取的宽度) 则DPR为2

css响应式单位 em rem vw vh

em: 在 font-size 中使用是相对于父元素的字体大小,在其他属性中使用是相对于自身的字体大小,如 width
rem: 根元素字体大小
vh vw: 相对于视口的高度 宽度的百分比

BFC

块级格式化上下文: 官方说法 决定了元素如何对其内容进行定位,以及与其它元素的关系和相互作用
通俗来讲: 他是一块独立的渲染区域 隔绝其内部与外部的影响

  1. 怎么创建BFC
  • position 不为static relative
  • float元素
  • display flex table之类的
  • overflow 不为 visable
  1. 作用
  • 常见的清除浮动
  • margin重叠问题

层叠上下文

可以简单理解为z轴, 在页面布局时决定元素的覆盖关系(点击事件怎么触发碰到最上层元素)

  1. 层叠水平 stacking level
    层叠水平大的显示在z轴前面 决定了同一个层叠上下文中元素在z轴上的显示顺序

  2. 层叠顺序 (规则)
    (在一般的元素中层叠顺序一般遵循 background/border --- 负z-index --- block --- float --- inline/inline-block --- auto/0 z-index --- 正z-index )

  • 层叠黄金准则 当元素发生层叠时 起覆盖关系遵循 1. 谁大谁上(比较z-index) 2.后来居上(同一层叠水平 处于dom流后面的元素覆盖前面的)

包含块 containing block

元素的尺寸和位置会受他包含块所影响 对于一些属性如width height margin padding 绝对定位元素(包括absolute fixed)的偏移值 当我们对其赋予百分比的值时,这些值的计算值就是通过包含块计算得来的

如何判定包含块:

  • 如果元素的 positiion 是 relative 或 static ,那么包含块由离它最近的块容器(block container)的内容区域(content area)的边缘建立。
  • 如果 position 属性是 fixed,那么包含块由视口建立。
  • 如果元素使用了 absolute 定位,则包含块由它的最近的 position 的值不是 static (也就是值为fixed、absolute、relative 或 sticky)的祖先元素的内边距区的边缘组成。

特殊情况 如果 position 属性是 absolute 或 fixed,包含块也可能是由满足以下条件的最近父级元素的内边距区的边缘组成的:

  • transform 或 perspective 的值不是 none
  • will-change 的值是 transform 或 perspective
    (注意上两条即可)
  • filter 的值不是 none 或 will-change 的值是 filter(只在 Firefox 下生效).
  • contain 的值是 paint (例如: contain: paint;)

position: sticky 粘性定位

元素正常计算位置信息 偏移值不会影响任何其他元素的位置
其偏移值是相对于 最近的滚动祖先元素(overflow属性不为visible) 和 包含块 位置计算

flex: 1;

flex值为1 其实为3个属性的缩写 flex-grow: 1 flex-shrink: 1 flex-basis: 0% flex: 1 0; flex-grow是如果有剩余空间,是否扩大,0为不扩大
flex-shrink是如果剩余空间不够,是否缩小,1为缩小
flex-basis为项目本身的大小,默认值是auto

如果flex为有单位的值: 则为定义flex-basis 的值了(其他默认)

JS

浏览器的事件循环

浏览器中的多种工作任务 如 html css代码解析 js代码执行 gc 等等都是运行在渲染进程中的渲染主线程上的
这就是js是单线程的原因,基本上js运行在渲染主线程上
由于js是单线程,他避免不了有多种异步操作,事件循环机制就是用来调度各种异步操作的机制
在渲染主线程开启后,它会主动开启一个无限循环,每一次循环都会检查消息队列里面是否有任务,有任务就拿出最前面那个任务执行执行后就进入下次循环; 任务是没有优先级的,但是往哪个消息队列里面取任务是有优先级 早先的说法消息队列分为微队列与宏队列,但根据w3c最新标准,这种微宏任务机制无法满足当前复杂多变的浏览器环境,取而代之的是新的队列模式,浏览器里面最少要有微任务队列,微任务队列优先级最高,其他队列视情况而定
如chrome中有 微队列(Promise.then MutationObserver回调) 交互队列 计时队列 这就是在同等条件下,Promise时机总是比settimeout时机快的原因

总而言之: js单线程是异步产生的原因,事件循环是异步实现的方式

     题目: 以下两段代码能否一直执行下去  表现是什么
    function demo1() {
      setTimeout(demo1, 0)
    }
    demo1()
    
    
   function demo2() {
      return Promise.resolve().then(demo2)
   }
   demo2()
    
    能一直执行下去(类似死循环) 但demo1不会去造成阻塞 因为得益于 计时队列对计时会有延时 当嵌套超过5层时每一个嵌套执行时间会延迟4ms左右 但是demo2会一直往微队列里面添加后拿出来执行 这个过程中没有间歇会造成卡死

浏览器渲染原理

浏览器渲染过程本质是将html代码转换为每个点的像素信息的过程

浏览器渲染过程一般分为八大阶段分别是

  1. html解析 parse
    将html字符串转换为 dom tree 和 cssom tree
    (当解析过程遇到css 为了提高渲染效率 浏览器会启动预解析线程来下载和解析css 再把结果给到主线程)
    (当解析过程遇到js 主线程将暂停一切行为 等待js 下载执行完成后再继续下面的解析 其中预解析线程可以分担一点下载任务 async defer 两个属性可以影响 async表示下载完成后执行 defer表示html文档解析完成后执行 )

  2. 样式计算 style 此过程会将domtree和 cssom tree 计算生成带有 computed style的dom tree
    这个过程多数预设值会变成绝对值 相对单位会变成绝对单位

  3. 布局 layout 计算出每个节点位置 和尺寸位置 生成layout tree
    layout tree可能与dom tree不一致 如伪元素的影响 display:none 影响

  4. 分层 layer 为保证渲染高效 分层渲染
    滚动条 transform会影响分层决策 will-change属性可以更大程度影响分层结果

  5. 绘制 paint
    生成绘制指令 (类比canvas api的绘制指令)

  6. 分块 tiling
    从这个步骤开始 以下步骤都是交给浏览器的合成线程
    每一层分为多个小区域

  7. 光栅化 raster
    生成位图(类比二维数组的像素信息)

  8. 画 draw
    拿到位图生成quad(指引)信息 给到gpu进程它调用gpu负责完成屏幕成像
    此过程quad包含位图绘制到屏幕的信息 以及会考虑到的 旋转 缩放 变形(transform filter)
    transform不仅是发生在合成线程中 并且可以启用gpu,这是他效率高效的本质

重绘 repaint 与回流 reflow

reflow本质是重新计算layout tree 类比上面的8大过程(尺寸位置发生变化样式)
repaint 重新绘制 无须重新生成layout tree (如 background color visibility transform )

原型与原型链

每个函数都有prototype属性(它是一个对象) 称之为函数原型
prototype默认有一个constructor属性 它指向构造函数 通过函数创建出来的对象会有__proto__属性,它指向 该函数的prototype 称之为隐式原型

特点: 当访问对象属性的时候,先在对象本身属性上找,如果找不到会去其隐式原型__proto__上找(也就是它构造函数的prototype上找) 由于prototype又是一个对象,如果找不到还会继续像该函数的构造函数上面去找这样一层一层找下去 我们把这个链条称之为原型链

null 与 undefined 区别

总的来说 null 与undefined都代表空,主要区别在于 undefined 表示尚未初始化的变量的值,而null表示该变量缺少对象指向
undefined变量从根本上就未定义,null这个值虽然定义了,但是它并没有指向任何内存中的对象

注意:虽然null为原始类型的一种 但是typeof null 结果为 'object' (原型链中 Object最底层指向的就是null) null的类型转换后 转换为Number为0, Number(null) 返回值为0 undefined的类型转换 转换成Number为NaN

typeof 和 instanceof

  1. typeof
    typeof 对于原始类型(null除外) 都可以检测出类型
    typeof 对于对象 function会显示出 'function' 其他都是 'object'

  2. instanceof
    instanceof原理是 通过原型链的方式判断是否为构造函数实例 所以这也是有缺陷的(原型链的链式)

  3. 引申 如何判断一个变量是否是数组

  • Array.isArray() 判断
  • 使用 instanceof Array 判断
  • Object.prototype.toString.call()判断 返回值 '[object Array]'

数组与类数组

  1. 类数组
  • 具有length属性
  • 其他属性为非负整数(即索引)
  1. Array.from
    Array.from 可以将类数组转换为数组, 也可以将可遍历对象转换为数组(可遍历对象有 Symbol.iterator 属性 为一个函数)
// 如果Array.from转换的是一个普通对象没有length属性 他会返回空数组
// 以下为类数组?
  let arrLike = {
    0: 1
  }
  Array.from(arrLike) => []
  
  let arrLike1 = {
     0: 1length: 1
  }
  Array.from(arrLike1) => [1]
  1. [...arrayLike] 扩展运算符转换可以将可遍历对象转换为数组 必须是可迭代对象 否则报错

== 与 === 与 Object.is

  1. === 首先比较 两侧变量数据类型是否相等,再比较值是否相等 只有数据类型和值同时相等后才返回true

  2. Object.is Object.is 与 === 基本一致 但是存在差异

  • NaN 与 NaN相等
  • -0 与 +0 不相等
  1. == 以下是比较过程
    1. 首先判断两侧数据类型是否相等 如果相等进行值判断
    2. 如果类型不同进行类型转换
    • 比较的双方是 null undefined 两者 返回true
    • 比较双方是 string 和 number 将字符串转换成number后比较值
    • 比较一方是 boolean 将boolean转换为number再进行判断
    • 比较一方为object 另一方为基本类型 将object转换为原始类型再进行判断(先调用valueof方法 没有再toString)

注意
undefined转换为number为 NaN
null转换为number为 0
[] 转换为number为 0

js变量存储机制

在js中,对于原始类型,数据本身是存在栈内的,对于引用类型,在栈中存的只是一个堆内地址的引用

原因:

  1. 栈存储无论是分配新的空间还是释放空间(进栈和出栈)都很简单 访问栈里面的变量也非常快速
  2. 从数据的储存看,栈不能全取代堆,但堆可以取代栈
  3. 堆在分配和释放空间时要做相当多的工作 比如寻找合适大小的空间、GC 此外访问对立面的数据也比栈更慢 所以这些让堆存储的运行代价很高 影响性能
  4. 所以js对于那些数据结构固定的变量尽量放在栈里面

js内存泄漏

什么是内存泄漏: 不会被GC机制及时回收的内存,我们叫这种现象为内存泄漏
什么会导致内存泄漏

  1. 闭包
  2. 全局变量 对于全局变量GC很难判定什么时候需要清除
  3. 定时器的timeId
  4. 游离的dom的引用 当dom实例被保存的变量中 dom被删除后 此时变量并不会被垃圾回收
  5. 多余事件监听

js GC机制

对于GC,先要了解v8的内存机制

  1. v8内存分配: new Space 、 old Space 、 large Object Space(大对象存储空间)、code Space(代码编译存储空间)
  • new Space(64m) 分为 semi space from、semi space to 它们两块空间是平分 64m的 为了方便 scavenge算法实现
  • old Space(1.4g)分为 old point space(这就是大多数对象的引用指针存储位置和原始数据类型存储位置)、old data space
  1. 新老生代的GC机制完全不同
  • 新生代采用scavenge算法来完成from和to两大空间的替换的 (可以简单的理解为copy算法)
  • 变量定义一开始是存储到from的,from塞满之后,将from中无用变量去除后完全copy 与 to空间调换(内容调换)(典型的牺牲空间换时间)
  • 当space from和to两边经历过一次scavenge交换算法后,to空间已经使用了25%后 此时 to空间的变量则晋升至老生代
  • 老生代的GC是使用标记算法 常见是标记清除 和标记整理(整理后清除)(mark-sweep、mark-compact)
  • 标记清除:清理过程是GC Root出发(以GC Root出发扫描整个引用链) 内存中找出有引用的地址标记 没有引用的就其他清除(缺陷: 最后清除出的内存是不连续的)
  • 标记整理:标记整理是标记清除的优化后的算法 主要流程是先整理后清除 且整理过程还可以做替换 且GC是全停顿标记的(GC也是运用到主线程上)这样就不会出现需要锁的问题 GC还采用三色标记法去优化整个回收效率
  1. js中函数执行完成后,GC不会立即去回收其内部,局部变量的占用会等下一次需要用到(标记整理到)此块内存空间的时候启用标记整理重新回收空间
  2. GC是运行在js主线程上的,GC启用时 js代码会完全停止 GC完成后会重新恢复脚本执行

继承

  1. es5 寄生组合继承

// 父类 
  function Father(name) { this.name = name; this.colors = ["red", "blue", "green"]; }

  Father.prototype.sayName = function () { alert(this.name); };  // 方法定义在原型对象上(原型继承核心 实现方法继承) 

  // 子类
  function Son(name, age) {
    Father.call(this, name); // (借用构造函数继承核心  父类函数重新在子类执行一遍 完成属性赋值继承) 
    this.age = age;
  }
  Son.prototype = Object.create(Father.prototype);// (寄生继承核心 重新创建子类prototype 不会出现共享函数) 
  Son.prototype.constructor = Son; // 修复子类的 constructor 的指向

  1. es6继承
// 以下为es6 提供的Class继承  类比上面的寄生组合继承
  class Father {
    constructor(name) {
      this.name = name
    }

    sayName() {
      alert(this.name);
    }
  }

  class Son extends Father {
    constructor(name) { // prototype指向 且每次constructor都是生成新的对象
      super(name) // 借用构造函数
      this.age = age
    }
  }

ES6中的class和ES5的类有什么区别?

  1. ES6 class 内部所有定义的方法都是不可枚举的;
  2. ES6 class 必须使用 new 调用;
  3. ES6 class 不存在变量提升;
  4. ES6 class 默认即是严格模式;
  5. ES6 class 子类必须在构造函数中调用super(),这样才有this对象;ES5中类继承的关系是相反的,先有子类的this,然后用父类的方法应用在this上。

闭包

官方说法是延长作用域链,最简单的闭包就是一个函数(a)返回另一个函数,被返回的这个函数里面有使用到a函数里面的变量
闭包的作用就是驻留变量 使得其变量无法被GC回收
如何销毁闭包: 将闭包函数置为null 当闭包函数置为null后 其里面所驻留的变量自然也会被GC自动回收走(回答可以引申出 null为啥可以引发GC)

深浅拷贝

实现一个深拷贝

function deepClone(obj) {
    //递归拷贝 
    if (obj === null) return null; //null 的情况 
    if (obj instanceof RegExp) return new RegExp(obj);
    if (obj instanceof Date) return new Date(obj);
    if (typeof obj !== 'object') { //如果不是复杂数据类型,直接返回 
      return obj;
    }
    /** * 如果obj是数组,那么 obj.constructor 是 [Function: Array] 
     * 如果obj是对象,那么 obj.constructor 是 [Function: Object] */
    let t = new obj.constructor(); 
    for (let key in obj) {
      //如果 obj[key] 是复杂数据类型,递归 
      t[key] = deepClone(obj[key]);
    } return t;
  }

另:

  1. 如果要考虑属性有函数的时候 需要用 function() { return obj.apply(this, arguments) } 这种方式来复制函数

requestIdleCallback和requestAnimationFrame

  1. 需要了解这两个api的作用可以了解每一帧的生命周期:
  • 处理用户的交互
  • js开始执行
  • 帧开始(窗口尺寸变化、页面滚动等等处理)
  • requestAnimationFrame
  • 布局
  • 绘制
  • 如果以上时间未超过的时间 16.6ms 则执行 requestIdleCallback 否则不执行(浏览器空闲时间 操作不具备可预测性 在绘制的最后阶段后面执行 最好不要操作dom 会导致回流)

防抖与节流

  1. 防抖 debounce 频繁触发的耗时操作 但是结果可以只取最后一次触发的结果 如:窗口尺寸变化、按钮的高频点击 (只关心最终状态)
function debounce(fn, duration = 300) {
    let timeId;
    return function (...args) {
      clearTimeout(timeId)
      timeId = setTimeout(() => {
        fn.apply(this, args) // 重点注意 this指向  参数传递问题
      }, duration);
    }
  }
  1. 节流 throttle 目的是减少它的触发次数 以稳定的速率来处理函数(前一次执行完了再去管后一次)
 function throttle(fn, duration = 300) {
    let canNext = true
    return function (...args) {
      if (canNext) {
        setTimeout(() => {
          canNext = true
          fn.apply(this, args)
        }, duration);
      }
     canNext = false
    }
  }
  
  //时间差实现
  function throttle1(fn, duration = 300) {
    let pre = Date.now()
    return function (...args) {
      const now = Date.now()
      if (now - pre >= duration) {
        fn.apply(this, args)
        pre = Date.now()
      }
    }
  }

0.1 + 0.2问题 js精度问题

  • js运算时 他会将数字转换成二进制 0.1转换成2进制是一个无限循环小数0.000110011001100... (小数部分转换为2进制是不断乘2取它的整数部分顺序排列直到结果的小数部分为0)
  • js变量number保存时没有浮点数 以64位存储空间存储 存储方式为科学计数法 1.xxxxx * 2n次方形式 会经过截取保存 此时第一次精度丢失
  • 相加后进行第二次精度丢失
  1. 方案1:判断问题 0.1+0.2-0.3<Number.EPSILON
    Number.EPSILON表示的是1与Number可表示的大于1的最小的浮点数之间的差值
  2. 方案2:放大处理 放大为整数后得到结果进行相应的缩小这也是一些类库的解决方案

XMLHTTPRequest

  1. 手写XMLHTTPRequest get
const xhr = new XMLHttpRequest()
console.log(xhr);
xhr.open('GET', '/')
xhr.onreadystatechange = (e) => {
  // 监听网络回来的状态
  if(xhr.readyState === 4){
    // 当readyState为4的时候才能拿到响应的东西(无论是200 还是 500)
    }
}
xhr.send(params)    // 如果是post请求 send中可以携带请求体参数(注意这边用字符串 JSON转)他会依据params的类型来自动设置 content-type类型的 
  1. xhr.abort() 可以取消此次请求
  2. xhr.status 获取响应状态码

TS

interface 与 type

区别:

  • interface为接口 type叫做类型别名
  • 继承实现方式不同 interface使用extends关键字 type使用 & 符号
  • interface可以重复定义 重复定义后的效果是: 相同属性会报错不同属性会扩展, type不能重复定义会报错
  • type 可以覆盖interface无法覆盖的场景如: 组合 交叉 提取接口属性
/** 联合 */ 
type MixedType = string | number; 
/** 交叉 */ 
type IntersectionType = { id: number; name: string; } & { age: number; name: string }; 
/** 提取接口属性类型 */ 
type AgeType = ProgramLanguage['age'];

泛型

泛型是指在定义函数、接口或类的时候,不预先指定具体类型,而是在使用的时候再指定类型。
T是一个占位类型(首先不确定), 当他经过某些操作(如函数 类定义)后, 返回的与T有关的类型 这么一个过程可以用泛型来约束

一些关键字巧用

  1. unknown类型
  2. never类型 实现 a b不能同时存在但又要有其一的情况
  3. 模板字符串拼接: 取value的类型形成联合类型(类比去value 合并成一个数组)
  4. keyof: 取key类型形成联合类型

ts内置高级类型

  1. Partial 部分的 作用: 将所有属性变成可选的
  2. Required 必须的 作用是将所有属性变成必须的
  3. Readonly 只读的 作用将所有属性变成只读
  4. Pick 选择
  5. Record 记录
  6. Exclude 排除
  7. ReturnType 函数返回类型

实现内置ReturnType

// 使用extends 和 infer 结合判断
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

VUE

mvvm

mvvm即是 model - view - viewModel 的简称,view为用户看到的视图,model一般是本地数据或者数据库中的数据

传统mvc有一个controller去控制view和model两个层级,这样控制器中就会臃肿,mvvm将controller换成viewmodel,其中view只和viewmodel绑定,model的更新会自动通知到viewmodel层,类比到vue框架ViewModel 就是组件的实例。View 就是模板,Model 的话在引入 Vuex 的情况下是完全可以和组件分离的。

在 MVVM 架构中,引入了 ViewModel 的概念。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种情况下,View 和 Model 都可以独立出来,任何一方改变了也不一定需要改变另一方,并且可以将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel。

vue响应式原理

响应式原理的本质是当data中对象的属性发生变化的时候,会自动的触发一些函数的执行(最常见的就是render函数)

vue响应式核心部件: Observer Dep Watcher Scheduler vue通过以下几个步骤实现响应式:

  1. 在data中声明的变量函数,被 Observer 包装成响应式对象(getter setter) 底层原理是用 Object.defineproperty
    此过程是发生在beforeCreated 和 created两生命周期之间
    注意:
  • Vue.observable() 方法是和此过程是一致的
  • Object.defineproperty 缺点 只能相应对象属性的变化 无法相应对象属性删除和添加所以vue提供了 setset get 两个实例方法弥补
  • 无法对数组进行相应 所以vue改写数组里面的原型方法 push shift等方法进行改写
  1. 包装后,vue通过Dep实例去解耦属性的依赖和更新操作 当某个属性读取时,getter中会触发 dep.depend() 添加当前依赖 当属性改变的时候会通过dep.notify()派发更新

  2. vue如何收集依赖的 当响应到某个数据改变时,vue不会直接触发相应的函数去派发更新,他会把他交给watcher实例运行,watcher实例以变量的形式记录当前依赖函数,运行完清空(每次的改动都是为下次数据依赖的变化收集的 并且每次数据变化都会重新收集依赖 因为每次依赖都不一样)

  3. 收集的依赖很多,怎么去触发依赖相应更新 vue内部有scheduler调度器来解决 当相应派发到watcher时 vue不会立即执行 而是会将所有的watcher都放到队列里面去执行

  • Vue 在内部对异步队列尝试使用原生的 Promise.thenMutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

vue 模板编译

模板编译的本质是将template 模块编译成一个render函数

由于vue可以以js的方式来引入使用 所以编译方式分为两种

  • 运行时编译 (vue.js 在运行时编译)
  • 预编译 (工程化项目借助 vue-loader 预编译)
    大致方式是差不多的 以预编译为解析过程:
  1. 将模板解析为AST 原理是使用正则表达式去匹配模板内容 然后将内容转换为基本的AST对象(包含type tag attrsList children等等属性)

  2. 优化AST vue2中版本做了些许优化如提取静态内容 可以跳过对比算法 在vue3中 此阶段做了如下优化

  • 静态提升 静态节点的内容不放入render函数 在真正需要的时候调用 避免每次render都要去生成静态节点渲染函数
  • 预字符串化 遇到大量连续的静态内容 直接编译成普通字符串的节点
  • 缓存事件处理函数 保证事件处理函数只生成一次 节约内存空间
  • BlookTree 在BlookTree节点中记录其子节点哪一些是动态节点 diff算法时可以精准找出需要更新的子节点
  • PatchFlag 单个节点会记录哪一块是动态内容 如属性? 内容? 标签?在diff时可以不用做判断对比 直接更新标记块
  1. 将AST转换为render函数 遍历整个AST树 根据条件形成不同代码

vue diff算法

diff算法本质是 新老VDom树的对比 找到新老Vdom的不同从而针对性的更新不同处的真实Dom
这种精准的更新真实Dom才能提高Vue框架的效率 以下是对比过程

  1. 响应式对象触发setter 通过 Dep.notify 派发更新后 watcher就会调用patch方法 给真实dom打补丁
  2. vue只会在同层进行diff比较
    patch方法接收的是两个参数 oldVnode newVnode,通过isSameVnode方法判断 如果当前节点不同(是否为同一种类型的标签)直接用New代替Old 如果相同会进一步进行以下比较
  • 当前节点下都是文本节点 做文本节点更新
  • 新旧子节点 一个有一个没有 做删除或者增加子节点操作
  • 新旧字节点都存在的情况 触发updateChildren方法
  1. updateChildren方法采用首尾指针法 对比新旧子节点 首尾指针法会进行 新旧的 头头 头尾 尾头 尾尾比较 如果比较成功指针向中间靠拢 当start>end后终止比较 如果以上都没有匹配上 会把所有旧节点的key做一个映射,在新的Vnode里面的key找可以复用的

vue3对这一块进行了更新: vue3采用双端快速diff,只进行头头 尾尾的比较 通过最长递增子序列算法找出递增的是哪些就去更新哪一些 这样做虽然这样做在算法上时间复杂度会高一点 但是更大的好处是可以减少dom的移动 因为dom移动对比这点时间增加会更耗性能的

vuex原理

  1. 首先vuex(本质是一个对象)是vue的一个插件 他必然有install方法 除此之外他还有一个Store的类
  2. install方法是与所有vue插件一样 在使用vue.use时 初始化当前插件,本质上是store这个实例挂载到所有的组件上变成$store 此时他会对State里面的数据进行Observer的包装 这样state就有了响应式 并且store上有 commit,dispatch 这些方法 去触发相应的方法
  3. 除此之外 vuex还有个插件的概念 如我们经常用的vue-persistedstate 帮助vuex做持久化的

vue3: watch watchEffect 有啥区别

后端 nodejs

koa的洋葱模型

工程化 & 框架

esmodule 与commonjs 有什么区别

  1. ES6模块在编译时,就能确定模块的依赖关系,以及输入和输出的变量。 (静态导入)
    CommonJS 模块,运行时加载。
  2. ES6 模块自动采用严格模式,无论模块头部是否写了 "use strict";
  3. require 可以做动态加载,import 语句做不到,import 语句必须位于顶层作用域中。
  4. ES6 模块中顶层的 this 指向 undefined,CommonJS 模块的顶层 this 指向当前模块。
  5. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

npm run xxx 发生了什么

  1. 运行 npm run xxx的时候,首先会去项目的package.json文件里找相应的scripts

  2. 当命令中含有安装的包的缩写名称时 npm 会先在当前目录的 node_modules/.bin 查找要执行的程序,如果找到则运行;(我们运行npm i xxx时, 相应包的软连接会自动写入进.bin文件夹)

  3. 没有找到则从全局的 node_modules/.bin 中查找

  4. 如果全局目录还是没找到,那么就从 path 环境变量中查找有没有其他同名的可执行程序。

webpack编译过程

  1. 初始化
    webpack将配置文件 命令行参数 默认配置三者进行融合 行程最终配置对象

  2. 编译

  • 启动webpack,创建Compiler对象并开始解析项目;
  • 从入口文件(entry)开始解析,并且找到其导入的依赖模块,递归遍历分析,形成依赖关系树;
  • 对不同文件类型的依赖模块文件使用对应的Loader进行编译,最终转为Javascript文件; (这个是loader机制)
  1. 输出chunk
    根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,(这步是可以修改输出内容的最后机会) 在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统(或者内存当中)

在2 3整个过程中webpack会通过发布订阅模式,向外抛出一些hooks,而webpack的插件即可通过监听这些关键的事件节点,执行插件任务进而达到干预输出结果的目的 (这个是plugin机制)

webpack Loader + Plugin

  1. loader
    webpack默认只支持js文件的模块化处理,在打包时,webpack会讲所有的遇到的文件都当做js来处理,这个时候当项目存在非js文件时 我们就需要对其进行必要的转换才能继续执行打包任务 这也是Loader机制存在的意义

  2. 如何编写一个Loader:
    Loader本质是一个函数,其入参是上一个loader的输出(前一个loader的输出作为下一个loader的输入 在配置中这个顺序是倒序配置的 类比scss的配置顺序为css-postcss-url-sass) 他接收一个source参数 函数内部还有个this 他是webpack提供的上下文可以通过它获取当前loader需要的各种信息数据 webpack官方提供的 loader-utils可以获得很多操作方法

  3. plugin
    webpack在所有打包流程中预埋了一些触发事件(生命周期)在这些生命周期hook中 plugin可以操作打包对象 从而影响输出结果,loader是负责文件转换 plugin可以说是负责功能扩展的

  4. 如何编写一个Plugin:
    plugin本质是一类 他有一个apply函数 函数入参是 compiler compiler.plugin(事件名,处理函数) 处理函数里面 compilation 入参 可以通过它来拿到各种打包信息 还有done函数入参 当plugin中有异步操作 调用done函数可以保证在异步之后继续操作
    常见的生命周期是 compilation emit

编写plugin优化 针对dev环境页面多 启动项目慢 占用内存多的打包性能问题 1. 可以通过对页面配置进行打包差异(只打包需要开发涉及到的页面) 2. emit事件中 输出一份缓存文件 保存起来 下次项目启动先找缓存文件中有的

webpack babel-loader

  1. babel是一个js编译器,他主要功能是把新版的js编译成浏览器可以执行的版本

bable处理js的三个步骤如下:

  • 解析(parse) 将代码转换为AST,babel通过Babylon实现的 解析过程分为两个阶段:词法解析和语法解析
    词法解析是字符串形式的代码转换为令牌(tokens)流 类似于解析成AST中的节点
    语法解析是把一个令牌流转化为AST的形式,同时这个阶段会把令牌中的信息转化为AST的表述结构
  • 转换(transform) Babel 接受得到 AST 在此过程中对节点进行添加、更新及移除操作 babel的插件也是介入这一部分的工作
  • 生成(generate) 将经过转换的 AST构建成转换后的代码字符串
  1. 如何编写一个babel插件
    babel插件本质是一个函数 函数的返回值中包含各种配置
    配置中最基本的是 visitor 表示访问者 visitor中各个ast节点的函数可以在每次ast树遍历时触发 如:VariableDeclaration 代表变量声明(var let const),identifier(表示标识符 一般是变量)在这些方法当中提供了参数 path 对于整个节点都可以访问到并进行操作 如 replaceWith remove

webpack优化

总体webpack优化考虑两个方面:

  • 减少打包时间
  • 减少打包出来的包大小
  1. 打包时间 常用方法:
  • cache-loader可以用来将其他loader解析出来的东西cache起来(磁盘中)
  • happypack 可以让webpack将任务分解放在子进程中去并发进行 子进程处理完后发送给主进程
  • HardSourceWebpackPlugin 为模块可以提供中间缓存
  1. 减少包大小
  • 公共代码的一起压缩 DllPlugin
  • 开启按需加载 vue按需router
  • Tree Shaking
  • 使用webpack-bundle-analyzer插件 打包后同时生成包大小分析页面 逐一分析

前端监控

  1. 前端数据监控
  • PV/UV
  • 全局埋点 body上点击事件 通过冒泡的形式到顶层 拿到标签绑定属性 (注意冒泡会被阻止 可以换用捕获形式 无法阻止)
  • 停留时长 --- 引申元素曝光埋点 如何做 1. IntersectionObserver 监控页面是否处于视口位置 2. 没有此api 考虑如何埋点 页面mounted时记录位置判断 页面滚动中记录位置埋点(但含义不同 滚动过程中的埋点与滚动停下埋点)
  1. 性能监控
    借助浏览器端的Performance api去计算各个指标时间
    一般使用google提供的开源库 web-vitals 常见指标: LCP 最大内容绘制 TTFB 首次字节时间 FCP 第一个内容在视口中渲染时间 FP 首次绘制 第一个像素出现在视口

  2. 异常监控

  • window.onerror 事件 能访问到大部分的详细报错信息 (注意 跨域代码运行错误会显示 Script error 对于这种情况需要在script 标签上添加 crossorigin )
  • window.onunhandledrejection 访问到Promise没有reject处理的时候的报错信息 一般有两个对象 promise 和 reason

小程序架构

为什么小程序中没有dom概念 无法操作dom

  1. 通常的h5网页,页面线程和脚本运行是运行在同一个渲染线程下面的,这也是脚本运行可能会导致页面失去响应的原因
  2. 在小程序中 二者是分开的 分别运行在渲染线程和脚本线程中 这个也是我们常见的小程序"双线程模型" 逻辑线程运行在jsCore中, 并没有一个完整的浏览器对象 因而缺少相关的dom api和 bom api 同时jsCore环境也和node环境不相同 所以一些npm的包在小程序中也无法运行

微信小程序 双线程模型

微信小程序双线程模型 采用Skyline渲染引擎的方式
渲染线程负责: layout Paint 等任务
AppService线程负责: js逻辑 dom树创建逻辑

这样双线程的特点:

  • 界面不容易被逻辑阻塞 进一步减少卡顿

  • 无需为每个页面新建webbiew实例 减少内存开销

但是同时也产生问题: js询问页面信息等接口会变成异步 效率也可能有所下降

常见性能优化

juejin.cn/post/719440…

  1. spa项目首页白屏如何优化?

对于普通pc项目:

  • 对于静态文件的cdn加速
  • 前端代码打包优化 涉及webpack等等
  • 懒路由加载 或者 懒组件加载 (涉及ES6的动态地加载模块 import()) webpack分包机制(类比小程序如何分包) 可以配合link标签 preload prefetch属性做预加载
  • 分块渲染 (分帧渲染) 首页渲染阻塞主流程 (requestAnimationFrame)
  • js延迟执行 defer async关键属性
  • 如何做http缓存 协商缓存&强缓存
  • 必要的时候还可以pwa

对于app in h5项目还可干预webview的初始化来实现更快优化:

  • 提前初始化webview
  • 做离线包 (可以扩展离线包机制&怎么做离线包控制&客户端怎么做离线包的展示优化)

对于用户体验方面:

  • 做loading 还可以考虑loading动画前置到webview中
  • 做骨架屏 (骨架屏是进入html后的 可以做图片骨架屏也可以做html的骨架屏 前置解析html生成相应的)
  1. 客户端容器webview怎么优化(客户端方面)
  • 预初始化 初始化一个webView 等要用的时候再改变其样式拿到视图中来加载url(缺点: 占用内存)
  • 预启动&复用池 发现初始化过程中大部分耗时用在了启用chrome相关服务 在销毁webview后二次启用webview这部分时间没有(客户端复用池机制) 在cpu空闲时间去初始化一个webview (但不显示在视图上) 过一段时间销毁此webview 等二次启动webview时间大大减少(大概可以节省 400ms 总启动时间 600ms)
  • 离线包机制
  1. 离线包怎么优化
  • 优化离线包的更新策略
  • 优化离线包内请求
  • 离线包怎么做增量更新

算法

1. 实现并发请求函数

  /** 并发请求
   * @param urls 请求地址 urls数组
   * @param max 最大并发数
   * **/
  function concurrency(urls, max = 3) {
    return new Promise((resolve) => {
      const result = []
      if (urls.length === 0) {
        resolve(result)
        return
      }

      let index = 0 // 表示当前取出来的urls下标用来发送请求
      let count = 0 //当前请求完成数量
      function runFetch() {
        if (index === urls.length) return
        const curIndex = index //表示当前请求的下标
        const url = urls[index]
        index++
        fetch(url).then(res => {
          console.log(res);
          result[curIndex] = res
        }, err => {
          result[curIndex] = err
        }).finally(() => {
          count++
          //是否所有请求都完成
          if (count === urls.length) {
            resolve(result)
          }
          runFetch()

        })
      }

      const itemsCount = Math.min(urls.length, max) //保证极值情况
      for (let i = 1; i <= itemsCount; i++) {
        runFetch()
      }
    })
  }

  const urls = []
  for (let index = 1; index < 21; index++) {
    urls.push(`https://jsonplaceholder.typicode.com/todos/${index}`)
  }


  concurrency(urls, 5).then(res => {
    console.log(res);
  })

2. 函数keli化

// 当返回的闭包函数【参数数量】等于【fn参数数量】时,才执行fn函数,否则继续返回闭包函数本身供调用
function curry(fn: Function): Function {
  const params = [];
  return function curried(...args: unknown[]) {
    params.push(...args);
    if (params.length === fn.length) {
      return fn(...params);
    }
    return curried;
  };
};

3. 实现Promise.all

function myPromiseAll(promises) {
      return new Promise((resolve, reject) => {
        // 还是需要判断是否迭代对象
        if (typeof promises[Symbol.iterator] !== "function") {
          reject("参数需要为一个迭代对象");
        }

        if (promises.length === 0) {
          resolve([]);
        } else {
          let count = 0;
          let resultArr = [];
          for (const promise of promises) {
            Promise.resolve(promise).then(
              (res) => {
                count++;
                resultArr.push(res);
                if (count === promises.length) {
                  resolve(resultArr);
                }
              },
              (err) => {
                reject(err);
              }
            );
          }
        }
      });
    }

4. 实现Promise.race

function MyPromiseRace(promises) {
      return new Promise((resolve, reject) => {
        // promises判断是否是一个可迭代对象
        if (typeof promises[Symbol.iterator] !== "function") {
          reject("args is not iteratable!");
        }

        if (promises.length === 0) {
          return;
        } else {
          for (const promise of promises) {
            // 这里不使用peomise.then 而使用 Promise.resolve(promise).then 是因为 promise也可能是非PromiseLisk

            Promise.resolve(promise).then(
              (res) => {
                resolve(res);
              },
              (err) => {
                reject(err);
              }
            );
          }
        }
      });
    }

5. 最长子串问题:

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

输入: s = "abcabcbb" 输出: 3 解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

输入: s = "bbbbb" 输出: 1 解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

// 滑动窗口法 双指针  
    
    function lengthOfLongestSubstring(s) {
    if (s.length === 0) {//极端情况
        return 0;
    }
    
    // 窗口初始与结束指针
    let start = 0
    let end = 0

    let maxLength = 0;
    const set = new Set()

    for (end; end < s.length; end++) {
        if (!set.has(s[end])) {
            set.add(s[end])
            maxLength = Math.max(maxLength, set.size) // 如果需要保存最长子串则需要在这边做操作
        } else {
            while (set.has(s[end])) {
                set.delete(s[start])
                start++
            }
            set.add(s[end])
        }
    }
    return maxLength  
};

6. 如何找出链表中间位

思路: 快慢指针法

                             

7. 0 1数组排序

非空数组arr, 其中是由0和1乱序组合而成。需要实现函数, 将所有的0排在1前面。 要求不能使用 Array.sort,时间复杂度 O(n), 空间复杂度 O(1)

example:
输入: [1, 0, 1, 1, 0, 0, 1, 0]
输出: [0, 0, 0, 0, 1, 1, 1, 1]
实现: function mySort() { }
console.log(mySort([1, 0, 1, 1, 0, 0, 1, 0])) //[0, 0, 0, 0, 1, 1, 1, 1]

                             
 function sort(arr) {
  let index = -1
  let walk = 0
  while (walk < arr.length) {
    if (arr[walk] === 0) {
      index++
      arr[walk] = 1
      arr[index] = 0
    }
    walk++
  }
  return arr
}                           

8. 实现二叉树的 前中后序遍历方法

前序: https://leetcode.cn/problems/binary-tree-preorder-traversal/
中序: https://leetcode.cn/problems/binary-tree-inorder-traversal/
后续: https://leetcode.cn/problems/binary-tree-postorder-traversal/
// 无论前中后序遍历都是用的 递归遍历方式 只是输出的元素顺序不同 “序” 指的是根元素在左右元素的哪个位置
// 前序遍历即为根左右
var preorder = function (node, res) {
    if (!node) return
    res.push(node.val)
    preorder(node.left, res)
    preorder(node.right, res)
}

var preorderTraversal = function (root) {
    let res = []
    if (!root) return res
    preorder(root, res)
    return res
};  

// 中序遍历即为左根右
var inorder = function(node,res) {
    if (!node) return
    inorder(node.left, res)
    res.push(node.val)
    inorder(node.right, res)
}
/**
 * @param {TreeNode} root
 * @return {number[]}
 */
var inorderTraversal = function(root) {
    if(!root) return []
    let res = []
    inorder(root, res)
    return res
};    

// 后续遍历为左右根 此处省略
// 除了递归遍历法 常用的还有迭代遍历法  
// 需要借用栈的思想
var preorderTraversal = function (root) {
    // 使用迭代遍历法需要借助栈的思想  
    if (!root) return []
    let stack = []
    let res = []
    stack.push(root)
    while (stack.length) {
        const cur = stack.pop();
        res.push(cur.val)
        if (cur.right) { //这里注意出栈入栈思想 先入栈的后处理
            stack.push(cur.right)
        }
        if (cur.left) {
            stack.push(cur.left)
        }
    }
    return res
};

9. 快速排序

 const quickSort1 = arr => {
      if (arr.length <= 1) { return arr; }
      //取基准点 
      const midIndex = Math.floor(arr.length / 2);
      //取基准点的值,splice(index,1) 则返回的是含有被删除的元素的数组。 
      const valArr = arr.splice(midIndex, 1);
      const midIndexVal = valArr[0]; const left = [];
      //存放比基准点小的数组 
      const right = []; //存放比基准点大的数组 //遍历数组,进行判断分配 
      for (let i = 0; i < arr.length; i++) {
        if (arr[i] < midIndexVal) {
          left.push(arr[i]);
          //比基准点小的放在左边数组 
        } else {
          right.push(arr[i]);
          //比基准点大的放在右边数组 
        }
      } //递归执行以上操作,对左右两个数组进行操作,直到数组长度为 <= 1 
      return quickSort1(left).concat(midIndexVal, quickSort1(right));
    };