LOADING

MiniKano的小窝


 

【已完结】Vue学习笔记

简介

Vue是一个动态构建用户界面的渐进式 Javascript 框架

特点

  1. 遵循mvvm模式
  2. 编码简洁,体积小,运行效率高,适合移动/pc 端开发
  3. 它本身只关注UI,也可以引入其他第三方库开发项目

浅尝一下

引入Vue.js

<script src="https://cn.vuejs.org/js/vue.js"></script>

HTML容器:

<!-- 准备一个容器 -->
<div id="root">
    <!-- 插值 -->
    <p>Hello {{name}}</p>
    <p>事{{name2}}捏</p>
</div>

Vue脚本:

//创建Vue实例
const x = new Vue({
    //配置对象
    el: '#root', //el 用于指定当前Vue实例为哪个容器服务,css选择器格式
    data: { //data 中用于指定存储的数据,数据供el所指定的容器去使用
        name: 'kanokano',
        name2: '嘉然'
    }
});

Vue的工作条件

  • 想让Vue工作,就必须创建一个Vue实例,且要传入一个配置对象
  • root容器里的代码依然符合html的规范标准,只是混入了vue的一些语法
  • root容器里的代码被称为Vue模板
  • 一个容器对应一个Vue实例,反之亦然,Vue实例配合组件一起使用
  • {{}}中需要写的是js表达式,且变量可以自动读取到data中所有的属性
  • 一旦data中的数据发生改变,模板(页面)中的目标也会自动更新

模板语法

插值语法

插值语法直接在html标签体内使用Vue插值模板,填入data中的属性即可

<div id="root">
    <!-- 插值 -->
    <p>Hello {{name}}</p>
</div>
//创建Vue实例
const x = new Vue({
    //配置对象
    el: '#root', 
    data: { 
        name: 'kanokano',
    }
});

指令语法

指令语法以 v- 开头,用于解析标签(包括标签属性,标签体内容,绑定事件等)

举例 (v-bind)

使用v-bind:来模板化href属性

<div id="root">
    <a v-bind:href="url">kanokano的博客</a>
</div>
 const x = new Vue({
     //配置对象
     el: '#root', 
     data: {
         url: 'https://kanokano.cn'
     }
 });

有时,v-bind: 也可以简写为 :

<div id="root">
    <a :href="url">kanokano的博客</a>
</div>

总结:标签体内用插值语法,标签属性用指令语法

数据绑定

由于v-bind是单向的数据绑定,只能通过对象传递到DOM

有了单向数据绑定这个概念,那就一定会有双向数据绑定

双向数据绑定使用:v-model:

<span>单向数据绑定:</span><input type="text" :value="name2"><br>
<span>双向数据绑定:</span><input type="text" v-model:value="name2">

注意,以下代码是错误的

<h1 v-model:data="name"></h1>

这是因为 v-model 只支持表单类等输入型元素内

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

<span>双向数据绑定:</span><input type="text" v-model="name2">

el与data的两种写法

el的两种写法:

对象属性

const v = new Vue({
    el: '#root',
    data: {
        text: "你好呀"
    }
});

使用实例对象的原型对象上的$mount方法挂载元素

const v = new Vue({
    data: {
        text: "你好呀"
    }
});
v.$mount('#root')

data的两种写法

对象式:

data: {
    text: "你好呀"
}

函数式:

 data: function(){
     //此处的this式Vue的实例对象
     console.log(this)
     return {
         name:'你好呀'
     }
 }

