面試題分享,修改數(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é)苑