Vue的双向绑定原理

1.什么是数据的双向绑定

举个例子,用户在界面上指向某些操作,如在输入框中输入文本,数据同步发生改变;同样,通过代码或者其他方式修改数据,界面同步改变。

2.原理

Vue的双向数据绑定是通过数据劫持结合发布订阅者模式实现的。其中在vue2中,数据劫持是通过Object.defineProperty的getter和setter方法在读取和修改数据时候进行拦截实现的;vue3中数据劫持则是通过proxy和reflect结合实现的。

再来谈谈发布订阅者模式,一个消息可以被多个订阅者订阅,一个发布者可以发布多条消息。在数据发生改变时候,发布者会将信息发布给一些相关的订阅者,这些订阅者就会触发对应的监听回调去渲染视图。

3.详细过程

3.1 原理图

在这里插入图片描述

3.2 过程

下面来分析一下整个过程(以vue2为例)。

(1)new一个Vue构造实例:

class Vue{
    constructor(options) {
        //通过传过来的`options`对构造实例进行初始化;
        this.$el=document.querySelector(options.el);
        this.$data=options.data;
        this.$options=options;
        this.$watchEvent={}
        ...
    }
}

(2)初始化完成之后,执行observe方法用于对data进行响应式处理
data的数据进行递归遍历,使用Object.defineProperygetter/setter方法对data中的每个key都进行数据劫持(具体的数据劫持在下面我会详细讲),在setter方法中,会更新视图上涉及到该key变化的所有地方)。

class Vue{
    constructor(options) {
        //通过传过来的`options`对构造实例进行初始化;
        this.$el=document.querySelector(options.el);
        this.$data=options.data;
        this.$options=options;
        this.$watchEvent={}
        //处理数据响应式
        this.observe();
    }
    observe(){
         //observe方法用于对data进行响应式处理,对data中的数据进行递归遍历
         //(下面没有进行递归,因为只是示例代码,不可能那么完善)
         //给每个key都加上getter和setter方法。其中在setter方法中,需要去更新视图模板
         //上涉及到该key变化的所有地方
        for (let key in this.$data){
            let value=this.$data[key];
            let _this=this;
            Object.defineProperty(this.$data,key,{
                get(){
                   return value;
                },
                set(val) {
                    value=val;
                    //更新视图模板上涉及到该key变化的所有地方
                     _this.$watchEvent[key].forEach((item,index)=>{
                        item.update()
                    })
                }
            })
        }
    }
}

(3)调用Compiler指令解析指令与模板插值、添加相对应的订阅者
通过递归遍历,从根元素开始遍历,将模板中的变量替换为数据,然后渲染视图。同时给每个节点添加上订阅者。如果收到通知,就会更新视图。

class Vue{
    constructor(options) {
        //通过传过来的`options`对构造实例进行初始化;
        this.$el=document.querySelector(options.el);
        this.$data=options.data;
        this.$options=options;
        this.$watchEvent={}
        //处理数据响应式
        this.observe();
        //调用Compiler指令解析指令与模板插值、添加相对应的订阅者
        this.compile(this.$el);
    }
    //observe方法用于对data进行响应式处理,对data中的数据进行递归遍历
    //(下面没有进行递归,因为只是示例代码,不可能那么完善)
   //给每个key都加上getter和setter方法。其中在setter方法中,需要去更新视图模板
   //上涉及到该key变化的所有地方
    observe(){
        for (let key in this.$data){
            let value=this.$data[key];
            let _this=this;
            Object.defineProperty(this.$data,key,{
                get(){
                   return value;
                },
                set(val) {
                    value=val;
                     //更新视图模板上涉及到该key变化的所有地方
                     _this.$watchEvent[key].forEach((item,index)=>{
                        item.update()
                    })
                }
            })
        }
    }
    //通过递归遍历,从根元素开始遍历,将模板中的变量替换为数据,
   //然后渲染视图。同时给每个节点添加上订阅者。如果收到通知,就会更新视图。
    compile(node){
      node.childNodes.forEach( (child,index)=>{
          //子节点是元素节点时候
          if (child.nodeType==1){
              if (child.hasAttribute("@click")){
                  let eventFuncName=child.getAttribute("@click");
                  eventFuncName=eventFuncName.trim();
                  //绑定相关事件
                  child.addEventListener("click",()=>{
                      this.$options.methods[eventFuncName].call(this);
                  })
              }
              ...
              if (child.childNodes.length>0){
                  this.compile(child)
              }
          }
          // 子节点是文本节点时候,
          if (child.nodeType==3){
              let reg=/\{\{(.*?)\}\}/g
              let text=child.textContent;
              child.textContent=text.replace(reg,(match,key)=>{
                  key=key.trim()
                  //给每个文本节点都添加了一个订阅者
                  let watcher=new  Watch(this,key,child,"textContent")
                  //依赖收集
                  if (this.$watchEvent[key]){
                      this.$watchEvent[key].push(watcher)
                  }else {
                      this.$watchEvent[key]=[]
                      this.$watchEvent[key].push(watcher)
                  }
                  return this.$data[key];
              })
          }
      })
    }
}

class  Watch{
    constructor(vm,key,node,attr) {
        this.vm=vm;
        this.key=key;
        this.node=node;
        this.attr=attr;
    }
    update(){
        this.node[this.attr]=this.vm[this.key];
    }
}

vue3的过程和vue2有些不一样。

  • 第一vue3中不是通过new来初始化vue实例,而是通过createApp函数;
  • 第二,vue3中处理响应式不是基于Object.defineProperygetter/setter方法;而是基于es6中的proxy+reflect

3.3 依赖收集

如上,有一个过程叫依赖收集:vue组件实例中datakey可能在模板中出现多次,每次都需要用一个watcher来维护,这样子才能保证当data中某个key发生改变的时候,模板中依赖该key的所有地方都发生改变。收集这些watcher的过程称为依赖收集,一般在vue组件类的construct构造函数中初期就需要进行依赖收集。多个watcher需要用一个dep进行管理,当某个key发生改变时候,有dep去统一通知。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

太阳与星辰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值