vue整理
1.VUE原理理解
MVVM开发模式
MVVM即 Model-View-ViewModel 模式,分为Model、View、ViewModel三者。
Model 代表数据模型,数据和业务逻辑都在Model层中定义,通过 Ajax/fetch 等 API 完成客户端和服务端业务 Model 的同 步;
View 代表UI视图,在 MVVM ⾥,整个 View 是⼀个动态模板,负责数据的展示;
ViewModel 负责监听 Model 中数据的改变并且控制视图View的更新,处理用户交互操作;
Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。因此当 >Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。优点:
1.这种模式实现了 Model 和 View 的数据自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要自己操作 dom。
2.分离了Model 和 View ,使代码更加清晰。缺点:
1.对于⼤型的图形应⽤程序,视图状态较多, ViewModel 的构建和维护的成本都会⽐较⾼。
2. 数据绑定的声明是指令式地写在View 的模版当中的,这些内容没办法去打断点 debug。
vue 响应式原理(数据双向绑定)
vue的响应式也叫作数据双向绑定,通过观察者模式将数据劫持和模板编译结合,当数据被修改时,视图会进行更新;
原理:
- 数据劫持observer: 封装 Object.defineProperty 方法用来劫持对象属性的getter和setter,以此来追踪数据变化;
- 收集依赖:当外界通过Watcher读取数据时,便会触发getter从而将Watcher收集到Dep中。依赖收集的目的是将观察者 Watcher 对象存放到当前数据属性劫持(Object.defineProperty)闭包中的 Dep 的 subs 中,当数据发生变化时,会循环subs依赖列表,把所有的Watcher都通知一遍;
- 观察者 Watcher:当属性发生变化后,触发set(), 通过循环dep的subs依赖列表通知Watcher自己的值改变了,需要重新渲染视图;
- Dep 类为依赖找一个存储依赖的地方,用来收集和管理依赖,在getter中收集,在setter中通知。
- Observer 类用来将一个对象的所有属性和子属性都变成响应式的,通过递归调用defineReactive来实现;
- 修改数据时触发setter,并遍历依赖列表,通知所有相关依赖(Watcher)
在vue中实现方式:
- 在newVue()后,Vue会调用_init函数进行初始化,在这时data通过Observer转化成了getter/setter的形式,来对数据进行追踪;
- 当 render(组件模板渲染)的时候,会读取被模板引用的属性的值,然后触发属性的getter函数,将Watcher收集到Dep依赖列表中;
- 在修改对象的值的时候,会触发对应的 setter, setter通知之前依赖收集得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher就会开始调用 update 来更新与它关联的组件视图。
观察者模式与订阅发布模式的区别
1.观察者模式
观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯。
class Dep {constructor() {// 用一个数组来存储自己的订阅者,subs 是英语 subscribes 订阅者的意思this.subs = [];}// 纪录依赖 watcheraddSub(sub) {this.subs.push(sub);}// 通知更新notify(){let subs = [...this.subs]; // 浅拷贝一份subs// 遍历for(let i=0,l=subs.length;i<l;i++) {subs[i].update(); // 这里调用了 Watcher 实例的update方法,通知每一个订阅了 Dep 所在数据的 Watcher}}
}
let dep = new Dep();
class Watcher {constructor(expression, cb) {this.expression = expression;this.cb = cb;}// 当收到通知时执行update,从而调用cbupdate() {this.cb();}
}
let watcher1 = new Watcher('phone', function() {console.log('watcher1')
});
let watcher2 = new Watcher('phone', function() {console.log('watcher2')
});
dep.addSub(watcher1);
dep.addSub(watcher2);
dep.notify();
- 订阅-发布模式
它是观察者模式的一个别称
发布订阅模式中,称为发布者的消息发送者不会将消息直接发送给订阅者,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个角色,称为调度中心或事件通道,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者。
class Dep {constructor() {// 用一个数组来存储自己的订阅者,subs 是英语 subscribes 订阅者的意思this.subs = [];}// 添加订阅者subscribe(evt, cb) {if(this.subs[evt]) {this.subs[evt].push(cb);}else {this.subs[evt] = [cb];}}// 发布更新publish(evt,...age){let list = this.subs[evt] || [];// 遍历for(let i=0,l=list.length;i<l;i++) {list[i](...age); // 通知每一个订阅者的回调}}
}let dep = new Dep();dep.subscribe('phone', function() {console.log('watcher1',arguments);
});
dep.subscribe('phone', function() {console.log('watcher2')
});
dep.publish('phone', '666',999);
3.区别:
发布订阅模式相比观察者模式多了个事件通道,事件通道作为调度中心,管理事件的订阅和发布工作,彻底隔绝了订阅者和发布者的依赖关系。即订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。
观察者模式有两个重要的角色,即目标和观察者。在目标和观察者之间是没有事件通道的。一方面,观察者要想订阅目标事件,由于没有事件通道,因此必须将自己添加到目标(Subject) 中进行管理;另一方面,目标在触发事件的时候,也无法将通知操作(notify) 委托给事件通道,因此只能亲自去通知所有的观察者。
两种模式在vue中的应用场景
发布/订阅模式:子组件与父组件的通信方式、兄弟组件通信
观察者模式:数据实时更新视图
vue2和vue3的响应式原理的区别
vue2 用的是 Object.defindProperty ,vue3用的是Proxy
Object.defindProperty 缺点:
一次只能对一个属性进行监听,需要递归遍历来监听所有属性
Proxy就没有这个问题,可以监听整个对象的数据变化,所以vue3.0用Proxy代替definedProperty
Proxy
let proxy=new Proxy(obj,handle);
其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为
Proxy的作用:在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
作为构造函数,Proxy接受两个参数:
第一个参数是所要代理的目标对象(上例是一个空对象),即假如没有Proxy的介入,操作原来要访问的就是这个对象;
第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作;
对比Object.defindProperty() 的get()和set(newValue),Proxy的get、set 多了三个参数 obj, propKey, receiver,set修改时的newValue在第三位入参=》set(obj, propKey, newValue,receiver){};
target:目标对象
propKey:属性名
receiver(可选):proxy实例
2.vue生命周期与8个钩子函数
生命周期就是组件或者实例,从创建到被销毁(初始化数据、编译模板、挂载DOM、渲染一更新一渲染、销毁/卸载)的过程。
生命周期四个阶段
第一阶段(创建阶段):beforeCreate,created
第二阶段(挂载阶段):beforeMount(render),mounted
第三阶段(更新阶段):beforeUpdate,updated
第四阶段(销毁阶段):beforeDestroy,destroyed
钩子函数
1.beforeCreate
在实例初始化之后,数据初始化之前,进行数据侦听和事件/侦听器的配置之前同步调用;
ps:在这个阶段,数据是获取不到的,dom元素也未渲染;
2.created
在实例创建完成,数据初始化完成之后被立即同步调用。在这一步中,实例已完成对选项的处理,意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数,然而,挂载阶段还没开始,且 $el property 目前尚不可用。;
ps:在这个阶段,可以访问到数据了,但是页面当中真实dom节点还是没有渲染出来,在这个钩子函数里面,可以进行相关初始化事件的绑定、发送请求操作;
3.beforeMount
在实例挂载开始之前被调用:相关的 render 函数首次被调用;
ps:这代表dom马上就要被渲染出来了,但是却还没有真正的渲染出来,这个钩子函数与created钩子函数用法基本一致,可以进行相关初始化事件的绑定、发送ajax操作;
4.mounted
实例被挂载后调用,这时 el 被新创建的
vm.$el
替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时vm.$el
也在文档内;
ps:注意 mounted 不会保证所有的子组件也都被挂载完成。如果你希望等到整个视图都渲染完毕再执行某些操作,可以在 mounted 内部使用 vm.$nextTick;
挂载阶段的最后一个钩子函数,数据挂载完毕,真实dom元素也已经渲染完成了,这个钩子函数内部可以做一些实例化相关的操作;
5.beforeUpdate
在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器;
ps:这个钩子函数初始化的时候不会执行,当组件挂载完毕的时候,并且当数据改变的时候,才会立马执行,这个钩子函数获取dom的内容是更新之前的内容;
6.updated
在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或 watcher 取而代之;
ps:这个钩子函数获取dom的内容是更新之后的内容生成新的虚拟dom,新的虚拟dom与之前的虚拟dom进行比对,差异之后,就会进行真实dom渲染。在updated钩子函数里面就可以获取到因diff算法比较差异得出来的真实dom渲染了;
7.beforeDestroy
实例销毁之前调用。在这一步,实例仍然完全可用;
ps:当组件销毁的时候,就会触发这个钩子函数。代表销毁之前,可以做一些善后操作,可以清除一些初始化事件、定时器相关的东西;
8.destroyed
实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁;
ps:Vue实例失去活性,完全丧失功能;
9.activited
被 keep-alive 缓存的组件激活时调用;
10.deactivated
被 keep-alive 缓存的组件失活时调用;
3. vue常用实例方法
vm.$watch(expOrfn, callback(newData, oldData), [options]):
观察 Vue 实例的一个表达式或计算函数。回调的参数为新值和旧值。表达式可以是某个键路径或任意合法绑定表达式。
ps: 通常情况下,我们会使用 vue 实例内的 watch 属性来统一实现监听,因为在vue实例化时, watch 属性会为每个键调用 $watch()
vm.$emit(event, […args]):
触发当前实例上的事件。附加参数都会传给监听器回调。常用于父子组件之间通信;
vm.$nextTick(callback):
将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后,等待 DOM 更新完成后立即使用它。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上;
4. vue实例属性
$data:
组件实例正在侦听的数据对象。组件实例代理了对其 data 对象 property 的访问;
$props:
当前组件接收到的 props 对象。组件实例代理了对其 props 对象 property 的访问;
$parent:
父实例,如果当前实例有的话;
5. vue实例内置组件
component:
渲染一个“元组件”为动态组件。依 is 的值,来决定哪个组件被渲染。is 的值是一个字符串,它既可以是 HTML 标签名称也可以是组件名称;
<!-- 动态组件由 vm 实例的 `componentId` 属性控制 --> <component :is="componentId"></component> <!-- 可以通过字符串引用组件 --> <component :is="condition ? 'FooComponent' : 'BarComponent'"></component> <!-- 可以用来渲染原生 HTML 元素 --> <component :is="href ? 'a' : 'span'"></component>
keep-alive:
主要用于保留组件状态或避免重新渲染。
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
当组件在 内被切换时,他有两个专属钩子函数 activated(激活时触发) 和 deactivated(移除时触发)。(这会运用在 的直接子节点及其所有子孙节点。)
提供 include 和 exclude 属性,两者都支持逗号分隔字符串、正则表达式、数组来表示, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
ps: 要求同时只有一个子元素被渲染;
solt插槽:
插槽,也就是slot,slot就是子组件里的一个占位符,一个slot的核心问题,就是显不显示,显示的话显示话,该如何去展示出来,这是由父组件所控制的,但是插槽显示的位置是由子组件自己所决定的,slot写在组件template的什么位置,父组件传过来的模板将会显示在什么位置;
匿名插槽:
这是一个子组件,我们使用了默认插槽(匿名插槽),父组件的内容将会代替显示出来
<template><div>父组件分发的内容: <slot></slot></div>
</template>
<script>
export default {name: 'templateChildren'
}
</script>
父组件,注册了子组件,使用它并且填充了内容
<template><div><templateChildren>这是将要替换子组件solt插槽的内容</templateChildren></div>
</template>
<script>
import templateChildren from './templateChildren'
export default {components: 'templateChildren'
}
</script>
渲染后结果
<template><div>父组件分发的内容: 这是将要替换子组件solt插槽的内容</div>
</template>
具名插槽:
有时我们一个组件里面需要多个插槽。我们怎么来区分多个slot,而且不同slot的显示位置也是有差异的.对于这样的情况, solt元素有一个特殊的特性:name。这个特性可以用来定义多个插槽,设置了name的slot插槽也叫具名插槽,在向具名插槽提供内容的时候,我们可以在一个 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
ps: 一个匿名插槽会带有一个隐含的名字"default"
在向具名插槽提供内容的时候,我们可以在一个 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称
ps: v-slot 只能添加在一个 上 (只有一种例外情况)
例外情况: 当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上;
子组件children需要多个插槽:
// children.vue
<div><slot name="one"></slot><slot></slot><slot name="two"></slot>
</div>
父组件模板:
<children><template v-slot:one>One</template><template v-slot:two>Two</template><p>Default A</p>
</children>// 2.6.0 以后废弃的写法,可以直接用于非template元素
<children><p slot="one">One</p><p slot="two">Two</p><p>Default A</p>
</children>
渲染后结果
<div><p>One</p><p>Default A</p><p>Two</p>
</div>
作用域插槽:
有时候,父组件填充到子组件插槽的内容中有需要访问到子组件里面的内容,类似子组件里的slot可以绑定一些当前作用域,从而传出来,使用组件时,插槽的内容就可以拿到slot传出来的数据,父级的插槽内容可用。
子组件绑定在 slot 元素上的特性被称为插槽 prop;
//children.vue
<div><slot name="slot1" :name="name" :age="age"></slot><slot></slot>
</div>
data() {return {name: '张三',age: 18}
}
在父级作用域中,我们可以给 v-slot 带一个值来定义我们提供的插槽 prop 的名字,这是一个对象形式的数据,可以通过props.name 或者 props[key] 拿到子组件传出来的数据:
<children><template v-slot:slot1="props">子组件插槽的名字叫{{ props.name}},今年{{ props.age }}岁</template>
</children>// 2.6.0以后废弃的写法,可以直接用于非template元素
<children><p slot="slot1" slot-scope=="props">子组件插槽的名字叫{{ props.name}},今年{{ props.age }}岁</p>
</children>
渲染结果:
<div>子组件插槽的名字叫张三,今年18岁
</div>
6.v-if 和 v-show 的区别
v-if 是真正的条件渲染,会控制这个 DOM 节点的存在与否。因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块;
v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的 “display” 属性进行切换;
使用场景:当我们需要经常切换某个元素的显示/隐藏时,使用v-show会更加节省性能上的开销;当只需要一次显示或隐藏时,使用v-if更加合理;
7. vuex
每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)
特点:
1.Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新;
2. 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化;
vuex核心概念5个模块
- state:vuex使用单一状态树,一个state对象就包含了全部的应用层级状态,可以在这里设置默认的初始状态,通过this.$store.state获取状态;
vuex辅助函数: mapState,帮助我们生成计算属性,mapState 函数返回的是一个对象。我们如何将它与局部计算属性混合使用呢?通常,我们需要使用一个工具函数将多个对象合并为一个,以使我们可以将最终对象传给 computed 属性。但是自从有了对象展开运算符,我们可以极大地简化写法:
import { mapState } from 'vuex'
computed: {localComputed () { /* ... */ },// 使用对象展开运算符将此对象混入到外部对象中// 当映射的计算属性的名称与 state 的子节点名称相同时,我们也可以给 mapState 传一个字符串数组...mapState(['count','total'])
}
- getter: 从基本数据派生的数据,Vuex 允许我们在 store 中定义“getter”(可以认为是 store 的计算属性),Getter 接受 state 作为其第一个参数,getter可以返回一个函数,为数据做筛选,通过this.$store.getters获取状态;
vuex辅助函数:mapGetters, 将 store 中的 getter 映射到局部计算属性:
import { mapGetters } from 'vuex'export default {// ...computed: {// 使用对象展开运算符将 getter 混入 computed 对象中...mapGetters(['doneTodosCount','anotherGetter',// ...])}
}
- mutation: 是唯一更改 store 中状态的方法,且必须是同步函数;Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串的事件类型 (type)和一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且它会接受 state 作为第一个参数:
mutations: {increment (state) {state.count+1} }
要唤醒一个 mutation 处理函数,你需要以相应的 type 调用 store.commit 方法:
store.commit('increment')
你可以向 store.commit 传入额外的参数:store.commit('increment', 666);
在组件中可以使用this.$store.commit('xxx')
提交 mutation,或者使用 mapMutations 辅助函数将组件中的 methods 映射为 store.commit 调用(需要在根节点注入 store)
- Action: 类似于 mutation,不同在于Action 提交的是 mutation,而不是直接变更状态;Action 可以包含任意异步操作。
Action 函数接受一个与 store 实例具有相同方法和属性的 context 对象,因此你可以调用 context.commit 提交一个 mutation,或者通过 context.state 和 context.getters 来获取 state 和 getters;actions: {increment ({ commit }) { // 实践中,我们会经常用到 ES2015 的参数解构来简化代码commit('increment')} }
Action 通过 store.dispatch 方法触发:`store.dispatch(‘increment’)
在组件中使用 this.$store.dispatch(‘xxx’) 分发 action,或者使用 mapActions 辅助函数将组件的 methods 映射为 store.dispatch 调用(需要先在根节点注入 store)
- Module: 模块化Vuex,允许将单一的 Store 拆分为多个 store 且同时保存在单一的状态树中
8. vue的单向数据流
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。
这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
子组件想修改props数据时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改;
每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。
.sync修饰符
.sync 是 Vue 中用于实现简单的“双向绑定”的语法糖。简单来说,它的功能就是:当一个子组件通过提交
$emit('update:title', newTitle)
事件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定;
父组件可以监听这个事件并根据需要更新一个本地的数据 property:
<text-documentv-bind:title="title"v-on:update:title="title = $event"
></text-document>
为了方便起见,vue为这种模式提供一个缩写,即 .sync 修饰符:
<text-document v-bind:title.sync="title"></text-document>
9. computed 和 watch 的区别和应用场景
- computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
- watch:监听属性,更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
应用场景:
- 当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算;
- 当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的
10. Vue 的父组件和子组件生命周期钩子函数执行顺序
父组件挂载完成之前,子组件必须先完成挂载到父组件上,父组件完整了才能够挂载到根组件
Vue 的父组件和子组件生命周期钩子函数执行顺序:
- 加载渲染过程 :
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted- 组件更新过程 :
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated- 销毁过程 :
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
11. 组件中 data 为什么是一个函数,而new Vue 中的data可以是一个对象
- 原因是为了保证组件的独立性和可复用性,因为如果data是一个对象的话,复用组件中设置的data都会引用同一个内存地址,而用函数的话,则会在每次引用的时候返回一个新的地址;
- 一个组件被复用多次的话,也就会创建多个实例。本质上,这些实例用的都是同一个构造函数。如果data是对象的话,对象属于引用类型,会影响到所有的实例。所以为了保证组件不同的实例之间data不冲突,data必须是一个函数;而 new Vue 的实例,是不会被复用的,因此不存在引用对象的问题;
12. Vue 组件间通信有哪几种方式
Vue 组件间通信主要指3 类通信:父子组件通信、隔代组件通信、兄弟组件通信:
props / $emit
, 官方推荐使用,适用于父子组件通信
props
:父组件传入子组件的数据;
$emit
: 触发当前实例上的事件。附加参数都会传给监听器回调;
ref
与$parent / $children
,适用于父子组件通信:
ref
:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例;
$parent / $children
: 访问父/子组件实例;
EventBus ($emit / $on)
适用于 父子、隔代、兄弟组件通信通过一个空的 Vue 实例作为事件中心,用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件;
EventBus 实现:
// main.js
Vue.prototype.$bus = new Vue(); // 在main.js文件中定义一个新的bus对象并且挂载在Vue原型链上// a.vue
created(){this.$bus.$on('brotherEvent', res=>{console.log(res) // 得到666})
}// b.vue
methods:{bAction(){this.$bus.$emit('brotherEvent', '666')}
}
$attrs/$listeners
适用于 隔代组件通信
$attrs
:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过v-bind="$attrs"
传入内部组件。通常配合 inheritAttrs 选项一起使用。
$listeners
:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过v-on="$listeners"
传入内部组件
provide / inject
适用于 隔代组件通信
provide/inject
需要一起使用,我们可以从父组件的provide传值,子组件或者孙组件,就可以用inject来接受父组件的provide属性值:
代码实现:
// 父组件
provide(){return{count:this.count, // 这种传递方式不是响应式的,如果想要响应式那provide传递的值就得是个响应式对象,例如:value: this.value}},// 和data,method等属性平级,
data(){return{count:1,value: {name: 'name'}}
}// 子孙组件接收
inject: ['count','value']
mounted(){conslole.log(this.count,this.value.name)
}
vuex 适用于 父子、隔代、兄弟组件通信
上面找 7.vuex
13. vue-router
路由分为前端路由和后端路由;
后端路由:
URL的请求地址与服务器上的资源对应,根据不同的请求地址返回不同的资源;
前端路由:
在单页面应用中,根据用户触发的事件,改变URL在不刷新页面的前提下,改变显示内容;
通过 Vue.js,我们已经用组件组成了我们的应用。当加入 Vue Router 时,我们需要做的就是将我们的组件映射到路由上,让 Vue Router 知道在哪里渲染它们;
<script src="https://unpkg.com/vue@3"></script>
<script src="https://unpkg.com/vue-router@4"></script><div id="app"><h1>Hello App!</h1><p><!--使用 router-link 组件进行导航 --><!--通过传递 `to` 来指定链接 --><!--`<router-link>` 将呈现一个带有正确 `href` 属性的 `<a>` 标签--><router-link to="/">Go to Home</router-link><router-link to="/about">Go to About</router-link></p><!-- 路由出口 --><!-- 路由匹配到的组件将渲染在这里 --><router-view></router-view>
</div>
router-link
:自定义组件, vue-router 使用它来创建链接,这使得 Vue Router 可以在不重新加载页面的情况下更改 URL,处理 URL 的生成以及编码;
router-view
:
router-view
将显示与 url 对应的组件。你可以把它放在任何地方,以适应你的布局;
通过调用 app.use(router),我们会触发第一次导航且可以在任意组件中以this.$router
的形式访问它,并且以this.$route
的形式访问当前路由
在 Vue Router 中,我们可以在路径中使用一个动态字段来实现将给定匹配模式的路由映射到同一个组件, 路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以this.$route.params
的形式暴露出来
在组件中我们经常使用this.$route.params
从 path 中提取已解码路径参数;this.$route.query
获取URL 中存在参数;
router.push()
:除了使用
<router-link>
创建 a 标签来定义导航链接,我们还可以借助 router 的实例方法,通过编写代码来实现,实际上点击<router-link>
创建的a标签时,内部会调用router.push()
;
const username = 'eduardo'
// 带有路径的对象
router.push({ path: '/users/eduardo' })
// 命名的路由,并加上参数,让路由建立 url
router.push({ name: 'user', params: { username: 'eduardo' } })
// 带查询参数,结果是 /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })
// 带 hash,结果是 /about#team
router.push({ path: '/about', hash: '#team' })// 我们可以手动建立 url,但我们必须自己处理编码
router.push(`/user/${username}`) // -> /user/eduardo
// 同样
router.push({ path: `/user/${username}` }) // -> /user/eduardo
// 如果可能的话,使用 `name` 和 `params` 从自动 URL 编码中获益
router.push({ name: 'user', params: { username } }) // -> /user/eduardo
// `params` 不能与 `path` 一起使用
router.push({ path: '/user', params: { username } }) // -> /user
router.replace
:
替换当前位置,它的作用类似于 router.push,唯一不同的是,它在导航时不会向 history 添加新记录,正如它的名字所暗示的那样——它取代了当前的条目,也可以直接在传递给 router.push 的 routeLocation 中增加一个属性 replace: true;
router.push({ path: '/home', replace: true })
// 相当于
router.replace({ path: '/home' })
vueRouter有三种路由模式:hash、history、adstract, 我们常用hash模式
- hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;
- history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;
- abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.
hash 和 history 路由模式实现原理
- hash 模式:
早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容,我们通过修改 # 后面的内容,再使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)特性:
1.URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;
2.hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换;
3.可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
- history 模式:
HTML5 提供了 History API 来实现 URL 的变化。其中最主要的 API 有以下两个:history.pushState()
和history.repalceState()
。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录;特性:
1.可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
2.history.pushState()
或history.replaceState()
这两个api不会触发 popstate 事件,这时我们需要手动触发页面跳转或渲染。这相比于hash模式稍显繁琐;
全局路由前置守卫
vue-router 提供的路由导航守卫主要用来通过跳转或取消的方式守卫导航
// 你可以使用 router.beforeEach 注册一个全局前置守卫:
const router = createRouter({ ... })router.beforeEach((to, from) => {// ...// 返回 false 以取消导航return false
})
当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中;
每个守卫方法接收三个参数:
- to: 即将要进入的目标
- from: 当前导航正要离开的路由
- next(可选): 导航到一个新的路由(next({ name: ‘Login’ }))或进行下一步 ( next() );
// next router.beforeEach((to, from, next) => {if (to.name !== 'Login' && !isAuthenticated) {next({ name: 'Login' })} else { next() } })
>
> 可以返回的值如下:
> false: 取消当前的导航。如果浏览器的 URL 改变了(可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址;
> 一个新的路由地址: 通过一个路由地址跳转到一个不同的地址,就像你调用 router.push() 一样,你可以设置诸如 replace: true 或 name: 'home' 之类的配置。当前的导航被中断,然后进行一个新的导航,就和 from 一样。```javascriptrouter.beforeEach(async (to, from) => {if (// 检查用户是否已登录!isAuthenticated &&// ❗️ 避免无限重定向to.name !== 'Login') {// 将用户重定向到登录页面return { name: 'Login' }}})
路由独享的守卫
你可以直接在路由配置上定义 beforeEnter 守卫:
const routes = [{path: '/users/:id',component: UserDetails,beforeEnter: (to, from) => {// reject the navigationreturn false},},
]
beforeEnter 守卫 只在进入路由时触发,不会在 params、query 或 hash 改变时触发
14. vm.$set()实现原理
在vue2.x版本中受现代 JavaScript 的限制 ,Vue 无法检测到对象属性的添加或删除。
由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
Vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value)来实现为对象添加响应式属性,实现方式:
export function set (target: Array<any> | Object, key: any, val: any): any {// target 为数组if (Array.isArray(target) && isValidArrayIndex(key)) {// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误target.length = Math.max(target.length, key)// 利用数组的splice变异方法触发响应式target.splice(key, 1, val)return val}// key 已经存在,直接修改属性值if (key in target && !(key in Object.prototype)) {target[key] = valreturn val}const ob = (target: any).__ob__// target 本身就不是响应式数据, 直接赋值if (!ob) {target[key] = valreturn val}// 对属性进行响应式处理defineReactive(ob.value, key, val)ob.dep.notify()return val
}
vm.$set() 总结:
- 如果目标是数组,直接使用数组的 splice 方法触发相应式;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理( defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法;
15.虚拟 DOM
虚拟 DOM 是什么?
虚拟 DOM 本质是一个js对象,它于初次渲染dom时保存,它有三个关键属性:
let obj = {target: 'ul',attr: 'class',children: [{target: 'ul',attr: 'class',children: [...]},...]
}
target:需要创建的dom标签,如 div,必须得有;
attr:dom标签属性,如class;
children: 子级元素;
用来做什么?
document.createElement
用来创建dom节点,通过新旧虚拟dom对比,局部更新dom;
如何提升vue的渲染效率的?
在更新dom时,获取一个新的虚拟dom,通过新旧虚拟dom对比,局部更新dom;
将直接操作dom(影响性能)=> js对象的比较(只有执行效率上的问题 )
虚拟dom实现原理
1.用 JavaScript 对象模拟真实 DOM 树,对真实 DOM 进行抽象;
2.diff算法 — 比较真实Dom变化之前和变化之后两棵虚拟 DOM 树的差异;
3.patch算法 — 将两个虚拟 DOM 对象的差异应用到真正的 DOM 树;
我们其实可以把虚拟Dom理解成对应真实Dom的一种状态。当真实Dom发生变化后,虚拟Dom可以为我们提供这个真实Dom变化之前和变化之后的状态,我们通过对比这两个状态,即可得出真实Dom真正需要更新的部分,即可实现最小量更新。在一些比较复杂的Dom变化场景中,通过对比虚拟Dom后更新真实Dom会比直接更新真实Dom的效率高,这也就是虚拟Dom和diff算法存在的意义
diff算法:
把树形结构按照层级分解,只比较同级元素,不同层级的节点只有创建和删除操作;
patch算法:
将新老虚拟dom节点进行比对,然后将根据两者的比较结果进行最小单位地修改视图,而不是将整个视图根据新的VNode重绘;
16. vue项目优化
1.v-if与v-show区分使用场景;
2.computed 和 watch 区分使用场景;
3.避免v-for与v-if同时使用;
4.第三方插件按需引入;
5.路由懒加载;
6.图片资源懒加载;
7.按需加载ui组件库;
8.删除生产环境用不到的依赖;
9.删除项目中的console.*;
10.处理打包后的sourcemap文件,前端用不到.map文件,可以添加配置:module.exports = {productionSourceMap: false, // 打包时不会生成 .map 文件,加快打包速度 }
11.图片压缩;
12.提取公共代码;
13.模板预编译;
14.提取组件的css
15.在离开页面时,销毁事件监听,清除定时器;
16.碰到连续事件、频繁触发的时候就要考虑用防抖、节流来限制;
17.for循环时,目的达成用break跳出循环;
17. vue模板编译原理
- 模板字符串 转换成 element AST(解析器)
- 对 AST 进行静态节点标记,主要用来做虚拟DOM的渲染优化(优化器)
- 使用 element AST 生成 render 函数代码字符串(代码生成器)
18. vue data() {retrun {}} 为什么是一个函数
Object是引用数据类型,里面保存的是内存地址,单纯的写成对象形式,就使得所有复用的组件实例共用了一份data,就会造成一个变了全都会变的结果;
因为通过函数返回的data在vue实例里面是个闭包属性;
vue利用闭包数据具有的私密性,在我们每次使用组件实例时都会重新创建一个data,它们各自维护各自内部的data,这样就不会被互相影响到;
19. this.$nextTick(callback)
用法: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM;
Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替
原理:本质是对 JavaScript 执行原理EventLoop的一种应用。EventLoop其实就是事件循环(另外写篇笔记)。nextTick 的核心是利用了如 Promise 、MutationObserver、setImmediate、setTimeout的原生 JavaScript 方法来模拟对应的微/宏任务的实现,本质是为了利用 JavaScript 的这些异步回调任务队列来实现 Vue 框架中自己的异步回调队列;
vue底层逻辑
【模板编译】将template模板,经过编译系统后生成VNode,(模板字符串→AST→Render函数)
【渲染】然后再通过渲染系统来将VNode生成真实DOM(document.createElement && Mount挂载到真实DOM节点上)
【响应式】通过响应式系统对数据进行监听,当数据发生改变时,触发依赖项(组件)
【Diff & Patch】组件内收到通知后,会通过diff算法对比VNode的变化,尽可能复用代码,找出最小差异,保证性能消耗最小。
【渲染】拿到需要新增/删除/修改的VNode后,逐一去操作真实DOM进行修改(通过选择器选择到对应真实DOM节点进行修改)