【新鲜出炉,持续更新】Vue教程

视频源

视频地址

Vue中的MVVM

Vue的MVVM模式

View层

  • 视图层
  • 在前端开发中,通常就是DOM层
  • 主要作用是给用户展示各种信息

Model层

  • 数据层
  • 数据可能是固定的死数据,更可能是来自服务器的,从网络上请求下来的数据

ViewModel层【核心】

  • 视图模型层
  • 是 View 和 Model 沟通的桥梁
  • 其实就是Vue实例
  • 一方面,它实现了 Data Binding(数据绑定),将 Model 的改变实时的反映到了 View 中
  • 另一方面,它实现了 DOM Listener(DOM 监听),当DOM发生一些事件(如点击、滚动等)时,可以监听到这些事件,并在需要的情况下改变对应的 Data
<body>
    
<!-- ------------- 此处为 View ------------- -->
<div id="app">
    <h2>当前计数:{{counter}}</h2>
    <button v-on:click="add">+</button>
    <button v-on:click="sub">-</button>
</div>
<!-- --------------------------------------- -->
    
<script src="vue.js"></script>
<script>
    
    // ---- 此处为 Model ---- //
    const obj = {
        counter: 0
    }
    // ------------------- //
    
    const app = new Vue({ // ⬅此处为 ViewModel
        el: "#app",
        data: obj,
        methods: {
            add: function () {
                this.counter++;
            },
            sub: function () {
                this.counter--;
            }
        }
    })
</script>
</body>

Vue示例中的options

  • el
    • 类型:string | HTMLElement
    • 作用:决定之后Vue实例会管理哪个DOM
  • data
    • 类型:Object | Function(组件中data必须是一个函数)
    • 作用:Vue实例对应的数据对象
  • methods
    • 类型:{[key: string]: Function}
    • 作用:定义Vue的方法,可以在其他地方调用,也可以在指令中调用

Vue的生命周期

插值操作

Mustache语法

mustache:胡须

<div id="app">
  <h2>{{message}}</h2> //⬅Mustache语法
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
      el: "#app",
      data: {
        message: "你好"
      },
  })
</script>

v-once 指令

该指令表示元素和组件只渲染一次,不会随着数据的改变而改变。

该指令后面不需要跟任何表达式。

<div id="app">
  <h2>{{message}}</h2>
  <h2 v-once>{{message}}</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
      el: "#app",
      data: {
          message: "你好"
      },
  })
  app.message = "我修改了message!"
</script>
v-once

v-html 指令

该指令会解析渲染后面跟着的string。

该指令后面往往会跟上一个string。

<div id="app">
<h2 v-html="url"></h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      url: '<a href="https://www.bilibili.com">Bilibili干杯!</a>'
    }
  })
</script>
v-html

v-text 指令

与Mustache类似,但是会覆盖掉innerText内容哦。

<div id="app">
  <h2>{{message}},嘎嘎</h2>
  <h2 v-text="message">,嘎嘎</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
      el: "#app",
      data: {
        message: "你好"
      },
  })
</script>
v-text

v-pre 指令

该指令用于跳过这个元素和它子元素的编译过程,用于显示原本的Mustache语法。

<div id="app">
  <h2>{{message}}</h2>
  <h2 v-pre>{{message}}</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      message: "你好"
    },
  })
</script>
v-pre

v-cloak 指令

cloak:斗篷

可以通过为该指令提供相应的样式,来对未编译就显示出来的Mustache语法进行修饰。

原理就是,在编译完成后,div标签中的v-cloak会消失。

<style>
  [v-cloak] {
    display: none;
  }
</style>

<body>
  <div id="app">
    <h2>{{message}}</h2>
    <h2 v-cloak>{{message}}</h2>
  </div>

  <script src="../vue.js"></script>
  <script>
    setTimeout(function (){
      const app = new Vue({
        el: "#app",
        data: {
          message: "你好"
        },
      })
    }, 1000);
  </script>
</body>
v-cloak

绑定属性 v-bind

前面我们介绍的是将数据插入进模板的内容中。其实我们除了需要动态决定内容,有时也需要动态地来绑定元素的属性。

例如:

  • 动态绑定a元素的href属性
  • 动态绑定img元素的src属性

这时,就可以使用v-bind指令:

  • 作用:动态绑定属性
  • 缩写:

v-bind 的基本使用

<div id="app">
  <img v-bind:src="imgURL">
<!--  语法糖写法  -->
  <img :src="imgURL">
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
      el: "#app",
      data: {
          imgURL: "https://gagalab.tech/images/avatar.png"
      },
  })
</script>

v-bind 动态绑定class(对象语法)

<style>
  .red{
    color: red;
  }
  .grow{
    font-size: 50px;
  }
</style>
<body>
  <div id="app">
    <h2 :class="{red: isRed, grow: isGrow}">{{message}}</h2>
    <button v-on:click="turnRed">变红</button>
    <button v-on:click="grow">变大</button>
  </div>

  <script src="../vue.js"></script>
  <script>
    const app = new Vue({
      el: "#app",
      data: {
        message: "你好",
        isRed: false,
        isGrow: false,
      },
      methods: {
        turnRed: function (){
          this.isRed = !this.isRed;
        },
        grow: function (){
          this.isGrow = !this.isGrow;
        }
      }
    })
  </script>
</body>
v-bind动态绑定class(对象语法)

如果嫌HTML代码冗长,也可以将class的值放入methods或computed中:

...
<h2 :class="getClasses()">{{message}}</h2>
...
methods: {
  getClasses: function (){
    return {red: this.isRed, grow: this.isGrow};
  }
}
...

v-bind 动态绑定class(数组语法)

<style>
  .red{
    color: red;
  }
  .grow{
    font-size: 50px;
  }
</style>
<body>
  <div id="app">
    <h2 :class="[class1, class2]">{{message}}</h2>
  </div>

  <script src="../vue.js"></script>
  <script>
    const app = new Vue({
      el: "#app",
      data: {
        message: "你好",
        class1: "red",
        class2: "grow"
      }
    })
  </script>
</body>
v-bind动态绑定class(数组语法)

同样的,如果嫌HTML代码冗长,也可以将class的值放入methods或computed中。

v-bind 动态绑定style(对象语法)

在我们在后续开发中,肯定要使用模板,当我们想要改变模板的样式时,就可以使用v-bind来动态绑定style

<div id="app">
  <h2 :style="{color: messageColor, fontSize: messageFontSize+'px'}">{{message}}</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      message: "你好",
      messageColor: "red",
      messageFontSize: 50
    },
  })
</script>

效果如上。

v-bind 动态绑定style(数组语法)

<div id="app">
  <h2 :style="[messageColor, messageFontSize]">{{message}}</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      message: "你好",
      messageColor: {color: 'red'},
      messageFontSize: {fontSize: '50px'}
    },
  })
</script>

效果如上。

计算属性 computed

就像下面的例子,如果在开发中,我们反复使用像第二行的代码,就会使代码的可读性变差,也显得冗长。这时就可以使用计算属性,像第三行一样,来使代码更加简洁清晰。

