vue2响应式原理+模拟实现v-model

效果

简述原理

配置对象传入vue实例

模板解析,遍历出所有文本节点,利用正则替换插值表达式为真实数据

data数据代理给vue实例,以后通过this.xxx访问

给每个dom节点增加观察者实例,由观察者群组管理,内部每一个键值含有多个对不同dom的观察者

data数据劫持,给data的每个属性增加get和set函数,当值改变时触发观察者的update方法,更新所有与当前属性值相关的dom元素

劫持数据,说的挺好听的,就是加工数据嘛,多了set变化触发了模板重新渲染,该渲染方式使用观察者模式,获取观察者收集的各个dom的所有属性 div,观察的属性,div的属性textContent,同时根据最新值渲染模板

div.textContent=vm[key]

html

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <!-- <script src="./vue.js"></script> -->
</head>

<body>
  <div id="app">
    {{ name }} {{age}}
    <h1>{{age}}</h1>
    <button @click="cli">按钮</button>
    <input type="text" v-model="name">
  </div>
</body>
<script src="./vue.js">
</script>
<script>

  new Vue({
    el: '#app',
    data: {
      name: 'Zwwwww',
      age: 18,
    },
    methods: {
      cli() {
        console.log(this);
        console.log(this.age);
      }
    },
  })

</script>

</html>

js代码

js 复制代码
class Vue {
  constructor(options) {
    // 获取配置对象的节点,存放在vm$el身上
    this.$el = document.querySelector(options.el)
    // console.log(this.$el)
    // 将配置对象的data对象代理到$data
    this.$data = options.data
    // 获取配置对象的method值,
    // vue实例监听,当触发了方法执行对应函数
    this.$methods = options.methods
    // 代理数据,后续通过this调用data对象的值
    this.$allWatcher = {}
    this.proxyData()
    // 劫持数据,为其增加观察者监视数据变化引起视图渲染
    this.observe()
    // 收集所有观察者,用对象的属性存放
    this.compile(this.$el)
  }

  // 数据代理到vue实例身上,后续this调用方法和data值
  proxyData() {
    // 遍历$data身上所有key
    for (let key in this.$data) {
      // 数据代理给vue实例,this
      Object.defineProperty(this, key, {
        // 使用get和set后续触发获取值和设置值做额外操作
        get() {
          // 返回当前data对应的key属性值
          return this.$data[key]
        },
        set(value) {
          // 设置新值给当前属性
          this.$data[key] = value
        },
      })
    }
  }
  // js数据替换{{name}},模板解析
  compile(node) {
    // 遍历根节点下的所有节点
    node.childNodes.forEach((item, index) => {
      //递归元素节点,
      //如果还没到文本节点,也就是说元素节点内还有元素节点
      //则继续递归,直到元素节点没有子节点

      //第二种可能,如果为元素元素节点,判断是否有@click属性,并获取值
      //该值为绑定的methods方法
      if (item.nodeType === 1) {
        if (item.childNodes.length > 0) {
          this.compile(item)
        }
        if (item.hasAttribute('@click')) {
          let domKey = item.getAttribute('@click')
          // console.log('我是dom标签的key', domKey)
          // 设置监听器,如果被点击了,触发配置对象中的method函数
          item.addEventListener('click', () => {
            // 通过模板获取的属性值方法命,调用函数
            // 由于$methods只是引用地址,this指向还是原来的methods
            // 我们这里使用call来绑定他的上下文this,也就是绑定他的调用者
            // 在html部分我们就可以使用this.$data.age来获取vue实例上的数据
            // 如果我们想直接this.age 就需要将data代理到vue实例身上
            this.$methods[domKey.trim()].call(this)
          })
        }
        if (item.hasAttribute('v-model')) {
          let vmodelKey = item.getAttribute('v-model').trim()
          // console.log('我是v-model的key', vmodelKey)
          // 设置监听器,如果被点击了,触发配置对象中的method函数
          // 先单向给input框设置值
          item.value = this.$data[vmodelKey]
          item.addEventListener('input', () => {
            console.log('用户正在输入')
            // 每次输入时将输入框的值重新赋给data对象属性值,完成双向绑定
            this.$data[vmodelKey] = item.value
            console.log(this.$data[vmodelKey])
            // 数据更新的同时重新解析模板
            // 这里使用观察者类观察数据变化所作出的响应
          })
        }
      }
      // 判断是否为文本节点,nodeType == 3
      // console.log(item.nodeType)
      // 如果是文本节点,进行数据替换
      // 如果不是文本节点,为元素节点则往里递归遍历文本节点
      if (item.nodeType === 3) {
        // 定义正则,替换{{xxx}}形式的字串为data下的属性值
        let reg = /\{\{(.*?)\}\}/g
        // 获取原本标签里的值,后续进行替换
        let text = item.textContent
        // console.log(text)
        item.textContent = text.replace(reg, (match, dataKey) => {
          // 先将dataKey去空格处理
          dataKey = dataKey.trim()
          // match为匹配到的整体,datakey为捕获到的子内容(.*?)
          //我们这里只需获取dataKey对应的值并塞入即可
          // console.log(match, dataKey)
          // 返回值作为替换内容 去除dataKey的前后空格

          // 增加观察者,传vue实例对象,data属性,item标签,标签属性
          // 相当于给每个文本节点都添加了一个观察者
          // 将所有观察者收集到vue实例上,在数据发生变化时调用观察者的update方法
          let watcher = new Watcher(this, dataKey, item, 'textContent')
          // 先进行判断观察者群组里是否有该节点的观察者
          // 如果有,就push添加,因为一个dataKey可能有多个模板使用
          // 举个例子,name属性可能在div1里使用也在div2里使用
          // 也就是将多个文本节点与同个datakey绑定
          if (this.$allWatcher[dataKey]) {
            this.$allWatcher[dataKey].push(watcher)
          }
          // 如果没有该属性的观察者存在,则新建空数组,push该观察者进入
          else {
            this.$allWatcher[dataKey] = []
            this.$allWatcher[dataKey].push(watcher)
          }
          return this.$data[dataKey]
        })
      }
    })
  }
  observe() {
    console.log('开始劫持')
    // 遍历所有的key,对其data数据劫持,值增加响应式功能
    for (let key in this.$data) {
      // 先获取value,否则数据重新定义后值会丢失
      // 此处的value变量不会随着observe方法的结束而销毁
      // 与内部匿名函数get和set作为闭包永远绑定在一起
      // 同时value值是对$data的一个引用,修改value值会引起$data变化
      let value = this.$data[key]
      // 保存一份vue的引用_this=this,
      // 防止后续在组件外部,也就是input输入框
      // 此时触发的set为一个闭包环境,上下文变成由defineproper定义的this.$data数据对象
      // 此时找不到vue实例作为上下文,对key和其他数据的引用也会失效
      let _this = this
      Object.defineProperty(this.$data, key, {
        get() {
          console.log('有人要获取劫持数据值', value)
          // 返回上面存储的value值
          // 由于是响应式的,只有当观察到数据变化时所以才接触数据
          // 其value值作用域也作用在劫持过程中
          return value
        },
        set(newValue) {
          console.log('劫持到数据,修改值为', newValue)
          console.log('劫持前的数据为', value)
          value = newValue
          // 更新值的同时进行模板更新
          // 由于观察者队列含有观察者来观察不同属性管理的若干个模板
          // 调用该属性值下所有模板观察者即可,
          // 只要属性值变化,该属性值下的所有观察者重新渲染模板
          console.log(_this.$allWatcher)
          console.log(_this.$allWatcher[key])
          _this.$allWatcher[key].forEach((watcher, index) => {
            watcher.update()
          })
        },
      })
    }
    console.log('劫持成功')
  }
}

class Watcher {
  constructor(vm, key, node, attr) {
    this.vm = vm
    this.key = key
    this.node = node
    this.attr = attr
  }
  //  item.textContent = this.$data[dataKey.trim()]
  update() {
    console.log('开始渲染')
    // 将原始dom标签内容值替换为 data里的属性值
    this.node[this.attr] = this.vm[this.key]
  }
}

代码参考

VUE双向绑定原理分析~实现视图和数据的双向绑定~_哔哩哔哩_bilibili

相关推荐
燃先生._.17 分钟前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖1 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235241 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240252 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar2 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人3 小时前
前端知识补充—CSS
前端·css
GISer_Jing3 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245523 小时前
吉利前端、AI面试
前端·面试·职场和发展
理想不理想v3 小时前
webpack最基础的配置
前端·webpack·node.js