浅析 vue2 数据响应式原理

Title:浅析 vue2 数据响应式原理

Date:2022-04-27

什么是数据响应式对象本身对象属性被读和写的时候,我们需要知道该数据被操作了,并在这过程中执行一些函数,例如:render函数,而这一过程我把它定义为数据响应式。

那么vue具体是如何实现数据响应式的呢?接下来我们通过vue的源码探究一下响应式数据的始末。

响应式数据源码./src/core/observer下面

在这里插入图片描述

在具体实现上面,vue用到了4个核心部件:

  1. Observer
  2. Dep
  3. Watcher
  4. Scheduler

Observer

Observer的目的很简单,它主要就是把一个普通的对象转换成响应式的对象。

那Observer到底是如何做到把一个普通对象转换成响应式对象的呢?

为了实现这一点,Observer通过object.defineProperty将一个普通对象包装成一个带有getter/setter属性的特殊对象,当访问属性的时候会调用getter,修改属性的时候会调用setter,这样一来,我们就可以知道数据什么时候被读写了。

知道实现逻辑了,那我们就来实现一个简单的响应式数据吧!

首先我们先定义一个普通对象

const obj = {
    a: 1,
    b: 2
}

console.log(obj);

很显然,这个对象它并不具备响应式,从控制台输出就可以看得出来

在这里插入图片描述

接下来我们通过Object.defineProperty来改写上面的对象


let obj = {
  b:2
}
let val = 1
Object.defineProperty(obj, 'a', {
    enumerable: true,
    configurable: true,
    get() {
        console.log('a被读取了')
        return val
    },
    set(newVal) {
        console.log('a被修改了')
        obj.a = newVal
    }
})

在这里插入图片描述

这下就很明显了,a属性和b属性完全不同了,当对a读写对时候会就回出发相应的getter/setter方法

在这里插入图片描述

Observer 的核心代码如下


export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    /* 
     * 将Observer实例绑定到data的__ob__属性上面去,
     * observe的时候会先检测是否已经有__ob__对象存放Observer实例了,
     * def方法定义可以参考https://github.com/vuejs/vue/blob/dev/src/core/util/lang.js#L16 
     */
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)  /*直接覆盖原型的方法来修改目标对象*/
      } else {
        copyAugment(value, arrayMethods, arrayKeys)  /*定义(覆盖)目标对象或数组的某一个方法*/
      }
      /*如果是数组则需要遍历数组,将数组中的所有元素都转化为可被侦测的响应式*/
      this.observeArray(value)
    } else {
      /*如果是对象则直接walk进行绑定*/
      this.walk(value)
    }
  }

从Observer的源码可以看出,Observer对对象和数组的响应式处理有所不同,如果是对象就直接调用walk,遍历每一个对象并且在它们上面绑定getter与setter,如果是数组则需要遍历数组,将数组中的所有元素都转化为可被侦测的响应式

1.Object
在这里插入图片描述

walk (obj: Object) {
    const keys = Object.keys(obj)
    /*walk方法会遍历对象的每一个属性进行defineReactive绑定*/
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
    
  //...
  
  /*对象的子对象递归进行observe并返回子节点的Observer对象*/
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
       /*如果原本对象拥有getter方法则执行*/
      const value = getter ? getter.call(obj) : val
      // ...
      return value
    },
    set: function reactiveSetter(newVal) {
      /*通过getter方法获取当前值,与新值进行比较,一致则不需要执行下面的操作*/
      const value = getter ? getter.call(obj) : val
      // ...
      val = newVal
      
      /*新的值需要重新进行observe,保证数据响应式*/
      childOb = !shallow && observe(newVal)
      
    }
  })
}

总之就是递归遍历对象的所有属性,以完成深度属性转换

2.Array

如果是数组,vue会重写数组的一些方法,更改Array的隐式原型,之所以要这样做,是因为vue需要监听哪些方法可能改变数组数据。分别重写了这些方法:push, pop, shift, unshift, splice, sort, reverse

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hf7fxgIz-1651081546729)(/Users/luck200217/Desktop/6.png)]


if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)  /*直接覆盖原型的方法来修改目标对象*/
      } else {
        copyAugment(value, arrayMethods, arrayKeys)  /*定义(覆盖)目标对象或数组的某一个方法*/
      }
      /*如果是数组则需要遍历数组,将数组中的所有元素都转化为可被侦测的响应式*/
      this.observeArray(value)
}

/* 位置:./src/core/observer/array.js
 * 改变数组自身内容的7个方法
 */
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/*
 * 这里重写了数组的这些方法,
 * 在保证不污染原生数组原型的情况下重写数组的这些方法,
 * 截获数组的成员发生的变化,
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]     // 缓存原生方法
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)  /*调用原生的数组方法*/
    /*数组新插入的元素需要重新进行observe才能响应式*/
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    return result
  })
})


总结:Observer的目标就是,当对象的属性被读写,数组的数据被增删改时都要被vue感知到。

Dep

Observer只是让vue感知到数据被读写了,但是接下来究竟要干什么就需要Dep来解决了。

Dep的含义是Dependency,表示依赖的意思,vue会为对象中的每一个属性,对象本身,数组本身创建一个Dep实例,而每个Dep实例都会做两件事:

  • 搜集依赖,即谁在使用该数据,
  • 通知依赖更新,即当数据发生改变的时候,通知依赖更新,

总结一句话就是:在getter中收集依赖,在setter中通知依赖更新

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xfy3S7hx-1651081546730)(/Users/luck200217/Desktop/7.png)]



// ./src/core/observer/index.js
/*为对象defineProperty上在变化时通知的属性*/
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  /*定义一个dep对象*/
  const dep = new Dep() 
  
	//...
  
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
   		// ...
      if (Dep.target) {
        /*进行依赖收集*/
        dep.depend()
        if (childOb) {
           /* 
            * 子对象进行依赖收集,
            * 其实就是将同一个watcher观察者实例放进了两个depend中,
            * 一个是正在本身闭包中的depend,另一个是子元素的depend
            */
          childOb.dep.depend()
          if (Array.isArray(value)) {
            /*是数组则需要对每一个成员都进行依赖收集,如果数组的成员还是数组,则递归。*/
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      
      // ...
      
      /*dep对象通知所有的观察者*/
      dep.notify()
    }
  })
}



/**
 * ./src/core/observer/dep.js
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  /*添加一个观察者对象*/
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  /*移除一个观察者对象*/
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  /*依赖收集,当存在Dep.target的时候添加观察者对象*/
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  /*通知所有订阅者*/
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}


Watcher

Watcher又是干什么的呢?

dep收集依赖后,当数据发生改变,准备派发通知的时候,不知道该派给谁,或者说不知道谁用了该数据,于是就需要watcher了。

当某个函数在执行的过程中,使用到了响应式数据时,vue就会为响应式数据创建一个watcher实例,当数据发生改变时,vue不直接通知相关依赖更新,而是通知依赖对应的watcher实例去执行。

watcher会设置一个全局变量window.targe,让全局变量记录当前负责执行的watcher等于自己,然后在执行函数,在执行的过程中,如果发生了依赖记录dep.depenf(),那么Dep会把这个全局变量记录下来,表示有一个watcher实例用到了这个响应式数据。

watcher核心源代码


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
  }
}

我们分析Watcher类的代码实现逻辑:

  1. 当实例化Watcher类时,会先执行其构造函数;
  2. 在构造函数中调用了this.get()实例方法;
  3. get()方法中,首先通过window.target = this把实例自身赋给了全局的一个唯一对象window.target上,然后通过let value = this.getter.call(vm, vm)获取一下被依赖的数据,获取被依赖数据的目的是触发该数据上面的getter,上文我们说过,在getter里会调用dep.depend()收集依赖,而在dep.depend()中取到挂载window.target上的值并将其存入依赖数组中,在get()方法最后将window.target释放掉。
  4. 而当数据变化时,会触发数据的setter,在setter中调用了dep.notify()方法,在dep.notify()方法中,遍历所有依赖(即watcher实例),执行依赖的update()方法,也就是Watcher类中的update()实例方法,在update()方法中调用数据变化的更新回调函数,从而更新视图。

参考文档

Scheduler

当在setter中调用了dep.notify()方法,在dep.notify()方法中,遍历所有依赖(即watcher实例)时,如果watcher执行重运行对应的函数,就会导致函数频繁执行,从而降低了效率,试想一下,如果一个函数,里面用到了a,b,c,d等响应式数据,这些数据都会记录依赖,于是当这些数据发生变化时会触发多次更新,例如:

state.a = "new value";
state.b = "new value";
state.c = "new value";
state.d = "new value";
...
// 每更新一个值触发一次更新

这样显然是不合适的,因此,当watcher收到派发的更新通知后,watcher不会立即执行,而是将自己交给一个调度器scheduler

调度器scheduler维护一个执行队列,同一个watcher在该队列中只会存在一次,队列中的watcher不会立即执行,而是通过nextTick的工具函数执行,nextTick是一个微队列,会把需要执行的watcher放到事件循环的微队列中执行。

nextTick的具体做法是通过Promise完成的,具体实现方法专利暂时不探讨,nextTick文档

总结

  1. vue首先通过Observer类,使用Object.defineProperty方法包装了数据,使object变成一个具有getter/setter属性的数据。

  2. 读取数据的时候通过getter方法读取,并在getter方法里面调用了Dep模块的dep.depend()方法收集依赖,并为该依赖创建一个对应的watcher实例。

  3. 通过setter方法改变数据的时候调用了Dep模块的dep.notify()方法来通知依赖,即依赖对应的watcher实例,遍历所有的watcher实例。

  4. watcher实例不直接更新视图,而是交给scheduler调度器,scheduler维护一个事件队列通过nextTick执行事件,从而更新视图。

流程图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cqwSd1NZ-1651081546730)(/Users/luck200217/Desktop/8.png)]

补充

  1. Observer发生在beforeCreatecreated之间,

  2. 由于遍历时只能遍历到对象的当前属性,因此无法监测到将来动态增加或删除的属性。

    // html
    <template>
      <div class="hello">
        <h1>a:{{ obj.a }}</h1>
        <h1>b:{{ obj.b }}</h1>
        <button @click="obj.b = 2">Add B</button>
      </div>
    </template>
    
    // js
    <script>
    export default {
      name: "HelloWorld",
      data() {
        return {
          obj: {
            a: 1,
          },
        };
      },
    };
    

在这里插入图片描述

当点击 Add B动态给obj添加b属性时,obj数据更新了,但是页面没有展示,由此可见之后动态添加和删除的数据不具备响应式特性。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5x8HwXyU-1651081546730)(/Users/luck200217/Desktop/10.png)]

因此vue提供了$set$delete两个实例方法来解决这种情况。

// 新增
this.$set(this.obj, b, 2)

//删除
this.$delete(this.obj, b)

以上仅个人理解,如有不当之处还请不吝赐教

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值