Vue2.x - 核心

目录

Vue简介

Vue是什么

为什么需要Vue来构建用户界面?传统的基于原生JavaScript或者基于jQuery库的DOM操作有何缺点?

什么是渐进式框架

Vue特点

MVVM模型

初识Vue

MVVM模型 

el和data

el的取值说明

View的指定时机

vm实例和View的一 一对应关系

data的取值说明

data属性的数据代理

Vue响应式概念及原理简介

data小结

模板语法

Mustache插值语法

v-bind 属性绑定指令

vue指令

v-bind指令

v-bind指令缩写

总结

事件处理

v-on指令

v-on指令缩写

options.methods

this指向

调用方式和传参

事件修饰符

按键修饰符

系统键修饰符

键名键码修饰符

自定义按键修饰符

鼠标按键修饰符

计算属性

为什么需要计算属性

计算属性的本质和优点

计算属性的简写

计算属性的getter的调用时机

侦听属性

为什么需要侦听属性

侦听属性定义options.watch

侦听属性的handler方法执行时机

深度侦听

侦听属性的两个定义时机,以及简写

侦听属性和计算属性的区别与联系

样式绑定

class样式绑定

字符串变量表达式写法

数组变量表达式写法

对象变量表达式写法

v-bind样式绑定不会覆盖原生class,而是追加

style样式绑定

条件渲染

v-show

v-if

v-else-if、v-else

template标签

列表渲染

v-for指令基本使用

key属性

虚拟DOM

Diff算法

深入理解虚拟DOM运作和key属性作用

key运作原理的总结

key=index引发的错误DOM更新问题

列表过滤和列表排序

基于computed进行列表过滤

基于watch进行列表过滤

列表排序案例

数据模型更新

Vue.set方法

Vue.set为对象添加响应式属性

Vue.set为数组添加响应式属性

数组原型方法的响应式改造

双向数据绑定

v-model

收集表单数据

value属性何时需要预置值

checkbox两种使用情况说明

收集表单数据的数据模型设计

v-model修饰符

.trim修饰符

.number修饰符 

 .lazy修饰符

内置指令

v-text

v-html

v-once

v-cloak

v-pre

自定义指令

Vue内置指令设计分析

自定义指令

基础语法

钩子函数

钩子函数的this指向

通过一个例子理解bind和inserted钩子的区别

bind和update两个钩子的关系,及自定义指令函数式写法

全局自定义指令

生命周期

Vue生命周期图

beforeCreate

created

beforeMount

mounted

beforeUpdate和updated

beforeDestory和destoryed

生命周期钩子函数使用总结


Vue简介

Vue是什么

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

为什么需要Vue来构建用户界面?传统的基于原生JavaScript或者基于jQuery库的DOM操作有何缺点?

无论是原生JS的DOM操作,还是jQuery封装库的DOM操作,其实都是命令式的。

何谓命令式?

所谓命令式,就是需要书写固定步骤的代码去完成某个功能,比如更新某个DOM元素内容的功能,如下:

Vue2.x - 核心_第1张图片

首先需要为button按钮绑定click事件,当button被点击,则获取div元素,并且更改div内容。

这种命令式的代码是繁琐且重复的,并且不同水平的开发者对于同一个功能的开发,可能产出的命令式代码风格迥异和性能良莠不齐。比如下面这段代码:

Vue2.x - 核心_第2张图片

而Vue将固定重复的命令式DOM操作全部底层化,只对开发者提供声明式指令和模板语法,开发者只需要使用相应的指令和模板语法即可完成同样的功能,并且风格一致,性能最优。

Vue2.x - 核心_第3张图片

也就是说使用Vue就可以摆脱繁琐的DOM操作。

什么是渐进式框架

Vue可以自底向上逐层应用。

Vue框架可以分为两个层次:核心库应用、组件化开发

Vue的核心库应用可以独立使用,基于Vue核心库,我们可以简单的将一个网页的构建交由Vue管理,开发者从而全身心投入业务逻辑开发。

Vue也可以进行大型复杂网站前端的开发,他将一个大型网站前端分为N多个组件,网站的每个网页都是由某些可复用的组件组合而成,并且Vue可以很好的处理组件间通信问题。

也就是说,Vue是一把宰牛刀,但是也可以用来杀鸡。如果你使用Vue杀鸡,那你就学Vue如何杀鸡,而不需要学宰牛,等你需要宰牛时,学习Vue的宰牛技巧也可以。

杀鸡是底,宰牛是顶,所以Vue可以自底向上逐层应用。

Vue特点

  • 声明式编码,让前端开发人员无需直接操作DOM,提高了个人和团队的开发协同效率。
  • 使用虚拟DOM和Diff算法,尽可能复用DOM节点,节约了浏览器渲染时间,提高了性能。
  • 采用组件化编程方式,并且组件可以高复用,避免了重复代码,且代码更易维护。

MVVM模型

初识Vue

Vue框架就是一个js文件,如果我们想要将使用Vue,则只需要在网页中引入Vue.js即可。

Vue2.x - 核心_第4张图片

 一旦Vue.js引入成功,则网页全局对象window下就多了一个构造函数:Vue。

Vue2.x - 核心_第5张图片

MVVM模型 

所谓MVVM模型,是一种前端架构模型,它提倡将一个网页分离为两部分:网页模板、数据模型。即将网页中动态数据抽离出来形成一个数据模型,而网页中原本动态数据的位置被模板变量替代。

而MVVM模型即:

  • M:数据模型(Model)
  • V:视图模板(View)
  • VM:视图数据控制器(ViewModel)

其中ViewModel占主导地位,ViewModel的工作如下:

  • 初始化时,或者Model数据发生变化时,ViewModel就会基于Model重新渲染View。(即:单向数据绑定,数据由Model流向View)
  • 当View的模板变量接收用户输入时,则ViewModel会将用户输入数据更新到Model中,此时由于Model数据发生变化,ViewModel又基于Model重新渲染了View。(即:双向数据绑定,数据先从View流向Model,又从Model流向View)

Vue2.x - 核心_第6张图片

 而Vue框架设计参考了MVVM模型,Vue构造函数的实例就是ViewModel的实现,所以我们通常将Vue构造函数实例成为vm实例

el和data

vm实例的一个重要任务就是分别与View和Model建立联系,所以Vue构造函数在创建实例时,需要传入一个配置对象options,该配置对象有两个重要属性:el和data,它们的作用就是分别建立vm和View以及Model的联系

Vue2.x - 核心_第7张图片

el的取值说明

el属性的值可以是一个DOM对象,也可以是一个DOM选择器字符串。 

Vue2.x - 核心_第8张图片

如果el传入的是字符串,则Vue底层会基于字符串进行document.querySelector找到对应DOM对象,并将此DOM对象作为被vm实例管理的View。

如果el传入DOM对象,则直接将传入值作为被vm实例管理的View。

Vue2.x - 核心_第9张图片

View的指定时机

el的作用是指定vm实例管理的View,并且是在创建vm实例过程中指定。这个时机可能并不能满足所有业务场景。比如在vm实例创建时,View对应的DOM对象还没有产生,则此时就无法通过el指定。所以指定View的时机必然有两个:

  • 创建vm实例(使用el指定)
  • 创建vm实例(使用vm.$mount方法指定)

在Vue构造函数的原型对象上有一个$mount方法,该方法可以被所有vm实例访问,该方法的作用是指定vm实例管理的View

Vue2.x - 核心_第10张图片

该方法也能接收DOM选择器字符串 和 DOM对象 两种类型值。 

而el底层实现,其实也是基于$mount方法。 

Vue2.x - 核心_第11张图片

vm实例和View的一 一对应关系

一个vm实例只能管理一个View

指定vm实例管理的View有两种方式:

  • options.el属性
  • vm.$mount方法

这两种方式都只能指定一个DOM对象作为View,而无法指定多个。

一个View只能被一个vm实例管理

如果存在多个vm实例管理同一个View,则View只会被第一个mount上的vm实例管理,其他vm实例将失效。

Vue2.x - 核心_第12张图片

data的取值说明

data属性支持传入一个JS对象或者一个JS函数。

当传入JS对象时,则JS对象本身就作为被vm实例管理的Model。

当传入JS函数时,则JS函数的返回值就作为被vm实例管理的Model。

data属性的数据代理

创建vm实例时传入的options.data对象的属性,会被转移到vm实例身上,并且会被改造为响应式的属性。

Vue2.x - 核心_第13张图片

 data上属性转移到vm实例上的过程较为复杂,可以简单描述一下:

function Vue(options) {
    // ...
    
    // 为vm实例新增_data属性,并且该属性指向options.data对应的对象
    const data = this._data = options.data

    // ....

    // 将vm._data上的属性代理给vm实例本身,相当于将options.data上的属性代理给vm实例本身
    proxy(this, "_data", key);

    // ....

    // 对options.data对象的属性进行响应式改造(getter&setter改造,响应式属性的watch依赖注入)
    observe(data, true /* asRootData */);

    // ...
}

最终,我们访问vm.desc相当于访问options.data.desc,我们修改vm.desc相当于修改options.data.desc,而这就是vm对于options.data的数据代理。

而vm对于options.data的数据代理实现,就是上面代码中proxy(this, '_data', key)的实现,以下是Vue源码中proxy的实现:

Vue2.x - 核心_第14张图片

关于setter、getter、Object.defineProperty的介绍请看下节

Vue响应式概念及原理简介

前面介绍了MVVM模型时说了:Model的变化,会被ViewModel监听到,然后ViewModel会用新的Model数据重新渲染View。

而Vue响应式的概念就是:Model变化,View就会连锁自动变化。

那么如何做到Model变化,就能立马被vm监听到呢?

Vue2是基于Object.defineProperty实现的,Object.defineProperty可以为一个对象添加一个访问器属性。

何谓访问器属性?它与对象普通属性有何区别?

JavaScript语言将对象属性分为了两类:数据属性、访问器属性

数据属性的定义方式有两种:

1、直接定义

const obj = {}

obj.a = 1

2、使用Object.defineProperty定义

const obj = {}

Object.defineProperty(obj, 'a', { // 为obj对象定义属性'a',属性'a'的描述符如下
    value: 1, // obj.a属性的初始值为1
    writable: true, // obj.a属性的值可以改变,即可写属性
    enumerable: true, // obj.a属性可以被枚举,即可以被Object.keys、for...in枚举出来
    configurable: true // obj.a属性可配置,即可删除,如delete obj.a
})

其中value、writable、enumerable、configurable是属性的描述符,作用如上注释解释

直接定义和Object.defineProperty定义出来的obj.a的功能是一模一样的,而Object.defineProperty的方式更加繁琐,唯一优势就是可以变更属性的描述符。

而访问器属性则只能使用Object.defineProperty方式定义,访问器属性和数据属性的区别在于它们的属性描述符不同,访问器属性的描述符有:get方法、set方法、configurable属性、enumerable属性

const obj = {}

let tmp = 1

Object.defineProperty(obj, 'a', {
    get(){
        return tmp
    },
    set(val){
        tmp = val
        // 通知vm基于新的Model重新渲染View
    },
    configurable: true,
    enumerable: true
})

如果我们访问对象的访问器属性,则会触发属性的getter方法执行;

如果我们修改对象的访问器属性,则会触发属性的setter方法执行;

而setter方法执行正是Vue响应式实现的关键点。想象一下,我们修改了data对象中某个属性desc,而该属性又是具有getter、setter的,所以必然出了setter的执行,所以我们完全可以在setter方法更新完属性值后,通知vm实例基于Model进行View的重新渲染。

另外还有一点需要注意的是,定义对象访问器属性时,需要借助一个第三者变量tmp,那么如果没有第三者变量tmp,只依靠对象访问器属性自身会出现什么问题呢?即如下代码的问题:

const obj = {}

Object.defineProperty(obj, 'a', {
    get(){
        return obj.a
    },
    set(val){
        obj.a = val
    },
    configurable: true,
    enumerable: true
})

Vue2.x - 核心_第15张图片

 可以发现

当我们修改obj.a时,会发生栈内存溢出,原因是setter递归调用溢出;

当我们访问obj.a时,也会发生栈内存溢出,原因是getter递归调用溢出;

为什么呢?

因为当我们修改obj.a时,触发setter执行,setter执行又触发obj.a = val,而这又涉及修改obj.a属性,所以又触发setter执行,依此递归往复,没有尽头,最终栈内存溢出;

访问obj.a同理。

所以在使用Object.defineProperty定义访问器属性时,必须要要借助第三发中转。

当然,上面在全局作用域下定义中转变量不太好,因为我们只希望中转变量用于getter,setter,而不希望被其他人使用,但是作为全局变量,这是无法保障的。

此时我们需要对Object.defineProperty进行封装,借助闭包变量来实现中转变量,这样就可以保障中专变量只用于getter、setter了

// 假设val只传入简单值,不传入对象值
function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get(){
            // watcher依赖收集
            return val
        },
        set(new){
            val = new
            // 响应式处理
        },
        configurable: true,
        enumerable: true
    })
}

此时defineReactive方法的形参val其实就是一个闭包变量,该闭包变量被defineReactive的内层函数get、set访问了,并且get、set只有在对象obj被销毁时才会销毁,所以闭包变量val的生命周期得到了延长。

data小结

  • data用于指定vm实例管理的Model
  • data上的属性会被代理给vm实例,即我们可以通过vm实例直接访问和修改data上的属性
  • data上属性的变化会被vm实例监听到,并且vm实例会将变化的数据更新到View模板中

模板语法

模板语法即应用于View中模板变量的语法。

View本质就是一个DOM对象,即一段HTML代码,且是一段没有实际数据,只有模板变量的HTML代码。而模板语法,就是如何在HTML代码中定义模板变量的语法。

在学习模板语法前,我们需要知道HTML代码其实可以看成一个节点树,包含:

  • 标签节点
  • 内容节点(标签体内容)
  • 属性节点(标签属性)
  • 注释节点

其中内容节点、属性节点的数据经常是动态的,所以模板语法主要是针对这两个节点的。

  • 标签内容:Mustache语法
  • 标签属性:v-bind语法

Mustache插值语法

Mustache的含义是人的胡子,而人的胡子特点是左右对称的一撇一捺

Vue2.x - 核心_第16张图片

Mustache语法的形式就是双大括号:{{}},非常像人的胡子。

Mustache语法主要注意两点:

  • 只能用于标签体内容的模板表示
  •  双大括号中只能是JS表达式,不能是JS语句;常用的JS表达式有:字面量表达式、变量表达式、函数调用表达式、运算表达式

Vue2.x - 核心_第17张图片

关于Mustache中可以使用的变量的来源:

  •  vm实例属性和方法,及其原型对象上的属性和方法
  • window全局对象的白名单方法

前面学习data属性可知,options.data对象的属性会被全部代理给vm实例,所以我们可以通过vm实例访问到options.data上的属性,而Mustache语法可以直接访问vm实例上的属性,则最终导致:Mustache语法可以直接访问options.data上的属性。

Vue2.x - 核心_第18张图片

Vue底层为Mustache语法定义了一个window全局对象可访问内置方法的白名单,即在Mustache语法中是无法访问用户定义的全局变量的,只能访问一些全局对象下的工具类和工具方法。

Vue2.x - 核心_第19张图片

Vue2.x - 核心_第20张图片

 关于Mustache还有一点需要注意的是:

如果模板值为undefined,则Vue不会将undefined渲染到模板中,即展示为空白

v-bind 属性绑定指令

vue指令

vue指令是带有 v- 前缀的特殊 attribute。(如v-bind指令)

vue指令的值预期是单个 JavaScript 表达式 (v-for 是例外情况,稍后我们再讨论)。

vue指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM

vue指令的形式如:v-xxx:attribute="expression"

v-bind指令

v-bind指令可以将HTML标签属性(a[href])与某表达式(link变量)的值进行绑定。

当v-bind指定的表达式值发生改变时,就会响应式的更新到HTML标签属性中。

Vue2.x - 核心_第21张图片

v-bind指令由于是为标签属性绑定值,而标签属性的值基本都是一个确定值(除了标签事件属性可以是语句),所以v-bind值可以使用表达式表示,因为表达式结果是一个确定值。

而v-bind指令值如果是变量表达式,则变量来源可以是:

  • vm实例属性和方法,以及其原型对象上的属性和方法
  • window全局对象白名单属性和方法

Vue2.x - 核心_第22张图片

其实不仅仅只是v-bind指令值中可以访问上述变量,其他所有的v-xxx指令值中都可以访问上述变量

v-bind指令缩写

v-bind是一个高频使用的指令,但是v-bind单词较多,所以一旦在View模板中大量应用v-bind,会造成大量重复冗余,所以vue允许将 "v-bind:" 简写为 ":"

Vue2.x - 核心_第23张图片

总结

模板语法其实就是一种模板字符串语法,如{{}}、v-bind:xxx="expression"就是一种模板化的字符串,而Vue正是通过识别这些特殊的模板字符串,进行真实数据替换。

模板语法主要亮点在于其响应式的数据更新,即{{}},v-bind绑定的表达式的值改变时,这些模板语法会重新加载新数据来更新View。

事件处理

v-on指令

v-on指令用于为HTML标签绑定事件,使用形式:v-on:event="expression || inline statement"

v-on的参数为需要绑定的事件类型,v-on的值可以是表达式或内联语句。

如下v-on为button元素绑定了一个click事件,并且v-on值其实就是作为事件的回调

Vue2.x - 核心_第24张图片

v-on指令缩写

和v-bind相同,v-on也是一个高频使用指令,所以Vue允许将 ”v-on:“ 简写为 ”@“ 

Vue2.x - 核心_第25张图片

options.methods

v-on指令值可以是表达式,使用表达式可以进行一些简单的逻辑,如进行运算,但是当事件回调的逻辑比较复杂时,我们只能将逻辑写成函数定义到options.methods中。 

Vue2.x - 核心_第26张图片

此时Vue会将optiions.methods中的所有方法都赋值到vm实例上,即我们可以在Mustache语法或者指令语法中直接访问到options.methods中定义的方法

Vue2.x - 核心_第27张图片

Vue2.x - 核心_第28张图片

另外需要注意options.methods中定义的方法的:

  • this指向
  • 调用方式和传参

this指向

如果options.methods中定义的方法为普通函数,则this指向当前的vm实例;

如果options.methods中定义的方法为箭头函数,则this指向箭头函数外层作用域的this;

Vue2.x - 核心_第29张图片

这也说明,在options.methods中定义的普通函数方法可以通过this(vm实例)来访问options.data中的属性。

调用方式和传参

v-on指令值支持只传入一个options.methods对象的方法名,v-on底层会自动调用方法名,并传入一个默认参数:事件对象

Vue2.x - 核心_第30张图片

 v-on指令值也支持传入一个options.methods对象的方法调用,此时v-on底层不会再传入默认的事件对象作为入参,需要手动在方法调用时传入事件对象的固定名字:$event

Vue2.x - 核心_第31张图片

当然v-on指令值写成方法调用形式,主要是为了传入用户自定义实参,需要知道的是$event无位置要求,即可以和用户自定义实参混合传入

Vue2.x - 核心_第32张图片

事件修饰符

事件绑定成功后,事件回调函数可以接收一个事件对象e,基于事件对象,我们可以阻止事件默认行为 e.preventDefault(),阻止事件冒泡e.stopPropagation(),利用e.target可以保证事件只在自身触发等等。

虽然在v-on绑定的事件回调函数(options.methods中定义的方法)中可以获取到事件对象,但是我们不建议在事件回调函数中利用事件对象进行DOM行为控制。Vue的设计宗旨是避免开发直接操作DOM,所有DOM操作都由Vue底层完成。所以Vue将事件对象的一些操作封装进了事件修饰符。

修饰符是由点开头的指令后缀来表示的,如v-on:click.prevent="expression"

事件修饰符 原生操作 含义
.prevent e.preventDefault() 阻止事件默认行为
.stop e.stopPropagation() 阻止事件冒泡
.capture addEventListener(type, listener, {
    capture: true
})
让事件回调在捕获阶段触发
.self e.target === this 当事件源和事件绑定的DOM是同一个时触发
.once addEventListener(type, listener, {
    once: true
})
事件只触发一次
.passive addEventListener(type, listener, {
    passive: true
})
提前告知浏览器不阻止该事件默认行为,避免浏览器创建冗余监听,提升性能

另外事件修饰符是可以多个串行的,但是串行修饰符之间是有顺序的,比如我可以先阻止默认行为,再阻止冒泡

按键修饰符

 常用事件类型中,用的最多的就是鼠标事件和键盘事件,对于键盘事件(keydown,keyup)来说,经常需要通过事件对象e,来获取e.key(键名)或者e.keyCode(键码,ASCII码值),之后通过键码或者键名来判断用户的按键,然后进行对应业务处理。

Vue2.x - 核心_第33张图片

Vue同样将这种基于事件对象获取键码键名的DOM操作设计成了指令修饰符,目前常用的按键修饰符如下

按键修饰符 含义
.enter 按下回车键时触发
.tab

按下tab键时触发事件回调,需要注意:tab键在浏览器中有默认功能,就是当tab按下时会切换浏览器的焦点,所以当我们为keyup事件追加.tab修饰符时,会造成tab无法触发事件回调的情况,因为事件回调需要在tab键按下并抬起时触发,而当tab键按下时就已经触发了切换浏览器焦点的事件了。

所以我们一般将.tab用于keydown事件。

.delete 按下删除键或退格键时触发
.esc 按下esc键触发
.space 按下空格键触发
.up 按下方向上键触发
.down 按下方向下键触发
.left 按下方向左键触发
.right 按下方向右键触发

系统键修饰符

在键盘上还有一些系统按键,如ctrl,alt,shift,meta键,这些键通常不单独使用,而是结合其他按键一起使用,比如我们常用的ctrl+c,ctrl+v。

使用这些系统键完成某个功能时,如粘贴功能,需要先按下ctrl,再按下v【keydown触发】,再松开v【keyup触发】。

而系统键修饰符同样是在上述流程完毕时触发事件的。

Vue2.x - 核心_第34张图片

键名键码修饰符

对于一些常用的按键,如果字母按键,我们可以直接使用其键名e.key作为按键修饰符

Vue2.x - 核心_第35张图片

如果键名是多个单词,如caps lock按键,则需要转成cabe-case写法

Vue2.x - 核心_第36张图片

需要注意的是数字的无法使用键名作为修饰符,因为Vue不仅支持键名作为修饰符,还支持键码作为修饰符,键码即ASCII码,是数字,所以数字键名和键码类型冲突了,此时我们只能使用数字的键码作为修饰符。

Vue2.x - 核心_第37张图片

自定义按键修饰符

Vue.config.keyCodes.自定义按键修饰符= 键码

Vue2.x - 核心_第38张图片

鼠标按键修饰符

鼠标按键有左键,中键,右键,Vue也为每个键的点击设计了修饰符

.left 鼠标左键按下触发
.middle 鼠标中键按下触发
.right 鼠标右键按下触发

计算属性

为什么需要计算属性

有一个需求:在options.data中定义了人的姓firstName和名lastName两个属性,但是页面需要展示人的全名,则有如下实现方式:

1、在Mustache语法中对firstName和lastName进行字符串拼接

Vue2.x - 核心_第39张图片

 2、利用methods定义的方法

Vue2.x - 核心_第40张图片

虽然上面两种方式都可以实现需求,但是都已弊端:

第一种方式将全名的生成逻辑放到了Mustache语法中,如果生成逻辑再复杂一点,则Mustache中需要写更多的业务代码,而Musatche只能写一些简单的表达式。

第二种方式虽然可以在methods中方法里写全名生成逻辑,但是如果网页中多个地方使用全名,则需要一次渲染中调用多次fullName方法,造成性能浪费。 

Vue2.x - 核心_第41张图片

计算属性的本质和优点

为了解决上述两个方式的缺点,Vue提出了计算属性,所谓计算属性,即可以依赖于已有options.data属性计算出来的新属性,计算属性的使用语法如下:

  • 在options.computed配置中定义一个对象,如fullName,我们需要为该对象设置setter和getter方法
  • 在options.computed配置的对象,会自动代理给vm实例,即我们可以通过vm实例来访问计算属性,而之前为fullNanme对象设置的setter、getter就会作为vm实例代理fullName的getter和setter逻辑
  • 计算属性的getter,setter方法的this指向当前vm实例

所以本质上看,options.computed中定义的对象fullName其实算一个配置对象,它指定了计算属性的属性名就是配置对象的名字fullName,计算属性被vm实例代理的逻辑setter和getter

  • 由于计算属性被代理给了vm实例,所在在模板语法中,可以直接访问计算属性

Vue2.x - 核心_第42张图片

 计算属性的优点在于:

  • 可以将计算逻辑写getter方法中,并且支持setter修改
  • 计算属性在每次渲染中计算出来的结果会被缓存起来,这样如果有页面中有多个地方引用则不会引起计算属性的getter执行多次,而是引用缓存的结果。

Vue2.x - 核心_第43张图片

需要注意的是,计算属性的setter需要引起计算属性依赖的options.data属性的变化,否则Vue不会将计算属性的更新到页面中

计算属性的简写

计算属性更多的使用场景是在访问,读取,而修改场景用的非常少,所以Vue支持省去计算属性的setter方法定义,而只需要定义一个getter方法,而由于只有一个getter方法定义,则可以直接省略getter方法名,而将计算属性定义为方法形式。

Vue2.x - 核心_第44张图片

此时虽然计算属性写成了函数形式,但是其本质还是一个配置对象。

并且最终fullName还是会被代理给vm实例,作为vm实例的一个属性,所以我们千万不能在模板语法中将计算属性当成函数来调用。 

Vue2.x - 核心_第45张图片

计算属性的getter的调用时机

计算属性的getter会在:

  • 模板首次渲染,即初始化时调用一次
  • 当计算属性依赖的options.data属性发生变化时,就会引起计算属性getter的调用

另外计算属性的setter也可以引起计算属性依赖的options.data属性的改变,如果将计算属性看成一种Watcher,则计算属性和其依赖的options.data属性之间也会形式一种双向数据绑定。

侦听属性

为什么需要侦听属性

前面我们已经简单了解了Vue的响应式原理,即将options.data中的属性通过Object.definedProperty定义为访问器属性,这样就可以通过setter方法来在属性修改后这个时刻,添加一些响应式行为,比如重新渲染模板。

这上面这个过程中,当options.data属性发生改变,会发生Vue内置的响应式行为即:重新渲染模板。

// 假设val只传入简单值,不传入对象值
function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get(){
            // watcher依赖收集
            return val
        },
        set(new){
            val = new
            // 响应式处理:如重新渲染模板
        },
        configurable: true,
        enumerable: true
    })
}

那么我们是否可以在属性修改后的响应式操作中添加其他行为呢?

Vue提供了侦听属性,即侦听options.data中属性是否被修改,若被修改,则在Vue内置响应式行为之前,执行侦听属性注入的行为。

侦听属性的好处是,为开发者提供了一个自定义响应式行为的渠道。

侦听属性定义options.watch

我们可以在options.watch中定义侦听属性,侦听属性本质是一个配置对象:

  • 配置对象名:为被侦听的options.data.xxx的名字
  • 配置对象方法handler:侦听的options.data.xxx属性发生改变时要注入的行为,handler是一个方法,该方法接收两个参数,按顺序是 “被侦听的属性修改后的新值”、“被侦听的属性修改前的旧值”
  • 配置对象方法handler方法如果定义为普通函数,则this指向当前vm实例,如果定义为箭头函数,则this为外层作用域的this

Vue2.x - 核心_第46张图片

Vue2.x - 核心_第47张图片

侦听属性的handler方法执行时机

侦听属性的handler方法默认只在被侦听的options.data.xxx发生改变时,才会执行。

即初始化,由于被侦听的options.data.xxx属性没有发生改变,所以侦听属性的handler方法不会执行。

但是有些场景,我们需要侦听属性的handler在初始化时执行,此时依赖于侦听属性的immediate配置,该配置默认为false,即默认初始化时不执行handler,我们可以改为true,侦听属性的handler在初始化就执行

Vue2.x - 核心_第48张图片

 需要注意此时被侦听的options.data.xxx属性的旧值为undefined

深度侦听

侦听属性侦听的是options.data.xxx,其中xxx可能是一个简单值,此时当xxx的值发生变化,侦听属性就可以侦听到。但是如果xxx是一个对象值(比如对象、数组),到底是:

  • xxx指向的对象地址值发生改变,才会引起侦听属性handler执行
  • xxx的子级属性值发生改变,就会引起侦听属性handler执行

Vue2.x - 核心_第49张图片

可以发现options.data.content.desc属性值改变,无法引起侦听属性content的handler执行,即侦听属性默认不会进行深度侦听。

那我们该如何侦听到被侦听属性的子属性变化呢?

方案一就是,直接侦听需要侦听的属性对象的子属性:

Vue2.x - 核心_第50张图片

 但是如果需求是,侦听对象下的任一属性发生改变,则上述方案则无法奏效,此时我们需要借助侦听属性的deep配置,如下

Vue2.x - 核心_第51张图片

由于侦听属性是content对象,所以侦听的新旧值也是对象,这里我们将对象序列化一下

Vue2.x - 核心_第52张图片

这里发现侦听属性content深度侦听到的新旧值相同,这是因为content对象地址值并未改变,只是对象属性desc值改变,所以对于侦听属性来说,新旧值都是指向同一个对象,所以打印出来的内容必然都是新值内容。

侦听属性的两个定义时机,以及简写

 前面介绍的侦听属性都是在创建vm实例时设置的,即options.watch

new Vue({
    el: '#root',
    data: {
        content: {
            auth: 'qfc',
            desc: 'Hello Vue'
        }
    },
    watch: {
        content: {
            immediate: true,
            deep: true,
            handler(newVal,oldVal){
                console.log(`content被修改了,新值为${JSON.stringify(newVal)},旧值为${JSON.stringify(oldVal)}`);
            }
        }
    }
})

还有另一种时机指定侦听属性,即创建vm实例之后,通过

xvm.$watch(属性,handler:function,options:{immediate, deep})

const vm = new Vue({
    el: '#root',
    data: {
        content: {
            auth: 'qfc',
            desc: 'Hello Vue'
        }
    }
})

vm.$watch('content', function(newVal,oldVal){
    console.log(`content被修改了,新值为${JSON.stringify(newVal)},旧值为${JSON.stringify(oldVal)}`);
}, {
    immediate: true,
    deep: true
})

Vue2.x - 核心_第53张图片

 对于第一种方式,如果我们不需要设置immediate,deep,只需要设置handler,则可以简写为

new Vue({
    el: '#root',
    data: {
        content: {
            auth: 'qfc',
            desc: 'Hello Vue'
        }
    },
    watch: {
        content(newVal,oldVal) {
          console.log(`content被修改了,新值为${JSON.stringify(newVal)},旧值为${JSON.stringify(oldVal)}`);
        }
    }
})

Vue2.x - 核心_第54张图片

对于第二种方式$watch,如果无需设置immediate,deep,则可以省略$watch方法的第三个参数。

Vue2.x - 核心_第55张图片

侦听属性和计算属性的区别与联系

计算属性其实也算是一种侦听属性,比如计算属性fullName,实时侦听options.data.firstName和options.data.lastName是否发生改变,一旦发生改变则重新计算fullName。

计算属性和侦听属性最大区别在于:计算属性的getter要求同步return,而侦听属性的handler不需要,这种区别源于二者用途的不同;

计算属性需要侦听options.data中的属性,并计算得到一个新属性的值,所以需要同步return。

侦听属性侦听options.data中的属性,目的在于,在将属性新值渲染到模板前,进行一些自定义行为,而不是计算得到一个新属性。

而计算属性同步return的要求,限制了计算属性无法用于异步操作,而侦听属性则可以进行异步操作,比如侦听某个属性发生改变后3s在进行handler的执行。

Vue2.x - 核心_第56张图片

此时需要注意的是,由于Vue默认响应式操作,如渲染,都是同步性质的,所以此时用户自定义的异步性质的handler会在同步渲染后执行 

样式绑定

HTML标签有两个特殊的属性:class和style,这两个属性属于样式属性,前面介绍过属性绑定需要使用v-bind指令,并且v-bind指令值通常是单个表达式。而对于class属性和style属性的绑定,表达式取值有特殊。

class样式绑定

字符串变量表达式写法

当我们不确定标签所要绑定的样式名字时,则可以使用字符串表达式

Vue2.x - 核心_第57张图片

数组变量表达式写法

当我们不确定标签所要绑定的样式是什么,且有数量也不确定时,则适用数组表达式写法

Vue2.x - 核心_第58张图片

对象变量表达式写法

当我们确定了标签所要绑定的样式的名字和个数,但是不确定使用哪些样式时,可以使用对象表达式写法

Vue2.x - 核心_第59张图片

v-bind样式绑定不会覆盖原生class,而是追加

Vue2.x - 核心_第60张图片

style样式绑定

style样式绑定同样可以写成字符串形式

Vue2.x - 核心_第61张图片

 对象形式

Vue2.x - 核心_第62张图片

数组形式

Vue2.x - 核心_第63张图片

需要注意的是,对象形式和数组形式中style样式属性名都要写成驼峰命名法则,如background-color属性要写成backgroundColor,font-size属性要写出fontSize。

整体而言,style样式绑定不是很实用,当前使用class比较多。

条件渲染

v-show

v-show指令不用于绑定标签属性,而是单独作为标签属性使用,指令值为一个表达式,

  • 若表达式结果为true,则隐藏对应标签
  • 若表达式值为false,则显示对应标签

Vue2.x - 核心_第64张图片

v-show控制标签显示隐藏的本质利用的是样式属性display

Vue2.x - 核心_第65张图片 即v-show指令隐藏标签并不是真的移除对应标签,而是为标签添加display:none样式。

v-show指令适用于哪些高频率切换显示或隐藏的标签,采用display:none样式可以有效避免同一个标签的新增和移除,减少了DOM树重新构建的次数,提高了渲染性能。

v-if

v-if指令也不用于绑定标签属性,而是单独作为标签属性使用,v-if指令的值为一个表达式

  • 如果表达式的值为true,则保留该标签
  • 如果表达式的值为false,则移除该标签 

Vue2.x - 核心_第66张图片

和v-show的区别是,v-show是控制标签的显示隐藏,隐藏的标签不会被移除,而是使用display:none控制,而v-if是控制标签是否移除,若被移除,则将对应标签直接从DOM树种移除。

v-if 适用于低频的,或者一次性的绝对是否显示隐藏标签的场景。

v-else-if、v-else

上面例子中v-if的使用,其实并不是性能最佳的,因为”欢迎游客“和”欢迎会员“是一次性判断,如果两个标签都使用v-if控制,则会进行两次判断。

Vue2.x - 核心_第67张图片

所以Vue参考if...else if...else设计了v-else-if和v-else指令

Vue2.x - 核心_第68张图片

需要注意的是:

  • v-else-if可以带指令值,v-else不需要带指令值,v-else-if可以多次使用,v-else只能使用一次
  • v-if可以分别只和v-else-if或者v-else结合使用,或者全部一起使用
  • v-if、v-else-if、v-else组合使用时,标签之间要紧密,不能有其他标签,否则会报错

Vue2.x - 核心_第69张图片

template标签

 当多个同级DOM标签需要使用一个条件判断是否展示时:

  • 对于有父级标签的,则直接在父级标签上添加v-if判断
  • 对于没有父级标签的,如果我们为它们创造一个父级标签,然后基于新的父级标签加v-if判断,则可能会导致DOM结构无缘无故多了一个标签,即会破坏整体结构

所以为了解决没有父级标签的,依赖于同一判断展示条件的多个DOM标签,可以使用template标签包裹,即使用template标签作为父级标签,而template相较于传统的div标签而言,在Vue重新渲染模板时,会自动移除template标签,即不会破坏整体DOM结构

Vue2.x - 核心_第70张图片

列表渲染

v-for指令基本使用

在前端有一个高频需求,就是将数组渲染为列表在页面展示,Vue提供了v-for指令来实现。

Vue2.x - 核心_第71张图片

 v-for指令可以:

  • 遍历出options.data.xxx数组中的每个元素,遍历语法:v-for="(item, index) in xxx",其中item是每一项数组元素,index是该项数组元素的索引位置,且index是可选的,v-for遍历出来的item,index可以直接用于模板语法中,即Mustache语法和v-bind指令中。
  • v-for可以将所在标签进行循环创建,即v-for遍历数组几次,就会创建几次所在的标签。

v-for其实不仅可以遍历数组,还可以遍历对象、字符串、数字

Vue2.x - 核心_第72张图片

v-for遍历对象时,遍历的对象的属性名和属性值,且属性值相当于item,属性名相当于index,其实很好理解:数组是一个特殊对象,索引值index相当于属性名,元素item相当于属性值。

Vue2.x - 核心_第73张图片

遍历字符串,其实就相当于将字符串看成一个数组,字符串的每个字符就是数组元素,字符索引就是数组元素索引。

Vue2.x - 核心_第74张图片

遍历数字,即从1遍历到给的数字,而遍历出来的数字1的索引就是0。 

key属性

当我们使用v-for循环创建标签时,Vue底层会为循环创建出来的每个标签添加一个隐式的属性key,且key的值为v-for循环遍历的index值。即相当于如下等价代码:

Vue2.x - 核心_第75张图片

那么为什么v-for遍历默认搭配key属性呢?

我们知道v-for常用于数组的循环遍历,并创建装在遍历结果item,index的标签。而数组作为一种数据容器,时常会发生数组元素的更新,而一旦数组发生更新行为,则需要将新数据重新渲染到模板中。此时有两种方案:

  • 推倒重来式:即基于数组重新循环遍历,为所有数组元素重新创建标签,然后替换掉原有标签
  • 精确打击式:只创建发生改变的数组元素对应的标签,然后替换对应的原有标签

明显是精确打击式方案更加性能友好,但是实现有如下难点:

  • 如何做到只创建发生改变的数组元素的标签?
  • 如何找到旧的需要替换的标签?

虚拟DOM

Vue提出了虚拟DOM,所谓虚拟DOM即为真实DOM结构的一种JS对象形式的表达。

具体对比如下,即每个真实DOM节点都可以转为一个虚拟DOM的JS对象

Vue2.x - 核心_第76张图片

 而Vue渲染模板时,不会直接创建真实DOM,而是先创建虚拟DOM,再根据虚拟DOM创建真实DOM。

那么虚拟DOM的作用是啥呢?

当Vue渲染好模板后,意味着真实DOM已经创建完成,而对应虚拟DOM会被缓存在vm._vnode属性中。

当下次渲染前,Vue又会创建新的虚拟DOM对象,然后将新的虚拟DOM对象和保存在vm._vnode中的旧的虚拟DOM对象进行对比,然后找到具有差异虚拟DOM节点,然后只创建发生差异的真实DOM节点,最后将真实DOM节点替换到旧的DOM结构上。

这个过程又遇到了一个问题:新旧虚拟DOM的节点之间如何对应?

此时就要借助DOM节点的key属性了。key属性的作用就是帮助新旧虚拟DOM对比求异时,帮助Vue对应新旧虚拟DOM之间节点的。

Vue2.x - 核心_第77张图片

Diff算法

有了虚拟DOM,可以帮助Vue完成了精确打击式的DOM更新操作了,所以DOM构建的性能得到了优化,但是此时压力来到了另一边,即节省了DOM构建的时间,但是却增加了新旧DOM对比查找差异的时间,所以如何减少新旧DOM对比求异的时间成为进一步提示性能的关键点。

此时Vue借鉴了业界公认的比较快的虚拟DOM Diff算法,该算法可以减少新旧DOM对比求异的时间,提高效率。而Diff算法具体逻辑后续搞个章节专门说。

深入理解虚拟DOM运作和key属性作用

前面我们已经知道了key属性的作用就是帮助Vue在新旧虚拟DOM对比求异时,可以匹配二者之间的key属性值相同的节点。

而对于v-for指令循环遍历创建出来的标签,默认使用的是 :key="index",即使用数组元素的索引作为标签key属性值。

我们知道数组通常有如下更新操作:

  • 插入:unshift(头插)、push(尾插)
  • 删除:shift(头删)、pop(尾删)
  • 乱插乱删:splice

其中unshift头插、shift头删会造成数组元素索引的整体变更,splice可能会造成数组元素索引的整体变更。

push和pop不会造成遗留数组元素索引的变更。

那么一旦使用引起数组元素索引整体变更的数组更新操作,即会影响Vue的虚拟DOM对比性能:Vue2.x - 核心_第78张图片

可以发现,一旦数组元素的索引发生整体变更,则新旧虚拟DOM对比时,会发现对应的新旧DOM节点都不一样,所以都需要创建DOM真实节点,然后替换掉旧DOM真实节点。 

这样依赖,虚拟DOM不仅没能提升性能,反而会拖累性能。即对比新旧虚拟DOM的工作白做了,还要整体替换更新DOM节点。

此时,我们有两种解决方案:

  • 数组更新不使用影响数组元素索引的操作,比如shift、unshift、splice
  • key属性值不使用数组元素索引值

第一种方案无法适应全部场景,有些场景就是需要往数组中乱序地加入新元素,所以第一种方案不推荐。

第二种方案是值得推荐的。目前企业级开发中,数组元素如果是对象,则基本都要求有一个id属性,该属性是当前数据的唯一标识,不可重复。如果key使用id作为值,则可就不限制使用数组更新操作了。

Vue2.x - 核心_第79张图片

Vue2.x - 核心_第80张图片 此时新旧虚拟DOM对比时,根据key就可以找到真正对应的节点了。

key运作原理的总结

虚拟DOM中key的作用:

key是虚拟DOM对象的标识,新旧虚拟DOM对比时,会将key属性值相同的虚拟DOM节点进行对比找差异。

运作流程:

如果新虚拟DOM根据key可以找到旧虚拟DOM对应节点,则对比:

  • 如果节点内容未发生改变,则不创建新虚拟DOM节点的真实DOM对象,沿用旧真实DOM节点
  • 如果节点内容发生改变,则创建新虚拟DOM节点的真实DOM对象,替换旧真实DOM节点

如果新虚拟DOM根据key没有找到旧虚拟DOM对应节点,则:

  • 该节点为新增节点,直接创建真实DOM对象,然后渲染到页面中

用index作为key的问题

一旦数组发生改变整体数组元素索引的更新操作,如shift、unshift、splice等,则会导致新旧虚拟DOM对比无意义。反而会拖累渲染性能。

如何选择key值

使用数据的唯一标识,比如id值作为key

key=index引发的错误DOM更新问题

如果选择index作为key,则被v-for循环创建的标签内容包含input输入类标签,则会发生意想不到的错误

Vue2.x - 核心_第81张图片

一开始 qfc对应输入001,但是当我们对数组进行头插一个新元素后,发现001输入对应到了xxx,这是不对的。 错误原因是:

首先是数组元素索引已经乱了,所以新旧虚拟DOM根据key对比发现文本节点已经不一致所以需要更新文本节点。

那么为什么input标签节点没有发生更新呢?

比如旧DOM中 qfc文本节点对应的input节点已经有了用户输入,而新DOM的input是没有值的,那么为啥没有发生更新呢?

因为旧虚拟DOM对象在被缓存时,input节点是没有值的,而旧虚拟DOM被转为真实DOM时,才接收的用户输入,所以旧虚拟DOM中input是没有用户输入的,所以新旧虚拟DOM对比input是没有差别的,所以Vue不会更新key=0的input标签,也就保留了用户输入。

Vue2.x - 核心_第82张图片

列表过滤和列表排序

我们经常需要对查询结果(列表)进行条件过滤或者排序,而对于Vue来说,v-for只负责列表渲染,而列表过滤和列表排序本质上是v-for循环遍历的源数组的过滤和排序。而无论是排序还是过滤,其实都有一个基本原则,就是不能造成源数组丢失,即我们只是在源数组的备份上进行排序和过滤,当排序或过滤条件改变后,我们可以继续从源数组上获取备份重新操作。

基于computed进行列表过滤

Vue2.x - 核心_第83张图片

基于watch进行列表过滤

Vue2.x - 核心_第84张图片

列表排序案例

Vue2.x - 核心_第85张图片

数据模型更新

Vue.set方法

options.data数据模型的指定时机有两个:

  • 创建vm实例时,传入Vue构造函数的options.data配置来指定
  • 创建vm实例后,通过vm.$set或者Vue.set方法来指定

前面已经学习了第一种方式,下面来演示下第二种方式

Vue2.x - 核心_第86张图片

 $set是Vue构造函数原型对象上的方法(实例方法),set是Vue构造函数的方法(静态方法),并且$set底层就是使用的Vue.set实现的,$set其实是Vue.set的别名。

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

 Vue.set方法的入参分别是:

  • target 响应式对象
  • propertyName 新增属性的属性名,index新增元素的索引号
  • value 新增属性的属性值,或者新增元素的值

Vue.set方法的作用:

响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。

那么options.data是响应式对象吗?

前面学习了Vue响应式原理,我们了解到,vm实例会新增一个_data属性指向options.data配置对象,然后将options.data下的属性都改造为响应式的(即有getter、setter方法的属性),但是options.data本身并没有被改造为响应式的,我们可以检查vm._data是否有getter、setter来验证:

Vue2.x - 核心_第87张图片

 可以发现,vm._data并不是一个响应式属性。所以Vue.set并不能向vm._data(即options.data)上新增属性。只能向vm._data.xxx上新增属性,因为vm._data.xxx是响应式属性。

即Vue.set方法的target参数不能是: Vue 实例,或者 Vue 实例的根数据对象

Vue.set为对象添加响应式属性

Vue2.x - 核心_第88张图片

 如果我们直接为vm.obj添加属性,则新增的属性并非响应式的,即无法触发模板更新。 

如果我们使用Vue.set为vm.obj添加新属性,则新增属性是响应式的,可以触发模板更新。

Vue.set为数组添加响应式属性

Vue2.x - 核心_第89张图片

如果我们直接粗暴的在vm.arr[index] = xxx来修改数组元素,则无法触发响应式,进而导致无法触发页面重新渲染。

 如果我们使用Vue.set为vm.arr修改指定索引位置的元素值,则可以完成响应式动作,进而触发页面重新渲染。

数组原型方法的响应式改造

数组是一种特殊的对象,数组和对象的区别在于,数组的用途是作为存储数据的容器,而对象更多是承担数据模型的角色。

所以对象的属性的个数是较少的,而数组的元素是可能非常多的。

所以,我们不能将每个数组元素都改造为响应式的,因为当数组存在成千上万个元素时,改造工作量是非常大的,改造完的数组也是极其笨重的。

所以前面例子中,我只展示了Vue.set新增对象属性后,该属性的响应式结构,而没有展示Vue.set新增数组元素后,新增元素的响应式结构,因为新增的元素没有响应式结构。

那就产生了一个问题,数组元素没有响应式结构getter、setter,它是如何通知vm去重新渲染模板的?

其实响应式的目的就是:当数据变化时,重新渲染模板。

对象属性的响应式实现是:将对象属性改造为访问器属性,即有getter、setter的属性,当对象属性发生改变时,setter调用,我们只需要在setter调用中加入重新渲染模板的逻辑,就实现了响应式。

所以getter、setter只是一种监听数据变化的手段。但并不是监听数据变化的唯一手段。

我们更新数组,不仅可以有基于“arr[index] = xxx”这种对象属性式操作,还可以基于丰富的数组原型上的内置方法,包括:

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

而Vue底层改造了这些数组原型的方法,在原有方法逻辑基础上,添加了重新渲染模板的逻辑,即当这些方法更新完数组后,就重新渲染模板。

由于我们操作数组,基本上都是使用这些方法,很少直接基于索引去操作数组,所以Vue将这些方法改造为响应式方法是符合实际开发场景的。

Vue2.x - 核心_第90张图片

双向数据绑定

v-model

前面介绍了v-bind指令,v-bind指令可以将options.data中的属性绑定为标签属性值,这是一种单向绑定,所谓单向,指的是数据只从Model流向View。

另外还存在一种双向数据绑定,即数据先从View流向Model,再有Model流向View。双向数据绑定发生在哪些可以接收用户输入的标签上。

Vue为双向数据绑定设计了一个指令:v-model。该指令专门用于绑定标签的value属性。所以"v-model:value='expression'" 可以简写为 "v-model='expression'"。

Vue2.x - 核心_第91张图片

收集表单数据

具有内置value属性的标签最常见的就是表单域标签,所以v-model指令常用来收集用户在表单中输入的数据。但是不同的表单域标签,v-model收集到的数据也有所不同。

 下面是一个用户信息收集表单代码,里面包含了常用的表单域标签,如input:text、input:password、input:radio、input:checkbox、select、textarea




  
  
  
  Document
  


  
用户名:

密码:



爱好: 吃饭 睡觉 打豆豆

居住地:

自我介绍:

同意协议:

Vue2.x - 核心_第92张图片

value属性何时需要预置值

通过运行效果,可以看出,

input:text、input:password、textarea这三个表单域标签是可以接收用户直接输入,用户直接输入会被HTML标签赋值给其value属性,所以v-model绑定标签value属性后,可以直接接收用户输入。

input:radio、input:checkbox、select无法接收用户直接输入,只能接收用户勾选状态,所以需要提前定义好标签value属性,当用户勾选动作发生后,HTML标签就会将其value属性激活,这样v-model就会得到预置好的value属性值。

checkbox两种使用情况说明

关于input:checkbox,有两种用法,一种是作为多选框(如:爱好),一种是作为开关框(如:同意协议)

input:checkbox作为多选框时,即存在多个checkbox选项,此时每个checkbox都需要预置value属性值,且v-model绑定的模型属性需要为数组,这样才能存储多个checkbox的value值。

Vue2.x - 核心_第93张图片

input:checkbox作为开关框时,即只存在一个checkbox选项,此时该checkbox只是用来收集用户勾选状态checked属性值true/false,而v-model绑定的模型属性只要是非数组就可以。

Vue2.x - 核心_第94张图片

收集表单数据的数据模型设计

收集表单数据的目的是提交表单数据,比如通过ajax提交到后端服务器,所以我们期望将收集到的表单数据设计为一个对象模型,比如用户数据设计为userInfo对象,这样当form表单submit时,就可以直接在options.methods.xxx回调中直接将this.userInfo提交到服务器。如果没有将收集的表单数据收集到一个对象中,则我们需要手动将分散的表单数据组装为对象,然后才能提交。

v-model修饰符

.trim修饰符

我们在收集用户输入信息时,通常需要对用户输入信息进行必要的检查,如去除输入字符串的前后无效空格,此时可以为v-model指令添加.trim修饰符

Vue2.x - 核心_第95张图片

.number修饰符 

一般而言,从表单域标签中接收的用户输入都是字符串类型,但是有些数据模型上对应属性要求是数字类型,为了不进行冗余的数据类型转换,可以为v-model添加.number修饰符,即告知Vue底层自动将用户输入值转为数字类型

Vue2.x - 核心_第96张图片

 .lazy修饰符

在前端开发中,遇到表单输入框,不得不提一个有关性能的问题,就是防抖和节流。

所谓防抖,即一次事件触发后N秒内,为回城等待时间,等待时间内事件再次触发则重新等待N秒,若等待时间内该事件没有再次触发,则执行回调。

所谓节流:即一次事件触发后N秒内,为技能冷却时间,冷却时间内事件再次触发不会执行事件回调,冷却时间过后,事件再次触发才会执行回调。

指定了v-model指令属性的input输入框,当我们输入时,每输入一个字符都会引起数据从View流向Model,并且Model改变,又会导致数据从Model流向View,即模板重新渲染。

而我们在输入框输入内容是一个频繁行为,如果每输入一个字符就触发一次模板渲染,则非常浪费性能,我们期望的是:当输入完毕后,再进行双向数据绑定,重新渲染模板。

