大前端进阶-读懂vuejs源码1

此文章适合准备第一次阅读vuejs源码的童鞋,由于vuejs的源码非常多,而且分布在各个文件夹中,因此想要读懂源码,需要理清整个框架的脉络。此文章就是从编译入口出发,找到源码中的关键点。

准备工作

打包源码

浏览器调试比单纯的阅读源码更有效率,那么如何为vuejs添加sourceMap?

  1. fork vue源码仓库到自己的github仓库中,这样,可以随便添加注释和修改。
  2. 下载项目,打开package.json文件,找到文件中:

大前端进阶-读懂vuejs源码1_第1张图片

  1. vuejs使用rollup打包,在dev命令最后添加--sourcemap
  2. 执行npm run dev进行打包,生成带sourcemap的vue文件。
    大前端进阶-读懂vuejs源码1_第2张图片
  3. 找到examples文件夹,随便找一个用例,将vue文件的引用地址改为新打包生成的文件。

大前端进阶-读懂vuejs源码1_第3张图片

  1. 用浏览器打开html文件,打开控制台,就可以看到源码。

了解打包文件

在命令行中执行npm run build,会打包所有版本的vue文件,打包结果如下:
大前端进阶-读懂vuejs源码1_第4张图片

其中:

  • common 表示符合commonjs规范的文件。
  • ems 表示符合ES Module规范的文件。
  • dev 表示文件内容未压缩,是可读的。
  • prod 表示文件内容是压缩过的。
  • runtime

表示运行时版本,不包含模版编译功能。也就是在声明组件的时候无法编译template模版,只能使用render函数。 vue-cli构建的项目中,由于打包的时候会将template模版编译成render函数,所以其打包后引用的vue版本为运行时版本。

在vue-cli创建的项目中执行vue inspect > out.js。可以将所有的webpack配置输出到out.js文件中,查看其中的resolve配置可以看到其打包的vue版本:

resolve: {
    alias: {
      vue$: 'vue/dist/vue.runtime.esm.js'
    }
}
  • 未加common,es

表示是umd规范文件,此文件可以支持commonjs,ES Module,AMD,或者直接通过window.Vue方式引用。

  • 未加rentime

表示完整版本,包含运行时和编译器。整体代码比运行时版本多。

入口文件

vuejs项目的打包入口文件,可以当作是源码阅读的入口文件。

打包的时候执行的是如下命令:

rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap

其中scripts/config.js为rollup配置文件所在路径,rollup配置文件要求导出一个对象,其中input属性指定打包入口文件。

scripts/config.js

在该文件的最后:

if (process.env.TARGET) {
  module.exports = genConfig(process.env.TARGET)
} else {
  exports.getBuild = genConfig
  exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
}

由于执行的打包命令中包含--environment TARGET:web-full-dev,所以此时process.env.TARGET值为web-full-dev,也就是通过
genConfig获取配置并导出。

在getConfig方法中,通过const opts = builds[name]获取内置的配置,其中name为web-full-dev。通过opts.entry指定input属性值,其最终值为platforms/web/entry-runtime-with-compiler.js

platforms文件加中存放的是和平台相关的代码,其中web文件夹是和web相关的代码,weex文件夹是和weex相关的代码。

platforms/web/entry-runtime-with-compiler.js

此文件的功能并不复杂,只是修改了Vue原型上的$mount方法和添加静态方法compile

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
   // 具体逻辑
}
// 静态方法compile
Vue.compile = compileToFunctions

在原有$mount方法的基础上添加判断,当vue组件没有定义render时,判断是否传入了templete,如果传了,就将其编译成render。具体逻辑可精简为:

const options = this.$options
// 如果没有传入render
if (!options.render) {
    // 获取template
    let template = options.template

    // .... 此处包含template的各种情况判断

    if (template) {
        // 编译template为render函数
        const { render, staticRenderFns } = compileToFunctions(template, {
            outputSourceRange: process.env.NODE_ENV !== 'production',
            shouldDecodeNewlines,
            shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
        }, this)
        // 在options上面添加render函数
        options.render = render
        // 静态render函数,用于优化Dom渲染过程
        options.staticRenderFns = staticRenderFns
    }
}
// 调用原有的mount方法
return mount.call(this, el, hydrating)
此处 compileToFunctions是模版编译的入口,等到后面编译部分再继续。

platforms/web/runtime/index.js

entry-runtime-with-compiler.js文件中的Vue类引入自platforms/web/runtime/index.js这个文件,此文件为Vue类添加了web平台特有功能,如Dom操作。

此文件可以分为三大块:

  • 扩展config
// 添加web平台特有的一些辅助方法
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag // 判断是否是保留tag,如input
Vue.config.isReservedAttr = isReservedAttr // 是否是保留属性
Vue.config.getTagNamespace = getTagNamespace // 获取元素的命名空间
Vue.config.isUnknownElement = isUnknownElement

此处添加的方法大部分是Vue内部使用,平时工作上几乎用不到,所以不再详解。

  • 添加全局内置组件和指令
// 添加web平台相关的全局内置组件和指令
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

此处添加的组件有: transitiontransition-group
此处添加的指令有: v-modelv-show

  • 添加原型方法

添加了两个关键方法:Dom挂载和Dom更新。

// 添加虚拟Dom更新操作
Vue.prototype.__patch__ = inBrowser ? patch : noop

// 添加挂载方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  // 渲染虚拟Dom
  return mountComponent(this, el, hydrating)
}
mountComponent是Vnodes渲染的入口方法,后续会详细降到Vnodes渲染过程。

core/index.js

platforms/web/runtime/index.js中的Vue类引入自core/index.js文件。

core文件夹包含了vue核心代码,其与平台没有任何关系。

此文件只包含一个关键代码:

// 为Vue类添加全局静态方法如Vue.extend, Vue.component等。
initGlobalAPI(Vue)
// ... 剩余的是和ssr服务端渲染相关的全局属性,此处省略。

initGlobalAPI

定义在core/global-api/index.js中:

export function initGlobalAPI(Vue: GlobalAPI) {
    // 定义config
    Object.defineProperty(Vue, 'config', configDef)

    // 帮助函数,不要直接使用,vuejs不保证会正确执行
    Vue.util = {
        warn,
        extend,
        mergeOptions,
        defineReactive
    }

    // 设置全局的set,delete和nextTick
    Vue.set = set
    Vue.delete = del
    Vue.nextTick = nextTick

    // 添加observable方法
    Vue.observable = (obj: T): T => {
        observe(obj)
      return obj
    }
    // 创建全局的options对象
    Vue.options = Object.create(null)
    // 初始化options中的components,directives,filters三个属性。
    ASSET_TYPES.forEach(type => {
            Vue.options[type + 's'] = Object.create(null)
        })
    // Vue.use
    initUse(Vue)
    // Vue.mixin
    initMixin(Vue)
    // Vue.extend
    initExtend(Vue)
    // Vue.component, Vue.directive, Vue.filter
    initAssetRegisters(Vue)
  }

core/instance/index.js

core/index.js中的Vue类引入自core/instance/index.js文件,此文件定义了Vue的构造函数和实例方法。

定义构造函数

function Vue (options) {
  // 确保Vue不会被当作函数调用
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 执行_init方法,此方法在initMixin中定义
  this._init(options)
}

声明实例属性方法

// 添加实例方法属性
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
此处体现了vuejs针对代码逻辑文件的划分,将不同的功能划分到不同的文件中。
  • initMixin

定义在core/instance/init.js文件中。

export function initMixin(Vue: Class) {
    Vue.prototype._init = function (options?: Object) {
        // ... 省略
    }
}

该文件主要为Vue实例添加_init方法,当创建Vue实例的时候,此方法会被立即调用。

下一部分Vue初始化过程的入口就是此方法。

  • stateMixin

定义在core/instance/state.js中:

export function stateMixin(Vue: Class) {
    // 定义$data属性
    Object.defineProperty(Vue.prototype, '$data', dataDef)
    // 定义$props属性
    Object.defineProperty(Vue.prototype, '$props', propsDef)

    // 定义get,set方法
    Vue.prototype.$set = set
    Vue.prototype.$delete = del

    // 定义watch方法
    Vue.prototype.$watch = function (
        expOrFn: string | Function,
        cb: any,
        options?: Object
    ): Function {
        // ... 省略,在响应式源码部分详解
    }
}
  • eventsMixin

定义在core/instance/events.js中:

export function eventsMixin(Vue: Class) {
    // 定义$on
    Vue.prototype.$on = function (event: string | Array, fn: Function): Component {
        // ... 省略
    }
    // 定义$once
    Vue.prototype.$once = function (event: string, fn: Function): Component {
        // ... 省略
    }
    // 定义$off
    Vue.prototype.$off = function (event?: string | Array, fn?: Function): Component {
        // ... 省略
    }
    // 定义$emit
    Vue.prototype.$emit = function (event: string): Component {
        // ... 省略
    }
}

这里定义的事件注册方法逻辑很近似,都是将注册的方法存储在Vue实例的_events属性中。

_events属性是在_init方法执行的过程中初始化的。

  • lifecycleMixin

定义在core/instance/lifecycle.js中:

export function lifecycleMixin(Vue: Class) {
    // 定义_update方法
    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
        // 调用__patch__执行更新渲染
        if (!prevVnode) {
            // initial render
            vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
        } else {
            // updates
            vm.$el = vm.__patch__(prevVnode, vnode)
        }

    }

    // 定义强制更新方法
    Vue.prototype.$forceUpdate = function () {

    }

    // 定义销毁方法
    Vue.prototype.$destroy = function () {

    }
}

此文件中定义的_update方法是响应式过程中的关键一环,作为观察者Watcher的回调函数,当vm的数据放生变化的时候,会被调用。

  • renderMixin

定义在core/instance/render.js中:

export function renderMixin(Vue: Class) {
    // 定义nextTick方法
    Vue.prototype.$nextTick = function (fn: Function) {
        // 。。。省略
    }
    Vue.prototype._render = function (): VNode {
        // 内部调用options中的render方法,生成虚拟Dom
    }
}

_render方法将配合_update,_update更新时对比的是虚拟Dom,而_render方法就是用于生成虚拟Dom。

总结

至此,vuejs整个项目的web平台文件关系理顺,如下:

core/instance/index.js: 声明Vue构造函数和实例方法属性。
core/index.js:为Vue添加静态方法。
platforms/web/runtime/index.js:针对web平台,添加Dom渲染和加载方法。
platforms/web/entry-runtime-with-compiler.js: 扩展Vue模版编译能力,如果是不带编译的运行时版本就无需针对template进行处理。

你可能感兴趣的:(javascript,前端,vue.js,源码分析)