vue源码分析(一)

942 阅读6分钟

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.。

  1. rollup和webpack的区别(为什么vue打包选择rollup而不是webpack)

优势:更加轻量,尤其在处理js部分,编译后的代码也更加友好。

劣势:Rollup 不支持一些特定的高级功能,尤其是用在构建一些应用程序的时候,特别是代码拆分和运行时态的动态导入

  1. 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,适用于 NodeBrowserify/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

process.argv

  • 第三步 构建

将配置传递给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')