Vue双向绑定原理及实现

参考自b站技术蛋老师的视频Vue.js 数据双向绑定的原理及实现_哔哩哔哩_bilibili

html部分如下:

<div id="app">
            <span>名字: {{name}}</span>
        <input type="text" v-model="name" />
        <span>工资:{{more.salary}}</span>
        <input type="text" v-model="more.salary" />
    </div>

<script>
    const vm = new Vue({
        el: '#app',
        data: {
            name: '阿毛',
            more: {
                salary: 1000
            }
        }
    })
</script>

1. 数据劫持 —— 监听实例里的数据Observer

就是想办法得到实例的所有属性(data里的name),注意对象的,就用递归遍历。然后用Obj.defineProperty进行数据劫持。

function Observer(data_instance) {
  // 递归出口,没有子属性或者不是对象时退出
  if (!data_instance || typeof data_instance !== "object") return;
  Object.keys(data_instance).forEach((key) => {
    // 使用defineProperty后属性里的值会被修改 需要提前保存属性的值
    let value = data_instance[key];
    // 递归劫持data里的子属性
    Observer(value);
    Object.defineProperty(data_instance, key, {
      enumerable: true,
      configurable: true,
      // 收集数据依赖
      get() {
        // console.log(`获取了属性值 ${value}`);
        return value;
      },
      // 触发视图更新
      set(newVal) {
        console.log(`修改了属性值`);
        value = newVal;
        // 处理赋值是对象时的情况 如果给name设置成{minzi:'xia'}时,就和上面一样了
        Observer(newVal);
      },
    });
  });
}

2. 模板解析 —— 替换DOM内容 把vue实例上的数据解析到页面上Complie

用了fragment这个对象,转化成文档DOM,然后把{{name}}换成data里的值,再重新添加回DOM。又是因为data中复杂对象难以解析。

function Complie(element, vm) {
  vm.$el = document.querySelector(element);
  // 使用文档碎片来临时存放DOM元素 减少DOM更新
  const fragment = document.createDocumentFragment();
  let child;
  // 将页面里的子节点循环放入文档碎片
  while ((child = vm.$el.firstChild)) {
    fragment.appendChild(child);
  }
  fragment_compile(fragment);
  // 替换fragment里文本节点的内容
  function fragment_compile(node) {
    // 使用正则表达式去匹配并替换节点里的{{}}
    const pattern = /\{\{\s*(\S+)\s*\}\}/;
    if (node.nodeType === 3) {
      // 获取正则表达式匹配文本字符串获得的所有结果
      const result_regex = pattern.exec(node.nodeValue);
      if (result_regex) {
        const arr = result_regex[1].split("."); // more.salary => ['more', 'salary']
        // 使用reduce归并获取属性对应的值 = vm.$data['more'] => vm.$data['more']['salary']
        const value = arr.reduce((total, current) => total[current], vm.$data);

        node.nodeValue =  node.nodeValue.replace(pattern, value);
      }
    }
    // 对子节点的所有子节点也进行替换内容操作
    node.childNodes.forEach((child) => fragment_compile(child));
  }
  // 操作完成后将文档碎片添加到页面
  // 此时已经能将vm的数据渲染到页面上 但还未实现数据变动的及时更新
  vm.$el.appendChild(fragment);
}

上面两步还是挺好懂得,不认识的API可以去查一下,不认识的变量在控制台输出一下就懂了。关键在于加了订阅发布者后就比较难理解了。

3.发布订阅模式——用于存放订阅者和通知订阅者更新

这里只是定义了发布订阅者

//依赖 —— 实现发布-订阅模式 用于存放订阅者和通知订阅者更新
class Dependency {
  constructor() {
    this.subscribers = []; // 用于收集依赖data的订阅者信息
  }
  addSub(sub) {
    this.subscribers.push(sub);
  }
  notify() {
    this.subscribers.forEach((sub) => sub.update());
  }
}

// 订阅者
class Watcher {
  // 需要vue实例上的属性 以获取更新什么数据
  constructor(vm, key, callback) {
    this.vm = vm;
    this.key = key;
    this.callback = callback;
    //临时属性 —— 触发getter 把订阅者实例存储到Dependency实例的subscribers里面
    Dependency.temp = this;
    // console.log(`用属性${key}创建订阅者`);
    key.split(".").reduce((total, current) => total[current], vm.$data);
    Dependency.temp = null; // 防止订阅者多次加入到依赖实例数组里
  }
  update() {
    const value = this.key.split(".").reduce((total, current) => total[current], this.vm.$data);
      console.log(value,'sss');
    this.callback(value);//这个value就是那个newValue
  }
}

4.结合第三步对前两个步骤进行更改

感觉干了几件事。

在劫持函数中创建发布者实例。

在模板解析的时候(结点值替换内容时),创建订阅者实例。在触发getter的时候添加到订阅者数组中(发布者中),然后在setter中通知订阅者更新。

最后就在解析函数中添加处理一下v-model属性了(这里其它的vue指令没有处理了),还有视图驱动数据的问题,也有很多细节。

function Observer(data_instance) {
  // 递归出口
  if (!data_instance || typeof data_instance !== "object") return;//没有子属性或者不是对象时退出
  // 每次数据劫持一个对象时都创建Dependency实例 用于区分哪个对象对应哪个依赖实例和收集依赖
  const dependency = new Dependency();
  Object.keys(data_instance).forEach((key) => {
    // 使用defineProperty后属性里的值会被修改 需要提前保存属性的值
    let value = data_instance[key];
    // 递归劫持data里的子属性
    Observer(value);
    Object.defineProperty(data_instance, key, {
      enumerable: true,
      configurable: true,
      // 收集数据依赖
      get() {
        console.log(`获取了属性值 ${value}`);
        //如果这个temp属性不为空,就将订阅者加到发布者的subscibers数组中
        //也就是保存订阅者消息
        //其实就是先把当前watcher存到dependency的临时变量,然后触发getter,
        //在getter中将watcher添加到dep中的订阅者数组
        Dependency.temp && dependency.addSub(Dependency.temp);
        // if(Dependency.temp){console.log(Dependency.temp);}
        return value;
      },
      // 触发视图更新
      set(newVal) {
        console.log(`修改了属性值`);
        value = newVal;
        // 处理赋值是对象时的情况 如果给name设置成{minzi:'xia'}时,就和上面一样了
        Observer(newVal);
        dependency.notify();//修改了数据,通知订阅者更新
      },
    });
  });
}
function Complie(element, vm) {
  vm.$el = document.querySelector(element);
  // 使用文档碎片来临时存放DOM元素 减少DOM更新
  const fragment = document.createDocumentFragment();
  let child;
  // 将页面里的子节点循环放入文档碎片
  while ((child = vm.$el.firstChild)) {
    fragment.appendChild(child);
  }
  fragment_compile(fragment);
  // 替换fragment里文本节点的内容
  function fragment_compile(node) {
    // 使用正则表达式去匹配并替换节点里的{{}}
    const pattern = /\{\{\s*(\S+)\s*\}\}/;
    if (node.nodeType === 3) {
      // 提前保存文本内容 否则文本在被替换一次后 后续的操作都会不生效
      // 打工人: {{name}}  => 打工人:西维 如果不保存后续修改name会匹配不到{{name}} 因为已经被替换
      const texts = node.nodeValue;
      // 获取正则表达式匹配文本字符串获得的所有结果
      const result_regex = pattern.exec(node.nodeValue);
      if (result_regex) {
        const arr = result_regex[1].split("."); // more.salary => ['more', 'salary']
        // 使用reduce归并获取属性对应的值 = vm.$data['more'] => vm.$data['more']['salary']
        const value = arr.reduce((total, current) => total[current], vm.$data);
        node.nodeValue = texts.replace(pattern, value);
        // 在节点值替换内容时 即模板解析的时候 添加订阅者
        // 在替换文档碎片内容时告诉订阅者如何更新 即告诉Watcher如何更新自己
        new Watcher(vm, result_regex[1], (newVal) => {
          node.nodeValue = texts.replace(pattern, newVal);
        });
        //(vm,key属性,callback回调函数)
      }
    }
    // 替换绑定了v-model属性的input节点的内容
    if (node.nodeType === 1 && node.nodeName === "INPUT") {
      const attr = Array.from(node.attributes);
      attr.forEach((item) => {
        if (item.nodeName === "v-model") {
          const value = item.nodeValue
            .split(".")
            .reduce((total, current) => total[current], vm.$data);
          node.value = value;
          new Watcher(vm, item.nodeValue, (newVal) => {
            node.value = newVal;
          });
          //视图改变数据
          node.addEventListener("input", (e) => {
            // ['more', 'salary']
            const arr1 = item.nodeValue.split(".");
            // ['more']
            const arr2 = arr1.slice(0, arr1.length - 1);
            // vm.$data.more
            const final = arr2.reduce(
              (total, current) => total[current],
              vm.$data
            );
            // vm.$data.more['salary'] = e.target.value
            final[arr1[arr1.length - 1]] = e.target.value;
          });
        }
      });
    }
    // 对子节点的所有子节点也进行替换内容操作
    node.childNodes.forEach((child) => fragment_compile(child));
  }
  // 操作完成后将文档碎片添加到页面
  // 此时已经能将vm的数据渲染到页面上 但还未实现数据变动的及时更新
  vm.$el.appendChild(fragment);
}

总之:这里基本上就是Vue2双向绑定的原理了,虽然不全面,只是这对上面的html做的。

全部代码:somethingday/提问/vuetiwen/双向绑定 at main · xianianmian/somethingday (github.com)

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值