史上最全Vue3总结

298 阅读20分钟

MVC设计模式与MVVM设计模式

使用Vue框架开发前端项目,最大的优势就是再也不用进行复杂的DOM操作了,我们只要关心数据的变化即可,Vue框架会帮我们把复杂的DOM进行渲染,这背后都要归功于他的设计思想,即MVVM设计模式。

了解MVVM设计模式之前,有必要先了解一下MVC设计模式,MVVM模式是在MVC模式基础上演变而来的。

最早的MVC设计模式是出现在后端开发中,主要目的就是让视图层与数据层分离,职责更加清晰,方便开发等等,例如:Spring MVC、ASP.NET MVC等等。

随着Ajax技术的流行,前后端分离开发越来越流行,前端需要处理复杂的视图与数据,迫使前端也急需一种设计模式来进行分层处理,所以MVC设计模式开始进入前端领域。

早期比较经典的前端MVC框架就是backbone.js,但是前后端还是有很大差异的,所以对传统MVC做了一些改良。

backbone.js存在的问题:

  • 数据流混乱,尤其是多视图多数据场景下
  • 控制层单薄,可有可无

于是2009年Angular.js横空出世,带来了全新的MVVM设计模式,让开发者眼前一亮,除了M和V层以外,就是这个VM层啦,即:viewModel层。MVVM设计模式的核心思想就是不让Model和View这两层直接通信,而是通过VM层来连接。

02-04-Vue-MVVM.png

MVVM设计模式比MVP模式的优势:

  • ViewModel能够观察到数据的变化,并对视图对应的内容进行自动更新
  • ViewModel能够监听到视图的变化,并能够通知数据发生变化

虽然最早提出MVVM模式的是Angular.js,但是Vue把MVVM设计模式发扬光大了,Vue也成为了当下最主流的前端框架之一。

Vue官网上的一段话:虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示组件实例。

MVVM 模型中 M 和 V 不能直接操作,需要VM层加持。但Vue比较灵活,可以直接去操作原生DOM,也就是直接去操作V层。

选项式API的编程风格与优势

选项式API,即:options API

let vm = createApp({
  methods: {},
  computed: {},
  watch: {},
  data(){},
  mounted(){}
})

这种写法的优势:

  • 只有一个参数,不会出现参数顺序的问题,随意调整配置的位置
  • 非常清晰,语法化特别强
  • 非常适合添加默认值的
  • 声明式渲染及响应式数据实现原理

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

<div id="counter">
  Counter: {{ counter }}
</div>
const Counter = {
  data() {
    return {
      counter: 0
    }
  }
}
Vue.createApp(Counter).mount('#counter')

声明式编程:不需要编写具体是如何实现的,直接调用声明就可以实现功能。SQL就是比较经典的声明式语言:

SELECT * from user WHERE username = xiaoming
for(var i=0;i<user.length;i++)
{
    if(user[i].username == "xiaoming")
    {
     print("find");
     break;
    }
}

注意:数据是通过 {{ }} 模板语法来完成的,模板语法支持编写JS表达式

响应式数据实现的原理:利用JS的Proxy对象。Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,其实就是直接监控值的修改,当值发生改变的时候,可以监控到。

<div id="app"></div>
<script>
    let data = new Proxy(
        {
            message: "hello",
        },
        {
            get(target) {
                console.log("get")
                return target.message
            },
            set(target, key, value) {
                console.log("set")
                app.innerHTML = value;
            },
        }
    )
    app.innerHTML = data.message;
    setTimeout(() => {
        data.message = "hi"
    }, 2000)
</script>

指令系统与事件方法及传参处理

指令系统就是通过自定义属性实现的一套功能,也是声明式编程的体现。

通常在标签上添加 v-* 字样,常见的指令有:

  • v-bind -> 操作标签属性,可通过 : 简写
  • v-on -> 操作事件,可通过 @ 简写
<div id="app">
    <p :title="message">这是一个段落</p>
    <button @click=" message = 'hi' ">点击</button>
</div>
{{ message }}
<script>
    let vm = Vue.createApp({
        data(){
            return {
                message: 'hello'
            }
        }
    }).mount('#app')
</script>

如何添加事件方法,通过methods选项API实现,并且Vue框架已经帮我们帮事件传参机制处理好了。

<div id="app">
	<button @click="toClick($event, 123)">点击</button>
</div>
<script>
    let vm = Vue.createApp({
      methods: {
        toClick(ev, num){
        }
      }
    }).mount('#app')
</script>

计算属性与侦听器区别与原理

计算属性

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护,所以过于复杂的逻辑可以移植到计算属性中进行处理。

<div id="app">
{{ reverseMessage }}
</div>
<script>
    let vm = Vue.createApp({
      data(){
        return {
          message: 'hello'
        }
      },
      computed: {
        reverseMessage(){
          return this.message.split('').reverse().join('')
        }
      }
    }).mount('#app')
</script>

计算属性与方法比较像,如下所示:

<div id="app">
{{ reverseMessageMethod() }}<br>
{{ reverseMessageMethod() }}<br>
{{ reverseMessage }}<br>
{{ reverseMessage }}<br>
</div>
<script>
    let vm = Vue.createApp({
        data(){
            return {
                message: 'hello world'
            }
        },
        methods: {
            reverseMessageMethod(){
                console.log(1);
                return this.message.split(' ').reverse().join(' ');
            }
        },
        computed: {
            reverseMessage(){
                console.log(2);
                return this.message.split(' ').reverse().join(' ');
            }
        }
    }).mount('#app');
</script>

计算属性跟方法相比,具备缓存的能力,而方法不具备缓存,所以上面代码执行完,会弹出两次1和一次2。

注意:默认是只读的,一般不会直接更改计算属性,如果想更改也是可以做到的,通过Setter写法实现,官方地址

既然计算属性编写的是一个函数,而调用的时候以函数名的形式进行使用,其实实现起来也不是特别难的事情:

let computed = {
    num(){
        return 123;
    }
}
let vm = {}
for(let attr in computed){
    Object.defineProperty(vm, attr, {
        value: computed[attr]()
    })
}

侦听器

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。侦听器的目的:侦听器用来观察和响应Vue实例上的数据变动,类似于临听机制+事件机制。当有一些数据需要随着其它数据变化而变化时,就可以使用侦听器。

<div id="app">
    {{ message }}
</div>
<script>
    let vm = Vue.createApp({
        data(){
            return {
                message: 'hello'
            }
        },
        watch: {
            message(newVal, oldVal){
            }
        }
    }).mount('#app')
</script>

有时候,计算属性 和 侦听器 往往能实现同样的需求,那么他们有何区别呢?

  • 计算属性适合:多个值去影响一个值的应用;而侦听器适合:一个值去影响多个值的应用
  • 侦听器支持异步的程序,而计算属性不支持异步的程序

条件渲染与列表渲染及注意点

条件渲染

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染。

在 JavaScript 中,truthy(真值)指的是在布尔值上下文中,转换后的值为真的值。所有值都是真值,除非它们被定义为 falsy 假值(即除 false、0、-0、0n、“”、null、undefined 和 NaN 以外皆为真值)。

<div id="app">
    <div v-if="isShow">aaaaa</div>
    <div v-else>bbbbb</div>
</div>
<script>
    let vm = Vue.createApp({
        data(){
            return {
                isShow: 0
            }
        }
    }).mount('#app');
</script>

列表渲染

v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组,而 item 则是被迭代的数组元素的别名。

<div id="app">
	<div v-for="item, index in list">{{ item }}, {{ index }}</div>
    <div v-for="value, key, index in info">{{ value }}, {{ key }}, {{ index }}</div>
    <div v-for="item in num">{{ item }}</div>
    <div v-for="item in text">{{ item }}</div>
</div>
<script>
    let vm = Vue.createApp({
        data(){
            return {
                list: ['a', 'b', 'c'],
                info: { username: 'xiaoming', age: 20 },
                num: 10,
                text: 'hello'
            }
        }
    }).mount('#app');
</script>

条件渲染与列表渲染需要注意的点

  • 列表渲染需要添加key属性,用来跟踪列表的身份
  • v-if 和 v-for 尽量不要一起使用,可利用计算属性来完成筛选这类功能(因为v-if优先级高于v-for,这样v-if拿不到v-for中的item属性)
  • template标签起到的作用,形成一个整体容器

class样式与style样式的三种形态

操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute,所以我们可以用 v-bind 处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind 用于 classstyle 时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。

  • 字符串
  • 数组
  • 对象
let vm = Vue.createApp({
    data() {
        return {
            myClass1: 'box box2',
            myClass2: ['box', 'box2'],
            myClass3: { box: true, box2: true },
            myStyle1: 'background: red; color: white;',
            myStyle2: ['background: red', 'color: white'],
            myStyle3: { background: 'red', color: 'white' },
        }
    },
}).mount("#app")

数组和对象的形式要比字符串形式更加的灵活,也更容易控制变化。

表单处理与双向数据绑定原理

表单是开发过程中经常要进行操作的,一般需要收集表单数据,发送给后端,或者把后端的数据进行回显等。在Vue中是通过v-model指令来操作表单的,可以非常灵活的实现响应式数据的处理。

<div id="app">
	<input type="text" v-model="message"> {{ message }}
</div>
<script>
let vm = Vue.createApp({
    data() {
        return {
            message: 'hello'
        }
    }
}).mount("#app")
</script>

尽管有些神奇,但 v-model 本质上不过是语法糖。可通过value属性 + input事件来实现同样的效果。

<div id="app">
	<input type="text" :value="message" @input="message = $event.target.value"> {{ message }}
</div>
<script>
let vm = Vue.createApp({
    data() {
        return {
            message: 'hello'
        }
    }
}).mount("#app")
</script>

v-model除了可以处理输入框以外,也可以用在单选框、复选框、以及下拉菜单中。

<div id="app">
    <input type="checkbox" v-model="fruits" value="苹果">苹果<br>
    <input type="checkbox" v-model="fruits" value="西瓜">西瓜<br>
    <input type="checkbox" v-model="fruits" value="哈密瓜">哈密瓜<br>
    {{ fruits }
    <input type="radio" v-model="gender" value="女">女<br>
    <input type="radio" v-model="gender" value="男">男<br>
    {{ gender }}
    <select v-model="city">
      <option value="北京">北京</option>
      <option value="上海">上海</option>
      <option value="杭州">杭州</option>
    </select>
    {{ city }}
</div>
<script>
    let vm = Vue.createApp({
        data(){
            return {
                fruits: ['西瓜', '哈密瓜'],
                gender: '男',
                city: '杭州'
            }
        }
    }).mount('#app');
</script>

生命周期钩子函数及原理分析

每个组件在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

就是工厂的流水线,每个工人站在各自的岗位,当任务流转到工人身边的时候,工人就开始工作。

简单来说生命周期钩子函数就是回调函数,在Vue的某个时机去调用对应的回调函数。就像定时器一样,谁调用的定时器的回调函数呢?其实就是定时器内部在调用的。

setTimeout(()=>{
	console.log('2秒后被执行了');
}, 2000)

官方提供的生命周期图示

生命周期可划分为三个部分:

  1. 初始阶段:beforeCreate、created、beforeMount、mounted
  2. 更新阶段:beforeUpdate、updated
  3. 销毁阶段:beforeUnmout、unmounted

注:一般在,created,mounted中都可以发送数据请求,但是,大部分时候,会在created发送请求。因为这样可以更短的时间去响应数据。

搜索关键词加筛选条件的综合案例

案例图示如下

准备好案例的JSON数据

[
  {
    "id": 1,
    "name": "小明",
    "gender": "女",
    "age": 20
  },
  {
    "id": 2,
    "name": "小强",
    "gender": "男",
    "age": 18
  },
  {
    "id": 3,
    "name": "大白",
    "gender": "女",
    "age": 25
  },
  {
    "id": 4,
    "name": "大红",
    "gender": "男",
    "age": 22
  }
]

参考代码

<style>
    .active-gender{
      background: red;
    }
</style>
<div id="app">
    <input type="text" v-model="message">
    <button :class="activeGender('全部')" @click="handleGender('全部')">全部</button>
    <button :class="activeGender('男')" @click="handleGender('男')"></button>
    <button :class="activeGender('女')" @click="handleGender('女')"></button>
    <ul>
      <li v-for="item in filterList" :key="item.id">{{ item.name }}, {{ item.gender }}, {{ item.age }}</li>
    </ul>
</div>
<script>
    let vm = Vue.createApp({
      data() {
        return {
          list: [],
          message: '',
          gender: '全部'
        }
      },
      created(){
        fetch('./02-data.json').then((res)=> res.json()).then((res)=>{
          this.list = res;
        })
      },
      computed: {
        filterList(){
          return this.list
                  .filter((v)=> v.name.includes(this.message))
                  .filter((v)=> v.gender === this.gender || '全部' === this.gender);
        }
      },
      methods: {
        activeGender(gender){
          return { 'active-gender': this.gender === gender };
        },
        handleGender(gender){
          this.gender = gender;
        }
      }
    }).mount('#app');
</script>

组件的概念及组件的基本使用方式

组件的概念

组件是带有名称的可复用实例,通常一个应用会以一棵嵌套的组件树的形式来组织,比如:页头、侧边栏、内容区等组件。

组件可以拥有自己独立的结构,样式,逻辑。这样对于后期的维护是非常方便的。下面给出评分组件与按钮组件的抽离过程。

03-02-组件的划分.png

组件的命名方式与规范

  • 定义组件可通过驼峰、短线两种风格定义
  • 调用组件推荐短线方式
<div id="app">
    <my-head></my-head>
</div>
<script>
let app = Vue.createApp({
    data(){
        return {
        }
    }
})
app.component('my-head', {
    template: `
    <header>
        <div>{{ message }}</div>
        <h2>logo</h2>
        <ul>
            <li>首页</li>
            <li>视频</li>
            <li>音乐</li>
        </ul>
    </header>`,
    data(){
    	return {
        	message: 'hello world'
        }
    }
});
let vm = app.mount('#app');
</script>

根组件

app容器可以看成根组件,所以根组件跟普通组件都具备相同的配置信息,例如:data、computed、methods等等选项。

<div id="app">
	<my-head></my-head>
</div>
<script>
    // 根组件
    let RootApp = {
      data(){
        return {
        }
      }
    };
    // MyHead组件
    let MyHead = {
      template: `
        <header>
          <div>{{ message }}</div>
          <h2>logo</h2>
          <ul>
            <li>首页</li>
            <li>视频</li>
            <li>音乐</li>
          </ul>
        </header>
      `
    };
    let app = Vue.createApp(RootApp)
    app.component('MyHead', MyHead);
    let vm = app.mount('#app');
  </script>

根组件与MyHead组件形成了父子组件。

局部组件与全局组件

局部组件只能在指定的组件内进行使用,而全局组件可以在容器app下的任意位置进行使用。

组件之间是如何进行互相通信的

上一个小节中,我们了解了组件是可以组合的,那么就形成了父子组件,父子组件之间是可以进行通信的, 那么为什么要通信呢?主要是为了让组件满足不同的需求。

父子通信

最常见的通信方式就是父传子,或者子传父,那么父传子通过props实现,而子传父则通过emits自定义事件实现。

03-04-父子通信.png

<div id="app">
    <my-head :count="count" @custom-click="handleClick"></my-head>
</div>
<script>
    let app = Vue.createApp({
        data(){
            return {
                count: 10
            }
        },
        methods: {
            handleClick(data){
              console.log(data);
            }
        }
    })
    app.component('MyHead', {
        props: ['count'],
        emits: ['custom-click'], 
        template: `
        <header>
          <div>{{ count }}</div>
          <h2>logo</h2>
          <ul>
            <li>首页</li>
            <li>视频</li>
            <li>音乐</li>
    	  </ul>
    	</header>`,
        mouted(){
        	this.$emit('custom-click', 'MyHead Data')
    	}
    });
    let vm = app.mount('#app');
</script>

父子通信需要注意的点

  • 组件通信的props是可以定义类型的,在运行期间会进行检测
  • 组件之间的数据是单向流动的,子组件不能直接修改传递过来的值
  • 但是有时候也需要数据的双向流动,可利用v-model来实现

组件的属性与事件是如何进行处理的

有时候组件上的属性或事件并不想进行组件通信,那么Vue是如何处理的呢?

组件的属性与事件

默认不通过props接收的话,属性会直接挂载到组件容器上,事件也是如此,会直接挂载到组件容器上。可通过 inheritAttrs 选项阻止这种行为,通过指定这个属性为false,可以避免组件属性和事件向容器传递。可通过 $attrs 内置语法,给指定元素传递属性和事件,代码如下:

<div id="app">
    <my-head title="hello world" class="box" @click="handleClick"></my-head>
</div>
<script>
    let app = Vue.createApp({
      data(){
        return {
        }
      },
      methods: {
        handleClick(ev){
          console.log(ev.currentTarget);
        }
      }
    })
    app.component('MyHead', {
      template: `
        <header>
          <h2 v-bind:title="$attrs.title">logo</h2>
          <ul v-bind:class="$attrs.class">
            <li>首页</li>
            <li>视频</li>
            <li>音乐</li>
          </ul>
        </header>
      `,
      mounted(){
        console.log( this.$attrs );   // 也可以完成父子通信操作
      },
      inheritAttrs: false   // 阻止默认的属性传递到容器的操作
    });
    let vm = app.mount('#app');
</script>

$attrs也可以实现组件之间的间接通信。

组件的内容是如何组合与分发处理的

在前面的小节中,我们学习了组件之间的通信,让组件之间实现了不同的需求,我们通过给组件添加不同的属性来实现。那么在Vue中如何去传递不同的组件结构呢?这就涉及到了组件内容的分发处理。

插槽slot

在Vue中是通过插槽slot方式来进行分发处理的,Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 元素作为承载分发内容的出口。

<div id="app">
	<my-head>
    	<p>logo</p>
    </my-head>
</div>
<script>
    let app = Vue.createApp({
      data(){
        return {
          message: 'hello'
        }
      }
    })
    app.component('MyHead', {
      data(){
        return {
        };
      },
      template: `
        <header>
          <slot></slot>
        </header>`,
    });
    let vm = app.mount('#app');
</script>

组件内的结构,即<p>logo</p>会被分发到<slot></slot>所在的区域。

内容分发与插槽的注意点

  • 渲染作用域 -> 插槽只能获取当前组件的作用域
  • 具名插槽 -> 处理多个插槽的需求,通过v-slot指令实现,简写为#
  • 作用域插槽 -> 希望插槽能访问子组件的数据

完整代码如下:

<div id="app">
    <my-head>
      <template #title>
        <p>logo, {{ message }}, {{ count }}</p>
      </template>
      <template #list="{ list }">
        <ul>
          <li v-for="item in list">{{ item }}</li>
        </ul>
      </template>
    </my-head>
  </div>
  <script>
    let app = Vue.createApp({
      data(){
        return {
          message: 'hello'
        }
      }
    })

    app.component('MyHead', {
      data(){
        return {
          count: 123,
          list: ['首页', '视频', '音乐']
        };
      },
      template: `
        <header>
          <slot name="title"></slot>
          <hr>
          <slot name="list" :list="list"></slot>
        </header>
      `,
    });
    let vm = app.mount('#app');
</script>
# 仿Element Plus框架的el-button按钮组件实现

本小节利用前面学习的组件通信知识,来完成一个仿Element Plus框架的el-button按钮组件实现。仿造的地址:uhttps://element-plus.org/zh-CN/component/button.html

实现需求

  • 按钮类型
  • 按钮尺寸
  • 按钮文字
  • 添加图标

完整代码如下:

<style>
    .el-button{
      display: inline-flex;
      justify-content: center;
      align-items: center;
      line-height: 1;
      height: 32px;
      white-space: nowrap;
      cursor: pointer;
      background-color: #ffffff;
      border: 1px solid #dcdfe6;
      border-color: #dcdfe6;;
      color: #606266;
      -webkit-appearance: none;
      text-align: center;
      box-sizing: border-box;
      outline: none;
      transition: .1s;
      font-weight: 500;
      user-select: none;
      vertical-align: middle;
      padding: 8px 15px;
      font-size: 14px;
      border-radius: 4px;
    }
    .el-button--primary{
      color: white;
      background-color: #409eff; 
    }
    .el-button--success{
      color: white;
      background-color: #67c23a; 
    }
    .el-button--large{
      height: 40px;
      padding: 12px 19px;
      font-size: 14px;
    }
    .el-button--small{
      height: 24px;
      padding: 5px 11px;
      font-size: 12px;
      border-radius: 3px;
    }
</style>
<link rel="stylesheet" href="./iconfont/iconfont.css">
<script src="../vue.global.js"></script>
<div id="app">
    <el-button>default</el-button>
    <el-button type="primary" size="small">Primary</el-button>
    <el-button type="success" size="large">Success</el-button>
    <el-button type="success" size="large">
      <template #icon>
        <i class="iconfont iconfangdajing"></i>
      </template>
      Success
    </el-button>
  </div>
<script>
    let ElButton = {
        data(){
            return {
                buttonClass: {
                    'el-button': true,
                    [`el-button--${this.type}`]: this.type !== '', 
                    [`el-button--${this.size}`]: this.size !== '' 
                }
            }
        },
        props: {
            type: {
                type: String,
                default: ''
            },
            size: {
                type: String,
                default: ''
            }
        },
        template: `
        <button :class="buttonClass">
          <slot name="icon"></slot>
          <slot></slot>
    	</button>`
    };

    let vm = Vue.createApp({
        data(){
            return {
            }
        },
        components: {
            ElButton
        }
    }).mount('#app');
</script>

单文件组件SFC及Vue CLI脚手架的安装使用

Vue 单文件组件(又名 *.vue 文件,缩写为 SFC)是一种特殊的文件格式,它允许将 Vue 组件的模板、逻辑 与 样式封装在单个文件中。

为什么要使用 SFC

使用 SFC 必须使用构建工具,但作为回报带来了以下优点:

如何支持SFC

可通过项目脚手架来进行支持,Vue支持Vite脚手架和Vue CLI脚手架。这里我们先来介绍Vue CLI的基本使用方式。

# 安装
npm install -g @vue/cli
# 创建项目
vue create vue-study
# 选择default
default (babel, eslint)
# 启动脚手架
npm run serve

通过localhost:8080进行访问。

脚手架文件的组成

  • src/main.js -> 主入口模块
  • src/App.vue -> 根组件
  • src/components -> 组件集合
  • src/assets -> 静态资源

单文件的代码组成

  • template -> 编写结构
  • script -> 编写逻辑
  • style -> 编写样式

03-05-单文件组件.png

其中style中的scoped属性,可以让样式成为局部的,不会影响到其他组件,只会作用于当前组件生效,同时在脚手架下支持常见的文件进行模块化操作,例如:图片、样式、.vue文件等。

脚手架原理之webpack处理html文件和模块打包

为了更好的理解项目脚手架的使用,我们来学习一下webpack工具,因为脚手架的底层就是基于webpack工具实现的。

安装

webpack工具是基于nodejs的,所以首先要有nodejs环境,其次需要下载两个模块,一个是代码中使用的webpack模块,另一个是终端中使用的webpack-cli模块。

npm install --save-dev webpack
npm install --save-dev webpack-cli

配置文件

通过编写webpack.config.js文件,来编写webpack的配置信息,完成工具的操作行为。webpack最大的优点就是可以把模块化的JS文件进行合并打包,这样可以减少网络请求数,具体是通过entry和output这两个字段来完成的。

// webpack.config.js 
module.exports = {
  entry: {
    main: './src/main.js'
  },
  output: {
    path: __dirname + '/dist',
    clean: true
  }
}

在终端中进行nodejs脚本build的调用,这样去进行webpack执行,需要我们自己配置一下package.json的脚本。

npm run build   # -> webpack

这样在项目目录下就产生了一个 /dist 文件夹,里面有合并打包后的文件。那么我们该如何预览这个文件呢?一般可通过html文件进行引入,然后再通过浏览器进行访问。

但是html的编写还需要我们自己引入要预览的JS文件,不是特别的方便,所以是否可以做到自动完成html的操作呢?答案是可以利用webpack工具的插件HtmlWebpackPlugin来实现。

这样HtmlWebpackPlugin插件是需要安装的,通过npm i HtmlWebpackPlugin来完成。

// webpack.config.js
module.exports = {
    ...,
    plugins: [
        new HtmlWebpackPlugin({
          template: './public/index.html',
          title: 'vue-study'
        }),
        new VueLoaderPlugin()
      ]
}

脚手架原理之webpack启动服务器和处理sourcemap

启动服务环境

目前我们的webpack是没有服务环境的,那么如何启动一个web服务器呢?可以通过webpack-dev-server模块,下载使用即可。

npm install webpack-dev-server

安装好后,再package.json中配置scripts脚本,"serve": "webpack-dev-server",然后运行serve脚本。这样就会启动一个http://localhost:8080的服务。

当开启了web服务后,咱们的/dist文件可以不用存在了,服务会把dist的资源打入到内存中,这样可以大大加快编译的速度,所以/dist文件夹可以删除掉了,不影响服务的启动和访问。

处理sourcemap

socurcemap启动映射文件的作用,可以通过浏览器查找到原始的文件,这样对于调试是非常有帮助的,配置如下:

module.exports = {
    devtool: 'inline-source-map'
}
# 脚手架原理之webpack处理样式模块和图片模块

loader预处理文件

在模块化使用中,默认只会支持JS文件,那么怎么能够让其他类型的文件,例如:css文件、图片文件、json文件等等都可以当作模块化进行使用呢?这就需要使用loader技术。

支持css模块化

可以通过安装,css-loaderstyle-loader这两个模块来支持css模块化操作。其中css-loader作用是让css文件能够import方式进行使用,而style-loader的作用是把css代码抽离到<style>标签中,这样样式就可以在页面中生效。

module.exports = {
    module: {
    	rules: [
            {
                test: /\.css$/i,
                use: ['style-loader', 'css-loader']
            }
        ]
    }
}

注意数组的顺序是先执行后面再执行前面,所以先写style-loader再写css-loader,这样就可以通过import './assets/common.css'在main.js中进行使用。

图片模块

同样的情况,如果让webpack支持图片模块化也要使用对应的loader,不过在最新版本的webpack中已经内置了对图片的处理,所以只需要配置好信息就可以支持图片模块化。

module.exports = {
    module: {
    	rules: [
            ...,
            {
                test: /\.(png|jpg|gif)$/i,
                type: 'asset/resource'
            }
        ]
    }
}

脚手架原理之webpack处理单文件组件及loader转换

处理单文件组件

目前我们的webpack还不支持对.vue文件的识别,也不支持.vue模块化使用,所以需要安装一些模块来实现。

npm install vue @vue/complier-sfc vue-loader

vue模块主要是为了让vue功能生效。@vue/complier-sfc是对单文件组件的支持。vue-loader是把单文件组件进行转换。下面看一下webpack的完整配置,如下:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');

module.exports = {
  entry: {
    main: './src/main.js'
  },
  output: {
    path: __dirname + '/dist',
    clean: true
  },
  devtool: 'inline-source-map',
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ['style-loader', 'css-loader']
      },
      {
        test: /\.(png|jpg|gif)$/i,
        type: 'asset/resource'
      },
      {
        test: /\.vue$/i,
        use: ['vue-loader']
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
      title: 'vue-study'
    }),
    new VueLoaderPlugin()
  ],
  mode: 'development'
};

通过配置操作后,目前已经可以完成一个小型的脚手架,支持模块化文件,也支持.vue文件的使用,还可以启动web服务器。

仿Element Plus的el-rate评分组件实现(单文件组件)

插件的安装

在完成本小节案例之前,先安装一下vsCode和chrome的相关插件。

  • vsCode Plugin : Vue Language Features (Volar)
  • vsCode Plugin : Vue VSCode Snippets
  • Chrome Plugin: Vue.js devtools

这些插件都有助于vue框架的使用。下面就看一下我们要做的案例吧。

前面我们仿Element Plus实现了一个按钮组件,不过没有在脚手架的环境下,本小节会在脚手架的环境下完成一个仿Element Plus的el-rate评分组件实现。仿造组件的地址:element-plus.org/zh-CN/compo…

实现需求

  • 最大分值
  • 选中分值
  • 事件交互
<template>
  <ul class="rate">
    <li v-for="index in max" :key="index" @mouseenter=" $emit('update:modelValue', index) " @mouseleave=" $emit('update:modelValue', initValue) " @click=" initValue = index "><i :class="rateClass(index)"></i></li>
  </ul>
</template>

<script>
import '@/assets/iconfont/iconfont.css'
  export default {
    data(){
      return {
        initValue: this.modelValue
      }
    },
    props: {
      max: {
        type: Number,
        default: 5
      },
      modelValue: {
        type: Number,
        default: 0
      }
    },
    emits: ['update:modelValue'],
    methods: {
      rateClass(index){
        return {
          iconfont: true,
          'icon-xingxing': true,
          active: this.modelValue >= index
        }
      }
    }
  }
</script>

<style scoped>
.rate{
  display: flex;
  list-style: none;
}
.rate i{
  font-size: 30px;
  color: #ccc;
}
.rate .active{
  color: blueviolet;
}
</style>

调用评分组件,如下:

<template>
  <h2>hello vue</h2>
  <el-rate v-model="value1"></el-rate>{{ value1 }}
  <el-rate :max="6" v-model="value2"></el-rate>{{ value2 }}
</template>

<script>
import ElRateVue from './components/ElRate.vue'
export default {
  name: 'App',
  data(){
    return {
      value1: 0,
      value2: 3
    }
  },
  components: {
    ElRate: ElRateVue
  }
}
</script>

ref属性在元素和组件上的分别使用

ref属性

前面我们介绍过,Vue是基于MVVM设计模式进行实现的,视图与数据不直接进行通信,但是Vue并没有完全遵循这一原则,而是允许开发者直接进行原生DOM操作。

在Vue中可通过ref属性来完成这一行为,通过给标签添加ref属性,然后再通过vm.$refs来获取DOM,代码如下:

<template>
  <div>
    <h2>ref属性</h2>
    <button @click="handleClick">点击</button>
    <div ref="elem">aaaaaaaaa</div>
    <div ref="elem2">bbbbbbbbb</div>
  </div>
</template>

<script>
  export default {
    methods: {
      handleClick(){
        // ref属性添加到元素身上,可以获取到当前元素的原生DOM
        console.log( this.$refs.elem );
        console.log( this.$refs.elem2 );
      }
    }
  }
</script>

除了可以把ref属性添加给DOM元素外,还可以把ref属性添加给组件,这样可以获取到组件的实例对象,可以间接的实现组件之间的通信,代码如下:

<template>
  <div>
    <h2>ref属性</h2>
    <my-head ref="elem3"></my-head>
  </div>
</template>

<script>
  import MyHead from '@/2_头部组件.vue'
  export default {
    methods: {
      handleClick(){
        // ref属性添加到组件身上,可以获取到当前组件的vm对象(实例对象)
        console.log( this.$refs.elem3 );
        console.log( this.$refs.elem3.message );
        this.$refs.elem3.handleMessage('根组件的数据');
        //$refs 也可以实现间接的父子通信
      }
    }
  }
</script>

2_头部组件.vue文件:

<template>
  <div>
    hello myhead
  </div>
</template>

<script>
  export default {
    data(){
      return {
        message: '头部组件的消息'
      }
    },
    methods: {
      handleMessage(data){
        console.log(data);
      }
    }
  }
</script>

利用nextTick监听DOM更新后的情况

本小节我们将学习一下nextTick方法,它的主要作用是将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。

默认情况下,数据的更新会产生一个很小的异步延迟,所以直接再数据改变后取获取DOM是得不到DOM更新后的结果,而得到的是DOM更新前的结果。

<template>
  <div>
    <h2>hello nextTick</h2>
    <div ref="elem">{{ message }}</div>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        message: 'hello world'
      }
    },
    mounted(){
      setTimeout(()=>{
        this.message = 'hi vue';
        console.log( this.$refs.elem.innerHTML );  // 'hello world'
      }, 2000)
    }
  }
</script>

如何才能得到DOM更新后的结果呢,可以有两种方案,第一种就是利用生命周期updated这个钩子函数,第二种就是利用我们讲的nextTick方法,支持两种风格即回调和promise。

<template>
  <div>
    <h2>hello nextTick</h2>
    <div ref="elem">{{ message }}</div>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        message: 'hello world'
      }
    },
    mounted(){
      setTimeout(()=>{
        this.message = 'hi vue';
        /* this.$nextTick(()=>{
          console.log( this.$refs.elem.innerHTML );   // 'hi vue'
        }) */
        this.$nextTick().then(()=>{
          console.log( this.$refs.elem.innerHTML );  // 'hi vue'
        })
      }, 2000)
    },
    updated(){
       console.log( this.$refs.elem.innerHTML );  // 'hi vue'
    }
  }
</script>

自定义指令与自定义全局属性及应用场景

除了核心功能默认内置的指令 (例如 v-model 和 v-show),Vue 也允许注册自定义指令,来实现一些封装功能。

自定义指令的实现

首先我们先来实现一个简单的v-color指令,用于给元素添加背景色,代码如下:

<template>
  <div>
    <h2>自定义指令</h2>
    <div @click="handleClick" v-color="color">aaaaaaa</div>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        color: 'red'
      }
    }, 
    //创建局部的自定义指令
    directives: {
      /* color: {
        mounted(el, binding){
          el.style.background = binding.value
        },
        updated(el, binding){
          el.style.background = binding.value
        }
      } */
      color: (el, binding) => {
        el.style.background = binding.value
      }
    }
  }
</script>

这里的回调函数是指令中mounted生命周期和updated生命周期的简写方式。

下面我们来完成一个实际可以应用的指令,按钮权限指令,一般情况下这种指令不会局部使用,而是全局使用,所以可以通过vue来实现一个全局的按钮权限指令,代码如下:

// main.js
app.directive('auth', (el, binding) => {
    let auths = ['edit', 'delete'];
    let ret = auths.includes(binding.value);
    if(!ret){
    	el.style.display = 'none';
    }
});

// demo.vue
<template>
<button v-auth="'edit'">编辑</button>
</template>

自定义全局属性

添加一个可以在应用的任何组件实例中访问的全局 property,这样在引入一些第三方模块的时候,就不用每一次进行import操作,而是直接通过this对象去访问全局属性即可,下面举一个例子,实现一个http的全局属性。

// main.js
app.config.globalProperties.$http = http;

//demo.vue
<script>
export default {
   created(){
       this.$http.get();
   }
}
</script>

复用组件功能之Mixin混入

Mixin混入

本小节我们来了解一下mixin混入,它是选项式API的一种复用代码的形式,可以非常灵活的复用功能。

// mymixin.js
const myMixin = {
  data(){
    return {
      message: '复用的数据'
    }
  },
  computed: {
    message2(){
      return '复用的数据2'
    }
  }
};
export {
  myMixin
}
// mymixin.vue
<template>
  <div>
    <h2>mixin混入</h2>
    <div>{{ message }}</div>
    <div>{{ message2 }}</div>
  </div>
</template>
<script>
  import { myMixin } from '@/mymixin.js'
  export default {
    mixins: [myMixin]
  }
</script>

这样就可以直接在.vue中使用这些混入的功能。当然这种方式是局部混入写法,也可以进行全局混入的写法,代码如下:

// main.js
import { myMixin } from '@/mymixin.js'
app.mixin(myMixin)

mixin存在的问题也是有的,那就是不够灵活,不支持传递参数,这样无法做到差异化的处理,所以目前比较推荐的复用操作还是选择使用组合式API中的use函数来完成复用的逻辑处理。后面章节我们会学习到这种组合式API的应用。

插件的概念及插件的实现

插件是自包含的代码,通常向 Vue 添加全局级功能。例如:全局方法、全局组件、全局指令、全局mixin等等。基于Vue的第三方模块都是需要通过插件的方式在Vue中进行生效的,比如:Element Plus、Vue Router、Vuex等等。

// myplugin.js
import * as http from '@/http.js'
export default {
  install(app, options){
    console.log(options);
    app.config.globalProperties.$http = http;
    app.directive('auth', (el, binding) => {
      let auths = ['edit', 'delete'];
      let ret = auths.includes(binding.value);
      if(!ret){
        el.style.display = 'none';
      }
    });
    app.component('my-head', {
      template: `<div>hello myhead</div>`
    })
  }
}
// main.js 让插件生效
import myplugin from './myplugin.js'
app.use(myplugin, { info: '配置信息' })

可以看到,让插件生效的语法为app.use,这样就可以跟Vue结合到一起,所以插件就可以独立出去,成为第三方模块。

# Element Plus框架的安装与使用

前面小节中介绍了自定义插件的实现,那么本小节来看下一比较出名的第三方插件Element Plus如何安装与使用。

Element Plus框架

Element Plus是一套基于PC端的组件库,可以直接应用到很多管理系统的后台开发中,使用前需要先下载安装,除了下载组件库以外,最好也下载组件库对应的icon图标模块,如下:

npm install element-plus @element-plus/icons-vue

接下来把element plus完整引入到Vue中,包装全局组件,全局样式。

import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'

app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
	app.component(key, component)
}

基本使用方式

element plus中提供了非常多的常见组件,例如:按钮、评分、表单控件等等。

<template>
  <div>
    <h2>element plus</h2>
    <el-button @click="handleClick" type="primary" icon="Clock">Primary</el-button>
    <el-button @click="handleClick2" type="success">Success</el-button>
    <el-rate v-model="value1" />
    <el-icon><Clock /></el-icon>
  </div>
</template>
<script>
  import { ElMessage, ElNotification } from 'element-plus';
  export default {
    data(){
      return {
        value1: 3
      }
    },
    mounted(){
      setTimeout(()=>{
        this.value1 = 5;
      }, 2000)
    },
    methods: {
      handleClick(){
        ElMessage.success('提示成功的消息');
      },
      handleClick2(){
        ElNotification({
          title: '邮件',
          message: '您今日的消费记录'
        });
      }
    }
  }
</script>

除了常见的组件外,element plus中也提供了一些逻辑组件,这些逻辑组件是可以直接在JavaScript中进行使用,例如:ElMessage,ElNotification等方法。

transition动画与过渡的实现

在Vue中推荐使用CSS3来完成动画效果。当在插入、更新或从 DOM 中移除项时,Vue 提供了多种应用转换效果的方法。

transition动画

Vue中通过两个内置的组件来实现动画与过渡效果,分别是:<transition><transition-group>,代码如下:

<template>
  <div>
    <h2>hello transition</h2>
    <button @click=" isShow = !isShow ">点击</button>
    <transition name="slide" mode="out-in">
      <div v-if="isShow" class="box"></div>
      <div v-else class="box2"></div>
    </transition>
  </div>
</template>
<script>
  export default {
    data(){
      return {
        isShow: true
      }
    }
  }
</script>
<style scoped>
.box{
  width: 200px;
  height: 200px;
  background: skyblue;
}
.box2{
  width: 200px;
  height: 200px;
  background: pink;
}
.slide-enter-from{
  opacity: 0;
  transform: translateX(200px);
}
.slide-enter-to{
  opacity: 1;
  transform: translateX(0);
}
.slide-enter-active{
  transition: 1s;
}

.slide-leave-from{
  opacity: 1;
  transform: translateX(0);
}
.slide-leave-to{
  opacity: 0;
  transform: translateX(200px);
}
.slide-leave-active{
  transition: 1s;
}
</style>

其中<transition>组件通过name属性去关联CSS中的选择器,CSS中的选择器主要有6种,分别:

  • v-enter-from:进入动画的起始状态。在元素插入之前添加,在元素插入完成后的下一帧移除。
  • v-enter-active:进入动画的生效状态。应用于整个进入动画阶段。在元素被插入之前添加,在过渡或动画完成之后移除。这个 class 可以被用来定义进入动画的持续时间、延迟与速度曲线类型。
  • v-enter-to:进入动画的结束状态。在元素插入完成后的下一帧被添加 (也就是 v-enter-from 被移除的同时),在过渡或动画完成之后移除。
  • v-leave-from:离开动画的起始状态。在离开过渡效果被触发时立即添加,在一帧后被移除。
  • v-leave-active:离开动画的生效状态。应用于整个离开动画阶段。在离开过渡效果被触发时立即添加,在过渡或动画完成之后移除。这个 class 可以被用来定义离开动画的持续时间、延迟与速度曲线类型。
  • v-leave-to:离开动画的结束状态。在一个离开动画被触发后的下一帧被添加 (也就是 v-leave-from 被移除的同时),在过渡或动画完成之后移除。

04-01-动画与过渡.png 默认情况下,进入和离开在两个元素身上是同时执行的,如果想改变其顺序,需要用到mode属性,其中out-in表示先离开再进入,而in-out表示先进入再离开。

动态组件与keep-alive组件缓存

动态组件

动态组件可以实现在同一个容器内动态渲染不同的组件,依一个内置组件<component>is属性的值,来决定使用哪个组件进行渲染。

<template>
  <div>
    <h2>动态组件</h2>
    <button @click=" nowCom = 'my-com1' ">组件1</button>
    <button @click=" nowCom = 'my-com2' ">组件2</button>
    <button @click=" nowCom = 'my-com3' ">组件3</button>
 	<component :is="nowCom"></component>
  </div>
</template>

<script>
import MyCom1 from '@/13_MyCom1.vue'
import MyCom2 from '@/14_MyCom2.vue'
import MyCom3 from '@/15_MyCom3.vue'
  export default {
    data(){
      return {
        nowCom: 'my-com1'
      }
    },
    components: {
      'my-com1': MyCom1,
      'my-com2': MyCom2,
      'my-com3': MyCom3
    }
  }
</script>

keep-alive组件

当我们点击的时候,就会进行组件的切换。在每次切换的过程中都会重新执行组件的渲染,这样组件操作的行为就会还原,而我们如何能够保证组件不变呢?可以利用<keep-alive>对组件进行缓存,这样不管如何切换,都会保持为初始的组件渲染,这样可以很好的保留之前组件的行为。

组件的切换也可以配合<transition>完成动画的切换。

<template>
  <div>
    <h2>动态组件</h2>
    <button @click=" nowCom = 'my-com1' ">组件1</button>
    <button @click=" nowCom = 'my-com2' ">组件2</button>
    <button @click=" nowCom = 'my-com3' ">组件3</button>
    <transition name="slide" mode="out-in">
      <keep-alive>
        <component :is="nowCom"></component>
      </keep-alive>
    </transition>
  </div>
</template>

异步组件与Suspense一起使用

异步组件

在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。

在上一个小节的动态组件的基础上,进行异步组件的演示。首先可以打开chrome浏览器的network网络,可以观察到在动态组件切换的时候,network网络中没有进行任何请求的加载,这证明了在初始的时候,相关的动态组件就已经加载好了。

所以对于大型项目来说,如果能实现按需载入的话,那么势必会对性能有所提升,在Vue中主要就是利用defineAsyncComponent来实现异步组件的。

<script>
import { defineAsyncComponent } from 'vue'
export default {
    data(){
        return {
            nowCom: 'my-com1'
        }
    },
    components: {
        'my-com1': defineAsyncComponent(() => import('@/MyCom1.vue')),
        'my-com2': defineAsyncComponent(() => import('@/MyCom2.vue')),
        'my-com3': defineAsyncComponent(() => import('@/MyCom3.vue'))
    }
}
</script>

Suspense组件

由于异步组件是点击切换的时候才去加载的,所以可能会造成等待的时间,那么这个时候可以配合一个loading效果,在Vue中提供了一个叫做<Suspense>的组件用来完成loading的处理。

<template>
	<suspense>
        <component :is="nowCom"></component>
        <template #fallback>
            <div>loading...</div>
		</template>
	</suspense>
</template>
# 跨组件间通信方案 Provide_Inject

跨组件通信方案

正常情况下,我们的组件通信是需要一级一级的进行传递,通过父子通信的形式,那么如果有多层嵌套的情况下,从最外层把数据传递给最内层的组件就非常的不方便,需要一级一级的传递下来,那么如何才能方便的做到跨组件通信呢?

可以采用Provide 和 inject 依赖注入的方式来完成需求,代码如下:

04-02-依赖注入.png

// provide.vue
<script>
export default {
    provide(){
        return {
            message: 'hello provide',
            count: this.count,
            getInfo(data){
                console.log(data);
            }
        }
    }
}
</script>

// inject.vue
<template>
<div>
    hello inject, {{ message }}, {{ count }}
 </div>
</template>

<script>
export default {
    inject: ['message', 'getInfo', 'count'],
    mounted(){
        this.getInfo('hello inject');
    }
}
</script>

Provide与Inject注意点

  • 保证数据是单向流动的,从一个方向进行数据的修改
  • 如果要传递响应式数据,需要把provide改造成工厂模式发送数据

Teleport实现传送门功能

Teleport组件

Teleport可以实现传送门功能,也就是说逻辑属于当前组件中,而结构需要在组件外进行渲染,例如:按钮模态框组件。

// 模态框.vue
<template>
  <div>
    <button @click=" isShow = true ">点击</button>
    <teleport to="body">
      <div v-if="isShow">模态框</div>
    </teleport>
  </div>
</template>

<script>
  export default {
    data(){
      return {
        isShow: false
      }
    }
  }
</script>
// 调用模态框.vue
<template>
  <div>
    <h2>传送门</h2>
    <my-modal></my-modal>
  </div>
</template>

<script>
import MyModal from '@/模态框.vue'
  export default {
    components: {
      'my-modal': MyModal
    }
  }
</script>

逻辑组件

但是往往我们需要的并不是普通组件的调用方式,而是逻辑组件的调用方式,那么如何实现逻辑组件呢?代码如下:

//  定义逻辑组件,modal.js

import { createApp } from 'vue';
import ModalVue from '@/模态框.vue';

function modal(){
  let div = document.createElement('div');
  createApp(ModalVue).mount(div);
  document.body.append(div);
}

export default modal;
// 调用逻辑组件
<template>
  <div>
    <h2>传送门</h2>
    <button @click="handleClick">点击</button>
  </div>
</template>

<script>
import modal from '@/modal.js'
export default {
  methods: {
    handleClick(){
      modal();
    }
  }
}
</script>

13-虚拟DOM与render函数及Diff算法

虚拟DOM

Vue框架帮我们完成了大量的DOM操作,那么在底层Vue并没有直接操作真实的DOM,因为真实的DOM直接去操作是非常好性能的,所以最好在JS环境下进行操作,然后在一次性进行真实DOM的操作。

const vnode = {
  type: 'div',
  props: {
    id: 'hello'
  },
  children: [
    /* 更多 vnode */
  ]
}

那么在Vue中是如何把<template>模板中的字符串编译成虚拟DOM的呢?需要用到内置的render函数,这个函数可以把字符串转换成虚拟DOM。

04-03-虚拟DOM.png

Diff算法

当更新的时候,一个依赖发生变化后,副作用会重新运行,这时候会创建一个更新后的虚拟 DOM 树。运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去。

而两个虚拟DOM进行对比的时候,需要加入一些算法提高对比的速度,这个就是Diff算法。

04-04-Diff算法.png 在脚手架下我们推荐使用<template>来完成结构的编写,那么也可以直接通过render函数进行虚拟DOM的创建,代码如下:

<template>
  <div>
    <h2>render</h2>
  </div>
</template> 
<script>
  import { h } from 'vue';
  export default {
    render(){
      return h('div', h('h2', 'render2'))
    }  
  }
</script>

<style scoped>

</style>

setup方法与script_setup及ref响应式

setup方法与script_setup

在Vue3.1版本的时候,提供了setup方法;而在Vue3.2版本的时候,提供了script_setup属性。那么setup属性比setup方法对于操作组合式API来说会更加的简易。


<template>
  <div>
    <h2>setup方法</h2>
    {{ count }}
  </div>
</template>

// Vue3.1
<script>
export default {
  setup () {
    let count = 0;
    return {
      count
    }
  }
}
</script>

// Vue3.2
<script setup>
let count = 0;
</script>

setup方法是需要把数据进行return后,才可以在template标签中进行使用,而setup属性方式定义好后就可以直接在template标签中进行使用。

ref响应式

下面来学习一下,如何在组合式API中来完成数据的响应式操作,通过的就是ref()方法,需要从vue模块中引入这个方法后才可以使用。

<script setup>
import { ref } from 'vue';
let count = ref(0);   // count -> { value: 0 }
//count += 1;   //✖
count.value += 1;   // ✔
</scirpt>

count数据的修改,需要通过count.value的方式,因为vue底层对响应式数据的监控必须是对象的形式,所以我们的count返回的并不是一个基本类型,而是一个对象类型,所以需要通过count.value进行后续的操作,那么这种使用方式可能会添加我们的心智负担,还好可以通过Volar插件可以自动完成.value的生成,大大提升了使用方式。

那么现在count就具备了响应式变化,从而完成视图的更新操作。

那么ref()方法还可以关联原生DOM,通过标签的ref属性结合到一起,也可以关联到组件上。

<template>
  <div>
    <h2 ref="elem">setup属性方式</h2>
  </div>
</template>
<script setup>
import { ref } from 'vue';
let elem = ref();
setTimeout(()=>{
  console.log( elem.value );   //拿到对应的原生DOM元素
}, 1000)
</script>

事件方法_计算属性 reactive_toRefs

事件方法与计算属性

下面看一下在组合式API中是如何实现事件方法和计算属性的。

<template>
  <div>
    <button @click="handleChange">点击</button>
    {{ count }}, {{ doubleCount }}
  </div>
</template>
<script setup>
import { computed, ref } from 'vue';
let count = ref(0);
let doubleCount = computed(()=> count.value * 2)
let handleChange = () => {
  count.value += 1;
};
</script>

事件方法直接就定义成一个函数,计算属性需要引入computed方法,使用起来是非常简单的。

reactive与toRefs

reactive()方法是组合式API中另一种定义响应式数据的实现方式,它是对象的响应式副本。

<template>
  <div>
    <h2>reactive</h2>
    {{ state.count }}
  </div>
</template>

<script setup>
import { reactiv} from 'vue';
let state = reactive({
  count: 0,
  message: 'hi vue'
})
state.count += 1;
</script>

reactive()方法返回的本身就是一个state对象,那么在修改的时候就不需要.value操作了,直接可以通过state.count的方式进行数据的改变,从而影响到视图的变化。

ref()和reactive()这两种方式都是可以使用的,一般ref()方法针对基本类型的响应式处理,而reactive()针对对象类型的响应式处理,当然还可以通过toRefs()方法把reactive的代码转换成ref形式。

<template>
  <div>
    <h2>reactive</h2>
    {{ state.count }}, {{ count }}
  </div>
</template>

<script setup>
import { reactive, toRefs } from 'vue';

let state = reactive({
  count: 0
})
let { count } = toRefs(state);   //  let count = ref(0)
setTimeout(() => {
  //state.count += 1;
  count.value += 1;
}, 1000)

</script>

生命周期_watch_watchEffect

生命周期钩子函数

在学习选项式API的时候,我们学习了生命周期钩子函数,那么在组合式API中生命周期又是如何使用的呢?下面我们从图中先看一下对比的情况吧。

05-03-生命周期对比.png 那么具体的区别如下:

  • 组合式中是没有beforeCreate和created这两个生命周期,因为本身在组合式中默认就在created当中,直接定义完响应式数据后就可以直接拿到响应式数据,所以不需要再有beforeCreate和created这两个钩子
  • 组合式的钩子前面会有一个on,类似于事件的特性,那就是可以多次重复调用
<script>
import { onMounted, ref } from 'vue';
let count = ref(0);
onMounted(()=>{
  console.log( count.value );
});
onMounted(()=>{
  console.log( count.value );
});
onMounted(()=>{
  console.log( count.value );
});
</script>

watch与watchEffect

这里先说一下watchEffect的用法,为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect 函数。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

watchEffect常见特性:

  • 一开始会初始触发一次,然后所依赖的数据发生改变的时候,才会再次触发
  • 触发的时机是数据响应后,DOM更新前,通过flush: 'post' 修改成DOM更新后进行触发
  • 返回结果是一个stop方法,可以停止watchEffect的监听
  • 提供一个形参,形参主要就是用于清除上一次的行为
<template>
  <div>
    <h2>watchEffect</h2>
    <div>{{ count }}</div>
  </div>
</template>

<script setup>
import { ref, watchEffect } from 'vue';
let count = ref(0);
// const stop = watchEffect(()=>{
//   console.log(count.value);
// }, {
//   flush: 'post'
// })

// setTimeout(()=>{
//   stop();
// }, 1000)

// setTimeout(()=>{
//   count.value += 1;
// }, 2000)

watchEffect((cb)=>{
  console.log(count.value);
  cb(()=>{
    //更新前触发和卸载前触发,目的:清除上一次的行为(停止上一次的ajax,清除上一次的定时器)
    console.log('before update');
  })
})

setTimeout(()=>{
  count.value += 1;
}, 2000)
</script>

再来看一下watch侦听器的使用方式,如下:

<script setup>
import { ref, watch } from 'vue';
let count = ref(0);
watch(count, (newVal, oldVal) => {
  console.log(newVal, oldVal);
})
setTimeout(()=>{
  count.value = 1;
}, 2000)
</script>

那么watch与watchEffect的区别是什么呢?

  • 懒执行副作用
  • 更具体地说明什么状态应该触发侦听器重新运行
  • 访问侦听状态变化前后的值

跨组件通信方案provide_inject

依赖注入

在Vue中把跨组件通信方案provide_inject也叫做依赖注入的方式,前面我们在选项式中也学习了它的基本概念,下面看一下在组合式API中改如何使用。

// provide.vue
<template>
  <div>
    <my-inject></my-inject>
  </div>
</template>
<script setup>
import MyInject from '@/inject.vue'
import { provide, ref, readonly } from 'vue'
//传递响应式数据
let count = ref(0);
let changeCount = () => {
  count.value = 1;
}
provide('count', readonly(count))
provide('changeCount', changeCount)

setTimeout(()=>{
  count.value = 1;
}, 2000)
</script>

//inject.vue
<template>
  <div>
    <div>{{ count }}</div>
  </div>
</template>

<script setup>
import { inject } from 'vue'
let count = inject('count')
let changeCount = inject('changeCount')
setTimeout(()=>{
  changeCount();
}, 2000);

</script>

依赖注入使用的时候,需要注意的点:

  • 不要在inject中修改响应式数据,可利用回调函数修改

  • 为了防止可设置成 readonly

    复用组件功能之use函数

为了更好的组合代码,可以创建统一规范的use函数,从而抽象可复用的代码逻辑。利用use函数可以达到跟mixin混入一样的需求,并且比mixin更加强大。

// useCounter.js
import { computed, ref } from 'vue';
function useCounter(num){
  let count = ref(num);
  let doubleCount = computed(()=> count.value * 2 );
  return {
    count,
    doubleCount
  }
}

export default useCounter;
<template>
  <div>
    <h2>use函数</h2>
    <div>{{ count }}, {{ doubleCount }}</div>
  </div>
</template>
<script setup>
import useCounter from '@/compotables/useCounter.js'
let { count, doubleCount } = useCounter(123);
setTimeout(()=>{
  count.value += 1;
}, 2000);
</script>

通过useCounter函数的调用,就可以得到内部return出来的对象,这样就可以在.vue文件中进行功能的使用,从而实现功能的复用逻辑。