Vue源码分析数据响应

这篇文章主要分析一下Vue数据响应,并且通过这篇文章你将了解到:
1.Vue数据响应式的设计思想
2.观察者模式
3.了解Observer,Dep,Watcher的源码实现原理

首先,Vue使用的数据响应使用了数据劫持的方法:

当我们在new Vue()的时候将data属性下的对象转化成可观察的,用的是这个方法: Observer.defineProterty()。当数据改变的时候触发set(),当获取数据时触发get()

使用了观察者模式来实现数据响应:

观察者模式
观察者模式也称为发布-订阅模式。主要用于处理不同对象之间的交互通信问题。
发布者:当有事件触发的时候,通知订阅者。
订阅者:当收到发布者的通知时,做出响应的反应。
观察者模式的使用场景:当一个对象的状态发生变化时,所有的依赖对象都将得到通知。


先来看看Vue中new一个实例时

new Vue({
  data(){
    return {
      name: 'Helen',
      age: 18
    }
  },
  watch: {
    age(newVal){
      ...
      cb()
    }
  }
})

后面我将围绕这一个示例来讲解,我们先在data中将nameage转成可观察的,然后watch age这个属性,当age改变时,执行回调函数。先想一下实现这个功能的思路。
首先,我们要将在data中声明了的属性变成可观察的,然后再去监听这个属性。

class Observer{
  constructor(data){
    this.walk(data)
  }
  walk(){
    //遍历data的每一个属性
    Object.keys(data).forEach(key => {
      defineReactive(data, key, data[key])
    })
  }
}

function defineReactive(data, key, val){
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get(){
      return val
    },
    set(newVal){
      val = newVal
    }
  })
}

上面的代码将对在new Observer对象时,初始化是执行walkdata的属性循环调用defineReactivedefineReactivedata的属性转成访问器属性。当修改data属性值时,可以通过set get获取通知。现在还需要一个Dep来存放依赖

class Dep{
    this. depends = []
}

当data中的属性改变时,使用Dep来触发订阅者,当获取值时,用Dep来收集依赖。
然后还需要一个Watcher,在依赖发生改变的时候将执行Watcher。源码中有三处用到了new Wacher

1.在更新视图的时候,updateComponent
2.watch 属性的时候:$watcher(就是实例中用到的)
3.computed
其实讲到这里,已经可以看出Watcher其实是一个订阅者。只有当data的数据改变的时候触发。

怎么将Dep和Watcher向关联呢?
class Watcher{
    constructor(exp, cb){
      this.exp = exp //exp 是监听的属性
      this.cb = cb //cb是回调函数
      //多加了这一句
      this.value = data[exp]
    }
  }

this.value = data[exp]这一句的作用是什么?data已经变成访问器对象了,获取data属性的值就是触发了Observerget,额。。这就关联起来了。
现在需要添加代码:在获取属性时收集依赖,在设置属性值时发送通知执行watcher。

function defineReactive(data, key, val){
   let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get(){
      //收集依赖
      dep.depend()
      return val
    },
    set(newVal){
      //发送通知
      dep.notify()
      val = newVal
    }
  })
}
//在修改一下Dep类
class Dep{
    this. depends = []
    depend(){
      //这里需要将Watcher.target这个静态属性设置成一个Watcher的实例,然后加入到依赖的数组
      this.depends.push(Watcher.target)
  }
  notify(){
    //遍历依赖,执行回调函数
    this.depends.forEach(depend => {
      depend.cb()
    })
  }
}
//在修改一下watcher
class Watcher{
  constructor(exp, cb){
    this.exp = exp
    this.cb = cb
    //将Watcher.target设置成实例
    Watcher.target = this 
    this.value = data[exp]
  }
}

下面再看一遍源码:

function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  //每一个属性都有一个dep实例,这个Dep实例在get和set闭包作用域链中
  const dep = new Dep()
  //返回属性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  //如果该属性不能被删除或修改则不继续执行
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  /**
   * 直接将对象描述符的get和set封装成getter和setter函数
   * 当没有手动设置get和set的时候,getter和setter是undefined
   */
  const getter =  property && property.get
  const setter = property && property.set
  //进行递归绑定
  let childOb = observe(val)
  //每一个属性都要变为可观察的
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      //这里只有Dep.target存在的情况下才收集依赖,Dep.target其实是        
      //一个watcher对象,全局的Dep.target是唯一的,只有在watcher在
      //收集依赖的时候才会执行dep.depend(),在直接使用js访问属性的
      //时候直接取值
      if (Dep.target) {
        dep.depend()
        //递归绑定
        if (childOb) {
          childOb.dep.depend()
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }

      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = observe(newVal)
      //data对象的属性值改变就会触发notify来通知订阅者
      dep.notify()
    }
  })
}

