vue2中为什么可以通过this获取数据和方法。

104 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 20 天,点击查看活动详情

start

  • vue2中我们获取数据和方法,都可以通过 this.xxx 的形式来获取。
  • 看一下vue2是如何处理的

开始调试

1. 调试的代码:

<!DOCTYPE html>
<html lang="en">

<body>
  <!-- 1. 这里引入 Vue.js,版本 v2.7.8 ,开发环境-->
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>

  <script>
    // 2.这里加一个断点方便调试
    debugger

    // 3. 这里 new 一下 Vue,获得 vue实例vm
    var vm = new Vue({

      data: {
        name: '测试名称数据·',
      },
      methods: {
        sayName () {
          console.log('sayName方法执行了:', this.name);
        }
      },
    }
    )

    // 4. 为什么这里可以直接 vm. 的形式获取到数据?
    console.log(vm.name) // 测试名称数据·
    vm.sayName() // sayName方法执行了: 测试名称数据·

  </script>
</body>

</html>

调试之前的注意事项:

  1. 为了简单起见,就以 cdn 的形式引用 Vue.js

  2. 本次调试vue版本为 vue2;

  3. 引入的vue最好是开发版本的,也就是未被压缩过的,这样方便阅读;(上述示例的代码就是开发版本的)

  4. 代码中加了一个单词 debugger,我们可以直接使用浏览器打开我们的html页面,然后右键检查,打开开发者工具。

  5. 效果图:

image-20220817145342411.png

  1. 这里推荐使用 Edge浏览器,底层也是Chromium,而且它的本土化做的比较好。特别是初次使用浏览器的调试工具,有中文提示会对新手友好一点。

2. 调试工具简单介绍

image-20220817151005021.png

简单介绍:

  1. 想看代码怎么执行的,可以按 F9,相当于下一步下一步。(我写一堆同步异步的代码,利用这个调试也毫无问题。)
  2. 但是大多数时候我们并不关心某些函数是如何执行的,所以用的比较多的就是 F10,单步跳过函数调用;
  3. 想看一个函数如何执行,F11;

3. 开始

中文注释皆为我添加的,英文注释为官方自带的

new Vue的过程中其实是会执行 Vue函数的。我们当断点到 new 的时候按F11;

function Vue

function Vue(options) {
  // 1. 判断是不是使用 new 关键词调用的Vue。
  if (!(this instanceof Vue)) {
    warn$2('Vue is a constructor and should be called with the `new` keyword')
  }

  // 2. 开始调用Vue原型上的 _init 方法开始初始化 ;options是我们的传入的配置;
  this._init(options)
}
总结:
  1. Vue本身是一个函数。
  2. 函数主要做了:
    • 判断是不是 new 调用的Vue;
    • 调用原型上的 _init 方法

Vue.prototype._init

Vue.prototype._init = function (options) {
  /* ...删减*/

  /* 
   看点我们看得懂的,vm就是我们的实例。
   依据英文的释义,这里依次初始化:
   1.生命周期
   2.事件
   3.render函数
   4.钩子 beforeCreate
   5.inject
   6.initState 初始化状态
   7.provide
   8.钩子 created
  */
  vm._self = vm
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook$1(vm, 'beforeCreate', undefined, false /* setContext */)
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook$1(vm, 'created')

  /* 如果配置项存在 el , 调用$mount */
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

总结
  1. 主要功能就是调用初始化的方法。

  2. 关注的重点是方法 initState (这个方法中会初始化我们的data和 method)

  3. 其次我们也有收获:了解了实例数据初始化的先后顺序:

    `钩子 beforeCreate``inject``initState 初始化状态``provide``钩子 created`
    

initState 初始化状态

  function initState (vm) {
    vm._watchers = [];
    var 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);
    }
  }
总结
  1. 这个就比较熟悉了,我们vue中的各种选项。

  2. 我们可以得到 选项初始化的先后顺序:

    props 》methods 》data 》computed 》watch

initMethods

function initMethods(vm, methods) {
  /* 
  1. 这里的 methods其实就是我们 传入的数据
    methods: {
      sayName () {
        console.log('sayName方法执行了:', this.name);
      }
    },
  */

  var props = vm.$options.props
  // 2. 开始遍历我们传入的方法
  for (var key in methods) {
    {
      // 3. 如果传入的不是方法,直接报错
      if (typeof methods[key] !== 'function') {
        warn(
          'Method "' +
            key +
            '" has type "' +
            typeof methods[key] +
            '" in the component definition. ' +
            'Did you reference the function correctly?',
          vm
        )
      }

      // 4. 是否和 props重名
      if (props && hasOwn(props, key)) {
        warn('Method "' + key + '" has already been defined as a prop.', vm)
      }

      // 5. 是否和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 $.'
        )
      }
    }

    // 6. 这里才是这个方法的主要目的,利用bind将 methods中的方法绑定到 vm实例上
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}

/* 下面是对 bind的 兼容处理 */
function polyfillBind(fn, ctx) {
  function boundFn(a) {
    var l = arguments.length
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }

  boundFn._length = fn.length
  return boundFn
}

function nativeBind(fn, ctx) {
  return fn.bind(ctx)
}

var bind = Function.prototype.bind ? nativeBind : polyfillBind
总结
  1. 其实就做了两件事;

    • 判断函数名命名是否重复,否则及进行报错处理;

    • 将 methods 上的方法通过bind 绑定到 vm 上;

疑问:

stop,说几个对上述代码的疑问:

  1. 为啥注释的序号为3所在区域的 if 语句外层有一个大括号;
  2. 为什么在注释的序号为 3 的地方已经校验了 methods[key] 是否为函数,在注释的序号为 6 的地方还校验了一次methods[key] 是否为函数;
  3. noop是什么;

为了了解真相,我下载了源码

  • vue@2.6.14
  • 文件目录:\node_modules\vue\src\core\instance\state.js第 256 行

image-20220817165230149.png

答案:

问题1:

process.env.NODE_ENV !== 'production'区分了是不是生产环境的代码。我们使用的是开发环境的代码,然后打包工具保留了大括号;

问题2:

正是因为区分了开发和生产,而我们引入的代码是开发环境的,所以这个地方存在重复判断的情况。

问题3:

文件目录:\node_modules\vue\src\shared\util.js 第 258 行

/**
 * Perform no operation.
 * Stubbing args to make Flow happy without leaving useless transpiled code
 * with ...rest (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/).
 */
export function noop (a?: any, b?: any, c?: any) {}

// 有什么作用?给你一个函数什么都不做........

initData

function initData(vm) {
  var data = vm.$options.data
  // 1.第一个就很有意思,这里就实现了 data 可以为函数也可以为对象,这里就做了处理
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}

  // 4.是不是对象
  if (!isPlainObject(data)) {
    data = {}
    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
  var keys = Object.keys(data)
  var props = vm.$options.props
  var methods = vm.$options.methods
  var i = keys.length
  while (i--) {
    var key = keys[i]

    // 5.是否和函数重名
    {
      if (methods && hasOwn(methods, key)) {
        warn(
          'Method "' + key + '" has already been defined as a data property.',
          vm
        )
      }
    }

    // 6.是否和props重名
    if (props && hasOwn(props, key)) {
      warn(
        'The data property "' +
          key +
          '" is already declared as a prop. ' +
          'Use prop default value instead.',
        vm
      )
      // 7.是否保留字符
    } else if (!isReserved(key)) {
      // 8.代理
      proxy(vm, '_data', key)
    }
  }

  // 13. observe我们的数据
  // observe data
  observe(data, true /* asRootData */)
}

// 2. 看一下这个方法做了什么,其实就是call调用了一下
function getData(data, vm) {
  // #7573 disable dep collection when invoking data getters

  // 3. pushTarget和popTarget,上面英文已经注释了,作用: 在调用数据获取器时禁用dep收集。
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, 'data()')
    return {}
  } finally {
    popTarget()
  }
}

// 10. 这个其实就是一个配置对象
var sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop,
}

// 9.看一看 proxy, 我们调用的方式: proxy(vm, '_data', key)
function proxy(target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter() {
    // 12. 所以这里可以这样理解  vm.name  》 vm._data['name']
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter(val) {
    // 这里同 12
    this[sourceKey][key] = val
  }

  // 11. 所以this指向target
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
总结:
  1. 这里初始化了数据data到 vm._data上;
  2. 处理的数据的获取: 例如vm.name 》 vm._data['name']
  3. 可以尝试把vm._data给清空,vm.name就无法获取数据了;

4.阅读源码整体总结

  • method 是通过遍历配置对象的method,依次将method的属性绑定到 this 上的;
  • data 是先将数据绑定到vm._data,再通过 Object.defineProperty 的get set,劫持了 vm[key],实际取值还是获取的vm._data的数据

end

  • 本文参考了 为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘

  • 随即我也亲自去调试和阅读了这一块相关的代码,并添加了我自己的注释。

  • 总结一下我的收获:

    • 熟悉了浏览器的调试工具;
    • 理解了使用 this 能获取到 data 和 method 中属性的原因。
    • 配置项初始化的顺序:props 》methods 》data 》computed 》watch
  • 加油!!