深度学习vue系列 —— vm.$mount挂载函数

2,145 阅读7分钟

简要描述

  • 在不同的构建版本中,vm.$mount的表现都不一样,其差异要体现在完整版本vue.js和只包含运行时版本vue.runtime.js之间。

  • 完整版本与只包含运行时版本之间的差异在于是否有编译器。

  • 在完整的构建版本中,vm.$mount首先会检查templateel选项所提供的模板是否已经转换成渲染函数(render函数)。如果没有,则立即进入编译过程,将模板编译成渲染函数,完成之后再进入挂载与渲染的流程中。

  • 只包含运行时版本的vm.$mount没有编译步骤,默认实例上已经存在渲染函数,如果不存在会设置一个。并且这个渲染函数在执行时会返回一个空节点的Vnode,来保证执行时不会因为函数不存在而报错。

$mount()的思路就是,判断用户传入的option有没有render函数,

  • 有就走运行时版本

  • 没有就自动生成render函数,然后在执行运行时版本(其实就是编译时版本,比运行时版本多了异步生成render函数的步骤。)

执行运行时版本的时候

  • 通过render()或得Vnode
  • Vnode传入_update()实现渲染

一、使用方法

vm.$mount([elementOrSelector])

参数

- {Element | string} [elementOrSelector]
- {boolean} [hydrating]

返回值

vm - 实例自身

用法

  • 如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例。

  • 如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且你必须使用原生 DOM API 把它插入文档中。

  • 这个方法返回实例自身,因而可以链式调用其它实例方法。

示例:

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

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

// 同上
new MyComponent({ el: '#app' })

// 或者,在文档之外渲染并且随后挂载
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)

二、vm.$mount的实现原理

只包含运行时版本的vm.#mount的实现原理

这里的$mount是一个public mount method。

// public mount method
// el: 可以是一个字符串或者Dom元素
// hydrating 是Virtual DOM 的补丁算法参数
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 判断el, 以及宿主环境, 然后通过工具函数query重写el。
  el = el && inBrowser ? query(el) : undefined
  // 执行真正的挂载并返回
  return mountComponent(this, el, hydrating)
}

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元素(只考虑合并的情况)。

mountComponent方法: 真正执行绑定组件

export function mountComponent(vm, el){
	if(!vm.$options.render){
    	// 不存在渲染函数 设置默认渲染函数createEmptyVNode,
        // 该渲染函数执行后,会返回一个注释类型的VNode节点
    	vm.$options.render = createEmptyVNode;
        if(process.env.NODE_ENV !== 'production'){
            // 在开发环境发出警告
        }
        // 触发生命周期钩子
        callHook(vm, 'beforeMount');
        // 挂载
        vm._watcher = new Watcher(vm, ()=>{
        	vm._update(vm._render())
        },noop);
        // 触发生命周期钩子
        callHook(vm, 'mounted');
        return vm;
    }
}
  • vm._updtae作用:调用虚拟DOM中的patch方法来执行节点的对比与渲染操作,

  • vm._render作用:执行渲染函数,得到一份新的VNode节点树。

  • vm._update(vm._render())作用:先调用渲染函数得到一份最新的VNode节点树,然后通过vm._update方法对最新的VNode和上一次渲染用到的旧VNode进行对比并跟新DOM节点。简单来说就是执行了渲染操作。

挂载是持续性的,而持续性的关键就在于new Watcher ,当数据发生变化时,watcher会一次又一次的执行函数进入渲染流程,如此反复,这个过程会持续到实例被销毁。

完整版vm.#mount的实现原理

这个版本的$mount会被进行重写。并且增加了把template模板转成render渲染函数。

缓存挂载的$mount

在Vue原型上的$mount方法被一个新的方法覆盖。新方法中会调用原始的方法,这种做法通常被称为函数劫持。通过函数劫持,可以在原始功能上新增一些其他功能。

const mount = vue.prototype.#mount;
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
	// 通过query方法重写el(挂载点: 组件挂载的占位符)
  	el = el && query(el)
 	return mount.call(this, el, hydrating)
}

使用query获取DOM元素,对el参数通过query函数进行获取指入的挂载点,获取的Dom元素赋值给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
  }
}

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

这里的判断挂载点是否是<body>元素或者是<html>元素,在生产环境下会报出警。不要挂载到htmlbody元素上。

const mount = vue.prototype.#mount;
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
	// 通过query方法重写el(挂载点: 组件挂载的占位符)
  	el = el && query(el)
    // 提示不能把body/html作为挂载点, 开发环境下给出错误提示
  	// 因为挂载点是会被组件模板自身替换点, 显然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
  	}
    
 	return mount.call(this, el, hydrating)
}

编译器

  • 声明options变量,把初始化合并到实列对象上的$options对象赋值给options变量。
  • 判断options选项中是否有render函数,既渲染函数。
    • 有则直接调用运行版本的$mount函数,在之前运行时的$mount函数已经缓存给了mount变量。则直接通过mountComponent方法进行渲染挂载,由此可知,渲染整个DOM结构需要render渲染函数做支撑。
    • 没有render函数时候优先考虑template属性,如果template选项不存在,那么使用el元素的getOuterHTML作为模板内容
const mount = vue.prototype.#mount;
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
	// 通过query方法重写el(挂载点: 组件挂载的占位符)
  	el = el && query(el)
    // 提示不能把body/html作为挂载点, 开发环境下给出错误提示
  	// 因为挂载点是会被组件模板自身替换点, 显然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
  	}
    
    // $options是在new Vue(options)时候_init方法内执行.
  	// $options可以访问到options的所有属性如data, filter, components, directives等
  	const options = this.$options
  	// resolve template/el and convert to render function
  	// 如果包含render函数则执行跳出,直接执行运行时版本的$mount方法
  	if (!options.render) {
  		// 没有render函数时候优先考虑template属性
    	let template = options.template
    	if (template) {
        
        }else if (el) {
      		// 如果template选项不存在,那么使用el元素的outerHTML 作为模板内容
      		template = getOuterHTML(el)
    	}
  	}
    
 	return mount.call(this, el, hydrating)
}

判断有template属性时,ast语法转化入口解析。

if (template) {
	// template存在且template的类型是字符串
    if (typeof template === 'string') {
    	if (template.charAt(0) === '#') {
         	// template是ID
          	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 的类型是元素节点,则使用该元素的 innerHTML 作为模板
        	template = template.innerHTML
      } else {
        	// 若 template既不是字符串又不是元素节点,
            // 那么在开发环境会提示开发者传递的 template 选项无效
        if (process.env.NODE_ENV !== 'production') {
        	warn('invalid template option:' + template, this)
        }
        return this
      }
 }
  • 判断template是否是字符串。
    • 字符串是否以id为元素选择器,通过charAt方法匹配是否字符串首字符是#号,调用idToToTemplate函数,把元素选择器传入传为参数。
  • 判断template不是字符串时,template是否为元素节点。
    • 使用该元素的 innerHTML 作为模板
  • 判断template既不是字符串也不是无素节点处理警告。

idToTemplate(template)解析

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

参数:template 元素选择符

实现原理:idToTemplate内部通过闭包进行缓存转化后的模版。当执行idToTemplate的时候引用了

cached执行的返回函数。通过query获取该ID获取DOM并把该元素的innerHTML作为模板

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

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

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

else if (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 {
    // fix IE9-11 中 SVG 标签元素是没有 innerHTML 和 outerHTML 这两个属性
    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。

ast解析转render渲染函数

if (template) {
	/* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    	mark('compile')
    }
    // 获取转换后的render函数与staticRenderFns,并挂在$options上
    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

    /* istanbul ignore if */
    // 用来统计编译器性能, config是全局配置对象
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
    }
}
  • 在各种情况下template成功获取之后。通过compileToFunctions进行ast语法树转换,得到render泻染函数,赋值到实例的$options选项上。
  • 最后调用mount缓存函数进行挂载,前面提到过如果同时有templateel选项,此时el只会是一个挂载点。会优先根据template选项生成真正的模版。

compileToFunctions函数部分解析

compileToFunctions函数是将模板编译成代码字符串并将代码字符串转换成渲染函数的过程。

function compileToFunctions(template, options, vm){
	options = extend({}, options);
    // 检查缓存
    const key = options.delimiters
   	? String(options.delimiters) + tempalte 
    : template;
    if(cache[key]){
    	return cache[key]
    }
    // 编译
    const compiled = compile(template, options);
    // 将代码字符串转换为函数
    const res = {};
    res.render = createFunction(compiled.render);
    return (cache[key] = res)
}
function createFunction(code){
	return new Function(code)
}
  • 首先,将options属性混合到空对象中,其目的是让options称为可选参数。

  • 检查缓存中是否已经存在编译后的模板。如果模板已经被编译,就会直接返回缓存中的结果,不会重复编译,保证不做无用功来提升性能。

  • 调用compile函数来编译模板,将模板编译成代码字符串并存储在compiled中的render属性中。

  • 调用createFunction函数将代码字符串转换成函数。其实现原理很简单,使用new Function(code)就可以完成。

完整版本的$mount方法实现

// 缓存运行时候定义的公共$mount方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
	// 通过query方法重写el(挂载点: 组件挂载的占位符)
  	el = el && query(el)

  	/* istanbul ignore if */
  	// 提示不能把body/html作为挂载点, 开发环境下给出错误提示
  	// 因为挂载点是会被组件模板自身替换点, 显然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
  	}
  	// $options是在new Vue(options)时候_init方法内执行.
  	// $options可以访问到options的所有属性如data, filter, components, directives等
  	const options = this.$options
  	// resolve template/el and convert to render function
  
  	// 如果包含render函数则执行跳出,直接执行运行时版本的$mount方法
  	if (!options.render) {
    	// 没有render函数时候优先考虑template属性
    	let template = options.template
    	if (template) {
      		// template存在且template的类型是字符串
      		if (typeof template === 'string') {
        		if (template.charAt(0) === '#') {
            		// template是ID
         	 		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 的类型是元素节点,则使用该元素的 innerHTML 作为模板
        		template = template.innerHTML
      		} else {
       			// 若 template既不是字符串又不是元素节点,
        		// 那么在开发环境会提示开发者传递的 template 选项无效
        		if (process.env.NODE_ENV !== 'production') {
         			warn('invalid template option:' + template, this)
        		}
        		return this
      		}
    	} else if (el) {
      		// 如果template选项不存在,那么使用el元素的outerHTML 作为模板内容
      		template = getOuterHTML(el)
    	}
    	// template: 存储着最终用来生成渲染函数的字符串
    	if (template) {
    		/* istanbul ignore if */
      		if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        		mark('compile')
      		}
      		// 获取转换后的render函数与staticRenderFns,并挂在$options上
      		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

      		/* istanbul ignore if */
      		// 用来统计编译器性能, config是全局配置对象
      		if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        		mark('compile end')
        		measure(`vue ${this._name} compile`, 'compile', 'compile end')
      		}
   		}
 	}
  	// 调用之前说的公共mount方法
  	// 重写$mount方法是为了添加模板编译的功能
  	return mount.call(this, el, hydrating)
}