Giter Site home page Giter Site logo

aps-ui's People

Contributors

linzx1993 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

newming wodemax

aps-ui's Issues

重温vue双向绑定原理解析

摘要:因为项目刚开始用的vue框架,所以早期也研究了一下他的代码看过相关文章的解析,说说也能说个七七八八。不过今天再去看以前的demo的时候,发现忽然一知半解了,说明当时可能也没有理解透,所以写篇文章让自己理解的更深一些。

本篇文章大多数知识点实在学习了这篇Vue.js双向绑定的实现原理文章,讲的更清楚。自己写这篇文章是因为当时看完过了3个月竟然有点忘了,所以决定自己记录下来学习过程,好记性不如烂笔头嘛。


一、访问器属性

如果稍微看过相关文章的人都知道vue的实现是依靠Object.defineproperty()来实现的。每个对象都有自己内置的set和get方法,当每次使用set时,去改变引用该属性的地方,从而实现数据的双向绑定。简单举例

const obj = {};
Object.defineProperty(obj,'hello',{
    get(value){
        console.log("啦啦啦,方法被调用了");
    },
    set(newVal,oldVal){
        console.log("set方法被调用了,新的值为" + newVal)
    }
})
obj.hello; //get方法被调用了
obj.hello = "1234"; //set方法被调用了

二、极简双向绑定的实现

基于这个原理,如果想实现显示文字根据输入input变化,实现一个简单版的。

<input type="text" id="a"/>
<span id="b"></span>

<script>
    const obj = {};
    Object.defineProperty(obj,'hello',{
        get(){
            console.log("啦啦啦,方法被调用了");
        },
        set(newVal){
            document.getElementById('a').value = newVal;
            document.getElementById('b').innerHTML = newVal;
        }
    })
    document.addEventListener('keyup',function(e){
        obj.hello = e.target.value;
    })
</script>

上面这个实例实现的效果是:随着文本框输入文字的变化,span会同步显示相同的文字内容。同时在控制台用js改变obj.hello,视图也会更新。这样就实现了view->model,model->view的双向绑定。

三、拆解任务,实现vue的双向数据绑定

我们最终实现下面vue的效果

<div id="app">
    <input type="text" v-model="text"/>
</div>
<script>
const vm = new Vue({
    id : "app",
    data : {
        text : "hello world"
    }
})
</script>

1.输入框的文本与文本节点的data数据绑定
2.输入框的内容发生变化时,data中的数据也发生变化,实现view->model的变化
3.data中的数据发生变化时,文本节点的内容同步发生变化,实现model->view的变化

要实现1的要求,则又涉及到了dom的编译,其中有一个DocumentFragment的知识点。

四、DocumentFragment

众所周知,vue吸收了react虚拟DOM的优点,使用DocumentFragment处理节点,其性能和速度远胜于直接操作dom。vue进行编译时,就是将所有挂载在dom上的子节点进行劫持到使用DocumentFragment处理节点,等到所有操作都执行完毕,将DocumentFragment再一模一样返回到挂载的目标上。

先实现一段劫持函数,将要操作的dom全部劫持到DocumentFragment中,然后再append会原位置。

<div id="app">
    <input type="text" v-model="text"/>
</div>
<script>
const app = document.getElementById("app");
const nodetoFragment = (node) => {
    const flag = document.createDocumentFragment();
    let child;
    whild(child = node.firstChild){
        flag.appendChild(child);//不断劫持挂载元素下的所有dom节点到创建的DocumentFragment
    }
    return flag
}
const dom = nodetoFragment(app);
</script>

五、数据初始化绑定

当已经获取到所有的dom元素之后,则需要对数据进行初始化绑定,这里简单涉及到了模板的编译。

//  编译HTML模板
	const compile = (node,vm) => {
		const regex = /\{\{(.*)\}\}/;//为临时正则表达式,为demo而生
		//如果节点类型为元素的话
		if(node.nodeType === 1){
			const attrs = node.attributes;//学到一个新属性。。。
			for(let i = 0;i < attrs.length; i++){
				let attr = attrs[i];
				if(attr.nodeName === "v-model"){
					let name = attr.nodeValue;
					node.addEventListener("input",function (e) {
						vm.data[name] = e.target.value;
					});
					node.value = vm.data[name];
					node.removeAttribute("v-model");
				}
			}
		}
		//如果节点类型为文本的话
		if(node.nodeType === 3){
			if(regex.test(node.nodeValue)){
				let name = RegExp.$1;//获取搭配匹配的字符串,又学到了。。。
				name = name.trim();
				node.nodeValue = vm.data[name];
			}
		}
	};

	//劫持挂载元素到虚拟dom
	let nodeToFragment = (node,vm) => {
		const flag = document.createDocumentFragment();
		let child;
		while(child = node.firstChild){
			compile(child,vm);//绑定数据,插入到虚拟DOM中
			flag.appendChild(child);
		}
		return flag;
	};

	//初始化
	class Vue {
		constructor(option){
			this.data = option.data;
			let id = option.el;
			let dom = nodeToFragment(document.getElementById(id),this);
			document.getElementById(id).appendChild(dom);
		}
	}

	const vm = new Vue({
		el : "app",
		data : {
			text : "hello world"
		}
	})

通过以上代码先实现了第一个要求,文本框和文本节点已经出现了hello woeld了

六、响应式的数据绑定

接下来我们要实现数据双向绑定的第一步,即view->model的绑定。根据之前那个简单的例子看到,我们实时获取input中的值,通过Object.defineProperty将data中的text设置为vm的访问器属性,通过set方法,当我们在设置vm.data的值时,实现数据层的绑定。在这一步,set中要做的操作是更新属性的值。

let defineReactive = (obj,key,val) => {
    Object.defineProperty(obj,key,{
        get(val){
            return val;
        }
    	set(newVal,oldVal){
    	    if(newVal === oldVal) return;
    	    val = newVal;
    	}
    })
};

//监听数据
let observe = (obj,vm) => {
	Object.keys(obj).forEach((key)=>{
		defineReactive(vm.data,key,obj[key]);
	})
};

七、订阅/发布模式(subscribe&publish)

text 属性变化了,set方法触发了,可以通过view层的改变实时改变数据,可是并没有改变文本节点的数据。一个新的知识点:订阅发布模式。

订阅发布模式(又称为观察者模式)定义了一种一对多的关系,让多个观察者同时监听一个主题对象,这个主体对象的改变会通知所有观察者对象。

发布者发出通知=>主题对象收到通知并推送给订阅者=>订阅者执行操作

//	三个订阅者
   let sub1 = {updata(){console.log(1);}};
   let sub2 = {updata(){console.log(2);}};
   let sub3 = {updata(){console.log(3);}};

   //  一个主题发布器
   class Dep{
   	constructor(){
   		this.subs = [sub1,sub2,sub3];
   	}
   	notify(){
   		subs.forEach((sub) => {
   			sub.updata();
   		})
   	}
   }
   const dep = new Dep();
//	一个发布者
   const pub = {
   	publish(){
   	    dep.notipy();
   	}
   };
   pub.publish();

上图为一个简单实例,发布者执行发布命令,所有这个主题的订阅者执行更新操作。接下去我们要做的就是,当set方法触发后,input作为发布者,改变了text属性;而文本节点作为订阅者,在收到消息后执行更新操作。

八、双向绑定的实现

每次new一个新的vue对象时,主要是做了两件事,一件是监听数据:observer(监听数据),第二个是编译HTML,nodeToFragement(id)。

在监听数据的过程中,会为data中的每一个属性生成一个主题对象。

而在编译HTML的过程中,会为每个与数据绑定的相关节点生成一个订阅者watcher,订阅者watcher会将自己订阅到相应属性的dep中。

在前面的方法中已经实现了:修改输入框内容=>再时间回调中修改属性值=>触发属性的set方法。

接下来要做的是发出通知dep.notify=>发出订阅者的uodate方法=>更新视图。

那么如何将watcher添加到关联属性的dep中呢。

编译HTML过程中,为每一个与data关联的节点生成一个watcher,那么watcher中又发生了什么?

//  每一个属性节点的watcher
class Watcher{
	constructor(vm,node,name){
		Dep.target = this;
		this.name = name;
		this.node = node;
		this.vm = vm;
		this.update();
		Dep.target = null;
	}
	update(){
		//获得最新值,然后更新视图
		this.get();
		this.node.nodeValue = this.value;
	}
	get(){
		this.value = this.vm.data[this.name];
	}
}

在编译HTML的过程中,生成watcher

let complie = (node,vm){
    ......
    //如果节点类型为文本的话
    if(node.nodeType === 3){
	    if(regex.test(node.nodeValue)){
	        let name = RegExp.$1;
	        name = name.trim();
                node.nodeValue = vm.data[name];
            
            new Watcher(vm,node,name);//在此处添加订阅者
        }
    }
}

首先将自己赋给了一个全局变量Dep.target;然后执行了uodate方法,进而执行了get方法,读取了vm的访问器属性,从而触发了访问器属性的get方法,get方法将相应的watcher添加到对应访问器属性的dep中。再次,获取属性的值,然后更新视图。最后将dep.target设置为空,是因为这是个全局变量也是watcher与dep之间唯一的桥梁,任何时间都只能保证只有一个值。(其实就是说全局一个主题,每个订阅者和发布者都是通过这个主题进行沟通。当执行代码时,这个主题接受到一个发布通知,通知完所有订阅者,然后注销掉,用于下一个通知发布。啰嗦了一段就是想讲为什么要设置Dep.target = null)。

//  一个主题发布器
class Dep(){
    constructor(){
        this.subs = [];
    }
    notify(){
        this.subs.forEach((sub) => {
            sub.update();
        }
    }
    addSub(sub){
        this.subs.push(sub);
    }
}
let defineReactive = (obj,key,val) => {
    let dep = new Dep();
    Object.defineProperty(obk,key,{
        get(){
            if(dep.target) dep.addSub(dep.target);
        }        
        set(newVal,oldVal){
            if(newVal === oldVal) return;
            val = newVal;
            dep.notify();
        }
    })
}
至此,hello world 双向绑定就基本实现了。文本内容会随输入框内容同步变化,在控制器中修改 vm.text 的值,会同步反映到文本内容中。

基于vue-cli的webpack优化配置之路

最近的项目度过了开始忙碌的基建期,也慢慢轻松下来,准备记录一下自己最近webpack优化的措施,希望有温故知新的效果。

项目采用的是vue全家桶,构建配置都是基于vue-cli去改进的。关于原始webpack配置大家可以看下这篇文章vue-cli#2.0 webpack配置分析,文章基本对于文件每行代码都做了详细的解释,有助于更好的理解webpack。

项目位置链接

仔细总结了一下,自己的优化基本还是网上流传的那几点

  • 通过 externals 配置来提取常用库,引用cdn
  • 合理配置CommonsChunkPlugin
  • 善用alias
  • dllplugin启用预编译
  • happypack多核构建项目

不过经过自己的实践最后三点是对自己项目优化最大的。文章也主要对后面几点详细说明一下

原来打包一个项目所需要的时间基本在40秒左右,那再经过后面三步优化大概要多久呢

1.使用dllplugin预编译与引用

首先为什么要引用Dll?在网上浏览了一些文章后,我发现上除了加快构建速度以外,使用webpack的dll还有一个好处。

Dll打包以后是独立存在的,只要其包含的库没有增减、升级,hash也不会变化,因此线上的dll代码不需要随着版本发布频繁更新。 因为使用Dll打包的基本上都是独立库文件,这类文件有一个特性就是变化不大。当我们正常打包这些库文件到一个app.js里的时候,由于其他业务文件的改变,影响了缓存对构建的优化,导致每次都要重新去npm包里寻找相关文件。而使用了DLL之后,只要包含的库没有升级, 增减,就不需要重新打包。这样也提高了构建速度。

那么如何使用Dll去优化项目呢
首先要建立一个dll的配置文件,引入项目所需要的第三方库。这类库的特点是不需要随着版本发布频繁更新,长期稳定。

const webpack = require('webpack');
const path = require('path');

module.exports = {
  entry: {
    //你需要引入的第三方库文件
    vendor: ['vue','vuex','vue-router','element-ui','axios','echarts/lib/echarts','echarts/lib/chart/bar','echarts/lib/chart/line','echarts/lib/chart/pie',
      'echarts/lib/component/tooltip','echarts/lib/component/title','echarts/lib/component/legend','echarts/lib/component/dataZoom','echarts/lib/component/toolbox'],
  },

  output: {
    path: path.join(__dirname, 'dist-[hash]'),
    filename: '[name].js',
    library: '[name]',
  },

  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, 'dll', '[name]-manifest.json'),
      filename: '[name].js',
      name: '[name]',
    }),
  ]
};

基本配置参数和webpack基本一模一样,相信来看优化的都明白什么意思,我就不解释了。然后执行代码编译文件。(我的配置文件是放在build里面,下方路径根据项目路径需要变动)

webpack -p --progress --config build/webpack.dll.config.js

当运行完执行后,会生成两个新文件在目录同级,一个是生成在dist文件夹下的verdor.js,里面是刚刚入口依赖被压缩后的代码;一个是dll文件夹下的verdor-manifest.json,将每个库进行了编号索引,并且使用的是id而不是name。

接下去你只要去你的webpack配置文件的里的plugin中添加一行代码就ok了。

const manifest = require('./dll/vendor-manifest.json');
...
...,
plugin:[
    new webpack.DllReferencePlugin({
        context: __dirname,
        manifest,
    }),
]

这时候再执行webpack命令,可以发现时间直接从40秒锐减到了20s左右,整整快了一倍有木有(不知道是不是因为自己依赖库太多了才这样的,手动捂脸)。

2.happypack多线程编译

一般node.js是单线程执行编译,而happypack则是启动node的多线程进行构建,大大提高了构建速度。使用方法也比较简单。以我项目为例,在插件中new一个新的happypack进程出来,然后再使用使用loader的地方替换成对应的id

var HappyPack = require('happypack');
...
...
modules:{
    rules : [
        ...
        {
            test: /\.js$/,
            loader:[ 'happypack/loader?id=happybabel'],
            include: [resolve('src')]
        },
        ...
    ]
},
...
...
plugin:[
    //happypack对对 url-loader,vue-loader 和 file-loader 支持度有限,会有报错,有坑。。。
    new HappyPack({
          id: 'happybabel',
          loaders: ['babel-loader'],
          threads: 4,//HappyPack 使用多少子进程来进行编译
    }),
    new HappyPack({
          id: 'scss',
          threads: 4,
          loaders: [
                'style-loader',
                'css-loader',
                'sass-loader',
          ],
    })
]

这时候再去执行编译webpack的代码,打印出来的console则变成了另外一种提示。而编译时间大概从20s优化到了15s左右(感觉好像没有网上说的那么大,不知道是不是因为本身js比重占据太大的缘故)。

3.善用alias

本来是没有第三点的,只不过在搜索网上webpack优化相关文章的时候,看到用人提到把引入文件改成库提供的文件(原话怎么说忘记了,就是有些库会直接提供编译好的文件给你引用,你可以直接把路径改成提供的路径。还是看例子吧,手动捂脸)。

vue-cli配置文件中提示也有提到这一点,就是下面这段代码

resolve: {
    //自动扩展文件后缀名,意味着我们require模块可以省略不写后缀名
    extensions: ['.js', '.vue', '.json'],
    //模块别名定义,方便后续直接引用别名,无须多写长长的地址
    alias: {
      'vue$': 'vue/dist/vue.esm.js',//就是这行代码,提供你直接引用文件
      '@': resolve('src'),
    }
  },

然后我将其他所有地方关于vue的引用都替换成了vue$之后,比如

// import 'vue';
import 'vue/dist/vue.esm.js';

时间竟然到了12s,也是把我吓了一跳。。。

然后我就把jquery,axios,vuex等等全部给替换掉了。。。大概优化到了9s左右,美滋滋,O(∩_∩)O~~

4.webpack3升级

本来是没第四点,刚刚看到公众号推出来一篇文章讲到升级到webpack3的一些新优点,比如Scope Hoisting(webpack2升级到webpack3基本上没有太大问题)。通过添加一个新的插件

//  2017-08-13配合最新升级的webpack3提供的新功能,可以使压缩的代码更小,运行更快
...
plugin : [
    new webpack.optimize.ModuleConcatenationPlugin(),
]
    

不过在添加这行代码之后,构建时间并没有太大变化,不过运行效率没试过,不知道新的效果怎么样

好了基本上感觉就是以上这些效果对项目的优化最大,虽然没有到网上说的那种只要3~4秒时间那么变态,不过感觉基本8,9秒的时间也可以了。

promise源码解析(译)

最新项目中有用promise几个api,对代码结构看起来的确很爽。然后想着去网上找了几篇promise库源码解析的文章。但是看了几篇,感觉还是不能够很理解,然后看到一篇翻译文章有说道q.js库的作者有对promise实现的递进讲解,看了一下,还不错,

Q.js作者源码分析:Q.js作者promise递进讲解实现

网上找的promise源码翻译。文章有些地方翻译的很好,但是对比原文发现少了一些内容,所以读起来很不顺畅。所以自己根据原文也翻译了一遍。对了,本文适合用过promise的人阅读。如果你还没有接触过。可以右转阮一峰的promise讲解


一、极简版异步回调

假如你正在写一个函数不立即返回值,而是等待几秒钟后才返回执行结果,你会怎么写呢?思考几秒钟。

最简单的做法自然是写一个回调函数依靠定时器来返回值,比如下面这个

var oneOneSecondLater = function (callback) {
    setTimeout(function () {
        callback(1);
    }, 1000);
};

这是一个很简单解决问题的方法,但是还有改进的地方,比如能够添加代码执行错误时给出提示。

