Vue $mount的挂载入口的奥秘

16,059 阅读7分钟

$mount挂载入口的奥秘

在想了解$mount挂载入口的时候,希望先了解一下关于我公众号14篇关于Vue合并策略的解析。这样可以更好的了解$mount挂载入口的区分意义

合并策略已经讲解完成。在合并策略之后还有很多始化操作,在执始化执行到最后就是执行Vue原型上$mount方法将组件挂载到开发者给定的元素之上。$mount存在两种挂载方式,手动挂载 、同样在api中向外暴露了。第二个则是自动挂载,一旦有el选项,则会在执行_init最后进行内部的自动挂载。

挂载入口进行分析

渲染挂载

首先第一个挂载入口在src/platforms/web/runtime/index.js,在Vue原型上挂载了$mount函数。在src/platfroms/web/runtime/index.js中进行了两个步骤的处理,第一个在合并策略中已经提到过,对平台进行区分重写,添加了一些针对于平台内置的components,和directives。第二个则是vue runtime-only的版本,在此入口进行rollup打包进之后是一个不经过编译的版本,但是需要通过打包工具把template转成render渲染函数。

手动挂载Demo

var MyComponent = Vue.extend({
  template: '<div>Hello!</div>'
})

// 创建并挂载到 #app (会替换 #app)
new MyComponent().$mount('#app')
// 或者
new MyComponent().$mount(document.querySelector('#app'))

$mount文档中规定传入的el参数可以是两种情况,{Element | string} [elementOrSelector],要么字符串,要么是DOM元素。通过$mount进行挂载,会替换入的el对应的DOM无素。上面的DEMO是通过手动调用$mount进行挂载。同样创建实列的时候传入el进行自动挂载

渲染$mount源码解析

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

渲染$mount传入两个参数,一个是el可以是一个DOM元素,也可以是一个字符串

 el = el && inBrowser ? query(el) : undefined

内部执行的第一部先拿到换化后的el参数,因为el可能是字符串,可能会是Dom元素,也有可能是一个不符合参数要求的值。首先做两个判断,是否el有值,并且此时执行的环境是浏览器环境

inBrowser 判断是否是浏览器环境

在 src/core/util/env文件中可以查看inBrowser的实现

export const inBrowser = typeof window !== 'undefined'

描述: 检查当前执行环境是否是浏览器环境

实现原理: 只有在浏览器环境中才会有window对象,通过typeof去检测window的类型,如果window对象不存在,肯定是undefined


满足了两者条件之后,通地query方法去解析el参数,获取到真正的DOM元素,否则不满足两者条件,直接返回undefined.

query方法

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

描述: 通过无素选择器去获取元素

参数: el 可以是Dom无素,也可以是字符串

实现方式: 首选判断el参数是否是字符串。是字符串使用document.querySelector方法通过元素选择器去获取真正的dom元素。如果获取不到,开发环境的情况下,发出'Cannot find element: ' + el警告。创建一个空的div元素返回出去.el参数不是字符串的情况下,不做任何操作直接返回el参数。但在这个情况下还有两种可能,一个是不合法的值,比说传入了一个数字或布而值,或者传入了真正的DOM元素(只考虑合并的情况)。

return mountComponent(this, el, hydrating)

最后调用mountComponent进行真正的挂载工作。最后返回的则是挂载后的组件实列。

const vm = new MyComponent().$mount('#app')
console.log(vm)

通过$mount渲染挂载之后执行mountComponent之后返回了vm实列,所以可以通过挂载后通过赋值给自定义一个变量,拿到最后挂载后的实列。

自动挂载Demo

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

在执行_init()函数的最后,当初始化工作完成之后,el经过全并已经合并到vm.$options对象上,对$options进行检测,如果存在el则进行自动挂载,传入el参数,调用是Vue原型上$mount函数。

ast语法树转render函数与渲染挂载$mount

如果此时运行的版本是runtime with compiler版本,这个版本的$mount会被进行重写。并且增加了把template模板转成render渲染函数。运行的入口在 src/platforms/web/entry-runtime-with-compiler文件中。

缓存挂载的$mount

const mount = Vue.prototype.$mount

前面分析的渲染挂载的$mount在自执行的过程中,比src/platforms/web/entry-runtime-with-compiler文件中的$mount先挂在Vue的原型上,负责在页面渲染真正的DOM结构,通过mount变量缓存了运行时版本的渲染挂载的函数。

Vue.prototype.$mount = function () {
  ...省略
}

紧接着把Vue原型上原本挂载的运行时版本的渲染挂载函数进行重写,这里重写的原因主要因为这不但是一个运行时的版本,同时也担作着编译模版转化为render函数的作用。此时针对了版本需求的不同进行了重写。

el = el && query(el)

el参数通过query函数进行获取指入的挂载点,获取的Dom元素赋值给el参数,关于query函数的运用已经解释过,如果不是元素选择器,则原封不动返回。

body与html元素不能被替换的原因

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
}

这里的判断挂载点是否是<body>元素或者是<html>元素,在生产环境下会报出警。不要挂载到htmlbody元素上,对其它元素进行替换。从Demo理解真正原因:

<body>
  <p id="app">
  </p>
</body>
var MyComponent = Vue.extend({
  template: '<div>Hello!</div>'
})

const vm = new MyComponent().$mount('#app')

合并结果, 对挂载元素进行审核:

<html>
  <body>
    <div>Hello</div>
  </body>
</html>

可以发现idappp元素已经被MyComponent组件模版被替换掉了,此时的挂载点只是一个被将要被替换的占位符。如果此时挂载点为body元素或者html元素的情况,bodyhtml元素同样会被替换掉,此时html页面则不是一个标准规定的html标准体了。浏览器同样不会对此进行解析。

const options = this.$options

声明options变量,把初始化合并到实列对象上的$options对象赋值给options变量。

if (!options.render) {
}
return mount.call(this, el, hydrating)

判断options选项中是否有render函数,既渲染函数。有则直接调用运行版本的$mount函数,在之前运行时的$mount函数已经缓存给了mount变量。则直接通过mountComponent方法进行渲染挂载,由此可知,渲染整个DOM结构需要render渲染函数做支撑。render函数到底是从那里来?为什么有render函数可以直接开始调用mountComponent方法进行渲染。

  1. 根据官方文档进行提示进行手动编写在render选项中
  2. 通过打包过具通过vue-loadertemplate模版进行转化成render函数。
  3. 通过runtime-with-complier版本,经过compileToFunctions函数把template模版编译成render函数。

ast语法转化入口解析。

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

在没有render选项的情况。通过template或者el两者任意一个选项让模版进行转化成render渲染函数,声明template变量,通过options.template选项赋值给template变量。

  1. 先对template选项获取模版,当既有template选项时,也有el选项时,template则优先作为转化render函数的模版, el则作为实例的挂载点。

当template是字符串的时候

如果有template,再判断template是否是字符串。字符串是否以id为元素选择器。

字符串是元素选器

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

通过charAt方法匹配是否字符串首字符是#号,调用idToToTemplate函数,把元素选择器传入传为参数。

idToTemplate(template)

描述:

通过元素选择符获取到元素,通过获取到的元素拿到内部的innerHTML

参数

template: 元素选择符

实现原理:

idToTemplate内部通过闭包进行缓存转化后的模版。当执行idToTemplate的时候引用了cached执行的返回函数

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

传入了一个参数为元素选择器的字符,内部则是利用query函数获取对应转化后的元素。如果转化成功后返回元素内的innerHTML 关于cached函数在合并策略中已经讲解过了。原理就是利用闭包的原理,传入一个纯函数,如果缓存对象上有已经缓存过的属性。因为id选择器是唯一的,根据id选择器转化后的属性和值会记录在缓存对象上,一旦再次获取同样的选择器的元素,可以通过缓存对象进行比对,一旦比对成功,则直接从缓存中获取。

if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }

在开发环境中,如果通过id选择器并没有获取到对应的元素时。则会报错一个警告

当template是node节点时

tempalte还可以直接传入node节点,请看DEMO

<div id="app">
  <div>
     <p>{{a}}</p>
  </div>
 </div>
</body>

<script>
new Vue({
  el: '#app',
  template: document.querySelector('#app'),
  data: {
    a: 10
  }
})
</script>

如果是元素节点的分支的源码

else if (template.nodeType) {
        template = template.innerHTML
}

此时通过demo可以看出此时tempalte传入的是一个元素节点,代码运行时会跑入上面的分支代码,直接获取元素的innerHTML作为模版

template既不是字符串也不是无素节点处理警告

if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this

如果template既不是字符串也不是元素节点并在开发环境下,会报一个警告,请检查template选项

是字符串模版

template: `<div>
                <p>{{a}}</p>
            </div>`,

在以上的可能性都已经分析过了。如果以前的情况都通过,则用转化为的template模版,但是还有一种最常用的情况,当处理为字符串的时候,字符串开头并不是以#开头,直接默认认为是开发者用模版字符串写入。以上这样子的写法同样生效。

ast解析转render渲染函数

const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

在各种情况下template成功获取之后。通过compileToFunctions进行ast语法树转换,得到render泻染函数,赋值到实例的$options选项上。

最后调用mount缓存函数进行挂载,前面提到过如果同时有templateel选项,此时el只会是一个挂载点。会优先根据template选项生成真正的模版。

如果只存在el选项时,并没有template选项。el既作为挂载点,也作为模版

如果没有template选项时,模版只会通过以下代码进行转换

else if (el) {
      template = getOuterHTML(el)
}

通过getOuterHTML方法传入el参数获取template模版。

通过el挂载并生成模版的DEMO:

 <div id="app">
  <div>
     <p>{{a}}</p>
  </div>
 </div>
</body>

<script>
new Vue({
  el: document.querySelector('#app'),
  data: {
    a: 10
  }
})
</script>

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

首先判断el元素是否有outerHTML,正常的元素的outerHTML则是传入el元素自身。说明有些情况下元素会没有outerHTML,从注示上可以看于对于ie浏览器中SVG无素是获取不到outerHTML,此时就需要通过一个hack处理,创建一个containerdiv的空元素,深度克隆el元素,通过appendChild方法把克隆后的el元素添加到cantainer容器中,成为子节点。最后返回的container中的innerHTML,这样的操作等同于获取了元素的outerHTML.

在只有el的情况下,又作为template转化的模版,也要作为mountComponent函数的替换元素的情况下,el必须是一个Dom元素。通过el获取到了template模版之后,调用compileToFunctions转化成render函数。最后调用缓存的mount函数进行渲染Dom结构体。