DOM元素挂载
实例初始化完成后,会调用$mount
开始DOM元素挂载。这个阶段会触发两个钩子函数:beforeMount
和mounted
。
// src/platforms/web/entry-runtime-with-compiler.js Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
// src/platforms/web/runtime/index.js // 覆写$mount方法 const mount = Vue.prototype.$mount Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) const options = this.$options if (!options.render) { let template = options.template // 选项中指定了template if (template) { if (typeof template === 'string') { // 如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板 if (template.charAt(0) === '#') { template = idToTemplate(template) if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { // 虽然在文档中并未说明,但template还可以指定一个DOM元素作为模板 template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { // 选项中指定了el template = getOuterHTML(el) } // 将模板解析成render函数 if (template) { 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 } } // ...省略部分代码 return mount.call(this, el, hydrating) }
// src/core/instance/lifecycle.js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { // 将对DOM元素的引用保存到$el vm.$el = el // ...省略部分代码 // 调用beforeMount前需要执行模板编译逻辑 callHook(vm, 'beforeMount') let updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false if (vm.$vnode == null) { // 标记为已挂载 vm._isMounted = true // 触发mounted事件 callHook(vm, 'mounted') } return vm }
beforeMount
我们知道vue需要render函数来生成vnode。但是在实际开发中,基本都是通过template
和el
来指定模板,很少直接提供一个render函数。因此在触发beforeMount
前,vue最重要的一个工作就是将HTML模板编译成render函数。beforeMount
钩子函数被调用时,我们尚不能访问DOM元素。
mounted
每个vue实例都会对应一个render watcher
。render watcher
会创建vnode(通过_render
方法),并对vnode进行diff后,创建或者更新DOM元素(通过_update
方法)。对于初次渲染来说,当创建完DOM元素后,把DOM树的根元素插入到body中,然后触发mounted
钩子函数。此时,在钩子函数中可以对DOM元素进行操作了。
更新
实例完成初始化和挂载之后,如果由于用户的交互导致实例的状态发生了变化,实例将进入更新阶段。例如在代码中执行 this.msg = 'update msg'
,vue实例需要更新DOM元素。
实例的更新是异步的。前面提到过,render watcher
会负责调度程序创建vnode、创建更新DOM元素。当数据发生变化后,vue不会立即启动DOM的更新,而是先把实例对应的render watcher
添加到一个队列中。然后在下一个事件循环中,统一执行DOM更新,清空队列。也就是调用下面代码中的flushSchedulerQueue
函数。
此阶段会触发的钩子是:beforeUpdate
和updated
。
/** * 清空所有的队列并执行watcher的更新逻辑 */ function flushSchedulerQueue () { flushing = true let watcher, id // 队列按照watcher的id升序排序,目的是确保: // 1. 组件总是从父向子进行更新 // 2. 用户创建的watcher先于渲染watcher更新 // 3. 如果组件在父组件的watcher运行时被销毁,该组件的watcher可以跳过处理 queue.sort((a, b) => a.id - b.id) for (index = 0; index < queue.length; index++) { watcher = queue[index] // 调用watcher.before,触发beforeUpdate钩子 if (watcher.before) { watcher.before() } id = watcher.id has[id] = null // 更新dom watcher.run() } const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() resetSchedulerState() // 触发activated钩子 callActivatedHooks(activatedQueue) // 触发updated钩子 callUpdatedHooks(updatedQueue) } // 触发updated钩子 function callUpdatedHooks (queue) { let i = queue.length while (i--) { const watcher = queue[i] const vm = watcher.vm if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) { callHook(vm, 'updated') } } }