详解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 事件对象属性小结
Apr 27 Javascript
去掉gridPanel表头全选框的小例子
Jul 18 Javascript
js写的方法实现上传图片之后查看大图
Mar 05 Javascript
JavaScript开发人员的10个关键习惯小结
Dec 05 Javascript
js树插件zTree获取所有选中节点数据的方法
Jan 28 Javascript
AngularJS操作键值对象类似java的hashmap(填坑小结)
Nov 12 Javascript
canvas 实现中国象棋
Feb 17 Javascript
form表单序列化详解(推荐)
Aug 15 Javascript
vue transition 在子组件中失效的解决
Nov 12 Javascript
Vue解决echart在element的tab切换时显示不正确问题
Aug 03 Javascript
微信小程序实现modal弹出框遮罩层组件(可带文本框)
Dec 20 Javascript
详解微信小程序(Taro)手动埋点和自动埋点的实现
Mar 02 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中利用XML技术构造远程服务(上)
2006/10/09 PHP
Javascript的IE和Firefox兼容性汇编
2006/07/01 Javascript
jquery $(document).ready() 与window.onload的区别
2009/12/28 Javascript
关于 文本框默认值 的操作js代码
2012/01/12 Javascript
jquery实现的可隐藏重现的靠边悬浮层实例代码
2013/05/27 Javascript
比例尺、缩略图、平移缩放之百度地图添加控件方法
2015/08/03 Javascript
JS实现先显示大图后自动收起显示小图的广告代码
2015/09/04 Javascript
javascript实现在网页中运行本地程序的方法
2016/02/03 Javascript
原生JS和jQuery版实现文件上传功能
2016/04/18 Javascript
微信小程序开发(一) 微信登录流程详解
2017/01/11 Javascript
JS复制对应id的内容到粘贴板(Ctrl+C效果)
2017/01/23 Javascript
jquery实现轮播图效果
2017/02/13 Javascript
使用openSpeDiv方法实现Ecshop登录弹窗框效果
2017/03/13 Javascript
axios学习教程全攻略
2017/03/26 Javascript
Vue 实现列表动态添加和删除的两种方法小结
2018/09/07 Javascript
Element-UI踩坑之Pagination组件的使用
2018/10/29 Javascript
javascript实现手动点赞效果
2019/04/09 Javascript
微信小程序使用Vant Weapp组件库的方法步骤
2019/08/01 Javascript
浅谈实现在线预览PDF的几种解决办法
2020/08/10 Javascript
[48:52]DOTA2上海特级锦标赛A组小组赛#2 Secret VS CDEC第一局
2016/02/25 DOTA
[01:33]完美世界DOTA2联赛PWL S3 集锦第二期
2020/12/21 DOTA
python简单获取本机计算机名和IP地址的方法
2015/06/03 Python
Python文件处理
2016/02/29 Python
详解django中自定义标签和过滤器
2017/07/03 Python
python 3利用Dlib 19.7实现摄像头人脸检测特征点标定
2018/02/26 Python
tensorflow 输出权重到csv或txt的实例
2018/06/14 Python
python3的数据类型及数据类型转换实例详解
2019/08/20 Python
pytorch载入预训练模型后,实现训练指定层
2020/01/06 Python
Python多分支if语句的使用
2020/09/03 Python
手对手的教你用canvas画一个简单的海报的方法示例
2018/12/10 HTML / CSS
Python里面如何拷贝一个对象
2014/02/17 面试题
如何写毕业求职自荐信
2013/11/06 职场文书
无故旷工检讨书
2014/01/26 职场文书
关于祖国的演讲稿
2014/05/04 职场文书
婚礼证婚人演讲稿
2014/09/13 职场文书
小组组名及励志口号
2015/12/24 职场文书