Vue源码精解_01_从构建开始

前言

其实之间也写过关于vue2的一些源码分析的笔记,但是因为各种各样的原因导致先帝创业未半而中道崩殂这样的局面。因为自己的目标是成为前端架构师,为了往这个方向靠拢一直在学习各方面的知识,前端后端运维相关都在学习,自己感觉很累。然后平时学习的东西大部分工作中用不上,这又让自己感觉浪费了很多时间和精力。左脚踩着学习,右脚踩着躺平,所以前段子就一直在这种状态下反复横跳。但是最近在B站上看到技术胖给的建议:https://www.bilibili.com/video/BV1Tf4y1G7dq?spm_id_from=333.880.my_history.page.click

首先解决态度问题,心态上放稳,放弃一些东西 得到一些东西。确定目标。然后四个方法一个流程的走

1.精熟技术 2.揣摩实现 3.打造轮子 4.找到调性 

所以我打算这段时间精熟技术,揣摩实现,主要是根据黄轶老师的Vue.js技术揭秘博客来学习Vue源码,然后自己补充一些其他的内容,希望这段时间能够利用业余时间专心地把这系列笔记写好,有写的错误地方欢迎指出,respect

1. Vuejs源码如何构建

vue2基于rollup构建的,它的构建相关配置都在 scripts 目录下
当在开发执行 npm run dev的时候,
实际上就会执行 rollup -w -c scripts/config.js --environment TARGET:web-full-dev
在这里插入图片描述
于是rollup找到根目录下的script目录下的config.js文件,

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

根据参数TARGET:web-full-dev调用genConfig获取当前环境的配置

  // Runtime+compiler development build (Browser)
  'web-full-dev': {
  	// 构建的入口 js 文件地址
    entry: resolve('web/entry-runtime-with-compiler.js'),
    // 构建后的 js 文件地址
    dest: resolve('dist/vue.js'),
    // 构建的格式
    format: 'umd',
    // 构建的环境
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },

web-full-dev配置为例,它的entryresolve('web/entry-runtime-with-compiler.js'),先来看下resolve函数的定义

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

取数组第一个元素设置为base,在我们这个例子中,参数pweb/entry-runtime-with-compiler.jsbasewebbase并不是真的路径,而是借助了别名的配置,现在看一下别名配置的代码,在script/alias

const path = require('path')

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

这里 web对应的真实的路径是 path.resolve(__dirname, '../src/platforms/web'),这个路径就找到了 Vue.js源码的 web 目录。然后 resolve函数通过 path.resolve(aliases[base], p.slice(base.length + 1)) 找到了最终路径,它就是 Vue.js 源码 web 目录下的 entry-runtime-with-compiler

2.从入口开始

我们之前提到过 Vue.js 构建过程,在 web 应用下,我们来分析 Runtime + Compiler 构建出来的 Vue.js,它的入口是src/platforms/web/entry-runtime-with-compiler.js

/* @flow */

import config from "core/config";
import { warn, cached } from "core/util/index";
import { mark, measure } from "core/util/perf";

import Vue from "./runtime/index";
import { query } from "./util/index";
import { compileToFunctions } from "./compiler/index";
import {
  shouldDecodeNewlines,
  shouldDecodeNewlinesForHref,
} from "./util/compat";

// ...... 

export default Vue;

那么,当我们的代码执行 import Vue from 'vue'的时候,就是从这个入口执行代码来初始化 Vue

3.Vue的入口

3.1 找到入口

在这个入口 JS 的上方我们可以找到 Vue 的来源:import Vue from './runtime/index',我们先来看一下这块儿的实现,它定义在 src/platforms/web/runtime/index.js

import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser, isChrome } from 'core/util/index'

import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index'

import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'

// ... 折叠对Vue对象的扩展

export default Vue

这里关键的代码是 import Vue from 'core/index',之后的逻辑都是对 Vue 这个对象做一些扩展,可以先不用看,我们来看一下真正初始化 Vue 的地方,在 src/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)

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

这里有 2 处关键的代码,import Vue from './instance/index' 和 initGlobalAPI(Vue),初始化全局 Vue API(稍后介绍),我们先来看第一部分,在 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'

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 的真面目,实际上是用一个 Function 实现的类,我们只能通过 new Vue取实例化,为什么这里不用ES6的 Class 去实现呢?往后面我们可以看到里面有很多 xxxMixin 的函数调用,并把Vue作为参数传入,它们的功能都是给 Vue 的prototype 上扩展一些方法,Vue按功能把这些扩展分散到多个模块中去实现,而不是在一个模块中实现所有,这是用 Class 难以实现的。这样做的好处是非常方便代码的维护和管理,这种编程技巧也非常值得我们去学习。

3.2 扩展Vue的一些mixin调用

initMixin(Vue):在Vue的原型对象上挂载了 _init 方法,当执行 new Vue()的时候,才会执行 function Vue 中的 this._init

stateMixin(Vue):通过Object.definePropertyVue.prototype上定义了两个属性 $data$props,通过重写的get返回对应的私有属性,如果在开发环境下通过$data$props去改变值,就会报对应的警告,因为这个两个属性是只读的,然后又在Vue.prototype(Vue)上定义了 $set/$delete/$watch方法,这三个方法在后面会讲解,知道是在这里定义的就行

eventsMixin(Vue):在Vue.prototype上定义了$on$once$off$emit等方法,这些方法如何实现自行了解,比如说面试题实现事件总线,定义一个全局的_event对象,然后存储emit发射的方法…

lifecycleMixin(Vue):在Vue.prototype上定义了 _update(目前知道有这个东西就行),$forceUpdate(这个就是强制触发依赖更新,99%情况下不要用);然后$destroy方法

renderMixin(Vue)Vue.prototype上定义了 $nextTick_render

4.initGlobalAPI

Vue.js 在整个初始化过程中,除了给它的原型 prototype上扩展方法,还会给 Vue 这个对象本身扩展全局的静态方法,它的定义在 src/core/global-api/index.js

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

这里就是在 Vue 上扩展的一些全局方法的定义,Vue 官网中关于全局 API 都可以在这里找到,这里不会介绍细节,会在之后的章节我们具体介绍到某个 API 的时候会详细介绍。有一点要注意的是,Vue.util 暴露的方法最好不要依赖,因为它可能经常会发生变化,是不稳定的

4.1 configDef

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

这里劫持了Vue的config属性,使的无法对其进行修改。

4.2 Vue.util

Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

4.2.1 warn

首先看 warn,它实际来自于/src/core/util/debug.js

warn = (msg, vm) => {
    const trace = vm ? generateComponentTrace(vm) : ''

    if (config.warnHandler) {
      config.warnHandler.call(null, msg, vm, trace)
    } else if (hasConsole && (!config.silent)) {
      console.error(`[Vue warn]: ${msg}${trace}`)
    }
  }

  tip = (msg, vm) => {
    if (hasConsole && (!config.silent)) {
      console.warn(`[Vue tip]: ${msg}` + (
        vm ? generateComponentTrace(vm) : ''
      ))
    }
  }

用过Vue的warnHandler功能应该都知道,这是一个自定义警告处理函数。其实也就是我们可以通过自定义warnHandler函数做一些项目警告的收集,同样的功能还有errorHandler,如果有需要可以去官方文档看看。generateComponentTrace方法会追踪到项目警告组件的踪迹,也就是一个定位功能。如果没有定义warnHandler,在写组件不规范的情况下,就会在控制台打印错误

4.2.2 extend

顺着找下去来自于 /share/utils

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

作用就是将源对象的属性混入到目标对象。

4.2.3 mergeOptions

/core/util/options.js

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)

  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
}

作用是将父子的策略项合并为一个checkComponents就是检查组件,内部遍历了传入的childcomponents属性检查组件名字是否规范。
normalizeProps其作用就是统一将props: [?]props: {?}形式转换为后者的形式。
normalizeInject也是统一标准化传入的injectinject一般是父组件或者祖父组件 provide的值,也是一种通信方式。
normalizeDirectives标准化自定义指令,如果传入的是个function, 会被定义成{ bind: function, update: function}这种形式。
下面还有如果子组件有 extends或者mixins将会改变父组件等功能,等遇到具体调用的时候再分析。
mergeField这个是实际做的事情,它会按照strats定义的的策略进行合并

strats.el = strats.propsData = function (parent, child, vm, key) {
strats.data = function () {...}
strats.props = ...
strats.methods = ...
strats.computed = ...
strats.watch = ...
...

4.2.4 defineReactive

是Vue 实现响应式的关键,后续会详细再说,这里相当于给util赋予了这个功能

4.2.5 其他(可了解)

setdel作用就是响应式的添加或删除属性
nextTick可以在下次 DOM更新循环结束之后执行延迟回调。与JS的事件运行机制非常像,会单独记录一篇文章。
Vue.observable这个是 Vue 2.6版本新增的API,很明显的看到了它使用了Vue的Observe,假如一个小型的项目根本用不上Vuex进行状态管理,可以使用它自定义一个小型的响应式store供全局使用,详情可以参照官方文档。
Vue.options

// 遍历了ASSET_TYPES初始化Vue.options
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })
// 下面是ASSET_TYPES的定义
  export const ASSET_TYPES = [
    'component',
    'directive',
    'filter'
  ]

extend(Vue.options.components, builtInComponents) builtInCompoents属性混入Vue.options.components,里面是一些keep-alive相关的东西。

initUse(Vue)

export function initUse (Vue: GlobalAPI) {
  Vue.use = function (plugin: Function | Object) {
    const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
    if (installedPlugins.indexOf(plugin) > -1) {
      return this
    }

    // additional parameters
    const args = toArray(arguments, 1)
    args.unshift(this)
    if (typeof plugin.install === 'function') {
      plugin.install.apply(plugin, args)
    } else if (typeof plugin === 'function') {
      plugin.apply(null, args)
    }
    installedPlugins.push(plugin)
    return this
  }
}

Vue上添加了use,接收一个插件参数,安装插件。平时我们注册类似router等插件的时候,就会用到Vue.use(router)。其内部维护了一个_installedPlugins数组用来存储所有安装的插件,安装时会判断插件是否实现了install方法,如果有就会执行插件的install方法,在这里Vue巧妙的将自己注入到install方法的参数中,这样的好处就是,在实现install方法就不需要import Vue了。

initMixin

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}

全局混入,将mixin通过合并策略混入全局的options,一旦使用全局混入,由于局部注册组件,其实是调用了Vue.extend方法,其调用时也会进行mergeOptions,所以会影响每一个之后创建的 Vue 实例。应当恰当使用。

initExtend

export function initExtend (Vue: GlobalAPI) {
  /**
   * Each instance constructor, including Vue, has a unique
   * cid. This enables us to create wrapped "child
   * constructors" for prototypal inheritance and cache them.
   */
  Vue.cid = 0
  let cid = 1

  /**
   * Class inheritance
   */
  Vue.extend = function (extendOptions: Object): Function {
    extendOptions = extendOptions || {}
    const Super = this
    const SuperId = Super.cid
    const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {})
    if (cachedCtors[SuperId]) {
      return cachedCtors[SuperId]
    }

    const name = extendOptions.name || Super.options.name
    if (process.env.NODE_ENV !== 'production' && name) {
      validateComponentName(name)
    }

    const Sub = function VueComponent (options) {
      this._init(options)
    }
    Sub.prototype = Object.create(Super.prototype)
    Sub.prototype.constructor = Sub
    Sub.cid = cid++
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

    // For props and computed properties, we define the proxy getters on
    // the Vue instances at extension time, on the extended prototype. This
    // avoids Object.defineProperty calls for each instance created.
    if (Sub.options.props) {
      initProps(Sub)
    }
    if (Sub.options.computed) {
      initComputed(Sub)
    }

    // allow further extension/mixin/plugin usage
    Sub.extend = Super.extend
    Sub.mixin = Super.mixin
    Sub.use = Super.use

    // create asset registers, so extended classes
    // can have their private assets too.
    ASSET_TYPES.forEach(function (type) {
      Sub[type] = Super[type]
    })
    // enable recursive self-lookup
    if (name) {
      Sub.options.components[name] = Sub
    }

    // keep a reference to the super options at extension time.
    // later at instantiation we can check if Super's options have
    // been updated.
    Sub.superOptions = Super.options
    Sub.extendOptions = extendOptions
    Sub.sealedOptions = extend({}, Sub.options)

    // cache constructor
    cachedCtors[SuperId] = Sub
    return Sub
  }
}

这里实现了一个js的经典继承,此方法用于创建Vue的子类构造函数。在这里就可以验证了上面说的,调用时会将Super.optionsextendOptions合并。

initAssetRegisters

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

ASSET_TYPES在上面已经提到过了,这里是初始化Vue的注册器,比如我们要注册组建的时候会调用Vue.component,要自定义指令时会使用Vue.directive,就是在这定义的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值