计算属性的基本使用

<div id="app">
  <h2>{{firstName}} {{lastName}}</h2>
  <h2>{{fullName}}</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      firstName: "Leslie",
      lastName: "Cheung"
    },
    computed: {
      fullName: function (){
        return this.firstName + ' ' + this.lastName;
      }
    }
  })
</script>
计算属性的基本使用

计算属性的复杂操作

<div id="app">
  <h2>书的总价为 {{totalPrice}}</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      books: [
        {id: 0, price: 93},
        {id: 1, price: 65},
        {id: 2, price: 123},
        {id: 3, price: 89}
      ]
    },
    //⬇ methods一般用动词命名,computed用名词 
    computed: {
      totalPrice: function (){
        let totalPrice = 0;
        for (let i=0; i<this.books.length; i++){
          totalPrice += this.books[i].price;
        }
        return totalPrice;
      }
    }
  })
</script>
计算属性的复杂操作

计算属性的getter和setter

<div id="app">
  <h2>{{fullName}}</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      firstName: "Leslie",
      lastName: "Cheung"
    },
    computed: {
      //计算属性一般没有set方法,因为它是只读属性
      fullName: {
        get: function (){
          return this.firstName + ' ' + this.lastName;
        },
        // set: function (value){
        //   let name = value.split(' ');
        //   this.firstName = name[0];
        //   this.lastName = name[1];
        // }
      }
      //以上写法等价于(这也就是在Mustache语法中fullName不加()的原因):
      // fullName: function (){
      //   return this.firstName + ' ' + this.lastName;
      // }
    }
  })
</script>

计算属性和methods的对比

其实,我们使用methods也可以实现我们上述的功能,那么为什么我们要使用计算属性呢?

原因:计算属性会进行缓存,如果使用多次,计算属性只会调用一次,而methods会调用多次。

看了下面的例子,你就懂啦!

<div id="app">
  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>
  <h2>{{getFullName()}}</h2>
  <h2>{{getFullName()}}</h2>
  <h2>{{getFullName()}}</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      firstName: "Leslie",
      lastName: "Cheung"
    },
    computed: {
      fullName: function (){
        console.log("Invoking fullName");
        return this.firstName + ' ' + this.lastName;
      }
    },
    methods: {
      getFullName: function (){
        console.log("Invoking getFullName()")
        return this.firstName + ' ' + this.lastName;
      }
    }
  })
</script>
计算属性和methods的对比结果

事件监听 v-on

  • 作用:绑定事件监听器
  • 缩写@

v-on 的基本使用

v-on指令已经很熟悉啦,在生命周期那一节用到的计数器中,就有使用。

<div id="app">
  <h2>当前计数:{{counter}}</h2>
  <button @click="counter++">+</button>
  <button @click="counter--">-</button>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      counter: 0
    },
  })
</script>

聪明如你,肯定也知道当@click后面跟的表达式过于复杂的话,可以封装成函数,放进methods中调用。

v-on 的参数问题

  1. 当methods中定义的方法不需要额外的参数时,@click后面的方法可以不加()
<div id="app">
  <button @click="onClick">按钮</button>
  <!--等价于-->
  <button @click="onClick">按钮</button>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    methods: {
      onClick(){
        ...
      }
    }
  })
</script>
  1. 当methods中定义的方法需要额外的参数时,

    1. 老老实实传入参数:
    <div id="app">
      <button @click="onClick(message)">按钮</button>
    </div>
    
    <script src="../vue.js"></script>
    <script>
      const app = new Vue({
        el: "#app",
        data: {
          message: "Click!",
        },
        methods: {
          onClick(message){
            console.log(message);
          }
        }
      })
    </script>
    1. 写了(),但不传入参数:用上面的例子的话,会打印undefined
    2. ()都不写了:用上面的例子的话,会打印event(点击事件)
    event打印结果

那么问题来了,我们要如何既传参,又获取event呢?

此时我们可以通过$event来传入事件:

<div id="app">
  <button @click="onClick(message, $event)">按钮</button>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      message: "Click!",
    },
    methods: {
      onClick(message, event){
        console.log("传入的message:" + message);
        console.log("点击事件:" + event);
      }
    }
  })
</script>

$event

v-on 的修饰符

Vue提供了一些修饰符来方便我们处理事件:

  • .stop:调用event.stopPropagation(),阻止冒泡
  • .prevent:调用event.preventDefault(),阻止默认行为
  • .{keyCode | keyAlias}:只有当特定键盘按键被触发时才会触发回调
    • keyCode:键代码
    • keyAlias:键别名
  • .native:监听组件根元素的原生事件
  • .once:该事件只触发一次回调

.stop 阻止冒泡

原理:.stop调用event.stopPropagation()

<div id="app">
  <div @click="onClickDiv">
    <button @click="onClickBtn">按钮</button>
  </div>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    methods: {
      onClickBtn(){
        console.log("Button is clicked!");
      },
      onClickDiv(){
        console.log("Div is clicked!");
      }
    }
  })
</script>
.stop修饰符

可见,当我们点击按钮时,onClickDiv也被触发了,为了防止发生这样的情况,我们可以使用.stop修饰符:

<div id="app" @click="onClickDiv">
  <button @click.stop="onClickBtn">按钮</button>
</div>

.prevent 阻止默认行为

原理.prevent调用event.preventDefault()

<div id="app">
  <form action="action">
    <input type="submit" value="提交" @click="onClickSubmit">
  </form>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    methods: {
      onClickSubmit(){
        console.log("Submit!");
      }
    }
  })
</script>

当我们点击页面中的提交按钮时,它会因为action="action"跳转至另一个页面,如果我们想手动让它进行跳转,就可以使用.prevent修饰符来prevent掉默认的行为。

<form action="action">
  <input type="submit" value="提交" @click.prevent="onClickSubmit">
</form>

.{keyCode | keyAlias} 键修饰符

<div id="app">
  <input type="text" @keyup.enter="onKeyUp">
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    methods: {
      onKeyUp(){
        console.log("Key Up!");
      }
    }
  })
</script>

上面的代码,会在用户输入按任意键的时候打印“Key Up!”,但有时我们需要当用户在按某个(键盘)按键时,才触发某个事件,这时我们可以使用键修饰符:

<input type="text" @keyup.enter="onKeyUp">

此时,只有用户在敲击“Enter”键时,才会打印“Key Up!”。

.native

TODO: .native修饰符待填坑

.once 仅触发一次修饰符

在某些特殊的情况下,如想要一个按钮在被点击时,只触发一次回调,就可以使用.once修饰符:

<button @click.once="onClick">按钮</button>

条件判断

v-if 的使用

<div id="app">
  <h2 v-if="show">我将被显示哦~</h2>
  <h2 v-if="notShow">我就不显示啦!</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      show: true,
      notShow: false,
    },
  })
</script>
v-if显示结果

v-else 的使用

<div id="app">
  <h2 v-if="isShow">isShow为true时显示我!</h2>
  <h2 v-else>isShow为false时显示我!</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      isShow: true,
    },
  })
</script>
v-else显示结果

添加这一句:

app.isShow = false;

将显示:

v-else显示结果2

v-else-if 的使用

<div id="app">
  <h2 v-if="score >= 90">优秀</h2>
  <h2 v-else-if="score >= 80">良好</h2>
  <h2 v-else-if="score >=60">及格</h2>
  <h2 v-else>不及格</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      score: 87
    },
  })
</script>

不过还是不建议向上面这样写,使用computed属性会更好哦~

案例 - 切换登录方式

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<div id="app">
  <span v-if="doUseUsername">
    <label for="username">用户名</label>
    <input type="text" id="username" placeholder="用户名">
  </span>
  <span v-else>
    <label for="email">邮箱</label>
    <input type="email" id="email" placeholder="邮箱">
  </span>
  <button @click="doUseUsername = !doUseUsername">切换登录方式</button>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      doUseUsername: true
    },
  })
</script>
</body>
</html>
GIF 2021-6-29 11-31-20

Vue的虚拟DOM

做到这里,其实是有一个小问题的。例如,当用户在用户名输入框输入了“Jack”后,点击切换登录方式的按钮,我们可以发现“Jack”仍在输入框中。

小问题

有没有发现不对,上面的代码中我们明明创建了两个完全不同的input,为什么用户的输入会留在另一个输入框中呢?

这里我们就需要引入**Vue的虚拟DOM(Virtual DOM)**的概念。虚拟DOM的实现,使得我们可以在不直接操作DOM元素的情况下,只操作数据就能够重新渲染页面。为了提高浏览器的性能,Vue在进行虚拟DOM渲染时,会尽可能地复用已经存在的元素,而不是重新创建新的元素。

在本案例中,当doUseUsername被切换为false时,Vue实际上是直接使用了第一个<input>,所以“Jack”会被保留。

那么我们该如何避免这样的情况的发生呢?此时可以在<input>中添加一个属性:key。当两个<input>中的key的值不同时,Vue就不会复用input元素啦!如下:

<span v-if="doUseUsername">
    <label for="username">用户名</label>
    <input type="text" id="username" placeholder="用户名" key="username">
</span>
<span v-else>
    <label for="email">邮箱</label>
    <input type="email" id="email" placeholder="邮箱" key="email">
</span>
解决后

v-show的使用

v-if的用法是一样的,也用于决定一个元素是否被渲染。

v-if 与 v-show 的对比

既然v-ifv-show的作用相同,那么我们在开发中该如何选择呢?

我们编写下面的代码:

<div id="app">
  <h2 v-if="false">{{message}}</h2>
  <h2 v-show="false">{{message}}</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      message: "你好"
    },
  })
</script>

不出所料,页面上应该什么都没有显示。但是我们打开控制台,看看页面的 Elements,如下:

v-if与v-show的对比

可以看到:

  • v-if="false"时,DOM中根本就没有该元素
  • v-show="false"时,仅仅是将在该元素中增加了display: none;

所以,在开发中,我们应这样选择:

  • 选择 v-show:当元素需要频繁地在显示和隐藏中切换
  • 选择 v-if:当元素只需要切换一次

循环遍历

v-for 的使用

遍历数组

<div id="app">
  <!--01 - 获取数组元素值-->
  <ul>
    <li v-for="item in people">{{item}}</li>
  </ul>
  <!--02 - 获取数组下标-->
  <ul>
    <li v-for="(item, index) in people">{{index + 1}}. {{item}}</li>
  </ul>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      people: ["Jack", "Rose", "Tom", "Jerry"]
    },
  })
</script>

v-for遍历数组

遍历对象

<div id="app">
  <!--01 - 获取对象的value-->
  <ul>
    <li v-for="value in person">{{value}}</li>
  </ul>
  <!--02 - 获取对象的key-->
  <ul>
    <li v-for="(value, key) in person">{{key}}: {{value}}</li>
  </ul>
  <!--03 - 获取对象的index-->
  <ul>
    <li v-for="(value, key, index) in person">{{index + 1}}. {{key}}: {{value}}</li>
  </ul>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      person: {
        name: "Jack",
        sex: "male",
        age: 18,
      }
    },
  })
</script>

v-for遍历对象

📌 添加 key 属性的作用

官方推荐我们在使用v-for时,给对应的元素或组件添加 key 属性::key=""

原因如下:

正如上文(还记得登录方式切换案例中的小问题吗?)所述,Vue(和React都)实现了一套虚拟 DOM 结构,使得我们可以不直接操作 DOM 元素,只操作数据便可以重新渲染页面,而隐藏在背后的原理便是Vue高效的Diff 算法

Vue 和 React 的虚拟 DOM 的 Diff 算法大致相同,其核心是基于两个简单的假设:

  1. 两个相同的组件产生类似的DOM结构;两个不同的组件产生不同的DOM结构
  2. 同一层级的一组节点可以通过唯一的id进行区分

基于以上这两点假设,使得虚拟DOM的Diff算法的复杂度从O(n3)降到了O(n)。

针对假设中组件层级的说法,我们来看React’s diff algorithm中的一张图就可以明白:

节点层级

当页面数据发生变化时,Diff 算法只会比较同一层级的节点:

  • 如果节点类型相同,则重新设置该节点的属性,从而实现节点更新
  • 如果节点类型不同,则直接删掉旧的节点,创建并插入新的节点,然后不会再比较这个节点以后的子节点了

举个例子:

:key-例子-1

如上图,我们希望在B和C的中间插入一个F,Diff 算法默认执行起来是这样的:

:key-例子-1

我们可以发现,Diff 算法将C更新为F,D更新为C,E更新为D,最后插入了E,这效率也太低了吧!

所以官方才推荐我们使用 key 属性来作为每个节点的唯一标识,这样 Diff 算法就可以轻松地找到正确的位置来插入新的节点。

有无key的对比图

【总结】key的作用:

key 的作用主要是为了高效得更新虚拟 DOM。在Vue中,过渡切换相同标签名的元素时,应该使用key属性,目的是为了让Vue可以区分它们,否则Vue指挥替换其内部属性,而不会触发过渡效果。

key 的选取

我们来实践在 v-for 时添加绑定 key 属性。下面这个例子实现了上文所说的在B的后面插入F:

<div id="app">
  <ul>
    <li v-for="item in array">{{item}}</li>
  </ul>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      array: ["A", "B", "C", "D", "E"]
    },
  })
</script>

我们在控制台输入

app.array.splice(2, 0, "F");

以达到在B的后面插入F的目的。

那么问题来了,我们应该选谁来作为 key呢?

先说答案,在下面这个例子中,在array数组中的元素不重复的情况下,应该选用item来充当 key。

<div id="app">
  <ul>
    <li v-for="item in array" :key="item">{{item}}</li>
  </ul>
</div>

为什么不用index作为 key 呢?

