# JavaScript

# 基础

# 数据类型

数据类型检测的四种方法
  1. typeof(缺点:数组和 null 会被判定为 object)
  2. instanceof(缺点:只能判断引用类型)
  3. Object.prototype.toString.call()
  4. constructor(缺点:对象原型改变时无法正确检测类型)
隐式类型转换

type

ull和undefined的区别

null 是一个表示” 无” 的对象,转为数值时为 0;undefined 是一个表示” 无” 的原始值,转为数值时为 NaN。
当声明的变量还未被初始化时,变量的默认值为 undefined。
null 用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象。

undefined 表示” 缺少值”,就是此处应该有一个值,但是还没有定义。典型用法是:

  1. 变量被声明了,但没有赋值时,就等于 undefined。
  2. 调用函数时,应该提供的参数没有提供,该参数等于 undefined。
  3. 对象没有赋值的属性,该属性的值为 undefined。
  4. 函数没有返回值时,默认返回 undefined。

null 表示” 没有对象”,即该处不应该有值。典型用法是:

  1. 作为函数的参数,表示该函数的参数不是对象。
  2. 作为对象原型链的终点。
类型转换规则
  1. 首先会判断两者类型是否相同,相同的话就比较两者的大小;
  2. 类型不相同的话,就会进行类型转换;
  3. 会先判断是否在对比 null 和 undefined,是的话就会返回 true
  4. 判断两者类型是否为 string 和 number,是的话就会将字符串转换为 number
  5. 判断其中一方是否为 boolean,是的话就会把 boolean 转为 number 再进行判断
  6. 判断其中一方是否为 object 且另一方为 string、number 或者 symbol,是的话就会把 object 转为原始类型再进行判断

其他值转字符串

  • Null 和 Undefined 类型 ,null 转换为 "null",undefined 转换为 "undefined",
  • Boolean 类型,true 转换为 "true",false 转换为 "false"。
  • Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
  • Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
  • 对普通对象来说,除非自行定义 toString () 方法,否则会调用 toString ()(Object.prototype.toString ())来返回内部属性 [[Class]] 的值,如 "[object Object]"。如果对象有自己的 toString () 方法,字符串化时就会调用该方法并使用其返回值。

其他值转数字

  • Undefined 类型的值转换为 NaN。
  • Null 类型的值转换为 0。
  • Boolean 类型的值,true 转换为 1,false 转换为 0。
  • String 类型的值转换如同使用 Number () 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
  • Symbol 类型的值不能转换为数字,会报错。
  • 对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作 DefaultValue)检查该值是否有 valueOf () 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString () 的返回值(如果存在)来进行强制类型转换。

如果 valueOf () 和 toString () 均不返回基本类型值,会产生 TypeError 错误。

其他值转布尔值
以下这些是假值:

  • undefined
  • null
  • false
  • +0、-0 和 NaN
  • ""
undefined>=undefined、null>=null、[]==![]

涉及隐式转换

  1. NaN>=NaN,false
  2. 0>=0,true
  3. []==false -> []==0 -> ''0 -> 00 ,true
includes和indexOf差别

includes 内部使用 Number.isNaN 对 NaN 进行检测,而 indexOf 无法检测 NaN

介绍一下Set、Map、WeakSet、WeakMap的区别

Set:不能出现重复的原始值和引用值
Map:键值对的集合,类似一个字典表
WeakSet:成员都是对象且弱引用,可以被垃圾回收机制回收从而防止内存泄漏,可以保存 DOM 节点
WeakMap:只接受对象作为键名(null 除外)且是弱引用,值任意,键名所指向的对象可以被垃圾回收机制回收,此时键名无效不能遍历,方法只有 get、set、has、delete

# 核心

var和let、const的区别和实现原理
  1. var 和 let 声明变量,const 声明常量且必须初始化赋值
  2. var 是函数作用域,let、const 是块级作用域,所以 let 和 const 无法存在 window 上
  3. var 存在变量提升,在 var 之前打印变量返回 undefined,而 let 和 const 不存在变量提升会报错(暂时性死区)
  4. var 可以重复声明,let 和 const 不行

JS 引擎在读取变量时,先找到变量绑定的内存地址,然后找到地址所指向的内存空间,最后读取其中内容,当变量改变时,JS 引擎不会用新值覆盖之前的旧值的内存空间,而是重新分配一个新的内存空间来存储新值,并将新的内存地址与变量进行绑定,JS 引擎会在何时的时机进行 GC,回收旧的内存空间
const 定义常量时,变量名与内存地址之间建立一种不可变的绑定关系,阻隔变量地址改变,当 const 定义的变量重新赋值时,JS 引擎会抛出异常

new操作符做了什么
  1. 在内存中创建一个新对象。
  2. 将新对象内部的 proto 赋值为构造函数的 prototype 属性。
  3. 将构造函数内部的 this 被赋值为新对象(即 this 指向新对象)。
  4. 执行构造函数内部的代码(给新对象添加属性)。
  5. 如果构造函数返回非空对象,则返回该对象。否则返回 this。
讲讲JS继承方式
  1. 原型链继承
  2. 构造函数继承
  3. 组合继承
  4. 寄生组合继承
  5. class 继承

具体原理、优缺点、实现可搜索相关贴子或教材

箭头函数特性
  1. 没有 this,call ()、apply ()、bind () 等方法不能改变箭头函数中 this 的指向
  2. 没有 prototype,故不能作为构造函数
  3. 没有 arguments 对象
  4. 箭头函数不能用作 Generator 函数,不能使用 yeild 关键字
forEach如何跳出循环

try catch 捕获函数,在特定条件下 throw error

[1,2,3].map(parseInt)

[1,NaN,NaN]
parseInt(value,index)
parseInt(1,0)、parseInt(2,1)、parseInt(3,2)

高阶函数

高阶函数是指传入参数为函数(数组的 map、reduce、forEach)或输出参数为函数(手写 add)

a==1 && a==2 && a==3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 拦截法
var val = 0;
Object.defineProperty(window, 'a', {
get: function () {
return ++val;
},
});

// 重写toString(node环境无效)
var a = {
i: 1,
toString: function () {
return a.i++;
},
};

// 重写数组方法
var a = [1, 2, 3];
a.join = a.shift;

立即执行函数作用域

1
2
3
4
5
6
7
8
9
var b = 10;
(function b() {
// 内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b做赋值;
// IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
// (这里说的“内部机制”,想搞清楚,需要去查阅一些资料,弄明白IIFE在JS引擎的工作方式,堆栈存储IIFE的方式等)
b = 20;
console.log(b); // [Function b]
console.log(window.b); // 10,不是20
})();

严格模式下,会报错 "Uncaught TypeError: Assignment to constant variable."

call 和 apply 的区别是什么,哪个性能更好一些

call 更好一些,因为 apply 多了一次将数组解构的操作

+++

# 概念机制

严格模式

规则:

  • 变量必须声明后再使用
  • 函数的参数不能有同名属性,否则报错
  • 不能使用 with 语句
  • 不能对只读属性赋值,否则报错
  • 不能使用前缀 0 表示八进制数,否则报错
  • 不能删除不可删除的属性,否则报错
  • 不能删除变量 delete prop,会报错,只能删除属性 delete global [prop]
  • eval 不会在它的外层作用域引入变量
  • eval 和 arguments 不能被重新赋值
  • arguments 不会自动反映函数参数的变化
  • 不能使用 arguments.callee
  • 不能使用 arguments.caller
  • 禁止 this 指向全局对象
  • 不能使用 fn.caller 和 fn.arguments 获取函数调用的堆栈
  • 增加了保留字(比如 protected、static 和 interface)

目的:

  • 消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为;
  • 消除代码运行的一些不安全之处,保证代码运行的安全;
  • 提高编译器效率,增加运行速度;
  • 为未来新版本的 Javascript 做好铺垫。
内存泄漏

内存泄漏是指,应当被回收的对象没有被正常回收,变成常驻老生代的对象,导致内存占用越来越高。内存泄漏会导致应用程序速度变慢、高延时、崩溃等问题。

内存生命周期包括

  1. 分配:按需分配内存。
  2. 使用:读写已分配的内存。
  3. 释放:释放不再需要的内存。

常见原因

  1. 全局变量没有手动回收。
  2. 函数变量闭包
  3. 使用 JavaScript 对象来做缓存,且不设置过期策略和对象大小控制。
  4. 定时器未解绑
  5. 事件监听未销毁
垃圾回收

V8 中有两个垃圾收集器。主要的 GC 使用 Mark-Compact 垃圾回收算法,从整个堆中收集垃圾。小型 GC 使用 Scavenger 垃圾回收算法,收集新生代垃圾。两种不同的算法应对不同的场景:

  • 使用 Scavenger 算法主要处理存活周期短的对象中的可访问对象。
  • 使用 Mark-Compact 算法主要处理存活周期长的对象中的不可访问的对象。

因为新生代中存活的可访问对象占少数,老生代中的不可访问对象占少数,所以这两种回收算法配合使用十分高效。

  1. 分代垃圾收集
    在 V8 中,所有的 JavaScript 对象都通过堆来分配。V8 将其管理的堆分成两代:新生代和老生代。其中新生代又可细分为两个子代(Nursery、Intermediate)。即新生代中的对象为存活时间较短的对象,老生代中的对象为存活时间较长或常驻内存的对象。

分代垃圾收集
2. Mark-Compact 算法(Major GC)
Mark-Compact 算法可以看作是 Mark-Sweep(标记清除)算法和 Cheney 复制算法的结合。该算法主要分为三个阶段:标记、清除、整理。

Major GC

(1)标记(Mark):标记是找所有可访问对象的过程。GC 会从一组已知的对象指针(称为根集,包括执行堆栈和全局对象等)中,进行递归标记可访问对象。
(2)清除(Sweep):清除是将不可访问的对象留下的内存空间,添加到空闲链表(free list)的过程。未来为新对象分配内存时,可以从空闲链表中进行再分配。
(3)整理(Compact):整理是将可访问对象,往内存一端移动的过程。主要解决标记清除阶段后,内存空间出现较多内存碎片时,可能导致无法分配大对象,而提前触发垃圾回收的问题。
3. Scavenger 算法(Minor GC)
V8 对新生代内存空间采用了 Scavenger 算法,该算法使用了 semi-space(半空间) 的设计:将堆一分为二,始终只使用一半的空间:From-Space 为使用空间,To-Space 为空闲空间。

Minor GC
新生代在 From-Space 中分配对象;在垃圾回收阶段,检查并按需复制 From-Space 中的可访问对象到 To-Space 或老生代,并释放 From-Space 中的不可访问对象占用的内存空间;最后 From-Space 和 To-Space 角色互换。

被淘汰的引用计数法:跟踪每个变量被使用的情况,当声明或赋值时引用 count+1,如果变量的值改变则原值引用 count-1,循环时回收引用次数为 0 的变量,但有循环引用导致内存泄漏的问题

模块化AMD和CommonJs的理解

CommonJS 加载模块是同步的,适用 nodeJs,因为本地加载很快,拷贝输出,可修改引用值,运行时
AMD 规范则是异步步加载模块,允许指定回调函数,适用浏览器,网络请求不一定很快,引用输出,只读,编译时

# ES6

# 作用域、作用域链、闭包、预编译

作用域和作用域链的理解

函数的变量访问基于函数目前所处的环境,优先访问函数作用域也就是代码块中的变量,若没有则沿作用域链向上单向访问直到 window/global

# 原型、原型链、继承、this

JS 中 this 的五种情况
  1. 作为普通函数执行时,this 指向 window。
  2. 当函数作为对象的方法被调用时,this 就会指向该对象。
  3. 构造器调用,this 指向返回的这个对象。
  4. 箭头函数 箭头函数的 this 绑定看的是 this 所在函数定义在哪个对象下,就绑定哪个对象。如果有嵌套的情况,则 this 绑定到最近的一层对象上。
  5. 基于 Function.prototype 上的 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply 接收参数的是数组,call 接受参数列表,bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。若为空默认是指向全局对象 window。

# 异步编程

浏览器和Node中的事件循环

JS 是单线程的,为了防止一个函数执行时间过长阻塞后面的代码,所以会先将同步代码压入执行栈中,依次执行,将异步代码推入异步队列,异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列
在浏览器环境中,有 JS 引擎线程和渲染线程,且两个线程互斥。 Node 环境中,只有 JS 线程。 不同环境执行机制有差异,不同任务进入不同 Event Queue 队列。 当主程结束,先执行准备好微任务,然后再执行准备好的宏任务,一个轮询结束。

浏览器中的事件环(Event Loop)

eventLoop
事件环的运行机制是,先会执行栈中的内容,栈中的内容执行后执行微任务,微任务清空后再执行宏任务,先取出一个宏任务,再去执行微任务,然后在取宏任务清微任务这样不停的循环。

eventLoop 是由 JS 的宿主环境(浏览器)来实现的;

事件循环可以简单的描述为以下四个步骤:

  1. 函数入栈,当 Stack 中执行到异步任务的时候,就将他丢给 WebAPIs, 接着执行同步任务,直到 Stack 为空;
  2. 此期间 WebAPIs 完成这个事件,把回调函数放入队列中等待执行(微任务放到微任务队列,宏任务放到宏任务队列)
  3. 执行栈为空时,Event Loop 把微任务队列执行清空;
  4. 微任务队列清空后,进入宏任务队列,取队列的第一项任务放入 Stack (栈)中执行,执行完成后,查看微任务队列是否有任务,有的话,清空微任务队列。重复 4,继续从宏任务中取任务执行,执行完成之后,继续清空微任务,如此反复循环,直至清空所有的任务。
    eventLoop

Node 是基于 V8 引擎的运行在服务端的 JavaScript 运行环境,在处理高并发、I/O 密集 (文件操作、网络操作、数据库操作等) 场景有明显的优势。虽然用到也是 V8 引擎,但由于服务目的和环境不同,导致了它的 API 与原生 JS 有些区别,其 Event Loop 还要处理一些 I/O,比如新的网络连接等,所以 Node 的 Event Loop (事件环机制) 与浏览器的是不太一样。
eventLoop

相关API
浏览器:
宏任务:setTimeout、setInterval、requestAnimationFrame
微任务:promise.then(async/await)、MutationObserver
Node:
宏任务:setTimeout、setInterval、setImmediate、I/O
微任务:promise.then(async/await)、process.nextTick

区别
node 环境下定时器时依次一起执行的,而浏览器是一个个分开的,有单独的线程处理
浏览器的微任务执行是在每个宏任务之后,而 node 中则是在按阶段执行,一个阶段一轮回

如何做到并发请求

Promise.all 或者 web worker

Window.onLoad和DOMContentLoaded事件执行优先级

dom 树构建完成时执行 DOMContentLoaded,然后页面挂载时执行 Window.onLoad。

# TypeScript

Interface 和 Type区别

相同点

  1. Interface 和 Type 描述的类型都可以被 class 实现。
  2. Interface 和 Type 都可以扩展类型。但 interface 的实现方式是 extends,Type 则是用交叉类型的方式,extends 中的同名字段的类型必须是兼容的。而交叉类型中出现了同名字段且类型不同时,则类型一般是 never。

不同点

  1. interface 只能描述对象,适用于接口类型校验,type 则是可以定义任何类型
  2. Interface 可以重载、而 Type 不可重复定义。
  3. Type 可以使用 in 关键字动态生成属性,而 Interface 的索引值必须是 string 或 number 类型,所以 Interface 并不支持动态生成属性。

# 框架

# Vue

Object.defineProperty有什么缺陷
  1. 无法监听数组下标引起的改变、对象新增属性
  2. 需要深度遍历整个 dom 树为每个节点添加 getter 和 setter
Vue双向数据绑定

Vue2 采用 Object.defineProperty 为虚拟 dom 对象的每个属性添加 getter 和 setter 方法(观察者模式)
Vue3 采用 Proxy 做数据代理,相当于有一面墙在一个对象外,每次读写都会穿过这一面墙进行 get、set

Vue3diff算法(虚拟dom、keys原理与下面react相同)

vue2 中 diff 算法对虚拟 dom 进行全量对比,而 3 中新增了静态标记(PatchFlag),在与旧树对比时只对比带有标记的节点(如模板语法节点)。
所以说 vue2 中每次都要重新创建元素,而 vue3 只需要对不参与更新的元素创建一次,之后不断复用
优化静态 slot,使其父级元素改变时 slot 不做重渲染
事件缓存

Vue的生命周期(vue3)

Vue生命周期

v-if和v-show区别、v-if和v-for执行顺序

v-if 控制 dom 有无,v-show 控制节点属性 display:none
vue2 中 v-for > v-if,v-for 套 v-if 可以用 computed 解决,v-if 套 v-for 可以用 template
vue3 中 v-if > v-for

vue组件通信
  1. prop(父子)
  2. emit(子传父)
  3. provide/inject(多级向下)
  4. mitt(组件间)
  5. pinia(状态仓库)
Vue性能优化
  1. 事件代理
  2. keep-alive 缓存组件
  3. 组件懒加载、图片懒加载、虚拟列表
  4. 防抖节流
nextTick的实现原理

nextTick 会在 dom 更新循环结束后执行延迟回调,主要使用了任务队列,根据环境兼容性分别使用
Promise->MutationObserver->setImmediate->setTimeout

Vue 中的 computed 是如何实现的

computed 本身是通过代理的方式代理到组件实例上的,所以读取计算属性的时候,执行的是一个内部的 getter,而不是用户定义的方法。

computed 内部实现了一个惰性的 watcher,在实例化的时候不会去求值,其内部通过 dirty 属性标记计算属性是否需要重新求值。当 computed 依赖的任一状态(不一定是 return 中的)发生变化,都会通知这个惰性 watcher,让它把 dirty 属性设置为 true。所以,当再次读取这个计算属性的时候,就会重新去求值。

v-if、v-show、v-html 的原理是什么,它是如何封装的?

v-if 会调用 addIfCondition 方法,生成 vnode 的时候会忽略对应节点,render 的时候就不会渲染
v-show 会生成 vnode,render 的时候也会渲染成真实节点,只是在 render 过程中会在节点的属性中修改 show 属性值,也就是常说的 display
v-html 会先移除节点下的所有节点,调用 html 方法,通过 addProp 添加 innerHTML 属性,归根结底还是设置 innerHTML 为 v-html 的值

# React

React 虚拟DOM、diff算法原理、keys 的作用是什么

虚拟 DOM 本质是对象,通过遍历 dom 树克隆 dom 上的属性生成。操作对象比操作 dom 性能消耗更少

diff 算法是将新生成的虚拟 dom 树按树形结构比较旧树的同级元素,为每个组件状态中需要改变的 dom 节点标记为 dirty,并在事件循环结束时重新渲染
为了降低时间复杂度,React 和 Vue 的思路是基于以下两个假设条件,缩减递归迭代规模,将 Diff 算法的时间复杂度降低为 O (n):
相同类型的组件产生相同的 DOM 结构,反之亦然。所以不同类型组件的结构不需要进一步递归 Diff。
同一层级的一组节点,可以通过唯一标识符进行区分。

keys 一般出现在 for 循环中,且每个 for 循环的 keys 是独立的,keys 是 react 追踪哪些列表中的元素被增删改的辅助标识,它保证一个 for 循环中某个元素的唯一性,使得 react 在进行 diff 算法时能更高效的进行比对,以确定哪些元素需要被增删改,keys 尽量不要用 index,否则删除节点时也会重新渲染整个列表

React的生命周期

React生命周期

React为什么要废除componentWillMount componentWillUpdate 和componentWillReceiveProps

react 分为 render phase 和 commit phase 的,而像 componentWillmount componentWillUpdate 和 componentWillReceiveProps 等几个生命周期函数(包括 render)都是属于 render phase 的,在 fiber 机制提出前,render phase 阶段是不可被打断的(同步渲染),但是同步渲染会有体验问题,比如有几千个组件在渲染时,用户是没办法和浏览器进行交互的(js 线程被占用)。在 fiber 机制提出后,render phase 阶段可被打断,被打断后再次执行(优先级别),就会有以上提到的几个生命周期函数被多次调用。所以被废弃掉。componentwillmount 这个生命周期函数之前,有很多程序猿在里面写一些有副作用的 code,比如 ajax 调用,但这种做法 react 官方是不推荐的。可是又不能禁止。看 16.7 之后,它推出的新的生命周期 getDerivedStateFromProp 函数,就是一个 static 函数,在里面拿不到 this 也无法 setstate,更符合纯函数的概念。

React常用的hook有哪些
React的事件代理机制(react17事件合成机制)

React 中的事件代理并非和原生一样(为了解决跨浏览器兼容),而是采用合成事件(SyntheticEvent),将事件绑定冒泡到根节点(16 是 document)统一管理

实现合成事件主要为了:
解决浏览器兼容问题,并且支持跨端开发
对于原生浏览器事件来说绑定一个事件则创建一个事件对象,如果有多个事件监听则会分配很多事件对象,造成高额的内存分配问题,合成事件则采用事件池专门管理事件的创建与销毁
具体可以看这条链接
https://juejin.cn/post/6955636911214067720#heading-1

说一下 react-fiber
React setState是同步还是异步?

