重学Vue【new Vue 发生了什么】

简介: 重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。

网络异常,图片无法展示
|

重学Vue源码,根据黄轶大佬的vue技术揭秘,逐个过一遍,巩固一下vue源码知识点,毕竟嚼碎了才是自己的,所有文章都同步在 公众号(道道里的前端栈)github 上。


正文


我们一般在 main.js 中会这样写:

import Vue from "vue";
new Vue({
  el: "#app"
})
// 或者
new Vue({
}).$mount("#app")

接着在里面定义 datamethodsmounted 等,既然 Vue 是可以 new 出来的,那 Vue 就应该是一个构造函数,在源码中,分为 定义Vue构造函数扩展Vue构造函数,总体的代码定义在: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'
// install platform specific utils
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)
}
// ...
export default Vue

可以看出来第一句:import Vue from 'core/index' 引入了定义好的 Vue,后面基本上都是对 Vue 这个对象进行了一些扩展,所以可以分为两步看,先看初始化 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


这里除了使用 Object.defineProperty 去定义 Vue 的东西之外,比较关键的就是 import Vue from './instance/index'initGlobalAPI(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 的构造函数,this instanceof Vue 这行代码规定了 Vue 只能是一个构造函数,所以我们的代码中使用了 new Vue({}) 去实例化 Vue。

后面的就是一堆 Mixin,比如 initMixin初始化混入stateMixin状态混入eventMixi事件混入lifecycleMixin生命周期混入renderMixin渲染混入


来简单看一个 initMixin 方法,看它做了什么事情,代码在 src/core/instance/init.js

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // 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)
    }
    // 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
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // 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)
    }
  }
}


挑一个说一下:里面的 mergeOptions 其实就是把传入的 options 最终合并到 vm 的 $options 上,所以就可以通过 $options.el 访问 new Vue({}) 代码里面的 el,通过 $options.data 访问 new Vue({}) 代码里面的 data,methods 和 mounted 等也是同样的道理。紧接着后面初始化了生命周期,事件,渲染,状态等等,最后有这样的代码:

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

意思就是说我们代码里定义的的 el 会通过 $mount 挂载到 vm 上,也就是上面的 #app 了。一旦执行完 $mount 之后,dom上的双向绑定就会生效,定义的数据就会渲染到dom上。


再来看上面提到的 initGlobalAPI(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 上扩展了一些全局方法,扩展的方法都可以在官方文档的API中找到。


现在想一个问题:为什么 mounted 里面可以通过 this. 来获取到 data 里的 name 呢?

import Vue from "vue";
new Vue({
  el: "#app",
  mounted(){
    console.log(this.name);
  },
  //先写成这样,后面说为什么报错
  data: {
    name: "abc"
  }
})

还记得上面的初始化方法 initMixin 么,里面有一个 initState(vm),我们来看下它,在 src/core/instance/state.js 中:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}


可以看到它的逻辑,如果在 $options 定义了 props,就初始化 props,如果在 $options 定义了 methods,就初始化 methods,如果在 $options 定义了 data,就初始化 data,现在重点看下 initData,因为例子中是访问的 data 里的属性name。

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}
export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

它的逻辑大致是,判断 data 是不是一个 function,如果是就走下面的 getData,把 vm 指向 data,这样就可以使用 this.data 了,同时把这个方法赋值给了 vm._data,如果不是一个 function,就重新定义这个 data 为一个对象,紧接着拿到 props,keys 和 methods做一层遍历,如果在 data 中定义一个变量,就不能在 props 中定义这个变量了,methods 中也是如此,因为它们最终都会挂载到vm上,也就是new Vue 实例上,**这就是为什么我们定义同样的变量在 methods 或者 props 中就会报错的原因。**所以上面的例子中的 data 要返回一个对象:

import Vue from "vue";
new Vue({
  el: "#app",
  mounted(){
    console.log(this.name);
  },
  data() {
    return {
      name: "abc"
    }
  }
})


那为什么可以通过 this.name 拿到 data 里的 name 呢?就是后面的 proxy 函数。

const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

proxy 定义了 get 和 set,然后通过 Object.defineProperty 在 target(就是vm) 上定义了 _data 属性,get 方法可以拿到 vm[_data][key],这个 key 就是代码中传入的 name ,也就是说可以通过 this._data.name 获取代码中的 data 里的 name,也就是说上面的 console.log(this.name) 其实就是调用了 console.log(this._data.name)

这就是 proxy 的作用,把访问 this.name 中的 name,代理到了 this._data.name,前面的把 vm._data 赋值给 data,代码中的 proxy(vm, "_data", key) 就是把 data 赋值给了 this._data,所以在 methods 中,可以通过 this.name 获取到 data 里面定义的 name。

目录
相关文章
|
7天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
7天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。
|
7天前
|
存储 缓存 JavaScript
如何在大型 Vue 应用中有效地管理计算属性和侦听器
在大型 Vue 应用中,合理管理计算属性和侦听器是优化性能和维护性的关键。本文介绍了如何通过模块化、状态管理和避免冗余计算等方法,有效提升应用的响应性和可维护性。
|
7天前
|
存储 缓存 JavaScript
Vue 中 computed 和 watch 的差异
Vue 中的 `computed` 和 `watch` 都用于处理数据变化,但使用场景不同。`computed` 用于计算属性,依赖于其他数据自动更新;`watch` 用于监听数据变化,执行异步或复杂操作。
|
6天前
|
JavaScript 前端开发 UED
vue学习第二章
欢迎来到我的博客!我是一名自学了2年半前端的大一学生,熟悉JavaScript与Vue,目前正在向全栈方向发展。如果你从我的博客中有所收获,欢迎关注我,我将持续更新更多优质文章。你的支持是我最大的动力!🎉🎉🎉
|
8天前
|
存储 JavaScript 开发者
Vue 组件间通信的最佳实践
本文总结了 Vue.js 中组件间通信的多种方法,包括 props、事件、Vuex 状态管理等,帮助开发者选择最适合项目需求的通信方式,提高开发效率和代码可维护性。
|
6天前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript和Vue的大一学生。自学前端2年半,熟悉JavaScript与Vue,正向全栈方向发展。博客内容涵盖Vue基础、列表展示及计数器案例等,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
|
8天前
|
存储 JavaScript
Vue 组件间如何通信
Vue组件间通信是指在Vue应用中,不同组件之间传递数据和事件的方法。常用的方式有:props、自定义事件、$emit、$attrs、$refs、provide/inject、Vuex等。掌握这些方法可以实现父子组件、兄弟组件及跨级组件间的高效通信。
|
13天前
|
JavaScript
Vue基础知识总结 4:vue组件化开发
Vue基础知识总结 4:vue组件化开发
|
13天前
|
存储 JavaScript
Vue 状态管理工具vuex
Vue 状态管理工具vuex