vue进阶一~数据响应式,数据响应到视图层,手写v-model,订阅发布者模式,

1,数据响应式

          当数据发生改变的时候,我们立即知道数据发生改变,并做出相关的操作:发送请求,打印文字,操作DOM等。

1.1,vue实现数据响应的原理

         vue中使用了两种模式来实现数据响应式,分别是vue2(Object.defineProperty ()),vue3(Proxy)。

1.2,vue2实现数据响应:对象属性拦截Object.defineProperty ()  

首先我们先要知道如何多一个对象进行赋值:两种方式,字面量赋值和Object.defineProperty ()

A:字面量赋值

我们定义一个数据的时候,通过字面值的数据进行定义:

<script>
  let obj={
     name:"小智"
      }
  obj.name="小红"
  let app=document.getElementById('app')
  console.log(obj);
  app.innerText=obj.name
    </script>

 这时我们在控制台上看到好像是可以知道obj属性的变化的,我们再在控制台上打印:

 obj的name属性是小红,我们再修改name属性值。

B:Object.defineProperty ()  方法拦截对象属性

JavaScript提供了Object.defineProperty ()  方法拦截对象属性。

let obj={}
let _name="小红" 
Object.defineProperty(obj,'name',{
  // 当访问对象的属性的时候,get方法被调用
   get(value){
       return _name
    },
  // 当修改对象属性的时候被调用
  set(newValue){
      _name=newValue
     }
})

_name属性是一个中间变量,用于解决get/set联动问题。

 c:强化属性劫持方案

一般情况下,我们需要将一个个变量都放到一个对象中,这时候实现Object.defineProperty ()赋值的时候,中间变量会变得有点多,这时候我们需要对其进行优化处理:

  let data={
            name:"小智",
            age:12,
            sex:"男"

        }
        Object.keys(data).forEach(key=>{
            Observer(data,key,data[key])
            console.log(key+data[key]);
        })
        function Observer(obj,key,value){
            Object.defineProperty(obj,key,{
                get(){
                    return value
                },
                set(Newvalue){
                    value=Newvalue
                }
            })
        }

总结:响应式是指拦截数据的访问和设置,并对其做一些操作。

2,数据响应式反应到视图

我们已经可以实现数据响应式,拦截在数据设置和访问的状态了,例如:

let data={
          name:"小吴",
          age:16
      }
      Object.keys(data).forEach(key=>{
        webserver(data,key,data[key])
      })
function webserver(obj,key,value){
     Object.defineProperty(obj,key,{
        get(){
          return value+'vip'
         },
         set(newValue){
           value=newValue+'vip'
             }
          })
      }

我们对对象属性设置和访问都进行拦截,并假设“vip”字符串,在控制台打印一下会发现:

 我们只进行初始化没有进行任何后续的赋值都会在name和age后字段加‘vip’字符串,这个就是属性响应式的原理。

问题:这里发现一个有趣的问题,我本来就喜欢将功能写在一起,但是这里就导致了一个问题,问题代码如下:

let data={
          name:"小吴",
          age:16
      }
Object.keys(data).forEach(key=>{    
   Object.defineProperty(data,key,{
   get(){
        return data[key]
      },
   set(newValue){
       data[key]=newValue
      }
      })
})

在控制台打印。属性发生以下错误:

开始不知道为什么,后来才知道,这是作用域的问题,响应式是一个异步的过程,而forEach是一个同步的过程,在循环结束之后,才执行Object.defineProperty内的代码,解决方法有两个,一个是定义外部状态,形成闭包,代码如下:

  let data={
          name:"小吴",
          age:16
      }
      Object.keys(data).forEach(key=>{
        // webserver(data,key,data[key])
        let obj=data
        let item=key
        let value=data[key]
        Object.defineProperty(obj,item,{
              get(){
                  return value
              },
              set(newValue){
                value=newValue
              }
          })
      })

另一种就是封装到一个方法里面,我们知道,方法的形参是可以提升的,以上封装方法的方式提示相当一下代码:

let data={
          name:"小吴",
          age:16
      }
      Object.keys(data).forEach(key=>{
        webserver(data,key,data[key])
      })
function webserver(obj,key,value){
    let obj
    let key
    let value
    Object.defineProperty(obj,key,{
        get(){
          return value+'vip'
         },
         set(newValue){
           value=newValue+'vip'
             }
          })
      }

这样在Object.defineProperty内部形成一个闭包。

2.1.数据反映到视图层---编程式响应

之前我们已经实现类数据响应式,那如何将数据响应到视图呢?说过一句话,响应式是用来拦截数据的设置和读取的,也就是说,在设置之前(set方法里面),我们是可以拿到数据的,只要将拿到的数据添加到DOM节点上就可以了:

let data={
          name:"小吴",
          age:16
      }
      let app=document.getElementById('app')
      app.innerText=data.name
      Object.keys(data).forEach(key=>{
        webserver(data,key,data[key])
      })
      function webserver(obj,key,value){
          Object.defineProperty(obj,key,{
              get(){
                  return value
              },
              set(newValue){
                if(newValue===value){//如果设置的值和原来的值相等,不执行赋值代码
                      return
                  }
                app.innerText=newValue
                value=newValue
              }
          })
      }

这个是单数据响应,多数据响应处理起来还是比较麻烦,处理思路为:(等有空再说,会补坑的)

A:双向数据绑定

思路:给input绑定一个内容发生变化就会触发的一个函数,通过该函数改变变量实现双向数据绑定。

<body>
    <div id="app"></div>
    <!-- <div id="app1"></div> -->
    <input type="text" oninput="change()" id="text">
    <script>
        let text=document.getElementById('text')
        let app=document.getElementById('app')
        let data="小美"
        app.innerText=data
        text.value=data
        function change(data){  
            data=text.value
            app.innerText=data
        }

    </script>
</body>

2.2.数据反映到视图层(数据双向绑定)

 将数据响应到视图层的方式,在vue2使用的是Object.defineProperty()来实现,需要劫持到数据的变化,在数据变化的时候将其渲染到视图上:

第一步:数据层到视图层的响应

A,首先需要遍历data的数据,获取到每个data的属性

B,其次,获取到data的每个属性之后,对属性进行拦截。

C,在拦截时,需要将其值反映到对应的视图标签上。

第C步中又可以分为:

        a,获取到视图中每个节点的DOM对象, 

        b,遍历DOM对象,获取到每个DOM对象对应的node对象(节点对象)。

        c,遍历node对象,获取到node对象中含有v-model属性的节点

        d,将对应的data的属性值赋值给node节点的value值上

代码实现为:

<body>
    <div id="app">
        名字:<input type="text" v-model="name" value="12"><br>
        年龄:<input type="text" v-model="age" value="2323">
        <div class="sjkx">
            <h1 class="name">模拟v-model</h1>
        </div>
    </div>
    <script>
        let data = {
            name: '',
            age: '',
        }
        let view = document.getElementById('app')
        // 数据反映到视图方法
        function getVmodelNode(view, data) {
            // a,获取到视图中每个节点的DOM对象,
            let allDom = view.getElementsByTagName("*")
            let allDomArray = Array.from(allDom)//将伪数组转化成数组
            let attributeArray = []
            // b,遍历DOM对象,获取到每个DOM对象对应的node对象。
            allDomArray.forEach(nodeItem => {
               //c, 遍历node对象,过滤获取到node对象中含有v-model的节点
                Array.from(nodeItem.attributes).forEach(item => {
                    if (item.nodeName === 'v-model') {
                        //d,将对应的data的属性值赋值给node节点的value值上
                        nodeItem.value = data[item.nodeValue]
                    }
                })
            })
        }
        //  A,首先需要遍历data的数据,获取到每个data的属性
        Object.keys(data).forEach(key => {
            // B,其次,获取到data的每个属性之后,对属性进行拦截。
            debugger
            WebServer(data,key,data[key])
        })
        function WebServer(data, key, value) {
            Object.defineProperty(data, key, {
                get() {
                    return value
                },
                set(newValue) {
                    value= newValue
                    // C,在拦截时,需要将其值反映到对应的视图标签上。
                    getVmodelNode(view,data)
                }
            })
        }
    </script>
</body>

第二步:视图层到数据层的响应。

视图层响应到数据层的思路:当输入框的数据发生变化的时候,调用函数,将输入框的值赋值给data对应的属性上。

实际上就是在第一步的第C步多加了一步:为对应的node对象绑定一个input事件,并将node的value赋值给data对应的属性上。

<body>
    <div id="app">
        名字:<input type="text" v-model="name" value="12"><br>
        年龄:<input type="text" v-model="age" value="2323">
        <div class="sjkx">
            <h1 class="name">模拟v-model</h1>
        </div>
    </div>
    <script>
        let data = {
            name: '',
            age: '',
        }
        let view = document.getElementById('app')
        // 数据反映到视图方法
        function getVmodelNode(view, data) {
            // a,获取到视图中每个节点的DOM对象,
            let allDom = view.getElementsByTagName("*")
            let allDomArray = Array.from(allDom)//将伪数组转化成数组
            let attributeArray = []
            // b,遍历DOM对象,获取到每个DOM对象对应的node对象。
            allDomArray.forEach(nodeItem => {
               //c, 遍历node对象,过滤获取到node对象中含有v-model的节点
                Array.from(nodeItem.attributes).forEach(item => {
                    if (item.nodeName === 'v-model') {
                        //d,将对应的data的属性值赋值给node节点的value值上
                        nodeItem.value = data[item.nodeValue]
                        //视图层反映到数据层上,
                        nodeItem.addEventListener('input',(e)=>{
                            data[item.nodeValue]=e.target.value
                        })
                    }

                })

            })
        }
        //  A,首先需要遍历data的数据,获取到每个data的属性
        Object.keys(data).forEach(key => {
            // B,其次,获取到data的每个属性之后,对属性进行拦截。
            
            WebServer(data,key,data[key])
        })
        function WebServer(data, key, value) {
            Object.defineProperty(data, key, {
                get() {
                    return value
                },
                set(newValue) {
                    value= newValue
                    // C,在拦截时,需要将其值反映到对应的视图标签上。
                    getVmodelNode(view,data)
                }
            })
        }
         //视图响应到数据层
         getVmodelNode(view, data)
    </script>
</body>

这就是完整的模拟v-model的代码了。我们还可以通过这个代码实现v-text,只需要加个判断就可以了

<body>
    <div id="app">
        名字:<input type="text" v-model="name" value="12"><br>
        年龄:<input type="text" v-model="age" value="2323">
        <div class="sjkx">
            <h1 class="name" >模拟v-model</h1>
            <p v-text="text"> </p>
        </div>
    </div>
    <script>
        let data = {
            name: '',
            age: '',
            text:'hHHHHHHH'
        }
        let view = document.getElementById('app')
        // 数据反映到视图方法
        function getVmodelNode(view, data) {
            // a,获取到视图中每个节点的DOM对象,
            let allDom = view.getElementsByTagName("*")
            let allDomArray = Array.from(allDom)//将伪数组转化成数组
            let attributeArray = []
            // b,遍历DOM对象,获取到每个DOM对象对应的node对象。
            allDomArray.forEach(nodeItem => {
               //c, 遍历node对象,过滤获取到node对象中含有v-model的节点
                Array.from(nodeItem.attributes).forEach(item => {
                    // 实现v-text核心代码
                    if (item.nodeName === 'v-text') {
                        console.log("haha",data[item.nodeValue]);
                        //d,将对应的data的属性值赋值给node节点的value值上
                        nodeItem.innerText = data[item.nodeValue]
                    }
                    //实现v-model核心代码
                    console.log(item.nodeName);
                    if (item.nodeName === 'v-model') {
                        //d,将对应的data的属性值赋值给node节点的value值上
                        nodeItem.value = data[item.nodeValue]
                        nodeItem.addEventListener('input',(e)=>{
                            let nodeValue=e.target.value
                            data[item.nodeValue]=nodeValue
                        })
                    }

                })

            })
        }
        //  A,首先需要遍历data的数据,获取到每个data的属性
        Object.keys(data).forEach(key => {
            // B,其次,获取到data的每个属性之后,对属性进行拦截。
            WebServer(data,key,data[key])
        })
        function WebServer(data, key, value) {
            Object.defineProperty(data, key, {
                get() {
                    return value
                },
                set(newValue) {
                    console.log("被调用了");
                    value= newValue
                    // C,在拦截时,需要将其值反映到对应的视图标签上。
                    getVmodelNode(view,data)
                }
            })
        }
        //视图响应到数据层
         getVmodelNode(view, data)
    </script>
