原生js实现 vue的数据双向绑定
原生js实现一个简单的vue的数据双向绑定
vue是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调
所以我们要先做好下面3步:
1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。
3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。
1.实现一个Observer
Observer是一个数据监听器,主要依赖于Object.defineProperty()方法,而这个方法在ie8及以下存在兼容问题,请看(MDN defineProperty)所以如vue官网所说:
兼容性
Vue 不支持 IE8 及以下版本,因为 Vue 使用了 IE8 无法模拟的 ECMAScript 5 特性。但它支持所有(兼容 ECMAScript 5 的浏览器。)
正因为这个方法,我们就可以利用Obeject.defineProperty()来监听属性变动 那么就可以把需要observer的数据对象进行递归遍历,给他的每个属性都可以加上get,set。
而当给这个对象的某个值赋值操作的时候,就会触发setter,那么就能监听到了数据变化。
function observer(data) {
// 当不是对象的时候,退出
if (!data || typeof data !== 'object') {
return;
}
// 取出所有属性遍历
Object.keys(data).forEach(function(key) {
// 给每个属性加上get,set
defineReactive(data, key, data[key]);
});
};
function defineReactive(data, key, val) {
observer(val); // 监听子属性
Object.defineProperty(data, key, {
enumerable: true, // 可遍历
configurable: false, // 不能修改,删除
get: function() {
return val;
},
set: function(newVal) {
val = newVal;
console.log("已改变 "+newVal);
}
});
}
测试一下:
var obj={
name:'aaaaa',
book:{
name:'bbbbbb'
}
}
observer(obj)
obj.name='cc' //已改变 cc
obj.book.name='dd' //已改变 dd
现在已经监听了数据中的属性变化了,而接下来就是在代码中加入发布者-订阅者模式,关于这个设计模式不懂的可以看这里(js设计模式-发布订阅模式)
首先我们先创造一个订阅器Dep,他是用来收集订阅者Watcher的,然后在属性发生变化的时候执行对应订阅者的更新函数update 。
在上面的 defineReactive 函数最后加入下面代码:
function Dep() {
this.subs = [];//存放消息数组
}
Dep.prototype = {
addSub: function(sub) { //增加订阅者函数
this.subs.push(sub);
},
notify: function() { //发布消息函数
this.subs.forEach(function(sub) {
sub.update(); //这里是订阅者的更新方法
});
}
};
然后修改上面的 defineReactive 函数:
function defineReactive(data, key, val) {
var dep = new Dep(); //实例化一个订阅器
observer(val); // 监听子属性
Object.defineProperty(data, key, {
enumerable: true, // 可遍历
configurable: false, // 不能修改,删除
get: function() {
return val;
},
set: function(newVal) {
if (val === newVal){return} //当前后数值相等,不做改变
val = newVal;
dep.notify(); //当前后数值变化,这时就通知订阅者了
console.log("已改变 "+newVal);
}
});
}
在vue里面针对data这个对象的处理,区分了数组和对象,我们这只考虑对象这一种情况,更多(vue-数组处理)正由于这种处理,所以vue对于:
由于 JavaScript 的限制,Vue 不能检测以下变动的数组:
- 当你利用索引直接设置一个项时,例如:
vm.items[indexOfItem] = newValue- 当你修改数组的长度时,例如:
vm.items.length = newLength
这种情况,设计了 vm.$set 这个实例方法来弥补这方面限制带来的不便。
2.实现Watcher
在第一步我们实现了订阅器,这一步实现订阅者,从上面代码看,在dep 调用 notify() 方法的时候,我们就该去遍历订阅者Watcher了,并且调用他自己的update()方法,
先实现订阅者对象,如下:
function Watcher (vm, exp, cb){
this.cb = cb;
this.vm = vm;
this.exp = exp;
this.value = this.get();//初始化的时候就调用
}
Watcher.prototype={
// 只有在订阅者Watcher初始化的时候才需要添加订阅者
get:function(){
Dep.target = this; // 在Dep.target缓存下订阅者
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数
Dep.target = null; // 释放订阅者
return value;
},
// dep.subs[i].notify() 会执行到这里
update:function() {
this.run();
},
run:function() {
// 执行 get()获得value ,call更改cb的this指向 。
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
}
}
而在这个时候,我们需要通过Dep的addSub(),将Watcher加进去,在Watcher的构造函数中会调用这句话
this.value = this.get();//初始化的时候就调用
所以我们可以在 defineReactive 函数中做个调整,修改一下Object.defineProperty的get要调用的函数,来对应Watcher构造函数的这个get函数。
而在这里需要判断一下:是不是Watcher的构造函数在调用,如果是,说明他就是这个属性的订阅者。
这里就会用到 Dep.target 这样一个全局唯一的变量,用来判断。代码如下:
function defineReactive(data, key, val) {
var dep = new Dep(); //实例化一个订阅器
observer(val); // 监听子属性
Object.defineProperty(data, key, {
enumerable: true, // 可遍历
configurable: false, // 不能修改,删除
get: function() {
// 如果这个属性存在,说明这是watch 引起的
if(Dep.target){
// 那我调用dep.addSub把这个订阅者加入订阅器里面
dep.addSub(Dep.target)
}
return val;
},
set: function(newVal) {
if (val === newVal){return} //当前后数值相等,不做改变
val = newVal;
dep.notify(); //当前后数值变化,这时就通知订阅者了
console.log("已改变 "+newVal);
}
});
}
最后在Dep函数后面加上:
Dep.target = null;//释放每一个订阅者
写到这里我们可以写个简单的例子来测试一下:
先写个函数将我们的Observer和Watcher关联起来:
function pvp(data,el,exp){
this.data=data;
observer(data); //给data每个属性加上get,set
el.innerHTML=this.data[exp]; //粗暴的直接绑定,测试一下效果
new Watcher(this,exp,function(val){
el.innerHTML = val; //调用Watcher直接赋值
})
return this
}
在html中写下:
<h1 id="name">{{name}}</h1>
js中写下:
var ele = document.querySelector('#app');
var pvp = new pvp(
{test: 'hello pvp'},
ele,
'test'
);
结果如下:

