资讯专栏INFORMATION COLUMN

VueJS源码学习——项目结构&目录

ad6623 / 468人阅读

摘要:所以整个的核心,就是如何实现这三样东西以上摘自囧克斯博客的一篇文章从版本开始这个时候的项目结构如下源码在里面,为打包编译的代码,为打包后代码放置的位置,为测试代码目录。节点类型摘自资源另一位作者关于源码解析

本项目的源码学习笔记是基于 Vue 1.0.9 版本的也就是最早的 tag 版本,之所以选择这个版本,是因为这个是最原始没有太多功能拓展的版本,有利于更好的看到 Vue 最开始的骨架和脉络以及作者的最初思路。而且能和后续的 1.x.x 版本做对比,发现了作者为了修复 bug 而做出的很多有趣的改进甚至回退,如 vue nextTick 的版本迭代经历了更新、回退和再次更新

原文地址
项目地址

囧克斯的一篇源码解析文章

Vue.js 是一个典型的 MVVM 框架,整个程序从最上层分为

1 全局设计:包括全局接口、默认选项

2 vm 实例设计: 包括接口设计(vm 原型)、实例初始化过程设计(vm构造函数)

构造函数核心的工作内容:

整个实例初始化过程,关键在于将 数据(Model) 和 视图(view)建立起关联关系:

通过 observer 对 data 进行了监听,并且提供订阅某个数据项的变化的能力

把 template 解析成一段 document fragment,然后解析其中的 directive,得到每一个 directive 所依赖的数据项及其更新方法。比如 v-text="message" 被解析之后 (这里仅作示意,实际程序逻辑会更严谨而复杂):

所依赖的数据项 this.$data.message,以及

相应的视图更新方法 node.textContent = this.$data.message

通过 watcher 把上述两部分结合起来,即把 directive 中的数据依赖订阅在对应数据的 observer 上,这样当数据变化的时候,就会触发 observer,进而触发相关依赖对应的视图更新方法,最后达到模板原本的关联效果。

所以整个 vm 的核心,就是如何实现 observer, directive (parser), watcher 这三样东西

以上摘自囧克斯博客的一篇文章

从 v1.0.9 版本开始

这个时候的项目结构如下:

源码在 src 里面,build 为打包编译的代码,dist 为打包后代码放置的位置, test 为测试代码目录。

从 package.json 里可以了解到项目用到的依赖包以及项目的开发和运行方式,其中编译代码是:

    "build": "node build/build.js",

于是我们到对应的这个文件里:

var fs = require("fs")
var zlib = require("zlib")
var rollup = require("rollup")
var uglify = require("uglify-js")
var babel = require("rollup-plugin-babel")
var replace = require("rollup-plugin-replace")
var version = process.env.VERSION || require("../package.json").version

var banner =
  "/*!
" +
  " * Vue.js v" + version + "
" +
  " * (c) " + new Date().getFullYear() + " Evan You
" +
  " * Released under the MIT License.
" +
  " */"

// CommonJS build.
// this is used as the "main" field in package.json
// and used by bundlers like Webpack and Browserify.
rollup.rollup({
  entry: "src/index.js",
  plugins: [
    babel({
      loose: "all"
    })
  ]
})
...

可以知道这个时候用的是 rollup 来进行打包编译的, 入口文件是 __src/index.js__,index.js 的代码很简洁:

import Vue from "./instance/vue"
import directives from "./directives/public/index"
import elementDirectives from "./directives/element/index"
import filters from "./filters/index"
import { inBrowser } from "./util/index"

Vue.version = "1.0.8"

/**
 * Vue and every constructor that extends Vue has an
 * associated options object, which can be accessed during
 * compilation steps as `this.constructor.options`.
 *
 * These can be seen as the default options of every
 * Vue instance.
 */

Vue.options = {
  directives,
  elementDirectives,
  filters,
  transitions: {},
  components: {},
  partials: {},
  replace: true
}

export default Vue

// devtools global hook
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production") {
  if (inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
    window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit("init", Vue)
  }
}

从这里可以知道实例 vue 的实现在 src/instance/vue 中, 还涉及了 directives 应该是用于指令解析的方法和 filter 过滤器,这个在 2.0 已经不存在但在 1.0 使用比较频繁的功能, 同时 inBrowser 应该是用来判断是否是浏览器环境,说明 src/util 是一个工具类的目录,这里一个个验证

工具类方法 inBrowser

首先看 inBrowser__, 发现 __util/index.js 也只是一个工具函数入口文件:

export * from "./lang"
export * from "./env"
export * from "./dom"
export * from "./options"
export * from "./component"
export * from "./debug"
export { defineReactive } from "../observer/index"

从字面可以知道涉及到的工具类有 语言、环境?、dom操作、options?、组件化、开发类、实时定义? 这些类型的工具, 而 inBrowser 应该属于 env 或者 dom,在 util/env 中找到了其实现:

...
// Browser environment sniffing
export const inBrowser =
  typeof window !== "undefined" &&
  Object.prototype.toString.call(window) !== "[object Object]"
...

