Vue2.x、Vue3.x的数据响应式核心原理

文章目录

        • 1.概念
        • 2.Vue2.x数据响应式核心原理
        • 3. Vue 3.x数据响应式原理
        • 4. 发布/订阅模式
        • 5.观察者模式
        • 6.两种模式的区别

1.概念

在学习核心原理前,我们先了解两个概念:

  • 数据响应式:数据模型仅仅是普通的JS对象,而我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率。(内部操作DOM,我们只是修改数据)
  • 双向数据绑定:数据改变,视图也改变;视图改变,数据也改变。在表单上使用v-model创建双向数据绑定。

2.Vue2.x数据响应式核心原理

在官方文档有这么一段话:

当你把一个普通的JavaScript对象传入Vue实例作为data选项,Vue将遍历此对象所有的属性,并使用Object.defineProperty把这些属性全部转为getter/setter。这些getter/setter对用户来说是不可见的,但是内部它们让Vue能够追踪依赖,在属性被访问和修改时通知变更。

实例一:


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数据响应式原理title>
head>
<body>
    <div id="app">
        Hello Vue
    div>
    <script>
        //模拟Vue实例中的data选项
        let data={
      
            msg:'Hello Vue'
        }

        //模拟Vue的实例
        let vm={
      };

        //数据劫持,当访问或者设置vm中的成员的时候,做一些干预操作
        Object.defineProperty(vm,'msg',{
      
            //可枚举(即可被遍历)
            enumerable:true,
            //可配置(可以使用delete删除,可以通过defineProperty重新定义)
            configurable:true,

            //当获取值时执行
            get(){
      
                console.log('getter:',data.msg);
                return data.msg;
            },
            //当设置、更新msg变量时执行
            set(newValue){
      
                console.log("setter:",newValue);
                if(data.msg===newValue){
      
                    return;//前后数据相同,则不用做操作DOM的多余操作
                }
                data.msg=newValue;
                document.querySelector("#app").textContent=newValue;
            }
        })

        //测试setter
        vm.msg="Hello 响应式原理";

        //测试getter
        console.log(vm.msg);
    script>
body>
html>

Vue2.x、Vue3.x的数据响应式核心原理_第1张图片
上面实例只是对msg这个属性实现了响应式,那Vue中data选项有多个属性,怎么做到让它们都成为响应式呢?

示例2:


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>数据响应式原理title>
head>
<body>
    <div id="app">
        Hello Vue
    div>
    <script>
        //模拟Vue实例中的data选项
        let data={
      
            msg:'Hello Vue',
            count:0
        }

        //模拟Vue的实例
        let vm={
      };

        function defineProperties(data){
      
            //循环给每个属性使用Object.defineProperty()
            Object.keys(data).forEach(element => {
      
                //数据劫持,当访问或者设置vm中的成员的时候,做一些干预操作
                Object.defineProperty(vm,element,{
      
                    //可枚举(即可被遍历)
                    enumerable:true,
                    //可配置(可以使用delete删除,可以通过defineProperty重新定义)
                    configurable:true,

                    //当获取值时执行
                    get(){
      
                        console.log('getter:',data[element]);
                        return data[element];
                    },
                    //当设置、更新msg变量时执行
                    set(newValue){
      
                        console.log("setter:",newValue);
                        if(data[element]===newValue){
      
                            return;//前后数据相同,则不用做操作DOM的多余操作
                        }
                        data[element]=newValue;
                        document.querySelector("#app").textContent=newValue;
                    }
                })
            });
        }
        
        //执行该函数,使每个属性添加响应式
        defineProperties(data);

        //测试setter
        vm.msg="Hello 响应式原理";

        //测试getter
        console.log(vm.msg);
    script>
body>
html>

Vue2.x、Vue3.x的数据响应式核心原理_第2张图片
在控制台设置count,同样是响应式:

Vue2.x、Vue3.x的数据响应式核心原理_第3张图片

3. Vue 3.x数据响应式原理

Vue2.x的数据响应式核心是Object.defineProperty
而Vue3.x核心是Proxy。两者相比,Proxy的效率更高,原因是Proxy是直接监听对象,而defineProperty是监听每个对象里的属性。Proxy是ES6新增的,IE浏览器不支持。

示例:


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Proxy数据响应式原理title>
head>
<body>
    <div id="app">
        Hello Vue
    div>
    <script>
        //模拟Vue实例中的data选项
        let data={
      
            msg:'Hello Vue',
            count:0
        }

        //模拟Vue实例
        //第一个参数data就是我们要代理的对象
        let vm=new Proxy(data,{
      
            //执行代理行为的函数
            //当访问vm的成员会执行
            //target其实就是我们的代理对象data
            get(target,key){
      
                console.log('getter,key:',key,'-',target[key]);
                return target[key];
            },
            set(target,key,newValue){
      
                console.log('set,key:',key,'-',newValue);
                if(target[key]===newValue){
      
                    return
                }
                target[key]=newValue;
                document.querySelector("#app").textContent=target[key];
            }
        })

        //测试setter
        vm.msg="Hello 响应式原理";
        //测试getter
        console.log(vm.msg);
    script>
body>
html>

Vue2.x、Vue3.x的数据响应式核心原理_第4张图片
控制台设置count:
Vue2.x、Vue3.x的数据响应式核心原理_第5张图片
可以看出Proxy实现数据响应式原理要比defineProperty简便,而且Proxy是直接面向整个对象的属性,而defineProperty对一个对象的多个属性都实现数据响应式,则要循环使用
Object.defineProperty

4. 发布/订阅模式

注意:发布/订阅模式观察者模式通常被混为一谈,但它们在Vue中有着不同的应用场景。

发布/订阅模式:

  • 订阅者
  • 发布者
  • 信号中心

我们假定:存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”一个信号,其它任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候可以开始执行,这就是发布/订阅模式。

下面我们看看在Vue中,这种模式的应用:

示例:Vue的自定义事件

let vm=new Vue()

//可以为同一事件设置两个监听事件,两者都会被执行
//监听即是在订阅dataChange
vm.$on('dataChange',()=>{
     
	console.log('dataChange')
})

vm.$on('dataChange',()=>{
     
	console.log('xxxx')
})

//发射dataChange即是发布事件
vm.$emit('dataChange');

示例:兄弟组件通信过程

兄弟组件间通信,通常通过设置事件总线$bus进行通信,
$bus其实就是Vue实例。

let $bus=new Vue();

//组件A中发布消息
sendMess:function(){
     
	$bus.$emit('send_mess',{
      mess:"这是组件A发布的消息" });
}

//组件B中订阅消息,在created钩子函数设置订阅
created:function(){
     
	$bus.$on('send_mess',(params)=>{
     
		console.log("在这里处理接受到的消息")
})
}

下面自己实现订阅/发布模式:


<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>发布/订阅模式title>
head>
<body>
    <script>
       class EventEmitter{
      
           constructor(){
      
               //this.subs是一个对象,相当于this.subs={},记录着每个事件的订阅函数,即回调函数
               //this.subs={'click':[fun1,fun2...]}
               //Object.create()的参数是它的原型链,null即是没有原型链
               this.subs=Object.create(null);
           }
           
           //注册函数,注册即表明是订阅,将它追加到this.subs中
           $on(eventType,handler){
      
               //如果this.subs中有eventType这种类型的事件了,就不改变,如果没有这种事件
               //就初始化为空数组,然后将handler追加进数组
                this.subs[eventType]=this.subs[eventType]||[];
                this.subs[eventType].push(handler);
           }
           //当触发了发布函数,那么就将this.subs存储着对应类型的函数执行
           $emit(eventType){
      
               //如果不为空,那就进入执行函数即可
                if(this.subs[eventType]){
      
                    this.subs[eventType].forEach(func => {
      
                        func();
                    });
                }
           }
       } 

       let em=new EventEmitter();
       //第一次订阅
       em.$on('getData',()=>{
      
           console.log("订阅getData(),当发布时,执行我这个回调函数")
       })
       //第二次订阅
       em.$on('getData',()=>{
      
           console.log('我也要订阅getData,哈哈哈');
       })

       //3秒后我将发布getData
       setTimeout(()=>{
      
            em.$emit('getData');
       },3000)
    script>
body>
html>

3秒后,指定订阅者的回调函数:

Vue2.x、Vue3.x的数据响应式核心原理_第6张图片
上面实现了EventEmitter类,只有一个subs属性和两个方法。subs中记录着订阅者的函数,在发布信息时,就执行订阅者的函数即可。

比如:

在华为要发布新手机时,很多爱国者就提前订阅了这款手机,然后华为后台是记录着每个订阅手机的用户的信息的(subs),当手机一发布,那么就逐一根据用户填写的信息(即上面的回调函数)给订阅者发货。

5.观察者模式

观察者模式:

  • 观察者(订阅者)–Watcher

每个观察者必须有一个update()方法,当事件发生时,执行观察者的update()。

  • 目标(发布者)–Dep
  • subs数组:存储所有的观察者
  • addSub():添加观察者
  • notify():当事件发生时,调用所有观察者的update(),达到通知目的。

代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>观察者模式</title>
</head>
<body>
    <script>
      //目标-发布者
      class Dep{
     
        constructor(){
     
            //记录所有订阅者
            this.subs=[];
        }
        addSub(sub){
     
            //sub必须有update才是合格的订阅者
            if(sub&&sub.update){
     
                this.subs.push(sub);
            }
        }
        notify(){
     
            //通知,即执行每个sub的update()
            this.subs.forEach(sub=>{
     
                sub.update();
            })
        }
      }
      //观察者-订阅者
      class Watch{
     
          update(){
     
              console.log('update');
          }
      }

      //测试
      //新建观察者
      let watch=new Watch();
      //新建发布者
      let dep=new Dep();
      //给发布者添加观察者
      dep.addSub(watch);
      //3秒后发布通知
      setTimeout(()=>{
     
        dep.notify();
      },3000)
    </script>
</body>
</html>

Vue2.x、Vue3.x的数据响应式核心原理_第7张图片
其实观察者就相当于订阅者。

6.两种模式的区别

区别:

  • 观察者模式是由具体目标调度的,比如当事件触发时,Dep就会去调用观察者的update方法,所以观察者模式的订阅与发布者之间时存在以来的。
  • 发布/订阅模式,由同一调度中心调用,因此发布者和订阅者不需要知道对方的存在。

Vue2.x、Vue3.x的数据响应式核心原理_第8张图片

你可能感兴趣的:(源码学习,vue,vue.js,前端)