一个超级简易的vue响应式的实现,适合新手理解vue响应式原理

269 阅读3分钟

我们都知道在vue2中,响应式是发布订阅模式结合Object.defineProperty实现的。但是面试中往往回答到这里是远远不够的。最近准备面试,所以就梳理一下vue2中的响应式的实现原理,一步一步剖析,并以代码的形式记录下来,希望对自己对大家有所帮助。由于是第一次分析源码,可能有些地方不深入,甚至会有错误,多多包涵,发现有错的地方希望大家能指出来,共同进步。

这里简单实现一个响应式

首先创建一个Obeserve类,用来实例化一个响应式对象。

class Observer{
  constructor(value){
    this.value = value
    // 判断是数组先暂时不用管待会儿补充
    if(Array.isArray(value)){
      return 
    }
    this.walk(this.value)
  }
  walk(obj){
    const keys = Object.keys(obj);
    console.log('当前监听对象的所有属性名',keys);
    for(let i = 0;i<keys.length;i++){
      defineReactive(obj,keys[i],obj[keys[i]])
    }
  }
  
}
function defineReactive(obj,key,val){
  Object.defineProperty(obj,key,{
    get(){
      console.log('属性被读取')
      return val
    },
    set(newval){
      if(newval!=val){
        console.log('属性值被设置了')
        val = newval
      }
    }
  })
}

let obj = new Observer({
  name:'DGT',
  age:'26',
  number:'18229282791'
})

到这里,一个简单的对象数据的响应式监听就完成了。 接下来要做的就是依赖收集。

我们都知道数据变更就会通知视图进行更新,但是视图很大,它是怎么知道哪部分视图应该被更新呢?肯定会想到,谁用到了这个数据就更新谁,我们可以为每一个响应式数据创建一个管理器,用来存放当前数据被哪些视图所使用(依赖),这个过程就可以被称为依赖收集。

总结:在响应式数据的getter中进行依赖收集,在setter中进行通知依赖更新。

下面我们来写一个依赖收集器用来管理收集的依赖。

class Dep{
  constructor(){
    this.subs = []; // 实例化的dep都有一个subs用来存放当前响应式对象所有收集到的依赖
  }
  addSub(sub){
    this.subs.push(sub)
  }
  // 删除依赖
  removeSub(){
    remove(this.subs, sub)
  }
  // 添加一个依赖
  depend(){
  // 全局唯一target用来指定当前的依赖
    if (window.target) {
      this.addSub(window.target)
    }
  }
  // 通知所有依赖更新
  notify(){
    const subs = this.subs.slice(); // 浅拷贝一份数据
    for(let i = 0,i<subs.length;i++){
      subs[i].update(); // //调用对应依赖的视图更新方法
    }
  }
}
/**
 * Remove an item from an array
 */
export function remove (arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item)
    if (index > -1) {
      return arr.splice(index, 1)
    }
  }
}

至此我们完成了一个简易的依赖管理器,现在改写一下我们的Observer类:

class Observer{
  constructor(value){
    this.value = value
    // 判断是数组先暂时不用管待会儿补充
    if(Array.isArray(value)){
      return 
    }
    this.walk(this.value)
  }
  walk(obj){
    const keys = Object.keys(obj);
    console.log('当前监听对象的所有属性名',keys);
    for(let i = 0;i<keys.length;i++){
      defineReactive(obj,keys[i],obj[keys[i]])
    }
  }
  
}
function defineReactive(obj,key,val){
  const dep = new Dep()  //实例化一个依赖管理器,生成一个依赖管理数组dep
  Object.defineProperty(obj,key,{
    get(){
      console.log('属性被读取')
      dep.depend(); // 进行依赖收集
      return val
    },
    set(newval){
      if(newval!=val){
        console.log('属性值被设置了')
        val = newval
        dep.notify(); // 进行统治依赖收集更新
      }
    }
  })
}

let obj = new Observer({
  name:'DGT',
  age:'26',
  number:'18229282791'
})

现在大体上明白了,Observer类跟Dep和dep之间的关系。 我们刚刚说了dep中存放的是收集到的依赖,但是这些依赖到底长什么样子呢?

在vue中创建了一个Watcher类,Watcher类的实例就是当前依赖数据的那个依赖。所以dep中存放的就是当前响应式对象属性的watcher。

总结一下:当读取数据的时候,调用dep.depend()方法,将当前watcher进行收集,存放在dep中。当数据变化的时候,调用dep.notify()方法通知收集到的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的时候,会调用get方法,将当前的实例挂载在全局属性target中,然后通过let value = this.getter.call(vm, vm)获取一下被依赖的数据,获取被依赖数据的目的是触发该数据上面的getter,getter中当判断有window.target的时候会进行依赖收集。收集完后执行window.target = undefined;以免二次收集。 以上就是vue2中响应式的原理,以及依赖收集等方面的原理。