likes
comments
collection
share

Vue全家桶的整理文案 Part1

作者站长头像
站长
· 阅读数 11

前言

省流:前端核心分析,响应式原理,初始化过程,组件之间的七种通信方式。

情况呢,就是这么个情况:

团队内部交流学习,让我讲一下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全家桶的整理文案 Part1

关键词 渐进式

Vue的渐进式表现:

声明式渲染

Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统

Vue全家桶的整理文案 Part1

响应式数据

页面使用了数据,当数据改变后,页面也会自动更新。 Vue全家桶的整理文案 Part1

Vue全家桶的整理文案 Part1

计算属性和侦听器 computed 和 watch

🚀官网地址

计算属性 官网例子

Vue全家桶的整理文案 Part1

模板内的表达式非常便利,但是在模板中放入太多的逻辑,会让模板过重且难以维护。

所以,对于任何复杂逻辑,你都应当使用计算属性。

基础例子

Vue全家桶的整理文案 Part1

官网例子,声明了一个计算属性 reversedMessage。

我们提供的函数将用作 property vm.reversedMessage 的 getter 函数。

计算属性缓存 computed vs 方法 methods

我们可以通过,在表达式中调用方法,来达到同样的效果。

Vue全家桶的整理文案 Part1 Vue全家桶的整理文案 Part1

这两种方式的最终结果确实是完全相同的。

不同的是:

计算属性是基于它们的响应式依赖进行缓存的。

只在相关响应式依赖发生改变时它们才会重新求值。

这就意味着:只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。

这也同样意味着下面的计算属性将不再更新,因为 Date.now() 不是响应式依赖:

Vue全家桶的整理文案 Part1

相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。

如果你不希望有缓存,请用方法来替代。

计算属性的 setter

计算属性默认只有 getter,不过在需要时你也可以提供一个 setter

Vue全家桶的整理文案 Part1 Vue全家桶的整理文案 Part1 Vue全家桶的整理文案 Part1

侦听器 watcher

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。

🚀watcher源码

Vue全家桶的整理文案 Part1 Vue全家桶的整理文案 Part1

watch监听数组或对象

handler:监听数组或对象的属性时用到的方法**

deep:深度监听,为了发现对象内部值的变化,可以在选项参数中指定 deep:true 。注意监听数组的变动不需要这么做

还有个属性 immediate 默认为 false

如果设置 immediate: true

代表在wacth里声明了这个方法之后,立即先去执行handler方法。

Vue全家桶的整理文案 Part1 Vue全家桶的整理文案 Part1 Vue全家桶的整理文案 Part1

计算属性computed 和 watcher 区别

computed计算属性,watch监听一个值的变化。

从应用场景上来看:

computed一般是多对一,一个属性受到多个属性的影响,譬如购物车结算。

Vue全家桶的整理文案 Part1

而watcher一般情况是一对多,一个属性影响到多个属性,譬如搜索框。

Vue全家桶的整理文案 Part1

computed支持缓存,watch不支持缓存。

computed不支持异步,watch支持异步。

响应式原理

简单说一下,Vue2.x响应式原理

Vue全家桶的整理文案 Part1

运行代码,改变vm.name的值,触发了数据挟持。

如果,是多属性的对象,通过 Object.keys(obj) 循环遍历。

Vue全家桶的整理文案 Part1

看起来没啥问题,访问属性时,会触发get方法,返回data[key],但是访问data[key]也会触发get方法,一直递归调用。

Obsever (观察者)

Vue全家桶的整理文案 Part1

因此,需要设置一个中转 Obsever (观察者),这样get中return的值,就不再直接访问 data[key]

Vue全家桶的整理文案 Part1

如果,对象的属性,仍然是个对象,继续递归调用 Observer 方法。 Vue全家桶的整理文案 Part1

通过 Observer 将一个普通对象,转换为响应式对象。

Vue全家桶的整理文案 Part1

以上这些例子,用来说明Vue响应式的原理。Vue源码的实际操作,比这个稍微复杂亿点点。

依赖数据的观察者称为 watcher

data -> watcher

Vue 通过在 datawatcher 间创建一个 dep 对象,来记录这种依赖关系

data - dep -> watcher

