通俗易懂了解Vue双向绑定原理及实现

 看到一篇文章,以为写得挺好的,拿过来给你们分享一下,恰好解答了一些困扰个人一些疑惑!!!html


1. 前言

每当被问到Vue数据双向绑定原理的时候,你们可能都会脱口而出:Vue内部经过Object.defineProperty方法属性拦截的方式,把data对象里每一个数据的读写转化成getter/setter,当数据变化时通知视图更新。虽然一句话把大概原理归纳了,可是其内部的实现方式仍是值得深究的,本文就以通俗易懂的方式剖析Vue内部双向绑定原理的实现过程。vue

2. 思路分析

所谓MVVM数据双向绑定,即主要是:数据变化更新视图,视图变化更新数据。以下图:
node

也就是说:数组

  • 输入框内容变化时,data 中的数据同步变化。即 view => model 的变化。
  • data 中的数据变化时,文本节点的内容同步变化。即 model => view 的变化。

要实现这两个过程,关键点在于数据变化如何更新视图,由于视图变化更新数据咱们能够经过事件监听的方式来实现。因此咱们着重讨论数据变化如何更新视图。缓存

数据变化更新视图的关键点则在于咱们如何知道数据发生了变化,只要知道数据在何时变了,那么问题就变得迎刃而解,咱们只需在数据变化的时候去通知视图更新便可。函数

3. 使数据对象变得“可观测”

数据的每次读和写可以被咱们看的见,即咱们可以知道数据何时被读取了或数据何时被改写了,咱们将其称为数据变的‘可观测’。post

要将数据变的‘可观测’,咱们就要借助前言中提到的Object.defineProperty方法了,关于该方法,MDN上是这么介绍的:性能

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。学习

在本文中,咱们就使用这个方法使数据变得“可观测”。测试

首先,咱们定义一个数据对象car

1 let car = {
2         'brand':'BMW',
3         'price':3000
4     }

 

咱们定义了这个car的品牌brandBMW,价格price是3000。如今咱们能够经过car.brandcar.price直接读写这个car对应的属性值。可是,当这个car的属性被读取或修改时,咱们并不知情。那么应该如何作才可以让car主动告诉咱们,它的属性被修改了呢?

接下来,咱们使用Object.defineProperty()改写上面的例子:

  let car = {}
    let val = 3000
    Object.defineProperty(car, 'price', {
        get(){
            console.log('price属性被读取了')
            return val
        },
        set(newVal){
            console.log('price属性被修改了')
            val = newVal
        }
    })

 

经过Object.defineProperty()方法给car定义了一个price属性,并把这个属性的读和写分别使用get()set()进行拦截,每当该属性进行读或写操做的时候就会出发get()set()。以下图:

能够看到,car已经能够主动告诉咱们它的属性的读写状况了,这也意味着,这个car的数据对象已是“可观测”的了。

为了把car的全部属性都变得可观测,咱们能够编写以下两个函数:

 1 /**
 2      * 把一个对象的每一项都转化成可观测对象
 3      * @param { Object } obj 对象
 4      */
 5     function observable (obj) {
 6         if (!obj || typeof obj !== 'object') {
 7             return;
 8         }
 9         let keys = Object.keys(obj);
10         keys.forEach((key) =>{
11             defineReactive(obj,key,obj[key])
12         })
13         return obj;
14     }
15     /**
16      * 使一个对象转化成可观测对象
17      * @param { Object } obj 对象
18      * @param { String } key 对象的key
19      * @param { Any } val 对象的某个key的值
20      */
21     function defineReactive (obj,key,val) {
22         Object.defineProperty(obj, key, {
23             get(){
24                 console.log(`${key}属性被读取了`);
25                 return val;
26             },
27             set(newVal){
28                 console.log(`${key}属性被修改了`);
29                 val = newVal;
30             }
31         })
32     }

 

如今,咱们就能够这样定义car:

1 let car = observable({
2         'brand':'BMW',
3         'price':3000
4     })

 

car的两个属性都变得可观测了。

4. 依赖收集

完成了数据的'可观测',即咱们知道了数据在何时被读或写了,那么,咱们就能够在数据被读或写的时候通知那些依赖该数据的视图更新了,为了方便,咱们须要先将全部依赖收集起来,一旦数据发生变化,就统一通知更新。其实,这就是典型的“发布订阅者”模式,数据变化为“发布者”,依赖对象为“订阅者”。

如今,咱们须要建立一个依赖收集容器,也就是消息订阅器Dep,用来容纳全部的“订阅者”。订阅器Dep主要负责收集订阅者,而后当数据变化的时候后执行对应订阅者的更新函数。