</body>

现存问题:这里现存一个问题,就是在执行v-model核心代码的时候,我们只是输入一个值,但是v-model的核心代码块执行了两次,这不是我们想要的,而是应该我只输入一个值得时候,另一个值得v-model代码块不应该被执行

 这时候需要引入一个优化点——精确更新

因为我们在执行的时候,执行到

 if (item.nodeName === 'v-model') {
    //d,将对应的data的属性值赋值给node节点的value值上
    nodeItem.value = data[item.nodeValue]
    nodeItem.addEventListener('input',(e)=>{
    let nodeValue=e.target.value
    data[item.nodeValue]=nodeValue
   })
}

这个代码块的时候,我们都需要操作nodeItemitem.nodeValue  nodeItem  是我们需要操作的DOM节点,item.nodeValue  是我们需要赋值的属性值,每次都操作同一个的时候,每次执行都会被创建,这会导致资源过多被使用。

因此,优化的思路为:  将nodeItemitem.nodeValue       存储到浏览器内存内,每次调用就从浏览器内存取出而不是创建。

存贮到浏览器内存中的方式:闭包 ,代码如下

()=>{nodeItem.value = data[item.nodeValue]}

上述还有一个问题,由于我们响应式数据绑定到的dom节点可能有多个,所以node 节点可能存在多个一旦响应式属性(name) 发生变化与name属性相关的所有的dom节点都需要进行一轮更新, 所以属性和更新函数之间是一个对多的关系

{
    nodeValue:[
    ()=>{nodeItem1.value = data[item.nodeValue]},//name标签的
    ()=>{nodeItem1.value = data[item.nodeValue]},//age标签的
    ......
    ]
}

而要实现精确更新的这个过程,也被称之为订阅发布者模式

3,订阅发布者模式

我们先要知道定义发布者模式,在浏览器中,我们不能为同一个标签绑定相同的事件名称,此时我们可以使用addEventListener为多个DOM节点绑定相同的事件名,这种模式就是订阅发布者模式,

我们可以自定义一个定义发布者模式事件:

第一步:定义一个map,key为事件名,value为事件回调函数集合

let map = {}

第二步:收集事件名和回调函数存贮到map中

function collect(eventName, fn) {
   if (!map[eventName]) {
     map[eventName] = []
    }
    map[eventName].push(fn)
  }

第三步,执行回调函数

function trigger(eventName) {
   map[eventName].forEach(fn => fn())
}

为了代码美观,我们把全部代码放到一个对象内。

 let addDeep = {
     map: {},
     collect: function (eventName, fn) {
         if (!this.map[eventName]) {
         this.map[eventName] = []
         }
         this.map[eventName].push(fn)
     },
     trigger:function (eventName)  {
         this.map[eventName].forEach(fn => fn())
     }
}

测试代码:

        addDeep.collect('lick', () => {
            console.log("加一");
        })
        addDeep.collect('lick', () => {
            console.log("加二");
        })
        addDeep.trigger('lick')

此时一个简单的订阅发布者模式就做好了。

3.1.使用订阅发布者模式实现精确更新

思路:我们在实现v-model和v-text的时候,更新的代码分别为 data[item.nodeValue]=nodeValue和nodeItem.innerText = data[item.nodeValue],我们需要更新的是data的属性值,因此我们需要收集到相关属性,之前我们说需要把nodeItemitem.nodeValue   形成一个闭包,保存到内存中。因此回调函数的代码就是

nodeItem.value = data[item.nodeValue]

也就是将更新操作放到回调函数中。

在核心代码块处:收集相关数据

 if (item.nodeName === 'v-model') {
     // 收集数据
     addDeep.collect(item.nodeValue,()=>{
         nodeItem.value = data[item.nodeValue]
    })
    nodeItem.addEventListener('input',e=>{
        let dataValue=e.target.value
        data[item.nodeValue]=dataValue
        console.log("11",data);
        })
    }

   实现精确更新

 set(newValue) {
    value= newValue
     // 精确更新
      addDeep.trigger(key)
}

完整代码(v-model)

<body>
    <div id="app">
        名字:<input type="text" v-model="name" value="12"><br>
        年龄:<input type="text" v-model="age" value="2323">
        <div class="sjkx">
            <h1 class="name">模拟v-model</h1>
        </div>
    </div>
    <script>
        let data = {
            name: '',
            age: '',
        }
        let addDeep = {
            map: {},
            collect: function (eventName, fn) {
                if (!this.map[eventName]) {
                    this.map[eventName] = []
                }
                this.map[eventName].push(fn)
            },
            trigger:function (eventName)  {
                this.map[eventName].forEach(fn => fn())
            }
        }
        let view = document.getElementById('app')
        // 数据反映到视图方法
        function getVmodelNode(view, data) {
            // a,获取到视图中每个节点的DOM对象,
            let allDom = view.getElementsByTagName("*")
            let allDomArray = Array.from(allDom)//将伪数组转化成数组
            let attributeArray = []
            // b,遍历DOM对象,获取到每个DOM对象对应的node对象。
            allDomArray.forEach(nodeItem => {
               //c, 遍历node对象,过滤获取到node对象中含有v-model的节点
                Array.from(nodeItem.attributes).forEach(item => {
                    if (item.nodeName === 'v-model') {
                        //d,将对应的data的属性值赋值给node节点的value值上
                        // nodeItem.value = data[item.nodeValue]
                        console.log("22",data);
                        addDeep.collect(item.nodeValue,()=>{
                            nodeItem.value = data[item.nodeValue]
                        })
                        //视图层反映到数据层上,
                        nodeItem.addEventListener('input',e=>{
                            let dataValue=e.target.value
                            data[item.nodeValue]=dataValue
                            console.log("11",data);
                        })
                    }

                })

            })
        }
        // console.log(addDeep.map);
        //  A,首先需要遍历data的数据,获取到每个data的属性
        Object.keys(data).forEach(key => {
            // B,其次,获取到data的每个属性之后,对属性进行拦截。 
            WebServer(data,key,data[key])
        })
        function WebServer(data, key, value) {
            Object.defineProperty(data, key, {
                get() {
                    return value
                },
                set(newValue) {
                    value= newValue
                    // C,在拦截时,需要将其值反映到对应的视图标签上。
                    // getVmodelNode(view,data)
                    addDeep.trigger(key)
                }
            })
        }
        getVmodelNode(view, data)
        // console.log(data);
    </script>
</body>

在v-model代码核心处打印:

                     if (item.nodeName === 'v-model') {
                        //d,将对应的data的属性值赋值给node节点的value值上
                        // nodeItem.value = data[item.nodeValue]
                        console.log("22",data);
                        addDeep.collect(item.nodeValue,()=>{
                            nodeItem.value = data[item.nodeValue]
                        })
                        //视图层反映到数据层上,
                        nodeItem.addEventListener('input',e=>{
                            let dataValue=e.target.value
                            data[item.nodeValue]=dataValue
                            console.log("11",data);
                        })
                    }

初始化时:调用两次

 输入新值:

 值调用一次更新函数。这就实现了优化比较好的v-model源码了。

结束了,看都看到这了,点赞收藏吧~~~~~算我球球你啦!!!

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值