var maybeOneOneSecondLater = function (callback, errback) {
    setTimeout(function () {
        //进行判断情况,是执行成功的回调,还是执行错误的回调
        if (Math.random() < .5) {
            callback(1);
        } else {
            errback(new Error("Can't provide one."));
        }
    }, 1000);
};

一般的做法是提供一个能同时返回值并且能抛出错误的工具。上面这个例子则演示同时提供回调和错误处理。但是这种写法实在是太定制化了,并不好。

二、Promise基本雏形设计

所以考虑到大多数的情况,代替最简单的返回值和抛出异常,我们更希望函数通常会返回一个对象用来表示最后执行成功或者失败的结果,而这个返回的对象就是promise。从名字上理解,promise表示承诺,那么最终这个promise(承诺)是要被resolve(履行,执行)掉的。

接下去我们开始迭代设计promise。我们先设计一个具有“then”方法的promise模型,通过“then”方法,我们能注册回调函数并且延迟执行。

var maybeOneOneSecondLater = function () {
    var callback;
    setTimeout(function () {
        callback(1);
    }, 1000);
    return {
        then: function (_callback) {
            callback = _callback;
        }
    };
};

maybeOneOneSecondLater().then(callback1);

代码写好了。但是大家仔细观察发现该方案仍然还有两个缺点

  • 一是现在方案只能执行一个添加的回调函数。最好的做法是每一个通过then添加进来的回调都能被通知到然后顺序执行。
  • 二是如果这个回调函数是在promise创建好1s之后通过then添加进去,它将无法被调用。

敲黑板,注意注意,接下去开始慢慢搭建promise了。

正常情况下,我们希望可以接收任何数量的回调,且不管是否超时,仍然可以继续注册回调。为了实现这些,我们将创建一个包含两个功能的promise对象

我们暂时设计了一个defer对象,他的返回值一个包含两部分的对象(这个对象就是promise),一个用来注册观察者(就是"then方法添加回调),一个用来通知所有的观察者执行代码(就是resolve去执行之前添加的所有回调)。

promise没有被resolve之前,所有回调函数会存储在一个"pengding"的数组中。

promiseresolve之后,立即执行之前存储的所有回调函数,当回调函数全部执行完毕之后,我们将根据"pengding"来区分状态。

let defer = () => {
    let pending = [],value;
    return {
        resolve(_value){
            value = _value
            for(let i = 0;i < pending.length; i++){
                pending[i](value)
            }
            pending = undefined;
        },
        then(_callback){
            if(pending){
                pending.push(_callback)
            }else{
                _callback();
            }
        }
    }
}
let oneOneSecondLater = () => {
    let result = defer();
    setTimeout(()=> {
        result.resolve(1);
    }, 1000);
    return result;
};

oneOneSecondLater().then(callback);

这开始的第一步很关键啊,因为此时我们已经可以做到

  1. 可以任意时间添加任意多的回调;
  2. 可以人为决定什么时候resolve;
  3. 当promise被resolve之后,还可以添加回调,只不过此时立即就执行了

但是还有一些问题,比如

  1. defer可以被resolve执行多次,我们并没有给出一个错误的提示。而且事实上为了避免恶意或者无意的不断去resolve,我们仅允许第一次调用可以通知回调并执行。
  2. 添加回调只能通过defer.then添加,不能链式调用,即defer.then(callback).then(callback)

那么接下来我们先修正第一个问题

let defer = () => {
    let pending = [],value
    return {
        resolve(_value){
            if(pending){
                value = _value
                for(let i = 0;i < pending.length; i++){
                    pending[i](value)
                }
                pending = undefined;
            }else{
                throw new Error("A promise can only be resolved once.")
            }
        },
        then(_callback){
            if(pending){
                pending.push(_callback)
            }else{
                _callback();
            }
        }
    }
}

好,现在我们已经保证不能重复defer.resolve()的问题了,那么我们还希望可以实现通过链式调用来添加回调。可是目前要只能通过defer().then(callback1),defer().then(callback2),defer().then(callback3)这种方式添加回调,这显然不是我们想要的方式。接下来我们将一步一步实现。

三、promise职责分离

但是在实现链式回调之前,为了后期结构,我们希望对我们的promise进行职责区分,一个注册观察者,一个执行观察者。根据最少授权原则,我们希望如果授权给某人一个promise,这里只允许他增加观察者;如果授权给某人resolver,他应当仅仅能决定什么时候给出解决方案。因为大量实验表明任何任何不可避免的越权行为会导致后续的改动变得很难维护。(其实就是希望把添加回调的then功能移植到promise中,从defer.then转变成defer.promise.then,保证功能的纯粹性)

let defer = () => {
    let pending = [],value;
    return {
        resolve(_value){
            if(pending){
                value = _value
                for(let i = 0;i < pending.length; i++){
                    pending[i](value)
                }
                pending = undefined;
            }else{
                throw new Error("A promise can only be resolved once.")
            }
        },
        promise: {
            then (callback) {
                if (pending) {
                    pending.push(callback);
                } else {
                    callback(value);
                }
            }
        }
    }
}

当职责分离完之后,我们就可以接下去实现一步关键的改造

四、promise的链式调用

上文说道要实现链式回调,我们首先要能在下一个回调函数里接受上一个回调的值。依靠上一步的职责分离的基础,我们接下来要跨非常大的一步,就是使用旧的promise去驱动新的promise。我们希望通过promise组合的使用,来实现值的传递。

举个例子,让你写一个相加的函数,接受两个回调函数返回的数字相加。大家可以考虑如何实现。

var twoOneSecondLater = function (callback) {
    var a, b;
    var consider = function () {
        if (a === undefined || b === undefined)return;
        callback(a + b);
    };
    oneOneSecondLater(function (_a) {
        a = _a;
        consider();
    });
    oneOneSecondLater(function (_b) {
        b = _b;
        consider();
    });
};

twoOneSecondLater(function (c) {
    // c === 2
});

上面这个方法虽然做到了,但是这个方法是脆弱的,因为我们在执行相加函数时,需要额外的代码去判断相加的数字是否有效。

于是我们希望用更少的代码去实现上面的需求,比如就像下面这样

//上面的函数如果用更少的步骤来表达就是
var a = oneOneSecondLater();
var b = oneOneSecondLater();
var c = a.then(function (a) {
    return b.then(function (b) {
        return a + b;
    });
});

上面这个例子其实想表达的就是实现callback返回值的传递,如callback1的返回值传给callback2,将callback2的返回值传给callback3。
为了实现上面例子的这种效果,我们要实现以下几点

  • 每个then方法后必须要返回一个promise
  • 每一个promise被resolve后,返回的必然是一个新的promise或者是一个执行过的值
  • 返回的promise最终可以带着回调的值被resolve掉(这句话有点难翻译,感觉就是promise.resolve(_value));

我们实现一个函数可以将获得的值传给下一个回调使用

let ref = (value) => {
    return {
        then(callback){
            callback(value);
        }
    }
}

不过考虑到有时候返回的值不仅仅是一个值,而且还可能是一个promise函数,所以我们需要加个判断

let ref = (value) => {
    if(value && typeof value.then === "function"){
        return value;
    }
    return {
        then(callback){
            callback(value);
        }
    }
}

这样子我们在使用中就不需要考虑传入的值是一个普通值还是一个promise了。

接下来,为了能使then方法也能返回一个promise,我们来改造下then方法;我们强制将callback的返回值传入下一个promise并立即返回。
这个例子存储了回调的值,并在下一个回调中执行了。但是上面第三点没有实现,因为返回值可能是一个promise,那么我们继续改进一下方法

let ref = (value) => {
    if(value && typeof value.then === "function"){
        return value;
    }
    return {
        then(callback){
            return ref(callback(value));
        }
    }
}

通过这一步增强之后,基本上就可以做到获得上一个回调值并不断链式调用下去了。

接下去我们考虑到一种比较复杂的情况,就是defer中存储的回调会在未来某个时间调用。于是我们需要在defer里面将回调进行一次封装,我们将回调中执行完后通过then方法去驱动下一个promise并传递一个返回值。

此外,resolve方法应该能处理本身是一个promise的情况,resolve可以将值传递给promise。因为不管是ref还是defer都可以返回一个then方法。如果promise是ref类型的,将会通过then(callback)立即执行回调。如果是promise是defer类型的,callback暂时被存储起来,依靠下一个then(callback)调用才能执行;所以变成了callback可以监听一个新的promise以便能获取完全执行后的value。

根据以上要求,得出了下面最终版的promise

let isPromise = (value) => {
    return value && typeof value.then === "function";
};

let ref = (value) => {
    if (value && typeof value.then === "function")
        return value;
    return {
        then (callback) {
            return ref(callback(value));
        }
    };
};

let defer = () => {
    let pending = [], value;
    return {
        resolve: function (_value) {
            if (pending) {
                value = ref(_value); // values wrapped in a promise
                for (let i = 0, ii = pending.length; i < ii; i++) {
                    let callback = pending[i];
                    value.then(callback); // then called instead
                }
                pending = undefined;
            }
        },
        promise: {
            then: function (_callback) {
                let result = defer();
                // callback is wrapped so that its return
                // value is captured and used to resolve the promise
                // that "then" returns
                let callback = function (value) {
                    result.resolve(_callback(value));
                };
                if (pending) {
                    pending.push(callback);
                } else {
                    value.then(callback);
                }
                return result.promise;
            }
        }
    };
};

let a = defer();
a.promise.then(function(value){console.log(value);return 2}).then(function(value){console.log(value)});
a.resolve(1);

将defer分为两个部分,一个是promise,一个是resolve

到了这一步基本上的promise功能已经实现了,可以链式调用,可以在自己控制在未来某个时间resolve。接下去就是功能的增强和补足了。

这一块回调基本上就写完了,看了很久原文的描述,对着代码理解作者想表达的意思。不过英语不太好,写的磕磕绊绊。╮(╯▽╰)╭,感觉还是有些地方写的不对。希望有人能够纠错出来。

五、提供错误的回调

为了实现错误消息的传递,我们还需要一个错误的回调函数(errback)。就像promise完全执行时调用callback一样,它会告知执行errback以及告诉我们拒绝的原因。

实现一个类似于前面ref的函数。

let reject = (reason) => {
    return {
        then(callback,errback){
            return ref(errback(reason);
        }
    }
}

最简单的实现方法是当监听到返回值时,立即执行代码

reject("Meh.").then((value) => {},(reason) => {
    throw new Error(reason);
}

那么接下来我们改进原来promsie这个API,引入“errback”。

为了将错误回调添加到代码中,defer需要添加一种新的容器来添加成功回调和错误回调。因此之前那个存储在数组(pending)中的只有一种待处理回调函数,我们需要重新设计一个同时包含成功回调和错误回调的数组([callback,errback]),根据then传入的参数决定调用哪个。

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            if (pending) {
                value = ref(_value);
                for (var i = 0, ii = pending.length; i < ii; i++) {
                    // apply the pending arguments to "then"
                    value.then.apply(value, pending[i]);
                }
                pending = undefined;
            }
        },
        promise: {
            then: function (_callback, _errback) {
                var result = defer();
                var callback = function (value) {
                    result.resolve(_callback(value));
                };
                var errback = function (reason) {
                    result.resolve(_errback(reason));
                };
                if (pending) {
                    pending.push([callback, errback]);
                } else {
                    value.then(callback, errback);
                }
                return result.promise;
            }
        }
    };
};

let ref = (value) => {
    if (value && typeof value.then === "function")
        return value;
    return {
        then: function (callback) {
            return ref(callback(value));
        }
    };
};

let reject = (reason) => {
    return {
        then: function (callback, errback) {
            return ref(errback(reason));
        }
    };
};

代码写完了,但是仍然还有地方可以改进。

比如作者说到这一步有一个问题,就是如果按照上面这么写,那么所有的then函数就必须提供错误回调函数(_errback),如果不提供就会出错。所以最简单的解决方法是提供一个默认的回调函数。甚至文中还说,如果仅仅是对错误回调有需要,那么忽略不写成功回调(_callback)也是可以的。所以为了满足需求,我们为_callback和_errback都提供一个默认的回调函数。(好吧,其实我就是觉得这是一个好的库的容错处理)

var defer = function () {
    ...
    return{
        ...
        promise : {
             then: function (_callback, _errback) {
                var result = defer();
                // 提供一个默认的成功回调和错误回调
                _callback = _callback || function (value) {
                    // 默认执行
                    return value;
                };
                _errback = _errback || function (reason) {
                    // 默认拒绝
                    return reject(reason);
                };
                var callback = function (value) {
                    result.resolve(_callback(value));
                };
                var errback = function (reason) {
                    result.resolve(_errback(reason));
                };
                if (pending) {
                    pending.push([callback, errback]);
                } else {
                    value.then(callback, errback);
                }
                return result.promise;
            }
        }
        }
    }
    
}

好了,现在我们已经实现了接收构造或者隐含的错误回调这一步的完成版

六、安全性和稳定性

我们还有需要需要提高的地方就是要保证callbacks和errbacks在未来他们被调用的时候,应该是和注册时的顺序是保持一致的。这将显著降低异步编程中流程控制出错可能性。文中举了一个有趣的小例子.

var blah = function () {
    var result = foob().then(function () {
        return barf();
    });
    var barf = function () {
        return 10;
    };
    return result;
};

上面这个函数在执行后会出现两种情况,一是抛出一个异常,二是顺利执行并返回了值10。而决定是哪个结果的是foob()是否在正确顺序里。因为我们希望哪怕回调在未来被延迟执行了,它能够执行成功。

下面添加了一个enqueue方法,我的理解就是依靠setTimeout的异步将所有回调按照顺序添加到任务队列中,保证按照顺序执行代码。

let enqueue = (callback) => {
    setTimeout(callback,1)
}
let enqueue = (callback) => {
    //process.nextTick(callback); // NodeJS
    setTimeout(callback, 1); // Naïve browser solution
};

let defer = function () {
    let pending = [], value;
    return {
        resolve: function (_value) {
            if (pending) {
                value = ref(_value);
                for (let i = 0, ii = pending.length; i < ii; i++) {
                    enqueue(function () {
                        value.then.apply(value, pending[i]);
                    });
                }
                pending = undefined;
            }
        },
        promise: {
            then: function (_callback, _errback) {
                let result = defer();
                _callback = _callback || function (value) {
                    return value;
                };
                _errback = _errback || function (reason) {
                    return reject(reason);
                };
                let callback = function (value) {
                    result.resolve(_callback(value));
                };
                let errback = function (reason) {
                    result.resolve(_errback(reason));
                };
                if (pending) {
                    pending.push([callback, errback]);
                } else {
                    // XXX
                    enqueue(function () {
                        value.then(callback, errback);
                    });
                }
                return result.promise;
            }
        }
    };
};

let ref = function (value) {
    if (value && value.then)
        return value;
    return {
        then: function (callback) {
            let result = defer();
            // XXX
            enqueue(function () {
                result.resolve(callback(value));
            });
            return result.promise;
        }
    };
};

let reject = function (reason) {
    return {
        then: function (callback, errback) {
            var result = defer();
            // XXX
            enqueue(function () {
                result.resolve(errback(reason));
            });
            return result.promise;
        }
    };
};

虽然将需要的回调依照次序添加到了队列中

作者有考虑到一些新的问题,比如

  • callback或者errback必须以同样的顺序被调用
  • callback或者errback可能会被同时调用
  • callback或者errback可能会被调用多次

于是我们需要找个机会then的回调函数,为了保证当回调函数中程序出错时,可以转入到报错函数中。(其实又是一个库的容错处理,保证代码出错时不中断程序的执行)。

用when方法封装下promise以此阻止错误发生,确保不会有哪些突发性的错误,包括哪些非必需的事件流控制,并且也能使callback和errback各自保持独立。

var when = function (value, _callback, _errback) {
    var result = defer();
    var done;

    _callback = _callback || function (value) {
        return value;
    };
    _errback = _errback || function (reason) {
        return reject(reason);
    };

    var callback = function (value) {
        try {
            return _callback(value);
        } catch (reason) {
            return reject(reason);
        }
    };
    var errback = function (reason) {
        try {
            return _errback(reason);
        } catch (reason) {
            return reject(reason);
        }
    };

    enqueue(function () {
        ref(value).then(function (value) {
            if (done)
                return;
            done = true;
            result.resolve(ref(value).then(callback, errback));
        }, function (reason) {
            if (done)
                return;
            done = true;
            result.resolve(errback(reason));
        });
    });

    return result.promise;
};

这一步的完整版

六、消息传递

现在这一步来看,promise已经成为了一个具有接受消息功能的类了。Deferred promise根据获得的消息来执行对应的回调函数,返回对应的值。当你接收到完全成功执行的值,则在then中执行成功的回调函数返回msg;获得错误的值则在then中执行错误回调函数,返回错误的原因

因此我们基本可以认为promise这个类可以接受任何的值,包括"then/when"这些信息。这对于一些非立即执行函数的监听非常有用。举个例子,当你发了一个网络请求,等待返回值才能执行函数。我们等待这个请求的往返的过程中浪费了许多时间,而promise仿佛在电脑中另外开了一个线程进行监听这些返回值,然后执行对应的回调函数(这个例子是自己理解举的,非原文,如有不对,欢迎改正)。

翻到这里有点崩溃了,捂下脑子,接下去感觉有点头疼了,以后再补吧,因为基本形态的promise已经出来。接下去是另外一种需求的promise了

接下来我们要包装一种新型的promise,这套promise基于一些能发送任意消息的方法之上,可以满足 "get", "put", "post"能发送相应的消息,并且能根据返回结果中执行相应的promise。


第一次尝试翻译,真的是个体力活,花了快2天的时间,整个人都是炸的。不过所幸是比以前明白了一些恭喜。
原文大概讲解了基本的promise构成,但是现在还是有许多方法并没有分析,接下去我按照自己的想法去实现以下promise.all方法。如果写的不好,欢迎大家指正,帮我进步一下,谢谢。(手动捂脸)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.