一、导学
学习前提
了解javascript和ES6基本语法
用过Vue React和Webpack
前端常见面试流程
一面(基础知识):JS基础知识、框架基本使用
二面(高级特性+原理):框架高级特性、框架原理
三面(设计+经验):项目设计能力、工作经验和环境
知识点介绍
Vue框架部分
Vue基本使用
Vue高级特性
Vue原理
React框架部分
React基本使用
React高级特性
React原理
工具部分
Webpack配置
性能优化
babel
项目设计
状态设计
组件设计
组件通讯
二、课程介绍
先看几个面试题
先看几个面试题,先不解答,自己思考
思考如何应对这些,以及所有的面试
Vue面试题
v-show和v-if的区别
为何v-for中要用key
描述Vue组件生命周期(有父子组件的情况)
Vue组件如何通讯
描述组件渲染和更新的过程(非常重要)
双向数据绑定v-model的实现原理
React面试题
React组件如何通讯
JSX本质是什么
context是什么,有何用途
shouldCompoentUpdate的用途
描述redux单向数据流
setState是同步还是异步(场景图,见下文)
框架综合应用
基于React设计一个todolist(组件结构,redux state 数据结构)
基于Vue设计一个购物车(组件结构,vuex state 数据结构)
webpack面试题
前端代码为何要进行构建和打包
module chunk bundle分别什么意思,有何区别?
loader和plugin的区别
webpack如何实现懒加载
webpack常用性能优化
babel-runtime和babel-polyfill的区别
如何应对上述面试题
框架的使用(基本使用,高级特性,周边插件)
框架的原理(基本原理的了解,热门技术的深度,全面性)
框架的实际应用,即设计能力(组件结构,数据结构)
面试官为何要这样考察
保证候选人能正常工作—考察使用
多个候选人竞争时,选择有技术追求—考察原理
看候选人是否能独立承担项目—考察设计能力
接下来
逐步讲解Vue React webpack的使用和原理
讲解Vue React如何做项目设计
三、Vue使用
1.概述
基本使用,组件使用—常用,必须会
高级特性—不常用,但体现深度
Vuex和Vue-router的使用
2.面试题
v-show和v-if的区别
为何v-for中要用key
描述Vue组件生命周期(有父子组件的情况)
Vue组件如何通讯
描述组件渲染和更新的过程
双向数据绑定v-model的实现原理
3.Vue基本使用
日常使用,必须掌握,面试必考(不一定会考)
梳理知识点,从冗长的文档中摘出考点和重点
考察形式不限(参考后面的面试真题),但都在范围之内
模板(指令、插值)
插值、表达式
指令、动态属性
v-html:会有XSS风险,会覆盖子组件
<template>
<div>
<p>文本插值 {{message}}</p>
<p>JS 表达式 {{ flag ? 'yes' : 'no' }} (只能是表达式,不能是 js 语句)</p>
<p :id="dynamicId">动态属性 id</p>
<hr/>
<p v-html="rawHtml">
<span>有 xss 风险</span>
<span>【注意】使用 v-html 之后,将会覆盖子元素</span>
</p>
<!-- 其他常用指令后面讲 -->
</div>
</template>
<script>
export default {
data() {
return {
message: 'hello vue',
flag: true,
rawHtml: '指令 - 原始 html <b>加粗</b> <i>斜体</i>',
dynamicId: `id-${Date.now()}`
}
}
}
</script>
computed和watch
computed有缓存,data不变则不会重新计算
watch如何深度监听
watch监听引用类型,拿不到oldVal
<template>
<div>
<p>num {{num}}</p>
<p>double1 {{double1}}</p>
<input v-model="double2"/>
</div>
</template>
<script>
export default {
data() {
return {
num: 20
}
},
computed: {
double1() {
return this.num * 2
},
double2: {
get() {
return this.num * 2
},
set(val) {
this.num = val/2
}
}
}
}
</script>
<template>
<div>
<input v-model="name"/>
<input v-model="info.city"/>
</div>
</template>
<script>
export default {
data() {
return {
name: '双越',
info: {
city: '北京'
}
}
},
watch: {
name(oldVal, val) {
// eslint-disable-next-line
console.log('watch name', oldVal, val) // 值类型,可正常拿到 oldVal 和 val
},
info: {
handler(oldVal, val) {
// eslint-disable-next-line
console.log('watch info', oldVal, val) // 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
},
deep: true // 深度监听
}
}
}
</script>
class和style
使用动态属性
使用驼峰式写法
<template>
<div>
<p :class="{ black: isBlack, yellow: isYellow }">使用 class</p>
<p :class="[black, yellow]">使用 class (数组)</p>
<p :style="styleData">使用 style</p>
</div>
</template>
<script>
export default {
data() {
return {
isBlack: true,
isYellow: true,
black: 'black',
yellow: 'yellow',
styleData: {
fontSize: '40px', // 转换为驼峰式
color: 'red',
backgroundColor: '#ccc' // 转换为驼峰式
}
}
}
}
</script>
<style scoped>
.black {
background-color: #999;
}
.yellow {
color: yellow;
}
</style>
条件渲染
v-if v-else的用法,可使用变量,也可以使用===表达式
v-if和v-show的区别
v-if和v-show的使用场景:切换频繁使用v-show
v-show和v-if都能控制元素的显示和隐藏。
v-show本质就是通过设置css中的display设置为none,控制隐藏,无论true或者false初始都会进行渲染,因此切换开销比较小,初始开销较大
v-if是动态的向DOM树内添加或者删除DOM元素,因为懒加载,初始为false时,不会渲染,因此初始渲染开销较小,切换开销比较大,要不停的销毁和创建)
切换频繁使用v-show
<template>
<div>
<p v-if="type === 'a'">A</p>
<p v-else-if="type === 'b'">B</p>
<p v-else>other</p>
<p v-show="type === 'a'">A by v-show</p>
<p v-show="type === 'b'">B by v-show</p>
</div>
</template>
<script>
export default {
data() {
return {
type: 'a'
}
}
}
</script>
循环(列表)渲染
如何遍历对象?—也可以用v-for
key的重要性。key不能乱写(如random或者index)
v-for和v-if不能一起使用
因为v-for的优先级比v-if高,意味着v-if 将分别重复运行于每个 v-for 循环中,造成不必要的计算,影响性能
解决方法:将 v-if 置于外层元素 或在计算属性中进行条件过滤
<template>
<div>
<p>遍历数组</p>
<ul>
<li v-for="(item, index) in listArr" :key="item.id">
{{index}} - {{item.id}} - {{item.title}}
</li>
</ul>
<p>遍历对象</p>
<ul >
<li v-for="(val, key, index) in listObj" :key="key">
{{index}} - {{key}} - {{val.title}}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
flag: false,
listArr: [
{ id: 'a', title: '标题1' }, // 数据结构中,最好有 id ,方便使用 key
{ id: 'b', title: '标题2' },
{ id: 'c', title: '标题3' }
],
listObj: {
a: { title: '标题1' },
b: { title: '标题2' },
c: { title: '标题3' },
}
}
}
}
</script>
事件
event参数,自定义参数
事件修饰符,按键修饰符
【观察】事件被绑定到哪里
<template>
<div>
<p>{{num}}</p>
<button @click="increment1">+1</button>
<button @click="increment2(2, $event)">+2</button>
</div>
</template>
<script>
export default {
data() {
return {
num: 0
}
},
methods: {
increment1(event) {
// eslint-disable-next-line
console.log('event', event, event.__proto__.constructor) // 是原生的 event 对象
// eslint-disable-next-line
console.log(event.target)
// eslint-disable-next-line
console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和 React 不一样
this.num++
// 1. event 是原生的
// 2. 事件被挂载到当前元素
// 和 DOM 事件一样
},
increment2(val, event) {
// eslint-disable-next-line
console.log(event.target)
this.num = this.num + val
},
loadHandler() {
// do some thing
}
},
mounted() {
window.addEventListener('load', this.loadHandler)
},
beforeDestroy() {
//【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
// 自己绑定的事件,需要自己销毁!!!
window.removeEventListener('load', this.loadHandler)
}
}
</script>
表单
v-model
常见表单项textarea checkbox radio select
修饰符lazy number trim
当添加.lazy修饰符之后,相当于 双向数据绑定不起作用了,主要用于控制数据同步的时机,改变input框中的内容并不会使数据发生变化,当输入框失去焦点后触发change事件,数据才更新
<template>
<div>
<p>输入框: {{name}}</p>
<input type="text" v-model.trim="name"/>
<input type="text" v-model.lazy="name"/>
<input type="text" v-model.number="age"/>
<p>多行文本: {{desc}}</p>
<textarea v-model="desc"></textarea>
<!-- 注意,<textarea>{{desc}}</textarea> 是不允许的!!! -->
<p>复选框 {{checked}}</p>
<input type="checkbox" v-model="checked"/>
<p>多个复选框 {{checkedNames}}</p>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
<p>单选 {{gender}}</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>下拉列表选择 {{selected}}</p>
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<p>下拉列表选择(多选) {{selectedList}}</p>
<select v-model="selectedList" multiple>
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
</div>
</template>
<script>
export default {
data() {
return {
name: '双越',
age: 18,
desc: '自我介绍',
checked: true,
checkedNames: [],
gender: 'male',
selected: '',
selectedList: []
}
}
}
</script>
4.组件
props和$emit
组件间通讯-自定义事件
//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>
//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()
//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>
生命周期
单个组件
- 挂载阶段(beforeCreate、created、beforeMount、mounted)
- 更新阶段(beforeUpdate、updated)
- 销毁阶段(beforeDestory、destroyed )
created和mounted区别
created把vue示例给初始化,只是存在js内存模型的内存变量而已,并没有开始渲染;
mounted组件真正在网页上汇聚完成了,页面渲染完成了
beforeDestory上可以做什么
解除绑定,销毁子组件以及事件监听器
父子组件
创建初始化vue实例是从外到内,只有父组件初始化完才能初始化子组件
渲染是从内到外,只有把子组件渲染完父组件才能渲染完
beforeUpdate:更新的时候父组件的data首先被修改,首先执行beforeUpdate
updated:只有子组件更新完了父组件才能更新完
渲染过程:父组件挂载完成一定是等子组件都挂载完成后,才算是父组件挂载完,所以父组件的mounted在子组件mouted之后。父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted
子组件更新过程:
影响到父组件: 父beforeUpdate -> 子beforeUpdate->子updated -> 父updated
不影响父组件: 子beforeUpdate -> 子updated
父组件更新过程:
影响到子组件: 父beforeUpdate -> 子beforeUpdate->子updated -> 父updated
不影响子组件: 父beforeUpdate -> 父updated
销毁过程:父beforeDestroy -> 子beforeDestroy -> 子destroyed -> 父destroyed
不管是哪种情况,都一定是父组件等待子组件完成后,才会执行自己对应完成的钩子
5.高级特性
//index.vue
<template>
<div>
<p>vue 高级特性</p>
<hr>
<!-- 自定义 v-model -->
<!-- <p>{{name}}</p>
<CustomVModel v-model="name"/> -->
<!-- nextTick -->
<!-- <NextTick/> -->
<!-- slot -->
<!-- <SlotDemo :url="website.url">
{{website.title}}
</SlotDemo> -->
<!-- <ScopedSlotDemo :url="website.url">
<template v-slot="slotProps">
{{slotProps.slotData.title}}
</template>
</ScopedSlotDemo> -->
<!-- 动态组件 -->
<!-- <component :is="NextTickName"/> -->
<!-- 异步组件 -->
<!-- <FormDemo v-if="showFormDemo"/>
<button @click="showFormDemo = true">show form demo</button> -->
<!-- keep-alive -->
<!-- <KeepAlive/> -->
<!-- mixin -->
<MixinDemo/>
</div>
</template>
<script>
// import CustomVModel from './CustomVModel'
// import NextTick from './NextTick'
// import SlotDemo from './SlotDemo'
// import ScopedSlotDemo from './ScopedSlotDemo'
// import KeepAlive from './KeepAlive'
import MixinDemo from './MixinDemo'
export default {
components: {
// CustomVModel
// NextTick
// SlotDemo,
// ScopedSlotDemo,
// FormDemo: () => import('../BaseUse/FormDemo'),
// KeepAlive
MixinDemo
},
data() {
return {
name: '双越',
website: {
url: 'http://imooc.com/',
title: 'imooc',
subTitle: '程序员的梦工厂'
},
// NextTickName: "NextTick",
showFormDemo: false
}
}
}
</script>
自定义v-model
//index.vue
<template>
<div>
<!-- 自定义 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.event 要对应起来
3. text1 属性对应起来
-->
</template>
<script>
export default {
model: {
prop: 'text1', // 对应 props text1
event: 'change1'
},
props: {
text1: String,
default() {
return ''
}
}
}
</script>
refs与$nextTick
Vue是异步渲染(原理部分会详细讲解)
data改变之后,DOM不会立刻渲染
$nextTick会在DOM渲染之后被处罚,以获取最新DOM节点
//index.vue
<template>
<div>
<!-- nextTick -->
<NextTick/>
</div>
</template>
<script>
import NextTick from './NextTick'
export default {
components: {
NextTick
},
data() {
return {
}
}
}
</script>
//NextTick.vue
<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 渲染完再回调
// 3. 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
this.$nextTick(() => {
// 获取 DOM 元素
const ulElem = this.$refs.ul1
// eslint-disable-next-line
console.log( ulElem.childNodes.length ) //没有$nextTick返回3,6,有返回6,9
})
}
}
}
</script>
slot
基本使用
//index.vue
<template>
<div>
<!-- slot -->
<SlotDemo :url="website.url">
{{website.title}}
</SlotDemo>
</div>
</template>
<script>
import SlotDemo from './SlotDemo'
export default {
components: {
SlotDemo
},
data() {
return {
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>
作用域插槽
//index.vue
<template>
<div>
<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 {
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>
具名插槽
动态组件
:is="component-name"用法
需要根据数据,动态渲染的场景,即组件类型不确定
比如一个新闻详情页,可能包含text、image、video组件,而且排序展示可能不一致
//index.vue
<template>
<div>
<!-- 动态组件 -->
<!-- <component :is="NextTickName"/> -->
<div v-for="(val,key) in newsData" :key="key">
<component :is="val.type"/>
</div>
</div>
</template>
<script>
import ScopedSlotDemo from './ScopedSlotDemo'
export default {
components: {
ScopedSlotDemo
},
data() {
return {
//NextTickName: "NextTick",
newsData:[
1:{type:'text},
2:{type:'text},
3:{type:'image},
]
}
}
}
</script>
异步组件
import()函数
按需加载,异步加载大组件
//index.vue
<template>
<div>
<!-- 异步组件 -->
<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>
keep-alive
缓存组件
频繁切换,不需要重复渲染
Vue常见性能优化
//index.vue
<template>
<div>
<!-- keep-alive -->
<KeepAlive/>
</div>
</template>
<script>
import KeepAlive from './KeepAlive'
export default {
components: {
KeepAlive
},
data() {
return {
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>
点击A-B-C-A-B-C
未加keep-alive前
加keep-alive后,离开不会destroyed,再次加载不会mounted
mixin
多个组件有相同的逻辑,抽离出来
mixin并不是完美的解决方案,会有一些问题
Vue 3 提出的Composition API旨在解决这些问题
//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和组件可能出现多对多的关系,复杂度较高
- 组件 的 data, methods 优先级高于mixin data, methods 优先级
- 生命周期函数,先执行 mixin 里面的,再执行组件里面的
- 组件的 自定义属性 优先级高于 mixin 自定义属性 优先级
6.Vuex使用
面试考点并不多(因为熟悉Vue之后,vuex没有难度)
但基本概念,基本使用和API必须掌握
可能会考察state的数据结构设计(后面会讲)
Vuex基本概念
state
getters
action
mutation
异步操作在Actions里进行
用于Vue组件
dispatch
commit
mapState
mapGetters
mapActions
mapMutations
Vuex 是专门为 Vue.js 设计的状态管理库,简单地说就是采用全局单例模式,将组件的共享状态抽离出来管理,使组件树中的每一个位置都可以获取共享的状态(变量)或者触发行为。实现响应式的全局变量
state–状态,在store实例中注册state;在组件中使用store.state访问
getters–类似计算属性,对store中某个属性相同的处理操作抽出出来,做了一个公共的处理,组件中通过store.getters调用
action–异步更改状态,参数有context和payload,context.state获取store变量,context.commit触发mutation,提交mutation去修改state,组件中使用this.store.dispatch调用
mutation–更改store中状态的唯一方法,参数有state和payload,不能包含异步操作,组件中使用this.$store.commit调用
辅助函数:mapState、mapMutations、mapGetters、mapActions,它们的使用我们可以配合ES6的展开运算符将其与局部计算属性或方法混合使用
import {mapState、mapMutations、mapGetters、mapActions} from ‘vuex’
compoted:{
...mapState({})
}
compoted:{
...mapGetters({})
}
methods:{
...mapActions({})
}
methods:{
...mapMutations({})
}
7.Vue-router使用
面试考点并不多(前提是熟悉Vue)
路由模式(hash、H5 history)
路由配置(动态路由、懒加载)
Vue-router路由模式
hash模式(默认),如abc.com/#/user/10
H5 history模式,如abc.com/user/20
后者需要server端支持,因此无特殊需求可选择前者
H5 history模式404不会跳转,需要配置404情况
Vue-router路由配置
四、Vue原理
Vue原理(大厂必考)
面试为何会考察原理
面试中如何考察?以何种方式
考察重点,而不是考察细节,掌握好2/8原则
和使用相关联的原理,例如vdom、模板渲染
整体流程是否全面?热门技术是否有深度?
Vue原理包括哪些?
组件化、响应式、vdom和diff、模板编译、渲染过程、前端路由
如何理解MVVM
组件化基础
“很久以前”就有组件化
asp jsp php已经有组件化了
nodejs中也有类似的组件化
数据驱动视图(MVVM,setState)
传统组件,只是静态渲染,更新还要依赖于操作DOM
数据驱动视图–Vue MVVM
数据驱动视图–React setState(暂时先不看)
Vue MVVM
总结
组件化
数据驱动视图
MVVM
监听data变化的核心API是什么
Vue响应式
组件data的数据一旦变化,立刻触发视图的更新
实现数据驱动视图的第一步
考察Vue原理的第一题(vue响应式的原理是什么?)
核心API-Object.defineProperty
如何实现响应式,代码演示
Object.defineProperty的一些缺点(Vue3.0启用Proxy)
Proxy有兼容性问题
Proxy兼容性不好,且无法polyfill
Vue2.x还会存在一段时间,所以都得学
Vue3.0相关知识,下一章讲,这里只是先提一下
Object.defineProperty基本用法
Object.defineProperty实现响应式
监听对象,监听数组
复杂对象,深度监听
几个缺点
如何深度监听data变化、数组变化
Object.defineProperty缺点
深度监听,需要递归到底,一次性计算量大
无法监听新增属性/删除属性(Vue.set Vue.delete)
无法原生监听数组,需要特殊处理
// 触发更新视图
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) // 监听数组
总结
基础API-Object.defineProperty
如何监听对象(深度监听),监听数组
Object.defineProperty的缺点
虚拟DOM-面试里的网红
虚拟DOM(Virtual DOM)和diff
vdom是实现vue和React的重要基石
diff算法是vdom中最核心、最关键的部分
vdom是一个热门话题,也是面试中的热门话题
DOM操作非常耗费性能
以前用jQuery,可以自行控制DOM操作的时机,手动调整
Vue和React是数据驱动视图,如何有效控制DOM操作?
解决方案-vdom
有了一定复杂度,想减少计算次数比较难
能不能把计算,更多的转移为JS计算?因为JS执行速度很快
vdom-用JS模拟DOM结构,计算出最小的变更,操作DOM
通过snabbdom学习vdom
简洁强大的vdom库,易学易用
Vue参考它实现的vdom和diff
github.com/snabbdom/sn…
Vue3.0重写了vdom的代码,优化了性能
但vdom的基本理念不变,面试考点也不变
React vdom具体实现和Vue也不同,但不妨碍统一学习
用过虚拟DOM吗?
snabbdom重点总结
h函数
vnode数据结构
patch函数
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script src="./demo1.js"></script>
</body>
</html>
const snabbdom = window.snabbdom
// 定义 patch
const patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])
// 定义 h
const h = snabbdom.h
const container = document.getElementById('container')
// 生成 vnode
const vnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),
h('li.item', {}, 'Item 2')
])
patch(container, vnode)
document.getElementById('btn-change').addEventListener('click', () => {
// 生成 newVnode
const newVnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),
h('li.item', {}, 'Item B'),
h('li.item', {}, 'Item 3')
])
patch(vnode, newVnode)
})
//table-without-vdom.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.2.0/jquery.js"></script>
<script type="text/javascript">
const data = [
{
name: '张三',
age: '20',
address: '北京'
},
{
name: '李四',
age: '21',
address: '上海'
},
{
name: '王五',
age: '22',
address: '广州'
}
]
// 渲染函数
function render(data) {
const $container = $('#container')
// 清空容器,重要!!!
$container.html('')
// 拼接 table
const $table = $('<table>')
$table.append($('<tr><td>name</td><td>age</td><td>address</td>/tr>'))
data.forEach(item => {
$table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td>/tr>'))
})
// 渲染到页面
$container.append($table)
}
$('#btn-change').click(() => {
data[1].age = 30
data[2].address = '深圳'
// re-render 再次渲染
render(data)
})
// 页面加载完立刻执行(初次渲染)
render(data)
</script>
</body>
</html>
//table-with-vdom.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script type="text/javascript">
const snabbdom = window.snabbdom
// 定义关键函数 patch
const patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])
// 定义关键函数 h
const h = snabbdom.h
// 原始数据
const data = [
{
name: '张三',
age: '20',
address: '北京'
},
{
name: '李四',
age: '21',
address: '上海'
},
{
name: '王五',
age: '22',
address: '广州'
}
]
// 把表头也放在 data 中
data.unshift({
name: '姓名',
age: '年龄',
address: '地址'
})
const container = document.getElementById('container')
// 渲染函数
let vnode
function render(data) {
const newVnode = h('table', {}, data.map(item => {
const tds = []
for (let i in item) {
if (item.hasOwnProperty(i)) {
tds.push(h('td', {}, item[i] + ''))
}
}
return h('tr', {}, tds)
}))
if (vnode) {
// re-render
patch(vnode, newVnode)
} else {
// 初次渲染
patch(container, newVnode)
}
// 存储当前的 vnode 结果
vnode = newVnode
}
// 初次渲染
render(data)
const btnChange = document.getElementById('btn-change')
btnChange.addEventListener('click', () => {
data[1].age = 30
data[2].address = '深圳'
// re-render
render(data)
})
</script>
</body>
</html>
vdom总结
用JS模拟DOM结构(vnode)
新旧vnode对比,得出最小的更新范围,最后更新DOM
数据驱动视图的模式下,有效控制DOM操作
虚拟DOM-diff算法概述
diff算法是vdom中最核心、最关键的部分
diff算法能在日常使用vue React中体现出来(如key)
diff算法是前端热门话题,面试“宠儿”
diff即对比,是一个广泛的感念,如linux diff命令、git diff等
两个js对象也可以做diff,如github.com/cujojs/jiff
两棵树做diff,如这里的vdom diff
树diff的时间复杂度O(n^3)
第一,遍历tree1;第二,遍历tree2
第三,排序
1000个节点,要计算1亿次,算法不可用
优化时间复杂度到O(n)
只比较同一层级,不跨级比较
tag不相同,则直接删掉重建,不再深度比较
tag和key,两者都相同,则认为是相同节点,不再深度比较
深入diff算法源码
生成vnode
h函数
patch函数
执行pre hook
第一个参数不是vnode-创建一个空的vnode(emptyNodeAt),关联到这个DOM元素
判断vnode是否相同(sameVnode)-key和sel都相等
相同执行patchVnode
不相同,直接删掉重建
patchVnode函数(vnode对比)
- 执行prepatch hook(生命周期的钩子)
- 设置vnode.elem
- vnode.text===undefined(vnode.children一般有值)
新旧都有children,updateChildren;
新children有,旧children无(旧text有),清空text,添加children(addVnodes);
旧children有,新children无,移除children(removeVnodes);
旧text有,清空
vnode.text!==undefined(vnode.children一般无值)
新旧text不一样,移除旧children,设置新text\
updateChildren函数
-
开始和开始对比
patchVnode()
累加累减 -
结束和结束对比
patchVnode()
累加累减 -
开始和结束对比
patchVnode()
累加累减 -
结束和开始对比
patchVnode()
累加累减 -
以上四个都未命中
拿新节点key,能否对应上oldCh中的某个节点的key
没对应上,New element
对应上,拿到对应上key的节点,判断sel是否相等,不相等New element,相等patchVnode()
不使用key全部删掉然后插入,使用key直接移动过来,不用做销毁然后重新渲染的过程
虚拟DOM-考点总结和复习
diff算法总结
patchVnode
addVnodes removeVnodes
updateChildren(key的重要性)
vdom和diff-总结
细节不重要,updateChildren的过程也不重要,不要深究
vdom核心概念很重要:h、vnode、patch、diff、key等
vdom存在的价值更加重要:数据驱动视图,控制DOM操作
模板编译前置知识点-with语法
模板编译
模板是vue开发中最常用的部分,即与使用相关联的原理
它不是html,有指令、插值、JS表达式,到底是什么
面试不会直接问,但会通过“组件渲染和更新过程”考察
前置知识:JS的with语法
vue template complier将模板编译为render函数
执行render函数生成vnode
with语法
改变{}内自由变量的查找规则,当做obj属性来查找
如果找不到匹配的obj属性,就会报错
with要慎用,它打破了作用域规则,易读性变差
const obj = {a:100,b:200}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) //undefined
//使用with,能改变{}内自由变量的查找方式
//使用{}内自由变量,当做obj的属性来查找
with(obj) {
console.log(a)
console.log(b)
console.log(c) //会报错!!!
}
vue模板被编译成什么?
模板不是html,有指令、插值JS表达式,能实现判断、循环
html是标签语言,只有JS才能实现判断、循环(图灵完备的:能实现顺序执行、判断、循环)
因此,模板一定是转换为某种JS代码,即编译模板
const compiler = require('vue-template-compiler')
// 插值
// const template = `<p>{{message}}</p>`
// with(this){return _c('p',[_v(_s(message))])}
// with(this){return createElement('p',[createTextVNode(toString(message))])}
//this->new Vue({....})
// h -> vnode
// createElement -> vnode
// // 表达式
// const template = `<p>{{flag ? message : 'no message found'}}</p>`
// // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}
// // 属性和动态属性
// 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}})])}
// // 条件
// 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")])])}
// 循环
// 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)}
// 事件
// const template = `
// <button @click="clickHandler">submit</button>
// `
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}
// 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}}})}
// render 函数
// 返回 vnode
// patch
// 编译
const res = compiler.compile(template)
console.log(res.render)
// ---------------分割线--------------
// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
// target._o = markOnce;
// target._n = toNumber;
// target._s = toString;
// target._l = renderList;
// target._t = renderSlot;
// target._q = looseEqual;
// target._i = looseIndexOf;
// target._m = renderStatic;
// target._f = resolveFilter;
// target._k = checkKeyCodes;
// target._b = bindObjectProps;
// target._v = createTextVNode;
// target._e = createEmptyVNode;
// target._u = resolveScopedSlots;
// target._g = bindObjectListeners;
// target._d = bindDynamicKeys;
// target._p = prependModifier;
// }
编译模板
模板编译为render函数,执行render函数返回vnode
基于vnode再执行patch和diff(后面会讲)
使用webpack vue-loader,会在开发环境下编译模板(重要)
vue组件中使用render代替template
讲完模板编译,再讲这个render,就比较好理解了
在有些复杂情况中,不能用template,可以考虑用render
React一直都用render(没有模板),和这里一样
总结
with语法
模板到render函数,再到vnode,再到渲染和更新
vue组件可以用render代替template
回顾和复习已学的知识点
组件渲染/更新过程
一个组件渲染到页面,修改data触发更新(数据驱动视图)
其背后原理是什么,需要掌握哪些要点
考察对流程了解的全面程度
回顾学过的知识
响应式:监听data属性getter setter(包括数组)
模板编译:模板到render函数,再到vnode
vdom:patch(elem,vnode)和patch(vnode,newVnode)
组件渲染/更新过程
初次渲染过程
更新过程
异步渲染
vue组件是如何渲染和更新的
初次渲染过程
- Step1:解析模板为 render 函数(这步操作或在开发中通过 vue-loader 已完成)
- Step2:触发响应式,监听 data 属性 getter 和 setter(下一步执行 render 函数可能会用到 getter)
- Step3:执行 render 函数,生成 vnode,渲染节点 patch(elem, vnode)
执行render函数会触发getter
<p>{{message}}</p>
<script>
export default {
data(){
return {
message:'hello',//会触发get
city:'北京'//不会触发get,因为模板没用到,即和视图没关系
}
}
}
</script>
更新过程
- Step1:修改 data,触发 setter(前提是该 data 此前在 getter 中已被监听,即模板中被引用的 data)
- Step2:重新执行 render 函数,生成 newVnode
- Step3:更新节点 patch(vnode, newVnode)
其中 vnode 和 newVnode 的最小差异由 patch 的 diff 算法计算。
完整流程图
组件渲染与更新的完整流程图如下所示:
- 黄色方框为 render 函数(此时模板已经编译完),它会生成 vnode(绿色 Virtual DOM Tree)。
- 黄色方框在执行 render 时,会触发(Touch)紫色圆圈(Data)里面的 getter。
- 紫色圆圈(Data)里的 getter 触发时,会收集依赖,模板里哪个变量的 getter 被触发了,就会将相应变量观察起来(蓝色圆圈 Watcher)
- 一旦修改了 Data,就会通知 Watcher,如果修改的 data 是之前作为依赖被观察的,则重新触发渲染(re-render)。
异步渲染
回顾$nextTick
汇总data的修改,一次性更新视图
减少DOM操作次数,提高性能
// 异步渲染
methods: {
addItem() {
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
// 1. 异步渲染,$nextTick 待 DOM 渲染完再回调
// 3. 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
this.$nextTick(() => {
// 获取 DOM 元素
const ulElem = this.$refs.ul1
// eslint-disable-next-line
console.log( ulElem.childNodes.length )
})
}
}
总结
渲染和响应式的关系
渲染和模板编译的关系
渲染和vdom的关系
如何用JS实现hash路由
前端路由原理
稍微复杂一点的SPA,都需要路由
vue-router也是vue全家桶的标配之一
属于“和日常使用相关联的原理”,面试常考
回顾vue-router的路由模式
hash
H5 history
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>
如何用JS实现H5 history路由
H5 history
用url规范的路由,但跳转不刷新页面
history.pushState
window.onpopstate
正常页面浏览
github.com/xxx 刷新页面
github.com/xxx/yyy 刷新页面
github.com/xxx/yyy/zzz 刷新页面
改造成H5 history模式
github.com/xxx 刷新页面
github.com/xxx/yyy 前端跳转,不刷新页面
github.com/xxx/yyy/zzz 前端跳转,不刷新页面
<!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>
总结
hash–window.onhashchange
H5 history–history.pushState和window.onpopstate
H5 history需要后端支持
两者选择
to B的系统推荐用hash,简单易用,对url规范不敏感
to C的系统,可以考虑选择H5 history,但需要服务端支持
能选择简单的,就别用复杂的,要考虑成本和收益
vue原理-考点总结和复习
组件化
组件化的历史
数据驱动视图
MVVM
响应式
Object.defineProperty
监听对象(深度),监听数组
Object.defineProperty的缺点(3个缺点)(Vue3用Proxy,后面会讲)
vdom和diff
应用背景
vnode结构
snabbdom使用:vnode h patch
模板编译
with语法
模板编译为render函数
执行render函数生成vnode
渲染过程
初次渲染过程
更新过程
异步渲染
前端路由
hash
H5 history
两者对比(如何选择)
五、Vue 面试真题演练
1.v-show和v-if的区别
- v-if 在编译过程中会被转化成三元表达式,条件不满足时不渲染此节点。
- v-show 会被编译成指令,条件不满足时控制样式将对应节点隐藏 (display:none)
使用场景
- v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景
- v-show 适用于需要非常频繁切换条件的场景
2.为何在v-for中用key
必须用key,且不能是index和random
diff算法中通过tag和key来判断,是否是sameNode
减少渲染次数,提升渲染性能
如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以更准确、更快速
更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
相关代码如下
// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用
function isSameVnode(oldVnode, newVnode) {
return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}
// 根据key来创建老的儿子的index映射表 类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index;
});
return map;
}
// 生成的映射表
let map = makeIndexByKey(oldCh);
3.描述vue组件生命周期(父子组件)
单组件:
- beforeCreate 在实例初始化之后,数据观测observer 和event、watcher事件配置之前被调用
- created 实例已经创建完成,在这一步,以下配置被完成
- 数据观测
- 属性和方法的运算
- watch/event时间回调
- $el尚未生成
- beforeMount 在挂载之前被调用,render尚未被调用
- mounted el被新创建的vm.$el替换,并挂载到实例上去之后调用
- beforeUpdate 数据更新时,被调用,发生在虚拟Dom重新渲染和打补丁之前
- update 由于数据更改导致的虚拟Dom重新渲染和打补丁,在这之后调用
- beforeDestroy 实例销毁之前调用
- destroyed 实例销毁之后调用,调用后Vue实例的所有东西都会被解绑,所有的事件监听会被移除,子实例被销毁,该钩子在服务端渲染期间不被调用
- keep-alive(activated & deactivated)
vue 的父组件和子组件生命周期钩子函数执行顺序?
- 加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted - 子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated - 父组件更新过程
父 beforeUpdate -> 父 updated - 销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
4.vue组件如何通讯(常见)
- props
- $emit
- $attr
- $listener
- provide inject (隔代通信)
- children
- vuex
5.描述组件渲染和更新的过程
组件渲染与更新
Vue 原理的三大模块分别为响应式、vdom 和模板编译,前面已经分别学习过,现在通过总结渲染过程来将它们串起来回顾。
初次渲染过程
- Step1:解析模板为 render 函数(这步操作或在开发中通过 vue-loader 已完成)
- Step2:触发响应式,监听 data 属性 getter 和 setter(下一步执行 render 函数可能会用到 getter)
- Step3:执行 render 函数,生成 vnode,渲染节点 patch(elem, vnode)
更新过程
- Step1:修改 data,触发 setter(前提是该 data 此前在 getter 中已被监听,即模板中被引用的 data)
- Step2:重新执行 render 函数,生成 newVnode
- Step3:更新节点 patch(vnode, newVnode)
其中 vnode 和 newVnode 的最小差异由 patch 的 diff 算法计算。
完整流程图
组件渲染与更新的完整流程图如下所示:
- 黄色方框为 render 函数(此时模板已经编译完),它会生成 vnode(绿色 Virtual DOM Tree)。
- 黄色方框在执行 render 时,会触发(Touch)紫色圆圈(Data)里面的 getter。
- 紫色圆圈(Data)里的 getter 触发时,会收集依赖,模板里哪个变量的 getter 被触发了,就会将相应变量观察起来(蓝色圆圈 Watcher)
- 一旦修改了 Data,就会通知 Watcher,如果修改的 data 是之前作为依赖被观察的,则重新触发渲染(re-render)。
6.双向数据绑定v-model的实现原理
简单的说,就是 :value 和 @input 的结合使用,v-model就是他们两个的语法糖。
input元素的value - this.name
绑定input事件 this.name = $event.target.value
data更新触发re-render
v-model 只是语法糖而已
v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
- text 和 textarea 元素使用 value property 和 input 事件;
- checkbox 和 radio 使用 checked property 和 change 事件;
- select 字段将 value 作为 prop 并将 change 作为事件 在普通标签上
<input v-model="sth" /> //这一行等于下一行
<input v-bind:value="sth" v-on:input="sth = $event.target.value" />
在组件上
<currency-input v-model="price"></currentcy-input>
<!--上行代码是下行的语法糖
<currency-input :value="price" @input="price = arguments[0]"></currency-input>
-->
<!-- 子组件定义 -->
Vue.component('currency-input', {
template: `
<span>
<input
ref="input"
:value="value"
@input="$emit('input', $event.target.value)"
>
</span>
`,
props: ['value'],
})
7.对MVVM的理解
8.computed 和 watch 的区别和运用的场景
computed 是计算属性,依赖其他属性计算值,并且 computed 的值有缓存,只有当计算值变化才会返回内容,它可以设置 getter 和 setter。
watch 监听到值的变化就会执行回调,在回调中可以进行一些逻辑操作。
计算属性一般用在模板渲染中,某个值是依赖了其它的响应式对象甚至是计算属性计算而来;而侦听属性适用于观测某个值的变化去完成一段复杂的业务逻辑
9.为何组件data必须是一个函数
因为js本身的特性带来的,如果 data 是一个对象,那么由于对象本身属于引用类型,当我们修改其中的一个属性时,会影响到所有Vue实例的数据。如果将 data 作为一个函数返回一个对象,那么每一个实例的 data 属性都是独立的,不会相互影响了
10.ajax请求应该放在哪个生命周期
可以在钩子函数 created、beforeMount、mounted 中进行异步请求,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值。
如果异步请求不需要依赖 Dom 推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面 loading 时间;
- ssr 不支持 beforeMount 、mounted 钩子函数,所以放在 created 中有助于一致性;
作者:Big shark@LX
链接:juejin.cn/post/696122…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
11.如何将组件所有props传递给子组件
$props:<User v−bind="$props" />
12.如何自己实现v-model
13.多个组件有相同的逻辑,如何抽离?
mixin
以及mixin的一些缺点
mixin的缺点:
变量来源不明确,不利于阅读
多mixin可能会造成命名冲突
mixin和组件可能出现多对多的关系,复杂度较高
mixin和组件的顺序?
- 组件 的 data, methods 优先级高于mixin data, methods 优先级
- 生命周期函数,先执行 mixin 里面的,再执行组件里面的
- 组件的 自定义属性 优先级高于 mixin 自定义属性 优先级
14.何时要使用异步组件
加载大组件
路由异步加载
15.何时需要使用keep-alive
缓存组件,不需要重复渲染
如多个静态tab页的切换
优化性能
juejin.cn/post/704307…
16.何时需要使用beforeDestory
解绑自定义事件 event.$off
清除定时器
解绑自定义的DOM时间,如window scroll等
17.什么是作用域插槽
18.Vuex中action和mutation有何区别
juejin.cn/post/696122…
action中处理异步,mutation不可以
mutation做原子操作
action可以整合多个mutation
19.Vue-router常用的路由模式
hash默认
H5 history(需要服务端支持)
vuerouter的两种模式的区别
- vue-router中有三种模式,分别是hash、history、abstract
- abstract在不支持浏览器的API换景使用
- hash模式兼容性好,但是不美观,不利于SEO
- history美观,historyAPI+popState,但是刷新会出现404
作者:海明月
链接:juejin.cn/post/704307…
vue-router 中常用的路由模式实现原理
hash 模式
- location.hash 的值实际就是 URL 中#后面的东西 它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,因此改变 hash 不会重新加载页面。
- 可以为 hash 的改变添加监听事件
window.addEventListener("hashchange", funcRef, false);
history 模式
利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。
这两个方法应用于浏览器的历史记录站,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
特点:虽然美观,但是刷新会出现 404 需要后端进行配置
作者:Big shark@LX
链接:juejin.cn/post/696122…
20.如何配置Vue-router异步加载
21.请用vnode描述一个DOM结构
22.监听data变化的核心API是什么
Object.defineProperty
以及深度监听、监听数组
有何缺点
23.Vue如何监听数组变化
Object.defineProperty不能监听数组变化
重新定义原型,重写push pop等方法,实现监听
Proxy可以原生支持监听数组变化
24.请描述响应式原理
监听data变化
组件渲染和更新的流程
25.diff算法的时间复杂度
O(n)
在O(n^3)基础上做了一些调整
26.简述diff算法过程
patch(elem,vnode)和patch(vnode,newVnode)
patchVnode和addVnodes和removeVnodes
updateChildren(key的重要性)
27.Vue为何是异步渲染,$nextTick何用
异步渲染(以及合并data修改),以提高渲染性能
$nextTick在DOM更新完之后,触发回调
nextTick 中的回调是在下次 DOM 更新循环结束之后执行的延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。主要思路就是采用微任务优先的方式调用异步方法去执行 nextTick 包装的方法
28.Vue常见性能优化方式
合理使用v-show和v-if
合理使用computed
v-for时价key,以及避免和v-if同时使用
自定义事件、DOM事件及时销毁
合理使用异步组件
合理使用keep-alive
data层级不要太深
使用vue-loader在开发环境做模板编译(预编译)
webpack层面的优化(后面会讲)
前端通用的性能优化,如图片懒加载
使用SSR
-
对象层级不要过深,否则性能就会差
-
不需要响应式的数据不要放到 data 中(可以用 Object.freeze() 冻结数据)
-
v-if 和 v-show 区分使用场景
-
computed 和 watch 区分使用场景
-
v-for 遍历必须加 key,key 最好是 id 值,且避免同时使用 v-if
-
大数据列表和表格性能优化-虚拟列表/虚拟表格
-
防止内部泄漏,组件销毁后把全局变量和事件销毁
-
图片懒加载
-
路由懒加载
-
第三方插件的按需引入
-
适当采用 keep-alive 缓存组件
-
防抖、节流运用
-
服务端渲染 SSR or 预渲染
六、vue3预学习
Vue3要来了Vue2就要过时了吗
Vue3
Vue3尚未发布,还在开发中
面试会考察候选人对新技术的关注程度(Vue太热门)
新版本发布之后,再做补充
Vue2.x马上就要过时了吗
Vue3从正式发布到推广开发,还需要一段时间
Vue2.x应用范围非常广,有大量项目需要维护、升级
Proxy存在浏览器兼容性问题,且不能polyfill
Vue3升级内容
全部用ts重写(响应式、vdom、模板编译等)
性能提升,代码量减少
会调整部分API
Proxy实现响应式
回顾Object.defineProperty
Proxy实现响应式
两者对比
Object.defineProperty的缺点
深度监听需要一次性递归
无法监听新增属性/删除属性(Vue.set Vue.delete)
无法原生监听数组,需要特殊处理
Proxy实现响应式
基本使用
Reflect
实现响应式
Proxy基本使用
// 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)
return result // 返回结果
},
set(target, key, val, receiver) {
// 重复的数据,不处理
if (val === target[key]) {
return true
}
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 // 是否删除成功
}
})
Reflect作用
和Proxy能力一一对应
规范化、标准化、函数式
替代掉Object上的工具函数
Vue3用Proxy实现响应式
Proxy实现响应式
深度监听,性能更好
可监听新增/删除属性
可监听数组变化
// 创建响应式
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)
性能如何提升的?
只有get时递归,而且不是一次性递归,获取proxyData.info时只会到city和a这一层,不会到b、c、d、e这一层
总结
Proxy能规避Object.defineProperty的问题
Proxy无法兼容所有浏览器,无法polyfill