setState 会根据场景的不同来决定,通过 isBathingUpdates 来判断 setState 是先存进 state 队列还是直接更新。在 react 可以控制的地方,如生命周期事件和合成事件,都会走合并操作延迟更新,而无法控制的地方,如原生事件中就走的同步操作

React性能优化

主要方向有以下几个

  1. 减少组件重新渲染(memo)
  2. 缓存状态和函数(useMemo、useCallback)
  3. 长列表懒加载(虚拟列表),组件懒加载、图片懒加载
React组件通信方式
  1. props(父传子、子传父 [回调函数])
  2. useImperativeHandle(子传父)
  3. Context(多级往下)
  4. mitt(两个组件间)
  5. mobx、recoil(状态仓库)

# 框架综合

Vue和React区别

状态:Vue 采用 Proxy 做数据代理监听每个状态的变换;React 默认通过比较引用(diff)进行,因此 react 需要绑定很多的 memo、useMemo、useCallback 做缓存处理
主要是 react 更强调数据不可变以及单向数据流,而 vue 强调可变数据以及数据的双向绑定,
渲染:Vue 再渲染时会跟踪每一个组件的依赖关系,不需要重新渲染整个渲染树,React 在渲染时则会直接渲染该组件以及其子组件

React和Vue的diff算法时间复杂度从O(n^3^)降到O(n),是如何计算出来的?
  1. 如果父节点不同,放弃对子节点的比较,直接删除旧节点,然后添加新的节点重新渲染
  2. 如果子节点有变化,VirtualDom 不会计算而是重新渲染
  3. 通过 key 唯一策略
框架如何优化首页的加载速度?首页白屏是什么问题引起的?如何解决呢?

首页加载过慢,其原因是因为它是一个单页应用,需要将所有需要的资源都下载到浏览器端并解析。
解决办法

  1. 使用首屏 SSR + 跳转 SPA 方式来优化
  2. 改单页应用为多页应用,需要修改 webpack 的 entry
  3. 改成多页以后使用应该使用 prefetch 的就使用
  4. 处理加载的时间片,合理安排加载顺序,尽量不要有大面积空隙
  5. CDN 资源还是很重要的,最好分开,也能减少一些不必要的资源损耗
  6. 使用 Quicklink,在网速好的时候 可以帮助你预加载页面资源
  7. 骨架屏这种的用户体验的东西一定要上,最好借助 stream 先将这部分输出给浏览器解析
  8. 合理使用 web worker 优化一些计算
  9. 缓存一定要使用,但是请注意合理使用
  10. 可以借助一些工具进行性能评测,重点调优,例如使用 performance 自己实现下等
路由概念,前端路由与后端路由区别

路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。 —— 维基百科

对于 Web 开发来说,路由的实质是 URL 到对应的处理程序的映射。Web 路由既可以由服务端,也可以由前端实现。
其中前端路由根据实现方式的不同,可以分为 Hash 路由 和 History 路由。

前端路由对于服务端路由来说,最显著的特点是页面可以在无刷新的情况下进行页面的切换。基于前端路由的这一特点,诞生了一种无刷新的单页应用开发模式 SPA。SPA 通过前端路由避免了页面的切换打断用户体验,让 Web 应用的体验更接近一个桌面应用程序。

单页面应用(SPA)的优缺点

优点:1、利用 ajax 技术,前后端分离,实现数据局部获取渲染 2、基于动态路由,页面转换时可以自定义动画,记录 location
缺点:1、不利于 seo 2、没有路由导航 3、资源大,加载耗时长,首屏问题(可以用组件懒加载解决)

路由中hash和history的原理

Hash特点

  1. hash 通过 window.onhashchange 的方式,来监听 hash 的改变,借此实现无刷新页面切换的功能。
  2. hash 变化会改变浏览器的历史记录。
  3. hash 不会触发页面重新加载(hash 的改变是记录在 window.history 中),所有页面的跳转都是在客户端进行操作,永远不会提交到 server 端(可以理解为只在前端自生自灭)。因此,这并不算是一次 http 请求,所以这种模式不利于 SEO 优化。hash 只能修改 # 后面的部分,所以只能跳转到与当前 url 同文档的 url 。

优点:兼容性好,无需服务端配置
缺点:服务端无法获取 hash 部分内容、可能和锚点功能冲突、SEO 不友好。

History特点

  1. history API 是 H5 提供的新特性,允许开发者通过 pushState 、 replaceState 来实现无刷新跳转的功能。
  2. 新的 url 可以是与当前 url 同源的任意 url ,也可以是与当前 url 一样的地址,但是这样会导致的一个问题是,会把重复的这一次操作记录到栈当中。
  3. 通过 history.state ,添加任意类型的数据到记录中。

优点:服务端可获取完整的链接和参数、前端监控友好、SEO 相对 Hash 路由友好。
缺点:兼容性稍弱、需要服务端额外配置(各 path 均指向同一个 HTML)。

两者的差别
使用 history 模式时,在对当前的页面进行刷新时,此时浏览器会重新发起请求。如果 nginx 没有匹配得到当前的 url ,就会出现 404 的页面。
而对于 hash 模式来说, 它虽然看着是改变了 url ,但不会被包括在 http 请求中。所以,它算是被用来指导浏览器的动作,并不影响服务器端。因此,改变 hash 并没有真正地改变 url ,所以页面路径还是之前的路径, nginx 也就不会拦截。
因此,在使用 history 模式时,需要通过服务端来允许地址可访问,如果没有设置,就很容易导致出现 404 的局面。

路由权限的实现

权限管理一般需求是页面权限和按钮权限的管理

具体实现的时候分后端和前端两种方案:
前端方案会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个 asyncRoutes 数组,需要认证的页面在其路由的 meta 中添加一个 roles 字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过 router.addRoutes (accessRoutes) 方式动态添加路由即可。
后端方案会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过 addRoutes 动态添加路由信息
按钮权限的控制通常会实现一个指令,例如 v-permission,将按钮要求角色通过值传给 v-permission 指令,在指令的 moutned 钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮。

纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!

如果让你从零开始写一个vue路由,说说你的思路
  • 借助 hash 或者 history api 实现 url 跳转页面不刷新
  • 同时监听 hashchange 事件或者 popstate 事件处理跳转
  • 根据 hash 值或者 state 值从 routes 表中匹配对应 component 并渲染之

一个 SPA 应用的路由需要解决的问题是页面跳转内容改变同时不刷新,同时路由还需要以插件形式存在,所以:

首先我会定义一个 createRouter 函数,返回路由器实例,实例内部做几件事:

  1. 保存用户传入的配置项
  2. 监听 hash 或者 popstate 事件
  3. 回调里根据 path 匹配对应路由

将 router 定义成一个 Vue 插件,即实现 install 方法,内部做两件事:

实现两个全局组件:router-link 和 router-view,分别实现页面跳转和内容显示
定义两个全局变量:routeroute和 router,组件内可以访问当前路由和路由器实例

vue路由执行顺序
  1. 导航被触发 -> 在失活的组件里调用 beforeRouteLeave 守卫 -> 调用全局 beforeEach 前置守卫 -> 重用的组件调用 beforeRouteUpdate 守卫 -> 路由配置调用 beforeEnter-> 解析异步路由组件 -> 在被激活的组件里调用 beforeRouteEnter 守卫 -> 调用全局的 beforeResolve 守卫 -> 导航被确认 -> 调用全局的 afterEach-> 触发 DOM 更新 -> 调用  beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入

# Web

# DOM

# BOM

BOM中的各种位置

layer:最近相对定位父级元素
clint:浏览器可视窗口(不包括控制台、菜单、滚动条、工具栏)
offset:浏览器可视窗口(包括上面这些)
scroll:目前可视窗口 + 滚动条离顶部或左边隐藏的部分
page:document 对象
screen:屏幕

# Node

require 的模块加载机制
  1. 计算模块绝对路径
  2. 如果缓存中有该模块,则从缓存中取出该模块
  3. 按优先级依次寻找并编译执行模块,将模块推入缓存(require.cache)中
  4. 输出模块的 exports 属性
Node 更适合处理 I/O 密集型任务还是 CPU 密集型任务?为什么?

Node 更适合处理 I/O 密集型的任务。因为 Node 的 I/O 密集型任务可以异步调用,利用事件循环的处理能力,资源占用极少。Javascript 是单线程的原因,Node 不适合处理 CPU 密集型的任务,CPU 密集型的任务会导致 CPU 时间片不能释放,使得后续 I/O 无法发起,从而造成阻塞。

阅读次数

请我喝[茶]~( ̄▽ ̄)~*

EvilMoOd 微信支付

微信支付

EvilMoOd 支付宝

支付宝