Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue 源码分析(_init 函数) #12

Open
yangdui opened this issue May 16, 2020 · 0 comments
Open

Vue 源码分析(_init 函数) #12

yangdui opened this issue May 16, 2020 · 0 comments

Comments

@yangdui
Copy link
Owner

yangdui commented May 16, 2020

Vue 源码分析(_init 函数)

接下来的会以下面的例子展开

let vm = new Vue({
	el: '#app',
	data: {
		a: 1
	},
	created: function() {
		console.log('created');
	}
});

这里有一个约定:忽略源码中 process.env.NODE_ENV !== 'production' 部分

我们知道 Vue 构造函数是

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 构造函数的时候,实际就是在执行 this._init(options)。_init 函数是在 core/instance/init.js 文件中定义的

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

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    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
      )
    }
    /* istanbul ignore else */
    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')

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

选项合并

Vue 选项合并有两种情况,如果是组件就走 initInternalComponent 这个函数。我们这里不是组件,所以在mergeOptions 完成。

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

mergeOptions 有三个参数,第一个参数是 resolveConstructorOptions(vm.constructor) 函数返回值,在这里就是 Vue 构造函数的选项(以后在介绍 Vue.extend 时会详细分析这个函数,这里就简单理解为 Vue 构造函数的选项)。Vue 构造函数的选项有 components、directives、filters、_base

mergeOptions 在 src/core/util/options.js 文件

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

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

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  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)
  }
  return options
}

首先要搞清楚谁和谁合并,mergeOptions 的参数现实是 parent 和 child 合并。parent 就是构造函数(未必是 Vue 构造函数,以后会涉及,当然这里就是 Vue)的选项,child 就是我们传递给构造函数的参数,这里就是 new Vue(obj) 中的 obj。

如果 child 是函数,那么取 child 的 options 属性

if (typeof child === 'function') {
	child = child.options
}

接下来的三个函数

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

分别是规范 props、inject、directives 的。规范的意思:props、inject、directives 会有多种写法,我们最后需要统一成一种格式。比如说 props

props: ['title', 'name', 'age']
props: {
	title: String,
	name: {
		type: String,
		required: true
	}
	age: Number
}

最后都要规范为

props: {
	propA: {
		type: ...,
		...
	}
}

child 的 extends、mixins 选项合并会调用 mergeOptions ,我们稍后在介绍。

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

两个 for 循环遍历 parent,child 的 key,真正合并的是 mergeField 函数。合并分为几个策略

  • 默认策略

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

    如果 child 有选项,采用 child,否则就采用 parent。选项 el、propsData 采用默认策略

  • data 合并策略

    strats.data = function (
      parentVal: any,
      childVal: any,
      vm?: Component
    ): ?Function {
      if (!vm) {
        if (childVal && typeof childVal !== 'function') {
          process.env.NODE_ENV !== 'production' && warn(
            'The "data" option should be a function ' +
            'that returns a per-instance value in component ' +
            'definitions.',
            vm
          )
    
          return parentVal
        }
        return mergeDataOrFn(parentVal, childVal)
      }
    
      return mergeDataOrFn(parentVal, childVal, vm)
    }
    

    这里根据是否传入 vm 进行不同情况处理,没有 vm 是处理组件 data。组件的 data 是要函数形式的,如果不是直接返回 parent 的 data。最后都经过 mergeDataOrFn 函数处理,只不过组件不传入 vm。

    export function mergeDataOrFn (
      parentVal: any,
      childVal: any,
      vm?: Component
    ): ?Function {
      if (!vm) {
        // in a Vue.extend merge, both should be functions
        if (!childVal) {
          return parentVal
        }
        if (!parentVal) {
          return childVal
        }
        // when parentVal & childVal are both present,
        // we need to return a function that returns the
        // merged result of both functions... no need to
        // check if parentVal is a function here because
        // it has to be a function to pass previous merges.
        return function mergedDataFn () {
          return mergeData(
            typeof childVal === 'function' ? childVal.call(this, this) : childVal,
            typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
          )
        }
      } 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)
          } else {
            return defaultData
          }
        }
      }
    }
    

    一般情况下,合并 data 返回的都是函数。返回函数的原因是在初始化的时候可以使用 props 和 inject,因为这两个选项先于 data 初始化。

    data 合并,如果有相同的 key,child 会覆盖 parent。

  • 生命周期合并策略

    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
    }
    
    LIFECYCLE_HOOKS.forEach(hook => {
      strats[hook] = mergeHook
    })
    
    

    LIFECYCLE_HOOKS 就是生命周期数组,通过 mergeHook 函数合并后的结果就是,如果 parent 和 child 都存在相同的生命周期函数,都需要保留,并且 parent 先执行。比如

    let Parent = Vue.extend({
    	created: function() {
    		console.log('Parent created');
    	}
    });
    
    let child = new Parent({
    	el: '#app',
    	created: function() {
    		console.log('child created');
    	}
    });
    
    // Parent created
    // child created
    
  • assets 选项合并策略

    function mergeAssets (
      parentVal: ?Object,
      childVal: ?Object,
      vm?: Component,
      key: string
    ): Object {
      const res = Object.create(parentVal || null)
      if (childVal) {
        process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
        return extend(res, childVal)
      } else {
        return res
      }
    }
    

    assets 合并采用 create 创建的对象 res。首先令 res 关联到 parent 上

    const res = Object.create(parentVal || null)
    

    然后通过 extend 函数将 res 和 childVal 合并。

  • watch 合并策略

    strats.watch = function (
      parentVal: ?Object,
      childVal: ?Object,
      vm?: Component,
      key: string
    ): ?Object {
      // work around Firefox's Object.prototype.watch...
      if (parentVal === nativeWatch) parentVal = undefined
      if (childVal === nativeWatch) childVal = undefined
      /* istanbul ignore if */
      if (!childVal) return Object.create(parentVal || null)
      if (process.env.NODE_ENV !== 'production') {
        assertObjectType(key, childVal, vm)
      }
      if (!parentVal) return childVal
      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
    }
    

    watch 合并开始处理了浏览器兼容性问题,然后也是分别判断是否存在 parent,child。如果不存在 child,那么返回以 parent 为原型的对象,如果不存在 parent,直接返回 child。假如都存在,那么通过 for in 循环,将 watch 对象中的属性变为数组,如果 child 存在相同的属性,直接加入到相应的数组。

  • props、methods、inject、computed 合并策略

    strats.props =
    strats.methods =
    strats.inject =
    strats.computed = function (
      parentVal: ?Object,
      childVal: ?Object,
      vm?: Component,
      key: string
    ): ?Object {
      if (childVal && process.env.NODE_ENV !== 'production') {
        assertObjectType(key, childVal, vm)
      }
      if (!parentVal) return childVal
      const ret = Object.create(null)
      extend(ret, parentVal)
      if (childVal) extend(ret, childVal)
      return ret
    }
    

    如果没有 parent 直接返回 child,如果有 parent ,那么先创建一个原型为 null 的对象,然后分别将 parent 和 child (如果有)合并到 ret 上。

  • provide 合并策略

    strats.provide = mergeDataOrFn
    

    因为 data 也是采用 mergeDataOrFn 函数,所以 provide 和 data 合并策略是一样的

  • mixins 和 extends 合并

    开始的时候我们遗留了这两个

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

    其实也非常简单,如果是通过 extends、mixins 添加选项,最后会合并到 parent 上面,然后在和其他选项合并。比如说

初始化

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

接下来的这段和初始化相关的内容,没有什么特别的。

其中 callHook(vm, 'beforeCreate')callHook(vm, 'created') 调用 beforeCreatecreated 生命周期函数。其中 callHook 函数在 src/core/instance/lifecycle.js 文件

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

pushTarget()popTarget() 和数据响应有关以后会介绍。

首先取出相关的生命周期函数

const handlers = vm.$options[hook]

如果 handlers 存在,必定是数组,循环执行。

if (vm._hasHookEvent) {
	vm.$emit('hook:' + hook)
}

这一段是没有公开的用法,忽略。

实例挂载

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

最后这里是关于实例挂载的,遇见渲染的时候会说明。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant