Vue2学习

视频教程来自尚硅谷:尚硅谷Vue2.0+Vue3.0全套教程丨vuejs从入门到精通_哔哩哔哩_bilibili,vue2文档:介绍 — Vue.js

1. vue基础知识和原理

1.1 初识vue

  • 想让Vue工作,就必须创建一个Vue实例,且要传入一个配置对象

  • app容器里的代码依然符合html规范,只不过混入了一些特殊的Vue语法

  • app容器里的代码被称为【Vue模板】

  • Vue实例和容器是一一对应的,不能一对多和多对一

  • 真实开发中只有一个Vue实例,并且会配合着组件一起使用

  • 是Vue的语法:插值表达式,可以读取到data中的所有属性
  • 一旦data中的数据发生改变,那么页面中用到该数据的地方也会自动更新(Vue实现的响应式)
    代码:这里是直接引入开发版本的vue.js,可以去官网下载

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>01</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<!--
容器和vue实例是一对一的,不能一对多和多对一
-->
<!--准备好一个容器-->
<div id="app">
    <h1>Hello,{{name}} {{op}} {{wifu}}</h1>
</div>
<script type="text/javascript">
    <!--关闭生产环境提示-->
    Vue.config.productionTip = false;
    new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
            name: 'ZQG',
            op: 'BC',
            wifu: 'YM'
        }
    });
</script>
</body>
</html>

1.2 模板语法

Vue模板语法有2大类:

  • 插值语法:

​ 功能:用于解析标签体内容

​ 写法:,xxx是js表达式,且可以直接读取到data中的所有属性

  • 指令语法:

​ 功能:用于解析标签(包括:标签属性、标签体内容、绑定事件…)

​ 举例:v-bind:href=“xxx” 或 简写为 :href=“xxx”,xxx同样要写js表达式,且可以直接读取到data中的所有属性

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>模板语法</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<div id="app">
    <h1>插值语法</h1>
    <h2>Hello,{{name}}</h2>
    <hr/>
    <h1>指令语法</h1>
    <a v-bind:href="link.url">去{{link.title}}</a>
    <!-- v-bind简写 -->
    <br/>
    <a :href="link.url">去{{link.title}}2</a>
</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
            name: 'ZQG',
            link:{
                title: '百度',
                url: 'https://www.baidu.com'
            }

        }
    });

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

1.3 数据绑定

Vue中有2种数据绑定的方式:

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

注意:

1.双向绑定一般都应用在表单类元素上(如:input、select等)

2.v-model:value 可以简写为 v-model,因为v-model默认收集的就是value值

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>模板语法</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<div id="app">
    单向数据绑定:<input type="text" v-bind:value="name"><br/>
    双向数据绑定:<input type="text" v-model:value="name"><br/>

    <!--    简写-->
    单向数据绑定2:<input type="text" :value="name"><br/>
    <!-- 简写 v-model:value 可以简写为 v-model,因为v-model默认收集的就是value值-->
    双向数据绑定2:<input type="text" v-model="name"><br/>
    
</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
            name: 'ZQG'
        }
    });

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

1.4 el与data的两种写法

el有2种写法

  • new Vue时候配置el属性
  • 先创建Vue实例,随后再通过vm.$mount(‘#root’)指定el的值

data有2种写法

  • 对象式

  • 函数式

    注意:在组件中,data必须使用函数式。一个重要原则:由Vue管理的函数不能是箭头函数,因为这样写this就不再是Vue实例了。

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>el和data的两种写法</title>
        <script type="text/javascript" src="js/vue.js"></script>
    </head>
    <body>
    
    <!--准备好一个容器-->
    <div id="app">
        <h1>Hello,{{name}} </h1>
    </div>
    <script type="text/javascript">
        Vue.config.productionTip = false;
        //el的两种写法
        /*
        const vm = new Vue({
           // el: '#app',//第一种写法
            data: { //数据,给el 容器中的元素提供数据
                name: 'ZQG'
            }
        });
        console.log(vm);
        vm.$mount('#app') //第二种写法,挂载容器
    */
        // data的两种写法
        const vm2 = new Vue({
            el: '#app',
            // data: { //第一种写法,对象
            //     name: 'ZQG'
            // }
    
            // 第二种写法,函数式
            // data: function () {}
            data() {
                console.log(this)
                return {
                    name: 'ZQG'
                }
            }
        });
    </script>
    </body>
    </html>
    

1.5 MVVM模型

  • M:模型(Model) :data中的数据

  • V:视图(View) :模板代码

  • VM:视图模型(ViewModel):Vue实例

mvvm

data中的所有属性都在vm中。

vm的所有属性,包括Vue原型上所有属性,Vue模板都可以直接使用。

1.6 数据代理

通过一个对象代理对另一个对象中的属性的操作(读/写)。

主要通过js方法:Object.defineProperty()实现的,需要先去了解Object.defineProperty()相关知识,属性标志,属性描述符,getter,setter.

简单介绍一下:

属性标志:

对象属性(properties),除 value 外,还有三个特殊的特性(attributes),也就是所谓的“标志”

writable — 如果为 true,则值可以被修改,否则它是只可读的,默认为false
enumerable — 如果为 true,则表示是可以遍历的,可以在for… .in Object.keys()中遍历出来,默认为false
configurable — 如果为 true,则此属性可以被删除,这些特性也可以被修改,否则不可以,默认为false

<script type="text/javascript">
    let num = 18;
    let person = {
        name: 'ZQG',
        //age: 18,
        sex: '男'
    };
    Object.defineProperty(person, 'age',{
         value: num,
        // enumerable: true, // 是否可枚举,默认为false
        // configurable: true,// 是否可以删除,默认为false
        // writable: true// 能否修改,默认为false
    })
  console.log(person);
</script>

如图:

image-20250123165409650

如果不将相应的配置设置为true,修改不起作用,删除也返回false,也无法枚举。

使用getter和setter时:

<script type="text/javascript">
    let num = 18;
    let person = {
        name: 'ZQG',
        //age: 18,
        sex: '男'
    };
    Object.defineProperty(person, 'age',{
         //value: num,
        // enumerable: true, // 是否可枚举,默认为false
        // configurable: true,// 是否可以删除,默认为false
        // writable: true// 能否修改,默认为false
        get() {
            console.log('读取age属性');//person.age 起作用
            return num;
        },
        set(value) {
            console.log('设置age属性,值是:', value);//person.age = 20 起作用
            num = value;
        }
    })
  console.log(person);
</script>

image-20250123170003588

每次访问person.age时,会触发getter函数,返回num值;当设置person.age = xx值时,会把值设置给num,这样再次读取会返回最新的num值,(直接设置num值,获取age值也是返回设置后的num值)这样就实现了数据代理。

Vue中的数据代理

Vue中的数据代理:通过vm对象来代理data对象中属性的操作(读/写)
Vue中数据代理的好处:更加方便的操作data中的数据
基本原理:
通过Object.defineProperty()把data对象中所有属性添加到vm上。
为每一个添加到vm上的属性,都指定一个getter/setter。
在getter/setter内部去操作(读/写)data中对应的属性。

我们代码中的data实际和Vue实例中的_data是相等的,所以取值,Vue为了代码写的方便,通过数据代理将_data的属性值,直接放在vm中,所以直接取值用就可以了。

验证代码:为了验证data和我们代码中的数据(person)是一样的,所以把person定义在外面,这样可以直接可以_data==person作比较

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>数据代理</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<div id="app">
    <h1>Hello,{{name}},{{age}},{{sex}}</h1>
</div>
<script type="text/javascript">
    let person = {
        name: 'ZQG',
        age: 18,
        sex: '男'
    };
  const vm = new Vue({
      el: '#app',
      data: person
  });
  console.log(vm)
</script>
</body>
</html>

image-20250123175247017

image-20250123175508777

image-20250123175744591

1.7 事件处理

事件的基本使用:

  • 使用v-on:xxx 或 @xxx 绑定事件,其中xxx是事件名
  • 事件的回调需要配置在methods对象中,最终会在vm上
  • methods中配置的函数不要用箭头函数,这会导致this不再是vm了
  • methods中配置的函数,都是被Vue所管理的函数,this的指向是vm 或 组件实例对象
  • @click=”show” 和@click=”show($event)”,效果一致,但是后者可以传参
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>事件处理</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<div id="app">
    <h1>Hello,{{name}},欢迎来到{{address}}</h1>
    <button v-on:click="showInfo">点我</button>
    <button @click="showInfo">点我2(不传参)</button>
    <button @click="showInfo3($event,666)">点我3(传参)</button>
</div>
<script type="text/javascript">
    Vue.config.productionTip = false;
    new Vue({
        el: '#app',
        data: {
            name: 'ZQG',
            address: '支配剧场'
        },
        methods: {
            showInfo: function (event) {
                alert('Hello,Vue!');
                console.log(event);// event对象
                console.log(this);// this指向当前Vue实例
            },
            showInfo3(event, num) {
                console.log(event.target.innerText);// event对象
                //console.log(this);// this指向当前Vue实例
                alert(num)
            }
        }
    });
</script>
</body>
</html>

Vue中的事件修饰符

  • prevent:阻止默认事件(常用)
  • stop:阻止事件冒泡(常用)
  • once:事件只触发一次(常用)
  • capture: 使用事件的捕获模式
  • self: 只有event.target是当前的操作元素时才触发事件
  • passive: 事件的默认行为立即执行,无需等待事件回调完毕
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>事件处理</title>
    <script type="text/javascript" src="js/vue.js"></script>
    <style type="text/css">
        * {
            margin-top: 10px;
        }
        .dd{
            width: 100px;
            height: 80px;
            background-color: red;
        }
        .aa{
            width: 100px;
            height: 60px;
            background-color: skyblue;
        }
        .bb{
            width: 100px;
            height: 20px;
            background-color: green;
        }
        .ll{
            width: 150px;
            height: 100px;
            background-color: orange;
            overflow: auto;
        }
        li{
            height: 50px;
        }
    </style>
</head>
<body>
<div id="app">
    <h1>Hello,{{name}},欢迎来到{{address}}</h1>
    <!--    阻止事件-->
    <a href="https://www.baidu.com" @click.prevent="showInfo">点我跳转</a>

    <div class="dd" @click="showInfo">
        <!--   阻止事件冒泡     -->
       <button @click.stop="showInfo">点我</button>
         <!--   修饰符可以连续写-->
        <a href="https://www.baidu.com" @click.prevent.stop="showInfo">点我跳转</a>
    </div>
    <!--    事件只触发一次-->
    <button @click.once="showInfo">点我</button>
    <!-- 原本: 事件捕获=》事件冒泡(调用),所以打印2再打印1. 使用capture后:在捕获的时候就会调用,此时先打印1后打印2 -->
    <div class="aa" @click.capture="showMsg(1)">
        div1
        <div class="bb" @click="showMsg(2)">
            div2
        </div>
    </div>

    <div class="dd" @click.self="showInfo">
        <!--   只有event.target是当前的操作元素时才触发事件     -->
        <button @click="showInfo">点我</button>
    </div>

<!--    @scroll 滚动条,@wheel 鼠标滚轮-->
    <ul @wheel.passive="work" class="ll">
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
    </ul>

</div>
<script type="text/javascript">
    Vue.config.productionTip = false;
    new Vue({
        el: '#app',
        data: {
            name: 'ZQG',
            address: '支配剧场'
        },
        methods: {
            showInfo: function (event) {
                alert('Hello,Vue!');
                console.log(event);// event对象
                console.log(this);// this指向当前Vue实例
            },
            showMsg: function (msg) {
                alert(msg);
            },
            work: function (event) {
                for (let i = 0; i < 10000; i++) {
                    console.log('#');
                }
            }
        }
    });
</script>
</body>
</html>

1.8 键盘事件

键盘事件语法糖:@keydown,@keyup

1.Vue中常用的按键别名:

  • 回车 => enter
  • 删除 => delete(捕获删除和退格键)
  • 退出 => esc
  • 空格 => space
  • 换行 => tab (特殊,必须配合keydown去使用)
  • 上 => up
  • 下 =>down
  • 左 =>left
  • 右 =>right

2.Vue未提供别名的键,可以使用按键的原始key值去绑定,但是要注意转为kebab-case(短横线命名)

3.系统修饰键(用法特殊):ctrl,alt,shift,meta

​ 3.1配合keyup使用:按下修饰键的同时,再按下其他键,随后释放,事件才会触发

​ 3.2配合keydown使用:正常触发

4.可以使用keyCode去指定具体的按键(不推荐)

5.Vue.config.keyCode.自定义键名=键码,可以定制按键别名

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>键盘事件</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<div id="app">
    <h1>Hello,{{name}}</h1>
    <input type="text" placeholder="按下回车提示输入" @keyup="showInfo">
    <input type="text" placeholder="按下tab提示输入" @keydown.tab="showInfo">
        <!--  连写 ctrl + y -->
    <input type="text" placeholder="按下ctrl提示输入" @keyup.ctrl.y="showInfo">
</div>
<script type="text/javascript">
    Vue.config.productionTip = false;
    Vue.config.keyCodes.dsb = 13;//自定义按键,dsb替换成enter @keyup.dsb="",不推荐使用
    let person = {
        name: 'ZQG'
    };
  new Vue({
      el: '#app',
      data: person,
      methods: {
          showInfo(e) {
              console.log(e.key,e.keyCode,e.target.value)
          }
      }
  });

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

1.9 计算属性

定义:要用的属性不存在,要通过已有属性计算得来。
原理:底层借助了Objcet.defineProperty方法提供的getter和setter
get函数什么时候执行?
(1).初次读取时会执行一次
(2).当依赖的数据发生改变时会被再次调用
优势:与methods实现相比,内部有缓存机制(复用),效率更高,调试方便
备注:
计算属性最终会出现在vm上,直接读取使用即可
如果计算属性要被修改,那必须写set函数去响应修改,且set中要引起计算时依赖的数据发生改变

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>姓名案例</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>

<!--准备好一个容器-->
<div id="app">
    姓: <input type="text" v-model="firstName"/><br>
    名:<input type="text" v-model="lastName"/><br>
    全名:<span>{{fullName}}</span><br>
    全名:<span>{{fullName2}}</span>
</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    const vm = new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
            firstName: 'ZQG',
            lastName: 'YM'
        },
        computed:{
            fullName:{
                //初次读取fullName时,get方法会调用一次
                // 所依赖的数据发生变化时,get方法会调用一次
                //其他情况,会读取缓存的数据
                get(){
                    console.log('get调用');
                    console.log(this);
                    return this.firstName + '-' + this.lastName;
                },
                //set方法不是必须写的,如果计算属性确定没有修改的场景,可以不写set
                // 当fullName被修改时,set方法会调用一次
                set(value){
                    const names = value.split('-');
                    this.firstName = names[0];
                    this.lastName = names[1];
                }
            },
            //简写,不包含set方法
            fullName2() {
                return this.firstName + '-' + this.lastName;
            }
        }


    });

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

1.10 监视属性

监视属性watch:

  • 当被监视的属性变化时, 回调函数自动调用, 进行相关操作
  • 监视的属性必须存在,才能进行监视
  • 监视的两种写法:
    • (1).new Vue时传入watch配置
    • (2).通过vm.$watch监视
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>天气案例</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<!--
容器和vue实例是一对一的,不能一对多和多对一
-->
<!--准备好一个容器-->
<div id="app">
    <h2>今天天气很{{info}}</h2>
    <button @click="changeWeather">切换天气</button>
</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    const vm = new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
            isHot: true,
        },
        computed: {
            info() {
                return this.isHot ? '炎热' : '凉爽';
            }
        },
        methods: {
            changeWeather() {
                this.isHot = !this.isHot;
            }

        },
        watch: {
            isHot: {
                // 监听isHot属性的变化
                handler(newValue, oldValue) {
                    console.log('isHot', newValue, oldValue);
                },
                immediate: true, //初始化时执行handler,默认false
                deep: true //深度监听
            },
            //简写,不需要immediate,deep时
            // 监听isHot属性的变化
            isHot(newValue, oldValue) {
                console.log('isHot', newValue, oldValue);
            }

            //info,计算属性也可以被监听
            // info: {
            //     // 监听info属性的变化
            //     handler(newValue, oldValue) {
            //         console.log('info', newValue, oldValue);
            //     },
            //     immediate: true, //初始化时执行handler,默认false
            //     deep: true //深度监听
            // }
        }
    });
    // 监听isHot属性的变化
   /* vm.$watch('isHot', {
        handler(newValue, oldValue) {
            console.log('isHot', newValue, oldValue);
        },
        immediate: true, //初始化时执行handler,默认false
        deep: true //深度监听
    });
  */
    //简写,不需要配置immediate,deep时
    vm.$watch('isHot',function (newValue, oldValue) {
        console.log('isHot改变了', newValue, oldValue);
    })
</script>
</body>
</html>

(1).Vue中的watch默认不监测对象内部值的改变(一层)
(2).配置deep:true可以监测对象内部值改变(多层)
备注:

(1).Vue自身可以监测对象内部值的改变,但Vue提供的watch默认不可以

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

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>天气案例-深度监视</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<!--
容器和vue实例是一对一的,不能一对多和多对一
-->
<!--准备好一个容器-->
<div id="app">
    <h2>今天天气很{{info}}</h2>
    <button @click="changeWeather">切换天气</button>
    <hr>
    <h2>a的值是{{num.a}}</h2>
    <button @click="num.a++">点我a+1</button>
    <hr>
    <h2>b的值是{{num.b}}</h2>
    <button @click="num.b++">点我b+1</button>
</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    const vm = new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
            isHot: true,
            num:{
                a:1,
                b:2
            }
        },
        computed: {
            info() {
                return this.isHot ? '炎热' : '凉爽';
            }
        },
        methods: {
            changeWeather() {
                this.isHot = !this.isHot;
            }

        },
        watch: {
            isHot: {
                // 监听isHot属性的变化
                handler(newValue, oldValue) {
                    console.log('isHot', newValue, oldValue);
                },
                immediate: true, //初始化时执行handler,默认false
                deep: true //深度监听
            },
            // 监听多级结构中某个属性,num.a属性的变化
            'num.a':{
                handler(newValue, oldValue) {
                    console.log('num.a改变了', newValue, oldValue);
                },
            },
            // 监听多级结构中所有属性的变化
            num:{
                handler(newValue, oldValue) {
                    console.log("num改变了");
                },
                deep: true//深度监听
            }
        }
    });
</script>
</body>
</html>

computed和watch之间的区别:

computed能完成的功能,watch都可以完成
watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作
两个重要的小原则:

1.所被Vue管理的函数,最好写成普通函数,这样this的指向才是vm 或 组件实例对象

2.所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数等、Promise的回调函数),最好写成箭头函数,这样this的指向才是vm 或 组件实例对象

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>姓名案例-watch写法</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<!--准备好一个容器-->
<div id="app">
    姓: <input type="text" v-model="firstName"/><br>
    名:<input type="text" v-model="lastName"/><br>
    全名:<span>{{fullName}}</span>
</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
            firstName: 'ZQG',
            lastName: 'YM',
            fullName: 'ZQG-YM'
        },
       watch:{
            firstName(newValue,oldValue){
                setTimeout(()=>{
                    this.fullName = newValue +'-'+ this.lastName;
                },1000)
            },
            lastName(newValue,oldValue){
                this.fullName = this.firstName +'-'+ newValue;
            }
       }
    });
</script>
</body>
</html>

1.11 绑定样式

class样式

写法::class=“xxx” xxx可以是字符串、对象、数。

所以分为三种写法,字符串写法,数组写法,对象写法

字符串写法

字符串写法适用于:类名不确定,要动态获取。

数组写法

数组写法适用于:要绑定多个样式,个数不确定,名字也不确定。

对象写法

对象写法适用于:要绑定多个样式,个数确定,名字也确定,但不确定用不用。

style样式

有两种写法,对象写法,数组写法.

:style = “{forntSize:xxx}” ,xxx时动态值

:style=”[a,b]” a,b是样式对象

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>绑定样式</title>
    <script type="text/javascript" src="js/vue.js"></script>
    <style>
        .basic{
            text-align: center;
            width: 400px;
            height: 100px;
            border: 1px solid black;
        }
        .happy{
            background-color: yellow;
            border: 4px solid red;
        }
        .sad{
            border: 4px solid blue;
            background-color: gray;
        }
        .normal{
            background-color: skyblue;
        }
        .zqg1{
            background-color: yellowgreen;
        }
        .zqg2{
            font-size: 30px;
            text-shadow:2px 2px 10px red;
        }
        .zqg3{
            border-radius: 20px;
        }

    </style>
</head>
<body>
<!--准备好一个容器-->
<div id="app">
<!--    字符串写法,适用于:类名不确定,要动态获取-->
    <div class="basic" :class="mood" @click="changeMood">{{name}}</div>
    <br/>
<!--   数组写法,适用于:要绑定多个样式,个数不确定,名字也不确定-->
    <div class="basic" :class="arr">{{name}}</div><br/>
<!--    对象写法适用于:要绑定多个样式,个数确定,名字也确定,但不确定用不用-->
    <div class="basic" :class="classObj">{{name}}</div><br/>

    <div class="basic" :style="styleObj">{{name}}</div><br/>

    <div class="basic" :style="styleArr">{{name}}</div><br/>

</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
            name: 'ZQG',
            mood:'normal',
            arr:['zqg1','zqg2','zqg3'],
            classObj:{
                zqg1:true,
                zqg2:false,
                zqg3:true
            },
            styleObj:{
                color:'red',
                fontSize:'35px',
                backgroundColor:'orange'
            },
            styleArr:[{color:'blue', fontSize: '45px'},{backgroundColor:'gray'}]
        },
        methods: {
            changeMood: function () {
                const arr = ['happy', 'sad','normal'];
                this.mood = arr[Math.floor(Math.random() * arr.length)];
            }
        }

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

1.12 条件渲染

v-if

写法:

(1).v-if=“表达式”

(2).v-else-if=“表达式”

(3).v-else=“表达式”

适用于:切换频率较低的场景

特点:不展示的DOM元素直接被移除

注意:v-if可以和:v-else-if、v-else一起使用,但要求结构不能被“打断”

v-show

  • 写法:v-show=“表达式”
  • 适用于:切换频率较高的场景
  • 特点:不展示的DOM元素未被移除,仅仅是使用样式隐藏掉(display:none)

备注:使用v-if的时,元素可能无法获取到,而使用v-show一定可以获取到;v-if 是实打实地改变dom元素,v-show 是隐藏或显示dom元素

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>条件渲染</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<!--准备好一个容器-->
<div id="app">
    <h2 v-show="condition">你好,{{name}}</h2>
    <br/>
    <h2 v-show="1===1">你好,{{name}}</h2>
    <hr>
    <h2 v-if="1===1">你好,{{name}}</h2>

    <hr>
    <h2>当前i的值是:{{i}}</h2>
    <button @click="i++">点我++</button>
    <div v-if ="i === 1">Angular</div>
    <div v-else-if ="i === 2 ">React</div>
    <div v-else-if="i === 3">Vue</div>
    <div v-else>666</div>

<!--  template只能和v-if配合使用  -->
    <template v-if="i === 4">
        <h2>zqg</h2>
        <h2>bc</h2>
        <h2>ym</h2>
    </template>

</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
            name: 'ZQG',
            condition: true,
            i: 0
        }
    });
</script>
</body>
</html>

1.13 列表渲染

v-for指令

  • 用于展示列表数据
  • 语法:v-for=“(item, index) in xxx” :key=“yyy”
  • 可遍历:数组、对象、字符串(用的很少)、指定次数(用的很少)
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>列表渲染</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<!--准备好一个容器-->
<div id="app">
    <h2>人员列表</h2>
 <ul>
     <li v-for="p in persons" :key="p.id">{{p.name}}-{{p.age}}</li>
     <hr/>
     <li v-for="(p,index) in persons" :key="index">{{p.name}}-{{p.age}}</li>
 </ul>
    <hr/>
    <h2>汽车信息</h2>
    <li v-for="(value,key,index) in car" :key="key">
        {{index}}-{{key}}-{{value}}
    </li>
    <hr/>
    <h2>字符串信息</h2>
    <li v-for="(value,index) in str" :key="index">
        {{index}}--{{value}}
    </li>
    <hr>
    <h2>遍历次数</h2>
    <li v-for="(value,index) in 5" :key="index">
        {{index}} -- {{value}}
    </li>

</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
           persons: [
               {id: '001', name: 'zqg', age: 18},
               {id: '002', name: 'bc',age: 19},
               {id: '003', name: 'ym',age: 20},
           ],
            car: {
                name: '奔驰',
                price: '1000000',
                color: '黑色'
            },
            str: 'hello world'
        }
    });
</script>
</body>
</html>

key的原理

可以先了解一下虚拟DOM和真实DOM,以及它们之间的差异:深入剖析:Vue核心之虚拟DOM使用 Vue 做项目也有两年时间了,对 Vue 的 api也用的比较得心应手了,虽然对 - 掘金

虚拟DOM中key的作用

key是虚拟DOM对象的标识,当数据发生变化时,Vue会根据【新数据】生成【新的虚拟DOM】, 随后Vue进行【新虚拟DOM】与【旧虚拟DOM】的差异比较,比较规则如下:

  1. 旧虚拟DOM中找到了与新虚拟DOM相同的key:

    ①.若虚拟DOM中内容没变, 直接使用之前的真实DOM!

    ②.若虚拟DOM中内容变了, 则生成新的真实DOM,随后替换掉页面中之前的真实DOM。

  2. 旧虚拟DOM中未找到与新虚拟DOM相同的key:

    创建新的真实DOM,随后渲染到到页面。

用index作为key可能会引发的问题:

  1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:

    会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低。

  2. 如果结构中还包含输入类的DOM:

    会产生错误DOM更新==>界面有问题。

开发中如何选择key:

  1. 最好使用每条数据的唯一标识作为key,如id,身份证号,手机号,学号等。
  2. 如果不存在对数据的逆序添加,逆序删除等破坏顺序操作,仅用于渲染列表展示,可以使用index作为key。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>列表渲染-key的原理</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<!--准备好一个容器-->
<div id="app">
    <h2>人员列表</h2>
    <button @click.once="add">添加一个老张</button>
 <ul>
     <li v-for="p in persons" :key="p.id">{{p.name}}-{{p.age}} <input type="text"></li>
     <hr/>
     <li v-for="(p,index) in persons" :key="index">{{p.name}}-{{p.age}} <input type="text"></li>
 </ul>
</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
           persons: [
               {id: '001', name: 'zqg', age: 18},
               {id: '002', name: 'bc',age: 19},
               {id: '003', name: 'ym',age: 20},
           ]
        },
        methods: {
            add() {
                const p = {id: '004', name: '老张', age: 30};
                this.persons.unshift(p);
            }
        }
    });
</script>
</body>
</html>

Vue中key原理图:

index作为key时:注意,如果不指定key,Vue会默认用index作为key。

image-20250210160230853

因为老刘被插到第一个,重刷了 key 的值,vue Diff 算法根据 key 的值判断虚拟DOM 全部发生了改变,然后全部重新生成新的真实 DOM(key所对应的input输入框,算法比对是一样的,所以直接复用了,这导致页面上输入框对不上)。实际上,张三,李四,王五并没有发生更改,是可以直接复用之前的真实 DOM,而因为 key 的错乱,导致要全部重新生成,造成了性能的浪费。

使用唯一标识作为key时:

image-20250210160037121

1.14 列表过滤

watch和computed都可以实现,computed更简单方便

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>列表过滤</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<!--准备好一个容器-->
<div id="app">
    <h2>人员列表</h2>
    <input type="text" v-model="keyword" placeholder="请输入名字">
 <ul>
     <li v-for="p in filterPersons" :key="p.id">{{p.name}}-{{p.age}}</li>
 </ul>
