# 响应式的基本原理

Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式

# vue2.x版本

使用Object.defineProperty 来实现数据响应式

通过定义对象的get与set行为来实现的

# 使用示例

    <h1 id="test1">0</h1>
    <button id="add">add</button>
1
2
let obj = {
    value: 0
}
let v = obj.value;
let test1 = document.getElementById('test1')
Object.defineProperty(obj, 'value', {
    get: function () {
        return v
    },
    set: function (value) {
        v = value
        test1.textContent = value
    }
})
document.querySelector('#add').addEventListener('click', function () {
    obj.value++;
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 封装成watch方法

function watch(obj, key, callback) {
    let v = obj[key]
    Object.defineProperty(obj, key, {
        get: function () {
            return v
        },
        set: function (newValue) {
            v = newValue
            callback(v)
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
12
    <h1 id="test1">0</h1>
    <h1 id="test2">0</h1>
    <button id="add">add</button>
1
2
3
let obj = {
    value1: 0,
    value2: 2
}
let test1 = document.getElementById('test1')
let test2 = document.getElementById('test2')
watch(obj, 'value1', (res) => {
    test1.textContent = res
})
watch(obj, 'value2', (res) => {
    test2.textContent = res
})
document.querySelector('#add').addEventListener('click', function () {
    obj.value1++;
    obj.value2 *= 2;
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 模拟实现observe

将普通对象转化为响应式对象

// 重定义数组的常规方法
const originProto = Array.prototype
const newArrayProto = Object.create(originProto)

    ;['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
        newArrayProto[method] = function () {
            // 先执行本来的操作
            originProto[method].apply(this, arguments)
            // 新数据也变为响应式
            for (const v of arguments) {
                observe(v)
            }
            // 通知更新视图
            notifyUpdate()
        }
    })
/**
 * 将普通对象变为响应式对象
 * @param {object} obj 
 */
function observe(obj) {
    // 如果不是对象直接返回
    if (obj === null || typeof obj !== 'object') {
        return obj
    }

    // 如果是数组替换其原型
    if (Array.isArray(obj)) {
        Object.setPrototypeOf(obj, newArrayProto)
        for (const v of obj) {
            observe(v)
        }
    } else {
        // 定义每个属性的get与set方法
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            const key = keys[i];
            // 对每个key都进行拦截
            defineReactive(obj, key, obj[key])
        }
    }
}

/**
 * 定义指定key的get与set
 * @param {object} obj 
 * @param {string} key 
 * @param {any} val 
 */
function defineReactive(obj, key, val) {
    // 递归遍历
    // val可能也是对象
    observe(val)
    Object.defineProperty(obj, key, {
        get() {
            // 进行依赖搜集
            return val
        },
        set(newVal) {
            // 新的值可能也是对像
            observe(newVal)
            // 通知更新视图
            notifyUpdate()
        }
    })
}

/**
 * 通知视图更新
 */
function notifyUpdate() {
    // ...code
    console.log('更新视图')
    // ...code
}
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

# 测试

// ------testCode-------
const data = {
    name: 'xm',
    info: {
        age: 18,
        id: 23
    },
    children: [
        { name: 'a', age: 18 },
        { name: 'b', age: 20 }
    ]
}
// 变为响应式对象
observe(data)

data.name = 'xxmm' // 更新视图
data.info.age = 20 // 更新视图
data.info = { // 更新视图
    age: 30,
    id: 32
}
data.children[0].name = 'aa' // 更新视图
data.children.push({ name: 'c', age: 17 }) // 更新视图
data.children[2].name = 'cc' // 更新视图

data.children[1] = { // 不更新
    name: 'ccc',
    age: 38
}
data.children[3] = { // 不更新
    name: 'ee',
    age: 33
}
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

# 存在的问题

  1. 需要响应的数据较大时,递归存在性能问题
  2. 新增或者删除属性无法被监听
data.newKey = {
    a:1
}
delete data.name
1
2
3
4
  1. 数组
    1. 响应需要额外的实现
    2. 修改有语法限制,不能通过下标索引直接进行新增与修改

# vue3.x

使用defineProperty只能定义属性的getset等行为

es6提供了proxy(代理),可以自定义更多的行为(13种),可以参考es6相关电子书籍

# 简单使用

 <h1 id="title1">0</h1>
<button id="add">add</button>
1
2
let obj = {
    num: 0
}
let p = new Proxy(obj, {
    set(target, property, value) {
        target[property] = value;
        if (property === 'num') {
            document.getElementById('title1').textContent = value
        }
    }
})

document.getElementById('add').addEventListener('click', function () {
    p.num++;
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 封装成watch方法

function watch(obj, callback) {
    return new Proxy(obj, {
        set(target, key, value) {
            target[key] = value
            callback(key, value)
        },
        get(target, key) {
            return target[key]
        }
    })
}
1
2
3
4
5
6
7
8
9
10
11
<h1 id="title1">0</h1>
<button id="add">add</button>
1
2
let obj = {
    num: 0
}
let p = watch(obj, (key, value) => {
    if (key === 'num') {
        document.getElementById('title1').textContent = value
    }
})
document.getElementById('add').addEventListener('click', function () {
    p.num++;
})
1
2
3
4
5
6
7
8
9
10
11

# 模拟实现reactive

function isObject(obj) {
    return typeof obj === 'object' && obj !== null
}
// 用于缓存
const toProxy = new WeakMap()
const toRaw = new WeakMap()

// 依赖收集:建立target.key和响应函数之间对应关系 
const effectStack = []

// 映射关系表 target->key->[fn1,fn2,...]
const targetsMap = new WeakMap()

/**
 * 依赖搜集
 * @param {object} target 
 * @param {string} key 
 */
function track(target, key) {
    // 从栈中取出响应函数
    const activeEffect = effectStack[effectStack.length - 1]
    if (activeEffect) {
        // 获取target的依赖列表
        let depsMap = targetsMap.get(target)
        if (!depsMap) { // 不存在则创建
            targetsMap.set(target, (depsMap = new Map()))
        }
        // 获取key对应的响应函数列表
        let dep = depsMap.get(key)
        if (!dep) { // 不存在则创建
            depsMap.set(key, (dep = new Set()))
        }
        // 不存在则将新的响应函数加入对应的集合
        if (!dep.has(activeEffect)) {
            dep.add(activeEffect)
        }
    }
}

/**
 * 触发响应函数
 * @param {object} target 
 * @param {string} type 
 * @param {String} key 
 */
function trigger(target, type, key) {
    // 获取依赖列表
    const depsMap = targetsMap.get(target)
    if (depsMap) {
        // 获取响应函数集合
        const deps = depsMap.get(key)
        const effects = new Set()
        if (deps) {
            // 添加所有的响应函数到一个新的集合
            deps.forEach(effect => {
                effects.add(effect)
            })
        }

        // 特殊处理数组元素的新增与删除
        if (type === 'ADD' || type === 'DELETE') {
            if (Array.isArray(target)) {
                const deps = depsMap.get('length')
                if (deps) {
                    deps.forEach(effect => {
                        effects.add(effect)
                    })
                }
            }
        }

        // 执行effects
        effects.forEach(effect => effect())
    }
}

/**
 * 将普通对象转换成响应式对象
 * @param {object} obj 
 */
function reactive(obj) {
    if (!isObject(obj)) {
        return obj
    }
    // 取出缓存
    if (toProxy.has(obj)) {
        return toProxy.get(obj)
    }
    if (toRaw.has(obj)) {
        return obj
    }

    const observed = new Proxy(obj, {
        get(target, key, receiver) {
            const v = Reflect.get(target, key, receiver)
            // console.log(`获取${key}:${target[key]}`);
            // 依赖收集
            track(target, key)
            return isObject(v) ? reactive(v) : v
        },
        set(target, key, value, receiver) {
            const isOwnProperty = target.hasOwnProperty(key)
            const oldVal = target[key]
            const v = Reflect.set(target, key, value, receiver)
            // console.log(`设置${key}:${value}`);

            if (!isOwnProperty) {
                console.log(`新增${key}:${value}`);
                trigger(target, 'ADD', key)
            } else if (oldVal !== value) {
                console.log(`设置${key}:${value}`);
                trigger(target, 'SET', key)
            }
            return v
        },
        deleteProperty(target, key) {
            const isOwnProperty = target.hasOwnProperty(key)
            const v = Reflect.deleteProperty(target, key)
            if (v && isOwnProperty) {
                console.log(`删除${key}属性`);
                trigger(target, 'DELETE', key)
            }
            return v
        }
    })

    // 缓存
    toProxy.set(obj, observed)
    toRaw.set(observed, obj)

    return observed
}

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133

# 模拟effect

/**
 * 模拟effect任务
 * @param {function} fn 
 */
function effect(fn) {
    const wapperEffect = function (...args) {
        return run(wapperEffect, fn, args)
    }
    wapperEffect()

    return wapperEffect
}

/**
 * 执行包装函数
 * @param {function} effect 
 * @param {function} fn 
 * @param {any[]} args 
 */
function run(effect, fn, args) {
    try {
        effectStack.push(effect)
        // 收集依赖
        return fn(...args)
    } finally {
        effectStack.pop()
    }
}
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

# 测试

// --------testReactive-----------
const data = {
    name: 'xm',
    info: {
        age: 18,
        id: 23
    },
    children: [
        { name: 'a', age: 18 },
        { name: 'b', age: 20 }
    ]
}

const rData = reactive(data)
// --------testEffect--------
effect(() => {
    // afterUpdate
    console.log('info age 发生了变化', rData.info.age);
    // ...more code
})

rData.info.age = 100

// -------------testResponsive----------
console.log(rData === reactive(rData));// true

// 更新已存在属性
rData.name = 'xxmm'
rData.children.push({ name: 'c', age: 22 })
rData.children[0].age = 20
rData.info.age = 20
rData.info = { age: 20, id: 999 }
rData.info.id = 0

// 新增不存在的属性
rData.name2 = 'name22222'

// 删除指定的属性
delete rData.name
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