vue2源码学习 (4).响应式原理-2.`new Vue()` 到 `observe`

93 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 8 天,点击查看活动详情

start

Vue.js 对我们的 data 到底做了哪些处理?

现在开始探索。

我先列一下初始化 data 经过了那几个步骤。

  1. new Vue()

  2. this._init()

  3. this.initState(vm)

  4. this.initData

  5. observe()

简述一下初始化 data 的逻辑

1. new Vue()

new Vue({
  el: '#app',
  data() {
    return { a: { name: '你好' } }
  },
})

上面是我们使用的场景,new Vue() 相当于执行 Vue 这个函数。

src\core\instance\index.js

function Vue(options) {
  if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

执行 Vue 这个函数,会执行 this._init(options) ,并且传入对应的参数options_init 来自于 Vue 的原型。

src\core\instance\init.js

// initMixin 会在 `/src/core/instance/index.js`中执行 (传入的是 Vue构造函数)
export function initMixin(Vue: Class<Component>) {
  // Vue 原型上添加 _init方法
  // 如果是 _开头,则可以理解为是提供给内部使用的内部属性。如果是 $开头是提供给用户使用的外部属性。
  Vue.prototype._init = function (options?: Object) {
    // 1. 存储当前的this,到变量 vm 上
    const vm: Component = this

    // a uid
    // 2. 实例的一个唯一标识
    vm._uid = uid++

    // 性能检测
    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    // 避免被观察到的标志 (Vue实例,不被转换为响应式)
    vm._isVue = true

    // merge options
    // 3. 主要操作就是合并配置options 到  vm.$options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }

    // 4.开始初始化 例如 生命周期,事件,Render state....
    // expose real self
    vm._self = vm

    // initLifecycle函数,向实例中挂载属性。
    initLifecycle(vm)

    // initEvents  主要做了:  1.定义属性_events;  2.初始化了父组件注册了的子组件
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')

    // initInjections 主要做了:初始化inject, 本质上是,匹配 子组件到上层组件的的_provided 和 inject是否有同名属性。
    initInjections(vm) // resolve injections before data/props

    // initState 主要做了: 依次初始化: props methods data computed watch
    initState(vm)

    // initProvide 主要做了:初始化 provide
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    // 如果元素存在,就开始挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

结合上述的代码和相关的注释。可以了解到 _init主要做的操作:

  1. 合并传入的配置 option;
  2. 初始化属性。

在初始化属性中,先看和 data 有关的 initState(vm);,这里的 vm 存储的是 Vue 实例。

src\core\instance\state.js

// 初始化状态
export function initState(vm: Component) {
  // 在实例上定义了一个_watchers, 这里的 _watchers 后续会存储这个组件的所有 watcher实例
  vm._watchers = []

  const opts = vm.$options
  // 根据实例的配置 ($options)依次初始化  props methods data computed watch
  // 这里的顺序很重要,也就解释了为什么 watch中为什么可以监听computed
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    // 这里如果默认没有传入 data. 返回一个响应式的空对象
    observe((vm._data = {}), true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)

  // nativeWatch是因为 火狐浏览器的object.prototype上有一个属性watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

由上方相关代码可以得知,initState 实现的功能是按顺序依次对 props methods data computed watch 做初始化。

  • 这里可以解释为什么 watch 中可以使用 computed

仔细看看 data 相关的方法

// 如果传入的配置存在 data =》 initData.
if (opts.data) {
  initData(vm)
} else {
  // 这里如果默认没有传入 data. 返回一个响应式的空对象
  observe((vm._data = {}), true /* asRootData */)
}

对是否传入 data 做一个逻辑判断,如果没有传入 data 配置,默认赋值一个空对象给,实例的_data

如果传入了 data 配置, 调用 initData

// 初始化data
function initData(vm: Component) {
  // 拿到data
  let data = vm.$options.data

  // 判断传入的data是不是函数 是函数就 getData 处理一下,不是函数拿来直接使用,注意一下 他这里把传入的 data 也在 vm._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]
    // 是否和  methods props重 复
    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)) {
      // 和 props 类似,这里也做了代理 , 我们 this.xxxData 其实访问的还是 this._data.xxxData
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  // 监听 data
  observe(data, true /* asRootData */)
}

// 如果传入的 data 是函数,执行该函数,执行后的返回值。
export function getData(data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  // 在调用数据getter时禁用dep收集
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

// 代理 `vm.xxxProps 或者 this.xxxProps` 实际上访问的是=> `vm._props.xxxProps`
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)
}

/**
 * Check if a string starts with $ or _
 * 检查字符串是否以$或_开头
 */
export function isReserved(str: string): boolean {
  // 取第一项,然后判断是否是 $ or _
  // charCodeAt() 方法可返回指定位置的字符的 Unicode 编码。
  const c = (str + '').charCodeAt(0)
  // 0x24 =>36 =>$   0x5f =>95 =>_
  return c === 0x24 || c === 0x5f
}

initData主要操作:

  1. 处理我们传入的 data,如果是函数,执行改函数,返回对应过得返回值;

  2. 处理好的数据赋值给vm._data

    这也就解释了为什么,实例上会有一个 _data 属性,而且this.a===this._data.a

  3. 遍历 data,看属性名是否和 method props 重复;

  4. 检查 data 中的属性名是否是 $_ 开头;

  5. 使用 proxy 函数代理我们的数据,(作用,方便我们直接通过 this 使用, this.xxxData 访问的本质还是 this._data.xxxData);

  6. 最后最重要的逻辑,使用 observe 处理我们的 data observe(data, true /* asRootData */);

end

本文主要讲述了, 处理 data 的前置逻辑。

  1. 处理 data。(是函数就执行该函数,返回对应的返回值)
  2. 校验了 data 是不是对象;
  3. 校验了是否和 methods, props 是否重名;
  4. 校验了是否是 $_ 开头;
  5. 使用 proxy 函数代理 _data 的数据;
  6. 核心的响应式逻辑,都在 observe

思考

  1. _data中的属性为什么和 data 中的同名属性完全相等?

因为两者本来就是一样的,直接通过 this 使用, this.xxxData 访问的本质还是 this._data.xxxData

  1. 为什么 Vue 组件中定义 data 推荐使用函数返回值的形式?
// 重点的几行代码

let data = vm.$options.data
data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}

// 如果 data 不是函数,直接赋值 `vm.$options.data`
// 如果这个组件被多次使用,此时多次使用的组件的 data 存储的引用地址,指向的是同一个。(会导致组件的data之间相互影响。)
  1. 本章节提到的 proxy 方法
  • 注意需要和 ES6 的 Proxy 区分。
  • 主要作用是:代理 vm.xxxProps 或者 this.xxxProps 实际上访问的是=> vm._props.xxxProps
  • 除了 data, props 也使用 proxy 方法做了代理。
  • 这就解释了实例上的 _data _props属性的作用。

end

  • 本节内容主要是梳理了,从 new Vue(), 到真正处理 data 的 initData 方法
  • 处理 data 最终都会通过observe方法。