Vue源码解析:this.$data、this._data、this.xxx 为什么都能获取数据?data为什么是个函数?

 

data中定义了一个数据msg, vue实例上访问这个数据有两种方式,this.$data.msg 和 this.msg,请问,为vue如何实现this.msg能直接访问到data中的msg变量???data又为什么是个函数?

分析:

clone 下 vue 的项目源码,然后打开 src/core/instance/index.js

调用 init 方法时先进行了检查,确保已经使用 new Vue 初始化Vue(已构造一个作用域安全的构造函数)并将options 参数传入_init 内 ,this._init 是 Vue.prototype._init ,这个init方法来自于 ‘./init.js’ 中, 是下面initMixin(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) // 判断 this 对象的 prototype 是否存在于构造函数 Vue 的原型链上
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options) // 已构造一个作用域安全的构造函数:new Vue 进行初始化 Vue 并将options 参数传入_init 内 ,this._init 是 Vue.prototype._init ,这个init方法来自于 ‘./init.js’ 中, 是下面initMixin(Vue)增加的
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

进入 src/core/instance/init.js

此时先看这句代码 const vm: Component = this ,此处 this 即 vue实例,然后赋值给了 vm , vm将做为参数向下传递,也就是一会以下的 vm 都是指向 vue 实例

然后看代码 initState(vm) ,初始化状态(这个状态包含data props methods)

// src/core/instance/init.js

export function initMixin (Vue: Class) {
  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) // 初始化render
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm) // 初始化data
    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) // 挂载
    }
  }
}

进入 src/core/instance/state.js

看代码 initData(vm) 初始化 data

// src/core/instance/state.js

// 这个初始化解决了实例化是通过 this 获取 data props methods 内数据
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) { // 如果实例定义了 data
    initData(vm) // 初始化 data
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  ………
}

看以下代码实现了 this.$data.xxx 和 this._data 是两份一模一样的值,虽然我们平时用的都是 vm.$data 里的值,但是看源码其实还有个 vm._data

function initData (vm: Component) {
  let data = vm.$options.data // vm.$options 是访问自定属性,此处就是vue实例中的 this.$data
  data = vm._data = typeof data === 'function' // 先执行三元表达式,然后赋值给 vm._data 和 data,这样子这两个值都是同时变化的,就是 this.$data.xxx 和 this._data 同时变化
    ? getData(data, vm)
    : data || {}
  ………
}

我们打印一下项目中的this.$data、this._data,亲们可以看一下效果

Vue源码解析:this.$data、this._data、this.xxx 为什么都能获取数据?data为什么是个函数?_第1张图片

重点来了:return data.call(vm, vm)

data 函数执行的时候 用 call 方法,让 vm 继承了 data 的属性和方法,也就是 this 继承了 this.$option.data 的属性和方法, 所以我们可以使用 this.xxx

call 知识点:call 是 Function 对象自带的一个方法,可以改变函数体内部的 this 指向,第一个参数就是 this要指向的对象,也就是想指定的上下文,后面的参数它会按顺序传递进去。它的函数会被立即调用。

官网解释:调用一个对象的一个方法,以另一个对象替换当前对象。也就是继承模式:挟持另一个对象的方法,继承另外一个对象的属性

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 为什么是个函数而不是个对象

看以上代码,initData() 方法:先有个 三元表达式 typeof data === ‘function' ? getData(data, vm) : data || {} ,如果data是个函数则执行 getData() 方法,如果不是则返回原值,如果原值不存在则返回一个空对象,但是为什么 data 得是个函数???

Vue源码解析:this.$data、this._data、this.xxx 为什么都能获取数据?data为什么是个函数?_第2张图片

这里直接说结论吧

如果 data 是个对象,那么整个vue实例将共享一份数据,也就是各个组件实例间可以随意修改其他组件的任意值,这就很坑爹了。

但是 data 定义成一个函数,将会 return 出一个唯一的对象,不会和其他组件共享一个对象。

我们注册组件的时候实际上是建立了一个组件构造器的引用,只有使用组件的时候才会真正创建一个组件实例。

那么问题来了,import引入文件时,我们一个页面A引用了3次同一个组件B,但是B组件里又引用了1次组件C,那它们各import了几次呢?

答案:import 都只执行一次,但是不止如此,但凡 export default {} 外的方法都只执行一次,但是 export default {} 对象里的方法都执行了3次。

总结:

this.$data.xxx、this._data.xxx、this.xxx 都能获取数据,先让 this.$data.xxx = this._data.xxx,然后用 call 让当前组件实例 this 继承了 this.$data 的值,也就是 getData 方法 return 出了一份用 call 改变指向将 $data 指向 this 的数据。

data 定义成一个函数,确保了各个组件实例能拥有一份只属于自己的唯一数据

 

历史文章:

  • 手把手教你搭建一个结构清晰易开发易维护的公司的Vue项目,包含axios服务,vuex,公共组件/指令/过滤器/服务等
  • 微信小程序名:你的专属简历(微信可直接搜索出来查看),教你前端如何一个人从零基础开发完整的小程序项目,包括后台可视化数据库
  • koa2+webpack4+React+pm2纯手工架子搭建,SSR项目入门教程以及流程指引详解:手把手教你实现服务端首屏渲染SSR项目
  • 精简版:Vue双向数据绑定 分别用 Object.defineProperty和Proxy的方法
  • Vue源码解析:this.$data、this._data、this.xxx 为什么都能获取数据?data为什么是个函数?
  • 用10行代码实现类似Element-ui里的tree树组件,以及一个有意思的小坑++i为什么与i+1不一样的详解

你可能感兴趣的:(Vue,JavaScript)