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