面試題分享,修改數(shù)據(jù)無(wú)法更新UI
這道面試題大概是這樣的,在vue中,一個(gè)組件你修改了數(shù)據(jù),但是頁(yè)面沒有更新,通常是什么原因造成的。
我:嗯...,大概可能是數(shù)據(jù)流原因造成的,如果一個(gè)子組件依賴父級(jí),通常來(lái)說(shuō)如果模版里未直接引用props,而是通過(guò)子組件data中一個(gè)變量去接收props值,如果父組件更新,但是如果此時(shí)子組件不監(jiān)聽props值變化,而從新賦值的話,那么一直都會(huì)是初始化的那個(gè)值。
我:或者是當(dāng)你在使用hooks時(shí),在子組件直接使用hooks導(dǎo)出的值,而不是通過(guò)父組件傳子組件的值,你在父組件以為修改同一個(gè)hooks值時(shí),子組件的值依然不會(huì)變化。
面試官:還有其他場(chǎng)景方式嗎?
我:暫時(shí)沒想到...
面試官:現(xiàn)在子組件有一個(gè)數(shù)組,假設(shè)你初始化數(shù)組的數(shù)據(jù)里面是多個(gè)字符串?dāng)?shù)組,然后我在子組件內(nèi)部我是通過(guò)獲取索引的方式去改變的,比如你在mounted通過(guò)數(shù)組索引下標(biāo)的方式去改變,數(shù)據(jù)發(fā)生了變化,模版并不會(huì)更新,這也是一種場(chǎng)景
我:一般沒有這么做,通常如果修改的話,會(huì)考慮在計(jì)算屬性里面做,但是這種應(yīng)該可以更新吧?于是我說(shuō)了vue響應(yīng)式如何做的,我想修改數(shù)組下標(biāo)的值,為啥不是不會(huì)更新模版,不是有做對(duì)象劫持嗎?修改值不會(huì)觸發(fā)set方法嗎,只要觸發(fā)了set那么就會(huì)觸發(fā)內(nèi)部一個(gè)dep.notify去更新組件啊,這不科學(xué)啊。但事實(shí)上,如果一個(gè)數(shù)組的item是基礎(chǔ)數(shù)據(jù)類型,用數(shù)組下標(biāo)方式去修改數(shù)組值還真是不會(huì)更新模版。
于是去翻閱源碼,寫一個(gè)例子證實(shí)下。
正文開始...
開始一個(gè)例子
新建一個(gè)index.html
...
<div id="app">
<div v-for="item in dataList">{{item}}</div>
<div v-for="item in dataList2">{{item.name}}</div>
</div>
<script src="./vue.js"></script>
然后我們引入index.js
var vm = new Vue({
el: '#app',
data() {
return {
dataList: ['Maic', 'Test'],
dataList2: [
{
name: '深圳'
},
{
name: '廣州'
}
]
};
},
mounted() {
debugger;
this.dataList[0] = '111';
}
});
我們?cè)趍ounted中寫入了一行調(diào)試代碼,并且我們用數(shù)組索引改變dataList[0]選項(xiàng)的值
因?yàn)樵O(shè)置值肯定有改變數(shù)據(jù)的攔截,所以我在源碼的defineReactive$$1也寫入一行debugger
打開頁(yè)面,我們可以看到
我們從第一行源碼到defineReactive$$1方法的debugger分析進(jìn)行逐步分析
首先是實(shí)例new Vue(options),實(shí)際上Vue就是下面的一個(gè)Vue$3構(gòu)造函數(shù),當(dāng)傳入options,此時(shí)會(huì)調(diào)用_init方法并傳入options,這個(gè)options就是
// 以下就是Vue構(gòu)造函數(shù)中的options
/*
{
el: '#app',
data() {
return {
}
},
mounted() {
}
}
*/
function Vue$3(options) {
if ("development" !== 'production' &&
!(this instanceof Vue$3)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
然后我們會(huì)發(fā)現(xiàn)_init是掛載在Vue$3.prototype._init上,實(shí)際當(dāng)我們一new Vue()時(shí),就會(huì)執(zhí)行_init方法,而_init方法,主要做了以下幾件事情
1、為每一個(gè)實(shí)例vm對(duì)象綁定了一個(gè)uid
2、判斷傳入的options中是否含有component,注冊(cè)這個(gè)傳入的組件
3、合并options對(duì)象,并且會(huì)將傳入的options動(dòng)態(tài)綁定到$options中去
4、劫持options這個(gè)傳入的對(duì)象,將這個(gè)傳入的對(duì)象通過(guò)new Proxy(vm),從而綁定在vm._renderProxy這個(gè)對(duì)象上
5、動(dòng)態(tài)綁定_self屬性并指向vm實(shí)例對(duì)象
6、在_init方法干的最重要的幾件事
initLifecycle(vm)主要是綁定一些自定義接口,比如你常常用this訪問(wèn)$children、$parent、$refs,_watcher等
initEvents(vm)這個(gè)方法主要是事件的更新監(jiān)聽
callHook(vm, 'beforeCreate'),主要執(zhí)行Vue指定的鉤子函數(shù)beforeCreate
當(dāng)執(zhí)行breforeCreate之后,那么此時(shí)就是進(jìn)入initState(vm),這時(shí)對(duì)傳入的options的數(shù)據(jù)進(jìn)行響應(yīng)式初始化操作
數(shù)據(jù)進(jìn)行劫持,響應(yīng)式后,就是執(zhí)行callHook(vm, 'created')
調(diào)用initRender(vm)方法更新頁(yè)面
具體代碼可以參考以下
...
initLifecycle(vm) //
initEvents(vm)
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
initRender(vm)
我們依次從執(zhí)行棧中去尋找真相
當(dāng)調(diào)用initState方法后,此時(shí)會(huì)進(jìn)入initData方法
在initData主要做什么呢?
1、主要是獲取傳入的data,并且對(duì)傳入的data做了一些兼容處理,可以是函數(shù),也可以是對(duì)象,并且對(duì)data必須返回一個(gè)對(duì)象做了防御性處理
function initData(vm) {
var data = vm.$options.data
data = vm._data = typeof data === 'function'
? data.call(vm)
: data || {}
}
對(duì)傳入的data中的屬性進(jìn)行proxy劫持處理,將data是兩個(gè)數(shù)組dataList,dataList2直接掛在了vm對(duì)象上,所以我們?cè)趘ue中都是直接this.dataList,this.dataList2,或者能訪問(wèn)methods的一些方法,就是這里在初始化的時(shí)候,進(jìn)行了proxy,主要看下面這個(gè)proxy方法
function initData(vm) {
...
// proxy data on instance
var keys = Object.keys(data)
var props = vm.$options.props
var i = keys.length
while (i--) {
if (props && hasOwn(props, keys[i])) {
"development" !== 'production' && warn(
"The data property \"" + (keys[i]) + "\" is already declared as a prop. " +
"Use prop default value instead.",
vm
)
} else {
proxy(vm, keys[i])
}
}
// observe data
observe(data)
data.__ob__ && data.__ob__.vmCount++
}
當(dāng)對(duì)data中的屬性進(jìn)行一一proxy后,此時(shí)我們看到有有進(jìn)行observer(data)這個(gè)操作
observer這是一個(gè)非常重要的方法,所有data中的數(shù)據(jù)在初始化時(shí)候,都會(huì)被放入new Observer(value)中去
我們具體看下observe這個(gè)方法
/* value 就是
{
dataList: ['Maic', 'Test'],
dataList2: [{}, {}]
}
*/
function observe(value) {
if (!isObject(value)) {
return
}
// debugger;
var ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!config._isServer &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
return ob
}
進(jìn)入new Observer()中,我們可以看到以下代碼
var Observer = function Observer(value) {
/*
value:
{
dataList: ['Maic','Test'],
dataList2: [{}]
}
*/
// debugger;
this.value = value // data中返回的值
// 動(dòng)態(tài)綁定一個(gè)dep對(duì)象
this.dep = new Dep()
this.vmCount = 0
// 主要會(huì)將value值copy到this的__ob__
def(value, '__ob__', this)
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
};
從以上這段代碼中首先每一個(gè)傳入的對(duì)象會(huì)有一個(gè)this.dep = new Dep(),每一個(gè)對(duì)象都會(huì)有一個(gè)dep對(duì)象
首先會(huì)判斷傳入的value是不是一個(gè)對(duì)象,如果是對(duì)象就會(huì)走walk方法
walk方法的作用就是遍歷傳入的value,然后將value變成一個(gè)響應(yīng)式的對(duì)象,用defineReactive$$1來(lái)劫持每個(gè)對(duì)象
// walk
Observer.prototype.walk = function walk(obj) {
var keys = Object.keys(obj)
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i], obj[keys[i]])
}
};
此時(shí)當(dāng)我們進(jìn)入defineReactive$$1后
我們會(huì)發(fā)現(xiàn),對(duì)于{dataList: ['Maic', 'Test']},首先會(huì)遍歷dataList,獲取dataList的值,然后把數(shù)組的值進(jìn)行observe,在observe中,我們可以看到,如果這個(gè)值不是對(duì)象,直接通過(guò)isObject方法進(jìn)行return了,?那么不會(huì)被Observer
function observe(value) {
// 這行代碼是根據(jù)數(shù)組索引修改值,不會(huì)更新的根本原因
if (!isObject(value)) {
return
}
// debugger;
var ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!config._isServer &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
return ob
}
并且每個(gè)值都會(huì)有一個(gè)有一個(gè)對(duì)應(yīng)的dep = new Dep(),在訪問(wèn)對(duì)象時(shí)會(huì)調(diào)用depend方法進(jìn)行依賴收集
每一個(gè)對(duì)象都有一個(gè)dep對(duì)象,在dep對(duì)象的subs中就會(huì)添加一個(gè)watch
當(dāng)從_init方法調(diào)用的,到數(shù)據(jù)初始化完成響應(yīng)式攔截后,initState走完了,然后就是callHook(vm, 'created'),最后initRender(vm),然后就走到了我們?cè)趍ounted方法debugger的位置
我們繼續(xù)下一步,此時(shí)我們會(huì)走到修改數(shù)組
當(dāng)我們直接進(jìn)行下面操作
this.dataList[0] = "111";
首先會(huì)通過(guò)proxy方法,直接可以從vm對(duì)象data中獲取dataList值
function proxy(vm, key) {
if (!isReserved(key)) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get: function proxyGetter() {
return vm._data[key]
},
set: function proxySetter(val) {
vm._data[key] = val
}
})
}
}
由于dataList在初始化的時(shí)候,數(shù)組中每一項(xiàng)都會(huì)先進(jìn)行循環(huán),如果是對(duì)象,則會(huì)遍歷數(shù)組內(nèi)部的對(duì)象,然后添加響應(yīng)式,每一項(xiàng)都會(huì)dep依賴
但是由于dataList的每一項(xiàng)是數(shù)組字符串,我們可以繼續(xù)看到這段代碼
var Observer = function Observer(value) {
// debugger;
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// 由于dataList是數(shù)組
if (Array.isArray(value)) {
var augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
// 遍歷數(shù)組
this.observeArray(value)
} else {
this.walk(value)
}
};
看下observeArray,observe每一項(xiàng)
Observer.prototype.observeArray = function observeArray(items) {
for (var i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
};
然后看observe
function observe(value) {
if (!isObject(value)) {
return
}
// debugger;
var ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!config._isServer &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
return ob
}
只有每一項(xiàng)被new Observer后,就會(huì)去調(diào)用walk,然后繼續(xù)defineReactive$$1,這樣每一項(xiàng)item就被Object.defineProperty攔截了。
此時(shí)如果是對(duì)象,當(dāng)你對(duì)數(shù)組的item對(duì)象進(jìn)行修改時(shí),就會(huì)觸發(fā)set進(jìn)而更新頁(yè)面了。
所以你修改this.dataList[0] = "111";,因?yàn)閐ataList的每一項(xiàng)item并不是一個(gè)對(duì)象,并沒有被observer,所以修改其值,只是改變對(duì)原對(duì)象值,但是根本不會(huì)觸?發(fā)攔截對(duì)象的set方法,自然就不會(huì)dep.notify()去派發(fā)更新,觸發(fā)頁(yè)面更新了
并沒有更新頁(yè)面
于是當(dāng)你這樣處理時(shí)
...
mounted() {
debugger;
this.dataList[0] = "111";
this.dataList2[0].name = '北京';
},
你會(huì)發(fā)現(xiàn),頁(yè)面會(huì)更新了,但是實(shí)際上修改dataList并不會(huì)立即更新頁(yè)面,會(huì)等dataList2[0]修改了,批量更新
所以當(dāng)修改dataList2[0].name執(zhí)行完畢后
已經(jīng)可以看到頁(yè)面更改了
另外你看到下面可能會(huì)疑惑
...
data() {
return {
test: "Web技術(shù)學(xué)苑",
dataList: ["Maic", "Test"],
dataList2: [
{
name: "深圳",
},
{
name: "廣州",
},
],
};
},
我在data中申明了一個(gè)test他的值也是字符串,不是對(duì)象啊,那么為什么我直接修改,也可以更新數(shù)據(jù)呢
mounted() {
debugger;
this.dataList[0] = "111";
this.test = "前端早早聊";
},
這樣你會(huì)發(fā)現(xiàn)this.test直接訪問(wèn)了data的數(shù)據(jù),并且修改了test的數(shù)據(jù)。
其實(shí)當(dāng)你修改test時(shí),本質(zhì)就會(huì)觸發(fā)vm對(duì)象,這個(gè)this就是那個(gè)實(shí)例對(duì)象,因?yàn)閷?shí)例對(duì)象在初始化的時(shí)候,這個(gè)對(duì)象就已經(jīng)被Observer,所以當(dāng)你修改test就是在設(shè)置實(shí)例化對(duì)象上的屬性,自然就會(huì)觸發(fā)set所以頁(yè)面就更新了。
如果你直接修改this.dataList = ['aa', 'bb'],那么也是可以更新數(shù)據(jù)的,因?yàn)榇藭r(shí)dataList是綁定在實(shí)例化對(duì)象上的,這個(gè)dataList已經(jīng)被proxy處理直接掛載了this對(duì)象上,而這個(gè)this對(duì)象也是被Observer了,所以你修改其值,自然就會(huì)觸發(fā)set,所以頁(yè)面就會(huì)更新
在vue中,initState的時(shí)候,會(huì)將data中的所有數(shù)據(jù)變成響應(yīng)式,每一個(gè)屬性對(duì)象都會(huì)有一個(gè)dep,當(dāng)這個(gè)屬性值是數(shù)組時(shí),會(huì)對(duì)數(shù)組進(jìn)行遍歷,如果數(shù)組的每項(xiàng)是引用數(shù)據(jù)類型,那么每一項(xiàng)都會(huì)被Observer,數(shù)組的每一項(xiàng)都會(huì)增加一個(gè)dep對(duì)象,當(dāng)數(shù)據(jù)更新時(shí),會(huì)派發(fā)更新所有的數(shù)據(jù)。
總結(jié)
當(dāng)一個(gè)組件數(shù)據(jù)發(fā)生了變化,但是視圖層沒有發(fā)生變化,形成的原因只有以下幾種
1、 數(shù)據(jù)流的問(wèn)題,如果一個(gè)子組件的props數(shù)據(jù)時(shí)直接通過(guò)子組件data中去接收props,當(dāng)修改負(fù)組件props時(shí),如果子組件不監(jiān)聽props,重新對(duì)data賦值那么可能會(huì)導(dǎo)致子組件數(shù)據(jù)并不會(huì)更新
2、 如果使用hooks,如果并不會(huì)是從負(fù)組件傳入的props,而是重新在子組件重新引入hooks,在負(fù)組件你修改同一份hooks引用,子組件并不會(huì)有效果,因?yàn)閔ooks每次調(diào)用都會(huì)時(shí)一份新的引用,所以子組件只能從props接口獲取
當(dāng)一個(gè)數(shù)組的每一個(gè)item并不是對(duì)象時(shí),其實(shí)此時(shí)item并不是一個(gè)響應(yīng)式,并不會(huì)被Observe,在data初始化的每一個(gè)對(duì)象vue初始化時(shí),都會(huì)給每一個(gè)對(duì)象變成reactive,并且每一個(gè)對(duì)象會(huì)有一個(gè)dep對(duì)象。只有被Observer,修改其值才會(huì)觸發(fā)set,從而更新視圖層
我們每一個(gè)data中返回的對(duì)象的值都會(huì)被Observer,每一個(gè)數(shù)組對(duì)象在初始化時(shí)都會(huì)被Observer,數(shù)組中的每一個(gè)對(duì)象都會(huì)添加一個(gè)dep對(duì)象,當(dāng)數(shù)組對(duì)象發(fā)生變化時(shí),就會(huì)觸發(fā)對(duì)象攔截,更新操作。如果數(shù)組中的每一項(xiàng)是基礎(chǔ)數(shù)據(jù)類型,那么通過(guò)索引方式修改其值并不會(huì)觸發(fā)更新UI?
code example[1]
參考資料
[1]
code example: https://github.com/maicFir/lessonNote/tree/master/vue/03-數(shù)組響應(yīng)式測(cè)試
最后,看完覺得有收獲的,點(diǎn)個(gè)贊,在看,轉(zhuǎn)發(fā),收藏等于學(xué)會(huì),歡迎關(guān)注Web技術(shù)學(xué)苑,好好學(xué)習(xí),天天向上!
作者:Maic
歡迎關(guān)注微信公眾號(hào) :web技術(shù)學(xué)苑