</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
           persons: [
               {id: '001', name: '马冬梅', age: 18},
               {id: '002', name: '周冬雨',age: 19},
               {id: '003', name: '周杰伦',age: 20},
               {id: '004', name: '温兆伦',age: 25}
           ],
            //filterPersons:[],
            keyword: ''
        },
        computed: {
            filterPersons() {
                return this.persons.filter(p => p.name.indexOf(this.keyword) !== -1);
            }
        },
        /*
        watch: {
            keyword:{
                immediate: true,
                handler(newValue, oldValue) {
                    this.filterPersons = this.persons.filter(p => p.name.indexOf(newValue) !== -1);
                }
            }
        }
        */
    });
</script>
</body>
</html>

1.15 列表排序

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>列表排序</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<!--准备好一个容器-->
<div id="app">
    <h2>人员列表</h2>
    <input type="text" v-model="keyword" placeholder="请输入名字">
    <button @click="sortType=1">年龄升序</button>
    <button @click="sortType=2">年龄降序</button>
    <button @click="sortType=0">原顺序</button>
 <ul>
     <li v-for="p in filterPersons" :key="p.id">{{p.name}}-{{p.age}}</li>
     <hr/>
 </ul>
</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
           persons: [
               {id: '001', name: '马冬梅', age: 28},
               {id: '002', name: '周冬雨',age: 19},
               {id: '003', name: '周杰伦',age: 20},
               {id: '004', name: '温兆伦',age: 25}
           ],
            keyword: '',
            sortType:0 //0:原顺序,1:升序,2:降序
        },
        computed: {
            filterPersons() {
                return this.persons.filter(p => p.name.indexOf(this.keyword) !== -1).sort(
                    (p1, p2) => {
                        if (this.sortType === 1) {
                            return p1.age - p2.age;
                        } else if (this.sortType === 2) {
                            return p2.age - p1.age;
                        } else {
                            return 0;
                        }
                    }
                );
            }
        },

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

1.16 Vue监测数据的原理

监测对象数据:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>更新时的问题</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<!--准备好一个容器-->
<div id="app">
    <h2>人员列表</h2>
    <button @click="updateYm">更新ym</button>
 <ul>
     <li v-for="p in persons" :key="p.id">{{p.name}}-{{p.age}}</li>
 </ul>
    <button @click="addGender">添加性别</button>
    <h3>姓名:{{student.name}}</h3>
    <h3>年龄:{{student.age}}</h3>
    <h3>性别:{{student.gender}}</h3>
</div>
<script type="text/javascript">
    <!--生产环境提示-->
    Vue.config.productionTip = false;
    const vm = new Vue({
        el: '#app',//挂载点,指定当前vue示例为哪个容器服务,值通常为css选择器字符串
        data: { //数据,给el 容器中的元素提供数据
           persons: [
               {id: '001', name: 'zqg', age: 18},
               {id: '002', name: 'bc',age: 19},
               {id: '003', name: 'ym',age: 20},
           ],
            student:{
                name:'lyf',
                age:18
            }
        },
        methods:{
            updateYm(){
                //this.persons[2].name = 'lyf' //成功,生效
                this.persons[0]={id: '001', name: 'zzz', age: 10}//失效,因为vue对数组的变更进行了监听,如果直接修改数组中的某一个对象,vue是无法知道这个数组发生了变化,所以需要使用数组的方法进行操作,如:push、pop、shift、unshift、splice等
            },
            addGender(){
                //Vue.set(this.student,'gender','男')
                this.$set(this.student,'gender','女')
            }
        }
    });
</script>
</body>
</html>

现象:直接修改person数组中某个对象的某个属性值室,Vue可以监测到并解析模板重新渲染,但是把某个对象改了(替换),代码是执行了,内存中的数据夜修改成功了,但是页面没有变化,说明Vue没有监测到。

Vue 监测数据变化的原理:

加工data中的数据(主要是添加响应式的getter和setter方法,用的是Object.defineProperty()方法)==>把加工后的对象传给_data,也就是vm._data = data ==> 属性值改变会调用set方法,在set方法中去解析模板,生成虚拟DOM,新旧DOM比较,更新页面。

简单模拟实现一下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>模拟数据监测</title>
</head>
<body>
<script type="text/javascript" >
    let data = {
        name:'zqg',
        address:'bj',
    }
    //创建一个监视的实例对象,用于监视data中属性的变化,(Vue实际做的更多,更完善)
    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) => {
            Object.defineProperty(this, k, {
                get() {
                    return obj[k]
                },
                set(val) {
                    console.log(`${k}被改了,我要去解析模板,生成虚拟DOM.....我要开始忙了`)
                    obj[k] = val
                }
            })
        })
    }
</script>
</body>
</html>

Vue.set 的使用

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

用法:

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

image-20250212180852337

监测数组:

vue 没有为数组中的元素生成 getter 和 setter,所以监测不到数据的更改(根据index替换元素的值),也不会引起页面的更新。

想要Vue监测到数据变化,需要使用数组的方法进行操作,如:push、pop、shift、unshift、splice等。(这些方法已经被Vue包装了)

image-20250213112714047

除了用数组方法,当然还可以用Vue.set()或者vm.$set()来修改数组元素。

总结:

Vue监视数据的原理:

vue会监视data中所有层次的数据。

如何监测对象中的数据?

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

对象中后追加的属性,Vue默认不做响应式处理,如需给后添加的属性做响应式,请使用如下API:

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

如何监测数组中的数据?

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

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

1、使用这些API:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
2、Vue.set() 或 vm.$set()

注意:Vue.set() 或 vm.$set() 不能给vm对象和根数据对象_data,添加响应式的属性。

1.17 收集表单数据

若:<input type=”text”/>,则v-model收集的是value值,用户输入的就是value值。

若:<input type=”radio”/>,则v-model收集的是value值,且要给标签配置value值。

若:<input type=”checkbox”/>

没有配置input的value属性,那么收集的就是checked(勾选 or 未勾选,是布尔值)
配置input的value属性:
v-model的初始值是非数组,那么收集的就是checked(勾选 or 未勾选,是布尔值)
v-model的初始值是数组,那么收集的的就是value组成的数组。

备注:v-model的三个修饰符:

lazy:失去焦点再收集数据

number:输入字符串转为有效的数字

trim:输入首尾空格过滤

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="js/vue.js"></script>
</head>
<body>
<div id="app">
    <form @submit="submitForm">
        <label for = "userName">账号:</label>
        <input type="text" id="userName" v-model.trim="userInfo.userName"><br>
        <label for="password">密码:</label>
        <input type="password" id="password" v-model="userInfo.password"></input><br>
        <br>
        性别:
        男<input type="radio" name="sex" value="1" v-model="userInfo.sex">
        女<input type="radio" name="sex" value="0" v-model="userInfo.sex"><br>
        <br>
        年龄: <input type="number" v-model.number="userInfo.age"><br><br>
        爱好:
        打游戏<input type="checkbox" name="hobby" value="game" v-model="userInfo.hobby">
        看电影<input type="checkbox" name="hobby" value="movie" v-model="userInfo.hobby">
        跑步<input type="checkbox" name="hobby" value="run" v-model="userInfo.hobby">
        <br>
        <br>
        学历:
        <select name="userInfo.education" >
            <option value="大专">大专</option>
            <option value="本科">本科</option>
            <option value="硕士">硕士</option>
            <option value="博士">博士</option>
        </select>
        <br>
        <br>
        <label for="birthday">出生日期:</label>
        <input type="date" id="birthday" v-model="userInfo.birthday"><br></input>
        <br>
        其他信息:
        <textarea name="otherInfo" cols="30" rows="10" v-model.lazy="userInfo.otherInfo"></textarea>
        <br>
        <br>
        <input type="checkbox" name="agree" value="agree" v-model="userInfo.agree"></input>
        阅读并接受<a href="https://zhangqingguo.github.io/">《用户协议》</a>
        <br>
        <br>
        <button type="submit">提交</button>
    </form>
</div>

</body>
<script type="text/javascript">
    Vue.config.productionTip = false;
    const vm = new Vue({
        el: '#app',
        data: {
            userInfo: {
                userName: '',
                password: '',
                sex: '',
                age: null,
                hobby: [],
                education: '',
                birthday: '',
                otherInfo: '',
                agree: false
            }
        },
        methods: {
            submitForm(e) {
                e.preventDefault();
                console.log(this.userInfo.userName);
                console.log(JSON.stringify(this.userInfo))
            }
        }
    })

</script>
</html>

1.18 过滤器使用

定义:对要显示的数据进行特定格式化后再显示(适用于一些简单逻辑的处理)。

语法:

  • 注册过滤器:Vue.filter(name,callback) ==》全局过滤器 或 new Vue{filters:{}} ==》局部过滤器
  • 使用过滤器:{ { xxx | 过滤器名} } 或 v-bind:属性 = “xxx | 过滤器名”

备注:

1.过滤器也可以接收额外参数、多个过滤器也可以串联

2.并没有改变原本的数据, 是产生新的对应的数据。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="./js/vue.js"></script>
    <script type="text/javascript" src="./js/dayjs.min.js"></script>
</head>
<body>
<div id="app">
    <h2>显示格式化时间</h2>
    现在是:{{time}}<br>
    <!--计算属性-->
    现在是:{{formatTime}}<br>
    <!-- 方法-->
    现在是:{{getTime()}}<br>
    <!--过滤器-->
    现在是:{{time | timeFormater}}<br>
    <!--过滤器传参-->
    现在是:{{time | timeFormater('YYYY年MM月DD日 HH:mm:ss')}}<br>
    <!--多个过滤器串联使用-->
    现在是:{{time | timeFormater('YYYY年MM月DD日 HH:mm:ss') | strSplice(0,11)}}<br>
    <!--v-bind使用-->
    <h3 :x="mag | strSplice(0,5)">zzz</h3>
</div>
</body>
<script type="text/javascript">
    Vue.config.productionTip = false;
    // 全局 过滤器
    Vue.filter('timeFormater', function (value, str = 'YYYY-MM-DD HH:mm:ss') {
        return dayjs(value).format(str)
    })
    const vm = new Vue({
        el: '#app',
        data: {
            time: Date.now(),
            mag: 'hello world'
        },
        // 计算属性
        computed: {
            formatTime() {
                return dayjs(this.time).format('YYYY-MM-DD HH:mm:ss')
            }
        },
        // 方法
        methods: {
            getTime() {
                return dayjs(this.time).format('YYYY-MM-DD HH:mm:ss')
            }
        },
        // 局部的 过滤器
        filters: {
            timeFormater(value, str = 'YYYY-MM-DD HH:mm:ss') {
                console.log('value=', value)
                return dayjs(value).format(str)
            },
            strSplice(value, start, end) {
                return value.substring(start, end)
            }
        }
    })
</script>
</html>

1.19 内置指令

已经学过的指令:

v-bind:属性绑定,单向数据绑定,简写为 :xxx
v-model:双向数据绑定
v-on:事件绑定,监听,简写为 @
v-if:条件渲染,动态控制节点是否存在
v-else:条件渲染,动态控制节点是否存在
v-show:条件渲染,动态控制节点是否显示
v-for:循环渲染,遍历数组/对象/字符串

v-text指令:

1.作用:向其所在的节点中渲染文本内容。

2.与插值语法的区别:v-text会替换掉节点中的内容,{{xx}}则不会。

v-html指令:

1.作用:向指定节点中渲染包含html结构的内容。

2.与插值语法的区别:

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

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

v-cloak指令:

  • 本质是一个特殊属性,Vue实例创建完毕并接管容器后,会删掉v-cloak属性。
  • 使用css配合v-cloak可以解决网速慢时页面展示出{{xxx}}的问题(未解析的模板,直接展示影响用户体验)。

v-once指令:

  • v-once所在节点在初次动态渲染后,就视为静态内容了。
  • 以后数据的改变不会引起v-once所在结构的更新,可以用于优化性能。

v-pre指令:

  • 跳过其所在节点的编译过程
  • 可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译。
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="./js/vue.js"></script>
    <style>
        [v-cloak]{
            display: none;
        }
    </style>
</head>
<body>
<!--
v-bind:属性绑定,单向数据绑定,简写为 :xxx
v-model:双向数据绑定
v-on:事件绑定,监听,简写为 @
v-if:条件渲染,动态控制节点是否存在
v-else:条件渲染,动态控制节点是否存在
v-show:条件渲染,动态控制节点是否显示
v-for:循环渲染,遍历数组/对象/字符串
v-text:文本绑定,
v-html:html绑定,
    1.作用:向指定节点中渲染包含html结构的内容。
    2.与插值语法的区别:
    v-html会替换掉节点中所有的内容,{{xx}}则不会。
    v-html可以识别html结构。
    3.严重注意:v-html有安全性问题!!!!
    在网站上动态渲染任意HTML是非常危险的,容易导致XSS攻击。
    一定要在可信的内容上使用v-html,永不要用在用户提交的内容上!
-->
<div id="app">
    <div>hello, {{name}}</div>
    <div v-text="name"></div>
    <div v-html="msg"></div>
    <h2 v-cloak>{{name}}</h2>
<hr>
    <h2 v-once>初始化数值num:{{num}}</h2>
    <h2>当前数值num:{{num}}</h2>
    <button @click="num++">点我num++</button>
    <hr>
    <h2 v-pre> 这是一个普通标签</h2>
</div>

</body>
<script>
    const vm = new Vue({
        el: '#app',
        data: {
            msg: '<h3>hello vue</h3>',
            name: 'zhangsan',
            num: 100
        }
    })
</script>
</html>

1.20 自定义指令

需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍。

需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点。

语法:

局部指令:

new Vue({

​ directives:{指名:配置对象}

})

或者

new Vue({

​ directives:{指令名:回调函数}

})

全局指令:Vue.directive(指令名,配置对象) 或者 Vue.directive(指令名,回调函数)

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

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

备注:

  • 指令定义时不加v-,但使用时要加v-。

  • 指令名如果是多个单词,要使用kebab-case命名方式,不要使用驼峰命名。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="./js/vue.js"></script>
</head>
<body>
<!--
需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍。

