1、前言

说起双向绑定可能大家都会说:Vue内部通过Object.defineProperty方法属性拦截的方式,把data对象里每个数据的读写转化成getter/setter,当数据变化时通知视图更新。虽然一句话把大概原理概括了,但是其内部的实现方式还是值得深究的,本文就以从简入繁的形式给大家撸一遍,让大家了解双向绑定的技术细节。

2、来一个简单的版本

让我们的数据变得可观测,实现原理不难,利用Object.defineProperty重新定义对象的属性描述符。

 /*** 把一个对象的每一项都转化成可观测对象* @param { Object } obj 对象*/function observable(obj) {if (!obj || typeof obj !== 'object') {return;}let keys = Object.keys(obj);keys.forEach((key) => {defineReactive(obj, key, obj[key])})return obj;}/*** 使一个对象转化成可观测对象* @param { Object } obj 对象* @param { String } key 对象的key* @param { Any } val 对象的某个key的值*/function defineReactive(obj, key, val) {Object.defineProperty(obj, key, {get() {console.log(`${key}属性被读取了`);return val;},set(newVal) {console.log(`${key}属性被修改了`);val = newVal;}})}let car = observable({'brand': 'BMW','price': 3000})//测试console.log(car.brand);复制代码

3、一步一步实现一个观察者模式的双向绑定

