前言:
记录调试vue源码的方法,理解vue的核心:系统性的理解一个好的框架的产生。我自己的学习方法是,最先概括性的全局了解一下,然后针对各个知识点搜集知识逐个攻破,得到正反馈。
看完这篇笔记可以让你清楚的知道怎么调试vue 然后收藏它 当你有时间 打开vue源码开始调试它。
准备:
1.vue源码:2.6.14 github拉取源码
2.使用Rollup 打包工具 比webpack更轻便 rollup只处理js文件 在这些库中更方便 vue就是用的rollup
3.给package.json 的dev命令设置 sourcemap 方便调试源码 查看报错信息
目标:
了解怎么看源码;学会查找入口文件;熟悉vue的入口文件;vue的实例成员;vue的静态成员;
这篇文章会涉及到许多知识:我自己的建议是结合着实例去掌握自己不会不懂的知识。再针对不懂的知识进行逐个击破:
阅读源码的时候,或者是做项目的时候,会遇到很多自己不懂的知识点 或者不熟悉的知识点,或者出现问题。不过不要怕,很多问题都是已经遇到过得,有了答案,很多知识点都是前人已经总结好的,我们只需要迈出去那一小步,去看去学习去总结。一个一个的攻克会给你带来成就感的。
1阅读源码前准备
这个小标题下涉及到的知识 以及可以延伸的知识点,感兴趣的可以在主任务外自由开发分支:
1.打包工具的区别:Rollup、Webpack、Gulp、Grunt
2.打包后不同文件夹后缀的含义:xxx.js xxx.min.js xxx.runtime.js xxx.js.map
3.sourcemap的含义;runtime的含义 对应第二个知识点
4.npm script 脚本传参的含义:--sourcemap
下载好源码后:
1.安装依赖
npm i
2.设置 sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
3.启动项目:
npm run dev
可以看到这个打包的js platforms\web\entry-runtime-with-compiler.js
平台相关目录下的web平台下的entry-runtime-with-compiler版本打包的jis runtime 是运行时版本(稍后解释)
4.调试案例:
随便找个案例:
examples/grid/index.html 将第八行的vue.min.js 改为vue.js 方便开发环境查看控制台的警告
5.1运行 npm run build
把vue的不同版本js 都打包出来
这里的vue.js.map 就是之前我们 npm dev 的时候 在 npm script里 添加了--sourcemap参数 是源文件的信息文件记录
dist文件里的readme.md 对vue的不同版本解释
Explanation of Build Files 编译文件说明
| UMD | CommonJS | ES Module | |
|---|---|---|---|
| Full | vue.js | vue.common.js | vue.esm.js |
| Runtime-only | vue.runtime.js | vue.runtime.common.js | vue.runtime.esm.js |
| Full (production) | vue.min.js | ||
| Runtime-only (production) | vue.runtime.min.js |
Terms
- Full: builds that contain both the compiler and the runtime. (包含编译器和运行时的构建。)
- 完整版 同时包含编译器和运行时的版本
- Compiler: code that is responsible for compiling template strings into JavaScript render functions.(负责将模板字符串编译成JavaScript渲染函数的代码。)
- 编译器 用来将模板字符串编译成JavaScript渲染函数的代码 体积大 体积大 效率低(就是我们new Vue时 传递的template 把这个转换成render函数 我们可以传递template参数 和 render函数 )
- Runtime: code that is responsible for creating Vue instances, rendering and patching virtual DOM, etc. Basically everything minus the compiler.(负责创建Vue实例、渲染和修补虚拟DOM等的代码。基本上除了编译器。)
- 运行时 体积小 效率高 去除了编译器的代码
- UMD: UMD builds can be used directly in the browser via a
<script>tag. The default file from Unpkg CDN at unpkg.com/vue is the Runtime + Compiler UMD build (vue.js).(UMD构建可以通过上的默认文件是Runtime + Compiler UMD构建(vue.js)。) - UMD版本的通用模块版本 支持多种模块方式 vue.js 默认文件就是运行时+编译器的UMD版本
- CommonJS: CommonJS builds are intended for use with older bundlers like browserify or webpack 1. The default file for these bundlers (
pkg.main) is the Runtime only CommonJS build (vue.runtime.common.js).(CommonJS构建旨在与较老的打包工具(如browserify或webpack 1)一起使用。这些绑定器的默认文件(pkg.main)是仅运行时的CommonJS构建(vue.runtime.common.js)。) - CommonJS版本用来配合老的打包工具
- ES Module: ES module builds are intended for use with modern bundlers like webpack 2 or rollup. The default file for these bundlers (
pkg.module) is the Runtime only ES Module build (vue.runtime.esm.js).(ES模块构建旨在与webpack 2或rollup等现代打包工具一起使用。这些绑定器(pkg.module)的默认文件是仅运行时ES模块构建(vue.runtime.esm.js)。) - 从2.6开始 Vue提供两个ESM构建文件 为现代打包工具提供的版本 ESM 格式被设计为可以被静态分析 所以打包工具可以利用这一点进行 tree-shaking 并将用不到的代码排除出最终的包
Runtime + Compiler vs. Runtime-only
If you need to compile templates on the fly (e.g. passing a string to the template option, or mounting to an element using its in-DOM HTML as the template), you will need the compiler and thus the full build.
When using vue-loader or vueify, templates inside *.vue files are compiled into JavaScript at build time. You don't really need the compiler in the final bundle, and can therefore, use the runtime-only build.
Since the runtime-only builds are roughly 30% lighter-weight than their full-build counterparts, you should use it whenever you can. If you wish to use the full build instead, you need to configure an alias in your bundler.
如果你需要动态编译模板(例如传递一个字符串到模板选项,或者挂载一个元素使用它的dom内HTML作为模板),你将需要编译器,因此需要完整的构建。
当使用vue-loader或vueify时,*. .vue文件在构建时编译成JavaScript。最终包中并不需要编译器,因此可以使用仅运行时构建。
由于仅运行时构建的重量大约比完整构建的重量轻30%,所以您应该尽可能地使用它。如果希望使用完整构建,则需要在绑定器中配置别名。
5.2 完整版和运行时的区别 vue.js 和vue.runtime.js
在example文件夹里面新建一个调试文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Runtime+Compiler</title>
</head>
<body>
<div id="app">
Hello World
</div>
<script src="../../dist/vue.js"></script>
<!-- <script src="../../dist/vue.runtime.js"></script> -->
<script>
// compiler
// 需要编译器,把 template 转换成 render 函数
const vm = new Vue({
el: '#app',
template: '<h1>{{ msg }}</h1>',
data: {
msg: 'Hello Vue'
}
})
// const vm1 = new Vue({
// el: '#app',
// // template: '<h1>{{ msg }}</h1>',
// render(h) {
// return h('h1', this.msg)
// },
// data: {
// msg: 'Hello Vue'
// }
// })
// // 如果同时设置template和render此时会渲染什么?
// const vm2 = new Vue({
// el: '#app',
// template: '<h1>Hello Template</h1>',
// render(h) {
// return h('h1', 'Hello Render')
// }
// })
</script>
</body>
</html>
其中 vue.js 可以使用template 因为包含编译器 可以将template 转换成render函数
<script src="../../dist/vue.js"></script>
<script>
// compiler
// 需要编译器,把 template 转换成 render 函数
const vm = new Vue({
el: '#app',
template: '<h1>{{ msg }}</h1>',
data: {
msg: 'Hello Vue'
}
})
</script>
vue.runtime.js 不可以使用template,可以直接使用render函数
<script src="../../dist/vue.runtime.js"></script>
<script>
const vm = new Vue({
el: '#app',
// template: '<h1>{{ msg }}</h1>',
render(h) {
return h('h1', this.msg)
},
data: {
msg: 'Hello Vue'
}
})
</script>
这是对有没有编译器版本的vuejs文件的测试
知识点:
1.打包工具:
可以查看我之前的 webpack源码笔记 和gulp的学习笔记 以及这次出现的rollup打包工具,熟悉掌握他们的不同,最新新出现的vite打包工具 满满的都是知识 这里稍稍总结一下:
rollup:像vue 或者其它开源库使用,只处理js文件,比webpack方便快捷 即框架、组件库、生成单一umd文件的场景
webpack:前端工程化绕不开的更全面、强大的打包工具,我们做项目 工程化的时候频繁使用 即应用程序开发
gulp:基于流的操作,输入、输出的管道思想对静态资源处理,提供方法,开发人员自己对文件进行操作处理。对 静态资源密集型场景,如css、img等静态资源整合常使用
传送门:
2.webpack、gulp、rollup、tsc/babel 使用对比
2.打包后不同文件夹后缀的含义:xxx.js xxx.min.js xxx.runtime.js xxx.js.map
打包后的文件:xxx.js xxx.min.js xxx.runtime.js xxx.js.map
.js是JavaScript 打包后的源码文件
.min.js是压缩版的js文件 目的就是为了减小体积,防止窥视和窃取源代码。
.runtime.js 是vue的运行时文件
xxx.js.map 是源码打包后的存储信息位置的独立文件 runtime已经知道了 现在看一下sourcemap
sourcemap:Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置
在vue里的体现就是 多出来了vue.js.map 是对应vue.js的位置信息
在 npm script的命令行里添加的 --sourcemap 在vue.js的最后一行就出现了这个
延伸一下: 在webpack这个打包工具上,用的到sourcemap的时候是 webpack.config.js 中添加 devtools参数,
传送门:devtools参数
4.npm script 脚本传参的含义:--sourcemap
所以这个 --sourcemap 也就知道怎么回事了 我们这个是学习script的用法
稍稍总结一下:
npm 允许在package.json文件里面,使用scripts字段定义脚本命令
执行npm run,就会自动新建一个 Shell,在这个 Shell 里面执行指定的脚本命令
就会先去当前目录的node_modules/.bin子目录里面去寻找, 在webpack的时候就有介绍 执行webpack cli 的时候 就是去.bin目录下最后寻找到文件夹执行了compiler.run函数
延伸一下:
npm的命令和npx的命令区别 npm npx yarn cnpm pnpm 满满的知识 可以看我之前的gulp的学习笔记有介绍
2.查找vue的入口文件
1.npm script 命令行查找script/config.js
我们在npm run dev 的时候就已经出现了一个文件夹
可以看到这个打包的js platforms\web\entry-runtime-with-compiler.js
还可以在npm script的命令行查看 "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev",
scripts/config.js
2.web/entry-runtime-with-compiler.js
都可以找到这个文件夹 web/entry-runtime-with-compiler.js
这个文件夹看做了什么:
1.是web平台相关的入口
2.重写了平台相关的$mount()方法
- 注册了Vue.compile()方法,传递一个HTML字符串,返回render函数。
这里我们看到Vue是什么 还没有找到 继续去./runtime/index 这个文件夹寻找 就是src/platforms/web/runtime/index##### 3.src/platforms/web/runtime/index
看这个文件夹做了什么
1.web平台相关
2.注册和平台相关的全局指令 v-model v-show
3.注册和平台相关的全局组件 v-transition v-transition-group
4.全局方法: patch 把虚拟dom转换成真实dom
5.$mount 挂载方法
继续看到Vue引用在core/index
4.core/index
1.与平台无关
2.设置vue的静态方法 initGlobalAPI(VUE)
继续看到vue不在这里 在/instance/index
5.src/core/instance/index
终于找到了 Vue 在这里定义了vue构造函数 导出了vue
1.定义构造函数 调用this._init方法
2.给vue中混入了常用的实例成员 #### 3.vue的静态成员
知识点:
我们回到 刚刚在core/index里发现的initGlobalAPI 这个方法
1.跳转到 global-api/index
从这个文件夹名称可以看出 这是一个存放全局api的地方
最上面是import的 一些引入 然后是
1.初始化Vue.config对象 并禁止对config赋值 可以在里面挂载方法
2.初始化Vue.until 工具方法
3.初始化Vue的静态方法 set delete nextTick
4.初始化 Vue.observable 让对象可响应式
5.初始化Vue.options对象 并赋值 'components', 'directives', 'filters'三个成员
6.设置keep-alive组件 extend把一个对象的属性拷贝到另一个对象中来
7.注册Vue.use 用来注册插件
1.1extend
// 设置keep-alive组件 extend把一个对象的属性拷贝到另一个对象中来
// export function extend (to: Object, _from: ?Object): Object {
// for (const key in _from) {
// to[key] = _from[key]
// }
// return to
// }
1.2 initUse(Vue)
给Vue.ues赋值 参数是plugin 是一个函数或者对象
处理this
如果插件是一个对象,必须提供 install 方法
/* @flow */
import { toArray } from '../util/index'
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
// 谁调用 this就是谁 这是vue这个构造函数
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
// // 判断插件有没有被注册
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters 把 plugin第一个参数去掉, 把this插入到第一个元素
const args = toArray(arguments, 1)
args.unshift(this)//this 指向 Vue 对象,所以数组参数第一个始终是vue对象
// 调用插件install方法和传递参数
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args) // plugin是函数的话 ,直接调用,传入参数。注意:其内this为null
}
installedPlugins.push(plugin)
return this
}
}
插件:
插件通常用来为 Vue 添加全局功能。
添加全局方法或者 property。如:vue-custom-element
添加全局资源:指令/过滤器/过渡等。如 vue-touch
通过全局混入来添加一些组件选项。如 vue-router
添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现
一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router
1.3initMixin(Vue):
注册Vue.mixin 实现混入
混入:
混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
minix:混入的含义: 公共逻辑: 比如页码
将组件的公共逻辑或者配置提取出来,哪个组件需要用到时,直接将提取的这部分混入到组件内部即可。这样既可以减少代码冗余度,也可以让后期维护起来更加容易。
mixin中的生命周期函数会和组件的生命周期函数一起合并执行。
mixin中的data数据在组件中也可以使用
mixin中的方法在组件内部可以直接调用
生命周期函数合并后执行顺序:先执行mixin中的,后执行组件的
不同组件中的mixin是相互独立的!
源码:
// vue/src/core/global-api/mixin.js
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
// vue/src/core/util/options.js
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {
if (child.mixins) { // 判断有没有mixin 也就是mixin里面挂mixin的情况 有的话递归进行合并
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}
const options = {}
let key
for (key in parent) {
mergeField(key) // 先遍历parent的key 调对应的strats[XXX]方法进行合并
}
for (key in child) {
if (!hasOwn(parent, key)) { // 如果parent已经处理过某个key 就不处理了
mergeField(key) // 处理child中的key 也就parent中没有处理过的key
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key) // 根据不同类型的options调用strats中不同的方法进行合并
}
return options
}
上述代码的作用主要是这3点
- 优先递归处理 mixins
- 先遍历合并 parent 中的key,调用mergeField方法进行合并,然后保存在变量options
- 再遍历 child,合并补上 parent 中没有的key,调用mergeField方法进行合并,保存在变量options
其实核心在于strats中对应的不同类型的处理方法,我们接下来分为几种类型来看下对应的合并策略
在我们调用Vue.mixin的时候会通过mergeOptions方法将全局基础options(component', 'directive', 'filter)进行合并在mergeOptions内部优先进行mixins的递归合并,然后先父再子调用mergeField进行合并,不同的类型走不同的合并策略 它不是简单的把属性从一个对象里复制到另外一个对象里,而是根据被合并的不同的选项有着不同的合并策略。这就是设计模式中非常典型的策略模式。
替换型策略有props、methods、inject、computed, 就是将新的同名参数替代旧的参数
合并型策略是data, 通过set方法进行合并和重新赋值
队列型策略有生命周期函数和watch,原理是将函数存入一个数组,然后正序遍历依次执行
叠加型有component、directives、filters,将回调通过原理链联系在一起
mixin的优缺点
优点
- 提高代码复用性
- 无需传递状态
- 维护方便,只需要修改一个地方即可
缺点
- 命名冲突
- 滥用的话后期很难维护
- 不好追溯源,排查问题稍显麻烦
- 不能轻易的重复代码
var mixin = {
created: function () {
console.log('混入对象的钩子被调用')
}
}
new Vue({
mixins: [mixin],
created: function () {
console.log('组件钩子被调用')
}
})
// => "混入对象的钩子被调用"
// => "组件钩子被调用"
1.4.initExtend(Vue)
基于传入的option 返回一个组件的构造函数
Vue.extend是 Vue 构造函数的一个静态方法,它提供了一种灵活的挂载组件的方式,它在日常开发中使用不多,但是在一些特殊场景会派上用场。在 ElementUI 里,我们使用this.$message('hello')的时候,其实就是通过这种方式创建一个组件实例,然后再将这个组件挂载到了 body 上。
Vue.extend = function (extendOptions: Object): Function {
extendOptions = extendOptions || {}
const Super = this
const SuperId = Super.cid //SuperId就保存Vue中的唯一标识(每个实例都有自己唯一的cid)
const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
if (cachedCtors[SuperId]) {
return cachedCtors[SuperId]
}
const name = extendOptions.name || Super.options.name //name变量来保存组件的名字
if (process.env.NODE_ENV !== 'production' && name) {
validateComponentName(name)
}
const Sub = function VueComponent (options) {
// 调用init初始化
this._init(options)
}
// 原型继承Vue
//创建一个子类Sub,这里我们通过继承,使Sub拥有了Vue的能力,并且添加了唯一id(每个组件的唯一标识符)
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.cid = cid++
//这里调用了mergeOptions函数实现了父类选项与子类选项的合并,并且子类的super属性指向了父类。
Sub.options = mergeOptions(
Super.options,
extendOptions
)
Sub['super'] = Super
// For props and computed properties, we define the proxy getters on
// the Vue instances at extension time, on the extended prototype. This
// avoids Object.defineProperty calls for each instance created. 初始化了props和computed.
if (Sub.options.props) {
initProps(Sub)
}
if (Sub.options.computed) {
initComputed(Sub)
}
// allow further extension/mixin/plugin usage 将父类的方法复制到子类
Sub.extend = Super.extend
Sub.mixin = Super.mixin
Sub.use = Super.use
// create asset registers, so extended classes
// can have their private assets too.
ASSET_TYPES.forEach(function (type) {
Sub[type] = Super[type]
})
// enable recursive self-lookup
if (name) {
Sub.options.components[name] = Sub
}
// keep a reference to the super options at extension time.
// later at instantiation we can check if Super's options have
// been updated. 新增属性
Sub.superOptions = Super.options
Sub.extendOptions = extendOptions
Sub.sealedOptions = extend({}, Sub.options)
// cache constructor 将父类的id保存在子类的属性上,属性值为子类,在之前会进行判断如果构造过子类,就直接将父类保存过的id值给返回了,就是子类本身不需要重新初始化,,作为一个缓存策略。
cachedCtors[SuperId] = Sub
return Sub
}
建一个类来继承了父级,顶级一定是Vue.这个类就表示一个组件,我们可以通过new的方式来创建。学习了extend我们就很容易的实现一个编程式组件。
案例:
其实这个初始化和平时的new Vue()是一样的,毕竟两个执行的同一个方法。但是在实际的使用中,我们可能还需要给组件传 props,slots 以及绑定事件.
<template>
<div class="message-box">
{{ message }}
</div>
</template>
<script>
export default {
props: {
message: {
type: String,
default: ''
}
}
}
</script>
const MessageBoxCtor = Vue.extend(MessageBox)
new MessageBoxCtor({
propsData: {
message: 'hello'
}
}).$mount('#target')
为什么使用propsData
if (opts.props) initProps(vm, opts.props)
function initProps(vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {}
const props = (vm._props = {})
// ...省略其他逻辑
}
这里的 propsData 就是数据源,他会从vm.$options.propsData上取,上文说过在执行_init的时候new MessageBoxCtor(options)的options会被合并和vm.$options上,所以我们就可以在options中传入propsData属性,使得initProps()能取到这个值,从而进行props的初始化。绑定事件:
const MessageBoxCtor = Vue.extend(MessageBox)
const messageBoxInstance = new MessageBoxCtor({
propsData: {
message: 'hello'
}
}).$mount('#target')
messageBoxInstance.$on('some-event', () => {
console.log('success')
})
使用插槽;
<template>
<div class="message-box">
{{ message }}
<slot name="footer"/>
</div>
</template>
<script>
export default {
props: {
message: {
type: String,
default: ''
}
}
}
</script>
const MessageBoxCtor = Vue.extend(MessageBox)
const messageBoxInstance = new MessageBoxCtor({
propsData: {
message: 'hello'
}
})
const h = this.$createElement
messageBoxInstance.$scopedSlots = {
footer: function() {
return [h('div', 'slot-content')]
}
}
messageBoxInstance.$mount('#target')
这里需要注意的是$mount一定要在设置完$scopedSlots之后,因为$mount中会执行渲染函数,我们要保证在执行渲染函数时能获取到$scopedSlots
如果你想使用作用域插槽,也很简单,和普通插槽是一样的,只需要在函数中接收参数就可以了:
<slot name="head" :message="message"></slot>
复制代码
messageBoxInstance.$scopedSlots = {
footer: function(slotData) {
return [h('div', slotData.message)]
}
}
复制代码
这样就可以成功渲染出message了。
1.5 initAssetRegisters(Vue)
注册Vue.component(), Vue.directive(), Vue.filter()
/* @flow */
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'
export function initAssetRegisters (Vue: GlobalAPI) {
/**
* Create asset registration methods.
*/
ASSET_TYPES.forEach(type => {
Vue[type] = function (
id: string,
definition: Function | Object
): Function | Object | void {
if (!definition) {
return this.options[type + 's'][id]
} else {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && type === 'component') {
validateComponentName(id)
}
if (type === 'component' && isPlainObject(definition)) {
definition.name = definition.name || id
definition = this.options._base.extend(definition)
}
if (type === 'directive' && typeof definition === 'function') {
definition = { bind: definition, update: definition }
}
this.options[type + 's'][id] = definition
return definition
}
}
})
}
'shared/constants
export const ASSET_TYPES = [
'component',
'directive',
'filter'
]
这里的 vue的静态成员就介绍完了#### 4.vue的实例成员
4.1src/core/instance/index
注册vm的_init方法 初始化vm
initMixin(Vue)
注册vm的props/delete/$watch/
stateMixin(Vue)
初始化事件相关方法 off/$emit
eventsMixin(Vue)
初始化生命周期相关的混入方法 _update destroy
lifecycleMixin(Vue)
混入render $nextTick _render
renderMixin(Vue)
4.2_initMixin(Vue) _init()
// 给Vue类的原型上绑定_init方法
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options 合并options 用户传入的和vue构造函数的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 设置渲染时代理对象 _renderProxy */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
// 渲染过程中 会使用
vm._renderProxy = vm
}
// expose real self 调用一些初始化函数来为Vue实例初始化一些属性,事件,响应式数据等
vm._self = vm
initLifecycle(vm) // 初始化生命周期
initEvents(vm) // 初始化事件 vm的事件监听初始化 父组件绑定在当前组件上的事件
initRender(vm) // 初始化渲染
callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
initInjections(vm) // resolve injections before data/props //初始化injections
initState(vm) // 初始化props,methods,data,computed,watch
initProvide(vm) // resolve provide after data/props // 初始化 provide
callHook(vm, 'created') // 调用生命周期钩子函数
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果传入了则调用$mount函数进入模板编译与挂载阶段, 没有传入 需要用户手动执行vm.$mount方法才进入下一个生命周期阶段。
// 创建构造器
/* var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function() {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建 Profile 实例,并挂载到一个元素上。 new Profile()就会调用_init方法
new Profile().$mount('#mount-point')
也可以这样子 new Profile({ el: '#mount-point' })
*/
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
1.vm赋值 const vm = this
2.合并用户传入的和vue构造函数的options
2.1 mergeOptions ( parent: Object,child: Object, vm?: Component): Object {} src/core/util/options.js
3.调用一些初始化函数来为Vue实例初始化一些属性,事件,响应式数据等
4.2.1 initLifecycle(vm)
// 初始化生命周期
Vue实例上挂载了一些属性并设置了默认值 找到当前组件的根组件$root 抽象组件实例中一定有个属性abstract:true 这就是一个自上到下将根实例的$root属性依次传递给每一个子实例的过程。
4.2.2 initEvents(vm)
// 初始化事件 初始化实例的事件系统。
父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化;而浏览器原生事件是在父组件中处理。
实例初始化阶段调用的初始化事件函数initEvents实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件
4.2.3 initRender(vm)
// 初始化渲染
初始化插槽信息$slots以及初始化$createElement方法,
使用defineReactive方法让$attrs、$listeners响应式
整个过程其实就是解析了组件的 options 配置项与父组件的绑定参数,并对插槽和数据域插槽进行不同处理,最后给组件添加 _createElement 的事件指向绑定,并响应式处理两个组件内部没有直接定义的参数/事件。
4.2.4 callHook(vm, 'beforeCreate')
// 调用生命周期钩子函数
4.2.5 initInjections(vm)
// resolve injections before data/props //初始化injections
inject选项,那必然离不开provide选项,这两个选项都是成对出现的,它们的作用是:允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。并且
provide 选项应该是一个对象或返回一个对象的函数
4.2.6initState(vm)
// 初始化props,methods,data,computed,watch这个顺序也就知道了为什么data中可以使用props,在watch中可以观察data和props,之所以可以这样做,就是因为在初始化的时候遵循了这种顺序,先初始化props,接着初始化data,最后初始化watch
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initProps
function initProps (vm: Component, propsOptions: Object) {
//父组件传入的真实props数据。
const propsData = vm.$options.propsData || {}
//指向vm._props的指针,所有设置到props变量中的属性都会保存到vm._props中
const props = vm._props = {}
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
//指向vm.$options._propKeys的指针,缓存props对象中的key,将来更新props时只需遍历vm.$options._propKeys数组即可得到所有props的key
const keys = vm.$options._propKeys = []
// 当前组件是否为根组件。
const isRoot = !vm.$parent
// root instance props should be converted 判断当前组件是否为根组件,如果不是,那么不需要将props数组转换为响应式的,toggleObserving(false)用来控制是否将数据转换成响应式
if (!isRoot) {
toggleObserving(false)
}
// 遍历props选项拿到每一对键值,先将键名添加到keys中,然后调用validateProp函数(关于该函数下面会介绍)
//校验父组件传入的props数据类型是否匹配并获取到传入的值value,然后将键和值通过defineReactive函数添加到props(即vm._props)中
for (const key in propsOptions) {
keys.push(key)
const value = validateProp(key, propsOptions, propsData, vm)
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
const hyphenatedKey = hyphenate(key)
if (isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
)
}
defineReactive(props, key, value, () => {
if (vm.$parent && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
)
}
})
} else {
defineReactive(props, key, value)
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
//断这个key在当前实例vm中是否存在,如果不存在,则调用proxy函数在vm上设置一个以key为属性的代码,当使用vm[key]访问数据时,其实访问的是vm._props[key]
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
validateProp (key,propOptions,propsData,vm)
validateProp函数的定义位于源码的src/core/util/props.js中
key:遍历propOptions时拿到的每个属性名。
propOptions:当前实例规范化后的props选项。
propsData:父组件传入的真实props数据。
vm:当前实例
const prop = propOptions[key]
const absent = !hasOwn(propsData, key)
let value = propsData[key]
prop:当前key在propOptions中对应的值。
absent:当前key是否在propsData中存在,即父组件是否传入了该属性。
value:当前key在propsData中对应的值,即父组件对于该属性传入的真实值
判断prop的type属性是否是布尔类型(Boolean),getTypeIndex函数用于判断prop的type属性中是否存在某种类型,如果存在,则返回该类型在type属性中的索引(因为type属性可以是数组),如果不存在则返回-1。
assertProp
assertProp (prop,name,value,vm,absent)
是校验父组件传来的真实值是否与prop的type类型相匹配,如果不匹配则在非生产环境下抛出警告。
initmethods
判断method有没有?method的命名符不符合命名规范?如果method既有又符合规范那就把它挂载到vm实例上。
function initMethods (vm, methods) {
const props = vm.$options.props
for (const key in methods) {
if (process.env.NODE_ENV !== 'production') {
if (methods[key] == null) {
warn(
`Method "${key}" has an undefined value in the component definition. ` +
`Did you reference the function correctly?`,
vm
)
}
//methods中某个方法名与props中某个属性名重复了,就抛出异常
if (props && hasOwn(props, key)) {
warn(
`Method "${key}" has already been defined as a prop.`,
vm
)
}
//判断如果methods中某个方法名如果在实例vm中已经存在并且方法名是以_或$开头的,就抛出异常:提示用户方法名命名不规范
if ((key in vm) && isReserved(key)) {
warn(
`Method "${key}" conflicts with an existing Vue instance method. ` +
`Avoid defining component methods that start with _ or $.`
)
}
}
vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
}
}
initData
通过一系列条件判断用户传入的data选项是否合法,最后将data转换成响应式并绑定到实例vm上
// 获取到用户传入的data选项,赋给变量data,同时将变量data作为指针指向vm._data
function initData (vm: Component) {
// 获取到用户传入的data选项,赋给变量data,同时将变量data作为指针指向vm._data
let data = vm.$options.data
// 判断data是不是一个函数,如果是就调用getData函数获取其返回值,将其保存到vm._data中
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
// 无论传入的data选项是不是一个函数,它最终的值都应该是一个对象,如果不是对象的话,就抛出警告:提示用户data应该是一个对象
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
// 遍历data对象中的每一项,在非生产环境下判断data对象中是否存在某一项的key与methods中某个属性名重复,
// 如果存在重复,就抛出警告:提示用户属性名重复。
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// 再判断是否存在某一项的key与prop中某个属性名重复,如果存在重复,就抛出警告:提示用户属性名重复
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
// 调用proxy函数将data对象中key不以_或$开头的属性代理到实例vm上,这样,我们就可以通过this.xxx来访问data选项中的xxx数据了
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
}
}
// observe data 调用observe函数将data中的数据转化成响应式
observe(data, true /* asRootData */)
}
initComputed
initWatch
以上两个参考: Vue源码系列-Vue中文社区###### 4.2.7 initProvide(vm)
// resolve provide after data/props // 初始化 provide
在调用initInjections函数对inject初始化完之后需要先调用initState函数对数据进行初始化,最后再调用initProvide函数对provide进行初始化。
callHook(vm, 'created') // 调用生命周期钩子函数
provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
4.判断是否有el属性,如果传入了则调用mount方法才进入下一个生命周期阶段。
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
// 如果传入了则调用$mount函数进入模板编译与挂载阶段, 没有传入 需要用户手动执行vm.$mount方法才进入下一个生命周期阶段。
// 创建构造器
/* var Profile = Vue.extend({
template: '<p>{{firstName}} {{lastName}} aka {{alias}}</p>',
data: function() {
return {
firstName: 'Walter',
lastName: 'White',
alias: 'Heisenberg'
}
}
})
// 创建 Profile 实例,并挂载到一个元素上。 new Profile()就会调用_init方法
new Profile().$mount('#mount-point')
new Profile({ el: '#mount-point' }) //也可以这样子
*/
剩下这几个就不细说了 和initMixin(Vue)一样的查看方法 不懂的就要谷歌。
// 注册vm的props/delete/$watch/
stateMixin(Vue)
// 初始化事件相关方法 off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update destroy
lifecycleMixin(Vue)
// 混入render $nextTick _render
renderMixin(Vue)
5.调试vue初始化
主要调试四个导出Vue的文件
1.准备文件:
在example下新建一个文件夹 example/initVue/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>initVue</title>
</head>
<body>
<div id="app">
<div><h1>Hello World</h1></div>
{{ msg }}
</div>
<script src="../../dist/vue.js"></script>
<script>
const vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue'
}
})
</script>
</body>
2.打断点
四个断点分别是:
1.src/core/index : initGlobalAPI(Vue)
- src/core/instance/index.js initMixin(Vue)
3.src/platforms/web/entry-runtime-with-compiler.js const mount = Vue.prototype.$mount
4.src/platforms/web/runtime/index.js Vue.config.mustUseProp = mustUseProp
- 监视Vue构造函数变化 在控制台如上图监视那里添加Vue对象
3.调试
断点打好之后,刷新进行调试 首先是 // 注册vm的_init方法 初始化vm initMixin(Vue)
点击F10 进行下一个函数调用后 可以看到 Vue的原型上出现了_init函数
然后继续点击 可以看到vue原型继续注册一些方法和属性**
$delete**
$destroy
$emit
$forceUpdate
$off
$on
$once
$set
$watch
_init
_update
$data 属性
$props 属性
继续点击F10 出现字母开头的函数
-
继续点击F10 到下一个断点 initGlobalAPI(Vue) 点过去 可以看到 vue实例增加了许多方法
**util**
**use**
**set**
**options**
**observable**
**nextTick**
**mixin**
**filter**
**extend**
**directive**
**delete**
**component**
2. 然后是和平台相关的代码
注册了 组件 和一些指令
以及原型中的 $mount
patch
最后一个断点 可以看到是重写了$mount方法 最后又挂载了compile方法
之后断点执行完毕
这一次的调试到这里 之后的会继续学习完成之后 更新