6

重要说明:本文会在我有空闲时间时持续更新,相当于是将官网的示例给完全呈现,是为了帮助初学者,也是为了巩固我自己的技术,我决定将官网给过滤一道消化,敬请期待。

一.介绍

vue是一种渐进式框架,被设计为自底向上逐层应用。所谓渐进式框架,我的理解就是vue是循序渐进的,一步一步的用。
举个简单的例子,就算我们不会webpack,不会node,但也能很快的入门。更多详情参阅渐进式

二.起步

1.hello,world

在学习vue之前,需要有扎实的HTML,CSS,JavaScript基础。任何一个入门语言都离不开hello,world!例子,我们来写这样一个例子:
新建一个html文件,helloworld.html,如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>hello,world</title>
  </head>
  <body>
    <div id="app">
      {{ message }}
    </div>
    <script>
    //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

var app = new Vue({
  el:"#app",
  data:{
    message:"hello,world!"
  }
})

现在我们已经成功创建了第一个vue应用,数据和DOM已经被关联,所有的东西都是响应式的,我们要如何确定呢,打开浏览器控制台,修改app.message的值。

在这其中data对象的写法,我们还可以写成函数形式,如下:

var app = new Vue({
  el:"#app",
  //这里是重点
  data(){
     return{
        message:"hello,world!"
     }
  }
})

2.文本插值

当然除了文本插值,我们还可以绑定元素属性,如下:

   <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <!-- 引入vue.js开发版本 -->
        <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
        <title>v-bind</title>
      </head>
      <body>
        <div id="app">
          <span v-bind:title="message">鼠标悬浮上去可以看到</span>
        </div>
        <script>
        //这里写JavaScript代码
        </script>
      </body>
    </html>
    

js代码如下:

 var app = new Vue({
  el:"#app",
  data:{
    message:"页面加载于:" + new Date().toLocaleString()
  }
})
   

查看效果,前往此处查看效果:
同样的我们也可以修改message的值,这样的话,鼠标悬浮上去,悬浮的内容就会改变了。在这个例子中v-bind(或者也可以写成':')其实就是一个指令,指令通常前缀都带有v-,用于表示vue指定的特殊特性,在渲染DOM的时候,它会应用特殊的响应式行为。这个指令所表达的意思就是:将这个title属性的值与vue实例的message值保持一致。

3.元素的显隐

当然,我们也可以控制一个元素的显隐,那也是非常的简单,只需要使用v-show指令即可:

     <!DOCTYPE html>
        <html>
          <head>
            <meta charset="utf-8">
            <!-- 引入vue.js开发版本 -->
            <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
            <title>v-if</title>
          </head>
          <body>
            <div id="app">
              <span v-show="seen">默认你是看不到我的哦</span>
            </div>
            <script>
            //这里写JavaScript代码
            </script>
          </body>
        </html>
     

js代码如下:

  var app = new Vue({
       el:"#app",
       data:{
          seen:false
       }
   })   

尝试在控制台中修改seen的值,也就是app.seen = true,然后你就可以看到页面中的span元素了,具体示例

4.列表渲染

还有v-for指令,用于渲染一个列表,如下:

     <!DOCTYPE html>
        <html>
          <head>
            <meta charset="utf-8">
            <!-- 引入vue.js开发版本 -->
            <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
            <title>v-for</title>
          </head>
          <body>
            <div id="app">
              <div v-for="(item,index) in list" :key="index">
                <span>{{ item.name }}</span>
                <p>{{ item.content }}</p>
              </div>
            </div>
            <script>
            //这里写JavaScript代码
            </script>
          </body>
        </html>
        

js代码如下:

   var app = new Vue({
       el:"#app",
       data:{
          list:[
            { name:"项目一",content:"HTML项目"},
            { name:"项目二",content:"CSS项目"},
            { name:"项目三",content:"JavaScript项目"},
          ]
       }
   })             
        
        

当然你也可以自己在控制台改变list的值,具体示例

5.事件

vue通过v-on + 事件属性名(也可以写成'@' + 事件属性名)指令添加事件,例如v-on:click@click如下一个示例:

       <!DOCTYPE html>
        <html>
          <head>
            <meta charset="utf-8">
            <!-- 引入vue.js开发版本 -->
            <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
            <title>v-on</title>
          </head>
          <body>
            <div id="app">
              <span>{{ message }}</span>
              <button type="button" v-on:click="reverseMessage">反转信息</button>
              <!--也可以写成-->
              <!--<button type="button" @click="reverseMessage">反转信息</button>-->
            </div>
            <script>
            //这里写JavaScript代码
            </script>
          </body>
        </html>

js代码如下:

    var app = new Vue({
       el:"#app",
       data:{
          message:"hello,vue.js!"
       },
       methods:{
         reverseMessage:function(){
             //在这里this指向构造函数构造的vue实例
            this.message = this.message.split('').reverse().join('');
         }
       }
   }) 

反转信息的思路就是使用split()方法将字符串转成数组,,然后使用数组的reverse()方法将数组倒序,然后再使用join()方法将倒序后的数组转成字符串。

你也可以尝试在这里查看示例

6.组件

组件是vue中的一个核心功能,它是一个抽象的概念,它把所有应用抽象成一个组件树,一个组件树就是一个预定义的vue实例,在vue中使用Vue.component()注册一个组件,它有两个参数,第一个参数为组件名(尤其要注意组件名的命名),第二个参数为组件属性配置对象,如:

//定义一个简单的组件
Vue.component('todo-item',{
   template:`<li>待办事项一</li>`
})

现在我们来看一个完整的例子:

       <!DOCTYPE html>
        <html>
          <head>
            <meta charset="utf-8">
            <!-- 引入vue.js开发版本 -->
            <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
            <title>component</title>
          </head>
          <body>
            <div id="app">
              <ul>
                <todo-item v-for="(item,index) in todoList" v-bind:todo="item" v-bind:key="index"></todo-item>
              </ul>
            </div>
            <script>
            //这里写JavaScript代码
            </script>
          </body>
        </html>
        

js代码如下:

 Vue.component('todo-item',{
    props:['todo'],
    template:`<li>{{ todo.number }}.{{ todo.text }}</li>`
 })
 var app = new Vue({
       el:"#app",
       data:{
          todoList:[
             { number:1,text:"html"},
             { number:2,text:"css"},
             { number:3,text:"javascript"}
          ]
       },
       methods:{
         
       }
   })
   

这样,一个简单的组件就完成了,在这里,我们知道了,父组件app可以通过props属性将数据传递给子组件todo-item,这是vue父子组件之间的一种通信方式。
你可以尝试在此处具体示例

三.核心

1.vue实例

每个vue应用都是通过Vue构造函数创建的一个新的实例开始的:

var vm = new Vue({
   //选项对象
})

在这其中vm(viewModel的简称)通常都表示vue实例的变量名。当创建一个vue实例,你都可以传入一个选项对象作为参数,完整的选项对象,你可能需要查看API文档

一个vue应用应该由一个通过new Vue构造的根实例和许多可嵌套可复用的组件构成,这也就是说所有的组件都是vue实例。

2.数据与方法

当一个vue实例被创建完成之后,就会向它的vue响应系统中加入了data对象中能找到的所有属性,当这些属性的值发生改变之后,视图就会发生响应,也就是更新相应的值。我们来看一个例子:

//源数据对象
var obj = { name:"eveningwater" };
//构建实例
var vm = new Vue({
   data:obj
})

//这两者是等价的
vm.name === obj.name;
//这也就意味着
//修改data对象里的属性也会影响到源数据对象的属性
vm.name = "waterXi";
obj.name;//"waterXi"
//同样的,修改源数据对象的属性也会影响到data对象里的属性
obj.name = 'stranger';
vm.name;//"stranger"

可能需要注意的就是,只有data对象中存在的属性才是响应式的,换句话说,你为源数据对象添加一个属性,根本不会影响到data对象。如:

obj.sex = "男";
vm.sex;//undefined
obj.sex;//'男'
obj.sex = "哈哈哈";
vm.sex;//undefined

这也就意味着你对sex的修改并不会让视图更新,如此一来,你可能需要在data对象中初始化一些值,如下:

data:{
   str:'',
   bool:false,
   arr:[],
   obj:{},
   err:null,
   num:0
}

查看此处具体示例

只是还有一个例外Object.freeze(),这个方法就相当于锁定(冻结)一个对象,使得我们无法修改现有属性的特性和值,并且也无法添加新属性。因此这会让vue响应系统无法追踪变化:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>freeze</title>
  </head>
  <body>
    <div id="app">
      <span>{{ message }}</span>
      <button type="button" v-on:click="reverseMessage">反转信息</button>
    </div>
    <script>
        //这里写JavaScript代码
    </script>
  </body>
</html>  

js代码如下:

      var obj = {
          message: "hello,vue.js!"
      }
      //阻止对象
      Object.freeze(obj);
      var app = new Vue({
        el: "#app",
        data:obj,
        methods: {
          reverseMessage: function() {
            this.message = this.message.split("").reverse().join("");
          }
        }
      });  

如此一来,无论我们怎么点击按钮,都不会将信息反转,甚至页面还会报错。
可前往此处具体示例自行查看效果。

当然除了数据属性以外,vue还暴露了一些有用的实例属性和方法,它们通常都带有$前缀,这样做的方式是以便与用户区分开来。来看一个示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>property</title>
  </head>
  <body>
    <div id="app">
        
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

  var obj = {
     name:'eveningwater'
  }
  var vm = new Vue({
    data:obj,
  });
  //这行代码表示将vue实例挂载到id为app的DOM根节点上,相当于在实例的选项对象中的el选项,即
  //el:'#app'
 vm.$mount(document.querySelector('#app')) 
 //数据是相等的
 vm.$data === obj;//true
 //挂载的根节点
 vm.$el === document.querySelector('#app');//true
 //以上两个属性都是实例上的属性,接下来还有一个watch即监听方法是实例上的方法
 vm.$watch('name',function(oldValue,newValue){
   //数据原来的值
   console.log(oldValue);
   //数据最新的值
    console.log(newValue);
 })

接下来,可以尝试在浏览器控制台修改name的值,你就会发现watch()方法的作用了。
这个示例可前往具体示例

3.实例生命周期

每个vue实例在被创建的时候都会经历一些初始化的过程,这其中提供了一些生命周期钩子函数,这些钩子函数代表不同的生命周期阶段,这些钩子函数的this就代表调用它的那个实例。对于生命周期,有一张图:

lifecycle.png

你不需要立即这张图所代表的含义,我们来看一个示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>vue life cycle</title>
  </head>
  <body>
    <div id="app">
        <span>vue生命周期</span>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

  var obj = {
     name:'eveningwater'
  }
  var app = new Vue({
    data:obj,
    beforeCreate:function(){
        //此时this指向app这个vue实例,但并不能得到data属性,因此this.name的值是undefined
        console.log('实例被创建之前,此时并不能访问实例内的任何属性' + this.name)
    }
  });
  

关于生命周期的全部理解,我们需要理解后续的组件知识,再来补充,此处跳过。这个示例可前往具体示例

4.模板语法

vue使用基于HTML的模板语法,在vue的底层是将绑定数据的模板渲染成虚拟DOM,并结合vue的响应式系统,从而减少操作DOM的次数,vue会计算出至少需要渲染多少个组件。

最简单的模板语法莫过于插值了,vue使用的是Mustache语法(也就是双大括号"{{}}")。这个只能对文本进行插值,也就是说无论是字符串还是标签都会被当作字符串渲染。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>Mustache</title>
  </head>
  <body>
    <div id="app">
        <span>{{ greeting }}World!</span>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var obj = { greeting:"Hello,"};
 var vm = new Vue({
    data:obj
 });
 vm.$mount(document.getElementById('app'));
 

如此以来Mustache标签就会被data对象上的数据greeting给替代,而且我们无论怎么修改greeting的值,视图都会响应,具体示例

我们还可以使用v-once指令对文本进行一次性插值,换句话说,就是这个指令让插值无法被更新:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>Mustache</title>
  </head>
  <body>
    <div id="app">
        <span v-once>{{ greeting }}World!</span>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var obj = { greeting:"Hello,"};
 var vm = new Vue({
    data:obj
 });
 vm.$mount(document.getElementById('app'));

在浏览器控制台中我们输入vm.greeting="stranger!"可以看到视图并没有被更新,这就是这个指令的作用,我们需要注意这个指令对数据造成的影响。具体实例

既然双大括号只能让我插入文本,那要是我们要插入HTML代码,我们应该怎么办呢?v-html这个指令就可以让我们插入真正的HTML代码。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-html</title>
  </head>
  <body>
    <div id="app">
        <p>{{ message }}</p>
        <p v-html="message"></p>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var obj = { message:"<span style='color:#f00;'>hello,world!</span>"};
 var vm = new Vue({
    data:obj
 });
 vm.$mount(document.getElementById('app'));
 

页面效果如图所示;

图片描述

可前往具体示例

关于HTML特性,也就是属性,我们需要用到v-bind指令,例如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-bind</title>
  </head>
  <body>
    <div id="app">
        <div v-bind:id="propId">使用v-bind指令给该元素添加id属性</div>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var obj = { propId:"myDiv"};
 var vm = new Vue({
    data:obj
 });
 vm.$mount(document.getElementById('app'));
 

打开浏览器控制台,定位到该元素,我们就能看到div元素的id属性为"myDiv",如下图所示:

图片描述

具体示例

在绑定与元素实际作用相关的属性,比如disabled,这个指令就被暗示为true,在默认值是false,null,undefined,''等转换成false的数据类型时,这个指令甚至不会表现出来。如下例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-bind</title>
  </head>
  <body>
    <div id="app">
        <button type="button" v-bind:disabled="isDisabled">禁用按钮</button>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var obj = { isDisabled:123};
 var vm = new Vue({
    data:obj
 });
 vm.$mount(document.getElementById('app'));
 

这样一来,无论我们怎么点击按钮都没用,因为123被转换成了布尔值true,也就表示按钮已经被禁用了,我们可以打开控制台看到:

图片描述

你可以尝试这个示例具体示例

在使用模板插值的时候,我们可以使用一些JavaScript表达式。如下例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>expression</title>
  </head>
  <body>
    <div id="app">
      <p>{{ number + 1 }}</p>
      <p>{{ ok ? "确认" : "取消" }}</p>
      <p>{{message.split("").reverse().join("")}}</p>
      <div v-bind:id="'my' + elementId">
        元素的id为<span :style="{ 'color':color }">myDiv</span>
      </div>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var obj = {
    number: 123,
    ok: true,
    message: "hello,vue.js!",
    elementId: "Div",
    color: "red"
  };
  var vm = new Vue({
    data: obj
  });
  vm.$mount(document.getElementById("app"));
  

这些JavaScript表达式都会被vue实例作为JavaScript代码解析,具体示例

值得注意的就是有个限制,只能绑定单个表达式,像语句是无法生效的。如下例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>sentence</title>
  </head>
  <body>
    <div id="app">
      <p>{{ var number = 1 }}</p>
      <p>{{ if(ok){ return '确认'} }}</p>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

  var obj = {
    number: 123,
    ok: true
  };
  var vm = new Vue({
    data: obj
  });
  vm.$mount(document.getElementById("app")); 
  

像这样直接使用语句是不行的,浏览器控制台报错,如下图:

图片描述

不信可以自己试试具体示例

指令(Directives)是带有v-前缀的特殊特性,通常指令的预期值就是单个JavaScript表达式(v-for除外),例如v-ifv-show指令,前者表示DOM节点的插入和删除,后者则是元素的显隐。所以,指令的职责就是根据表达式值的改变,响应式的作用于DOM。现在我们来看两个示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-if</title>
  </head>
  <body>
    <div id="app">
      <p v-if="value === 1">{{ value }}</p>
      <p v-else-if="value === 2">{{ value }}</p>
      <p v-else>{{ value }}</p>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

  var obj = {
    value: 1
  };
  var vm = new Vue({
    el: "#app",
    data() {
      return obj;
    }
  });
  

运行在浏览器效果如图:

图片描述

现在你可以尝试在浏览器控制台更改vm.value = 2vm.value = 3我们就可以看到页面的变化。你也可以狠狠点击此处具体示例查看和编辑。

我们再看v-show的示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-show</title>
  </head>
  <body>
    <div id="app">
      <p v-show="value === 1">{{ value }}</p>
      <p v-show="value === 2">{{ value }}</p>
      <p v-show="value === 3">{{ value }}</p>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

var obj = {
   value:1
} 
var vm = new Vue({
    data:obj
});
vm.$mount(document.querySelector('#app'))

然后查看效果如图:

图片描述
尝试在控制台修改vm.value = 2vm.value = 3我们就可以看到页面的变化。你也可以狠狠点击具体示例查看。

从上面两个示例的对比,我们就可以看出来v-showv-if指令的区别了,从切换效果来看v-if显然不如v-show,这说明v-if有很大的切换开销,因为每一次切换都要不停的执行删除和插入DOM元素操作,而从渲染效果来看v-if又比v-show要好,v-show只是单纯的改变元素的display属性,而如果我们只想页面存在一个元素之间的切换,那么v-if就比v-show要好,这也说明v-show有很大的渲染开销。

而且v-if还可以结合v-else-ifv-else指令使用,而v-show不能,需要注意的就是v-else必须紧跟v-if或者v-else-if之后。当需要切换多个元素时,我们还可以使用template元素来包含,比如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>template</title>
  </head>
  <body>
    <div id="app">
        <template v-if="value > 1">
            <p>{{ value }}</p>
            <h1>{{ value }}</h1>
        </template>
        <template v-else>
            <span>{{ value }}</span>
            <h2>{{ value }}</h2>
        </template>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

  var obj = {
    value: 1
  };
  var vm = new Vue({
    el: "#app",
    data() {
      return obj;
    }
  });
  

此时template相当于一个不可见元素,如下图所示:

图片描述
尝试在控制台修改vm.value = 2就可以看到效果了,你也可以狠狠的点击此处具体示例

对于可复用的元素,我们还可以添加一个key属性,比如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>key</title>
  </head>
  <body>
    <div id="app">
      <template v-if="loginType === 'username'">
        <label>username:</label>
        <input type="text" key="username" placeholder="enter your username" />
      </template>
      <template v-else-if="loginType === 'email'">
        <label>email:</label>
        <input type="text" key="email" placeholder="enter your email" />
      </template>
      <template v-else>
        <label>mobile:</label>
        <input type="text" key="mobile" placeholder="enter your mobile" />
      </template>
      <button type="button" @click="changeType">
        toggle login type
      </button>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

      var obj = {
        loginType: "username",
        count:1
      };
      var vm = new Vue({
        el: "#app",
        data() {
          return obj;
        },
        methods: {
          changeType() {
            this.count++;
            if (this.count % 3 === 0) {
              this.loginType = "username";
            } else if (this.count % 3 === 1) {
              this.loginType = "email";
            } else {
              this.loginType = "mobile";
            }
          }
        }
      });

效果如图:

图片描述
你可以狠狠的点击具体示例查看。

从这几个示例我们也可以看出v-if就是惰性,只有当条件为真时,v-if才会开始渲染。值得注意的就是v-ifv-for不建议合在一起使用。来看一个示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-if与v-for</title>
  </head>
  <body>
    <div id="app">
        <ul>
            <li v-for="(item,index) in list" v-bind:key="index" v-if="item.active">
                <span>{{ item.value }}</span>
            </li>
        </ul>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

  var obj = {
    list:[
        {
            value:'html',
            active:false
        },
        {
            value:'css',
            active:false
        },
        {
            value:"javascript",
            active:true
        }
    ]
  };
  var vm = new Vue({
    el: "#app",
    data() {
      return obj;
    }
  });
  

虽然以上代码不会报错,但这会造成很大的渲染开销,因为v-for优先级高于v-if,这就造成每次执行v-if指令时总要先执行v-for遍历一遍数据。你可以点击此处具体示例查看。

遇到这种情况,我们可以使用计算属性。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-if和v-for</title>
  </head>
  <body>
    <div id="app">
      <ul v-if="newList">
        <li v-for="(item,index) in newList" v-bind:key="index">
          <span>{{ item.value }}</span>
        </li>
      </ul>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

var obj = {
    list: [
      {
        value: "html",
        active: false
      },
      {
        value: "css",
        active: false
      },
      {
        value: "javascript",
        active: true
      }
    ]
  };
  var vm = new Vue({
    el: "#app",
    //先过滤一次数组
    computed: {
      newList: function() {
       return this.list.filter(function(item) {
          return item.active;
        });
      }
    },
    data() {
      return obj;
    }
  });      

如此一来,就减少了渲染开销,你可以狠狠点击这里具体示例查看。

指令的用法还远不止如此,一些指令是可以带参数的,比如v-bind:title,在这里title其实就是被作为参数。基本上HTML5属性都可以被用作参数。比如图片路径的src属性,再比如超链接的href属性,甚至事件的添加也属于参数,如v-on:click中的click其实就是参数。来看一个示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>param</title>
  </head>
  <body>
    <div id="app">
        <a v-bind:href="url">思否</a>
        <img :src="src" alt="美女" />
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

  var obj = {
    url: "https://segmentfault.com/",
    src:"http://eveningwater.com/project/imggallary/img/15.jpg"
  };
  var vm = new Vue({
    el: "#app",
    data() {
      return obj;
    }
  }); 
  

效果如图所示:

图片描述
你可以点击此处具体示例查看。

v-on指令还可以添加修饰符,v-bindv-on指令还可以缩写成:@。缩写对于我们在繁琐的使用指令的项目当中是一个很不错的帮助。

5.计算属性

模板表达式提供给我们处理简单的逻辑,对于更复杂的逻辑,我们应该使用计算属性。来看两个示例的对比:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>mustache</title>
  </head>
  <body>
    <div id="app">
      <span>{{ message.split('').reverse().join('') }}</span>
    </div>
    <script>
        //这里写JavaScript代码
    </script>
  </body>
</html>  

js代码:

 var obj = {
   message:"hello,vue.js!"
 }
 var vm = new Vue({
     data:obj
 })
 vm.$mount(document.querySelector('#app'))

第二个示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>mustache</title>
  </head>
  <body>
    <div id="app">
      <span>{{ reverseMessage }}</span>
    </div>
    <script>
        //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

var obj = {
   message:"hello,vue.js!"
 }
 var vm = new Vue({
     data:obj,
     computed:{
         reverseMessage:function(){
            return this.message.split('').reverse().join('');
         }
     }
 })
 vm.$mount(document.querySelector('#app')) 
 
 

与第一个示例有所不同的就是在这个示例当中,我们申明了一个计算属性reverseMessage,并且提供了一个getter函数将这个计算属性同数据属性message绑定在一起,也许有人会有疑问getter函数到底在哪里呢?
如果我们将以上示例修改一下:

var obj = {
   message:"hello,vue.js!"
 }
 var vm = new Vue({
     data:obj,
     computed:{
         reverseMessage:{
            get:function(){
               return this.message.split('').reverse().join('');
            }
         }
     }
 })
 vm.$mount(document.querySelector('#app'))
 

相信如此一来,就能明白了。你可以狠狠点击此处具体示例。你可以通过控制台修改message的值,只要message的值发生改变,那么绑定的计算属性就会发生改变。事实上,在使用reverseMessage绑定的时候,我们还可以写成调用方法一样的方式,如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>mustache</title>
  </head>
  <body>
    <div id="app">
      <span>{{ reverseMessage() }}</span>
    </div>
    <script>
        //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var obj = {
   message:"hello,vue.js!"
 }
 var vm = new Vue({
     data:obj,
     computed:{
         reverseMessage:function(){
            return this.message.split('').reverse().join('');
         }
     }
 })
 vm.$mount(document.querySelector('#app'))
 

那么这两者有何区别呢?虽然两者的结果都一样,但计算属性是根据依赖进行缓存的,只有相关依赖发生改变时它们才会重新求值。比如这里计算属性绑定的依赖就是message属性,一旦message属性发生改变时,那么计算属性就会重新求值,如果没有改变,那么计算属性将会缓存上一次的求值。这也意味着,如果计算属性绑定的是方法,那么计算属性不是响应式的。如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>mustache</title>
  </head>
  <body>
    <div id="app">
      <span>{{ date }}</span>
    </div>
    <script>
        //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var vm = new Vue({
     data:obj,
     computed:{
         reverseMessage:function(){
            return Date.now();
         }
     }
 })
 vm.$mount(document.querySelector('#app'))

与调用方法相比,调用方法总会在页面重新渲染之后再次调用方法。我们为什么需要缓存,假设你要计算一个性能开销比较大的数组,而且如果其它页面也会依赖于这个计算属性,如果没有缓存,那么无论是读取还是修改都会去多次修改它的getter函数,这并不是我们想要的。

计算属性默认只有getter函数,让我们来尝试使用一下setter函数,如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>computed</title>
  </head>
  <body>
    <div id="app">
       <input type="text" v-model="name">
    </div>
    <script>
        //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

   var vm = new Vue({
    el: "#app",
    data: {
      first_name: "li",
      last_name: "qiang"
    },
    computed: {
      name: {
        get: function() {
          return this.first_name + ' ' + this.last_name;
        },
        set: function(newValue) {
          var names = newValue.split(' ');
          this.first_name = names[0];
          this.last_name = names[names.length - 1];
        }
      }
    }
  }); 
  

现在,我们只需要修改vm.name的值就可以看到first_namelast_name的值相应的也改变了。你可以狠狠点击此处具体示例

6.侦听器

虽然计算属性在大多数情况下更合适,但有时候也可以使用侦听器。vue通过watch选项提供一个方法来响应数据的变化。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
    <title>watch</title>
    <style>
        img{
           width:200px;
           height:200px;
        }
    </style>
  </head>
  <body>
    <div id="app">
       <p>
          可以给我提出一个问题,然后我来回答?
          <input type="text" v-model="question">
       </p>
       <p>{{ answer }}</p>
       <img :src="answerImg" alt="答案" v-if="answerImg"/>
    </div>
    <script>
        //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

    var vm = new Vue({
        el:"#app",
        data(){
            return{
                answer:"我不能回答你除非你提出一个问题!",
                question:"",
                answerImg:""
            }
        },
        created:function(){
           // `_.debounce` 是一个通过 Lodash 限制操作频率的函数。
           // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
           // AJAX 请求直到用户输入完毕才会发出。想要了解更多关于
           // `_.debounce` 函数 (及其近亲 `_.throttle`) 的知识,
           // 请参考:https://lodash.com/docs#debounce
           this.debounceGetAnswer = _.debounce(this.getAnswer,500);
        },
        //如果question值发生改变
        watch:{
           question:function(oldValue,newValue){
              this.answer="正在等待你停止输入!";
              this.debounceGetAnswer();
           }
        },
        methods:{
            getAnswer:function(){
               //如果问题没有以问号结束,则返回
               if(this.question.indexOf('?') === -1){
                 this.answer = "提出的问题需要用问号结束!";
                 return;
               }
               this.answer = "请稍等";
               var self = this;
               fetch('https://yesno.wtf/api').then(function(response){
                   //fetch发送请求,json()就是返回数据
                   response.json().then(function(data) {
                      self.answer = _.capitalize(data.answer);
                      self.answerImg = _.capitalize(data.image);
                   });
               }).catch(function(error){
                  self.answer = "回答失败,请重新提问!";
                  console.log(error);
               })
            }
        }
    })
    

现在咱们来看一下效果:

图片描述
你可以狠狠点击此处具体示例查看。

7.计算属性vs侦听器

当在页面中有一些数据需要根据其它数据的变动而改变时,就很容易滥用侦听器watch。这时候命令式的侦听还不如计算属性,请看:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>watch</title>
  </head>
  <body>
    <div id="app">
       <p>{{ fullName }}</p>
    </div>
    <script>
        //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var vm = new Vue({
      el:"#app",
      data:{
         firstName:"li",
         lastName:"qiang",
         fullName:"li qiang"
      },
      watch:{
         firstName:function(val){
            this.fullName = val + ' ' + this.lastName;
         },
         lastName:function(val){
            this.fullName = this.firstName + ' ' + val;
         }
      }
 })
 

再看通过计算属性实现的:

 
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>computed</title>
  </head>
  <body>
    <div id="app">
       <p>{{ fullName }}</p>
    </div>
    <script>
        //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var vm = new Vue({
      el:"#app",
      data:{
         firstName:"li",
         lastName:"qiang"
      },
      computed:{
         fullName:function(){
           return this.firstNmae + ' ' + this.lastName;
         }
      }
 })

通过计算属性实现的功能看起来更好,不是吗?你可以自行尝试具体示例(watch)具体示例(computed)进行对比。

8.class与style绑定

操作元素的classstyle是构建一个页面所常见的需求,因为它们都是属性,所以利用v-bind指令就可以操作元素的classstyle样式。如;

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
     <!-- 引入vue.js开发版本 -->
     <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>class</title>
    <style>
        .font-red{
            color: #f00;
        }
    </style>
</head>
<body>
    <div id="app">
        <span v-bind:class="className">添加一个class类,改变字体颜色为红色。</span>
    </div>
    <script>
    //这里写JavaScript代码
    </script>
</body>
</html>

js代码如下;

var vm = new Vue({
    el:"#app",
    data:{
        className:"font-red"
    }
})

你可以狠狠点击此处具体示例 查看。

再来看一个简单绑定style的示例。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
     <!-- 引入vue.js开发版本 -->
     <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>style</title>
</head>
<body>
    <div id="app">
        <span v-bind:style="styleProp">改变元素的字体颜色为红色。</span>
    </div>
    <script>
    //这里写javascript代码
    </script>
</body>
</html>


js代码:

var vm = new Vue({
    el:"#app",
    data:{
        styleProp:"color:#f00;"
    }
})

你可以狠狠点击此处具体示例查看。

这只是classstyle的简单用法,vue.js专门在这方面做了增强,使得绑定classstyle 的值可以是对象,也可以是数组,如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>class</title>
    <style>
      .font-red{
          color: #f00;
      }
      .font-blue{
          color: #00f;
      }
    </style>
</head>
<body>
    <div id="app">
        <span v-bind:class="{ 'font-red':isRed,'font-blue':isBlue }">改变字体的颜色</span>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
</body>
</html>  

js代码:

  var vm = new Vue({
      el:"#app",
      data:{
          isRed:true,
          isBlue:false
      }
  })
  

我们可以看到页面效果如图:

图片描述

你还可以通过控制台修改vm.isBluevm.isRed的值,这充分说明这两个值是响应式的。如:

图片描述
你可以狠狠的点击此处具体示例查看。

同样的,style一样也可以使用对象语法,如:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>style</title>
  </head>
  <body>
    <div id="app">
      <span v-bind:style="{ color:fontColor,fontSize:font18,'font-weight':fontBold }"
        >字体大小为18px,字体颜色为红色,并且加粗的字体。</span
      >
    </div>
    <script>
    //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

  var vm = new Vue({
    el: "#app",
    data: {
      fontColor: "#f00",
      font18: "18px",
      fontBold:"bold"
    }
  });
  

效果如图:

图片描述
我们一样可以修改其中的值,这些值也是响应式的,比如修改vm.fontColor="#0f0"就表示将字体颜色改变为蓝色。从上例我们也可以看出,我们可以使用驼峰式 (camelCase) 短横线分隔 (kebab-case,需要用单引号括起来)来定义css属性名。
你可以狠狠点击此处具体示例查看。

当然在更多时候,我们直接绑定一个对象更有利于让模板变得清晰,也方便我们理解。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>style</title>
  </head>
  <body>
    <div id="app">
      <span v-bind:style="styleObject"
        >字体大小为18px,字体颜色为红色,并且加粗的字体。</span
      >
    </div>
    <script>
    //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

  var vm = new Vue({
    el: "#app",
    data: {
        styleObject:{
           fontSize:"18px",
           color:"#f00",
           'font-weight':"bold"
        }
    }
  });  
  

这也是一样的效果,你可以点击此处具体示例查看。

除了对象语法,数组语法也同样适用于classstyle,如:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>class</title>
    <style>
      .font-red {
        color: #f00;
      }
      .font-18 {
        font-size: 18px;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <span v-bind:class="[fontColor,fontSize]">颜色为红色大小为18px的字体</span>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

  var vm = new Vue({
    el: "#app",
    data: {
      fontColor: "font-red",
      fontSize: "font-18"
    }
  });
  

运行效果如图:
图片描述
你同样可以修改class的名字,诸如vm.fontColor="font-blue",这样页面就会将font-red更改为font-blue,这毕竟是响应式的。你可以狠狠点击此处具体示例查看。

同样的,style也能如此做,如:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>style</title>
  </head>
  <body>
    <div id="app">
      <span v-bind:style="[colorF,sizeF]">颜色为红色大小为18px的字体</span>
    </div>
    <script>
      //javascript代码
    </script>
  </body>
</html>

js代码:

 var vm = new Vue({
    el: "#app",
    data: {
        colorF: {
            color:"#f00"
        },
        sizeF: {
            fontSize:"18px"
        }
    }
  });

这里尤其注意如下的写法是错误的,vue.js并不能渲染出样式:

  //这说明style绑定的数组项只能是一个对象,而不能是字符串
 var vm = new Vue({
    el: "#app",
    data: {
        colorF: "color:#f00;",
        sizeF: "font-size:18px;"
    }
  });

同样,我们注意修改值的时候也应该修改成一个对象,如:

vm.sizeF = {
   'font-size':"20px"
}

这点是需要注意的,另外在遇到带有前缀的css属性,如transition时,我们不必写前缀,因为vue会自动帮我们添加前缀。你可以狠狠点击此处具体示例查看。

style的用法还不止如此,我们还可以绑定多重值。如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>style</title>
  </head>
  <body>
    <div id="app">
      <span v-bind:style="{ display:[webkitD,nomarD] }">颜色为红色大小为18px的字体</span>
    </div>
    <script>
     //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var vm = new Vue({
    el: "#app",
    data: {
        webkitD:"-webkit-flex",
        nomarD:"flex"
    }
  });
  

这样一来,浏览器会根据支持-webkit-flexflex而采用支持的写法,这个是在vue2.3.0+版本中增加的功能。你可以点击此处具体示例查看。

9.条件渲染

v-if指令用于条件性的渲染一块内容,这个指令只在它绑定的值为truthy的时候才会渲染内容。例如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-if</title>
  </head>
  <body>
    <div id="app">
        <p v-if="true">正常显示</p>
        <span v-if="false">不显示</span>
        <div v-if="show">也是正常显示</div>
        <a href="#" v-if="hidden">也是不显示</a>
    </div>
    <script>
      var vm = new Vue({
        el: "#app",
        data:{
            show:true,
            hidden:false
        }
      });
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。

v-if指令也可以与v-else指令结合使用,注意v-else必须紧跟v-if或者v-else-if之后。比如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-if</title>
  </head>
  <body>
    <div id="app">
        <p v-if="true">正常显示</p>
        <span v-else="false">不显示</span>
        <div v-if="show">也是正常显示</div>
        <a href="#" v-else>也是不显示</a>
    </div>
    <script>
      var vm = new Vue({
        el: "#app",
        data:{
            show:true
        }
      });
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。

v-if也可以直接在<template></template>标签上使用,这种情况下,我们通常是为了切换多个元素,因为v-if必须添加到一个元素上,而且会把template当作不可见元素来渲染,也就是说最终渲染不会包含template元素。比如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-if</title>
  </head>
  <body>
    <div id="app">
        <template  v-if="show">
            <p>呵呵呵</p>
            <h1>哈哈哈</h1>
            <div>嘻嘻嘻</div>
            <span>嘿嘿嘿</span>
        </template>
    </div>
    <script>
      var vm = new Vue({
        el: "#app",
        data:{
            show:true
        }
      });
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。

vue2.1.0新增了v-else-if,顾名思义,也就是紧跟v-if之后,v-else之前的指令,可以使用多个v-else-if指令。比如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-if</title>
  </head>
  <body>
    <div id="app">
        <p v-if="type===0">哈哈哈</p>
        <span v-else-if="type===1">嘿嘿嘿</span>
        <div v-else-if="type===2">嘻嘻嘻</div>
        <h2 v-else>呵呵呵</h2>
    </div>
    <script>
      var vm = new Vue({
        el: "#app",
        data:{
            type:0
        }
      });
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。在这些示例中,只要绑定的是在vue实例data选项中的数据,那么值就是响应式的,我们可以直接在控制台中修改,比如以上的vm.type = 1,我们就可以看到页面的的元素以及内容被改变,并重新渲染。

由于vue是简单的复用元素,而不是重新渲染元素,因此,这会让vue非常的高效,但这不可避免出现了一个问题,如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-if</title>
</head>

<body>
    <div id="app">
        <template v-if="type==='name'">
            <label>姓名:</label>
            <input type="text" placeholder="请输入姓名">
        </template>
        <template v-else-if="type==='email'">
            <label>邮箱:</label>
            <input type="text" placeholder="请输入邮箱">
        </template>
        <template v-else>
            <label>密码:</label>
            <input type="password" placeholder="请输入密码">
        </template>
        <button type="button" @click="changeType">切换</button>
    </div>
    <script>
        var vm = new Vue({
            el: "#app",
            data: {
                type: "name",
                count:0
            },
            methods:{
                changeType:function(){
                    this.count++;
                    if(this.count % 3 === 0){
                        this.type = 'name';
                    }else if(this.count % 3 === 1){
                        this.type = 'email';
                    }else{
                        this.type="password"
                    }
                }
            }
        });
    </script>
</body>

</html>

你可以狠狠点击此处具体示例查看效果。在输入框中输入值,然后再点击切换按钮,你会发现input的内容并没有被清空,这也说明vue并不是重新渲染元素,而是高效的复用元素而已。再实际开发中,这样肯定是不符合需求的,那么我们应该如何解决这个问题呢?

还好,vue提供了一个key属性,我们只需要给每个复用的元素绑定一个key属性,用于区分它们是不同的元素。如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-if</title>
</head>

<body>
    <div id="app">
        <template v-if="type==='name'">
            <label>姓名:</label>
            <input type="text" placeholder="请输入姓名" key="name">
        </template>
        <template v-else-if="type==='email'">
            <label>邮箱:</label>
            <input type="text" placeholder="请输入邮箱" key="email">
        </template>
        <template v-else>
            <label>密码:</label>
            <input type="password" placeholder="请输入密码" key="password">
        </template>
        <button type="button" @click="changeType">切换</button>
    </div>
    <script>
        var vm = new Vue({
            el: "#app",
            data: {
                type: "name",
                count:0
            },
            methods:{
                changeType:function(){
                    this.count++;
                    if(this.count % 3 === 0){
                        this.type = 'name';
                    }else if(this.count % 3 === 1){
                        this.type = 'email';
                    }else{
                        this.type="password"
                    }
                }
            }
        });
    </script>
</body>

</html>

现在你再尝试在输入框中输入值,然后点击切换按钮,就会发现值会被清空了。请点击具体示例查看效果。

需要注意的是label元素其实也是被复用了,因为它们没有添加key属性。

v-show的指令用法跟v-if差不多,唯一需要注意的区别就是v-show仅仅只是改变了元素的display属性而已,其DOM元素仍然存在于文档中,并且v-show之后没有v-else-ifv-else指令。看一个简单的示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-show</title>
  </head>
  <body>
    <div id="app">
        <p v-show="true">正常显示</p>
        <span v-show="false">不显示</span>
        <div v-show="show">也是正常显示</div>
        <a href="#" v-show="hidden">也是不显示</a>
    </div>
    <script>
      var vm = new Vue({
        el: "#app",
        data:{
            show:true,
            hidden:false
        }
      });
    </script>
  </body>
</html>

具体效果如下图所示:

clipboard.png

你可以狠狠点击此处具体示例查看效果。

所以我们也可以看得出来v-ifv-show的区别:

clipboard.png

还要注意的一点就是不推荐v-ifv-for一起使用,因为它们同时使用的时候,v-for的优先级高于v-if

10.列表渲染

vue.js使用v-for指令来渲染一个列表,形式类似item in itemsitem of items,在这之中,item是数组中每一项的迭代名称,而items则是源数据,在渲染列表的时候,通常都要添加一个key属性,用以给vue.js一个提示,以便vue.js更好的跟踪每个节点的变化。key属性工作方式就像一个属性,因此使用v-bind指令来绑定,并且key属性也是唯一的。理想的key属性就是每一个数组项的唯一id。来看一个示例:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-for</title>
</head>
<body>
    <div id="app">
        <ul v-for="item in items">
            <li>{{ item }}</li>
        </ul>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
</body>
</html>


js代码:

   var vm = new Vue({
      el:"#app",
      data:{
          items:['html','css','javascript']
      }
  })
  

数组中的数组项还可以是对象,如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-for</title>
</head>
<body>
    <div id="app">
        <ul v-for="item in items">
            <li>{{ item.id }}.{{ item.value }}</li>
        </ul>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
</body>
</html> 

js代码:

  var vm = new Vue({
      el:"#app",
      data:{
          items:[
              {
                  id:1,
                  value:"html"
              },
              {
                  id:2,
                  value:"css"
              },
              {
                  id:3,
                  value:"javascript"
              }
          ]
      }
  })
  

你可以狠狠点击具体示例一具体示例二查看。

这也就是说,v-for包含块中的父作用域,我们是有完全访问的权限的,而且v-for还提供第二个可选参数,表示对当前项的索引。如:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-for</title>
</head>
<body>
    <div id="app">
        <ul v-for="(item,index) in items">
            <li>索引:{{ index }}-{{ item }}</li>
        </ul>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
</body>
</html>

js代码如下:

   var vm = new Vue({
      el:"#app",
      data:{
          items:['html','css','javascript']
      }
  }) 
  

你可以点击此处具体示例查看。

v-for同样可以渲染一个对象,可选有三个参数,第一个参数为对象值,第二个参数为对象属性键名,第三个参数则是索引。如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-for</title>
</head>
<body>
    <div id="app">
        <ul v-for="(value,key,index) in info">
            <li>索引:{{ index }} + 属性名:{{ key }} + 属性值:{{ value }}</li>
        </ul>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
</body>
</html> 


js代码:

   var vm = new Vue({
      el:"#app",
      data:{
          info:{
              name:"李白",
              value:"李太白"
          }
      }
  })
  

你可以狠狠点击此处具体示例查看。

为了vue.js 能够高效的更新虚拟DOM,我们有必要给vuev-for提供一个key属性,理想的key属性就是每一项的id。这是一个属性,所以使用v-bind来绑定,添加key属性是方便vue跟踪每个节点,从而根据数据的变化来重排序节点,要理解key属性的意义,你可能需要理解虚拟DOM的DIFF算法的知识。虽然vue.js默认就采用就地复用的策略,但这只针对不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出,所以最好的方法就是添加key属性。

 <!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-for</title>
  </head>
  <body>
    <div id="app">
        <div v-for="item in items" :key="item.id" :class="item.value">
            <p>{{ item.value }}</p>
        </div>
    </div>
    <script>
        //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var vm = new Vue({
    el: "#app",
    data() {
      return{
          items:[
              {
                  id:1,
                  value:"html"
              },
              {
                  id:2,
                  value:"css"
              },
              {
                  id:3,
                  value:"javascript"
              }
          ]
      }
    }
  }); 
  

为了方便理解key属性带来的高效性,可以尝试在控制台更改数据,比如增删改查操作。你可以点击此处具体示例查看。

vue包含一组数组的变异方法,他们也可以让视图随着数据的改变而更新。方法如下:
push(),pop(),unshift(),shift(),splice(),reverse(),sort()。我们可以来看一个示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>mutation methods</title>
  </head>
  <body>
    <div id="app">
        <ul>
            <li v-for="(item,index) in items" :key="index">{{ item.name }}</li>
        </ul>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

    var vm = new Vue({
        el:"#app",
        data:{
            items:[
                {
                    id:1,
                    name:"html"
                },
                {
                    id:2,
                    name:"css"
                },
                {
                    id:3,
                    name:"javascript"
                }
            ]
        }
  });
  //vm.items.push({ id:4,name:"java"})
  //vm.items.pop()
  //vm.items.shift()
  //vm.items.unshift({ id:4,name:"java"})
  //vm.items.splice(0,1,{ id:1,name:"java"})
  //vm.items.reverse()

尝试在控制台,使用这些变异方法修改数组items的值,我们就可以看到这些方法的作用了。你可以狠狠点击此处具体示例查看。

变异的方法,顾名思义,就是会改变原数组,既然有变异方法,那当然也有非变异的方法,比如:filter(),slice()concat(),虽然这些方法不会改变一个数组,但总是会返回要给新数组,我们可以用新数组替换原数组。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>mutation methods</title>
  </head>
  <body>
    <div id="app">
      <ul>
        <li v-for="(item,index) in items" :key="index">{{ item.name }}</li>
      </ul>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

var vm = new Vue({
    el: "#app",
    data: {
      items: [
        {
          id: 1,
          name: "html"
        },
        {
          id: 2,
          name: "css"
        },
        {
          id: 3,
          name: "javascript"
        }
      ]
    }
  });
  //   vm.items = vm.items.filter(function(item) {
  //     return item.name.match(/javascript/);
  //   });
//   vm.items = vm.items.concat([{ id: 4, name: "java" }]);
// vm.items = vm.items.slice(1); 

尝试在控制台将以上注释的js代码给测试一下,你可以狠狠点击此处具体示例尝试一下。

通过以上示例,你可能会认为vue抛弃了原数组,重新渲染了DOM,但实际上并不是这样,这也说明这样替换数组是非常高效的一个操作。

虽然我们可以使用变异方法修改数组,从而达到视图的更新,但我们也要注意两点,那就是以下两点并不会触发视图的更新:

1.根据数组的索引来修改数组项的值。示例如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>limit</title>
  </head>
  <body>
    <div id="app">
      <ul>
        <li v-for="(item,index) in items" :key="index">{{ item.name }}</li>
      </ul>
    </div>
    <script>
        //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

  var vm = new Vue({
    el: "#app",
    data: {
      items: [
        {
          id: 1,
          name: "html"
        },
        {
          id: 2,
          name: "css"
        },
        {
          id: 3,
          name: "javascript"
        }
      ]
    }
  });
//vm.items[0].value = "java";并不会触发视图的更新。

你可以点击此处具体示例亲自查看。

2.修改数组的长度。如以下示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>limit</title>
  </head>
  <body>
    <div id="app">
      <ul>
        <li v-for="(item,index) in items" :key="index">{{ item.name }}</li>
      </ul>
    </div>
    <script>
        //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

  var vm = new Vue({
    el: "#app",
    data: {
      items: [
        {
          id: 1,
          name: "html"
        },
        {
          id: 2,
          name: "css"
        },
        {
          id: 3,
          name: "javascript"
        }
      ]
    }
  });
//vm.items.length= 2;并不会触发视图的更新。  

你同样可以点击此处具体示例进行查看。

针对第一个问题,我们可以通过Vue.set()方法和变异方法splice()方法来解决,如下:

 //第一种方案
Vue.set(vm.items[0],'name','java')
//或this.$set(vm.items[0],'name','java');
//第二种方案
vm.items.splice(0,1,{ id:1,name:"java"});

你可以点击此处具体示例查看。

针对第二个问题,你可以使用splice()方法来改变。如下:

vm.items.splice(2); 

你可以点击此处具体示例进行查看。

从以上的示例,我们可以看出Vue.set()this.$set()[只是一个别名而已]的参数可以传3个,即要改变的数组项,数组项的索引或者属性名,新数组项的属性值。而splice()则可以传1到3个参数,即数组的起始项索引,删除几项,从第三个参数开始就是需要替换的项或者说是需要添加的项

对于对象,其实也仍然有限制,对象的新增和删除,vue是无法检测的。来看如下例子:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>limit</title>
  </head>
  <body>
    <div id="app">
      <ul>
        <li>{{ item.value }}</li>
        <li>{{ item.name }}</li>
      </ul>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

var vm = new Vue({
    el: "#app",
    data: {
      item: {
          value:"123"
      }
    }
  });
  //vm.item.name = "javascript";//无效
  //delete vm.item.value;//无效

你可以狠狠点击此处具体示例查看。

针对以上属性的添加的问题,我们还是使用Vue.set()方法来解决。而属性的删除,我们可以使用Vue.delete()来解决,理论上在项目开发中应该很少用到这个方法。

//对象属性的添加
Vue.set(vm.item,'name','javascript');//或this.$set(vm.item,'name','javascript'); 
//对象属性的删除
Vue.delete(vm.item,'value');

当我们需要添加多个对象时,可以使用Object.assign()或_.extend()[jquery方法],如下:

vm.item = Object.assign({},vm.item,{ age:26,skill:['html','css','javascript']})
    //或者vm.item = $.extend({},vm.item,{ age:26,skill:['html','css','javascript']})

你可以狠狠点击此处具体示例查看。

有时候,在某种情况下,我们会需要过滤或者排序一个数组,并且不能改变原数组,从而渲染出满足条件的数组项,这时候,我们可以通过计算属性结合数组的迭代方法filter() 这个方法不会改变原数组的值。我们来看一个示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>filter array</title>
  </head>
  <body>
    <div id="app">
        <ul>
            <li v-for="(item,index) in filterNumbers" :key="index">{{ item }}</li>
        </ul>
    </div>
    <script>
     //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

 var vm = new Vue({
        el:"#app",
        data:{
            items:[1,2,3,4,5,6,7,8,9]
        },
        computed:{
            filterNumbers(){
                return this.items.filter((num) => {
                    return num < 5;
                })
            }
        }
  });
  
  

你可以狠狠点击此处具体示例查看效果。
当然,你也可以在计算属性不适用的情况下,使用方法来替代。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>filter array</title>
  </head>
  <body>
    <div id="app">
        <ul>
            <li v-for="(item,index) in filterNumbers(items)" :key="index">{{ item }}</li>
        </ul>
    </div>
    <script>
     //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

 var vm = new Vue({
        el:"#app",
        data:{
            items:[1,2,3,4,5,6,7,8,9]
        },
        methods:{
            filterNumbers(numbers){
                return numbers.filter((num) => {
                    return num < 5;
                })
            }
        }
  });
  

你可以狠狠点击此处具体示例查看效果。

也许有时候,我们是没必要渲染每一个父元素的,只需要渲染子元素的,这时候我们就可以使用template元素来使用v-for指令渲染元素,渲染的数据也不一定是一个数组,也可以是一个整数。需要注意的就是当添加key属性时,需要将这个属性添加到真实的DOM元素上,这也就是说template元素是不推荐使用key属性的。如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>template </title>
  </head>
  <body>
    <div id="app">
        <ul>
            <template v-for="n in number" >
                <span :key="n">{{ n }}</span>
            </template>
        </ul>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

  var vm = new Vue({
        el:"#app",
        data:{
            number:100
        },  
  });

你可以狠狠点击此处具体示例查看效果。

v-ifv-for指令结合一起使用时,由于v-if指令的优先级要高于v-for,因此每次进判断都要重复遍历一道数组,不过这在只需要渲染某些项的时候会非常有用。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-for with v-if </title>
  </head>
  <body>
    <div id="app">
        <ul>
            <li v-for="(item,index) in items" :key="index" v-if="item.show">
                <span >{{ item.value }}</span>
            </li>
        </ul>
    </div>
    <script>
     //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

 var vm = new Vue({
        el:"#app",
        data:{
            items:[
                {
                    show:true,
                    value:'html'
                },
                {
                    show:false,
                    value:"css"
                },
                {
                    show:false,
                    value:"javascript"
                }
            ]                
        },  
  });

你可以狠狠点击此处具体示例进行查看。

v-for还可以用在组件当中,但在组件中使用v-for是必须要加上key属性的。如下是一个简易版的todolist组件示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>todolist demo</title>
  </head>
  <body>
    <div id="app">
      <ul>
        <form @submit.prevent="addNewToDo">
          <span>add a need to do thing</span>
          <input type="text" v-model="newToDo" placeholder="eg:write the code"/>
          <button type="button" @click="addNewToDo">add</button>
        </form>
        <todo-item         
          v-for="(item,index) in items" :key="index"
          :item="item"
          :index="index"
          @on-remove="items.splice(index,1)"
        ></todo-item>
      </ul>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

Vue.component("todoItem", {
    template: `
        <div>
            <li v-if="item.show">
                <span>{{ item.value }}</span>
                <button type="button" @click="$emit('on-remove')">remove</button>
            </li>
        </div>
      `,
    props: ["item"]
  });
  var vm = new Vue({
    el: "#app",
    data: {
      newToDo: "",
      items: [
        {
          show: true,
          value: "html"
        },
        {
          show: true,
          value: "css"
        },
        {
          show: true,
          value: "javascript"
        }
      ]
    },
    methods: {
      addNewToDo() {
        if (this.newToDo) {
          this.items.push({
            value: this.newToDo,
            show: true
          });
        }else{
            alert('please input your need to do thing!')
        }
      }
    }
  }); 
  

你可以狠狠点击此处具体示例进行查看。

11.事件处理

Vue中使用v-on(简写@)指令来为DOM添加点击事件。如:


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
        <button v-on:click="count += 1">click me!</button>
      <p>The button above has been clicked {{ count }} times!</p>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码:

 var vm = new Vue({
    data:{
        count:0
    }
  });
  vm.$mount(document.getElementById("app"));
  

你可以狠狠点击此处具体示例查看效果。

很多时候,事件的复杂程度远不止如此,因此,事件可以用方法来表示。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
        <button v-on:click="clickMethod">click me!</button>
      <p>The button above has been clicked {{ count }} times!</p>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

  var vm = new Vue({
    data:{
        count:0
    },
    methods:{
        clickMethod:function(event){
            //this指向vm这个vue实例
            this.count += 1;
            alert('你点击了按钮' + this.count + '次!');
            alert(event.target.tagName);
        }
    }
  });
  //还可以直接调用方法
//   vm.clickMethod();
  vm.$mount(document.getElementById("app"));

你可以狠狠点击此处具体示例查看效果。

除了直接绑定方法,当然还可以在内联JavaScript语句中调用方法。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
      <button v-on:click="clickMethod(msg1)">click me!</button>
      <button v-on:click="clickMethod(msg2)">click me!</button>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

var vm = new Vue({
    data: {
      msg1:'hello,',
      msg2:"world!"
    },
    methods: {
      clickMethod: function(message) {
        alert(message)
      }
    }
  });
  vm.$mount(document.getElementById("app"));

你可以狠狠点击此处具体示例查看效果。

当然有时候我们需要访问原生DOM事件,这时候可以使用一个特殊的变量$event传入方法。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
      <button v-on:click="clickMethod(msg,$event)">click me!</button>
    </div>
    <script>
      //这里写JavaScript代码
    </script>
  </body>
</html>

js代码如下:

var vm = new Vue({
    data: {
      msg: "hello,world!"
    },
    methods: {
      clickMethod: function(message, event) {
        //现在你可以访问原生的事件对象
        if (event) event.preventDefault();
        alert(message);
      }
    }
  });
  vm.$mount(document.getElementById("app")); 

你可以狠狠点击此处具体示例查看效果。

在事件中阻止事件的默认行为或者阻止事件冒泡是非常常见的需求,通常阻止事件的默认行为,都可以在事件方法内调用事件对象的preventDefault()方法。例如:

   var myDiv = document.getElementById('myDiv');
   myDiv.onclick = function(event){
       //event即事件对象
       event.preventDefault();
   }

再比如阻止事件冒泡,即调用stopPropagation()。例如以下一个示例:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <style>
      #parent,#child{
        margin: 0;padding: 0;
      }
      #parent{
        width: 200px;
        height: 200px;
        text-align: center;
        line-height: 200px;
        background-color: #ff0000;
      }
      #child{
        width: 100px;
        height: 100px;
        text-align: center;
        line-height: 100px;
        background-color: #00ff00;
      }
    </style>
  </head>
  <body>
    <div id="parent">
        父元素
        <div id="child">子元素</div>
    </div>
    <script>
      parent.onclick = function(){
         alert('点击了父元素!')
      }
      child.onclick = function(e){
        // e.stopPropagation();
        alert('点击了子元素!')
      }
    </script>
  </body>
</html>

