vue双向绑定的简易实现

162 阅读2分钟

什么是双向绑定?

简单来说就是把数据绑定到视图层。也就是随着数据改变,视图层对应的值会改变。一般做到这里就是单向绑定了。

如果当视图层值(比如input标签里面的值)改变时候,它对应绑定的数据值也会改变,那么就实现了双向绑定。

简单概括来就是:

  • 数据变化后更新视图
  • 视图变化后更新数据

分部实现双向绑定

  • 数据劫持
  • 发布订阅监听
  • 数据渲染

数据劫持,数据渲染。就是用正则表达式,遍历节点找到差值表达式{{}}.找到对应数据值。去替换,渲染。

发布订阅用来实时监听,一旦数据发生改变就进行数据劫持,数据渲染。当然可以用addEventListening,去监听视图层的变化。

废话不多说。直接上代码;

HTML部分

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <span>黑黑:{{name}}</span>
    <input type="text" v-model="name">
    <span>黑黑的npy(from future)like:{{npy.like}}</span>
    <input type="text" v-model="npy.like">
  </div>
  
  <script src="./myvue.js"></script>
  //引用自己实现的vue
  <script >
  //创建vue实例
    let vm = new Myvue(
      {
        el:'#app',
        data:{
          name:'heihei',
          npy:{
            like:'dance'
          }
        }
      }
    )
  </script>
</body>
</html>

html文件中没用太复杂的东西。下面来看myvue的实现。

如图所示为大致框架。

image.png

vue类

//初始化时获取vue实例,赋值给$data,然后调用Observer,Compile函数;
class Myvue {
    constructor(vue_instance){
       this.$data = vue_instance.data;
       //监听数据
       Observer(this.$data)
       //解析模版
       Compile(vue_instance.el,this);
    }
}

Observer函数

如下所示,主要采用Object.defineProperty中set,get对数据进行操作。vue3中则采用proxy(),代理模式对数据处理。同时采用递归dfs,对data中深层次数据进行监控。

function  Observer(data){
    //递归出口
    if(typeof data !== 'object'|| !data ) return;
    const dependency  = new Dependency();
    //这里是调用了发布者订阅,创建了实例。
    Object.keys(data).forEach(
        (item)=>{
            let value = data[item]; 
            //递归调用
            Observer(value)
            Object.defineProperty(data,item,{
                enumerable:true,
                configurable:true,
                get(){
                    //初始化的时候就会添加订阅,加入到订阅者队列。
                    Dependency.temp && dependency.addSub(Dependency.temp)
                    return value
                },
                set(newvalue){
                    //修改时候监听新数据
                    Observer(newvalue)
                    value =  newvalue
                 //修改的时候通知订阅者,实时修改。
                   dependency.notify()
                }
            })
        }
    )
}

发布者

//发布者
class Dependency{
    constructor(){
         //订阅者队列
        this.subscribers = []; 
    }
    addSub(sub){
       //添加订阅者
        this.subscribers.push(sub)
    }
    notify(){
      //通知所有订阅者,修改数据
        this.subscribers.forEach(sub => {
           sub.update()
        })
    }
}

订阅者

//订阅者
class Watcher {
  constructor(vm,key,callback){
   //初始化,没个订阅者自身有三个数据,vue实例,data的某个属性值(这个属性应当是唯一的),回调函数
    this.vm = vm;
    this.key = key;
    this.callback = callback;
    
    //注意⚠️
    Dependency.temp = this;
    //这里为了触发get,从而触发订阅加到数组里面;
    key.split('.').reduce(
        (total,current) => total[current],vm.$data
    )
    //加完立即清除确保单例
    Dependency.temp = null
  }
  update(){   
  //更新数据函数,reduce去解决
    let value = this.key.split('.').reduce(
        (total,current) => total[current],this.vm.$data
    )
    this.callback(value);
  }
}

注意1:这里是为了添加订阅者到订阅数组,而我们的添加方法只在get里面调用,但是又不能重复添加,所以设置变量,这个变量消除之前遍历一边data的值。从而达成每个监听的变量只会放入到订阅者数组中一次。

模版解析

遍历整个html,在遍历过程中,去劫持数据,增加监听。

function Compile(root,vm){
 vm.$el = document.querySelector(root);
 const fragment = document.createDocumentFragment();
 let child;
  while(child = vm.$el.firstChild ){
    fragment.appendChild(child)
  }
  //上面部分是获取所有节点
 fragment_instead(fragment);
 //解析节点函数
 function fragment_instead(node){
    const pattern = /\{\{\s*(\S+)\s*\}\}/
    //3是文本节点
    if(node.nodeType === 3){
      const oldValue = node.nodeValue
      const res = node.nodeValue.match(pattern)
      if(res){    
        const arr = res[1].split('.');
        const value =arr.reduce(
            (total,current) => total[current],vm.$data
        )
        node.nodeValue = oldValue.replace(pattern,value);
        //就是这里创建了订阅者实例
        new Watcher(vm,res[1],(newvalue)=>{
            node.nodeValue = oldValue.replace(pattern,newvalue)
        })
      }
      return
    }
    //以上是获取差值表达式内容,用数据去替换,结合发布订阅实现数据响应。
    if(node.nodeType===1 && node.nodeName ==='INPUT'){
        const ary =Array.from(node.attributes) 
        ary.forEach((i)=>{
            if(i.nodeName ==='v-model'){
               let value = i.nodeValue.split('.').reduce(
                (total,current)=>total[current],vm.$data
               )          
             node.value =value
             new Watcher(vm,i.nodeValue,(newvalue)=>{
                node.value = newvalue
            })
            //监听视图层数据,并修改data(这里以input为例)
            node.addEventListener('input',e=>{
                const arr1 = i.nodeValue.split('.');
                const arr2 = arr1.slice(0,arr1.length-1);
                const final =arr2.reduce((total,current)=>total[current],vm.$data)
                final[arr1[arr1.length-1]] = e.target.value;
            })
            }
        })
    }
    node.childNodes.forEach(child=> fragment_instead(child))
    //递归调用,处理深层次数据。
   
}
//最后将操作好的节点重新渲染
vm.$el.appendChild(fragment)
}

结果

初视图

image.png 修改后的图

image.png