Vue2源码阅读——Vue初始化的时候做了什么?

526 阅读2分钟

前提

  1. 当我们初始化一个Vue对象的时候, Vue的内部是如何运行的。 我们带着问题去探索;
  2. Vue的源码版本为: 2.6.14
  3. 贴出的所有源码都是 经过 删减后的简化代码

示例代码

使用官网的入门教程的例子。

<div id="app">
    {{ message }}
</div>
var app = new Vue({
    el: '#app',
    data: {
        message: 'Hello Vue!' 
    }
})

Vue的构造函数

源码定位: src/core/instance/index.js

function Vue (options) {
  this._init(options) // 初始化时 调用 _init原型方法
}

_init原型方法

源码定位: src/core/instance/init.js

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    vm._uid = uid++
    // a flag to avoid this being observed
    vm._isVue = true  
    // merge options
    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )
    vm._renderProxy = vm
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate') // 生命周期
    initInjections(vm)
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created')  // 生命周期

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

我们逐行进行分析:

const vm: Component = this // 存储当前的实例对象
vm._uid = uid++ // 当前对象的uid, vm._uid = 0
vm._isVue = true

合并选项

vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
)

resolveConstructorOptions

在本文中, 直接返回 构造函数的 options

mergeOptions 合并options

源码定位: src/util/options.js

function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
) {

  normalizeProps(child, vm) // 统一格式 props 
  normalizeInject(child, vm) // 统一格式 inject
  normalizeDirectives(child) // 统一格式 Directives
  
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  // 按照 合并策略进行合并 
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}
normalizeProps

根据文档 props 可以有几种格式:

  1. 字符串数组: ['a', 'b', 'c']
  2. 纯对象指定类型:
{
  a: Number,
  b: String,
  c: Boolean
}
  1. 带有验证格式
props: {
  // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
  propA: Number,
  // 多个可能的类型
  propB: [String, Number],
  // 必填的字符串
  propC: {
    type: String,
    required: true
  },
  // 带有默认值的数字
  propD: {
    type: Number,
    default: 100
  },
  // 带有默认值的对象
  propE: {
    type: Object,
    // 对象或数组默认值必须从一个工厂函数获取
    default: function () {
      return { message: 'hello' }
    }
  },
  // 自定义验证函数
  propF: {
    validator: function (value) {
      // 这个值必须匹配下列字符串中的一个
      return ['success', 'warning', 'danger'].indexOf(value) !== -1
    }
  }
}

该函数 就是将 props属性 ,变成统一格式

{
    props: {
        a: {
            type: xxxx
        }
    }
}
function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  }
  options.props = res
}

  1. 如果props是字符串数组, 那么遍历数组 将 数组转为 对 带有 type: any 的对象格式
// props: ['a', 'b', 'c']
props: {
    a: { type: null },
    b: { type: null },
    c: { type: null }
}
  1. 否则 ,如果是纯对象, 那么 则遍历 所有属性
  2. 如果 属性值是 纯对象, 那么则不做处理
// 不做处理
props: {
    a: {
         type: String,
        required: true
    }
}
  1. 否则 将属性值 转为 {type: 属性值}
props: {
    a: String
}
// ===>
props: {
    a: {
        type: String
    }
}
  1. 如果都不是 则报错
normalizeInject

统一Inject格式, Provide 和 Inject 一般情况下用不到, 故作了解

function normalizeInject (options: Object, vm: ?Component) {
  const inject = options.inject
  if (!inject) return
  const normalized = options.inject = {}
  if (Array.isArray(inject)) {
    for (let i = 0; i < inject.length; i++) {
      normalized[inject[i]] = { from: inject[i] }
    }
  } else if (isPlainObject(inject)) {
    for (const key in inject) {
      const val = inject[key]
      normalized[key] = isPlainObject(val)
        ? extend({ from: key }, val)
        : { from: val }
    }
  }
}
  1. 如果 inject 不存在 ,则直接返回
  2. 否则 如果是 字符串数组, 则 转为 {from: ''} 格式
 {
     inject: ['a']
 }
 // =====>
 {
     inject: {
         a: {
             from: a
         }
     }
 }
  1. 如果 是 纯对象, 则遍历对象
  2. 如果 属性值 是对象, 则 进行浅拷贝 extend({ from: key }, val)
  3. 否则 转为 {from: ''} 格式
