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