思考🤔
v-show和v-if的区别- v-show: 节点一直存在,只是通过css display 来显示或隐藏,不会重新渲染和更新
- v-if: 节点会根据值来动态创建和销毁
- 使用场景
- 组件频繁切换显示状态时,用
v-show,可以降低渲染消耗组件创建以后; - 不需要频繁切换显示状态的用
v-if
- 组件频繁切换显示状态时,用
- 为何
v-for中要用key - Vue组件的生命周期调用顺序(有父子组件的情况)
- Vue组件如何通讯
- 父子组件,使用属性和触发事件
- 组件之间无关或者层级较深,使用自定义事件
- 使用Vuex通讯
- 描述组件渲染和更新的过程
- 描述数据绑定v-mode的实现原理
Vue 使用
- 基本使用 组件使用 ---- 常用 必须会
- 高级特性 ---- 不常用 可以体现深度
- Vuex 和 Vue-router 常用
Vue3.0出来后 Vue 和 React 越来越接近
- Vue3 Options API对应React Class Component
- Vue3 Composition API 对应 React Hooks
Vue基本使用
- 插值、表达式
- 指令、动态属性
- v-html: 会有 XSS 风险,会覆盖子组件
<template>
<div>
<p>插入值: {{ message }}</p>
<p>JS 表达式 {{ flag ? "yes" : "no" }} (只能是表达式,不能是 js 语句)</p>
<p>一条语句执行一个动作,一个表达式产生一个值</p>
<p :id="dynamicId">动态属性 id</p>
<p v-html="rawHtml"></p>
<!-- <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>",
dynamicId: `id-${Date.now()}`,
};
},
};
</script>
- computed 有缓存, data 不变不会重新计算
<template>
<p>num {{ num }}</p>
<p>aDouble {{ aDouble }}</p>
<input v-model="aPlus" />
</template>
<script>
export default {
data() {
return { num: 20 };
},
// computed有缓存,缓存可以提高性能
computed: {
// 仅读取
aDouble() {
return this.num * 2;
},
// 读取和设置
aPlus: {
get: function () {
return this.num * 1;
},
set: function (v) {
this.num = v / 2;
},
},
},
};
</script>
watch 监听
- watch 如何深度监听
- watch 监听引用类型,拿不到oldVal,指向同一个指针
<template>
<div>
<input v-model="name" />
<input v-model="info.city" />
</div>
</template>
<script>
export default {
data() {
return {
name: "aki",
info: {
city: "珠海",
},
};
},
watch: {
name(oldValue, val) {
console.log("watch name: ", oldValue, val); // 值类型,可以正常拿到 oldValue val
},
info: {
handler(oldValue, val) {
console.log("watch info: ", oldValue, 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: false,
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-show
- v-if v-else-if v-else 用法 可使用变量,也可以使用 === 表达式
- v-if 和 v-show 的区别
v-if 不会渲染
v-show 会渲染 但是会用display: none;来隐藏

- v-if 和 v-show 的使用场景
- 从性能考虑,频繁销毁就是用v-show,一次性的话就是用v-if
<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'">v-show A</p>
<p v-show="type === 'b'">v-show B</p>
</div>
</template>
<script>
export default {
data() {
return { type: "b" };
},
};
</script>
v-for
<template>
<div>
可以通过v-fo in 数字,来迭代
<div v-for="(val, index) in 10">val: {{ val }}, index: {{ index }}</div>
</div>
</template>

event
- 基本使用 传参
- 观察事件绑定在哪里 与react对比
<template>
<p>{{ num }}</p>
<button @click="increment1">+1</button>
<button @click="increment2(2, $event)">+2</button>
</template>
<script>
export default {
data() {
return {
num: 1,
};
},
methods: {
increment1(event) {
console.log("event :>> ", event);
console.log("event.currentTarget :>> ", event.currentTarget); // 注意,事件是被注册到当前元素的,和 React 不一样
console.log("event.__proto__ :>> ", event.__proto__.constructor);
this.num++;
// 与react对比
// 1. event 是原生的
// 2. 事件被挂载到当前元素
// 和 DOM 事件一样
},
increment2(val, event) {
this.num += val;
console.log('event :>> ', event.target);
}
},
};
</script>

- 事件修饰符

- 按键修饰符

表单 Form
- v-model
- 常见表单项 textarea select checkbox radio
- 修饰符 lazy number trim
<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>
<br />
<button @click="consoleData">consoledata</button>
</div>
</template>
<script>
export default {
data() {
return {
name: "aki",
age: 10,
desc: "自我介绍",
checked: true,
checkedNames: [],
gender: "male",
selected: "",
selectedList: [],
newProp: "",
};
},
methods: {
consoleData() {
console.log(this.$data);
},
},
};
</script>
Vue 中动态添加具有响应式的 form 属性的方法如下:
使用 Vue.set 方法: 使用下标访问:
this.$set(this.form, 'newProp', '')
调用 Vue.set 方法或 this.$set 方法,来动态添加一个具有响应式的 newProp 属性。
请注意,在 Vue 2.x 版本中,不能直接为对象添加新的属性,因为新增的属性不会被监听。因此,必须使用上述方法来动态添加属性,从而实现具有响应式的效果。
踩过坑:动态赋值,select 时没有回显
Vue父子组件如何通讯
Vue组件使用
父子组件通讯
- props 父组件 -> 子组件
- $emit 子组件 触发 父组件 事件
组件之间如何通讯
通过自定义事件
event.js
import Vue from 'vue'
export default new Vue()
组件A
```js
import event from "./event";
...
mounted() {
event.$on("onAddTitle", this.onAddTitle);
},
beforeUnmount() {
event.$off("onAddTitle");
},
```
组件B
```js
import event from "./event";
...
event.$emit("onAddTitle", this.title);
```
如何用自定义事件进行vue组件通讯
- new Vue实例进行通信
- event.$emit() 触发
- event.$on() 监听
- event.$off() 解绑(防止内存泄露) 事件绑定时定义了名字的函数进行绑定,方便后续解绑 销毁,防止内存泄露
export default {
mounted() {
event.$on('onAdd', this.add)
},
beforeDestroy() {
event.$off('onAdd')
}
}
生命周期调用顺序
生命周期图示
- beforeCreate created
- beforeMount mounted
- beforeUpdate updated
- activated deactivated
- beforeDestroy destroyed
- errorCaptured

- beforeDestroy可能要做什么?
- created和mounted有什么区别?
生命周期(单个组件)
- 挂载阶段
- beforeCreate created
- beforeMount mounted
- 更新阶段
- beforeUpdate updated
- 销毁阶段
- beforeDestroy destroyed
生命周期(父子组件)
调用顺序
- 创建(从外到内)、渲染(从内到外)
- beforeCreate(父)
- created(父)
- beforeMount(父)
- beforeCreate(子)
- created(子)
- beforeMount(子)
- mounted(子)
- mounte(父)
- 更新
- 父 beforeUpdate
- 子 beforeUpdate
- 子 updated
- 父 updated
- 销毁
- 父 beforeDestroy
- 子 beforeDestroy
- 子 destroyed
- 父 destroyed
高级特性
自定义v-model

$nextTick
Vue.nextTick( [callback, context] )
Vue是异步渲染,data改变后,DOM不会立刻渲染,$nextTick会在DOM渲染之后被触发,以获取最新DOM节点
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
refs
slot
- 是什么:作用域插槽、具名插槽
- 作用:让父组件可以往子组件中插入一段内容(不一定是字符串,可以是其他的组件,只要是符合Vue标准的组件或者标签都可以)
具名插槽

作用域插槽
让父组件可以访问到子组件的数据

动态、异步组件
- 动态组件
- 例子:新闻页面,图文视频组件的不同排列组合

- : is="component-name"用法
- 需要根据数据,动态渲染的场景。即组件类型不确定。

- 例子:新闻页面,图文视频组件的不同排列组合
- 异步组件(常用)
import 函数
import CustomComponent from './index': 同步加载,打包时也是打一个包出来
按需加载,异步加载大组件

Vue如何缓存组件:keep-alive
- keep-alive作用
- 缓存组件
- 频繁切换 不需要重复渲染
- 性能优化
- 使用场景:常见的tab切换使用keep-alive
- 用法
<keep-alive> <!-- tab 切换 -->
<KeepAliveStageA v-if="state === 'A'"/> <!-- v-show -->
<KeepAliveStageB v-if="state === 'B'"/>
<KeepAliveStageC v-if="state === 'C'"/>
</keep-alive>
- keep-alive 和 v-show的区别
- v-show是通过css样式属性display控制 显示/隐藏 DOM
- keep-alive是vue框架层级进行的js对象的渲染(一个组件就是一个js对象)
- 使用原则
- 带有层级的复杂组件,用keep-alive去包裹起来进行缓存,切换组件直接从缓存读取,大大提高性能。因为这样可以避免组件的频繁渲染销毁
- 简单组件 用v-show
Vue组件如何抽离公共逻辑?(mixin) Vue3中的Component API是什么?
多个组件有相同逻辑
- 实现

- mixin问题
- 变量来源不明确,不利于阅读(mixin中的变量或方法再当前组件是查不到的)
- 多mixin可能会造成命名冲突
- mixin和组件可能会出现多对多的关系,复杂度较高(一个组件引入多个mixin,多个组件引用一个mixin)
- Vue3 Component API解决逻辑复用方案
Vuex 状态管理模式
- 基本概念
- State
- Getters
- Actions
- Mutations
- Modules
- 用于Vue组件
- Dispatch
- Commit
- mapState
- mapGetters
- mapActions
- mapMutations
- 数据流(重点) Actions里才能做异步操作,常用于Ajax请求
Mutations是同步操作,力求原子最小化
进行异步操作(后端API接口数据)必须在Actions中进行,同时Actions整合多个Mutations的commit操作,Mutations是原子操作

vue-router
路由模式(hash、H5 history)
- hash模式(默认),eg: http://abc.com/#/user/001
- H5 history模式,eg: http://abc.com/user/001
- 需要server支持,因此无特殊需求选择hash模式
配置
const router = new Vue({
mode: 'history',
routes: [...]
})
路由配置(动态路由、懒加载)
- 动态路由
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('../components/A')
},
{
path: '/B',
component: () => import('../components/B')
},
]
})
Vue 原理
组件化?
如何理解MVVM
过去是通过操作DOM来实现网页的更新显示,现在是通过监听数据变化,驱动修改DOM
React和Vue都是这个模型
- M:Model 数据,组件中的data,或者是Vuex里的数据
- V:View 看到的视图
- VM: ViewModel 沟通 model 和 view 的桥梁,监听事件,监听指令,
- View 点击事件 各种DOM事件 ViewModel监听到 触发修改Model
- Model数据修改后,ViewModel Directives 重新渲染

监听数据变化的核心API(实现响应式)
Object.defineProperty
- 通过该API来监听数据,如果有变化,立刻触发视图渲染
- 缺点
- 深度监听,递归,一次性计算量大
- 无法监听新增、删除属性(Vue.set Vue.delete)
- 无法监听数组,需要特殊处理
基本用法
const object1 = {};
Object.defineProperty(object1, "name", {
get() {
return name;
},
set(newVal) {
name = newVal;
console.log("监听到name修改 set new value: ", name);
return name;
}
});
object1.name = "haha";
console.log(object1.name);
console.log(object1);
// https://codepen.io/huangzonggui/pen/ExvMpYO
监听对象(深度监听data变化)
监听数组变化
- 通过Object.create(Array.prototype)重新定义数组原型
- Ojbect.create创建的对象 原型指向Array.prototype,扩展该对象方法,不会污染原数组
- 判断监听的数据类型如果是数组,将监听对象的隐式原型修改为重新定义的数组原型

Proxy
Vue3采用Proxy 来监听数据变化
缺点
- Proxy浏览器兼容性不好,且不可以polyfill
vdom 和 diff
vdom(virtual DOM)
- 操作DOM耗费性能
- jQuery是通过控制操作DOM的时机,手动调整
- vue 和 react 是通过数据驱动视图,修改数据,计算出最小改变的DOM,再一次性修改,减少计算
- vdom 跨平台:浏览器、Android、iOS、小程序
如何计算最小变化
- 因为JS计算快,所以可以通过将操作DOM的频繁变更转移到计算JS中
- 用JS模拟DOM,构建出一棵Virtual DOM树,通过js来计算最小变更
- 最后一次性修改操作DOM
JS 模拟DOM的结构(vnode)
- tag
- props (className style id 事件等)
- childrens(子节点 数组 字符串)

模拟上图的DOM结构
{
tag: 'div',
props: {
id: 'div1',
className: ['container']
},
children: [
{
tag: 'p',
children: 'vdom'
},
{
tag: 'ul',
props: {
style: 'font-size: 20px'
},
children: [
{
tag: 'li',
children: 'a'
}
]
}
]
}
通过 snabbdom 学习虚拟DOM
A virtual DOM library with focus on simplicity, modularity, powerful features and performance.
- 简洁强大的vdom
- Vue 参考它实现vdom和diff
- snabbdom代码的解析
- 重点
- h函数
- vnode(virtual node 虚拟节点)的数据结构
- patch函数
snabbdom源码上vnode的数据结构
export interface VNode {
sel: string | undefined;
data: VNodeData | undefined;
children: Array<VNode | string> | undefined;
elm: Node | undefined;
text: string | undefined;
key: Key | undefined;
}
export interface VNodeData {
props?: Props;
attrs?: Attrs;
class?: Classes;
style?: VNodeStyle;
dataset?: Dataset;
on?: On;
hero?: Hero;
attachData?: AttachData;
hook?: Hooks;
key?: Key;
ns?: string; // for SVGs
fn?: () => VNode; // for thunks
args?: any[]; // for thunks
[key: string]: any; // for any other 3rd party module
}
diff算法
- diff 算法是vdom的核心、最关键部分
- diff 算法能在日常使用Vue React中体现出来(for 循环体中的 key 重要性)
- diff 即对比差异 是一个广泛的概念,如linux diff命令,git diff等
- 两个js对象也可以做diff,如:https://github.com/cujojs/jiff
- 两颗树做diff,如vdom dif
参考:diff 算法图
对比两颗树的差异
- 一般做法
- 遍历tree1
- 遍历tree2
- 排序
- 时间复杂度O(n^3),1000个节点,计算1亿次,算法不可用
- 优化时间复杂度为O(n)
- 只比较同一等级
- 如果
tag不同,则直接删除重建,不做深度比较 - 如果
tag和key都相同,则认为是相同节点,不做深度比较


源码
看源码,主要找到核心函数,h() patch()等,看它的参数、返回值和主要逻辑,不需要过于抠细枝末节
h函数:helper function for creating vnodespatch函数(调用patchVnode):The
patchfunction returned byinittakes two arguments. The first is a DOM element or a vnode representing the current view. The second is a vnode representing the new, updated view.patch(oldVnode, newVnode);
- patch函数逻辑
- 第一个参数不是
vnode- 第一个参数是DOM Element(DOM元素), 创建一个空的
vnode关联到这个 DOM 元素上
- 第一个参数是DOM Element(DOM元素), 创建一个空的
- 第一个参数是
vnode,调用sameNode判断vnode是否相同function sameVnode (vnode1: VNode, vnode2: VNode): boolean {
// key 和 sel 都相等
// undefined === undefined // true
return vnode1.key === vnode2.key && vnode1.sel === vnode2.sel;
}- 相同的 vnode(
key和sel都相等):vnode 对比(patchVnode) - 不同的 vnode,直接删除重建
- 相同的 vnode(
- 第一个参数不是
- patch函数逻辑
patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue)函数(调用updateChildren):对比两个节点,如果不同,节点相同,返回
节点不同(主要看
text或children,二选一,也就是有子节点,或者有字符串)- text没有值(一般children有值)
- 新旧节点都有`children`,调用 `updateChildren`
- 新节点有`children`,旧节点无`children`,添加(调用 `addVnodes`)
- 旧节点有`children`,新节点无`children`,删除children (调用`removeVnodes`)
- text有值(一般children无值)
- 将新的`text`替换旧的`text`function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
// 执行 prepatch hook
const hook = vnode.data?.hook;
hook?.prepatch?.(oldVnode, vnode);
// 设置 vnode.elem
const elm = vnode.elm = oldVnode.elm!;
// 旧 children
let oldCh = oldVnode.children as VNode[];
// 新 children
let ch = vnode.children as VNode[];
if (oldVnode === vnode) return;
// hook 相关
if (vnode.data !== undefined) {
// cbs: callbacks
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode);
vnode.data.hook?.update?.(oldVnode, vnode);
}
// vnode.text === undefined (vnode.children 一般有值)
if (isUndef(vnode.text)) {
// 新旧都有 children
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
// 新 children 有,旧 children 无 (旧 text 有)
} else if (isDef(ch)) {
// 清空 text
if (isDef(oldVnode.text)) api.setTextContent(elm, '');
// 添加 children
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
// 旧 child 有,新 child 无
} else if (isDef(oldCh)) {
// 移除 children
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
// 旧 text 有
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '');
}
// else : vnode.text !== undefined (vnode.children 无值)
} else if (oldVnode.text !== vnode.text) {
// 移除旧 children
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1);
}
// 设置新 text
api.setTextContent(elm, vnode.text!);
}
hook?.postpatch?.(oldVnode, vnode);
}
addVnodes、removeVnodesupdateChildren函数:四个首尾部指针对比,指针慢慢往中间移动,直到指针相遇,循环结束当遇到相同的节点,就会移动指针,这样做,循环体中的节点,如果没有变化,不需要重新渲染
当没有遇到相同的节点
- 判断`key`是否相同
- 不同,则是新的节点,插入
- 相同,则判断sel(内容)是否相等
- 相等则是旧的节点,直接移动,不用生成新的节点
- 不相等,生成新的节点
key的重要性- 当不传key时,会全部删除重建
- 当传index作为key时,会有排序的变化,例如,之前是0的元素排到第一位,也会出现问题
- 传入业务id作为key,直接移动

vdom 和 diff 总结
- 细节不重要,updateChildren 的过程也不重要,不用深究
- vdom 核心概念(重要):h、vnode、patch、diff、key 等
- vdom 存在的价值(重要):数据驱动视图、控制 DOM 操作
模板编译
- with语法
- 模板编译成render函数
- 执行render函数生成vnode
渲染过程
- 初次渲染过程
- 更新过程
- 异步渲染
前端路由原理
路由模式
- H5 history
- hash
选择
- toB系统,简单易用,推荐使用hash路由,对url规范不敏感
- toC系统,考虑选择H5 history,需要服务端支持
- 能选择简单的就不要选择复杂的,考虑成本和收益
hash
window.onhashchange
当 一个窗口的 hash (URL 中 # 后面的部分)改变时就会触发 hashchange 事件
url#号后面的部分
- hash变化,会触发路由的跳转,前进后退
- hash不会触发页面的刷新,这个是single page app(SPA)特点
- hash跟服务器无关,不会提交到后台
通过hash变化触发路由的跳转,后退,触发视图的渲染
修改hash方式
- 通过JS location.href=#user 来修改hash值
- 通过手动修改浏览器地址栏的hash值
- 通过浏览器的前进、后退
H5 history
用url规范的路由,但跳转不刷新页面
常规路由
H5 history路由
- http://github.com/xxx 刷新页面
- http://github.com/xxx/yyy 前端跳转,不刷新页面
- http://github.com/xxx/yyy/zzz 前端跳转,不刷新页面
实现的api
history.pushState()方法向当前浏览器会话的历史堆栈中添加一个状态history.pushState(state, title[, url])window.onpopstate是popstate事件在window对象上的事件处理程序.
H5 history 需要后端配合
- 无论你访问什么页面,都返回index.html这个路由
- 然后再由前端通过history.pushState()的方式除触发路由的切换
