准备工作
-
下载
vue
源码,可以先将vue
项目fork
到自己的github
仓库,然后在clone
自己仓库的vue
,这样在解读源码的时候可以随时添加注释,并将注释提交到自己的仓库。 -
源码代码主要结构说明:
dist
:打包生成的文件examples
:实例代码目录src
:源码文件目录compiler
:编译器相关代码,把template
模板转化成render
函数core
:核心代码components
:定义vue
自带的keep-alive
组件global-api
:定义vue
中的静态方法,包括mixin
、extend
、use
等instance
:创建vue
实例成员,包括构造函数、初始化和生命周期函数。observer
:响应式实现util
:工具方法vdom
:虚拟DOM
实现,重写了Snabbdom
,增加了组件的机制。
platforms
:平台相关处理web
:web
平台weex
:基于vue
的移动端框架
server
:服务器端渲染sfc
:单文件组件,将单文件组件转换成js
模块
-
Vue2.x
使用了Flow
来进行代码静态类型检查。 -
Vue
使用rollup
进行打包,相对于webpack
来说,rollup
打包不会生成冗余代码。rollup
命令行参数说明:-
-w
:开启监视模式 -
-c
:指定配置文件 -
--sourcemap
:开启sourceMap
功能,之后能在浏览器中的源码中看到源码对应的src
文件夹
-
--environment
:指定运行环境参数,TARGET
指定打包生成的版本。"rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
-
-
Vue
构建版本之间的差别
术语说明:
完整版(Full): 同时包含编译器和运行时的版本
编译器: 将模板字符串编译成javascript
渲染函数的代码
运行时(Runtime-only): 用来创建Vue
实例、渲染并处理虚拟DOM
等的代码。基本上就是除去编译器的其它一切。运行时版本体积小,运行效率更高。
UMD: 可以通过<script>
标签直接用在浏览器中。
CommonJS:CommonJS
版本用来配合老的打包工具比如Browserify
或webpack 1
。这些打包工具的默认文件(pkg.main)
是只包含运行时的CommonJS
版本
ES Module: 会提供两个ES Modules (ESM)
构建文件:- 为打包工具提供的
ESM
:为诸如webpack 2
或Rollup
提供的现代打包工具。ESM
格式被设计为可以被静态分析(编译时解析模块依赖),所以打包工具可以利用这一点来进行tree-shaking
并将用不到的代码排除出最终的包。为这些打包工具提供的默认文件pkg.module
是只有运行时的ES Module
构建(vue.runtime.esm.js)
。 - 为浏览器提供的
ESM (2.6+)
:用于在现代浏览器中通过<script type="module">
直接导入。
- 为打包工具提供的
-
寻找入口文件(以
dev
命令为例)-
根据
package.json
中的命令行找到配置文件
-
找到配置文件中最后的导出语句,然后向前解析找到最后导出的内容
// 判断环境变量是否有 TARGET // 如果有的话 使用 genConfig() 生成 rollup 配置文件 if (process.env.TARGET) { // 如果有 TARGET 则打包生成指定的目标版本 module.exports = genConfig(process.env.TARGET) } else { // 如果没有则打包生成所有版本 exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig) }
这里关键是是
genConfig
方法,查看方法,定位到builds
,builds
存储了不同TARGET
对应的配置。const builds = { // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify 'web-runtime-cjs-dev': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.common.dev.js'), format: 'cjs', env: 'development', banner }, 'web-runtime-cjs-prod': { entry: resolve('web/entry-runtime.js'), dest: resolve('dist/vue.runtime.common.prod.js'), format: 'cjs', env: 'production', banner }, }
resolve
是用来解析模块路径中可能存在的alias
别名,并返回一个绝对路径。const resolve = p => { // 根据路径中的前半部分去alias中找别名 const base = p.split('/')[0] if (aliases[base]) { return path.resolve(aliases[base], p.slice(base.length + 1)) } else { return path.resolve(__dirname, '../', p) } }
alias
别名配置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') }
-
根据配置找到入口文件,从入口文件开始解读。
-
-
按照上面的方法找到入口文件
entry-runtime-with-compiler.js
,通过阅读入口文件了解到该文件的主要功能是重写Vue
的$mount
方法,用来渲染DOM
。通过源码的阅读可以知道以下几处注意事项:-
Vue
实例创建时传入的el
不能是body
或html
// el 不能是 body 或者 html 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 }
-
创建
Vue
实例时如果同时传入了template
和render
,则会忽略template
-
通过调试确定
$mount
是在什么时候使用的。编译时开启sourceMap
,直接在浏览器中定位到入口文件,并打下断点,刷新浏览器,运行到断点处,可以看到右侧Call Stack
调用栈中当前正在执行Vue.$mount
,下面一行则是调用Vue.$mount
的运行环境,即在Vue.init
中,一直向下可以追溯到Vue
实例的创建的执行环境。在调用栈中可以查看函数的调用过程。
-
-
根据导入的
Vue
查看runtime/index.js
-
为
vue.config
定义一些属性。// 判断是否是关键属性(表单元素的 input/checked/selected/muted) // 如果是这些属性,设置el.props属性(属性不设置到标签上) Vue.config.mustUseProp = mustUseProp Vue.config.isReservedTag = isReservedTag Vue.config.isReservedAttr = isReservedAttr Vue.config.getTagNamespace = getTagNamespace Vue.config.isUnknownElement = isUnknownElement
-
定义平台环境上的指令
(v-show、v-modal)
和组件(Transition,TransitionGroup)
// install platform runtime directives & components extend(Vue.options.directives, platformDirectives) extend(Vue.options.components, platformComponents)
-
定义
patch
方法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) // 渲染组件 }
-
执行
devtools
的init
钩子,并对运行环境的不足做出提示。if (inBrowser) { setTimeout(() => { if (config.devtools) { if (devtools) { devtools.emit('init', Vue) } else if ( process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test' ) { console[console.info ? 'info' : 'log']( 'Download the Vue Devtools extension for a better development experience:\n' + 'https://github.com/vuejs/vue-devtools' ) } } if (process.env.NODE_ENV !== 'production' && process.env.NODE_ENV !== 'test' && config.productionTip !== false && typeof console !== 'undefined' ) { console[console.info ? 'info' : 'log']( `You are running Vue in development mode.\n` + `Make sure to turn on production mode when deploying for production.\n` + `See more tips at https://vuejs.org/guide/deployment.html` ) } }, 0) }
-
-
Vue
的初始化:通过runtime/index.js
中依赖的追溯,找到core/index.js
,这个文件中定义了Vue
的初始化的过程。-
初始化
Vue
的静态成员。initGlobalAPI(Vue)
-
初始化
Vue.config
对象// config const configDef = { } configDef.get = () => config if (process.env.NODE_ENV !== 'production') { configDef.set = () => { warn( 'Do not replace the Vue.config object, set individual fields instead.' ) } } // 初始化 Vue.config 对象 Object.defineProperty(Vue, 'config', configDef)
-
暴露
util
方法,但不将其作为Vue API
的一部分,尽量不要依赖他们// exposed util methods. // NOTE: these are not considered part of the public API - avoid relying on // them unless you are aware of the risk. // 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们 Vue.util = { warn, extend, mergeOptions, defineReactive }
-
定义静态
set
、del
和nextTick
方法// 静态方法 set/delete/nextTick Vue.set = set Vue.delete = del Vue.nextTick = nextTick
set
为对象设置为响应式的属性,实现原理解析:function set (target: Array<any> | Object, key: any, val: any): any { }
-
确保
target
是对象或数组if (process.env.NODE_ENV !== 'production' && (isUndef(target) || isPrimitive(target)) ) { warn(`Cannot set reactive property on undefined, null, or primitive value: ${ (target: any)}`) }
-
target
是数组时,确保key
是有效索引,并扩展数组长度,使数组包含key
索引,通过splice
方法设置值。这里的splice
方法是经过处理的方法,在方法内部会调用dep.notify()
发送通知。// 判断 target 是否是数组,key 是否是合法的索引 if (Array.isArray(target) && isValidArrayIndex(key)) { target.length = Math.max(target.length, key) // 扩展数组长度 // 通过 splice 对key位置的元素进行替换 // splice 在 array.js 进行了响应化的处理 target.splice(key, 1, val) return val }
-
key
是target
对象中的属性,直接赋值。// 如果 key 在对象中已经存在直接赋值 if (key in target && !(key in Object.prototype)) { target[key] = val return val }
-
如果
target
是vue
实例或$data
,直接返回// 获取 target 中的 observer 对象 const ob = (target: any).__ob__ // 如果 target 是 vue 实例或者 $data 直接返回 if (target._isVue || (ob && ob.vmCount)) { process.env.NODE_ENV !== 'production' && warn( 'Avoid adding rea
-
-
-