vue的数据响应式原理和双向数据绑定的原理是什么?二者有区别吗?如何实现双向绑定?

先说结论,数据响应式是指通过数据驱动DOM视图的变化,是单向的过程;而双向数据绑定的数据和DOM是一个双向的关系。

Vue的数据响应式

Vue响应式指的是:组件中的data发生变化,立刻触发视图的更新。

Vue响应式的实现主要是基于数据劫持发布-订阅者模式,依赖于Object.defineProperty(vue2)和Proxy(vue3)对象,把data的数据对象转换为getter和sertter。当数据被访问时,触发getter函数进行依赖收集当数据属性被修改时,触发setter函数来通知依赖于此数据的Watcher实例重新计算,从而触发视图的更新

Vue的双向数据绑定

Vue的双向数据绑定是:基于mvvm思想,数据变化更新视图,视图变化更新数据,使数据在视图和组件之间进行双向的数据流动。

双向数据绑定的效果可以使用的v-model指令来体现,它是Vue的一个特性,也可以说是一个input事件和value的语法糖。

v-model

v-model本质上是v-bindv-on的语法糖。

  • 作用在表单元素上

    js 复制代码
    <input v-model="data" />
    //等价于
    <input v-bind:value="data" v-on:input="data=$event.target.value"/>
    // 动态绑定了 input 的 value 指向了 data 变量,并且在触发 input 事件的时候去动态把 data 设置为当前dom的value值
  • 作用在表单元素上

    js 复制代码
    //父组件中定义子组件
    <child :value="data"  @input="function(e){data = e}"></child>
    
    //子组件中
    <input v-bind:value="value" v-on:input="inputChange"></aa-input>
    props:{value},
    methods:{
        inputChange(e){
            this.$emit('input',e.target.value)
        }
    }
    // 一个组件上的 v-model 会把传入的变量用 prop 当作 value 接收
    // 子组件中,js 监听 input 输入框输入数据,触发 input 事件把数据 $emit 出去
    // 父组件中,通过同名的 input 事件接收数据

实现双向数据绑定

建议大家在阅读代码之前可以观看配套视频传送门,这个系列的学习视频讲的很清楚,对于初学者难度会大大降,接下来的代码总结仅供有基础的同学食用。

前置技术点(可跳过)

使用reduce链式获取对象属性的值

js 复制代码
const obj = {
  name: 'zs',
  info: {
    address: {
      location: '北京顺义',
    },
  },
}
// 需求:通过以下字符串在obj中获取相应的属性值
const attrStr = 'info.address.location'

// const location = attrs.reduce((newObj, k) => { return newObj[k] }, obj)
// 第一次 reduce,
//        初始值是 obj 这个对象,
//        当前的 k 项是   info
//        第一次 reduce 的结果是 obj.info 属性对应的对象

// 第二次 reduce,
//        初始值是 obj.info 这个对象,
//        当前的 k 项是   address
//        第二次 reduce 的结果是 obj.info.address 属性对应的对象

// 第三次 reduce,
//        初始值是 obj.info.address 这个对象,
//        当前的 k 项是   location
//        第三次 reduce 的结果是 obj.info.address.location 属性的值

const location = attrStr.split('.').reduce((newObj, k) => newObj[k], obj)
console.log(location) //北京顺义

发布-订阅者模式

js 复制代码
// 创建Dep类:作用是收集依赖/收集订阅者,并且触发订阅者的回调
class Dep {
  constructor() {
    // 这个 subs 数组,用来存放所有订阅者的信息
    this.subs = []
  }

  // 向 subs 数组中,添加订阅者的信息
  addSub(watcher) {
    this.subs.push(watcher)
  }

  // 发布通知的方法
  notify() {
    this.subs.forEach((watcher) => watcher.update())
  }
}

// 创建Watcher类:订阅者的类,每一个订阅者都要有update方法,用于收到通知后作出一些行为
class Watcher {
  constructor(cb) {
    this.cb = cb
  }

  // 触发回调的方法
  update() {
    this.cb()
  }
}

const w1 = new Watcher(() => {
  console.log('我是第1个订阅者')
})

const w2 = new Watcher(() => {
  console.log('我是第2个订阅者')
})

const dep = new Dep()
dep.addSub(w1)
dep.addSub(w2)

dep.notify()
// 我是第1个订阅者
// 我是第2个订阅者

使用Object.defineproperty进行数据劫持

js 复制代码
const obj = {
  name: 'zs',
  age: 20,
}

Object.defineProperty(obj, 'name', {
  enumerable: true, // 当前属性,允许被循环
  configurable: true, // 当前属性,允许被配置 
  get() {
    // getter
    console.log("name属性被访问")
    return obj.name
  },
  set(newVal) {
    // setter
    console.log("name属性被修改")
    obj.name = newVal
  },
})

const name = obj.name //name属性被访问

obj.name = 'ls' //name属性被修改

双向数据绑定的流程(代码篇)

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe中;
  2. 数据初始化完成后,对模板执行编译,这个过程发生Compile中;
  3. 进行模板编译的最后阶段,给每一种渲染方式创建watcher的实例,把渲染dom的关键操作封装为watcher的更新函数,这个过程发生在replaceNode函数中;
  4. new出来的每一个watcher的实例,都会被Dep依次收集起来;
  5. 将来data中数据⼀旦发生变化,会进入setter中,首先找到该属性对应的Dep,执行notify方法通知所有watcher执行更新函数。

实现一个简单的双向数据绑定代码如下,可以复制到本地运行,了解Vue是如何将数据劫持与发布订阅者模式结合起来的,其中有很多巧妙的细节,可以搭配视频仔细体会。

html 复制代码
<body>
  <div id="app">
    <h3>姓名是:{{name}}</h3>
    <h3>年龄是:{{age}}</h3>
    <h3>info.a 是:{{info.a}}</h3>
    <h3>姓名:<input v-model="name" /></h3>
    <h3>年龄:<input v-model="age" /></h3>
    <h3>info.a :<input v-model="info.a" /></h3>
  </div>

  <script src="./vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        name: 'zs',
        age: 20,
        info: {
          a: 'a1',
          c: 'c1'
        }
      }
    })

    console.log(vm)
  </script>
</body>
js 复制代码
//vue.js

class Vue{
  constructor(options){
    this.$data = options.data
    //调用数据劫持的方法
    Observe(this.$data)

    // 把this.$data上的属性代理到vm实例上 vm.$data.name => vm.name
    Object.keys(this.$data).forEach(key=>{
      Object.defineProperty(this,key,{
          enumerable: true,
          configurable: true,
          get() {
              return this.$data[key]
          },
          set(newValue) {
              this.$data[key] = newValue
          }
      })
    })

    //数据初始化,调用模板编译的函数渲染页面
    Compile(options.el,this)
  }
}

function Observe(obj){
  // 递归终止条件
  if(!obj || typeof obj !== 'object') return;
  const dep = new Dep()
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    // 进行递归,给对象类型的数据的子属性也添加getter/setter
    Observe(value)
    // 把data中的所有数据转换为getter/setter
    Object.defineProperty(obj,key,{
      enumerable:true,
      configurable:true,
      get(){
        // Dep.target指向watcher实例,如果key属性有对应的订阅者,就把该订阅者收集起来
        Dep.target && dep.addSubs(Dep.target)
        return value
      },
      set(newValue){
        value = newValue
        Observe(value)
        // 属性被修改时,向每一个订阅者发出通知,触发渲染dom的回调
        dep.notify()
      }
    })
  });
}

function Compile(el,vm){
  //获取el对应的DOM元素
  vm.$el = document.querySelector(el)
  //创建文档碎片,提高DOM操作的性能
  const fragment = document.createDocumentFragment()
  while ((childNode = vm.$el.firstChild)) {
      fragment.appendChild(childNode)
  }
  //进行模板编译
  replaceNode(fragment)
  //模板编译结束后,渲染页面
  vm.$el.appendChild(fragment)

  function replaceNode(node){
    const regMustache = /\{\{\s*(\S+)\s*\}\}/
    // 对文本子节点进行正则的匹配与替换
    if(node.nodeType === 3){
      const text = node.textContent
      const execResult = regMustache.exec(text)
      if (execResult){
        const value = execResult[1].split('.').reduce((newObj,k) => newObj[k],vm)
        node.textContent = text.replace(regMustache,value)
        // 在这里,创建watcher的实例
        new Watcher(vm, execResult[1],(newValue)=>{
            node.textContent = text.replace(regMustache, newValue)
        })
      }
      return 
    }
    //如果是DMO节点 并且是输入框
    if (node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT'){
      //获取节点的所有属性
      const attrs = Array.from(node.attributes)
      const findResult = attrs.find(attr=>attr.name === 'v-model')
      if (findResult){
        const expStr = findResult.value  
        const value = expStr.split('.').reduce((newObj, k) => newObj[k], vm)
        node.value = value
        // 在这里,创建watcher的实例
        new Watcher(vm, expStr, (newValue) => {
            node.value = newValue
        })
        //实现双向绑定的关键步骤
        // 监听文本框的input输入事件,拿到文本框最新的值,把最新的值,更新到vm上即可
        node.addEventListener('input',(e)=>{
          const keyArr = expStr.split('.')
          const obj = keyArr.slice(0, keyArr.length - 1).reduce((newObj, k) => newObj[k], vm)
          console.log(obj);
          obj[keyArr[keyArr.length - 1]] = e.target.value
        })
      }
    }
    // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理 递归获取所有纯文本节点
    node.childNodes.forEach(child => replaceNode(child))
  }
}

class Dep {
  constructor() {
    this.subs = []
  }

  addSubs(watcher) {
    this.subs.push(watcher)
  }

  notify() {
    this.subs.forEach(watcher => watcher.update())
  }
}


// 订阅者的类
class Watcher {
  constructor(vm,key,cb) {
    this.vm = vm
    this.key = key
    this.cb = cb
    // 下面三行负责把创建的Watcher 实例存到Dep实例的subs数组中
    // 这个步骤通过第二行代码访问ledata中的属性,触发了getter,巧妙的将watcher实例收集到dep中
    Dep.target = this
    key.split('.').reduce((newObj,k)=>newObj[k],vm)
    Dep.target = null
  }
  // 触发回调函数的方法 发布者通知我们更新
  update() {
    const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
    this.cb(value)
  }
}

总结

双向数据绑定 = (数据劫持 + 发布订阅者模式) + 通过js监听dom事件反向给vm属性赋值

相关推荐
tangbin58308512 分钟前
iOS Swift:蓝牙 BLE 连接外设CoreBluetooth
前端
WWWWW先生14 分钟前
02 登录功能实现
前端·javascript
嚴寒14 分钟前
我用 AI 画了个设计稿,然后让它自己写成了代码
前端·ai编程
踏浪无痕18 分钟前
MCP 是什么?用大白话讲清楚
面试
彭锐34324 分钟前
哨兵节点实现的自驱式任务队列
前端
阿星AI工作室28 分钟前
我做了个飞书转公众号排版器,6套高颜值主题想换就换
前端·人工智能
UrbanJazzerati1 小时前
PostgreSQL 完全实战指南:从小白到高手 DDL篇
后端·面试
_Eleven1 小时前
继TailWindCss和UnoCss后的CSS-in-JS vs Utility-First 深度对比
前端
UrbanJazzerati1 小时前
Python实现Salesforce Bulk API 2.0批量数据导入:从Excel到云端的高效方案
后端·面试
GinoWi1 小时前
CSS属性 - 边距属性
前端