Vue 2.x 源码解读系列《入坑篇》

前言

阅读 Vue 的源码第一篇,本文主要是说 Vue 的文件目录

在源代码中有一个 src 目录,这个目录就是存放 Vue 源码的文件的,里面有 complier、core、platforms、server、sfc、shared

├─ src
│   ├─ compiler                // 模版解析
│   │   ├─ codegen             // 把 AST(抽象语法树) 转换为 Render 函数
│   │   ├─ directives          // 生成 Render 函数之前需处理的指令
│   │   ├─ parser              // 解析模版成 AST
│   ├─ core                    // Vue 核心代码,包括内置组件,全局API封装,Vue 实例化,观察者,虚拟DOM, 工具函数等等。
│   │   ├─ components          // 组件相关属性,主要是Keep-Alive
│   │   ├─ global-api          // Vue 中的一些全局 API,比如:Vue.use, Vue.extend, Vue.mixin 等
│   │   ├─ instance            // 实例化相关内容,生命周期、事件等
│   │   ├─ observer            // 响应式代码,双向数据绑定相关文件
│   │   ├─ util                // 工具方法
│   │   └─ vdom                // 包含虚拟 DOM 创建(creation)和打补丁(patching) 的代码
│   ├─ platforms               // 和平台相关的内容,Vue.js 是一个跨平台的 MVVM 框架
│   │   ├─ web                 // web 端
│   │   │   ├─ compiler        // web 端编译相关代码,用来编译模版成render函数 basic.js
│   │   │   ├─ runtime         // web 端运行时相关代码,用于创建Vue实例等
│   │   │   ├─ server          // 服务端渲染
│   │   │   └─ util            // 工具类
│   │   └─ weex                // 基于通用跨平台的 Web 开发语言和开发经验,来构建 Android、iOS 和 Web 应用
│   ├─ server                  // 服务端渲染
│   ├─ sfc                     // 转换单文件组件(*.vue)
│   └─ shared                  // 全局共享的方法、常量

上面对一些目录作了备注

Vue 的代码结果非常地清晰,一个 Vue 类代码分散到多个文件中,方便管理。也不会让人看起源码来感觉到无比恐惧。

core 目录中 instance 目录就是定义 Vue 类的相关文件,其中最主要的要数 index.js、init.js、jinject.js、lifecycle.js、proxy.js、render.js、state.js、event.js 。

index.js 代码如下:

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'

// 定义 Vue 构造器
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')
  }
  // 当通过 new 创建 Vue 实例时,调用 _init() 方法,对 Vue 实例进行初始
  this._init(options)
}
// 给 Vue.prototype 添加 _init() 
initMixin(Vue)
// 给 Vue.prototype 添加 $data 对象、$props 对象、$set()、$delete()、$watch()
stateMixin(Vue)
// 给 Vue.prototype 添加 $on()、$once()、$off()、$emit()、
eventsMixin(Vue)
// 给 Vue.prototype 添加 _update()、$forceUpdate()、$destroy()、
lifecycleMixin(Vue)
// 给 Vue.prototype 添加 $nextTick()、_render()
renderMixin(Vue)

export default Vue

// 从上面的代码可以看出这个 Vue 文件表面上看起来非常地简单,但是其内部的实现其实是相当的复杂。
// 官方在代码的规划上可以说是相当的清晰。把有相似逻辑的代码都抽到一块,然后通过引入的方法进行代码的组装。
// 我们可以把这个文件当做是 Vue 的一个结构大纲,至于一些具体的实现结节则在各代码块中单独实现。
// 当我们在 main.js 中 new Vue({}) 时,会调用 _init() 所以我们可以把 _init() 方法看作是生成 Vue 实例的一个入口。

从代码中可以看出 Vue 类的初始化,基本都是通过调用 一个方法,然后传入 Vue 类进行挂载的。这样就可以把代码很好的作归类了,把挂载好 属性和方法的 Vue 暴露出去。

而当我们通过 new 来创建实例时,也只是执行了一个 this._init(options) 方法,所有的初始化都封装在了这个方法里。其中主要代码为:

export function initMixin(Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    // options._isComponent 是在 createComponentInstanceForVnode() 方法中初始化的,这个方法在 vue-dev\src\core\vdom\create-component.js
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // 合并选项,并挂载到 this.$options 上
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor), // 返回 Vue 构造函数自身的配置项
        options || {}, // 用户配置项
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    //  在实例上挂载一些属性:$parent,$root,$children,$refs,_watcher,_inactive,_directInactive,_isMounted,_isDestroyed,_isBeingDestroyed
    initLifecycle(vm)
    // 添加事件监听
    initEvents(vm)
    // 在实例上挂载一些属性:_vnode,_staticTrees,$slots,$scopedSlots,$attrs,$listeners
    initRender(vm)
    // 触发 beforeCreate 钩子函数
    callHook(vm, 'beforeCreate')
    // 注入一些父级 provide(提供)出来的属性,inject 配全后面的 provide 就是我们平时在组件中解决跨多级组件通信问题的一种方法
    initInjections(vm) // resolve injections before data/props
    // 给实例添加 _watchers,使用方法 initProps(),initMethods(),initData(),initWatch() 
    // 分别初始化 props,methods,data,watch
    // 这就是为什么特定的属性或者方法只能在特定的钩子函数中才能访问到的原因
    initState(vm)
    // 向子孙组件提供数据,由于这个方法在 initState() 方法之后执行,所以我们可以把当前组件中的状态(如:props,methods,data)数据传到子子孙组件中。
    initProvide(vm) // resolve provide after data/props
    // 触发 created 钩子函数,当 created 钩子函数触发时,组件中所需要的东西(prop,data,methods,watch)已经创建完成
    callHook(vm, 'created')

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
    // 这个是在什么情况下才会调用???
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

在 this._init() 方法里我们基本可以看到创建一个 Vue 实例的整个过程。更多细节则封装在各个方法中,在 this._init() 方法中直接调用 ,对生命周期、事件、渲染、状态、注入等进一步进行初始化。

initState(vm) 方法中对 props 属性、methods 属性、data 属性、 computed 计算属性、watch 属性等进行初始化。props 属性 和 data 属性以及 computed 计算属性的初始化主要是给其定义的属性添加观察者(拦截)。

至于这些方法的具体实现,后面文章慢慢呈现。

声明

1.原创文章,不经本站同意,不得以任何形式转载,如有不便,请多多包涵!

2.本文永久链接:http://yunkus.com/post/5e3fb871a3b2bb42

3.如果觉得本文对你有帮助,或者解决了你的问题,不妨扫一扫右边的二维码打赏支持,你的一分一毫,可能会让世界变得更美好。

微信
扫一扫,赏我
支付宝
扫一扫,赏我