浅羲Vue源码-10-响应式数据-initState(4)

488 阅读5分钟

「这是我参与2022首次更文挑战的第11天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

在前面的几篇文字 initState(1)-(3) 中,我们详细讨论了 initProps 的过程,现在我们继续讨 initState 中其余的逻辑,在此之前先回顾 initState 整体逻辑:

  1. initProps 初始化 vm._props,将 propsOptions 中的数据变为响应式的
  2. initMethods 初始 methods
  3. initData 初始化 data
  4. initComputed 初始化 computed
  5. initWatcher 初始化 watch

值得一提的时候,在上面这一系列的初始化中,props、methods、data、computed 的优先级是按这个列出顺序由高到底排列的,这是因为这些都要代理到 vm 上(或直接添加到 vm),属性不能有重复。这一点很好立即,大家都是通过 this.xx 访问这四项的。

二、initMethods

2.1 方法位置:

src/core/instance/state.js -> function initMethods

2.2 方法作用:

处理 vm.$options.methods ,即我们创建组件时传递的 methods 对象,此外校验其中的 key 不能和 props 重复,key 对应的值必须为函数类型,最后将 methods 中的方法都复制到 vm 上,这个不是代理,是真实复制;

另外,这些方法的 this 被绑定为 vm,这就保证了你可以肆意在 methods 的方法中使用 this 访问 vm 上的所有。

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (typeof methods[key] !== 'function') {
         // 校验类型必须为函数
      }
      if (props && hasOwn(props, key)) {
        // 属性不能和 props 中的 key 重复
      }
      if ((key in vm) && isReserved(key)) {
        // 不能是 vue 保留的方法名
      }
    }
    // 复制到 vm 上
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

三、initData

3.1 方法位置:

src/core/instance/state.js -> function initData

3.2 方法作用:

  1. 判断 data 如果是个工厂函数,调用工厂函数获取 data 对象
  2. 判断 data 中的属性不能和 propsmethods 中的属性重名
  3. 调用 proxy() 方法将 data 中的属性都代理到 vm 上,便于 this.xx 获取 data
  4. 调用 observe() 方法将 data 中的数据变为响应式,核心还是 defineReactive
function initData (vm: Component) {
  let data = vm.$options.data

  // 得到 data 对象
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    // ....
  }

  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)) {
        // key 和 methods 重名了
      }
    }
    if (props && hasOwn(props, key)) {
      // 和 props 重名了
    } else if (!isReserved(key)) {
      // 代理属性到 vm 上
      proxy(vm, `_data`, key)
    }
  }
  
  // 调用 observe 为 data 对象上的数据设置响应式,
  observe(data, true /* asRootData */)
}

四、initComputed

方法位置:src/core/instance/state.js -> function initComputed

方法作用:遍历 vm.$options.computed 选项,为每一个 key 创建一个 Watcher 实例,默认是 lazy 执行,然后 调用 defineComputed() 方法将每个 key 代理到 vm 上,最后校验 computed 中的 key 不能和 data、props、methods 中的 key 重复;

值得一提的是,computed 本身使用 watcher 实现的,所以大家在使用上也会感受到计算属性 computed 和自定义的监听器很相像,宏观的区别如缓存和异步等这里不再赘述,后面随着源码的深入会仔细探讨这些的。

function initComputed (vm: Component, computed: Object) {
  // 注意这个 vm._computedWatchers 对象,后面的 createComputedGetter 方法中要用到了
  const watchers = vm._computedWatchers = Object.create(null)
 
  const isSSR = isServerRendering()

  // 遍历 computed 对象
  for (const key in computed) {
    // 获取 key 对应的值,即 getter 函数
    const userDef = computed[key]
    
    // 判断 getter 必须是个函数,这个 getter 就是定义 computed 时的函数
    const getter = typeof userDef === 'function' ? userDef : userDef.get
     // ....
    if (!isSSR) {
      // 为每个 computed key 创建 watcher 实例,并且添加到 watchers 中,每个 key 一个
      // watchers 即 vm._computedWatchers 对象
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions // 配置项,lazy 为 true
      )
    }

    if (!(key in vm)) {
      // 代理 computed 对象中的属性到 vm 上
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      // 非生产环境判重,computed 的 key 不能和 data、props 中的 key 相同,警告信息输出已省略
      if (key in vm.$data) {
      } else if (vm.$options.props && key in vm.$options.props) {
      } else if (vm.$options.methods && key in vm.$options.methods) {
      }
    }
  }
}

4.1 Watcher 类

类的位置:src/core/observer/watcher.js 类的作用:创建 Watcher 实例的时候将会解析传入的表达式并收集表达式中的依赖,当表达式的值发生变化时触发传入的回调。

有个概念叫做渲染 watcher,这是一种特殊的 watcher,后面到渲染阶段的时候就会遇到,他是用于渲染组件模板用的 watcher,当模板绑定的数据发生变化时将会重新渲染模板。

前面有提到过,当数据发生变化时,就会触发一个响应式数据的 setter,而这些 setter 早就做好了依赖收集,这些 watcher 依赖这个数据早就被 dep 保存起来了,接着 setter 在更新值之后就会触发 dep.notify(),在 dep.notify() 中就会逐个调用 watcher.update 方法来更新,这个 watcher.update 就是 Watcher.prototype.update 方法。

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()
  }
  // .... 原型方法暂时省略去
  
}

4.2 Watcher 构造函数

4.2.1 Watcher 构造函数接收参数:

  • vmVue 实例
  • expOrFn:表达式或者函数,创建 computed 时接收到的是 computed 定义函数或一个空函数
  • cb:回调,初始化 computed 时接收到的是个空函数
  • optionswatcher 的配置,初始化 computed 时接收到的是 { lazy: true }
  • isRenderWatcher:是否为渲染 watcher,初始 computed 时没传入,所以接收到的 undefined,即 false
watchers[key] = new Watcher(
  vm,
  getter || noop,
  noop,
  computedWatcherOptions // 配置项,lazy 为 true
)

4.2.2 构造函数的工作

这段描述中出现在 Watcher 构造函数中的 this 均为 Watcher 的实例

  1. 如果 isRenderWatchertrue,则将当前渲染 watcher 赋值给 vm._watcher 属性
  2. 根据接收到 options 参数初始化 this.deep/this.user/this.lazy/this.sync/this.before 属性,初始化 computed 创建的 watcher 传入的 options{ lazy: true },所以此时 this.lazy = true,其余的都是 false
  3. cb 参数赋值给 this.cb,另外 this.dirty 赋值为 this.lazy,这个是个有用的属性,这个东西是 computed 计算属性有缓存而 watch 监听器没有缓存的重点
  4. 如果 expOrFn 是个函数类型,例如初始化 computed 时,expOrFn 传的就是函数,将 this.getter 赋值为 expOrFn;如果不是函数就要调用 parsePath(expOrFn)解析成函数;
  5. this.value 赋值,如果 this.lazytrue,就赋值 undefined,否则调用 this.get() 方法;

4.3 defineComputed

computed 的每个 key 代理到 vm,这个方法也涉及了不少细节,下个主题再具体展开

4.4 判重处理

如果非生产环境下还要校验 computedkey 不能和 props、methods、datakey 重复复,如果重复则输出警告信息;

五、总结

本篇小作文着重介绍了 initStateinitPorps 之后的 initMethodsinitData、和initComputed

其中 initMethods 就是将 methods 中的方法复制到 vm 上,并且绑定方法的 thisvm

initData 方法调用 observe 方法将 data 中的数据及其子代嵌套的数据都变为响应式;

initComputed 方法是初始化 computed 中的计算属性,所谓计算属性本质是一种 Watcher 实例,只不过是 lazyWatcher,这个 lazy 体现在不直接求值,而是等调用 Watcher.prototye.evaluate 方法时才求值。

另外这里出场了 Vue 中的重磅角色 Watcher 类,他致力于解析收到的 expOrFn(表达式或函数),收集其中的依赖,expOrFn 发生变化时调用其传入的 cb 执行更新逻辑。这个在后面讲到渲染 watcher 时就会明确很多,渲染 watchercbupdateComponent 方法,这个方法会重新计算虚拟 DOM,这个过程中就会重新获取 vm 上的数据,此时得到的就是新的数据,进而得到新的虚拟 DOM

下一篇将会详述 initComputed 未尽的细节方法,与之并行的就是 Watcher` 的原型方法讨论。