Vue源码笔记之项目架构

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_41694291/article/details/100051118

终于开启了Vue源码的阅读之旅!虽然只有三个月左右的使用经验,阅读源码时会比较吃力,但是无所谓,当我们欣赏一件“艺术品”时,重点在于是否用心去欣赏,而不在于欣赏到什么程度。

本阅读笔记基于Vue 2.6.10,主要记录了自己对Vue源码的一些理解,并参考了刘博文著的《深入浅出Vue.js》一书以及csdn博主恰恰虎的Vue源码学习系列文章。由于能力有限,笔记中对源码的认识可能不够深入,如果感兴趣,可以去官网下载Vue源码进行阅读,github地址为https://github.com/vuejs/vue

下载的源码在webstorm中打开是这样的,让我们挨个分析,找出需要研究的重点代码:

1. 项目总体结构

在这里插入图片描述
各个文件夹的说明如下:

vue-dev
  |- .circleci //配置文件,可以忽略
  |- .github   //github相关配置,可以忽略
  |- benchmarks//性能测试代码
  |- dist      //构建后的各个版本的Vue
  |- examples  //官方提供的Vue示例
  |- flow      //facebook的静态类型检查工具
  |- node_modules //项目的依赖库
  |- packages  //存放独立发布的包的目录
  |- scripts   //构建命令及相关配置
  |- src   //Vue源码,重点关注对象
  |- test      //测试文件 
  |- types     //基于typescript的类型声明
     ...       //配置文件,可以忽略 
     package.json  //项目依赖描述
     ...       //配置文件,可以忽略

对项目有一定了解的人应该都很容易看出来,src文件夹存放的就是整个项目的源码,而其他所有的文件都是它的衍生品。阅读源码只需要把目光集中在这里即可。

下面我们打开src目录,看看各个文件夹的作用:

2. src子目录概览

首先打开src下的一级子目录:
在这里插入图片描述
各个文件夹的作用如下:

src
  |- compiler //Vue编译器,用于解析Vue模板:template
  |- core     //Vue核心代码。Vue对象的构造和初始化,
              //全局API,响应式系统构建,虚拟DOM
              //的构建都在这里定义,并且是平台无关的
  |- platforms//平台相关的代码,主要指Vue支持的三种平台:
  			  //web(浏览器),weex和server(服务端渲染)
  |- server   //服务端渲染相关代码
  |- sfc      //单文件组件(.vue文件)的解析
  |- shared   //全局方法和共享的静态变量等

compiler:
上述子目录中,compiler是Vue的编译器,负责将模板template解析成渲染函数render,比如下面的模板:

<div id="app">
  <ul>
    <li v-for="item in items">
      itemid:{{item.id}}
    </li>
  </ul>
</div>

会被Vue的编译器编译为下面的渲染函数:

function render(){
  with(this){
  return 
     _c('div',{
         attrs:{"id":"app"}
     },
     [_c('ul',_l((items),function(item){
       return _c('li',
           [_v("\n itemid:"+_s(item.id)+"\n ")]
       )}
      )
     )]
   )}
}

执行完编译过程得到这个函数之后,Vue就不再需要模板了。Vue只需要调用这个函数,就可以生成该组件对应的虚拟DOM节点(VNode)。_c,_l,_v,_s等都是Vue内部定义的渲染函数会用到的函数。详细列表可以参考src/core/instance/render-helpers/index.js:
在这里插入图片描述
core:
这里是Vue最为核心的代码。Vue构造函数定义及对象初始化、响应式系统的搭建、全局API定义、平台无关的组件keep-alive的定义、虚拟DOM树的构建等都在这里完成。这里定义的属性和方法是平台无关的,即它们在Vue支持的任意一个平台都是一致的,后面会继续展开介绍。

platforms:
Vue被设计为可以在多个平台上运行,这包括web(浏览器)、weex(阿里巴巴的一款跨平台框架)和server(服务端渲染)。对于Vue而言,与平台无关的代码被放到core中,而与平台相关的则放到platforms中定义。例如,使用服务端渲染时不会用到transition组件(服务端渲染不提供类似的过渡效果),那么这个组件的定义就被放到了platforms/web下面去定义,而不会出现在Vue的核心代码core中。而keep-alive组件则是在三个平台下都可以使用的,因此keep-alive在core/components中定义。

server:
服务端渲染相关文件,定义了server平台使用的相关方法,由于我们主要探讨Vue在web中的使用,这里将不再详述。

