--本文采自本人公众号【猴哥别瞎说】
接着前文的准备。我们知道:在 Vue2.6.10 的源码结构中,入口文件是在 src/platforms/web/entry-runtime-with-compiler.js。那么我们就具体来看看里面的代码吧。
在初次看源码的时候,有两个值得注意的点:
- 不要抠细节,把握整体的方向,以囫囵吞枣的方式看即可。
- 制定阅读的目标,有重点地去看。
我们先来看看我们此次的目标:Vue的构造函数到底在哪里?具体的初始化过程又是怎样的?
追踪Vue构造函数
我们先来看 entry-runtime-with-compiler.js 文件的核心部分:
//将$mount函数进行拓展,对用户输入的$options的template/el/render进行解析、处理
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
//处理用户自定义选项,可知执行顺序是 render > template > el
const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
if (template) {
//字符串模板
if (typeof template === 'string') {
//选择器
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
//传进来的是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)
}
if (template) {
//获取render函数
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)
}
看代码知道,这个文件仅仅是对已有的 mount 函数做了增强:将用户输入的自定义模板选项,进行处理并发挥render函数。
从这里可以得出的结论是:
1.如果参数中同时存在el、render、template变量,其优先级顺序为:render > template > el。
2.编译发生的时间段(compileToFunctions,将模板转化为render函数)在整个初始化过程中非常早的,在执行具体mount函数之前。
这里并不是真正的 Vue 构造函数所在。我们接着看 ./runtime/index.js 文件:
// install platform patch function
//定义补丁函数,这是将虚拟DOM转换为真实DOM的操作,非常重要
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
//定义挂载函数
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
//这才是真正的挂载函数
return mountComponent(this, el, hydrating)
}
结果发现,该文件仅仅是定义了原型链上的__patch__
函数以及$mount
函数。而详细的挂载过程还不在这里。具体细节我们先不看。
继续找:core/index.js
//初始化全局API
initGlobalAPI(Vue)
结果依然还是失望的。不禁感慨:这个俄罗斯套娃也太多层了吧。在core/index.js内部,也仅仅只是初始化了全局API而已。这个并不是今天我们关注的重点,还是继续找:src/core/instance/index.js :
function Vue (options) {
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
在这里,终于找到了Vue的构造函数!!!与此同时,我们也发现:该文件执行了许多 Mixin 操作。这些详细的部分我们先不理会,我们先来看看构造函数中的 this._init() 到底做了什么事情呢?
可以通过浏览器调试的方式,也可以通过全局搜索prototype._init
的方式,找到_init()
的所在文件:src/core/instance/init.js。
//初始化顺序:生命周期->事件监听->渲染->beforeCreate->注入->state初始化->provide->created
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化 props/data/watch/methods, 此处会是研究数据响应化的重点
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
//如果存在el元素,则会自动执行$mount,这也是必须要理解的
//也就是说,在写法上如果有el元素,可以省略$mount
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
从这里可以得出的结论是:
1.在
_init()
函数中,我们看到vue实例初始化时候的执行顺序:生命周期->事件监听->渲染->beforeCreate->注入数据inject->组件状态初始化->提供数据provide->created。2.如果存在el元素,则会自动执行挂载。
如果我们想要了解数据响应化的细节,那就应该详细去看initState()函数。它会是我们下节课的重点。
在这个时候,我们找到了主要的文件脉络。至于具体的初始化过程,我们还需要深入去看$mount过程中做了什么。于是我们回到真正的挂载函数mountComponent()函数里面去看看,其中发生的挂载细节:
//这才是真正的mount函数
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
...
callHook(vm, 'beforeMount')
//核心代码逻辑
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
...
} else {
updateComponent = () => {
//更新Component,主要做了两个事情:render(生成vdom)、update(转换vdom为dom)
vm._update(vm._render(), hydrating)
}
}
// 在此处定义Watcher(一个Vue实例对应的是一个Watcher),并且与updateComponent关联起来
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
callHook(vm, 'mounted')
}
return vm
}
在上面的代码中,我们看到,在vue实例挂载的过程中,会新建一个 Watcher。这个 Watcher 的作用是类似于一个观察者,它如果收到数据发生了变化的消息,那么就会执行 updateComponent 函数。而这个 updateComponent 函数,主要做了两个事情:render(生成 vdom)、update(转换 vdom 为 dom)。
综上,梳理为以下的流程图:
那么,Watcher 的工作原理是怎样的,它是如何在整个数据响应化的过程中发挥作用的?下篇文章将会给你答案!
vue源码解读文章目录:
Vue 更多系列: