我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
这篇博客旨在从源码的角度全面的概括 Vue 的运行流程,让大家对 Vue 的总体知识体系有所了解,也是对前面写的 30 篇文章的一个小总结,首先抛出 Vue 官网中的生命周期图,如下所示:
这张图是 Vue 官网中的生命周期图,相信大家对这张图都很熟悉了,接下来,我们从源码的角度对图中的内容进行一步步的解析。
1,new Vue() ==> beforeCreate
1-1,执行 new Vue()
function Vue (options) {
// 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义
this._init(options)
}
执行 new Vue(),Vue 构造函数内部会执行原型上的 _init() 方法,对当前的 Vue 实例进行初始化。
1-2,进入 _init() 方法
_init() 方法是 Vue 原型对象中的方法,通过 Vue 构造函数创建的实例能够通过原型链访问到这个方法,该方法定义在 src/core/instance/init.js 文件中。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// vm 就是 Vue 的实例对象,在 _init 方法中会对 vm 进行一系列的初始化操作
const vm: Component = this
// 赋值唯一的 id
vm._uid = uid++
// 下面这个 if else 分支需要注意一下。
// 在 Vue 中,有两个时机会创建 Vue 实例,一个是 main.js 中手动执行的 new Vue({}),还有一个是当我们
// 在模板中使用组件时,每使用一个组件,就会创建与之相对应的 Vue 实例。也就是说 Vue 的实例有两种,一种是
// 手动调用的 new Vue,还有一种是组件的 Vue 实例。组件的 Vue 实例会进入下面的 if 分支,而手动调用的
// new Vue 会进入下面的 else 分支。
//
// 合并 options,options 用于保存当前 Vue 组件能够使用的各种资源和配置,例如:组件、指令、过滤器等等
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else {
// options 中保存的是当前组件能够使用资源和配置,这些都是当前组件私有的。
// 但还有一些全局的资源,例如:使用 Vue.component、Vue.filter 等注册的资源,
// 这些资源都是保存到 Vue.options 中,因为是全局的资源,所以当前的组件也要能访问到,
// 所以在这里,将这个保存全局资源的 options 和当前组件的 options 进行合并,并保存到 vm.$options
vm.$options = mergeOptions(
// resolveConstructorOptions 函数的返回值是 Vue 的 options
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 初始化与生命周期有关的内容
initLifecycle(vm)
// 初始化与事件有关的属性以及处理父组件绑定到当前组件的方法。
initEvents(vm)
// 初始化与插槽和渲染有关的内容
initRender(vm)
// 在 beforeCreate 回调函数中,访问不到实例中的数据,因为这些数据还没有初始化
// 执行 beforeCreate 生命周期函数
callHook(vm, 'beforeCreate')
......
......
}
}
从进入 _init() 方法到触发 beforeCreate 生命周期函数,Vue 一共做了四件事,非别是:options 的合并、初始化与生命周期有关的内容、初始化父组件在当前组件实例上注册的事件、初始化插槽和渲染有关的内容。
options 的合并可以看我的这篇文章。
初始化父组件在当前组件实例上注册的事件可以看我的这篇文章。
插槽可以看我的这篇文章。
2,beforeCreate ==> created
执行完 beforeCreate 生命周期函数之后,接下来就开始 Vue 实例 状态、方法、计算属性和侦听器的初始化工作,源码如下所示:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
......
......
// 在 beforeCreate 回调函数中,访问不到实例中的数据,因为这些数据还没有初始化
// 执行 beforeCreate 生命周期函数
callHook(vm, 'beforeCreate')
// 解析初始化当前组件的 inject
initInjections(vm) // resolve injections before data/props
// 初始化 state,包括 props、methods、data、computed、watch
initState(vm)
// 初始化 provide
initProvide(vm) // resolve provide after data/props
// 在 created 回调函数中,可以访问到实例中的数据
// 执行 created 回调函数
callHook(vm, 'created')
......
......
}
}
在 beforeCreate 和 created 生命周期函数之间执行了三个方法,非别是:initInjections()、initState()、initProvide(),initInjections() 和 initProvide() 用于处理 provide / inject,initState() 用于初始化当前实例的 props、methods、data、computed、watch。这两块内容会在后续的博客中专门写两篇文章进行解析。
3,判断有没有配置 el 选项,有的话执行 vm.$mount(vm.$options.el)
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
......
......
// 执行 created 回调函数
callHook(vm, 'created')
// 如果配置中有 el 的话,则自动执行挂载操作
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
created 生命周期函数执行完之后,开始判断有没有配置 el 选项,如果配置了的话,就开始进行挂载操作,如果没有配置的话,则需要用户手动调用 vm.$mount(el) 触发执行挂载操作。
4,模板字符串编译成 render 函数
在这一步,首先判断选项中有没有配置自定义的 render 函数,如果没有配置的话,则需要框架将模板字符串编译成 render 函数。在进行编译之前,首先要做的是先获取到模板字符串,如果在配置选项中配置了 template 的话,则该选项的值就是模板字符串,如果配置选项没有配置 template 的话,则将 el 节点的 outerHTML 作为模板字符串。
获取到模板字符串之后,则调用 compileToFunctions() 方法将模板字符串编译成 render 函数,编译出的 render 函数保存到 vm.$options 对象中。
有关模板编译的解析可以看我的以下这几篇文章:
编译成 render 函数并保存到 vm.$options 对象中之后,则开始挂载操作。
对应的源码如下所示:
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 根据 el 获取其对应的 DOM 元素
el = el && query(el)
// 拿到 new Vue() 传递的配置对象
const options = this.$options
// 判断配置对象中有没有写 render 函数,如果没有定义 render 的话,接下来会根据提供的 template 或者 el
// 生成 render 函数,并赋值给 options
// 也就是说:最终 Vue 只认 render 函数,如果用户定义了 render 函数的话,那就直接使用,如果没有定义的话,Vue 会为其生成
if (!options.render) {
// 判断配置对象中有没有 template
let template = options.template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
}
} 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 选项没有配置的话,使用 innerHTML 属性获取 el 节点的字符串形式
template = getOuterHTML(el)
}
// 到这里,我们获取到了 template
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// compileToFunctions 是一个函数,作用是将:template 模板字符串编译成 render 函数
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 在确保 options 中有 render 函数之后,就开始执行 runtime 中挂载的 $mount 进行渲染
// 也就是说:(1)entry-runtime-with-compiler 中的 $mount 负责编译的工作,最终的处理结果就是 options 中一定会有用于渲染的 render 函数
// (2)而 runtime 中的 $mount 函数则负责根据生成的 render 函数进行页面的渲染
return mount.call(this, el, hydrating)
}
5,进行挂载操作
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// 将 el 设值到 vm 中的 $el
vm.$el = el
// 触发执行 beforeMount 生命周期函数(挂载之前)
callHook(vm, 'beforeMount')
let updateComponent = () => {
// vm._render() 函数的执行结果是一个 VNode
// vm._update() 函数执行虚拟 DOM 的 patch 方法来执行节点的比对与渲染操作
vm._update(vm._render(), hydrating)
}
// 这里的 Watcher 实例是一个渲染 Watcher,组件级别的
vm._watcher = new Watcher(vm, updateComponent, noop)
// 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
}
updateComponent 函数的作用是将组件最新的 vnode 渲染(挂载)到页面上。在这里将 updateComponent 函数作为一个参数创建 Watcher 实例,在 Watcher 类中会调用执行 updateComponent 函数,进行页面的渲染。这里所创建的 Watcher 实例是当前 Vue 实例的渲染 Watcher,这个 Watcher 实例的作用是监控当前组件中状态的变化,如果组件中的状态发生变化的话,则会触发这个 render Watcher 中的 update() 方法,update() 方法会进而触发执行传递进去的 updateComponent 函数,进行组件的重新渲染。
vm._render() 的作用是使用组件的 render 函数结合组件当前最新的状态生成最新的 vnode,vnode 是 patch diff 算法的原材料。
vm._update() 的作用是使用最新的 vnode 执行 patch diff 算法,进行组件的渲染。
至此,组件的首先渲染就完成了。
相关的博客如下所示:
6,组件的状态发生变更,引起组件的重新渲染
当组件的状态发生变化的时候,会触发数据的 setter 函数,在 setter 函数中会触发数据对应 Dep 实例的 notify() 方法。这个 Dep 实例和数据是一一对应关系,保存着该数据被依赖的所有 Watcher 实例。
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 在此进行依赖收集
get: function reactiveGetter () {
......
......
},
// 在此进行派发更新
set: function reactiveSetter (newVal) {
// 拿到旧的 value
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// 如果存在用户自定义的 setter 的话,用这个用户自定义的 setter 赋值这个 value
if (setter) {
setter.call(obj, newVal)
} else {
// 否则就直接将 newVal 赋值给 val
val = newVal
}
// 将新设置值中的 keys 也转换成响应式的
childOb = !shallow && observe(newVal)
// 触发依赖的更新
dep.notify()
}
})
Dep 实例的 notify() 方法会触发保存所有 Watcher 实例的 update() 方法。
export default class Dep {
// 用于收集依赖的数组
subs: Array<Watcher>;
constructor () {
// 将当前的 uid 当做 id 赋值给 id
this.id = uid++
// 初始化保存依赖的数组 subs
this.subs = []
}
// 触发 subs 数组中依赖的更新操作
notify () {
// 数组的 slice 函数具有拷贝的作用
const subs = this.subs.slice()
// 遍历 subs 数组中的依赖项
for (let i = 0, l = subs.length; i < l; i++) {
// 执行依赖项的 update 函数,触发执行依赖
subs[i].update()
}
}
}
Watcher 中的 update() 方法会间接的触发执行 updateComponent() 方法,updateComponent() 方法会触发执行 Vue 实例的 _update() 方法。
在 _update() 方法中会判断当前的 Vue 实例有没有挂载过,如果当前的组件已经被挂载过的话,则触发执行 beforeUpdate 生命周期函数。
接下来执行 __patch__ 方法进行 diff 算法重新渲染组件。
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
if (vm._isMounted) {
// 如果当前的 vm 已经挂载了的话,说明当前是第(2)中情况,所以需要执行 beforeUpdate 回调函数
callHook(vm, 'beforeUpdate')
}
const prevEl = vm.$el
// 上一次渲染时的 VNode
// 第一次渲染时,为空
const prevVnode = vm._vnode
const prevActiveInstance = activeInstance
activeInstance = vm
vm._vnode = vnode
if (!prevVnode) {
// 首次渲染
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
)
// no need for the ref nodes after initial patch
// this prevents keeping a detached DOM tree in memory (#5851)
vm.$options._parentElm = vm.$options._refElm = null
} else {
// 依赖的数据更新时,页面需要重新渲染
vm.$el = vm.__patch__(prevVnode, vnode)
}
}
接下来需要说说什么时候执行 updated 生命周期函数,这需要说一下另外一个知识点,就是当触发执行 Watcher 实例的 update 方法时,并不会立即触发 Watcher 实例的 run 方法,而是先将当前的 Watcher 实例缓存到一个数组中,这个数组用于缓存在当前的事件循环中,需要组件重新渲染的所有 Watcher 实例,当下轮事件循环开始时,统一执行缓存的所有 Watcher 实例的 run 方法进行组件的重新更新,这样做,是为了优化性能,具体解析可以看我的这篇文章 —— Vue源码阅读(27):vm.$nextTick、Vue.nextTick 的源码解析。
缓存 Watcher 实例和执行所有缓存 Watcher 的 run 方法的代码逻辑在 src/core/observer/scheduler.js 文件中,相关源码如下所示:
const queue: Array<Watcher> = []
function flushSchedulerQueue () {
flushing = true
let watcher, id
// 刷新前对队列排序。
// 这可确保:
// 1. 组件从父级更新到子级(因为父级组件总是在子组件之前创建)。
// 2. 组件的自定义 watcher 在组件的渲染 watcher 之前运行(因为自定义 watcher 在渲染 watcher 之前创建)。
// 3. 如果在父组件的渲染 watcher 运行期间,子组件被销毁,该子组件的 watcher 会被跳过。
queue.sort((a, b) => a.id - b.id)
// 不要使用变量固定缓存当前状态 watcher 队列的长度,,因为新的 watcher 有可能随时被 push 到队列中
// 遍历触发执行队列中的 watcher 实例
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
// 将 map 中,当前 watcher 的 id 置空
has[id] = null
// 核心:执行 watcher 实例的 run 方法
watcher.run()
}
// 重置调度程序的状态
resetSchedulerState()
// 触发执行 updated 生命周期函数
callUpdatedHooks(updatedQueue)
}
function callUpdatedHooks (queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
可以发现,在 flushSchedulerQueue() 函数中,循环执行完缓存 Watcher 的 run 方法之后,组件的更新就完成了,接下来会执行 callUpdatedHooks 方法,在 callUpdatedHooks 方法中触发执行 updated 生命周期函数。
7,销毁组件实例
当执行 Vue 实例的 $destroy 原型方法时,会进行组件销毁的逻辑。
vm.$destroy 的官方文档点击这里。
对应的源码如下所示:
// Vue 实例销毁的方法
// beforeDestroy 和 destroyed 的生命周期函数都是在这里触发执行的
Vue.prototype.$destroy = function () {
const vm: Component = this
// _isBeingDestroyed 变量用于防止实例被重复执行 $destroy 方法
// 如果 vm._isBeingDestroyed 值为 true 的话,则直接 return
if (vm._isBeingDestroyed) {
return
}
// 触发执行 beforeDestroy 生命周期函数
callHook(vm, 'beforeDestroy')
// 将 _isBeingDestroyed 属性设置为 true
vm._isBeingDestroyed = true
// 解除当前 Vue 实例和父 Vue 实例的关系
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// 销毁当前实例的 render Watcher
if (vm._watcher) {
// teardown() 方法用于将 watcher 实例从依赖的所有数据的 dep 实例中移除
vm._watcher.teardown()
}
// Vue 实例中除了 render Watcher,还有计算属性 Watcher 和侦听器 Watcher,
// 这些 Watcher 实例都保存在 vm._watchers 数组中,
// 遍历执行所有 Watcher 实例的 teardown 方法即可。
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// 将 _isDestroyed 设置为 true,表明 vm 已经被销毁了
vm._isDestroyed = true
// 执行 vm 的 __patch__ (重新渲染)方法,__patch__ 方法的第二个参数是 null,
// 这会解除绑定的指令,移除组件的 DOM
vm.__patch__(vm._vnode, null)
// 执行 destroyed 生命周期函数
callHook(vm, 'destroyed')
// turn off all instance listeners.
// 移除 vm 上绑定的事件
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}