名词解释:
vm:指Vue实例
一、目标效果
vue定义
(1)新建vm时,可以通过一个data对象,或者data函数,其属性可以通过vm直接访问,而data对象可以通过vm._data获取
(2)修改vm._data.xxx时,等价于修改this.xxx
响应式对象
(1)能够监听到data对象属性的修改
(2)能够监听到data下对象属性的属性
响应式数组
(1)能够监听到通过数组方法修改数组
(2)数组中保存的对象,也能被监测修改
(3)不监听数组通过下标修改
测试先行:
<script src="../dist/vue.js"></script>
<script>
var vm = new Vue({
data() {
return {
student: {
name: 'Chicken',
age: 17
},
value: 12,
hobby: ['Sing', 'Jump', 'Rap', {
play: ['basketball']
}]
}
},
})
console.log(vm)
console.log('vm.value === vm._data.value:',vm.value === vm._data.value);
console.log('1、修改data下属性,可触发响应式:');
vm.value = 15
console.log('2、修改data内对象属性,可触发响应式:');
vm.student.name = 'lisi'
console.log('3、使用数组方法,可触发响应式:');
vm.hobby.push('football')
console.log('4、直接通过数组下标修改数组,不触发响应式:');
vm.hobby[3] = 'football'
</script>
二、Vue实例定义
参考Vue源码,使用构造函数的方式,通过创建传入一个配置对象生成实例
option传给vue之后,直接通过this.$option挂载到示例上,方便后面,读取配置项进行初始化
初始化单独在一个init文件中进行
import init from "./init"
function Vue(option) {
// 挂载到vms上
this.$option = option
init(this)
};
export default Vue
三、在Vue实例中定义data属性
data中的值,可以通过多个地方修改,但修改的结果时同步的,此时就不能直接粗暴地挂载在vm上:例如:
要把data.foo挂载到this.foo,如果直接使用this.foo = data.foo,则执行this.foo = newVal之后,data.foo还是旧的值。
由上面的反例可得,我们要挂载的内容不是foo的值,而是data的foo这个属性,通过this.foo修改值,应该同步到data.foo上去
通过Object.defineProperty可以动态地给对象添加属性,读取的值和返回的值都可以通过getter、setter自己定义,所以我们获取、修改值时,直接去操作data对象。
// init.js
function initData(vm) {
const opt = vm.$option
let data = opt.data
if (typeof data === 'function') {
data = data()
}
// 在vm上定义_data和data中的数据
defineGlobal(vm, '_data', { _data: data })
Object.keys(data).forEach(key => {
defineGlobal(vm, key, data)
})
}
/**
* 在vm上定义代理对象
* @param {*} vm vue实例
* @param {*} key 需要在实例上定义的key
* @param {*} source 需要被代理的源对象
*/
function defineGlobal(vm, key, source) {
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get() {
return source[key]
},
set(v) {
source[key] = v
}
})
}
四、对象监听
与vue挂载data类似,可以使用Object.defineProperty去代理data中的所有属性,并且递归地去代理所有的子属性,通过setter就可以监听属性的修改。
具体实现:创建一个Observer类,需要监听的对象作为参数传入构造函数,然后在这个类的构造函数中去给对象添加响应式。添加响应式监听之后,把类实例对象挂到监听对象中去,作为一个已经被监听的标志位,也直接使用监听的相关方法,否则,其实直接通过方法处理就足够了,并不需要作为一个类。
// init.js
function initData(vm) {
...
// 观察data中内容
observe(data)
...
}
// observe/index.js
class Observer {
constructor(obj) {
if (Array.isArray(obj)) {
...
} else {
this.observeObj(obj)
}
// 在被监听的对象上,定义已被监听的标志位,指向Observer对象自身
// 在其它文件中,也方便直接使用监听的相关方法
Object.defineProperty(obj, '__ob__', {
value: this
})
}
/**
* 监听对象
* @param {Object} obj 需要观察的对象
*/
observeObj(obj) {
Object.keys(obj).forEach(key => {
const val = obj[key]
// 尝试监听对象的属性
observe(val)
// 定义响应式
defineReactive(obj, key)
})
}
}
// 导出的方法
/**
*
* @param {Object} obj 需要监听的对象
* @returns
*/
export function observe(obj) {
if (typeof obj !== 'object') {
return null
}
if (obj.__ob__) {
return obj.__ob__
}
return new Observer(obj)
}
/**
* 给对象添加响应式
* @param {Object} obj 需要定义响应式的对象
* @param {String} key 对象的属性key
*/
export function defineReactive(obj, key) {
let value = obj[key]
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
get() {
return value
},
set(val) {
console.log('set data', obj, key, val);
value = val
}
})
}
五、数组监听
由于数组长度可能比较大,如果对数组的每个对象都进行响应式监听,则性能损耗太高,比如数组长度为1万,则需要对一万个属性的进行响应式监听:Object.defineProperty(arr, i, {})
由于数组对数组进行操作,主要时通过push、pop、splice等等方法去修改的,通过下标直接修改比较少,因此只监听数组的方法调用进行监听。
具体实现:
举例,我们要监听对象arr的push方法,不能直接重写原型对象方法arr.__proto__.push = XXX,因为arr._proto指向的是Array.prototype, 直接修改后,所有数组对象的push方法都会被修改。
利用原型链中会逐级查找属性的特点,我们可以创建一个对象,在这个对象中重新定义push等方法,然后,需要监听的数组,只需要原型对象指我们创建的数组原型重写对象,就可以实现监听
而这个对象的原型对象应该指向Array对象,因为我们并不需要重写所有的数组方法,而是只需要重写一部分会修改原数组的方法,并且,我们重新定义的方法,最后也是要调用Array中的原方法是实现方法逻辑的,相当于对方法进行了一层代理,每次调用时,能够通知到我们,做一些操作。
// array.js
// 需要监听的方法
const observeMethods = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
const arrayProto = Array.prototype
// 创建一个空对象,原型对象指向Array的原型属性上
const obArrayProto = Object.create(arrayProto)
// 重写需要监听的方法
observeMethods.forEach(methodName => {
const originalMethod = arrayProto[methodName]
obArrayProto[methodName] = function (...args) {
let inserted
// 新增元素的方法,需要给新元素添加响应式
switch (methodName) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
default:
break;
}
const observe = this.__ob__
if (inserted) {
observe.observeArray(inserted)
}
console.log(methodName, args);
// 需要使用call,因为originalMethod方法没有通过XXX对象去调用,因此,如果直接执行,方法中的this会指向window,在严格模式下会指向undefined
// 通过call方法调用,让originalMethod方法内的this指向数组对象,因为这个方法是通过arr.XXX()调用的
return originalMethod.call(this, ...args)
}
})
/**
* 监听数组方法
* @param {Array} arr
*/
export function observeArrayMethod(arr) {
// 数组的原型对象指向重写了部分方法的新的原型对象
Object.setPrototypeOf(arr, obArrayProto)
}
// ********************** observe/index.js ********************
import { observeArrayMethod } from "./array"
class Observer {
constructor(obj) {
if (Array.isArray(obj)) {
this.observeArray(obj)
} else {
...
}
...
}
/**
* 监听数组
* @param {Array} arr 需要观察的数组
*/
observeArray(arr) {
// 监听数组的修改方法
observeArrayMethod(arr)
// 尝试监听数组内的所有元素
arr.forEach(observe)
}
}
总结:
目录结构:
![](https://i-blog.csdnimg.cn/blog_migrate/564a0ff54522717d1438227ef9bb9818.png)