重学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 = () => { vm._update(vm._render(), hydrating) }
然后将 updateComponent
扔到 渲染watcher(new Watcher) 里面,从而挂载成功!
updateComponent
函数其实是执行了一次真实的渲染,渲染过程除了首次的_render
和_update
,在之后更新数据的时候,还是会触发这个渲染watcher(new Watcher)
,再次执行updateComponent
,它是一个监听到执行的过程,当数据发生变化,在修改的时候,入口也是updateComponent
。