前言
转眼一过去,从实习到转正已经过去七八个月了,工作的内容感觉越来越枯燥,每天的搬砖让自己的身体被掏空 自己的思维僵化,继续这样下去我感觉我就废了,所以出这么一篇文章是为了一边强迫自己去思考,一边去调整自己的状态。
另外,新人理解源码不一定正确,还希望各位大佬指点一下。
准备工作
项目地址: https://github.com/vuejs/vue.git
版本: 2.6.14
项目结构:
稍微提一嘴:
我们知道
.vue文件经过webpack的vue-loader打包后会变成js对象,这个对象里会多出一个render的属性,这个属性来自于.vue文件的template。编译器就是干的这种事。
再来看package.json:
我们会发现 vue 是用 rollup 进行打包的,所以我们需要全局安装 rollup,接着再安装依赖,最后运行npm run dev即可。 [注: 我在dev中添加了--sourcemap, 为了之后debug可以看到在源文件的哪个位置]
{
"name": "vue",
"version": "2.6.14",
"main": "dist/vue.runtime.common.js",
"module": "dist/vue.runtime.esm.js",
"sideEffects": false,
"scripts": {
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
"dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
"dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
...
},
}
可以看到,npm run dev会执行scripts/config.js的文件,而且还指定了参数web-full-dev。
scripts/config.js
const aliases = require('./alias')
const resolve = p => {
const base = p.split('/')[0]
if (aliases[base]) {
return path.resolve(aliases[base], p.slice(base.length + 1))
} else {
return path.resolve(__dirname, '../', p)
}
}
const builds = [
...
// Runtime+compiler development build (Browser)
'web-full-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development',
alias: { he: './entity-decoder' },
banner
},
...
]
alias.js
const path = require('path')
const resolve = p => path.resolve(__dirname, '../', p)
module.exports = {
vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
compiler: resolve('src/compiler'),
core: resolve('src/core'),
shared: resolve('src/shared'),
web: resolve('src/platforms/web'),
weex: resolve('src/platforms/weex'),
server: resolve('src/server'),
sfc: resolve('src/sfc')
}
很明显,这个 web/entry-runtime-with-compiler.js 就是入口文件了,可是回顾一下我们的目录,好像也没 web 这个文件夹。于是我们注意到 resolve 这个函数应该是有做相关的处理,然后分析一下代码:
resolve('web/entry-runtime-with-compiler.js')
-> base = 'web';
-> alias[web] = 'src/platforms/web';
-> path.resolve('src/platforms/web/entry-runtime-with-compiler.js')
Ohhhhh!!!终于找到入口了!!终于可以结束这漫长的准备工作了。
entry-runtime-with-compiler.js
一进来就看到 mount 这个函数被改写了,what's up??
冷静的一波分析发现,这原来是跟 render 有关,不知道大家会不会好奇如果传入的 options 中有 el, render, el 这几个选项的时候,会先处理哪个?代码中就清晰的表明了顺序。
/* @flow */
import Vue from "./runtime/index";
const idToTemplate = cached((id) => {
const el = query(id);
return el && el.innerHTML;
});
// 扩展$mount方法
const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el);
const options = this.$options;
// resolve template/el and convert to render function
// 先查看是否有render选项,有的话直接mount
// 没有的话,查看是否有template选项
// 有且template是以#开头(说明是id选择器),取其innerHTML
// 有且template是一个元素,取其innerHTML
// 否则直接return 实例
//
// 没有的话,查看是否有el选项
// 有且el有outerHTML,取其outerHTML
// 有且el无outterHTML,则创建一个div容器包裹它,取容器的innerHTML
// 处理完之后template = getHTML(el)
// 通过以上步骤,存在template选项则编译它,目的是获取render函数
if (!options.render) {
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 {
return this;
}
} else if (el) {
template = getOuterHTML(el);
}
if (template) {
// 编译,将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);
};
/**
* Get outerHTML of elements, taking care
* of SVG elements in IE as well.
*/
function getOuterHTML(el: Element): string {
if (el.outerHTML) {
return el.outerHTML;
} else {
const container = document.createElement("div");
container.appendChild(el.cloneNode(true));
return container.innerHTML;
}
}
Vue.compile = compileToFunctions;
export default Vue;
总结:
- 打包的入口
- 处理编译
src/platforms/web/runtime/index.js
上面我们留意到 Vue 是从本文件中被引入的,所以这里应该有 Vue 的相关信息,我们接着来看一下吧。
/* @flow */
import Vue from 'core/index'
import config from 'core/config'
import { mountComponent } from 'core/instance/lifecycle'
// install platform patch function
// 安装更新函数,它的作用是将vdom转换为dom
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
// 实现一个$mount方法,调用mountComponent
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
export default Vue
总结:
- 安装补丁函数,实现vdom -> dom
- 实现$mount
src/core/index
上面文件的 Vue 是从本文件中引入的,这次都翻到 core 里面了,总该到头了吧,快让我们康康!
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
// 初始化全局的api: Vue.use/component/set...
initGlobalAPI(Vue)
Object.defineProperty(Vue.prototype, '$isServer', {
get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
get () {
/* istanbul ignore next */
return this.$vnode && this.$vnode.ssrContext
}
})
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue
总结:
- 初始化全局api
src/core/instance/index
淦,都翻到 instance 了,总该到头了吧。。
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
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')
}
// 初始化,不过这_init是哪来的???
this._init(options)
}
// 初始化实例方法和属性
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
疑惑:
- 构造函数中的
_init方法是哪来的?
总结:
- Vue构造函数的声明
- 初始化实例的属性和方法
src/core/instance/init
抱着疑问我点开了 initMixin 定义的地方,在这里找到了答案。
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;
// 声明周期相关的属性初始化$parent
initLifecycle(vm);
// 自定义组件事件的监听
initEvents(vm);
// 插槽处理,$createElement
initRender(vm);
// 调用声明周期的钩子函数
callHook(vm, "beforeCreate");
// 下面是组件数据和状态初始化
initInjections(vm); // resolve injections before data/props
// data/props/methods/computed/watch
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, "created");
// 有el选项执行挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
}
总结:
- 定义了Vue实例的初始化过程
- 按顺序执行了初始化操作
- initLifecycle - 初始化了与生命周期相关的属性,
$parent/$root/$ref/$children等 - initEvents - 初始化自定义事件监听的相关属性以及函数
- initRender - 插槽以及
$createElement的处理 - callHook(vm, "beforeCreate"); - 调用
beforeVreate - initInjections - 注入Injection
- initState - 初始化
data/props/methods/computed/watch - initProvide - 处理Provide选项
- callHook(vm, "created") - 调用
created - 如果选项中有
el,调用$mount
- initLifecycle - 初始化了与生命周期相关的属性,
小结
通过上面一连串的追踪,我们基本了解到了 new Vue 的过程,这时候就能了解到一些常见的问题的答案:
问:new Vue({el: "#app"}) 的时候做了什么?
答:执行初始化操作 -> 得到一个根实例 -> 执行挂载函数 -> 得到渲染函数并执行 -> 生成vdom -> 通过patch函数将vdom 转成dom -> 将dom append 到 #el 中
问: 当选项中同时出现el,render,template选项的时候,会优先执行哪一个?
答:render > template > el,优先执行render函数
问:beforeCreate和 created中,能访问 injection 和 data吗?
答:beforeCreate中两个都无法访问,created中都能访问