
重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
正文
本篇过一下 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 挂载到 或 上。
因为它是会覆盖的,如果可以挂在到 或者 上的话,就会把整个 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
的情况大致分为三种:
- 直接写
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
)
}
}
}
template
是一个dom
if (template.nodeType) {
template = template.innerHTML
}
- 以及不写
template
,通过 el
去获取 template
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
扔到 渲染watcher(new Watcher) 里面,从而挂载成功!
updateComponent
函数其实是执行了一次真实的渲染,渲染过程除了首次的 _render
和 _update
,在之后更新数据的时候,还是会触发这个 渲染watcher(new Watcher)
,再次执行 updateComponent
,它是一个监听到执行的过程,当数据发生变化,在修改的时候,入口也是 updateComponent
。