玩命加载中 . . .

Vue源码阅读之数据响应式


在Vue源码中,数据的响应式原理是一个重点。此外,vue使用了观察者模式,在源码中体现为

  • Observer:发布者
  • Dep:依赖,联系发布者与订阅者的纽带
  • Watcher:订阅者

其中,Observer是发布者,用于监视数据的读写对应的index.js文件中,实现了数据劫持。而数据劫持的关键,在于使用ES5的Object.defineProperty()重写属性的getter/setter:

  1. getter中实现依赖收集
    所谓依赖,就是vue会在组件渲染的过程中把“接触”(也就是读取数据,触发其get函数)过的数据属性记录为依赖(此为依赖收集),方便后续组件的更新。
// index.js
get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // getA发生的时候,Dep.target == DepM
        dep.depend() // 这一句为关键
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return val
    },
    
// dep.js
  /**
   * Dep类中函数depend,调用Dep.target中存放的Watcher实例的addDep方法
   */
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  
// watcher.js
  /**
   * 添加依赖,即订阅Dep,同时让Dep知道该Watcher订阅着它
   * @param {Dep} dep 
   */
  addDep (dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep) // WatcherM.deps.push(DepA) // WatcherM.deps = [DepA, DepB]
      if (!this.depIds.has(id)) {
        // 收集订阅者Watcher
        dep.addSub(this)  //  DepA.subs.push(WatcherM) // DepA.subs = [WatcherM, WatcherN, WatcherX]
      }
    }
  }
  1. setter中实现分发更新
    当依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染(此为分发更新)。
set: function reactiveSetter(newVal) {
  const value = val

  if (newVal === value) {
    return
  }

  // console.log("newVal = ", newVal)
  val = newVal

  childOb = observe(newVal)
  // 通过dep依赖告知更新
  dep.notify()	// 这一句为关键

  // vm._update at core/instance/index.js
}

// dep.js
notify () {
    // 广播,当劫持到数据变更时,通知订阅者Watcher进行update
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
}

// watcher.js
  update () {
    if (this.lazy) {
      this.dirty = true
    } else {
      this.run()
    }
  }

  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value)
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }

如下为Observer/index.js中的代码,是Observer类的构造函数的一个代码片段:

this.value = value
this.dep = new Dep()
// 把当前Observer对象 绑定在value.__ob__上,作为该数据对象已经被Observer观察的标识
// this代指Observer类
def(value, '__ob__', this)

该Observer实例对象被绑定到了vue实例上,并作为属性‘ob‘,我们还可以发现,该Observer实例对象还有成员dep(依赖对象实例),因此我们再到dep.js文件中看看Dep类。

constructor () {
this.id = uid++
/**subs: Watcher[],依赖列表,用于收集订阅者Watcher */
this.subs = []
}

上面是Dep类的构造函数,由于依赖项需要通知Watcher更新组件,那么就需要记录与其绑定的Watcher,这里的成员id是该dep的唯一区分标识,而subs用于记录与其相关的订阅者Watcher。

我们自己简单地写一个数据响应式:

// 订阅者Watcher类
class Watcher {
    constructor(name) {
        this.name = name
    }

    // 值变化时调用的方法
    update(str) {
        console.log(`${this.name}发生了update.`);
        this.name = str
        console.log(this.name);
    }
}

// 发布者Dep类
class Dep {
    constructor() {
        this.subs = []
    }
    // 添加Watcher
    addSub(watcher) {
        this.subs.push(watcher)
    }
    // 遍历订阅者列表中的所有订阅者
    notify(str) {
        this.subs.forEach(item => {
            // item是订阅者Watcher
            item.update(str)
        })
    }
}

const w1 = new Watcher('张三')
const w2 = new Watcher('李四')
const w3 = new Watcher('王五')

const dep = new Dep()
dep.addSub(w1)
dep.addSub(w2)
dep.addSub(w3)

dep.notify('赵六')
  • 在上面这个简单的栗子中,我们定义了三个订阅者w1, w2, w3,一个发布者dep
  • 对于订阅者,我们订阅(监视)的是name属性
  • 对于发布者Dep,我们可以向其订阅者列表中添加订阅者w1, w2, w3,dep.addSub()
  • 一旦发布者有新内容要发布,就会通知(notify)其订阅者新内容(订阅对象)
  • 上面的通知notify也就是遍历订阅者列表,调用每一个订阅者自身的update()更新方法

上面的逻辑初步体现了vue中的数据响应式逻辑,但是还有很多细节,包括Dep何时调用notify()方法,还需要根据源码进一步分析。


文章作者: 鹿卿
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 鹿卿 !
评论
  目录