当前位置:   article > 正文

Vue2源码探析之数据响应式原理+面试过程中如何回答Vue2响应式原理

vue2响应式原理

面试题:(【红色字体部分】内的内容是更深层次,可选讲,锦上添花,如果不能理解可以不讲)

1.Vue2是如何实现响应式的?(由浅到深,不再忽悠)

        首先,Vue2实现响应式的原理核心之一是利用Object.defineProperty()函数,他的主要作用就是数据的劫持/代理,既然要实现响应式,那么就需要有触发者和响应者,数据是触发者,那么Object.defineProperty()就是监视数据的变化,主要通过set和get方法,在我们访问数据的时候会触发get方法,例如obj.a,当我们修改数据的时候就会触发set方法,例如obj.a=6,这样我们就能知道数据在变化了,但是由于set修改后的值无法及时被get捕捉到,我们就需要让get的return值返回一个变量,而且是一个需要保留的变量,下次访问还希望存在,但同时不希望声明成全局变量污染环境,于是用到了闭包,我们将Object.defineProperty()函数封装成defineReactive()函数方法,我们在函数内全局声明var tep 将他作为get的返回值,这样数据就实时更新了,到此我们就及时捕捉到数据的变化了,换而言之就是数据能够作为触发者了

【由于数据类型中对象和数组比较特殊,数组也是一种对象,或者可以为了处理复杂对象类型(对象类嵌套对象),我们需要将对象内的所有数据都进行响应式处理,就需要做递归处理,这里的递归不同与普通递归,而是循环调用的递归,需要用到三个函数分别是observe、Observer类、defineReactive(下方图可以帮助理解),他们的作用分别是observe是入口函数(启动函数)他是检测一层是否已经被响应式处理过,也就是检测该层也没有__ob__属性(注意他只是检测,__ob__不是这里加的),如果没有响应式处理则会new Observer()实例(或者可以理解调用了Observer类),Observer则是一个类,如果调用该类就证明没有响应式,那么他会先为该层加上__ob__属性(方便今后判断),然后将这层的数据进行响应式处理,也就是通过调用defineReactive函数对数值进行响应式(比如这层有a:5 b:{c:3}  那么他就会对5和{c:3} 进行响应式处理),最后是defineReactive()函数是实现具体数据的响应式,但是由于传进来的可能还是一个对象,在defineReactive函数中又会调用一次入口函数observe函数,自此就形成了循环调用的递归,值得注意的是observe还有一个功能是判断传入值是否为对象,如果是不是对象直接return,循环就结束了,所以也不存在死循环,数组的响应式主要是通过改写数组的七种方法分别是psuh、pop、shift、unshift、splice、sort、reverse,也就是对数据的增删改排序等方法,其实实现响应式的的思想就是数据发生变化的时候我能够监测到,并做出反应,实现手段是通过以Array.prototype为原型创建一个arrayMethods的对象,对七种方法的改写就写在其中,然后通过Object.setPrototypeOf(value,arrayMethods); 修改数组__proto__指向,使其指向arrayMethods后再指向Array.prototype原型,这样做的目的就是当调用七种方法的时候,先去改写的arrayMethods对象中找函数,如果没有再去下一层,这里就是利用原型链的知识,js调用方法会沿着原型链一直往下找,这样就能在arrayMethods监视到数据的修改并且做处理了,值得注意这里arrayMethods方法中最终还是需要指回Array.prototype,因为除了改写的七种方法以外数组还有很多其他方法,所以提前备份Array.prototype中的方法,再修改七种方法后,需要将apply方法恢复原有的功能(七种方法在监视完数据依旧要实现原有的功能,如果只改变指向不恢复功能,那就监测到变化,但是实际未去做数组增删改的操作),所以这里也埋下了一个问题,那就是通过arr[2],这种数组下标修改数据无法被检测到,这也是Vue2的一个经典问题,但是Vue3实现响应式用到的proxy代理对象解决了这个问题,与Vue2不同的是proxy修改的是代理对象,而不是源对象,这样一旦数据发生变化能够及时捕获变化。】

