核心前端体系知识点

前端大神之路

共 47150字,需浏览 95分钟

 · 2021-12-24

本文适合想对前端体系知识进行梳理的小伙伴阅读。

欢迎关注前端早茶,与广东靓仔携手共同进阶~

一、前言

    最近有小伙伴私聊广东靓仔,快到年尾了,想充实下自己的前端知识体系,但是没有个方向。我们都知道前端知识纵横交错,知识体系庞大,相信有不少小伙伴无从下手、囫囵吞枣。

    广东靓仔收集了一些比较重要的知识点,下面我们展开一起来看看。

315ab8524af4b358678f566e80d23488.webp

二、HTML

1.语义化

    所谓,语义化的标签,说明让标签有自己的含义。也是近十年。最典型的栗子就是header,footer等,它可以让你在没有样式的情况下,就大概能想到,他就是个头部或者底部。他存在的意义,就是让前端开发人员,在开发过程中,更容易去阅读代码,以及明白这些代码的意义。

好处

  • 能够更好的展示内容结构
  • 便于团队的维护与开发
  • 有利于SEO
  • 爬虫可以分析每个关键词的权重
  • 方便其他设备解析 (如屏幕阅读器)

2.SEO

    身为前端,我们不得不知道的SEO,这涉及到公司的网站推广。SEO,中文称搜索引擎优化,一种利用搜索引擎的搜索规则来提高目前网站在有关搜索引擎内的自然排名的方式。他的实现原来分别为,页面抓取,分析入库,检索排序。

如何优化SEO

  • title、description、keywords
  • 利用好html语义化
  • 重要的东西放前面
  • 少用iframe


3.doctype

    前端经常在html头部看到DOCTYPE的声明,一般常位于文档的第一行。那么他的作用是什么,可能对新的浏览器或者新的网站暂无什么影响,但是相对古老的浏览器或者是网站,可能会出现不同。因为浏览器有标准模式与兼容模式,差异相对比较大。标准模式的渲染方式和 JS 引擎的解析方式都是以该浏览器支持的最高标准运行。兼容模式中,页面以宽松的向后兼容的方式显示 ,模拟老式浏览器的行为以防止站点无法工作。而DOCTYPE的存在,就是为了声明,该页面使用标准模式。不声明,可能一些旧的网站会出现兼容模式。


4.link与@import

link与import , 本质使用上,我们都是用他来引入css。

区别:

  • link是一种引入资源的标签,import是引入css的方式。所以,import引入的只能是css,而link可以引入所有的资源,包括图片,RSS等。
  • 加载顺序上也有一些差异。link引用的CSS会同时被加载。import引用的CSS会等到页面全部被下载完再加载。
  • 兼容性的差别。link无任何兼容问题,import兼容IE5以上。(当然,IE5估计也找不到了)
  • 动态引入样式 link可以后期引入样式,而import是不可以后期引入的,只能初始化页面之前引入。
  • 复用率的问题 import可以复用之前的css文件,而link只能一次引用一个文件。当然,import复用文件时,在浏览器实际上是加载了多个文件,会有多个请求。而每一个link只是一个http请求。

5.async与defer

    首先这两个东西为什么而存在的问题。在日渐复杂的前端,异常已经是程序的一部分。如果出现一些小问题,或者服务器加载上出现延迟。而我们默认的引入的script脚本,会阻塞后续的DOM渲染。一旦没有部分异常无法及时加载完成,那么我们的页面因为阻塞问题,将整个白屏。也许我们可以保证自己服务器的正常,但是你决定保证不了第三方服务器的正常,于是引入了asyncdefer来优化这个问题。再来谈谈script的默认,asyncdefer的之前的差异。默认情况下:浏览器会立即加载并执行指定的脚本。指定的脚本,指在script标签之上的脚本。所以,如果script放在header中,而对应的文件还未加载完成,会形成阻塞。所以这就是现在很多页面,都会使用默认且把scipt放在页面结尾的原因。async情况下:async ,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。async是乱序的。defer情况下:defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。defer是顺序执行。此外,async跟defer,不支持或者不兼容IE9一下浏览器,总体来说,script放最下方靠谱一些。


6.捕捉,冒泡与委托

    适合用事件委托的事件:click,mousedown,mouseup,keydown,keyup,keypress。执行顺序:捕捉--》目标--》冒泡event.stopPropagation()阻止事件的传递行为. event.preventDefault();阻止默认行为,比如阻止a的href

优点:

  • 减少事件注册,节省内存。例如上面代码,只指定 父元素的处理程序,即可管理所有所有子元素的“click”事件;
  • 简化了dom节点更新时,相应事件的更新

缺点:

  • 利用事件冒泡的原理,不支持不冒泡的事件;
  • 层级过多,冒泡过程中,可能会被某层阻止掉;
  • 理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,比如在ol上代理li,而不是在document上代理li。
  • 把所有事件都用代理就可能会出现事件误判。比如,在document中代理了所有button的click事件,另外的人在引用改js时,可能不知道,造成单击button触发了两个click事件。


7.渐进增强与优雅降级

    渐进增强:针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高级浏览器进行效果、交互等改进,达到更好的用户体验。优雅降级:一开始就构建完整的功能,然后再针对低版本浏览器进行兼容。

三、js函数根基

1.函数的独特之处    函数是第一型对象,因为函数可以像对象通过字面量进行创建,赋值变量、数组,作为函数的返回值,拥有动态创建并返回赋值的属性。    函数最重要的特性是它可以被调用,而通常都是以异步的方式进行调用,其原理是浏览器事件轮询是单线程的,即每个事件都是按照在队列放放置的顺序类执行的,就是FIFO(先进先出)列表,在任何情况下单线程不可能同时执行两个程序,所以必须等待当前事件结束之后才能执行另外一个事件,就是说事件执行的时间不可知所以事件处理函数调用异步。
2.函数声明

函数是使用字面量进行声明从而创建函数值的,函数字面量由四个部分组成

1.function关键词 
2.可选名称
3.括号内部,一个以逗号分隔开的参数列表
4.函数体

函数名是可选的,它只是函数的引用,匿名函数就是一个栗子。可以通过访问函数的name属性获取函数名

function ninja(){}
ninja.name //ninja

另外值得注意的是书上说创建一个匿名函数并赋值给一个变量,这个变量并不是该函数的name,但是本人发现在支持ES6语法的chrome下获取函数name不是空而是匿名函数赋值给该变量的变量名。

var fn = function () {};
// ES5
fn.name // ""
// ES6
fn.name // "fn"

也可以通过属性访问的方式获取形参长度,注意是形参不是实参,形参和实参的区别是:形参是函数声明时定义的参数,而实参是函数调用时传给函数的参数。下面会讲怎么获取实参。
var fn = function (a,b) {};
fn.length // 2

3.函数调用    函数被调用的时候到底发生了什么?事实上函数被调用的方式对其函数内部代码执行有着巨大的影响。有四种不同的方式进行函数调用,每个方式都有细微的差别。
  • 作为一个函数调用,是简单的形式
  • 作为一个方法调用,在对象上进行调用
  • 作为构造器调用,创建一个对象
  • 通过apply()和call()方法进行调用

有趣的是在函数调用的时候都会传递两个隐式参数:arguments和this,所谓隐式就是这些参数不会显示在函数签名里,但是它们会传递给函数并在函数作用域内,在函数内可以进行访问和使用。

arguments

arguments是函数调用时传给函数的实际参数集合,可以用length属性获取集合的长度,但是arguments并不是真正的数组。

function fn (a,b,c){console.log(arguments.length) } 
fn.length // 3
fn(6,6,) // 2 

this参数

this就是函数上下文,而this指向依赖于函数的调用方式,所以this称作调用上下文更合适。当函数作为“函数调用“,是指区别于方法、构造器以及apply/call,this指向与widow对象

function ninja(){console.log(this)}
ninja() //window

当函数作为“方法调用”,是指当函数被赋值给对象的一个属性,并使用引用该函数的这个属性进行调用,this指向该对象,实例如下:

var o={};
o.ninja=function()console.log(this)};
o.ninja(); // o


当函数作为“构造器”进行调用时:

  • 创建一个新的对象
  • 将构造器函数作用域赋值给新对象(因此this指向了这个新对象)
  • 执行构造函数中的代码(为这个新对象添加属性)
  • 如果没有显式的返回值,返回新对象
function Ninja(){
    this.shulk=function(){console.log(this)}
}
var Ninja1=new Ninja();
var Ninja1=new Ninja();
Ninja1.shulk() //Ninja1
Ninja2.shulk() //Ninja2

使用apply()和call()方法进行调用 在函数调用的时候JavaScript为我们提供一种可以显式指定任何一个对象作为函数的上下文,使用apply()和call()方法可以实现这种功能
function juggle(){
    var result=0;
    forvar i=0;i<arguments.length;i++){
        result=+arguments[i];
    }
    this.result=result;
}
var ninja1={},ninja2={};

juggle.apply(ninja1,[1,2,3,4]) 
console.log(ninja1.result) // 10
juggle.call(ninja2,5,6,7,8
console.log(ninja2.result) // 26

在本例中可以看出apply()方法把函数juggle上下文指定给了ninja1对象,call()方法把函数juggle上下文指定给了ninja2。常见使用apply()、call()方法是在实现函数回调的时候,比如下面实现一个简单的forEach函数:
function forEach(list,callback)(){
    for(var i=0;i>list.length;i++){
        callback.call(list[i],list[i],i)
    }
}
var arr=['Tom','alice','jack'];
forEach(arr,function(e,i){
    console.log(e)
})  //Tom,alice,jack

apply()和call()的功能基本相同,我们该选择哪个比较好呢?如果在变量里有很多无关的值或者是指定为字面量,使用call()则可以直接将其作为参数列表传进去,但是如果这些参数已经在一个数组里了,或者很容易将其收集到数组里,apply()是更好选择。

4.async/await 原理

    async/await语法糖就是使用Generator函数+自动执行器来运作的


5.判断数组的方法以及优缺点

  • Array.isArray(arr) :兼容性不好
  • arr instanceof Array
  • arr.proto === Array.prototype
  • arr.constructor === Array
  • Object.prototype.toString.call(arr) === '[object Array]'

6.js脚本加载问题,async、defer问题

  • 如果依赖其他脚本和 DOM 结果,使用 defer
  • 如果与 DOM 和其他脚本依赖不强时,使用 async


7.什么是作用域链?

JavaScript 引擎会沿着“当前执行上下文–>内嵌函数闭包–> 全局执行上下文”的冒泡的方式顺序查找变量,就形成了一条作用域链


8.this 的指向

  • 当函数作为对象的方法调用时,函数中的 this 就是该对象;
  • 当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;


9.改变 this 指向的方式:

  • 用 call、apply、bind设置
  • 通过对象调用方法设置(指向该对象)
  • 通过构造函数中设置(构造函数this指向 new 的对象)


10. 用过哪些设计模式

11. Javascript垃圾回收

当对象不再被 引用 时被垃圾回收机制回收(“对象有没有其他对象引用到它”)。

内存泄露就是不再被需要的内存, 由于某种原因, 无法被释放

  • 引用计数法:对象是否不再需要(限制:循环引入不能被回收)
  • 标记清除法:从根开始,找所有从根开始引用的对象标记,然后找这些对象引用的对象标记,把不能达到的回收(这个算法把“对象是否不再需要”简化定义为“对象是否可以获得”。)
  • 通过构造函数中设置(构造函数this指向 new 的对象)

避免内存泄漏:

  • 尽少创建全局变量
  • 手动清除计时器
  • 少用闭包
  • 使用弱引用WeakMap和WeakSet


12. 0.1 + 0.2 === 0.3 嘛?为什么?

计算机存储以二进制的方式,而0.1 在二进制中是无限循环的一些数字,所以会出现裁剪,精度丢失会出现,0.100000000000000002 === 0.1,0.200000000000000002 === 0.2 // true 这两加起来肯定不等于0.3解决:parseFloat((0.1 + 0.2).toFixed(10)) === 0.3 // true

四、es6

es6新特性

1. Proxy 有什么特点

用于修改某些操作的默认行为,(拦截对象对其内置方法进行重载)

可以直接拦截对象和数组

let target = {
    x10,
    y20
};
let hanler = {
    // obj=>target,prop =>x
    get(obj, prop) => 42
};

target = new Proxy(target, hanler);
target.x; //42
target.y; //42
target.x; // 42

可以拦截的对象:

  • get、set、has、apply、construct、ownKeys
  • deleteProperty、defineProperty、isExtensible、preventExtensions
  • getPrototypeOf、setPrototypeOf、getOwnPropertyDescriptor

实际用途可以拦截的对象:

  • 设置对象的初始值覆盖undefined
  • 缓存封装是否过期拦截


2.CommonJs 和 ES6 module 区别

  • 前者支持动态引入即require(${path}/xx.js),后者不支持
  • ES6 module 静态引入,即编译时引入,CommonJs 动态引入,即执行时引入
  • 前者是对模块的浅拷贝,后者是对模块的引用
  • 前者可以对导出对值赋值(改变指针),而后者不能相当于 const
  • 后者支持 tree shaking,前者不支持


3. 什么是UMD

((root, factory) => {
    if (typeof define === 'function' && define.amd) {
        //AMD
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        //CommonJS
        var $ = requie('jquery');
        module.exports = factory($);
    } else {
        root.testModule = factory(root.jQuery);
    }
})(this, ($) => {
    //todo
});

4. weak-Set、weak-Map 和 Set、Map 区别

WeakMap 是类似于 Map 的集合,它仅允许对象作为键,并且一旦通过其他方式无法访问它们,便会将它们与其关联值一同删除。WeakSet 是类似于 Set 的集合,它仅存储对象,并且一旦通过其他方式无法访问它们,便会将其删除。

5. WebAssembly 是什么

WebAssembly 是一种可以使用非 JavaScript 编程语言编写代码并且能在浏览器上运行的技术方案


6.柯里化有什么作用

主要有3个作用:参数复用提前返回和 延迟执行

es6解构用法

1.获取用户对象数组中的id集合

普通写法:

let users = [{id:'abc',name:'tom'},{id:'dfg',name:'jake'}]
 function getUserIds(){
     let userIds = [];
    users.forEach(user=>{
        userIds.push({id:user.id})
    }) 
    return userIds
 } 
 getUserIds() // [{id:'abc'},{id:'dfg'}]

bigger 写法:

letlet users = [{id:'abc',name:'tom'},{id:'dfg',name:'jake'}]
function getUserIds(){
  let userIds = users.map(user=>{
     let {id} = user
     return {id}
   }) 
   return userIds

 getUserIds() // [{id:'abc'},{id:'dfg'}]

2.删除对象

let regionTree=[
    {
      disabled:false,
      value:'广州',
      children:[]
    },
    {
      disabled:true,
      value:'深圳',
      children:[]
    }
  ]
let result = regionTree.map(item => {
         let { disabled, ...values } = item;
         return values;
       });
//result=[{value:'广州',children:[]},{value:'深圳',children:[]}]

3.优雅地获取年月日

const [all, year, month, day] = /^(\d{4})-(\d{1,2})-(\d{1,2})$/.exec('2020-01-20');
// 2020-01-20 2020 01 20

4.为Ajax请求设置预警

假设 this.$http(url) 返回的对象正确格式为{code:0,data:{id:'123',name:'tony'} },如data返回为空则抛出异常

 function toastErr (){
    alert('俄欧,没有获取到任何数据~')
 }
 let {code,data:y=toastErr()} = this.$http(url)

5.获取函数参数

function getArguments(...args//这里args 是函数内置的Arguments类数组
   console.log(args) // [1,3,2]
}
let a=1,b=2,c=3
getArguments(a,b,c)

五、前端设计模式

工厂模式

简单工厂- 处理变与不变的

工厂模式:将创建对象的过程单独封装,实现无脑传参,核心:处理变与不变的
改进前:

function Coder(name , age{
    this.name = name
    this.age = age
    this.career = 'coder' 
    this.work = ['写代码','写系分''修Bug']
}
function ProductManager(name, age{
    this.name = name 
    this.age = age
    this.career = 'product manager'
    this.work = ['订会议室''写PRD''催更']
}
function Factory(name, age, career{
    switch(career) {
        case 'coder':
            return new Coder(name, age) 
            break
        case 'product manager':
            return new ProductManager(name, age)
            break
        ...
}

改进后

function User(name,age,career,word){
    this.name = name 
    this.age = age
    this.career = career
    this.word = word 
}

function Factory(name,age,career){
   let word=[]
   switch(career){
     case 'coder':
       word=['写代码','写系分''修Bug']
       break;
     case 'product manager':
       word=['写原型','催进度']
       break
     case 'boss':
       word=['喝茶','见客户','谈合作']
       break
  }
 return new User(name,age,career,word)
}


抽象工厂- 开放封闭原则

简单工厂因为没有遵守开放封闭原则, 暴露一个很大的缺陷。例如若我们添加管理层一些考评权限,难道我们要重新去修改Factory函数吗?这样做会导致Factory会变得异常庞大,而且很容易出bug,最后非常难维护
class MobilePhoneFactory {
    // 提供操作系统的接口
    createOS(){
        throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
    }
    // 提供硬件的接口
    createHardWare(){
        throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
    }
}
// 具体工厂继承自抽象工厂
class FakeStarFactory extends MobilePhoneFactory {
    createOS() {
        // 提供安卓系统实例
        return new AndroidOS()
    }
    createHardWare() {
        // 提供高通硬件实例
        return new QualcommHardWare()
    }
}

// 定义操作系统这类产品的抽象产品类
class OS {
    controlHardWare() {
        throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
    }
}

// 定义具体操作系统的具体产品类
class AndroidOS extends OS {
    controlHardWare() {
        console.log('我会用安卓的方式去操作硬件')
    }
}

class AppleOS extends OS {
    controlHardWare() {
        console.log('我会用🍎的方式去操作硬件')
    }
}
// 定义手机硬件这类产品的抽象产品类
class HardWare {
    // 手机硬件的共性方法,这里提取了“根据命令运转”这个共性
    operateByOrder() {
        throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
    }
}

// 定义具体硬件的具体产品类
class QualcommHardWare extends HardWare {
    operateByOrder() {
        console.log('我会用高通的方式去运转')
    }
}

class MiWare extends HardWare {
    operateByOrder() {
        console.log('我会用小米的方式去运转')
    }
}

// 这是我的手机
const myPhone = new FakeStarFactory()
// 让它拥有操作系统
const myOS = myPhone.createOS()
// 让它拥有硬件
const myHardWare = myPhone.createHardWare()
// 启动操作系统(输出‘我会用安卓的方式去操作硬件’)
myOS.controlHardWare()
// 唤醒硬件(输出‘我会用高通的方式去运转’)
myHardWare.operateByOrder()

对原有的系统不会造成任何潜在影响所谓的“对拓展开放,对修改封闭”

单例模式

单例模式 - 保证一个类只有一个实例

保证一个类仅有一个实例,并提供一个访问它的全局访问点。
class SingleDog {
 show(){
  console.log('单例方法')
 }
}
let single1=new SingleDog()
let single2=new SingleDog()

single1===single2 // false

单例模式要求不管我们尝试去创建多少次,它都只给你返回第一次所创建的那唯一的一个实例

class SingleDog {
 show(){
  console.log('单例方法')
 }
 static getInstance(){
   if(!SingleDog.instance){
    SingleDog.instance = new SingleDog()    
    }
    return SingleDog.instance
  }
}
let single1 = SingleDog.getInstance()
let single2 = SingleDog.getInstance()
single1===single2 // true

单例实际应用 vuex

Store 存放共享数据的唯一数据源,要求一个 Vue 实例只能对应一个 Store,即Vue 实例(即一个 Vue 应用)只会被 install 一次 Vuex 插件

let Vue // 这个Vue的作用和楼上的instance作用一样
...

export function install (_Vue{
  // 判断传入的Vue实例对象是否已经被install过Vuex插件(是否有了唯一的state)
  if (Vue && _Vue === Vue) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  // 若没有,则为这个Vue实例对象install一个唯一的Vuex
  Vue = _Vue
  // 将Vuex的初始化逻辑写进Vue的钩子函数里
  applyMixin(Vue)
}

装饰器模式

装饰器模式 - 实现只添加不修改

拓展弹窗功能

// 定义打开按钮
class OpenButton {
    // 点击后展示弹框(旧逻辑)
    onClick() {
        const modal = new Modal()
     modal.style.display = 'block'
    }
}

// 定义按钮对应的装饰器
class Decorator {
    // 将按钮实例传入
    constructor(open_button) {
        this.open_button = open_button
    }
    onClick() {
        this.open_button.onClick()
        // “包装”了一层新逻辑
        this.changeButtonStatus()
    }
    changeButtonStatus() {
        this.changeButtonText()
        this.disableButton()
    }
    disableButton() {
        const btn =  document.getElementById('open')
        btn.setAttribute("disabled"true)
    }
    changeButtonText() {
        const btn = document.getElementById('open')
        btn.innerText = '快去登录'
    }
}

const openButton = new OpenButton()
const decorator = new Decorator(openButton)

document.getElementById('open').addEventListener('click'function() {
    // openButton.onClick()
    // 此处可以分别尝试两个实例的onClick方法,验证装饰器是否生效

    decorator.onClick()
})


es7 装饰器

function classDecorator(target{
    target.hasDecorator = true
   return target
}

// 将装饰器“安装”到Button类上
@classDecorator
class Button {
    // Button类的相关逻辑
}

装饰器的原型

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

装饰器只能用于类或者类的方法原因:普通函数声明会提升


实现 react 的高阶函数

定义装饰器

import React, { Component } from 'react'

const BorderHoc = WrappedComponent => class extends Component {
  render() {
    return <div style={{ border: 'solid 1px red' }}>
      <WrappedComponent />
    </div>

  }
}
export default borderHoc

应用装饰器

import React, { Component } from 'react'
import BorderHoc from './BorderHoc'

// 用BorderHoc装饰目标组件
@BorderHoc 
class TargetComponent extends React.Component {
  render() {
    // 目标组件具体的业务逻辑
  }
}

// export出去的其实是一个被包裹后的组件
export default TargetComponent

适配器模式

适配器模式 - 兼容就是一把梭

原 ajax 定义

function Ajax(type, url, data, success, failed){
    // 创建ajax对象
    var xhr = null;
    if(window.XMLHttpRequest){
        xhr = new XMLHttpRequest();
    } else {
        xhr = new ActiveXObject('Microsoft.XMLHTTP')
    }
 
   ...(此处省略一系列的业务逻辑细节)
   
   var type = type.toUpperCase();
    
    // 识别请求类型
    if(type == 'GET'){
        if(data){
          xhr.open('GET', url + '?' + data, true); //如果有数据就拼接
        } 
        // 发送get请求
        xhr.send();
 
    } else if(type == 'POST'){
        xhr.open('POST', url, true);
        // 如果需要像 html 表单那样 POST 数据,使用 setRequestHeader() 来添加 http 头。
        xhr.setRequestHeader("Content-type""application/x-www-form-urlencoded");
        // 发送post请求
        xhr.send(data);
    }
 
    // 处理返回数据
    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4){
            if(xhr.status == 200){
                success(xhr.responseText);
            } else {
                if(failed){
                    failed(xhr.status);
                }
            }
        }
    }
}

fetch 请求封装

export default class HttpUtils {
  // get方法
  static get(url) {
    return new Promise((resolve, reject) => {
      // 调用fetch
      fetch(url)
        .then(response => response.json())
        .then(result => {
          resolve(result)
        })
        .catch(error => {
          reject(error)
        })
    })
  }
  
  // post方法,data以object形式传入
  static post(url, data) {
    return new Promise((resolve, reject) => {
      // 调用fetch
      fetch(url, {
        method'POST',
        headers: {
          Accept'application/json',
          'Content-Type''application/x-www-form-urlencoded'
        },
        // 将object类型的数据格式化为合法的body参数
        bodythis.changeData(data)
      })
        .then(response => response.json())
        .then(result => {
          resolve(result)
        })
        .catch(error => {
          reject(error)
        })
    })
  }
  
  // body请求体的格式化方法
  static changeData(obj) {
    var prop,
      str = ''
    var i = 0
    for (prop in obj) {
      if (!prop) {
        return
      }
      if (i == 0) {
        str += prop + '=' + obj[prop]
      } else {
        str += '&' + prop + '=' + obj[prop]
      }
      i++
    }
    return str
  }
}


fetch 兼容 ajax(放弃ajax)

// Ajax适配器函数,入参与旧接口保持一致
async function AjaxAdapter(type, url, data, success, failed{
    const type = type.toUpperCase()
    let result
    try {
         // 实际的请求全部由新接口发起
         if(type === 'GET') {
            result = await HttpUtils.get(url) || {}
        } else if(type === 'POST') {
            result = await HttpUtils.post(url, data) || {}
        }
        // 假设请求成功对应的状态码是1
        result.statusCode === 1 && success ? success(result) : failed(result.statusCode)
    } catch(error) {
        // 捕捉网络错误
        if(failed){
            failed(error.statusCode);
        }
    }
}

// 用适配器适配旧的Ajax方法
async function Ajax(type, url, data, success, failed{
    await AjaxAdapter(type, url, data, success, failed)
}

代理模式

事件代理:点击子元素,用父元素代理

缓存代理

const addAll = function() {
    console.log('进行了一次新计算')
    let result = 0
    const len = arguments.length
    for(let i = 0; i < len; i++) {
        result += arguments[i]
    }
    return result
}

// 为求和方法创建代理
const proxyAddAll = (function(){
    // 求和结果的缓存池
    const resultCache = {}
    return function() {
        // 将入参转化为一个唯一的入参字符串
        const args = Array.prototype.join.call(arguments',')
        
        // 检查本次入参是否有对应的计算结果
        if(args in resultCache) {
            // 如果有,则返回缓存池里现成的结果
            return resultCache[args]
        }
        return resultCache[args] = addAll(...arguments)
    }
})()

策略模式 - 消除 if-else 能手

// 定义一个询价处理器对象
const priceProcessor = {
  pre(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 20;
    }
    return originPrice * 0.9;
  },
  onSale(originPrice) {
    if (originPrice >= 100) {
      return originPrice - 30;
    }
    return originPrice * 0.8;
  },
  back(originPrice) {
    if (originPrice >= 200) {
      return originPrice - 50;
    }
    return originPrice;
  },
  fresh(originPrice) {
    return originPrice * 0.5;
  },
};
// 询价函数
function askPrice(tag, originPrice{
  return priceProcessor[tag](originPrice)
}

观察者模式

应用例子:需求发布

流程产品经理开群然后拉人(开发)进群,需求更新时通知开发者,开发者接到到需求开始工作。
    // 发布者
    class Publisher{
      constructor(){
        this.observers=[]
      }
      add(observer){
        this.observers.push(observer)
        
      }
      remove(observer){
        const index =this.observers.findIndex(item=>item===observer)
        this.observers.splice(index,1)
      }
      notify(state){
        this.observers.forEach(observer=>observer.update(state))
      }
    }
  // 观察者
    class Observer{
      constructor() {
        console.log('Observer created')
    }
      update(){
        console.log('我干活辣')
      }
    }
    // 产品经理类 (文档发布者)
    class Prdpublisher extends Publisher{
      constructor(){
        super()
        this.prdState = {}
        this.observers=[]
        console.log('Prdpublisher created')
      }
      getState(){
        return this.prdState
      }
      setState(state){
        console.log('this.observers',this.observers)
       this.prdState = state
       this.notify(state)
      }
    }
    // 开发者类
    class DeveloperObserver extends Observer{
     constructor(){
       super()
       this.prdState={}
       console.log('DeveloperObserver created')
     }
     update(state){
      this.prdState = state
      this.word()
     }
     word(){
       const prdState = this.prdState
       console.log('start wording',prdState)
     }
    }
    const observeA = new DeveloperObserver() //前端
    const observeB = new DeveloperObserver() //后端
    const lilei = new Prdpublisher() // 产品经理
    lilei.add(observeA) // 拉群
    lilei.add(observeB)
    let prd={
      // 需求内容
      'login':3,
      'auth':2
    }
    // 更新需求 同时通知开发者
    lilei.setState(prd)

vue 响应试原理-观察者模式

观察者模式和发布-订阅模式的区别是:发布-订阅模式,事件的注册和触发发生在独立于双方的第三方平台。观察者模式:发布者会直接触及到订阅者
 function observe(target){
     if(target && typeof target==='object'){
       Object.keys(target).forEach(key=>{
        defineReactive(target,key,target[key])
       })
     }
    }
    function defineReactive(target, key,val{
        observe(val)
        let dep = new Dep()
        Object.defineProperty(target, key, {
          enumerable:true,
          configurable:false,
          get() {
            return val
          },
          set(value) {
            val=value
            dep.notify()
          },
        });
      }
    class Dep {
      constructor(dep) {
        this.deps = [];
      }
      add(dep) {
        this.deps.push(dep);
      }
      notify() {
        this.deps.forEach((dep) => dep.update());
      }
    }

vue eventBus

 class EventEmitter {
      constructor() {
        this.handlers = {};
      }
      on(eventName, cb) {
        if (!this.handlers[eventName]) {
          this.handlers[eventName] = [cb];
        } else {
          this.handlers[eventName].push(cb);
        }
      }
      emit(eventName, data) {
        if (!this.handlers[eventName]) {
          console.log('监听器不存在');
          return;
        }
        const events = this.handlers[eventName].slice();
        events.forEach((cb) => {
          cb(data);
        });
      }
      off(eventName, cb) {
        if (!this.handlers[eventName]) {
          console.log('监听器不存在');
          return;
        }
        const callBacks = this.handlers[eventName];
        const index = callBacks.findIndex((item) => item === cb);
        callBacks.splice(index, 1);
      }
      once(eventName, cb) {
        const wrap = (data) => {
          let fn = this.handlers[eventName];
          cb(data);
          this.off(eventName, fn);
        };
       
        this.on(eventName, wrap);
      }
    }
    let eventBus = new EventEmitter();
    eventBus.once('success', (data) => {
      console.log('data', data);
    });
    eventBus.emit('success'456);
    eventBus.emit('success'577);

六、HTTP

从输入 URL 到页面加载完成,发生了什么?

1、HTTP 请求准备阶段

  • 构建请求
浏览器构建请求行信息,准备发起网络请求 GET /index.html HTTP1.1
  • 查找缓存
如果浏览器发现请问资源在浏览器中存在副本,根据强缓存规则,如没有过期那么直接返回资源,如何查找失败进入下一个环节:
  • 准备 ip 地址和端口
  • DNS(域名和ip的映射系统) 域名解析,拿到ip之后找端口,默认为80
  • 建立tcp链接(三次握手)
  • 如果是https 还需要建立TLS连接

2、HTTP 发送请求

  • 浏览器向服务端发起http请求,把请求头和请求行一起发送个服务器,服务端解析请求头如发现cache-control和etag(if-none-match),if-modified(if-modified-since)字段就会判断缓存是否过期,如果没有返回304,否则返回200

3、HTTP 响应返回

  • 浏览器拿到响应数据,首先判断是否是4XX或者5XX是就报错,如果是3XX就重定向,2XX就开始解析文件,如果是gzip就解压文件
  • TCP断开连接4次挥手
  • 浏览器解析渲染建立根据html建立dom树和css树,如果遇到script首选判断是否defer和async否则会阻塞渲染并编译执行js,如果没有则组合生成render tree,最后浏览器开启GPU进行绘制合成图层,将内容显示屏幕。


HTTP0.9 特性

  • 没有请问头和请求体只有请求行

  • 只能发送html文件


HTTP1.0 特性

  • 可以发送javaScript、CSS、图片、音频

  • 加上请求头和请求体

  • 状态码

  • cache 机制

  • 每进行一次 HTTP 通信,都需要经历建立 TCP 连接、传输 HTTP 数据和断开 TCP 连接三个阶段


HTTP1.1 特性

  • 持久连接的方法,它的特点是在一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么该 TCP 连接会一直保持(提高性能)
  • 持久连接虽然能减少 TCP 的建立和断开次数,但是它需要等待前面的请求返回之后,才能进行下一次请求。如果 TCP 通道中的某个请求因为某些原因没有及时返回,那么就会阻塞后面的所有请求,这就是著名的队头阻塞的问题(在 HTTP 1.1 中,每一个链接都默认是长链接,因此对于同一个 TCP 链接,HTTP 1.1 规定:服务端的响应返回顺序需要遵循其接收到相应的顺序。但这样存在一个问题:如果第一个请求处理需要较长时间,响应较慢,将会“拖累”其他后续请求的响应,这是一种队头阻塞。)
  • 引入了 Cookie、虚拟主机的支持、对动态内容的支持
  • 浏览器为每个域名最多同时维护 6 个 TCP 持久连接(提高性能)
  • 使用 CDN 的实现域名分片机制。(提高性能)
  • 对带宽的利用率却并不理想(tcp 启动慢、头部堵塞、tcp 竞争)


HTTP2 特点

  • 只使用一个 TCP 长连接来传输数据,实现资源的并行请求,也就是任何时候都可以将请求发送给服务器,解决头部堵塞(多路复用)
  • 二进制传输
  • 多路复用(原理二进制分帧层,携带id的帧流到服务器)
单一的长连接,减少了 SSL 握手的开销,多路复用能大幅提高传输效率,不用等待上一个请求的响应,加大cpu 的使用率
  • 头部压缩
头部被压缩,减少了数据传输量
  • 服务端推送
有了二进制分帧层还能够实现请求的优先级、服务器推送、头部压缩等特性,从而大大提升了文件传输效率但是HTTP2还是会存在 tcp 堵塞(理解,http 堵塞就是需要等待前面的http包发送完成而造成的等待,tcp 堵塞是TCP 传输过程中,由于单个数据包的丢失而造成的阻塞)


HTTP3

QUIC 看成是集成了“TCP+HTTP/2 的多路复用 +TLS 等功能


TCP与UDP的区别

1、基于连接与无连接;

2、对系统资源的要求(TCP较多,UDP少);

3、UDP程序结构较简单;

4、流模式与数据报模式 ;

5、TCP保证数据正确性,UDP可能丢包;

6、TCP保证数据顺序,UDP不保证。


TCP 握手过程

  • 建立tcp链接(三次握手,客户端发送 syn=j 给服务端然后处于 syn_send 状态;

  • 服务端接受到syn,然后发送自己的syn包syn=k,和 ack=j+1(确认客户端包),状态为 syn_recv;

  • 客户端收到ack和syn则发送 ack=k+1给服务端表示确认,服务端和客户端都进入了establish状态),

  • 为什么要3次握手:确认客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常


HTTP 缓存

强缓存(浏览器内部完成)

max-age:数值,单位是秒,从请求时间开始到过期时间之间的秒数。基于请求时间(Date字段)的相对时间间隔,而不是绝对过期时间

expires:和max-age一样指缓存过期时间,但是他的指定了具体时间GMT格式,HTTP/1.1,Expire已经被Cache-Control替代,原因在于Expires控制缓存的原理是使用客户端的时间与服务端返回的时间做对比,那么如果客户端与服务端的时间因为某些原因(例如时区不同;客户端和服务端有一方的时间不准确)发生误差,那么强制缓存则会直接失效,这样的话强制缓存的存在则毫无意义


协商缓存(需要询问服务器)

Last-Modified/If-Modified-Since(服务端时间对比):本地文件在服务器上的最后一次修改时间。缓存过期时把浏览器端缓存页面的最后修改时间发送到服务器去,服务器会把这个时间与服务器上实际文件的最后修改时间进行对比,如果时间一致,那么返回304,客户端就直接使用本地缓存文件。(浏览器最后修改时候和服务端对比,如果一致则走缓存)

问题:

  • 现在大多数服务端都采用了负载均衡策略,可能导致不同虚拟主机返回的Last-Modified时间戳不一致,导致对比失败~

  • 文件也许会周期性的更改,但是他的内容并不改变,不希望客户端重新get

Etag/If-None-Match:(EntityTags,内容摘要)是URL的tag,用来标示URL对象是否改变,一般为资源实体的哈希值。和Last-Modified类似,如果服务器验证资源的ETag没有改变(该资源没有更新),将返回一个304状态告诉客户端使用本地缓存文件。Etag的优先级高于Last-Modified,Etag主要为了解决 Last-Modified无法解决的一些问题。


文件缓存策略

  1. 有文件指纹:index.html 用不用缓存,其他用强缓存+文件指纹

  2. 无指纹:index.html 用不用缓存,其他用协商etag缓存(文件变了就缓存)


HTTPS

原理

服务器端的公钥和私钥,用来进行非对称加密

客户端生成的随机密钥,用来进行对称加密

  • A 与 B 通信(被监听)

  • A 给 B 一个对成密钥(对称加密)(密钥可能被劫持)

  • A 有自己的公钥和私钥(并且只要自己的私钥解开公钥)B也是有自己的公私钥,但每次通信都要解密特别麻烦(非对称加密)

  • A 直接把公钥发给B,然后让B通过公钥加密‘对称密钥’,效率就快很多

  • 那么这里又存在一个问题 A 传给 B 的公钥被截取了然后冒充 A跟B通信

  • 所以就有了ca验证中心证明 A就是A

完整流程:

那么A直接把公钥发和证书给 B,B 通过 ca 验证A身份后后,通过A的公钥加密‘对称密钥’发给A,A用自己的私钥解密得到了‘对称密钥’,以后就通过对称加密方式交流

通讯流程

d465ecb93a38c78d1210de489b076792.webp

  • 浏览器发起请求

  • 服务器返回公钥+签名证书

  • 浏览器向CA认证中心询问证书是否有效

  • CA认证返回结果

  • 浏览器用公钥加密对称秘钥

  • 服务器用自己的私钥解密 对称称秘钥,发起通讯


http 请求方法

post 和 put 的区别

  • 都能新增和修改数据

  • put 无副作用,调用一次和多次效果相同,post 调用每次都会新增

  • post 面向资源集合,put 面向单资源

put 和 patch 的区别

他两都是同来更新数据的,而patch是同来更新局部资源比如某个对象只更改了某个字段则可以用 patch

常见状态码

  • 206 部分内容,代表服务器已经成功处理了部分GET请求,可以应用断点陆续上传

  • 301 永久重定向

  • 302 临时重定向

  • 304 命中协商缓存

  • 400 请求报文存在语法错误

  • 401 需求http认证

  • 403 请求资源被服务器拒绝

  • 404 服务端找不到资源

  • 500 服务端执行请求发送错误

  • 501 请求超出服务端能力范围(不支持某个方法)

  • 502 作为网关或者代理工作的服务器尝试执行请求时,从远程服务器接收到了一个无效的响应

  • 503 由于超载或系统维护,服务器暂时的无法处理客户端的请求


HTTP 缓存

http 缓存过程

  1. 客户端向服务端发起第一次请求资源

  • 服务端返回资源,并通过响应头返回缓存策略

  • 客服端根据缓存策略将策略和资源缓存起来

结论:

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识

  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

64be0665f05398031ff5583792feaaf1.webp

  1. 当客户端再次向服务端发起请求时

  • 客户端判断是否有缓存,有则判断是否存在 cache-control,并根据max-age 判断其是否已过期,没有过期直接读取缓存,返回200

  • 若已过期则携带缓存策略if-none-match发送到服务端

  • 服务端根据 etag 与if-none-match 相同则返回304继续使用缓存,更新新鲜度

  • 不相同则重新返回资源

19129c8d8350e043e2d7889c0e882ce4.webp

缓存位置

  • from memory cache代表使用内存中的缓存,
  • from disk cache则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为memory –> disk
  • 内存缓存(from memory cache):内存缓存具有两个特点,分别是快速读取和时效性:
  • 快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。
  • 时效性:一旦该进程关闭,则该进程的内存则会清空。
  • 硬盘缓存(from disk cache):硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行I/O操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢。
在浏览器中,浏览器会在js和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取(from memory cache);而css文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存(from disk cache)


Web安全防御

xss 跨域脚本攻击(利用网站对用户的信任)

  1. 非持久型 - url参数直接注入 脚本,伪造成正常域名诱惑用户点击访问(浏览器默认防范?)

  2. 持久型 - 存储到DB后读取时注入 (灾难)

两者都是通过脚本获取cookies 然后通过img或者ajax请求发送到黑客服务器 

防范手段:

  • html字符转译,或者只过滤script这些脚步

  • 限制资源请求来源csp

  • cookies 防范: value 如果用于保存用户登录态,应该将该值加密,不能使用明文的用户标识 http-only 不能通过 JS 访问 Cookie,减少 XSS 攻击
    Content-Security-Policy: img-src https://* 只允许加载 HTTPS 协议图片 Content-Security-Policy: default-src ‘self’ 只允许加载本站资源

  • 前端方案:设置 meta 标签的方式

Csrf 跨域请求伪造(利用用户对网站的信任):

即跨站请求伪造,是一种常见的Web攻击,它利用用户已登录的身份,在用户毫不知情的情况下,以用户的名义完成非法操作 在A网站没有退出的情况下,引诱用户访问第三方网站(评论发链接)

访问页面自动发起请求 第三方默认给A发起请求


防御:

  • Get 请求不对数据进行修改

  • 不让第三方网站访问到用户 Cookie

  • 阻止第三方网站请求接口

  • 请求时附带验证信息,比如验证码或者 Token


http 和 websocket 的区别

相同点主要

  1. 都是基于TCP的应用层协议;
  2. 都使用Request/Response模型进行连接的建立;
  3. 在连接的建立过程中对错误的处理方式相同,在这个阶段WS可能返回和HTTP相同的返回码;
  4. 都可以在网络中传输数据。

不同之处

  1. WS使用HTTP来建立连接,但是定义了一系列新的header域,这些域在HTTP中并不会使用;
  2. WS的连接不能通过中间人来转发,它必须是一个直接连接;
  3. WS连接建立之后,通信双方都可以在任何时刻向另一方发送数据;
  4. WS连接建立之后,数据的传输使用帧来传递,不再需要Request消息;
  5. WS的数据帧有序。

七、webpack

一、Webpack 是什么

Webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 Webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。核心概念:

  • 入口(entry)

    其指示 Webpack 应该用哪个模块,来作为构建其内部依赖图的开始,进入入口起点后,Webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。每个依赖项随即被处理,最后输出到称之为 bundles 的文件中。

  • 输出(output)

    output 属性告诉 Webpack 在哪里输出它所创建的bundles,以及如何命名这些文件,默认值为 ./dist。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。

  • module

    模块,在 Webpack 里一切皆模块,在Webpack中,CSS、HTML、js、静态资源文件等都可以视作模块,Webpack 会从配置的 Entry 开始递归找出所有依赖的模块,一 个模块对应着一个文件。

  • Chunk

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

  • loader

    loader 让 Webpack 能够去处理那些非 JavaScript 文件(Webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 Webpack 能够处理的有效模块,然后你就可以利用 Webpack 的打包能力,对它们进行处理。本质上,Webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块

  • 插件(Plugins)

    plugin 用来扩展Webpack的功能,其通过构建流程的特定时机注入钩子实现的,插件接口功能极其强大,给Webpack带来很大的灵活性。

二、Webpack 有什么特点

模块化,压缩,打包 具体作用:

  • 搭建开发环境开启服务器,监视文件改动,热更新
  • 通过建立依赖图把模块打包成一个或者多个chuck。
  • 通过loader 把将sass/less、图片、vue等文件转成Webpack可以识别的格式的文件
  • 能够通过插件对资源进行模块分离,压缩,整合等


三、webapck 打包流程

详细流程:

  1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。
  2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
  3. 确定入口:根据配置中的 entry 找出所有的入口文件。
  4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理。
  5. 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会。
  7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统

简单流程:

  1. 入口文件开始分析:

  • 哪些依赖文件
  • 转换代码


  1. 递归分析依赖代码

  • 哪些依赖文件
  • 转换代码
  1. 生成浏览器可以识别执行的bundle文件


四、Webapck 打包原理

  1. 通过fs.readFileSync读取入口文件,然后通过@babel/parser获取ast抽象语法树,借助@babel/core和 @babel/preset-env,把ast语法树转换成合适的代码最后输出一个文件对象,下面举个栗子:

打包入口文件为index.js:

import fn1 from './a.js'
fn1()

依赖文件a.js

const fn1 =()=>{
  console.log('1111111111')
}
export default fn1

生成文件对象

 {
   filename:'index.js,' // 文件路径
   dependencies:'./a.js',//依赖文件
   code:'\n\nvar _a = __webpack_require__(/*! ./a.js */ "./a.js");....' // 代码
  }

然后递归dependencies,最后生成:

 [{
   filename:'index.js,' // 文件路径
   dependencies:'./a.js',//依赖文件
   code:'\n\nvar _a = __webpack_require__(/*! ./a.js */ "./a.js");....' // 代码
  },{
   filename:'a.js,' // 文件路径
   dependencies:{},
   code:"\n\nObject.defineProperty(exports, "__esModule", ({\n    value: true\n}));...
  }
]
 

转换格式

 {
   'index.js':'\n\nvar _a = __webpack_require__(/*! ./a.js */ "./a.js");....',
   'a.js':' '\n\nObject.defineProperty(exports, "__esModule", ({\n    value: true\n}));...'
  }

  1. 由于生成的代码还包含了浏览器无法识别 require 函数,所以实现了一个__webpack_require__替换require来实现模块化,通过自执行函数传入index文件对象。流程是先通过eval函数运行index.js的代码,当遇到__webpack_require__时通过__webpack_modules__字典对象获取 a.js 的代码并且通过eval运行其代码。

详细看下图:

a79406305326cf921aad759c2580f0d3.webp

五、常见优化手段

  • MiniCssExtractPlugin插件:对CSS进行分离和压缩
  • happypack插件:HappyPack开启多个线程打包资源文件
  • DllPlugin、DllReferencePlugin插件:DllPlugin通过配置Webpack.dll.conf.js把第三方库:vue、vuex、element-ui等打包到一个bundle的dll文件里面,同时会生成一个名为 manifest.json映射文件,最后使用 DllReferencePlugin检测manifest.json映射,过滤掉已经存在映射的包,避免再次打包进bundle.js。
  • ParallelUglifyPlugin插件:开启多个子进程压缩输出的 JS 代码(Webpack4.0 默认使用了 TerserWebpackPlugin,默认就开启了多进程和缓存)
  • optimization.splitChunks:抽离公共文件
  • 其他:exclude/include配置、externals配置、使用cache-loader等

六、Webpack 常见面试题

  1. 热更新原理

    监听文件变动,通过websocket协议自动刷新网页(详细内容还没有深入研究)

  1. loader与plugin的区别

loader:loader是一个转换器是在 import 或"加载"模块时预处理文件,将A语言转成B语言,如 TypeScript转换为 JavaScript,less转成CSS,单纯的文件转换成浏览器可以识别的文件。
plugin:插件是一个扩展器,目的在于解决 loader 无法实现的其他事。
  1. 常用的loader和常见plugin有哪些

loader:

  • babel-loader:把 ES6 转换成 ES5
  • less-loader:将less代码转换成CSS
  • css-loader:加载 CSS,支持模块化、压缩、文件导入等特性
  • style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS
  • eslint-loader:通过 ESLint 检查 JavaScript 代码
  • vue-loader:加载 Vue.js 单文件组件
  • cache-loader: 可以在一些性能开销较大的 Loader 之前添加,目的是将结果缓存到磁盘里
  • file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 (处理图片和字体)
  • url-loader:与 file-loader 类似,区别是用户可以设置一个阈值,大于阈值时返回其 publicPath,小于阈值时返回文件 base64 形式编码 (处理图片和字体)


plugin:

  • CopyWebpackPlugin:将单个文件或整个目录复制到构建目录
  • HtmlWebapckPlugin:简单创建 HTML 文件,用于服务器访问
  • ParallelUglifyPlugin: 多进程执行代码压缩,提升构建速度
  • MiniCssExtractPlugin: 分离样式文件,CSS 提取为独立文件,支持按需加载 (替代extract-text-Webpack-plugin)
  1. 用过哪些可以提高效率的webapck插件

此答案请参考目录五、常见优化手段


  1. 实现一个简单的loader

实现一个替换源码中字符的loader

//index.js
console.log("hello");

//replaceLoader.js
module.exports = function(source{
  // source是源码
  return source.replace('hello','hello loader'
};

在配置文件中使用loader

//需要node模块path来处理路径 
const path = require('path')
module: {
rules: [ {
         test/\.js$/,
         use: path.resolve(__dirname,"./loader/replaceLoader.js")
}]
}

  1. Webpack插件原理,如何写一个插件

原理:在 Webpack 运行的生命周期中会广播出许多事件(run、compile、emit等),Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果

实现一个copy功能的Plugin

class CopyrightWebpackPlugin {
  //compiler:webpack实例
  apply(compiler) {
    //emit 生成资源文件到输出目录之前
    compiler.hooks.emit.tapAsync(
      "CopyrightWebpackPlugin",
      (compilation, cb) => {
        // assets目录输出copyright.txt
        compilation.assets["copyright.txt"] = {
          // 文件内容
          sourcefunction() {
            return "hello copy";
          },
          // 文件大小
          sizefunction() {
            return 20;
          }
        };
        // 完成之后 走回调,告诉compilation事情结束
        cb();
      }
    );
    // 同步的写法;
    compiler.hooks.compile.tap("CopyrightWebpackPlugin", compilation => {
      console.log("开始了");
    });
  }
}
module.exports = CopyrightWebpackPlugin;

使用CopyrightWebpackPlugin插件

const CopyrightWebpackPlugin = require("./plugins/copyright-webpack-plugin");
plugins: [
    new CopyrightWebpackPlugin()
  ]
  1. Webpack的require是如何如何查找依赖的

  • 解析相对路径:查找相对当前模块的路径下是否有对应文件或文件夹是文件则直接加载,是文件夹则继续查找文件夹下的 package.json 文件
有 package.json 文件则按照文件中 main 字段的文件名来查找文件 无 package.json 或者无 main 字段则查找 index.js 文件
  • 解析模块名:查找当前文件目录下,父级目录及以上目录下的 node_modules 文件夹,看是否有对应名称的模块
  • 解析绝对路径:直接查找对应路径的文件

八、性能优化

性能优化思路

  1. 性能监控:lighthouse 插件 和浏览器 performance 工具
  2. 网络层面性能优化
  • 减少HTTP请求次数(缓存、本地存储)
  • 减少HTTP请求文件体积(文件压缩)
  • 加快HTTP请求速度(CDN)
  1. 渲染层面性能优化
  • DOM 优化
  • CSS 优化
  • JS 优化
  • 首屏渲染
  • 服务端渲染


网络层面性能优化

减少HTTP请求次数

  1. 浏览器缓存
  • Memory Cache
  • HTTP Cache
  • Service Worker Cache
  • Push Cache
  1. 本地存储
  • Web Storage(容量5-10M)
  • indexdb 运行在浏览上的非关系型数据库(容量>100M)
  • Cookies (容量4KB)


浏览器缓存--Memory Cache
MemoryCache,是指存在内存中的缓存。从优先级上来说,它是浏览器最先尝试去命中的一种缓存。从效率上来说,它是响应速度最快的一种缓存。Memory Cache一般存储体积较小的文件。

浏览器缓存--HTTP Cache
设置HTTP Header里面缓存相关的字段,实现HTTP缓存

  • 强缓存:Cache-Control,expires

Cache-Control 的几个取值含义:

private: 仅浏览器可以缓存(HTML文件可以设置避免CDN缓存)
public: 浏览器和代理服务器都可以缓存
max-age=xxx: 过期时间
no-cache: 不进行强缓存
no-store:禁用任何缓存,每次都会向服务端发起新的请求
在设置强缓存情况下,请求返回Response Headers Cache-Control的值,如果有max-age=xxx秒,则命中强缓存,如果Cache-Control的值是no-cache,说明没命中强缓存,走协商缓存
  • 协商缓存:Etag/If-None-Match,Last-Modified/If-Modified-Since
ETag:每个文件有一个,改动文件了就变了,可以看似md5
Last-Modified:文件的修改时间
协商缓存触发条件:Cache-Control 的值为 no-cache (不强缓存)或者 max-age 过期了 (强缓存,但总有过期的时候)

协商缓存步骤总结:在设置协商缓存情况下,在第一次请求的时候,返回的Response Headers会带有Etag/Last-Modified值。当再次请求没有命中强制缓存的时候,这个时候我们的Request Headers就会携带If-None-Match/If-Modified-Since字段,它们的值就是我们第一次请求返回给我们的Etag/Last-Modified值。服务端再拿Etag和If-None-Match,Last-Modified和If-Modified-Since来进行比较较,如果相同(命中就直接使 缓存),返回304,浏览器读取本地缓存,如果不同(没有命中)则返回200,再重新从服务端拉取新的资源。 

浏览器缓存-Service Worker Cache

Service Worker 是一种独立于主线程之外的 Javascript 线程。它脱离于浏览器窗体,因此无法直接访问 DOM。这样独立的个性使得 Service Worker 的“个人行为”无法干扰页面的性能,这个“幕后工作者”可以帮我们实现离线缓存、消息推送和网络代理等功能。我们借助 Service worker 实现的离线缓存就称为Service Worker Cache ,其实现基于HTTPS。


本地存储--Web Storage

Web Storage 又分Local Storage 和 Session Storage,特色容量大5-10M它们的区别是Local Storage 永久缓存除非手动删除,Session Storage浏览器窗口关闭即失效。


减少HTTP请求文件体积

  1. 图片优化
  • 压缩图片
性能优化最有效果主要是图片压缩,因为图片的体积比较大,压缩图片效果比较显著。压缩工具 tinypng.com/
  • 使用webP
webP支持透明、支持动态、体积小,缺点是兼容性不好
  • 大图尽量用JPG代替PNG
  • 使用 CSS Sprites雪碧图(HTTP2不需要)
  • 使用iconfont(icon首选)
  • 使用base64(小图解决方案)
  • 复杂图形地图等使用SVG
  • 图片懒加载
  1. 资源打包压缩(webpack打包抽离公共CSS/JS)
  • MiniCssExtractPlugin插件:对CSS进行分离和压缩
  • optimization.splitChunks:抽离公共JS文件
  • optimization.minimizer :文件压缩
  1. 服务端开启gzip压缩
accept-encoding:gzip

Gzip 压缩背后的原理,是在一个文本文件中找出一些重复出现的字符串、临时替换它们,从而使整个文件变小。根据这个原理,文件中代码的重复率越高,那么压缩的效率就越高,使用 Gzip 的收益也就越大,反之亦然。开启Gzip省下了一些传输过程中的时间开销,而其代价是服务器压缩的时间开销和 CPU 开销(以及浏览器解析压缩文件的开销)。

使用CDN(内容分发网络)

CDN (Content Delivery Network,即内容分发网络)指的是一组分布在各个地区的服务器。这些服务器存储着数据的副本,因此服务器可以根据哪些服务器与用户距离最近,来满足数据的请求。CDN 提供快速服务,较少受高流量影响,

CDN 是静态资源提速的重要手段。


渲染层次性能优化

DOM 优化

1.减少DOM操作
  • 使用虚拟DOM
  • 使用事件委托
  • 使用DOM Fragment
DocumentFragment 接口表示一个没有父级文件的最小文档对象。它被当做一个轻量版的 Document 使用,用于存储已排好版的或尚未打理好格式的XML片段。因为 DocumentFragment 不是真实 DOM 树的一部分,它的变化不会引起 DOM 树的重新渲染的操作(reflow),且不会导致性能等问题。
  • 缓存DOM结点
// 只获取一次container
let container = document.getElementById('container')
for(let count=0;count<10000;count++){ 
  container.innerHTML += '<span>我是一个小测试</span>'

  • 减少使用iframe


JS 优化

  • 使用防抖截流

// 防抖:定时执行
function debounce(fn,wait=2000{
  let setTime
  return function(...args{
    if(setTime) clearTimeout(setTime)
     setTime = setTimeout(function() {
       fn.apply(null, args)
       clearTimeout(setTime)
     }, 2000)
  }
}
// 节流:规定时间内只取最后一次执行
 function throttle(fn,wait=3000{
         let start= new Date()
            return function(...args{
            if(new Date()- start> wait){
              start= new Date()
              console.log('args', args)
              fn.apply(null, args)
            }
        }
    }  
  • 避免使用全局变量

  • 避免内存泄漏

  • 删除多余代码

Tree-Shaking:ES6的mudule 已经实现的Tree-Shaking,在Webpack打包中会把冗余模块剔除掉栗子:

import { add1, add2 } from './pages'
console.log(add1)
// 本栗子add2模块代码不会被打包bundle文件


CSS 优化

  1. 有效使用选择器,避免使用calc表达式、CSS滤镜
  2. 减少重排和重绘
  • 布局优先使用flex
  • 尽量避免使用getBoundingClientRect,getComputedStyle等属性
  1. 触发渲染层(transform: translateZ(0);backface-visibility: hidden;)

服务端渲染

服务器把需要的组件或页面渲染成 HTML 字符串,然后把它返回给客户端。客户端拿到手的,是可以直接渲染然后呈现给用户的 HTML 内容,不需要为了生成 DOM 内容自己再去跑一遍 JS 代码。

const Vue = require('vue')
// 创建 个express应 
const server = require('express')()
// 提取出renderer实 
const renderer = require('vue-server-renderer').createRenderer()
server.get('*', (req, res) => { // 编写Vue实 (虚拟DOM节点) const app = new Vue({
    data: {
      url: req.url
},
// 编写模板HTML的内容
template`<div>访问的 URL 是: {{ url }}</div>` })
// renderToString 是把Vue实 转化为真实DOM的关键 法 renderer.renderToString(app, (err, html) => {
    if (err) {
      res.status(500).end('Internal Server Error')
      return
}
// 把渲染出来的真实DOM字符 插 HTML模板中 res.end(`
      <!DOCTYPE html>
      <html lang="en">
        <head><title>Hello</title></head>
        <body>${html}</body>
      </html>

`) })
})
server.listen(8080)


首屏渲染

  1. 内联首屏关键CSS
  2. 异步加载CSS(动态插入)
  3. JS放body 底部,或者异步加载js(defer、async)
defer、async 都不会阻塞页面渲染,他们的区别是async编译完成立刻执行,defer编译完成同时还要等整个文档解析完成、DOMContentLoaded 事件即将被触发才执行
  1. 预加载 preload
<link rel="preload" href="index.js" as="script">
  1. 页面loading动画
  2. 骨架图
使用page-skeleton-webpack-plugin插件

vue 层面优化

  1. 引入生产环境的 Vue 文件
  2. 使用单文件组件预编译模板
  3. 提取组件的 CSS 到单独到文件
  4. 利用Object.freeze()提升性能
  5. 扁平化 Store 数据结构
  6. 组件懒加载
  7. 路由懒加载
  8. 虚拟滚动


九、Vue

Vue 双向绑定原理

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

数据劫持

遍历 data 属性通过Object.defineproprety 劫持 setter 和 getter


vue 依赖收集过程

  1. new wacther 时,通过 pushTarget 把该 Dep.target 指向改 wacther,在 render 执行渲染 Vnode 过程中触发了 getter

function pushTarget (_target: ?Watcher{
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

updateComponent = () => {
 vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
    callHook(vm, 'beforeUpdate')
   }
}}, true /* isRenderWatcher */)
  1. getter 触发 dep.depend() 即执行 Dep.target.addDep(this) ,即执行wacther 里面的 addDep 方法

// Dep 类
depend () {
 if (Dep.target) { // Dep.target =》watcher
  Dep.target.addDep(this// this =》dep
 }
}
  1. dep作为参数在 addDep 里通过map has 判断 dep 的 id 是否已经存在了 dep,如果没有就压入栈 (过滤重复的dep)

  2. 后执行 刚才传入的 dep 的 addSub 方法收集wacther

// Watcher 类
addDep (dep: Dep) {
 const id = dep.id
 if (!this.newDepIds.has(id)) {
 this.newDepIds.add(id)
 this.newDeps.push(dep)
 if (!this.depIds.has(id)) { // 防止重复收集 dep
    dep.addSub(this// dep 收集 watcher
   }
}}

watch 和 computd 的区别

  • computd 在getter执行了之后会缓存

  • computd 适合比较耗性能的计算场景

  • watch 更多是监听,监听某个变化而执行回调

Vue key 的作用

key 是给每一个 vnode 的唯一 id,可以依靠 key,更准确, 更快的拿到 oldVnode 中对应的 vnode 节点。

  • 更准确

因为带 key 就不是就地复用了,在sameNode函数 a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。

  • 更快

利用 key 的唯一性生成map对象来获取对应节点,比遍历方式更快

不带 key 时或者以 index 作为 key 时:比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位


Vue nextTick 原理

  • 它可以在 DOM 更新完毕之后执行一个回调,使我们可以操作更新后的 dom
  • 可以监听 dom 的变化就是 h5 新特性的 mutationObserver,但是 Vue 并不是通过监听 dom 变化的方式实现的
  • 而是通过 eventloop 原理,因为 eventloop 的 task 执行完后(完成一次事件循环)进行一次 DOM 更新
  • 而完成一个 task 分界点就是 微任务完成 所以 Vue 首先是用 promise.then 然后就是 mutationObserver。
  • 为了兼容性然后降级到宏任务 setImmediate 和 setTimeout
// timerFunc 收集异步 task 事件
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let timerFunc

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterDatatrue
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// 用 callbacks 收集回调函数
export function nextTick (cb?: Function, ctx?: Object{
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

diff 算法流程(patch)

    第一次走 createElm 生成真实 dom,以后通过 sameVnode 判断同结点则进行 patchVNode 结点的 child ,如果新旧结点都存在则走 updateChildren 流程,不断通过新旧头头、尾尾、交叉 sameVnode 对,都比对不成功 ,则直接拿key去映射表查找,如找到有相同的 key 结点则进行 sameVnode 对比,成立则又进入下子结点的patchVNode,直到遍历完成。

add8e1d56b6b7924a60aa73a5bb1658a.webp

new vue() 流程

06c06bed6e35f392b6980bba2a9e2f43.webp


Vuex 有什么特点

    首先说明 Vuex 是一个专为 Vue 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 核心概念重点同步异步实现 action mutation


Vue 组件为什么 data 不能是对象

Vue 组件可能存在多个实例,如果使用对象形式定义 data,则会导致它们共用一个data对象,那么状态变更将会影响所有组件实例,

mvvm 的理解

通过指令的方式实现 model 和 view 主动数据响应和view更新

Vue 有哪些性能优化

  • 路由懒加载
  • keep-alive 缓存页面
  • 用 v-show 复用dom
  • 长列表纯数据展示用 Object.freeze 冻结
  • 长列表大数据用虚拟滚动
  • 事件及时销毁
  • 服务端渲染 ssr


服务端渲染原理

在客户端请求服务器的时候,服务器到数据库中获取到相关的数据,并且在服务器内部将Vue组件渲染成HTML,并且将数据、HTML一并返回给客户端,这个在服务器将数据和组件转化为HTML的过程,叫做服务端渲染SSReed765dc7b3459e7f67c11171132477a.webp

使用SSR的好处

352957c310fba4bd570b00df26e8108b.webp

  1. 有利于SEO

其实就是有利于爬虫来爬你的页面,因为部分页面爬虫是不支持执行JavaScript的,这种不支持执行JavaScript的爬虫抓取到的非SSR的页面会是一个空的HTML页面,而有了SSR以后,这些爬虫就可以获取到完整的HTML结构的数据,进而收录到搜索引擎中。
  1. 白屏时间更短

相对于客户端渲染,服务端渲染在浏览器请求URL之后已经得到了一个带有数据的HTML文本,浏览器只需要解析HTML,直接构建DOM树就可以。而客户端渲染,需要先得到一个空的HTML页面,这个时候页面已经进入白屏,之后还需要经过加载并执行JavaScript、请求后端服务器获取数据、JavaScript 渲染页面几个过程才可以看到最后的页面。特别是在复杂应用中,由于需要加载 JavaScript 脚本,越是复杂的应用,需要加载的 JavaScript 脚本就越多、越大,这会导致应用的首屏加载时间非常长,进而降低了体验感。


Vue3 有什么新特性

  • 数据劫持:用 proxy 做代理
  • 虚拟 dom 重构:v2会把静态结点转成vdom,新只会构造动态结点的vdom
  • 将大多数全局API和内部组件移至ES模块导出,tree-shaking更友好
  • 支持了 ts
  • Composition api:新增 setup 函数有利于代码复用(替代mixin,mixin会容易存在命名冲突)


Vue2 响应式弊端

    响应化过程需要递归遍历,消耗较大新加或删除属性无法监听数组响应化需要额外实现Map、Set、Class等无法响应式修改语法有限制


生命周期2.x与Composition之间的映射关系

  • beforeCreate -> use setup()
  • created -> use setup()
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured

Vue 组件通信

props★★(父传子)emit/emit/emit/on★★事件总线(跨层级通信)vuex★★★(状态管理常用皆可)优点:一次存储数据,所有页面都可访问parent/parent/parent/children(父=子项目中不建议使用)缺点:不可跨层级attrs/attrs/attrs/listeners(皆可如果搞不明白不建议和面试官说这一种)provide/inject★★★(高阶用法=推荐使用)优点:使用简单 缺点:不是响应式


v-model vs .sync

区别:

一个组件可以多个属性用.sync修饰符,可以同时"双向绑定多个“prop”,而并不像v-model那样,一个组件只能有一个

使用场景:

v-model针对更多的是最终操作结果,是双向绑定的结果,是value,是一种change操作

sync针对更多的是各种各样的状态,是状态的互相传递,是status,是一种update操作

v-model 可以在只需子组件更新父组件的场景使用如:

// 父组件接收
<CreativityGroup :planTemplateModel.sync="planParams"/> 
 // 子组件传值
@Watch('formModle', { deeptrue })
  formModleChange(nVal) {
    this.$emit('input', nVal)
  }

v-model vs .sync 使用例子

<template>
  <div>
    my myParam {{ value }}<br />
    paramsync {{ paramsync }}
    <button type="button" @click="change">change my</button>
  </div>

</template>

<script>
export default {
  props: {
    value: {
      type: String,
      default: ""
    },
    paramsync: {
      type: Number,
      default: 0
    }
  },
  computed: {
    value1: {
      set(val) {
        this.$emit("input", val);
      },
      get() {
        return this.value;
      }
    }
  },
  methods: {
    change() {
      this.value1 = "rtrtr";
      console.log("this.value", this.value);
    //   this.paramsync = 78;
    // this.$emit('input','更新之后')
      this.$emit("update:paramsync",5555);
    }
  }
};
</
script>

Vue 生命周期

beforeCreate:在实例初始化之后,数据观测(data observe)和event/watcher事件配置之前被调用,这时无法访问data及props等数据;


created:在实例创建完成后被立即调用,此时实例已完成数据观测(data observer),属性和方法的运算,watch/event事件回调,挂载阶段还没开始,$el尚不可用。


beforemount: 在挂载开始之前被调用,相关的render函数首次被调用。


mounted:实例被挂载后调用,这时el被新创建的vm.el替换,若根实例挂载到了文档上的元素上,当mounted被调用时vm.el替换,若根实例挂载到了文档上的元素上,当mounted被调用时vm.el替换,若根实例挂载到了文档上的元素上,当mounted被调用时vm.el也在文档内。注意mounted不会保证所有子组件一起挂载。


beforeupdata:数据更新时调用,发生在虚拟dom打补丁前,这时适合在更新前访问现有dom,如手动移除已添加的事件监听器。


updated:在数据变更导致的虚拟dom重新渲染和打补丁后,调用该钩子。当这个钩子被调用时,组件dom已更新,可执行依赖于dom的操作。多数情况下应在此期间更改状态。如需改变,最好使用watcher或计算属性取代。注意updated不会保证所有的子组件都能一起被重绘。


beforedestory:在实例销毁之前调用。在这时,实例仍可用。


destroyed:实例销毁后调用,这时 Vue 实例的所有指令都被解绑,所有事件监听器被移除,所有子实例也被销毁。


Vue compile 过程

编译过程整体分为解析、优化和生成


解析 - parse

解析器将模板解析为抽象语法树,基于AST可以做优化或者代码生成工作

优化- optimize

优化器的作用是在AST中找出静态子树并打上标记。静态子树是在AST中永远不变的节点,如纯文本节点。标记静态子树的好处:每次重新渲染,不需要为静态子树创建新节点虚拟DOM中patch时,可以跳过静态子树

代码生成- generate

将AST转换成渲染函数中的内容,即代码字符串。

Vue2 vs Vue3

Vue2 响应式弊端:响应化过程需要递归遍历,消耗较大新加或删除属性无法监听数组响应化需要额外实现Map、Set、Class等无法响应式修改语法有限制

十、React

React 是什么?

用于构建界面的 javascript 库

特点:

  • 声明式

  • 组件式

优点:

  • 开发团队和社区非常强大

  • api 简洁

缺点:

  • 没有官方系统解决方案,选型成本高

  • 过于灵活,对代码设计要求高

jsx 是 React.createElement 的语法糖


React 的 class 组件和函数组件的区别

相同:都可以接收 props 并返回 react 对象

不同:

  • 编程思想和内存:类组件需要创建实例面向对象编程,它会保存实例,需要一定的内存开销,而函数组件面向函数式编程,可节约内存

  • 可测试性:函数式更利用编写单元测试

  • 捕获特性:函数组件具有值捕获特性(只能得到渲染前的值)

  • 状态:class 组件定义定义状态,函数式需要使用 useState

  • 生命周期:class 组件有完整的生命周期,而组件没有,可以用useEffect 实现类生命周期功能

  • 逻辑复用:类组件通过继承或者高阶组件实现逻辑复用,函数组件通过自定义组件实现复用

  • 跳过更新:类组件可以通过shouldComponents 和 PureComponents(浅比较,深比较可以用immer) 来跳过更新,函数组件react.memo 跳过更新

  • 发展前景:函数组件将成为主流,因为他更好屏蔽this问题,和复用逻辑,更好的适合时间分片和并发渲染

React 设计理念

  • 跨平台渲染=>虚拟dom

  • 快速响应=>异步可中断(fiber)+增量更新


fiber

fiber 是一个执行单元,每次执行完成一个执行单元,react 会检测当前帧还剩多少时间,如果没有时间就将控制器让出去

fiber 是一种数据结构

  • react 目前的做法使用链表,每个vdom结点内部表示为一个fider

  • 从顶点开始遍历

  • 如果有第一个儿子,则先遍历第一个儿子

  • 如果没有第一个儿子,则标志此结点遍历完成

  • 如果有弟弟则遍历弟弟

  • 如果没有弟弟,则返回父节点标志父节点遍历完成,如果有叔叔则遍历叔叔

  • 没有叔叔则遍历结束

(儿子=〉弟弟=〉叔叔)

494cb72562b181713a5dc321e27c5ffc.webp

fiber 出现背景

React15 架构不能支撑异步更新,当渲染出现大量的组件递归时间超过 16.7ms 就会出现卡帧

React16 架构可以分为三层:

  • Scheduler(调度器类似 requestIdleCallback 功能)— 调度任务的优先级,高优任务优先进入Reconciler

  • Reconciler(协调器)— 负责找出变化的组件

  • Renderer(渲染器)— 负责将变化的组件渲染到页面上


react 渲染过程分为三步骤

  • 调度

  • 调和

  • 提交(不可暂停)

fe3a0f055a4103a1b74d48bf526ff26c.webp

fiber 实现关键原理

  • 基于requestIdleCallback,获取当前一帧还有剩余时间deedline,(将在浏览器的空闲时段内调用的函数排队)

  • 由于兼容性实际是用Messagechannel + requestAnimationFrame 模拟requestIdleCallback

effect 副作用,表示将要对一个dom 元素进行操作,副作用链就是子孙后代,作用倒挂fiber构建dom结点


fiber 的作用

  • 能够把可中断的任务切片处理。

  • 能够在父元素与子元素之间交错处理,以支持 React 中的布局。

  • 能够在render()中返回多个元素。

  • 更好地支持错误边界。

原理利用 requestIdleCallback 函数将在浏览器器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执⾏行行后台和低优先级⼯工作,⽽而不不会影响延迟关键事件,如动画和输⼊入响应。


fiber 递归流程

function performUnitOfWork(fiber{
  // 执行beginWork

  if (fiber.child) {
    performUnitOfWork(fiber.child);
  }

  // 执行completeWork

  if (fiber.sibling) {
    performUnitOfWork(fiber.sibling);
  }
}

React 与 Vue 的区别

相同:

  • 都是前端界面实现 javascript 库
  • 都可以实现数据驱动模版更新,而不需直接操作dom,核心都是 vdom
不同:
  • Vue通过数据劫持,当数据改动时,界面就会自动更新,而React里面需要调用方法setState。
  • Vue 的更新的颗粒度是当前组件,而React除了当前组件还包括子组件(会有性能瓶颈)
  • 在设计上,Vue 数据和模版和方法是分开的,而 React 不分开,Vue 会有很多的指令,React 只有 js
  • 从 diff 上,当 Vue 的className不一致是认为不同的结点,而 React 认为是同一个结点,Vue 有头头,尾尾,交叉对比,而 React 没有。
  • React16 之后版本有 fider 之后可以实现异步切片更新
  • React 难得是它没有类似vue现成的全家桶技术栈,需要自己去选择和衡量,这就需要时间去踩坑,而且 React 周边使用起来并不太简洁明朗,需要了解所选周边的编写方式和最佳实践,这就耗费时间和增加入门门槛了
  • React 的生态比 Vue 完善


hook

Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数,可以使你在函数组件使用状态。


setState 是异步还是同步

React的setState本身并不是异步的,是因为其批处理机制给人一种异步的假象。

【React的更新机制】

生命周期函数和合成事件中:

  1. 无论调用多少次setState,都不会立即执行更新。而是将要更新的state存入'_pendingStateQuene',将要更新的组件存入'dirtyComponent';

  2. 当根组件didMount后,批处理机制更新为false。此时再取出'_pendingStateQuene'和'dirtyComponent'中的state和组件进行合并更新;

原生事件和异步代码中:

  1. 原生事件(原生js绑定的事件)不会触发react的批处理机制,因而调用setState会直接更新;

  2. 异步代码中调用setState,由于js的异步处理机制,异步代码会暂存,等待同步代码执行完毕再执行,此时react的批处理机制已经结束,因而直接更新。

总结:
react会表现出同步和异步(setTimeout/setInterval)的现象,但本质上是同步的,是其批处理机制造成了一种异步的假象。(其实完全可以在开发过程中,在合成事件和生命周期函数里,完全可以将其视为异步)


React 合成事件

16 事件流(没有分别注册捕获和冒泡事件 bug )

bac9ffbfa5b0723795939aca774990c7.webp

3b3fe8297215bb89d7045bb81c8ba17d.webp

bug :点击弹窗没反应

e01f379fb9a1ae2af080a4a8513d0c83.webp

原因:点击事件直接冒泡到 document 原生事件了,state 变成了 false

解决:

68d7a55a44dd045903a4574a2155bfa9.webp

原理:

39b8733457c1f91c6e4a7abfb02a9409.webp

17 事件流

事件委托不再是 document 而是 挂载点容器,可以让一个页面可以使用多个 React 版本

c8b4a3f52c905a71e9decabb255a7e66.webp

不挂载到 document ,结点变成父子关系所以,stopPropagation(阻止冒泡) 生效


redux

什么时候使用redux:

  • 某个组件的状态,需要共享

  • 某个状态需要在任何地方都可以拿到

  • 一个组件需要改变全局状态

  • 一个组件需要改变另一个组件的状态   

原文:https://juejin.cn/user/2524134427068199/posts

十一、总结

    在我们阅读完官方文档后,我们一定会进行更深层次的学习,比如看下框架底层是如何运行的,以及源码的阅读。    这里广东靓仔给下一些小建议:
  • 在看源码前,我们先去官方文档复习下框架设计理念、源码分层设计
  • 阅读下框架官方开发人员写的相关文章
  • 借助框架的调用栈来进行源码的阅读,通过这个执行流程,我们就完整的对源码进行了一个初步的了解
  • 接下来再对源码执行过程中涉及的所有函数逻辑梳理一遍

关注我,一起携手进阶

欢迎关注前端早茶,与广东靓仔携手共同进阶~

浏览 28
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报