需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点。
-->
<div id="app">
    <h3>当前值:<span v-text="num"></span></h3>
    <h3 >放大10倍:<span v-big="num"></span></h3>
    <h3 >放大15倍:<span v-big-number="num"></span></h3>
    <button @click="num++">点我num++</button>
    <br>
    <input type="text" v-fbind:value="num">
</div>

</body>
<script type="text/javascript">
Vue.config.productionTip = false;
// 全局自定义指令
    Vue.directive('big', {
        bind(el, binding){
            el.innerText = binding.value * 10
        }
    })
    Vue.directive('fbind', {
        // 指令与元素成功绑定时(一上来)
        bind(el, binding){
            el.value = binding.value
        },
        // 指令所在元素被插入页面时
        inserted(el){
            el.focus()
        },
        // 指令所在的模板被重新解析时
        update(el, binding){
            el.value = binding.value
        }
    })
    new Vue({
        el: '#app',
        data: {
            num: 1
        },
        // 局部自定义指令
        directives: {
            //big函数什么时候调用:1.指令与元素成功绑定时(一上来)2.指令所在的模板被重新解析时
            big(el, binding){
                //this 是window
                console.log('big',this);
                // el是绑定指令的元素,真实的dom
                console.log(el)
                console.log(el instanceof HTMLElement)
                el.innerText = binding.value * 10
            },
            'big-number'(el, binding){
                el.innerText = binding.value * 15
            },
            fbind:{
                bind(el, binding){
                    el.value = binding.value
                },
                // 指令所在元素被插入页面时
                inserted(el){
                    el.focus()
                },
                update(el, binding){
                    el.value = binding.value
                }
            }
        }
    })

</script>
</html>

1.21 生命周期

先上图

life

beforeCreate(创建前):数据监测(getter和setter)和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。
created(创建后):实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el属性。
beforeMount(挂载前):在挂载开始之前被调用,相关的render函数首次被调用。此阶段Vue开始解析模板,生成虚拟DOM存在内存中,还没有把虚拟DOM转换成真实DOM,插入页面中。所以网页不能显示解析好的内容。
mounted(挂载后):在el被新创建的 vm.$el(就是真实DOM的拷贝)替换,并挂载到实例上去之后调用(将内存中的虚拟DOM转为真实DOM,真实DOM插入页面)。此时页面中呈现的是经过Vue编译的DOM,这时在这个钩子函数中对DOM的操作可以有效,但要尽量避免。一般在这个阶段进行:开启定时器,发送网络请求,订阅消息,绑定自定义事件等等
beforeUpdate(更新前):响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染(数据是新的,但页面是旧的,页面和数据没保持同步呢)。
updated(更新后) :在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
beforeDestroy(销毁前):实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例。在这个阶段一般进行关闭定时器,取消订阅消息,解绑自定义事件。(这个时候修改数据不会触发更新)
destroyed(销毁后):实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器(Vue自定义的,dom原生的监听事件还在)会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。

created之后:

先判断有没有 el 这个配置项,没有就调用 vm.$mount(el),如果两个都没有就一直卡着,显示的界面就是最原始的容器的界面。有 el 这个配置项,就进行判断有没有 template 这个配置项,没有 template 就将 el 绑定的容器编译为 vue 模板。

第一种情况,有 template:

如果 el 绑定的容器没有任何内容,就一个空壳子,但在 Vue 实例中写了 template,就会编译解析这个 template 里的内容,生成虚拟 DOM,最后将 虚拟 DOM 转为 真实 DOM 插入页面(其实就可以理解为 template 替代了 el 绑定的容器的内容)。

image-20250415151131396

第二种情况,没有 template:

没有 template,就编译解析 el 绑定的容器,生成虚拟 DOM,后面就顺着生命周期执行下去。

总结:

常用生命周期钩子:

1、mounted:发送ajax请求,启动定时器,绑定自定义事件,订阅消息【初始化操作】

2、beforeDestroy:清除定时器,解绑自定义事件,取消订阅消息等【收尾工作】

关于销毁Vue实例

1、销毁后借助Vue开发工具看不到任何信息。

2、销毁后自定义事件会失效,但是原生DOM事件依然有效。

3、一般不会在beforeDestroy操作数据,因为即使操作数据,也不会触发更新流程。

1.21 非单文件组件

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

作用:复用编码,简化项目编码,提高运行效率。

非单文件组件:一个文件包含多个组件。

单文件组件:一个文件只包含1个组件。

基本使用
Vue中使用组件的三大步骤:

定义组件(创建组件)
注册组件
使用组件(写组件标签)

定义组件

使用Vue.extend(options)创建,其中options和new Vue(options)时传入的那个options几乎一样,但也有点区别;

区别如下:

el不要写,为什么? ———> 最终所有的组件都要经过一个vm的管理,由vm中的el决定服务哪个容器。
data必须写成函数(普通函数),为什么? ————> 避免组件被复用时,数据存在引用关系。

举例说明 data为什么要写成函数?

这是因为js底层设计的原因:

对象形式:

let data = {
    a: 99,
    b: 100
}

let x = data;
let y = data;
// x 和 y 引用的都是同一个对象,修改 x 的值, y 的值也会改变
x.a = 66;
console.log(x); // a:66 b:100
console.log(y); // a:66 b:100

函数形式:

function data() {
    return {
        a: 99,
        b: 100
    }
}
let x = data();
let y = data();
console.log(x === y); // false,调用函数,每次返回新的对象

备注:使用template可以配置组件结构。

注册组件

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

几个注意点:
1、关于组件名:

一个单词组成:

第一种写法(首字母小写):school
第二种写法(首字母大写):School
多个单词组成:

第一种写法(kebab-case命名):my-school
第二种写法(CamelCase命名):MySchool (需要Vue脚手架支持)

备注:

(1).组件名尽可能回避HTML中已有的元素名称,例如:h2、H2都不行。

(2).可以使用name配置项指定组件在开发者工具中呈现的名字。

2、关于组件标签

第一种写法:<school></school>

第二种写法:<school/>

备注:不使用脚手架时,<school/>会导致后续组件不能渲染。

3、一个简写方式:

const school = Vue.extend(options) 可简写为:const school = options

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="./js/vue.js"></script>

</head>
<body>
<div id="app">
    <h2>{{msg}}</h2>
    <hr>
    <school></school>
    <hr>
    <person></person>
    <hr>
    <student></student>
</div>

</body>
<script type="text/javascript">
    Vue.config.productionTip = false;
    // 简写 组件
    const student = {
        template: `
            <div>
                <h2>学生姓名:{{name}}</h2>
                <h2>学生年龄:{{age}}</h2>
                <h2>学生性别:{{sex}}</h2>
                <button @click="showInfo">点我提示学生姓名</button>
            </div>
        `,
        data() {
            return {
                name: '李四',
                age: 19,
                sex: '女'
            }
        },
        methods: {
            showInfo(){
                alert(this.name)
            }
        }
    }
    const school = Vue.extend({
        template: `
            <div>
                <h2>学校名称:{{name}}</h2>
                <h2>学校地址:{{address}}</h2>
                <button @click="showSchool">点我提示学校名称</button>
            </div>
        `,
        data() {
            return {
                name: '圣芙蕾雅',
                address: '极东'
            }
        },
        methods: {
            showSchool() {
                alert(this.name)
            }
        }
    })
    const person = Vue.extend({
        template: `
            <div>
                <h2>姓名:{{name}}</h2>
                <h2>年龄:{{age}}</h2>
                <h2>性别:{{sex}}</h2>
                <button @click="showInfo">点我提示姓名</button>
            </div>
        `,
        data() {
            return {
                name: '张三',
                age: 18,
                sex: '男'
            }
        },
        methods: {
            showInfo(){
                alert(this.name)
            }
        }
    })

    // 全局注册组件
    Vue.component('student', student)
    Vue.component('school', school)
    Vue.component('person', person)
    new Vue({
        el: '#app',
        data: {
            msg: '你好啊'
        },
        // 局部 注册组件
        // components: {
        //    school: school,
        //    person: person
        //   // ES6简写形式
        //   school,
        //   student
        // }
    })




</script>
</html>

组件嵌套:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script type="text/javascript" src="./js/vue.js"></script>

</head>
<body>
<div id="app">
</div>

</body>
<script type="text/javascript">
    Vue.config.productionTip = false;
    // 简写 组件
    const student = {
        template: `
            <div>
                <h2>学生姓名:{{name}}</h2>
                <h2>学生年龄:{{age}}</h2>
                <h2>学生性别:{{sex}}</h2>
                <button @click="showInfo">点我提示学生姓名</button>
            </div>
        `,
        data() {
            return {
                name: '李四',
                age: 19,
                sex: '女'
            }
        },
        methods: {
            showInfo(){
                alert(this.name)
            }
        }
    }
    const school = Vue.extend({
        template: `
            <div>
                <h2>学校名称:{{name}}</h2>
                <h2>学校地址:{{address}}</h2>
                <student></student>
                <button @click="showSchool">点我提示学校名称</button>
            </div>
        `,
        data() {
            return {
                name: '圣芙蕾雅',
                address: '极东'
            }
        },
        components:{
            student
        },
        methods: {
            showSchool() {
                alert(this.name)
            }
        }
    })
    const myAddress = Vue.extend({
        template: `
            <div>
                <h2>地址:{{address}}</h2>
            </div>
        `,
        data() {
            return {
                address: '神州'
            }
        }
    })
    const person = Vue.extend({
        template: `
            <div>
                <h2>姓名:{{name}}</h2>
                <h2>年龄:{{age}}</h2>
                <h2>性别:{{sex}}</h2>

                <my-address></my-address>
                <button @click="showInfo">点我提示姓名</button>
            </div>
        `,
        data() {
            return {
                name: '张三',
                age: 18,
                sex: '男'
            }
        },
        components: {
            'my-address': myAddress
        },
        methods: {
            showInfo(){
                alert(this.name)
            }
        }
    })

    const app = {
        template: `
            <div>
                <h2>{{msg}}</h2>
                <hr>
                <school></school>
                <hr>
                <person>
                </person>
                <hr>
            </div>
        `,
        data() {
            return {
                msg: '你好啊'
            }
        },
        components: {
            school,
            person
        },
    }
    const vm = new Vue({
        el: '#app',
        template: '<app></app>',
        components: {
            app
        }
    })
    
</script>
</html>

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

一个重要的内置关系

  • 一个重要的内置关系:VueComponent.prototype._proto_=== Vue.prototype
  • 为什么要有这个关系:让组件实例对象(vc)可以访问到 Vue原型上的属性、方法。

1.22 单文件组件

单文件组件就是将一个组件的代码写在 .vue 这种格式的文件中,webpack 会将 .vue 文件解析成 html,css,js这些形式。

2、vue脚手架,自定义事件,插槽等复杂内容

2.1 脚手架

使用前置:

第一步(没有安装过的执行):全局安装 @vue/cli

npm install -g @vue/cli

第二步:切换到要创建项目的目录,然后使用命令创建项目

vue create xxxxx

第三步:启动项目

npm run serve

脚手架文件结构

├── 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:包版本控制文件

import Vue,不需要加文件路径,脚手架已经处理好了, package.json文件中的module指定了具体引入的文件。

image-20250502200345172

main.js入口文件:

/**
 * 整个项目的入口文件
 */
//引入Vue(vue.runtime.esm.js,精简版,不包含模板解析器)
import Vue from 'vue'
//引入App组件,它是所有组件的父组件
import App from './App.vue'
//关闭Vue的生产提示
Vue.config.productionTip = false
/**
 * vue.js与vue.runtime.xxx.js的区别:
 * vue.js是完整版的Vue,包含:核心功能+模板解析器。
 * vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。
 * 因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用render函数接收到的createElement函数去指定具体内容。
 */
//创建Vue实例对象
new Vue({
  //将app组件,渲染到#app容器中
  //render: h => h(App),

  //完整写法
  render(crateElement){
    console.log(typeof crateElement)
    return crateElement(App)
  }

}).$mount('#app')

render函数

为什么要使用render函数?

image-20250504174642814

这样template写法会报错:

image-20250504175136022

所以,要么使用完整版本的Vue,要么使用运行版本Vue提供的render函数,否则无法解析编译模板。

//完整的render函数写法
render(crateElement){
     return crateElement(App)
}

因为 render 函数内并没有用到 this,所以可以简写成箭头函数。只有一个参数就可以把圆括号去了,函数体内部只有一个 return 就可以把大括号去掉,return去掉,最后简写成:render: h => h(App)

new Vue({
  render: h => h(App),
}).$mount('#app')

不同版本 vue 的区别

vue.js与vue.runtime.xxx.js的区别:
vue.js是完整版的Vue,包含:核心功能+模板解析器。
vue.runtime.xxx.js是运行版的Vue,只包含:核心功能;没有模板解析器。
因为vue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用render函数接收到的createElement函数去指定具体内容。

修改脚手架的默认配置

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

脚手架中的index页面

<!DOCTYPE html>
<html lang="">
  <head>
    <meta charset="utf-8">
    <!--  针对IE浏览器的兼容性     -->
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
<!--   开启移动端的理想视口   -->
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
<!--配置页签图标,BASE_URL值是public目录-->
    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<!--配置网页标题-->
    <title><%= htmlWebpackPlugin.options.title %></title>
  </head>
  <body>
    <!-- 当浏览器不支持js,标签内容会被渲染 -->
    <noscript>
      <strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <!-- 挂载点,容器 -->
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

2.2 vue的其他知识

ref属性

1、被用来给元素或子组件注册引用信息(id的替代者)
2、应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)
3、使用方式:
打标识:<h1 ref=”xxx”>…..</h1>或 <School ref=”xxx”></School>
获取:this.$refs.xxx

props配置项

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

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

  3. 接收数据:

    1. 第一种方式(只接收):props:['name']

    2. 第二种方式(限制类型):props:{name:String}

    3. 第三种方式(限制类型、限制必要性、指定默认值)

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

备注:

1.props是只读的,Vue底层会监测你对props的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制props的内容到data中一份,然后去修改data中的数据

2.属性名不要使用已有的关键名字,如key,ref等

mixin(混入)

混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

定义混入

export const mixin = {
    methods: {
        showName() {
            alert(this.name)
        }
    },
    mounted() {
        console.log('mixin mounted')
    }
}

export const mixin2 = {
    data() {
        return {
            x: 100,
            y: 200
        }
    }
}

使用混入

1、局部混入:

<script>
import {mixin,mixin2} from '../mixin'
export default {
  name: 'SchoolInfo',
  data() {
    return {
      name: '折纸大学',
      address: '匹诺康尼'
    }
  },
  mixins: [mixin, mixin2]
}
</script>

2、全局混入:不建议使用,会导致整个Vue,组件都引入了混入的配置。

在main.js中引入使用。

//全局混入
import {mixin,mixin2} from "@/mixin";

Vue.mixin(mixin)
Vue.mixin(mixin2)

选项合并

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。

1、数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。

2、同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。

3、值为对象的选项,例如 methodscomponentsdirectives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

插件

插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制。

通过全局方法 Vue.use() 使用插件。它需要在你调用 new Vue() 启动应用之前完成。

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

定义插件:

export default  {
    install(Vue,y){
        // y是自定义参数
        console.log('插件被调用了', Vue,y)
        Vue.filter('filterName', function(value) {
            return value.slice(0,2)
        })

        Vue.mixin({
            data() {
                return {
                    x: 100
                }
            }
        })

        Vue.directive('fbind', {
            bind(el, binding) {
                el.value = binding.value
            },
            inserted(el) {
                el.focus()
            },
            update(el,binding) {
                el.value = binding.value
            }
        })

        //给Vue原型上添加一个方法,vm和vc都可以使用
        Vue.prototype.hello = function() {
            alert('你好啊')
        }
    }
}

引入使用插件:

在main.js中:

import MyPlugins from './plugins.js'
//调用MyPlugins.install方法,可以带自定义参数
Vue.use(MyPlugins,111)

这样就可以在组件里使用插件里面的功能了。

scoped样式

  1. 作用:让样式在局部生效,防止冲突。
  2. 写法:<style scoped>

style里面的lang指定语言,css,less等

总结TodoList案例

组件化编码流程:

(1).拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突。

(2).实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:

1).一个组件在用:放在组件自身即可。

2). 一些组件在用:放在他们共同的父组件上(状态提升)。

(3).实现交互:从绑定事件开始。

props适用于:

(1).父组件 ==> 子组件 通信

(2).子组件 ==> 父组件 通信(要求父先给子一个函数)

使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以修改的!

props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这样做。

2.3 浏览器本地存储

Cookie是最早被提出来的本地存储方式,在此之前,服务端是无法判断网络中的两个请求是否是同一用户发起的,为解决这个问题,Cookie就出现了。Cookie 是存储在用户浏览器中的一段不超过 4 KB 的字符串。它由一个名称(Name)、一个值(Value)和其它几个用 于控制 Cookie 有效期、安全性、使用范围的可选属性组成。不同域名下的 Cookie 各自独立,每当客户端发起请求时,会自动把当前域名下所有未过期的 Cookie 一同发送到服务器。

Cookie的特性:

  • Cookie一旦创建成功,名称就无法修改
  • Cookie是无法跨域名的,也就是说a域名和b域名下的cookie是无法共享的,这也是由Cookie的隐私安全性决定的,这样就能够阻止非法获取其他网站的Cookie
  • 每个域名下Cookie的数量不能超过20个,每个Cookie的大小不能超过4kb
  • 有安全问题,如果Cookie被拦截了,那就可获得session的所有信息,即使加密也于事无补,无需知道cookie的意义,只要转发cookie就能达到目的
  • Cookie在请求一个新的页面的时候都会被发送过去

Cookie 在身份认证中的作用

客户端第一次请求服务器的时候,服务器通过响应头的形式,向客户端发送一个身份认证的 Cookie,客户端会自动 将 Cookie 保存在浏览器中。

随后,当客户端浏览器每次请求服务器的时候,浏览器会自动将身份认证相关的 Cookie,通过请求头的形式发送给 服务器,服务器即可验明客户端的身份。

Cookie 不具有安全性

由于 Cookie 是存储在浏览器中的,而且浏览器也提供了读写 Cookie 的 API,因此 Cookie 很容易被伪造,不具有安全性。因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器。

注意:千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等。

Session

Session是另一种记录客户状态的机制,不同的是Cookie保存在客户端浏览器中,而Session保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是Session。客户端浏览器再次访问时只需要从该Session中查找该客户的状态就可以了session是一种特殊的cookie。cookie是保存在客户端的,而session是保存在服务端。

为什么要用session 由于cookie 是存在用户端,而且它本身存储的尺寸大小也有限,最关键是用户可以是可见的,并可以随意的修改,很不安全。那如何又要安全,又可以方便的全局读取信息呢?于是,这个时候,一种新的存储会话机制:session 诞生了

session原理 当客户端第一次请求服务器的时候,服务器生成一份session保存在服务端,将该数据(session)的id以cookie的形式传递给客户端;以后的每次请求,浏览器都会自动的携带cookie来访问服务器(session数据id)。

LocalStorage

LocalStorage是HTML5新引入的特性,由于有的时候我们存储的信息较大,Cookie就不能满足我们的需求,这时候LocalStorage就派上用场了。

LocalStorage的优点:

  • 在大小方面,LocalStorage的大小一般为5MB,可以储存更多的信息
  • LocalStorage是持久储存,并不会随着页面的关闭而消失,除非主动清理,不然会永久存在
  • 仅储存在本地,不像Cookie那样每次HTTP请求都会被携带

LocalStorage的缺点:

  • 存在浏览器兼容问题,IE8以下版本的浏览器不支持
  • 如果浏览器设置为隐私模式,那我们将无法读取到LocalStorage
  • LocalStorage受到同源策略的限制,即端口、协议、主机地址有任何一个不相同,都不会访问

LocalStorage的使用场景:

  • 有些网站有换肤的功能,这时候就可以将换肤的信息存储在本地的LocalStorage中,当需要换肤的时候,直接操作LocalStorage即可
  • 在网站中的用户浏览信息也会存储在LocalStorage中,还有网站的一些不常变动的个人信息等也可以存储在本地的LocalStorage中

SessionStorage

SessionStorage和LocalStorage都是在HTML5才提出来的存储方案,SessionStorage 主要用于临时保存同一窗口(或标签页)的数据,刷新页面时不会删除,关闭窗口或标签页之后将会删除这些数据。

SessionStorage与LocalStorage对比:

  • SessionStorage和LocalStorage都在本地进行数据存储
  • SessionStorage也有同源策略的限制,但是SessionStorage有一条更加严格的限制,SessionStorage只有在同一浏览器的同一窗口下才能够共享
  • LocalStorage和SessionStorage都不能被爬虫爬取

总结:

WebStorage

1、存储大小一般支持5MB(不同浏览器可能不一样)

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

3、相关API:

localStorage:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>localStorage</title>
</head>
<body>
<h2>localStorage</h2>
<button onclick="setLocalStorage()">设置localStorage</button>
<button onclick="getLocalStorage()">获取localStorage</button>
<button onclick="removeLocalStorage()">删除localStorage</button>
<button onclick="clearLocalStorage()">清空localStorage</button>

<script type="text/javascript">
    function setLocalStorage() {
        //setItem的第二个参数是string,会调用toSting方法
        let p = {name: '张三', age: 18, sex: '男'};
        localStorage.setItem('msg', 'hhhhhh2');
        localStorage.setItem('n', 18);
        localStorage.setItem('person', JSON.stringify(p));
        localStorage.setItem('person2', p);
    }
    function getLocalStorage() {
        console.log(localStorage.getItem('msg'));
        console.log(localStorage.getItem('n'));
        console.log(localStorage.getItem('person'));
        console.log(JSON.parse(localStorage.getItem('person')));
    }
    function removeLocalStorage() {
        localStorage.removeItem('msg');
        localStorage.removeItem('n');
        localStorage.removeItem('person');
        localStorage.removeItem('person2');
    }
    function clearLocalStorage() {
        localStorage.clear();
    }
</script>
</body>
</html>

sessionStorage:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>sessionStorage</title>
</head>
<body>
<h2>sessionStorage</h2>
<button onclick="setSessionStorage()">设置sessionStorage</button>
<button onclick="getSessionStorage()">获取sessionStorage</button>
<button onclick="removeSessionStorage()">删除sessionStorage</button>
<button onclick="clearSessionStorage()">清空sessionStorage</button>

<script type="text/javascript">
    function setSessionStorage() {
        //setItem的第二个参数是string,会调用toSting方法
        let p = {name: '张三', age: 18, sex: '男'};
        sessionStorage.setItem('msg', 'hhhhhh2');
        sessionStorage.setItem('n', 18);
        sessionStorage.setItem('person', JSON.stringify(p));
        sessionStorage.setItem('person2', p);
    }
    function getSessionStorage() {
        console.log(sessionStorage.getItem('msg'));
        console.log(sessionStorage.getItem('n'));
        console.log(sessionStorage.getItem('person'));
        console.log(JSON.parse(sessionStorage.getItem('person')));
    }
    function removeSessionStorage() {
        sessionStorage.removeItem('msg');
        sessionStorage.removeItem('n');
        sessionStorage.removeItem('person');
        sessionStorage.removeItem('person2');
    }
    function clearSessionStorage() {
        sessionStorage.clear();
    }
</script>
</body>
</html>

4、备注:

  1. sessionStorage存储的内容会随着浏览器窗口关闭而消失。
  2. localStorage存储的内容需要手动清除才会消失。
  3. getItem(key),获取不到key对应的value值,返回null。
  4. JSON.parse(null)结果是null。

2.4 组件自定义事件

组件自定义事件是一种组件间通信的方式,适用于:子组件 ===> 父组件

使用场景

A是父组件,B是子组件,B想给A传数据,那么就要在A中给B绑定自定义事件(事件的回调在A中),B触发自定义事件。

给谁绑定事件,就找谁触发事件。

绑定自定义事件:

第一种方式,在父组件中:<StudentInfo v-on:zqg="getStudentName"/><StudentInfo @zqg="getStudentName"/>

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

使用 this.$refs.xxx.$on() 这样写起来更灵活,比如可以加定时器。this.$refs.xxx.$once(),只调用一次。

App.vue ,给student组件绑定自定义事件:zqg

<StudentInfo ref="student" @click.native="show"></StudentInfo>
...
methods: {
  //可以声明接收多个参数,也可以指定一个参数,其他参数全部存到params数组中
  getStudentName(name, ...params) {
    console.log('app getStudentName receive:', name, params)
  }
}
mounted() {
   this.$refs.student.$on('zqg', this.getStudentName)
 }

Student.vue 触发zqg事件

<button @click="sendStudentName">点我发送学生姓名</button>
...
methods: {
    sendStudentName() {
      //  触发组件实例的zqg事件
      this.$emit('zqg',this.name)
    }
}

解绑自定义事件this.$off('zqg')==》解绑一个, this.$off('xx','yy')==》解绑多个,this.$off()==》解绑所有

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

<StudentInfo ref="student" @click.native="show"></StudentInfo>

注意:通过this.$refs.xxx.$on('atguigu',回调)绑定自定义事件时,回调要么配置在methods中,要么用箭头函数,否则this指向会出问题!(使用普通函数,this是触发事件的组件的实例对象)

举例:下面写法不行,无法给app的studentName赋值,因为this指向不对

//回调是普通函数,this指向student实例对象, this.studentName = name 会出问题,这种写法不行
this.$refs.student.$on('zqg', function (name){
  console.log('app mounted $refs receive:', name)
  this.studentName = name
  console.log(this)//this指向student实例对象
})

可以的写法:

//回调是箭头函数,没有this,往外找,找到app实例,this.studentName = name 成功
this.$refs.student.$on('zqg', (name) => {
  console.log('app mounted $refs receive:', name)
  this.studentName = name
  console.log(this)//this指向app实例对象
})

或者:

methods: {
  getSchoolName(name) {
    console.log('app getSchoolName receive:', name)
  },
  //可以声明接收多个参数,也可以指定一个参数,其他参数全部存到params数组中
  getStudentName(name, ...params) {
    console.log('app getStudentName receive:', name, params)
  }
}
 mounted() {
    // this.getStudentName,传入app实例对象的method的函数,会以app的函数为准,不会出现问题
    this.$refs.student.$on('zqg', this.getStudentName)
  }

2.5 全局事件总线(GlobalEventBus)

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

2、安装全局事件总线:

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

3、使用事件总线:

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

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

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

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

2.6 消息订阅与发布

  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) //订阅消息
      }
      
    4. 提供数据:pubsub.publish('xxx',数据)

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

    2.7 nextTick

    1. 语法:this.$nextTick(回调函数)
    2. 作用:在下一次 DOM 更新结束后执行其指定的回调。
    3. 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行。
    // 在dom节点更新完后,执行设置焦点
    this.$nextTick(() => {
      this.$refs.inputText.focus();
    });
    

2.8 Vue封装的过度与动画

image-20250523105250111

  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>
      

      name 的作用可以让让不同的元素有不同的动画效果,用name区分开不同元素的样式。指定了name后,样式名称要改成 xxx-enter, 比如hello-enter-active

    3. 备注:若有多个元素需要过度,则需要使用:<transition-group>,且每个元素都要指定key值。

      appear属性,作用是一开始进入就展示动画效果。

  3. 具体案例

    单个元素动画

    <script>
    export default {
      name: 'MyTest',
      data() {
        return {
          isShow: true
        }
      },
    }
    </script>
    
    <template>
    <div>
      <button @click="isShow = !isShow">显示/隐藏</button>
    <!--  <h2 v-show="isShow" class="go">bcym</h2>-->
      <transition name="hello" appear>
        <h2 v-show="isShow">bcym</h2>
      </transition>
    </div>
    </template>
    
    <style scoped>
    h2 {
      background: orange;
    }
    .hello-enter-active{
      animation: mymove 1s linear;
    }
    .hello-leave-active {
      animation: mymove 1s reverse linear;
    }
    .come{
      animation: mymove 1s;
    }
    .go{
      animation: mymove 1s reverse;
    }
    @keyframes mymove {
      from {
        margin-left: 0px;
        transform: translateX(-100%);
      }
      to {
        margin-left: 200px;
        transform: translateX(0px);
      }
    }
    </style>
    

    已经有了动画,想要借助Vue实现,那么用<transition>标签包裹,同时在v-enter-active,v-leave-active配置好已有的动画。

    单个元素过度:

    <script>
    export default {
      name: 'MyTest',
      data() {
        return {
          isShow: true
        }
      },
    }
    </script>
    
    <template>
      <div>
        <button @click="isShow = !isShow">显示/隐藏</button>
        <transition name="hello" appear>
          <h2 v-show="isShow">bcym</h2>
        </transition>
      </div>
    </template>
    
    <style scoped>
    h2 {
      background: 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>
    

    没有写好的动画,想要靠过度实现动画效果,进入的起点,终点,离开的起点、终点,还有过程中,都要配置好。

    多个元素过度:

    <script>
    export default {
      name: 'MyTest',
      data() {
        return {
          isShow: true
        }
      },
    }
    </script>
    
    <template>
      <div>
        <button @click="isShow = !isShow">显示/隐藏</button>
        <transition-group name="hello" appear>
          <h2 v-show="!isShow" key="1">zqg</h2>
          <h2 v-show="isShow" key="2">bcym</h2>
        </transition-group>
      </div>
    </template>
    
    <style scoped>
    h2 {
      background: 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>
    

导入第三方库:Animate.css | A cross-browser library of CSS animations.。需要安装npm install animate.css、导入import 'animate.css',然后使用。指定name,配置进入,离开等的clas。

<script>
import 'animate.css'

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

<template>
  <div>
    <button @click="isShow = !isShow">显示/隐藏</button>
    <transition-group name="animate__animated animate__bounce"
                      enter-active-class="animate__swing"
                      leave-active-class="animate__backOutUp"
                      appear>
      <h2 v-show="!isShow" key="1">zqg</h2>
      <h2 v-show="isShow" key="2">bcym</h2>
    </transition-group>
  </div>
</template>

<style scoped>
h2 {
  background: orange;
}
</style>

2.9 vue脚手架配置代理

跨域问题:

1、服务器返回响应时添加cors的特殊响应头

2、jsonp,利用script 的src,只能解决get请求,不常用。

3、使用代理服务器,(让代理服务器和浏览器同源,不会有跨域问题,然后中转一下真实服务器返回的数据。常见代理有:nginx,vue-cli、vite等

image-20250523144159375

注意:代理服务器和前端开发服务器确实需要监听同一个端口(如8080),但不会冲突,因为它们实际上是同一个服务器提供的两种功能。关键点:代理服务器是前端服务器的一部分。

误区:”配置了proxy就是启动了一个独立代理服务器”
事实:proxy只是开发服务器的一个功能,没有额外端口占用

误区:”proxy配置的目标服务器就是代理服务器”
事实:目标服务器是你的后端API服务,不是代理服务器

使用脚手架配置代理服务器:

方法一

在vue.config.js中添加如下配置:

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

说明:

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

方法二

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

module.exports = {
    devServer: {
    proxy: {
      '/api1': {// 匹配所有以 '/api1'开头的请求路径
        target: 'http://localhost:5000',// 代理目标的基础路径
        changeOrigin: true,
        pathRewrite: {'^/api1': ''}//代理服务器将请求地址转给真实服务器时会将 /api1 去掉
      },
      '/api2': {// 匹配所有以 '/api2'开头的请求路径
        target: 'http://localhost:5001',// 代理目标的基础路径
        changeOrigin: true, //控制请求头中host的值
        pathRewrite: {'^/api2': ''},
        ws: true//  支持代理websocket
      }
    }
  }
}
/*
   changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
   changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:8080
   changeOrigin默认值为true
*/

说明:

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

2.10 插槽

  1. 作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于 父组件 ===> 子组件

  2. 分类:默认插槽、具名插槽named slot、作用域插槽scoped slot

  3. 使用方式:

    1. 默认插槽:

      父组件中:
              <Category>
                 <div>html结构1</div>
              </Category>
      子组件中:
              <template>
                  <div>
                     <!-- 定义插槽 -->
                     <slot>插槽默认内容...</slot>
                  </div>
              </template>
      
    2. 具名插槽:

      父组件中:
              <Category>
                  <template slot="center">
                    <div>html结构1</div>
                  </template>
      
                  <template v-slot:footer>
                     <div>html结构2</div>
                  </template>
              </Category>
      子组件中:
              <template>
                  <div>
                     <!-- 定义插槽 -->
                     <slot name="center">插槽默认内容...</slot>
                     <slot name="footer">插槽默认内容...</slot>
                  </div>
              </template>
      
    3. 作用域插槽:

      1. 理解:数据在组件的自身(子组件),但根据数据生成的结构需要组件的使用者(父组件)来决定。(games数据在Category(子)组件中,但使用数据所遍历出来的结构由App(父)组件决定)。

      2. 具体编码:

        父组件中:
                <Category>
                    <template scope="scopeData">
                        <!-- 生成的是ul列表 -->
                        <ul>
                            <li v-for="g in scopeData.games" :key="g">{{g}}</li>
                        </ul>
                    </template>
                </Category>
                <Category>
                    <template slot-scope="{games}">
                        <!-- 生成的是ul列表 -->
                        <ol>
                            <li v-for="g in games" :key="g">{{g}}</li>
                        </ol>
                    </template>
                </Category>
        
                <Category>
                    <template  v-slot="scopeData">
                        <!-- 生成的是h4标题 -->
                        <h4 v-for="g in scopeData.games" :key="g">{{g}}</h4>
                    </template>
                </Category>
        子组件中:
                <template>
                    <div>
                    <!-- 通过数据绑定就可以把子组件的数据传到父组件 -->
                        <slot :games="games"></slot>
                    </div>
                </template>
                
                <script>
                    export default {
                        name:'Category',
                        props:['title'],
                        //数据在子组件自身
                        data() {
                            return {
                                games:['红色警戒','穿越火线','劲舞团','超级玛丽']
                            }
                        },
                    }
                </script>
        

注意: scope 属性在 Vue 2.x 中已经被废弃,推荐使用 slot-scope。使用 v-slot(Vue 2.6.0+ 推荐,包括Vue 3.x)。在 Vue 3 中,v-slot 是唯一支持的语法。作用域插槽也可以指定名字。

v-slot="{ games }"(解构赋值)

作用:直接从插槽的 props 对象中解构出 games 属性,并直接在模板中使用 games
适用场景:当只需要 games 数据,而不关心插槽提供的其他属性时。

v-slot="scopeData"(访问对象属性)

作用:接收整个插槽的 props 对象(假设命名为 scopeData),然后通过 scopeData.games 访问具体数据。
适用场景:当需要访问插槽提供的多个属性时。

3.Vuex

vuex

3.1 概念

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

3.2 何时使用?

多个组件需要共享数据时

3.3 搭建vuex环境

先安装 npm i vuex@3, 注意,Vue2.x使用的Vuex是3.x版本,Vue3使用Vuex4版本。

  1. 创建文件:src/store/index.js

    //引入Vue核心库
    import Vue from 'vue'
    //引入Vuex
    import Vuex from 'vuex'
    //应用Vuex插件,必须在创建store实例之前使用
    Vue.use(Vuex)
    
    //准备actions对象——响应组件中用户的动作
    const actions = {}
    //准备mutations对象——修改state中的数据
    const mutations = {}
    //准备state对象——保存具体的数据
    const state = {}
    
    //创建并暴露store
    export default new Vuex.Store({
        actions,
        mutations,
        state
    })
    
  2. main.js中创建vm时传入store配置项

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

3.4 基本使用

store.js

import Vue from 'vue'

import Vuex from 'vuex'

//  使用vuex,必须在创建store实例之前使用
Vue.use(Vuex)
// 定义actions,mutations,state
const actions = {
  // add(context, value) {
  //   context.commit('ADD', value)
  // },
  // minus(context, value){
  //   context.commit('MINUS', value)
  // },
  addOdd(context, value){
    if(context.state.count % 2){
      context.commit('ADD', value)
    }
  },
  addWait(context, value){
    setTimeout(() => {
      context.commit('ADD', value)
    }, 1000)
  }
}
const mutations = {
  ADD(state, value) {
    state.count += value
  },
  MINUS(state, value) {
    state.count -= value
  },

}
const state = {
  count: 0,
}

// 创建,并导出 store 实例
export default new Vuex.Store({
  actions,
  mutations,
  state
})
  1. 组件中读取vuex中的数据:$store.state.count
  2. 组件中修改vuex中的数据:$store.dispatch('action中的方法名',数据)$store.commit('mutations中的方法名',数据)
  3. 备注:若没有网络请求或其他业务逻辑,组件中也可以越过actions,即不写dispatch,直接编写commit也是可以的。

Count.vue

<script>
export default {
  name: "MyCount",
  data() {
    return {
      num: 1
    }
  },
  methods: {
    increment() {
     // this.$store.dispatch('add', this.num)
      //没有业务逻辑,直接提交
      this.$store.commit('ADD', this.num)
    },
    decrement() {
      //this.$store.dispatch('minus', this.num)
      this.$store.commit('MINUS', this.num)
    },
    incrementOdd() {
      //业务逻辑,写在store中
      this.$store.dispatch('addOdd', this.num)
      // if (this.$store.state.count % 2) {
      //   this.$store.dispatch('add', this.num)
      // }
    },
    incrementWait() {
      this.$store.dispatch('addWait', this.num)
      // setTimeout(() => {
      //   this.$store.dispatch('add', this.num)
      // }, 1000)
    }

  }
}
</script>

<template>
  <div>
    <h1>当前和为:{{ $store.state.count }}</h1>
    <select v-model.number="num">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="incrementOdd">当前和为奇数,则执行加法</button>
    <button @click="incrementWait">等一等再加</button>
  </div>

</template>

<style scoped>
button  {
  margin: 0 10px;
}
</style>

3.5 getters的使用

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

  2. store.js中追加getters配置

    ......
    
    const getters = {
        bigSum(state){
            return state.sum * 10
        }
    }
    
    //创建并暴露store
    export default new Vuex.Store({
        ......
        getters
    })
    
  3. 组件中读取数据:$store.getters.bigSum

3.6 四个map方法的使用

导入

import {mapState, mapGetters, mapActions, mapMutations} from 'vuex'
  1. mapState方法:用于帮助我们映射state中的数据为计算属性

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

    computed: {
        //借助mapGetters生成计算属性:bigSum(对象写法)
        ...mapGetters({bigSum:'bigSum'}),
    
        //借助mapGetters生成计算属性:bigSum(数组写法)
        ...mapGetters(['bigSum'])
    },
    
  3. mapActions方法:用于帮助我们生成与actions对话的方法,即:包含$store.dispatch(xxx)的函数

    methods:{
        //靠mapActions生成:incrementOdd、incrementWait(对象形式)
        ...mapActions({incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
    
        //靠mapActions生成:incrementOdd、incrementWait(数组形式)
        ...mapActions(['jiaOdd','jiaWait'])
    }
    
  4. mapMutations方法:用于帮助我们生成与mutations对话的方法,即:包含$store.commit(xxx)的函数

    methods:{
        //靠mapActions生成:increment、decrement(对象形式)
        ...mapMutations({increment:'JIA',decrement:'JIAN'}),
        
        //靠mapMutations生成:JIA、JIAN(对象形式)
        ...mapMutations(['JIA','JIAN']),
    }
    

备注:mapActions与mapMutations使用时,若需要传递参数需要:在模板中绑定事件时传递好参数,否则传的参数是事件对象(event)。

Count.vue

<script>
import { mapState,mapGetters,mapMutations,mapActions } from 'vuex'
export default {
  name: "MyCount",
  data() {
    return {
      num: 1
    }
  },
  computed: {
    //手动写计算属性,
    // count() {
    //   return this.$store.state.count
    // },
    // evenOrOdd() {
    //   return this.$store.getters.evenOrOdd
    // },
    // race() {
    //   return this.$store.state.race
    // },
    // score() {
    //   return this.$store.state.score
    // },

    //使用mapState生成计算属性,从state中获取
    //...(展开运算符):因为 mapState 返回的是一个对象,使用 ... 可以将其展开并合并到组件的 computed 属性中。
 /*   ...mapState({
      count: 'count',  // 将 Vuex 的 `state.count` 映射到组件的 `count` 计算属性
      race: 'race',    // 将 Vuex 的 `state.race` 映射到组件的 `race` 计算属性
      score: 'score'   // 将 Vuex 的 `state.score` 映射到组件的 `score` 计算属性
    }),
*/
    //可以简写,因为key和value相同.数组写法
    ...mapState(['count', 'race', 'score']),

    // bigCount() {
    //   return this.$store.getters.bigCount
    // },
    ...mapGetters(['bigCount', 'evenOrOdd'])

  },
  methods: {
    // increment() {
    //   //没有业务逻辑,直接提交
    //   this.$store.commit('ADD', this.num)
    // },
    // decrement() {
    //   //this.$store.dispatch('minus', this.num)
    //   this.$store.commit('MINUS', this.num)
    // },
    //  使用mapMutations生成方法,  mapMutations会返回一个对象,对象中的方法会映射成组件的methods.注意这个时候需要传参,应为生成的函数是带参数的。
    ...mapMutations({increment: 'ADD',  decrement: 'MINUS'}),

    //数组写法,注意,生成的函数名字是ADD,MINUS,组件的点击事件函数名要修改,如@click="ADD(num)"
   // ...mapMutations(['ADD', 'MINUS']),
   //  incrementOdd() {
   //    //业务逻辑,写在store中
   //    this.$store.dispatch('addOdd', this.num)
   //  },
   //  incrementWait() {
   //    this.$store.dispatch('addWait', this.num)
   //  }

    ...mapActions({incrementOdd: 'addOdd', incrementWait: 'addWait'}),
    //数组写法,对应点击函数也要改
   // ...mapActions(['addOdd', 'addWait'])
  },
  mounted() {
    console.log(mapState({ss: 'count'}))
  }
}
</script>

<template>
  <div>
    <h1>我在{{ race }}赢得了{{ score }}分好成绩!你也来试试吧!</h1>
    <h1>当前和为:{{ count }}</h1>
    <h1>当前和是:{{ evenOrOdd }}</h1>
    <h1>当前和扩大10倍是:{{ bigCount }}</h1>
    <select v-model.number="num">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="increment(num)">+</button>
    <button @click="decrement(num)">-</button>
    <button @click="incrementOdd(num)">当前和为奇数,则执行加法</button>
    <button @click="incrementWait(num)">等一等再加</button>
  </div>

</template>

<style scoped>
button  {
  margin: 0 10px;
}
</style>

3.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
      }
    })
    
  3. 开启命名空间后,组件中读取state数据:

    //方式一:自己直接读取
    this.$store.state.personAbout.list
    //方式二:借助mapState读取:
    // 用 mapState 取 countAbout 中的state 必须加上 'countAbout'
    ...mapState('countAbout',['sum','school','subject']),
    
  4. 开启命名空间后,组件中读取getters数据:

    //方式一:自己直接读取
    this.$store.getters['personAbout/firstPersonName']
    //方式二:借助mapGetters读取:
    ...mapGetters('countAbout',['bigSum'])
    
  5. 开启命名空间后,组件中调用dispatch

    //方式一:自己直接dispatch
    this.$store.dispatch('personAbout/addPersonWang',person)
    //方式二:借助mapActions:
    ...mapActions('countAbout',{incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
    
  6. 开启命名空间后,组件中调用commit

    //方式一:自己直接commit
    this.$store.commit('personAbout/ADD_PERSON',person)
    //方式二:借助mapMutations:
    ...mapMutations('countAbout',{increment:'JIA',decrement:'JIAN'}),
    

模块拆分后,可以单独写count.js,person.js,然后export,在index.js中import。

count.js

export default {
    namespaced: true,//命名空间,默认为false,为true,mapState,mapMutations,mapGetters,mapActions会自动加上该模块的namespace
    actions: {
        addOdd(context, value) {
            if (context.state.count % 2) {
                context.commit('ADD', value)
            }
        },
        addWait(context, value) {
            setTimeout(() => {
                context.commit('ADD', value)
            }, 1000)
        }
    },
    mutations: {
        ADD(state, value) {
            state.count += value
        },
        MINUS(state, value) {
            state.count -= value
        }
    },
    state: {
        count: 0,
        race: "bcym",
        score: 100
    },
    getters: {
        evenOrOdd(state) {
            return state.count % 2 ? '奇数' : '偶数'
        },
        bigCount(state) {
            return state.count * 10
        }
    }
}

person.js

import axios from 'axios'
import { nanoid } from 'nanoid'
export default {
    namespaced: true,
    actions: {
        addPersonBc(context, value) {
            //只允许添加bc开头的
            if(value.name.indexOf('bc')===0){
                context.commit('ADD_PERSON', value)
            }else {
                alert('bc开头的才被允许添加')
            }
        },

        addPersonServer(context) {
            axios.get('https://www.klapi.cn/api/yiyan.php?type=txt').then(
                res => {
                    context.commit('ADD_PERSON', {id: nanoid(), name: res.data})
                },
                err => {
                    alert('请求失败:',  err.message)
                }
            )

        }
    },
    mutations: {
        ADD_PERSON(state, value) {
            state.personList.unshift(value)
        }
    },
    state: {
        personList: [
            {id: '001', name: '张三'},
            {id: '002', name: '李四'},
            {id: '003', name: '王五'},
            {id: '004', name: 'bcym'}
        ]
    },
    getters: {
        firstPersonName(state) {
            return state.personList[0].name
        }
    }
}

4. 路由

  1. 理解: 一个路由(route)就是一组映射关系(key - value),多个路由需要路由器(router)进行管理。
  2. 前端路由:key是路径,value是组件。

4.1 基本使用

  1. 安装vue-router,命令:npm i vue-router

  2. 应用插件:Vue.use(VueRouter)

  3. 编写router配置项:

    //引入VueRouter
    import VueRouter from 'vue-router'
    //引入路由 组件
    import About from '../components/About'
    import Home from '../components/Home'
    
    //创建router实例对象,去管理一组一组的路由规则
    export default new VueRouter({
        routes:[
            {
                path:'/about',
                component:About
            },
            {
                path:'/home',
                component:Home
            }
        ]
    })
    
  4. 实现切换(active-class可配置高亮样式)

    <router-link active-class="active" to="/about">About</router-link>
    
  5. 指定展示位置

    <router-view></router-view>
    

4.2 几个注意点

  1. 路由组件通常存放在pages文件夹,一般组件通常存放在components文件夹。
  2. 通过切换,“隐藏”了的路由组件,默认是被销毁掉的,需要的时候再去挂载。
  3. 每个组件都有自己的$route属性,里面存储着自己的路由信息。
  4. 整个应用只有一个router,可以通过组件的$router属性获取到。

4.3 多级路由(嵌套路由)

  1. 配置路由规则,使用children配置项:

    routes:[
        {
            path:'/about',
            component:About,
        },
        {
            path:'/home',
            component:Home,
            children:[ //通过children配置子级路由
                {
                    path:'news', //此处一定不要写:/news
                    component:News
                },
                {
                    path:'message',//此处一定不要写:/message
                    component:Message
                }
            ]
        }
    ]
    
  2. 跳转(要写完整路径):

    <router-link to="/home/news">News</router-link>
    
  3. 指定展示位置

    <router-view></router-view>
    

4.4 路由的query参数

  1. 传递参数

    <!-- 跳转并携带query参数,to的字符串写法 -->
    <router-link :to="/home/message/detail?id=666&title=你好">跳转</router-link>
                    
    <!-- 跳转并携带query参数,to的对象写法 -->
    <router-link 
        :to="{
            path:'/home/message/detail',
            query:{
               id:666,
                title:'你好'
            }
        }"
    >跳转</router-link>
    
  2. 接收参数:

    $route.query.id
    $route.query.title
    

4.5 命名路由

  1. 作用:可以简化路由的跳转。

  2. 如何使用

    1. 给路由命名:

      {
          path:'/demo',
          component:Demo,
          children:[
              {
                  path:'test',
                  component:Test,
                  children:[
                      {
                            name:'hello' //给路由命名
                          path:'welcome',
                          component:Hello,
                      }
                  ]
              }
          ]
      }
      
    2. 简化跳转:

      <!--简化前,需要写完整的路径 -->
      <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>
      

4.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
                    }
                ]
            }
        ]
    }
    
  2. 传递参数

    <!-- 跳转并携带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配置!

  3. 接收参数:

    $route.params.id
    $route.params.title
    

