若是以为写得不错,请到GitHub
我一个Starhtml
下一篇:Vue2.0源码分析:响应式原理(上)vue
本篇Vue2.6.11
源码分析文章由观看Vue.js源码全方位深刻解析视频,阅读深刻浅出Vue.js书籍以及参考其余Vue
源码分析博客而来,阅读视频和书籍请支持正版。node
Vue.js
在Github
上第一次提交,此时名字叫作Element
,后来被更名为Seed.js
,到如今的Vue.js
。Github
发布0.6
版本,并正式改名为Vue.js
。Hacker News
网站上时候首次公开。Vue.js
发布1.0.0
版本。Vue.js
发布2.0
版本。Vue2.0
版本和Vue1.0
版本之间虽然内部变化很是大,整个渲染层都重写了,但API
层面的变化却很小,对开发者来讲很是友好,另外Vue2.0
版本还引入了不少特性:webpack
Virtual DOM
虚拟DOM。JSX
语法。TypeScript
。ssr
。weex
。正确理解虚拟DOM:Vue
中的虚拟DOM借鉴了开源库snabbdom的实现,并根据自身特点添加了许多特性。引入虚拟DOM的一个很重要的好处是:绝大部分状况下,组件渲染变得更快了,而少部分状况下反而变慢了。引入虚拟DOM这项技术一般都是在解决一些问题,然而解决一个问题的同时也可能会引入其它问题,这种状况更多的是如何作权衡、如何作取舍。所以,一味的强调虚拟DOM在任什么时候候都能提升性能这种说法须要正确对待和理解。git
核心思想:Vue
两大核心思想是数据驱动和组件化,所以咱们在介绍完源码目录设计和总体流程后,会先介绍这两方面。github
Vue.js
源码目录设计以下:web
|-- dist # 构建目录
|-- flow # flow的类型声明,相似于TypeScipt
|-- packages # 衍生的npm包,例如vue-server-renderer和vue-template-compiler
|-- scripts # 构建配置和构建脚本
|-- test # 端到端测试和单元测试用例
|-- src # 源代码
| |-- compiler # 编译相关代码
| |-- core # 核心代码
| |-- platforms # 跨平台
| |-- server # 服务端渲染
| |-- sfc # .vue文件解析逻辑
| |-- shared # 工具函数/共享代码
复制代码
对以上目录简要作以下介绍:express
dist
:rollup
构建目录,里面存放了全部Vue
构建后不一样版本的文件。npm
flow
:它是Facebook出品的JavaScript
静态类型检查工具,早期Vue.js
选择了flow
而不是如今的TypeScript
来作静态类型检查,而在最新的Vue3.0
版本则选择使用TypeScript
来重写。json
packages
:Vue.js
衍生的其它npm
包,它们在Vue
构建时自动从源码中生成而且始终和Vue.js
保持相同的版本,主要是vue-server-renderer
和vue-template-compiler
这两个包,其中最后一个包在咱们使用脚手架生成项目,也就是使用.vue
文件开发Vue
项目时会使用到这个包。
scripts
:rollup
构建配置和构建脚本,Vue.js
可以经过不一样的环境构建不一样的版本的秘密都在这个目录下。
test
:Vue.js
测试目录,自动化测试对于一个开源库来讲是相当重要的,测试覆盖率在必定程度上是衡量一个库质量的一个重要指标。测试用例不管对于开发仍是阅读源码,都是有很大益处的,其中经过测试用例去阅读Vue
源码是广泛认为可行的一种方式。
src/compiler
:此目录包含了与Vue.js
编译相关的代码,它包括:模板编译成 AST 抽象语法树、AST 抽象语法树优化和代码生成相关代码。编译的工做能够在构建时用runtime-only
版本,借助webpack
和vue-loader
等工具或插件来进行编译。也能够在运行时,使用包含构建功能的runtime + compiler
版本。显然,编译是一项比较消耗性能的工做,因此咱们平常的开发中,更推荐使用runtime-only
的版本开发(体积也更小),也就是经过.vue
文件的形式开发。
// 须要使用带编译的版本
new Vue({
data: {
msg: 'hello,world'
}
template: '<div>{{msg}}</div>'
})
// 不须要使用带编译的版本
new Vue({
data: {
msg: 'hello,world'
},
render (h) {
return h('div', this.msg)
}
})
复制代码
src/core
:此目录包含了Vue.js
的核心代码,包括:内置组件keep-alive
、全局 API(Vue.use
、Vue.mixin
和Vue.extend
等)、实例化、响应式相关、虚拟 DOM 和工具函数等。|-- core
| |-- components # 内助组件
| |-- global-api # 全局API
| |-- instance # 实例化
| |-- observer # 响应式
| |-- util # 工具函数
| |-- vdom # 虚拟DOM
复制代码
src/platform
:Vue2.0
提供了跨平台的能力,在React
中有React Native
跨平台客户端,而在Vue2.0
中其对应的跨平台就是Weex
。|-- platform
| |-- web # web浏览器端
| |-- weex # native客户端
复制代码
src/server
: Vue2.0
提供服务端渲染的能力,全部跟服务端渲染相关的代码都在server
目录下,此部分代码是运行在服务端,而非 Web 浏览器端。
src/sfc
:此目录的主要做用是如何把.vue
文件解析成一个JavaScript
对象。
src/shared
:此目录下存放了一些在 Web 浏览器端和服务端都会用到的共享代码。
咱们经过以上目录结构能够很容易的发现,Vue.js
总体分为三个部分:核心代码、跨平台相关和公共工具函数。
同时其架构是分层的,最底层是一个构造函数(普通的函数),最上层是一个入口,也就是将一个完整的构造函数导出给用户使用。在中间层,咱们须要逐渐添加一些方法和属性,主要是原型prototype
相关和全局API相关。
Vue.js
经过rollup
构建工具进行构建,它是一个相似于webpack
的打包工具,区别于webpack
它更适合一个Library
库的打包。在学习Vue.js
源码以前,咱们有必要知道Vue.js
是如何构建不一样版本的。
同webpack
同样,rollup
也有如下几大核心概念:
input
:入口文件,类比于webpack
的entry
,它指明了咱们库文件入口位置。output
:输出位置,它指明了打包后的输出信息,包括:输出目录,打包文件名等。plugins
:插件,rollup
在构建过程当中,插件可提供一些辅助功能,例如:alias
别名解析、转义ES6
等。external
:当咱们的库依赖于其它第三方库时,咱们不须要把这些第三方库一块儿打包,而是应该把依赖写在external
里面。同webpack
同样,rollup
一样适合使用配置文件的作法来配置打包的选项,例如:
// rollup.config.js
export default {
input: 'src/main.js',
output: [
{ file: 'dist/vue.js', format: 'umd', name: 'Vue' },
{ file: 'dist/vue.common.js', format: 'cjs', name: 'Vue' },
{ file: 'dist/vue.esm.js', format: 'es', name: 'Vue' }
]
}
复制代码
构建版本说明:
umd
:此选项构建出来的库文件主要适用于Web
端,能够经过不一样的方式去使用:script
标签引入,ES Module
规范引入和CommonJs
规范引入等。cjs
: 此选项构建出来的库文件主要为CommonJs
规范,可在Node
环境中使用。es
:此版本构建出来的库文件主要为ES Module
规范,可在支持ES Module
也就是import/export
的环境中使用。有了以上配置文件,咱们能够在package.json
中进行以下修改:
{
"name": "Vue",
"version": "1.0.0",
"scripts": {
"dev": "rollup -w -c scripts/rollup.config.dev.js",
"build": "rollup -c scripts/rollup.config.prod.js"
}
}
复制代码
参数说明:
-c
:为--config
的缩写,表示设置rollup
打包的配置。-w
:为--watch
的缩写,在本地开发环境添加-w
参数能够监控源文件的变化,自动从新打包。rollup
并不像webpack
那样强大,它须要和其它插件配合使用才能完成特定的功能,经常使用的插件有:
@rollup/plugin-json
: 支持从.json
读取信息,配合rollup
的Tree Shaking
可只打包.json
文件中咱们用到的部分。@rollup/plugin-commonjs
:将CommonJs
规范的模块转换为ES6
提供rollup
使用。@rollup/plugin-node-resolve
:与@rollup/plugin-commonjs
插件一块儿使用,配合之后就可使用node_modules
下的第三方模块代码了。@rollup/plugin-babel
:把ES6
代码转义成ES5
代码,须要同时安装@babel/core
和@babel/preset-env
插件。注意:若是使用了高于ES6
标准的语法,例如async/await
,则须要进行额外的配置。rollup-plugin-terser
:代码压缩插件,另一种方案是rollup-plugin-uglify
+ uglify-es
进行代码压缩,不过更推荐第一种方案。以上插件使用方式以下:
// rollup.config.js
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import resolve from '@rollup/plugin-node-resolve'
import babel from '@rollup/plugin-babel'
import { terser } from 'rollup-plugin-terser'
const config = {
input: 'src/index.js',
output: [
{ file: 'dist/vue.js', format: 'umd', name: 'Vue' },
{ file: 'dist/vue.common.js', format: 'cjs', name: 'Vue', exports: 'auto' },
{ file: 'dist/vue.esm.js', format: 'es', name: 'Vue', exports: 'auto' }
],
plugins: [
json(),
resolve(),
babel(),
commonjs(),
terser()
]
}
export default config
复制代码
正如你在上面看到的那样,咱们能够像webpack
同样进行开发环境和生产环境的配置区分,咱们把和rollup
构建相关的文件都放在scripts
目录下:
|-- scripts
| |-- rollup.config.base.js # 公共配置
| |-- rollup.config.dev.js # 开发环境配置
| |-- rollup.config.prod.js # 生产环境配置
复制代码
根据咱们的拆分逻辑,rollup.config.base.js
代码以下:
import commonjs from '@rollup/plugin-commonjs'
import json from '@rollup/plugin-json'
import resolve from '@rollup/plugin-node-resolve'
import babel from '@rollup/plugin-babel'
const config = {
input: 'src/index.js',
plugins: [
json(),
resolve(),
babel(),
commonjs()
]
}
export default config
复制代码
rollup.config.dev.js
代码以下:
import baseConfig from './rollup.config.base.js'
import serve from 'rollup-plugin-serve'
import { name } from '../package.json'
const config = {
...baseConfig,
output: [
{ file: 'dist/vue.js', format: 'umd', name },
{ file: 'dist/vue.common.js', format: 'cjs', name, exports: 'auto' },
{ file: 'dist/vue.esm.js', format: 'es', name, exports: 'default' }
],
plugins: [
...baseConfig.plugins,
serve({
open: true,
port: '4300',
openPage: '/example/index.html',
contentBase: ''
})
]
}
export default config
复制代码
配置说明:本地开发环境下,咱们能够有选择的添加rollup-plugin-serve
插件,它相似于webpack-dev-server
,能在开发环境下起一个服务方便咱们进行开发和代码调试。
rollup.config.prod.js
代码以下:
import baseConfig from './rollup.config.base.js'
import { terser } from 'rollup-plugin-terser'
import { name } from '../package.json'
const config = {
...baseConfig,
output: [
{ file: 'dist/vue.min.js', format: 'umd', name },
{ file: 'dist/vue.common.min.js', format: 'cjs', name, exports: 'auto' },
{ file: 'dist/vue.esm.min.js', format: 'es', name, exports: 'default' }
],
plugins: [
...baseConfig.plugins,
terser()
]
}
export default config
复制代码
配置说明:生产环境下,咱们须要对代码进行压缩处理,对ES Module
,CommonJs
和UMD
等规范分别生成其对应的压缩文件。
分别运行npm run dev
和npm run build
以后,咱们能够获得以下的目录:
|-- dist
| |-- vue.js # UMD未压缩版本
| |-- vue.min.js # UMD压缩版本
| |-- vue.esm.js # ES Module未压缩版本
| |-- vue.esm.min.js # ES Module压缩版本
| |-- vue.common.js # CommonJs未压缩版本
| |-- vue.common.min.js # CommonJs压缩版本
复制代码
最后,若是咱们像Vue.js
同样构建的是一个库文件,那么咱们还须要在package.json
进行以下配置:
{
"main": "dist/vue.common.js",
"module": "dist/vue.esm.js"
}
复制代码
在阅读Vue.js
源码时,咱们首先应该去看其package.json
文件内容,在Vue.js
项目中其精简掉与compiler
、weex
和ssr
相关的内容之后,以下所示:
{
"name": "vue",
"version": "2.6.11",
"main": "dist/vue.runtime.common.js",
"module": "dist/vue.runtime.esm.js",
"scripts": {
"dev": "rollup -w -c scripts/config.js --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",
"dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
"build": "node scripts/build.js"
}
}
复制代码
咱们能够从上面很容易的发现,其精简后的内容和咱们在rollup
基础知识里面的配置十分类似,其构建脚本一样放置在scripts
目录下。在scripts
目录下,咱们须要重点关注下面几个文件:
alias.js
:与rollup
构建别名相关的配置。config.js
:与rollup
构建不一样版本相关的代码。build.js
:rollup
构建不一样压缩版本Vue.js
文件相关代码。咱们在开发Vue
应用时,常常会用到@
别名,其中@
表明src
目录:
// 使用别名
import HelloWorld from '@/components/HelloWorld.vue'
// 至关于
import HelloWorld from 'src/components/HelloWorld.vue'
复制代码
在scripts/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')
}
复制代码
以core
别名为例,在Vue.js
源码中,咱们经过别名进行以下引入:
// 使用core别名
import Vue from 'core/instance/index.js'
// 至关于
import Vue from 'src/core/instance/index.js'
复制代码
其中alias.js
文件是在config.js
中引入并使用的:
// config.js文件
import alias from 'rollup-plugin-alias'
import aliases from './alias.js'
function genConfig () {
const config = {
plugins: [
alias(Object.assign({}, aliases))
])
}
return config
}
复制代码
注意:因为Vue.js
中使用rollup
主版本以及其周边插件的版本较低,若是你使用了最新的rollup
版本或者其周边的插件,须要按照最新插件的配置要求来,这里以最新的@rollup/plugin-alias
插件为例:
const path = require('path')
const resolve = p => path.resolve(__dirname, '../', p)
module.exports = [
{ file: 'vue', replacement: resolve('src/platforms/web/entry-runtime-with-compiler') },
{ file: 'compiler', replacement: resolve('src/compiler') },
{ file: 'core', replacement: resolve('src/core') },
{ file: 'shared', replacement: resolve('src/shared') },
{ file: 'web', replacement: resolve('src/platforms/web' },
{ file: 'weex', replacement: resolve('src/platforms/weex') },
{ file: 'server', replacement: resolve('src/server') },
{ file: 'sfc', replacement: resolve('src/sfc') }
]
复制代码
其在config.js
新的使用方式一样须要作调整,以下:
// config.js文件
import alias from '@rollup/plugin-alias'
import aliases from './alias.js'
function genConfig () {
const config = {
plugins: [
alias({ entries: aliases })
])
}
return config
}
复制代码
首先咱们从package.json
打包命令中能够看到,在development
环境下它经过-c
指定了rollup
的配置文件,因此会使用到scripts/config.js
文件,而且打包命令还提供了一个叫作TARGET
的环境变量:
{
"scripts": {
"dev": "rollup -w -c scripts/config.js --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",
}
}
复制代码
那么在scripts/config.js
文件下,咱们能够看到它是经过module.exports
导出的一个对象:
function genConfig (name) {
const opts = builds[name]
const config = {
input: opts.entry,
external: opts.external,
plugins: [
flow(),
alias(Object.assign({}, aliases, opts.alias))
].concat(opts.plugins || []),
output: {
file: opts.dest,
format: opts.format,
name: opts.moduleName || 'Vue'
},
onwarn: (msg, warn) => {
if (!/Circular/.test(msg)) {
warn(msg)
}
}
}
return config
}
if (process.env.TARGET) {
module.exports = genConfig(process.env.TARGET)
} else {
exports.getBuild = genConfig
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}
复制代码
在以上代码中,咱们能够看到module.exports
导出的对象,主要是经过genConfig()
函数返回的,其中这个函数接受的参数正是咱们在打包命令中提供的环境变量TARGET
。咱们再来粗略的看一下genConfig()
函数,它的主要做用依然是生成rollup
几大核心配置,而后返回配置完毕后的对象。
咱们再来看一个叫作builds
的对象,因为在源码中它的内容很是多,为了节省篇幅咱们精简后其代码以下:
const builds = {
// Runtime+compiler CommonJS build (CommonJS)
'web-full-cjs-dev': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.dev.js'),
format: 'cjs',
env: 'development',
},
'web-full-cjs-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.common.prod.js'),
format: 'cjs',
env: 'production'
},
// Runtime+compiler ES modules build (for bundlers)
'web-full-esm': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.esm.js'),
format: 'es'
},
// 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'
},
// Runtime+compiler production build (Browser)
'web-full-prod': {
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.min.js'),
format: 'umd',
env: 'production'
}
}
复制代码
咱们能够发现它的键名正好是咱们打包命令中提供的环境变量TARGET
的值,这里以web-full-dev
为例,它经过web-full-dev
这个键能够获得一个对象:
{
entry: resolve('web/entry-runtime-with-compiler.js'),
dest: resolve('dist/vue.js'),
format: 'umd',
env: 'development'
}
复制代码
而后配合resolve
函数和上面咱们已经提到过的别名配置,就能够构造下面这样的rollup
配置对象:
{
// 省略其它
input: 'src/platforms/web/entry-runtime-with-compiler.js',
output: {
dest: 'dist/vue.js',
format: 'umd',
name: 'Vue'
}
}
复制代码
srcipts/build.js
文件的做用就是经过配置而后生成不一样版本的压缩文件,其中它获取配置的方式一样是在scripts/config.js
文件中,其中关键代码为:
// config.js中导出
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
// build.js中引入
let builds = require('./config').getAllBuilds()
复制代码
在以前的介绍中,咱们知道Vue.js
内部会根据Web浏览器
、Weex
跨平台和SSR服务端渲染
不一样的环境寻找不一样的入口文件,但其核心代码是在src/core
目录下,咱们这一节的主要目的是为了搞清楚从入口文件到Vue
构造函数执行,这期间的总体流程。
在分析完从入口到构造函数的各个部分的流程后,咱们能够获得一份大的流程图:
咱们会在src/core/index.js
文件中看到以下精简代码:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
initGlobalAPI(Vue)
export default Vue
复制代码
在以上代码中,咱们发现它引入了Vue
随后调用了initGlobalAPI()
函数,此函数的做用是挂载一些全局API
方法。
咱们首先能在src/core/global-api
文件夹下看到以下目录结构:
|-- global-api
| |-- index.js # 入口文件
| |-- assets.js # 挂载filter、component和directive
| |-- extend.js # 挂载extend方法
| |-- mixin.js # 挂载mixin方法
| |-- use.js # 挂载use方法
复制代码
随后在index.js
入口文件中,咱们能看到以下精简代码:
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { observe } from 'core/observer/index'
import { extend, nextTick } from '../util/index'
export function initGlobalAPI (Vue: GlobalAPI) {
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
Vue.observable = (obj) => {
observe(obj)
return obj
}
initUse(Vue)
initMixin(Vue)
initExtend(Vue)
initAssetRegisters(Vue)
}
复制代码
咱们能从以上代码很清晰的看到在index.js
入口文件中,会在Vue
构造函数上挂载各类全局API
函数,其中set
、delete
、nextTick
和observable
直接赋值为一个函数,而其余几种API
则是调用了一个以init
开头的方法,咱们以initAssetRegisters()
方法为例,它的精简代码以下:
// ['component','directive', 'filter']
import { ASSET_TYPES } from 'shared/constants'
export function initAssetRegisters (Vue: GlobalAPI) {
ASSET_TYPES.forEach(type => {
Vue[type] = function () {
// 省略了函数的参数和函数实现代码
}
})
}
复制代码
其中ASSET_TYPES
是一个定义在src/shared/constants.js
中的一个数组,而后在initAssetRegisters()
方法中遍历这个数组,依次在Vue
构造函数上挂载Vue.component()
、Vue.directive()
和Vue.filter()
方法,另外三种init
开头的方法调用挂载对应的全局API
是同样的道理:
// initUse
export function initUse(Vue) {
Vue.use = function () {}
}
// initMixin
export function initMixin(Vue) {
Vue.mixin = function () {}
}
// initExtend
export function initExtend(Vue) {
Vue.extend = function () {}
}
复制代码
最后,咱们发现还差一个Vue.compile()
方法,它实际上是在runtime+compile
版本才会有的一个全局方法,所以它在src/platforms/web/entry-runtime-with-compile.js
中被定义:
import Vue from './runtime/index'
import { compileToFunctions } from './compiler/index'
Vue.compile = compileToFunctions
export default Vue
复制代码
所以咱们根据initGlobalAPI()
方法的逻辑,能够获得以下流程图:
在上一节咱们讲到了initGlobalAPI
的总体流程,这一节,咱们来介绍initMixin
的总体流程。首选,咱们把目光回到src/core/index.js
文件中:
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
initGlobalAPI(Vue)
export default Vue
复制代码
咱们发现,它从别的模块中引入了大Vue
,那么接下来咱们的首要任务就是揭开Vue
构造函数的神秘面纱。
在看src/core/instance/index.js
代码以前,咱们发现instance
目录结构以下:
|-- instance
| |-- render-helpers # render渲染相关的工具函数目录
| |-- events.js # 事件处理相关
| |-- init.js # _init等方法相关
| |-- inject.js # inject和provide相关
| |-- lifecycle.js # 生命周期相关
| |-- proxy.js # 代理相关
| |-- render.js # 渲染相关
| |-- state.js # 数据状态相关
| |-- index.js # 入口文件
复制代码
能够看到,目录结构文件有不少,并且包含的面也很是杂,但咱们如今只须要对咱们最关心的几个部分作介绍:
events.js
:处理事件相关,例如:$on
,$off
,$emit
以及$once
等方法的实现。init.js
:此部分代码逻辑包含了Vue
从建立实例到实例挂载阶段的全部主要逻辑。lifecycle.js
:生命周期相关,例如:$destroy
、$activated
和$deactivated
。state.js
:数据状态相关,例如:data
、props
以及computed
等。render.js
:渲染相关,其中最值得关注的是Vue.prototype._render
渲染函数的定义。在介绍了instance
目录结构的及其各自的做用之后,咱们再来看入口文件,其实入口文件这里才是Vue
构造函数庐山真面目:
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')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
复制代码
代码分析:
Vue
构造函数其实就是一个普通的函数,咱们只能经过new
操做符进行访问,既new Vue()
的形式,Vue
函数内部也使用了instanceof
操做符来判断实例的父类是否为Vue
构造函数,不是的话则在开发环境下输出一个警告信息。Vue
构造函数,这部分的代码也调用了几种mixin
方法,其中每种mixin
方法各司其职,处理不一样的内容。从以上代码中,咱们能获得src/core/instance/index.js
文件很是直观的代码逻辑流程图:
接下来咱们的首要任务是弄清楚_init()
函数的代码逻辑以及initMixin
的总体流程。咱们从上面的代码发现,在构造函数内部会调用this._init()
方法,也就是说:
// 实例化时,会调用this._init()方法。
new Vue({
data: {
msg: 'Hello, Vue.js'
}
})
复制代码
而后,咱们在init.js
中来看initMixin()
方法是如何被定义的:
export function initMixin (Vue) {
Vue.prototype._init = function (options) {
// 省略代码
}
}
复制代码
咱们能够发现,initMixin()
方法的主要做用就是在Vue.prototype
上定义一个_init()
实例方法,接下来咱们来看一下_init()
函数的具体实现逻辑:
Vue.prototype._init = function (options) {
const vm = this
// 1. 合并配置
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// 2.render代理
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// 3.初始化生命周期、初始化事件中心、初始化inject,
// 初始化state、初始化provide、调用生命周期
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
// 4.挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
复制代码
由于咱们是要分析initMixin
总体流程,对于其中某些方法的具体实现逻辑会在后续进行详细的说明,所以咱们能够从以上代码获得initMixin
的总体流程图。
stateMixin
主要是处理跟实例相关的属性和方法,它会在Vue.prototype
上定义实例会使用到的属性或者方法,这一节咱们主要任务是弄清楚stateMixin
的主要流程。在src/core/instance/state.js
代码中,它精简后以下所示:
import { set, del } from '../observer/index'
export function stateMixin (Vue) {
// 定义$data, $props
const dataDef = {}
dataDef.get = function () { return this._data }
const propsDef = {}
propsDef.get = function () { return this._props }
Object.defineProperty(Vue.prototype, '$data', dataDef)
Object.defineProperty(Vue.prototype, '$props', propsDef)
// 定义$set, $delete, $watch
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function() {}
}
复制代码
咱们能够从上面代码中发现,stateMixin()
方法中在Vue.prototype
上定义的几个属性或者方法,所有都是和响应式相关的,咱们来简要分析一下以上代码:
$data和$props
:根据以上代码,咱们发现$data
和$props
分别是_data
和_props
的访问代理,从命名中咱们能够推测,如下划线开头的变量,咱们通常认为是私有变量,而后经过$data
和$props
来提供一个对外的访问接口,虽然能够经过属性的get()
方法去取,但对于这两个私有变量来讲是并不能随意set
,对于data
来讲不能替换根实例,而对于props
来讲它是只读的。所以在原版源码中,还劫持了set()
方法,当设置$data
或者$props
时会报错:if (process.env.NODE_ENV !== 'production') {
dataDef.set = function () {
warn(
'Avoid replacing instance root $data. ' +
'Use nested data properties instead.',
this
)
}
propsDef.set = function () {
warn(`$props is readonly.`, this)
}
}
复制代码
$set
和$delete
:set
和delete
这两个方法被定义在跟instance
目录平级的observer
目录下,在stateMixin()
中,它们分别赋值给了$set
和$delete
方法,而在initGlobalAPI
中,也一样使用到了这两个方法,只不过一个是全局方法,一个是实例方法。
$watch
:在stateMixin()
方法中,详细实现了$watch()
方法,此方法实现的核心是经过一个watcher
实例来监听。当取消监听时,一样是使用watcher
实例相关的方法,关于watcher
咱们会在后续响应式章节详细介绍。
Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function () {
watcher.teardownunwatchFn()
}
}
复制代码
在以上代码分析完毕后,咱们能够获得stateMixin
以下流程图:
在使用Vue
作开发的时候,咱们必定常用到$emit
、$on
、$off
和$once
等几个实例方法,eventsMixin
主要作的就是在Vue.prototype
上定义这四个实例方法:
export function eventsMixin (Vue) {
// 定义$on
Vue.prototype.$on = function (event, fn) {}
// 定义$once
Vue.prototype.$once = function (event, fn) {}
// 定义$off
Vue.prototype.$off = function (event, fn) {}
// 定义$emit
Vue.prototype.$emit = function (event) {}
}
复制代码
经过以上代码,咱们发现eventsMixin()
所作的事情就是使用发布-订阅模式来处理事件,接下来让咱们先使用发布-订阅实现本身的事件中心,随后再来回顾源码。
$on
方法的实现比较简单,咱们先来实现一个基础版本的:
function Vue () {
this._events = Object.create(null)
}
Vue.prototype.$on = function (event, fn) {
if (!this._events[event]) {
this._events[event] = []
}
this._events[event].push(fn)
return this
}
复制代码
接下来对比一下Vue
源码中,关于$on
的实现:
Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
const vm: Component = this
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$on(event[i], fn)
}
} else {
(vm._events[event] || (vm._events[event] = [])).push(fn)
// optimize hook:event cost by using a boolean flag marked at registration
// instead of a hash lookup
if (hookRE.test(event)) {
vm._hasHookEvent = true
}
}
return vm
}
复制代码
代码分析:
Vue
源码中,$on
方法还接受一个数组event
,这实际上是在Vue2.2.0
版本之后才有的,当传递一个event
数组时,会经过遍历数组的形式递归调用$on
方法。$on
的事件所有绑定在_events
私有属性上,这个属性实际上是在咱们上面已经提到过的initEvents()
方法中被定义的。export function initEvents (vm) {
vm._events = Object.create(null)
}
复制代码
咱们先来实现一个简单的$emit
方法:
Vue.prototype.$emit = function (event) {
const cbs = this._events[event]
if (cbs) {
const args = Array.prototype.slice.call(arguments, 1)
for (let i = 0; i < cbs.length; i++) {
const cb = cbs[i]
cb && cb.apply(this, args)
}
}
return this
}
复制代码
接下来,咱们使用$emit
和$on
来配合测试事件的监听和触发:
const app = new Vue()
app.$on('eat', (food) => {
console.log(`eating ${food}!`)
})
app.$emit('eat', 'orange')
// eating orange!
复制代码
最后咱们来看Vue
源码中关于$emit
的实现:
Vue.prototype.$emit = function (event: string): Component {
const vm: Component = this
// ...省略处理边界代码
let cbs = vm._events[event]
if (cbs) {
cbs = cbs.length > 1 ? toArray(cbs) : cbs
const args = toArray(arguments, 1)
const info = `event handler for "${event}"`
for (let i = 0, l = cbs.length; i < l; i++) {
invokeWithErrorHandling(cbs[i], vm, args, vm, info)
}
}
return vm
}
复制代码
代码分析:
$emit
实现方法很是简单,第一步从_events
对象中取出对应的cbs
,接着一个个遍历cbs
数组、调用并传参。invokeWithErrorHandling
代码中会使用try/catch
把咱们函数调用并执行的地方包裹起来,当函数调用出错时,会执行Vue
的handleError()
方法,这种作法不只更加友好,并且对错误处理也很是有用。$off
方法的实现,相对来讲比较复杂一点,由于它须要根据不一样的传参作不一样的事情:
event
参数时,只移除此event
对应的监听器。event
参数和fn
回调,则只移除此event
对应的fn
这个监听器。在了解了以上功能点后,咱们来实现一个简单的$off
方法:
Vue.prototype.$off = function (event, fn) {
// 没有传递任何参数
if (!arguments.length) {
this._events = Object.create(null)
return this
}
// 传递了未监听的event
const cbs = this._events[event]
if (!cbs) {
return this
}
// 没有传递fn
if (!fn) {
this._events[event] = null
return this
}
// event和fn都传递了
let i = cbs.length
let cb
while (i--) {
cb = cbs[i]
if (cb === fn) {
cbs.splice(i, 1)
break
}
}
return this
}
复制代码
接下来,咱们撰写测试代码:
const app = new Vue()
function eatFood (food) {
console.log(`eating ${food}!`)
}
app.$on('eat', eatFood)
app.$emit('eat', 'orange')
app.$off('eat', eatFood)
// 不执行回调
app.$emit('eat', 'orange')
复制代码
最后咱们来看Vue
源码中关于$off
的实现:
Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
const vm: Component = this
// all
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i < l; i++) {
vm.$off(event[i], fn)
}
return vm
}
// specific event
const cbs = vm._events[event]
if (!cbs) {
return vm
}
if (!fn) {
vm._events[event] = null
return vm
}
// specific handler
let cb
let i = cbs.length
while (i--) {
cb = cbs[i]
if (cb === fn || cb.fn === fn) {
cbs.splice(i, 1)
break
}
}
return vm
}
复制代码
关于$once
方法的实现比较简单,能够简单的理解为在回调以后立马调用$off
,所以咱们来实现一个简单的$once
方法:
Vue.prototype.$once = function (event, fn) {
function onFn () {
this.$off(event, onFn)
fn.apply(this, arguments)
}
this.$on(event, onFn)
return this
}
复制代码
接着咱们对比一下Vue
源码中的$once
方法:
Vue.prototype.$once = function (event: string, fn: Function): Component {
const vm: Component = this
function on () {
vm.$off(event, on)
fn.apply(vm, arguments)
}
on.fn = fn
vm.$on(event, on)
return vm
}
复制代码
注意:在源码中$once
的实现是在回调函数中使用fn
绑定了原回调函数的引用,在上面已经提到过的$off
方法中也一样进行了cb.fn === fn
的判断。
在实现完以上几种方法后,咱们能够获得eventsMixin
以下流程图:
和以上其它几种方法同样,lifecycleMixin
主要是定义实例方法和生命周期,例如:$forceUpdate()
、$destroy
,另外它还定义一个_update
的私有方法,其中$forceUpdate()
方法会调用它,所以lifecycleMixin
精简代码以下:
export function lifecycleMixin (Vue) {
// 私有方法
Vue.prototype._update = function () {}
// 实例方法
Vue.prototype.$forceUpdate = function () {
if (this._watcher) {
this._watcher.update()
}
}
Vue.prototype.$destroy = function () {}
}
复制代码
代码分析:
_update()
会在组件渲染的时候调用,其具体的实现咱们会在组件章节详细介绍$forceUpdate()
为一个强制Vue
实例从新渲染的方法,它的内部调用了_update
,也就是强制组件重选编译挂载。$destroy()
为组件销毁方法,在其具体的实现中,会处理父子组件的关系,事件监听,触发生命周期等操做。lifecycleMixin()
方法的代码不是不少,咱们也能很容易的获得以下流程图:
相比于以上几种方法,renderMixin
是最简单的,它主要在Vue.prototype
上定义各类私有方法和一个很是重要的实例方法:$nextTick
,其精简代码以下:
export function renderMixin (Vue) {
// 挂载各类私有方法,例如this._c,this._v等
installRenderHelpers(Vue.prototype)
Vue.prototype._render = function () {}
// 实例方法
Vue.prototype.$nextTick = function (fn) {
return nextTick(fn, this)
}
}
复制代码
代码分析:
installRenderHelpers
:它会在Vue.prototype
上挂载各类私有方法,例如this._n = toNumber
、this._s = toString
、this._v = createTextVNode
和this._e = createEmptyVNode
。_render()
:_render()
方法会把模板编译成VNode
,咱们会在其后的编译章节详细介绍。nextTick
:就像咱们以前介绍过的,nextTick
会在Vue
构造函数上挂载一个全局的nextTick()
方法,而此处为实例方法,本质上引用的是同一个nextTick
。在以上代码分析完毕后,咱们能够获得renderMixin
以下流程图: