Vue2/3入门笔记

Vue包括

  • Vue: 基础用户界面
  • Vue-cli: 脚手架, 用于工程化开发
  • vue-router: 前端路由
  • vuex: 数据保管服务

Vue2核心

Vue是一套用于构建用户界面(数据->页面)的渐进式(自底向上)JavaScript框架

  • 采用组件化的模式构建: 每一个组件都是一个.vue文件, 文件包含了组件需要的JS/CSS/HTML
  • 声明式编码: 在写代码的时候不需要实现考虑DOM的实现, 也无需直接操作DOM元素(像我们之前使用的NodeJS的拼串实际上就是命令式编码)
  • 采用虚拟DOM+Diff算法, 尽量复用DOM节点(在内容发生变化的时候优先在虚拟DOM中修改, 之后使用Diff算法比较变化后有哪些元素发生变化, 只对变化的元素进行更新)

引入Vue开发版: <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>

引入后, global就增加了Vue对象

Vue2的全局配置可以在Vue.config中修改,

HelloWorld

一个简单的实例调用Vue

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
    <div id="root">
        <h1>hello, {
  {name}}</h1>    <!--4. 插值语法将数据写入h1-->
    </div>
</body>
<script>
    new Vue({
                // 1. 创建一个Vue实例, 参数是配置对象
        el: '#root',    // 2. 选中MVVM到的元素, 参数可以是CSS选择器, 也可以是JS的元素对象(getElement...)
        data: {
              // 3. 需要绑定的数据, 可以是一个对象
            name: "Liu"
        }
    });    
</script>
</html>

尝试修改

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

<script>
    new Vue({
                
        el: 'h1',       // 有两个元素存在, 相当于我同时选了两个元素
        data: {
              
            name: "Liu"
        }
    }); 
</script>

发现, 只有第一个元素被选中了, 一个Vue实例只能对应第一个选中的元素, 如果真的想实现可以尝试

<div id="root">
    <h1 class = 'R1'>hello, {
  {name}}</h1>
    <h1 class = 'R2'>hello, {
  {name}}</h1> 
</div>

<script>
    new Vue({
                
        el: '.R1',       
        data: {
              
            name: "Liu"
        }
    });  
    new Vue({
                
        el: '.R2',       
        data: {
              
            name: "Liu"
        }
    });    
</script>

再去关注插值语法中 { { }} 可以写入些什么? 可以写入的是JS表达式, 例如

  • MVVM的变量(会被转译为app.data.变量): name, name.toUpperCase()
  • 算数表达式: 1+1
  • JS的表达式: Date.now()

我们还需要对元素的属性进行MVVM, 例如a标签的地址, 按照之前的思路我们应该写href={ {url}}, 这是不被接受的, 我们需要指令语法指定标签属性来被Vue绑定, 指定方法是将href={ {url}}改写为: v-bind:href="url"或者:href="url", 此处的href可以是任意属性, 之后Vue会将引号内的内容做替换, 例如

<body>
    <div id="root">
        <h1>插值语法: hello, {
  {name.toUpperCase()}}</h1>
        <h1>指令语法: </h1>
        <a v-bind:href='url'>click</a>
    </div>
</body>
<script>
    new Vue({
                
        el: '#root',       
        data: {
              
            name: "Liu",
            url: "https://liukairui.cc"
        }
    });  
</script>
  • 插值语法: 用于标签体, 使用{ { }}
  • 指令语法: 用于标签属性/标签体/标签事件, 使用v-XX: = ""

数据绑定

数据绑定有单向数据绑定与双向数据绑定, 区别是

  • 单向数据绑定使用v-bind:进行绑定, 只会在JS文件中对应值发生变化的时候修改DOM, 在DOM中值发生变化时并不会修改JS中变量
  • 双向数据绑定使用v-model:进行绑定, JS文件中对应值发生变化的时候修改DOM, 在DOM中值发生变化会反向修改JS中变量
<body>
    <div id="root">
        Name1: <input type="text" :value="name">
        Name2: <input type="text" v-model:value="name">
    </div>
</body>
<script>
    new Vue({
                
        el: '#root',       
        data: {
              
            name: "Liu",
        }
    });  
</script>

可以尝试修改Name1中的值, Name2不变, 修改Name2中的值, Name1发生变化

但是, 并不是所有的属性都可以使用v-model:绑定, v-model:只能绑定到表单类元素, 注意的是v-model:value可以简写成v-model(这是默认值)

el与data的多种写法

  • 可以使用.$mount()指定挂在对象, 从而取代el
    const app = new Vue({           
    -   el: '#root',       
        data: {         
            name: "Liu",
        }
    });  
    + app.$mount('#root')
    
    使用这种方法的有点是绑定更加灵活, 例如
    const app = new Vue({           
    -   el: '#root',       
        data: {         
            name: "Liu",
        }
    });  
    - app.$mount('#root')
    + setTimeout(()=>{app.$mount('#root')},1000);
    
  • 可以使用函数代替对象作为data(但是要求函数返回一个对象)
    const app = new Vue({           
        el: '#root',    
    -   data: {         
    -       name: "Liu",
    -   },   
    +   data() {
    +     return{
    +         name: 'Liu'
    +     }
    +   },
    });  
    
    p.s. 这个函数是Vue在执行的时候帮你调用的, 这里要求函数不能写箭头函数, 否则函数内部的this就变成global了, 而不是Vue(在后期后果有点严重)

MVVM模型

MVVM的意思是: 模型(JS)-视图(DOM)-视图模型

  • 模型中的数据通过视图模型的Bind绑定到视图上
  • 模型视图使用listeners监听视图的变化, 修改模型中的数据

可以将他们对应起来

<body>
    <div id="root">                                         <!-- V: 视图 -->
        Name1: <input type="text" :value="name">
        Name2: <input type="text" v-model:value="name">
    </div>
</body>
<script>
    const vm = new Vue({
                                         // VM: 视图模型
        el: '#root',            
        data: {
                                                  // M: 模型
            name: "Liu",
        }
    });  
</script>

可以在浏览器中Console中查看vm, 打印后可以看到是一个Vue对象, 里面有一个name属性正好是我们设置的name, 我们可以尝试调用一些非我们定义的属性, 例如:value="$el"我们发现页面确实显示了vm$el变量, 由此可见vm确实是一个视图模型, 视图通过调用视图模型变量实现数据绑定

数据代理

数据代理调用了方法是Object.defineProperty(), 使用方法是: Object.defineProperty(对象,键,{值的配置}), 例如

let person = {
   
  name: 'AAA',
  isMale: true,
};

Object.defineProperty(person, 'age', {
   
  value: 18,
  // enumerable:true, //控制属性是否可以枚举,默认值是false
  // writable:true, //控制属性是否可以被修改,默认值是false
  // configurable:true //控制属性是否可以被删除,默认值是false

  //当有人读取person的age属性时,get函数(getter)就会被调用,且返回值就是age的值
  get(){
   
      console.log('有人读取age属性了')
      return number
  },
  
  //当有人修改person的age属性时,set函数(setter)就会被调用,且会收到修改的具体值
  set(value){
   
      console.log('有人修改了age属性,且值是',value)
      number = value
  }
});

Object.defineProperty()与直接为对象加入元素的区别是, 该方法获得的对象默认不可枚举, 不可修改, 不可删除

数据代理就是通过一个对象, 代理对另一个对象属性的操作(读/写)

简单的数据代理, obj2代理obj1

let obj = {
   x:100}
let obj2 = {
   y:200}

Object.defineProperty(obj2,'x',{
   
    get(){
   
        return obj.x
    },
    set(value){
   
        obj.x = value
    }
})

在Vue中, 视图模型VM就实现了对模型M的代理, 在定义Vue实例的时候, data中的数据会被链接到app._data中(还要加入一些MVVM用的函数), 而data中的键key又会被app.key代理

  • 视图V获取数据(app.key)的时候, VM中的get就会获取M中的数据(app._data.key)
  • 视图V设置数据的时候, VM中的set就会修改M中的数据

事件处理

使用v-on:XX绑定事件, 例如v-on:click = 'showAlert', 这样, 在点击之后, Vue会寻找methods中的showAlert函数处理事件

<body>
    <div id="root">
        <h1>Hi, {
  {name}} for here</h1>
        <button v-on:click = 'showAlert'>ClickMe</button>
    </div>
</body>
<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                name: 'Liu',
            }
        },
        methods: {
     
            showAlert(){
     
                alert('你好')
            }
        },
    });  
</script>

这里v-on:click可以简写为@click

这里的回调函数的调用方式有

  • 在调用方式为@click = 'showAlert'时, 函数可以接受一个参数event
  • 在调用方式为@click = 'showAlert(15)'时, 函数只接受一个变量15, event消失了
  • 在调用方式为@click = 'showAlert(15, $event)'时, 函数接受15与event两个参数, 此时相当于使用关键字手动调用了event

习惯这种'foo(1,2)'调用函数的方式

<body>
    <div id="root">
        <h1>Hi, {
  {name}} for here</h1>
        <button @click = 'showAlert'>ClickMe</button>
        <button @click = 'showLog($event, 50)'>ClickMe2</button>
    </div>
</body>
<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                name: 'Liu',
            }
        },methods: {
     
            showAlert(){
     
                alert('你好')
            },
            showLog(e, v){
     
                console.log('你好', e, v)
            }
        },
    });  
</script>

点击ClickMe2后输出: 你好, 鼠标事件, 50

同时应该知道, 这些函数与data一样在vm上, 但是没有做数据代理(谁会修改回调函数呢?)

既然这么说了, 理论上我们可以把methods的东西放在data里面(反正最终都是绑定到vm上), 但是这会增大Vue的负担, 额外的将这些方法进行了数据代理

事件修饰符

部分标签存在默认行为, 朴素的v-on:不会进行阻止, 例如

<a href = 'baidu.com' @click = 'showLog'>ClickMe2</a>

就会先执行showLog, 然后跳转网页, 我们可以手动阻止, 也可以将v-on:click(@click)改为v-on:click.prevent(@click.prevent)阻止

这里的.prevent就是事件修饰符, Vue中的修饰符有

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

修饰符支持连写@click.stop.once = 'foo'

键盘事件

按键事件的别名

我们希望实现一个input中按下回车在Console输出input内容, 可以这么写

<body>
    <div id="root">
        <input type="text" placeholder="按下回车" @keyup = 'showInfo'>
    </div>
</body>
<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                name: 'Liu',
            }
        },methods: {
     
            showInfo(e){
     
                if(e.keyCode ==13)
                    console.log(e.target.value);
            }
        },
    });  
</script>

我们需要手动判断按下的是不是回车, 实际上Vue提供了很多键盘事件别名实现当特定情况发生再触发事件, 可以将上述代码修改为

<body>
    <div id="root">
-       <input type="text" placeholder="按下回车" @keyup = 'showInfo'>
+       <input type="text" placeholder="按下回车" @keyup.enter = 'showInfo'>
    </div>
</body>
<script>
    const app = new Vue({           
        el: '#root',    
        data() {
            return{
                name: 'Liu',
            }
        },methods: {
            showInfo(e){
-               if(e.keyCode ==13)
                console.log(e.target.value);
            }
        },
    });  
</script>

@keyup.enter表示只有enter键才能触发, 类似的别名还有

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

那么, 他们为什么叫做事件别名呢? 这是因为Vue将JS原生代码中的Enter等键名起了一个别名小写化为了enter, 可以使用@keyup.Enter等验证原名也是可用的

据此, 我们可以借助Vue设置一些没有别名的键的事件@ketup.key, 例如@keyup.s = showInfo实现按下s键时调用showInfo(e)(@keyup.ASCII代码 = showInfo也可以触发, 但是强烈不推荐)

注意: 这里的key应该与JS中的Key大小写相同(例如Alt不能写成alt), 如果想要查看某一个键的key可以让js打印e.key, 同时如果某一个键的Key是多个大写单词拼成的(例如CapsLock), 应该全部转换为小写并在单词连接处加-(例如CapsLock -> caps-lock, 这种命名法叫做kebab-case)

在自定义事件修饰的时候, 系统级修饰键(ctrl、alt、shift、meta)存在特殊规则

  • 配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发. (同时不会阻止默认行为)
  • 配合keydown使用:正常触发事件.

我们也可以自定义别名

Vue.config.keyCodes.myhui = 13;      // 回车的ASCII(别名也是kebab-case)
// @keyup.myhui

修饰符支持连写@keyup.ctrl.y = 'foo'(不能与浏览器默认快捷键冲突, 无法阻止默认行为)

计算属性

尝试实现一个组件, 在输入框输入姓名, 显示So, Hi 姓-名, 可以使用最原始的插值语法

<body>
    <div id="root">
        姓:<input type="text" v-model:value="v_x">
        名:<input type="text" v-model:value="v_m">
        <p>so, Hi {
  {v_x}} {
  {v_m}}</p>
    </div>  
</body>
<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                v_m: 'Kr',
                v_x: 'L',
            }
        },
    });  
</script>

更先进的

<body>
    <div id="root">
        姓:<input type="text" v-model:value = 'v_x'>
        名:<input type="text" v-model:vaule = 'v_m'>
        <p>so, Hi {
  {getFullName()}}</p>
    </div>  
</body>
<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                v_m: 'Kr',
                v_x: 'L',
            }
        },methods: {
     
            getFullName(){
     
                return this.v_x+'-'+this.v_m.toUpperCase();
            }
        },
    });  
</script>

使用指令语法

<body>
    <div id="root">
        姓:<input type="text" @keyup.enter = 'chgLst'>
        名:<input type="text" @keyup.enter = 'chgFst'>
        <p>so, Hi {
  {v_x}} {
  {v_m}}</p>
    </div>  
</body>
<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                v_m: 'Kr',
                v_x: 'L',
            }
        },methods: {
     
            chgLst(e){
     
                this.v_x = e.target.value;
            }, chgFst(e){
     
                this.v_m = e.target.value;
            }
        },
    });  
</script>

还可以使用计算属性实现, 所谓计算属性就是通过已有的属性计算出我们需要的属性, 在Vue中属性存放在data中, 计算属性被单独的写在computed里面, 如何让Vue在获取数据的同时刷新数据值, 我们想到了Object.defineProperty()中的get属性, 他可以在读取数据的时候执行特定函数, 并得到结果, 实际上Vue就是这么实现的, 并且将结果绑定到vm上.

<body>
    <div id="root">:<input type="text" v-model:value = 'v_x'>:<input type="text" v-model:vaule = 'v_m'>
        <p>so, Hi {
   {
   fullName}}</p>
    </div>  
</body>
<script>
    const app = new Vue({
              
        el: '#root',    
        data() {
   
            return{
   
                v_m: 'Kr',
                v_x: 'L',
            }
        },computed: {
   
            fullName: {
   
                get(){
   
                    return this.v_x+'-'+this.v_m.toUpperCase();
                }
            }
        }
    });  
</script>

在浏览器中, 我们可以看到app.fullName: (...), 浏览器不会显示数值, 因为我们没有调用get(), 点击(...)之后, 浏览器才会通过get()获取值, 这也证明了computed中值被绑定到了vm

计算属性采用了缓存机制与插值语法相比性能更优, 当数据没有变化的时候不会反复get()

计算属性还支持Set, 例如我需要有一个按钮, 按下之后就将名字修改成布-吉岛, 可以设置如下

<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                v_m: 'Kr',
                v_x: 'L',
            }
        },computed: {
     
            fullName: {
     
                get(){
     
                    return this.v_x+'-'+this.v_m.toUpperCase();
                },
                set(v){
     
                    [this.v_x, this.v_m] = v.split('-') 
                }
            }
        },methods: {
     
            initFull(){
     
                this.fullName = '布-吉岛'
            }
        },
    });  
</script>

当确定你的计算属性只get不set的时候, 可以做简写

computed: {
   
    fullName(){
   
        return this.v_x+'-'+this.v_m.toUpperCase();
    },
}

最终变成了methods的样子`~`

监视属性

当某一个属性发生变化的时候, 执行相关动作, 与计算属性不同的是:

  • 计算属性是构造一个虚拟的变量, 用于Vue的调用, 如果检测到构造这个变量的变量被修改了, 就在下次get的时候修改
  • 监视属性是监视属性, 变更的时候立刻执行回调函数

我们可以尝试写一个组件, 点击按钮反转天气文字, 使用计算属性可以这样写

<body>
    <div id="root">
        <h1>今天天气很{
  {weather}}</h1>
        <button @click = 'togWeather'>togWeather</button>
    </div>  
</body>
<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                v: false
            }
        },computed: {
     
            weather(){
     
                return this.v?'炎热':'凉爽'
            },
        }, methods: {
     
            togWeather(){
     
                this.v=!this.v;
            }
        },
    });  
</script>

使用监视属性

<body>
    <div id="root">
        <h1>今天天气很{
  {weather}}</h1>
        <button @click = 'togWeather'>togWeather</button>
        <p>{
  {numbers}}{
  {numbers.a}}</p>
    </div>  
</body>
<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                v: false,
                weather: '布吉岛',
                numbers: {
     
                    a:1,
                    b:2,
                    c:3
                }
            }
        }, methods: {
     
            togWeather(){
     
                this.v=!this.v;
            }
        }, watch:{
     
            v: {
     
                handler(newValue, oldValue){
     
                    this.weather = newValue?'凉爽':'炎热'
                },
                // immediate: true,        // 初始化的时候立即调用函数
            },
            'numbers.a':{
                        // 监视对象属性的变化
                handler(n,o){
     
                    console.log(n,o);
                }
            },
            numbers:{
     
                deep: true,                 // 开启深度监视, 如果不开启, 只会监视这个对象的地址有没有发生改变
                handler(n,o){
     
                    console.log(n,o);
                }
            }
        }
    });  
</script>

监视属性写在单独的watch中

其中handler()就是数据变化后的回调函数, 传入新值和老值

如果想要监控在初始化是时候执行一次就加上immediate, 如果想要检测对象内部值的变化要使用deep, 否则只监视对象地址变化

如果监视属性只有handler可以简写成

watch:{
   
    v(newValue, oldValue){
   
        this.weather = newValue?'凉爽':'炎热'
    },
}

可以在vm声明结束后声明变量监控

vm.$watch('监视变量',{
   }/function(newV, oldV){
   })

计算属性与属性监控的区别

  • computed和watch之间的区别:
    1.computed能完成的功能,watch都可以完成.
    2.watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作.

闲来无事写了一个计算属性的异步调用

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
    <div id="root">
        <h1>今天气温{
  {weather.is_hot}}度</h1>
        <h1>顺便看看这个计时器ID: {
  {weather.counter}}</h1>
        <button @click = 'togWeather'>点击让5s后温度下降一度</button>
    </div>  
</body>
<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                // v.val代表温度, 这样每次对象地址变换但是val不变Vue依旧会刷新
                // Vue太精明了, this.v = this.v + 1 - 1 + Math.random()*0 不会触发计算属性刷新
                v: new Object({
     val: 50}),           
                // 降低一度?
                chgMe: false
            }
        }, methods: {
     
            togWeather(){
     
                this.chgMe=!this.chgMe;
                // 修改v触发计算属性
                this.v = new Object({
     val: this.v.val});
            }
        }, computed: {
     
            weather(){
     
                // 通过返回new Object刷新地址 防止缓存
                return new Object({
     
                    is_hot: this.v.val,
                    counter: setTimeout(() => {
     
                        console.log("IN-timeOut");
                        if(this.chgMe){
     
                            this.chgMe = false;
                            // !用这个方法返回新的v, 但是会触发新的定时器...
                            // this.v = new Object({val: this.v.val - 1});
                            // !返回新的v.val, 地址不变, 但是为什么还是触发新的定时器?
                            this.v.val -= 1;
                            console.log("IN-chg")
                        }
                    }, 5000)
                })
            }
        }
    });  
</script>
</html>

Class与Style的绑定

  • 字符串绑定class(样式名字不确定个数确定)
    可以动态指定class, 由于class有多个, 不变的可以正常写, 变化的使用数据绑定
    <div class="basic" :class = 'classB' @click = 'changeMod'>{
        {name}}</div>
    
    这样我们就只需要维护一个变量classB就实现了Class的调整, 用于动态决定class值
  • 数组绑定class(样式名字不确定个数不确定)
      <div class="basic" :class = 'classArray' @click = 'changeMod'>{
        {name}}</div>
    
    也可以绑定成数组
  • 对象绑定class(样式名字确定个数确定动态决定要不要)
      <div class="basic" :class = 'classObj' @click = 'changeMod'>{
        {name}}</div>
      <!-- ... -->
      classObj: {
          atguigu1: false,
          atguigu2: true,
      }
    
    对象的TF对应加不加class

对于内联样式

<!-- 绑定style样式--对象写法 -->
<div class="basic" :style="styleObj">{
  {name}}</div> <br/><br/>
<!-- 绑定style样式--数组写法 -->
<div class="basic" :style="styleArr">{
  {name}}</div>
<!-- ... -->
styleObj2:{
    backgroundColor:'orange'
},
styleArr:[
    {
        fontSize: '40px',           // 注意这里要转驼峰命名, 注意单位
        color:'blue',
    },
    {
        backgroundColor:'gray'
    }
]

条件渲染

符合某些条件再渲染某些元素

  • v-show渲染
    使用v-show属性实现, 值是一个布尔表达式, 例如
    <body>
        <div id="root">
            <h2 v-show = "show">{
        {welcomText}}</h2>
            <button @click='togShow'>切换</button>
        </div>  
    </body>
    <script>
        const app = new Vue({
                      
            el: '#root',    
            data() {
           
                return{
           
                    welcomText: '欢迎来到',
                    show: true
                }
            }, methods: {
           
                togShow(){
           
                    this.show = !this.show;
                }
            }, computed: {
           
            }
        });  
    </script>
    
    不显示之后可以看到h2的display = none
  • v-if渲染
    也可以实现v-show功能, 但是比较狠, 一旦为false直接删除元素
    <!DOCTYPE html>
    <html>
    <head>
        <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
    </head>
    <body>
        <div id="root">
            <h2 v-if = "show">{
        {welcomText}}</h2>
            <button @click='togShow'>切换</button>
        </div>  
    </body>
    <script>
        const app = new Vue({
                      
            el: '#root',    
            data() {
           
                return{
           
                    welcomText: '欢迎来到',
                    show: true
                }
            }, methods: {
           
                togShow(){
           
                    this.show = !this.show;
                }
            }, computed: {
           
            }
        });  
    </script>
    </html>
    
    所以, 如果DOM变化频率高建议用show, 频率低用if, 否则频繁的插入删除节点不易于维护
    v-if还支持v-else-if,v-else, 在连续的if/else-if/else之间不能出现多余的元素
    <body>
        <div id="root">
            <h2 v-if = "show === 1">1</h2>
            <h2 v-else-if = "show === 2">2</h2>
            <h2 v-else-if = "show === 3">3</h2>
            <h2 v-else = "show === 4">4</h2>
            <button @click='togShow'>切换</button>
        </div>  
    </body>
    <script>
        const app = new Vue({
                      
            el: '#root',    
            data() {
           
                return{
           
                    welcomText: '欢迎来到',
                    show: 1
                }
            }, methods: {
           
                togShow(){
           
                    this.show = (this.show)%4 + 1;
                }
            }, computed: {
           
            }
        });  
    </script>
    
    当有一些紧挨的元素想要批量显示, 可以写
    <h2 v-if = "show === 1">A</h2>
    <h2 v-if = "show === 1">A</h2>
    <h2 v-if = "show === 1">A</h2>
    <h2 v-if = "show === 2">B</h2>
    <h2 v-if = "show === 2">B</h2>
    <h2 v-if = "show === 2">B</h2>
    
    比较麻烦, 可以改成
    <div v-if = "show === 1">
        <h2>A</h2>
        <h2>A</h2>
        <h2>A</h2>
    </div>
    <div v-if = "show === 2">
        <h2>B</h2>
        <h2>B</h2>
        <h2>B</h2>
    </div>
    
    但是修改了DOM结构, 会跟着变很多CSS/JS, 解决方法是直接吧if绑定在template标签上, 这种标签在最终渲染的时候会被隐藏, 例如
    <template v-if = "show === 1">
        <h2>A</h2>
        <h2>A</h2>
        <h2>A</h2>
    </template>
    <template v-if = "show === 2">
        <h2>B</h2>
        <h2>B</h2>
        <h2>B</h2>
    </template>
    
    最终会渲染为
    <h2>A</h2>
    <h2>A</h2>
    <h2>A</h2>
    

列表渲染

类似for循环生成多个标签, 使用了与JS的for类似写法

<body>
    <div id="root">
        <div v-for="(item, index) in items" :key="item.id">{
  {item.id}}-{
  {item.val}}</div>
    </div>  
