Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue 源码分析(响应式原理) #14

Open
yangdui opened this issue May 16, 2020 · 0 comments
Open

Vue 源码分析(响应式原理) #14

yangdui opened this issue May 16, 2020 · 0 comments

Comments

@yangdui
Copy link
Owner

yangdui commented May 16, 2020

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

我们知道 Vue 是数据响应式的,这一章节我们探讨其实现原理。

核心是通过 Object.defineProperty 设置对象属性的属性描述符,通过 setter、getter 将设置和获取属性值变成函数。

首先简单描述一下响应式的过程,这样有一个总体的认识,方便理解。

Vue 中的 defineProperty:

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    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 */
    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
    }
    childOb = !shallow && observe(newVal)
    dep.notify()
  }
})

只看 get 中的 dep.depend() 和 set 中的 dep.notify()。

dep 先简单理解为一个收集容器。

dep.depend() 就是一个收集过程。收集东西就是 Watcher,Watcher 又是什么呢?Watcher 实际是一个 Class。在介绍渲染的时候,mountComponent 函数中出现了 Watcher:

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

我们这里就为了理解,把 Watcher 当做一个组件来看待(Wathcer 当然不是组件)。

dep.notify() 就是遍历的过程。在数据发生改变的时候,将 dep 收集的 Watcher 重新渲染。比如:

let vm = new Vue({
	el: '#app',
	data: {
		val: 1
	},
	template: `
		<div>
			{{this.val}}
			<button @click="onClick">click me</button>
		</div>
	`,
	methods: {
		onClick: function () {
			this.val = 2;
		}
	}
});

数据 val 的 getter 函数会将 template 收集到 dep 中,当修改 val 数据的时,setter 函数会调用 dep.notify() 重新渲染 template。

原理很简单,实际考虑的情况非常多。

响应式对象

在 Vue 初始化阶段,会执行 initState 函数。

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

在 initState 函数中,先关注 initData。

function initData (vm: Component) {
	...
	
	proxy(vm, `_data`, key)
	
  // observe data
  observe(data, true /* asRootData */)
}

initData 函数最后一句 observe(data, true) 就是将 data 对象中的属性添加 setter 和 getter 的入口。

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
}

在 observe 函数中,是通过 new Observer(value) 处理的。在这之前判断 data 对象是否已经有 ‘__ob__’ 属性,如果有 '__ob__' , 证明已经处理为响应式对象了,直接使用 ‘__ob__’ 属性的值。

proxy

在 initData 函数中有 proxy(vm, _data, key) 这一句代码。其中 proxy 函数是把 props 和 data 上的属性代理到 vm 实例上:

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

如果没有 proxy 函数,我们访问 data 数据就是 vm._data.value,现在我们可以直接通过 vm 访问 vm.value。

Observe

Observe 是一个 Class ,作用就是给对象添加 setter 和 getter。

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * 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])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

在构造函数中实例化了 Dep 对象,但是这个 dep 对象和收集 Watcher 的 dep 不是同一个对象,在后面会介绍。

然后判断 value 是数组还是对象,数组采用 observeArray 方法,对象采用 walk 方法。先关注对象的处理。在 walk 方法中会调用 defineRective 函数。defineRective 最终给对象属性添加 setter 和 getter 的地方。

defineRective

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  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
      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
      }
      /* 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
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

首先实例化了 Dep 对象 dep。这个 dep 对象就是 data 对象属性的收集依赖的容器。最后通过 Object.defineProperty 方法添加 getter 和 setter 数据描述符。

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
  return
}

如果对象属性的 configurable 是 false 的话,直接返回,不处理。

const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
  val = obj[key]
}
let childOb = !shallow && observe(val)

定义 getter 和 setter 变量储存 property 的 get 和 set 函数。

if 语句中的 val 关系到是否进行深度观测 let childOb = !shallow && observe(val)。如果 val 没有值,是不会进行深度观测的。在 data 是对象,在调用 Observe 类中的 walk 方法时, defineReactive(obj, keys[i]) defineRective 函数只传递了两个参数。所以 aruments.length === 2 是成立的。shallow 也是 undefined。

现在关注点在 (!getter || setter) 上面。为什么会存在这样一个表达式?应该还是基于可靠性作出的选择。

因为如果存在 getter ,这个 getter 是用户自己定义的,是不可控的,不知道在 getter 函数中做了什么操作,目的是什么,对于 Vue 来说都不知道。所以就不会进行深度观测。

但是存在 setter 为什么又要进行深度观测?通过 Object.defineProperty 之后,属性会添加 getter 和 setter。如果开始没有 getter,在之后会添加 getter,那么就会进行深度观测。这和开始不观测是不一致的,所以采用属性原本存在 setter时,无论是否存在 getter ,都要进行深度观测。

这里个人觉得处理是有问题的,假如一开始都存在 getter 和 setter 时,会执行 val = obj[key],就会执行 getter 函数,那么就会丢失开始设计的目的:存在 getter 时,不进行深度观测。一般情况下,getter 和 setter 都是成对出现的。

收集依赖 getter

响应式对象的 getter 就是收集依赖的

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    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
  },

  ...

 }

收集依赖是通过 dep.depend() 完成的。在 getter 函数里判断了 childOb 是否存在,如果存在,调用 childOb.dep.depend() 。我们知道 Vue2.x 版本是没有通过 Proxy 直接拦截给对象添加属性的操作。所以 Vue 提供了 Vue.set 、$set 给对象添加属性的同时触发依赖。触发依赖的前提要收集依赖,childOb.dep.depend() 就是把属性 key 的依赖收集到到 key 对象自身中。给 key 对象添加属性时,和修改属性 key 时触发依赖是相同的

Vue.set 如下:

Vue.set = function (obj, key, val) {
  defineReactive(obj, key, val)
  obj.__ob__.dep.notify()
}

Dep

Dep 是收集依赖和触发依赖的容器,定义在 src/core/observer/dep.js 文件中

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

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

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

  depend () {
    if (Dep.target) {
      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()
    }
  }
}

其中 depend 函数

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

Dep.target 就是 Watcher。收集依赖实际就是收集 Watcher,Watcher 的 addDep 方法会调用 Dep 的 addSub 方法,将 Watcher 收集到 subs 数组中。

Watcher

在介绍具体过程中,先了解 Watcher 类

Watcher 定义在 src/core/observer/watcher.js 文件中

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy 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 = noop
        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
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.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
  }

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

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

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

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }
}

Watcher 就是一个 Class 。在 mountComponent 函数中实例化了 Watcher,其他地方遇见了在说明。

在构造函数中定义了和 Dep 相关的属性

this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()

这些属性是避免重复收集依赖和移除无用依赖相关的。

Watcher 也定义了一些其他原型方法:addDep、cleanupDeps 等。

关于 Watcher 具体作用需要结合整个收集触发依赖讲解。

收集依赖过程

收集是从 getter 开始的,也就是 dep.depend() 开始的。depend 函数如下

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

Dep.target 全局变量又是通过实例化 Watcher 得到的,对于 Vue 的 data 选项,是在 mountComponent 函数中实例化 Watcher。

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

...

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

在 Watcher 类中,get 方法会调用 pushTarget 函数。pushTarget 定义在 src/core/observer/dep.js 文件中

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

在 pushTarget 函数中,首先把 target(也就是 Watcher 实例)压入栈中,然后 Dep.target 赋值为 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)
    }
  }
}

if (!this.newDepIds.has(id)) 是避免在一次渲染求值中多次重复收集依赖。比如说:

<div id="app">
	{{age}} --- {{age}}
</div>

在一次渲染求值中,age 会求两次值,需要避免第二次收集依赖。

if (!this.depIds.has(id)) 是避免多次渲染求值中重复收集依赖。比如:

<div id="app">
	{{age}}
</div>

在一次渲染中 age 的 getter 只会收集一次依赖,但是在修改 age 后,重新渲染会再次收集依赖。此时检查 depIds 中是否存在该依赖。每个 dep 对象都有一个唯一的 id,newDepIds 表示本次渲染中 dep 实例数组。depIds 储存上次渲染 dep 实例数组。

dep.addSub(this) 是真正执行收集的地方。

又回到 Dep 的 addSub 方法中

addSub (sub: Watcher) {
  this.subs.push(sub)
}

addSub 方法很简单,将 Watcher 实例放在 dep 的 subs 数组中。

到这里收集依赖的过程已完成。

但是 dep.depend 是在 getter 函数中,getter 函数需要求值才会调用。那么什么时候才会求值 data 中的属性呢。当然是在渲染时候,会将 data 中的属性求值展示出来。这个过程还是要回到 Watcher 中。

在渲染过程中实例化 Watcher 会传入 updateComponent 函数

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

在 Watcher 的构造函数中,会将 updateComponent 赋值给 this.getter,并执行 get 方法。

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
}

在 get 方法中,会执行 this.getter 函数,也就是 updateComponent 函数,最终会获取 data 中属性的值,进而收集依赖。

在 get 方法中,有一个 finally 语句。这段代码的逻辑是收集完依赖之后,需要收尾工作。this.deep 遇见时在介绍。

第一,就是通过 popTarget 恢复 Dep.target 的值。

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

第二,需要通过 this.cleanupDeps() 清空依赖。

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
}

while 循环作用是移除不在需要的依赖 Watcher。然后把 newDepIds 和 depIds 交换,newDeps 和 deps 交换,并把 newDepIds 和 newDeps 清空。

触发依赖 setter

setter 是触发依赖的函数

Object.defineProperty(obj, key, {
    
    ...

  set: function reactiveSetter (newVal) {
    const value = getter ? getter.call(obj) : val
    /* eslint-disable no-self-compare */
    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
    }
    childOb = !shallow && observe(newVal)
    dep.notify()
  }
})

重点就是最后两段代码

childOb = !shallow && observe(newVal)
dep.notify()

childOb = !shallow && observe(newVal) 是对新设置的值进行深度观测。

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

notify 对遍历 subs 数组,调用 Watcher 实例的 update 方法。

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

update 方法对于不同 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)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

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

queueWatcher 函数引入了队列,并不是每次数据改变就立刻更新。而是将 watcher 放入队列中,等数据完全改变之后,最后通过 nextTick 异步更新。

if (has[id] == null) 是防止将重复的 watcher 放入队列中。

if (!flushing) 处理正在更新时,watcher 放入队列的情况。

if (!waiting) 确保无论 queueWatcher 函数调用多少次,只执行一次这个 if 语句,也就是只执行一次 nextTick 函数。

flushSchedulerQueue

nextTick 的回调函数是 flushSchedulerQueue 。

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

重要的是下面节点

  1. 队列排序: queue.sort((a, b) => a.id - b.id)

    至于为什么需要排序,源码中的注释已经给出原因:

    • 组件更新时从父到子。
    • 用户自定义的 watcher 优先于渲染 watcher 进行。
    • 如果一个组件在父组件执行期间被销毁,那么该组件就可以跳过不执行。
  2. 队列遍历

    循环执行队列中的 watcher。对于渲染过程而言,watcher.run ---> watcher.get ---> updateComponent

  3. 状态恢复

    执行 setSchedulerState 函数。

    function resetSchedulerState () {
      index = queue.length = activatedChildren.length = 0
      has = {}
      if (process.env.NODE_ENV !== 'production') {
        circular = {}
      }
      waiting = flushing = false
    }
    

    把一些变量恢复到开始状态。

  4. 钩子函数的执行

    callActivatedHooks(activatedQueue)
    callUpdatedHooks(updatedQueue)
    

nextTick

nextTick 实现在 src/core/util/next-tick.js 文件。

nextTick 函数作用相当于 setTimeout,不使用 setTimeout 的原因是 setTimeout·精度不够。在浏览器中,存在 macrotask 和 microtask 两种队列,microtask 队列任务执行完成之后就会执行渲染,如果数据更新能在 microtask 完成,那么只需一次重新渲染就会得到最新的 DOM。setTimeout 的回调会放在 macrotask 中,所以 setTimeout 是最后的选择。

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

最终是调用了 timerFunc。根据浏览器兼容性,timerFunc 有以下几种形态

  • 使用 Promise

    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      timerFunc = () => {
        p.then(flushCallbacks)
        // In problematic UIWebViews, Promise.then doesn't completely break, but
        // it can get stuck in a weird state where callbacks are pushed into the
        // microtask queue but the queue isn't being flushed, until the browser
        // needs to do some other work, e.g. handle a timer. Therefore we can
        // "force" the microtask queue to be flushed by adding an empty timer.
        if (isIOS) setTimeout(noop)
      }
      isUsingMicroTask = true
    } 
    
  • 使用 MutationObserver

    else if (!isIE && typeof MutationObserver !== 'undefined' && (
      isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
      // Use MutationObserver where native Promise is not available,
      // e.g. PhantomJS, iOS7, Android 4.4
      // (#6466 MutationObserver is unreliable in IE11)
      let counter = 1
      const observer = new MutationObserver(flushCallbacks)
      const textNode = document.createTextNode(String(counter))
      observer.observe(textNode, {
        characterData: true
      })
      timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
      }
      isUsingMicroTask = true
    }
    
  • 使用 setImmediate

    else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      // Fallback to setImmediate.
      // Techinically it leverages the (macro) task queue,
      // but it is still a better choice than setTimeout.
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    }
    
  • 使用 setTimeout

    else {
      // Fallback to setTimeout.
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    

计算属性

计算属性 computed 初始化在 initComputed 函数完成,initComputed 定义在 src/core/instance/state.js 文件中

const computedWatcherOptions = { lazy: true }

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}

initComputed 函数主要创建了计算属性观测者:computed watcher。并存储在 watchers 对象中。watchers 对象是通过 const watchers = vm._computedWatchers = Object.create(null) 创建的。

其中 computed watcher:

watchers[key] = new Watcher(
  vm,
  getter || noop,
  noop,
  computedWatcherOptions
)

getter 参数就是计算属性的值:

const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get

getter 既可以是函数也可以是带有 get 属性的对象。

initComputed 函数最后调用了 defineComputed 函数,defineComputed 作用就是给计算属性添加 getter。

export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

在 defineComputed 函数最后调用 Object.defineProperty(target, key, sharedPropertyDefinition) 设置 getter。

getter (sharedPropertyDefinition.get) 是 createComputedGetter 函数的返回值 computedGetter 函数。

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

所以最后取计算属性的值是computedGetter 函数的返回值 watcher.value。

计算属性实现过程

通过例子查看计算属性具体实现过程

let vm = new Vue({
	el: '#app',
	data: {
		val: 1
	},
  computed: {
    age: function () {
      return this.val + 1;
    }
  },
  template: `
    <div>
      <div> {{val}}---{{age}} </div>
      <button @click="onClick">click</button>
    </div>
  `,
	methods: {
		onClick: function () {
			this.val++;
		}
	}
});

计算属性的初始化在渲染之前。所以在获取计算属性的值时,就是调用 computedGetter 函数的过程。

function computedGetter () {
  const watcher = this._computedWatchers && this._computedWatchers[key]
  if (watcher) {
    if (watcher.dirty) {
      watcher.evaluate()
    }
    if (Dep.target) {
      watcher.depend()
    }
    return watcher.value
  }
}

首先获取计算属性观察者 watcher。这里 watcher 肯定是存在,if (watcher) 为 true。if (watcher.dirty) 在这里也是为 true 的。在初始化时:

const computedWatcherOptions = { lazy: true }

设置 computedWatcherOptions 属性 lazy 为 true。然后在实例化 Watcher 时:

this.dirty = this.lazy

将 lazy 赋值给 dirty。

然后执行 watcher.evaluate。找到 Watcher 的 evaluate 方法:

evaluate () {
  this.value = this.get()
  this.dirty = false
}

this.get() 就是在执行实例中计算属性函数:

function () {
  return this.val + 1;
}

将 this.get() 结果赋值给 this.value 也就是 watcher.value。最后设置 this.dirty = false。

在 Vue 官方文档中:

计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。

在首次渲染获取计算属性值时,会调用计算属性函数。我们知道 val 是响应式的,会在获取值时收集依赖,这里是会收集计算属性观测者。

当 val 发生改变时,会调用计算属性观测者对象的 update 方法

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}

在实例化计算属性观察者对象时会 Watcher 构造函数中初始化 this.lazy = this.lazy = !!options.lazy = true。所以这里会重新设置 this.dirty = true(在首次渲染后 this.dirty 置为 false)。

在获取计算属性值时,如果 watcher.dirty 为 true 会重新求值,也就是重新调用计算属性函数。如果 watcher.dirty 为 false,直接返回上一次的值。

所以计算属性只有在相关依赖发生改变时,才会重新求值。

侦听属性

Vue watch 选项通过 initWatch 初始化.。

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

在 initWatch 中调用 createWatcher 函数,在 createWatcher 函数中又是通过 vm.$watch(expOrFn, handler, options) 完成的。$watch 和 watch 选项是相同的。

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  return function unwatchFn () {
    watcher.teardown()
  }
}

判断 cb 是否是对象,如果是对象,循环执行 createWatcher 函数。

设置 watch 为用户自定义:options.user = true。

实例化 Watcher:const watcher = new Watcher(vm, expOrFn, cb, options)。

如果 immediate 为 true ,需要立刻执行回调函数。

返回可以取消监听函数 unwatchFn。

侦听属性实现过程

在实例化过程中,被监听的 key 会收集实例化对象。比如

let vm = new Vue({
	el: '#app',
	data: {
		val: 1
	},
	watch: {
		val: function watchVal (val, oldVal) {
			console.log(val);
		}
	},
	template: `
		<div>
			{{val}}
			<button @click="onClick">click</button>
		</div>
	`,
	methods: {
		onClick: function () {
			this.val++;
		}
	}
})

执行到 const watcher = new Watcher(vm, expOrFn, cb, options) 这里时,val 会收集实例化对象 watcher。当 val 发生改变时,就会执行 val 的观察函数 watchVal。

在实例化 Watcher 时候,可以传入 deep 选项。我们知道 deep 为 true 时,如果被监听的 key 是对象,那么对象内部发生变动,也会触发观察函数。

实现的原理是:假如被监听的是 obj 对象,obj 对象有 a 属性。除了 obj 本身收集 wathcer 之外,obj.a 的 getter 也收集 watcher。

如果 deep 为 true,在实例化 Wathcer 时,get 方法:

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
}

进入 traverse(value)。traverse 函数会调用 _traverse。_traverse 定义在 src/core/observer/traverse.js

function _traverse (val: any, seen: SimpleSet) {
  let i, keys
  const isA = Array.isArray(val)
  if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
    return
  }
  if (val.__ob__) {
    const depId = val.__ob__.dep.id
    if (seen.has(depId)) {
      return
    }
    seen.add(depId)
  }
  if (isA) {
    i = val.length
    while (i--) _traverse(val[i], seen)
  } else {
    keys = Object.keys(val)
    i = keys.length
    while (i--) _traverse(val[keys[i]], seen)
  }
}

只有对象和数组,没有被冻结和不是 VNode 对象才会做深度观测。

if (val.__ob__) 语句是处理死循环。

var obj = {
	a: obj1
};

var obj1 = {
	a: obj
}

如果 seen.has(depId) 为真,那么证明是已经观测过了,直接返回,否则把 depId 存入 seen 中。

最后的 if...else 语句通过 while 循环收集观察者:val[i] 和 val[keys[i]] 会获取相应的值,此时会执行 getter 函数收集观察者。

特殊处理

添加属性

defineReactive 函数中 Object.defineProperty 处理的是对象存在的属性,对象或数组需要新添加属性,Vue 必须要使用 Vue.set 或 $set 处理。最后都是通过 set 函数处理的。

set 函数定义在 src/core/observer/index.js

export function set (target: Array<any> | Object, key: any, val: any): any {
  if (process.env.NODE_ENV !== 'production' &&
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: 		any)}`)
  }
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

判断 target 是否是数组,并且 key (索引)是否可用,进行相应的处理。数组在后面介绍。

如果 key 存在 target 上,并且不再原型上时,不做特殊处理,因为如果是这样的情况,该 key 已经被观测过了,直接赋值返回。

如果 target 对象自身没有被观测过,也就是没有 __ob__ 属性,也是不做处理,直接赋值返回。因为 target 对象可能是被冻结的对象。

通过 defineReactive(ob.value, key, val) 观测新添加的 value 值。

通过 ob.dep.notify() 手动触发依赖。这里的 ob.dep 是在 defineReactive 函数中完成赋值的

let childOb = !shallow && observe(val)

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      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
    },
    
    ...
    
    })

childOb 就是这里的 ob 值,在 get 函数中 childOb.dep.depend() 收集了相关依赖。在介绍 Observe 时候,我们说过 Observe 类和 defineReactive 函数中的 const dep = new Dep() 是不一样的,前者是为 Vue.set 准备的。

处理数组

Vue 对于通过索引修改数组也是不能监测的:vm.arr[indx] = value。这种情况使用 Vue.set(vm.arr, indx, value)。

数组不同于一般对象,如果用 Object.defineProperty 收集和触发依赖,既复杂还可能造成性能问题,所以在 Vue 中单独处理。

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * 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])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

在 Observe 构造函数中,如果 value 是数组,执行:

if (Array.isArray(value)) {
  if (hasProto) {
    protoAugment(value, arrayMethods)
  } else {
    copyAugment(value, arrayMethods, arrayKeys)
  }
  this.observeArray(value)
}

对数组自身响应式处理是通过 protoAugment 或 copyAugment 处理。对于数组元素通过 observeArray 循环处理。

hasProto 判断 __proto__ 是否可用,如果可用,调用 protoAugment:

function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}

否则调用 copyAugment:

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

protoAugment 函数直接将 target.__proto__ 修改为 src。copyAugment 函数遍历 keys,通过 Object.defineProperty 定义自身属性。它们的目的都是是 target 能使用 arrayMethods 对象中的方法。

能改变数组自身的方法有:push、pop、shift、unshift、splice、sort、reverse。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'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original 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)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

def(arrayMethods, method, function mutator (...args) {...} 将上面那些方法定义在 arrayMethods 对象上。

mutator 函数必须保证数组方法原有的作用:

const result = original.apply(this, args)

push、unshift、splice 方法会修改或增加值,对于改变的值也需要观测:

if (inserted) ob.observeArray(inserted)

最后手动触发依赖

ob.dep.notify()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant