vue源码分析(一)
此文章参考huangyi大佬的视频,算是个人笔记,欢迎各位大佬补充
一、准备工作
1.flow
- flow是什么
FLOW IS A STATIC TYPE CHECKER FOR JAVASCRIPT.(flow在官方文档中的介绍) flow是为了js静态类型校验而生的,github start 高达20k。
vue中的类型检查用的正是flow,
- flow工作方式 类型推断,类型注释
类型推断:通过变量的使用上下文来推断出变量的类型,根据推断做校验
类型注释:实现注释好预期的类型,flow基于这些注释判断
- vue中的使用
1.代码头部加入了 /* @flow */
,那么该文件在flow校验之下
2.根目录.flowconfig配置相应的interface
[libs]
flow
3.代码中的类型注释
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
...
}
...
}
2.vue源码目录设计
──src
├── compiler 编译相关(template -> render function)
├── core 核心逻辑
| |
| ├── components 内置组件相关
| ├── global-api 全局API(mixins,extend等)
| ├── instance 初始化生命周期等
| ├── observer 响应式数据相关
| ├── util 工具方法
| └── vdom 2.0重大更新 virtual dom
|
├── platforms 平台相关(weex web mpvue等)
├── server 服务器端渲染
├── sfc .vue文件解析
└── shared 共享代码(常量,工具方法)
3.vue打包工具rollup
Rollup is a module bundler for JavaScript which compiles small pieces of code into something larger and more complex, such as a library or application.。
- rollup和webpack的区别(为什么vue打包选择rollup而不是webpack)
优势:更加轻量,尤其在处理js部分,编译后的代码也更加友好。
劣势:Rollup 不支持一些特定的高级功能,尤其是用在构建一些应用程序的时候,特别是代码拆分和运行时态的动态导入
- vue中rollup相关使用(通过pkg script分析)
// package.json
"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:test": "karma start test/unit/karma.dev.config.js",
"dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
"dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
"dev:weex": "rollup -w -c scripts/config.js --environment TARGET:weex-framework",
"dev:weex:factory": "rollup -w -c scripts/config.js --environment TARGET:weex-factory",
"dev:weex:compiler": "rollup -w -c scripts/config.js --environment TARGET:weex-compiler ",
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",
...
},
当运行npm run build 则会执行 node scripts/build.js
// scripts/build.js
// 核心部分
// 拿到相关配置
let builds = require('./config').getAllBuilds()
// 对配置进行处理
// filter builds via command line arg
if (process.argv[2]) {
const filters = process.argv[2].split(',')
builds = builds.filter(b => {
return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
})
} else {
// filter out weex builds by default
builds = builds.filter(b => {
return b.output.file.indexOf('weex') === -1
})
}
// 构建项目
build(builds)
- 第一步 拿到配置
// scripts/build.js
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
可以看到获取配置的核心在于把builds作为参数依次传入genConfig函数,通过map最终生成配置
// scripts/build.js
exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
// scripts/build.js
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
}
...
}
entry 和 dest 均调用了 resolve 方法
// scripts/build.js
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)
}
}
// scripts/alias.js
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')
}
模拟其中的一次构建过程,build的key为 web-runtime-cjs-dev
entry为
resolve('web/entry-runtime.js')
// scripts/build.js
const resolve = p => {
const base = p.split('/')[0] // base = web
if (aliases[base]) { // web 在aliases中找到了value则为 ../src/platforms/web
return path.resolve(aliases[base], p.slice(base.length + 1)) // 最终返回 ./src/platforms/webentry-runtime.js 这一完整入口路径
} else {
return path.resolve(__dirname, '../', p)
}
}
dest为
resolve('dist/vue.runtime.common.dev.js')
base = dist alias中没有key为dist,则执行else, 输入路径解析为 ../dist/vue.runtime.common.dev.js
format -f, --format 输出的文件类型 (amd, cjs, esm, iife, umd)
amd – 异步模块定义,用于像RequireJS这样的模块加载器
cjs – CommonJS,适用于 Node 和 Browserify/Webpack
esm – 将软件包保存为 ES 模块文件,在现代浏览器中可以通过 <script type=module> 标签引入
iife – 一个自动执行的功能,适合作为<script>标签。(如果要为应用程序创建一个捆绑包,您可能想要使用它,因为它会使文件大小变小。)
umd – 通用模块定义,以amd,cjs 和 iife 为一体
system - SystemJS 加载器格式
// vue.runtime.esm.js
export default Vue
// vue.runtime.common.dev.js
module.exports = Vue;
然后执行genConfig
函数,将从build拿到的配置,真正转换为rollup所接受的配置( input: opts.entry)
- 第二步 对配置进行处理
通过 process.argv[2]
拿到script传递的参数,来筛选web和weex
- 第三步 构建
将配置传递给rollup,rollup.rollup(config),生成log日志
4.RuntimeOnly 和 Runtime + compiler
compiler包含了vue的编译过程,即tempate -> render 函数,vue最终接受的是一个render函数,templete是为了开发方便,render函数对于开发并不够直观,在平时webpack + vue 的开发模式中,compiler部分作为vue-loader相关的处理,所以打包后的代码其实是render函数,如果是单独的js单独引入vue,则会在客户端进行编译,也就是会出现{{}}一闪而过的情况,把compiler放在客户端对于编译是消耗性能。
5. vue入口(entry-runtime-with-compiler)
vue是一层一层向外暴露,有一个基础的function Vue一步一步扩展成一个庞大的构造函数(类)
挂载$mount方法
// src/platforms/web/entry-runtime-with-compiler.js
import Vue from './runtime/index'
...
Vue.prototype.$mount=function( el?: string | Element,
hydrating?: boolean
): Component {
...
}
export default Vue
挂载全局配置,__patch__方法,$mount
// src/platforms/web/runtime/index.js
import Vue from 'core/index'
...
Vue.config.xxx = xxx
...
Vue.prototype.__patch__ = xxx
...
Vue.prototype.$mount = xxx
...
export default Vue
initGlobalAPI,初始化一些全局Api( set delete nextTick等)作为静态方法
// src/core/index.js
import Vue from './instance/index'
...
initGlobalAPI(Vue)
...
export default Vue
往Vue上挂载原型方法
// src/core/instance/index.js
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使用function,一级一级往出暴露,不断的挂载方法,使一个简单的构造函数一步步扩展成一个非常庞大的构造函数,其中实例方法,原型方法非常多,这里使用模块化的思想将不同的功能,分别放在了不同的文件中,分别引入,进行挂载,这对于一个项目的维护是非常有利的,在平时的开发过程中,我们也可以使用这样的思想,讲代码模块化管理
附上一个cli的简单模块化
──src
├── api 接口统一管理
├── assets 静态资源
├── components 公共组件
├── declare 全局声明文件
├── directive 全局自定义指令
├── layout 布局相关代码
├── mixins 混入相关代码
├── page 基本页面
├── plugin 插件相关
├── router 路由相关
├── store vuex相关
├── style 全局样式相关
└── util 公共方法
// main.ts
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import element from '@/plugin/element/index'
import filter from '@/filter'
import directive from '@/directive'
import components from '@/components'
Vue.config.productionTip = false
Vue.use(element)
.use(filter)
.use(directive)
.use(components)
export default new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')