vue 之 双向绑定原理
一、实现双向绑定
详细版:
前端MVVM实现双向数据绑定的做法大致有如下三种:
1.发布者-订阅者模式(backbone.js)
思路:使用自定义的data属性在HTML代码中指明绑定。所有绑定起来的JavaScript对象以及DOM元素都将“订阅”一个发布者对象。任何时候如果JavaScript对象或者一个HTML输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。
vueJS 的思路流程:发布者dep发出通知 => 主题对象subs收到通知并推送给订阅者 => 订阅者watcher执行相应操作
2.脏值检查(angular.js)
思路:angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过
setInterval()
定时轮询检测数据变动,angular只有在指定的事件触发时进入脏值检测,大致如下:
DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
XHR响应事件 ( $http )
浏览器Location变更事件 ( $location )
Timer事件( $timeout , $interval )
执行 $digest() 或 $apply()
3.数据劫持(Vue.js)
思路: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过
Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变动时发布消息给订阅者,触发相应的监听回调。
Object.defineProperty
作用定义:直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
Object.defineProperty(obj, prop, descriptor)
参数 obj => 要在其上定义属性的对象;
prop => 要定义或修改的属性的名称;
descriptor => 将被定义或修改的属性描述符。
属性描述符 => 数据描述符和存取描述符,两者取一
数据描述符: 具有值的属性
存取描述符: 由getter-setter函数对描述的属性
具有的属性:
注:configurable 可配置性相当于属性的总开关,只有为true时才能设置,而且不可逆
enumerable 是否可枚举,为false时for..in以及Object.keys()将不能枚举出该属性
writable 是否可写,为false时将不能够修改属性的值
get 一个给属性提供 getter 的方法
set 一个给属性提供 setter 的方法
返回值 被传递给函数的对象obj。
示例:
var obj = {};
Object.defineProperty(obj, 'hello', {
get: function() {
console.log('get val:'+ val);
return val;
},
set: function(newVal) {
val = newVal;
console.log('set val:'+ val);
}
}); obj.hello; // 触发 getter =>get val:undefined
obj.hello='111'; // 触发 setter =>set val:111
obj.hello; // 触发 getter =>get val:111
vue 实现双向绑定
实现mvvm的双向绑定的步骤:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者
流程图:
流程解析:
从图中可以看出,当执行 new Vue() 时,Vue 就进入了初始化阶段,一方面Vue 会遍历 data 选项中的属性,并用 Object.defineProperty 将它们转为 getter/setter,实现数据变化监听功能;另一方面,Vue 的指令编译器Compile 对元素节点的指令进行解析,初始化视图,并订阅Watcher 来更新视图, 此时Wather 会将自己添加到消息订阅器中(Dep),初始化完毕。当数据发生变化时,Observer 中的 setter 方法被触发,setter 会立即调用Dep.notify(),Dep 开始遍历所有的订阅者,并调用订阅者的 update 方法,订阅者收到通知后对视图进行相应的更新。因为VUE使用Object.defineProperty方法来做数据绑定,而这个方法又无法通过兼容性处理,所以Vue 不支持 IE8 以及更低版本浏览器。另外,查看vue原代码,发现在vue初始化实例时, 有一个proxy代理方法,它的作用就是遍历data中的属性,把它代理到vm的实例上,这也就是我们可以这样调用属性:vm.a等于vm.data.a。
Observer
利用Obeject.defineProperty()
来监听属性变动,将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter
和getter
当给这个对象的某个值赋值,就会触发setter
,进而监听到数据变化
监听到变化之后通知订阅者,需要实现一个消息订阅器Dep,通过维护一个数组subs,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法
流程图:
完整代码:
function Observer(data) {
this.data = data;
this.walk(data);
} Observer.prototype = {
walk: function(data) {
var me = this;
Object.keys(data).forEach(function(key) {
me.convert(key, data[key]);
});
},
convert: function(key, val) {
this.defineReactive(this.data, key, val);
}, defineReactive: function(data, key, val) {
var dep = new Dep();
var childObj = observe(val); Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: false, // 不能再define
get: function() {
// 添加订阅者watcher到主题对象Dep
if (Dep.target) {
dep.depend();
}
return val;
},
set: function(newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 新的值是object的话,进行监听
childObj = observe(newVal);
// 通知订阅者
dep.notify();
}
});
}
}; function observe(value, vm) {
if (!value || typeof value !== 'object') {
return;
} return new Observer(value);
}; var uid = 0; function Dep() {
this.id = uid++;
this.subs = [];
} Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
}, depend: function() {
Dep.target.addDep(this);
}, removeSub: function(sub) {
var index = this.subs.indexOf(sub);
if (index != -1) {
this.subs.splice(index, 1);
}
}, notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
}; Dep.target = null;
Watcher
Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、定义一个update()方法
3、在Observe中,待属性变动触发dep.notice()发出通知,调用watcher实例自身的update()方法,并触发Compile中绑定的回调
完整代码:
function Watcher(vm, expOrFn, cb) {
this.cb = cb; // 回调函数
this.vm = vm; // this 调用对象
this.expOrFn = expOrFn; // watch的对象的key
this.depIds = {};
console.log(typeof expOrFn, expOrFn);
if (typeof expOrFn === 'function') { // function
this.getter = expOrFn;
} else { // express
// this.getter 等于 this.parseGetter 的return返回的匿名函数
this.getter = this.parseGetter(expOrFn);
}
// 调用get方法,从而触发getter
// this.get() ==> this.getter.call(this.vm, this.vm) ==> this.parseGetter(expOrFn)
// this.value = parseGetter中return匿名函数的返回值
this.value = this.get();
} Watcher.prototype = {
update: function() {
this.run(); // 属性值变化收到通知,每次data属性值变化触发dep.notify()
},
run: function() {
var value = this.get(); // 取到最新值
var oldVal = this.value;
if (value !== oldVal) { // 新值与旧值比较
this.value = value;
this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
}
},
addDep: function(dep) {
// 1. 每次调用run()的时候会触发相应属性的getter
// getter里面会触发dep.depend(),继而触发这里的addDep
// 2. 假如相应属性的dep.id已经在当前watcher的depIds里,说明不是一个新的属性,仅仅是改变了其值而已
// 则不需要将当前watcher添加到该属性的dep里
// 3. 假如相应属性是新的属性,则将当前watcher添加到新属性的dep里
// 如通过 vm.child = {name: 'a'} 改变了 child.name 的值,child.name 就是个新属性
// 则需要将当前watcher(child.name)加入到新的 child.name 的dep里
// 因为此时 child.name 是个新值,之前的 setter、dep 都已经失效,如果不把 watcher 加入到新的 child.name 的dep中
// 通过 child.name = xxx 赋值的时候,对应的 watcher 就收不到通知,等于失效了
// 4. 每个子属性的watcher在添加到子属性的dep的同时,也会添加到父属性的dep
// 监听子属性的同时监听父属性的变更,这样,父属性改变时,子属性的watcher也能收到通知进行update
// 这一步是在 this.get() --> this.getVMVal() 里面完成,forEach时会从父级开始取值,间接调用了它的getter
// 触发了addDep(), 在整个forEach过程,当前wacher都会加入到每个父级过程属性的dep
// 例如:当前watcher的是'child.child.name', 那么child, child.child, child.child.name这三个属性的dep都会加入当前watcher
if (!this.depIds.hasOwnProperty(dep.id)) {
dep.addSub(this);
this.depIds[dep.id] = dep;
}
},
get: function() {
Dep.target = this; // 将当前订阅者指向自己
console.log(Dep.target);
var value = this.getter.call(this.vm, this.vm); // 触发getter,添加自己到属性订阅器中
Dep.target = null; // 添加完毕,重置
console.log(Dep.target);
return value;
}, parseGetter: function(exp) {
if (/[^\w.$]/.test(exp)) return; var exps = exp.split('.');
// this.getter.call(this.vm, this.vm)的第二个this.vm 传入 obj
return function(obj) {
for (var i = 0, len = exps.length; i < len; i++) {
if (!obj) return;
obj = obj[exps[i]];
}
return obj;
}
}
};
实例化Watcher
的时候,调用get()
方法,通过Dep.target = watcher实例
标记订阅者是当前watcher实例,强行触发属性定义的getter
方法,getter
方法执行的时候,就会在属性的订阅器dep
添加当前watcher实例,从而在属性值有变化的时候,watcher实例就能收到更新通知
Compile
Compile 指令实现,解析指令,模版渲染,更新视图,并将每个指令对应的节点绑定更新函数new Updater(),添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el
转换成文档碎片fragment
进行解析编译操作,解析完成,再将fragment
添加回原来的真实dom节点中;监听数据、绑定更新函数的处理是在compileUtil.bind()
这个方法中,通过new Watcher()
添加回调来接收数据变化的通知
流程图:
完整代码:
function Compile(el, vm) {
this.$vm = vm;
this.$el = this.isElementNode(el) ? el : document.querySelector(el); if (this.$el) {
this.$fragment = this.node2Fragment(this.$el);
this.init();
this.$el.appendChild(this.$fragment);
}
} Compile.prototype = {
// dom节点 转化为 Fragment文档碎片
node2Fragment: function(el) {
var fragment = document.createDocumentFragment(),
child; // 将原生节点拷贝到fragment
while (child = el.firstChild) {
fragment.appendChild(child);
} return fragment;
}, init: function() {
this.compileElement(this.$fragment);
}, compileElement: function(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)) { //节点类型为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) {
var attrName = attr.name;
if (me.isDirective(attrName)) {
var exp = attr.value; // 属性值
var dir = attrName.substring(2); // v-on:
if (me.isEventDirective(dir)) { // 事件指令 on
compileUtil.eventHandler(node, me.$vm, exp, dir);
} else { // 普通指令
compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
} node.removeAttribute(attrName);
}
});
},
// 编译 解析 文本节点
compileText: function(node, exp) {
compileUtil.text(node, this.$vm, exp);
}, isDirective: function(attr) {
return attr.indexOf('v-') == 0;
}, isEventDirective: function(dir) {
return dir.indexOf('on') === 0;
}, isElementNode: function(node) {
return node.nodeType == 1;
}, isTextNode: function(node) {
return node.nodeType == 3;
}
}; // 指令处理集合
var compileUtil = {
text: function(node, vm, exp) {
this.bind(node, vm, exp, 'text');
}, html: function(node, vm, exp) {
this.bind(node, vm, exp, 'html');
}, model: function(node, vm, exp) {
this.bind(node, vm, exp, 'model'); var me = this,
val = this._getVMVal(vm, exp);
node.addEventListener('input', function(e) {
var newValue = e.target.value;
if (val === newValue) {
return;
} me._setVMVal(vm, exp, newValue);
val = newValue;
});
}, class: function(node, vm, exp) {
this.bind(node, vm, exp, 'class');
}, 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);
});
}, // 事件处理
eventHandler: function(node, vm, exp, dir) {
var eventType = dir.split(':')[1],
fn = vm.$options.methods && vm.$options.methods[exp]; if (eventType && fn) {
node.addEventListener(eventType, fn.bind(vm), false);
}
}, _getVMVal: function(vm, exp) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k) {
val = val[k];
});
return val;
}, _setVMVal: function(vm, exp, value) {
var val = vm;
exp = exp.split('.');
exp.forEach(function(k, i) {
// 非最后一个key,更新val的值
if (i < exp.length - 1) {
val = val[k];
} else {
val[k] = value;
}
});
}
}; // 更新函数集合
var updater = {
textUpdater: function(node, value) {
node.textContent = typeof value == 'undefined' ? '' : value;
}, htmlUpdater: function(node, value) {
node.innerHTML = typeof value == 'undefined' ? '' : value;
}, classUpdater: function(node, value, oldValue) {
var className = node.className;
className = className.replace(oldValue, '').replace(/\s$/, ''); var space = className && String(value) ? ' ' : ''; node.className = className + space + value;
}, modelUpdater: function(node, value, oldValue) {
node.value = typeof value == 'undefined' ? '' : value;
}
};
MVVM
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,Compile解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。
完整代码:
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);
}); // 初始化computed
this._initComputed(); // 调用observe,监测数据是否变化
observe(data, this); // 编译解析指令模板
this.$compile = new Compile(options.el || document.body, this)
} MVVM.prototype = {
$watch: function(key, cb, options) {
new Watcher(this, key, cb);
}, _proxyData: function(key, setter, getter) {
var me = this;
setter = setter ||
Object.defineProperty(me, key, {
configurable: false,
enumerable: true,
get: function proxyGetter() {
return me._data[key];
},
set: function proxySetter(newVal) {
me._data[key] = newVal;
}
});
}, _initComputed: function() {
var me = this;
var computed = this.$options.computed;
if (typeof computed === 'object') {
Object.keys(computed).forEach(function(key) {
Object.defineProperty(me, key, {
get: typeof computed[key] === 'function' ?
computed[key] : computed[key].get,
set: function() {}
});
});
}
}
};
数据双向绑定的简单实现 实例
<!DOCTYPE html>
<head></head>
<body>
<div id="app">
<input type="text" id="a" v-model="text">
{{text}}
</div>
<script type="text/javascript">
function Compile(node, vm) {
if(node) {
this.$frag = this.nodeToFragment(node, vm);
console.log('vm===>', vm);
console.log('$frag=>>>',this.$frag) // #document-fragment
return this.$frag;
}
}
Compile.prototype = {
nodeToFragment: function(node, vm) {
var self = this;
var frag = document.createDocumentFragment();
var child; console.log('node=>>>', node) // #app 节点 while(child = node.firstChild) {
console.log('child=>>>', child)
self.compileElement(child, vm);
frag.append(child); // 将所有子节点添加到fragment中
}
return frag;
},
compileElement: function(node, vm) {
var reg = /\{\{(.*)\}\}/;
console.log('reg===>', reg);
console.log('node.nodeType==>>', node.nodeType); //节点类型为元素
if(node.nodeType === 1) {
var attr = node.attributes;
// 解析属性
for(var i = 0; i < attr.length; i++ ) {
if(attr[i].nodeName == 'v-model') {
var name = attr[i].nodeValue; // 获取v-model绑定的data中的属性名 [text]
console.log('name===>',name);
node.addEventListener('input', function(e) {
// 给相应的data属性赋值,进而触发该属性的set方法
vm[name]= e.target.value;
});
node.value = vm[name]; // 将data的值赋给该node
new Watcher(vm, node, name, 'value');
}
};
}
//节点类型为text
if(node.nodeType === 3) {
console.log('node.nodeValue==>>', node.nodeValue);
console.log(reg.test(node.nodeValue))
if(reg.test(node.nodeValue)) {
var name = RegExp.$1; // 获取匹配到的字符串
name = name.trim();
node.nodeValue = vm[name]; // 将data的值赋给该node
new Watcher(vm, node, name, 'nodeValue');
console.log(vm, node, name)
}
}
},
}
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
})
}
}
// node => dom真实节点
// name => data 属性key
//
function Watcher(vm, node, name, type) {
Dep.target = this;
this.name = name;
this.node = node;
this.vm = vm;
this.type = type;
this.update();
Dep.target = null;
} Watcher.prototype = {
update: function() {
this.get();
// console.log('node, type=>', this.node, this.type);
this.node[this.type] = this.value; // 订阅者执行相应操作
},
// 获取data的属性值
get: function() {
this.value = this.vm[this.name]; //触发相应属性的get
// console.log('value, name=>',this.value, this.name)
}
}
function defineReactive (obj, key, val) {
var dep = new Dep();
Object.defineProperty(obj, key, {
get: function() {
console.log(Dep.target)
// <input type="text" id="a" v-model="text"> => Watcher {name: "text", node: input#a, vm: Vue, type: "value"}
// {{text}} => Watcher {name: "text", node: text, vm: Vue, type: "nodeValue"}
//添加订阅者watcher到主题对象Dep
if(Dep.target) {
// JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
dep.addSub(Dep.target);
}
return val;
},
set: function (newVal) {
console.log(newVal === val, val, newVal);
if(newVal === val) return;
val = newVal;
console.log(val);
// 作为发布者发出通知
dep.notify();
}
})
}
function observe(obj, vm) {
Object.keys(obj).forEach(function(key) {
defineReactive(vm, key, obj[key]);
})
} function Vue(options) {
this.data = options.data;
var data = this.data;
observe(data, this);
var id = options.el;
// console.log(this)
var dom =new Compile(document.getElementById(id),this); // 编译完成后,将dom返回到app中
document.getElementById(id).appendChild(dom);
}
var vm = new Vue({
el: 'app',
data: {
text: 'hello world'
}
});
console.log(vm)
</script>
</body>
</html>
完整代码:https://github.com/136shine/MVVM_ada
参考:https://www.cnblogs.com/libin-1/p/6893712.html
https://segmentfault.com/a/1190000006599500
http://baijiahao.baidu.com/s?id=1596277899370862119&wfr=spider&for=pc
vue 之 双向绑定原理的更多相关文章
- vue的双向绑定原理及实现
前言 使用vue也好有一段时间了,虽然对其双向绑定原理也有了解个大概,但也没好好探究下其原理实现,所以这次特意花了几晚时间查阅资料和阅读相关源码,自己也实现一个简单版vue的双向绑定版本,先上个成果图 ...
- vue数据双向绑定原理
vue的数据双向绑定的小例子: .html <!DOCTYPE html> <html> <head> <meta charset=utf-> < ...
- vue的双向绑定原理解析(vue项目重构二)
现在的前端框架 如果没有个数据的双向/单向绑定,都不好意思说是一个新的框架,至于为什么需要这个功能,从jq或者原生js开始做项目的前端工作者,应该是深有体会. 以下也是个人对vue的双向绑定原理的一些 ...
- vue的双向绑定原理浅析与简单实现
很久之前看过vue的一些原理,对其中的双向绑定原理也有一定程度上的了解,只是最近才在项目上使用vue,这才决定好好了解下vue的实现原理,因此这里对vue的双向绑定原理进行浅析,并做一个简单的实现. ...
- 西安电话面试:谈谈Vue数据双向绑定原理,看看你的回答能打几分
最近我参加了一次来自西安的电话面试(第二轮,技术面),是大厂还是小作坊我在这里按下不表,先来说说这次电面给我留下印象较深的几道面试题,这次先来谈谈Vue的数据双向绑定原理. 情景再现: 当我手机铃声响 ...
- Vue数据双向绑定原理及简单实现
嘿,Goodgirl and GoodBoy,点进来了就看完点个赞再go. Vue这个框架就不简单介绍了,它最大的特性就是数据的双向绑定以及虚拟dom.核心就是用数据来驱动视图层的改变.先看一段代码. ...
- Vue之双向绑定原理动手记
Vue.js的核心功能有两个:一是响应式的数据绑定系统,二是组件系统.本文是通过学习他人的文章,从而理解了双向绑定原理,从而在自己理解的基础上,自己动手实现数据的双向绑定. 目前几种主流的mvc(vm ...
- Vue.js双向绑定原理
Vue.js最核心的功能有两个,一个是响应式的数据绑定系统,另一个是组件系统.本文仅仅探究双向绑定是怎样实现的.先讲涉及的知识点,再用简化的代码实现一个简单的hello world示例. 一.访问器属 ...
- 探讨vue的双向绑定原理及实现
1.vue的实现原理 vue的双向绑定是由数据劫持结合发布者-订阅者模式实现的,那么什么是数据劫持?vue是如何进行数据劫持的?说白了就是通过Object.defineProperty()来劫持对象属 ...
- 【Vue】vue的双向绑定原理及实现
vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的,那么vue是如果进行数据劫持的,我们可以先来看一下通过控制台输出一个定义在vue初始化数据上的对象是个什么东西. 代码: var ...
随机推荐
- python编译报错
UnicodeDecodeError: 'ascii' codec can't decode byte 0xa3 in position 3:ordi 因为同时安装了python2和python3,所 ...
- iOS堆栈内存区别
堆和栈的区别: · 1> 堆空间的内存是动态分配的,一般存放对象,并且需要手动释放内存. · 2> 栈空间的内存由系统自动分配,一般存放局部变量等,不需要手动管理内存. 接下来我将从以下几 ...
- Unity3D-Rigidbody
挂载Rigidbody的Gameobject受物理引擎的作用,有真实的物理力学. Mass质量:物体的质量(任意单位).建议一个物体的质量不要多余或少于其他单位的100倍. Drag阻力:当受力移动时 ...
- ELK日志分析之安装
ELK日志分析之安装 1.介绍: NRT elasticsearch是一个近似实时的搜索平台,从索引文档到可搜索有些延迟,通常为1秒. 集群 集群就是一个或多个节点存储数据,其中一个节点为主节点,这个 ...
- 操作系统 - Linux操作系统 - Centos - Centos7 - 安装|命令|使用汇总
镜像: http://mirrors.aliyun.com/centos/7/isos/x86_64/http://archive.kernel.org 网络配置 - DHCP # /etc/res ...
- Jmeter 03 Jmeter断言之Json断言
json断言可以让我们很快的定位到响应数据中的某一字段,当然前提是响应数据是json格式的,所以如果响应数据为json格式的话,使用json断言还是相当方便的. 还是以之前的接口举例 Url: htt ...
- curl基本用法
curl 是一种命令行工具,作用是发出网络请求,然后获取数据,显示在"标准输出"(stdout)上面. 以下是博主整理的一些关于curl命令的基本用法. -A/--user-age ...
- [转帖]安全公告【安全公告】CVE-2019-0708远程桌面服务远程代码执行漏洞
[安全公告]CVE-2019-0708远程桌面服务远程代码执行漏洞 https://www.landui.com/help/nshow-9716.html 漏洞层出不穷 漏洞信息: 2019年5月14 ...
- servlet3.0文件上传与下载
描述:文件上传与下载是在JavaEE中常见的功能,实现文件上传与下载的方式有多种,其中文件上传的方式有: (1)commons-fileupload: (2)Servlet 3.0 实现文件上传 (3 ...
- TortoiseGit不用每次输入用户名和密码的方法
TortoiseGit每次同步代码时,都会让输入用户名和密码,虽然安全,但是自己用有点麻烦. 怎么解决呢?废话不多说,直接上图: 1.设置—编辑本地.git/config 2.在本地.git/conf ...