Vue原理篇——响应式实现(双向数据绑定)

1.Vue2响应式原理

相信大家都经常听说Vue2的双向数据绑定是通过发布订阅者模式结合数据劫持实现的。要理解这句话,需要3步,一是了解什么是发布订阅者模式,二是了解什么数据劫持,三是如何将二者结合实现响应式。
在这里插入图片描述

1.1 理解发布订阅者模式

两种理解方式:一种比较偏向于生活化,一种比较偏向于技术化。

(1)第一种理解方式——生活化:
先不要管代码,想象一下我们在淘宝上买东西,暂时没货了,所以我们先把商品(需要监听的数据)加入到了购物车中,此时购物车就会知道我们需要该商品的消息,而一旦该商品发生了变化(比如降价了),就会发布消息告知我们。

所以按照这个想法,我们要制作一个订阅器模型,里面有3个东西:
第一个是容器(对象),用于存放用户的各种消息函数(数组)。这里你可以把容器理解成购物车,key是商品唯一标识,value就是该商品的各种消息。

第二个是添加订阅的方法(两个参数,一个是商品唯一标识key,一个是要添加的方法),就是往容器中的对应key值添加订阅的消息。理解容器后,这个就简单了,如果购物车中没有商品,那我们就先初始化商品,然后再添加需要知道的消息,比如是降价了,还是变贵了。

第三个是发布的方法,告诉用户数据发生了变化,并触发订阅的方法。对应来说,就是那个商品发生了改变就要发出该商品的所有消息。

(2)第二种理解方式——技术化:
订阅发布者模式主要理解订阅器模型Dep。分为3个部分:
1.clientList:{target:[fn1,fn2…]},容器,用于存放每个target/tag(通常一个target对应一个变量)的事件数组
2.listen(key,fn),订阅者,用于往clientList容器中订阅/添加指定target的事件
3.trigger(tag,val) 发布者,用于触发clientList容器中定义的指定target的事件

// 订阅器模型
// 订阅发布者模式主要订阅器模型Dep。分为3个部分:
//    1.clientList:{target:[fn1,fn2...]},容器,用于存放每个target/tag(通常一个target对应一个变量)的事件数组
//    2.listen(key,fn),订阅者,用于往clientList容器中订阅/添加指定target的事件
//    3.trigger(tag,val) 发布者,用于触发clientList容器中定义的指定target的事件
let Dep = {
    
    
  clientList:{
    
    },// 容器
  // 添加订阅
  listen:function(key,fn){
    
    
    // if(!this.clientList[key]){
    
    
    //   this.clientList[key] = []
    // }
    // this.clientList[key].push(fn)
    // 短路表达式 fn:附送消息
    (this.clientList[key] || (this.clientList[key] = [])).push(fn)
  },
  // 发布
  trigger:function(){
    
    
    let key = Array.prototype.shift.call(arguments),    // tag
        fns = this.clientList[key]  
    if(!fns || fns.length === 0){
    
    
      return false
    }
    for(let i = 0,fn;fn = fns[i++];){
    
    
      // console.log('this:',this ) // Dep
      // console.log('arguments:',arguments)  // 这是视图1/这是视图2
      fn.apply(this,arguments)
    }
  }
}

1.2 理解数据劫持

其实数据劫持也没什么好理解的,它就是Object暴露出来的一个方法Object.defineProperty,用于监听指定对象中指定key的value的获取和设置(变化)。get方法可以获取对象(vue实例中的data返回值)属性的值,set方法可以监听属性的变化。

let value = ''
const obj = {
    
    }
Object.defineProperty(obj,'age',{
    
    
    get:function(){
    
    
      console.log('取值')
      return value
    },
    set:function(val){
    
    
      console.log('设置值')
      value = val
    }
})

在这里插入图片描述

1.3 发布订阅者模式结合数据劫持

(1)设计一个函数,传入要劫持的数据obj(相当于vue的data),目标元素target(相当于{ {}}包裹的部分),目标元素dataKey(相当于data中的key),选择器selector(比如元素的class)
(2)这个函数每次使用,都会使用订阅器中的liste发布者,给容器client的对应target中添加一个事件(比如el.innerTExt = text)
(3)然后使用数据劫持Object.defineProperty,劫持传入的data的dateKey属性(就是vue中的data中的属性),每次发生改变,就使用订阅器中的订阅者trigger,传入target和value,从而触发对应target的事件,并使用传入的value作为参数
(4)每个虚拟dom都会成为target,并且使用该函数

index.js:

