详解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 相关文章推荐
JavaScript Date对象 日期获取函数
Dec 19 Javascript
一款Jquery 分页插件的改造方法(服务器端分页)
Jul 11 Javascript
面向对象的Javascript之二(接口实现介绍)
Jan 27 Javascript
node.js中的buffer.slice方法使用说明
Dec 10 Javascript
js常用系统函数用法实例分析
Jan 12 Javascript
jQuery超赞的评分插件(8款)
Aug 20 Javascript
Node.js中使用jQuery的做法
Aug 17 Javascript
基于Bootstrap的Metronic框架实现条码和二维码的生成及打印处理操作
Aug 29 Javascript
ES6学习之变量的解构赋值
Feb 12 Javascript
vue3.0 CLI - 2.1 -  component 组件入门教程
Sep 14 Javascript
vue+element实现表格新增、编辑、删除功能
May 28 Javascript
vue 在单页面应用里使用二级套嵌路由
Dec 19 Vue.js
基于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
重置版游戏视频
2020/04/09 魔兽争霸
PHP实现事件机制的方法
2015/07/10 PHP
PHP中配置IIS7实现基本身份验证的方法
2015/09/24 PHP
浅析php如何实现App常用的秒发功能
2016/08/03 PHP
laravel框架select2多选插件初始化默认选中项操作示例
2020/02/18 PHP
JavaScript Cookie的读取和写入函数
2009/12/08 Javascript
jQuery 表格工具集
2010/04/25 Javascript
两个select多选模式的选项相互移动(示例代码)
2014/01/11 Javascript
Jquery 点击按钮自动高亮实现原理及代码
2014/04/25 Javascript
jQuery搜索子元素的方法
2015/02/10 Javascript
Node.js中使用socket创建私聊和公聊聊天室
2015/11/19 Javascript
js图片跟随鼠标移动代码
2015/11/26 Javascript
基于JavaScript实现本地图片预览
2017/02/08 Javascript
原生JS+Canvas实现五子棋游戏
2020/05/28 Javascript
node跨域请求方法小结
2017/08/25 Javascript
微信小程序实现留言板功能
2018/11/02 Javascript
使用layui实现树形结构的方法
2019/09/20 Javascript
浅谈JS for循环中使用break和continue的区别
2020/07/21 Javascript
vue 修改 data 数据问题并实时显示操作
2020/09/07 Javascript
python使用多线程不断刷新网页的方法
2015/03/31 Python
python实现用于测试网站访问速率的方法
2015/05/26 Python
Python yield 使用浅析
2015/05/28 Python
Python调用C++程序的方法详解
2017/01/24 Python
python使用matplotlib绘制柱状图教程
2017/02/08 Python
Python基于OpenCV库Adaboost实现人脸识别功能详解
2018/08/25 Python
django自带调试服务器的使用详解
2019/08/29 Python
python使用HTMLTestRunner导出饼图分析报告的方法
2019/12/30 Python
Python序列化pickle模块使用详解
2020/03/05 Python
python 视频下载神器(you-get)的具体使用
2021/01/06 Python
html5指南-3.如何实现html元素拖拽功能
2013/01/07 HTML / CSS
大学生个人总结的自我评价
2013/10/05 职场文书
工伤死亡理赔协议书
2014/10/20 职场文书
初中生思想道德自我评价
2015/03/09 职场文书
2016年国庆节宣传标语
2015/11/25 职场文书
用Python制作灯光秀短视频的思路详解
2021/04/13 Python
python实现股票历史数据可视化分析案例
2021/06/10 Python