基本原理

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

思路整理

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

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

流程图如下:

分布实现

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,注册回调函数,添加监听数据的订阅者,数据变动时,更新视图

解析流程如下:

  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进行监听。

参考文章

剖析Vue原理&实现双向绑定MVVM

Vue双向数据绑定原理解析的更多相关文章

  1. vue双向数据绑定原理探究(附demo)

    昨天被导师叫去研究了一下vue的双向数据绑定原理...本来以为原理的东西都非常高深,没想到vue的双向绑定真的很好理解啊...自己动手写了一个. 传送门 双向绑定的思想 双向数据绑定的思想就是数据层与 ...

  2. Vue双向数据绑定原理分析(转)

    add by zhj: 目前组里使用的是前端技术是jQuery + Bootstrap,后端使用的Django,Flask等,模板是在后端渲染的.前后端没有分离,这种做法有几个缺点 1. 模板一般是由 ...

  3. Vue双向数据绑定原理深度解析

    首先,什么是双向数据绑定?Vue是三大MVVM框架之一,数据绑定简单来说,就是当数据发生变化时,相应的视图会进行更新,当视图更新时,数据也会跟着变化. 在分析其原理和代码的时候,大家首先了解如下几个j ...

  4. 手写MVVM框架 之vue双向数据绑定原理剖析

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  5. 16、前端知识点--Object.defineProperty 的用法+双向数据绑定原理解析

    一.Object.defineProperty 的用法 Object.defineProperty 可以用于给对象添加更新属性. <script> // Object.defineProp ...

  6. Vue 双向数据绑定原理分析 以及 Object.defineproperty语法

    第三方精简版实现 https://github.com/luobotang/simply-vue Object.defineProperty 学习,打开控制台分别输入以下内容调试结果 userInfo ...

  7. vue 双向数据绑定原理

    博客地址: https://ainyi.com/8 采用defineProperty的两个方法get.set 示例 <!-- 表单 --> <input type="tex ...

  8. Vue双向数据绑定原理

    https://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension

  9. Vue双向绑定原理(源码解析)---getter setter

       Vue双向绑定原理      大部分都知道Vue是采用的是对象的get 和set方法来实现数据的双向绑定的过程,本章将讨论他是怎么利用他实现的. vue双向绑定其实是采用的观察者模式,get和s ...

随机推荐

  1. [COGS 1752] 摩基亚Mokia

    照例先上题面 1752. [BOI2007]摩基亚Mokia 输入文件:mokia.in   输出文件:mokia.out 时间限制:1.5 s   内存限制:128 MB [题目描述] 摩尔瓦多的移 ...

  2. Jmeter之app性能测试(ios,android)

    测试小菜鸟alter今天一大早又学习了一点东西关于jmeter-app性能测试,现在记录一下.提到性能测试,那我就简单总结一下.(想跳过的,直接将页面下移到下一个红色字体处) 性能测试的相关知识介绍 ...

  3. Making the Grade (bzoj1592)

    题目描述       FJ打算好好修一下农场中某条凹凸不平的土路.按奶牛们的要求,修好后的路面高度应当单调上升或单调下降,也就是说,高度上升与高度下降的路段不能同时出现在修好的路中. 整条路被分成了N ...

  4. NYOJ 289 苹果(01背包)

    苹果 时间限制:3000 ms  |  内存限制:65535 KB 难度:3   描述 ctest有n个苹果,要将它放入容量为v的背包.给出第i个苹果的大小和价钱,求出能放入背包的苹果的总价钱最大值. ...

  5. JavaScript闭包只学这篇就够了

    闭包不是魔法 这篇文章使用一些简单的代码例子来解释JavaScript闭包的概念,即使新手也可以轻松参透闭包的含义. 其实只要理解了核心概念,闭包并不是那么的难于理解.但是,网上充斥了太多学术性的文章 ...

  6. cocoa pods 命令不执行command not found

    bogon:~ mrbtios01$ cd Desktop/改版app/lingMoney新改版的 //当出现如下问题时: bogon:lingMoney新改版的 mrbtios01$ vim pod ...

  7. .NET定位CPU使用率过高问题

    摘要: 当一个.net应用在生产环境CPU突然居高不下,如何快速准确的定位问题所在,并且对实时业务影响最小化?如何不抓Dump也不用live debug就可以知道你的应用在做什么?如何确认你的应用是由 ...

  8. ionic3 app 退出应用程序

    在ionic3 打包的app,如果要实现代码来控制应用程序的推出. 在android以下代码是可以的,但是在ios是不支持的.因为这不适用于ios app,因为苹果apple 不允许应用程序以编程的方 ...

  9. layer弹出层详解

    前言:学习layer弹出框,之前项目是用bootstrap模态框,后来改用layer弹出框,在文章的后面,我会分享项目的一些代码(我自己写的). layer至今仍作为layui的代表作,她的受众广泛并 ...

  10. NoSql数据库简介及Redis学习

    NO-Sql数据库:Not Only不仅仅是SQL 定义:非关系型数据库:NoSQL用于超大规模数据的存储.(例如谷歌或Facebook每天为他们的用户收集万亿比特的数据).这些类型的数据存储不需要固 ...