那么有了触发者就要有响应者,就用到了Watcher类和Dep类,(PS:最底部有更详细通俗总结)他们主要是利用了两个点一个是订阅发布模式,还有就是在get中收集依赖,在set中触发依赖,Watcher主要作用就是在数据get函数触发的时候将依赖收集到Dep类中储存起来,Dep类更像是一个记事本,他将依赖存储到一个数组中,当数据发生变化的时候,就会触发set方法,set方法就会触发Dep类中的通知方法,Dep实例会循环通知列表中与依赖相关watcher(s),然后watcher(s)就会去dom做一个视图的更新,这样就完成了数据的响应式

        Tips:依赖听起来可能有点抽象,其实他很通俗,比如一个页面或组件用到了哪些数据,那么这些数据更新的时候对应的页面和组件就应该发现变化,那么就可以说这个页面或者组件依赖这些数据,由于数据很大,所以提出了watcher这个概念,可以更小范围管理数据,避免一个数据变化整个页面都去更新开销有点大,有了watcher就可以进行组件局部更新减小开销

一、简单数据实现响应式

1.Object.defineProperty()主要作用是数据劫持/数据代理

定义:直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并且返回此对象。

属性值:(部分/常用)

①定义一个对象上的新属性,obj中的a和b属性

  1. var obj ={}
  2. Object.defineProperty(obj,'a',{
  3. value:3
  4. })
  5. Object.defineProperty(obj,'5',{
  6. value:5
  7. })
  8. console.log(obj)
  9. console.log(obj.a,obj.b)
  10. 打印结果是:
  11. {a:3,b:5}
  12. 3 5

②writable定义一个属性是否可写,当a属性设置writeable:false的时候,obj.a++不能够使对象中的a值自增

③enumerable顶一个属性是否可以被枚举,当b属性设置enumerable:false的时候,obj.b就不能通过for或者forEach等遍历循环出来,常用在定义一些不想被修改值,比如响应式的ob的这类值

④get和set函数(重要)

  1. Object.defineProperty(obj,'a',{
  2. get(){
  3. console.log(“你试图访问obj的a属性”)
  4. }
  5. set(){
  6. console.log(“你试图改变obj的a属性”)
  7. }
  8. })

这里需要注意get和value不能同时存在,如果设置get就需要删除value:3,这里其实就是数据的劫持了,劫持的作用其实就是希望在属性被访问或者改变的时候做点事情,所以有了get和set

2.defineReactive()

背景:①get被访问的时候需要通过return返回值,但是如果返回的是常量,set修改后无法即使返回;②需要保留变量并且不污染全局环境,代码如下

  1. var obj ={}
  2. Object.defineProperty(obj,'a',{
  3. get(){
  4. console.log(“你试图访问obj的a属性”)
  5. return 7
  6. }
  7. set(newValue){
  8. console.log(“你试图改变obj的a属性”,newValue)
  9. }
  10. })
  11. console.log(obj.a)
  12. obj.a = 9
  13. console.log(obj.a) // 7

打印结果是:

你试图访问obj的a属性 7

你试图改变obj的a属性 9

你试图访问obj的a属性 7

解释:

虽然修改了a的值,也在set中打印出来了,但是无法及时更新给obj.a,所以这个时候就需要一个中转变量来解决这个问题了。

解决方案:

  1. var temp;
  2. Object.defineProperty(obj,'a',{
  3. get(){
  4. console.log(“你试图访问obj的a属性”)
  5. return temp
  6. }
  7. set(newValue){
  8. console.log(“你试图改变obj的a属性”,newValue)
  9. temp = newValue
  10. }
  11. })

