为了让数据有响应式,就需要在读写数据的时候能做一些操作,比如,写入数据时让页面渲染,如何直接使用obj.xx这样读取数据是没有办法在读取时执行操作的,所有就需要把写数据改成set函数,这时在set中就可以调用方法来渲染数据,那么如果使用Object.definePropertity,就需要对一个对象遍历多次来变成响应式对象,而且之后在这个对象中新增加的属性是没有响应式的,因为没有把这个属性的读写变成get和set,
在vue2中通过observe方法来观察一个对象,把它变成响应式对象,observe方法会把传进来的对象遍历,每一个属性执行defineReactive方法来使他具有响应式,当一个属性的值是对象时,递归调用observe来
而如果使用proxy,就不用循环遍历对象,当对一个proxy代理对象进行操作时
在vue3中,当我们用ref函数声明一个变量时,参数若是基础类型,例如
1 | const count = ref(0) |
然后在vue的源码中会调用 createRef
1 | function ref(value){ |
这里的第二参数,意思是shallow,(shallow 是干嘛的)[https://chatgpt.com/share/673ed4f4-d3b0-800b-a80f-409e87694cf7]
在createRef函数中会判断value是否已经是Ref类型的数据,如果已经是Ref类型则直接返回这个值,否则 return new RefImpl(value, false)
1 | function isRef(r) { |
在RefImpl函数中,每次创建的实例都会有一个dep属性,它用来在变量被读写时做一些操作,就像上写说的我们要在响应式变量被读取时记录下来,被修改时触发一些要执行的函数,所以在RefImpl构造函数中有这样一行代码,
1 | this.dep = new Dep(); |
同时这里也是包装响应式变量的地方,所以在这里也会有一些变量类型的标识。
总结一下这里的结构就是
1 | class RefImpl{ |
在创建每一个vue的component实例时,会有一个scope属性,scope中有effect属性,有run,stop 等方法,effect是要收集的响应式数据
1 | scope = new EffectScope(true) // 创建一个与上下文分离的scope |
这个scope在instance上面
创建完实例,执行setup方法时,当创建响应式变量时,每一个响应式变量实例都会创建dep实例,还有set中调用dep实例的trigger,get中调用dep实例的track,
然后在setup执行到watchEffect方法时,会把watch监听的响应式变量转换成getter方法,(这个getter方法会通过 new ReactiveEffect(fn) 转换成一个effect)并把这个effect放到上面提到的scope的effect数组中,此时还会通过一些列判断把当前effect放到全局变量activeSub上,然后执行 effect.run 方法,在run的时候就是触发了getter,此时就会触发track,在track中会把全局变量activeSub也就是当前的effect和dep通过Link类连接起来,这是一个双向链表,每一个节点都有sub和dep,(sub 就是 effect,相当于是订阅了某个响应式变量),effect中还有一个属性是scheduler,它是一个方法,会在适当的时机调用watchEffect的callback,此时就形成了响应式,在setter中可以通过dep拿到sub,也就是effect,这些effect会被推到一个job数组中被批量调用