当前位置: 首页 > news >正文

Vue 2.x源码学习:数据响应式改造

众所周知,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会对该对象的__ob__属性依赖收集,在上级组件中更改了原始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

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

相关文章:

  • 大楼物流公司网站源码/网站建设费用多少钱
  • 网站系统维护一般多长时间/教育机构网站
  • 做网站 信科网站建设便宜/域名注册查询软件
  • html5软件/上海关键词优化的技巧
  • 网站的技术维护一般要做些什么/网站收录查询系统
  • 网站建设用英文怎么说/百度访问量统计
  • OpenJudge NOI 2.2 9273:PKU2506Tiling | 2.3 9273:PKU2506Tiling
  • 【微信小程序】动态设置导航栏标题
  • day49 业务逻辑水平垂直越权访问控制脆弱验证
  • 超详解通讯录,保姆式教学 ,轻松学会实现通讯录的功能【c语言】
  • 表达式求值【NOIP2013普及组】
  • java 常见函数 和 mapper
  • 命名空间!C++之路扬帆起航
  • N+1终于等到了 但却放弃了
  • 小程序样式和组件基础
  • Lr 12 ACR 15:蒙版
  • 操作系统面试题(史上最全、持续更新)
  • Openmp Runtime 库函数汇总(下)——深入剖析锁原理与实现