Vue3.0学习总结

654 阅读14分钟

一、生命周期(钩子)

1. 创建期间的生命周期函数

创建期间的生命周期函数:

  • beforeCreate:实例刚在内存中被创建出来,此时,还没有初始化好data和 methods 属性。
  • created:实例已经在内存中创建完成,此时data和methods已经创建完成,此时还没有开始编译模板,在处理读/写反应数据时,使用created 的方法很有用。 例如,要进行API调用然后存储该值,则可以在此处进行此操作。最好在这里执行此操作,而不是在mounted 中执行此操作,因为它发生在Vue的同步初始化过程中,并且我们需要执行所有数据读取/写入操作。
  • beforeMount:此时已经完成了模板的编译,但是还没有挂载到页面中。
  • mounted:此时已经将编译好的模板,挂载到了页面指定的容器中显示。

2. 运行期间的生命周期函数

运行期间的生命周期函数:

  • beforeUpdate:状态更新之前执行此函数,此时data 中的状态值是最新的,但是界面上显示的数据还是旧的,因为此时还没有开始重新渲染DOM节点。
  • updated:实例更新完毕之后调用此函数,此时data 中的状态值和界面上显示的数据,都已经完成了更新,界面已经被重新渲染好了。

3. 销毁期间的生命周期函数

销毁期间的生命周期函数:

  • beforeUnmounted:实例销毁之前调用。在这一步,实例仍然完全可用。
  • unmounted:Vue实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。

二、指令标签

1.v-text

v-text:用来获取数据并将数据以文本的形式渲染到指定标签内部,类似于javascript 中 innerText。

<template>
  <div class="app">
    <span>{{message}}</span>
    <span v-text="message"></span>
  </div>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    let message = ref("hello world");
    return { message };
  }
};
</script>

插值表达式:

  • 可写js表达式:{{Math.max(1,2,3)}}
  • 也可直接写:{{content}} {{}}(插值表达式)和v-text获取数据的区别在于:
  • 使用v-text取值会将标签中原有的数据覆盖,使用插值表达式的形式不会覆盖标签原有的数据。
  • 使用v-text可以避免在网络环境较差的情况下出现插值闪烁。

2.v-html

<template>
  <div class="app">
    <span v-html='message'></span>
    <span v-text="message"></span>
  </div>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    let message = ref("<a href='www.baidu.com'>百度一下</a>");
    return { message };
  }
};
</script>

v-html和v-text区别:

  • 共同点:都可以直接根据data中数据名,将数据渲染到标签内部
  • 不同点:
    • v-text: v-text将获取数据直接以文本形式渲染到标签内部 innerText
    • v-html: v-html将获取数据中含有html标签解析之后渲染到对应标签内部 innerHtml

3.事件指令—v-on

  • js 事件三要素:
    • 事件源:发生事件源头称之为事件源,一般指的是html标签
    • 事件:发生特定动作 onclick单击 dbclick 双击 onkeyup ......
    • 监听器:事件处理器函数 function(){}
  • vue事件:v-on
    • 事件指令v-on:click=“handleClick” 简写成 @click="handleClick"
<template>
  <div class="app">
    <!--给button按钮绑定多个事件-->
    <button type="button" @click="click" v-on:mouseover="mouseOver" v-on:mouseout="mouseOut">点我</button>
  </div>
</template>
<script>
export default {
  setup() {
    const click = function () {
      alert("click");
    };
    const mouseOver = function () {
      alert("mouse over");
    };
    const mouseOut = function () {
      alert("mouse out");
    };
    return { click, mouseOver, mouseOut };
  }
};
</script>

3.1 阻止事件冒泡

<div @click="maopao">
    <button @click.stop="handleClick()">通过.stop修饰符来禁止maopao事件的执行</button>
</div>

3.2 阻止默认事件

例如,form表单在提交时,会自动刷新页面,此时就需要阻止默认事件,只执行对应的方法:

<template>
  <div class="app">
    <form @click.prevent='onSubmit'>
      <input type="submit" value="提交">
    </form>
  </div>
</template>
<script>
export default {
  setup() {
    const onSubmit = function () {
      alert("click");
    };
    return { onSubmit };
  }
};
</script>

3.3 只调用一次

<input type="button" value="提交" @click.once="onSubmit"/>

3.4 按键与鼠标修饰符

//按键修饰符 .enter键 .tab .delete esc up down left right
//鼠标修饰符 left right middle
<template>
  <div class="app">
    <input @keydown="handleKeyDown" placeholder="点击任意键盘执行事件"/>
    <input @keydown.enter="enterDown" placeholder="点击任意键盘+回车执行事件"/>
    <div @click.ctrl.exact="ctrlAndMouseDown">
      按住ctrl+鼠标点击这个div才执行事件
    </div>
  </div>
</template>
<script>
export default {
  setup() {
    const handleKeyDown = function () {
      alert("handleKeyDown");
    };
    const enterDown = function () {
      alert("enterDown");
    };
    const ctrlAndMouseDown = function () {
      alert("ctrlAndMouseDown");
    };
    return { handleKeyDown, enterDown, ctrlAndMouseDown };
  },
};
</script>

3.5 事件参数传递

注意两点:

  • 不使用圆括号,event被自动当作实参传入。
  • 使用圆括号,必须显式的传入event对象,如果不传入可能最终找到的是全局的window.event。
<template>
  <div class="app">
    <button @click='handleBtnClick(2, $event)'>新增</button>
    计数器:{{counter}}
  </div>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    let counter = ref(0);
    const handleBtnClick = function(num, event){
      //event可获得原生js事件,获得一些dom属性进行操作
      console.log(event.target);
      counter.value += num;
    };
    return { handleBtnClick, counter };
  }
};
</script>

4.循环指令——v-for

<template>
  <div class="app">
    <h2>{{ msg }}</h2>
    <h1>遍历对象</h1>
    <h2 v-for="(value, key, index) in user" :key="index">
      index: {{ index }} key:{{ key }} value:{{ value }}
    </h2>
    <h1>遍历数组</h1>
    <h2 v-for="(school, index) in schools" :key="index">
      index:{{ index }} schools:{{ school }}
    </h2>
    <h1>遍历数组中含有对象</h1>
    <h2 v-for="(user, index) in users" :key="user.id">
      index: {{ index }} name:{{ user.name }} age:{{ user.age }} bir:{{
        user.bir
      }}
    </h2>
  </div>
</template>
<script>
import { reactive,ref } from "vue";
export default {
  setup() {
    let msg = ref("hello world");
    let user = reactive({ name: "zhangsan", age: 22, bir: "2009:12:22" });
    let schools = ["北京", "上海", "广州"];
    let users = reactive([
      { id: "1", name: "张三", age: 23, bir: "2002-03-03" },
      { id: "2", name: "李四", age: 34, bir: "2006-04-02" },
      { id: "3", name: "王五", age: 12, bir: "2004-05-01" },
    ]);
    return { msg, user, schools, users };
  },
};
</script>
  • 1.在使用v-for的时候一定要注意加入:key 用来给vue内部提供重用和排序的唯一key

5. v-show、v-if

  • v-show: 用来控制页面中某个标签元素是否展示,相当于:style="display:block/none"。
  • v-if: 用来控制页面元素是否展示。
<template>
  <div class="app">
    <h1 v-if="isShow">{{msg}}--"v-if"</h1>
    <h1 v-show="isShow">{{msg}}---"v-show"</h1>
  </div>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    let msg = ref("hello world");
    let isShow = ref(false);
    return { msg, isShow };
  },
};
</script>

总结:

  • 在使用v-show时可以直接书写boolean值控制元素展示,也可以通过变量控制标签展示和隐藏。
  • 在v-show中可以通过boolean表达式控制标签的展示和隐藏。
  • v-if、v-show : 作用:都是用来控制页面中标签是否展示和隐藏 使用:标签:v-if="true|false",v-show="true|false" 区别:
  • v-show: 底层在控制页面标签是否展示时使用的是css 中的 display 属性来标签展示和隐藏 。推荐使用:v-show 在数据量比较大和控制显示状态切换频繁时。
  • v-if : 底层在控制页面标签是否展示时是直接操作dom元素,通过对dom元素删除和添加来控制标签的展示和隐藏。

6. v-bind

v-bind: 用来绑定标签的属性,从而通过vue动态修改标签的属性。

v-bind:value="inputValue"可简写为:value="inputValue"

6.1 绑定html class

6.1.1 对象语法

我们可以给v-bind:class一个对象,以动态的切换class,注意:v-bind:class指令可以与普通的class特性共存。

<template>
  <div class="app">
    <ul class='box' v-bind:class="{'textColor': isColor, 'textSize':isSize}">
      <li>苹果</li>
      <li>香蕉</li>
    </ul>
  </div>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    let isColor = ref(true);
    let isSize = ref(true);
    return { isColor, isSize };
  },
};
</script>
<style>
  .box {
    border: 1px dashed #f0f;
  }
  .textColor {
    color: #000;
    background-color: #eef;
  }
  .textSize {
    font-size: 30px;
    font-weight: bold;
  }
</style>

也可以直接绑定数据里的一个对象:

<template>
  <div class="app">
    <ul class='box' v-bind:class="classObject">
      <li>苹果</li>
      <li>香蕉</li>
    </ul>
  </div>
</template>
<script>
import { reactive } from "vue";
export default {
  setup() {
    let classObject = reactive({
      'textColor': true, 
      'textSize':false //不渲染
    })
    return { classObject };
  },
};
</script>

6.1.2 数组语法

我们可以把一个数组传给v-bind:class,以应用一个class列表。

<template>
  <div class="app">
    <ul class='box' v-bind:class="[classA, classB]">
      <li>苹果</li>
      <li>香蕉</li>
    </ul>
  </div>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    const classA = ref('textColor');
    const classB = ref('textSize');
    return { classA, classB };
  },
};
</script>
<style>
  .box {
    border: 1px dashed #f0f;
  }
  .textColor {
    color: #000;
    background-color: #eef;
  }
  .textSize {
    font-size: 30px;
    font-weight: bold;
  }
</style>

6.2 绑定内联样式

6.2.1 对象语法

v-bind:style的对象语法十分直观,非常像CSS,其实它是一个JavaScript对象,CSS属性名必须用驼峰命名法。

<template>
  <div class="app">
    <ul class='box' v-bind:style="{color: activeColor, fontSize: size}">
      <li>苹果</li>
      <li>香蕉</li>
    </ul>
  </div>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    const activeColor = ref('#f00');
    const size = ref('30px');
    return { activeColor, size };
  },
};
</script>

也可以直接绑定到一个样式对象,这样更好,让模板更清晰:

<template>
  <div class="app">
    <ul class='box' v-bind:style="styleObject">
      <li>苹果</li>
      <li>香蕉</li>
    </ul>
  </div>
</template>
<script>
import { reactive } from "vue";
export default {
  setup() {
    const styleObject = reactive({color: '#f00', fontSize: '30px'});
    return { styleObject };
  },
};
</script>

6.2.2 数组语法

可以将多个样式应用到一个元素上。

<template>
  <div class="app">
    <ul class='box' v-bind:style="[styleObjectA, styleObjectB]">
      <li>苹果</li>
      <li>香蕉</li>
    </ul>
  </div>
</template>
<script>
import { reactive } from "vue";
export default {
  setup() {
    const styleObjectA = reactive({color: 'blue', fontSize: '36px'});
    const styleObjectB = reactive({textDecoration: 'underline'});
    return { styleObjectA, styleObjectB };
  },
};
</script>

6.3 v-model

数据绑定形式。一般用于表单数据绑定 表单常用标签:input、textarea、checkbox、radio、select

<template>
    <!--下拉框-->
    <select v-model="selected">
      <option value="A被选">A</option>
      <option value="B被选">B</option>
      <option value="C被选">C</option>
    </select>
    <span>Selected: {{ selected }}</span>  
     <br>
    <!--单选按钮-->
    <input type="radio" id="small" value="small_value" v-model="picked">
    <label for="small">small</label>
    <br>
    <input type="radio" id="big" value="big_value" v-model="picked">
    <label for="big">big</label>
    <br>
    <span>Picked: {{ picked }}</span>
    <br>
    <!--复选框-->
    <input type="checkbox" id="one" value="value_one" v-model.lazy="checkedNames">
    <label for="one">选项一</label>
    <input type="checkbox" id="two" value="value_two" v-model.lazy="checkedNames">
    <label for="two">选项二</label>
    <input type="checkbox" id="three" value="value_three" v-model.lazy="checkedNames">
    <label for="three">选项三</label>
    <br>
    <span>Checked names: {{ checkedNames }}</span>
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    const selected = ref('');
    const picked = ref('');
    const checkedNames = ref([]);
    return {selected, picked, checkedNames}
  },
};
</script>

v-model也可以和.lazy、.trim和.number这些修饰符一起使用

  <!-- 在每次 input 事件触发后将输入框的值与数据进行同步,添加 lazy 修饰符,
  从而转变为使用 change 事件进行同步 -->
  <input v-model.lazy="msg" >
  <!--去除字符串首尾的空格-->
  <input v-model.trim="msg">
  <!--将数据转化为值类型-->
  <input v-model.number="age" type="number">

7. 一些其他常用指令——v-pre、v-once、v-block

7.1 v-pre

跳过这个元素和它的子元素的编译过程。可以用来显示原始的Mustache标签。跳过大量没有指令的节点会加快编译。也就是说vue遇到这个标签就不进行编译渲染,跳过此标签,直接显示原始数据。

<!-- v-pre -->
<span v-pre>{{ this will not be compiled }}</span>
<!-- 页面直接显示 -->
{{ this will not be compiled }}

7.2 v-once

只渲染元素和组件一次,使用了此指令的元素/组件及其所有的子节点,都会当作静态内容并跳过,这可以用于优化性能。 当修改input框的值时,使用了v-once指令的p元素不会随之改变,而第二个p元素时可以随之改变的。

<template>
  <p v-once>v-once:{{msg}}</p>
  <p>not v-once:{{msg}}</p>
  <input type="text" v-model="msg" />
</template>
<script>
import { ref } from "vue";
export default {
  setup() {
    const msg = ref('13');
    return {msg}
  },
};
</script>

四、组件

1.认识组件

在之前的案例中,我们只是创建了一个组件App;如果我们一个应用程序将所有的逻辑都放在一个组件中,那么这个组件就会变成非常的臃肿和难以维护;所以组件化的核心思想应该是对组件进行拆分,拆分成一个个小的组件;再将这些组件组合嵌套在一起,最终形成我们的应用程序。

我们来分析一下下面代码的嵌套逻辑,假如我们将所有的代码逻辑都放到一个App.vue组件中: 

<template>
  <div id="app">
    <div class="header"><h2>这里是header,哈哈哈</h2></div>
    <div class="main">
      <ul>
        <li>我是Banner,嘻嘻嘻1</li>
        <li>我是Banner,嘻嘻嘻2</li>
        <li>我是Banner,嘻嘻嘻3</li>
      </ul>
      <ul>
        <li>我是ProductList,嘻嘻嘻1</li>
        <li>我是ProductList,嘻嘻嘻2</li>
        <li>我是ProductList,嘻嘻嘻3</li>
      </ul>
    </div>
    <div class="footer"><h2>这里是footer,嘿嘿嘿</h2></div>
  </div>
</template>

我们会发现,将所有的代码逻辑全部放到一个组件中,代码是非常的臃肿和难以维护的。

并且在真实开发中,我们会有更多的内容和代码逻辑,对于扩展性和可维护性来说都是非常差的。

所以,在真实的开发中,我们会对组件进行拆分,拆分成一个个功能的小组件。

2.组件的拆分

我们可以按照如下的方式进行拆分: image.png 按照如上的拆分方式后,我们开发对应的逻辑只需要去对应的组件编写就可。各自组件去做各自的功能,当然组建内部还能进行拆分为多个组件。

3.全局组件

直接挂载定义。全局组件具有复用性,互不影响。组件定义了在整个实例中处处可用,但很影响性能。

    //全局组件具有复用性,互不影响。组件定义了处处可用,但很影响性能
    <div id="root">
    </div>
    <script src="https://unpkg.com/vue@next"></script>
    <script>
        const app = Vue.createApp({
            template: `
            <div><counter-parent/></div>
            <div><counter/></div>
            <div><counter/></div>`
        });
        //注册全局组件
        app.component('counter', {
            data() {
                return {
                    count: 0
                }
            },
            template: `<button @click="count += 1">{{count}}</button>`
        });
        app.component('counter-parent', {
            template: `<counter/>`
        });
        app.mount("#root");
    </script>

4.局部组件

工作中常用局部组件,与全局组件不同的是要做名字的映射对象。 注意:局部注册的组件在其子组件中不可用

    <div id="root">
    </div>
    <script src="https://unpkg.com/vue@next"></script>
    <script>
        const Counter = {
            data() {
                return {
                    count: 0
                }
            },
            template: `<button @click="count += 1">{{count}}</button>`
        };
        const HelloWorld = {
            template: `<div>hello</div>`
        };
        const app = Vue.createApp({
            components: { 'counter': Counter, 'hello-world': HelloWorld },
            //挂载 局部组件,上下顺序很重要。可以直接简写为Counter
            //components: {Counter, HelloWorld}
            template: `
                <div> <counter /></div>
                <div><hello-world /></div>`
        });
        app.mount("#root");
    </script>

在 ES2015+ 中,在对象中放一个类似 ComponentA 的变量名其实是 ComponentA: ComponentA 的缩写,即这个变量名同时是:

  • 用在模板中的自定义元素的名称
  • 包含了这个组件选项的变量名

5.组件名

定义组件名的方式有两种:

5.1 使用 kebab-case

Vue.component('my-component-name', { /* ... */ })

当使用 kebab-case (短横线分隔命名) 定义一个组件时,必须在引用这个自定义元素时使用 kebab-case。

5.2 使用 PascalCase

Vue.component('MyComponentName', { /* ... */ })

当使用 PascalCase (首字母大写命名) 定义一个组件时,在引用这个自定义元素时两种命名法都可以使用。但是,直接在 DOM (即非工程化) 中使用时只有 kebab-case 是有效的。

6.组件的data选项必须是函数

  • 主要是为了数据能够隔离开,没创建一个新的组件就开创一个新的对象,而不是共用一个对象。
  • 共用一个对象的结果就是每个组件内部的数据会受到其他组件的修改,应为多个组件指向同一个内存堆控件。

7.组件之间的通信

上面的嵌套逻辑如下,它们存在如下关系:

  • App组件是Header、Main、Footer组件的父组件
  • Main组件是Banner、ProductList组件的父组件 在开发过程中,我们会经常遇到需要组件之间相互进行通信

比如App可能使用了多个Header,每个地方的Header展示的内容不同,那么我们就需要使用者传递给Header一些数据,让其进行展示;

又比如我们在Main中一次性请求了Banner数据和ProductList数据,那么就需要传递给它们来进行展示;也可能是子组件中发生了事件,需要由父组件来完成某些操作,那就需要子组件向父组件传递事件。

总之,在一个Vue项目中,组件之间的通信是非常重要的环节,所以接下来我们来看一下组件之间是如何进行数据传递的。

不要在子组件中直接修改父组件的状态数据()。数据和处理数据的函数应该在同一模块内。

7.1 父组件传递给子组件

在开发中很常见的就是父子组件之间通信,比如父组件有一些数据,需要子组件来进行展示:这个时候我们可以通过props来完成组件之间的通信

props使你可以在组件上注册一些自定义的属性;父组件给这些属性赋值,子组件通过属性的名称获取到对应的值。

props有两种常见的用法:

  • 方式一:字符串数组,数组中的字符串就是属性的名称
  • 方式二:对象类型,对象类型我们可以在指定属性名称的同时,指定它需要传递的类型、是否是必须的、默认值等等 1)props的数组用法 image.png 2)props的对象用法 数组用法中我们只能说明传入的属性的名称,并不能对其进行任何形式的限制,接下来我们来看一下对象的写法是如何让我们的props变得更加完善的。

当使用对象语法的时候,我们可以对传入的内容限制更多:

  • 指定传入的属性的类型
  • 指定传入的属性是否是必传的
  • 指定没有传入时,属性的默认值
props: {
    //指定类型,默认值
    title: { type: String, default: "默认值title" }, //指定类型、是否必传
    message: { type: String, required: true },
}

7.2子组件传递给父组件

什么情况下子组件需要传递内容到父组件呢? 

当子组件有一些事件发生的时候,比如在组件中发生了点击,父组件需要切换内容;子组件有一些内容想要传递给父组件的时候。

我们如何完成上面的操作呢?

  • 1)首先,我们需要在子组件中定义好在某些情况下触发的事件名称
  • 2)其次,在父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中;
  • 3)最后,在子组件中发生某个事件的时候,根据事件名称触发对应的事件

我们封装一个计数器按钮的组件,内部监听两个按钮的点击,点击之后通过 emit的方式发出去事件。

<template>
  <div>
    <button @click="increment">+</button> <button @click="decrement">-</button>
    <h2></h2>
    <input type="text" v-model.number="num" /> <button @click="addN">+n</button>
  </div>
</template>

<script>
import {ref} from 'vue';
export default {
  name: "User",
  setup(props, {emit}) {
    let num = ref(0)
    function increment(){ 
      emit('add');
    }
    function decrement(){ 
      emit('sub')
    }
    function addN(){        
      console.log(num.value)        
      emit('addN',num.value)  
    }
    return {num, increment, decrement, addN}
  }
}
</script>

引入上述组件时,对事件进行监听

<template>
  <div>
    {{ counter }}
    <counter-com @add="increment" @sub="decrement" @addN="addN"> </counter-com>
  </div>
</template>
<script>
import CounterCom from "./views/User.vue";
import {ref} from 'vue';
export default {
  components: { CounterCom },
  setup(){
    let counter = ref(0);
    function increment() {
      counter.value++;
    }
    function decrement() {
      counter.value--;
    }
    function addN(num) {
      counter.value += num;
    }
    return {
      counter,
      increment,
      decrement,
      addN
    }
  }
};
</script>

自定义事件的时候,我们也可以传递一些参数给父组件:

emit('addN',num.value)

7.3 父子组件相互访问–父组件访问子组件$refs

主要步骤:

  • 给子组件绑定一个ref信息。
  • 在父组件通过事件,在回调函数中通过refs获取这个ref信息。
    <div id="app">
        <!--  给子组件绑定一个ref-->
        <subcom ref="info"></subcom>
        <button @click="getChildrenCom">获取子组件</button>
    </div>
    <template id="sub">
        <h3>这是一个子组件</h3>
        <button @click="clickSub">点击了子组件的按钮</button>
    </template>
    <script>
        //1、创建一个子组件
        const sub = {
            data() {
                return {
                    msg: '子组件的信息'
                }
            },
            methods: {
                printinfo() {
                    console.log('来自子组件中的方法')
                },
                clickSub() {
                    alert('点击了子组件的按钮')
                }
            },
            template: '#sub'
        }
        //这里是父组件
        const app = Vue.createApp({
            data() {
                return {
                    msg: '默认信息'
                }
            },
            methods: {
                getChildrenCom() {
                    //this.$refs获取子组件的数据
                    console.log(this.$refs.info.msg)
                    this.$refs.info.printinfo()
                }
            },
            components: {
                'subcom': sub
            }

        })
        app.mount('#app')
    </script>

7.4 父子组件相互访问–子组件访问父组件$parent

一般不建议子组件访问父组件,会破坏父组件的复用性。

    <div id="app">
        <sub-Box></sub-Box>
    </div>
    <!--把模板标签化了-->
    <template id="subbutton">
        <h3>这是第一个subbutton子组件</h3>
        <button @click="subtbclickTime">点击了按钮{{count}}次</button>
    </template>
    <!--把模板标签化了-->
    <template id="subBox">
        <p>这是subBox组件</p>
        <subbuttoncom></subbuttoncom>
    </template>
    <script>
        //1、创建一个子组件
        const subbutton = {
            data() {
                return {
                    count: 0
                }
            },
            methods: {
                subtbclickTime() {
                    this.count++
                    //子组件访问父组件
                    // console.log(this.$parent.parentInfo)
                    // console.log(this.$parent.$parent)

                    //子组件访问root符
                    // console.log(this.$root)
                    console.log(this.$root.msg)
                }
            },
            template: '#subbutton'

        }

        //2、再创建一个子组件
        const subBox = {
            data() {
                return {
                    parentInfo: '父组件的默认信息'
                }
            },
            components: {
                'subbuttoncom': subbutton
            },
            template: '#subBox',
        }
        //这里是根组件
        const app = Vue.createApp({
            data() {
                return {
                    msg: '这是根组件的默认信息'
                }
            },
            components: {
                'subBox': subBox
            }
        })
        app.mount('#app')
    </script>

8.插槽

8.1 什么是插槽

插槽就是子组件提供给父组件使用的一个占位符,用表示,父组件可以在这个占位符中填充任何模板代码,如html,组件等,填充的内容会替换子组件标签。

或者这么说:slot的出现是为了父组件可以堂而皇之地在子组件中加入内容。

8.2 插槽的使用

8.2.1 匿名插槽(单个插槽、默认插槽)

匿名插槽就是没有设置name属性的插槽。可以放置在组件的任意位置。一个组件中只能有一个匿名插槽。作为找不到匹配的内容片段时的备用插槽。匿名插槽只能作为没有slot属性的元素的插槽。

<div class="child">
    <h1>子组件</h1>
    <slot name="head">头部默认值</slot>
    <slot name="body">主体默认值</slot>
    <slot>这是个匿名插槽(没有name属性),这串字符是匿名插槽的默认值。</slot>
 </div>
<div class="parent">
     <h1>父组件</h1>
     <child>
         <p slot="body">我是主体</p>
         <p>我是其他内容</p>
         <p slot="footer">我是尾巴</p>
     </child>
 </div>
父组件
子组件
头部默认值 (head的默认值被渲染:默认值只会在没有提供内容的时候被渲染。)
我是主体 (body的默认值被覆盖)
我是其他内容 (名插槽的默认值被覆盖)

注意:

<p slot="footer">我是尾巴</p>

被丢弃了,因为子组件中没有name="footer"的插槽与之匹配。如果子组件中的匿名插槽不存在,则<p>我是其他内容</p>也会被丢弃。

8.2.2 具名插槽

具有名字的插槽,名字通过属性name来定义。一个组件中可以有很多具名插槽,出现在不同的位置。

<!-- <base-layout>组件-->
<div class="container">
    <header>
        <slot name="header"></slot>
    </header>
    <main>
        <slot></slot>
    </main>
    <footer>
        <slot name="footer"></slot>
    </footer>
</div>

可以在一个 <template>元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供插槽名称,

<base-layout>
  <template v-slot:header>
    <h1>我是头header</h1>
  </template>

  <p>我是main的内容111</p>
  <p>我也是main的内容222</p>

  <template v-slot:footer>
    <p>我是footer</p>
  </template>
</base-layout>

注意:任何没有被包裹在带有 v-slot 的 template 中的内容都会被视为默认插槽的内容。一个不带 name 的 slot 插槽会带有隐含的名字 default 。如果你希望更明确一些,可以在一个 <template>中包裹默认插槽的内容:

<base-layout>
  <template v-slot:header>
    <h1>我是头header</h1>
  </template>

  <template v-slot:default>
    <p>我是main的内容111</p>
    <p>我也是main的内容222</p>
  </template>

  <template v-slot:footer>
      <p>我是footer</p>
  </template>
</base-layout>

注意 v-slot 只能添加在 <template> 上。

8.2.3 作用域插槽

<!-- <Child> 组件: -->
<template>
  <div>
    <h1>hey,我是组件Child的标题</h1>
    <slot></slot>
  </div>
</template>
<script>
export default {
  data() {
     return{
        childUser:{Name:"Tom",Age:23}
    }
}
</script>

当使用Child组件时,想访问Child中的数据childUser并且将其展示在插槽的位置:

<!-- 这是父组件<Father>哦-->
<div>
  <h1>hey,我是父组件Father的标题</h1>
  <Child>
    {{childUser.Name}},
    {{childUser.Age}}
  </Child>
</div>

然而上述代码不会正常工作,因为父级模板里的所有内容都是在父级作用域中编译的;子级模板里的所有内容都是在子 作用域中编译的。只有 <Child> 组件可以访问到 childUser,而我们提供的内容:

{{childUser.Name}}, {{childUser.Age}}

是在父级<Father>渲染的。为了让 childUser 在父级的插槽内容中可用,需要把 childUser 从 <Child> 子级作用域传递到 <Father>父级作用域。 做法就是将 childUser 作为 <slot> 元素的一个属性绑定上去:

<template>
  <div>
    <h1>hey,我是组件Child的标题</h1>
    <slot v-bind:childData="childUser"></slot>
  </div>
</template>
<script>
export default {
  data() {
     return{
        childUser:{Name:"Tom",Age:23}
    }
}
</script>

绑定在 <slot> 元素上的 属性childData被称为插槽 prop。 现在在父级作用域中,我们可以使用带值的 v-slot 来定义 插槽 prop 的名字:

<!-- 这是父组件哦-->
<div>
  <h1>hey,我是父组件Father的标题</h1>
  <Child>
    <template v-slot:default="slotProps">
      {{ slotProps.childData.Name}}
      {{ slotProps.childData.Age}}
    </template>
  </Child>
</div>

在这个例子中,我们将包含所有插槽 prop 的对象命名为slotProps,也可以自定义。 因为在上述情况下,被提供的内容只有默认插槽,组件的标签可以被当作插槽的模板来使用。这样我们就可以把 v-slot 直接用在组件上:

<!-- 这是父组件哦-->
<div>
  <h1>hey,我是父组件Father的标题</h1>
  <Child v-slot:default="slotProps">
      {{ slotProps.childData.Name}}
      {{ slotProps.childData.Age}}
  </Child>
</div>

但是默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:

<!-- 无效,会导致警告 -->
<Child  v-slot="slotProps">
  {{ slotProps.childData.Name }}
  <template v-slot:other="otherSlotProps">
    slotProps is NOT available here
  </template>
</Child >

只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template> 的语法:

<Child >
  <template v-slot:default="slotProps">
    {{ slotProps.childData.Name }}
  </template>

  <template v-slot:other="otherSlotProps">
    ...
  </template>
</Child>

8.2.4 解构插槽 prop

作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里,所以,这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。

<Child  v-slot="{ childData}">
  {{ childData.Name }}
</Child>

这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 childData重命名为 person:

<Child   v-slot="{ childData: person }">
  {{ person.Name }}
</Child  >

甚至可以定义默认内容,用于插槽 prop 是 undefined 的情形:

<Child   v-slot="{ childData= { Name: 'Guest' } }">
  {{ childData.Name }}
</Child >

v-onv-bind 一样,v-slot 也有缩写,即把 v-slot:替换为字符 #。例如 v-slot:header 可以被重写为 #header,和其它指令一样,该缩写只在其有参数的时候才可用。

如果你希望使用缩写的话,你必须始终以明确插槽名取而代之,default不可以省略:

<Child  #default="{ childData}">
  {{ childData.Name }}
</Child >

9.动态组件

  • 当组件之间切换的时候,有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。
  • 动态组件是根据数据的变化,结合component标签,来动态切换组件的显示代码。实际就是动态调整组件名称。 使用动态组件:is=‘动态变量’来实现组件的动态呈现,实际上就是让component的名称动态来改变。
<body>
    <div id="app">
    </div>
    <script>
        //这里是根组件
        const app = Vue.createApp({
            data() {
                return {
                    //这里的开关变量的值是组件的名称
                    showItem: 'mybutton' //还可以是myinput
                }
            },
            methods: {
                changeHandler() {
                    if (this.showItem === 'mybutton') {
                        this.showItem = 'myinput'
                    } else {
                        this.showItem = 'mybutton'
                    }
                }
            },
            //这里使用:is="数据名"来完成动态显示的效果,对于失活组件用keep-alive来缓存之前输入的数据
            template: `      
                <keep-alive>
                    <component :is="showItem"></component>
                </keep-alive>
                <button @click="changeHandler">切换</button>
                `
            })
            //1、创建一个子组件
            app.component('mybutton', {
                template: `<button>按钮全局组件</button>`
            })
            app.component('myinput', {
                template: `<input type="text" placeholder="请输入一些东西"/>`
            })
            app.mount('#app')
    </script>
</body>

10.父子组件props传值的动态绑定传参

<body>
    <div id="app">
        <subcom></subcom>
    </div>
    <script>
        //创建一个子组件
        const sub = {
            data() {
                return {
                    mytotal: this.total
                }
            },
            props: ['number1', 'number2', 'number3', 'number4'],
            template: `
                        <h3>我是一个子组件</h3>
                        <div>数据1:{{number1}}</div>
                        <div>数据2:{{number2}}</div>
                        <div>数据3:{{number3}}</div>
                        <div>数据4:{{number4}}</div>
                      `
        }
        //这里是根组件
        const app = Vue.createApp({
            data() {
                return {
                    numberObj: {
                        number1: 100,
                        number2: 200,
                        number3: 300,
                        number4: 400,
                    }
                }
            },
            components: {
                'subcom': sub
            },
            template: `
                <subcom v-bind="numberObj"></subcom>
            `
        })
        app.mount('#app')
    </script>
</body

11.no-props父组件向子组件传递标签属性

non-prop属性:父组件给子组件传递内容时,子组件不通过pros接收时,情况如下:

  • 1.子组件存在单个根节点时
    • 底层会将父组件传递过来的内容置于子组件最外层dom标签上,变成子组件最外层dom标签的一个属性
    • 如果不希望在子组件的标签上展示该属性,可以通过inheritAttrs: false,不继承父组件传过来的non-props属性
  • 2.子组件存在多个根节点时
    • 如果希望父组件传递过来的non-props指令生效,
    • 可以通过v-bind指令,v-bind='$attrs',把父组件传递过来的所有non-props属性放到指定div上,
    • 也可以指定具体属性,如:msg='$attrs.msg'
  • 3.不管inheritAttrs为true或者false,子组件中都能通过$attrs属性获取到父组件中传递过来的属性,默认值为true。
<body>
    <div id="app">
    </div>
    <script>
        //创建一个子组件
        const sub = {
            props: ['info'],
            template: `
        <h3>我是子组件--{{info}}</h3>
        <div :info="$attrs.info">我是子组件--{{info}}</div>
        <div :style="$attrs.style">我是子组件--{{info}}</div>
        <div v-bind="$attrs">我是子组件--{{info}}</div>
       `
        }
        //这里是根组件
        const app = Vue.createApp({
            components: {
                'subcom': sub
            },
            //父组件传递给子组件
            template: `
        <subcom info="test attrs" style="width: 200px; height: 100px; background-color: red"></subcom>
       `
        })
        app.mount('#app')
    </script>
</body>

五、ES6 Promise 用法

1.ES6 Promise 先拉出来遛遛

复杂的概念先不讲,我们先简单粗暴地把Promise用一下,有个直观感受。那么第一个问题来了,Promise是什么玩意呢?是一个类?对象?数组?函数?   别猜了,直接打印出来看看吧,console.dir(Promise),就这么简单粗暴。 image.png 这么一看就明白了,Promise是一个构造函数,自己身上有all、reject、resolve这几个眼熟的方法,原型上有then、catch等同样很眼熟的方法。这么说用Promise new出来的对象肯定就有then、catch方法喽,没错。   那就new一个玩玩吧。

var p = new Promise(function(resolve, reject){
    //做一些异步操作
    setTimeout(function(){
        console.log('执行完成');
        resolve('随便什么数据');
    }, 2000);
});

Promise的构造函数接收一个参数,是函数,并且传入两个参数:resolve,reject,分别表示异步操作执行成功后的回调函数和异步操作执行失败后的回调函数。其实这里用“成功”和“失败”来描述并不准确,按照标准来讲,resolve是将Promise的状态置为fullfiled,reject是将Promise的状态置为rejected。不过在我们开始阶段可以先这么理解,后面再细究概念。

在上面的代码中,我们执行了一个异步操作,也就是setTimeout,2秒后,输出“执行完成”,并且调用resolve方法。运行代码,会在2秒后输出“执行完成”。注意!我只是new了一个对象,并没有调用它,我们传进去的函数就已经执行了,这是需要注意的一个细节。所以我们用Promise的时候一般是包在一个函数中,在需要的时候去运行这个函数,如:

function runAsync(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('执行完成');
            resolve('随便什么数据');
        }, 2000);
    });
    return p;            
}
runAsync()

