计算属性只有它的相关wpf 依赖项属性发生改变时才会重新求值

对于最终的结果,两种方式确实是相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要&message&还没有发生改变,多次访问&reversedMessage&计算属性会立即返回之前的计算结果,而不必再次执行函数。例如:
&div id="root"&
&p&now is: "{{ method_now() }}"&/p&
&p&cnow is: "{{ computed_now}}"&/p&
var vm = new Vue({
el: '#root',
method_now(){
return Date.now();
computed:{
computed_now: function () {
return Date.now();
相比而言,只要发生重新渲染,method 调用总会执行该函数。
我们为什么需要缓存?假设我们有一个性能开销比较大的的计算属性&A&,它需要遍历一个极大的数组和做大量的计算。然后我们可能有其他的计算属性依赖于&A&。如果没有缓存,我们将不可避免的多次执行&A&的 getter!如果你不希望有缓存,请用 method 替代。
阅读(...) 评论()posts - 101,&
comments - 1,&
trackbacks - 0
&!DOCTYPE html&
&script src="/vue@2.4.2"&&/script&
&meta charset="utf-8" /&
&title&&/title&
&div id="app-6"&
&p v-once v-if="see"&{{ message }}&/p&
v-if="see"&{{ reversedMessage
&p v-if="see"&{{ reversedMessages()
v-model="message"&
&todo-item
v-for="item in groceryList"
v-bind:todo="item"
v-bind:key="item.id"
&&/todo-item&
window.onload = function () {
ponent('todo-item', {
template: '&li&{{todo.text}}&/li&',
props:['todo']
app6 = new Vue({
el: '#app-6',
see: true,
groceryList: [
{ id: 0, text: '蔬菜' },
{ id: 1, text: '奶酪' },
{ id: 2, text: '随便其他什么人吃的东西' }
message: 'Hello Vue!'
},//计算属性
computed: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
},//方法返回
methods: {
reversedMessages: function () {
return this.message.split('').reverse().join('')
  注意 由于计算属性与method 与data中的属性都是保存在app6这个对象的一级属性里面 所以如果重名 后定义的将会覆盖前定义的对象
官方文档解释两者的差别
我们可以将同一函数定义为一个 method 而不是一个计算属性。对于最终的结果,两种方式确实是相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要&message&还没有发生改变,多次访问&reversedMessage&计算属性会立即返回之前的计算结果,而不必再次执行函数。
这也同样意味着下面的计算属性将不再更新,因为&Date.now()&不是响应式依赖:
computed: {
now: function () {
return Date.now()
相比而言,只要发生重新渲染,method 调用总会执行该函数。
文档地址&https://cn.vuejs.org/v2/guide/computed.html
阅读(...) 评论()Knockout 官方中文文档
使用计算属性下(Computed Observable){"debug":false,"apiRoot":"","paySDK":"/api/js","wechatConfigAPI":"/api/wechat/jssdkconfig","name":"production","instance":"column","tokens":{"X-XSRF-TOKEN":null,"X-UDID":null,"Authorization":"oauth c3cef7c66aa9e6a1e3160e20"}}
{"database":{"Post":{"":{"title":"Javascript元编程(一)","author":"starkwei","content":"这几天把一年多前买的《松本行弘的程序世界》重新看了看,很多当时不能理解的东西现在再去看真是茅塞顿开呀,看到元编程那一段真是把我震撼到了,后来发现 Javascript 里其实也是有一些支持元编程的特性的,今天就用一个 DEMO 示范一下吧。什么元编程“元编程”这个名字看起来高端大气上档次,它的含义也是相当高端:“写一段自动写程序的程序”,不要误会,我们做的可不是人工智能。言简意赅地说,元编程就是将代码视作数据,直接用字符串 or AST or 其他任何形式去操纵代码,以此获得一些维护性、效率上的好处。Javascript 中,eval、new Function() 便是两个可以用来进行元编程的特性。原始示例现在我们有一堆用户的数据,具体字段有name, sex, age, address等等,通过类似 /get_name?id=123456 来拉取数据那么我们很容易写出这样的代码:class User {\n
constructor(userID) {\n
this.id = userID;\n
get_name() {\n
return $.ajax(`/get_name?id=${this.id}`);\n
get_sex() {\n
return $.ajax(`/get_sex?id=${this.id}`);\n
//下面是get_age、get_address......\n}这段代码的问题在哪呢?首先,用户数据有多少个字段,我们就要定义多少个 get_something 方法,更可怕的是这些方法里逻辑都是重复的,都是一个简单的 ajax。进阶(一)我们可以把拉取数据的逻辑封装到 __fetchData 里:class User {\n
constructor(userID) {\n
this.id = userID;\n
__fetchData(key) {\n
//这是一个private方法,直接调用类似__fetchData(\"age\")是不被允许的\n
return $.ajax(`/get_${key}?id=${this.id}`)\n
get_name() {\n
return this.__fetchData('name');\n
get_sex() {\n
return this.__fetchData(\"sex\");\n
//下面是get_age、get_address......\n}然后,冗余的问题可以通过 registerProperties 来解决:class User {\n
constructor(userID) {\n
this.id = userID;\n
this.registerProperties([\"name\", \"age\", \"sex\", \"address\"]);\n
registerProperties(keyArray) {\n
keyArray.forEach(key =& {\n
this[`get_${key}`] = () =& this.__fetchData(key);\n
__fetchData(key) {\n
//这是一个private方法,直接调用类似__fetchData(\"age\")是不被允许的\n
return $.ajax(`/get_${key}?id=${this.id}`)\n
}\n}进阶(二)到目前为止我们都没有涉及到任何元编程的概念,下面我们加上更高的需求:在拉去数据之后,我们要对部分数据进行一定的处理,比如对 name 我们要去掉首尾的空格,对 age 我们要加上一个 岁 字。具体的处理方法定义在 __handle_something 里面。这里我们便可以通过 new Function() 来动态生成函数,元编程开始显现威力:class User {\n
constructor(userID) {\n
this.id = userID;\n
this.registerProperties([\"name\", \"age\", \"sex\", \"address\"]);\n
registerProperties(keyArray) {\n
keyArray.forEach(key =& {\n
//注意这里的fnBody内部依然采用ES5的写法,因为babel目前不会编译函数字符串。\n
var fnBody = `return this.__fetchData(\"/get_${key}?id=${this.id}\")\n
.then(function(data){\n
return this.__handle_${key}?_this.handle_${key}(data):\n
this[`get_${key}`] = new Function(fnBody);\n
__handle_name(name) {\n
//do somthing with name...\\n
__handle_age(age) {\n
//do somthing with age...\\n
__fetchData(key) {\n
return $.ajax(`/get_${key}?id=${this.id}`)\n
}\n\n}进阶(三)下面我们让需求更加变态一点:数据并非通过 ajax 直接拉取,而是通过一个别人封装好的 UserDataBase 里的方法来拉取;数据的字段并非只有 name, sex, age, address 四个,而是要根据 UserDataBase 里给你的方法决定。给你1000个get不同字段的方法,User 类里也要有对应的1000个方法。class UserDataBase {\n
constructor() {}\n
get_name(id) {}\n
get_age(id) {}\n
get_address(id) {}\n
get_sex(id) {}\n
get_anything_else1(id) {}\n
get_anything_else2(id) {}\n
get_anything_else3(id) {}\n
get_anything_else4(id) {}\n
//......\n\n}这里我们就需要用到 JS 的反射机制来读取所有拉取字段的方法,然后通过元编程的方式来动态生成对应的方法。class User {\n
constructor(userID, dataBase) {\n
this.id = userID;\n
this.__dataBase = dataB\n
for (var method in dataBase) {\n
//对每一个方法\n
this.registerMethod(method);\n
registerMethod(methodName) {\n
//这里除去了前置的\"get_\"\n
var propertyName = methodName.slice(4);\n
//注意这里拉取数据的方法改为使用dataBase\n
var fnBody = `return this.__dataBase.${methodName}()\n
.then(function(data){\n
return this.__handle_${propertyName}?_this.handle_${propertyName}(data):\n
this[`get_${propertyName}`] = new Function(fnBody);\n
__handle_name(name) {\n
//do somthing with name...\\n
__handle_age(age) {\n
//do somthing with age...\\n
}\n}\n\nvar userDataBase = new UserDataBase();\nvar user = new User(\"123\", userDataBase);这样即使用户数据有一万种不同的属性字段,只要保证 UserDataBase 中良好地定义了对应的拉取方法,我们的 User 就能自动生成对应的方法。这也就是元编程的优点之一,程序可以根据传入参数/对象的不同,动态地生成对应的程序,从而减少大量冗余的代码。进阶(四)现在程序里还有点小瑕疵://用户数据中不存在www字段,若这样执行会报错:\nuser.get_www(); //user.get_www is not a function现在我们要保证像上面那样执行任意的 user.get_xxxx() ,程序不会报错,而是返回 false://用户数据中不存在www字段:\nuser.get_www(); // =& falseJavascript 里缺少了 Ruby 中 method_missing 这样黑科技的内核方法,但是我们可以通过 ES6 的 Proxy 特性来模拟:function createUser(id, userDataBase) {\n
return new Proxy(new User(id, userDataBase), {\n
get: (target, property) =& (typeof(target[property]) === \"function\" ? target[property] : () =& false)\n
})\n}\n\nvar userDataBase = new UserDataBase();\nvar user = createUser(\"123\", userDataBase);\n\nuser.get_name() =& // fetch name data\nuser.get_wwwwww() // =& false\n\n总结其实这里的 DEMO 只是元编程的一个小应用,下一篇文章里我们会通过元编程实现一个简单的表单验证 DSL ://类似\nform.name[\"is not empty\"][\"length is between\",1,20] // =& true or false\n参考","updated":"T05:35:35.000Z","canComment":false,"commentPermission":"anyone","commentCount":3,"likeCount":20,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T13:35:35+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"/426f31eec5b07af3e665be613d6b052d_r.jpg","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":3,"likesCount":20},"":{"title":"手写一个CommonJS打包工具(二)","author":"starkwei","content":"前一篇文章:上周就写完了这个小东西:一直很忙(lǎn)没有补readme和注释,今天补上了。目前还不支持node_modules和global变量,有时间再加吧。其实具体的实现蛮简单的,所以就不再一段段解释,直接写到代码的注释里了:懒得点开的话可以直接看下面:import Promise from \"bluebird\";\nimport fs_origin from \"fs\";\nimport { js_beautify } from \"js-beautify\";\nimport path from \"path\";\n\nvar fs = Promise.promisifyAll(fs_origin);\n\n//__MODULES用于映射moduleID\nvar __MODULES = [\n
// 0: 'index',\n
// 1: 'module1'\n
// 2: 'test/module2'\n];\n\n//读取命令行参数\nif (process.argv[2]) {\n
console.log(\"starting bundle \" + process.argv[2]);\n
pack(process.argv[2]);\n} else {\n
console.log(\"No File Input\");\n}\n\n\nfunction pack(fileName) {\n
var name = fileName.replace(/\\.js/, \"\");\n\n
//基本的module模板\n
var str = \"function(module, exports, require, global){\\n{{moduleContent}}\\n},\\n\";\n\n
//递归打包\n
bundleModule(name, './')\n
.then(() =& {\n
console.log(__MODULES);\n\n
//把模块名替换成数字ID\n
return Promise.map(__MODULES, (moduleName =& replaceRequireWithID(moduleName)))\n
.then(moduleContents =& {\n
//合并模块\n
var modules = \"[\";\n
moduleContents.forEach(content =& {\n
modules += str.replace(/{{moduleContent}}/, content);\n
return modules += \"]\"\n
.then(modules =& fs.readFileAsync(\"packSource.js\", \"utf-8\").then(content =& content + \"(\" + modules + \")\"))\n
.then(result =& js_beautify(result))\n
.then(x =& log(x))\n
.then(result =& fs.writeFileAsync(\"bundle.js\", result))\n
.then(() =& console.log(\"bundle success!\"));\n}\n\n\n//递归打包的方法\n//接收两个参数:moduleName是模块名,nowPath是当前路径\nfunction bundleModule(moduleName, nowPath) {\n
console.log(\"reading :\", path.normalize(nowPath + moduleName + '.js'));\n
return fs.readFileAsync(path.normalize(nowPath + moduleName + '.js'), 'utf-8')\n
.then(contents =& {\n
//在__MODULES中注册这个模块名\n
__MODULES.push(path.normalize(nowPath + moduleName))\n
.then(contents =& matchRequire(contents))//解析出require\n
.then(requires =& {\n
if (requires.length & 0) {\n
//对每个require分别递归打包\n
return Promise.map(requires, (requireName =& {\n
return bundleModule(requireName, path.dirname(nowPath + moduleName) + \"/\")\n
} else {\n
return Promise.resolve();\n
})\n}\n\n//把模块名替换成ID的方法\n//接收一个参数:moduleName即模块名\nfunction replaceRequireWithID(moduleName) {\n
var dirPath = path.dirname(moduleName) + '/';\n
return fs.readFileAsync(moduleName + '.js', 'utf-8')\n
.then(code =& {\n
matchRequire(code).forEach(item =& {\n
var reg1 = new RegExp(\"require\\\\(\\\"\" + item + \"\\\"\\\\)\");\n
var reg2 = new RegExp(\"require\\\\(\\'\" + item + \"\\'\\\\)\");\n
var modulePath = path.normalize(dirPath + item);\n
var moduleID = __MODULES.indexOf(modulePath);\n
code = code.replace(reg1, \"require(\" + moduleID + \")\").replace(reg2, \"require(\" + moduleID + \")\");\n
})\n}\n\n\n//解析依赖的模块名\nfunction matchRequire(code) {\n
var requires1 = code.match(/require\\(\"\\S*\"\\)/g) || [];\n
var requires2 = code.match(/require\\('\\S*'\\)/g) || [];\n
return requires1.map(item =& item.match(/\"\\S*\"/)[0]).map(item =& item.substring(1, item.length - 1))\n
.concat(requires2.map(item =& item.match(/'\\S*'/)[0]).map(item =& item.substring(1, item.length - 1)));\n}\n\n\nfunction log(a) {\n
console.log(a);\\n}\n\n","updated":"T01:08:09.000Z","canComment":false,"commentPermission":"anyone","commentCount":2,"likeCount":1,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T09:08:09+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"/eab1ce569f175e_r.png","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":2,"likesCount":1},"":{"title":"一个浏览器和NodeJS通用的小型RPC框架","author":"starkwei","content":"这几天写了个小型的RPC框架,最初只是想用 TCP-JSON 写个纯 NodeJS 平台的东西,后来无意中开了个脑洞,如果基于 Websocket 把浏览器当做 RPC Server ,那岂不是只要是能运行浏览器(或者nodejs)的设备,都可以作为分布式计算中的一个 Worker 了吗?打开一张网页,就能成为分布式计算的一个节点,看起来还是挺酷炫的。一、什么是RPC可以参考:简单地说就是你可以这样注册一个任意数量的 Worker (姑且叫这个名字好了),它里面声明了具体的方法实现:var rpcWorker = require('maus').\nrpcWorker.create({\n
add: (x, y) =& x + y\n}, 'http://192.168.1.100:8124');然后你可以在另一个node进程里这样调用:var rpcManager = require('maus').\nrpcManager.create(workers =& {\n\tworkers.add(1, 2, result =& console.log(result));\n}, 8124)这里我们封装了底层的通信细节(可以是tcp、http、websocket等等)和任务分配,只需要用异步的方式去调用 worker 提供的方法即可,通过这个我们可以轻而易举地做到分布式计算的 map 和 reduce :rpcManager.create(workers =& {\n\t//首先定义一个promise化的add\n\tvar add = function(x, y){\n\t\treturn new Promise((resolve, reject)=&{\n\t\t\tworkers.add(x, y, result =& resolve(result));\n\t\t})\n\t}\n\t//map&reduce\n\tPromise.all([add(1,2), add(3,4), add(4,5)])\n\t\t.then(result =& result.reduce((x, y) =& x + y))\n\t\t.then(sum =& console.log(sum)) //19\n}, 8124)如果我们有三个已经注册的 Worker(可能是本地的另一个nodejs进程、某个设备上的浏览器、另一个机器上的nodejs),那么我们这里会分别在这三个机器上分别计算三个 add ,并且将三个结果在本地相加,得到最后的值,这就是分布式计算的基础。二、Manager的实现0、通信标准要实现双向的通信,我们首先要定义这样一个“远程调用”的通信标准,在我的实现中比较简单:{\n\t[id]: uuid
//在某些通信中需要唯一标识码\n\tmessage: '......'
//消息类别\n\tbody: ......
//携带的数据\n}1、初始化首先我们要解决的问题是,如何让 Manager 知道 Worker 提供了哪些方法可供调用?这个问题其实很简单,只要在 websocket 建立的时刻发送一个`init`消息就可以了,`init`消息大概长这样:{\n\tmessage: 'init',\n\tbody: ['add', 'multiply'] //body是方法名组成的数组\n}同时,我们要将 Manager 传入的回调函数,记录到 Manager.__workersStaticCallback 中,以便延迟调用:manager.create(callback, port) //记录下这个callback\n\n//一段时间后。。。。。。\n\nmanager.start() //任务开始2、生成workers实例现在我们的 Manager 收到了一个远程可调用的方法名组成的数组,我们接下来需要在 Manager 中生成一个 workers 实例,它应该包含所有这些方法名,但底层依然是调用一个webpack通信。这里我们可以用类似元编程的奇技淫巧,下面的是部分代码://收到worker发来的init消息之后\nvar workers = {\n
__send: this.__send.bind(this), //这个this指向Manager,而不是自己\n
__functionCall: this.__functionCall.bind(this) //同上\n};\nvar funcNames = data. //比如['add', 'multiply']\nfuncNames.forEach(funcName =& {\n\t//使用new Function的奇技淫巧\n\trpc[funcName] = new Function(`\n
//截取参数\n
var params = Array.prototype.slice.call(arguments,0,arguments.length-1);\n
var callback = arguments[arguments.length-1];\n
//这个__functionCall调用了Manager底层的通信,具体在后面解释\n
this.__functionCall('${funcName}',params,callback);\n
`)\n})\n//将workers注册到Manager内部\nthis.__workers =\n//如果此时Manager已经在等待开始了,那么开始任务\nif (this.__waitingForInit) {\n
this.start();\n}还记得上面我们有个 start 方法么?它是这样写的:start: function() {\n
if (this.__workers != undefined) {\n
//如果初始化完毕,workers实例存在\n
this.__workersStaticCallback(this.__workers);\n
this.__waitingForInit =\n
} else {\n
//否则将等待初始化完毕\n
this.__waitingForInit =\n
}\n},3、序列化如果只是单个 Worker 和单个 Manager ,并且远程方法都是同步而非异步的,那么我们显然不需要考虑返回值顺序的问题:比如我们的 Manager 调用了下面一堆方法:workers.add(1, 1, callback);\nworkers.add(2, 2, callback);\nworkers.add(3, 3, callback);由于 Worker 中 add 的是同步的方法,那么显然我们收到返回值的顺序是:2\n4\n6但如果 Worker 中存在一个异步调用,那么这个顺序就会被打乱:workers.readFile('xxx', callback);\nworkers.add(1, 1, callback);\nworkers.add(2, 2, callback);\n显然我们收到的返回值顺序是:2\n4\ncontent of xxx所以这里就需要对发出的函数调用做一个序列化,具体的方法就是对于每一个调用都给一个 uuid(唯一标识码)。比如我们调用了:workers.add(1, 1, stupid_callback);那么首先 Manager 会对这个调用生成一个 uuid :d7-4c94-84c8-f4然后在 __callbackStore 中将这个 uuid 和 stupid_callback
绑定,然后向选中的某个 Worker 发送函数调用信息(具体怎么选 Worker 我们后面再说):{\n\tid: 'd7-4c94-84c8-f4',\n\tmessage: 'function call',\n\tbody: { \n\t\tfuncName: 'add', \n\t\tparams: [1, 1] \n\t}\n}Worker 执行这个函数之后,发送回来一个函数返回值的信息体,大概是这样:{\n\tid: 'd7-4c94-84c8-f4',\n\tmessage: 'function call',\n\tbody: { \n\t\tresult: 2 \n\t}\n}然后我们就可以在 __callbackStore 中找到这个 uuid 对应的 callback ,并且执行它:this.__callbackStore[id](result);这就是 workers.add(1, 1, stupid_callback) 这行代码背后的原理。4、任务分配如果存在多个 Worker ,显然我们不能把所有的调用都傻傻地发送到第一个 Worker 身上,所以这里就需要有一个任务分配机制,我的机制比较简单,大概说就是在一张表里对每个 Worker 记录下它是否繁忙的状态,每次当有调用需求的时候,先遍历这张表,如果找到有空闲的 Worker ,那么就将对它发送调用;如果所有 Worker 都繁忙,那么先把这个调用暂存在一个队列之中;当收到某个 Worker 的返回值后,会检查队列中是否有任务,有的话,那么就对这个 Worker 发送最前的函数调用,若没有,就把这个 Worker 设为空闲状态。具体任务分配的代码比较冗余,分散在各个方法内,所以只介绍方法,就不贴上来了/w\\全部的Manager代码在这里(抱歉还没时间补注释):三、Worker的实现这里要再说一遍,我们的RPC框架是基于websocket的,所以 Worker 可以是一个PC浏览器!!!可以是一个手机浏览器!!!可以是一个平板浏览器!!!Worker 的实现远比 Manager 简单,因为它只需要对唯一一个 Manager 通信,它的逻辑只有:接收 Manager 发来的数据;根据数据做出相应的反应(函数调用、初始化等等);发送返回值所以我也不放代码了,有兴趣的可以看这里:四、写一个分布式算法假设我们的加法是通过这个框架异步调用的,那么我们该怎么写算法呢?在单机情况下,写个斐波拉契数列简直跟喝水一样简单(事实上这种暴力递归的写法非常非常傻逼且性能低下,只是作为范例演示用):var fib = x =& x&1 ? fib(x-1)+fib(x-2) : x但是在分布式环境下,我们要将 workers.add 方法封装成一个Promise化的 add ://这里的x, y可能是数字,也可能是个Promise,所以要先调用Promise.all\nvar add = function(x, y){\n\treturn Promise.all([x, y])\n\t\t.then(arr =& new Promise((resolve, reject) =& {\n\t\t\tworkers.add(arr[0], arr[1], result =& resolve(result));\n\t\t}))\n}然后我们就可以用类似同步的递归方法这样写一个分布式的 fib 算法:var fib = x =& x&1 ? add(fib(x-1), fib(x-2)) : x然后你可以尝试用你的电脑里、树莓派里、服务器里的nodejs、手机平板上的浏览器作为一个 Worker ,总之集合所有的计算能力,一起来计算这个傻傻的算法(事实上相比于单机算法会慢很多很多,因为通信上的延迟远大于单机的加法计算,但只是为了演示啦)://分布式计算fib(40)\nfib(40).then(result =& console.log(result));","updated":"T06:08:41.000Z","canComment":false,"commentPermission":"anyone","commentCount":1,"likeCount":7,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T14:08:41+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"/83be33017abd2e80d29531_r.jpg","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":1,"likesCount":7},"":{"title":"给自己的RPC做了点小改进","author":"starkwei","content":"前几天写的这个小东西,今天做完了0.2.0版。改进的地方主要有:1、不再依赖 express 了;2、Manager 的接口稍微优雅了一点;3、允许远程调用的参数为函数:简而言之你可以在 Worker 中这样定义一个函数:var rpcWorker = require('maus').\nrpcWorker.create({\n
calculate: (x, f) =& f(x)\n}, 'http://localhost:8124');\n然后在 Manager 中传入一个函数作为参数:var rpcManager = require('maus').\n\nvar myManager = new rpcManager(8124);\n\nmyManager.do(workers =& {\n
workers.calculate(1, x =& x+1, result =& console.log(result)); //2\n
//甚至可以写一个递归(使用\"__this\"来指代函数自身):\n
var fib = x =& x & 1 ? __this(x - 1) + __this(x - 2) :\n
workers.calculate(10, fib, result =& console.log(result)); //55\n})\n","updated":"T04:30:59.000Z","canComment":false,"commentPermission":"anyone","commentCount":0,"likeCount":1,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T12:30:59+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"/83be33017abd2e80d29531_r.jpg","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":0,"likesCount":1},"":{"title":"Maus 0.3发布","author":"starkwei","content":"本来应该早几天就写完的,结果拖延症到现在。Maus 0.3新特性如下:当任意Worker连接失去时,Manager会把已发出给这个Worker但未完成的函数调用重新发送到其它可用的Worker上。简单地说即使,现在的框架允许任意Worker失联(当然起码要有一个Worker啦),大大提高了容错性。加入了Parkserver(职介所)的新特性,下面详细说说Parkserver是啥。Parkserver一个最简单的分布式框架大概就是一个Manager和若干个Worker,Manager对Worker发出函数调用,Worker执行完成后把结果返回至Manager进行下一步处理。这种模式的问题在于Manager和Worker是强耦合的,尤其是Manager,一旦它停止工作,那么所有的Worker只会傻傻地等待Manager回归网络。如果我们存在多个Manager,那么只能很不智能地手工分配Worker(因为连接Manager需要在代码中写入Manager的地址)。所以我们需要一个类似『职介所』的东西,所有的Worker不再直接连接Manager,而是到一个『职介所』注册自己的信息。职介所负责调度Worker,一旦收到Manager请求Worker的信息,那么它会搜寻合适的Worker,并且让这些Worker连接到Manager。Worker向职介所注册自己的信息;Manager向职介所请求Worker(可以指定数量、类型);职介所根据已经注册的信息,挑选合适的Worker,向这些Worker发送Manager的地址,把这些Worker的状态设为unavaliable;被选中的Worker连接到Manager;Manager如果停止工作,那么被选中的Worker会通知职介所把自己的状态重新更新为avaliable。这样有什么好处呢?第一,Worker在这种模式下更像是一种『计算资源』,而不是『节点』,它可以被Parkserver分配到任何Manager上。第二,新的Manager可以随时加入计算网络,像Parkserver请求计算资源后,执行自己的任务。执行完任务后,可以释放自己的Worker。第三,新的Worker也可以随时加入计算网络,与以往不同的是,加入计算网络时不再需要预先设置好自己的Manager,而只需要向Parkserver注册即可。API说了这么多,下面直接看新的接口吧:现在你可以这样开启一个Parkserver:var parkserver = require('maus').\nvar myParkserver = new parkserver(8500);\n然后Worker中这样连接Parkserver:var rpcWorker = require('maus').\n\n//这里我们注册了一个类型为common的Worker\nrpcWorker.registerParkserver('http://localhost:8500', 'common', {\n
add: (x, y) =& x + y,\n
fib: fib,\n
do: (v, f) =& f(v)\n})\nManager中这样向Parkserver请求Worker:var rpcManager = require('maus').\nvar Manager = new rpcManager(8124);\n\n//连接Parkserver\nManager.connectParkserver('http://localhost:8500');\n\n//请求两个类型为common的Worker,注意要附上自己的地址\nManager.getWorker({\n
amount: 2,\n
workerType: 'common',\n
address: 'http://localhost:8124'\n}).do(workers =& {\n
//Do Something...\n});\nManager可以这样释放Worker:Manager.end();\nps:昨天看完了谷歌那三篇著名的论文(BigTable、GFS、MapReduce),涨了好多姿势,未来这段时间想自己写一个玩具级的分布式文件系统。","updated":"T13:58:12.000Z","canComment":false,"commentPermission":"anyone","commentCount":0,"likeCount":3,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T21:58:12+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"/83be33017abd2e80d29531_r.jpg","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":0,"likesCount":3},"":{"title":"使用 WebRTC 构建简单的前端视频通信","author":"starkwei","content":"在传统的 Web 应用中,浏览器与浏览器之间是无法直接相互通信的,必须借助服务器的帮助,但是随着 WebRTC 在各大浏览器中的普及,这一现状得到了改变。WebRTC(Web Real-Time Communication,Web实时通信),是一个支持网页浏览器之间进行实时数据传输(包括音频、视频、数据流)的技术,谷歌于2011年5月开放了工程的源代码,目前在各大浏览器的最新版本中都得到了不同程度的支持。这篇文章里我们采用 WebRTC 来构建一个简单的视频传输应用。一、关于 WebRTC 的一些基本概念传统的视频推流的技术实现一般是这样的:客户端采集视频数据,推流到服务器上,服务器再根据具体情况将视频数据推送到其他客户端上。但是 WebRTC 却截然不同,它可以在客户端之间直接搭建基于 UDP 的数据通道,经过简单的握手流程之后,可以在不同设备的两个浏览器内直接传输任意数据。这其中的流程包括:采集视频流数据,创建一个 RTCPeerConnection创建一个 SDP offer 和相应的回应为双方找到 ICE 候选路径成功创建一个 WebRTC 连接下面我们介绍这其中涉及到的一些关键词:1、RTCPeerConnection 对象RTCPeerConnection 对象是 WebRTC API 的入口,它负责创建、维护一个 WebRTC 连接,以及在这个连接中的数据传输。目前新版本的浏览器大都支持了这一对象,但是由于目前 API 还不稳定,所以需要加入各个浏览器内核的前缀,例如 Chrome 中我们使用 webkitRTCPeerConnection 来访问它。2、会话描述协议(SDP,Session Description Protocol)为了连接到其他用户,我们必须要对其他用户的设备情况有所了解,比如音频视频的编码解码器、使用何种编码格式、使用何种网络、设备的数据处理能力,所以我们需要一张“名片”来获得用户的所有信息,而 SDP 为我们提供了这些功能。一个 SDP 的握手由一个 offer 和一个 answer 组成。3、交互式连接建立(ICE,Interactive Connectivity Establishment)通信的两侧可能会处于不同的网络环境中,有时会存在好几层的访问控制、防火墙、路由跳转,所以我们需要一种方法在复杂的网络环境中找到对方,并且连接到相应的目标。WebRTC 使用了集成了 STUN、TURN 的 ICE 来进行双方的数据通信。二、创建一个 RTCPeerConnection首先我们的目标是在同一个页面中创建两个实时视频,一个的数据直接来自你的摄像头,另一个的数据来自本地创建的 WebRTC 连接。看起来是这样的:首先我们创建一个简单的 HTML 页面,含有两个 video 标签:&!DOCTYPE html&\n&html&\n&head&\n
&title&&/title&\n
&style type=\"text/css\"&\n
#theirs{\n
position:\n
top: 100\n
left: 100\n
width: 500\n
position:\n
top: 120\n
left: 480\n
width: 100\n
z-index: 9999;\n
border:1px solid #\n
&/style&\n&/head&\n&body&\n&video id=\"yours\" autoplay&&/video&\n&video id=\"theirs\" autoplay&&/video&\n&/body&\n&script type=\"text/javascript\" src=\"./main.js\"&&/script&\n&/html&下面我们创建一个 main.js 文件,先封装一下各浏览器的 userMedia 和 RTCPeerConnection 对象:function hasUserMedia() {\n
navigator.getUserMedia = navigator.getUserMedia || navigator.msGetUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserM\n
return !!navigator.getUserM\n}\n\nfunction hasRTCPeerConnection() {\n
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerC\n
return !!window.RTCPeerC\n}\n然后我们需要浏览器调用系统的摄像头 API `getUserMedia` 获得媒体流,注意要打开浏览器的摄像头限制。Chrome由于安全的问题,只能在 https 下或者 localhost 下打开摄像头。var yourVideo = document.getElementById(\"yours\");\nvar theirVideo = document.getElementById(\"theirs\");\nvar yourConnection, theirC\n\nif (hasUserMedia()) {\n
navigator.getUserMedia({ video: true, audio: false },\n
stream =& {\n
yourVideo.src = window.URL.createObjectURL(stream);\n
if (hasRTCPeerConnection()) {\n
// 稍后我们实现 startPeerConnection\n
startPeerConnection(stream);\n
} else {\n
alert(\"没有RTCPeerConnection API\");\n
err =& {\n
console.log(err);\n
)\n}else{\n
alert(\"没有userMedia API\")\n}没有意外的话,现在应该能在页面中看到一个视频了。下一步是实现 startPeerConnection 方法,建立传输视频数据所需要的 ICE 通信路径,这里我们以 Chrome 为例:function startPeerConnection(stream) {\n
//这里使用了几个公共的stun协议服务器\n
var config = {\n
'iceServers': [{ 'url': 'stun:stun.' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.:19302' }]\n
yourConnection = new RTCPeerConnection(config);\n
theirConnection = new RTCPeerConnection(config);\n\n
yourConnection.onicecandidate = function(e) {\n
if (e.candidate) {\n
theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate));\n
theirConnection.onicecandidate = function(e) {\n
if (e.candidate) {\n
yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate));\n
}\n}我们使用这个函数创建了两个连接对象,在 config 里,你可以任意指定 ICE 服务器,虽然有些浏览器内置了默认的 ICE 服务器,可以不用配置,但还是建议加上这些配置。下面,我们进行 SDP 的握手。由于是在同一页面中进行的通信,所以我们可以直接交换双方的 candidate 对象,但在不同页面中,可能需要一个额外的服务器协助这个交换流程。三、建立 SDP Offer 和 SDP Answer浏览器为我们封装好了相应的 Offer 和 Answer 方法,我们可以直接使用。function startPeerConnection(stream) {\n
var config = {\n
'iceServers': [{ 'url': 'stun:stun.' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.:19302' }]\n
yourConnection = new RTCPeerConnection(config);\n
theirConnection = new RTCPeerConnection(config);\n
yourConnection.onicecandidate = function(e) {\n
if (e.candidate) {\n
theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate));\n
theirConnection.onicecandidate = function(e) {\n
if (e.candidate) {\n
yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate));\n
//本方产生了一个offer\n
yourConnection.createOffer().then(offer =& {\n
yourConnection.setLocalDescription(offer);\n
//对方接收到这个offer\n
theirConnection.setRemoteDescription(offer);\n
//对方产生一个answer\n
theirConnection.createAnswer().then(answer =& {\n
theirConnection.setLocalDescription(answer);\n
//本方接收到一个answer\n
yourConnection.setRemoteDescription(answer);\n
});\n}和 ICE 的连接一样,由于我们是在同一个页面中进行 SDP 的握手,所以不需要借助任何其他的通信手段来交换 offer 和 answer,直接赋值即可。如果需要在两个不同的页面中进行交换,则需要借助一个额外的服务器来协助,可以采用 websocket 或者其它手段进行这个交换过程。四、加入视频流现在我们已经有了一个可靠的视频数据传输通道了,下一步只需要向这个通道加入数据流即可。WebRTC 直接为我们封装好了加入视频流的接口,当视频流添加时,另一方的浏览器会通过 `onaddstream` 来告知用户,通道中有视频流加入。yourConnection.addStream(stream);\ntheirConnection.onaddstream = function(e) {\n
theirVideo.src = window.URL.createObjectURL(e.stream);\n}\n以下是完整的 main.js 代码:function hasUserMedia() {\n
navigator.getUserMedia = navigator.getUserMedia || navigator.msGetUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserM\n
return !!navigator.getUserM\n}\n\nfunction hasRTCPeerConnection() {\n
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection || window.msRTCPeerC\n
return !!window.RTCPeerC\n}\n\nvar yourVideo = document.getElementById(\"yours\");\nvar theirVideo = document.getElementById(\"theirs\");\nvar yourConnection, theirC\n\nif (hasUserMedia()) {\n
navigator.getUserMedia({ video: true, audio: false },\n
stream =& {\n
yourVideo.src = window.URL.createObjectURL(stream);\n
if (hasRTCPeerConnection()) {\n
startPeerConnection(stream);\n
} else {\n
alert(\"没有RTCPeerConnection API\");\n
err =& {\n
console.log(err);\n
})\n} else {\n
alert(\"没有userMedia API\")\n}\n\n\nfunction startPeerConnection(stream) {\n
var config = {\n
'iceServers': [{ 'url': 'stun:stun.' }, { 'url': 'stun:stunserver.org' }, { 'url': 'stun:stun.:19302' }]\n
yourConnection = new RTCPeerConnection(config);\n
theirConnection = new RTCPeerConnection(config);\n\n
yourConnection.onicecandidate = function(e) {\n
if (e.candidate) {\n
theirConnection.addIceCandidate(new RTCIceCandidate(e.candidate));\n
theirConnection.onicecandidate = function(e) {\n
if (e.candidate) {\n
yourConnection.addIceCandidate(new RTCIceCandidate(e.candidate));\n
theirConnection.onaddstream = function(e) {\n
theirVideo.src = window.URL.createObjectURL(e.stream);\n
yourConnection.addStream(stream);\n\n
yourConnection.createOffer().then(offer =& {\n
yourConnection.setLocalDescription(offer);\n
theirConnection.setRemoteDescription(offer);\n
theirConnection.createAnswer().then(answer =& {\n
theirConnection.setLocalDescription(answer);\n
yourConnection.setRemoteDescription(answer);\n
});\n}\n","updated":"T13:24:47.000Z","canComment":false,"commentPermission":"anyone","commentCount":8,"likeCount":37,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T21:24:47+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"/f8dde3d409d4f9_r.png","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":8,"likesCount":37},"":{"title":"RPC框架的类型系统和错误处理","author":"starkwei","content":"这几天给自己的 RPC 框架
写完了类型系统和错误处理,可以算是0.4.0版本了吧。新特性之前的『类型传输』只是简单地调用 JSON.stringify 把返回值字符串化,传输到 manager 后,再调用 JSON.parse 将JSON还原为原来的对象,但这样存在一个明显的问题,比如:var err = new Error(); //err是一个Error类型的对象\nvar foo = JSON.stringify(err); // 将err字符串化,得到\"{}\"\nvar err2 = JSON.parse(string); // {},返回一个空对象\n这显然是不对的,同样的情况还会发生在 Date、RegExp、Infinity、NaN、Undefined这几个比较特殊的类型或变量中。所以这几天加入了一个更严谨的类型传输协议,现在的框架可以这样使用了://worker.js\nvar rpcWorker = require('maus').\nrpcWorker.create({\n
divide: (x, y) =& x / y,\n
newRegExp: (reg, config) =& new RegExp(reg, config),\n}, 'http://localhost:8124');\nvar rpcManager = require('maus').\n\nvar myManager = new rpcManager(8124);\nmyManager.do(workers =& {\n
var callback = result =& console.log(result);\n\n
workers.divide(10, 2, callback); // 返回一个Number类型,5\n
workers.divide(100, 0, callback); // 返回Infinity\n
workers.newRegExp(\"abc\", \"ig\", callback); // 返回一个正则表达式 /abc/ig\n})有了这个完善的类型系统之后,错误处理也变得很简单了:myManager.do(workers =& {\n
workers.doSomething(params,(result, err) =& {\n
console.log(result, err);\n
})\n})","updated":"T14:10:44.000Z","canComment":false,"commentPermission":"anyone","commentCount":0,"likeCount":1,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T22:10:44+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"/7c7b0b5cf188a_r.jpg","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":0,"likesCount":1},"":{"title":"JavaScript函数式编程(一)","author":"starkwei","content":"一、引言说到函数式编程,大家可能第一印象都是学院派的那些晦涩难懂的代码,充满了一大堆抽象的不知所云的符号,似乎只有大学里的计算机教授才会使用这些东西。在曾经的某个时代可能确实如此,但是近年来随着技术的发展,函数式编程已经在实际生产中发挥巨大的作用了,越来越多的语言开始加入闭包,匿名函数等非常典型的函数式编程的特性,从某种程度上来讲,函数式编程正在逐步“同化”命令式编程。JavaScript 作为一种典型的多范式编程语言,这两年随着React的火热,函数式编程的概念也开始流行起来,RxJS、cycleJS、lodashJS、underscoreJS等多种开源库都使用了函数式的特性。所以下面介绍一些函数式编程的知识和概念。二、纯函数如果你还记得一些初中的数学知识的话,函数 f 的概念就是,对于输入 x 产生一个输出 y = f(x)。这便是一种最简单的纯函数。纯函数的定义是,对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。下面来举个栗子,比如在Javascript中对于数组的操作,有些是纯的,有些就不是纯的:var arr = [1,2,3,4,5];\n\n// Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的\n// 可以,这很函数式\nxs.slice(0,3);\n//=& [1,2,3]\nxs.slice(0,3);\n//=& [1,2,3]\n\n// Array.splice是不纯的,它有副作用,对于固定的输入,输出不是固定的\n// 这不函数式\nxs.splice(0,3);\n//=& [1,2,3]\nxs.splice(0,3);\n//=& [4,5]\nxs.splice(0,3);\n//=& []在函数式编程中,我们想要的是 slice 这样的纯函数,而不是 splice这种每次调用后都会把数据弄得一团乱的函数。为什么函数式编程会排斥不纯的函数呢?下面再看一个例子://不纯的\nvar min = 18;\nvar checkage = age =& age &\n\n//纯的,这很函数式\nvar checkage = age =& age & 18;在不纯的版本中,checkage 这个函数的行为不仅取决于输入的参数 age,还取决于一个外部的变量 min,换句话说,这个函数的行为需要由外部的系统环境决定。对于大型系统来说,这种对于外部状态的依赖是造成系统复杂性大大提高的主要原因。可以注意到,纯的 checkage 把关键数字 18 硬编码在函数内部,扩展性比较差,我们可以在后面的柯里化中看到如何用优雅的函数式解决这种问题。纯函数不仅可以有效降低系统的复杂度,还有很多很棒的特性,比如可缓存性:import _ from 'lodash';\nvar sin = _.memorize(x =& Math.sin(x));\n\n//第一次计算的时候会稍慢一点\nvar a = sin(1);\n\n//第二次有了缓存,速度极快\nvar b = sin(1);三、函数的柯里化函数柯里化(curry)的定义很简单:传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。比如对于加法函数 var add = (x, y) =& x + y ,我们可以这样进行柯里化://比较容易读懂的ES5写法\nvar add = function(x){\n
return function(y){\n
return x + y\n
}\n}\n\n//ES6写法,也是比较正统的函数式写法\nvar add = x =& (y =& x + y);\n\n//试试看\nvar add2 = add(2);\nvar add200 = add(200);\n\nadd2(2); // =&4\nadd200(50); // =&250对于加法这种极其简单的函数来说,柯里化并没有什么大用处。还记得上面那个 checkage 的函数吗?我们可以这样柯里化它:var checkage = min =& (age =& age & min);\nvar checkage18 = checkage(18);\ncheckage18(20);\n// =&true事实上柯里化是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法:import { curry } from 'lodash';\n\n//首先柯里化两个纯函数\nvar match = curry((reg, str) =& str.match(reg));\nvar filter = curry((f, arr) =& arr.filter(f));\n\n//判断字符串里有没有空格\nvar haveSpace = match(/\\s+/g);\n\nhaveSpace(\"ffffffff\");\n//=&null\n\nhaveSpace(\"a b\");\n//=&[\" \"]\n\nfilter(haveSpace, [\"abcdefg\", \"Hello World\"]);\n//=&[\"Hello world\"]四、函数组合学会了使用纯函数以及如何把它柯里化之后,我们会很容易写出这样的“包菜式”代码:h(g(f(x)));虽然这也是函数式的代码,但它依然存在某种意义上的“不优雅”。为了解决函数嵌套的问题,我们需要用到“函数组合”://两个函数的组合\nvar compose = function(f, g) {\n
return function(x) {\n
return f(g(x));\n
};\n};\n\n//或者\nvar compose = (f, g) =& (x =& f(g(x)));\n\nvar add1 = x =& x + 1;\nvar mul5 = x =& x * 5;\n\ncompose(mul5, add1)(2);\n// =&15 \n我们定义的compose就像双面胶一样,可以把任何两个纯函数结合到一起。当然你也可以扩展出组合三个函数的“三面胶”,甚至“四面胶”“N面胶”。这种灵活的组合可以让我们像拼积木一样来组合函数式的代码:var first = arr =& arr[0];\nvar reverse = arr =& arr.reverse();\n\nvar last = compose(first, reverse);\n\nlast([1,2,3,4,5]);\n// =&5五、Point Free有了柯里化和函数组合的基础知识,下面介绍一下Point Free这种代码风格。细心的话你可能会注意到,之前的代码中我们总是喜欢把一些对象自带的方法转化成纯函数:var map = (f, arr) =& arr.map(f);\n\nvar toUpperCase = word =& word.toUpperCase();这种做法是有原因的。Point Free这种模式现在还暂且没有中文的翻译,有兴趣的话可以看看这里的英文解释:用中文解释的话大概就是,不要命名转瞬即逝的中间变量,比如://这不Piont free\nvar f = str =& str.toUpperCase().split(' ');这个函数中,我们使用了 str 作为我们的中间变量,但这个中间变量除了让代码变得长了一点以外是毫无意义的。下面改造一下这段代码:var toUpperCase = word =& word.toUpperCase();\nvar split = x =& (str =& str.split(x));\n\nvar f = compose(split(' '), toUpperCase);\n\nf(\"abcd efgh\");\n// =&[\"ABCD\", \"EFGH\"]这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。当然,为了在一些函数中写出Point Free的风格,在代码的其它地方必然是不那么Point Free的,这个地方需要自己取舍。六、声明式与命令式代码命令式代码的意思就是,我们通过编写一条又一条指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节。而声明式就要优雅很多了,我们通过写表达式的方式来声明我们想干什么,而不是通过一步一步的指示。//命令式\nvar CEOs = [];\nfor(var i = 0; i & companies. i++){\n
CEOs.push(companies[i].CEO)\n}\n\n//声明式\nvar CEOs = companies.map(c =& c.CEO);命令式的写法要先实例化一个数组,然后再对 companies 数组进行for循环遍历,手动命名、判断、增加计数器,就好像你开了一辆零件全部暴露在外的汽车一样,虽然很机械朋克风,但这并不是优雅的程序员应该做的。声明式的写法是一个表达式,如何进行计数器迭代,返回的数组如何收集,这些细节都隐藏了起来。它指明的是做什么,而不是怎么做。除了更加清晰和简洁之外,map 函数还可以进一步独立优化,甚至用解释器内置的速度极快的 map 函数,这么一来我们主要的业务代码就无须改动了。函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。相反,不纯的不函数式的代码会产生副作用或者依赖外部系统环境,使用它们的时候总是要考虑这些不干净的副作用。在复杂的系统中,这对于程序员的心智来说是极大的负担。七、尾声任何代码都是要有实际用处才有意义,对于JS来说也是如此。然而现实的编程世界显然不如范例中的函数式世界那么美好,实际应用中的JS是要接触到ajax、DOM操作,NodeJS环境中读写文件、网络操作这些对于外部环境强依赖,有明显副作用的“很脏”的工作。这对于函数式编程来说也是很大的挑战,所以我们也需要更强大的技术去解决这些“脏问题”。我会在下一篇文章中介绍函数式编程的更加高阶一些的知识,例如Functor、Monad等等概念。八、参考1、2、3、《JavaScript函数式编程》【美】迈克尔·佛格斯","updated":"T06:37:41.000Z","canComment":false,"commentPermission":"anyone","commentCount":35,"likeCount":386,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T14:37:41+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"/a48eedb38a417c57a3171aec1d10dd0b_r.jpg","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":35,"likesCount":386},"":{"title":"彻底解决Webpack打包性能问题","author":"starkwei","content":"这几天写腾讯实习生 Mini 项目的时候用上了 react 全家桶,当然同时引入了 Webpack 作为打包工具。但是开发过程中遇到一个很棘手的问题就是,react 加上 react-router、material-ui、superagent、eventproxy 这些第三方轮子一共有好几百个 module,Webpack 的打包速度极慢。这对于开发是非常不好的体验,同时效率也极低。问题分析我们先来看一下完全没有任何优化的时候,Webpack 的打包速度(使用了jsx和babel的loader)。下面是我们的测试文件://test.js\nvar react = require('react');\nvar ReactAddonsCssTransitionGroup = require('react-addons-css-transition-group');\nvar reactDOM = require('react-dom');\nvar reactRouter = require('react-router');\nvar superagent = require(\"superagent\");\nvar eventproxy = require(\"eventproxy\");运行webpack test.js在我的2015款RMBP13,i5处理器,全SSD下,性能是这样的:没错你没有看错,这几个第三方轮子加起来有整整668个模块,全部打包需要20多秒。这意味着什么呢?你每次对业务代码的修改,gulp 或者 Webpack 监测到后都会重新打包,你要足足等20秒才能看到自己的修改结果。但是需要重新打包的只有你的业务代码,这些第三方库是完全不用重新打包的,它们的存在只会拖累打包性能。所以我们要找一些方法来优化这个过程。配置externalsWebpack 可以配置 externals 来将依赖的库指向全局变量,从而不再打包这个库,比如对于这样一个文件:import React from 'react';\nconsole.log(React);如果你在 Webpack.config.js 中配置了externals:module.exports = {\n
externals: {\n
'react': 'window.React'\n
//其它配置忽略...... \n};等于让 Webpack 知道,对于 react 这个模块就不要打包啦,直接指向 window.React 就好。不过别忘了加载 react.min.js,让全局中有 React 这个变量。我们来看看性能,因为不用打包 React 了所以速度自然很快,包也很小:配置externals的缺陷问题如果就这么简单地解决了的话,那我就没必要写这篇文章了,下面我们加一个 react 的动画库 react-addons-css-transition-group 来试一试:import React from 'react';\nimport ReactAddonsCssTransitionGroup from 'react-addons-css-transition-group';\nconsole.log(React);对,你没有看错,我也没有截错图,新加了一个很小很小的动画库之后,性能又爆炸了。从模块数来看,一定是 Webpack 又把 react 重新打包了一遍。我们来看一下为什么一个很小很小的动画库会导致 Webpack 又傻傻地把 react 重新打包了一遍。找到 react-addons-css-transition-group 这个模块,然后看看它是怎么写的:// react-addons-css-transition-group模块\n// 入口文件 index.js\nmodule.exports = require('react/lib/ReactCSSTransitionGroup');这个动画模块就只有一行代码,唯一的作用就是指向 react 下面的一个子模块,我们再来看看这个子模块是怎么写的:// react模块\n// react/lib/ReactCSSTransitionGroup.js\nvar React = require('./React');\nvar ReactTransitionGroup = require('./ReactTransitionGroup');\nvar ReactCSSTransitionGroupChild = require('./ReactCSSTransitionGroupChild');\n//....剩余代码忽略这个子模块又反回去依赖了 react 整个库的入口,这就是拖累 Webpack 的罪魁祸首。总而言之,问题是这样产生的:Webpack 发现我们依赖了 react-addons-css-transition-group; Webpack 去打包 react-addons-css-transition-group 的时候发现它依赖了 react 模块下的一个叫 ReactTransitionGroup.js 的文件,于是 Webpack 去打包这个文件;ReactTransitionGroup.js 依赖了整个 react 的入口文件 React.js,虽然我们设置了 externals ,但是 Webpack 不知道这个入口文件等效于 react 模块本身,于是我们可爱又敬业的 Webpack 就把整个 react 又重新打包了一遍。读到这里你可能会有疑问,为什么不能把这个动画库也设置到 externals 里,这样不是就不用打包了吗?问题就在于,这个动画库并没有提供生产环境的文件,或者说这个库根本没有提供 react-addons-css-transition-group.min.js 这个文件。这个问题不只存在于 react-addons-css-transition-group 中,对于 react 的大多数现有库来说都有这个依赖关系复杂的问题。初级解决方法所以对于这个问题的解决方法就是,手工打包这些 module,然后设置 externals ,让 Webpack 不再打包它们。我们需要这样一个 lib-bundle.js 文件:window.__LIB[\"react\"] = require(\"react\");\nwindow.__LIB[\"react-addons-css-transition-group\"] = require(\"react-addons-css-transition-group\");\n// ...其它依赖包我们在这里把一些第三方库注册到了 window.__LIB 下,这些库可以作为底层的基础库,免于重复打包。然后执行 webpack lib-bundle.js lib.js得到打包好的 lib.js。然后去设置我们的 externals :var webpack = require('webpack');\nmodule.exports = {\nexternals: {\n
'react': 'window.__LIB[\"react\"]',\n
'react-addons-css-transition-group': 'window.__LIB[\"react-addons-css-transition-group\"]',\n
// 其它库\n
//其它配置忽略...... \n};这时由于 externals 的存在,Webpack 打包的时候就会避开这些模块超多,依赖关系复杂的库,把这些第三方 module 的入口指向预先打包好的 lib.js 的入口 window.__LIB,从而只打包我们的业务代码。终极解决方法上面我们提到的方法本质上就是一种“动态链接库(dll)”的思想,这在 windows 系统下面是一种很常见的思想。一个 dll 包,就是一个很纯净的依赖库,它本身不能运行,是用来给你的 app 或者业务代码引用的。同样的 Webpack 最近也新加入了这个功能:webpack.DllPlugin。使用这个功能需要把打包过程分成两步: 打包ddl包 引用ddl包,打包业务代码首先我们来打包ddl包,首先配置一个这样的 ddl.config.js:const webpack = require('webpack');\n\nconst vendors = [\n
'react',\n
'react-dom',\n
'react-router',\n
// ...其它库\n];\n\nmodule.exports = {\n
output: {\n
path: 'build',\n
filename: '[name].js',\n
library: '[name]',\n
entry: {\n
\"lib\": vendors,\n
plugins: [\n
new webpack.DllPlugin({\n
path: 'manifest.json',\n
name: '[name]',\n
context: __dirname,\n
],\n};webpack.DllPlugin 的选项中:path 是 manifest.json 文件的输出路径,这个文件会用于后续的业务代码打包;name 是 dll 暴露的对象名,要跟 output.library 保持一致;context 是解析包路径的上下文,这个要跟接下来配置的 webpack.config.js 一致。运行Webpack,会输出两个文件一个是打包好的 lib.js,一个就是 manifest.json,它里面的内容大概是这样的:{\n
\"name\": \"vendor_ac51ba426d4f259b8b18\",\n
\"content\": {\n
\"./node_modules/react/react.js\": 1,\n
\"./node_modules/react/lib/React.js\": 2,\n
\"./node_modules/react/node_modules/object-assign/index.js\": 3,\n
\"./node_modules/react/lib/ReactChildren.js\": 4,\n
\"./node_modules/react/lib/PooledClass.js\": 5,\n
\"./node_modules/react/lib/reactProdInvariant.js\": 6,\n
// ............\n
}\n}接下来我们就可以快乐地打包业务代码啦,首先写好打包配置文件 webpack.config.js:const webpack = require('webpack');\nmodule.exports = {\n
output: {\n
path: 'build',\n
filename: '[name].js',\n
entry: {\n
app: './src/index.js',\n
plugins: [\n
new webpack.DllReferencePlugin({\n
context: __dirname,\n
manifest: require('./manifest.json'),\n
],\n};webpack.DllReferencePlugin 的选项中:context 需要跟之前保持一致,这个用来指导 Webpack 匹配 manifest.json 中库的路径;manifest 用来引入刚才输出的 manifest.json 文件。DllPlugin 本质上的做法和我们手动分离这些第三方库是一样的,但是对于包极多的应用来说,自动化明显加快了生产效率。","updated":"T06:51:58.000Z","canComment":false,"commentPermission":"anyone","commentCount":59,"likeCount":334,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T14:51:58+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"/897c2df2f21aa6b2f9f4a_r.png","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":59,"likesCount":334},"":{"title":"得瑟一下","author":"starkwei","content":"哈哈哈哈Mini项目拿金奖啦拿金奖咯~一个多星期前其他组在组队的时候都不怎么欢迎前端,侧重做APP,前端最多弄个宣传页H5啥的。然而他们不知道的是,javascript这种真·全平台的语言极其适合这种这种7天的快速开发。我要是PM一定组12个前端,6个用node写服务器,6个用react写用户端(还可以顺便把react-native做了)答辩的时候评委里碰巧有个前端T3,她也是难得看到一个侧重前端技术的组(不知道是不是唯一一个)。我们用react、react-router做的SPA当然有很多地方可以和她谈笑风生啦,从技术选型说到react的组件状态管理再说到react服务器端直出渲染,顺便说了说前几天发的那篇关于优化Webpack打包速度的文章。看她最后的表情我能确定起码这个评委被搞定了(^?^)ノ当然队友们也很给力,服务器端架构简直让评委无黑点,安卓同学一个人通了几天宵单挑APP,PM的用户调研、竞品分析也是所有组最棒的,测试同学做的压力测试和安卓帧率测试的数据也非常详尽。哈哈总之就是感谢腾讯,感谢SNG,也感谢这群靠谱的队友~","updated":"T13:03:22.000Z","canComment":false,"commentPermission":"anyone","commentCount":8,"likeCount":30,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","sourceUrl":"","publishedTime":"T21:03:22+08:00","links":{"comments":"/api/posts//comments"},"url":"/p/","titleImage":"","summary":"","href":"/api/posts/","meta":{"previous":null,"next":null},"snapshotUrl":"","commentsCount":8,"likesCount":30},"":{"title":"用 JavaScript 写一个超小型编译器","author":"starkwei","content":"前几天看到 Github 上一个非常好的编译器 Demo:虽然是一个很小很小的并没有什么卵用的编译器,但可以向我们展示编译器的很多东西。昨天和今天有空,,如果可以的话,建议直接去看代码,Github上的阅读体验更好:当然也可以看下面,不过知乎编辑器对于代码的支持真是蛋疼。。。用客户端APP的同学请使用『浏览器打开』。/**\n * 今天让我们来写一个编译器,一个超级无敌小的编译器!它小到如果把所有注释删去的话,大概只剩\n * 200行左右的代码。\n * \n * 我们将会用它将 lisp 风格的函数调用转换为 C 风格。\n *\n * 如果你对这两种风格不是很熟悉,下面是一个简单的介绍。\n *\n * 假设我们有两个函数,`add` 和 `subtract`,那么它们的写法将会是下面这样:\n * \n *
add(2, 2)\n *
(subtract 4 2)
subtract(4, 2)\n *
2 + (4 - 2)
(add 2 (subtract 4 2))
add(2, subtract(4, 2))\n *\n * 很简单对吧?\n *\n * 这个转换就是我们将要做的事情。虽然这并不包含 LISP 或者 C 的全部语法,但它足以向我们\n * 展示现代编译器很多要点。\n * \n */\n\n/**\n * 大多数编译器可以分成三个阶段:解析(Parsing),转换(Transformation)以及代码\n * 生成(Code Generation)\n *\n * 1. *解析*是将最初原始的代码转换为一种更加抽象的表示(译者注:即AST)。*\n *\n * 2. *转换*将对这个抽象的表示做一些处理,让它能做到编译器期望\n *
它做到的事情。\n *\n * 3. *代码生成*接收处理之后的代码表示,然后把它转换成新的代码。\n */\n\n/**\n * 解析(Parsing)\n * -------\n *\n * 解析一般来说会分成两个阶段:词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)。\n *\n * 1. *词法分析*接收原始代码,然后把它分割成一些被称为 Token 的东西,这个过程是在词法分析\n *
器(Tokenizer或者Lexer)中完成的。\n *\n *
Token 是一个数组,由一些代码语句的碎片组成。它们可以是数字、标签、标点符号、运算符,\n *
或者其它任何东西。\n *\n * 2. *语法分析* 接收之前生成的 Token,把它们转换成一种抽象的表示,这种抽象的表示描述了代\n *
码语句中的每一个片段以及它们之间的关系。这被称为中间表示(intermediate representation)\n *
或抽象语法树(Abstract Syntax Tree, 缩写为AST)\n *\n *
抽象语法树是一个嵌套程度很深的对象,用一种更容易处理的方式代表了代码本身,也能给我们\n *
更多信息。\n *\n * 比如说对于下面这一行代码语句:\n *\n *
(add 2 (subtract 4 2))\n *\n * 它产生的 Token 看起来或许是这样的:\n *\n *
{ type: 'paren',
value: '('
{ type: 'name',
value: 'add'
{ type: 'number', value: '2'
{ type: 'paren',
value: '('
{ type: 'name',
value: 'subtract' },\n *
{ type: 'number', value: '4'
{ type: 'number', value: '2'
{ type: 'paren',
value: ')'
{ type: 'paren',
value: ')'
]\n *\n * 它的抽象语法树(AST)看起来或许是这样的:\n *\n *
type: 'Program',\n *
body: [{\n *
type: 'CallExpression',\n *
name: 'add',\n *
params: [{\n *
type: 'NumberLiteral',\n *
value: '2'\n *
type: 'CallExpression',\n *
name: 'subtract',\n *
params: [{\n *
type: 'NumberLiteral',\n *
value: '4'\n *
type: 'NumberLiteral',\n *
value: '2'\n *
}\n */\n\n/**\n * 转换(Transformation)\n * --------------\n *\n * 编译器的下一步就是转换。它只是把 AST 拿过来然后对它做一些修改。它可以在同种语言下操\n * 作 AST,也可以把 AST 翻译成全新的语言。\n *\n * 下面我们来看看该如何转换 AST。\n *\n * 你或许注意到了我们的 AST 中有很多相似的元素,这些元素都有 type 属性,它们被称为 AST\n * 结点。这些结点含有若干属性,可以用于描述 AST 的部分信息。\n *\n * 比如下面是一个“NumberLiteral”结点:\n *\n *
type: 'NumberLiteral',\n *
value: '2'\n *
}\n *\n * 又比如下面是一个“CallExpression”结点:\n *\n *
type: 'CallExpression',\n *
name: 'subtract',\n *
params: [...nested nodes go here...]\n *
}\n *\n * 当转换 AST 的时候我们可以添加、移动、替代这些结点,也可以根据现有的 AST 生成一个全新\n * 的 AST\n *\n * 既然我们编译器的目标是把输入的代码转换为一种新的语言,所以我们将会着重于产生一个针对\n * 新语言的全新的 AST。\n * \n *\n * 遍历(Traversal)\n * ---------\n *\n * 为了能处理所有的结点,我们需要遍历它们,使用的是深度优先遍历。\n *\n *
type: 'Program',\n *
body: [{\n *
type: 'CallExpression',\n *
name: 'add',\n *
params: [{\n *
type: 'NumberLiteral',\n *
value: '2'\n *
type: 'CallExpression',\n *
name: 'subtract',\n *
params: [{\n *
type: 'NumberLiteral',\n *
value: '4'\n *
type: 'NumberLiteral',\n *
value: '2'\n *
}\n *\n * So for the above AST we would go:\n * 对于上面的 AST 的遍历流程是这样的:\n *\n *
1. Program - 从 AST 的顶部结点开始\n *
2. CallExpression (add) - Program 的第一个子元素\n *
3. NumberLiteral (2) - CallExpression (add) 的第一个子元素\n *
4. CallExpression (subtract) - CallExpression (add) 的第二个子元素\n *
5. NumberLiteral (4) - CallExpression (subtract) 的第一个子元素\n *
6. NumberLiteral (4) - CallExpression (subtract) 的第二个子元素\n *\n * 如果我们直接在 AST 内部操作,而不是产生一个新的 AST,那么就要在这里介绍所有种类的抽象,\n * 但是目前访问(visiting)所有结点的方法已经足够了。\n *\n * 使用“访问(visiting)”这个词的是因为这是一种模式,代表在对象结构内对元素进行操作。\n *\n * 访问者(Visitors)\n * --------\n *\n * 我们最基础的想法是创建一个“访问者(visitor)”对象,这个对象中包含一些方法,可以接收不\n * 同的结点。\n *\n *
var visitor = {\n *
NumberLiteral() {},\n *
CallExpression() {}\n *
};\n *\n * 当我们遍历 AST 的时候,如果遇到了匹配 type 的结点,我们可以调用 visitor 中的方法。\n *\n * 一般情况下为了让这些方法可用性更好,我们会把父结点也作为参数传入。\n */\n\n/**\n * 代码生成(Code Generation)\n * ---------------\n *\n * 编译器的最后一个阶段是代码生成,这个阶段做的事情有时候会和转换(transformation)重叠,\n * 但是代码生成最主要的部分还是根据 AST 来输出代码。\n *\n * 代码生成有几种不同的工作方式,有些编译器将会重用之前生成的 token,有些会创建独立的代码\n * 表示,以便于线性地输出代码。但是接下来我们还是着重于使用之前生成好的 AST。\n *\n * 我们的代码生成器需要知道如何“打印”AST 中所有类型的结点,然后它会递归地调用自身,直到所\n * 有代码都被打印到一个很长的字符串中。\n * \n */\n\n/**\n * 好了!这就是编译器中所有的部分了。\n *\n * 当然不是说所有的编译器都像我说的这样。不同的编译器有不同的目的,所以也可能需要不同的步骤。\n *\n * 但你现在应该对编译器到底是个什么东西有个大概的认识了。\n *\n * 既然我全都解释一遍了,你应该能写一个属于自己的编译器了吧?\n *\n * 哈哈开个玩笑,接下来才是重点 :P\n *\n * 所以我们开始吧...\n */\n\n/**\n * =======================================================================\n *
(/^▽^)/\n *
词法分析器(Tokenizer)!\n * =======================================================================\n */\n\n/**\n * 我们从第一个阶段开始,即词法分析,使用的是词法分析器(Tokenizer)。\n *\n * 我们只是接收代码组成的字符串,然后把它们分割成 token 组成的数组。\n *\n *
(add 2 (subtract 4 2))
[{ type: 'paren', value: '(' }, ...]\n */\n\n// 我们从接收一个字符串开始,首先设置两个变量。\nfunction tokenizer(input) {\n\n
// `current`变量类似指针,用于记录我们在代码字符串中的位置。\n
var current = 0;\n\n
// `tokens`数组是我们放置 token 的地方\n
var tokens = [];\n\n
// 首先我们创建一个 `while` 循环, `current` 变量会在循环中自增。\n
// 我们这么做的原因是,由于 token 数组的长度是任意的,所以可能要在单个循环中多次\n
// 增加 `current` \n
while (current & input.length) {\n\n
// 我们在这里储存了 `input` 中的当前字符\n
var char = input[current];\n\n
// 要做的第一件事情就是检查是不是右圆括号。这在之后将会用在 `CallExpressions` 中,\n
// 但是现在我们关心的只是字符本身。\n
// 检查一下是不是一个左圆括号。\n
if (char === '(') {\n\n
// 如果是,那么我们 push 一个 type 为 `paren`,value 为左圆括号的对象。\n
tokens.push({\n
type: 'paren',\n
value: '('\n
// 自增 `current`\n
current++;\n\n
// 结束本次循环,进入下一次循环\\n
// 然后我们检查是不是一个右圆括号。这里做的时候和之前一样:检查右圆括号、加入新的 token、\n
// 自增 `current`,然后进入下一次循环。\n
if (char === ')') {\n
tokens.push({\n
type: 'paren',\n
value: ')'\n
current++;\\n
// 继续,我们现在检查是不是空格。有趣的是,我们想要空格的本意是分隔字符,但这现在\n
// 对于我们储存 token 来说不那么重要。我们暂且搁置它。\n
// 所以我们只是简单地检查是不是空格,如果是,那么我们直接进入下一个循环。\n
var WHITESPACE = /\\s/;\n
if (WHITESPACE.test(char)) {\n
current++;\\n
// 下一个 token 的类型是数字。它和之前的 token 不同,因为数字可以由多个数字字符组成,\n
// 但是我们只能把它们识别为一个 token。\n
(add 123 456)\n
Only two separate tokens\n
这里只有两个 token\n
// 当我们遇到一个数字字符时,将会从这里开始。\n
var NUMBERS = /[0-9]/;\n
if (NUMBERS.test(char)) {\n\n
// 创建一个 `value` 字符串,用于 push 字符。\n
var value = '';\n\n
// 然后我们循环遍历接下来的字符,直到我们遇到的字符不再是数字字符为止,把遇到的每\n
// 一个数字字符 push 进 `value` 中,然后自增 `current`。\n
while (NUMBERS.test(char)) {\n
value +=\n
char = input[++current];\n
// 然后我们把类型为 `number` 的 token 放入 `tokens` 数组中。\n
tokens.push({\n
type: 'number',\n
value: value\n
// 进入下一次循环。\\n
// 最后一种类型的 token 是 `name`。它由一系列的字母组成,这在我们的 lisp 语法中\n
// 代表了函数。\n
(add 2 4)\n
Name token\n
var LETTERS = /[a-z]/i;\n
if (LETTERS.test(char)) {\n
var value = '';\n\n
// 同样,我们用一个循环遍历所有的字母,把它们存入 value 中。\n
while (LETTERS.test(char)) {\n
value +=\n
char = input[++current];\n
// 然后添加一个类型为 `name` 的 token,然后进入下一次循环。\n
tokens.push({\n
type: 'name',\n
value: value\n
// 最后如果我们没有匹配上任何类型的 token,那么我们抛出一个错误。\n
throw new TypeError('I dont know what this character is: ' + char);\n
// 词法分析器的最后我们返回 tokens 数组。\\n}\n\n/**\n * =======================================================================\n *
? o\\?\n *
语法分析器(Parser)!!!\n * =======================================================================\n */\n\n/**\n *
语法分析器接受 token 数组,然后把它转化为 AST\n *\n *
[{ type: 'paren', value: '(' }, ...]
{ type: 'Program', body: [...] }\n */\n\n// 现在我们定义 parser 函数,接受 `tokens` 数组\nfunction parser(tokens) {\n\n
// 我们再次声明一个 `current` 变量作为指针。\n
var current = 0;\n\n
// 但是这次我们使用递归而不是 `while` 循环,所以我们定义一个 `walk` 函数。\n
function walk() {\n\n
// walk函数里,我们从当前token开始\n
var token = tokens[current];\n\n
// 对于不同类型的结点,对应的处理方法也不同,我们从 `number` 类型的 token 开始。\n
// 检查是不是 `number` 类型\n
if (token.type === 'number') {\n\n
// 如果是,`current` 自增。\n
current++;\n\n
// 然后我们会返回一个新的 AST 结点 `NumberLiteral`,并且把它的值设为 token 的值。\n
return {\n
type: 'NumberLiteral',\n
value: token.value\n
// 接下来我们检查是不是 CallExpressions 类型,我们从左圆括号开始。\n
token.type === 'paren' &&\n
token.value === '('\n
// 我们会自增 `current` 来跳过这个括号,因为括号在 AST 中是不重要的。\n
token = tokens[++current];\n\n
// 我们创建一个类型为 `CallExpression` 的根节点,然后把它的 name 属性设置为当前\n
// token 的值,因为紧跟在左圆括号后面的 token 一定是调用的函数的名字。 \n
var node = {\n
type: 'CallExpression',\n
name: token.value,\n
params: []\n
// 我们再次自增 `current` 变量,跳过当前的 token \n
token = tokens[++current];\n\n
// 现在我们循环遍历接下来的每一个 token,直到我们遇到右圆括号,这些 token 将会\n
// 是 `CallExpression` 的 `params`(参数)\n
// 这也是递归开始的地方,我们采用递归的方式来解决问题,而不是去尝试解析一个可能有无限\n
// 层嵌套的结点。\n
// 为了更好地解释,我们来看看我们的 Lisp 代码。你会注意到 `add` 函数的参数有两个,\n
// 一个是数字,另一个是一个嵌套的 `CallExpression`,这个 `CallExpression` 中\n
// 包含了它自己的参数(两个数字)\n
(add 2 (subtract 4 2))\n
// 你也会注意到我们的 token 数组中有多个右圆括号。\n
{ type: 'paren',
value: '('
{ type: 'name',
value: 'add'
{ type: 'number', value: '2'
{ type: 'paren',
value: '('
{ type: 'name',
value: 'subtract' },\n
{ type: 'number', value: '4'
{ type: 'number', value: '2'
{ type: 'paren',
value: ')'
}, &&& 右圆括号\n
{ type: 'paren',
value: ')'
&&& 右圆括号\n
// 遇到嵌套的 `CallExpressions` 时,我们将会依赖嵌套的 `walk` 函数来\n
// 增加 `current` 变量\n
// 所以我们创建一个 `while` 循环,直到遇到类型为 `'paren'`,值为右圆括号的 token。 \n
(token.type !== 'paren') ||\n
(token.type === 'paren' && token.value !== ')')\n
// 我们调用 `walk` 函数,它将会返回一个结点,然后我们把这个节点\n
// 放入 `node.params` 中。\n
node.params.push(walk());\n
token = tokens[current];\n
// 我们最后一次增加 `current`,跳过右圆括号。\n
current++;\n\n
// 返回结点。\\n
// 同样,如果我们遇到了一个类型未知的结点,就抛出一个错误。\n
throw new TypeError(token.type);\n
// 现在,我们创建 AST,根结点是一个类型为 `Program` 的结点。\n
var ast = {\n
type: 'Program',\n
body: []\n
// 现在我们开始 `walk` 函数,把结点放入 `ast.body` 中。\n
// 之所以在一个循环中处理,是因为我们的程序可能在 `CallExpressions` 后面包含连续的两个\n
// 参数,而不是嵌套的。\n
(add 2 2)\n
(subtract 4 2)\n
while (current & tokens.length) {\n
ast.body.push(walk());\n
// 最后我们的语法分析器返回 AST \\n}\n\n/**\n * =======================================================================\n *
⌒(?&???&?)⌒\n *
遍历器!!!\n * =======================================================================\n */\n\n/**\n * 现在我们有了 AST,我们需要一个 visitor 去遍历所有的结点。当遇到某个类型的结点时,我们\n * 需要调用 visitor 中对应类型的处理函数。\n *\n *
traverse(ast, {\n *
Program(node, parent) {\n *
// ...\n *
},\n *\n *
CallExpression(node, parent) {\n *
// ...\n *
},\n *\n *
NumberLiteral(node, parent) {\n *
// ...\n *
});\n */\n\n// 所以我们定义一个遍历器,它有两个参数,AST 和 vistor。在它的里面我们又定义了两个函数...\nfunction traverser(ast, visitor) {\n\n
// `traverseArray` 函数允许我们对数组中的每一个元素调用 `traverseNode` 函数。\n
function traverseArray(array, parent) {\n
array.forEach(function(child) {\n
traverseNode(child, parent);\n
// `traverseNode` 函数接受一个 `node` 和它的父结点 `parent` 作为参数,这个结点会被\n
// 传入到 visitor 中相应的处理函数那里。\n
function traverseNode(node, parent) {\n\n
// 首先我们看看 visitor 中有没有对应 `type` 的处理函数。\n
var method = visitor[node.type];\n\n
// 如果有,那么我们把 `node` 和 `parent` 都传入其中。\n
if (method) {\n
method(node, parent);\n
// 下面我们对每一个不同类型的结点分开处理。\n
switch (node.type) {\n\n
// 我们从顶层的 `Program` 开始,Program 结点中有一个 body 属性,它是一个由若干\n
// 个结点组成的数组,所以我们对这个数组调用 `traverseArray`。\n
// (记住 `traverseArray` 会调用 `traverseNode`,所以我们会递归地遍历这棵树。)\n
case 'Program':\n
traverseArray(node.body, node);\\n\n
// 下面我们对 `CallExpressions` 做同样的事情,遍历它的 `params`。\n
case 'CallExpression':\n
traverseArray(node.params, node);\\n\n
// 如果是 `NumberLiterals`,那么就没有任何子结点了,所以我们直接 break\n
case 'NumberLiteral':\\n\n
// 同样,如果我们不能识别当前的结点,那么就抛出一个错误。\n
default:\n
throw new TypeError(node.type);\n
// 最后我们对 AST 调用 `traverseNode`,开始遍历。注意 AST 并没有父结点。\n
traverseNode(ast, null);\n}\n\n/**\n * =======================================================================\n *
?(??????????)?\n *
转换器!!!\n * =======================================================================\n */\n\n/**\n * 下面是转换器。转换器接收我们在之前构建好的 AST,然后把它和 visitor 传递进入我们的遍历\n * 器中 ,最后得到一个新的 AST。\n *\n * ----------------------------------------------------------------------------\n *
原始的 AST
转换后的 AST\n * ----------------------------------------------------------------------------\n *
type: 'Program',
type: 'Program',\n *
body: [{\n *
type: 'CallExpression',
type: 'ExpressionStatement',\n *
name: 'add',
expression: {\n *
params: [{
type: 'CallExpression',\n *
type: 'NumberLiteral',
callee: {\n *
value: '2'
type: 'Identifier',\n *
name: 'add'\n *
type: 'CallExpression',
name: 'subtract',
arguments: [{\n *
params: [{
type: 'NumberLiteral',\n *
type: 'NumberLiteral',
value: '2'\n *
value: '4'
type: 'CallExpression',\n *
type: 'NumberLiteral',
callee: {\n *
value: '2'
type: 'Identifier',\n *
name: 'subtract'\n *
argume}

我要回帖

更多关于 vue 改变计算属性 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信