Vue2.x 源码 - 学习准备

虽然说 vue3.x 已经出来很久了,但是大部分人还是习惯使用 vue2.x 进行开发;所以这里我还是想先理顺 vue2.x 的源码。

之前也零零散散的看过一些 vue2.x 的源码,但是比较分散,现在正好有个机会,我决定系统的看看 vue2.x 的整个源码,然后整理出来分享学习,同时也作为自己学习的一个总结。

这篇文章主要介绍一下学习 vue 源码之前需要做哪些准备!

个人能力

想学习 vue 的源码,首先你需要会使用 vue 这个框架,至少有两个或者以上的项目开发经验,其次需要具备一定的原生 javascript 基础。具备这两点之后,你最好能熟悉 vue 各种 API 的使用,这样才能更好的理解源码。最后,你还需要一个系统的计划和足够的闲余时间。

下载 vue 源码

学习源码第一步肯定是下载 ,在 vue 官网上可以直接下载对应版本的源码。
在这里插入图片描述

认识 Flow

Flow是 facebook 出品的 javascript 静态类型的检查工具;vue 源码使用了 Flow 做了静态类型检查。

为什么要使用Flow?

1、Javascript 是动态类型的语言,由于它的灵活性使得我们很容易写出一些非常隐蔽的隐患代码,这些代码在编译过程中没有问题,但是在运行是会出现各种神奇的 BUG。所以这种类型检查工具就应运而生,在编译阶段尽早发现问题,避免影响代码的运行。
2、Flow 对于 babel 和 ESlint 都有对应的插件来支持,使得 vue 可以以非常小的改动来拥有静态类型检查的能力。

参考官网列举几个具体使用:
1、类型推断

function fn(str){
	return str.split(' ');
}
fn(11)

Flow 在检查的时候会报错,因为 split 期待的是字符串,二入参是数字。

2、类型注释

//函数入参和返回值注释
function fn(a:number,b:number):number{
	return a+b;
}
fn('aa',11);

Flow 再检查的时候会报错,因为 a 期待的是数字,而入参是字符串,同时 fn 方法期待返回也是一个数字。

//对象注释
let arr:array<number> = [1,2];
arr.push('ssa');

Flow 再检查的时候会报错,因为 a 期待的是数字类型的数组,而入参是字符串。

3、常见的类型注释

class obj : {
	a: string; //字符串
	b: string | number; //字符串或者数字
	c: boolean; //布尔值
	d: array<number>; //数字类型的数组
	e: null; // 可传null
	f: void; // 可传undefined
}

4、vue 源码中的 Flow
首先有一个 .flowcxonfig 文件,这里面是 Flow 的配置信息。
在这里插入图片描述

ignore:告诉 Flow 在类型检查代码时忽略与指定正则表达式匹配的文件;
include:告诉 Flow 检查包含指定的文件或目录;
untyped:告诉 Flow 不要对匹配指定正则表达式的文件进行类型检查;
libs:告诉 Flow在类型检查代码时包含指定的库定义;
options:可以包含以下形式的几个键值对;
version:您希望使用哪个版本的 Flow;
declarations:告诉 Flow 在声明模式下解析与指定正则表达式匹配的文件。

在 vue 源码配置中,libs 部分指向的是自定义的库的目录 flow。在 flow 文件夹里面都是 vue 源码自定义的一些类型检查的自定义类型。
在这里插入图片描述

vue 源码目录

1、src 目录下:这里是 vue 源码的主要代码
在这里插入图片描述

compiler:vue 所有编译相关的代码,包括模板解析、ast 语法树优化等功能;
core:vue 核心代码,包括内置组件 keep-alive、全局 API、实例化、观察者、VDOM、公共方法等;
platforms:里面区分 web 和 weex ,是 vue 的两个主要的入口;
server:服务端渲染相关逻辑(vue2.x 以后),是跑在node上的代码;
sfc:这部分代码让我们可以使用.vue 文件编写代码,会将 .vue 文件解析成 js 的对象;
shared:共享代码,里面是一些全局共享的方法和常量。

2、其他的文件夹:

.circleci:包含 CircleCI 持续集成/持续部署工具的配置文件;
.github:项目相关的说明文档;
benchmarks:基准,性能测试文件;
dist:构建后文件的 输入目录;
examples:存放一些使用Vue 开发的应用案例;
flow:类声明,检查器;
packages:存放独立发布的包的目录;
scripts:存放 npm 脚本配置文件;
test:包含所有测试文件;
types:vue 新版本支持 TypeScript,主要是 TypeScript 类型声明文件;
node_modules:npm 包存放目录;

3、配置文件:

.babelrc:babel 配置;
.editorconfig:文本编码样式配置文件;
.eslintignore:eslint 校验忽略文件
.eslintrc:eslint 配置文件;
.flowconfig:flow 配置文件;
.gitignore:Git 提交忽略文件配置;
BACKERS.md:赞助者信息文件;
LICENSE:项目开源协议;
package.json:依赖;
README.md :说明文件;
yarn.lock :yarn 版本锁定文件;

vue 源码构建

vue 源码是基于 Rollup 构建的,相关配置在 scripts 目录下面。

webpack 和 Rollup

1、webpack 和 rollup 都是处理打包的工具;
2、webpack 的功能会更强大一些,它会把 img、js 等静态资源都编译成 js 文件,而 rollup 更倾向于 js 库的编译,只处理 js 部分,其他的不处理;
3、rollup 更轻量,编译后的代码也更友好,所以一些大型项目的源码都会选择 rollup 在作为打包工具;

vue 源码也是发布在 npm 上的 ,所有基于 npm 包管理的项目都会有一个 package.json 文件,在这里可以看到 vue 源码构建的时候运行的指令。

"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",

这三个都是构建 vue 的命令,后面两个是基于第一个基础上,添加了环境变量 ssr 和 weex,但是每次构建都会运行 scripts/build.js 这个文件。

下面就是 vue 源码构建的一个过程:
1、构建入口文件 scripts/build.js 中:

//判断打包的dist目录是否存在,不存在则同步创建一个dist目录
if (!fs.existsSync('dist')) {
  fs.mkdirSync('dist')
}
//获取配置文件里面的配置信息
let builds = require('./config').getAllBuilds()
// 有参数时,通过命令行参数过滤构建,这样可以构建出不用用途的vue.js
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 {
  //没有参数时过滤掉weex
  builds = builds.filter(b => {
    return b.output.file.indexOf('weex') === -1
  })
}
//执行构建方法
build(builds)

这里先判断 dist 文件夹是否存在,没有就创建一个;然后从配置文件里面拿配置信息,在通过命令行参数进行过滤,获取到想要的那一部分打包配置,最后将处理后的配置信息传给 build 构建方法。

2、配置文件 scripts/config.js

if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  //key数组过滤
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

这里主要暴露出去一个 getAllBuilds 方法,这个方法里面获取所有的 builds 的 key 数组,通过 map 这个数组,把数组里面每一项通过 genConfig 方法处理一遍,返回一个数组。

这里有一个 builds 对象,和 genConfig 方法。

builds 对象及相关转化:(配置很多,只看第一个)

//配置信息
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//注释,可动态配置
  }
}
//resolve方法
const resolve = p => {
	//拿到路径的第一个值作为base
  const base = p.split('/')[0]
  //aliases真实地址映射
  if (aliases[base]) {
  	//查找对应的文件
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}

配置信息里面包括入口、出口、格式、环境、注释等配置;其中对于路径做了几层映射关系处理,在 builds 里面使用 resolve 方法拿到路径的第一层作为 base ,通过 aliases 配置做一层映射,获取到项目中的真实文件路径;然后调用 path.resolve 进行拼接;最终返回的是项目中对应文件的真实路径。

aliases 配置:

//__dirname:当前文件夹,../:上一层,p:目标文件
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')
}

genConfig 方法:

//根据key获取对应的对象,构造出一个新的config对象,config是rollup打包所需要的配置对象
//只是转换的作用
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,
      banner: opts.banner,
      name: opts.moduleName || 'Vue'
    },
    onwarn: (msg, warn) => {
      if (!/Circular/.test(msg)) {
        warn(msg)
      }
    }
  }
  // built-in vars
  const vars = {
    __WEEX__: !!opts.weex,
    __WEEX_VERSION__: weexVersion,
    __VERSION__: version
  }
  // feature flags
  Object.keys(featureFlags).forEach(key => {
    vars[`process.env.${key}`] = featureFlags[key]
  })
  // build-specific env
  if (opts.env) {
    vars['process.env.NODE_ENV'] = JSON.stringify(opts.env)
  }
  config.plugins.push(replace(vars))
  if (opts.transpile !== false) {
    config.plugins.push(buble())
  }
  Object.defineProperty(config, '_name', {
    enumerable: false,
    value: name
  })
  return config
}

主要作用就是将 builds 配置信息转化成 rollup 打包所需要的数据类型。

3、再回到 scripts/build.js 的构建方法 build

//对过滤后的 builds 数组进行轮询,调用 buildEntry 方法处理每一个配置项
function build (builds) {
  let built = 0
  const total = builds.length
  const next = () => {
    buildEntry(builds[built]).then(() => {
      built++
      if (built < total) {
        next()
      }
    }).catch(logError)
  }
  next()
}
//拿到对应config进行rollup编译
function buildEntry (config) {
  const output = config.output
  const { file, banner } = output
  const isProd = /(min|prod)\.js$/.test(file)
  return rollup.rollup(config)
    .then(bundle => bundle.generate(output))
    .then(({ output: [{ code }] }) => {
    	//判断是否是生产环境或者是否需要压缩
      if (isProd) {
        const minified = (banner ? banner + '\n' : '') + terser.minify(code, {
          toplevel: true,
          output: {
            ascii_only: true
          },
          compress: {
            pure_funcs: ['makeMap']
          }
        }).code
        return write(file, minified, true)
      } else {
        return write(file, code)
      }
    })
}

build 方法会轮询过滤后的 builds 数组,并调用 buildEntry 方法对每一项配置进行处理;buildEntry 方法里面 调用 rollup.rollup()进行配置打包。还会通过 write 方法输出一些 console.log 日志信息。

到这里 vue 源码的打包就结束了。

入口文件

在使用脚手架初始化一个 vue 项目的时候,通常会有两个版本:runtimeruntime and compiler;这两个版本对应的文件在 src/platforms/web/ 下面。

这两个版本的区别是:

runtime :需要借助 webpack 的 vue-loader 工具把 .vue 文件编译成 javascript ,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因此代码体积也会更轻量。
runtime and compiler :如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则需要在客户端编译模板。

// 需要编译的版本
new VUe({
	template:"<div>{{hello}}</div>"
})
//runtime
new Vue({
	render (h){
		return h("div",this.hello)
	}
})

推荐使用 runtime 版本!

在项目里使用 import Vue from 'vue' 的时候,首先会从对应版本的入口文件开始执行:

runtime :src/platforms/web/entry-runtime.js
runtime and compiler:src/platforms/web/entry-runtime-with-compiler.js

runtime and compiler 会在 vue 原型上绑定一个 $mount 方法,用来处理编译相关的问题;然后在 vue 上绑定 compile 静态方法。而 runtime 则是直接返回引入的 Vue。

关于 Vue 的来源最终是在 src/core/instance/index.js 文件里,在这里通过 function 定义了一个 Vue 方法,然后通过各种 mixin 方法给 Vue 的原型挂载了很多原型方法,在通过 global-api 给 Vue 挂载了很多静态方法,这样我们就可以在代码中去使用这些方法了。

(这里牵扯的页面太多,这里就把大致的流程说一下,后面会详细的看看每一个页面里面的内容)

数据驱动

Vue 的核心思想是数据驱动,视图的修改不是直接操作 DOM ,而是通过修改数据。相比传统的前端开发,比如 JQuery 等前端库直接操作 DOM,减少代码量。遇到复杂交互时只关注数据的修改会让逻辑更加清晰,利于维护。

源码调试

1、下载 2.6.12 版本 vue 源码:下载地址
2、打开文件夹,然后打开控制台
在这里插入图片描述
3、安装依赖、修改配置、重新打包
控制台执行 npm install 安装依赖,然后打开 package.json 文件修改 script 标签中的 dev 命令;
在这里插入图片描述
然后控制台运行 npm run dev ,然后就会出现下面情况:
在这里插入图片描述
这个时候重新打包就已经完成了,然后可以查看 dist 文件夹下,此时已经新增了一个 vue.js.map 文件;

4、调式
examples/commits 文件夹下面新建文件 test.html
在这里插入图片描述
然后直接打开这个 html 页面,这个时候就可以在源码的任意位置打断点了。

准备工作到这里就结束了,后面会了解学习一下 Vue 的核心。

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值