vue2-实例化过程

674 阅读6分钟

1. 入口

由于我本地调试代码是通过 npm run dev 构建的,所以可以在 package.json 找到真实运行的命令

"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev"

通过我们之前开篇分析,我们可以透过 TARGET:web-full-devscripts/config.js 中找到编译入口

 // Runtime+compiler development build (Browser)
'web-full-dev': {
  entry: resolve('web/entry-runtime-with-compiler.js'),
  dest: resolve('dist/vue.js'),
  format: 'umd',
  env: 'development',
  alias: { he: './entity-decoder' },
  banner
}

我们就此来到 web/entry-runtime-with-compiler.js 文件,在最后一行我们找到

export default Vue

这个 Vue 便是我们在 Vue 中最终输出的构造函数了,通过溯源我们可以找到它是哪里来的

entry-runtime-with-compiler.js -> web/runtime/index -> core/index -> core/instance/index

终于找到你,还好没放弃

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

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

在次我们可以看到 Vue 的真实面目,是个构造函数,但是这个函数有点简单,就一句代码初始化 this._init(options),我们甚至没看到 _init 方法是在哪里定义的。

这样做的好处是将构造函数的逻辑拆分成不同的部分,按照逻辑解耦分离,不同的 Mixin 函数为 Vue 提供不同的功能方法,其中 _init 方法就是在 initMixin 定义的。

2. 初始化函数

我们接着看看 _init 函数做了些什么,代码比较多,我就直接在其上面写注释了,我们主要关注主流程即可。

let uid = 0

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

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

    // merge options
    if (options && options._isComponent) {
      // 组件实例 暂时跳过
      initInternalComponent(vm, options)
    } else {
      // resolveConstructorOptions 是从构造函数的父类继承方法
      // 这边一般返回 vm.constructor.options {components: {}, directives: {}, filters: {}}
      // 其中vm.constructor.options => Vue.options 的定义是在 core/index.js 中的 initGlobalAPI(Vue)
      // initGlobalAPI 详见2.1
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }

    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      // proxy代理vm,主要用于提示错误和键名规范,如果 key is not defined on the instance but
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化生命周期及相关数据 如vm._isMounted vm._isDestroyed vm._inactive vm.$parent vm.$children 等
    // 详见2.2
    initLifecycle(vm)
    // 初始化事件 注册在组件中定义的事件如 <hello @confirm="xx" />
    // 详见2.3
    initEvents(vm)
    // 初始化渲染函数 暂不分析
    initRender(vm)
    // 调用组件的beforeCreate钩子
    callHook(vm, 'beforeCreate')
    // inject 暂时跳过
    initInjections(vm) // resolve injections before data/props
    // 初始化数据 详见2.4
    initState(vm)
    // provide 暂时跳过
    initProvide(vm) // resolve provide after data/props
    // 调用组件created钩子 可以看出来 beforeCreate 和 created 的差别在于中间几个数据初始化函数 特别是 initSate
    callHook(vm, 'created')

    // 挂载节点
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

现在我们对构造函数中的唯一调用方法 this._init 方法进行了学习,基本可以知道实例化的大致逻辑流程了。因为函数中调用的方法已经高度封装,所以代码量虽少,但逻辑还是挺多的其实,下面我们将对上述流程中的部分方法进行学习,进一步搞清初始化逻辑。

2.1 initGlobalAPI

我们在前面的初始化函数中有提到 vm.contructor.options,但是我们没看到在何处有定义它的地方,实际上我们在前面找构造函数 Vue 的定义的时候,曾经溯源了几个文件 entry-runtime-with-compiler.js -> web/runtime/index -> core/index -> core/instance/index,各个文件不是单纯的去引入输出,而是在各自的范围内进行了一系列操作,其中在 core/index 就有这么一行代码

initGlobalAPI(Vue)

我们来看看 core/global-api/index.jsinitGlobalAPI 的具体逻辑

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  Object.defineProperty(Vue, 'config', configDef)

  // 定义了Vue的静态工具函数 但不属于开放API 我们尽量不去使用
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  // 原来set delete nextTick 在这边初始化
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }

  // 我们的主角options出场
  Vue.options = Object.create(null)
  // ASSET_TYPES的定义在 share/constants 中 ASSET_TYPES = ['component', 'directive', 'filter']
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // 不知 先跳过
  Vue.options._base = Vue

  // KeepAlive逻辑 先跳过
  extend(Vue.options.components, builtInComponents)

  // 一些常用的方法定义,通过解耦思想在函数中实现 use mixin extend 方法
  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  // 比较特殊 通过遍历ASSET_TYPES分别定义 Vue.directive Vue.component Vue.filter
  initAssetRegisters(Vue)
}

