重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的

正文

本篇过一下 Vue 的实例挂载,也就是 ​​vm.$mount​​ 都做了什么事情。

打开 ​​src/platforms/web/entry-runtime-with-compiler.js​​​ 可以看到有一个 ​​Vue.prototype.$mount​​ 方法:

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

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

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

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)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}

const { render, staticRenderFns } = compileToFunctions(template, {
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')
}
}
}
return mount.call(this, el, hydrating)
}

可以看到 ​​Vue.prototype.$mount​​​ 赋值给了 ​​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 && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}

那为什么又重新定义了一遍呢,是因为 Vue 有 ​​Runtime-Complier​​​ 版本和 ​​Runtime-Only​​ 版本:


Runtime-Only是编译阶段运行,也就是使用 webpack 的 ​​vue-loader​​,把 .vue 文件编译成JavaScript 使用。

Runtime-Complier是通过页面内的 ​​template​​​ 编译成 ​​render​​ 函数,最终渲染到页面上。


最开始的 ​​Vue.prototype.$mount​​​ 是给 ​​Runtime-Only​​​ 版本使用的,所以在使用 ​​Runtime-Complier​​ 版本的时候,需要把它给重写。

还记得 Vue在初始化的时候有一个 ​​vm.$mount(vm.$options.el)​​ 么:

// src/core/instance/init.js
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}

这个里面的 ​​vm.$mount(vm.$options.el)​​​ 实际上就是调用的重写之后的 ​​$mount​​ 函数。来看下这个函数都做了事情:

首先对传入的 ​​el​​​ 参数进行处理,它可以是一个 String,也可以是一个 Element ,之后调用了 ​​query​​ 方法,看下这个 query 方法做了什么事情:

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

它调用了原生方法 ​​document.querySelecto​​​ 来获取传入的 ​​el​​​,如果 el 是一个字符串,就调用这个原生方法获取 dom,如果找不到就返回一个空的 div,如果 ​​el​​​ 是个 dom 对象,就直接返回这个 dom 对象。此时返回的 ​​el​​ 一定是一个 dom 对象。

接着,拿到这个 ​​el​​​ 以后,判断 ​​el​​​ 是不是 body 或者文档标签,如果是,就报一个错,说不可以把 Vue 挂载到 ​​<html>​​​ 或 ​​<body>​​ 上。

因为它是会覆盖的,如果可以挂在到 <html> 或者 ​<body>​上的话,就会把整个 body 给替换掉! 所以我们一般使用一个 id 为 app 的方式去使用它

然后拿到 ​​options​​​,紧接着有一句 ​​if (!options.render)​​​,意思是判断有没有定义 ​​render​​​ 方法,接着判断有没有 ​​template​​​,以下写法定义一个 ​​template​​ 是可以的:

new Vue({
el: "#app",
template: ``,
data(){
return{
name: "abc"
}
}
})

继续看它的源码逻辑,如果 ​​template​​​ 是一个字符串,就对它做一点处理,如果是 ​​template.nodeType​​​ 也就是一个dom对象的话,就 ​​innerHTML​​​, 否则就会走一个 ​​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
}
}

​getOuterHTML​​​ 判断传入的 ​​el​​​ 有没有 ​​outerHTML​​​ 方法,没有就把 ​​el​​​ 外面包一层 div,然后 ​​innerHTML​​,此时的 template 最终是一个字符串。

接着开始进行编译阶段,判断有没有 ​​template​​​,大致就是拿到一个 ​​complieToFunctions​​​ 的​​render​​​ 函数,和一个 ​​staticRenderFns​​ 函数,并且赋值。

整体过一遍这个 ​​$mount​​ 做了事情:


首先对 ​​el​​​ 进行一个解析,然后看看有没有 ​​render​​​ 方法,没有的话就转化成一个 ​​template​​​,然后这个 ​​template​​​ 最终通过编译成一个 ​​render​​ 方法。

即 Vue 只认 ​​render​​​ 函数,如果有 ​​render​​​ 函数,就直接 ​​return mount.call(this, el, hyrating)​​​,return出去,如果没有 ​​render​​​ 函数,就通过一系列操作,把 ​​template​​​ 转化编译成 ​​render​​ 函数。


此刻, ​​render​​​ 函数一定存在,然后 return 的 ​​mount.call(this, el, hyrating)​​​ 中的 ​​mount​​​ 就是之前缓存的 ​​mount​​,也就是:

const mount = Vue.prototype.$mount

中的 ​​mount​​​,然后进行最开始的 ​​Vue.prototype.$mount​​ 方法:

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

接着进行 ​​mountComponent​​​ 方法,定义是在:​​src/core/instance/lifecycle.js​​ 中:

export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
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) !== '#') ||
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 */
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) {
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
}

现在开始分析这个 ​​mountComponent​​ 做了什么事情:

首先把 ​​el​​​ 缓存给 ​​vm.$el​​​,然后判断有没有 ​​render​​​ 函数,如果没有或者没有将 ​​template​​​ 正常转为 ​​render​​​,就定义一个 ​​createEmptyVNode​​​,一个虚拟dom。接着判断在开发环境下的 ​​template​​​ 的第一个不是 ​​#​​,就报一个错。

简单说就是开发过程使用 ​​Runtime-Only​​​ 版本的 Vue,然后使用了 ​​template​​​,但是没有使用 ​​render​​ 函数,就会报一个错:


You are using the runtime-only build of Vue where the templatecompiler is not available. Either pre-compile the templates intorender functions, or use the compiler-included build.


或者使用了 ​​Runtime-Complier​​​ 版本的Vue, 没有写 ​​template​​​ ,或者没有写 ​​render​​ 函数,就会报一个错:


Failed to mount component: template or render function not defined.


这个错应该很熟悉吧。就是没有正确的 ​​render​​​ 函数,所以报这个错,Vue 最终只认 ​​render​​ 函数。

接着,定义了一个 ​​updateComponent​​​,有关 ​​mark​​​ 和 ​​performance​​ 的判定先忽略,它是一些性能埋点的校验,一般情况下直接走最后:

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

调用了 ​​vm._update​​​ 方法,第一个参数是通过 ​​render​​​ 渲染出来一个 ​​VNode​​,第二个参数是一个服务端渲染的参数,先忽略,默认为false。

紧接着后面,调用了一个 ​​new Watcher​​​ 函数,它是一个 ​​渲染watcher​​​,记住这个点,一般在写代码的时候,watch被用来监听一些东西,所以这个 ​​new Watcher​​ 是一个和监听有关的强相关的一个类,也就是一个 观察者模式。代码中可以有很多自定义watcher,内部逻辑会有一个 ​​渲染watcher​​​。来看下这个 渲染watcher是干嘛的,在 ​​src/core/observer/watcher.js​​ 里,一个特别大的 watcher 定义:

/**
* A watcher parses an expression, collects dependencies,
* and fires callback when the expression value changes.
* This is used for both the $watch() api and directives.
*/
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
computed: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
dep: Dep;
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,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.computed = !!options.computed
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.computed = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.computed // for computed watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
// parse expression for getter
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
if (this.computed) {
this.value = undefined
this.dep = new Dep()
} else {
this.value = this.get()
}
}

/**
* Evaluate the getter, and re-collect dependencies.
*/
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

/**
* Add a dependency to this directive.
*/
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

/**
* Clean up for dependency collection.
*/
cleanupDeps () {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}

/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.computed) {
// A computed property watcher has two modes: lazy and activated.
// It initializes as lazy by default, and only becomes activated when
// it is depended on by at least one subscriber, which is typically
// another computed property or a component's render function.
if (this.dep.subs.length === 0) {
// In lazy mode, we don't want to perform computations until necessary,
// so we simply mark the watcher as dirty. The actual computation is
// performed just-in-time in this.evaluate() when the computed property
// is accessed.
this.dirty = true
} else {
// In activated mode, we want to proactively perform the computation
// but only notify our subscribers when the value has indeed changed.
this.getAndInvoke(() => {
this.dep.notify()
})
}
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run () {
if (this.active) {
this.getAndInvoke(this.cb)
}
}

getAndInvoke (cb: Function) {
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
this.dirty = false
if (this.user) {
try {
cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
cb.call(this.vm, value, oldValue)
}
}
}

/**
* Evaluate and return the value of the watcher.
* This only gets called for computed property watchers.
*/
evaluate () {
if (this.dirty) {
this.value = this.get()
this.dirty = false
}
return this.value
}

/**
* Depend on this watcher. Only for computed property watchers.
*/
depend () {
if (this.dep && Dep.target) {
this.dep.depend()
}
}

/**
* Remove self from all dependencies' subscriber list.
*/
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}

看下传进去的参数都有哪些:​​vm​​​, ​​expOrFn​​​,​​cb​​​,​​option​​​,​​isRenderWatcher​​。

在上面,传入的参数有:

vm,  // vm实例

updateComponent, // vm._update方法

noop, // 一个空函数

{ //一个生命周期函数
before () {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate')
}
}
},

true // 是不是一个渲染watcher

接着,判断 ​​isRenderWatcher​​​ 是不是 true,也就是说,传进来的是不是一个 渲染watcher,如果是,就在 ​​vm​​​ 下添加一个 ​​_watcher​​​,然后把所有东西都 push 这个 ​​_watcher​​​ 里面。​​options​​​ 的判定先忽略,后面,定义了一个 ​​expression​​​,如果是在开发环境就 ​​expOrFn.toString()​​。

后面,判断 ​​expOrFn​​​ 是不是一个函数,如果是,就赋值给 ​​getter​​​,否则调用 ​​parsePath​​​ 然后赋值给 ​​getter​​​。后面的 ​​this.computed​​​ 是有关计算属性的设置,先忽略。到 ​​value = this.getter.call(vm, vm)​​​ 这一步,这句会把刚才赋值的 ​​this.getter​​​ 调用,也就是刚才传入的 ​​updateComponent​​​ 被调用执行,也就是 ​​vm._update(vm._render(), hydrating)​​​ 会执行。​​vm._update​​​ 和 ​​vm._render​​ 就是 最终挂载到真实dom 的函数。

首先执行 ​​vm._render​​​,还记得么,上面的 ​​render​​​ 最终生成了一个 VNode,然后调用 ​​_update​​,把 VNode 传进去。

至此,Vue实例就挂载好了。

总体来捋一遍:

Vue 实例挂载是通过 ​​vm.prototype.$mount​​​ 实现的,先获取 ​​template​​​, ​​template​​ 的情况大致分为三种:

  1. 直接写 ​​template​​​​if (typeof template === 'string') { ​
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
)
}
}
}
  1. ​template​​​ 是一个dom
if (template.nodeType) {
template = template.innerHTML
}
  1. 以及不写 ​​template​​​,通过 ​​el​​​ 去获取 ​​template​​​​if (el) { ​
if (el) {
template = getOuterHTML(el)
}

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

接着把 ​​template​​​ 通过一堆操作转化成 ​​render​​​ 函数,然后调用 ​​mountComponent​​​ 方法,里面定义了 ​​updateComponent​​ 方法:

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

然后将 ​​updateComponent​​ 扔到 渲染watcher(new Watcher) 里面,从而挂载成功!


​updateComponent​​​ 函数其实是执行了一次真实的渲染,渲染过程除了首次的 ​​_render​​​ 和 ​​_update​​​,在之后更新数据的时候,还是会触发这个 ​​渲染watcher(new Watcher)​​​,再次执行 ​​updateComponent​​​,它是一个监听到执行的过程,当数据发生变化,在修改的时候,入口也是 ​​updateComponent​​。