vue 2.x内部运行机制系列文章-响应式原理

634 阅读2分钟

vuejs的核心原理就是响应式。在了解vue.js的响应式之前,我们需要先认识一下vue实现响应式的基本方法Object.defineProperty()

Object.defineProperty

/**
obj: 对象
prop: 对象的属性名
descriptor: 描述符,是一个对象,包括一些属性
*/
Object.defineProperty(obj, prop, descriptor)

其中,descriptor对象中我们用到的属性包括如下,了解更多请参考文档

  • get方法:当读取obj上的prop时调用该方法
  • set方法:当修改或设置obj上的prop时调用该方法
  • configurable,属性是否可以被修改或者删除,默认 false。
  • enumerable,属性是否可枚举,默认 false。

Observer

我们看如下代码,这就是实现响应式的关键之处

// 
// defineReactive方法,省略部分处理逻辑
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 创建一个dep
  // get时用来进行依赖收集
  // set时用来通知watcher更新dom
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      if (Dep.target) {
        // 进行依赖收集,之后会有介绍
        dep.depend()
      }    
      return value
    },
    set: function reactiveSetter (newVal) {
      // 修改value的值
      val = newVal
      // 通知watcher更新
      dep.notify()
    }
  })
}

从上面代码可以看到每执行一次defineReactive方法,都会存在一个dep,即每存在一个data,都会有一个dep与之对应,这句话需要牢记,这对理解下面的依赖收集和视图更新过程至关重要。

当然,光有这个是不够的,我们从之前的 内部运行机制总览 中知道,在new Vue()后,我们会执行一个init()方法,在init()方法内部我们会执行observe()


// src/core/observer/index.js
// observe方法  为方便说明,省略了部分源码
export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 不是对象或是VNode则退出
  const ob = new Observer(value)
  return ob
}


// src/core/observer/index.js
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
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 如果是数组
      this.observeArray(value)
    } else {
      // 如果是对象
      this.walk(value)
    }
  }

  // 如果value是对象,执行walk方法,对每一个key进行defineReactive
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  // 如果是数组,则执行observeArray,对每一项进行observe
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

这样通过以上代码,然后配合defineReactive为每一项data设置getset。当我们获取vue中data的时候,会执行get,并进行依赖收集;修改data的时候,会执行set,并执行update方法来更新视图。

ok,我们现在具体来分析,它是怎么依赖收集的呢

依赖收集阶段

那么,我们现在有个问题就是,为什么要进行依赖收集?

举个例子来说明

new Vue({
    template: 
        `<div>
            <span>{{text1}}</span> 
            <span>{{text2}}</span> 
        <div>`,
    data: {
        text1: 'text1',
        text2: 'text2',
        text3: 'text3'
    }
});

比如这时候我们执行了如下代码

this.text3 = newText3

显然,当我们修改这个text3的时候,会触发set方法,并更新视图。但是,在这种情况下,由于我们在视图中并未用到text3这个变量,那么我们就不需要执行视图更新的方法。那么,这时候我们就需要在执行get的时候来确定哪些数据应该执行更新,哪些数据不应该执行更新,这个过程就叫依赖收集。

再举个例子



let p = new Vue({
    template: 
        `<div>
            <o1 :text1={text1}></o1> 
            <o2 :text1={text1}></o2> 
        <div>`,
    data:  {
        text1: 'text1'
    };
});

let o1 = new Vue({
    template:
        `<div>
            <span>{{text1}}</span> 
        <div>`,
    props: ['text1']
});

let o2 = new Vue({
    template:
        `<div>
            <span>{{text1}}</span> 
        <div>`,
     props: ['text1']
});

我们假设在p父组件内部有两个子组件 o1 , o2 都用到了父组件p的数据text1,这时候我们在p组件内部执行以下代码

this.text1 = 'newText1'

这个时候我们会通知o1 , o2执行视图更新。那么我们依赖收集的作用就是让text1知道,当自己变化的时候,需要通知依赖自己的组件更新视图。最后形成如下图的一种关系

简单来理解,data表示数据, Dep表示存放子组件(watcher对象)的盒子,watcher表示依赖data的子组件(或者说存放子组件更新方法的对象)。

ok,了解了这些,我们来看看vue具体怎么实现Dep,和Watcher的吧。

Dep

// src/core/observer/dep.js
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的时候添加观察者对象
  // 这里的Dep.target其实是Watcher实例
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  // 通知所有观察者,执行update
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
  
}

这里重点介绍几个方法:

  • depend(): 这是依赖收集的入口,这里的Dep.target指的是watcher对象,addDep()方法就是把当前的watcher添加到对应的dep对象的subs中。形成如下图的关系

  • addSub(): 向subs中添加watcher
  • removeSub(): 移除watcher
  • notify(): 通知视图更新的方法

Watcher

我们再来看watcher的实现

// 省略部分源码
export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
   // new Watcher时把this赋值给Dep.target,get时使用
   Dep.target = this; 
  }
  // 添加依赖,将当前watcher添加到dep的subs中去
  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)
      }
    }
  }

  

  // 当依赖发生改变的时候进行回调。
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  
  // 执行watcher回调
  run () {
    if (this.active) {
      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
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue) // 
        }
      }
    }
  }
}
  • addDep: 就是调用了dep的addSub方法将,watcher和dep关联。
  • update: 当依赖发生改变的时候进行回调,更新视图。

这里简要说一下queueWatcher这个方法,在vue中,当数据更新时,我们会维护一个quene队列,这个队列里存放着变化的数据所依赖的所有watcher,这个quene队列会放在nextTick里执行每一个watcher上的run方法。这个run方法会重新执行render()方法并进行diff dom

queueWatcher方法简介

总结

首先在 observer 的过程中会注册 get 方法,该方法用来进行依赖收集。在它的闭包中会有一个 Dep 对象,这个对象用来存放 Watcher 对象的实例。其实依赖收集的过程就是把 Watcher 实例存放到对应的 Dep 对象中去。get 方法可以让当前的 Watcher 对象(Dep.target)存放到它的 subs 中(addSub)方法,在数据变化时,set 会调用 Dep 对象的 notify 方法通知它内部所有的 Watcher 对象进行视图更新。