Vue 源码 – 响应式原理

Vue 在创建实例对象时,会将 datapropscomputedwatch 设置为 响应式对象

这些过程发生在 initState(vm) 内,它的定义在 src/core/instance/state.js

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

initState 函数会依次处理 propsmethodsdatacomputedwatch

先说一下 data的响应原理,其他的会分别写一篇文章

data 的响应原理

initState函数执行的过程中,如果 opts.data存在,调用initData去初始化 data,将data变为响应式对象

initData

initData函数定义在src/core/instance/state.js

function initData (vm: Component) {
  let data = vm.$options.data
  // data 是一个函数的话,先去执行这个函数拿到 返回值并赋值给 vm._data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  // 保证 methods、props、data 中没有同名属性
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      // 如果 methods 中存在 key 报错
      if (methods && hasOwn(methods, key)) {
        warn(`Method "${key}" has already been defined as a data property.`, vm)
      }
    }
    // 如果 props 中存在 key 报错
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(/* ... */)
    } else if (!isReserved(key)) {
      // 使开发者可以直接通过 this.xxx 的方式访问 data 中的属性
      proxy(vm, `_data`, key)
    }
  }
  // 给 data 添加响应
  observe(data, true /* asRootData */)
}
复制代码

initData总共会做 3 件事:

  • 首先会验证 methodspropsdata 中没有同名属性;
  • 通过 proxydata 中的 key代理到 vm 上,从而可以通过this.xxx 的方式访问 data 中的属性;
  • 通过observe函数创建观察者实例并给 data所有属性添加响应

proxy

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
// 设置代理,将 key 代理到 target 上
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)
}
复制代码

observe

定义在/src/core/observer/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 非对象和 VNode 实例不做响应式处理
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 如果 value 对象上存在 __ob__ 属性,说明已经添加响应了,直接返回 __ob__ 属性
  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)
  }
  // 如果当前添加响应的对象是 data,asRootData 是 true
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
复制代码

observe 函数接收 2 个参数

  • value 要添加响应的对象
  • asRootData 如果添加响应的对象是 dataasRootDatatrue

如果shouldObservetrue ,并且value为非 VNode 对象 ,则通过Observer创建观察者实例,如果对象已经被观察过,返回已有的观察者实例

Observer

定义在/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
    // 创建 Dep 实例
    this.dep = new Dep()
    this.vmCount = 0
    // 将 自身实例 挂载到 value.__ob__ 上
    def(value, '__ob__', this)
    // 数组的处理逻辑(后面说)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      // 如果 value 是对象
      this.walk(value)
    }
  }
  // 遍历所有属性,并为属性添加响应
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  /**
   * 遍历数组,如果数组元素为对象,则为该对象创建观察者实例,并添加响应
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
复制代码

Observer在实例化过程中将this挂载到 value.__ob__ 上;也就是说响应式对象都有一个__ob__属性,指向Observer实例。还会在this.dep上挂载一个Dep 实例;

接下来判断是不是数组,如果是数组会走数组的处理逻辑(后面会说);如果是对象则调用walk遍历对象的每个属性,通过defineReactive方法给每个属性设置gettersetter

defineReactive

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 实例化 dep,一个 key 对应一个 dep
  const dep = new Dep()
  // 获取 obj[key] 的属性描述符,发现它是不可配置对象的话直接 return
  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]
  }
  // 递归调用,处理 val 即 obj[key] 的值为对象的情况,保证对象中的所有 key 都被观察
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // ...
      const value = getter ? getter.call(obj) : val
      // ...
      return value
    },
    set: function reactiveSetter (newVal) {/* ... */}
  })
}
复制代码

defineReactive 函数

  • 实例化一个 Dep 对象,一个key对应一个 Dep实例
  • 接着拿到 obj 的属性描述符,判断是否是可配置的;
  • 对子对象递归调用 observe 方法,这样就保证了无论 obj 的结构多复杂,它的所有子属性也能变成响应式的对象,这样访问或修改 obj 中一个嵌套较深的属性,也能触发 gettersetter
  • 利用 Object.defineProperty 去给 obj 的属性 key 添加 gettersetter

数组响应原理

Observer中有如下逻辑

if (Array.isArray(value)) {
  if (hasProto) {
    protoAugment(value, arrayMethods)
  } else {
    copyAugment(value, arrayMethods, arrayKeys)
  }
  this.observeArray(value)
} else {
  // 如果 value 是对象
  this.walk(value)
}
复制代码

在实例化Observer过程中,会判断value 是不是数组,如果是数组则会先判断当前环境有没有__proto__属性,如果有则执行protoAugment反之执行copyAugment

// can we use __proto__?
export const hasProto = '__proto__' in {}
复制代码

在看protoAugmentcopyAugment之前先看下 arrayMethods

arrayMethods定义在src/core/observer/array.js

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

methodsToPatch.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    // 方法添加的元素
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2) // splice 方法第二个参数之后都是新增元素
        break
    }
    // 对新增元素设置响应
    if (inserted) ob.observeArray(inserted)
    // 手动触发依赖通知
    ob.dep.notify()
    return result
  })
})
复制代码

Vue 会重写数组的一些方法,将重写后的方法,挂载到arrayMethods上。
重写思路如下:

  • 当调用数组方法时,会先执行原生方法,获取原生函数的返回值reslut
  • 如果是新增方法,则对新增的元素添加响应,前提是新增元素是对象
  • 触发当前数组的dep.notify 通知Watcher更新
  • 返回原生函数的返回值reslut

知道了arrayMethods的内容后,看下protoAugmentcopyAugment,这俩函数比较简单就不展示代码了,直接说干了啥

  • protoAugmentarrayMethods赋值给了数组的__proto__属性
  • copyAugment 遍历arrayKeysarrayMethods的属性名集合),将arrayMethods所有属性都挂载到了数组上

也就是说当被监听的对象如果是数组,Vue 会重写数组的一些方法,并将这些重写后的方法,挂载到数组上。当调用这些方法时,会先执行原方法并获取返回值,如果是新增属性,则对新增属性添加响应;最后手动触发更新。

需要注意的点是这里的dep是数组的Dep,在实例化Observer过程中,也会创建一个Dep实例

添加响应的流程图如下

observer.jpg

依赖收集

在看 getter 方法前,先看下前面说的 DepWatcher

Dep

import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  
  constructor () {
    this.id = uid++
    // 初始化 subs 数组,用于存储依赖此属性的 Watcher
    this.subs = []
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
Dep.target = null
const targetStack = []
// 修改 Dep.target 的值,此时 Dep.target 的值 就是当前正在更新的 Watcher(入栈)
// 并将当前正在更新的 Watcher 添加到 targetStack 中
export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}
// 删除 targetStack 最后一个元素,并修改 Dep.target 的值(出栈)
export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

复制代码

Dep 是一个 Class,它定义了一些属性和方法

  • Dep.target:这是一个全局唯一 Watcher,在同一时间只能有一个全局的 Watcher 被计算
  • this.subs:初始化 subs 数组,用于存储依赖此属性的 Watcher
  • addSub:将依赖此属性的Watcher添加到 subs中,做依赖收集
  • removeSub:删除 subs 中的Watcher
  • notify:遍历subs中所有元素,并执行元素的update方法,去通知 Watcher更新

Watcher

Watcher实例化过程比较复杂,在依赖收集的流程中会具体说一下

依赖收集的流程

先从挂载开始说,$mount会调用mountComponent函数去创建 一个渲染Watcher,并传入updateComponent函数,也就是class Watcher的第二个参数expOrFn

// ...
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
// ...
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher 这里将是 true  */)
复制代码

看下 Watcher 的定义

export default class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    // 对于渲染 Watcher而言,isRenderWatcher 为 true
    if (isRenderWatcher) {
      // 只有渲染 Watcher 才会将自身挂载到 vm._watcher 上
      vm._watcher = this
    }
    vm._watchers.push(this)
    if (options) {
      // 初始化 Watcher 的 属性,对于渲染 Watcher 来说 都是 false
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      // 渲染 Watcher 的 before 函数会执行 beforeUpdate 生命周期
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid
    this.active = true
    this.dirty = this.lazy
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
      // 渲染 Watcher 的 expOrFn 就是 updateComponent,是一个函数
    if (typeof expOrFn === 'function') {
      // 将 updateComponent 赋值给 getter 属性
      this.getter = expOrFn
    } else {}
    // 渲染 Watcher 的 lazy 属性为 false,所以在初始化过程中会执行 get 方法
    this.value = this.lazy
      ? undefined
      : this.get()
  }
  get () {}
  addDep (dep: Dep) {}
  cleanupDeps () {}
  update () {}
  run () {}
  evaluate () {}
  depend () {}
  teardown () {}
}
复制代码

初始化 渲染Watcher 的时候,需要注意以下几个点

  • vm._watcher = this将自身挂载到vm._watcher
  • 初始化四个属性
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
复制代码
  • updateComponent 赋值给 getter 属性
  • 执行 this.get()方法

看下 get方法

get () {
    // 将 Dep.target 赋值成当前 Watcher
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 执行 getter 方法,也就是执行 updateComponent 方法
      value = this.getter.call(vm, vm)
    } catch (e) {
    } finally {
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
}
复制代码

首先调用pushTargetDep.target 指向当前 渲染Watcher,调用this.getter从而执行_render方法,在执行_render方法的过程中,会获取响应式属性的属性值,从而触发属性的getter方法做依赖收集

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  // Object.getOwnPropertyDescriptor 返回对象上一个自有属性对应的属性描述符
  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]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 获取当前值
      const value = getter ? getter.call(obj) : val
      // 这里添加判断的可能性
      // 1. 修改属性值的时候,可能会获取其他属性的属性值,这里这样做可以防止再次进行依赖收集
      // 2. 当执行生命周期函数的时候,防止进行依赖收集
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          // 这样做的目的是
          // 1. 当执行数组的某些方法时,可以更新视图
              // 因为调用数组的某些方法其实会手动触发更新,但是更新的是数组 dep 里面存放的Watcher,所以在依赖收集过程中,需要将这个Watcher存放到数组的Dep实例里面
          // 2. 通过 $set 给对象添加属性时,可以更新视图(原因和第一条相同)
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // 如果是数组并且数组元素中有对象,将这个 Watcher 添加到对象的 dep.subs 中
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {/* ... */}
  })
}
复制代码

getter首先判断Dep.target是否存在,此时Dep.target指向当前渲染Watcher,接着执行dep.depend(),也就是执行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 做准备

回到 getter方法,如果 childOb 是一个Observer实例,则将当前 Watcher 添加到 childOb.depsubs 中;判断 value 是不是数组,如果是则执行dependArraydependArray作用是如果数组中有对象,则将 Watcher 添加到对象的dep.subs中。

依赖收集完成之后,回到 Watcher 的 get方法中,执行popTarget()恢复 Dep.target 的值,然后执行cleanupDeps清空过程;比对 newDepsdeps,如果 deps 有而 newDeps 没有,说明新的VNode已经不依赖当前属性了,则将当前watcher从这个 depsubs中删除

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

依赖收集完成后,VNode也创建完了,进入patch过程渲染节点。

派发更新

依赖收集的目的就是当修改数据的时候,可以对相关的依赖派发更新

当修改某个属性值的时候,会执行属性的setter方法

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()
  // Object.getOwnPropertyDescriptor 返回对象上一个自有属性对应的属性描述符
  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]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {},
    set: function reactiveSetter (newVal) {
      // 获取 旧的属性值
      const value = getter ? getter.call(obj) : val
      // 如果新旧属性值相等,直接返回
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      // 开发环境下,如果传入了 customSetter 执行 customSetter
      // 现在看来只有一种情况是 作为 props 的时候会报错
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (getter && !setter) return
      // 设置新值
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 如果新值是对象,给新值添加响应
      childOb = !shallow && observe(newVal)
      // 通知所有的订阅者更新
      dep.notify()
    }
  })
}
复制代码

setter方法会先比对新老属性值是否相等,如果不相等,则设置新值,执行dep.notify(),通知所有订阅者更新

dep.notify 是 Dep 类的一个方法:

 notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    // 触发 subs 中每个 Watcher 的 update 方法
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
复制代码

在依赖收集过程中,依赖此属性的 Watcher 都会添加到 subsdep.notify就是遍历subs,触发每个 Watcher 的update方法

  update () {
    // 渲染 Watcher 的 lazy 是 false,在计算属性的一节中会说这块
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      // 渲染 Watcher 的 sync 也是 false,在 watch 一节中会说
      this.run()
    } else {
      // 渲染 Watcher 会执行 queueWatcher
      queueWatcher(this)
    }
  }
复制代码

代码比较简单,对于渲染 Watcher 而言,就是执行 queueWatcher 方法

queueWatcher 方法定义在src/core/observer/scheduler.js

const queue: Array<Watcher> = [] // 队列,存储将要执行的 Watcher
let has: { [key: number]: ?true } = {} // 保证同一个 Watcher 只添加一次
let waiting = false // 保证对 nextTick(flushSchedulerQueue) 的调用逻辑在同一时刻只有一次
let flushing = false
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)
    }
  }
}
复制代码

queueWatcher方法先验证传入的Watcher有没有在队列中,保证同一个 Watcher 只添加一次;然后如果flushingfalse,则将 Watcher 添加到队列中;至于flushingtrue的情况在 watch 一节中会说;接下来如果waitingfalse,说明当前时刻没有nextTick等待执行,调用nextTick(flushSchedulerQueue)

从上面逻辑中不难看出, Vue 在做派发更新的时候并不会每次数据改变都触发 watcher 的回调,而是把这些 watcher 先添加到一个队列里,然后在nextTick后执行 flushSchedulerQueue

接下来看 flushSchedulerQueue 的实现,它的定义在 src/core/observer/scheduler.js

export const MAX_UPDATE_COUNT = 100
let circular: { [key: number]: number } = {}

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id
  // 对队列中的Watcher排序,根据 id 从小到大
  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      // 渲染Watcher 有 before 方法,会执行 beforeUpdate 钩子, 先父后子
      watcher.before()
    }
    id = watcher.id
    // 将 has 里面对应的 id 变成 null
    has[id] = null
    // 调用 watcher.run 方法触发更新
    watcher.run()
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      // 每个watcher执行一次,circular[id]就加一
      circular[id] = (circular[id] || 0) + 1
      // 当执行次数大于 100 时,会报错,循环执行
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn()
        break
      }
    }
  }
  // ...
  
  const updatedQueue = queue.slice()
  
  // 清空 queue、has、circular,并将 waiting、flushing 置为 false
  resetSchedulerState()
  // ...
  
  callUpdatedHooks(updatedQueue)
}
复制代码

当执行到下一队列时,会执行flushSchedulerQueue函数,首先对对队列中的Watcher排序,根据 id 从小到大:

    1. 组件更新是从父到子。(因为父组件总是在子组件之前创建)
    1. 组件的User WatcherRender Watcher之前执行(因为先初始化的watch,在初始化过程中会创建 User Watcher
    1. 如果一个组件在父组件的 Watcher 执行期间被销毁,那么它对应的 Watcher 执行都可以被跳过,所以父组件的 Watcher 应该先执行

然后遍历队列,执行watch.before方法,Render Watcherbefore方法就是执行组件的beforeUpdate生命周期函数。将has里面对应的id变成null,这样做的目的是在所有Watcher更新期间如果当前id对应的Watcher又要更新,可以再次将这个Watcher添加到队列中(比如在侦听器中修改某响应式属性的值)。然后调用watcher.run()触发更新。

  run () {
    if (this.active) {
      // 触发 Watcher 的 get 方法
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) { /* ... */ }
    }
  }
复制代码

对于渲染 Watcher 来说 watcher.run()方法会执行 Watcher 类的 get 方法触发组件更新,因为渲染 Watcher 的get方法,返回的是undefined,所以剩余if逻辑不会执行,这些逻辑跟计算属性、watch相关。组件更新完成之后回到flushSchedulerQueue函数中继续触发其他Watcher的run方法,等所有Watcher 的 run方法执行完之后,flushSchedulerQueue函数会执行resetSchedulerState去清空queuehascircular,并将waitingflushing置为false。然后调用callUpdatedHooks方法。

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    // 只有渲染 Watcher 才会将自身绑定到 vm._watcher 上
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'updated')
    }
  }
}
复制代码

callUpdatedHooks 说白了就是执行组件的updated生命周期,注意是先子后父的执行顺序。前提是vm._watcher === watcher并且组件已经执行过mounted生命周期,并且组件没有卸载。到此派发更新就结束了。

总结

Vue 的响应式原理就是通过Object.defineProperty去拦截数据的访问和修改;当执行组件render函数时,如果使用了响应式属性,会触发该属性的getter,并将组件的Render Watcher添加到此属性的dep.subs中这个过程叫做依赖收集。

当修改响应式属性时,触发setter,会通知dep.subs中所有Watcher更新,从而重新渲染组件。

官网上的响应式原理图
image.png

数组响应式

对于数组,Vue重写了 7 个方法;当获取数组数据时,在getter中会调用childOb.dep.depend(),将当前 Watcher 添加到数组Observer实例的this.dep中。当调用重写的方法时,会手动通知数组Observer实例的dep.subs中的所有 Watcher 更新

Vue.prototype.$set

向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性

当通过this.$set修改对象或数组数据时,也会触发视图更新,比如

this.$set(this.arr, 1, '修改数组元素')
this.$set(this.obj, 'test', '修改对象属性')
复制代码

先看下 set的代码

export function set (target: Array<any> | Object, key: any, val: any): any {
  // ...
  
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    // 如果是数组则调用 splice 方法触发更新
    target.splice(key, 1, val)
    return val
  }
  // 如果 target 中已经存在 key,说明是响应式属性,不需要后续操作
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  
  // ...
  
  // target 不是 Observer 实例
  if (!ob) {
    target[key] = val
    return val
  }
  // 将 新增的 key 添加到 ob.value 中并添加响应
  defineReactive(ob.value, key, val)
  // 手动触发依赖通知
  ob.dep.notify()
  return val
}
复制代码

Vue.prototype.$set方法也是通过手动触发通知的方式让视图更新,并对新增属性添加响应。

如果target是数组,则调用数组的splice方法,因为 Vue 重写了数组的splice方法,所有最后在重写的splice方法内手动触发更新;如果新增元素是一个对象,会对这个对象添加响应;

如果target是一个响应式对象,会获取__ob__属性(值为Observer实例),通过defineReactive,将新增属性添加到target上并添加响应;然后手动触发更新。

组件中的data为什么是一个函数

一个组件被复用多次,会创建多个实例。每个实例在初始化过程中都会从options中获取data,如果 data是一个对象的话,每个组件实例中data的指向是相同的,会造成冲突;所以如果是一个返回对象的函数的话,就可以解决这个问题。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享