小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金
Vue
Vue 中我们是通过 $mount 实例方法去挂载 vm 的,$mount 方法在多个文件中都有定义
如:
src/platform/web/entry-runtime-with-compiler.jssrc/platform/web/runtime/index.jssrc/platform/weex/runtime/index.js因为$mount这个方法的实现是和平台、构建方式都相关的。接下来我们重点分析带compiler版本的$mount实现,因为抛开webpack的vue-loader,我们在纯前端浏览器环境分析Vue的工作原理,有助于我们深入理解原理。
我会在代码中给大家进行注释,在代码中,方便大家阅读理解,如果觉得不行的话,请留言哈!马上改!
Vue.prototype.$mount
地址:src\platforms\web\entry-runtime-with-compiler.js
我们查看的版本是 runtime + compiler版本
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
代码块1
代码块2
代码块3
}
首先 我们看到如下的操作
代码块1
解析我放在代码块中啦!
el = el && query(el)
这里我们调用了 query 方法 ,
query 方法
路径:src\platforms\web\util\index.js
/**
* 如果元素选择器还不是元素,则查询该元素选择器。
*/
export function query (el: string | Element): Element {
// 根据上面 Vue.prototype.$mount定义的时候给 el的类型定义
// 这里要进行一些判断 根据el的类型进行对于的动作
// 当然还有 querySelectorAll() 但是不符合我们的需求
if (typeof el === 'string') {
// 如果是字符串 我们就调用 document.querySelector
// 查找 匹配指定选择器的第一个元素
const selected = document.querySelector(el)
if (!selected) {
// 如果找不到对应的挂载点 就会报个错
process.env.NODE_ENV !== 'production' && warn(
'Cannot find element: ' + el
)
// 并且 返回一个空的div
return document.createElement('div')
}
// 这个就是找到的值 不多加赘述
return selected
} else {
// 如果直接是dom对象 就直接返回这个对象了
return el
}
}
从上看 query这个方法还是非常简单的,让我们接着回到代码里面
代码块2
解析我放在代码块中啦!
// 在经过代码块1的操作之后 el就是我们需要的 DOM 对象了
// 这里做了一个简单的判断
// 如果el 是 <body> 元素 或者 <html> 元素 的话 会给用户一个报错
if (el === document.body || el === document.documentElement) {
// ps: 这里的报错内容是 你不能把vue挂载到 <html> 或者 <body> 元素上
// 因为他是会覆盖的你这样操作的话 整个html就不对了
// 所以我们一般都是搞个 <div id="app"></div> 这样的给vue去进行挂载
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
代码块3(最长的来了)
解析我放在代码块中啦!
// $options 获取到自定义属性
const options = this.$options
// 这里主要是 解析模板/el 并转换为渲染函数
// 接下来的是很关键的逻辑
if (!options.render) {
// 如果自定义属性中没有 render 方法这里就会读取template
// 是否有template字符串
let template = options.template
if (template) {
// 如果 template 存在 且 为 string 类型的话,读取template
if (typeof template === 'string') {
// charAt 指定位置的字符 判断不多做赘述
if (template.charAt(0) === '#') {
// idToTemplate 方法在下面进行详解
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 是个dom对象的话
// 我们这里获取的就是他的 innerhtml
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 在我们没有定义 template 的时候,这个时候我们会根据
// query中传入的el进行挂载点的获取
// getOuterHTML 详解 在下方
// getOuterHTML返回的是一个字符串
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 编译章节 之后进行详细解答
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 */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
idToTemplate 方法
解析我放在代码块中啦!
// 根据下面的方法 cached 这边返回了对应的缓存给内部使用 更高效的进行数据的获取
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
cached方法
解析我放在代码块中啦!
// src\shared\util.js
// 乍一看,就是一个高阶函数而已。但是仔细揣摩,方才发现尤大的用意,顿觉精妙。
// 高阶函数,简单的说一般都是一个函数,参数是函数,返回值也是函数。
// 一般都用到了闭包作为公共变量或缓存。
export function cached<F: Function> (fn: F): F {
const cache = Object.create(null)
// const idToTemplate = cached(id => {}) 中传递来的id
return (function cachedFn (str: string) {
const hit = cache[str]
return hit || (cache[str] = fn(str))
}: any)
}
getOuterHTML 方法
解析我放在代码块中啦!
function getOuterHTML (el: Element): string {
// 根据当前挂载dom对象进行对应的判断
// outerHTML 除了包含innerHTML的全部内容外, 还包含对象标签本身
if (el.outerHTML) {
return el.outerHTML
} else {
// 创建一个空的div
const container = document.createElement('div')
// cloneNode() 方法创建节点的拷贝,并返回该副本。
// cloneNode() 方法克隆所有属性以及它们的值。
// 如果您需要克隆所有后代,请把 deep 参数设置 true,否则设置为 false。
container.appendChild(el.cloneNode(true))
// 然后返回 从对象的起始位置到终止位置的全部内容, 不包括Html标签。
return container.innerHTML
}
}
代码块4
解析我放在代码块中啦!
在经过上面三个代码块的执行之后,我们就会执行 mount 这里的方法
return mount.call(this, el, hydrating)
但是 注意了 要敲黑板了
Vue源码中src\platforms\web\entry-runtime-with-compiler.js 第17行有以下的这个操作
// 这里 mount 又缓存了 Vue.prototype.$mount
const mount = Vue.prototype.$mount
上面操作 是将 Vue.prototype.$mount 进行了缓存 , 而不是我们修改之后的方法参数,是不是很厉害
只能说一句尤大大牛逼啊
Vue.prototype.$mount
地址:src\platforms\web\runtime\index.js
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 这里对el进行了一次判断
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
Vue.prototype.$mount
解析我放在代码块中啦!
地址:src\core\instance\lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 首先我们先将 el 这个dom元素 用 vm.$el 进行缓存操作
vm.$el = el
// 这里判断了 render 函数是否被正确赋值或填写
if (!vm.$options.render) {
// 如果没有写 render 就创建一个空Vnode
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
)
}
}
}
callHook(vm, 'beforeMount')
// 定义一个方法
let updateComponent
/* istanbul ignore if */
// 这个是vue的性能埋点 之后我们查询一下看看
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)
}
}
// 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 */)
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 类(下次说哈!)
解析我放在代码块中啦!
地址: src\core\observer\watcher.js
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
//
vm: Component,
expOrFn: string | Function,
// 回调
cb: Function,
// 配置
options?: ?Object,
// 是否渲染watch的配置
isRenderWatcher?: boolean
) {
具体内容我们之后进行介绍 嘿嘿
}
总结
在挂载之后 render 函数 可以由用户自己进行书写,在render 函数 在用户没有进行书写的情况下,我们会进行判断使用 ,template是否存在,在没有 template的情况下,render 函数 由内部函数自己生成
引用一下 vue官网的话
提供一个在页面上已存在的
DOM元素作为Vue实例的挂载目标。可以是CSS选择器,也可以是一个HTMLElement实例。 在实例挂载之后,元素可以用vm.$el访问。 如果在实例化时存在这个选项,实例将立即进入编译过程,否则,需要显式调用vm.$mount()手动开启编译。 提供的元素只能作为挂载点。不同于Vue 1.x,所有的挂载元素会被Vue生成的DOM替换。因此不推荐挂载root 实例到<html>或者<body>上。 如果render函数和template property都不存在,挂载DOM元素的HTML会被提取出来用作模板,此时,必须使用Runtime + Compiler构建的Vue库。
和大家说几句话
我会在代码中给大家进行注释,在代码中,方便大家阅读理解,如果觉得不行的话,请留言哈!马上改!