vue源碼:https://github.com/vuejs/
這里肴沫,我的調(diào)試環(huán)境為:
window10 x64
vue2.5
今天的目標(biāo):搭建調(diào)試環(huán)境醋寝,找入口文件
要學(xué)習(xí)源碼甚亭,就要先學(xué)會如何調(diào)試源碼嘹悼,所以我們第一步可以先拉取vue源碼萨醒,然后在本地配置下調(diào)試環(huán)境:
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浮毯,方便我們用于測試和調(diào)試了~
配制調(diào)試環(huán)境的坑
安裝過程可能提示找不到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...
根據(jù)提示级零,去下載壓縮包断医,放到對應(yīng)位置就好提示以下錯誤
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規(guī)范硬耍,用于webpack1
- esm:es模塊垄琐,用于webpack2+
- 沒帶以上字樣的: 使用umd,統(tǒng)一模塊標(biāo)準(zhǔn)默垄,兼容cjs和amd此虑,用于瀏覽器,也是之后我們要用于測試的文件
我們可以在test文件夾下口锭,創(chuàng)建我們的測試文件test1.html:
<!-- test/test1.html -->
<head>
<script src="../dist/vue.js"></script>
</head>
<body>
<div id="demo">
<h1>初始化流程</h1>
<p>{{msg}}</p>
</div>
<script>
const app = new Vue({
el: "#demo",
data: {
msg: 'hello'
}
})
</script>
</body>
找入口文件
- 在package.json中朦前,dev運行腳本中找配置文件(-c 指向配置文件):
rollup -w -c build/config.js --sourcemap --environment TARGET:web-full-dev
- 進(jìn)入配置文件中,根據(jù)
TARGET
找到對應(yīng)的配置文件TARGET:web-full-dev
鹃操,搜索這個環(huán)境韭寸,讓到對應(yīng)的entry入口文件
// 1. build/config.js根據(jù)target環(huán)境,來找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是在別名文件中有對應(yīng)地址
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. 根據(jù)aliases找到alias.js文件恩伺,從中找到web對應(yīng)的相應(yīng)地址
module.exports = {
web: resolve('src/platforms/web'),
}
// 4. 最后根據(jù)拼接規(guī)則,我們終于找到真正的對應(yīng)入口
src/platforms/web/entry-runtime-with-compiler.js
查看入口文件
帶個問題去看源碼椰拒,以下這個vue實例中晶渠,最終掛載起作用是的哪個?
// render,template,el哪個的優(yōu)先級高燃观?
const app = new Vue({
el: "#demo",
template: "<div>template</div>",
render(h) {
return h('div', 'render')
},
data: {
foo: 'foo'
}
})
// 答案:render > template > el
可以從源碼找答案褒脯,在主要的地方我已添加中文注釋(英文注釋是源碼本身的),可查看對應(yīng)注釋地方:
// 保存原來的$mount
const mount = Vue.prototype.$mount
// 覆蓋默認(rèn)的$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// 如果看到有以下這樣注釋的缆毁,一般用于調(diào)試階段輸出一些警告信息番川,我們在學(xué)習(xí)時為了簡單點,可以直接忽略的部分
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - 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)
}
// 如果存在模板颁督,執(zhí)行編譯
if (template) {
// ...
// 編譯得到渲染函數(shù)
const { render, staticRenderFns } = compileToFunctions(template, {
shouldDecodeNewlines,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 最后執(zhí)行掛載,可以看到使用的是父級原來的mount方式掛載
return mount.call(this, el, hydrating)
}
從以上1浇雹,2沉御,3步驟中,我們就可以得出剛剛的答案了昭灵。
src/platforms/web/entry-runtime-with-compiler.js文件作用:入口文件嚷节,覆蓋$mount,執(zhí)行模板解析和編譯工作
找Vue的構(gòu)造函數(shù)
這里主要找Vue的構(gòu)造函數(shù)虎锚,中間路過一些文件會寫一些大概的作用硫痰,但主線不會偏離我們的目標(biāo)
- 在入口文件
entry-runtime-with-compiler.js
中,可以查看Vue引入文件
import Vue from './runtime/index'
-
/runtime/index.js
文件
import Vue from 'core/index'
// 定義了patch補洞芑ぁ:將虛擬dom轉(zhuǎn)為真實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的構(gòu)造函數(shù)效斑,它只做了一件事,就是初始化柱徙,這個初始化方法是通過minxin傳送到這個文件的缓屠,所以我們接下來是要去查看Init的方法奇昙,這也是我們以后要常看的一個文件
// 構(gòu)造函數(shù)
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) // 對父組件傳入的事件添加監(jiān)聽
initRender(vm) // 聲明$slot,$createElement()
callHook(vm, 'beforeCreate') // 調(diào)用beforeCreate鉤子
initInjections(vm) // 注入數(shù)據(jù) resolve injections before data/props
initState(vm) // 重中之重:數(shù)據(jù)初始化晦攒,響應(yīng)式
initProvide(vm) // 提供數(shù)據(jù) resolve provide after data/props
callHook(vm, 'created') // 調(diào)用created鉤子
// 定義初始化方法
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
// 合并選項闽撤,將用戶設(shè)置的options和vue默認(rèn)設(shè)置的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,這里說明創(chuàng)建組件是自上而下的
initEvents(vm) // 對父組件傳入的事件添加監(jiān)聽
initRender(vm) // 聲明$slot栋操,$createElement()
callHook(vm, 'beforeCreate') // 調(diào)用beforeCreate鉤子
initInjections(vm) // 注入數(shù)據(jù) resolve injections before data/props
initState(vm) // 重中之重:數(shù)據(jù)初始化闸餐,響應(yīng)式
initProvide(vm) // 提供數(shù)據(jù) resolve provide after data/props
callHook(vm, 'created') // 調(diào)用created鉤子
// ...
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
如有錯誤之處,還望指出哈