美文网首页
每周一更 -- 手动实现vue2.0双向绑定原理简化版(data

每周一更 -- 手动实现vue2.0双向绑定原理简化版(data

作者: 迪迪妮粑粑 | 来源:发表于2025-04-24 14:17 被阅读0次
vue2.0可能对很多人来说过时了,但是思想不过时,回顾一下前人的思想,谢谢前人的肩膀,很菜的我看了三天好似明白了一点,下周前端看什么,可以留言,看看能不能看懂后讲清楚
盗图1
首先上图,双向绑定的核心是实现这个图,名词解释一下
  • Observer观察者:劫持data,通过Object.defineProperty让data的数据变成响应式;
    什么叫劫持,就是用支付宝付钱,本质上就是用银行卡付钱,支付宝付了钱,银行卡就会扣掉;
    为什么不直接用银行卡付钱,因为从支付宝过一下会给积分,但是支付宝也可以在付完钱后额外做一些事情,比如推个广告给你;

    大神说响应式不是双向数据绑定,不能混为一谈

    • 响应式
      数据模型仅仅是普通的JS对象,而当我们修改数据时,视图会进行更新,避免了繁琐的DOM操作,提高开发效率。

    • 双向数据绑定
      数据改变,视图改变;视图改变,数据改变

  • Compile模板编译:就是{{name}} ,v-text="name"怎么提取出来的过程,到这里也没有完成双向绑定;此外,v-model="name",提取到name后,会node.addEventListener("input", () => {})去修改this.$data.name,此时实现了视图改变,数据改变

  • Watcher订阅者:需要结合Dep一起说

  • Dep依赖管理:这个有点难,结合下面的代码看,做了这几件事

    1. Object.defineProperty的getter被触发的时候(就是访问this.$data.name),会通知Dep,你快存一下sub订阅者(就是Watcher的实例),然后就执行了Dep 的depend()

    2. 然后这个sub订阅者(就是Watcher的实例)又是哪里跑出来的,Dep.target就是Watcher的实例,在Compile里,把{{name}}提取出name后,就会new Watcher;

    3. new Watcher的时候会传入key也就是name,传入vue实例,传入当前name的值,传入更新视图的回调函数callback

    4. 传入callback后,执行Dep.target = this操作,把当前实例赋指向到Dep.target;

    5. 重点来了,后面this.oldValue = getter()这一步,会访问$data.name,
      由于步骤1中data里的name被劫持,触发了Object.defineProperty的getter,而getter触发了Dep 的depend(),
      depend方法内的Dep.target就是Watcher实例中的Dep.target即Watcher实例本身,this.addSub(Dep.target)就把模板里name的订阅实例Watcher和Observer中Object.defineProperty的name属性的setter关联到Dep中

    6. Observer中let dep = new Dep()对Dep实例化,并且通过getter和setter的函数闭包,持久化到内存中不被释放,当修改name的时候,setter自然通过闭包能找到对应的dep实例里的subs,循环执行subs就可以更新数据了;这样数据一修改,setter就会通知dep实例执行callback更新模板{{name}} ,v-text等,实现了数据改变,视图改变

    7. 结合下面代码块慢慢看

class Dep {
  constructor() {
    this.subs = [];
  }
  // 添加订阅者
  addSub(sub) {
    if (sub && sub.update) {
      this.subs.push(sub);
    }
  }

  depend() {
    if (Dep.target) {
      this.addSub(Dep.target);
    }
  }

  // 发布通知
  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}
class Watcher {
  constructor(vm, key, getter, callback) {
    //指向vue实例
    this.vm = vm;
    //data中的属性名
    this.key = key;
    this.getter = getter;
    //回调函数负责更新视图
    this.callback = callback;
    Dep.target = this; // 标记当前 Watcher 实例为“正在收集依赖”
    this.oldValue = getter();
    Dep.target = null; // 重置,避免后续依赖收集错误
  }

  update() {
    let newValue = this.getter();
    if (this.oldValue === newValue) {
      return;
    }
    this.oldValue = newValue;
    this.callback(newValue);
  }
}
class Observer {
  constructor(data) {
    this.look(data);
  }

  look(data) {
    if (!data || typeof data !== "object") {
      // 劫持的数据必须是对象
      return;
    }

    Object.keys(data).forEach((key) => {
      this.defineReactive(data, key);
    });
  }

  defineReactive(obj, key) {
    let _this = this;
    let val = obj[key];
    _this.look(val); //如果val还是对象,循环劫持深层属性

    let dep = new Dep(); //订阅者收集发布中心

    Object.defineProperty(obj, key, {
      get() {
        console.log("getter-------", key, val);
        // 添加订阅者
        dep.depend();
        return val;
      },
      set(newVal) {
        if (newVal === val) {
          // 减少setter调用
          return;
        }
        console.log("setter--------", newVal);
        val = newVal;
        _this.look(newVal);
        // 数据变化后,要发布通知给订阅者
        dep.notify();
      },
    });
  }
}

关于computed简单处理

computed和data是一样的实现方式,只是在Compile时,Watcher当前值的入参是函数计算后的结果,因此Watcher的当前值的入参写的是一个函数setter,这样可以保持Watcher的逻辑一致;

但是有个问题,name只能触发name的双向绑定,doubleCount是一个计算值,并不靠自身修改导致doubleCount改变,而需要通过data.count去改变,为什么依然不用额外增加代码呢?

因为模板{{doubleCount}}调用doubleCount的时候,new Watcher中Dep.target指向【doubleCount的Watcher订阅者实例】,doubleCount的getter会收集【doubleCount的Watcher订阅者实例】到Dep实现绑定;

并且,doubleCount的computed内部有访问了this.count,触发了count的getter去收集,
但是此时的Dep.target指向的是【doubleCount的Watcher订阅者实例】;

因此当count修改时,count的setter被触发后通知dep.notify遍历subs,subs里会处理【doubleCount的Watcher订阅者实例】的回调函数,去更新doubleCount

class Compiler {
  constructor(vm) {
    this.vm = vm;
    this.compile(vm.$el);
  }

  //判断元素是否是指令
  isDirective(attrName) {
    return attrName.startsWith("v-");
  }
  // 判断元素是否是点击事件
  isClickEvent(attrName) {
    return attrName.startsWith("@click");
  }
  //判断节点是否是文本节点
  isTextNode(node) {
    return node.nodeType === 3;
  }
  //判断节点是否是元素节点
  isElementNode(node) {
    return node.nodeType === 1;
  }

  // 编译模板,处理文本节点和元素节点
  compile(el) {
    // 获取所有子元素
    let childNodes = el.childNodes;
    Array.prototype.forEach.call(childNodes, (node) => {
      if (this.isTextNode(node)) {
        // 处理文本节点
        this.compileText(node);
      } else if (this.isElementNode(node)) {
        // 处理元素节点
        this.compileElement(node);
        // 递归编译子节点
        if (node.childNodes && node.childNodes.length) {
          this.compile(node);
        }
      }
    });
  }

  // 处理文本节点
  compileText(node) {
    let textContent = node.textContent;
    let reg = /\{\{(.+?)\}\}/;
    if (reg.test(textContent)) {
      // 判断过滤掉回车等
      let match = textContent.match(reg);
      let result = match ? match[1] : null;
      let newText = textContent;
      let key = result.trim(); //除去前后空格
      const keys = key.split(".");
      if (!this.vm.computed[keys[0]]) {
        // reduce累加器,初始化赋值,第二个参数为初始值
        const val = keys.reduce(($data, key) => {
          // console.log(key, "-----------", $data?.[key]);
          return $data?.[key];
        }, this.vm.$data);
        newText = newText.replace(match[0], val ?? "");
        node.textContent = newText;
        //创建watcher对象,当数据变化时更新视图
        new Watcher(
          this.vm,
          key,
          () => {
            return this.vm.$data[key];
          },
          (newValue) => {
            node.textContent = newValue;
          }
        );
      } else {
        // computed
        new Watcher(
          this.vm,
          keys[0],
          () => {
            const val = this.vm.computed[keys[0]].call(this.vm);
            node.textContent = val;
            return val;
          },
          (newValue) => {
            node.textContent = newValue;
          }
        );
      }
    }
  }
  // 处理元素节点
  compileElement(node) {
    //遍历所有属性节点
    Array.prototype.forEach.call(node.attributes, (attr) => {
      //判断是否是指令
      let attrName = attr.name;
      if (this.isDirective(attrName)) {
        attrName = attrName.substr(2);
        let key = attr.value;
        this.directive(node, key, attrName);
      } else if (this.isClickEvent(attrName)) {
        let eventName = attr.value;
        let eventFn = this.vm.methods[eventName];
        node.addEventListener("click", () => {
          eventFn.call(this.vm);
        });
      }
    });
  }

  // 处理指令类
  directive(node, key, attrName) {
    let updateFn = this[attrName + "Directive"];
    updateFn && updateFn.call(this, node, this.vm.$data[key], key);
  }

  //处理v-text指令
  textDirective(node, value, key) {
    // 处理页面初始化时的文本内容值填充视图
    node.textContent = value;
    // 处理监听到变化后的文本内容值填充视图
    new Watcher(
      this.vm,
      key,
      () => {
        return this.vm.$data[key];
      },
      (newValue) => {
        node.textContent = newValue;
      }
    );
  }

  //处理v-model指令
  modelDirective(node, value, key) {
    // 处理页面初始化时的model值填充视图
    node.value = value;
    // 处理监听到变化后的model值填充视图
    new Watcher(
      this.vm,
      key,
      () => {
        return this.vm.$data[key];
      },
      (newValue) => {
        node.value = newValue;
      }
    );
    //双向数据绑定,视图变更同步修改数据模型
    node.addEventListener("input", () => {
      this.vm.$data[key] = node.value;
    });
  }
}

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>实现简化版本vue</title>
</head>
<body>
  <div id="app">
    <h1>差值表达式</h1>
    <div>
      <h3>{{msg}}</h3>
    </div>
    <h3>{{count}} <button @click="addCount">递增</button></h3>
    <h3>{{person.home.address}}</h3>
    <h1>v-text指令</h1>
    <div v-text="msg"></div>
    <div v-text="count"></div>
    <div v-text="words"></div>
    <h1>v-model指令</h1>
    <label style="display: block;">
      <span>msg</span>
      <input type="text" v-model="msg">
    </label>
    <label style="display: block;">
      <span>count</span>
      <input type="text" v-model="count">
    </label>
    <label style="display: block;">
      <span>words</span>
      <input type="text" v-model="words">
    </label>
    <h1>computed</h1>
    <h3>{{doubleCount}} </h3>
    <h3>{{ThreeCount}} </h3>
  </div>
  <script src="./js/dep.js"></script>
  <script src="./js/watcher.js"></script>
  <script src="./js/compiler.js"></script>
  <script src="./js/observer.js"></script>
  <script src="./js/vue.js"></script>
  <script>
    let vm=new Vue({
      el:'#app',
      data:{
        msg:'Hello Vue',
        count:100,
        language:'chinese',
        person:{
          name:"zs",
          home:{
            address:'杭州'
          }
        }
      },
      computed:{
        doubleCount:function() {
          return this.$data.count*2
        },
        ThreeCount:function(){
          return this.$data.count*3
        }
      },
      methods:{
        addCount(){
          this.$data.count++
        }
      }
    })
  </script>
</body>
</html>

关于函数

看看html文件的定义 + Compiler 的@click相关部分就好了

参考文献1
参考文献2
参考文献3

相关文章

网友评论

      本文标题:每周一更 -- 手动实现vue2.0双向绑定原理简化版(data

      本文链接:https://www.haomeiwen.com/subject/sjogbjtx.html