携手创作,共同成长!这是我参与「掘金日新计划 · 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>
调试之前的注意事项:
-
为了简单起见,就以 cdn 的形式引用 Vue.js
-
本次调试vue版本为 vue2;
-
引入的vue最好是开发版本的,也就是未被压缩过的,这样方便阅读;(上述示例的代码就是开发版本的)
-
代码中加了一个单词
debugger
,我们可以直接使用浏览器打开我们的html页面,然后右键检查,打开开发者工具。 -
效果图:
- 这里推荐使用 Edge浏览器,底层也是Chromium,而且它的本土化做的比较好。特别是初次使用浏览器的调试工具,有中文提示会对新手友好一点。
2. 调试工具简单介绍
简单介绍:
- 想看代码怎么执行的,可以按 F9,相当于下一步下一步。(我写一堆同步异步的代码,利用这个调试也毫无问题。)
- 但是大多数时候我们并不关心某些函数是如何执行的,所以用的比较多的就是 F10,单步跳过函数调用;
- 想看一个函数如何执行,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)
}
总结:
- Vue本身是一个函数。
- 函数主要做了:
- 判断是不是 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)
}
}
总结:
-
主要功能就是调用初始化的方法。
-
关注的重点是方法 initState (这个方法中会初始化我们的data和 method)
-
其次我们也有收获:了解了实例数据初始化的先后顺序:
`钩子 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);
}
}
总结:
-
这个就比较熟悉了,我们vue中的各种选项。
-
我们可以得到 选项初始化的先后顺序:
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
总结:
-
其实就做了两件事;
-
判断函数名命名是否重复,否则及进行报错处理;
-
将 methods 上的方法通过bind 绑定到 vm 上;
-
疑问:
stop,说几个对上述代码的疑问:
- 为啥注释的序号为
3
所在区域的 if 语句外层有一个大括号; - 为什么在注释的序号为
3
的地方已经校验了methods[key]
是否为函数,在注释的序号为6
的地方还校验了一次methods[key]
是否为函数; noop
是什么;
为了了解真相,我下载了源码
- vue@2.6.14
- 文件目录:
\node_modules\vue\src\core\instance\state.js
第 256 行
答案:
问题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)
}
总结:
- 这里初始化了数据data到 vm._data上;
- 处理的数据的获取: 例如
vm.name 》 vm._data['name']
- 可以尝试把
vm._data
给清空,vm.name
就无法获取数据了;
4.阅读源码整体总结
- method 是通过遍历配置对象的method,依次将method的属性绑定到 this 上的;
- data 是先将数据绑定到
vm._data
,再通过Object.defineProperty
的get set,劫持了 vm[key],实际取值还是获取的vm._data
的数据
end
-
随即我也亲自去调试和阅读了这一块相关的代码,并添加了我自己的注释。
-
总结一下我的收获:
- 熟悉了浏览器的调试工具;
- 理解了使用 this 能获取到 data 和 method 中属性的原因。
- 配置项初始化的顺序:
props 》methods 》data 》computed 》watch
-
加油!!