Vue源码学习

TIP

最后更新时间:2019年10月02日

字数:3163

Vue源码学一点,找工作吹一波,忽悠住面试官,工资你懂得😏😏😏

正版课程链接

TIP

请大家尊重正版,购买正版课程(慕课网) http://coding.imooc.com/class/228.html

基础要求

  • 熟练使用Vuejs,做过项目
  • 一定的原生JavaScript功底
  • 常用数据结构和正则表达式
  • 热爱前端,有深入探究研究源码的决心

知识点介绍

  • 核心:

    • 准备工作:
      • Flow
      • 目录结构
      • 源码构建
    • 数据驱动:
      • 数据 —> DOM创建完整流程
    • 组件化:
      • 组件创建
      • 组件相关核心概念
    • 响应式原理:
      • 完整讲述响应式实现原理
  • 编译:

    • parse
      • 模板 —> AST树
    • optimize:
      • 优化AST树
    • codegen:
      • AST树 —> 代码
  • 扩展:

    • event和v-model:
      • 事件和v-model的实现原理
    • slot和keep-alive:
      • 内置组件的实现原理
    • transition和transition-group:
      • 过度的实现原理
  • 生态:

    • Vue-Router:
      • 官方路由的实现原理
    • Vuex:
      • 官方状态管理的实现原理

认识Flow

  • Flow官网

  • Facebook出品

  • JavaScript 静态类型检查工具

为什么用Flow

  • JavaScript 是动态类型语言,灵活性虽好,但是也会造成问题
  • 类型检查是当前动态类型语言的发展趋势
    • 就是在编译期尽早发现(由类型错误引起的)bug
    • 又不影响代码运行(不需要运行时动态检查类型)
  • 项目越复杂就越需要通过工具的手段来保证项目的维护性和增强代码的可读性
  • Babel 和 ESLint 都有对应的 Flow 插件以支持语法,可以完全沿用现有的构建配置,非常小成本的改动就可以拥有静态类型检查的能力

类型推断

  • 类型推断
    • 事先注释好我们期待的类型,Flow就会基于这些注释来评估
  • 类型注释
    • 通过变量的使用上下文来推断出变量类型,然后根据这些推断来检查类型

类型推断

  • 它不需要任何代码修改即可进行类型检查,最小化开发者的工作量。它不会强制你改变开发习惯,因为它会自动推断出变量的类型。这就是所谓的类型推断,Flow 最重要的特性之一
/*@flow*/

function foo(x) {
  return x.split(' ');
}

foo(88);
1
2
3
4
5
6
7

当你在终端运行 npm run flow 命令的时候,上述代码会报错,因为函数 foo() 的期待参数是字符串,而我们输入了数字 ,报错如下

index.js:4
  4:   return x.split(' ');
                ^^^^^ property `split`. Property not found in
  4:   return x.split(' ');
              ^ Number
1
2
3
4
5

类型注释

  • 在某些特定的场景下,添加类型注释可以提供更好更明确的检查依据。
  • 要求我们需要额外编写只在开发阶段起作用的代码,最后在代码编译打包的阶段被剔除。显然,这种额外添加类型注释的方式增加了工作量
/*@flow*/

function add(x: number, y: number): number {
  return x + y
}

add('Hello', 11)
1
2
3
4
5
6
7
  • 明确指出 add() 的参数必须为数字 。
  • 类型注释是以冒号 : 开头,可以在函数参数,返回值,变量声明中使用。

JavaScript常见数据类型

  • 原始数据类型(基本数据类型):

    • 按值访问,可以操作保存在变量中实际的值。原始类型汇总中null和undefined比较特殊
    • number
    • string
    • boolean
    • nullundefined
  • 引用数据类型(Object):

    • 引用类型的值是保存在内存中的对象。
    • Function
    • Array
    • Date
    • ......等
  • 说明:

    • 与其他语言不同的是,JavaScript不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际上是在操作对象的引用而不是实际的对象。所以引用类型的值是按引用访问的。

Vuejs应用

  • 在 Vue.js 的主目录下有 .flowconfig 文件, 它是 Flow 的配置文件
  • 这其中的 [libs] 部分用来描述包含指定库定义的目录,默认是名为 flow-typed 的目录
  • Vuejs中 [libs] 配置的是 flow,表示指定的库定义都在 flow 文件夹内
  • 官方文档:https://flow.org/en/docs/config/

.flowconfig

[ignore]
.*/node_modules/.*
.*/test/.*
.*/scripts/.*
.*/examples/.*
.*/benchmarks/.*

[include]

[libs]
flow

