vue 双向绑定实现原理

        在前端发展的过程中,vue这个框架,成为了越来越多公司及工作者的选择。笔者认为vue设计最棒的一点,就在于他是响应式的,即数据变化,视图也跟着一起变化,那么vue的响应式,是如何实现的呢?

        这篇文档会带着读者一步步实现一个vue的双向绑定,这篇文章会解开你对vue双向绑定的所有雾水,下面就让我们一步步实现它

一、实现目标及大体思路

        1. 目标:其实目标非常简单,就是数据发生变化的时候,视图也跟着变化

        2. 大体思路:首先解析到HTML模板,判断节点是文本节点还是dom节点,

                ①、如果是文本节点,则看看文本节点的value值是不是可以与vue数据响应模板语法{{  }}匹配,如果匹配,则获取data中对应该节点的value替换html中的文本,并对其进行监听,如果发生变化,则再次对html中的文本进行更新

                ②、如果是元素节点,则看看他的attribute属性中,存不存在vue指令,如v-model等,如果存在,则执行对应的方法

二、双向绑定用到的所有类及他们的作用

        1. Compile: 解析HTML模板,通过递归的方式获取他们所有的Dom节点

                ①如果是文本节点并且节点的值匹配上vue响应数据的固定格式({{  }}形式),则为它创建一个观察者watcher,如果数据发生变化,则更新这个节点

                ②如果是dom节点类型的,则获取他们的attribute属性,看看是不是存在v-model、v-on等vue指令,如果存在则执行各个指令所对应的函数

        2. Observer:通过Object.defineProperty方法,为data中的每个元素增加一个监听器,元素被获取或者改变,分别执行对应的get、set方法

        3. Watcher:观察数据是否变化,数据发生变化,则提示视图进行更新

        4. Dep:订阅者,作为Watcher和Observer的桥梁,收集所有订阅者

        5. selfVue: vue实例,使用方法和new vue一样,都是传递一个配置项(data,el等)

        很多人对Dep订阅者,存在很大的疑问,为什么要增加一个订阅者呢??其实原因很简单,因为vue是响应式的,数据发生变化,则所有依赖这个数据的地方,都要做出变化;

        那假如只有一个数据发生变化,反而要更新所有dom节点的值,是不是很浪费性能呢?原因是会的,但是我们在Watcher上做下手脚呢??如果数据没有发生变化,watcher就不去更新dom,直接return出去不就解决了嘛

三、实现过程

        在实现之前,咱们先看下咱们自己实现的vue响应式该如何使用

import { Observer } from "../responsive/Observer.js";
import { Compile } from "../responsive/Compile.js";
class SelfVue {
  constructor(options) {
    // option为vue的配置项,其中包括了el,data等
    this.data = options.data;
    this.el = options.el;
    // 监听器,在vue创建的时候,对所有的数据都进行监控,触发get和set的时候对应执行响应的方法
    new Observer(this.data);
    // 解析器
    new Compile(this.el, this);// 这里的this为当前vue实例,感兴趣的可以自行打印下看看
  }
}
export { SelfVue };

        是不是看着很简单!确实也是真的很简单,别慌!下面就一步步实现它

1. 实现一个Observer,能对HTML文本节点解析出来的value值进行监听.他的入参只有一个,即new self的时候传递的data

import { Dep } from "./Dep.js";
class Observer {
  constructor(data) {
    this.data = data;
    this.observer(data);
    // Dep为订阅者,关于Dep的使用,在下文详解
    this.dep = new Dep();
  }
  observer(data) {
    if (!data || typeof data !== "object") {
      // data不存在,或者data为基础数据类型
      return;
    }
    Object.keys(data).forEach((key) => {
      this.defineReactive(data, key, data[key]);
    });
  }
  defineReactive(data, key, val) {
    let self = this;
    // 递归
    this.observer(val);
    Object.defineProperty(data, key, {
      enumerable: true,
      configurable: true,
      get() {
        // Dep.target为当前Watcher的实例,它具有update,和get方法
        if (Dep.target) {
          // 将当前实例化的watcher,保存到Dep订阅者数组中
          self.dep.addSub(Dep.target);
        }
        return val;
      },
      set(newVal) {
        if (newVal === val) {
          return;
        }
        val = newVal;
        // 下文详解Dep作用,现在不用关心,到此知道这个属性被监听到了即可
        self.dep.notify();
      },
    });
  }
}
export { Observer };

2. 实现一个观察者Watcher

        使用场景:解析HTML模板的时候,为每个匹配到vue响应式数据格式的节点创建一个watcher,对应data值变化的时候,watcher会对其进行更新,对于以下代码的Dep和callback回调函数部分依然不用纠结,不久后,咱们会一起揭开它神秘的面纱!

import { Dep } from "../responsive/Dep.js";
class Watcher {
  // vm当前实例(包括了method,data等),exp监听的那个属性,callback回调函数,返回改变后的值,更新视图
  constructor(vm, exp, callback) {
    this.vm = vm;
    this.exp = exp;
    this.callback = callback;
    this.value = this.get(); // 这里的value是data中真实的数据
  }
  get() {
    // this当前watcher实例,监听的是HTML模板中某一项可匹配的value值,即以{{  }}形式出现的
    Dep.target = this; // 这里的this为当前watcher的实例
    let value = this.vm.data[this.exp]; // 注意!!!这里会触发Object.defineProperty的get方法
    Dep.target = null; // 释放自己
    return value;
  }
  update() {
    this.run();
  }
  run() {
    // 现在this.vm.data[this.exp]中的值已经为最新的
    this.value = this.vm.data[this.exp];
    this.callback(this.value);
  }
}
export { Watcher };

        3. 实现一个Dep订阅者

                使用场景:Dep的作用只有一个,存放所有的watcher实例,数据改变的时候,执行所有的watcher实例,更新视图

// Dep存放的是所有的watcher
class Dep {
  constructor() {
    this.subs = [];
  }
  static target = null;
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach((sub) => {
      // 每个sub都是对应HTML模板的watcher实例
      sub.update();
    });
  }
}
export { Dep };

        4. 实现一个Compile节点处理解析器

                Compile是整个vue响应式执行的第一步,如果上面的代码看的也是带有疑惑,可根据注释中的提示,在看一遍,相信这个提示,具有醍醐灌顶的功能

import { Watcher } from "./Watcher.js";
class Compile {
  constructor(el, vm) {
    this.ele = document.querySelector(el);
    this.vm = vm;
    this.fragment = null; // createDocumentFragment创建的虚拟节点片段
    this.init();
  }

  init() {
    if (this.ele) {
      this.fragment = this.nodeToFragment(this.ele);
      this.compileElement(this.ele);
      // 将Fragment虚拟节点加入HTML结构中
      this.ele.appendChild(this.fragment);
    } else {
      console.error("el不存在");
    }
  }

  nodeToFragment(ele) {
    // 递归获取所有子节点
    /*
     知识补充
      1. createDocumentFragment方法在遍历原来子节点并将原来子节点添加进fragment的时候,会删除要添加的节点
      2. createDocumentFragment()方法,是用来创建一个虚拟的节点对象,或者说,是用来创建文档碎片节点。它可以包含各种类型的节点,在创建之初是空的。
      3. DocumentFragment节点不属于文档树,继承的parentNode属性总是null
      4. 当请求把一个DocumentFragment节点插入文档树时,插入的不是DocumentFragment自身,而是它的所有子孙节点,即插入的是括号里的节点
     */
    let fragment = document.createDocumentFragment();
    let child = ele.firstChild;
    while (child) {
      fragment.appendChild(child);
      child = child.firstChild;
    }
    return fragment;
  }

  compileElement(ele) {
    let childNodes = ele.childNodes;
    childNodes.forEach((node) => {
      const reg = /\{\{(.*)\}\}/;
      let text = node.textContent;
      if (this.isElementNode(node)) {
        // 元素节点
        this.compileNode(node);
      } else if (this.isTextNode(node) && reg.test(text)) {
        // 当前节点类型是文本节点,并且节点的value匹配上了{{ }}格式
        // 文本节点
        this.compileText(node, reg.exec(text)[1]);
      }
      // 递归遍历所有子节点
      if (node.childNodes && node.childNodes.length) {
        this.compileElement(node);
      }
    });
  }

  compileNode(node) {
    // 主要针对node节点的attribute属性,判断有没有v-model等属性
    let nodeAttrs = node.attributes;
    Array.prototype.forEach.call(nodeAttrs, (attr) => {
      if (this.isDirective(attr.name)) {
        // 含有vue自带指令
        if (this.isModelAttr(attr.name)) {
          this.compileModel(node, attr.value);
        }
      }
    });
  }

  compileText(node, exp) {
    exp = exp.trim();
    let initText = this.vm.data[exp];
    this.updateText(node, initText);
    // 为当前节点创建一个watcher观察者实例,如果数据改变,则更新对应的节点值
    // 提示:可以在这里看看watcher是怎么实现的,在留意下Dep中target属性
    new Watcher(this.vm, exp, (value) => {
      this.updateText(node, value);
    });
  }

  updateText(node, value) {
    // 更新文本节点的value值
    node.textContent = typeof value == "undefined" ? "" : value;
  }

  compileModel(node, exp) {
    let val = this.vm.data[exp];
    // 执行v-model对应的方法
    this.modelUpdate(node, exp, val);
    new Watcher(this.vm, exp, (value) => {
      this.modelUpdate(node, exp, value);
    });
    // input 的双向绑定
    node.addEventListener("input", (e) => {
      let newVal = e.target.value;
      if (val === newVal) {
        return;
      }
      this.vm.data[exp] = newVal;
      val = newVal;
    });
  }

  modelUpdate(node, exp, value) {
    node.value = typeof this.vm.data[exp] == "undefined" ? "" : value;
  }

  isModelAttr(attrName) {
    return (
      attrName.split("-").length === 2 && attrName.split("-")[1] === "model"
    );
  }

  isDirective(attrName) {
    return attrName.indexOf("v-") === 0;
  }

  isElementNode(node) {
    return node.nodeType === 1;
  }

  isTextNode(node) {
    return node.nodeType === 3;
  }
}

export { Compile };

到此一个完整的vue响应式就已经实现了,如有不对之处还望之处

        创作不易,如果对您有一些帮助或者启发,麻烦留个赞在离开~~,后续会持续更新vue的一些实现原理,如果喜欢的话,关注不迷路!

参考链接:vue的双向绑定原理及实现 - canfoo#! - 博客园

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值