</body>

<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                items: [
                    {
     id: 1, val: "Liu"},
                    {
     id: 2, val: "Kai"},
                    {
     id: 3, val: "Rui"}
                ]
            }
        }
    });    
</script> 

其中被遍历的对象可以是任意可迭代对象例如

  • 数组: (值,下标) in arr
  • 对象: (值,键名) in obj
  • 字符串: (字符,下标) in str
  • 类似py的range写法: (数字,index) in 数字
    (v,index) in 5
    (1,0)
    (2,1)
    (3,2)
    (4,3)
    (5,4)
    

内部的元素要指明是对象.属性

注意这个Key

这里的Key相当于是当前这条迭代对象的唯一标识符, 尽量使用后端提供的ID, 如果不写:key默认会指定为:key=index 这是一种非常危险的做法

首先要明白Vue为什么要在循环中加入这个Key?, 如果仅仅是为了表示循环, key完全可以默认表示index, 而不需要用户指定. 这个Key是用于Vue的diff算法中的, 当数据发生变化后, diff算法会对比虚拟DOM发生了什么变化, 这个时候识别for中哪条元素变化就是通过Key识别的

当我们使用index做Key时, 上面例子渲染的是

<div :key=0>{
  {1}}-{
  {"Liu"}}</div>
<div :key=1>{
  {2}}-{
  {"Kai"}}</div>
<div :key=2>{
  {3}}-{
  {"Rui"}}</div>
<!-- 
    使用的数据是
    {id: 1, val: "Liu"},
    {id: 2, val: "Kai"},
    {id: 3, val: "Rui"}
 -->

当我们在后面加入一条新数据后,

<div :key=0>{
  {1}}-{
  {"Liu"}}</div>
<div :key=1>{
  {2}}-{
  {"Kai"}}</div>
<div :key=2>{
  {3}}-{
  {"Rui"}}</div>
<div :key=3>{
  {4}}-{
  {"CC"}}</div>
<!-- 
    使用的数据是
    {id: 1, val: "Liu"},
    {id: 2, val: "Kai"},
    {id: 3, val: "Rui"},
    {id: 4, val: "CC"}
 -->

Vue的Diff算法会对比前后虚拟DOM的不同

<div :key=0>{
  {1}}-{
  {"Liu"}}</div>
<div :key=1>{
  {2}}-{
  {"Kai"}}</div>
<div :key=2>{
  {3}}-{
  {"Rui"}}</div>
+ <div :key=4>{
  {4}}-{
  {"CC"}}</div>

Key=1,2,3没有变化, 于是在浏览器DOM中加入一条数据

看起来使用index也没有什么问题, 但是如果我们的数据是

{id: 4, val: "CC"},
{id: 1, val: "Liu"},
{id: 2, val: "Kai"},
{id: 3, val: "Rui"}

呢?

使用id渲染结果是

<div :key=4>{
  {4}}-{
  {"CC"}}</div>
<div :key=1>{
  {1}}-{
  {"Liu"}}</div>
<div :key=2>{
  {2}}-{
  {"Kai"}}</div>
<div :key=3>{
  {3}}-{
  {"Rui"}}</div>

Vue对比之前代码发现,

+ <div :key=4>{
  {4}}-{
  {"CC"}}</div>
<div :key=1>{
  {1}}-{
  {"Liu"}}</div>
<div :key=2>{
  {2}}-{
  {"Kai"}}</div>
<div :key=3>{
  {3}}-{
  {"Rui"}}</div>

于是在浏览器DOM中增加一个标签

使用index渲染结果是

<div :key=0>{
  {4}}-{
  {"CC"}}</div>
<div :key=1>{
  {1}}-{
  {"Liu"}}</div>
<div :key=2>{
  {2}}-{
  {"Kai"}}</div>
<div :key=3>{
  {3}}-{
  {"Rui"}}</div>

Vue对比之前代码发现,

- <div :key=0>{
  {1}}-{
  {"Liu"}}</div>
+ <div :key=0>{
  {4}}-{
  {"CC"}}</div>
- <div :key=1>{
  {2}}-{
  {"Kai"}}</div>
+ <div :key=1>{
  {1}}-{
  {"Liu"}}</div>
- <div :key=2>{
  {3}}-{
  {"Rui"}}</div>
+ <div :key=2>{
  {2}}-{
  {"Kai"}}</div>
+ <div :key=3>{
  {3}}-{
  {"Rui"}}</div>

Vue可不认识你是在前面加入了元素, 他只认识Key, 这样Vue就需要重新在浏览器DOM中渲染所有的标签, 造成巨大的资源损失

同时, 对于表单等输入类数据, 存在十分严重的问题, 如果在input中输入了数据, 在乱序修改数组后输入数据会保留在相对位置不变, 例如

<div :key=1>{
  {1}}-{
  {"Liu"}}<input/></div>
<div :key=2>{
  {2}}-{
  {"Kai"}}<input/> <!--我在这里输入了AAA--> </div>
<div :key=3>{
  {3}}-{
  {"Rui"}}<input/></div>

用:key=index后

在前面加入数据