先给一张思维导向图吧(图盗的,链接:www.cnblogs.com/libin-1/p/6… 这张图我就不解释,我们先跟着一步一步的把代码撸出来,再回头来看这张图,问题不大。

建议在读之前一定要了解观察者模式和发布订阅模式以及其区别,一篇简单的文章总结了一下两种模式的区别(链接:www.cnblogs.com/chenlei987/…
Vue的双向绑定使用的就是观察者模式,其中Dep对象就是观察者的目标对象,而Watcher就是观察者,然后等待Dep对象的通知更新的,其中update方法是由watcher自己管理的,并非如发布订阅模式由目标对象去管理,在观察者模式中,目标对象管理的订阅者列表应该是Watcher本身,而不是事件/订阅主题。

3.1、声明一个Vue类,并将data里面的数据代理到Vue实例上面。

var Vue = (function (){class Vue{constructor (options = {}){//简化处理this.$options = options;let data = (this._data = typeof this.$options.data == 'function' ? this.$options.data() : this.$options.data);Object.keys(data).forEach(key =>{ this._proxy(key) });// 监听数据//observe(data);}_proxy (key){//用this这个对象去代理 this._data这个对象里面的keyObject.defineProperty(this, key, {configurable: true,enumerable: true,set: (val) => {this._data[key] = val},get: () =>{return this._data[key]}})}}return Vue;
}
let VM = new Vue({data (){return {a: 1,arr: [1,2,3,4,5,6]}},
});
//说明 _proxy代理成功了
console.log(VM.a);
VM.a = 2;
console.log(VM.a);复制代码

3.2、让data里面的数据变得可观测,开启observe之旅

注:下面我所说的"data里面"就是指vue实例的data属性。 上面代码Vue类的constructor里面我注释了一行代码,下面我取消注释,并且开始定义observe函数

// 监听数据observe(data);
复制代码

在定义observe方法之前,首先明白我们observe要做什么? 实参是data数据,我们要遍历整个data数据的key,为data数据的每一个key都用Object.defineProperty去重新定义它的 getter和setter函数,从而使其可观测。

class Observer{constructor (value){this.value = value;this.walk(value);}// 遍历属性值并监听walk(value) {Object.keys(value).forEach(key => this.convert(key, value[key]));}// 执行监听的具体方法convert(key, val) {defineReactive(this.value, key, val);}}function defineReactive(obj, key, val) {const dep = new Dep();// 给当前属性的值添加监听let chlidOb = observe(val);Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: () => {//do something//  if (Dep.target) {//    dep.depend();//}return val;},set: newVal => {if (val === newVal) return;val = newVal;//do something// 对新值进行监听//chlidOb = observe(newVal);// 通知所有订阅者,数值被改变了//dep.notify();},});}function observe(value) {// 当值不存在,或者不是复杂数据类型时,不再需要继续深入监听if (!value || typeof value !== 'object') {return;}return new Observer(value);}复制代码

看到在get和set函数里面的do something了吗,可以理解为在data里面的每个key的设置和获取都被我们截取到了,在每个key的设置和获取时我们可以干些事情了。比如更数据对应的DOM。 要做什么呢?
get函数: 从思维图图1可以看出需要把当前的Watcher添加进Dep对象,等待数据更新,调用回调。
set函数: 数据更新,Dep对象通知所有订阅的watcher更新,调用回调,更新视图。

3.3、Watcher

先声明一个Watcher类,用于添加进Dep对象并通知更新视图使用。

 let uid = 0;class Watcher {constructor(vm, expOrFn, cb) {// 设置id,用于区分新Watcher和只改变属性值后新产生的Watcherthis.id = uid++;this.vm = vm; // 被订阅的数据一定来自于当前Vue实例this.cb = cb; // 当数据更新时想要做的事情this.expOrFn = expOrFn; // 被订阅的数据this.val = this.get(); // 维护更新之前的数据}// 对外暴露的接口,用于在订阅的数据被更新时,由订阅者管理员(Dep)调用update() {this.run();}addDep(dep) {// 如果在depIds的hash中没有当前的id,可以判断是新Watcher,因此可以添加到dep的数组中储存// 此判断是避免同id的Watcher被多次储存//这里要是不限制重复,你会发现在响应的过程中,Dep实例下的subs会成倍的增加watcher。多输入几个字浏览器就卡死了。if (!dep.depIds.hasOwnProperty(this.id)) {dep.addSubs(this);dep.depIds[this.id] = dep;}}run() {const val = this.get();if (val !== this.val) {this.val = val;this.cb.call(this.vm, val);}}get() {// 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者Dep.target = this;//注意:在这里获取该属性 从而就触发了defineProperty的get方法,该watcher已经进入Dep的subs队列了const val = this.vm._data[this.expOrFn]; //初始化执行一遍回调this.cb.call(this.vm, val);//  置空,用于下一个Watcher使用Dep.target = null;return val;}}复制代码

上面代码我们先从constructor看起,接受三个参数,vm当前的vue实例,expOrFn实例化时该watcher实例所 代表/处理 的"data里面"(‘data里面’上面有解释,这里提醒一下)的哪个值,cb,回调函数,也就是当数据更新后需要做什么(自然是更新DOM咯)。
然后在constructor里面还调用了 this.get()。详细看一下get函数的定义,两行代码需要注意:

// 当前订阅者(Watcher)读取被订阅数据的最新更新后的值时,通知订阅者管理员收集当前订阅者
Dep.target = this;
//注意:在这里获取该属性 从而就触发了defineProperty的get方法,该watcher已经进入Dep的subs队列了
const val = this.vm._data[this.expOrFn]; 复制代码

Dep.target = this;确定了当前的活动的watcher实例,Dep.target我们可以认为它是一个全局变量,用于存放当前活动的watcher实例。 const val = this.vm._data[this.expOrFn];获取数据,这句话其实就已经触发了其自身的getter方法(这点要注意,不然你连流程都理解不通)。 进入了getter方法,也就把当前活动的实例的watcher添加进dep对象等待更新。 添加进Dep对象后,置空,用于下一个Watcher使用 Dep.target = null;

3.4、Dep

一直在说dep对象,我们一定要知道dep对象就是观察者模式里面的目标对象,用于存放watcher和负责通知更新的。 下面来定义一个Dep对象,放到class Watcher前面。 注意Dep的作用范围.

class Dep{constructor (){this.depIds = {}; // hash储存订阅者的id,避免重复的订阅者//订阅者列表  watcher实例列表this.subs = [];}depend (){Dep.target.addDep(this);//相当于调用this.addSubs 将 watcher实例添加进订阅列表 等待通知更新//本来按照我们的理解,在denpend里面是需要将watcher添加进 Dep对象, 等待通知更新的,所以应该调用 this.addSubs(Dep.target)//但是由于需要解耦 所以 先调用 watcher的addDep 在addDep中调用Dep实例的addSubs//简化理解就是 将 watcher实例添加进订阅列表 等待通知更新}addSubs (sub) {//这里的sub肯定是watcher实例this.subs.push(sub);}notify (){//监听到值的变化,通知所有订阅者watcher更新this.subs.forEach((sub) =>{sub.update();});}}Dep.target = null;//存储当前活动的watcher复制代码

再改改defineReactive,把注释打开

function defineReactive(obj, key, val) {const dep = new Dep();// 给当前属性的值添加监听let chlidOb = observe(val);Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: () => {// 如果Dep类存在target属性,将其添加到dep实例的subs数组中// target指向一个Watcher实例,每个Watcher都是一个订阅者// Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法if (Dep.target) {dep.depend();}return val;},set: newVal => {if (val === newVal) return;val = newVal;// 对新值进行监听chlidOb = observe(newVal);// 通知所有订阅者,数值被改变了dep.notify();},});}复制代码

然后起一个watcher来监听

3.5、让数据响应起来

先给Vue暴露一个方法 $watcher 可以调用实例化Watcher。

class Vue{constructor (options = {}){//简化处理this.$options = options;let data = (this._data = typeof this.$options.data == 'function' ? this.$options.data() : this.$options.data);Object.keys(data).forEach(key =>{ this._proxy(key) });// 监听数据observe(data);}// 对外暴露调用订阅者的接口,内部主要在指令中使用订阅者$watch(expOrFn, cb) {//property需要监听的属性  cb在监听到更新后的回调new Watcher(this, expOrFn, cb);}_proxy (key){//用this这个对象去代理 this._data这个对象里面的keyObject.defineProperty(this, key, {configurable: true,enumerable: true,set: (val) => {this._data[key] = val},get: () =>{return this._data[key]}})}}复制代码

3.6、测试: 声明一个实例

html部分

 <h3>Vue双向绑定</h3><input type="text" id="input"><p id="react"></p><h3>Vue数组双向绑定</h3><input type="text" id="arr-input"><p id="arr-reat"></p>复制代码
let reactElement = document.querySelector("#react");let input = document.getElementById('input');input.addEventListener('keyup', function (e) {VM.a = e.target.value;});VM.$watch('a', val => reactElement.innerHTML = val); //监听属性 a 当a发生改变时//数组的响应并不能实现let arrReactElement = document.querySelector("#arr-reat");let arrInput = document.getElementById('arr-input');arrInput.addEventListener('keyup', function (e) {VM.arr.push(e.target.value);console.log(VM.arr);});VM.$watch('arr', val => arrReactElement.innerHTML = val); //监听属性 a 当a并没有发生改变时复制代码

VM.$watch就可以实例化一个watcher,从而去劫持data里面某个属性的改变,在改变时调用回调函数。 数组的改变并没有实现。上面的代码见https://gitee.com/cchennlleii/MyTest/blob/master/vueReactive/vue-reactive%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0.html

4、对数组的支持

在说这个之前我们先去看一看vue官网对于数组更新检测的说明,链接:cn.vuejs.org/v2/guide/li…

总的来说,对于数组支持更新的只是数组原型上的方法,对于vm.items[index] = newValue是不支持的。 其实Object.defineProperty对于数组都是不支持的,根据消息vue3.0用的proxy对于数组得到了完美的支持,但是兼容性不怎么样。 既然vue实现了对数组原型方法的支持,那么我们也来让我们的例子对数组方法也支持吧。 原理不难,vue对于所有的数组原型方法都写了一层hack,让其支持更新。那么下面我们就一步一步来实现。

4.1、准备一套数组原型方法的hack

/*** Define a expOrFn.*/function def(obj, key, val, enumerable) {Object.defineProperty(obj, key, {value: val,enumerable: !!enumerable,writable: true,configurable: true});}//数组改变的监听var arrayProto = Array.prototype;var arrayMethods = Object.create(arrayProto);var methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'];/*** Intercept mutating methods and emit events*/methodsToPatch.forEach(function (method) {// cache original methodvar original = arrayProto[method];def(arrayMethods, method, function mutator() {var args = [], len = arguments.length;while (len--) args[len] = arguments[len];var result = original.apply(this, args);var ob = this.__ob__;var inserted;switch (method) {case 'push':case 'unshift':inserted = args;breakcase 'splice':inserted = args.slice(2);break}if (inserted) { ob.observeArray(inserted); }// notify changeob.dep.notify(); //调用该数组下的 __ob__.dep 详细可见class Observer的constructor里的注释return result});});复制代码

上面代码准备了一个arrayMethods的对象,它继承自Array.prototype,并且对methodsToPatch里面的方法进行了改写,后面我们会把arrayMethods这个对象挂到"data里面"每个数组下,让该数组调用数组原生方法,比如[].push其实调用的是arrayMethods里面被改写的方法,从而在该数组改变时获取到该数组的更新。 下面开始挂载arrayMethods对象,在挂载我之前我们看到有一个this.__ob__属性,这里的this指向要观测的数组。这个__ob__就是前面的observe对象,并且每个observe下面还有一个dep对象。下面我们来理清楚这层关系。

class Observer{constructor (value){this.value = value;//下面两行代码虽然很简单,但是我们需要从这里理清楚关系//假如 有数据如 {a: [1,2,3], b: 1},  然后调用oberve(vm.a),vm当前vue实例//会自动挂载 __ob__ 和 __ob__.dep// 那么对数组a进行oberserve的对象就是a.__ob__, 它所对应的dep对象就是 a.__ob__.dep//详细使用可以在对数组的方法进行hack的时候 使用到def(value, '__ob__', this);//让被监听的数据都带上一个不可枚举的属性 __ob__ 代表observe对象this.dep = new Dep();//首先每个oberserve实例下有一个dep对象//在这里处理数组if (Array.isArray(value)){//调用数组的hack方法, 让数组也能被监听  arrayMethodsvar arrayKeys = Object.getOwnPropertyNames(arrayMethods);for (var i = 0, l = arrayKeys.length; i < l; i++) {var key = arrayKeys[i];def(value, key, arrayMethods[key]);}}   else{//对象 遍历key  添加监听this.walk(value);}}//Observer的其他方法//...
}复制代码

上面代码首先给每个值挂载__ob__属性(不可枚举),然后给每个Obeserve对象挂载Dep对象。然后根据value的类型,如果是数组就会挂载arrayMethods方法。 现在我们来理清数组在哪里依赖收集,在哪里通知更新的。 在对数组hack的方法里面(上上一段代码)有一段ob.dep.notify(); 这里通知更新,所以依赖收集也一定要收集到value.ob.dep对象里面,两个dep对象应该是相同的,下面我们来看看依赖收集写在哪里的。

function defineReactive(obj, key, val) {const dep = new Dep();// 给当前属性的值添加监听let childOb = observe(val);Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: () => {// 如果Dep类存在target属性,将其添加到dep实例的subs数组中// target指向一个Watcher实例,每个Watcher都是一个订阅者// Watcher实例在实例化过程中,会读取data中的某个属性,从而触发当前get方法if (Dep.target) {dep.depend();if (childOb) {childOb.dep.depend();if (Array.isArray(val)) {dependArray(val);}}}return val;},set: newVal => {if (val === newVal) return;val = newVal;// 对新值进行监听childOb = observe(newVal);// 通知所有订阅者,数值被改变了dep.notify();},});}function dependArray(value) {for (var e = (void 0), i = 0, l = value.length; i < l; i++) {e = value[i];e && e.__ob__ && e.__ob__.dep.depend();if (Array.isArray(e)) {dependArray(e);}}}复制代码

数组虽然在Object.defineProperty里面set方法无法响应,但是get方法是没有问题的,所以在数组get的时候,判断val如果是array,会调用value.ob.dep.depend进行依赖收集。与上面依赖通知使用了同意个dep对象,也就是挂载在自身的__ob__.dep。 写到这里我们就完全实现对数组原生方法的支持了。 下面看一下效果 代码地址:gitee.com/cchennlleii…

4.2测试代码

html部分

<h3>Vue双向绑定</h3><input type="text" id="input"><p id="react"></p><h3>Vue数组双向绑定</h3><input type="text" id="arr-input"><p id="arr-reat"></p><h3>Vue对nextTick实现</h3><button id="addBtn">加100000次</button><p id="react-tick"></p>复制代码
let reactElement = document.querySelector("#react");let input = document.getElementById('input');input.addEventListener('keyup', function (e) {VM.a = e.target.value;});VM.$watch('a', val => reactElement.innerHTML = val); //监听属性 a 当a发生改变时//数组的响应并能实现let arrReactElement = document.querySelector("#arr-reat");let arrInput = document.getElementById('arr-input');arrInput.addEventListener('keyup', function (e) {VM.arr.push(e.target.value);console.log(VM.arr);});VM.$watch('arr', val => arrReactElement.innerHTML = val); //监听属性 a 当a发生改变时let reactTick = document.querySelector("#react-tick");VM.$watch('tickData', val => {console.log(val);reactTick.innerHTML = val;}); //监听属性 a 当a发生改变时document.querySelector('#addBtn').addEventListener('click', function () {for (let i = 0; i < 100000; i++) {VM.tickData = i;}}, false)复制代码

效果:

5、对nextTick的支持

vue官网对nextTick的解释:

nextTick如果自己实现就是在下一个envet loop执行,不在本次同步任务中执行。 自己实现一个简单的:

//nextTick的实现
let callbacks = [];
let pending = false;function nextTick(cb) {callbacks.push(cb);if (!pending) {pending = true;setTimeout(flushCallbacks, 0);}
}
function flushCallbacks() {pending = false;const copies = callbacks.slice(0);callbacks.length = 0;for (let i = 0; i < copies.length; i++) {copies[i]();}
}复制代码

简单理解: 在本次event loop中收集cb(任务),放到下一个event loop去执行。 关于不知道event loop的可以参考这篇文章:www.cnblogs.com/chenlei987/… 在理解event loop的同时也需要同时了解 microtask和macrotask的区别。 好了言归正传,在vue的'data里面'某个属性发生了改变,并被观测到后,调用了watcher.update,并不会立即调用watcher.run去更新视图,它会经过nextTick之后再更新视图,说起来有点牵强。 还是第四部=步的代码,没有实现对nextTick的优化。 代码:

<h3>Vue双向绑定</h3><input type="text" id="input"><p id="react"></p><h3>Vue数组双向绑定</h3><input type="text" id="arr-input"><p id="arr-reat"></p><h3>Vue对nextTick实现</h3><button id="addBtn">加1000次</button><p id="react-tick"></p>let reactTick = document.querySelector("#react-tick");VM.$watch('tickData', val => {console.log(val);reactTick.innerHTML = val;}); //监听属性 a 当a发生改变时document.querySelector('#addBtn').addEventListener('click', function () {for (let i = 0; i < 1000; i++) {VM.tickData = i;}}, false)复制代码

效果是这样的:

现在的效果是VM.tickData加1000次,那么cb(回调)就会调用1000次,这样是非常影响性能的,我们想要的效果是无论VM.tickData在本次event loop加多少次,都不会触发回调,只需要在VM.tickData加完之后,触发一次最终的cb(回调)就ok了。 下面我们就来实现这种优化,代码不多。

//nextTick的实现let callbacks = [];let pending = false;function nextTick(cb) {callbacks.push(cb);if (!pending) {pending = true;setTimeout(flushCallbacks, 0);}}function flushCallbacks() {pending = false;const copies = callbacks.slice(0);callbacks.length = 0;for (let i = 0; i < copies.length; i++) {copies[i]();}}let has = {};let queue = [];let waiting = false;function queueWatcher(watcher) {const id = watcher.id;if (has[id] == null) {has[id] = true;queue.push(watcher);if (!waiting) {waiting = true;nextTick(flushSchedulerQueue);}}}function flushSchedulerQueue() {let watcher, id;for (index = 0; index < queue.length; index++) {watcher = queue[index];id = watcher.id;has[id] = null;watcher.run();}waiting = false;}复制代码

然后更改Watcher里面的update方法,并不直接调用watcher.run,而是经过queueWatcher控制

update() {queueWatcher(this);// this.run();
}
复制代码

代码地址:gitee.com/cchennlleii…

6、总结

如果面试官问我关于双向绑定的问题,从这三个方面去回答,Object.definproperty,观察者模式,nextTick,当然,你需要把这三个点联系起来去描述,相信我你把上面的看懂了,联系起来完全没问题的,你是最棒的!

7、本文参考:

codepen.io/xiaomuzhu/p… www.jianshu.com/p/2df6dcddb…

转载于:https://juejin.im/post/5d09be37f265da1bcd37db8a

撸一个vue的双向绑定相关推荐

  1. 西安电话面试:谈谈Vue数据双向绑定原理,看看你的回答能打几分

    最近我参加了一次来自西安的电话面试(第二轮,技术面),是大厂还是小作坊我在这里按下不表,先来说说这次电面给我留下印象较深的几道面试题,这次先来谈谈Vue的数据双向绑定原理. 情景再现: 当我手机铃声响 ...

  2. Vue.js双向绑定的实现原理

    Vue.js 最核心的功能有两个,一是响应式的数据绑定系统,二是组件系统.本文仅探究双向绑定是怎样实现的.先讲涉及的知识点,再用简化得不能再简化的代码实现一个简单的 hello world 示例. 一 ...

  3. 小猿圈解析vue数据双向绑定的缺点

    vue是当今前端很流行的一种框架,但是vue也是有一定的缺陷的,你有过了解吗?下面小猿圈web前端老师就为你解析一下vue数据双向绑定的缺陷,希望对你有所帮助,下面我们一起了解一下吧. 1.vue 实 ...

  4. vue的双向绑定原理及实现

    前言 使用vue也好有一段时间了,虽然对其双向绑定原理也有了解个大概,但也没好好探究下其原理实现,所以这次特意花了几晚时间查阅资料和阅读相关源码,自己也实现一个简单版vue的双向绑定版本,先上个成果图 ...

  5. “约见”面试官系列之常见面试题第九篇vue实现双向绑定原理(建议收藏)

    目录 1.原理 2.实现 在目前的前端面试中,vue的双向数据绑定已经成为了一个非常容易考到的点,即使不能当场写出来,至少也要能说出原理.本篇文章中我将会仿照vue写一个双向数据绑定的实例,名字就叫m ...

  6. vue radio双向绑定_Vue 双向绑定

    Vue 双向绑定 MVC模式 以往的MVC模式是单向绑定,即Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新 MVVM模式 MVVM模式就是Model– ...

  7. vue数据双向绑定的原理

    vue数据双向绑定的原理 一 复习闭包 1 闭包含义: 当函数嵌套时,内部函数使用了外部函数的变量,就会产生闭包 当函数可以记住并访问自己的作用域时,就会产生闭包 2 闭包注意点 ① 队列里的代码执行 ...

  8. Vue的基础认知二---vue的双向绑定/vue获取DOM节点

    在这篇博文之前,我们已经开了一个vue的头了,需要的小伙伴可以点击这个链接:Vue的基础认知一-构建环境/v指令的使用,好了,我们继续来看我们接下来要看的内容. 一.vue的双向绑定 mvvm框架: ...

  9. vue checkbox双向绑定_Vue的双向绑定

    一.什么是Vue的双向绑定?其实就是v-model 很多时候都会用v-model,但是他到底是什么呢? v-model的作用:v-model可以实现绑定一个变量: 1.在变量变化的时候,ui会变化 2 ...

最新文章

  1. mysql连接池失效_连接池隔天失效之异常处理
  2. C++ explicit关键字
  3. 残差网络ResNet笔记
  4. 1 理解Linux系统的“平均负载”
  5. 如何轻松将上亿的数据玩弄于股掌之中?
  6. oracle 邮件过程,oracle 发邮件 存储过程
  7. 操作系统--内核级线程实现
  8. 央视报道短视频侵权 呼吁多方配合保护影视版权
  9. 最新容器项目 Kata 曝光
  10. 开课吧:C++开发需要知晓的知识有哪些?
  11. 微软补丁地址以及查找方式
  12. 浅析成套设备研制中的项目模板管理
  13. php做个抽签人名,基于JS实现的随机数字抽签实例
  14. android定位附近店铺,高德地图怎么添加店铺位置_高德地图定位怎么设置添加自己家店铺位置_攻略...
  15. 【头歌C语言程序与设计】结构体
  16. 深度揭秘Xshell后门事件:入侵感染供应链软件的大规模定向攻击
  17. Quectel移远展锐平台5G模组RX500U/RG200U使用指南(四)-工作模式】
  18. 2016传智SSH框架CRM项目(5天)笔记(2017年5月20日22:11:15)
  19. 8. 关于打分函数F1分数 TPR PPV等
  20. 人工智能的“黑镜”——计算机人脸识别!

热门文章

  1. Linux定时任务Crontab详解
  2. SQLite指南(4) - FAQ列表(important)
  3. [Python] L1-042 日期格式化-PAT团体程序设计天梯赛GPLT
  4. 使用Genymotion Android模拟器无法连接电脑本机的服务器
  5. 如何在MySQL中设置外键约束
  6. perl如何遍历指定文件夹下的指定扩展名文件,并按时间顺序要求删除
  7. codevs 5965 [SDOI2017]新生舞会
  8. mysql主从配置,innobackup备份
  9. 在Unity中为模型使用表情
  10. Linux 命令(140)—— tree 命令