前端 - Vue基础

Vue文档

简介

Vue是一套用于构建用户界面渐进式JS框架:

  • 构建用户界面:将数据通过界面显示给用户
  • 渐进式:对于简单的应用,只需要一个小巧的核心库;对于复杂应用,可以引入各式各样的Vue插件

特点:

  1. 组件化模型,提高代码复用率、让代码更好维护
  2. 声明式编码,无需直接操作DOM,提高开发效率
  3. 使用虚拟DOMDiff算法,尽量复用DOM节点

环境安装

  1. 直接用<script>标签引入
  2. 使用npm

暂时先使用标签引入的方法。

Hello案例

Hello案例:

<body>
    <div id="root">
        <h1>Hello {{name}}!</h1>
        <h2>Age: {{age}}</h2>
    </div>    
    <script>
        const x = new Vue({
            el: '#root',  // el用来指定当前Vue实例为哪个容器服务, 通常为css选择器
            data() {  // data用于存储数据, 数据供el指示的容器去使用
                return {
                    name: 'test',
                    age: 30
                }
            },
        });
    </script>
</body>

Vue实例和容器是一一对应的

Vue实例需要和容器一一对应,当出现以下两种情况时:

  • 多对一:例如两个Vue实例指定同一个容器,则第二个Vue实例的任何操作都是无效的
  • 一对多:Vue实例的el通过class选择器等匹配时,即使CSS选择器能匹配到多个标签,Vue实例只会和找到的第一个容器绑定

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

Vue模板

在上面的案例中,root容器中的代码依然符合HTML规范,只不过混入了一些Vue语法。root容器中的代码被称为Vue模板。在{{}}中,需要写JS表达式,且内部可以读到data中的所有属性,例如,作出如下修改:

<div id="root">
    <!-- Hello TEST150! -->
    <h1>Hello {{name.toUpperCase() + age * 5}}!</h1>
    <!-- Age: 3.141592653589793 -->
    <h2>Age: {{Math.PI}}</h2>
</div>

JS表达式是一种特殊的JS代码,一个表达式会产生一个值,可以放在任何一个需要值的地方,例如:

  1. a
  2. a + b
  3. func(a)
  4. x === y ? 'a': 'b'

el和data的两种写法

  • el写法1:new Vue实例的时候配置el
  • el写法2:先创建Vue实例,然后通过指定el的值
const v = new Vue({
    // el: '#root',  // el写法1
    data: {  // data写法1
        name: 'test',
        age: 30
    }
});

v.$mount('#root');  // el写法2

下面的两种写法都可以,但使用组件时,必须采取函数式,否则报错

  • data写法1:如上面所示的对象式
  • data写法2:函数式,需要返回一个对象
new Vue({
    el: '#root',
    // data: function () { // data写法2
    //     return {
    //         name: 'test',
    //         age: 30
    //     }
    // }
    // data写法2, 简写
    data(){
        return {
            name: 'test',
            age: 30
        }
    }
});

注意:

  • 由Vue管理的函数,不要写箭头函数,否则this将不再是Vue实例

模板语法

上面的Hello {{name}}就属于Vue模板语法中的插值语法,除了插值语法,还有指令语法

  1. 插值语法:用于解析标签体内容,写作{{xxx}}xxx是JS表达式,且能够直接读到data中的所有内容
  2. 指令语法:用于解析标签 (标签属性、标签体内容、绑定事件等) ,用法有v-bind:href="xxx",同样的,xxx是JS表达式

一个使用v-bind:的例子,且能够简写为:

<div id="root">
    <a v-bind:href="baidu">百度</a>
    <a :href="bing.toUpperCase()">必应</a>
</div>
<script>
    new Vue({
        el: '#root',
        data() {
            return {
                baidu: 'https://www.baidu.com',
                bing: 'https://cn.bing.com'
            }
        },
    });

</script>

数据绑定

  1. v-bind单向绑定:数据只能从data流向页面
  2. v-model双向绑定:数据不仅能从data流向页面,还能从页面流向data
    • 由于双向绑定一般应用在表单类元素上,例如<input>, <select>等,默认收集value值,所以v-model:value可以简写为v-model
<div id="root">
    单向数据绑定:<input type="text" :value="bind1">
    <br>
    双向数据绑定:<input type="text" v-model:value="bind2">
    <br>
    <input type="text" v-model="bind2">

    <!-- 下面这条语句报错, 因为v-model只能应用在表单类元素上 (输入类) -->
    <!-- <h2 v-model:x="bind2">Hello</h2> -->
</div>

MVVM模型

Vue官方文档中,出现了下面这句话:

虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm(ViewModel 的缩写) 这个变量名表示 Vue 实例。

  1. M模型 (Model) :对应data中的数据
  2. V视图 (View) :模板
  3. VM视图模型 (View Model) :Vue实例对象

image

打印一下VM,即Vue实例对象:

const vm = new Vue({
    el: '#root',
    data(){
        return {
            name: 'test'
        }
    }
});
console.log(vm);

从输出可以发现,name包含在Vue实例对象上,事实上,Vue模板等能读到Vue实例对象的所有属性和方法,包括隐式原型对象中的属性和方法,例如可以下面这样写:

<div id="root">
    <h1>Hello, {{name}}</h1>
    <!-- [object HTMLDivElement] -->
    <h2>{{$el}}</h2>
</div>

总结:

  1. data中的所有属性,最后都出现在了Vue实例上
  2. Vue实例的所有属性以及Vue原型的所有属性,Vue模板都可以直接使用

数据代理

回顾Object.defineproperty()

Object.defineProperty(obj, prop, descriptor)

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

备注:应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用。

  • 使用该方法添加的属性,默认不可枚举:
let person = {
    name: 'Lee',
    gender: 'male'
};
Object.defineProperty(person, 'age', {
    value: 30
});

console.log(person, Object.keys(person));
// {name: 'Lee', gender: 'male', age: 30}
// ['name', 'gender']
  • 一些默认值:
Object.defineProperty(person, 'age', {
    value: 30,
    // enumerable: true,  // 控制属性是否能枚举, 默认false
    // writable: true,  // 控制属性是否能被修改, 默认false
    // configurable: true  // 控制属性是否能被删除, 默认false
});

person.age = 1;
console.log(person);  // {name: 'Lee', gender: 'male', age: 30}

console.log(delete person.gender, delete person.age);  // true false
console.log(person);  // {name: 'Lee', age: 30}
  • 通过gettersetter,使得变量myAgeperson.age建立关联
let myAge = 25;
Object.defineProperty(person, 'age', {
    /* 使用Object.defineProperty() 定义对象属性的时候,
     *  如果设置了 set 或 get, 就不能设置 writable 和 value 中的任何一个 
     */
    // value: 30,
    // writable: true,

    // 当age属性被读取时调用get(), 返回值就是age的值
    // 这样做使得myAge被修改, person.age也被修改
    get() {
        console.log('age被读取');
        return myAge;
    },
    // 当age属性被修改时调用set(), value是被修改的值
    // 这样做使得person.age被修改, myAge也被修改
    set(value) {
        console.log('age被修改');
        myAge = value;
    }
});

person.age = 10;  // age被修改
console.log(person.age);  // age被读取 10
console.log(myAge);  // 10

myAge = 50;
console.log(person.age);  // age被读取 50

数据代理定义

通过一个对象代理另一个对象中属性的操作。上面的例子中,将变量myAge包装成一个对象,则就是一个最简单的数据代理。

Vue中的数据代理

通过vm对象来代理data对象中属性的操作,能够更好的操作data中的数据:

let myData = {
    name: 'Lee',
    age: 30
};
const vm = new Vue({
    el: '#root',
    data() {
        return myData;
    }
});
console.log(vm._data === myData);  // true
console.log(vm.name, vm.age);  // Lee 30
  1. 通过Object.defineProperty()来把data对象中的数据都添加到vm上,例如上面的vm.namevm.age
  2. vm.namevm.age中,都指定getter和setter,建立数据代理

于是,可以发现:

  • vm._data就是实例化Vue对象时传入的myData
  • 实例化时,vm根据vm._data对象,通过Object.defineProperty()来把vm._data对象中的每个数据添加到自身,于是对象有了vm.namevm.age属性
  • 最后,通过getter和setter建立数据代理,这样就不需要使用vm._data.name访问数据,而是直接使用vm.name

事件处理

一个简单的例子:

<div id="root">
    <button v-on:click="show">Click me</button>
    <!-- 简写方式 -->
    <!-- <button @click="show">Click me</button> -->
</div>
<script>
    const vm = new Vue({
        el: '#root',
        data() {
            return {
                name: 'test'
            }
        },
        methods: {
            show(event) {
                console.log(this.name + event.target.innerHTML);  // testClick me
                console.log(this === vm);  // true, 这说明this就是vm
            }
        },
    });
</script>

传参数:

<div id="root">
    <!-- 注意使用$event给event对象占位, 否则方法接受不到事件对象 -->
    <button @click="show($event, 100)">Click me</button>
</div>
<script>
	const vm = new Vue({
        el: '#root',
        methods: {
            show(event, number) {
                console.log(event);
                console.log(number);
            }
        },
    });
</script>
  • 理论上,将方法写进data,程序也能正常运行,但是这样会让Vue实例对象将该方法也进行一次代理,而方法本身是不需要数据代理的,所以需要写入methods

总结:

  1. 使用v-on:xxx或者@xxx绑定事件,其中xxx是事件名
  2. methods配置的方法,不要写成箭头函数,否则this不再是vm或者组件的实例对象
  3. @click="show($event)"可以简写成@click="show"

事件修饰符

Vue提供的数据修饰符:

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

例如阻止<a>的默认跳转行为:

<a href="https://www.baidu.com" @click.prevent="show">百度</a>

这样写等价于:

const vm = new Vue({
    el: '#root',
    methods: {
        show(event) {
            event.preventDefault();
        }
    },
});

按键事件

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

    • enter
    • delete (捕获删除和退格)
    • esc, space
    • tab,较为特殊,需要配合keydown,如果配合keyup,则松开Tab时,焦点已经移走,无法触发事件
    • up, down, left, right
  2. 系统修饰键ctrl, alt, shift, meta

    • 配合keyup使用:按下修饰键的同时,再按下其他键,则其他键释放后,才触发
    • 配合keydown使用:正常触发
  3. 对于Vue没有提供的按键,按照按键原始的key值绑定

  4. 可以使用keyCode绑定具体按键,但是不推荐

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

<div id="root">
    <input type="text" placeholder="请输入" @keydown="show">
</div>

<script>
    const vm = new Vue({
        el: '#root',
        methods: {
            show(event) {
                // 如果输入回车, 则输出input标签的信息
                if (event.keyCode == 13) {
                    console.log(event.target.value);
                }

            }
        },
    });

</script>

上面的写法等价于:

<div id="root">
    <input type="text" placeholder="请输入" @keydown.enter="show">
</div>
<script>
    const vm = new Vue({
        el: '#root',
        methods: {
            show(event) {
                console.log(event.target.value);
            }
        },
    });
</script>

对于使用key值绑定的,需要改成小写,如果遇到多个单词,中间加下划线,例如CapsLockPageUp需要变为:

<input type="text" placeholder="请输入" @keydown.caps-lock="show">
<input type="text" placeholder="请输入" @keydown.page-up="show">

计算属性

姓名案例

首先,尝试使用插值语法和methods实现相同的功能,最后再使用计算属性,插值语法实现

<div id="root">
    <input type="text" placeholder="请输入姓" v-model="surname">
    <br>
    <input type="text" placeholder="请输入名" v-model="givenName">
    <div>{{surname.slice(0, 2)}}-{{givenName}}</div>
</div>

<script>
    const vm = new Vue({
        el: '#root',
        data() {
            return {
                surname: '', 
                givenName: ''
            }
        },
    });
</script>

使用插值语法可以发现,当出现一些需求时,例如上面的字符串切片{{surname.slice(0, 2)}},会导致其中的代码不易阅读,这违背了简单的计算属性的代码风格。

使用methods实现,只需要作出简单修改,首先添加方法:

methods: {
    fullName() {
    return this.surname + '-' + this.givenName;
    }
}

<div>修改为:

<div>{{fullName()}}</div>

这种方法效率一般,因为data中的任何一个数据发生变化,Vue模板都会重新解析,从而引起fullName()方法的多次调用。

应用计算属性

最后,使用计算属性实现,首先为vm添加属性:

computed: {
    fullName: {  // 需要是一个对象
        // 当需要更新fullName时, get就会被调用, 且返回值作为fullName的值
        // 调用时机: 1. 第一次读fullName 2. 所依赖的数据发生变化
        get() {
        	return this.surname.slice(0, 3) + '-' + this.givenName;
        }
        // 当fullName被修改时调用set
        set(value) {
            const arr = value.split('-');
            this.surname = arr[0];
            this.givenName = arr[1];
        }
    }
}

<div>修改为:

<div>{{fullName}}</div>

计算属性:

  1. 定义:计算属性需要通过已有的属性计算而来
  2. 原理:借助Object.defineproperty()方法提供的getter和setter
  3. 优势:内部缓存机制,效率更高,换句话来说,如果使用methods方法,则每次读取fullName都会调用一次函数,而计算属性fullName只会在值改变时运行get(),其余时间直接读取缓存
  4. 计算属性最终会出现在vm,可以直接读取
  5. 如果计算属性被修改,则必须设置set()函数去响应修改,否则报错

计算属性简写

当不考虑修改计算属性时,即不需要写set()方法时,可以将上面的计算属性简写为:

computed: {
    fullName() {
        return this.surname.slice(0, 3) + ' - ' + this.givenName;
    }
}

此时函数体内容即为上面get()方法的内容。

监视属性

watch用来监视属性:

watch: {
    fullName: {
        immediate: true,  // 初始化时是否调用handler, 默认为false
        handler(newValue, oldValue) {  // 当fullName发生改变时调用
        	console.log(newValue, oldValue);
        }
    }
}

上面的写法等价于:

vm.$watch('fullName', {
    immediate: true,
    handler(newValue, oldValue) {
        console.log(newValue, oldValue);
    }
});

需要注意:

  1. 当被监视的属性变化时,回调函数handler自动调用
  2. 监视的属性必须存在,才能被监视

深度监视

<div id="root">
    <div>a的值{{nums.a}}</div>
    <button @click="nums.a++">a++</button>
    <div>b的值{{nums.b}}</div>
    <button v-on:click="nums.b++">b++</button>
</div>
const vm = new Vue({
    el: '#root',
    data() {
        return {
            nums: {
                a: 1,
                b: 1
            }
        }
    },
    watch: {
        'nums.a': {  // 监视多级结构的某个属性
            handler() {
                console.log('Change a.');
            }
        },
        nums: {  // 监视多级结构中所有属性的变化
            deep: true,  // 是否开启深度监视
            handler() {
                console.log('Change nums.');
            }
        }
    }
});

当我们监视一个对象时,即使对象的属性值改变了,引用也没有变化,所以需要开启deep深度监视。

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

监视的简写

当不需要配置immediatedeep,只需要handler时,可以简写:

vm.$watch('fullName', function (newValue, oldValue) {
    console.log(newValue, oldValue);
});

或者:

watch: {
    'nums.a'() {
        console.log('Change a.');
    }
}

计算属性和监视属性的对比

计算属性和监视属性对比

函数写法

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

绑定样式

绑定class样式

方式1:字符串写法,通过v-bind动态指定样式

  • 适用于:class属性不确定,需要动态指定
<div id="root">
    <div class="basic" :class="myStyle"></div>
    <button @click="change">随机切换样式</button>
</div>
const vm = new Vue({
    el: '#root',
    data() {
        return {
            myStyle: 'test1'
        }
    },
    methods: {
        change() {  // 随机切换样式
            styles = ['test1', 'test2', 'test3'];
            let index = Math.floor(Math.random() * styles.length);
            this.myStyle = styles[index];
        }
    },
});

方式2:数组写法

  • 适用于:要绑定的class个数不确定,名字也不确定
<div class="basic" :class="styles"></div>
const vm = new Vue({
    el: '#root',
    data() {
        return {
            styles: ['test1', 'test2', 'test3']
        }
    }
});

通过方式2,<div>拥有了styles数组中的所有class,可以通过push()shift()等数组增删操作为<div>增加或删除class属性。

方式3:对象写法

  • 适用于:要绑定的class个数确定,名字确定,需要动态增删class属性
// 与方式2类似, 唯一的不同是styles是一个对象
data() {
    return {
        styles: {
            test1: true,
            test2: true,
            test3: false
        }
    }
}

于是可以通过修改对象的属性值来给<div>增加或删除class属性。

绑定style样式

绑定style样式,同样的可以使用对象写法或者数组写法

<div id="root">
    <!-- 对象方式 -->
    <div class="basic" :style="style1">Hello</div>
    <!-- 数组方式 -->
    <div class="basic" :style="[style1, style2]">Hello</div>
</div>
const vm = new Vue({
    el: '#root',
    data() {
        return {
            style1: {
                fontSize: '40px',  // 对应font-size
                color: 'red'
            }, 
            style2: {
                backgroundColor: 'skyblue'  // 对应backgroundColor
            }
        }
    }
});

条件渲染

  1. v-if
    • 特点:不展示的节点,DOM直接移除,所以适用于切换频率较低的场景
    • 注意:可以搭配v-else-ifv-else,要求结构连续,不能被打断
  2. v-show
    • 特点:不展示的节点等价于添加style="display: none;",所以适用于切换频率较高的场景

使用v-show的时候:

<div id="root">
    <div>n的值是{{n}}</div>
    <button @click="n++">n = n + 1</button>

    <!-- v-show举例 -->
    <div v-show="n === 1">1111111</div>
    <div v-show="n === 2">2222222</div>
    <div v-show="n === 3">3333333</div>
</div>

n的值为0时,网页文档实际上为:

<div id="root">
    <div>n的值是0</div> <button>n = n + 1</button>
    
    <div style="display: none;">1111111</div>
    <div style="display: none;">2222222</div>
    <div style="display: none;">3333333</div>
</div>

而使用v-if的时候:

<!-- v-if举例 -->
<div v-if="n === 1">1111111</div>
<div v-else-if="n === 2">2222222</div>
<div v-else-if="n === 3">3333333</div>
<div v-else>xxxxxxx</div>

n的值为0时,网页文档实际上为:

<div id="root">
    <div>n的值是0</div> <button>n = n + 1</button>
    <div>xxxxxxx</div>
</div>

如果想一次性控制多个标签,需要将他们都放入一个<div>容器,这样做的缺点是破坏文档原有的结构,于是可以使用<template>

<template v-if="n === 1">
    <h1>111</h1>
    <h2>aaa</h2>
</template>
<template v-else>
    <h1>xxx</h1>
    <h2>???</h2>
</template>

n的值为0时,网页文档实际上为:

<div id="root">
    <div>n的值是0</div> <button>n = n + 1</button>
    <h1>xxx</h1>
    <h2>???</h2>
</div>

注意<template>只能搭配v-if使用,不能搭配v-show,否则条件无效

列表渲染

使用v-for指令展示列表数据,可遍历数组对象等:

<div id="root">
    <!-- 遍历数组 -->
    <ul>
        <li v-for="(p, index) of persons" :key="p.key">
            {{p.name}} - {{p.age}}
        </li>
    </ul>
    <!-- 遍历对象 -->
    <ul>
        <li v-for="(val, k) of stu" :key="k">
            {{k}} - {{val}}
        </li>
    </ul>
</div>
const vm = new Vue({
    el: '#root',
    data() {
        return {
            persons: [
                {id: '001', name: 'zhangsan', age: 18},
                {id: '002', name: 'lisi', age: 19},
                {id: '003', name: 'wangwu', age: 20}
            ],
            stu: {
                name: 'Lee',
                age: 30, 
                grade: 6
            }
        }
    }
});

输出:

zhangsan - 18
lisi - 19
wangwu - 20
----------
name - Lee
age - 30
grade - 6
  • 一般来说,最好不要使用遍历时拿到的index作为key,而是使用:key="p.key"的方式

key的原理

有关key的说明参考官方文档:key

使用index作为key时,一旦数组原来的顺序被打破,就会效率变低(本来可以复用的节点重新渲染),且有可能出现错误:

image

虚拟DOM中key的作用:

  • key是虚拟DOM对象的标识,当数据发生变化,Vue根据新数据生成新的虚拟DOM
  • 随后使用diff算法对比新旧虚拟DOM
  • 没有改变的虚拟DOM直接复用之前的真实DOM,改变的虚拟DOM直接生成新的真实DOM

列表过滤与排序

做一个简单的搜索功能:

<div id="root">
    <input type="text" placeholder="关键字搜索" v-model:value="keyWord">
    <ul>
        <li v-for="p of filPersons" :key="p.id">
            {{p.name}}-{{p.age}}
        </li>
    </ul>
</div>

通过computed实现:

data() {
    return {
        // persons: [...],
        keyWord: '',
        sortType: 1  // 0表示不排序, 1表示升序, 2表示降序
    }
},
computed: {
    filPersons() {
        // 利用数组的filter方法过滤出符合的新数组
        const arr = this.persons.filter((p) => {
            return p.name.indexOf(this.keyWord) !== -1;
        });  
        // 根据sortType判断是否排序以及按什么顺序排序
        if (this.sortType !== 0) {
            arr.sort((p1, p2) => {
                return this.sortType === 1 ? p1.age - p2.age: p2.age - p1.age;
            });
        }
        return arr;
    }
}

通过watch也可以实现类似功能,注意immediate: true

Vue检测数据变化

对上面的例子稍作修改,添加下面的语句:

<button @click="change">修改数据</button>
methods: {
    change() {
        /* 有效, Vue成功检测, 页面更新 */
        // this.persons[0].name = 'test';
        // this.persons[0].age = 80;

        /* 无效, Vue无法检测到, 页面不更新 */
        this.persons[0] = { id: '001', name: 'test', age: 80 }
    }
}

在控制台输出vm.persons,得到:

(4) [{…}, {…}, {…}, {…}, __ob__: Observer]
    0: {id: '001', name: 'test', age: 80}
    1: {__ob__: Observer}
    2: {__ob__: Observer}
    3: {__ob__: Observer}
    length: 4
    __ob__: Observer {value: Array(4), dep: Dep, vmCount: 0}
    [[Prototype]]: Array

可以看到,由于直接修改对象,被修改的数据缺少了一些属性。在上面的Vue数据代理中,可以发现vm._data === myData,但在实例化Vue对象时,其实首先对myData做了一些修改。

原理

写一个简单的Observer对象,实现数据检测:

function MyOberver(obj) {
    // 汇总obj的所有key, 形成数组keys
    const keys = Object.keys(obj);
    // 遍历keys
    keys.forEach((k) => {
        // this是实例化的MyOberver对象
        // 为this添加obj的所有属性, 并添加getter和setter
        Object.defineProperty(this, k, {
            get() {
                return obj[k];
            },
            set(val) {
                obj[k] = val;
            }
        });
    });
}

let myData = {
    name: 'test',
    address: 'beijing'
};
let obs = new MyOberver(myData);

let vm = {};  // 实例化的Vue对象
vm._data = myData = obs;

上面的MyObserver和真实的Vue中的Oberver相比,有两个功能未实现:

  1. 没有建立数据代理,即不能直接通过vm.name访问数据,而是必须通过vm._data.name
  2. 没有考虑data的属性可能是多层的,例如data中可能有对象,Vue中的Oberver会递归地给对象的属性添加getter和setter

Vue.set()方法

当我们希望给vmdata添加方法时,如果直接添加:

const vm = new Vue({
    el: '#root',
    data() {
        return {
            name: 'test',
            address: 'beijing'
        };
    },
});

vm._data.sex = 'male';
console.log(vm.sex);  // undefined

出现undefined是因为在实例化Vue对象时,每个属性都会被数据代理,而像上面这种方式,则没有数据代理的过程,可以使用Vue.set()方法:

const vm = new Vue({
    el: '#root',
    data() {
        return {
            student: {
                name: 'Lee',
                age: 30
            }
        };
    },
});

// 为vm._data.student添加sex属性
// vm.$set(vm.student, 'sex', 'male');  // 二者等价
Vue.set(vm.student, 'sex', 'male');
  • 注意:Vue不允许直接给vm._data添加属性,必须是例如vm._data.student的响应式对象

Vue检测数组

const vm = new Vue({
    el: '#root',
    data() {
        return {
            student: {
                name: 'Lee',
                age: 30
            },
            arr: [111, 222, 333]
        };
    },
});

当输出vm,可以发现,vm.student的每个属性都被添加了getter和setter,但vm.arr只有数组对象有getter和setter,它的每个索引值,例如vm.arr[0]是没有getter和setter的。

但是,使用数组的7种变更方法改变内部的值时,Vue都能检测到。

vm.arr.push(999);  // 成功被响应

这是因为Vue 将被侦听的数组的变更方法进行了包裹,详见官方文档

console.log(vm.arr.push === Array.prototype.push);  // false

除了上面的7种变更方法,还可以用Vue.set()方法更新数组:

methods: {
    change() {
        // vm.arr[1] = 300;  // 无法被响应
        Vue.set(vm.arr, 1, 300);  // 成功响应
    }
},

或者直接返回新的数组对象。

收集表单数据

<div id="root">
    <!-- 表单提交时阻止默认行为, 即刷新页面 -->
    <form @submit.prevent="demo">
        <!-- 对于text和password, 用户输入的和v-model收集的都是value值 -->
        <!-- v-model可以用trim进行修饰, 去除输入开头和结尾的空格 -->
        账号:<input type="text" v-model.trim="userInfo.account"> <br>
        密码:<input type="password" v-model="userInfo.password"> <br>
        <!-- v-model可以用number进行修饰 -->
        年龄:<input type="number" v-model.number="userInfo.age">
        <!-- radio需要自己配value值 -->
        性别:男 <input type="radio" name="sex" value="male" v-model="userInfo.sex">
        女 <input type="radio" name="sex" value="female" v-model="userInfo.sex"> <br>
        <!-- check需要自己配value值, 且v-model使用数组接收 -->
        爱好:篮球 <input type="checkbox" name="hobby" value="basketball" v-model="userInfo.hobby">
        足球 <input type="checkbox" name="hobby" value="football" v-model="userInfo.hobby"> <br>
        地址:
        <select name="address" v-model="userInfo.address">
            <option value="">请选择校区</option>
            <option value="beijing">北京</option>
            <option value="shanghai">上海</option>
            <option value="hangzhou">杭州</option>
        </select>
        <br>
        其他:<!-- v-model可以用lazy进行修饰, 失去焦点后再收集数据 -->
        <textarea name="" id="" cols="30" rows="10" v-model.lazy="userInfo.area"></textarea> <br>
        <button>提交</button>
    </form>
</div>
const vm = new Vue({
    el: '#root',
    data() {
        return {
            userInfo: {  // 存储表单中的所有信息
                account: '',
                password: '',
                age: 18,
                sex: 'male',
                hobby: [],  // checkbox需要设置成数组
                address: '',
                area: ''
            }
        };
    },
    methods: {
        demo() {
            const jsonStr = JSON.stringify(this.userInfo);
            console.log(jsonStr);
        }
    },
});

内置指令补充

v-text

向其所在节点渲染文本,会替换掉节点中的内容。

<div v-text="hello.toUpperCase()">123</div>
<!-- HELLO! -->

v-html

效果和v-text类似,不同的是,会解析包含HTML结构的内容,而不是当成字符串。

<div v-html="baidu"></div>
<!-- baidu的值为 '<a href="http://www.baidu.com">Baidu</a>' -->

v-cloak

当标签被添加了v-cloak属性,一旦Vue实例创建完成并接管容器后,v-cloak属性被删除。

<h1 v-cloak>{{hello}}</h1>
[v-cloak] {
    display: none;
}

配合CSS可以达到以下效果:当vue.js还没有被加载时,用户不会看到{{hello}},因为所有带有v-cloak属性的节点都被隐藏了;一旦Vue实例创建完成,v-cloak属性被删除,hello变量对应的值就被展示出来了。

v-once

v-once属性所在节点在初次渲染之后,视为静态内容,以后的数据改变不受影响。

<h1 v-once>{{n}}</h1>
<h2>{{n}}</h2>
<button @click="n++">n++</button>

<h1>标签一直为n的初始值,<h2>标签的值会随着点击按钮而增大。

v-pre

  1. 跳过其所在节点的编译过程
  2. 建议在没有使用指令语法、插值语法的节点上使用,加快编译
<h1 v-pre>Hello</h1>
<!-- 不要加v-pre, 否则会显示{{n}} -->
<h2>{{n}}</h2>

自定义指令

实现两个自定义指令:

  1. v-big-number:与v-text功能类似,不同的是需要将n扩大10倍
  2. v-fbind:与v-bind功能类似,不同的是初始化时会自动获取焦点
<div id="root">
    <h1 v-text="n"></h1>
    <h2 v-big-number="n"></h2>
    <button @click="n++">n++</button>
    <input type="text" v-fbind:value="n">

</div>
const vm = new Vue({
    // ...
    directives: {
        /* big被调用的时机: 1. 指令与元素成功绑定 (初始化时)
                          2. 指令所在的模板重新解析 
         */
        'big-number'(element, binding) {
            element.innerText = binding.value + '0';
        },
        fbind: {
            bind(element, binding) {  // 指定与元素成功绑定时调用
                console.log(this);  // window
                element.value = binding.value;
            },
            inserted(element, binding) {  // 指令所在元素被插入页面时调用
                console.log(this);  // window
                element.focus();  // 获取焦点
            }, 
            update(element, binding) {  // 指令所在模板被重新解析时调用
                console.log(this);  // window
                element.value = binding.value;
            },
        }
    }
});

以上的写法属于局部自定义指令,即v-big-numberv-fbind只对vm接管的#root容器有效,修改为全局指令:

