本文內容來自網絡,整理出來分享於你們~~
參考至小冊 剖析 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問題嚴重,不利於搜索引擎的查找
三、前進、後退、地址欄、書籤等,都須要程序進行管理,頁面的複雜度很高
其中前二者是他的最主要問題。
未完待續~~,接下篇