vue双向数据绑定的实现原理

2021-12-25  本文已影响0人  东邪_黄药师

实现数据响应式

在Javascript里实现数据响应式一般有俩种方案,分别对应着vue2.xvue3.x使用的方式,他们分别是:

实现对象属性拦截

cript>
    let data = {}
    Object.defineProperty(data,'name',{
        // 访问name属性就会执行此方法 返回值就是获取到的值
        get(){
           console.log('name属性被获取了')
           return '林云龙'
        },
        // 设置新值就会执行此方法 newVal就是设置的新值
        set(newVal){
           console.log('name属性被设置新值了')
           console.log(newVal)
        }
    })

优化- get和set

上述代码get方法中返回的值始终是林云龙,是固定的,set中拿到新值之后,我们如何让get中可以得到newVal使我们需要解决的问题

image.png

解决方法:

我们可以 通过一个中间变量例如:_name 来中转get函数和set函数之间的联动

let data = {}
    let _name = '李云龙'
    Object.defineProperty(data, 'name', {
      get() {
        console.log('你访问了data1的name属性')
        return _name
      },

      set(newValue) {
        console.log('你修改了data1的name属性最新的值为', newValue)
        _name = newValue
      }
    })
image.png

优化2-更加通用的劫持方案

 //  1.如何把这个提前申明好的对象,把里面的所有的属性都变成我们刚才讲过的 set和get的形式?
    //  可以做到不管是访问data中的任何一个属性还是设置data中的任何一个属性我们都能知道
    // 1. 由于有多个属性 对象的遍历
    Object.keys(data).forEach((key) => {
        console.log(key, data[key])
        // key代表data对象的每一个属性名
        // data[key]代表data对象每一个属性对应的value值
        // data 源对象
        // 处理每一个对象key转变成响应式
        defineReactive(data, key, data[key])
    })
    1. 函数定义形参相当于在内部 申明了和形参名字对应的变量 并且初始值为undefined
    2. 函数调用传入实参的时候 相当于给内部申明好的变量做了赋值操作 (首次遍历举例)
    3. defineReactive函数调用完毕 本来应该内部所有的变量都会被回收 但是如果内部有其它函数使用了当前变量则形成了闭包 不会被回收
    4. 内部由于有其它方法引用了value属性  所以defineReactive函数的执行并不会导致value变量的销毁 会一直常驻内存
    5. 由于闭包的特性 每一个传入下来的value都会常驻内存  相当于我们上一节讲的中间变量_name 目的是为了set和get的联动
    function defineReactive(data, key, value) {
        Object.defineProperty(data, key, {
            get() {
                console.log('您访问了属性', key)
                return value
            },
            set(newValue) {
                console.log('您修改了属性', key)
                value = newValue
            }
        })
    }
image.png

结构说明:这个地方实际上使用了闭包的特性,看下图,在每一次的defineReactive函数执行的时候,都会形成一块独立的函数作用域,传入的value 因为闭包的关系会常驻内存,这样一来,每个defineReactive函数中的value 会作为各自set和get函数操作的局部变量

image.png

响应式总结

数据的变化反应到视图

<body>
  <div id="app">
    <!-- p标签就是我们想要把每一次的数据都反映上来的视图 -->
    <p></p>
  </div>
  <script>
    // 1. 准备数据
    let data = {
      name: '林云龙'
    }
    // 2. 将数据转换成响应式 (数据发生变化之后操作我们的视图)
    Object.keys(data).forEach((key) => {
      defineReactive(data, key, data[key])
    })
    function defineReactive(data, key, value) {
      // 进行转换操作
      Object.defineProperty(data, key, {
        get() {
          console.log('您访问了属性', key)
          return value
        },
        set(newValue) {
          // set函数的执行 不会自动判断俩次修改的值是否相等
          // 显然如果相等 不应该执行变化的逻辑
          if (newValue === value) {
            return 
          }
          console.log('您修改了属性', key)
          value = newValue
          // 这里我们把最新的值 反映到视图中  这里是关键的位置
          // 核心:操作dom  就是通过操作dom api 把最新的值设置上去
          document.querySelector('#app p').innerText = newValue
          // 只要修改name属性 就会触发set函数的执行 内部我们把最新的值通过dom操作
          // 设置到dom内部  实现了数据的变化 反映到了视图中
        }
      })
    }
    document.querySelector('#app p').innerText = data.name
  </script>
</body>
image.png

我们将data中name属性的值作为文本渲染到标记了v-text的p标签内部,在vue中,我们把这种标记式的声明式渲染叫做指令

 v-text 声明式的指令版本实现
目标:一旦data中的name发生变化之后 标记了v-text的p标签的文本内容会立刻得到更新
   实现指令的核心:不管是指令也好还是插值表达式也好 它们都是数据和视图之间建立关联的‘标识’
   所以本质就是通过一定的手段找到符合标识的dom元素 然后把数据放上去 每当数据发生变化 就重新
   执行一遍放置数据的操作
   实现步骤:
    1. 先通过标识查找把数据放到对应的dom上显示出来
    2. 数据变化之后再次执行将最新的值放到对应的dom上 (数据变化之后再次执行compile函数即可)

代码:

<body>

  <div id="app">
     <p v-text="name"></p>
     <div v-text="name"></div>
     <a href="3" v-text="age"></a>
  </div>
   
  <script>
    let data = {
      name: '李云龙',
      age: 18
    }
    // 把data中的属性变成响应式的
    Object.keys(data).forEach((key) => {
      defineReactive(data, key, data[key])
    })
    function defineReactive(data, key, value) {
      // 进行转换操作
      Object.defineProperty(data, key, {
        get() {
          console.log('您访问了属性', key)
          return value
        },
        set(newValue) {
          // set函数的执行 不会自动判断俩次修改的值是否相等
          // 显然如果相等 不应该执行变化的逻辑
          if (newValue === value) {
            return
          }
          console.log('您修改了属性', key)
          value = newValue
          // 这里我们把最新的值 反映到视图中  这里是关键的位置
          // 核心:操作dom  就是通过操作dom api 把最新的值设置上去
          compile()
        }
      })
    }
    function compile () {
      let app = document.getElementById('app')
      let childNodes =  app.childNodes
      // console.log(childNodes)
      childNodes.forEach ((node) => {
        // console.log(node.nodeType)
        if (node.nodeType === 1) {
          // console.log(node)
          // 拿到所有的标签属性
          const attrs = node.attributes
          // console.log(attrs)
          Array.from(attrs).forEach(attr =>{
            console.log(attr)
            const nodeName = attr.nodeName
            const nodeValue = attr.nodeValue
            // nodeName  -> v-text  就是我们需要查找的标识
            // nodeValue -> name    data中对应数据的key
            console.log(nodeName, nodeValue)
            // 把data中的数据 放到满足标识的dom上
            if (nodeName === 'v-text') {
              console.log('设置值', node)
              node.innerText = data[nodeValue]
            }
          })
        }
      })
    }
    compile()
  </script>
</body>

v-model的实现原理

<body>
  <div id="app">
    <input type="text" v-model="name" style="width: 100%;" />
  <script>
    let data = {
      name: '李云龙',
      age: 17
    }
    // 把data中的属性变成响应式的
    Object.keys(data).forEach((key) => {
      defineReactive(data, key, data[key])
    })
    function defineReactive(data, key, value) {
      // 进行转换操作
      Object.defineProperty(data, key, {
        get() {
          console.log('您访问了属性', key)
          return value
        },
        set(newValue) {
          // set函数的执行 不会自动判断俩次修改的值是否相等
          // 显然如果相等 不应该执行变化的逻辑
          if (newValue === value) {
            return
          }
          console.log('您修改了属性', key)
          value = newValue
          // 这里我们把最新的值 反映到视图中  这里是关键的位置
          // 核心:操作dom  就是通过操作dom api 把最新的值设置上去
          compile()
        }
      })
    }
    // 1.通过标识查找把数据放到对应的dom上显示出来
    function compile() {
      let app = document.getElementById('app')
      // 拿到所有节点
      const childNodes = app.childNodes // 所有类型的节点包括文本节点和标签节点
      console.log(childNodes)
      // 刷选出来目标节点 -> p
      childNodes.forEach(node => {
        console.log(node.nodeType)
        if (node.nodeType === 1) {
          // 这里拿到的是标签节点
          console.log(node)
          // 刷选v-text属性 p id class (v-text)
          // 拿到所有的标签属性
          const attrs = node.attributes
          console.log(attrs)
          Array.from(attrs).forEach(attr => {
            console.log(attr)
            const nodeName = attr.nodeName
            const nodeValue = attr.nodeValue
            console.log(nodeName, nodeValue)
            // 实现v-model
            if (nodeName === 'v-model') {
              // 调用dom操作给input标签绑定数据
              node.value = data[nodeValue]
              // 监听input事件 在事件回调中 拿到最新的输入值 赋值给绑定的属性
              node.addEventListener('input', (e) => {
                let newValue = e.target.value
                // 反向赋值
                data[nodeValue] = newValue
              })
            }
          })
        }
      })
    }
    compile()
  </script>
</body>
image.png

上述代码的问题

发布订阅模式理解实现

node.innerText = data[dataProp]

为了保存当前的node和dataProp,我们再次设计一个函数执行利用闭包函数将每一次编译函数执行时候的node和dataProp都缓存下来,所以每一次数据变化之后执行的是这样的一个更新函数

() => {
  node.innerText = data[dataProp]
}

2.一个响应式数据可能会有多个视图部分都需要依赖,也就是响应式数据变化之后,需要执行的更新函数可能不止一个,如下面的代码所示,name属性有俩个div元素都使用了它,所以当name变化之后,俩个div节点都需要得到更新,那属性和更新函数之间应该是一个一对多的关系

<div id="app">
   <div v-text="name"></div>
   <div v-text="name"></div>
   <p v-text="age"></p>
   <p v-text="age"></p>
</div>

<script>
  let data = {
     name: 'cp',
     age: 18
  }
</script>

经过分析我们可以得到下面的存储架构图,每一个响应式属性都绑定了相对应的更新函数,是一个一对多的关系,数据发生变化之后,只会再次执行和自己绑定的更新函数

image.png

理解发布订阅模式(自定义事件)

// 增加dep对象 用来收集依赖和触发依赖
const dep = {
    map: Object.create(null),
    // 收集
    collect(dataProp, updateFn) {
      if (!this.map[dataProp]) {
        this.map[dataProp] = []
      }
      this.map[dataProp].push(updateFn)
    },
    // 触发
    trigger(dataProp) {
      this.map[dataProp] && this.map[dataProp].forEach(updateFn => {
        updateFn()
      })
    }
}

发布订阅模式优化架构(最终优化版)

<body>
  <div id="app">
    <p v-text="name"></p>
    <p v-text="name"></p>
    <span v-text="age"></span>
    <input type="text" v-model="age">
  </div>

  <script>
    // 引入发布订阅模式
    const Dep = {
      map: {},
      // 收集事件的方法
      collect(eventName, fn) {
        // 如果当前map中已经初始化好了 click:[]  
        // 就直接往里面push  如果没有初始化首次添加  就先进行初始化
        if (!this.map[eventName]) {
          this.map[eventName] = []
        }
        this.map[eventName].push(fn)
      },
      // 触发事件的方法
      trigger(eventName) {
        this.map[eventName].forEach(fn => fn())
      }
    }

    let data = {
      name: '柴达木',
      age: 17
    }
    // 把data中的属性变成响应式的
    Object.keys(data).forEach((key) => {
      defineReactive(data, key, data[key])
    })
    function defineReactive(data, key, value) {
      // 进行转换操作
      Object.defineProperty(data, key, {
        get() {

          return value
        },
        set(newValue) {
          // set函数的执行 不会自动判断俩次修改的值是否相等
          // 显然如果相等 不应该执行变化的逻辑
          if (newValue === value) {
            return
          }
          value = newValue
          // 这里我们把最新的值 反映到视图中  这里是关键的位置
          // 核心:操作dom  就是通过操作dom api 把最新的值设置上去
          // 在这里进行精准更新 -> 通过data中的属性名找到对应的更新函数依次执行
          Dep.trigger(key)
        }
      })
    }
    // 1.通过标识查找把数据放到对应的dom上显示出来
    function compile() {
      let app = document.getElementById('app')
      // 拿到所有节点
      const childNodes = app.childNodes // 所有类型的节点包括文本节点和标签节点
      childNodes.forEach(node => {
        if (node.nodeType === 1) {
          const attrs = node.attributes
          Array.from(attrs).forEach(attr => {
            const nodeName = attr.nodeName
            const nodeValue = attr.nodeValue
            // 实现v-text
            if (nodeName === 'v-text') {
              node.innerText = data[nodeValue]
              // 收集更新函数
              Dep.collect(nodeValue, () => {
                console.log(`当前您修改了属性${nodeValue}`)
                node.innerText = data[nodeValue]
              })
            }
            // 实现v-model
            if (nodeName === 'v-model') {
              // 调用dom操作给input标签绑定数据
              node.value = data[nodeValue]
              // 收集更新函数
              Dep.collect(nodeValue,()=>{
                node.value = data[nodeValue]
              })
              // 监听input事件 在事件回调中 拿到最新的输入值 赋值给绑定的属性
              node.addEventListener('input', (e) => {
                let newValue = e.target.value
                // 反向赋值
                data[nodeValue] = newValue
              })
            }
          })
        }
      })
    }
    compile()
    console.log(Dep)
  </script>
</body>
上一篇 下一篇

猜你喜欢

热点阅读