一、vue的基本原理
- 当一个Vue实例创建时,vue会遍历data中的属性,用Object.defineProperty(vue 3.0 使用proxy)将它们转化为getter/setter,并在内部追踪其相关依赖,在属性被访问和修改时通知变化
-
-
二、双向数据绑定的原理
vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProerty来劫持各个属性的setter,getter,在数据变动的时候,发布消息給订阅者,触发相应的监听回调
主要分为三个模块
1、 对需要observe的对象,递归遍历,给每个属性添加getter,setter,当数据发生变化的时候,就会调用setter,这样就能监听到数据的变化
2、 compile对每个指令进行编译,将其中的变量替换成数据,然后每个指令的节点绑定更新函数,添加监听数据的订阅者,一旦数据发生变化就能收到通知
3、watcher订阅者是observe和compile的通信桥梁,主要做的事情
- 在自身实例化的的时候,在属性订阅器中添加自己
- 自身必须有一个update方法
- 当属性发生变化的时候,就会发出dep.notice通知,自己再调用update方法,并触发compile的回调函数
4、MVVM作为数据绑定的入口,整个observe、compile、watcher,objerve监听数据的变化,complie编译模版指令,达到数据发生变化视图更新,页面交互变化达到数据发生变化的双向绑定效果
三、Object.definePerporty与Proxy的区别
Object.definePerporty
Object.definePerporty共有三个参数,第一个参数是目标对象,第二个参数是目标对象的key,第三个参数是一个对象,其内部包含对对象属性key的增删改查配置
Object.defineProperty(obj,key,{
value:xxx,
writable:false,//是否可修改,默认是false
enumerable:false,//是否可枚举,默认是false
configurable:false//是否可删除,默认是false
})
通过Object.defineProperty操作对象的属性可以配置其值,是否可以枚举,是否可以修改,是否可删除
Object.defineProperty缺点
1、对对象直接增加的属性,无法监听
2、对数组未监听的索引以及部分数据的方法,无法监听,比如push
3、对数据的深度监听,需要深度递归遍历整个对象,耗费性能
vue2的Object.defineProperty
vue2使用Object.defineProperty完成数据响应式时,有几个特殊点
- 对操作数据的方法重新并监听数据变化,指听过数据的方法push,pop,shift等操作响应式依然存在,通过索引下标直接修改书的响应式无是无法生效的
- 对象属性时是深度监听,但是对对象新增加的属性,不具备响应式
- 在对象和数据的响应式丢失的时候,可以通过this.$set(data,key,newValue)重新重新具备响应式
proxy优势
1、完美的支持对数据各种方法和索引的拦截监听
2、对对象的新增属性支持拦截监听
3、vue3基于proxy也做了深度监听,但是一开始不会全部拦截对象的每一个属性,当我们访问或者操作属性时,才会动态的拦截具体属性
总体对比而言
- proxy直接对对象新增属性监听
- proxy直接对数组任何操作的监听
- proxy重新来多达13种数组操作方法
- proxy返回的是一个新的对象,我们可以直接操作新对象达到目的,而Object.defineProxy 是在原对象中直接修改
- 因为proxy是Es6新语法,所以兼容性没Object.defineProperty好
四、插槽
匿名插槽
作用: 对父组件的子元素,通过slot放在合适的位置使用
使用:
- 直接在组件的标签中加入要传入组件内部中要使用的内容
- 组件内容通过slot标签进行接收
具名插槽
作用: 对父组件的子元素进行分类,然后通过slot分发作用,将其放在合适的位置上
使用:
- 父组件使用template标签 使用v-slot:或者使用#进行命名
- 子组件 slot标签 使用name属性进行接收
作用域插槽
作用: 当数据在组件自身,但是数据生成的结果是父组件定的
使用:
- 在slot标签中自定义属性名,值为要传递的内容
- 在template标签中 通过scope属性进行值的接收
组件的封装
#### 全局组件
在vue创建createApp实例后,通过.component属性创建,第一个值是属性名,第二参数是组件内容
直接使用,不需要导入
#### 局部组件
创建一个组件后,使用的时候需要导入,并且在父组件中components 进行注册
vue组件传值
父传子组件通信: props
子传父: ref
兄弟组件通信: eventBus;veux
import { bus } from '@/bus.js';
在A组件中使用 bus.$emit('addition',{
num: this.num ++
})
在B组件使用 bus.$on('addition',arg => {
this.count = this.count + arg.num;
})
如果想要移除事件的监听,可以 bus.$off('addition',{})
vue的once,off
- $on(eventName:string|Array,callbacl) 监听事件
监听当前实例上的自定义事件,事件由vm.$emit触发,回调函数会接受所有传入的参数
- $once(eventName,callback)
- 监听一个自定义事件,但是只触发一次,一旦触发后,监听器自动就会被移除
- 同一个once事件,可以绑定多个回调函数,触发后,顺序执行
- $off(eventName,callback) 移除自定义事件监听器
- 如果没有提供参数,则移除所有的事件监听器
- 如果只提供了事件,则移动该事件所有的监听器;
- 如果同时提供了事件与回调,则只移除这个回调的监听器
- $emit(eventName,[...args]) 触发事件
触发当前实例上的事件,附加参数都会传给监听器的回调
Vue 常用的修饰符都有哪些
表单修饰符 v-model
- .lazy 在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同步
- .trim 自动过滤用户输入的首空格字符,而中间的空格不会过滤
- .number 动将用户的输入值转为数值类型,但如果这个值无法被`parseFloat`解析,则会返回原来的值
事件修饰符
- .stop 阻止了事件冒泡,相当于调用了event.stopPropagation方法
- .prevent 阻止了事件的默认行为,相当于调用了event.preventDefault方法
- .self 只当在 event.target 是当前元素自身时触发处理函数
- .once 绑定了事件以后只能触发一次,第二次就不会触发
- .capture 使事件触发从包含这个元素的顶层开始往下触发
- .passive 在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符
- .native 让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事件
v-bind 修饰符
.sync: 能对props进行双向绑定
- 使用sync需要注意:使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一致.
- 注意带有 .sync 修饰符的 v-bind 不能和表达式一起使用,将 v-bind.sync 用在一个字面量的对象上,例如 v-bind.sync=”{ title: doc.title }”,是无法正常工作的
父组件
<comp :myMessage.sync= 'bar'></Comp>
子组件
this.$emit('update:myMessage',params)
Vue-router 有哪几种路由守卫?
全局守卫
beforeEach
- 路由进入前全局调用。可以用来做权限验证,如果验证失败可以通过返回 `false` 或调用 `next(false)` 来取消导航。
- 示例
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth) {
// 验证用户是否登录
if (isAuthenticated()) {
next();
} else {
next({
name: 'Login' // 跳转到登录页面
});
}
} else {
next(); // 无需验证,直接放行
}
});
beforeResolve:
- 路由解析前全局调用,它在 `beforeEach` 之后和 `afterEach` 之前调用。由于它在路由解析之后被调用,因此你可以访问到 `to`、`from` 和即将要渲染的组件实例 `to.matched.components`。
afterEach
- 路由确认后全局调用。不会接收 `next` 函数也不会改变导航本身。
路由独享守卫
#### beforeEach:
- 针对某个路由配置的守卫,只有在访问这个路由时才会被调用。可以在路由配置中使用 `beforeEnter` 函数。
- 示例
const router = new VueRouter({
routes: [
{
path: '/some-path',
component: SomeComponent,
beforeEnter: (to, from, next) => {
// 路由独享的守卫逻辑
next();
}
}
]
});
组件内守卫
beforeRouteEnter /beforeRouterUpdate/ beforeRouteLeave
- 这两个守卫是组件内的守卫,分别在路由进入和离开时调用。它们不能访问 `next` 函数,因为它们是组件内部的方法,而不是全局路由守卫。
- 示例:
export default {
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被确认前调用
// 不能访问 `this`,因为守卫执行前实例还没被创建
next();
},
beforeRouteLeave(to, from, next) {
// 在离开该组件的对应路由时调用
// 可以访问 `this`,因为守卫执行时组件已经被创建
next();
}
};
Vue 怎么实现跨域
1. 后端代理
在后端服务器上设置代理,将前端发出的跨域请求先发送到后端服务器,然后由后端服务器转发到目标服务器。这样,前端和后端服务器之间是同源的,从而避免了浏览器的跨域限制。
2. CORS(跨源资源共享)
CORS 是一种浏览器安全特性,它允许服务器在响应头中添加特定的头信息,从而告诉浏览器允许跨域请求。后端服务器需要设置如下响应头
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Requested-With
3. JSONP
JSONP是一种利用`<script>`标签不受同源策略限制的特性来实现跨域请求的技术。虽然JSONP只支持GET请求,但它可以作为一种简单的跨域通信手段。需要注意的是,JSONP存在安全风险,因为它容易受到XSS攻击
4. 在Vue中实现跨域请求
在开发环境中,如果你的Vue项目运行在本地服务器上,你可能还需要配置本地服务器以允许跨域请求。例如,如果你使用的是`vue-cli`创建的项目,可以在`vue.config.js`中配置代理:
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://example.com',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
};
说说你对 SPA 单页面的理解,它的优缺点分别是什么?(必会)
优点:
- 实现局部刷新,而不是刷新整个页面
- 前后端分离:开发和维护更加灵活
- 前端路由: spa使用前端路由来管理应用的导航,这使得url更加友好和标签化,便于seo优化
- 状态管理: spa框架状态管理的解决方案,如vuex,redux
- 性能优化: spa可以通过缓存、懒加载等技术来优化性能,减少不必要的网络请求和加载时间
缺点
- 初始化加载时间: SPA在首次加载时可能需要加载较大的JavaScript和CSS文件,这可能导致初始加载时间较长。
- seo: 虽然现代SPA框架提供了SEO解决方案,如服务器端渲染(SSR)和预渲染(Prerendering),但SPA仍然可能面临搜索引擎优化的挑战。
如何对 Vue 首屏加载实现优化?
一、依赖模块采用第三方cdn资源(对于第三方js库的优化,分离打包)
- 目前采用引入依赖包生产环境的js文件方式加载,直接通过window可以访问暴露出的全局变量,不必通过import引入,Vue.use去注册
- 使用 CDN 的好处有以下几个方面:
- 加快打包速度。分离公共库以后,每次重新打包就不会再把这些打包进 vendors 文件中。
- CDN减轻自己服务器的访问压力,并且能实现资源的并行下载。浏览器对 src 资源的加载是并行的(执行是按照顺序的)。
- 使用:修改vue.config.js
module.exports = {
...
externals: {
'vue': 'Vue',
'vuex': 'Vuex',
'vue-router': 'VueRouter',
'axios': 'axios',
'element-ui': 'ELEMENT',
'underscore' : {
commonjs: 'underscore',
amd: 'underscore',
root: '_'
},
'jquery': {
commonjs: 'jQuery',
amd: 'jQuery',
root: '$'
}
}
...
}
二、 异步组件和懒加载方式
- 组件在使用的时候,采用异步引入
- 在配置router的时候,异步引入,即路由懒加载
- 图片懒加载:使用vue-lazyload插件
//引入,配置vue懒加载
import VueLazyload from 'vue-lazyload'
//方法一: 没有页面加载中的图片和页面图片加载错误的图片显示
// Vue.use(VueLazyload)
//方法二: 显示页面图片加载中的图片和页面图片加载错误的图片
//引入图片
import loading from '@/assets/images/load.jpg'
//注册图片懒加载
Vue.use(VueLazyload, {
// preLoad: 1.3,
error: '@/assets/images/error.jpg',//图片错误的替换图片路径(可以使用变量存储)
loading: loading,//正在加载的图片路径(可以使用变量存储)
// attempt: 1
})
// 使用:
<div class="lazyLoad">
<ul>
<li v-for="img in arr">
<img v-lazy="img.thumbnail_pic_s">
</li>
</ul>
</div>
三、webpack开启gzip压缩文件传输模式
- gzip压缩是一种http请求优化方式,通过减少文件体积来提高加载速度,html,js,css,都可以用它压缩,可以减少60%以上的加载速度,通过compression-webpack-plugin
- 使用: 如果浏览器不支持这种方式压缩,也不用担心,自动会访问源代码
const CompressionPlugin = require('compression-webpack-plugin');//引入gzip压缩插件
module.exports = {
plugins:[
new CompressionPlugin({//gzip压缩配置
filename: '[path][base].gz',
algorithm: 'gzip', // 压缩算法,官方默认压缩算法是gzip
test:/\.js$|\.css$|\.html$|\.eot$|\.woff$/,// 使用gzip压缩的文件类型
threshold:10240,//对超过10kb的数据进行压缩,默认是10240
deleteOriginalAssets:false,//是否删除原文件
minRatio: 0.8, // 最小压缩比率,默认是0.8
})
]
}
四、webpack 代码分割与优化
模块拆分: 配置Webpack将代码拆分成多个小块,利用Tree Shaking、代码压缩等技术减少代码体积。这将减少初始加载所需的下载时间,提高页面加载速度。
// vue.config.js
module.exports = {
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
};
五、禁止生成map文件
- 在设置了productionSourceMap: false之后,就不会生成map文件,map文件的作用在于:项目打包后,代码都是经过压缩加密的,如果运行时报错,输出的错误信息无法准确得知是哪里的代码报错。也就是说map文件相当于是查看源码的一个东西。如果不需要定位问题,并且不想被看到源码,就把productionSourceMap 置为false,既可以减少包大小,也可以加密源码。
module.exports = {
productionSourceMap: false, // 生产环境是否生成 sourceMap 文件,一般情况不建议打开
}
六、 图片资源的压缩、icon资源使用、雪碧、代码压缩
七、代码层面
- 合理使用v-if和v-show
- 合理使用watch和computed
- 使用v-for必须添加key, 最好为唯一id, 避免使用index, 且在同一个标签上,v-for不要和v-if同时使用
- 定时器的销毁。可以在beforeDestroy()生命周期内执行销毁事件;也可以使用$once这个事件侦听器,在定义定时器事件的位置来清除定时器。详细见vue官网
- 长列表性能优化
- 图片资源懒加载
- 前端接口防止重复请求实现方案-CSDN博客
- 用innerHTML代替dom操作,减少dom操作的次数,优化js性能
- 合理使用requestAnimationFrame动画代替setTimeOut
- 通过创建文档碎片 document.createDocumentFragment()-创建虚拟dom来更新dom
vue.nextTick的原理
- Vue 的 nextTick 方法允许你在 Vue 实例的数据变化之后,等待 DOM 更新完成,然后再执行某些操作。
- nextTick 的原理基于 JavaScript 的异步和事件循环机制。当你调用 `nextTick` 方法时,你提供的回调函数会被放入另一个队列中。在 DOM 更新完成后,Vue 会检查这个队列,并执行所有等待的回调函数。这意味着 `nextTick` 的回调总是在 DOM 更新之后执行
JS 内存泄漏的解决方式
内存泄漏是JavaScript开发中常见的问题,它可能导致应用程序性能下降或崩溃。为了解决内存泄漏问题,开发者可以采取以下措施:
1. 避免意外创建全局变量。
- 在非严格模式下,未声明的变量会默认成为全局对象的属性,这可能导致内存泄漏。
- 解决方法是使用var、let或const声明变量,或者在JS文件开头添加'use strict'来开启严格模式2。
3. 正确处理闭包
- 闭包可以导致内存泄漏,因为它们会保持对外部函数变量的引用。
- 解决方法是在不再需要闭包时,手动解除对其引用,例如closureFunction = null,并确保使用let关键字声明变量,避免在全局范围内创建变量
3. 移除不再需要的事件监听器
- 未移除的事件监听器会导致DOM元素无法被垃圾回收。
- 解决方法是使用removeEventListener来移除不再需要的事件监听器,并确保在元素被销毁时解除引用。
4. 解除DOM引用
- 保留对已删除DOM元素的引用会阻止垃圾回收。
- 解决方法是将引用设置为null,例如a = null,以确保垃圾回收器可以回收对应的DOM元素7。
5.清理定时器
- 未清理的setInterval或setTimeout定时器会导致回调函数及其内部依赖的变量不能被回收。
- 解决方法是在不再需要定时器时,使用clearInterval或clearTimeout来清除它们7。
6. 处理循环引用
- 在JavaScript中,循环引用可能导致内存泄漏,特别是在使用对象和数组时。
- 解决方法是确保在不再需要对象时手动断开循环引用,或者使用`WeakMap`或`WeakSet`来存储对象的弱引用,使其更容易被垃圾回收、
new 操作符具体做了什么
- 创建一个新的空对象,作为要返回的对象实例
- 将构造函数的原型赋值给新对象的原型:新创建的对象会继承构造函数的原型对象的属性和方法,这样新对象就可以访问构造函数原型上定义的属性和方法
- 将构造函数的作用域赋值给新对象: 通过call,或者apply方法,将构造函数的作用域this绑定到新对象上,这样就可以执行构造函数的代码
- 返回新对象: 如果构造函数没有显示的返回其他对象,那么new操作符会隐式返回新创建的对象实例。如果构造函数显示地返回一个非对象值,则忽略该返回值,仍返回新创建的实例
-
function mynew(fun,...args){
```
// 1.创建一个新对象
const obj = {}
// 2.新对象原型指向构造函数原型对象
obj.__proto__ = fun.prototype
// 3.将构建函数的this指向新对象
const result = Fun.apply.call(obj,args)
// 4.根据返回值判断
return result instanceOf Object ? result:obj
}
- 测试
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function () {
console.log(this.name)
}
let p = mynew(Person, "huihui", 123)
console.log(p) // Person {name: "huihui", age: 123}
p.say() // huihui
JavaScript 中如何对一个对象进行深度 clone?
方法一、使用 JSON.parse和 JSON.stringify
- 但它有一些限制,例如无法复制函数、undefined、循环引用等。
方法二、使用递归函数
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return null; // 如果是null或者undefined,直接返回
if (typeof obj !== 'object') return obj; // 非对象直接返回
if(typeof obj=='object'){
if(obj instanceof Array){
var result = [];
for(var i=0;i<obj.length;i++){
result[i] = cloneObj(obj[i]);
}
return result;
}else{
var result = {};
for(var i in obj){
result[i] = cloneObj(obj[i]);
}
return result;
}
}else{
return obj;
}
}
let original = { a: 1, b: { c: 2 }, d: [3, 4] };
let clone = deepClone(original);
console.log(clone); // 输出: { a: 1, b: { c: 2 }, d: [3, 4] }
使用lodash.cloneDeep
数组去重
1. 使用Set对象
Set是一个集合数据结构,它只允许存储唯一的值。你可以利用这个特性来去重数组。
let array = [1, 2, 2, 3, 4, 4, 5];
let uniqueArray = [...new Set(array)];
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]
2. 使用filter 搭配 indexOf方法
你可以使用filter方法结合一个对象来检查数组中的元素是否已经存在。
let array = [1, 2, 2, 3, 4, 4, 5];
let uniqueArray = array.filter((item, index) => array.indexOf(item) === index);
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]
3. 使用reduce方法
reduce方法可以遍历数组并累积结果,你可以用它来创建一个新数组,其中只包含唯一的值。
let array = [1, 2, 2, 3, 4, 4, 5];
let uniqueArray = array.reduce((accumulator, currentValue) => {
if (!accumulator.includes(currentValue)) {
accumulator.push(currentValue);
}
return accumulator;
}, []);
console.log(uniqueArray); // 输出: [1, 2, 3, 4, 5]
4. 使用for循环和splice方法
通过for循环遍历数组,并使用splice方法删除重复的元素。
复制
let array = [1, 2, 2, 3, 4, 4, 5];
for (let i = 0; i < array.length; i++) {
for (let j = i + 1; j < array.length; j++) {
if (array[i] === array[j]) {
array.splice(j, 1);
j--; // 调整索引,因为数组长度已经改变
}
}
}
console.log(array); // 输出: [1, 2, 3, 4, 5]
5. 一层for循环,创建一个数组 配个使用
谈谈你对 Javascript 垃圾回收机制的理解?
JavaScript的垃圾回收机制是一种自动内存管理机制,它负责释放不再使用的内存空间,以便这些空间可以被重新分配给新的变量或对象。这种机制对于防止内存泄漏和提高程序性能至关重要。
垃圾回收的基本原理
JavaScript主要依赖于以下几种垃圾回收算法:
- 标记-清除(Mark and Sweep) :
- 标记阶段:垃圾回收器遍历所有根对象(如全局对象、活动函数的调用栈等),并标记所有从根对象开始可达的对象。
- 清除阶段:垃圾回收器遍历整个堆内存,清除所有未标记的对象,释放它们占用的内存。
- 标记-整理(Mark and Compact) :
- 这个算法在标记-清除的基础上增加了一个整理阶段,它会将所有存活的对象移动到内存的一端,以消除内存碎片并创建连续的内存空间。
- 复制(Copying) :
- 这种算法将内存分为两个相等的区域,每次只使用一个区域。当需要分配新对象时,它会将所有存活的对象复制到另一个区域,并释放当前区域的内存。这种方法可以避免内存碎片,但效率较低,因为它需要复制所有存活的对象。
- 分代回收(Generational Garbage Collection) :
- 这种算法将对象分为几个代(Generation),通常分为新生代(Young Generation)和老生代(Old Generation)。新创建的对象在新生代中,如果它们经过几次垃圾回收仍然存活,就会被移动到老生代。这种算法基于这样一个观察:大多数对象的生命周期都很短,因此频繁地回收新生代可以提高垃圾回收的效率。
垃圾回收的触发
垃圾回收不是连续进行的,而是周期性地触发。在JavaScript中,垃圾回收通常是自动的,但某些操作(如执行大量分配操作的循环)可能会触发垃圾回收。此外,一些JavaScript引擎提供了手动触发垃圾回收的API,但现代引擎已经足够智能,能够根据内存使用情况自动优化垃圾回收的时机。
内存泄漏的预防
尽管垃圾回收机制可以自动管理内存,但开发者仍然需要注意避免内存泄漏。内存泄漏通常发生在以下情况:
- 全局变量被无意中创建或未被正确清理。
- 未移除的事件监听器或定时器。
- 闭包错误使用,导致变量无法被回收。
- 循环引用,特别是当对象和DOM元素相互引用时。
为了避免这些问题,开发者应该:
- 使用严格模式(
'use strict')来避免全局变量的意外创建。 - 移除不再需要的事件监听器和定时器。
- 正确使用闭包,确保不再需要的闭包可以被回收。
- 避免不必要的循环引用,特别是在处理DOM元素时。
结论
JavaScript的垃圾回收机制是确保程序性能和稳定性的关键。虽然它可以帮助开发者管理内存,但开发者仍然需要了解其工作原理,并采取适当的措施来避免内存泄漏。随着JavaScript引擎的不断优化,垃圾回收的效率和智能性也在不断提高,使得开发者可以更专注于业务逻辑的实现。
Class 和普通构造函数有何区别?
Class 在语法上更加贴合面向对象的写法
Class 实现继承更加易读、易理解
本质还是语法糖,使用 prototype
class继承比构造函数继承方便
防抖 节流 代码演示
以下是防抖(debounce)和节流(throttle)的简单代码示例,展示了它们的基本用法和实现方式。
防抖(Debounce)示例
防抖函数通过延迟执行来限制事件处理函数的触发频率。只有当一定时间内没有新的事件触发时,事件处理函数才会执行。
function debounce(func, wait) {
let timeout;
return function() {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(function() {
func.apply(context, args);
}, wait);
};
}
// 示例:连续输入时触发的搜索功能
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(function() {
console.log('搜索: ' + this.value);
}, 300)); // 每300毫秒触发一次搜索
节流(Throttle)示例
节流函数确保事件处理函数在特定的时间间隔内只被执行一次,无论事件触发了多少次。
function throttle(fn, interval) {
// 1.记录上一次的开始时间
let lastTime = 0
// 2.事件触发时, 真正执行的函数
const _throttle = function () {
// 2.1.获取当前事件触发时的时间
const nowTime = new Date().getTime()
// 2.2.使用当前触发的时间和之前的时间间隔以及上一次开始的时间, 计算出还剩余多长时间需要去触发函数
const remainTime = interval - (nowTime - lastTime)
//第一次会执行,原因是nowTime刚开始是一个很大的数字,结果为负数
//若最后一次没能满足条件,不会执行
if (remainTime <= 0) {
// 2.3.真正触发函数
fn()
// 2.4.保留上次触发的时间
lastTime = nowTime
}
}
return _throttle
}
// 示例:滚动时触发的事件处理
window.addEventListener('scroll', throttle(function() {
console.log('滚动事件触发');
}, 200)); // 每200毫秒触发一次滚动处理
在这两个示例中,我们创建了自定义的防抖和节流函数,并将其应用于实际的事件处理中。对于防抖,我们在输入框的input事件上应用了防抖函数,以避免用户每次按键时都触发搜索。对于节流,我们在窗口的scroll事件上应用了节流函数,以限制滚动事件的处理频率。
这些简单的实现可以帮助你理解防抖和节流的基本原理,并根据需要将它们应用到你的项目中。在生产环境中,你可能会使用更复杂的库(如Lodash)提供的防抖和节流函数,因为它们提供了更多的功能和更好的性能。
栈和堆的区别?
栈(Stack)和堆(Heap)是计算机内存中用于存储数据的两个重要区域,它们在内存管理、数据结构和算法实现中扮演着关键角色。尽管它们都用于存储数据,但它们在结构、用途和管理方式上有着本质的区别。
栈(Stack)
- 用途:栈主要用于存储局部变量和函数调用的上下文。当一个函数被调用时,一个新的栈帧(stack frame)被推入(push)到栈中,包含函数的局部变量、参数和返回地址。函数执行完毕后,这个栈帧被弹出(pop)栈。
- 结构:栈是后进先出(LIFO)的数据结构,即最后推入栈的元素会最先被弹出。
- 管理:栈的内存管理是自动的,由操作系统或运行时环境负责。当函数调用结束时,栈帧会自动释放。
- 大小:栈通常有大小限制,因此不能用于存储大量的数据。
- 访问速度:栈的访问速度较快,因为它使用连续的内存空间,且由CPU直接管理。
堆(Heap)
- 用途:堆用于存储动态分配的内存,如对象、数组和其他复杂的数据结构。在JavaScript中,几乎所有的对象和数组都是通过堆来分配的。
- 结构:堆不遵循特定的数据结构,它是一个自由的内存空间,可以动态地分配和释放内存。
- 管理:堆的内存管理是手动的,需要程序员通过
new关键字分配内存,并使用delete操作符或垃圾回收机制来释放内存。 - 大小:堆的大小通常远大于栈,但受到可用系统内存的限制。
- 访问速度:堆的访问速度较慢,因为它可能包含内存碎片,且分配和释放操作相对复杂。
总结
栈和堆是两种不同的内存分配策略,它们各自适用于不同的场景。栈适用于存储生命周期确定的局部变量和函数调用上下文,而堆适用于存储生命周期不确定的动态数据。在编程时,了解栈和堆的区别对于优化内存使用、避免内存泄漏和提高程序性能至关重要。在JavaScript中,开发者通常不需要直接管理内存,因为垃圾回收机制会自动处理堆内存的分配和释放。然而,理解栈和堆的工作原理仍然有助于编写更高效和更可靠的代码。
var、let、 const
var 声明的变量具有函数作用域或全局作用域,有变量提升
let const 只有块级作用域,没有变量提升
let 和 const 都是 JavaScript 中的声明变量的关键字,它们都具有块级作用域的特性。这意味着它们声明的变量只在包含它们的代码块(通常是一个括号块或者函数体)内部可见和可用。
块级作用域(Block Scope)
块级作用域是指变量的作用域被限定在一个特定的代码块内,例如 if 语句、for 循环、while 循环、函数体等。这种作用域的概念与其他语言中的作用域规则相似,它有助于避免变量名冲突和意外的全局变量污染。
手写promise
new Promise((resolve,reject)=> {
resolve('petra')
}).then((value1)=> {},(value2)=>{})
function MyPromise(executor){
this.status = 'pending'
this.value = ''
this.reason=''
const resolve= (value) => {
if(this.status === 'pending') {
this.status = 'fulfilled'
this.value = value
}
}
const reject = (reason)=> {
if(this.status === 'pending') {
this.status = 'rejected'
this.reason = reason
}
}
try {
executor(resolve,reject)
} catch (error) {
reject(error)
}
}
MyPromise.prototype.then = (onFulFilled,onRejected)=> {
if(typeof onFulFilled !== 'function') {
onFulFilled = (value) => value
}
if(typeof onRejected !== 'function') {
onRejected = (value) => onRejected
}
if(this.value === 'fulfilled') {
onFulFilled(this.value)
}
if(this.value === 'rejected') {
onRejected(this.reason)
}
// 如果promise还在pending中
let result ;
if(this.status === 'pending') {
result = this
}
setTimeout(()=> {
if (this.status === 'fulfilled' && result === this) {
onFulfilled(this.value);
} else if (this.status === 'rejected' && result === this) {
onRejected(this.reason);
}
},0)
}
git 撤销 的几个命令
在Git中,撤销更改是一个常见的操作,可以通过几个不同的命令来实现。以下是一些常用的Git撤销命令及其用途:
-
git checkout: 这个命令可以用来撤销工作目录中的修改。当你想要撤销对某个文件的修改,可以使用:
git checkout -- <file>这将会用最后一次提交的版本覆盖工作目录中的文件,撤销所有未暂存的更改。
-
git reset: 这个命令用于撤销暂存区(索引区)中的更改。有两种模式:软回退(soft)和混合回退(mixed)。
-
软回退(
--soft): 将暂存区的更改回退到工作目录,但不会撤销工作目录中的修改。git reset --soft HEAD^ -
混合回退(
--mixed,这是默认模式): 将暂存区和工作目录的更改都回退到上一次提交。git reset --mixed HEAD^ -
硬回退(
--hard): 将暂存区、工作目录和本地仓库的更改都回退到指定的提交。这将会丢失所有后续更改,因此使用时需要谨慎。git reset --hard <commit-hash>
-
-
git revert: 这个命令用于创建一个新的提交,它撤销指定提交的更改。这是一个安全的操作,因为它不会重写历史记录。
git revert <commit-hash>如果需要撤销一系列的提交,可以使用:
git revert <commit-hash>^..<commit-hash> -
git clean: 这个命令用于删除未跟踪的文件和目录,即那些从未被Git添加过的文件。
git clean如果你想删除特定目录下的未跟踪文件,可以使用:
git clean -d <directory>加上
-f或--force参数可以强制删除未跟踪的文件,即使它们被.gitignore文件排除。git clean -f
使用这些命令时,需要根据你的具体需求和当前的Git状态来选择合适的命令。例如,如果你只是想要撤销工作目录中的修改,git checkout可能是一个好的选择。如果你想要撤销暂存区的更改,git reset可能更适合。而如果你需要撤销已经提交的更改,并且想要保持历史记录的完整性,git revert是最佳选择。最后,git clean用于清理工作目录中的未跟踪文件。
cookie、session、localStorage、sessionStorage区别
区分cookie与session
cookie的作用是在客户端保持状态,比如登录状态。它被存储在本地硬盘或内存里,并且在发送http请求的时候会被放进请求头中参与通信。每个cookie最大为4k,每个域名可以拥有的cookie数量在不同浏览器中是不同的,但都多于20个,早期的20个限制已经不存在了。
session的作用是在服务器端保持状态,它被存储在服务器上。session被创建的时候会生成一个sessionid,它被存储在cookie中用来访问session。由于关闭浏览器不会导致session被删,迫使服务器为session设置了失效时间。
2.共同点:
都是保存在浏览器端、且同源的
3.区别
1、cookie数据浏览器自动携带,而sessionStorage和localStorage不会自动把数据发送给服务器,仅在本地保存。
2、存储大小限制也不同,cookie数据不能超过4K,sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大 。
3、数据有效期不同,sessionStorage:仅在当前浏览器窗口关闭之前有效; localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie:只在设置的cookie过期时间之前有效,没有设置的话浏览器关闭就会失效。
4、作用域不同,sessionStorage不在不同的浏览器窗口中共享,即使是同一个页面;localstorage在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的 。
5、web Storage支持事件通知机制,可以将数据更新的通知发送给监听者 。
6、web Storage的api接口使用更方便。
热更新原理
热更新是指在开发过程中,当你修改了源代码后,Webpack会自动将修改后的代码注入到运行中的应用程序中,而无需刷新整个页面或重新加载整个应用程序。这使得开发者可以更快地看到修改的结果,提高开发效率。
工作原理
-
在Webpack配置中,你需要设置
devServer.hot为true,以启用热模块替换功能。 -
当你启动Webpack Dev Server时,它会创建一个Socket服务器,用于与浏览器建立WebSocket连接。
-
在浏览器中访问应用程序时,Webpack Dev Server会将一个运行时脚本(runtime script)注入到页面中。这个脚本会建立与Webpack Dev Server的WebSocket连接,以便实时接收来自服务器的更新通知。
-
当你修改了源代码并保存时,Webpack会监听文件系统的变化,并编译修改后的模块。
-
当编译完成后,Webpack会将更新的模块信息发送给Webpack Dev Server。
-
Webpack Dev Server会通过WebSocket连接将更新的模块信息推送给浏览器。
-
浏览器接收到更新的模块信息后,会使用Webpack的HMR Runtime(热模块替换运行时)来处理这些更新。HMR Runtime会根据模块的更新信息,将新的模块代码插入到应用程序中,而无需重新加载整个页面。
webpack 来优化前端性能
1. 压缩和混淆代码
使用TerserPlugin可以压缩JavaScript代码,减少文件大小。
复制
const TerserPlugin = require('terser-webpack-plugin');
module.exports = {
optimization: {
minimize: true,
minimizer: [new TerserPlugin()],
},
};
2. 代码分割
使用Webpack的splitChunks功能,将第三方库和应用程序代码分离,实现按需加载。
复制
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
},
},
};
3. Tree Shaking:启用Tree Shaking移除未使用的代码。
复制
module.exports = {
optimization: {
usedExports: true,
},
};
4. 使用CDN: 将常用的库和框架通过CDN引入,减少从服务器加载的时间。
// webpack.config.js
module.exports = {
// ...
output: {
// ...
publicPath: 'https://cdn.example.com/assets/',
},
};
5. 图片优化:使用url-loader和image-webpack-loader来压缩图片。
module.exports = {
module: {
rules: [
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: [
{
loader: 'url-loader',
options: {
limit: 8192, // 小于8KB的图片转Base64
},
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65,
},
// 其他图片处理选项...
},
},
],
},
],
},
};
6. 缓存优化使用cache-loader来缓存loader的结果,加快重新构建的速度。
{
module:
{
rules: [
{
test: /\.js$/,
use: ['cache-loader', 'babel-loader'],
},
],
},
};
7. 避免不必要的打包
使用externals配置排除不需要打包的依赖。
复制
module.exports = {
externals: {
react: 'React',
'react-dom': 'ReactDOM',
},
};
请详细说明一下 Babel 编译的原理是什么?
- babel 首先将js 转换成语法树,
- 然后遍历将语法树,并根据预设的规则对语法树进行变换,它以嵌套的节点形式展示优化的代码的结果。
- 在将转换后的语法树进行生成阶段之前,在这个阶段,babel会遍历转换后的语法树,然后将其生成新的源代码
怎么实现 webpack 的按需加载?
Webpack 的按需加载(也称为懒加载或代码分割)允许你将应用程序分割成多个独立的代码块,并在运行时按需加载它们。这样可以提高应用程序的初始加载速度,因为它只加载初始渲染所需的最小代码集。以下是实现按需加载的几种方法:
1. 使用 import() 语法
在 JavaScript 中使用 import() 函数来动态地导入模块。Webpack 会将这些动态导入转换为单独的代码块,并在运行时异步加载它们。
// 假设我们有一个 `loadableComponent.js` 文件
// 使用动态导入来按需加载组件
import('./loadableComponent.js').then(LoadableComponent => {
// 当模块加载完成后,你可以使用 LoadableComponent
console.log(LoadableComponent);
});
2. 配置 splitChunks 插件
在 Webpack 配置中使用 optimization.splitChunks 选项来分割代码。这可以帮助你将第三方库和公共模块提取到单独的文件中,从而实现更好的按需加载。
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
};
3. 使用 React.lazy 和 React.Suspense
对于 React 应用程序,可以使用 React.lazy 函数来按需加载组件,并结合 React.Suspense 来处理加载状态。
import React, { lazy, Suspense } from 'react';
// 动态导入组件
const LazyComponent = lazy(() => import('./MyComponent'));
function App() {
return (
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
);
}
export default App;
4. 预加载和预获取
你可以使用 React.lazy 的预加载和预获取方法来提前加载或获取模块。
import { lazy, preload, prefetch } from 'react';
// 预加载组件
preload(() => import('./MyComponent'));
// 预获取组件
prefetch('./MyComponent');
5. 路由懒加载
在 Webpack 4 中,你可以结合 HtmlWebpackPlugin 和 react-router 来实现路由级别的懒加载。
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
function App() {
return (
<Router>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/about" component={lazy(() => import('./About'))} />
</Switch>
</Router>
);
}
注意事项
- 确保服务器配置了正确的 MIME 类型,以便能够正确地提供 JavaScript 文件。
- 考虑使用
publicPath选项来指定资源的加载路径。 - 如果使用 CDN,确保 CDN 能够处理按需加载的文件。
- 对于服务端渲染(SSR),需要确保服务器能够处理按需加载的模块。
通过以上方法,你可以有效地实现 Webpack 的按需加载,从而优化应用程序的性能和用户体验。
vue生命周期
vue生命周期及执行顺序
1、系统自带生命周期一共有8个
beforeCreate(创建前)
created(创建后)
beforeMount(载入前)
mounted(载入后)
beforeUpdate(更新前)
updated(更新后)
beforeDestroy(销毁前)
destroyed(销毁后)
2、页面组件一旦加载执行生命周期
beforeCreate(创建前)
created(创建后)
beforeMount(载入前)
mounted(载入后)
3、页面组件一旦加载执行生命周期的不同点
beforeCreate ===》没有data没有el
created ===》有data没有el
beforeMount ===》有data没有el
mounted ===》有data有el
4、主要生命周期应用场景
created ===》一般发送请求
mounted ===》操作获取dom的插件
5、如果用到了vue内置的组件keep-alive,会多两个生命周期.
activated
deactivated
keep-alive这个组件的作用就是能够缓存不活动的组件。
组件进行切换的时候,默认会进行销毁,如果有需求,某个组件切换后不进行销毁,而是保存之前的状态,
那么就可以利用keep-alive来实现,或者使用路由中的meta属性控制,meta中的keepAlive为true
进行缓存,否侧不进行缓存。
keep-alive上有两个属性:
a:include 值为字符串或者正则表达式匹配的组件name会被缓存
b:exclude 值为字符串或正则表达式匹配的组件name不会被缓存。
例如:
<keep-alive include="home">
<router-view />
</keep-alive>
使用路由中的meta属性控制
<keep-alive>
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />
存在的问题,因为组件被缓存,并没有被销毁,所以组件在切换的时候也就不会被重新创建,也就不会
调用created等生命周期函数,所以此时要使用activated与deactivated来获取当前组件是否处于活动状态。
6、如果用到了keep-alive组件,生命周期的执行有以下变化
第一次进入组件会执行
beforeCreate(创建前)
created(创建后)
beforeMount(载入前)
mounted(载入后)
activated
第二次、第三次.....进入组件会执行
activated
父子组件生命周期及执行顺序
1、只有父组件时,页面组件一旦加载,生命周期及执行顺序
beforeCreate(创建前)
created(创建后)
beforeMount(载入前)
mounted(载入后)
2、存在父子组件时,页面组件一旦加载,生命周期及执行顺序
父组件:beforeCreate、created、beforeMount
子组件:beforeCreate、created、beforeMount、mounted
父组件:mounted
vue 如何做到样式只在本组件生效,而不影响到其他组件
在style标签中 加scoped属性
vue 如何影响组件中其他组件的样式
使用样式穿透 /deep/ 或者 >>> 这两种写法都行
vue props和data的优先级
props比data高,依据源码,看先处理的谁
vuex
- module:
- state
- getter: 类死computer 有缓存,只有依赖发生变化才会重新执行
- mutation:
- commit
- vuex 是单向数据流,不允许随意改动,只能通过mutation中的方法进行改动
vue proxy 代理
module.exports = {
derServer:{
proxy: 'http://localhost:3000'
}
}
axios({
url:'/home',
method: 'get'
})
虽然启动的项目地址是http://localhost:8000,但是经过proxy配置就会转成下面的地址
请求的地址会变成 http://localhost:3000/home
module.exports exports 和export的区别
- 总结: exports是module.exports的一个引用,指向同一个地址
- module.exports与export的区别
-
使用方法不一样,commonJs是通过require导入,module.exports导出,ES6模块是通过import export实现导入导出
-
commonJs是对值的拷贝,ES6是对值的引用,指向同一个地址
-
commonJs是运行时加载,ES6模块导入导出时编译时加载
-