Vue源码学习1.2:new Vue发生了什么

·  阅读 330

建议PC端观看,移动端代码高亮错乱

new Vue发生了什么

我们来一步步找到Vue的核心定义,以web-full-esm为例

// scripts/config.js
const builds = {
  // ...
  
  // Runtime+compiler ES modules build (for bundlers)
  'web-full-esm': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.js'),
    format: 'es',
    alias: { he: './entity-decoder' },
    banner
  },
}复制代码
  • src/platforms/web/entry-runtime-with-compiler.js(入口)
  • —> src/platforms/web/runtime/index.js
  • —> src/core/index.js
  • —> src/core/instance/index.js (核心)

Vue 构造函数定义就在src/core/instance/index.js

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

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue复制代码
  • initMixin(Vue):就做了一件事,定义_init方法。
  • stateMixin(Vue):定义数据相关的方法$set,$delete,$watch方法。
  • eventsMixin(Vue):定义事件相关的方法$on$once$off$emit
  • lifecycleMixin(Vue):定义_update,及生命周期相关的$forceUpdate$destroy
  • renderMixin(Vue):定义$nextTick_renderrender函数转为vnode

可以看到 new Vue 主要就是调用了 _init 方法

_init方法

this._init方法在执行 initMixin 时绑定的,在src/core/instance/init.js中:

export function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    const vm = this
    // uid
    vm._uid = uid++

    // a flag to avoid this being observed
    vm._isVue = true

    if (options && options._isComponent) {
      // 优化内部组件实例化,因为动态选项合并非常慢,并且没有内部组件选项需要特殊处理。
      initInternalComponent(vm, options)
    } else {
      // 合并options
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }

    // expose real self
    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')

    // 挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}复制代码

Vue 初始化主要就干了几件事情:

  • mergeOptions 合并配置
  • initLifecycle 初始化生命周期
  • initEvents 初始化事件中心
  • initRender 初始化渲染,比如定义 vm._cvm.$createElement
  • initState 初始化 datapropscomputedwatcher 等等。

这些内容在后续的章节都会介绍到,这里只需要了解做了什么事情即可。

数据代理

不知道大家有没有好奇过,为什么我们的数据定义在data中,可是却可以通过 this.xxx 访问

export default {
  data() {
    return {
      name: 'l1shu'
    }
  },
  mounted() {
    console.log(this.name) // l1shu
  }
}
复制代码

其中的奥秘就在 _initinitState 中,src/core/instance/state.js

// src/core/instance/state.js
export function initState (vm) {
  // ...
  
  if (opts.data) {
    initData(vm) // 执行 initData 方法
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  
  // ...
}复制代码

$options 中读取 data,并执行 initData方法:

// src/core/instance/state.js
function initData (vm) {
  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') {
      // 判断是否和 methods 同名冲突
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    // 判断是否和 props 同名冲突
    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, true /* asRootData */)
}复制代码

首先判断 data 是否为一个函数,是的话就执行 getData,此函数定义如下:

export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  // https://github.com/vuejs/vue/issues/7573
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}复制代码

可以看到 getData 函数逻辑非常简单,关于 pushTargetpopTarget 在之后的响应式章节会介绍。

回到 initData 函数:

  • 遍历 keys,判断定义的字段是否和 propsmethods同名冲突,冲突的话则在开发模式下抛出警告
  • isReserved 判断是否为保留字段,不能以 _ 或者 $ 开头
  • proxy 实现数据代理 (核心)
  • observe实现响应式处理,这个我们以后再说

最后看下 proxy 函数是怎么实现数据代理的:

// src/core/instance/state.js
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)
}复制代码

使用 Object.definePropertyvm 添加指定的 key,通过 setget 方法修改或读取 this._data

看到这里我们理解了,当我们调用this.message时候,实际上我们调用了this._data.message

总结

Vue 的初始化逻辑写的非常清楚,把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然

由于我们这一章的目标是弄清楚模板数据如何渲染成最终的 DOM,所以各种初始化逻辑我们先不看。

在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM,那么接下来我们来分析 Vue 的挂载过程。

分类:
前端
标签: