Vue.js源碼解讀--(1)

Vue框架對於前端來講有多重要就很少提了,三天前決定看看源碼,奈何本身是個菜雞,只能慢慢的一點一點啃,進行掃蕩式學習,初有收穫,特將筆記所記內容記下,邏輯略亂,各位客官以爲亂或者有問題的話請評論說下,我會從新組織語言並回答您。html

本文爲小白從頭掃蕩式教程,我都能懂你確定也能的~前端

好的,下面開始。vue

首先你要去github把源碼下載下來啦-- https://github.com/vuejs/vue node

首先咱們進入package.json文件 在咱們 npm run dev後執行這個語句ios

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

-w爲watch,監聽,-c修改執行文件路徑,進入scripts/config.js文件內,最底部。git

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

process.env.TARGET存在,執行github

function genConfig (name) {
  const opts = builds[name]
  const config = {
    input: opts.entry,
    external: opts.external,
    sourceMap:true,
    plugins: [
      replace({
        __WEEX__: !!opts.weex,
        __WEEX_VERSION__: weexVersion,
        __VERSION__: version
      }),
      flow(),
      buble(),
      alias(Object.assign({}, aliases, opts.alias))
    ].concat(opts.plugins || []),
    output: {
      file: opts.dest,
      format: opts.format,
      banner: opts.banner,
      name: opts.moduleName || 'Vue'
    }
  }

  if (opts.env) {
    config.plugins.push(replace({
      'process.env.NODE_ENV': JSON.stringify(opts.env)
    }))
  }

  Object.defineProperty(config, '_name', {
    enumerable: false,
    value: name
  })

  return config
}

上面方法爲把builds相同屬性的內容賦給opts,因此咱們的opts以下web

 'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },

接下來看到入口文件爲resolve('web/entry-runtime-with-compiler.js') npm

const aliases = require('./alias')
const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}

resolve方法將傳入參數通過aliases方法改變,因此最後咱們獲得的入口路徑爲src/platforms/web/entry-runtime-with-compiler.js。json

在該文件內,Vue由./runtime/index導入,進入後,Vue由core/index導入,進入後,Vue由./instance/index導入(instance有實例的意思),進入後,便可發現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' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

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

export default Vue

Vue構造函數中,前五行判斷開發者必須用NEW實例化vue,

new vue時,其中的el等值傳到option內,而後調用_init方法。
構造函數下方,調用initMixin(),進入該文件內

 1 let uid = 0
 2 
 3 export function initMixin (Vue: Class<Component>) {
 4   Vue.prototype._init = function (options?: Object) {
 5     const vm: Component = this
 6     // a uid
 7     vm._uid = uid++
 8 
 9     let startTag, endTag
10     /* istanbul ignore if */
11     if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
12       startTag = `vue-perf-start:${vm._uid}`
13       endTag = `vue-perf-end:${vm._uid}`
14       mark(startTag)
15     }
16 
17     // a flag to avoid this being observed
18     vm._isVue = true
19     // merge options
20     if (options && options._isComponent) {
21       // optimize internal component instantiation
22       // since dynamic options merging is pretty slow, and none of the
23       // internal component options needs special treatment.
24       initInternalComponent(vm, options)
25     } else {
26         console.log(Vue.options)
27       vm.$options = mergeOptions(
28         resolveConstructorOptions(vm.constructor),
29         options || {},
30         vm
31       )
32     }
33     /* istanbul ignore else */
34     if (process.env.NODE_ENV !== 'production') {
35       initProxy(vm)
36     } else {
37       vm._renderProxy = vm
38     }
39     // expose real self
40     vm._self = vm
41     initLifecycle(vm)
42     initEvents(vm)
43     initRender(vm)
44     callHook(vm, 'beforeCreate')
45     initInjections(vm) // resolve injections before data/props
46     initState(vm)
47     initProvide(vm) // resolve provide after data/props
48     callHook(vm, 'created')
49 
50     /* istanbul ignore if */
51     if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
52       vm._name = formatComponentName(vm, false)
53       mark(endTag)
54       measure(`vue ${vm._name} init`, startTag, endTag)
55     }
56 
57     if (vm.$options.el) {
58       vm.$mount(vm.$options.el)
59     }
60   }
61 }
View Code

第一行爲被調用方法,第二行在vue構造函數中掛載了_init方法,因此在構造函數或實例內均可以直接調用。
參數寫法爲flow,第一行參數意思爲傳入參數類型爲component,第二行爲傳入參數類型爲Object,加問號意思爲有無皆可,但類型必須爲Object,問號在後同理。繼續

函數內第一行的this指向被調用函數,即爲Vue實例化的對象,而後將函數掛載兩個屬性:_uid,_isVue,而後判斷options是否存在且其中是否有_isComponent,在尋找構造函數Vue時,core/index中將構造函數掛載了options,下圖爲core/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'

initGlobalAPI(Vue)
console.log(Vue.prototype)
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
})
        console.log(Vue.options)

Vue.version = '__VERSION__'

export default Vue

在initGlobalAPI(下圖)方法中,前十行將config值賦給configDef,並命名爲config到Vue上,且不容許修改configDef的值。而後又加了util,set,delete,nexttick。

export function initGlobalAPI (Vue: GlobalAPI) {
  // 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

  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)
}

下圖爲被加的config值

export type Config = {
  // user
  optionMergeStrategies: { [key: string]: Function };
  silent: boolean;
  productionTip: boolean;
  performance: boolean;
  devtools: boolean;
  errorHandler: ?(err: Error, vm: Component, info: string) => void;
  warnHandler: ?(msg: string, vm: Component, trace: string) => void;
  ignoredElements: Array<string | RegExp>;
  keyCodes: { [key: string]: number | Array<number> };

  // platform
  isReservedTag: (x?: string) => boolean;
  isReservedAttr: (x?: string) => boolean;
  parsePlatformTagName: (x: string) => string;
  isUnknownElement: (x?: string) => boolean;
  getTagNamespace: (x?: string) => string | void;
  mustUseProp: (tag: string, type: ?string, name: string) => boolean;

  // legacy
  _lifecycleHooks: Array<string>;
};

繼續initGlobalAPI方法,Vue加了options方法,並循環ASSET_TYPES的值末尾加s做爲鍵,值爲null,後加了options._base,再用extend方法將components對象中添加KeepAlive,extend方法目前理解爲將第二個值複製給第一個值,(keepalive是一個內容不少的重要文件,在src/core/components中,標記之後看),最下面四個方法分別加了use,mixin,extend(和上邊的不是一個),並在extend中使Vue.cid=0,最後一個方法被直接調用,以下圖二。

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]
import { ASSET_TYPES } from 'shared/constants'
import { isPlainObject, validateComponentName } from '../util/index'

export function initAssetRegisters (Vue: GlobalAPI) {
  /**
   * Create asset registration methods.
   */
  ASSET_TYPES.forEach(type => {
    Vue[type] = function (
      id: string,
      definition: Function | Object
    ): Function | Object | void {
      if (!definition) {
        return this.options[type + 's'][id]
      } else {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && type === 'component') {
          validateComponentName(id)
        }
        if (type === 'component' && isPlainObject(definition)) {
          definition.name = definition.name || id
          definition = this.options._base.extend(definition)
        }
        if (type === 'directive' && typeof definition === 'function') {
          definition = { bind: definition, update: definition }
        }
        this.options[type + 's'][id] = definition
        return definition
      }
    }
  })
}

該方法中,將Vue[component]加上了方法,因爲沒有definition值,options內也無id,因此返回的也是空(該過程僅僅是將Vue添加了三個空方法)。在initglobalapi方法經歷一番後,vue以下(僅爲在initglobalapi方法內所加)。

 

Vue.config
Vue.util = util
Vue.set = set
Vue.delete = del
Vue.nextTick = util.nextTick
Vue.options = {
    components: {
        KeepAlive
    },
    directives: {},
    filters: {},
    _base: Vue
}
Vue.use
Vue.mixin
Vue.cid = 0
Vue.extend
Vue.component = function(){}
Vue.directive = function(){}
Vue.filter = function(){}
let _isServer
export const isServerRendering = () => {
  if (_isServer === undefined) {
    /* istanbul ignore if */
    if (!inBrowser && !inWeex && typeof global !== 'undefined') {
      // detect presence of vue-server-renderer and avoid
      // Webpack shimming the process
      _isServer = global['process'].env.VUE_ENV === 'server'
    } else {
      _isServer = false
    }
  }
  return _isServer
}

最後回到initglobalapi方法調用處,第二,三個皆爲在Vue.prototype調用引號內屬性時,使用get內方法,isServerRendering方法(上圖)爲當調用時使當前環境變爲server,第三個同理(暫時不知道$vnode是什麼),第四個在Vue加上了FunctionalRenderContext屬性名及調用該方法的值(無參數,)最後Vue加上了版本。

再往前回到initMixin方法中,options存在但沒有options._isComponent,走else,

export function resolveConstructorOptions (Ctor: Class<Component>) {
  let options = Ctor.options
  if (Ctor.super) {
    const superOptions = resolveConstructorOptions(Ctor.super)
    const cachedSuperOptions = Ctor.superOptions
    if (superOptions !== cachedSuperOptions) {
      // super option changed,
      // need to resolve new options.
      Ctor.superOptions = superOptions
      // check if there are any late-modified/attached options (#4976)
      const modifiedOptions = resolveModifiedOptions(Ctor)
      // update base extend options
      if (modifiedOptions) {
        extend(Ctor.extendOptions, modifiedOptions)
      }
      options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
      if (options.name) {
        options.components[options.name] = Ctor
      }
    }
  }
  return options
}

resolveConstructorOptions方法中,參數爲vm.constructor,即爲Vue構造函數,暫無super值,直接返回options,調用mergeOptions方法,傳入三個參數,分別爲Vue.options,實例化時所傳參數,和Vue實例化對象。在mergeOptions方法中,

export function mergeOptions (
  parent: Object, //Vue.options
  child: Object, //實例化傳入參數
  vm?: Component //Vue實例化對象
): Object {
  if (process.env.NODE_ENV !== 'production') {  
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)
  const extendsFrom = child.extends
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, vm)
  }
  if (child.mixins) {
    for (let i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm)
    }
  }
  const options = {}
  let key
  for (key in parent) { //循環Vue.options的鍵
    mergeField(key)
  }
  for (key in child) { //循環實例化參數的鍵
    if (!hasOwn(parent, key)) { //若是當前的鍵與Vue.options全部的鍵不重複
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat;     //strats[key]尋找方法,strat定義方法 //defaultStrat爲哪一個有值返回哪一個
    
    options[key] = strat(parent[key], child[key], vm, key);     //左邊在options定義相同的鍵名,右邊調用上邊的方法 //至關於調starts.key方法並傳參
  }
//console.log(parent)
//console.log(child)
//console.log(options)
  return options
}
function checkComponents (options: Object) {
  for (const key in options.components) {
    validateComponentName(key)
  }
}

export function validateComponentName (name: string) {
  if (!/^[a-zA-Z][\w-]*$/.test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'can only contain alphanumeric characters and the hyphen, ' +
      'and must start with a letter.'
    )
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    )
  }
  
}

checkComponents方法爲檢查實例化所傳參數components的值是否合法(兩個方法分別爲檢查是否與內置或保留元素重名)。而後檢查child是否爲function,傳入參數的prop.inject,directives,以及兩個判斷。

而後定義options,循環options內的名及傳入參數的名,進入方法。

function mergeAssets (
  parentVal: ?Object,
  childVal: ?Object,
  vm?: Component,
  key: string
): Object {
  const res = Object.create(parentVal || null)
  if (childVal) {
    process.env.NODE_ENV !== 'production' && assertObjectType(key, childVal, vm)
    console.log(extend(res, childVal))
    return extend(res, childVal)
  } else {
    return res   
  }  
}

ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets
})

strats[屬性名]對應相應方法,ASSET_TYPES中有三個,可爲這三個添加方法,其他在該js上有獨自方法,options[key]直接調用mergeAssets或defaultStrat或本頁js上定義好的方法,最後合成options,從新梳理:循環Vue.options的鍵(爲父,實例化參數爲子)並定義同名鍵至新options,循環調用方法,ASSET_TYPES中有三個調用mergeAssets,該方法爲若是同鍵名的子屬性有值,則返回子屬性的值,沒有則返回空,添加至新的options,當沒有strats.key方法時,則調用defaultStrat方法,該方法爲若是子屬性有值則返回子屬性的值,沒有則返回副屬性的值,並添加至新options,調用strats.data時,有單獨的方法,該方法返回了一個方法,最後返回options。總結:mergeOptions方法新建options對象,將ASSET_TYPES中三個參數設爲新options的鍵並將值設爲空,若是實例化傳入的參數有相同的鍵則傳該值,而後將Vue.options其他值添加鍵名至新options並單獨處理其值,實例化傳參的對象也一樣,最後合併成一個擁有全部的鍵但值被處理過的對象。

 

繼續回溯,咱們在尋找Vue構造函數時在src/platforms/web/runtime/index.js停留過一會,如今看它,

Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

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

// 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)
}

 

前五行爲咱們以前Vue內的config內的鍵賦新值,而後將Vue.options內的兩個對象加新值,掛載patch,和$mount方法。query方法爲獲取該元素(獲取不到則報錯)。再回溯看
entry-runtime-with-compiler.js,

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}
View Code

一樣掛載了$mount方法,但咱們先執行的是這個而後纔是上一個,而後在最下方加上了Vue.compile。compileToFunctions 函數的做用,就是將模板 template 編譯爲render函數。
下面回到_init()方法內...本週就到這裏

{

下面爲小貼士~
在script/config.js內genConfig方法中,加上框內的可在瀏覽器調試(出現src目錄),接下來就能夠盡情的debugger了

}

 

相關文章
相關標籤/搜索