GithubHelp home page GithubHelp logo

notes's People

Contributors

xiyuyizhi avatar

Stargazers

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

Watchers

 avatar  avatar  avatar  avatar  avatar

notes's Issues

babel小记

对babel一直没具体总结过,趁周末看了下文档,记录一下

babel作为一个compiler,主要用在转换新的es标准实现来使所有浏览器都支持,这包含两方面

  1. 新的es标准语法,箭头函数、扩展运算符、块级作用域等

  2. 转化新的es标准方法或正被提议还未纳入标准的方法,,Array.from、Map、Promise、String.includes等

babel编译过程

babel的编译过程分为三个阶段,解析、转换、生成浏览器支持的代码。官网推荐了一个the-super-tiny-compiler,描述了类似babel这样的compiler大体是如何工作的。

  • 解析 解析源代码,构造抽象语法树

  • 转换 使用各种plugin处理AST,转换成一个新的AST

  • 生成代码 根据新的AST,生成代码字符串

具体细节参见the-super-tiny-compiler

处理新的语法

对es新增语法的处理是借助babel的各种plugin,各种plugin作用在babel编译的第二个阶段,转化阶段

presets

presets可以看做是一部分plugin的集合,目前官方提供的presets有env、react、flow

在babel还不支持env之前,我们一般在.babelrc中指定

{
    presets:['es2015','es2016','stage-2']
}

像es2015表示babel会使用如下这些plugin处理我们代码中使用的新语法

babel

现在我们可以这样写

{
    presets:['env']
}

等价于babel-preset-latest,可以转换已经在标准中的es6,es7等的新语法,需要注意的是env并不会处理被提议的stage-x中的新语法,要使用那些语法要自己在presets中执行stage-x

并且只是指定env,而不指定相关的targets信息的话,babel只会转换新语法,对新方法不会做处理

处理新的方法

babel-polyfill

为了支持es新增api的转化,我们可以使用babel-polyfill,这个库内部使用core-js(那个作者打广告说正在找工作的库)和regenerator来模拟实现新增api.

使用polyfill的缺点

  1. polyfill需要首先被引入,在文件首部或者webpack中entry: ["babel-polyfill", "./app/js"],整个文件会和我们src下的代码打包在一起,增大文件大小

  2. polyfill会在js内置对象的原型上增加方法,例如String.prototpe.includes,污染全局作用域

一个减小使用polyfill后打包代码过大问题的方法 useBuiltIns=true

useBuiltIns默认不开启,开启后,我们import "babel-polyfill"会根据当前targets指定的环境引入必须的文件

import "babel-polyfill";

输出:根据环境

import "core-js/modules/es7.string.pad-start";
import "core-js/modules/es7.string.pad-end";
import "core-js/modules/web.timers";
import "core-js/modules/web.immediate";
import "core-js/modules/web.dom.iterable";

一定程度上减小打包文件大小

transform-runtime

使用babel-polyfill会有使打包文件过大和污染全局作用域的问题,所以babel提供了babel-plugin-transform-runtime来解决一些问题

  1. 优化帮助函数引用

babel内部提供了很多帮助函数来处理语法转化的需要,transform-runtime会把对帮助函数的调用替换为对模块的引用

class Person {
}

输出:

"use strict";

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Person = function Person() {
  _classCallCheck(this, Person);
};

_classCallCheck是一个帮助函数,如果我们多个js文件中都有定义class类,_classCallCheck就会在多个文件中都存在,造成帮助函数重复,增大打包文件大小。而transform-runtime会将帮助函数以引用的方式调用(引用babel-runtime/helpers/xxx下面的),避免重复

  1. 对于新增api的转化,transform-runtime使用babel-runtime/core-js下对应的同名方法,而不需要引用babel-polyfill,只会需要哪个,就require哪个core-js下对应的实现.避免污染全局作用域

transform-runtime的缺点

因为不会在js原生对象原型上添加方法,所以transform-runtime不会转化新增的实例方法,例如不能处理"foobar".includes("foo")

对于新增api如何处理

如果项目中使用了大量新增api,并使用大量新增的实例方法,应该使用polyfill,为了一定程度上减小打包文件的体积,应该启用useBuiltIns=true,并指定代码的最低运行环境,尽量减少不必要的polyfill,同时加入transform-runtime,设置polyfill=false

{
    "presets":[
        ['env',{
            "targets":{
                "browsers": [
                    "last 2 versions",//各个浏览器的最新两个版本
                    "safari >= 7"
                ]
            },
            "debug": true,
            "useBuiltIns": true
        }]
    ],
    "plugins":[
        ["transform-runtime", { //不处理新的方法,只处理帮助函数
            "helpers": true,
            "polyfill": false,
            "regenerator": false,
            "moduleName": "babel-runtime"
        }]
    ]
}

如果项目并不使用新增实例方法(很少这样的情况),并不想污染全局作用域,应该使用transform-runtime

{
    "presets":['env'],//处理新的语法,但新的方法由transform-runtime插件处理
    "plugins":[
        ["transform-runtime", {
            "helpers": true,
            "polyfill": true,
            "regenerator": true,
            "moduleName": "babel-runtime"
        }]
    ]
}

正则 理解这些点就够了

不是完全的正则手册,只记录一些重要的,容易有误解的点

定义

正则表达式通过字面量形式或RegExp构造函数形式定义

const pattern=/\d/g
//或
const pattern = new RegExp('\d','g')

一般使用字面量形式,构造函数形式用在正则表达式在运行时才能确定下的情况,例如

function hasClass(ele, classname) {
    const pattern = new RegExp('(^|\\s)' + classname + '(\\s|$)')
    return pattern.test(ele.className)
}

另一方面:字符串中反斜杠有别的含义,要想表示\d等要使用两个反斜杠来转义\\d*

反斜杠

在正则表达式中反斜杠有重要的含义

  1. 是用来转义有特殊含义的字符,比如 [、^、.
要想只匹配.com 需要 /\.com/.test('.com')
  1. 预定的字符类以\开始,比如 \d \w \s

而在字符串中反斜杠同样是一个转义字符,\n \r \t

要想在字符串中表示\需要两个 \

new RegExp("[\\w\\.]").toString()=='/[\w\.]/'

()、[]与|

[]:集合操作符,表示一系列字符的任意一个

例如:/[abc]/ 表示a、b、c中的任意一个能匹配就可以了

对于/[a|b]/呢?

一个常见的误区是感觉/[a|b]/表示要匹配a或者b,其实是a、b或者|中的任意一个

/[a|b]/.test('|') === true

/(a|b)/.test('|') ===false

从上面可以看到,圆括号中的|是或的意思,表示要匹配()以|分割的两边的整体,注意是整体

例子:

/(abc|abd)/.test('ab') ===false
/(abc|abd)/.test('abc') ===true
/(abc|abd)/.test('abd') ===true

分组和捕获

上面只是介绍了圆括号中存在|时需注意的点,这里重点说一下圆括号

在正则中,圆括号有两种含义,一是用来分组,一是用来捕获想要的值

  1. 分组

()结合* ? + {} 使用时,是对圆括号内的整体进行repeat

/(ab)+/ 匹配一个或多个ab

/(ab)+|(cd)+/ 匹配一个或多个 ab或cd
  1. 捕获

捕获是一个强大的功能,也是很多时候我们使用正则的原因,同样以()来表示

例子:找出样式中的透明度值

<div id="opacity" style="opacity:0.5;filter:alpha(opacity=50);">

function getOpacity(elem) {
    var filter = elem.style.filter;
    if(filter){
        return filter.indexOf("opacity=") >= 0 ?(parseFloat(filter.match(/opacity=([^)]*)/)[1]) / 100) + "" : "" 
    }
    return elem.style.opacity
}

捕获主要结合exec()、match() 和 g标记使用,下面介绍

需要强调的是,因为分组和捕获一样使用(),所以,在一个正则表达式中既有用于分组的(),也有用于捕获的()时,对于分组部分,可以加上*?:**,这样,结果集就只包含我们想要捕获的部分*

例子

'<div>hahahahah<div>'.match(/(<[^>]+>)([^<]+)/)
> [ <div>hahahahah , <div> , hahahahah ] //两个捕获

如果我们只对标签内的文本感兴趣

'<div>hahahahah<div>'.match(/(?:<[^>]+>)([^<]+)/)
> [ <div>hahahahah , hahahahah ] //对于<div>,我们不关心,就不要了

说到?: 就要提一下长得差不多的 ?= 和 ?!

?= 表示后面必须跟着某些东西,并且结果中不包含?=指定的部分,并且不捕获

?! 表示后面必须不跟着某些东西

对比看一下

/a(?:b)/.exec('abc')
> ["ab", index: 0, input: "abc"] //注意匹配的是"ab"

/a(?=b)/.exec('abc')
> ["a", index: 0, input: "abc"] //注意匹配的只是"a"

再看个例子,数字字符串转千分位

function formatNumber(str) {
  return str.replace(/\B(?=(\d{3})+$)/g, ',')
}
formatNumber("123456789")
> 1,234,567,890

解释:

  1. \B表示除了字符串首字母之前的边界,比如1和2之间的边界,2和3之间的边界等

  2. 后面()中的?=(\d{3})+$表示上面提到的那些边界后面必须跟着3N个数字直到字符串尾部

  3. g表示全局匹配,即每个上面说的边界都要检测2,如果符合,replace把边界替换成,

exec()、match()与g标记的故事

exec()和match()都是返回数组,结果集中包含捕获的内容

在正则中不包含g时,exec()和match()返回的结果集是一样的,数组中依次是 整个匹配的字符串、依次的()指定的要捕获的部分

reg1

在有g的时候

match()返回的数组中的每一项是依次匹配到的整体字符串,不包含每个匹配中捕获到的内容

对比来看

"p123 q123".match(/\b[a-z]+(\d+)/)
> ["p123", "123", index: 0, input: "p123 q123"]

"p123 q123".match(/\b[a-z]+(\d+)/g)
> ["p123", "q123"]

可以看到加上g后,返回的数组就只有匹配项了

那么,即想匹配全部,又想获取到捕获怎么办呢?

使用while结合exec()

let pattern=/\b[a-z]+(\d+)/g
let str='p123 q123'
let match
while((match=pattern.exec(str)) !=null){
	console.log(match)
}

> ["p123", "123", index: 0, input: "p123 q123"]
  ["q123", "123", index: 5, input: "p123 q123"]

replace()

对于字符串的replace方法,重点说一下,接受的第二个参数,可以是一个函数

对于str.replace(/xxxxx/g,function(){})

函数在每次前面的正则匹配成功时都会执行,函数的参数依次是,完整的匹配文本、依次的捕获部分、当前匹配的索引、原始字符串

"border-bottom-width".replace(/-(\w)/g,(match,capture)=>{
    return capture.toUpperCase()
})
> "borderBottomWidth"

喜欢的给个star,谢谢

Mobx浅析与简单实践

响应式编程介绍

从不同层面来看,响应式的表象很多

从视图层来说,主流的框架都是响应式的,模型变化自动驱动视图变化,注意,这里说的是自动,这也是响应式最本质的概念(当某些东西改变后自动产生side effect),angularjs中的脏监测、vue使用的对象属性劫持等内部机制都是保障了模型到视图层面的自动响应

从异步,事件的角度来说,Rxjs给我们提供了一种统一的解决思路,所有的东西都是stream,不管是同步的、异步的,事件、还是未来的,我们可以用相同的方式处理

数据层面,Mobx专注于解决数据级别的响应,它不关系数据的来源方式,只要一个对象中的属性、一个基本类型变量发生了变化,对这些数据的订阅就会自动执行

  • mobx vs rxjs

mobx和rxjs是一种互补的关系,两种专注的层面不同,rxjs响应数据的来源,mobx响应数据的变化

例如:如果想在更新state之前对用户的输入操作节流,大致工作流是

DOM events -> RxJS -> Update state -> MobX -> Update UI
//rxjs用来处理事件,自动节流,Mobx来响应数据的变化

专注数据层面响应式的mobx

mobx的核心**是让一切需要应用状态的东西,在需要的时候,都能自动获取需要的数据,比如说

class Todo{

    @observable list=[]

    @computed get listCount(){
        return this.list.length
    }
    //listCount借助于list数组,当数组的长度发生变化时,使用listCount的地方自动变化
}

或者

class TodoStore{
    @observable list=[]
}

@observer
class Todo extends React.Component{
    render(){
        return this.props.todoStore.list.map(x=>{
            
        })
    }
}
 //当list变化时,Todo组件重新render
  • mobx的四个核心概念

state:就是代表应用的当前状态,纯数据,数据驱动视图嘛

derivations: 类似vue中的computed,使用@computed定义,怎么理解呢?学angularjs或react时,都会接触到一个概念,就是代表当前视图的模型数据应该尽可能没有重复的,用最少的数据来保证视图的正常变化

举个例子,有一个页面渲染一个列表,并展示列表的数量

class TodoList extends React.Component{

    constructor(props){
        super(props)
        this.state={
            list:[]
        }
    }
    render(){
        return (
            <div>
                <label>count: {this.state.list.length}</label>
                //render list
            </div>
        )
    }
}

这里没有为展示数量单独在state中定义一个length属性,使用state.lists.length可以间接的得到数量,

derivations就是用来计算那个可以通过state间接计算来的值

Reactions:代表剩下的那些需要对state变化动态作出反应的东西,比如说react的重新render,数据变化时自动重新ajax请求

举个例子: 一个搜索功能,我们有一个store类,代表应用的状态,有一个observable字段search,代表页面中input标签的用户输入值,当用户输入东西的时候,自动发送查询请求,这里,对search的变化自动发送ajax请求就是一个 reactions

class SearchGit{
      @observable search = ''

      constructor(){
           autorunAsync(() => {
                if (!this.search) return
                fetchRepos(this.search)
            }, 1000)
      }
}

actions: 改变state的操作,显式申明更好

简单实践

react结合mobx的一个github repos 搜索的功能,支持滚动加载

PS(从mobx仓库的首页中发现一个在线写代码的网站codesanbox,和jsfiddle等比起来颜值简直不能再高。。。。这个小练习就是直接在上面编辑的,不足之处就是不支持样式文件的方式,样式只能写成style)

初步实践总结:

  1. mobx提供一个autorunAsync,实现类似去抖的功能,不错

  2. 对于实现一个滚动加载,结合数据的自动响应式,流程还是很清晰的

  3. mobx只关注数据的变化,对状态存储在哪,异步请求写在哪没有一套最佳实践,持续探索吧

参考

Ten minute introduction to MobX and React

Mobx vs Reactive Stream Libraries (RxJS, Bacon, etc)

The introduction to Reactive Programming you've been missing


流动的数据——使用 RxJS 构造复杂单页应用的数据逻辑

对服务端渲染的一次实践

之前react做的一个应用,最近把首页改成了服务端渲染的形式,过程还是很周折的,踩到了不少坑,记录一些重点,希望有所帮助

前端使用的技术栈

  • react、react-dom 升级到 v16

  • react-router-dom v4

  • redux red-sage

  • antd-mobile 升级到 v2

  • ssr服务 express

项目地址

访问地址(手机模式)

非服务端渲染 服务端渲染

效果对比

nossr
ssr

前后处理流程对比

flow

react下ssr的实现方式

React下同构的解决方案有next.js、react-server等,这里,因为这个项目之前已经采用create-react-app、redux做完了,只是想在现有系统基础上把首页改成服务端直出的方式,就选择了webpack-isomorphic-tools这个模块

webpack-isomorphic-tools介绍

如果我们想在现有React系统中引入同构,首先要解决的一个重要问题是:代码中我们import了图片,svg,css等非js资源,在客户端webpack的各种loader帮我们处理了这些资源,在node环境中单纯的依靠babel-regisiter是不行的,执行renderToString()会报错,非js资源没法处理

而webpack-isomorphic-tools就帮助我们处理了这些非js资源,在客户端webpack构建过程中,webpack-isomorphic-tools作为一个插件,生成了一份json文件,形如:

isomorphic-json

有了这份映射文件,在同构的服务端,renderToString()执行的过程中,就可以正确的处理那些非js资源

比如我们有一个组件:

const App =()=>{
    return <img src={require('../common/img/1.png')}>
}

同构的服务端调用renderToString(<App />),就生成正确的

<img src="static/media/1.3b00ac49.png">标签

对webpack-isomorphic-tools的具体使用参见github

实现ssr需要解决的问题

  1. 非js资源引用的处理,上面已经说过

  2. 初始redux store数据的获取(即保证请求的服务端渲染的页面和单纯请求的首页的状态一致)

  3. 路由跳转如何处理

  4. 用户在客户端登录了,重新请求服务端页面,服务端如何加入用户已登录了的新状态

  5. 用户访问了服务端渲染的首页,客户端js加载完后还是会执行,组件componentDidMount()中的ajax请求如何避免触发

额,一一个说

初始redux store数据的获取

简单总结就是

  1. 我们请求了ssr服务,服务在给我们吐页面之前,实例化一个createStore()对象,要将原本在客户端初始请求的那几个ajax在这发,这几个请求完成后都dispatch(action),然后store中就有初始状态了

  2. 然后执行

renderToString(<Provider store={store}>
        <Router location={req.baseUrl}
            context={context}>
            <Routes />
        </Router>
    </Provider>)
 //得到填满数据的标签  

  1. 拼接html

注意,上面说的webpack-isomorphic-tools中生成的json文件中有js,css的对应关系,这里我访问那个json文件得到js、css的路径,拼到html中

还要返回store中保存的状态,供客户端js createStore使用

<script>
        window.__INITIAL_STATE__ = ${JSON.stringify(store.getState())}
    </script>
  1. 在客户端js中
const sagaMiddleware = createSagaMiddleware()
const store = createStore(
    reducer,
    window.__INITIAL_STATE__,
    applyMiddleware(sagaMiddleware)
)
sagaMiddleware.run(rootSaga)

路由

在做同构的时候不能用BrowserRouter,要使用无状态的StaticRouter,并结合location和context两个属性

有这样的路由结构

<div className="main">
    <Route exact path="/" render={() =>
        <Redirect to="/home"></Redirect>
    }></Route>
    <Route path="/home" component={Home}></Route>
    <Route path="/detail/:id" component={Detail}></Route>
    <Route path="/user" component={User}></Route>
    <Route path="/reptile" component={Reptile}></Route>
    <Route path="/collect" component={Collect}></Route>
</div>
//默认跳到/home,其他的该到哪到哪

server端的代码要这样

const context = {}
const html = renderToString(
    <Provider store={store}>
        <Router location={req.baseUrl}
            context={context}>
            <Routes />
        </Router>
    </Provider>)
//<Route>中访问/,重定向到/home路由时
if (context.url) {
    res.redirect('/home')
    return
}

StaticRouter可以根据request来的url来指定渲染哪个组件,context.url指定重定向到的那个路由

也就是说,要是访问 /,StaticRouter会给我们重定向到/home,并且StaticRouter自动给context对象加了url,context.url就是重定向的/home,当不是重定向时,context.url是undefined

我们还可以自己写逻辑 通过context来处理302、404等。但这里我不需要。。。。。,为什么呢?

我没做全栈的同构,只服务端渲染了主页,渲染一个和多个差不多,全都渲染的话就是在服务端要根据当前请求的路由来决定要发那些请求来填充Store

我对路由的处理流程上面的思维导图有说明,就是在nginx中多配一个代理。

对于访问/、/home这两个路由,代理到ssr服务,来吐首页内容,api代理到后端服务,其他的直接返回(也就是说如果在detail页面或user页面刷新了页面还是之前客户端渲染那套)

对登录操作的处理

上面说server端初始化数据的时候还有一个登陆问题没说。

用户初始访问了服务端渲染的首页,然后在客户端转到登录页面登陆了,重新回到首页刷新了页面,喔,又去请求了ssr服务,但服务端不知道当前用户登录了啊,还是原来的流程,返回的__INITIAL_STATE__中还是没有用户的个人信息和已登录状态

所以,在客户端登陆后,要将用户的token存到cookie中,这样,在首页就算用户刷新了页面,重新请求页面请求中也会带上cookie,在服务端,根据request.cookies中是否有token来决定发哪些请求填充store

if (auth) {
    //要是有token就去查用户信息和是否登录状态(还查是否登录是因为token有可能是被篡改过的)
        promises = [
            getMoviesList(store, auth),
            getCategory(store),
            checkLogin(store, auth),
            getUinfo(store, auth)
        ]
    } else {
        promises = [
            getMoviesList(store),
            getCategory(store),
        ]
}
Promise.all(promises).then(x=>{
    renderToString(<Provider store={store}></Provider>)
})

避免客户端js中初始请求的触发

到这一步,访问域名,就能够正确展示服务端渲染的页面,跳到别的路由,客户端的js也能正常处理接下来的事,但是,服务端渲染页面展示后,首页那几个ajax请求还是触发了,这是没必要的。

原以为这是react renderToString()生成的标签和客户端js hydrate()的有差异导致的,然而,实际上,js执行了,组件的生命周期该触发还是会触发的,不只是 attach event listeners to the existing markup

所以要手动避免

在App组件中

componentDidMount() {
        if (!window.__INITIAL_STATE__) {
            this.props.checkLogin()
            this.props.loadCategory()
        }
    }

//当当前页面是服务端返回的(因为window.__INITIAL_STATE__有初始状态),初始的ajax就不触发了

总结

服务端渲染的坑还是挺多的,这一个星期就搞它了。。。。这里记录一些比较重要的东西,具体细节有兴趣的可以看下代码.最后,最重要的,喜欢的给个star,感谢。。。。。。。

preact源码解析

最近读了读preact源码,记录点笔记,这里采用例子的形式,把代码的执行过程带到源码里走一遍,顺便说明一些重要的点,建议对着preact源码看

vnode和h()

虚拟结点是对真实DOM元素的一个js对象表示,由h()创建

h()方法在根据指定结点名称、属性、子节点来创建vnode之前,会对子节点进行处理,包括

  1. 当前要创建的vnode不是组件,而是普通标签的话,文本子节点是null,undefined,转成'',文本子节点是number类型,转成字符串

  2. 连续相邻的两个子节点都是文本结点,合并成一个

例如:

h('div',{ id: 'foo', name : 'bar' },[
            h('p',null,'test1'),
            'hello',
            null
            'world', 
            h('p',null,'test2')
        ]
)

对应的vnode={

    nodeName:'div',
    attributes:{
        id:'foo',
        name:'bar'
    },
    [
        {
            nodeName:'p',
            children:['test1']
        },
        'hello world',
        {
            nodeName:'p',
            children:['test2']
        }
    ]

}

render()

render()就是react中的ReactDOM.render(vnode,parent,merge),将一个vnode转换成真实DOM,插入到parent中,只有一句话,重点在diff中

return diff(merge, vnode, {}, false, parent, false);

diff

diff主要做三件事

  1. 调用idff()生成真实DOM

  2. 挂载dom

  3. 在组件及所有子节点diff完成后,统一执行收集到的组件的componentDidMount()

重点看idiff

idiff(dom,vnode)处理vnode的三种情况

  1. vnode是一个js基本类型值,直接替换dom的文本或dom不存在,根据vnode创建新的文本返回

  2. vnode.nodeName是function 即当前vnode表示一个组件

  3. vnode.nodeName是string 即当前vnode表示一个对普通html元素的js表示

一般我们写react应用,最外层有一个类似的组件,渲染时ReactDOM.render(<App/>>,root),这时候diff走的就是第二步,根据vnode.nodeName==='function'来构建组件,执行buildComponentFromVNode(),实例化组件,子组件等等

