observe类会在vue实例被创建的时候,去遍历data里的每一个属性,先调用Array.isArray()判断是不是数组 。第一个考点就来了,vue是怎么监控数组的?可以看到,如果属性是数组,就会直接将arrayMethods直接赋值给监控数组的_proto_上以达到重写数组方法的目的,所以实际上我们调用的这几个数组方法已经是经过mutator()重写过了的(所以官方称这些为数组变更方法),在这里重写数组方法的好处是只对想要监控的数组生效,不用担心会污染到全局的Array方法 。还有一点,虽然现在的浏览器基本都支持这种非标准属性(_proto_)的写法,因为这种写法本身就是早期浏览器自身厂商对原型属性规范的实现,但是为了以防有些浏览器不支持,源码这里还是对浏览器做了兼容,如果不支持,就将这些变异方法一个个绑定到监控的数组上 。
arrayMethods定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/array.ts,这里写了一个拦截器methodsToPatch用来拦截数组原有的7个方法并进行重写,这就是为什么vue只能通过变异方法来改变data里的数组,而不能使用array[0]=newValue的原因 。官网文档说是由于 JavaScript 的限制,Vue 不能检测数组和对象的变化 。其实就是因为defineProperty方法只能监控对象,不能监控数组 。
const arrayProto = Array.prototypeexport const arrayMethods = Object.create(arrayProto)const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse']/** * Intercept mutating methods and emit events */methodsToPatch.forEach(function (method) {// cache original methodconst original = arrayProto[method]def(arrayMethods, method, function mutator(...args) {const result = original.apply(this, args)const ob = this.__ob__let insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':inserted = args.slice(2)break}if (inserted) ob.observeArray(inserted)// notify changeif (__DEV__) {ob.dep.notify({type: TriggerOpTypes.ARRAY_MUTATION,target: this,key: method})} else {ob.dep.notify()}return result})})继续接上上面的observe类源码说,如果是属性是对象的话,则会对对象的每一个属性调用defineReactive() 。源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/index.ts#L131 。其重点是下面这些代码(L157~L213) 。
Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter() {const value = https://www.huyubaike.com/biancheng/getter ? getter.call(obj) : valif (Dep.target) {if (__DEV__) {dep.depend({target: obj,type: TrackOpTypes.GET,key})} else {dep.depend()}if (childOb) {childOb.dep.depend()if (isArray(value)) {dependArray(value)}}}return isRef(value) && !shallow ? value.value : value},set: function reactiveSetter(newVal) {const value = getter ? getter.call(obj) : valif (!hasChanged(value, newVal)) {return}if (__DEV__ && customSetter) {customSetter()}if (setter) {setter.call(obj, newVal)} else if (getter) {// #7981: for accessor properties without setterreturn} else if (!shallow && isRef(value) && !isRef(newVal)) {value.value = newValreturn} else {val = newVal}childOb = !shallow && observe(newVal, false, mock)if (__DEV__) {dep.notify({type: TriggerOpTypes.SET,target: obj,key,newValue: newVal,oldValue: value})} else {dep.notify()}}})这就是双向绑定最核心的部分了,利用object.defineProperty()给每个属性添加getter和setter 。getter里主要是调用了Dep类的depend(),Dep类的源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/dep.ts#L21,depend()主要是调用了Dep.target.addDep(),可以看到Dep类下有个静态类型target,它就是一个DepTarget,这个DepTarget接口是定义在#L10,而Watcher类则是对DepTarget接口的实现,所以addDep()的定义需要在Watcher类中去寻找,源码定义在https://github.com/vuejs/vue/blob/v2.7.10/src/core/observer/watcher.ts#L160,它又调用回dep.addSub(),其作用是将与当前属性相关的watcher实例之间的依赖关系存进一个叫subs的数组里,这个过程就是依赖收集 。那么问题来了:为什么这里要调过来调过去,直接调用不行么,这也是考点之一,vue的双向绑定采用的是什么设计模式?看了这段代码,你就知道了,它采用的是发布者-订阅者模式,而不是观察者模式,因为Dep类就充当了发布者订阅者中的一个消息中转站,就是所谓的调度中心,这样发布者和订阅者就不受对方干扰,实现解耦 。
经验总结扩展阅读
- Ruoyi字典源码学习
- 虚拟线程 - VirtualThread源码透视
- 【Spring boot】启动过程源码分析
- 八 Netty 学习:新连接接入源码说明
- 深入底层C源码 Redis核心设计原理
- 七 Netty 学习:NioEventLoop 对应线程的创建和启动源码说明
- ERP 系统的核心是什么?有什么作用?
- Spring mvc源码分析系列--Servlet的前世今生
- spring cron表达式源码分析
- 集合框架——LinkedList集合源码分析
