Vue全家桶的整理文案 Part1
前言
省流:前端核心分析,响应式原理,初始化过程,组件之间的七种通信方式。
情况呢,就是这么个情况:
团队内部交流学习,让我讲一下Vue全家桶。我在制作PPT之前,先整理一下文案。🏃
前端核心分析
Vue(读音 /vju/,类似于 view)是一套用于构建用户界面的渐进式框架,发布于2014年2月。
🚀 官网地址
为什么 Vue.js 会被认为比 Angular 和 React 更优秀?
1.Vue.js
轻量易学,有双向数据绑定和虚拟 DOM 等诸多特性。
2.React 处理的都是 JavaScript,使用 JavaScript 再造 HTML 和 CSS 是一个比较艰巨的任务。 Vue 的双向数据绑定比 React 更简单。
3.不同于Angular大而全的重量级框架,Vue是轻量级的。简单说,需要什么,就加什么。
正如Vue.js官网所说,Vue是一个 渐进式 的 JavaScript 框架。
关键词 渐进式
Vue的渐进式表现:
声明式渲染
Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染
进 DOM 的系统
响应式数据
页面使用了数据,当数据改变后,页面也会自动更新。
计算属性和侦听器 computed 和 watch
🚀官网地址
计算属性 官网例子
模板内的表达式非常便利,但是在模板中放入太多的逻辑,会让模板过重且难以维护。
所以,对于任何复杂逻辑,你都应当使用计算属性。
基础例子
官网例子,声明了一个计算属性 reversedMessage。
我们提供的函数将用作 property vm.reversedMessage 的 getter 函数。
计算属性缓存 computed vs 方法 methods
我们可以通过,在表达式中调用方法,来达到同样的效果。
这两种方式的最终结果确实是完全相同的。
不同的是:
计算属性是基于它们的响应式依赖进行缓存
的。
只在相关响应式依赖发生改变时它们才会重新求值。
这就意味着:只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。
这也同样意味着下面的计算属性将不再更新,因为 Date.now() 不是响应式依赖:
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
如果你不希望有缓存,请用方法来替代。
计算属性的 setter
计算属性默认只有 getter,不过在需要时你也可以提供一个 setter
侦听器 watcher
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。
watch监听数组或对象
handler
:监听数组或对象的属性时用到的方法**
deep
:深度监听,为了发现对象内部值的变化,可以在选项参数中指定 deep:true 。注意监听数组的变动不需要这么做
还有个属性 immediate
默认为 false
如果设置 immediate: true
代表在wacth里声明了这个方法之后,立即先去执行handler方法。
计算属性computed 和 watcher 区别
computed计算属性,watch监听一个值的变化。
从应用场景上来看:
computed一般是多对一,一个属性受到多个属性的影响,譬如购物车结算。
而watcher一般情况是一对多,一个属性影响到多个属性,譬如搜索框。
computed支持缓存,watch不支持缓存。
computed不支持异步,watch支持异步。
响应式原理
简单说一下,Vue2.x响应式原理
运行代码,改变vm.name的值,触发了数据挟持。
如果,是多属性的对象,通过 Object.keys(obj)
循环遍历。
看起来没啥问题,访问属性时,会触发get方法,返回data[key],但是访问data[key]也会触发get方法,一直递归调用。
Obsever (观察者)
因此,需要设置一个中转 Obsever (观察者)
,这样get中return的值,就不再直接访问 data[key]
如果,对象的属性,仍然是个对象,继续递归调用 Observer 方法。
通过 Observer 将一个普通对象,转换为响应式对象。
以上这些例子,用来说明Vue响应式的原理。Vue源码的实际操作,比这个稍微复杂亿点点。
依赖数据的观察者称为 watcher
data -> watcher
Vue 通过在 data
和 watcher
间创建一个 dep
对象,来记录这种依赖关系
data - dep -> watcher
结合Vue官方给的图,比较好理解:
- 通过
Object.defineProperty
替换配置对象属性的set,get方法,实现拦截
watcher
在执行getter
函数时,触发数据的get方法,建立依赖关系
- 写入数据时,触发
set
方法,从而借助dep发布通知
,进而watcher进行更新
dep
通知watcher
更新之后,watcher
不是立即执行,因为频繁运行,会导致效率低下。通过调度器scheduler
维护一个执行队列。- 调度器
scheduler
通一个叫做nextTick
的方法,把需要执行的watcher
放到事件循环的队列中。nextTick
的具体做法,是通过Promise
完成的。 nextTick
通过this.$nextTick
暴露给开发者,视图更新的异步操作,完成后执行这个方法。
Vue2.x 通过 Object.defineProperty
监听对象的某个属性来实现。因此,对象属性的添加或删除,无法监听。 也就是说,对象新增或者添加属性之后,视图不更新。
这也是开发过程中,会遇到的一些问题。
需要通过通过 $set
和 $delete
方法解决。
this.$set(this.obj,'sex','男')
this.$delete(this.obj,'name')
Vue3.x 通过 Proxy
(ES6语法)创建一个代理对象,直接监听整个对象。
const p = new Proxy(target, handler)
Proxy函数
入参:
target
传入对象
handler
处理方法
返回:
Proxy
不会修改传入对象,而是返回一个新的对象。
因为,Proxy代理整个对象,而不是对象的某个特定属性,因此无需遍历。
而且,新增和删除属性时,视图不更新的问题,也不存在了。
双向数据绑定
v-model
是 Vue 中最常用的指令,用于实现数据和视图的双向绑定。
v-model
是 v-bind
和 v-on
的语法糖,也就是简洁写法。
在组件中使用v-model
父组件
子组件
-
组件上的
v-model
默认会用 名为value
的prop
和名为input
的事件,在子组件通过$emit
触发一个事件。 -
radio
和checkbox
,是通过它的checked
属性和change
事件,实现双向数据绑定。 -
select
元素,是使用它的value
属性和change
事件,实现双向数据绑定。
内置指令
v-model
是使用频率最高的Vue内置指令,通过它来实现双向数据绑定。
上文提到,v-model
实际上是 v-bind
属性绑定和v-on
事件绑定的语法糖,也就是简写。
比较常见的内置指令,还有这些:
条件渲染 v-if,v-else,v-show
循环渲染 v-for
等等
自定义指令
除了核心功能默认内置的指令,Vue也允许注册自定义指令。
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus()
}
})
当页面加载时,该元素将获得焦点
<input v-focus>
防止按钮重复点击
Vue.directive('click', {
inserted(el, binding) {
el.addEventListener('click', () => {
if (!el.disabled) {
el.disabled = true;
setTimeout(() => {
el.disabled = false;
}, binding.value || 1000)
}
})
}
})
<button v-click="1500">防止重复点击的按钮</button>
自定义指令,十分灵活,还可以用他定义元素的颜色,等等。
Vue.directive("color",{
// bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置
bind:function(el,binding){
console.log(binding.name)
//有值就用,没值默认颜色
el.style.color=binding.value || "#F56C6C"
},
// inserted:被绑定元素插入父节点时调用
inserted:function(el){
console.log(el)
},
// update:所在组件的 VNode 更新时调用
update:function(el){
console.log(el)
}
})
组件之间的七种通信方式
prop 和 $emit
在组件中使用 v-model
的时候,提到了父子组件的通信方式。
prop
和 $emit
是也最常见的组件之间的通信方式。除此之外,还有几种通信方式。
provide / inject
如果子组件,下面还有子组件,用 prop
和 $emit
通信,就比较麻烦。
provide / inject
一个组件可以向它的所有子孙,传入一个依赖,不管组件层级有多少层。
这就好比:火之意志的继承。
木叶飞舞之处,火亦生生不息。
provide
inject
父组件
<template>
<div class="container">
<input v-model="message" />
<Child></Child>
</div>
</template>
<script>
// 引入组件
import ChildComponent from "./ChildComponent.vue";
export default {
// 注册组件
components: {
Child: ChildComponent,
},
provide() {
return {
grandpaMsg: () => this.message,
};
},
data() {
return {
message: "test",
};
},
methods: {},
};
</script>
子组件
<template>
<div>
子组件
<grandson-component />
</div>
</template>
<script>
import GrandsonComponent from "./GrandsonComponent.vue";
export default {
components: { GrandsonComponent },
name: "ChildComponent",
// 1. 接受父级传递的值
props: {
value: {
type: String,
default: "",
},
},
methods: {
input(event) {
this.$emit("input", event.target.value);
},
},
};
</script>
孙子组件
<template>
<div class="wrapper">
孙子组件
{{ grandpaMsg() }}
</div>
</template>
<script>
export default {
name:"GrandsonComponent",
components:{},
props:{},
inject: ['grandpaMsg'],
data(){
return {
}
},
watch:{},
computed:{},
methods:{},
created(){},
mounted(){}
}
</script>
<style scoped>
.wrapper{}
</style>
eventBus
provide / inject
虽然解决了祖孙之间的组件通信,但是兄弟组件之间通信,又该怎么办呢?
这种情况,可以使用 eventBus
它称作事件总线,通过一个空的Vue实例,作为事件中心。
通过它来触发事件和监听事件。实现了任何组件之间的通信,包括:
父子,兄弟,跨级。
实现方式
新建一个js文件
import Vue from 'vue'
export const EventBus = new Vue()
或者,直接在项目中的入口文件 main.js
初始化 EventBus
Vue.prototype.$EventBus = new Vue()
// 发送消息
EventBus.$emit(channel: string, callback(payload1,…))
// 监听接收消息
EventBus.$on(channel: string, callback(payload1,…))
示例:
<template>
<div class="wrapper">
<a-component />
<b-component />
</div>
</template>
<script>
import AComponent from './AComponent.vue'
import BComponent from './BComponent.vue'
export default {
name:"EventBus",
components:{AComponent, BComponent},
props:{},
data(){
return {
}
},
watch:{},
computed:{},
methods:{},
created(){},
mounted(){}
}
</script>
<style scoped>
.wrapper{}
</style>
A组件 发送消息
<template>
<button @click="sendMsg()">发送消息</button>
</template>
<script>
export default {
methods: {
sendMsg() {
this.$EventBus.$emit("aMsg", "来自A页面的消息");
},
},
};
</script>
B组件 接收消息
<template>
<p>接收消息: {{ msg }}</p>
</template>
<script>
export default {
data() {
return {
msg: "",
};
},
mounted() {
this.$EventBus.$on("aMsg", (msg) => {
// A发送来的消息
this.msg = msg;
});
},
};
</script>
ref / refs
如果在 DOM 元素上使用,引用的就是 DOM 元素
;
如果用在子组件上,引用的就是组件实例
。可以通过实例,直接调用组件的方法或访问数据。
<template>
<div class="wrapper">
<HaloWord ref="halo"/>
</div>
</template>
<script>
import HaloWord from "./HaloWord.vue";
export default {
name:"RefComponent",
components:{
HaloWord
},
props:{},
data(){
return {
}
},
watch:{},
computed:{},
methods:{},
created(){},
mounted(){
this.$refs.halo.message = "Hello Vue from RefComponent!"
}
}
</script>
<style scoped>
.wrapper{}
</style>
$children / $parent
子组件,或者父组件的实例,这和$refs相似。
console.log(this.$parent.msg)
$children是所有的子组件,一个数组。
console.log(this.$children);
console.log(this.$children[0].msg);
$attrs与 $listeners
$attrs
接收父级组件(可跨级)的所有绑定属性(class、style和props声明除外)
$listeners
接收除了带有 .native 事件修饰符的所有事件监听器
ps:native的修饰符(事件修饰符),使得该事件作用到内部的html标签身上
A组件
<template>
<div class="a">
<h1>A组件</h1>
<p>refreshData:{{ refreshData }}</p>
<p>x:{{ x }}</p>
<p>y:{{ y }}</p>
<p>z:{{ z }}</p>
<attrs-b :x="x" :y="y" :z="z" @refresh="handleRefresh" />
</div>
</template>
<script>
import AttrsB from "./AttrsB.vue";
export default {
components: { AttrsB },
data() {
return {
refreshData: "",
x: "xxx",
y: "yyy",
z: "zzz",
};
},
methods: {
handleRefresh(data) {
console.log("刷新", data);
this.refreshData = data;
},
},
};
</script>
B组件
<template>
<div class="b">
<h1>B组件</h1>
<p>A组件x:{{ x }}</p>
<attrs-c :x="x" v-bind="$attrs" v-on="$listeners" />
</div>
</template>
<script>
import AttrsC from "./AttrsC.vue";
export default {
components: { AttrsC },
props: ["x"],
};
</script>
C组件
<template>
<div class="c">
<h1>C组件</h1>
<p>A组件x:{{ x }}</p>
<p>A组件y:{{ y }}</p>
<button @click="handleBtn">按钮</button>
<attrs-d v-bind="$attrs" v-on="$listeners" />
</div>
</template>
<script>
import AttrsD from "./AttrsD.vue";
export default {
components: { AttrsD },
props: ["x", "y"],
methods: {
handleBtn() {
this.$emit("refresh", "CCC");
},
},
};
</script>
D组件
<template>
<div class="d">
D组件
<p>A组件z:{{ z }}</p>
<button @click="handleBtn">按钮</button>
</div>
</template>
<script>
export default {
methods: {
handleBtn() {
this.$emit('refresh', 'DDD')
}
},
props: ['z']
}
</script>
Vuex
官方介绍:
在一些大型应用中,有时我们会遇到单页面中包含着大量的组件及复杂的数据结构,
而且可能各组件还会互相影响各自的状态,在这种情况下组件树中的事件流会很快变得非常复杂,
也使调试变得异常困难。
为了解决这种情况,我们往往会引入状态管理这种设计模式,来降低这种情况下事件的复杂程度并且使调试变得可以追踪。而Vuex就是一个专门给为Vue.js设计的状态管理架构。
概述
Vuex中的数据是响应式
的,也就是说Vuex中的数据只要一变化,引用了Vuex中数据的组件都会自动更新。
核心概念
Store(仓库)、State(状态)、Mutations(变更)、Actions(动作)
安装
npm install --save vuex
modules
文件夹下的 settings.js
文件 可以根据业务需求,新建对应的js文件,配置如下。
const state = {
themeImg: 'blue',
botColor: 'white',
websiteName: "网站系统名称",//系统名称
copyright: "",
companyLogo: "",//管理平台logo
}
const mutations = {
CHANGE_SETTING: (state, { key, value }) => {
if (state.hasOwnProperty(key)) {
state[key] = value
}
}
}
const actions = {
changeSetting({ commit }, data) {
commit('CHANGE_SETTING', data)
}
}
export default {
// 为了解决不同模块命名冲突的问题,将不同模块的namespaced:true
namespaced: true,
state,
mutations,
actions
}
在vue组件中通过 dispatch
改变 state
的状态
async websiteSetting() {
try {
let params = {};
let { data } = await this.$api.getAction(
this.url.websiteSetting,
params
);
let array = ["copyright", "websiteName", "companyLogo", "indexTitle"];
array.forEach((element) => {
this.$store.dispatch("settings/changeSetting", {
key: element,
value: data[element],
});
});
} catch (error) {}
},
getters.js
文件
import Vue from 'vue'
const getters = {
themeImg: state => state.setttings.themeImg,
}
export default getters
组件中调用 state
的数据
computed: {
themeImg() {
return this.$store.state.settings.themeImg
},
websiteName: {
get() {
return this.$store.state.settings.websiteName
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'websiteName',
value: val
})
}
}
}
通过辅助函数 mapGetters
调用
import { mapGetters } from "vuex";
computed: {
...mapGetters(["themeImg"]),
}
vuex刷新会重新更新状态,可以通过 vuex-persistedstate
实现vuex持久化
可以设置存储方式,还可以只储存 state 中的指定数据。
//安装
npm i -S vuex-persistedstate
//配置
import persistedState from 'vuex-persistedstate'
export default new Vuex.Store({
// ...
plugins: [persistedState()]
})
最后,在main.js
中引入,挂载到Vue实例下 main.js
文件
import store from './store'
new Vue({
el: '#app',
store,
render: h => h(App)
})
vuex原理
每个组件(也就是Vue实例)在 beforeCreate
的生命周期中都混入(Vue.mixin)同一个 Store实例
作为属性store,也就是为什么可以通过this.$store.dispatch
等调用方法的原因。
vuex通过使用Vue的响应式系统,实例化一个vue对象,把state装载到data属性上面,并且把getters装载到computed属性上面,来实现数据的响应式化。
组件之间的通信,按照使用场景,可以分为三大类:
父子组件间通信:
props; $parent / $children; ref ; provide / inject ; $attrs / $listeners
兄弟组件间通信:
eventBus ; vuex;
跨级通信:
vuex; eventBus;provide / inject ; $attrs / $listeners
Vue初始化的时候,做了什么?
在项目入口文件 main.js
中
对Vue实例化,打断点看一下
进入函数看一下
初始化方法在 _init
里面,结合Vue源码来看,在这个文件中:
src\core\instance\index.js
通过初始化混入 initMixin(Vue) 方法在 initMixin 所在文件中:
src\core\instance\init.js
通过 Vue.prototype._init
方法(也就是 Vue 原型中 _init
方法)
可以看到 Vue 初始化时都做了什么。
initLifecycle
初始化组件实例关系的属性
譬如:$parent、$root、$children、$refs
initEvents
初始化自定义事件
这里需要注意的是,自定义事件是通过$on方法注册的
监听者不是父组件,而是当前组件的实例
initRender
初始化 render 渲染函数,
初始化插槽,获取 this.slots
callHook(vm, 'beforeCreate')
调用创建之前的钩子函数,执行 beforeCreate
钩子函数
initInjections
初始化注入,即将父组件的属性注入到子组件中
initState
数据初始化,响应式原理的核心
处理 props、methods、data、computed、watch
initProvide
解析组件配置项中的 provide
选项
将其挂载到 vm._provided 属性上
callHook(vm, 'created')
调用创建完成的钩子函数,执行 created
钩子函数
通过 _init()
可以知道:
beforeCreate
生命周期,不可以访问数据,因为还没有初始化
但是,可以拿到关系属性,插槽,自定义事件
推荐书籍
Spring Boot+Vue全栈开发实战
Vue.js快跑:构建触手可及的高性能Web应用
尤雨溪为你点赞
最后的话
以上,如果对你有用的话,不妨点赞收藏关注一下,谢谢 🙏
😊 微信公众号: OrzR3
💖 不定期更新一些技术类,生活类,读书类的文章。