zoukankan      html  css  js  c++  java
  • Vue双向绑定原理

    参考文章:https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension

    最简单的 双向绑定

    //极简双向绑定
    let $input = document.querySelector("input");
    let $p = document.querySelector("p");
    
    // 数据
    let obj = {};
    
    //通过输入事件改变 数据
    $input.addEventListener("input", e => {
      obj.txt = e.target.value;
    });
    
    //通过 数据拦截 改变 视图
    Object.defineProperty(obj, "txt", {
      set(val) {
        obj._txt = val;
        $p.innerText = val;
        $input.value = val;
      },
    });
    

    真正的双向绑定的最终实现

    <div id="app">
      <input type="text" v-model="text" />
      {{ text }}
    </div>
    
    <script>
      var vm = new Vue({
        el: "#app",
        data: {
          text: "你好,世界",
        },
      });
    </script>
    

    将上面划分成几个子任务:

    1. 输入框 以及 文本节点与 data 中的数据绑定。 DOM 节点绑定
    2. 输入框内容变化时,data 中数据同步变化。 view -> model
    3. data 中的数据变化时,文本节点的内容同步变化。 model -> view

    任务 一 思路:

    循环#app 下的所有子节点,将其挂载到文档片段中,并进行数据的绑定初始化,最后将文档片段返回到 #app 中

    1. 创建一个 Vue 的构造函数

    /**
     * 创建一个Vue的 构造函数
     * @param {Object} options
     */
    function Vue(options) {
      this.data = options.data; //将用户声明的data挂载到实例上
      let id = options.el; // 静态私有属性,内部自己调用
    
      // 任务2
      // 劫持监听 data 中的属性
      observe(this.data, this);
    
      // 全部挂载到文档片段,在返回到 #app中,第二个参数 就是 Vue的实例,因为 data 已经挂载到实例上了
      let fragment = nodeToFragment(document.querySelector(id), this);
    
      document.querySelector(id).appendChild(fragment);
    }
    

    2. DomcumentFragment (文档片段)

    当需要添加多个 dom 元素时,如果先将这些元素添加到 DocumentFragment 中,再统一将 DocumentFragment 添加到页面,会减少页面渲染 dom 的次数,效率会明显提升。

    DocumentFragment (文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到 DOM 中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。
    使用 DocumentFragment 处理节点速度和性能远远优于直接操作 DOM
    Vue 进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持,通过 append 方法,DOM 中的节点会被自动删除)到 DocumentFragment 中,经过一番处理后,再将 DocumentFragment 整体返回插入挂载目标

    /**
     * 挂载到文档片段中
     * @param node ‘#app’
     */
    function nodeToFragment(node, vm) {
      let fragment = document.createDocumentFragment();
    
      let child = null;
      //循环#app的子孩子,直到没有子孩子,退出循环
      while ((child = node.firstChild)) {
        compile(child, vm); // 调用 数据绑定初始化函数
        fragment.appendChild(child); //把 挂载在 #app 上的子节点全部劫到文档片段中
      }
    
      return fragment;
    }
    

    3. 数据绑定的初始化

    /**
     * 数据绑定初始化
     * @param {HTMLDocument} node
     * @param {*} vm
     */
    function compile(node, vm) {
      let reg = /{{(.*)}}/;
    
      //如果节点类型为 元素
      if (node.nodeType == 1) {
        // 遍历元素的属性,看看是否有 v-model
        for (const attr of node.attributes) {
          if (attr.nodeName == "v-model") {
            let name = attr.nodeValue;
    
            // 任务 2
            // 监听 输入事件,并赋值,由于vm 的msg有被数据劫持,所以可以如此
            node.addEventListener("input", function (e) {
              vm[name] = e.target.value;
            });
    
            node.value = vm[name]; // 将 vm 里data的 对应的数据 的值给该 node
    
            // vm.data 被任务2的 vm 替换
            //node.value = vm.data[attr.nodeValue]; // 将 vm 里data的 对应的数据 的值给该 node
            node.removeAttribute("v-model"); //为了不让在前端看到该属性,赋值完后移除该属性
          }
        }
    
        // 元素节点的 子节点中如果有 其他类型的,递归
        for (const txtNode of node.childNodes) {
          compile(txtNode, vm);
        }
      }
    
      //如果节点类型为 文本
      if (node.nodeType == 3) {
        // 如果有 mustache 语法
        if (reg.test(node.nodeValue)) {
          let name = RegExp.$1; // 获取 正则匹配到的值,并去除两边的空白
          name = name.trim();
          node.nodeValue = vm[name]; //这里节点是文本节点,所以要用 nodeValue
        }
      }
    }
    

    任务 二 思路:

    向输入框输入数据时,首先触发 input 事件(或者 keyup、change 事件),在相应的事件处理程序中,我们获取输入框的 value 并赋值给 vm 实例的 msg 属性
    利用 defineProperty 将 data 中的 msg 设置为 vm 的访问器属性,因此给 vm.msg 赋值,就会触发 set 方法。
    在 set 方法中主要做两件事,第一是更新属性的值,第二是通知监听者修改数据

    /**
     * 劫持函数
     * @param {Vue实例}} vm
     * @param {属性名} key
     * @param {旧值} oldVal
     */
    function defineDataProperty(vm, key, oldVal) {
      Object.defineProperty(vm, key, {
        get() {
          return oldVal;
        },
        set(newVal) {
          if (oldVal === newVal) return;
          oldVal = newVal;
          console.log(oldVal);
        },
      });
    }
    
    /**
     * 劫持vm.data中所有的属性
     * @param {vm.data} data
     * @param {Vue实例} vm
     */
    function observe(data, vm) {
      Object.keys(data).forEach(key => {
        // 【注】要劫持的是 vm.data里的属性,但是实际确实让 vm 来控制。
        // 即: vm.msg  等价于 vm.data.msg
        defineDataProperty(vm, key, data[key]);
      });
    }
    
    • 为什么明明是 vm.data.msg 里的数据,却可以用 vm.msg 来操作?
      因为在进行 数据劫持,defineProperty 的时候,第一参数是 vm,第二参数为 msg,然后会创建一个 vm 的 msg 属性,所以可以看到 vm 和 vm.data 都有一个 msg 属性。(如果第一参数是 vm.data,那么就没 vm 什么事了)

    • 有了 数据劫持,就可以在 compile 函数中,给 nodeType 为元素的节点添加 监听 input 事件,当 value 改变时,去改变 vm.msg ,这样就可以 view -> model。数据层的数据就改变了


    任务 三 思路:

    vm 的 msg 属性发生了变化,但是 其他文本节点也没发生变化
    So,这里用 发布订阅者模式,定义 一对多的关系。让多个订阅者同时监听某一个对象,该对象发生改变时,就让发布者通知所有订阅者

    // 发布订阅者模式
    class Dep {
      constructor() {
        this.subs = [];
      }
    
      static target = null;
    
      add(sub) {
        this.subs.push(sub);
      }
      notify() {
        this.subs.forEach(v => {
          v.update();
        });
      }
    }
    
    class Watcher {
      constructor(vm, node, name) {
        Dep.target = this;
        this.node = node; // 绑定的文本节点
        this.name = name; // 绑定的data属性名
        this.vm = vm; // Vue实例
    
        this.update(); // 触发 vm 的属性的 get()访问器
        Dep.target = null;
      }
      update() {
        // 更新
        this.node.nodeValue = this.vm[this.name];
      }
    }
    

    每个属性都需要有一个 发布者, html 文档中的每个对应的文本节点都需要有一个 订阅者

    • 每次 defineProperty 之前就 new 一个发布者
    • compile 时候,每次遇到一个 文本节点,就 new 一个订阅者,之后构造函数中会马上将该订阅者与 dep 绑定

      如何绑定? new 订阅者时,会将自身赋值给 Dep.target,且会触发 vm 的 get 访问器,该访问器会 dep.add(Dep.target)


    总结

    主要原理是通过: 数据劫持 + 发布订阅者模式

    1. 先对数据进行劫持 Object.defineDataProperty且 new 一个发布者
      • 数据劫持第一个参数是 vm 实例,第二个参数是 key(data 数据里的 key)
      • get 访问器中,如果 Dep.target 有值,那么将其 添加至 dep.subs 中(Dep.target 会在 new 订阅者时有值)
      • set 访问器中,dep.notify()
    2. 解析文档中的元素、文本节点,(内部细节:依次遍历首位子元素,分析节点做处理,并将其转移至文档片段中,最后文档片段在移动到'#app'里)
      • 当遇到文本节点时,就 new 一个订阅者
      • 当遇到元素节点,遍历循环看是否有 v-model 属性,有的话,给其添加一个 input 事件,该事件将 值 value 赋值给了 vm 的属性,这样会触发 vm 属性的 set 访问器,会通知其他订阅者修改数据
  • 相关阅读:
    小程序云开发 -- 云函数定时触发器配置
    小程序echarts 构建中国地图并锚定区域点击事件
    微信小程序接入百度OCR(身份证识别)
    微信小程序开发技巧总结(三)-- 云开发时效数据刷新和存储 (access_token等)
    微信小程序仿朋友圈功能开发(发布、点赞、评论等功能)
    微信小程序开发技巧总结(二) -- 文件的选取、移动、上传和下载
    SpringBoot Jpa 自定义查询
    SpringBoot Jpa 分页查询最新配置方式
    【软工】提问回顾与个人总结
    【软工】[技术博客] 用Monaco Editor打造接近vscode体验的浏览器IDE
  • 原文地址:https://www.cnblogs.com/pengnima/p/13060133.html
Copyright © 2011-2022 走看看