Vue.directive('big-number', function (element, binding) {
    element.innerText = binding.value + '0';
});

Vue.directive('fbind', {
    bind(element, binding) {  // 指定与元素成功绑定时调用
        console.log(this);  // window
        element.value = binding.value;
    },
    inserted(element, binding) {  // 指令所在元素被插入页面时调用
        console.log(this);  // window
        element.focus();  // 获取焦点
    },
    update(element, binding) {  // 指令所在模板被重新解析时调用
        console.log(this);  // window
        element.value = binding.value;
    },
});
  • 注意:全局指令写在初始化Vue实例之前

Vue实例生命周期

生命周期:

  1. 又被称为:生命周期钩子、生命周期函数、生命周期回调函数
  2. Vue在关键时刻自动调用的一些特殊名称的函数
  3. this指向vm或组件实例对象

官方文档:实例生命周期钩子

挂载流程

Vue完成模板解析,并把初次的真实DOM放入页面之后(挂载完毕),调用mounted()方法。

<div id="root">
    <h1>{{n}}</h1>
    <button @click="addN">n++</button>
</div>
const vm = new Vue({
    el: '#root',
    data() {
        return {
            n: 1
        };
    },
    methods: {
        addN() {
            console.log('addN!!!');
            this.n++;
        }
    },
    /*
     * 初始化生命周期、事件, 数据代理还未开始
     * 无法通过vm访问data中的数据、methods中的方法 
     */
    beforeCreate() {
        console.log('beforeCreate: ');
        console.log(this._data);  // undefined
        console.log(this.addN);  // undefined
        // debugger;
    },
    /*
     * 初始化数据检测、数据代理 
     * 可以通过vm访问data中的数据、methods中的方法 
     */
    created() {
        console.log('created: ');
        console.log(this._data);  // {__ob__: Observer}
        console.log(this.addN);  // ƒ addN() { this.n++; }
    },
    /*
     * Vue完成解析模板, 内存中生成虚拟DOM, 页面还不能生成解析好的内容
     * 此时页面呈现的是未经Vue编译的DOM结构, 所有对DOM的操作最终都不奏效
      */
    beforeMount() {
        console.log('beforeMount: ');
        // debugger;  // 如果有断点, 则页面会直接显示未经解析的{{n}
    },
    /*
     * 将内存的虚拟DOM转为真实DOM插入页面, 此时初始化过程结束
     * 页面呈现经过Vue编译的DOM, 对DOM的所有操作有效(尽量避免), 
     * 一般在此: 开启定时器、发送网络请求、订阅消息、绑定自定义事件等初始化操作
     */
    mounted() {
        console.log('mounted: ');
        // 转换后的真实DOM被保存在了vm.$el中
        console.log(this.$el instanceof HTMLElement);  // true
        // 开启定时器操作等
    },           
});

更新流程

const vm = new Vue({
	// ...
    /*
     * 数据被更新了, 但是还没有同步到页面
     */    
    beforeUpdate() {
        // 点击一下n++按钮
        console.log('beforeUpdate:');
        console.log(this.n);  // 2
        // debugger;  // 如果有断点, 此时页面显示的n还是1
    },    
    /*
     * 生成新的虚拟DOM, 经过diff与旧的DOM比较, 最终完成页面更新
     * 数据和页面已经完成同步
      */    
    updated() {
        console.log('updated:');
        console.log(this.n);  // 2
        // debugger;  // 如果有断点, 此时页面显示的n变为2
    },                    
});

销毁流程

vm.$destroy()被调用时,启动销毁流程,完全销毁当前实例, 清理它与其他实例的连接,解绑所有指令和自定义事件监听器

  • 销毁后原声DOM事件依然有效,即每当点击按钮,依然运行addN(),并打印addN!!!,但是数据不再更新
const vm = new Vue({
	// ...
    /*
     * 此时vm的data, methods, 指令等都处于可用状态
     * 此时对数据进行的修改不再更新
     * 一般在此执行: 关闭定时器、取消订阅消息、解绑自定义等收尾操作
     */ 
    beforeDestroy() {        
        console.log('beforeDestroy:');
        console.log(this.n);  // 1
    },  
    /*
     * 销毁完成, 较少使用
     */ 
    destroyed() {
        console.log('destroyed:');
    },                   
});
posted @ 2022-03-11 14:39  lv6laserlotus  阅读(82)  评论(0)    收藏  举报