那麼,它有什麼缺點?javascript
首先你得說說相同點,兩個都是MVVM框架,數據驅動視圖,無爭議。若是說不一樣,那可能分爲如下這麼幾點:css
MVVM的核心是數據驅動
即ViewModel,ViewModel
是View和Model的關係映射
。html
MVVM本質就是基於操做數據
來操做視圖
進而操做DOM
,藉助於MVVM無需直接
操做DOM,開發者只需編寫ViewModel
中有業務
,使得View徹底實現自動化
。前端
SPA( single-page application )即一個web項目
就只有一個頁面
(即一個HTML文件,HTML 內容的變換是利用路由機制實現的。vue
僅在 Web 頁面初始化
時加載
相應的 HTML、JavaScript 和 CSS
。一旦頁面加載完成
,SPA 不會
由於用戶的操做
而進行頁面的從新加載或跳轉
;取而代之的是利用路由機制
實現 HTML 內容
的變換,UI 與用戶的交互,避免頁面的從新加載。java
優勢:node
用戶體驗好、快
,內容的改變不須要從新加載整個頁面,避免了沒必要要的跳轉和重複渲染;服務器壓力小
;職責分離
,架構清晰
,前端進行交互邏輯,後端負責數據處理;缺點:react
初次加載耗時多
:爲實現單頁 Web 應用功能及顯示效果,須要在加載頁面的時候將 JavaScript、CSS 統一加載,部分頁面按需加載;前進後退路由管理
:因爲單頁應用在一個頁面中顯示全部的內容,因此不能使用瀏覽器的前進後退功能,全部的頁面切換須要本身創建堆棧管理;webpack
SEO 難度較大
:因爲全部的內容都在一個頁面中動態替換顯示,因此在 SEO 上其有着自然的弱勢。ios
什麼是vue生命週期?Vue 實例
從建立
到銷燬
的過程
,就是生命週期。
注意:瀏覽器有8個鉤子,可是node
中作服務端渲染的時候只有beforeCreate
和created
new Vue()
以後觸發的第一個
鉤子,在當前階段data、methods、computed以及watch上的數據和方法都不能被訪問。 能夠作頁面攔截。當進一個路由的時候咱們能夠判斷是否有權限進去,是否安全進去,攜帶參數是否完整,參數是否安全。使用這個鉤子好函數的時候就避免了讓頁面去判斷,省掉了建立一個組建Vue實例。實例建立完成後
,當前階段已經完成了數據觀測
,也就是能夠使用數據,更改數據,在這裏更改
數據不會
觸發updated
函數。能夠作一些初始數據的獲取,在當前階段沒法
與Dom
進行交互
(由於Dom尚未建立),若是非要想,能夠經過vm.$nextTick
來訪問Dom。掛載以前
,在這以前template模板已導入渲染函數編譯。而當前階段虛擬Dom
已經建立完成
,即將開始渲染。在此時也能夠對數據進行更改,不會觸發updated。掛載完成後
,在當前階段,真實
的Dom
掛載完畢,數據完成雙向綁定
,能夠訪問
到Dom節點
,使用$refs屬性對Dom進行操做。更新以前
,也就是響應式數據發生更新,虛擬dom從新渲染以前被觸發,你能夠在當前階段進行更改數據,不會形成重渲染。更新完成以後
,當前階段組件Dom已完成更新。要注意的是避免在此期間更改數據,由於這可能會致使無限循環的更新。實例銷燬以前
,在當前階段實例徹底能夠被使用,咱們能夠在這時進行善後收尾工做,好比清除計時器,銷燬父組件對子組件的重複監聽。beforeDestroy(){Bus.$off("saveTheme")}
加載渲染過程 父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount- >子mounted->父mounted
子組件更新過程 父beforeUpdate->子beforeUpdate->子updated->父updated
父組件更新過程 父 beforeUpdate -> 父 updated
銷燬過程 父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
它的生命週期中有多個事件鉤子,讓咱們控制
Vue實例過程更加清晰
。
第一次頁面加載時會觸發 beforeCreate, created, beforeMount, mounted 這幾個鉤子
v-if
事件監聽器
和子組件
適當地被銷燬和重建
;惰性
的:若是在初始渲染時條件爲假,則什麼也不作——直到條件第一次變爲真時,纔會開始渲染條件塊。v-show
無論初始條件是什麼,元素老是會被渲染,而且只是簡單地基於 CSS 的 「display」 屬性進行切換。
因此:
不多改變條件
,不須要
頻繁切換條件的場景;很是頻繁
切換條件的場景。背景:
全部的 prop 都使得其父子 prop 之間造成了一個單向下行綁定
:父級 prop 的更新會向下流動到子組件中,可是反過來則不行。這樣會防止從子組件意外改變
父級組件的狀態,從而致使你的應用的數據流向變的混亂。
每次父級組件發生更新時,子組件中全部的 prop 都將會刷新爲最新的值。這意味着你不該該在一個子組件內部改變 prop。若是你這樣作了,Vue 會在瀏覽器的控制檯中發出警告。子組件想修改時,只能經過 $emit
派發一個自定義事件
,父組件接收到後,由父組件修改
。
有兩種常見的試圖改變一個 prop 的情形 :
在第2狀況下,最好定義一個本地的 data屬性並將這個 prop 用做其初始值:
props: ['initialCounter'], data: function () { return { counter: this.initialCounter//定義本地的data屬性接收prop初始值 } }
這個 prop 以一種原始的值傳入且須要進行轉換。
在這種狀況下,最好使用這個 prop 的值來定義一個計算屬性
props: ['size'], computed: { normalizedSize: function () { return this.size.trim().toLowerCase() } }
官方實例
的異步請求是在mounted
生命週期中調用的,而實際上也能夠在created生命週期中調用。
本人推薦在 created 鉤子函數中調用異步請求,有如下優勢:
更快
獲取到服務端數據
,減小
頁面 loading 時間;ssr
不支持 beforeMount 、mounted 鉤子函數,因此放在 created 中有助於一致性;1. 父子props,on
// 子組件
<template> <header> <h1 @click="changeTitle">{{title}}</h1>//綁定一個點擊事件 </header> </template> <script> export default { data() { return { title:"Vue.js Demo" } }, methods:{ changeTitle() { this.$emit("titleChanged","子向父組件傳值");//自定義事件 傳遞值「子向父組件傳值」 } } } </script>
// 父組件
<template> <div id="app"> <Header @titleChanged="updateTitle" ></Header>//與子組件titleChanged自定義事件保持一致 <h2>{{title}}</h2> </div> </template> <script> import Header from "./Header" export default { data(){ return{ title:"傳遞的是一個值" } }, methods:{ updateTitle(e){ //聲明這個函數 this.title = e; } }, components:{ Header } } </script>
2. parent / $children與 ref
// A 子組件
export default { data () { return { title: 'a組件' } }, methods: { sayHello () { alert('Hello'); } } }
// 父組件
<template> <A ref="comA"></A> </template> <script> export default { mounted () { const comA = this.$refs.comA; console.log(comA.title); // a組件 comA.sayHello(); // 彈窗 } } </script>
3.attrs,listeners
attrs: 包含了父做用域
中不被 prop
所識別
(且獲取) 的特性綁定 ( class 和 style 除外 )。當一個組件沒有聲明任何 prop 時,這裏會包含全部父做用域的綁定 ( class 和 style 除外 ),而且能夠經過 v-bind="$attrs" 傳入內部組件。一般配合 inheritAttrs 選項一塊兒使用。
listeners: :包含了父做用域中的 (不含 .native 修飾器的) v-on 事件監聽器。它能夠經過 v-on="$listeners" 傳入內部組件
// index.vue
<template> <div> <h2>浪裏行舟</h2> <child-com1 :foo="foo" :boo="boo" :coo="coo" :doo="doo" title="前端工匠"></child-com1> </div> </template> <script> const childCom1 = () => import("./childCom1.vue"); export default { components: { childCom1 }, data() { return { foo: "Javascript", boo: "Html", coo: "CSS", doo: "Vue" }; } }; </script>
// childCom1.vue
<template class="border"> <div> <p>foo: {{ foo }}</p> <p>childCom1的$attrs: {{ $attrs }}</p> <child-com2 v-bind="$attrs"></child-com2> </div> </template> <script> const childCom2 = () => import("./childCom2.vue"); export default { components: { childCom2 }, inheritAttrs: false, // 能夠關閉自動掛載到組件根元素上的沒有在props聲明的屬性 props: { foo: String // foo做爲props屬性綁定 }, created() { console.log(this.$attrs); // 父組件中的屬性,且不在當前組件props中的屬性。{ "boo": "Html", "coo": "CSS", "doo": "Vue", "title": "前端工匠" } } }; </script>
// childCom2.vue
<template> <div class="border"> <p>boo: {{ boo }}</p> <p>childCom2: {{ $attrs }}</p> <child-com3 v-bind="$attrs"></child-com3> </div> </template> <script> const childCom3 = () => import("./childCom3.vue"); export default { components: { childCom3 }, inheritAttrs: false, props: { boo: String }, created() { console.log(this.$attrs); // / 父組件中的屬性,且不在當前組件props中的屬性。{"coo": "CSS", "doo": "Vue", "title": "前端工匠" } } }; </script>
// childCom3.vue
<template> <div class="border"> <p>childCom3: {{ $attrs }}</p> </div> </template> <script> export default { props: { coo: String, title: String } }; </script>
4. Provide、inject的使用:
父組件
<template> <div id="app"> </div> </template> <script> export default { data () { return { datas: [ { id: 1, label: '產品一' } ] } }, provide { return { datas: this.datas } } } </script>
子組件
<template> <div> <ul> <li v-for="(item, index) in datas" :key="index"> {{ item.label }} </li> </ul> </div> </template> <script> export default { inject: ['datas'] } </script>
SSR也就是服務端渲染
,也就是將Vue在客戶端把標籤
渲染成HTML
的工做放在服務端完成,而後再把html直接返回
給客戶端。
服務端渲染 SSR 的優缺點以下:
(1)服務端渲染的優勢:
SPA
中是抓取不到
頁面經過 Ajax
獲取到的內容
;而 SSR
是直接由服務端
返回已經渲染好
的頁面(數據已經包含在頁面中),因此搜索引擎爬取工具能夠抓取渲染好的頁面;SPA
會等待
全部 Vue 編譯後的 js
文件都下載完成後
,纔開始
進行頁面的渲染
,文件下載等須要必定的時間等,因此首屏渲染須要必定的時間;SSR 直接由服務端渲染好頁面直接返回顯示,無需等待下載 js 文件及再去渲染等,因此 SSR 有更快的內容到達時間;(2) 服務端渲染的缺點:
beforCreate
和 created
兩個鉤子函數,這會致使一些外部擴展庫須要特殊處理,才能在服務端渲染應用程序中運行;而且與能夠部署在任何靜態文件服務器上的徹底靜態單頁面應用程序 SPA 不一樣,服務端渲染應用程序,須要處於 Node.js server 運行環境;vue-router 有 3 種路由模式:hash
、history
、abstract
,對應的源碼以下所示:
switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } }
路由模式的說明以下:
全部瀏覽器
,包括不支持 HTML5 History Api 的瀏覽器;JavaScript
運行環境,如 Node.js 服務器端。若是發現沒有瀏覽器的 API,路由會自動強制進入這個模式.(1)hash 模式的實現原理
早期的前端路由的實現就是基於 location.hash
來實現的。其實現原理很簡單,location.hash 的值就是 URL 中 # 後面的內容。
好比下面這個網站,它的 location.hash 的值爲 '#search'
:
https://www.word.com#search
hash 路由模式的實現主要是基於下面幾個特性:
客戶端
的一種狀態
,也就是說當向服務器端發出請求時
,hash 部分不會被髮送
;改變
,都會在瀏覽器的訪問歷史
中增長
一個記錄
。所以咱們能經過瀏覽器的回退、前進按鈕控制hash 的切換;hashchange
事件來監聽 hash 值的變化,從而對頁面進行跳轉(渲染)。(2)history 模式的實現原理
HTML5 提供了 History API
來實現 URL 的變化,其中作最主要的 API 有如下兩個:
這兩個 API 能夠在不進行刷新
的狀況下,操做
瀏覽器的歷史紀錄
。惟一不一樣的是,前者是新增一個歷史記錄,後者是直接替換當前的歷史記錄,以下所示:
window.history.pushState(null, null, path); window.history.replaceState(null, null, path);
history 路由模式的實現主要基於存在下面幾個特性:
popstate
事件來監聽 url 的變化,從而對頁面進行跳轉(渲染);key 是爲 Vue 中 vnode
的惟一標記
,經過這個 key,咱們的 diff
操做能夠更準確、更快速
。
Vue 的 diff 過程能夠歸納爲:
oldCh
和 newCh
各有兩個頭尾
的變量 oldStartIndex、oldEndIndex
和 newStartIndex、newEndIndex
,它們會新節點和舊節點會進行兩兩對比
,即一共有4種比較方式:newStartIndex 和oldStartIndex 、newEndIndex 和 oldEndIndex 、newStartIndex 和 oldEndIndex 、newEndIndex 和 oldStartIndex,若是以上 4 種比較都沒匹配
,若是設置了key
,就會用 key 再進行
比較,在比較的過程當中,遍歷會往中間靠
,一旦 StartIdx > EndIdx 代表 oldCh 和 newCh 至少有一個已經遍歷完了,就會結束比較。
因此 Vue 中 key 的做用是:key 是爲 Vue 中 vnode 的惟一標記,經過這個 key,咱們的 diff 操做能夠更準確、更快速
更快速:利用 key 的惟一性生成 map 對象來獲取對應節點,比遍歷方式更快,源碼以下:
function createKeyToOldIdx (children, beginIdx, endIdx) { let i, key const map = {} for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key if (isDef(key)) map[key] = i } return map }
參考1:Vue2.0 v-for 中 :key 到底有什麼用?
虛擬 DOM 的實現原理主要包括如下 3 部分:
優勢:
DOM 操做
的實現必須是普適
的,因此它的性能並非最優的;可是比起粗暴的 DOM 操做性能要好不少,所以框架的虛擬 DOM 至少能夠保證在你不須要手動優化
的狀況下,依然能夠提供還不錯
的性能
,即保證性能的下限;缺點:
Proxy 的優點以下:
Object.defineProperty 的優點以下:
Proxy 是 ES6 中新增的功能,它能夠用來自定義對象中的操做。
let p = new Proxy(target, handler)
添加代理
的對象對象中
的操做
,好比能夠用來自定義 set 或者 get 函數。下面來經過 Proxy 來實現一個數據響應式:
let onWatch = (obj, setBind, getLogger) => { let handler = { get(target, property, receiver) { getLogger(target, property) return Reflect.get(target, property, receiver) }, set(target, property, value, receiver) { setBind(value, property) return Reflect.set(target, property, value) } } return new Proxy(obj, handler) } let obj = { a: 1 } let p = onWatch( obj, (v, property) => { console.log(`監聽到屬性${property}改變爲${v}`) }, (target, property) => { console.log(`'${property}' = ${target[property]}`) } ) p.a = 2 // 監聽到屬性a改變爲2 p.a // 'a' = 2
在上述代碼中,經過自定義 set 和 get 函數的方式,在本來的邏輯中插入了咱們的函數邏輯,實現了在對對象任何屬性進行讀寫時發出通知。
固然這是簡單版的響應式實現,若是須要實現一個 Vue 中的響應式,須要在 get 中收集依賴,在 set 派發更新,之因此 Vue3.0 要使用 Proxy 替換本來的 API 緣由在於 Proxy 無需一層層遞歸爲每一個屬性添加代理,一次便可完成以上操做,性能上更好,而且本來的實現有一些數據更新不能監聽到,可是 Proxy 能夠完美監聽到任何方式的數據改變,惟一缺陷就是瀏覽器的兼容性很差。
Vue 框架是經過遍歷數組 和遞歸遍歷對象,從而達到利用 Object.defineProperty() 也能對對象和數組(部分方法的操做)進行監聽。
vue2:
數組
就是使用 object.defineProperty
從新定義數組的每一項
,能引發數組變化的方法爲 pop 、 push 、 shift 、 unshift 、 splice 、 sort 、 reverse
這七種,只要這些方法執行改了數組內容,就更新內容
函數劫持
的方式,重寫
了數組方法,具體就是更改了數組的原型,更改爲本身的,用戶調數組的一些方法的時候,走的就是本身的方法,而後通知視圖去更新(本質就是在原有的方法上又調用了更新數據的方法)。對象
,那麼就對數組的每一項進行觀測vue3:
改用 proxy ,可直接監聽對象數組的變化。
Vue 數據雙向綁定主要是指:數據變化更新視圖,視圖變化更新數據
輸入框內容變化時,Data 中的數據同步變化。即 View => Data 的變化。 Data 中的數據變化時,文本節點的內容同步變化。即 Data => View 的變化。
其中,View 變化更新 Data ,能夠經過事件監聽的方式來實現,因此 Vue 的數據雙向綁定的工做主要是如何根據 Data 變化更新 View。
Vue 主要經過如下 4 個步驟來實現數據雙向綁定的:
Object.defineProperty()
對屬性都加上 setter
和 getter
。這樣的話,給這個對象的某個值賦值,就會觸發 setter,那麼就能監聽到了數據變化。變量
都替換成數據
,而後初始化渲染頁面視圖,並將每一個指令
對應的節點綁定更新函數
,添加監聽數據的訂閱者,一旦數據有變更,收到通知,調用更新函數進行數據更新。v-model 指令在表單 input、textarea、select 等元素上建立雙向數據綁定,v-model 本質上是語法糖
,會在內部爲不一樣的輸入元素使用不一樣的屬性並拋出不一樣的事件:
value
屬性和 input
事件;checked
屬性和 change
事件;prop
並將 change
做爲事件。以 input 表單元素爲例:
<input v-model='something'>
至關於
<input :value="something" @input="something = $event.target.value">
爲何組件中的 data 必須是一個函數,而後 return 一個對象,而 new Vue 實例裏,data 能夠直接是一個對象?
// data
data() { return { message: "子組件", childName:this.name } }
// new Vue
new Vue({ el: '#app', router, template: '<App/>', components: {App} })
一個組件被複用屢次的話,也就會建立多個實例,本質上,這些實例用的都是同一個構造函數。
若是data是對象的話,對象屬於引用類型,會影響到全部的實例,因此爲了保證組件不一樣的實例之間data不衝突,data必須是一個函數。
而 new Vue 的實例,是不會被複用的,所以不存在引用對象的問題。
keep-alive 是 Vue 內置
的一個組件
,能夠使被包含
的組件保留狀態
,避免從新渲染
,其有如下特性:
include
和 exclude
屬性,二者都支持字符串或正則表達式, include 表示只有名稱匹配
的組件會被緩存
,exclude 表示任何名稱匹配
的組件都不會被緩存
,其中 exclude 的優先級
比 include 高
;activated
和 deactivated
,當組件被激活
時,觸發鉤子函數 activated,當組件被移除
時,觸發鉤子函數 deactivated。keep-alive的生命週期
好比有父組件 Parent 和子組件 Child,若是父組件監聽到子組件掛載 mounted 就作一些邏輯處理,能夠經過如下寫法實現:
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() { this.$emit("mounted"); }
以上須要手動經過 $emit 觸發父組件的事件,更簡單的方式能夠在父組件引用子組件時經過 @hook 來監聽便可,以下所示:
// Parent.vue
<Child @hook:mounted="doSomething" ></Child> doSomething() { console.log('父組件監聽到 mounted 鉤子函數 ...'); },
// Child.vue
mounted(){ console.log('子組件觸發 mounted 鉤子函數 ...'); },
// 以上輸出順序爲:
// 子組件觸發 mounted 鉤子函數 ...
// 父組件監聽到 mounted 鉤子函數 ...
固然 @hook 方法不只僅是能夠監聽 mounted,其它的生命週期事件,例如:created,updated 等均可以監聽。
因爲 JavaScript 的限制,Vue 不能檢測到如下數組的變更:
vm.items[indexOfItem] = newValue
vm.items.length = newLength
爲了解決第一個問題,Vue 提供瞭如下操做方法:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// vm.$set(Vue.set的一個別名)
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
爲了解決第二個問題,Vue 提供瞭如下操做方法:
// Array.prototype.splice
vm.items.splice(newLength)
使用了函數劫持
的方式,重寫
了數組的方法,Vue將data
中的數組
進行了原型鏈重寫
,指向了本身定義
的數組原型方法
。這樣當調用數組api時,能夠通知依賴更新。若是數組中包含着引用類型,會對數組中的引用類型再次遞歸遍歷進行監控。這樣就實現了監測數組變化。
簡單來講,diff算法有如下過程
同級比較, 再比較子節點,先判斷一方有子節點一方沒有子節點的狀況(若是新的children沒有子節點,將舊的子節點移除)
比較都有子節點的狀況(核心diff)遞歸比較子節點
正常Diff兩個樹
的時間複雜度是O(n^3),但實際狀況下咱們不多會進行跨層級的移動DOM,因此Vue將Diff進行了優化,從O(n^3) -> O(n),只有當新舊children都爲多個子節點時才須要用核心的Diff算法進行同層級比較。
Vue2的核心Diff算法採用了雙端比較
的算法,同時重新舊children的兩端開始進行比較,藉助key值找到可複用的節點,再進行相關操做。相比React的Diff算法,一樣狀況下能夠減小移動節點次數,減小沒必要要的性能損耗,更加的優雅。
Vue3.x借鑑了 ivi算法和 inferno算法 在建立VNode時就肯定其類型,以及在mount/patch的過程當中採用位運算來判斷一個VNode的類型,在這個基礎之上再配合核心的Diff算法,使得性能上較Vue2.x有了提高。 該算法中還運用了動態規劃的思想求解最長遞歸子序列。
簡單說,Vue的編譯過程就是將template轉化爲render函數的過程。會經歷如下階段:
首先解析模版
,生成AST
語法樹(一種用JavaScript對象的形式來描述整個模板)。
使用大量的正則
表達式對模板
進行解析
,遇到標籤
、文本
的時候都會執行
對應的鉤子
進行相關處理。
Vue的數據是響應式的,但其實模板中並非全部的數據都是響應式的。有一些數據首次渲染
後就不會
再變化
,對應的DOM
也不會變化。那麼優化過程就是深度遍歷
AST樹,按照相關條件對樹節點
進行標記
。這些被標記的節點
(靜態節點)咱們就能夠跳過
對它們的比對
,對運行時的模板起到很大的優化做用。
編譯的最後一步是將優化後的AST樹轉換
爲可執行
的代碼
。
computed:
計算屬性
,也就是計算值,它更多用於計算值的場景緩存性
,computed的值在getter執行後是會緩存的,只有在它依賴的屬性值改變以後,下一次獲取computed的值時纔會從新調用對應的getter來計算watch:
props
$emit
或者本組件
的值,當數據變化時來執行回調進行後續操做不變化
也會執行
小結:
在下次
DOM
更新循環結束以後
執行延遲迴調
。在這裏面的代碼會等到dom更新之後
再執行。
<template> <section> <div ref="hello"> <h1>Hello World ~</h1> </div> <el-button type="danger" @click="get">點擊</el-button> </section> </template> <script> export default { methods: { get() { } }, mounted() { console.log(333); console.log(this.$refs['hello']); this.$nextTick(() => { console.log(444); console.log(this.$refs['hello']); }); }, created() { console.log(111); console.log(this.$refs['hello']); this.$nextTick(() => { console.log(222); console.log(this.$refs['hello']); }); } } </script>
Vue在初始化數據時,會使用Object.defineProperty
從新定義data中的全部屬性
,當頁面使用
對應屬性時,首先會進行依賴收集
(收集當前組件的watcher),若是屬性發生變化
會通知
相關依賴進行更新操做
(發佈訂閱)
具體的過程:
initData
初始化用戶傳入的參數
new Observer
對數據進行觀測
對象類型
就會調用 this.walk(value)
對對象進行處理,內部使用 defineeReactive
循環對象屬性定義響應式變化,核心就是使用 Object.defineProperty
從新定義數據。Vue3.x改用Proxy替代Object.defineProperty。由於Proxy能夠直接監聽
對象和數組的變化,而且有多達13種攔截方法。而且做爲新標準將受到瀏覽器廠商重點持續的性能優化。
Proxy只會代理對象的第一層,那麼Vue3又是怎樣處理這個問題的呢?
判斷當前Reflect.get
的返回值是否爲Object
,若是是則再經過reactive
方法作代理, 這樣就實現了深度觀測。
監測數組的時候可能觸發屢次get/set,那麼如何防止觸發屢次呢?
咱們能夠判斷key
是否爲當前被代理對象target
自身屬性,也能夠判斷舊值
與新值
是否相等,只有知足以上兩個條件之一時,纔有可能執行trigger
。
不能同名 由於無論是計算屬性仍是data仍是props 都會被掛載在vm實例
上,所以 這三個都不能同名
找到config/index.js
配置文件,找build
打包對象裏的assetsPublicPath
屬性 默認值爲/
,更改成./
就行了
由於動態添加src
被當作靜態資源
處理了,沒有進行編譯
,因此要加上require。
<img :src="require('../../../assets/images/xxx.png')" />
Object.freeze
適合一些 big data
的業務場景。尤爲是作管理後臺的時候,常常會有一些超大數據量
的 table
,或者一個含有 n 多數據的圖表,這種數據量很大的東西使用起來最明顯的感覺就是卡。但其實不少時候其實這些數據其實並不須要響應式變化,這時候你就能夠使用 Object.freeze 方法了,它能夠凍結一個對象
(注意它不併是 vue 特有的 api)。
當你把一個普通的 JavaScript 對象傳給 Vue 實例的 data 選項,Vue 將遍歷此對象全部的屬性,並使用 Object.defineProperty 把這些屬性所有轉爲 getter/setter,它們讓 Vue 能進行追蹤依賴,在屬性被訪問和修改時通知變化。
使用了 Object.freeze 以後,不只能夠減小 observer
的開銷,還能減小很多內存開銷
。
使用方式:
this.item = Object.freeze(Object.assign({}, this.item))
先了解一下,在 vue 中,有不少內置的指令.
好比:
因此,關於指令,咱們能夠總結下面幾點:
HTML 屬性
地方的,<input v-model='name' type='text' />
v-
開頭的.Vue自定義指令案例1
例如:咱們須要一個指令,寫在某個HTML表單元素上,而後讓它在被加載到DOM中時,自動獲取焦點.
// 和自定義過濾器同樣,咱們這裏定義的是全局指令 Vue.directive('focus',{ inserted(el) { el.focus() } }) <div id='app'> <input type="text"> <input type="text" v-focus placeholder="我有v-focus,因此,我獲取了焦點"> </div>
先總結幾個點:
Vue.directive()
來新建一個全局指令
,(指令使用在HTML元素屬性上的)第一個參數
focus是指令名
,指令名在聲明的時候,不須要加 v-<input type="text" v-focus placeholder="我有v-focus,因此,我獲取了焦點"/>
咱們須要加上 v-.第二個參數
是一個對象
,對象內部
有個 inserted()
的函數,函數有 el
這個參數.DOM元素
,在這裏就是後面那個有 placeholder
的 input
,el 就等價於 document.getElementById('el.id')
$(el)
無縫鏈接 jQuery指令的生命週期
用指令咱們須要:
當一個指令綁定到一個元素上時,其實指令的內部會有五個生命週期事件函數.
bind(){}
當指令綁定
到 HTML 元素
上時觸發.只調用一次.inserted()
當綁定了指令的這個HTML元素
插入到父元素
上時觸發(在這裏父元素是 div#app
).但不保證,父元素已經插入了 DOM 文檔.updated()
所在組件的VNode更新
時調用.componentUpdate
指令所在的組件的VNode以及其子VNode 所有更新後
調用.unbind
: 指令和元素解綁
的時候調用,只調用一次Vue 指令的聲明周期函數
Vue.directive('gqs',{ bind() { // 當指令綁定到 HTML 元素上時觸發.**只調用一次** console.log('bind triggerd') }, inserted() { // 當綁定了指令的這個HTML元素插入到父元素上時觸發(在這裏父元素是 `div#app`)**.但不保證,父元素已經插入了 DOM 文檔.** console.log('inserted triggerd') }, updated() { // 所在組件的`VNode`更新時調用. console.log('updated triggerd') }, componentUpdated() { // 指令所在組件的 VNode 及其子 VNode 所有更新後調用。 console.log('componentUpdated triggerd') }, unbind() { // 只調用一次,指令與元素解綁時調用. console.log('unbind triggerd') } })
HTML
<div id='app' v-gqs></div> 結果: bind triggerd inserted triggerd
發現默認狀況下只有 bind 和 inserted 聲明周期函數觸發了.
那麼剩下的三個何時觸發呢?
<div id='app' > <p v-gqs v-if="show">v-if是刪除或者新建dom元素,它會觸發unbind指令聲明週期嗎?</p> <button @click="show=!show">toggle</button> </div>
當指令綁定的元素被銷燬時,會觸發指令的 unbind 事件.
(新建並顯示,仍然是觸發 bind & inserted)
unbind觸發.gif
<p v-gqs v-show="show2">v-show設置元素的display:block|none,會觸發componentUpdated事件</p> <button @click="show2=!show2">toggle-v-show</button>
根據官方文檔定義:
若是在實例建立以後
添加新的屬性
到實例上,它不會觸發視圖更新。
Vue 不容許在已經建立
的實例
上動態添加
新的根級響應式屬性
(root-level reactive property)。
然而它能夠使用 Vue.set(object, key, value)
方法將響應屬性添加到嵌套的對象上。
多個實例
引用了相同或類似的方法或屬性
等,可將這些重複的內容抽取出來做爲mixins的js,export出去,在須要引用的vue文件經過mixins屬性注入,與當前實例
的其餘內容
進行merge
。
一個混入對象能夠包含任意組件選項
。同一個生命週期,混入對象
會比組件
的先執行
。
//暴露兩個mixins對象
export const mixinsTest1 = { methods: { hello1() { console.log("hello1"); } }, created() { this.hello1(); }, } export const mixinsTest2 = { methods:{ hello2(){ console.log("hello2"); } }, created() { this.hello2(); }, }
<template> <div> home </div> </template> <script> import {mixinsTest1,mixinsTest2} from '../util/test.js' export default { name: "Home", data () { return { }; }, created(){ console.log("1212"); }, mixins:[mixinsTest2,mixinsTest1] // 先調用哪一個mixins對象,就先執行哪一個 } </script> hello2 hello1 1212
v-on: .stop .prevent
<input v-model="msg" type="text" v-on="{input:a, focus:b}"/>
安裝scss依賴包:
npm install sass-loader --save-dev npm install node-sass --save-dev
在build文件夾下修改 webpack.base.conf.js 文件,在 module 下的 rules 裏添加配置,以下:
{ test: /\.scss$/, loaders: ['style', 'css', 'sass'] }
應用:
在vue文件中應用scss時,須要在style樣式標籤上添加lang="scss",即<style lang="scss">。
watch 中的 immediate 會讓監聽在初始值聲明的時候去執行監聽計算,不然就是 created 先執行
created():在實例建立完成後被當即調用。在這一步,實例已完成如下的配置:數據觀測 (data observer),property 和方法的運算,watch/event 事件回調。然而,掛載階段還沒開始,$el property 目前尚不可用。
activated():是在路由設置<keep-alive></keep-alive>
時,纔會有這個生命週期。在被 keep-alive 緩存的組件激活時調用。
由於在插入數據或者刪除數據的時候,會致使後面的數據的key綁定的index變化,進而致使重新渲染,效率會下降
動態組件使用方法
<keep-alive> <component :is="isWhich"></component> </keep-alive> 使用標籤保存狀態,即切換組件再次回來依然是原來的樣子,頁面不會刷新,若不須要能夠去掉。 經過事件改變is綁定的isWhich值便可切換成不一樣的組件,isWhich的值爲組件名稱。
使用場景:
好比,有一個表單,表單提交成功後,但願組件恢復到初始狀態,重置data數據。
使用Object.assign()
,vm.$data
能夠獲取當前狀態下的data,vm.$options.data
能夠獲取到組件初始化狀態下的data
初始狀態下設置data數據的默認值,重置時直接bject.assign(this.$data, this.$options.data())
說明:
this[屬性名] = this.$options.data()[屬性名]
,如this.message = this.$options.data().message
<template comments> ... </template>
v-data-something
屬性,再在選擇器時加上對應[v-data-something]
,即CSS屬性選擇器,以此完成相似做用域的選擇方式而後在 index.html 中添加:
<link rel="shortcut icon" type="image/x-icon" href="static/favicon.ico">
Babel
默認只轉換新的JavaScript句法
(syntax),而不轉換新的API
,好比Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise
等全局對象,以及一些定義在全局對象
上的方法(好比Object.assign
)都不會轉碼。
舉例來講,ES6在Array對象上新增了Array.from方法。Babel就不會轉碼這個方法。若是想讓這個方法運行,必須使用babel-polyfill
,爲當前環境提供一個墊片。
強制從新渲染
this.$forceUpdate()
強制從新刷新某組件
//模版上綁定key <SomeComponent :key="theKey"/> //選項裏綁定data data(){ return{ theKey:0 } }
//刷新key達到刷新組件的目的
theKey++;
加入.native修飾符
報錯 "Method 'xxx' has already been defined as a data property"
鍵名優先級:props > data > methods
實例建立以後,能夠經過 vm.$data
訪問原始數據對象
。Vue 實例也代理了 data 對象上全部的屬性,所以訪問 vm.a
等價於訪問 vm.$data.a
。
以 _
或 $
開頭的屬性 不會
被 Vue 實例代理
,由於它們可能和 Vue 內置
的屬性、API 方法衝突
。能夠使用 vm.$data._property
的方式訪問
這些屬性。
使用了history模式,然後端又沒有進行相關資源配置。
v-model默認的觸發條件是input事件,加了.lazy
修飾符以後,v-model會在change
事件觸發的時候去監聽
diff算法要求,源碼中patch.js中的patchVnode也是根據樹狀結構進行遍歷
生命週期的鉤子函數不能使用箭頭函數,否者this不能指向vue實例
<template></template>
有什麼用包裹嵌套其它元素,使元素具備區域性,自身具備三個特色:
緩存過濾
組見名稱
是由vue中組件name決定的解析和轉換
.vue 文件,提取
出其中的邏輯代碼 script、樣式代碼 style、以及 HTML 模版 template,再分別把它們交給對應的 Loader 去處理。
設置 path: '*'
, 而且放在最後一個
爲何要響應參數變化?
路由參數
發生了變化
,可是頁面數據
並未
及時更新,須要強制刷新後纔會變化。解決方案:
使用 watch 監聽
watch: { $route(to, from){ if(to != from) { console.log("監聽到路由變化,作出相應的處理"); } } }
向 router-view 組件中添加 key
<router-view :key="$route.fullPath"></router-view>
$route.fullPath
是完成後解析的URL,包含其查詢參數信息和hash完整路徑
在路由實例中配置 scrollBehavior(ro,form,savedPosition){ //滾動到頂部 return {x:0,y:0} //保持原先的滾動位置 return {selector:falsy} }
全稱: Model-View-ViewModel
, Model 表示數據模型層
, view 表示視圖層
, ViewModel 是 View 和 Model 層的橋樑
,數據綁定
到 viewModel
層並自動渲染到頁面中,視圖變化
通知 viewModel
層更新數據。
事件綁定有幾種?
addEventListener
實現。$on
方法 。普通元素
的原生
事件綁定在上是經過@click
進行綁定的組件
的原生
事件綁定是經過@click.native
進行綁定的,組件中的nativeOn
是等價於on的。組件
的自定義
事件是經過@click
綁定的,是經過 $on
方法來實現的,必須有$emit
才能夠觸發。解釋下這2種的區別:
<div @click="getData"></div>
,直接觸發的就是原生的點擊事件<BtnGroup @click="getName" @click.native="getData"></BtnGroup>
,這時候,要觸發原生的點擊事件getData,就須要使用修飾符.native
,由於直接使用@click
是接收來自子組件emit
過來的事件getName,這樣纔不會衝突。let compiler = require('vue-template-compiler'); // vue loader中的包 let r1 = compiler.compile('<div @click="fn()"></div>'); // 給普通標籤綁定click事件 // 給組件綁定一個事件,有兩種綁定方法 // 一種@click.native,這個綁定的就是原生事件 // 另外一種@click,這個綁定的就是組件自定義事件 let r2 = compiler.compile('<my-component @click.native="fn" @click="fn1"></mycomponent>'); console.log(r1.render); // {on:{click}} console.log(r2.render); // {nativeOn:{click},on:{click}} // 爲何組件要加native?由於組件最終會把nativeOn屬性放到on的屬性中去,這個on會單獨處理 // 組件中的nativeOn 等價於 普通元素on,組件on會單獨處理
Vue在更新DOM
時是異步執行
的,只要偵聽
到數據變化
,將開啓
一個隊列
,並緩衝
在同一事件循環中發生的全部數據變動
,若是同一個watcher
被屢次觸發
,只會被推入
到隊列
中一次
,這種在緩衝
時去除重複數據
對於減小沒必要要
的計算和DOM操做
是很是重要的.
而後,在下一個的事件循環tick中,Vue刷新隊列並執行實際(已去重的)工做,Vue在內部對異步隊列嘗試使用原生的Promise.then、MutationObserver和setImmediate
,若是執行環境不支持,則會採用setTimeout(fn, 0)
代替。
描述
對於Vue爲什麼採用異步渲染,簡單來講就是爲了提高性能
,由於不採用異步更新,在每次更新
數據都會對當前組件進行從新渲染
,爲了性能考慮,Vue會在本輪數據
更新後,再去異步更新視圖
,舉個例子,讓咱們在一個方法內重複更新一個值。
this.msg = 1; this.msg = 2; this.msg = 3;
事實上,咱們真正想要的其實只是最後一次更新而已,也就是說前三次DOM更新都是能夠省略的,咱們只須要等全部狀態都修改好了以後再進行渲染就能夠減小一些性能損耗。
對於渲染方面的問題是很明確的,最終只渲染一次確定比修改以後即渲染所耗費的性能少,在這裏咱們還須要考慮一下異步更新隊列的相關問題,假設咱們如今是進行了相關處理使得每次更新數據只進行一次真實DOM渲染,來讓咱們考慮異步更新隊列的性能優化。
假設這裏是同步更新隊列,this.msg=1,大體會發生這些事:
msg值更新 -> 觸發setter -> 觸發Watcher的update -> 從新調用 render -> 生成新的vdom -> dom-diff -> dom更新
這裏的dom更新並非渲染
(即佈局、繪製、合成等一系列步驟),而是更新內存
中的DOM樹結構
,以後再運行this.msg=2,再重複上述步驟,以後的第3次更新一樣會觸發相同的流程,等開始渲染的時候,最新的DOM樹中確實只會存在更新完成3,從這裏來看,前2次對msg的操做以及Vue內部對它的處理都是無用的操做,能夠進行優化處理。
若是是異步更新隊列,會是下面的狀況:
運行this.msg=1,並非當即進行上面的流程,而是將對msg有依賴的Watcher都保存在隊列中,該隊列可能這樣[Watcher1, Watcher2...]
,當運行this.msg=2後,一樣是將對msg有依賴的Watcher保存到隊列中,Vue內部
會作去重判斷
,此次操做後,能夠認爲隊列數據沒有發生變化,第3次更新也是上面的過程。
固然,你不可能只對msg有操做,你可能對該組件中的另外一個屬性也有操做,好比this.otherMsg=othermessage,一樣會把對otherMsg有依賴的Watcher添加到異步更新隊列中,由於有重複判斷操做,這個Watcher也只會在隊列中存在一次,本次異步任務執行結束後,會進入下一個任務執行流程,其實就是遍歷異步更新隊列中的每個Watcher,觸發其update,而後進行從新調用render -> new vdom -> dom-diff -> dom更新
等流程,可是這種方式和同步更新隊列相比,無論操做多少次msg,Vue在內部只會進行一次從新調用真實更新流程。
因此,對於異步更新隊列不是節省了渲染成本
,而是節省了Vue內部計算及DOM樹操做的成本
,無論採用哪一種方式,渲染確實只有一次。
此外,組件內部實際使用VirtualDOM進行渲染,也就是說,組件內部實際上是不關心哪一個狀態發生了變化,它只須要計算一次就能夠得知哪些節點須要更新,也就是說,若是更改了N個狀態,其實只須要發送一個信號就能夠將DOM更新到最新,若是咱們更新多個值。
this.msg = 1; this.age = 2; this.name = 3;
此處咱們分三次修改了三種狀態,但其實Vue只會渲染一次,由於VIrtualDOM只須要一次就能夠將整個組件的DOM更新到最新,它根本不會關心這個更新的信號究竟是從哪一個具體的狀態發出來的。
而爲了達到這個目的,咱們須要將渲染操做
推遲到全部
的狀態都修改完成
,爲了作到這一點只須要將渲染操做推遲到本輪事件循環的最後或者下一輪事件循環,也就是說,只須要在本輪事件循環
的最後
,等前面更新狀態
的語句都執行完以後
,執行一次
渲染操做,它就能夠無視前面各類更新狀態的語法,不管前面寫了多少條更新狀態的語句,只在最後渲染一次就能夠了。
將渲染推遲到本輪事件循環的最後執行渲染的時機會比推遲到下一輪快不少,因此Vue優先將渲染操做推遲到本輪事件循環的最後,若是執行環境不支持會降級到下一輪,Vue的變化偵測機制(setter)決定了它必然會在每次狀態發生變化時都會發出渲染的信號,但Vue會在收到信號以後檢查隊列中是否已經存在這個任務,保證隊列中不會有重複,若是隊列中不存在則將渲染操做添加到隊列中,以後經過異步的方式延遲執行隊列中的全部渲染的操做並清空隊列,當同一輪事件循環中反覆修改狀態時,並不會反覆向隊列中添加相同的渲染操做,因此咱們在使用Vue時,修改狀態後更新DOM都是異步的。
當數據變化
後會調用notify
方法,將watcher
遍歷,調用update
方法通知watcher進行更新,這時候watcher並不會當即去執行,在update中會調用queueWatcher
方法將watcher放到了一個隊列裏,在queueWatcher會根據watcher的進行去重,若多個屬性依賴一個watcher,則若是隊列中沒有該watcher就會將該watcher添加到隊列中,而後便會在$nextTick
方法的執行隊列中加入一個flushSchedulerQueue方法(這個方法將會觸發在緩衝隊列的全部回調的執行),而後將$nextTick
方法的回調加入$nextTick
方法中維護的執行隊列,flushSchedulerQueue中開始會觸發一個before的方法,其實就是beforeUpdate,而後watcher.run()纔開始真正執行watcher,執行完頁面就渲染完成,更新完成後會調用updated鉤子。
$nextTick
在上文中談到了對於Vue爲什麼採用異步渲染,假如此時咱們有一個需求,須要在頁面渲染完成後取得頁面的DOM元素,而因爲渲染是異步的,咱們不能直接在定義的方法中同步取得這個值的,因而就有了vm.$nextTick
方法,Vue中$nextTick
方法將回調延遲到下次DOM更新循環以後執行,也就是在下次DOM更新循環結束以後執行延遲迴調,在修改數據以後當即使用這個方法,可以獲取更新後的DOM。簡單來講就是當數據更新時,在DOM中渲染完成後,執行回調函數。
經過一個簡單的例子來演示$nextTick
方法的做用,首先須要知道Vue在更新DOM時是異步執行的,也就是說在更新數據時其不會阻塞代碼的執行,直到執行棧中代碼執行結束以後,纔開始執行異步任務隊列的代碼,因此在數據更新時,組件不會當即渲染,此時在獲取到DOM結構後取得的值依然是舊的值,而在$nextTick
方法中設定的回調函數會在組件渲染完成以後執行,取得DOM結構後取得的值即是新的值。
<!DOCTYPE html> <html> <head> <title>Vue</title> </head> <body> <div id="app"></div> </body> <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'Vue' }, template:` <div> <div ref="msgElement">{{msg}}</div> <button @click="updateMsg">updateMsg</button> </div> `, methods:{ updateMsg: function(){ this.msg = "Update"; console.log("DOM未更新:", this.$refs.msgElement.innerHTML) this.$nextTick(() => { console.log("DOM已更新:", this.$refs.msgElement.innerHTML) }) } }, }) </script> </html>
異步機制#
Js是單線程的,其引入了同步阻塞與異步非阻塞的執行模式,在Js異步模式中維護了一個Event Loop
,Event Loop是一個執行模型
,在不一樣的地方有不一樣的實現,瀏覽器和NodeJS基於不一樣的技術實現了各自的Event Loop。瀏覽器的Event Loop是在HTML5
的規範中明肯定義,NodeJS的Event Loop是基於libuv
實現的。
在瀏覽器中的Event Loop
由執行棧
Execution Stack、後臺線程
Background Threads、宏隊列
Macrotask Queue、微隊列
Microtask Queue組成。
主線程
執行同步任務
的數據結構
,函數調用造成了一個由若干幀組成的棧。setTimeout、setInterval、XMLHttpRequest
等等的執行線程。異步任務
的回調會依次進入宏隊列,等待後續被調用,包括setTimeout、setInterval、setImmediate(Node)、requestAnimationFrame、UI rendering、I/O等操做。異步任務
的回調會依次進入微隊列,等待後續調用,包括Promise、process.nextTick(Node)、Object.observe、MutationObserver等操做。當Js執行時,進行以下流程:
實例#
// Step 1 console.log(1); // Step 2 setTimeout(() => { console.log(2); Promise.resolve().then(() => { console.log(3); }); }, 0); // Step 3 new Promise((resolve, reject) => { console.log(4); resolve(); }).then(() => { console.log(5); }) // Step 4 setTimeout(() => { console.log(6); }, 0); // Step 5 console.log(7); // Step N // ... // Result /* 1 4 7 5 2 3 6 */
分析#
在瞭解異步任務的執行隊列後,回到中$nextTick
方法,當用戶數據更新時,Vue將會維護一個緩衝隊列
,對於全部的更新數據將要進行的組件渲染與DOM操做進行必定的策略處理後加入緩衝隊列,而後便會在$nextTick
方法的執行隊列中加入一個flushSchedulerQueue
方法(這個方法將會觸發在緩衝隊列的全部回調的執行),而後將$nextTick
方法的回調加入$nextTick
方法中維護的執行隊列,在異步掛載的執行隊列觸發時就會首先會首先執行flushSchedulerQueue
方法來處理DOM渲染的任務,而後再去執行$nextTick
方法構建的任務,這樣就能夠實如今$nextTick
方法中取得已渲染完成的DOM結構。
在測試的過程當中發現了一個頗有意思的現象,在上述例子中的加入兩個按鈕,在點擊updateMsg按鈕的結果是3 2 1,點擊updateMsgTest按鈕的運行結果是2 3 1。
<!DOCTYPE html> <html> <head> <title>Vue</title> </head> <body> <div id="app"></div> </body> <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { msg: 'Vue' }, template:` <div> <div ref="msgElement">{{msg}}</div> <button @click="updateMsg">updateMsg</button> <button @click="updateMsgTest">updateMsgTest</button> </div> `, methods:{ updateMsg: function(){ this.msg = "Update"; setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) this.$nextTick(() => { console.log(3) }) }, updateMsgTest: function(){ setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) this.$nextTick(() => { console.log(3) }) } }, }) </script> </html>
這裏假設運行環境中Promise對象是徹底支持的,那麼使用setTimeout是宏隊列在最後執行這個是沒有異議的,可是使用$nextTick
方法以及自行定義的Promise實例是有執行順序的問題的,雖然都是微隊列任務,可是在Vue中具體實現的緣由致使了執行順序可能會有所不一樣,首先直接看一下$nextTick
方法的源碼,關鍵地方添加了註釋,請注意這是Vue2.4.2版本的源碼,在後期$nextTick
方法可能有所變動。
/** * Defer a task to execute it asynchronously. */ var nextTick = (function () { // 閉包 內部變量 var callbacks = []; // 執行隊列 var pending = false; // 標識,用以判斷在某個事件循環中是否爲第一次加入,第一次加入的時候才觸發異步執行的隊列掛載 var timerFunc; // 以何種方法執行掛載異步執行隊列,這裏假設Promise是徹底支持的 function nextTickHandler () { // 異步掛載的執行任務,觸發時就已經正式準備開始執行異步任務了 pending = false; // 標識置false var copies = callbacks.slice(0); // 建立副本 callbacks.length = 0; // 執行隊列置空 for (var i = 0; i < copies.length; i++) { copies[i](); // 執行 } } // 若是支持promise if (typeof Promise !== 'undefined' && isNative(Promise)) { var p = Promise.resolve(); var logError = function (err) { console.error(err); }; timerFunc = function () { p.then(nextTickHandler).catch(logError); // 掛載異步任務隊列 if (isIOS) { setTimeout(noop); } }; } else if (typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { var counter = 1; var observer = new MutationObserver(nextTickHandler); var textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = function () { counter = (counter + 1) % 2; textNode.data = String(counter); }; } else { // fallback to setTimeout /* istanbul ignore next */ timerFunc = function () { setTimeout(nextTickHandler, 0); }; } return function queueNextTick (cb, ctx) { // nextTick方法真正導出的方法 var _resolve; callbacks.push(function () { // 添加到執行隊列中 並加入異常處理 if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); //判斷在當前事件循環中是否爲第一次加入,如果第一次加入則置標識爲true並執行timerFunc函數用以掛載執行隊列到Promise // 這個標識在執行隊列中的任務將要執行時便置爲false並建立執行隊列的副本去運行執行隊列中的任務,參見nextTickHandler函數的實現 // 在當前事件循環中置標識true並掛載,而後再次調用nextTick方法時只是將任務加入到執行隊列中,直到掛載的異步任務觸發,便置標識爲false而後執行任務,再次調用nextTick方法時就是一樣的執行方式而後不斷如此往復 if (!pending) { pending = true; timerFunc(); } if (!cb && typeof Promise !== 'undefined') { return new Promise(function (resolve, reject) { _resolve = resolve; }) } } })();
回到剛纔提出的問題上,在更新DOM操做時會先觸發$nextTick
方法的回調,解決這個問題的關鍵在於誰先將異步任務掛載到Promise對象上。
首先對有數據更新的updateMsg按鈕觸發的方法進行debug,斷點設置在Vue.js的715行,版本爲2.4.2,在查看調用棧以及傳入的參數時能夠觀察到第一次執行$nextTick
方法的實際上是因爲數據更新而調用的nextTick(flushSchedulerQueue)
語句,也就是說在執行this.msg = "Update";的時候就已經觸發了第一次的$nextTick
方法,此時在$nextTick
方法中的任務隊列會首先將flushSchedulerQueue方法加入隊列並掛載$nextTick
方法的執行隊列到Promise對象上,而後纔是自行自定義的Promise.resolve().then(() => console.log(2))
語句的掛載,當執行微任務隊列中的任務時,首先會執行第一個掛載到Promise的任務,此時這個任務是運行執行隊列,這個隊列中有兩個方法,首先會運行flushSchedulerQueue方法去觸發組件的DOM渲染操做,而後再執行console.log(3),而後執行第二個微隊列的任務也就是() => console.log(2),此時微任務隊列清空,而後再去宏任務隊列執行console.log(1)。
接下來對於沒有數據更新的updateMsgTest按鈕觸發的方法進行debug,斷點設置在一樣的位置,此時沒有數據更新,那麼第一次觸發$nextTic
k方法的是自行定義的回調函數,那麼此時$nextTick
方法的執行隊列纔會被掛載到Promise對象上,很顯然在此以前自行定義的輸出2的Promise回調已經被掛載,那麼對於這個按鈕綁定的方法的執行流程即是首先執行console.log(2),而後執行$nextTick
方法閉包的執行隊列,此時執行隊列中只有一個回調函數console.log(3),此時微任務隊列清空,而後再去宏任務隊列執行console.log(1)。
簡單來講就是誰先掛載Promise對象的問題,在調用$nextTick
方法時就會將其閉包內部維護的執行隊列掛載到Promise對象,在數據更新時Vue內部首先就會執行$nextTick
方法,以後便將執行隊列掛載到了Promise對象上,其實在明白Js的Event Loop模型後,將數據更新也看作一個$nextTick
方法的調用,而且明白$nextTick
方法會一次性執行全部推入的回調,就能夠明白其執行順序的問題了,下面是一個關於$nextTick方法的最小化的DEMO。
var nextTick = (function(){ var pending = false; const callback = []; var p = Promise.resolve(); var handler = function(){ pending = true; callback.forEach(fn => fn()); } var timerFunc = function(){ p.then(handler); } return function queueNextTick(fn){ callback.push(() => fn()); if(!pending){ pending = true; timerFunc(); } } })(); (function(){ nextTick(() => console.log("觸發DOM渲染隊列的方法")); // 註釋 / 取消註釋 來查看效果 setTimeout(() => console.log(1)) Promise.resolve().then(() => console.log(2)) nextTick(() => { console.log(3) }) })();