如果我们用index来作为 key,那么想一想我们在B的后面添加F后,index如何变化?那还用说,肯定是F的下标为2,C的下标为3,以此类推。因为index永远应该是连续的,不可能是“0162345”,所以用index来作为 key 将起不到区分节点的作用。因此,在本例中,我们选用item来作为 key,当然也是在array数组中的元素不重复的情况下。

可响应式的数组方法

我们先看看什么是响应式(来自官方文档):

Vue 最独特的特性之一,是其非侵入性的响应式系统。

数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

那什么叫可响应式的数组方法?先看下面的例子:

<div id="app">
  <button @click="insert">插入元素</button>
  <ul>
    <li v-for="item in array">{{item}}</li>
  </ul>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      array: ["A", "B", "C", "D", "E"]
    },
    methods: {
      insert() {
        app.array.push("New Item");
      }
    }
  })
</script>
GIF 2021-6-29 17-38-59

你看!其实Array.push()方法就是可响应式的数组方法,当我们点击 button,向array中压入新元素,视图就自己更新了。下面我们修改一下insert()方法:

methods: {
    insert() {
        app.array[app.array.length] = "New Item"; //在array的后面插入"New Item"
    }
}

此时,我们点击 button,页面没有发生任何变化。但我们打开控制台,输出array,可以看到array确确实实发生了变化,但是视图并没有更新。这就说明,有些对数组的操作,并不是可响应式的。

image-20210629175411295

下面是可响应式的数组方法:

  • Array.prototype.push()
    • 语法:arr.push(element1, ..., elementN)
  • Array.prototype.pop()
    • 语法:arr.pop()
  • Array.prototype.shift():从数组中删除第一个元素,并返回该元素的值。
    • 语法:arr.shift()
  • Array.prototype.unshift():将一个或多个元素添加到数组的开头,并返回该数组的新长度
    • 语法:arr.unshift(element1, ..., elementN)
  • Array.prototype.splice():通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。
    • 语法:array.splice(start[, deleteCount[, item1[, item2[, ...]]]])(没法理解的话,不用理解哈哈哈哈,直接看说明就好)
    • 参数说明:
      • start:传要修改的开始位置(从0计数)
        • 超出了数组的长度时,从数组末尾开始添加内容
        • 负值时,从数组末位开始的第几位(从-1计数)
        • 负数的绝对值大于数组的长度时,表示开始位置为第0位。
      • deleteCount(可选):传要删除的元素个数
        • 被省略或大于start之后的元素的总数时,在start后面的元素都将被删除
        • 0或负数时,不移除元素
      • item1, item2, ...(可选):传要添加的元素;被省略,则将只进行删除操作
    • 操作说明:array.splice(在第几个元素的后面)
      • 删除元素:第二个参数传要删除的元素个数,不传则删除后面所有元素
      • 插入元素:第二个参数传0,第三个参数传要添加的元素
      • 替换元素:可以理解为先删除元素,再插入新元素
  • Array.prototype.sort()
    • 语法:arr.sort([compareFunction]):用原地算法对数组的元素进行排序,并返回数组。
    • 参数说明:
      • compareFunction(可选):用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符的Unicode位点进行排序。
        • 注意:如果要对数字进行排序,则必须传参!
    • 操作说明:
      • 升序(数字数组)numbers.sort((a, b) => a - b);
      • 降序(数字数组):numbers.sort((a, b) => b - a);
  • Array.prototype.reverse():将数组中元素的位置颠倒,并返回该数组。
    • 语法:arr.reverse()

表单绑定 v-model

v-model 的基本使用

Vue中使用v-model指令来实现表单元素和数据的双向绑定

什么是双向绑定呢?我们来看下面的例子,我们在<input中添加了v-model="message",这样就实现了双向绑定:

<div id="app">
  <input type="text" v-model="message">
  <h2>{{message}}</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      message: "你好"
    },
  })
</script>

本例中的双向绑定实际上就是:

  1. input元素中value的值向h2元素message中绑定

    v-model双向绑定-1
  2. h2元素中message的值向input元素中value绑定

v-model双向绑定-

当然,v-model 除了可以用在 input 元素中,同样也可以用在 textarea 元素中哦。

v-model 的原理

下面我们试试手动实现 v-model

第一步:input 元素中value的值向 h2 元素message中绑定【v-on 指令给 input 元素绑定 input 事件】

...
<input type="text" @input="inputVal">
...
	methods: {
      inputVal(event){
        this.message = event.target.value;
      }
...

这样看着是不是有点麻烦,还记得我们之前学过的$event嘛?我们现在对上面的代码进行下简化:

...
<input type="text" @input="message = $event.target.value;">
...

第二步:h2 元素中message的值向 input 元素中value绑定【v-bind 绑定 value 属性】

...
<input type="text" @input="message = $event.target.value;" :value="message">
...

这样就实现了和 v-model 相同的功能。

总结起来:

v-model 其实是一个语法糖,它本质上包含了两个操作:

  • v-bind 绑定 value 属性
  • v-on 指令给当前元素绑定 input 事件

v-model 结合 radio类型

<div id="app">
  <label for="male">
    <input type="radio" id="male" value="" v-model="sex"></label>
  <label for="female">
    <input type="radio" id="female" value="" v-model="sex"></label>
  <h2>性别:{{sex}}</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      sex: "男"
    },
  })
</script>
GIF 2021-6-30 14-11-55

注意:为了让两个 radio 互斥,我们以往使用的是添加name="sex",但是这里用了v-model="sex"就不需要使用 name 属性啦。

v-model 结合 checkbox类型

<div id="app">
  <!--单选框-->
  <div>
    <label for="license">同意
      <input id="license" type="checkbox" v-model="isAgree">
    </label>
    <button :disabled="!isAgree">下一步</button>
  </div>

  <!--多选框-->
  <div>
    <label for="swimming">
      <input id="swimming" type="checkbox" value="游泳" v-model="hobbies">游泳
    </label>
    <label for="soccer">
      <input id="soccer" type="checkbox" value="足球" v-model="hobbies">足球
    </label>
    <label for="basketBall">
      <input id="basketBall" type="checkbox" value="篮球" v-model="hobbies">篮球
    </label>
    <label for="running">
      <input id="running" type="checkbox" value="跑步" v-model="hobbies">跑步
    </label>
    <span>我的爱好:{{hobbies}}</span>
  </div>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      isAgree: false,
      hobbies: []
    },
  })
</script>

v-model 结合 select 元素

在 select 元素中添加 v-model。

<div id="app">
  <!--选择一个-->
  <div style="margin-bottom: 20px">
    <select v-model="fruit">
      <option value="苹果">苹果</option>
      <option value="香蕉">香蕉</option>
      <option value="葡萄">葡萄</option>
      <option value="草莓">草莓</option>
    </select>
  </div>

  <!--选择多个-->
  <div>
    <select name="fruits" v-model="fruits" multiple>
      <option value="苹果">苹果</option>
      <option value="香蕉">香蕉</option>
      <option value="葡萄">葡萄</option>
      <option value="草莓">草莓</option>
    </select>
    <span>我选择了:{{fruits}}</span>
  </div>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      fruit: '葡萄',
      fruits: []
    },
  })