normalizeDirectives

统一Directives格式

{
    directives: {
        "a": function() {}
    }
}
// =====> 
{
    directives: {
        "a": {
             bind: function() {},
             update: function() {}
        }
    }
}
function normalizeDirectives (options: Object) {
  const dirs = options.directives
  if (dirs) {
    for (const key in dirs) {
      const def = dirs[key]
      if (typeof def === 'function') {
        dirs[key] = { bind: def, update: def }
      }
    }
  }
}
  1. 如果 含有指令 并且 属性值为 函数的, 统一为: { bind: def, update: def }

至此, mergeOptions 分析完毕, 继续回到 _init 函数。

initLifecycle(vm)

顾名思义, 初始化生命周期, 但是看了源码,只是新增了一些属性

function initLifecycle (vm: Component) {
 const options = vm.$options
 let parent = options.parent
 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
}

initEvents(vm)

初始化 事件

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

initRender(vm)

初始化渲染

function initRender (vm: Component) {
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  
  const options = vm.$options // 获取 当前实例的 $option
  
  const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
const parentData = parentVnode && parentVnode.data

defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
}

resolveSlots()

处理 插槽

function resolveSlots (
  children: ?Array<VNode>,
  context: ?Component
): { [key: string]: Array<VNode> } {
  if (!children || !children.length) {
    return {}
  }
  const slots = {}
  for (let i = 0, l = children.length; i < l; i++) {
    const child = children[i]
    const data = child.data
    // remove slot attribute if the node is resolved as a Vue slot node
    if (data && data.attrs && data.attrs.slot) {
      delete data.attrs.slot
    }
    // named slots should only be respected if the vnode was rendered in the
    // same context.
    if ((child.context === context || child.fnContext === context) &&
      data && data.slot != null
    ) {
      const name = data.slot
      const slot = (slots[name] || (slots[name] = []))
      if (child.tag === 'template') {
        slot.push.apply(slot, child.children || [])
      } else {
        slot.push(child)
      }
    } else {
      (slots.default || (slots.default = [])).push(child)
    }
  }
  // ignore slots that contains only whitespace
  for (const name in slots) {
    if (slots[name].every(isWhitespace)) {
      delete slots[name]
    }
  }
  return slots
}

vm.$createElement

提供给 初始化的时候 render 属性

$attrs$listeners

高阶组件, 不做讲解

callHook(vm, 'beforeCreate')

调用 beforeCreate 钩子, 面试经常问到的, 读了上面的源码,可以看出, beforeCreate 之前 只是做了一些准备工作, 此时 你无法 拿到data里的数据 以及 $el, 无法拿到 挂载点, 不要在这里 对 节点 进行操作

initInjections(vm)

function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      defineReactive(vm, key, result[key])
    })
    toggleObserving(true)
  }
}

function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    // inject is :any because flow is not smart enough to figure out cached
    const result = Object.create(null)
    // 获取所有键名
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)

    // 遍历 键名
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      // #6574 in case the inject object is observed...
      if (key === '__ob__') continue
      const provideKey = inject[key].from 
      let source = vm
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break
        }
        source = source.$parent
      }
      if (!source) {
        if ('default' in inject[key]) {
          const provideDefault = inject[key].default
          result[key] = typeof provideDefault === 'function'
            ? provideDefault.call(vm)
            : provideDefault
        } else if (process.env.NODE_ENV !== 'production') {
          warn(`Injection "${key}" not found`, vm)
        }
      }
    }
    return result
  }
}
inject: ['foo'],
inject: {
    foo: {
        from: 'xxx',
        default: !function || function
    }
}
  1. 获取所有的inject键名,开始遍历
  2. 如果 已经是 响应式 则跳过
  3. 如果 祖先级别 provide 提供了 对应的键名的值, 则进行 赋值
  4. 否则 如果 含有 default 属性, 如果 是函数 则 调用 函数, 否则直接赋值
  5. 否则 报错
  6. defineReactive 最后 进行响应式 处理

initState(vm)

处理 props,methods, data, computed, watch

function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  // 初始化 props
  if (opts.props) initProps(vm, opts.props) 
  // 初始化 methods
  if (opts.methods) initMethods(vm, opts.methods)
  // 初始化 data
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化 computed
  if (opts.computed) initComputed(vm, opts.computed)
  // 初始化 watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initProps(vm, opts.props)


// 初始化props
function initProps (vm: Component, propsOptions: Object) {
  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)
  }
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    defineReactive(props, key, value)
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
 
 // 检验 prop
function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
): any {
  const prop = propOptions[key]
  const absent = !hasOwn(propsData, key)
  let value = propsData[key]
  // boolean casting
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false
    } else if (value === '' || value === hyphenate(key)) {
      // only cast empty string / same name to boolean if
      // boolean has higher priority
      const stringIndex = getTypeIndex(String, prop.type)
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        value = true
      }
    }
  }
  // check default value
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key)
    // since the default value is a fresh copy,
    // make sure to observe it.
    const prevShouldObserve = shouldObserve
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
  return value
}
  1. 验证 props 的 所有属性 的合法性
  2. 类型检查 是否含有 Boolean 类型, 如果是, 那么 是否有 default 属性, 没有的话 直接 赋值 false
  3. 如果是空字符串, 则 Boolean的优先级最高, 直接 为 true
  4. 如果 不含有 Boolean 类型, 并且 为 undefined, 则获取 default 默认值,getPropDefaultValue
  5. 如果没有default属性, 则返回 undefined
  6. 如果 是 function 并且 type 不含有 Function, 则调用 函数方法 7.否则 直接返回值
  7. 对返回的值 进行 观察 new Observer(value)
  8. prop 进行响应式

简单来说, 就是 对 props的 属性进行响应式

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 $.`
        )
      }
    }
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

这边还是比较好理解, 就是 对 methods属性进行遍历,

  1. 判断 是否为函数, 不是则 抛出异常
  2. 判断 是否在props中存在, 如果是, 则抛出异常
  3. 判断 是否是保留关键字, 如果是, 则抛出异常
  4. 否则 就绑定 上下文

initData

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

initMethods

  1. data 是否是 函数,(子组件必须是函数),如果是 则执行函数,获取返回值, 否则 直接赋值
  2. 如果 data 的值 不是 对象, 则依旧抛出异常
  3. 遍历 所有的 属性
  4. 判断 是否在 methodsprops 中 初始化过, 如果是 则抛出异常
  5. 否则 进行 响应式

initComputed

删除 服务端渲染

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      } else if (vm.$options.methods && key in vm.$options.methods) {
        warn(`The computed property "${key}" is already defined as a method.`, vm)
      }
    }
  }
}
  1. 创建一个空的 watchers对象
  2. 遍历computed的属性, 如果 属性值 是function,则 直接 赋值给 getter变量,否则 获取 属性值的get属性
  3. 如果 getter 不存在 则直接抛出异常
  4. 否则 对 computed 的属性,进行观察
  5. 判断 是否在 props,methods,data中初始化过, 如果是 则抛出异常

initWatch

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
  1. 遍历 watch, 如果是数组, 则进行遍历,createWatcher
  2. 否则 直接 createWatcher
  3. createWatcher中, 如果 参数handler 是一个 对象 则 获取 handler.handler属性
  4. 如果 handler 是一个字符串, 则从 当前实例中获取 方法, 其实就是 在 methods 中获取
  5. 调用 $watch 进行, 细节会在 后续文章

initProvide(vm)

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

相对简单, 初始化 provide, 如果是 函数, 则运行函数, 否则直接赋值 给 vm._provided, 共 子组件的 inject使用。 回到 initInject 中,我们看到 这样一行代码:

if (source._provided && hasOwn(source._provided, provideKey)) {
  result[key] = source._provided[provideKey]
  break
}

callHook(vm, 'created')

调用 created 钩子, 此时 此刻, 可以拿到 data, inject, props, computed, watch, methods 内的数据

最后 挂载

如果 选项中 有 el , 则调用 挂载, 否则需要 主动 调用 $mount 方法

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

挂载 和 更新 , 会有新的一篇文章 进行描述

总结

上面陈述的 只是一个流程, 关于 如何 依赖收集响应式挂载更新 这些 深层次的 源码, 会写在后续文章