这样就能解决这个问题了,能够及时返回更新后的值,这个时候又面临一个新的问题,需要定义一个变量他是会被保留的,但是又不希望是全局的,会污染变量环境,这个时候就需要用到闭包了,所以需要封装一个defineReactive()函数。

  1. var obj = {}
  2. function defineReactive(data,key,val){
  3. //如果传入的是两个参数值,值等于对象本身值
  4. if (arguments.length == 2) {
  5. val = data[key]
  6. }
  7. Object.defineProperty(data,key,{
  8. //可枚举
  9. enumerable:true,
  10. //可以被配置,比如可以被delete
  11. configurable:true,
  12. //getter
  13. get(){
  14. console.log('访问obj的a属性')
  15. return val;
  16. }
  17. set(newValue){
  18. console.log('修改obj的a属性',newValue)
  19. //当新值和旧值一样的时候直接return无需修改
  20. if(val === newValue) {
  21. return;
  22. }
  23. val = newValue
  24. }
  25. })
  26. }

这样,我们就可以通过defineReactive(obj,'a',10)定义obj对象的a属性值,这样就实现了简单的数据响应式,当我们进行数据访问和修改的时候都会做出反应,当我们能够监视到数据的变化就可以依据变化做出我们需要的操作了。

二、复杂对象实现响应式(对象内嵌套对象):

背景:

  1. var obj = {
  2. a:{
  3. m:{
  4. n:5
  5. }
  6. }
  7. b:3
  8. }

如上述代码,当一个对象内嵌套对象的时候,无法通过obj.a.m.n响应式的访问到内部数据,对象内的值n还没有实现响应式,即使能够访问到数据也无法被劫持或者说被监视到。

1.定义Observer类:

作用:

将一个正常的object传换为每个层级的属性都是响应式(可以被监视到的值)的object。

  1. export default class Observer {
  2. //构造器
  3. constructor(value){
  4. //1.给实例(this,一定要注意,构造函数中的this不是表示类本身,而是实例,也就是this指向的是实 例本身)2.这里的def就是下述1.2中定义的
  5. def(value,'ob',this,false)
  6. console.log("我是Observer构造器",value) //能够在实例中看到ob属性,先标记
  7. //下面就是Observer的作用,将这层的函数转换成响应式
  8. this.walk(value)
  9. }
  10. //遍历
  11. walk(value) {
  12. for (let k of value){
  13. defineReactive(value,k)
  14. }
  15. }
  16. }

1.1 object一般有很多层级,虽然Observer类能够将每层的数据转换成响应式,但是为了确保每层都被转换成响应式,需要引入一个属性叫__ob__,当一层被Observer转换成响应式后添加这个属性,如果没有就需要调用Observer类将其转换成响应式,以此类推,将一个多层次对象由外到内全部转换成响应式,即可完成一个对象的全部响应式。

  1. //创建observe函数,这里不同Observer类,这里没有r注意区分,他的作用在上述说明了。
  2. function observe(value){
  3. //如果value不是对象,什么都不需要做
  4. if (typeof value != 'object') return;
  5. //定义ob,判断这层是否已经被转换成响应式
  6. var ob
  7. //如果存在ob,将value.ob赋值给ob属性,通过return将其作为observe的返回值返回
  8. if (typeof value.ob !=='undefined') {
  9. ob = value.ob
  10. } else {
  11. ob = new Observer(value)
  12. }
  13. return ob
  14. }

1.2创建utils.js文件,里面存放def函数,他的作用,简化Object.defineProperty()函数,定义一个默认可写和被删除的数据。

  1. export const def = function (obj,key,value,enumerable){
  2. Object.defineProperty(obj,key,{
  3. value,
  4. //是否可被枚举
  5. enumerable,
  6. //是否可写
  7. writable:true,
  8. //是否可被删除
  9. configurable:true
  10. })
  11. }

2.改写defineProperty函数,引入observe属性,实现循环调用,将对象全部属性进行响应式处理。

  1. function defineReactive(data,key,val){
  2. //如果传入的是两个参数值,值等于对象本身值
  3. if (arguments.length == 2) {
  4. val = data[key]
  5. }
  6. // 子元素要进行observe,至此形成了“递归”,这里的递归并不是自己调用自己,而是多个函数、类循环调 用的过程。
  7. let childOb = observe(val)
  8. Object.defineProperty(data,key,{
  9. //可枚举
  10. enumerable:true,
  11. //可以被配置,比如可以被delete
  12. configurable:true,
  13. //getter
  14. get(){
  15. console.log('访问obj的a属性')
  16. return val;
  17. }
  18. set(newValue){
  19. console.log('修改obj的a属性',newValue)
  20. //当新值和旧值一样的时候直接return无需修改
  21. if(val === newValue) {
  22. return;
  23. }
  24. val = newValue
  25. //当设置了新值的时候,这个新值也需要被observe
  26. childOb = observe(newValue)
  27. }
  28. })
  29. }

总结:

首先通过observe(obj)函数启动响应式处理,他的作用是入口也是判断所在层是否被响应式处理,如果没有响应式了就不会有__ob__属性,这个时候就会实例一个类,也是new Observer,将obj作为value参数传入。

  1. if (typeof value.__ob__!=='undefined') {
  2. ob = value.__ob__
  3. } else {
  4. ob = new Observer(value)
  5. }

那么就轮到Observer类发挥作用了,他的作用是为这层对象添加__ob__属性,observe是通过__ob__属性进行判断,而Observer这里是设置__ob__属性,设置完后还需要进行重要的一步,那就是将此层数据进行响应式处理。

①def(value,'__ob__',this,false) //定义__ob__属性,表示这层已经被响应式处理了

②在walk函数这里,通过遍历将这层的所有数据传递给defineReactive()进行响应式处理

  1. walk(value) {
  2. for (let k of value){
  3. defineReactive(value,k)
  4. }
  5. }

如果是简单数据到这里就结束了,但是这里就是讨论是嵌套对象,那么就到下一层了,这里k属性名对应下的属性值还有对象,那么就到defineReactive()这一步了

defineReactive(data,key,val)//这里的val就是属性名对应下的属性值

将值传递给observe()函数,observe函数作用除了判断__ob__是否存在,还能判断他是不是一个对象,如果不是对象直接return,那么相当于val没有被做过任何处理,但是如果是对象的话,这里相当于新的一轮又启动了,这里observe又回到最初的作用,既是鉴别这层有没有被响应式处理过又是入口函数,至此循环调用就起到作用了,这也是巧妙地地方,由于不同于递归,这种循环调用的方式也不用担心死循环的问题,最后一层肯定是一个具体数值,对象里面最终肯定还是为了方便存值,所以到最后一层不是对象后他就return回来了,而childOb也不是循环的判断条件什么的,自然也不存在死循环。

let childOb = observe(val)

还有一个注意点是在set函数里面,新值也需要做一个检测,原因很简单,可能会被一个简单的值改成一个对象,比如b:3改成b:{b1:2,b2:6}那么等于又加深了一个层级,肯定是需要继续做响应式处理的。

val = newValue

//当设置了新值的时候,这个新值也需要被observe

childOb = observe(newValue)

自此,对象的响应式处理算是全部完成了。

三、数据的响应式处理

背景:

数组数据更改的时候无法按照预期监测

解决:

改写数组的七个方法,这七个方法是写在Array原型上的也就是Array.prototype

思路就是以Array.prototype为原型创建一个arrayMethods的对象,他的身上有其中方法,我们都知道原型链的概念,就是找一个方法如果在此对象上没有找到对应的方法就会沿着原型链往下找,数组最终找到的原型就是Array.prototype,这里我们需要改写七种方法,也就是在完成这七个操作的同时,我们希望能够做一些自定义的操作,所以我们就改变数组的原型链指向,先指向我们创建的arrayMethods对象,经过处理后再去指向

1.改写数组方法

  1. //得到Array.prototype
  2. const arrayPrototype = Array.prototype
  3. // 以Array.prototype为原型创建arrayMethods对象并且暴露出去,为Observer判断所用
  4. export const arrayMethods = Object.create(arrayPrototype)
  5. // 要被改写的七个数组方法
  6. const methodsNeedChange = [
  7. 'push',
  8. 'pop',
  9. 'shift',
  10. 'unshift',
  11. 'splice',
  12. 'sort',
  13. 'reverse'
  14. ]
  15. methodsNeedChange.forEach(methodName=>{
  16. //备份原有的方法,这步很巧妙,因为改写只有七个方法,原来还有很多方法,并且改写后数组依旧需要有 原来的功能,所以这步必不可少
  17. const original = arrayPrototype[methodName]
  18. //定义新的方法
  19. def(arrayMethods,methodName,function(){
  20. console.log("123")
  21. //这里有需要注意的点就是包裹函数不能写成箭头函数,如果是箭头函数this指向就出问题了,目前this指 向的就是调用者也就是数组,如果是箭头函数也没有arguments了,所以这里必须是这样,这里的this就是 数组的上下文,例如a.push(1,2,3)那么push就是this,(1,2,3)就是arguments或者说参数
  22. //恢复原有功能,通过绑定this的指向,并且调用原有方法,bind不会调用,apply和call会调用
  23. original.apply(this,arguments)
  24. },false)
  25. })

1.1改写Observer函数的构造器使其能够检测到数组

  1. constructor(value){
  2. //1.给实例(this,一定要注意,构造函数中的this不是表示类本身,而是实例,也就是this指向的是实 例本身)2.这里的def就是下述1.2中定义的
  3. def(value,'ob',this,false)
  4. console.log("我是Observer构造器",value) //能够在实例中看到ob属性,先标记
  5. //下面就是Observer的作用,将这层的函数转换成响应式
  6. **判断他是数组还是对象
  7. if (Array.isArray(value){
  8. //如果是数组,就强行将这个数组的原型指向arrayMethods
  9. Object.setPrototypeOf(value,arrayMethods);
  10. //让这个数组变observe
  11. this.observeArray(value)
  12. }else{
  13. this.walk(value)
  14. })
  15. //数组的特殊遍历
  16. observeArray(arr) {
  17. for (let i=0,l=arr.length;i<l;i++){
  18. //逐项进行observe
  19. observe(arr[i])
  20. }
  21. }
  22. }

1.2

  1. methodsNeedChange.forEach(methodName=>{
  2. //备份原有的方法,这步很巧妙,因为改写只有七个方法,原来还有很多方法,并且改写后数组依旧需要有 原来的功能,所以这步必不可少
  3. const original = arrayPrototype[methodName]
  4. //定义新的方法
  5. def(arrayMethods,methodName,function(){
  6. // 把这个数组身上的ob取出来,ob已经被添加了,为什么已经被添加了?因为数组肯定不是最高层,比 如Vue初始化数组的时候有一个data对象,然后将数组对象放在里面,所以Observer在初始化data对象 的时候就为数组对象添加了ob属性,因为数组对象其实也是一个对象,所以会为他加上ob属性,所以能 够拿到
  7. //因为arguments是类数组对象,要将其转换成数组对象
  8. const args = [...arguments]
  9. const ob = this.ob
  10. //有三种方法push\unshift\splice能够插入新项,现在要把插入的新项也要变成observe的,也就是响应 式的
  11. let inserted = []
  12. swith(methodName) {
  13. case 'push':
  14. case 'unshift':
  15. inserted = args
  16. break
  17. case 'splice':
  18. //因为splice方法使用的时候是splice(下标,数量,插入的新项),所以这里取2
  19. inserted = args.slice(2)
  20. break
  21. }
  22. //让新项也变成响应式的
  23. if(inserted){
  24. ob.observeArray(inserted)
  25. }
  26. console.log("123")
  27. //这里有需要注意的点就是包裹函数不能写成箭头函数,如果是箭头函数this指向就出问题了,目前this 指向的就是调用者也就是数组,如果是箭头函数也没有arguments了,所以这里必须是这样,这里的this 就是数组的上下文,例如a.push(1,2,3)那么push就是this,(1,2,3)就是arguments或者说参 数
  28. //恢复原有功能
  29. original.apply(this,arguments)
  30. },false)
  31. })

