美文网首页
双向数据绑定原理

双向数据绑定原理

作者: 接下来的冬天 | 来源:发表于2021-11-14 14:47 被阅读0次

1.数组的reduce方法

应用场景: 下次操作的初始值,依赖于上次操作的返回值

  • 数组的累加计算
const arr = [3, 8, 9 ,12, 89, 56, 43]

// 普通程序员的实现逻辑
let total = 0;
arr.forEach(item => {
    total += item;
})
console.log(total)


// reduce方法实现
// arr.reduce(函数, 初始值)
// arr.reduce((上次计算的结果, 当前循环的item) => {}, 0)
const total = arr.reduce((oldValue, item) => {
    return oldValue + item
}, 0)
console.log(total)
  • 链式获取对象属性的值
const obj = {
    name: 'zs',
    info: {
        address: {
            location: '北京顺义'
        }
    }
}

const attrs = ['info', 'address', 'location']

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

2.发布订阅模式

1. Dep类

  • 负责进行依赖收集
  • 首先有个数组专门来存放所有的订阅信息
  • 其次,还要提供一个向数组中追加订阅信息的方法
  • 然后,还要提供一个循环,循环触发数组中的每个订阅信息

2. Watcher类

  • 负责订阅一些事件
// 收集依赖/收集订阅者
class Dep {
    constructor() {
        // 这个 subs 数组,用来存放所有订阅者的信息
        this.subs = []
    }
    
    // 向 subs 数组中,添加订阅者信息
    addSub(watcher) {
        this.subs.push(watcher)
    }
    
    // 发布通知(订阅)的方法
    notify() {
        this.subs.forEach(watcher => watcher.update())
    }
}

// 订阅者的类
class Watcher {
    constructor(cb) {
        // 这里的作用就是cb回调函数,根据得到的最新数据来更新自己的DOM结构的
        this.cb = cb
    }
    
    update() {
        this.cb()
    }
}

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

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

// 将w1 和 w2这两个观察者放入 Dep 的 subs 数组中
const dep = new Dep()
dep.addSub(w1)
dep.addSub(w2)

// 只要我们为 Vue 中 data 数据重新赋值了,这个赋值操作,会被 Vue 监听到
// 然后 Vue 要把数据的变化,通知到每个订阅者
// 接下来,订阅者(DOM元素)要根据最新的数据,更新自己的内容
dep.notify()

这里 Vue 要做的事情就是要把 data 的变化通知到每一个订阅者,在这里每一个订阅者就是DOM元素,当 Vue 发现数据变化的时候会通知到每个订阅者拿到最新的数据,这里通过 dep.notify 方法来执行watcher中的 update 方法,update 方法中的回调函数来实现 DOM 元素数据的更新

3.使用 Object.defineProperty() 进行数据劫持

  • 通过 get() 劫持取值操作
  • 通过 set() 劫持赋值操作
    Object.defineProperty 语法,在 MDN 上是这么定义的:

Object.defineProperty(obj, prop, descriptor)

(1)参数

  • obj

    要在其上定义属性的对象。

  • prop

    要定义或修改的属性的名称。

  • descriptor

    将被定义或修改的属性描述符。

(2)返回值

被传递给函数的对象。

(3)属性描述符

Object.defineProperty() 为对象定义属性,分 数据描述符 和 存取描述符 ,两种形式不能混用。

数据描述符和存取描述符均具有以下可选键值:

  • configurable

当且仅当该属性的 configurabletrue 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false

  • enumerable

当且仅当该属性的 enumerabletrue 时,该属性允许被循环。默认为 false

Object.defineProperty(obj, 'name', {
    enumerable: true, // 当前属性,允许被循环
    configurable: true // 当前属性允许被配置 delete
})

存取描述符具有以下可选键值

  • get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为 undefined

  • set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined

const obj = {
    name: 'zs',
    age: '23',
}

Object.defineProperty(obj, 'name', {
    get() {
        return '我不是zs'
    }
    set(newVal) {
        console.log('我不要你给的值', newVal)
        dep.notify()
    }
})

console.log(obj.name) // 我不是张三
// 这里如果没有`defineProperty`对属性进行get操作,那么打印结果应该是zs,但是通过get操作,这里的结果应该是:我不是zs,说明get方法可以拦截这个属性取值操作(getter)
obj.name = ls // 执行后结果为:我不要你给的值 ls
//说明set方法可以拦截这个属性的赋值操作(setter)

4.模拟Vue实现简单的双向数据绑定

  • 原理图:
双向数据绑定原理图
  • html部分:
 <div id="app">
    <h3>姓名是: {{name}}</h3>
    <h3>年龄是:{{age}}</h3>
    <h3>info.a的值是:{{info.a}}</h3>
    <div>name的值是:<input type="text" v-model="name" /></div>
    <div>info.a的值是:<input type="text" v-model="info.a" /></div>
  </div>
  <script src="./vue.js"></script>
  <script>
    const vm = new Vue({
      el: '#app',
      data: {
        name: 'zs',
        age: 20,
        info: {
          a: 'a1',
          b: 'b1'
        }
      }
    })
  </script>
  • vue.js内容:
class Vue {
  // options指向的就是传进来的对象
  constructor(options) {
    this.$data = options.data

    // 调用数据劫持的方法
    Observe(this.$data)

    // 属性代理
    // 我们希望只通过vm就能获取到data中第一层属性的值
    // 这里就比如我们在生命周期中获取data中属性 name 的值可以直接使用 this.name 就是因为我们做了属性代理
    // 即:获取 vm.name -> 自动去找 vm.$data.name vm在这里只是做了一个代理
    Object.keys(this.$data).forEach(key => {
      Object.defineProperty(this, key, {
        enumerable: true,
        configurable: true,
        get() {
          return this.$data[key]
          // 这里只要有访问vm获取data值的时候,它本身并没有,直接去找 $data 获取对应的值
        },
        set(newVal) {
          this.$data[key] = newVal
        }
      })
    })

    // 调用模板编译的函数
    Compile(options.el, this)
  }
}


// 定义一个数据劫持的方法
function Observe(obj) {
  // 这是递归的终止条件
  if(!obj || typeof obj !== 'object') return
  const dep = new Dep()

  // 通过 Object.keys 获取到 obj 上的每一个属性
  Object.keys(obj).forEach(key => {
    // 当前被循环的 key 所对应的属性值
    let value = obj[key]
    // 判断 value 是否是一个对象,如果是对象那么继续递归,如果不是,那么在开头就会被递归终止条件终止了
    // 把 value 这个子节点进行递归
    Observe(value)
    // 需要为当前的 key 所对应的属性添加 getter 和 setter
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      // getter拦截取值后我们应该返回拦截属性所对应的值
      get() {
        // Dep.target 此时还没有为null,还是指向 Watcher 实例
        //只要执行了下面这一行,那么刚才 new 的 Watcher 实例
        // 就被放入了 dep.subs 这个数组中
        // target 所指向的 Watcher 实例,加到数组中
        Dep.target && dep.addSub(Dep.target)

        return value
      },
      // setter拦截赋值,应该把拦截属性当前值修改为新的值
      set(newVal) {
        value = newVal
        // 为新赋值的对象添加 getter 和 setter 
        Observe(value)

        // 通知每一个订阅者更新自己的文本
        dep.notify()
      }
    })
  })
}


// 对HTML结构进行模板编译的方法
function Compile(el, vm) {
  // 获取到的 dom 元素直接挂载到 vm 的 $el 上
  vm.$el = document.querySelector(el)
  
  // 创建文档碎片,提高 DOM 操作性能
  // 如果我们页面中有很多的插值表达式,那么我们要频繁的去更新 dom 元素的内容,这个时候会触发页面的重绘和重排。浪费我们的内存
  // 内容发生变化会触发重绘,定位和位置发生变化会触发重排
  // 这时候我们就要创建一个文档碎片,所谓文档碎片就是一块内存,把页面的每个 dom 节点都存进去
  // 这时候页面中就没有这个 dom 节点了,我们这时候直接在内存中操作 dom 元素
  // 由于文档碎片不在页面上,所以我们这时候随意修改也不会触发重绘和重排
  const fragment = document.createDocumentFragment() // 创建文档碎片
  while(childNode = vm.$el.firstChild) {
    fragment.appendChild(childNode) // 把所有节点都放入文档碎片中,这时候页面中就没有 dom 节点了
  }

  // 再把文档碎片中的节点放回到页面中
  // 在这里进行模板编译
  // 因为在这一行之前页面中还没有dom节点,我可以在这个节点的时候dom元素还在文档碎片中放着呢
  // 此时我们可以操作文档碎片中的每个子节点进行编译,编译完成后在append回去就不会触发重绘和重排)
  Replace(fragment)

  vm.$el.appendChild(fragment)


  // 负责对 dom 节点进行编译的方法
  function Replace(node) {
    // 对插值表达式进行正则
    const regMustache = /\{\{\s*(\S+)\s*\}\}/
    // 证明当前的node节点是一个文本子节点,需要进行正则的替换
    if(node.nodeType === 3) {
      // 注意:文本子节点也是一个 dom 对象
      // 如果要获取文本子节点的字符串内容,需要调用 textContent 属性获取
      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) // 这里的replace方法是字符串本身的方法

        // 在这个时候创建 watcher 类的实例
        // 为什么要在这里调用Watcher类?
        // 当执行到上面这行代码的时候,你是第一次知道怎么来更新自己
        // 这个时候你应该立即把怎么更新自己的代码存到cb这个回调函数中
        // 因为cb回调函数就是来记录怎么更新自己的
        // 怎么存到cb中?这时候需要new一个实例才能存到cb中
        new Watcher(vm, execResult[1], (newVal) =>{
          // 根据最新的value值来更新自己的文本内容
          node.textContent = text.replace(regMustache, newVal)
        })
      }


      // 终止递归的条件
      return
    }

    // 实现文本框数据绑定
    // 如果是一个 dom 节点,就要判断你身上有没有 v-model 这个属性
    // 如果存在我就认为你是一个文本框,并且要给你提供一个值
    // 判断当前的 node 节点是否为 input 输入框
    if(node.nodeType === 1 && node.tagName.toUpperCase() === 'INPUT') {
      const attrs = Array.from(node.attributes)
      const findResult = attrs.find(x => x.name === 'v-model')
      if(findResult) {

        // 获取到当前 v-model 属性的值 v-model="name" v-model="info.a"
        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) 
          obj[keyArr[keyArr.length - 1]] = e.target.value
        })
      }
    }

    // 走到这一步证明不是文本子节点,需要进行递归处理
    node.childNodes.forEach(child => Replace(child))

  }

}


// 我们只用 Object.defineProperty 我们只能实现在页面打开的一瞬间实现数据编译
// 但是后面页面数据发生变化的时候是没有办法重现渲染页面的
// 这时候就需要用到发布订阅模式来实现数据的实时更新
// 因为加了发布订阅就相当于每个dom订阅了数据更新的一个行为,只要数据更新就会自动进行发布

// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
  constructor() {
    // 今后,所有的 watcher 都要存在这个数组中
    this.subs = []
  }

  // 向 subs 数组中添加 watcher 的方法
  addSub(watcher) {
    this.subs.push(watcher)
  }

  // 负责同志每一个 watcher 的方法
  notify() {
    this.subs.forEach((watcher) => watcher.update())
  }
}


// 订阅者的类
class Watcher {
  // cb 回调函数中,记录着当前 watcher 如何更新自己的文本内容
  // 但是,只知道如何更新自己还不行,还必须拿到最新的数据
  // 因此,还需要在 new Watcher 期间,把vm也传递进来(因为vm中存着最新的数据)
  // 除此之外,还需要知道在 vm 身上众多的数据中,哪个数据才是当前自己所需要的数据
  // 因此必须在 new Watcher 期间,指定watcher对应的数据的名字
  constructor(vm, key, cb) {
    this.vm = vm
    this.key = key
    this.cb = cb

    Dep.target = this
    // 当我们执行这一步的操作的时候可以拿到对应key的值,但是我们的目的不是为了拿到key的值
    // 因为这一步触发了 getter 方法,到这一步会暂缓下面的代码执行,跳到 getter 函数中,这就是我们的目的(具体看上面getter中操作)
    // 我们这里的真正目的是为了将 new Watcher 每次调用的观察者存入 Dep 数组中,要不然下次无法通知到它
    key.split('.').reduce((newObj, k) => newObj[k], vm)
    Dep.target = null
  }

  // watcher 实例需要有 update 函数,从而让发布者能够通知我们进行更新
  update() {
    const value = this.key.split('.').reduce((newObj, k) => newObj[k], this.vm)
    this.cb(value)
  }
}

相关文章

  • 深入Vue响应式原理

    1.Vue的双向数据绑定 参考 vue的双向绑定原理及实现Vue双向绑定的实现原理Object.definepro...

  • vue 双向数据绑定

    Vue实现数据双向绑定的原理:Object.defineProperty()vue实现数据双向绑定主要是:采用数据...

  • 【转】JavaScript的观察者模式(Vue双向绑定原理)

    关于Vue实现数据双向绑定的原理,请点击:Vue实现数据双向绑定的原理原文链接:JavaScript设计模式之观察...

  • 前端理论面试--VUE

    vue双向绑定的原理(详细链接) VUE实现双向数据绑定的原理就是利用了 Object.definePropert...

  • Vue实现数据双向绑定的原理

    Vue实现数据双向绑定的原理:Object.defineProperty() vue实现数据双向绑定主要是:采用数...

  • vue面试知识点

    vue 数据双向绑定原理 vue实现数据双向绑定原理主要是:采用数据劫持结合发布订阅设计模式的方式,通过对data...

  • vue 面试汇总(更新中...)

    1.说说对双向绑定的理解 1.1、双向绑定的原理是什么 我们都知道Vue是数据双向绑定的框架,双向绑定由三个重要部...

  • Vue双向数据绑定原理

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • 关于双向绑定的问题

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • 剖析Vue原理&实现双向绑定MVVM

    1、了解vue的双向数据绑定原理以及核心代码模块 2、缓解好奇心的同时了解如何实现双向绑定 几种实现双向绑定的做法...

网友评论

      本文标题:双向数据绑定原理

      本文链接:https://www.haomeiwen.com/subject/bdfxtrtx.html