优秀的编程知识分享平台

网站首页 > 技术文章 正文

Vue 源码浅析:初始化 init 过程(vue-cli初始化项目)

nanyue 2024-09-06 20:22:34 技术文章 7 ℃


前言

Vue 源码浅析,分三块大内容:初始化、数据动态响应、模板渲染。

这系列算不上逐行解析,示例的代码可能只占源码一小部分,但相信根据二八法则,搞清这些内容或许可以撑起 80% 对源码的理解程度。

我借此机会,在玩 Vue3.0 之前开始最后一段 Vue2 的学习收尾,同时把这些学习总结分享给各位。

离网上那些 Vue 深入浅析的文章还有很多差距,如有不对之处,请各位指正。

最后,因为头条排版原因限制可能部分格式不太友好,大家可以点击尾部的“点击原文”查看。

从 Vue 构造函数开始

我们写 Vue 代码时,都是通过新建 Vue 实例开始:

var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
});

那么就先找到 Vue 的函数定义:

// source-code\vue\src\core\instance\index.js
function Vue (options) {
  //...
  this._init(options)
}
initMixin(Vue)
//...

这个对象的引用 this._init 就是之后通过 initMixin 方法中声明好的 Vue.prototype._init 原型方法。

// source-code\vue\src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    //...
  }
}

那么我们接下来的一切都是以此 _init 为起点展开。

处理 options

跳过一些目前不涉及的逻辑,我们看下对象引用 vm.$options 到底是怎么取得的:

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  //...
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}

内部通过调用 mergeOptions 方法得到最终的 vm.$options。

接下来细看 mergeOptions 方法:

// source-code\vue\src\core\util\options.js
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  //...
  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }

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

标准化部分属性 options

涉及 vue 中的:props、inject、directives:

normalizeProps(child, vm)
normalizeInject(child, vm)
normalizeDirectives(child)

为什么需要标准化呢?

就是为了给我们提供多种编写代码的方式,最后按照规定的数据结构标准化。

下面分别贴出对应上述三者的相关代码,相信很容易知道它们在做什么:

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 (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } 
}

比如,props: ['name', 'nick-name'] 会被转成如下形式:


同样,inject 和 directives 也会对用户的简写方式做标准化处理,这里不做过多描述。

比如:为 inject 中的字段属性添加 from 字段;为 directives 中的函数定义,初始化 bind 和 update 方法。

递归合并 mergeOptions

会根据当前实例的 extends、mixins 和 parent 中的属性做合并操作:

if (!child._base) {
  if (child.extends) {
    parent = mergeOptions(parent, child.extends, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
}

当然此处的 mergeOptions 还是递归调用本方法。所以此方法不是重点,核心在后面的方法:mergeFields


合并字段 mergeField

逻辑非常明显,遍历 parent 上的属性 key,然后根据某种策略,将该属性 key 挂到 options 对象上;之后,遍历 child (本 Vue 对象)属性 key,只要不是 parent 的属性,也一并加到 options 上:

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


策略 strat

上面提到了某种策略,其实就是特定写了几种父子合并取值优先级的判断。

显示最基本的 defaultStrat 默认策略:

const defaultStrat = function (parentVal: any, childVal: any): any {
  return childVal === undefined
    ? parentVal
    : childVal
}

child 属性不存在,则直接使用 parent 属性。

剩下就是根据特殊属性,来定义的策略:

  • strats.el , strats.propsData //defaultStrat
  • strats.data //mergeDataOrFn
  • strats[hook] //concat
  • strats[ASSET_TYPES+'s'] (components,directives,filters) //extend
  • strats.watch //concat
  • strats.props,strats.methods,strats.inject,strats.computed //extend
  • strats.provide //mergeDataOrFn

涉及很多我们常用的 vue 属性,但抛去一些特殊的策略,某些策略还是有共性的,比如都调用了:mergeDataOrFn。

能看到 mergeDataOrFn 中核心代码,主要根据 call 来执行对应的属性值 function:

export function mergeDataOrFn (
  parentVal: any,
  childVal: any,
  vm?: Component
): ?Function {
  if (!vm) {
    //...
  } else {
    return function mergedInstanceDataFn () {
      // instance merge
      const instanceData = typeof childVal === 'function'
        ? childVal.call(vm, vm)
        : childVal
      const defaultData = typeof parentVal === 'function'
        ? parentVal.call(vm, vm)
        : parentVal
      if (instanceData) {
        return mergeData(instanceData, defaultData) //(child,parent)
      } else {
        return defaultData
      }
    }
  }
}

最后将结果,扔给 mergeData 方法:

function mergeData (to: Object, from: ?Object): Object {
  if (!from) return to
  let key, toVal, fromVal

  const keys = hasSymbol
    ? Reflect.ownKeys(from)
    : Object.keys(from)

  for (let i = 0; i < keys.length; i++) {
    key = keys[i]
    // in case the object is already observed...
    if (key === '__ob__') continue
    toVal = to[key]
    fromVal = from[key]
    if (!hasOwn(to, key)) { //特别说明 hasOwn 是根据 hasOwnProperty 做的;忽略 prototype 属性
      set(to, key, fromVal)
    } else if (
      toVal !== fromVal &&
      isPlainObject(toVal) &&
      isPlainObject(fromVal)
    ) {
      mergeData(toVal, fromVal)
    }
  }
  return to
}

如果 child 中没有 key 属性,则将 parent 属性赋值给它。

当如果 child or parent 是一个对象时,则会继续递归 mergeData ,直至全部处理完。

特别说明 hasOwn 方法是根据 hasOwnProperty 做的,会忽略 prototype 属性,所以在 set 方法中会有特别的处理:

export function set (target: Array<any> | Object, key: any, val: any): any {
  
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  const ob = (target: any).__ob__
  
  if (!ob) {
    target[key] = val
    return val
  }
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}

这是生命周期对应的策略,会遍历所有的生命周期方法,并把父子的周期方法做 concat 操作:

function mergeHook (
  parentVal: ?Array<Function>,
  childVal: ?Function | ?Array<Function>
): ?Array<Function> {
  const res = childVal
    ? parentVal
      ? parentVal.concat(childVal)
      : Array.isArray(childVal)
        ? childVal
        : [childVal]
    : parentVal
  return res
    ? dedupeHooks(res)
    : res
}

对于 ASSET_TYPES 类型以及 props、methods、inject、computed 策略,则会做 extend 继承操作。

export function extend (to: Object, _from: ?Object): Object {
  for (const key in _from) {
    to[key] = _from[key]
  }
  return to
}

最后 watch 会稍微复杂写,直接看代码:

strats.watch = function (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): ?Object {
  //...
  const ret = {}
  extend(ret, parentVal)
  for (const key in childVal) {
    let parent = ret[key]
    const child = childVal[key]
    if (parent && !Array.isArray(parent)) {
      parent = [parent]
    }
    ret[key] = parent
      ? parent.concat(child)
      : Array.isArray(child) ? child : [child]
  }
  return ret
}

同时保留 parent、child 的 watch 属性,毕竟他们都要工作。通过 concat 和生命周期处理方式一样,都保存起来。

proxy 代理

目前我们这里不是 Vue 3.0 ,还未涉及新增加的 proxy 新特性,但在目前的版本中,已经有相关 proxy 的功能,不过对于非 production 环境没做强制要求,在开发环境中也只是做些 warn 之类的功能。

if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}

阮一峰老师的 es6 文章已经对 proxy 做了很细致的说明,所以这里不再对 has、get 之类的功能做补充,只是贴出相关代码:

const hasHandler = {
  has (target, key) {
    const has = key in target
    const isAllowed = allowedGlobals(key) ||
          (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
    if (!has && !isAllowed) {
      if (key in target.$data) warnReservedPrefix(target, key)
      else warnNonPresent(target, key)
    }
    return has || !isAllowed
  }
}

const getHandler = {
  get (target, key) {
    if (typeof key === 'string' && !(key in target)) {
      if (key in target.$data) warnReservedPrefix(target, key)
      else warnNonPresent(target, key)
    }
    return target[key]
  }
}

initProxy = function initProxy (vm) {
  if (hasProxy) {
    // determine which proxy handler to use
    const options = vm.$options
    const handlers = options.render && options.render._withStripped
    ? getHandler
    : hasHandler
    vm._renderProxy = new Proxy(vm, handlers)
  } else {
    vm._renderProxy = vm
  }
}

注意,如果浏览器不支持 proxy 特性,最后将执行到:vm._renderProxy = vm


一些初始化工作

接下来对生命周期、事件、渲染函数做些初始化工作,不是太重要,这里简单示意下:

initLifecycle(vm)
initEvents(vm)
initRender(vm)

调用生命周期方法 callHook

之后,我们将见到第一次调用生命周期 beforeCreate 方法;当然初始化数据响应状态的流程后,还会调用 created 方法。

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

我们看下 callHook 怎么工作:

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}
export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    //...
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

能看到 call 对应的生命周期名字后,就会通过 invokeWithErrorHandling 方法内的来执行对应的生命周期方法,并且通过 try/catch 来捕获出现的错误。

不过重要的是,还是要知道不同生命周期方法在整个 Vue 运行过程中的切入点(这里先贴出此处两个方法):


解析依赖和注入

正如官网所述:

provide 和 inject 主要在开发高阶插件/组件库时使用。并不推荐用于普通应用程序代码中。

可能我们平时的开发代码很少用到,现在看下初始化 init 中,他们是如何被初始化的。


声明 provide

虽然 inject 先于 provide 初始化,但必须现有 provide 这个蛋(父类提供),看下做了什么:

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

定义了 vm._provided 属性,它将在后面交给对应注入的 inject 属性 key 运作。


注入 injection

先看下入口方法 initInjections

export function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  //...
}
export 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 对象上的内容(在 options 合并时,已经做了参数的标准化。比如,具备了 from 参数),把对象上的属性名 key,在 vm._provided 对象上寻找,父类是否有提供过依赖。

如果找到,变将赋值给当前 inject 属性 key 对应的 value,当然没有找到,则会执行 inject 定义的 default:

result[key] = source._provided[provideKey]

不过还没完,因为我们知道 inject 注入的依赖,可以在 vue 中不同的地方使用(比如,官网示例提到的生命周期方法,和 data 属性),并且赋予了数据响应能力,就是执行了如下方法(具体分析后续章节展开):

export function initInjections (vm: Component) {
  //...
  defineReactive(vm, key, result[key])
  //...
}

初始化状态 state

备注下,initState 方法先于 initProvide,这里文章排版,放在此处说明:

initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props

initState 方法内涉及了 vue 中,我们常用的属性:prop、methods、data、computed、watch,这些都是具备数据动态响应的能力,所以解释起来会比较复杂,下篇继续:

// source-code\vue\src\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)
  }
}
最近发表
标签列表