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>
第一种方式打断点截图
第二种方式打断点截图 VSCode 安装 Debugger for Chrome 插件
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 上存在(内部私有变量,比如不能使用 或 _ 开头,如果是警告
使用 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。