Home  >  Article  >  Web Front-end  >  This article will give you an in-depth analysis of the principle of Vue two-way binding (understand it thoroughly)

This article will give you an in-depth analysis of the principle of Vue two-way binding (understand it thoroughly)

青灯夜游
青灯夜游forward
2022-02-18 19:47:514048browse

This article customizes a vue and gradually implements two-way binding of data. It will give you a step-by-step understanding of the principle of vue two-way binding through examples. I hope it will be helpful to everyone!

This article will give you an in-depth analysis of the principle of Vue two-way binding (understand it thoroughly)

Custom vue class

  • Vue requires at least two parameters: template and data. [Related recommendations: vue.js video tutorial]

  • Create a Compiler object, render the data to the template, and mount it to the specified node.

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

Realizing data to the page for the first time

##Compiler

1. The node2fragment function extracts the template elements into the memory, so that the data can be rendered into the template and then mounted to the page at once.

2. After the template is extracted into the memory, use the buildTemplate function to traverse the template elements

  • Element node

      Use the buildElement function to check the attributes starting with v- on the element
  • Text Node

      Use the buildText function to check whether there is {{}} content in the text
3, create the CompilerUtil class to process vue instructions and {{}}, complete the rendering of data

4. This completes the first data rendering. Next, it is necessary to automatically update the view when the data changes.

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

Implementing data-driven view

Observer

1, use the defineRecative function to do Object.defineProperty processing on the data, so that Each data in data can be monitored by get/set

2. Next, we will consider how to update the view content after monitoring the change of data value? Using the Observer design pattern, create Dep and Water classes.

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('监听到数据的变化');
        }
      }
    })
  }
}

Use the observer design pattern to create the Dep and Wather classes

1. The purpose of using the observer design pattern is:

  • Parses the template and collects the DOM node set that a certain data in the data is used in the template. When the data changes, the data update is achieved by updating the DOM node set.

  • Dep: Used to collect the set of dom nodes that a certain data attribute depends on, and provide update methods

  • Watcher: Each dom node's Package object

      attr: the data attribute used by the dom
    • cb: the callback function that modifies the dom value, will receive
    when created
2. At this point, I feel that the idea is OK, and I am already confident of winning. So how to use Dep and Watcher?

  • Add a dep for each attribute to collect the dependent dom

  • Because the data data will be read when the page is rendered for the first time. At this time, the getter of the data will be triggered, so collect dom

  • specifically. When the CompilerUtil class parses v-model, {{}} and other commands, the getter will be triggered. , we create Water before triggering, add a static attribute to Watcher, pointing to the dom, then obtain the static variable in the getter function, and add it to the dependency, thus completing a collection. Because the static variable is assigned a value every time the getter is triggered, there is no case of collecting wrong dependencies.

  • 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. At this point, the view is automatically updated when data is bound. I originally wanted to implement the code step by step, but found it difficult to handle, so I posted the complete class. .

Implementing view-driven data

In fact, it is to monitor the input and change events of the input box. Modify the model method of CompilerUtil. The specific code is as follows

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 two-way binding principle

vue receives a template and data parameters. 1. First, recursively traverse the data in data, execute Object.defineProperty on each property, and define get and set functions. And add a dep array for each property. When get is executed, a watcher will be created for the called DOM node and stored in the array. When set is executed, the value is reassigned and the notify method of the dep array is called to notify all watchers that use this attribute and update the corresponding dom content. 2. Load the template into memory, recurse the elements in the template, and detect that the element has a command starting with v- or a double brace instruction, and the corresponding value will be taken from the data to modify the template content. At this time, the template content will be modified. The dom element is added to the attribute's dep array. This implements a data-driven view. When processing the v-model instruction, add an input event (or change) to the dom, and modify the value of the corresponding attribute when input, realizing page-driven data. 3. After binding the template to the data, add the template to the real DOM tree.

How to put watcher in dep array?

When parsing the template, the corresponding data attribute value will be obtained according to the v-instruction. At this time, the get method of the attribute will be called. We first create a Watcher instance and obtain the attribute value inside it. , stored inside the watcher as an old value. Before we get the value, add the attribute Watcher.target = this on the Watcher prototype object; and then get the value, it will be Watcher.target = null; so that when get is called, Obtain the watcher instance object according to Watcher.target.

The principle of methods

When creating a vue instance, receive the methods parameter

Encounter v-on when parsing the template instruction. A listener for the corresponding event will be added to the dom element, and the call method will be used to bind vue to this method: vm.$methods[value].call(vm, e);

The principle of computed

When creating a vue instance, receive the computed parameter

When initializing the vue instance, perform Object for the computed key .defineProperty processing and adding get properties.

(Learning video sharing: web front-end)

The above is the detailed content of This article will give you an in-depth analysis of the principle of Vue two-way binding (understand it thoroughly). For more information, please follow other related articles on the PHP Chinese website!

Statement:
This article is reproduced at:juejin.cn. If there is any infringement, please contact admin@php.cn delete