sfc:
单文件组件编译。在使用Vue构建单页应用时,我们通常会用到单文件组件,也就是vue后缀的文件,如button.vue。这类文件的结构通常如下:

//模板定义
<template>
  ...
</template>
//脚本代码,data、methods等都在这里定义
<script>
  ...
</script>
//样式定义,scoped表示该样式只对当前组件有效,
//如果去掉会泄露为全局样式
<style scoped>
  ...
</style

sfc下的代码负责将上述单文件组件解析为对象的形式。

shared:
全局方法和共享的静态变量。如shared/constants中定义了生命周期钩子:
在这里插入图片描述
shared/utils.js则定义了大量的工具方法,如isUndef、isObject、hasOwn等等。

从源码阅读的角度来说,我们最应该关注的是compiler、core和platforms这三个目录。compiler负责模板编译,core负责Vue核心功能的搭建,platforms负责处理平台差异。

下面我们就集中来看这三个目录。

3. compiler/core/platforms结构

3.1 src/compiler

上面讲到,compiler定义了Vue编译器,下面是compiler的结构:
在这里插入图片描述
Vue编译模板分为三步:

  1. 解析HTML,生成一棵抽象语法树AST。
  2. 标记静态节点。
  3. 生成渲染函数。

对应上述目录结构来看:
compiler/parser下的文件负责第一步,如(该例子来自csdn恰恰虎的文章:VUE源码学习第七篇–编译(parse)):

<div id="app">
  <ul>
    <li v-for="item in items">
      itemid:{{item.id}}
    </li>
  </ul>
</div>

将被解析为:

{
    "type": 1,
    "tag": "div",
    "attrsList": [
    {
       "name": "id",
       "value": "app"
    }
    ],
    "attrsMap": {
      "id": "app"
    },
    "children": [
    {
      "type": 1,
      "tag": "ul",
      "attrsList": [],
      "attrsMap": {},
      "parent": {
      "$ref": "$"
    },
      "children": [
      {
        "type": 1,
        "tag": "li",
        "attrsList": [],
        "attrsMap": {
          "v-for": "item in items"
        },
        "parent": {
          "$ref": "$[\"children\"][0]"
        },
        "children": [
          {
            "type": 2,
            "expression": "\"\\n itemid:\"+_s(item.id)+\"\\n \"",
            "tokens": [
              "\n      itemid:",
            {
              "@binding": "item.id"
            },
            "\n    "
            ],
            "text": "\n      itemid:{{item.id}}\n    "
            }
        ],
        "for": "items",
        "alias": "item",
        "plain": true
        }
            ],
            "plain": true
        }
    ],
    "plain": false,
    "attrs": [
        {
            "name": "id",
            "value": "\"app\""
        }
    ]
}

该对象就被称为一棵抽象语法树。实际上就是对模板字符串的一种结构化描述。

compiler/optimizer.js负责第二步,标记静态节点。所谓标记静态节点,就是将不需要更新的节点标记出来,提高虚拟DOM的比对速度。比如:

<div>
  <div>
    <p>{{message}}</p>
  </div>
  
  <div>
    <p>Hello World!</p>
  </div>
</div>

这里的第一个段落p绑定到了变量message,因此它的内容将随着message值的变化而变化。而第二个段落p里面是静态文本,那么无论业务数据如何变化,它渲染的内容都是不会变的,所以第二个p和包裹它的div将被标记为静态节点,进行虚拟DOM比对时将跳过该节点(其中包裹这个p标签的div会被标记为静态根节点,因为它的所有子节点都是静态节点)。

compiler/codegen(code generate,代码生成)负责根据上述经过静态标记的抽象语法树生成渲染函数。生成结果即为上文compiler简介中的那个render函数:

function render(){
  with(this){
  return 
     _c('div',{
         attrs:{"id":"app"}
     },
     [_c('ul',_l((items),function(item){
       return _c('li',
           [_v("\n itemid:"+_s(item.id)+"\n ")]
       )}
      )
     )]
   )}
}

得到这个渲染函数后,模板就已经没用了,Vue会为当前对象新增一个方法:vm._render用于保存该渲染函数。构建虚拟DOM就借助于该渲染函数。

注意:
运行Vue代码并不总是需要编译器。web平台的Vue分为两个版本:完整版本和运行时版本。两者的区别就是前者包含了编译器,而后者不包含编译器。通常在两种情况下我们不需要编译器:

  1. 手写渲染函数。
  2. 使用打包工具打包项目。

我们知道,编译器的作用就是将模板编译为渲染函数。因此如果我们选择手写渲染函数,就可以使用运行时版本的Vue,它比完整版的Vue代码少了上千行,可以有效压缩框架体积。另外,如果使用打包工具如webpack进行打包,这些打包工具会在打包时提前将模板编译为渲染函数,因此打包后的文件使用的就是运行时版本。

3.2 src/core

这一部分是Vue的核心代码,我们来看目录结构(由于文件较多,这里不全部展开):
在这里插入图片描述
components,它里面定义了keep-alive组件,由于在各个平台都可以使用该组件,因此它被定义在核心代码中。

global-api,顾名思义,它定义了一些全局api,包括:Vue.component(全局组件注册)、Vue.directive(定义全局指令)、Vue.filter(全局过滤器)、Vue.extend(Vue继承接口)、Vue.mixin(Vue混入)、Vue.use(插件安装入口)。

instance,该文件夹定义了Vue实例的相关属性和方法。首先在instance/index.js中定义了Vue构造函数,然后使用mixin(混入)向Vue原型对象上注入了init(初始化)、state(状态)、events(事件)、lifecycle(生命周期)、render(渲染)相关的所有方法。使用new Vue({ … })构造Vue实例对象时,也是在这里进行初始化的,后面的文章将重点讨论。

observer(观察者),构建响应式系统的相关方法。通过Object.defineProperty这个原生api构建起来的响应式系统,非常值得学习。最重要的概念包括:

  1. Observer:观察者,用于监听数据变化,并通知发布者。
  2. Dep:发布者,用于收集依赖,数据变化时通知watcher更新视图。
  3. Watcher:订阅者,管理视图更新。
  4. queueWatcher:视图更新队列。当数据变化时,Vue默认不会立即更新视图,而是暂时放入一个微任务队列中,等数据全部更新完毕再去更新视图。

util,工具方法,如debug接口,error工具,环境参数接口等,这里不再详述。

vdom,虚拟DOM相关代码,vnode.js定义了虚拟DOM节点的结构,patch.js定义了比对和修补DOM树的相关方法,其余文件通过这两个文件来构建和更新虚拟DOM树。

3.3 src/platfroms

这里根据平台的不同,向核心版本的Vue添加了一些平台相关的方法和属性。目录结构如下:
在这里插入图片描述
这里分为两个目录:web和weex,我们这里不探讨weex平台下Vue的使用,因此该文件夹可以暂时忽略。

web根目录下有五个带entry前缀的文件,它们是使用打包工具生成Vue代码时的五个入口文件(也就是说打包工具从这里开始打包),不同的入口文件引用的模块不同,最终生成的代码版本也不一样。对应关系如下:

  1. entry-compiler,生成独立的Vue编译器。
  2. entry-runtime,生成运行时版本的Vue,不包含编译器。
  3. entry-runtime-with-compiler,生成完整版Vue。
  4. entry-server-basic-renderer,生成服务端Vue的基本版本。
  5. entry-server-renderer,生成服务端Vue的完整版本。

除了这五个入口文件,还有4个文件夹:compiler、runtime、server和util。

compiler,编译器,这里只是从src/compiler/index.js中导入核心版本的编译器,然后传入web平台相关参数,生成用于web平台下的编译器。

runtime,运行时Vue,从src/core/index.js导入核心版本的Vue,向其扩展一些只能用于web平台下的方法和组件,得到可以运行在web平台上的Vue。

server,为服务端渲染扩展的一些只能在服务端使用的方法和指令等,关于服务端渲染这里不再详述。

util,平台处理部分用到的工具方法。

Vue的整体架构和打包过程图解

Vue源码的整体结构如下(图片参照深入浅出Vue.js一书手绘):
在这里插入图片描述
Vue完整版的打包过程如下(同样参考自深入浅出Vue.js):
在这里插入图片描述

总结

本文是对Vue源码项目结构的大致分析,暂未涉及代码的实现。后面将分别介绍Vue的构造和初始化、响应式原理、编译器、虚拟DOM。由于本人水平有限,可能无法过于深入的讲解它们的实现细节(仅web平台下的完整版Vue就有一万三千多行代码,探究其中的所有细节并没有太大意义),因此将以介绍其实现原理为主。希望在本系列文章完结时,我本人能对Vue的源码有更进一步的认识。

文章链接

Vue源码笔记之项目架构
Vue源码笔记之初始化
Vue源码笔记之响应式系统
Vue源码笔记之编译器
Vue源码笔记之虚拟DOM

猜你喜欢

转载自blog.csdn.net/qq_41694291/article/details/100051118