详解Vue双向数据绑定原理解析


Posted in Javascript onSeptember 11, 2017

基本原理

Vue.采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter和getter,数据变动时发布消息给订阅者,触发相应函数的回调。

思路整理

要实现mvvm的双向绑定,需要实现如下几点:

1.实现一个数据监听器Observer,能够对对象的所有属性进行监听,发生变化时拿到最新值通知订阅者
2.实现一个解析器Compile,对每个子元素节点的指令进行扫描和解析,根据模板指令替换数据,初始化视图以及绑定相应的回调函数;
3.实现一个Watcher,作为Observer和Compile的桥梁,能够订阅属性变动的通知,执行指令绑定的回调函数,更新视图
4.mvvm的入口,整合以上三者

流程图如下:

详解Vue双向数据绑定原理解析

分布实现

1. MVVM.js

function MVVM(options) {
  this.$options = options || {};
  var data = this._data = this.$options.data;
  var me = this;

  // 数据代理
  // 实现 vm.xxx -> vm._data.xxx
  Object.keys(data).forEach(function(key) {
    me._proxyData(key);
  });
  // 代理计算属性
  // 同样通过Object.defineProperty进行劫持
  this._initComputed();

  observe(data, this);

  this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
  $watch: function(key, cb, options) {
    new Watcher(this, key, cb);
  }
}

MVVM入口文件,整合Observer/Compile/Watcher三者,达到数据变化->更新视图;视图变化->数据变更的双向绑定效果。(结合钩子函数,理解Vue生命周期中各个阶段的作用)

2. Observer.js

function Observer(data) {
  Object.keys(data).forEach(function() {
    defineReactive(data, key, data[key]);
  });
}
function defineReactive (data, key, val) {
  var dep = new Dep();
  var childObj = observe(val);

  Object.defineProperty(data, key, {
    enumerable: true, // 可枚举
    configurable: false, // 不能再define
    get: function() {
      if (Dep.target) {
        dep.depend();
      }
      return val;
    },
    set: function(newVal) {
      if (newVal === val) {
        return;
      }
      val = newVal;
      // 新的值是object的话,进行监听
      childObj = observe(newVal);
      // 通知订阅者
      dep.notify();
    }
  });
}

对需要监测的对象的每个属性进行递归遍历,通过Object.defineProperty设置setter和getter。当设置新的属性值时,触发相应的setter,通知订阅者。

function Dep() {
  this.id = uid++;
  this.subs = [];
}
Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  },
  depend: function() {
    Dep.target.addDep(this);
  },
  notify: function() {
    this.subs.forEach(function(sub) {
      sub.update();
    });
  }
};

订阅者模式,每个属性维护一个Dep,记录自己的订阅者(即watcher),notify通知每个订阅者执行相应的update方法,更新视图。

3. Compile.js

Compile做了两件事情:

1.解析模板指令,替换变量,初始化渲染视图;
2.生成一个watcher,注册回调函数,添加监听数据的订阅者,数据变动时,更新视图

详解Vue双向数据绑定原理解析

解析流程如下:

1.将DOM转成文档碎片fragment,提升查询效率
2.遍历所有元素节点及其子节点,调用对应的指令渲染函数渲染,并调用对应的指令更新函数进行绑定
3.将fragment添加回真实的DOM中

遍历元素

function compileElement (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) {
    // 指令以v-xxx命名
    // <span v-html="content"></span>
    var attrName = attr.name; // v-html
    if (me.isDirective(attrName)) {
      var exp = attr.value; // content
      var dir = attrName.substring(2);
      // 事件指令
      if (me.isEventDirective(dir)) {
        compileUtil.eventHandler(node, me.$vm, exp, dir);
        // 普通指令
      } else {
        compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
      }
      node.removeAttribute(attrName);
    }
  });
}

指令处理与更新函数

var compileUtil = {
  html: function(node, vm, exp) {
    this.bind(node, vm, exp, 'html');
  },
  
  bind: function(node, vm, exp, dir) {
    var updaterFn = updater[dir + 'Updater'];
    // 第一次初始化视图
    updaterFn && updaterFn(node, this._getVMVal(vm, exp));
    // 实例化Watcher,添加订阅者
    new Watcher(vm, exp, function(value, oldValue) {
      // 属性变化的视图更新函数
      updaterFn && updaterFn(node, value, oldValue);
    });
  },
}

var Updater = {
  htmlUpdater: function(node, value) {
    node.innerHTML = typeof value == 'undefined' ? '' : value;
  }
}

4. Watcher.js

Watcher作为Observer与Compile之间通信的桥梁,属性变化的订阅者,做了如下的事情:

1.自身实例化时在属性订阅器集合dep里添加自己
2.自身需有update方法
3.调用dep.notice时,watcher调用自身的update ,触发Compile中定义的回调

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

Watcher.prototype = {
  update: function() {
    this.run();
  },
  run: function() {
    var value = this.get();
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
  },
  get: function() {
    Dep.target = this;
    var value = this.getter.call(this.vm, this.vm);
    Dep.target = null;
    return value;
  }
};

这里需要注意的点是,实例化watcher的时候,调用get方法,通过Dep.target = curInstance,强行触发获属性值的getter方法,在属性的订阅器中添加当前watcher实例。

小结

双向绑定的原理很简单,通过数据劫持,当设置新属性值的时候通过订阅者更新视图;编译指令,替换变量,同时绑定更新函数到订阅者;对应事件绑定调用addEventListener进行监听。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
许愿墙中用到的函数
Oct 07 Javascript
兼容FireFox 的 js 日历 支持时间的获取
Mar 04 Javascript
超酷的网页音乐播放器DewPlayer使用方法
Dec 18 Javascript
Jquery中对数组的操作代码
Aug 12 Javascript
浅析JavaScript中的typeof运算符
Nov 30 Javascript
javascript操纵OGNL标签示例代码
Jun 16 Javascript
jQuery中innerWidth()方法用法实例
Jan 19 Javascript
微信小程序教程系列之设置标题栏和导航栏(7)
Jun 29 Javascript
mpvue 如何使用腾讯视频插件的方法
Jul 16 Javascript
vue+element+Java实现批量删除功能
Apr 08 Javascript
浅谈发布订阅模式与观察者模式
Apr 09 Javascript
判断“命令按钮”是否被鼠标单击详解
Jul 31 Javascript
基于DOM节点删除之empty和remove的区别(详解)
Sep 11 #Javascript
在原生不支持的旧环境中添加兼容的Object.keys实现方法
Sep 11 #Javascript
基于bootstrop常用类总结(推荐)
Sep 11 #Javascript
利用JQuery操作iframe父页面、子页面的元素和方法汇总
Sep 10 #jQuery
利用纯js + transition动画实现移动端web轮播图详解
Sep 10 #Javascript
原生JS实现移动端web轮播图详解(结合Tween算法造轮子)
Sep 10 #Javascript
vue系列之动态路由详解【原创】
Sep 10 #Javascript
You might like
php中替换字符串中的空格为逗号','的方法
2014/06/09 PHP
浅析php适配器模式(Adapter)
2014/11/25 PHP
ThinkPHP模板Volist标签嵌套循环输出多维数组的方法
2016/03/23 PHP
Mac系统下安装PHP Xdebug
2018/03/30 PHP
zShowBox 图片放大展示jquery版 兼容性
2011/09/24 Javascript
40个有创意的jQuery图片和内容滑动及弹出插件收藏集之二
2011/12/31 Javascript
js简单的表格添加行和删除行操作示例
2014/03/31 Javascript
yepnope.js使用详解及示例分享
2014/06/23 Javascript
详解JavaScript中数组的相关知识
2015/07/29 Javascript
JS实现网页顶部向下滑出的全国城市切换导航效果
2015/08/22 Javascript
小程序实现左滑删除功能
2018/10/30 Javascript
jquery制作的移动端购物车效果完整示例
2020/02/24 jQuery
[01:45]绝对公平!DOTA2队长征召模式详解
2014/04/25 DOTA
用Python实现一个简单的能够发送带附件的邮件程序的教程
2015/04/08 Python
使用FastCGI部署Python的Django应用的教程
2015/07/22 Python
Python3下错误AttributeError: ‘dict’ object has no attribute’iteritems‘的分析与解决
2017/07/06 Python
python+splinter实现12306网站刷票并自动购票流程
2018/09/25 Python
为什么Python中没有&quot;a++&quot;这种写法
2018/11/27 Python
一文了解Python并发编程的工程实现方法
2019/05/31 Python
解决pyecharts在jupyter notebook中使用报错问题
2020/04/23 Python
python实现小世界网络生成
2019/11/21 Python
TensorFlow 输出checkpoint 中的变量名与变量值方式
2020/02/11 Python
如何基于Python实现word文档重新排版
2020/09/29 Python
Python 爬取淘宝商品信息栏目的实现
2021/02/06 Python
css3实现平移效果(transfrom:translate)的示例
2020/11/13 HTML / CSS
html5教程实现Photoshop渐变色效果
2013/12/04 HTML / CSS
Opodo英国旅游网站:预订廉价航班、酒店和汽车租赁
2018/07/14 全球购物
德国受欢迎的旅游和休闲网站:lastminute.de
2019/09/23 全球购物
项目申报专员岗位职责
2014/07/09 职场文书
生产工厂门卫岗位职责
2014/09/26 职场文书
法人代表证明书格式
2014/10/01 职场文书
小学班主任事迹材料
2014/12/17 职场文书
幼儿园教师求职信
2015/03/20 职场文书
2015仓库保管员年终工作总结
2015/05/13 职场文书
Python实战之用tkinter库做一个鼠标模拟点击器
2021/04/27 Python
Java中常用解析工具jackson及fastjson的使用
2021/06/28 Java/Android