[options]
unsafe.enable_getters_and_setters=true
module.name_mapper='^compiler/\(.*\)$' -> '<PROJECT_ROOT>/src/compiler/\1'
module.name_mapper='^core/\(.*\)$' -> '<PROJECT_ROOT>/src/core/\1'
module.name_mapper='^shared/\(.*\)$' -> '<PROJECT_ROOT>/src/shared/\1'
module.name_mapper='^web/\(.*\)$' -> '<PROJECT_ROOT>/src/platforms/web/\1'
module.name_mapper='^weex/\(.*\)$' -> '<PROJECT_ROOT>/src/platforms/weex/\1'
module.name_mapper='^server/\(.*\)$' -> '<PROJECT_ROOT>/src/server/\1'
module.name_mapper='^entries/\(.*\)$' -> '<PROJECT_ROOT>/src/entries/\1'
module.name_mapper='^sfc/\(.*\)$' -> '<PROJECT_ROOT>/src/sfc/\1'
suppress_comment= \\(.\\|\n\\)*\\$flow-disable-line

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

flow目录

这都是Vue.js 其中的自定义类型的定义

flow
├── compiler.js        # 编译相关
├── component.js       # 组件数据结构
├── global-api.js      # Global API 结构
├── modules.js         # 第三方库定义
├── options.js         # 选项相关
├── ssr.js             # 服务端渲染相关
├── vnode.js           # 虚拟 node 相关
1
2
3
4
5
6
7
8

Vuejs目录

src
├── compiler        # 编译相关 
├── core            # 核心代码 
├── platforms       # 不同平台的支持
├── server          # 服务端渲染
├── sfc             # .vue 文件解析
├── shared          # 共享代码
1
2
3
4
5
6
7

compiler

  • 模板解析成 ast 语法树
  • ast 语法树优化,代码生成

core

  • 内置组件
  • 全局 API 封装
  • Vue 实例化
  • 观察者
  • 虚拟 DOM
  • 工具函数

platform

  • 跨平台

server

  • 所有服务端渲染相关的逻辑都在这个目录下
  • 注意:这部分代码是跑在服务端的 Node.js,不要和跑在浏览器端的 Vue.js 混为一谈
  • 服务端渲染主要的工作是把组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序

sfc

  • 把 .vue 文件内容解析成一个 JavaScript 的对象

shared

  • Vue.js 会定义一些工具方法,这里定义的工具方法都是会被浏览器端的 Vue.js 和服务端的 Vue.js 所共享的

源码构建

  • 构建入口文件:scripts/build.js

Runtime Only VS Runtime + Compiler

Runtime Only

  • 我们在使用 Runtime Only 版本的 Vue.js 的时候,通常需要借助如 webpack 的 vue-loader 工具把 .vue 文件编译成 JavaScript,因为是在编译阶段做的,所以它只包含运行时的 Vue.js 代码,因此代码体积也会更轻量

Runtime + Compiler

  • 我们如果没有对代码做预编译,但又使用了 Vue 的 template 属性并传入一个字符串,则需要在客户端编译模板
// 需要编译器的版本
new Vue({
  template: '<div>{{ hi }}</div>'
})

// 这种情况不需要
new Vue({
  render (h) {
    return h('div', this.hi)
  }
})
1
2
3
4
5
6
7
8
9
10
11
  • 因为在 Vue.js 2.0 中,最终渲染都是通过 render 函数,如果写 template 属性,则需要编译成 render 函数,那么这个编译过程会发生运行时,所以需要带有编译器的版本

  • 很显然,这个编译过程对性能会有一定损耗,所以通常我们更推荐使用 Runtime-Only 的 Vue.js

TIP

尽管在实际开发过程中我们会用 Runtime Only 版本开发比较多,但为了分析 Vue 的编译过程,我们这门课重点分析的源码是 Runtime + Compiler 的 Vue.js

入口

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

  • 当我们的代码执行 import Vue from 'vue' 的时候,就是从这个入口执行代码来初始化 Vue

Vue的来源

// 1. src/platforms/web/runtime/index.js
import Vue from './runtime/index'

// 2. src/core/index.js (真正初始化 Vue 的地方,注意:初始化,不是定义)
import Vue from 'core/index'

// 3. 真正定义Vue的地方
import Vue from './instance/index'
1
2
3
4
5
6
7
8

真正的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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

TIP

Vue本质上就是一个用 Function 实现的 Class,然后它的原型 prototype 以及它本身都扩展了一系列的方法和属性

|| 和 &&

||

  • 只要“||”前面为false,不管“||”后面是true还是false,都返回“||”后面的值

  • 只要“||”前面为true,不管“||”后面是true还是false,都返回“||”前面的值

&&

  • 只要“&&”前面是false,无论“&&”后面是true还是false,结果都将返“&&”前面的值

  • 只要“&&”前面是true,无论“&&”后面是true还是false,结果都将返“&&”后面的值

initGlobalAPI

  • Vue.js 在整个初始化过程中,除了给它的原型 prototype 上扩展方法,还会给 Vue 这个对象本身扩展全局的静态方法,它的定义在 src/core/global-api/index.js