这里是目录
- ---------------- VUE相关 -----------------
- 1 - Vue3 是怎么得更快的?
- 1-1 Fragment [fræɡˈment]
- 1-2 Suspense [səˈspens]
- 1-3 Teleport [ˈtelipɔːt]
- 1-4 v-memo
- 2- 说一下 Composition API
- 3- 说一下 setup
- 4- watch 和 watchEffect 的区别
- 5- Vue3 响应式原理和 Vue2 的区别
- 6- defineProperty 和 Proxy 的区别
- 7- proxy
- 8- Vue3 的生命周期
- 9- Vue3的diff算法和Vue2的区别
- 10- SPA与前端路由
- vue2中路由跳转
- vue3 query跳转
- Vue3跳转新页面
- router.resolve和 router.push和router.replace的区别
- 11- 路由守卫
- 全局守卫
- 路由独享的守卫
- 组件内部守卫
- 12- 关于前端鉴权这块
- 13- app.use() 和 install
- 14- js修改组件的props(createApp)
- 15- pinia和mittBus的使用场景
- ----------------- JS相关 ------------------
- **1- CommonJS 和 ES6 模块化的详细总结:**
- 2- Array.from()
- 3- 浅拷贝
- 4- 闭包
- 5- js中 ??、 !!、 操作符的意思
- 6- js 判断数据类型
- 7- JWT与Token详解
- 8- map和werkmap区别
- 9- class 和 function 的区别?什么时候用那个?
- 10- 知名符号
- Symbol.toStringTag:
- Symbol.iterator:
- Symbol.species:
- Symbol.hasInstance:
- Symbol.isConcatSpreadable:
- 11- 迭代器(Iterator)和生成器(Generator)
- 12- JavaScript 构造函数、纯函数、new 做了什么?
- 纯函数
- 13- 正则
- 14- JS 模块导入导出
- 15- JS动态操作类名
- 16- HTMLElement 、 HTMLDivElement
- 17- **WebSocket**与Web **Workers**
- ---------- TS相关 ------------------
- 1- declare 和 type 和 interface
- 2- 泛型和函数重载
- 3- 枚举和继承和class
- ----------------- css相关 ------------------
- 1- sass继承、minni
---------------- VUE相关 -----------------
1 - Vue3 是怎么得更快的?
Vue3 通过新增几个组件、指令和 API,以及重构响应式系统、虚拟 DOM 等方面来提高性能:
- 新增了 Fragment、Suspense 和 Teleport 等组件,提升了渲染效率,支持更灵活的视觉布局和异步加载组件;
- 引入了 v-memo 指令,可以缓存不变的模板,优化了程序运行时的性能;
- 支持 Tree-Shaking,去除无用代码,减小包体积;
- 新增 Composition API,使逻辑复用和代码组织更简单,同时解决 mixin 带来的问题;
- 使用 Proxy 代替 Object.defineProperty 重构响应式系统,实现更细粒度的监听和拦截;
- 重构了虚拟 DOM,添加了静态标记、Diff 算法使用最长递增子序列优化对比流程,大幅提升了虚拟 DOM 的生成速度;
- 支持在 里使用 v-bind,给 CSS 绑定 JS 变量;
- 使用 setup 代替了 beforeCreate 和 created 生命周期,更好地控制组件的初始化行为;
- 新增了开发环境的两个钩子函数,方便有针对性地进行调试;
- 对 TypeScript 的支持更好,提供更好的类型检查和提示。
总的来说,Vue3 通过优化渲染、响应式、虚拟 DOM 等方面的实现,提高了性能和开发体验。
1-1 Fragment [fræɡˈment]
在 Vue2 中,模板中的元素必须有一个根元素,也就是说,所有的元素都必须包含在一个外层标签内。比如下面这个例子:
<template><div><h1>标题</h1><p>正文1</p><p>正文2</p></div>
</template>
在这个例子中,<h1>
、<p>
和 <p>
这三个元素都包含在一个 <div>
元素中。
但是,在某些情况下,我们不想使用一个外层标签来包裹多个元素,这个时候就可以使用 Fragment 组件。Fragment 组件不会渲染出任何 HTML 标签,它只是一个占位符,在渲染的时候被忽略掉。因此,使用 Fragment 组件可以让模板更加灵活。
上面的例子可以改写成下面这样:
<template><fragment><h1>标题</h1><p>正文1</p><p>正文2</p></fragment>
</template>
这个例子中,<h1>
、<p>
和 <p>
这三个元素没有被包裹在任何标签中,直接放在了 Fragment 组件内部。在渲染的时候,Fragment 组件会被忽略掉,只渲染出 <h1>
、<p>
和 <p>
这三个元素。
1-2 Suspense [səˈspens]
Vue3 中引入了 Suspense 组件,它 用于在组件渲染之前显示指定的占位符内容,以及在异步加载组件时显示指定的等待时间内容。这样可以改善用户体验,让用户知道页面正在加载中,同时还可以减少页面闪烁的情况。
举个例子,假设我们有一个异步加载的组件 <AsyncComponent>
,并且在加载完成之前需要显示一个加载中的动画 <Loading>
。在 Vue2 中,可能需要通过特定的代码来实现这个效果
V2 <template><div><!-- 显示加载中的动画 --><Loading v-if="loading" /><!-- 异步加载的组件 --><AsyncComponent v-if="!loading" /></div>
</template><script>
import AsyncComponent from './AsyncComponent.vue'
import Loading from './Loading.vue'export default {components: {AsyncComponent,Loading},data() {return {loading: true // 初始化为 true}},mounted() {// 异步加载组件loadAsyncComponent().then(() => {this.loading = false // 加载完成后将 loading 设为 false})}
}
</script>
在这个例子中,我们使用了 v-if
指令来根据 loading
变量的值来控制组件的显示和隐藏。当 loading
为 true
时,显示 <Loading>
组件;当 loading
为 false
时,显示 <AsyncComponent>
组件。
这种方式虽然可以实现异步加载组件并显示等待效果,但是需要在组件内部写一些特定的代码,稍微有些复杂。而在 Vue3 中,可以使用 Suspense 组件来简化这个过程:
<template><Suspense><template #default><AsyncComponent /></template><template #fallback><div>Loading...</div></template></Suspense>
</template><script>
import { defineAsyncComponent } from 'vue'const AsyncComponent = defineAsyncComponent(() =>import('./AsyncComponent.vue')
)export default {components: {AsyncComponent,},// 在组件中添加 errorCaptured 钩子函数errorCaptured(err, vm, info) {console.error('异步组件加载失败:', err, vm, info)}
};
</script>
在这个例子中,我们首先通过 defineAsyncComponent
函数定义了异步组件 AsyncComponent
。然后,在模板中使用 Suspense
组件包裹该组件,并使用 #default
和 #fallback
指令分别指定默认内容和备选内容。
当异步组件正在加载时,就会显示备选内容 <div>Loading...</div>
;等到异步组件加载完成后,就会显示 AsyncComponent
组件的内容。
需要注意的是,Suspense 组件只能包裹一个异步组件,并且该组件必须是通过 defineAsyncComponent 函数定义的异步组件。超出这个范围的使用可能导致程序异常
需要注意的是,如果异步组件加载失败,那么会触发 errorCaptured
钩子函数,并显示备选内容。因此,在使用 Suspense 组件时,建议加上相应的错误处理逻辑,以便更好地处理异常情况。
1-3 Teleport [ˈtelipɔːt]
Vue 3 中的 Teleport 组件提供了一种方便的方式,可以将组件渲染到指定的目标位置,而不需要手动处理 DOM 操作。
Teleport 组件的使用非常简单,在组件中包裹要渲染的内容,并通过 to
属性指定目标位置即可。例如:
<template><fragment><button @click="showModal = true">显示模态框</button><!-- 插入body中 --><teleport to="body"><div v-if="showModal" class="modal"><!-- 插入组件 --><HelloWorld /><div class="modal-content"><h2>这是一个模态框</h2><p>这是模态框的内容</p><button @click="showModal = false">关闭</button></div></div></teleport></fragment>
</template><script setup>
import HelloWorld from "~/HelloWorld.vue";
import { ref } from "vue";
let showModal = ref(false);
</script><style>
.modal {position: fixed;top: 0;left: 0;width: 100%;height: 100%;background: rgba(0, 0, 0, 0.5);display: flex;justify-content: center;align-items: center;
}.modal-content {background: #fff;padding: 20px;border-radius: 5px;
}
</style>
在这个示例中,我们使用Teleport将模态框的内容渲染到了<body>
标签中。这意味着无论我们在页面上的哪个位置点击“显示模态框”按钮,模态框都会在页面的中央出现。
需要注意的是,在使用 Teleport 组件时,目标位置必须是存在于当前 HTML 文档中的元素。同时,如果目标位置在其他组件中,也需要确保该组件已经被渲染到 HTML 文档中。否则,渲染可能会失败或者产生意料之外的效果。
1-4 v-memo
v-memo:可以做性能优化,v-memo中值若不发生变化,整个子树的更新会被跳过。
场景描述
假设后端返回来了100条数据。
前端需要做筛选。
选出符合条件的数据进行展示。
如果没有符合条件的。则保持上次的搜索结果。
<template><div class="home"><input type="text" v-model="value" /><!-- v-memo中值若不发生变化,则不会进行更新 --><ul v-memo="[shouldUpdate]"><li class="licss" v-for="item in arr" :key="item">{{ value }} -- {{ animalType[value] }}</li></ul></div>
</template>
<script lang="ts" setup>
import { ref, watch } from "vue";const arr = new Array(100);
// 这个对象可以有任意数量的字符串类型的属性,并且它们的值也都是字符串类型
const animalType: { [key: string]: string } = {yang: "羊",niu: "牛",miao: "猫",
};
const value = ref("niu");
const shouldUpdate = ref(0);// 监听value(输入框中的值)。
// 如果数据发生变化,并且在animalType对象中存在。试图进行更新。否则试图不进行更新。
watch(() => value.value,() => {if (Object.keys(animalType).includes(value.value)) {shouldUpdate.value++;}}
);
</script>
2- 说一下 Composition API
和 Options API 的区别?
Composition API
也叫组合式 API,它主要就是为了解决 Vue2 中 Options API 的问题。
一是在 Vue2 中只能固定用 data
、computed
、methods
等选项组织代码,在组件越来越复杂的时候,一个功能相关的属性和方法就会在文件上中下到处都有,很分散,变越来越难维护
二是 Vue2 中虽然可以用 minxin
来做逻辑的提取复用,但是 minxin 里的属性和方法名会和组件内部的命名冲突,还有当引入多个 minxin 的时候,我们使用的属性或方法是来于哪个 minxin 也不清楚
而 Composition API
刚才就解决了这两个问题,可以让我们自由的组织代码,同一功能相关的全部放在一起,代码有更好的可读性更便于维护,单独提取出来也不会造成命名冲突,所以也有更好的可扩展性
3- 说一下 setup
// 方法
setup(props, context){ return { name:'uname' } }
它接收两个参数 `props` 和 `context`。
它里面不能使用 `this`,而是通过 context 对象来代替当前执行上下文绑定的对象,
context 对象有四个属性:`attrs`、`slots`、`emit`、`expose`
props:组件的props属性,可以通过props属性接收父组件传递的数据。
context:组件的上下文对象,包含了一些常用的属性和方法,如attrs、slots、emit等。
attrs:父组件传递的非props属性,可以通过attrs来获取。
slots:插槽内容,可以通过slots来获取插槽内容。
emit:触发父组件事件的方法,可以通过emit来触发父组件的事件。// 语法糖<script setup> ... </script>
setup()
方法是在 beforeCreate()
生命周期函数之前执行的函数;
4- watch 和 watchEffect 的区别
watch
作用是对传入的某个或多个值的变化进行监听;触发时会返回新值和老值;也就是说第一次不会执行,只有变化时才会重新执行
const name = ref('沐华')
watch(name, (newValue, oldValue)=>{ ... }, {immediate:true, deep:true})// 响应式对象
const boy = reactive({ age:18 })
watch(()=>boy.age, (newValue, oldValue)=>{ ... })// 监听多个
watch( [name, ()=>boy.age], (newValue, oldValue)=>{ ... } )
watchEffect
是传入一个立即执行函数,所以默认第一次也会执行一次;不需要传入监听内容,会自动收集函数内的数据源作为依赖,在依赖变化的时候又会重新执行该函数,如果没有依赖就不会执行;而且不会返回变化前后的新值和老值
watchEffect(onInvalidate =>{ ... })
<template><p>Count: {{ state.count }}</p>
</template><script setup>
import { reactive, watchEffect } from "vue";const state = reactive({count: 0,
});const timer = setInterval(() => {state.count++;
}, 1000);watchEffect((onInvalidate) => {console.log(111);document.title = `Count: ${state.count}`;// onInvalidate函数会在watchEffect停止监测响应式数据时执行,例如组件卸载时或者手动停止监测。onInvalidate(() => {if (state.count >= 2) clearInterval(timer);console.log(666);});
});onUnmounted(() => {clearInterval(timer);
});
</script>
这段代码会触发onInvalidate是因为在watchEffect函数中,我们监听了响应式数据state.count的变化,当state.count变化时,watchEffect函数会重新执行,并且在重新执行前会先停止上一次的执行,同时触发onInvalidate函数。,从而更新document.title的值。同时,我们还传递了一个回调函数onInvalidate,这个函数会在watchEffect停止监测响应式数据时执行,例如组件卸载时或者手动停止监测。在onInvalidate中,我们判断了state.count的值是否大于等于2,如果是,则清除定时器timer,并输出666。所以当state.count的值达到2时,会触发onInvalidate函数。
在watchEffect函数第一次执行时并不会触发onInvalidate函数,因为在这个时候watchEffect函数还没有开始监测任何响应式数据,也就不存在停止监测的情况。只有在watchEffect函数开始监测响应式数据后,当响应式数据发生变化时,watchEffect函数会重新执行,并且在重新执行前会先停止上一次的执行,同时触发onInvalidate函数。
5- Vue3 响应式原理和 Vue2 的区别
众所周知 Vue2
数据响应式是通过 Object.defineProperty()
劫持各个属性 getter 和 setter,在数据变化时发布消息给订阅者,触发相应的监听回调,而这之间存在几个问题
-
初始化时需要遍历对象所有 key,如果对象层次较深,性能不好
-
通知更新过程需要维护大量 dep 实例和 watcher 实例,额外占用内存较多
-
Object.defineProperty 无法监听到数组元素的变化,只能通过劫持重写数方法
-
动态新增,删除对象属性无法拦截,只能用特定 set/delete API 代替
-
不支持 Map、Set 等数据结构
监听数组元素的变化:Vue2通过重写数组的部分原型方法(如push、pop、shift等),在这些方法调用时手动触发更新。这些方法被称为“变异方法”,因为它们会改变原始数组,而不需要进行显式的重新赋值操作。Vue2还提供了一些辅助函数(如set、set、delete等)来实现主动修改数据并触发更新。
对象新增/删除属性的变化:Vue2无法监听对象新增或删除属性的变化。但是,可以使用Vue.set和Vue.delete方法来添加或删除响应式属性,并触发更新。
而在 Vue3
中为了解决这些问题,使用原生的 proxy
代替,支持监听对象和数组的变化,并且多达13种拦截方法,动态属性增删都可以拦截,新增数据结构全部支持,对象嵌套属性只代理第一层,运行时递归,用到才代理,也不需要维护特别多的依赖关系,性能取得很大进步
6- defineProperty 和 Proxy 的区别
为什么要用 Proxy 代替 defineProperty ?好在哪里?
- Object.defineProperty 是 Es5 的方法,Proxy 是 Es6 的方法
- defineProperty 不能监听到数组下标变化和对象新增属性,Proxy 可以
- defineProperty 是劫持对象属性,Proxy 是代理整个对象
- defineProperty 局限性大,只能针对单属性监听,所以在一开始就要全部递归监听。Proxy 对象嵌套属性运行时递归,用到才代理,也不需要维护特别多的依赖关系,性能提升很大,且首次渲染更快
- defineProperty 会污染原对象,修改时是修改原对象,Proxy 是对原对象进行代理并会返回一个新的代理对象,修改的是代理对象
- defineProperty 不兼容 IE8,Proxy 不兼容 IE11
7- proxy
Proxy
是 ES6 中新增的一个对象,用于创建一个对象的代理,可以拦截并重定义对象的各种操作,比如属性访问、属性赋值、函数调用等等。
使用 Proxy
可以对目标对象进行拦截和代理,从而可以实现一些高级的功能,比如数据绑定、数据校验、数据缓存等等。
Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”
const proxy =new Proxy(target,handle);
Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。
其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
Proxy
的使用方式如下:
<script setup lang="ts">
type Person = {name: string;age: number;
};const person: Person = {name: "张三",age: 25,
};const handler = {get(target: never, prop: string) {console.log(`获取: ${prop}`);return target[prop];},set(target: never, prop: string, value: never) {console.log(`设置: ${prop} to ${value}`); //设置: age to 30target[prop] = value;return true;},
};const proxy = new Proxy(person, handler);proxy.age = 30;
console.log(proxy.age); // 输出 30
</script>
上面的代码中,我们创建了一个 target
对象和一个 handler
对象,然后使用 new Proxy()
方法创建了一个 proxy
对象,通过 proxy
对象可以对 target
对象进行代理。在 handler
对象中,我们定义了 get
和 set
方法,用于拦截 proxy
对象对 target
对象的访问和修改操作,从而可以在这些操作中进行一些自定义的处理。
8- Vue3 的生命周期
基本上就是在 Vue2 生命周期钩子函数名基础上加了 on
;beforeDestory 和 destoryed 更名为 onBeforeUnmount 和 onUnmounted;然后用setup代替了两个钩子函数 beforeCreate 和 created;新增了两个开发环境用于调试的钩子
9- Vue3的diff算法和Vue2的区别
Vue2 使用的是双向指针遍历虚拟DOM树,对比新旧节点,进行增、删、改操作。
Vue3 中使用了静态分析,将模板转成渲染函数,在编译时生成标记和缓存,优化了渲染过程。同时也引入了新的响应式系统,通过 Proxy 和 Reflect 对象实现,提供更好的性能和开发体验。在 diff 算法方面,Vue3 引入了 PatchFlag 标记位,用于标识节点是否需要更新,只对有变化的节点进行更新操作,减少了不必要的计算。此外,Vue3 还采用了 Block tree 结构优化组件树的更新,提高了性能。
Vue3的diff算法相较于Vue2主要有以下几个方面的改进:
- 静态分析
在Vue3中,编译器会将模板转化为渲染函数,并在编译时根据代码的静态结构来生成标记和缓存。这样做优化了渲染过程,避免了运行时的重复计算
- 新的响应式系统
在Vue3中,响应式系统通过ES6的Proxy API和Reflect对象实现,代替了Vue2中的Object.defineProperty。这样做提供了更好的性能和开发体验。
- PatchFlag
Vue3中引入了PatchFlag标记位,用于标识是否需要对节点进行更新。只有被标记为需要更新的节点才会进行比较和更新操作,从而减少了不必要的计算。PatchFlag标记包括以下几种:
- TEXT:文本内容变化
- CLASS:class绑定变化
- STYLE:style绑定变化
- PROPS:非响应式属性变化
- FULL_PROPS:所有属性变化(包括响应式属性)
- HYDRATE_EVENTS:事件处理函数变化
- Block tree
Vue3中采用了Block tree结构优化组件树的更新。Block tree是一个基于深度优先遍历的树形结构,这可以使得Vue3在进行模板编译时就可以将组件树转化为Block tree,这样在执行diff算法时可以避免对整棵组件树的遍历。
总结来说,Vue3的diff算法主要通过静态分析、新的响应式系统、PatchFlag标记和Block tree优化等手段来提高性能和效率。相较于Vue2的双向指针遍历虚拟DOM树进行增删改操作,Vue3在维护响应式数据方面具有更高的效率,并将渲染过程更好地与Vue的生命周期钩子关联起来,提高了整个框架的性能和开发体验。
在Vue2中,响应式系统是通过Object.defineProperty来实现的。这种方式虽然可以实现数据响应式,但是在以下几个方面存在一些限制:
- 对象新增或删除属性无法响应
由于Object.defineProperty需要显式定义getter和setter,所以对于对象新增或删除属性时,不能自动地响应到视图层。
- 数组变异方法无法响应
除非通过重写数组原型上的方法来手动实现,否则使用数组变异方法(如push、pop、shift、unshift、splice、sort、reverse)时,视图无法自动更新。
- 无法监听属性的不存在和深层次嵌套的属性
由于Vue2的响应式系统只能监听已经存在的键值,无法监听新增键值,也无法监听深层次嵌套的属性。
为了解决以上问题,Vue3采用了ES6的Proxy API和Reflect对象来实现响应式系统,代替了Vue2中的Object.defineProperty。
具体实现方式如下:
- 使用Proxy API代理目标对象
使用Proxy API创建一个目标对象的代理,当对代理对象进行读取或修改时,会被拦截并触发相应的操作。
- 在get和set拦截器中监听数据变化
在get拦截器中监听数据的读取行为,如果读取的是响应式数据,则将其依赖收集起来,并返回对应的值。在set拦截器中监听数据的修改行为,如果发现数据被修改,则触发依赖更新。
- 使用Reflect API进行操作
由于Proxy API只是一个代理对象,所以无法直接对目标对象进行操作。因此,在拦截器中需要使用Reflect API来进行真正的操作。
通过以上方式,Vue3的响应式系统可以实现以下功能:
- 自动响应新增或删除属性的变化
使用Proxy代理时,不需要显式地定义getter和setter,因此可以自动响应新增或删除属性的变化。
- 数组变异方法也能够响应
通过代理数组并重写其原型上的变异方法,可以实现对数组的响应式支持。
- 能够监听不存在和深层次嵌套的属性
通过递归代理子属性,可以实现对深层次嵌套的属性的监听。
总之,使用ES6的Proxy API和Reflect对象实现Vue3的响应式系统,可以提高性能和减少代码量,并且在功能上更加灵活和强大。
10- SPA与前端路由
一、SPA单页面
前端路由本质是,通过改变 URL,在不重新请求页面的情况下,更新页面视图。
传统的页面应用,是用一些超链接来实现页面切换和跳转的。而vue-router单页面应用中,则是路径之间的切换,也就是组件的切换。路由模块的本质 就是建立起url和页面之间的映射关系。
SPA(单页面应用,全程为:Single-page Web applications)指的是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序,简单通俗点就是在一个项目中只有一个html页面,它在第一次加载页面时,将唯一完成的html页面按需加载的组件一起下载下来,所有的组件的展示与切换都在这唯一的页面中完成,这样切换页面时,不会重新加载整个页面,而是通过路由来实现不同组件之间的切换。
优点
- 具有桌面应用的即时性、网站的可移植性和可访问性
- 用户体验好、快,内容的改变不需要重新加载整个页面
- 良好的前后端分离,分工更明确
缺点
- 不利于搜索引擎的抓取
- 首次渲染速度相对较慢
二、vue-router实现原理(模式)
更新视图但不重新请求页面,是前端路由原理的核心之一,目前在浏览器环境中这一功能的实现主要有2种方式:
hash – 默认值,利用 URL 中的hash (http://localhost:8080/#/login)
history – 利用URL中的路径 http://localhost:8080/login)
三、hash模式和history模式的区别
hash的url有’#'号,history没有
history和hash都是利用浏览器的两种特性实现前端路由,history是利用浏览历史记录栈的API实现,hash是监听location对象hash值变化事件来实现
history是H5新增,并且需要后端配合,如果后端不配合刷新新页面会出现404,hash不需要
HashRouter的原理:通过window.onhashchange方法获取新URL中hash值,再做进一步处理;HistoryRouter的原理:通过history.pushState 使用它做页面跳转不会触发页面刷新,使用window.onpopstate 监听浏览器的前进和后退,再做其他处理
hash模式
vue-router默认hash模式,使用URL的hash来模拟一个完成URL,于是当URL改变时,页面不会重新加载
hash(#)是URL的锚点,代表的是页面中的某个位置,单单改的是#后的部分,浏览器只会滚动搭到相应的位置,不会重新加载页面,也就是说hash 出现在 URL 中,但不会被包含在 http 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面
同时每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用”后退”按钮,就可以回到上一个位置;
Hash模式通过锚点值的改变,然后通过解析hash值,获取相应的路由信息,最终渲染出对应的页面。
hash 模式的原理是 hashchange 事件(监测hash值变化),可以在 window 对象上监听这个事件。
window.addEventListener('hashchange', function(event) {// 获取新的hash值并解析路由信息var newHash = location.hash.slice(1);var routeInfo = parseRoute(newHash);// 根据路由信息更新动态内容updateContent(routeInfo);
});
history模式
在history模式中,通过使用HTML5 History API来实现路由的切换。这个API包括了pushState、replaceState和popstate三个方法,分别用于添加、修改和删除浏览历史记录中的状态。通过使用这些方法,我们可以在不刷新页面的情况下修改URL,并且通过监听popstate事件来更新路由信息。
例如,以下代码可以使用pushState方法来添加一个新的浏览历史记录,并根据新的路径来更新路由信息:
var newPath = '/new/path';
var newState = { some: 'data' };history.pushState(newState, '', newPath);// 监听popstate事件来更新路由信息
window.addEventListener('popstate', function(event) {var routeInfo = getRouteFromURL(location.pathname);// 根据路由信息更新动态内容updateContent(routeInfo);
});
四、路由跳转
vue2中路由跳转
//query 传值
methods: {gotodetail(item) {this.$router.push({ path: "/detail", query: { obj: item } });},},//取值
<script>
export default {data() {return {obj: this.$route.query.obj,};},
};
</script>//params传参用name 没有 "/"
//对home页面进行id为1,state为0的传值。
this.$router.push({ name: 'homeName', params: {id:'1',state:0}}) //此方法需在router.js中配置name
{path: '/home/:id',name: 'homeName',component: () => import('../pages/index/home.vue')
},//params 接收参数
export default {data() {return {id:this.$route.params.idstate:this.$route.params.state };},
};// 声明式<div @click="$router.push({ name: 'home', params: { id: 6666666 } })">跳转home</div>{{ $route.params.id }}
vue3 query跳转
第一步:引入
import {useRouter} from 'vue-router' 第二步:存给变量
const router = useRouter();第三步:携参数跳转
setup() {function gotodetail() {router.push({path: "/regiter", query: {item: item}});}return {gotodetail};}, vue3 接收
第一步:
import {useRoute} from 'vue-router'第二步:
const route = useRoute();// 声明式跳转 @click="$router.push({path: '/regiter', query: {name: item.good_name}})"
//接收 <p>{{$route.query.name}}</p>
Vue3跳转新页面
// 声明式
<router-link target="_blank" :to="{ path: '/home', query: { id: '8' } }">新页面打开home页</router-link>// 声明式接受 {{ $route.query.id }}
// 编程式
import { useRouter } from "vue-router";
const router = useRouter();<div @click="jumpLink">跳转new_home</div>const jumpLink = () => {const newPage = router.resolve({name: "home",params: {id: "一giao我里giao",},});window.open(newPage.href, "_blank");
};
// 动态路由{path: "/home/:id",name: "home",component: () => import("./home.vue"),}, // 行内接受 {{ $route.params.id }} // 编程式接受
import { useRoute } from "vue-router";
const route = useRoute();
console.log('\😂👨🏾❤️👨🏼==>: ', route.params.id);
router.resolve和 router.push和router.replace的区别
router.resolve
方法是用于解析目标路由的。它接收一个对象作为参数,该对象包含了进入新路由的信息,例如path
、name
、params
等,然后返回一个包含了有关目标路由的信息的Promise,其中包括原始的、规范化的和解析后的路由信息。通常情况下,我们不会直接使用router.resolve
方法进行页面跳转,而是使用router.push
或router.replace
来完成路由的跳转。
router.push
方法是用于进行路由导航的。它接收一个路由对象或者一个路由路径字符串作为参数,用于切换到新路由。在新路由被确认之前,可以取消当前正在进行的导航。该方法会将新的路由添加到浏览器的访问历史记录中,因此在浏览器中按返回按钮可以回退到前一个路由。
router.replace
方法也是用于进行路由导航的。它与router.push
方法的作用类似,但是不会向浏览器的访问历史记录中添加新的路由。因此在使用router.replace
方法后,无法通过浏览器的返回按钮回退到前一个路由。
总结,router.resolve
、router.push
、router.replace
都是Vue Router中的路由跳转方法,它们的作用不同。router.resolve
方法用于解析目标路由,返回一个解析后的路由对象;router.push
方法可用于进行导航跳转,并将新的路由添加到浏览器的访问历史记录中;router.replace
方法也可用于进行导航跳转,但不会将新的路由添加到历史记录中,因此无法通过浏览器的返回按钮回退到前一个路由。根据实际需求选择合适的方法即可。
11- 路由守卫
分类:
全局守卫
单个路由守卫
组件内部守卫
全局守卫
全局前置守卫beforeEach
场景:用户页面是否跳转到登录界面,是否有权限跳转到某个页面
定义:
beforeEach()全局前置守卫:初始化时执行、每次路由切换前执行;
三个参数:
全局前置守卫中三个参数:to
,from
,next
。
to
:to 表示去往哪个路由
from
:from 表示从哪个路由离开
next
: next 是一个方法,代表的是是否展示路由页面,如果不使用next方法,那么指定页面无法显示
route中有一个配置项是meta,专门提供给我们,在meta项中配置自己的属性与值,可用于在守卫中的权限判断
//前置路由
router.beforeEach((to, from, next) => {if (localStorage.getItem('token')) {next()} else {if (to.meta.isRelease) { //没有token有些页面也是可以放行的 isRelease为true 守卫不让进,跳入login// 将用户重定向到登录页面return { name: 'Login' }} else {next()// 返回 false 以取消导航// return false}}})
// 全局后置守卫afterEach (当你真正进入到某个页面之后才执行)//对于分析、更改页面标题、声明页面等辅助功能都很有用
//你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:
router.afterEach((to, from) => {// 设置路由的标题 (可自定义)document.title = to.meta.title || '常驻标题'// 将所有的页面切换之后滚动到最顶部window.scrollTo(0, 0)
})
全局解析守卫beforeResolve
是获取数据或执行任何其他操作(如果用户无法进入页面时你希望避免执行的操作)的理想位置(用的不多)
你可以用
router.beforeResolve
注册一个全局守卫。这和router.beforeEach
类似,因为它在每次导航时都会触发,不同的是,解析守卫刚好会在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用。这里有一个例子,确保用户可以访问自定义 meta 属性requiresCamera
的路由:
router.beforeResolve(async to => {if (to.meta.requiresCamera) {try {await askForCameraPermission()} catch (error) {if (error instanceof NotAllowedError) {// ... 处理错误,然后取消导航return false} else {// 意料之外的错误,取消导航并把错误传给全局处理器throw error}}}
})
路由独享的守卫
beforeEnter
守卫 只在进入路由时触发,不会在params
、query
或hash
改变时触发。例如,从/users/2
进入到/users/3
或者从/users/2#info
进入到/users/2#projects
。它们只有在 从一个不同的 路由导航时,才会被触发。你也可以将一个函数数组传递给
beforeEnter
,这在为不同的路由重用守卫时很有用:
{path: '/admin',name: 'admin',component: () => import('../views/mine/admin.vue'),//beforeEnter(to,form.next)=>{判断是否登陆代码},点击进入admin也面时,路由独享守卫启用beforeEnter:(to,form,next)=>{if (!localStorage.getItem('user')) {if (to.name == "login") {next();} else {router.push('login')}} else {next();}}
},
组件内部守卫
最后,你可以在路由组件内直接定义路由导航守卫(传递给路由配置的)
可用的配置 API
你可以为路由组件添加以下配置:
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
beforeRouteEnter(to, from) {// 在渲染该组件的对应路由被验证前调用// 不能获取组件实例 `this` !// 因为当守卫执行时,组件实例还没被创建!},beforeRouteUpdate(to, from) {// 在当前路由改变,但是该组件被复用时调用// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`},beforeRouteLeave(to, from) {// 在导航离开渲染该组件的对应路由时调用// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`},
beforeRouteEnter
beforeRouteEnter
守卫 不能 访问this
,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。不过,你可以通过传一个回调给
next
来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数:
<script>import { useRouter } from 'vue-router'import { reactive } from 'vue'export default {setup(){const router = useRouter();var data = reactive({routerIndex:''})let goBack = () =>{// 如果从C.vue来,则返回router.go(-3),回到A.vue,否则正常返回上级页面if(data.routerIndex == 'C'){router.go(-3);}else{router.go(-1);}}return{data,goBack}},// 组件内守卫beforeRouteEnter:(to,form,next)=>{//to 到哪里去//form 从哪里来next( vm => {// 通过 `vm` 访问组件实例vm.data.routerIndex = form.name;})},}
</script>
注意
beforeRouteEnter
是支持给next
传递回调的唯一守卫。对于beforeRouteUpdate
和beforeRouteLeave
来说,this
已经可用了,所以不支持 传递回调,因为没有必要了:
beforeRouteUpdate
beforeRouteUpdate(to, from) {// 在当前路由改变,但是该组件被复用时调用// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`// just use `this`this.name = to.params.name},
这个 离开守卫 通常用来预防用户在还未保存修改前突然离开。该导航可以通过返回
false
来取消。
beforeRouteLeave
beforeRouteLeave (to, from) {const answer = window.confirm('Do you really want to leave? you have unsaved changes!')if (!answer) return false
}
12- 关于前端鉴权这块
Token、Cookie、Session、JWT、单点登录是什么?有什么作用?你一般是怎么做的?以及你是怎么存储的呢?那你又是怎么保证 它 的安全的呢?
关于前端鉴权,下面是这些概念的简要解释和作用:
-
Token: Token 是一种独特的、难以伪造的字符串,用于在客户端和服务器之间传递身份验证信息。它通常在用户登录时颁发,并在后续请求中作为身份验证凭证使用。
-
Cookie: Cookie 是由服务器发送到用户浏览器并在浏览器中存储的小型文本文件。它们通常用于存储用户的会话信息和偏好设置,以及进行身份验证。
-
Session: Session 是服务器端存储的一种临时状态,用于跟踪用户在网站上的活动。Session 通常依赖于 Cookie 来存储会话 ID,以便服务器可以识别特定的用户。
-
JWT (JSON Web Token): JWT 是一种用于身份验证和授权的开放标准。它是一种简洁的、URL 安全的字符串,可用于在客户端和服务器之间安全地传输 JSON 数据。JWT 通常用于无状态 API 认证,因为它们包含了足够的信息来验证用户。
-
单点登录 (Single Sign-On, SSO): 单点登录是一种允许用户在多个应用程序之间使用一个身份验证凭据(如用户名和密码)进行登录的方法。这样可以减少用户需要记住的凭据数量,同时简化身份验证和授权过程。
在实现前端鉴权时,通常的做法是使用 JWT 或 Token。登录时,用户将其凭据发送给服务器,服务器验证后返回一个 JWT 或 Token。该 Token 会被存储在客户端(例如:localStorage 或 sessionStorage),并在后续请求中附加到请求头中,以便服务器验证用户身份。
为了确保安全性,你可以采取以下措施:
- 使用 HTTPS:确保所有传输数据都经过加密,防止中间人攻击。
- 限制 Token 有效期:设置 Token 的过期时间,以减少 Token 被盗用的风险。
- 使用 HttpOnly Cookie:将 Token 存储在 HttpOnly Cookie 中,以防止跨站脚本攻击(XSS)。
- 验证请求来源:使用 CORS 策略验证请求来源,防止跨站请求伪造(CSRF)攻击。
- 服务器端验证:对所有请求进行身份验证和授权,确保用户只能访问他们被授权的资源。
13- app.use() 和 install
app.use() 和 install 都是用于安装插件的方法。
app.use() 方法是全局注册插件的方式。它接收一个插件对象作为参数,该插件对象通常包含一个 install 方法函数用于安装插件。
在 Vue3 应用程序中,这个方法同样很常用,因为它可以在全局范围内注册一些通用的插件,例如路由、状态管理、自定义插件等,使得这些插件可以在整个应用程序中使用。例如:
import MyPlugin from './my-plugin';
app.use(MyPlugin);
在这个例子中,我们将自定义插件 MyPlugin 安装到Vue应用中,MyPlugin 插件必须具有一个 install 方法。
install 方法是一个静态方法,它接收两个参数:app 和其他可选参数(options)。app 是 Vue 应用实例,可以通过该实例访问全局 API 和组件。其他参数可以根据插件的需求进行传递。以下是一个简单的插件示例:
const MyPlugin = {install(app) {// 在这里执行插件的安装逻辑app.config.globalProperties.$myMethod = () => {console.log('This is a custom method provided by MyPlugin.');};}
};export default MyPlugin;
上述示例中,我们通过 install 方法将一个自定义方法 $myMethod 注册到了 Vue 实例的全局属性中。
总结来说,app.use() 方法用于全局注册插件,而插件则通过自身的 install 方法实现具体的逻辑。
在Vue 3中,app.use() 和 install 方法都可以传递一些参数来满足插件的需求。
对于 app.use() 方法,它接收一个插件对象作为参数,并且可以传递额外的参数给插件的 install 方法。这些参数是可选的,根据插件的设计和需求来决定是否需要传递。例如:
import MyPlugin from './my-plugin';
const options = {// 一些配置选项
};
app.use(MyPlugin, options); // 传递给install插件
在这个例子中,我们将 options 对象作为第二个参数传递给了 app.use() 方法,并在插件的 install 方法中接收并使用这些参数。
对于插件的 install 方法,它接收两个参数:app 和其他可选参数。app 是 Vue 应用实例,可以通过该实例访问全局 API 和组件。其他参数则是开发者自定义的,可以根据插件的需要进行传递。例如:
const MyPlugin = {install(app, options) {// 在这里执行插件的安装逻辑,并使用 options 参数console.log(options); // 输出传递的选项对象app.config.globalProperties.$myMethod = () => {console.log('This is a custom method provided by MyPlugin.');};}
};export default MyPlugin;
在这个示例中,我们接收了 options 参数,并在插件的安装逻辑中使用它。
总结来说,app.use() 方法可以传递额外的参数给插件的 install 方法,而插件的 install 方法可以接收这些参数并根据需要进行处理。具体传递什么参数以及如何使用这些参数取决于插件的设计和实现。
-
插件应该在调用 app.use() 之前被导入。
-
插件的 install 方法是可选的,只有当插件需要执行特定的逻辑时才需要定义。如果没有提供 install 方法,插件将被视为一个普通对象。
-
在 install 方法中,可以通过 app.config.globalProperties 将方法或属性添加到全局上下文中。当我们想在组件内调用时需要使用getCurrentInstance()来获取。
// 之前(Vue 2.x) Vue.prototype.$http = () => {}// 之后(Vue 3.x) const app = Vue.createApp({}) app.config.globalProperties.$http = () => {}//当我们想在组件内调用http时需要使用getCurrentInstance()来获取。 import { getCurrentInstance, onMounted } from "vue"; export default {setup( ) {const { ctx } = getCurrentInstance(); //获取上下文实例,ctx=vue2的thisonMounted(() => {console.log(ctx, "ctx");ctx.http();});}, };/** getCurrentInstance代表上下文,即当前实例。ctx相当于Vue2的this, 但是需要特别注意的是ctx代替this只适用于开发阶段,如果将项目打包放到生产服务器上运行,就会出错,ctx无法获取路由和全局挂载对象的。此问题的解决方案就是使用proxy替代ctx,代码参考如下。 **/ import { getCurrentInstance } from 'vue' export default ({name: '',setup(){const { proxy } = getCurrentInstance() // 使用proxy代替ctx,因为ctx只在开发环境有效onMounted(() => {console.log(proxy, "proxy");proxy.http();});} })
-
如果插件依赖其他库或模块,确保在安装插件之前先加载这些依赖项。
-
如果一个插件需要在多个组件中使用,可以考虑使用 mixin 或 provide/inject 来共享功能,而不是将所有逻辑放在插件中。
-
当安装插件时,确保在应用程序的根组件(例如 main.js)中进行注册。
-
多次调用 app.use() 安装同一个插件,并不会重复安装。
14- js修改组件的props(createApp)
在Vue 3中,你可以使用setup函数来创建和返回响应式数据和方法。然后,你可以在JavaScript文件中创建组件实例并调用这些方法。
例如,假设你有一个名为MyComponent的Vue组件,它有一个名为updateValue的方法,你可以这样调用它:
import { createApp, ref } from 'vue';
import MyComponent from './MyComponent.vue';// 创建应用实例
const app = createApp(MyComponent);// 创建组件实例
const componentInstance = app.mount('#app');// 调用方法
componentInstance.updateValue('new value');
在你的Vue组件中,你可以这样定义updateValue方法:
export default {setup() {const value = ref('old value');const updateValue = (newValue) => {value.value = newValue;};return {value,updateValue};}
};
然后,你可以在JavaScript文件中调用这个方法来改变value的值。
请注意,这只适用于在JavaScript文件中直接创建和操作Vue组件实例。如果你在Vue组件模板中使用这个组件,你应该使用props和emit来进行父子组件之间的通信。
还可以通过createApp的第二个参数来传递props给组件。这可以用来修改组件的样式或者其他属性。
例如,假设你有一个名为MyComponent的Vue组件,它接受一个名为color的prop,你可以这样设置它:
import { createApp } from 'vue';
import MyComponent from './MyComponent.vue';// 创建应用实例并传递props
const app = createApp(MyComponent, {color: 'red'
});// 创建组件实例
const componentInstance = app.mount('#app');
在你的Vue组件中,你可以这样定义color prop:
export default {props: {color: String}
};
然后,你可以在组件的样式中使用这个color prop来改变组件的颜色。例如:
<style scoped>
.my-component {color: v-bind(color);
}
</style>
这样,你就可以在JavaScript文件中通过props来修改Vue组件的样式了。
你可以创建一个新的div元素并将Vue应用挂载到它上面。你不一定需要将应用挂载到id为app的元素上,这只是一个常见的约定。
例如,你可以这样做:
import { createApp } from 'vue';
import MyComponent from './MyComponent.vue';// 创建一个新的div元素
const div = document.createElement('div');// 将div元素添加到body中
document.body.appendChild(div);// 创建应用实例
const app = createApp(MyComponent);// 将应用挂载到新的div元素上
app.mount(div);
这样,你的Vue应用就会被挂载到新的div元素上,并且会在页面上显示。
但是,请注意,如果你没有将应用挂载到DOM上,那么你的Vue组件将不会在页面上显示。mount方法是必要的,因为它告诉Vue在哪里渲染你的应用。
15- pinia和mittBus的使用场景
mittBus 是一个事件总线库,它可以用于在 Vue 组件之间传递事件和数据,包括父子组件、兄弟组件,甚至是完全不相关的组件。你可以在一个组件中触发一个事件,然后在另一个组件中监听这个事件。这种方式可以让你在组件之间传递数据,而不需要通过 props 或者 Vuex。
pinia 是一个状态管理库,它是 Vuex 的一个轻量级替代品。你可以使用 pinia 来在应用的不同部分之间共享状态。pinia 提供了一种组织和管理全局状态的方式,它可以让你在不同的组件中访问和修改同一份数据。
mittBus 和 pinia 的主要区别在于它们的使用场景:
- 如果你需要在组件之间传递事件和数据,而这些组件并不共享一个公共的状态,那么 mittBus 可能是一个更好的选择。
- 如果你的应用有一个全局的状态,需要在多个组件中共享,那么 pinia 可能是一个更好的选择。
总的来说,mittBus 更适合于事件驱动的场景,而 pinia 更适合于状态管理的场景。
mittBus 的例子:
<!-- 组件 A -->
<template><!-- ... -->
</template><script setup>
import mittBus from '/@/utils/mitt';onMounted(() => {mittBus.on('customEvent', handleEvent);
});onUnmounted(() => {mittBus.off('customEvent', handleEvent);
});const handleEvent = (data) => {console.log(data);
};
</script>
<!-- 组件 B -->
<template><button @click="triggerEvent">Trigger Event</button>
</template><script setup>
import mittBus from '/@/utils/mitt';const triggerEvent = () => {mittBus.emit('customEvent', 'Hello from Component B');
};
</script>
pinia 的例子:
// store.js
import { defineStore } from 'pinia';export const useStore = defineStore('main', () => {const count = ref(0);const increment = () => {count.value++;};return {count,increment};
});
在这个例子中,我们使用 defineStore 创建了一个新的 store,然后在函数中定义了 count 状态和 increment 方法。这个函数返回一个对象,包含了我们想要暴露的状态和方法。
然后在组件中,你可以这样使用这个 store:
<!-- 组件 A -->
<template><div>Count: {{ count }}<button @click="increment">Increment</button></div>
</template><script setup>
import { useStore } from './store';const store = useStore();
const { count, increment } = toRefs(store);
</script>// import { storeToRefs } from 'pinia';
// import useQuestionStore from '/@/stores/modules/questionBank';
// const useStore = useQuestionStore();// const { setRiskIdMap, getRiskIdMap } = useStore; // 方法可以直接用
// const riskIdMap = storeToRefs(useStore); // 数据需要使用storeToRefs// 模版中可直接使用store变量
// js 中需看响应式声明的方式// ref
console.log(isLogin.value)
// reactive
console.log(state.loginTime)
// https://mp.weixin.qq.com/s/m0aJ5eJ2EGGjggIDYmyPsg
在这个组件中,我们首先导入了 useStore,然后在 setup 函数中使用了它。我们使用 toRefs 将 store 转换为响应式引用,然后在模板中使用 count 状态和 increment 方法。
----------------- JS相关 ------------------
1- CommonJS 和 ES6 模块化的详细总结:
CommonJS
使用 module.exports 导出模块,使用 require() 导入模块。
require() 是动态执行的,可以在代码的任何地方调用。
模块被加载时会创建一个新的对象,并将其分配给 module.exports,因此当其他模块加载时,它们获得的是该对象的一个副本。
ES6
使用 export 和 import 来导入和导出模块。
import 和 export 是静态解析的,导入和导出模块的位置必须在文件的顶部,否则会抛出错误。
export 基于引用的,导出的是一个变量或函数的引用,而不是一个值的副本。这使得在多个模块中共享同一个对象时更加容易。
使用动态 import() 函数可以实现动态加载模块,在运行时异步加载模块。
总体而言,ES6 的模块化提供了一种更加灵活、简洁、清晰和可维护的方式来组织和管理模块化代码。虽然 CommonJS 也能够实现模块化,但是它的语法和机制相对比较简单,缺乏 ES6 的一些高级特性和优势。因此,在使用 JavaScript 进行模块化开发时,建议使用 ES6 的模块化语法来实现。
2- Array.from()
Array.from()
是 JavaScript 中的一个方法,用于将类似数组或可迭代对象转换为一个新的数组实例。
比如:
const arrayLike = {0: 'a', 1: 'b', 2: 'c', length: 3};
const newArray = Array.from(arrayLike);
console.log(newArray); // ['a', 'b', 'c']
在这个例子中,我们首先定义了一个类数组对象 arrayLike
,它有 3 个元素和一个 length
属性。然后我们使用 Array.from()
方法将其转换为一个新数组 newArray
,并输出该新数组的值。
注意:Array.from()
是 ES6 新增的方法,可能不兼容老版本的浏览器。需要注意浏览器兼容性问题。
参数用法
Array.from()
方法有两个参数:
-
第一个参数:必须,指定要转换为数组的对象,可以是类数组对象或可迭代对象,如字符串、Set、Map、NodeList 等。
-
第二个参数(可选):是一个回调函数,用来对数组的每个元素进行处理,类似于数组的
map()
方法。这个回调函数有三个参数:当前元素的值、当前元素的索引,以及原始对象本身。回调函数必须返回处理后的值。
例如:
Array.from([1, 2, 3], num => num * 2); // [2, 4, 6]
这里的第一个参数是一个数组,第二个参数是一个回调函数,将数组中的每个元素乘以 2 并返回一个新的数组。
注意:回调函数支持的参数个数取决于被转换对象的类型,如果被转换的是类数组对象,则回调函数有两个参数,值和索引;
如果被转换的是 Map 或 Set,则回调函数有两个参数,值和 KEY,且 KEY 参数的顺序与 Map 或 Set 中的迭代顺序一致。
3- 浅拷贝
obj1和obj2为什么不一样
浅拷贝只会影响引用类型的属性,而不会影响基本类型的属性。当浅拷贝一个对象时,基本类型的属性会被复制而不是引用
浅拷贝只有是引用类型才会 两个对象相互影响,如果是基本类型不会互相影响
const obj1 = { name: "Alice", age: 30, hobbies: ["reading", "running"] };
const obj2 = Object.assign({}, obj1);obj2.name = "Bob";
obj2.hobbies.push("cooking");console.log(obj1); // { name: "Alice", age: 30, hobbies: ["reading", "running", "cooking"] }
console.log(obj2); // { name: "Bob", age: 30, hobbies: ["reading", "running", "cooking"] }obj1和obj2为什么不一样?
/**
obj1和obj2不一样是因为在使用Object.assign()方法将obj1的属性复制到obj2时,只有name和age这两个基本类型的属性值被复制,而hobbies属性值则是复制了引用。
当我们修改obj2的name属性时,只是修改了obj2的name值,并没有影响obj1的name值。
但是当我们修改obj2的hobbies属性时,实际上是通过复制了引用的方式将新元素"cooking"添加到了原本obj1的hobbies数组中,所以两个对象的hobbies属性值都被修改了。这就是为什么obj1和obj2最终不一样的原因。
**/
4- 闭包
闭包是指一个函数能够访问其词法作用域之外的变量,即使这个函数在词法作用域之外被调用。典型用法函数作为参数被传递,函数作为返回值被返回
词法作用域是指变量的可见性和访问权限是由它们在代码中的位置决定的
作为参数被传递、作为返回值被返回
function outer() {var x = 10;function inner() {console.log(x);}return inner;
}var innerFunc = outer();
innerFunc();
当innerFunc
函数被调用时,它可以访问outer
函数中定义的变量x
。这是因为inner
函数形成了一个闭包,它可以访问其外部函数的词法作用域中的变量。在这个例子中,inner
函数可以访问outer
函数中定义的变量x
,即使outer
函数已经返回并且在全局作用域中调用了innerFunc
函数。
5- js中 ??、 !!、 操作符的意思
??操作符是空值合并操作符。如果左侧的操作数为null或undefined,那么它就会返回右侧的操作数。
// 例如,
'a' ?? 'default' 将返回 'a',如果 'a' 是 undefined 或 null,则返回 'default'。!!: 这个操作符将一个值强制转换为布尔类型。如果值是null,undefined,0,NaN,空字符串("")或者false,那么结果为false。其他所有值都将转换为true。
// 例如,!!0 或者 !!null 都将返回 false,而 !!1 或者 !!'abc' 都将返回 true。
6- js 判断数据类型
在JavaScript中,有多种方式可以判断数据类型。下面是其中一些常用的方法及其优缺点:
- 使用typeof操作符:
alert(typeof "helloworld") ------------------>"string"
alert(typeof 123) ------------------>"number"
alert(typeof [1,2,3]) ------------------>"object"
alert(typeof new Function()) ------------------>"function"
alert(typeof new Date()) ------------------>"object"
alert(typeof new RegExp()) ------------------>"object"
alert(typeof Symbol()) ------------------>"symbol"
alert(typeof true) ------------------>"true"
alert(typeof null) ------------------>"object"
alert(typeof undefined) ------------------>"undefined"
alert(typeof 'undefined') ------------------>"string"
- 优点:这是一种简单、快速的方式,适用于大多数基本数据类型(如数字、字符串、布尔值、undefined)。
- 缺点:对于复杂数据类型(如数组、对象、null),typeof操作符的结果并不准确。它将数组和null都视为"object",不能细分出具体的类型。
- 使用instanceof操作符:
用法:value instanceof Arrayvalue instanceof Object
-
优点:instanceof操作符可以判断一个对象是否属于某个类或构造函数的实例,因此非常适合用于判断复杂数据类型,如数组或自定义对象。
[1,2,3] instanceof Array -------->true new Date() instanceof Date -------->true new Function() instanceof Function -------->true new Function() instanceof function -------->false null instanceof Object -------->false
-
缺点:对于基本数据类型,例如字符串或数字,instanceof操作符并不适用。此外,如果涉及到多个全局执行环境,可能会出现判断错误的情况。
- 使用Object.prototype.toString.call()方法:
用法:Object.prototype.toString.call(value)export const typeOf = <T>(obj: T): string => {return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
};
-
优点:这种方法可以几乎准确地判断数据的类型,无论是基本数据类型还是复杂数据类型。
-
缺点:需要使用较长的代码来实现,并且在使用时需要考虑上下文环境。知名符号Symbol.toStringTag 就可以修改它
例如,我们可以创建一个对象,并使用 Symbol.toStringTag 修改它的默认标签:
let obj = {[Symbol.toStringTag]: 'Custom' };console.log(Object.prototype.toString.call(obj)); *// 输出 '[object Custom]'*
在这个例子中,我们创建了一个对象 obj,并使用 Symbol.toStringTag 修改了它的默认标签为 ‘Custom’。然后,当我们调用 Object.prototype.toString.call(obj) 时,返回的字符串就是 ‘object Custom]’,而不是默认的 ‘[object Object]’。
- 使用Array.isArray()方法(仅适用于判断数组类型):
用法:Array.isArray(value)
- 优点:这是一种简单、直观的方法,用于判断一个值是否为数组。
- 缺点:只适用于判断数组类型,对于其他类型则无法使用。
需要根据具体的使用场景选择合适的方法。在实际开发中,可以结合多种方式来进行数据类型的判断,以满足不同的需求。
7- JWT与Token详解
JWT的结构
一个典型的JWT由三部分组成:头部(header)、有效载荷(payload)和签名(signature)。
头部(Header):一个JSON对象,包含了关于JWT的元数据,如算法(algorithm)和密钥(secret)等。
有效载荷(Payload):另一个JSON对象,包含了实际的数据,如用户信息。
签名(Signature):使用头部中指定的算法和密钥对头部和有效载荷进行签名,以保证JWT的完整性和真实性。
8- map和werkmap区别
Map
键的类型:Map 的键可以是任何值,包括对象、数组、字符串、数字、布尔值等。
键的引用:Map 中的键会创建强引用,这意味着如果你删除或替换了键所对应的对象,那么在 Map 中的键值对仍然存在。
大小:Map 有一个 size 属性,可以告诉你 Map 中有多少个键值对。
遍历顺序:Map 中的键值对是按照插入的顺序进行遍历的。
迭代器:Map 提供了迭代器,可以遍历所有的键值对。
WeakMap
键的类型:WeakMap 的键必须是对象或 null。
键的引用:WeakMap 中的键是弱引用的,这意味着如果键所对应的对象被垃圾回收,那么键值对会被自动移除。
大小:WeakMap 没有 size 属性,因为弱引用可能导致对象被垃圾回收,所以不能准确地知道 WeakMap 中有多少个键值对。
遍历顺序:WeakMap 中的键值对也是按照插入的顺序进行遍历的。
内存泄漏
内存泄漏是指在程序中存在的不再被使用的内存,但是由于某些原因没有被正确释放,导致内存无法被垃圾回收器回收。
强引用: 是我们平常在编程中最常见的引用类型。当一个对象被强引用指向时,只要这个强引用还存在,那么垃圾回收器就不会回收这个对象。例如:
let obj = { name: 'CursorBot' };
在这个例子中,obj 是一个强引用,指向一个对象。只要 obj 还在作用域内,垃圾回收器就不会回收这个对象。
弱引用: 是一种特殊的引用,它不会阻止JavaScript的垃圾回收器回收它引用的对象。这意味着,如果一个对象只被弱引用指向,那么这个对象就可能被垃圾回收器回收。如果没有其他的强引用指向 弱引用的变量,垃圾回收器则会将其回收 。这与普通的 Map 不同。如果我们使用 Map,那么只要 Map 存在,Map 中的键就不会被垃圾回收器回收。
弱引用的主要用途是实现对对象的非主要引用,例如缓存、关联数据等,这些引用不应该阻止垃圾回收器回收对象。
9- class 和 function 的区别?什么时候用那个?
- 语法和结构
function: 传统上,JavaScript使用函数作为构造器来创建对象。函数可以有参数,可以返回一个值,并且可以有局部变量。
class: ES6引入了class关键字,它提供了一种更接近传统面向对象编程语言的语法。class定义了构造函数、实例方法和静态方法,并且提供了extends关键字用于继承。 - 构造器和原型
function: 函数可以作为构造器使用,通过new操作符创建对象实例。每个函数都有一个prototype属性,它是一个对象,包含了所有通过prototype添加的方法和属性,实例对象会继承这些方法。
class: class作为构造器,也有一个prototype属性,其方法和属性被所有实例共享。class通过constructor方法初始化对象,这个方法与函数的构造器类似,但class的constructor方法是自动添加的,如果没有显式定义,JavaScript会提供一个空的constructor。 - 实例化和继承
function: 通过new操作符调用函数来创建实例。
class: 使用new操作符调用class的构造器来创建实例。class支持继承,使用extends关键字可以继承另一个类的属性和方法。 - 静态方法和属性
function: 函数可以直接定义静态方法,也可以通过Object.defineProperty在函数对象上定义静态属性。
class: class提供了静态方法和属性的概念,直接在类上定义,而不是在类的实例上。 - function: 当需要创建具有特定行为的简单对象时,或者当需要将行为封装在一个可复用的函数单元中时。
class: 当需要创建具有层次结构的对象,或者需要编写更接近传统面向对象编程的代码时。 - 总结来说,class是function的语法糖,提供了更接近传统面向对象的编程方式,但本质上仍然是基于原型的继承模型。两者都可以用来创建对象,并根据具体需求选择使用。
10- 知名符号
ES6 定义了“知名符号”来代表 JS 中一些公共行为,而这些行为此前被认为只能是内部操作,但在ES6中使用了知名符号来暴露了内部方法,提供了更加方便的调用这种公用方法的一种方式。每一个知名符号都对应全局 Symbol 对象的一个属性,例如 Symbol.create。
知名符号有:
Symbol.hasInstance :供 instanceof 运算符使用的一个方法,用于判断对象继承关系;
Symbol.isConcatSpreadable :一个布尔类型值,在集合对象作为参数传递给
Array.prototype.concat() 方法时,指示是否要将该集合的元素扁平化;
Symbol.iterator :返回迭代器的一个方法;
Symbol.match :供 String.prototype.match() 函数使用的一个方法,用于比较字符串;
Symbol.replace :供 String.prototype.replace() 函数使用的一个方法,用于替换子字符串;
Symbol.search :供 String.prototype.search() 函数使用的一个方法,用于定位子字符;
Symbol.species :用于产生派生对象的构造器;
Symbol.split :供 String.prototype.split() 函数使用的一个方法,用于分割字符串;
Symbol.toPrimitive :返回对象所对应的基本类型值的一个方法;
Symbol.toStringTag :供 String.prototype.toString() 函数使用的一个方法,用于创建对象的描述信息;
Symbol.unscopables :一个对象,该对象的属性指示了哪些属性名不允许被包含在with 语句中;
Symbol.toStringTag:
Symbol.toStringTag 是一个内置的 Symbol 值,它可以被用来修改 Object.prototype.toString 方法返回的默认标签。例如,我们可以创建一个对象,并使用 Symbol.toStringTag 修改它的默认标签:
let obj = {[Symbol.toStringTag]: 'Custom'
};console.log(Object.prototype.toString.call(obj)); // 输出 '[object Custom]'
Symbol.iterator:
一个返回一个对象默认迭代器的方法。对象需要实现这个方法,才能被 for…of 循环遍历。
let array = [1, 2, 3];
let iterator = array[Symbol.iterator]();
console.log(iterator.next()); // 输出 {value: 1, done: false}
Symbol.species:
一个表示对象默认构造函数的函数。可以被 instanceof 操作符使用。
class MyArray extends Array {static get [Symbol.species]() { return Array; }}let array = new MyArray();let mapped = array.map(x => x);console.log(mapped instanceof MyArray); // 输出 falseconsole.log(mapped instanceof Array); // 输出 true
Symbol.hasInstance:
一个确定一个构造器对象识别的对象是否是它的实例的方法。
class MyClass {static [Symbol.hasInstance](instance) {return Array.isArray(instance);}}console.log([] instanceof MyClass); // 输出 true
Symbol.isConcatSpreadable:
一个布尔值,表示当在一个对象上调用 Array.prototype.concat 时,这个对象的数组元素是否可以被展开。
let array = [1, 2, 3];array[Symbol.isConcatSpreadable] = false;console.log([].concat(array)); // 输出 [[1, 2, 3]]
11- 迭代器(Iterator)和生成器(Generator)
在 JavaScript 中,迭代器是一个对象,它提供了一个 next 方法,这个方法返回一个包含两个属性:value 和 done 的对象。value 属性表示下一个值,done 属性是一个布尔值,当没有更多的值可以返回时,done 为 true。
例如,我们可以创建一个简单的迭代器,它生成一个无限的斐波那契数列:
let fibonacci = {[Symbol.iterator]() {let pre = 0, cur = 1;return {next() {[pre, cur] = [cur, pre + cur];return { done: false, value: cur }}};}
}
let iterator = {current: 1,next() {let result = { value: null, done: true };if (this.current <= 3) {result.value = this.current;result.done = false;this.current++;}return result;}
};console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: null, done: true}
生成器(Generator)
生成器是一种特殊的函数,可以在执行过程中被暂停和恢复。生成器函数使用 function* 语法定义,并且使用 yield 关键字返回值。当你调用一个生成器函数时,它返回一个迭代器,你可以使用这个迭代器的 next 方法来控制函数的执行。
function* generator() {yield 1;yield 2;yield 3;
}let gen = generator();console.log(gen.next()); // {value: 1, done: false}
console.log(gen.next()); // {value: 2, done: false}
console.log(gen.next()); // {value: 3, done: false}
console.log(gen.next()); // {value: undefined, done: true}
总的来说,迭代器和生成器都是处理数据流的工具。迭代器提供了一种通用的接口来遍历序列,而生成器则提供了一种强大的工具来控制函数的执行流程。
12- JavaScript 构造函数、纯函数、new 做了什么?
在 JavaScript 中,构造函数是用于创建新对象的特殊函数。构造函数的名称通常以大写字母开头,以区别于非构造函数。
假设我们有一个构造函数 Person:
function Person(name, age) {this.name = name;this.age = age;
}
当我们使用 new 关键字创建一个新的 Person 实例时:
let john = new Person('John', 30);
JavaScript 实际上做了以下事情:
-
创建一个新的空对象: let john = {};
-
将这个新对象的原型设置为 Person.prototype: john.proto = Person.prototype;
-
将 this 绑定到新创建的对象,并执行 Person 函数的代码:Person.call(john, ‘John’, 30);
-
如果 Person 函数没有返回其他对象,那么 new 表达式将返回这个新对象。在这个例子中,Person 函数没有返回其他对象,所以 new Person(‘John’, 30) 返回的就是 john 对象。
所以,let john = new Person(‘John’, 30); 实际上等价于上述的三个步骤。
纯函数
返回值仅由其输入决定,并且在执行过程中不产生副作用。例如:
function add(a, b) {return a + b; }
在这个例子中,add 是一个纯函数,因为它的返回值仅由其输入 a 和 b 决定,并且在执行过程中不会改变任何外部状态。
可预测性:纯函数的输出只取决于其输入,不依赖于或改变外部状态,这使得它们的行为非常可预测。
可测试性:纯函数更容易进行单元测试,因为你只需要检查函数的输出是否符合预期。
可重用性:纯函数通常更容易重用,因为它们不依赖于特定的上下文或状态。
有些问题可能不适合使用纯函数来解决,例如,如果你需要改变外部状态或执行 I/O 操作(如读写文件或数据库),那么纯函数可能不是最好的选择
13- 正则
匹配所有的数字和点,可以使用字面量正则表达式 /[\d.]/g ,这个正则表达式会匹配所有的数字(\d)和点(.)。
例如,如果你想要从一个字符串中删除所有的数字和点,你可以这样做:
let str = "abc123.456def";
str = str.replace(/[\d\.]/g, '');
console.log(str); // 输出 "abcdef"
这段代码会将 str 中的所有数字和点替换为 ‘’(空字符串),所以输出的结果是 “abcdef”。
^加上这个就是取反?
是的,当 ^ 符号在正则表达式的字符集([])内部时,它表示取反。也就是说,它会匹配所有不在这个字符集中的字符。
例如,[^a-z] 会匹配所有非小写字母的字符,[^0-9] 或 [^\d] 会匹配所有非数字的字符。
所以,[^\d\.] 会匹配所有非数字和非点的字符。
正则表达式字面量和 new RegExp() 是 JavaScript 中创建正则表达式的两种方式,它们有一些区别:
-
语法:正则表达式字面量使用 / 作为开始和结束的界定符,例如 /abc/。new RegExp() 是一个构造函数,接受一个字符串作为参数,例如 new RegExp(‘abc’)。
-
转义字符:在正则表达式字面量中,只需要一个 \ 来转义特殊字符,例如 /\d/。在 new RegExp() 中,需要两个 \ 来转义特殊字符,例如 new RegExp(‘\\d’)。
-
编译时间:正则表达式字面量在脚本加载时编译,而 new RegExp() 在运行时编译。这意味着正则表达式字面量的性能通常更好,但 new RegExp() 可以在运行时动态创建正则表达式。
-
使用场景:如果正则表达式是固定的,可以使用正则表达式字面量。如果正则表达式需要动态生成(例如,包含变量),则需要使用 new RegExp()。
例如:
let str = 'abc';
let regexLiteral = /abc/;
let regexConstructor = new RegExp(str);
14- JS 模块导入导出
export default { nun };
这种导出不能使用 {} ,可以引入赋值为任意变量名。
如果从 模块 a 导出一个变量,并在 模块 b 中导入它,你可以修改这个变量,前提是这个变量 不是原始数据类型(如 string, number, boolean),因为这些是按值传递 的。如果是对象、数组或其他引用类型的话,你可以修改它们的属性或内容,这将反映在原始模块 a 中。
例如:
模块 a (a.ts):
export let myVar = { count: 0 };
模块 b (b.ts):
import { myVar } from './a';// 修改对象属性
myVar.count += 1;
在这个例子中,myVar 是一个对象,你在模块 b 中修改了它的 count 属性,这将影响模块 a 中的 myVar。
但如果 myVar 是一个原始数据类型,比如一个数字或字符串,那么在模块 b 中的任何修改都不会反映到模块 a 中,因为它们是按值传递的。
模块 a (a.ts):
export let myVar = 0;
模块 b (b.ts):
import { myVar } from './a';// 尝试修改值
myVar += 1; // 这不会改变模块 a 中的 myVar
15- JS动态操作类名
假设您有一个元素
const element = document.getElementById('myElement');
删除一个类名
element.classList.remove('my-class');
添加一个类名
element.classList.add('another-class');
切换一个类名(如果存在则删除,不存在则添加)
element.classList.toggle('toggle-class');
16- HTMLElement 、 HTMLDivElement
HTMLElement: 是所有 HTML 元素的基类。它提供了所有 HTML 元素共有的属性和方法,比如 id、className、style 等。当你不确定元素的具体类型,或者不需要访问任何特定于元素类型的属性时,可以使用 HTMLElement。
HTMLDivElement: 继承自 HTMLElement,代表
元素。它提供了所有 HTMLElement 的属性和方法,并且可能会添加一些特定于元素的属性和方法(尽管在当前的 Web 标准中,HTMLDivElement 并没有添加额外的属性或方法)。当你知道正在处理的元素是并且想要表明这一点时,可以使用 HTMLDivElement。
document.querySelectorAll、类名获取、ID 获取
document.querySelectorAll: 这个方法接受一个 CSS 选择器字符串作为参数,返回一个 NodeList 对象,这个对象是一个包含了所有匹配给定选择器的元素的集合。这个方法非常灵活,因为它可以用来选择任何 CSS 选择器可以选择的元素。
通过类名获取 (document.getElementsByClassName): 这个方法接受一个类名作为参数,返回一个包含了所有具有该类名的元素的 HTMLCollection。与 querySelectorAll 不同,它只能通过类名来选择元素。
通过 ID 获取 (document.getElementById): 这个方法接受一个 ID 作为参数,返回一个具有该 ID 的元素。由于 ID 在一个 HTML 文档中是唯一的,所以这个方法返回的是单个元素(HTMLElement),而不是一个集合。
总的来说,HTMLElement 和 HTMLDivElement 是类型注解,用于在 TypeScript 中指定元素的类型,而 document.querySelectorAll、通过类名获取和通过 ID 获取是实际的 DOM 操作方法,用于选取页面中的元素。
document.getElementsByClassName 返回的 HTMLCollection 是一个实时的、动态的集合。这意味着,如果你在遍历这个集合的同时修改了页面上的元素(比如添加或删除了匹配该类名的元素),那么这个集合的内容也会相应地更新,这可能会导致循环的逻辑出现问题,比如无意中造成无限循环。
为了避免这种情况,你可以采取以下几种方法之一:
- 使用静态集合
在开始循环之前,将 HTMLCollection 转换为一个静态的数组。这样,即使原始的 HTMLCollection 在循环过程中发生变化,你遍历的数组也不会改变。
const elements = Array.from(document.getElementsByClassName('your-class-name'));
elements.forEach(element => {// 在这里操作元素
});
2.逆向遍历
如果你需要在循环中移除数组中的元素,逆向遍历确实是一个很好的方法。这样可以避免因为数组长度的变化导致索引错乱的问题。下面是一个使用TypeScript进行逆向遍历并在循环中移除元素的示例:
let arr = [1, 2, 3, 4, 5];// 逆向遍历数组
for (let i = arr.length - 1; i >= 0; i--) {// 根据条件移除元素,这里以移除偶数为例if (arr[i] % 2 === 0) {arr.splice(i, 1);}
}console.log(arr); // 输出: [1, 3, 5]
在这个例子中,我们从数组的末尾开始遍历,检查每个元素是否满足特定条件(这里是是否为偶数)。如果满足条件,就使用splice方法将其从数组中移除。由于我们是从后向前遍历的,即使数组的长度在遍历过程中发生变化,也不会影响到我们的遍历逻辑,从而安全地移除了元素。
17- WebSocket与Web Workers
WebSocket:
是一种网络通信协议,它提供了在单个TCP连接上进行全双工通信的能力。这意味着服务器可以在任何时候向客户端发送消息,客户端也可以随时向服务器发送消息,通信是实时的。
- 适用场景:WebSocket非常适合需要服务器实时推送数据到客户端的场景,如在线聊天、实时游戏、实时交易平台等。
- 轮询对比:与轮询相比,WebSocket更高效,因为它不需要客户端定期发送请求来检查更新。一旦WebSocket连接建立,数据可以随时从服务器推送到客户端,减少了网络延迟和不必要的数据传输。
Web Workers
是一种JavaScript多线程解决方案,允许开发者在后台线程中运行代码,而不会阻塞主线程。
- 适用场景:Web Workers适合执行那些可能会阻塞UI渲染的耗时任务,如大量计算、数据处理或执行复杂的算法。
- 轮询对比:如果你的应用需要定期从服务器获取数据,并且这个过程可能会影响到主线程的性能,那么使用Web Workers来进行轮询是一个好方法。它可以在不影响用户界面和用户交互的情况下,后台定期更新数据。在Web Worker中处理的数据与DOM操作相关,您必须通过postMessage方法与主线程通信,由主线程来更新DOM。这是一种典型的使用模式,其中Worker线程负责执行耗时的计算或数据处理任务,然后将结果发送回主线程,由主线程负责更新用户界面。
在Web Worker中
// 执行任务...
let result = performSomeTask();
// 将结果发送回主线程
self.postMessage(result);
在主线程中
// 创建一个新的Worker
const myWorker = new Worker('worker.js');// 监听来自Worker的消息
myWorker.onmessage = function(e) {// 接收到结果后更新DOMdocument.getElementById('someElement').textContent = e.data;
};// 向Worker发送消息以启动任务
myWorker.postMessage('start');
通过这种方式,Web Workers可以在不干扰主线程的情况下执行复杂的任务,而主线程则可以继续处理用户交互和更新UI。
哪个更好?
- 如果你需要服务器实时推送数据,WebSocket是更好的选择。它可以大大减少服务器和客户端之间的通信延迟和负载。
- 如果你需要处理耗时的任务,而这些任务不需要服务器实时推送数据,那么Web Workers是一个好选择。
在选择技术时,应该根据实际需求来决定。如果你的应用不需要服务器实时推送数据,而是需要定期从服务器拉取数据,那么结合使用Web Workers进行轮询可能是一个合适的选择。如果你的应用场景需要实时的双向通信,那么WebSocket可能更适合你的需求。
总结
Web Workers 是一个标准的API,它允许在后台线程中运行代码,而不会干扰主线程的执行。是一种双向通信协议,需要服务器和客户端都实现相应的逻辑来建立和维护持久连接。
WebSocket 是一个标准的API,它提供了一种在客户端和服务器之间进行全双工通信的能力。是浏览器的多线程解决方案,允许在后台线程中执行代码,以避免阻塞UI,它们不需要服务器端的特别配合,除非Worker需要通过网络与服务器通信(例如,通过HTTP请求)。
---------- TS相关 ------------------
1- declare 和 type 和 interface
declare 主要用于在 TS 中定义和声明全局变量或模块或者第三方依赖,可以用来告诉编译器某个变量或函数已经在其他地方定义过了,避免报错。
在TypeScript中,type和interface都用于创建自定义类型。它们之间的区别在于:
interface主要用于对象的形状(属性和方法)描述,而type可以描述任何类型,包括基本类型、联合类型、函数类型等。
interface支持继承和合并,可以通过extends关键字扩展其它接口。而type不支持继承和合并,但可以使用交叉类型(&)组合多个类型。
type允许为已有类型定义别名,这样可以方便地重复使用类型。而interface不能定义别名。
一般来说,如果需要描述一个对象的形状,就使用interface;如果需要描述一个类型别名,或者需要描述更复杂的类型,就使用type。但是实际项目中,这两种方式也可以混合使用,根据实际需要灵活选择。
declare 示例:
// 声明全局变量
declare var globalVariable: string;// 使用全局变量
console.log(globalVariable);
axios示例 vite.config.ts 和 request.ts
server: {host: '0.0.0.0',port: env.VITE_PORT as unknown as number,open: env.VITE_OPEN,proxy: {// 选项写法'/api': {// 1 目标路径 这里相当于公共的地址target: 'https://api.oioweb.cn/api/ai/chat',//2 允许跨域changeOrigin: true,// 3 重写路径rewrite: (path: string) => path.replace(/^\/api/, ''),},},},
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Session } from '/@/utils/storage';// 配置新建一个 axios 实例
const service: AxiosInstance = axios.create({baseURL: import.meta.env.VITE_API_URL,timeout: 50000,headers: { 'Content-Type': 'application/json' },
});// 添加请求拦截器
service.interceptors.request.use((config: AxiosRequestConfig) => {// 在发送请求之前做些什么 tokenif (Session.get('token')) {config.headers!['Authorization'] = `${Session.get('token')}`;}return config;},(error) => {// 对请求错误做些什么return Promise.reject(error);}
);// 添加响应拦截器
service.interceptors.response.use((response) => {// 对响应数据做点什么const res = response.data;if (res.code && res.code !== 0) {// `token` 过期或者账号已在别处登录if (res.code === 401 || res.code === 4001) {Session.clear(); // 清除浏览器全部临时缓存window.location.href = '/'; // 去登录页ElMessageBox.alert('你已被登出,请重新登录', '提示', {}).then(() => {}).catch(() => {});}return Promise.reject(service.interceptors.response);} else {return response.data;}},(error) => {// 对响应错误做点什么if (error.message.indexOf('timeout') != -1) {ElMessage.error('网络超时');} else if (error.message == 'Network Error') {ElMessage.error('网络连接错误');} else {if (error.response.data) ElMessage.error(error.response.statusText);else ElMessage.error('接口路径找不到');}return Promise.reject(error);}
);// 导出 axios 实例
export default service;
axios.d.ts
/* eslint-disable */
import * as axios from 'axios';// 扩展 axios 数据返回类型,可自行扩展
declare module 'axios' {export interface AxiosResponse<T = any> {code: number;data: T;message: string;type?: string;/* [key: string]: T; 这段代码是定义了一个索引签名,它表示可以使用任意字符串作为key,并且对应的值的类型是T。索引签名允许在对象中使用动态的属性,也就是说,在定义AxiosResponse接口时,除了预定义的code、data、message属性,还可以添加其他任意属性,且属性的值的类型是T。*/[key: string]: T;}
}
在上述代码中,当我们调用了axios的get方法发送请求后,得到的响应对象会被传递到.then()
函数中。在该函数中,我们可以访问响应对象的.data
属性,该属性的类型就是AxiosResponse
,并且可以获取到服务端返回的数据。
所以,不需要在具体的代码中使用AxiosResponse
类型,它已经自动绑定到axios的响应对象上了。你可以直接使用响应对象的.data
属性来获取返回数据。
api.ts
import request from '/@/utils/request';/*** @method signIn* @param params 登录用户名和密码* @description 登录* @returns Promise* @author witt* @createDate 2023/06/25 11:33:13* @lastFixDate 2023/06/25 11:33:13*/
export const signIn = (params: object) => {return request.post('/user/signIn', params);
};/*** @method signOut* @description 退出* @returns Promise* @author witt* @createDate 2023/06/25 11:33:59* @lastFixDate 2023/06/25 11:33:59*/
export const signOut = () => {return request.post('/user/signIn');
};
type 示例:
// 定义类型别名
type Point = { x: number; y: number };// 使用类型别名
const point: Point = { x: 1, y: 2 };
interface 示例:
// 定义接口
interface Person {name: string;age: number;sayHello: () => void;
}// Student 类实现了 Person 接口
/**
表示 Student 类实现了 Person 接口,Student 类拥有 Person 接口中定义的属性和方法。这意味着,在实现 Student 类的过程中,必须要提供 Person 接口所要求的属性和方法的实现。通过实现接口,可以确保类的实例满足特定的接口约束,以保持代码的一致性和可预测性。
接口定义了一种合同或契约,而类则承诺实现这个合同。通过使用 implements 关键字,我们可以明确表示一个类实现了一个或多个接口,以达到接口定义的要求。
**/
class Student implements Person {name: string;age: number;constructor(name: string, age: number) {this.name = name;this.age = age;}sayHello() {console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);}
}const student = new Student("Alice", 20);
student.sayHello(); // 输出:Hello, my name is Alice and I am 20 years old.
// 定义一个对象的接口
interface Person {name: string;age: number;
}// 定义一个函数类型的别名
type AddFunc = (num1: number, num2: number) => number;// 组合多种类型,定义一个包含字符串和数字的联合类型别名
type LogLevel = 'info' | 'warning' | 'error' | number;// 定义一个继承自Person的接口
interface Student extends Person {grade: number;
}------------------------------------------------------------------
// 实现一个函数,参数为一个Person对象,返回值为字符串
function getInfo(person: Person): string {return `My name is ${person.name}, and I am ${person.age} years old.`;
}// 定义一个变量,类型为AddFunc的别名
const add: AddFunc = function(num1, num2) { return num1 + num2;
};// 定义一个变量,类型为LogLevel的别名
const logLevel: LogLevel = 'info';// 定义一个变量,类型为Student接口
const student: Student = { name: 'Alice', age: 18, grade: 12 };
2- 泛型和函数重载
- 泛型允许我们在定义函数、接口或类时 使用类型参数,使得代码可以 适用于多种类型的数据。
- 泛型能够 增加代码的复用性和灵活性。
function reverse<T>(arr: T[]): T[] {return arr.reverse();
}
// 数组字符类型
const strings = ["hello", "world"];
const reversedStrings = reverse(strings); // ["world", "hello"]
// 数组数字类型
const numbers = [1, 2, 3];
const reversedNumbers = reverse(numbers); // [3, 2, 1]---------------------------------------------------------------------------
function identity<T>(arg: T): T {return arg;
}let value = identity<number>(10); // 在使用时指定具体类型为 number
console.log(value); // 输出:10
函数重载的作用:
-
函数重载允许 同一个函数名字有多个函数类型定义,根据传入参数的不同,编译器会自动选择正确的函数类型调用。
function double(x: number): number; function double(x: string): string; function double(x: any): any { if (typeof x === "number") {return x * 2; } else if (typeof x === "string") {return x + x; } else {return x; } }const num = double(10); // 20 const str = double("hello"); // "hellohello"
3- 枚举和继承和class
枚举(Enums)
允许我们定义一组有名字的常量,可以更加清晰地表示一组相关的值。定义一个枚举的格式为
enum Name { member1, member2, ... }
,其中 Name 是枚举的名称,member1, member2, … 是枚举的成员。例如:
enum Color {Red,Green,Blue
}let myColor = Color.Green;
console.log(myColor); // 输出:1---------------------------------------------------enum Color {Red = 'red',Blue = 'blue',Green = 'green'
}console.log(Color.Red); // 输出:red
console.log(Color.Blue); // 输出:blue
console.log(Color.Green); // 输出:green//通过枚举,我们可以直观地获取和使用颜色常量,避免了硬编码,提高了代码的可读性。
类继承(Inheritance)
1.允许我们 创建一个类并基于另一个类派生出新的类。在派生类中,可以继承父类的属性和方法,并且可以重写父类的方法。使用 extends
关键字进行继承。
- 在子类构造函数中调用super()方法可以访问到父类的属性和方法,并且需要在使用this之前调用super()。这是因为在ES6的类继承中,子类在构造函数中必须先初始化父类的属性和方法,然后才能使用this关键字引用它自己的属性和方法。
- constructor方法用于初始化类的实例属性,在子类中也可以定义constructor方法来覆盖父类的constructor方法
示例:
本次演示了类继承的用法。它定义了一个名为 “Person” 的父类,其中包含一个构造函数和一个方法 “say”,以及两个子类 “Person1” 和 “Person2”,它们都继承自 “Person”。“Person1” 类添加了一个新属性和一个新方法,重写了从 “Person” 继承的 “say” 方法,并修改了继承的属性。“Person2” 类添加了不同的新属性和新方法,但没有重写 “say” 方法。最后,创建了所有三个类的实例,并调用了它们的方法。
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>教会你es6 class用法</title></head><body><script>// 定义父类 class Person {constructor(name, age) {// 构造函数,作用是初始化对象this.name = name;this.age = age;}say() {console.log(`我是${this.name},我今年${this.age}岁了!`);}}const per = new Person("小帅", 26);per.say(); //输出:我是小帅,我今年26岁了!// 定义 Person1 子类 继承 Person 父类 并添加自己的属性与方法,同时修改父类属性 class Person1 extends Person {constructor(name, age, sex) {// 在子类中也可以定义constructor方法来覆盖父类的constructor方法super(name, age); // 调用父类的 constructor 方法this.sex = sex; // 添加新的属性}say() {// 重写从父类继承而来的方法console.log(`我是重写从父类继承而来的方法,我叫${this.name},今年${this.age}岁了!`);}getSex() {// 添加新的方法console.log(`我是${this.sex}生!`);}}// 创建 Person1 实例对象const per1 = new Person1("小美", 20, "女");// 修改继承的属性per1.name = "小黑子";per1.age = 18;// 调用继承自父类的方法per1.say(); // 输出:我是重写从父类继承而来的方法,我叫小黑子,今年18岁了!// 调用子类新增的方法per1.getSex(); // 输出:我是女生!// 多个子类需要继承同一个父类 class Person2 extends Person {constructor(name, age, game) {super(name, age); // 调用父类的 constructor 方法this.game = game; // 添加新的属性}// 继承父类的方法say() {console.log(`我是${this.name},我今年${this.age}岁了!`);}// 添加新的方法getGame() {console.log(`我喜欢玩${this.game}游戏!`);}}// 创建 Person2 实例对象const per2 = new Person2("小明", 21, "王者荣耀");per2.say(); // 输出:我是小明,我今年21岁了!per2.getGame(); // 输出:我喜欢玩王者荣耀游戏!</script></body>
</html>
场景: 假设我们前端需要定义一个类来表示动物,然后有具体的子类来表示不同种类的动物。我们可以使用继承来实现代码的复用和扩展。
class Animal {name: string;constructor(name: string) {this.name = name;}speak() {console.log(`${this.name} makes a sound`);}
}class Dog extends Animal {constructor(name: string) {super(name);}speak() {console.log(`${this.name} barks`);}
}let dog = new Dog('Bobby');
dog.speak(); // 输出:Bobby barks
场景:假设我们前端需要创建一个用户类,用来表示网站上的用户信息。我们可以使用class来结构化地组织代码。示例代码如下:
class User {name: string;age: number;constructor(name: string, age: number) {this.name = name;this.age = age;}displayInfo() {console.log(`Name: ${this.name}, Age: ${this.age}`);}
}let user = new User('John', 25);
user.displayInfo(); // 输出:Name: John, Age: 25
----------------- css相关 ------------------
1- sass继承、minni
- 何时使用mixin:当你有一段CSS代码需要在多个地方重复使用,但可能需要传入不同的参数或者有轻微的变化时,你可以使用mixin。例如,你可以创建一个用于生成按钮的mixin,然后在需要的地方调用它并传入不同的颜色或大小。
@mixin button($bg-color, $font-size) {background-color: $bg-color;font-size: $font-size;// 其他样式...
}.btn-primary {@include button(blue, 16px);
}.btn-secondary {@include button(green, 14px);
}
- 何时使用继承:当你有一段CSS代码需要在多个地方完全重复使用时,你可以使用继承。例如,你的.wh100类就是一个很好的例子,它被.container-wrapper和.main类继承。
在SCSS中,你可以使用%符号来创建一个占位符选择器。占位符选择器在编译后的CSS中不会出现,除非它们被@extend指令使用。这样可以避免生成未使用的CSS规则。
%wh100 {width: 100%;height: 100%;
}
.container-wrapper {@extend %wh100;background-color: #f8f8f8;padding: 15px;.main {@extend %wh100;background-color: #fff;}
}
编译后的CSS将不再包含%wh100占位符:
.container-wrapper, .container-wrapper .main {width: 100%;height: 100%;
}
.container-wrapper {background-color: #f8f8f8;padding: 15px;
}
.container-wrapper .main {background-color: #fff;
}
- 何时使用循环:当你需要生成一系列有规律的CSS代码时,你可以使用循环。例如,你可以创建一个循环来生成一系列的边距类。
@for $i from 1 through 5 {.m#{$i} {margin: #{$i * 10}px;}
}
这将生成.m1到.m5的类,每个类的边距从10px到50px。
4.sass遍历数组 ,使用**@each**指令来遍历数组
$colors: (red, blue, green);@each $color in $colors {.text-#{$color} {color: $color;}
}
这段代码会生成以下的CSS:
.text-red {color: red;
}.text-blue {color: blue;
}.text-green {color: green;
}
在这个例子中,KaTeX parse error: Expected 'EOF', got '#' at position 51: …个元素生成一个对应的CSS类。#̲{color}语法用于插入变量的值。