Vue 双方向データ バインディングの原則 (面接で必ず聞かれること) Vue 双方向データ バインディング面接の言い方

Vue.js は、パブリッシャー/サブスクライバー モードと組み合わせたデータ ハイジャックを使用し、Object.defineProperty() を通じて各プロパティのセッターとゲッターをハイジャックし、データが変更されたときにサブスクライバーにメッセージをパブリッシュし、対応する監視コールバックをトリガーしてビューをレンダリングします

具体的な手順

  • 1. セッターとゲッターを使用して、サブ属性オブジェクトの属性を含むオブザーバーのデータ オブジェクトの再帰的トラバーサルが必要です。この場合、このオブジェクトに値を割り当てるとセッターがトリガーされ、データ変更が可能になります。監視される
  • 2. コンパイルはテンプレート命令を解析し、テンプレート内の変数をデータに置き換えてから、レンダリング ページ ビューを初期化し、各命令に対応するノードを更新関数でバインドし、データをリッスンするサブスクライバを追加します。 、通知を受信し、ビューを更新します
  • 3. Watcher サブスクライバは、Observer と Compile の間の通信ブリッジです。主に行うべきことは次のとおりです:
    (1) それ自体をインスタンス化するときに、それ自体を属性サブスクライバ (dep) に追加します
    (2) update() メソッドが必要です
    (3)属性の変更が dep.notice() によって通知されると、独自の update() メソッドを呼び出し、コンパイルでバインドされたコールバックをトリガーしてから終了します。
  • 4. MVVM はデータ バインディングの入り口として使用され、Observer、Compile、Watcher を統合し、Observer を通じて独自のモデル データの変更を監視し、Compile を通じてテンプレート命令を解析してコンパイルし、最後に Watcher を使用して Observer と Compile 間の通信ブリッジを構築します。データ変更を実現する -> 更新を表示、対話型変更 (入力) を表示 -> データ モデル変更の双方向バインディング効果。

データ双方向バインディングとは何ですか?

Vue は mvvm フレームワーク、つまり双方向のデータ バインディングです。つまり、データが変更されるとビューも変更され、ビューが変更されるとデータも同期的に変更されます。これも Vue の本質です。 ここで説明する双方向データ バインディングは UI コントロール用である必要があり、非 UI コントロールには双方向データ バインディングが含まれないことに注意してください一方向のデータ バインディングは、redux などの状態管理ツールを使用するための前提条件です。vuex を使用する場合、データ フローも単一項目になるため、双方向のデータ バインディングと競合します。

データの双方向バインディングを実装する理由

vue では、vuex を使用する場合、データは実際には一方向です。双方向データ バインディングと呼ばれる理由は、UI コントロールに使用されるためです。フォームを処理する場合、vue の双方向データ バインディングは非常に便利です。快適に使い切ることができます。

つまり、この 2 つは相互に排他的ではなく、グローバル データ フローでは追跡に便利な単一アイテムが使用され、ローカル データ フローではシンプルで操作が容易な双方向が使用されます。

1. Object.defineProperty とは何ですか?

1.1 構文:

Object.defineProperty(obj, prop, descriptor)

パラメータの説明:

  1. オブジェクト: 必須。目標
  2. 小道具: 必須。定義または変更するプロパティの名前
  3. 記述子: 必須。ターゲット属性が持つプロパティ

戻り値:

関数に渡されるオブジェクト。つまり、最初のパラメータ obj です。

属性については、読み取り専用か書き込み不可か、for...in または Object.keys() で走査できるかどうかなど、この属性の特性を設定できます。

オブジェクトのプロパティに機能の説明を追加します。現在、データの説明とアクセサーの説明の 2 つの形式が提供されています。

オブジェクトのプロパティを変更または定義する場合は、このプロパティにいくつかのプロパティを追加します。

1. アクセサプロパティ

Object.defineProperty() 関数は、オブジェクトのプロパティ関連記述子を定義できます。set 関数と get 関数は、双方向のデータ バインディングを完了する上で重要な役割を果たします。次に、この関数の基本的な使用法を見てみましょう。

var obj = {
      foo: 'foo'
    }

    Object.defineProperty(obj, 'foo', {
      get: function () {
        console.log('将要读取obj.foo属性');
      }, 
      set: function (newVal) {
        console.log('当前值为', newVal);
      }
    });

    obj.foo; // 将要读取obj.foo属性
    obj.foo = 'name'; // 当前值为 name

