VUE源码阅读笔记2:初识源码整体结构,如何写一个类似Vue的庞大的类

蛰伏已久 蛰伏已久 2019-02-13

在上一篇,我们知道vue的核心源码在src/core下,我们可以先看一下整体的目录结构

src
  |--core
     |---component     //猜测是vue用到的组件,看到该目录下主要是keep-alive(页面缓存)源码
     |--golbal-api     //全局api应该在这里设置,对应vue api文档中的全局api
     |--instance       //vue实例化的主要代码
     |--observer       //vue双向绑定采用了观察者模式,观察者模式相关代码
     |--util           //小工具助手代码
     |--vdom           //猜测为虚拟dom的代码
     |--config.js      //配置文件
     |--index.js       //入口文件

可以看到整个核心代码分成了5大模块:全局api,实例化主要代码,观察者模式,助手代码,虚拟dom。

一个良好的目录结构是代码可读性的第一步,我们也应该重视目录结构的划分

先从入口文件index.js读起

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'


//初始化全局API
initGlobalAPI(Vue)

//为vue原型添加只读属性$isServer和$ssrContext
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue

整个文件很简单,主要是两部分,初始化全局API,为vue原型定义了几个只读属性

在这里我们需要重视的是Object.defineProperty这个利器,基本上在一些类库中经常会用到,可以为对象动态添加属性,并对添加的属性进行配置,比如可读,可写,可枚举,可删除等,参考这篇文章http://shanhuxueyuan.com/news/detail/90.html

我们阅读源码最好对应着api文档来看,便于印证我们的猜想,https://cn.vuejs.org/v2/api/

这里为Vue.prototype添加了只读的'$isServer'属性,注意是对Vue的原型protype添加的属性,而不是对Vue添加的属性,因此只有实例化Vue之后,才能访问'$isServer'属性,对应的是api文档中的实例属性vm.$isServer

针对Vue添加的属性属于全局属性,不需要实例化即可访问,即api中的全局属性Vue.**,两种方式对比如下

//全局属性的添加方式,不需要实例化,直接通过Vue.config访问
Object.defineProperty(Vue, 'config', configDef)

//实例属性的添加方式,需要实例化后访问 let vue=Vue(...)   vm.$ssrContext
Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})


接下来,我们看下initGlobalAPI(Vue)主要发生了什么,进入相关文件

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
  
  //config属性的配置,配置为只读方式,只定义了get 
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  
  //再次使用Object.defineProperty,为Vue添加只读属性config
  Object.defineProperty(Vue, 'config', configDef)

  //这个不属于全局API,最好别用
  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  
  //定义全局api,set,delete,nextTick,可以通过api文档查看具体作用
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

  //一系列初始化
  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

这里定义了Vue的一些全局api,可以配合api文档查看其具体api作用,先不深入阅读源码,我们先看整体结构。

再次返回到index.js,里面有个重要内容

import Vue from './instance/index'
....

'./instance/index'文件才是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' &&
    //如果不是通过new关键字实例化的 
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  
  //调用初始化,该方法是由下面的initMinxin(Vue)混入的
  this._init(options)
}


//通过Minxin混入构造复杂的类
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

这里我感觉有两个地方可以学习的,第一个就是如何让一个类必须通过new方式调用,否则就报错。

es6之前,类的写法都是先写一个构造函数,这个函数用户可以进行直接调用的,不一定非要通过new的方式,但是不通过new的方式,实际是不符合我们想法的,因此要想办法阻止

看一个例子

function Test() {
    if(!(this instanceof Test)){
        throw new Error("Test必须通过new关键字调用")
    }

    this.a=123
}

//通过该方式调用会报错
Test()

//通过该方式调用是可行的
var test=new Test()
console.log(test.a)

 为什么直接调用Test()会报错呢,因为此时调用函数,this指向的是window对象,window对象不是Test的实例,所以报错

而通过new方式调用,实际发生了四步操作,可以看出,此时的this就是Test的一个实例,因此不会报错

  1.  创建一个新对象;

  2. 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象) ;

  3. 执行构造函数中的代码(为这个新对象添加属性) ;

  4. 返回新对象。


第二个可以学习的地方就是如何写一个复杂的类,

如果我们把类的所有代码写在一个文件,势必很庞大,不好维护,而vue的这种写法给了我们思路,将不同的模块分成几个不同的文件去写,有负责初始化的,有负责状态管理的,有负责事件管理的,有负责声明周期的,有负责render渲染的,不同的功能模块放到不同的文件中去,然后通过Minxin混入的方式合成一个大类

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

以initMinxin(Vue)为例,我们看看是如何做的

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
   .....
  }
}

在initMinxin中我们传入了Vue,然后给Vue添加原型方法,通过这种方式,实现了将一个庞大的类,拆分成不同的模块/文件去编写

下一节我们看看vue的初始化到底都做了什么



分享到

点赞(0)