目录
一、入门案例
二、模板语法
三、数据绑定
四、el和data的两种写法
五、MVVM模型
六、Object.defineproperty方法
七、Vue中响应式原理
八、数据代理
九、methods配置项
十、Vue中的事件处理
十一、Vue中的键盘事件
十二、计算属性
十三、监视属性watch
十四、绑定Class样式
十五、绑定style样式
十六、条件渲染
十七、列表渲染
十八、Key的原理以及diff算法
十九、列表过滤、排序
二十、过滤器
二十一、Vue如何监视data配置项
二十二、Vue如何监测数组
二十三、Vue.$set()方法
二十四、收集表单的value数据
二十五、v-text和v-html指令
二十六、v-cloak、v-once、v-pre指令
二十七、自定义指令directives
二十八、生命周期
一、入门案例
Vue可以看作是一个数据管理大师,其目的通常是将其管理的数据展示到HTML页面上,或者收集HTML页面上的数据进行储存在自身或者传递给服务器。
Vue管理的数据大致可以分为:Ⅰ,对象数据:对象里面包含一些基本数据类型和嵌套着一些其他对象。Ⅱ,数组;Ⅲ,函数。我们需要了解Vue是如何管理对象、数组、函数这3种数据类型,包括:如何响应式这些数据、如何代理这些数据等。
下面的代码是一个入门案例。首先引入vue.js的完整版,引入vue.js之后,在window上就多出了一个Vue构造函数对象,使用new Vue()就可以创建一个Vue实例对象vm,在创建vm的时候,需要把希望给vm管理的数据通过data配置项交给vm,把希望给vm管理的函数通过methods配置项交给vm。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>初识vue</title>
<!-- 引入Vue -->
<script src="../js/vue.js"></script>
</head>
<body>
<!-- 准备好一个容器 -->
<div id="root">
<h1>Hello!{{name}}!</h1>
</div>
<script>
Vue.config.productionTip = false // 阻止vue在启动时生成生产提示
new Vue({
el:'#root', //el用于指定当前Vue实例为哪个容器服务,值通常为css选择器字符串
data:{ //data用于存储数据,数据共el所指定的容器去使用
name:'么么哒'
}
})
</script>
</body>
</html>
vm实例在得到数据之后,首先会进行一系列的加工,然后把数据放到HTML页面上。所有需要告诉vm把数据放到哪个DOM节点。使用el配置项或者vm.$mount()就可以指定vm管理哪一个DOM节点(通常是div节点)。可以将一个字符串选择器或者一个DOM节点原始js对象交给el和$mount,例如:el:"#root"或者el:document.getElementByID("#root")。
在得到管理的模板之后,就可以将数据展示到模板当中了。使用模板语法中的插值语法将管理的数据name使用{{name}}展示到模板页面当中。{{}}里面必须是js表达式,这样才能够使用eval()函数进行运行识别,同时,{{}}里面可以直接使用vm管理的数据。
二、模板语法
1.插值语法:使用双大括号{{}}可以在模板中插入变量或表达式的值。例如:{{name}}。
2.指令语法:Vue 提供了一些指令,例如v-for、v-bind等。
他们的共同点就是:{{js表达式}}或者v-bind="js表达式",作用就是将vm管理的数据呈现到HTML模板页面上。js表达式除了原始js表达式之外,还可以写vm自身管理的数据。
什么是js表达式?答案:执行之后能够得到具体值的语句。试想一下vm需要将数据展示到HTML页面当中,如果你不通过函数return数据、或者表达式计算出数据来,vm拿什么放到页面上面呢?当然undefined不会被vm放入到页面当中。
三、数据绑定
数据绑定的本质就是:1,vm将数据放到页面,称为单向绑定;2,一些表单节点可以采集数据,vm可以对他们进行收集,也可以把他们放到页面,称为双向绑定。思考:是否需要接触到DOM节点对象本身才能拿到这些数据呢?
因为存在表单类DOM节点,Vue不得已设计了两个指令来处理数据的绑定,对于表单类节点,例如<input>、<select>、<textarea>等,使用v-model指令进行数据的双向绑定;对于其他节点如<div>使用v-bind进行绑定。
v-bind可以简写为冒号:。v-model默认收集表单节点的value属性,那么可以使用v-model直接代替value,也就是v-model:value可以简写为v-model。
四、el和data的两种写法
el:一种是使用el配置项;另一种是使用vm.$mount()方法。
data:一种是直接传入一个对象;另一种是使用函数函数return一个对象。使用函数返回是为了避免多个组件或者多个vm实例共用同一个data数据配置项。原因:函数返回这种形式每次返回的data都是船新版本的对象;而直接传入data由于引用传递的原因导致大家都在操作同一个data配置对象。
五、MVVM模型
Vue就是典型的MVVM架构,M表示数据模型,也就是对象{}、数组[]等原始js数据组织形式;V就是view视图,也就是HTML页面;VM就是视图模型,也就是vm实例或者vc组件实例,负责数据管理和呈现。
六、Object.defineproperty方法
Object.defineproperty方法是Vue中实现数据代理和数据劫持的核心方法。但是不幸的是,这个Object.defineproperty只能对对象的属性进行操作(传入的第一个参数是对象),而对象包装的属性有3种:基本数据类型、对象、数组对象。
Object.defineproperty 的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,Object.defineproperty可以接收三个参数,Object.defineproperty(obj, prop, desc)。obj : 第一个参数就是要在哪个对象身上添加或者修改属性;prop : 第二个参数就是添加或修改的属性名;desc : 配置项,一般是一个对象。
value:给定的初始值;
writable:默认为控制属性是否可以被修改,默认false;
configurable:控制属性是否可以被删除,默认false;
enumerable: 控制属性是否可以枚举,true的话简单的说就是可以遍历获取该值,默认false。
注意:当使用了getter或setter方法,不允许使用writable和value这两个属性(如果使用,会直接报错滴)。原因:都可以使用getter或setter方法了,难道还能把value固定或者writable变为false不成?
get 是获取值的时候的方法,获取值的时候会被调用,不设置时默认返回undefined。set 是设置值的时候的方法,设置值的时候会被调用。get或set不是必须成对出现,任写其一就可以。
七、Vue中响应式原理
Vue响应式的核心就是:数据被修改了、被读取了,Vue能够知道数据被修改或读取了。那只能依靠 Object.defineproperty这个API的set和get方法。而Object.defineproperty是针对对象属性的,拿到一个对象,不管这个对象嵌套了多深,Vue都是进行遍历将每一个对象的属性套上set和get方法,只有这样Vue才知道数据有没有被修改或者读取。
所以Vue无法直接处理数组对象,因为只有数组的地址变了,Vue才知道他所管理的这个数组被改变了,如果只是修改数组里面的值,Vue就无法进行监测,也就是数组里面的数据变化Vue是不知道的,所以Vue只能劫持重载数组对象的push、shift等方法才能知道数组里面的数据是否被修改。原因就是数组也是一个引用对象,只要地址不变,Vue就判断你没变,即使数组里面的数据已经变化了。什么是数组?内存里面连续的一段储存空间就是数组。
另外,后面的数据(不是通过data配置对象给的),就只能通过vm.$set()方法把数据变成响应式。而且这种方式存在效率问题,不管是线程执行时间还是内存消耗方面。
八、数据代理
什么是数据代理?数据代理就是使用A对象对B对象的属性进行操作。在Vue中,对象A就是vm,对象B就是data配置项(vm上的_data对象)、computed上面的属性、methods中的方法。还有其他组件传递过来的props属性等。
Vue为什么使用vm去代理vm._data?原因就是为了方便在模板语法当中或者vm实例当中可以直接使用vm._data上的数据。本质:Vue中的数据代理本质上就是两次Object.defineproperty封装,第一次是数据劫持,第二次就是数据代理,都是使用的 Object.defineproperty,但是目的不一样。
九、methods配置项
methods是Vue的options的配置项对象中的一个属性,其本身也是一个对象,但是里面放的是函数。这些函数最终也会被vm进行代理,从而可以通过vm去访问这些方法函数。这些方法一般写成普通函数,以确保函数执行时的this指向vm或者vc。
而this的作用就是能够从指定对象身上拿到一些数据。
<script>
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
name:'JOJO'
},
methods:{
showInfo1(event){
console.log(event)
},
showInfo2(evnet,num){
console.log(event,num)
}
}
})
</script>
十、Vue中的事件处理
浏览器中的事件一般是通过浏览器的交互线程进行识别。浏览器的事件类型大致分为:鼠标事件、键盘事件(keydown、keyup、keypress)、表单事件(input、change、select)、网页状态事件(load、domcontextload、readystatechange等)、焦点/剪贴板事件(focus、blur等)、窗口事件(scroll、resize)、touch事件等。每种事件类型的event对象都是不同的,但是都存在currentTarget 、Target 、type三个属性以及preventDefault() 、stopPropagation()两个方法。
此外,事件还分为HTML事件、DOM0级事件、DOM2级事件。Vue中使用的DOM0级事件,也就给节点对应事件的回调从null改成Vue管理的函数或者其他地方的函数。因此,vm实例销毁之后,这些事件仍然存在。
当一个事件比如click由鼠标发出,浏览器的事件识别线程识别到了鼠标按下,然后生成一个事件对象event,这个event对象取决于事件类型,不同的事件类型的event对象是不一样的。就开始事件流,先从document节点开始捕获到具体节点,然后冒泡执行,冒泡阶段所有被捕获的节点的事件都会被触发。
在Vue当中,使用v-on:事件名指令或者@事件名称符号给节点绑定事件,例如下面的代码。事件回调一般写在vm中的methods配置项当中。建议使用function关键字写成普通函数,确保函数当中的this为vm或者vc。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>事件的基本用法</title>
<script src="../js/vue.js"></script>
</head>
<body>
<div id="root">
<h2>hello,{{name}}</h2>
<button v-on:click="showInfo1">点我提示信息1</button>
<button @click="showInfo2($event,66)">点我提示信息2</button>
</div>
<script>
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
name:'JOJO'
},
methods:{
showInfo1(event){
console.log(event)
},
showInfo2(evnet,num){
console.log(event,num)
}
}
})
</script>
</body>
</html>
事件的参数传递问题:首先@click="demo"和@click="demo($event)"等价。其次可以进行参数传递,例如@click="demo(vm.a,123)"。最后,为了防止$event对象丢失,可以使用@click="demo(vm.a,123,$event,345)"进行占位置,在回调函数的地方的第三个位置接收$event即可。
事件修饰符:@click.self.once.passive.capture.stop.prevent="demo",一共6个。self表示event的target是当前节点也就是event.currenttaarget==target时触发;once只触发一次;passive表示事件的默认行为(例如交互渲染行为)立即执行,无需等待回调执行完毕;capture表示在捕获阶段执行回调;stop表示阻止事件冒泡;prevent表示阻止事件默认行为。
十一、Vue中的键盘事件
键盘事件使用很多很频繁。键盘事件包括三种:keyup:所有按键都能触发(例如A、B、C和win键);keydown:同keyup;keypress:按下有值的键时触发,即按下 Ctrl、Alt、Shift、Meta 这样无值的键,这个事件不会触发。对于有值的键,按下时先触发keydown事件,再触发这个keyup事件。
一些常见的键盘事件:@keydown.left、keydown.enter、keydown.13、keydown.esc等。
组合键盘事件:比如CTRL+C,我们CV代码时,小拇指会摁住CTRL不放,然后不断地摁C和V,因此组合键盘事件配合CTRL的keypress和其他键的keyup使用,比如:@ctrl.s,@ctrl.v。
自定义键码:Vue.config.keyCodes.自定义键名 = 键码
,可以自定义按键别名,例如Vue.config.keyCodes.A = 666.
十二、计算属性
计算属性是Vue中的一个配置项,用于实时的加工已有的数据。一是要满足实时性、二只能处理已经存在的数据(指数据当前已经在内存里面存在了)。实时性是因为加工得到的数据可能会放置到HTML页面当中,也可能是其他数据链会依赖当前加工的数据,所以需要速度快,不要异步获取数据(否则卡住主线程)。
<div id="app">
姓:<input type="text" v-model:value="firstName"><br/>
名:<input type="text" v-model:value="lastName"><br/>
全名:<span>{{fullName}}</span>
</div>
<script>
const vm = new Vue({
el:'#app',
data:{
firstName:'wang',
lastName:'y'
},
computed:{
fullName:{
get(){
return this.firstName+'-'+this.lastName;
},
set(value){
const arr = value.split('-');
this.firstName=arr[0];
this.lastName=arr[1];
}
}
// 如果没有set函数,可以采用如下的简写形式
// fullName(){
// return this.firstName+'-'+this.lastName;
// }
}
});
</script>
计算属性的用法:简写形式时,把函数名当作属性使用。
计算属性的底层:依靠的是Object.defineProperty()方法提供的getter和setter。也就是使用vm代理这些方法(显示的提供Object.defineProperty()给程序员使用)。另外,能够自动识别所使用数据的变化,但使用的数据发生变化,自动更新属性值。
计算属性能够使用的数据:vm自身的数据,字面量数据等,最好处理的数据都是响应式数据。其次,计算属性是响应式数据
计算属性的思考:计算属性在语法层面是一个函数或者一个对象。而且他是一个被Vue管理的响应式函数,当其数据链中的响应式数据变化,计算属性computed就会自动重新计算,说明computed计算属在底层被放置在其所使用数据链路中数据的set()中。
计算属性的调用时机:1、get函数在初次读取时会执行一次;2、当计算属性所依赖的响应式数据发生改变的时候,get()函数会被再次调用。
计算属性的优势:与methods实现相比,计算属性采用了缓存机制(脏值检测),在获取计算属性的属性值时,若所依赖的属性没有发生变化,那么就不会执行get()函数,而是将缓存的计算属性值返回。与methods每次都会执行计算逻辑相比,效率更高。
底层原理2:计算属性被Vue中的Watcher类进行封装,当监听的属性发生变化,也就是数据链上的数据发生变化时,自动更新整条数据链。
计算属性最重要的就是其返回值。
十三、监视属性watch
监视属性是一个配置项,是一个Vue进行管理的函数,其主要作用就是监视数据有没有发生变化,那么就可以肯定监视的数据必须是响应式数据,也就是被Vue通过set()劫持过的数据(某个数据可以被挂载在多个对象上实现多重响应式,也就是多个监视点)。
计算属性也可以被监视,因为计算属性也是被响应式的数据。此外,data配置项中的多级嵌套对象的属性也可以进行监视,比如:"school.name"=function(){}。总的来说,只要是vm身上的响应式数据都可以进行监视且成功地运行响应式(就是换一个对象进行监视,多个对象监视同一个数据)。
其目的是当数据发生变化,就去执行相应的handler函数。然后通过handler函数去执行一些其他逻辑(比如修改vm的数据、给vm添加新的响应式数据等),同时刷新数据链和执行相关的渲染函数。
<div id="app">
<h3>今天天气很{{info}}</h3>
<button @click="changeWeather()">切换天气</button>
</div>
<script>
const vm = new Vue({
el:'#app',
data:{
isHot:true,
},
computed:{
info:{
get(){
return this.isHot?'炎热':'凉爽'
}
}
},
methods: {
changeWeather(){
this.isHot = !this.isHot
}
},
// 监视写法一
// watch:{
// // 完整写法
// // isHot:{
// // // immediate:true, // 该参数设置为true,可以在初始化的时候让handler调用一下
// // // deep:true, // 深度监视
// // // 属性变化的回调函数
// // handler(newValue,oldValue){
// // console.log('属性isHot被修改了',newValue,oldValue);
// // },
// // },
// // 简写形式
// // 当我们监视的配置属性只有handler()函数,不需要设置deep和immediate时,可以采用简写形式
// isHot(newValue,oldValue){
// console.log('属性isHot被修改了',newValue,oldValue);
// }
// }
});
// // 正常写法
// vm.$watch('isHot',{
// // immediate:true, // 该参数设置为true,可以在初始化的时候让handler调用一下
// // deep:true, // 深度监视
// // 属性变化的回调函数
// handler(newValue,oldValue){
// console.log('属性isHot被修改了',newValue,oldValue);
// }
// });
// 简写形式
vm.$watch('isHot',function(newValue,oldValue){
console.log('属性isHot被修改了',newValue,oldValue);
})
</script>
监视属性的两种写法:
第一种:通过watch配置项,这种分为简写和详细写,详细写就是写成一个对象形式,对象里面写3个配置属性deep、immediate、和handler回调。如果不要求deep和immediate,那么就可以简写为函数形式:ishot(isHot(newValue,oldValue)){}。
第二种:通过vm.$watch(),如上面的代码,写法和watch配置项一模一样。
对于deep默认为false,当监视的数据是一个多层嵌套的对象时,可以开启deep为true,这样不管嵌套多深的对象发生数据变化都可以被监视到。 对于immediate,当被监视属性的回调函数注册到vm身上时,立即执行一次。
回调函数的分类:什么时候写成普通函数?什么时候写成箭头函数?答案就是handler只能写成普通函数,才能确保this指向vm或者vc。
如果回调函数不需要将一些任务函数放到异步队列当中,写成普通函数,这样this就是vm或者vc,此时的watch和computed效果上等价。如果回调函数handler里面使用了异步任务的回调函数,需要扔到消息队列当中,在将来的某个时刻拿到数据,那么handler里面的回调需要写成箭头函数。
原因就是:vm只是管理的handler函数,当运行handler时,才会生成handler方法体里面的箭头函数,此时这些箭头函数是在vm对象的方法栈生成的,所以handler方法体里面的回调函数的this就是vm或者vc。
注意:function关键字生成的函数对象在生成的时候就指定this了,也就是handler函数里面的function定义的函数在handler方法栈运行生成且被直接调用 的时候,默认this为window,这就是为什么handler()函数里面的内嵌回调函数不能写成普通函数的原因。
十四、绑定Class样式
DOM节点的class属性本质上是一个字符串,多个类使用空格进行隔开,空格两边就是类名,所以Vue绑定class样式的本质就是通过已有的数据和条件动态修改class属性的字符串。
Vue绑定class的前提之一就是,对应的类选择器样式已经存在。
情况一:添加一个class属性,使用:class="vm身上的字符串类名"就可以,最终Vue整合所有的class之后放入到DOM节点当中。这里的js表达式可以是字面量字符串也可以是vm身上的数据。
<div id="root">
<!-- 绑定class样式--字符串写法,适用于:样式的类名不确定,需要动态指定 -->
<div class="basic" :class="mood" @click="changeMood">{{name}}</div> <br/><br/>
<!-- 绑定class样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定 -->
<div class="basic" :class="classArr">{{name}}</div> <br/><br/>
<!-- 绑定class样式--对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用 -->
<div class="basic" :class="classObj">{{name}}</div> <br/><br/>
<!-- 绑定style样式--对象写法 -->
<div class="basic" :style="styleObj">{{name}}</div> <br/><br/>
<!-- 绑定style样式--数组写法 -->
<div class="basic" :style="styleArr">{{name}}</div>
</div>
情况二:绑定多个类样式。这种情况下可以使用数组[]将所有的类名放在一起,然后Vue自动将里面的字符串组合在一起放在DOM节点当中。也就是:class="[vm.a,vm.b,vm.c]",v-bind遇到class时自动合成字符串。
情况三::class="obj",根据obj对象里面的值来确定哪些类名被整合到最终的字符串当中,这个obj中的key一般是vm身上的属性,value一般是false和true。
<script type="text/javascript">
Vue.config.productionTip = false
const vm = new Vue({
el:'#root',
data:{
name:'尚硅谷',
mood:'normal',
classArr:['atguigu1','atguigu2','atguigu3'],
classObj:{
atguigu1:false,
atguigu2:false,
},
})
</script>
十五、绑定style样式
style样式和class样式如出一辙,最终都是变成一个字符串。但是style样式的字符串是以key:value的形式表现的。所以style通常绑定一个vm所管理的js对象。例如:style="{fontSize:xx}",其中xx是vm身上的数据。而且Key必须是小驼峰写法,默认是css的样式名称。
第二种情形就是使用数组,例如style="[styleobj1,styleobj2]",其中styleobj1,styleobj2是vm身上的数据对象。
<script type="text/javascript">
Vue.config.productionTip = false
const vm = new Vue({
el:'#root',
data:{
name:'尚硅谷',
styleObj:{
fontSize: '40px',
color:'red',
},
styleObj2:{
backgroundColor:'orange'
},
styleArr:[
{
fontSize: '40px',
color:'blue',
},
{
backgroundColor:'gray'
}
]
},
})
</script>
十六、条件渲染
条件渲染的目的就是从多个DOM节点中选择一个进行展示,这种就是v-if,v-if-else,v-else等指令。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>条件渲染</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<div id="root">
<h2>当前的n值是:{{n}}</h2>
<button @click="n++">点我n+1</button>
<h2 v-show="true">Hello,{{name}}!</h2>
<div v-if="n === 1">Angular</div>
<div v-else-if="n === 2">React</div>
<div v-else>Vue</div>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false
const vm = new Vue({
el:'#root',
data:{
name:'jojo',
n:0
}
})
</script>
</html>
另一种就是决定某一个DOM节点是否进行展示,这就是v-show指令。这种方式的DOM的display变成None就被隐藏。
v-if写法:v-if="表达式" v-else-if="表达式" v-else。适用于:切换频率较低的场景。特点:不展示的DOM元素直接被移除。注意:v-if可以和v-else-if、v-else一起使用,但要求结构不能被打断
v-show写法:v-show="表达式"。适用于:切换频率较高的场景。特点:不展示的DOM元素未被移除,仅仅是使用样式隐藏掉。
十七、列表渲染
列表渲染的本质是将Vue管理的容器数据通过循环展示到HTML页面上。Vue管理的容器类数据包括:字符串、数组、嵌套对象等。列表渲染循环生成的同一个类型的DOM节点,然后放入到模板当中。此时就需要使用:key这个预占的属性给Vue进行区分唯一的DOM节点。
一般语法为:(value,id) in 容器。然后value和id就可以在模板语法当中使用。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>基本列表</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<div id="root">
<h2>人员列表(遍历数组)</h2>
<ul>
<li v-for="(p,index) in persons" :key="index">
{{p.name}}-{{p.age}}
</li>
</ul>
<h2>汽车信息(遍历对象)</h2>
<ul>
<li v-for="(value,k) in car" :key="k">
{{k}}-{{value}}
</li>
</ul>
<h2>遍历字符串</h2>
<ul>
<li v-for="(char,index) in str" :key="index">
{{char}}-{{index}}
</li>
</ul>
<h2>遍历指定次数</h2>
<ul>
<li v-for="(number,index) in 5" :key="index">
{{index}}-{{number}}
</li>
</ul>
</div>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
persons:[
{id:'001',name:'张三',age:18},
{id:'002',name:'李四',age:19},
{id:'003',name:'王五',age:20}
],
car:{
name:'奥迪A8',
price:'70万',
color:'黑色'
},
str:'hello'
}
})
</script>
</body>
</html>
v-for
指令:用于展示列表数据
- 语法:
<li v-for="(item, index) in xxx" :key="yyy">
,其中key可以是index,也可以是遍历对象的唯一标识 - 可遍历:数组、对象、字符串(用的少)、指定次数(用的少)
十八、Key的原理以及diff算法
Vue的vm实例一共管理着3个DOM对象,分别是他的模板对应的真实DOM、真实DOM的备份、以及重新生成的新的DOM,后两个可以看成是虚拟DOM,只要后面两个DOM的DOM树的叶子或者节点不一样,那么就会通过渲染函数重新更新真实DOM。
使用列表渲染时,会生成新的节点,这些节点不是一开始在模板当中写的,属于后来的DOM节点,所以需要特殊管理。
列表渲染新生成的DOM节点一般是一棵子DOM树,使用key可以记录这棵子树的根节点,diff差分算法就是根据从这个子树的根节点开始进行对比,判断DOM节点是否发生变化。
如果发现key不存在,直接创建新的子树。
如果key存在,则会对子树遍历。但是对于一些真实DOM中存在的内容,比如input框里面的内容,Vue是不知道的,因为Vue只有一开始真实DOM的备份,后面这个真实DOM发生了什么变化Vue是不知道的。这就会导致真实DOM的一部分被替换掉,而另外一部分还是残留的老的真实DOM。
如果遍历容器时,使用index作为key,则会出现部分真实DOM残留的问题。实际开发中,一般使用独一无二的index作为key。
十九、列表过滤、排序
列表过滤和排序的本质:对容器里面的数据进行筛选,然后将筛选得到的新容器的数据放到页面上。那么进行筛选,必然会产生新的临时容器。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>列表过滤</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<div id="root">
<h2>人员列表</h2>
<input type="text" placeholder="请输入名字" v-model="keyWord">
<ul>
<li v-for="(p,index) of filPersons" :key="index">
{{p.name}}-{{p.age}}-{{p.sex}}
</li>
</ul>
</div>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
keyWord:'',
persons:[
{id:'001',name:'马冬梅',age:19,sex:'女'},
{id:'002',name:'周冬雨',age:20,sex:'女'},
{id:'003',name:'周杰伦',age:21,sex:'男'},
{id:'004',name:'温兆伦',age:22,sex:'男'}
]
},
computed:{
filPersons(){
return this.persons.filter((p)=>{
return p.name.indexOf(this.keyWord) !== -1
})
}
}
})
</script>
</body>
</html>
二十、过滤器
过滤器本质上就是数据二次加工的一些方法函数。其目的就是在数据真正放入HTML页面之前进行最后一步处理:例如格式化字符串等。
这些方法和函数必须注册到vm身上才能使用,有两种方法:第一种使用fliters局部过滤器。第二种:使用Vue.fliter这个API注册为全局过滤器,所有vm和vc实例都可以使用。
Vue.fliter这个API注册过滤器函数的第一个参数是过滤名称,第二个参数是对于的回调,这个回调函数可以接受一个参数,就是管道符|前面的vm身上的数据或者字面量。
对于局部过滤器,直接把函数名当作过滤器名称。
过滤器的一般用法:在模板语法当作数据后面使用管道符|过滤器名称。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>过滤器</title>
<script type="text/javascript" src="../js/vue.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/dayjs/1.10.6/dayjs.min.js"></script>
</head>
<body>
<div id="root">
<h2>时间</h2>
<h3>当前时间戳:{{time}}</h3>
<h3>转换后时间:{{time | timeFormater()}}</h3>
<h3>转换后时间:{{time | timeFormater('YYYY-MM-DD HH:mm:ss')}}</h3>
<h3>截取年月日:{{time | timeFormater() | mySlice}}</h3>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false
//全局过滤器
Vue.filter('mySlice',function(value){
return value.slice(0,11)
})
new Vue({
el:'#root',
data:{
time:1626750147900,
},
//局部过滤器
filters:{
timeFormater(value, str="YYYY年MM月DD日 HH:mm:ss"){
return dayjs(value).format(str)
}
}
})
</script>
</html>
二十一、Vue如何监视data配置项
新建一个Vue实例对象时,我们传入了一个data配置对象,然后它变成了vm身上的_data对象。那么Vue到底对这个data做了什么手脚呢?原始的data对象又变成了什么样子?
data对象里面无非状态三种东西:基本数据类型、嵌套对象、数组。首先Vue使用Observe对象不断地遍历data对象,如果他的属性是基本数据类型,那么set/get走一波。如果是数组则进行重写数组的各种方法。如果还是对象则继续重复上面的过程。
data被Observe进行包装之后,就得到_data对象,然后原始的data又会指向_data对象。然后vm对_data对象进行数据代理。
这样就会存在一个问题,后添加到vm身上的数据没有被set/get进行包装,不具有响应式特性。那么可以使用Vue.$set()或者vm.$set()将后面的数据变成响应式。变成响应式的过程必然要找到一个可以依赖的对象(object.defineproperty的第一个参数所决定的),那么这个可以依赖的对象不能是vm也不能是_data,更不能是window。他必须是data里面的一个对象,且这个对象必然是响应式的。
二十二、Vue如何监测数组
一般来说,数组作为的是data对象里面的一个属性,使用Object.defineproperty将数组变成响应式的时候,只要这个数组的堆内存地址不发生改变,就无法监测数组里面的内容是否发生改变。那么Vue是如何监测数组里面内容的改变的呢?
包装数据永不过时。对于数组对象,我们重写数组的:pop、push、shift、unshift、slice、sort、reverse方法。当vm拿到原生js数组之后,给这个数组穿上马甲,以push为例,调用push时会去找穿了马甲的数组的push方法,这个push一方面往数组里面加东西,另一方面去执行对于的渲染函数,由此实现数组的响应式。
对于通过data配置项对象传进去的数组如此,但是对于后面添加的新的数组也是如此。
二十三、Vue.$set()方法
Vue.set和vm.$set都可以把后面来的数据变成响应式数据,这些后来的数据包括:数组、基本数据类型、对象、多层级嵌套对象。
这个API的原型是Vue.$set(target,key,value)。
其中target必须是一个对象,这是由Object.defineproperty所决定的且这个target必须是vm身上的一个响应式对象。但不能是vm身上的根响应式对象,实际上可以但是Vue做了if的判断禁止这么做。
第二个key必须是可哈希的,在js语法当种就只能是数值和字符串。
第三个value就是key所对应的值。
二十四、收集表单的value数据
对于一个form表单,里面存在input、select、textarea等表单元素,对于这些DOM元素,他们是特殊的。对input来说他的value就是输入框里面的值,而且这个值还随着input的type不同而不同,例如number、password、checkbox等,那么他们的value的个数和类型也会不同。
对于Vue的v-model来说,他主要搜集表单元素的value值,由于表单的value值个数和类型是不确定,所以vue针对不同的表单类型进行了处理,使用不同的数据类型去接收不同个数和类型的表单的value值。总结:每个表单元素都得初始化value属性,对于多个value值,vm使用数组进行接收。
若:<input type="text"/>,则v-model收集的是value值,用户输入的内容就是value值
若:<input type="radio"/>,则v-model收集的是value值,且要给标签配置value属性
若:<input type="checkbox"/>:1.没有配置value属性,那么收集的是checked属性(勾选 or 未勾选,是布尔值);2.配置了value属性:v-model的初始值是非数组,那么收集的就是checked(勾选 or 未勾选,是布尔值)v-model的初始值是数组,那么收集的就是value组成的数组
v-model的三个修饰符:lazy:失去焦点后再收集数据;number:输入字符串转为有效的数字;trim:输入首尾空格过滤。
二十五、v-text和v-html指令
用于向其所在的DOM节点中插入文本,也就是给DOM.innertext进行赋值。赋值数据来源就是vm所管理的数据或者网络请求得到的数据。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>v-text指令</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<body>
<div id="root">
<div>你好,{{name}}</div>
<div v-text="name"></div>
<div v-text="str"></div>
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
name:'JOJO',
str:'<h3>你好啊!</h3>'
}
})
</script>
</html>
而v-html指令则是在当前节点append一个新的节点,会把数据当作元素HTML处理。
二十六、v-cloak、v-once、v-pre指令
由于网络卡顿的原因,当HTML页面已经渲染到页面窗口,但是js脚本还没有运行,页面就会展示还没有经过Vue处理的页面。这个时候可以给vm绑定模板添加v-cloak指令,然后css样式将含有v-cloak属性的元素变为隐藏,当模板被vue接管并mount之后,删掉v-cloak属性,模板就呈现到页面之上了。
v-once指令:当前DOM只经过vue管理一次,Vue就会放弃对这个节点的管理。
v-pre指令:对于不包含vue模板语法的dom元素,给他们加上v-pre,那么vue就会在编译模板时忽略这些HTML元素。
v-cloak
指令(没有值):
- 本质是一个特殊属性,Vue实例创建完毕并接管容器后,会删掉
v-cloak
属性 - 使用css配合
v-cloak
可以解决网速慢时页面展示出{{xxx}}
的问题
v-once
指令:
-
v-once
所在节点在初次动态渲染后,就视为静态内容了 -
以后数据的改变不会引起
v-once
所在结构的更新,可以用于优化性能
v-pre
指令:
- 跳过其所在节点的编译过程。
- 可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译
二十七、自定义指令directives
通过上面对Vue指令的学习,我们可以知道Vue指令不仅能修改DOM元素innerText内容还能修改innerHTML内容,由此可知这些指令铁定拿到了对应的DOM元素。
另外,例如v-bind指令v-bind:class="arr",指令还能根据""里面的表达式得到具体的数据。那么指令是如何运行这里面的js表达式的?
最后,指令可以直接触摸到DOM节点,那么指令和生命周期又有什么关系呢?对于v-on绑定事件指令,所有vm对象都可以使用,那么能不能自定义只针对与某个vm自己使用的指令?答案是可以的。
本质上来说:指令就是将vm身上的数据通过操作DOM节点,把vm身上的数据放到HTML页面的过程。而Vue是存在虚拟DOM的,所以这些指令一直操作的都是虚拟DOM,操作完毕之后再真正地展现到HTML页面当中。
注册局部指令可以使当前的vm或者vc实例对象使用自定义指令;注册全局指令在Vue构造函数身上,所有的vm和vc实例都可以使用这些指令。
局部指令注册方法如下:
new Vue({
directives:{指令名:配置对象}
})
new Vue({
directives:{指令名:回调函数}
})
全局指令注册方法如下:所有的vm和vc都可以使用。
Vue.directive(指令名,配置对象)
Vue.directive(指令名,回调函数)
有两种声明:一种是函数式声明,一种是配置对象声明。函数式声明的调用时机:1.指令与元素成功绑定时(一上来) 2.指令所在的模板被重新解析时,也就是函数式声明只会执行bind回调函数和update函数。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>自定义指令</title>
<script type="text/javascript" src="../js/vue.js"></script>
</head>
<!--
需求1:定义一个v-big指令,和v-text功能类似,但会把绑定的数值放大10倍。
需求2:定义一个v-fbind指令,和v-bind功能类似,但可以让其所绑定的input元素默认获取焦点。
-->
<body>
<div id="root">
<h2>当前的n值是:<span v-text="n"></span> </h2>
<h2>放大10倍后的n值是:<span v-big="n"></span> </h2>
<button @click="n++">点我n+1</button>
<hr/>
<input type="text" v-fbind:value="n">
</div>
</body>
<script type="text/javascript">
Vue.config.productionTip = false
new Vue({
el:'#root',
data:{
n:1
},
directives:{
//big函数何时会被调用?1.指令与元素成功绑定时(一上来) 2.指令所在的模板被重新解析时
big(element,binding){
console.log('big',this) //注意此处的this是window
element.innerText = binding.value * 10
},
fbind:{
//指令与元素成功绑定时(一上来)
bind(element,binding){
element.value = binding.value
},
//指令所在元素被插入页面时
inserted(element,binding){
element.focus()
},
//指令所在的模板被重新解析时
update(element,binding){
element.value = binding.value
}
}
}
})
</script>
</html>
在新的DOM被放到HTML页面上之前,所有指令对虚拟DOM的操作都应该被完成。在操作DOM之前,首先要确定指令与虚拟DOM当中某个节点的联系,也就是bind,所以当指令与模板中某个DOM节点绑定时就会执行bind回调。
由于Vue是数据驱动的,数据变化模板也会变化,所以当vm管理的数据发生变化时,对应的指令就会执行去修改虚拟DOM然后进行diff然后放到HTML页面。所以只要vm的数据发生改变,该模板上的所有指令都会被重新执行,然后得到最终的虚拟DOM。
细说element和binding:
这两个在回调函数当中的对象,element表示该指令所绑定的模板中的DOM节点对象,binding对象是一个上下文信息对象,他记录的有js表达式字符串和指令名称等,然后使用eval函数执行得到value值,这个value值在binding身上,即需要展现到HTML页面上的数据都在binding身上。
对于有些页面效果或者默认行为,需要DOM节点在HTML页面当中已经呈现了才能进行展示,比如focus得等到那个节点在页面上了之后才能有获取焦点这个效果。所有指令还提供了update回调,他的作用就是当页面已经呈现到HTML页面之后执行的回调,相当于挂载DOM之后再次挂在一次,保证虚拟DOM被挂载到页面,再去执行focus获取焦点。
细说:bind、inserted、update,首先bind、inserted、一上来就会被执行,也就是第一次生成虚拟DOM和将虚拟DOM放入HTML页面,然后后面就不会执行了,第二次、第三次把虚拟DOM放入HTML页面时,只会执行update回调函数。所以功能上inserted、update是一致的,只不过inserted只执行一次。而且这三个回调也称为钩子hook,但是与生命周期的hook有显著差异。
一些注意点:1,模板语法当中,指令使用-分割,例如:v-big-number。但是注册指令的时候,多个单词的指令使用可哈希字符串,"big-number",且不加v。2,hook回调函数种的this皆是window而不是vm或者vc,因为数据已经通过binding传递过来了,而且element天然属于window的document,而且还能防止通过this穿透恶意修改vm中的数据。
二十八、生命周期
生命周期就是vm实例从生成、渲染模板、销毁的整个过程,然后在一些特殊时刻调用对应的钩子函数。这些函数主要用于修改vm或者vc身上的数据,添加或者删除自定义事件等操作。由于这个原因,生命周期当中的钩子函数中的this都是vm或者vc自身,这样才能拿到vm或者vc身上的数据和其他的一些东西。
下面的图是Vue各个生命周期所需要做的事:
思考vm的整个流程:
1、一切都从new Vue开始,短期目标就是成为一个数据管理大师,首先就是初始化所有的生命周期钩子函数(类似多少岁上学、多少岁结婚)这就是beforecreated;然后就是先把data穿上马甲变成自己的,再把methods穿上马甲变成自己的,这就是created,此时空有数据,但是没地方放数据。
2、等待有缘人,等待一个模板能够被自己接管,或者被vc组件接受。
3、成功入职。得到模板之后,把自身的数据通过各种指令生成一个虚拟DOM。等待被展示,这就是beforedmounted。
4、得到展示到HTML页面的机会,将虚拟DOM成功放入HTML中,mounted成功。
5、牛马一生阶段1。自己的数据不断被修改、不断地增加数据、删减数据,然后被各种指令进行修改,不断地得到新的虚拟DOM,但是还得老板同意之后才能再次放入HTML页面,这就是beforeupdated。
6、牛马一生阶段2。老板同意修改之后的虚拟DOM上线,updated成功。然后在beforeupdated和updated不断地循环重复。
7、被老板辞掉了。准备跟老板吵架,打算删库跑路,beforedestoryed。
8、干不过老板,destoryed完毕。
总结来说:Vue生命周期钩子是一些函数,他们在特殊的时刻点被自动的调用。由此可知,当vm被创建之处,必须先初始化各种钩子函数(也就是需要生成函数对象)。这些函数的作用就是在特殊时刻点执行一些动作,例如:CRUD数据、添加组件、自定义事件、删除自定义事件等。
另外,这些生命周钩子一般当作的是Vue的配置项,与data、methods同层次。