>  기사  >  웹 프론트엔드  >  이 기사에서는 Vue 양방향 바인딩의 원리에 대한 심층 분석을 제공합니다(철저하게 이해하세요).

이 기사에서는 Vue 양방향 바인딩의 원리에 대한 심층 분석을 제공합니다(철저하게 이해하세요).

青灯夜游
青灯夜游앞으로
2022-02-18 19:47:514070검색

이 글은 Vue를 커스터마이징하고 점차적으로 데이터의 양방향 바인딩을 구현합니다. 예제를 통해 Vue 양방향 바인딩의 원리를 단계별로 이해하는 데 도움이 될 것입니다.

이 기사에서는 Vue 양방향 바인딩의 원리에 대한 심층 분석을 제공합니다(철저하게 이해하세요).

Custom vue class

  • vue에는 템플릿과 데이터라는 두 가지 이상의 매개변수가 필요합니다. [관련 권장사항: vue.js 비디오 튜토리얼]

  • 컴파일러 객체를 생성하고, 데이터를 템플릿에 렌더링하고, 지정된 노드에 마운트합니다.

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;
  }
}

페이지에 대한 첫 번째 데이터 렌더링 실현

Compiler

1 node2fragment 함수는 템플릿 요소를 메모리로 추출하여 데이터를 템플릿에 쉽게 렌더링한 다음 마운트합니다.

2. 템플릿이 메모리에 추출된 후 buildTemplate 함수를 사용하여 템플릿 요소

  • 요소 노드

    • buildElement 함수를 사용하여 요소에서 v-로 시작하는 속성을 확인합니다.
  • Text 노드

    • buildText 사용 이 함수는 text

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;
  }
}

데이터 기반 뷰 구현

Observer

1. 데이터에 Object.defineProperty 처리를 수행하여 데이터의 모든 데이터를 get/set으로 모니터링할 수 있습니다

2. 다음으로, 데이터 값의 변화를 들은 후 뷰 콘텐츠를 업데이트하는 방법을 고려해보세요. 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을 수집하세요

  • 페이지가 처음 렌더링될 때 데이터 데이터를 읽게 되고 이때 데이터의 getter가 트리거되므로 여기에서 dom을 수집하세요

  • 어떻게 구체적으로 수집하나요? CompilerUtil 클래스가 v-model, {{}} 및 기타 명령을 구문 분석하면 getter가 트리거되기 전에 Water를 생성하고 Watcher에 정적 속성을 추가합니다. dom을 가리킨 다음 getter 함수에서 정적 변수를 가져와 종속성에 추가하여 컬렉션을 완성합니다. 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);
    })
},

Summary

vue 양방향 바인딩 원리

vue는 템플릿과 데이터 매개변수를 받습니다. 1. 먼저 data의 데이터를 재귀적으로 순회하고, 각 속성에 대해 Object.defineProperty를 실행하고, get 및 set 함수를 정의합니다. 그리고 각 속성에 대한 dep 배열을 추가합니다. get이 실행되면 호출된 DOM 노드에 대한 감시자가 생성되어 배열에 저장됩니다. set이 실행되면 값이 다시 할당되고 dep 배열의 알림 메서드가 호출되어 이 속성을 사용하는 모든 감시자에게 알리고 해당 DOM 콘텐츠를 업데이트합니다. 2. 템플릿을 메모리에 로드하고, 템플릿의 요소를 반복하고, 요소에 v- 또는 이중 중괄호 명령으로 시작하는 명령이 있는지 감지하면 해당 값이 데이터에서 가져와 템플릿 내용을 수정합니다. 이번에는 템플릿 콘텐츠가 속성의 dep 배열에 추가됩니다. 이는 데이터 기반 보기를 구현합니다. v-model 명령 처리 시 DOM에 입력 이벤트(또는 변경)를 추가하고, 입력 시 해당 속성의 값을 수정하여 페이지 중심의 데이터를 구현합니다. 3. 템플릿을 데이터에 바인딩한 후 실제 DOM 트리에 템플릿을 추가합니다.

감시자를 심층 배열에 넣는 방법은 무엇입니까?

템플릿을 구문 분석하면 v-command에 따라 해당 데이터 속성 값을 가져옵니다. 이 때 먼저 Watcher 인스턴스를 생성하고 그 내부의 속성 값을 가져옵니다. 값을 가져오기 전에 Watcher 프로토타입 객체에 Watcher.target = this 속성을 추가한 다음 값을 가져오면 이 방식으로 Watcher.target = null이 됩니다. , get이 호출되면 Watcher.target watcher 인스턴스 객체를 기반으로 얻을 수 있습니다.

메서드의 원리

vue 인스턴스를 생성할 때 메소드 매개변수

를 받고 템플릿을 구문 분석할 때 v-on 명령어를 만나게 됩니다. 해당 이벤트에 대한 리스너가 DOM 요소에 추가되고 호출 메소드는 vue를 이 메소드에 바인딩하는 데 사용됩니다. vm.$methods[value].call(vm, e);

계산 원리

vue 인스턴스를 생성할 때 계산된 매개변수를 받습니다.

Initialize vue 인스턴스 중에 계산된 키에 대해 Object.defineProperty 처리를 수행하고 get 속성을 추가합니다.

(학습 영상 공유: 웹 프론트엔드)

위 내용은 이 기사에서는 Vue 양방향 바인딩의 원리에 대한 심층 분석을 제공합니다(철저하게 이해하세요).의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

성명:
이 기사는 juejin.cn에서 복제됩니다. 침해가 있는 경우 admin@php.cn으로 문의하시기 바랍니다. 삭제