Vue 源码解读通俗易懂(一)初始化阶段

介绍: 本篇文章我们将粗略的去了解,在 Vue 实例创建的过程中,分别执行了什么操作,为了照顾功底不深的小伙伴我们省去了部分方法实现的详细源码,有兴趣的可以自己去下载源码文档查看,或者关注我的后续文章


本节学习将让大家彻底了解 Vue 的结构和其执行顺序流程,让我们对 Vue 的使用更加行云流水


1. 首先我们来看下源码中,定义 Vue 这个文件夹中发生了什么

① 首先定义了 Vue 构造函数,在构造函数中加入了判断,是否使用 new Vue() 的样式来调用,然后调用了该原型下的 _init() 方法,并且将我们传入的一系列数据传入,像 data, methods, computed, watch, 还有生命周期钩子等等
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
复制代码
② 我们在 new Vue() 执行前,先执行了以下这一系列函数,将该方法传入,我们分别看看他们干了什么
	initMixin(Vue)
	stateMixin(Vue)
	eventsMixin(Vue)
	ifecycleMixin(Vue)
	renderMixin(Vue)
复制代码

2. 在new Vue 之前做了什么

① initMixin(Vue)

为Vue函数原型上添加了在构造器中执行的_init函数

  Vue.prototype._init = function (options) {
   // 其中的代码我们稍后在研究
   }
复制代码
② stateMixin(Vue)

为Vue 添加了一系列实例方法和参数 涉及到了 观察者Watcher,将在下一篇文章中专门讲解

  • $data: 返回的是new Vue 参数中传入的 data
  • $props: 返回的是当前组件接收到的 props 对象
  • $set: 弥补后期向 data 中的新添加数据不支持响应式的不足,由于响应式数据只发生在初始化 data 数据中
  • $delete: 删除 data 中的数据
  • $watch: 用于监听一个值的变化
export function stateMixin (Vue) {
  const dataDef = {}
  dataDef.get = function () { return this._data }
  const propsDef = {}
  propsDef.get = function () { return this._props }
  
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (expOrFn,cb,options){
  }
}
复制代码
③ eventsMixin 函数
  • o n : 监听实例上的自定义事件,与 on: 监听实例上的自定义事件,与 ` emit `一同使用,相当与发布订阅模式
  • e m i t : 用于触发 emit: 用于触发 ` on` 上定义的事件
  • o f f : 用于删除 off: 用于删除 ` on ` 上的事件
  • $once:定义在实例中只执行一次的自定义事件
 Vue.prototype.$on = function (event, fn): Component {
  }

  Vue.prototype.$once = function (event, fn){

  }

  Vue.prototype.$off = function (event, fn) {

  }

  Vue.prototype.$emit = function (event) {

  }
复制代码
④ lifecycleMixin(Vue)

这个阶段只干了三件事情,为Vue原型上增加两个生命周期方法和一个没有暴露的方法 其中涉及到了callHook钩子函数的执行,我们在下文中讲解

  • $forceUpdate: 主动使 Vue 实例上的所有 watcher 更新
  • $destroy: 先调用了 beforeDestroy 钩子函数,解绑所有 Vue 实例中的连接,最后调用destroyed钩子函数。
  • _update: 添加一个方法,用来更新 dom ,参数为一个虚拟node,通过 diff 算法,更新新的dom 节点
export function lifecycleMixin (Vue) {
  Vue.prototype.$forceUpdate = function () {
    const vm = this
    if (vm._watcher) {
      vm._watcher.update()
    }
  }

  Vue.prototype.$destroy = function () {
    const vm = this
    callHook(vm, 'beforeDestroy')
    
		// 删除所有 Vue 实例相关绑定
    
    callHook(vm, 'destroyed')
  }
    Vue.prototype._update = function (vnode, hydrating) {
    
  }
}
复制代码
⑤ renderMixin
  • $nextTick:暴露一个实例方法,参数为一个方法,将该方法添加到全局定义的 callback 队列中,在dom更新之后自动执行
  • _render: 返回一个虚拟节点准备更新到 dom 树上
  • installRenderHelpers : 为 Vue 上绑定了一系列渲染所需要的方法
function renderMixin (Vue: Class<Component>) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }

  Vue.prototype._render = function (): VNode {
  }
}
复制代码

3. this._init(options)

在执行 Vue 构造方法之前,我们先初始化了一系列Vue 原型上的方法和数据,现在我们进入下一步来了解初始化的过程。

① 进行合并
  if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
复制代码
② 执行初始化
  • 通过以下代码一目了然,在beforeCreate 钩子执行前,data 中的数据还没有初始化为响应式,也没有添加到 vue 实例中,因此通过 this 访问不到
	initLifecycle(vm) // 初始化生命周期
    initEvents(vm) // 初始化事件
    initRender(vm) // 初始化渲染树
    callHook(vm, 'beforeCreate') // 执行钩子函数
    initInjections(vm) // 初始化 inject,用于子组件接收父组件的传参
    initState(vm) // 初始化 data 响应式,method,computed 等等 
    initProvide(vm) // 负责给子组件提供参数
    callHook(vm, 'created') // 执行 created 钩子函数
复制代码

4. 在上一节中讲的初始化中, 我们来具体看每一个初始化函数中做了什么

① initLifecycle(vm)

在实例上定义了新的参数

  • $parent :当前实例的父实例
  • $root: 当前实例的根实例,如果没有则为自己本身
  • $children : 当前实例下的子实例,也就是子组件
  • $refs: 当前实例下所有有 ref 属性的 dom 节点
  const options = vm.$options
  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)

初始化了一个 _event对象,用来存放通过 $on 调用的事件

  vm._events = Object.create(null)
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
复制代码
③ initRender(vm)

初始化渲染函数

  • $slots: 访问当前实例下的 slot 标签,返回的是一个具名对象,默认问default
  • $scopedSlots: 用来访问作用域插槽。对于包括 默认 slot 在内的每一个插槽,该对象都包含一个返回相应 VNode 的函数。
  • $attrs: 用来访问父作用域传入子组件上的所有 prop,除了class和style
  • $listeners: 访问父作用域中的 (不含 .native 修饰器的) v-on 事件监听器
  • 定义了_c,$createElement函数来创建一个虚拟 node
  vm._vnode = null // the root of the child tree
  vm._staticTrees = null // v-once cached trees
  const options = vm.$options
  const parentVnode = vm.$vnode = options._parentVnode
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  const parentData = parentVnode && parentVnode.data
  
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
复制代码
④ callHook(vm, 'beforeCreate');

我们来看下钩子函数内部的实现

  • 这里使用了策略模式,将我们对应的钩子函数赋值给 handlers,然后执行invokeWithErrorHandling 函数
  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)
  }
复制代码
  • 我们来进一步了解一下 invokeWithErrorHandling 里边发生了什么

在这里调用了传过来的钩子函数,并把该函数this 指向 vm 实例

function invokeWithErrorHandling (handler,context,args,vm,info) 
{
    args ? handler.apply(context, args) : handler.call(context)
}
复制代码
⑤ initInjections(vm)
  • resolveInject :该函数将遍历该实例的父实例,去查找 inject 的值
  • toggleObserving:控制是否将值定义为 响应式的,因此inject/provide提供的值为非响应式的
  • defineReactive:将值添加到 vm 实例上,可直接通过实例访问
  if (result) {
  	const result = resolveInject(vm.$options.inject, vm)
    toggleObserving(false)
    Object.keys(result).forEach(key => {
        defineReactive(vm, key, result[key])
    })
    toggleObserving(true)
  }
复制代码
⑥ initState(vm)

这个函数很简单,就是对传入的参数,方法,数据,computed,watch进行初始化,让我们看看他们内部是怎么一回事

  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)
  }
复制代码
  • initProps(vm, opts.props)

该方法中,将父给子组件传入的参数解析出来,并保存到实例的_props下边,如果该实例不是根实例,则该参数不是响应式的

  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  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)

    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
复制代码
  • initMethods(vm, opts.methods)

初始化函数中,将我们传入的每一个 methods的方法的执行上下文绑定问 vm,也就是this指向method

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
  	vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}
复制代码
  • initData(vm)

这个函数中主要做了一件事情,就是将 Data 中的数据变为响应式的,并且判断该名字是否与methodsprops中的重复,重复将报错

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)

	//判断名字是否重复 

  observe(data, true /* asRootData */)
}
复制代码
  • initComputed(vm, opts.computed)

为每一个computed中的键值生成一个观察者,并且将每一个computed中的函数作为gettersetter绑定在vm实例对应的键值上,获取该值时,将自身watcher放置到Dep.targer的位置,然后触发对应依赖值的getter,将该watch放入自身依赖中,就形成了一个完整的观察者模式,此处不做解释,我们将在之后文章中专门去讲观察者模式

function initComputed (vm: Component, computed: Object) {

  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}
复制代码
  • initWatch(vm, opts.watch)

1. 遍历watch中的方法,为其创建一个观察者,并添加到对应的依赖上 2. 在 createWatcher 中,又继续调用了 vm.$watch,而在vm.$watch中有immediate的判断,是否立即调用

  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)
    }
  }
复制代码
⑦ initProvide(vm)

这个方法中将我们传入的provide放置在实例的_provided中,方便 inject 向上查找该值

function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}
复制代码
⑧ callHook(vm, 'created')

在上边我们讲过了 callHook的运行机制,这里我们就不多说,调用了 created 钩子函数

5. 在所有初始化结束之后,我们就进入了挂载阶段

① 最后一步,将 el 挂载到 dom 树中

我们将在下一篇文章中详细讲述

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

至此,初始化阶段就全部结束了,下一步,让我们来学习挂载阶段。

おすすめ

転載: juejin.im/post/7074849663131582500