阅读 315

大白话讲明白vue数据双向绑定

vue双向数据绑定的原理

预备知识,reduce和Object.defineProperty

这两个知识会在下面大量的用到,其中Object.defineProperty是核心,reduce的话下面大量操作会借助于他,通俗的来说,就是前者具有战术意义,后者具有战略意义

reduce

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

1.数组的累加计算

const newarr = arr.reduce((pre,cur)=>{return pre+cur},0)
复制代码

2.链式获取对象属性的值

   const obj = {
       name:'zs',
       info:{
           address:{
               location:'北京'
           }
       }
   }
   const arrs = ['info','address','location']
   const zs_location = arrs.reduce((pre,cur)=>{return pre[cur]},obj)
   console.log(zs_location)
复制代码

升级操作

const obj = {
       name:'zs',
       info:{
           address:{
               location:'北京'
           }
       }
   }
const arrStr = 'info.address.location'
const zs_location = arrStr.split('.').reduce((pre,cur)=>pre[cur],obj)
console.log(zs_location)
复制代码

watch

监听对象的时候,不能直接使用变量,要加上引号

 'testdata.count'(){
            console.log(this.testdata.count)
            this.$store.dispatch('SetCount',this.testdata.count)
            this.$store.dispatch('PrintCount')
            
        },
复制代码

发布订阅模式

1.Dep类:负责进行依赖收集

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

2.Watch类:负责订阅一些事件

dom相当于订阅者,当数据发生改变的时候,发布者会将消息发布给订阅者,订阅者拿到消息之后进行重新渲染

Object.defineProperty

const obj = {
    name:'zs'
}
Object.defineProperty(obj,'name',{
    enumerable:true,//当前属性允许被循环
    configurable:true,//当前属性,允许被配置 delete
    get(){
        return '我不是zs'
    },
    set(newVal){
        console.log(newVal)
    }
})
console.log(obj.name)//我不是zs,说明结果被拦截了,访问name会被get所拦截
obj.name='ls'//给name赋值的时候会被set所拦截
​
复制代码

需要3个类

class Vue{}//通过实例化Vue创建vm对象
class Dep{}//依赖收集的类/收集watcher订阅者的类
class Watcher{}//订阅者的类
复制代码

两个方法

function Observe(obj){}//定义一个数据劫持的方法
function Compile(el,vm){}//对HTML结构进行模板编译的方法
复制代码

双向绑定实现:单向数据绑定(能够将vm实例中数据渲染到页面),单向数据动态绑定(更改vm实例中数据会重新渲染对应页面),双向数据(在单项数据绑定的基础上,实现修改页面对应vm的数据能够影响vm实例中的数据)

首先我们来考虑如何实现将vm实例中的数据渲染到页面中,像vue那样,在模板表达式中使用obj.name就可以访问vm实例中的数据,那我们就需要一个模板编译函数,来实现vm实例中数据到页面的渲染

function Compile(el, vm) {
  // 获取 el 对应的 DOM 元素
  vm.$el = document.querySelector(el)
​
  // 创建文档碎片,提高 DOM 操作的性能
  const fragment = document.createDocumentFragment()
​
  while ((childNode = vm.$el.firstChild)) {
    fragment.appendChild(childNode)
  }
​
  // 进行模板编译
  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)
      console.log(execResult)
      if (execResult) {
        const value = execResult[1].split('.').reduce((newObj, k) => newObj[k], vm)
        node.textContent = text.replace(regMustache, value)
    
      }
      // 终止递归的条件
      return
    }
    
    }
​
    // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
    node.childNodes.forEach((child) => replace(child))
  }
}
​
复制代码

但是这样我们只能第一次刷新页面的时候才可以使用vm中的数据,后面我们修改vm实例中数据,页面并不会变化,也就是说我们只是实现了单项数据的渲染,但是没有动态的进行绑定,如何绑定喃?

这个时候就需要使用到上面所说的订阅发布机制了,当我们创建一个具有数据的DOM的时候,DOM对象可以理解为一个订阅者,我将我的信息存到发布者那里,当发布者监听到DOM对应的vm数据发生变化的时候就发送信息告诉DOM进行页面重新渲染,所以我们在创建一个DOM的时候就要创建一个Watcher类(订阅者类,里面需要拿到对应的vm、自己dom对应的数据key,以及如何更新的回调函数),每次创建一个DOM的时候就实例化一个Watcher类,并将其存储在发布者类里面,所以我们还需要有一个依赖收集类Dep类,里面存放着订阅的信息,这个类的功能:存放订阅信息(watcher实例)、一个添加watcher实例的方法、一个通知每个watcher进行更新的方法,这么看好像Dep要简单点,所以我们首先来实现Dep类

Dep

// 依赖收集的类/收集 watcher 订阅者的类
class Dep {
  constructor() {
    // 今后,所有的 watcher 都要存到这个数组中
    this.subs = []
  }
​
  // 向 subs 数组中,添加 watcher 的方法
  addSub(watcher) {
    this.subs.push(watcher)
  }
​
  // 负责通知每个 watcher 的方法
  notify() {
    this.subs.forEach((watcher) => watcher.update())
  }
}
复制代码

为了方便起见,我们没有做到非常详细只更新某一部分,而是只要数据有更新我们就更新全部,中心在理解数据绑定上面

接下来是Watcher类

// 订阅者的类
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
​
    // ↓↓↓↓↓↓ 下面三行代码,负责把创建的 Watcher 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
    Dep.target = this
    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)
  }
}
 
复制代码

同时我们还要有一个数据劫持的方法,我们需要去劫持vm实例上data数据,比如某个数据访问的时候我们该做什么操作,修改某个数据的时候该进行什么操作,递归给data中每一个数据都进行数据劫持,我们编译类中只要使用到了vm中的数据,对应操作为get,那么我们就需要实例化一个watcher类,然后存在Dep里面,一旦vm数据发生变化,即触发set,然后dep通知watcher进行更新

// 定义一个数据劫持的方法
function Observe(obj) {
  // 这是递归的终止条件
  if (!obj || typeof obj !== 'object') return
  const dep = new Dep()
​
  // 通过 Object.keys(obj) 获取到当前 obj 上的每个属性
  Object.keys(obj).forEach((key) => {
    // 当前被循环的 key 所对应的属性值
    let value = obj[key]
    // 把 value 这个子节点,进行递归
    Observe(value)
    // 需要为当前的 key 所对应的属性,添加 getter 和 setter
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
        // 就被放到了 dep.subs 这个数组中了
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set(newVal) {
        value = newVal
        Observe(value)
        // 通知每一个订阅者更新自己的文本
        dep.notify()
      },
    })
  })
}
复制代码

当前还只是实现了单项数据绑定,即vm中data数据变化会引发页面中对应dom重新渲染,但是我们还没有实现双向数据绑定,双向数据绑定其实还较为简单,在编译方法中进行修改,在具有v-model属性的文本框上添加监听函数,即node.addEventListener,数据变化的时候把最新的数据更新到vm上即可 // 对 HTML 结构进行模板编译的方法 function Compile(el, vm) { // 获取 el 对应的 DOM 元素 vm.$el = document.querySelector(el)

// 创建文档碎片,提高 DOM 操作的性能 const fragment = document.createDocumentFragment()

while ((childNode = vm.$el.firstChild)) { fragment.appendChild(childNode) }

// 进行模板编译 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)
  console.log(execResult)
  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
}

// 判断当前的 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)
      const leafKey = keyArr[keyArr.length - 1]
      obj[leafKey] = e.target.value
    })
  }
}

// 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
node.childNodes.forEach((child) => replace(child))
复制代码

} }

完整代码

<div id="app">
        {{name}}
        <input type="text" v-model='name'>
</div>
<script>
    const vm = new Vue({
    el:'#app',
    data:{
        name:'zs',
        age:20,
        info:{
            a:'a1'
        }
    }
})
</script>
复制代码

vue.js

class Vue {
  constructor(options) {
    this.$data = options.data
​
    // 调用数据劫持的方法
    Observe(this.$data)
​
    // 属性代理
    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) 获取到当前 obj 上的每个属性
  Object.keys(obj).forEach((key) => {
    // 当前被循环的 key 所对应的属性值
    let value = obj[key]
    // 把 value 这个子节点,进行递归
    Observe(value)
    // 需要为当前的 key 所对应的属性,添加 getter 和 setter
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get() {
        // 只要执行了下面这一行,那么刚才 new 的 Watcher 实例,
        // 就被放到了 dep.subs 这个数组中了
        Dep.target && dep.addSub(Dep.target)
        return value
      },
      set(newVal) {
        value = newVal
        Observe(value)
        // 通知每一个订阅者更新自己的文本
        dep.notify()
      },
    })
  })
}
​
// 对 HTML 结构进行模板编译的方法
function Compile(el, vm) {
  // 获取 el 对应的 DOM 元素
  vm.$el = document.querySelector(el)
​
  // 创建文档碎片,提高 DOM 操作的性能
  const fragment = document.createDocumentFragment()
​
  while ((childNode = vm.$el.firstChild)) {
    fragment.appendChild(childNode)
  }
​
  // 进行模板编译
  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)
      console.log(execResult)
      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
    }
​
    // 判断当前的 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)
          const leafKey = keyArr[keyArr.length - 1]
          obj[leafKey] = e.target.value
        })
      }
    }
​
    // 证明不是文本节点,可能是一个DOM元素,需要进行递归处理
    node.childNodes.forEach((child) => replace(child))
  }
}
​
// 依赖收集的类/收集 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
​
    // ↓↓↓↓↓↓ 下面三行代码,负责把创建的 Watcher 实例存到 Dep 实例的 subs 数组中 ↓↓↓↓↓↓
    Dep.target = this
    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)
  }
}
复制代码

\

文章分类
前端
文章标签