vue源码:https://github.com/vuejs/
这里,我的调试环境为:
window10 x64
vue2.5
今天的目标:搭建调试环境,找入口文件
要学习源码,就要先学会如何调试源码,所以我们第一步可以先拉取vue源码,然后在本地配置下调试环境:
cd ./vue
npm i
npm npm i -g rollup
// 修改package.json中script中dev运行脚本,添加--sourcemap
"dev": "rollup -w -c build/config.js --sourcemap --environment TARGET:web-full-dev",
npm run dev
// 之后就会在dist目录下生成vue.js,方便我们用于测试和调试了~
配制调试环境的坑
安装过程可能提示找不到PhantomJS:
PhantomJS not found on PATH
Downloading https://github.com/Medium/phantomjs/releases/download/v2.1.1/phantomjs-2.1.1-windows.zip Saving to C:\Users\ADMINI~1\AppData\Local\Temp\phantomjs\phantomjs-2.1.1-windows.zip Receiving...
根据提示,去下载压缩包,放到对应位置就好提示以下错误
Error: Could not load D:\vue-source\vue\src\core/config (imported by D:\vue-source\vue\src\platforms\web\entry-runtime-with-compiler.js): ENOENT: no such file or directory, open 'D:\vue-source\vue\src\core/config'
查了下资料,说是rollup-plugin-alias插件中解析路径的问题,有人提PR了(https://github.com/vuejs/vue/issues/2771),尤大大说是没有针对window10做处理造成的,解决方法是将 node_modules/rollup-plugin-alias/dist/rollup-plugin-alias.js 改为
// var entry = options[toReplace]
// 81行,上面那句,改为:
var entry = normalizeId(options[toReplace]);
打包后dist输出的文件一些后缀说明
- 有runtime字样的:说明只能在运行时运行,不包含编译器(也就是如果我们直接使用template模板,是不能正常编译识别的)
- common:commonjs规范,用于webpack1
- esm:es模块,用于webpack2+
- 没带以上字样的: 使用umd,统一模块标准,兼容cjs和amd,用于浏览器,也是之后我们要用于测试的文件
我们可以在test文件夹下,创建我们的测试文件test1.html:
初始化流程
{{msg}}
找入口文件
- 在package.json中,dev运行脚本中找配置文件(-c 指向配置文件):
rollup -w -c build/config.js --sourcemap --environment TARGET:web-full-dev
- 进入配置文件中,根据
TARGET
找到对应的配置文件TARGET:web-full-dev
,搜索这个环境,让到对应的entry入口文件
// 1. build/config.js根据target环境,来找entry入口
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
},
}
// 2. 查看resolve解析方法,从中看出web是在别名文件中有对应地址
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)
}
}
// 3. 根据aliases找到alias.js文件,从中找到web对应的相应地址
module.exports = {
web: resolve('src/platforms/web'),
}
// 4. 最后根据拼接规则,我们终于找到真正的对应入口
src/platforms/web/entry-runtime-with-compiler.js
查看入口文件
带个问题去看源码,以下这个vue实例中,最终挂载起作用是的哪个?
// render,template,el哪个的优先级高?
const app = new Vue({
el: "#demo",
template: "template",
render(h) {
return h('div', 'render')
},
data: {
foo: 'foo'
}
})
// 答案:render > template > el
可以从源码找答案,在主要的地方我已添加中文注释(英文注释是源码本身的),可查看对应注释地方:
// 保存原来的$mount
const mount = Vue.prototype.$mount
// 覆盖默认的$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// 如果看到有以下这样注释的,一般用于调试阶段输出一些警告信息,我们在学习时为了简单点,可以直接忽略的部分
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to or - mount to normal elements instead.`
)
return this
}
// 从这里开始,解析我们的配置
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)
//...
}
} 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 = getOuterHTML(el)
}
// 如果存在模板,执行编译
if (template) {
// ...
// 编译得到渲染函数
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 最后执行挂载,可以看到使用的是父级原来的mount方式挂载
return mount.call(this, el, hydrating)
}
从以上1,2,3步骤中,我们就可以得出刚刚的答案了。
src/platforms/web/entry-runtime-with-compiler.js文件作用:入口文件,覆盖$mount,执行模板解析和编译工作
找Vue的构造函数
这里主要找Vue的构造函数,中间路过一些文件会写一些大概的作用,但主线不会偏离我们的目标
- 在入口文件
entry-runtime-with-compiler.js
中,可以查看Vue引入文件
import Vue from './runtime/index'
-
/runtime/index.js
文件
import Vue from 'core/index'
// 定义了patch补丁:将虚拟dom转为真实dom
Vue.prototype.__patch__ = inBrowser ? patch : noop
// 定义$mount
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
-
core/index.js
文件
import Vue from './instance/index'
// 定义了全局API
initGlobalAPI(Vue)
-
src/core/instance/index.js
文件
终于找到了Vue的构造函数,它只做了一件事,就是初始化,这个初始化方法是通过minxin传送到这个文件的,所以我们接下来是要去查看Init的方法,这也是我们以后要常看的一个文件
// 构造函数
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) // _init方法是通过mixin传入的,从这里可以找到初始化方法
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
初始化方法定义的文件 src/core/instance/init.js
划重点,比较重要,可以看出初始化操作主要做以下事件:
initLifecycle(vm) // 初始化生命周期,声明$parten,$root,$children(空的),$refs
initEvents(vm) // 对父组件传入的事件添加监听
initRender(vm) // 声明$slot,$createElement()
callHook(vm, 'beforeCreate') // 调用beforeCreate钩子
initInjections(vm) // 注入数据 resolve injections before data/props
initState(vm) // 重中之重:数据初始化,响应式
initProvide(vm) // 提供数据 resolve provide after data/props
callHook(vm, 'created') // 调用created钩子
// 定义初始化方法
export function initMixin (Vue: Class) {
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
// 合并选项,将用户设置的options和vue默认设置的options,做一个合并处理
// 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
)
}
//...
// 重点在这里,初始化的一堆操作!
// expose real self
vm._self = vm
initLifecycle(vm) // 初始化生命周期,声明$parten,$root,$children(空的),$refs,这里说明创建组件是自上而下的
initEvents(vm) // 对父组件传入的事件添加监听
initRender(vm) // 声明$slot,$createElement()
callHook(vm, 'beforeCreate') // 调用beforeCreate钩子
initInjections(vm) // 注入数据 resolve injections before data/props
initState(vm) // 重中之重:数据初始化,响应式
initProvide(vm) // 提供数据 resolve provide after data/props
callHook(vm, 'created') // 调用created钩子
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
如有错误之处,还望指出哈