前端面试题汇总 -前端框架
- 1. vue基本使用
- 2. vue组件使用
- 3. vue高级特性
- 4. vuex
- 5. vue-router
- 6. 组件化
- 7. 响应式
- 8. vdom和diff
- 9. 模板编译
- 10. 渲染过程
- 11. 前端路由
- 12. 双向数据绑定 v-model 的实现原理
- 13. 为何组件data必须是一个函数?
- 14. ajax请求应该放在哪个生命周期?
- 15. 如何将组件所有的props传递给自组件?
- 16. 多个组件有相同的逻辑,如何抽离?
- 17. 何时要使用异步组件?
- 18. 何时需要使用keep-alive?
- 19. 何时需要使用beforeDestroy?
- 20. Vuex中 action 和 mutation 有何区别?
- 21. 请描述响应式原理
- 22. 简述diff算法过程
- 23. Vue常见性能优化方式
- 24. Vue3 升级内容
- 25. 社区热门知识点:Proxy重写响应式
- 26、React 实现 Todo List 设计
- 27、Vue设计购物车(组件结构,vuex state 数据结构)
- 28、baseURL配置
1. vue基本使用
(1)computed和watch
- computed有缓存,data不变则不会重新计算,可以提高性能
- watch如何深度监听?
watch: {
name(oldVal, val){
console.log('watch name', oldVal, val)
// 值类型,可正常拿到oldVal和val
}
info:{
handler(oldVal, val){
console.log('watch info', oldVal, val)
// 引用类型,拿不到oldVal。因为指针相同,此时已经指向了新的val
},
deep: true // 深度监听
}
}
- watch监听引用类型,拿不到oldValue
(2)class和style
- 使用动态属性
<p :class="{black: isBlack, yellow: isYellow}">使用class</p>
<p :style="styleData">使用style</p>
- 使用驼峰式写法
data(){
return {
styleData: {
fontSize: '40px',
color: 'red'
}
}
}
(3)条件渲染
- v-if 和 v-else 的用法,可使用变量,也可以使用===表达式
- v-if 和 v-show 的区别?
- v-show 通过CSS display 控制显示和隐藏
- v-if 组件真正的渲染和销毁,而不是显示和隐藏
- v-if 和 v-show 的使用场景?
一次性的或更新不频繁的,选择 v-if
频繁切换的,选择 v-show
(4)循环(列表)渲染
- 如何遍历对象? —— 也可以用v-for
- key的重要性。key 不能乱写(如 random 或者 index),尽量与业务相关联
- v-for 和 v-if 不能一起使用!
为何在v-for中用key?
- 必须用key,且不能是index和random
- diff算法中通过tag和key
- 减少渲染次数,提升渲染性能
(5)事件
- event 参数,自定义参数
<button @click="increment1">+1</button>
<button @click="increment2(2, $event)">+2</button>
<script>
methods:{
increment1(event){
console.log('event', event.__proto__.constructor) // 是原生的event对象,没有进行过任何装饰
console.log(event.target) //button对象
console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和React不一样
}
increment2(val, event){
}
}
</script>
- 事件修饰符,按键修饰符
事件修饰符
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素出发的事件现在此处理,然后才交由内部元素进行处理 -->
<div v-on:click:capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
按键修饰符
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @ckick.ctrl="onClick">A</button>
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>
- 【观察】事件被绑定到哪里?
事件被挂载到当前元素
(6)表单
- v-model
- 常见表单项 textarea checkbox radio select
- 修饰符 lazy number trim
<p>输入框</p>
<input type="text" v-model.trim="name" /> // 截掉前后的空格
<input type="text" v-model.lazy="name" /> // 全部输入结束后显示
<input type="text" v-model.number="age" /> // 转化为数字
<p>多行文字</p>
<textarea v-model="desc"></textarea>
<!-- 注意,<textarea>{{desc}}</textarea> 时不允许的!-->
<p>复选框</p>
<input type="checkbox" v-model="checked" />
<p>多个复选框</p>
<input type="checkbox" id="jack" value="Jack" v-model="checked" />
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checked" />
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checked" />
<label for="mike">Mike</label>
<p>单选</p>
<input type="radio" id="male" value="male" v-model="gender" />
<label for="male">男</label>
<input type="radio" id="female" value="female" v-model="gender" />
<label for="female">女</label>
<p>下拉列表</p>
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<p>下拉列表(多选)</p>
<select v-model="selectedList" multiple>
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
2. vue组件使用
(1)props 和 $emit
(2)组件间通讯 - 自定义事件
Index.vue
<template>
<div>
<Input @add="addHandler"/>
<List :list="list" @delete="deleteHandler"/>
</div>
</template>
<script>
import Input from './Input'
import List from './List'
export default {
components: {
Input,
List
},
data() {
return {
list: [
{
id: 'id-1',
title: '标题1'
},
{
id: 'id-2',
title: '标题2'
}
]
}
},
methods: {
addHandler(title) {
this.list.push({
id: `id-${Date.now()}`,
title
})
},
deleteHandler(id) {
this.list = this.list.filter(item => item.id !== id)
}
},
created() {
// eslint-disable-next-line
console.log('index created')
},
mounted() {
// eslint-disable-next-line
console.log('index mounted')
},
beforeUpdate() {
// eslint-disable-next-line
console.log('index before update')
},
updated() {
// eslint-disable-next-line
console.log('index updated')
},
}
</script>
List.vue
<template>
<div>
<ul>
<li v-for="item in list" :key="item.id">
{{item.title}}
<button @click="deleteItem(item.id)">删除</button>
</li>
</ul>
</div>
</template>
<script>
import event from './event'
export default {
// props: ['list']
props: {
// prop 类型和默认值
list: {
type: Array,
default() {
return []
}
}
},
data() {
return {
}
},
methods: {
deleteItem(id) {
this.$emit('delete', id)
},
addTitleHandler(title) {
// eslint-disable-next-line
console.log('on add title', title)
}
},
created() {
// eslint-disable-next-line
console.log('list created')
},
mounted() {
// eslint-disable-next-line
console.log('list mounted')
// 绑定自定义事件
event.$on('onAddTitle', this.addTitleHandler)
},
beforeUpdate() {
// eslint-disable-next-line
console.log('list before update')
},
updated() {
// eslint-disable-next-line
console.log('list updated')
},
beforeDestroy() {
// 及时销毁,否则可能造成内存泄露(及时解绑自定义事件)
event.$off('onAddTitle', this.addTitleHandler)
}
}
</script>
Input.vue
<template>
<div>
<input type="text" v-model="title"/>
<button @click="addTitle">add</button>
</div>
</template>
<script>
import event from './event'
export default {
data() {
return {
title: ''
}
},
methods: {
addTitle() {
// 调用父组件的事件
this.$emit('add', this.title)
// 调用自定义事件
event.$emit('onAddTitle', this.title)
this.title = ''
}
}
}
</script>
event.js
import Vue from 'vue'
export default new Vue()
Vue组件如何通讯?
- 父子组件:props和$emit
- 组件之间无关系:自定义事件
- vuex通讯
(3)组件生命周期
单个组件:
created 和 mounted 区别?
created 只是把 vue 的实例初始化了,它只是存在于内存中的一个变量而已,并没有开始渲染。mounted 时真正在网页上绘制完成了。大部分时候需要在 mouted 里进行处理。
- 挂载阶段
- 更新阶段
- 销毁阶段
父子组件:
- 父组件先created,子组件再created
- 子组件先mounted,父组件再mounted
- 父组件先beforeUpdate,子组件再beforeUpdate
- 子组件先updated,父组件再updated
3. vue高级特性
(1)自定义 v-model
index.vue
<template>
<div>
<p>vue 高级特性</p>
<hr>
<!-- 自定义 v-model -->
<p>{{name}}</p>
<CustomVModel v-model="name"/>
</div>
</template>
<script>
import CustomVModel from './CustomVModel'
export default {
components: {
CustomVModel
},
data() {
return {
name: '双越'
}
}
}
</script>
CustomVModel.vue
<template>
<!-- 例如:vue 颜色选择 -->
<input type="text"
:value="text1"
@input="$emit('change1', $event.target.value)"
>
<!--
1. 上面的 input 使用了 :value 而不是 v-model
2. 上面的 change1 和 model.event1 要对应起来
3. text1 属性对应起来
-->
</template>
<script>
export default {
model: {
prop: 'text1', // 对应 props text1
event: 'change1'
},
props: {
text1: String,
default() {
return ''
}
}
}
</script>
(2)$nextTick
- Vue 是异步渲染
- data 改变之后,DOM不会立刻渲染,会异步渲染
- $nextTick 会在DOM渲染之后被触发,以获取最新DOM节点
<template>
<div id="app">
<ul ref="ul1">
<li v-for="(item, index) in list" :key="index">
{{item}}
</li>
</ul>
<button @click="addItem">添加一项</button>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
list: ['a', 'b', 'c']
}
},
methods: {
addItem() {
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
// 1. 异步渲染,$nextTick 待 DOM 渲染完再回调
// 2. 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
this.$nextTick(() => {
// 获取 DOM 元素
const ulElem = this.$refs.ul1
// eslint-disable-next-line
console.log( ulElem.childNodes.length )
})
}
}
}
</script>
(3)slot
父组件往自组件里面插点东西
a. 基本使用
Index.vue
<template>
<div>
<p>vue 高级特性</p>
<hr>
<!-- slot -->
<SlotDemo :url="website.url">
{{website.title}}
</SlotDemo>
</div>
</template>
<script>
import SlotDemo from './SlotDemo'
export default {
components: {
SlotDemo
},
data() {
return {
name: '双越',
website: {
url: 'http://imooc.com/',
title: 'imooc',
subTitle: '程序员的梦工厂'
}
}
}
}
</script>
SlotDemo.vue
<template>
<a :href="url">
<slot>
默认内容,即父组件没设置内容时,这里显示
</slot>
</a>
</template>
<script>
export default {
props: ['url'],
data() {
return {}
}
}
</script>
b. 作用域插槽
Index.vue
<template>
<div>
<p>vue 高级特性</p>
<hr>
<!-- slot -->
<ScopedSlotDemo :url="website.url">
<template v-slot="slotProps">
{{slotProps.slotData.title}}
</template>
</ScopedSlotDemo>
</div>
</template>
<script>
import ScopedSlotDemo from './ScopedSlotDemo'
export default {
components: {
ScopedSlotDemo
},
data() {
return {
name: '双越',
website: {
url: 'http://imooc.com/',
title: 'imooc',
subTitle: '程序员的梦工厂'
}
}
}
}
</script>
ScopedSlotDemo.vue
<template>
<a :href="url">
<slot :slotData="website">
{{website.subTitle}} <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
</slot>
</a>
</template>
<script>
export default {
props: ['url'],
data() {
return {
website: {
url: 'http://wangEditor.com/',
title: 'wangEditor',
subTitle: '轻量级富文本编辑器'
}
}
}
}
</script>
c. 具名插槽
<!-- NamedSlot组件 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footr>
<slot name="footer"></slot>
</footer>
</div>
<NamedSlot>
<!-- 缩写 <template #header> -->
<template v-slot:header>
<h1>将插入 header slot 中</h1>
</template>
<p>将插入到 main slot 中,即未命名的 slot</p>
<template v-slot:footer>
<p>将插入到 footer slot 中</p>
</template>
</NamedSlot>
(4)动态组件
- :is="component-name"用法
- 需要根据数据,动态渲染的场景。即组件类型不确定。
Index.vue
<template>
<div>
<p>vue 高级特性</p>
<hr>
<!-- 动态组件 -->
<!-- <component :is="NextTickName"/> -->
<div v-for="(val, key) in newsData" :key="key">
<component :is="val.type"/>
</div>
</div>
</template>
<script>
import NextTick from './NextTick'
export default {
components: {
NextTick
},
data() {
return {
// NextTickName: "NextTick",
newsData: {
1: {
type: 'text'
}
2: {
type: 'text'
}
3: {
type: 'image'
}
}
}
}
}
</script>
(5)异步组件
- import() 函数
- 按需加载,异步加载大组件
Index.vue
<template>
<div>
<p>vue 高级特性</p>
<hr>
<!-- 异步组件 -->
<FormDemo v-if="showFormDemo"/>
<button @click="showFormDemo = true">show form demo</button>
</div>
</template>
<script>
export default {
components: {
FormDemo: () => import('../BaseUse/FormDemo')
},
data() {
return {
showFormDemo: false
}
}
}
</script>
(6)keep-alive
- 缓存组件
- 频繁切换,不需要重复渲染
- Vue常见性能优化方式之一
Index.vue
<template>
<div>
<p>vue 高级特性</p>
<hr>
<!-- keep-alive -->
<KeepAlive/>
</div>
</template>
<script>
import KeepAlive from './KeepAlive'
export default {
components: {
KeepAlive
},
data() {
return {
name: '双越',
website: {
url: 'http://imooc.com/',
title: 'imooc',
subTitle: '程序员的梦工厂'
},
// NextTickName: "NextTick",
showFormDemo: false
}
}
}
</script>
KeepAlive.vue
<template>
<div>
<button @click="changeState('A')">A</button>
<button @click="changeState('B')">B</button>
<button @click="changeState('C')">C</button>
<keep-alive> <!-- tab 切换 -->
<KeepAliveStageA v-if="state === 'A'"/> <!-- v-show -->
<KeepAliveStageB v-if="state === 'B'"/>
<KeepAliveStageC v-if="state === 'C'"/>
</keep-alive>
</div>
</template>
<script>
import KeepAliveStageA from './KeepAliveStateA'
import KeepAliveStageB from './KeepAliveStateB'
import KeepAliveStageC from './KeepAliveStateC'
export default {
components: {
KeepAliveStageA,
KeepAliveStageB,
KeepAliveStageC
},
data() {
return {
state: 'A'
}
},
methods: {
changeState(state) {
this.state = state
}
}
}
</script>
KeepAliveStateA.vue
<template>
<p>state A</p>
</template>
<script>
export default {
mounted() {
// eslint-disable-next-line
console.log('A mounted')
},
destroyed() {
// eslint-disable-next-line
console.log('A destroyed')
}
}
</script>
KeepAliveStateB.vue
<template>
<p>state B</p>
</template>
<script>
export default {
mounted() {
// eslint-disable-next-line
console.log('B mounted')
},
destroyed() {
// eslint-disable-next-line
console.log('B destroyed')
}
}
</script>
KeepAliveStateC.vue
<template>
<p>state C</p>
</template>
<script>
export default {
mounted() {
// eslint-disable-next-line
console.log('C mounted')
},
destroyed() {
// eslint-disable-next-line
console.log('C destroyed')
}
}
</script>
(7)mixin
- 多个组件有相同的逻辑,抽离出来
- mixin并不是完美的解决方案,会有一些问题
- Vue3提出的Composition API旨在解决这些问题
Index.vue
<template>
<div>
<p>vue 高级特性</p>
<hr>
<!-- mixin -->
<MixinDemo/>
</div>
</template>
<script>
import MixinDemo from './MixinDemo'
export default {
components: {
MixinDemo
},
data() {
return {
name: '双越',
website: {
url: 'http://imooc.com/',
title: 'imooc',
subTitle: '程序员的梦工厂'
},
// NextTickName: "NextTick",
showFormDemo: false
}
}
}
</script>
MixinDemo.vue
<template>
<div>
<p>{{name}} {{major}} {{city}}</p>
<button @click="showName">显示姓名</button>
</div>
</template>
<script>
import myMixin from './mixin'
export default {
mixins: [myMixin], // 可以添加多个,会自动合并起来
data() {
return {
name: '双越',
major: 'web 前端'
}
},
methods: {
},
mounted() {
// eslint-disable-next-line
console.log('component mounted', this.name)
}
}
</script>
mixin.js
export default {
data() {
return {
city: '北京'
}
},
methods: {
showName() {
// eslint-disable-next-line
console.log(this.name)
}
},
mounted() {
// eslint-disable-next-line
console.log('mixin mounted', this.name)
}
}
mixin的问题:
- 变量来源不明确,不利于阅读
- 多mixin可能会造成命名冲突
- mixin和组件可能出现多对多的关系,复杂度较高
4. vuex
(1)state
(2)getters
(3)action
只有action里面可以做异步操作。
(4)mutation
(5)用于Vue组件
a. dispatch
b. commit
c. mapState
d. mapGetters
e. mapActions
f. mapMutations
5. vue-router
(1)路由模式(hash、H5 history)
- hash模式(默认),如http://abc.com/#/user/10
- H5 history模式,如http://abc.com/user/20
const router = new VueRouter({
mode: 'history', // 使用 h5 history 模式
routes: [...]
})
- 后者需要server端支持,因此无特殊需求可选择前者
(2)路由配置(动态路由、懒加载)
- 动态路由
const User = {
// 获取参数如10 20
template: '<div>User {{$route.params.id}}</div>'
}
const router = new VueRouter({
routes: [
// 动态路径参数 以冒号开头。能命中‘/user/10’,‘/user/20’等格式的路由
{ path: '/user/:id', component: User}
]
})
- 懒加载
export default new VueRouter({
routes: [
{
path: '/',
component: () => import(
/* webpackChunkName: "navigator" */
'./../components/Nabigator'
)
},
{
path: '/feedback',
component: () => import(
/* webpackChunkName: "feedback" */
'./../components/FeedBack'
)
}
]
})
6. 组件化
(1)“很久以前”的组件化
- asp、jsp、php已经有组件化了
- nodejs中也有类似的组件化
- 现在vue和react等的组件化和之前的jsp等的组件化有一个本质的区别,那就是数据驱动视图。
- 传统组件,只是静态渲染,更新还要依赖于操作DOM
- vue是通过MVVM,React是通过setState来数据驱动视图
- 即我们不再去自己操作DOM,而是改变数据,框架帮我们根据数据重新渲染视图
- 这一点使得我们在用vue和react开发时更加关注数据和业务逻辑,而不是关注于如何添加修改DOM,使前端开发到了一个新的平台上。
View中有点击等事件ViewModel会监听到,然后修改Model中的数据,Model中的数据变化会修改View视图。
7. 响应式
- 组件 data 的数据一旦变化,立即触发视图的更新
- 实现数据驱动视图的第一步
- 核心API - Object.defineProperty
- 由于Object.defineProperty的一些缺点,Vue3.0使用Proxy实现响应式
Proxy有兼容性问题
- Proxy兼容性不好,且无法polyfill
Object.defineProperty基本用法
const data = {}
const name = 'zhangsan'
Object.defineProperty(data, 'name', {
get: function(){
console.log('get')
return name
}
set: function(){
console.log('set')
name = newVal
}
});
// test
console.log(data.name) // get zhangsan
data.name = 'lisi' // set
Object.defineProperty缺点
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增属性 / 删除属性(需要 Vue.set, Vue.delete 去解决)
- 无法原生监听数组,需要特殊处理 - 重新定义原型,重写push、pop等方法,实现监听
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
// data.name = 'lisi'
// data.age = 21
// // console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组
8. vdom和diff
参考:
(1)virtual dom 虚拟DOM
(2)MVVM
9. 模板编译
- 模板是vue开发中最常用的部分,即与使用相关联的原理
- 它不是html,有指令、插值、JS表达式,能实现判断、循环
- vue template compiler 将模板编译为render函数
- 执行render函数生成vnode
- 基于vnode再执行patch和diff
- 使用webpack vue-loader,会在开发环境下编译模板
- 模板转换成html的过程,叫做模板编译
(1)with语法
- 改变{ }内自由变量的查找规则,当作obj属性来查找
- 如果找不到匹配的obj属性,就会报错
- with要慎用,它打破了作用于规则,易读性变差
(2)vue模板编译
const template = `<p>{{message}}</p>`
// with(this){return _c('p', [_v(_s(message))])}
_c: createElement 相当于h函数
_v: createTextVNode
_s: toString
_l: renderList
(3)表达式
const template = `<p>{{flag ? message : 'no message found'}}</p>`
// width(this){return _c('p', [_v(_s(flag? message: 'no message found'))])}
(4)属性和动态属性
const template = `
<div id="div1" class="container">
<img :src="imgUrl" />
</div>
`
// with(this){return _c('div', {staticClass: "container", attrs: {"id": "div1"}}, [_c('img', {attrs: {"src": imgUrl}})])}
(5)条件
const template = `
<div>
<p v-if="flag === 'a'">A<p>
<p v-else>B</p>
</div>
`
// with(this){return _c('div', [(flag === 'a') ? _c('p', [_v("A")]) : _c('p', [_v("B")])])}
(6)循环
const template = `
<ul>
<li v-for="item in list" :key="item.id">{{item.title}}</li>
</ul>
`
// with(this){return _c('ul', _l((list), function(item){return _c)("li", {key: item.id}, [_v(_s(item.title))])}), 0)}
(7)事件
const template = `
<button @click="clickHandler">submit</button>
`
// with(this){return _c('button', {on: {"click": clickHandler}}, [_v("submit")])}
(8)v-model
const template = `<input type="text" v-model="name" />`
// 主要看input事件
// with(this){return _c("input", {directives: [{name: "model", rawName: "v-model", value: (name), expression: "name"}], attrs: {"type": "text"}, domProps: {"value": (name)}, on: {"input": function($event){if($event.target.composing)return; name=$event.target.value}}})}
(9)vue组件中使用render代替template
- 在有些复杂情况中,不能用template,可以考虑用render
- React一直都用render(没有模板)
Vue.component('heading', {
// template: 'xxx',
render: function(createElement){
return createElement(
'h' + this.level,
[
createElement('a', {
attrs: {
name: 'headerId',
href: '#' + 'headerId'
}
}, 'this is a tag')
]
)
}
})
10. 渲染过程
(1)初次渲染过程
- 解析模板为render函数(或在开发环境已完成, vue-loader)
- 触发响应式,监听data属性getter、setter
- 执行render函数,生成vnode,patch(ele,vnode)
执行render函数会触发getter
<p>{{message}}</p>
<script>
export default{
data(){
return {
message: 'hello', // 会触发get
city: '北京' // 不会触发get,因为模板没用到,即和视图没关系
}
}
}
</script>
(2)更新过程
- 修改data,触发setter(此前在getter中已被监听)
- 重新执行render 函数,生成newVnode
- patch(vnode, newVnode)
(3)异步渲染
- $nextTick
- 汇总data的修改,一次性更新视图
- 减少DOM操作次数,提高渲染性能
- $nextTick
<template>
<div id="app">
<ul ref="ul1">
<li v-for="(item, index) in list" :key="index">
{{item}}
</li>
</ul>
<button @click="addItem">添加一项</button>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
list: ['a', 'b', 'c']
}
},
methods: {
addItem() {
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
// 1. 异步渲染,$nextTick 待 DOM 渲染完再回调
// 2. 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
this.$nextTick(() => {
// 获取 DOM 元素
const ulElem = this.$refs.ul1
// eslint-disable-next-line
console.log( ulElem.childNodes.length )
})
}
}
}
</script>
11. 前端路由
(1)前端路由原理
- hash
- H5 history
(2)网页url组成部分
// http://127.0.0.1:8881/01-hash.html?a=100&b=20#/aaa/bbb
location.protocol // 'http:'
location.hostname // '127.0.0.1'
location.host // '127.0.0.1:8881'
location.port // '8881'
location.pathname // '01-hash.html'
location.search // '?a=100&b=20'
location.hash // '#/aaa/bbb'
(3)hash的特点
- hash变化会触发网页跳转,即浏览器的前进、后退
- hash变化不会刷新页面,SPA必需的特点
- hash永远不会提交到server端(前端自生自灭)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>hash test</title>
</head>
<body>
<p>hash test</p>
<button id="btn1">修改 hash</button>
<script>
// hash 变化,包括:
// a. JS 修改 url
// b. 手动修改 url 的 hash
// c. 浏览器前进、后退
window.onhashchange = (event) => {
console.log('old url', event.oldURL)
console.log('new url', event.newURL)
console.log('hash:', location.hash)
}
// 页面初次加载,获取 hash
document.addEventListener('DOMContentLoaded', () => {
console.log('hash:', location.hash)
})
// JS 修改 url
document.getElementById('btn1').addEventListener('click', () => {
location.href = '#/user'
})
</script>
</body>
</html>
(4)H5 history
- 用url规范的路由,但跳转时不刷新页面
- history.pushState
- history.onpopstate
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>history API test</title>
</head>
<body>
<p>history API test</p>
<button id="btn1">修改 url</button>
<script>
// 页面初次加载,获取 path
document.addEventListener('DOMContentLoaded', () => {
console.log('load', location.pathname)
})
// 打开一个新的路由
// 【注意】用 pushState 方式,浏览器不会刷新页面
document.getElementById('btn1').addEventListener('click', () => {
const state = { name: 'page1' }
console.log('切换路由到', 'page1')
history.pushState(state, '', 'page1') // 重要!!
})
// 监听浏览器前进、后退
window.onpopstate = (event) => { // 重要!!
console.log('onpopstate', event.state, location.pathname)
}
// 需要 server 端配合,可参考
// https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
</script>
</body>
</html>
(5)如何选择哪一种?
- to B的系统推荐用hash,简单易用,对url规范不敏感
- to C的系统,可以考虑选择H5 history,但需要服务端支持
- 能选择简单的,就不用复杂的,要考虑成本和收益
12. 双向数据绑定 v-model 的实现原理
const template = `<input type="text" v-model="name" />`
// 主要看input事件
// with(this){return _c("input", {directives: [{name: "model", rawName: "v-model", value: (name), expression: "name"}], attrs: {"type": "text"}, domProps: {"value": (name)}, on: {"input": function($event){if($event.target.composing)return; name=$event.target.value}}})}
- input元素的value = this.name
- 绑定input事件 this.name = $event.target.value
- data 更新触发re-render
13. 为何组件data必须是一个函数?
因为使用时,export default 其实是当作一个类来使用,对类进行实例化。如果data不是函数的话,多个实例化的属性是能够共享的,修改一个,其他的也会改变。如果data是函数的话,data中的变量在闭包中,不会被其他实例化修改。
14. ajax请求应该放在哪个生命周期?
- mounted
- JS是单线程的,ajax异步获取数据
- 放在mounted之前没有用,只会让逻辑更加混乱
15. 如何将组件所有的props传递给自组件?
$props
<User v-bind="$props" />
16. 多个组件有相同的逻辑,如何抽离?
- mixin
- 以及mix的一些缺点
17. 何时要使用异步组件?
- 加载大组件
- 路由异步加载
18. 何时需要使用keep-alive?
- 缓存组件,不需要重复渲染
- 如多个静态tab页的切换
- 优化性能
19. 何时需要使用beforeDestroy?
- 解绑自定义事件 event.$off
- 清除定时器
- 解绑自定义的DOM事件,如window scroll等
20. Vuex中 action 和 mutation 有何区别?
- action 中处理异步,mutation 不可以
- mutation 做原子操作
- action 可以整合多个 mutation
21. 请描述响应式原理
- 监听data变化
- 组件渲染和更新的流程
22. 简述diff算法过程
- patch(elem, vnode) 和 patch(vnode, newVnode)
- patchVnode 和 addVnodes 和 removeVnodes
- updateChildren (key 的重要性)
23. Vue常见性能优化方式
- 合理使用 v-show 和 v-if
- 合理使用 computed,因为computed有缓存
- v-for 时加 key,以及避免和v-if同时使用
- 自定义事件、DOM事件及时销毁,不销毁可能倒置内存泄漏
- 合理使用异步组件
- 合理使用 keep-alive
- data 层级不要太深,响应式时计算深度太多,会卡
- 使用 vue-loader 在开发环境做模板编译(预编译)
- webpack 层面的优化
- 前端通用的性能优化,如图片懒加载
- 使用SSR
24. Vue3 升级内容
- 全部用 ts 重写(响应式、vdom、模板编译等)
- 提升性能,代码量减少
- 会调整部分API
25. 社区热门知识点:Proxy重写响应式
(1)基本使用
const data = {
name: 'zhangsan',
age: 20
}
// const data = ['a', 'b', 'c']
const proxyData = new Proxy(data, {
get(target, key, receiver){
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if(ownKeys.includes(key)){
console.log('get', key)
}
const result = Reflect.get(target, key, receiver)
console.log('get', key)
return result // 返回结果
},
set(target, key, val, receiver){
// 重复的数据,不处理
if(val === target[key]){
return true
}
const result = Reflect.set(target, key, receiver)
console.log('set', key, val)
console.log('result', result)
return result // 是否设置成功
},
deleteProperty(target, key){
const result = Reflect.deleteProperty(target, key)
console.log('deleteProperty', key)
console.log('result', result)
return result // 是否删除成功
}
})
// test
proxyData.age // get age // 20
proxyData.age = 30 //set age 30 // result true
delete proxyData.age //delete property age // result true
// data 为数组时
proxyData.push('d')
// get length
// set 3 d
Reflect.ownKeys([10, 20, 30]) //["0", "1", "2", "length"]
Reflect.ownKeys({a: 10, b: 20}) //["a", "b"]
(2)Reflect 作用
- 和 Proxy 能力一一对应
- 规范化、标准化、函数式
const obj = {a: 100, b: 200}
// 假如想判断 obj 中是否存在 a
'a' in obj // true
// 用Reflect
Reflect.has(obj, 'a') // true
// 删除a
delete obj.a // true
// 用Reflect写法
Reflect.deleteProperty(obj, 'b') // true
- 替代掉Object上的工具函数
const obj = {a: 100, b: 200}
Object.getOwnPropertyNames(obj) // ["a", "b"]
Reflect.ownKeys(obj) // ["a", "b"]
(3)Proxy 实现响应式
- 深度监听,性能更好
- 可监听 新增 / 删除 属性
- 可监听数组变化
- 能规避Object.defineProperty的问题
- Proxy无法兼容所有浏览器,无法polyfill
proxy-observe.js
// 创建响应式
function reactive(target = {}) {
if (typeof target !== 'object' || target == null) {
// 不是对象或数组,则返回
return target
}
// 代理配置
const proxyConf = {
get(target, key, receiver) {
// 只处理本身(非原型的)属性
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('get', key) // 监听
}
const result = Reflect.get(target, key, receiver)
// 深度监听
// 性能如何提升的?
return reactive(result)
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
const ownKeys = Reflect.ownKeys(target)
if (ownKeys.includes(key)) {
console.log('已有的 key', key)
} else {
console.log('新增的 key', key)
}
const result = Reflect.set(target, key, val, receiver)
console.log('set', key, val)
// console.log('result', result) // true
return result // 是否设置成功
},
deleteProperty(target, key) {
const result = Reflect.deleteProperty(target, key)
console.log('delete property', key)
// console.log('result', result) // true
return result // 是否删除成功
}
}
// 生成代理对象
const observed = new Proxy(target, proxyConf)
return observed
}
// 测试数据
const data = {
name: 'zhangsan',
age: 20,
info: {
city: 'beijing',
a: {
b: {
c: {
d: {
e: 100
}
}
}
}
}
}
const proxyData = reactive(data)
26、React 实现 Todo List 设计
(1)state 数据结构设计
- 用数据描述所有的内容
- 数据要结构化,易于程序操作(遍历、查找)
- 数据要可扩展,以便增加新的功能
(2)组件设计、组件通讯
- 从功能上拆分层次
- 尽量让组件原子化
- 容器组件(只管理数据)& UI组件(只显示视图)
<App> {/* 只负责管理数据 */}
<Input /> {/* 只负责输入,将数据结果给父组件 */}
<List> {/* 只负责显示列表,从父组件获取数据 */}
<ListItem /> {/* 显示单条数据,删除、切换完成状态*/}
<ListItem />
<ListItem />
</List>
</App>
(3)结合redux
27、Vue设计购物车(组件结构,vuex state 数据结构)
(1)data 数据结构设计
- 用数据描述所有的内容
- 数据要结构化,易于程序操作(遍历、查找)
- 数据要可扩展,以便增加新的功能
(2)组件设计、组件通讯
- 从功能上拆分层次
- 尽量让组件原子化
- 容器组件(只管理数据)& UI组件(只显示视图)
(3)结合vuex
<App> <!-- 管理所有数据 -->
<ProductionList><!-- 商品列表 -->
<ProductionListItem />
<ProductionListItem />
<ProductionListItem />
</ProductionList>
<CartList><!-- 购物车列表和总和 -->
<CartItem />
<CartItem />
</CartList>
</App>
28、baseURL配置
(1)在config/dev.env.js 和 config/prod.env.js 中配置开发环境和生产环境的默认路径
module.exports = merge(prodEnv, {
NODE_ENV: '"development"',
BASE_API: '"https://xxx.xx.com/vue-admin"',
})
module.exports = {
NODE_ENV: '"production"',
BASE_API: '"https://xxx.xx.com/vue-admin"',
}
(2)在需要的地方引入
axios.default.baseURL = process.env.BASE_API