前言
我们都知道Vue是MVVM框架的一种,它的最大特点就是数据驱动视图。那么什么是数据驱动试图呢? 这里我们可以简单的把数据看为状态,把视图看做UI, UI不可能一成不变的,它应该是动态变化的,所以得出状态变化视图随之变化就可以称为数据驱动试图。我们用以下数学公式来描述:
UI = Render(State)
在这里UI代表用户界面,State代表状态,Vue充当了Render角色。Vue发现state改变之后,经过一系列的加工最终呈现到用户UI上。那么第一个问题来了,Vue怎么知道state什么时候变化了呢?
一. 什么是变化检测
Vue是怎么知道state变化了呢?这就得出了变化检测这个概念,即状态追踪,当state发生改变,去通知视图进行更新。 变化检测是出现很久的一个词,在其它MVVM框架中,React使用对比虚拟节点来完成变化检测,Angular使用脏数据检查流程来完成变化检测。那么接下来我就来通过源码解析来分析Vue是怎么完成变化检测的机制,我们这儿解析的版本为2.6.x版本。
二. Object的变化检测
我们知道MVVM框架的核心是数据驱动视图,知道了数据什么时候变化,然后通知视图图去更新,那么问题便迎刃而解了。在Vue 2中使用的是Obeject.defineProperty来检测数据的变化。
1. 使Object对象变为可检测。
let myTeslaCar = {
name: '我的小车车',
age: 1,
description: '2016年 黑色 Tesla'
}
Object.defineProperty(myTeslaCar, "name", {
set(newVal) {
console.log('name属性被设置了值',newVal)
},
get () {
console.log('name属性被读取了')
}
})
let carName = myTeslaCar.name
myTeslaCar.name = '我的tesla Model3'
输出结果如下:
接下来:我们让myTeslaCar的每一个属性变为可检测,我们创建一个Observe类,完整代码如下所示。
function def(object, key ,val) {
Object.defineProperty(object,key, {
configurable: true,
writable: true,
enumerable: true,
value: val
})
}
// 对应vue源码位置: src/core/observer/index.js
/** Observer类通过递归的方式把一个对象的所有属性都变为可监测对象 */
class Observer {
constructor(value) {
this.value = value;
//给value新增一个‘__ob__'属性,值位当前Observe实例。
// 可以理解为打上了一个标记,标记它为响应式了,避免重复操作
def(value,'__ob__',this);
if(Array.isArray(value)) {
// 当val为数组的时的逻辑
} else {
this.walk(value);
}
}
/**
* @description 遍历每一个属性,然后转为可检测
* @param {Object} obj
*/
walk(obj) {
const keys = Object.keys(obj).filter(key => key!== '__ob__');
for(let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
}
/**
* @description: 将一个对象的key转为可检测
* @param {object} obj
* @param {string} key
* @param {*} val
*/
function defineReactive(obj, key, val) {
// 如果只传了obj和key,那么val=obj[key]
if(arguments.length === 2) {
val = obj[key]
}
// 如果val是对象,就递归
if(typeof val === 'object') {
new Observer(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`${key}属性被读取了`)
return val;
},
set(newVal) {
if(val === newVal){
return
}
console.log(`${key}属性被设置新值${newVal}`)
val = newVal;
}
})
}
我们创建了一个ObServer类,并将将一个Object转换成了可检测的Object.
我们给Value添加了一个__ob__属性标记此对象以及转换成响应式的了避免重复操作;然后只有object类型才会调用walk将每一个属性转换成getter/setter的形式来检测属性变化。在defineReactive中若果子属性还是一个对象,那么使用new Observer(val)来递归子属性,这样我们就可以将object中的所有属性(包括子属性)转换成getter/setter形式。也就是说,只要将对象传入Observe,就能将对象的所有属性变化可检测的,响应的object。
observer类位于源码的src/core/observer/index.js中。
现在我们重新定义一个myCar2对象:
const myCar2 = {
name: '我的小车车',
age: 1,
description: '2016年 黑色 Tesla',
infomation: {
color: 'black',
date: '2016-9.10'
}
}
const carObserver = new Observer(myCar2);
console.log(carObserver)
console.log(carObserver.value.name)
运行结果如下:
这样myCar2的所有对象就变为响应式的了。
2. 依赖收集
2.1 什么是依赖收集?
在上一章中,我们完成了第一步,我们将一个对象转换成了响应式的可检测对象。知道了数据变化的时机,我们就能通知视图去更新变化。那么问题来了,我们究竟怎样通知视图去变化呢?视图那么大,我们该通知谁去变化?总不能一个数据变化,就把整个视图都更新一遍?这样是显然是不合理的。
这个时候你会想到,那么谁用到了对应的状态,就更新谁呗! 对了,就是这个思想,现在我们换一种优雅的说法:我们把谁用到了数据改为谁依赖了状态,我们把每一个状态创建一个依赖数组(因为一个状态可能被用于多处),当一个状态发生了变化,我们就去对应的依赖数组去通知每一个依赖,告诉它们你们依赖的状态变化呢,你们改更新啦!这一过程就是依赖收集。
2.2 何时收集依赖,何时通知依赖更新?
明白了依赖收集这个概念之后,那我们来思考这个问题,那到底何时收集依赖?何时更新依赖呢?
你是不是想到了getter和setter呢?对的,就是这两个关键点。其实,谁用到了状态,就相当于谁读取了状态。这样我们就应该在getter中收集依赖,然后在状态变化时在setter中通知对应依赖变化。总结一句话如下:
getter中收集依赖,setter中通知依赖更新
2.3 把依赖收集到哪里?
了解了依赖收集和怎样收集依赖和何时更新依赖之后,我们来思考下,把依赖收集到哪儿呢?
在2.1中我们说到可以用一个依赖数组来保存依赖,谁依赖了状态就把谁放进对应状态的依赖数组。但是单单使用一个数组来管理依赖的话,功能好像并不完善并且代码的耦合性也过于高。更好的做法是,我们应该扩展管理依赖的功能,对每一个数据都建立一个依赖管理器,把这个数据的所有依赖都管理起来。Vue中使用了Dep类来管理依赖,我们来看如下简版依赖管理类Dep。
/*
* @Description: 依赖管理类Dep
*/
export default class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs, sub)
}
depend() {
if(window.target) {
this.addSub(window.target)
}
}
}
/** 删除数组中的给定item */
export function remove(arr, item) {
if(arr.length) {
const index = arr.indexOf(item)
if(index > -1) {
return arr.splice(index, 1)
}
}
}
我们定义了一个简易的Dep类,然后添加增删依赖的方法,使用depend添加依赖,使用notify方法通知依赖更新。现在我们就可以在getter中进行依赖收集,在setter中通知依赖更新了。看defineReactive中的setter和getter。
function defineReactive(obj, key, val) {
// 如果只传了obj和key,那么val=obj[key]
if(arguments.length === 2) {
val = obj[key]
}
// 如果val是对象,就递归
if(typeof val === 'object') {
new Observer(val);
}
const dep = new Dep(); // 实例化一个依赖管理器
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`${key}属性被读取了`)
// 收集依赖
dep.depend();
return val;
},
set(newVal) {
if(val === newVal){
return
}
console.log(`${key}属性被设置新值${newVal}`)
val = newVal;
dep.notify(); // 通知依赖更新
}
})
}
3. 依赖到底是谁?
通过上一章的学习,你现在应该了解了什么是收集依赖,以及收集依赖和通知依赖更新的时机。那么到底依赖是谁呢?
虽然我们说谁用到了状态谁就是依赖,但这只是我们的口述,我们需要知道真正的依赖在代码中时怎么实现的。
在Vue2中,其实还实现了一个Watcher类,它就是我们上文所描述的谁,换句话说: 谁用到了状态,谁就是依赖,就为谁创建一个Watch实例。在数据发生变化时,我们不是直接通知依赖,而是通知依赖的Watch实例,由Watch实例来通知视图更新。
下面是Watch类的简版实现:
export default class Watcher {
constructor (vm,expOrFn,cb) {
this.vm = vm;
this.cb = cb;
this.getter = parsePath(expOrFn)
this.value = this.get()
}
get () {
window.target = this;
const vm = this.vm
let value = this.getter.call(vm, vm)
window.target = undefined;
return value
}
update () {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
/**
* Parse simple path.
* 把一个形如'data.a.b.c'的字符串路径所表示的值,从真实的data对象中取出来
* 例如:
* data = {a:{b:{c:2}}}
* parsePath('a.b.c')(data) // 2
*/
const bailRE = /[^\w.$]/
export function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
谁用到了状态谁就是依赖,我们就为每一个依赖创建一个Watch实例,在Watch实例初始化过程中读取了数据,首先它会将自己挂载到全局唯一的地方window.target(vue2源码使用的是Dep.target)。 由于读取了数据,将会触发该数据的getter收集依赖, 然后将会通过Dep.depend取到当前正在读取数据的依赖(即Watch实例, window.target)并存入依赖数组,然后在Watch的get方法中将window.target释放掉。
当数据发生改变时,会在当前数据的setter中通过dep.notify()通知依赖更新,然后在dep.notify()中遍历依赖然后调用Watch的update方法,从而更新视图。
简单来说:
Watch会将自己挂载到全局唯一的位置上,然后读取数据时将会通过
getter收集依赖,取得是全局上面挂载的这个值(如本例的window.target)并添加到依赖组里。收集好依赖之后,Dep会通知所有的依赖更新,调用Watch的update来达到更新视图。
为了便于理解,可以参照下图:
graph LR
Data(数据) --> Getter(getter) --3.取到当前正在读取数据的依赖--> Global(window.target)
Data(数据) --> Setter(setter)
Getter(getter) -.2.读取数据触发getter返回数据.-> Wather(Wather)
Wather(Wather) --1.挂载到全局唯一的位置--> Global(window.target)
Global --4.添加到依赖数组--> Dep(Dep)
4. 不足之处
- Object.defineProperty有一点不足就是:向对象添加新的一对
key/value时,它是无法进行检测的,即会出现一个问题,当我们添加或删除一个key/value时,无法知道状态变化了,无法通知依赖,无法进行视图更新。这也就是我们开发时常发现数据变更但视图没有更新的原因之一。 - Object.defineProperty是无法拦截到数组的大部分操作的,Vue2的解决方案是重写了数组中的常使用的几个方法,这一点我们将在数组的变化检测章详细说明。
当然Vue2也注意到了这一点,提供了全局API来解决这个问题,分别是Vue.set和Vue.delete,这两个API将在全局API章节详细解析实现原理。
5.总结
首先,我们通过Object.defineProperty实现了对属性的变化检测,并且封装了Observer类实现了对Object的所有属性都转换成getter/setter的形式来完成动态检测。
然后我们学习了什么是收集依赖,知道了在getter中收集依赖,在setter中通知依赖更新。然后封装了Dep依赖管理类来管理依赖收集。
最后,我们为每一个依赖都创建了Wacth实例,当数据变化时,我们去通知Watch实例,由Watch实例去做更新操作。
整体流程如下:
1.通过Observer类将对象转换成getter/setter形式来追踪状态变化。
2.当外界使用Watch实例读取数据就会触发getter将Watch实例添加到Dep中去。
3.当数据发生变化时,将会由setter通知依赖(Watch)去更新变化。
4.当Watch收到通知时,会通知外界,外界收到通知后可能会更新视图,也有可能调用用户设置的回调函数。
三. 数组的变化检测
上一章中我们介绍了对象的变化检测,那么这一章我们将来学习数组的变化检测。为什么Array的变化检测要轮出来讲,为什么和Object的是两套逻辑?
因为我们使用的是对象的原型上的defineProperty方法,所以Array是无法使用的,我们需要了解Vue是怎么设计数组的变化检测逻辑。
虽然对象的数组检测拥有另外的一套逻辑,但基本思想并没有改变:
在读取数据时收集依赖,在数据变化时通知所有依赖进行更新。
1. 在哪儿收集依赖
我们应该将使用到Array的地方收集作为依赖,那么问题来了,在哪儿收集依赖呢?
其实Array的收集方式和Object相同,都是在getter中收集。
有的同学就会问了,defineProperty不是无法监听到数组变化,那又要怎么监听到数据变化,那又怎么触发getter呢?
我们回想一下,我们定义数组时候的方式是不是如下方式:
export default {
data () {
return {
...,
hobby: ['爱好1', '爱好2',...,'爱好n']
...,
}
},
}
有的同学是不是突然就发现了,我们的数据都是写到对象里面的,要取到这个hobby只要从对象读取一下就行了,这样就触发了hobby的getter,从而可以收集依赖了。如此得到标题的解:
Array收集依赖也是在getter中进行。
2. 让Array变为可检测。
上一节我们已经知道Array收集依赖也是通过getter进行,我们回想一下Object的变为可检测的流程,读取数据时应该在getter中收集依赖,数据变化时在setter中通知依赖更新。此时我们已经知道收集依赖通过getter进行,也就完成了一部分,即我们知道了Array何时被读取了,我们无法感知Array何时变化了。下面我们就围绕这一问题来进行分析:当Array型数据发生变化时我们如何得知?
2.1 思路
Object的数据变化可以通过setter感知到,但是Array并没有setter,我们如何得知数据何时变化的了?
其实思考一下,Array改变,绝对调用了数组的方法,而改变原数组的方法就那么几个。我们能不能在不改变原有功能的情况下,重写一下这几个方法并扩展一下其功能,例如下面的例子:
Array.prototype.newPush = function(value) {
this.push(value)
console.log(`数组被修改了`)
}
浏览器运行结果如图所示:
2.2 拦截器
改变自身数组的方法有7个,分别是push,pop,shift,unshift, splice, sort, reverse基于我们的思路,我们需要实现一个拦截器arrayMethods,它拦截Array实例到原型对象这一层,Array实例实际调用的是我们拦截器里实现的方法。
// 源码位置:/src/core/observer/array.js
const arrayPrototype = Array.prototype
export const arrayMethods = Object.create(arrayPrototype)
const arrayMap = [
'push',
'pop',
'shift',
'unshift',
'splice',
'reverse',
'sort'
]
arrayMap.forEach(function(method){
const origin = arrayPrototype[method];
Object.defineProperty(arrayMethods, method, {
configurable: true,
enumerable: true,
writable: true,
value: function mutator(...args) {
const result = origin.apply(this, args)
return result;
}
})
})
为了方便理解,调用流程如图所示:
graph LR
Array(array) --> pop --> 拦截器[arrayMethods]
Array --> push --> 拦截器
Array --> shift --> 拦截器
Array --> unshift --> 拦截器
Array --> splice --> 拦截器
Array --> sort --> 拦截器
Array --> reverse --> 拦截器
拦截器--> Array.prototype
上述代码,我们首先创建了一个继承Array的原型对象的空拦截器对象arrayMethods。 然后使用defineProperty对改变原数组的7个方法逐个进行封装,array实例使用这7个方法时实际是使用了我们拦截器中同名方法,在同名方法中origin代表原始对应方法,这样我们拦截中的方法就可以扩展一些逻辑,比如说通知更新。
2.3 使用拦截器。
上一节中我们定义好了拦截器,但是我们还没有挂载到数组实例与数组原型对象之间,挂载其实只要当数据类型为Array时,把数据的__proto__赋值为我们的拦截器arrayMethods就可以了。
下面我们看下添加了Array定义的Observe类代码:
class Observer {
constructor(value) {
this.value = value;
//给value新增一个‘__ob__'属性,值位当前Observe实例。
// 可以理解为打上了一个标记,标记它为响应式了,避免重复操作
def(value,'__ob__',this);
if(Array.isArray(value)) {
// 当val为数组的时的逻辑
const augment = hastProto ? protoToTarget : copyToSelf;
augment(value, arrayIntercepter, arrayIntercepterKeys)
} else {
// 对象时变为可检测的逻辑
this.walk(value);
}
}
// 省略之前的代码...
}
/** 检测浏览器是否支持__proto__ */
export const hastProto = '__proto__' in {}
const arrayIntercepterKeys = Object.getOwnPropertyNames(arrayIntercepter)
/** 挂载src的原型到target,即src.__proto__ = target */
function protoToTarget(src,target) {
src.__proto__ = target;
}
/**
* @description: 复制目标上面的属性到自身上面
* @param {object} src
* @param {object} target
* @param {string[]} keys
*/
function copyToSelf(src, target, keys) {
for(let i =0 ; i < keys.length; i++) {
const key = keys[i];
def(src, key, target[key]);
}
}
上述代码,我们首先检测浏览器是否支持__proto__属性,如果支持我们就调用protoToTarget将原型属性__proto__赋值为我们上文写的拦截器arrayIntercepter;如果不支持我们就调用copyToSelf方法把拦截器上面的定义的7个重写方法复制到value里面。
当拦截器生效之后,当数组发生变化之后,我们就可以在拦截器里面通知变化了,也就是说我们知道数组何时发生变化了,这样我们也就完成了Array的变化检测。
3. 数组的依赖收集
3.1 数组的依赖收集在哪儿
我们知道Observer类完成了给数据添加了getter/setter,所以依赖应该也应该在Observer进行收集,源码是如下完成的:
export class Observer {
constructor(value) {
this.value = value;
//给value新增一个‘__ob__'属性,值位当前Observe实例。
// 可以理解为打上了一个标记,标记它为响应式了,避免重复操作
def(value,'__ob__',this);
// 实例化一个依赖管理器,用来收集数组依赖
this.dep = new Dep();
if(Array.isArray(value)) {
// 当val为数组的时的逻辑
const augment = hastProto ? protoToTarget : copyToSelf;
augment(value, arrayIntercepter, arrayIntercepterKeys)
} else {
// 对象时变为可检测的逻辑
this.walk(value);
}
}
}
源码是在实例上添加了一个dep依赖管理器来收集数组的依赖。
3.2 怎么收集依赖
在上文,我们有提到数组的依赖也应该在getter中收集,也在Observe类中添加了一个依赖管理器来收集数组的依赖,那么我们怎么在getter里面进行收集依赖呢?
我们思考一下:我们是不是只要在getter拿到Observer类的实例中的dep就可以了,那另一个问题又来了;我们怎么拿到Observer的实例呢?
哎,有的同学是不是已经想到了我们的标记属性__ob__, 是的,就是利用这个属性,它存储的值就是Observer实例本身,下面我们看看源码中的实现方法。
function defineReactive(obj, key, val) {
// ...省略这部分的代码...
const childOb = observer(val); // 拿到val的Observer实例
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
console.log(`${key}属性被读取了`)
// 收集依赖
dep.depend();
if(childOb) {
// 数组本身的依赖进行收集。
childOb.__ob__.dep.depend();
if(Array.isArray(value)) {
// 数组的每一项进行依赖收集
dependArray(value);
}
}
return val;
}
// ...省略这部分的代码...
})
})
/**
* @description 数组的每一项依赖进行收集
*/
function dependArray(arrayData) {
if(!Array.isArray(arrayData)) {
return;
}
for(let i = 0; i < arrayData.length; i++) {
const e = arrayData[i];
e && e.__ob__ && e.__ob__.dep.depend();
if(Array.isArray(e)) {
dependArray(e);
}
}
}
function isObject (obj) {
return obj !== null && typeof obj === 'object'
}
/**
* @description 返回value的Observer实例,如果__ob__没有就通过Observer变为响应式
* @param {object} value
*/
export function observer(value) {
// 不是对象或者是虚拟节点就返回
if(!isObject(value) || value instanceof VNode) {
return;
}
let ob;
if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value);
}
return ob;
}
上述代码中,我们首先尝试判断传入的val是否拥有__ob__属性,我们在前文中有提及到,拥有此属性的对象就代表已经成为响应式的了,如果没有,就调用new Observer(val)转为响应式的数据并返回这个Observer实例;这样我们就可以拿到实例的依赖管理器,然后我们在getter中进行依赖收集,然后递归收集数组每一项的依赖。
Tips: typeof 数组或者null的结果为Object
3.3 怎么通知依赖更新
前文我们提及到数组的数据新增或减少是不会粗发setter的, 而改变数组自身的方法有7种,所以我们封装了数组实例与数组原型对象之间的拦截器,我们应该在拦截器中的同名方法中通知依赖。
那具体怎么做呢,首先要通知依赖,就要先访问到依赖。我们应该拿到响应式数据的Observer实例,即__ob__属性,拿到了之后,就可以访问到对应实例的依赖管理器,调用notify方法通知更新即可。下面我们看代码:
arrayMap.forEach(function(method){
const origin = arrayPrototype[method];
Object.defineProperty(arrayIntercepter, method, {
configurable: true,
enumerable: true,
writable: true,
value: function mutator(...args) {
const result = origin.apply(this, args)
// 拿到Observer实例
const ob = this.__ob__;
// 通知依赖更新
ob.dep.notify();
return result;
}
})
})
由于拦截器是挂载到原型属性上,所以this代表的就是数据value,拿到了Observer实例之后,你就可以访问到依赖管理器dep然后通过notify方法进行更新了。到这,数组的变化检测就已经完成了。
4. 实现深度检测
我们上文所说的都是增对数组自身的变化检测而言,对数组添加一个元素或者删除一个元素都是可以检测到的。但是如果数组的子元素变化了,以上操作就检测不到。而在Vue中,无论是Object数据还是Array数据都是深度检测,Object类型的数据我们已经在defineReactive中进行递归处理完成了。
那么Array类型的数据怎么实现深度检测呢?是不是有的同学就会想到,我们遍历每一项,然后通过new Observer不就得了。对的,其核心思想就是如此。我们看下代码:
tips: 深度检测即不但要检测自身的变化,还要检测到每一个子元素的数据变化。
export class Observer {
constructor(value) {
this.value = value;
def(value,'__ob__',this);
this.dep = new Dep();
if(Array.isArray(value)) {
const augment = hastProto ? protoToTarget : copyToSelf;
augment(value, arrayIntercepter, arrayIntercepterKeys)
// 将数组的每一项转换为可被检测的响应式数据
this.observeArray(value);
} else {
this.walk(value);
}
}
/**
* @description: 将数组的每一项变为响应式数据
* @param {Array} items
*/
observeArray (items) {
for(let i = 0 ; i < items.length; i++) {
observer(items[i]);
}
}
//... 省略其它定义代码
}
/**
* @description 返回value的Observer实例,如果ob没有就通过Observer变为响应式
* @param {Observer} value
*/
export function observer(value) {
if(!isObject(value) || value instanceof VNode) {
return;
}
let ob;
if(value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else {
ob = new Observer(value);
}
return ob;
}
上述代码会调用observerArray递归数组的每一项,然后调用observer将不是响应式的子元素转为响应式数据。
5. 新增元素的变化检测。
对于数组我们已经完成了深度检测,但是上述逻辑还会有一种问题,我们在新增一个元素时,新增的此元素并不是响应式的,我们还需将数组新增的元素转为响应式的。
我们思考一下,向数组添加元素的方法无非三种,push,unshift,和splice这三个,当调用这三个方法时,我们在拦截器添加转换为响应式数据的逻辑处理,将新增的元素通过Observer的observeArray方法转为响应式不就完成了?我们直接看代码:
arrayMap.forEach(function(method){
const origin = arrayPrototype[method];
def(arrayIntercepter, method, function mutator(...args) {
const result = origin.apply(this, args)
// 拿到Observer实例
const ob = this.__ob__;
let inserted;
switch (method) {
case 'push':
case 'unshift': {
// 如果是push和unshift第一项就是待插入的元素
inserted = args;
break;
}
case 'splice': {
// splice第2个参数是待插入的元素
inserted = args.splice(2);
}
}
// 如果插入的元素存在,则调用observe函数将新增的元素转为响应式数据
if(inserted) ob.observeArray(inserted);
// 通知依赖更新
ob.dep.notify();
return result;
})
})
tips: inserted为数组,因为observeArray参数为数组。
6. 不足之处
以上对于数组的变化操作都是基于原型对象的拦截,但是我们日常使用的另一种方式是使用下标访问和操作,这就出现了一个问题,如果使用下标进行操作数组,那么变化检测就检测不到了,如下代码所示:
let array = [1,2,3]
array[0] = 5;
array.length = 0;
当然Vue也注意到了这点,它添加了两个全局API来解决这个问题,即Vue.set和Vue.delete,这两个API的实现原理我将会在全局API分析一节单独为大家仔细分析,这儿就不再阐述。
7. 总结
在这一章中我们可以很清楚的知道,数据的访问我们是很容易通过getter感知到,但数据变化我们是无从得知。如是我们通过了拦截器重写了变化数组的7个方法来感知数组的变化。其次,我们对数组的依赖收集和通知依赖更新进行了深层次的分析,我们了解到Vue不但对数组自身作了变化检测,也对数组的子元素和新增的元素作了 变化检测逻辑,我们也分析了实现原理,由浅至深的向大家阐述了Vue实现数据变化检测的核心思想和基本原理。以下是思维导图。
四. 结语
4.1 作者有言
如果大家发现文中有阐述不合理和表述错误时欢迎批评和指正;如果还有不明白的地方也可以咨询作者,不用不好意思,我将知无不言。
成为大佬之路没有捷径可言,只有热爱和不断的学习。