一、定义

vue的数据双向绑定是基于Object.defineProperty方法,通过定义data属性的get和set函数来监听数据对象的变化,一旦变化,vue利用发布订阅模式,通知订阅者执行回调函数,更新dom。

二、实现

vue关于数据绑定的生命周期是: 利用options的data属性初始化vue实力data---》递归的为data中的属性值添加observer--》编译html模板--》为每一个{{***}}添加一个watcher;

var app = new Vue({

  data:{

    message: 'hello world',

    age: 1,

    name: {

      firstname: 'mike',

      lastname: 'tom'

    }

  }

});  

1.初始化data属性

this.$data = options.data || {};

这个步骤比较简单将data属性挂在到vue实例上即可。

2.递归的为data中的属性值添加observer,并且添加对应的回调函数(initbinding)

function Observer(value, type) {
this.value = value;
this.id = ++uid;
Object.defineProperty(value, '$observer', {
value: this,
enumerable: false,
writable: true,
configurable: true
});
this.walk(value); // dfs为每个属性添加ob }
Observer.prototype.walk = function (obj) {
let val;
for (let key in obj) {
if (!obj.hasOwnProperty(key)) return; val = obj[key]; // 递归this.convert(key, val);
}
};
Observer.prototype.convert = function (key, val) {
let ob = this;
Object.defineProperty(this.value, key, {
enumerable: true,
configurable: true,
get: function () {
if (Observer.emitGet) {
ob.notify('get', key);
}
return val;
},
set: function (newVal) {
if (newVal === val) return;
val = newVal;
ob.notify('set', key, newVal);//这里是关键
}
});
};

上面代码中,set函数中的notify是关键,当用户代码修改了data中的某一个属性值比如app.$data.age = 2;,那么ob.notify就会通知observer来执行上面对应的回掉函数。

绑定回掉函数

exports._updateBindingAt = function (event, path) {
let pathAry = path.split('.');
let r = this._rootBinding;
for (let i = 0, l = pathAry.length; i < l; i++) {
let key = pathAry[i];
r = r[key];
if (!r) return;
}
let subs = r._subs;
subs.forEach((watcher) => {
watcher.cb(); // 这里执行watcher的回掉函数
});
}; /**
* 执行本实例所有子实例发生了数据变动的watcher
* @private
*/
exports._updateChildrenBindingAt = function () {
if (!this.$children.length) return;
this.$children.forEach((child) => {
if (child.$options.isComponent) return;
child._updateBindingAt(...arguments);
});
}; /**
* 就是在这里定于数据对象的变化的
* @private
*/
exports._initBindings = function () {
this._rootBinding = new Binding(); this.observer.on('set', this._updateBindingAt.bind(this))
};

 有2点需要注意:1),如果data中message:'hello world' => message: {id: 1, str: 'hello world'},message.id不会添加observer,所以一般为$data增加属性时,可以使用全局VM.set(target, key, value)方法。

2),如果是data属性值是一个数组,那么数组变化就不能检测到了,这时候可以从写这个数组对象的原生方法,在里面监听数据的变化就可以。具体做法是重写数组对象的__proto__。

const aryMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
const arrayAugmentations = []; aryMethods.forEach((method)=> { // 这里是原生Array的原型方法
let original = Array.prototype[method]; arrayAugmentations[method] = function () {
console.log('我被改变啦!');// 这里添加wather return original.apply(this, arguments);
}; }); let list = ['a', 'b', 'c']; list.__proto__ = arrayAugmentations;
list.push('d');

3.编译模板

这个是数据绑定的关键步骤,具体可以分为一下2个步骤。

A)解析htmlElement节点,这里要dfs所有的dom和上面对应的指令(v-if,v-modal)之类的

B)解析文本节点,把文本节点中的{{***}}解析出来,通过创建textNode的方法来解析为真正的HTML文件

在解析的过程中,会对指令和模板添加Directive对象和Watcher对象,当data对象的属性值发生变化的时候,调用watcher的update方法,update方法中保存的是Directive对象更新dom方法,把在当directive对应的textNode的nodeValue变成新的data中的值。比如执行app.$data.age = 1;

首先编译模板

exports._compile = function () {

    this._compileNode(this.$el);
}; /**
* 渲染节点
* @param node {Element}
* @private
*/
exports._compileElement = function (node) { if (node.hasChildNodes()) {
Array.from(node.childNodes).forEach(this._compileNode, this);
}
}; /**
* 渲染文本节点
* @param node {Element}
* @private
*/
exports._compileTextNode = function (node) {
let tokens = textParser.parse(node.nodeValue); // [{value:'姓名'}, {value: 'name‘,tag: true}]
if (!tokens) return; tokens.forEach((token) => {
if (token.tag) {
// 指令节点
let value = token.value;
let el = document.createTextNode('');
_.before(el, node);
this._bindDirective('text', value, el);
} else {
// 普通文本节点
let el = document.createTextNode(token.value);
_.before(el, node);
}
}); _.remove(node);
}; exports._compileNode = function (node) {
switch (node.nodeType) {
// text
case 1:
this._compileElement(node);
break;
// node
case 3 :
this._compileTextNode(node);
break;
default:
return;
}
};

上面代码中在编译textNode的时候会执行bindDirctive方法,该方法的作用就是绑定指令,{{***}}其实也是一条指令,只不过是一个特殊的text指令,他会在本ob对象的directives属性上push一个Directive对象。Directive对象本身在构造的时候,在构造函数中会实例化Watcher对象,并且执行directive的update方法(该方法就是把当前directive对应的dom更新),那么编译完成后就是对应的html文件了。

/**
* 生成指令
* @param name {string} 'text' 代表是文本节点
* @param value {string} 例如: user.name 是表示式
* @param node {Element} 指令对应的el
* @private
*/
exports._bindDirective = function (name, value, node) {
let descriptors = dirParser.parse(value);
let dirs = this._directives;
descriptors.forEach((descriptor) => {
dirs.push(
new Directive(name, node, this, descriptor)
);
});
};
function Directive(name, el, vm, descriptor) {
this.name = name;
this.el = el; // 对应的dom节点
this.vm = vm;
this.expression = descriptor.expression;
this.arg = descriptor.arg;this._bind();
} /**
* @private
*/
Directive.prototype._bind = function () {
if (!this.expression) return; this.bind && this.bind(); // 非组件指令走这边
this._watcher = new Watcher(
// 这里上下文非常关键
// 如果是普通的非组件指令, 上下文是vm本身
// 但是如果是prop指令, 那么上下文应该是该组件的父实例
(this.name === 'prop' ? this.vm.$parent : this.vm),
this.expression,
this._update, // 回调函数,目前是唯一的,就是更新DOM
this // 上下文
);
this.update(this._watcher.value); };
exports.bind = function () {
}; /**
* 这个就是textNode对应的更新函数啦
*/
exports.update = function (value) {
this.el['nodeValue'] = value;
console.log("更新了", value);
};

但是,用户代码修改了data怎么办,下面是watcher的相关代码,watcher来帮你解决这个问题。

/**
* Watcher构造函数
* 有什么用呢这个东西?两个用途
* 1. 当指令对应的数据发生改变的时候, 执行更新DOM的update函数
* 2. 当$watch API对应的数据发生改变的时候, 执行你自己定义的回调函数
* @param vm
* @param expression {String} 表达式, 例如: "user.name"
* @param cb {Function} 当对应的数据更新的时候执行的回调函数
* @param ctx {Object} 回调函数执行上下文
* @constructor
*/
function Watcher(vm, expression, cb, ctx) {
this.id = ++uid;
this.vm = vm;
this.expression = expression;
this.cb = cb;
this.ctx = ctx || vm;
this.deps = Object.create(null);//deps是指那些嵌套的对象属性,比如name.frist 那么该watcher实例的deps就有2个属性name和name.first属性
this.initDeps(expression);
}
/**
* @param path {String} 指令表达式对应的路径, 例如: "user.name"
*/
Watcher.prototype.initDeps = function (path) {
this.addDep(path);
this.value = this.get();
}; /**
根据给出的路径, 去获取Binding对象。
* 如果该Binding对象不存在,则创建它。
* 然后把当前的watcher对象添加到binding对象上,binding对象的结构和data对象是一致的,根节点但是rootBinding,所以根据path可以找到对应的binding对象
* @param path {string} 指令表达式对应的路径, 例如"user.name"
*/
Watcher.prototype.addDep = function (path) {
let vm = this.vm;
let deps = this.deps;
if (deps[path]) return;
deps[path] = true;
let binding = vm._getBindingAt(path) || vm._createBindingAt(path);
binding._addSub(this);
};

初始化所有的绑定关系之后,就是wather的update了

/**
* 当数据发生更新的时候, 就是触发notify
* 然后冒泡到顶层的时候, 就是触发updateBindingAt
* 对应的binding包含的watcher的update方法就会被触发。
* 就是执行watcher的cb回调。watch在
* 两种情况, 如果是$watch调用的话,那么是你自己定义的回调函数,开始的时候initBinding已经添加了回调函数
* 如果是directive,那么就是directive的_update方法
* 其实就是各自对应的更新方法。比如对应文本节点来说, 就是更新nodeValue的值
*/

三、结论

vue数据绑定原理的更多相关文章

  1. 17: VUE数据绑定 与 Object.defineProperty

    VUE数据绑定原理:https://segmentfault.com/a/1190000006599500?utm_source=tag-newest Object.defineProperty(): ...

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

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

  3. Vue数据绑定和响应式原理

    Vue数据绑定和响应式原理 当实例化一个Vue构造函数,会执行 Vue 的 init 方法,在 init 方法中主要执行三部分内容,一是初始化环境变量,而是处理 Vue 组件数据,三是解析挂载组件.以 ...

  4. 「每日一题」有人上次在dy面试,面试官问我:vue数据绑定的实现原理。你说我该如何回答?

    关注「松宝写代码」,精选好文,每日一题 ​时间永远是自己的 每分每秒也都是为自己的将来铺垫和增值 作者:saucxs | songEagle 来源:原创 一.前言 文章首发在「松宝写代码」 2020. ...

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

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

  6. vue 实现数据绑定原理

      案例: Vue 底层原理   // 目的: 使用原生js来实现Vue深入响应式   var box = document.querySelector('.box')   var button = ...

  7. 浅析vue数据绑定

    前言:最近团队需要做一个分享,脚进脑子,不知如何分享.最后想着之前一直想研究一下 vue 源码,今天刚好 "借此机会" 研究一下. 网上研究vue数据绑定的文章已经非常多了,但是自 ...

  8. Vue工作原理小结

    本文能帮你做什么?1.了解vue的双向数据绑定原理以及核心代码模块2.缓解好奇心的同时了解如何实现双向绑定为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考 ...

  9. vue运行原理

    Vue工作原理小结 本文能帮你做什么? 1.了解vue的双向数据绑定原理以及核心代码模块 2.缓解好奇心的同时了解如何实现双向绑定 为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简 ...

随机推荐

  1. Swoole笔记(三)

    WebSocket 使用Swoole可以很简单的搭建异步非阻塞多进程的WebSocket服务器. WebSocket服务器 <?php $server = new swoole_websocke ...

  2. poj2104(划分树模板)

    poj2104 题意 给出一个序列,每次查询一个区间,要求告诉这个区间排序后的第k个数. 分析 划分树模板,O(mlogn). 建树.根据排序之后的数组,对于一个区间,找到中点的数,将整个区间分为左右 ...

  3. PHP漏洞之session会话劫持

    本文主要介绍针对PHP网站Session劫持.session劫持是一种比较复杂的攻击方法.大部分互联网上的电脑多存在被攻击的危险.这是一种劫持tcp协议的方法,所以几乎所有的局域网,都存在被劫持可能. ...

  4. sqlmap详细使用 [精简]

    1. 基础用法: 一下./sqlmap.py 在kali和backtrack中使用sqlmap的时候,直接用:sqlmap ./sqlmap.py -u “注入地址” -v 1 –dbs   // 列 ...

  5. [BZOJ3675]序列分割

    3675: [Apio2014]序列分割 Time Limit: 40 Sec  Memory Limit: 128 MB Description 小H最近迷上了一个分隔序列的游戏.在这个游戏里,小H ...

  6. Java 并发 – 线程安全?

    线程安全的定义常常让人迷惑,搜索引擎会发现无数定义,比如: 多个线程同时执行也能正确工作就是线程安全的代码 多个线程同时执行能以正确的方式操纵共享数据就是线程安全的代码. 而且还有很多类似的定义 你是 ...

  7. 浅谈js中的正则表达式

    很多时候多会被正则表达式搞的晕头转向,最近抽出时间对正则表达式进行了系统的学习,整理如下: 正则表达式的创建 两种方法,一种是直接写,由包含在斜杠之间的模式组成:另一种是调用RegExp对象的构造函数 ...

  8. 耍一把codegen,这样算懂编译么?

    最近使用protobuf搭了些服务器,对protobuf的机制略感兴趣,所以研究了下. 大致分析没有什么复杂的 1 对定义的结构体生成消息封包协议 2 对定义的rpc函数生成接口定义 3 用户按pro ...

  9. Java NIO 核心组件学习笔记

    背景知识 同步.异步.阻塞.非阻塞 首先,这几个概念非常容易搞混淆,但NIO中又有涉及,所以总结一下[1]. 同步:API调用返回时调用者就知道操作的结果如何了(实际读取/写入了多少字节). 异步:相 ...

  10. JavaScript深入浅出补充——(二)语句和严格模式,对象

    三.语句和严格模式 JavaScript程序由语句组成,语句遵守语法规则. 例如:if语句,while语句,with语句等等-- block块语句 常用于组合0~多个语句,块语句用{}定义 直接以花括 ...