一. Vue基础
1. Composition API学习
1) reactive ref
reactive使用代理Proxy使普通对象(Object)和内置对象(Map,Set)响应式;
reactive出的是一个深度响应的Proxy对象;它所有的属性都是响应式的;但一旦被取出就不是响应式的了const state = reactive({foo: {count: 1},bar:2});state.foo.count++; // 响应式的const b = state.bar;b++; // 不是响应式的了
reactive出的Proxy对象,值为对象的属性也是一个Proxy对象;具有响应式 ```javascript const proxy = reactive({foo: { bar: 1}}) console.log(proxy.foo) // proxy类型的对象 console.log(proxy.foo.bar === 1) // true
const raw = {} proxy.nested = raw console.log(proxy.nested === raw) // false
`ref`包装一个值,使之成为响应式的1. `ref`包装一个原始值,变成一个对象;value指向原始值,value属性具有响应式;```javascriptconst count = ref(1)count.value++ // 响应式的
ref包装一个对象,对象变成Proxy类型,value指向这个Proxy对象;value指向的Proxy对象具有深度响应const count = ref({foo: {bar: 1}})count.value.foo // Proxy对象
Ref类型的值在<template>中自动unwrap,即可以不用value获取它的原始值;任何地方都可以自动解包装 ```javascript
4. `Ref`类型在`reactive`对象中也自动解包装javascript
const count = ref(2)
const state = reactive({ count })
console.log(state.count) // 2
5. `Ref`类型在数组Array和集合Collections(Map,Set)中不会自动解包装javascript
const count = ref(2)
const state = reactive([ count ])
console.log(state[0].value)
<a name="lwZ02"></a>
#### 2. v指令
1. `v-bind`:绑定响应式数据
1. `v-on`:绑定方法
1. `v-model`:input标签中`:value`和`@input`的缩写html
// 等价
4. `v-if v-else-if v-else`:判断是否渲染元素;如果为false元素不会添加到DOM中
4. `v-for`:循环,最好加上`:key`
<a name="uBxn0"></a>
#### 3. 计算属性computed
计算属性会收集函数中的依赖;当依赖改变的时候触发回调函数
<a name="OCvgn"></a>
#### 4. 声明周期钩子

<a name="Gu3Cv"></a>
#### 5. 监听器watch
响应式数据发生变化时触发的回调
<a name="Iia9K"></a>
#### 6. 父组件向子组件传递数据props
Composition API提供了运行时的宏命令`defineProps`
<a name="xdHVn"></a>
#### 7. 子组件向父组件传递数据events
Composition API提供了运行时的宏命令`defineEmits`声明自定义事件javascript
const emit = defineEmits([‘response’]);
emit(‘response’, ‘hi’);
<a name="ycIRL"></a>
#### 8. 插槽slot
插槽会替换`<slot>`标签内的内容,显示传递进来的元素html
// 父元素
// 子元素
<a name="hHJFF"></a>### 二. 进阶<a name="JQFVm"></a>#### 1. 理解MVC和MVVM<a name="TtSp7"></a>##### 1) MVCMVC将应用程序分为三个角色:Model,View和Controller;<br />Model也就是模型用于处理应用程序**数据逻辑**部分,通常模型对象负责存取数据。<br />View也也就是视图负责处理**数据显示**的部分,视图通常依据模型对象创建。<br />Controller也就是控制器负责处理**用户交互**的部分;控制器通常从视图读取数据,控制用户输入并向模型发送数据。Controller和View之间使用策略模式;用户操纵View生成一个事件(如按钮点击事件);Controller对象接收并解释事件,即应用策略模式实现不同的响应View和Model之间使用观察者模式;View注册为Model的观察者,当Model发生变化时就能通知到View<br /><br />参考:[浅谈 MVC 和 MVVM 模型](https://segmentfault.com/a/1190000020969313)<a name="bpC7e"></a>##### 2) MVVMMVVM指Model,View和ViewModel<br />Model指用于处理数据到模型;<br />View指视图;<br />ViewModel是连接View和Model的桥梁,他有两个方向:1. 将模型Model转换为视图View;即将后端传递的数据转换成看到的页面,通过**数据绑定**实现1. 将视图View转化为模型Model;即将页面转化为后端的数据,通过**DOM事件监听**实现。两个方向都是实现了可以称之为数据的双向绑定<br />参考:[Vue 的 MVVM 思想](https://juejin.cn/post/6879300070962003982)<a name="a2prE"></a>#### 2. date为什么是函数为了防止组件复用时data共享的问题。- data是组件原型上的一个属性`MyComponent.prototype.data`;- 如果data是对象,所有的组件实例共享data对象;- 如果data是函数返回一个对象,那么每次创建组件实例data方法会返回一个新的对象。<a name="xTCrE"></a>#### 3. 组件间通信的方式1. `props和emits`。父组件通过props向子组件传递数据;子组件通过emits注册事件,父组件监听该事件完成传值/ 通信在Options API中是`props emits`;在Composition API中是`defineProps defineEmits`;在组件实例上是`$props $emit()````html<ChildComp :message="msg" @response="getResponse" />
$parent和$children,$refs,直接获取组件的父组件实例,子组件实例,或某个组件的实例;vue3没有提供$children<input ref="input1"><script>...this.$refs.input1</script>
provide inject,通过依赖注入的方式可以向子孙组件传值,Options API和Composition API方法名相同 ```javascript // 父组件setup中 import { provide } from ‘vue’; provide(‘a’, 1);
// 子孙组件中,多种形式 inject: { b: { from: ‘a’ } }, inject: [‘a’]
4. `$attrs`,fallthrough属性,父组件传递给子组件,但是子组件没有在props或emits上声明的属性vue2中使用`$attrs`和`$listeners`实现多层嵌套传值```javascript// 父组件中<HelloWorld class="red" />// 子组件中console.log('this.$attrs :>> ', this.$attrs);// this.$attrs :>>// Proxy {class: 'red', __vInternal: 1}// ...// [[Target]]: Object// class: "red"// ...
- vuex状态管理
vue可以使用vuex集中状态管理,下面是vue2中使用vue3的教程
安装vuex
npm i -S vuex@3
创建
src/store/index.js文件,在里面创建vuex插件,并导出Vuex.Store实例store。
state用于存储状态,对应的辅助函数mapState,可以放在computed中getters是修饰器,辅助函数是mapGetters,可以放在computed中mutations用于修改状态,使用commit触发,必须是同步函数,辅助函数是mapMutations,可以放在methods中actions提交mutations,可以是异步函数;使用dispatch触发,辅助函数是mapActions,可以放在methods中
import Vue from 'vue';import Vuex from 'vuex';// Vue使用Vuex插件Vue.use(Vuex);const store = new Vue.Store({state: {number: 1},getters: {getHelloNumber(state) {return `hello ${state.number}`;}},mutations: {setNumber(state, payload) {state.number = payload.number;}},actions: {setNumberAsync(content, payload) {return new Promise((resolve) => {setTimeout(() => {content.commit('setNumber', payload);resolve();}, 1500)})}}});export default store;
在
main.js导入store,并在new Vue的时候加入store,以便能全局使用store,this.$store.state等import store from '@/store/index.js';new Vue({store // 可以全局this.$store使用vuex}).mount('#app');
vuex在组件中的用法
// 普通的使用方法this.$store.state.numberthis.$store.getters.getHelloNumberthis.$store.commit('setNumber', {number: 2}) // 后面只允许一个参数,建议用对象payloadthis.$store.dispatch('setNumberAsync', {number: 3}) //后面只允许一个参数
modules可以将一个store分割成多个模块 ```javascript const moduleA = {store…}; const moduleB = {store…}; modules: { a: moduleA, b: moduleB }
// 除了获取state外,其他使用方法不变,不需要加模块名 this.$store.state.a.count; this.$store.getters.getHelloNumber;
参考:[手把手教你使用Vuex](https://juejin.cn/post/6928468842377117709)6. EventBus事件总线可以创建一个新的Vue实例作为事件总线,在上面注册和监听事件;有两种创建EventBus的方法:作为Vue原型链上的属性或作为一个模块。<br />事件总线的缺点:1. 无法确定由谁触发事件,导致混乱1. 某个页面刷新后可能导致与之相关的EventBus被移除,其他组件无法监听到事件1. 重复操作某个页面可能导致EventBus重复触发```javascript// 第一种方式 main.js中Vue.prototyep.$EventBus = new Vue();// 第二种方式 EventBus.js中import Vue from 'vue';export default new Vue();// 注册事件this.$EventBus.$emit('hi', 1);import EventBus from './EventBus';EventBus.$emit('hello', 2);// 监听事件this.$EventBus.$on('hi', handler);import EventBus from './EventBus';EventBus.$on('hello', handler);
4. Vue3的生命周期
vue生命周期是组件从创建到销毁的过程;在这一过程中Vue提供一些生命周期钩子函数,让开发者在组件不同的阶段添加自己的代码逻辑。
Vue2和Vue3 Options API的生命周期钩子的名称基本相同,除了destory -> unMount
Composition API使用setup替代了beforeCreated和create
vue3生命周期Options API调用源码位置
beforeCreate在实例初始化后,数据观测(data observer)和watch/event事件配置之前被调用。此时data,methods,watch,computed上的数据和方法都访问不到created在实例创建完成后调用,完成以下配置:inject,methods,data,computed,watch,provide选项函数的解析。此时组件的属性和方法已经可以访问。但是DOM还不可以。可以使用this.$nextTick回调beforeMount在render函数首次被调用,DOM被挂载前被调用。服务端渲染期间不被调用mounted在挂载完成后发生,真实DOM完成挂载,数据完成双向绑定。beforeUpdate在响应式属性更新,DOM被更新前触发。在这个钩子中进一步更改状态不会触发附加的重新渲染updatedDOM更新完成。应该避免在这里修改响应式数据,可能导致无限循环更新。服务端渲染期间不被调用beforeUnmount/beforeDestory卸载组件实例前被调用;实例仍然可用,可以在此期间清除定时器等unmounted/destoryed卸载组件实例后被调用activedkeep-alive专属,在组件被激活时调用deactivedkeep-alive专属,在组件被未被激活时调用
问题:异步请求在哪里调用?
答:可以在created,beforeMount和mounted内进行。这是data被初始化完成,方法也可以调用
参考:Vue3生命周期详解
Vue 的生命周期之间到底做了什么事清?(源码详解,带你从头梳理组件化流程)
5. v-if和v-for的区别
- v-if是会被编译成三元表达式,条件不满足组件不会渲染
v-show会被编译成指令,条件不满足元素的display:none
- v-if条件转变元素会在真实DOM中插入和移除,适合不需要频繁切换的场景
v-show用
display控制元素显隐,适合频繁切换的场景6. 如何理解vue单向数据流
所有的props形成的父子props单行向下流动;父组件props的更新会向下流动到子组件中,但是子组件不行,这样防止子组件意外更新父级组件状态,导致数据流向的困惑。
在vue中子组件修改props会收到警告;子组件一般通过事件响应的机制和父组件沟通。7. watch和computed的区别
computed是计算属性,依赖其他响应式数据更新值;computed是有缓存的;只有当其他响应式数据发生变化的时候才会更新。
watch监听到响应式数据的值发生变化就会去执行对应的回调。
- computed适合用于渲染模板中;watch适合在数据发生改变时执行相应的业务逻辑
8. v-for和v-if优先级问题
vue2中v-for优先于v-if执行,会先循环渲染出节点后再用v-if判断,造成性能浪费;可以使用computed优化
vue3中v-if的优先级高于v-for;所以v-if不能访问v-for作用域中的值,会报错(访问不到todo);可以用template包裹 ```javascript - {{ todo.name }}
// 代替方案
<a name="NlDhr"></a>#### 9. Vue2响应式原理整理思路:数据劫持+观察者模式1. Observer:使用`defineProperty`对对象属性递归劫持get,set。用于收集依赖和派发更新1. dep:用于收集当前响应式对象的依赖关系,每个对象包含子对象都有一个dep实例(`dep.subs`是watcher实例数组)。当数据变化时触发set;通过`dep.notify()`通知各个watcher更新。这是**发布订阅模式**1. watcher:观察的对象,分为渲染watcher,计算属性watcher和侦听器watcher三种;组件会在渲染的过程中创建相应的Watcher实例记录依赖的数据属性(收集依赖)。当依赖项改动,setter触发`dep.notify()`,触发watcher的`update`。从而使关联的组件重新渲染,(触发comptuted,watch回调)1. 重写数组原型上的7种方法(`pop, push, shift, unshift, splice, sort, reverse`),调用以上方法触发`dep.notify`达到响应式的目的。1. `defineProperty`无法观测到对象、数组的增加,删除。所以Vue新增了`set, delete`方法确保新增和删除(更新也可以使用)能触发响应式。```javascript/*** @name Vue数据双向绑定(响应式系统)的实现原理*/// observe方法遍历并包装对象属性function observe(target) {// 若target是一个对象,则遍历它if (target && typeof target === "Object") {Object.keys(target).forEach((key) => {// defineReactive方法会给目标属性装上“监听器”defineReactive(target, key, target[key]);});}}// 定义defineReactive方法function defineReactive(target, key, val) {const dep = new Dep();// 属性值也可能是object类型,这种情况下需要调用observe进行递归遍历observe(val);// 为当前属性安装监听器Object.defineProperty(target, key, {// 可枚举enumerable: true,// 不可配置configurable: false,get: function () {return val;},// 监听器函数set: function (value) {dep.notify();},});}class Dep {constructor() {this.subs = [];}addSub(sub) {this.subs.push(sub);}notify() {this.subs.forEach((sub) => {sub.update();});}}
10. Vue3响应式原理
使用Proxy代理对象,在set中track收集依赖,在get中trigger触发更新。
Vue有一个WeakMap对象targetMap用于收集依赖;key是对象target,value是Map对象depsMap;depsMap的key是对象target的属性,value是一个Set存放收集到的副作用函数。
11. Vue父子组件生命周期钩子函数执行顺序
- 加载渲染:父beforeCreate -> 父created -> 父 beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子 mounted -> 父mounted
- 子组件更新:父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated
父组件销毁:父beforeDestory/beforeUnmount -> 子beforeDestory/beforeUnmount -> 子destoryed/unmounted ->父destoryed/unmounted
12. 虚拟DOM的优缺点
优点:配合数据双向绑定无需手动操作DOM;在无需手动操纵DOM的基础上保证性能下限;跨平台
缺点:首次渲染大量DOM时虚拟DOM计算量大;无法极致优化13. v-model原理
v-model是一些标签的数据绑定和事件的语法糖。
用于基础的HTML标签
<input type="text">,<textarea></textarea>:value属性和input事件<input type"radio">,<input type="checkbox">:checked属性和change事件;radio值为value,checkbox值为checked<select>选中的值和change事件
- 用于组件就是
<Child :someProp="prop" @update:someProp="(newVal) => prop = newVal" />的语法糖<Child v-model:someProp="prop" />or<Child v-model="prop"/>
在组件内就是要触发someProp更新:this.$emit('update:someProps', "new value") or this.$emit('someProps', "new value")
14.Vue事件绑定原理
原生事件是通过addEventListener添加,自定义事件通过发布订阅模式$on $emit绑定发布
15. diff算法
1) React diff算法
- 使用key来判断新子节点列表
nextChildren和旧子节点列表prevChildren中的节点是不是同一个节点 - 使用嵌套的双循环遍历两个列表
nextChildren和prevChildren,这里就是从nextChildren中取出一个nextVNode,在prevChildren列表中寻找是否存在;能找到,则patch这个节点,记prevVNode的下标为j。 - 先说新节点在旧列表中能找到的情况,我们维护一个变量lastIndex,代表循环中新的节点
newVNode在旧的列表prevChildren中找到的旧节点prevVNode的下标的最大值;初始值是0。 - 如果
j > lastIndex,则不需要移动,更新lastIndex为j这代表了当前新节点比之前新节点在旧节点列表中更靠后 如果
j < lastIndex,则需要移动。将DOM插入到当前新节点nextChildren[i]的前一个节点nextChildren[i-1]的下一个兄弟的前面const refNode = nextChildren[i-1].el.nextSibling;container.insertBefore(preVNode.el, refNode);
如果没有找到,说明是新增的节点,应该创建新节点挂载。
最后遍历旧节点列表,找到在新节点列表中不存在的节点删除。
/*** 记录lastIndex* 遍历新的children* 如果当前节点旧的index < lastIndex 则需要移动该节点* 如果当前节点旧的index > lastIndex 则更新lastIndex* 多余的节点删除,少的节点插入到合适的位置*/let lastIndex = 0;for (const i in nextChildren) {const nextVNode = nextChildren[i];let find = false;for (const j in prevChildren) {const prevVNode = prevChildren[j];if (nextVNode.key === prevVNode.key) {find = true;patch(prevVNode, nextVNode, container);if (j < lastIndex) {/*** 移动节点* 找到新children种需要移动节点a的前一个节点* 找到它的后继节点b* 将旧children种需要移动的节点插入b之前*/const refNode = nextChildren[i - 1].el.nextSibling;container.insertBefore(prevVNode.el, refNode);break;} else {/** 更新节点 */lastIndex = j;}break;}}if (!find) {/** 多余的节点应该挂载 */const refNode = (i - 1 < 0)? prevChildren[0].el: prevChildren[i - 1].el.nextSibling;mount(nextChildren[i], container, false, refNode);}}/** 移除已经不存在的节点 */for (const i in prevChildren) {const prevChild = prevChildren[i];const has = nextChildren.find((nextChild) =>nextChild.key === prevChild.key);if (!has) {container.removeChild(prevChild.el);}}
2) Vue2双端比较
Vue2采用双端比较:
获取新旧节点列表开头和结尾的下标,对应的VNode。
oldStartIdx,oldEndIdx,newStartIdx,newEndIdx- 循环比对,直到start下标超过end。四个节点互相对比,
1. [oldStart, newStart],2. [oldStart, newEnd],3. [oldEnd, newStart],4. [oldEnd, newEnd]。如果是start节点匹配到则++后移,end节点匹配到则迁移 - 如果是1和4这两种情况相匹配,不需要移动,直接patch;并将新旧节点的下标后移一位(1),或前移一位(4)
如果是
2. [oldStart, newEnd]匹配,首先patch,然后将oldStartVnode移动到oldEndVnode后面;也就是移到当前列表的最后一位;最后更新下标和对应节点patch(oldStartVnode, newEndVnode, container);container.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
如果是
3. [oldEnd, newStart],匹配,首先patch,然后将oldEndVnode移动到newEnd前面;也就是移动到当前列表第一位;最后更新下标和对应节点patch(oldEndVnode, newStartVnode, container);container.insertBefore(oldendVnode.el, oldStartVnode.el);
上面四种情况都没有成功就是非理想情况;此时在旧列表中找到与
newStartVnode相同的节点,patch,移动到当前旧列表的最前面;如果没有找到,说明是新节点需要创建挂载;最后更新newStartVnodeconst i = prevChildren.findIndex((node) => node.key === newStartVnode.key)/** 找到了就patch,移动到当前列表最前端 */patch(prevChildren[i], newStartVnode, container);container.insertBefore(prevChildren[i].el, newStartVnode.el);/** 将移动好的节点删除 */prevChildren[i] = undefined;/** 更新节点 */newStartVnode = nextChildren(++newStartIdx)
最后添加新节点,如果
oldIdx先越界,创建挂载新节点- 删除移除的节点,如果
newIdx先越界,移除不存在的节点 ```javascript /**- 双端比较
- */ let oldStartIdx = 0; let oldEndIdx = prevChildren.length - 1; let newStartIdx = 0; let newEndIdx = nextChildren.length - 1;
let oldStartVNode = prevChildren[oldStartIdx]; let oldEndVNode = prevChildren[oldEndIdx]; let newStartVNode = nextChildren[newStartIdx]; let newEndVNode = nextChildren[newEndIdx];
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (!oldStartVNode) { /**
* 0.1 旧的第一个元素是undefined*/oldStartVNode = prevChildren[++oldStartIdx];} else if (!oldEndVNode) {/*** 0.2 酒得最后一个元素是undefined*/oldEndVNode = prevChildren[--oldEndIdx];} else if (oldStartVNode.key === newStartVNode.key) {/*** 1. 旧的第一个元素和新的第一个元素key相同*/patch(oldStartVNode, newStartVNode, container);oldStartVNode = prevChildren[++oldStartIdx];newStartVNode = nextChildren[++newStartIdx];} else if (oldEndVNode.key === newEndVNode.key) {/*** 2. 旧的最后一个元素和新的最后一个元素key相同*/patch(oldEndVNode, newEndVNode, container);oldEndVNode = prevChildren[--oldEndIdx];newEndVNode = nextChildren[--newEndIdx];} else if (oldStartVNode.key === newEndVNode.key) {/*** 3. 旧的第一个元素和新的最后一个元素key相同*/patch(oldStartVNode, newEndVNode, container);container.insertBefore(oldStartVNode.el, oldEndVNode.el.nextSibling);oldStartVNode = prevChildren[++oldStartIdx];newEndVNode = nextChildren[--newEndIdx];} else if (oldEndVNode.key === newStartVNode.key) {/*** 4. 旧的最后一个元素和新的第一个元素key相同*/patch(oldEndVNode, newStartVNode, container);container.insertBefore(oldEndVNode.el, oldStartVNode.el);oldEndVNode = prevChildren[--oldEndIdx];newStartVNode = nextChildren[++newStartIdx];} else {/*** 5. 非理想情况,旧的某一个中间元素和新的第一个元素key相同*/const idxInOld = prevChildren.findIndex((node) => node.key === newStartVNode);if (idxInOld >= 0) {/** 将找到的节点移动到oldStartVNode前面,并将prevChildren[idxInOld]设置为undefined */const vnodeToMove = prevChildren[idxInOld];patch(vnodeToMove, newStartVNode, container);container.insertBefore(vnodeToMove.el, oldStartVNode.el);prevChildren[idxInOld] = undefined;} else {/*** 全新节点 需要挂载*/mount(newStartVNode, container, false, oldStartVNode.el);}newStartVNode = nextChildren[++newStartIdx];}
} if (oldEndIdx < oldStartIdx) { / 添加多余的节点 */ for (let i = newStartIdx; i <= newEndIdx; i++) { mount(nextChildren[i], container, false, oldStartVNode.el); } } else if (newEndIdx < newStartIdx) { / 移除多余的节点 */ for (let i = oldStartIdx; i <= oldEndIdx; i++) { container.removeChild(prevChildren[i].el); } }
<a name="XJkz5"></a>##### 3) Vue3的优化1. 事件缓存:元素已经添加的事件会被缓存,复用1. 静态标记与复用:不会变的静态元素会在创建时被标记静态节点。patch的时候跳过,渲染的时候复用1. 头和头,尾和尾比较,这里相同的元素直接patch;未比较的元素,按未比较的新`nextChildren`的长度创建一个source数组,初始值为-1;从`prevChildren`中找出他们的下标放在source数组中。<br />遍历完后寻找最长递增子序列,这里的节点不需要移动,移动其他节点。数组中-1代表该位置的元素是新增的,<a name="dJDbi"></a>#### ath16. 路由守卫路由钩子的执行顺序:<br />全局`beforeEach`<br />router中定义的`beforeEnter`<br />重用的组件里调用`beforeRouteUpdate`<br />组件中定义的`beforeRouteEnter`<br />全局组件的`beforeResolve`<br />全局的`afterEach`<br />DOM更新<br />`beforeRouteEnter`中`next`的回调<a name="RvRuj"></a>#### 17. Vue scoped css原理在元素和css选择器上添加唯一attribute`data-v-hash`<a name="Z91ds"></a>#### 18. 动态路由把path匹配到的路由映射到同一个组件上,使用`:`表示需要被匹配的字段```javascriptpath: '/user/:id'this.$route.params ===> {id: 'jay'} // 组件中
动态路由的组件被复用的时候,如从/user/jay导航到/user/wang,组件被复用导致组件复用,生命周期钩子不会触发,可以使用watch监听路由或给路由加key
watch: {'$route.params.id': function(){}}<router-view :key="$route.fullPath"></router-view>
19. 为什么$nextTick要用微任务队列?
防止页面频繁更新
