Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

new Vue 发生了什么 #1

Open
daodaolee opened this issue Feb 23, 2021 · 0 comments
Open

new Vue 发生了什么 #1

daodaolee opened this issue Feb 23, 2021 · 0 comments

Comments

@daodaolee
Copy link
Owner

daodaolee commented Feb 23, 2021

我们一般在 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。

@daodaolee daodaolee changed the title new Vue 发什么了什么 new Vue 发生了什么 Feb 23, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant