🔥Vue 响应式原理(原理及源码分析)

·  阅读 18
🔥Vue 响应式原理(原理及源码分析)
  • 篇幅较长,不过都是精华。
  • 不过静下心来通篇吃透,保你收获不小。
  • 有任何问题欢迎评论区留言。
  • 来吧,话不多说,就是干!

响应式原理回顾

  • 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter
  • 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
  • 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。 依赖收集
  • 之后当依赖项的 setter 触发时,通知 watcher,从而使它关联的组件重新渲染。 派发更新

以上原理回顾来自 Vue 官方文档,更多信息可以进官方文档细看。

简介

Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。接下来我们通过对部分 Vue 源码(本文主要针对 v2.6.11 的 Vue 源码进行分析)的简单分析和学习来深入了解 Vue 中的数据响应式是怎样实现的。我们将这一部分的代码分析大致分为三部分:怎么让数据变成响应式依赖收集派发更新

一、怎么让数据变成响应式?

整体流程图

调试代码调用栈

我自己 debug 了一下,上图流程的调用栈如下图所示:

具体一点的函数调用流程

1、调用 new Vue() 执行构造函数

src/core/instance/index 中定义了 Vue 的构造函数,传入的参数是 options。进行 vue 实例化时主要调用了一个 this._init() 的函数。

2、构造函数中执行 initMixin(Vue) 中定义的 _init() 方法

function Vue (options) {
  // ...
  this._init(options)
}
initMixin(Vue)     
stateMixin(Vue)     
// ...
export default Vue
复制代码

3、_init() 方法 中 调用 initState(vm)

在 _init() 中对当前传入的 options 进行了一些处理,主要是判断当前实例化的是否为组件,使用 mergeOptions 方法对 options 进行加工,此处不做赘述。然后又调用了一系列方法进行了生命周期、事件、渲染器的初始化等,这里我们主要关注 initState 这个方法:

Vue.prototype._init = function (options?: Object) {
  
  // ...

  // 初始化 vm 的 _props/methods/_data/computed/watch
  initState(vm)

  // ...
}
复制代码

4、initState(vm) 中调用 initData(vm)

initState(vm) 中对 propsmethodsdatacomputedwatch 进行了初始化,这些都是 Vue 实例化方法中传入参数 options 对象的一些属性,这些属性都需要被响应式化。而针对于 data 的初始化分了两种情况,一种是 options 中没有 data 属性的,该方法会给 data 赋值一个空对象并进行 observe,如果有 data 属性,则调用 initData 方法进行初始化。

export function initState(vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe((vm._data = {}), true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
复制代码

5、initData(vm) 方法中 最后 调用了 observe(data, true /* asRootData */)

initData 方法对 options 中的 data 进行处理,主要是有两个目的

  • 1、将 data 代理到 Vue 实例上,同时检查 data 中是否使用了 Vue 中的保留字、是否与 propsmethods 中的属性同名。
  • 2、使用 observe 方法将 data 中的属性变成响应式的。

接下来会对 data 中的每一个数据进行遍历,遍历过程将会使用 hasOwn(methods, key)hasOwn(props, key)方法对该数据是否占用保留字、是否与 propsmethods 中的属性重名进行判断并在浏览器等控制台给出 warn 提示信息。最后再用 !isReserved(key) 判断当前属性名是否是以$和_开头的(即判断是否是 Vue 中的保留字),最后才调用 proxy 方法将其代理到 Vue 实例上。

此处需要注意的是:如果 data 中的属性与 propsmethods 中的属性重名,那么在 Vue 实例上调用这个属性时的优先级顺序是 props > methods > data

最后对 data 中的每一个属性调用 observe 方法,该方法赋予 data 中的属性可被监测的特性。

function initData(vm: Component) {
  let data = vm.$options.data
  // 初始化 _data,组件中 data 是函数,调用函数返回结果
  // 否则直接返回 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 */)
}
复制代码

6、observe(value) 中最后实例化了 Observer 类,并且把需要响应式的数据传入

export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
复制代码

7、Observer 类的构造函数中调用了它的实例方法 this.walk(value)

可以看到该构造函数有以下几个目的

  • 针对当前的数据对象新建了一个订阅器
  • 为每个数据的 value 都添加一个__ob__属性,该属性不可枚举并指向自身
  • 针对数组类型的数据进行单独处理
  • 调用 this.walk(value)
constructor(value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0 // 初始化实例的 vmCount 为0
  def(value, '__ob__', this) // 将实例挂载到观察对象的 __ob__ 属性
  // 数组的响应式处理
  if (Array.isArray(value)) {
    if (hasProto) {//判断当前浏览器是否支持 __proto__ 这个属性
      protoAugment(value, arrayMethods)
    } else {
      copyAugment(value, arrayMethods, arrayKeys)
    }
    // 为数组中的每一个对象创建一个 observer 实例
    this.observeArray(value)
  } else {
    // 遍历对象中的每一个属性,转换成 setter/getter
    this.walk(value)
  }
}
复制代码

8、this.walk(value) 中遍历了 value 中的所有属性,调用 defineReactive(obj, keys[i])

/**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
walk(obj: Object) {
  // 获取观察对象的每一个属性
  const keys = Object.keys(obj)
  // 遍历每一个属性,设置为响应式数据
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}
复制代码

9、defineReactive(obj, keys[i])

defineReactive() 方法中最终使用 Object.defineProperty 把这些 property 全部转为 getter/setter。是真正为数据添加 getset 属性方法的方法,它将 data 中的数据定义一个响应式对象,并给该对象设置 getset 属性方法,其中 get 方法是对依赖进行收集, set 方法是当数据改变时通知 Watcher 派发更新。

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean // 浅的,如果为 true 就只监听第一层数据,false 进行深度监听
) {
  // 创建依赖对象实例 作用是为当前这个属性收集依赖 也就是收集观察当前这个属性的所有 watcher
  const dep = new Dep()
  // 获取 obj 的属性描述符对象
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    //当前这个属性是不可配置的
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 判断是否递归观察子对象,并将子对象属性都转换成 getter/setter,返回子观察对象
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 如果预定义的 getter 存在则 value 等于 getter 调用的返回值
      // 否则直接赋予属性值
      const value = getter ? getter.call(obj) : val
      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 */
      // 如果新值等于旧值或者新值旧值为NaN则不执行
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 判断是否递归观察子对象,观察子对象并返回子对象的 observer 对象
      childOb = !shallow && observe(newVal)
      // 派发更新(给订阅者发布更新通知)
      dep.notify()
    }
  })
}
复制代码

到此我们就把创建 Vue 实例时传入的 options 中的 data 通过 Object.defineProperty() 转换为一个响应式对象了

二、依赖收集

依赖收集的简要原理就是:当我们的视图被渲染时,会用到我们定义在 data 中的数据,这样就会触发这些数据的 get 属性方法,Vue 通过 get 方法进行依赖收集。get 方法中的代码如下:

get: function reactiveGetter() {
  // 如果预定义的 getter 存在则 value 等于 getter 调用的返回值
  // 否则直接赋予属性值
  const value = getter ? getter.call(obj) : val
  if (Dep.target) {
    dep.depend()
    if (childOb) {
      childOb.dep.depend()
      if (Array.isArray(value)) {
        dependArray(value)
      }
    }
  }
  return value
},
复制代码

可以看到 get 属性方法中调用了一个 dep.depend 方法,该方法正是依赖收集的开始。

Dep 类之订阅器原理

数据对象中的 get 方法主要使用 depend 方法进行依赖收集,而 dependDep 类中的属性方法,我们来看下 Dep 类的代码是怎样实现的:

export default class Dep {
  static target: ?Watcher
  id: number
  subs: Array<Watcher>

  constructor() {
    this.id = uid++
    this.subs = []
  }
  // 添加新的订阅者 watcher 对象
  addSub(sub: Watcher) {
    this.subs.push(sub)
  }
  // 移除订阅者
  removeSub(sub: Watcher) {
    remove(this.subs, sub)
  }
  // 将观察对象和 watcher 建立依赖
  depend() {
    if (Dep.target) {
      // 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
      Dep.target.addDep(this)
    }
  }
  // 发布通知
  notify() {
    // stabilize the subscriber list first
    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
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
// Dep.target 用来存放目前正在使用的 watcher
// 全局唯一,并且一次也只能有一个watcher被使用
// 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 = []
// 入栈并将当前 watcher 赋值给 Dep.target
// 父子组件嵌套的时候先把父组件对应的 watcher 入栈,
// 再去处理子组件的 watcher,子组件的处理完毕后,再把父组件对应的 watcher 出栈,继续操作
export function pushTarget(target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
复制代码

Dep 类中定义了三个属性:targetidsubs,分别表示全局唯一的静态数据依赖的监听器 Watcher、该属性的 id 以及订阅这个属性数据的订阅者列表 subs。其中 subs 其实就是存放了所有订阅了该数据的订阅者们。另外还提供了将订阅者添加到订阅者列表的 addSub 方法、从订阅者列表删除订阅者的 removeSub 方法。

我们能看到在执行 dep.depend() 方法之前,其实是需要先判断 Dep.target 这个全局唯一的静态数据依赖的监听器 Watcher 是否存在的,只有当它有值的时候才会去执行 dep.depend() 收集依赖。所以我们得先搞清楚这个 Dep.target 是在哪里被赋值的

Dep.target 是在哪里被赋值的

我们在 Dep 类的代码中能看到这个 Dep.target 是一个 Watcher 的实例,所以我通过查看源码找到了创建 watcher 对象的地方,发现是在 src/core/instance/lifecycle.js中的 mountComponent函数中执行了 new Watcher()

mountComponent()

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)// 是否为渲染watcher的标识
复制代码

watcher 类的构造函数

通过 Watcher 类的构造函数传参可以看到 mountComponent 中创建的 Watcher 是一个渲染 Watcher,将当前的 getter 设置为了 updateComponent 方法(这也就是重新渲染视图的方法),最后调用了 get 属性方法。我们接下来看看 get 方法中做了什么

this.value = this.lazy //lazy 的含义是-是否延迟加载
  ? undefined
  : this.get()
复制代码

watcher 类的静态方法 get()

/**
   * Evaluate the getter, and re-collect dependencies.
   */
get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } 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
}
复制代码
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
复制代码

get 方法中首先调用了在 Dep 类文件中定义的全局方法 pushTarget,将 Dep.target 置为当前正在执行的渲染 Watcher,并将这个 watcher 压到了 targetStack。接下来就会调用 wathcergetter 方法

