Vue 渲染过程
Vue 的执行过程主要分两大阶段:Vue 自身的初始化阶段 和 实例的生命周期管理阶段。
当通过 script 脚本或者 import Vue from 'vue' 引入 Vue 时,Vue 框架本身的代码会被执行,这一阶段的作用是对框架自身进行初始化。简单来说,就是定义构造函数 function Vue,并为其添加大量的原型方法(以及一些工具方法)。
而在执行 new Vue({ ... }) 语句时,就进入了实例的生命周期管理阶段。这一阶段是调用上述构造函数,构造和初始化 Vue 实例,并且管理它的整个生命周期。
一、Vue 自身的初始化阶段
该阶段定义 Vue 构造函数,并往 Vue.prototype 上挂载 $mount、$watch、$nextTick 等原型方法,以及 Vue.extend、Vue.mixin、Vue.use 等全局方法。执行完后,内存中的 Vue 结构已经准备好。
二、组件实例的生命周期管理阶段
这一阶段开始的标志就是调用 new Vue() 来构造一个 Vue 组件实例。自该语句开始,一个 Vue 应用正式被构建。该阶段又可分为两个子阶段:初始化阶段 和 挂载(销毁)阶段。当初始化完成时,如果 el 配置存在,则立即进入挂载阶段,否则等待手动调用 $mount 才会进入挂载阶段。
回顾一下 Vue 构造函数的实现:
function Vue(options) {
if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the new keyword')
}
this._init(options)
}
真正有效的就只有一行代码:this._init(options),即调用原型上的 _init 方法,传入 options,初始化组件实例。
初始化阶段的关键点
- 合并 options(合并策略:data / methods / computed / watch 等)
- 初始化生命周期(保存父子关系、$parent / $root / $children)
- 初始化事件中心(_events)
- 初始化渲染(创建 $vnode / $slots / $scopedSlots)
- initState:初始化 props / data / methods / watch / computed
- 调用 beforeCreate / created 钩子
- 如果有 el,调用 $mount 进入挂载阶段
在 _init 函数的最末尾,Vue 会检查 el 属性是否存在,如果存在将进入挂载阶段:
Vue.prototype._init = function (options) {
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
如果没有 el 属性,则需要等到手动调用 $mount 方法时才会进行挂载。
在讲解挂载阶段之前,我们先回头探讨一下响应式系统。响应式系统的核心对象是 data,所以响应式系统主要是在 initData 中构建起来的(props、computed 等都间接地依赖 data)。
挂载、更新和销毁阶段
组件初始化完毕后,如果 el 属性存在,就可以进行挂载以生成真正的 DOM。
模板编译
首先检查 render 函数是否存在。对于完整版本的 Vue,如果 render 函数不存在,它会调用自身的模板编译器对 template 进行编译;对于运行时版本,如果 render 函数不存在则会直接抛出异常。整个编译过程较为复杂,下面给出编译前后的效果:
模板:
<template>
<div id="app">
<ul>
<li v-for="item in items">
itemid: {{ item.id }}
</li>
</ul>
</div>
</template>
渲染函数:
vm._render = function () {
with (this) {
return _c('div', { attrs: { id: 'app' } }, [
_c('ul', _l(items, function (item) {
return _c('li', [
_v('itemid: ' + _s(item.id))
])
}))
])
}
}
_render 生成 VNode
vm._render() 内部会调用上述 render 函数,新生成一个对 DOM 的虚拟描述,返回的是一个 JavaScript 对象 —— 虚拟节点(vnode)。对于一个 Vue 应用来说,所有的虚拟节点会组成一整棵树状结构,也就是虚拟 DOM 树。
_update 转为真实 DOM
vm._update 内部会调用 patch:
- 如果旧的 vnode 不存在,说明这是首次绘制,patch 将依据虚拟 DOM 生成真实 DOM 并绘制到页面。
- 如果旧的 vnode 存在,说明当前组件已经被绘制到页面上,这时 patch 将比对两个 vnode,判断如何最高效地更新真实 DOM。
function updateComponent() {
vm._update(vm._render())
}
接入响应式系统
updateComponent 在数据变化时自动调用,所以必须接入响应式系统:
function mountComponent() {
const updateComponent = () => {
vm._update(vm._render())
}
// 将 updateComponent 接入响应式系统
new Watcher(vm, updateComponent, noop, {
before() {
callHook('beforeUpdate')
}
})
}
watcher 是数据对象的订阅者,负责在数据变化时执行某些操作。
初始化 watcher 的过程中会触发 data 属性的 get 方法,因此这个 watcher 就会被 Dep 收集起来,传入的回调函数正是 updateComponent 方法。当数据变化时,Observer 通知 Dep,Dep 依次调用订阅者 watcher 的 run 方法,run 里面会执行回调函数 updateComponent,于是视图得到更新。
这样就实现了"修改数据之后自动更新视图"。
销毁阶段
当手动调用 this.$destroy(),或由于 v-if 等原因导致组件必须被销毁时,Vue 主要执行以下过程:
- 调用 beforeDestroy 生命周期钩子函数。
- 清除组件的依赖关系(从父组件 $children 中移除、解绑事件)。
- 销毁 watcher(让组件失去响应能力)。
- 调用 destroyed 生命周期钩子函数。
- 最后注销组件的事件监听,清除一些附属参数,组件彻底被销毁。
来源整理自:vue3js.cn 面试官系列