如果不对子元素调用stopPropagation(),那么父元素的事件就会传播到子元素上,也就是说会弹出两个弹出框,父元素的事件冒泡到了子元素上,而如果调用该方法,则表明父元素的事件被阻止冒泡了,结果点击子元素也就只弹出一个弹出框提示"点击了子元素",这是在原生当中的用法,而vue则提供了事件修饰符。
事件修饰符以点开头加上指令后缀。包含.stop,.prevent,.capture,.self,.once,.passive等事件修饰符。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
    <style>
        #parent{
            width: 200px;
            height: 200px;
            background-color: #ff0000;
            text-align: center;
            line-height: 200px;
        }
        #child{
            width: 100px;
            height: 100px;
            background-color: #00f000;
            text-align: center;
            line-height: 100px;
            display: inline-block;
        }
    </style>
  </head>
  <body>
    <div id="app">
       <div id="parent" @click="clickParent">
           父元素
           <div id="child" @click="clickChild">子元素</div>
           <!--<div id="child" @click.stop="clickChild">子元素</div> -->
       </div>
       <p>{{ count }}</p>
    </div>
    <script>
      var vm = new Vue({
        data:{
            count:0
        },
        methods:{
            clickParent(){
                this.count++;
            },
            clickChild(){
                this.count = 4;
            }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

对比加了.stop和不加.stop的效果如下图所示:

//加了

图片描述

//不加

图片描述

你可以狠狠点击此处具体示例查看效果。

.prevent事件修饰符其实也就是阻止事件的默认行为,比如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
     <form action="" method="post">
         <input type="text" placeholder="请输入账号">
         <input type="password" placeholder="请输入密码">
         <input type="submit" value="提交" @click.prevent="submit">
     </form>
    </div>
    <script>
      var vm = new Vue({
        data: {
         
        },
        methods: {
            submit(){
                alert('阻止了表单提交');
            }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

尝试将.prevent事件修饰符去掉,你会发现当你点击了提交按钮提交的时候,页面会重载。你可以狠狠点击此处具体示例进行查看。

.capture修饰符的作用就是让元素最先执行事件,如果有多个元素拥有该事件修饰符,那么则由外到内依次触发该事件。如下例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
      <div @click.capture="doWhat">
        点击我
        <p @click.capture="doWhat">
          <span @click="doWhat">请点击我</span>
          <br>
          点击我啊
        </p>
      </div>
      <span>{{ count }}</span>
    </div>
    <script>
      var vm = new Vue({
        data: {
          count: 0
        },
        methods: {
          doWhat() {
            this.count++;
          }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

当点击最外层的div元素时,触发最外层的事件,当点击到p元素(不是span元素),则先执行div的事件,然后再执行p元素,因此count的值就会相加两次。同理,点击span元素,则相加三次。你可以狠狠点击此处具体示例查看效果。

.self事件修饰符在只有event.target的值为自身元素的时候才会触发,类似.stop事件修饰符,该事件修饰符阻止了事件的冒泡,因为event.target的值始终只会有一个值。如下例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
      <div @click.self="doWhat($event)">
        点击我
        <p @click.self="doWhat($event)">
          <span @click="doWhat($event)">请点击我</span>
          <br>
          点击我啊
        </p>
      </div>
      <span>{{ count }}</span>
    </div>
    <script>
      var vm = new Vue({
        data: {
          count: 0
        },
        methods: {
          doWhat(e) {
              console.log(e.target)
            this.count++;
          }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

如果点击div元素,那么event.target的值就是div元素,因此也就触发该元素的事件,count的值就加1,同理,点击pspan元素就是一样的道理,你可以狠狠点击此处具体示例查看效果。

.once事件修饰符只允许事件执行一次。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
      <button v-on:click.once="clickMethod">click me!</button>
      <p>{{ count }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
          count:0
        },
        methods: {
          clickMethod: function() {
            this.count++;
          }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

运行点击按钮,我们会发现count的值只会加1,不管点击按钮多少次,都没有用,这也就是说事件只会执行一次。你可以狠狠的点击此处具体示例查看效果。

.passive事件修饰符主要是为了提升页面滑动的性能,主要是对passive事件监听器的一个封装,要理解这个事件修饰符,我们需要知道原生的Passive Event Listeners。这是chrome提出的一个新的浏览器特性:Web开发者通过一个新的属性passive来告诉浏览器,当前页面内注册的事件监听器内部是否会调用preventDefault函数来阻止事件的默认行为,以便浏览器根据这个信息更好地做出决策来优化页面性能。当属性passive的值为true的时候,代表该监听器内部不会调用preventDefault函数来阻止默认滑动行为,Chrome浏览器称这类型的监听器为被动(passive)监听器。目前Chrome主要利用该特性来优化页面的滑动性能,所以Passive Event Listeners特性当前仅支持mousewheel/touch相关事件。更多详情参阅。我们来看一个示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      section {
        width: 100%;
        height: 500px;
      }
      #sec-1 {
        background-color: #ff0000;
      }
      #sec-2 {
        background-color: #00ff00;
      }
      #sec-3 {
        background-color: #0000ff;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <section
        v-for="(item,index) in items"
        :key="index"
        :id="'sec-' + (index + 1)"
        @mousewheel="onScroll"
      >
        {{ item.content }}
      </section>
    </div>
    <script>
      var vm = new Vue({
        data: {
          items: [
            { content: "页面内容一" },
            { content: "页面内容二" },
            { content: "页面内容三" }
          ]
        },
        mounted() {},
        methods: {
          onScroll() {
            console.log("页面滚轮滚动!");
            //如果去掉注释,加了passive事件修饰符,页面还能流畅的滑动,但是如果不加,就不会流畅的滑动了
            // for (let i = 0; i < 1; i--) {
            //   console.log(i);
            // }
          }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

如以上注释所示,如果给section加了passive事件修饰符,并且将注释去掉,我们会发现页面还是能流畅的滑动的。你可以狠狠点击此处具体示例查看效果。

事件修饰符也可以组合使用,但组合的顺序非常重要,比如@click.self.prevent@click.prevent.self完全是两个不同的概念,前者会阻止event.target的值指向的那个元素的所有点击效果,因为是先执行.prevent事件修饰符,即阻止了该元素的所有点击事件。而后者则只是阻止了event.target的值指向的那个元素的点击事件,而不会阻止默认事件的执行。如下例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
      <div @click="clickDiv1">
        <a href="https://www.baidu.com/"  @click.prevnt.self="clickA">
          <div @click="clickDiv2">点击我</div>
        </a>
        <!-- 这两个事件修饰符互换顺序是不同的效果的:<a href="https://www.baidu.com/"  @click.self.prevent="clickA">
          <div @click="clickDiv2">点击我</div>
        </a> -->
      </div>
    </div>
    <script>
      var vm = new Vue({
        data: {
          
        },
        methods: {
          clickDiv1(){
            alert('点击了div1!');
          },
          clickA(){
            alert('点击了a标签!')
          },
          clickDiv2(){
            alert('点击了div2!')
          }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。

此外,值得注意的就是,不能将.passive.prevent事件修饰符组合在一起使用。否则浏览器会给出一个警告,如下图所示:

图片描述

在监听键盘事件时,往往需要监听键盘的键值,vue则提供了按键修饰符,允许为键盘事件添加键值。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
        <button v-on:keyup.13="clickMethod($event)">键盘按键事件</button>
        <p>你按下的键值为:{{ count }}</p>
    </div>
    <script>
      var vm = new Vue({
        data:{
            count:0
        },
        methods:{
            clickMethod(e){
                console.log(e.keyCode);
                this.count = e.keyCode;
            }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。

13是键盘上enter键的键值,通常我们不容易记住键盘的键值,这时候vue提供了别名。如以上的键盘事件可以改写为:

v-on:keyup.enter //@keyup.enter

目前已有的全部按键别名有.enter,.tab,.delete,.esc,.space,.up,.down,.left,.right,当然我们还可以通过全局去自定义别名。
比如设置数字键0(字母键的正上方的数字键,键值为48)的别名,写法如:Vue.config.keyCodes.num0 = 48。如示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
      <button v-on:keyup.num0="clickMethod($event)">键盘按键事件,鼠标定位到按钮上</button>
      <p>你按下的键值为:{{ count }}</p>
    </div>
    <script>
      Vue.config.keyCodes.num0 = 48;
      var vm = new Vue({
        data: {
          count: 0
        },
        methods: {
          clickMethod(e) {
            console.log(e.keyCode);
            this.count = e.keyCode;
          }
        }
      });

      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击具体示例查看效果。

也可以通过直接将keyboardEvent.key暴露的任意有效键名转换成kebab-case(短横线命名)来作为修饰符,比如键盘上的PageDown键可以写成page-down,PageUp可以写成page-up,End可以写成end等。如下例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
      <button v-on:keyup.end="clickMethod($event)">键盘按键事件,鼠标定位到按钮上</button>
      <p>你按下的键值为:{{ count }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
          count: 0
        },
        methods: {
          clickMethod(e) {
            console.log(e.keyCode);
            this.count = e.keyCode;
          }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

将鼠标定位到按钮上,按住键盘上的end键,查看效果。你可以狠狠点击此处具体示例查看效果。

值得注意的就是,有一些按键(.esc以及所有的方向键)在IE9中有不同的key值,如果想支持IE9,它们的内置别名应该是首选。

除了常用键以外,还有四个系统修饰键,即.ctrl,.alt,.shift,.meta(在 Mac 系统键盘上,meta 对应 command 键 (⌘)。在 Windows 系统键盘 meta 对应 Windows 徽标键 (⊞)。在 Sun 操作系统键盘上,meta 对应实心宝石键 (◆)。在其他特定键盘上,尤其在 MIT 和 Lisp 机器的键盘、以及其后继产品,比如 Knight 键盘、space-cadet 键盘,meta 被标记为“META”。在 Symbolics 键盘上,meta 被标记为“META”或者“Meta”),这四个键通常是和其他常规按键一起组合使用,也就是说只有当按下这四个系统修饰键,再按下相应的常规键,才能触发键盘事件,而且这四个键单独使用是没有效果的。要想让四个键单独有效果,那么你就只能使用keyCode
比如@keyup.ctrl你应该写成@keyup.17这样才会单独按下ctrl键,发生键盘事件,产生效果。所以,我们只能像下面那样使用:

 <!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
      <button v-on:keyup.shift.num0="clickMethod($event)">键盘按键事件,鼠标定位到按钮上</button>
      <p>你按下的键值为:{{ count }}</p>
    </div>
    <script>
      Vue.config.keyCodes.num0 = 48;
      var vm = new Vue({
        data: {
          count: 0
        },
        methods: {
          clickMethod(e) {
            console.log(e.keyCode);
            this.count = e.keyCode;
          }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

鼠标光标定位到按钮上,按下shift键,再同时按下数字键0(左上字母键对应的数字键0),就能看到效果呢。你可以狠狠点击此处具体示例查看效果。

当然我们也可以将系统修饰符组合使用到非键盘事件,比如click事件中,如下例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
      <button v-on:click.ctrl="clickMethod($event)">键盘按键事件,鼠标定位到按钮上</button>
      <p>事件名为:{{ eventname }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
          eventname: 0
        },
        methods: {
          clickMethod(e) {
            this.eventname = e.type;
          }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

鼠标定位到按钮上,首先单独点击按钮是没有效果的,需要按住键盘上的ctrl键,然后再点击按钮,才能产生效果。你可以狠狠点击此处具体示例查看效果。

由此看来,单独使用.exact修饰符,就表示只要按下了系统修饰键,就不会执行点击事件。如下例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
      <button @click.exact="clickMethod($event)">键盘按键事件,鼠标定位到按钮上</button>
      <p>你按下的键值为:{{ count }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
          count: 0
        },
        methods: {
          clickMethod(e) {
            this.count = e.type;
          }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

当按住键盘上的ctrl,shift,meta,alt键,然后再点击按钮就不会有效果。你可以狠狠点击此处具体示例查看效果。

除了常用键盘修饰符以及系统修饰符之外,vue还提供了鼠标按钮修饰符,用于指定鼠标哪个按键按下才会执行事件。鼠标按钮修饰符有.left,.middle,.right。如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-on</title>
  </head>
  <body>
    <div id="app">
      <button @click.middle="clickMethod($event)">按钮</button>
      <p>鼠标按钮按住中间的滚轮键触发:{{ event }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
          event:""
        },
        methods: {
          clickMethod(e) {
            this.event = e.type;
          }
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

用鼠标滚轮键点击按钮,就可以查看效果,你可以狠狠点击此处具体示例查看效果。

12.表单输入绑定

vue可以通过v-model指令在<input>,<select>,<textarea>这三种表单元素上创建数据双向绑定,这个指令会根据表单控件类型选择正确的方法来更新元素。v-model只不过是一个语法糖,通过监听用户输入事件从而达到更新数据的目的,这在一些前端极端场景中尤为重要。

在使用v-model指令时,需要注意它会忽略所有表单元素中的valued,selected,checked等特性的初始值,而将vue实例的data对象作为数据初始值,所以在使用过程中需要在data选项中声明初始值。

一个最简单的示例如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-model</title>
  </head>
  <body>
    <div id="app">
      <input type="text" v-model="message" />
      <p>绑定的数据:{{ message }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
          message: "hello,vue.js!"
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

当用户在输入框中输入数据的时候,对应绑定在data选项中的数据就会发生改变。你可以狠狠点击此处具体示例查看效果。

还可以绑定在textarea标签上,如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-model</title>
  </head>
  <body>
    <div id="app">
      <textarea type="text" v-model="message"></textarea>
      <p>绑定的数据:{{ message }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
          message: "hello,vue.js!"
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。

绑定到单个复选框示例如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-model</title>
  </head>
  <body>
    <div id="app">
      <input type="checkbox" v-model="isChecked">
      <p>是否选中:{{ isChecked }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
            isChecked: false
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。

除此之外,还可以将多个复选框绑定到一个数组,如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-model</title>
  </head>
  <body>
    <div id="app">
      <input type="checkbox" id="name" value="eveningwater" v-model="checkArr"/>
      <label for="name">eveningwater</label>
      <input type="checkbox" id="sex" value="male" v-model="checkArr"/>
      <label for="sex">male</label>
      <input type="checkbox" id="skill" value="write code" v-model="checkArr" />
      <label for="skill">write code</label>
      <p>选中的值:{{ checkArr }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
            checkArr: []
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。

还可以绑定单选按钮,如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-model</title>
  </head>
  <body>
    <div id="app">
      <input type="radio" id="first" value="first" v-model="checkedValue">
      <label for="first">first</label>
      <input type="radio" id="second" value="second" v-model="checkedValue" />
      <label for="second">second</label>
      <p>选中的值:{{ checkedValue }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
            checkedValue: []
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。

绑定到select的示例如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-model</title>
  </head>
  <body>
    <div id="app">
      <select v-model="message">
          <option disabled value="">请选择值</option>
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
      </select>
      <p>选中的值:{{ message }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
          message: ""
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。

在这里需要注意的就是,如果v-model绑定的初始值未匹配任何选项,那么select元素就会渲染为'未选中状态',在IOS中,这会导致用户无法选中第一项,因为并没有触发change事件,因此更推荐像以上那样提供一个默认禁用的选项。

select多选时:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-model</title>
  </head>
  <body>
    <div id="app">
      <select v-model="message" multiple>
          <option disabled value="">请选择值</option>
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="3">3</option>
      </select>
      <p>选中的值:{{ message }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
          message: []
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。

还可以用v-for指令渲染动态选项:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-model</title>
  </head>
  <body>
    <div id="app">
      <select v-model="selected">
        <option v-for="(option,index) in selects" :value="option.value">{{
          option.text
        }}</option>
      </select>
      <p>选中的值:{{ selected }}</p>
    </div>
    <script>
      var vm = new Vue({
        data: {
          selected: "first",
          selects: [
            {
              value: "first",
              text: "第一项"
            },
            {
              value: "second",
              text: "第二项"
            },
            {
              value: "third",
              text: "第三项"
            }
          ]
        }
      });
      vm.$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看效果。

对于单选按钮,复选框,选择框来说,v-model指令通常绑定的是静态字符串,对于复选框,还可以是布尔值。它们的value值,我们也是可以绑定到vue实例的data选项上的,而且绑定的值不一定是静态字符串。一个完整的示例如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-model</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .form-radio,
      .form-checkbox,
      .form-select {
        width: 600px;
        padding: 16px 8px;
        background-color: #e2e3e4;
        margin: 15px auto;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div class="form-radio">
        <input type="radio" id="male" v-model="sex" value="男" />
        <label for="male">男</label>
        <input type="radio" id="female" v-model="sex" value="女" />
        <label for="female">女</label>
        <p>你选择的是:{{ sex }}</p>
        <input type="radio" id="male" v-model="bindSex" v-bind:value="bindMale" />
        <label for="male">{{ bindMale }}</label>
        <input type="radio" id="female" v-model="bindSex" v-bind:value="bindFemale" />
        <label for="female">{{ bindFemale}}</label>
        <p>将value值动态绑定到vue实例上:{{ bindSex }}</p>
      </div>
      <div class="form-checkbox">
        <input type="checkbox" id="checked" v-model="isChecked" />
        <label for="checked">绑定的是布尔值</label>
        <p>布尔值:{{ isChecked }}</p>
        <input
          type="checkbox"
          v-model="toggle"
          true-value="是"
          false-value="否"
        />
        <p>是否选中:{{ toggle }}</p>
      </div>
      <div class="form-select">
        <select v-model="selected">
          <option value="请选择你喜欢做的事情" disabled
            >请选择你喜欢做的事情</option
          >
          <option
            v-for="(option,index) in options"
            :key="index"
            :value="option.text"
            v-text="option.text"
          ></option>
        </select>
        <p>你喜欢:{{ selected }}</p>
      </div>
    </div>
    <script>
      var vm = new Vue({
        el: "#app",
        data: {
          bindSex: "",
          bindMale: "男",
          bindFemale: "女",
          isChecked: false,
          sex: "男",
          toggle: "是",
          selected: "",
          options: [
            {
              text: "读书"
            },
            {
              text: "游戏"
            },
            {
              text: "唱歌"
            }
          ]
        }
      });
      console.log(vm.toggle === "是");
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例进行查看。

在表单绑定的时候,vue也提供了3个修饰符.lazy,.number,.trim,.lazy的作用就是改变表单元素触发的input事件,一般说来,v-model通常是在每次input事件(即用户只要输入内容时),将输入框的值与绑定的数据进行同步更新,如果不想要与input事件同步,而是与change事件同步,那么就可以使用.lazy修饰符,.number修饰符的作用就是将输入的值转换成number类型,因为默认输入的值时string(字符串)类型。而.trim修饰符的作用就是自动过滤掉首位的空白。示例如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>v-model</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .font-green {
        color: #00ff31;
      }
      .box {
        width: 400px;
        padding: 20px;
        line-height: 35px;
        margin: 20px auto;
        background-color: #e2e2e2;
      }
      .box input{
          width: 100%;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div class="box">
        <h2>未使用<span class="font-green">.lazy</span>修饰符:</h2>
        <input type="text" v-model="notMsg" />
        <p>{{ notMsg }}</p>
      </div>
      <div class="box">
        <h2>使用<span class="font-green">.lazy</span>修饰符:</h2>
        <input type="text" v-model.lazy="msg" />
        <p>{{ msg }}</p>
      </div>
      <div class="box">
        <h2>未使用<span class="font-green">.number</span>修饰符:</h2>
        <input type="text" v-model="notCount" />
        <p>
          {{ typeof notCount === 'string' ? '字符串值:"'+notCount+'"' : notCount }}
        </p>
      </div>
      <div class="box">
        <h2>使用<span class="font-green">.number</span>修饰符:</h2>
        <input type="text" v-model.number="count" />
        <p>
          {{ typeof count === 'number' ? '数值:' + count : count }}
        </p>
      </div>
      <div class="box">
        <h2>未使用<span class="font-green">.trim</span>修饰符:</h2>
        <input type="text" v-model="notTrim" />
        <p>{{ notTrim }}</p>
      </div>
      <div class="box">
        <h2>使用<span class="font-green">.trim</span>修饰符:</h2>
        <input type="text" v-model.trim="trim" />
        <p>{{ trim }}</p>
      </div>
    </div>
    <script>
      var vm = new Vue({
        el: "#app",
        data: {
          notMsg: "未使用.lazy修饰符",
          msg: "使用.lazy修饰符",
          notCount: "1",
          count: 1,
          notTrim: "首尾如果有空白,值就会有空白 ",
          trim: "首尾如果有空白,值也不会有空白,因为.trim消除了空白"
        }
      });
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例进行查看。

13.组件基础

组件是可复用的vue实例,除了el选项不一致以外,其余的data,prop,computed,methods,watch等选项都是一样的,并且组件都需要一个名字。一个基本的示例如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
  </head>
  <body>
    <div id="app">
      <button-num></button-num>
    </div>
    <script>
      Vue.component("button-num", {
        template: `<button v-on:click="num++">You click me {{ num }} times!</button>`,
        data: function() {
          return {
            num: 0
          };
        }
      });
      var vm = new Vue().$mount(document.getElementById("app"));
    </script>
  </body>
</html>

以上定义了一个简单的示例,通过这个示例我们可以知道组件是可复用的vue实例因此可以接收与new Vue()一样的选项参数,组件的名字就是button-num,通过Vue.component()来全局注册一个组件,这个方法接受2个参数,第一个参数就是组件名,第二个参数则是组件配置对象,配置对象的选项与vue实例是一致的。
你可以狠狠点击此处具体示例进行查看。

组件还可以复用,当基础组件要使用的多的时候,可以使用v-for来渲染。如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
  </head>
  <body>
    <div id="app">
      <button-num></button-num>
      <button-num></button-num>
      <button-num v-for="n in 5" :key="n"></button-num>
    </div>
    <script>
      Vue.component("button-num", {
        template: `<button v-on:click="num++">You click me {{ num }} times!</button>`,
        data: function() {
          return {
            num: 0
          };
        }
      });
      var vm = new Vue().$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以点击这里具体示例查看该示例。在这里,我们需要注意一点,组件中的data选项必须是一个函数,如果不是一个函数,控制台则会弹出一个警告,如下图所示:

图片描述

一个大型的网站项目,比如一个博客,我们可以将博客分割成多个小组件,比如导航组件,比如内容组件等等,这由许多个小组件组织起来,就成了一个SPA单页应用。如下图所示:

为了能够在模板中使用,需要先注册这些组件以便vue根实例来识别。注册组件有两种方式:全局注册局部注册,在这里,我们都是通过Vue.component()来全局注册的。
全局注册组件之后,就表示你可以在你被注册之后任何vue根实例中使用,也包括所有子组件中,到目前为止,我们不需要考虑局部注册。

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
  </head>
  <body>
    <div id="app">
        <blog-component title="博客组件的标题"></blog-component>
    </div>
    <script>
      Vue.component("blog-component", {
        template: `<h3>{{title}}</h3>`,
        props:['title']
      });
      var vm = new Vue().$mount(document.getElementById("app"));
    </script>
  </body>
</html>

在上例中,我们可以像访问data选项一样访问props传递的值,props可以接收任意类型传递的值,你可以狠狠点击此处具体示例进行查看。

一个prop被注册之后,我们其实可以像下面这样把数据作为一个自定义的特性传递:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
  </head>
  <body>
    <div id="app">
        <blog-component title="博客组件的标题1"></blog-component>
        <blog-component title="博客组件的标题2"></blog-component>
        <blog-component title="博客组件的标题3"></blog-component>
    </div>
    <script>
      Vue.component("blog-component", {
        template: `<h3>{{title}}</h3>`,
        props:['title']
      });
      var vm = new Vue().$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看该示例。

然而,在一个典型的应用当中,我们需要传递的数据不可能是如此简单的一个字符串,有可能是一个对象,甚至是一个数组。这样,以上的示例,我们需要如此修改:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
  </head>
  <body>
    <div id="app">
      <blog-component v-for="(blog,index) in blogData" :key="index" v-bind:title="blog.title"></blog-component>
    </div>
    <script>
      Vue.component("blog-component", {
        template: `<h3>{{title}}</h3>`,
        props: ["title"]
      });
      var vm = new Vue({
        data: {
          blogData: [
            { title: "博客组件的标题1", content: "博客组件的内容1" },
            { title: "博客组件的标题2", content: "博客组件的内容2" },
            { title:"博客组件的标题3",content:"博客组件的内容3"}
          ]
        }
      }).$mount(document.getElementById("app"));
    </script>
  </body>
</html>

如上所示,你应该会发现我们使用v-bind来动态传递prop,这在你不知道要渲染的具体内容,尤其是当渲染一个API数据接口时是非常有用的。到目前为止,prop需要了解的就只有这些呢。你可以点击此处具体示例查看该示例。

但在一篇博文当中,我们不仅仅是需要添加标题,最起码我们需要渲染博文的内容,因此你的模板内容就需要做修改:

template:`<h3>{{ title }}</h3><div v-html="content"></div>`;

然而如果这样写的话,vue会显示一个错误,every component must have a single root element (每个组件必须只有一个根元素),因此我们需要用一个根元素来包裹,如下:

template:`<div class="blog-demo"><h3>{{ title }}</h3><div v-html="content"></div></div>`;

当组件变得越来越复杂时,如果单独定义一个prop,这会看起来越来越复杂。一篇博文可能会包含发布日期,评论等,我们如此做法,代码看起来也不够简洁:

<blog-component v-for="(blog,index) in blogData" :key="index" v-bind:title="blog.title" :content="content" :date="date" :recommen="commen"></blog-component>

重构一下这个组件,让它接收一个单独的prop,如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
  </head>
  <body>
    <div id="app">
      <blog-component v-for="(blog,index) in blogData" :key="index" :blog="blog"></blog-component>
    </div>
    <script>
      Vue.component("blog-component", {
        props: ["blog"],
        template: `
            <div class="blog-demo">
                <h3>{{blog.title}}</h3>
                <div v-html="blog.content"></div>
                <p>
                    <span class="date">{{ blog.date }}</span>
                    </span class="recommen">{{ blog.commen }}</span>
                </p>
            </div>
        `
      });
      var vm = new Vue({
        data: {
          blogData: [
            { title: "博客组件的标题1", content: "博客组件的内容1",date:"2019年2月20日",commen:"真棒!" },
            { title: "博客组件的标题2", content: "博客组件的内容2",date:"2019年2月20日",commen:"真的好棒!" },
            { title:"博客组件的标题3",content:"博客组件的内容3",date:"2019年2月20日",commen:"真的真的非常棒!"}
          ]
        }
      }).$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看。在开发blog组件时候,有时候,我们不仅需要建立父组件向子组件的通信,我们也许还需要建立子组件向父组件传递数据之间的通信,这时候,我们可以采用自定义事件来向父组件传递数据。例如,我们想要添加一个按钮,用于改变字体的颜色,我们可以自定义一个颜色框,然后让用户选择字体颜色来改变字体的颜色。如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
  </head>
  <body>
    <div id="app">
      <blog-component v-for="(blog,index) in blogData" :key="index" :blog="blog" @change-color="setColor($event,index)"></blog-component>
    </div>
    <script>
      Vue.component("blog-component", {
        props: ["blog"],
        template: `
            <div class="blog-demo" :style="{color:blog.color}">
                <h3>{{blog.title}}</h3>
                <div v-html="blog.content"></div>
                <p>
                    <span class="date">{{ blog.date }}</span>
                    </span class="recommen">{{ blog.commen }}</span>
                </p>
                <div class="setFontColor">
                    <input type="color" v-model="colorValue" />
                    <button type="button" @click="$emit('change-color',colorValue)">改变字体颜色</button>
                </div>
            </div>
        `,
        data:function(){
            return{
                colorValue:""
            }
        }
      });
      var vm = new Vue({
        data: {
          blogData: [
            { title: "博客组件的标题1", content: "博客组件的内容1",date:"2019年2月20日",commen:"真棒!" },
            { title: "博客组件的标题2", content: "博客组件的内容2",date:"2019年2月20日",commen:"真的好棒!" },
            { title:"博客组件的标题3",content:"博客组件的内容3",date:"2019年2月20日",commen:"真的真的非常棒!"}
          ]
        },
        methods:{
            setColor:function(color,index){
                this.$set(this.blogData[index],'color',color);
            }
        }
      }).$mount(document.getElementById("app"));
    </script>
  </body>
</html>

通过以上示例我们可以看出,父组件通过像处理native DOM事件一样通过v-on指令监听子组件的任意事件,然后子组件通过$emit()方法来自定义一个事件,这个方法接收两个参数,第一个参数就是自定义的事件名(在这里是change-color),第二个参数则是要传递的参数,在这里,传递了颜色选择器选择的颜色值colorValue,然后父组件使用这个事件,有了这个自定义的v-on:change-color监听器,父组件就可以为数组的每一项对象新增一个color属性,通过将设置style,将color属性绑定到style标签内,然后父组件就可以更新数据,并渲染结果了。你可以狠狠点击此处具体示例来查看。

通过自定义事件我们还可以实现在组件上使用v-model指令,如下:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
  </head>
  <body>
    <div id="app">
      <ew-input v-model="msg"></ew-input>
      <p>{{ msg }}</p>
    </div>
    <script>
      Vue.component("ew-input", {
        props: ["value"],
        template: `
          <input type="text" :value="value"  v-on:input="$emit('input',$event.target.value)" />
        `
      });
      var vm = new Vue({
        data: {
          msg: "在组件上使用v-model指令"
        }
      }).$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看。

和HTML元素一样,通常我们也需要向组件传递内容,这时候,我们可以使用插槽。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
    <style>
        *{
            margin: 0;
            padding: 0;
        }
        .ew-alert{
            position:absolute;
            left: 50%;
            top: 50%;
            min-width: 300px;
            min-height: 200px;
            background-color: #ffffff;
            box-shadow: 0 0 15px #939393;
            text-align: center;
            transform: translate(-50%,-50%);
            border-radius: 15px;
        }
        .ew-alert > .ew-alert-title{
            padding: 10px 0;
        }
        .ew-alert > .ew-alert-content{
            padding: 8px 9px;
        }
    </style>
  </head>
  <body>
    <div id="app">
      <ew-alert title="弹出框标题"><p>{{ msg }}</p></ew-alert>
    </div>
    <script>
      Vue.component("ew-alert", {
        props: ["title"],
        template: `
          <div class="ew-alert">
            <div class="ew-alert-title">{{ title }}</div>
            <div class="ew-alert-content"><slot></slot></div>
          </div>
        `
      });
      var vm = new Vue({
        data: {
          msg: "弹出框主题内容"
        }
      }).$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看。

有时候,我们需要动态切换组件,也就是在做一个选项卡组件的时候,我们把每一个导航对应一个导航组件,这时候就需要动态切换组件。如果要动态切换组件,我们可以使用一个特性is来实现。例如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .tab-component-demo{
          width: 900px;
          margin: 15px auto;
          padding: 15px 10px;
      }
      button {
        outline: none;
        border: 1px solid #ebebeb;
        background-color: transparent;
        display: inline-block;
        padding: 6px 7px;
        text-align: center;
        line-height: 1;
        transition: all 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
        cursor: pointer;
        font-size: 16px;
        border-radius: 4px;
      }
      button:hover,
      button:active {
        border-color: #58e9fc;
        background-color: #0abce9;
        color: #ffffff;
      }
      .ew-page{
          border: 1px solid #999;
          padding: 15px 8px;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div class="tab-component-demo">
        <button
          v-for="(tab,index) in tabs"
          :key="index"
          @click="currentTab = tab"
        >
          {{ tab }}
        </button>
        <component :is="currentTabComponent"></component>
      </div>
    </div>
    <script>
      Vue.component("tab-home", {
        template: `
          <div class="ew-page">
           home page!
          </div>
        `
      });
      Vue.component("tab-list", {
        template: `
          <div class="ew-page">
           list page!
          </div>
        `
      });
      Vue.component("tab-contact", {
        template: `
          <div class="ew-page">
          contact page!
          </div>
        `
      });
      var vm = new Vue({
        data: {
          currentTab: "home",
          tabs: ["home", "list", "contact"]
        },
        computed: {
          currentTabComponent: function() {
            return "tab-" + this.currentTab.toLowerCase();
          }
        }
      }).$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看。通过这个示例,我们可以知道is绑定的值currentTabComponent可以是一个组件名,但事实上,我们还可以绑定一个组件的选项对象,如下例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }
      .tab-component-demo {
        width: 900px;
        margin: 15px auto;
        padding: 15px 10px;
      }
      button {
        outline: none;
        border: 1px solid #ebebeb;
        background-color: transparent;
        display: inline-block;
        padding: 6px 7px;
        text-align: center;
        line-height: 1;
        transition: all 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
        cursor: pointer;
        font-size: 16px;
        border-radius: 4px;
      }
      button:hover,
      button:active {
        border-color: #58e9fc;
        background-color: #0abce9;
        color: #ffffff;
      }
      .ew-page {
        border: 1px solid #999;
        padding: 15px 8px;
      }
    </style>
  </head>
  <body>
    <div id="app">
      <div class="tab-component-demo">
        <button
          v  v-for="(tab,index) in tabs"
          :key="index"
          @click="currentTab = tab"
        >
          {{ tab.name.slice(4) }}
        </button>
        <component :is="currentTab.component"></component>
      </div>
    </div>
    <script>
      var tabs = [
        {
          name: "tab-home",
          component: {
            template: <div class="ew-page">home page! </div>`
          }
        },
        {
          name: "tab-list",
          component:{
            template: `<div class="ew-page">list page!</div>`
          }
        },
        {
          name: "tab-contact",
          component:{
            template: `<div class="ew-page">contact page!</div>`
          }
        }
      ];
      var vm = new Vue({
        data: {
          currentTab: tabs[0],
          tabs: tabs
        }
      }).$mount(document.getElementById("app"));
    </script>
  </body>
</html>

你可以狠狠点击此处具体示例查看。

在解析DOM模板时,我们还需要注意一点,那就是有些HTML元素,诸如<ul>,<ol>,<table>,<select>,对于哪些元素出现在其内部是有严格限制的,而有些元素,诸如<li>,<tr>,<option>,只能出现在特定的元素内部。这会导致我们在使用这些有约束的元素时出现问题,如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
  </head>
  <body>
    <div id="app">
        <table>
            <ew-div></ew-div>
        </table>
    </div>
    <script>
      Vue.component("ew-div", {
        template: `
           <div>测试</div>
        `
      });
      var vm = new Vue().$mount(document.getElementById("app"));
    </script>
  </body>
</html>

我们可以打开浏览器控制台,看到以上自定义的ew-div组件中的元素,被渲染到最外部去了,如下图所示:

图片描述
你可以狠狠点击此处具体示例查看这个问题。

幸运的是,你可以使用is解决这个问题。如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
  </head>
  <body>
    <div id="app">
        <table>
            <tr is="ew-div"></tr>
        </table>
    </div>
    <script>
      Vue.component("ew-div", {
        template: `
           <div>测试</div>
        `
      });
      var vm = new Vue().$mount(document.getElementById("app"));
    </script>
  </body>
</html>

此时再看浏览器控制台,你会发现自定义的组件已经被渲染在table内部,如下图所示:

图片描述

你可以狠狠点击此处具体示例查看。

当然,如果我们从如下来源使用模板的话,这条限制是不存在的。

1.字符串(如:template:'')
2.单文件组件(.vue)
3.<script type="text/x-template"></script>

到目前为止,需要了解的组件基础知识大概就只有这些呢,接下来是深入组件的了解。

四.组件深入

1.组件注册

在注册一个组件的时候,我们需要为组件起一个名字,比如全局注册的时候,我们已经知道了

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

也就是Vue.component()的第一个参数。在给组件起名的时候,如果不是字符串模板或者单文件组件,直接在DOM中使用组件,组件名是有规范的(字母全小写,并且单词之间用连字符连接)。你可以查阅API风格指南

这也就是说定义组件名有两种方式,即(kebab-case)短横线分隔命名(PascalCase)首字母大写命名。当使用短横线命名时,我们在使用组件的时候,也必须是采用短横线命名,而使用首字母大写命名则既可以采用首字母大写也可以采用短横线命名,当然在DOM模板而非字符串模板或者单文件组件中,也还是只能采用短横线命名。

到目前为止,我们只是用Vue.component来全局注册一个组件,如:

Vue.component('com-a',{})
Vue.component('com-b',{})
Vue.component('com-c',{})

然后,你就可以在new Vue()根实例内的任何组件中使用这三个组件,比如:

//js
new Vue(el:"#app")
//html
<div id="app">
    <com-a>
      <com-b></com-b>
    </com-a>
    <com-b>
      <com-c></com-c>
    </com-b>
    <com-c>
      <com-a></com-a>
    </com-c>
</div>

这往往是不够理想的,比如在一个通过webpack构建的系统应用中,全局注册所有的组件,则意味着即便你不再使用一个组件了,但组件仍然被包含在最终构建的结果中,这也就增加了一些无谓的JavaScript代码。

不过,我们可以局部注册组件。局部注册也就是将一个组件当作普通的JavaScript对象来定义。如下例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
  </head>
  <body>
    <div id="app">
        <component-a></component-a>
        <component-b></component-b>
    </div>
    <script>
      var componentA = {
          template:`<div>a</div>`
      },
      componentB = {
          template:`<div>b</div>`
      }
      var vm = new Vue(
          {
              el:"#app",
              components:{
                  'component-a':componentA,
                  'component-b':componentB
              }
          }
      )
    </script>
  </body>
</html>

通过JavaScript定义一个组件对象,然后在vue实例中添加components选项,components选项的属性名就是组件名,属性值就是这个组件的选项对象。你可以狠狠点击此处具体示例查看效果。

但是局部注册组件是无法在其子组件中使用另一局部注册的组件的,如果想要这样的效果,那么代码需要写成下面那样:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>component</title>
  </head>
  <body>
    <div id="app">
      <component-a></component-a>
      <component-b></component-b>
    </div>
    <script>
      var componentA = {
          template: `<div>a</div>`
        },
        componentB = {
          template: `<div>b<component-a></component-a></div>`,
          components:{
              'component-a':componentA
          }
        };
      var vm = new Vue({
        el: "#app",
        components: {
          "component-a": componentA,
          "component-b": componentB
        }
      });
    </script>
  </body>
</html>

也就是往局部注册组件对象中添加一个components选项,注册需要添加的局部注册的组件而已。你可以狠狠点击此处具体示例查看效果。

如果是使用es6与babel以及webpack,那么代码,则更像是如下:

import componentA from './componentA.vue'

export default{
  components:{ componentA }
}

在对象中放一个componentA其实也就是componentA:componentA的缩写,也就是说这个变量名同时是:

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

如果你还不熟悉模块化构建,那最好还是跳过这里。

2.prop

由于HTML特性是不区分大小写的,也就是说浏览器会将所有的大写字母转换成小写字母,这也就意味着当你使用camelCase(驼峰命名法)来为prop定义的时候,你在子组件使用prop的时候需要使用等价的kebab-case(短横线命名)来命名,比如:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>prop</title>
    <style>
        .ew-button {
            outline: none;
            border: 1px solid transparent;
            display: inline-block;
            border-radius: 4px;
            font-size: 14px;
            text-align: center;
            transition: color 0.2s linear, backgroud-color 0.1s linear,
                border-color 0.1s linear, box-shadow 0.2s linear;
            cursor: pointer;
            border-color: #f3f3f3;
            color: #a9adb1;
            background-color: #ffffff;
            padding: 5px 15px 6px;
        }

        .ew-button-success {
            background-color: #2196ff;
            border-color: #4ca4f1;
            color: #ffffff;
        }

        .ew-button-success:hover,
        .ew-button-success:active {
            box-shadow: 0 0 3px #943321;
            opacity: 0.8;
        }
    </style>
</head>

<body>
    <div id="app">
        <ew-button ew-type="success">{{ content }}</ew-button>
    </div>
    <script>
        var ewButton = {
            props: ["ewType"],
            template: `<button type="button" :class="'ew-button-'+ ewType" class="ew-button"><slot></slot></button>`
        };
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                    content: "success"
                };
            },
            components: {
                ewButton: ewButton
            }
        });
    </script>
</body>

</html>

在这里,我们是使用的局部注册一个组件,也就是用一个对象表示一个组件,然后在Vue实例中添加components选项来注册这个组件,组件名虽然使用的camelCase(驼峰命名法),也就是ewButton,但我们也看到了,在使用这个组件的时候,需要使用kebab-case(短横线命名),即ew-button来表示,同样的,定义的prop名也是如此,在子组件中定义成ewType,但在使用的时候则是ew-prop,当然在单文件组件以及字符串模板中是不存在这个限制的,这里是使用DOM模板来表示的。

你可以狠狠点击此处具体示例查看效果。

到目前为止,我们只看到了prop的形式只是一个字符串数组:

props:['str','num','bool','arr','obj']

但是,通常我们想要每个prop都有指定的值和类型,这时候,我们可以以对象的形式来列出prop,这些属性的名称和值分别代表prop名类型。如下:

props:{
  str:String,
  num:Number,
  bool:Boolean,
  arr:Array,
  obj:Object
}

如果指定的prop类型不对,那么浏览器就会在控制台报一个invalid prop的错误,这是vue内部实现的。

在此之前,我们可以看到prop有两种用法,第一种是静态的prop,如上例的:

<ew-button ew-type="success">{{ content }}</ew-button>

这里的ew-type就是一个静态的prop,但实际上,我们还可以用v-bind来表示一个动态的prop,以上的静态prop,我们就可以表示成如下:

<ew-button v-bind:ew-type="'success'">{{ content }}</ew-button>
//或者<ew-button :ew-type="'success'">{{ content }}</ew-button>

有了v-bind,我们就可以为prop绑定任意类型的值。例如:

以下是传递一个数值:

//即便100是静态的,我们也需要使用v-bind来告诉vue,这是一个JavaScript表达式,而不是字符串
<ew-button v-bind:ew-type="100">{{ content }}</ew-button>
//绑定一个变量名
<ew-button v-bind:ew-type="number">{{ content }}</ew-button>

在js代码中,我们就可以如此写:

new Vue({
    ...
    data(){
      return{
          number:100
      }
    }
    ...
})

以下是传递一个布尔值:

//如果不传入值,默认就是一个布尔值true
<ew-button ew-type>{{ content }}</ew-button>
//即便false是静态的,我们也需要使用v-bind来告诉vue,这是一个JavaScript表达式,而不是字符串
<ew-button :ew-type="false">{{ content }}</ew-button>
/绑定一个变量名
<ew-button :ew-type="bool">{{ content }}</ew-button>

在js代码中,我们就可以如此写:

new Vue({
    ...
    data(){
      return{
          bool:false
      }
    }
    ...
})

以下是传递一个数组:

//即便数组是静态的,我们也需要使用v-bind来告诉vue,这是一个JavaScript表达式,而不是字符串
<ew-button :ew-type="[1,2,3]">{{ content }}</ew-button>
/绑定一个变量名
<ew-button :ew-type="arr">{{ content }}</ew-button>

在js代码中,我们就可以如此写:

new Vue({
    ...
    data(){
      return{
          arr:[1,2,3]
      }
    }
    ...
})

以下是传递一个对象:

//即便对象是静态的,我们也需要使用v-bind来告诉vue,这是一个JavaScript表达式,而不是字符串
<ew-button :ew-type="{ name:'eveningwater',sex:'male'}">{{ content }}</ew-button>
/绑定一个变量名
<ew-button :ew-type="obj">{{ content }}</ew-button>

在js代码中,我们就可以如此写:

new Vue({
    ...
    data(){
      return{
          obj:{
            name:"eveningwater",
            sex:"male"
          }
      }
    }
    ...
})

如果你想将一个对象的所有属性都作为prop传入,那么你可以使用不带参数的v-bind来取代v-bind:propName。例如:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>prop</title>
</head>

<body>
    <div id="app">
        <ew-div v-bind="item"></ew-div>
        //等价于<ew-div v-bind:name="item.name" v-bind:sex="item.sex"></ew-div>
    </div>
    <script>
        Vue.component('ew-div',{
            props:['name','sex'],
            template:`<div class="myDiv"><h1>{{ name }}</h1><p v-text="sex"></p></div>`,

        })
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                   item:{
                       name:"eveningwater",
                       sex:"male"
                   }
                };
            }
        });
    </script>
</body>

</html>

以上定义的v-bind="item"实际上就等价于v-bind:name="item.name"v-bind:sex="item.sex",所以,我们才可以在子组件的props里获取到namesex,并且将props的值绑定到元素中去。你可以狠狠点击此处具体示例查看效果。

所有的prop都使得父子之间的prop形成了一个单向下行绑定:父组件的prop如果发生了更新,则会向下流动到子组件中,但是反过来修改子组件的prop就不行。这样也就防止了子组件意外改变父组件的状态,从而导致应用的数据流难以理解。

这也就是说一旦父组件更新了,那么子组件也就会立刻刷新为最新的值,这也意味着你不应该修改一个子组件的prop,如果你如此做了,浏览器会发出一个警告,这是vue内部实现的。来看一个示例:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>prop</title>
</head>

<body>
    <div id="app">
        <ew-div :content="content"></ew-div>
    </div>
    <script>
        Vue.component('ew-div',{
            props:['content'],
            template:`<div class="myDiv"><p>{{ content }}</p><button type="button" @click="updateContent">改变content的值</button></div>`,
            methods:{
                updateContent(){
                    this.content = '修改了值,浏览器控制台会给出一个警告!'
                }
            }
        })
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                   content:"文档的内容"
                };
            }
        });
    </script>
</body>

</html>

如上例,当我们点击了按钮,去修改prop的值,尽管最终值发生了改变,但是浏览器控制台还是给出了一个警告信息,如下图所示:

clipboard.png

你可以狠狠点击此处具体示例查看效果。实际上,这个警告信息也就是提示,不能直接在子组件中修改prop,但是我们有两种办法解决这个问题。

1.我们将父组件传给子组件的prop作为一个初始值,然后在子组件的data选项中定义一个新的数据,将prop的值用作其初始值,然后在绑定数据的时候就不用绑定prop,而是绑定定义在data选项中的值。如下所示:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>prop</title>
</head>
<body>
    <div id="app">
        <ew-div :content="content"></ew-div>
    </div>
    <script>
        Vue.component('ew-div',{
            props:['content'],
            template:`<div class="myDiv"><p>{{ curContent }}</p><button type="button" @click="updateContent">改变content的值</button></div>`,
            data(){
                return{
                    curContent:this.content
                }
            },
            methods:{
                updateContent(){
                    this.curContent = '修改了值,浏览器控制台不会给出一个警告!'
                }
            }
        })
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                   content:"文档的内容"
                };
            }
        });
    </script>
</body>
</html>

现在,我们再点击按钮,值发生了改变,而且浏览器也没有给出警告。你可以狠狠点击此处具体示例查看效果。

2.如果是这个prop作为初始值,并且要进行转换,那么,最好还是使用一个计算属性来表示。例如:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>prop</title>
</head>
<body>
    <div id="app">
        <ew-div :content="content"></ew-div>
    </div>
    <script>
        Vue.component('ew-div',{
            props:['content'],
            template:`<div class="myDiv"><p>{{ curContent }}</p></div>`,
            computed:{
                curContent(){
                    return '计算属性:' + this.content;
                }
            }
        })
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                   content:"文档的内容"
                };
            }
        });
    </script>
</body>
</html>

你可以狠狠点击此处具体示例查看效果。又或者,综合以上两个方法,修改如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>prop</title>
</head>
<body>
    <div id="app">
        <ew-div :content="content"></ew-div>
    </div>
    <script>
        Vue.component('ew-div',{
            props:['content'],
            template:`<div class="myDiv"><p>{{ curContent }}</p><button type="button" @click="updateContent">改变content的值</button></div>`,
            data(){
                return{
                    newContent:""
                }
            },
            computed:{
                curContent:{
                    get(){
                        if(this.newContent){
                            return '计算属性:' + this.newContent;
                        }else{
                            return '计算属性:' + this.content;
                        }
                    },
                    set(newVal){
                        this.newContent = newVal;
                    }
                }           
            },
            methods:{
                updateContent(){
                    this.curContent = '修改了值,浏览器控制台不会给出一个警告!'
                }
            }
        })
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                   content:"文档的内容"
                };
            }
        });
    </script>
</body>
</html>

你可以狠狠点击此处具体示例查看效果。

注意在JavaScript数组和对象中,由于数组和对象是通过引用传入的,因此修改子组件的数据可能会影响到父组件的状态。

在使用prop的时候,我们还可以为prop指定验证要求,如果不满足验证要求,则vue会在浏览器控制台中警告你。这在对于开发一个被别人用到的组件时尤为重要。

为了指定prop的验证需求,你应该为props的值指定一个带有验证需求的对象,而不是字符串数组。如下一个示例:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>validate-prop</title>
    <style>
        .font-red {
            color: #ff1944;
        }
    </style>
</head>

<body>
    <div id="app">
        <ew-div :num-success="count1" :num-fail="count2" :str-success="str1" :str-fail="str2" :str-require="str3"
            :num-or-str-success="numorstr1" :num-or-str-fail="numorstr2" define-by-self="字符串"></ew-div>
    </div>
    <script>
        Vue.component('ew-div', {
            props: {
                numSuccess: Number,//验证数值成功
                numFail: Number,//验证数值失败
                strSuccess: String,//验证字符串成功
                strFail: String,//验证字符串失败
                strRequire: {
                    type: String,
                    required: true //验证字符串必填
                },
                numOrStrSuccess: [Number, String],//验证字符串或者数值成功
                numOrStrFail: [Number, String],//验证字符串或者数值失败
                defaultNum: {
                    type: Number,
                    default: 50
                },//带有默认值的验证
                defaultObj: {
                    type: Object,
                    default: function () {
                        return { name: "eveningwater", sex: "male" }
                    } //默认值为对象,对象或数组的默认值必须通过一个函数返回
                },
                defineBySelf: {
                    validator: function (value) {
                        // 值必须是以下数组中的几个
                        return [123, '字符串', [1, 2, 3], { name: "eveningwater" }].indexOf(value) > -1;
                    },
                    default: 123
                }
            },
            template: `
                <div class="myDiv">
                    <p>数值类型验证成功:{{ numSuccess }}</p>
                    <p>需要注意的就是null和undefined会通过任何类型的验证,除了设置required为true。</p>
                    <p>
                        数值类型验证失败:{{ numFail }}
                        浏览器控制台会报如此的警告:<span class="font-red">[Vue warn]: Invalid prop: type check failed for prop "numFail". 
                        Expected Number with value 123, got String with value "123".</span>。大致意思就是:
                        不可用的prop,prop'numFail'的类型验证失败,使用数值型的123替代字符串的"123"。
                    </p>
                    <p>字符串类型验证成功:{{ strSuccess }}</p>
                    <p>
                        字符串类型验证失败:{{ strFail }}
                        浏览器控制台会报如此的警告:<span class="font-red">Invalid prop: type check failed for prop "strFail". 
                            Expected String with value "124", got Number with value 124.</span>。大致意思就是:
                        不可用的prop,prop'strFail'的类型验证失败,使用字符串的"124"替代数值的124。
                    </p>
                    <p>
                        必填字符串(包含空字符串):{{ strRequire }}
                        如果不满足条件,比如是null,则一样会抛出类似警告:
                        <span class="font-red">
                        [Vue warn]: Invalid prop: type check failed for prop "strRequire".
                         Expected String with value "null", got Null.     
                        </span>
                    </p>
                    <p>字符串或数值类型验证成功:{{ numOrStrSuccess }}</p>
                    <p>
                        字符串或数值类型验证失败:{{ numOrStrFail }}
                        浏览器控制台会报如此的警告:<span class="font-red">Invalid prop: type check failed for prop "numOrStrFail". 
                            Expected Number, String, got Object </span>。大致意思就是:
                        不可用的prop,prop'numOrStrFail'的类型验证失败,使用字符串,数值替代对象。
                    </p>
                    <p>带有默认值的验证:{{ defaultNum }}</p>
                    <p>
                        对象或数组的默认值必须以一个函数返回:
                        <span>{{ defaultObj.name }}</span><br>
                        <span>{{ defaultObj.sex }}</span>
                    </p>
                    <p>自定义验证函数:{{ defineBySelf }}如果验证失败,则控制台会提示:
                        <span class="font-red">[Vue warn]: Invalid prop: custom validator check failed for prop "defineBySelf".</span>
                    </p>
                </div>
            `
        })
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                    count1: 100,
                    count2: '123',
                    str1: "成功!",
                    str2: 124,
                    str3: '123',
                    numorstr1: '字符串',
                    numorstr2: {}
                };
            }
        });
    </script>
</body>

</html>

根据以上示例,我们可以得知一个prop可以验证js当中的基本数据类型,也可以验证对象或数组,可以指定必要值以及默认值,还可以自定义验证函数。你可以狠狠点击此处具体示例查看效果。

这里需要注意一点,就是prop会在一个组件实例创建之前进行验证,因此实例中的data,computed等属性在defaultvalidator函数中是不可用的。

前面提到prop可以通过一个type属性来进行验证,在这里,type的值可以是如下几个原生构造函数:

Number,String,Boolean,Object,Array,Function,Date,Symbol

额外的,type其实也可以是一个自定义的构造函数,并且通过instanceof来检查确认。比如以下一个示例:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>prop type</title>
</head>

<body>
    <div id="app">
        <name-component :name="fullName"></name-component>
    </div>
    <script>
        function FullName(firstName,lastName){
            this.firstName = firstName;
            this.lastName = lastName;
            this.fullName = firstName + ' ' + lastName;
        }
        var f = new FullName('夕','水');
        var nameComponent = {
            props:{
                name:{
                    type:FullName
                }
            },
            template:`
                <div class="name">
                    <p>
                        <span>姓:</span>
                        <span>{{ name.firstName }}</span>
                    </p>
                    <p>
                        <span>名:</span>
                        <span>{{ name.lastName }}</span>
                    </p>
                    <p>
                        <span>姓名:</span>
                        <span>{{ name.fullName }}</span>
                    </p>
                </div>
            `
        }
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                    fullName:f
                }
            },
            components:{
                nameComponent:nameComponent
            }
        })
    </script>
</body>

</html>

在这里,只要name是自定义构造函数FullName的实例,那么数据就验证成功,如果不是,那么vue就会在浏览器控制台给出警告。你可以狠狠点击此处具体示例查看效果。

在前面,我们都是显式的定义prop,然而,当我们不显式的定义prop的时候,实际上,它也作为了一个特性,传给了组件,只不过它是一个非prop特性,因为该组件并没有显式的定义该prop特性,显式的定义prop固然适用于传向一个组件,但是组件库的作者并不能总是预见到prop特性会应用于什么样的场景。所以如果不显式的定义prop特性,那么该特性就会被添加到组件的根元素上。如下例:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>not prop</title>
</head>

<body>
    <div id="app">
        <name-component :name="nameValue"></name-component>
    </div>
    <script>
        var nameComponent = {
            template:`
                <div class="name">
                   <p>组件并没有显式的定义prop,所以特性被添加到了根元素上。</p>
                </div>
            `
        }
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                    nameValue:"eveningwater"
                }
            },
            components:{
                nameComponent:nameComponent
            }
        })
    </script>
</body>

</html>

在浏览器控制台,我们可以看到,name特性被添加到class为name的根元素上,如下图所示:

clipboard.png

你可以狠狠点击此处具体示例查看效果。

在这里,对于绝大多数的特性来说,外部提供的特性都会替换组件内部的特性,例如想象一下,如果组件内部是这样一个模板:

<input type="text" v-model="newValue" class="ew-input"/>

如果我们在组件外部提供一个type="number"的特性,一旦替换掉组件内部的特性,那么就改变了原有的DOM元素,这并不是我们所想要的。来看如下一个例子:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>replace prop</title>
</head>

<body>
    <div id="app">
        <ew-input :type="type"></ew-input>
    </div>
    <script>
        var ewInput = {
            template:`
                <input type="text" v-model="value" class="ew-input">
            `,
            data(){
                return{
                    value:"eveningwater"
                }
            }
        }
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                    type:"number"
                }
            },
            components:{
                ewInput:ewInput
            }
        })
    </script>
</body>

</html>

显然,type="number"已经替换掉了原有的type="text"属性,这并不是我们所想要的。
你可以狠狠点击此处具体示例查看效果。

庆幸的是,classstyle则会替换或合并原有的特性,只要原有特性与提供的特性不同,那么就会合并原有的特性。如下例所示:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>replace prop</title>
</head>

<body>
    <div id="app">
        <ew-input :class="className" :style="stylePadding"></ew-input>
    </div>
    <script>
        var ewInput = {
            template:`
                <input type="text" v-model="value" class="ew-input" style="border:2px solid #2396fe;">
            `,
            data(){
                return{
                    value:"eveningwater"
                }
            }
        }
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                    className:"ew-success-input",
                    stylePadding:"padding:6px 7px;"
                }
            },
            components:{
                ewInput:ewInput
            }
        })
    </script>
</body>

</html>

现在,你在控制台中查看该组件的根元素,你会发现classstyle都已经被合并了。你可以狠狠点击此处具体示例查看效果。

虽然是如此,但我们有时候并不想要这样的效果,也就是说我们想要组件的根元素不能继承特性,那么我们可以在组件的选项中设置inheritAttrs:false。格式如下:

Vue.component('my-component',{
    inheritAttrs:false,
    ......
})

当然,这个属性的设置并不会影响styleclass的绑定。我们可以将这个属性与实例的$attrs属性一起结合使用,在构造基础组件的时候非常常用,$attrs属性也是一个对象,包含传递给一个组件的特性名和特性值。例如:

{
  placeholder:"请输入你的用户名",
  id:"#myInput"
}

这个模式更允许你像写原始的HTML元素一样写一个组件,而不需要担心组件的根元素到底是哪个元素。如下例:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>inherit prop</title>
</head>

<body>
    <div id="app">
        <ew-input v-model="name" label="姓名:" placeholder="请输入你的姓名" name="eveningwater"></ew-input>
    </div>
    <script>
        var ewInput = {
            inheritAttrs:false,
            props:['label','value'],
            template:`
                <label>
                    {{ label }}
                    <input type="text" v-bind="$attrs" :value="value" class="ew-input">
                </label>
            `
        }
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                   name:""
                }
            },
            components:{
                ewInput:ewInput
            }
        })
    </script>
</body>

</html>

这样,我们就已经完成了一个基本的表单输入框组件,再加点样式扩展,我们可以完美的构造一个媲美常用的UI框架的表单输入框组件。你可以狠狠的点击此处具体示例查看效果。

3.自定义事件

事件名不同于prop和组件名的注册,是不存在大小写的转换的。也就是说事件名注册的是什么,使用的时候就应该是什么。比如如果用camelCase来定义事件名的话,那么用kebab-case的事件名是无效的。如下例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>emit</title>
  </head>
  <body>
    <div id="app">
        <p>{{ count }}</p>
        <emit-button @add-count="count+=1"></emit-button>
    </div>
    <script>
        Vue.component('emit-button',{
            template:`<button class="btn" @click="$emit('addCount')">click me</button>`
        })
      var vm = new Vue({
        el: "#app",
        data:{
            count:0
        }
      });
    </script>
  </body>
</html>

在这个示例中,无论我们怎么点击按钮,都不会触发自定义事件,因为我们事件定义的名称是camelCase,而在使用的时候则是kebab-case,这也说明了事件名是不存在大小写的转换的。你可以狠狠点击此处具体示例查看效果。

所以,推荐用kebab-case来定义事件名。

v-model指令默认是利用一个props和一个名为input的事件来完成的,但在像单选框,复选框,下拉框等类型的控件中可能会将value特性用于其它目的。所以vue提供了model选项来避免这样的冲突,来看一个自定义v-model的示例:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>自定义v-model</title>
</head>

<body>
    <div id="app">
        <div class="form">
            <base-check v-model="gameChecked">游戏</base-check>
            <div class="form-item">
                <p>是否选中:<span>{{ gameChecked }}</span></p>
            </div>
        </div>
    </div>
    <script>
        const baseCheck = {
            model: {
                prop: 'checked',
                event: "change"
            },
            props: {
                checked: Boolean
            },
            template: `
                <div class="checkbox">
                    <input type="checkbox" :value="checked" @change="onChecked">
                    <slot></slot>
                </div>
            `,
            methods:{
                onChecked(e){
                    this.$emit('change',e.target.checked)
                }
            }
        }
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                    gameChecked:false
                }
            },
            components: {
                baseCheck: baseCheck
            }
        });
    </script>
</body>

</html>

这里的gameChecked的值会被传入名为checkedprop,同时当base-checkbox被更新的时候,这个作为属性的值也会发生相应的改变。你可以狠狠点击此处具体示例查看效果。

在这里你需要注意的就是,需要显式的声明checked这个prop

在某些时候,我们是想要直接在组件上监听一个元素的原生事件的,这个时候,我们可以直接使用.native事件修饰符。比如:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>native</title>
</head>

<body>
    <div id="app">
        <base-input @on-focus="count+=1"></base-input>
        <p>{{ count }}</p>
    </div>
    <script>
        const baseInput = {
            template: `
                <input type="text" @focus.natve="$emit('on-focus',$event.target.value)">
            `
        }
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                    count:0
                }
            },
            methods:{

            },
            components: {
                baseInput: baseInput
            }
        });
    </script>
</body>

</html>

你可以点击此处具体示例查看效果。

在有的时候,这样确实不错,但事实上,在有些情况下,这样做却并不是一个好主意。比如在编写一个基本的base-input组件时,这个组件的根元素不一定是input元素,这个时候监听input的元素的原生事件时,则会无效,尽管浏览器控制台并不会报错。如:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>base-input</title>
</head>

<body>
    <div id="app">
        <base-input @focus.native="msg='hello'" label="姓名"></base-input>
        <p>{{ msg }}</p>
    </div>
    <script>
        const baseInput = {
            props:['label','value'],
            template: `
               <div class="base-input-container">
                    <div class="label">{{ label }}</div>
                    <input type="text" v-bind="$attrs" :value="value" @input="$emit('on-input',$event.target.value)">
               </div>
            `
        }
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                    msg:"base input component"
                }
            },
            methods:{

            },
            components: {
                baseInput: baseInput
            }
        });
    </script>
</body>

</html>

显然,这个事件的监听并没有产生效果,你可以狠狠点击此处具体示例查看效果。

为了解决这个问题,vue提供了一个$listeners属性,它是一个对象,里面就包含了这个组件的所有监听器。例如:

{
    focus:function(event){/*业务逻辑代码*/},
    input:function(event){/*业务逻辑代码*/}
    ...
}

有了这个属性,就可以配合v-on:$listeners将所有的事件监听器指向组件特定的子元素。对于类似input的你希望它也可以配合v-model工作的组件来说,使用计算属性来创建这些监听器是非常有用的。如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>base-input</title>
</head>

<body>
    <div id="app">
        <base-input @focus="msg='hello,vue.js!'" label="姓名" v-model="msg"></base-input>
        <p>{{ msg }}</p>
    </div>
    <script>
        const baseInput = {
            inheritAttrs:false,
            props:['label','value'],
            computed:{
                input$Listeners:function(){
                    let vm = this;
                    return Object.assign({},this.$listeners,{
                        focus(e){
                            vm.$emit('focus',e.target.value)
                        },
                        input(e){
                            vm.$emit('input',e.target.value)
                        }
                    })
                }
            },
            template: `
               <div class="base-input-container">
                    <div class="label">{{ label }}</div>
                    <input type="text" v-bind="$attrs" :value="value" v-on="input$Listeners">
               </div>
            `
        }
        var vm = new Vue({
            el: "#app",
            data() {
                return {
                    msg:"base input component"
                }
            },
            methods:{
                
            },
            components: {
                baseInput: baseInput
            }
        });
    </script>
</body>

</html>

现在,base-input已经完全和一个普通的input元素一摸一样了,你可以狠狠点击此处具体示例查看效果。

在前面,我们知道props是单向数据流,只能从父组件中修改,而不能从子组件中修改,这会让vue在浏览器中给出一个警告,可是在实际场景中,我们难免不会遇到想让props成为双向绑定的存在,这会比用vuex更为方便一些。当然,很不幸的是,这种愿望是不便实现的,因为双向绑定会带来维护上的问题。虽然子组件可以修改父组件,但父组件与子组件之间并没有明显的改动来源。

当然,这也并不是说我们就不能修改子组件从而触发父组件的改变,vue2.3.0推荐v-on:update:propName的模式来触发事件并修改prop的值。如下一个示例:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>sync修饰符</title>
  </head>
  <body>
    <div id="app">
        <text-document :title="title" v-on:update:title="title = $event"></text-document>
    </div>
    <script>
        Vue.component('text-document',{
            props:['title'],
            template:`<div class="text">
                <p>{{ title }}</p>
                <button type="button" @click="$emit('update:title',$event.target.textContent)">change props</button>
            </div>`
        })
      var vm = new Vue({
        el: "#app",
        data:{
            title:'标题'
        }
      });
    </script>
  </body>
</html>

在这里,定义了一个名为titleprop,然后绑定父组件实例data选项中的title数据,传入子组件,子组件定义了一个按钮,通过点击触发update:title的事件,将按钮的文本传递给父组件,然后父组件v-on:update:title监听事件,并修改data选项中的值,这样也算是真正的让prop实现了双向绑定吧。你可以狠狠点击此处具体示例查看该示例。

v-on:update:title这样看着总有些麻烦,为了方便起见,vue将这种模式缩写为v-bind:title.sync。需要注意的就是这种模式是不能和JavaScript表达式一起使用的,例如:v-bind:title.sync=myTitle + '!'这种写法就是无效的。也就是说,我们只能为其绑定一个属性名,类似v-model指令的功能。

当我们用一个对象设置多个prop的时候,我们还可以将这种模式简写为:v-bind.sync=object,这样会把object对象的每一个属性作为一个独立的prop传进去,然后通过v-on添加各自的事件监听器。例如:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>sync修饰符</title>
  </head>
  <body>
    <div id="app">
        <text-document  v-bind.sync="articleObj"></text-document>
    </div>
    <script>
        Vue.component('text-document',{
            props:['title','content'],
            template:`<div class="text">
                <p>{{ title }}</p>
                <article>{{ content }}</article>
                <button type="button" @click="$emit('update:title',$event.target.textContent)">出师表</button>
                <button type="button" @click="$emit('update:content',$event.target.textContent)">先帝创业未半而中道崩殂。。。</button>
            </div>`
        })
      var vm = new Vue({
        el: "#app",
        data:{
            articleObj:{
                title:"标题",
                content:"内容"
            }
        }
      });
    </script>
  </body>
</html>

在这里,我们绑定了一个articleObj对象,有文章标题title,文章内容content属性,然后在组件,分别接收它们,通过增加update事件模式,我们就可以修改数据了。你可以狠狠点击此处具体示例查看效果。

不过我们也要注意一点,就是v-bind.sync不能绑定一个字面量对象,例如v-bind.sync="{ title:"标题"}"是无法正常工作的,这是vue基于很多边缘情况考虑而决定的。

4.插槽

vue自定义组件当中,如果嵌套了内容,是会被抛弃的。也就是说,组件标签之间的内容是无法被渲染的。可有时候,我们又需要在组件当中渲染嵌套的内容,就像标签里嵌套标签一样。vue提供了<slot>元素,来作为内容分发的一个API。在组件当中,<slot>元素更像是一个分发内容的出口。如下例:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot</title>
</head>

<body>
    <div id="app">
        <base-nav v-bind="nav">
            navgation url
        </base-nav>
    </div>
    <script>
        Vue.component('base-nav', {
            props: ['url', 'target'],
            template: `
                <a :href="url" :target="target">
                    <slot></slot>
                </a>
            `
        })
        var vm = new Vue({
            el: "#app",
            data: {
                nav: {
                    url: "https://www.baidu.com/",
                    target: "_blank"
                }
            }
        });
    </script>
</body>

</html>

以上定义了一个base-nav组件,我们可以看到在组件标签之间,我们传入了navgation url作为分发的内容。不止是文本,我们还可以在里面嵌套HTML标签,甚至是另一组件,这都是可以的。你可以狠狠点击此处具体示例查看效果。

我们来看嵌入标签和组件的示例:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot</title>
</head>

<body>
    <div id="app">
        <base-nav v-bind="nav">
            navgation url
        </base-nav>
        <base-nav v-bind="nav">
            <p>这是一个p标签</p>
            简单的文本内容
            <base-li></base-li>
        </base-nav>
    </div>
    <script>
        Vue.component('base-nav', {
            props: ['url', 'target'],
            template: `
                <a :href="url" :target="target">
                    <slot></slot>
                </a>
            `
        })
        const baseLi = {
            template:`<li>这是一个li标签</li>`
        }
        var vm = new Vue({
            el: "#app",
            data: {
                nav: {
                    url: "https://www.baidu.com/",
                    target: "_blank"
                }
            },
            components:{
                baseLi:baseLi
            }
        });
    </script>
</body>

</html>

我们可以看到如下图所示:

clipboard.png

你可以狠狠点击此处具体示例查看效果。

当我们在插槽中使用数据时,我们可以使用相同作用域下的实例中的数据,但我们不能使用不同作用域的数据。我们只需要记住一条规则:父模板的所有内容都是在父级作用域中编译的,子模版的所有内容都是在子作用域编译的。来看一个示例:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot</title>
</head>

<body>
    <div id="app">
        <base-nav v-bind="nav" title="导航">
            navgation url:{{ nav.url}}{{ title }}
        </base-nav>
    </div>
    <script>
        Vue.component('base-nav', {
            props: ['url', 'target','title'],
            template: `
                <a :href="url" :target="target">
                    <slot></slot>
                </a>
            `
        })
        var vm = new Vue({
            el: "#app",
            data: {
                nav: {
                    url: "https://www.baidu.com/",
                    target: "_blank"
                }
            }
        });
    </script>
</body>

</html>

如上例所示,我们可以看到在组件的插槽中,我们也可以访问vm实例中的数据对象nav对象,可是不同作用域的title,也就是组件上的数据,是无法被访问到了,所以浏览器控制台会报一个错误,如下图所示:

clipboard.png

现在,你可以狠狠点击此处具体示例查看效果。

有时候,我们需要为插槽提供默认的内容,vue也叫做后备内容。比如在封装一个提交按钮组件时,我们可以提供submit或提交作为后备内容。如下图所示:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot default content</title>
    <style>
        .ew-btn{
            display: inline-block;
            transition: all .2s linear;
            outline: none;
            border: 1px solid transparent;
            text-align: center;
            padding: 6px 7px;
            line-height: 1;
            cursor: pointer;
            border-radius: 4px;
        }
        .ew-primary-btn{
            background-color: #2396ef;
            color: #ffffff;
        }
        .ew-primary-btn:hover,
        .ew-primary-btn:active{
            background-color: #23f8ff;
            color: #535353;
        }
    
    </style>
</head>

<body>
    <div id="app">
        <base-button type="primary"></base-button>
        <base-button type="primary">提交</base-button>
    </div>
    <script>
        Vue.component('base-button', {
            props:['type'],
            template: `
                <button type="button" :class="'ew-'+ type +'-btn'" class="ew-btn">
                    <slot>submit</slot>
                </button>
            `
        })
        var vm = new Vue({
            el: "#app",
            data: {
                
            }
        });
    </script>
</body>

</html>

如上例所示,当我们在组件之间并没有提供内容的时候,就会将submit作为按钮文本渲染出来,而当我们提供了内容之后,就会渲染我们所提供的内容。你可以狠狠点击此处具体示例查看效果。

有时候,我们需要用到多个插槽,比如在一个页面布局当中,我们可能会有头部,内容,底部组件,网页DOM结构大概如下所示:

<div class="page">
    <header class="base-header">
        <!-- 头部 -->
    </header>
    <main class="base-main">
      <!-- 主体内容 -->
    </main>
    <footer class="base-footer">
      <!-- 底部 -->
    </footer>
</div>

这时候,头部,主体和底部都会用到插槽,我们可以为每个插槽指定一个属性:name。如果没有显示的指定这个属性的值,那么这个值会默认的指定为default。然后,我们在父组件中使用该组件并且提供插槽内容的时候,我们可以通过v-slot的参数形式结合template元素来指定添加到哪个插槽中。比如:

定义的组件:

<div class="page">
    <header class="base-header">
        <!-- 头部 -->
        <slot name="header"></slot>
    </header>
    <main class="base-main">
      <!-- 主体内容 -->
       <slot name="main"></slot>
    </main>
    <footer class="base-footer">
      <!-- 底部 -->
       <slot name="footer"></slot>
    </footer>
</div>

假定这个布局组件的组件名为base-layout,现在我们在使用这个组件:

<base-layout>
  <template v-slot:header>
      <h1>这是头部内容</h1>
  </template>
  <template v-slot:main>
    <p>这是主题内容</p>
  </template>
  <template v-slot:footer>
    <span>这是底部的内容</span>
  </template>
</base-layout>

当然主题内容,我们也可以不必指定v-slot,这样就是默认的default。这样我们就完成了一个基本的布局页面:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot</title>
</head>

<body>
    <div id="app">
        <base-layout>
            <template v-slot:header>
                <h1>这是头部内容</h1>
            </template>
            <template v-slot:main>
                <p>这是主题内容</p>
            </template>
            <template v-slot:footer>
                <span>这是底部的内容</span>
            </template>
        </base-layout>
    </div>
    <script>
        Vue.component('base-layout', {
            template: `
                <div class="page">
                    <header class="base-header">
                        <!-- 头部 -->
                        <slot name="header"></slot>
                    </header>
                    <main class="base-main">
                      <!-- 主体内容 -->
                       <slot name="main"></slot>
                    </main>
                    <footer class="base-footer">
                      <!-- 底部 -->
                       <slot name="footer"></slot>
                    </footer>
                </div>
            `
        })
        var vm = new Vue({
            el: "#app",
            data: {

            }
        });
    </script>
</body>

</html>

你可以狠狠点击具体示例查看效果。注意这里的v-slot只能添加到一个template元素上,只有一种例外,后续会赘述。

在有的时候,我们需要将插槽内容访问子组件的数据。比如有一个base-button组件如下:

<base-button>
  <span>{{ btn.text }}</span>
</base-button>

在子组件中,我们可能会有如下结构:

<button type="button">
  <slot>{{ btn.text }}</slot>
</button>

子组件的数据应该如下:

data(){
  return{
    btn:{
      text:"submit"
    }
  }
}

我们在子组件中是可以访问到btn.text数据的,但是在父组件中,我们是访问不到的,它们是不同的作用域,这时候我们可以使用v-bind特性将数据绑定到slot元素上,这种特性也被叫做插槽prop。如下:

<button type="button">
  <slot :btn="btn"></slot>
</button>

然后在父级组件中,我们可以使用v-slot带一个值来定义插槽prop的名字,结构如:v-slot="slotProps"v-slot:default="slotProps"。如下:

<base-button v-slot="slotprops">
  <span>{{ btn.text }}</span>
</base-button>

当然我们也可以在父级组件中使用template元素,将v-slot绑定到template元素上,例如:

<base-button>
  <template v-slot="slotProps">
    <span>{{ btn.text }}</span>
  </template>
</base-button>

然后父级中绑定的数据,我们应该修改成slotProps.btn.text。这样,我们就完成了插槽之间的数据传递。完整代码如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot</title>
</head>

<body>
    <div id="app">
        <base-button v-slot="slotProps">
            <template >
                <span>{{ slotProps.text }}</span>
            </template>
        </base-button>
    </div>
    <script>
        Vue.component('base-button', {
            template: `
                <button type="button" class="btn">
                    <slot :text="text"></slot>
                </button>
            `,
            data(){
                return{
                    text:"submit"
                }
            }
        })
        var vm = new Vue({
            el: "#app",
            data: {

            }
        });
    </script>
</body>

</html>

你可以狠狠点击此处具体示例查看效果。

在这个例子中,我们将包含所有插槽prop的对象命名为slotProps,当然,也可以换其它的命名,这取决于每个人自己的想法。在这里,我们需要注意的就是,当被提供的插槽只有默认插槽的时候,组件的标签才可以被当做模板使用,我们才可以将v-slot绑定到组件标签上,如下:

<base-button v-slot:default="slotProps">
    <span>{{ slotProps.text }}</span>
</base-button>

当然,这种写法是还可以更简单的,vue把不带参数的v-slot指定为默认插槽。如下:

<base-button v-slot="slotProps">
    <span>{{ slotProps.text }}</span>
</base-button>

当有多个插槽,也就是有具名插槽的时候,以上的写法是不被允许的,因为这会导致作用域不明确。如以下示例:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot</title>
</head>

<body>
    <div id="app">
        <base-layout v-slot="footer">
            <template v-slot:header>
                <h1>这是头部内容</h1>
            </template>
        </base-layout>
    </div>
    <script>
        Vue.component('base-layout', {
            template: `
                <div class="page">
                    <header class="base-header">
                        <!-- 头部 -->
                        <slot name="header"></slot>
                    </header>
                    <main class="base-main">
                      <!-- 主体内容 -->
                       <slot name="main"></slot>
                    </main>
                    <footer class="base-footer">
                      <!-- 底部 -->
                       <slot name="footer"></slot>
                    </footer>
                </div>
            `
        })
        var vm = new Vue({
            el: "#app",
            data: {

            }
        });
    </script>
</body>

</html>

vue会在浏览器控制台给出一个提示,如下图:

clipboard.png

你可以狠狠点击此处具体示例查看效果。

只要出现多个插槽,就应该将v-slot绑定到<template>元素上。如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot</title>
</head>

<body>
    <div id="app">
        <base-layout>
            <template v-slot:header>
                <h1>这是头部内容</h1>
            </template>
            <template v-slot:footer>
                <h1>这是底部内容</h1>
            </template>
        </base-layout>
    </div>
    <script>
        Vue.component('base-layout', {
            template: `
                <div class="page">
                    <header class="base-header">
                        <!-- 头部 -->
                        <slot name="header"></slot>
                    </header>
                    <main class="base-main">
                      <!-- 主体内容 -->
                       <slot name="main"></slot>
                    </main>
                    <footer class="base-footer">
                      <!-- 底部 -->
                       <slot name="footer"></slot>
                    </footer>
                </div>
            `
        })
        var vm = new Vue({
            el: "#app",
            data: {

            }
        });
    </script>
</body>

</html>

这样;vue就不会在浏览器中给出警告了。你可以狠狠点击此处具体示例查看效果。

作用域插槽的工作原理其实就是将插槽内容包括在一个封装的单个参数的函数里面,就像如下所示:

function slot(slotProps){
    //插槽内容
}

这也就意味着v-slot的值实际上就是任何可以用作函数参数的JavaScript表达式,因此,在浏览器支持或者单文件组件的情况下,我们也是可以使用es2015的解构赋值来作为传入的插槽。例如:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot</title>
</head>

<body>
    <div id="app">
        <base-button>
            <template v-slot="{ user }">
                <span>{{ user.firstName }}</span>
            </template>
        </base-button>
    </div>
    <script>
        Vue.component('base-button', {
            template: `
                <button type="button">
                    <slot :user="user"></slot>
                </button>
            `,
            data(){
                return{
                    user:{
                        firstName:"evening",
                        lastName:"water"
                    }
                }
            }
        })
        var vm = new Vue({
            el: "#app",
            data: {
                
            }
        });
    </script>
</body>

</html>

你可以狠狠点击此处具体示例查看效果。

你甚至还可以将插槽prop重命名,例如:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot</title>
</head>

<body>
    <div id="app">
        <base-button>
            <template v-slot="{ user:person }">
                <span>{{ person.firstName }}</span>
            </template>
        </base-button>
    </div>
    <script>
        Vue.component('base-button', {
            template: `
                <button type="button">
                    <slot :user="user"></slot>
                </button>
            `,
            data(){
                return{
                    user:{
                        firstName:"evening",
                        lastName:"water"
                    }
                }
            }
        })
        var vm = new Vue({
            el: "#app",
            data: {
                
            }
        });
    </script>
</body>

</html>

如上,我们将user重命名为person,你可以狠狠点击此处具体示例查看效果。

我们甚至还可以定义后备内容,用于插槽propundefined的情况,比如:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot</title>
</head>

<body>
    <div id="app">
        <base-button>
            <template v-slot="{ user = { firstName:'evening'} }">
                <span>{{ user.firstName }}</span>
            </template>
        </base-button>
    </div>
    <script>
        Vue.component('base-button', {
            template: `
                <button type="button">
                    <slot>{{ user.lastName }}</slot>
                </button>
            `,
            data(){
                return{
                    user:{
                       lastName:"water"
                    }
                }
            }
        })
        var vm = new Vue({
            el: "#app",
            data: {
                
            }
        });
    </script>
</body>

</html>

你可以狠狠点击此处具体示例查看效果。

我们也可以将具名插槽进行缩写,也就是把v-slot:缩写为#。例如,将v-slot:footer缩写为#footer,值得注意的就是这种写法是不被允许的,#={user}。我们必须要指定插槽名,哪怕是默认插槽也一样。如#default={user}。如下例:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot</title>
</head>

<body>
    <div id="app">
        <base-layout>
            <template #default>
                <span>这是默认插槽的内容</span>
            </template>
            <template #header>
                <h1>这是头部内容</h1>
            </template>
            <template #main>
                <p>这是主题内容</p>
            </template>
            <template #footer>
                <span>这是底部的内容</span>
            </template>
        </base-layout>
    </div>
    <script>
        Vue.component('base-layout', {
            template: `
                <div class="page">
                    <slot></slot>
                    <header class="base-header">
                        <!-- 头部 -->
                        <slot name="header"></slot>
                    </header>
                    <main class="base-main">
                      <!-- 主体内容 -->
                       <slot name="main"></slot>
                    </main>
                    <footer class="base-footer">
                      <!-- 底部 -->
                       <slot name="footer"></slot>
                    </footer>
                </div>
            `
        })
        var vm = new Vue({
            el: "#app",
            data: {

            }
        });
    </script>
</body>

</html>

如果我们将#default中的default去掉,那么vue会在浏览器中给出一个警告,如下图所示:

clipboard.png

你可以狠狠点击此处具体示例查看效果。

我们也可以绑定动态插槽,如:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>slot</title>
</head>

<body>
    <div id="app">
        <base-layout>
            <template #default>
                <span>这是默认插槽的内容</span>
            </template>
            <template v-slot:[slotname]>
                <h1>这是头部内容</h1>
            </template>
            <template #main>
                <p>这是主题内容</p>
            </template>
            <template #footer>
                <span>这是底部的内容</span>
            </template>
        </base-layout>
    </div>
    <script>
        Vue.component('base-layout', {
            template: `
                <div class="page">
                    <slot></slot>
                    <header class="base-header">
                        <!-- 头部 -->
                        <slot name="header"></slot>
                    </header>
                    <main class="base-main">
                      <!-- 主体内容 -->
                       <slot name="main"></slot>
                    </main>
                    <footer class="base-footer">
                      <!-- 底部 -->
                       <slot name="footer"></slot>
                    </footer>
                </div>
            `
        })
        var vm = new Vue({
            el: "#app",
            data: {
                slotname:'header'
            }
        });
    </script>
</body>

</html>

动态插槽就是以v-slot:[slotName]的形式,但在这里需要注意一点,那就是动态插槽的插槽名不能包含大写字母,否则,vue会在浏览器中给出一个警告,如下图所示:

clipboard.png

你可以狠狠点击此处具体示例查看效果。

关于插槽的基础就只有这些,但要深入的了解可以浏览这些诸如Vue Virtual Scroller,Vue Promised,Portal Vue 等库。

5.动态组件与异步组件

在前面,我们曾完成这样利用is特性的组件切换示例,is绑定组件名is绑定组件选项对象。现在我们来尝试修改一下示例的代码,如下:

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <!-- 引入vue.js开发版本 -->
    <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <title>keep-alive</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .tab-component-demo {
            width: 900px;
            margin: 15px auto;
            padding: 15px 10px;
        }

        button {
            outline: none;
            border: 1px solid #ebebeb;
            background-color: transparent;
            display: inline-block;
            padding: 6px 7px;
            text-align: center;
            line-height: 1;
            transition: all 0.3s cubic-bezier(0.075, 0.82, 0.165, 1);
            cursor: pointer;
            font-size: 16px;
            border-radius: 4px;
        }

        button:hover,
        button:active {
            border-color: #58e9fc;
            background-color: #0abce9;
            color: #ffffff;
        }

        .ew-page {
            border: 1px solid #999;
            padding: 15px 8px;
        }
    </style>
</head>

<body>
    <div id="app">
        <div class="tab-component-demo">
            <button v-for="(tab,index) in tabs" :key="index" @click="currentTab = tab">
                {{ tab }}
            </button>
            <component :is="currentTabComponent"></component>
        </div>
    </div>
    <script>
        const childComponents = [
            {
                name: "tab-header",
                component: {
                    template: `<div class='home-content'>the header of home page!</div>`
                }
            },
            {
                name: "tab-content",
                component: {
                    template: `<div class='home-content'>the content of home page!</div>`
                }
            },
            {
                name: "tab-footer",
                component: {
                    template: `<div class='home-content'>the footer of home page!</div>`
                }
            }
        ];
        Vue.component("tab-home", {
            template: `
                <div class="ew-page">
                  <button
                      v-for="(childTab,index) in childTabs"
                      :key="index"
                      @click="currentChildTab = childTab"
                      >
                      {{ childTab.name.slice(4) }}
                   </button>
                   <component :is="currentChildTab.component"></component>
                </div>
            `,
            data() {
                return {
                    childTabs: childComponents,
                    currentChildTab: childComponents[0]
                }
            }
        });
        Vue.component("tab-list", {
            template: `
                <div class="ew-page">
                 list page!
                </div>
            `
        });
        Vue.component("tab-contact", {
            template: `
                <div class="ew-page">
                contact page!
                </div>
            `
        });
        var vm = new Vue({
            data: {
                currentTab: "home",
                tabs: ["home", "list", "contact"]
            },
            computed: {
                currentTabComponent: function () {
                    return "tab-" + this.currentTab.toLowerCase();
                }
            }
        }).$mount(document.getElementById("app"));
    </script>
</body>

</html>

你会注意到,当我们第一个导航(即home)所展示的页面中又继续包含3个子导航,我们可以再继续切换。但是当我们切换了子导航之后,再去切换父导航,最后切换回到父导航home,我们会发现之前切换过的子导航会回到初始状态(即header),这是因为每次切换,vue都新创建了一个组件实例。你可以狠狠点击此处具体实例查看效果。

尽管重新创建动态组件是非常有用的,但在这个例子中,我们可能更希望组件在第一次被创建的时候就缓存下来。为了解决这个问题,vue提供了<keep-alive>组件。我们只需要用这个组件包裹动态组件即可,如下:

  <keep-alive>
    <component :is="currentTabComponent"></component>
  </keep-alive>

现在我们再来测试一下这个修改后的示例具体示例。我们可以看到,现在组件已经保持了它的状态。

注意一点,<keep-alive>组件要求被切换的组件都有自己的名字,不论是通过组件的name选项还是局部或者全局注册。

你可以在keep-alive API文档中查看更多细节内容。


夕水
5.2k 声望5.7k 粉丝

问之以是非而观其志,穷之以辞辩而观其变,资之以计谋而观其识,告知以祸难而观其勇,醉之以酒而观其性,临之以利而观其廉,期之以事而观其信。