好了,下面我们来实现对dom节点的解析,可以让pvp写起来更像vue的写法
3.实现Compile
解析模板,首先需要获取到dom元素,然后对含有dom元素上含有指令的节点进行处理,这个环节需要对dom操作比较频繁,
所以可以用一个通用的办法,先建一个fragment片段,将需要解析的dom节点存入fragment片段里再进行处理。请看(空文档对象)
//创造一个空白节点
nodeToFragment: function (el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// 将Dom每个元素都移入fragment中
fragment.appendChild(child);
child = el.firstChild;
}
return fragment;
},
而在这里我们需要实现一个 Compile 方法,代码如下:
function Compile(el, vm) {
this.vm = vm;
this.el = document.querySelector(el);
this.fragment = null;
this.init(); //初始化一个方法,直接调用解析节点
}
Compile.prototype = {
init: function () {
if (this.el) {
this.fragment = this.nodeToFragment(this.el); //调用了上面的方法,把元素放入并返回
this.compileElement(this.fragment);//对这个里面的元素解析
this.el.appendChild(this.fragment);//再重新放回去
} else {
console.error("找不到节点")
}
},
//创造一个空白节点
nodeToFragment: function (el) {
var fragment = document.createDocumentFragment();
var child = el.firstChild;
while (child) {
// 将Dom每个元素都移入fragment中
fragment.appendChild(child);
child = el.firstChild;
}
return fragment;
},
// 解析节点
compileElement: function (el) {
var childNodes = el.childNodes;
var self = this;
[].slice.call(childNodes).forEach(function(node) {
var reg = /\{\{(.*)\}\}/;
var text = node.textContent;
if (node.nodeType == 1) { //如果是元素节点
self.compileFirst(node);
} else if (node.nodeType == 3 && reg.test(text)) { //如果是文本节点
self.compileText(node, reg.exec(text)[1]);
}
if (node.childNodes && node.childNodes.length) { //如果下面还有子节点,继续循环
self.compileElement(node);
}
});
},
//如果是元素节点
compileFirst: function(node) {
var nodeAttrs = node.attributes;
var self = this;
Array.prototype.forEach.call(nodeAttrs, function(attr) {
var attrName = attr.name;
var exp = attr.value;
if (attrName='p-model') { //当这个属性为p-model的时候就解析model
self.compileModel(node, self.vm, exp);
}
});
},
//如果是文本节点
compileText: function(node, exp) {
var self = this;
var initText = this.vm[exp];
this.updateText(node, initText);
new Watcher(this.vm, exp, function (value) {
self.updateText(node, value);//通知Watcher,开始订阅
});
},
//解析p-model
compileModel: function (node, vm, exp) {
var self = this;
var val = this.vm[exp];
this.modelUpdater(node, val);
new Watcher(this.vm, exp, function (value) {
self.modelUpdater(node, value); //通知Watcher,开始订阅
});
node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
}
self.vm[exp] = newValue;
val = newValue;
});
},
updateText: function (node, value) {
//这里是直接替换文本节点
node.textContent = typeof value == 'undefined' ? '' : value;
},
modelUpdater: function(node, value, oldValue) {
//如果不存在就返回空,这里是更新model
node.value = typeof value == 'undefined' ? '' : value;
}
}
然后我们在对上面的关联函数 pvp 进行修改主要是对 data 用 defineProperty 方法进行再一次封装,方便我们使用 xx.data 的写法去调用属性,在对这个函数进行修改,代码如下:
function Pvp(options){
var self = this;
this.data=options.data;
Object.keys(this.data).forEach(function(key) {
self.proxyKeys(key);
});
observer(this.data); //给data每个属性加上get,set
new Compile(options.el,this)
return this
}
Pvp.prototype = {
proxyKeys: function (key) {
var self = this;
Object.defineProperty(this, key, {
enumerable: false,
configurable: true,
get: function getter () {
return self.data[key];
},
set: function setter (newVal) {
self.data[key] = newVal;
}
});
}
}
最后在我们的html的script中写下:
var src=new Pvp({
el: '#app',
data: {
test: 'hello world',
}
});
打完收工!!!! 最后点(源码)获取源码
原生js实现 vue的数据双向绑定的更多相关文章
- Vue的数据双向绑定和Object.defineProperty()
Vue是前端三大框架之一,也被很多人指责抄袭,说他的两个核心功能,一个数据双向绑定,一个组件化分别抄袭angular的数据双向绑定和react的组件化思想,咱们今天就不谈这种大是大非,当然我也没到达那 ...
- vue中数据双向绑定的实现原理
vue中最常见的属v-model这个数据双向绑定了,很好奇它是如何实现的呢?尝试着用原生的JS去实现一下. 首先大致学习了解下Object.defineProperty()这个东东吧! * Objec ...
- 【Vue】-- 数据双向绑定的原理 --Object.defineProperty()
Object.defineProperty()方法被许多现代前端框架(如Vue.js,React.js)用于数据双向绑定的实现,当我们在框架Model层设置data时,框架将会通过Object.def ...
- 对象的属性类型 和 VUE的数据双向绑定原理
如[[Configurable]] 被两对儿中括号 括起来的表示 不可直接访问他们 修改属性类型:使用Object.defineProperty() //IE9+ 和标准浏览器 支持 查看属性的 ...
- vue实现数据双向绑定的原理
一.知识准备Object.defineProperty( )方法可以直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,并返回这个对象.Object.defineProperty(obj,pr ...
- 利用JS实现vue中的双向绑定
Vue 已经是主流框架了 它的好处也不用多说,都已经是大家公认的了 那我们就来理解一下Vue的单向数据绑定和双向数据绑定 然后再使用JS来实现Vue的双向数据绑定 单向数据绑定 指的是我们先把模板写好 ...
- 一、vue的数据双向绑定的实现
响应式系统 一.概述 Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图 ...
- vue中数据双向绑定注意点
最近一个vue和element的项目中遇到了一个问题: 动态生成的对象进行双向绑定是失败 直接贴代码: <el-form :model="addClass" :rules=& ...
- Vue的数据双向绑定原理——Object-defineProperty
一.定义 ①方法会直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象. ②vue.js的双向数据绑定就是通过Object.defineProperty方法实现的,俗称属性拦截 ...
随机推荐
- 基于Bilateral Attention和Pyramid Filling Block的图像修复方法
One-Stage Inpainting with Bilateral Attention and Pyramid Filling Block 论文链接:https://arxiv.org/abs/1 ...
- ES数据库重建索引——Reindex(数据迁移)
应用背景: 1.当你的数据量过大,而你的索引最初创建的分片数量不足,导致数据入库较慢的情况,此时需要扩大分片的数量,此时可以尝试使用Reindex. 2.当数据的mapping需要修改,但是大量的数据 ...
- Android蓝牙读取短信调研
对“直接通过蓝牙来获取连接手机的短信信息”这个需求做了一些技术调研,如下是调研过程中的一些记录. 1.无法得到BluetoothMasClient类 在文章https://blog.csdn.net/ ...
- redis实操-sentinel
本文主要记录一些操作步骤,作为自己学习的一个记录,也供虚拟机上学习redis的人参考. 实操篇,可以参考 http://redis.cn/ 主从复制实操 在test目录下新建配置进行练习 1.mkdi ...
- Pandas 复习2
import pandas as pd import numpy as np food_info = pd.read_csv('food_info.csv') 1.处理缺失值(可使用平均数,众数填充) ...
- 线程_使用multiprocessing启动一个子进程及创建Process 的子类
from multiprocessing import Process import os # 子进程执行的函数 def run_proc(name): print("子进程运行中,名称:% ...
- Skill 返回list中的倒数第二个数据
https://www.cnblogs.com/yeungchie/ code procedure(ycLast2(list) prog((index ouput) unless(type(list) ...
- MyBatis-Plus使用(4)-集成SpringBoot
我这里使用的MyBatis-Plus是当前最新的3.2.0版本, 1. 引入需要的jar,基础jar包括: <dependencies> <dependency> <gr ...
- 符合AUTOSAR(AP&CP)的嵌入式系统和软件设计工具
AUTOSAR Builder功能介绍 AUTOSAR Builder 是达索旗下一种基于 Eclipse 的开放性.可扩展工具套件,用于设计和开发符合 AUTOSAR 标准的系统和软件.最新版本20 ...
- C调用C++代码
有时C程序里需要用到C++的类,但是C语言又不能直接调用类,这时需要把C++的类使用C接口封装后,再调用, 可以将封装后的C++代码编译成库文件,供C语言调用: 需要注意的是,封装的C++代码库文件是 ...