面试题分享,修改数据无法更新UI

简介: 面试题分享,修改数据无法更新UI

这道面试题大概是这样的,在vue中,一个组件你修改了数据,但是页面没有更新,通常是什么原因造成的。


我:嗯...,大概可能是数据流原因造成的,如果一个子组件依赖父级,通常来说如果模版里未直接引用props,而是通过子组件data中一个变量去接收props值,如果父组件更新,但是如果此时子组件不监听props值变化,而从新赋值的话,那么一直都会是初始化的那个值。


我:或者是当你在使用hooks时,在子组件直接使用hooks导出的值,而不是通过父组件传子组件的值,你在父组件以为修改同一个hooks值时,子组件的值依然不会变化。


面试官:还有其他场景方式吗?


我:暂时没想到...


面试官:现在子组件有一个数组,假设你初始化数组的数据里面是多个字符串数组,然后我在子组件内部我是通过获取索引的方式去改变的,比如你在mounted通过数组索引下标的方式去改变,数据发生了变化,模版并不会更新,这也是一种场景


我:一般没有这么做,通常如果修改的话,会考虑在计算属性里面做,但是这种应该可以更新吧?于是我说了vue响应式如何做的,我想修改数组下标的值,为啥不是不会更新模版,不是有做对象劫持吗?修改值不会触发set方法吗,只要触发了set那么就会触发内部一个dep.notify去更新组件啊,这不科学啊。但事实上,如果一个数组的item是基础数据类型,用数组下标方式去修改数组值还真是不会更新模版。


于是去翻阅源码,写一个例子证实下。


正文开始...


开始一个例子


新建一个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中写入了一行调试代码,并且我们用数组索引改变dataList[0]选项的值


因为设置值肯定有改变数据的拦截,所以我在源码的defineReactive$$1也写入一行debugger


打开页面,我们可以看到

752bc110c89f54a77b47e5397399f09d.png

我们从第一行源码到defineReactive$$1方法的debugger分析进行逐步分析


  • 首先是实例new Vue(options),实际上Vue就是下面的一个Vue$3构造函数,当传入options,此时会调用_init方法并传入options,这个options就是


// 以下就是Vue构造函数中的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)
}

然后我们会发现_init是挂载在Vue$3.prototype._init上,实际当我们一new Vue()时,就会执行_init方法,而_init方法,主要做了以下几件事情


  • 1、为每一个实例vm对象绑定了一个uid
  • 2、判断传入的options中是否含有component,注册这个传入的组件
  • 3、合并options对象,并且会将传入的options动态绑定到$options中去
  • 4、劫持options这个传入的对象,将这个传入的对象通过new Proxy(vm),从而绑定在vm._renderProxy这个对象上
  • 5、动态绑定_self属性并指向vm实例对象
  • 6、在_init方法干的最重要的几件事
  • initLifecycle(vm)主要是绑定一些自定义接口,比如你常常用this访问$children$parent$refs,_watcher
  • initEvents(vm)这个方法主要是事件的更新监听
  • callHook(vm, 'beforeCreate'),主要执行Vue指定的钩子函数beforeCreate
  • 当执行breforeCreate之后,那么此时就是进入initState(vm),这时对传入的options的数据进行响应式初始化操作
  • 数据进行劫持,响应式后,就是执行callHook(vm, 'created')
  • 调用initRender(vm)方法更新页面


具体代码可以参考以下


...
initLifecycle(vm) // 
initEvents(vm)
callHook(vm, 'beforeCreate')
initState(vm)
callHook(vm, 'created')
initRender(vm)

我们依次从执行栈中去寻找真相

a807bcd806ff76e4949fa86e241b6f56.png

当调用initState方法后,此时会进入initData方法


initData主要做什么呢?


  • 1、主要是获取传入的data,并且对传入的data做了一些兼容处理,可以是函数,也可以是对象,并且对data必须返回一个对象做了防御性处理


function initData(vm) {
    var data = vm.$options.data
    data = vm._data = typeof data === 'function'
      ? data.call(vm)
      : data || {}
  }
  • 对传入的data中的属性进行proxy劫持处理,将data是两个数组dataListdataList2直接挂在了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中的数据在初始化时候,都会被放入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中返回的值
    // 动态绑定一个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变成一个响应式的对象,用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


我们会发现,对于{dataList: ['Maic', 'Test']},首先会遍历dataList,获取dataList的值,然后把数组的值进行observe,在observe中,我们可以看到,如果这个值不是对象,直接通过isObject方法进行return了,那么不会被Observer


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
  }

并且每个值都会有一个有一个对应的dep = new Dep(),在访问对象时会调用depend方法进行依赖收集

9cb1599941172b6cbc9cce962cd64c07.png

每一个对象都有一个dep对象,在dep对象的subs中就会添加一个watch


当从_init方法调用的,到数据初始化完成响应式拦截后,initState走完了,然后就是callHook(vm, 'created'),最后initRender(vm),然后就走到了我们在mounted方法debugger的位置

51bfc9b8b40ab520b79529666a1230a8.png

我们继续下一步,此时我们会走到修改数组


当我们直接进行下面操作


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在初始化的时候,数组中每一项都会先进行循环,如果是对象,则会遍历数组内部的对象,然后添加响应式,每一项都会dep依赖


但是由于dataList的每一项是数组字符串,我们可以继续看到这段代码


var Observer = function Observer(value) {
    // debugger;
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // 由于dataList是数组
    if (Array.isArray(value)) {
      var augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      // 遍历数组
      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后,就会去调用walk,然后继续defineReactive$$1,这样每一项item就被Object.defineProperty拦截了。


此时如果是对象,当你对数组的item对象进行修改时,就会触发set进而更新页面了。


所以你修改this.dataList[0] = "111";,因为dataList的每一项item并不是一个对象,并没有被observer,所以修改其值,只是改变对原对象值,但是根本不会触发拦截对象的set方法,自然就不会dep.notify()去派发更新,触发页面更新了


并没有更新页面

b0b78209f8f1bf20d446ab15e057472d.png

于是当你这样处理时


...
mounted() {
    debugger;
    this.dataList[0] = "111";
    this.dataList2[0].name = '北京';
},

你会发现,页面会更新了,但是实际上修改dataList并不会立即更新页面,会等dataList2[0]修改了,批量更新

8cfe02d20ea3aa7332a5915e95db7551.png

所以当修改dataList2[0].name执行完毕后

422f1893f90d656a40eb4d80de637f41.png

已经可以看到页面更改了

4ea1660c63b2f4a898db9eb01bb0a011.png

另外你看到下面可能会疑惑


...
  data() {
    return {
      test: "Web技术学苑",
      dataList: ["Maic", "Test"],
      dataList2: [
        {
          name: "深圳",
        },
        {
          name: "广州",
        },
      ],
    };
  },

我在data中申明了一个test他的值也是字符串,不是对象啊,那么为什么我直接修改,也可以更新数据呢


mounted() {
    debugger;
    this.dataList[0] = "111";
    this.test = "前端早早聊";
 },

这样你会发现this.test直接访问了data的数据,并且修改了test的数据。


其实当你修改test时,本质就会触发vm对象,这个this就是那个实例对象,因为实例对象在初始化的时候,这个对象就已经被Observer,所以当你修改test就是在设置实例化对象上的属性,自然就会触发set所以页面就更新了。


如果你直接修改this.dataList = ['aa', 'bb'],那么也是可以更新数据的,因为此时dataList是绑定在实例化对象上的,这个dataList已经被proxy处理直接挂载了this对象上,而这个this对象也是被Observer了,所以你修改其值,自然就会触发set,所以页面就会更新


vue中,initState的时候,会将data中的所有数据变成响应式,每一个属性对象都会有一个dep,当这个属性值是数组时,会对数组进行遍历,如果数组的每项是引用数据类型,那么每一项都会被Observer,数组的每一项都会增加一个dep对象,当数据更新时,会派发更新所有的数据。


总结


  • 当一个组件数据发生了变化,但是视图层没有发生变化,形成的原因只有以下几种
    1、 数据流的问题,如果一个子组件的props数据时直接通过子组件data中去接收props,当修改负组件props时,如果子组件不监听props,重新对data赋值那么可能会导致子组件数据并不会更新
    2、 如果使用hooks,如果并不会是从负组件传入的props,而是重新在子组件重新引入hooks,在负组件你修改同一份hooks引用,子组件并不会有效果,因为hooks每次调用都会时一份新的引用,所以子组件只能从props接口获取


  • 当一个数组的每一个item并不是对象时,其实此时item并不是一个响应式,并不会被Observe,在data初始化的每一个对象vue初始化时,都会给每一个对象变成reactive,并且每一个对象会有一个dep对象。只有被Observer,修改其值才会触发set,从而更新视图层


  • 我们每一个data中返回的对象的值都会被Observer,每一个数组对象在初始化时都会被Observer,数组中的每一个对象都会添加一个dep对象,当数组对象发生变化时,就会触发对象拦截,更新操作。如果数组中的每一项是基础数据类型,那么通过索引方式修改其值并不会触发更新UI


  • code example[1]
相关文章
|
2月前
|
存储 缓存 关系型数据库
滴滴面试:单表可以存200亿数据吗?单表真的只能存2000W,为什么?
40岁老架构师尼恩在其读者交流群中分享了一系列关于InnoDB B+树索引的面试题及解答。这些问题包括B+树的高度、存储容量、千万级大表的优化、单表数据量限制等。尼恩详细解释了InnoDB的存储结构、B+树的磁盘文件格式、索引数据结构、磁盘I/O次数和耗时,以及Buffer Pool缓存机制对性能的影响。他还提供了实际操作步骤,帮助读者通过元数据找到B+树的高度。尼恩强调,通过系统化的学习和准备,可以大幅提升面试表现,实现“offer直提”。相关资料和PDF可在其公众号【技术自由圈】获取。
|
2月前
|
监控 Java easyexcel
面试官:POI大量数据读取内存溢出?如何解决?
【10月更文挑战第14天】 在处理大量数据时,使用Apache POI库读取Excel文件可能会导致内存溢出的问题。这是因为POI在读取Excel文件时,会将整个文档加载到内存中,如果文件过大,就会消耗大量内存。以下是一些解决这一问题的策略:
295 1
|
2月前
|
存储 关系型数据库 MySQL
面试官:MySQL一次到底插入多少条数据合适啊?
本文探讨了数据库插入操作的基础知识、批量插入的优势与挑战,以及如何确定合适的插入数据量。通过面试对话的形式,详细解析了单条插入与批量插入的区别,磁盘I/O、内存使用、事务大小和锁策略等关键因素。最后,结合MyBatis框架,提供了实际应用中的批量插入策略和优化建议。希望读者不仅能掌握技术细节,还能理解背后的原理,从而更好地优化数据库性能。
|
2月前
|
存储 大数据 数据库
Android经典面试题之Intent传递数据大小为什么限制是1M?
在 Android 中,使用 Intent 传递数据时存在约 1MB 的大小限制,这是由于 Binder 机制的事务缓冲区限制、Intent 的设计初衷以及内存消耗和性能问题所致。推荐使用文件存储、SharedPreferences、数据库存储或 ContentProvider 等方式传递大数据。
100 0
|
4月前
|
Java
【Java基础面试五】、 int类型的数据范围是多少?
这篇文章回答了Java中`int`类型数据的范围是-2^31到2^31-1,并提供了其他基本数据类型的内存占用和数值范围信息。
【Java基础面试五】、 int类型的数据范围是多少?
|
4月前
|
前端开发 JavaScript UED
element-ui 表格数据究竟隐藏着怎样的神秘样式与格式化技巧?快来揭开谜底!
【8月更文挑战第22天】《element-ui 表格数据样式及格式化案例》展示了如何利用 element-ui 的表格组件实现美观且易读的数据展示。通过简单配置,可以自定义表格样式,如边框、背景色等,并通过 formatter 实现数据格式化,例如将成绩保留一位小数。此外,还能依据条件设置行样式,如成绩达优则高亮显示,从而增强用户体验和数据可读性。
80 1
|
5月前
|
canal 缓存 NoSQL
Redis常见面试题(一):Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;先删除缓存还是先修改数据库,双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
Redis常见面试题(一):Redis使用场景,缓存、分布式锁;缓存穿透、缓存击穿、缓存雪崩;双写一致,Canal,Redis持久化,数据过期策略,数据淘汰策略
|
4月前
|
存储 负载均衡 算法
[go 面试] 一致性哈希:数据分片与负载均衡的黄金法则
[go 面试] 一致性哈希:数据分片与负载均衡的黄金法则
|
5月前
Element UI【级联选择器】el-cascader 获取选中内容的 label 数据,鼠标悬浮显示超长内容
Element UI【级联选择器】el-cascader 获取选中内容的 label 数据,鼠标悬浮显示超长内容
754 3
|
5月前
Element UI 多选表格--判断勾选数据行的 Checkbox 时为选中还是取消选中
Element UI 多选表格--判断勾选数据行的 Checkbox 时为选中还是取消选中
66 1

热门文章

最新文章