Vue双向绑定的实现原理系列(三):监听器Observer和订阅者Watcher

监听器Observer和订阅者Watcher

实现简单版Vue的过程,主要实现{{}}、v-model和事件指令的功能

主要分为三个部分

github源码

 1.数据监听器Observer,能够对数据对象的所有属性进行监听;
    实现数据的双向绑定,首先要对数据进行劫持监听,所以我们需要设置一个监听器Observer,用来监听所有属性

 2.Watcher将数据监听器和指令解析器连接起来,数据的属性变动时,执行指令绑定的相应回调函数,
    1.如果属性发上变化了,就需要告诉订阅者Watcher看是否需要更新。

 3.指令解析器Compile,
    对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher

 因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。=

监听器Observer

Observer是一个数据监听器,核心是前面一直谈论的Object.defineProperty(),
对所有属性监听,利用递归来遍历所有的属性值,对其进行Object.defineProperty()操作:

    function definReactive(data,key,val){
        observers(val);//递归所有子属性

        Object.defineProperty(data,key,{
            enumerable:true,
            configurable:true,
            get:function(){
                console.log(‘属性‘+key+‘执行get‘);
                return val;
            },
            set:function(newVal){
                val = newVal;
                console.log(‘属性:‘+key+‘以及被监听,现在值为:‘+newVal.toString());
            }
        })
    }

    function observers(data){
        if(!data || typeof data!=‘object‘){
            return;
        }
        Object.keys(data).forEach(function(key){
            definReactive(data,key,data[key]);
        })
    }
    var library = {
        book1:{name:‘‘},
        book2:‘‘
    }
    observers(library);

    library.book1.name = ‘vue书籍‘;
    library.book2 = ‘没有书‘;
    //属性book1执行get
    //属性:name以及被监听,现在值为:vue书籍
    //属性:book2以及被监听,现在值为:没有书

接下来创建一个收集所有订阅者的订阅器Dep,阅器Dep主要负责收集订阅者,然后再属性变化的时候执行对应订阅者的更新函数,
再改写一下订阅器Observer,创建一个observer.js:

    function Observe(data){
        this.data = data;
        this.walk(data);
    }
    Observe.prototype = {
        walk:function(data){
            var self = this;
            Object.keys(data).forEach(function(key) {
                self.defineReactive(data, key, data[key]);
            });
        },
        defineReactive:function(data,key,val){
            observers(val);//递归所有子属性
            var dep = new Dep();

            Object.defineProperty(data,key,{
                enumerable:true,
                configurable:true,
                get:function(){
                    if(是否需要添加订阅者){
                        dep.addSub(Watcher);//在这里添加一个订阅者
                    }
                    console.log(‘属性‘+key+‘执行get‘);
                    return val;
                },
                set:function(newVal){
                    if(val === newVal){
                        return;
                    }
                    val = newVal;
                    dep.notify();//如果数据变化,通知所有订阅者
                    console.log(‘属性:‘+key+‘以及被监听,现在值为:‘+newVal.toString());
                }
            })
        }
    }
    function observers(data){
        if(!data || typeof data!=‘object‘){
            return;
        }
        return new Observe(data);
    }

    /**Dep:创建一个可以容纳订阅者的消息订阅器
     * **/

    function Dep(){
        this.subs = [];
    }
    Dep.prototype = {
        addSub:function(sub){//添加订阅者
            this.subs.push(sub);
        },
        notify:function(){//通知订阅者
            this.subs.forEach(function(sub){
                sub.update();
            })
        }
    }

    可以看出,订阅器Dep,添加一个订阅者是在Object.defineProperty()的get里面,这是为了让Watcher初始化进行触发,
    因此要判断是不是需要添加订阅者,后面解释。在set里面,如果数据变化,就会通知所有的订阅者,订阅者就会去执行对应的更新的函数
    以上,一个完整的订阅器完成。

订阅者Watcher

Watcher在初始化的时候要将自己添加进订阅者Dep中,如何做到:

已经知道监听器Observer是在get函数执行了添加订阅者Wather的操作的,

所以我们只要在订阅者Watcher初始化的时候触发对应的get函数,去执行添加订阅者操作即可,

那要如何触发get的函数:

只要获取对应的属性值就可以触发了,核心原因就是因为我们使用了Object.defineProperty()进行数据监听。

注意:

我们只要在订阅者Watcher初始化的时候才需要添加订阅者,所以需要做一个判断操作,

因此可以在订阅器上做一下手脚:在Dep.target上缓存下订阅者,添加成功后再将其去掉就可以了。

创建一个watcher.js

    function Watcher(vm,exp,cb){
        this.cb = cb;
        this.vm = vm;
        this.exp = exp;
        this.value = this.get();//将自己添加到订阅器的操作
    }
    Watcher.prototype = {
        update:function () {
            this.run();
        },
        run:function () {
            var value = this.vm.data[this.exp];
            var oldVal = this.value;
            if(value != oldVal){
                this.value = value;
                this.cb.call(this.vm,value,oldVal);
            }
        },
        get:function () {
            Dep.target = this;//缓存自己
            var value = this.vm.data[this.exp];//强制执行监听器observer里的Object.defineProperty()里的get函数
            Dep.target = null;//释放自己
            return value;
        }
    }

再调整下observer.js的defineReactive函数里的get操作:

    defineReactive:function(data,key,val){
        observers(val);//递归所有子属性
        var dep = new Dep();

        Object.defineProperty(data,key,{
            enumerable:true,
            configurable:true,
            get:function(){
                if(Dep.target){
                    dep.addSub(Dep.target);//在这里添加一个订阅者
                }
                console.log(‘属性‘+key+‘执行get‘);
                return val;
            },
            set:function(newVal){
                if(val === newVal){
                    return;
                }
                val = newVal;
                dep.notify();//如果数据变化,通知所有订阅者
                console.log(‘属性:‘+key+‘以及被监听,现在值为:‘+newVal.toString());
            }
        })
    }
    //Dep加个target属性
    function Dep(){
        this.subs = [];
        this.target = null;
    }

简单版的Watcher设计好了,
只要将Observer和Watcher关联起来,就可以实现一个简单的双向绑定数据了。
这里没有还没有设计解析器Compile,所以对于模板数据我们都进行写死处理:
模板有个节点,id为name,双向数据绑定的变量name,这里大框号暂时没有用:

<body>
    <h1 id="name">{{name}}</h1>
</body>

selVue.js 关联Observer和Watcher

    function SelfVue(data,el,exp){
        this.data = data;
        observers(data);
        el.innerHTML = this.data[exp];//初始化模板数据的值

        new Watcher(this,exp,function(value){
            el.innerHTML = value;
        });
        return this;
    }

页面上实现双向数据绑定:

<h1 id="name">{{name}}</h1>
    <script src="js/observer.js"></script>
    <script src="js/watcher.js"></script>
    <script src="js/selfVue.js"></script>
    <script>
        var ele = document.querySelector(‘#name‘);
        var self_Vue = new SelfVue({
            name:‘第一次显示数据‘
        },ele,‘name‘);

        window.setTimeout(function(){
            console.log(‘值变了‘);
            self_Vue.data.name = ‘重新赋值了‘;
        },2000);
    </script>

打开页面,可以看到页面刚开始显示了是“第一次显示数据”,过了2s后就变成“重新赋值了”了。到这里,完成了一部分

注意:赋值的时候是 self_Vue.data.name = ‘重新赋值了‘,但是希望是 self_Vue.name = ‘重新赋值了‘,
需要在new SelVue的时候做个代理,让访问self_Vue的属性代理为访问self_Vue.data的属性,
实现原理还是使用Object.defineProperty()对属性再包一层,

修改selVue.js:

    function SelfVue(data,el,exp){
        var self = this;
        this.data = data;

        Object.keys(data).forEach(function (key) {
            self.proxyKeys(key);//绑定代理属性
        });

        observers(data);
        el.innerHTML = this.data[exp];//初始化模板数据的值
        new Watcher(this,exp,function(value){
            el.innerHTML = value;
        });
        return this;
    }

    SelfVue.prototype = {
        proxyKeys:function(key){
            var self = this;
            Object.defineProperty(this,key,{
                enumerable:false,
                configurable:true,
                get:function proxyGetter(){
                    return self.data[key];
                },
                set:function proxySetter(newVal){
                    self.data[key] = newVal;
                }
            })
        }
    }

    //这下我们就可以直接通过self_Vue.name = ‘重新赋值了‘的形式来进行改变模板数据。

至此监听器Observer和订阅者Watcher功能基本完成,后面再加上指令解析器compile的功能!

系列文章的目录:

Vue双向绑定的实现原理系列(一):Object.defineproperty
Vue双向绑定的实现原理系列(二):设计模式
Vue双向绑定的实现原理系列(三):监听器Observer和订阅者Watcher
Vue双向绑定的实现原理系列(四):补充指令解析器compile

原文地址:https://www.cnblogs.com/baimeishaoxia/p/12034577.html

时间: 2024-08-28 00:01:06

Vue双向绑定的实现原理系列(三):监听器Observer和订阅者Watcher的相关文章

Vue双向绑定的实现原理系列(四):补充指令解析器compile

补充指令解析器compile github源码 补充下HTML节点类型的知识: 元素节点 Node.ELEMENT_NODE(1) 属性节点 Node.ATTRIBUTE_NODE(2) 文本节点 Node.TEXT_NODE(3) CDATA节点 Node.CDATA_SECTION_NODE(4) 实体引用名称节点 Node.ENTRY_REFERENCE_NODE(5) 实体名称节点 Node.ENTITY_NODE(6) 处理指令节点 Node.PROCESSING_INSTRUCTIO

梳理vue双向绑定的实现原理

Vue 采用数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,通过Object.defineProperty来劫持数据的setter,getter,在数据变动时发布消息给订阅者,订阅者收到消息后进行相应的处理. 要实现mvvm的双向绑定,就必须要实现以下几点: Compile-指令解析系统,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数 Observer-数据监听系统,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者 Dep+Watche

vue双向绑定(数据劫持+发布者-订阅者模式)

参考文献:https://www.cnblogs.com/libin-1/p/6893712.html 实现mvvm主要包含两个方面,数据变化更新视图,视图变化更新数据. 关键点在于data如何更新view,因为view更新data其实可以通过事件监听即可.我们着重来分析,当数据改变,如何更新视图的. 如何知道数据变了,就是通过Object.defineProperty( )对属性设置一个set函数,当数据改变了就会来触发这个函数,所以我们只要将一些需要更新的方法放在这里面,就可以实现data更

[Vue源码]一起来学Vue双向绑定原理-数据劫持和发布订阅

有一段时间没有更新技术博文了,因为这段时间埋下头来看Vue源码了.本文我们一起通过学习双向绑定原理来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vue模板编译原理(一)-Template生成AST 一起来学Vue模板编译原理(二)-AST生成Render字符串 一起来学Vue虚拟DOM解析-Virtual Dom实现和Dom-diff算法 这些文章统一放在我的git仓库:https://github.com/yzsun

vue双向绑定原理

vue双向绑定原理的核心 它的实现的核心是通过Object.defineProperty(),对data的每个属性进行了get.set的拦截. 其实只要Object.defineProperty()已经可以实现双向绑定,只是这样做效率非常低. 观察者模式 它在双向绑定当中是什么角色呢? 它其实是让双向绑定更有效率 为什么? 观察者模式,它是一对多的一种模式,在vue里面,"一"是改了某一data数据,"多"是页面上凡是用了这个数据的地方,都更新.这就是页面上的很多&

vue双向绑定原理源码解析

当我们学习angular或者vue的时候,其双向绑定为我们开发带来了诸多便捷,今天我们就来分析一下vue双向绑定的原理. 简易vue源码地址:https://github.com/maxlove123/simple-Vue.git 1.vue双向绑定原理 vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调.我们先来看Object.definePr

IOS自带输入法中文不触发KEYUP事件导致vue双向绑定错误问题

先上图: 可以看到输入框中的内容和弹出框的内容不一致, <input class="am-fr labRight" id="txcode" type="text" placeholder="请输入纳税人识别号" v-model="invBuyer.TaxCode" /> 文本框使用的是vue的v-model双向绑定,在android中是ok的,在IOS上不行, 导致问题出现的原因是IOS自带输入

Vue.js双向绑定的实现原理

Vue.js最核心的功能有两个,一是响应式的数据绑定系统,二是组件系统.本文仅探究几乎所有Vue的开篇介绍都会提到的hello world双向绑定是怎样实现的.先讲涉及的知识点,再参考源码,用尽可能少的代码实现那个hello world开篇示例. 参考文章:https://segmentfault.com/a/1190000006599500 一.访问器属性 访问器属性是对象中的一种特殊属性,它不能直接在对象中设置,而必须通过defineProperty()方法单独定义. var obj = {

Vue双向绑定原理详解

前言:Vue最核心的功能之一就是响应式的数据绑定模式,即view与model任意一方改变都会同步到另一方,而不需要手动进行DOM操作,本文主要探究此功能背后的原理. 思路分析 以下是一个最简单的双向绑定的例子: <body> <div id="app"> <input type="text" v-model="msg"> <p>{{msg}}</p> </div> </