迫于菜🐶 - Vue.js 源码(四)

432 阅读4分钟

往期章节

  1. 第一章-源码目录
  2. 第二章-项目构建
  3. 第三章-入口开始

前面三章中,我们跟随 Vue.js 技术揭秘 , 对 Vue.js 的源码部分又有了进一步的了解,今天一起来学习 Vue 的一大核心思想 —— 数据驱动,看看在 new Vue 时,到底发生了什么。

何为数据驱动

传统前端一般都是通过 Ajax 获取数据,然后操作 DOM 来改变 View(视图层),或者在处理交互的时候,也需要手动去操作更新 DOM ,这样就会让代码变得更加繁琐,待操作的 DOM 数量一多,状态难以维护,也更加容易出现失误。

而数据驱动的出现,让这一切都引刃而解。所谓数据驱动,是指我们不再需要关注 DOM ,不论是 DOM 初始化还是状态变更时的处理,整个流程围绕着如何操作数据就可以了。除此之外,还可以方便做优化,因为整个流程都是数据,加上配合 VDOM(虚拟dom)对底层的抽象,我们可以做类似于 diff patch 算法的优化,多了层抽象意味着有了很多优化空间。

至于与数据驱动紧密相关的 MVVM(双向数据绑定),可谓是 Vue.js 的另一大核心思想,这里只做简单介绍,后期会有单独章节进行分析。

MVVM 是 Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对 View 和 ViewModel 的双向数据绑定,这使得 ViewModel 的状态改变可以自动传递给 View ,即所谓的数据双向绑定。

new Vue

我们继续从入口代码开始分析,看看 new 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 实际上就是一个 Function 实现的 class ,并且只能通过 new 关键字来初始化。然后调用了 this._init 方法,而 init 方法则是在 initMixin 里定义的,我们找到 ./core/instance/init.js ,通过阅读源码可以看出,在这里其实是定义了很多初始化的方法。

比如定义了 uid

// ...
Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++
// ...

比如合并 options

可以将它理解为它会把传入的 options merge 到 vm.$options。 然后我们就可以通过 $options.el 或者 $options.data 来获取用户自定义的内容。

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

再比如

初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。

vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

最后,经过下面一系列的判断来看有没有传入 el ,此时的 el 还只是一个字符串,只有通过 vm.$mount 挂载,挂载的目标就是把模板渲染成最终的Dom。

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

data 初始化

我们平时撸码一把梭的时候,一般都是在 data 中定义变量 Xxx,然后在 mounted 或者 methods 里面直接用 this.Xxx 来获取它,可是为什么仅仅这样就能获取到呢? 我们来找一下原因。

回到上面那一堆初始化代码找到 initState(vm) ,它是定义在 ./core/instance/state.js 中。

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 */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

这段代码做了一系列的判断,如果定义了 propsmethods,那它就会调用 initPropsinitMethods 来初始化它们,如果定义了 data 呢?

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
    )
  }
  // ...
  }
  // observe data
  observe(data, true /* asRootData */)
}

它首先从 vm.$options.data 中拿到自定义的 data ,然后判断它是对象还是函数(一般都是函数),然后会调用 getData(data, vm) 并进行了 data.call(vm, 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)
 }
}

接下来它进行了一个循环比对,大致作用是如果在 data 中定义使用了 Xxx ,那么就不能在 props 或者 methods 里面也定义这个 Xxx ,因为它们最终都会通过 proxy 方法挂载到 vm 上,而传入的 _data 就是在定义 data 的地方同时定义了vm._data

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

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

再来看看 proxy 方法,它通过 sharedPropertyDefinition 定义了 get 和 set ,然后 Object.defineProperty 代理了 targetkey 的访问,target 就是传入的 vm ,而 sourceKey 则是 _data

所以当我们通过 this.Xxx 获取的时候,实际上是调用了 this._data.Xxx ,这才是真正我们能访问 data 的原因之所在。

总结

Vue 的初始化逻辑写的非常清楚,把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然。我们这一章学习到了 new Vue 的过程 以及 data 是如何初始化的,还是需要多多揣摩几遍。另外还是之前那句话,一定要自己 debugger 和 console 来跟踪一下代码运行过程,这样才会有更深的理解和印象。