vue源码手写实现(二):实现基本的响应式

  1. 1. 项目入口和初始化
  2. 2. 处理data数据
  3. 3. 实现基本响应式
  4. 4. 数组响应式的特殊处理

项目入口和初始化

我们知道,Vue虽然复杂,但本质上仍旧是一个函数。所以第一步,我们先创建一个Vue函数。先在根目录下创建index.js,在里边创建Vue函数。

然后,我们需要获取到传递给Vue函数的参数,本质是一个对象,我们命名为options,包含Vue的如component、data、template等一系列配置。这个操作也应当抽离(init.js),再在主函数中调用。

因为这个配置比较常用,所以将它挂在到Vue的实例下。我们知道Vue实例上的属性函数等都会带上$符号,此处亦然。

1
2
3
4
5
6
7
8
9
10
11
// index.js
import { initMixin } from './init'

function Vue(options){// options是用户的选项
this._init(options)
}

initMixin(Vue);// 扩展了init方法
initLifecycle(Vue)// 扩展了生命周期方法

export default Vue
1
2
3
4
5
6
7
8
9
10
11
// init.js
import { initState } from './state'

export function initMixin(Vue){// 给Vue增加init方法
Vue.prototype._init = function(options){
// 初始化操作
const vm = this;
vm.$options = options;// 将用户的选项挂载到实例上

}
}

处理data数据

目前我们已经把data挂载到Vue实例上。不过我们平时使用data里的数据都是this.a进行访问。此处this即为Vue实例,即直接使用Vue实例进行访问data中的数据。那么我们需要将data做一个备份,将这个备份代理到vm上,即可直接访问。

同样这个功能应当抽出为一个独立js文件(state.js)。

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
41
42
43
44
// state.js

export function initState(vm){
const opts = vm.$options; // 获取用户的选项

// if(opts.props){
// initProps(vm, opts.props);
// }

if(opts.data){
initData(vm);
}
}

function proxy(vm,target,key){// 让data数据能够直接从实例上获取
Object.defineProperty(vm,key,{
get(){
return vm[target][key];
},
set(newVal){
vm[target][key] = newVal;
}
});
}

function initData(vm){
let data = vm.$options.data;

/**
* 这里源代码是直接使用data.call(vm)
* 我实现时若data为function执行之后仍旧是function,所以这里将data覆盖,以此得到data的返回值
*/
typeof data === 'function'? data = data.call(vm) : data;

vm._data = data;// 无论data是function还是object,都要处理成object绑定到vm实例上
// 劫持数据,vue2采用 defineProperty
observe(data);


// 将vm._data用vm来代理就可以了
for(let key in data){
proxy(vm,'_data',key);
}
}

这里写的initState方法会在_init中调用,执行结果即将 _data代理到vm上。

实现基本响应式

响应式逻辑比较核心,后续源码实现多处也会再次调用此处,所以抽为一个模块:新建observe文件夹,里边新建index.js。

vue2响应式主要依赖Object.definePoperty,监测对象的set和get方法,进行重新渲染来实现。

这里我们首先将data中的每个属性挂上set和get方法。

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// oberserve/index.js

import Dep from './dep';

class Observer {
constructor(data) {
// Object.deineProperty只能劫持已经存在的属性(vue会为此单独写一些api $set $delete)

/**
* 给数据加上一个标识,如果数据上有__ob__属性,则说明已经被劫持过了
* 但是在对象中增加__ob__后进入walk方法,会对__ob__进行监测,里边又会有walk导致进入死循环
* 所以设置__ob__为一个隐藏属性,不可被枚举
*/

data.__ob__ = this;
Object.defineProperties({
value: this,
enumerables: false,// 设置__ob__为一个隐藏属性,循环时不会被获取
})
if(Array.isArray(data)){
// 对数组进行劫持:重写数组中的方法
data.__proto__ = newArrayProto;// 重写数组原型
this.observeArray(data);// 如果数组中监测的是对象,可以监控到对象的变化
}else{
this.walk(data);// 循环对象,对属性依次劫持
}
}

walk(data) {// 循环对象,对属性依次劫持

// 重新定义属性
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
});
}

/**
* 监测数组
*
*/
observeArray(data){
data.forEach(item => {
observe(item);
})
}
}

//深层次嵌套会递归,递归多了性能差,不存在属性监控不到,存在的属性要重写方法vue3-> proxy
function dependArray(value){
for(let i=0;i<value.length;i++){
let current = value[i];
current?.dep.depend();
if(Array.isArray(current)){
dependArray(current);
}
}
}


export function defineReactive(target, key, value) {// 闭包 属性劫持
let childob = observe(key);// 如果仍旧是一个对象,则递归劫持 childOb.dep用来收集依赖

let dep = new Dep();
Object.defineProperty(target, key, {
get() {// 取值的时候会执行get
if(Dep.target){
dep.depend();// 让这个属性的收集器记住当前的watcher
if(childob){
childob.dep.depend();// 让数组和对象本身也能实现依赖收集
if(Array.isArray(value)){
dependArray(value);
}
}
}
return value;
},
set(newValue) {// 赋值的时候会执行set
if(newValue === value) return;
observe(newValue);
value = newValue;
dep.notify();
},
})
}

export function observe(data) {
// 对这个对象进行劫持

if (typeof data!== 'object' || data === null) {
return;// 只对对象进行劫持
}

// 如果一个对象被劫持过,则不再劫持
// 判断一个对象是否被接触过,可以增添一个实例,用实例来判断是否被劫持过
if (data.__ob__ instanceof Observer) {
return data.__ob__;
}

return new Observer(data);
}

数组响应式的特殊处理

通过循环对象绑定set和get对数组的某些情况无法处理到。比如数组的push、unshift等钩子,为数组增加的元素不会被绑定到,所以我们需要重写数组的方法,单独给数组内新增的元素绑定响应式。

在observe下新建array.js。

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
41
42
43
44
45
46
47
48
49
50
51
52
// obeserve/array.js

/**
* 重写数组中的部分方法
*/

// 获取数组的原型
let oldArrayProto = Array.prototype

let newArrayProto = Object.create(oldArrayProto)

let methods = [ // 找到所有变异方法
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse',
]// concat slice 都不会改变原数组,所以不需要重写

methods.forEach(method => {
newArrayProto[method] = function (...args) {// 重写方法
// 内部调用原方法
const result = oldArrayProto[method].call(this,...args)

//对新增的数据再次劫持
let inserted;
let ob = this.__ob__;
switch (method) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = args.slice(2);
break;
case'sort':
case'reverse':
default:
break;
}

if (inserted) {
ob.observeArray(inserted)
}

return result
}
})

export default newArrayProto