计算机系统应用教程网站

网站首页 > 技术文章 正文

手动实现MVVM双向绑定(v-model原理)

btikc 2024-10-12 13:25:42 技术文章 44 ℃ 0 评论

v-model原理

如果觉得看文章不过瘾,可以直接观看视频版(建议先看文章,再看视频):

手动实现双向绑定(v-model原理) - 1

手动实现双向绑定(v-model原理) - 2

手动实现双向绑定(v-model原理) - 3


注意视频和文章稍稍有一些区别:

- 视频中是用es6的class类的方式创建对象的

- 视频中`初始化data数据`时没有做递归处理,此处应该以文章为准

- 视频中获取data数据没有使用reduce循环,此处应该以文章为准


目标

  • 掌握 Object.defineProperty的使用
  • 掌握js的观察者模式
  • 手动实现v-model
  • 1- 前置知识

    1. Object.defineProperty

  • 作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性 Object.defineProperty(对象,属性,描述符对象)
  • 为对象的属性直接赋值的情况下,对象的属性也可以修改和删除,但是通过 Object.defineProperty()定义属性,通过描述符的设置可以进行更精准的控制对象属性
  •   <script>
      var Person={}
      var reactiveName = "zhangsan"
    
      Object.defineProperty(Person, 'name', {
          // 可配置
          configurable: true, 
          // 可枚举
          enumerable: true,
          // 访问器属性`get`
          // 读取`Person.name`的时候触发的方法
          get () {
    
            console.log('执行get')
            return reactiveName
          },
          // 访问器属性`set`
          // 修改`Person.name`的时候触发的方法
          set (newValue) {
            console.log('执行set')
            reactiveName = newValue
    
          }
      })
    
    
      console.log(Person.name); // 'zhangsan'
      Person.name = 'lisi';
      console.log(Person.name); // 'lisi'
      </script>

    2. 观察者模式

    <script>
        // 订阅者
        class Dep {
          constructor() {
            this.watchers = []
          }
          // 添加观察者
          addWatcher (watcher) {
            this.watchers.push(watcher)
          }
    
          // 通知
          notify  () {
            this.watchers.forEach(watcher => {
              watcher.update()
            })
          }
    
        }
    
    
        // 观察者
        class Watcher {
          constructor(callback) {
            this.callback = callback
          }
          update () {
            console.log('update');
            this.callback();
          }
        }
    
    
        // 创建订阅者
        const dep = new Dep();
        // 创建观察者
        const watcher1 = new Watcher(() => console.log('watcher1'));
        const watcher2 = new Watcher(() => console.log('watcher2'));
        // 添加观察者
        dep.addWatcher(watcher1)
        dep.addWatcher(watcher2)
        // 触发通知
        dep.notify();
    </script>

    3. 小结

  • Object.defineProperty用来定义对象的属性,可以配置getter和setter访问器
  • 观察者模式就是有两个订阅者和观察者对象,订阅者可以收集观察者,并且在合适的时机触发观察者执行更新操作
  • 实现v-model时,就是在get方法里面收集观察者,set方法里面触发更新操作
  • 2- 实现v-model

    1. 采用类似Vue的调用方式

      <div id="app">
        <input type="text" v-model="msg" />
      </div>
    
      <script>
      const vm = new MyVue({
        el: '#app',
        data: {
          msg: 'abc'
        }
      })
      </script>

    所以我们要创建MyVue对象

    <script>
      class MyVue {
        constructor({el, data}) {
          this.container = document.querySelector(el);
          this.data = data;
        }
      }
    </script>

    2. 初始化data数据

  • 使用Object.defineProperty定义data中的每个属性
  • <script>
      class MyVue {
        constructor({el, data}) {
          this.container = document.querySelector(el);
          this.data = data;
    
          // 初始化data
          this.initData(this, this.data)
        }
    
        initData(vm, data) {
          for (let key in data) {
            let initVal = data[key];
    
            Object.defineProperty(vm, key, {
              get() {
                return initVal
              },
              set(val) {
                initVal = val;
              }
            })
            // 判断是否是对象,递归调用initData
            if (Object.prototype.toString.call(data[key]) === "[object Object]") {
              this.initData(vm[key], data[key])
            }
          }
        }
      }
    </script>
    <script>
      const vm = new MyVue({
        el: '#app',
        data: {
          msg: 'abc'
        }
      });
    
      console.log(vm.msg) // abc
    </script>

    3. 初始化v-model表单

    <script>
        class MyVue {
          constructor({ el, data }) {
            this.container = document.querySelector(el);
            this.data = data;
    
            // 初始化data
            this.initData(this, this.data)
    
            // 初始化v-model表单
            this.initVModel();
          }
          initVModel () {
            // 获取所有v-model元素
            const nodes = this.container.querySelectorAll('[v-model]');
    
            nodes.forEach(node => {
              const key = node.getAttribute('v-model');
    
              // 初始化赋值
              node.value = this[key];
    
              // 监听输入事件
              node.addEventListener('input', ev => {
                this[key] = ev.target.value;
              }, false)
    
    
            });
          }
        }
    </script>  

    上面的代码只能拿到data中第一层的数据,如果是这样绑定:

    <input type="text" v-model="obj.a" />

    那其实获取 this['obj.a']是获取不到数据的,所以还需要重写一个获取data数据的方法:

    getData(str) {
      const arr = str.split('.'); // ["obj", "a"]
    
      const res = arr.reduce((target, item) => {
        // 第一次遍历 target等于data
        return target[item] // return之后,target就等于target[item] ===> data.obj
        // 第二次遍历  target等于data.obj
        // return之后,target就等于data.obj.a
      }, this)
    
      return res;
    },    
    initVModel () {
      // 获取所有v-model元素
      const nodes = this.container.querySelectorAll('[v-model]');
    
      nodes.forEach(node => {
        const key = node.getAttribute('v-model');
    
        // 初始化赋值
        // node.value = this[key]; // 如果key是"obj.a"这种字符串,获取不到数据
        node.value = this.getData(key); 
    
        // 监听输入事件
        node.addEventListener('input', ev => {
          // this[key] = ev.target.value; // 也要处理key是"obj.a"格式的字符串
          const arr = key.split("."); // ["obj", "a"]
    
          // 如果arr只有一个元素,直接赋值
          if (arr.length === 1) {
            this[key] = node.value;
            return;
          }
    
          // 获取倒数第二级的对象(对于`obj.a`,那就获取this.obj)
          const res = this.getData(key.substring(0, key.lastIndexOf("."))) // this.obj
    
          // 赋值最后一级(this.obj.a = node.value)
          res[arr[arr.length - 1]] = node.value;
        }, false)
    
    
      });
    },

    此时已经实现了视图变化触发模型的变化,接下来实现模型的变化触发视图的变化

    4. 模型的变化触发视图更新

    1. 引入观察者模式

        // 订阅者
        class Dep {
          constructor() {
            this.watchers = []
          }
          // 添加观察者
          addWatcher (watcher) {
            this.watchers.push(watcher)
          }
    
          // 通知
          notify  () {
            this.watchers.forEach(watcher => {
              watcher.update()
            })
          }
    
        }
    
    
        // 观察者
        class Watcher {
          constructor(callback) {
            this.callback = callback
          }
          update () {
            console.log('update');
            this.callback();
          }
        }

    2. 通知触发的时机

    模型驱动视图更新,就是当修改 vm.msg时(比如 vm.msg="123"),需要触发dom更新。所以我们需要在set方法里面调用 dep.notify()

    // 初始化data
    initData(vm, data) {
      for (let key in data) {
        let initVal = data[key];
        //++++++++++++++++++++++++++++++
        const dep = new Dep(); 
        //++++++++++++++++++++++++++++++
        Object.defineProperty(vm, key, {
          get() {
            return initVal
          },
          set(val) {
            initVal = val;
            //++++++++++++++++++++++++++++++
            // 通知视图更新
            dep.notify();
            //++++++++++++++++++++++++++++++
          }
        })
        // 判断是否是对象,递归调用initData
        if (Object.prototype.toString.call(data[key]) === "[object Object]") {
          this.initData(vm[key], data[key])
        }
      }
    }

    3. 创建观察者

    观察者需要更新dom,所以需要在初始化v-model表单的时候创建观察者

    initVModel () {
      // 获取所有v-model元素
      const nodes = this.container.querySelectorAll('[v-model]');
    
      nodes.forEach(node => {
        const key = node.getAttribute('v-model');
    
        // 初始化赋值
        // node.value = this[key]; // 如果key是"obj.a"这种字符串,获取不到数据
        node.value = this.getData(key); 
    
        //++++++++++++++++++++++++++++++
        // 创建观察者
        const watcher = new Watcher(() => {
          node.value = this.getData(key)
        })
        //++++++++++++++++++++++++++++++
    
        // 监听输入事件
        node.addEventListener('input', ev => {
          // this[key] = ev.target.value; // 也要处理key是"obj.a"格式的字符串
          const arr = key.split("."); // ["obj", "a"]
    
          // 如果arr只有一个元素,直接赋值
          if (arr.length === 1) {
            this[key] = node.value;
            return;
          }
    
          // 获取倒数第二级的对象(对于`obj.a`,那就获取this.obj)
          const res = this.getData(key.substring(0, key.lastIndexOf("."))) // this.obj
    
          // 赋值最后一级(this.obj.a = node.value)
          res[arr[arr.length - 1]] = node.value;
        }, false)
    
    
      });
    },

    4. 添加观察者

    当创建了观察者后,应该马上添加到订阅者中,但是订阅者怎么知道已经创建了观察者呢?

  • 这时我们在观察者的构造函数里面获取一下data数据,就会执行 Object.defineProperty定义的的get方法。所以我们在get方法里面添加观察者。
  • // 创建观察者时传入this和key
    const watcher = new Watcher(this, key, () => {
      node.value = this.getData(key)
    })
    
    // 观察者
    class Watcher {
      constructor(vm, key, callback) {
        this.callback = callback;
        // 获取data数据,触发get方法
        vm.getData(key);
      }
      update () {
        console.log('update');
        this.callback();
      }
    }
    
    
    // get方法中添加观察者
    get() {
        dep.addWatcher(watcher)
        return initVal
    },

    5. 将观察者实例赋值给Dep的某个属性

    上面 dep.addWatcher(watcher)中的 watcher并不存在,我们需要在观察者的构造函数中先将观察者实例赋值给Dep的某个属性,再获取data数据

    // 观察者
    class Watcher {
      constructor(vm, key, callback) {
        this.callback = callback;
    
        //将观察者实例赋值给Dep的target属性
        Dep.target = this;
        // 获取data数据,触发get方法
        vm.getData(key);
      }
      update () {
        console.log('update');
        this.callback();
      }
    }
    
    
    
    // get方法
    get() {
        dep.addWatcher(Dep.target)
        return initVal
    },

    6. 避免反复添加观察者

    上面已经实现了模型数据驱动视图更新,但是我们发现update方法会执行多次,那是因为 dep.addWatcher(Dep.target)执行了多次,所以dep的watchers数组中有多个观察者,所以update方法会执行多次。解决方法就是获取data数据后重置Dep.target。

    // 观察者
    class Watcher {
      constructor(vm, key, callback) {
        this.callback = callback;
    
        //将观察者实例赋值给Dep的target属性
        Dep.target = this;
        // 获取data数据,触发get方法
        vm.getData(key);
        // 获取data数据后重置Dep.target,避免重复添加观察者
        Dep.target = null;
      }
      update () {
        console.log('update');
        this.callback();
      }
    }
    
    
    // get方法
    get() {
        Dep.target && dep.addWatcher(Dep.target)
        return initVal
    },

    7. data多次设置为同样的值时不需要触发更新

    set(val) {
    
      // 如果前后两次设置的值相等,就不触发更新
      if (initVal === val) {
        return;
      }
    
      initVal = val;
    
      dep.notify(); // 通知视图更新
    }

    完整代码

    // 订阅者
    class Dep {
      constructor() {
        this.watchers = []
      }
      // 添加观察者
      addWatcher(watcher) {
        this.watchers.push(watcher)
      }
    
      // 通知
      notify() {
        this.watchers.forEach(watcher => {
          watcher.update()
        })
      }
    
    }
    
    
    // 观察者
    class Watcher {
      constructor(vm, key, callback) {
        this.callback = callback
    
        // 第二件事,把当前实例传给get方法
        Dep.target = this;
    
        // 第一件事,获取当前的data
        const val = vm.getData(key)
    
        // 为了防止重复添加观察者,添加完之后马上清空
        Dep.target = null;
      }
      update() {
        console.log('update');
        this.callback();
      }
    }
    
    
    class MyVue {
      constructor({
        el,
        data
      }) {
        this.container = document.querySelector(el);
        this.data = data;
    
        // 初始化数据
        this.initData(this, this.data)
    
        // 初始化v-model表单
        this.initVModel()
      }
    
      initData(vm, data) {
        for (let key in data) {
          let initVal = data[key];
          const dep = new Dep();
          Object.defineProperty(vm, key, {
            get() {
              // 只要获取data数据,就会进入get方法,然后添加观察者
              Dep.target && dep.addWatcher(Dep.target)
              return initVal
            },
            set(val) {
    
              // 如果前后两次设置的值相等,就不触发更新
              if (initVal === val) {
                return;
              }
    
    
              // console.log(key + '被修改数据')
              initVal = val;
    
              dep.notify(); // 通知视图更新
            }
          })
          // 判断是否是对象,递归调用initData
          if (Object.prototype.toString.call(data[key]) === "[object Object]") {
            this.initData(vm[key], data[key])
          }
        }
    
      }
    
      initVModel() {
        // 获取所有的v-model表单
        const nodes = this.container.querySelectorAll('[v-model]')
        // console.log(nodes)
    
        nodes.forEach(node => {
          // 获取v-model属性的值
          const key = node.getAttribute('v-model')
    
    
          // 把表单创建成观察者
          new Watcher(this, key, () => {
            node.value = this.getData(key)
          })
    
    
    
          // console.log(123, key, this[key])
          const val = this.getData(key)
          // console.log(234234, val)
    
          // 给表单赋值
          node.value = val
    
          // 视图驱动数据更新:监听表单事件,修改data。
          node.addEventListener('input', () => {
            // this[key] = node.value   // key === "obj.b"
    
            const arr = key.split("."); // ["obj", "b"]
    
            // 如果arr只有一个元素,直接赋值
            if (arr.length === 1) {
              this[key] = node.value;
              return;
            }
    
            // let res = this.getData(key) // this.obj.b ===> 2
            const res = this.getData(key.substring(0, key.lastIndexOf("."))) // this.obj
    
            // res = node.value;
            res[arr[arr.length - 1]] = node.value;
          })
    
        })
      }
    
      // 传入'a.b.c'这种格式的字符串,获取this.a.b.c的值
      getData(str) {
        const arr = str.split('.'); // ["obj", "a"]
    
        const res = arr.reduce((target, item) => {
          // 第一次遍历 target等于data
          return target[item] // return之后,target就等于target[item] ===> data.obj
          // 第二次遍历  target等于data.obj
          // return之后,target就等于data.obj.a
        }, this)
    
        return res;
      }
    }

    总结

  • Object.defineProperty
  • 观察者模式
  • v-model手动实现
  • 通过 Object.defineProperty定义data的所有属性,在get方法中收集观察者,在set方法中触发观察者更新DOM
  • 本文暂时没有评论,来添加一个吧(●'◡'●)

    欢迎 发表评论:

    最近发表
    标签列表