Vue为v-model指令提供了.lazy修饰符,添加了.lazy修饰符的v-model只有在输入框失去焦点后,才会触发双向数据绑定,重新渲染模板。

Vue2.x - 核心_第97张图片

Vue2.x - 核心_第98张图片

内置指令

前面学习的v-bind、v-on、v-for、v-if、v-show、v-model指令都是Vue的内置指令,除了这些内置指令,Vue还有一些其他的常用的内置指令。

v-text

v-text用于渲染标签体的文本内容,和Mustache语法的区别是:v-text指令值会覆盖标签已有的内容,而Mustache语法不会。

Vue2.x - 核心_第99张图片

v-html

v-html用于渲染定标签体的带有HTML结构的内容,v-html指令值也会覆盖标签体已有的内容,v-html和v-text的区别在于,v-text无法渲染HTML结构,而v-html可以。

Vue2.x - 核心_第100张图片

但是v-html容易造成XSS注入攻击,比如

Vue2.x - 核心_第101张图片

Vue2.x - 核心_第102张图片

 如果我们需要使用v-html渲染文本内容,则必须对文本内容数据进行校验,避免出现可能产生XSS注入的字符。

但是我们最好避免使用v-html指令。

v-once

v-once指令所在的标签在初次被渲染后,就不会再次改变,即变为了静态内容。在实际业务场景中,网页中有很多内容只在初始化时需要被动态渲染,初始化之后就固定了。

即v-once的作用是,在首次模板渲染后,将v-once所在标签的模板变量全部去除

Vue2.x - 核心_第103张图片

v-cloak

如果我们引入的vue.js是某CDN仓库的,可能会发生CDN仓库繁忙的情况,此时网页引入vue.js缓慢,则可能会发生什么情况呢?我们模拟一个静态资源托管服务器,使之可以提供vue.js,并且延迟3s秒提供。

Vue2.x - 核心_第104张图片

然后我们在网页中引入该服务器提供的vue.js,引入位置有两个地方:

  • 视图模板之前引入
  • 视图模板之后引入

我们知道浏览器渲染网页过程中,首先要构建DOM树,而构建DOM树过程中如果遇到script标签引入js,则DOM树构建被阻塞,浏览器全力去加载js文件,并执行它,当js文件被成功执行后,DOM树才能继续构建。所以script标签引入js会阻塞DOM树构建,后果是渲染过程中,网页白屏事件变长,用户体验变差。

所以如果在视图模板之前引入vue.js,则会造成3s白屏时间。

Vue2.x - 核心_第105张图片

 为了避免让用户看到长时间白屏,我们一般将script标签放到视图模板之后,此时视图模板将先被渲染到网页中,而不会被script标签引入js阻塞,但是出现了新的问题:那就是视图模板的模板字符串语法被呈现给了用户。这当然也是非常差的用户体验,因为用户压根看不懂这是啥,大概率认为网站出了bug,引发信任危机。

出现这种情况的原因是:vue.js引入太慢了,导致Vue没有办法快速接管网页构建工作。如果vue.js可以瞬间被引入,则原始视图模板只会在一瞬间内出现,然后迅速被Vue重新渲染,而用户对于这个瞬时过程是无法感知的。现在是因为vue.js引入太慢,原始试图模板出现的时间变长了,可以被用户感知到了。

Vue2.x - 核心_第106张图片

 此时Vue想出来一个办法,即在Vue还没有接手网页构建前,使用固定的CSS类型选择器 [v-cloak] 来将视图模板的模板语法隐藏,而当Vue接手后,立马移除掉所以视图模板标签中的v-cloak属性,这样就可以解决由于vue.js引入太慢,导致原始视图模板的模板语法被渲染到网页的问题了。

Vue2.x - 核心_第107张图片

Vue2.x - 核心_第108张图片

v-pre

在视图模板中,存在很多的标签没有使用Vue模板语法,指令语法,即固定静态的内容,这些内容虽然是静态的,但是Vue在编译模板时,依旧会去检查这些标签是否有模板语法、指令语法,这是非常浪费性能的,所以为了节约性能,Vue对于这些固定静态的内容,可以给定一个v-pre指令,该指令的功能是在Vue编译到该标签告诉Vue这个标签是静态的,无需检查的,然后Vue编译就会跳过此标签,从而加速了整体编译速度。

Vue2.x - 核心_第109张图片

由于h1标签已经指定了v-pre指令,所以Vue编译会直接跳过该标签,将其作为静态内容展示,即使其内部使用了模板语法。 

自定义指令

Vue内置指令设计分析

如果Vue内置指令还不能满足开发需求的话,Vue支持开发自定义指令。在自定义指令之前,我们需要思考下Vue指令有哪些关键设计:

Vue指令的完整语法形式:v-xxx:arg.modifier = "expression || inline statement"

  • xxx:指令名称,如bind指令,on指令,model指令
  • arg:指令参数,如v-bind:href,v-on:click,v-model:value
  • modifier:指令修饰符,如v-on:click.prevent、v-model:value.trim
  • expression || inline statement:指令值,通常是一个表达式,也可以是内联语句,如v-bind:href="link"中link就是一个变量表达式,v-on:click="n++" 中 n++就是一个自增语句

Vue指令的职责在于:当Vue指令值发生改变时,Vue指令需要将改变作用到DOM上

  • 如v-bind:href="link"中link变量值改变,则v-bind指令需要将新的link值更新到当前所在标签的href属性上。
  • 如v-for="p in persons"中persons变量值发生改变时,v-for需要重新生成虚拟DOM对象,和old虚拟DOM对象进行对比,然后生成差异部分的真实DOM,并更新到old真实DOM中。

自定义指令

基础语法

在创建vm时,传入Vue构造函数的配置对象options中有一个属性directives,该属性接收一个对象,而options.directives对象得属性就是自定义指令,属性名就是指令名,属性值就是指令定义对象。即如下所示:

new Vue({
    el: '#root',
    data: {
        desc: 'Hello Vue',
    },
    directives: {
        xxx: { // xxx为自定义指令名称,指令定义为一个配置对象

        }
    }
})

这里需要注意的是,指令定义时名称不带v-前缀,而在使用时需要加v-前缀。

钩子函数

自定义指令的定义对象,主要包含如下几个可选的钩子函数:

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
  • inserted:指令所在元素被插入到页面时调用
  • update:指令所在模板被重新解析时调用
  • componentUpdated:指令所在组件被重新解析时调用
  • unbind:指令与元素解绑时调用。只调用一次。

这几个钩子函数都默认接收四个参数:

  • el:指令所在元素对于的DOM对象,可以基于el进行DOM操作
  • binding:指令关键要素对象,包含如下指令相关信息属性:
  1. name:指令定义的名词
  2. value:指令的表达式值
  3. oldValue:上一个表达式值。仅在 update 和 componentUpdated 钩子中可用
  4. expression:字符串形式的指令的表达式
  5. arg:指令的参数
  6. modifier:指令的修饰符,是一个对象,如v-model.trim.lazy的该属性值为:{trim:true, lazy:true}
  • vnode:Vue 编译生成的虚拟DOM对象
  • oldvnode:上一个虚拟DOM对象。仅在 update 和 componentUpdated 钩子中可用 

注意:除了 el 之外,其它参数都应该是只读的,切勿进行修改。

Vue2.x - 核心_第110张图片

 上面自定义指令v-mymodel的指令名称出现了多个单词,使用时建议考虑使用cabe-case写法,即v-my-model,而在定义时,将指令名称也写为'my-model'

钩子函数的this指向

Vue2.x - 核心_第111张图片

可以发现Vue指令的钩子函数的this指向都是window全局对象,那么为啥不是vm实例对象,因为Vue指令本质是操作DOM,即操作View模板,而不是操作Model数据模型,所以不需要得到vm实例。 

通过一个例子理解bind和inserted钩子的区别

需求:当页面加载时,输入框元素自动获得焦点

思考:其实DOM操作很简单,就是dom.focus()即可,关键难点在于何时进行dom.focus(),需求是页面加载时,那么到底是bind钩子还是inserted钩子呢?

Vue2.x - 核心_第112张图片

可以发现在bind钩子中,虽然已经获取到了输入框元素对象,但是无法使他聚焦。

Vue2.x - 核心_第113张图片

而在inserted钩子中,可以实现输入框聚焦。

原因是:focus函数使DOM元素聚焦,必须要该DOM元素已经插入到页面中,对于未插入页面的DOM元素,focus方法无法使它聚焦。说白了,聚焦是浏览器对于网页元素的动作,而对于尚在内存层面的DOM元素,focus无能为力。

bind钩子函数调用时机是:当指令与元素绑定时,但指令所在元素尚未被插入页面时,所以此时bind钩子函数获得的el的DOM对象尚处于内存中。

inserted钩子函数调用时机时:当指令所在元素插入页面时,此时el对应的DOM已经在网页中了,所以可以进行focus聚焦。

bind和update两个钩子的关系,及自定义指令函数式写法

之前在学习侦听属性时,我们知道侦听属性默认情况下是不侦听初始化时属性的变化的,而只会侦听初始化后属性的更新。而我们可以通过设置侦听属性的immediate:true来开启初始化侦听动作。此时侦听属性的初始化侦听和更新侦听其实都是使用的handler处理函数,即行为一致。

而自定义指令的bind和update钩子函数,其实就是类似于初始化和更新,很多时候,初始化和更新的行为是一致的,所以此时自定义指令可以定位为函数式

Vue2.x - 核心_第114张图片

自定义指令的函数式写法,相当于如下对象式写法

全局自定义指令

如果自定义指令是多个vm实例通用的,则将其定义为某个vm实例的option.directives就不合适了,因为此时自定义指令只能在该vm实例管理的视图模板中使用,而不能被其他vm实例使用,所以Vue支持定义全局的自定义指令。

Vue.directive(指令名称,指令定义对象 || 指令定义函数)

Vue2.x - 核心_第115张图片

Vue2.x - 核心_第116张图片

生命周期

Vue生命周期图

Vue2.x - 核心_第117张图片

当我们new Vue创建vm实例时,Vue底层会经历上述流程:

beforeCreate

在beforeCreate阶段,vm实例被创建,并且给vm实例赋予了生命周期,事件能力,但是vm实例对于options.data的数据代理还没有开始。

beforeCreate阶段完成后,即可调用beforeCreate生命周期钩子函数。如下示例:

Vue2.x - 核心_第118张图片

可以发现,beforeCreate钩子函数的this指向了当前vm实例,此时vm实例还未完成对于options.data的数据代理。 

created

在beforeCreate之后,Vue底层立即将options.data的属性改造为响应式的,并且将options.data下的属性全部代理给了vm实例,当这个动作完成后,就会触发created钩子函数。

Vue2.x - 核心_第119张图片

 可以发现此时已经可以通过vm实例访问到options.data上的属性了,且已经完成了响应式改造

beforeMount

在create之后,Vue底层就会进行视图模板的解析工作,并生成虚拟DOM,但是视图模板的指定有多种方式,和多种情况,此时获取视图模板的逻辑如下:

  1. 如果指定了options.el,则直接下一步;如果未指定options.el,则等待vm.$mount(el),然后下一步;
  2. 如果指定了options.template,则将options.template作为视图模板来解析,如果未指定options.tempalte,则将上一步指定的el作为视图模板来解析
  3. 基于视图模板生成虚拟DOM对象

这里需要注意的是:

1、如果同时指定了options.template,和options.el(或vm.$mount(el)),则以options.template指定的视图模板为准

Vue2.x - 核心_第120张图片

一旦使用了option.template模板,则options.el或vm.$mount(el)指定的模板根元素将被覆盖

Vue2.x - 核心_第121张图片

 options.template模板定义时必须只能有一个顶级元素,因为vm管理的视图模板只能有一个

Vue2.x - 核心_第122张图片

 在beforeMount阶段,仅仅只是生成了试图模板对应的虚拟DOM对象,还没有将视图模板解析完成挂载到网页中,即此时网页中还在展示包含模板语法的原始视图模板。

所以此时我们虽然可以获取到DOM,但是这些DOM都是未被Vue编译的原始视图模板的DOM,最终都会被包含时机数据的视图覆盖掉。

mounted

在beforeMount后,Vue底层就可以得到一个虚拟DOM对象,基于虚拟DOM对象将其中模板语法替换为真实数据,然后生成真实DOM,并挂载到网页中,此时将触发mounted钩子函数。所以理论上来说,再mounted钩子函数中获取DOM并操作DOM都是有效的,但是我们不推荐在Vue中直接操作DOM。

我们将 beforeCreate、created、beforeMount、mounted四个阶段称为Vue的初始化阶段。mounted作为初始化阶段的最后一个阶段的钩子函数,我们通常在此阶段做一些网页初始化动作,比如发送ajax到服务器获取某些网页所需的初始化展示数据等。

实现需求:网页一打开,网页内容就进行透明度0~1的循环变化。

这个需求的难点在于一打开网页就进行某个动作,目前,我们能想到的网页一打开仅能进行操作的方式无非就是:侦听属性打开immediate:true,实现如下

Vue2.x - 核心_第123张图片

这种方式虽然能实现需求,但是有一个致命问题:由于侦听opacity属性,且初始化opacity属性时就会侦听,然后触发handler函数执行,开启一个定时器,进行opacity的0~1循环渐变。

但是opacity更新,又会触发侦听属性handler的执行,则又开启了一个定时器,然后又触发了opacity的更新,....,所以会造成开启无数个定时器,最终内存溢出。

Vue2.x - 核心_第124张图片

 那么有没有什么补救措施呢?

其实思考下我们使用侦听属性的目的,只是用来实现初始化动作的开启,而opacity的循环渐变和侦听属性目的无关,所以我们完全可以侦听一个不会发生更新的属性,如下面的tmp,此时就不会产生无数个定时器了。

Vue2.x - 核心_第125张图片

这个需求实现方案引入了一个无意义的属性,所以我们在实际业务开发中不会使用这个方案来实现初始化动作的开启。

而mounted钩子函数是实现初始化动作开启的最佳方案:

Vue2.x - 核心_第126张图片

beforeUpdate和updated

当我们初始化完成后,必然会对页面数据进行操作,触发options.data的属性数据的更新,此时需要注意的是:虽然options.data的属性数据发生改变,会迅速地反馈到网页中,但是这个过程是一点都不简单直接,而是曲折的。

首先options.data的属性数据发送改变,触发了其setter的执行,setter更新数据后,会继续触发重新生成虚拟DOM,然后新旧虚拟DOM对比,然后找出差异部分,生成新的真实DOM,然后再更新到网页中。

由于这个过程很长,但是有两个很明显的分界点:

  1. 数据是新的,网页是旧的
  2. 数据是新的,网页是新的

我们将1阶段称为beforeUpdate,2阶段称为updated。

beforeUpdate和updated之间的动作就是:新旧虚拟DOM对比,生成差异部分真实DOM,并更新到网页中。

Vue2.x - 核心_第127张图片

上图中,在beforeUpdate钩子函数中加了断点,当vm.desc更新后,数据是新的,而网页还是旧的。

Vue2.x - 核心_第128张图片当走到下一个updated钩子函数中的断点时,网页也变成了新的。

beforeUpdate和updated阶段被称为更新阶段。

beforeDestory和destoryed

生命周期,有生必有死,vm实例可以被创建,也可以被销毁。

我们可以使用vm.$destory()来销毁vm实例,而vm的销毁流程也会经历两个阶段:

  • 弥留之际,交代后事
  • 彻底死亡

其中beforeDestory就是vm.$destory()调用后,但是vm实例还未彻底死亡前的弥留之际,此时vm实例的各项功能都处于可用状态,但是实际上,我们通过vm实例进行的操作基本上不会生效

Vue2.x - 核心_第129张图片

可以发现beforeDestroy钩子函数中,对于options.data的属性数据修改,不会再更新到网页中了,因为Vue认为在 beforeDestroy中的更新操作是无意义的。打个比方,你快要老死的时候,你和你的子女说:快帮我整个容,我要美美的死去。你的子女肯定认为这是无意义的。

那beforeDestroy钩子函数的作用是啥呢?

简单来说,类似于finally的作用,比如我们总是在finally中关闭数据库连接,即收尾动作。而beforeDestroy钩子函数就是用来收尾的,比如关闭定时器。

另外在vm被销毁之后,Vue的工作成果是被保留下来的。即vm虽然被销毁了,但是Vue构建出来的用户界面是得以保留的,其中包括页面中的数据,还有标签绑定的事件及事件处理程序:

Vue2.x - 核心_第130张图片

生命周期钩子函数使用总结

  • 我们一般在mounted中开启初始化动作,在beforeDestroy中进行收尾动作
  • beforeCreate中vm实例还未完成对options.data的数据代理,所以不要再此钩子中通过vm实例直接操作options.data的属性
  • beforeMount中不要进行任何DOM操作,因为之后的mounted会编译出网页覆盖调beforeMount中一切DOM操作
  • beforeUpdate时,options.data中属性数据是新的,但是视图模板中数据是旧的,只有在update阶段数据和视图才能保持一致。
  • beforeDestroy时,不要进行任何更新行为,因为Vue认为这是无意义,不会执行。
  • vm实例被彻底销毁后,Vue构建的用户界面依旧会被保留,并且网页中的数据和事件绑定都可用。

你可能感兴趣的:(Vue,vue)