Vue篇
data为什么是个函数?
在Vue中,data必须是一个函数,这是因为当data是函数时,每个组件实例化时都会调用该函数,返回一个新的数据对象,从而保证每个组件实例拥有独立的数据,避免数据冲突和不正确的状态更新。
具体来说,如果data是一个对象字面量,多个组件实例会共享同一个数据对象,导致数据冲突和不正确的状态更新。而当data是一个函数时,每次创建组件实例时都会调用该函数,返回一个新的数据对象,每个组件实例都有自己的数据对象,互不干扰。这样,改变其中一个组件的状态不会影响到其他组件。
优点:
- 避免数据冲突:通过将data定义为函数,每个组件实例都会获得一个独立的数据对象,避免了数据冲突和不正确的状态更新。
- 更好的封装和组件化:将data定义为函数可以更好地封装组件的状态,使得组件的逻辑更加清晰和模块化,也使得组件的测试更加容易。
- 提高性能:由于data是函数而不是对象字面量,Vue可以更好地跟踪依赖关系和变化,从而提高响应性和性能。
生命周期:
Vue2的生命周期:
-
beforeCreate
:实例初始化之后,数据观测(data observer)和事件/watcher 设置之前被调。 -
created
:实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算,watch/event
事件回调。然而,挂载阶段还没开始,$el
属性目前不可见。 -
beforeMount
:模板编译/挂载之前被调用。在这之前,$el
属性还不存在。 -
mounted
:el 被新创建的vm.$el
替换,并挂载到实例上去之后调用。一旦挂载完成,实例就处于可用状态。 -
beforeUpdate
:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。 -
updated
:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。 -
activated
:在使用vue-router
时,路由变化(切换到当前组件)时被调用。 -
deactivated
:在使用vue-router
时,路由变化(离开当前组件)时被调用。 -
beforeDestroy
:实例销毁之前调用。在这一步,实例仍然完全可用。 -
destroyed
:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑,所有的事件监听器会被移除,所有的子实例也会被销毁。
Vue3的生命周期:
- setup - Vue2中的beforecreate和created被setup方法本身所替代
-
onBeforeMount
- 组件挂载之前调用。 -
onMounted
- 组件挂载之后调用。 -
onBeforeUpdate
- 组件更新之前调用。 -
onUpdated
- 组件更新之后调用。 -
onBeforeUnmount
- 组件卸载之前调用。 -
onUnmounted
- 组件卸载之后调用。 -
onActivated
- 组件被keep-alive激活时调用。 -
onDeactivated
- 组件被keep-alive停用时调用。 -
onErrorCaptured
- 捕获子组件的错误时调用。
父子组件生命周期:
创建时:父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
销毁时:父beforeUpdated -> 子beforeUpdated -> 子updated -> 父updated
父子组件传参和事件:
子组件
<template>
<div class="hello">
<div>{{ msg }} 子组件</div>
<button @click="add">加一</button>
<button @click="add2">加二</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
props: {
msg: String
},
data() {
return {
child: '儿子'
}
},
methods: {
add() {
this.$emit('child')// 触发父组件的自定义事件
},
add2() {
this.$emit('child', 2)// 触发父组件的自定义事件
},
}
}
</script>
<style scoped></style>
父组件
<template>
<div id="app">
<!-- <img alt="Vue logo" src="./assets/logo.png"> -->
<h1>父组件</h1>
<HelloWorld :msg="AppMsg" @child="parentMethod" ref="childComponentRef" />
{{ count }}
<button @click="callChildMethod">调用子组件方法</button>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
components: {
HelloWorld
},
data() {
return {
AppMsg: "Welcome to Your Vue.js App",
count: 1
}
},
methods: {
parentMethod(e) {
if (e) {
this.count += e
} else {
this.count++
}
},
callChildMethod() {
this.$refs.childComponentRef.add(); // 调用子组件的方法
this.AppMsg = this.$refs.childComponentRef.child; // 访问子组件的属性
},
}
}
</script>
<style></style>
使用provide/inject组件通讯
可以方便地在祖先组件中提供数据给任意后代组件,无需逐层传递props。
提供的组件
export default {
provide() {
return {
provideData: this.oneData
};
},
data() {
return {
oneData: '来自提供的组件的数据'
};
}
};
子组件使用
<script>
export default {
inject: ['provideData'],
mounted() {
console.log('从提供的组件中获取的数据:', this.provideData);
}
};
</script>
组件的封装:
父组件(使用被封装的组件)
<template>
<div id="map-container">
<!-- 使用组件 -->
<mapItem :mapData="mapData" :isFullScreen="isFullScreen" ref="mapIndex" @pointermove="clickPoint">
<div
class="map-item"
v-for="(item, index) in mapData"
:key="index"
:class="{ active: index + 1 === parseInt(pointName) }"
>
插槽内容
</div>
</mapItem>
</div>
</template>
<script>
import mapItem from "@/components/map/index.vue";//引用组件
export default {
name: "map",
data() {
return {
pointName: "",//传递给组件的数据
isFullScreen: true,
mapData: [
//传递给组件的数据
],
};
},
components: {
mapItem,//声明组件
},
mounted() {},
methods: {
clickPoint(e) {
this.pointName = e;
},
},
};
</script>
<style scoped>
#map-container {
height: 100%;
}
#map-container {
display: flex;
}
.margin-top {
margin-top: 10px;
margin-left: 5px;
}
::v-deep .el-descriptions-item {
line-height: 0.85 !important;
font-size: 11px;
}
.map-item {
border: 1px solid #a3cffe;
border-bottom: 0;
}
.map-item:last-child {
border-bottom: 1px solid #a3cffe;
}
.active {
background-color: #a3cffe;
}
</style>
组件
<template>
<div class="map-container">
<div class="map-content">
</div>
<div class="map-right" v-if="$slots.default">
<!-- 默认插槽 -->
<slot />
</div>
</div>
</template>
<script>
export default {
name: "Map",
props: {//接受父组件传来的数据
mapData: Array,
isFullScreen:Boolean,
},
data() {
return {
pointName: "",
};
},
mounted() {},
methods: {
changePoint(e) {
this.pointName = e;
this.$emit('pointermove',this.pointName)
},
},
};
</script>
<style scoped>
.map-container {
height: 100%;
width:100%;
}
.map-container {
display: flex;
}
.map-container .map-content {
flex: 8;
}
.map-container .map-right {
flex: 2;
}
</style>
::v-deep和:deep()
在Vue中,::v-deep 和 :deep() 都用于修改CSS选择器的作用域。 区别在于:
- :deep() 是一个伪类选择器,可以用于将CSS规则应用于当前组件及其所有子组件中匹配选择器的元素。例如,.foo :deep(.bar)会选择包含class为"bar"的元素的所有嵌套层次结构。
- ::v-deep 是一个特殊的深度作用选择器,它只在scoped样式中起作用,并且可以将CSS规则应用于当前组件及其所有子组件中匹配选择器的元素。例如,.foo::v-deep .bar 会选择包含class为"bar"的元素的所有嵌套层次结构,但仅对 .foo组件的样式生效。
$.nextTick
为什么使用nextTick
因为 vue 采用的异步更新策略,当监听到数据发生变化的时候不会立即去更新DOM,而是开启一个任务队列,并缓存在同一事件循环中发生的所有数据变更;这种做法带来的好处就是可以将多次数据更新合并成一次,减少操作DOM的次数,如果不采用这种方法,假设数据改变100次就要去更新100次DOM,而频繁的DOM更新是很耗性能的。
nextTick的作用和使用
- nextTick 接收一个回调函数作为参数,并将这个回调函数延迟到DOM更新后才执行;
- 使用场景:想要操作 基于最新数据生成的DOM 时,就将这个操作放在 nextTick 的回调中;
new Vue({
data() {
return {
message: 'Hello, Jock!!'
}
},
mounted() {
this.message = 'Hi,Tom'
this.$nextTick(() => {
// 在DOM更新之后执行的操作
console.log(this.$el.textContent) // 输出:"Hi,Tom"
})
}
})
scoped的作用和原理
作用:
实现组件的私有化,不对全局造成样式污染,表示当前style属性只属于当前模块。
原理:
- 为组件实例生成一个唯一标识,给组件中的每个标签对应的dom元素添加一个标签属性:data-v-xxxx
- 给<style scoped>中的每个选择器的最后一个选择器添加一个属性选择器,原选择器[data-v-xxxx],如:原选择器为.container #id div,则更改后选择器为.container #id div[data-v-xxxx]
v-if 和 v-for是否可以一起使用 ?
可以一起使用,但是当 Vue
处理指令时,v-for
比 v-if
具有更高的优先级,这意味着,即使使用v-if来过滤某些元素不显示,Vue仍然会为数据集中的每个元素运行v-if判断,这导致了不必要的计算和渲染开销。此外,如果在一个元素上同时使用v-if和v-for,Vue会在开发者控制台中给出提示,建议将v-if移到外层去,以避免这种性能问题。
Vue.set
一般定义在data里的数据,由于双向绑定,操作后会发生改变,但是对于复杂数据类型中的值的改变Vue不能检测,所以我们需要用到Vue.set来实现。
为什么要使用Vue.set
因为 value 数据发生变化的时候,Vue 没有立刻去更新 DOM ,而是将修改数据的操作放在了一个异步操作队列中,如果一直修改相同数据,异步操作队列还会进行去重,等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行 DOM 的更新.
Vue.set的使用:
Vue.set( target, key, value )
target:要更改的数据源(可以是对象或者数组)
key:要更改的具体数据
value :重新赋的值
Vuex
- 提供了一个中心化的状态管理机制,便于多个组件共享和管理状态。
- 支持异步操作,适合大型项目,可以有效降低组件间的耦合度
v-show和v-if
- v-show是通过控制dom元素的display属性来控制元素的显示与隐藏;而v-if是直接在dom树中添加或者删除元素来控制元素的显示与隐藏。
- 切换条件时,v-if控制的元素会进行销毁和重建,而v-show控制的元素只是进行css切换。
- 如果进行不断的条件切换时,因为v-if会对元素进行销毁和重建,所以消耗的性能更大;v-show只是对css进行变更,所以消耗的性能更小。
Vue动态样式设置
对象语法:
可以传递一个对象给:style
,对象的属性是样式属性,值是动态计算的样式值
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<script>
export default {
data() {
return {
activeColor: 'red',
fontSize: 30
}
}
}
</script>
数组语法:
可以传递一个数组给:style
,数组中可以包含样式对象。
<div :style="[baseStyles, overridingStyles]"></div>
直接使用样式属性:
可以在元素上直接定义style
属性,内联写入样式。
<div style="color: green; font-size: 14px;"></div>
绑定到计算属性 :
可以将样式绑定到一个计算属性上,这个计算属性返回一个样式对象。
<div :style="computedStyle"></div>
data() {
return {
isActive: true
}
},
computed: {
computedStyle() {
return {
color: this.isActive ? 'green' : 'red',
fontWeight: this.isActive ? 'bold' : 'normal'
}
}
}
Vue2和Vue3的区别和优点
Vue2和Vue3的区别
Vue 2和Vue 3之间的主要区别体现在多个方面,包括底层API、语法、根节点、兼容性、生态支持以及开发工具的支持。以下是详细的对比:
底层API和响应式原理
- Vue 2:使用
Object.defineProperty
来实现响应式数据,这种方式可以劫持对象的属性,但对于数组和对象的变化处理不够灵活,新增属性需要手动处理才能实现响应式。 - Vue 3:采用
Proxy
来实现响应式数据,可以劫持整个对象,递归返回属性的值的代理,从而实现对整个对象的深度监听,包括属性的新增和删除。
根节点
- Vue 2:必须有一个根节点。
- Vue 3:可以没有根节点,多个根节点会被默认包裹在一个
Fragment
虚拟标签中,这有助于减少内存使用。
兼容性
- Vue 2:不支持IE8及以下版本。
- Vue 3:不支持IE11及以下版本。
语法和API
- Vue 2:采用选项式API,将数据和函数集中在一个对象中处理,这种方式在复杂项目中不利于代码的阅读和维护。
- Vue 3:采用组合式API,将同一个功能的代码集中在一起处理,使得代码更加有序,有利于代码的书写和维护。
开发工具
- Vue 2:主要使用Vetur插件进行代码高亮和语法提示,Vue 2 Snippets提供语法片段支持。
- Vue 3:推荐使用Volar插件进行代码高亮和语法提示,Vue 3 Snippets提供更好的Vue 3支持。
生态支持
- Vue 2:由JavaScript编写,对TypeScript的支持相对较弱。
- Vue 3:由TypeScript重写,提供更好的TypeScript支持,有利于使用TypeScript进行开发。
生命周期
Vue3.0中 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系:
- beforeCreate===>setup()
- created=======>setup()
- beforeMount ===>onBeforeMount
- mounted=======>onMounted
- beforeUpdate===>onBeforeUpdate
- updated =======>onUpdated
- beforeUnmount ==>onBeforeUnmount
- unmounted =====>onUnmounted
插槽
插槽就是子组件中的提供给父组件使用的一个占位符,用slot标签表示,父组件可以在这个占位符中填充任何模板代码,如 HTML、组件等,填充的内容会替换子组件的slot标签。简单理解就是子组件中留下个“坑”,父组件可以使用指定内容来补“坑”。vue插槽之插槽的用法及作用域插槽详解-CSDN博客
官方文档:https://v2.cn.vuejs.org/v2/guide/components-slots.html
数据双向绑定的原理
Vue的双向绑定原理是通过数据劫持和发布者-订阅者模式来实现的,关键步骤如下:
- 数据劫持:Vue组件初始化时,对 data 中的 item 进行递归遍历,会通过Object.defineProperty()方法来劫持对象的属性,这个方法可以对一个对象的属性进行定义或修改,包括定义getter和setter。这样,当这些被劫持的属性被读取或赋值时,就会触发相应的getter或setter函数。
- 依赖收集:在编译器编译模板的过程中,如果模板中出现了某个数据对象的属性,那么Vue会为这个属性添加一个订阅者,这个过程就是依赖收集。这样,当数据变化时,就可以通知所有依赖于这个数据的订阅者进行更新。
- 派发更新:当数据发生变化时,会触发setter函数,进而通知所有的订阅者。订阅者收到通知后,就会执行对应的更新函数,从而更新视图。
- 视图更新:最后,Vue会根据更新函数的指示,对DOM进行操作,从而实现视图的更新。
ref()和 reactive() 的区别
数据类型处理:
ref()
可以处理基础类型的值(如数字、字符串等)和引用类型的值(如对象)。当ref()
传递一个对象时,它实际上在内部使用了reactive()
方法将对象转换成具有深层次的响应式对象。这意味着,即使ref()
表面上看起来是在处理基础类型的值,如果传递的对象,它实际上会将这个对象通过reactive()
转换为响应式对象。reactive()
仅处理引用类型的值,不允许直接传递基础类型的值。如果尝试向reactive()
传递一个基础类型的值,它无法对其进行响应式追踪。
数据访问方式:
- 使用
ref()
时,无论是处理基础类型还是对象,都需要通过.value
的形式来访问数据。但在模板中使用ref()
的值时,不需要带上.value
。更新ref()
中的数据也需要通过.value
来进行。 reactive()
对象可以直接当作常规对象使用,无需通过特定的方法来访问或更新其属性。
返回值类型:
ref()
返回的是一个持有原始数据的RefImpl
实例,这个实例包含了原始值以及一些用于追踪变化的额外信息。reactive()
返回的是原始数据的代理Proxy
实例,这个代理实例允许对原始数据进行拦截和修改,从而实现对数据变化的处理和追踪。
总结:
选择使用ref()
还是reactive()
取决于你的具体需求和数据类型。如果你需要处理基础类型的值或者想要一个可以直接赋值的响应式引用,可以使用ref()
;如果你需要处理复杂的对象数据结构,并且希望这些对象的所有属性都能被Vue追踪变化,那么应该使用reactive()。
页面白屏的原因
- 元素标签或组件等重要格式出错,引用外部资源格式出错。
- 在渲染页面的时候需要加载很大的JS文件( app.js 和vendor.js ),在JS解析加载完成之前无法展示页面,从而导致了白屏(当网速不佳的时候也会产生一定程度的白屏)。
- 浏览器兼容问题。
- URL 网址无效或者含有中文字符。
- 缓存问题。
- 页面报错。
HTML篇
rem、em、vh、vw、px
em
- em是相对长度单位,它是基于父元素的字体大小。如父元素的字体大小为14px,子元素字体大小为1.2em,显示出来的就是16px。
- 当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸(1em = 16px)。
- 如果自身定义了
font-size
按自身来计算,整个页面内1em不是一个固定的值。
rem
rem也是相对单位,但它相对的只是HTML根元素font-size
的值。
vw,vh
- vw 就是根据窗口的宽度,分成100等份,100vw就表示满宽,50vw就表示一半宽。(vw 始终是针对窗口的宽),同理,vh则为窗口的高度。
- 在pc端中,指的是浏览器的可视区域。
- 在移动端中,指的则是布局视口。
px
绝对单位,页面按精确像素展示。
H5低版本浏览器如何处理
使用html5.js插件兼容(推荐)
<!--[if IE]>
<script src=”http://html5shiv.googlecode.com/svn/trunk/html5.js”></script>
<![endif]-->
CSS篇
CSS3新特性
- 圆角(border-radius)
div { border-radius: 10px; }
- 阴影(box-shadow)
div { box-shadow: 5px 5px 10px #888888; }
- 渐变(gradient)
div { background: linear-gradient(to right, red, yellow); }
- 变换(transform)
div:hover { transform: rotate(360deg); }
- 动画(animation)
@keyframes example { from {background-color: red;} to {background-color: yellow;} } div { animation-name: example; animation-duration: 4s; }
- 多列布局(multi-column layout)
div { column-count: 3; }
- 过渡效果(transition)
div:hover { transition: all 0.3s ease-in-out; }
- 用户界面(UI)
input[type="range"] { -webkit-appearance: none; background: #333; height: 10px; }
- 2D/3D转换(transform)
div { transform: translate(50px, 100px) rotate(360deg) scale(1.5); }
- 文字效果(text-effects)
div { text-shadow: 2px 2px 2px #888888; }
- 伪元素(::before, ::after)、媒体查询(@media)、字体图标(@font-face)、网格布局、弹性布局、透明度等。
display: none 和 visibily: hidden 区别?
- display:none:隐藏对应的元素,整个元素消失不占空间。
- visibily:hidden:隐藏对应的元素,元素还会占用空间。
Flex布局
Flex布局,也称为弹性布局,是一种现代的CSS布局方式,旨在为盒状模型提供最大的灵活性。这种布局模式允许最大程度地控制页面中元素的布局,特别是当屏幕尺寸或设备类型发生变化时。Flex布局特别适用于创建复杂的布局结构,如多行对齐、元素之间的间距调整、元素的顺序变化等。
Flex布局的优点:
Flex布局的优势在于其响应性和灵活性,能够轻松适应不同的屏幕尺寸和设备类型,提供了一种更加现代化和用户友好的网页布局方式。通过这些属性,开发者可以精细地控制元素的排列和对齐,从而实现复杂的布局效果。
Flex布局的核心概念包括:
- flex容器:任何可以被设置为Flex布局的元素,如通过设置
display: flex;
或display: inline-flex;
的元素。 - flex项目:容器内部的子元素,这些元素被称为flex项目。
Flex布局的主要属性包括:
- flex-direction:定义主轴的方向,可以是行(row)或列(column)。
- flex-wrap:如果一条轴线排不下,决定是否换行。
- flex-flow:这是flex-direction和flex-wrap的简写形式。
- justify-content:定义项目在主轴上的对齐方式。
- align-items:定义项目在交叉轴上的对齐方式。
- align-content:当有多根轴线时,定义这些轴线的对齐方式。
- gap row-gap、column-gap:设置容器内项目间的间距。
JS篇
typeof和instanceof的区别是什么?
- 作用:typeof检测数据类型,操作符返回一个字符串,表示未经计算的操作数的类型。instanceof检测对象之间的关联性,运算符用于检测构造函数的
prototype
属性是否出现在某个实例对象的原型链上。 - 返回值:typeof返回小写字母字符串表示数据属于什么类型,instanceof返回布尔值。
- 操作数:typeof是简单数据类型、函数或者对象,instanceof的左边必须是引用数据类型右边必须是函数。
- 操作数数量:typeof是1个,instanceof是2个。
typeof的用法
typeof "John" // 返回 string
typeof 3.14 // 返回 number
typeof false // 返回 boolean
typeof [1,2,3,4] // 返回 object
typeof {name:'John', age:34} // 返回 object
typeof undefined // 'undefined'
typeof Symbol() // 'symbol'
typeof null // 'object'
instanceof 的用法
// 定义构建函数
let Car = function() {}
let benz = new Car()
benz instanceof Car // true
let car = new String('xxx')
car instanceof String // true
let str = 'xxx'
str instanceof String // false
instanceof的底层原理
instanceof查找构造函数的原型对象是否在实例对象的原型链上,如果在返回true,如果不在返回false。 所以,只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false。
function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
var O = R.prototype; // 取 R 的显示原型
L = L.__proto__; // 取 L 的隐式原型
while (true) {
if (L === null)
return false;
if (O === L) // 当 O 显式原型 严格等于 L隐式原型 时,返回true
return true;
L = L.__proto__;
}
}
懒加载
懒加载也就是延迟加载。当访问一个页面的时候,先把img元素或是其他元素的背景图片路径替换成一张大小为1*1px图片的路径(这样就只需请求一次,俗称占位图),只有当图片出现在浏览器的可视区域内时,才设置图片正真的路径,让图片显示出来。这就是图片懒加载。
懒加载的原理
- 页面中的img元素,如果没有src属性,浏览器就不会发出请求去下载图片,
- 只有通过javascript设置了图片路径,浏览器才会发送请求。
- 懒加载的原理就是先在页面中把所有的图片统一使用一张占位图进行占位,
- 把真正的路径存在元素的“data-url”(这个名字起个自己认识好记的就行)属性里,要用的时候就取出来,再设置;
懒加载的实现步骤
- 首先,不要将图片地址放到src属性中,而是放到其它属性(data-original)中。
- 页面加载完成后,根据scrollTop判断图片是否在用户的视野内,如果在,则将data-original属性中的值取出存放到src属性中。
- 在滚动事件中重复判断图片是否进入视野,如果进入,则将data-original属性中的值取出存放到src属性中。
懒加载的优点
页面加载速度快、可以减轻服务器的压力、节约了流量,用户体验好。
Promise
Promise 是 ES6 引入的异步编程的新解决方案。语法上 Promise 是一个构造函数, 用来封装异步操作并可以获取其成功或失败的结果。
- 1) Promise 构造函数: Promise (excutor) {}
- 2) Promise.prototype.then 方法
- 3) Promise.prototype.catch 方法
Promise的具体表达:
- 从语法上来说: Promise 是一个构造函数
- 从功能上来说: promise 对象用来封装一个异步操作并可以获取其结果
promise 的状态改变
- pending 变为 resolved
- pending 变为 rejected
promise 的基本流程
promise 的基本使用
// 1 导入模块
const fs = require('fs')
// 2. 封装一个函数
function ReadFileFn(path) {
return new Promise((resolve, reject) => {
// 异步任务
fs.readFile(path, (err, reason) => {
if (err) {
reject(err)
} else {
resolve(reason)
}
})
})
}
// 3. 调用
ReadFileFn('./resource/1.txt').then(value => {
console.log(value.toString())
}, reason => {
console.log(reason)
})
为什么要用 Promise
指定回调函数的方式更加灵活
- 旧的: 必须在启动异步任务前指定
- promise: 启动异步任务 => 返回promie对象 => 给promise对象绑定回调函数(甚至可以在异步任务结束后指定/多个)
支持链式调用, 可以解决回调地狱问题
回调地狱的缺点?
- 不便于阅读
- 不便于异常处理
解决方案?
- promise 链式调用
终极解决方案?
- async/await
如何使用 Promise?
Promise 构造函数: Promise (excutor) {}
- (1) excutor 函数: 执行器 (resolve, reject) => {}
- (2) resolve 函数: 内部定义成功时我们调用的函数 value => {}
- (3) reject 函数: 内部定义失败时我们调用的函数 reason => {}
Promise.prototype.then 方法: (onResolved, onRejected) => {}
- (1) onResolved 函数: 成功的回调函数 (value) => {}
- (2) onRejected 函数: 失败的回调函数 (reason) => {}
Promise.prototype.catch 方法: (onRejected) => {}
- onRejected 函数: 失败的回调函数 (reason) => {}
Promise.resolve 方法: (value) => {}
- value: 成功的数据或 promise 对象
Promise.reject 方法: (reason) => {}
- reason: 失败的原因
Promise.all 方法: (promises) => {}
- promises: 包含 n 个 promise 的数组
Promise.race 方法: (promises) => {}
- promises: 包含 n 个 promise 的数组
promise 的几个关键问题
1. 如何改变 promise 的状态?
- (1) resolve(value): 如果当前是 pendding 就会变为 resolved
- (2) reject(reason): 如果当前是 pendding 就会变为 rejected
- (3) 抛出异常: 如果当前是 pendding 就会变为 rejected
2. 一个 promise 指定多个成功/失败回调函数, 都会调用吗?
- 当 promise 改变为对应状态时都会调用
3. 改变 promise 状态和指定回调函数谁先谁后?
- (1) 都有可能, 正常情况下是先指定回调再改变状态, 但也可以先改状态再指定回调
- (2) 如何先改状态再指定回调?
- 在执行器中直接调用 resolve()/reject()
- 延迟更长时间才调用 then()
- (3) 什么时候才能得到数据?
- 如果先指定的回调, 那当状态发生改变时, 回调函数就会调用, 得到数据
- 如果先改变的状态, 那当指定回调时, 回调函数就会调用, 得到数据
4. promise.then()返回的新 promise 的结果状态由什么决定?
- (1) 简单表达: 由 then()指定的回调函数执行的结果决定
- (2) 详细表达:
- 如果抛出异常, 新 promise 变为 rejected, reason 为抛出的异常
- 如果返回的是非 promise 的任意值, 新 promise 变为 resolved, value 为返回的值
- 如果返回的是另一个新 promise, 此 promise 的结果就会成为新 promise 的结果
Promise面试题
下面代码的输出顺序:
const first = () => (new Promise((resolve, reject) => {
console.log(3);//同
let p = new Promise((resolve, reject) => {
console.log(7);//同
setTimeout(() => {//宏
console.log(5);//宏 同
resolve(6)//宏 微 没有执行 状态已经更改为1 不会再更改了
}, 0)
resolve(1);//微
})
resolve(2);//微
p.then((arg) => {
console.log(arg);//微 1
})
}))
first().then(arg => {
console.log(arg);;//微 2
})
console.log(4);//同
/*
3
7
4
1
2
5
*/
对象的遍历
使用for...in
循环:
let obj = { a: 1, b: 2, c: 3 };
for (let key in obj) {
if (obj.hasOwnProperty(key)) { // 确保属性是对象自身的而不是继承的
console.log(key, obj[key]);
}
}
使用Object.keys()
结合forEach
:
let obj = { a: 1, b: 2, c: 3 };
Object.keys(obj).forEach(key => {
console.log(key, obj[key]);
});
使用Object.entries()
结合for...of
循环:
let obj = { a: 1, b: 2, c: 3 };
for (let [key, value] of Object.entries(obj)) {
console.log(key, value);
}
使用Object.getOwnPropertyNames()
结合forEach
:
深拷贝和浅拷贝
let obj = { a: 1, b: 2, c: 3 };
Object.getOwnPropertyNames(obj).forEach(key => {
console.log(key, obj[key]);
});
Set 和 Map 的区别
- Set 是一组唯一值的集合,而 Map 是一组键值对的集合。
- Set 中的值是唯一的,不允许重复;Map 中的键是唯一的,值可以重复。
- Set 中的值是插入顺序排序的,无法通过索引访问;Map 中的键是无序的,可以通过键来访问值。
- Set 提供了一些常见的集合操作方法,如添加、删除、查找等;Map 提供了一些常见的字典操作方法,如添加、删除、查找等。
- Set 可以通过 forEach 方法遍历,而 Map 可以通过 for...of 遍历。
- Set 没有键值对的概念,只有值;Map 有键值对的概念,需要同时操作键和值。
闭包
闭包(closure)是一个函数以及它所引用环境的组合。创建闭包的常见方式是在一个函数内部创建另一个函数,而该内部函数可以访问外部函数的局部变量,即使外部函数已经执行完毕。【JavaScript】一文了解JS的闭包_js闭包-CSDN博客
防抖与节流
- 防抖:触发高频事件后 n 秒内函数只会执行一次,如果 n 秒内高频事件再次被触发,则重新计算时间。作用:防止事件连续或高频触发,让它只触发一次或者最后一次。
var btn = document.querySelector("button"); var tiemr = null; btn.onclick = function () { clearInterval(tiemr); tiemr = setTimeout(() => { console.log("触发了") }, 500) }
- 节流:高频事件触发,但在 n 秒内只会执行一次,所以节流会稀释函数的执行频率。作用:降低事件触发的频率,比如1s内最多执行一次。
let btn = document.querySelector("button"); let jie = true; btn.onclick = function () { if (jie) { jie = false; console.log('触发了'); setTimeout(() => { jie = true; }, 2000) } }
axios的封装
Axios 是一个基于 promise 网络请求库。封装后我们只需要改变方法,请求路径,和请求返回的数据即可发送网络请求,省去大量重复的代码。
Axios封装代码
import axios from 'axios';
// 创建axios实例
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API, // api的base_url
timeout: 5000 // 请求超时时间
});
// 请求拦截器
service.interceptors.request.use(
config => {
// 可以在这里添加请求头等信息
// 例如:config.headers['Authorization'] = 'Bearer your-token';
return config;
},
error => {
// 请求错误处理
console.log(error); // for debug
Promise.reject(error);
}
);
// 响应拦截器
service.interceptors.response.use(
response => {
// 对响应数据做处理,例如只返回data部分
const res = response.data;
// 如果返回的状态码为200,说明成功,可以直接返回数据
if (res.code === 200) {
return res.data;
} else {
// 其他状态码都当作错误处理
// 可以在这里对不同的错误码进行不同处理
return Promise.reject({
message: res.message || 'Error',
status: res.code
});
}
},
error => {
// 对响应错误做处理
console.log('err' + error); // for debug
return Promise.reject(error);
}
);
export default service;
Axios封装后的使用
import service from '@/utils/request';
// 获取用户列表
export function getUserList(params) {
return service.get('/user/list', { params: params });
}
// 创建用户
export function createUser(data) {
return service.post('/user/create', data);
}
// 更新用户信息
export function updateUser(id, data) {
return service.put(`/user/update/${id}`, data);
}
call、apply、bind
bind、apply、call是Function原型的方法,而在js中所有的函数都是Function的实例,所以,有所有函数都可以使用这三个方法。
bind、apply、call有什么作用?
改变this指向 ,这是它们的共同作用,如下所示:
function fn() {
console.log(this, 111)
}
let obj = {
name: '谭梦寻'
}
fn() // 输出的是window全局对象
fn.call(obj) // 输出:{ name: '谭梦寻' } 111
bind、apply、call三者的区别
bind的特点:
- 不会调用原来的函数 可以改变原来函数内部的this 指向
- 返回的是原函数改变this之后产生的新函数
- 如果有的函数我们不需要立即调用,但是又想改变这个函数内部的this指向此时用bind
call的特点:
- call()方法接受的语法和作用与apply()方法类似,只有一个区别就是call()接受的是一个参数列表,而apply()方法接受的是一个包含多个参数的数组或伪数组。
- call()、apply()和bind的区别就是它们会调用函数
- call 的主要作用可以实现继承
apply的特点:
- 使用 apply, 我们可以只写一次这个方法然后在另一个对象中继承它,而不用在新对象中重复写该方法。
- apply 与 call() 非常相似,不同之处在于提供参数的方式。apply 使用参数数组或者伪数组,而不是一组参数列表。
- apply 可以使用数组字面量(array literal),如 fun.apply(this, [‘eat’, ‘bananas’]),或数组对象, 如 fun.apply(this, new Array(‘eat’, ‘bananas’))。
详细内容: JavaScript中bind、apply、call的理解-CSDN博客
数组常用方法
push()
- 在数组末尾添加一个或多个元素,并返回新的长度。
pop()
- 删除数组的最后一个元素,并返回那个元素。
shift()
- 删除数组的第一个元素,并返回那个元素。
unshift()
- 在数组的开始添加一个或多个元素,并返回新的长度。
slice()
- 选取数组的一部分,返回数组的一个浅拷贝。
splice()
- 通过删除现有元素和/或添加新元素来更改一个数组的内容。
concat()
- 连接两个或更多数组,并返回一个新数组。
join()
- 将数组中的所有元素转换为一个字符串。
reverse()
- 颠倒数组中元素的顺序。
sort()
- 对数组的元素进行排序。
forEach()
- 遍历数组中的每个元素并执行回调函数。
map()
- 创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。
filter()
- 创建一个新数组, 其包含通过所提供函数符合条件所有元素。
reduce()
- 对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。
reduceRight()
- 对数组中的每个元素执行一个由您提供的reducer函数(降序执行),将其结果汇总为单个返回值。
some() - 检测数组元素中是否有元素符合指定条件。
fill() - 使用一个固定值来填充数组。
更多内容参考:JavaScript Array 对象 | 菜鸟教程
Git篇
版本冲突
- 检测当前分支
git branch
- 确保你处于需要合并的分支上
git checkout your-branch
- 尝试合并另一个分支。
git merge other-branch
- 如果出现冲突,Git会告诉你哪些文件冲突了
# 查看所有冲突的文件 git status
- 打开冲突文件,你会看到类似这样的标记:
<<<<<<< your-branch 这是你的版本的内容 ======= 这是other-branch的内容 >>>>>>> other-branch
- 解决冲突后,添加并提交这些文件。
git add . git commit -m "解决冲突"
版本回退
- 查看提交历史,找到你想回退到的那个版本的commit id
git log
- 使用
git reset
命令回退到指定的commit id。git reset --hard commit-id
- 强制推送来更新远程仓库
git push origin your-branch -f
网络综合篇
url请求的过程
浏览器进程与网络进程的通信:浏览器进程通过进程间通信(IPC)将URL请求发送至网络进程。
本地缓存检查:网络进程首先检查本地缓存是否有所请求的资源。如果有缓存资源,则直接返回给浏览器进程;如果没有,则进入网络请求流程。
DNS解析:在进行网络请求前,需要进行DNS解析以获取请求域名的服务器IP地址。如果请求协议是HTTPS,还需要建立TLS连接。
TCP连接建立:利用IP地址和服务器建立TCP连接。
构建请求信息:连接建立后,浏览器端会构建请求行、请求头等信息,并将和该域名相关的Cookie等数据附加到请求头中,然后向服务器发送构建的请求信息。
服务器响应:服务器接收到请求信息后,根据请求信息生成响应数据(包括响应行、响应头和响应体等信息),并发给网络进程。
响应头解析:网络进程接收响应行和响应头后,开始解析响应头的内容。如果发现返回的状态码是301或302,说明需要进行重定向到其他URL。
重定向处理:网络进程从响应头的Location字段里读取重定向的地址,然后发起新的HTTP或HTTPS请求,重新开始整个过程。
LocalStorage 、 SessionStorage、cookie
共同点:
- 都是存储在浏览器本地的、相对不安全。
不同点:
- 写入方式:cookie 是由服务端写入的,需要服务器支持。而 localStorage 和 sessionStorage 都是由前端写入的。
- 生命周期:如果没有设置过期时间,cookie 默认关闭浏览器立即消失。而 localStorage 默认是永久储存,除非手动清除。sessionStorage 是页面关闭的时候就会自动清除。
- 存储大小:cookie 的存储大小大概4kb,存储条数为 20 条,超过20条后面的数据会替代前面的数据。localStorage 和 sessionStorage 存储大小大概5M,存储条数不限制。
- 兼容性:cookie 可以兼容低版本浏览器。localStroage 和 sessionStorage 是 H5 新特性,无法兼容 ie8 及以下的浏览器。
- 多Tab:LocalStorage 可以在多个 Tab(窗口) 打开,SessionStorage 限制必须在同一个页面,多 tab(窗口) 的时候不可以共享数据。
- 应用场景:前端向后端发起请求时会自动携带 cookie 里面的数据,但是 SessionStorage 和 localStorage 不会,所以他们的应用场景也不同。Cookie 一般用于存储登录验证信息 SessionID 或者 token。localStorage 常用于存储不易变动的数据,减轻服务器的压力。SessionStorage 可以用来检测用户是否刷新进入页面,如音乐播放器恢复播放进度条的功能。
什么是跨域?
跨域(Cross-Origin)指的是浏览器阻止前端网页从一个域名(Origin)向另一个域名的服务器发送请求。具体来说,一个页面的协议、域名、端口三者任意一个与请求的目标地址不同,就被视为跨域请求。
更多内容可以参考:什么是跨域?——详解跨域问题及其解决方案-CSDN博客
Http 、Https
这里参考两篇大佬的文章:【计算机网络之HTTP篇】HTTP协议详解_网址的协议名是什么-CSDN博客
POST请求提交数据的编码方式有三种
application/x-www-form-urlencoded
这是最常见的POST提交数据的方式了。浏览器的原生form表单,如果不设置 enctype属性,那么最终就会默认以application/x-www-form-urlencoded方式提交数据。这种情况下请求头的Content-Type被设置成application/x-www-form-urlencoded,提交的数据按照 key1=value1&key2=value2的方式进行编码,key和value都进行了URL转码。大部分服务端语言都对这种方式有很好的支持。很多时候,我们用Ajax提交数据时,也是使用这种方式。
multipart/form-data
表示在发送前不对数据进行编码,这种方式一般出现在通过form表单上传文件的场景中,在HTML的form标签中通过设置属性enctype=multipart/form-data来表示通过这种方式提交数据。上面提到的这两种POST数据的方式,都是浏览器原生支持的。
application/json
application/json 这个Content-Type作为响应头大家肯定不陌生。实际上,现在越来越多的人把它作为请求头,用来告诉服务端消息主体是序列化后的JSON字符串。由于JSON规范的流行,除了低版本IE之外的各大浏览器都原生支持JSON.stringify,服务端语言也都有处理JSON的函数,使用JSON不会遇上什么麻烦。
前端如何实现性能的优化:
- 设置Viewport:通过设置HTML的viewport,可以加快页面的渲染速度。
- 减少DOM节点:DOM节点过多会影响页面渲染速度,应尽量减少不必要的节点。
- 使用CSS3动画:合理使用CSS3动画代替JavaScript动画,以减少资源消耗和提高性能。
- 优化高频事件:如scroll、touchmove等事件,应使用函数防抖或节流技术来限制事件处理函数的执行频率。
- 不滥用WEB字体:减少不必要的WEB字体使用,因为字体需要下载、解析和重绘,会增加页面加载时间。
- 文件命名和归类:统一且有意义地命名文件,同类文件归类到相同文件夹中,便于管理和加载。
- SEO优化:包括标题、描述和关键词的优化,以及网站代码的精简,以提高搜索引擎排名和用户体验。
- 压缩和合并文件:将多个CSS和JavaScript文件合并成一个,并使用压缩工具减小文件大小,加快加载速度。
- 使用CDN加速:通过内容分发网络将静态资源分发到全球各地的服务器,减少用户访问延迟,提高加载速度。
- 优化图片:选择适当的图片格式,压缩图片大小,使用懒加载等技术减少初始加载时间。
- 开启GZIP压缩:对静态文件进行压缩,减少文件大小,提高加载速度。
- 异步加载script文件:将标签放在HTML文档的底部或添加defer属性,避免影响HTML解析和CSSOM构建。
- 减少重排和重绘:优化CSS选择器,减少DOM操作,避免不必要的布局变化。
- 使用Web Workers:对于计算密集型任务,使用Web Workers在后台线程中处理,避免阻塞主线程。
- 延迟加载组件:对于不常用的组件或资源,采用懒加载方式,只在需要时加载,减少初始加载时间。
前端如何处理浏览器兼容性问题:
- 使用兼容性函数来绑定事件 。
- 添加浏览器前缀、使用CSS hack等方法。
- 使用兼容性函数来获取样式和事件对象。