结合Vue官方给的图,比较好理解:

  1. 通过 Object.defineProperty 替换配置对象属性的set,get方法,实现拦截
  2. watcher在执行getter函数时,触发数据的get方法,建立依赖关系
  3. 写入数据时,触发set方法,从而借助dep发布通知,进而watcher进行更新
  4. dep通知watcher更新之后,watcher不是立即执行,因为频繁运行,会导致效率低下。通过调度器scheduler 维护一个执行队列。
  5. 调度器 scheduler 通一个叫做 nextTick 的方法,把需要执行的 watcher 放到事件循环的队列中。nextTick 的具体做法,是通过 Promise 完成的。
  6. nextTick 通过 this.$nextTick 暴露给开发者,视图更新的异步操作,完成后执行这个方法。
Vue全家桶的整理文案 Part1 ## $set 和 $delete

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不会修改传入对象,而是返回一个新的对象。

Vue全家桶的整理文案 Part1

因为,Proxy代理整个对象,而不是对象的某个特定属性,因此无需遍历。

而且,新增和删除属性时,视图不更新的问题,也不存在了。

双向数据绑定

v-model 是 Vue 中最常用的指令,用于实现数据和视图的双向绑定。

v-modelv-bindv-on 的语法糖,也就是简洁写法。

Vue全家桶的整理文案 Part1 Vue全家桶的整理文案 Part1

在组件中使用v-model

父组件 Vue全家桶的整理文案 Part1

子组件 Vue全家桶的整理文案 Part1

  1. 组件上的 v-model 默认会用 名为 valueprop 和名为input的事件,在子组件通过 $emit 触发一个事件。

  2. radiocheckbox,是通过它的checked属性和change事件,实现双向数据绑定。

  3. select元素,是使用它的value属性和change事件,实现双向数据绑定。

内置指令

v-model 是使用频率最高的Vue内置指令,通过它来实现双向数据绑定。

上文提到,v-model 实际上是 v-bind 属性绑定和v-on事件绑定的语法糖,也就是简写。

比较常见的内置指令,还有这些:

条件渲染 v-if,v-else,v-show

循环渲染 v-for

等等

🚀Vue指令,官网地址

自定义指令

除了核心功能默认内置的指令,Vue也允许注册自定义指令。

// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function (el) {
    // 聚焦元素
    el.focus()
  }
})
Vue全家桶的整理文案 Part1

当页面加载时,该元素将获得焦点

<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全家桶的整理文案 Part1 Vue全家桶的整理文案 Part1

自定义指令,十分灵活,还可以用他定义元素的颜色,等等。

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)
  }
})
Vue全家桶的整理文案 Part1 Vue全家桶的整理文案 Part1

组件之间的七种通信方式

prop 和 $emit

在组件中使用 v-model 的时候,提到了父子组件的通信方式。

Vue全家桶的整理文案 Part1

prop$emit 是也最常见的组件之间的通信方式。除此之外,还有几种通信方式。

provide / inject

如果子组件,下面还有子组件,用 prop$emit 通信,就比较麻烦。

provide / inject 一个组件可以向它的所有子孙,传入一个依赖,不管组件层级有多少层。

这就好比:火之意志的继承。

木叶飞舞之处,火亦生生不息。

Vue全家桶的整理文案 Part1

provide

Vue全家桶的整理文案 Part1

inject

Vue全家桶的整理文案 Part1

父组件

<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 虽然解决了祖孙之间的组件通信,但是兄弟组件之间通信,又该怎么办呢?

Vue全家桶的整理文案 Part1

这种情况,可以使用 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>
Vue全家桶的整理文案 Part1

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全家桶的整理文案 Part1

对Vue实例化,打断点看一下

Vue全家桶的整理文案 Part1

进入函数看一下

Vue全家桶的整理文案 Part1

初始化方法在 _init 里面,结合Vue源码来看,在这个文件中:

src\core\instance\index.js

Vue全家桶的整理文案 Part1

通过初始化混入 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 生命周期,不可以访问数据,因为还没有初始化

但是,可以拿到关系属性,插槽,自定义事件

Vue全家桶的整理文案 Part1

推荐书籍

Spring Boot+Vue全栈开发实战

Vue全家桶的整理文案 Part1

Vue.js快跑:构建触手可及的高性能Web应用

Vue全家桶的整理文案 Part1

尤雨溪为你点赞

Vue全家桶的整理文案 Part1

最后的话

以上,如果对你有用的话,不妨点赞收藏关注一下,谢谢 🙏

😊 微信公众号: OrzR3

💖 不定期更新一些技术类,生活类,读书类的文章。