源码分析,Vue2中data和methods直接通过this访问的原理

前言

文章中的Vue均指的是Vue2版本。(笔者使用的Vue版本是2.7.0-alpha.4

Vue中, 我们经常用下面的写法定义组件的methods以及data, 从而定义一个Vue组件。

const app = new Vue({
    el: '#app',
    data() {
        return {
            count: 0
        }
    },
    methods: {
        add() {
            this.count++;
        }
    }
});
复制代码

这对于我们来说是在熟悉不过了。不过,为什么我们能在Vue内部定义的函数中通过this直接访问该Vue实例上的datamethods中定义的属性。

要回答这个问题,我们可以深入Vue源码, 再次声明,这里使用的版本是v2.7.0-alpha.4来看看。 (tips: 不同的版本可能会有所差异,v2.7.0-alpha.4目前应该已经使用了ts的写法)。

搭建调试环境

克隆代码

git clone https://github.com/vuejs/vue.git
复制代码

下载到源码后,我们来首先看看Readme.md,接着我们看看.github/CONTRIBUTING.md, 阅读文档,来开始我们的开发环境。

安装依赖

Vue项目使用了pnpm, 进行开发,可以了解一下pnpm

pnpm i
复制代码

安装后,我们根据脚本npm run dev, 实质上是运行下方的代码

rollup -w -c scripts/config.js --environment TARGET:full-dev
复制代码

这个时候,每当你改变了src下的源代码,rollup会进行检测,并重新打包,以保证最新产物。

image.png

运行产物

上方我们已经得到了产物,那我们怎么使用呢?

在这里,我们使用了http-server去手动启动一个服务器,然后在对应的代码文件中引用即可。

新建测试文件

我们在examples下新建文件index.html,编写如下代码

<!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">
  <title>Document</title>
</head>
<body>
  <div id="app">
    The count is {{ count }}
    <button @click="count++">add</button>
  </div>
</body>
<script src="../dist/vue.js"></script>
<script>
  const app = new Vue({
    el: '#app',
    data() {
      return {
        count: 1
      }
    },
    methods: {
      log() {
        console.log('create');
      }
    },
    mounted() {
      console.log('mounted');
      this.log();
      console.log(this.count);
    }
  })
</script>
</html>
复制代码

http-server运行

接者,我们来到项目目录, 使用http-server

http-server -p 8082 .  -c-1
复制代码

访问http://127.0.0.1:8082/examples/, 得到如下页面。

image.png

同时,我们去src/core/index.ts中修改源码。

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

+ console.log(Vue);

initGlobalAPI(Vue)
复制代码

看到下图,则说明我们可以边修改边调试源码了。

image.png

但上面的准备,足够了吗?

生成sourcemap

实际上,上面,我们调试的仅仅时产物,其实不是特别方便,这里都是js的打包产物,对其进行debug,其实并没能回溯到我们的源代码。

所以,我们需要开启打包时生成sourcemap,追述到我们的源代码, 修改package.jsondev命令。

{
    // ...
    “script”: {
        "dev": "rollup -w -c scripts/config.js --environment TARGET:full-dev --sourcemap",
        // ....
    }
    // ...
}
复制代码

重新跑npm run dev, 得到下方页面。

image.png

并且,我们打上断点,如果进入调试模式,那么我们的调试环境基本完成了。

image.png

那么,接下来到了读源码的时候了。

分析源码

由于笔者对于vue有过一段了解,对于datamethods的话,我们猜测一下,应该是在init的阶段。 我们来看vue中的__init, 这里__init的函数时在initMixin时候定义的。

跟着debug, 我们来到了initState(vm)函数中,这里是初始化了datamethods

initState.png

这里,其实是初始化了props,data, methods等。这里我们主要看initMethodsinitData,看看这两个过程做了啥?

initMethods

下面就是initMethods,即Vue中初始过程中对methods属性的操作。

initMethods.png

这里我们看主要逻辑(__DEV__中的逻辑,主要是避免methods中的keyprops,保留字冲突,报警告)。

function initMethods(vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    // ... code  __DEV__ 环境下 处理逻辑
    
    // noop 为空函数 bind做了一层`polyfill`, 做兼容
    vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
  }
}
复制代码

这里,我们就能在Vue实例中的函数通过this访问options中定义的methods中的属性了。

接着,我们看看initData.

initData

下方便是initData的实现。

initData.png

首先,由于options中的data可以为函数/对象。在这里,我们要拿到实际的data对象,所以就有了

let data: any = vm.$options.data
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
复制代码

接着,我们看看核心代码

function initData(vm: Component) {
  // get data
  // ...code
  // proxy data on instance
  // 得到`data`所有键
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  // 遍历代码,如果`data`中的`key`和`methods`/`props`中的键是否冲突,只有不冲突,并不为保留关键字的才会,使用执行proxy(vm, `_data`, key)
  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
  // ... code
}
复制代码

整个流程如下:

  1. get Data, 得到传入的data, 获取data对象,并赋值到vm._data上。
  2. data对象进行遍历,判断key是否和methodsprops冲突。
  3. 如果不冲突,再执行proxy data

接着,我们看看proxy函数。

proxy.png

实质上,是使用了defineProperty, 拦截了get, set, 从而实现代理。

这样子,当我们访问对应代理的key时候,会访问vm._data[key], 这样子我们就能再组件实例中的函数访问直接访问对应的变量了。

总结

阅读源码能让我们学习一些不错的设计思想,以及部分编程规范。这里我们通过阅读vue的源码,了解了以下的知识点:

  1. Vue2的源码调试。
  2. Vueinit阶段中的initMethods以及initData的过程。

学而知不足,上方的知识也是Vue的一小部分,也能让我们学习到部分知识点。通过阅读源码,学习别人编码思想,也是一种益处。

参考

猜你喜欢

转载自juejin.im/post/7105766593098940446