vue2 this 能直接获取到 data 和 methods

149 阅读4分钟

1 学习目标

1. 如何学习调试 vue2 源码
2. data 中的数据为什么可以用 this 直接获取到
3. methods 中的方法为什么可以用 this 直接获取到
4. 学习源码中优秀代码和思想,投入到自己的项目中

2 为什么 this 能直接获取到 data 和 methods ?

const vm = new Vue({
  data: {
    name: '张三'
  },
  methods: {
    sayName() {
      console.log(this.name)
    }
  }
})
console.log(vm.name) // 张三
console.log(vm.sayName()) // 张三

<!-- 普通构造器,如何做到类似 vue 的效果 -->
function Student() {}
const student = new Student({
  data: {
    name: '张三'
  },
  methods: {
    sayName() {
      console.log(this.name)
    }
  }
})
// undefined
console.log(student.name)
// Uncaught TypeError: student.sayName is not a function
console.log(student.sayName())

3 准备调试环境调试源码,新建 index.html 文件

两种调试方式:
1、使用谷歌浏览器,按 F12 打开 Sources 面板调试
2、使用 VSCode ,把 vue.js 拷贝到本地,使用 VSCode 插件 Debugger for Chrome 调试
在 const vm = new Vue({ 打上断点

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="https://unpkg.com/vue@2.6.14/dist/vue.js"></script>
    <title>Document</title>
  </head>
  <body></body>
</html>

<script>
  const vm = new Vue({
    data: {
      name: '张三'
    },
    methods: {
      sayName() {
        console.log(this.name)
      }
    }
  })
  console.log(vm.name)
  console.log(vm.sayName())
</script>

第一种方式打断点截图

image.png

第二种方式打断点截图 VSCode 安装 Debugger for Chrome 插件

image.png


3.1 Vue 构造函数

在new Vue实例时,其实只执行了一个 this._init(options) 继续在 this._init(options) 上打断点

function Vue(options) {
  // 判断是否使用了 new 关键字调用构造函数
  if (!(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

3.2 _init 初始化函数

initState 是对 data 和 props 的处理过程,在 initState(vm) 上 继续打断点

function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    var vm = this
    // a uid
    vm._uid = uid$3++

    var startTag, endTag
    /* istanbul ignore if */
    if (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 */
    {
      initProxy(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 (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)
    }
  }
}

3.3 initState 初始化状态

在 initState 函数中可以发现,这里做了几件事
初始化 props
初始化 methods
初始化、监听 data
初始化 computed
初始化 watch

// Firefox has a "watch" function on Object.prototype...
var nativeWatch = ({}).watch;

function initState(vm) {
  vm._watchers = []
  var opts = vm.$options
  // 存在 props
  if (opts.props) {
    initProps(vm, opts.props)
  }
  // 存在 methods
  if (opts.methods) {
    initMethods(vm, opts.methods)
  }
  // 存在 data
  if (opts.data) {
    initData(vm)
  } else {
    // 给 _data 绑定观察者 observe 函数
    observe((vm._data = {}), true /* asRootData */)
  }
  // 存在 computed
  if (opts.computed) {
    initComputed(vm, opts.computed)
  }
  // 存在 watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

重点看 initMethods 和 initData ,都给打上断点

3.4 initMethods 初始化函数

initMethods 方法 主要是做了一些判断,以及绑定方法 this 指向
判断 methods 中的每一项是不是函数,如果不是警告
判断 methods 中的每一项是不是和 prop 重复定义,如果是警告
判断 methods 中的每一项是不是已经 new Vue 实例 vm 上存在(内部私有变量,比如不能使用 refs),并且以refs ),并且以 或 _ 开头,如果是警告
使用 bind 绑定函数的 this 指向为 vm,也就是 new Vue 的实例对象
这就是为什么可以通过 this 访问 methods 的原因

function initMethods(vm, methods) {
  var props = vm.$options.props
  for (var key in methods) {
    {
      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)
  }
}

/**
 * 是否是对象本身的属性
 * Check whether an object has the property.
 */
var hasOwnProperty = Object.prototype.hasOwnProperty
function hasOwn(obj, key) {
  return hasOwnProperty.call(obj, key)
}

/**
 * 是否是内部私有属性字符串 $ 和 _ 开头
 * Check if a string starts with $ or _
 */
function isReserved(str) {
  var c = (str + '').charCodeAt(0)
  return c === 0x24 || c === 0x5f
}

/* istanbul ignore next */
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

3.4.1 bind、call、apply函数的用法

<!-- 带着问题去思考,bind、call、apply是什么,需要它们做什么? -->
<!-- 先看一下正常函数的调用 -->
function a(xx) {        
    this.x = xx;
}
<!-- this 绑定的其实是 windows  -->
a(5);
console.log(window.x) // 5

<!-- 当我们使用new 关键字创建实例的时候,this 就指向了我们创建的对象  -->
function b(xx) {        
    this.m = xx;
}
var c = new b(5);
console.log(window.m) // undefined

<!-- 如何让正常函数,改变 this 指向?-->

<!-- apply -->
<!--  apply() 接收两个参数,第一个是绑定 this 的值,第二个是一个参数数组 -->
function d(xx) {        
    this.x = xx;
}
var e = {};
// 通俗的说,就是让d的 this 指向e, this.x = 5 等于 e.x = 5
d.apply(e, [5]);
console.log(d.x);    // undefined
console.log(e.x);    // 5

<!-- call -->
<!--  call() 接收多个参数,第一个是绑定 this 的值,其他的都是正常参数传递 -->
<!-- 跟apply的区别,像是把[]解构 -->
d.call(e, 5);

<!-- bind -->
<!-- bind() 参数和 call 一样 -->
<!-- 区别是会生成一个新的函数,并不会立即调用,而apply 和 call 都是立即调用 -->
var f = d.bind(e, 5);
console.log(e) // {}
f() // 执行f()才是调用
console.log(e) // {x: 5}

3.5 initData 初始化函数

initData 方法,主要也是做了一些判断
把 data 数据赋值给 _data
判断 data 是否为对象,如果不是,则警告
遍历 data 数据
如果变量名和 method 冲突,则警告
如果变量名和 props 冲突,则警告
如果变量名不是内部私有变量名称,则对_data 进行代理
监测 data 数据,把 data 变成响应式数据

function initData(vm) {
  var data = vm.$options.data
  data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {}
  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]
    {
      if (methods && hasOwn(methods, key)) {
        warn('Method "' + key + '" has already been defined as a data property.', vm)
      }
    }
    if (props && hasOwn(props, key)) {
      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 */)
}

3.5.1 getData 初始化函数

function getData(data, vm) {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, 'data()')
    return {}
  } finally {
    popTarget()
  }
}

function pushTarget(target) {
  targetStack.push(target)
  Dep.target = target
}

function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

3.5.2 proxy 代理函数

利用 Object.defineProperty 挟持对象的 get 和 set
主要作用是:将 data 绑定到 vm 上,this.xxx === this._data.xxx
这就是为什么可以通过 this 访问 data 的原因

function noop(a, b, c) {}

var sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}

function proxy(target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

3.5.3 Object.defineProperty 定义对象属性

接受三个参数 1、属性所在的对象 2、属性名 3、属性描述对象

const object = {};

Object.defineProperty(object, 'pro', {
  // value: 10, 获取属性时所返回的值。不可与set/get混用
  // writable: true, 属性是否可写。不可与set/get混用
  enumerable: true, // 属性在for in循环中是否会被枚举。
  configurable: true, // 属性是否可被删除。
  get: function () {
    return '11'
  },
  set: function () {
    console.log('set')
  },
});
console.log(object.pro); // 11

3.5.4 Object.defineProperties 定义或修改多个属性

var obj = Object.defineProperties(
  {},
  {
    p1: { value: 1, enumerable: true },
    p2: { value: 2, enumerable: true },
    p3: {
      get: function () {
        return this.p1 + this.p2
      },
      enumerable: true,
      configurable: true
    }
  }
)
// {p1: 1, p2: 2, p3: 3}

4 总结

学习了如何对代码进行调试
为什么 this 可以直接访问 methods 里面的方法:
因为 methods 里的方法是通过 bind 指定了 this 为 new Vue的实例(vm)。
为什么 this 可以直接访问 data 里面的数据:
data里的数据会存储到new Vue的实例上的 _data对象中,访问 this.xxx,是访问 Object.defineProperty代理后的 this._data.xxx。