详解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 相关文章推荐
Jquery实战_读书笔记1—选择jQuery
Jan 22 Javascript
JQuery操作Select的Options的Bug(IE8兼容性视图模式)
Apr 21 Javascript
Flex通过JS获取客户端IP和计算机名的实例代码
Nov 21 Javascript
jquery动态改变onclick属性导致失效的问题解决方法
Dec 04 Javascript
让人蛋疼的JavaScript语法特性
Sep 30 Javascript
JQuery实现可直接编辑的表格
Apr 16 Javascript
jQuery实现鼠标经过图片变亮其他变暗效果
May 08 Javascript
jquery实现全选、不选、反选的两种方法
Sep 06 Javascript
JS给按钮添加跳转功能类似a标签
May 30 Javascript
Angular 4 指令快速入门教程
Jun 07 Javascript
深入理解基于vue-cli的vuex配置
Jul 24 Javascript
AngularJS 教程及实例代码
Oct 23 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
西德产收音机
2021/03/01 无线电
Ajax+PHP边学边练 之五 图片处理
2009/12/03 PHP
php中Ctype函数用法详解
2014/12/09 PHP
Windows7下的php环境配置教程
2015/02/28 PHP
php利用smtp类实现电子邮件发送
2015/10/30 PHP
WordPress中注册菜单与调用菜单的方法详解
2015/12/18 PHP
基于Unit PNG Fix.js有时候在ie6下不正常的解决办法
2013/06/26 Javascript
动态添加option及createElement使用示例
2014/01/26 Javascript
JavaScript中的object转换函数toString()与valueOf()介绍
2014/12/31 Javascript
jQuery的animate函数实现图文切换动画效果
2015/05/03 Javascript
检查表单元素的值是否为空的实例代码
2016/06/16 Javascript
JavaScript中函数声明与函数表达式的区别详解
2016/08/18 Javascript
jQuery如何跳转到另一个网页 就这么简单
2016/12/28 Javascript
jquery使用EasyUI Tree异步加载JSON数据(生成树)
2017/02/11 Javascript
jQuery插件FusionCharts绘制的3D环饼图效果示例【附demo源码】
2017/04/02 jQuery
AngularJs 常用的过滤器
2017/05/15 Javascript
node.js实现微信JS-API封装接口的示例代码
2017/09/06 Javascript
全面解析vue router 基本使用(动态路由,嵌套路由)
2018/09/02 Javascript
python操作mysql数据库
2017/03/05 Python
Python3 XML 获取雅虎天气的实现方法
2018/02/01 Python
Python 循环语句之 while,for语句详解
2018/04/23 Python
python如何编写win程序
2020/06/08 Python
浅析python字符串前加r、f、u、l 的区别
2021/01/24 Python
css3进阶之less实现星空动画的示例代码
2019/09/10 HTML / CSS
马来西亚航空官方网站:Malaysia Airlines
2017/07/28 全球购物
心碎乌托邦的创业计划书范文
2013/12/26 职场文书
小学教师听课制度
2014/02/01 职场文书
大学校运会广播稿
2014/02/03 职场文书
幼儿园社区活动总结
2014/07/07 职场文书
高中学生自我评价范文
2014/09/23 职场文书
市级三好学生评语
2014/12/29 职场文书
辞职信格式范文
2015/05/13 职场文书
2016年国陪研修感言
2015/11/18 职场文书
简单谈谈Python面向对象的相关知识
2021/06/28 Python
《王者天下》第4季首话新剧照 4月9日正式开播
2022/04/07 日漫
类和原型的设计模式之复制与委托差异
2022/07/07 Javascript