第三种情况一般出现在组件的定义是以普通标签包裹的,组件内部状态发生改变了或者初次实例化时,要render组件了,此时,要将当前组件现有的dom与执行compoent.render()方法得到的新的vnode进行Diff,来决定当前组件要怎么更新DOM

class Comp1 extends Component{

    render(){
        return <div>
                {
                    list.map(x=>{
                        return <p key={x.id}>{x.txt}</p>
                    })
                }
            <Comp2></Comp2>
        </div>
    }
    //而不是
    //render(){
    //    return <Comp2></Comp2>
    //}

}

普通标签元素及子节点的diff

我们以一个真实的组件的渲染过程来对照着走一下表示普通dom及子节点的vnode和真实dom之间的diff过程

假设现在有这样一个组件


class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      change: false,
      data: [1, 2, 3, 4]
    };
  }

 change(){
    this.setState(preState => {
        return {
            change: !preState.change,
            data: [11, 22, 33, 44]
        };
    });
 }

  render(props) {
    const { data, change } = this.state;
    return (
      <div>
        <button onClick={this.change.bind(this)}>change</button>
        {data.map((x, index) => {
          if (index == 2 && this.state.change) {
            return <h2 key={index}>{x}</h2>;
          }
          return <p key={index}>{x}</p>;
        })}
        {!change ? <h1>hello world</h1> : null}
      </div>
    );
  }
}

初次渲染

App组件初次挂载后的DOM结构大致表示为

dom = {
   	tageName:"DIV",
   	childNodes:[
   		<button>change</button>
   		<p key="0">1</p>,
   		<p key="1">2</p>,
   		<p key="2">3</p>,
   		<p key="3">4</p>,
   		<h1>hello world</h1>
   	]
}

更新

点击一下按钮,触发setState,状态发生变化,App组件实例入渲染队列,一段时间后(异步的),渲染队列中的组件被渲染,实例.render执行,此时生成的vnode结构大致是

vnode= {
	nodeName:"div"
	children:[
		{ nodeName:"button", children:["change"] },
		{ nodeName:"p", attributes:{key:"0"}, children:[11]},
		{ nodeName:"p", attributes:{key:"1"}, children:[22]},
 		{ nodeName:"h2", attributes:{key:"2"}, children:[33]},
		{ nodeName:"p", attributes:{key:"3"}, children:[44]},
	]
 }

//少了最后的h1元素,第三个p元素变成了h2

然后在renderComponent方法内diff上面的dom和vnode diff(dom,vnode),此时在diff内部调用的idff方法内,执行的就是上面说的第三种情况vnode.nodeType是普通标签,关于renderComponent后面介绍

首先dom和vnode标签名是一样的,都是div(如果不一样,要通过vnode.nodeName来创建一个新元素,并把dom子节点复制到这个新元素下),并且vnode有多个children,所以直接进入innerDiffNode(dom,vnode.children)函数

innerDiffNode(dom,vchildren)工作流程

  1. 对dom结点下的子节点遍历,根据是否有key,放入两个数组keyed和children(那些没有key放到这个里)

  2. 遍历vchildren,为当前的vchild找一个相对应的dom下的子节点child,例如,key一样的,如果vchild没有key,就从children数组中找标签名一样的

  3. child=idiff(child, vchild); 递归diff,根据vchild来得到处理后的child,将child应用到当前父元素dom下

接着看上面的例子

  1. dom子节点遍历 得到两个数组
keyed=[
    <p key="0">1</p>,
   	<p key="1">2</p>,
   	<p key="2">3</p>,
   	<p key="3">4</p>
]
children=[
    <button>change</button>,
    <h1>hello world</h1>
]
  1. 迭代vnode的children数组

存在key相等的

vchild={ nodeName:"p", attributes:{key:"0"}, children:[11]},
child=keyed[0]=<p key="0">1</p>

存在标签名改变的

vchild={ nodeName:"h2", attributes:{key:"2"}, children:[33]},
child=keyed[2]=<p key="2">3</p>,

存在标签名相等的

vchild={ nodeName:"button", children:["change"] },
child=<button>change</button>,

然后对vchild和child进行diff

child=idff(child,vchild)

看一组子元素的更新

看上面那组存在keys相等的子元素的diff,vchild.nodeName=='p'是个普通标签,所以还是走的idff内的第三种情况。

但这里vchild只有一个后代元素,并且child只有一个文本结点,可以明确是文本替换的情况,源码中这样处理,而不是进入innerDiffNode,算是一点优化

let fc = out.firstChild,
		props = out[ATTR_KEY],
		vchildren = vnode.children;

	if (props == null) {
		props = out[ATTR_KEY] = {};
		for (let a = out.attributes, i = a.length; i--;) props[a[i].name] = a[i].value;
	}

	// Optimization: fast-path for elements containing a single TextNode:
	if (!hydrating && vchildren && vchildren.length === 1 && typeof vchildren[0] === 'string' && fc != null && fc.splitText !== undefined && fc.nextSibling == null) {
		if (fc.nodeValue != vchildren[0]) {
			fc.nodeValue = vchildren[0];
		}
	}

所有执行child=idiff(child,vchild)

child=<p key="0">11</p>
//文本值更新了

然后将这个child放入当前dom下的合适位置,一个子元素的更新就完成了

如果vchild.children数组有多个元素,又会进行vchild的子元素的迭代diff

至此,diff算是说了一半了,另一半是vnode表示一个组件的情况,进行组件渲染或更新diff

组件的渲染、diff与更新

和组件的渲染,diff相关的方法主要有三个,依次调用关系

buildComponentFromVNode

  1. 组件之前没有实例化过,实例化组件,为组件应用props,setComponentProps()

  2. 组件已经实例化过,属于更新阶段,setComponentProps()

setComponentProps

在setComponentProps(compInst)内部进行两件事

  1. 根据当前组件实例是首次实例化还是更新属性来调用组件的componentWillMount或者componentWillReceiveProps

  2. 判断是否时强制渲染,renderComponent()或者把组件入渲染队列,异步渲染

renderComponent

renderComponent内会做这些事:

  1. 判断组件是否更新,更新的话执行componentWillUpdate(),

  2. 判断shouldComponentUpdate()的结果,决定是否跳过执行组件的render方法

  3. 需要render,执行组件render(),返回一个vnode,diff当前组件表示的页面结构上的真实DOM和返回的这个vnode,应用更新.(像上面说明的那个例子一样)

依然从例子入手,假设现在有这样一个组件

class Welcom extends Component{

    render(props){
        return <p>{props.text}</p>
    }

}

class App extends Component {

    constructor(props){
        super(props) 
        this.state={
            text:"hello world"
        }
    }

    change(){
        this.setState({
            text:"now changed"
        })
    }

    render(props){

        return <div>
                <button onClick={this.change.bind(this)}>change</button>
                <h1>preact</h1>
                <Welcom text={this.state.text} />
            </div>

    }

}

render(<App></App>,root)

vnode={
    nodeName:App,
}

首次render

render(<App/>,root)执行,进入diff(),vnode.nodeName==App,进入buildComponentFromVNode(null,vnode)

程序首次执行,页面还没有dom结构,所以此时buildComponentFromVNode第一个参数是null,进入实例化App组件阶段

c = createComponent(vnode.nodeName, props, context);
if (dom && !c.nextBase) {
    c.nextBase = dom;
    // passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L229:
    oldDom = null;
}
setComponentProps(c, props, SYNC_RENDER, context, mountAll);
dom = c.base;

在setComponentProps中,执行component.componentWillMount(),组件入异步渲染队列,在一段时间后,组件渲染,执行
renderComponent()

rendered = component.render(props, state, context);

根据上面的定义,这里有

rendered={
    nodeName:"div",
    children:[
        {
            nodeName:"button",
            children:['change']
        },
        {
            nodeName:"h1",
            children:['preact']
        },{
            nodeName:Welcom,
            attributes:{
                text:'hello world'
            }
        }
    ]
}

nodeName是普通标签,所以执行

base = diff(null, rendered) 
//这里需要注意的是,renderd有一个组件child,所以在diff()-->idiff()[**走第三种情况**]---->innerDiffNode()中,对这个组件child进行idiff()时,因为是组件,所以走第二种情况,进入buildComponentFromVNode,相同的流程

component.base=base //这里的baes是vnode diff完成后生成的真实dom结构,组件实例上有个base属性,指向这个dom

base大体表示为

base={
    tageName:"DIV",
   	childNodes:[
        <button>change</button>
   		<h1>preact</h1>
        <p>hello world</p>
   	]
}

然后为当前dom元素添加一些组件的信息

base._component = component;
base._componentConstructor = component.constructor;

至此,初始化的这次组件渲染就差不多了,buildComponentFromVNode返回dom,即实例化的App的c.base,在diff()中将dom插入页面

更新

然后现在点击按钮,setState()更新状态,setState源码中

let s = this.state;
if (!this.prevState) this.prevState = extend({}, s);
extend(s, typeof state==='function' ? state(s, this.props) : state);
/**
* _renderCallbacks保存回调列表
*/
if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
enqueueRender(this);

组件入队列了,延迟后执行renderComponent()

这次,在renderComponent中,因为当前App的实例已经有一个base属性,所以此时实例属于更新阶段isUpdate = component.base =true,执行实例的componentWillUpdate()方法,如果实例的shouldComponentUpdate()返回true,实例进入render阶段。

这时候根据新的props,state

rendered = component.render(props, state, context);

rendered={
    nodeName:"div",
    children:[
        {
            nodeName:"button",
            children:['change']
        },
        {
            nodeName:"h1",
            children:['preact']
        },{
            nodeName:Welcom,
            attributes:{
                text:'now changed' //这里变化
            }
        }
    ]
}

然后,像第一次render一样,base = diff(cbase, rendered),但这时候,cbase是上一次render后产生的dom,即实例.base,然后页面引用更新后的新的dom.rendered的那个组件子元素(Welcom)同样执行一次更新过程,进入buildComponentFromVNode(),走一遍buildComponentFromVNode()-->setComponentProps()--->renderComponent()--->render()--->diff(),直到数据更新完毕

总结

preact src下只有15个js文件,但一篇文章不能覆盖所有点,这里只是记录了一些主要的流程,最后放一张有毒的图

preact

从history api看主流框架的路由机制

前端路由库的作用是改变地址栏,支持浏览器前进、后退,并同步路由对应的视图,这里以react-router及其依赖的history库说一下路由机制

前提

首先简单介绍一下前端路由机制所依赖的pushState、popstate事件、hash及对应的hashChange事件

  1. pushState,popstate
  • 对于支持html5 新增pushState、replaceState方法的浏览器,可以通过设置pushState来在浏览器history栈中新增一条记录

  • 设置pushState(),replaceState()时并不会触发popstate事件,popstate事件只在点击浏览器前进、后退按钮或调用history.back()、history.forward()等时触发

  • pushState()方法第一个参数可以指定一个state对象,并通过history.state或popstate事件回调中event对象获取


history.pushState(state,title,path)

console.log(history.state)

window.addEventListener('popstate',(e)=>{

    console.log(e.state)

})

  1. location.hash hashChange

对于不支持pushState方法的浏览器,可以通过改变location.hash和借助hashChange事件来实现路由功能

window.addEventListener('hashchange',e=>{

})

location.hash="test"

  1. 对比

通过设置history.pushState(state,title,path),可以给对应路由设置一个state,这就给路由之间的数据传递提供了一种新途径,并且,state对象是保存在本地的,刷新页面依然存在,但通过hash方式实现的路由就没法使用,react-router v4版本也去除了对state的模拟

history库介绍

history库提供了三种不同的方法来创建history对象,这里的history对象是对浏览器内置window.history方法的扩展,扩展了push,go,goBack,goForward等方法,并加入了location、listen字段,并对非浏览器环境实现polyfill

createBrowserHistory()
createHashHistory()
createMemoryHistory()

react-router的路由实现(BrowserRouter和createBrowserHistory)

react-router路由实现大体过程

  1. 调用history.push跳转路由时,内部执行window.history.pushState在浏览器history栈中新增一条记录,改变url,执行<Router></Router>组件注册的回调函数,

  2. createBrowserHistory中注册popstate事件,用户点击浏览器前进、回退时,在popstate事件中获取当前的event.state,重新组装一个location,执行<Router></Router>组件注册的回调函数

  3. history库对外暴露createBrowserHistory方法,react-router中实例化createBrowserHistory方法对象,在<Router>组件中注册history.listen()回调函数,当路由有变化时,<Route>组件中匹配location,同步UI

分别来看

history.push

在react中,我们可以调用history.push(path,state)来跳转路由,实际执行的就是createBrowserHistory中的push方法

在这个方法中主要做三件事

  1. 根据传递的path,state参数创建一个location,不同于window.location,这里的location只有这些属性
   location= {
      path:
      search:
      hash:
      state:
      key
    };
    const location = createLocation(path, state, createKey(), history.location);

这个location会在<Router><Route>组件中使用,来根据location中的值和<Route path='xxx'></Route>中的path匹配,匹配成功的Route组件渲染指定的component

  1. 执行globalHistory.pushState({ key, state }, null, href);

  2. 执行Router中注册的listener

const action = "PUSH"
setState({ action, location });

const setState = nextState => {
    Object.assign(history, nextState);

    history.length = globalHistory.length;

    transitionManager.notifyListeners(history.location, history.action);
};

history中对popstate事件的注册

popstate事件触发时,可以得到event.state,createBrowserHistory中会根据这个state和当前window.location重新生成一个location对象,执行Router组件注册的listener,同步UI

const setState = nextState => {
    Object.assign(history, nextState);

    history.length = globalHistory.length;

    transitionManager.notifyListeners(history.location, history.action);
  };