4.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,
          a: 1,
          b: 'hello'
        }
    }
}

方便在要跳转去的组件里更简便的写法

跳转去组件的具体代码

<template>
  <ul>
      <h1>Detail</h1>
      <li>消息编号:{{id}}</li>
      <li>消息标题:{{title}}</li>
      <li>a:{{a}}</li>
      <li>b:{{b}}</li>
  </ul>
</template>

<script>
export default {
    name: 'Detail',
    props: ['id', 'title', 'a', 'b'],
    mounted () {
        console.log(this.$route);
    }
}
</script>

<style>

</style>

4.8 <router-link>的replace属性

  1. 作用:控制路由跳转时操作浏览器历史记录的模式
  2. 浏览器的历史记录有两种写入方式:分别为push(栈结构)和replacepush是追加历史记录,replace是替换当前记录。路由跳转时候默认为push
  3. 如何开启replace模式:<router-link replace .......>News</router-link>

4.8 <router-link>的replace属性

  1. 作用:控制路由跳转时操作浏览器历史记录的模式
  2. 浏览器的历史记录有两种写入方式:分别为pushreplacepush是追加历史记录,replace是替换当前记录。路由跳转时候默认为push
  3. 如何开启replace模式:<router-link replace .......>News</router-link>

4.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() //可前进也可后退
    

4.10 缓存路由组件

  1. 作用:让不展示的路由组件保持挂载,不被销毁。

  2. 具体编码:

    这个 include 指的是组件名

    <keep-alive include="News"> 
        <router-view></router-view>
    </keep-alive>
    

4.11 两个新的生命周期钩子

作用:路由组件所独有的两个钩子,用于捕获路由组件的激活状态。 具体名字:

  • activated路由组件被激活时触发。
  • deactivated路由组件失活时触发。

这两个生命周期钩子需要配合前面的缓存路由组件使用(没有缓存路由组件不起效果)

activated() {
  console.log('news组件被激活了');
  this.timer = setInterval(() => {
    this.opacity -= 0.01;
    if (this.opacity <= 0) {
      this.opacity = 1;
    }
  }, 18);
},
deactivated() {
  console.log('news组件失活了');
  clearInterval(this.timer);
}

4.12 路由守卫

  1. 作用:对路由进行权限控制

  2. 分类:全局守卫、独享守卫、组件内守卫

  3. 全局守卫:

    //全局前置路由守卫————初始化时、每次路由切换之前调用
    router.beforeEach((to,from,next)=>{
        console.log(to,from)
        //  if(to.name === 'message' || to.name === 'news'){}。用name判断也行
        // 如果to.path等于/home/message或者/home/news,则跳转到/home,否则放行
        //to.path === '/home/message' || to.path === '/home/news'
        if(to.meta.isAuth){
            if(localStorage.getItem('user')==='zqg'){
                //放行
                next()
            }else {
                alert('user不是zqg,无权限。')
            }
        }else{
            //放行
            next()
        }
    })
    
    //全局后置路由守卫————初始化时、每次路由切换之后调用
    router.afterEach((to,from)=>{
        console.log(to,from)
        document.title = to.meta.title || 'Vue_Router'
    
    })
    

    完整代码

//引入VueRouter
import VueRouter from 'vue-router'
//引入路由 组件
import About from '../pages/About.vue'
import Home from '../pages/Home.vue'

//创建router实例对象,去管理一组一组的路由规则
const router = new VueRouter({
    routes:[
        {
            name:'about',
            path:'/about',
            component:About,
            meta:{title:'关于'}
        },
        {
            name:'home',
            path:'/home',
            component:Home,
            meta:{title:'主页'},
            children:[
                {
                    path:'news', //注意这里的path不要写/
                    component:()=>import('../pages/News.vue'),
                    meta:{isAuth:true,title:'新闻'}
                },
                {
                    path:'message',
                    component:()=>import('../pages/Message.vue'),
                    meta:{isAuth:true,title:'消息'},
                    children:[
                        {
                            name:'detail',
                            path:'detail/:id/:title',
                            component:()=>import('../pages/Detail.vue'),
                            //  props的第一种写法,值为对象,该对象中的所有key-value都会以props的形式传给Detail组件
                           /* props:{
                                a:1,
                                b:'hello'
                            }*/
                            //  props的第二种写法,值为布尔值,若布尔值为真,就会把该路由组件收到的所有params参数,以props的形式传给Detail组件
                            //props:true,

                            // props的第三种写法,值为函数,params和query都适用。该对象中的所有key-value都会以props的形式传给Detail组件
                            props($route){
                                return {
                                    id:$route.params.id,
                                    title:$route.params.title
                                }
                            }
                        }
                    ]
                }
            ]
        }
    ]
})
//全局前置路由守卫————初始化时、每次路由切换之前调用
router.beforeEach((to,from,next)=>{
    console.log(to,from)
    //  if(to.name === 'message' || to.name === 'news'){}。用name判断也行
    // 如果to.path等于/home/message或者/home/news,则跳转到/home,否则放行
    //to.path === '/home/message' || to.path === '/home/news'
    if(to.meta.isAuth){
        if(localStorage.getItem('user')==='zqg'){
            //放行
            next()
        }else {
            alert('user不是zqg,无权限。')
        }
    }else{
        //放行
        next()
    }
})

//全局后置路由守卫————初始化时、每次路由切换之后调用
router.afterEach((to,from)=>{
    console.log(to,from)
    document.title = to.meta.title || 'Vue_Router'

})
export default router

4.独享守卫:

就是在 routes 子路由内写守卫,只对单独的路由起作用

    //独享路由守卫
                    beforeEnter:(to,from,next)=>{
                        if(to.meta.isAuth){
                            if(localStorage.getItem('user')==='zqg'){
                                //放行
                                next()
                            }else {
                                alert('user不是zqg,无权限。')
                            }
                        }else{
                            //放行
                            next()
                        }
                    }

5.组件内守卫:

在具体组件内写守卫

//进入守卫:通过路由规则,进入该组件时被调用
beforeRouteEnter (to, from, next) {
},
//离开守卫:通过路由规则,离开该组件时被调用
beforeRouteLeave (to, from, next) {
}

4.13 路由器的两种工作模式

  1. 对于一个url来说,什么是hash值?—— #及其后面的内容就是hash值。
  2. hash值不会包含在 HTTP 请求中,即:hash值不会带给服务器。
  3. hash模式:
    1. 地址中永远带着#号,不美观 。
    2. 若以后将地址通过第三方手机app分享,若app校验严格,则地址会被标记为不合法。
    3. 兼容性较好。
  4. history模式:
    1. 地址干净,美观 。
    2. 兼容性和hash模式相比略差。
    3. 应用部署上线时需要后端人员支持,解决刷新页面服务端404的问题。

5.Vue UI组件库

5.1 移动端常用UI组件库

  1. Vant https://youzan.github.io/vant

  2. CubeUI https://didi.github.io/cube-ui

  3. Mint UI http://mint-ui.github.io

5.2 PC端常用UI组件库

  1. Element UI https://element.eleme.cn
  2. IView UI https://www.iviewui.com

Vue3快速上手

1.Vue3简介

2.Vue3带来了什么

1.性能的提升

  • 打包大小减少41%

  • 初次渲染快55%, 更新渲染快133%

  • 内存减少54%

    ……

2.源码的升级

  • 使用Proxy代替defineProperty实现响应式

  • 重写虚拟DOM的实现和Tree-Shaking

    ……

3.拥抱TypeScript

  • Vue3可以更好的支持TypeScript

4.新的特性

  1. Composition API(组合API)

    • setup配置
    • ref与reactive
    • watch与watchEffect
    • provide与inject
    • ……
  2. 新的内置组件

    • Fragment
    • Teleport
    • Suspense
  3. 其他改变

    • 新的生命周期钩子
    • data 选项应始终被声明为一个函数
    • 移除keyCode支持作为 v-on 的修饰符
    • ……

一、创建Vue3.0工程

1.使用 vue-cli 创建

官方文档:https://cli.vuejs.org/zh/guide/creating-a-project.html#vue-create

## 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version
## 安装或者升级你的@vue/cli
npm install -g @vue/cli
## 创建
vue create vue_test
## 启动
cd vue_test
npm run serve

2.使用 vite 创建

官方文档:https://v3.cn.vuejs.org/guide/installation.html#vite

vite官网:https://vitejs.cn

  • 什么是vite?—— 新一代前端构建工具。
  • 优势如下:
    • 开发环境中,无需打包操作,可快速的冷启动。
    • 轻量快速的热重载(HMR)。
    • 真正的按需编译,不再等待整个应用编译完成。
  • 传统构建 与 vite构建对比图

image-20250615185843691

image-20250615185958852

## 创建工程
npm init vite-app <project-name>
## 进入工程目录
cd <project-name>
## 安装依赖
npm install
## 运行
npm run dev

3.分析文件目录

main.js

Vue2项目的main.js

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
}).$mount('#app')

我们再来看看Vue3项目中的main.js

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

分析一下

// 引入的不再是Vue构造函数了,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'

// 创建应用实例对象——app(类似于之前Vue2中的vm,但app比vm更“轻”)
const app = createApp(App)
console.log(app)
// 挂载
app.mount('#app')

App.vue

我们再来看看组件

template标签里可以没有根标签了

<template>
    <!-- Vue3组件中的模板结构可以没有根标签 -->
    <img alt="Vue logo" src="./assets/logo.png">
    <HelloWorld msg="Welcome to Your Vue.js App"/>
</template>

二、常用 Composition API

官方文档: https://v3.cn.vuejs.org/guide/composition-api-introduction.html

1.拉开序幕的setup

  1. 理解:Vue3.0中一个新的配置项,值为一个函数。
  2. setup是所有Composition API(组合API)“ 表演的舞台 ”
  3. 组件中所用到的:数据、方法等等,均要配置在setup中。
  4. setup函数的两种返回值:
    1. 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)
    2. 若返回一个渲染函数:则可以自定义渲染内容。(了解)
  5. 注意点:
    1. 尽量不要与Vue2.x配置混用
      • Vue2.x配置(data、methos、computed…)中可以访问到setup中的属性、方法。
      • 但在setup中不能访问到Vue2.x配置(data、methos、computed…)。
      • 如果有重名, setup优先。
    2. setup不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)

2.ref函数

  • 作用: 定义一个响应式的数据
  • 语法: const xxx = ref(initValue)
    • 创建一个包含响应式数据的引用对象(reference对象,简称ref对象)
    • JS中操作数据: xxx.value
    • 模板中读取数据: 不需要.value,直接:<div>{{xxx}}</div>
  • 备注:
    • 接收的数据可以是:基本类型、也可以是对象类型。
    • 基本类型的数据:响应式依然是靠Object.defineProperty()getset完成的。
    • 对象类型的数据:内部 “ 求助 ” 了Vue3.0中的一个新函数—— reactive函数。

3.reactive函数

  • 作用: 定义一个对象类型的响应式数据(基本类型不要用它,要用ref函数)
  • 语法:const 代理对象= reactive(源对象)接收一个对象(或数组),返回一个代理对象(Proxy的实例对象,简称proxy对象)
  • reactive定义的响应式数据是“深层次的”。
  • 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。

4.Vue3.0中的响应式原理

vue2.x的响应式

  • 实现原理:

    • 对象类型:通过Object.defineProperty()对属性的读取、修改进行拦截(数据劫持)。

    • 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。

      Object.defineProperty(data, 'count', {
          get () {}, 
          set () {}
      })
      
  • 存在问题:

    • 新增属性、删除属性, 界面不会更新。
    • 直接通过下标修改数组, 界面不会自动更新。

Vue3.0的响应式

5.reactive对比ref

  • 从定义数据角度对比:
    • ref用来定义:基本类型数据
    • reactive用来定义:对象(或数组)类型数据
    • 备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过reactive转为代理对象
  • 从原理角度对比:
    • ref通过Object.defineProperty()getset来实现响应式(数据劫持)。
    • reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
  • 从使用角度对比:
    • ref定义的数据:操作数据需要.value,读取数据时模板中直接读取不需要.value
    • reactive定义的数据:操作数据与读取数据:均不需要.value

6.setup的两个注意点

  • setup执行的时机

    • 在beforeCreate之前执行一次,this是undefined。
  • setup的参数

    • props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。
    • context:上下文对象
      • attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于 this.$attrs
      • slots: 收到的插槽内容, 相当于 this.$slots
      • emit: 分发自定义事件的函数, 相当于 this.$emit

7.计算属性与监视

1.computed函数

  • 与Vue2.x中computed配置功能一致

  • 写法

    import {computed} from 'vue'
    
    setup(){
        ...
        //计算属性——简写
        let fullName = computed(()=>{
            return person.firstName + '-' + person.lastName
        })
        //计算属性——完整
        let fullName = computed({
            get(){
                return person.firstName + '-' + person.lastName
            },
            set(value){
                const nameArr = value.split('-')
                person.firstName = nameArr[0]
                person.lastName = nameArr[1]
            }
        })
    }
    

2.watch函数

  • 与Vue2.x中watch配置功能一致

  • 两个小“坑”:

    • 监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)。
    • 监视reactive定义的响应式数据中某个属性时:deep配置有效。
    //情况一:监视ref定义的响应式数据
    watch(sum,(newValue,oldValue)=>{
        console.log('sum变化了',newValue,oldValue)
    },{immediate:true})
    
    //情况二:监视多个ref定义的响应式数据
    watch([sum,msg],(newValue,oldValue)=>{
        console.log('sum或msg变化了',newValue,oldValue)
    }) 
    
    /* 情况三:监视reactive定义的响应式数据
                若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue!!
                若watch监视的是reactive定义的响应式数据,则强制开启了深度监视 
    */
    watch(person,(newValue,oldValue)=>{
        console.log('person变化了',newValue,oldValue)
    },{immediate:true,deep:false}) //此处的deep配置不再奏效
    
    //情况四:监视reactive定义的响应式数据中的某个属性
    watch(()=>person.job,(newValue,oldValue)=>{
        console.log('person的job变化了',newValue,oldValue)
    },{immediate:true,deep:true}) 
    
    //情况五:监视reactive定义的响应式数据中的某些属性
    watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{
        console.log('person的job变化了',newValue,oldValue)
    },{immediate:true,deep:true})
    
    //特殊情况
    watch(()=>person.job,(newValue,oldValue)=>{
        console.log('person的job变化了',newValue,oldValue)
    },{deep:true}) //此处由于监视的是reactive素定义的对象中的某个属性,所以deep配置有效
    

3.watchEffect函数

  • watch的套路是:既要指明监视的属性,也要指明监视的回调。

  • watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。

  • watchEffect有点像computed:

    • 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
    • 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
    //watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
    watchEffect(()=>{
        const x1 = sum.value
        const x2 = person.age
        console.log('watchEffect配置的回调执行了')
    })
    

8.生命周期

vue2.x的生命周期lifecycle_2
vue3.0的生命周期lifecycle_2
  • Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有有两个被更名:
    • beforeDestroy改名为 beforeUnmount
    • destroyed改名为 unmounted
  • Vue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:
    • beforeCreate===>setup()
    • created=======>setup()
    • beforeMount ===>onBeforeMount
    • mounted=======>onMounted
    • beforeUpdate===>onBeforeUpdate
    • updated =======>onUpdated
    • beforeUnmount ==>onBeforeUnmount
    • unmounted =====>onUnmounted

9.自定义hook函数

  • 什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装。

  • 类似于vue2.x中的mixin。

  • 自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。

10.toRef

  • 作用:创建一个 ref 对象,其value值指向另一个对象中的某个属性。

  • 语法:const name = toRef(person,'name')

  • 应用: 要将响应式对象中的某个属性单独提供给外部使用时。

  • 扩展:toRefstoRef功能一致,但可以批量创建多个 ref 对象,语法:toRefs(person)

三、其它 Composition API

1.shallowReactive 与 shallowRef

  • shallowReactive:只处理对象最外层属性的响应式(浅响应式)。

  • shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。

  • 什么时候使用?

    • 如果有一个对象数据,结构比较深, 但变化时只是外层属性变化 ===> shallowReactive。
    • 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生新的对象来替换 ===> shallowRef。

2.readonly 与 shallowReadonly

  • readonly: 让一个响应式数据变为只读的(深只读)。
  • shallowReadonly:让一个响应式数据变为只读的(浅只读)。
  • 应用场景: 不希望数据被修改时。

3.toRaw 与 markRaw

  • toRaw:
    • 作用:将一个由reactive生成的响应式对象转为普通对象
    • 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
  • markRaw:
    • 作用:标记一个对象,使其永远不会再成为响应式对象。
    • 应用场景:
      1. 有些值不应被设置为响应式的,例如复杂的第三方类库等。
      2. 当渲染具有不可变数据源的大列表时,跳过响应式转换可以提高性能。

4.customRef

  • 作用:创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。

  • 实现防抖效果:

    <template>
        <input type="text" v-model="keyword">
        <h3>{{keyword}}</h3>
    </template>
    
    <script>
        import {ref,customRef} from 'vue'
        export default {
            name:'Demo',
            setup(){
                // let keyword = ref('hello') //使用Vue准备好的内置ref
                //自定义一个myRef
                function myRef(value,delay){
                    let timer
                    //通过customRef去实现自定义
                    return customRef((track,trigger)=>{
                        return{
                            get(){
                                track() //告诉Vue这个value值是需要被“追踪”的
                                return value
                            },
                            set(newValue){
                                clearTimeout(timer)
                                timer = setTimeout(()=>{
                                    value = newValue
                                    trigger() //告诉Vue去更新界面
                                },delay)
                            }
                        }
                    })
                }
                let keyword = myRef('hello',500) //使用程序员自定义的ref
                return {
                    keyword
                }
            }
        }
    </script>
    

5.provide 与 inject

  • 作用:实现祖与后代组件间通信

  • 套路:父组件有一个 provide 选项来提供数据,后代组件有一个 inject 选项来开始使用这些数据

  • 具体写法:

    1. 祖组件中:

      setup(){
          ......
          let car = reactive({name:'奔驰',price:'40万'})
          provide('car',car)
          ......
      }
      
    2. 后代组件中:

      setup(props,context){
          ......
          const car = inject('car')
          return {car}
          ......
      }
      

6.响应式数据的判断

  • isRef: 检查一个值是否为一个 ref 对象
  • isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
  • isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
  • isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

四、Composition API 的优势

1.Options API 存在的问题

使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 。

2.Composition API 的优势

我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。

五、新的组件

1.Fragment

  • 在Vue2中: 组件必须有一个根标签
  • 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
  • 好处: 减少标签层级, 减小内存占用

2.Teleport

  • 什么是Teleport?—— Teleport 是一种能够将我们的组件html结构移动到指定位置的技术。

    <teleport to="移动位置">
        <div v-if="isShow" class="mask">
            <div class="dialog">
                <h3>我是一个弹窗</h3>
                <button @click="isShow = false">关闭弹窗</button>
            </div>
        </div>
    </teleport>
    

3.Suspense

  • 等待异步组件时渲染一些额外内容,让应用有更好的用户体验

  • 使用步骤:

    • 异步引入组件

      import {defineAsyncComponent} from 'vue'
      const Child = defineAsyncComponent(()=>import('./components/Child.vue'))
      
    • 使用Suspense包裹组件,并配置好defaultfallback

      <template>
          <div class="app">
              <h3>我是App组件</h3>
              <Suspense>
                  <template v-slot:default>
                      <Child/>
                  </template>
                  <template v-slot:fallback>
                      <h3>加载中.....</h3>
                  </template>
              </Suspense>
          </div>
      </template>
      

六、其他

1.全局API的转移

  • Vue 2.x 有许多全局 API 和配置。

    • 例如:注册全局组件、注册全局指令等。

      //注册全局组件
      Vue.component('MyButton', {
        data: () => ({
          count: 0
        }),
        template: '<button @click="count++">Clicked {{ count }} times.</button>'
      })
      
      //注册全局指令
      Vue.directive('focus', {
        inserted: el => el.focus()
      }
      
  • Vue3.0中对这些API做出了调整:

    • 将全局的API,即:Vue.xxx调整到应用实例(app)上

      2.x 全局 API(Vue 3.x 实例 API (app)
      Vue.config.xxxx app.config.xxxx
      Vue.config.productionTip 移除
      Vue.component app.component
      Vue.directive app.directive
      Vue.mixin app.mixin
      Vue.use app.use
      Vue.prototype app.config.globalProperties

2.其他改变

  • data选项应始终被声明为一个函数。

  • 过度类名的更改:

    • Vue2.x写法

      .v-enter,
      .v-leave-to {
        opacity: 0;
      }
      .v-leave,
      .v-enter-to {
        opacity: 1;
      }
      
    • Vue3.x写法

      .v-enter-from,
      .v-leave-to {
        opacity: 0;
      }
      
      .v-leave-from,
      .v-enter-to {
        opacity: 1;
      }
      
  • 移除keyCode作为 v-on 的修饰符,同时也不再支持config.keyCodes

  • 移除v-on.native修饰符

    • 父组件中绑定事件

      <my-component
        v-on:close="handleComponentEvent"
        v-on:click="handleNativeClickEvent"
      />
      
    • 子组件中声明自定义事件

      <script>
        export default {
          emits: ['close']
        }
      </script>
      
  • 移除过滤器(filter)

    过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。

  • ……