关于前端:Vue-2x源码学习响应式改造

8次阅读

共计 13154 个字符,预计需要花费 33 分钟才能阅读完成。

家喻户晓,Vue 是以数据驱动视图展现的,即 Vue 会监听数据的变动,从而主动从新渲染页面的构造。

Vue 次要通过三步走来实现这个性能:

第一步是对数据进行响应式革新,即对数据的读写操作进行劫持;

第二步是对模板依赖的数据进行收集;

第三步是在数据发生变化时,触发组件更新。

数据响应式革新

0. defineReactive

对数据进行响应式革新的外围代码

// core/observer/index.js
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.target` 不为空时,通常指向一个 `watcher` 实例
        dep.depend() // 属性被收集到以后 `watcher` 实例的依赖数组中
        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()}
  })
}

通过 Object.defineProperty 批改对象属性的属性描述符 descriptor,来实现劫持对象属性的读写操作。

前置常识,对象的属性分为 data 型和 accessor 型。

data 型的属性描述符蕴含 value 和 writable;accessor 型的属性描述符蕴含 getter 和 setter 函数(两者至多存在一个)。

由上述代码能够看出,所有属性被解决成了 accessor 型属性,即通过 getter 和 setter 来实现读写,比方当咱们读取 person 对象上的属性 name,理论失去的是name 的属性拜访符中的 getter 函数执行后的返回值。

上述的响应式革新中,每个属性会对应一个 dep 实例:const dep = new Dep(),如果属性值 val 是对象或数组,会被列入察看对象,他的属性会被递归进行响应式革新let childOb = !shallow && observe(val)

get 函数被用于收集依赖

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.target 不为空,即当下有一个监听器 watcher 在收集依赖,就进行依赖的收集,dep 实例会被收集到该 watcher 的依赖数组 newDeps 中,同时 dep 也会将此 watcher 记录到本人的 subs 订阅数组中,记录有谁订阅了本人的变更。

如果 childOb 不为空(即属性值 val 为数组或对象,且可扩大),就对 val 的 __ob__ 属性也进行收集操作。

如果 value 是数组,对数组中的对象元素也进行依赖收集。

就是一层层的递归收集。

set 函数被用于告诉变更:

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

实现属性赋值操作后,调用 dep.notify(),告诉所有订阅了本人的watcher 实例执行 update 操作,即上面代码中的 for 循环操作。

// core/observer/dep.js
export default class Dep {
  // ...

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

如果是同步 this.sync 的 watcher 会立刻被执行,否则会插入到 watcher 队列 queueWatcher(this) 排队期待执行:

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

1. initInjections

// core/instance/inject.js
export function initInjections (vm: Component) {const result = resolveInject(vm.$options.inject, vm)
  if (result) {toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}

通过执行 resolveInject 解析 inject 中的数据,解析后果赋值给 result。result 蕴含 inject 中所有的 key,如果下级组件中没有对应 inject 数据的 provide,就赋默认值,简略来说大抵就是result[key] = inject[key] || default

再调用 defineReactive(vm, key, result[key]) 将这些 key 加到 vm 实例上,即 inject 中的数据也会进行响应式解决。

假如存在一个 inject:["person"],如果person 的值是个对象,它的值会被列为察看对象,以后子组件的 watcher 会对该对象的属性依赖收集,在下级组件中更改了原始 person 的某个属性,就会触发子组件的更新。

2. initProps

// core/instance/state.js
function initProps (vm: Component, propsOptions: Object) {const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  if (!isRoot) {toggleObserving(false)
  }
  for (const key in propsOptions) {keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

propsOptions,接管的是 vm.$options.props,是申明接管的 props 的配置,;vm.$options.propsData 是理论接管到的 props 数据。

调用 defineReactive(props, key, result[key]) 将 propsOptions 上的 key 加到 props 对象上,即 vm._props 上,进行响应式解决,如果是在 vm 上不存在的 key,通过 proxy(vm, '_props', key) 操作,使得能够通过 vm 间接拜访到 _props 的属性,而不须要通过 _props 对象来拜访。

3. initData

// core/instance/state.js
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 */)
}

data 选项会被挂在 vm._data 上,从上述代码中能够看出,data 必须是一个对象,或者返回值为对象的函数。

通过 proxy(vm, '_data', key) 操作,vm 能够间接拜访到 _data 的属性,而不须要通过 _data 对象来拜访。

最初通过 observe(data, true /* asRootData */) 来对数据做响应式革新,能够看到这个 observe 办法多传了一个参数值为 true,标记以后解决的数据是$options.data 对象。

observe办法理论是创立一个新的 ob 实例,数据的 __ob__ 属性置信在控制台打印过 vue 中数据的同学都不生疏,都是指向 ob 实例。

// core/observe/index.js
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
}
// core/observe/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
    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])
    }
  }
}

从下面 Observer 的构造函数中能够看出,创立 ob 实例后,这个实例就挂载数据的 __ob__ 属性上了,因为在 iniDats 时传递给构造函数的参数是个对象,所以会调用 walk 办法,持续看 walk 办法的定义,能够看出,是把这个对象的属性一一取出,调用 defineReactive(obj, keys[i]) 进行响应式革新。

4. initComputed

// core/instance/state.js
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)
      } else if (vm.$options.methods && key in vm.$options.methods) {warn(`The computed property "${key}" is already defined as a method.`, vm)
      }
    }
  }
}

取得每个计算属性对应 watcher 的初始值

从上述代码中能够看出,会遍历 computed 所有的属性,每个属性对应配置一个 watcher 实例,watcher 实例在创立时,会调用每个 computed 对应的 getter 获取一遍初始值,放在 watcher 实例的 value 属性上

// core/observer/watcher.js
export default class Watcher {
  // ...

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {vm._watcher = this}
    vm._watchers.push(this)
    // ...
    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
  }

  // ...
}
// core/observer/dep.js
export function pushTarget (target: ?Watcher) {targetStack.push(target)
  Dep.target = target
}

能够看到,执行 watcher 实例的 get() 办法时,会进行一个 pushTarget(this) 的操作,此操作批改了Dep.target,使它指向了以后的 watcher 实例,如果某个 computed 属性依赖了 data 中的某个属性,须要读取 data 中的某个属性值,就会触发该 data 属性的 getter 函数,使得该 data 属性被收集到以后 watcher 实例的依赖数组中。

实现 computed 属性的取值后,执行popTarget(),即上面的代码:

// core/observer/dep.js
export function popTarget () {targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

会使 Dep.target 指回上一个 watcher 实例。

最初清理依赖 this.cleanupDeps(),将不再关联的依赖 dep 其订阅数组中对应的 watcher 移除,将newDeps 赋值给 deps 并清空 newDeps,代表该watcher 实例一次依赖收集结束。

计算属性被读取时,其对应 watcher 依赖的数据会被以后 watcher 收集为本身的依赖

如果 computed 某个属性的标识符不在 vm 实例上,就继续执行 defineComputed(vm, key, userDef),会将给 vm 实例增加一个名为key 的属性,该属性的 getter 函数由下述代码定义:

// core/instance/state.js
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
    }
  }
}

即在这个计算属性被读取时,会拿到它所对应的 watcher 实例,如果以后 Dep.target 不为 null 时,watcher 会执行实例办法depend()

export default class Watcher {
  // ...

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

  // ...
}

能够看到此 watcher 实例的依赖 deps 会被一一取出,执行 dep 实例的 depend 办法:

// core/observerdep.js
depend () {if (Dep.target) {Dep.target.addDep(this)
    }
  }

即此 watcher 实例的依赖都会被收集到以后 Dep.target 指向的 watcher 实例的依赖数组中。

5. initWatch

// core/instance/state.js
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)
    }
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

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) {const info = `callback for immediate watcher "${watcher.expression}"`
      pushTarget()
      invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
      popTarget()}
    return function unwatchFn () {watcher.teardown()
    }
  }

initWatch 的内容比较简单,就是通过调用 createWatcher(vm, key, handler),一一对应生成watcher 实例,并且给 watcher 实例标记 options.user = true,代表这个watcher 是用户配置的。

每个 watch 通常是有一个对应的表达式(通常是 vm 的 data 数据)和一个对应的回调函数,应用场景通常是当 vm 中的某些数据产生扭转时,用户须要做一些自定义的操作来做解决。

与 computed 中生成对应 watcher 实例相似,watcher 实例在创立时,每个 watch 对应的表达式就会被求值一遍,即 vm 实例上的某些数据属性被读取,这些属性对应的 dep 会被收集到该 watcher 实例的依赖数组中,求得的值会放在 watcher 实例的 value 属性上,如果某个 watch 配置了 immediate,就立刻执行一遍 watch 对应的回调函数,入参为watchervalue属性值。

能够看到在执行回调函数前,执行了一个 pushTarget(),此时 Dep.target 会指向空,所以在回调函数执行过程中,如果 vm 的某些数据属性被拜访,这些属性不会被收集依赖,因为属性的getter 函数中在属性被收集依赖前有个对 Dep.target 的判空查看。

6. 一些阐明

__ob__是给对象加的属性,指向 observer 实例,ob 和对象是一对一,代表这个对象被察看,该对象的属性的读写操作会被做响应式解决,即被劫持。

dep是给属性配置的用于依赖收集的,通常对象的某个属性与 dep 是一对一,能够被多个 watcher 收集,即多个 watcher 实例在监听这个属性的变动;__ob__是对象的非凡属性,它也有本人的 dep,能够被 watcher 收集。

一个 watcher 实例只会关联一个 vm,一个 vm 实例能够关联多个 watcher,watcher 实例会放在vm._watchers 数组中;渲染 watcher 还会放在 vm._watcher 上,渲染 watcher 从字面上了解就是与组件渲染无关的watcher

正文完
 0