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

yizhihongxing

解析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日

相关文章

  • 浅谈Express.js解析Post数据类型的正确姿势

    浅谈Express.js解析Post数据类型的正确姿势 在使用Node.js开发Web应用程序时,我们通常会使用Express.js框架来帮助我们搭建应用程序的基本结构。而处理Post请求,获取Post数据则是开发Web应用程序时必不可少的一部分。本篇文章将会详细讲解,在Express.js中,如何正确地解析不同类型的Post数据。 解析applicatio…

    node js 2023年6月8日
    00
  • Nuxt配合Node在实际生产中的应用详解

    Nuxt.js是一个基于Vue.js的服务端渲染应用框架,可通过Node.js和Express.js进行构建和开发。在实际生产中,Nuxt.js能够提供更好的SEO和首屏渲染时间,同时在流量高峰期间也能够提供更好的性能和稳定性。本篇文档将详细讲解使用Nuxt配合Node在实际生产中的应用相关细节。 环境搭建及Nuxt项目结构简介 在开始使用Nuxt之前,首先…

    node js 2023年6月8日
    00
  • JavaScript ES6中类与模块化管理超详细讲解

    JavaScript ES6中类与模块化管理超详细讲解 什么是ES6中的类 在ES6之前,我们用函数来模拟类,从而实现面向对象编程。但是这种方式并不直观,并且容易出错。在ES6中,我们可以通过关键字class来定义类,这样就更加符合面向对象编程的直观性。 如何定义一个类 使用关键字class可以定义一个类,其中类名的首字母通常大写(和Java等其他面向对象编…

    node js 2023年6月8日
    00
  • node将geojson转shp返回给前端的实现方法

    要实现“node将geojson转shp返回给前端”的功能,可以采用以下步骤: 安装相关依赖 在Node.js中,我们可以使用geojson2shp库将GeoJSON文件转换为Shapefile文件。首先需要在命令行中安装该库,命令如下: npm install geojson2shp –save 创建服务器 使用Node.js创建一个简单的服务器,监听前…

    node js 2023年6月8日
    00
  • node 命令方式启动修改端口的方法

    当我们使用Node.js开发Web应用程序时,常常需要在本地电脑启动一个Web服务器。在启动Web服务器时,我们需要指定Web服务器监听的端口号。通常,我们可以通过命令行运行如下命令,来启动Web服务器并指定端口号: node index.js 3000 上述命令会启动一个名为 index.js 的 Node.js 应用程序,并且指定该应用程序监听3000端…

    node js 2023年6月8日
    00
  • 一篇文章搞定JavaScript类型转换(面试常见)

    这里给出一份完整攻略,帮助大家更好的理解和应用JavaScript中的类型转换。 什么是类型转换? 在JavaScript中,类型转换是将一个数据类型转换为另一个数据类型的操作。由于JavaScript是一种弱类型的动态语言,所以通常需要进行类型转换以使得程序正确运行。 类型转换的方法 显式类型转换 显式类型转换是通过一些JavaScript内置的方法将数据…

    node js 2023年6月8日
    00
  • koa源码中promise的解读

    下面是关于“koa源码中promise的解读”的完整攻略: 1. koa中的Promise koa是一个基于Node.js平台的下一代web开发框架,它实现了ES6中的async/await, 而async/await依赖于Promise。因此在koa中,Promise是一个非常重要的概念。 在koa的实现中,Promise主要用于解决异步回调嵌套的问题,通…

    node js 2023年6月8日
    00
  • Node.js返回JSONP详解

    一、什么是JSONP? JSONP是一种跨域访问数据的方式,它通过动态生成script标签,将请求发送到跨域地址上,跨域地址返回一段特定格式的JavaScript代码,调用一个回调函数,将数据作为参数传递给该函数。由于script标签不受同源策略的限制,因此可以轻松实现跨域请求数据的功能。 二、JSONP的实现原理 创建script标签,将请求发送至跨域地址…

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