深入浅出Promise
Abstract
Promise的意思是承诺(在红宝书中翻译为期约),新华字典:(动)对某项事务答应照办。
Promise最早出现在Commn JS,随后形成了Promise/A规范。
Promise是异步编程的一种解决方案,简单讲是一个容器其中保存这某个未来才会结束的事件的结果,从语法上说,Promise对象可以获取异步操作的消息,而且Promise提供了统一的API对于各种异步操作都可以用同样的方法进行处理
比如我们发出请求调用服务器数据,由于网络延时原因,我们此时无法调用到数据,我们可以接着执行其它任务,等到将来某个时间节点服务器响应数据到达客户端,我们即可使用promise自带的一个回调函数来处理数据,所以要想真正理解Promise我们必须从回调开始
1、回调函数定义
回调是一个在另一个函数完成执行后所执行的函数——故此得名“回调”。
2、为什么需要回调
JavaScript 是一种事件驱动的单线程语言。这意味着,在继续之前, JavaScript 不会等待响应,而是继续执行且同时监听事件。
举例:
function first(){
console.log(1);
}
function second(){
console.log(2);
}
first();
second();
//1
//2
如果再first中有计时器呢?
function first(){
// 模拟代码延迟
setTimeout( function(){
console.log(1);
}, 500 );
}
function second(){
console.log(2);
}
first();
second();
//2
//1
//并不是 JavaScript 没有按照我们想要的顺序执行我们的函数,而是在继续执行 second() 之前, JavaScript 没有等待 first() 的响应。
1.回调函数字面意思:就是回调就是一个函数的调用过程。那么就从理解这个调用过程开始吧。函数a有一个参数,这个参数是个函数b,当函数a执行完以后执行函数b。那么这个过程就叫回调。
2.回调函数的意义:
异步:一般ajax请求都是异步的。请求发出去后,处理器会继续执行下面的代码。如果你想ajax请求完成后,做一些事情,显然,直接在下一行写代码是达不到目的。而作为回调函数传给ajax请求,可以控制请求在哪个阶段去调用回调函数,并且不用担心后面的代码执行到什么地方了。
3.回调函数的执行:就是异步的函数体执行成功或失败调用的传递进来的函数,调用的函数就是回调,为了不影响代码执行的效率,我们不会等待异步的代码执行,而是直接像下面执行,但是像请求,计时器,事件等操作我们在一些情况下必须拿到对应的数据或者返回值,这个时候就可以在异步函数里传入一个回调函数,也就是在异步操作执行结束之后会调用这个回调函数执行一段代码
3.回调的局限性
//普通函数
// 第一步,打开冰箱
function open(){
setTimeout(()=>{
console.log('打开冰箱');
return 'success';
}, 1000)
}
// 第二步,放牛进去
function settle(){
setTimeout(()=>{
console.log('放牛进去');
return 'success';
}, 3000)
}
// 第三步,关上冰箱
function close(){
setTimeout(()=>{
console.log('关上冰箱');
return 'success';
}, 1000)
}
function closeCow(){
open();
settle();
close()
}
closeCow();
//"打开冰箱"
//"关上冰箱"?
//"放牛进去"?
//回调函数实现
function closeCow() {
setTimeout(() => {
console.log("打开冰箱");
setTimeout(() => {
console.log("放牛进去");
setTimeout(() => {
console.log("关闭冰箱");
}, 1000);
}, 3000);
}, 1000);
}
如何解决回调嵌套?
1.保持你的代码简短(给函数取有意义的名字,见名知意,而非匿名函数,写成一大坨)
2.模块化(函数封装,打包,每个功能独立,可以单独的定义一个js文件Vue,react中通过import导入就是一种体现)
3.Promise/生成器/ES6等
4.Promise的特点
特点1
Promise有三种状态,分别是pending(进行中)、fulfilled(已成功)、rejected(已失败)。只有异步操作的结果可以决定当前是哪一种状态,任何其他操作都无法改变这个操作,Promise(承诺)这个名字也是由此而来,表示其他手段无法改变状态
特点2
如果状态发生了改变,就不会再改变而且任何时候都可以得到这个结果,Promise状态的改变只有两种情况一种是变为fulfilled另一种是变为rejected,改变后状态就凝固了不会再有任何改变,会一直保持这个结果,这是就成为resolved(已定形)。而且,如果改变已经发生你也可以对Promise添加回调函数获得结果,这与事件有根本的区别,事件如果不监听(dom.addEventListener),错过之后就无法再得到结果
特点3
无法取消Promise
,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部。第三,当处于pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)
5.Promise的使用
基础语法
const promise = new Promise((resolve, reject) => {
resolve('fulfilled...'); // 状态由 pending --> fulfilled
});
promise.then(res => {
console.log(res); // 只会调用 resolve
}, err => {
console.log(err); // 不会调用 rejected
})
// fulfilled
Promise
1.特性:
立即执行,自身是异步
let promise = new Promise(function(resolve, reject) {
console.log('Promise');
resolve();
});
promise.then(function() {
console.log('resolved.');
});
console.log('Hi!');
// ?
// ?
// ?
上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。
2.特性
值穿透
Promise.resolve(1)
.then(2)
.then(Promise.resolve(3))
.then(console.log)
// 输出 ?
//Promise的then方法的参数期望是函数,传入非函数则会发生值穿透。
6.Promise API
比较简单,
建议查看MDN文档
7.手写Promise
核心点
1.状态的改变
2.值的改变
3.返回值
4.class的使用
基础版
// 构建
const PENDING = 'pending'//进行中
const FULFILLED = 'fulfilled'//已成功
const REJECTED = 'rejected'//已失败
class NewPromise {
//接受一个函数handle
constructor (handle) {
if (!isFunction(handle)) {
throw new Error('MyPromise must accept a function as a parameter')
}
// 添加状态
this._status = PENDING
// 添加状态
this._value = undefined
// 添加成功回调函数队列
this._fulfilledQueues = []
// 添加失败回调函数队列
this._rejectedQueues = []
// 执行handle
try {
//执行回调函数,class是严格模式指向undefined,通过bind修改指向resove的指向
handle(this._resolve.bind(this), this._reject.bind(this))
//收集错误
} catch (err) {
this._reject(err)
}
}
}
//then方法的对应状态的回调执行时机,值的改变
then (onFulfilled, onRejected) {
const { _value, _status } = this
switch (_status) {
// 当状态为pending时,将then方法回调函数加入执行队列等待执行
case PENDING:
this._fulfilledQueues.push(onFulfilled)
this._rejectedQueues.push(onRejected)
break
// 当状态已经改变时,立即执行对应的回调函数
case FULFILLED:
onFulfilled(_value)
break
case REJECTED:
onRejected(_value)
break
}
// 返回一个新的Promise对象
return new MyPromise((onFulfilledNext, onRejectedNext) => {
})
}
//那返回的新的 Promise 对象什么时候改变状态?改变为哪种状态呢?
//then方法确认Promise状态
then (onFulfilled, onRejected) {
const { _value, _status } = this
// 返回一个新的Promise对象
return new MyPromise((onFulfilledNext, onRejectedNext) => {
// 封装一个成功时执行的函数
let fulfilled = value => {
try {
if (!isFunction(onFulfilled)) {
onFulfilledNext(value)
} else {
let res = onFulfilled(value);
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
// 封装一个失败时执行的函数
let rejected = error => {
try {
if (!isFunction(onRejected)) {
onRejectedNext(error)
} else {
let res = onRejected(error);
if (res instanceof MyPromise) {
// 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
}
} catch (err) {
// 如果函数执行出错,新的Promise对象的状态为失败
onRejectedNext(err)
}
}
// 执行对应函数
switch (_status) {
// 当状态为pending时,将then方法回调函数加入执行队列等待执行
case PENDING:
this._fulfilledQueues.push(fulfilled)
this._rejectedQueues.push(rejected)
break
// 当状态已经改变时,立即执行对应的回调函数
case FULFILLED:
fulfilled(_value)
break
case REJECTED:
rejected(_value)
break
}
})
}
完全版--1
ES6语法实现Promise // 判断变量否为function const isFunction = variable => typeof variable === 'function' // 定义Promise的三种状态常量 const PENDING = 'PENDING' const FULFILLED = 'FULFILLED' const REJECTED = 'REJECTED' class MyPromise { constructor (handle) { if (!isFunction(handle)) { throw new Error('MyPromise must accept a function as a parameter') } // 添加状态 this._status = PENDING // 添加状态 this._value = undefined // 添加成功回调函数队列 this._fulfilledQueues = [] // 添加失败回调函数队列 this._rejectedQueues = [] // 执行handle try { //class是严格模式,如果不bind会Uncaught TypeError: Cannot read property undefined handle(this._resolve.bind(this), this._reject.bind(this)) } catch (err) { this._reject(err) } } // 添加resovle时执行的函数 _resolve (val) { const run = () => { //不是进行状态,已凝固直接返回 if (this._status !== PENDING) return // 依次执行成功队列中的函数,并清空队列 const runFulfilled = (value) => { let cb; while (cb = this._fulfilledQueues.shift()) { cb(value) } } // 依次执行失败队列中的函数,并清空队列 const runRejected = (error) => { let cb; while (cb = this._rejectedQueues.shift()) { cb(error) } } /* 如果resolve的参数为Promise对象,则必须等待该Promise对象状态改变后, 当前Promsie的状态才会改变,且状态取决于参数Promsie对象的状态 */ if (val instanceof MyPromise) { val.then(value => { this._value = value this._status = FULFILLED runFulfilled(value) }, err => { this._value = err this._status = REJECTED runRejected(err) }) } else { this._value = val this._status = FULFILLED runFulfilled(val) } } // 为了支持同步的Promise,这里采用异步调用 setTimeout(run, 0) } // 添加reject时执行的函数 _reject (err) { if (this._status !== PENDING) return // 依次执行失败队列中的函数,并清空队列 const run = () => { this._status = REJECTED this._value = err let cb; //将删除的第一个元素的值赋值给cb while (cb = this._rejectedQueues.shift()) { cb(err) } } // 为了支持同步的Promise,这里采用异步调用 setTimeout(run, 0) } // 添加then方法 then (onFulfilled, onRejected) { const { _value, _status } = this // 返回一个新的Promise对象 return new MyPromise((onFulfilledNext, onRejectedNext) => { // 封装一个成功时执行的函数 let fulfilled = value => { try { if (!isFunction(onFulfilled)) { onFulfilledNext(value) } else { let res = onFulfilled(value); if (res instanceof MyPromise) { // 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调 res.then(onFulfilledNext, onRejectedNext) } else { //否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数 onFulfilledNext(res) } } } catch (err) { // 如果函数执行出错,新的Promise对象的状态为失败 onRejectedNext(err) } } // 封装一个失败时执行的函数 let rejected = error => { try { if (!isFunction(onRejected)) { onRejectedNext(error) } else { let res = onRejected(error); if (res instanceof MyPromise) { // 如果当前回调函数返回MyPromise对象,必须等待其状态改变后在执行下一个回调 res.then(onFulfilledNext, onRejectedNext) } else { //否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数 onFulfilledNext(res) } } } catch (err) { // 如果函数执行出错,新的Promise对象的状态为失败 onRejectedNext(err) } } switch (_status) { // 当状态为pending时,将then方法回调函数加入执行队列等待执行 case PENDING: this._fulfilledQueues.push(fulfilled) this._rejectedQueues.push(rejected) break // 当状态已经改变时,立即执行对应的回调函数 case FULFILLED: fulfilled(_value) break case REJECTED: rejected(_value) break } }) } // 添加catch方法 catch (onRejected) { return this.then(undefined, onRejected) } // 添加静态resolve方法 static resolve (value) { // 如果参数是MyPromise实例,直接返回这个实例 if (value instanceof MyPromise) return value return new MyPromise(resolve => resolve(value)) } // 添加静态reject方法 static reject (value) { return new MyPromise((resolve ,reject) => reject(value)) } // 添加静态all方法 static all (list) { return new MyPromise((resolve, reject) => { /** * 返回值的集合 */ let values = [] let count = 0 for (let [i, p] of list.entries()) { // 数组参数如果不是MyPromise实例,先调用MyPromise.resolve this.resolve(p).then(res => { values[i] = res count++ // 所有状态都变成fulfilled时返回的MyPromise状态就变成fulfilled if (count === list.length) resolve(values) }, err => { // 有一个被rejected时返回的MyPromise状态就变成rejected reject(err) }) } }) } // 添加静态race方法 static race (list) { return new MyPromise((resolve, reject) => { for (let p of list) { // 只要有一个实例率先改变状态,新的MyPromise的状态就跟着改变 this.resolve(p).then(res => { resolve(res) }, err => { reject(err) }) } }) } finally (cb) { return this.then( value => MyPromise.resolve(cb()).then(() => value), reason => MyPromise.resolve(cb()).then(() => { throw reason }) ); } }
注释:
1.Try
完全版-2
ES5语法实现(function () { // 判断function function isFunction(fn) { return typeof fn === 'function'; } // 状态 pending、fulfilled、rejected var PENDING = 'pending'; var FULFILLED = 'fulfilled'; var REJECTED = 'rejected'; // 构造方法 var Kromise = function (handle) { // 当前状态 this._status = PENDING; // 添加成功回调队列 this._fulfilledQueue = []; // 添加失败回调队列 this._rejectedQueue = []; // 引用当前this对象 var self = this; if (!isFunction(handle)) { throw new Error('Parameter handle is not a function!') } // 添加resolve时执行的函数 function _resolve(val) { var run = function () { if (self._status !== PENDING) return; // 依次执行成功队列中的函数,并清空队列 var runFulfilled = function (res) { var resolve; while (resolve = self._fulfilledQueue.shift()) { // 出栈 resolve(res); } }; // 依次执行失败队列中的函数,并清空队列 var runRejected = function (err) { var reject; while (reject = self._rejectedQueue.shift()) { // 出栈 reject(err); } }; /* 如果resolve的参数为Kromise对象,则必须等待该Kromise对象状态改变后, * 当前Kromise的状态才会改变,且状态取决于参数Kromise对象的状态 */ if (val instanceof Kromise) { val.then(function (value) { self._status = FULFILLED; self._value = value; runFulfilled(value) }, function (err) { self._status = REJECTED; self._value = err; runRejected(err); }) } else { self._status = FULFILLED; self._value = val; runFulfilled(val); } }; // 为了支持同步的Promise,这里采用异步调用 setTimeout(run, 0) } // 添加reject时执行的函数 function _reject(err) { var run = function () { if (self._status !== PENDING) return; // 依次执行成功队列中的函数,并清空队列 self._status = REJECTED; self._value = err; var reject; while (reject = self._fulfilledQueue.shift()) { // 出栈 reject(err); } }; // 为了支持同步的Promise,这里采用异步调用 setTimeout(run, 0) } // 执行handle,捕获异常 try { handle(_resolve.bind(this), _reject.bind(this)); } catch (e) { _reject(e); } }; // 属性 Kromise.length = 1; // 实例方法 // 实现then方法 Kromise.prototype.then = function (onFulfilled, onRejected) { var self = this; // 返回一个新的Kromise对象 return new Kromise(function (onFulfilledNext, onRejectedNext) { // 成功时的回调 var fulfilled = function (val) { try { // 如果不是函数,值穿透 if (!isFunction(onFulfilled)) { onFulfilledNext(val) } else { var res = onFulfilled(val); // 如果当前回调函数返回Kromise对象,必须等待其状态改变后在执行下一个回调 if (res instanceof Kromise) { res.then(onFulfilledNext, onRejectedNext); } else { //否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数 onFulfilledNext(res); } } } catch (e) { // 如果函数执行出错,新的Kromise对象的状态为失败 onRejectedNext(e); } }; // 失败时的回调 var rejected = function (err) { try { if (!isFunction(onRejected)) { onRejectedNext(err) } else { var res = onRejected(err); if (res instanceof Kromise) { res.then(onFulfilledNext, onRejectedNext); } else { onFulfilledNext(res); } } } catch (e) { onRejectedNext(e) } }; switch (self._status) { // 当状态为pending时,将then方法回调函数加入执行队列等待执行 case PENDING: self._fulfilledQueue.push(fulfilled); self._rejectedQueue.push(rejected); break; // 当状态已经改变时,立即执行对应的回调函数 case FULFILLED: fulfilled(self._value); break; case REJECTED: rejected(self._value); break; } }); }; // 实现catch方法 Kromise.prototype.catch = function (onRejected) { return this.then(undefined, onRejected); }; // 实现finally方法 Kromise.prototype.finally = function (onFinally) { return this.then(function (value) { Kromise.resolve(onFinally()).then(function () { return value; }) }, function (err) { Kromise.resolve(onFinally()).then(function () { throw new Error(err); }) }) }; // 静态方法 // 实现resolve方法 Kromise.resolve = function (value) { // 如果参数是Kromise实例,直接返回这个实例 if (value instanceof Kromise) { return value; } return new Kromise(function (resolve) { resolve(value) }) }; // 实现reject方法 Kromise.reject = function (value) { return new Kromise(function (resolve, reject) { reject(value) }) }; // 实现all方法 Kromise.all = function (arr) { var self = this; return new Kromise(function (resolve, reject) { var values = []; for (var i = 0, len = arr.length; i < len; i++) { // 数组参数如果不是Kromise实例,先调用Kromise.resolve self.resolve(arr[i]).then(function (res) { values.push(res); // 所有状态都变成fulfilled时返回的Kromise状态就变成fulfilled if (values.length === arr.length) { resolve(values); } }, function (e) { // 有一个被rejected时返回的Kromise状态就变成rejected reject(e); }) } }) }; // 实现race方法 Kromise.race = function (arr) { var self = this; return new Kromise(function (resolve, reject) { for (var i = 0, len = arr.length; i < len; i++) { // 只要有一个实例率先改变状态,新的Kromise的状态就跟着改变 self.resolve(arr[i]).then(function (res) { resolve(res); }, function (err) { reject(err); }) } }) }; // 实现any方法 Kromise.any = function (arr) { var self = this; return new Kromise(function (resolve, reject) { var count = 0; var errors = []; for (var i = 0, len = arr.length; i < len; i++) { // 只要有一个实例状态变为fulfilled,新的Kromise状态就会改变为fulfilled self.resolve(arr[i]).then(function (res) { resolve(res); }, function (err) { errors[count] = err; count++; // 否则等待所有的rejected,新的Kromise状态才会改变为rejected if (count === arr.length) { reject(errors); } }) } }) }; // 实现allSettled方法 Kromise.allSettled = function (arr) { var results = []; var len = arr.length; for (var i = 0; i < len; i++) { this.resolve(arr[i]).then(function (res) { results.push({status: FULFILLED, value: res}); }, function (err) { results.push({status: REJECTED, value: err}); }) } // 一旦结束,状态总是`fulfilled`,不会变成`rejected` return new Kromise(function (resolve, reject) { resolve(results) }) }; // 实现try方法 Kromise.try = function (fn) { if (!isFunction(fn)) return; return new Kromise(function (resolve, reject) { return resolve(fn()); }) }; // 挂载 window.Kromise = Kromise;})();
总结
promise通过自己的回调嵌套解决别人的问题
8.参考文档
[1].JavaScript | MDN
[2].Promise迷你书
[3].廖雪峰的官方网站
[4].ES6网道教程
深入浅出Promise的更多相关文章
- 深入浅出:promise的各种用法
https://mp.weixin.qq.com/s?__biz=MzAwNTAzMjcxNg==&mid=2651425195&idx=1&sn=eed6bea35323c7 ...
- ES6深入浅出-9 Promise-3.Promise的细节
await 拿到用户信息,函数前面加await await等待Promise成功或者失败. 如果Promise里面失败了 什么也拿不到.报了一个错误,叫做不认识. 如果想拿到正常错误,就绪try一下 ...
- ES6深入浅出-9 Promise-2.Promise的用法
回调是不需要return 就可以传递数据.缺点就是嵌套多了就成了回调地狱 回调的另外一个问题拿不准应该怎么去传这个参数.以为node.js为例.nodejs有个readFile去读取文件,读取成功就用 ...
- 深入浅出写一个多级异步回调从基础到Promise实现的Demo
今天一时兴起,写了一个渐进升级的异步调用demo,记录一下. 1. 最基础的同步调用 //需求:f2在f1之后执行,且依赖f1的返回值.如下: function f1(){ var s="1 ...
- 深入理解jQuery、Angular、node中的Promise
最初遇到Promise是在jQuery中,在jQuery1.5版本中引入了Deferred Object,这个异步队列模块用于实现异步任务和回调函数的解耦.为ajax模块.队列模块.ready事件提供 ...
- 深入浅出node(1) Node简介
这一系列主要是自己在学习深入浅出node.js这本书的学习笔试,部分加入了自己的一些理解 分享给一起学习node的小伙伴 自己还是个初学者 有很多地方理解的不到位 一起交流 一 什么是node 1.1 ...
- 深入浅出 React Native:使用 JavaScript 构建原生应用
深入浅出 React Native:使用 JavaScript 构建原生应用 链接:https://zhuanlan.zhihu.com/p/19996445 原文:Introducing React ...
- 深入浅出ES6(十七):展望未来
作者 Jason Orendorff github主页 https://github.com/jorendorff 出于对文章长度的考虑,我们还保留了一些尚未提及的新特性,在最后的这篇文章中我会集 ...
- 深入浅出ES6(十一):生成器 Generators,续篇
作者 Jason Orendorff github主页 https://github.com/jorendorff 欢迎回到深入浅出ES6专栏,望你在ES6探索之旅中收获知识与快乐!程序员们在工作 ...
随机推荐
- Bootstrap工具包--用于响应式布局和移动设备优先的web项目
Bootstrap是用于前端开发的工具包,是一个css/html框架 用于响应式布局和移动设备优先的web项目 响应式布局--一个网站能兼容多个终端 有很多版本:v3,v4,v5 三个没啥区别 ...
- 多维数据处理之主成分分析(PCA)
在灵巧手与假手理论中,为了研究人手的运动协同关系,需要采集各个关节的运动学量或者多个采集点的肌电信号,然而由于人手关节数目或者EMG采集点数量较多,加上多次采样,导致需要过多的数据需要处理.然而事实上 ...
- Jquery 代码参考
jquery 代码参考 jQuery(document).ready(function($){}); jQuery(window).on('load', function(){}); $('.vide ...
- php与mysql 绑定变量和预定义处理
非select 语句(没有结果集的) 1.建立连接数据库 $mysqli=new mysqli("localhost","root","", ...
- FreeSql之Expression表达式拼接参数扩展
在FreeSql源码中Expression表达式拼接默认最多支持到5个泛型参数,当我们使用表关联比较多的时候,就需要进行扩展. 新建一个类,将命名空间改为System.Linq.Expressions ...
- transformer模型转torchscript格式
from transformers import BertModel, BertTokenizer, BertConfig import torch enc = BertTokenizer.from_ ...
- web.xml常用配置详解
web.xml常用配置详解 context-param 指定 ServletContext(上下文) 配置文件路径,基本配置一般是Spring配置文件,或者是spring-security的配置文件. ...
- Vue(1):用Vue-cli构建Vue3项目
使用Vue-cli构建Vue3项目 1.检查node版本 node -v 以上node版本位14.15.0满足Vue3项目的创建条件(Vu3需要node 版本8以上) 为什么需要安装node? vue ...
- 敏杰开题——软工团队项目选择与NABCD分析
这是一篇软件工程课程博客 Q A 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 团队项目选择 我们在这个课程的目标是 团队协作实践敏捷开发 这个作业在哪个具 ...
- 分析型CRM系统都分析什么?
在之前的文章中我们曾经讲过,目前市面上常见的CRM系统大概可以分为通用型.协助型和分析型三种类型.由于每个企业的类型.业务的不同,就需要选择一款适合的CRM客户关系管理系统.今天我们就来说一说,分析型 ...