ご覧のとおり、プロパティにアクセスすると get が呼び出され、プロパティ値を設定すると set が呼び出されます。

2. 簡単な双方向データバインディングの実装方法

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>forvue</title>
</head>
<body>
  <input type="text" id="textInput">
  输入:<span id="textSpan"></span>
  <script>
    var obj = {},
        textInput = document.querySelector('#textInput'),
        textSpan = document.querySelector('#textSpan');

    Object.defineProperty(obj, 'foo', {
      set: function (newValue) {
        textInput.value = newValue;
        textSpan.innerHTML = newValue;
      }
    });

    textInput.addEventListener('keyup', function (e) {
        obj.foo = e.target.value;
    });

  </script>
</body>
</html>

最終レンダリング

単純な双方向データ バインディングを実装するのは難しくないことがわかります。Object.defineProperty() を使用してプロパティの設定関数を定義し、プロパティが割り当てられるときに、Input と innerHTML の値を変更します。このような単純な双方向データ バインディングは、keyup イベント内のオブジェクトのプロパティ値を変更することで実現できます。

双方向バインディング ディレクティブは v-model です。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Vue入门之htmlraw</title>
  <script src="https://unpkg.com/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <!-- v-model可以直接指向data中的属性,双向绑定就建立了 -->
    <input type="text" name="txt" v-model="msg">
    <p>您输入的信息是:{
   
   { msg }}</p>
  </div>
  <script>
    var app = new Vue({
      el: '#app',
      data: {
        msg: '双向数据绑定的例子'
      }
    });
  </script>
</body>
</html>

最終結果は、入力テキスト ボックスの内容を変更すると、それに応じて p タグの内容も変更されます。

3. 課題を実現するための考え方

上記では最も単純な双方向データ バインディングを実装しただけですが、実際に達成したいのは次の方法です。

    <div id="app">
        <input type="text" v-model="text">
        {
   
   { text }}
    </div>  

    <script>
        var vm = new Vue({
            el: '#app', 
            data: {
                text: 'hello world'
            }
        });
    </script> 

つまり、vueと同様にデータの双方向バインディングが実現されます。次に、実装プロセス全体を次のステップに分割できます。

  • 入力ボックスとテキスト ノードはデータ内のデータにバインドされます
  • 入力ボックスの内容が変更されると、data 内のデータも同期して変更されます。つまり、 ビュー => モデル の変更です
  • データ内のデータが変更されると、テキスト ノードの内容も同期して変更されます。つまり、 モデル => ビューの変更です

四、DocumentFragment

タスク 1 を達成したい場合は、次のように、コンテナーと見なすことができる DocumentFragment ドキュメント フラグメントも使用する必要があります。

<div id="app">
        
    </div>
    <script>
        var flag = document.createDocumentFragment(),
            span = document.createElement('span'),
            textNode = document.createTextNode('hello world');
        span.appendChild(textNode);
        flag.appendChild(span);
        document.querySelector('#app').appendChild(flag)
    </script>

このようにして、次の DOM ツリーを取得できます。

ドキュメント フラグメントを使用する利点は、実際の DOM に影響を与えることなく、ドキュメント フラグメント上で DOM を操作できることです。操作が完了したら、それを実際の DOM に追加できるため、正式な DOM を直接変更するよりもはるかに効率的です。

vue はコンパイル時に、マウント ターゲットのすべての子ノードを DocumentFragment にハイジャックし、いくつかの処理の後、DocumentFragment を全体として返し、それをマウント ターゲットに挿入します

次のように :

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>forvue</title>
</head>
<body>
    <div id="app">
        <input type="text" id="a">
        <span id="b"></span>
    </div>

    <script>
        var dom = nodeToFragment(document.getElementById('app'));
        console.log(dom);

        function nodeToFragment(node) {
            var flag = document.createDocumentFragment();
            var child;
            while (child = node.firstChild) {
                flag.appendChild(child);
            }
            return flag;
        }

        document.getElementById('app').appendChild(dom);
    </script>

</body>
</html>

つまり、最初に div を取得し、次に documentFragment を通じてそれをハイジャックし、次にドキュメント フラグメントを div に追加します。

5. データバインディングの初期化

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>forvue</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="text">
        {
   
   { text }}
    </div>
        
    <script>
        function compile(node, vm) {
            var reg = /{
   
   {(.*)}}/;

            // 节点类型为元素
            if (node.nodeType === 1) {
                var attr = node.attributes;
                // 解析属性
                for (var i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == 'v-model') {
                        var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                        node.value = vm.data[name]; // 将data的值赋值给该node
                        node.removeAttribute('v-model');
                    }
                }
            }

            // 节点类型为text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    node.nodeValue = vm.data[name]; // 将data的值赋值给该node
                }
            }
        }

        function nodeToFragment(node, vm) {
            var flag = document.createDocumentFragment();
            var child;

            while (child = node.firstChild) {
                compile(child, vm);
                flag.appendChild(child); // 将子节点劫持到文档片段中
            }
            
            return flag;
        }

        function Vue(options) {
            this.data = options.data;
            var id = options.el;
            var dom = nodeToFragment(document.getElementById(id), this);
            // 编译完成后,将dom返回到app中。
            document.getElementById(id).appendChild(dom);
        }

        var vm  = new Vue({
            el: 'app',
            data: {
                text: 'hello world'
            }
        });


    </script>

</body>
</html>

上記のコードはタスク 1 を実装しています。入力ボックスとテキスト ノードに hello world が表示されていることがわかります。

6. レスポンシブなデータバインディング

タスク 2 の実装アイデアを見てみましょう。入力ボックスにデータを入力すると、まず入力イベント (またはキーアップ、変更イベント) がトリガーされ、対応するイベント ハンドラーで、入力ボックスにそれを割り当てます。vm インスタンスのテキスト属性を指定します。defineProperty を使用して、データ内のテキストを vm のアクセサー プロパティとして設定します。そのため、vm.text に値を割り当てると、set メソッドがトリガーされます。set メソッドで行うことは主に 2 つあり、1 つ目は属性の値を更新すること、2 つ目はタスク内にとどまることです。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>forvue</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="text">
        {
   
   { text }}
    </div>
        
    <script>
        function compile(node, vm) {
            var reg = /{
   
   {(.*)}}/;

            // 节点类型为元素
            if (node.nodeType === 1) {
                var attr = node.attributes;
                // 解析属性
                for (var i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == 'v-model') {
                        var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                        node.addEventListener('input', function (e) {
                            // 给相应的data属性赋值,进而触发属性的set方法
                            vm[name] = e.target.value;
                        })


                        node.value = vm[name]; // 将data的值赋值给该node
                        node.removeAttribute('v-model');
                    }
                }
            }

            // 节点类型为text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    node.nodeValue = vm[name]; // 将data的值赋值给该node
                }
            }
        }

        function nodeToFragment(node, vm) {
            var flag = document.createDocumentFragment();
            var child;

            while (child = node.firstChild) {
                compile(child, vm);
                flag.appendChild(child); // 将子节点劫持到文档片段中
            }
            
            return flag;
        }

        function Vue(options) {
            this.data = options.data;
            var data = this.data;

            observe(data, this);

            var id = options.el;
            var dom = nodeToFragment(document.getElementById(id), this);
            // 编译完成后,将dom返回到app中。
            document.getElementById(id).appendChild(dom);
        }

        var vm  = new Vue({
            el: 'app',
            data: {
                text: 'hello world'
            }
        });



        function defineReactive(obj, key, val) {
            // 响应式的数据绑定
            Object.defineProperty(obj, key, {
                get: function () {
                    return val;
                },
                set: function (newVal) {
                    if (newVal === val) {
                        return; 
                    } else {
                        val = newVal;
                        console.log(val); // 方便看效果
                    }
                }
            });
        }

        function observe (obj, vm) {
            Object.keys(obj).forEach(function (key) {
                defineReactive(vm, key, obj[key]);
            });
        }


    </script>

</body>
</html>

上記でタスク 2 が完了し、テキスト属性値が入力ボックスの内容と同期して変更されます。

7. サブスクライブ/パブリッシュモード (サブスクライブ&パブリッシュ)

