思考🤔
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>
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: [],
};
},
methods: {
consoleData() {
console.log(this.$data);
},
},
};
</script>
Vue父子组件如何通讯
-
Vue组件使用
-
父子组件通讯
- props 父组件 -> 子组件
- $emit 子组件 触发 父组件 事件
-
组件之间如何通讯
通过自定义事件
event.js
import Vue from 'vue' export default new Vue()组件A
import event from "./event"; ... mounted() { event.$on("onAddTitle", this.onAddTitle); }, beforeUnmount() { event.$off("onAddTitle"); },组件B
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': 同步加载,打包时也是打一个包出来
- 按需加载,异步加载大组件
- import 函数
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: abc.com/#/user/001
- H5 history模式,eg: 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,扩展该对象方法,不会污染原数组
- Ojbect.create创建的对象 原型指向Array.prototype,扩展该对象方法,不会污染原数组
- 判断监听的数据类型如果是数组,将监听对象的隐式原型修改为重新定义的数组原型
Proxy
-
Vue3采用Proxy 来监听数据变化
-
缺点
- Proxy浏览器兼容性不好,且不可以polyfill
vdom 和 diff
vdom(virtual DOM)
- 操作DOM耗费性能
- jQuery是通过控制操作DOM的时机,手动调整
- vue 和 react 是通过数据驱动视图,修改数据,计算出最小改变的DOM,再一次性修改,减少计算
如何计算最小变化
- 因为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,如:github.com/cujojs/jiff
- 两课数做diff,如vdom dif
对比两颗树的差异
- 一般做法
- 遍历tree1
- 遍历tree2
- 排序
- 时间复杂度O(n^3),1000个节点,计算1亿次,算法不可用
- 优化时间复杂度为O(n)
- 只比较同一等级
- 如果
tag不同,则直接删除重建,不做深度比较 - 如果
tag和key都相同,则认为是相同节点,不做深度比较
源码
看源码,主要找到核心函数,h() patch()等,看它的参数、返回值和主要逻辑,不需要过于抠细枝末节
-
h函数:helper function for creating vnodes -
patch函数(调用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
- 将新的
- text没有值(一般children有值)
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、removeVnodes -
updateChildren函数:四个首尾部指针对比,指针慢慢往中间移动,直到指针相遇,循环结束- 当遇到相同的节点,就会移动指针,这样做,循环体中的节点,如果没有变化,不需要重新渲染
- 当没有遇到相同的节点
- 判断
key是否相同- 不同,则是新的节点,插入
- 相同,则判断sel(内容)是否相等
- 相等则是旧的节点,直接移动,不用生成新的节点
- 不相等,生成新的节点
- 判断
key的重要性- 当不传key时,会全部删除重建
- 当传index作为key时,会有排序的变化,例如,之前是0的元素排到第一位,也会出现问题
- 传入业务id作为key,直接移动
模板编译
- 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规范的路由,但跳转不刷新页面
常规路由
- 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 前端跳转,不刷新页面
实现的api
history.pushState()方法向当前浏览器会话的历史堆栈中添加一个状态history.pushState(state, title[, url])window.onpopstate是popstate事件在window对象上的事件处理程序.
H5 history 需要后端配合
- 无论你访问什么页面,都返回index.html这个路由
- 然后再由前端通过history.pushState()的方式除触发路由的切换