MVVM
View层:在 Vue 中是绑定 dom 对象的 HTML
ViewModel 层:在 Vue 中是实例的 vm 对象
Model层:在 Vue 中是 data、computed、methods 等中的数据
在 Model 层的数据变化时,View层会在ViewModel的作用下,实现自动更新
在 Model 层的数据变化时,View层会在ViewModel的作用下,实现自动更新
Vue 响应式原理的核心:数据驱动视图;属于非侵入式响应式系统。
侵入式:需要刻意地去调用他人的 API,才能改变数据的值。
非侵入式:只是更新数据,并没有其他操作。
Vue2 的响应式主要依靠 ES5 的 Object.defineProperty 实现(这也是为什么 Vue.js 不能兼容 IE8 及以下浏览器的原因)。
数据劫持/数据代理
obj : 是要在其上定义的对象;
prop: 是要定义或修改的属性的名称;
descriptor: 是将被定义或修改的属性描述符。
descriptor 的相关配置项:
{
*// 可枚举*
enumerable:true,
*// 可以被配置,比如可以被delete*
configurable:true,
*// 属性对应的值*
value: 'abc',
*// 是否可写,为 true 时上面的 value 值才能被改变。*
writable: 'true',
*// getter*
get() {
console.log('你试图访问'+ key+ '属性')
return val
},
*// setter*
set(newValue) {
console.log('你试图改变'+ key+ '属性', newValue)
if (val=== newValue) {
return
}
val= newValue
},
}
可见,Object.defineProperty() 方法中存在一个 getter 和 setter 的可选项,可以对属性值的获取和设置造成影响。 Vue 中会编写了一个 wather 来处理数据,在使用 getter 方法时,总会通知 wather 实例对 view 层渲染页面;同样的,在使用 setter 方法时,总会在变更值的同时,通知 wather 实例对 view 层进行更新。
在 Vue 的初始化阶段,_init 方法执行的时候,会执行 initState(vm) 方法,它的定义在 src/core/instance/state.js 中。
initState 方法 主要是对 props、methods、data、computed 和 wathcer 等属性做了初始化操作。
export function initState(vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe((vm._data = {}), true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initProps 方法 是 props 的初始化主要过程,就是遍历定义的 props 配置。遍历调用 defineReactive 方法把每个 prop 对应的值变成响应式,可以通过 vm._props.xxx 访问到定义 props 中对应的属性。
initData 方法 是 data 的初始化,主要是对定义 data 函数返回对象的遍历,通过 proxy 把每一个值 vm._data.xxx 都代理到 vm.xxx 上;并且调用 observe 方法 观测整个 data 的变化,把 data 也变成响应式,可以通过 vm._data.xxx 访问到定义 data 返回函数中对应的属性。
上面两个方法都是遍历 props 和 data 里面的数据添加响应式,那么具体是如何添加响应式的呢。
observe
在 vue 初始化的时候,initState 方法中会调用一个方法 observe,就是用来监测数据的变化。
export function observe(value) {
*// 如果value不是对象,什么都不做*
if (typeof value != 'object') return
*// 定义ob*
var ob
if (typeof value.__ob__ !== 'undefined') {
*// 判断对象是否有__ob__这个属性,可以理解为响应式标志*
ob = value.__ob__
} else {
*//如果没有的话就生成一个实例对象,Observer类的内部去转化生成一个响应式对象*
ob = new Observer(value)
}
return ob
}
observe 方法的作用就是给非 VNode 的对象类型数据添加一个 Observer,如果已经添加过则直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例。
export default class Observer {
constructor(value) {
*// 每一个Observer的实例身上,都有一个dep*
this.dep = new Dep()
*// 给实例(this,一定要注意,构造函数中的this不是表示类本身,而是表示实例)添加了__ob__属性,值是这次new的实例*
def(value, '__ob__', this, false)
*// 不要忘记初心,Observer类的目的是:将一个正常的object转换为每个层级的属性都是响应式(可以被侦测的)的object*
*// 检查它是数组还是对象*
if (Array.isArray(value)) {
*// 如果是数组,要非常强行的蛮干:将这个数组的原型,指向arrayMethods*
Object.setPrototypeOf(value, arrayMethods)
*// 让这个数组变的observe*
this.observeArray(value)
} else {
this.walk(value)
}
}
*// 遍历*
walk(value) {
for (let k in value) {
defineReactive(value, k)
}
}
*// 数组的特殊遍历*
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
*// 逐项进行observe*
observe(arr[i])
}
}
}
Observer 的构造函数逻辑很简单,首先实例化 Dep 对象,接着通过执行 def 函数把自身实例添加到数据对象 value 的 ob 属性上,def 函数就是简单的 Object.defineProperty 的封装,给对象添加__ob__ 属性。
Observer 接下来会对 value 做判断,对于数组会调用 observeArray 方法,否则对纯对象调用 walk 方法。可以看到 observeArray 是遍历数组再次调用 observe 方法,而 walk 方法是遍历对象的 key 调用 defineReactive 方法.
调用数组方法
由于 Object.defineProperty 不支持数组的检测,所以当数据是数组时,为了让其也变成响应式添加__ob__属性,因此需要特殊处理一个数组数据。本质上就是 重写数组上面的七个方法。
*// 得到Array.prototype*
**const** arrayPrototype **=** Array.prototype
*// 以Array.prototype为原型创建arrayMethods对象,并暴露*
**export** **const** arrayMethods **=** Object.create(arrayPrototype)
*// 要被改写的7个数组方法*
**const** methodsNeedChange **=** [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
]
methodsNeedChange.forEach((methodName) => {
*// 备份原来的方法,因为push、pop等7个函数的功能不能被剥夺*
**const** original **=** arrayPrototype[methodName]
*// 定义新的方法*
def(
arrayMethods,
methodName,
**function** () {
*// 恢复原来的功能*
**const** result **=** original.apply(**this**, arguments)
*// 把类数组对象变为数组*
**const** args **=** [...arguments]
*// 把这个数组身上的__ob__取出来,__ob__已经被添加了,为什么已经被添加了?因为数组肯定不是最高层,比如obj.g属性是数组,obj不能是数组,第一次遍历obj这个对象的第一层的时候,已经给g属性(就是这个数组)添加了__ob__属性。*
**const** ob **=** **this**.__ob__
*// 有三种方法push\unshift\splice能够插入新项,现在要把插入的新项也要变为observe的*
**let** inserted **=** []
**switch** (methodName) {
**case** 'push'**:case** 'unshift'**:**inserted **=** args
**breakcase** 'splice'**:***// splice格式是splice(下标, 数量, 插入的新项)*
inserted **=** args.slice(2)
**break**}
*// 判断有没有要插入的新项,让新项也变为响应的*
**if** (inserted) {
ob.observeArray(inserted)
}
*// 新插入的数据也会触发依赖通知watcher更新视图*
ob.dep.notify()
**return** result
},
**false**)
})
值得注意的是 push,unshift,splice 三个方法会插入新的数据,此时为了使其也成为响应式数据,需要再次调用 Observer 类里面的 observeArray 方法,遍历每一项使其具有响应式。注意 def 的第三个参数是函数,也就是给定义的新方法名定义的函数,只有调用这个方法名的时候才会调用这些新方法。所以这就是初始化 data 中有数组,不直接调用方法的原因。
在 Observer 类中,普通对象的遍历执行 defineReactive 方法并且传入 2 个参数 ,对象和当前的 key。
defineReactive 的功能就是定义一个响应式对象,给对象动态添加 getter 和 setter。
export default function defineReactive(data, key, val) {
const dep = new Dep()
// 如果参数只有两个的话,val值就是传入对象中的那个key的值
if (arguments.length == 2) {
val = data[key]
}
// 子元素要进行observe,至此形成了递归。这个递归不是函数自己调用自己,而是多个函数、类循环调用
let childOb = observe(val) //形成递归的关键
Object.defineProperty(data, key, {
// 可枚举
enumerable: true,
// 可以被配置,比如可以被delete
configurable: true,
// getter
get() {
console.log('你试图访问' + key + '属性')
// 如果现在处于依赖收集阶段
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
}
return val
},
// setter
set(newValue) {
console.log('你试图改变' + key + '属性', newValue)
if (val === newValue) {
return
}
val = newValue
// 当设置了新值,这个新值也要被observe
childOb = observe(newValue)
// 发布订阅模式,通知dep
dep.notify()
},
})
}
使用 defineReactive 函数不需要设置临时变量了,而是用闭包。接收的第三个参数 val 就是闭包的形式接收变量值的。
defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。最后利用 Object.defineProperty 去给 obj 的属性 key 添加 getter 和 setter。
vue 是在初始化时,会对 data 中的所有数据进行遍历调用 Object.defineProperty 方法,设置 getter、setter 响应式处理。如果 data 某个对象新增了属性,进行访问和设值都不会触发 get 和 set,即无响应式。
所以弊端就是 Object.defineProperty只对初始对象里的属性有监听作用,而对新增的属性无效。这也是为什么 Vue2 中对象新增属性的修改需要使用 Vue.$set 来设值的原因。
响应式对象最主要的是 Observer 类(观察者),负责把 对象转化成具有__ob__ 属性的响应式对象,将一个正常的 object 转换为每个层级的属性都是响应式(可以被侦测的) 的 object。
Vue2 响应式的核心是通过 Object.defineProperty 拦截对数据的访问和设置
响应式的数据分为两类:
对象,循环遍历对象的所有属性,为每个属性设置 getter、setter,以达到拦截访问和设置的目的,如果属性值依旧为对象,则递归为属性值上的每个 key 设置 getter、setter
数组,增强数组的那 7 个可以更改自身的原型方法,然后拦截对这些方法的操作
参考:
https://zhuanlan.zhihu.com/p/499408009
https://blog.csdn.net/weixin_48186771/article/details/108578630
https://juejin.cn/post/6950826293923414047