- <div :key=0>{
  {1}}-{
  {"Liu"}}<input></div>
+ <div :key=0>{
  {4}}-{
  {"CC"}}<input></div>
- <div :key=1>{
  {2}}-{
  {"Kai"}}<input></div>
+ <div :key=1>{
  {1}}-{
  {"Liu"}}<input><!--Vue认准了输入AAA的input在Key=1里面--></div>
- <div :key=2>{
  {3}}-{
  {"Rui"}}<input></div>
+ <div :key=2>{
  {2}}-{
  {"Kai"}}<input></div>
+ <div :key=3>{
  {3}}-{
  {"Rui"}}<input></div>

直接造成了输入框内容移位

  • 用index作为key可能会引发的问题:
    1. 若对数据进行:逆序添加、逆序删除等破坏顺序操作:
      会产生没有必要的真实DOM更新 ==> 界面效果没问题, 但效率低.
    2. 如果结构中还包含输入类的DOM:
      会产生错误DOM更新 ==> 界面有问题.
  • 开发中如何选择key?:
    1. 最好使用每条数据的唯一标识作为key, 比如id、手机号、身份证号、学号等唯一值.
    2. 如果不存在对数据的逆序添加、逆序删除等破坏顺序操作,仅用于渲染列表用于展示,使用index作为key是没有问题的.

通过列表渲染实现搜索功能, 搜索排序

<body>
    <div id="root">
        <input type="text" v-model:value = 'searchTxt'>
        <button @click='searchBy = 0'>Norm</button>
        <button @click='searchBy = 1'>A->Z</button>
        <button @click='searchBy = -1'>Z->A</button>
        <div v-for="(item, index) in searchRes" :key="item.id">{
  {index+1}}-{
  {item.val}}</div>
    </div>  
</body>

<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                items: [
                    {
     id: 1, val: "123"},
                    {
     id: 2, val: "234"},
                    {
     id: 3, val: "211"},
                ],
                searchBy: 0,
                searchTxt: ''
            }
        },
        computed:{
     
            searchRes(){
     
                const res = this.items.filter((d)=>{
     
                    return d.val.includes(this.searchTxt)
                });
                res.sort((n,o)=>{
     
                    return this.searchBy*(n.val-o.val);
                })
                return res;
            }
        }
    });    
</script>  

Vue数据代理与检测的原理

Vue的数据代理之前说是使用Object.defineProperty实现的, 但是他具体是怎么实现的呢, 想当然的, 我们这样实现

const data = {
   
  demo: 10,
};

Object.defineProperty(data, 'demo', {
   
  get() {
   
    console.log("YOU GET");
    return this.demo;
  },
  set(v) {
   
    console.log("YOU SET and SYNC to DOM");
    this.demo = v;
  },
});

console.log(data.demo);
data.demo = 11;

但是JS会报错

RangeError: Maximum call stack size exceeded
    at Object.get [as demo] (/home/liukairui/CODE/code-snippet/Vue/VueDemo/demo.js:7:17)
    at Object.get [as demo] (/home/liukairui/CODE/code-snippet/Vue/VueDemo/demo.js:7:17)
    at Object.get [as demo] (/home/liukairui/CODE/code-snippet/Vue/VueDemo/demo.js:7:17)

显示与之前学的不同, 以前我们使用Object.defineProperty是维护一个变量, 然后get的时候通过其他变量获得值得, 但是这次是获取他自己, 这样JS又会调用get返回自己, 造成无限递归, 爆栈

那Vue是如何避免无限递归的呢, 使用一个中间层, 在get的时候获取原始数据, 在Set的时候修改原始数据并更新DOM, 我们实现一个简单的例子

const data = {
   
  demo1: 10,
  demo2: 10,
  demo3: 10,
  demo4: 10,
};

function Observer(obj) {
   
  let keys = Object.keys(obj); // 获取被监视对象的全部Key
  keys.forEach((d)=>{
   
    Object.defineProperty(this, d, {
   
      get() {
   
        return obj[d];
      },
      set(v) {
   
        obj[d] = v;
        console.log("我开始刷新DOM了")
      },
    });
  });
}

let _data = new Observer(data);

console.log(_data.demo1);
_data.demo1 = 15;
console.log(_data.demo1);

console.dir(_data);
console.dir(data);

结果是

10
我开始刷新DOM了
15
Observer {}
{ demo1: 15, demo2: 10, demo3: 10, demo4: 10 }

可以看到开发者工具中vm有_data, 这里的_data相当于我们的Observer, 可以看到_data中有每一个属性的get, 但是他不是Object.defineProperty是Vue自定义的

get items: ƒ reactiveGetter()
set items: ƒ reactiveSetter(newVal)

响应式的get和set, 还记得代码中data的属性p会同时绑定到vm.pvm._data.p, 这两个p是完全等价的, 当我们修改任何一个属性的时候会调用这个响应式的get/set

同时Vue的响应式get/set可以对对象进行"穿透", 不论对象有多少层, 或者数组有多少个元素, Vue都可以递归将对象内部的对象设置get/set, 直到内部是一个非对象

例如, 我们使用Vue定义

const app = new Vue({
              
    el: '#root',    
    data() {
   
        return{
   
            items: [
                {
   
                    id: 1, 
                    bas: {
   
                        name: "A",
                        age: 12,
                    }
                },
            ],
        }
    }
}); 

看到app._data

items: Array(1)
    0:
        bas: Object                                         
            age: 12
            name: "A"
            __ob__: Observer {
   value: {
   }, dep: Dep, vmCount: 0}
            get age: ƒ reactiveGetter()                             // item.has.age监视
            set age: ƒ reactiveSetter(newVal)
            get name: ƒ reactiveGetter()                            // item.has.name监视
            set name: ƒ reactiveSetter(newVal)
            [[Prototype]]: Object
        id: 1
        __ob__: Observer {
   value: {
   }, dep: Dep, vmCount: 0}
        get bas: ƒ reactiveGetter()                                 // item.has监视
        set bas: ƒ reactiveSetter(newVal)
        get id: ƒ reactiveGetter()                                  // item.id监视
        set id: ƒ reactiveSetter(newVal)
        [[Prototype]]: Object
    length: 1
    __ob__: Observer {
   value: Array(1), dep: Dep, vmCount: 0}
    [[Prototype]]: Array
__ob__: Observer {
   value: {
   }, dep: Dep, vmCount: 1}
get items: ƒ reactiveGetter()                               // 对于item的get
set items: ƒ reactiveSet(newVal)

在实际开发中会遇到这样的一个问题

<body>
    <div id="root">
    <div v-for="item in items">{
  {item.name}}</div>
    <button @click='chg'>修改值</button>
    </div>  
</body>

<script>
    const app = new Vue({
                
        el: '#root',    
        data() {
     
            return{
     
                items: [
                    {
     name: 1},
                    {
     name: 2},
                    {
     name: 3},
                    {
     name: 4},
                ],
            }
        }, methods: {
     
            chg(){
     
                // this.items[0].name = 'S';   // Ok
                this.items[0] = {
     name: 'S'};   // Err
            }
        },
    });  
</script>  

执行被注释的代码, 页面修改成功, 但是执行没有注释的代码, 那么页面就修改失败了, 这是因为对这个对象整体赋值的时候没有为他内部的元素加入reactiveGetter()

那么, 如果我们希望实现将

data() {
   
    return{
   
        info: {
   name: 4},
    }
}

变为

data() {
   
    return{
   
        info: {
   name: 4, birth: '2000'},
    }
}

应该怎么处理呢, 首先应该使用类似Object.defineProperty的方法加入get与set, 使属性可以被监视代理, 可以使用Vue.set(对象, '属性', 值)或者vm.$set(对象, '属性', 值), 例如vm.set(vm.info, 'bitrh', '2000')

对于对象很好说, 对于数组, 我们将

data() {
   
    return{
   
        info: [{
   name: 4}]
    }
}

变为

data() {
   
    return{
   
        info: [{
   name: 4}, {
   birth: '2000'}],
    }
}

我们知道, 如果直接对JS的Array进行push那么birth所在对象应该是不会被监控, 但是在列表渲染小节中中我们发现这是可行的, 这是因为Vue对Array的push, pop…进行了重新封装, 不需要什么手动Set, 但是如果我们还是想要对数组中的元素进行直接替换, 我们需要使用vm.$set(vm.arr,下标,newVal)

注意, $set的数据不能是Vue对象, 也不能在data上直接set数据, 建议set到data.obj上

数据劫持: 将data中的属性转化为geter/geter形式, 每次修改获取数据都被上述函数劫持, 代为修改读取

表单数据

<!DOCTYPE html>
<html>
<head>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
</head>
<body>
    <div id="root">
    <form @submit.prevent="ajaxSubmit">
        <!-- 用trim去除两边空格 -->
        帐号<input v-model.trim='userInfo.account' type="text"><br><br>
        密码<input v-model='userInfo.passwd' type="password"><br><br>
        <!-- 这里加了两个number, 第一个是让Vue知道输入的是数字, 第二个是浏览器限制输入内容必须是数字 -->
        年龄<input v-model.number='userInfo.age' type="number"><br><br>
        性别<br><br>
        <!-- 要亲自配置value --><input type="radio" name="sex" v-model='userInfo.sex' value=""><br><br><input type="radio" name="sex" v-model='userInfo.sex' value=""><br><br>
        爱好<br><br>
        <!-- 要亲自配置value --><input type="checkbox" v-model="userInfo.hobby" value=""><input type="checkbox" v-model="userInfo.hobby" value=""><input type="checkbox" v-model="userInfo.hobby" value="">
        位置<br><br>
        <select v-model="userInfo.pos">
            <option value="N">N</option>
            <option value="S">S</option>
            <option value="W">W</option>
            <option value="E">E</option>
        </select><br><br>
        多行<br><br>
        <!-- lazy标记使得内容只在失去焦点的时候才MVVM, 提高了性能 -->
        <textarea v-model.lazy='userInfo.multext'></textarea>
        同意<input type="checkbox" v-model="userIn
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Liukairui

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值