mount 挂载实现原理

1,087 阅读2分钟

《new Vue 发生了什么》一文中,详细地讲解其内部究竟是怎样实现的。从该文可知,如果在实例化 Vue 时传入的参数 el 不为空时,那么在其内部会自动调用 Vue 实例上函数 $mount 进行挂载;否则会进行手动挂载。那么,Vue 源码中是如何实现挂载的呢?这将是本文要探究的内容。

mount 挂载实现原理

沿着主线将其实现逻辑整理成一张图,如下:

mount.png

接下来根据这张图一步一步地讲解其内部是如何实现的?

在函数 mount 实现逻辑中,主要可以分为 4 步,分别如下:

1、通过 el 查询元素
el = el && query(el)

/**
 * Query an element selector if it's not an element already.
 */
export function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}

通过 typeof 判断 el 类型,如果是 string 类型,则调用 document.querySelector(el) 查询元素,并将其返回;否则直接返回。

2、检查 Vue 实例是否挂载在 html 或者 body
  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

如果 Vue 实例挂载在 html 或者 body 上,则抛出告警,并且终止程序。

3、根据是否有 render 函数来决定是否需要将 template 编译成 render 函数

如果传参没有包含 render 函数,则使用 template 编写,需要将 template 编译成 render 函数。

4、调用保存的函数 mount

准备工作做好了,此时调用在重新定义 mount 前保存的 mount,即在文件 src/platforms/web/runtime/index.js 定义的函数 mount。在其内部实现中,核心代码就一行:

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

即调用函数 mountComponent(this, el, hydrating)

mountComponent 内部实现

1、检查 Vue 实例是否有 render 函数
  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
        )
      }
    }
  }

如果 Vue 实例没有 render 函数,则会其赋值默认函数:createEmptyVNode,作用创建空的虚拟结点。与此同时,在开发环境下会抛出告警。

2、调用生命周期函数 beforeMount
callHook(vm, 'beforeMount')
3、定义函数 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)
    }
  }

在函数 updateComponent 内部实现中,有两个比较重要的方法:vm._rendervm._updatevm._render 作用是将 Vue 实例渲染成一个虚拟 Node;而 vm._update 作用是将虚拟 Node 渲染成真实 DOM。后续章节会详细分析这两个方法的内部实现逻辑。

4、实例化 Watcher
// 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 */)

在实例化 Watcher 时,需要传入 5 个参数,其中有一个是刚刚定义的函数:updateComponent,作为参数传入,此后在其回调函数中会被执行;除此之外,isRenderWatcher 值为 true,表示此 Watcher 是渲染 Watcher,作用是为了区别其他 Watcher,比如计算属性 Watcher。那么来看下Watcher 内部是如何实现的?

Watcher 在这里有两个作用:一个是在初始化时会执行其回调函数,也就是作为参数传入的函数 updateComponent;另一个是当 Vue 实例监测到数据发生变化时,也会执行其回调函数,即响应式原理。

那么这里主要分析 Watcher 初始化部分,即其构造函数是如何实现的?构造函数接收 5 个参数:

export default class Watcher {
  ...
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
      ...
    }
}
  • vm:Vue 实例
  • expOrFn:数据类型为字符串或者函数
  • cb :回调函数
  • options:可选参数,数据类型为 Object
  • isRenderWatcher:表示是否为渲染 Watcher

isRenderWatcher 参数体现在这行代码:

if (isRenderWatcher) {
  vm._watcher = this
}

对于包含子组件的组件,子组件的挂载需要依赖已经定义 vm._watcher 。重点来看下如何解析参数 expOrFn,具体实现如下:

// parse expression for getter
if (typeof expOrFn === 'function') {
  this.getter = expOrFn
} else {
  this.getter = parsePath(expOrFn)
  if (!this.getter) {
    this.getter = noop
    process.env.NODE_ENV !== 'production' && warn(
      `Failed watching path: "${expOrFn}" ` +
      'Watcher only accepts simple dot-delimited paths. ' +
      'For full control, use a function instead.',
      vm
    )
  }
}

如果 expOrFn 传入的是一个函数,则将其赋值给 getter;否则是一个字符串,应该是一个路径,调用函数 parsePath 对其进行解析,为空的话则抛出告警。

最终执行回调函数在最后一行代码,即

this.value = this.lazy ? undefined : this.get()

由于 lazy 值为 false,则会调用 Watcher 实例方法 get。其实现逻辑如下:

/**
  * Evaluate the getter, and re-collect dependencies.
  */
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
}

核心代码就一行:value = this.getter.call(vm, vm)。即调用作为参数传进来的函数:updateComponent,将 Vue 实例渲染成虚拟 Node,再将虚拟 Node 渲染成真实 DOM。

5、完成 Vue 实例挂载
// 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')
}

此时判断为根节点时设置 vm._isMountedtrue,表示 Vue 实例已经完成挂载;同时执行生命周期函数 mounted。需要注意的是

vm.$vnode 表示 Vue 实例的父虚拟 Node。

至此,Vue 源码如何实现挂载分析完了,其中涉及到 template 如何编译成 render 函数、vm._render 将 Vue 实例渲染成虚拟 DOM、vm._update 将虚拟 DOM 渲染成真实 DOM 的实现逻辑后续章节会详情介绍。

参考链接

Vue 实例挂载的实现