由于此时的 getter 就是 updateComponent 方法,执行 updateComponent 方法,也就是 vm._update(vm._render(), hydrating) 进行渲染页面了,渲染过程中就会 touchdata 中相关的依赖属性,这样就会触发各个属性中的 get 方法了,然后就会走到方法里面的 dep.depend()(因为此时 Dep.target已经是被赋值为当前正在执行的渲染 Watcher

// 将观察对象和 watcher 建立依赖
depend() {
  if (Dep.target) {
    // 如果 target 存在,把 dep 对象添加到 watcher 的依赖中
    Dep.target.addDep(this)
  }
}
复制代码

depend() 方法很简单,只是执行了一下 Dep.target.addDep(this),这里的 Dep.target 是一个 Watcher 实例,里面的参数 this 也就是当前的 Dep 实例。我们看下在 Watcher 中的 addDep() 方法执行了啥操作

/**
   * 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)
    }
  }
}
复制代码
  • addDep 方法中做了一些优化,对同一个属性只收集一次依赖。
  • dep.addSub(this) 就是调用 Dep 的属性方法 addSub 将当前 Watcher 添加到依赖数组中,用于之后的派发更新。
// 添加新的订阅者 watcher 对象
addSub(sub: Watcher) {
  this.subs.push(sub)
}
复制代码

在本次渲染完之后会将当前的渲染 WatchertargetStack 推出,进行下一个 watcher 的任务。最后会进行依赖的清空,每次数据的变化都会引起视图重新渲染,每一次渲染又会进行依赖收集,又会触发 data 中每个属性的 get 方法。有下面这种情况

<template>
  <div>
    <span v-if="isShow"> {{ jing }} </span>
    <span v-else> {{ hao }} </span>
  </div>
</template>
复制代码

第一次当 isShow 为真时,进行渲染,当我们把 isShow 的值改为 false 时,这时候属性 jing 根本不会被使用,所以如果不进行依赖清空,会导致不必要的依赖通知。依赖清空主要是对 newDeps 进行更新,通过对比本次收集的依赖和之前的依赖进行对比,把不需要的依赖清除。

/**
   * 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
}
复制代码

三、派发更新

debug 实例

页面代码如下:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>observe</title>
  </head>
  <body>
    <div id="app">
      <span> {{ arr }}</span>
      <child-one :arr="arr"></child-one>
      <child-two :arr="arr"></child-two>
      <child-three :arr="arr"></child-three>
    </div>

    <script src="../../dist/vue.js"></script>
    <script>
      Vue.component('child-one', {
        props: ['arr'],
        template: '<p>{{ arr }}</p>'
      })
      Vue.component('child-two', {
        props: ['arr'],
        template: '<div>{{ arr }}</div>'
      })
      Vue.component('child-three', {
        props: ['arr'],
        template: '<p>{{ arr }}</p>'
      })
      const vm = new Vue({
        el: '#app',
        data: {
          arr: [2, 3, 5]
        }
      })
    </script>
  </body>
</html>
复制代码

调试效果如下:

可以很清楚的看到 arr 这个数据的 dep.subs 数组中收集了 4Watcher 实例。

dep.notify()

// 发布通知
notify() {
  // stabilize the subscriber list first
  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
    subs.sort((a, b) => a.id - b.id)
  }
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}
复制代码

而当数据变化时,会触发数据的 setter,在 setter 中调用了 dep.notify() 方法,在dep.notify()方法中,遍历所有依赖(即watcher实例),执行依赖的 update() 方法,也就是 Watcher 类中的update()实例方法。

update()

/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}
复制代码

update 方法是在 Watcher 类文件中,里面使用了队列的方式来管理订阅者的更新派发,其中主要调用了 queueWatcher 这个方法来实现该逻辑:

queueWatcher()

/**
 * 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

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
复制代码

首先使用一个名为 hasMap 来保证每一个 watcher 仅推入队列一次。flushing 这个标识符表示目前这个队列是否正在进行更新派发,如果是,那么将这个 id 对应的订阅者进行替换,如果已经路过这个 id,那么就立刻将这个 id 对应的 watcher 放在下一个排入队列。接下来根据 waiting 这个标识符来表示当前是否正在对这个队列进行更新派发,如果没有的话,就可以调用 nextTick(flushSchedulerQueue) 进行真正的更新派发了。

flushSchedulerQueue()

/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  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.
  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')
  }
}
复制代码

这个方法首先对队列中的 watcher 按照其 id 进行了排序,排序的主要目的注释中写的很清楚了。

  • 1、Vue 的组件更新是由父到子,因为父组件的创建过程是先于子的,所以 watcher 的创建也是先父后子,执行顺序也应该保持先父后子。
  • 2、用户的自定义 watcher 要优先于渲染 watcher 执行;因为用户自定义 watcher (initState) 是在渲染 watcher (mountComponent) 之前创建的。 * *initState 是比 mountComponent 先执行的)
  • 3、如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 都应该被跳过,所以父组件的 watcher 应该先执行。

排序之后就遍历这个队列,执行 watcher.run() 方法。

watcher.run()

/**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
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)
      }
    }
  }
}
复制代码

watcher.run() 方法中

  • 新值与老值不同时会触发回调函数
  • 但是如果值是对象或者 deeptrue 时都会触发回调函数

当回调函数重新渲染视图的时候,又会进行下一轮的依赖收集,之后又派发更新......无限套娃

四、🌈🌈🌈💨💨💨

  • 好了,我是金同学,这次的分享就到这里了。
  • 要是有下一篇,应该就是关于 Vue 虚拟 Dom源码的相关分享了。
  • 当然了,希望我们会再次重逢。
  • 努力生活是头等大事,祝朋友们岁月静好。
  • 希望疫情下的次生灾难少一点、悲剧发生的少一点。🙏🏻🙏🏻🙏🏻

image.png

分类:
前端
收藏成功!
已添加到「」, 点击更改