迫于菜🐶 - Vue.js 源码(五)

839 阅读4分钟

往期章节

  1. 第一章-源码目录
  2. 第二章-项目构建
  3. 第三章-入口开始
  4. 第四章-new Vue

前面四章中,我们跟随 Vue.js 技术揭秘 , 对 Vue.js 的源码部分又有了进一步的了解,今天一起来学习 Vue 实例挂载是怎么一回事。

$mount

Vue 中我们是通过 $mount 实例方法去挂载 vm 的,$mount 方法在多个文件中都有定义。因为 $mount 这个方法的实现是和平台、构建方式都相关的,我们来分析一下带 compiler 版本的 $mount 实现。定位到 src/platform/web/entry-runtime-with-compiler.js

// ...
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
// ...
}

而原型上的 $mount 方法其实是在 src/platform/web/runtime/index.js 就定义过的。

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

之所以重新定义,是因为 runtime only 版本的其实是没有这块逻辑的,这块代码主要是为了复用,它可以被 runtime only 的 Vue 直接使用。继续分析这个函数做了哪些操作。

首先它对传入的 el 做了限制处理,可以是 string 或者 element , 然后调用了 query 方法,query 是在 src/platform/web/util/index.js 定义的。

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

这个函数比较简单,它判断 el 如果是字符串,则调用 document.querySelector ,如果找不到的话,就报出一个警告,并且返回一个空的 div。否则就已经是 dom 对象了,所以直接返回就可以了。OK,回到主线上来,那么此时 el = el && query(el) 就已经是一个 dom 对象了。继续往下看,它又对 el 进行了一个判断,不让它挂载到 bodyhtml 这样的根节点上,因为它是会覆盖的,如果挂上去,会把整个 html 文档都覆盖掉。

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

接下来的逻辑就比较关键了。它通过 this.$options 去判断有没有定义 render 方法。通过我们工程都是脚手架生成的,所以一般是是不会去手写 render,实际上它是可以手动定义的。

// ...
const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
// ...

顺着逻辑读下来,它会走到 template = getOuterHTML(el) ,看一下 getOuterHTML 是什么玩意。

/**
* Get outerHTML of elements, taking care
* of SVG elements in IE as well.
*/
function getOuterHTML (el: Element): string {
 if (el.outerHTML) {
   return el.outerHTML
 } else {
   const container = document.createElement('div')
   container.appendChild(el.cloneNode(true))
   return container.innerHTML
 }
}

在 MDN 中找到了关于 outerHTML 的定义

element DOM接口的outerHTML属性获取描述元素(包括其后代)的序列化HTML片段。它也可以设置为用从给定字符串解析的节点替换元素。

那此函数就简单了,判断有没有 el.outerHTML ,有则直接返回,没有则在外面包一层 div,然后执行 innerHTML ,其实就是 outerHTML API 的一种 polyfill 方法。这样的话就能在代码中拿到我们定义的最外层的 div ,注意,此时返回的是一个字符串。例如

<div id="app"></div>

下面的代码就是与编译相关的了,我们这次暂时不关注。 我们只需要知道

如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。在 Vue 2.x 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions 方法实现的。

有了 render 函数之后,会调用 mount 方法,这里的 mount 就是最开始定义的 ,它调用原型上的 $mount 做挂载。

const mount = Vue.prototype.$mount

紧接着又会执行到 src/platform/web/runtime/index.js 里,还是前面所说可供复用的方法,这里再贴一次代码。

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

$mount 方法支持传入 2 个参数,第一个是 el,它表示挂载的元素,可以是字符串,也可以是 DOM 对象,如果是字符串在浏览器环境下会调用 query 方法转换成 DOM 对象的。第二个参数是和服务端渲染相关,在浏览器环境下我们不需要传第二个参数。

$mount 方法实际上会去调用 mountComponent 方法,这个方法定义在 src/core/instance/lifecycle.js 文件中,我们来逐步分析一下。 首先通过 vm.$el = el 来做缓存,然后判断是否有 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) !== '#') ||
       ) {
      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
      )
    }
  }
}

如果没有 render 函数,并且 template 也没有正确的转换成 render 函数,那么就执行 vm.$options.render = createEmptyVNode 去创建一个空 VNode 给它。接着判断如果定义了 template ,但是第一个不是 # ,或者是写了 render 函数,但是用的是 complier 版本吗,就会报一个警告啦,警告大致是说,使用了 runtime only 版本,然后又没写 render 函数,然后 template 是不可编译的。或者是两者都没有,就会报 else 里的另外一个警告。

接下来就是一堆关于 Vue 性能埋点的逻辑代码,暂时不看,它最后会走到这里。

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

vm._render() 可以理解为返回出来一个 VNode,而 hydrating 是服务端渲染相关,可以理解为就是 false。 那 updateComponent 是怎么执行的呢。它实际上调用了 new 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 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数。

可以把理解为它是一个渲染 Wathcer ,看到它传了几个参数,vm 实例,updateComponent 函数,noop 空函数,配置对象,布尔值。找到 Wathcer 的定义位置,在 src/core/observe/watcher.js ,其实 Wathcer 也是一个 class ,它的构造函数是这样。

constructor (
 vm: Component,  
 expOrFn: string | Function,
 cb: Function,
 options?: ?Object,
 isRenderWatcher?: boolean  // 是否是渲染 watcher
)

this.vm = vm
if (isRenderWatcher) {
 vm._watcher = this
}
vm._watchers.push(this)

然后在 vm 上添加了一个 _watcher ,并 push 到所有的 _watchers 里面。继续往下执行,可以找到 value = this.getter.call(vm, vm) ,调用 this.getter 也就是执行到 updateComponent 方法了,它会先调用 vm._render() 生成一个 VNode ,然后通过 vm._update() 挂载更新 DOM。

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

函数最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。

总结

这章主要分析了 $mount 的实现,和 mountComponent 方法的详细内容,下一章我们来分析 vm._update()vm._render()