本文内容来自网络,整理出来分享于你们~~
参考至小册 剖析 Vue.js 内部运行机制
先来一张整体图,而后咱们对每一部分详细分析。javascript
new Vue
以后回调用一个_init
方法去初始化,会初始化data
、props
、methods
、声明周期
、watch
、computed
、事件
等。其中最重要的一点就是经过Object.defineProperty
来设置getter
和setter
,从而实现数据的【双向绑定响应式】和【依赖收集】。html
初始化完以后会调用一个$mount
来实现挂载。若是是运行时编译,则不存在render function
,存在template
的状况须要从新编译。(我理解的意思:最开始咱们须要去解析编译template
中的内容,实现依赖收集和数据绑定,最后会生成一个render function
.可是若是是运行时候好比响应数据的更改等,则不会在生成render function
,而是经过diff
算法直接操做虚拟DOM
,实现正式结点的更新)。vue
Vue是一款MVVM的框架,数据模型仅仅是普通的js对象,可是在操做这些对象的时候确能够及时的响应视图的变化。依赖的就是Vue的【响应式系统】。java
面试题 —— 你了解Vue的MVVM吗?
MVVM包含三层:模型层Model,视图层View,控制层ViewModel.node
联系:
总之:DOM事件监听和数据绑定是MVVM的关键。DOM Listeners
监听页面全部View层DOM元素的变化,当发生变化,Model
层的数据随之变化;Data Bindings
监听Model
层的数据,当数据发生变化,View
层的DOM元素随之变化。
首先咱们来介绍一下 Object.defineProperty,Vue.js就是基于它实现「响应式系统」的。react
Object.defineProperty(obj, prop, descriptor);
descriptor的一些属性,简单介绍几个属性:git
var o = {}; // 建立一个新对象 // 【1】在对象中添加一个属性与数据描述符的示例 Object.defineProperty(o, "a", { value : 37, writable : true, enumerable : true, configurable : true }); // 对象o拥有了属性a,值为37 // 【2】在对象中添加一个属性与存取描述符的示例 var bValue; Object.defineProperty(o, "b", { get : function(){ return bValue; }, set : function(newValue){ bValue = newValue; }, enumerable : true, configurable : true }); o.b = 38; // 对象o拥有了属性b,值为38 // o.b的值如今老是与bValue相同,除非从新定义o.b
这是响应式系统最为重要的一步。利用的即是咱们上面提到的Object.defineProperty
。面试
实现一个简单的对数据的getter和setter监听:正则表达式
// 遍历数据对象的每一个属性,这里咱们只作了一层,实际上会使用递归去处理深层次的数据 // 这里为了咱们的方便理解,就假设是单层对象 function observer (value) { if (!value || (typeof value !== 'object')) { return; } Object.keys(value).forEach((key) => { defineReactive(value, key, value[key]); }); } // 函数模拟视图更新 function cb (val) { console.log("视图更新啦~", val); } // 数据对象成员的响应式监听 function defineReactive (obj, key, val) { Object.defineProperty(obj, key, { enumerable: true, // 可枚举 configurable: true, // 可配置 get: function reactiveGetter () { return val; // 当使用到咱们的这个属性的时候会触发get方法,这里用来依赖收集,咱们以后实现 }, set: function reactiveSetter (newVal) { // 监听数据的修改,模拟视图更新,其实这里的过程至关的复杂,diff是一个必通过程 if (newVal === val) return; val = newVal; cb(newVal); } }); } class Vue { constructor(options) { this._data = options.data; // 获取数据对象 observer(this._data); // 实现对数据中每一个元素的观察,即为每一个属性去设置get和set。 } } // 测试案例 let o = new Vue({ data: { test: "I am test." } }); o._data.test = "hello,test.";
上面咱们实现的是一个简单的响应式原理案例,咱们只是实现了对数据对象的观察。当咱们的数据使用和被修改的时候会调用咱们的自定义get和set方法。下面咱们去了解一下,数据【依赖收集】。算法
为何要进行依赖收集呢?
new Vue({ template: `<div> <span>{{text1}}</span> <span>{{text2}}</span> <div>`, data: { text1: 'text1', text2: 'text2', text3: 'text3' } });
上面例子中,text1,text2使用了一次,text3未使用。
若是咱们对某一个数据进行了修改,那么咱们应该知道的哪些地方使用了该数据,为了咱们视图的更新作好准备。
「依赖收集」会让
text1
这个数据知道“哦~有两个地方依赖个人数据,我变化的时候须要通知它们~”。
订阅者Dep
class Dep { constructor () { /* 用来存放Watcher对象的数组 */ this.subs = []; } /* 在subs中添加一个Watcher对象 */ addSub (sub) { this.subs.push(sub); } /* 通知全部Watcher对象更新视图 */ notify () { this.subs.forEach((sub) => { sub.update(); }) } }
订阅者对象含有两个方法,addSub用来收集watcher对象,notify用来通知watcher对象去更新视图。
观察者Watcher
class Watcher { constructor () { /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */ Dep.target = this; } /* 更新视图的方法 */ update () { console.log("视图更新啦~"); } } Dep.target = null;
观察者对象在实例化的时候就须要绑定它所属的Dep。同时还有一个update方法去更新视图。
依赖收集原理
function defineReactive (obj, key, val) { /* 一个Dep类对象 */ const dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { /* 将Dep.target(即当前的Watcher对象存入dep的subs中) */ dep.addSub(Dep.target); return val; }, set: function reactiveSetter (newVal) { if (newVal === val) return; /* 在set的时候触发dep的notify来通知全部的Watcher对象更新视图 */ dep.notify(); } }); } class Vue { constructor(options) { this._data = options.data; observer(this._data); /* 新建一个Watcher观察者对象,这时候Dep.target会指向这个Watcher对象*/ // 实例化一个观察者 new Watcher(); /* 在这里模拟render的过程,为了触发test属性的get函数 */ console.log('render~', this._data.test); // 触发get以后,会将上面刚实例化的watcher对象,添加到Dep对象中。 // 注:这里只实例化了一个watcher,其实watcher对象没有咱们上诉的那么简单,它记录的是当前引用的相关信息。为方便下次数据的更新时候,去更新视图 } }
当触发一个属性的get方法后,会执行咱们的依赖收集。首先实例化一个watcher对象,这个watcher对象有这个属性的更新视图的方法。而后经过Dep的addSub方法将该watcher对象添加到Dep订阅者中。
【依赖收集】的关键条件:(1)触发get方法 (2)新建一个watcher对象
总结: 到了这里咱们已经吧响应式系统学了,主要是get进行依赖收集,set中用过watcher观察者去更新视图。面试题 —— 你了解Vue的响应式系统原理吗?
Vue
采用的是数据劫持的方式,当你设置data
属性的值时候,vue
就会遍历data
属性,对每个属性经过Object.defineProperty
来设置getter
和setter
。当触发render function
渲染的时候,就会触发属性的getter
方法,同时触发getter
方法中的依赖收集,所谓的依赖收集就是将观察者Watcher
对象存放到当前闭包中的订阅者Dep
的 subs
中。造成以下所示的这样一个关系。
在修改对象的值的时候,会触发对应的setter
, setter
通知以前「依赖收集」获得的Dep
中的每个 Watcher
,告诉它们本身的值改变了,须要从新渲染视图。这时候这些 Watcher
就会开始调用 update
来更新视图,固然这中间还有一个patch
的过程以及使用队列来异步更新的策略。实质就是在数据变更时发布消息给订阅者,触发须要修改的watcher
中的notify
方法相应的监听回调.
compile编译能够分红 Html解析parse
、优化optimize
与 转换generate
三个阶段,最终须要获得render function
。
parse解析
<div :class="c" class="demo" v-if="isShow"> <span v-for="item in sz">{{item}}</span> </div>
对HTML进行字符串解析,从而获得class、style、指令等数据,造成AST。AST是一种抽象语法树。上面的例子解析完后是:
{ /* 标签属性的map,记录了标签上属性 */ 'attrsMap': { ':class': 'c', 'class': 'demo', 'v-if': 'isShow' }, /* 解析获得的:class */ 'classBinding': 'c', /* 标签属性v-if */ 'if': 'isShow', /* v-if的条件 */ 'ifConditions': [ { 'exp': 'isShow' } ], /* 标签属性class */ 'staticClass': 'demo', /* 标签的tag */ 'tag': 'div', /* 子标签数组 */ 'children': [ { 'attrsMap': { 'v-for': "item in sz" }, /* for循环的参数 */ 'alias': "item", /* for循环的对象 */ 'for': 'sz', /* for循环是否已经被处理的标记位 */ 'forProcessed': true, 'tag': 'span', 'children': [ { /* 表达式,_s是一个转字符串的函数 */ 'expression': '_s(item)', 'text': '{{item}}' } ] } ] }
optimize优化
optimize
主要做用就跟它的名字同样,用做「优化」
。
这个涉及到后面要讲 patch
的过程,由于patch的过程其实是将 VNode
节点进行一层一层的比对,而后将「差别」
更新到视图上。
那么一些静态节点
是不会根据数据变化而产生变化的,咱们就须要为静态的节点作上一些「标记」
,在 patch 的时候咱们就能够直接跳过这些被标记的节点的比对,从而达到「优化」的目的。
generate 转为 render function
generate
会将 AST
转化成 render funtion
字符串
render function 看起来就像下面:
with(this){ return (isShow) ? _c( 'div', { staticClass: "demo", class: c }, _l( (sz), function(item){ return _c('span',[_v(_s(item))]) } ) ) : _e() }
经历过这些过程之后,咱们已经把 template
顺利转成了 render function
了,以后 render function
就会转换为Virtual DOM。
虚拟DOM实质就是一个实打实的javascript对象。它是对真是DOM的一层映射。用对象属性来描述某个结点,以及它的子结点。因为虚拟DOM是javascript对象为基础,因此不依赖任何环境,因此具备跨平台的特性。也正式由于基于这一点,Vue具备跨平台的能力~~
咱们来看一个简单的虚拟DOM实例:
class VNode { constructor (tag, data, children, text, elm) { /*当前节点的标签名*/ this.tag = tag; /*当前节点的一些数据信息,好比props、attrs等数据*/ this.data = data; /*当前节点的子节点,是一个数组*/ this.children = children; /*当前节点的文本*/ this.text = text; /*当前虚拟节点对应的真实dom节点*/ this.elm = elm; } }
咱们有一段template代码:
<template> <span class="demo" v-show="isShow"> This is a span. </span> </template>
用js对象表示就是:
function render () { return new VNode( 'span', { /* 指令集合数组 */ directives: [ { /* v-show指令 */ rawName: 'v-show', expression: 'isShow', name: 'show', value: true } ], /* 静态class */ staticClass: 'demo' }, [ new VNode(undefined, undefined, undefined, 'This is a span.') ] ); }
转换成 VNode 之后的状况。
{ tag: 'span', data: { /* 指令集合数组 */ directives: [ { /* v-show指令 */ rawName: 'v-show', expression: 'isShow', name: 'show', value: true } ], /* 静态class */ staticClass: 'demo' }, text: undefined, children: [ /* 子节点是一个文本VNode节点 */ { tag: undefined, data: undefined, text: 'This is a span.', children: undefined } ] }
该种形式就可让咱们在不一样的平台实现很好的兼容了。
如何产生上诉对象呢,咱们须要经过一些自定义函数来实现,举一个简答例子:咱们建立一个空结点。
function createEmptyVNode () { const node = new VNode(); node.text = ''; return node; }
因此虚拟DOM能够经过调用一系列自定义的内部函数来实现,最终建立的就是 一个 VNode 实例对象。
再来看咱们的
render function
:
with(this){ return (isShow) ? _c( 'div', { staticClass: "demo", class: c }, _l( (sz), function(item){ return _c('span',[_v(_s(item))]) } ) ) : _e() }
上面这个 render function看到这里可能会纳闷了,这些_c,_l 究竟是什么?其实他们是 Vue.js 对一些函数的简写,好比说 _c对应的是createElement 这个函数。
到了这里你是否是懂了咱们以前所说的一句话了:咱们以前说render function
是用来生成虚拟DOM对象的。其实render function
就是一个复杂的函数调用。最后会经过层层调用来实现一个真正的js对象(虚拟对象)。
当咱们触发数据的更新时,会调用Dep
中的watcher
对象的update
方法来更新视图。最终是将新产生的 VNode
节点与老 VNode
进行一个 patch
的过程,比对得出「差别」,最终将这些「差别」更新到视图上。
patch过程其实就是利用diff算法进行一个差别比对的过程~
推荐两个diff算法执行过程的图解:
总结
无oldStartVnode则移动(参照round6) 对比头部,成功则更新并移动(参照round4) 对比尾部,成功则更新并移动(参照round1) 头尾对比,成功则更新并移动(参照round5) 尾头对比,成功则更新并移动(参照round2) 在oldKeyToIdx中根据newStartVnode的能够进行查找,成功则更新并移动(参照round3) (更新并移动:patchVnode更新对应vnode的elm,并移动指针)
咱们在整个过程当中使用了diff算法去逐一判断,经过patch去判断两个节点是否更新,而后做出相应的DOM操做。总之:diff算法告诉咱们如何去处理同层下的新旧VNode。
Diff过程当中,Vue会尽量的复用DOM,能不移动就不移动。
咱们知道在咱们修改data 以后其实就是一个“setter -> Dep -> Watcher -> patch -> 视图”
的过程。
假设咱们有以下这么一种状况。
<template> <div> <div>{{number}}</div> <div @click="handleClick">click</div> </div> </template> export default { data () { return { number: 0 }; }, methods: { handleClick () { for(let i = 0; i < 1000; i++) { this.number++; } } } }
当咱们按下 click 按钮的时候,number 会被循环增长1000次。
那么按照以前的理解,每次 number 被 +1 的时候,都会触发 number 的 setter 方法,从而根据上面的流程一直跑下来最后修改真实 DOM。那么在这个过程当中,DOM 会被更新 1000 次!这样子太消耗性能了,太可怕了~。
Vue作了相应的处理:
Vue.js在默认状况下,每次触发某个数据的 setter 方法后,对应的 Watcher 对象其实会被 push 进一个队列 queue 中,这些watcher对象都设置了标识,若是是对同一个数据的更新,watcher的标识是相同的,在下一个 tick 的时候将这个队列 queue 所有拿出来 run( Watcher 对象的一个方法,用来触发 patch 操做) 一遍。run的时候会进行筛选,而后根据标识判断重复的watcher对象只执行最后的。
let watch1 = new Watcher(); let watch2 = new Watcher(); watch1.update(); watch1.update(); watch2.update();
watch1只调用最后那次。
上面咱们对Vue的底层进行了必定的了解,虽然不是源码解析,可是咱们用一种简介明了的方式理解了底层的大体运行流程,下面咱们针对一些面试题目,来温习一下咱们Vue的知识点吧~~
(1)数据双向绑定
vue的数据响应式原理,技术上是采用Object.defineProperty和存储属性get、set来是实现的基于依赖收集的数据观测机制。核心是viewModel,保证数据和视图的一致性。
(2)组件
Vue中万物皆组件的理念使得它与虚拟DOM的契合度达到了很是好的地步。
.vue
组件的形式以下:
一、模板(template):模板声明了数据和最终展示给用户的DOM之间的映射关系。
二、初始数据(data):一个组件的初始数据状态。对于可复用的组件来讲,这一般是私有的状态。
三、接受的外部参数(props):组件之间经过参数来进行数据的传递和共享。
四、方法(methods):对数据的改动操做通常都在组件的方法内进行。
五、生命周期钩子函数(lifecycle hooks):一个组件会触发多个生命周期钩子函数,最新2.0版本对于生命周期函数名称改动很大。
六、私有资源(assets):Vue.js当中将用户自定义的指令、过滤器、组件等统称为资源。一个组件能够声明本身的私有资源。私有资源只有该组件和它的子组件能够调用。
等等。
渐进式指的是:没有多作职责之外的事。Vue只提供了组件系统和数据响应式系统两大核心。基于vue-cli的生态,则还须要vue-router、vuex等的第三方库的支持。咱们学习使用Vue,能够是须要什么功能,咱们就学什么功能。
Vue与React、Angular的不一样是,但它是渐进的:
<h1 v-if="awesome">Vue is awesome!</h1> <h1 v-else>Oh no 😢</h1>
template上也可使用if,来是想分组。
key来管理可复用的组件:
<template v-if="loginType === 'username'"> <label>Username</label> <input placeholder="Enter your username"> </template> <template v-else> <label>Email</label> <input placeholder="Enter your email address"> </template>
vue为了尽量的实现快速,减小没必要要的性能消耗,一般会复用已有的元素,这样作会使得vue变得很快。
上例子来讲,咱们经过v-if来条件渲染,那么label和input元素会被高度复用,咱们输入的内容在切换的过程当中是不会被清除掉的。所以为了能清空输入,咱们能够给input添加不同的key值,这样每次切换都会从新渲染input组件。
<template v-if="loginType === 'username'"> <label>Username</label> <input placeholder="Enter your username" key="username-input"> </template> <template v-else> <label>Email</label> <input placeholder="Enter your email address" key="email-input"> </template>
<h1 v-show="ok">Hello!</h1>
show不能适用于template。
<ul id="example-2"> <li v-for="(item, index) in items"> {{ index }} - {{ item.message }} </li> </ul>
还能够列表渲染对象。
<div v-for="(value, keyName, index) in object"> {{ index }}. {{ keyName }}: {{ value }} </div>
在遍历对象时,会按 Object.keys() 的结果遍历,可是不能保证它的结果在不一样的 JavaScript 引擎下都一致。
尽可能在使用for的时候哦使用key来标识,由于他能够帮咱们来跟踪每个结点,对复用和重排现有元素起着很是大的做用。由于它是 Vue 识别节点的一个通用机制,key 并不只与 v-for 特别关联。后面咱们将在指南中看到,它还具备其它用途。
<div v-for="item in items" v-bind:key="item.id"> <!-- 内容 --> </div>
<a v-bind:href="url">...</a>
<a v-on:click="doSomething">...</a>
2.6以后容许传入js表达式来动态修改传入的变量值。
<a v-bind:[attributeName]="url"> ... </a>
<a v-on:[eventName]="doSomething"> ... </a>
// link:'<a href="#" rel="external nofollow" >这是一个链接</a>' 若是想显示{{ }}标签,而不进行替换,使用v-pre便可跳过这个元素和它的子元素的编译过程 <span v-pre>{{ 这里的内容不会被编译 }}</span> <span v-html="link"></span>
<div id="app"> <p v-once>{{msg}}</p> //msg不会改变 <p>{{msg}}</p> // msg会不断变化 <p> <input type="text" v-model = "msg" name=""> </p> </div> <script type="text/javascript"> let vm = new Vue({ el : '#app', data : { msg : "hello" } }); </script>
相同
都能实现DOM的显示与隐藏。都是用于条件渲染。接收boolean来判断是否显示。
不一样
一、数组变异方法
vue对一些数组的方法作了包裹处理,咱们在调用这些方法的时候,仍然能够触发视图的更新。
push() pop() shift() unshift() splice() sort() reverse()
好比 this.items.push({ message: 'Baz' })。也会触发视图的更新。
固然上面的方法会修改原来的数组,还有一些方法返回的是新的数组,并不会修改原来的数组,好比:filter()、concat() 和 slice().可使用以下方法:
example1.items = example1.items.filter(function (item) { return item.message.match(/Foo/) })
二、vue存在不能监测数组和对象属性的异常
因为js的限制,Vue 不能检测如下数组的变更:
vm.items[indexOfItem] = newValue
vm.items.length = newLength
var vm = new Vue({ data: { items: ['a', 'b', 'c'] } }) vm.items[1] = 'x' // 不是响应性的 vm.items.length = 2 // 不是响应性的
解决办法:
// Array.prototype.splice vm.items.splice(indexOfItem, 1, newValue) // 先删除后添加
this.$set(this.items, index, newValue)
为了解决第二类问题,你可使用 splice:
vm.items.splice(newLength)
因为js的限制,其实对象添加属性也存在一些问题:
var vm = new Vue({ data: { a: 1 } }) // `vm.a` 如今是响应式的 vm.b = 2 // `vm.b` 不是响应式的
对于已经建立的实例,Vue 不容许动态添加根级别的响应式属性
对于不是根级别的,若是要添加新的属性:
var vm = new Vue({ data: { userProfile: { name: 'Anika' } } }) this.$set(this.userProfile, "age", 27);
若是要新添加多个值:
Object.assign( {}, this.userProfile, { age: 27, favoriteColor: 'Vue Green' })
v-on
经常使用一些修饰符来简单的实现咱们预期的效果。
事件修饰符
.stop
- 调用 event.stopPropagation()
,禁止事件冒泡。.prevent
- 调用 event.preventDefault()
,阻止事件默认行为。.capture
- 添加事件侦听器时使用 capture 模式。捕获事件模式.self
- 只当事件是从侦听器绑定的元素自己触发时才触发回调。.{keyCode | keyAlias}
- 只当事件是从特定键触发时才触发回调。.native
- 监听组件根元素的原生事件。.once
- 只触发一次回调。.left
- (2.2.0) 只当点击鼠标左键时触发。.right
- (2.2.0) 只当点击鼠标右键时触发。.middle
- (2.2.0) 只当点击鼠标中键时触发。.passive
- (2.3.0) 以 { passive: true } 模式添加侦听器
<!-- 阻止单击事件继续传播 --> <a v-on:click.stop="doThis"></a> <!-- 提交事件再也不重载页面,阻止默认事件行为 --> <form v-on:submit.prevent="onSubmit"></form> <!-- 修饰符能够串联 --> <a v-on:click.stop.prevent="doThat"></a> <!-- 只有修饰符 --> <form v-on:submit.prevent></form> <!-- 添加事件监听器时使用事件捕获模式 --> <!-- 即元素自身触发的事件先在此处理,而后才交由内部元素进行处理 --> <div v-on:click.capture="doThis">...</div> <!-- 只当在 event.target 是当前元素自身时触发处理函数 --> <!-- 即事件不是从内部元素触发的 --> <div v-on:click.self="doThat">...</div> <!-- 滚动事件的默认行为 (即滚动行为) 将会当即触发 --> <!-- 而不会等待 `onScroll` 完成 --> <!-- 这其中包含 `event.preventDefault()` 的状况 --> // 能够用于提高移动端的性能 <div v-on:scroll.passive="onScroll">...</div> <!-- 点击事件将只会触发一次 --> <a v-on:click.once="doThis"></a> <!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` --> <input v-on:keyup.enter="submit">
为了在必要的状况下支持旧浏览器,Vue 提供了绝大多数经常使用的按键码的别名:
按键修饰符
.enter .tab .delete (捕获“删除”和“退格”键) .esc .space .up .down .left .right
能够用以下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。
.ctrl .alt .shift .meta
注意:在 Mac 系统键盘上,meta 对应 command 键 (⌘)。在 Windows 系统键盘 meta 对应 Windows 徽标键 (⊞)。
<!-- Alt + C --> <input @keyup.alt.67="clear"> <!-- Ctrl + Click --> <div @click.ctrl="doSomething">Do something</div>
你可能注意到这种事件监听的方式违背了关注点分离 (separation of concern) 这个长期以来的优良传统。咱们一般都是js中获取DOM来绑定事件,然而这种方式确所有绑定在了HTML中。
咱们其实没必要担忧,我我的见解是:这种方式绑定在一个一个的元素上,而咱们Vue
是基于虚拟DOM的,也就是说template
中的内容,最终会编译为renderfunction
,转为虚拟DOM后最终由viewModel去管理。它不会致使任何维护上的难题。相反,这样还有一些好处:
on能够监听多个事件,可是不能是同一事件,会报错~~
<input type="text" :value="val" @input="inputHandler" @focus="focusHandler" @blur="blurHandler" /> // 下面这种会报错 <a href="javascript:;" @click="methodsOne" @click="methodsTwo"></a>
咱们不少人都会对key是否能加快diff速度而产生疑惑?
diff算法只比较同层的节点,若是节点类型不一样,直接干掉前面的节点,再建立并插入新的节点,不会再比较这个节点之后的子节点了。若是节点类型相同,则会从新设置该节点的属性,从而实现节点的更新。
好比咱们有以下状况:
咱们但愿能够在B和C之间加一个F,Diff算法默认执行起来是这样的:
在没有key的状况下,会原地复用,修改节点信息,最后还会新增一个节点。
即把C更新成F,D更新成C,E更新成D,最后再插入E,这样只有在当咱们的每一个结点较为简单的状况下才会快速。
若是是设置了key的状况:效果以下:
从以上来看,不带有key,而且使用简单的模板,基于这个前提下,能够更有效的复用节点,diff速度来看也是不带key更加快速的,由于带key在增删节点上有耗时。这就是vue文档所说的默认模式。可是这个并非key做用,而是没有key的状况下能够对节点就地复用,提升性能。
本人认为如下才是key提升diff算法速度的要点
diff算法用于比对新旧虚拟DOM对象,当咱们在比较头尾节点无果后,会根据新节点的key去对比旧节点数组中的key,从而找到相应旧节点。若是没找到就认为是一个新增节点。若是找到了就去比对,而后更新节点。(这里能够借助于map高效的定位性来加快diff的查找速度)
还有一种状况
vue中在使用相同标签名元素的过渡切换时,也会使用到key属性,其目的也是为了让vue能够区分它们,不然vue只会替换其内部属性而不会触发过渡效果。具体例子能够看上面第三题中的input替换问题/
//html部分 <a href="javascript:void(0);" data-id="12" @click="showEvent($event)">event</a> //js部分 showEvent(event){ //获取自定义data-id console.log(event.target.dataset.id) //阻止事件冒泡 event.stopPropagation(); //阻止默认 event.preventDefault() }
vue是基于数据驱动页面的,视图的更新是异步执行的。即咱们修改数据的当下,不会当即执行视图更新,而是会添加到一个异步的队列中,等当前事件循环中的数据变化所有完成以后,才会统一处理。$nextTick就是用来知道何时DOM更新完成的.
案例:
咱们先来看这样一个场景:有一个div,默认用 v-if 将它隐藏,点击一个按钮后,改变 v-if 的值,让它显示出来,同时拿到这个div的文本内容。若是v-if的值是 false,直接去获取div内容是获取不到的,由于此时div尚未被建立出来,那么应该在点击按钮后,改变v-if的值为 true,div才会被建立,此时再去获取,示例代码以下:
<div id="app"> <div id="div" v-if="showDiv">这是一段文本</div> <button @click="getText">获取div内容</button> </div> <script> var app = new Vue({ el : "#app", data:{ showDiv : false }, methods:{ getText:function(){ this.showDiv = true; // 原生事件绑定 var text = document.getElementById('div').innnerHTML; console.log(text); } } }) </script>
这段代码并不难理解,可是运行后在控制台会抛出一个错误:Cannot read property 'innnerHTML of null
,意思就是获取不到div元素。这里就涉及Vue一个重要的概念:异步更新队列。
Vue在观察到数据变化时并非直接更新DOM,而是开启一个队列,并缓冲在同一个事件循环中发生的因此数据改变。在缓冲时会去除重复数据,从而避免没必要要的计算和DOM操做。而后,在下一个事件循环tick中,Vue刷新队列并执行实际(已去重的)工做。因此若是你用一个for循环来动态改变数据100次,其实它只会应用最后一次改变,若是没有这种机制,DOM就要重绘100次,这当然是一个很大的开销。
简单的浏览器事件机制
同步代码执行 -> 查找异步队列,推入执行栈,执行callback1[事件循环1] -> 查找异步队列,推入执行栈,执行callback2[事件循环2]...
结合nextTick的由来,能够推出每一个事件循环中,nextTick触发的时机:
(1)同一事件循环中的代码执行完毕 -> (2)DOM 更新 -> (3)nextTick callback触发
当咱们触发数据变更的时候,此时处于1,此时DOM还没更新,vue实现了一个$nextTick语法糖,Vue会根据当前浏览器环境优先使用原生的Promise.then和MutationObserver,若是都不支持,就会采用setTimeout代替。这个方法其实就是将咱们的DOM操做代码放入了下一轮循环的异步队列中,下一轮循环中当将其掉入主线程咱们才能顺利的执行回调中的代码~
举例一个业务场景:select选择咱们要显示那种下面的控件。该控件依赖第三方库,须要获取DOM。
watch:{ type: function (val, oldVal) { if(val==2){ // 异步 Vue.nextTick(function () { //或者用 this.$nextTick $('#select').selectpicker(); }) } } }
理论上,咱们应该不用去主动操做DOM,由于Vue的核心思想就是数据驱动DOM,但在不少业务里,咱们避免不了会使用一些第三方库,好比 popper.js、swiper等,这些基于原生javascript的库都有建立和更新及销毁的完整生命周期,与Vue配合使用时,就要利用好$nextTick。
//为何data函数里面要return一个对象 <script> export default { data() { return { // 返回一个惟一的对象,不要和其余组件共用一个对象进行返回 menu: MENU.data, poi: POILIST.data } } } </script>
组件是能够被重用的,组件的其余方法能够被共用,可是数据对象确不能,由于咱们想要不一样调用处的组件有本身的数据对象,而不能被互相影响,所以返回对象,则不会每次的引用地址就是不一样的了。
当他们处于同一个元素上,for的优先级要高于if。
<li v-for="todo in todos" v-if="!todo.isComplete"> {{ todo }} </li>
上例子会根据条件进行渲染。
若是你的目的是有条件地跳过循环的执行,那么能够将 v-if 置于外层元素
<ul v-if="todos.length"> <li v-for="todo in todos"> {{ todo }} </li> </ul> <p v-else>No todos left!</p>
场景:当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以免反复重渲染致使的性能问题。例如咱们来展开说一说这个多标签界面:
这是一个来自官网的案例,咱们点击右侧以后会让左侧销毁,当点击左侧的时候会进行重建,这显然不是咱们想要的了。所以呢keep-alive即是关键了。
keep-alive:主要用于保留组件状态或避免从新渲染。
好比: 有一个列表页面和一个 详情页面,那么用户就会常常执行打开详情=>返回列表=>打开详情这样的话 列表 和 详情 都是一个频率很高的页面,那么就能够对列表组件使用<keep-alive></keep-alive>进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是从新渲染。
当组件在 <keep-alive>
内被切换,它的 activated
和 deactivated
这两个生命周期钩子函数将会被对应执行。
<!-- 基本 --> <keep-alive> <component :is="view"></component> </keep-alive> <!-- 多个条件判断的子组件 --> <keep-alive> <comp-a v-if="a > 1"></comp-a> <comp-b v-else></comp-b> </keep-alive> <!-- 和 `<transition>` 一块儿使用 --> <transition> <keep-alive> <component :is="view"></component> </keep-alive> </transition>
include 和 exclude 属性容许组件有条件地缓存。两者均可以用逗号分隔字符串、正则表达式或一个数组来表示:
<!-- 逗号分隔字符串 --> <keep-alive include="a,b"> <component :is="view"></component> </keep-alive> <!-- 正则表达式 (使用 `v-bind`) --> <keep-alive :include="/a|b/"> <component :is="view"></component> </keep-alive> <!-- 数组 (使用 `v-bind`) --> <keep-alive :include="['a', 'b']"> <component :is="view"></component> </keep-alive>
匹配它的局部注册名称 (父组件 components 选项的键值)。
使用<keep-alive>会将数据保留在内存中,若是要在每次进入页面的时候获取最新的数据,须要在activated阶段获取数据,承担原来created钩子中获取数据的任务。
被包含在 <keep-alive> 中建立的组件,会多出两个生命周期的钩子: activated
与 deactivated
注意:只有组件被 keep-alive 包裹时,这两个生命周期才会被调用,若是做为正常组件使用,是不会被调用,以及在 2.1.0 版本以后,使用 exclude 排除以后,就算被包裹在 keep-alive 中,这两个钩子依然不会被调用!另外在服务端渲染时此钩子也不会被调用的。
何时获取数据?
当引入keep-alive 的时候,页面第一次进入,钩子的触发顺序created-> mounted-> activated,退出时触发deactivated。当再次进入(前进或者后退)时,只触发activated。
咱们知道 keep-alive 以后页面模板第一次初始化解析变成HTML片断后,再次进入就不在从新解析而是读取内存中的数据,即,只有当数据变化时,才使用VirtualDOM进行diff更新。故,页面进入的数据获取应该在activated中也放一份。数据下载完毕手动操做DOM的部分也应该在activated中执行才会生效。
因此,应该activated中留一份数据获取的代码,或者不要created部分,直接将created中的代码转移到activated中。
在编写组件的时候,时刻考虑组件是否可复用是有好处的。一次性组件跟其余组件紧密耦合不要紧,可是可复用组件必定要定义一个清晰的公开接口。
Vue.js组件 API 来自 三部分:prop、事件、slot:
vue组件经历从建立到销毁的过程。其中要经历: 开始建立 —— 初始化 —— 模版编译 —— 挂载与渲染 —— 更新与渲染 —— 卸载销毁。
每个过程对对应了一个生命周期钩子函数,咱们能够在不一样阶段去书写咱们的代码
beforeCreate
: 此时尚未进行数据的观测和事件初始化created
: 已经完成了数据观测,事件初始化完成,属性和方法的运算。可是$el尚未beforeMount
: 相关的render函数首次被调用,去建立虚拟DOM,准备挂载到真实DOM上mounted
: 自此DOM已经彻底呈现了。能够访问$el。beforeUpdate
: 数据更新的时候调用,虚拟DOM会被更新。这里适合在更新以前访问现有的 DOM,好比手动移除已添加的事件监听器updated
: 数据更新完成。在beforeUpdate和updated之间进行的操做就是新旧虚拟DOM的patch过程和从新渲染的过程beforeDestroy
: 实例销毁以前调用。在这一步,实例仍然彻底可用。destroyed
: Vue 实例销毁后调用。调用后,Vue 实例指示的全部东西都会解绑定,全部的事件监听器会被移除,全部的子实例也会被销毁。销毁的过程实在beforeDestroy和destroyed之间进行的。errorCaptured
: 当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。activated
: keep-alive 组件激活时调用deactivated
: keep-alive 组停用时调用仔细分析上图,咱们来叙述如下Vue的生命周期过程吧~
new Vue 以后,会先作一些初始化工做,这时候会经过依赖收集对数据进行观测、事件绑定,属性计算,beforeCreated和created是这一操做的先后。created以后就已经完成了这些操做,可是$el好没有。
接下来,检查vue配置,即new Vue{}里面的el项是否存在,有就继续检查template项。没有则等到手动绑定调用vm.$mount()。对template进行编译处理,获得render function。render function是产生虚拟DOM的关键。产生虚拟DOM后会将其转为真实DOM挂载到根结点上。beforeMounted和mounted就是这一操做的以前和以后。mounted以后咱们就能够拿到真实的DOM了,这时候咱们能够进行一些DOM的计算和操做。
组件更新,会产生一个新的虚拟DOM,会经过diff算法进行patch差别比对操做,最终更新咱们的旧的虚拟DOM,从而更新咱们的真实DOM。beforeUpdated和updated是这一操做的先后阶段。
updated: function () { this.$nextTick(function () { // DOM更新完毕以后调用 // Code that will run only after the // entire view has been re-rendered }) }
使用v-cloak
指令,v-cloak
不须要表达式,它会在Vue
实例结束编译时从绑定的HTML元素上移除,常常和CSS的display:none
配合使用。
<div id="app" v-cloak> {{message}} </div> <script> var app = new Vue({ el:"#app", data:{ message:"这是一段文本" } }) </script>
这时虽然已经加了指令v-cloak,但其实并无起到任何做用,当网速较慢、Vue.js 文件还没加载完时,在页面上会显示{{message}}的字样,直到Vue建立实例、编译模版时,DOM才会被替换,因此这个过程屏幕是有闪动的。只要加一句CSS就能够解决这个问题了:(显示这一{{message}},其实就是在created到mounted之间出现的。)
[v-cloak]{ display:none; }
在通常状况下,v-cloak是一个解决初始化慢致使页面闪动的最佳实践,对于简单的项目很实用。能够隐藏未编译的 Mustache 标签直到实例准备完毕。
{{ message | capitalize }}
filters: { capitalize: function (value) { if (!value) return '' value = value.toString() return value.charAt(0).toUpperCase() + value.slice(1) } } //或者全局 Vue.filter('capitalize', function (value) { if (!value) return '' value = value.toString() return value.charAt(0).toUpperCase() + value.slice(1) })
// 两次过滤 {{ message | filterA | filterB }} // filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将做为参数传入到函数中。而后继续调用一样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。
传入自定义的参数
{{ message | filterA('arg1', arg2) }}
十个经常使用过滤器:
//去除空格 type 1-全部空格 2-先后空格 3-前空格 4-后空格 function trim(value, trim) { switch (trim) { case 1: return value.replace(/\s+/g, ""); case 2: return value.replace(/(^\s*)|(\s*$)/g, ""); case 3: return value.replace(/(^\s*)/g, ""); case 4: return value.replace(/(\s*$)/g, ""); default: return value; } } //任意格式日期处理 //使用格式: // {{ '2018-09-14 01:05' | formaDate(yyyy-MM-dd hh:mm:ss) }} // {{ '2018-09-14 01:05' | formaDate(yyyy-MM-dd) }} // {{ '2018-09-14 01:05' | formaDate(MM/dd) }} 等 function formaDate(value, fmt) { var date = new Date(value); var o = { "M+": date.getMonth() + 1, //月份 "d+": date.getDate(), //日 "h+": date.getHours(), //小时 "m+": date.getMinutes(), //分 "s+": date.getSeconds(), //秒 "w+": date.getDay(), //星期 "q+": Math.floor((date.getMonth() + 3) / 3), //季度 "S": date.getMilliseconds() //毫秒 }; if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length)); for (var k in o) { if(k === 'w+') { if(o[k] === 0) { fmt = fmt.replace('w', '周日'); }else if(o[k] === 1) { fmt = fmt.replace('w', '周一'); }else if(o[k] === 2) { fmt = fmt.replace('w', '周二'); }else if(o[k] === 3) { fmt = fmt.replace('w', '周三'); }else if(o[k] === 4) { fmt = fmt.replace('w', '周四'); }else if(o[k] === 5) { fmt = fmt.replace('w', '周五'); }else if(o[k] === 6) { fmt = fmt.replace('w', '周六'); } }else if (new RegExp("(" + k + ")").test(fmt)) { fmt = fmt.replace(RegExp.$1, (RegExp.$1.length == 1) ? (o[k]) : (("00" + o[k]).substr(("" + o[k]).length))); } } return fmt; } //字母大小写切换 /*type 1:首字母大写 2:首页母小写 3:大小写转换 4:所有大写 5:所有小写 * */ function changeCase(str, type) { function ToggleCase(str) { var itemText = "" str.split("").forEach( function (item) { if (/^([a-z]+)/.test(item)) { itemText += item.toUpperCase(); } else if (/^([A-Z]+)/.test(item)) { itemText += item.toLowerCase(); } else { itemText += item; } }); return itemText; } switch (type) { case 1: return str.replace(/\b\w+\b/g, function (word) { return word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase(); }); case 2: return str.replace(/\b\w+\b/g, function (word) { return word.substring(0, 1).toLowerCase() + word.substring(1).toUpperCase(); }); case 3: return ToggleCase(str); case 4: return str.toUpperCase(); case 5: return str.toLowerCase(); default: return str; } } //字符串循环复制,count->次数 function repeatStr(str, count) { var text = ''; for (var i = 0; i < count; i++) { text += str; } return text; } //字符串替换 function replaceAll(str, AFindText, ARepText) { raRegExp = new RegExp(AFindText, "g"); return str.replace(raRegExp, ARepText); } //字符替换*,隐藏手机号或者身份证号等 //replaceStr(字符串,字符格式, 替换方式,替换的字符(默认*)) //ecDo.replaceStr('18819322663',[3,5,3],0) //result:188*****663 //ecDo.replaceStr('asdasdasdaa',[3,5,3],1) //result:***asdas*** //ecDo.replaceStr('1asd88465asdwqe3',[5],0) //result:*****8465asdwqe3 //ecDo.replaceStr('1asd88465asdwqe3',[5],1,'+') //result:"1asd88465as+++++" function replaceStr(str, regArr, type, ARepText) { var regtext = '', Reg = null, replaceText = ARepText || '*'; //repeatStr是在上面定义过的(字符串循环复制),你们注意哦 if (regArr.length === 3 && type === 0) { regtext = '(\\w{' + regArr[0] + '})\\w{' + regArr[1] + '}(\\w{' + regArr[2] + '})' Reg = new RegExp(regtext); var replaceCount = this.repeatStr(replaceText, regArr[1]); return str.replace(Reg, '$1' + replaceCount + '$2') } else if (regArr.length === 3 && type === 1) { regtext = '\\w{' + regArr[0] + '}(\\w{' + regArr[1] + '})\\w{' + regArr[2] + '}' Reg = new RegExp(regtext); var replaceCount1 = this.repeatStr(replaceText, regArr[0]); var replaceCount2 = this.repeatStr(replaceText, regArr[2]); return str.replace(Reg, replaceCount1 + '$1' + replaceCount2) } else if (regArr.length === 1 && type === 0) { regtext = '(^\\w{' + regArr[0] + '})' Reg = new RegExp(regtext); var replaceCount = this.repeatStr(replaceText, regArr[0]); return str.replace(Reg, replaceCount) } else if (regArr.length === 1 && type === 1) { regtext = '(\\w{' + regArr[0] + '}$)' Reg = new RegExp(regtext); var replaceCount = this.repeatStr(replaceText, regArr[0]); return str.replace(Reg, replaceCount) } } //格式化处理字符串 //ecDo.formatText('1234asda567asd890') //result:"12,34a,sda,567,asd,890" //ecDo.formatText('1234asda567asd890',4,' ') //result:"1 234a sda5 67as d890" //ecDo.formatText('1234asda567asd890',4,'-') //result:"1-234a-sda5-67as-d890" function formatText(str, size, delimiter) { var _size = size || 3, _delimiter = delimiter || ','; var regText = '\\B(?=(\\w{' + _size + '})+(?!\\w))'; var reg = new RegExp(regText, 'g'); return str.replace(reg, _delimiter); } //现金额大写转换函数 //ecDo.upDigit(168752632) //result:"人民币壹亿陆仟捌佰柒拾伍万贰仟陆佰叁拾贰元整" //ecDo.upDigit(1682) //result:"人民币壹仟陆佰捌拾贰元整" //ecDo.upDigit(-1693) //result:"欠人民币壹仟陆佰玖拾叁元整" function upDigit(n) { var fraction = ['角', '分', '厘']; var digit = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖']; var unit = [ ['元', '万', '亿'], ['', '拾', '佰', '仟'] ]; var head = n < 0 ? '欠人民币' : '人民币'; n = Math.abs(n); var s = ''; for (var i = 0; i < fraction.length; i++) { s += (digit[Math.floor(n * 10 * Math.pow(10, i)) % 10] + fraction[i]).replace(/零./, ''); } s = s || '整'; n = Math.floor(n); for (var i = 0; i < unit[0].length && n > 0; i++) { var p = ''; for (var j = 0; j < unit[1].length && n > 0; j++) { p = digit[n % 10] + unit[1][j] + p; n = Math.floor(n / 10); } s = p.replace(/(零.)*零$/, '').replace(/^$/, '零') + unit[0][i] + s; //s = p + unit[0][i] + s; } return head + s.replace(/(零.)*零元/, '元').replace(/(零.)+/g, '零').replace(/^整$/, '零元整'); } //保留2位小数 function toDecimal2(x){ var f = parseFloat(x); if (isNaN(f)) { return false; } var f = Math.round(x * 100) / 100; var s = f.toString(); var rs = s.indexOf('.'); if (rs < 0) { rs = s.length; s += '.'; } while (s.length <= rs + 2) { s += '0'; } return s; } export{ trim, changeCase, repeatStr, replaceAll, replaceStr, checkPwd, formatText, upDigit, toDecimal2, formaDate }
// 找 filter/filter.js import * as filters from './filter/filter.js' //遍历全部导出的过滤器并添加到全局过滤器 Object.keys(filters).forEach((key) => { Vue.filter(key, filters[key]); })
单页面应用SPA的缺点:
一、首次加载耗时长
二、SEO问题严重,不利于搜索引擎的查找
三、前进、后退、地址栏、书签等,都须要程序进行管理,页面的复杂度很高
其中前二者是他的最主要问题。
未完待续~~,接下篇