解析Vue2.0双向绑定实现原理

解析Vue2.0双向绑定实现原理

什么是双向绑定

在开发中我们经常需要将数据动态的改变,并且改变后的数据还需要重新展现到页面上。在传统的开发模式下,我们需要手动更新视图,这个操作比较繁琐,代码比较复杂。双向绑定机制的引入,使得开发者不需要手动的去更新DOM,只需要关注数据的状态,页面会自动根据数据的变化来更新页面,这样开发效率大大提高。

Vue的双向绑定实现原理

Vue.js实现数据的双向绑定主要是通过数据劫持结合发布者-订阅者模式的设计思想来实现的。其中,数据劫持指的是通过 Object.defineProperty() 方法来对数据进行劫持,也就是监听对象属性的变化,当数据变动时,劫持器会通知订阅者,再由订阅者去更新视图。

在Vue.js中,实现双向绑定的关键在于UI层的模板解析(Compile),数据绑定(Observer),还有数据监听者(Watch)等操作。

Compile模板解析

Compile模板解析主要作用于 Vue.js 中的模板处理,同时也是解析器的核心。它的作用是对每个元素节点(Node)以及它们含有的指令(Directive)进行分析,将模板中的变量替换成实际的值。在解析过程中,Compile还需要实现一些其他功能,例如指令的解析、模板解析等。

Compile模板解析处理的是Vue.js实例中的所有DOM节点,包括了v-model、{{}}等指令。Compile负责将模板解析成AST(抽象语法树),由AST生成渲染函数。

Observer数据劫持

Observer数据劫持主要是通过 Object.defineProperty() 方法来对数据进行劫持,也就是对对象属性赋值进行监听,当数据变动时,劫持器会通知订阅者,再由订阅者去更新视图。

function defineReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      return val;
    },
    set: function(newVal) {
      if (val === newVal) return;
      val = newVal;
      console.log(`属性${key}已经被监听,现在值为:“${newVal}”。`);
    }
  });
}

Observer会遍历vue中的所有属性,对每个属性进行劫持,这样当属性发生变化的时候就会自动触发getter和setter方法,在setter方法中可以做一些其他操作,例如通知更新以及开发者自定义的操作。

Watch数据监听者

Watch数据监听者主要是监听 Observer 发布的消息,并触发指令的回调函数去重新渲染页面。Watcher分其中主要分为两个类型,委托Watcher和自己Watcher。

VM实例

function Dep () {
  this.subs = []
}
Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub)
  },
  notify: function() {
    this.subs.forEach(function(sub) {
      sub.update()
    })
  }
}

function defineReactive(data, key, val) {
  var dep = new Dep()

  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      if (Dep.target) {
        dep.addSub(Dep.target)
      }
      return val;
    },
    set: function(newVal) {
      if (val === newVal) return;
      val = newVal;
      dep.notify()
      console.log(`属性${key}已经被监听,现在值为:“${newVal}”。`);
    }
  });
}

function Compile(el) {
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);
  if (this.$el) {
    this.$fragment = this.node2Fragment(this.$el);
    this.init();
    this.$el.appendChild(this.$fragment);
  }
}

Compile.prototype = {
  node2Fragment: function(el) {
    var fragment = document.createDocumentFragment();
    var child;
    while (child = el.firstChild) {
      fragment.appendChild(child);
    }
    return fragment;
  },

  init: function() {
    this.compileElement(this.$fragment);
  },

  // 指令解析
  compileElement: function(el) {
    var childNodes = el.childNodes,
      me = this;
    [].slice.call(childNodes).forEach(function(node) {
      var text = node.textContent;
      var reg = /\{\{(.*)\}\}/;

      if (me.isElementNode(node)) {
        me.compile(node);

      } else if (me.isTextNode(node) && reg.test(text)) {
        me.compileText(node, RegExp.$1);
      }

      if (node.childNodes && node.childNodes.length) {
        me.compileElement(node);
      }
    });
  },

  compile: function(node) {
    var nodeAttrs = node.attributes,
      me = this;
    [].slice.call(nodeAttrs).forEach(function(attr) {
      var attrName = attr.name;
      if (me.isDirective(attrName)) {
        var exp = attr.value;
        var dir = attrName.substring(2);
        if (me.isEventDirective(dir)) {
          // 事件指令
          compileUtil.eventHandler(node, me.$vm, exp, dir);
        } else {
          // v-model 指令
          compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
        }
        node.removeAttribute(attrName);
      }
    });
  },
  compileText: function(node, exp) {
    compileUtil.text(node, this.$vm, exp);
  },

  isDirective: function(attr) {
    return attr.indexOf('v-') == 0;
  },

  isEventDirective: function(dir) {
    return dir.indexOf('on') === 0;
  },

  isElementNode: function(node) {
    return node.nodeType == 1;
  },

  isTextNode: function(node) {
    return node.nodeType == 3;
  }
};

var compileUtil = {
  // v-model
  model: function(node, vm, exp) {
    this.bind(node, vm, exp, 'model');

    var me = this,
      val = this._getVmVal(vm, exp);
    node.addEventListener('input', function(e) {
      var newValue = e.target.value;
      if (val === newValue) {
        return;
      }
      me._setVmVal(vm, exp, newValue);
      val = newValue;
    });
  },

  // 事件处理
  eventHandler: function(node, vm, exp, dir) {
    var eventType = dir.split(':')[1],
      fn = vm.$options.methods && vm.$options.methods[exp];
    if (eventType && fn) {
      node.addEventListener(eventType, fn.bind(vm), false);
    }
  },

  // v-text
  text: function(node, vm, exp) {
    this.bind(node, vm, exp, 'text');
  },

  bind: function(node, vm, exp, dir) {
    var updaterFn = updater[dir + 'Updater'];
    updaterFn && updaterFn(node, this._getVmVal(vm, exp));
    new Watcher(vm, exp, function(value, oldValue) {
      updaterFn && updaterFn(node, value, oldValue);
    });
  },

  _getVmVal: function(vm, exp) {
    var val = vm;
    exp = exp.split('.');
    exp.forEach(function(k) {
      val = val[k];
    });
    return val;
  },

  _setVmVal: function(vm, exp, value) {
    var val = vm;
    exp = exp.split('.');
    exp.forEach(function(k, i) {
      // 非最后一个key,更新val的值
      if (i < exp.length - 1) {
        val = val[k];
      } else {
        val[k] = value;
      }
    });
  }
};

var updater = {
  modelUpdater: function(node, value, oldValue) {
    node.value = typeof value == 'undefined' ? '' : value;
  },

  textUpdater: function(node, value) {
    console.log('text:', value)
    node.textContent = typeof value == 'undefined' ? '' : value;
  }
};

function Watcher(vm, exp, cb) {
  this.cb = cb;
  this.vm = vm;
  this.exp = exp;
  this.value = this.get();
}

Watcher.prototype = {
  update: function() {
    this.run();
  },

  run: function() {
    var value = this.vm.data[this.exp];
    var oldValue = this.value;
    if (value !== oldValue) {
      this.value = value;
      this.cb.call(this.vm, value, oldValue);
    }
  },

  get: function() {
    Dep.target = this;
    var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
    Dep.target = null;
    return value;
  }
};

function Vue(data, el) {
  this.$data = data;
  new Observer(data);
  this.$el = el;
  this.$compile = new Compile(el, this);
}

var vm = new Vue({
  data: {
    text: 'demo test',
    text1: 'A',
    text2: 'B'
  }
});

在上述代码中,定义了vue中的 Dep 类,Dep 类主要实现了订阅者的统计和通知。而 Compile 类主要进行解析vue的模板。

双向绑定实现原理的两条示例说明

示例1

<div id="app">
  <input type="text" v-model="text"/>
  <h1>{{text}}</h1>
</div>
<script>
  var vm = new Vue({
    el: '#app',
    data: {
      text: 'Hello, ZhuangZhuang!'
    }
  })
</script>

在这个例子中,我们通过 v-model 指令将 input 标签和 text 变量进行了绑定,只要我们输入内容就会自动更新到页面上。

这是因为在上述代码中,我们取到了dom对象,给dom添加了绑定事件,对于这个事件的回调函数我们传入了watcher中的callback。当我们的input发生了变化后会执行callback中的update方法,该方法最终执行是 updater['modelUpdate'] ,该方法负责更新我们的视图。

示例2

<div id="app">
  <input type="text" v-model="text1"/>
  <input type="text" v-model="text2"/>
  <h1>{{text1}} {{text2}}</h1>
</div>
<script>
  var vm = new Vue({
    el: '#app',
    data: {
      text1: 'A',
      text2: 'B'
    }
  })
</script>

在这个例子中,我们通过 v-model 指令将2个 input 标签和 text1text2 两个变量分别进行了双向绑定,只要我们输入内容就会自动更新到页面上。

在实例的生命周期中,会先执行Observer,Observer会对vue中的所有属性进行遍历,对每个属性进行劫持;接着执行Compile,Compile负责模板解析,对v-model进行解析后生成getter和setter,与Observer进行关联,并且在数据更新的时候通知watcher进行更新,从而实现了双向绑定。 当我们的输入框发生变化时,监听器会检测到这一变化,并触发setter进行状态更新,更新完成后,监听器又会自动将新的状态通知给订阅它的节点,从而完成了整个数据流的自动处理过程。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:解析Vue2.0双向绑定实现原理 - Python技术站

(0)
上一篇 2023年6月8日
下一篇 2023年6月8日

相关文章

  • 在Linux系统中搭建Node.js开发环境的简单步骤讲解

    下面是在Linux系统中搭建Node.js开发环境的简单步骤: 1. 安装Node.js 要搭建Node.js开发环境,首先需要在Linux系统上安装Node.js。我们可以通过命令行工具来进行安装,具体步骤如下: 打开终端(Terminal),按Ctrl+Alt+T快捷键或者在应用程序中找到Terminal; 执行以下命令即可安装Node.js: sudo…

    node js 2023年6月8日
    00
  • node.js平台下利用cookie实现记住密码登陆(Express+Ejs+Mysql)

    下面是详细的攻略: 准备工作 在进行下一步操作前,请确保你已经安装好了以下软件: Node.js MySQL数据库 同时,在项目目录下创建一个 .env 文件,用于存储私密信息。文件格式如下: PORT=3000 SECRET=your_secret_key DB_HOST=localhost DB_PORT=3306 DB_USER=root DB_PAS…

    node js 2023年6月8日
    00
  • node.js Promise对象的使用方法实例分析

    关于“node.js Promise对象的使用方法实例分析”,我准备了以下攻略,希望对你有所帮助。 Promise是什么 Promise是ES6中用于处理异步编程的一种解决方案,它代表一种异步操作的最终完成(或失败)及其结果值的表示。 如何创建Promise对象 在node.js中,可以使用Promise构造函数来创建Promise对象。Promise构造函…

    node js 2023年6月8日
    00
  • nodejs实现一个word文档解析器思路详解

    下面是“nodejs实现一个word文档解析器思路详解”的完整攻略: 1. 了解Word文档格式 要实现一个Word文档解析器,首先要了解Word文档的格式。Word使用的是二进制文件格式(.doc),这种格式非常复杂,需要逐个字节地解析文件内容。我们可以使用第三方库docx来进行解析,这个库会将Word文档转为XML格式,方便我们进行解析。 2. 安装No…

    node js 2023年6月8日
    00
  • express结合nodejs开启服务示例模版

    本文将详细讲解如何使用Express结合Node.js开启服务示例模版。以下是完整攻略: 安装Node.js 首先,确保您已经安装了Node.js。Node.js是一个基于Chrome V8引擎的JavaScript运行时,可用于在服务器端运行JavaScript代码。您可以在官网上下载并安装Node.js:https://nodejs.org/en/dow…

    node js 2023年6月8日
    00
  • Windows下nodejs安装及环境配置的实战步骤

    下面是详细的“Windows下nodejs安装及环境配置的实战步骤”攻略: 一. 下载Node.js 首先,我们需要下载Node.js的安装文件。请访问Node.js的官方网站(https://nodejs.org/),然后下载适合您计算机的版本,选择LTS版本即可。推荐使用Windows Installer (.msi)版本,下载完成后,双击打开,开始安装…

    node js 2023年6月8日
    00
  • node后端与Vue前端跨域处理方法详解

    一、 前言 在前后端分离的开发模式中,前端Vue与后端Node进行沟通交互涉及到跨域问题。下面我们来详细讲解node后端与Vue前端跨域处理方法。 二、 跨域原理 同源策略(Same Origin Policy)要求网页只能访问与本网页同一个域名、端口、协议的网页。也就是说,一个源的的脚本仅能读写属于该源的窗口和文档。如果读写的目标不是同源的,就会出现跨域问…

    node js 2023年6月8日
    00
  • Node.js 中如何收集和解析命令行参数

    收集和解析命令行参数是 Node.js 进程中一个常见且重要的任务。Node.js 提供了内置的 process 对象,该对象包含了一个 argv 属性,用于获取用户在命令行中传递的参数列表。本篇攻略将详细介绍 Node.js 中如何收集和解析命令行参数。 获取命令行参数 Node.js 中可以使用 process.argv 属性获取命令行的参数。proce…

    node js 2023年6月8日
    00
合作推广
合作推广
分享本页
返回顶部