text プロパティが変更され、set メソッドがトリガーされますが、text ノードの内容は変更されていません。テキストにバインドされているテキスト ノードも同期的に変更するにはどうすればよいでしょうか? もう 1 つの知識ポイントは、サブスクリプション公開モードです。

オブザーバー モードとも呼ばれるサブスクリプション パブリッシング モードは、1 対多の関係を定義し複数のオブザーバーがサブジェクト オブジェクトを同時に監視できるようにします。サブジェクト オブジェクトの状態が変化すると、すべてのオブザーバー オブジェクトに通知されます。 。

パブリッシャーが通知を発行 =>トピック オブジェクトが通知を受信しサブスクライバーにプッシュ => サブスクライバーが対応する操作を実行します。

// 一个发布者 publisher,功能就是负责发布消息 - publish
        var pub = {
            publish: function () {
                dep.notify();
            }
        }

        // 多个订阅者 subscribers, 在发布者发布消息之后执行函数
        var sub1 = { 
            update: function () {
                console.log(1);
            }
        }
        var sub2 = { 
            update: function () {
                console.log(2);
            }
        }
        var sub3 = { 
            update: function () {
                console.log(3);
            }
        }

        // 一个主题对象
        function Dep() {
            this.subs = [sub1, sub2, sub3];
        }
        Dep.prototype.notify = function () {
            this.subs.forEach(function (sub) {
                sub.update();
            });
        }


        // 发布者发布消息, 主题对象执行notify方法,进而触发订阅者执行Update方法
        var dep = new Dep();
        pub.publish();

ここでの考え方が依然として非常に単純であることを理解するのは難しくありません。パブリッシャーはメッセージのパブリッシュを担当し、サブスクライバーはメッセージの送受信を担当し、最も重要なのはサブジェクト オブジェクトであり、すべてを記録する必要があります。この特別なメッセージを購読し、公開する責任を負う人々。メッセージは、メッセージを購読した人々に通知されます。

したがって、set メソッドがトリガーされたときに行う 2 番目のことは、発行者として「私が属性テキストであり、変更されました」という通知を発行することです。テキスト ノードはサブスクライバとして、メッセージを受信した後、対応する更新アクションを実行します。

八、双方向バインディングの実現

振り返ってみると、新しい Vue が作成されるたびに、主に 2 つのことが行われます。1 つ目はデータを監視することです (observe(data))。2 つ目は HTML をコンパイルすることです (nodeToFragment(id))。

データを監視するプロセスで、データ内の属性ごとにテーマ オブジェクト dep が生成されます。

HTML のコンパイルのプロセスで、データ バインディングに関連するノードごとにサブスクライバー ウォッチャーが生成され、ウォッチャーは対応する属性の dep に自分自身を追加します。

これで、入力ボックスの内容を変更 => イベント コールバック関数の属性値を変更 => 属性の set メソッドをトリガーすることができました。

次に達成したいことは、通知を送信する dep.notify() => サブスクライバ更新メソッドをトリガーする => ビューを更新することです。

ここでの重要なロジックは、関連付けられた属性の dep にウォッチャーを追加する方法です。

function compile(node, vm) {
            var reg = /{
   
   {(.*)}}/;

            // 节点类型为元素
            if (node.nodeType === 1) {
                var attr = node.attributes;
                // 解析属性
                for (var i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == 'v-model') {
                        var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                        node.addEventListener('input', function (e) {
                            // 给相应的data属性赋值,进而触发属性的set方法
                            vm[name] = e.target.value;
                        })


                        node.value = vm[name]; // 将data的值赋值给该node
                        node.removeAttribute('v-model');
                    }
                }
            }

            // 节点类型为text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    // node.nodeValue = vm[name]; // 将data的值赋值给该node

                    new Watcher(vm, node, name);
                }
            }
        }

HTML のコンパイル プロセス中に、データに関連付けられたノードごとにウォッチャーが生成されます。では、Watcher 機能では何が起こっているのでしょうか?

function Watcher(vm, node, name) {
            Dep.target = this;
            this.name = name;
            this.node = node;
            this.vm = vm;
            this.update();
            Dep.target = null;
        }

        Watcher.prototype = {
            update: function () {
                this.get();
                this.node.nodeValue = this.value;
            },

            // 获取data中的属性值
            get: function () {
                this.value = this.vm[this.name]; // 触发相应属性的get
            }
        }