2.2 initLifecycle

我们再来看看在 initLifecycle 究竟是如何初始化生命周期的

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // 向上寻找真实父节点
  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  // 初始化实例属性$parent $root $children $refs 等
  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  // 生命周期定义相关的属性初始化
  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

实际上,可以看出来 initLifecycle 函数比我们想的简单一些,它就是定义了实例和父子节点相关的属性及生命周期描述相关属性。这其实是可以理解的,因为生命周期钩子函数我们在组件中定义,在初始化和渲染更新中触发,所以理论上调用钩子函数也应该在实际渲染的各个过程中才对,就如我们前面在 _init 函数中调用的 callHook(vm, 'created'),在 created 钩子调用中再去修改生命周期相关属性及调用函数。

2.3 initEvents

initEvents 函数本身比较简单,主要是为组件上定义的函数调用 updateComponentListeners 进行注册处理,关于 updateComponentListeners 我们就先不分析了,后面有机会再进行分析

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

2.4 initState

我们最后再来分析下 initState,这是个比较重要的函数,我们可以从中分析得出框架在 beforeCreatecreated 之间做了哪些数据处理

export function initState (vm: Component) {
  vm._watchers = []
  // 这段代码真是短小精悍 短短的几行代码 分别初始化了 props methods data computed 我们来分别看看
  const opts = vm.$options
  // 详见2.4.1
  if (opts.props) initProps(vm, opts.props)
  // 详见2.4.2
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    // 详见2.4.3
    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)
  }
}

2.4.1 initProps

initProps 到底是如何初始化的,我们来看看

function initProps (vm: Component, propsOptions: Object) {
  // propsOptions为组件Props定义形如{name: String}
  // propsData为组件接收到的数据形如{name: 'Joke'}
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}

  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent

  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false)
  }

  // 遍历prop定义
  for (const key in propsOptions) {
    keys.push(key)
    // validateProp用于获取prop的值(propsData数据或默认值)
    const value = validateProp(key, propsOptions, propsData, vm)

    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      // 这边能遇见我们的错误提示老朋友 不允许重新父组件传来的prop值
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      // 为props添加响应式 其中前面toggleObserving(false)的作用就在这边 
      // 当value为对象时不会进一步递归添加响应式
      defineReactive(props, key, value)
    }
   
    // vm代理_props值 也就是我们的props
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

2.4.2 initMethods

相比之下 initMethods 就简单很多了

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') {
        warn(
          `Method "${key}" has type "${typeof methods[key]}" in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    // 上面都是开发环境的校验和错误提示
    // 正式环境只有这句实际逻辑 将methods下的方法都复制给vm 并将this指定为vm
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

2.4.3 initData

我们继续看 initData 的逻辑

function initData (vm: Component) {
  let data = vm.$options.data
  // 如果是函数则将this指向vm并执行函数
  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--) {
    // 校验是否和methods和props键重复
    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)) {
      // vm代理_data
      proxy(vm, `_data`, key)
    }
  }

  // 响应式数据 后面文章单独分析
  // observe data
  observe(data, true /* asRootData */)
}

2.4.4 initComputed和initWatch

涉及watch,我们后面再讲

3. 结语

前面我们大致分析了 Vue 函数实例化的流程。

  • 通过 entry-runtime-with-compiler.js 找到入口,再通过溯源找到 Vue 函数的定义

  • _init 函数的流程

初始化生命周期数据 -> 初始化组件事件 -> 初始化渲染 -> 调用beforeCreate -> 初始化数据 -> 调用created -> 挂载节点

  1. Vue静态方法的初始化 initGlobalAPI 逻辑

  2. 实例化流程相关函数 initLifecycle initEvents initState 逻辑

  3. initStateprops methods data 的初始化逻辑

还有些流程没有分析的,我将在后面的文章继续

  1. initRender 初始化渲染

  2. initState 初始化数据中的 computed watch 初始化

  3. initData 中的 observe(data) 数据监测

最后啰嗦一句,贴的代码比较多。分析的有不对的地方希望帮忙指正,有不清楚的地方也可以提出来,大家一起交流~