这道面试题大概是这样的,在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
打开页面,我们可以看到
我们从第一行源码到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)
我们依次从执行栈中去寻找真相
当调用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
是两个数组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
中的数据在初始化时候,都会被放入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
方法进行依赖收集
每一个对象都有一个dep
对象,在dep
对象的subs
中就会添加一个watch
当从_init
方法调用的,到数据初始化完成响应式
拦截后,initState
走完了,然后就是callHook(vm, 'created')
,最后initRender(vm)
,然后就走到了我们在mounted
方法debugger
的位置
我们继续下一步,此时我们会走到修改数组
当我们直接进行下面操作
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()
去派发更新,触发页面更新了
并没有更新页面
于是当你这样处理时
... mounted() { debugger; this.dataList[0] = "111"; this.dataList2[0].name = '北京'; },
你会发现,页面会更新了,但是实际上修改dataList
并不会立即更新页面,会等dataList2[0]
修改了,批量更新
所以当修改dataList2[0].name
执行完毕后
已经可以看到页面更改了
另外你看到下面可能会疑惑
... 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]