首先说一下vue的响应式,数据模型仅仅是普通的js对象,当你修改它们的时候,视图就会更新。

当把一个普通js对象传给Vue实例的data,Vue将遍历对象所有的属性,使用Object.defineProperty将这些属性转为getter/setter,
在内部让Vue追踪依赖,在属性被访问或者修改的时候通知变化,每个组件实例都有watcher实例。它在组件渲染的途中把属性记为依赖,之后当依赖项的setter被调用时,通知watcher重新计算,从而使关联的组件更新。

vue的响应式
核心有三点 observer、dep、watcher
observer:遍历data属性 ,使用Object.defineProperty的get/set进行数据劫持。
dep: 每个属性都有自己的消息订阅器dep,用于存放了所有订阅了该属性的观察者对象
watcher:观察者 对象,通过dep实现对响应属性的监听,监听到果,主动触发自己的回调进行响应。

对响应式有个初步了解后,我们来看看comouted相关的源码
初始化computed是在src/core/instance/state.js 文件中的 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 */)
  }
  // computed初始化
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

调用了 initComputed 函数(其前后也分别初始化了 initData 和 initWatch )并传入两个参数 vm 实例和 opt.computed 开发者定义的 computed 选项,转到 initComputed 函数:


const computedWatcherOptions = { computed: 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)
      }
    }
  }
}

我们拆开来看最主要的部分

1.获取计算属性的定义 userDef 和 getter 求值函数

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

定义一个computed有两种方法,一种是直接跟一个函数,另一种是添加一个get和set方法的对象形式,所以这里首先获取计算属性定义的userDef,再根据userDef的类型来获取响应的getter求值函数。

2.计算属性的观察者 watcher 和消息订阅器 dep


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

这里的 watchers 也就是 vm._computedWatchers 对象的引用,存放了每个计算属性的观察者 watcher 实例(注:后文中提到的“计算属性的观察者”、“订阅者”和 watcher 均指代同一个意思但注意和 Watcher 构造函数区分),Watcher 构造函数在实例化时传入了 4 个参数:vm 实例、getter求值函数、noop 空函数、computedWatcherOptions 常量对象(在这里提供给 Watcher 一个标识 {computed:true} 项,表明这是一个计算属性而不是非计算属性的观察者,我们来到 Watcher 构造函数的定义:


class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    if (options) {
      this.computed = !!options.computed
    } 
 
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
      this.value = this.get()
    }
  }
  
  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      value = this.getter.call(vm, vm)
    } catch (e) {
      
    } finally {
      popTarget()
    }
    return value
  }
  
  update () {
    if (this.computed) {
      if (this.dep.subs.length === 0) {
        this.dirty = true
      } else {
        this.getAndInvoke(() => {
          this.dep.notify()
        })
      }
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
 
  evaluate () {
    if (this.dirty) {
      this.value = this.get()
      this.dirty = false
    }
    return this.value
  }
 
  depend () {
    if (this.dep && Dep.target) {
      this.dep.depend()
    }
  }
}

突出重点
对于计算属性而言 watcher 会执行 if 条件成立的代码 this.dep = new Dep(),而 dep 也就是创建了该属性的消息订阅器。


export default class Dep {
  static target: ?Watcher;
  subs: Array<Watcher>;
 
  constructor () {
    this.id = uid++
    this.subs = []
  }
 
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
 
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
 
  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}
 
Dep.target = null

Dep 同样精简了部分代码,我们观察 Watcher 和 Dep 的关系,用一句话总结
watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

3.defineComputed 定义计算属性


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

因为 computed 属性是直接挂载到实例对象中的,所以在定义之前需要判断对象中是否已经存在重名的属性

defineComputed 传入了三个参数:vm实例、计算属性的 key 以及 userDef 计算属性的定义(对象或函数)


export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : userDef
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : userDef.get
      : noop
    sharedPropertyDefinition.set = userDef.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)
}

在这段代码的最后调用了原生 Object.defineProperty 方法,其中传入的第三个参数是属性描述符sharedPropertyDefinition
初始化为


const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

随后根据 Object.defineProperty 前面的代码可以看到 sharedPropertyDefinition 的 get/set 方法在经过 userDef 和 shouldCache 等多重判断后被重写,当非服务端渲染时,sharedPropertyDefinition 的 get 函数也就是 createComputedGetter(key) 的结果,我们找到 createComputedGetter 函数调用结果并最终改写 sharedPropertyDefinition 大致呈现如下:

sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
            watcher.depend()
            return watcher.evaluate()
        }
    },
    set: userDef.set || noop
}

当计算属性被调用时便会执行 get 访问函数,从而关联上观察者对象 watcher 然后执行 watcher.depend() 收集依赖和 watcher.evaluate() 计算求值。

总结一下流程
1.当组件初始化的时候,computed 和 data 会分别建立各自的响应系统,Observer遍历 data 中每个属性,使用Object.defineProperty的get/set进行数据劫持。
2.初始化 computed 会调用 initComputed 函数
2.1.注册一个 watcher 实例,并在内实例化一个 Dep 消息订阅器用作后续收集依赖(比如渲染函数的 watcher 或者其他观察该计算属性变化的 watcher )
2.2使用计算属性时会触发其Object.defineProperty的get访问器函数
2.3.调用 watcher.depend() 方法向自身的消息订阅器 dep 的 subs 中添加其他属性的 watcher
2.4.调用 watcher 的 evaluate 方法(进而调用 watcher 的 get 方法)让自身成为其他 watcher 的消息订阅器的订阅者,首先将 watcher 赋给 Dep.target,然后执行 getter 求值函数,当访问求值函数里面的属性(比如来自 data、props 或其他 computed)时,会同样触发它们的 get 访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并返回求值函数结果。

3.当某个属性发生变化,触发 set 拦截函数,然后调用自身消息订阅器 dep 的 notify 方法,遍历当前 dep 中保存着所有订阅者 wathcer 的 subs 数组,并逐个调用 watcher 的 update 方法,完成响应更新。

Logo

前往低代码交流专区

更多推荐