通过上面代码的注释,可以知道Dep是在收集订阅者,每一个data对象的属性都有一个Dep,每一个Dep可以有多个订阅者,这一句dep.depend()其实很关键,其实是在收集订阅者Watcher
再来看一下Dep部分的代码:

 class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>; //subs存的是这个Dep的所有订阅者Watcher

  constructor () {
    this.id = uid++  //每个Vue实例中的每一个Dep实例都有不同的id
    this.subs = []  //用来收集订阅者
  }
//添加订阅者
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
//删除订阅者
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
//给watcher收集依赖
//这里是一个关键步骤,Dep.target是一个watcher实例
//先将这个Dep实例添加到Watcher的依赖中
//然后在watcher中调用dep.addSub将watcher添加到dep的订阅者中。
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
//通知订阅者,数据有更新
  notify () {
    // stablize the subscriber list first
    const subs = this.subs.slice()
    //遍历这个依赖的所有订阅者watcher
    for (let i = 0, l = subs.length; i < l; i++) {
      //update()的最终目的就是要执行Watcher的getter
      //执行这个Watcher的getter的时候就会触发这个Watcher的依赖们的get()
      //然后重新收集依赖
      subs[i].update()
    }
  }
}

注释已经写得很详细了,先看看Watcher的代码,再讲一下Dep的问题

class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: Set;
  newDepIds: Set;
  getter: Function;
  value: any;
  
  constructor (
    vm: Component,
    expOrFn: string | Function,  
    cb: Function,
    options?: Object 
  ) {
    this.vm = vm
    vm._watchers.push(this)
   
    this.cb = cb
    this.id = ++uid // uid for batching
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
   // parse expression for getter
   //expOrFn可以是字符串,也可能是函数,如果是函数就直接赋给
   //this.getter,如果是字符串表达式就回去parsePath
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    //这里在初始化时就会去执行get(),然后在get()中执行expOrFn
    //(后面讲详细讲这里)。
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  //Watcher收集依赖
  get () {
    //pushTarget函数将target设置为该watcher实例
    pushTarget(this)
    const value = this.getter.call(this.vm, this.vm) //就在这一步收集依赖
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    //清理依赖
    this.cleanupDeps()
    return value
  }

  /**
   * Add a dependency to this directive.
   */
//添加依赖
  addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      //去重
      if (!this.depIds.has(id)) {
        dep.addSub(this)
      }
    }
  }

  /**
   * Clean up for dependency collection.
   */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      //如果这个watcher不依赖与某个数据就要把这个依赖给删除
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    //更新depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    //清空newDepIds
    this.newDepIds.clear()
    tmp = this.deps
    //这里赋值了this.deps
    //更新deps
    this.deps = this.newDeps
    this.newDeps = tmp
    //清空newDeps
    this.newDeps.length = 0
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
//update的最终都会执行this.run()
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
//run方法中,会执行this.get(),就会重新收集依赖
  run () {
    if (this.active) {
      //重点是这里,会this.get()
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        //在$watch的时候用的回调函数
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}

我们来想一下什么时候expOrFn是字符串呢?答案是:Vue实例的watch中写表达式的时候。那什么时候expOrFn又是函数呢?在computed的时候,还有在mount的时候,将undateComponent作为expOrFn传入。
我们在初始化Watcher的时候会执行expOrFn,会发生什么呢?其实会去获取data的对象的属性值,那么就会执行观察者的get(),让我们再看一下get部分的代码:

get(){
  if (Dep.target) {
    dep.depend()
  }
}

这里就收集了订阅者Watcher,同时也收集了依赖

依赖到底是什么?收集依赖又是什么?

Dep是data对象中的每一个属性数据,Watcher可以是一个updateComponent模板,也可以是函数,也可以是表达式,无论是那种情况,都依赖data中的属性数据,也就是说,Watcher的依赖就是这些数据。
直接用一个例子来说:

new Vue({
  data(){
    return {
      a: 1,
      b: 2
    }
  },
  computed: {
    sum(){
      return this.a + this.b
    }
  }
})

sum()就是一个Watcher,this.athis.b就是这个Watcher的Dep。

Dep.target为什么是唯一的

我们在收集订阅这的时候,需要知道这个依赖的订阅者是谁。

用一张图说明一下

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容