まず、自分自身をグローバル変数 Dep.target に割り当てます。

次に、update メソッドが実行され、次に get メソッドが実行されます。get メソッドは vm のアクセサー属性を読み取り、それによってアクセサー属性の get メソッドがトリガーされ、get メソッドは対応する dep にウォッチャーを追加します。アクセサ属性。

再度、連続した値を取得し、ビューを更新します。

最後に Dep.target を空に設定します。これはグローバル変数であり、watcher と dep の間の唯一のブリッジであるため、Dep.target には常に 1 つの値のみが含まれることを保証する必要があります。

最終的には以下の通り

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>forvue</title>
</head>
<body>
    <div id="app">
        <input type="text" v-model="text"> <br>
        {
   
   { text }} <br>
        {
   
   { text }}
    </div>
        
    <script>
        function observe(obj, vm) {
            Object.keys(obj).forEach(function (key) {
                defineReactive(vm, key, obj[key]);
            });
        }


        function defineReactive(obj, key, val) {

            var dep = new Dep();

            // 响应式的数据绑定
            Object.defineProperty(obj, key, {
                get: function () {
                    // 添加订阅者watcher到主题对象Dep
                    if (Dep.target) {
                        dep.addSub(Dep.target);
                    }
                    return val;
                },
                set: function (newVal) {
                    if (newVal === val) {
                        return; 
                    } else {
                        val = newVal;
                        // 作为发布者发出通知
                        dep.notify()                        
                    }
                }
            });
        }
        
        function nodeToFragment(node, vm) {
            var flag = document.createDocumentFragment();
            var child;

            while (child = node.firstChild) {
                compile(child, vm);
                flag.appendChild(child); // 将子节点劫持到文档片段中
            }
            
            return flag;
        }

        function compile(node, vm) {
            var reg = /{
   
   {(.*)}}/;

            // 节点类型为元素
            if (node.nodeType === 1) {
                var attr = node.attributes;
                // 解析属性
                for (var i = 0; i < attr.length; i++) {
                    if (attr[i].nodeName == 'v-model') {
                        var name = attr[i].nodeValue; // 获取v-model绑定的属性名
                        node.addEventListener('input', function (e) {
                            // 给相应的data属性赋值,进而触发属性的set方法
                            vm[name] = e.target.value;
                        })
                        node.value = vm[name]; // 将data的值赋值给该node
                        node.removeAttribute('v-model');
                    }
                }
            }

            // 节点类型为text
            if (node.nodeType === 3) {
                if (reg.test(node.nodeValue)) {
                    var name = RegExp.$1; // 获取匹配到的字符串
                    name = name.trim();
                    // node.nodeValue = vm[name]; // 将data的值赋值给该node

                    new Watcher(vm, node, name);
                }
            }
        }

        function Watcher(vm, node, name) {
            Dep.target = this;
            this.name = name;
            this.node = node;
            this.vm = vm;
            this.update();
            Dep.target = null;
        }

        Watcher.prototype = {
            update: function () {
                this.get();
                this.node.nodeValue = this.value;
            },

            // 获取data中的属性值
            get: function () {
                this.value = this.vm[this.name]; // 触发相应属性的get
            }
        }

        function Dep () {
            this.subs = [];
        }

        Dep.prototype = {
            addSub: function (sub) {
                this.subs.push(sub);
            },

            notify: function () {
                this.subs.forEach(function (sub) {
                    sub.update();
                });
            }
        }

        function Vue(options) {
            this.data = options.data;
            var data = this.data;

            observe(data, this);

            var id = options.el;
            var dom = nodeToFragment(document.getElementById(id), this);
            // 编译完成后,将dom返回到app中。
            document.getElementById(id).appendChild(dom);
        }

        var vm  = new Vue({
            el: 'app',
            data: {
                text: 'hello world'
            }
        });

    </script>
</body>
</html>

 

大昌面接の質問を共有する 面接質問バンク

フロントエンド面接とバックエンド面接の質問バンク(面接に必要) オススメ度: ★★★★★

アドレス:フロントエンド インタビューの質問バンク  Web フロントエンド インタビューの質問バンク VS Java バックエンド インタビューの質問バンク Daquan

おすすめ

転載: blog.csdn.net/weixin_42981560/article/details/131422679