完成以上部分就能够做到数组的响应式,并且能够通过七个方法对数组操作的时候被监视到,但是这里有一个需要注意的点,也是vue2的一个经典问题,那就是如果你通过obj.g[3] = 56这种方法修改是无法被监视到的,因为它不属于七个方法中的任一个。

四、依赖收集

概念:

需要用到数据的地方,就叫依赖,用我的话说就是如果一个数据变化会影响到其他数据跟着变化,那么我就可以说其他数据依赖这个数据,收集依赖也就是收集这些会被我影响的数据。

Vue1.x,细粒度依赖,用到数据的DOM都是依赖

Vue2.X,中等粒度依赖,用到数据的组件是依赖

Vue的特点:

在getter中收集依赖,在setter中触发依赖

1.Dep类和Watcher类

1.1 Dep类

作用:把依赖收集的代码封装成一个Dep类,他专门用来管理依赖,每个Observer实例,成员中都有一个Dep的实例

1.2 Watcher类

作用:他是一个中介,当数据发生变化的时候通过Watcher中转,通知组件

2.实现思路:

依赖就是watcher。只有watcher触发的getter才会收集依赖,哪个watcher触发了getter,就把哪个watcher收集到Dep中

Dep使用发布订阅模式,当数据发生变化时,会循环依赖列表,把所有的watcher都通知一遍

代码实现的巧妙之处:watcher把自己设置到全局的一个指定位置,然后读取数据,因为读取了数据,所以会触发这个数据的getter。在getter中就能得到当前正在读取数据的watcher,并把这个watcher收集到dep中。

3.Dep类代码结构:

  1. export default class Dep {
  2. constructor() {
  3. //用数组存贮自己的订阅者,subs是英语订阅者的意思,全称subscribes
  4. //这个数组里面放的是Watcher的实例
  5. this.subs = []
  6. }
  7. //添加订阅
  8. addSub(){
  9. this.subs.push(sub)
  10. }
  11. // 添加依赖
  12. depend(){
  13. //Dep.target就是一个我们自己指定的全局的位置,和window.target一样,只要是全局唯一,没有歧义就可以了
  14. ifDep.target){
  15. this.addSub(Dep.target)
  16. }
  17. }
  18. //通知更新
  19. notify(){
  20. //浅克隆一份
  21. const subs = this.subs.slice()
  22. //遍历通知订阅者
  23. for (let i = 0,l=subs.length; i<l;i++){
  24. subs[i].update()
  25. }
  26. }
  27. }

3.1 在Observer构造器中对dep类进行实例化

  1. constructor(value){
  2. **实例化dep,每一个Observer的实例身上都有一个dep,作用就是存储收集的依赖
  3. this.dep = new Dep()
  4. //1.给实例(this,一定要注意,构造函数中的this不是表示类本身,而是实例,也就是this指向的是实 例本身)2.这里的def就是下述1.2中定义的
  5. def(value,'ob',this,false)
  6. console.log("我是Observer构造器",value) //能够在实例中看到ob属性,先标记
  7. //下面就是Observer的作用,将这层的函数转换成响应式
  8. **判断他是数组还是对象
  9. if (Array.isArray(value){
  10. //如果是数组,就强行将这个数组的原型指向arrayMethods
  11. Object.setPrototypeOf(value,arrayMethods);
  12. //让这个数组变observe
  13. this.observeArray(value)
  14. }else{
  15. this.walk(value)
  16. })
  17. //数组的特殊遍历
  18. observeArray(arr) {
  19. for (let i=0,l=arr.length;i<l;i++){
  20. //逐项进行observe
  21. observe(arr[i])
  22. }
  23. }
  24. }

