MVVM
(Model-View-ViewModel)也称(Model-View-Binder)
Model数据层,数据和业务逻辑都在Model层中定义;
View视图层,在前端开发中通常就是DOM层,主要用于给用户展示各种信息;
ViewModel视图模型层,是View和Model沟通的桥梁
响应式原理:
Model和ViewModel之间有着双向数据绑定的联系。一方面ViewModel它实现了Data Binding,可以将Model的改变实时的反应到View中。另一方面它实现了DOM Listener,可以将因用户交互操作而改变的数据也会在Model中同步。
严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了$refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。
1、Mastache语法 { {}}
作用:用于显示model中的内容,实现数据的双向绑定
2、v-once语法
作用:DOM内容只被渲染一次,即使model数据改变,view数据也不会变,适用于内容不允许徐更改的时候
3、v-html语法
作用:当model中的数据为 HTML 代码时 能被DOM正确解析,不会显示HTML代码
4、v-text语法
作用:和{ {}}一样 后边跟字符串
5、v-pre语法
作用:该元素及其子元素不会被编译,用于显示原本的Mastache内容
6、v-cloak
v-cloak: 这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none }
一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕
v-bind绑定属性动态更新HTML元素上的属性
v-on用于监听DOM事件,语法糖@
v-html更新元素的 innerHTML,内容按普通 HTML 插入 - 不会作为 Vue 模板进行编译,scoped 的样式不会应用在 v-html 内部。原理:会先移除节点下的所有节点,调用html方法,通过addProp添加innerHTML属性,归根结底还是设置innerHTML为v-html的值。注意防止xss攻击
v-text更新元素的text Content
v-model
v-if、v-else、v-else-if
v-show始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS property display,v-show 不支持 元素,也不支持 v-else
v-for:当和 v-if 一起使用时,v-for 的优先级比 v-if 更高
v-once DOM内容只被渲染一次,即使model数据改变,view数据也不会变,适用于内容不允许徐更改的时候
v-clock这个指令保持在元素上直到关联实例结束编译,解决初始化慢导致页面闪动的最佳实践;
v-pre跳过这个元素和它的子元素的编译过程,用于显示原本的Mastache内容
指令给当前元素绑定事件,语法糖 @
v-on:click=“increment”
@click="decrement"语法糖
绑定从服务器请求过来的动态数据,某些属性。 比如a的href属性,img的src属性,css, style。语法糖 :
原生事件绑定是通过 addEventListener 绑定给真实元素的,组件事件绑定是通过 Vue 自定义的$on 实现的。如果要在组件上使用原生事件,需要加.native 修饰符,这样就相当于在父组件中把子组件当做普通 html 标签,然后加上原生事件。
o n 、 on、 on、emit 是基于发布订阅模式的,维护一个事件中心,on 的时候将事件按名称存在事件中心里,称之为订阅者,然后 emit 将对应的事件进行发布,去执行事件中心里的对应的监听器
v-model 只是语法糖而已,它的背后本质上是包含两个操作:
-1.v-bind绑定一个value属性
-2.v-on指令给当前元素绑定input事件
作用:
实现了表单内容和model中数据的双向绑定。
双向绑定的原理:
利用v-bind绑定vue实例里面的data数据,当data数据改变,会将内容实时的渲染到DOM,输入框的内容发生改变,即model改变view;
利用v-on绑定事件,当监听到输入框数据改变的时候,会通过比如@change @input等去触发事件修改实例数据中 的值,即view改变model;
v-model 在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
text 和 textarea 元素使用 value property 和 input 事件;
checkbox 和 radio 使用 checked property 和 change 事件;
select 字段将 value 作为 prop 并将 change 作为事件。
事件修饰符:
.stop 阻止事件继续传播
.prevent 阻止标签默认行为
.capture 使用事件捕获模式,即元素自身触发的事件先在此处处理,然后才交由内部元素进行处理
.self 只当在 event.target 是当前元素自身时触发处理函数
.once 事件将只会触发一次
.passive 告诉浏览器你不想阻止事件的默认行为
v-model修饰符:
.lazy 通过这个修饰符,转变为在 change 事件再同步
.number 自动将用户的输入值转化为数值类型
.trim 自动过滤用户输入的首尾空格
键盘事件的修饰符:
.enter
.tab
.delete (捕获“删除”和“退格”键)
.esc
.space
.up
.down
.left
.right
系统修饰符:
.ctrl
.alt
.shift
.meta
鼠标案件修饰符:
.left
.right
.middle
遍历数组
< h1 v-for="(index,value) in movies">{
{
value}}{
{
index}}</>
注释:遍历数组的时候 可以放两个参数 , index可选可不选。
遍历对象
< h1 v-for="(index,key,value) in movies">{
{
index}}{
{
key}}{
{
value}}</>
注释:遍历对象的时候可以放三个参数
// 变更方法:变更会调用这些方法的原始数组
push()
pop()
unshift()
shift()
splice()
sort()
reverse()
注意: 通过索引值修改数组中的元素
// 不是响应式的
this.letters[0] = 'bbbbbb';
// 是响应式的
this.letters.splice(0, 1, 'bbbbbb')
// Vue 不能检测数组和对象的变,可以通过set(要修改的对象, 索引值, 修改后的值)
Vue.set(this.letters, 0, 'bbbbbb')
加key是为了给元素添加唯一标识,因为vue是虚拟dom,用diff算法对节点进行一一比对,要修改哪个元素,这个元素一定要有一个唯一标识,比如修改了原数组,没有给li上加key,那么在进行运算的时候,就重新将整体渲染一遍,但是如果有key,那么它就会按照key找到修改内容的那个li元素,改掉它自己,不需要对其他元素进行修改
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,diff 操作可以更准确、更快速
更准确:因为带 key 就不是就地复用了,在 sameNode 函数a.key === b.key对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快
vue 中 key 值的作用可以分为两种情况来考虑:
第一种情况
是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的元素,那么这个元素就会被复用。
如果是相同的 input 元素,那么切换前后用户的输入不会被清除掉,这样是不符合需求的。因此可以通过使用 key 来唯一的标识一个元素,这个情况下,使用 key 的元素不会被复用。这个时候 key 的作用是用来标识一个独立的元素。
第二种情况
是 v-for 中使用 key。用 v-for 更新已渲染过的元素列表时,它默认使用“就地复用”的策略。如果数据项的顺序发生了改变,比如修改了原数组,没有给li上加key,那么在进行运算的时候,就重新将整体渲染一遍,但是如果有key,那么它就会按照key找到修改内容的那个li元素,改掉它自己,不需要对其他元素进行修改。Vue 不会移动 DOM 元素来匹配数据项的顺序,而是简单复用此处的每个元素。因此通过为每个列表项提供一个 key 值,来以便 Vue 跟踪元素的身份,从而高效的实现复用。这个时候 key 的作用是为了高效的更新渲染虚拟 DOM。
把C变为F,把D变为C,E更新成D,最后再插入E,很没有效率?
因此添加key唯一标识后,Diff算法就可以正确的识别此节点,找到正确的位置区插入新的节点。
组件中使用v-for必须添加唯一标识key
简述:
使用index 索引值作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2…这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。
节点翻转的场景:key的顺序没变,传入的值完全变了
**分析:**导致 Vue 会复用错误的旧子节点,做很多额外的工作。
本来按照最合理的逻辑来说,旧的第一个vnode 是应该直接完全复用 新的第三个vnode的,因为它们本来就应该是同一个vnode,自然所有的属性都是相同的。但是在进行子节点的 diff 过程中,会在 旧首节点和新首节点用sameNode对比。 这一步命中逻辑,因为现在新旧两次首部节点 的 key 都是 0了,然后把旧的节点中的第一个 vnode 和 新的节点中的第一个 vnode 进行 patchVnode 操作。在进行 patchVnode 的时候,会去检查 props 有没有变更,如果有的话,会通过 _props.num = 3 这样的逻辑去更新这个响应式的值,触发 dep.notify,触发子组件视图的重新渲染等一套很重的逻辑。然后,还会额外的触发以下几个钩子,假设我们的组件上定义了一些dom的属性或者类名、样式、指令,那么都会被全量的更新。
updateAttrs
updateClass
updateDOMListeners
updateDOMProps
updateStyle
updateDirectives
而这些所有重量级的操作(虚拟dom发明的其中一个目的不就是为了减少真实dom的操作么?),都可以通过直接复用 第三个vnode 来避免,是因为我们偷懒写了 index 作为 key,而导致所有的优化失效了。
v-show 指令的功能与 v-if 指令相似。
相同点:v-show和v-if都能控制元素的显示和隐藏。
区别:
v-if 指令会根据表达式重建或销毁元素或组件以及它们所绑定的事件。v-if是动态的向DOM树内添加或者删除DOM元素
v-show是通过设置css中的display设置为none,控制隐藏
因为 v-if 指令开销较大,所以更适合条件不经常改变的场景。而 v-show 指令适合条件频繁切换的场景
具体解释:使用v-show(无论true或者false初始都会进行渲染,此后通过css来控制显示隐藏,因此切换开销比较小,初始开销较大),如果不需要频繁切换某节点时,使用v-if(因为懒加载,初始为false时,不会渲染,但是因为它是通过添加和删除dom元素来控制显示和隐藏的,因此初始渲染开销较小,切换开销比较大)
v-for优先于v-if被解析,如果同时出现,每次渲染都会先执行循环再判断条件,无论如何,循环都不可避免,浪费了性能。
要避免出现这种情况,可以将 v-if 置于外层元素 (或 ),在这一层进行v-if判断,然后在内部进行v-for循环。如果条件出现在循环内部,可通过计算属性提前过滤掉那些不需要显示的项。
<ul v-if="shouldShowUsers">
<li v-for="user in users" :key="user.id">
{
{
user.name }}
</li>
</ul>
或者放到计算属性里:
computed: {
activeUsers: function () {
return this.users.filter(function (user) {
return user.isActive
})
}
}
<ul>
<li v-for="user in activeUsers :key="user.id">
{
{
user.name }}
</li>
</ul>
可以将同一函数定义为一个 method 或者一个计算属性。对于最终的结果,两种方式是相同的
不同点:
computed: 计算属性是基于它们的依赖进行缓存的,只有在它的相关依赖发生改变时才会重新求值;
method 定义函数,需要手动调用;
computed 是计算属性:
计算属性不在 data 中,它是基于data 或 props 中的数据通过计算得到的一个新值,这个新值根据已知值的变化而变化;computed中的方法只有依赖的数据发生改变的时候才会执行,且计算的结果会缓存起来;它可以设置 getter 和 setter,如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。
watch:
主要用来监听某些特定数据的变化,从而进行某些具体的业务逻辑操作,可以看作是 computed 和 methods 的结合体;可以监听的数据来源:data,props,computed内的数据;watch支持异步,不支持缓存,监听的数据改变,直接会触发相应的操作,监听函数有两个参数,第一个参数是最新的值,第二个参数是输入之前的值,顺序一定是新值,旧值。
运用场景:
当需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时都要重新计算。
当需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用 watch 选项允许执行异步操作 ( 访问一个 API ),限制执行该操作的频率,并在得到最终结果前,设置中间状态。这些都是计算属性无法做到的
组件化思想:
如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展。
但如果,我们将一个完整的页面分成很多个组件,每个组件都用于实现页面的一个功能块,而每一个组件又可以进行细分,那么之后整个页面的管理和维护就变得非常容易了。
组件使用的三个步骤:
组件中的 data 写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果。
第一种 props 常用和$emit
组件封装用的多
prop 父传子,子组件通过props来定义变量用来接收父组件传递过来的信息,然后对变量进行操作
我们用的最多方式,可以通过Prop向子组件传递数据。
用一个形象的比喻来说,父子组件之间的数据传递相当于自上而下的下水管子,只能从上往下流,不能逆流。这也正是Vue的设计理念之单向数据流。而Prop正是管道和管道之间的一个衔接口,这样(水)数据才能向下流.
$emit
子传父
子组件向父组件传递数据,通过this.$emit
方法提交一个事件addParentAge,父组件通过语法糖v-on(即简写为“@”)来监听子组件提交的事件addParentAge
<my-child :deliverParentAge="parentAge" @addParentAge="handleAddParentAge"></my-child>
第二种 $parent,$children,refs
父访问子:refs,children
首先,我们通过ref给某一个子组件绑定一个特定的ID。
其次,通过this.$refs.ID
就可以访问到该组件了
this.$refs是一个对象,持有当前组件中注册过 ref特性的所有 DOM 元素和子组件实例
ref放在不同的位置,有不同的效果:
节点时,可以通过this.$refs.p得到节点的属性或者方法,如<p ref="p">hello</p>
组件时,可以通过this.$refs.child得到相应的组件实例,从而得到组件上面的属性和方法,如
<child-component ref="child"></child-component>
子访问父$parent
,不推荐使用
$parent获取当前组件的父组件, 注意:
$root来访问根组件的实例;
在根组件#app上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent得到的是undefined,而在最底层的子组件拿$children是个空数组;
$children 的值是无序数组,而$parent是个对象
第三种 依赖注入
依赖注入:父组件中通过 provide 来提供变量,然后在子组件中通过 inject 来注入变量。(官方不推荐在实际业务中使用,但是写组件库时很常用)provide:Object | () => Object
inject:Array<string> | {
[key: string]: string | Symbol | Object }
父组件:
provide(){
return {
/* 将自己暴露给子孙组件 ,这里声明的名称要于子组件引进的名称保持一致 */
father:this
}
},
子组件:
inject:['father'],
第四种:vuex
vuex 状态管理如果业务逻辑复杂,很多组件之间需要同时处理一些公共的数据,这个时候可以使用 vuex,将这一些公共的数据抽离出来,将它作为一个全局的变量来管理,然后其他组件就可以对这个公共数据进行读写操作,这样达到了解耦的目的。
第五种:事件总线
EventBus 核心思想是事件的绑定和触发,这一点和vue中 this.$emit 和 this.$on
一样,这个也是整个EventBus核心思想。
事件总线就相当于一个桥梁,组件通过它来通信,兄弟组件数据传递 event.$emit('名称',方法)发布,event.$on('名称',方法)接受
有两种方式:
一、新建一个bus.js文件,初始化一个空的Vue实例,作为中央总线,然后再组件引用时调用这个bus.js文件
import Vue from 'vue';
const EventBus = new Vue();
export default EventBus;
二、是main.js全局定义,将bus挂载到vue.prototype上
以vue实例作为eventBus中心,除了我们可以用$on,$emit之外,我们还可以用vue下的data,watch等方法
而且我们建立多个多个vue,作为不同模块的数据通信桥梁,相比上边那个EventBus方法,new Vue这种方法更高效,更适合vue项目场景。
在main.js中
import Vue from 'vue'
Vue.prototype.$bus = new Vue()
在GoodListItem组件中:
methods:{
imageLoad(){
this.$bus.$emit('itemImageLoad')
},
在Home.vue中:mouted页面加载完成
mounted() {
// 1.图片加载完成后的事件监听,非父子组件的通信
this.$bus.$on("itemImageLoad", () => {
refresh();
});
},
第六种:$attrs 和$listeners
祖孙
跨代通信A->B->C。Vue 2.4 开始提供了$attrs 和$listeners
来解决这个问题。
爷组件传递给孙组件的逻辑流程就是:
通过爷组件首先传递给父组件,当然父组件不在props中接收,那么爷组件传递给父组件的数据就会存放到父组件的$attrs
对象中里面了,然后,再通过v-bind="$attrs"
,再把这个$attr
传递给孙组件,在孙组件中使用props
就能接收到$attrs
中的数据了,这样就实现了,祖孙之间的数据传递。
使用v-on="$listeners
"可以实现孙组件的数据传递到爷组件中去,逻辑的话,也是用在中间的桥梁父组件上面去,我的理解就是$listeners
可以将孙组件emit的方法通知到爷组件。
APP.vue
//此处监听了两个事件,可以在B组件或者C组件中直接触发
Child1.vue
props: {
{pChild1}}
$attrs: {
{$attrs}}
C组件:Child2.vue
props: {
{pChild2}}
$attrs: {
{$attrs}}
数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
不应该在一个子组件内部改变 prop。如果这样做了,Vue 会在浏览器的控制台中发出警告。
有两种变更prop的情形:
1.这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用:可以再 data 里面定义一个变量 并用 prop 的值初始化它 之后用$emit 通知父组件去修改。也就是第一种父子组件通信
2.prop 以一种原始的值传入且需要进行转换:使用计算属性:
props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
http://caibaojian.com/vue-slot.html
Vue 实现了一套内容分发的 API,将slot标签作为承载分发内容的出口。**插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。**插槽内可以包含任何模板代码,包括 HTML,甚至其它的组件。
如果 child 的 template 中没有包含一个 slot>元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃。
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
插槽实现原理:
当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot
中,
默认插槽为vm.$slot.default
,
具名插槽为vm.$slot.xxx
,xxx 为插槽名,
当组件执行渲染函数时候,遇到slot标签,使用$slot
中的内容进行替换,
此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
slot又分三类,默认插槽,具名插槽和作用域插槽。
默认插槽:又名匿名插槽,当slot没有指定name属性值的时候一个默认显示插槽,隐含名default,一个组件内只有有一个匿名插槽。
具名插槽:带有具体名字的插槽,也就是带有name属性的slot,一个组件可以出现多个具名插槽。
作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现一个默认插槽:
父组件
<todo-list>
<template v-slot:default>
任意内容
<p>我是匿名插槽 </p>
</template>
</todo-list>
//v-slot:default写上感觉和具名写法比较统一,容易理解,也可以不用写
子组件
<slot>我是默认值</slot>
##显示##
// 任意内容
// 我是匿名插槽
例如:对于一个带有如下模板的 组件:
```javascript
使用具名插槽:子
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
在向具名插槽提供内容的时候,我们可以在一个 元素上使用 v-slot 指令,并以 v-slot 的参数的形式提供其名称:
<base-layout>
<template v-slot:header>
<h1>Here might be a page title</h1>
</template>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
<template v-slot:footer>
<p>Here's some contact info</p>
</template>
</base-layout>
现在 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 中的内容都会被视为默认插槽的内容。
作用域插槽
总结:首先在子组件的slot上绑定数据
然后在父组件通过的方式将这个值赋值给 一个变量
最后通过{ {slotProps.user.age}}
的方式获取数据
子组件:
<template>
<h1>
<slot :user="user">
{
{
user.name}}
</slot>
</h1>
</template>
<script>
export default {
name: 'HelloWorld',
data(){
return {
user:{
name:'wanglu',
age:10
}
}
}
}
父组件:
<template>
<div id="app">
<hello-world>
<template v-slot:default="slotProps">