Vue用了有一段时间了,每当有人问到Vue双向绑定是怎么回事的时候,总是不能给大家解释的很清楚,正好最近有时间把它梳理一下,让自己理解的更清楚,下次有人问我的时候,可以侃侃而谈?。
//直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象
Object.defineProperty(obj,prop,descriptor)
| 属性 | 默认值 | 说明 |
|---|---|---|
| configurable | false | 描述属性是否可以被删除,默认为 false |
| enumerable | false | 描述属性是否可以被for...in或Object.keys枚举,默认为 false |
| writable | false | 描述属性是否可以修改,默认为 false |
| get | undefined | 当访问属性时触发该方法,默认为undefined |
| set | undefined | 当属性被修改时触发该方法,默认为undefined |
| value | undefined | 属性值,默认为undefined |
// Object.defineProperty(对象,属性,属性描述符)
var obj={}
console.log('obj:',obj);
Object.defineProperty(obj, 'name', {
value: 'James'
});
console.log('obj的默认值:',obj);
delete obj.name;
console.log('obj删除后:', obj);
console.log('obj枚举:', Object.keys(obj));
obj.name = '库里';
console.log('obj修改后:', obj);
Object.defineProperty(obj, 'name', {value: '库里'});
运行结果:
从运行结果可以发现,使用Object.defineProperty()定义的属性,默认是不可以被修改,不可以被枚举,不可以被删除的。可以与常规的方式定义属性对比一下:如果不使用Object.defineProperty()定义的属性,默认是可以修改、枚举、删除的:
const obj = {};
obj.name = 'James';
console.log('枚举:', Object.keys(obj));
obj.name = ' 库里';
console.log('修改:', obj);
delete obj.name;
console.log('删除:', obj);
运行结果:

const o = {};
Object.defineProperty(o, 'name', {
value: 'James', // name属性值
writable: true, // 可以被修改
enumerable: true, // 可以被枚举
configurable: true, // 可以被删除
});
console.log(o);
console.log('枚举:', Object.keys(o));
o.name = '科比';
console.log('修改:', o);
Object.defineProperty(o, 'name', {
value: 'Po'
});
console.log('修改:', o);
delete o.name;
console.log('删除:', o);
运行结果:

结果表明,修改writable、enumerable、configurable这三个描述符为true时,属性可以被修改、枚举和删除。
注意:
1、如果writable为false,configurable为true时,通过o.name = "科比"是无法修改成功的,但是使用Object.defineProperty()修改是可以成功的
2、如果writable和configurable都为false时,如果使用Object.defineProperty()修改属性值会报错:Cannot redefine property: name
const o = {};
Object.defineProperty(o, 'name', { value: 'James', enumerable: true });
Object.defineProperty(o, 'contact', { value: (str) => { return str+' baby' }, enumerable: false });
Object.defineProperty(o, 'age', { value: '18' });
o.skill = '前端';
console.log('枚举:', Object.keys(o));
console.log('trim: ', o.contact('nihao'))
console.log(`o.propertyIsEnumerable('name'): `, o.propertyIsEnumerable('name'));
console.log(`o.propertyIsEnumerable('contact'): `, o.propertyIsEnumerable('contact'));
console.log(`o.propertyIsEnumerable('age'): `, o.propertyIsEnumerable('age'));
运行结果:

注:设置set或者get,就不能在设置value和wriable,否则会报错
const o = {
__email: ''
};
Object.defineProperty(o, 'email', {
enumerable: true,
configurable: true,
// writable: true, // 如果设置了get或者set,writable和value属性必须注释掉
// value: '', // writable和value无法与set和get共存
get: function () { // 如果设置了get 或者 set 就不能设置writable和value
console.log('get', this);
return 'My email is ' + this.__email;
},
set: function (newVal) {
console.log('set', newVal);
this.__email = newVal;
}
});
console.log(o);
o.email = 'laowang@163.com';
o.email;
console.log(o);
o.email = 'laozhang@163.com';
console.log(o);
运行结果:

<!DOCTYPE html>
<head>
<title>最简单的双向绑定</title>
</head>
<body>
<div>
<input type="text" name="name" id="name" />
</div>
</body>
<script>
var data={
__name:''
};
Object.defineProperty(data,'name',{
enumerable: true,
configurable: true,
// writable: true, // 如果设置了get或者set,writable和value属性必须注释掉
// value: '', // writable和value无法与set和get共存
get: function () { // 如果设置了get 或者 set 就不能设置writable和value
return this.__name;
},
set: function (newVal) {
this.__name=newVal; //更新属性
document.querySelector('#name').value = newVal; //更新视图
}
});
//监听input事件,更新name
document.querySelector('#name').addEventListener("input",(event)=>{
data.name=event.currentTarget.value
})
</script>
</html>
运行结果:

文本框输入"老王",查看name属性变为"老王";修改name属性为"老张",文本框变为“老张”;
最简单的双向绑定完成了?
vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。读完这句话是不是还有50%的懵逼,接下来继续分析。
双向绑定的经典示例图,各位细品:

分析每个模块的作用:
Observer:数据监听器,对每个vue的data中定义的属性循环用Object.defineProperty()实现数据劫持,以便利用其中的setter和getter,然后通知订阅者,订阅者会触发它的update方法,对视图进行更新
Compile:指令解析器,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
Watcher:作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
Dep:依赖收集,每个属性都有一个依赖收集对象,存储订阅该属性的Watcher
Updater:更新视图
1、首先创建index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>2.0双向绑定原理</title>
<script src="./Dep.js"></script>
<script src="./MYVM.js"></script>
<script src="./Observer.js"></script>
<script src="./Watcher.js"></script>
<script src="./TemplateCompiler.js"></script>
</head>
<body>
<div id="app">
<!--模拟vue指令绑定name属性 -->
<span v-text="name"></span>
<!--模拟vue指令v-model双向绑定 -->
<input type="text" v-model="name">
<!-- 模拟{{}} -->
{{name}}
</div>
<script>
//假设已经有MYVM对象,实例化该对象
//params是一个对象 el是要挂载的dom data是一个对象包含响应式属性
var vm = new MYVM({
el: '#app',
data: {
name: 'James'
}
})
</script>
</body>
</html>
2、创建MYVM.js,主要作用是调用Observer进行数据劫持和调用TemplateCompiler进行模板解析
function MYVM(options){
//属性初始化
this.$vm=this;
this.$el=options.el;
this.$data=options.data;
//视图必须存在
if(this.$el){
//添加属性观察对象(实现数据挟持)
new Observer(this.$data)
//创建模板编译器,来解析视图
this.$compiler = new TemplateCompiler(this.$el, this.$vm)
}
}
3、创建Observer.js,实现数据劫持
//数据解析,完成对数据属性的劫持
function Observer(data){
//判断data是否有效且data必须是对象
if(!data || typeof data !=='object' ){
return
}else{
var keys=Object.keys(data)
keys.forEach((key)=>{
this.defineReactive(data,key,data[key])
})
}
}
Observer.prototype.defineReactive=function(obj,key,val){
Object.defineProperty(obj,key,{
//是否可遍历
enumerable: true,
//是否可删除
configurable: false,
//取值
get(){
return val
},
//修改值
set(newVal){
val=newVal
}
})
}
上面代码完成了数据属性的劫持,读取和修改属性会执行get、set,运行结果:

4、给Observer.js增加订阅和发布功能,新建Dep.js,进行订阅和发布管理
//创建订阅发布者
//1.管理订阅
//2.集体通知
function Dep(){
this.subs=[];
}
//添加订阅
//参数sub是watcher对象
Dep.prototype.addSub=(sub)=>{
this.subs.push(sub)
}
//集体通知,更新视图
Dep.prototype.notify=()=>{
this.subs.forEach((sub) => {
sub.update()
})
}
5、把Dep安装到Observer.js,代码如下
//数据解析,完成对数据属性的劫持
function Observer(data){
//判断data是否有效且data必须是对象
if(!data || typeof data !=='object' ){
return
}else{
var keys=Object.keys(data)
keys.forEach((key)=>{
this.defineReactive(data,key,data[key])
})
}
}
Observer.prototype.defineReactive=function(obj,key,val){
//创建Dep实例
var dep=new Dep();
Object.defineProperty(obj,key,{
//是否可遍历
enumerable: true,
//是否可删除
configurable: false,
//取值
get(){
//watcher创建时,完成订阅
//检查target是否有watcher,有的话进行订阅
var watcher = Dep.target;
watcher && dep.addSub(watcher)
return val
},
//修改值
set(newVal){
val=newVal
dep.notify()
}
})
}
var dep=new Dep() 创建了Dep的实例
get的时候检查是否有watcher,有就添加到订阅数组
set的时候通知所有的订阅者,进行视图更新
至此属性数据劫持,订阅和发布就已经实现完了
6、接下来实现模板编译器,首先创建TemplateCompiler.js
// 创建模板编译工具
// el 要编译的dom节点
// vm MYVM的当前实例
function TemplateCompiler(el,vm){
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
//将对应范围的html放入内存fragment
var fragment = this.node2Fragment(this.el)
//编译模板
this.compile(fragment)
//将数据放回页面
this.el.appendChild(fragment)
}
}
//是否是元素节点
TemplateCompiler.prototype.isElementNode=function(node){
return node.nodeType===1
}
//是否是文本节点
TemplateCompiler.prototype.isTextNode=function(node){
return node.nodeType===3
}
//转成数组
TemplateCompiler.prototype.toArray=function(arr){
return [].slice.call(arr)
}
//判断是否是指令属性
TemplateCompiler.prototype.isDirective=function(directiveName){
return directiveName.indexOf('v-') >= 0;
}
//读取dom到内存
TemplateCompiler.prototype.node2Fragment=function(node){
var fragment=document.createDocumentFragment();
var child;
//while(child=node.firstChild)这行代码,每次运行会把firstChild从node中取出,指导取出来是null就终止循环
while(child=node.firstChild){
fragment.appendChild(child)
}
return fragment;
}
//编译模板
TemplateCompiler.prototype.compile=function(fragment){
var childNodes = fragment.childNodes;
var arr = this.toArray(childNodes);
arr.forEach(node => {
//判断是否是元素节点
if(this.isElementNode(node)){
this.compileElement(node);
}else{
//定义文本表达式验证规则
var textReg = /\{\{(.+)\}\}/;
var expr = node.textContent;
if (textReg.test(expr)) {
//获取绑定的属性
expr = RegExp.$1;
//调用方法编译
this.compileText(node, expr)
}
}
});
}
//解析元素节点
TemplateCompiler.prototype.compileElement=function(node){
//获取节点所有属性
var arrs=node.attributes;
this.toArray(arrs).forEach(attr => {
//获取属性名称
var attrName=attr.name;
if(this.isDirective(attrName)){
//获取v-modal的modal
var type = attrName.split('-')[1]
//获取属性对应的值(绑定的属性)
var expr = attr.value;
CompilerUtils[type] && CompilerUtils[type](node, this.vm, expr)
}
});
}
//解析文本节点
TemplateCompiler.prototype.compileText=function(node,expr){
CompilerUtils.text(node, this.vm, expr)
}
TemplateCompiler的主要逻辑:
a、dom节点读入到内存
b、遍历所有节点,判断节点类型,元素节点和文本节点分别使用不同方法编译
c、元素节点编译,遍历所有属性,根据指令名称称找到CompilerUtils对应的指令处理方法,执行视图初始化和订阅
d、文本节点编译,正则匹配找到绑定的属性,使用CompilerUtils的text执行初始化和订阅
7、创建CompilerUtils编辑工具对象,实现视图初始化和订阅
//编译工具
CompilerUtils = {
//对应视图v-modal指令,使用该方法进行视图初始化和订阅
//params node当前节点 vm myvm对象 expr绑定的属性
//modal方法执行一次,进行视图初始化、事件订阅,添加视图到模型的事件
model(node, vm, expr) {
//节点更新方法
var updateFn = this.updater.modelUpdater;
//初始化,更新node的值
updateFn && updateFn(node, vm.$data[expr])
//实例化一个订阅者,添加到订阅数组
new Watcher(vm, expr, (newValue) => {
//发布的时候,按照之前的规则,对节点进行更新
updateFn && updateFn(node, newValue)
})
//视图到模型(观察者模式)
node.addEventListener('input', (e) => {
//获取新值放到模型
var newValue = e.target.value;
vm.$data[expr] = newValue;
})
},
//对应视图v-text指令,使用该方法进行视图初始化和订阅
//params node当前节点 vm myvm对象 expr绑定的属性
//text方法执行一次,进行视图初始化、事件订阅
text(node, vm, expr) {
//text更新方法
var updateFn = this.updater.textUpdater;
//初始化,更新text的值
updateFn && updateFn(node, vm.$data[expr])
//实例化一个订阅者,添加到订阅数组
new Watcher(vm, expr, (newValue) => {
//发布的时候,按照之前的规则,对文本节点进行更新
updateFn && updateFn(node, newValue)
})
},
updater: {
//v-text数据更新
textUpdater(node, value) {
node.textContent = value;
},
//v-model数据更新
modelUpdater(node, value) {
node.value = value;
}
}
}
CompilerUtils的主要逻辑:
a、根据指令对节点进行数据初始化,实例化观察者Watcher到订阅数组
b、不同的指令进行不同的逻辑处理
8、创建Watcher.js,实现订阅者逻辑
//声明一个订阅者
//vm 全局vm对象
//expr 属性名称
//cb 发布时需要执行的方法
function Watcher(vm, expr, cb) {
//缓存重要属性
this.vm = vm;
this.expr = expr;
this.cb = cb;
//缓存当前值,为更新时做对比
this.value = this.get()
}
Watcher.prototype.get=function(){
//设置全局Dep的target为当前订阅者
Dep.target = this;
//获取属性的当前值,获取时会执行属性的get方法,get方法会判断target是否为空,不为空就添加订阅者
var value = this.vm.$data[this.expr]
//清空全局
Dep.target = null;
return value;
}
Watcher.prototype.update=function(){
//获取新值
var newValue = this.vm.$data[this.expr]
//获取老值
var old = this.value;
//判断后
if (newValue !== old) {
//执行回调
this.cb(newValue)
}
}
Watcher的主要逻辑:
a、get 把当前订阅者添加到属性对应的依赖数组,保存值
b、update 发布的时候执行,进行新老值对比,更新节点内容
到此一个简单的MVVM框架就完成了,整体运行效果如下:

梳理过程中参考很多大佬文章,感谢各位。看完基本能把VUE2.0的双向绑定原理讲清楚了,希望能帮助有缘人,?!
我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden
有没有办法在这个简单的get方法中添加超时选项?我正在使用法拉第3.3。Faraday.get(url)四处寻找,我只能先发起连接后应用超时选项,然后应用超时选项。或者有什么简单的方法?这就是我现在正在做的:conn=Faraday.newresponse=conn.getdo|req|req.urlurlreq.options.timeout=2#2secondsend 最佳答案 试试这个:conn=Faraday.newdo|conn|conn.options.timeout=20endresponse=conn.get(url
我想在Ruby中创建一个用于开发目的的极其简单的Web服务器(不,不想使用现成的解决方案)。代码如下:#!/usr/bin/rubyrequire'socket'server=TCPServer.new('127.0.0.1',8080)whileconnection=server.acceptheaders=[]length=0whileline=connection.getsheaders想法是从命令行运行这个脚本,提供另一个脚本,它将在其标准输入上获取请求,并在其标准输出上返回完整的响应。到目前为止一切顺利,但事实证明这真的很脆弱,因为它在第二个请求上中断并出现错误:/usr/b
我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。
我的工作要求我为某些测试自动生成电子邮件。我一直在四处寻找,但未能找到可以快速实现的合理解决方案。它需要在outlook而不是其他邮件服务器中,因为我们有一些奇怪的身份验证规则,我们需要保存草稿而不是仅仅发送邮件的选项。显然win32ole可以做到这一点,但我找不到任何相当简单的例子。 最佳答案 假设存储了Outlook凭据并且您设置为自动登录到Outlook,WIN32OLE可以很好地完成此操作:require'win32ole'outlook=WIN32OLE.new('Outlook.Application')message=
华为OD机试题本篇题目:明明的随机数题目输入描述输出描述:示例1输入输出说明代码编写思路最近更新的博客华为od2023|什么是华为od,od薪资待遇,od机试题清单华为OD机试真题大全,用Python解华为机试题|机试宝典【华为OD机试】全流程解析+经验分享,题型分享,防作弊指南华为o
C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.
我正在尝试在Rails上安装ruby,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf
//1.验证返回状态码是否是200pm.test("Statuscodeis200",function(){pm.response.to.have.status(200);});//2.验证返回body内是否含有某个值pm.test("Bodymatchesstring",function(){pm.expect(pm.response.text()).to.include("string_you_want_to_search");});//3.验证某个返回值是否是100pm.test("Yourtestname",function(){varjsonData=pm.response.json
在前面两节的例子中,主界面窗口的尺寸和标签控件显示的矩形区域等,都是用C++代码编写的。窗口和控件的尺寸都是预估的,控件如果多起来,那就不好估计每个控件合适的位置和大小了。用C++代码编写图形界面的问题就是不直观,因此Qt项目开发了专门的可视化图形界面编辑器——QtDesigner(Qt设计师)。通过QtDesigner就可以很方便地创建图形界面文件*.ui,然后将ui文件应用到源代码里面,做到“所见即所得”,大大方便了图形界面的设计。本节就演示一下QtDesigner的简单使用,学习拖拽控件和设置控件属性,并将ui文件应用到Qt程序代码里。使用QtDesigner设计界面在开始菜单中找到「Q