3

Vue 源码解读系列 (一):如何阅读源码

 2 years ago
source link: https://www.clloz.com/programming/front-end/js/2022/03/25/vue-source-code-1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Vue 源码解读系列 (一):如何阅读源码

Clloz · 12小时前 · 8次浏览 ·

Clloz

Vue 作为在国内最热门的前端框架,网上的教程一堆,也有不少写的不错的,比如以前看的 《深入浅出 Vue.js》就讲的不错。不过和大部分上网浏览的知识一样,好讲的东西基本大家都讲到了,不好讲的或一些细节大家就都没讲到。而且大部分文章或源码解析都是将 Vue 中的几个比较重要的模块单独来讲了一下,比如数据响应式原理,虚拟 DOM,大家都能说上两句,但除了这些“通俗”的内容,其他的基本都没讲,看完这些教程你就知道了 Vue 中的一些模块,整个 Vue 是怎么工作的还是不清楚。我想写一个系列文章结合实际的例子讲清楚 Vue 的整个执行过程。

本文所使用的是 Vue2.0 的最新版本 2.6.14

目录结构分析

```
├── benchmarks 一些特殊复杂场景的跑分 demo 比如大数据量表格或者大量元素的 svg 的渲染
├── dist rollup 的输出目录,不同环境的 vue 文件
├── examples 例子
├── flow     flow 的类型声明文件
├── packages 
├── scripts 脚本,对应 package.json 的 scripts
├── src
│   ├── compiler 模版编译相关
│   │   ├── codeframe.js
│   │   ├── codegen
│   │   ├── create-compiler.js
│   │   ├── directives
│   │   ├── error-detector.js
│   │   ├── helpers.js
│   │   ├── index.js
│   │   ├── optimizer.js
│   │   ├── parser
│   │   └── to-function.js
│   ├── core
│   │   ├── components 组件相关
│   │   ├── config.js 
│   │   ├── global-api 全局 API
│   │   ├── index.js
│   │   ├── instance 实例化
│   │   ├── observer 数据响应式
│   │   ├── util 工具方法
│   │   └── vdom 虚拟 dom
│   ├── platforms
│   │   ├── web
│   │   └── weex
│   ├── server
│   │   ├── bundle-renderer
│   │   ├── create-basic-renderer.js
│   │   ├── create-renderer.js
│   │   ├── optimizing-compiler
│   │   ├── render-context.js
│   │   ├── render-stream.js
│   │   ├── render.js
│   │   ├── template-renderer
│   │   ├── util.js
│   │   ├── webpack-plugin
│   │   └── write.js
│   ├── sfc
│   │   └── parser.js
│   └── shared
│       ├── constants.js
│       └── util.js
├── test
└── types
```

如何阅读源码

源码阅读肯定不能从上到下一个一个看,我的目的是了解 Vue 是如何工作的,Vue 说到底也就是一个大函数对象,我其实就是想搞清楚这个函数上有哪些属性,我们 new Vue 的时候函数内部执行了些什么。

那么怎么找到这个函数呢?刚开始看源码的时候我们就会开始迷茫,入口在哪里?因为 Vue 的这些源码最后会用 rollup 打包成一个文件(根据不同的环境会打包出很多文件,在 dist 目录中)。我们可以通过 package.json 中的脚本来找到入口文件,比如第一个脚本是 "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev" ,虽然我们不懂 rollup,但是根据 webpack 的使用经验也能估计出来这行脚本的意思就是用 scripts 目录下的 config.js 文件作为配置文件执行 rollup。后面有个 TARGET:web-full-dev 我们到配置文件中一搜就有了下面这段:

// 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',
  alias: { he: './entity-decoder' },
  banner
},
JavaScript

我们可以看到在配置文件的 build 对象中有很多配置,对应的就是 TARGET,这里的配置就是 vue 根据不同环境编写的,由于我们是阅读源码肯定是从 dev 来看,配置文件中的 entry 字段告诉了我们入口文件在 web/entry-runtime-with-compiler.js

这里有一点需要提醒,我们在 IDE 可能会发现 Vue 源码中有些 import 的文件路径无法跳转,这是因为这些路径都是写给 rollup 的路径,在 rollup 的配置文件中有 alias 的配置

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')
}

const aliases = require('./alias')
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)
  }
}
JavaScript

目前我没找到在 webstorm 或者 VS Code 里面解析 rollup alias 的方法,webstorm 中可以写个 webpack 的配置文件来达到这个目的,不过文件的解构不复杂,这点小问题也不影响我们分析。

找到 Vue 构造函数

我们回到上面的入口文件,在入口文件中我们能够看到有一个 import Vue from './runtime/index',我们去 src/platforms/web/runtime/index.js 中发现第一行就是 import Vue from 'core/index'corecompiler 两个文件夹是源码中最核心的。在 src/core/index.js 还是没有找到 Vue 构造函数,这次又是从 src/core/instance/index.jsimport 的,不过在 src/core/instance/index.js 中我们最终找到了 function Vue 也就是 Vue 的构造函数。

我们梳理一下我们从入口文件到最终找到的 Vue 构造函数所造的文件一共有四个文件,我们来扫一扫这几个文件都做了什么,不用特别仔细。

src/platforms/web/entry-runtime-with-compiler.js 文件中主要就是编写了一个 Vue.prototype.$mount 方法,这里有个小细节我们在之后阅读源码的时候要注意的就是 const mount = Vue.prototype.$mount 这行代码。

src/platforms/web/runtime/index.js 文件中我们发现它也定义了 Vue.prototype.$mount 方法,看上去似乎是重复定义了,这里就可以看出上面说道的那行代码的作用了,由于这个文件是被 entry-runtime-with-compiler.js 引入的,所以先执行,这个文件中定义的 Vue.prototype.$mount 方法最后会被重新赋值到一个 mount 变量中。至于为什么要这么做,以及每个函数是做什么的我们后面再分析。该文件还定义了一些 Vue.config 的属性和 Vue.prototype.__patch__

src/core/index.js 中最重要的就是 initGlobalAPI 方法的调用,从这个方法的名字我们就能知道它是初始化全局 API 的。同时还定义了四个属性,Vue.prototype.$isServerVue.prototype.$ssrContextVue.functionalRenderContextVue.version

到了 src/core/instance/index.js 中就是我们实际的 Vue 构造函数了,我们看到最初的构造函数十分简单,就调用了一个 _init 方法,然后执行了一堆初始化的函数,这些函数也就是在我们的这个简单的 Vue 构造函数以及其 prototype 上增加属性和方法,来提供 Vue 的一些列功能。

本文我分析了如何阅读 Vue 源码的一些经验以及找到了 Vue 的构造函数,其实最终我们使用的那个 Vue 也就是围绕这这个简单的 Vue 函数,其实例和其 prototype 提供的一系列方法和属性,把这几个文件在做了什么搞清楚我们也就知道 Vue 是如何工作的了。

按照 import 的文件先执行,我们就从最内部的 src/core/instance/index.js 开始分析,下一篇文章见。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK