1.更改數據後,頁面會立刻重新渲染嗎?
數據變化,頁面就會重新渲染。VUE更新DOM操作是異步執行的,只要偵聽到數據變化就會開啓一個異步隊列,同步執行棧執行完畢後會執行異步執行棧,隊列中微任務先執行。
2.如何在更改數據後,看到渲染後頁面上的值?
利用vm.$nextTick
或Vue.nextTick
頁面重新渲染,DOM更新後,會立刻執行vm.$nextTick
△兩者的區別:vue.nextTick
內部函數的this指向window,vm.$nextTick
內部函數的this指向vue實例對象。
nextTick是怎樣實現的?
nextTick是在下次DOM更新循環結束之後執行延遲迴調,在Vue更新Dom操作是異步執行的,分爲宏任務和微任務,微任務會先執行,在nextTick實現中,會先判斷是否執行微任務,不支持的話,纔會執行宏任務,他根據執行環境分別嘗試採用Promise,MutationObserve,setImmedate,如果以上都不行採用setTimeout.
if(typeof Promise !== 'undefined') {
// 微任務
// 首先看一下瀏覽器中有沒有promise
// 因爲IE瀏覽器中不能執行Promise
const p = Promise.resolve();
} else if(typeof MutationObserver !== 'undefined') {
// 微任務
// 突變觀察
// 監聽文檔中文字的變化,如果文字有變化,就會執行回調
// vue的具體做法是:創建一個假節點,然後讓這個假節點稍微改動一下,就會執行對應的函數
} else if(typeof setImmediate !== 'undefined') {
// 宏任務
// 只在IE下有
} else {
// 宏任務
// 如果上面都不能執行,那麼則會調用setTimeout
}
3.插值表達式{{ }}中可以寫什麼?
1.將vue中的數據寫在插值表達式中
2.直接填寫數據值(數字,布爾值,字符串,undefined,null,數組,對象)
3.寫表達式,運算表達式,邏輯表達式,三元表達式
不能寫什麼?
不可以寫語句,流程控制。
記住:插值表達式中,可以寫:data、js數據、表達式,其他的想都不要想。
注意,只要插值表達式中使用了數據,必須在data中聲明過,否則會報錯(有一種可能不報錯,是寫了在原型鏈上找不到的,之爲undefined,就不報錯啦);
4.除了未被聲明,未被渲染的數據外,還有什麼數據更改後不會渲染頁面?
1)利用索引直接設置一個數組項的時候
2)修改數組長度
3)添加刪除對象
那如何響應式的更新數組和對象?
更改數組:
1. 利用數組變異方法:push、pop、shift、unshift、splice、sort、reverse
2. 利用vm.$set/Vue.set實例方法
3. 利用vm.$set或Vue.set刪除數組中的某一項
更改對象:
vm.$set是Vue.set的別名
使用方法:Vue.set(object, propertyName, value),也就是這個意思:Vue.set(要改誰,改它的什麼,改成啥)
vm.$delete是Vue.delete的別名
使用方法:Vue.delete(object, target),也就是這個意思:Vue.delete(要刪除誰的值,刪除哪個)
爲什麼不會被渲染上頁面?
vue 2.x利用object.defineProperty實現響應式
const data = {
name: 'shanshan',
age: 18,
shan: {
name: 'shanshan',
age: 18,
obj: {}
},
arr: [1, 2, 3]
}
const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift' ,'sort', 'splice', 'reverse'].forEach(method => {
arrayMethods[method] = function () {
arrayProto[method].call(this, ...arguments);
render();
}
})
function defineReactive (data, key, value) {
observer(value);
Object.defineProperty(data, key, {
get () {
return value;
},
set (newVal) {
if(value === newVal) {
return;
}
value = newVal;
render();
}
})
}
function observer (data) {
if(Array.isArray(data)) {
data.__proto__ = arrayMethods;
return;
}
if(typeof data === 'object') {
for(let key in data) {
defineReactive(data, key, data[key])
}
}
}
function render () {
console.log('頁面渲染啦');
}
function $set (data, key, value) {
if(Array.isArray(data)) {
data.splice(key, 1, value);
return value;
}
defineReactive(data, key, value);
render();
return value;
}
function $delete(data, key) {
if(Array.isArray(data)) {
data.splice(key, 1);
return;
}
delete data[key];
render();
}
observer(data);
缺點:
(1)需要遞歸查詢
(2)監聽不到數組不存在的索引的改變
(3)監聽不到數組長度的改變
(4)監聽不到對象的增刪
簡單說一下Vue2.x響應式數據原理
Vue在初始化數據時,會使用Object.defineProperty重新定義data中的所有屬性,當頁面使用對應屬性時,首先
會進行依賴收集(收集當前組件的watcher)如果屬性發生變化會通知相關依賴進行更新操作(發佈訂閱)。
那你知道Vue3.x響應式數據原理嗎?
Vue3.x改用Proxy替代Object.defineProperty。因爲Proxy可以直接監聽對象和數組的變化,
並且有多達13種攔截方法。並且作爲新標準將受到瀏覽器廠商重點持續的性能優化。
Proxy只會代理對象的第一層,那麼Vue3又是怎樣處理這個問題的呢?
判斷當前Reflect.get的返回值是否爲Object,如果是則再通過reactive方法做代理,
這樣就實現了深度觀測。
❝
監測數組的時候可能觸發多次get/set,那麼如何防止觸發多次呢?
❞
我們可以判斷key是否爲當前被代理對象target自身屬性,也可以判斷舊值與新值是否相等,只有滿足以上兩個條件之一時,纔有可能執行trigger。
再說一下vue2.x中如何監測數組變化?
使用了函數劫持的方式,重寫了數組的方法,Vue將data中的數組進行了原型鏈重寫,指向了自己定義的數組原型
方法。這樣當調用數組api時,可以通知依賴更新。如果數組中包含着引用類型,會對數組中的引用類型再次遞歸
遍歷進行監控。這樣就實現了監測數組變化。
5.爲什麼不用index作爲數組的key值
因爲數組改變的時候頁面會重新渲染,Vue會根據key值來判斷需不需要移動元素,用索引作key的話,頁面重新渲染後,元素的key值會被重新賦值。Vue會對比渲染前後擁有同樣key的元素,如果發現有變動,就會再生成一個元素,如果用索引作爲key值,假如你reverse之後,所有元素會被重新生成。
6.處理複雜邏輯,計算屬性(computed)和方法(method)最本質的區別;
計算屬性是基於響應式依賴進行緩存的,計算屬性的值一直存在與緩存中(緩存可以減少開銷),只要他依賴的data數據不變,每次訪問計算屬性,會like返回緩存的結果,而不是再次執行函數。而方法是每次觸發新的渲染,調用方法鍾會再次執行函數。
7.偵聽器(watch) VS 計算屬性(computed)
computed:本質是一個具備緩存的watcher,依賴的屬性發生變化就會更新視圖,適用於多個數據影響一個數據,比較消耗性能的計算場景。當表達式過於複雜時,模板中放過多邏輯會讓模板難以維護,可以將複雜的邏輯放入計算屬性中。
watch:沒有緩存性,更多的是觀察作用,監聽某些數據,執行回調,深度監聽對象的屬性的時候(deep:true),便於對對象中的每一個屬性進行監聽,一個數據影響多個數據。
(1)都可以觀察和響應VUE實例的數據變動
(2)watch:一個數據影響多個數據;計算屬性:多個數據影響一個數據
(3)偵聽器中可以異步執行,計算屬性不可以。
8.組件template中的data爲什麼是一個函數?
return{
data:
}
一個組件被複用多次會創建多個實例。本質上這些實例用的都是同一個構造函數,若data是對象,對象屬於引用類型,會影響到所有實例,是函數的話,每個實例可以維護一份被返回的對象的獨立拷貝,他可以保證組件不通實例之間的data不衝突。
9.v-text VS Mustache
<span v-text="msg"></span>
<!-- 和下面的一樣 -->
<span>{{msg}}</span>
區別:
v-text替換元素中所有的文本,Mustache只替換自己,不清空元素內容
v-text 優先級高於 {{ }}
10.textContent VS innerText
設置文本替換時,兩者都會把指定節點下的所有子節點也一併替換掉。
textContent 會獲取所有元素的內容,包括 <script>
和 <style>
元素,然而 innerText 不會。
innerText 受 CSS 樣式的影響,並且不會返回隱藏元素的文本,而textContent會。
由於 innerText 受 CSS 樣式的影響,它會觸發重排(reflow),但textContent 不會。
innerText 不是標準制定出來的 api,而是IE引入的,所以對IE支持更友好。textContent雖然作爲標準方法但是隻支持IE8+以上的瀏覽器,在最新的瀏覽器中,兩個都可以使用。
綜上,Vue這裏使用textContent是從性能的角度考慮的。
11.阻止默認事件
注意
使用修飾符時,順序很重要。相應的代碼會以同樣的順序產生。因此,
v-on:click.prevent.self 會阻止所有的點擊的默認事件
v-on:click.self.prevent 只會阻止對元素自身點擊的默認事件
不要把 .passive 和 .prevent 一起使用,因爲 .prevent 將會被忽略,同時瀏覽器可能會向你展示一個警告。
12.v-model原理
雙向數據綁定,本質是一個語法糖,可以看成是input+value方法的語法糖。可以根據model屬性的prop和event屬性來進行自定義。
VUE事件綁定原理:原生事件綁定是通過addeventlistener綁定給真實元素。
組件事件綁定是通過Vue自定義的$on實現的
手寫v-model
步驟:(1)通過綁定的數據給元素設置value(2)觸發input事件,去更改數據的值(3)
更改數據後,同步input的value值。
Vue.directive('mymodel',{
bind(el,binding,vnode){
const vm = vnode.context;
const{value,expression} = binding;
el.value = value;
el.oninput = function(e){
const inputVal = el.value;
vm[expression] = inputVal;
}
},
update(el,binding){
const {value} = binding;
el.value = value
}
})
13.key值的唯一性
key就是在更新組件時,判斷兩個節點是否相同,相同就複用,不同就創建新的刪除舊的。(強制更新組件,避免原地複用帶來的副作用)
不用key:
就地複用節點,a.key和b.key都是undefined,所以不會重新創建和刪除節點,只會在節點的屬性層面上進行比較和更新。
無法維持組件的狀態:動態效果啊開關等狀態
性能下降:要複用很多節點,順序和原來的完全不同,創建和刪除節點的數量就會比帶key的時候增加很多。
不建議將數組的索引作爲key值:當改變數組時,頁面會重新渲染,Vue會根據key值來判斷要不要移動元素。例如使用reverse當頁面重新渲染時,當使用數組的索引作爲key值時,頁面重新渲染後,元素的key值會重新被賦值。Vue會比對渲染前後擁有同樣key的元素,發現有變動,就會再生成一個元素,如果用索引作key值得話,那麼此時,所有的元素都會被重新生成。
14.v-for和v-if不能一起使用
v-for 比 v-if 具有更高的優先級,也許會在重新渲染的時候遍歷整個列表
15. .v-model VS .sync
Vue 1.x :.sync可以在子組件中修改父組件的狀態,會造成整個狀態的變換很難追溯。
vue 2.0 : 移除
vue 3.0 : 迴歸,語法糖和v-model實現原理一樣
(1)兩個都是用來實現雙向數據傳遞的,都是語法糖,最終通過prop + 事件來達成目的。
(2)當一個組件對外只暴露一個受控狀態,並且都符合統一標準的時候,用v-model來處理,.sync更靈活,雙向數據傳遞都可以使用。
16.keep-alive
包裹動態組件的時候,會緩存不活動的組件實例,而不是銷燬它們。keep-alive是一個抽象組件;他自身不會渲染一個DOM元素,也不會出現在父組件中。
常用的兩個屬性:include/exclude 允許組件有條件進行緩存。
兩個生命週期:actived/deactiveted 得知當前組件是否處於活躍狀態。
運用了LRU算法。
17.組建通信
父子:(1)prop
:父組件傳遞給子組件時;
(2)$emit
,$on
:子組件傳遞給父組件時;
(3)ref:父組件中訪問子實例的數據;$ref
只會在組件渲染完成之後生效
(4)provide & inject :祖先組件提供數據(provide),子組件按需注入(inject),會將組件的阻止方式耦合在一起,使組件重構困難,不推薦。
兄弟組件:(1)eventBus(事件總線)
(1)$attrs
:祖先組件 ->子孫組件【撰寫基礎組件】
(2)$listenner
:子孫組件中執行祖先組件的函數【監聽器】
(3)$root
:子組件中訪問根實例
跨級組建通信:$attr
.$listener
provide
inject
18.Vue生命週期
(1)beforeCreate:new vue()之後觸發的第一各鉤子,當前階段data,methods,computed,watch上的數據和方法都不能訪問。
(2)created:實例創建完成之後發生,完成了數據觀測,可以使用數據,更改數據,在這裏更改數據不會觸發update,可以做初始數據的獲取,無法與Dom進行交互(非想交互可以通過vm.$nextTick來訪問
)
(3)beforeMount:發生在掛在之前,在這之前template
模板已導入,渲染函數編譯。DOM穿件完成,即將開始渲染,此時可以更改數據,不會觸發update。
(4)mouted:掛載完成之後發生,真實DOM掛載完畢,數據完成雙向綁定,可以訪問到DOM節點,使用$refs
屬性。
(5)beforeUpdate:更新之前,響應式數據發生更新,虛擬dom重新渲染之前被觸發,可以在這個階段進行更改數據,不會被渲染。
(6)Updated:更新完成之後,dom已經完成更新,避免在此進行更改數據,可能會導致無限循環。
(7)beforeDestory:實例銷燬前,實例被使用,收尾(清楚定時器)
(8)destroyed:實例銷燬之後,只剩dom空殼,組件被拆解,數據綁定被卸除,監聽被移除,子例銷燬。
△除了beforeCreate和Create鉤子之外,其他鉤子均在服務器端渲染期間不被調用。
updated不要修改data裏面賦值的數據,否則會導致死循環。
①beforeCreate鉤子調用在initState之前,initState作用是對props,methods,data,computed,watch等做初始化處理。
②mount組件核心是先實例化一個渲染watcher在他的回調函數中調用了updatacomponent方法,在執行vm.render()函數渲染虛擬節點之前,把虛擬節點patch到真實Dom後,執行mouted鉤子。
③接口請求一般放在mounted中,但是服務端渲染時不支持mounted,需要放到eated中。
19.什麼是虛擬DOM
MVC MVP架構模式希望可以從代碼組織方式來降低維護複雜引用程序的難度,DOM還是要操作只是將數據,操作,視圖分離了。所以有了MVVM:只要在模板中聲明試圖組件是和什麼數據綁定的,雙向綁定引擎就會在狀態更新的時候自動更新視圖。MVVM會降低我們的維護狀態,大大減少了代碼中的試圖更新邏輯。
什麼是虛擬DOM?
創建虛擬DOM就是爲了更高效,頻繁的更新DOM。是一個常規的JS對象。
原理:
用新渲染的對象樹去和舊的對比,記錄這兩棵樹的差異。記錄下來的不同就是我們需要對頁面真正的DOM操作,然後把它們應用到真正的DOM樹上,頁面就變更了。這樣就可做到:視圖的結構整個全新渲染了,最後操作DOM的時候只變更有不同的地方。
即Virtual Dom算法:
用JS對象結構表示DOM樹結構,然後用這個樹構建一個真正的DOM樹,插到文檔中;當狀態變更的時候,重新構造一顆新的對象樹,用新的樹和舊的樹進行比較,記錄兩棵樹的差異,把差異應用到真正的DOM樹上,視圖就更新了。
虛擬DOM本質上是在JS和DOM上做了一個緩存。
虛擬DOM核心:diff算法
在實際的代碼中,會對新舊兩棵樹進行深度的遍歷,給每一個節點進行標
記。然後在新舊兩棵樹的對比中,將不同的地方記錄下來。
function diff(oldTree, newTree) {
var index = 0 // 當前節點的標誌
var patches = {} // 記錄每個節點差異的地方
dfsWalk(oldTree, newTree, index, patches)
return patches
}
function dfsWalk(oldNode, newNode, index, patches) {
// 對比 newNode 和 oldNode 的差異地方進行記錄
patches[index] = [...]
diffChildren(oldNode.children, newNode.children, index, patches)
}
function diffChildren(oldChildren, newChildren, index, patches) {
let leftNode = null
var currentNodeIndex = index
oldChildren.forEach((child, i) => {
var newChild = newChildren[i]
currentNodeIndex = (leftNode && leftNode.count) // 計算節點的
標記
? currentNodeIndex + leftNode.count + 1
: currentNodeIndex + 1
dfsWalk(child, newChild, currentNodeIndex, patches) // 遍歷子節
點
leftNode = child
})
}
同理,有 p 是 patches[1], ul 是 patches[3],以此類推
patches 指的是差異變化,這些差異包括:
1、節點類型的不同,
2、節點類型相同,但是屬性值不同,文本內容不同。
所以有這麼幾種類型:
var REPLACE = 0, // replace 替換
REORDER = 1, // reorder 父節點中子節點的操作
PROPS = 2, // props 屬性的變化
TEXT = 3 // text 文本內容的變化
如果節點類型不同,就說明是需要替換,例如將 div 替換成 section,就記錄
渡一教育
9
下差異:
patches[0] = [{
type: REPLACE,
node: newNode // section
},{
type: PROPS,
props: {
id: 'container'
}
}]
在標題二中構建了真正的 DOM 樹的信息,所以先對那一棵 DOM 樹進行
深度優先的遍歷,遍歷的時候同
patches 對象進行對比,找到其中的差異,然後應用到 DOM 操作中。
function patch(node, patches) {
var walker = {index: 0} // 記錄當前節點的標誌
dfsWalk(node, walker, patches)
}
function dfsWalk(node, walker, patches) {
var currentPatches = patches[walker.index] // 這是當前節點的差異
var len = node.childNodes
? node.childNodes.length
: 0
for (var i = 0; i < len; i++) { // 深度遍歷子節點
var child = node.childNodes[i]
walker.index++
dfsWalk(child, walker, patches)
}
if (currentPatches) {
applyPatches(node, currentPatches) // 對當前節點進行 DOM 操作
} }
// 將差異的部分應用到 DOM 中
function applyPatches(node, currentPatches) {
currentPatches.forEach((currentPatch) => {
switch (currentPatch.type) {
case REPLACE:
var newNode = (typeof currentPatch.node === 'string')
? document.createTextNode(currentPatch.node)
: currentPatch.node.render()
node.parentNode.replaceChild(newNode, node)
break;
case REORDER:
reorderChldren(node, currentPatch.moves)
break
case PROPS:
setProps(node, currentPatch.props)
break
case TEXT:
if (node.textContent) {
node.textContent = currentPatch.content
} else {
node.nodeValue = currentPatch.content
}
break
default:
throw new Error('Unknown patch type ' +
currentPatch.type)
}
})
}