Vue原理: data属性为什么能直接通过this.访问?

2,551 阅读4分钟

Hello, 这里是Link🤩, 从今天开始, 我打算写一份Vue源码分析的系列文章, 但是请放心, 绝对不会是单纯枯燥的源码, 我们从一些面试题, 场景出发, 我们每篇文章只去关注Vue实现的某些功能, 以及解决一些面试常问的问题. 之后再去理解具体的实现. 让你不必直接面对枯燥的源码.💪💪

20211217173440.jpg

后续, 我们会讲到Vue是如何用一个 watcher 去完成组件实例化, 计算属性, 监听属性等核心功能的, 并且你会理解到, Vue设计的很多精妙之处, 比如最为人熟知的响应式系统等, 一定会收获颇丰哦😋

Vue 实例化的流程图

这是我自己写的一份关于Vue的流程图笔记, 能够非常直观方便的了解整个实例化做的事情. 并且这张图已经放在我的Vue源码项目vue-core-analyse, 并且这个项目的源码有我个人读源码时的备注, 相信对你也会有一定的帮助😝, 欢迎star✨

这个图涵盖了整个Vue实例数据与组件实例化的流程 image.png

  • 这个流程图是vscode插件Draw.io Integration提供的, 你需要下载本插件才能启用

image.png

场景

为什么可以直接通过 this.message 的形式去访问到data函数中的值?

<script src="../../dist/vue.js"></script>
<script>
  var vm = new Vue({
    el: '#el',
    data () {
        return {
            message: "Hello Vue"
        }
    },
    mounted() {
        const message = this.message
    }
  })
</script>

在我们看来, 似乎mouteddata 是两个不同的作用域, 我们应该先访问共同的作用域, 然后再去访问这个data对象才合理🤔, 就像这样

mounted() {
    const message = this.data.message
}

原理

实际上这个处理也很好理解, 我们一起从入口来看一下 Vuedata 属性的处理

_init函数是在Vue这整个项目初始化的时候通过initMixin(Vue)挂载上去的, 当我们 new Vue({})的时候会执行下面这个函数, 这个options, 就是上面那个demo中我们传给 Vue 构造函数的大对象, 里面有data, mounted等配置

// src/core/instance/index.js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    // 要求 new 执行
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options) // 👈
}
  • 在 Vue 中的最后一行执行了一个 this._init(options) 函数就是Vue在被实例化的时候触发一系列行为 这里我对源码进行的删减, 我们只关注data部分的初始化. 这样的一个_init 函数 做了一下几件事情
  1. 定义一个vm等于当前实例

  2. 初步挂载$options并合并配置选项

    这里我们先简单的理解为 vm.$options = options

  3. 执行initState(vm)初始化全部状态

步骤3是我们最需要关注的. 在这个函数中负责初始化配置项中的 props methods data等属性

// src/core/intance/init.js
Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    
    // a flag to avoid this being observed
    vm._isVue = true
    
    // merge options
    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )
    // expose real self
    vm._self = vm
    
    // 初始化状态
    initState(vm)
  }
  • 这个函数这里也就到了我们的终极目标initData(vm)在这里负责初始我们的数据(data)
// src/core/intance/state.js
// 初始化状态
export function initState (vm: Component) {
  const opts = vm.$options
  // 基于我们options中存在的属性进行初始化
  if (opts.data) {
    // 初始化数据
    initData(vm)
  }
  
}

  1. initData函数首先会判断我们的data是否为一个函数, 是的话执行他并返回一个对象. 如果不是则直接使用.
  2. while循环首先对我们的dataprosmethod做一层重名校验, 这里的逻辑十分简单. 我们无需关注
  3. 将这个对象赋值一份给 datavm._data
  4. 然后调用 proxy(vm, _data, key)
// src\core\instance\state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
    
  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]
    /**
     * 对data中的key与 props 和 method 做对比, 要求不能够 key 重复
     */
    if (process.env.NODE_ENV !== 'production') {
      //..
    }
    if (props && hasOwn(props, key)) {
      //...
    } else if (!isReserved(key)) {
      // 代理
      proxy(vm, `_data`, key) // 👈
    }
  }
}

重点在于proxy(vm, _data, key)

  1. proxy函数首先为sharedPropertyDefinition对象定义了一个getset方法
  2. 通过defineProperty去对vm对象做拦截操作

也就是说 当我们通过 vm 也就是我们常用的 this.message 的形式 去访问数据时候, 会通过defineProperty函数做的拦截操作将 this._data.message 转发给我们(赋值同理)

// \src\core\instance\state.js
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
  }
  /**
   * 为什么 在其他属性里, 可以直接通过 this.message 就能拿到 data 中的值?
   *  答案就在这里, vue 在 初始化 data 的时候会通过这个代理函数
   *  将 data 中的 key 值直接放到 vm 实例上进行监控,然后基于上面的对象进行监控
   *  访问 this.message 相当于访问了 this._data.message
  */
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

流程图展示

image.png

new Vue({
    el: '#app',
    data:{
      msg: 'parent-Vue'
    },
    mounted () {
      console.log(this.msg);
      console.log(this._data.msg); // 当然并不推荐通过这种方式访问数据
    }
})

image.png

感谢😘


如果觉得文章内容对你有帮助: