每天阅读一会儿Vue源码(一)__Vue.js__前端
发布于 9 个月前 作者 banyungong 540 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

每天抽空阅读源码一小会儿,不为啥,一来自己经常使用Vue,阅读了应该会懂得更多,而来就是为了跟着卷一会儿…

阅读源码肯定要从入口文件先开始,而想找Vue的源码入口文件,就得先从package.json来从查看,因为我们通常是使用npm命令来打包编译的,所以这里一定有相关的文件指向。

"scripts": {
    "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
    "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
    "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
    "dev:test": "karma start test/unit/karma.dev.config.js",
    "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 ",
    "dev:weex": "rollup -w -c scripts/config.js --environment TARGET:weex-framework",
    "dev:weex:factory": "rollup -w -c scripts/config.js --environment TARGET:weex-factory",
    "dev:weex:compiler": "rollup -w -c scripts/config.js --environment TARGET:weex-compiler ",
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build -- weex",
    "test": "npm run lint && flow check && npm run test:types && npm run test:cover && npm run test:e2e -- --env phantomjs && npm run test:ssr && npm run test:weex",
    "test:unit": "karma start test/unit/karma.unit.config.js",
    "test:cover": "karma start test/unit/karma.cover.config.js",
    "test:e2e": "npm run build -- web-full-prod,web-server-basic-renderer && node test/e2e/runner.js",
    "test:weex": "npm run build:weex && jasmine JASMINE_CONFIG_PATH=test/weex/jasmine.js",
    "test:ssr": "npm run build:ssr && jasmine JASMINE_CONFIG_PATH=test/ssr/jasmine.js",
    "test:sauce": "npm run sauce -- 0 && npm run sauce -- 1 && npm run sauce -- 2",
    "test:types": "tsc -p ./types/test/tsconfig.json",
    "lint": "eslint src scripts test",
    "flow": "flow check",
    "sauce": "karma start test/unit/karma.sauce.config.js",
    "bench:ssr": "npm run build:ssr && node benchmarks/ssr/renderToString.js && node benchmarks/ssr/renderToStream.js",
    "release": "bash scripts/release.sh",
    "release:weex": "bash scripts/release-weex.sh",
    "release:note": "node scripts/gen-release-note.js",
    "commit": "git-cz"
}

呼~好家伙,一进来看着密密麻麻的这一坨命令的确吓一跳。不过,挑重点的来看,一眼望去第一行dev命令就很正,发现其执行的命令如下:

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

通过这行命令发现了命令会找这个文件:scripts/config.js,并且还有个web-full-dev,于是找到对应文件的位置发现以下代码:

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

看到entry就知道了,这是一个入口的文件位置,说明已经成功一半了,于是我跟着resolve('web/entry-runtime-with-compiler.js')路径跟着去找文件,发现一个问题:

image.png

震惊整个UC新闻部啊,没有web目录!
小小冷静了一会儿,发现问题应该是出在resolve上面(因为平时使用node会直接这样使用,所以造成了误解)。果然,发现这里的resolve是自定义的一个方法,如下:

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

原来,这个路径的/前面那一个是一个别名,在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'),
  sfc: resolve('src/sfc')
}

那就很清楚了,这个entry最终指向的是:src/platforms/web/entry-runtime-with-compiler.js,于是迫不及待打开这个文件,发现其中引入了Vue文件。天哪!主角登场了:

...
import Vue from './runtime/index'
...

跟着,又打开runtime/index文件发现又再次引入:

...
import Vue from 'core/index'
...

都叫core目录,一看就很核心,再次进入,发现如下代码:

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

最终的位置终于定格在了instance/index这个文件下了,因为我终于发现了我们的主角:

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

从这个文件可以知道,做了两件事情:

  1. 定义Vue对象:里面判断是否通过new生成的,并且执行了_init这个方法;
  2. Vue对象传入了各个Minxin方法中去。

看到这里,我立马就有一个疑问了,这个_init方法是在哪儿定义的,又是干嘛的?得~想知道答案,还得去那几个Mixin方法里面去找。于是,我先从initMixin方法中找,于是打开init.js文件:


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

一进来就看见Vue的原型链上挂了个常威,不对是_init方法。把思绪转向_init方法,开始探索:

  1. 首先发现定义了一个vm对象来指向当前实例;
const vm: Component = this
  1. 好像是做了一个性能的记录:
// a uid
vm._uid = uid++

let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  startTag = `vue-perf-start:${vm._uid}`
  endTag = `vue-perf-end:${vm._uid}`
  mark(startTag)
}
  1. 初始化参数:
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
  // optimize internal component instantiation
  // since dynamic options merging is pretty slow, and none of the
  // internal component options needs special treatment.
  initInternalComponent(vm, options)
} else {
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  )
}

上面第一句vm._isVue = true从注释看来应该是避免被观察(和观察者模式有关?)
接着,就通过判断,对实例对象进行不同的处理,看到这里我一直没发现哪里挂载了_isComponent属性,所以就直接看下面了。
发现了vm.$options是通过mergeOptions这个方法来合并参数的,在mergeOptions里面又有一个resolveConstructorOptions来做处理,于是找到该方法,发现:

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
}

发现是判断是否有super这个属性来做对应操作的,至少我看到这里是还没有发现哪里有挂载所以,直接返回了构造函数的options(这个又在哪儿定义的?),调试发现居然已经挂载些对象:

image.png

天!这哪里来的,只能先作罢,往前面继续看mergeOptions这个函数,发现其功能如下:

/**
 * Merge two option objects into a new one.
 * Core utility used in both instantiation and inheritance.
 */
export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

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

  normalizeProps(child, vm)
  normalizeInject(child, vm)
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, 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) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

发现这个是一个工具类,其作用注释翻译过来就是:

将两个选项对象合并为一个新对象。
用于实例化和继承的核心实用程序。

其中做了大抵两件事儿:一件检测参数,并做对应提示(很多我们开发中遇到的警告,可以在这儿找到),另外一件就是合并参数了,太难读了,暂时不钻研进去,先过流程(原谅我的菜)。

回到init.js文件,又发现这一块代码:

/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
  initProxy(vm)
} else {
  vm._renderProxy = vm
}

发现在非生产环境下,做了一个initProxy操作,我点进该方法一看,人傻了:

config.keyCodes = new Proxy(config.keyCodes, {
  set (target, key, value) {
    if (isBuiltInModifier(key)) {
      warn(`Avoid overwriting built-in modifier in config.keyCodes: .${key}`)
      return false
    } else {
      target[key] = value
      return true
    }
  }
})

怎么会有Proxy的操作啊,这个不是Vue3用的吗,可是看提交时间是以前的…

image.png

总之,这里做了一个代理操作,回去看其他流程吧。在init.js文件中又发现剩下代码(天,这个方法好磨人):

/ expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
  vm._name = formatComponentName(vm, false)
  mark(endTag)
  measure(`vue ${vm._name} init`, startTag, endTag)
}

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}

发现做了仨件事:

  1. 初始化生命周期、状态等(肯定很复杂,后面再看);
  2. 性能测试(和前面结合使用的吧);
  3. 根据判断来挂载el对象(这应该就是写了el属性不用$mount的原因吧,本质上就是通过$mount来挂载的)。

今儿先到这,阅读源码超时了,主要是我菜了,看着费劲。
To be continue…

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 可乐爱宅着 原文链接:https://juejin.im/post/7028553804887162916

回到顶部