</script>
image-20210630145024114

值绑定

其实在开发中,我们对于 input 元素的value通常不是固定的,需要动态绑定。以“v-model 结合 checkbox 类型”中举的例子为例,我们在data中,传入需要用户勾选的爱好,这时使用值绑定就可以动态的显示在页面中啦:

<div id="app">
  <label v-for="item in givenHobbies" :for="item">
    <input type="checkbox" :id="item" :value="item" v-model="hobbies">{{item}}
  </label>
  <span>我的爱好:{{hobbies}}</span>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      givenHobbies: ["游泳", "足球", "篮球", "跑步"],
      hobbies: []
    },
  })
</script>
image-20210630150614018

v-model 的修饰符

  • .lazy修饰符:让数据在输入框失去焦点或用户敲击回车时,才会更新
  • .number修饰符:让输入框中的内容自动转为数字类型(默认情况下为字符串类型)
  • .trim修饰符:过滤内容左右两边的空格

下面以lazy修饰符为例,来看下如何使用 v-model 修饰符:

<div id="app">
  <input type="text" v-model.lazy="message">
  <h2>{{message}}</h2>
</div>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      message: "你好"
    },
  })
</script>
GIF 2021-6-30 15-22-12

Vue 组件

Vue 的组件化思想

组件化是 Vue.js 中的重要思想:

  • 它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用
  • 任何应用都可以被抽象成一颗组件树(如下图)
  • 它让代码方便组织和管理,并且提升了扩展性

Vue的组件化思想

使用组件的步骤

使用组件分为三个步骤:

  1. 创建组件构造器
    • 调用Vue.extend()方法创建一个组件构造器
    • 同时传入template来作为自定义组件的模板(下例中模板为要显示的 HTML 代码;但是这种写法在 Vue 2.x 的文档中几乎已经被语法糖代替啦)
  2. 注册组件:调用Vue.component("注册组件的标签名", 组件的构造器)方法
  3. 使用组件:在 Vue 实例的作用范围内使用组件,否则不生效

下面我们按照上述步骤,来实际使用一下 Vue 组件。

Vue 组件的基本使用

<div id="app">
  <!--3. 使用组件-->
  <my-cpn></my-cpn>
  <my-cpn></my-cpn>
  <my-cpn></my-cpn>
</div>

<script src="../vue.js"></script>
<script>
  // 1. 创建组件构造器对象
  const cpnConstructor = Vue.extend({
    template: `
      <div>
        <h2>文章标题</h2>
        <h5>作者</h5>
        <p>文章内容,balablabala...</p>
        <hr>
      </div>
    `
  })
  // 2. 注册组件
  Vue.component("my-cpn", cpnConstructor);

  const app = new Vue({
    el: "#app",
  })
</script>
组件的基本使用

全局组件 和 局部组件

全局组件注册方法

其实,在上面的例子中,我们注册的组件就叫做全局组件。

Vue.component("my-cpn", cpnConstructor);

这样注册的组件可以在任意 Vue 实例下都可以生效(当然,脱离了 Vue 实例自然无法生效):

<div id="app">
</div>
<div id="anotherApp">
  <my-cpn></my-cpn>
</div>

<script src="../vue.js"></script>
<script>
  const cpnConstructor = Vue.extend({
    template: `<div><h2>文章标题</h2><h5>作者</h5><p>文章内容,balablabala...</p><hr></div>`
  })
  // 注册全局组件
  Vue.component("my-cpn", cpnConstructor);

  const app = new Vue({
    el: "#app",
  })
  const anotherApp = new Vue({
    el: "#anotherApp",
  })
</script>
image-20210630164746431

局部组件注册方法

那么我们该如何注册局部组件呢?

我们可以先想想,什么是局部组件。局部组件要求只在id="app"的标签内部才能够生效,因此可以想到,我们可以在名为app的 Vue 实例内部来注册:

<div id="app">
  <my-cpn></my-cpn>
</div>

<script src="../vue.js"></script>
<script>
  const cpnConstructor = Vue.extend({
    template: `<div><h2>文章标题</h2><h5>作者</h5><p>文章内容,balablabala...</p><hr></div>`
  })
  const app = new Vue({
    el: "#app",
    // 注册局部组件
    components: {
      myCpn: cpnConstructor
    }
  })
</script>

此时如果我们在id="anotherApp"的标签中使用<myCpn>,将不会生效。

注意:

  • **如果在注册组件时使用了驼峰命名法,在使用时要使用短橫线分隔命名。**如组件命名时为myCpn,在使用时要用<my-cpn>
  • 注册局部组件是components,注册全局组件是component

父子组件

在前面我们学习了组件树,所以组件和组件之间是存在层级关系的。其中,很重要的就是父子组件的关系,也就是说在父组件中使用子组件。

创建方法,其实就是在父组件中添加components属性,注册子组件;然后在template属性中使用它:

<div id="app">
  <father-cpn></father-cpn>
</div>

<script src="../vue.js"></script>
<script>
  // 子组件
  const sonCpnC = Vue.extend({
    template: `<div><h4>文章标题</h4><h5>作者</h5><p>文章内容,balablabala...</p><hr></div>`
  })
  // 父组件
  const fatherCpnC = Vue.extend({
    template: `
      <div>
        <h2>文章列表</h2>
        <son-cpn></son-cpn>
        <son-cpn></son-cpn>
      </div>`,
    components: {
      sonCpn: sonCpnC
    }
  })
  
  const app = new Vue({
    el: "#app",
    components: {
      fatherCpn: fatherCpnC
    }
  })
</script>
image-20210630173843555

注意:

  • **子组件要在父组件前面声明。**这样在父组件中注册子组件时,Vue 才能找到子组件呀。

  • **另外,子组件如果在父组件的层级中,子组件将会失效。**这是因为,当 Vue 发现<son-cpn>时,会去app的components中找sonCpn,发现没有这个组件;然后又去全局组件中找,发现也没有;所以就报错了。

    <div id="app">
      <father-cpn></father-cpn>
      <son-cpn></son-cpn>
    </div>
image-20210630173802154

组件注册的语法糖

Vue为了简化注册组件的过程,提供了注册组件的语法糖。

主要是省去了调用Vue.extend()的步骤,去直接使用一个包含template属性的对象。

<div id="app">
  <gloabl-cpn></gloabl-cpn>
  <part-cpn></part-cpn>
</div>

<script src="../vue.js"></script>
<script>
  // 注册全局组件的语法糖
  Vue.component('gloabl-cpn', {
    template: `<div><h4>我是全局组件</h4><p>全局组件内容,balablabala...</p><hr></div>`
  })
  const app = new Vue({
    el: "#app",
    // 注册局部组件
    components: {
      partCpn: {
        template: `<div><h4>我是局部组件</h4><p>局部组件内容,balablabala...</p><hr></div>`
      }
    }
  })
</script>
image-20210630175818109

组件模板的分离写法

你可能感觉到了,在上面的例子中,我们在对template赋值时,写了很多 HTML 代码。显然在 JavaScript 语法中,写 HTML 代码是很头痛的,所以我们可以将template抽离出来,直接写进 script 标签外面。

下面给出了两种方法:

  • 使用<script type="text/x-template"
  • 使用<template>
<div id="app">
  <script-temp-cpn></script-temp-cpn>
  <temp-temp-cpn></temp-temp-cpn>
</div>
<!--1. 使用script标签-->
<script type="text/x-template" id="scriptTempId">
  <div>
    <h4>我是 script 标签创建的组件</h4>
    <p>组件内容,balablabala...</p>
    <hr>
  </div>
</script>
<!--2. 使用template标签-->
<template id="tempTempId">
  <div>
    <h4>我是 template 标签创建的组件</h4>
    <p>组件内容,balablabala...</p>
    <hr>
  </div>
</template>

<script src="../vue.js"></script>
<script>
  Vue.component('script-temp-cpn', {
    template: "#scriptTempId"
  });
  const app = new Vue({
    el: "#app",
    components: {
      tempTempCpn: {
        template: "#tempTempId"
      }
    }
  })
</script>
image-20210630183004204

注意:在 template 标签内只能有一个根元素,所以我们最好在 <template> 的最外层包裹一层 <div>

image-20210630225830207

组件数据的存放

讨论

显然,在开发中我们组件中的数据不可能是固定的,所以我们需要将 Vue 实例中的数据绑定在组件中。

那么,组件可以直接访问 Vue 实例中的数据吗?我们来试一下!

<div id="app">
  <my-cpn></my-cpn>
</div>
<template id="tempId">
  <div>
    <h4>我是组件</h4>
    <p>组件内容:{{content}}</p>
    <hr>
  </div>
</template>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      content: "balabalabala...",
    },
    components: {
      myCpn: {
        template: "#tempId"
      }
    }
  })
</script>
image-20210630185134180 image-20210630185113503

可见,Vue 没有找到content,所以组件不能直接访问 Vue 实例的数据。

其实,也不应该能够直接被访问。因为,如果组件的数据也存进 Vue 实例的话,Vue 实例中的数据将会变得非常繁杂。

结论:Vue 组件应该有一个地方来存放自己的数据。

组件数据的存放

那么我没要想一个好位置,来存放属于组件的数据:那肯定是存在组件的实例中,对不对!是的,组件对象也有一个data(而且还有 methods 属性呢)。

不过,要注意的是:

  • 组件的data必须是一个函数
  • 而且,这个函数返回的是一个对象,这个对象的内部保存着组件的数据
<div id="app">
  <my-cpn></my-cpn>
</div>
<template id="tempId">
  <div>
    <h4>我是组件</h4>
    <p>组件内容:{{content}}</p>
    <hr>
  </div>
</template>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    components: {
      myCpn: {
        template: "#tempId",
        data() {
          return {
            content: "balabalabala...",
          }
        }
      }
    }
  })
</script>
image-20210630185844337

你也许会困惑,为什么组件的data必须是一个函数?

组件的 data 是函数的原因

这是一个很有趣的问题!

还记得,我们那个计数器的例子嘛。我们先把它封装成一个组件,然后对他进行多次调用看看。

<div id="app">
  <counter-cpn></counter-cpn>
  <counter-cpn></counter-cpn>
</div>
<template id="counterTemp">
  <div>
    <h2>当前计数:{{counter}}</h2>
    <button @click="counter--">-</button>
    <button @click="counter++">+</button>
  </div>
</template>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    components: {
      counterCpn: {
        template: "#counterTemp",
        data() {
          return {
            counter: 0
          }
        }
      }
    }
  })
</script>
GIF 2021-6-30 19-14-45

我们可以看到,两个组件的 counter 实际上是独立的。很奇妙对不对,这就是 data 是函数的功劳。

data() {
    return {
        counter: 0
    }
}

对于这个组件来说,不管它被使用了几次,因为data 返回的总是一个新创建的对象,所以组件之间的数据是相互独立、互不干扰的。

data: {
    counter: 0
}

反过来,如果 data 还是像 Vue 实例中那样,是一个对象实例,那么每个组件使用的数据其实都是来自同一个 data 对象实例,这样就会出现混乱。

父子组件的通信

在开发中,我们往往需要将一些数据从上层传递到下层。

比如,在一个页面中,我们先从服务器请求到很多数据。其中一部分数据,我们是要在页面中最大组件的子组件中显示的。这个时候,我们不应该再让子组件再次发生请求,而是要让大组件(父组件)直接传递数据给小组件(子组件)。这里就涉及到了,父子组件的通信。

那么如何进行父子组件的通信呢?Vue 官方提供了两种方法:

  • 通过 props 向子组件传递数据
  • 通过事件向父组件发送消息
父子组件通信

下面我们把 Vue 实例当作父组件,来举个例子。在真实的开发中,Vue 实例和子组件的通信适合父组件和子组件的通信过程是一样的。

父向子传数据 - props

当子组件需要向父组件中接收数据时,我们可以在注册子组件时添加 props属性,来获取需要的数据。

** props 的值有两种:**

  • 字符串数组,数组中的字符串就是接收时的名称
  • 对象,对象可以限制类型,设置默认值和必选值
    • 支持验证很多类型:String / Number / Boolean / Array / Object / Date / Function / Symbol / 自定义类型

具体流程:

  • 在子组件中,使用 props 来注册子组件的 properties
  • 在父组件使用的子组件标签中,通过 v-bind 动态绑定刚刚注册的 properties,properties 的值为父组件要传的数据名
  • 在子组件的模板中,以 properties 为名使用获取的数据
<div id="app">
  <h2>父组件</h2>
  <div>数据:{{movies}},{{message}}</div>
  <hr>
  <my-cpn :child-movies="movies" :child-message="message"></my-cpn>
</div>
<template id="myCpnTempId">
  <div>
    <h3>子组件</h3>
    <ul>
      <li v-for="item in childMovies">{{item}}</li>
    </ul>
    <h3>{{childMessage}}</h3>
  </div>
</template>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      movies: ["海王", "星际穿越", "盗梦空间"],
      message: "以上电影正在热映!"
    },
    components: {
      myCpn: {
        template: "#myCpnTempId",
        // 1. props为字符串数组
        // props: ["childMovies", "childMessage"]

        // 2. props为对象
        props: {
          // 2.1 限制类型
          // childMovies: Array,
          // childMessage: String,

          // 2.2 限制类型,设置默认值和必传值
          childMovies: {
            type: Array,
            default() {
              return [];
            },
            required: true
          },
          childMessage: {
            type: String,
            default: "!",
          }
        }
      }
    }
  })
</script>
image-20210630233717482

注意:

  • HTML 的属性名应该是 kebab-case 式命名的 ,所以尽管 props 中写的是 childMovie ,我们在 <my-cpn> 中也应该写 v-bind:child-movie

  • props 的类型为 Object/Array时,默认值应返回一个(工厂)函数,否则会报错

image-20210630232036943

子向父发消息 - $emit

当我们需要子组件向父组件发送消息时,我们要使用自定义事件来完成。

自定义事件的流程:

  • 在子组件中,通过 $emit() 来触发事件
  • 在父组件中,通过 v-on 来监听子组件的事件

具体流程:

  • 在子组件的模板中,添加事件,来携带消息
  • 在子组件的 methods 中,注册事件,使用 $emit() 来向父组件发送事件
    • $emit() 的第一个参数为下一步要用的监听事件名;第二个参数为要传递的参数
  • 在父组件的子组件标签中,使用 v-on 来添加事件监听,名为 $emit() 的第一个参数,并写一个监听到事件后会被触发的事件名
  • 在父组件的 methods 中,添加该事件,收到的参数就是 $emit() 的第二个参数
<div id="app">
  <my-cpn @child-btn-click="showItem"></my-cpn>
  <h2>父组件</h2>
  <div>用户点击了:{{categoryItem}}</div>
</div>
<template id="myCpnTempId">
  <div>
    <h3>子组件</h3>
    <button v-for="item in categories" @click="onChildBtnClick(item)">{{item}}</button>
    <hr>
  </div>
</template>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      categoryItem: ""
    },
    methods: {
      showItem(item) {
        this.categoryItem = item;
      }
    },
    components: {
      myCpn: {
        template: "#myCpnTempId",
        data() {
          return {
            categories: ["手机数码","电脑办公", "家用家电"]
          }
        },
        methods: {
          onChildBtnClick(item){
            this.$emit('child-btn-click', item);
          }
        }
      }
    }
  })
</script>
GIF 2021-7-1 1-43-47

注意:

** $emit 的第一个参数,应是 kebab-case 式命名的,因为 v-on 后面接的值只能是 kebab-case 命名的。**

父子组件的访问

在开发中,我们其实不仅要在父子组件之间传递数据、发送消息,有时还需要在父子组件之间互相访问各自的数据和方法。

父访问子组件 - $children

使用方法很简单,在父组件的方法中,直接使用 this.$children[x].方法名 就可以获取到相应的子组件对象了。

<div id="app">
  <my-cpn></my-cpn >
  <my-cpn></my-cpn>
  <my-cpn></my-cpn>
  <button @click="clickBtn">按钮</button>
</div>
<template id="cpnTempId">
  <h3>我是子组件</h3>
</template>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    methods: {
      clickBtn() {
        // console.log(this.$children);
        this.$children[0].logMessage();
        console.log(this.$children[0].message);
      }
    },
    components: {
      myCpn: {
        template: "#cpnTempId",
        data() {
          return {
            message: "我是子组件的message"
          }
        },
        methods: {
          logMessage(){
            console.log("我是子组件的方法logMessage(),我被调用了");
          }
        }
      }
    }
  })
</script>

点击按钮后,显示

image-20210701173406106

这就表明,父组件获取到了子组件的数据和方法。

我们执行 console.log(this.$children) 的结果如下。

image-20210701173603598

可见,$children 实际上是一个包含所有子组件的数组,所以在使用时记得表明要使用的子组件的下标。

那么问题也来了,既然我们要通过下标来访问子组件,当用户动态地在页面中添加了子组件的话,我们要访问的子组件的下标肯定也会发生变化。所以这对我们来说,是个坏消息。因此,在实际的开发中,我们通常使用 $refs 来访问子组件。

父访问子组件 - $refs

为了解决上一个方法的一些弊端,我们要先给父组件中的子组件标签上添加 ref="组件名" ,然后和 $children 一样,直接使用 this.$refs.组件名 就可以获取到相应的子组件对象了。

<div id="app">
  <my-cpn ref="myCpn1"></my-cpn >
  <my-cpn ref="myCpn2"></my-cpn>
  <my-cpn ref="myCpn3"></my-cpn>
  <button @click="clickBtn">按钮</button>
</div>
<template id="cpnTempId">
  <h3>我是子组件</h3>
</template>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    methods: {
      clickBtn() {
        console.log(this.$refs.myCpn1.message);
        this.$refs.myCpn1.logMessage();
      }
    },
    components: {
      myCpn: {
        template: "#cpnTempId",
        data() {
          return {
            message: "我是子组件的message"
          }
        },
        methods: {
          logMessage(){
            console.log("我是子组件的方法logMessage(),我被调用了");
          }
        }
      }
    }
  })
</script>

image-20210701174438073

注意:

  • 在父组件中的子组件标签上添加的属性名为 ref,没有 s
  • 如果多个组件的 ref 名一样,则后面的组件会覆盖前面的组件。所以在开发中尽量使用不同的 ref 名哦

子访问父 - $parent 和 子访问根 - $root

<div id="app">
  <h2>我是根组件</h2>
  <father-cpn></father-cpn >
</div>
<template id="fatherCpnTempId">
  <div>
    <h3>我是父组件</h3>
    <child-cpn></child-cpn>
  </div>
</template>
<template id="childCpnTempId">
  <div>
    <h4>我是子组件</h4>
    <button @click="clickBtn">按钮</button>
  </div>
</template>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    data: {
      message: "我是根组件的message"
    },
    methods: {
      rootLogMessage() {
        console.log("我是根组件的方法logMessage(),我被调用了");
      }
    },
    components: {
      fatherCpn: {
        template: "#fatherCpnTempId",
        data() {
          return {
            message: "我是父组件的message"
          }
        },
        methods: {
          parentLogMessage(){
            console.log("我是父组件的方法logMessage(),我被调用了");
          }
        },
        components: {
          childCpn: {
            template: "#childCpnTempId",
            methods: {
              clickBtn() {
                // 1. $parent访问父组件
                console.log(this.$parent.message);
                this.$parent.parentLogMessage();

                // 2. $root访问跟组件
                console.log(this.$root.message);
                this.$root.rootLogMessage();
              }
            },
          }
        }
      }
    }
  })
</script>

点击按钮,输出如下:

image-20210701200922092

在上面的例子中,我们

  • 使用 $parent 访问到了子组件的父组件的数据和方法,
  • 使用 $root 访问到了子组件的根组件的数据和方法。

不过要注意的是,跟组件返回的不是 Vue Component,而是一个 Vue 实例。我们执行 console.log(this.$root) 可以看一下。

image-20210701201325618

组件的插槽 slot

事实上,到目前为止,我们组件的模板都是固定的。但是在开发中,针对不同的应用场景,我们需要我们的组件要根据情况,结构发生一些变化。如下面的这种情况:

image-20210701204840438

上面显示的是我们模拟的文章列表,此时每篇文章都是有作者的。但是如果我的第二篇文章时转载的,我想把作者这个位置的数据换成一个链接,该怎么办呢?如果我们直接修改组件模板,那么必然所有文章的作者都变成了一个链接,所以现在该怎么办呢?

此时我们就可以用上插槽了,我们只需要对HTML的部分进行修改,在模板中将要变化的位置换成 <slot> 标签,然后在使用组件时,在组件标签内部添加你想加的HTML结构就可以了。

