Vue 양방향 데이터 바인딩의 원칙(인터뷰 필수) 말하는 방법 Vue 양방향 바인딩 인터뷰

Vue.js는 게시자-구독자 모드와 결합된 데이터 하이재킹 방법을 채택하고 Object.defineProperty()를 통해 각 속성의 setter 및 getter를 하이재킹하고 데이터가 변경되면 구독자에게 메시지를 게시하고 해당 모니터링 콜백을 트리거하여 렌더링합니다. 보기 .

특정 단계

  • 1. 하위 속성 개체의 속성을 포함하여 세터 및 게터와 함께 관찰자의 데이터 개체에 대한 재귀 순회가 필요합니다.이 경우 이 개체에 값을 할당하면 세터가 트리거되고 데이터 변경이 가능합니다. 감시당하다
  • 2. 컴파일은 템플릿 명령어를 파싱하고 템플릿의 변수를 데이터로 교체한 다음 렌더링 페이지 보기를 초기화하고 각 명령어에 해당하는 노드를 업데이트 기능으로 바인딩하고 데이터를 수신하는 구독자를 추가합니다. , 알림이 수신됨 , 보기 업데이트
  • 3. Watcher 구독자는 Observer와 Compile 사이의 통신 브리지입니다 주요 작업은 다음과 같습니다:
    (1) 자신을 인스턴스화할 때 속성 구독자(dep)에 추가합니다. ( 2
    ) update() 메서드가 있어야 합니다.
    속성 변경이 dep.notice()에 의해 통지되면 자체 update() 메서드를 호출하고 Compile에서 바인딩된 콜백을 트리거한 다음 폐기할 수 있습니다.
  • 4. MVVM은 데이터 바인딩의 입구로 사용되며, Observer, Compile 및 Watcher를 통합하고, Observer를 통해 자체 모델 데이터 변경 사항을 모니터링하고, Compile을 통해 템플릿 명령을 구문 분석 및 컴파일하고, 마지막으로 Watcher를 사용하여 Observer와 Compile 간의 통신 브리지를 구축합니다. 데이터 변경 달성 -> 업데이트 보기, 대화형 변경 보기(입력) -> 데이터 모델 변경의 양방향 바인딩 효과.

데이터 양방향 바인딩이란 무엇입니까 ?

Vue는 mvvm 프레임워크, 즉 양방향 데이터 바인딩입니다. 즉, 데이터가 변경되면 뷰도 변경되고 뷰가 변경되면 데이터도 동기적으로 변경됩니다. 이것은 Vue의 본질이기도 합니다.  우리가 말하는 양방향 데이터 바인딩은 UI 컨트롤용이어야 하며 UI가 아닌 컨트롤에는 양방향 데이터 바인딩이 포함되지 않는다는 점은 주목할 가치가 있습니다 . 단방향 데이터 바인딩은 redux와 같은 상태 관리 도구를 사용하기 위한 전제 조건입니다. vuex를 사용하면 데이터 흐름도 단일 항목이므로 양방향 데이터 바인딩과 충돌합니다.

양방향 데이터 바인딩을 구현하는 이유는 무엇입니까 ?

vue에서 vuex를 사용하면 데이터가 사실 단방향인데 양방향 데이터바인딩이라고 하는 이유는 UI컨트롤에 사용하기 때문입니다. 편안하게 사용할 수 있습니다.

즉, 이 둘은 상호 배타적이지 않고 단일 항목이 글로벌 데이터 흐름에 사용되어 추적에 편리하고 양방향은 로컬 데이터 흐름에 사용되며 간단하고 조작하기 쉽습니다.

1. Object.defineProperty란 무엇입니까?

1.1 구문:

Object.defineProperty(obj, prop, descriptor)

매개변수 설명:

  1. obj: 필수입니다. 표적
  2. 소품: 필수. 정의하거나 수정할 속성의 이름
  3. 설명자: 필수입니다. 대상 속성이 소유한 속성

반환 값:

함수에 전달된 개체입니다. 즉, 첫 번째 매개변수 obj;

속성의 경우 읽기 전용인지 쓰기 불가인지, for...in 또는 Object.keys()에 의해 순회될 수 있는지 여부와 같은 속성에 대한 몇 가지 특성을 설정할 수 있습니다.

개체의 속성에 기능 설명을 추가하고 현재 데이터 설명 및 접근자 설명의 두 가지 형식을 제공합니다.

개체의 속성을 수정하거나 정의할 때 이 속성에 몇 가지 속성을 추가합니다.

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와 같은 방식으로 구현됩니다. 그런 다음 전체 구현 프로세스를 다음 단계로 나눌 수 있습니다.

  • 입력 상자와 텍스트 노드는 데이터의 데이터 에 바인딩됩니다.
  • 입력 상자의 내용이 변경되면 데이터의 데이터가 동시에 변경됩니다. 즉,  view => model 의 변경 입니다 .
  • 데이터의 데이터가 변경되면 텍스트 노드의 내용이 동시에 변경됩니다. 즉,  model => view 의 변경 입니다 .

四、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 메서드에는 두 가지 주요 작업이 있습니다. 첫 번째는 속성 값을 업데이트하는 것이고 두 번째는 작업을 유지하는 것입니다.

<!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 메서드가 트리거되지만 텍스트 노드의 내용은 변경되지 않았습니다. 텍스트에 바인딩된 텍스트 노드도 동기적으로 어떻게 변경될 수 있습니까? 또 다른 지식 포인트는 구독 게시 모드입니다.

옵저버 모드라고도 하는 구독 게시 모드는 일대다 관계를 정의하여 여러 옵저버 가 동시에 주제 개체를 모니터링할 수 있도록 합니다 . 주제 개체의 상태가 변경되면 모든 옵저버 개체에 알림이 전송됩니다. .

게시자가 알림을 발행합니다  => 주제 개체가 알림을 수신 하고 구독자에게 푸시합니다 => 구독자가 해당 작업을 수행합니다.

// 一个发布者 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 메서드가 트리거될 때 두 번째로 수행할 작업은 게시자로서 "나는 속성 텍스트이며 변경되었습니다."라는 알림을 발행하는 것입니다. 텍스트 노드는 구독자로서 메시지를 받은 후 해당 업데이트 작업을 수행합니다.

여덟, 양방향 바인딩의 실현

돌이켜보면 새 Vue가 생성될 때마다 주로 두 가지 작업이 수행됩니다. 첫 번째는 데이터 모니터링: observe(data), 두 번째는 HTML 컴파일: nodeToFragment(id)

데이터를 모니터링하는 과정에서 데이터의 각 속성에 대한 테마 개체 dep가 생성됩니다.

HTML을 컴파일하는 과정에서 데이터 바인딩과 관련된 각 노드에 대해 구독자 감시자가 생성되고 감시자는 해당 속성의 dep에 자신을 추가합니다.

우리는 달성했습니다: 입력 상자의 내용 수정 => 이벤트 콜백 함수에서 속성 값 수정 => 속성의 설정 메서드 트리거.

다음으로 달성하고자 하는 것은 알림 전송 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의 accessor 속성을 읽어서 accessor 속성의 get 메소드를 트리거하고, get 메소드는 해당 vm의 dep에 watcher를 추가한다. 접근자 속성;

다시 순차 값을 가져온 다음 뷰를 업데이트합니다.

마지막으로 Dep.target을 비워 둡니다. 이것은 전역 변수이고 watcher와 dep 사이의 유일한 다리이기 때문에 Dep.target이 항상 하나의 값만 갖도록 보장해야 합니다.

궁극적으로 다음과 같이

<!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>

 

다창면접질문공유 면접질문은행

프론트엔드 및 백엔드 인터뷰 질문 은행(인터뷰에 필요) 추천: ★★★★★

주소: 프론트 엔드 인터뷰 질문 은행   웹 프론트 엔드 인터뷰 질문 은행 VS 자바 백엔드 인터뷰 질문 은행 Daquan

추천

출처blog.csdn.net/weixin_42981560/article/details/131422679