// 订阅器模型
let Dep = {
    
    
  clientList:{
    
    },// 容器
  // 添加订阅
  listen:function(key,fn){
    
    
    // if(!this.clientList[key]){
    
    
    //   this.clientList[key] = []
    // }
    // this.clientList[key].push(fn)
    // 短路表达式 fn:附送消息
    (this.clientList[key] || (this.clientList[key] = [])).push(fn)
  },
  // 发布
  trigger:function(){
    
    
    let key = Array.prototype.shift.call(arguments),    // tag
        fns = this.clientList[key]  
    // console.log('key:',key)
    // console.log('fns:',fns)
    if(!fns || fns.length === 0){
    
    
      return false
    }
    for(let i = 0,fn;fn = fns[i++];){
    
    
      // console.log('this:',this ) // Dep
      // console.log('arguments:',arguments)  // 这是视图1/这是视图2
      fn.apply(this,arguments)
    }
  }
}

// 数据劫持
// data:要劫持的数据
// tag:具体的目标元素
// dataKey:目标元素的key
// selector:选择器
let dataHi = function({
     
     data,tag,dataKey,selector}){
    
    
  let value = '',
      el = document.querySelector(selector)
  Object.defineProperty(data,dataKey,{
    
    
    // 取值
    get:function(){
    
    
      console.log('取值')
      return value
    },
    set:function(val){
    
    
      console.log('设置值')
      value = val
      // 发布——如果只有虚拟dom,没有初始化或者改变的话,不会触发set中的发布
      Dep.trigger(tag,val)
    }
  })
  Dep.listen(tag,function(text){
    
    
    el.innerText = text 
  })
}


// 描述一下:
// 发布订阅者模式结合数据劫持
// 1.了解数据劫持Object.defineProperty,get方法可以获取对象(vue实例中的data返回值)属性的值,set方法可以监听属性的变化
// 2.了解发布订阅者模式,其实我觉得叫订阅发布者模式更恰当,因为总是先订阅listen,才能发布trigger
// 订阅发布者模式主要理解订阅器模型Dep。分为3个部分:
//    1.clientList:{target:[fn1,fn2...]},容器,用于存放每个target/tag(通常一个target对应一个变量)的事件数组
//    2.listen(key,fn),订阅者,用于往clientList容器中订阅/添加指定target的事件
//    3.trigger(tag,val) 发布者,用于触发clientList容器中定义的指定target的事件
// 3.了解数据劫持结合发布订阅者模式实现双向绑定/响应式原理:
//    1.设计一个函数,传入要劫持的数据obj(相当于vue的data),目标元素target(相当于{
    
    {}}包裹的部分),目标元素dataKey(相当于data中的key),选择器selector(比如元素的class)
//    2.这个函数每次使用,都会使用订阅器中的liste发布者,给容器client的对应target中添加一个事件(比如el.innerTExt = text)
//    3.然后使用数据劫持Object.defineProperty,劫持传入的data的dateKey属性(就是vue中的data中的属性),每次发生改变,就使用订阅器中的订阅者trigger,传入target和value,从而触发对应target的事件,并使用传入的value作为参数
//    4.每个虚拟dom都会成为target,并且使用该函数
// 4.初始化或者更新data中的数据时都会触发订阅器中的trigger,从而完成响应式的操作

1.4 初始化或者更新data中的数据时都会触发订阅器中的trigger,从而完成响应式的操作

在这里插入图片描述
index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    订阅视图-1:<span class="box-1"></span>
    订阅视图-2:<span class="box-2"></span>
  </div>
  <script src="index.js"></script>
  <script>
    let obj = {
      
      }
    dataHi({
      
      
      data:obj,
      tag:'view-1',
      dataKey:'one',
      selector:'.box-1'
    })
    dataHi({
      
      
      data:obj,
      tag:'view-2',
      dataKey:'two',
      selector:'.box-2'
    })

    // 1.初始化赋值
    obj.one = '这是视图1'
    obj.two = '这是视图2'

    // 2.劫持数据,更新者负责重新渲染 N次


    // 响应式
    // 1.数据联动(双向绑定)
    // 2.需要捕获到修改
  </script>
</body>
</html>

初始化:
在这里插入图片描述
更新:
在这里插入图片描述

2.Vue3响应式原理

Vue3和Vue2完全不同,根本没用Object.defineProperty,而是使用了Proxy。

Proxy 可以对目标对象的读取、函数调用等操作进行拦截,然后进行操作处理。它不直接操作对象,而是像代理模式,通过对象的代理对象进行操作,在进行这些操作时,可以添加一些需要的额外操作。

一个 Proxy 对象由两个部分组成: target 、 handler 。在通过 Proxy 构造函数生成实例对象时,需要提供这两个参数。 target 即目标对象, handler 是一个对象,声明了代理 target 的指定行为。

const target = {
    
    
  name:'sheldon',
  age:10
}

let proxy = new Proxy(target,{
    
    
  get:function(target,key){
    
    
    return target[key]
  },
  set:function(target,key,value){
    
    
    console.log(`修改了${
      
      key}`)
    target[key] = value
  }
})

console.log(proxy.name)
proxy.age = 26

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_39055970/article/details/126959385