<div id="app">
  <my-cpn><h4>作者:XXX</h4></my-cpn>
  <my-cpn><a href="www.xxx.com">原文链接</a></my-cpn>
  <my-cpn><h4>作者:XXX</h4></my-cpn>
</div>
<template id="cpnTempId">
  <div>
    <h3>标题</h3>
    <slot></slot>
    <p>文章内容, balabalabala...</p>
    <hr>
  </div>
</template>
image-20210701205249935

是不是很简单!同时,我们还可以给 slot 添加一个默认值,来表示每篇文章的中间默认是显示作者:

<div id="app">
  <my-cpn></my-cpn>
  <my-cpn><a href="www.xxx.com">原文链接</a></my-cpn>
  <my-cpn></my-cpn>
</div>
<template id="cpnTempId">
  <div>
    <h3>标题</h3>
    <slot><h4>作者:XXX</h4></slot>
    <p>文章内容, balabalabala...</p>
    <hr>
  </div>
</template>
image-20210701204348950

具名插槽

现在我们的需求又变了,我希望每篇文章的结构都是可变的。也就是说,上部(标题区域)可以是文字也可以是链接,中部(作者区域)可以是文字也可以是链接,下部同理。这时该怎么办?聪明的你,一定可以想到,只需要将模板写成三个 <slot> ,在使用时组件中间添加结构就可以啦!我们来试试看:

<div id="app">
  <my-cpn><h3>标题</h3></my-cpn>
</div>
<template id="cpnTempId">
  <div>
    <slot></slot>
    <slot></slot>
    <slot></slot>
    <hr>
  </div>
</template>
image-20210701210153264

欸?事情并没有能够如我们所愿,其实原因也很简单,Vue 也不知道你这个 <h3>标题</h3> 应该插进哪个插槽。因此我们只需要给每个 <slot> 一个名字就可以啦。

这时我们就要使用具名插槽 <slot name=""> 了。我们献给每一个 <slot> 添加一个 name 属性,然后再使用组件时,将要插入的结构上表明 slot="?"即可。

<div id="app">
  <my-cpn>
    <h3 slot="top">标题</h3>
    <h4 slot="middle">作者:XXX</h4>
    <p slot="bottom">文章内容,balabalabala...</p>
  </my-cpn>
</div>
<template id="cpnTempId">
  <div>
    <slot name="top"></slot>
    <slot name="middle"></slot>
    <slot name="bottom"></slot>
    <hr>
  </div>
</template>
image-20210701210725149

作用域插槽

现在我们有这样一个页面:

image-20210701223556893

我们的需求是,将第二个 movieList 的格式改成“海王 - 海贼王 - 海尔兄弟”这个样子,那该怎么办呢?

我们先来分析一下,我们知道既然它是两个组件,那么 movieList 肯定是组件的数据。当我们想要修改第二个组件的格式时,肯定就需要在第二个组件中间使用 movieList。那么问题来了,如果我们像这样直接使用 movies 数组,其实是在访问 Vue 实例中的数据。

<body>
<div id="app">
  <my-cpn></my-cpn>
  <my-cpn>
    <!--这时错误的!-->
    <div>{{movies.join(' - ')}}</div>
  </my-cpn>
</div>
<template id="cpnTempId">
  <div>
    <slot>
      <ul>
        <li v-for="item in movies">{{item}}</li>
      </ul>
    </slot>
  </div>
</template>

<script src="../vue.js"></script>
<script>
  const app = new Vue({
    el: "#app",
    components:{
      myCpn: {
        template: "#cpnTempId",
        data() {
          return {
            movies: ['海王', '海贼王', '海尔兄弟']
          }
        }
      }
    }
  })
</script>

所以,我们要使用作用域插槽来在使用标签时获取到组件的数据。

具体步骤:

  1. <slot> 内动态绑定一个属性,属性的值为要传的数据
  2. 在使用组件时,在组件中间的 <template> 中添加属性 slot-scope,并设置一个值
  3. 在需要用数据的地方,使用 第二步设置的值.第一步动态绑定的属性名 就可以获取到数据了
<div id="app">
  <my-cpn></my-cpn>
  <my-cpn>
    <template slot-scope="slot">
      <div>{{slot.data.join(' - ')}}</div>
    </template>
  </my-cpn>
</div>
<template id="cpnTempId">
  <div>
    <slot :data="movies">
      <ul>
        <li v-for="item in movies">{{item}}</li>
      </ul>
    </slot>
  </div>
</template>

模块化

为什么要模块化

工程中,肯定会有很多个js文件,这时会不可避免地出现全局变量重名的问题。举个例子的话,我们在一个js文件中定义了一个变量为 true ,然后我们想在另一个文件中使用这个变量。但很不巧的是另一个在他的js文件里也声明了这个变量,恰好是false。这时如果他的文件在我们的文件之前被引入,我取到的这个变量就不是true了,这就出现了问题。

这个问题可以通过把每个js文件封装成一个匿名函数,然后直接调用它来解决。但是这就导致了,我无法复用其他文件中的变量和方法。

为了解决这个问题,最早其实是将,要导出(复用)的数据作为返回值,返回给js文件中的全局变量。只要保证文件之间的这个变量不重名就可以了。这就是模块化的雏形。

而现在我们可以使用模块化的思想,更简便的完成上面的功能。

模块化的规范有:Common JS、AMD、CMD、ES6的module。

正常,浏览器是用不了Common JS、AMD和CMD的,因为没有底层技术的支持。但它支持ES6的语法。而如果我们用了Webpack,就能给他们做一个底层的支持。

Webpack

官方的解释:

本质上,webpack是一个现代的JavaScript应用的静态 模块 打包 工具。

模块:上文已述。

打包:导入和导出是模块化的核心,所以模块之间的关系是非常复杂的,所以webpack可以把各种资源模块打包成一个或多个包(Bundle)。并且在打包的过程中,可以将比如说,less转成css,typescript转成JavaScript,还包括图片压缩等等,让这些本来无法被浏览器识别的文件,被识别。

Webpack

与 grunt/gulp 的对比

  • grunt/gulp的核心是task,被称为前端自动化任务管理工具,更加强调的是前端流程的自动化,模块化不是它的核心

  • webpack更加强调模块化开发管理,而文件压缩合并、预处理等功能。

什么时候用 grunt/gulp ?

  • 工程模块以来非常简单,甚至没有用到模块化的概念
  • 只需要进行简单的合并、压缩

js 文件的打包

Webpack 打包命令:

webpack ./src/main.js -o dist/bundle.js --mode development

但是生成的文件有些奇怪:

image-20210713231928859

按理来说,应该在 dist 文件夹下生成 bundle.js 文件。目前不知道如何解决。

引用打包后的文件

image-20210713232529911

Vue CLI

CLI 是什么

CLI(Command-Line Interface)命令行界面,俗称脚手架。

使用 vue-cli 可以快速搭建 Vue开发环境以及对应的 webpack 配置。