乐趣区

Vue源码学习一我new了个什么东西

从在基于 Vue-Cli 的项目中,我们在 main.js 一般是这样使用 Vue 的

import Vue from 'vue';
import router from './router';
import store from './store';
import App from './App.vue';

new Vue({
    el: '#app',
    router,
    store,
    render: h => h(App)
});

我们 new 的这个 Vue 倒是是个啥?log 看一下

所以我们 new 的是一个 Vue 对象的实例 ,它包含了图上那些属性和方法。那么这些 实例上的属性和方法又是再哪里加上的呢

我们在 new Vue 的时候用 chrome 打个断点,用下面这个 step into next function call 的工具看看这个 new Vue 到底调用了什么方法

构造函数

我们首先通过全局搜索 function Vue,我们找到真正Vue 的构造函数,在 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'

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 的构造函数,就做了一件事,执行自己的 _init 方法。但在执行 init 之前,我们 log 个一下这个 Vue 实例看看:

……
console.log(this);
this._init(options)
……


怎么就突然冒出来这么多奇奇怪怪的东西?这些在 _init 之前就存在的属性到底是什么时候加到我们这个 Vue 的原型上的?

各种 mixin

首先我们把怀疑的目光放在下面这些 mixin 上,毕竟我们的 _init 既然没有在 function Vue 这个构造函数中申明,那肯定是从哪里加到原型上的。

initMixin

我们先来看看 initMixin 执行前后,Vue 原型上的变化

console.log(Vue.prototype)
initMixin(Vue)
console.log(Vue.prototype)


所以,实际上 initMinxin 就在 Vue 的原型上挂了一个构造函数需要执行的 _init 方法。通过 initMinxin 函数的源码我们也可以印证这一点:

export function initMixin (Vue: Class<Component>) {
  // 对 Vue 扩展,实现_init 实例方法
  Vue.prototype._init = function (options?: Object) {……}
}

stateMixin

console.log(Vue.prototype)
stateMixin(Vue)
console.log(Vue.prototype)


stateMinix 里做的都是一些跟响应式相关的勾当,从上图可以看到他们是 $data$props 两个属性;$set$delete$watch三个方法。源码如下:

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') {dataDef.set = function () {
      warn(
        'Avoid replacing instance root $data.' +
        'Use nested data properties instead.',
        this
      )
    }
    propsDef.set = function () {warn(`$props is readonly.`, this)
    }
  }
  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 {……}
}

通过源码可以知道 $data$props 是只读属性,是通过 Object.defineProperty 来实现的。举个简单的例子,比如我们要直接暴力去修改 $propsVue.prototype.$props = a; 这时候就会触发 propsDefset方法,会警告说$props is readonly

eventMixin

console.log(Vue.prototype)
stateMixin(Vue)
console.log(Vue.prototype)


eventMinix 里做的都是一些事件相关的东西,从上图可以看到,挂载了 $on$once$off$emit 四个函数。

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {vm.$on(event[i], fn)
      }
    } else {(vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {vm._hasHookEvent = true}
    }
    return vm
  }

  Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on () {vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // all
    if (!arguments.length) {vm._events = Object.create(null)
      return vm
    }
    // array of events
    if (Array.isArray(event)) {for (let i = 0, l = event.length; i < l; i++) {vm.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    const cbs = vm._events[event]
    if (!cbs) {return vm}
    if (!fn) {vm._events[event] = null
      return vm
    }
    // specific handler
    let cb
    let i = cbs.length
    while (i--) {cb = cbs[i]
      if (cb === fn || cb.fn === fn) {cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(`Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    let cbs = vm._events[event]
    if (cbs) {cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}

从源码上我们可以看到,这几个函数其实都是在对 vm 实例上的_events 这个数组在进行操作。而 $on$once的区别也很清晰,$once的原理其实就是对 $on 执行的函数进行了封装,这个函数执行前会先将自己$off,从而达到只执行一次的目的。

lifecycleMixin

console.log(Vue.prototype)
lifecycleMixin(Vue)
console.log(Vue.prototype)


从上图可知,lifecycleMinix 里并不是我们想象中的那些生命周期的钩子函数,他挂载了 _update$forceUpdate$destroy 这三个函数。

export function lifecycleMixin (Vue: Class<Component>) {
  // 更新函数
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {……}

  // 强制更新
  Vue.prototype.$forceUpdate = function () {……}

  // 销毁
  Vue.prototype.$destroy = function () {……}
}

renderMixin

console.log(Vue.prototype)
renderMixin(Vue)
console.log(Vue.prototype)


从上图可以知道,renderMixin 里,做的事情就比较多了,除了 $nextTick_render 这两个函数,installRenderHelpers方法还挂载了很多在 render 过程中需要用到的工具函数。

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 {……}
}

我们看一下 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
}

来自上层的封装

在执行完上面 5 个 minxin 方法后,最后 log 出来的 Vue 原型上,与我们在 this._init() 执行之前所看到的 Vue 实例上的属性和方法还是有少了一些,比如 $mount 这个函数,这些东西又是啥时候加到原型上的呢?
既然当前这个 /src/core/instance/index.js 文件没有线索了,而且他最后还把 Vue 这个构造还是 export 了出去,所以我们需要看看有没有文件在外面 import 这个 Vue,然后再加上一些骚操作。
我们搜索 import Vue from 这个关键字,除了 test 目录下的测试代码外,我们发现还有几个文件在import Vue

core/index

initGlobalAPI

在 core/index 里,首先会去初始化化全局 API,即执行 initGlobalAPI(Vue) 这个函数,这个函数会给我们的 Vue 原型和构造函数里加很多东西。

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)

首先实在构造函数上加上了一个只读属性 config,这个 config 里就是 Vue 的全局配置项

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

然后就是 util,里面包含了mergeOptions(来自 src/core/util/options.js),defineReactive(来自 src/core/observer/index.js),extend,(来自 src/shared/util.js)warn(来自 src/core/util/debug.js)

Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick

// 2.6 explicit observable API
Vue.observable = <T>(obj: T): T => {observe(obj)
  return obj
}

接下来给 Vue 构造函数上加入了 setdelete 方法(来自 core/observer/index.js),nextTick方法(来自 src/core/util/next-tick.js),而 observable 实在observe(来自 core/observer/index.js)基础上进行了封装。

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

  Vue.options._base = Vue

  extend(Vue.options.components, builtInComponents)

然后就是在 Vue 构造函数上加入 option 对象,里面有 componentsdirectivesfilters_base。然后这个 extend 的作用是将 keep-alive 这个组件加入到上面的这个 Vue.options.components 中。

 initUse(Vue)
 initMixin(Vue)
 initExtend(Vue)
 initAssetRegisters(Vue)

最后就是初始化一下 useminixextend。其中,在 initExtend 的时候,会给我们的根组件构造器加上唯一 cid=0,以后通过 Vue.extend 构造组件实例的时候,也会给每个实例的构造器加上这个递增的 cid。然后再在用最后的 initAssetRegisters 做一次代理,把 options 里面的componentsdirectivesfilters 直接挂载到 Vue 的构造函数下面。

环境相关

接下来,在 core/index 里,去定义环境相关的一些属性。在 Vue 原型上定义了$isServer$ssrContext,看名字都知道是与服务端渲染 ssr 相关的东西。最后定义在构造函数上一个与函数式组件渲染上下文相关的FunctionalRenderContext

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__'
最后会在构造函数上加上我们的版本信息,在 webpack 打包的时候会替换成当前 Vue 的版本
const version = process.env.VERSION || require('../package.json').version

platforms/web/runtime/index

Vue.js 最初是为 Web 平台设计的,虽然可以基于 Weex 开发原生应用,但是 Web 开发和原生开发毕竟不同,在功能和开发体验上都有一些差异,这些差异从本质上讲是原生开发平台和 Web 平台之间的差异。因此,在 platforms/web/runtime/index 这一层,会加入一些与平台相关的属性与操作。

重写 config

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

因为现在的运行环境已经变成了 web 平台,所以一些全局配置就不能再粗暴的直接给个默认值,而是要具体问题具体分析了,比如这isReservedTag,默认值是 no 的它,就要被重写成根据 tag 返回一个布尔值:

export const isReservedTag = (tag: string): ?boolean => {return isHTMLTag(tag) || isSVG(tag)
}

安装平台的指令和组件

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

其中,web 平台的指令有 modelshow两个,组件有 TransitionTransitionGroup两个,这些都和 dom 息息相关。

安装平台补丁函数

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({nodeOps, modules})

在 Vue 原型上的 __patch__ 方法,是由一个工厂函数 createPatchFunction 返回的,实际上执行的是 src/core/vdom/patch.js 中第 700 的那个 patch 函数。

实现 $mount 方法

// 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 原型上的 $mount 方法,实际上执行的就是 mountComponent 这个方法,只是对 el 进行了一下处理。

web/entry-runtime-with-compiler.js

这个文件是 webpack 编译的入口文件,他主要做了两件事,第一是扩展 $mount 方法,第二个是挂载 compile 方法

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  ……
  // 最后执行的还是 Vue.prototype.$mount
  return mount.call(this, el, hydrating)
}

Vue.compile = compileToFunctions

总结

我们这一次把 Vue init 之前所有挂载的属性和方法都总结了一遍,目的不是为了搞清楚每个属性的意义,每个方法实现的功能(这工作量太大)。而是为后面的源码工作打下基础,知道原型和构造函数上有哪些属性和方法,又是在哪里定义的,不会看到之后一脸懵逼。然后再通过具体的流程去看每个函数,每个属性究竟有什么用。最后我用一脑图总结了一下 init 前 Vue 原型和构造函数上的属性和方法,让我们接下来去看 init 过程时有的放矢。

退出移动版