浅曦Vue源码-6-new Vue()那些事儿(3)-_init mergOptions 后初始化

471 阅读4分钟

「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

前面我们在讨论 new Vue 发生的那些事儿,核心在于 _init 方法,现在再来简单复习一下 _init()的细节逻辑:

  1. vmVue 的实例,在后面出现 vm 就要想到 Vue 的实例;
  2. 根据 options._isComponent 处理选项的合并,根实例获得 vm.$options 属性;
  3. 代理 _renderProxyvm
  4. 一系列的初始化和 beforeCreatedcreated 钩子的调用;
  5. 最后根据 vm.$options.el 属性决定是否调用 vm.$mount 方法实施挂载;

上一篇我们讨论了 mergeOptions 的细节,即合并 Vue.optionsnew Vue() 时传递的子选项,并且详细讨论了关于选项合并实现全局组件、指令、过滤器的原理,本篇将会继续讨论 _init 中的其他细节;

二、_renderProxy 代理到 vm

2.1 代理设置

这个 _renderProxy 是做什么用的呢?这里先大致交代一下,这个 vm._renderProxy 用于传递给 render 函数生成 vnode。只不对于开发环境和线上环境的实现不同,开发环境用的是 ES6 原生的 Proxy 实现的。

Vue.prototye._init = function () {
  // ...
  if (process.env.NODE_ENV !== 'production') {
    // 设置代理,将 vm 实例上的属性代理到 vm._renderProxy
    initProxy(vm)
  } else {
    vm._renderProxy = vm
  }
}

2.2 initProxy 方法

方法位置:src/core/instance/proxy.js -> initProxy

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

new Vue 过程中,通过断点看看到底 Proxy 使用了哪个 handler,然后再看这个 handler 都做了什么 image.png

很显然上面使用了 hasHandler,代码很简洁,就是判断你访问的属性是否在目标对象上正确存在或者是 window 上的全局属性,例如: Math / Date,如果不是就爆出提示告知你:渲染时使用的某个属性在实例上没有:

这是一个开发环境的预警,属于改善 DX 的提示信息;

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

三、initLifecycle 方法

方法位置:src/core/instance/lifecycle.js -> initLifeCycle

3.1 _init 中的调用,参数为 vm;

Vue.prototye._init = function () {
  // ...
  initLifeCycle(vm)
}

3.2 initLifeCycle 源码

该方法初始化了以下属性:

  1. 组件间关系的属性,比如 $parent/$children/$root/$refs
  2. 表示实例状态的属性:_inactive/_isDestroyed/_isMounted
export function initLifecycle (vm: Component) {
  const options = vm.$options

  // 定位到第一个非抽象元实例的父实例
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm // 如果没有 parent 说自己就是 $root

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

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

四、initEvents 方法

方法位置:src/core/instance/events.js -> initEvents

该方法初始化实例的自定义事件,在组件上注册的自定义事件,该事件的监听者是其自身;

4.1 方法调用

Vue.prototye._init = function () {
  // ...
  initLifeEvents(vm)
}

4.2 initEvents 源码

通过断点得知,该方法初始化了 mv._eventsvm._hasHookEvent 属性,在后面我们会了解到一个属性叫做 hookEvent,是一种使用生命周期前缀的事件命名方式,以这种方式命中的事件随着生命周期 hook 调用而被调用

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

方法位置:src/core/instance/render.js -> initRender

5.1 在 _init 中调用

Vue.prototye._init = function () {
  // ...
  initRender(vm)
}

5.2 initRender 源码

在该方法中,主要做了以下事情:

  1. 解析插槽信息,赋值到 vm.$slots 属性;
  2. 初始化了后面将要用到的一个重要工具方法方法,vm._cvm.$createElement
export function initRender (vm: Component) {
  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 // the placeholder node in parent tree
  const renderContext = parentVnode && parentVnode.context
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject

  // 初始化 _c,它是 createElement 的一个科里化方法,其意义在于提前绑定了 vm
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

  // 初始化 $createElement
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  const parentData = parentVnode && parentVnode.data

  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    // 开发环境校验 $attrs 和 $listeners 不可以修改
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
    }, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, () => {
      !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
    }, true)
  } else {
    defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
    defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)
  }
}

六、callHook(vm, 'beforeCreate')

方法位置:src/core/instance/lifecycle.js-> callHook

6.1 方法调用

Vue.prototye._init = function () {
  // ...
  callHook(vm, 'beforeCreate')
}

触发 beforeCreate 生命周期钩子,对的,就是 Vue 的生命周期中的 beforeCareate 钩子。我们看看 callHook 方法:

export function callHook (vm: Component, hook: string) {
  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)
    }
  }
  // vm._hasHookEvent 是标识有没有 hook event 的,这个值是哪里处理的呢?
  // 是处理事件监听的 vm.$on方法处理的
  //vm._hasHookEvent = /^hook:/g.test(eventName) 
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

这里面就提到了前面 initEvents 方法中的hookEvent,那么 hookEvent 长啥样子嘞?可以看到 vm.$emit('hook:', hook) 这行代码,这个就是触发 hookEvent,很显然,hookEvent 是以 hook: 打头的事件名,冒号后面是一个生命周期,比如:

<comp @hook:created="eventHandler" />

七、initInjections

方法位置:src/core/instance/inject.js -> initInjections

7.1 方法调用:

Vue.prototye._init = function () {
  // ...
  initInjections (vm)
}

7.2 源码

初始化选项中 inject 的配置项,injectprovide 组合使用,用于跨域层级从祖辈组件上接收 provide 提供的数据,和 ReactProvider 作用类似;

值得一提的是,inject 中的数据是响应式的,就是在这里处理的,它同样遵循单向数据流的原则,不要在子组件中修改 inject 中的数据,因为一旦祖辈组件上的数据发生变更,你子组件中的数据就会被覆盖掉

export function initInjections (vm: Component) {
  // 首先解析 inject 配置项,
  // 然后从祖代组件的配置中找到配置项中的每一个 key,value,
  // 最后得到 result[key] = val 
  const result = resolveInject(vm.$options.inject, vm)
  // 对 result 做响应式处理,也有代理 inject 配置中每个 key 到 vm 实例的作用;
  // 提示不要在子组件中更新这些数据,因为祖代组件中 provide 发生被改变,你在组件中的更改就会被覆盖
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}

7.3 resolveInject

方法位置:src/core/instance/inject.js -> resolveInject

7.3.1 resolveInject 源码

这个方法的作用很简单,就是利用前面 mergeOptionsnormalizeInject 的结果,把 inject 选项的值处理成 { from, default } 的对象形式,然后 while 循环获取当前实例的 $parent 上的 _provide 属性,从中取出 from 对应的值,如果遍历结束没有找到就用 default,如果没有 default` 就抛出错误

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
    const result = Object.create(null)

    // inject 配置项所有的 key
    const keys = hasSymbol
      ? Reflect.ownKeys(inject)
      : Object.keys(inject)

    // 遍历 key
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
            // 跳过 __ob__ 对象
      if (key === '__ob__') continue

      // 这里需要注意一下,在 前面 _init 的时候,有一个 mergeOptions 的调用,
      // 其中有进行 inject 的格式化的过程 normalizeInject(options.inject),
      // 把它整理成 { from, default } 的形式
      // 拿到 provide 中对应的 key(from 就是前面格式结果)
      const provideKey = inject[key].from
      let source = vm

      // 遍历所有的祖代组件,直到根组件,找到 provide 中对应的 key 的值,最后得到 result[key] = provide[provideKye]
      // 这也是为啥子代的会被父代的覆盖的主要原因吧,因为最终的取用值是父代 provide 的
      while (source) {
        if (source._provided && hasOwn(source._provided, provideKey)) {
          result[key] = source._provided[provideKey]
          break // 找到一个就终止,最近的 provide
        }
        source = source.$parent
      }

      // 如果上一个 while 循环未找到,
      // 则采用 inject[key].default 如果没有设置 default 值,则抛出错误
      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
  }
}

八、initState

Vue.prototye._init = function () {
  // ...
  initState(vm)
}

该方法设初始化数据响应式,处理 props/ methods/ data/ computed/ watch,这里先不多说,后面专门讲一个响应式数据的设置相关内容(这就说明我还要再写一篇,你就说气不气)。

很多人看到这里是不是要失望了 ~~~~~

你不要失望,荡气回肠是为了最美的平凡

九、callHook(vm, 'created')

触发 created 生命周期的钩子,这里就必要清楚为什么官方文档说 created 之后才有数据响应式了吧,因为前面刚刚才完成数据响应式的初始化啊~~~

Vue.prototye._init = function () {
  // ...
  callHook(vm, 'created')
}

十、根据 vm.$options.el 决定是否调用 $mount

Vue.prototye._init = function () {
  // ...
  if (vm.$options.el) {
    // 调用 $mount 方法,进入到挂载阶段
    vm.$mount(vm.$options.el)
  }
}

_init 最后一步是判断,vm.$options.el 是否存在,即你是否配置了 el 属性,这个东西大家不陌生,是个挂载点,如果有的话就自动 $mount 进入到挂载阶段,如果没有呢,就要手动挂载。

这个大家很清楚,一般创建根实例,即 new Vue 的过程中,我们都会传入 el 属性,一般 #app。自动挂载就是这里啊。

其实这个设计屡见不鲜了,webpack 源码中在,webpack 最后创建 compiler 后也会判断,webpack 是否传入了callback,如果传了就自动调用 compiler.run 进入遍历,否则就返回 compiler 编译器实例。

事实上,随着后续的深入,我们也会发现,一般只有根实例有 el 属性,我们自己开发组件选项的时候是不传 el 的,这个时候就需要 Vue 自己判断挂载点了,这里先不展开。

这里还没完数据响应式,所以暂时不展开 $mount 细节。