基本原理

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. mysql导出指定字段或指定数据到文件中

    使用mysqldump把mysql数据库的数据导出到文件中还是挺方便的:比如说要导出baijunyao数据库: // mysqldump -u用户名 -p 数据库名 [表名]> 导出的文件名 m ...

  2. html5图片上传时IOS和Android均显示摄像头拍照和图片选择

    最近在做信开发时,发现<input type="file" />在IOS中可以拍照或从照片图库选择,而Android系统则显示资源管理器,无拍照选项,网上查找资料,改为 ...

  3. 关于Oracle连接超时的问题

    测试环境ORACLE 11.2.0. 如果连接池设置单个连接闲置时间大于数据库连接超时时间,则连接池中的连接发出数据请求时会出现Connect timeout occurred错误, 这是由于连接超时 ...

  4. 解决 lispbox macOS 不兼容问题

    误打误撞,解决了很重要的入门级问题,简要记录下. lispbox 官网末尾说目前暂不兼容 10.4 以上系统: TODO: Compile on Mac OS X 10.4, for compatab ...

  5. 使用CHCA搭建静态博客

    [toc] chca是一个使用golang开发的静态博客生成器,简单.方便.快捷,抛弃每次都需要使用命令编译文件,采用文件监听方式编译,作者只需把markdown文件放到配置中的markdown文件夹 ...

  6. 7. leetcode 104. Maximum Depth of Binary Tree

    Given a binary tree, find its maximum depth. The maximum depth is the number of nodes along the long ...

  7. Base:一种 Acid 的替代方案

    原文链接: BASE: An Acid Alternative 数据库 ACID,都不陌生:原子性.一致性.隔离性和持久性,这在单台服务器就能搞定的时代,很容易实现,但是到了现在,面对如此庞大的访问量 ...

  8. jQuery防京东浮动网站楼层导航代码

    jQuery防京东浮动网站楼层导航代码   <!DOCTYPE html > <html xmlns="http://www.w3.org/1999/xhtml" ...

  9. 是什么让javascript变得如此奇妙

    What Makes Javascript Weird...and AWESOME -> First Class Functions -> Event-Driven Evironment ...

  10. Shell排序

    public void shellSort(int[] array) { int increment = array.length; do { increment = increment / 2; / ...