Vue笔记

VUE介绍

  • Vue的特点
    1. 构建用户界面,只关注View
    2. 简单易学,简洁、轻量、快速
    3. 渐进式框架
  • 框架VS库
    库,是一封装好的特定方法的集合,提供给开发者使用,库没有控制权,控制权在使用者手中;
    代表:jQuery、underscore、util
    框架,框架顾名思义就是一套架构,会基于自身的特点向用户提供一套相当完整的解决方案,而且控制权的在
    框架本身,使用者要找框架所规定的某种规范进行开发
    代表:backbone、angular、vue

理解渐进式

所谓的渐进式,可以一步一步、有阶段性的使用Vue,不是必须在一开始把所有的东西都用上。

  1. 声明式的渲染(Declarative Rendering)
  2. 组件系统(Components System)
  3. 客户端路由器(vue-router)
  4. 大规模的状态管理(vuex)
  5. 构建工具(vue-cli)


    image.png

Vue的两个核心点

  1. 响应式的数据绑定
    当数据发生改变 -> 视图自动更新
    忘记操作DOM这回事,而是专注于操作数据
  2. 可组合的视图组件
    把视图按照功能,切分若干基本单元;
    组件可以一级一级组合成整个应用,形成了倒置的组件树;
    使用组件的好处:可维护、可重用、可测试;


    视图组件

启动应用

  • 全局的构造函数
    new Vue({选项对象})
  • 选项对象
    告诉Vue你要做什么事情
    el: 告诉Vue管理的模板范围。 css selector || element
    data: 数据对象,会被实例代理,转成getter/setter形式。
    Object 定义好的数据可以直接在模板中使用
  • 插值
    1.使用双大括号(“Mustache”语法)做文本插值
    2.可以写任意表达式
    3.属性名、条件运算符、数据方法调用

声明式渲染

  • 声明式
    只需要声明在哪里where 做什么what,而无需关心如何实现how
  • 命令式
    需要以具体代码表达在哪里where做什么what,如何实现how
  • 声明式渲染理解
    1. DOM状态只是数据状态的一个映射;
    2. 所有的逻辑尽可能在状态的层面去进行,就是说,所以逻辑去操作数据,不要想着去操作DOM;
    3. 当状态改变了,view会被框架自动更新到合理的状态;
      过程图

指令directive

  • 理解指令
  1. 是一种特殊的自定义行间属性,以v-开头
  2. 将数据和DOM做关联,当表达式的值改变时,响应式的作用在视图
  3. 预期的值为javascript表达式
  • 指令初体验
    v-bind:动态的绑定数据。简写为

指令学习

DOM结构渲染说明

DOM结构为模板,先把DOM结构渲染出来,Vue会找到这个模板,进行解析,绑定值绑定在指定的位置,重新替换原来的DOM结构。

  • v-text:更新元素的 textContent,可代替{{}}
  • v-html:更新元素的 innerHTML
    不使用这个指令,插入的html结构不作为模板编译,作为文本显示;
    不要插入不安全的html结构;
  • v-cloak:隐藏未编译的 Mustache 标签直到实例准备完毕
  • v-once:只渲染一次,随后数据改变将不再重新渲染,视为静态内容,用于优化更新性能

列表渲染

  • v-for
    作用:对一组数据循环生成对应的结构
    语法:
    循环数组:v-for=“item,index in 数组”
    循环数组:v-for=“value,key,index in 对象”
    v-for:使用v-for循环对象时,拿到的下标是按照object.keys()的顺序来的;
  • key值
    对渲染的列表的结构采用“就地复用”的策略,也就说当数据重新排列数据时,会复用已在页面渲染好的元素,不会移动 DOM 元素来匹配数据项的顺序,这种模式是高效的,改变现有位置的结构的数据即可;
    需要提供一个key值,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素。

条件判断

  • v-if:根据表达式的值的真假条件渲染元素和移出元素;(true渲染,false不渲染)
    -v-else:成功走if,不成功走else,与js中的if else逻辑相同
    注意:v-if和v-else必须要连起来用,不能单独出现,会报错,并且,两者之间不可以有间隔
  • v-show: 根据表达式的值的真假条件,切换元素的 CSS 属性 display属性;
    区别:
    初始页面根据条件判断是否要渲染某一块的模板,使用v-if
    频繁的切换模板的显示隐藏,使用v-show

自定义指令

需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
注册方法:
全局注册(Vue.directive)与局部注册(directives:{}):
钩子函数
bind:只调用一次,指令第一次绑定到元素时调用
inserted:被绑定元素插入父节点时调用
update:更新时调用
componentUpdated: 更新完毕调用
unbind:只调用一次,指令与元素解绑时调用,就是当元素被移除DOM的时候就会触发这个函数
钩子函数参数

  • el:指令所绑定的元素,可以用来直接操作 DOM
  • binding:可以看成是一个配置自定义指令的一些属性的对象;
    binding对象中的一些属性说明
    • name:指令名,不包括v-前缀
    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 "foo"
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar中,修饰符对象为 { foo: true, bar: true }
  • vnodeVue 编译生成的虚拟节点。
  • oldVnode:上一个虚拟节点,仅在updatecomponentUpdated 钩子中可用。

自定义指令小练习

  • input自动获取焦点
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>input自动获取焦点</title>
</head>
<body>
    <div id="app">
        <!-- v-focus:这就是下面自定义的指令 -->
        <input type="text" v-focus>
    </div>
<script src="../vue.js"></script>
<script>

    //全局写法
    // Vue.directive('focus',{
    //     inserted(el,binding){
    //         el.focus();
    //     }
    // });
    new Vue({
        el:'#app',
        //局部写法
        directives:{
            focus:{
                inserted(el,binding){
                    console.log(binding)
                    el.focus();
                }
            }
        }
    });
</script>
</body>
</html>
  • 点击按钮显示下拉列表,点击随意位置隐藏下拉列表
 

事件系统

  • v-on:可以用v-on 指令监听 DOM 事件,简写为@ ;

  • 事件处理函数
    事件处理函数写在methods中;
    在模板中不传参,只写上函数名字,函数第一个参数事件对象作为事件处理函数的第一个参数传入的;
    在模板中传参,需要手动在模板中使用$event传入事件对象;
    事件处理函数(methods)中的this都指向根实例(Vue);

  • 事件修饰符
    方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
    语法:v-on:事件名.修饰符=”处理函数|表达式|处理函数执(tu)行”.stop(取消冒泡)、.prevent(阻止默认行为)、.capture(阻止捕获)、.self(在自己身上触发)、.once(事件只执行一次)
    按键修饰符.enter、.tab、.delete 、.esc、.space、.up、.down、.left、.right

简单的控制显示隐藏案例:

<div id="app">
<!--点击按钮,来改变display的值(true/flase),这里写的是表达式-->
    <button @click="display = !display">按钮1</button>
 <!--
1.事件指令写方法函数名,这个方法名要在vuer的methods中定义;
2.如果方法中需要传参,就可以直接写clickFn(str),这里写法和js的函数写法一样,下面方法中接收就可以了-->
      <button @click="clickFn">按钮2</button>

<!--通过上面按钮点击时,改变的display的值,使用指令v-show实现下面div的显示和隐藏-->
     <div v-show="display">点击显示隐藏</div>
</div>
<script>
     let vm = new Vue({
          el:'#app',
          data:{
              //通过创建一个状态,通过改变状态实现想要得到的效果;
              display:true
          },
          methods:{  //在模板中使用的方法,都统一写在这里
               clickFn(){
                   console.log('我是按钮2执行的方法')
               }
          }
     });
</script>

双向数据绑定

Vue将数据对象和DOM进行绑定,彼此之间互相产生影响,数据的改变会引起DOM的改变,DOM的改变也会引起数据的变化;

  • 可交互的表单元素
    input(text、checkbox、radio) textarea select

  • v-model 是个语法糖,数据绑定在value上,监听了oninput事件
    1.在表单元素上创建双向数据绑定;
    2.会根据控件类型自动选取正确的方法来更新元素交互的值;
    3.负责监听用户的输入事件以更新数据;
    注意:
    1.会忽略所有表单元素的value、checked、selected特性的初始值;
    2.将Vue实例的数据作为数据来源;
    3.需要在数据对象中声明初始值;

双向数据绑定示意

示意图
  • 上图流程说明:数据(Model)通过VM(ViewModel)绑定到视图(View),然后用户进行交互,交互后的数据又通过VM中绑定的Model,改变Model中的数据;
流程图

MVVM模式说明:

M=Model
V=view
VM=vue实例
数据通过VM层绑定在视图上,视图上有交互,改变了数据,改变之后会重新渲染;

响应式原理

把一个普通的 JavaScript 对象传给Vue实例的data 选项 Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter
Vue内部会对数据进行劫持操作,进而追踪依赖,在属性被访问和修改时通知变化;
具体步骤:
step1:需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 settergetter
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
step2compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
step3Watcher订阅者是ObserverCompile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
step4MVVM作为数据绑定的入口,整合ObserverCompileWatcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起ObserverCompile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

Object.defineProperty

  • 作用:直接在一个对象上定义一个新属性,或者修改一个对象的现有属性;(通俗意思就是给对象定义属性的一个方法)

  • 语法:Object.defineProperty(obj, prop, descriptor)

  • 参数:

obj要在其上定义属性的对象 prop 要定义或修改的属性的名称
descriptor :描述对象,将被定义或修改的属性·(prop)·描述符;

  • 数据描述对象

configurable: 是否可以删除目标属性。默认为false
enumerable : 此属性是否可以被枚举(遍历)。默认为false
value : 该属性对应的值,默认为 undefined
writable : 属性的值是否可以被重写。默认为false

  • 存取器描述(getter、setter)

getter:是一种获得属性值的方法(当获取值的时候触发的方法);
setter:是一种设置属性值的方法(设置值的时候触发的方法)
可以写configurableenumerable
不能写valuewritable
注意:这个方法可以新添加属性也可以对原有的属性进行配置;
setter方法中,在设置值时,会传一个参数newValue,这个是当前改变的值,newValue是跟着设置的值而改变的;

给一个对象定义属性的小例子:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>对象定义属性</title>
</head>
<body>
<script>
    /*
        普通定义方法
        以下添加的属性默认特性:可删除,可改写,可遍历
    */
    let obj ={a:1};
    obj.b = 10;//给obj这个对象添加一个b属性,并赋值一个10
    console.log(obj);//{a:1,b:10}

    /*
        Object.defineProperty(obj,property,描述对象/访问器(存取器(getter、setter)))定义方法
        通过描述对象中的属性,可以设置添加的属性,是否可以删除,改写,遍历等等一系列的控制;
        第三个参数是一个对象,里面可以是描述对象,也可以是存取器(getter和setter)
    */
    //以下为描述对象例子
    let obj2 = {a:1};
    Object.defineProperty(obj2,'b',{
        value:'我是b属性的值',
        //这里只是简单写个小实例
        writable:true // 属性是否可以被改写,默认是false为不可以,这里设置成true
    });
    //改写前
    console.log(obj2);//{a:1,b:'我是b属性的值'}
    obj2.b = 99;//这里进行改写
    //改写后
    console.log(obj2);//{a:1,b:'99'}

    //以下为存取器例子
    let obj3 = {a:1};
    let val = 1000; //这个变量就是设置成了动态的,一般常用的都是这种动态的
    Object.defineProperty(obj3,'b',{
        get(){//获取时触发的函数
            console.log('获取了');
            return val; //返回的就是要获取的值
        },
        set(newValue){//设置值时触发的函数
            console.log('设置了');
            console.log(newValue);
            val = newValue; //把改变的值赋值给指定变量
        }
    });

    obj3.b //这里触发的就是get中的函数
    obj3.b = 100 //这里触发的就是set中的函数,newValue就是100,

</script>
</body>
</html>

简单的数据劫持小例子:

  • object.defineProperty劫持:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>使用object.defineProperty劫持</title>
</head>
<body>
    <button id="btn">改变改变</button>
    <div id="d1"></div>
    <script>
        //获取元素
        let btn = document.querySelector('#btn');
        let d1 = document.querySelector('#d1');
        //准备数据
        let data ={
            title:'我是初始值'
        };
        //对要改变的数据进行劫持
        observer(data);
        //初始化赋值
        d1.innerHTML = data.title;
        btn.onclick = function(){
            //按钮点击后,改变数据中的值,这时,通过上面的数据劫持函数中的处理,
            //就可以直接改变页面中的数据了;
            data.title = "改变了";
        };
        //用来劫持数据
        function observer(data){
            //循环data数据中的每一个属性,再在每一个属性中添加get和set方法
            // Object.keys()返回的是对象所有属性的数组
            Object.keys(data).forEach(function(item){
                 definedReactive(data,item,data[item])
            });
        }
        /*
            给数据中的属性添加get set方法
             obj:要添加属性的对象
             property:要操作的属性
             value:要操作的值
         */
        function definedReactive(obj,property,value){
            Object.defineProperty(obj,property,{
                get(){
                    //在获取值函数中,直接返回要显示的值
                    return value;
                },
                set(newValue){
                    value = newValue;//将最新的值,赋值给value
                    //这里的d1.innerHTML是直接写死了,正常开发这里应该是动态的
                    d1.innerHTML = newValue;
                }
            });
        }

    </script>
</body>
</html>
  • proxy劫持:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>proxy劫持</title>
</head>
<body>
    <button id="btn">设置改变</button>
    <div id="d1"></div>

    <script>
        //获取元素
        let btn = document.querySelector('#btn');
        let d1 = document.querySelector('#d1');
        //准备数据
        let data = {
            title:'我是初始值'
        };
        //数据劫持函数,这个函数会返回一个新的对象数据,要把返回的数据再赋值给data数据中
        data = observer(data);
        //初始化div显示的数据
        d1.innerHTML = data.title;
        btn.onclick = function(){
            data.title = '改变了'
        };
        function observer(data){
            /*
                这里使用ES6中的proxy,对象代理,会返回一个新的对象
                新对象中会包括被代理对象中的所有属性与值,再进行其它的操作;
                比起之前使用Object.defineProperty省略了循环对象属性那一步,
                这个方法,只要代理了,就会自动得到所有属性不需要循环;
             Proxy(obj,{})
             obj:要代理的对象;{}:这里面写要实现的代理方法
            */
            let newData = new Proxy(data,{
                //这里需要给每一个属性都添加get和set方法

                /*
                 get(target,key):拦截(代理)对象属性的读取
                 target:就是obj这个原始数据中所有的数据;
                 key:数据对象中的key名,下面的monitor调用哪个属性,
                */
                get(target,key){
                    //target就是data的数据
                    return target[key]
                },
                /*
                 set(target,key,value):拦截对象设置属性
                 target:原始数据对象; key:要修改的属性;value:要修改的值;
                */
                set(target,key,value){
                    //把要改变的值赋值给对应的属性的值
                    target[key] = value;
                    //这里也是写死的,正常开发d1.innerHTML是动态的变量
                    d1.innerHTML = value;
                }
            });

            //proxy返回的是一个新的对象,返回的对象就是新的数据对象,所以最后把这个对象返回出去
            return newData;
        }
    </script>
</body>
</html>

响应的数据变化

data对象中的数据都会被转换为getter/setter,所以当数据发生变化时,自动更新在页面中;
如果没有在vue对象的data`属性中初始化变量的值,那么这个变量就不是响应的(不能检测属性的变化)

  • Vue.set( target, key, value )
    设置对象的属性。如果对象是响应式的,确保属性被创建后也是响应式的,同时触发视图更新。这个方法主要用于避开 Vue不能检测属性被添加的限制
  • 参数说明:
    target:要添加属性的对象;
    key:要添加的属性名;
    value:要添加的属性值;
    这个方法返回的是设置的值;
  • vm.$set( target, key, value ):实例上的方法
  • 参数说明:与上面的Vue.set()方法参数一样;
  • 替换对象Object.assign()
    注意:注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象;

数组的变异方法

vue实例中,可以使用this.的方式直接调取数据的一些常用方法,如:
push()、pop()、shift()、unshift()、splice()、sort()、reverse()

  • 以上这些变异方法的注意事项:

1.都可以改变原数组,因为这些方法vue做为了变异方法,其它的数组原有方法都没有变异;
2.不能使用下标改值,比如:
this.arr[0]=10; //这些写会没有效果,是不可以改的,如果想改变,可以使用splice()来改;
比如:concat这个方法是不可以改变原数组的,因为不是变异方法;
3.不能使用length改变数组,比如:
this.arr.length=1;//可以使用 splice()来改变或者也可以通过重新改变数组;

*注意:这些方法都是vue变异的方法,不是原生的;都是vue改写过的,为了更方便的使用。所以vue提供了观察数组的变异方法,使用这些方法将会触发视图更新;

计算属性

  • 计算属性定义在computed中,它不是方法,属性的值是函数的返回值;
  • 把对处理数据的逻辑抽离在计算属性中,使得模板更加轻量易读;
  • 计算属性的值会被缓存,并根据依赖的数据变化而重新计算;

计算属性语法

    key:function(){}
   //key的值是函数的返回值,内部调用这个函数;
   //所以计算属性会被放在实例上,通过this.可以找到对应的计算属性;
    //计算属性中的this都指向实例

计算属性与methods的区别

  • 计算属性只有在数据改变了才会执行,methods只要调用就执行;
  • 数据如果频繁更新改变,使用methods就比较合适;反之使用计算属性;

计算属性的使用

  • 取值: 触发get函数,默认设置的函数
  • 赋值: 触发set函数

watch观察

利用watch观察 Vue 实例上的数据变动,键是需要观察的表达式,值是对应回调函数

  • 回调函数会接受两个参数:
    newValue:变化的最新值
    oldValue:变化之前的值
具体写法:
    //效果是:点击按钮,message值变成123
    <input type="button" value="我要变变变" @click="message=123"/>
    <p>{{message}}</p>
    let vm = new Vue({
        el:'.app',
        data:{
            message:'hello word!',//单层数据
            name:{//多层数据
                 zhangsan:{
                      age:12
                  }
            }
        }
    });
    /*
      通过watch可以监控到data数据中属性改变的值; 
      vm对象下直接就有$watch()的方法,传两个参数;
      第一个参数:要改变的属性名(或者是路径);
      说明:直接写属性名相当于数据只有一层,当遇到多层数据,比如对象里面套对象这类时,就需要写路径了;
      第二个参数:数据改变后监控到值的回调;
              在这个回调函数中传两个参数newvalue,oldvalue:
              顾名思义,一个是改变后的新值,一个是改变前的旧值;
    */
    vm.$watch('message',function(newvalue,oldvalue){ //这是单层数据监控
          console.log('改变了')
    }');
    vm.$watch('name.zhangsan.age',function(newvalue,oldvalue){ //这是多层数据监控,针对性的监控某一个值
          console.log('改变了')
    }');

//总结:总的来说,以上多层监控时,哪个属性改变监控哪个。

/*
  一般实际项目中,watch都是写到new Vue里面的,直接写成
wacth:{
    message(newvalue,oldvalue){ //单层监控
          console.log('改变了')
    },
    'name.zhangsan.age'(newvalue,oldvalue){//多层监控
        console.log('改变了')
    }
}
*/
  • 深度监控
    handler(){}
    deep:true //true就是表示深度监控
    注意:由于watch在监控时,会遍历对象中的所有属于,而遍历是非常耗费性能的,所以不易监控太深的数据,在使用要特别的注意选择;
具体写法
    let vm = new Vue({
        el:'.app',
        data:{
          name:{//多层数据
                 zhangsan:{
                      age:12
                  }
            }
        },
        watch:{ //这里要监控name这个对象的改变,只要name里面的任何数据有了改变,就要监控到;
            //这种写法在页面第一次加载时,不会自动调用下面的监控
            'name':{
                handler(newvalue,oldvalue){
                        console.log('有属性发生改变')
                },
                deep:true
            }
           //下面这种写法是立即调用,就是在页面第一次加载时,就调用监控,当真正发生改变时,依旧会执行
            'name':{
                  handler(newvalue,oldvalue){
                          console.log(''改变了)
                  },
            //可以联着写,既可以加载就执行,也可以深度监控
                  immediate:true,
                  deep:true
            }
        }
    });

methods观察

定义一些方法,将被混入到 Vue 实例中

  • 使用method
    可以直接通过 VM 实例访问这些方法,或者在指令表达式中使用,方法中的this 自动绑定为 Vue 实例

组件化开发

组件可以将UI页面切分成一些单独的、独立的单元,整个页面就是这些单元组合而成的,只需要关注构建每个单独的单元,每个组件包含自己的结构、逻辑和样式,这样既减少了逻辑复杂度,又能实现组件的复用性。当不需要某个组件或替换组件时,而不影响整个应用的运行。

  • 组件开发的好处:
    降低耦合度、提高可维护性、可复用性便与协作开发;

全局注册组件

  • 分为三步骤:创建组件构造器、注册组件和使用组件
  • Vue.extend(选项对象)
       //当页面中需要多个vue实例时,就可以使用extend来实现
       <div id='app'>{{message}}</div>
       <div id='index'>{{message}}</div>
       //写法与new Vue一样,对象中使用的方法也是全部都一样,除了不可以写el;
       //子构造器
        let ex = Vue.extend({
               data(){
                     return {
                           message:'hello extend'
                     }
               }
       });
       //这里的ex返回的是一个实例,所以想要调用$mount方法必须要new
       //执行方法:extend必须要使用$mount()来实现挂载;
      new ex().$mount('#index')
    
       let vm = new Vue({
            data(){
                     return {
                           message:'hello extend'
                     }
               }
       });
        vm.$mount('#app'); //手动挂载
     
    
Vue.extend():返回构造器实例
  • 和根实例的选项对象一样,不同的是没有el
  • data必须是一个工厂方法,返回对象;
  • template定义组件的模板;
  • 通过extend来实现一个组件
    组件说明:首先组件就是一个函数;
      <div id='app'>
            //组件是以标签的形式执行的,也可以把注册好的组件看成是自定义的标签
            <custom-ex></custom-ex>
      </div>

      //创建一个组件的函数
       let ex = Vue.extend({
              data(){
                    return {
                          message:'hello extend'
                    }
              },
              template:`
                      //注意这里的message不是new Vue里面的,是extend子构造器中的,与Vue完全是独立的。
                      <div>{{message}}</div>
              `
      });
      //将上面的函数注册成一个组件,这里使用的方法是注册一个全局组件,传两个参数,第一个参数是组件的名字(id名),第二个参数是组件的函数(可以直接是函数,也可以是函数名)
      Vue.component('custom-ex',ex);

      let vm = new Vue({
          el:'#app',
           data(){
                    return {
                          message:'hello extend'
                    }
              }
      });
全局注册组件

Vue.component( id, [definition] )
id: 字符串
definition:构造器实例或对象
如果为对象,内部会自动调用 Vue.extend
Vue实例范围内使用注册组件的id为标签名

步骤图

全局组件的定义与注册

说明:下面案例通过Vue.component('组件名',{选项对象})可以直接定义组件,第二个参数直接提供一个对象,vue内部会自动调用Vue.extend(),然后把这个返回的对象传到第二个参数中;
组件的选项对象没有el属性;

    <div id="app">
          <custom-ex></custom-ex>
    </div>
    //这样就是一个简单的组件,注册组件里面的template必须要加上,否则会报错;
    Vue.component('custom-ex',{
          template:`<div>我是组件</div>`
    });
    let vm = new Vue({
      el:'#app'
    });

局部注册组件

  • 选项对象的components属性注册局部组件,只能在所注册的作用域模板中使用;
    {components:{组件id:实例构造器 | 对象 } }
    局部组件就是在哪里定义,在哪里使用;
局部组件写法
      <div id="app">
            <custom-cc></custom-cc>
      </div>
      <script>
            let vm = new Vue({
                  el:'#app',
                  components:{
                      //这个就是局部组件,局部组件的作用域只在new Vue的这个模板的范围中使用,
                      //不可以用在其它地方,比如放到其它组件中是不可以的,会报错;
                        'costom-cc':{
                             template:`<div>我是局部组件</div>`
                        }
                  }
            });
      </script>
  • 组件中data的注意

  1. data必须是一个函数,函数中返回一个新对象,如果是对象形式会警告必须是函数;
  2. 这样做防止组件引用同一个对象,导致数据相互影响;
  • html模板
    放在一对template标签中,在组件中使用标签的id
    放在一对script标签中,标签的type要设置为text/x-template

组件命名约定和模板

  • 注册组件命名:
    kebab-case (短横线分隔命名)
    camelCase (驼峰式命名)
    PascalCase (单词首字母大写命名)
    不能使用html规定的标签名
  • 使用组件命名:
    kebab-case (短横线分隔命名)

prop

prop上的属性都会放在当前组件的实例上面
props:['test'],可以是多个,也可以是对象形式的;

prorp的验证引用官网API:

Vue.component('my-component', {
props: {
// 基础的类型检查 (null 匹配任何类型)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组且一定会从一个工厂函数返回默认值
default: function () {
return { message: 'hello' }
} },// 自定义验证函数
propF: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
})

一个简单的组件小练习

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>news</title>
</head>
<body>
<div id="app">
     <!--循环新闻数据,将对应数据传到组件中-->
    <news-cutom v-for="item,index in newsData" :data="item"></news-cutom>
</div>
<script src="../vue.js"></script>
<script>
    /*
        注册组件,这里使用的是全局的
    */
    Vue.component('news-cutom',{
        /*组件通过props接收*/
        props:['data'],
        /*将接收的数据显示到页面中*/
        template:`
            <div>
                <h1>{{data.title}}</h1>
                <ul>
                    <li v-for="item,index in data.news">{{item}}</li>
                </ul>
            </div>`
    });
    //新闻数据
    let newsList = [
        {title:'娱乐',news:['娱乐新闻1','娱乐新闻2','娱乐新闻3']},
        {title:'体育',news:['体育新闻1','体育新闻2','体育新闻3']},
        {title:'视频',news:['视频新闻1','视频新闻2','视频新闻3']}
    ];
    let vm = new Vue({
        el:'#app',
        data:{
            newsData:newsList
        }
    });
</script>
</body>
</html>

父子组件之间的通信

使用在一个组件模板中的组件,称之为子组件。

  • 组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据,采用单向绑定的方式,避免子组件直接修改父组件产生副作用;
  • 但是有时候需要父子组件之间需要相互通信:父组件可能要给子组件传递数据,子组件则可能要将它内部发生的事情告知父组件。
  • 通过props将父组件的数据传递给子组件;
  • 触发子组件的某个事件时,可以通知到父组件,那么在子组件中发布一个自定义事件,父组件监听自定义事件;
  • 组件实例.$emit(自定义事件名,参数);
    通信流程

父组件传递数据示意图

数据示意图

子组件发布事件示意图

事件示意图
三层组件之间的通信(父传子,子传父)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>多层组件通信练习</title>
</head>
<body>
<!--下面写一个三层组件之间的通信(父传子,子传父)-->

    <div id="app">
        <h2>我是根组件</h2>
        {{vmData}}
        <!--将根组件的数据传给第2层,再传给第3层,数据显示在第3层的组件中-->
        <coustom1 :coudata1="vmData"></coustom1>
    </div>

    <script src="../vue.js"></script>
    <script>
        /*
            根组件传过来的数据,接收也是一层一层接
            使用props接收,先第2层接收,再第3层接收然后显示到第3层
        */
        Vue.component('coustom1',{
            props:['coudata1'],
            template:`
                <div>
                    <h2>我是第2层组件</h2>
                     <p>第2层组件收到的数据:{{coudata1}}</p>
                     <span>下面是第3层组件</span>
                     <coustom2 :coudata2="coudata1" @changeFn="changeFn"></coustom2>
                </div>
            `,
            //通过自定义事件的方法,实现子组件向父组件通信
            methods:{
                //父组件中写自定义的事件,如果有参数传过来,这里可以直接接收传过来的参数
                //从页实现父组件收到子组件的值
                changeFn(){
                    console.log('监听到了第3层组件');
                }
            }
        });
        /*
            这里接收第2层传过来的cou1Data的数据
        */
        Vue.component('coustom2',{
            props:['coudata2'],
            template:`
                <div>
                     <h2>我是第3层组件</h2>
                     <p>第3层组件收到的数据:{{coudata2}}</p>
                </div>
            `,
            mounted(){
                //父组件使用$emit方法传值,第二个参数传需要传的值,
                // 这里测试没有用到参数
                this.$emit('changeFn')
            }
        });

        //vm就是所有组件中的父级,也就是根组件
        let vm = new Vue({
            el:'#app',
            data:{
                vmData:'我是根组件的数据'
            }
        });
    </script>
</body>
</html>

多层组件嵌套之间通信

方法有很多种:

  • 1.组件由外到内或者由内到外层层的传数据,这样做,如果是较少的几层,还可以使用,当遇到多层,十层二十层时这个时候就会显的非常的繁琐,而且很容易出错;
  • 2.当遇到非常多层,并且好多组件的数据是相互引用的,这时就可以使用一个叫VueX(状态管理器)来处理这种情况;
  • 3.当遇到第三层组件直接向第一层组件通信时,可以用一个叫做事件总线(event-bus)的东西的来解决;
//事件总线使用方法:
let vm = new Vue();
//监听event事件
vm.$on('event',function(){}); 
//发布这个事件
vm.$emit('event')

组件间的改值

父组件可以通过props向子组件传,但是子组件不可以向父组件传,这里就有一个单向数据流的概念;在组件中在使用props只是从外向内传;
如何子组件修改父组件的数据呢?
解决方法两种:.sync修饰符v-model

  • sync方法
    .sync修饰符:在2.0被移除后,发现还是需要双向绑定这种需求的存在,所以从2.3.0重新引入后,就作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的v-on监听器。

.sync具体使用案例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>.sync练习</title>
</head>
<body>
    <div id="app">
        <h1>父级:{{message}}</h1>

        <!--.sync是编译时的语法糖-->
        <costum :title.sync="message"></costum>
        <!--
           下面是编译的写法,这里是自定义事件,名字自己取;
           解释:动态绑定了要改变的数据message,再监听自定义事件,并传参数val,将val赋值给要改变的数据message
        -->
        <costum :title="message" @customFn="val => message = val"></costum>
    </div>
    <script src="../vue.js"></script>
    <script>
        /*
            总结sync使用步骤:
            1.在<costum>组件标签中绑定并监听要改变的数据message,写法::title.sync="message";
            2.将组件标签中的title通过props传到组件中;
            3.给组件中的按钮加点击事件;
            4.在点击事件中,通过this.$emit('update:title','子级改变了父级!!!'),来更新同步改变的值;
              注意:在监听.sync的自定义事件中,第一个参数是固定写法:update:要同步的属性名;
                    第二个参数,要改变的值,可以是动态也可以写死
        */
        Vue.component('costum',{
            props:{
                title:{
                    type:String,
                    default:'子级的数据'
                }
            },
            template:`<div><button @click="changeFn">点击改变父级数据</button></div>`,
            methods:{
                changeFn(){
                    this.$emit('update:title','子级改变了父级!!!');
                }
            }
        });
        new Vue({
            el:'#app',
            data:{
                message:'父级的数据'
            }
        });
    </script>
</body>
</html>
  • v-model方法
    v-model:自定义事件可以用来创建自定义的表单输入组件,使用v-model来进行数据双向绑定,它也是编译的语法糖;
    在组件上使用v-model在子组件中使用value来接收v-model的值;再调用emit监听来监听input来改变对应的值;
    那么问题来了,如果当需要的这个value名被组件占用,或者emit中要监听的自定义事件input也被占用,怎么办?
    解决办法:
Vue.component('custom',{
      //在组件中的model对象中,可以改变v-model接收值的名字。
       //这里相当于改了个别名
      model:{
          prop:'modelTest', //要改变的属性名
          event:'testFn' //要监听的事件名
      }
});

v-model具体使用案例

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>v-model练习</title>
</head>
<body>
    <div id="app">
        <h1>父级:{{message}}</h1>
        <!--v-model是编译的语法糖-->
        <custom v-model="message"></custom>
        <!--
            编译写法
            原理:动态绑定要改变的数据message,监听input事件,并将value值赋值给要改变的message
        -->
        <custom :vlaue="message" @input="value=>message=value"></custom>
    </div>
    <script src="../vue.js"></script>
    <script>
        /*
            v-model使用步骤:
            1.直接在子组件标签中写v-model='要改变的数据名';
            2.在子组件按钮中添加点击事件;
            3.在点击事件中,使用this.$emit监听input事件,并输入要改变的数据;
              这里注意,在v-model的监听中,第一个参数要输入input,第二个参数输入要改变的数据;
        */
        Vue.component('custom',{
            props:{
                title:{
                    type:String,
                    default:'子级的数据'
                }
            },
            template:`<div>{{title}} <p><button @click="changeFn">点击改变父数据</button></p></div>`,
            methods:{
                changeFn(){
                    this.$emit('input','子级改变了父级~~~~');
                }
            }
        });
        new Vue({
            el:'#app',
            data:{
                message:'父级的数据'
            }
        });
    </script>
</body>
</html>

使用插槽分发内容

  • 编译作用域
    父组件模板的内容在父组件作用域内编译;
    子组件模板的内容在子组件作用域内编译;
  • 插槽的作用
    将父组件中写在子组件一对标签内的结构混合在子组件模板中,这个过程称之为内容分发。使用特殊的 <slot> 元素作为原始内容的插槽;
  • 单个插槽(无命名插槽)
    如果子组件中没有一对slot标签,写在子组件标签对的内容会被丢弃;
    子组件中有slot标签,子组件标签对的内容会整体替换在slot标签位置;
    如:<slot></slot> //这个就是默认的插槽,只要组件标签中写内容都会替换slot标签中的内容
    slot标签内的内容被视作备用内容;
  • 具名插槽(有命名的插槽)
    可以使用name来配置如何分发内容;
    没有nameslot被视为默认插槽;
    如:
//有名字插槽的使用方法
<div id="app">
    <custom>
          <!--替换指定名字,就是用slot="对应名字"-->
        <p slot="list">我是要替换的内容p标签</p>
   </custom>
</div>
 <slot name="list"><span>我是组件中默认内容<span></slot> //当有名字时,就可以指定替换某个名字插槽的内容
  • 批量替换插槽
    如:
<div id="app">
  <custom>
<!--使用template的方法如下-->
        <template slot="list">
          <p>123</p>
          <p>123</p>
          <p>123</p>
      </template>
  </custom>
</div>
/*当需要替换li时,就可以使用组件中提供的
  template标签
*/
<slot name="list">
      <ul>
          <li>默认的</li>
          <li>默认的</li>
    </ul>
</slot>

  • 作用域插槽(slot-scope)
    说明:2.1.0新增的,在这个版本中只能局限于<template>标签上使用,在2.5.0+ 版本后,能在任意元素或组件中使用了;
    作用:可以在父组件中显示子组件的数据;
    例子:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>slot-scope使用练习</title>
</head>
<body>
    <div id="app">
        {{ms1}}
        <hr>
        <costum>
            <!--slot-scope="props":数据都存在props中,得到的是子组件数据的对象-->
            <ul slot-scope="props">
                <li>{{props.msg1}}</li>
                <li>{{props.msg2}}</li>
            </ul>
        </costum>
    </div>

    <script src="../vue.js"></script>
    <script>
        //注册子组件
        Vue.component('costum',{
            data(){
                return {
                    cosData:'我是子组件的数据'
                }
            },
            /*
             :msg1="cosData" :msg1属性绑定的子组件data中的动态数据;
             msg2="子组件的属性":msg2属性绑定的是写死的数据;
             只要在slot标签中写key=value格式的值,都会存到props中,保存的是对象格式,
             所以上面就可以使用slot-scope标签中使用props去获取子组件中相应的值;
            */
            template:`<div><slot :msg1="cosData" msg2="子组件的属性">{{cosData}}</slot></div>`
        });
        let vm =new Vue({
            el:'#app',
            data:{
                ms1:'我是父组件的数据'
            }
        });
    </script>
</body>
</html>

使用插槽分发内容示意图

分发内容示意图

封装通用的组件

说明:以下三个所需要的API不是绝对的,具体还要看实现的业务需求;

  • Vue 组件的 API 来自三部分——prop、事件和插槽
  1. Prop 允许外部环境传递数据给组件;
  2. 事件允许从组件内触发通过外面;
  3. slot插槽允许外部环境将额外的内容组合在组件中;

封装Modal组件

1.组件API
  • 设置的props:
    modalTitle提醒信息 默认为 '这是一个模态框';
    show 控制显示隐藏,需要双向绑定;
  • 定制模板:
    slotmodal-content 定制提醒信息模板;
    slotmodal-footer 定制底部模板;
  • 监控内部变化:
    事件名on-ok: 点击确定触发
    事件名on-cancel: 点击取消触发
2.组件代码
<!DOCTYPE html>
<html lang="zh-cn">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title></title>
        <style>
            p,h4{
                margin:0;
            }
            .model{
                width: 500px;
                background-color: #fff;
                border: 1px solid rgba(0,0,0,.2);
                border-radius: 6px;
                box-shadow: 0 3px 9px rgba(0,0,0,.5);
                position: absolute;
                left: 50%;
                top: 50%;
                transform: translate(-50%,-50%);
                z-index: 111;
            }
            .model-header {
                padding: 15px;
                border-bottom: 1px solid #e5e5e5;
            }
            .model-content div {
                padding: 20px;
            }
            .model-footer {
                padding: 15px;
                text-align: right;
                border-top: 1px solid #e5e5e5;
            }
            .btn {
                padding: 5px 15px;

            }
            .mask {
                position: fixed;
                top: 0;
                bottom: 0;
                left: 0;
                right: 0;
                background-color: rgba(55,55,55,.6);
                height: 100%;
                z-index: 100;
            }
        </style>
    </head>
    <body>
        <!--
            弹窗的组件,一般弹窗都是渲染在body下,那么这时就需要操作底层的html(model组件结构),
            在页面渲染时放在body下,这就需要用到的自定义指令;
        -->
        <div id="app">
            <button @click="show=true">显示弹窗</button>
            <!--使用关心的内部事件-->
            <!--
                v-model="show": 这里使用了v-model的方法来实现数据的双向绑定,
                                让子组件可以改变show这个变量
                v-transform-body:自定义指令,作用将组件插入到body中,需要注意的是,自定义指令也是可以根据对应的值来判断是否要执行,默认是true;
            -->
            <model ok-value="OK" cancel-value="不要"
                   @gogo="okChangeFn" @nono="cancelChangeFn"
                   v-model="show"  v-transform-body>
                <!--header部分-->
                <h1 slot="header">我是一个超大的标题</h1>
                <!--这里就是改变弹框的context部分-->
                <template slot="content">
                     <p>报名器</p>
                    <form action="">
                        <p>姓名:<input type="text"></p>
                    </form>
                </template>
            </model>
        </div>
        <script src="../vue.js"></script>
        <script>
            /*
                model是一个通用的弹框组件,需要以下这些内容:
                1.定制数据(数据都写在props中)
                    title:标题数据
                    okValue:确认按钮
                    cancelValue:取消按钮
                2.关心的内部事件(组件对外发布相关事件)
                    okValue的点击事件
                    cancelValue的点击事件
                3.定制结构(在组件中的template属性中,通过slot实现)
                    content:内容结构
             下面就根据这些要求来写这个组件
            */
            Vue.component('model',{
                //定制的数据都是放在props中
                props:{
                    title:{
                        type:String,
                        default:'默认标题'
                    },
                    okValue:{
                        type:String,
                        default:'确认'
                    },
                    cancelValue:{
                        type:String,
                        default:'取消'
                    },
                    value:{//显示隐藏model框的标识
                        type:Boolean,
                        default:false
                    }
                },
                /*
                    1.将写好的props中的数据,都绑定到组件结构中,就可以在组件标题上自定义数据了
                    2.在methods中发布确认和取消按钮的事件,事件名为:gogo,nono
                    3.定制header、content、footer三部分的结构,使用slot标签来表示可以更改

                    v-show="value":这个是用来隐藏弹窗的
                    v-model是用value来接收,所以props中要写value
                */
                template:`
                    <div class="model-example" v-show="value">
                        <div class="mask"></div>
                        <div class="model">
                            <div class="model-header">
                                    <slot name="header"><h4>{{title}}</h4></slot>
                            </div>
                            <div class="model-content">
                                <slot name="content">在这里添加内容</slot>
                            </div>
                            <div class="model-footer">
                                <slot name="footer">
                                    <input class="btn" type="button" :value="okValue"  @click="okFn"/>
                                    <input class="btn" type="button" :value="cancelValue" @click="cancelFn"/>
                                </slot>
                            </div>
                        </div>
                    </div>
                `,
                methods:{
                    /*在组件中发布关心的按钮事件*/
                    okFn(){
                        this.$emit('gogo');//确认按钮事件
                        //改变父组件传来的show变量的值,让弹窗隐藏
                        this.$emit('input',false)
                    },
                    cancelFn(){
                        this.$emit('nono');//取消按钮事件
                        //改变父组件传来的show变量的值,让弹窗隐藏
                        this.$emit('input',false)
                    }
                }
            });
            /*
                model框默认是隐藏的,当点击按钮或者触发一个其它事件的时候才会显示出来;
                逻辑:
                    1.在父级组件中定义一个show的变量,默认为false隐藏
                    2.当触发某个事件时,由子级组件改变这个变量来实现显示与隐藏;
                    那么这里涉及到了数据的双向绑定,可以使用.sync或v-model来实现;
            */
            let vm = new Vue({
                el:'#app',
                data:{
                  show:false //显示、隐藏的标识
                },
                methods:{
                    /*父级这里写要自定义的行为*/
                    okChangeFn(){
                        //这里可以写任何点击确认按钮后,要做的事情
                        //alert('测试,我是改变的确认按钮');
                    },
                    cancelChangeFn(){
                        //alert('测试,我是改变的取消按钮');
                    }
                },
                //创建一个自定义指令
                directives:{
                    transformBody:{
                        inserted(el){
                            //将元素插入到body中
                            document.body.appendChild(el)
                        }
                    }
                }
            });
        </script>
    </body>
</html>

生命周期函数

一个组件从开始到最后消亡所经历的各种状态,就是一个组件的生命周期。Vue在执行过程中会自动调用生命周期钩子函数,只需要提供这些钩子函数即可钩子函数的名称都是Vue中规定好的;

  • 创建阶段:
    beforeCreate:数据劫持之前被调用,无法访问methods,data,computed上的方法或数据;
    created:实例已经创建完成之后被调用。但挂载阶段还没开始,$el 属性目前不可见。常用于ajax发送请求获取数据;
  • 挂载阶段:
    beforeMount:在挂载开始之前被调用;
    mountedvue实例已经挂载到页面中,可以获取到el中的DOM元素,进行DOM操作;
  • 更新阶段:
    beforeUpdate:更新数据之前调用;
    updated:组件 DOM 已经更新;
  • 销毁阶段:
    beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用;
    destroyedVue实例销毁后调用;

小练习

一.数据驱动的选项卡小练习:

/*
    知识要点:
    1.控制class:
      v-bind:class="{class名字:表达式}",根据表达式的布尔值,决定这个元素是否要添加这个class,true 添加,false不添加;
     2.控制style:
       v-bind:style="{display:这里可以写三元运算}" ; 
      当然在控制style中可以同时添加多个属性,使用逗号隔开,带单位的正常加上单位;
说明:
      这个案例主要通过创建变量itemIndex,在按钮点击时,改变itemIndex的值。
      通过改变itemIndex的值来判断按钮与选项块的显示与隐藏, 从而实现数据驱动选项卡;
*/
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>选项卡4</title>
    <style>
        #app div{width:200px;height:100px;background: #f00;}
        .cur{background:#ff0;}
        .fn-hide{display: block}
    </style>
</head>
<body>
    <div id="app">
        <!--当前点击的index等于itemIndex就加上cur这个class-->
        <button 
          v-for="item,index in data" 
          :class="{cur:itemIndex===index}" 
          @click="itemFn(index)">{{item.title}}
        </button>
        <!--判断当前index等于itemIndex,display就为block,否则就是none-->
        <div v-for="item,index in data" 
         :style="{display:itemIndex===index ? 'block' : 'none'}">
            <p v-for="item,index in item.newListS">{{item.list}}</p>
        </div>
    </div>
<script src="../vue.js"></script>
<script>
    //准备好要显示的数据
    let data = [
        {title:'新闻',newListS:[{list:'新闻1'},{list:'新闻2'},{list:'新闻3'}]},
        {title:'娱乐',newListS:[{list:'娱乐1'},{list:'娱乐2'},{list:'娱乐3'}]},
        {title:'体育',newListS:[{list:'体育1'},{list:'体育2'},{list:'体育3'}]}
    ];
    let vm = new Vue({
        el:'#app',
        data:{
            data,
            itemIndex:0 //用于储存当前按钮的下标
        },
        methods:{
            itemFn(index){
                //在按钮点击时,将当前传过来的下标值,赋值给itemIndex变量
                this.itemIndex = index;
            }
        }
    });
</script>
</body>
</html>

二.好友列表小练习

  //好友列表大致思路和选项卡是相同的,不同的是里面会有列表收回的交互;
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>好友列表</title>
    <style>
        body,div,h2,ul,li{padding:0;margin:0;list-style: none}
        [v-cloak]{display: none}
        #app {width:200px; margin: 100px auto 0}
        #app div{margin-bottom: 3px;}
        h2{text-align:center;background: #ff6700;color:#fff;}
        h2:hover{cursor: pointer}
        ul{background: #96c6d7;text-align: center;display: none;}
        ul li{height:30px;}
        .cur{background:#c6d796}
    </style>
</head>
<body>
    <div id="app" v-cloak>
        <div v-for="titleItem,index in nameData">
            <h2 @click="titleItemFn(index)"
                :class="{cur:showIndex===index}">{{titleItem.title}}</h2>
            <!--判断ul是否显示-->
            <ul :style="{display:showIndex===index ? 'block' : 'none'}">
                <li v-for="nameItem,index2 in titleItem.qqNameS">{{nameItem.name}}</li>
            </ul>
        </div>
    </div>
    <script src="../../vue.js"></script>
    <script>
        let data =[
            {title:'我的好友',qqNameS:[{name:'好友1'},{name:'好友2'},{name:'好友3'}]},
            {title:'我的家人',qqNameS:[{name:'家人1'},{name:'家人2'},{name:'家人3'}]},
            {title:'黑名单',qqNameS:[{name:'黑名单1'},{name:'黑名单2'},{name:'黑名单3'}]}
        ];
        let vm = new Vue({
            el:'#app',
            data:{
                nameData:data,
                showIndex:-1 //为-1表示ul不显示出来
            },
            methods:{
                titleItemFn(i){
                    //判断如果点击的元素下标与showIndex相等,那就表示下拉列表是已经显示出来的。
                    if(this.showIndex === i){
                        //这时就需要把showIndex设置为-1,这样下拉列表就会隐藏回去
                        this.showIndex = -1;
                    }else{
                        //否则就是没显示出来的,就将当前点击的元素下标,赋值给showIndex变量
                        this.showIndex = i;
                    }
                }
            }
        });
    </script>
</body>
</html>

三.音乐全选与单选小练习

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
        <style>
            body,ul,li {margin: 0;padding: 0;list-style: none;}
            .wrap{    width: 500px;
                margin: 113px auto 0;
                border: 10px solid #f00;}
            .list-head li div {margin: 0;}
            .list-head li {padding: 0 20px;}
            .list li span {height: 100%;font: 14px/36px "微软雅黑";flex: auto;text-align: center;}
            .list li div, .select .selectAll {float: left;height: 12px;position: relative;top: 11px;margin-right: 20px;font: 14px/12px arial;cursor: pointer;}
            .list li {height: 36px;padding: 0 30px;box-sizing: border-box;color: #000000;display: flex;flex-direction: row;}
            .list{    background: #ff7917;}
            .list-body li:nth-child(even) {background: yellow;}
            .list-body li:nth-child(odd) {background: #fff;}
            .list-body li:hover {background: #0f0;}
            .list-body li.checkedColor {background: #0f0;}
        </style>
        <script src="../../vue.js"></script>
    </head>
    <body>
        <div class="wrap" id="app">
            <div class="baidu">
                <ul class="list list-head">
                    <li>
                        <div>
                            <input type="checkbox" v-model="isCheckedAll"/>全选
                        </div>
                        <span>歌单</span>
                        <span>歌手</span>
                        <span>专辑</span>
                    </li>
                </ul>
                <ul class="list list-body">
                    <!--checkedColor 选中样式-->
                    <li v-for="item,index in nameData">
                        <div><input type="checkbox" v-model="item.checked"></div>
                        <span>{{item.name}}</span>
                        <span>{{item.song}}</span>
                        <span>{{item.album}}</span>
                    </li>
                </ul>
                <div class="select">
                    <span class="selectAll">
                        <span>统计:</span>
                    </span>
                    <div class="others">
                        <span><em></em>歌手有:{{nameData.length}}位</span>
                        <span><em></em>专辑有{{albums}}张</span>
                    </div>
                </div>
            </div>
        </div>
        <script>
            //数据中的checked用于标识是否选中
            let data = [
                {
                    id:Date.now()+Math.random(),
                    name: '萧亚轩',
                    song: '代言人',
                    checked: true,
                    album: 10
                },
                {
                    id:Date.now()+Math.random(),
                    name: '蔡依林',
                    song: '说爱你',
                    checked: false,
                    album: 10
                },
                {
                    id:Date.now()+Math.random(),
                    name: '王菲',
                    song: '红豆',
                    checked: false,
                    album: 10
                },
                {
                    id:Date.now()+Math.random(),
                    name: '周杰伦',
                    song: '等你下课',
                    checked: false,
                    album: 10
                }
            ];
            let vm = new Vue({
                el:'#app',
                data:{
                    nameData:data
                },
                computed:{
                    //isCheckedAll:是用来控制全选按钮的,所以需要有setter
                    isCheckedAll:{
                        get(){
                            //过滤所有checked值为true,并且为true的长度要与数据长度相等,就表示全部都选中了
                            // 简便写法
                            // return this.nameData.filter( item => item.checked ).length === this.nameData.length
                            return this.nameData.filter(item =>{
                                return item.checked===true
                            }).length === this.nameData.length;
                        },
                        set(newValue){
                            //set中的newValue是当前属性的值
                            //把这个值赋值给checked属性,就可以控制全选了。
                            return this.nameData.forEach(item=>item.checked = newValue)
                        }
                    },
                    albums(){//专辑总数
                        //这是传统的写法
                /*let num = 0;
                this.nameData.forEach(item=>{
                    num +=item.album;
                });
                return num;*/
                //下面使用reduce方法来写
                return this.nameData.reduce((item1,item2)=>{
                        return {
                            n:item1.n + item2.album
                        }
                },{n:0}).n;
                /*
                    使用说明:
                    reduce传一个函数参数,可以说是一个累加器,通过str1与str2相加,可以得到累计的数

                    参数说明:
                    reduce,会传入两个参数,第一个参数是函数,函数中有两个参数,分别是初始值str1,从数组中拿的第一个值str2;
                    第二个参数(n),是设置的初始值,这里输入几就是从几开始;初始值可以写成一个对象格式,也可以是一个数字;
                    返回的是计算后的数;

                    语法:
                        //普通数字写法
                        let arr = [1,2,3,4]
                        arr.reduce(function(str1,str2){
                            return str1+str2
                        },n);
                        //数组中是对象格式的写法
                        var o1 = [{n:1,m:2},{n:2,m:3}];
                        o1.reduce(function(str1,str2){
                             return { //这里可以返回一个对象
                                 n: item1.n + item2.n,
                                 m: item1.m + item2.m,
                             }
                        },{n:0,m:1});//这里的初始值参数也可以是一个对象
                */
                    }
                }
            });
        </script>
    </body>
</html>

四.商品筛选小练习

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>商品筛选</title>
    <style type="text/css">
        body {
            font-size: 14px;
            font-family: "Lantinghei SC Extralight",Arial;
        }
        ul {
            padding: 0;
            margin: 0;
            list-style: none;
        }
        a {
            text-decoration: none;
        }
        img {
            vertical-align: top;
        }
        #wrap {
            width: 788px;
            height: 260px;
            margin: 100px auto;
            padding: 50px 120px 175px;
            background: #c6d796;
        }
        #section {
            width: 788px;
            height: 260px;
            -moz-box-shadow: 0 0 4px rgba(0,0,0,.2);
            box-shadow: 0 0 4px rgba(0,0,0,.2);
        }
        #choose {
            width: 788px;
            height: 50px;
            margin: 0 auto;
            background: #a6d796;
            line-height: 50px;
            text-indent: 21px;
        }
        #type {
            height: 210px;
            background: #718c00;
            padding: 17px 0 16px 28px;
        }
        #type li {
            height: 44px;
            color: #4d4d4d;
            line-height: 44px;
        }
        #type a {
            margin: 0 12px 0 11px;
            color: #000;
        }
        #choose mark {
            position: relative;
            display: inline-block;
            height: 24px;
            line-height: 24px;
            border: 1px solid #28a5c4;
            color: #28a5c4;
            margin: 12px 5px 0;
            background: none;
            padding: 0 30px 0 6px;
            text-indent: 0;
        }
        #choose mark a {
            position: absolute;
            top: 3px;
            right: 2px;
            display: inline-block;
            width: 18px;
            height: 18px;
            background: #28a5c4;
            color: #fff;
            line-height: 18px;
            font-size: 16px;
            text-align: center;
        }
        .active {
            color:#0000ff !important
        }

        [v-cloak]{
            display: none;
        }
    </style>
</head>
<body>
<!--
    本页功能描述:
    1.选择手机商品的4种参数,并显示到'你的选择'中,并有选中后的样式;
    2.每种品牌手机的参数,只能选择一个(4行参数分别是单选);
    3.在'你的选择'中显示的信息,必须要按照品牌--网络这样的顺序,不可以改变顺序;
    4.即使删除其中的参数,也必须要按照要求3中的顺序;
-->
    <div id="wrap" v-cloak>
        <h1>随便调随便选,不要钱!!!</h1>
        <section id="section">
            <nav id="choose">
            你的选择:
                <!--循环goods中的数据-->
             <mark v-for="goodsItem,key in goods">{{goodsItem}}
                 <!--delGoodFn():删除选中商品-->
                 <a href="javascript:;" @click="delGoodFn(key)">x</a>
             </mark>
            </nav>
            <ul id="type">
                <!--循环所有title的数据-->
                <li v-for="listItem,listIndex in listData">
                    <span>{{listItem.title}}</span>
                    <!--
                        对每一个商品的操作说明:
                        1.循环names的数据;
                        2.选中商品时,需要加class为active;
                        3.给每一个商品加点击事件
                    -->
                    <a href="javascript:;"
                       v-for="namesItem,namesIndex in listItem.names"
                       :class="{active:listItem.titleIndex === namesIndex}"
                       @click="namesItemFn(listItem,namesIndex,namesItem,listIndex)"
                    >
                        {{namesItem}}
                    </a>
                </li>
            </ul>
        </section>
    </div>
    <script src="js/vue.js"></script>
    <script>
        //准备好的商品数据
        let titleData = [
            {
                title: '品牌',
                names: ["苹果", "小米", "锤子", "魅族", "华为", "三星", "OPPO", "vivo", "乐视", "360", "中兴", "索尼"]
            },
            {
                title: '尺寸',
                names: ["3.0英寸以下", "3.0-3.9英寸", "4.0-4.5英寸", "4.6-4.9英寸", "5.0-5.5英寸", "6.0英寸以上"]
            },
            {
                title: '系统',
                names: ["安卓 ( Android )", "苹果 ( IOS )", "微软 ( WindowsPhone )", "无", "其他"]
            },
            {
                title: '网络',
                names: ["联通3G", "双卡单4G", "双卡双4G", "联通4G", "电信4G", "移动4G"]
            }
        ];
        let vm = new Vue({
            el:'#wrap',
            data:{
                listData:titleData, //挂载商品数据
                goods:{} //这里使用对象形式储存选择后的商品,显示到mark标签中。格式为{0:val,1:val...}
            },
            methods:{
                /*
                    【添加商品方法】
                    参数说明:
                 listData:当前点击的商品数据对象
                 index:当前类型商品的索引
                 namesItem:当前点击的商品
                 listIndex:每一行的下标
                 */
                namesItemFn(listData,index,namesItem,listIndex){
                    //给每个商品对象中添加一个titleIndex属性,值是对应的索引;用于记录选中商品的位置
                    this.$set(listData,'titleIndex',index);
                    //给goods对象数据,格式为:key值放当前所有行的下标,value放当前点击的商品数据
                    //key值放当前商品所在行下标,目的是一行商品只能选一个,不可以多选,这样可以通过key的唯一性来实现单选
                    this.$set(this.goods,listIndex,namesItem);
                },
                /*
                    【删除商品方法】
                     key:当前商品下标,因为这里的goods存的key,就是当前商品的下标,所以这里直接使用Key值即可
                */
                delGoodFn(key){
                    //根据当前删除商品的下标,删除goods对象中对应的商品
                    this.$delete(this.goods,key);
                    //这行代码处理当删除商品后,选中商品的样式要去掉
                    this.$set(this.listData[key],'titleIndex',-1);
                }
            }
        });
    </script>
</body>
</html>

五.todosList小练习

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

<head>
    <meta charset="UTF-8">
    <title>todos</title>
    <link rel="stylesheet" type="text/css" href="css/base.css">
    <link rel="stylesheet" type="text/css" href="css/index.css">
    <style>
        [v-cloak]{display: none}
    </style>
</head>

<body>
<section class="todoapp" v-cloak>
    <div>
        <header class="header" >
            <h1>todos</h1>
            <input class="new-todo" placeholder="请输入内容"
                @keyUp.13="addTodo()"
                   v-model="title"
            />
        </header>
        <section class="main" v-show="listData.length">
            <!--全选按钮-->
            <input class="toggle-all" type="checkbox" v-model="isCheckedAll"/>
            <ul class="todo-list">
                <!--
                 completed:选中的样式
                 editing:编辑中的样式
                 -->
                <li :class="{completed:item.checked,editing:editIndex===index}"
                        v-for="item,index in filterList">
                    <div class="view">
                        <!--v-model="item.checked" 直接通过v-model绑定数据是否为选中-->
                        <input class="toggle" type="checkbox" v-model="item.checked"/>
                        <!--编辑todo-->
                        <label @dblclick="editTodo(index)">{{item.title}}</label>
                        <!--删除按钮-->
                        <button class="destroy" @click="delTodo(index)"></button>
                    </div>
                    <!--
                        编辑时要显示的输入框,需要将当前数据再显示到这个框中
                        ref="editDom" 给这个input加了一个名字,方法dom查找,这个是vue中提供的方法
                        @blur="editEnd(item)" :失去焦点完成编辑
                        @keyup.13="editEnd(item)":按回车也可以完成编辑
                        @keyup.esc="editCancel(item)":取消编辑,把对应的数据传过去
                    -->
                    <input class="edit" v-model="item.title" ref="editInput"
                           @blur="editEnd(item)" @keyup.13="editEnd(item)"
                           @keyup.esc="editCancel(item)"
                    />
                </li>
            </ul>
        </section>
        <footer class="footer" v-show="listData.length">
                <span class="todo-count">
                    <strong>{{checkedLen}}</strong>
                    <span>条未选中</span>
                </span>
            <ul class="filters">
                <!--判断hash值是否与数据中的hash一样-->
                <li><a href="#/all" :class="{selected:hash==='all'}">All</a></li>
                <li><a href="#/active" :class="{selected:hash==='active'}">Active</a></li>
                <li><a href="#/completed" :class="{selected:hash==='completed'}">Completed</a></li>
            </ul>
        </footer>
    </div>
</section>
</body>
<script src="../vue.js"></script>
<script>
    /*
         1. 增删改查数据,要使用localStorage做数据持久化
         2. 根据hash不同,过滤渲染的数据
         a. 全部任务
         b. 完成的任务
         c. 未完成的任务
         3. 自己设计数据结构
     */

    //页面一加载先获取存储的todoData,没数据就返回一个空数组
    let data = JSON.parse(localStorage.getItem('todoData')) || [];

    //将三种hash值放到对象中过滤
    let filterListData = {
        all(list){//所有状态
            return list;
        },
        active(list){//未选中
            return list.filter(item => !item.checked);
        },
        completed(list){//已选中
            return list.filter(item => item.checked);
        }
    };
    let vm = new Vue({
        el:".todoapp",
        data: {
            listData: data,
            title:'',//内容变量
            editIndex:-1, //编辑数据的下标变量
            beforeTitle:'', //编辑前的数据
            hash:'all' //默认状态
        },
        computed:{
            //过滤listData数据
            filterList(){
                return filterListData[this.hash](this.listData);
            },
            //过渡checked是否都是true,都是true,就自动显示全选
            isCheckedAll:{
                get(){
                    return this.listData.every(item => item.checked===true)
                },
                set(newValue){
                    this.listData.forEach((item)=>{
                        item.checked = newValue;
                    })
                }
            },
            //未选中条数
            checkedLen(){
                return this.listData.filter((item)=>{
                    return item.checked === false
                }).length;
            }
        },
        //监控listData中的数据,只要有改变就创建localStorage
        watch:{
            //深度监控listData数据
            listData:{
                handler(){
                    //当listData对象中数据发生改变时,就创建本地存储
                    return localStorage.setItem('todoData',JSON.stringify(this.listData));
                },
                deep:true
            }
        },
        //methods上面的方法,也会挂在实例上
        methods:{
            addTodo(){//添加方法
                if(this.title.trim() === '') return;
                this.listData.push({
                    title:this.title,
                    id:Date.now()+Math.random(),
                    checked:false
                });
                //添加后清空输入框
                this.title=''
            },
            delTodo(index){//删除方法
                this.listData.splice(index,1)
            },
            editTodo(index){//编辑方法
                /*
                    编辑逻辑:
                    1.编辑方法是在双击某条数据时,会显示输入框,这个是通过class样式来控制的,
                    所以,这里使用的方法是,通过变量,保存当前点击数据的下标,来判断是否添加输入框的class名;
                    2.在双击后,输入框需要获取焦点;
                    编辑遇到的问题:
                    直接在方法里写获取焦点是没有效果的,因为编辑的dom是动态判断显示的,页面加载后,编辑的输入框
                    并没有更新完,就直接获取了,所以这个时候是找不到输入框的;
                    这里处理的办法使用的是,Vue.nextTick();dom更新后才触发的方法;
                */
                //编辑的时候,保存一下编辑前的数据
                this.beforeTitle = this.listData[index].title;
                //重新渲染视图
                this.editIndex = index;
                //编辑输入框获取焦点
                this.$nextTick(()=>{
                    //获取输入框
                    //这里通过this.$refs.(ref的名字),可以获取到有ref属性的元素,具体使用方法和注意事项,请查API
                    this.$refs.editInput[index].focus();
                })
            },
            //完成编辑
            editEnd(item){
                //判断是否为空,为空就删除数据
                if( !item.title.trim()){
                    /*
                        如果是空,就调删除方法,删除数据,这里传的参数是一个下标,
                        所以这里使用了findIndex()es6新方法,来找到对应的下标;
                    */
                    this.delTodo(this.listData.findIndex((dataItem) =>{
                        dataItem === item;
                    }));
                }
                //通过改变editIndex变量为-1,去掉编辑样式
                this.editIndex = -1;
                //编辑完后,清空编辑前的数据
                this.beforeTitle =''
            },
            //取消编辑
            editCancel(item){
                //通过改变editIndex变量为-1,去掉编辑样式
                this.editIndex = -1;
                //把编辑前保存的数据,赋值给当前数据
                item.title = this.beforeTitle;
            }
        }
    });

    /*
        通过获取hash值的方法,来实现,all,active,completed 三种类型的选项
        大概思路:
            1.页面加载后,先拿到hash值;
            2.判断hash值的三种情况(存在的hash,hash为空,不存在的hash);
            3.逻辑,hash为这和不存在的hash,都默认是all
    */

    //改变hash值,触发的事件
    window.onhashchange=function(){
        //hash值
        let hash = window.location.hash;
        //判断三种情况
        if(hash){//hash值存在
            //获取hash值
            hash =hash.slice(2);
            //判断hash值是否为空,如果为空,就返回all
            hash = filterListData[hash] ? hash : 'all';
        }else{//不存在
            hash = 'all';
        }
        // 改变实例下的过滤条件
        vm.hash = hash;
    };
    //先执行hash事件
    window.onhashchange();
</script>

</html>

注:以上笔记不是全部内容,会不定期更新,增加一些小练习的代码;

推荐阅读更多精彩内容