注意,函数写法不可以是箭头函数,因为会扰乱this的指向(原本this指向一个Vue对象:鹿乃_打扰了:

//函数式可适用于vue组件

MVVM模型

  • M:模型(Model): 对应 data中的数据
  • V:视图(View): 模板
  • VM:视图模型(ViewModel): vue实例对象

image-20220802003004189

<body>
    <!-- View -->
    <div id="root">
        <p>{{kano}}</p>
    </div>
    <script>
        new Vue({ //ViewModel
            el: '#root',
            data: { //Model
                kano: "kano"
            }
        });
    </script>
</body>

数据代理

Object.defineproperty 方法

此方法可以设置一个对象的属性,该方法有三个参数,区别于直接赋值,该方法的作用更多,更高级

简单用法:

let singer = {
    name: 'kano'
}
Object.defineProperty(singer, 'age', {
    value: 8
})
console.log(singer);

这样就能给一个对象添加属性了,但是你会发现,这个属性居然不能修改,也不能删除,更不能被遍历出来!

其实是因为 Object.defineProperty 方法默认创建出来的属性,他的 writable, enumerable, configurable的值都为false.

console.log(Object.getOwnPropertyDescriptors(singer)); 
//该方法返回指定对象所有 自身属性 的描述对象

想要解决无法修改,配置,遍历属性的问题,其实很简单,只需要在第三个参数中指定属性的特征即可:

Object.defineProperty(singer, 'age', {
    value: 8,
    enumerable: true, //控制属性是否可被枚举
    writable: true, //控制属性是否可被修改
    configurable: true //控制属性是否可被删除
});
singer.age=11;
console.log(singer);

以上代码配置了可修改,可配置,可被枚举的属性特征,所以是可以遍历且值可以被修改删除

getter与setter

如标题所言,JavaScript已经开始逐渐像java/c#类这样的面向对象语言靠拢了(喜)

直接看使用方法:

//数据代理
let msg = 'hello~';
Object.defineProperty(singer, 'msg', {
    get: () => {
        console.log('调用了getter');
        return msg
    },
    set: value => {
        console.log('调用了setter');
        msg = value
    }
});
console.log(singer);

当读取msg属性的时候,get访问器就会自动调用函数获取相应的值,返回的值就是msg属性的值
修改(set访问器)也是一样,有java/C#类语言的基础会更好理解

这样,通过get访问器就可以实现代理msg变量了,实现了数据的双向/单向同步

总结:Object.defineProperty 是一个比较高级的给对象添加属性的方法,不仅增加了对象属性的安全性,也可以让我们更灵活使用对象中的属性,更重要的是 这个方法可以设置访问代理,Vue的数据双向绑定就是依照这个方法构建的

数据代理实例

我们可以通过一个简单的例子来实现数据代理:

let obj = {
    a: 10
};
let objj = {
    b: 20
};

Object.defineProperty(objj, 'a', {
    get: () => obj.a,
    set: (value) => {
        obj.a = value
    }
})
console.log(objj.a);
console.log(obj.a);
obj.a = 111;
console.log(objj.a);
console.log(obj.a);

Vue中的数据代理

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

原理图:

image-20220803221650284

示例代码:

let data = {
    name: 'kano',
    age: 10
}
var vm = new Vue({
    el: '#root',
    data
});
console.log(data === vm._data); //true
vm.name = "kanokano"
console.log(data.name, vm._data.name); //一致

事件处理

事件的基本使用

  1. 使用v-on:xxx 或 @xxx 绑定事件,其中xxx是事件名
  2. 事件的回调需要配置在methods对象中,最终会在vm上
  3. methods中配置的函数不能使用箭头函数,会造成this指向错误
  4. methods中配置的函数,都是被Vue所管理的函数,this的指向是 vm 或 组件的实例对象
  5. @click 中的字符串可以使用函数写法进行传参,@click="fun"@click=”fun($event)“是一样的
  6. 函数传参的时候如有多个参数,会造成event无法使用,可以在实参列表中,添加$event 进行占位,就可以使用了

代码演示

<div id="root">
    <h1>{{hello}}</h1>
    <button v-on:click="showInfo">点我</button>
    <!-- 简写 -->
    <!-- 函数没有参数时候可以省略括号 -->
    <!-- 想用event可以使用$event关键词占位 -->
    <button @click="showInfo1(123,$event)">点我点我</button>
</div>
const vm = new Vue({
    el: '#root',
    data: {
        hello: '你好!'
    },
    methods: { //methods内的方法不做数据代理
        showInfo(e) {
            alert('okk')
            console.log(this); //这里的this是vm
            console.log(e.target.innerText);
        },
        showInfo1(num, e) {
            alert(num)
            console.log(this); //这里的this是vm
            console.log(e.target.innerText);
        }
    }
});

事件修饰符

Vue中的事件修饰符:

  1. prevent:阻止默认事件(常用)
  2. stop:阻止事件冒泡(常用)
  3. once:事件只触发一次(常用)
  4. capture:使用事件捕获模式
  5. self:只有event.target 是当前操作的元素才会触发事件
  6. passive:事件的默认行为是立即执行,无需等待事件回调即可执行完毕

注意,事件修饰符是可以连用的 比如 @click.stop.prevent 先阻止冒泡。再阻止默认行为

<div id="root">
    <h1>{{hello}}</h1>
    <!-- .prevent就可以阻止默认行为 -->
    <!-- .prevent就是事件修饰符 -->
    <a href="//baidu.com" @click.prevent="showInfo">点我</a>

    <!-- 阻止事件冒泡(常用) -->
    <div @click="showInfo" style="background-color:skyblue">
        <button @click.stop="showInfo">点点点</button>
    </div>

    <!-- 一次性事件 -->
    <button @click.once="showInfo">我是一次性的</button>

    <!-- 事件捕获模式 -->
    <!-- 点击紫色盒子,先输出1再输出2 -->
    <div @click.capture="show(1)" style="background-color:skyblue;padding: 10px;">1
        <div @click.capture="show(2)" style="background-color:blueviolet;">2</div>
    </div>

    <!-- 只有event.target 是当前操作的元素才会触发事件,点击紫色盒子的时候会触发事件,此时e.target是点击的那个button,然后会触发事件冒泡 -->
    <!-- 事件冒泡到上层绑定事件的元素时,由于加了.self修饰,发现了触发事件的event不是自己,就不会触发事件操作 -->
    <div @click.self="show(1)" style="background-color:skyblue;padding: 10px;">1
        <button @click="show(2)" style="background-color:blueviolet;">2</button>
    </div>

    <!-- 事件的默认行为是立即执行,无需等待事件回调即可执行完毕 -->
    <!-- wheel是鼠标滚轮事件 -->
    <ul @wheel.passive="scrollbig">
        <li>1111</li>
        <li>2222</li>
        <li>3333</li>
        <li>4444</li>
    </ul>

</div>
const vm = new Vue({
    el: '#root',
    data: {
        hello: '你好!'
    },
    methods: { //methods内的方法不做数据代理
        showInfo(e) {
            alert('okk')
            console.log(this); //这里的this是vm
            console.log(e.target.innerText);
        },
        show(s) {
            console.log(s);
        },
        scrollbig() {
            for (var i = 0; i < 999; i++) {
                console.log('dddd');
            }
        }
    }
});

键盘事件

  1. 常用按键别名:
    • 回车 enter删除 delete (删除和退格都行
    • 退出 esc空格 space换行 tab (特殊,必须配合keydown)
    • 上 up
    • 下 down
    • 左 left
    • 右 right
  2. Vue未提供别名的按键,可以使用按键原始值的key值绑定,但要注意要转为 keybab-case 短横线小写命名
  3. 系统修饰键(特殊) ctrl alt shift 等
    1. 配合keyup使用:按下修饰键的同时,再按下其他键,随后释放,事件才能触发
    2. 配合keydown使用,正常触发事件
  4. 可以使用keyCode指定具体的按键(不建议)
  5. Vue.config.keyCodes.自定义按键名=键码,可以自定义按键别名
  6. 可以利用修饰符连写的特性,进行组合键的侦听
    <div id="root">
    <h2>{{name}}</h2>
    <input type="text" placeholder="按下回车键提示输入" @keyup.enter="into">
    <!-- 修饰符连写 -->
    <input type="text" placeholder="按下ctrl+y提示输入" @keyup.ctrl.y="into">
    </div>
    <script>
    Vue.config.keyCodes = 41;
    new Vue({
        el: '#root',
        data: {
            name: '键盘事件演示'
        },
        methods: {
            into(e) {
                console.log(e.target.value);
            }
        }
    });
    </script>

计算属性

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

代码演示

<div id="root">
    <!-- v-model 双向数据绑定 -->
    <p> 姓:<input type="text" v-model="firstname"> </p>
    <p> 名:<input type="text" v-model="lastname"></p>
    <!-- 上面的input内容只要发生变化,vue的模板都会重新解析一遍html,从而带动下面的函数的重载 -->
    <p> 姓名:<span>{{fullname}}</span></p>
</div>
const vm = new Vue({
    el: '#root',
    data: {
        firstname: '张',
        lastname: '三'
    },
    //计算属性,computed内的就是计算属性,用来处理复杂的属性计算,里面的属性值也是用数据代理
    //计算属性是有缓存的,get调用了一次,如果值没有改变,再次调用属性的时候就不会调用get函数了
    //初次读取fullname时候,get会被调用,所依赖的数据发生变化时,get也会被调用
    //computed 属性中的get不能被手动调用,Vue会在使用到属性的时候自动调用
    computed: {
        fullname: {
            get() {
                //此处的this是vm
                return this.firstname + '-' + this.lastname;
            },
            //当fullname被修改的时候自动调用set
            set(value) {
                let arr = value.split('-');
                //改的是这两个变量,而不是fullname
                this.firstname = arr[0];
                this.lastname = arr[1]
            }
        }
    }
});

此外,fullname还有一种简写形式:

//fullname的简写形式
fullname() {
    return this.firstname + '-' + this.lastname;
}

可以把属性写成一个函数,函数名就是属性名(es6)函数体默认就有getter的作用

注意!! 不要把fullname理解为一个函数了,它本身还是一个属性,函数只是负责计算返回结果,在模板内只需要写属性名即可!!!

监视属性watch

  1. 当被监视的属性变化时,回调函数自动调用,进行相关操作
  2. 监视的属性必须存在,才能进行监视操作
  3. 监视的两种写法
    1. new Vue 时传入 watch配置
    2. 通过vm.$watch('属性名',配置)

代码演示

<div id="root">
    <h1>今天天气很{{Info}}</h1>
    <button v-on:click="changeWeather">切换天气</button>
    <!-- 如果函数功能很简单,可以直接在click里面写语句 -->
    <button v-on:click="isHot = !isHot;">切换天气</button>
    <!-- 但是这里面不能写alert等函数,因为Vue中的原型对象不包含window对象 -->
    <!-- 解决方法:可以在vm实例中添加window属性,指向window对象即可 -->
    <!-- <button v-on:click="isHot = !isHot;">切换天气</button> -->
</div>
const vm = new Vue({
    el: '#root',
    data: {
        isHot: true
    },
    methods: {
        changeWeather() {
            this.isHot = !this.isHot;
        }
    },
    computed: {
        Info() {
            return this.isHot ? '炎热' : '凉爽';
        }
    },
    //监视属性
    // watch: {
    //     isHot: {
    //         //立即执行,初始化时候,让handler调用一次
    //         immediate: true,
    //         //handler当ishot发生改变时调用
    //         handler() {
    //             console.log('isHot被修改了');
    //         }
    //     }
    // },
});

//监视属性的另一种写法$watch('属性名',配置)
//属性名如果找不到的话也不会报错
vm.$watch('isHot', {
    handler() {
        console.log('isHot被修改了');
    }
})

这里有一个小问题注意:当模板里不使用info变量的时候,点击切换天气,vue开发者工具里的变量显示可能不会更新,但vm内属性实际是更新了的

深度监视

看完了监视属性的简单使用,这时候我会提出一个需求,那就是,如果data属性内有一个对象,我们该怎么监视该对象里面的特定属性呢?

监视多级结构中某属性的变化

答案是使用 ’对象名.属性名‘:{监视配置}

看代码:

data: {
    isHot: true,
        numbers: {
            a: 1,
                b: 1
        }
}

还原成原始写法,就可以使用 点. 来进行单个数据的监视了,也就是深度监视

'numbers.a': {
    handler() {
        console.log('a被修改了');
    }
},

这样就可以监视多级结构中的某个属性的变化了

深度监视的简易写法

如果监视属性里面的语句很简单,就可以使用简易写法:

//正常写法
isHot: {
    // immediate: true,
    // deep: true,
    handler(newValue, oldValue) {
        console.log('isHot被修改了');
    }
}
//简写
isHot() {
    console.log('isHot被修改了');
}

外部写法也是如此:

//正常外部写法
vm.$watch('isHot', {
    immediate: true,
    deep: true,
    handler(newValue, oldValue) {
        console.log('isHot被修改了');
    }
})

//简写
vm.$watch('isHot', function(newValue, oldValue) {
    console.log('isHot被修改了', newValue, oldValue);
});

watch 对比 computed

computed和watch的区别:

  1. computed能完成的功能,watch都可以完成
  2. watch能完成的功能,computed不一定能完成,例如,watch可以执行异步操作

重要的两个小原则:

  1. 所被vue管理的函数,最好写成普通函数,这样this的指向才会是vm
  2. 所有不会被vue所管理的函数,(定时器,ajax回调)。最好写成箭头函数,这样this指向才是vm

比如下面的姓名案例,用watch实现:

<div id="root">
    <p> 姓:<input type="text" v-model="firstname"> </p>
    <p> 名:<input type="text" v-model="lastname"></p>
    <p> 姓名:<span>{{fullName}}</span></p>
</div>
new Vue({
    el: '#root',
    data: {
        firstname: '张',
        lastname: '三',
        fullName: '张-三'
    },
    watch: {
        firstname(newValue) {
            //可以写定时器
            setTimeout(() => {
                this.fullName = newValue + '-' + this.lastname;
            }, 1000);

        },
        lastname(newValue) {
            this.fullName = this.firstname + '-' + newValue;
        }
    },
});

以上代码,会发现我在firstname的处理函数内写了一个定时器,达到了数据更改之后一秒后才执行更改的目的

绑定样式

绑定class样式

在vue中,绑定class的方式是使用 v-bind来动态处理class样式的

一共有三种方式:

  1. 字符串写法,适用于:样式类名不确定 ,需要动态指定
  2. 数组写法,适用于:样式类名不确定 ,个数也不确定,名字也不确定,需要动态指定
  3. 对象写法,适用于:样式类名确定 ,个数也确定,名字也确定,需要动态决定用不用

代码:

<div id="root">
    <!-- 绑定class样式 字符串写法,适用于:样式类名不确定 ,需要动态指定 -->
    <div class="basic normal" :class="mood" @click="changeMood">{{a}}1</div>
    <hr>
    <!-- 绑定class样式 数组写法,适用于:样式类名不确定 ,个数也不确定,名字也不确定,需要动态指定 -->
    <div class="basic" :class="array">{{a}}2</div>
    <button @click="md">点我删除类</button>
    <hr>
    <!-- 绑定class样式 对象写法,适用于:样式类名确定 ,个数也确定,名字也确定,需要动态决定用不用 -->
    <div class="basic" :class="classObj">{{a}}2</div>
</div>
new Vue({
    el: '#root',
    data: {
        a: 'hello',
        mood: 'normal',
        //存放类的数组
        array: ['normal', 'happy', 'sad'],
        classObj: {
            //类的对象写法,false为不启用
            happy: true,
            sad: true
        }
    },
    methods: {
        changeMood() {
            const arr = ['happy', 'sad', 'normal'];
            const index = Math.floor(Math.random() * 3);
            this.mood = arr[index]
        },
        md() {
            this.array.shift();
        }
    },
});

绑定style样式

绑定style样式的方法和绑定class样式大同小异

使用 :style="styleObj" 进行绑定

对象名可以使用驼峰命名的css属性 fontSize: '44px'

也可以是原始css属性,但需要加上引号 'font-size': '44px'

代码演示

<!-- 绑定style样式 也是对象写法 -->
<div class="basic" :style="styleObj">{{a}}2</div>

<!-- 绑定style样式 数组对象写法 -->
<div class="basic" :style="[styleObj1,styleObj]">{{a}}2</div>
data: {
    //样式的对象写法
    styleObj: {
        //vue中的css属性写法
        // fontSize: '44px'
        //也可以写原生css写法
        'font-size': '44px'
    },
    styleObj1: {
        //vue中的css属性写法
        // fontSize: '44px'
        //也可以写原生css写法
        color: 'red'
    }
}

条件渲染

1.v-if

写法:

  1. v-if="表达式"
  2. v-else-if="表达式"
  3. v-else="表达式"

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

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

注意:v-if可以用:v-else-if ,v-else一起使用,但中间不能有元素打断

2.v-show

写法:v-show=”表达式“

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

特点:不展示dom元素,未被移除,仅仅是使用样式隐藏掉

注意:使用v-if时候,元素可能无法被获取到,但是使用v-show是一定可以获取到的

v-if可以配合template模板使用

代码演示

<div id="root">
    <!-- 条件渲染 相当于css的display -->
    <h2 v-show="true">{{a}}</h2>
    <h2 v-show="1===1">{{a}}</h2>
    <h2 v-show="aa">{{a}}</h2>

    <hr>

    <!-- 使用v-if做条件渲染 影响结构-->
    <h2 v-if="aa" @click="n++">{{n}}</h2>
    <h2 v-if="true">{{a}}</h2>
    <h2 v-if="1===1">{{a}}</h2>

    <!-- v-else-if -->
    <!-- if和elseif 中间不能有元素打断 -->
    <div v-if="n===1">1111</div>
    <div v-else-if="n===2">2222</div>
    <div v-else-if="n===3">3333</div>
    <!-- v-else 不用写参数,是上面条件都不符合的情况 -->
    <div v-else>好</div>

    <!-- 使用template模板配合if渲染多个元素 -->
    <!-- 使用template模板只能配合if,不能配合show -->
    <template v-if="n===4">
        <h2 v-show="true">template{{a}}</h2>
        <h2 v-show="1===1">template{{a}}</h2>
        <h2 v-show="aa">template{{a}}</h2>
    </template>
</div>
new Vue({
    el: '#root',
    data: {
        a: 'hello',
        n: 1,
        aa: true
    }
});

v-for 指令

v-for指令可以用来循环遍历对象、数组、字符串、数字 到相应个数的标签中

作用、语法:

  1. 用于展示列表数据
  2. 语法:v-for="(item,index) in xxx" :key="yyy"
  3. 可遍历: 数组、对象、字符串、数字

注意:只要你使用了遍历语法,你就必须要使用key这个关键字,给每一个li设定唯一的标识

格式::key="xxx“

代码演示:

<div id="root">
    <h2>人员列表</h2>
    <ul>
        <!-- 使用v-for进行遍历输出 -->
        <!-- 只要你使用了遍历语法,你就必须要使用key这个关键字,给每一个li设定唯一的标识 -->
        <li v-for="p in persons" :key="p.id">{{p.name}}</li>
    </ul>
    <ul>
        <!-- key的另一种写法 -->
        <!-- a就相当于item,b就相当于index -->
        <li v-for="(a,b) in persons" :key="b">A:{{a}} B:{{b}} </li>

    </ul>
    <ul>
        <!-- 也可以使用of 遍历对象(in也可以) 这样a就是value b就是key了 -->
        <li v-for="(a,b) of car" :key="b">key: {{b}} ---- value :{{a}}</li>
    </ul>
    <ul>
        <!-- 甚至还能遍历字符串 -->
        <li v-for="(a,b) in str" :key="b">key: {{b}} ---- value :{{a}}</li>
    </ul>
    <ul>
        <!-- 甚至还能遍历输出数字 -->
        <li v-for="(a,b) in 10" :key="b">key: {{b}} ---- value :{{a}}</li>
    </ul>
</div>
new Vue({
    el: '#root',
    data: {
        persons: [{
            id: '001',
            name: '张三',
            age: '18'
        }, {
            id: '002',
            name: '李四',
            age: '19'
        }, {
            id: '003',
            name: '王五',
            age: '20'
        }],
        car: {
            name: '马自达',
            price: '114514'
        },
        str: 'kanokano123'
    }
});

v-for中key的作用与坑

key在v-for中扮演者极为重要的角色,key是vue作为判断数据的唯一性的重要标识

所以,key的唯一性成了一个非常重要的前提保证

这里就要提一下Vue在生成数据的时候的步骤了:

  1. 首先vue会在渲染页面前,在计算机内存中计算并生成一个虚拟DOM,然后才会渲染成真实DOM文档到页面上
  2. 如果需要在相同的地方进行重新渲染,Vue就会依次对比新数据和之前的虚拟DOM数据是否一致,如果发现旧数据有一致的地方,则会复用旧的虚拟DOM缓存渲染的真实DOM,以提高渲染效率,如果不一致,则会按照新dom来重新渲染

既然是依次对比,所以,这里就会牵扯到顺序问题,vue是按照key进行顺序遍历对比的,所以,key一定要是一个唯一的值!!!

要点归纳:
  1. 虚拟DOM中key的作用:
    • key是虚拟dom对象的标识,当状态中的数据发生变化时,Vue会根据新数据生成“新虚拟dom”
      随后Vue进行“新虚拟dom” 与 ”旧虚拟dom“ 的差异比较,规则如下:
  2. 对比规则:
    • 旧虚拟DOM中找到了与新的虚拟DOM相同的key
      • 若虚拟DOM中的内容没有改变,则会直接复用之前的真实dom
      • 若虚拟DOM中的内容改变了,则会生成新的真实DOM,随后替换掉页面中之前的真实DOM
    • 旧虚拟DOM中未找到与新虚拟DOM相同的key
      • 创建真实的DOM,随后渲染到页面
  3. 用index作为key可能会引发的问题:
    • 若对数据进行:逆序添加,逆序删除等破坏顺序的操作:
      • 产生没有必要的真实DOM更改 => 界面没有问题,但是渲染效率降低
    • 如果结构中还包括输入类的dom
      • 会产生错误的DOM更新 => 界面有问题,顺序错乱
  4. 开发中如何选择key?
    1. 最好使用每条数据的唯一标识作为key,比如id,手机号,身份证号,学号等唯一值
    2. 如果不存在对数据的逆序添加、逆序删除等破坏顺序的操作,仅用于渲染列表用于显示,使用index作为key是没有问题的
错误示例
<div id="root">
    <h2>人员列表</h2>
    <ul>
        <button @click="add">点我添加老六</button>
        <!-- 使用v-for进行遍历输出 -->
        <li v-for="(i,index) in persons" :key="index">
            {{i.name}}-{{i.id}}
            <input type="text">
        </li>
    </ul>
</div>
data: {
    persons: [{
        id: '001',
        name: '张三',
        age: '18'
    }, {
        id: '002',
        name: '李四',
        age: '19'
    }, {
        id: '003',
        name: '王五',
        age: '20'
    }]
},
methods: {
    add() {
        let p = {
            id: '004',
            name: '老六',
            age: 21
        };
        //往数组第一个写入数据
        this.persons.unshift(p);
    }
}

点击之前: image-20220807225408598

点击之后 : image-20220807225444623

你会发现,老六旁边的输入框本应该是空的,结果变成了张三,导致下面的输入框全都错了一位

这是因为:Vue在对比新老虚拟DOM的时候,发现张三在index 0的位置,无法渲染,但是旁边的输入框在虚拟DOM中都是空的,Vue则会复用之前的实际DOM,然后继续往后依次比对,发现每一个的姓名和index都对不上,所以只能使用新虚拟DOM重新渲染名字了,但是,输入框在新老虚拟DOM中,都是空的,Vue就会误以为可以直接复用,就会把前三个复用掉,最后才会渲染新的input元素到最后

这里的主要问题就是,index不是唯一的,修改DOM树的时候容易造成查找不正确,从而进行错误的操作

解决方法

:key="index" 替换为 :key="i.id"即可

或者在这个项目例子里,没有必要非要把老六放在第一位,可以使用push方法插入数据到数组尾部,这样就不会打乱index的标号顺序了

使用列表过滤实现简单搜索与排序

基本原理就是,使用计算属性于filter,sort函数

切记,计算属性执行的条件是:页面刚加载时和依赖数据发生变化时

<div id="root">
    <h2>人员列表</h2>
    <input type="text" placeholder="请输入姓名" v-model="Keyword">
    <button @click="sortType=2">年龄升序</button>
    <button @click="sortType=1">年龄降序</button>
    <button @click="sortType=0">原顺序</button>
    <ul>
        <!-- key的另一种写法 -->
        <!-- a就相当于item,b就相当于index -->
        <li v-for="(a,b) in findPersons" :key="a.id">{{a.name}}-{{a.age}}-{{a.sex}}</li>
    </ul>
</div>
new Vue({
    el: '#root',
    data: {
        Keyword: '',
        sortType: 0, //0原序 1 降序 2升序
        persons: [{
            id: '000',
            name: '张三',
            age: '11',
            sex: '男'
        }, {
            id: '001',
            name: '张麻子',
            age: '28',
            sex: '男'
        }, {
            id: '002',
            name: '李四',
            age: '19',
            sex: '男'
        }, {
            id: '003',
            name: '王五',
            age: '50',
            sex: '男'
        }, {
            id: '004',
            name: '马冬梅',
            age: '12',
            sex: '女'
        }],
        // findPersons: []
    },
    //使用计算属性实现
    computed: {
        //这玩意开始的时候执行一次
        //所依赖的keyword变化时候又会执行一次
        //里面用到了sortType 所以sortType改变的时候也会触发这个函数
        findPersons() {
            const arr = this.persons.filter((p) => {
                //true就是符合,返回一个新数组
                //indexof对于空字符串的返回是0(特别重要)
                return p.name.indexOf(this.Keyword) !== -1
            });
            //别急着返回,判断一下是否要排序
            if (this.sortType) {
                arr.sort((a, b) => {
                    //由于a和b在这里都是对象,不能直接比,我们要获取里面的age
                    return this.sortType == 1 ? b.age - a.age : a.age - b.age
                })
            }
            return arr;
        }
    }
    //#region
    //使用监视属性实现
    // watch: {
    //     Keyword: {
    //         //立即执行一次,否则页面没数据
    //         immediate: true,
    //         handler(val) {
    //             this.findPersons = this.persons.filter((p) => {
    //                 //true就是符合,返回一个新数组
    //                 //indexof对于空字符串的返回是0(特别重要)
    //                 return p.name.indexOf(val) !== -1
    //             })
    //         }
    //     }
    // },
    //#endregion
});

Vue数据侦测

实现类似vue的简单数据检测

代码演示

//需要传入的data对象
let data = {
    name: 'kano',
    adress: 'Janpan'
};
//创建一个监视的实例对象,用于监视data中属性的变化
const obs = new Observer(data);
console.log(obs);
//创建一个vm
let vm = {};
vm._data = obs;
vm.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("我被修改了,我要去处理页面响应");
                obj[k] = val;
            }
        })
    })
}

不完善的地方:无法找出数组中的对象并赋予getter setter , 多层对象也一样

使用Vue.set方法新增响应式属性

首先得注意的是:

这个set方法只能给vue data里面某个对象增加属性!!!
也就是要操作的对象不能是Vue实例,或者Vue实例的根数据对象(data、_data)

语法

Vue.set(vue实例, '属性', 值或对象)

例子

const vm = new Vue({
    el: '#root',
    data: {
        name: 'kanokano',
        adress: '湖南',
        student: {
            name: 'tony',
            // sex: '男',
        }
    },
});

//vm.$set(vm.student, 'sex', '男')
//或者
Vue.set(this.student, 'sex', '男')

数组更新检测

Vue将被监听的数组的变更方法进行了包装,所以也可以触发网页元素更新,方法包括:

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

注意:使用下标索引直接修改数组数据是不会触发视图更新的!!!

错误示范

vm.hobby[0]='ddd';

上面的操作是不会生效的(

对于非修改性质的方法,比如filter() ,过滤完成后可以直接替换赋值掉原对象

vm.hobby = vm.hobby.filter((x)=>{
    return x != '抽烟'
})

代码演示

const vm = new Vue({
    el: '#root',
    data: {
            name: 'Xukun Cai',
            hobby: ['唱', '跳', 'rap', '篮球'],
    }
});
//添加元素
vm.hobby.push('你干嘛~ 哎哟');
vm.hobby.shift();

此外,除了可以用上面提到的几个方法操作数组之外,其实也可以使用Vue.set方法进行变更数据:

Vue.set(vm.hobby,0,'唱');
//或者
vm.$set(vm.hobby,0,'唱');

总结

Vue监视数据的原理:

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

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

    • 通过setter实现监视,且要造new Vue 时就要传入要监测的数据
      • 对象中后追击啊的属性,Vue默认不做响应式处理
      • 如需给后添加的属性做响应式,请使用如下API
      • vue.set(target,propertyName/index,value)
      • vm.$set(target,propertyName/index,value)
  3. 如何监测数组中的数据?

    • 通过包装数组对应更新元素的方法来实现,本质就是
      • 调用原生对应的方法对数组进行更新
      • 重新解析没模板,进而更新页面
  4. vue修改数组中某一个元素一定要使用支持的方法

    1. API: push , pop , shift , unshift , splice , sort , reverse
    2. Vue.set 或者 vm.$set

    特别注意的是:Vue.set 和 vm.$set 不能给vm或者vm的根数据对象添加属性!

    对于非修改性质的方法,比如filter() ,过滤完成后可以直接替换赋值掉原对象

使用v-model收集表单数据

要点

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

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

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

  • lazy:失去焦点再收集数据
  • number:输入字符串转换为有效数字
  • trim:输入首尾空格过滤

代码演示

<div id="root">
    <form @submit.prevent="demo">
        <!-- trim修饰符,这个就不说了 -->
        <label for="username"> 账号:</label><input type="text" id="username" v-model.trim="userInfo.account">
        <br><br>
        <label for="passwd"> 密码:</label><input type="text" id="passwd" v-model.trim="userInfo.password">
        <br><br> 性别: 男
        <input type="radio" name="sex" value="男" v-model="userInfo.sex"> 女<input type="radio" name="sex" value="女" v-model="userInfo.sex">
        <br><br> 年龄
        <!-- v-model也是有修饰符的,这里可以使用修饰符使传入的数据为数字类型 -->
        <input type="number" name="age" value="男" v-model.number="userInfo.age">
        <br><br> 爱好: 唱
        <input type="checkbox" v-model="userInfo.hobby" value="唱"> 跳
        <input type="checkbox" v-model="userInfo.hobby" value="跳"> rap
        <input type="checkbox" v-model="userInfo.hobby" value="rap"> 篮球
        <input type="checkbox" v-model="userInfo.hobby" value="篮球">
        <br><br> 所属校区
        <!-- 下拉框的v-model需要配置在select上而不是option上 -->
        <select name="scholl" v-model="userInfo.city">
            <option value="">请选择校区</option>
            <option value="beijing">北京</option>
            <option value="shanghai">上海</option>
            <option value="shenzhen">深圳</option>
            <option value="wuhan">武汉</option>
        </select> 其他信息
        <br><br>
        <!-- lazy修饰符可以让用户输入完成之后再做出数据捕获的动作,节省资源 -->
        <textarea cols="30" rows="10" v-model.lazy="userInfo.other"></textarea>
        <br>
        <input type="checkbox" v-model="userInfo.accept">阅读并接受<a href="//kanokano.cn">用户协议</a>
        <br><br>
        <button>提交</button>
    </form>
</div>
new Vue({
    el: '#root',
    data: {
        userInfo: {
            account: '',
            password: '',
            //这里面要是有值的话,因为model的双向绑定,渲染的时候就会选中填入的值
            //sex由于是radio,需要手动配置标签的value属性
            sex: '',
            age: '',
            //hobby 是复选框,所以也需要手动配置value属性
            //复选框需要的属性,必须是数组类型的
            hobby: [],
            city: 'beijing',
            other: '',
            accept: false
        }
    },
    methods: {
        demo() {
            console.log(JSON.stringify(this.userInfo));
        }
    },
});

v-text 指令

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

与插值语法的区别:会替换标签里面现有的文本

演示

<div id="root">
    <div>{{name}}ddd</div>
    <!-- v-text -->
    <!-- 和插值语法的区别就是会替换标签里面现有的文本 -->
    <div v-text="name">ddd</div>
</div>
new Vue({
    el: '#root',
    data: {
        name: 'kano'
    }
});

v-html 指令

作用:向指定节点中渲染包含html解构的内容,使用上和v-text没有太大区别,但还是有点

与插值语法的区别:

  • v-html会替换掉节点中所有内容,插值语法则不会
  • v-html可以识别html结构

!严重注意:v-html会有安全性的问题:

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

演示

<div id="root">
    <div>{{name}}ddd</div>
    <!-- 可以解析html -->
    <div v-html="html">ddd</div>
</div>
<script>
    new Vue({
        el: '#root',
        //好用,但容易xss注入
        data: {
            html: '<a href="javascript:alert(document.cookie);">dddd</a>'
        }
    });
</script>

v-cloak指令

  1. 本质是一个特殊属性,Vue实例创建完毕并接管容器之后,会删掉v-cloak属性
  2. 使用css配合v-cloak可以解决网速慢时候页面展示出{{xxx}}的问题

演示

<style>
  /*这里需要使用属性选择器进行选择性隐藏*/
  [v-cloak]{
    display: none;
  }
</style>
<div id="root">
  <div v-cloak>{{name}}</div>
  <!-- 有时候我们需要把vue写在后面,这样会导致页面闪现问题 -->
  <script src="../vue.js"></script>
</div>
new Vue({
  el: '#root',
  data: {
    name: 'kano'
  }
});

v-once指令

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

代码演示

<div id="root">
  <div v-once>初始化的n是:{{n}}</div>
  <div >当前的n是:{{n}}</div>
  <button @click="n++">点我n+1</button>
  <script src="../vue.js"></script>
</div>
new Vue({
  el: '#root',
  data: {
    n:1
  }
});

v-pre指令

  1. 跳过其所在节点的编译过程
  2. 可利用它跳过,没有使用指令的语法,没有使用插值与法的节点,会加快编译

代码演示

<div id="root">
  <div v-pre>Vue</div>
  <div v-once v-pre>初始化的n是:{{n}}</div>
  <div v-pre>当前的n是:{{n}}</div>
  <button @click="n++">点我n+1</button>
</div> 

自定义指令

我们可以使用directives编写自定义指令

directives内可以有两种写法:

  1. 函数式,属性是函数,适用于处理简单一点的事务
  2. 对象式,里面有三个函数:bind,inserted,update
    • 指令与元素成功绑定时会执行bind
    • 指令被所在元素插入页面时会执行inserted
    • 指令所在的模板被重新解析时会执行update

注意,以上函数的this指向都为window
注意,指令定义的时候不加 v- 但使用的时候要加


局部写法:

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

全局写法:

Vue.directive('指令名',{bind,inserted,update})Vue.directive('指令名',回调函数)


big函数何时会被调用?

  1. 指令与元素成功绑定时(绑定时候是虚拟dom状态,还没有放入页面)
  2. 指令所在的模板被重新解析时

特别注意!!!

自定义指令不能使用驼峰名称或者大写字母,因为Vue会把指令名上所有大写字母转成小写字母!!,推荐使用-(需要还原为带引号的属性名)或者_作为分隔符

例子

定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大十倍

 <div id="root">
   <!-- 定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大十倍 -->
   <h2>当前的n是: <span v-text="n"></span></h2>
   <h2>放大十倍的n是: <span v-big="n"></span></h2>
   <button @click="n++">点我+1</button>
</div>
.....
//自定义指令区域
directives:{
  //两个参数,一个是dom,另一个是参数对象
  big(ele,binding){
    ele.innerText = parseInt(binding.value) * 10;
  },
}
....

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

<div id="root">
  <!-- 定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点 -->
  <input type="text" v-fbind:value="n">
</div>

当我们想要在一开始的时候获取input元素的焦点,除了可以在input标签里面添加autofocus之外,
我们还可以使用自定义指令,但是自定义指令如果写成函数式,就会出现页面加载后,自动对焦不起作用问题:

fbind(ele,binding){
  ele.value = binding.value;
  //无效
  ele.focus()
}

其原因在于:一开始dom并没有先渲染出来,只有点击+1后才会生效,因为元素已经在页面里面,所以执行focus就会生效

正确写法:

//自定义指令区域
directives:{
  fbind:{
    //三个固定函数
    //指令与元素成功绑定时
    bind(ele,binding){
      ele.value = binding.value;
    },
      //指令被所在元素插入页面时
      inserted(ele,binding){
        ele.focus();
      },
        //指令所在的模板被重新解析时
        update(ele,binding){
          ele.value = binding.value;
        }
  }
}

生命周期

Vue的生命周期中一共要执行很多重要的函数,mounted函数则是其中的一个

简介:

  1. 生命周期又叫生命周期回调函数、生命周期函数,生命周期钩子
  2. Vue在关键时刻帮我们调用的一些特殊名称的函数
  3. 生命周期函数的名字不可更改,但函数的具体内容是程序员根据需求编写的
  4. 生命周期函数中的this指向的是VM 或 组件的实例对象

流程图

生命周期

分析生命周期

vue实例的整个生命周期分别要执行如下几组函数:

  1. beforeCreate( ) 和 created( )
  2. beforeMount( ) 和 mounted( )
  3. beforeUpdate( ) 和 updated( )
  4. beforeDestroy( ) 和 destroyed( )

每个生命周期函数的作用:

beforeCreate函数

这个阶段初始化生命周期和事件,但是代理并没有开始
在这个阶段,无法通过vm访问倒data中的数据,method中的方法

created函数

生命周期和事件初始化完毕
在这个阶段,可以通过vm访问倒data中的数据,methods中的方法

beforeMount函数

在这个阶段,页面呈现的是未经Vue编译的DOM结构,故而在这期间,所有对dom的操作最终都是无效的
此时会发现,页面的基本结构已经渲染到页面上了,但是插值语法等并没有进行解析
这是因为在这期间,Vue并没有将真实DOM放到页面上,此时的DOM还是虚拟的

mounted函数

Vue完成模板解析并把真实DOM元素放入页面后(完成挂载)会调用一个名叫mounted的函数
这个函数整在个Vue的生命周期中只调用一次
此时页面中呈现的是经过VUe编译的ODM,对DOM的操作均有效(但不推荐)

至此初始化的过程就结束了
p.s: 这里一般可以进行:开启定时器,发送网络请求,订阅消息,绑定自定义事件,等其他初始化操作

beforeUpdate函数

当Vue准备发生响应式变化的时候,会触发beforeUpdate系列函数
此时,数据是新的,但页面是旧的
即:页面尚未和数据保持同步

updated函数

当数据更新操作执行完毕后,会触发updated系列函数
此时,数据是新的,页面也是新的
即:数据和页面保持同步

beforeDestroy函数

这个函数是在Vue实例销毁之前所执行的
此时,vm中所有的 data,method 指令等等都处于可用状态,马上要执行销毁作业,
但是在这里所有的对this的操作都不会触发页面更新了
一般在这个阶段可以做:关闭定时器,取消订阅消息,解绑自定义事件等操作

destroyed函数

Vue销毁实例之后会执行destroyed回调函数
此时vm已经销毁

以上就是Vue实例的整个生命周期了

生命周期函数演示

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

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="../vue.js"></script>
</head>

<body>
    <div id="root">
        <!-- 准备好一个容器 -->
        <h2>当前的n值是:{{n}}</h2>
        <button @click="add">点我n+1</button>
        <button @click="bye">点我销毁容器</button>
    </div>
    <script>
        const vm = new Vue({
            //如果没有el的话,Vue的生命周期就会暂停在created函数
            //直到遇到vm.$mount(el)调用
            el: '#root',
            data: {
                n:1
            },
            methods: {
                add(){
                    console.log('add()');
                    this.n++;
                },
                bye(){
                    console.log('destroy()');
                    this.$destroy()
                }
            },
            beforeCreate() {
                console.log('beforeCreate');
                // console.log(this.n);
                //此时this里面没有n
                // debugger;
            },
            created() {
                console.log('created');
                // console.log(this.n);
            },
            beforeMount() {
                console.log('BeforeMount');
                //此时会发现,页面的基本结构已经渲染到页面上了,但是插值语法的{{n}}并没有解析成数字
                // debugger;
            },
            mounted(){
                console.log('mounted');
                // debugger;
            },
            beforeUpdate() {
                console.log('beforeUpdate');
                console.log(this.n);
                // debugger;
            },
            updated() {
                console.log('updated');
                // this.n++;//别整活儿,会不停的触发数据更新操作
            },
            beforeDestroy() {
                console.log('beforeDestroy');
                this.n++;//不起作用
            },
            destroyed() {
                console.log('destroyed');
            },
        });
    </script>
</body>

</html>

总结生命周期

常用的生命周期钩子:

  1. mounted:可以发送Ajax请求、启动定时器、绑定自定义事、订阅消息等【初始化操作】
  2. beforeDestroy:清除定时器、解绑自定义事件、取消订阅消息等【收尾工作】

关于销毁Vue实例

  1. 销毁后借助Vue开发者工具不会看到任何信息
  2. 销毁后自定义事件会失效,但是原生DOM事件依旧有效(vue2.7版本之后无效)
  3. 一般不会在beforeDestory中操作数据,因为那样不会出发数据/页面更新流程了

Vue组件化编程

传统方式与组件化方式的区别

传统方式编写应用的问题:

  • 依赖关系混乱,不好维护
  • 代码复用率不大,耦合性较高

组件化编程方式的优点:

  • 依赖关系清晰,方便维护
  • 代码复用率非常高,组件之间互不影响

组件的概念

所谓的组件就是实现应用中局部功能代码和资源的集合

模块与组件、模块化与组件化

模块

理解:向外提供特定功能的 js 程序,一般为一个js文件

使用原因: js文件多而杂

作用:复用js,简化js的编写,提高js运行效率

组件

理解:用来实现局部(特定)功能效果的代码集合(html/css/js/image)

使用原因:一个界面的功能很复杂

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

模块化

当应用中的js都以模块来编写的,那这个应用就是一个模块化的应用

组件化

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

非单文件组件与单文件组件

非单文件组件

一个文件中包含有N个组件

单文件组件

一个文件中只包含有一个组件

非单文件组件编写

Vue使用组件的三大步骤

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

如何定义?

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

区别如下:

  1. el不要写,因为最终左右的组件都要经过一个vm管理,由vm中的el决定服务哪个容器。
  2. data必须写成函数,为什么? --- 避免组件被复用时,数据存在引用关系,使用函数形式,其中的return可以自动复制对象(深拷贝)

备注1:使用template可以配置组件html解构

备注2:vue.extend()方法其实是vue的一个构造器,继承自vue 使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。

如何注册?

  1. 局部注册:靠newVue时候传入的components选项
  2. 全局注册:靠Vue.component('组件名',组件);

编写组件标签

使用 <组件名></组件名> 来在页面上引用组件标签

代码示例

<div id="root">
    <!-- 3.编写组件标签 -->
    <school></school>
    <!-- 复用,事件对象互相独立 -->
    <school></school>
    <h1>{{msg}}</h1>
    <student></student>
    <hr>
    <!-- 全局组件 -->
    <hello></hello>
</div>
//1.注册组件
//创建一个school组件
const school = Vue.extend({
    //不要写el配置项,因为左右组件最终都有vm实例管理
    //写template
    template: `
            <div>
                <h2>学校名称 {{name}}</h2>
                <h2>学校地址 {{address}}</h2>
                <button @click="ChangeName">点我修改学校名为加里顿</button>
            </div>
            `,
    //     el: '#root',
    //data必须写为函数式,因为对象式为引用类型,会造成一些问题
    //使用函数形式,其中的return可以自动复制对象(深拷贝)
    data() {
        return {
            name: 'MIT',
            address: 'USA'
        }
    },
    methods: {
        ChangeName() {
            this.name = "加里顿"
        }
    },
})

//创建一个student组件
const student = Vue.extend({
    //写template
    template: `
            <div>
                <h2>学生姓名 {{name}}</h2>
                <h2>学生年龄 {{age}}</h2>
            </div>
            `,
    data() {
        return {
            name: 'kano',
            age: 19
        }
    }
})

//全局组件
const globalHello = Vue.extend({
    template: `
            <div>
                hello1234
            </div>
            `,
    data() {
        return {
            name: 'TOM'
        }
    }
})

//全局注册组件
Vue.component('hello', globalHello)

//2.注册组件(局部注册)

//创建vm
new Vue({
    el: '#root',
    //填入组件
    components: {
        //es6缩写
        school,
        student
    },
    data: {
        msg: 'hello'
    }
});

组件的命名方法

对于单个单词组成:首字母大写即可

对于多单词组成:

  1. 全部小写,使用短横线 ’ - ‘ (kebab-case命名) 连接 :my-component
  2. 每一个单词的首字母大写 (camelCase命名) :MyComponent (只能在脚手架环境中使用)

备注:

  • 组件名尽可能回避原生的标签名,例如:h2、H2 都不可以
  • 可以使用name配置项指定组件在开发者工具中呈现的名字

关于组件标签:

  • 第一种写法:<kano></kano>
  • 第二种写法:<kano/>
  • 备注:不使用脚手架时, 会导致后续组件无法渲染

一个简写形式:

  • const school = Vue.extend(options) 可简写为: const school = options
    可以简写是因为最后vm中的components会判断传入的是extends过的还是原对象,从而做出不同的反应

组件的嵌套

只需要注意一点:想嵌套到哪个组件就放到哪个组件内

代码示例:
<div id="root">
    <h1>{{msg}}</h1>
</div>
//创建一个student组件
const student = Vue.extend({
    //写template
    template: `
            <div>
                <h2>学生姓名 {{name}}</h2>
                <h2>学生年龄 {{age}}</h2>
            </div>
            `,
    data() {
        return {
            name: 'kano',
            age: 19
        }
    }
})

//school组件
const school = Vue.extend({
    //可以使用name属性强制指定组件在开发者工具中呈现的组件名
    name: 'kano',
    // student是school的子组件,所以要写在school的模板里面
    template: `
            <div>
                scholl组件开始
                <h2>学校名称 {{name}}</h2>
                <h2>学校地址 {{address}}</h2>
                <button @click="ChangeName">点我修改学校名为加里顿</button>
                <student></student>
                scholl组件结束
            </div>
            `,
    data() {
        return {
            name: 'MIT',
            address: 'USA'
        }
    },
    methods: {
        ChangeName() {
            this.name = "加里顿"
        }
    },
    //嵌套注册(注意student要在school前面)
    components: {
        student
    }
})

//定义hello组件
const hello = Vue.extend({
    template: `<h1>{{msg}}</h1>`,
    data() {
        return {
            msg: 'hello world'
        }
    }
})

//定义app组件
const app = Vue.extend({
    template: `
                <div>
                    <school></school>
                    <hello></hello>
                </div>
                `,
    components: {
        school,
        hello
    }
})
//创建vm
new Vue({
    template: `<app></app>`,
    el: '#root',
    //注册组件(局部)
    components: {
        //es6缩写
        app
    },
    data: {
        msg: 'hello'
    }
});

以上代码组件结构如下:

  • <root>
    • <app>
    • <kano>
    • <hello>

总结:被嵌套属性需要在template属性中写上嵌套者的组件标签,components中写上嵌套者的组件名即可完成嵌套操作

VueComponent构造函数

关于VueComponent:

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

如果我们创建了多个组件,分别输出组件的值:

//创建一个hello组件
const hello = Vue.extend({
    template: `<h2>{{msg}}</h2>`,
    data() {
        return {
            msg: '你好'
        }
    }
})
console.log(hello);
//school组件
//school组件
const school = Vue.extend({
    //可以使用name属性强制指定组件在开发者工具中呈现的组件名
    name: 'school',
    // student是school的子组件,所以要写在school的模板里面
    template: `
            <div>
                <h2>学校名称 {{name}}</h2>
                <h2>学校地址 {{address}}</h2>
                <button @click="ShowName">点我提示</button>
            </div>
            `,
    data() {
        return {
            name: 'MIT',
            address: 'USA'
        }
    },
    methods: {
        ShowName() {
            //这里的this是VueComponent的实例对象
            //不过在没渲染的时候,VueComponent并没有执行,所以这个this其实就是构造函数中的this
            alert(this.name)
        }
    },
})
console.log(school);

结果都为

ƒ VueComponent(options) {
    this._init(options);
}

但是从Vue.extend的源码来看,这两个VueComponent并不是同一个函数

因为Vue.extend源码是这样写的:

大致代码如下:

Vue.extend = function (extendOptions) {
    //省略。。。
    var Sub = function VueComponent(options) {
        this._init(options);
    };
    //省略。。。
    return Sub;
};

每次调用Vue.extend的时候,都会创建一个新变量用于接收新构造函数
所以每个组件的VueComponent都成为了独立的个体
直到需要渲染到页面时候,就会执行这些构造函数

如何获取Vue所管理的组件实例对象

可以使用vm.$children属性获取

vm.$children属性返回的是一个数组,里面有vm管理的组件实例

例:

//创建vm
vm = new Vue({
    el: '#root',
    components: {
        school,
        hello
    }
});
console.log(vm.$children);

备注:如果组件里面也有子组件,那这个组件的$children属性里面就会显示存放子组件

vm与vc的本质区别

vm与vc相比:

  1. vc没有独立的el属性
  2. vc的data必须写成函数式
  3. vc是VueComponent的实例对象,而vm是Vue的实例对象

组件的一个重要的内置关系

概述

一个重要的内置关系:VueComponent.prototype.proto === Vue.prototype

为什么要有这个关系:让组件实例对象(vc)可以访问到Vue原型上的属性和方法

这涉及到原型链的问题,我们知道,VueComponent 和 Vue 实例化之后两者的内置的方法和属性其实都大同小异,其中的关键在于,Vue复用了一些方法与属性:指 通过更改原型指向的方式共享Vue上的一些属性和方法 这种方式也叫 原型链继承

代码演示:

//创建hello组件
const hello = Vue.extend({
    template: `<h1>hello</h1>`
});

//创建vm
vm = new Vue({
    el: '#root',
    components: {
        hello
    }
});
console.log(hello.prototype.__proto__ === Vue.prototype) //true

Vue的原型链大致为:

Vue --> Vue的原型对象 --> Object的原型对象 --> null

VueComponent的原型链大致为:

VueComponent --> VueComponent的原型对象--> Vue的原型对象 --> Object的原型对象 --> null

图解

image-20220823004855399

单文件组件的编写

简介

单文件组件文件的扩展名是.vue

文件内可以写三种标签:

  1. <template>标签 ,用于填写预解析模板,也就是组件的结构
  2. <script>标签,用于填写组件交互相关的代码(数据,方法等)
  3. <style>标签,用于填写组件的样式

注意:多个vue单文件组件,最终会汇总于App.vue文件中,方便统一管理

script标签中由于填写的是组件相关的交互代码,故需要使用export向外暴露需要的变量

目录结构

一般一个单文件组件写成的工程的文件树是这样的

  • Root
    • School.vue
    • Student.vue
    • App.vue
    • main.js
    • index.html

其中:

  • App.vue汇总了所有的组件
  • main.js内实例化了vm
  • index.html提供了网页基础结构
代码演示:

School.vue

<template>
<!-- 组件的结构 -->
<div>
    <h2 class="demo">学校名称 {{name}}</h2>
    <h2>学校地址 {{address}}</h2>
    <button @click="ShowName">点我提示</button>
</div>
</template>

<script>
//简写 Vue.extend可以省略
export default {
    name: 'School',
    data() {
        return {
            name: 'MIT',
            address: 'USA'
        }
    },
    methods: {
        ShowName() {
            alert(this.name)
        }
    },
}
</script>

<style>
/*组件的样式*/
.demo{
    color: pink;
}
</style>

Student.vue

<template>
<div>
    <h2>学生姓名 {{name}}</h2>
    <h2>学生年龄 {{age}}</h2>
</div>
</template>

<script>
//创建一个student组件
export default {
    name:'Student',
    data() {
        return {
            name: 'kano',
            age: 19
        }
    }
}
</script>

App.vue

<template>
  <div>
    <School></School>
    <Student></Student>
  </div>
</template>

<script>
// 组件最终必须汇总于App.vue
// 引入组件
import School from './School.vue';
import Student from './Student.vue';
export default {
    name:'App',
    components:{
        School,
        Student
    }
}
</script>

main.js

// vm初始化在这
import App from './App.vue';
new Vue({
    el: '#root',
    components: { App }
})

index.html

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

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <script src="../vue.js"></script>
</head>

<body>
    <div id="root">
        <App></App>
    </div>
    <!-- 运行不了,需要在脚手架环境 -->
    <script type="module" src="./main.js"></script>
</body>

</html>

不过,你会发现,以上代码根本执行不了,原因是需要Vue脚手架的支持,脚手架将在下面小节讲,这里只是熟悉一下单文件组件的编写方法((

Vue脚手架

介绍

Vue脚手架又叫 Vue CLI(Command Line Interface)

安装

第一步:安装Node.js 后,在命令行直接输入

npm install -g @vue/cli

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

vue create xxxxx

第三步:启动项目

npm run serve

目录结构及文件描述

目录树如下:

│  babel.config.js babel的配置文件
│  jsconfig.json babel的配置文件
│  package.json 应用包的配置文件
│  README.md 
│  vue.config.js 配置webpack相关
│  yarn.lock
│
├─public
│      favicon.ico 页签图标
│      index.html 主页面
│
└─src
    │  App.vue
    │  main.js
    │
    ├─assets 存放静态资源
    │      logo.png
    │
    └─components 存放组件
            HelloWorld.vue

其中:

babel.config.js 是babel的控制文件,用于将es6语法向下转换为es5的兼容语法,一般不需要做改动

package.json 是包的管理文件,用来记录vue整个工程的名字,版本,主要包,启动参数等

yarn.lock 是yarn的依赖包版本锁,用于控制依赖包及依赖包版本

public 目录用于存放静态html资源,也是网页的入口

src 目录用于存放源码,下面细说:

  • src 根目录一般存放 main.js App.vue
  • asserts目录里面主要是存放一些前端资源文件,如图片媒体等
  • components目录主要存放Vue的组件

README.MD 是说明文档啦~

P.S:如果安装了lint工具的话,记得在编写程序的时候不要使用它,不然会影响编程效率(

执行vue serve

执行了serve操作之后,vue首先就会执行main.js 之后会按照流程创建vm。引入组件等操作

注意:run 之后 可能会有lint的报错导致无法启动,为了方便,可以直接在vue.config.js里面的defineConfig函数对象参数中添加 lintOnSave:false 即可

Render函数

问题

我们之前在main.js内Vue函数里面是这样写的

new Vue({
    el: '#app',
    compoments: { App }
})

但是,当我们使用脚手架打包运行的时候,会提示如下错误

[Vue warn]: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
(found in )

通过提示可知道:现有的vue运行时环境并不能使用模板编译器,可以选择使用render函数,或者使用完整的带解析器的vue运行环境

因为我们引用的时候直接写了import Vue from 'vue'
在vue的源代码内其实是引用了 dist/vue.runtime.esm.js(ECMAScript Module)

p.s:不引入完整的vue执行和渲染效率会高一点,占用空间会低一点

解决方法

  1. 使用完整版的Vue:import Vue from 'vue/dist/vue'
  2. 使用render函数

Render函数原理

render函数包含一个参数,类型为function 默认是createElement函数

render(createElement) {
    console.log(typeof createElement); //function
    return createElement(App)
}

箭头函数写法:

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

关于不同版本的vue

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

修改默认配置

Vue脚手架隐藏了所有的 webpack 相关的配置,如果香查看具体的webpack的配置,请执行: vue inspect > output.js

可以修改

entry: {
    app: [
        './src/main.js'
    ]
}

这个对象来自定义入口函数的名字

但直接修改这一句显然是不会奏效的,需要修改vue.config.js

vue.config.js 是一个可选的配置文件,如果项目的(和package.json同级的)根目录中存在这个文件,那么它会被@vue/cli-service自动加载。你也可以使用package.json中的 vue字段,但是注意这种写法需要你严格遵照JSON的格式来写。

如果我们想要修改main.js入口文件的名字,需要进入vue.config.js中的pages对象中进行修改:

//自定义pages
pages: {
    index: {
        // page 的入口
        entry: 'src/main.js',
        // 模板来源
        template: 'public/index.html',
        // 在 dist/index.html 的输出
        filename: 'index.html',
        // 当使用 title 选项时,
        // template 中的 title 标签需要是 <title><%= htmlWebpackPlugin.options.title %></title>
        title: 'Index Page',
        // 在这个页面中包含的块,默认情况下会包含
        // 提取出来的通用 chunk 和 vendor chunk。
        chunks: ['chunk-vendors', 'chunk-common', 'index']
    }
}

其他配置项可以i对脚手架进行个性化定制,详见:配置参考 | Vue CLI (vuejs.org)

使用ref标记DOM

有些时候我们需要操作DOM,但在Vue中使用原生DOM操作指令显得不是太友好,所以Vue为我们提供了一个比较好用的操作DOM的方法:ref标记

只需要给template标签中的元素添加一个ref属性即可:

<h1 v-text="msg" ref="title"></h1>

然后就可以使用vc中的:$refs 进行控制DOM等操作

...
    // 使用$refs可以输出设定的ref标记的DOM
    console.log(this.$refs.title);
...

但这里有个需要注意的点:如果给组件标签打上ref属性,那$refs的输出则是该组件标签与之对应的组件实例对象:

<School ref="school" />
...
    //组件标签的ref就是与之对应的组件
    console.log(this.$refs.school);
...

ref总结:

  1. ref属性被用来给元素或子组件注册引用信息 (id的替代者)
  2. 应用在html标签上获取的是真实DOM元素,应用在组件标签上是组件实例对象(vc)
  3. 使用方式:
    1. 打标识:

    2. 获取: this.$refs.msg

配置项porps

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

  1. 传输数据:

    1. <Demo name="xxx"/>
  2. 接收数据:

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

    2. 第二种方式(限制类型)

      props:{
        name:Number
      }
    3. 第三种方式(限制类型,限制必要性,指定默认值)

      props:{
        name:{
            type:String, //类型
            required:true, //必要性
            default: '33'
        }
      }

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

混入(mixin)

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

使用方式:

  1. 第一步定义混合,例如:

    export const mixin = {
       data(){...},
       methods:{...}
       ...
    }
  2. 第二步使用混入,例如:

    1. 全局混入:Vue.mixin(mixin)
    2. 局部混入:mixins:[mixin]

代码示例:

准备混入文件(mixin.js)

//分别暴露,抽离方法成混合
export const mixin = {
    methods: {
        showName() {
            alert(this.name)
        }
    },
    mounted() {
        console.log('加载完成');
    },
}
export const mixin2 = {
    data() {
        return {
            msg: 'kanokano'
        }
    }
}

局部混入(组件.vue):

//局部混合
import {mixin,mixin2} from '../mixin.js'
export default {
    name:'School',
    data(){
        return{
            name:'MIT',
            address:'ddddd'
        }
    },
    // mixins:[mixin,mixin2]
}

全局混入(main.js):

//全局混合
import { mixin, mixin2 } from './mixin'
Vue.mixin(mixin)
Vue.mixin(mixin2)

插件

功能:用于增强Vue

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

定义插件:

对象.instll = function(Vue,options){
    //添加全局过滤器
    Vue.filter(...);
    //添加全局指令
    Vue.directive(...);
    //配置全局混合(入)
    Vue.mixin(...);
    //添加实例方法
    Vue.prototype.$myMethod = function(){...};
    Vue.prototype.$myProperty = xxxx;;
}

使用插件:Vue.use(插件名)

scoped(局部)样式

在编写组件的时候,我们在每个组件中的style样式里面编写很多自定义样式,但是每个样式名字很容易重合,如果名字重合了,会出现样式覆盖的问题。

比如: 在student.vue 中,编写了demo样式,在school.vue中同样也编写了demo样式,而且在app.vue中先引入了school,后引入student,就会出现后者的样式覆盖掉前者的同名样式的问题

想要解决这个问题的话,需要使用一个全新的配置项: scoped

用法:只需要在style标签中添加即可

<style scoped>
.demo{
    background-color: skyblue;
}
</style>

样式块兼容less

想要在style块中兼容less,那么就需要在style标签里面添加 lang属性,写法如下:

<style scoped lang="less">
.demo{
  background-color: orange;
  .sex{
    background-color: green;
  }
}
</style>

注意:直接保存可能会报错,因为没有安装webpackless-loader插件

使用 npm i less-loader 即可

*不写lang属性默认就是css

组件化编码流程(通用)

  1. 实现静态组件:抽取组件。使用组件实现静态页面效果
  2. 展示动态数据:
    • 数据的类型、名称是什么?
    • 数据保存在哪个组件?
  3. 交互——从绑定事件监听开始

TodoList案例

代码下载:https://kanokano.cn/wp-content/uploads/2022/08/todo.zip

总结案例

  1. 组件化编码流程:
    • 拆分静态组件:组件要按照功能点拆分,命名不要与html元素冲突
    • 实现动态组件:考虑好数据的存放位置,数据是一个组件再用,还是一些组件再用:
      • 一个组件再用,放在组件自身就行
      • 一些组件再用:放在他们的共同的父组件上(状态提升)
    • 实现交互:从绑定事件开始
  2. props适用于:
    • 父组件 ==》 子组件 通信
    • 子组件 ==》 父组件 通信(要求父先给子一个函数)
  3. 使用v-model时要切记:v-model绑定的值不能是props传过来的值,因为props是不可以被修改的
  4. props传过来的若是对象类型的值,修改对象中的属性时Vue不会报错,但不推荐这么做
  5. 可以通过本地存储进行保存状态

组件的自定义事件

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

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

  3. 解绑自定义事件:

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

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

      
      ....
      mounted(){
        this.$refs.dd.$on('kano',this.test)
      }
    3. 若想让自定义事件只能触发一次,只需要加上once修饰符或者$once方法

    4. 触发自定义事件:this.$emit('kano',传参)

    5. 解绑自定义事件:this.$off('kano') 不写参数则为全部解绑,多参数需写成数组形式

    6. 组件上也可以绑定原生DOM事件,但需要native修饰符,例如<Student @click.native="show"/>

    7. 注意:通过this.$refs.xxx.$on(xxx,回调函数)绑定自定义事件时,回调要门配置在method中,要么使用箭头函数,总是需要注意this的指向

P.S: 组件的自定义事件可以在Todolist中用,替换掉全选等方法

全局事件总线

全局事件总线(Global Event Bus):可以实现任意组件之间的互相通信,数据传递

全局事件总线,其实不是一个特定的接口或者方法,而是前人总结出来的一种实现组件间互相通信的结构

原理与使用

由于Vue有一个内置的关系:VueComponent.prototype.__proto__ === Vue.prototype
我们可以直接利用这个内置关系,在Vue.prototype上创建一个总线的载体$bus(一般为vm的实例对象)

安装全局事件总线:

new Vue({
    ......
    //生命周期最开始的钩子
    beforeCreate() {
        //this就是这个vm
        //事件总线$bus是我们自己定义的
        Vue.prototype.$bus = this;
    }
})

使用事件总线:

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

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

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

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

//School.vue
...
beforeDestroy(){
    //用完就销毁,防止占用总线
    this.$bus.$off('hello');
}

小结

全局事件总线,其实就是在普通的自定义事件上,稍作修改,把事件绑定在了一个所有组件都能访问的原型对象(VM)上而已

消息订阅与发布(pubsub)

概念

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

使用步骤

  1. 安装pubsub:npm i pubsub-js

  2. 引入:import pubsub from pubsub-js

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

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

  5. 最好在beforeDestroy钩子中,用pubsub.unscrible(pid)取消订阅

这个组件能实现类似全局事件总线的功能,但是相比全局事件总线来说,需要手动安装,没有全局事件总线的原生优势

$nextTick

这个是在TodoList项目中使用的一个新的方法

语法this.$nextTick(回调函数)

作用:在下一次DOM更新结束后执行指定的回调方法

nextTick主要是用于处理数据动态变化后,DOM还未及时更新的问题,用nextTick就可以获取数据更新后最新DOM的变化:

在Todolist项目中,当我们点击编辑按钮时,会出现一个input框,我们就需要在点击编辑按钮后立马获取input的焦点,如果直接在编辑按钮的点击事件中focus input框的话,是无效的,因为Vue要先执行完method里面的东西之后,才会更新DOM文档

这时候就会想到,可以用异步代码去滞后执行自动对焦。比如定时器

当然是有更合理的解决方法的,Vue给我们提供了一个方法#nextTick 用nextTick就可以在模板更新后调用预设的方法:

edit(todo){
    ......
    this.$nextTick(()=>{
        this.$refs.autoFocus.focus();//自动对焦
    })
 }

Vue封装的过渡与动画

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

  2. 图示:
    image-20220903003656759

  3. 写法:

    1. 准备好样式:

      • 元素进入的样式:
        1. v-enter:进入的起点
        2. v-enter-active:进入过程中
        3. v-rnter-to:进入的终点
      • 元素离开的样式:
        1. v-leave:离开的起点
        2. v-leave-active:离开过程中
        3. v-leave-to:离开的终点
    2. 样式写法示例:

      /*vue能自动识别的样式*/
      .kanokano-enter-active,.kanokano-leave-active{
        transition: .5s linear;
      }
      /*进入的起点*//*离开的终点*/
      .kanokano-enter,.kanokano-leave-to{
        transform: translateX(-100%);
      }
      /*进入的终点*//*离开的起点*/
      .kanokano-enter-to,.kanokano-leave{
        transform: translateX(0);
      }
    3. 使用包裹要过度的元素,并配置name属性:

      
        

      HELLO

    4. 备注:若有多个元素需要过度,则需要使用:,且每个元素都要指定key值:

      
        

      HELLO

      HELLO

    5. 想让元素默认是显示,可以添加 appear 属性

    6. 样式的名字默认是v开头,如果想自定义样式头,可以在标签上加上name属性即可

      
        

      HELLO

    7. 使用插件控制过渡(animate.css):
      首先需要安装插件:npm install animate.css

      //由于是js,所以不需要from什么什么的
      import 'animate.css';
      
      
        

      HELLO

      HELLO

Vue代理

当我们在脚手架中编写ajax请求的时候,与服务器通信,有时候会遇到跨域问题

一般的做法就是在后端进行处理,设置响应头,或者使用cors插件进行跨域处理

但我们也可以使用 代理服务器 的方式进行间接的请求

代理服务器请求原理:

因为ajax请求有跨域问题,但是代理服务器与后端服务器之间的通信并不是使用ajax进行传输的,所以自然不会存在跨域的问题

使用方法

配置

  • 在vue.config.js中添加一个新属性:

    // 开启代理服务器
    devServer: {
      proxy: '后端服务器地址'//例如:http://api.kanokano.cn
    }

    这会告诉开发服务器将任何未知请求 (没有匹配到静态文件的请求) 代理到后端服务器地址

  • 把需要ajax请求的地址统一换为脚手架的服务器地址:

    axios.get('http://localhost:8080/student').then(
      response =>{
          console.log('请求成功',response.data);
      },
      error=>{
          console.log('请求失败',error.message);
      }
    )
  • 如果你想要更多的代理控制行为,也可以使用一个 path: options 成对的对象。

    devServer: {
      proxy: {
        '/api': {//匹配所有以’/api‘开头的请求路径
          target: '',//代理目标的基础路径
          ws: true,//websocket
          changeOrigin: true //修改请求源
        },
        '/foo': {
          target: ''
        }
      }
    }

    注意:有时设置代理的时候,需要去掉请求头 比如上面的'/api' 防止带入到后端服务器造成404
    使用pathRewrite 重写请求地址,格式:pathRewrite:{'正则表达式','替换字符串'}

    proxy: {
      '/api': {
          target: '',
          pathRewrite: {
              '^/api': ''
          }
      }
    },

github搜索框案例

案例下载: 本地下载

Vue-resource

注意,此插件已过时

如果不想用axios发送http请求,也可以使用一个Vue插件:Vue-resource

安装:

npm i vue-resource

引入:

//引入,使用资源文件
import vueResource from 'vue-resource';
Vue.use(vueResource);

使用:

//不使用axios 使用插件带的$http 是一样的效果,不过用的少
this.$http.get(`xxx`).then(
    response => {
        ...
    },
    error => {
        ...
    }
)

slot 插槽

当我们想要在同一个组件里面展示不同内容的时候,除了使用v-if之外,还可以使用插槽 来分发内容

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

分类: 默认插槽,具名插槽,作用域插槽

默认插槽

我们可以通过定义一个<slot></slot>来作为一个插槽

使用方法:

App.vue

<Category title="美食" :ListData="foods">
    <img src="xxx" alt="">
</Category>
<Category title="游戏" :ListData="games"/>
<Category title="电影" :ListData="films">
    <video src="xxxxx" muted="muted" autoplay="autoplay" loop="loop">
        您的浏览器不支持HTML5标签。
    </video>
</Category>

Category.vue

<!-- 定义一个插槽(挖个坑等待父级填充,如果父级有填充,则使用父级的结构,没有就使用插槽内的结构) -->
<slot>
    <ul>
        <li v-for="(item,index) in ListData" :key="index">{{item}}</li>
    </ul>
</slot>

具名插槽

有时我们需要多个插槽,默认插槽显然已经不能满足多插槽的使用情景,下面将引入具名插槽

使用方法:

App.vue

<Category title="美食">
    <!-- 自定义插槽名 -->
    <img slot="center" src="XXX" alt="">
    <a slot="footer" href="#">更多美食</a>
</Category>

Category.vue

<!-- 定义一个插槽(挖个坑等待父级填充,如果父级有填充,则使用父级的结构,没有就使用插槽内的结构) -->
<slot name="center"></slot>
<slot name="footer"></slot>

作用域插槽

理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定,(games数据在category组件中,但使用数据所遍历出来的结构由app组件决定)

2.具体编码:

App.vue

<Category title="游戏" >
    <!-- 子组件传递的数据包装了一层对象 -->
    <template scope="scop">
        <ul>  
            <li v-for="(item,index) in scop.games" :key="index">{{item}}</li>
        </ul>
        <h3>{{scop.msg}}</h3>
    </template>
</Category>

<Category title="游戏" >
    <!-- 还可以支持解构赋值 -->
    <template scope="{games}">
        <ol>  
            <li v-for="(item,index) in games" :key="index">{{item}}</li>
        </ol>
    </template>
</Category>

<Category title="游戏" >
    <!-- scope的另一种写法,slot-scope -->
    <template slot-scope="{games}">
        <h4 v-for="(item,index) in games" :key="index">{{item}}</h4>
    </template>
</Category>

Category.vue:

<!-- 使用作用域插槽,可以在app里面访问到这个组件传过去的东西 -->
<slot :games="games" msg="hello">我是默认内容</slot>
<script>
...
    data(){
      return{
        games:['GTA5','CSGO','原','MC'],
      }
    }
...
</script>

总结:这三种插槽可以按照实际情况配合使用

VueX

vueX是什么

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

什么时候使用VueX

  1. 多个组件依赖于同一个状态
  2. 来自不同组件的行为需要变更同一个状态

工作原理

vuex

上图是VueX的 “状态管理模式”:

这个状态自管理应用包含以下几个部分:

  • state,驱动应用的数据源(公共数据);
  • view,以声明方式将 state 映射到视图;
  • actions,响应在 view 上的用户输入导致的状态变化(类似中间件)。

其实就是类似于后端开发中的三层结构:视图层 业务层 持久层

工作流程

  1. 组件请求数据,使用 dispatch('name','kano'),请求一个变量名(key)为name,值为‘kano’的函数
  2. dispatch函数会到Actions中,这里类似中间件,可以在这里发送后端ajax请求,进行数据处理等操作,之后Actions会执行commit('name','kano')到Mutations
  3. commit函数到达Mutations中,会进行查找key为‘name’对应的函数,在这个函数内,存放了state 中的公共数据,进行Mutate操作,也就是 修改/处理 数据
  4. 在Mutate操作,修改数据成功时,Vue会渲染修改后的DOM到视图中,这样就完成了修改数据的流程

注意:Actions如果不进行特殊操作,仅仅只是传递数据,可以直接在Vue Component步骤时候直接调用commit('name','kano') 就可以跳过Actions阶段

搭建VueX环境

安装VueX:

注意:vue2中需要使用VueX的3版本,Vue3中需要使用VueX的4版本

Vue2:

npm i vuex@3

Vue3:

npm i vuex

使用VueX:

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

//该文件用于创建Vue中最为核心的store
//引入Vue
import Vue from 'vue'
//在这引入可以避免在use Vuex之前就创建了store造成报错
//引入Vuex
import Vuex from 'vuex'
//使用Vuex
Vue.use(Vuex);
//准备actions 用于响应组件中的动作
const actions = {}
//准备mutations 用于操作数据()
const mutations = {}
//准备state 用于存储数据
const state = {}
//准备一个getters ,用于将state中的数据进行加工
const getters = {}
//准备store
const store = new Vuex.Store({
    actions,
    mutations,
    state,
    getters
})
//导出
export default store

main.js中创建vm时传入store配置项

import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
//引入插件
import VueResource from 'vue-resource';
//使用插件
Vue.use(VueResource);
//引入store
import store from './store';
new Vue({
    render: h => h(App),
    store, //挂载为了$store
    beforeCreate() {
        Vue.prototype.$bus = this
    },
}).$mount('#app')

组件中读取Vuex中的数据:$store.state.sum

组件中修改Vuex中的数据:$store.dispatch('action中的方法名',数据) 或者 $store.commit('action中的方法名',数据)

getters的使用

src/store/index.js中添加getters对象即可

.......
//准备一个getters ,用于将state中的数据进行加工
const getters = {}
const store = new Vuex.Store({
    ......,
    getters
})

使用getters:$store.getters.属性名

四个map方法的使用

mapState方法:用于帮助我们映射state中的数据为计算属性

computed:{
    //使用mapState,,对象解构,因为mapState返回的是一个对象,用于映射状态对象属性
    //借助mapState读取state中的属性
    //..mapState({sum:'sum',nickname:'nickname',age:'age'}),

    //数组写法,如果需要映射的名称是一样的话
    ...mapState(['sum','nickname','age']),
}

mapGetters 方法:用于帮助我们映射getters中的数据为计算属性

computed:{
    //mapGetters也是一样的,借助它读取getters里面的属性
    // ...mapGetters({bigSum:'bigSum'}),

    //数组写法
    ...mapGetters(['bigSum'])
}

mapActions方法:用于帮助我们生成actions对话的方法,即:包含$store.dispatch(xxx)函数

methods:{
     //靠mapActions生成:incrementOdd,incrementWait(对象形式)
     ...mapActions({incrementOdd:'addOdd',incrementWait:'addWait'}),
     //靠mapActions生成:incrementOdd,incrementWait(数组形式),前提是方法和属性名相同
     ...mapActions(['addOdd','addWait'])
}

mapMutations方法:用于帮助我们生成mutations对话的方法,即:包含$store.commit(xxx)函数

methods:{
    //对象展开写法,这样的写法无法写参数,所以在使用的时候需要手动传入参数
    //方法会调用commit去联系mutation
    ...mapMutations({increment:'ADD',decrement:'DECR'}),

    //数组写法,方法会调用commit去联系mutation,前提是名字需要一致
    ...mapMutations(['ADD','DECR'])
}

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

VueX模块化与namespace

目的:让代码更好维护,让多种数据分类更加明确

修改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
    }
})

开启命名空间后,组件中读取state数据

//方式一,直接自己读取
this.$store.state.personAbout.list
//方式二,借助mapState读取
...mapState('countAbout',['sum','name','age']);

开启命名空间后,组件中读取getters数据

//方式一,直接自己读取
this.$store.getters['personAbout/firstPersonName']
//方式二,借助mapGetters读取
...mapGetters('countAbout',['bigSum'])

开启命名空间后,组件调用dispatch

//方式一,直接自己dispatch
this.$store.dispatch('personAbout/addPersonNotWang',personObj)
//方式二,借助mapActions
...mapActions('countAbout',{incrementOdd:'addOdd',incrementWait:'addWait'}),

开启命名空间后,组件调用commit

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

求和案例(VueX)

代码下载: 本地下载

Vue-router

相关理解

vue-router 的理解

是一个Vue的插件库,专门用来实现SPA应用

对SPA应用的理解

  1. 单页 Web 应用 (single page web application,SPA)
  2. 整个应用只有一个完整的页面
  3. 点击页面中的导航链接不会刷新页面,只会做页面的局部刷新
  4. 数据需要通过ajax请求获取。

路由的理解

  1. 什么是路由?
    1. 一个路由就是一组映射关系(key - value)
    2. key为路径,value可能是function或component
  2. 路由的分类
    1. 后端路由:
      1. 理解:value 是 function,用于处理客户端提交的请求
      2. 工作过程:服务器接收到一个请求时,根据请求路径找到匹配的函数来处理请求,返回响应数据
    2. 前端路由:
      1. 理解:value 是 component,用于展示页面内容
      2. 工作过程:当浏览器路径改变时,对应的组件就会显示。

路由的基本使用

  1. 安装vue-router,命令npm i vue-router(vue2安装 vue-router@3)

  2. 引用并应用插件:vue.use(VueRouter)

  3. 编写router配置项(/src/router/index.js):

    // 该文件专门用于创建整个应用的路由器
    import VueRouter from 'vue-router'
    
    import About from '../components/About'
    import Home from '../components/Home'
    
    //创建路由器,并共享出去
    export default new VueRouter({
       routes: [{
               path: '/about',
               component: About
           },
           {
               path: '/home',
               component: Home
           }
       ]
    })
  4. 实现切换(active-class可配置高亮样式)

    About
  5. 指定展示位置

注意:

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

多级路由

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

routes: [{ //一级路由
            path: '/about',
            component: About
        },
        { //一级路由
            path: '/home',
            component: Home,
            children: [{
                    path: 'news', //子路由,不加斜杠
                    component: News
                },
                {
                    path: 'message', //子路由
                    component: Message,
                    children: [{
                        path: 'detail',
                        component: Detail
                    }]
                }
            ]
        }
    ]

跳转(需写完整路径)

<!-- 二级路由 -->
<router-link class="list-group-item" active-class="active" to="/home/news">News</router-link>

路由的query参数

  1. 传递参数

    
    
    
    
    {{L.title}}
    
  2. 接收参数

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

命名路由

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

  2. 如何使用:

    1. 给路由命名:

      {
        path: 'message', //子路由
        component: Message,
        children: [{
             name: 'xiangqiang',
             path: 'detail',
             component: Detail
        }]
      }
    2. 简化跳转:

      
      {{L.title}}
      
      
      跳转
      

路由的params参数

  1. 配置路由,声明params参数

    {
       path: 'message', //子路由
       component: Message,
       children: [{
             name: 'xiangqing',
             // params参数,:表示占位符 
             path: 'detail/:id/:title',
             component: Detail
        }]
    }
  2. 传递参数

    
    {{L.title}}  
    
    
    {{L.title}}
    

    特别注意:路由携带params参数时,若使用to的对象写法,则不能使用path配置项,需要使用name写法

  3. 接收参数:

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

路由的props配置

作用:让路由组件更方便的接收到参数

{
    path: 'message', //子路由
    component: Message,
    children: [{
        name: 'xiangqing',
        // params参数,:表示占位符 
        path: 'detail/:id/:title',
        component: Detail,
        //props的第一种写法,值为对象,该对象的所有的key-value都会以props形式传给Detail组件
        // props: {
        //     a: 1,
        //     b: 'hello'
        // }

        //props的第二种写法,值为布尔值,若为真,就会把该路由组件收到的所有的params参数,以
        //props的形式传给Detail组件 (query不行)
        // props: true

        //props第三种写法,值为一个回调函数,就会把该路由组件收到的所有的参数传递回去,可query也可params
        props($route) {
            return {
                id: $route.params.id,
                title: $route.params.title
            }
        }
    }]
}

接收端只需要添加相应的props参数即可props:['id','title']

<router-link>的replace属性

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

编程式路由导航

  1. 作用:不借助<router-link>实现路由跳转,让路由跳转更加灵活

  2. 具体编码:

    //$router的两个API
    pushShow(L){
     // console.log(this.$router);
     //手动push
     this.$router.push({
       name:'xiangqing',
       params:{
         id:L.id,
         title:L.title
       }
     })
    },
    replaceShow(L){
     //手动replace
     this.$router.replace({
       name:'xiangqing',
       params:{
         id:L.id,
         title:L.title
       }
     })
    }

    前进和后退:

    //后退
    back(){
       this.$router.back();
    },
    //前进
    forward(){
       this.$router.forward();
    },
    //可前可后
    testgo(){
       this.$router.go(-2);
    }

缓存路由组件

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

  2. 具体编码:

    
    
       
    

    如果想缓存多个组件,可以写成数组形式

    
       
    

两个新的生命周期钩子

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

具体名字:

  1. actived路由组件被激活时触发。
  2. deactivate路由组件失活时触发。

active

//激活
activated(){
    console.log('我被激活了');
    this.timer = setInterval(() => {
        this.opacity -= 0.01
        console.log('@');
        if(this.opacity <= 0){
            this.opacity = 1
        }
    }, 16);
},

deactivate

//失活
deactivated(){
    console.log('我失活了');
    clearInterval(this.timer);
}

路由守卫

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

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

  3. 全局守卫:

    //全局前置路由守卫
    //在每一次路由切换之前都会调用这个函数,初始化的时候也会调用这个函数
    router.beforeEach((to, from, next) => {
       console.log(to, from, next);
       //使用元信息来匹配需要授权的路由
       if (to.meta.isAuth) {
           // if (to.name === 'xinwen' || to.name === 'xiaoxi') {
           // if (to.path === '/home/news' || to.path === '/home/message') {
           if (localStorage.getItem('name') !== 'kano') {
               alert('全局前置路由守卫不允许你进入该路径')
           } else {
               next();
           }
       } else {
           //放行
           next();
       }
    });
    
    //后置全局路由守卫 初始化的时候被调用、每次路由切换之后被调用
    router.afterEach(
       (to, from) => {
           console.log('后置路由守卫', to, from);
           //借助后置路由守卫,切换完成后执行,就不会出现标题bug
           document.title = to.meta.title || 'Vue'
       }
    )
    export default router;
  4. 独享路由守卫(只有前置没有后置)

    ///独享前置路由守卫(没有后置)
    beforeEnter: (to, from, next) => {
       if (to.meta.isAuth) {
           if (localStorage.getItem('name') !== 'kano') {
               alert('独享前置路由守卫不允许你进入该路径')
           } else {
               next();
           }
       } else {
           //放行
           next();
       }
    }
  5. 组件内守卫(千万不要理解为前置或者后置守卫,没有这种说法)

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

路由器的两种工作模式

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

切换方法:

//创建路由器,并共享出去
const router = new VueRouter({
    //选择路由工作模式(不写默认是hash)
    mode: 'history',
    // mode: 'hash'
    //两者的区别,hash模式兼容性好一点,但是会多一个#
    //history不会有多余的字符,但是兼容性稍差
.....

ElementUI

引入 Element

你可以引入整个 Element,或是根据需要仅引入部分组件。我们先介绍如何引入完整的 Element。

完整引入

在 main.js 中写入以下内容:

import Vue from 'vue';
import ElementUI from 'element-ui';
import 'element-ui/lib/theme-chalk/index.css';
import App from './App.vue';

Vue.use(ElementUI);

new Vue({
  el: '#app',
  render: h => h(App)
});

以上代码便完成了 Element 的引入。需要注意的是,样式文件需要单独引入。

按需引入

借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。

首先,安装 babel-plugin-component:

npm install babel-plugin-component -D

然后,将 .babelrc 修改为:

{
  "presets": [["es2015", { "modules": false }]],
  "plugins": [
    [
      "component",
      {
        "libraryName": "element-ui",
        "styleLibraryName": "theme-chalk"
      }
    ]
  ]
}

接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:

import Vue from 'vue';
import { Button, Select } from 'element-ui';
import App from './App.vue';

Vue.component(Button.name, Button);
Vue.component(Select.name, Select);
/* 或写为
 * Vue.use(Button)
 * Vue.use(Select)
 */

new Vue({
  el: '#app',
  render: h => h(App)
});

Vue3

使用vite创建工程

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

常用 Composition API

1.setup函数

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

属性

2.ref函数

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

3.reactive函数

  • 作用:定义一个对象类型的响应式数据(基本类型别用他,用ref函数)
  • 语法:const 代理对象 = reactive(被代理对象)接收一个对象(或数组),返回一个代理器对象(Proxy的实例对象)
  • reactive定义的响应式数据是’深层次的‘
  • 内部基于ES6中的Proxy实现,通过代理对象操作源对象内部数据都是响应式的

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

vue2.x的响应式
  • 实现原理:

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

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

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

    • 新增属性、删除属性,界面不会更新(没有响应式)
    • 直接通过下标修改数组,界面不会自动更新
Vue3.0的响应式
  • 实现原理:
    • 通过Proxy(代理): 拦截对象中任意属性的变化,包括属性值的读写、属性的添加与删除等
    • 通过Reflect(反射):对被代理对象中的属性进行操作。
    • MDN文档中有描述Proxy和Reflect的信息,可以去看看
//模拟Vue3中实现响应式
const p = new Proxy(person, {
    //target是p propname是操作的p中的属性名字
    //捕获读取的动作
    get(target, propName) {
        console.log(`有人读取了p身上的${propName}属性`);
        return Reflect.get(target, propName)
    },
    //捕获修改、添加的动作
    set(target, propName, value) {
        console.log(`有人修改了p身上的${propName}属性`)
        Reflect.set(target, propName, value)
    },
    //捕获删除的动作
    deleteProperty(target, propName) {
        console.log(`有人删除了p身上的${propName}属性`);
        //删除一个对象,返回删除的状态
        return Reflect.deleteProperty(target, propName)
    }
});
reactive对比ref
  • 从定义数据角度对比
    • ref用来定义:基本类型数据
    • reactive用来定义:对象(或数组)类型的数据
    • 备注:ref也可以用来定义对象(或数组)类型的数据,它内部会自动通过reactive转换为代理对象
  • 从原理角度对比:
    • ref通过Object.defineProperty()getset来实现响应式(数据劫持)
    • reactive通过使用 proxy 来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据
  • 从使用角度来对比:
    • ref定义的数据:操作数据需要.value 读取数据时模板中直接读取,不需要·value
    • reactive定义的数据:操作数据与读取数据:均不要.value
setup的两个注意点
  • setup执行的时机
    • 在beforeCreate之前执行一次,this是undefined
  • setup的参数
    • props:值为对象,包含:组件外部传递过来,且组件内声明接收了的属性
    • context:上下文对象
    • attrs:值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性,相当于this.$attrs
    • slots:收到的插槽内容,相当于this.$slots
    • emit:分发自定义事件的函数,相当于this.$emit
计算属性与监视
  1. computed函数

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

    • 写法

      import {computed} from 'vue';
      //Vue3中的计算属性-简写形式,没有考虑计算属性被修改的情况
      // person.fullName = computed(()=>{
      //   return person.firstName + person.lastName
      // })
      //完整写法
      person.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定义的多个响应式数据(使用数组)
      //如果是reactive的对象设置的属性,默认会强制开启深度监视(deep配置无效),无需设置,也不能关掉深度监视
      watch([sum,msg],(newValue,oldValue)=>{
       console.log('sum变化',newValue,oldValue);
      },{immediate:true})
      
      //情况三:监视reactive所定义的一个响应式数据,注意,此处无法正确获取oldValue
      watch(person,(newValue,oldValue)=>{
       console.log('person变化了');
       console.log(newValue,oldValue);//你会发现这两个的值是一样的
      })
      
      //情况四:监视reactive所定义的一个响应式数据中的某一个属性
      watch(()=>person.job.j1.salary,(newValue,oldValue)=>{
       console.log('salary变化了');
       console.log(newValue,oldValue);//你会发现这两个的值是一样的
      })
      
      //情况五:监视reactive所定义的一个响应式数据中的某些属性
      watch([()=>person.job.j1.salary,()=>person.name],(newValue,oldValue)=>{
       console.log('salary或者name变化了');
       console.log(newValue,oldValue);//你会发现这两个的值是一样的
      })
      
      //特殊情况,由于job没有地址上的变化,只是他里面属性发生了变化,所以监视不奏效
      watch(()=>person.job,(newValue,oldValue)=>{
       console.log('salary变化了');
       console.log(newValue,oldValue);
      },{deep:true})//此处由于监视的是reactive定义的对象的某个属性,deep有效

说白了,监视的如果是个对象里面包裹着对象,那你直接用reactive就可以直接监视到数据,如果你用的是ref,那么就加上deep或者是在监视对象后面加上value 即可正常监测数据变化

watchEffect函数
  • watch的套路是:既要指明监视的属性,又要指明监视的回调
  • watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性
    也就是,只会监视函数内所依赖的属性,并且默认immediate为true
  • watchEffect比较像compured:
    • 但computed注重的是计算出来的值(回调函数的返回值),所以必须要写返回值
    • 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值也行
//监视函数里面使用到的数据
watchEffect(()=>{
    //监视sum name salary
    const x1 = sum.value
    const x2 = person.name
    const x3 = person.job.j1.salary
    console.log('watchEffect所指定的回调执行了');
})

5.Vue3中的生命周期

Component lifecycle diagram

6.通过组合式API调用生命周期钩子

  1. 在Vue3中,setup会比beforeCreate和created的优先级要高 ,所以使用组合式API的时候,setup已经替代掉这两个生命周期钩子

代码演示

setup(){
    //数据
    let sum = ref(0)

    //通过组合式API(setup内)的形式使用生命周期钩子
    //组合式API中的生命周期钩子比配置项钩子先执行
    console.log('----setup----');
    onBeforeMount(()=>{
        console.log('----onBeforeMount----');
    })
    onMounted(()=>{
        console.log('----onMounted----');
    })
    onBeforeUpdate(()=>{
        console.log('----onBeforeUpdate----');
    })
    onUpdated(()=>{
        console.log('----onUpdated----');
    })
    onBeforeUnmount(()=>{
        console.log('----onBeforeUnmount----');
    })
    onUnmounted(()=>{
        console.log('----onUnmounted----');
    })

    //返回一个对象
    return {sum}
},

还有一个注意点是组合式API中的生命周期钩子比配置项钩子先执行

7.自定义hook函数

  • 什么是hook? ——本值是一个函数,把setup函数中使用的CompositionAPI进行封装
  • 类似vue2.x中的mixin
  • 自定义hook的优势:复用代码,让setup中的逻辑更清楚易懂

8.toRef

  • 作用:创建一个ref对象,其value值指向另一个对象中的某个属性值
  • 语法:const name = toRef(person,'name')
  • 应用:要将响应式对象中的某个属性单独提供给外部使用时
  • 扩展:toRefstoRef 功能一致,但可以批量创建多个ref对象,语法:toRefs(person)
import {reactive,toRef,toRefs} from 'vue';
setup(){
    //数据
    let person = reactive({
        name:'张三',
        age:18,
        job:{
            j1:{
                salary:20
            }
        }
    })

    //返回一个对象
    return {
        //这样做是无效的,会失去响应式,这是值传递而不是引用传递
        // name : person.name
        // name:toRef(person,'name'),
        // age:toRef(person,'age'),
        // salary:toRef(person.job.j1,'salary')
        //person.job本身就是引用类型,所以不需要toRef
        // job:person.job

        //torefs可以给里面每一个属性变成refimpl
        ...toRefs(person),
        ...toRefs(person.job.j1)
    }
}

其他Composition API

1.shallowReactive 与 shallowRef

  • shallowReactive:只处理对象最外层属性的响应式(浅响应式)
  • shallowRef:只处理基本数据类型的响应式,不进行对象的响应式处理
  • 什么时候使用?
    • 如果有一个对象数据,结构比较深,但变化时只是外层属性变化==>shallowReactive
    • 如果有一个对象数据,后续功能不会修改该对象中的属性,而是生成新的对象来替换==>shallowRef
// 引用
import {ref,reactive,toRefs,shallowReactive,shallowRef} from 'vue';
//数据
//浅层reactive,只处理第一层的对象的响应式
let person = shallowReactive({
    name:'张三',
    age:18,
    job:{
        j1:{
            salary:20
        }
    }
})
//shallowRef只处理基本数据类型的响应式,不进行对象响应式的计算
let x = shallowRef({
    y:0
})

2.readonly 与 shallowReadonly

  • readonly:让一个响应式数据变为只读的(深只读)。
  • shallowReadonly: 让一个响应式数据变为只读的(浅只读)
  • 应用场景:不希望数据被修改时(特别是别人传给你的)。
//person现在是只读的,readonly接收响应式数据
person = readonly(person)

//person的第一层对象的属性是只读的,job内的属性不受影响
person = shallowReadonly(person)

x  = readonly(x)

3.toRaw 与 markRaw

toRaw:

  • 作用:将一个由reactive生成的响应式对象转为普通对象
  • 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的而所有操作,不会引起页面更新

markRaw:

  • 作用: 标记一个对象,使其永远不会在成为响应式对象
  • 应用场景:
    • 有些值不应该被设置为响应式的,例如第三方的复杂类库
    • 当渲染具有不可变的数据源的大列表时,跳过响应式转换可以提高性能
// 引用
import {ref,reactive,toRefs,toRaw,markRaw} from 'vue';
//toRaw只能用在reactive创建的响应式对象上
function showRawPerson(){
    console.log(toRaw(person));

    //console.log(toRaw(x));//无效
}
function addCar(){
    let car = {name:'马自达',price:'1'}
    //添加的car是响应式的,得益于proxy
    // person.car = car

    //标记为markRaw的数据,即使数据更改了,vue也不会更新数据到页面
    person.car = markRaw(car)
}
function replaceCar(){
    person.car.name = 'nmsl'
    person.car.price++
    console.log(person.car);
}

4.customRef

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

  • 实现防抖效果

    // 引用
    import {ref,customRef} from 'vue';
    //自定义ref,实现防抖效果
    function myRef(value,delay){
      //追踪器track和触发器trigger
      return customRef((track,trigger)=>{
          let timer
          return {
              get(){
                  console.log('getter',value);
                  track()//通知Vue追踪value的变化(提前和get商量一下,让他认为这个value是有用的)
                  return value
              },
              set(val){
                  console.log('setter',val);
                  clearTimeout(timer);
                  timer = setTimeout(()=>{
                      value = val
                      trigger()//通知vue重新解析模板
                  },delay)
              }
          }
      })
    }
    // let keyWord = ref('hello')//使用vue提供的ref
    let keyWord = myRef('hello',1000)//使用自定义的ref

5.provide 与 inject

  • 作用:实现祖孙组件间的通信(中间隔一个父)
  • 套路:父组件有一个provide选项来提供数据,子组件有一个inject选项来开始使用这些数据
  • 具体写法:

    • 1.祖组件中:
    import { reactive,toRefs,provide} from 'vue'
    setup(){
        let car = reactive({
            name:'马自达',
            price:114514,
        })
        provide('car',car)//给自己的后代组件传递数据
        return {
            ...toRefs(car)
        }
    }
    • 2.孙组件中
    import {inject} from 'vue'
    setup(){
        let car = inject('car')//类似全局事件总线
        return {
            car
        }
    }

6.响应式数据的判断

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

四、Compostion API的优势

Options API (Vue2)存在的问题

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

Composition API (Vue3)的优势

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

五、新的组件

Fragment

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

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>

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” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。

点赞

发表回复

电子邮件地址不会被公开。必填项已用 * 标注