您好,我是bp技术知识库,欢迎关注。
时隔2个多月,我又开始发文了,这两个多月我逐步开始研究React和Vue源码相关的东西,并且也深入阅读了JavaScript相关的资料,接下来我会逐渐整理我学到的知识,进一步加深对这些知识的理解。
在平时写业务的时候,可能阅读源码并不会对你书写业务的效率提升多少,也许你觉得你现在写业务已经非常的得心应手了,那么下面这些与Vue有关的问题你是否思考过:
- 生命周期是如何实现的。
- 组件是如何和data还有props进行绑定的。
- 组件中的state更新时,为什么视图会发生变化。
- vue-router是如何实现单页面模拟路由切换的。
- vuex是如何实现的。
- …
为什么是Vue2不是Vue3?
Vue3的源码相对于Vue2来说已经发生了翻天覆地的变化,用TypeScript进行了重构,所以Vue3和Vue2的源码存在着巨大的区别,那么Vue3已经出了两年左右了,我为什么还要阅读Vue2的源码?
那是因为Vue2的代码经过多年的沉淀,已经不会有太大的改动了,Vue3的代码可能今天你看到是这样,明天就进行了非常大的改动。
好吧,其实是因为公司目前依然使用的Vue2…
所以我就从Vue2开始研究,等到Vue2研究的差不多的时候,再去研究Vue3。
但是看了一部分Vue2的源码后,我突然醒悟我其实可以直接看Vue3…所以关于Vue2的文章可能就只有这一篇,因为这篇文章我一开始研究的时候就写了个大概。后面的文章几乎都是Vue3和React的。
为什么我开始读源码?
阅读源码不光光是为了应付面试,最重要的是它可以给你很多启发,你看源码的过程中可能会忍不住发出惊叹:还有这种操作?
随着你对这些框架的深入了解,你写业务代码的时候也会不自觉的站在另一个角度去思考代码,即:如何让代码更具复用性。
那么说回正题,Vue项目的入口文件通常会有下面的代码:
new Vue({
render: h => h(App),
}).$mount('#app')
这小小的一段代码背后的故事,将分成两部分进行分析,分别是new Vue
部分和.$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)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
从代码中可以看到有5个Mixin
函数:
initMixin
:初始化相关。stateMixin
:state相关。eventsMixin
:事件相关。lifecycleMixin
:生命周期相关。renderMixin
:渲染相关。
initMixin
是本篇文章主要讲解的函数,因为它涉及到初始化相关的内容。
initMixin
该函数为Vue的原型上添加一个_init
方法。在这里可以看到源码:_init
。
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
...省略内容
}
}
而该方法会在new Vue
的时候会进行调用:
function Vue (options) {
// 调用_init方法
this._init(options)
}
所以我们要了解new Vue
做了什么最主要的就是需要了解_init
方法中做了什么。
关于_init
方法,我删除了一些new Vue
生成实例时并不会走的代码:
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
// a flag to avoid this being observed
vm._isVue = true
// merge options
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 {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// 如果有el参数,则调用.$mount方法
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
从源码中我们可以看出,下面两种写法是完全等价的:
new Vue({
render: h => h(App),
}).$mount('#app')
new Vue({
render: (h) => h(App),
el: "#app",
});
在_init
方法中会经过一系列的初始化内容:
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
从上面的命名可以看得出来,这一系列的函数是初始化生命周期、事件、渲染、Inject、State等内容。
这些内容不是本篇文章需要关注的重点,后面会以Vue3的源码作为参考,去探讨Vue的生命周期,State以及props等等内容。
那么通过上面的探讨,总结一下new Vue
中主要做了什么事情?
- 调用
_init
方法。 - 通过
mergeOptions
函数合并options
并且赋值给Vue
上的$options
属性。 - 初始化生命周期、事件、渲染、Inject、State等内容。
$mount
接下来就要来谈一下.$mount
中做了些什么操作,因为new Vue
中只是进行了一大堆的初始化操作,但是并没有涉及到如何将VNode
节点渲染到界面上。
这里可以找到它的源码:$mount源码
。
源码中的最后一句调用了一个方法:
return mount.call(this, el, hydrating)
即下面的这个方法:
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
它的源码地址。
可以看到.$mount
其实是调用mountComponent
函数:
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 && !vm._isDestroyed) {
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
}
这里的源码是比较饶的,而且几乎没有注释,如果不通过debugger
调试一下源码,可能根本就不知道方法之间的执行顺序,所以我个人推荐阅读源码,精髓就在于调试,如果你光看源码,可能看几行代码就放弃了。
代码中定义了一个updateComponent
方法:
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
接下来将该方法作为参数传入了Watcher
构造函数中:
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
所以在mountComponent
函数中,有3个点我们需要关注:
_update
方法。_render
方法。new Watcher
。
_update
方法和_render
方法是在new Watcher
中进行调用的,所以我们这里优先看一下new Watcher
中做了什么操作。
由于代码比较多,所以本篇文章只关注new Watcher中与.$mount
关系紧密的部分。
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
}
关键代码就是这句:this.getter.call(vm, vm)
。
它调用了updateComponent
函数,而该函数中又调用了_update
方法和_render
方法
执行顺序为先执行_render
方法,再执行_update
方法,所以我们先看_render
方法中做了什么处理。
if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}
如果在Vue中使用过jsx的朋友可能对于$scopedSlots
比较熟悉,它是跟插槽相关的内容。
于是我们根据代码可以大胆的推测:在_render
方法中主要是做了一些和插槽相关的工作,并且将$vnode
属性指向它的父组件。
前文中我们提到了lifecycleMixin这个函数,而_update
方法]就是在这个函数中被挂到原型上的,所以我们可以在这里找到它的源码:
_update
中调用了一个非常重要的方法vm.__patch__
这就是Vue2中的diff算法相关的内容。
而关于diff算法,在后面的文章中会以Vue3的代码单独进行探讨。
Vue2的diff算法所在的位置:patch。
我们再次大胆的猜测一下:_update
方法就是根据VNode
节点,生成一个DOM元素。
比起上面的_update
方法做了一大堆diff算法,衍生出了各种方法来讲_render
方法所做的事情就简单的多。
最后
我们总结一下在new Vue().$mount()
时Vue做了哪些事情:
- 初始化生命周期、事件、渲染、Inject、State等内容。
- 根据
VNode
节点创建真实DOM节点渲染到页面上。
虽然我们大致明白了new Vue
时的到底做了什么,但本文开头提出的问题都没有得到解答。
不过别着急,一口吃不成一个胖子,这些内容会在后面的Vue3源码相关的文章中进行探讨。
由于源码系列的参考资料并不是太多,所以文章中可能有些地方的理解是错误的,主要目的还是提供一个阅读源码的思路,推荐还是按照本文提供的思路,自己对源码进行一下调试,可能会有更深层次或者不同的理解。