3.2在defineReactive闭包中实例化

  1. function defineReactive(data,key,val){
  2. //**在闭包中实例化一个Dep实例
  3. const dep = new Dep()
  4. //如果传入的是两个参数值,值等于对象本身值
  5. if (arguments.length == 2) {
  6. val = data[key]
  7. }
  8. // 子元素要进行observe,至此形成了“递归”,这里的递归并不是自己调用自己,而是多个函数、类循环调 用的过程。
  9. let childOb = observe(val)
  10. Object.defineProperty(data,key,{
  11. //可枚举
  12. enumerable:true,
  13. //可以被配置,比如可以被delete
  14. configurable:true,
  15. //getter
  16. get(){
  17. console.log('访问obj的a属性')
  18. //如果现在处于以来的收集阶段
  19. if (Dep.target) {
  20. dep.depend()
  21. if(childOb){
  22. childOb.dep.depend()
  23. }
  24. }
  25. return val;
  26. }
  27. set(newValue){
  28. console.log('修改obj的a属性',newValue)
  29. //当新值和旧值一样的时候直接return无需修改
  30. if(val === newValue) {
  31. return;
  32. }
  33. val = newValue
  34. //当设置了新值的时候,这个新值也需要被observe
  35. childOb = observe(newValue)
  36. //*发布订阅通知,通知dep
  37. dep.notify()
  38. }
  39. })
  40. }

②同样数组这里也需要notify

  1. //让新项也变成响应式的
  2. if(inserted){
  3. ob.observeArray(inserted)
  4. }
  5. ob.dep.notify()
  6. 4.Watcher类代码结构:
  7. var uid = 0
  8. export default class Watcher{
  9. constructor(target,expression,callback){
  10. this.id = uid++
  11. this.target = target
  12. this.getter = parsePath(expression)
  13. this.callback = callback
  14. this.value = this.get()
  15. }
  16. update(){ ​ this.run()
  17. }
  18. get(){ ​ //进入依赖收集阶段,让全局的Dep.target设置为Watcher本身,那么就是进入依赖收集
  19. Dep.target = this
  20. const obj = this.target
  21. var value
  22. //只要能够找到就一直找
  23. try{
  24. value = this.getter(obj)
  25. }finally{
  26. Dep.target = null
  27. }
  28. return value
  29. }
  30. run(){
  31. this.getAndInvoke(this.callback)
  32. }
  33. getAndInvoke(){
  34. const value = this.get()
  35. if (value !== this.value || typeof value == 'object'){ ​ const oldValue = this.value
  36. this.value = value
  37. cb.call(this.target,value,oldValue)
  38. }
  39. }
  40. }

4.1在defineReactive函数的getter中定义收集函数

  1. get(){
  2. console.log('访问obj的a属性')
  3. //如果现在处于以来的收集阶段
  4. if (Dep.target) {
  5. dep.depend()
  6. if(childOb){
  7. childOb.dep.depend()
  8. }
  9. }
  10. return val;
  11. }

4.2parsePath是如何识别点语法的a.b.c.d的字符.

  1. function parsePath(str) {
  2. var segments = str.split(".")
  3. return (obj) => {
  4. for(let i=0;i<segments.length;i++){
  5. //如果.的值不存在就返回
  6. if(!obj) return
  7. // 这里obj分别是a:{b:{...}}、b:{c:{...}}、c:
  8. //{f:44,d:55:e:66};
  9. //obj[segments[i]]分别是a['b']、b['c']、c['d']
  10. //然后return回去的就是需要的结果数值,这里值得注意的是obj是
  11. //一个变量
  12. //他在逐层剥壳,直到拿到值为止,因为不能对象不能通过a.d直接
  13. //拿到最底层的值
  14. //而是需要一层一层拿下去,有几个点(.)就有几层,通过变量
  15. //obj一层一层循环
  16. //逐渐拿到最底层需要的值,函数的巧妙之处在于obj是变量每次都
  17. //更深一层
  18. obj = obj[segments[i]]
  19. }
  20. return obj
  21. }
  22. }
  23. var fn = parsePath('a.b.c.d')
  24. var v = fn({
  25. a:{
  26. b:{
  27. c:{
  28. f:44,
  29. d:55,
  30. e:66
  31. }
  32. }
  33. }
  34. })
  35. console.log(v) // 55

5.Watcher函数的应用

  1. new Watcher(obj,'a.m.n',(val)=>{
  2. console.log('@@@@',val)
  3. })

这样一旦修改了obj.a.m.n的值就会执行箭头函数,这个是不是就很像我们平时用的watch函数,Vue就是这样实现响应式的,这就是响应式的原理

例如这里obj.a.m.n = 66

控制台就会自动打印@@@@ 66

这不就是我们知道响应式嘛?

总结一下流程就是:

比如我们创建一个页面或组件,页面或组件在初次渲染的时候当遇到插值表达式或者说就是我们绑定动态数值的时候,就会实例化一个watcher,从watcher的代码结构中可以看出,实例化的过程中会执行get方法,get的触发就可以收集依赖了,在get函数处我们可以看到if(Dep.target)如果存在就加入依赖,相当于在这个页面或组件的视图(dom)更新会依赖到这个数据,数据又是不止一个的,有很多,所以在这里收集的依赖会被存储到Dep中,Dep是我们创建的一个类,我们会为这个动态数据实例化一个dep,他的作用就是拿来存储dom更新的时候需要依赖的数据有哪些?那么数据更新的时候,就会触发dep.notify()方法也就是会触发我们实例化的一个dep,之前说了每一个动态数据的时候我们都会实例化一个dep,这个dep是用来存储依赖列表的,而dep.notify就会循环依赖列表通知里面的watcher,然后watcher根据回调函数去dom做及时更新,更新的方法就写在watcher实例的回调函数这里了,比如更新后我需要他a+3显示,这样就完成了视图的即使更新,也是Vue精妙的地方,在get函数的地方通过watcher收集依赖存储到dep列表中,然后set函数更新的时候dep循环通知依赖的watcher,然后watcher根据回调去dom做视图的更新

顺便提一嘴,这个就是订阅发布模式,就像我们订阅某个公众号一样(Watcher),微信会将我们的订阅列表存储起来(Dep),一旦这里有公众号更新了,微信会先收到然后审核后(这就是Wacther的回调函数cb,也可能什么都不做)推送给我们。微信作为一个收发中心帮我们解决了很多问题,订阅的人只需要关注感兴趣的公众号,公众号只需要推送新内容即可,由微信作为路由收发,处理的逻辑也在微信这里 这样的解耦特别利于代码的维护和更新,我们只需要各司其职就好了,听着好像微信没有什么作用一样,但是公众号和关注者之间其实有很复杂的表关系,由微信处理后分担了两者的压力,他们只需要专注于内容即可。

这里的Watcher和我们平时用的watch函数很接近,我们平时用的watch函数其实就是一个watcher实例,比如有些时候组件内一些没有被监视到的数据,dom的元素又与之密切相关,这个时候我们就会写一个watch函数监视这个值的变化,当他发现变化的时候,即使通过我们写的cb回调函数进行处理做及时的视图更新,还有一个比较常用deep:true属性其实就是我们observer函数,如果为true我们就循环遍历下去监视这个obj里面的所有属性,如果为false当一个obj内部的属性比如obj.a发生变化的时候就无法检测到,因为obj的内存地址没有发生变化,顺提一嘴为什么没发现变化,因为对象是以键值对形式存贮内存地址,如图变量名obj对应的值0x123就是他的内存地址,而实际数据是保存在内存地址指向堆内存中,那么修改堆内数据,内存地址是不会发生变化的,所以无法被检测。

 参考视频:

【尚硅谷】Vue源码解析之数据响应式原理

参考文章:

【争霸爱好者】0年前端的Vue响应式原理学习总结1:基本原理

参考图片:

【尚硅谷】Vue源码解析之数据响应式原理

【兜里还剩五块出头】js基本数据类型与对象的存储方式

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/不正经/article/detail/248170
推荐阅读
相关标签
  

闽ICP备14008679号