源码共读 为什么 Vue2 this 能够直接获取到 data 和 methods

130 阅读3分钟

我正在参与掘金会员专属活动-源码共读第一期,点击参与

前言

从易到难开始学习源码。学习源码不是为了面试,只是想揭开别人造的轮子背后的神秘面纱。同时也希望在此过程中提升自己的能力和开拓视野。

准备环境

首页还是克隆一份 vue2源码 到我们本地

  1. 全局安装一个依赖 http-server , npm install -g http-server
  2. 运行命令 http-server -p 8083

image.png

看到上图,服务启动成功

开始调试

开始调试之前,我们先创建一个文件 examples/test.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue2 this 能够直接获取到 data 和 methods</title>
</head>
<body>
  <div id="app">
    {{ msg }}
  </div>
  <script src="../dist/vue.js"></script>
  <script>
    debugger
    new Vue({
      el: '#app',
      data() {
        return {
          msg: 'hello vue'
        }
      },
      methods: {
        printMsg(){
          console.log(this.msg);
        }
      },
    })
  </script>
</body>
</html>

然后使用浏览器打开我们的服务 http://127.0.0.1:8003/examples/test.html

image.png 进入到下图的文件,然后点击右键,选择第一个 Reveal in sidebar 选项,就可以看到源码对应的目录

image.png 然后在下一步往下走,进入到 _init 函数

export function initMixin(Vue: typeof Component) {
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this
		/*** 删除了部分代码 ***/
    // expose real self
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    initInjections(vm) // resolve injections before data/props
    // props、methods、data、computed、watch
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
		/*** 删除了部分代码 ***/
  }
}

这里我删除了部分代码,我们把重点放在 initState 函数上,在这个函数之上我写了注释:props、methods、data、computed、watch。是的没错这个函数就是处理我们 vue2 中的这些属性,让我们再下一步进入到 initState 函数内部,在执行这个函数的时候,传入了一个参数 vm: vm 是 vue 实例化对象。

export function initState(vm: Component) {
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)

  /*** 删除了部分代码 ***/

  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    const ob = observe((vm._data = {}))
    ob && ob.vmCount++
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

// 处理 data
function initData(vm: Component) {
  let data: any = vm.$options.data
  data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
  if (!isPlainObject(data)) {
    data = {}
    __DEV__ &&
      warn(
        'data functions should return an object:\n' +
          '<https://v2.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 (__DEV__) {
      if (methods && hasOwn(methods, key)) {
        warn(`Method "${key}" has already been defined as a data property.`, vm)
      }
    }
    if (props && hasOwn(props, key)) {
      __DEV__ &&
        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
  const ob = observe(data)
  ob && ob.vmCount++
}

// 处理 methods
function initMethods(vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (__DEV__) {
      if (typeof methods[key] !== 'function') {
        warn(
          `Method "${key}" has type "${typeof methods[
            key
          ]}" in the component definition. ` +
            `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(`Method "${key}" has already been defined as a prop.`, vm)
      }
      if (key in vm && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
            `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

initData 函数处理data

  1. 判断data类型是不是函数,如果是函数,则执行后返回,否则直接返回 data || {}
  2. 使用工具函数 isPlainObject 判断data是不是对象类型,如果不是则将 data 设置成空对象,并打印警告
  3. 循环data中所有的属性不能和 props、methods 上的属性相同
  4. 将 data 上的所有属性代理到 vm
  5. 将 data 设置成响应式数据

initMethods 函数处理 methods 对象

循环 methods 对象,判断 methods 上的属性不能不是函数类型,不能跟 props 的属性同名,最后使用 bind 改变每个属性(function 类型)的 this 指向,然后代理到 vm。

走到这里本节的目标就实现了,data 和 methods 的属性都代理到 vm,然后 methods 中的所有属性(function 类型)的指针 this 都是指向 vm。所以 this 等于 vm

总结

本文内容是 vue2 源码的入门级调试,也是刚走了个开头。通过实例化 Vue 进入到 _init 函数,一步一步 vue 初始化的流程,这里只读了其中几个函数的初始化过程,就找到了问题的答案。

本文学到了在 vue2 中 this 是如何 data 和 methods 下的属性。

以上是我对本节学习和理解,如果有什么不对的地方,还请指正!