Vue深入源码:new Vue的时候会做什么

2023-04-16 0 340

您好,我是bp技术知识库,欢迎关注。


时隔2个多月,我又开始发文了,这两个多月我逐步开始研究React和Vue源码相关的东西,并且也深入阅读了JavaScript相关的资料,接下来我会逐渐整理我学到的知识,进一步加深对这些知识的理解。

在平时写业务的时候,可能阅读源码并不会对你书写业务的效率提升多少,也许你觉得你现在写业务已经非常的得心应手了,那么下面这些与Vue有关的问题你是否思考过:

  • 生命周期是如何实现的。
  • 组件是如何和data还有props进行绑定的。
  • 组件中的state更新时,为什么视图会发生变化。
  • vue-router是如何实现单页面模拟路由切换的。
  • vuex是如何实现的。

为什么是Vue2不是Vue3?

Vue3的源码相对于Vue2来说已经发生了翻天覆地的变化,用TypeScript进行了重构,所以Vue3和Vue2的源码存在着巨大的区别,那么Vue3已经出了两年左右了,我为什么还要阅读Vue2的源码?

那是因为Vue2的代码经过多年的沉淀,已经不会有太大的改动了,Vue3的代码可能今天你看到是这样,明天就进行了非常大的改动。

好吧,其实是因为公司目前依然使用的Vue2…

所以我就从Vue2开始研究,等到Vue2研究的差不多的时候,再去研究Vue3。

但是看了一部分Vue2的源码后,我突然醒悟我其实可以直接看Vue3…所以关于Vue2的文章可能就只有这一篇,因为这篇文章我一开始研究的时候就写了个大概。后面的文章几乎都是Vue3和React的。

为什么我开始读源码?

阅读源码不光光是为了应付面试,最重要的是它可以给你很多启发,你看源码的过程中可能会忍不住发出惊叹:还有这种操作?

随着你对这些框架的深入了解,你写业务代码的时候也会不自觉的站在另一个角度去思考代码,即:如何让代码更具复用性。

那么说回正题,Vue项目的入口文件通常会有下面的代码:

new Vue({
  render: h => h(App),
}).$mount('#app')

这小小的一段代码背后的故事,将分成两部分进行分析,分别是new Vue部分和.$mount部分。

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

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

从代码中可以看到有5个Mixin函数:

  • initMixin:初始化相关。
  • stateMixin:state相关。
  • eventsMixin:事件相关。
  • lifecycleMixin:生命周期相关。
  • renderMixin:渲染相关。

initMixin是本篇文章主要讲解的函数,因为它涉及到初始化相关的内容。

initMixin

该函数为Vue的原型上添加一个_init方法。在这里可以看到源码:_init

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    ...省略内容
  }
}

而该方法会在new Vue的时候会进行调用:

function Vue (options) {
  // 调用_init方法
  this._init(options)
}

所以我们要了解new Vue做了什么最主要的就是需要了解_init方法中做了什么。

关于_init方法,我删除了一些new Vue生成实例时并不会走的代码:

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

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

    // 如果有el参数,则调用.$mount方法
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

从源码中我们可以看出,下面两种写法是完全等价的:

new Vue({
  render: h => h(App),
}).$mount('#app')

new Vue({
  render: (h) => h(App),
  el: "#app",
});

_init方法中会经过一系列的初始化内容:

  • initLifecycle(vm)
  • initEvents(vm)
  • initRender(vm)
  • callHook(vm, 'beforeCreate')
  • initInjections(vm)
  • initState(vm)
  • initProvide(vm)
  • callHook(vm, 'created')

从上面的命名可以看得出来,这一系列的函数是初始化生命周期、事件、渲染、Inject、State等内容。

这些内容不是本篇文章需要关注的重点,后面会以Vue3的源码作为参考,去探讨Vue的生命周期,State以及props等等内容。

那么通过上面的探讨,总结一下new Vue中主要做了什么事情?

  1. 调用_init方法。
  2. 通过mergeOptions函数合并options并且赋值给Vue上的$options属性。
  3. 初始化生命周期、事件、渲染、Inject、State等内容。

$mount

接下来就要来谈一下.$mount中做了些什么操作,因为new Vue中只是进行了一大堆的初始化操作,但是并没有涉及到如何将VNode节点渲染到界面上。

这里可以找到它的源码:$mount源码

源码中的最后一句调用了一个方法:

return mount.call(this, el, hydrating)

即下面的这个方法:

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

它的源码地址。

可以看到.$mount其实是调用mountComponent函数:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

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

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

这里的源码是比较饶的,而且几乎没有注释,如果不通过debugger调试一下源码,可能根本就不知道方法之间的执行顺序,所以我个人推荐阅读源码,精髓就在于调试,如果你光看源码,可能看几行代码就放弃了。

代码中定义了一个updateComponent方法:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

接下来将该方法作为参数传入了Watcher构造函数中:

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

所以在mountComponent函数中,有3个点我们需要关注:

  • _update方法。
  • _render方法。
  • new Watcher

_update方法和_render方法是在new Watcher中进行调用的,所以我们这里优先看一下new Watcher中做了什么操作。

由于代码比较多,所以本篇文章只关注new Watcher中与.$mount关系紧密的部分。

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

关键代码就是这句:this.getter.call(vm, vm)

它调用了updateComponent函数,而该函数中又调用了_update方法和_render方法

执行顺序为先执行_render方法,再执行_update方法,所以我们先看_render方法中做了什么处理。

if (_parentVnode) {
  vm.$scopedSlots = normalizeScopedSlots(
    _parentVnode.data.scopedSlots,
    vm.$slots,
    vm.$scopedSlots
  )
}

如果在Vue中使用过jsx的朋友可能对于$scopedSlots比较熟悉,它是跟插槽相关的内容。

于是我们根据代码可以大胆的推测:在_render方法中主要是做了一些和插槽相关的工作,并且将$vnode属性指向它的父组件。

前文中我们提到了lifecycleMixin这个函数,而_update方法]就是在这个函数中被挂到原型上的,所以我们可以在这里找到它的源码:

_update中调用了一个非常重要的方法vm.__patch__这就是Vue2中的diff算法相关的内容。

而关于diff算法,在后面的文章中会以Vue3的代码单独进行探讨。

Vue2的diff算法所在的位置:patch。

我们再次大胆的猜测一下:_update方法就是根据VNode节点,生成一个DOM元素。

比起上面的_update方法做了一大堆diff算法,衍生出了各种方法来讲_render方法所做的事情就简单的多。

最后

我们总结一下在new Vue().$mount()时Vue做了哪些事情:

  • 初始化生命周期、事件、渲染、Inject、State等内容。
  • 根据VNode节点创建真实DOM节点渲染到页面上。

虽然我们大致明白了new Vue时的到底做了什么,但本文开头提出的问题都没有得到解答。

不过别着急,一口吃不成一个胖子,这些内容会在后面的Vue3源码相关的文章中进行探讨。

由于源码系列的参考资料并不是太多,所以文章中可能有些地方的理解是错误的,主要目的还是提供一个阅读源码的思路,推荐还是按照本文提供的思路,自己对源码进行一下调试,可能会有更深层次或者不同的理解。

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务