电话

020-88888888

完美体育:【面试题】前端76道高频面试题汇总

标签: 2024-05-08 

每⼀个构造函数都有⼀个prototype属性,指向了它的原型

每⼀个实例有⼀个__proto__属性,这个属性指向了它的原型,原型上还有__proto__,指向了它⽗类的原型,⼀直到Object.prototype为⽌,所以Object的原型,是⼀个对象的终极原型,它的prototypenull

访问实例的⼀个属性,会从它的实例内部查找,若没有就到它的原型,还没有就继续向⽗⼀级原型查找,⼀直找到Object.prototype位置,没有就返回undefined



function Animal (name) {
  ...
}
Animal.prototype.eat = function () {...}

原型链继承:

假设构造函数B()需要继承构造函数A(),就可以通过将函数B()的显式原型指向⼀个函数A()的实例,然后再对B的显式原型进⾏扩展。那么通过函数B()创建的实例,既能访问函数B()的属性b,也能访问函数A()的属性a,从⽽实现了多层继承

function Cat (food) {...}
Cat.prototype = new Animal()
Cat.prototype.name = 'cat'

构造继承:

function Cat (name) {
  Animal.call(this)
  this.name = name
}

实例继承:

function Cat (name) {
  let instance = new Animal()
  instance.name = name
  return instance
}

ES6继承:

class 定义类,用 extends 继承类,用 super() 表示父类

a.__proto__.__proto__...=?=Array.prototype

instanceof:⽤于检测构造函数的prototype属性是否出现在某个实例对象的原型链上

例如在表达式left instanceof right中会沿着left的原型链查找看是否存在right的prototype对象

left.__proto__.__proto__...=?=right.prototype

typeof:用来获取一个值的类型

作域链:当查找变量的时候,会先从当前上下⽂的变量对象中查找,如果没有找到,就会从⽗级(词法层⾯上的⽗级)执⾏上下⽂的变量对象中查找,⼀直找到全局上下⽂的变量对象,也就是全局对象。这样由多个执⾏上下⽂的变量

  • 全局作用域:定义在 window 下的变量范围
  • 局部作用域:函数的内部定义的变量范围
  • 块级作用域:用 let 和 const 在任意代码块中定义的变量都认为是块级作用域

作⽤域链与原型链的区别:

  1. 作⽤域是对于变量⽽⾔,原型链是对于对象的属性
  2. 作⽤域链顶层是 window,原型链顶层是Object

闭包:通俗一点就是打通了一条在函数外部访问内部作用域的通道。正常情况下函数外部是访问不到内部作用域变量的

使用场景:

  • 封装组件
  • for 循环 + 定时器
  • for 循环 + dom 事件
  • 节流防抖

表象判断是不是闭包:函数嵌套函数,内部函数 return。内部函数调用外层函数的局部变量

优点:隔离作用域,不造成全局污染

缺点:闭包长期驻留内存,会导致内存泄漏

在面向对象的编程中,通常把用类创建对象的过程称为实例化,这个问题可以等同于:new 的过程发生了什么

var Model = function (p) {this.p = p }  var model = new Model(1)

Model = function(x){ console.log(x) }

console.log(model.constructor) //function (p){}     console.log(Model) //function (x){}

console.log(Model === model.constructor) //true

当 new 一个对象的时候,会自动执行这个对象的构造函数,就是这里的 Model,而且当 model 创建以后,它和它自己的构造函数就没有关系了,构造函数的改变并没有改变什么

function objectFactory() {
  // 取出第一个参数,就是我们要传入的构造函数
  // 此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数
  Constructor = [].shift.call(arguments) // 取得外部传入的构造器 即fn
  // var obj = Object.create(Constructor.prototype) // 也可以使用这种方式绑定原型
  // 用new Object() 的方式新建了一个对象 obj
  var obj = new Object()
  // 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性
  obj.__proto__ = Constructor.prototype
  // 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性
  var ret = Constructor.apply(obj, arguments) // 借用外部传入的构造器给obj设置属性
  return typeof ret === 'object' ? ret : obj // 确保构造器总是返回一个对象
}
  1. async:可选。表示应该立即下载脚本,但不应妨碍页面的其他操作,比如下载其他资源或等待加载其他脚本,只对外部脚本有效
  2. charset:可选。表示通过 src 属性指定代码的字符集,由于大多数浏览器会忽略它的值,所以不经常使用
  3. defer:可选。表示脚本可以延迟到文档完全被解析和显示之后再执行,只对外部脚本文件(src)有效
  4. language:已废弃
  5. src:可选。表示要执行代码的外部文件
  6. type:可选。可以看做 language 的替代属性,表示编写代码使用的脚本语言的内容类型(MIME类型),考虑到约定俗成和最大限度的浏览器兼容,目前 type 属性的值依旧是 text/javascript

改变一个对象属性值,另一个对象属性也会发生改变/不变

深拷贝:

深拷贝拷贝多层,每一级别的是数据都会拷贝(内部开辟了新的内存空间进行存储)

可以拆分为两步:浅拷贝 + 递归,所以循环赋值配合递归也能实现,除此之外:

for...in遍历并递归。(注意:如果使用Symbol值作为对象的属性名,这个属性通过for...in是无法拿到的,而且通过Objec.keys()方法也是获取不到Symbol属性名的,如果通过JSON.stringfy()去序列化对象为一个JSON字符串的话,Symbol属性也会被忽略掉)

因此我们还可以使用解构配合Object对象里面getOwnPropertySymbols()方法或者ES6的Reflect.ownKeys(obj)递归实现深拷贝

// 不考虑 symbol,考虑可新增查找遍历处理的逻辑

// 第一种
var deepCopy = function(obj) {
  if (typeof obj !== 'object') return
  var newObj = obj instanceof Array ? [] : {}
  for (var key in obj) {
      if (obj.hasOwnProperty(key)) {
          newObj[key] = typeof obj[key] === 'object'
            ? deepCopy(obj[key]) 
            : obj[key]
      }
  }
  return newObj
}

// 第二种
obj2 = JSON.parse(JSON.stringify(obj))

浅拷贝:

Object.assign() // ES6+

Array.from(arrayLike) // ES6+ 将一个类数组对象或可遍历对象转换为真正数组

...展开spread // ES6适合数组 ES2018适合对象

基本数据类型:Number、String、Boolean、undefined、Null、Symbol(ES6 新增)、BigInt(ES2020新增)

引用类型:Object, 包含 function、Array、Date

查找:getElementByid, getElementsByTagName, querySelector, querySelectorAll

插入:appendChild, insertBefore

删除:removeChild

克隆:cloneNode

设置和获取属性:setAttribute(“属性名”,”值”), getAttibute(“属性名”)

一是 html 事件处理程序:现在早已不用了,就是在 html 各种标签上直接添加事件,类似于css 的行内样式

<button onclick = ''> </button>

缺点:不好维护,因为散落在标签中,也就是耦合度太高

二是 DOM0 级事件处理程序:目前在 PC 端用的还是比较多的绑定事件方式,兼容性也好,主要是先获取 dom 元素,然后直接给 dom 元素添加事件

var btn=document.getElementById(‘id 元素’)

btn.onclick=function() {}

优点:兼容性好 缺点:只支持冒泡,不支持捕获

三是 DOM2 级事件处理程序:移动端用的比较多,也有很多优点,提供了专门的绑定和移除方法

var btn=document.getElementById(‘id 元素’)

绑定事件btn.addEventListener(‘click’,函数名,false)

移除事件btn.removeEventListener(‘click’,函数名,false)

优点:支持给个元素绑定多个相同事件,支持冒泡和捕获事件机制

JS 事件代理就是通过给父级元素(例如:ul)绑定事件,不给子级元素(例如:li)绑定事件,然后当点击子级元素时,通过事件冒泡机制在其绑定的父元素上触发事件处理函数

主要目的是为了提升性能,因为我不用给每个子级元素绑定事件,只给父级元素绑定一次就好了

在原生 js 里面是通过 event 对象的 targe 属性实现:

ul.onclick = function(event){ if( tevent.target.nodeName.toLowerCase()==’li’ ){事件逻辑} }

JQuery:$(“ul”).on(“click”,“li”,function(){事件逻辑}) 其中第二个参数指的是触发事件的具体目标

  1. 先创建 XHR 对象即 XMLHttpRequest()
  2. 然后 open 准备发送,open 中有三个参数:一是提交方式 getpost,二是接口地址,三是同步和异步
  3. send发送
  4. 在发送的过程中通过 onreadystatechange 来监听接收的回调函数,可以通过判断 readyState==4status==200 来判断是否成功返回,然后通过 responseText 接收成功返回的数据

同步:即 sync,形象的说就是代码一行行执行,前面代码和请求没有执行完,后面的代码和请求就不会被执行

缺点:容易导致代码阻塞 优点:程序员容易理解(因为代码从上往下一行行执行,强调顺序)

异步:即 async,形象的说就是代码可以在当前程序没有执行完,也可以执行后面的代码

缺点:程序员不易理解(因为不是按顺序执行的) 优点:可以解决代码阻塞问题,提升代码执行效率和性能

四种异步方法:(发展历程)

1.callback回调函数

缺点:回调地狱,不能用 try catch 捕获错误,不能 return

优点:解决了同步的问题(只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行)

2.Promise 实现了链式调用,也就是说每次 then 后返回的都是一个全新 Promise,如果我们在 then 中 return ,return 的结果会被 Promise.resolve() 包装

优点:解决了回调地狱的问题 缺点:无法取消 Promise ,错误需要通过回调函数来捕获

3.generator 可以控制函数的执行,可以配合 co 函数库使用

// 惰性执行 yield 配合 next()
function * xxx () {
  let id = 1
  while (true) {
    yield id++
  }
  const idMaker = xxx()
  idMaker.next().value
}

4.async 和 await 其实 await 就是 generator 加上 Promise的语法糖,且内部实现了自动执行 generator

async function () {
  const user = await ajax('/api/user.json')
}

优点:代码清晰,不用像 Promise 写一大堆 then 链,处理了回调地狱的问题

缺点:await 将异步代码改造成同步代码,如果多个异步操作没有依赖性而使用 await 会导致性能上的降低

Array.isArray(a)
a instanceof Array  
a.constructor === Array
OBject.prototype.toString.call(a) === '[object Array]'

IndexOf()/lastIndexOf():返回元素在数组第一次/最后一次出现的索引,从0开始。若不存在返回-1

slice(start, end):索引从start开始截取到索引end结束,没有参数复制整个数组

concat():合并数组,如果参数是数组会被拉平一次再加入到新数组

join():把当前Array的每个元素都用指定字符串连接起来,没有参数默认用,连接

相反 split():把字符串分割成字符串数组

toString():返回数组的字符串形式

valueOf():返回数组本身

map(function(elem, index, arr)):对数组的所有成员依次调用一个函数,返回值是一个新的数组。三个参数依次是当前成员、当前位置、数组本身

forEach():与map方法相似,也是遍历数组的所有成员,执行某种操作,一般没有返回值

for:遍历普通数组 for...in:遍历键值对 forEach:函数式遍历方法

filter():删选,返回过滤后的新数组

some():只要有一个数组成员的返回值为true,则整个some方法的返回值就是true,否则为false。空数组返回false

every():所有数组成员的返回值都是true才返回true,否则为false。空数组返回true

reduce()/reduceRight():依次处理数组的每个成员,最终累计为一个值

min : arr.reduce((min, num) => min<num ? min : num)

push():向数组的末尾添加若干元素,返回值是改变后的数组长度

pop():删除数组最后一个元素,返回值是删除的元素

unshift():向数组头部添加若干元素,返回值是改变后的数组长度

shift():删除数组第一个元素,返回值是删除的元素

sort():数组排序,默认将所有元素转换成字符串,再按字符串Unicode码点排序,返回值是新数组;如果元素都是数字,按从小到大排序,可以出传入一个回调函数作为参数

min : arr.sort( (a, b) => a-b[0]) 不到O(n)复杂度

reverse():颠倒数组中元素的位置

splice(start, deleteCount, item):stary 未开始的索引,deletecount 表示要移除的数组元素的个数,item 为要添加进数组的元素

from():ES6+ 将一个类数组对象或者可遍历对象转换为一个真正的数组(浅拷贝数组)

最简单循环加递归:循环中判断,如果子元素是数组则递归,不是则push到新数组

arr.toString().split(',')

arr.join(',').split(',')

reduce+concat循环递归

arr.reduce(function(init, item) { return init.concat(item是数组循环 不是返回) }, [])

some+扩展运算符...arr

some+Array.from(arr)

some+[].slice.call(arr)

arr.flat(展开多少层) ES2019

代码 1 执行fn()函数时,实际上就是通过对象 o 来调用的,所以 this 指向对象 o

var o = { fn () {   console.log(this)} }

o.fn() // o

代码 2 也是同样的道理,通过实例 a 来调用,this 指向类实例 a。

```js class A { fn () {console.log(this)} }

var a = new A() a.fn() // a ```

但是要注意的是,ES6 下的 class 内部默认采用的是严格模式,这类问题如果改变一下最后:

var fun = a.fn      fun() // 严格模式下不会指定全局对象为默认调用对象,所以答案是 undefined

代码 3 则可以看成是通过全局对象来调用,this 会指向全局对象(严格模式下指向 undefined)

function fn () { console.log(this) }

fn() // 浏览器:Window;Node.js:global

代码 4 forEach() 有两个参数,第一个参数是回调函数,第二个是 this 指向的对象,这里只传入了回调函数,第二个参数没有传入,默认为 undefined,所以 this 指向全局对象

var dx = { arr: [1] }

dx.arr.forEach(function() { console.log(this) }) // 浏览器:Window;Node.js:global

代码 5 ES6 新加入的箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this。可以简单地理解为箭头函数的 this 继承自上层的 this,但在全局环境下定义仍会指向全局对象。

var arrow = {fn: () => { console.log(this) }}

arrow.fn() // 浏览器:Window;Node.js:global

代码 6 改变 this 指向的常见 3 种方式有 bind、call 和 apply。callapply 用法功能基本类似,都是通过传入 this 指向的对象以及参数来调用函数。区别在于传参方式,前者为逐个参数传递,后者将参数放入一个数组,以数组的形式传递。bind 有些特殊,它不但可以绑定 this 指向也可以绑定函数参数并返回一个新的函数,当调用新的函数时,绑定之后的 this 或参数将无法再被改变。

function getName() {console.log(this.name)}

var b = getName.bind({name: 'bind'})        b()

getName.call({name: 'call'})

getName.apply({name: 'apply'})

由于 this 指向的不确定性,所以很容易在调用时发生意想不到的情况。在编写代码时,应尽量避免使用 this,比如可以写成纯函数的形式,也可以通过参数来传递上下文对象。实在要使用 this 的话,可以考虑使用 bind 等方式将其绑定

箭头函数中没有 this 的机制,不会改变 this 的指向;它的 this 是继承而来的,默认指向宿主对象,而不是执行时的对象

箭头函数和普通函数相比:

  • 不绑定 arguments 对象,也就是说在箭头函数内访问 arguments 对象会报错
  • 不能用作构造器,也就是说不能通过关键字 new 来创建实例
  • 默认不会创建 prototype 原型属性
  • 不能用作 Generator() 函数,不能使用 yeild 关键字
  • var 定义的变量,没有块的概念,可以跨块访问,不能跨函数访问
  • let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问
  • const 定义的常量,使用时必须初始化(赋值),只能在块作用域里访问,而且不能修改
  • let 与 const 是块作用域,在整个大括号{}内可见
  • let 与 const 在变量声明之前就访问变量的话,会直接提示 ReferenceError,而不像 var 使用默认值 undefined
  • let 与 const 存在暂时性死区,只要块级作用域有 let 或者 const 命令,它所声明的变量就 绑定 这个区域,不再受外部的影响,在 let const 变量被赋值前不可读写
  • let 与 const 不允许重复声明
  • const 定义的变量的引用不能够被更改

const 声明的成员不可修改,只是说我们不允许在声明过后重新去指向一个新的内存地址,并不是不允许修改恒量中的属性成员

const obj = {} // 恒量只是要求内层指向不允许被修改

obj.name = 'sgh' // 对于数据成员的修改是没有问题的

这个时候并没有去修改 obj 指向的内存地址,只是修改了这块内存空间的数据,相反如果我们是将 obj 等于一个新的空对象,就会报错

// 构建一个代理对象 第一个参数:需要代理的目标对象
const personProxy = new Proxy(person, {
  // 监视属性读取
  get (target, property) {} // 代理的目标对象 外部访问的属性名

  // 监视属性设置
  set (target, property, value) {} // 代理的目标对象 外部访问的属性名 写入的属性名
})
  1. Proxy 更强大。Object.defineProperty() 只能监视属性的读写,Proxy 能监视到更多对象操作,如 deleteProperty、has等等
  2. Proxy对数组对象的监视支持更好。以往想通过 Object.defineProperty() 去监视数组操作,最常见的一种方式时通过 重写数组 的操作方法(通过自定义的方法 去覆盖掉 数组原先对象的push、shift等方法 以此去 劫持对应这个方法调用的过程)但 Proxy 内部会自动根据 push 等操作去推算出它应该所处的下标
  3. Proxy 是以非侵入的方式监管了对象的读写。也就是说已经定义好的对象,不需要对对象本身做任何操作 就可以监视到内部成员的读写,而 Object.defineproperty() 就要求我们必须通过 特定方式单独定义对象中需要被监视的属性

赋值取值操作。通常有两种手段可以对 object 对象存、取值:1. 在对象初始化时、2. 在对象初始化后

// 对象初始化时
var o = {
  key: 0,
  get getkey () { return this.key }
  set setkey (value) { this.key = value }
}

// 对象初始化后
var o = { key: 0 }
o.prototype.__defineGetter__('key', function() { return this.key })
o.prototype.__defineSetter__('key', function(value) { this.key = value })

ES2015新增特性常用的主要有:

  • let/const
  • 箭头函数
  • 模板字符串:1.字面量:可换行,可通过${}插入表达式。2.标签数组:在模板字符串前添加一个标签,那个标签就是一个特殊函数,会调用这个函数,返回数组
  • 解构赋值
  • 模块的导入import和导出export default/export
  • Promise
  • 还有一些字符串的新方法:includes() startWith() endsWith()
  • 剩余参数...args、展开数组...arr:用于数组
  • 对象字面量增强:属性名变量名相同可省略、方法省略function、计算属性名[任意表达式]:123,
  • 一些数组新增方法:Object.assign浅拷贝
  • Object.is+0 -0 不同,NaN NaN相同
  • Proxy、Reflect:Proxy的默认实现 return target[property] / Reflect.get(target, property)。Reflect 统一的对象操作API,静态类
  • Class类、static、extends
  • 新增两种数据结构Set(成员不允许重复)和Map(键值对集合get、has、delete、clear)
  • Symbol(为对象添加独一无二属性名getOwnPropertySymbols()获取)、
  • for...of循环:可以 break,伪数组可用,普通对象不行(挂载 Interble)
  • 可迭代接口Iterble、Generator

ES2016

  • Array.prototype.includes:能够查找NaN
  • 指数运算符 Math.pow(2, 10) -> 2 ** 10

ES2017

  • Object.values:ES2015的keys()方法类似
  • Object.entries:以数组的形式返回对象当中所有的键值对
  • Object.getOwnPropertyDescriptors:自从ES5过后,我们就可以为对象去定义getter、setter属性,但它们是不能通过Object.assign()方法去完全复制的 const descriptors = Object.getOwnPropertyDescriptors(p1) const p2 = Object.defineProperties({}, descriptors)
  • 新增字符串填充方法String.prototype.padStart / padEnd -> html------------|005
  • 允许在函数参数中添加尾逗号

ES2018

  • Async/Await
  • Promise.finally():一个 finally() 就等价于一组回调函数相同的 then() 和 catch()
  • 剩余参数Rest/展开数组Spread:开始适用于对象

ES2019

  • String.prototype.trimStart() / trimEnd():来头和尾进行单独控制空格文本去掉。别名trimLeft()、trimRight()
  • Object.fromEntries():用于把键值对还原成对象结构。(与ES2017 Object.entries 对应)、try...catch
  • Array.prototype.flat() / Array.prototype.flatMap():flat()每次只能展开一层,想要展开多少层,可传参数[1, 2, [3, 4, [5, 6]]].flat(2);// [ 1, 2, 3, 4, 5, 6 ]
  1. TypeScript是JavaScript的一个超集,包含了JavaScript的所有元素,提供了类型系统和对ES6+的支持
  2. TypeScript 可以使用 JavaScript 中的所有代码和编码概念,TypeScript 是为了使 JavaScript 的开发变得更加容易而创建的
  3. TypeScript是JavaScript的类型超集,可以编译成纯JavaScript。编译出来的JavaScript可以运行在任何浏览器上,TypeScript编译工具可以运行在任何服务器和任何系统上

优点:

1.TypeScript非常包容 TypeScript是JavaScript的超集,.js文件直接重命名为.ts即可。即使不显式的定义类型也能够自动做出类型推断。可以定义从简单到复杂的几乎一切的类型。即使TypeScript编译报错,也可以生成JavaScript文件。TypeScript是开源的

2.更好的协作 类型安全是一种在编码期间检测错误的功能,而不是在编译项目时检测错误,这为开发团队创建了一个更高效的编码和调试过程

3.更强的生产力 干净的 ECMAScript 6 代码,自动完成和动态输入等因素有助于提高开发人员的工作效率。增强了编辑器和IDE的功能,包括代码补全、接口提示、跳转到定义、重构等

缺点:

1.增加学习成本 语言本身多了很多概念,例如接口(Interfaces)、泛型(Generics)、类(Classer)、枚举类型(Enums)等非前端概念,学习成本增加

2.项目初期会增加一些成本 毕竟要多写一些类型的定义,开发成本的增加是不可避免的。不过对于一些需要长期维护的项目,TypeScript能够减少其维护成本

3.可能和一些库结合的不是很完美 在实际开发中需要使用到第三方npm模块,而这些模块不一定是通过TypeScript编写的,所以它提供的成员就不会有强类型体验。但是目前越来越多的模块已经在内部集成类型声明文件了

字符串类型参数的函数 function add (a: string, b: string): string

数组类型参数的函数 function add (a: number, b: number): number

函数定义 function add (a: any, b: any): any { return a + b }

工程化:遵循一定的标准和规范,通过工具提高效率,降低成本的一种手段

  1. 开发使用 ES6+,发布前编译
  2. 使用 Less/Sass 等预编译语言
  3. 使用或封装模块,提高代码的复用性
  4. 检验代码质量,统一风格
  5. 自动压缩代码和资源文件,自动发布
  6. 解决依赖后端问题:如接口,模板引擎,本地运行,热更新调试等

六个核心概念:

Entry:入口,这是Webpack执行构建的第一步,可理解为输入。

Module:模块,在Webpack中一切皆模块,一个模块即为一个文件。Webpack会从Entry开始递归找出所有的依赖模块。

Chunk:代码块,一个Chunk由多个模块组合而成,它用于代码合并与分割。

Loader:模块转换器,用于将模块的原内容按照需求转换成新内容。

Plugin:扩展插件,在Webpack构建过程的特定时机注入扩展逻辑,用来改变或优化构建结果。

Output:输出结果,源码在Webpack中经过一系列处理后而得出的最终结果。

构建过程:

Webpack 在启动后,会从 Entry 开始,递归解析 Entry 依赖的所有 Module,每找到一个 Module,就会根据 Module.rules 里配置的 Loader 规则进行相应的转换处理;

对 Module 进行转换后,再解析出当前 Module 依赖的 Module ,得到了每个 Module 被转换/翻译后的最终内容以及它们之间的依赖关系;

这些 Module 会以 Entry 为单位进行分组,即为一个 Chunk。(因此一个 Chunk,就是一个 Entry 及其所有依赖的 Module 合并的结果)再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;

在确定好输出内容后,根据配置确定输出的路径和文件名,将所有的 Chunk 转换成文件输出 Output。

在整个构建流程中,Webpack 会在恰当的时机执行 Plugin 里定义的逻辑,从而完成 Plugin 插件的优化任务。

  1. 一切源代码文件均可通过各种 Loader 转换为 JS 模块(默认只支持js文件),模块之间可以互相引用
  2. webpack 通过入口递归处理各模块引用关系,最后输出为一个或多个产物包文件(bundle)
  3. 每一个入口都是一个块组(chunk group),在不考虑分包的情况下一个块组中只有一个代码块,这个 chunk 包含递归后的所有模块,都有对立打包后的输出文件(bundle)

全局依赖:

1.安装项目依赖

2.引入 Webpack, webpack-cli

common.js - module:

1.将Vue组件编译成普通的JavaScript模块vue-template-compiler vue-loader

2.需要Webpack在打包过程中同时处理其他ES6特性的转换 babel-loader @babel/core @babel/preset-env

3.引入资源模块加载处理CSS文件 css-loade vue-style-loader

4.处理 .less 文件的资源模块加载less less-loader style-loader

5.处理 图片资源模块file-load url-loader

6.ESLint 结合Webpack eslint eslint-loader eslint-friendly-formatter 创建 .eslintrc 文件并配置

common.js - plugins:

1.通过 Webpack 输出 HTML 文件 html-webpack-plugin

开发 dev.js:

1.希望在公共配置原有基础上添加插件而不是覆盖 webpack-merge

2.提供一个开发服务器 webpack-dev-server 集成 自动编译 和 自动刷新浏览器(热更新),打包结果暂存到内存中(减少磁盘读写)

3.Source Map:调试时将产物代码显示回源代码

4.热更新HMR:解决自动刷新导致页面状态丢失

package.jsonserve 定义 webpack-dev-server --config webpack.dev.js

生产 prod.js:

1.打包之前自动清除目录 clean-webpack-plugin

2.拷贝静态文件至输出目录 copy-webpack-plugin

3.Define Plugin:为代码注入全局成员,可以以此判断运行环境,如 process.env.NODE.ENV

4.Tree Shaking:可以自动检测出代码中未引用的代码并移除掉(Side Effects:允许通过配置的方式去标识代码中是否有副作用,从而为 Tree Shaking 提供更大的压缩空间)

5.Code Splitting:代码分割,防止 bundle 体积过大浪费流量和带宽,将模板打包到不同的 bundle 提高应用响应速度

package.jsonbuild 定义 webpack --config webpack.prod.js

Loader:专注实现资源模块的转换和加载(编译转换代码、文件操作、代码检查)

Plugin:解决其他自动化工作(打包之前清除 dist 目录、拷贝静态文件、压缩代码等等)

vue-cli-service 是包含 webpack 等工具的黑盒

webpack.common.js 公共环境配置:

module.exports = {
  entry: 'https://zhuanlan.zhihu.com/p/src/main.js', // 输入
  output: {
    filename: 'bundle.js', // 输出
    // path: path.join(__dirname, 'output') // 输出目录(绝对路径 通过path转换)
  },
  module: { // 用于配置加载器
    rules: [
      {
        test: /\.vue$/, // 匹配打包过程遇到的文件路径
        use: [ // 指定匹配到的文件使用到loader
          // 当我们配置多个loader 执行顺序从下至上
          'vue-loader' // 将Vue组件编译成普通的JavaScript模块
        ]
      },
      {
        test: /.js$/, // 它会应用到普通的 `.js` 文件以及 `.vue` 文件中的 `<script>` 块
        use: {
          loader: 'babel-loader',
          // webpack 仅仅是对模块完成打包工作,所以它才会对代码中的 import 和 export 做一些
          // 相应的转换,除此之外并不能去转换代码中的其他 ES6 特性
          // babel 只是转换JS代码的平台
          //需要基于不同插件转换代码中的具体特性 @babel/plugin-transform-modules-commonjs
          options: {
           presets: ['@babel/preset-env'] // 使用preset-env插件集合
          }
        }
      },
      {
        test: /\.css$/, // 它会应用到普通的 `.css` 文件以及 `.vue` 文件中的 `<style>` 块
        use: [
          'vue-style-loader', 
          'css-loader' // ①编译转化类 Loader
        ]
      },
      // 在样式文件中导入资源模块的两种常见方法:
      // 1)main.css
      // background-image: url(background.png)
      // 2)import指令加载其他样式资源模块
      // @import url(reset.css)
      // @import 'reser.css' // css-loader 支持sass/less风格的@import指令
      {
        test: /\.less$/, // 配置 less-loader ,应用到 .less 文件,转换成 css 代码
        use: [
          'style-loader', // creates style nodes from JS strings
          'css-loader', // translates CSS into CommonJS
          'less-loader' // compiles Less to CSS
        ]
      },
      {
        test: /\.(png|jpe?g|gif)$/,
        use: {
          // 文件资源加载器:通过拷贝物理文件的形式去处理文件资源
          // loader: 'file-loader', // ②文件操作类 Loader
          // 通过 DataUrls 形式去表示文件 (这种URL当中的文本已经包含了文件内容
          // 不用发送 http 请求,图片/字体 以base64编码后的结果表示)
          loader: 'url-loader',
          // 小文件使用 Data Urls,减少请求次数;大文件单独存放,提高加载速度
          options: {
            // 小于10kb的文件转化为Data URLs 嵌入代码中
            limit: 10 * 1024, // 超出10kb的文件单独提取存放
            name: 'img/[name].[ext]'
          }
        }
      },
      {
        test: /\.(js|vue)$/, // 配置 eslint-loader 检查代码规范,应用到 .js 和 .vue 文件
        use: {
          loader: 'eslint-loader', // ③代码检查类 Loader
          options: {
            formatter: require('eslint-friendly-formatter') // 默认的错误提示方式
          }
        },
        enforce: 'pre', // 编译前检查
        exclude: /node_modules/, // 不检查的文件
        include: [__dirname + '/src'], // 要检查的目录
      },
  ]},
  plugins: [ // 用于配置插件
    new webpack.DefinePlugin({ // 为代码注入全局成员
      // 值要求的是一个代码片段
      BASE_URL: JSON.stringify('')
    }),
    new VueLoaderPlugin(), // 配合 vue-loader 使用,用于编译转换 .vue 文件
    new HtmlWebpackPlugin({ // 用于生成 index.html 文件
      title: 'sgh vue project', // 设置html标题
      // meta: { // 设置对象中的元数据标签
      //   viewport: 'width=device-width'
      // },
      template: 'https://zhuanlan.zhihu.com/p/public/index.html' // 使用的模板地址
    })
  ]}
}

webpack.dev.js 开发环境配置:

const webpack = require('webpack')
const merge = require('webpack-merge') // 使用merge方法合并配置
const common = require('https://zhuanlan.zhihu.com/p/webpack.common') // 导入公共配置

// webpack4 不同模式预设配置
module.exports = merge(common, {
  mode: 'development', // 开发依赖
  // cheap-eval-module-source-map 能定位到原文件名,且显示内容为经过babel-loader转换后的代码
  // 点击调试能定位到行(缺少列信息),只能跳转到行首
  devtool: 'cheap-eval-module-source-map', // 开启 source-map
  devServer: {
    host: 'localhost',
    port: '2020',
    open: true, // 启动服务时,自动打开浏览器
    hot: true, // 开启热更新功能
    contentBase: 'public' // 额外指明静态资源目录
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin() // HMR热更新
  ]
})

webpack.prod.js 生产环境配置:

const common = require('https://zhuanlan.zhihu.com/p/webpack.common') // 导入公共配置
const merge = require('webpack-merge') // 使用merge方法合并配置
const { CleanWebpackPlugin } = require('clean-webpack-plugin') // 打包之前清除 dist 目录
const CopyWebpackPlugin = require('copy-webpack-plugin') // 拷贝静态文件至输出目录

module.exports = merge(common, {
  mode: 'none',
  output: {
    filename: 'js/bundle-[contenthash:8].js' // 文件级别的 不同的文件就有不同的hash值 指定8位
  },
  plugins: [
    new CleanWebpackPlugin(), // 自动清除目录
    // 项目中一些不需要参加构建的静态文件,webpack在打包时一并将它们复制到输出目录
    new CopyWebpackPlugin(['public']) // 通配符或'目录'
  ]
})







我们需要请求的接口地址没有/api。weboack-dev-server支持通过配置的方式添加代理服务

目标:将GitHub API 代理到开发服务器

...

module.exports = {
 ...
  devServer: {
    ...
    proxy: { // 添加代理服务配置
      '/api': { // 需要被代理的请求路径前缀
        // http://localhost:8080/api/users -> https://api.github.com/api/users
        target: 'https://api.github.com', // 代理目标
        // 但是我们需要请求的接口地址没有/api
        // http://localhost:8080/api/users -> https://api.github.com/users
        pathRewrite: { // 代理路径重写
          '^/api': '' // 以api开头的替换为空
        },
        // 不能使用 localhost:8080 作为请求 GitHub 的主机名
        changeOrigin: true // 以实际代理请求的主机名请求
      }
    }
  },
  ...
}

过程有点类似一个工作管道,可以在这个过程一次使用多个 Loader

module.exports = source => { return }
  1. 可以直接在项目根目录新建 test-loader.js (完成后也可以发布到 npm 作为独立模块使用)
  2. 这个文件需要导出一个函数,这个函数就是我们的 loader 对所加载到的资源的处理过程
  3. 函数输入加载到的资源输出加工后的结果
  4. 输出结果可以有两种形式:第一,输出标准的 JS 代码,让打包结果的代码能正常执行;第二,输出处理结果,交给下一个 loader 进一步处理成 JS 代码
  5. 在 webpack.config.js 中使用 loader,配置 module.rules ,其中 use 除了可以使用模块名称,也可以使用模块路径rules: [{test: '', use: ['名称', '路径']}]

相比 Loader.plugin 拥有更宽的能力范围:Loader 只是加载模块的环节工作,而插件的作用范围几乎触及到 Webpack 工作的每一个环节

Class MyPlugin {
  apply(compiler) {
    compiler.hooks.emit.tap(名称, compilation => {函数内容})
  }
}
  1. Plugin 通过钩子机制实现(类似于事件),为了便于插件的扩展,Webpack 几乎给每一个环节都埋下了一个钩子,我们在去开发插件时就可以通过往这些不同的节点上挂载不同的任务。
  2. Webpack 要求每一个插件必须是一个函数或者是一个包含 apply 方法的对象。一般把这个插件定义为一个类型,然后在这个类型中定义一个 apply 方法。使用就是通过这个类型构建一个实例去使用
  3. apply 方法接收一个 compiler 参数,包含了这次构建的所有配置信息,通过这个对象注册钩子函数
  4. 通过 compiler.hooks.emit.tap 注册钩子函数(emit也可以为其他事件),钩子函数第一个参数为插件名称,第二个参数 compilation 为此次打包的上下文,根据 compilation.assets 就可以拿到此次打包的资源,做一些相应的逻辑处理

三者都是前端构建工具,grunt和gulp在早期比较流行,现在webpack相对来说比较主流,不过一些轻量化的任务还是会用gulp来处理,比如单独打包CSS文件等

grunt 和 gulp 是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程

webpack是基于入口的。webpack会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能

1.组件化:就是可以将页面和页面中可复用的元素都看做成组件,写页面的过程,就是写组件,然后页面是由这些组件“拼接“起来的组件树

2.数据驱动:就是让我们只关注数据层,只要数据变化,页面(即视图层)会自动更新,至于如何操作 dom,完全交由 vue 去完成,咱们只关注数据,数据变了,页面自动同步变化了,很方便

JQuery:玩 dom 操作的神器,强大的选择器,分装了好多好用的 dom 操作方法 和 ajax方法

Vue:主要是数据驱动和组件化,很少操作dom(可以用ref)

v-if:根据表达式的值的真假条件渲染元素。在切换时元素及它的数据绑定/组件被销毁并重建。

v-show:根据表达式之真假值,切换元素的 display CSS 属性。

v-for:循环指令,基于一个数组或者对象渲染一个列表,vue 2.0 以上必须需配合 key 值使用。

v-bind:动态地绑定一个或多个特性/属性,或一个组件 prop 到表达式。

v-on:用于监听指定元素的 DOM 事件,比如点击事件。绑定事件监听器。

v-model:实现表单输入和应用状态之间的双向绑定

v-pre:跳过这个元素和它子元素的编译过程。用来显示原始Mustache标签。跳过大量没有指令的节点会加快编译。

v-once:只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。

  1. 如果同时出现,每次渲染都会先执行循环在判断条件,无法避免循环,浪费了性能
  2. 在外层嵌套<tepmlate>,在这一层进行v-if然后在内部进行v-for循环
  3. 如果条件出现在循环内部,可通过计算属性提前过滤不需要显示的项

1.全局自定义指令:Vue.directive(‘指令名’, { inserted(el) { } })

2.局部自定义指令:directives:{ }

修饰符用途:

  1. 通过自定义属性存储变量,避免暴露数据
  2. 防治污染 HTML 结构

v-on 指令常用修饰符<@>

.stop - 调用 event.stopPropagation(),禁止事件冒泡。

.prevent - 调用 event.preventDefault(),阻止事件默认行为。

.capture - 添加事件侦听器时使用 capture 模式。

.self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。

.{keyCode | keyAlias} - 只当事件是从特定键触发时才触发回调。

.native - 监听组件根元素的原生事件。

.once - 只触发一次回调。

v-bind 指令常用修饰符<:>

.prop - 被用于绑定 DOM 属性 (property)。(差别在哪里?看46)

.camel - (2.1.0+) 将 kebab-case 特性名转换为 camelCase. (从 2.1.0 开始支持)

.sync (2.3.0+) 语法糖,会扩展成一个更新父组件绑定值的 v-on 侦听器。

v-model 指令常用修饰符:

.lazy - 取代 input 监听 change 事件

.number - 输入字符串转为数字

.trim - 输入首尾空格过滤

v-bind 默认绑定到 DOM 节点的 attribute 上。使用 .prop修饰后,会绑定到 proterty

使用 property 获取最新的值
attribute 设置的自定义属性会在渲染后的 HTML 标签显示,property 不会

.prop<input :data = 'inputData'>

标签结构:<input :data = 'inputData的值'>

input.data === undefined
input.attributes.data === this.inputData

.prop<input :data.prop = 'inputData'>

标签结构:<input>

input.data === this.inputData
input.attributes.data === undefined
  1. Methods: 中都是封装好的函数,无论是否有变化只要触发就会执行
  2. Computed: 是vue独有的特性计算属性,可以对data中的依赖项再重新计算, 得到一个新值,应用到视图中,和 methods 本质区别是 computed 是可缓存的, 也就是说 computed 中的依赖项没有变化,则 computed 中的值就不会重新计算, 而 methods 中的函数是没有缓存的。
  3. Watch: 是监听 data 和计算属性中的新旧变化。 computed 和 watch 都是观察页面的数据变化的。数据变化时执行异步操作或开销较大的操作,这个时候使用 watch 是合适的

单向数据流主要是 vue 组件间传递数据是单向的,数据总是从父组件传递给子组件,子组件在其内部维护自己的数据,但它没有权限修改父组件传递给它的数据,当尝试这样做的时候vue 将会报错。这样做是为了组件间更好的维护。

在开发中可能有多个子组件依赖于父组件的某个数据,假如子组件可以修改父组件数据的话,一个子组件变化会引发所有依赖这个数据的子组件发生变化,所以vue 不推荐子组件修改父组件的数据

第一种:利用 H5 的 history API 实现:主要通过 history.pushStatehistory.replaceState 来实现,不同之处在于,pushState 会增加一条新的历史记录,而 replaceState则会替换当前的历史记录[发布项目时,需要配置下 apache]

this.$router.push(location, onComplete?, onAbort?) 点击 <router-link :to="..."> 等同于调用 router.push(...)

this.$router.replace(...) 点击 <router-link :to="..." replace> 等同于调用 router.replace(...)

第二种:利用 url 的 hash(#)实现:主要利用监听哈希值的变化来触发事件hashchange 事件来做页面局部更新

Vue 路由守卫(钩子):跳转过程中拦截当前路由和要跳转路由的信息,处理页面访问权限

全局:beforeEach (to, from, next)

组件内:beforeRouteEnter(to, from, next)

在同构渲染下,页面的拦截从服务端角度是在进入页面前就需要处理页面访问的,可以使用 路由中间件 处理,它允许定义一个自定义函数运行在一个页面或一组页面渲染前。既处理服务端渲染的路由拦截又处理客户端渲染的拦截

/**
 * 验证是否登录的中间件
 */

// 每一个中间件应放置在 middleware 目录,文件名的名称即为中间件名
export default function ({ store, redirect }) {
  // If the user is not authenticated
  if (!store.state.user) {
    return redirect('/login')
  }
}

// 使用
export default {
  ...
  // 在路由匹配组件渲染之前会先执行中间件处理
  middleware: ['authenticated']
}

跟创建自定义指令类似,也有全局和局部过滤器的形式

全局过滤器:Vue.filter(‘过滤器名’,function(参数 1,参数 2,…) { ... return 要返回的数据格式 })

局部过滤器:在组件内部添加 filters 属性来定义过滤器

fitlers:{ 过滤器名(参数 1,参数 2,,…参数 n) { ... return 要返回的数据格式 } }

第一种:父传子:主要通过 props 来实现

父组件通过 import 引入子组件,并注册( components:{} ),在子组件标签上添加要传递的属性,子组件通过 props 接收,接收有两种形式: 一是通过数组形式[‘要接收的属性’ ],二是通过对象形式{ }来接收,对象形式可以设置要传递的数据类型和默认值,而数组只是简单的接收

第二种:子传父:主要通过 $emit 来实现

子组件通过通过绑定事件触发函数,在其中设置this.$emit(‘要派发的自定义事件’,要传递的值),然后父组件中,在这个子组件身上@(v-on)派发的自定义事件,绑定事件触发的methods 中的方法接受的默认值,就是传递过来的参数,或者直接使用 @event 接收参数

第三种:兄弟之间传值有两种方法:

方法一:通过 event bus 实现

创建一个空的 vue 作为事件总线/事件中心 并暴露出去,这个作为公共的 bus,即当作两个组件的桥梁,在两个兄弟组件中分别引入刚才创建的 bus,在组件 A 中通过bus.$emit(’自定义事件名’,要发送的值)发送数据,在组件 B 中通过 bus.$on(‘自定义事件名‘,function(v) { //v 即为要接收的值 })接收数据

方法二:通过 vuex 实现

vuex 是一个状态管理工具,主要解决大中型复杂项目的数据共享问题,主要包括 state,actions,mutations,getters 和 modules 5 个要素,主要流程:组件通过 dispatch 到 actions,actions 是异步操作,再 actions中通过 commit 到 mutations,mutations 再通过逻辑操作改变 state,从而同步到组件,更新其数据状态.而 getters 相当于组件的计算属性对,组件中获取到的数据做提前处理的.再说到辅助函数的作用.

第四种:通过 ref 获取子组件

把它作用到普通 HTML 标签上,则获取到的是 DOM 对象

把它作用到组件标签上,则获取到的是组件实例对象

在使用子组件的时候,添加 ref 属性,然后在父组件等渲染完毕后使用 $refs 访问($refs 只会在组件渲染完成之后生效,并且它们不是响应式的)

  1. 合理使用 v-ifv-showv-for 遍历为 item 添加 key,避免同时使用 v-if
  2. 区分 computed 和 watch 的使用
  3. addEventListner添加的事件在组件销毁时用removeEventListner移除监听
  4. 路由懒加载 const test = () => import(‘@/...’))
  5. 开启 Gzip 压缩 config index.js module.exports = {...:{productionGzip: false}}
  6. 使用 webpack 的 externals 属性把不需要打包的库文件分离出去,减少打包后文件的大小,优化 Source Map
  7. 使用 vue 的服务端渲染(SSR),首屏快,SEO好
  8. 网络加载方面的优化:减少http请求(合并文件、CSS雪碧图、图片路由 懒加载),减少dom操作(dom 缓存),代码封装
  9. 第三方库用 cdn 加载(OSS优化),按需引入

一份代码,服务端先通过 server-side-rendering 生成 html 以及初始化数据(客户端SPA),客户端拿到代码后,通过对 html 的 dom 进行 path 和事件绑定对 dom 进行客户端激活。接管服务端渲染的内容把它激活为一个动态页面

  1. 客户端发起请求
  2. 服务端渲染首屏内容 + 生成客户端 SPA 相关资源
  3. 服务端将生成的首屏资源发送给客户端
  4. 客户端直接展示服务端渲染好的首屏内容
  5. 首屏中的 SPA 相关资源执行之后会激活客户端 Vue
  6. 之后客户端所有的交互都由客户端 SPA 处理

Vue.js 2.x 中响应式系统的核心 defineProperty

初始化时遍历 data 中的所有成员,通过 defineProperty 把对象的属性转换成 getter 和 setter,如果 data 中的属性又是对象的话,需要递归处理每一个子对象的属性。这些都是初始化时进行的,如果你、未使用这些属性也会进行响应式的处理完美体育

Vue.js 3.x 中使用 Proxy 对象重写响应式系统

1.Proxy 的性能本身就比 defineProperty 好,且代理对象可以拦截属性的访问、赋值、删除等操作,不需要初始化时遍历所有的属性,如果有多层属性嵌套只有访问某个属性时才会递归处理下一级属性

2.使用 Proxy 对象默认可以监听动态新增的属性,而 Vue.js 2.x 想要动态添加响应式属性需要调用 Vue.set 方法来处理

3.Vue.js 2.x 监听不到属性的删除

4.Vue.js 2.x 对数组的索引和 length 属性也监听不到

Vue 响应式原理通过数据劫持结合 发布订阅者模式来实现

观察者模式:

由具体目标调度:比如当事件触发,Dep就会调用观察者的方法(订阅者和发布者之间是存在依赖的)

发布订阅模式:

由统一调度中心调用,发布者和订阅者不需要知道对方的存在



在 new Vue的时候:在Observer中通过Object.defineProperty()达到数据劫持,代理所有数据的gettersetter属性,在每次触发setter的时候会通过Dep来通知Watcher,Watcher作为Observer数据监听器与Compiler模板解析器之间的桥梁。当Observer监听到数据发生改变时,通过Updater来通知Compiler更新视图,而Compiler通过Watcher订阅对应数据,绑定更新函数,通过Dep来添加订阅者

当创建好 Vue 实例后,新增一个成员,此时 data 并没有定义该成员,data 中的成员是在创建 Vue 对象的时候 new Observer 来将其设置成响应式数据,当 Vue 实例化完成之后,再添加一个成员,此时仅仅是给 vm 上增加了一个js属性而已,因此并不是响应式的

可以使用 Vue.set(object, propertyName, value)方法向嵌套对象添加响应式属性。还可以使用vm.$set实例方法,这也是全局Vue.set方法的别名。

原理:defineReactive(ob.value, key, val) 给新加的属性添加依赖,以后再直接修改这个新的属性的时候就会触发页面渲染。ob.dep.notify() 触发当前的依赖(这里的依赖依然可以理解成渲染函数),所以页面就会进行重新渲染

Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom

虚拟DOM是如何实现的:虚拟DOM是通过js语法来在内存中维护一个通过数据结构描述出来的一个模拟DOM树,当数据发生改变的时候,会先对虚拟DOM进行模拟修改,然后再通过新的虚拟DOM树与旧的虚拟DOM树来对比,而这个对比就是通过diff算法来进行的.虚拟DOM最大的意义不在于性能的提升(JavaScript对象比DOM对象性能高),而在于抽象了DOM的具体实现(对DOM进行了一层抽象)

  1. 首先对根元素进行对比,如果根元素发生改变就直接对根元素替换
  2. 如果根元素没有发生改变的话,再对下一层元素进行对比,如果对比发现元素发生删除,就执行删除,发现元素被替换就执行替换,发现添加了新的元素就执行添加
  3. 对比的同时,会通过key值来判断元素是否发生改变,判断元素是仅仅位置发生改变还是需要整个替换或删除
  4. 如果不是元素发生改变的话,再对内容进行对比,如果是内容发生改变的话,就直接修改内容
  5. 其实就是进行逐层对比,再通过不同的对比来判断执行不同的操作

虚拟DOM最大的意义不在于性能的提升(JavaScript对象比DOM对象性能高),而在于抽象了DOM的具体实现(对DOM进行了一层抽象)

作用:追踪列表中哪些元素被添加、被修改、被移除的辅助标志。可以快速对比两个虚拟DOM对象,找到虚拟DOM对象被修改的元素,然后仅仅替换掉被修改的元素,然后再生成新的真实DOM

好处:可以优化 DOM 的操作,减少Diff算法和渲染所需要的时间,提升性能

Vue 更新 DOM 是异步执行的、批量的,nextTick 就是在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM

例如:有一个 div,默认用 v-if 将它隐藏,点击一个按钮后,改变 v-if 的值,让它显示出来,同时拿到这个 div 的文本内容。如果 v-if 的值是 false,直接去获取 div 内容是获取不到的,因为此时 div 还没有被创建出来,那么应该在点击按钮后,改变 v-if 的值为 true,div 才会被创建,此时再去获取,

Vue 在观察到数据变化时并不是直接更新 DOM,而是开启一个队列,并缓冲在同一个事件循环中发生的所有数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和 DOM 操作。然后,在下一个事件循环 tick 中,Vue 刷新队列并执行实际(已去重的)工作。Vue 会根据当前浏览器环境优先使用原生的 Promise.thenMutationObserver,如果都不支持,就会采用 setImmediate(宏任务队列) setTimeout 代替

主要用于保留组件状态或避免重新渲染。属性:

include:字符串或正则表达式。只有匹配的组件会被缓存。

exclude:字符串或正则表达式。任何匹配的组件都不会被缓存。

包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。当组件在 <keep-alive> 内被切换,在 2.2.0 及其更高版本中,activateddeactivated生命周期 将会在 树内的所有嵌套组件中触发

场景:

  • Vue中前进刷新,后退缓存用户浏览数据
  • 列表页面 =>点击进入详情页=> 后退到列表页 要缓存列表原来数据
  • 重新进入列表页面 => 获取最新的数据

因为 vuex 中的 state 是存储在内存中的,一刷新就没了,例如登录状态,解决方案有:

第一种:利用 H5 的本地存储:localStorage, sessionStorage,不会自动把数据发送给服务器

第二种:利用第三方封装好的插件,例如:vuex-persistedstate

第三种:使用 js-cookie cookieparse(将Cookie字符串解析为一个对象) 插件来做存储

如果是现代化服务端渲染,不能放到本地存储,因为本地存储只有客户端可以获取到,我们希望数据既能被客户端获取又能被服务端获取。因此我们将数据放到cookie,这样前后端都能获取。

客户端加载 js-cookie 包

// process.client 是Nuxt中特殊提供的数据,运行在客户端为 true; 运行在服务端为 false
const Cookie = process.client ? require('js-cookie') : undefined

// 为了防止刷新页面数据丢失,需要将数据持久化
Cookie.set('user', data.user)

// 服务端渲染期间 nuxtServerInit 是一个特殊的 action 方法 
// commit:用来提交 mutation 的 commit 方法; req:服务端渲染期间的 request 请求对象
nuxtServerInit ({ commit }, { req }){
  const parsed = cookieparser.parse(req.headers.cookie)
  user = JSON.parse(parsed.user) // 转为对象
  commit('setUser', user)
}

第四种:可以把数据传递到后台,存储到数据库中,比较耗费资源

CookielocalStoragesessionStorage
数据生命周期一般由服务器生成,可设置失效时间。如果在浏览器生成,默认关闭后失效除非被消除,永久保存仅在当前会话下有小,关闭页面失效
大小4KB5MB5MB

cookie 最开始被设计出来并不是来做本地存储的。而是为了弥补HHTP请求在状态管理上的不足。HTTP为无状态协议,客户端向服务端发请求,服务器返回响应,下次发请求让服务端知道客户端是谁

cookie 是网站为了标识用户身份而存储在用户本地终端上的数据(通常加密),cookie始终在同源的http请求中携带(即使不需要)

vue 中的 http 请求如果散落在 vue 各种组件中,不便于后期维护与管理,所以项目中通常将业务需求统一存放在一个目录下管理,这里面放入组件中用到的所有封装好的 http 请求并导出,再其他用到的组件中导入调用

export function xxx() {
  return request({
    url: '/api',
    method: 'GET'
  })
}

axios 拦截器可以让我们在项目中对后端 http 请求和响应自动拦截处理,减少请求和响应的代码量,提升开发效率同时也方便项目后期维护

/**
 * 基于 axios 封装的请求模块
 */

const request = axios.create({
  baseURL: 'https://conduit.productionready.io/'
})

export default request

不同协议、不同域名、不同端口都会造成跨域。由于开发服务器的缘故,我们将应用运行在 loaclhost 的一个端口上面,而最终上线过后,一般又和 API 部署到同源地址下面

这就会产生一个非常见问题:实际生产当中可以直接访问API,但是回到开发环境就会产生跨域请求问题

我最推荐的⽅式就是: CORS 全称为 Cross Origin Resource Sharing(跨域资源共享)。这种⽅案对于前端来说没有什么⼯作量,和正常发送请求写法上没有任何区别,⼯作量基本都在后端这⾥。每⼀次请求,浏览器必须先以 options 请求⽅式发送⼀个预请求(也不是所有请求都会发送 options),通过预检请求从⽽获知服务器端对跨源请求⽀持的 HTTP ⽅法。在确认服务器允许该跨源请求的情况下,再以实际的 HTTP 请求⽅法发送那个真正的请求。推荐的原因是:只要第⼀次配好了,之后不管有多少接⼝和项⽬复⽤就可以了,⼀劳永逸的解决了跨域问题,⽽且不管是开发环境还是正式环境都能⽅便的使⽤

但总有后端觉得麻烦不想这么搞,那纯前端也是有解决⽅案的:

dev 开发模式下可以下使⽤ webpack 的 proxy,但这种⽅法在⽣产环境是不能使⽤的,在⽣产环境中需要使⽤ nginx 进⾏反向代理。不管是 proxy 还是 nginx 的原理都是⼀样的,通过搭建⼀个中转服务器来转发请求规避跨域的问题

如果只针对vue本身可以通过代理的方式可以实现:在 config中的index.js中配置prox

module.exports = {
  ...
  devServer: {
    proxy: {
      '/boss': {
        target: 'http://eduboss.lagou.com',
        // changeOrigin: true 以实际代理请求的主机名请求
        // 设置请求头中的 host 为 target,防⽌后端反向代理服务器⽆法识别
        changeOrigin: true
      }
    }
  }
}

JavaScript 中的内存管理:

申请内存空间,使用内存空间,释放内存空间(代码性能测试平台JSBench)

JavaScript 中的垃圾:

  1. JavaScript 中的内存管理是自动的
  2. 对象不再被引用时是垃圾
  3. 对象不能从根上访问到是垃圾

JavaScript中的可达对象:

  1. 可以访问到的对象就是可达对象(引用、作用域链)
  2. 可达的标准就是从根出发是否能够被找到
  3. JavaScript中的根就可以理解为是全局变量对象

V8 是一款主流的 JavaScript 执行引擎(Chrome、Node),采用即时编译(直接将源码翻译为机器码)

V8 内存设限(浏览器下足够使用,非增量标记最多需要1S)64bit 1.5G;32bit 800M

采⽤分代回收思想,内存分为新⽣代、⽼⽣代,针对不同对象采⽤不同的算法

新⽣代对象回收实现:(空间换时间)

1.回收过程采⽤复制算法+标记整理

2.新⽣代内存区分为两个等⼤⼩空间

3.使⽤空间From,空闲空间To

4.活动对象存储于From空间

5.标记整理后将活动对象拷⻉⾄To

6.From与To交换空间完成释放

⽼⽣代对象回收实现:

1.主要采⽤标记清除、标记整理、增量标记算法

2.⾸先使⽤标记清除完成垃圾空间的回收

3.采⽤标记整理进⾏空间优化(空间不⾜以晋升):将新生代移至老生代(一轮GC还存活的;To空间使用率超25%的)

4.采⽤增量标记进⾏效率优化:把完整的垃圾回收任务拆分为很多小的任务,穿插在其它的JS任务中间执行(防止全停顿造成的卡顿)

浏览器中的缓存作用分为两种情况,一种是需要发送HTTP请求,一种是不需要发送

  • 强缓存:这个阶段 不需要发送HTTP请求

在HTTP/1.0使用的是Expires Expires: Wed, 22 Nov 2019 08:41:00 GMT(服务器时间和浏览器时间不一致就不准确)

而HTTP/1.1使用的是Cache-Control Cache-Control:max-age=3600(no-store不缓存 no-cache跳过强缓存)

当Expires和Cache-Control同时存在的时候,Cache-Control会优先考虑

当资源缓存时间超时,也就是强缓存失效后,就进入到第二级屏障——协商缓存

  • 协商缓存:发送HTTP请求,这样的缓存tag分为两种: Last-Modified 和 ETag。这两者各有优劣

Last-Modified 即最后修改时间。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段

浏览器接收到后,如果再次请求,会在请求头中携带If-Modified-Since字段,这个字段的值也就是服务器传来的该文件最后修改时间,服务器拿到请求头中的If-Modified-Since的字段后,会和这个服务器中该资源的最后修改时间对比

ETag 是服务器根据当前文件的内容,给文件生成的唯一标识,只要里面的内容有改动,这个值就会变。服务器通过响应头把这个值给浏览器,浏览器接收到ETag的值,会在下次请求时,将这个值作为If-None-Match这个字段的内容,并放到请求头中,然后发给服务器,服务器接收到If-None-Match后,会跟服务器上该资源的ETag进行比对

两种的对比:

有些特定的场合下,一些静态的文件,可能会被频繁的更新, 但是文件内容没有变化,这时候如果使用Last-modified,服务器端始终返回最新的内容给浏览器,而Etag是根据文件内容来的,如果内容没有变化的话,始终会让浏览器使用本地缓存的文件。所以,使使用ETag可以更好的避免一些不必要的服务器相应

  • 缓存位置:若资源更新,返回资源和200状态码,否则,返回304,告诉浏览器直接从缓存获取资源

浏览器中的缓存位置一共有四种,按优先级从高到低排列分别是:

Service Worker:借鉴了 Web Worker的 思路,即让 JS 运行在主线程之外,由于它脱离了浏览器的窗体,因此无法直接访问DOM

Memory Cache:内存缓存,从效率上讲它是最快的。但是从存活时间来讲又是最短的,渲染进程结束也就不存在了

Disk Cache:存储在磁盘中的缓存,从存取效率上讲是比内存缓存慢的,但是他的优势在于存储容量和存储时长

Push Cache:推送缓存,这是浏览器缓存的最后一道防线,它是 HTTP/2 中的内容

  • 1xx(临时响应)表示临时响应并需要请求者继续执行操作的状态代码
  • 2xx(成功)表示成功处理了请求的状态码。 200 – 服务器成功返回网页
  • 3xx(重定向)表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。
    • 301 (永久移动) 请求的网页已永久移动到新位置。 服务器返回此响应时,会自动将请求者转到新位置。
    • 302 (临时移动) 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求。
    • 304 (未修改) 自从上次请求后,请求的网页未修改过。 服务器返回此响应时,不会返回网页内容。
  • 4xx(请求错误) 这些状态代码表示请求可能出错,妨碍了服务器的处理
    • 400 - 客户端请求参数错误
    • 401 - 未授权 Token验证失败
    • 403 - 没有权限
    • 404 – 请求的网页不存在
  • 5xx(服务器错误)这些状态代码表示服务器在尝试处理请求时发生内部错误。 可能是服务器本身的错误
    • 500 (服务器内部错误) 服务器遇到错误,无法完成请求。
    • 503 (服务不可用) 服务器目前无法使用(由于超载或停机维护)。 通常,这只是暂时状态。
  1. 构建请求
  2. 查找强缓存
  3. DNS 解析
  4. TCP 连接
  5. 发送 HTTP 请求
  6. 服务器处理请求并返回需要的数据(网络响应)
  7. 浏览器解析渲染页面
  8. 连接结束

输入了一个域名,域名要通过 DNS 解析找到这个域名对应的服务器地址(ip),通过 TCP请求 连接服务,通过 WEB 服务器(apache)返回数据,浏览器根据返回数据构建 DOM 树,通过css渲染引擎及js解析引擎将页面渲染出来,关闭 TCP 连接

防抖:防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行

节流:节流的原理很简单:如果你持续触发事件,每隔一段时间,只执行一次事件。根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器

css 加载不会阻塞 DOM 树的解析,css 加载会阻塞 DOM 树的渲染,css 加载会阻塞后面 js 语句的执行

因此,为了避免让用户看到长时间的白屏时间,我们应该尽可能提高 css 加载速度,比如可以使用以下几种方法:

  1. 使用 CDN(因为CDN会根据你的网络状况,替你挑选最近的一个具有缓存内容的节点为你提供资源,因此可以减少加载时间)
  2. 对 css 进行压缩(可以使用打包工具,比如 webpack,gulp等,也可以开启 gzip 压缩
  3. 合理的使用缓存(设置强缓存 cache-control、expires,以及协商缓存E-tag都是不错的,不好过要注意一个问题,就是文件更新后你要避免缓存而带来的影响,其中一个解决方案是在文件名字后面加一个版本号)
  4. 减少 http 请求数,将多个 css 文件合并,或者干脆写成内联样式(内联样式的一个缺点是不能缓存)

css3 比 css2 多了好多针对移动端的特性,比如:圆角:border-radius,盒阴影:box-shadow,还有动画:transition(过渡),transform(实现位移,倾斜,旋转,绽放),animation(关键帧动画)等

第一种思路:通过给 div 设置绝对定位,并且 left,right,top,bottom设置为 0,margin:auto 即可以水平垂直居中

第二种思路:通过给 div 设置绝对定位,left 为 50%,top 为 50%,再给div 设置距左是自身的一半即:margin-left:自身宽度/2,margin-top:自身高度/2

第三种思路:通过给 div 设置绝对定位,left 为 50%,top 为 50%,再给div 设置跨左和跟上是自身的一半:transform:translate3d(-50%,-50%,0)

第四种:flex 布局:思路:有两个div,父级div和子级div,给父级div设置display:flex,并且设置父级 div 的水平居中 justify-content:center,并且给父级 div 设置垂直居中 align-items:center 即可

第一种:过渡动画:主要通过 transition 来实现,通过设置过渡属性,运动时间,延迟时间和运动速度实现。

第二种:关键帧动画:主要通过 animation 配合@keyframes 实现transition 动画和 animation 动画的主要区别有两点:第一点 transition 动画需要事件来触发,animation 不需要;第二点:transition 只要开始结束两种状态,而 animation 可以实现多种状态,并且 animation 是可以做循环次数甚至是无限运动

rem 是相对于根字号,即相对于<html>标签的 font-size 实现的,浏览器默认字号是 font-size:16px

em:是相对于父元素标签的字号,和百分比%类似,%也是相对于父级的,只不过是%相对于父级宽度的,而 em 相对于父级字号的