VUE源码阅读笔记2:初识源码整体结构,如何写一个类似Vue的庞大的类
在上一篇,我们知道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的一个实例,因此不会报错
创建一个新对象;
将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象) ;
执行构造函数中的代码(为这个新对象添加属性) ;
返回新对象。
第二个可以学习的地方就是如何写一个复杂的类,
如果我们把类的所有代码写在一个文件,势必很庞大,不好维护,而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)