美文网首页大型vue架构FrontEnd程序员
Vue源码解析一——骨架梳理

Vue源码解析一——骨架梳理

作者: snow_in | 来源:发表于2019-03-14 19:32 被阅读0次

    大家都知道,阅读源码可以帮助自己成长。源码解析的文章也看了不少,但是好记性不如烂笔头,看过的东西过段时间就忘的差不多了,所以还是决定自己动手记一记。

    首先看下项目目录,大致知道每个文件夹下面都是干什么的


    Vue.png

    当我们阅读一个项目源码的时候,首先看它的package.json文件,这里包含了项目的依赖、执行脚本等,可以帮助我们快速找到项目的入口。

    我们来看几个重要字段:

    // main和module指定了加载的入口文件,它们都指向运行时版的Vue,
    "main": "dist/vue.runtime.common.js",
    "module": "dist/vue.runtime.esm.js",
    

    当打包工具遇到该模块时:

    1. 如果已经支持pkg.module字段,会优先使用es6模块规范的版本,这样可以启用tree shaking机制
    2. 否则根据main字段的配置加载,使用已经编译成CommonJS规范的版本。

    webpack2+和rollup都已经支持pkg.module, 会根据module字段的配置进行加载

    接下来看一下scripts里面部分脚本配置:

    "scripts": {
      // 构建完整版umd模块的Vue
      "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
      // 构建运行时cjs模块的Vue
      "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
      // 构建运行时es模块的Vue
      "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
      // 构建web-server-renderer包
      "dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
      "dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
      "build": "node scripts/build.js",
      "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer"
    },
    

    umd让我们可以直接用script标签来引用Vue

    cjs形式的模块是为browserify 和 webpack 1 提供的,他们在加载模块的时候不能直接加载ES Module

    webpack2+ 以及 Rollup可以直接加载ES Module,es形式的模块是为它们服务的

    接下来,我们将基于dev脚本进行分析

    当我们执行npm run dev命令时,

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

    可以看到配置文件是scripts/config.js,传给配置文件的TARGET变量的值是‘web-full-dev’。

    在配置文件的最后,是这样一段代码:

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

    因为process.env.TARGET有值,所以执行的是if里面的代码。根据process.env.TARGET === 'web-full-dev', 我们看到这样一段配置:

    // Runtime+compiler development build (Browser)
      'web-full-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'), // 入口文件
        dest: resolve('dist/vue.js'), // 最终输出文件
        format: 'umd', // umd模块
        env: 'development',
        alias: { he: './entity-decoder' },
        banner
      },
    

    现在我们知道了入口文件是'web/entry-runtime-with-compiler.js',但是web是指的哪一个目录呢?在scripts下面有一个alias.js文件,里面定义了一些别名:

    module.exports = {
      vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
      compiler: resolve('src/compiler'),
      core: resolve('src/core'),
      shared: resolve('src/shared'),
      web: resolve('src/platforms/web'),
      weex: resolve('src/platforms/weex'),
      server: resolve('src/server'),
      entries: resolve('src/entries'),
      sfc: resolve('src/sfc')
    }
    

    可以看到web是指的'src/platforms/web',所以入口文件的全路径就是src/platforms/web/entry-runtime-with-compiler.js

    我们使用Vue的时候,是用new关键字进行调用的,这说明Vue是一个构造函数,接下来我们就从入口文件开始扒一扒Vue构造函数是咋个情况。

    寻找Vue构造函数的位置

    打开入口文件src/platforms/web/entry-runtime-with-compiler.js,我们看到这样一句代码

    import Vue from './runtime/index'
    

    这说明Vue是从别的文件引进来的,接着打开./runtime/index文件,看到

    import Vue from 'core/index'
    

    说明这里也不是Vue的出生地,接着寻找。打开core/index,根据别名配置可以知道,core是指的'src/core'目录。Vue依然是引入的

    import Vue from './instance/index'
    

    没办法,接着找。在./instance/index下面看到这样一段代码

    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')
      }
      this._init(options)
    }
    

    长吁一口气,Vue构造函数终于找到源头了。最后我们再理一下这个路径

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

    ——> src/platforms/web/runtime/index.js

    ——> src/core/index.js

    ——> src/core/instance/index.js

    接下来我们从出生地开始一一来看

    Vue构造函数——实例属性和方法

    来看一下src/core/instance/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')
      }
      this._init(options)
    }
    
    initMixin(Vue)
    // 在Vue的原型上添加了_init方法。在执行new Vue()的时候,this._init(options)被执行
    stateMixin(Vue)
    // 在vue的原型上定义了属性: $data、$props,方法:$set、$delete、$watch
    eventsMixin(Vue)
    // 在原型上添加了四个方法: $on $once $off $emit
    lifecycleMixin(Vue)
    // 在Vue.prototye上添加了三个方法:_update $forceUpdate $destory
    renderMixin(Vue)
    // 在原型上添加了方法:$nextTick _render _o _n _s _l _t _q _i _m _f _k _b _v _e _u _g _d _p
    
    export default Vue
    
    

    该文件主要是定义了Vue构造函数,然后又以Vue为参数,执行了initMixin、stateMixin、eventsMixin、lifecycleMixin、renderMixin这五个方法。

    Vue构造函数首先检查了是不是用new关键字调用的,然后调用了_init方法。

    接下来五个方法分别在Vue的原型上添加了各种属性和方法。首先来看initMixin

    initMixin

    打开'./init'文件,找到initMixin方法,发现它其实只做了一件事:

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

    就是在Vue.prototype上挂载了_init方法,在执行new Vue()的时候,该方法会执行。

    stateMixin

    export function stateMixin (Vue: Class<Component>) {
      // flow somehow has problems with directly declared definition object
      // when using Object.defineProperty, so we have to procedurally build up
      // the object here.
      const dataDef = {}
      dataDef.get = function () { return this._data }
      const propsDef = {}
      propsDef.get = function () { return this._props }
      if (process.env.NODE_ENV !== 'production') { // 不是生产环境,设置set
        dataDef.set = function () {
          warn(
            'Avoid replacing instance root $data. ' +
            'Use nested data properties instead.',
            this
          )
        }
        propsDef.set = function () {
          warn(`$props is readonly.`, this)
        }
      }
      // $data 和 $props是只读属性
      Object.defineProperty(Vue.prototype, '$data', dataDef)
      Object.defineProperty(Vue.prototype, '$props', propsDef)
    
      Vue.prototype.$set = set
      Vue.prototype.$delete = del
    
      Vue.prototype.$watch = function (
        expOrFn: string | Function,
        cb: any,
        options?: Object
      ): Function {
        ...
      }
    

    这个方法首先在Vue.prototype上定义了两个只读属性$data$props。为什么是只读属性呢?因为为属性设置set的时候有一个判断,不能是生产环境。

    然后在原型上定义了三个方法:$set, $delete, $watch

    eventsMixin

    export function eventsMixin (Vue: Class<Component>) {
      const hookRE = /^hook:/
      Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {}
    
      Vue.prototype.$once = function (event: string, fn: Function): Component {}
    
      Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {}
    
      Vue.prototype.$emit = function (event: string): Component {}
    }
    

    这里面是在原型上挂载了四个方法,这几个方法平时也都经常用到,肯定很熟悉

    lifecycleMixin

    export function lifecycleMixin (Vue: Class<Component>) {
      Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {}
    
      Vue.prototype.$forceUpdate = function () {}
    
      Vue.prototype.$destroy = function () {}
    }
    

    添加了三个生命周期相关的实例方法:

    • _update:
    • $forceUpdate: 迫使Vue实例重新渲染,包括其下的子组件
    • $destory: 完全销毁一个实例, 触发生命周期beforeDestroy和destroyed

    renderMixin

    export function renderMixin (Vue: Class<Component>) {
      // install runtime convenience helpers
      installRenderHelpers(Vue.prototype)
    
      Vue.prototype.$nextTick = function (fn: Function) {
        return nextTick(fn, this)
      }
    
      Vue.prototype._render = function (): VNode {}
    }
    

    首先是以Vue.prototype为参数调用了installRenderHelpers方法,来看一下这个方法干了啥:

    export function installRenderHelpers (target: any) {
      target._o = markOnce
      target._n = toNumber
      target._s = toString
      target._l = renderList
      target._t = renderSlot
      target._q = looseEqual
      target._i = looseIndexOf
      target._m = renderStatic
      target._f = resolveFilter
      target._k = checkKeyCodes
      target._b = bindObjectProps
      target._v = createTextVNode
      target._e = createEmptyVNode
      target._u = resolveScopedSlots
      target._g = bindObjectListeners
      target._d = bindDynamicKeys
      target._p = prependModifier
    }
    

    也是在原型上挂载了各种方法, 用于构造render函数。

    之后又在原型上挂载了两个实例方法$nextTick_render

    至此我们大致了解了instance/index.js里面的内容,就是包装了Vue.prototyp,在其上挂载了各种属性和方法。

    Vue构造函数——挂载全局API

    接下来来看/src/core/index文件

    /** 
    * 添加全局API,在原型上添加了两个属性$isServer和$ssrContext,加了version版本属性
    */
    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'
    
    // 在 Vue 构造函数上添加全局的API
    initGlobalAPI(Vue)
    
    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的版本号
    Vue.version = '__VERSION__'
    
    export default Vue
    
    

    首先是导入了构造函数Vue和其他三个变量,接下来就是以Vue构造函数为参数调用了initGlobalAPI方法,该方法来自./global-api/index。我们先把下面的内容看完再回过头来分析该方法。

    接下来是在Vue.prototype上面挂载了两个只读属性$isServer$ssrContext。之后又在Vue构造函数上添加了FunctionalRenderContext属性,根据注释知道该属性是在ssr中用到的。

    最后在Vue构造函数上添加了静态属性version,其值是__VERSION__,这是个什么鬼?打开/scripts/config.js,可以看到这么一句代码:

    __VERSION__: version
    

    而version的值在文件最上面可以看到:

    process.env.VERSION || require('../package.json').version
    

    所以最终的值就是Vue的版本。

    我们再回过头来看一下initGlobalAPI函数,从函数名可以猜出它应该是定义全局API的,其实也就是这样。

    先看前部分代码

    // 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', configDef) // 只读属性
    
      // 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
      }
    
      Vue.set = set
      Vue.delete = del
      Vue.nextTick = nextTick
    
      // 2.6 explicit observable API
      Vue.observable = <T>(obj: T): T => {
        observe(obj)
        return obj
      }
    
    

    先是定义了只读属性config。接着定义了util属性,并且在util上挂载了四个方法。只不过util以及它下面的方法不被视为公共API的一部分,要避免使用,除非你可以控制风险。

    接着就是在Vue上添加了四个属性:set、delete、nextTick、observable.

    然后定义了一个空对象options

    Vue.options = Object.create(null)
    

    之后通过循环填充属性:

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

    ASSET_TYPES的值通过查找对应文件后知道为['component', 'directive', 'filter'],所以循环之后options对象变为:

    Vue.options = {
      components: Object.create(null),
      directives: Object.create(null),
      filters: 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
    

    这是在options上添加了_base属性

    接下来是这句代码

    // 将builtInComponents的属性混合到Vue.options.components中
      extend(Vue.options.components, builtInComponents)
    

    extend 来自于 shared/util.js 文件,代码也很简单

    /**
     * Mix properties into target object.
     */
    export function extend (to: Object, _from: ?Object): Object {
      for (const key in _from) {
        to[key] = _from[key]
      }
      return to
    }
    

    builtInComponents 来自于 core/components/index.js 文件

    import KeepAlive from './keep-alive'
    
    export default {
      KeepAlive
    }
    

    现在为止,Vue.options变成

    Vue.options = {
        components: {
            KeepAlive
        },
        directives: Object.create(null),
        filters: Object.create(null),
        _base: Vue
      }
    

    在函数的最后,调用了四个方法:

      // 在Vue构造函数上添加use方法,Vue.use()用来安装Vue插件
      initUse(Vue)
      // 添加全局API:Vue.mixin()
      initMixin(Vue)
      // 添加Vue.cid静态属性 和 Vue.extend 静态方法
      initExtend(Vue)
      // 添加静态方法:Vue.component Vue.directive Vue.filter
      // 全局注册组件、指令、过滤器
      initAssetRegisters(Vue)
    

    我们先大致了解这几个方法的作用,至于具体实现以后再详细分析。

    第二个阶段大体就了解完了,就是挂载静态属性和方法。

    Vue平台化包装

    接下来来看platforms/web/runtime/index.js文件,我们之前看的两个文件是在core目录下的,是Vue的核心文件,与平台无关的。platforms下面的就是针对特定平台对Vue进行包装。主要分两个平台:web和weex, 我们看的是web平台下的内容。

    首先是安装特定平台的工具函数

    // install platform specific utils
    Vue.config.mustUseProp = mustUseProp
    Vue.config.isReservedTag = isReservedTag
    Vue.config.isReservedAttr = isReservedAttr
    Vue.config.getTagNamespace = getTagNamespace
    Vue.config.isUnknownElement = isUnknownElement
    

    Vue.config我们之前见过,它代理的是/src/core/config.js文件抛出的内容,现在是重写了其中部分属性。

    // install platform runtime directives & components
    extend(Vue.options.directives, platformDirectives)
    extend(Vue.options.components, platformComponents)
    

    这是安装平台运行时的指令和组件。extend的作用我们都已经知道了。来看一下platformDirectives和platformComponents的内容。

    platformDirectives:

    import model from './model'
    import show from './show'
    
    export default {
      model,
      show
    }
    
    

    platformComponents:

    import Transition from './transition'
    import TransitionGroup from './transition-group'
    
    export default {
      Transition,
      TransitionGroup
    }
    
    

    Vue.options之前已经有过包装,经过这两句代码之后变成:

    Vue.options = {
        components: {
            KeepAlive,
            Transition,
            TransitionGroup
        },
        directives: {
            model,
            show
        },
        filters: Object.create(null),
        _base: Vue
    }
    

    继续看下面的代码

    // install platform patch function
    Vue.prototype.__patch__ = inBrowser ? patch : noop
    
    // public mount method
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && inBrowser ? query(el) : undefined
      return mountComponent(this, el, hydrating)
    }
    

    这是添加了两个实例方法:__patch__$mount

    看完之后我们就知道了该文件的作用

    1. 设置平台化的Vue.config
    2. 在Vue.options上混合了两个指令:modelshow
    3. 在Vue.options上混合了两个组件:TransitionTransitionGroup
    4. 在Vue.prototye上添加了两个方法:__patch__$mount

    compiler

    到目前为止,运行时版本的Vue已经构造完了。但是我们的入口是entry-runtime-with-compiler.js文件,从文件名可以看出来这里是多了一个compiler。我们来看看这个文件吧

    // 获取拥有指定ID属性的元素的innerHTML
    const idToTemplate = cached(id => {
      const el = query(id)
      return el && el.innerHTML
    })
    
    const mount = Vue.prototype.$mount
    Vue.prototype.$mount = function ( // 重写了$mount方法
      el?: string | Element,
      hydrating?: boolean
    ): Component {}
    
    /**
     * Get outerHTML of elements, taking care
     * of SVG elements in IE as well.
     */
    function getOuterHTML (el: Element): string {
      if (el.outerHTML) {
        return el.outerHTML
      } else {
        const container = document.createElement('div')
        container.appendChild(el.cloneNode(true))
        return container.innerHTML
      }
    }
    
    // 添加compile全局API
    Vue.compile = compileToFunctions
    
    export default Vue
    

    这个文件主要是重写了Vue.prototype.$mount方法,添加了Vue.compile全局API

    以上,我们从Vue构造函数入手,大致梳理了项目的脉络。理清楚了大体流程,之后再慢慢探索细节。

    相关文章

      网友评论

        本文标题:Vue源码解析一——骨架梳理

        本文链接:https://www.haomeiwen.com/subject/nszcmqtx.html