前端知识体系

JavaScript篇

原型和原型链

实例对象的__proto__和其构造函数的prototype和其是指向同一个地方的,这个地方叫做原型对象
__proto__的路径就叫原型链。
在这里插入图片描述

JavaScript深入之从原型到原型链

闭包

上级作用域内变量的生命周期,因为被下级作用域内引用,而没有被释放。

应用场景

  1. 函数作为一个函数的参数,或函数作为一个函数的返回值
  2. 防抖、节流

存在问题:内存泄漏

闭包导致了内部定义的函数引用数始终为1,垃圾回收机制就无法把它销毁。

防抖、节流

防抖

指定时间间隔内,频繁触发一个事件,只会执行最后一次事件。如防止重复点击查询按钮触发事件等。

源码实现:

function debouce (fn, time) {
    let args = arguments
    let timer = null
    return function () {
        if (timer) {
            clearTimeout(timer)
        }
        timer = setTimout(() => {
            fn.call(this, args)
        }, time)
    }
}

为什么防抖要用到闭包?

  1. 把timer变量放在内部函数中不能达到防抖的目的,因为每次执行内部的函数,都会创建一个新的timer变量;
  2. 用立即执行函数创造闭包可以实现防抖,是因为每次执行内部的函数,都是在操作同一个timer变量,由外部立即执行函数创建的timer变量。

节流

指定时间间隔内,只会执行一次事件。如下拉刷新。

源码实现:

function throttle (fn, time) {
    let args = arguments
    let canRun = true
    return function () {
        if (!canRun) return
        canRun = false
        setTimeout(() => {
            fn.call(this, args)
        }, time)
    }
}

this的指向

在绝大多数情况下,函数的调用方式决定了this的值(运行时绑定),this永远指向最后调用它的那个对象。

  1. 全局的this非严格模式指向window对象,严格模式指向undefined
  2. 对象的属性方法中的this指向对象本身
  3. apply、call、bind可以变更this指向为第一个传参
  4. 箭头函数中的this指向它的父级作用域,它本身不存在this

改变this的指向

  • 使用ES6的箭头函数
  • 在函数内部使用_this=this
  • 使用applycallbind
  • new实例化一个对象

this、apply、call、bind详细讲解文章

箭头函数

  1. 箭头函数内的this指定的是函数定义的对象,而不是函数执行时所在的对象;
  2. 箭头函数不能用作构造函数,因为它没有自己的this,无法实例化;
  3. 箭头函数不存在arguments对象
// ES5
var add = function (a, b) {
    return a + b;
};
// 使用箭头函数
var add = (a, b) => a + b;

// ES5
[1,2,3].map((function(x){
    return x + 1;
}).bind(this));
    
// 使用箭头函数
[1,2,3].map(x => x + 1);

细节:当你的函数有且仅有一个参数的时候,是可以省略掉括号的。当你函数返回有且仅有一个表达式的时候可以省略{} 和 return;

apply,call,bind的区别

相同点

  • 都是改变this指向的
  • 第一个参数都是this要指向的对象
  • 都可以利用后续参数传参;

区别

  • call和bind的参数是依次传参,一一对应
  • apply只有两个参数,第二个参数为数组
  • call和apply都是对函数直接调用,而bind方法返回的是一个函数
a.say.call(b,'男','学习');
a.say.apply(b,['男','学习'])
bind可以向cally一样传参:
例如:
a.say.bind(b,'男','学习')();
但由于bind返回的仍然是一个函数,所以我们还可以在调用的时候再进行传参。
例如:
a.say.bind(b)('男','学习');

浏览器的事件循环

  • JavaScript是单线程语言
  • Event Loop是JavaScript执行机制
    宏任务:整体script、setTimeout()、setInterval()、setImmediate()、I/O、UI渲染、用户交互操作
    微任务:Promise.then() catch()、async/await、process.nextTick(node)、new MutationObserver()
    宏任务和微任务都是异步任务。宏任务是由宿主(浏览器/node)发起的,微任务是由js引擎发起的。
    执行顺序:同步代码(主线程)➡微任务➡DOM渲染➡宏任务
    在这里插入图片描述
    解读:
  • 同步和异步任务分别进入不同的执行“场所”,同步的进入主线程,异步的进入Event Table并注册函数
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)

Event Loop执行

Event Loop中,每一次循环称为tick,每一次tick的任务如下:

  1. 执行栈(Call Stack)选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
  2. 检查是否存在微任务,有则会执行微任务队列为空
  3. 如果宿主为浏览器,可能会渲染页面
  4. 开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)

JavaScript中数据在栈和堆中的存储方式

  1. :基本数据类型(大小固定且操作简单):String、Number、Boolean、Null、Undefined、Symbol,先进后出
  2. :引用数据类型(大小不确定,将它们放入堆内存中,在申请内存时确定大小):Object、Array、Function、正则(RegExp)和日期(Date),堆可以被看成是一棵树,先进先出
  3. 栈的效率高于堆
  4. 栈内存中变量在执行环境结束后会立即进行垃圾回收,而堆内存中需要变量的所有引用都结束才会被回收

v8垃圾回收

「硬核JS」你真的了解垃圾回收机制吗

标记清除算法

  1. 垃圾收集器在运行时会给内存中的所有变量都加上一个标记,假设内存中所有对象都是垃圾,全标记为0
  2. 然后从各个根对象开始遍历,把不是垃圾的节点改成1
  3. 清理所有标记为0的垃圾,销毁并回收它们所占用的内存空间
  4. 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收

分代式垃圾回收

新老生代

新生代的对象为存活时间较短的对象,简单来说就是新产生的对象,通常只支持1~8M的容量,而老生代的对象为存活时间较长或常驻内存的对象,简单来说就是经历过新生代垃圾回收后还存货下来的对象,容量通常比较大。
在这里插入图片描述

新生代垃圾回收

新生代采用空间换时间的scavenge算法:整个空间分为两块,变量仅存在其中一块,回收的时候将存活变量复制到另一块空间,不存活的回收掉,周而复始轮流操作

老生代垃圾回收

老生代使用标记清楚和标记整理,标记清除:遍历所有对象标记可以访问到的对象(活着的),然后将不活的当作垃圾进行回收。回收完后避免内存的断层不连续,需要通过标记整理将活着的对象往内存一端进行移动,移动完成后再清理边界内存。

JS、CSS是否阻塞DOM渲染和解析

  1. CSS的加载不会阻塞DOM解析,因为CSS解析成CSSOM树和DOM tree是两个并行的进程;但是会阻塞DOM渲染,因为CSS会阻塞render tree的生成,进而会阻塞DOM的渲染
  2. JS的加载和执行会阻塞DOM的解析和渲染
  3. CSS的加载会阻塞JS的执行,CSS的渲染GUI线程和JS运行线程互斥,所以说CSS的加载也会阻塞DOM渲染
  4. 浏览器遇到<script>标签且没有defer或async属性时会触发渲染,以便获得最新的样式给JS代码

async和defer的区别

一般情况下,当执行到script标签时会进行下载+执行两步操作,这两步会阻塞HTML的解析;
async和defer能将script的下载阶段变成异步执行(和html解析同步进行)
async下载完成后立即执行js,此时会阻塞HTML解析;
defer会等全部HTML解析完成且在DOMContentLoaded事件之前执行;

浏览器事件机制

DOM事件流三阶段:

  1. 捕获阶段:事件最开始由不太具体的节点最早接受事件,而最具体的节点(触发节点)最后接受事件。为了让事件到达最终目标之前拦截事件。
    比如点击一个div,则click事件会按这种顺序触发:document=><html>=><body>=><div>,即由document捕获后沿着DOM树依次向下传播,并在各节点上触发捕获事件,直到到达实际目标元素。
  2. 目标阶段
    当事件到达目标节点,事件就进入了目标阶段。事件在目标节点上被触发(执行事件对应的函数),然后会逆向回流,直到传播至最外层的文档节点。
  3. 冒泡阶段
    事件在目标元素上触发后,会继续随着DOM树一层层往上冒泡,直到到达最外层的根节点。
    所有事件都要经历捕获阶段和目标阶段,但有些事件会跳过冒泡阶段,比如元素获得焦点focus和失去焦点blur不会冒泡。
    阻止事件冒泡:e.preventDefault()

new的过程

  1. 创建一个空的简单JavaScript对象(即{});
  2. 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象;
  3. 将步骤1新创建的对象作为this的上下文;
  4. 如果该函数没有返回对象,则返回this

变量提升

函数和变量在执行之前都提升到了代码开头。
函数提升优先级高于变量提升,且不会被同名变量声明时覆盖,但是会被同名变量赋值后覆盖。
实际上变量和函数声明在代码里的位置是不会改变的,而且是编译阶段在JavaScript引擎放入内存中。

	console.log(a); //undefined
	var a = 1;

因为有变量提升的缘故,上面代码的实际运行顺序为:

var a;
console.log(a);
a = 1;

promise

  1. promise是js异步编程的新的解决方案
  2. 是一个构造函数
  3. 用来封装一个异步操作,并可以获得结果

promise三个状态

  1. pending(进行中)
  2. resolved(成功)
  3. rejected(失败)

一旦状态改变,就不会再变。

promise api

  1. promise.all()
    所有promise都完成后执行,如果有一个promise执行失败,则promise.all()会立即返回rejected状态。
  2. promise.allSettled()
    所有promise都有结果了就执行,不论是成功还是失败。
  3. promise.any()
    任意一个promise成功了就执行。
  4. promise.race()
    返回一个新的promise, 第一个完成的promise的结果状态就是最终的结果状态。
  5. promise.reject()
  6. promise.resolve()

async/await

async是更为优雅的异步编程解决方案,函数的返回值的是一个promise对象。用同步的方式,执行异步操作。
await用来暂停等待异步函数的执行结果,如果是Promise,也就是等待它的settled状态,并且await只能在出现在async function内部,不可单独使用。

深拷贝/浅拷贝

浅拷贝

一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值;如果拷贝的是引用数据类型,拷贝的就是内存地址。

深拷贝

复制并创建一个一模一样的对象,不共享内存,修改新对象,旧对象保持不变。

手写实现深拷贝:

function deepClone (source) {
    if (source instanceof Object == false) return source
    let target = Array.isArray(source) ? [] : {}
    for (let i in source) {
        if (source.hasOwnProperty(i)) {
            if (typeof source[i] === 'object') {
                target[i] = deepClone(source[i])
            } else {
                target[i] = source[i]
            }
        }
    }
    return target
}

typeOf/instanceOf

区别:typeOf的返回值是一个字符串,用来说明变量的数据类型;instanceOf的返回值是布尔值,用于判断一个变量是否属于某个对象的实例。

总结:

  1. instanceOf在查找的过程中会遍历左边变量的原型链,直到找到右边变量的prototype,如果查找失败,则会返回false
  2. typeOf在对值类型number、string、Boolean、undefined以及引用类型的function的反应是精准的;但是对于对象{}、数组[]、null都会返回object,但对NaN返回的是number类型。为了弥补这一点,instanceOf从原型的角度,来判断某引用属于哪个构造函数,从而判定它的数据类型。

Map、Set

Map

Map是一组键值对的结构,和JSON对象类似。

在这里插入图片描述

Set

Set对象类似于数组,且成员的值都是唯一的。常用于数组去重。

在这里插入图片描述
区别:

  1. Map需要的是一个二维数组,而Set需要的是一维Array数组
  2. Map和Set都不允许键重复
  3. Map的键不能修改,但是对应的值是可以修改的;Set不能通过迭代器改变Set的值,因为Set的值就是键
  4. Map是键值对的存在,值也不作为键;而Set的value就是key

Vue篇

vue组件通信方式

  1. props/$emit
  2. $children/$parent
  3. provide/reject
  4. ref/refs
  5. eventBus
  6. Vuex
  7. localStorage/sessionStorage
  8. $attrs$listeners
  • 父子组件通信:props;$parent/$children;provide/inject;ref;$attrs/$listeners
  • 兄弟组件通信:eventBus/vuex
  • 跨级组件通信:eventBus/vuex;provide/inject;$attrs/$listeners

vuex

专为Vue.js应用程序开发的状态管理模式+库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

vuex各个模块

  1. state:用户数据的存储,是store中的唯一数据源
  2. getters:如vue中的计算属性一样,基于state数据的二次包装,常用于数据的筛选和多个数据的相关性计算
  3. mutations:类似函数,改变state数据的唯一途径,且不能用于处理异步事件
  4. actions:类似于mutation,用于提交mutation来改变状态,而不直接变更状态,可以包含任意异步操作
  5. modules:类似于命名空间,用于项目中将各个模块的状态分开定义和操作,便于维护

vue渲染列表为什么要加key?

Vue列表加key的目的是为diff算法添加标识,因为diff算法判断新旧VDOM是否相同的依据是节点的tag和key。如果tag和key相同则会进一步进行比较,使得尽可能多的节点进行复用。此外,key绑定的值一般是一个唯一的值,比如id。如果绑定数组的索引index,则起不到优化diff算法的作用,因为一旦数组内元素进行增删,后续节点绑定的key也会发生变化,导致diff进行多余的更新操作。

vue3和vue2的区别

双向绑定

vue3使用proxy代理整个对象,vue2使用Object.defineProperty代理对象的属性,如果要监听整个对象,需要遍历对象的所有属性。
vue2中是无法检测数组和对象的属性新增,无法检测通过索引改变数组的操作。

生命周期

Vue2(选项式api)Vue3(setup)描述
beforeCreated-实例创建前
created-实例创建后
beforeMountedonBeforeMountDOM挂载前调用
mountedonMountedDOM挂载完成调用
beforeUpdateonBeforeUpdate数据更新之前被调用
updatedonUpdated数据更新之后被调用
beforeDestoryonBeforeMount组件销毁前调用
destoryedonUnmounted组件销毁完成调用

composition api(组合式api)和选项式api

composition api(组合式api)
<template>
  <div @click="changeMsg">{{msg}}</div>
</template>

<script>
  import { ref, defineComponent } from "vue";
  export default defineComponent({
    setup () {
      const msg = ref('hello world')
      const changeMsg = () => {
        msg.value = 'hello juejin'
      }
      return {
        msg,
        changeMsg
      };
    },
  });
</script>
  1. reactive API
    reactive方法用来创建响应式对象,接收一个对象/数组参数,返回对象的响应式副本,当该对象的属性值发生变化,会自动更新使用该对象的地方。
import { reactive } from 'vue'

let reactiveObj = reactive({ name : 'Chris1993' });
let setReactiveObj = () => {
  reactiveObj.name = 'Hello Chris1993';
}

let reactiveArr = reactive(['a', 'b', 'c', 'd']);
let setReactiveArr = () => {
  reactiveArr[1] = 'Hello Chris1993';
}
  1. ref API
    ref的作用是将一个基本数据类型转换成一个带有响应式特性的数据类型,基本数据类型:String、Number、Boolean、Null、Undefined、Symbol
import { ref } from 'vue'

let refValue = ref('Chris1993');
let setRefValue = () => {
  refValue.value = 'Hello Chris1993';
}
let refObj = ref({ name : 'Chris1993' });
let setRefObj = () => {
  refObj.value.name = 'Hello Chris1993';
}
选项式api
<template>
  <div @click="changeMsg">{{msg}}</div>
</template>

<script>
  export default {
    data () {
      return {
        msg: 'hello world'
      }
    },
    methods: {
      changeMsg () {
        this.msg = 'hello juejin'
      }
    }
  }
</script>

vue3相对vue2的响应式优化

  1. vue2对于数据响应式的实现上是有一些局限性的,比如:
  • 无法检测数组和对象的新增
  • 无法检测通过索引改变数组的操作
  1. Object.defineProperty只能劫持对象的属性,从而需要对每个对象,每个属性进行遍历,如果属性值是对象,还需要深度遍历。Proxy可以劫持整个对象,并返回一个新的对象;
  2. proxy不仅可以代理对象,还可以代理数组,还可以代理动态增加属性

Vue2.0 不能监测数组和对象的变化原因以及解决方案
Vue3为何使用Proxy实现数据监听

vue diff算法

虚拟DOM

虚拟DOM表示真实DOM的JS对象,包含标签名、标签的属性以及子标签的名称、属性和文本节点。

<div>
    <p>123</p>
</div>

对应的virtual DOM(伪代码):

var Vnode = {
    tag: 'div',
    children: [
        { tag: 'p', text: '123' }
    ]
};

diff算法

本质就是比较两个JS对象的差异。

比较流程如下图:
在这里插入图片描述

updateChildren

关键点:1、同级比较;2、深度优先;3、首尾指针法
在这里插入图片描述
比较过程:

  1. 依次比较,当比较成功后退出当前比较;
  2. 渲染结果以newVnode为准;
  3. 每次比较成功后start点和end点向中间靠拢;
  4. 当新旧节点中有一个start点跑到end点右侧时终止比较;
  5. 如果都匹配不到,则旧虚拟DOM key值去比对新虚拟DOM的key值,如果key相同则复用,并移动到新虚拟DOM的位置。

vue双向绑定原理

响应式原理

Vue的核心特性之一,数据驱动视图,修改了数据之后视图随之响应更新。
Vue2.x是借助Object.defineProperty()实现的,而Vue3.x是借助Proxy实现的,Proxy相比Object.defineProperty在处理数组和新增属性的响应式处理上更加方便。

双向绑定

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

Vue2.x实现双向绑定核心是通过三个模块:Observer监听器、Watcher订阅者和Compile编译器。
实现原理:首先监听器会监听所有的响应式对象属性,编译器会将模板进行编译,找到里面动态绑定的响应式数据并初始化视图;watcher会去收集这些依赖;当响应式数据发生变更时Observer就会通知Watcher;watcher接收到监听器的信号就会执行更新函数去更新视图。

v-model原理

v-model是用来监听用户事件然后更新数据的语法糖。
其本质还是单向数据流,内部是通过绑定元素的value值向下传递数据,然后通过绑定input事件,向上接收并处理更新数据。

// 比如
<input v-model="sth" />
// 等价于
<input :value="sth" @input="sth = $event.target.value" />

computed和watch

computed

  1. 计算值
  2. 可以计算和处理data、props或$emit的值
  3. 具有缓存性

watch

  1. 观察的动作
  2. 监听props、$emit或本组件的执行异步操作
  3. 无缓存性

$nextTick原理

作用

Vue在更新数据时是异步执行的,当数据变化会产生一个异步更新队列,要等异步队列结束后才会统一进行更新视图,所以改了数据之后立即去拿dom还没有更新就会拿不到最新数据。所以提供了一个nextTick函数,它的回调函数会在DOM更新后立即执行。

原理

nextTick的本质就是创建了一个微任务,将其回调函数推入微任务队列,放到下次DOM更新之后执行。

nextTick之所以能够拿到DOM更新后的结果,是因为我们传入的callback会被添加到队列刷新函数(flushSchedulerQueue)的后面,这样等队列内部的更新函数都执行完毕,所有DOM操作也就结束了,callback自然能够获取到最新的DOM值。

所以项目中不使用$nextTick的话也可以直接使用Promise.then或者setTimeout实现相同的效果。

Vue异步更新

Vue更新DOM是异步更新的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM操作是非常重要的。

为什么要用虚拟DOM

频繁地修改DOM元素是非常影响性能的,严重的话甚至会导致整个页面掉帧、卡顿甚至失去响应。其实很多DOM操作是可以打包(多个操作压成一个)和合并(一个连续更新操作只保留最终结果),同时JS引擎的计算速度要快得多,所以可以把DOM操作先通过JS计算完成后统一来一次操作DOM,所以有了虚拟DOM的概念。
虚拟DOM操作的核心是diff算法,通过比较变化前后VNode的不同,计算出最小的DOM操作来改变DOM,提升性能。

Vuex流程&原理

Vuex利用vue的mixin机制,在beforeCreated钩子前混入了vuex init方法,这个方法实现了将store注入vue实例当中,并注册了store的引用属性$store,所以可以使用this.$store.xxx去引入vuex中定义的内容。

然后state是利用vue的data,通过new Vue({data:{{$$state: state}}将state转换成响应式对象,然后使用computed函数实时计算getter。

Vue.use

Vue.use本质就是执行需要注入插件的install方法。

编写一个vue插件

要暴露一个install方法,第一个参数是Vue构造器,第二个参数是一个可选的配置项对象

Myplugin.install = function(Vue, options = {}) {
  // 1、添加全局方法或属性
  Vue.myGlobalMethod = function() {}
  // 2、添加全局服务
  Vue.directive('my-directive', {
    bind(el, binding, vnode, pldVnode) {}
  })
  // 3、注入组件选项
  Vue.mixin({
    created: function() {}
  })
  // 4、添加实例方法
  Vue.prototype.$myMethod = function(methodOptions) {}
}

运行npm run xxx的时候发生了什么

  1. 运行npm run xxx的时候,npm会先在当前目录的node_modules/.bin查找要执行的程序,如果找到则运行;
  2. 没有找到则从全局的node_modules/.bin中查找,npm i -g xxx就是安装到全局目录;
  3. 如果全局目录还是没找到,那么就从path环境变量中查找有没有其他同名的可执行程序。

vue route/router区别

$route对象

$route对象表示当前的路由信息,包含了当前URL解析得到的信息。包含当前的路径、参数、query对象等。

$router对象

$router对象是全局路由的实例,是router构造方法的实例。
路由实例方法:push、go、replace

vue 路由传参

1. params传参(显示参数)

在url中会显示出传参的值,刷新页面不会失去拿到的参数,使用该方式传值需要子路由提前配置好参数

//路由配置
{
  path: '/child/:id',
  component: Child
}
//编程式使用
this.$router.push({
    path:'/child/${id}',
})

2. params传参(不显示参数)

//路由配置
{
  path: '/child',
  component: Child,
  name:Child
}
//编程式使用
this.$router.push({
    name:'Child',
    params:{
      id:123
    }
})

3. query传参

//路由配置
{
  path: '/child',
  component: Child,
  name:Child
}
//编程式使用
//name方式
this.$router.push({
    name:'Child',
    query:{
    id:1
}
})
//path方式
this.$router.push({
    path:'/child',
    query:{
    id:1
}
})

跨域问题

1. proxy 设置代理服务器

  1. vue.config.js设置
 devServer: {
    host: '0.0.0.0',
    proxy: {
      '/router': {
        target: URL,
        changeOrigin: true
      }
    }
  }
  1. axios设置baseUrl
// axios实例
const httpClient = axios.create({
  baseURL: '/router/rest',
  timeout: 10000,
  headers: { 'format': 'JSON' }
})

2. CORS【后端】

目前最主流、最简单的方案,直接让后端设置响应头,允许资源共享就ok了

//调用方法创建一个服务器
  const app = express()
//解析json格式的请求体
  app.use(express.json())
//解析查询字符串格式的情趣 
  app.use(exoress.urlencoded({extended:true}))
//第一种: 使用中间件函数来设置cors允许资源共享
  app.use((req.res.next)=>{
    //设置响应头 告诉浏览器任何地址都可以访问这个接口
    res.setHeader('Access-Cosntrol-Allow-Origin','*')
    //告诉浏览器支持这些方式
    res.setHeader('Access-Cosntrol-Allow-Methods','GET,POST,DELETE,PUT')
    next()
  })
//第二种 推荐使用插件
  const cors = require('cors')
  app.use(cors())

3. JSONP

曾经很流行,专治各种跨域问题,需要前后端配合使用。

使用原理:通过script标签的src来发请求没有跨域限制来获取资源
注意:JSONP只支持get请求,不支持post,请求回来的东西当做js来执行

插槽

1. 内容插槽

插槽内可以包含普通文本、任何模板代码(包括HTML)、其他组件、使用数据。

//home.vue
<test>
     Hello Word
</test>
//test.vue
<a href="#">
	 <slot></slot>
</a>

当组件渲染的时候,<slot></slot>会被替换为Hello Word

2. 后备内容(默认内容)插槽

3. 具名插槽

<slot>元素有一个特殊的特性:name,这个特性可以用来定义额外的插槽。

<div>
  <header>
    <!-- 我们希望把页头放这里 -->
  </header>
  
  <main>
    <!-- 我们希望把主要内容放这里 -->
  </main>
  
  <footer>
    <!-- 我们希望把页脚放这里 -->
  </footer>
</div>

这时候,我们就可以使用name属性:

<div>
  <header>
    <slot name="header"></slot>
  </header>
  
  <main>
    <slot></slot>
  </main>
  
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

4. 作用域插槽

父组件拿到子组件slot的值

//test.vue
<div>
	<!-- 设置默认值:{{user.lastName}}获取 Jun -->
	<!-- 如果home.vue中给这个插槽值的话,则不显示 Jun -->
	<!-- 设置一个 usertext 然后把user绑到设置的 usertext 上 -->
	<slot v-bind:usertext="user">{{user.lastName}}</slot>
</div>

//定义内容
data(){
  return{
	user:{
	  firstName:"Fan",
	  lastName:"Jun"
	}
  }
}

然后在home.vue中接收传过来的值:

//home.vue
<div>
  <test v-slot:default="slotProps">
    {{slotProps.usertext.firstName}}
  </test>
</div>

keep-alive

keep-alive是一个抽象组件,主要用于缓存内部组件实例

参数

  • include:可传字符串、正则表达式、数组,名称匹配成功的组件会被缓存
  • exclude:可传字符串、正则表达式、数组,名称匹配成功的组件不会被缓存
  • max:可传数字,限制缓存组件的最大数量

include和exclude,传数组情况居多

总结

  • 缓存
    • 首次渲染的时候设置缓存
    • 缓存渲染的时候不会执行组件的created、mounted等钩子函数,而是对缓存的组件执行patch过程,最后直接更新到目标元素
  • 使用LRU缓存策略对组件进行缓存
    • 命中缓存,则直接返回缓存,同时更新缓存key的位置
    • 不命中缓存则设置进缓存,同时检查缓存的实例数量是否超过max

Vue的设计模式

单例模式

确保一个类只有一个实例对象,并提供一个全局访问点供其访问。
场景:登录浮窗、Vue的axios实例、全局状态管理store、线程池、全局缓存

发布-订阅者模式

又叫观察者模式。场景:双向绑定

装饰器模式

在不改变对象自身的基础上,在程序运行期间给对象动态的添加职责;若直接修改函数体,则违背了“开放封闭原则”,也违背了我们的“单一职责原则”,简单的说就是允许向现有的函数添加新的功能,同时不改变其结构。
场景:vue中的表单验证与表单提交,遵循封闭开放原则。

vue模板编译

vue模板编译:模板-AST树-render-虚拟节点DOM-真实节点DOM

CSS篇

什么是BFC

BFC(Block Formatting context)块级格式上下文。

BFC是一个独立的渲染区域,相当于一个容器,在这个容器中的样式布局不会受到外界的影响。

创建BFC

  • 浮动元素(float值不为none)
  • 绝对定位元素(position值为absolute或fixed)
  • display为flex/inline-flex/inline-block/table/table-cell/table-caption
  • overflow!=visible

解决了什么问题

  1. 使用float脱离文档流,高度塌陷
  2. margin边距重叠
  3. 两栏布局

面试官:请说说什么是BFC?大白话讲清楚

flex

容器的属性

  • flex-direction
  • flex-wrap
  • flex-flow
  • justify-content
  • align-items
  • align-content
flex-direcition

决定主轴的方向(即项目的排列方向)

.box {
  flex-direction: row | row-reverse | column | column-reverse;
}
flex-wrap

默认情况下,项目都排在一条线(又称“轴线”)上。flex-wrap定义如果一条轴线排不下,如何换行

.box {
    flex-wrap: nowrap | wrap | wrap-reverse
}
flex-flow

flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认值为row nowrap

.box {
    flex-flow: <flex-direction> || <flex-wrap>;
}
justify-content

定义了项目在主轴上的对齐方式

.box {
    justify-content: flex-start | flex-end | center | space-between | spaca-around;
}
align-items

定义项目在交叉轴上如何对齐

.box {
    align-items: flex-start | flex-end | center | baseline | stretch;
}
align-content

定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。

.box {
    align-content: flex-start | flex-end | center | space-between | space-around | stretch;
}

项目的属性

  • order
  • flex-grow
  • flex-shrink
  • flex-basis
  • flex
  • align-self
order

定义项目的排列顺序。数值越小,排列越靠前,默认为0。

flex:1

flex:1 = flex: 1 1 0%; 可扩大,可缩小,flex-basis为0
flex属性是flex-growflex-shrinkflex-basis的简写,默认值是0 1 auto。
flex-grow是如果有剩余空间,是否扩大,1为扩大,0为不扩大
flex-shrink是如果剩余空间不够,是否缩小,1为缩小
flex-basis为项目本身的大小,默认值是auto
在这里插入图片描述

垂直水平居中

1. 父元素使用flex布局,并设置相关的属性值为center

这种方式要求父元素的高度是确定的,百分比形式的高度将不能生效。

.par-work {
    height: 100vh;
    display:flex;
    justify-content:center;
    align-items:center;
}

2. 使用绝对定位absolute+transform,给子元素添加如下样式

这种方式比较常用,父子元素都不确定宽高的情况也适用。
如果子元素的宽高确定的话,translate中的值也可以设置为子元素宽高的一半,即tranform:translate(-100px,-100px);

.work {
	position: absolute;
	top: 50%;
	left: 50%;
	transform: translate(-50%, -50%);
}

3.使用绝对定位absolute+margin,给子元素添加如下样式

这种方式适合子元素宽高确定的情况,给margin-top设置百分比的大小将不生效,即margin-top: -50%不能达到垂直居中的效果。

.work1 {
	positon: absolute;
	top: 50%;
	left: 50%;
	margin-top: -100px;
	margin-left: -100px;
}

4. 使用绝对定位absolute+margin:auto,给子元素添加如下样式

父子元素宽高都未知时也适用。

.work2 {
	postion: absolute;
	top: 0;
	bottom: 0;
	right: 0;
	left: 0;
	margin: auto;
}

5. 使用table-cell实现

这种方式需要父元素的宽高都是确定的,才能保证子元素在父元素中垂直水平都居中。

.par-work2 {
	height: 500px;
	width: 500px;
	display: table-cell;
	vertical-align: middle;
	text-align: center;
}
.son-work2 {
	display: inline-block;
}

6.使用grid布局

这种方式适用于父元素高度确定的情况。

.par-work3 {
	display: grid;
	height: 500px;
}
.son-work3 {
	align-self: center; /* 设置单元格内容的垂直位置 */
	justify-self: center; /* 设置单元格内容的水平位置 */
}

重绘、重排

重绘

重绘:当元素的一部分属性发生改变,如外观、背景、颜色等不会引起布局变化,只需要浏览器根据元素的新属性重新绘制,使元素呈现新的外观。

重排

重排:当render树中的一部分或者全部因为大小边距等问题发生改变而需要DOM树重新计算的过程。
重绘不一定需要重排(比如颜色的改变),重排必然导致重绘(比如改变网页位置)。

transition、transform和animation

transition

过渡。用于平滑改变css的属性,如盒子的长宽等,鼠标移入悬浮产生动效的情况等。
常用属性

  1. transiton-property:指定过滤的性质,比如background就是指background参与这个过渡
  2. transition-duration:指定这个过渡的持续时间
  3. transition-delay:延迟过渡时间
  4. transition-timing-function:指定过滤类型,有ease|linear|ease-in|ease-out|ease-in-out|cubic-bezier

transform

变化。常用来实现拉伸,压缩,旋转,偏移等。
常用属性:

  1. rotate
  2. translate
  3. scale
  4. skew扭曲

animation

动画,与transition区别是,transition只允许在两个状态中切换,而animation可以利用关键帧来实现多个状态,而且transition一定要有一个事件触发才会产生效果,就像是hover,click等。但是animation不需要任何事件的触发也可以随着时间的变化去改变css属性值,从而产生一种动画的效果。

src和href区别

  • src是source的简写,目的是要把文件下载到html页面中,src用于替换当前内容
  • href是Hypertext Reference的简写,表示超文本引用,指向网络资源所在的位置。href用于在当前文档和引用资源之间确立联系。

浏览器解析方式

  • 当浏览器遇到href会并行下载资源并且不会停止对当前文档的处理。(同时也是为什么建议使用link方式加载CSS,而不是使用@import方式,@import需要网页完全载入以后加载)
  • 当浏览器解析到src,会暂停其他资源的下载和处理,直到该资源加载或执行完毕。(这也是script标签为什么放在底部而不是头部的原因)

浏览器和网络篇

输入URL到页面显示的过程

浏览器有以下几个进程:

  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程。把从网络下载的HTML、JavaScript、CSS、图片等资源解析为可以显示和交互的页面。
  • 网络进程。主要负责页面的网络资源加载。
  • 插件进程。负责插件的运行,因插件易崩溃,所以需要通过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面造成影响。
  • GPU进程。GPU绘制页面。

1. 用户输入

2. URL请求过程

浏览器进程会通过进程间通信(IPC)把URL请求发送至网络进程,网络进程接收到URL请求后,会在这里发起真正的URL请求流程。

  1. 网络进程会查找本地缓存是否缓存了该资源。如果有缓存资源,那么直接返回资源给浏览器进程;如果在缓存中没有找到资源,那么直接进入网络请求流程。请求前的第一步是进行DNS解析,获取请求域名的服务器IP地址。如果请求协议是HTTPS,那么还需要建立TLS连接。
  2. 利用IP地址和服务器建立TCP连接(三次握手)。连接建立之后,浏览器端会构建请求行、请求头等信息,并把和该域名相关的cookie等数据附加到请求头中,向服务器发送请求。
  3. 服务器接口到请求信息后,会生成响应数据,并发给网络进程。

3. 准备渲染进程

通常情况下,打开新的页面都会使用单独的渲染进程。
如果协议根域名相同,则属于同一站点。新页面和当前页面属于同一站点的话,新页面会复用父页面的渲染进程。

4. 提交文档

浏览器进程网络进程接收到的HTML数据提交给渲染进程

5. 渲染进程

  1. 渲染进程将HTML内容转换为能够读懂的DOM树结构
  2. 渲染引擎将CSS样式表转换为浏览器可理解的styleSheets,计算出DOM节点的样式
  3. 创建布局树,并计算元素的布局信息
  4. 对布局树进行分层,并生成分层树
  5. 为每个图层生成绘制列表,并将其提交到合成线程
  6. 合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图
  7. 合成线程发送绘制图块命令DrawQuad给浏览器进程
  8. 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上

TCP/UDP

TCP:面向连接、传输可靠、速度慢。TCP/IP模型的传输层协议
一般用于文件传输,发送或接收邮件,远程登录等。
UDP:面向非连接、传输不可靠、速度快。TCP/IP模型传输层的无连接协议
一般用于即时通信,在线视频,语音电话,传输音频、视频文件等。

HTTPS

HTTP超文本传输协议,应用层协议。主要用于Web上传输超媒体文本的底层协议,经常在浏览器和服务器之间传递数据。通信就是以纯文本的形式进行。
HTTPS=HTTP+SSL/TLS

SSL是安全层,TLS是传输层安全,是SSL的继承。使用SSL或TLS可确保传输数据的安全性。
在这里插入图片描述
安全层:对发起的HTTP请求的数据进行加密操作对接收到HTTP的内容进行解密操作

HTTPS的工作流程

  1. 客户端发起HTTPS请求并连接到服务器的443端口,此过程和HTTP请求一样,进行三次握手
  2. 服务端向客户端发送数字证书,其中包含公钥、证书颁发者、到期日期;
    现比较流行的加解密码对,即公钥和私钥。公钥用于加密,私钥用于解密。所以服务器会保留私钥,然后发送公钥给客户端。
  3. 客户端收到证书,会验证证书的有效性。验证通过后会生成一个随机的pre-master key。再将密钥通过接收到的公钥加密然后发送给服务端;
  4. 服务端接收后使用私钥进行解密得到pre-master key;
  5. 获取pre-master key后,服务器和客户端可以使用主密钥进行通信。

HTTP 1/2/3 有啥区别

解读HTTP1/HTTP2/HTTP3

HTTP1.1

缺陷:

  1. 高延迟-队头阻塞(Head-Of-Line Blocking)
    带来页面加载速度的降低。队头阻塞是指当顺序发送的请求序列中的一个请求因为某种原因被阻塞时,在后面排队的所有请求也一并被阻塞,会导致客户端迟迟收不到数据。尝试通过以下办法解决:
  • 将同一页面的资源分散到不同域名下,提升连接上限。Chrome有个机制,对于同一个域名,默认允许同时建立6个TCP持久连接。
  • 合并小文件减少资源数。精灵图Sprite
  • 内联(Inlining)资源
  • 减少请求数量
  1. 无状态特性-阻碍交互
    无状态是指协议对于连接状态是没有记忆能力。纯净的HTTP是没有cookie等机制的,每一个连接都是一个新的连接。
  2. 明文传输-不安全性
  3. 不支持服务端推送

HTTP2新特性**

  1. 二进制传输
    HTTP/2传输数据量的大幅减少,主要有两个原因:以二进制方式传输Header压缩
    HTTP/2将请求和响应数据分割为更小的帧,并且他们采用二进制编码。
  2. Header压缩
    HTTP/2并没有使用传统的压缩算法,而是开发了专门的“HPACK”算法,在客户端和服务器两端建立“字典”,用索引号表示重复的字符串,还采用哈夫曼编码来压缩整数和字符串,可以达到50%~90%的高压缩率。
  3. 多路复用
  • 同个域名只需要占用一个TCP连接,使用一个连接并行发送多个请求和响应,这样整个页面资源的下载过程只需要一次慢启动,同时也避免了多个TCP连接竞争带宽所带来的问题。
  • 并行交错地发送多个请求/响应,请求/响应互不影响。
  • 在HTTP/2中,每个请求都可以带一个31bit的优先值,0表示最高优先级,数值越大优先级越低。
    在这里插入图片描述
    如上图所示,多路复用的技术可以只通过一个TCP连接就可以传输所有的请求数据。
  1. Server Push
    服务器推送:浏览器刚请求HTML的时候就提前把可能会用到的JS、CSS文件发给客户端,减少等待的延迟。
  2. 提高安全性

HTTP/2的缺点

虽然HTTP2解决了很多之前旧版本的问题,但是它还是存在一个巨大的问题,主要是底层支撑的TCP协议造成的。

  1. TCP以及TCP+TLS建立连接的延时
  2. TCP的队头阻塞并没有彻底解决
  3. 多路复用导致服务器压力上升
  4. 多路复用容易Timeout

总结

  1. HTTP1有两个主要的缺点:性能不足(队头阻塞)和安全不足
  2. HTTP2完全兼容HTTP1,是更安全的HTTP、更快的HTTPS,二进制传输、头部压缩、多路复用、服务器推送等技术充分利用带宽,降低延迟,从而大幅度提高上网体验;
  3. HTTP3:QUIC基于UDP实现,是HTTP3的底层支撑协议,该协议基于UDP,又取了TCP中的精华,实现了即快又可靠的协议。

http缓存

在浏览器加载资源的时候,会根据请求头的expirescache-control判断是否命中强缓存策略,判断是否向远程服务器请求资源还是去本地获取缓存资源。

强缓存

强缓存分为cache-control(http1.1规范)、expires(http1规范)

cache-control

优先级高于expires。在cache-control有常见的几个响应属性值:

指令参数说明
max-age必需,值如3600表示(当前时间+3600秒)内不与服务器请求新的数据资源
public可省略所有内容都将被缓存(客户端和代理服务器都可缓存)
private内容只缓存到私有缓存中(仅客户端可以缓存,代理服务器不可缓存)
no-cache可省略缓存前必须确认其有效性
no-store不缓存请求或响应的任何内容
expires

用来表示资源的过期时间的请求头字段,是一个绝对时间,由服务器端返回。如:Fri, 02 Sep 2022 08:03:35 GMT

协商缓存

ETag优先级大于Last-Modified

ETag/If-None-Match
  • Etag是一个文件的唯一标识符,当资源发生变化时这个ETag就会发生变化。
  • If-None-Match是浏览器请求数据时带上的字段,值是上次服务器返回的ETag
Last-Modified/If-Modified-Since
  • Last-Modified表示本地文件最后修改时间,由服务器返回
  • If-Modified-Since是浏览器载请求数据时返回的,值是上次浏览器返回的Last-Modified

Webpack篇

webpack的核心概念

Entry:入口,webpack执行构建的第一步从Entry开始,可抽象成输入。告诉webpack要使用哪个模块作为构建项目的起点,默认为./src/index.js
Output:出口,告诉webpack在哪里输出它打包好的代码以及如何命名,默认为./dist
Module:模块,在webpack里一切皆模块,一个模块对应着一个文件。webpack会从配置的Entry开始递归找出所有依赖的模块。
Chunk:代码块,一个Chunk由多个模块组合而成,用于代码合并与分割。
Loader:将webpack不认识的内容转化为认识的内容
Plugin:拓展插件,可以贯穿webpack打包的生命周期,执行不同的任务。

webpack的基本功能

代码转换:TypeScript编译成JavaScript、scss编译成css等
文件优化:压缩JavaScript、css、html代码,压缩合并图片等
代码分割:提取多个页面的公共代码、提取首屏不需要执行部分的代码让其异步加载
模块合并:在采用模块化的项目有很多模块和文件,需要构建功能把模块分类合并成一个文件
自动刷新:监听本地源代码的变化,自动构建,刷新浏览器
代码校验:在代码提交到仓库前需要检测代码是否符合规范,以及单元测试是否通过
自动发布:更新完代码后,自动构建出线上发布代码并传输给发布系统

常用loader

css-loader:读取合并css文件
style-loader:把css内容注入到JavaScript里
sass-loader:解析sass文件(安装sass-loader,node-sass)
postcss-loader:自动添加浏览器兼容前缀(postcss.config配置)
url-loader:将文件转换为base64 URL
vue-loader:处理vue文件

常见Plugins

html-webpack-plugin:自动在打包结束后生成Html文件,并引入bundle.js
clean-webpack-plugin:打包自动删除上次打包文件
webpack-bundle-analyzer:可视化webpack输出文件的体积(业务组件、依赖第三方模块)

webpack打包优化

  1. 压缩js、css代码,压缩图片;svg图片转化为base64格式图片
  2. 本地跑项目代码,实现代码热更新
  3. DllPlugin避免重复编译第三方库
  4. TreeShaking剔除无用js代码
  5. gzip压缩
  6. webpack-bundle-analyse分析打包文件大小

preload、prefetch

  1. preload主要用于加载当前页面需要的资源;
  2. prefetch主要用于加载将来页面可能需要的资源

性能优化

路由懒加载

将路由全部改成懒加载,按需加载。

路由懒加载的原理

懒加载前提的实现:ES6的动态加载模块-import()

调用import()之处,被作为分离的模块起点,被请求的模块和它引用的所有子模块,会分离到一个单独的chunk中。

要实现懒加载,就得先将懒加载的子模块分离出来,打包成一个单独的文件。
webpackChunkName作用是webpack在打包的时候,对异步引入的库代码(lodash)进行代码分割时,设置代码块的名字。webpack会将任何一个异步模块与相同的块名称组合到相同的异步块中。

组件懒加载

使用场景:

  1. 该页面的JS文件体积大,导致页面打开慢,可以通过组件懒加载进行资源拆分,利用浏览器并行下载资源,提升下载速度(比如首页
  2. 该组件不是一进入页面就展示,需要一定条件下才触发(比如弹框组件
  3. 该组件复用性高,很多页面都有引入,利用组件懒加载抽离出该组件,一方面可以很好利用缓存,同时也可以减少页面的JS文件大小(比如表格组件、图形组件等)

图片优化

  1. 图片转base64格式
  2. App.vue使用的全局小猴子loading.gif去除,替换成vant自带的loading
  3. 大图片文件压缩,并放到CDN加载

第三方组件加载优化

  1. momentjs替换为dayjs,替换后体积从400多k缩小至2kb
  2. lodash按需引用
  3. echarts按需引用

非主要进程延后加载

  1. OA申请H5 流程反馈模块延后加载
  2. 获取产品负责人信息延后加

Tree-shaking

作用:消除无用的js代码

骨架屏

让页面看起来更快地加载出来

长列表虚拟滚动

虚拟滚动:指的是只渲染可视区域的列表项,非可见区域的不渲染,在滚动时动态更新可视区域,该方案在优化大量数据渲染时效果是很明显的。
在这里插入图片描述

虚拟滚动基本原理

计算出totalHeight列表总高度,并在触发滚动事件时根据scrollTop值不断更新startIndex以及endIndex,以此从列表数据listData中截取对应元素。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值