建立消息订阅器Dep:

 1 class Dep {
 2         constructor(){
 3             this.subs = []
 4         },
 5         //增长订阅者
 6         addSub(sub){
 7             this.subs.push(sub);
 8         },
 9         //判断是否增长订阅者
10         depend () {
11             if (Dep.target) {
12                 this.addSub(Dep.target)
13             }
14         },
15 
16         //通知订阅者更新
17         notify(){
18             this.subs.forEach((sub) =>{
19                 sub.update()
20             })
21         }
22     }
23 Dep.target = null;

 

有了订阅器,再将defineReactive函数进行改造一下,向其植入订阅器:

 1 function defineReactive (obj,key,val) {
 2         let dep = new Dep();
 3         Object.defineProperty(obj, key, {
 4             get(){
 5                 dep.depend();
 6                 console.log(`${key}属性被读取了`);
 7                 return val;
 8             },
 9             set(newVal){
10                 val = newVal;
11                 console.log(`${key}属性被修改了`);
12                 dep.notify()                    //数据变化通知全部订阅者
13             }
14         })
15     }

 

从代码上看,咱们设计了一个订阅器Dep类,该类里面定义了一些属性和方法,这里须要特别注意的是它有一个静态属性 target,这是一个全局惟一 的Watcher,这是一个很是巧妙的设计,由于在同一时间只能有一个全局的 Watcher 被计算,另外它的自身属性 subs 也是 Watcher 的数组。

咱们将订阅器Dep添加订阅者的操做设计在getter里面,这是为了让Watcher初始化时进行触发,所以须要判断是否要添加订阅者。在setter函数里面,若是数据变化,就会去通知全部订阅者,订阅者们就会去执行对应的更新的函数。

到此,订阅器Dep设计完毕,接下来,咱们设计订阅者Watcher.

5. 订阅者Watcher

订阅者Watcher在初始化的时候须要将本身添加进订阅器Dep中,那该如何添加呢?咱们已经知道监听器Observer是在get函数执行了添加订阅者Wather的操做的,因此咱们只要在订阅者Watcher初始化的时候出发对应的get函数去执行添加订阅者操做便可,那要如何触发get的函数,再简单不过了,只要获取对应的属性值就能够触发了,核心缘由就是由于咱们使用了Object.defineProperty( )进行数据监听。这里还有一个细节点须要处理,咱们只要在订阅者Watcher初始化的时候才须要添加订阅者,因此须要作一个判断操做,所以能够在订阅器上作一下手脚:在Dep.target上缓存下订阅者,添加成功后再将其去掉就能够了。订阅者Watcher的实现以下:

 1   class Watcher {
 2         constructor(vm,exp,cb){
 3             this.vm = vm;
 4             this.exp = exp;
 5             this.cb = cb;
 6             this.value = this.get();  // 将本身添加到订阅器的操做
 7         },
 8 
 9         update(){
10             let value = this.vm.data[this.exp];
11             let oldVal = this.value;
12             if (value !== oldVal) {
13                 this.value = value;
14                 this.cb.call(this.vm, value, oldVal);
15             },
16         get(){
17             Dep.target = this;  // 缓存本身
18             let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
19             Dep.target = null;  // 释放本身
20             return value;
21         }
22     }

 

过程分析:

订阅者Watcher 是一个 类,在它的构造函数中,定义了一些属性:

  • vm:一个Vue的实例对象;
  • exp:node节点的v-modelv-on:click等指令的属性值。如v-model="name"exp就是name;
  • cb:Watcher绑定的更新函数;

当咱们去实例化一个渲染 watcher 的时候,首先进入 watcher 的构造函数逻辑,就会执行它的 this.get() 方法,进入 get 函数,首先会执行:

Dep.target = this;  // 缓存本身

 

实际上就是把 Dep.target 赋值为当前的渲染 watcher ,接着又执行了:

let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数

 

在这个过程当中会对 vm 上的数据访问,其实就是为了触发数据对象的getter

每一个对象值的 getter都持有一个 dep,在触发 getter 的时候会调用 dep.depend() 方法,也就会执行this.addSub(Dep.target),即把当前的 watcher 订阅到这个数据持有的 depsubs 中,这个目的是为后续数据变化时候能通知到哪些 subs 作准备。

这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并无,完成依赖收集后,还须要把 Dep.target 恢复成上一个状态,即:

Dep.target = null;  // 释放本身

 

由于当前vm的数据依赖收集已经完成,那么对应的渲染Dep.target 也须要改变。

update()函数是用来当数据发生变化时调用Watcher自身的更新函数进行更新的操做。先经过let value = this.vm.data[this.exp];获取到最新的数据,而后将其与以前get()得到的旧数据进行比较,若是不同,则调用更新函数cb进行更新。

至此,简单的订阅者Watcher设计完毕。

6. 测试

完成以上工做后,咱们就能够来真正的测试了。

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <h1 id="name"></h1>
    <input type="text">
    <input type="button" value="改变data内容" onclick="changeInput()">
    
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script>
    function myVue (data, el, exp) {
        this.data = data;
        observable(data);                      //将数据变的可观测
        el.innerHTML = this.data[exp];           // 初始化模板数据的值
        new Watcher(this, exp, function (value) {
            el.innerHTML = value;
        });
        return this;
    }

    var ele = document.querySelector('#name');
    var input = document.querySelector('input');
    
    var myVue = new myVue({
        name: 'hello world'
    }, ele, 'name');
    
    //改变输入框内容
    input.oninput = function (e) {
        myVue.data.name = e.target.value
    }
    //改变data内容
    function changeInput(){
        myVue.data.name = "难凉热血"
    
    }
</script>
</body>
</html>
observer.js

    /**
     * 把一个对象的每一项都转化成可观测对象
     * @param { Object } obj 对象
     */
    function observable (obj) {
        if (!obj || typeof obj !== 'object') {
            return;
        }
        let keys = Object.keys(obj);
        keys.forEach((key) =>{
            defineReactive(obj,key,obj[key])
        })
        return obj;
    }
    /**
     * 使一个对象转化成可观测对象
     * @param { Object } obj 对象
     * @param { String } key 对象的key
     * @param { Any } val 对象的某个key的值
     */
    function defineReactive (obj,key,val) {
        let dep = new Dep();
        Object.defineProperty(obj, key, {
            get(){
                dep.depend();
                console.log(`${key}属性被读取了`);
                return val;
            },
            set(newVal){
                val = newVal;
                console.log(`${key}属性被修改了`);
                dep.notify()                    //数据变化通知全部订阅者
            }
        })
    }
    class Dep {
        
        constructor(){
            this.subs = []
        }
        //增长订阅者
        addSub(sub){
            this.subs.push(sub);
        }
        //判断是否增长订阅者
        depend () {
            if (Dep.target) {
                this.addSub(Dep.target)
            }
        }

        //通知订阅者更新
        notify(){
            this.subs.forEach((sub) =>{
                sub.update()
            })
        }
        
    }
    Dep.target = null;
watcher.js

    class Watcher {
        constructor(vm,exp,cb){
            this.vm = vm;
            this.exp = exp;
            this.cb = cb;
            this.value = this.get();  // 将本身添加到订阅器的操做
        }
        get(){
            Dep.target = this;  // 缓存本身
            let value = this.vm.data[this.exp]  // 强制执行监听器里的get函数
            Dep.target = null;  // 释放本身
            return value;
        }
        update(){
            let value = this.vm.data[this.exp];
            let oldVal = this.value;
            if (value !== oldVal) {
                this.value = value;
                this.cb.call(this.vm, value, oldVal);
            }
    }
}

 

效果:

vue数据双向绑定原理及实现

7. 总结

总结一下:

实现数据的双向绑定,首先要对数据进行劫持监听,因此咱们须要设置一个监听器Observer,用来监听全部属性。若是属性发上变化了,就须要告诉订阅者Watcher看是否须要更新。由于订阅者是有不少个,因此咱们须要有一个消息订阅器Dep来专门收集这些订阅者,而后在监听器Observer和订阅者Watcher之间进行统一管理的。

(完)




免责声明

  • 本博客全部文章仅用于学习、研究和交流目的,欢迎非商业性质转载。
  • 博主在此发文(包括但不限于汉字、拼音、拉丁字母)均为随意敲击键盘所出,用于检验本人电脑键盘录入、屏幕显示的机械、光电性能,并不表明本人局部或所有赞成、支持或者反对观点。如须要详查请直接与键盘生产厂商法人表明联系。挖井挑水无水表,不会网购无快递。
  • 博主的文章没有高度、深度和广度,只是凑字数。因为博主的水平不高(实际上是个菜B),不足和错误之处在所不免,但愿你们可以批评指出。
  • 博主是利用读书、参考、引用、抄袭、复制和粘贴等多种方式打形成本身的文章,请原谅博主成为一个无耻的文档搬运工!

 

参考资料:http://www.javashuo.com/article/p-vbvongpy-ea.html

相关文章
相关标签/搜索