Vue响应式原理

# Vue响应式原理

[TOC]

# 一、Vue的数据响应

<div id="app" @click="changeMsg">
  {{ message }}
</div>
1
2
3
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  },
  methods: {
    changeMsg() {
      this.message = 'Hello World!'
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11

# 1.1 核心

  • 利用 Object.defineProperty 给数据添加了 getter 和 setter,目的就是为了在我们访问数据以及写数据的时候能自动执行一些逻辑。
  • getter 做的事情是依赖收集,setter 做的事情是派发更新。

# 1.1.1 Object.defineProperty

  • 直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。
    • obj:定义属性的对象。
    • prop: 定义或修改属性的名称。
    • descriptor:被定义或修改的属性描述符。
Object.defineProperty(obj, prop, descriptor)

// 定义多个属性
Object.defineProperties()
1
2
3
4

# 1.1.2 getter && setter

let obj = {name: 'lin'};
let value = obj.name;	// 避免 obj.name = newValue 会死循环
Object.defineProperty(obj, "name", {
    get() {
        console.log('get');
        return value;
    },
    set(newValue) {
        console.log('set');
        value = newValue;
    }
});

obj.name	// get ‘lin'
obj.name = 'guli'	// set 'guli'
obj.name	// get ‘guli'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 1.1.3 发布订阅模式

# 二、具体实现

mvvm

# 2.1 渲染数据

class Kvue {
    constructor(options) {
        // 使用$和标记,避免被vm.options覆盖
        this.$options = options;
        this._data = options.data;
        this.compile(options.el);
    }
    compile(el) {
        let element = document.querySelector(el);
        this.compileNode(element);
    }
    compileNode(element) {
        // 获取el的子节点列表(数组)
        let childNodes = element.childNodes;
        childNodes.forEach(node => {
            // 要对"{{}}"进行转义处理
            let reg = /\{\{\s*(\S*)\s*\}\}/;
            if (reg.test(node.textContent)) {
                // 这里的 RegExp 即为reg.test(node.textContent)匹配的结果
                let nodeContent = this._data[RegExp.$1];
                node.textContent = node.textContent.replace(reg, nodeContent);
            }
        });
    }
}
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
  • 测试
<body>
    <div id="app">
        {{message}}
        <p>{{message}}</p>
    </div>
</body>
<script>
    let vm = new Kvue({
        el: "#app",
        data: {
            message: "测试数据"
        }
    })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 2.2 更新视图

实现:更改vm._data.message时,视图也会发生变化。

# 2.2.1 数据劫持

class Kvue {
    constructor(options) {
        this.$options = options;
        this._data = options.data;
        this.observer(this._data);
    }
    observer(data) {
        Object.keys(data).forEach(key => {
            let value = data[key];
            Object.defineProperty(data, key, {
                configurable: true,
                enumerable: true,
                get() {
                    return value;
                },
                set(newValue) {
                    value = newValue;
                }
            })
        });
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2.2.2 发布订阅

  • 给data的每个key绑定一个发布者,在需要订阅的地方绑定一个订阅者。

  • 在get的时候登记订阅者(依赖收集),在set的时候发布更新(派发更新)。

  • 发布者要通知到更新即可,更新后要做的事放在订阅者的回调函数里。

class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(sub) {
        this.subs.push(sub);
    }
    // 只负责发布
    notify(newValue) {
        
        this.subs.forEach(sub => {
            sub.update(newValue);
        })
    }
}

class Watcher {
    constructor(vm, exp, cb) {
        // 将当前的Dep.target指向自己
        Dep.target = this;
        // 触发get()
        vm._data[exp];
        this.cb = cb;
        // 避免被重复登记
        Dep.target = null;
    }
    update(newValue) {
        this.cb(newValue);
    }
}
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
observer(data) {
    Object.keys(data).forEach(key => {
        let value = data[key];
        // 
        
        let dep = new Dep();
        Object.defineProperty(data, key, {
            configurable: true,
            enumerable: true,
            get() {
                // 登记订阅者
                if (Dep.target) {
                    dep.addSub(Dep.target);
                }
                return value;
            },
            set(newValue) {
                if (newValue !== value) {
                    value = newValue;
                }
                // 发布通知
                dep.notify(newValue);
            }
        })
    });
}

compileNode(element) {
    // 获取el的子节点列表
    let childNodes = element.childNodes;
    childNodes.forEach(node => {
        let reg = /\{\{\s*(\S*)\s*\}\}/;
        if (reg.test(node.textContent)) {
            // 这里的 RegExp 即为reg.test(node.textContent)匹配的结果
            // let nodeContent = this._data[RegExp.$1];
            // node.textContent = node.textContent.replace(reg, nodeContent);
            new Watcher(this, RegExp.$1, newValue => node.textContent = newValue);
        }
    });
}
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

# 2.3 v-model模拟

<input type="text" k-model="message">
{{message}}
1
2
compileNode(element) {
    let childNodes = element.childNodes;
    childNodes.forEach(node => {
        // 元素节点:element-1
        // 属性节点:attr-2
        // 文本节点:text-3
        if (node.nodeType === 3) {
            // 要对"{{}}"进行转义处理
            let reg = /\{\{\s*(\S*)\s*\}\}/;
            if (reg.test(node.textContent)) {
                new Watcher(this, RegExp.$1, newValue => node.textContent = newValue);
            }
        }else if(node.nodeType===1){
            // 获取节点的属性对象
            let attrs = node.attributes;
            Array.from(attrs).forEach(attr=>{
                let attrName = attr.name;
                let attrValue = attr.value;
                if(attrName.indexOf('k-')===0){
                    attrName = attrName.substr(2);
                    if(attrName === 'model') {
                        node.value = this._data[attrValue];
                    }
                    node.addEventListener('input',e=>{
                        this._data[attrValue]=e.target.value;
                    });
                    new Watcher(this,attrValue,newValue=>{
                        node.value = newValue;
                    });
                }
            })
        }
    });
}
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

# 三、完整代码

class Kvue {
    constructor(options) {
        this.$options = options;
        this._data = options.data;
        this.observer(this._data);
        this.compile(options.el);
    }
    observer(data) {
        Object.keys(data).forEach(key => {
            let value = data[key];
            let dep = new Dep();
            Object.defineProperty(data, key, {
                configurable: true,
                enumerable: true,
                get() {
                    if (Dep.target) {
                        dep.addSub(Dep.target);
                    }
                    return value;
                },
                set(newValue) {
                    if (newValue !== value) {
                        value = newValue;
                    }
                    dep.notify(newValue);
                }
            })
        });
    }
    compile(el) {
        let element = document.querySelector(el);
        this.compileNode(element);
    }
    compileNode(element) {
        let childNodes = element.childNodes;
        childNodes.forEach(node => {
            if (node.nodeType === 3) 
                let reg = /\{\{\s*(\S*)\s*\}\}/;
                if (reg.test(node.textContent)) {  
                    new Watcher(this, RegExp.$1, newValue => node.textContent = newValue);
                }
            }else if(node.nodeType===1){
                let attrs = node.attributes;
                Array.from(attrs).forEach(attr=>{
                    let attrName = attr.name;
                    let attrValue = attr.value;
                    if(attrName.indexOf('k-')===0){
                        attrName = attrName.substr(2);
                        if(attrName === 'model') {
                            node.value = this._data[attrValue];
                        }
                        node.addEventListener('input',e=>{
                            this._data[attrValue]=e.target.value;
                        });
                        new Watcher(this,attrValue,newValue=>{
                            node.value = newValue;
                        });
                    }
                })
            }
        });
    }
}


class Dep {
    constructor() {
        this.subs = [];
    }
    addSub(sub) {
        this.subs.push(sub);
    }
    notify(newValue) {
        this.subs.forEach(sub => {
            sub.update(newValue);
        })
    }
}

class Watcher {
    constructor(vm, exp, cb) {
        Dep.target = this;
        vm._data[exp];
        this.cb = cb;
        Dep.target = null;
    }
    update(newValue) {
        this.cb(newValue);
    }
}
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

# 四、Proxy

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

# 4.1 Object.defineProperty() 的问题

  • 不能监听数组的变化
  • 必须遍历对象的每个属性
  • 必须深层遍历嵌套的对象

# 4.2 Proxy

Proxy 在 ES2015 规范中被正式加入,它有以下几个特点:

  • 针对对象:针对整个对象,而不是对象的某个属性,故无须遍历对象的每个属性。这解决了上述 Object.defineProperty() 第二个问题。
  • 支持数组:Proxy 不需要对数组的方法进行重载,省去了众多 hack,减少代码量等于减少了维护成本,而且标准的就是最好的。
  • Proxy 的第二个参数可以有 13 种拦截方法,这比起 Object.defineProperty() 要更加丰富,不仅仅是只有getter和setter。
  • 可以监听对象属性的增删。
  • 惰性监听,不会在初始化的时候创建所有的观察者,而是在用到的时候才去监听。
let onWatch = (obj, setBind, getLogger) => {
    let handler = {
        get(target, property, receiver) {
            getLogger(target, property)
            return Reflect.get(target, property, receiver)
        },
        set(target, property, value, receiver) {
            setBind(value, property)
            return Reflect.set(target, property, value)
        }
    }
    return new Proxy(obj, handler)
}

let obj = { a: 1 }
let p = onWatch(
    obj,
    (v, property) => {
        console.log(`监听到属性${property}改变为${v}`)
    },
    (target, property) => {
        console.log(`'${property}' = ${target[property]}`)
    }
)
p.a = 2 // 监听到属性a改变
p.a 	// 'a' = 2
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

# 五、MVVM 与 MVC

参考教程:MVC,MVP 和 MVVM 的图示 (opens new window)

# 5.1 MVC

  • 模型Model - 视图 View - 控制器Controller 的缩写。
    • Model:模型持有所有的数据、状态和程序逻辑。(数据保存)
    • View:负责界面的布局和显示。(用户界面)
    • Controller:负责模型和界面之间的交互。(业务逻辑)
  • 实现业务逻辑和界面的解耦。

mvc

  • 所有的通信都是单向的。

    1. View 传送指令到 Controller。

    2. Controller 完成业务逻辑后,要求 Model 改变状态。

    3. Model 将新的数据发送到 View,用户得到反馈。

# 5.2 MVVM

  • Model(数据层) - View(视图层) - ViewModel(业务逻辑层)的缩写。
    • ViewModel:为View提供数据并且响应View的操作。

mvvm

  • 利用双向数据绑定技术,解决了数据频繁更新的问题。
    • Model 变化时,ViewModel 会自动更新,而ViewModel变化时,View也会自动更新。