文章中的
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
实例上的data
和methods
中定义的属性。
要回答这个问题,我们可以深入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
会进行检测,并重新打包,以保证最新产物。
上方我们已经得到了产物,那我们怎么使用呢?
在这里,我们使用了http-server
去手动启动一个服务器,然后在对应的代码文件中引用即可。
新建测试文件
我们在examples
下新建文件index.html
,编写如下代码
<!DOCTYPE html>
<html lang="en">
<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>
<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/
, 得到如下页面。
同时,我们去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)
看到下图,则说明我们可以边修改边调试源码了。
但上面的准备,足够了吗?
生成sourcemap
实际上,上面,我们调试的仅仅时产物,其实不是特别方便,这里都是js
的打包产物,对其进行debug
,其实并没能回溯到我们的源代码。
所以,我们需要开启打包时生成sourcemap
,追述到我们的源代码, 修改package.json
中dev
命令。
“script”: {
"dev": "rollup -w -c scripts/config.js --environment TARGET:full-dev --sourcemap",
重新跑npm run dev
, 得到下方页面。
并且,我们打上断点,如果进入调试模式,那么我们的调试环境基本完成了。
那么,接下来到了读源码的时候了。
由于笔者对于vue
有过一段了解,对于data
和methods
的话,我们猜测一下,应该是在init
的阶段。
我们来看vue
中的__init
, 这里__init
的函数时在initMixin
时候定义的。
跟着debug
, 我们来到了initState(vm)
函数中,这里是初始化了data
和methods
。
这里,其实是初始化了props
,data
, methods
等。这里我们主要看initMethods
和initData
,看看这两个过程做了啥?
initMethods
下面就是initMethods
,即Vue
中初始过程中对methods
属性的操作。
这里我们看主要逻辑(__DEV__
中的逻辑,主要是避免methods
中的key
和props
,保留字
冲突,报警告)。
function initMethods(vm: Component, methods: Object) {
const props = vm.$options.props
for (const key in methods) {
vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
这里,我们就能在Vue
实例中的函数通过this
访问options
中定义的methods
中的属性了。
接着,我们看看initData
.
initData
下方便是initData
的实现。
首先,由于options
中的data
可以为函数/对象。在这里,我们要拿到实际的data
对象,所以就有了
let data: any = vm.$options.data
data = vm._data = isFunction(data) ? getData(data, vm) : data || {}
接着,我们看看核心代码
function initData(vm: Component) {
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.`,
} else if (!isReserved(key)) {
proxy(vm, `_data`, key)
整个流程如下:
get Data
, 得到传入的data
, 获取data
对象,并赋值到vm._data
上。
对data
对象进行遍历,判断key
是否和methods
和props
冲突。
如果不冲突,再执行proxy data
。
接着,我们看看proxy
函数。
实质上,是使用了defineProperty, 拦截了get
, set
, 从而实现代理。
这样子,当我们访问对应代理的key
时候,会访问vm._data[key]
, 这样子我们就能再组件实例中的函数访问直接访问对应的变量了。
阅读源码能让我们学习一些不错的设计思想,以及部分编程规范。这里我们通过阅读vue
的源码,了解了以下的知识点:
Vue2
的源码调试。
Vue
中init
阶段中的initMethods
以及initData
的过程。
学而知不足
,上方的知识也是Vue
的一小部分,也能让我们学习到部分知识点。通过阅读源码,学习别人编码思想,也是一种益处。
Vue源码
为什么 Vue2 this 能够直接获取到 data 和 methods ? 源码揭秘!
FE @ FE
20.5k
粉丝