ホームページ  >  記事  >  ウェブフロントエンド  >  この記事では、Vue の双方向バインディングの原理を詳しく分析します (徹底的に理解してください)。

この記事では、Vue の双方向バインディングの原理を詳しく分析します (徹底的に理解してください)。

青灯夜游
青灯夜游転載
2022-02-18 19:47:513990ブラウズ

この記事では、vue をカスタマイズし、データの双方向バインディングを段階的に実装します。例を通じて、vue の双方向バインディングの原理を段階的に理解できます。皆さんのお役に立てれば幸いです。

この記事では、Vue の双方向バインディングの原理を詳しく分析します (徹底的に理解してください)。

#カスタム vue クラス

  • Vue には、テンプレートとデータという少なくとも 2 つのパラメーターが必要です。 [関連する推奨事項:

    vue.js ビデオ チュートリアル ]

  • Compiler オブジェクトを作成し、データをテンプレートにレンダリングして、指定したノードにマウントします。

    ##
    class MyVue {
      // 1,接收两个参数:模板(根节点),和数据对象
      constructor(options) {
        // 保存模板,和数据对象
        if (this.isElement(options.el)) {
          this.$el = options.el;
        } else {
          this.$el = document.querySelector(options.el);
        }
        this.$data = options.data;
        // 2.根据模板和数据对象,渲染到根节点
        if (this.$el) {
          // 监听data所有属性的get/set
          new Observer(this.$data);
          new Compiler(this)
        }
      }
      // 判断是否是一个dom元素
      isElement(node) {
        return node.nodeType === 1;
      }
    }
  • #ページへのデータの最初のレンダリングを実現

##コンパイラ

1. node2fragment 関数はテンプレート要素をメモリに抽出するため、データをテンプレートにレンダリングしてすぐにページにマウントできます。2. テンプレートがメモリに抽出された後、buildTemplate 関数を使用してテンプレート要素を走査します

要素ノード

  • buildElement 関数を使用して要素の v- で始まる属性を確認します

    • テキスト ノード
  • buildText 関数を使用して、テキストに {{}} コンテンツがあるかどうかを確認します

    • 3、vue 命令を処理する CompilerUtil クラスを作成し、{{}}、データのレンダリングを完了します
  • 4。これで、最初のデータのレンダリングが完了します。次に、自動的に更新する必要があります。データが変更されたときのビュー。
class Compiler {
  constructor(vm) {
    this.vm = vm;
    // 1.将网页上的元素放到内存中
    let fragment = this.node2fragment(this.vm.$el);
    // 2.利用指定的数据编译内存中的元素
    this.buildTemplate(fragment);
    // 3.将编译好的内容重新渲染会网页上
    this.vm.$el.appendChild(fragment);
  }
  node2fragment(app) {
    // 1.创建一个空的文档碎片对象
    let fragment = document.createDocumentFragment();
    // 2.编译循环取到每一个元素
    let node = app.firstChild;
    while (node) {
      // 注意点: 只要将元素添加到了文档碎片对象中, 那么这个元素就会自动从网页上消失
      fragment.appendChild(node);
      node = app.firstChild;
    }
    // 3.返回存储了所有元素的文档碎片对象
    return fragment;
  }
  buildTemplate(fragment) {
    let nodeList = [...fragment.childNodes];
    nodeList.forEach(node => {
      // 需要判断当前遍历到的节点是一个元素还是一个文本
      if (this.vm.isElement(node)) {
        // 元素节点
        this.buildElement(node);
        // 处理子元素
        this.buildTemplate(node);
      } else {
        // 文本节点
        this.buildText(node);
      }
    })
  }
  buildElement(node) {
    let attrs = [...node.attributes];
    attrs.forEach(attr => {
      // v-model="name" => {name:v-model  value:name}
      let { name, value } = attr;
      // v-model / v-html / v-text / v-xxx
      if (name.startsWith('v-')) {
        // v-model -> [v, model]
        let [_, directive] = name.split('-');
        CompilerUtil[directive](node, value, this.vm);
      }
    })
  }
  buildText(node) {
    let content = node.textContent;
    let reg = /\{\{.+?\}\}/gi;
    if (reg.test(content)) {
      CompilerUtil['content'](node, content, this.vm);
    }
  }
}
let CompilerUtil = {
  getValue(vm, value) {
    // 解析this.data.aaa.bbb.ccc这种属性
    return value.split('.').reduce((data, currentKey) => {
      return data[currentKey.trim()];
    }, vm.$data);
  },
  getContent(vm, value) {
    // 解析{{}}中的变量
    let reg = /\{\{(.+?)\}\}/gi;
    let val = value.replace(reg, (...args) => {
      return this.getValue(vm, args[1]);
    });
    return val;
  },
  // 解析v-model指令
  model: function (node, value, vm) {
    // 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
    new Watcher(vm, value, (newValue, oldValue) => {
      node.value = newValue;
    });
    let val = this.getValue(vm, value);
    node.value = val;
  },
  // 解析v-html指令
  html: function (node, value, vm) {
    // 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
    new Watcher(vm, value, (newValue, oldValue) => {
      node.innerHTML = newValue;
    });
    let val = this.getValue(vm, value);
    node.innerHTML = val;
  },
  // 解析v-text指令
  text: function (node, value, vm) {
    // 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
    new Watcher(vm, value, (newValue, oldValue) => {
      node.innerText = newValue;
    });
    let val = this.getValue(vm, value);
    node.innerText = val;
  },
  // 解析{{}}中的变量
  content: function (node, value, vm) {
    let reg = /\{\{(.+?)\}\}/gi;
    let val = value.replace(reg, (...args) => {
      // 在触发getter之前,为dom创建Wather,并为Watcher.target赋值
      new Watcher(vm, args[1], (newValue, oldValue) => {
        node.textContent = this.getContent(vm, value);
      });
      return this.getValue(vm, args[1]);
    });
    node.textContent = val;
  }
}

データ駆動型ビューの実装

オブザーバー

1、defineRecative 関数を使用して、Object.defineProperty 処理を実行します。 get/set2 でデータ内の各データを監視できるように、データを取得します。 次に、データ値の変化を監視した後、ビューの内容を更新する方法を考えます。 Observer デザイン パターンを使用して、Dep クラスと Water クラスを作成します。

class Observer {
  constructor(data) {
    this.observer(data);
  }
  observer(obj) {
    if (obj && typeof obj === 'object') {
      // 遍历取出传入对象的所有属性, 给遍历到的属性都增加get/set方法
      for (let key in obj) {
        this.defineRecative(obj, key, obj[key])
      }
    }
  }
  // obj: 需要操作的对象
  // attr: 需要新增get/set方法的属性
  // value: 需要新增get/set方法属性的取值
  defineRecative(obj, attr, value) {
    // 如果属性的取值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
    this.observer(value);
    // 第三步: 将当前属性的所有观察者对象都放到当前属性的发布订阅对象中管理起来
    let dep = new Dep(); // 创建了属于当前属性的发布订阅对象
    Object.defineProperty(obj, attr, {
      get() {
        // 在这里收集依赖
        Dep.target && dep.addSub(Dep.target);
        return value;
      },
      set: (newValue) => {
        if (value !== newValue) {
          // 如果给属性赋值的新值又是一个对象, 那么也需要给这个对象的所有属性添加get/set方法
          this.observer(newValue);
          value = newValue;
          dep.notify();
          console.log('监听到数据的变化');
        }
      }
    })
  }
}

オブザーバー デザイン パターンを使用して Dep クラスと Wather クラスを作成する

1. オブザーバー デザイン パターンを使用する目的は次のとおりです。

テンプレートを解析し、データ内の特定のデータがテンプレートで使用されている DOM ノード セットを収集し、データが変更された場合、DOM ノード セットを更新することでデータの更新を実現します。

  • Dep: 特定のデータ属性が依存する dom ノードのセットを収集し、更新メソッドを提供するために使用されます。

  • Watcher: 各 domノードのパッケージ オブジェクト

  • attr: dom によって使用されるデータ属性

    cb: dom 値を変更するコールバック関数は、次の場合に
    • を受け取ります。
    • 2 を作成しました。この時点で、このアイデアは問題ないと感じており、すでに勝利を確信しています。では、Dep と Watcher をどのように使用するのでしょうか?

各属性の dep を追加して、依存する dom を収集します

  • データ データは、ページが最初にレンダリングされるときに読み取られるためです。このとき、データのゲッターがトリガーされるので、dom

  • を具体的に収集します。CompilerUtil クラスが v-model、{{}} およびその他のコマンドを解析するとき、 getter がトリガーされます。トリガーする前に Water を作成し、dom を指す static 属性を Watcher に追加してから、getter 関数で static 変数を取得し、依存関係に追加して、コレクションを完成させます。 getter がトリガーされるたびに静的変数に値が割り当てられるため、間違った依存関係が収集されることはありません。

  • class Dep {
      constructor() {
        // 这个数组就是专门用于管理某个属性所有的观察者对象的
        this.subs = [];
      }
      // 订阅观察的方法
      addSub(watcher) {
        this.subs.push(watcher);
      }
      // 发布订阅的方法
      notify() {
        this.subs.forEach(watcher => watcher.update());
      }
    }
    class Watcher {
      constructor(vm, attr, cb) {
        this.vm = vm;
        this.attr = attr;
        this.cb = cb;
        // 在创建观察者对象的时候就去获取当前的旧值
        this.oldValue = this.getOldValue();
      }
      getOldValue() {
        Dep.target = this;
        let oldValue = CompilerUtil.getValue(this.vm, this.attr);
        Dep.target = null;
        return oldValue;
      }
      // 定义一个更新的方法, 用于判断新值和旧值是否相同
      update() {
        let newValue = CompilerUtil.getValue(this.vm, this.attr);
        if (this.oldValue !== newValue) {
          this.cb(newValue, this.oldValue);
        }
      }
    }

    3. この時点では、データがバインドされるとビューが自動的に更新されます。当初はコードを段階的に実装したかったのですが、扱いが難しいと感じたので、投稿しました完全なクラス。

  • ビュー駆動型データの実装

実際には、入力ボックスの入力と変更イベントを監視するためのものです。 CompilerUtilのモデルメソッドを変更します。具体的なコードは次のとおりです。

model: function (node, value, vm) {
    new Watcher(vm, value, (newValue, oldValue)=>{
        node.value = newValue;
    });
    let val = this.getValue(vm, value);
    node.value = val;
	// 看这里
    node.addEventListener('input', (e)=>{
        let newValue = e.target.value;
        this.setValue(vm, value, newValue);
    })
},

要約

vue 双方向バインディングの原則

vue はテンプレートとデータを受け取りますパラメーター。 1. まず、data 内のデータを再帰的に走査し、各プロパティに対して Object.defineProperty を実行し、get 関数と set 関数を定義します。そして、プロパティごとに dep 配列を追加します。 get が実行されると、呼び出された DOM ノードに対してウォッチャーが作成され、配列に格納されます。 set が実行されると、値が再割り当てされ、dep 配列の notify メソッドが呼び出されて、この属性を使用するすべてのウォッチャーに通知され、対応する dom コンテンツが更新されます。 2. テンプレートをメモリにロードし、テンプレート内の要素を再帰し、要素に v- で始まるコマンドまたは二重中括弧命令があることを検出すると、対応する値がデータから取得されてテンプレートの内容が変更されます。今回はテンプレートの内容が変更され、属性の dep 配列に dom 要素が追加されます。これにより、データ駆動型ビューが実装されます。 v-model命令を処理する際に、domに入力イベント(または変更)を追加し、入力時に対応する属性の値を変更することでページ駆動型データを実現します。 3. テンプレートをデータにバインドした後、テンプレートを実際の DOM ツリーに追加します。

ウォッチャーをdep配列に配置するにはどうすればよいですか?

テンプレートを解析すると、v 命令に従って対応するデータ属性値が取得されます。このとき、属性の get メソッドが呼び出されます。最初に Watcher インスタンスを作成し、内部の属性値を取得します。古い値としてウォッチャー内に保存されています。値を取得する前に、Watcher プロトタイプ オブジェクトに属性 Watcher.target = this を追加してから、Watcher.target = となる値を取得します。 null; get が呼び出されたときに Watcher.target に従ってウォッチャー インスタンス オブジェクトを取得するようにします。

メソッドの原理

vue インスタンスを作成するとき、メソッド パラメータを受け取ります

テンプレート命令を解析するときに v-on に遭遇します。対応するイベントのリスナーが DOM 要素に追加され、call メソッドを使用して vue をこのメソッドにバインドします: vm.$methods[value].call(vm, e);

計算の原理

vue インスタンスを作成するときに、計算されたパラメータを受け取ります。

vue インスタンスを初期化するときに、次のことを実行します。計算されたキーのオブジェクト .defineProperty の処理と取得プロパティの追加。

(学習ビデオ共有: Web フロントエンド )

以上がこの記事では、Vue の双方向バインディングの原理を詳しく分析します (徹底的に理解してください)。の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。