Vue源码学习1.3:$mount挂载

1,099 阅读6分钟

建议PC端观看,移动端代码高亮错乱

上一章中我们介绍了 _init 方法。 在_init的最后执行了挂载的操作

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

$mount 方法在多个文件中都有定义。因为 $mount 这个方法的实现是和平台、构建方式都相关的。接下来我们重点分析带 compiler 版本的 $mount 实现。

1. 重写的 $mount

src/platforms/web/entry-runtime-with-compiler.js

// src/platforms/web/entry-runtime-with-compiler.js

// 缓存公共的 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 将 el 转换成 DOM 节点
  el = el && query(el)

  // 挂载节点不能是 html 或 body 节点
  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
  }

  const options = this.$options
  // 不存在 render 则解析 template 并转换成 render函数
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          // 如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板
          template = idToTemplate(template)
          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 没有配置时,将 el 的outerHTML 作为模板,
      // 这也就是为什么 el 不能是 body 和 html的原因
      template = getOuterHTML(el)
    }
    if (template) {
      // 性能监控...

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      // 性能监控...
    }
  }
  return mount.call(this, el, hydrating)
}
  • 先把公共的 $mount 方法缓存下来
  • query方法将 el 转换成 DOM 节点,query 方法很简单,就是调用 document.querySelector(el) 返回 DOM
// src/platforms/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
  }
}

回到重写的 $mount 函数中接着往下看:

  • 判断 el 是否是 htmlbody 标签
  • 处理 template ,下文详细分析
  • template 转成 render,主要是通过 compileToFunctions 实现,这个我们在以后的编译章节再说
  • 执行缓存下来的公共 $mount 方法

1.1 处理 template

let template = options.template
if (template) {
  if (typeof template === 'string') {
    if (template.charAt(0) === '#') {
      // 如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板
      template = idToTemplate(template)
      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 是否存在
    • 如果 template 是否以 # 开头的字符串,如果是的话则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板。通过 idToTemplate的实现,稍后分析一下这个函数。
    • 如果 template 具有 nodeType 属性,则其是一个 DOM,然后template = template.innerHTML
    • 兜底情况是抛出一个错误:invalid template option:
  • 不存在则调用 getOuterHTML 函数,稍后分析一下这个函数。

先来看看 idToTemplate 这个函数:

// src/platforms/web/entry-runtime-with-compiler.js
const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

可以看到用 cached 方法包了一下,参数是一个箭头函数,箭头函数做了两件事:

  • 根据 id 获得 DOM 对象
  • 返回el.innerHTML

然后 cached 方法定义在 src/shared/util.js

// src/shared/util.js
export function cached<F: Function> (fn: F): F {
  const cache = Object.create(null)
  return (function cachedFn (str: string) {
    const hit = cache[str]
    return hit || (cache[str] = fn(str))
  }: any)
}
  • 创建一个纯净的 cache 对象,用来保存 templateinnerHTML 的键值对
  • 返回一个 cachedFn 函数
  • 当调用 idToTemplate 时,会执行这个 cachedFn 函数,参数传入的是 # 开头的 template 字符串,如果 template 存在于 cache 对象中,那么直接返回值,否则 return cache[str] = fn(str)

最后再看看 getOuterHTML 函数:

function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    // cloneNode(true):深度克隆子节点
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

2. 公共的 $mount

定义在 src/platforms/web/runtime/index.js

// src/platforms/web/runtime/index.js

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

传入两个参数:

  • el:挂载对象
  • hydrating:服务器渲染相关

做了两件事:

  • 如果存在el 且是浏览器环境,那么调用 query 获得 DOM 对象,否则返回 undefined。为什么这里还要重复修正 el呢?我们的 el不是再上面已经处理完了吗? 这是因为 runTime Only 版本的 $mount 逻辑只有这里而已
  • 执行 mountComponent 方法

mountComponent 方法定义在 src/core/instance/lifecycle.js 文件中

// src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // render函数不存在时
  if (!vm.$options.render) {
    // 创建一个空的vnode
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      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
        )
      }
    }
  }
  // 执行 beforeMount 钩子
  callHook(vm, 'beforeMount')

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

  // 实例化渲染 watcher
  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
}
  • 先实例化一个渲染Watcher,初始化会触发 updateComponent,并且之后 vm 实例中的监测的数据发生变化的时候也会触发 updateComponent,这个以后再介绍。
  • updateComponent 方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。关于 vm._rendervm._update 的分析在之后的章节再展开。
  • 函数最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示实例的占位符VNode,它为 Null 则表示当前是根 Vue 实例。

2.1 分析 渲染Wather

这里我们分析渲染Wather只做简要分析,更多依赖收集的细节不做讨论

// src/core/observer/watcher.js

export default class Watcher {
  // 一些实例属性...

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    
    // ...
    
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      // ...
    }
    
    // this.lazy 是 false
    this.value = this.lazy ? undefined : this.get()
  }
}

首先判断是否为 渲染Wather,将 this 赋值给 vm._wather,因为在 watcher 的初始化 patch阶段 可能会调用 $forceUpdate (比如在子组件的 moutned 钩子内部),这取决于 vm._watcher 是否定义。其实看看 forceupdate 的代码就知道了:

Vue.prototype.$forceUpdate = function () {
    var vm = this;
    if (vm._watcher) {
        // 就是调用这个 `vm` 的 `_watcher` 的 `update` 方法。用来强制更新
        vm._watcher.update();
    }
};

回到实例化 渲染Watcher 过程,最终会执行 this.get(),在当前情况下内部实际上只是执行了 this.getter.call(vm, vm),也就是会执行 updateComponent

总结

mountComponent 方法的逻辑也是非常清晰的,它会完成整个渲染工作,接下来我们要重点分析其中的细节,也就是最核心的 2 个方法:vm._rendervm._update