目录
一、前期工作
二、初始化
step 1
initLifecycle 中的属性
resolveSlots
step 2
initProps
initMethods
initData
initComputed
initWatch
step 3
三、挂载
四、更新
五、卸载
六、Proxy
Vue3 正如火如荼,Vue3 与 Vue2 还是有些许共通之处的,我们通过源码来简单回顾一下 Vue2 的相关知识,这里以 Vue 的 2.6.14 版本为例讲解。文中响应式相关的内容将在后续文章中补充
先扔张官网生命周期的图镇楼
官方的图已经将主流程描述的很清楚了,这里就不赘述流程了,我们直接开始
一、前期工作
...
function Vue (options) {
...
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
一切都从 new Vue(options)
开始,但在开始前,还有准备工作要做
-
initMixin:初始化,下一小节详解 -
stateMixin:使用 Object.defineProperty
给 Vue.protorype 加上$data
与$props
属性,然鹅只给这两个属性配置了 getter,因此无法修改、删除这两个属性,也不能枚举;继续增加$set
、$delete
、$watch
方法,这几个方法都与 数据双向绑定 有关,将在后续的文章详细介绍。 -
eventsMixin:Vue 原型增加 $on
、$once
、$off
、$emit
方法,通过_events
对象维护所有事件处理函数 -
$on
:监听当前实例上的自定义事件 -
$once
:监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器 -
$off
:移除自定义事件监听器 -
$emit
:触发、执行事件 -
lifecycleMixin:继续往 Vue 原型上加, _update
、$forceUpdate
、$destroy
方法 -
_update
:更新视图 -
$forceUpdate
:通过_watcher
更新视图 -
$destroy
: 卸载组件 -
renderMixin:还是往 Vue 原型上加东西,这次是 $nextTick
、_render
方法 -
$nextTick
:收集事件,在下一次事件循环一次性执行 -
_render
:根据配置返回对应的 VNode(Vue 中的虚拟节点)
小知识:无法枚举的属性在浏览器控制台展示的颜色会比正常属性浅(谷歌为浅紫色)
二、初始化
准备工作完成,现在可以开始执行 new Vue()
了。new Vue()
会调用 initMixin
挂在 Vue 原型上的 _init()
方法。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
...
vm._self = vm
// step 1
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
// step 2
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
...
// step 3
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
step 1
-
initLifecycle:这个比较简单,给属性赋上默认值,包括 $parent
、$children
、$refs
、上一小节提到的_watcher
属性等。 -
initEvents:若存在父组件绑定在当前组件上的事件,则调用 updateComponentListeners
更新这些方法 -
initRender:调用 resolveSlots
进行组件插槽(slot)的初始化;新增$attrs
、$listeners
属性,同时使用defineReactive
将这两个属性设为响应式属性 -
触发 beforeCreate
initLifecycle 中的属性
名称 | 描述 |
---|---|
$parent | 指定已创建的实例之父实例,在两者之间建立父子关系。子实例可以用 this.children 数组中。 |
$root | 当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。 |
$children | 当前实例的直接子组件。需要注意 $children 并不保证顺序,也不是响应式的。 |
$refs | 一个对象,持有已注册过 ref 的所有子组件。 |
_watcher | 组件实例相应的 watcher 实例对象。 |
_inactive | 表示 keep-alive 中组件状态,值为 false 就是激活状态,反之为 true。 |
_directInactive | 也是表示 keep-alive 中组件状态的属性。 |
_isMounted | 当前实例是否完成挂载(对应生命周期图示中的 mounted)。 |
_isDestroyed | 当前实例是否已经被销毁(对应生命周期图示中的 destroyed)。 |
_isBeingDestroyed | 当前实例是否正在被销毁,还没有销毁完成(介于生命周期图示中 deforeDestroy 和 destroyed 之间)。 |
resolveSlots
对插槽的处理
代码就不贴了,简单讲一下是如何处理的,详细代码在 resolve-slots.js
-
若组件 children 子节点为空,则组件中 this.$slots 对象为{}空对象 -
若组件子节点存在,则先移除 slot 属性,具名 slot 按照定义的 name 进行存储 -
当定义 slot 的标签为 template 时,则获取它的子节点 -
最后删除只包含空白字符(即不包含具体内容的 slot)
step 2
这一步将需要处理的属性都挂在当前组件实例的 $options
属性上,如:inject
、props
、data
等。
-
initInjections: -
若 inject
使用Symbol
作为 key,则使用Reflect.ownKeys
获取 inject 的所有 key 值,否则使用Object.keys
。 -
之后对 inject 中的各个 key 进行遍历,沿着父组件链一直向上查找 provide
中和inject
对应的属性,直到查找到根组件或者找到为止,然后返回结果 -
最后使用 defineReactive
将其属性设为响应式,在非生产环境下在当前组件内直接修改inject
的属性发出告警 -
initState:这一步囊括了很多初始化操作(以下排列顺序对应源码中的执行顺序) -
initProps -
initMethods -
initData -
initComputed -
initWatch -
initProvide:将 provide
映射到当前组件实例的_provided
属性上 -
到这里,一个 Vue 组件实例算是创建完成了,触发 created
initProps
遍历 props
-
检查 props
的属性是否为保留字符,是的话则告警 -
通过 defineReactive
将属性设为响应式,在非生产环境下在当前组件内直接修改props
的属性发出告警 -
将 props
的属性映射到当前组件实例的_props
上
initMethods
遍历 methods
-
非生产环境下,检查类型是否为 function
类型,不是则告警 -
非生产环境下,检查是否与 props
的属性重名了,是则告警 -
非生产环境下,检查是否为 Vue 保留字符,是则告警 -
将方法映射到当前组件实例上,并通过 bind
将方法内的 this 指向当前组件实例
initData
首先判断 data
属性是否为函数且返回一个对象,不满足则将当前执行环境下的 data
置空,在非生产环境下告警,若满足条件,则开始遍历 data
。
-
非生产环境下检查是否与 methods
的属性重名,是则告警 -
检查是否与 props
的属性重名 -
是,则告警 -
否,则将属性映射到当前组件实例的 _data
上 -
调用 observe
,数据双向绑定的关键,之后的文章详解
initComputed
这里要提前讲一下,根据功能划分,Vue 中的 watcher
可以分为三种
|类型| 作用| |render-watcher| 每个组件都有自己的 watcher| |user-watcher |用户自定义 watcher,比如通过 watcher API 定义| |computed-watcher |使用 computed 属性定义的 watcher,具有惰性特性|
创建顺序:computed-render -> user-watcher -> render-watcher
initComputed 内维护了一个 computed-watcher
的队列 _computedWatchers
开始遍历 computed
-
如果不是服务端渲染(SSR),则新建 watcher
,并且传入lazy=true
,回调函数为空,以 computed 的属性为 key 加入 _computedWatchers -
通过 Object.defineProperty
将属性映射到当前组件实例上,并将setter
设为noop
(不含函数体的函数),getter
分两种情况: -
非服务端渲染为 createGetterInvoker
,直接返回用户定义的getter
-
服务端渲染为 createComputedGetter
function createComputedGetter(key) {
return function computedGetter() {
// getter step 1
const watcher = this._computedWatchers && this._computedWatchers[key];
if (watcher) {
if (watcher.dirty) {
// getter step 2
watcher.evaluate();
}
if (Dep.target) {
// getter step 3
watcher.depend();
}
return watcher.value;
}
};
}
页面上引用了 computed
的值时,触发 watcher.update
,因为这里的 watcher 的 lazy
为 true
,所以 watcher.dirty
被设为 true
;走到 getter step 2 调用 evaluate
主动触发一遍 watcher.get
,刷新当前的 value 值,同时将 dirty 重新设为 false,等待下次页面引用时再重新计算。
这里 Dep.target
的值为组件的 render-watcher
,走到 getter step 3 computed-watcher
收集依赖。
export default class Watcher {
dirty: boolean;
lazy: boolean;
...
update() {
if (this.lazy) {
this.dirty = true;
}
...
}
...
evaluate() {
this.value = this.get();
this.dirty = false;
}
}
initWatch
遍历 watch
调用 createWatcher
function createWatcher(
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === "string") {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options);
}
因为 watch 可以以 对象、方法名字符串、数组、方法 四种形式传入,所以在 createWatcher
中进一步判断类型,获取相应的处理函数(handler),最后调用在前期工作中 stateMixin
定义在 Vue 原型上的 $watch
方法
export function stateMixin (Vue: Class<Component>) {
...
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
// watch step 1
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
// watch step 2
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
// watch step 3
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
// watch step 4
return function unwatchFn () {
watcher.teardown()
}
}
}
-
watch step 1:判断回调函数(cb)是不是一个纯对象,是的话意味着其中可能还有配置信息,再调用一遍 createWatcher
-
watch step 2:新建一个 watcher(user-watcher) -
watch step 3:如果配置了 immediate=true
,先暂停依赖收集,立即执行回调函数,之后再继续收集依赖 -
watch step 4: 返回一个取消观察函数,用来停止触发回调, watcher.teardown()
用来将自身从所有依赖收集订阅列表删除
step 3
// initMixin
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
再回到 initMixin
中,接下来是 step 3,若存在 el 则调用 $mount
,$mount 方法最终调用的是 mountComponent
方法,接下来就进入到挂载阶段
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 判断是不是浏览器渲染
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating);
};
三、挂载
// lifecycle.js
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// step 1
vm.$el = el
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
...
}
}
callHook(vm, 'beforeMount')
...
// step 2
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// step 3
new Watcher(vm, updateComponent, noop, {
// step 4
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false
// step 5
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}
-
step 1:将 el 替换为 $el
,若render
方法不存在,则给一个空节点的创建方法,触发 beforeMount -
step 2: updateComponent
作为下一步创建的 Watcher 对象的getter
函数,用来依赖收集。_update
最终调用的是__patch__
方法,用于将 Virtual DOM 渲染成真实 Dom 节点,不同平台(web/weex)有略微区别 -
step 3:新建一个 watcher
实例,这里就是render-watcher
,会挂到当前组件实例的_watcher
属性上;注意与_watchers
属性区分开 -
_watcher:当前实例的 render-watcher -
_watchers:存放当前实例的所有 watcher,包括 render-watcher(由于是挂载阶段才创建的,一般放在数组末尾,即在 computed-watcher 与 user-watcher 之后) -
step 4:初始 patch 操作可能会调用 $forceUpdate
,如: 在子组件的挂载钩子内部,因此在更新前增加 beforeUpdate 的 hook -
step 5: $vnode
为null
时即 挂载完成,修改_isMounted
为 true,触发 mounted
四、更新
-
beforeUpdate:在上一步挂载阶段的 render-watcher
在异步更新前触发,即:在调用watcher.run()
之前执行watcher.before()
-
updated:同样是 render-watcher
的异步更新,在调用watcher.run()
之后触发
五、卸载
非 keep-alive
组件,在 patch
阶段检测到旧节点存在,但新节点不存在时触发卸载,卸载过程的具体实现在 $destroy
方法中。
export function lifecycleMixin (Vue: Class<Component>) {
...
Vue.prototype.$destroy = function () {
const vm: Component = this
// step 1
if (vm._isBeingDestroyed) {
return
}
callHook(vm, 'beforeDestroy')
// step 2
vm._isBeingDestroyed = true
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// step 3
if (vm._watcher) {
vm._watcher.teardown()
}
// step 4
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// step 5
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// step 6
vm._isDestroyed = true
vm.__patch__(vm._vnode, null)
callHook(vm, 'destroyed')
// step 7
vm.$off()
// step 8
if (vm.$el) {
vm.$el.__vue__ = null
}
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
}
-
step 1:根据 _isBeingDestroyed
判断当前组件是否已经在执行卸载操作了,若不是则触发 beforeDestroy -
step 2:标记开始进行卸载组件,移除父组件对当前组件的引用 -
step 3:将当前组件实例从其他数据的依赖列表(deps)中删除 -
step 4:实例内的数据对其他数据的依赖都会存放在实例的_watchers 属性中,所以我们只需遍历 _watchers,将其中的每一个 watcher 都调用 teardown 方法,从而实现移除实例内数据对其他数据的依赖 -
step 5:从 _data.__ob__ 中删除引用,__ob__ 指向 Observer -
step 6:当前实例上添加 _isDestroyed 属性来表示当前实例已经被销毁,同时将实例的 VNode 树设置为 null 并更新视图,之后触发 destroyed -
step 7:移除实例上的所有事件监听器 -
step 8:移除 __vue__ 的引用,移除对父组件的引用
六、Proxy
在初始化时,非生产环境下调用了个 initProxy
,之后才开始执行 initLifecycle
、initEvents
等初始化操作
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
...
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
...
这里用的 Proxy 就是 Vue3 中使用的 Proxy API,不过在 Vue2.x 中的 Proxy 主要作用是在非生产环境下拦截对象属性的操作,对非法操作做出告警
// proxy.js
if (process.env.NODE_ENV !== 'production') {
...
// step 1
const hasProxy =
typeof Proxy !== 'undefined' && isNative(Proxy)
if (hasProxy) {
...
// step 2
config.keyCodes = new Proxy(config.keyCodes, {
set (target, key, value) {
if (isBuiltInModifier(key)) {
warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)
return false
} else {
target[key] = value
return true
}
}
})
}
const hasHandler = {
// step 3
has (target, key) {
const has = key in target
const isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data))
if (!has && !isAllowed) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return has || !isAllowed
}
}
const getHandler = {
// step 4
get (target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) warnReservedPrefix(target, key)
else warnNonPresent(target, key)
}
return target[key]
}
}
// step 5
initProxy = function initProxy (vm) {
if (hasProxy) {
const options = vm.$options
const handlers = options.render && options.render._withStripped
? getHandler
: hasHandler
vm._renderProxy = new Proxy(vm, handlers)
} else {
vm._renderProxy = vm
}
}
}
export { initProxy }
...
-
step 1:判断当前环境是否支持 Proxy API -
step 2:若支持 Proxy 则走到这一步,添加 set 捕捉器,在给 KeyCodes 设置属性时触发,检测自定义的 key 是否与内置修饰符重名 -
step 3:设置 has 捕捉器,查看或遍历组件实例的属性时触发,检测 $options
的属性名是否合法,是否存在 -
step 4:设置 get 捕捉器,获取组件实例的属性时触发,同样判断属性名称的前缀是否为保留字符,以及属性是否存在 -
step 5:若支持 Proxy,则使用 Proxy 捕捉 $options 的行为,并将 Proxy 实例映射到当前组件实例的 _renderProxy
属性;不支持则将当前组件实例本身映射到_renderProxy
属性
由于个人订阅号不具备评论功能,如有其它意见,欢迎前往以下任一站点评论留言:
https://melonfield.club/column/detail/cvwmo7Ae5VH(个人独立开发运营) https://juejin.cn/post/6999264503443488805
8参考
-
https://github.com/vuejs/vue -
https://www.imooc.com/article/29256 -
https://blog.csdn.net/a895865025/article/details/118852781
原文始发于微信公众号(MelonField):vue2源码系列-执行过程
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/93882.html