这里利用浏览器的全局对象 window 做区分,因为在 nodejs 环境下是没有 window 这个全局对象的,所以判断 typeof window 是否不为 "undefined" 且不是由用户自己创建的一个普通对象,如果是的话, Object.prototype.toString.call(window) // === [object Object]

而在浏览器环境下,则是这样的情况:

typeof window
// "object"
Object.prototype.toString.call(window)
// "[object Window]"
Vue 实例构造函数实现

再来看 __src/instance/vue__, 应该是实现了vue的实例初始化函数,从代码可以知道是一个实例的构造函数,也是顶层实现,底层代码位于子目录的 api 和 internal,分别实现了公用的方法和私有的方法变量等

import initMixin from "./internal/init"
import stateMixin from "./internal/state"
import eventsMixin from "./internal/events"
import lifecycleMixin from "./internal/lifecycle"
import miscMixin from "./internal/misc"

import globalAPI from "./api/global"
import dataAPI from "./api/data"
import domAPI from "./api/dom"
import eventsAPI from "./api/events"
import lifecycleAPI from "./api/lifecycle"

/**
 * The exposed Vue constructor.
 *
 * API conventions:
 * - public API methods/properties are prefixed with `$`
 * - internal methods/properties are prefixed with `_`
 * - non-prefixed properties are assumed to be proxied user
 *   data.
 *
 * @constructor
 * @param {Object} [options]
 * @public
 */

function Vue (options) {
  this._init(options)
}

// install internals
initMixin(Vue)
...

// install APIs
globalAPI(Vue)
...

export default Vue

目录如下:

从注释可以知道,尤大用前缀 $ 标记公用方法和变量,用 _标记私有的方法和变量,没有前缀的变量可能用来代理用户数据

从引入的文件可以知道私有方法和变量分别有 lifecycleMixin 生命周期、eventsMixin 事件机制、stateMixin 状态、miscMixin 过滤器, 以及实例的共有方法API: 全局 globalAPI 、数据绑定 dataAPI、DOM操作 domAPI、事件操作 eventsAPI、生命周期 lifecycleAPI

通过 initMixin(Vue) 向 Vue 的 prototype 添加原型方法:

export default function (Vue) {
    Vue.prototype.方法 = function(options) {
        ...
    }
}

具体如何实现都在 apiinternal 这两个文件夹里面,所以 src/instance 是 vue 实例构造函数的实现

directives、 filter 和 elementDirectives
// src/index.js
import Vue from "./instance/vue"
import directives from "./directives/public/index"
import elementDirectives from "./directives/element/index"
import filters from "./filters/index"
import { inBrowser } from "./util/index"

Vue.options = {
  directives,
  elementDirectives,
  filters,
  transitions: {},
  components: {},
  partials: {},
  replace: true
}

export default Vue

// devtools global hook
/* istanbul ignore if */
if (process.env.NODE_ENV !== "production") {
  if (inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__) {
    window.__VUE_DEVTOOLS_GLOBAL_HOOK__.emit("init", Vue)
  }
}

index.js 里剩下这三个都是作为 Vue.options 里的变量存在的,前面知道了 Vue 的构造函数实现,知道了利用 工具类 inBrowser 来判断是否处于浏览器,在判断window是否存在 __VUE_DEVTOOLS_GLOBAL_HOOK__ 这个变量, 如果存在,那么代表浏览器安装了 vue 的调试插件,那么还会调用这个变量的方法 init 告诉插件已经初始化好了 vue 对象。

从1.0 官网文档 custom-directive 中可以知道 directive 是让开发者开发自己的指令,具体例子如下

而 element-directive 和 directive 类似,只是形式上是作为一个元素存在,无法传输给元素数据,但是可以操作元素的属性

这是一个强大的功能,让开发者决定数据改变时以怎样的形式渲染到视图里,强大的功能代码量也不少,光 directives 里就20几个文件

src/directives/public/index 这个入口文件可以知道 custom directive 含有的方法和属性:

// text & html
import text from "./text"
import html from "./html"
// logic control
import vFor from "./for"
import vIf from "./if"
import show from "./show"
// two-way binding
import model from "./model/index"
// event handling
import on from "./on"
// attributes
import bind from "./bind"
// ref & el
import el from "./el"
import ref from "./ref"
// cloak
import cloak from "./cloak"

// must export plain object
export default {
  text,
  html,
  "for": vFor,
  "if": vIf,
  show,
  model,
  on,
  bind,
  el,
  ref,
  cloak
}

可以看到 directive 包含了 文本操作、逻辑操作(循环、条件)、双向绑定(这个是比较有趣且重要的额部分)、事件绑定、数据绑定、dom绑定还有一个cloak用于未渲染完成的样式情况

two-way binding 即 vue 中的 v-model 属性,是对表单输入类型的元素如 textarea、select 以及不同 type 的 input 元素做双向绑定,其余类型的元素则不支持这种绑定

// src/directives/public/index.js
import { warn, resolveAsset } from "../../../util/index"
import text from "./text"
import radio from "./radio"
import select from "./select"
import checkbox from "./checkbox"

const handlers = {
  text,
  radio,
  select,
  checkbox
}

export default {

  priority: 800,
  twoWay: true,
  handlers: handlers,
  params: ["lazy", "number", "debounce"],

  /**
   * Possible elements:
   *