这时候你应该有两个疑问:1.包装这么一个函数有毛线用?2.resolve('随便什么数据');这是干毛的? 我们继续来讲。在我们包装好的函数最后,会return出Promise对象,也就是说,执行这个函数我们得到了一个Promise对象。还记得Promise对象上有then、catch方法吧?这就是强大之处了,看下面的代码

runAsync().then(function(data){
    console.log(data);
    //后面可以用传过来的数据做些其他操作
    //......
});

在runAsync()的返回上直接调用then方法,then接收一个参数,是函数,并且会拿到我们在runAsync中调用resolve时传的的参数。运行这段代码,会在2秒后输出“执行完成”,紧接着输出“随便什么数据”。 这时候你应该有所领悟了,原来then里面的函数就跟我们平时的回调函数一个意思,能够在runAsync这个异步任务执行完成之后被执行。这就是Promise的作用了,简单来讲,就是能把原来的回调写法分离出来,在异步操作执行完后,用链式调用的方式执行回调函数。 你可能会不屑一顾,那么牛逼轰轰的Promise就这点能耐?我把回调函数封装一下,给runAsync传进去不也一样吗,就像这样:

function runAsync(callback){
    setTimeout(function(){
        console.log('执行完成');
        callback('随便什么数据');
    }, 2000);
}
 
runAsync(function(data){
    console.log(data);
});

效果也是一样的,还费劲用Promise干嘛。那么问题来了,有多层回调该怎么办?如果callback也是一个异步操作,而且执行完后也需要有相应的回调函数,该怎么办呢?总不能再定义一个callback2,然后给callback传进去吧。而Promise的优势在于,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。

2.链式操作的用法

所以,从表面上看,Promise只是能够简化层层回调的写法,而实质上,Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用,它比传递callback函数要简单、灵活的多。所以使用Promise的正确场景是这样的:

runAsync1()
.then(function(data){
    console.log(data);
    return runAsync2();
})
.then(function(data){
    console.log(data);
    return runAsync3();
})
.then(function(data){
    console.log(data);
});

这样能够按顺序,每隔两秒输出每个异步回调中的内容,在runAsync2中传给resolve的数据,能在接下来的then方法中拿到。运行结果如下:

image.png 猜猜runAsync1、runAsync2、runAsync3这三个函数都是如何定义的?没错,就是下面这样(代码较长请自行展开):

function runAsync1(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('异步任务1执行完成');
            resolve('随便什么数据1');
        }, 1000);
    });
    return p;            
}
function runAsync2(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('异步任务2执行完成');
            resolve('随便什么数据2');
        }, 2000);
    });
    return p;            
}
function runAsync3(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            console.log('异步任务3执行完成');
            resolve('随便什么数据3');
        }, 2000);
    });
    return p;            
}

在then方法中,你也可以直接return数据而不是Promise对象,在后面的then中就可以接收到数据了,比如我们把上面的代码修改成这样:

runAsync1()
.then(function(data){
    console.log(data);
    return runAsync2();
})
.then(function(data){
    console.log(data);
    return '直接返回数据';  //这里直接返回数据
})
.then(function(data){
    console.log(data);
});

那么输出就变成了这样:

image.png

3.reject的用法

到这里,你应该对“Promise是什么玩意”有了最基本的了解。那么我们接着来看看ES6的Promise还有哪些功能。我们光用了resolve,还没用reject呢,它是做什么的呢?事实上,我们前面的例子都是只有“执行成功”的回调,还没有“失败”的情况,reject的作用就是把Promise的状态置为rejected,这样我们在then中就能捕捉到,然后执行“失败”情况的回调。看下面的代码。

function getNumber(){
    var p = new Promise(function(resolve, reject){
        //做一些异步操作
        setTimeout(function(){
            var num = Math.ceil(Math.random()*10); //生成1-10的随机数
            if(num<=5){
                resolve(num);
            }
            else{
                reject('数字太大了');
            }
        }, 2000);
    });
    return p;            
}
 
getNumber()
.then(
    function(data){
        console.log('resolved');
        console.log(data);
    }, 
    function(reason){
        console.log('rejected');
        console.log(reason);
    }
);

getNumber函数用来异步获取一个数字,2秒后执行完成,如果数字小于等于5,我们认为是“成功”了,调用resolve修改Promise的状态。否则我们认为是“失败”了,调用reject并传递一个参数,作为失败的原因。 运行getNumber并且在then中传了两个参数,then方法可以接受两个参数,第一个对应resolve的回调,第二个对应reject的回调。所以我们能够分别拿到他们传过来的数据。多次运行这段代码,你会随机得到下面两种结果:

image.png

4.catch的用法

我们知道Promise对象除了then方法,还有一个catch方法,它是做什么用的呢?其实它和then的第二个参数一样,用来指定reject的回调,用法是这样:

getNumber()
.then(function(data){
    console.log('resolved');
    console.log(data);
})
.catch(function(reason){
    console.log('rejected');
    console.log(reason);
});

效果和写在then的第二个参数里面一样。不过它还有另外一个作用:在执行resolve的回调(也就是上面then中的第一个参数)时,如果抛出异常了(代码出错了),那么并不会报错卡死js,而是会进到这个catch方法中。请看下面的代码:

image.png 也就是说进到catch方法里面去了,而且把错误原因传到了reason参数中。即便是有错误的代码也不会报错了,这与我们的try/catch语句有相同的功能。

5.all的用法

Promise的all方法提供了并行执行异步操作的能力,并且在所有异步操作执行完后才执行回调。我们仍旧使用上面定义好的runAsync1、runAsync2、runAsync3这三个函数,看下面的例子:

Promise
.all([runAsync1(), runAsync2(), runAsync3()])
.then(function(results){
    console.log(results);
});

用Promise.all来执行,all接收一个数组参数,里面的值最终都算返回Promise对象。这样,三个异步操作的并行执行的,等到它们都执行完后才会进到then里面。那么,三个异步操作返回的数据哪里去了呢?都在then里面呢,all会把所有异步操作的结果放进一个数组中传给then,就是上面的results。所以上面代码的输出结果就是:

image.png 有了all,你就可以并行执行多个异步操作,并且在一个回调中处理所有的返回数据,是不是很酷?有一个场景是很适合用这个的,一些游戏类的素材比较多的应用,打开网页时,预先加载需要用到的各种资源如图片、flash以及各种静态文件。所有的都加载完后,我们再进行页面的初始化。

六、axios

1.axios简介

基于promise,用于浏览器和node.js的http客户端

2.特点

  • 支持浏览器和 node.js
  • 支持 promise
  • 能拦截请求和响应
  • 能转换请求和响应数据
  • 能取消请求
  • 自动转换 JSON 数据
  • 浏览器端支持防止 CSRF (跨站请求伪造)

3.安装

利用 npm 安装 npm install axios --save

4.例子

4.1 发送一个GET请求

//通过给定的ID来发送请求
axios.get('/user?ID=12345')
  .then(function(response){
    console.log(response);
  })
  .catch(function(err){
    console.log(err);
  });
//以上请求也可以通过这种方式来发送
axios.get('/user',{
  params:{
    ID:12345
  }
})
.then(function(response){
  console.log(response);
})
.catch(function(err){
  console.log(err);
});

4.2 发送一个POST请求

axios.post('/user',{
  firstName:'Fred',
  lastName:'Flintstone'
})
.then(function(res){
  console.log(res);
})
.catch(function(err){
  console.log(err);
});

4.3 一次性并发多个请求

function getUserAccount(){
  return axios.get('/user/12345');
}
function getUserPermissions(){
  return axios.get('/user/12345/permissions');
}
axios.all([getUserAccount(),getUserPermissions()])
  .then(axios.spread(function(acct,perms){
    //当这两个请求都完成的时候会触发这个函数,两个参数分别代表返回的结果
  }))

5.axios的API

5.1 axios可以通过配置(config)来发送请求

5.1.1 axios(config)

//发送一个`POST`请求
axios({
    method:"POST",
    url:'/user/12345',
    data:{
        firstName:"Fred",
        lastName:"Flintstone"
    }
});
5.1.2 axios(url[,config])
//发送一个`GET`请求(默认的请求方式)
axios('/user/12345');

5.2 请求方式的别名,这里对所有已经支持的请求方式都提供了方便的别名

axios.request(config);
axios.get(url[,config]);
axios.delete(url[,config]);
axios.head(url[,config]);
axios.post(url[,data[,config]]);
axios.put(url[,data[,config]])
axios.patch(url[,data[,config]])

注意:当我们在使用别名方法的时候,url,method,data这几个参数不需要在配置中声明

5.3 并发请求(concurrency)

//iterable是一个可以迭代的参数如数组等
axios.all(iterable)
//callback要等到所有请求都完成才会执行
axios.spread(callback)

5.4 创建一个axios实例,并且可以自定义其配置

5.4.1 axios.create([config])
var instance = axios.create({
  baseURL:"https://some-domain.com/api/",
  timeout:1000,
  headers: {'X-Custom-Header':'foobar'}
});
5.4.2 实例的方法

以下是实例方法,注意已经定义的配置将和利用create创建的实例的配置合并。

axios#request(config)
axios#get(url[,config])
axios#delete(url[,config])
axios#head(url[,config])
axios#post(url[,data[,config]])
axios#put(url[,data[,config]])
axios#patch(url[,data[,config]])

6.默认值

你可以设置默认配置,对所有请求都有效

6.1 全局默认配置

axios.defaults.baseURL = 'http://api.exmple.com';
axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;
axios.defaults.headers.post['content-Type'] = 'appliction/x-www-form-urlencoded';

6.2 自定义的实例默认设置

//当创建实例的时候配置默认配置
var instance = axios.create({
    baseURL: 'https://api.example.com'
});

//当实例创建时候修改配置
instance.defaults.headers.common["Authorization"] = AUTH_TOKEN;

6.3 配置中的有优先级

config配置将会以优先级别来合并,顺序是lib/defauts.js中的默认配置,然后是实例中的默认配置,最后是请求中的config参数的配置,越往后等级越高,后面的会覆盖前面的例子。

//创建一个实例的时候会使用libray目录中的默认配置
//在这里timeout配置的值为0,来自于libray的默认值
var instance = axios.create();
//覆盖掉library的默认值
//现在所有的请求都要等2.5S之后才会发出
instance.defaults.timeout = 2500;
//这里的timeout回覆盖之前的2.5S变成5s
instance.get('/longRequest',{
  timeout: 5000
});

7.拦截器

7.1 请求和响应拦截器

你可以在请求、响应在到达then/catch之前拦截他们

//添加一个请求拦截器
axios.interceptors.request.use(function(config){
  //在请求发出之前进行一些操作
  return config;
},function(err){
  //Do something with request error
  return Promise.reject(error);
});
//添加一个响应拦截器
axios.interceptors.response.use(function(res){
  //在这里对返回的数据进行处理
  return res;
},function(err){
  //Do something with response error
  return Promise.reject(error);
})

7.2 给自定义的axios实例添加拦截器

var instance = axios.create();
instance.interceptors.request.use(function(){})