详解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 解析Json字符串的性能比较分析代码
Dec 16 Javascript
JS实现带缓冲效果打开、关闭、移动一个层的方法
May 09 Javascript
H5用户注册表单页 注册模态框!
Sep 17 Javascript
概述一个页面从输入URL到页面加载完的过程
Dec 16 Javascript
Node.js中如何合并两个复杂对象详解
Dec 31 Javascript
JS实现微信摇一摇原理解析
Jul 22 Javascript
使用Angular自定义字段校验指令的方法示例
Feb 01 Javascript
简单实现vue中的依赖收集与响应的方法
Feb 18 Javascript
判断js数据类型的函数实例详解
May 23 Javascript
vue中npm包全局安装和局部安装过程
Sep 03 Javascript
Jquery Datatables的使用详解
Jan 30 jQuery
手写Vue2.0 数据劫持的示例
Mar 04 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
php代码优化及php相关问题总结
2006/10/09 PHP
Mysql中分页查询的两个解决方法比较
2013/05/02 PHP
浅析php设计模式之数据对象映射模式
2016/03/03 PHP
thinkphp中多表查询中防止数据重复的sql语句(必看)
2016/09/22 PHP
PHP基于PDO扩展操作mysql数据库示例
2018/12/24 PHP
javascript不可用的问题探究
2013/10/01 Javascript
node.js中的emitter.on方法使用说明
2014/12/10 Javascript
node.js中的buffer.Buffer.byteLength方法使用说明
2014/12/10 Javascript
AngularJS实现全选反选功能
2015/12/08 Javascript
js+canvas绘制矩形的方法
2016/01/28 Javascript
判断数组是否包含某个元素的js函数实现方法
2016/05/19 Javascript
vue.js入门教程之绑定class和style样式
2016/09/02 Javascript
IntersectionObserver API 详解篇
2016/12/11 Javascript
js实现贪吃蛇小游戏(容易理解)
2017/01/22 Javascript
微信小程序 this和that详解及简单实例
2017/02/13 Javascript
jQuery实现每隔一段时间自动更换样式的方法分析
2018/05/03 jQuery
用js限制网页只在微信浏览器中打开(或者只能手机端访问)
2020/12/24 Javascript
[02:41]DOTA2英雄基础教程 谜团
2013/12/10 DOTA
[03:06]V社市场总监Dota2项目负责人Erik专访:希望更多中国玩家加入DOTA2
2014/07/11 DOTA
[06:01]刀塔次级联赛top10第一期
2014/11/07 DOTA
在Python的Django框架中生成CSV文件的方法
2015/07/22 Python
详解pyqt5 动画在QThread线程中无法运行问题
2018/05/05 Python
解决Mac下首次安装pycharm无project interpreter的问题
2018/10/29 Python
selenium3+python3环境搭建教程图解
2018/12/07 Python
python 使用cycle构造无限循环迭代器
2020/12/02 Python
一款利用纯css3实现的360度翻转按钮的实例教程
2014/11/05 HTML / CSS
前端实现背景虚化但内容清晰且自适应 的实例代码
2019/08/01 HTML / CSS
选购国际女性时装设计师品牌:IFCHIC(支持中文)
2018/04/12 全球购物
Skyscanner台湾:全球知名的旅行比价引擎
2018/07/01 全球购物
门卫班长岗位职责
2013/12/15 职场文书
个人学习群众路线心得体会
2014/11/05 职场文书
火烧圆明园的观后感
2015/06/03 职场文书
申论不会写怎么办?教您掌握这6点思维和原则
2019/07/17 职场文书
Nginx 根据URL带的参数转发的实现
2021/04/01 Servers
小程序实现文字循环滚动动画
2021/06/14 Javascript
python解析json数据
2022/04/29 Python