vue2的响应式

结合源码分析一下vue的响应式

之前对于响应式,只是简单,很表面上的认识,知道vue的响应式主要通过Object.defineProperty()方法来进行数据劫持以及发布者-订阅模式来实现的,但是如何进行数据劫持呢?发布订阅者模式又是什么呢?等等问题,需要明确。。。

最近花了点时间,结合源码(vue2),重新学习分析了下,在此作一下总结笔记,供个人进一步理解记忆,同时也欢迎大家阅读评论,指出问题。相关的一些笔记是在学习了黄轶老师的课程后记录的,很不错,推荐一波

先来总结一下:

vue在初始化init过程中,会对data,props中的数据进行初始化。对props初始化(initProps),主要执行了defineReactive对props中定义的数据进行响应式处理,执行proxy对数据做了代理;对data的初始化(initData),也是执行了proxy做数据代理,然后执行observe函数,对数据进行响应式化。

这个observe函数,就是创建观察者,new 一个Observer,在new这个Observer之前,会先检查是否有_ob_这个属性,是否已经对数据响应式处理过了,如果处理过了,直接取缓存,如果没有处理过,再去new Observer创建观察者。

这个Observer是一个封装好的构造函数,在这里面会处理传入的数据,如果是数组,会去遍历数组,然后对数组的每一项执行obeserve函数,如果是对象,就执行walk函数,将对象的每一个key作为参数传入defineReactive函数进行响应式处理

这个defineReactive函数就是对 Object.defineProperty的一个封装,给每一个属性加上一个getter和setter,并且在其中实例化(new Dep)一个dep对象。在getter中执行dep.depend方法做依赖收集,在setter中执行dep.notify方法做派发更新

dep.depend方法,会执行构造函数Dep中的depend方法,在depend方法中如果存在Dep.target就会执行Dep.target.addDep(this),这个Dep.target即是当前正在处理的watcher,最后会把这个watcher保存到一个subs数组中,这就是依赖收集,收集watcher

当数据发生修改时,就会触发setter,执行dep.notify方法进行派发更新,就会遍历subs,让subs中收集的每一个watcher执行update函数,进行相对应的更新。

下面详细梳理一下

GO GO GO !!!

开始之前,我们必须清楚认识一下 Object.defineProperty( ) 这个方法,这是响应式的核心,但是这个方法不兼容ie8及以下,这也正是vue无法正常在ie8及以下使用的原因

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

 // 定义一个对象o
 var o = {};
 // 给 o 提供属性
 o.age = 19;
 // 等价于
 Object.defineProperty(o, 'age', {
   configurable: true, // 可配置的,一旦为false,就不能再设置他的(value,writable,configurable)
   writable: true, // 是否可写,为false -->只读
   enumerable: true, // 可枚举,遍历
   value: 19 // 值
 });

想要响应式,就表示在赋值和读取的时候, 附带的要做一些事情,主要在于Object.defineProperty()的set和get方法

  let _gender;
  Object.defineProperty(o, 'gender', {
    // 这里需要注意一个问题,set和get,不能和writable或value同时使用
    configurable: true,
    enumerable: true, // 可枚举
    get() { // 如果使用 o.gender 来访问数据, 就会调用 get 方法 ( getter, 读取器 )
      console.log('获取属性')
      return _gender;
    },
    set(newVal) { // 如果 o.gender = 'xxx', 那么就会调用 这个 set 方法, 并设置的值会最为参数传入 set
      console.log('赋值的新值为: ', newVal);
      _gender = newVal;
    }
  });
1. 初始化

在vue初始化过程中,会对定义在data里面的数据进行初始化处理(initData),如下

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

对于initData,他主要做了两件事

一是,拿到定义的data,对data循环遍历,通过proxy函数把每一个值vm._data.xxx代理到vm.xxx上

二是,通过observe观测整个data的变化,把data里边的数据变成响应式的

另外他还做了一些判断,data如果是函数必须要return返回,data中定义的值不能和methods,props中定义的值同名,否则给出相应的警告。

下面分析一下这两件事

2. proxy函数

vue2中,封装了一个proxy,用于代理,将对某一个对象的属性访问 直接映射到 对象的某一个属性成员上

说直白点,我们定义在data中的属性,正常访问的话应该是this.data.xxx , 但是vue对此作了代理,可以通过this.xxx直接能访问到这个属性

看下源码

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

还是通过Object.defineProperty ( ),在setter和getter中做了处理

简化一下源码

    function proxy( target, prop, key ) {
      Object.defineProperty( target, key, {
        enumerable: true,
        configurable: true,
        get () {
          return target[ prop ][ key ];
        },
        set ( newVal ) {
          target[ prop ][ key ] = newVal;
        }
      } );
    }
3. observe函数

这个函数用来监测数据变化(观察者),实现响应式

还是先c来源码,为了方便理解,直接加上一些说明注释

// - 先看 对象是否含有 __ob__, 并且是 Observer 的实例 ( Vue 中响应式对象的 标记 )
// - 有, 取缓存
// - 没有, 调用 new Observer( value ), 进行响应式化
export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return // 判断是否为对象 判断是否为VNode的实例,如果不满足响应式的条件就跳出
  }
  // 观察者 创建一个ob
  let ob: Observer | void
  // 检测是否有缓存ob
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__ // 直接将缓存的ob拿到
  } else if (
    // 如果没有缓存的ob
    shouldObserve && // 当前状态是否能添加观察者
    !isServerRendering() && // 不是ssr(服务器端渲染)
    (Array.isArray(value) || isPlainObject(value)) && // 是对象或数组
    Object.isExtensible(value) && // 是否可以在它上面添加新的属性
    !value._isVue // 是否是Vue实例
  ) {
    // new 一个Observer实例 复制给ob
    // 也是把value进行响应化,并返回一个ob实例,还添加了__ob__属性
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  // 最后返回ob,也就是一个Obesrver实例 有这个实例就有__ob__,然后其对象和数组都进行了响应化
  return ob
}

observe 方法的作用就是给非 VNode 的对象类型数据添加一个观察者 Observer,

如果已经添加过,就直接返回,否则在满足一定条件下去实例化一个 Observer 对象实例(实例化一个观察者)

下面就来看一下这个 Observer 构造函数

4. Observer

同样c来源码,加上一些注释,供参考理解

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
    // 这里会new一个Dep实例
    this.dep = new Dep()
    this.vmCount = 0
    // def 用来添加__ob__属性到value对象上
    def(value, '__ob__', this) // 技巧: 逻辑上等价于 value.__ob__ = this

    // 响应式化的逻辑
    if (Array.isArray(value)) {
      // 如果是数组
      // 检测当前浏览器中有没有Array.prototype
      // 当能使用__proto__时
      // 这里完成了数组的响应式,不使用这7个方法都不会触发响应式
      // 重点: 如何进行浏览器的能力检查
      if (hasProto) {
        // 判断浏览器是否兼容 __prop__
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value) // 遍历数组的元素, 进行递归 observe
    } else {
      this.walk(value) // 遍历对象的属性, 递归 observe,walk就是给对象的所有key进行响应化
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   * 遍历所有属性,将其转换为getter/setters。这个方法只应该在value的类型为对象时调用
   */
  walk(obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   * 遍历将数组所有元素进行observe
   */
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

Observer 构造函数,首先实例化Dep对象,然后通过def函数把自身实例添加到数据对象 value 的 __ob__ 属性上(这就是为什么我们在开发中,输出data中对象类型的数据时,会有一个__ob__ 属性)

这里我们看一下这个def函数,同样,def函数也是对Object.defineProperty ( )方法的一个简单的封装

export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

Observer 构造函数中接下来,会对value 进行判断,如果是数组,会调用observeArray ( ),这个函数就是遍历数组,对数组中每一项调用observe方法;如果不是数组的话,就直接调用walk方法,遍历对象的 key 调用 defineReactive 方法

这个defineReactive ( )方法,同样是基于Object.defineProperty()的一个封装

5. defineReactive方法

它的功能就是定义一个响应式对象,给对象动态添加 getter 和 setter

c来源码,分析一下

export function defineReactive(
  obj: Object, // 对象
  key: string, // 对象的key
  val: any, // 监听的数据
  customSetter?: ?Function, //日志函数
  shallow?: boolean // 是否要添加__ob__属性
) {
  // 实例化一个Dep对象
  const dep = new Dep()

  // 获得对象的 属性描述, 就是定义 Object.defineProperty 需要传入 对象 ( { enumerable, writable, ... } )
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 检测key中是否有描述符 如果是不可配置 直接返回
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 满足预定义的getter/setters
  // 获取key中的get
  const getter = property && property.get
  const setter = property && property.set
  // 如果getter不存在或setter存在 并且参数长度为2
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 递归响应式处理 给每一层属性附加一个Obeserver实例
  // shallow不存在时代表没有__ob__属性 将val进行observe返回一个ob实例赋值给childOb
  // 如果是对象继续调用 observe(val) 函数观测该对象从而深度观测数据对象
  // walk 函数中调用 defineReactive 函数时没有传递 shallow 参数,所以该参数是 undefined
  // 默认就是深度观测
  let childOb = !shallow && observe(val)
  // 数据拦截
  // 通过Object.defineProperty对obj的key进行数据拦截
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val // 保证了如果已经定义的 get 方法可以被继承下来, 不会丢失

      if (Dep.target) {
        dep.depend() // 关联的当前属性,依赖收集

        /** 收集子属性 */
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }

      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
      }

      // #7981: for accessor properties without setter
      if (getter && !setter) return

      if (setter) {
        setter.call(obj, newVal) // 保证了如果已经定义的 set 方法可以被继承下来, 不会丢失
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal) // 对新值进行响应式化
      dep.notify() // 派发更新
    },
  })
}

defineReactive 函数最开始初始化 Dep 对象的实例,接着拿到 obj 的属性描述符,然后对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样我们访问或修改 obj 中一个嵌套较深的属性,也能触发 getter 和 setter。最后利用 Object.defineProperty 去给 obj 的属性 key 添加 getter 和 setter。

在getter中,做了依赖收集的相关处理

在setter中,实现了派发更新的逻辑,

这一块相对复杂,我们拆分来看

6. getter 依赖收集

对于依赖收集,在defineReactive 函数中需要关注两个地方,一是 在进入getter之前,通过 const dep = new Dep( ) 实例化了一个dep对象 ,二是在get函数中,通过dep.depend ( )进行依赖收集

因此,我们需要了解一下这个Dep,它是整个依赖收集的核心

还是,源码拿来

export default class Dep {
  // 一个静态属性 target,这是一个全局唯一 Watcher
  // 同一时间只能有一个全局的 Watcher 被计算
  static target: ?Watcher
  id: number
  subs: Array<Watcher>

  constructor() {
    this.id = uid++
    // 存放Watcher对象的数组
    this.subs = []
  }

  addSub(sub: Watcher) {
    // 给subs数组添加一个Watcher对象
    this.subs.push(sub)
  }

  removeSub(sub: Watcher) {
    // 删除watcher对象
    remove(this.subs, sub)
  }
  // 添加watcher
  depend() {
    // target就是Watcher dep就是dep对象,dep中是否有watcher对象
    if (Dep.target) {
      // 用当前的watcher调用addDep
      Dep.target.addDep(this)
    }
  }

  /**
   * 每一个属性 都会包含一个 dep 实例
   * 这个 dep 实例会记录下 参与计算或渲染的 watcher
   */
  // 通知所有watcher对象更新视图,也就是执行update
  notify() {
    // stabilize the subscriber list first
    // 浅拷贝一份subs数组,也就是Watchers列表
    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
      // 如果不运行async,则不会在调度程序中对sub进行排序
      // 我们现在需要对它们进行分类以确保它们发射正确秩序
      subs.sort((a, b) => a.id - b.id)
    }
    // 所有subs中的wathcers执行update函数,也就是更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
// 正在评估的当前目标观察程序。
// 这是全局的的,因为只有一个观察者,可以在任何时候都被评估。
Dep.target = null
const targetStack = []
// 压栈
export function pushTarget(target: ?Watcher) {
  // 压栈
  targetStack.push(target)
  // target就是watcher dep是Dep对象
  Dep.target = target
}

export function popTarget() {
  // 出栈
  targetStack.pop()
  // 成为最后一个元素
  Dep.target = targetStack[targetStack.length - 1]
}

Dep 是一个 Class,它定义了一些属性和方法,这里需要特别注意的是它有一个静态属性 target,这是一个全局唯一 Watcher,这是一个非常巧妙的设计,因为在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组。

Dep 实际上就是对 Watcher 的一种管理,Dep 脱离 Watcher 单独存在是没有意义的,为了完整搞清楚依赖收集过程,我们有必要看一下 Watcher 的一些相关实现

let uid = 0

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  computed: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
	// 在 Vue 中使用了 二次提交的概念
  // 每次在数据 渲染 或 计算的时候 就会访问响应式的数据, 就会进行依赖收集
  // 就将关联的 Watcher 与 dep 相关联,
  // 在数据发生变化的时候, 根据 dep 找到关联的 watcher, 依次调用 update
  // 执行完成后会清空 watcher
  dep: Dep;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function; 
  getter: Function;
  value: any;

  constructor (
    vm: Component, // dom
    expOrFn: string | Function, //获取值的函数,或是更新视图的函数
    cb: Function, // 回调函数
    options?: ?Object, // 参数
    isRenderWatcher?: boolean // 是否是渲染过的watcher
  ) {
    this.vm = vm
    if (isRenderWatcher) { // 如果是已经渲染过的watcher
      vm._watcher = this  // 把当前Watcher对象给_wathcer 
    }
    vm._watchers.push(this)  // 把观察者添加到队列里面 当前Watcher添加到vue实例上
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.computed = !!options.computed
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.computed = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.computed // for computed watchers
    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
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get()
    }
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(this) // 实际上就是把 Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复用)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm) // this.getter 对应就是 updateComponent 函数,这实际上就是在执行 			   vm._update(vm._render(), hydrating)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "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]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
  // ...
}

Watcher 是一个 Class,在它的构造函数中,定义了一些和 Dep 相关的属性:

 this.deps = [] // 观察者队列
 this.newDeps = [] // 新的观察者队列
 this.depIds = new Set() // depId 不可重复
 this.newDepIds = new Set() // 新depId 不可重复

其中,this.depsthis.newDeps 表示 Watcher 实例持有的 Dep 实例的数组;而 this.depIdsthis.newDepIds 分别代表 this.depsthis.newDepsid ,Set(这个 Set 是 ES6 的数据结构)

除此之外,Watcher 还定义了一些原型的方法,和依赖收集相关的有 getaddDepcleanupDeps 方法

7. 依赖收集的过程分析

对数据对象的访问会触发他们的 getter 方法,那么这些对象什么时候被访问呢?

这里举种情况,当我们去实例化一个渲染 watcher 的时候,

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

首先进入 watcher 的构造函数逻辑,然后会执行它的 this.get() 方法,进入 get 函数,首先会执行pushTarget方法

export function pushTarget(target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

实际上就是把 Dep.target 赋值为当前的渲染 watcher 并压栈(为了恢复用)。接着又执行了

value = this.getter.call(vm, vm)

this.getter 对应就是 updateComponent 函数,这实际上就是在执行

vm._update(vm._render(), hydrating)

它会先执行 vm._render() 方法,这个方法会生成 渲染 VNode,并且在这个过程中会对 vm 上的数据访问,这个时候就触发了数据对象的 getter。

那么每个对象值的 getter 都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行 Dep.target.addDep(this)

刚才我们提到这个时候 Dep.target 已经被赋值为渲染 watcher,那么就执行到 addDep 方法:

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

这时候会做一些逻辑判断(保证同一数据不会被添加多次)后执行 dep.addSub(this),那么就会执行 this.subs.push(sub),也就是说把当前的 watcher 订阅到这个数据持有的 depsubs 中,这个目的是为后续数据变化时候能通知到哪些 subs 做准备

所以在 vm._render() 过程中,会触发所有数据的 getter,这样实际上已经完成了一个依赖收集的过程。

8. setter派发更新

setter 的逻辑有 2 个关键的点,一个是 childOb = !shallow && observe(newVal),如果 shallow 为 false 的情况,会对新设置的值变成一个响应式对象;另一个是 dep.notify(),通知所有的订阅者

class Dep {
  // ...
  notify () {
  // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

这里的逻辑非常简单,遍历所有的 subs,也就是 Watcher 的实例数组,然后调用每一个 watcherupdate 方法

 update () {
    /* istanbul ignore else */
    if (this.computed) {
      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

这里对于 Watcher 的不同状态,会执行不同的逻辑,在一般组件数据更新的场景,会走到最后一个 queueWatcher(this) 的逻辑

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

这里引入了一个队列的概念,这也是 Vue 在做派发更新的时候的一个优化的点,它并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在 nextTick 后执行 flushSchedulerQueue

这里有几个细节要注意一下,首先用 has 对象保证同一个 Watcher 只添加一次;最后通过 waiting 保证对 nextTick(flushSchedulerQueue) 的调用逻辑只有一次,并在下一个 tick,也就是异步的去执行 flushSchedulerQueue

let flushing = false
let index = 0
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
   // 刷新前对队列排序
  // 这样可以确保:
  // 1. 组件从父级更新到子级。(因为父对象总是在子对象之前创建)
  // 2. 组件的用户观察程序在其渲染观察程序之前运行(因为用户观察程序是在渲染观察程序之前创建的)
  // 3. 如果某个组件在父组件的观察程序运行期间被破坏,则可以跳过它的观察程序。

  // 刷新前对队列进行排序 根据id排序
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  // 当我们运行现有的观察者时,不要缓存长度,因为可能会推送更多观察者

  // 遍历观察者数组
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

这里有几个重要的逻辑要梳理一下

  • 队列排序

queue.sort((a, b) => a.id - b.id) 对队列做了从小到大的排序,这么做主要有以下要确保以下几点:

1.组件的更新由父到子;因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。

2.用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher 是在渲染 watcher 之前创建的。

3.如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行都可以被跳过,所以父组件的 watcher 应该先执行。

  • 队列遍历

在对 queue 排序后,接着就是要对它做遍历,拿到对应的 watcher,执行 watcher.run()。这里需要注意一个细节,在遍历的时候每次都会对 queue.length 求值,因为在 watcher.run() 的时候,很可能用户会再次添加新的 watcher,这样会再次执行到 queueWatcher,如下:

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // ...
  }
}

可以看到,这时候 flushing 为 true,就会执行到 else 的逻辑,然后就会从后往前找,找到第一个待插入 watcher 的 id 比当前队列中 watcher 的 id 大的位置。把 watcher 按照 id的插入到队列中,因此 queue 的长度发生了变化。

  • 状态恢复

这个过程就是执行 resetSchedulerState 函数

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let circular: { [key: number]: number } = {}
let waiting = false
let flushing = false
let index = 0
/**
 * Reset the scheduler's state.
 */
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}

把这些控制流程状态的一些变量恢复到初始值,把 watcher 队列清空。

接下来我们继续分析 watcher.run() 的逻辑

class Watcher {
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run () {
    if (this.active) {
      this.getAndInvoke(this.cb)
    }
  }

  getAndInvoke (cb: Function) {
    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
      this.dirty = false
      if (this.user) {
        try {
          cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        cb.call(this.vm, value, oldValue)
      }
    }
  }
}

run 函数实际上就是执行 this.getAndInvoke 方法,并传入 watcher 的回调函数。getAndInvoke 函数逻辑也很简单,先通过 this.get() 得到它当前的值,然后做判断,如果满足新旧值不等、新值是对象类型、deep 模式任何一个条件,则执行 watcher 的回调,注意回调函数执行的时候会把第一个和第二个参数传入新值 value 和旧值 oldValue,这就是当我们添加自定义 watcher 的时候能在回调函数的参数中拿到新旧值的原因。

那么对于渲染 watcher 而言,它在执行 this.get() 方法求值的时候,会执行 getter 方法:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

所以这就是当我们去修改组件相关的响应式数据的时候,会触发组件重新渲染的原因,接着就会重新执行 patch 的过程

综上,当数据发生变化的时候,触发 setter 逻辑,把在依赖过程中订阅的的所有观察者,也就是 watcher,都触发它们的 update 过程,这个过程又利用了队列做了进一步优化,在 nextTick 后执行所有 watcherrun,最后执行它们的回调函数。

你可能感兴趣的:(源码笔记,vue.js)