const handlePop = location => {
    const action = "POP";
    setState({action,location)
}

<Router>与<Route>组件

BrowserRouter组件中会实例化一个createBrowserHistory对象,传递给Router组件

class BrowserRouter extends React.Component{

    history = createHistory(this.props);

    render() {
        return <Router history={this.history} children={this.props.children} />;
    }

}

在Router组件中要注册history.listen()的一个监听函数,并且保存一份子组件(Route)使用的数据

getChildContext() {
    return {
      router: {
        ...this.context.router,
        history: this.props.history,
        route: {
          location: this.props.history.location, //history中的location
          match: this.state.match
        }
      }
    };
  }
componentWillMount{
    this.unlisten = history.listen(() => {
        this.setState({
            match: this.computeMatch(history.location.pathname)
        });
    });
}


当调用history.push或触发popstate事件时,这里注册的listener都会被createBrowserHistory执行,触发setState,然后Router的子组件中匹配的会重新渲染,


<Router>
<Route path='/path1' compoent={}>
<Route path='/path2' compoent={}>
<Router>

在Route中有一个match状态,在父组件props发生变化时会重新计算

state = {
    match: this.computeMatch(this.props, this.context.router)
  };

componentWillReceiveProps(nextProps, nextContext) {
    this.setState({
      match: this.computeMatch(nextProps, nextContext.router)
    });
}
//computeMatch主要工作就是匹配当前组件上指定的path和当前浏览器的路径是否一致,一致就渲染组件

render() {

    if (component) return match ? React.createElement(component, props) : null;

    if (render) return match ? render(props) : null;

}

总结

总结一下,react-router的路由机制就是

  1. 借助history库,history中实现了push,go,goBack等方法,注册了popstate事件,当路由跳转时,使用浏览器内置的history api 操作 history栈

  2. history库对外暴露的history对象提供了listen方法,<Router></Router>组件会注册一个listener

  3. 当调用hsitory.push或popstate事件触发时,执行listener

  4. <Router></Router>注册的监听函数内部会setState更新状态

  5. <Router></Router>的子组件<Route>的componentWillReceiveProps生命周期函数中能得到Router中context,根据当前path和浏览器当前location来判断当前route是否match,匹配就渲染component

虽然本文以react-router来介绍路由机制,但主流路由库实现原理都差不多,借助pushState或hash来更新url,在对应的事件处理函数中来做视图的同步

参考

使用canvas来表示最小生成树寻路过程

演示地址

源码

问题引入

在学习了加权无向图寻找最小生成树的算法之后,想通过可视化的方式来表示一个图的构造和最小生成树的寻路过程,就使用canvas来模拟了图的构造,连接边和加重显示最小生成树组成的边的过程

效果图

canvas

分析

整个过程可以分为三步

  1. 通过canvas画圆圈来表示图的M个顶点,随机生成顶点的坐标

  2. 随机生成N条边,通过canvas来连接每条边对应的两个顶点

  3. prim算法寻找最小生成树,canvas加粗显示最小生成树中的边

实现

主要用到的canvas方法有arc(),moveTo(),lineTo(),fillText()以及修改样式和颜色

  • 绘制顶点的位置

将canvas画布表示成一个(M+1)*(M+1)的二维数组,对于M个顶点,为每个顶点随机生成不大于M的两个二维数组的索引

顶点的表示

_random() {
        return {
            x: random(vertex) + 1,
            y: random(vertex) + 1
        }
    }

需要注意的是,因为是随机生成的,两个顶点在数组中的位置可能重复,所以遇到重复的情况,应该为顶点重新生成一个位置

大体过程

geneVertexAxis() {
        let i = 0
        const vertexList = []
        while (vertexList.length < vertex) {
            const p = this._random()
            if (this.axis[p.x][p.y] == -1) {
                this.axis[p.x][p.y] = i
                this.vertexPoints[i] = p
                vertexList.push(i)
                i++
            }
        }
    }
  • 绘制边

同样通过random()来随机生成两个需要连接的顶点(不大于M),并为这条边加一个权重,同时不能生成重复边, v-w 和 w-v视为同一条边

geneEdge() {
        for (let i = 0; i < edge; i++) {
            let v1 = random(vertex)
            let v2 = random(vertex)
            if (v1 !== v2) {
                let e = `${v1}-${v2}-${random(maxWeight)}`
                let eReverse = `${v2}-${v1}`
                if (!this._repeat(`${v1}-${v2}`) && !this._repeat(eReverse)) {
                    this.edgeList.push(e)
                }
            }
        }
    }

重要的是:

使用canvas来绘制圆,绘制直线都很简单,但这里,为了美观,在将两个顶点连接(即在两个圆之间画一条直线)时,对于直线,不能始于一个圆的圆心,止于另一个圆的圆心,而应该取两个圆与直线的焦点作为直线的两个端点

要做到这一点,需要借助数学中的勾股定理和等比三角形(O(∩_∩)O) 来求出焦点相对于圆心的横纵方向的偏移量

/**
 * 计算过两圆心的直线与圆的交点
 * @param {object} p1  圆心1位置
 * @param {object} p2  圆心2位置
 * @param {number} R   半径
 */

export function calculateInters(p1, p2, R) {
    const xAixsDis = Math.abs(p1.x - p2.x)
    const yAixsDis = Math.abs(p1.y - p2.y)
    const disOfTwoPoint = Math.sqrt(xAixsDis * xAixsDis + yAixsDis * yAixsDis).toFixed(2)
    const x = (R / disOfTwoPoint * xAixsDis).toFixed(2)
    const y = (R / disOfTwoPoint * yAixsDis).toFixed(2)

    let p1X = x
    let p1Y = y
    let p2X = x
    let p2Y = y
    
    if (p1.x <= p2.x && p1.y > p2.y) {
        p1Y = -p1Y
        p2X = -p2X
    }
    if (p1.x <= p2.x && p1.y <= p2.y) {
        p2X = -p2X
        p2Y = -p2Y
    }
    if (p1.x > p2.x && p1.y > p2.y) {
        p1X = -p1X
        p1Y = -p1Y
    }
    if (p1.x > p2.x && p1.y <= p2.y) {
        p1X = -p1X
        p2Y = -p2Y
    }

    return {
        p1Inters: {
            x: p1.x + Number(p1X),
            y: p1.y + Number(p1Y)
        },
        p2Inters: {
            x: p2.x + Number(p2X),
            y: p2.y + Number(p2Y)
        }
    }

  • 加重最小生成树的边

使用prim算法求出最小生成树后,我们可以得到一组最小生成树中的顶点列表,然后将列表中的顶点对延时加粗绘制出来

_drawMinLine(edge, edges, i, vertexPoints) {
        const { ctx } = this
        ctx.lineWidth = 3
        setTimeout(() => {
            ctx.beginPath();
            let p1 = edge.v
            let p2 = edge.w
            this.showMinEdgesWeightInfo(edges, i)
            this._drawLine(vertexPoints[p1], vertexPoints[p2])
            ctx.stroke();
        }, (i + 1) * 1000)
    }

Generators深度解读

翻译自

  • 概述

  • generators作为数据生产者(iterators)

  • generators作为数据消费者(observers)

  • generators作为协同程序(协作多个任务)

概述

  1. 什么是generators?

我们可以把generators理解成一段可以暂停并重新开始执行的函数

function* genFunc() {
    // (A)
    console.log('First');
    yield; //(B)
    console.log('Second'); //(C)
}

function*是定义generator函数的关键字,yield是一个操作符,generator 可以通过yield暂停自己执行,另外,generator可以通过yield接受输入和对外输入

当我们调用genFunc(),我们得到一个generator对象genObj,我们可以通过这个genObj控制程序的执行

const genObj = genFunc()

上面的程序初始会暂停在行A,调用genObj.next()会使程序继续执行直到遇到下一个yield

> genObj.next();
First
{ value: undefined, done: false }

这里先忽略genObj.next()返回的对象,之后会介绍

现在,程序�暂停在了行B,再次调用 genObj.next(),程序又开始执行,行C被执行

> genObj.next()
Second
{ value: undefined, done: true }

然后,函数就执行结束了,再次调用genObj.next()也不会有什么效果了

  1. generator能扮演的角色

generators 可以扮演三种角色

  • 迭代器(数据生产者)

每一个yield可以通过next()返回一个值,这意味着generators可以通过循环或递归生产一系列的值,因为generator对象实现了Iterable接口,generator生产的一系列值可以被ES6中任意支持可迭代对象的结构处理,两个例子,for of循环和扩展操作(...)

  • 观察者(数据消费者)

yield可以通过next()接受一个值,这意味着generator变成了一个暂停执行的数据消费者直到通过next()给generator传递了一个新值

  • 协作程序(数据生产者和消费者)

考虑到generators是可以暂停的并且可以同时作为数据生产者和消费者,不会做太多的工作就可以把generator转变成协作程序(合作进行的多任务)

下面详细介绍这三种

generators作为数据生产者(iterators)

generators同时实现了接口Iterable 和 Iterator(如下所示),这意味着,generator函数返回的对象是一个迭代器也是一个可迭代的对象

interface Iterable {
    [Symbol.iterator]() : Iterator;
}
interface Iterator {
    next() : IteratorResult;
}
interface IteratorResult {
    value : any;
    done : boolean;
}

generator对象完整的接口后面会提到,这里删掉了接口Iterable的return()方法,因为这个方法这一小节用不到

*generator函数通过yield生产一系列的值,这些值可以通过迭代器的next()方法来使用,*例如下面的generator函数生成了值a和b

function* genFunc(){
    yield 'a'
    yield 'b'
}

交互展示如下

> const genObj = genFunc();
> genObj.next()
{ value: 'a', done: false }

> genObj.next()
{ value: 'b', done: false }

> genObj.next() // done: true => end of sequence
{ value: undefined, done: true }
  1. 迭代generator的三种方式

  • for of循环
   for (const x of genFunc()) {
       console.log(x);
   }
   // Output:
   // a
   // b
  • 扩展操作符(...)
const arr = [...genFunc()]; // ['a', 'b']
  • 解构赋值
> const [x, y] = genFunc();
> x
'a'
> y
'b'
  1. generator中的return

上面的generator函数没有包含一个显式的return,一个隐式的return 返回undefined,让我们试验一个显式返回return的generator

function* genFuncWithReturn() {
    yield 'a';
    yield 'b';
    return 'result';
}

下面的结构表明return 指定的值保存在最后一个next()返回的对象中

> const genObjWithReturn = genFuncWithReturn();
> genObjWithReturn.next()
{ value: 'a', done: false }
> genObjWithReturn.next()
{ value: 'b', done: false }
> genObjWithReturn.next()
{ value: 'result', done: true }

然而,大部分和可迭代对象一起工作的结构会忽略done属性是true的对象的value值

for (const x of genFuncWithReturn()) {
    console.log(x);
}
// Output:
// a
// b

const arr = [...genFuncWithReturn()]; // ['a', 'b']

yield*会考虑done属性为true的value值,后面会介绍

  1. generator函数中抛异常

如果一个异常离开了generator函数,next()可以抛出它

function* genFunc() {
    throw new Error('Problem!');
}
const genObj = genFunc();
genObj.next(); // Error: Problem!

这意味着next()可以生产三种类型的值

  • 对于可迭代序列中的一项x,它返回 {value:x,done:false}

  • 对于可迭代序列的最后一项,明确是return返回的z,它返回{value:z,done:true}

  • 对于异常,它抛出这个异常

  1. 通过 yield*递归

我们只能在generator函数中使用yield,如果我们想通过generator实现递归算法,我们就需要一种方式来在一个generator中调用另一个generator,这就用到了yield*,现在,我们只介绍yield*用在generator函数产生值的情况,之后介绍yield*用在generator接受值的情况

generator递归调用另一个generator的方式

function* foo() {
    yield 'a';
    yield 'b';
}

function* bar() {
    yield 'x';
    yield* foo();
    yield 'y';
}

执行结构

const arr = [...bar()];
//['x', 'a', 'b', 'y']

在内部,yield*像下面这样工作的

function* bar() {
    yield 'x';
    for (const value of foo()) {
        yield value;
    }
    yield 'y';
}

另外,yield*的操作数不一定非得是一个generator函数生成的对象,可以是任何可迭代的

function* bla() {
    yield 'sequence';
    yield* ['of', 'yielded'];
    yield 'values';
}
const arr = [...bla()];
// ['sequence', 'of', 'yielded', 'values']

yield*考虑可迭代对象的最后一个值

ES6中的很多结构会忽略generator函数返回的可迭代对象的最后一个值(例如 for of,扩展操作符,如上面介绍过的那样),但是,yield*的结果是这个值

function* genFuncWithReturn() {
    yield 'a';
    yield 'b';
    return 'The result';
}
function* logReturned(genObj) {
    const result = yield* genObj;
    console.log(result); // (A)
}

执行结果

> [...logReturned(genFuncWithReturn())]
The result
[ 'a', 'b' ]

generators作为数据消费者(observers)

作为数据的消费者,generator函数返回的对象也实现了接口Observer

interface Observer {
    next(value? : any) : void;
    return(value? : any) : void;
    throw(error) : void;
}

作为observer,generator暂停执行直到它接受到输入值,这有三种类型的输入,通过以下三种observer接口提供的方法

  • next() 发送正常的输入

  • return() 终止generator

  • throw() 发送一个错误

  1. 通过next()发送值

function* dataConsumer() {
    console.log('Started');
    console.log(`1. ${yield}`); // (A)
    console.log(`2. ${yield}`);
    return 'result';
}

首先得到generator对象

const genObj = dataConsumer();

然后执行genObj.next(),这会开始这个generator.执行到第一个yield处然后暂停。此时next()的结果是yield在行A产出的值(是undifined,因为这地方的yield后面没有操作数)

> genObj.next()
//Started
{ value: undefined, done: false }

然后再调用next()两次,第一次传个参数'a',第二次传参数'b'

> genObj.next('a')
//1. a
{ value: undefined, done: false }

> genObj.next('b')
//2. b
{ value: 'result', done: true }

可以看到,第一个next()调用的作用仅仅是开始这个generator,只是为了后面的输入做准备

可以封装一下

function coroutine(generatorFunction) {
    return function (...args) {
        const generatorObject = generatorFunction(...args);
        generatorObject.next();
        return generatorObject;
    };
}

使用

const wrapped = coroutine(function* () {
    console.log(`First input: ${yield}`);
    return 'DONE';
});

> wrapped().next('hello!')
First input: hello!

  1. return() 和 throw()

generator对象有两个另外的方法,return()和throw(),和next()类似

让我们回顾一下next()是怎么工作的:

  1. generator暂停在yield操作符

  2. 发送x给这个yield

  3. 继续执行到下一个yield,return或者throw:

    • yield x 导致 next() 返回 {value: x, done: false}

    • return x 导致 next() 返回 {value:x, done:true}

    • throw err 导致 next() 抛出err

return()和throw() 和next()类似工作,但在第二步有所不同

  • return(x) 在 yield的位置执行 return x

  • throw(x) 在yield的位置执行throw x

return()终止generator

return() 在 yield的位置执行return

function* genFunc1() {
    try {
        console.log('Started');
        yield; // (A)
    } finally {
        console.log('Exiting');
    }
}

> const genObj1 = genFunc1();
> genObj1.next()
Started
{ value: undefined, done: false }
> genObj1.return('Result')
Exiting
{ value: 'Result', done: true }

阻止终止

我们可以阻止return()终止generator如果yield是在finally块内(或者在finally中使用return语句)

function* genFunc2() {
    try {
        console.log('Started');
        yield;
    } finally {
        yield 'Not done, yet!';
    }
}

这一次,return()没有退出generator函数,当然,return()返回的对象的done属性就是false

> const genObj2 = genFunc2();

> genObj2.next()
Started
{ value: undefined, done: false }

> genObj2.return('Result')
{ value: 'Not done, yet!', done: false }

可以再执行一次next()

> genObj2.next()
{ value: 'Result', done: true }

发送一个错误

throw()在yield的位置抛一个异常

function* genFunc1() {
    try {
        console.log('Started');
        yield; // (A)
    } catch (error) {
        console.log('Caught: ' + error);
    }
}
> const genObj1 = genFunc1();

> genObj1.next()
Started
{ value: undefined, done: false }

> genObj1.throw(new Error('Problem!'))
Caught: Error: Problem!
{ value: undefined, done: true }
  1. yield* 完整的故事

到目前为止,我们只看到以yield的一个层面: 它传播生成的值从被调用者到调用者。既然我们现在对generator接受值感兴趣,我们就来看一下yield的另一个层面:yield*可以发送调用者接受的值给被调用者。在某种程度上,被调用者变成了活跃的generator,它可以被调用者生成的对象控制

function* callee() {
    console.log('callee: ' + (yield));
}
function* caller() {
    while (true) {
        yield* callee();
    }
}
> const callerObj = caller();

> callerObj.next() // start
{ value: undefined, done: false }

> callerObj.next('a')
callee: a
{ value: undefined, done: false }

> callerObj.next('b')
callee: b
{ value: undefined, done: false }

generators作为协同程序(协作多个任务)

这一节介绍generator完整的接口(组合作为数据生产者和消费者两种角色)和一个同时要使用这两种角色的使用场景:协同操作多任务

  1. 完整的接口

interface Generator {
    next(value? : any) : IteratorResult;
    throw(value? : any) : IteratorResult;
    return(value? : any) : IteratorResult;
}
interface IteratorResult {
    value : any;
    done : boolean;
}

接口Generator结合了我们之前介绍过的两个接口:输出的Iterator和输入的Observer

interface Iterator { // data producer
    next() : IteratorResult;
    return?(value? : any) : IteratorResult;
}

interface Observer { // data consumer
    next(value? : any) : void;
    return(value? : any) : void;
    throw(error) : void;
}
  1. 合作多任务

合作多任务是我们需要generators同时处理输入和输出,在介绍generator是如何工作的之前,让我们先复习一下JavaScript当前的并行状态

js是单线程的,但有两种方式可以消除这种限制

  • 多进程: Web Worker可以让我们以多进程的方式运行js,对数据的共享访问是多进程的最大缺陷之一,Web Worker避免这种缺陷通过不分享任何数据。也就是说,如果你想让Web Worker拥有一段数据,要么发送给它一个数据的副本,要么把数据传给它(这样之后,你就不能再访问这些数据了)

  • 合作多任务:有不同的模式和库可以尝试进行多任务处理,运行多个任务,但每次只执行一个任务。每个任务必须显式地挂起自己,在任务切换发生时给予它完全的控制。在这些尝试中,数据经常在任务之间共享。但由于明确的暂停,几乎没有风险。

通过generators来简化异步操作

一些基于Promise的库通过generator来简化了异步代码,generators作为Promise的客户是非常理想的,因为它们可以暂停直到结果返回

下面的例子表明co是如何工作的

co(function* () {
    try {
        const [croftStr, bondStr] = yield Promise.all([  // (A)
            getFile('http://localhost:8000/croft.json'),
            getFile('http://localhost:8000/bond.json'),
        ]);
        const croftJson = JSON.parse(croftStr);
        const bondJson = JSON.parse(bondStr);

        console.log(croftJson);
        console.log(bondJson);
    } catch (e) {
        console.log('Failure to read: ' + e);
    }
});

注意这段代码看起来是多么的同步啊,虽然它在行A处执行了一个异步调用。

使用generators对co的一个简单的实现

function co(genFunc) {
    const genObj = genFunc();
    step(genObj.next());

    function step({value,done}) {
        if (!done) {
            // A Promise was yielded
            value
            .then(result => {
                step(genObj.next(result)); // (A)
            })
            .catch(error => {
                step(genObj.throw(error)); // (B)
            });
        }
    }
}

这里忽略了next()(行A)和throw()(行B)可以回抛异常

借助上面的使用分析一下:

首先得到generator对象

const genObj = genFunc();

然后将genObj.next()的返回值传递给step方法

step()中获取到value和done,如果generator没有执行完,当前的value就是上面使用中定义的promise

等到promise执行完,然后将结果result传递给generator函数

genObj.next(result)

然后在generator中程序继续往下执行

const [croftStr, bondStr] = yield XXXX
.
.
.
.

注意行A处递归调用step(genObj.next(result)),使得generator函数中可以存在多个异步调用,而co都能处理

整个过程多么的巧妙啊。。。。。。。。。

Rxjs入门实践-各种排序算法排序过程的可视化展示

Rxjs入门实践-各种排序算法排序过程的可视化展示

这几天学习下《算法》的排序章节,具体见对排序的总结,想着做点东西,能将各种排序算法的排序过程使用Rxjs通过可视化的方式展示出来,正好练系一下Rxjs的使用

本文不会太多介绍Rxjs的基本概念,重点介绍如何用响应式编程的**来实现功能

在线演示地址

源码

效果图

gif

需求

html

页面中包括一个随机生成300个数字的按钮和、一个选择不同排序算法的下拉列表和一个echart渲染的容器元素

点击按钮会随机生成300个随机数,同时页面渲染出300个数的柱状图,然后选择一种排序算法后,页面开始展示排序过程,在排序过程中如果我们切换成另一种排序算法,会停止当前算法的可视化展示,转而开始新的排序算法的可视化展示

思路

要展示出排序算法在排序过程中数组中数据的变化,我们要定期保存一下排序过程中当前数组的快照,然后通过echart展示当前数组的数据,重复这个过程直到排序完成,我们也就有了表示排序过程的一个动画展示

具体实现

在Rxjs中,一切皆是流,要实现这个功能,重要的是确定好数据流,以及数据流在未来一段时间内的变化过程

根据页面,可以清晰的确定几个数据流

按钮点击操作生成的数据流

const createNumber$ = Rx.Observable.fromEvent(query('.numberCreator'), 'click')

切换下拉列表生成的数据流

const select$ = Rx.Observable.fromEvent(query('.sortTypes'), 'change')

点击按钮生成随机数组并渲染echart图表很显然就用到map和do这两个operator

    createNumber$
    .map(e => {
        return numberCreator()
    })
    .do(nums => {
        const option = getOption(nums)
        echartInstance.setOption(option)
    })
    

切换下拉列表时我们要得到当前选择的排序算法的一个标识

let currentType
select$
    .map(e => e.target)
    .map(x => x.options[x.selectedIndex].value)
    .map(type => {
        return {
            type,
            timer:1
        }
    })
    .do(x => {
        currentType = x.type
    })

下面是重点

只点击按钮或者只切换下拉页面都不应该展示排序过程,只有当两个事件流都触发了,并且之后某一个再次触发的时候才会渲染排序过程的动画,所以我们需要combineLatest操作符,将两个数据流合并成一个

const combine$=Rx.Observable.combineLatest(
    createNumber$,
    select$
)

现在在combine$数据流中我们就有个随机数组和排序类型

[Array[300],'1']

然后就应该排序算法进行工作了,这里思考一下

  • [] 怎样来生成我们排序算法排序过程中数据的快照?

  • [] 生成的�数据快照什么时候让echart来渲染?

对于第一点,我们需要将排序算法封装成一个自定义的operator,在排序过程中不断next() 数据快照,
到这里我们的数据流就变成能在未来一段时间内不断生成新Value的一个数据流


Rx.Observable.prototype.sort = function () {
    const input = this
    return Rx.Observable.create((observer) => {
        input.subscribe((arr) => {
            const nums = clone(arr[0])
            const select = arr[1]
            const sortMethod = sortTypes[select.type]
            sortMethod(nums, function (arr) {
                observer.next({
                    nums: JSON.parse(JSON.stringify(arr)),
                    select
                })
            }, error => {
                observer.error(error)
            })
        }, )

    })
}

combine$.sort()

对于第二点,因为排序算法是非常快的,如果我们subscibe sort()操作符产生的新值就开始渲染echart,页面上是看不出动画效果的,所以,我们需要延迟echart渲染图表的过程,我们需要将sort()触发的值转变成一个异步的新事件流并打平到原数据流中

combine$
    .sort()
    .flatMap(obj => {
        return Rx.Observable.of(obj).delay(100 * obj.select.timer++)
    })

注意obj.select.timer++,对于sort()前后触发的两个值,为了展示出echart渲染的动画,我们要给它们渲染的时间依次递增

到这一步,我们的单次功能就能正常进行了,但如果在一个排序动画过程还没有结束,我们又点击了一个新的排序类型,则新旧两次的还在序列中没进行的渲染都会依次进行,干扰echart渲染的效果,所以在切换到新的类型时,我们要过滤序列中的值。

combine$
    .sort()
    .flatMap(obj => {
        return Rx.Observable.of(obj).delay(100 * obj.select.timer++)
    })
    .filter(x => {
        return x.select.type == currentType
    })
    .do(x => {
        const option = getOption(x.nums)
        echartInstance.setOption(option)
    })
    .subscribe(() => { }, null, () => {
        console.log('complete')
    })

整个数据流序列

   -createNumber$---------------------------------------------------------------------------------
 
   ---------------select$-------------------------------------------------------------------------
                             combineLatest()
   ---------------------------combine$------------------------------------------------------------
                              sort()
   ---------------------------v1       v2       v3       v4 .......v11      v22      v33----------
                                flatMap()
   ---------------------------delay1  delay2  delay3  delay4 ....delay11  delay22  delay33--------
                                 filter(currentType==type)
   ---------------------------delay1  delay2  delay11  delay22  delay33--------------------------

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.