vue2的响应式

2023-11-16

结合源码分析一下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,最后执行它们的回调函数。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

vue2的响应式 的相关文章

随机推荐

  • a和ajax跳转页面,ajax 页面跳转

    ajax 页面跳转 内容精选 换一换 面包屑组件 是项目中常用的一种组件 结构大致是 首页 菜单1 菜单2 菜单3 接入配置完成后 伙伴可以在能力开放页面配置伙伴平台回跳地址 以便于客户在完成订单支付后能返回到伙伴销售平台或者客户支付订单需
  • 适合下班后的副业,4个比较实际的副业兼职

    每一个上班族 都有一个发财的梦想 希望可以通过自己的努力 让领导看到自身价值 得到赏识 快速升职加薪 赚得盆满钵满 这样是不错 而且不少上班族单纯依靠工作就实现了这样的梦想 但是对于绝大部分普通人来说 还只存在于 想 如果现有的工作暂时没有
  • pdf注释上锁_如何在iPad上突出显示和注释PDF

    pdf注释上锁 Khamosh Pathak Khamosh Pathak The iPad is a great way to read PDFs but what if you want to highlight parts of it
  • STM32采用普通的IO口来测量PWM的频率

    STM32测量外部输入信号的频率的方法有很多 采用内部定时器输入捕获功能 采用普通的IO口设置外部中断 定时器的当时测量PWM信号的频率 这两种方式比较推荐使用第一种 比较使用内部的资源可以节省CPU资源的利用 当然当内部资源不够使用的时候
  • Unity LensFlares(镜头炫光)踩坑以及解决总结

    镜头光晕 Lens Flares 模拟相机镜头内的折射光线的效果 主要作用就是让太阳光 其他光源更加真实 Build in Build in管线中 可以直接添加Lens Flare组件即可获得效果 URP 2019 在Unity2019版本
  • 【mcuclub】CO2及TVOC检测-SGP30

    一 实物图 二 原理图 编号 名称 功能 1 VCC 电源正 2 GND 电源地 3 SDA 串行地址和数据输入 输出 4 SCL 串行时钟输入 三 简介 SGP30是一款单一芯片上具有多个传感元件的金属氧化物室内气体传感器 内部集成4个气
  • 电赛分几种_参加电赛需要具备哪些知识呢?

    本文转载自 微信公众号 47竞赛 ID gh 1814a7d91c55 经微信公众号授权转载 如需转载与原文作者联系 电赛需要准备哪些知识呢 先分析一下电赛的题目 你会发现 题目主要分为控制类 仪器仪表类 信号源类 电源类 放大器类 高频通
  • QT 布局,控件自适应大小 自动缩放 自动布局

    目录 前言 1 先来说简单的布局控件自适应 说明我们实现了自动布局 3 通过代码设置控件自动缩放重写resizeEvent 4 源码 https upload csdn net creation uploadResources 866208
  • 第1章 用物理模型进行高效的水模拟

    一 用物理模型进行高效的水模拟 一句话概括 基本网格的几何波动 动态法线贴图 1 1现状 1 快速傅里叶FFT在大中尺寸栅格取得逼真效果 并能适用于顶点shader和像素shader 2 能基于体素 Voxel 对简化的Navier Sto
  • 在培训班里学IT技术是否有用?和大家分享相关IT培训班里五大常见宣传手法、相关优势与实际效果

    目录 Introduction 引言 IT培训班常见宣传手法 培训班的优势 如何评判IT培训班的效果与质量 除IT培训班之外的学习渠道 总结 其它资料下载 Introduction 引言 随着信息技术的飞速发展 学习IT技术成为许多人追求职
  • win10搜索大文件

    直接在资源管理器的搜索框中敲 size gt 1G win 10计算机查找大文件 教你如何在Win10系统中查找大文件
  • WIN+R 实用大总结

    文章目录 cmd 与管理员cmd 打开网络共享中心 ncpa cpl 打开画画 mspaint 打开系统配置 msconfig 打开设备管理器 devmgmt msc 打开远程桌面连接 mstsc 任务管理器 taskmgr 系统属性 sy
  • Win10+vs2017 webrtc下载和编译

    现在使用webrtc的小伙伴越来越多 我也来凑凑热闹 第一步自然是下载源码 其实官网上面写的还是蛮详细的 只是环境搭建稍稍复杂了点 再加上国内不能访问google 所以简单的事情就变得复杂起来 我就按照官网上面的流程给大家简单介绍下 具体细
  • Flink Web UI 介绍

    一 提交flink任务到yarn flink run m yarn cluster yn 1 p 2 yjm 1024 ytm 1024 ynm FlinkOnYarnSession MemberLogInfoProducer d c co
  • 金蝶生成凭证模板_金蝶精斗云产品的优势

    1 金蝶精斗云产品免维护安装 产品免安装 免维护 免年结 自动升级 账号式授权加密 自动备份 会计归档 不需要固定的服务器 e64845f06572190e4634c2be37ab9ee9 png 2 金蝶精斗云系统凭证便捷生成 图片 PD
  • 反接保护电路:

    反接保护电路 通常我们的电子产品 为防止用户将正负极接反 会对接口做防反接保护 比如接口做成梯形或者开个缺口 反了不容易插进 但你真的永远不知道你的产品用户是萌妹纸还是暴力怪蜀黍 最终 这些防接反设计还是被突破了 被暴力插了进去 插进去了
  • uboot联网以及uboot重启问题

    一 配置uboot联网 虚拟机联网 配置uboot联网 1 配置uboot环境变量 setenv ipaddr 192 168 10 50 开发板ip地址 setenv ethaddr 00 04 9f 04 d2 35 mcu期间地址 多
  • ESP8266 CUT HERE FOR EXCEPTION DECODER解决办法

    串口log信息 CUT HERE FOR EXCEPTION DECODER Soft WDT reset gt gt gt stack gt gt gt ctx cont sp 3ffffd40 end 3fffffc0 offset 0
  • java使用多线程同时插入数据库数据例子

    今天自己在家准备面试内容 写了个java使用多线程往mysql数据库插入数据的例子 总结 不管数据库引擎是MYISAM还是InnoDB 情况都是 没有线程池的情况下就不说了 一直创建数据库连接一会就出错了 基本对于上万条的数据插入不可用 使
  • vue2的响应式

    结合源码分析一下vue的响应式 之前对于响应式 只是简单 很表面上的认识 知道vue的响应式主要通过Object defineProperty 方法来进行数据劫持以及发布者 订阅模式来实现的 但是如何进行数据劫持呢 发布订阅者模式又是什么呢