Yapei Li

专注于前端领域

0%

Vue2实现之初始化状态及数据响应式

Alt text

vue2options API的,在vue3中 保留了

options api 无法 tree-shaking

vue2是一个构造函数不是类(class),通过原型的方式vue实现功能:vue.prototype = xxx

.babelrc 文件不用引入 默认会执行,preset 预设插件集合

package.json

1
2
3
4
"main": "rollup.config.js", //指定配置文件=
"scripts": {
"dev": "rollup -c -w" //-c(-config)表示 使用配置文件;-w(-watch)表示文件变化时 重新打包
},

1、vue初始化

当用户 new Vue的时候 就调用init方法进行vue的初始化

2、vm.$options 代表 用户传入的所有属性

3、响应式数据变化,数据代理 (Object.defineProperty)

3.1、初始化状态initState(vm)

将所有数据都定义在vm属性上,并且后续更改都需要触发视图更新

3.1.1、数据的初始化initData

3.1.1.1、数据劫持

vue实例的data有两种情况 :要么是函数要么是对象,对data类型进行判断 如果是 函数就获取函数返回值

获取函数执行结果的时候 用 data.call(vm) 保证 data函数中的 this 指向vue实例 其实和返回值无关

3.1.1.2、观测这个数据observe(对象类型拦截)

props 初始化在data之前

只对对象类型进行观测 非对象类型无法观测直接return

通过Observe类来实现对数据的观测,类方便扩展,类会产生实例以作为唯一标识

1
2
3
4
5
6
7
8
9
10
11
export function observe(data){
// console.log(data,'observe')
if(typeof data !== 'object' || data == null){
return;
}
if(data.__ob__){ //数据上有这个属性表示已经观测过了 防止 循环引用
return;
}
// 通过类来实现对数据的观测,类方便扩展,类会产生实例以作为唯一标识
return new Observe(data)
}

3.1.1.2.1、将对象中的所有key 重新用defineProperty定义为响应式的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function defineReactive(data,key,value){
// value可能也是一个对象
observe(value); // 对结果进行递归拦截
Object.defineProperty(data,key,{
get(){
console.log('get')
return value;
},
set(newValue){
console.log('set')
if(newValue === value) return;
observe(newValue) //如果用户设置的是一个对象的话 就继续将用户设置的对象劫持 变成响应式的
value = newValue
}
})
}

3.1.1.2.2、在initData中给实例增加一个_data属性指向劫持后的数据,用户可以通过vm._data获取劫持后的数据

3.1.1.2.3、在initData中自己写一个代理(proxy)将_data的数据全部代理到vm上(即通过vm.属性名 就可以获取),原理还是Object.defineProperty

实现 将vm.nam 转化为 vm._data.name

1
2
3
4
5
6
7
8
9
10
11
12
function proxy(vm,source,key){
Object.defineProperty(vm,key,{
get(){
return vm[source][key];
},
set(newValue){
vm[source][key] = newValue
}
})
}

proxy(vm,'_data',key) //vm.nam => vm._data.name

两个Object.defineProperty,一个是为了将数据直接代理到vue实例上,一个是对每一个数据进行取值和设置代理

3.1.1.2.4、再次调用observe对数据进行递归拦截

递归的时候会再次判断数据类型只对对象类型进行观测 非对象类型无法观测直接return

所以 vue2中 数据不要嵌套过深,不然递归浪费性能太多

代码见3.1.1.2.1

3.1.1.2.5、如果给vm设置了一个新的对象类型数据,应该在Object.defineProperty 的set中再次劫持变成响应式的

1
2
3
4
5
//设置了一个新的对象类型数据
vm.obj = {name:'aaa',age:19}
//在 3.1.1.2.1 的set方法中 加入劫持
if(newValue === value) return;
observe(newValue) //如果用户设置的是一个对象的话 就继续将用户设置的对象劫持 变成响应式的

3.1.1.3、观测数据observe(数组类型拦截)

按照3.1.1.2的逻辑,现在的情况是,给数组的每一项都进行了数据劫持,这样是有性能问题的,而且数组的属性可能是各种各样的,比如: 函数、length

而且我们去直接更改数组的某一项用到的也不多

数组不用Object.defineProperty 代理 性能太差,通过监听(改写)数组自身的方法来实现,比如:push、shift、reverse、sort

3.1.1.3.1、在监测数据的时候 对数组和对象分类处理,是对象的话就按照上边的处理

要重写数组自身的方法,增加更新逻辑来监听数组变化,这也是为啥不调用数组的方法直接arr[0]=xx不能被监控的原因

不能直接改写数组的方法,只有被vue控制的数组才需要改写

1
let arrayMethods = Object.create(Array.prototype); // 让arrayMethods 继承于Array.prototype,arrayMethods找不到去Array.prototype找

改写方法时使用 AOP切片编程,做一些操作以后 还是要调用数组原来的方法

比如新增,我们需要只需要在对应方法呗调用的时候去,将新增的数据放到一个数组中(也就是arguements),然后再调用observeArray就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let methods = [
`push`,
`pop`,
`shift`,
`unshift`,
`reverse`,
`sort`
]
methods.forEach(method => { //AOP切片编程
arrayMethods[method] = function(...args){ //重写数组方法
//todu...
let result = oldArrayPrototypeMethods[method].call(this,...args); //做一些操作以后 还是要调用数组原来的方法
return result
}
})

3.1.1.3.2、当是数组时 通过value.__proto__ = arrayMethods挂载重写后的方法

value.__proto__ = arrayMethods等同于Object.setPrototypeOf(value,arrayMethods)

向数组里push对象,或者数组里边直接放对象监控不到

vue中数组中的对象发生了变化也要更新视图(就是数据劫持变为响应式的):观测数组中的每一项observeArray

调用上边的observe 只对对象进行观测

观测的节点有两个:一个是数组中原有的对象,一个是 用户新增的数据是对象

数组中原有的对象,就是通过observeArray循环调用observe

不用判断每一项的数据类型,因为上边的observe函数中已经做了判断

1
2
3
4
5
observeArray(value){
for(let i = 0; i<value.length;i++){
observe(value[i])
}
}

用户新增的数据是对象,只对会增加的方法进行处理

新增有两种:一种是push、unshift这种参数只有增加项的方法,一种是splice这种参数有好项的增加方式

因此需要获取到加入的数据,并通过observeArray拦截

对数据进行拦截的时候是先赋值再通过observeArray拦截

在别的地方拿到实例上的observeArray

我们在Observeconstructor中给观测的数据加上自定义属性__ob__ 指向实例本身(就是this),根据原型链实例身上带有observeArray

value.__ob__ = this;(暂时的,这个写法有问题,下边是原因和解决办法)

因为walk方法 循环了对象的所有属性并对对象属性循环调用Observe,并且value.__ob__ = this;__ob__肯定是对象所以会造成死循环
解决办法是通过Object.defineProperty方法给数据设置__ob__属性并且将__ob__属性设置为不可枚举(表示不能被循环),同时设置为不能删除,这样 walk方法就不能循环到__ob__属性了
1
2
3
4
5
6
7
8
// value.__ob__ = this; 因为`walk方法` 循环了对象的所有属性并对对象属性循环调用Observe,并且`value.__ob__ = this;`中`__ob__`肯定是对象所以会造成死循环
//解决办法是通过Object.defineProperty方法给数据设置`__ob__`属性并且将`__ob__`属性设置为不可枚举的
Object.defineProperty(value,'__ob__',{
value: this,
enumerable: false, //不能被枚举 表示不能被循环
configurable: false, //不能删除此属性

})
最终数组方法改写的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
let oldArrayPrototypeMethods = Array.prototype;
//不能直接改写数组的方法,只有被vue控制的数组才需要改写
export let arrayMethods = Object.create(Array.prototype); // 让arrayMethods 继承于Array.prototype,arrayMethods找不到去Array.prototype找
let methods = [
`push`,
`pop`,
`shift`,
'splice',
`unshift`,
`reverse`,
`sort`
]
methods.forEach(method => { //AOP切片编程
arrayMethods[method] = function(...args){ //重写数组方法
//todu...
console.log('数组改变了')
let result = oldArrayPrototypeMethods[method].call(this,...args); //做一些操作以后 还是要调用数组原来的方法
// 用户新增的数据可能是对象
let inserted; //要保证新增的都是数组
let ob = this.__ob__;
switch(method){
case 'push':
case 'unshift':
inserted = args
break;
case 'splice': //slice(0,1,xxx)
inserted = args.slice(2)
break;
default:

break;
}
// 如果有的话 inserted 中的每一项都调用observeArray
if(inserted) ob.observeArray(inserted)



return result
}
})

如果数据有__ob__属性,证明已经观测过了,不应再次观测

代码 见 3.1.1.2