Deep In React(五)setState中的黑魔法

以下代码全部基于React15(React16代码太复杂了看不懂哇- -)。 setState不一定是同步的 在React官方文档中有这么一句话state-updates-may-be-asynchronous 。 下面这两个很经典也是新人很容易糊涂的场景就是由上面这句模棱两可的话带来的。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 class Demo extends Component { state = { count: 1 } onClickHandler = () => { this.setState({count: this.state.count + 1}); console.log(this.state.count); // console.log 结果 1 this.setState({count: this.state.count + 1}); console.log(this.state.count); // console.log 结果 1 } render() { const { count } = this.state; return ( <button onClick={this.onClickHandler}>{count}</button> ); } } class SetTimeoutDemo extends Component { state = { count: 1 } onClickSetTimeoutHandler = () => { setTimeout(() => { this.setState({count: this.state.count + 1}); console.log(this.state.count); // console.log 结果 2 this.setState({count: this.state.count + 1}); console.log(this.state.count); // console.log 结果 3 }, 0); } render() { const { count } = this.state; return ( <button onClick={this.onClickSetTimeoutHandler}>{count}</button> ); } } stackBlitz的demo在这 Clicker 和 SetTimeoutClicker ...

2019年1月14日 · 3 分钟 · hateonion

Js中的防抖与节流

为什么需要防抖和节流? 日常与浏览器打交道比较多的前端开发者对于浏览器的各种事件想必都不会陌生,我们会针对某一个特定的事件绑定对应的响应函数,在事件被触发时让浏览器自动调用该函数。 1 2 3 4 5 function onResizeHandler() { // do something on resize } window.addEventListener('resize', onResizeHandler); 如果只是一些触发频率很低的事件,那么上面的代码并没有什么问题。但是如果像resize这样可能在短时间内被频繁触发的事件(比如click/keydown/touchStart等),我们不去做任何的处理的话,可能导致事件的响应函数在短时间内被大量的触发,造成浏览器卡顿,伤害用户体验。 而节流和防抖一个很经典的应用场景就是去控制我们的事件的触发,节省浏览器开销。 什么是防抖 JavaScript中的防抖指的是只有在x ms内没有调用该函数,才会真正调用该函数。如下图 该图上方是事件触发的次数,下方是响应函数触发的次数,可以发现在事件大量被触发时(色块密集),响应函数并没有被触发。当事件停止触发一段事件后(三个色块的时间间隔)后,响应函数才会被真正的触发。 如果你想自己试试上面的例子,可以访问这个 codePen 。 防抖的简单实现 防抖函数其实是一个高阶函数,接收一个函数和防抖事件作为参数,返回值为防抖处理的函数。 的实现其实很简单,我们需要的是是一个定时器,如果在定时器的定时结束前,响应函数被触发的话则重置这个定时器的时间。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 function debounce(fn, wait, leading=false) { /** @type {number} */ let timer; /** @type {number} */ let lastCallTime; /** @type {boolean} */ let isInvoked = false; return function debounced(...args) { const context = this; const thisCallTime = Date.now(); if(leading) { if(!isInvoked) { fn.apply(context, args); isInvoked = true; } if(thisCallTime - lastCallTime >= wait) { fn.apply(context, args); } lastCallTime = Date.now(); return; } clearTimeout(timer); timer = setTimeout(() => fn.apply(context, args), wait); } } 需要注意的是,上面的函数添加了leading参数。传统的防抖函数响应函数第一次触发也需要等待x ms,使用该参数可让响应函数立即进行触发。 ...

2019年1月2日 · 2 分钟 · hateonion

git中的二分查找– git bisect

从二分查找说起 二分查找是一种很简单的算法,适用的对象是 已经排序的数组 。一个最简单的二分查找的实现如下。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /** * @param {Array} sortedList 排好序的list * @param {number} target 需要检索的值 * @return {number} 检索到的值的index,未检索到返回-1 */ function binarySearch(sortedList, target) { let left = 0; let right = sortedList.length - 1; let middle = parseInt((right+left) / 2); while(sortedList[middle] !== target && left !== right) { if(sortedList[middle] > target) { right = middle - 1; } else { left = middle + 1; } middle = parseInt((right + left) / 2); } return sortedList[middle] === target ? middle : -1; } 一个简单的递归实现如下 ...

2018年12月24日 · 2 分钟 · hateonion

使用yeoman创建属于你自己的脚手架

什么是yeoman generator 真正开始写一个yeoman脚手架之前,我们需要先搞清楚 generator 这个名词。 generator是yeoman提供的对于脚手架的封装,每一个generator是一个单独的npm package。不同的generator之间可以通过组合生成新的generator(e.g. generator-generator是对于generator-node的组合 ) 如果你想使用一个已经发布的generator,你只需要 1 2 3 yarn global add yo generator-[generator-name] # e.g generator-node yo [generator-name] # e.g yo node 如何编写一个yeoman generator 上面也提到过generator其实是yeoman提供的一个对于脚手架抽象的封装,所以在yeoman对于generator的项目结构也做出了一些开发者必须要满足的要求。 在项目的package.json中 项目必须依赖于 yeoman-generator, 项目 keywords 必须有 yemoan-generator package名应该以generator-[generator-name]的格式进行命名 项目结构 yeoman generator支持两种不同的项目组织模式 模式1 ├───package.json └───generators/ ├───app/ │ └───index.js └───router/ └───index.js 模式2 ├───package.json ├───app/ │ └───index.js └───router/ └───index.js 需要注意的是, 第二种模式下必须要在pacakge.json里面显式的做出声明 1 2 3 4 5 6 { "files": [ "app", "router" ] } 一个最简单的generator 以下是一个最简单的Generator ...

2018年12月17日 · 3 分钟 · hateonion

Deep In React (四) stack reconciliation

前提 本篇文章中所有的Host Component均以DOMComponent为例。 Internal Instance更新 在之前我们建立了Internal Instance的mount和unmount方法用来处理Internal Instance的挂载和卸载。为了完成更新功能,我们需要建立一个叫receive的方法。 Composite Component更新 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 class CompositeComponent { receive(nextElement) { const previousElement = this.currentElement; const previousProps = previousElement.props; const publicInstance = this.publicInstance; const previousRenderedComponent = this.renderedComponent; const previousRenderedElement = previousRenderedComponent.currentElement; // 更新 this.currentElement = nextElement; const type = nextElement.type; const nextProps = nextElement.props; let nextRenderedElement; if (isClass(type)) { if (publicInstance.componentWillUpdate) { publicInstance.componentWillUpdate(nextProps); } publicInstance.props = nextProps; nextRenderedComponent = publicInstance.render(); } else if (typeof type === "function") { nextRenderedComponent = type(nextProps); } } } 上面的这个render方法对应的是当Composite Component的type没有发生改变的时候,我们只对对应的component进行更新,而不是每次有任何更新都进行重新的mount。而这一个过程同样也是一个递归的过程。 ...

2018年11月18日 · 3 分钟 · hateonion

Deep In React (三)Internal Instance

从一次挂载说起 如果你写过React代码,那么ReactDOM.render(<App />, container) 可能是你最熟悉的一段代码了。<App />是一个React Element,container是一个node节点。这个方法是把React element映射到真正的DOM结构的一个触发器,当你调用这个方法之后,会把React Element渲染成virtual DOM Tree。 首先有必要提一下Element和virtual DOM Tree这两个概念。 React Element可能会存在type为Component Element的节点。而Virtual DOM Tree指代完全映射为真实DOM结构的树,所有节点的type都是string类型。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 { // React Element Tree type: App, props: { children:[{ type: 'div', ... }, { type: Button, ... }] } } // 最终渲染成的virtual DOM Tree { // Virtual DOM Tree type: 'div', props: { children:[{ type: 'div', ... }, { type: 'button', ... }] } } } 渲染是怎么发生的? React在接受<App />这个Element的时候,其实是不知道这个Element的深度是多少,每个节点对应的DOM元素是什么的,<App />这个Element下,可能只是一个简单的<div />,也可能是许多复杂组件的组合。因此,React需要自顶向下进行递归的渲染,最终得到一个对应到真实DOM结构的Virtual DOM树。 ...

2018年11月12日 · 5 分钟 · hateonion

Deep In React (二) Component,Element和渲染

先导知识: React Component和React Element 在React中,有着组件(component)和元素(element)两个概念。我们日常可能听到过很多次Component这个名词,但是对于Element这个名词在React中似乎聊得并不多。今天就来给大家讲讲什么是Element,什么是Component,以及他们和React渲染的关系。 React Element React Element从数据结构上来讲就是简单的plain object, 这个plain object会提供对于整个React组件树的描述。 1 2 3 4 5 6 7 8 9 10 11 { type: 'button', props: { classNames: 'foo', children: { type: 'span', props: { children: 'click' } } } 讲这个Element翻译成HTML就是如下结构 1 2 3 <button class="foo"> <span>click</span> </button> 需要注意的是type这个属性,在上面的例子中,type是一个string,而type是string的element React将其定义为HostElement。Host的意思是这个元素会根据不同的平台对应不同的Native View。比如ReactDOM会对应成相应的HTML Tag,而React-Native则会对应成Native View。 而除开HostElement, React还支持Component类型的element。我们在React代码中经常会这样组合我们的组件。 1 2 3 4 5 6 <Container> <Header /> <Sidebar /> <div>content</div> <Footer /> </Container> 这段代码对应生成的element的描述如下 ...

2018年11月1日 · 3 分钟 · hateonion

Deep In React (一) 高性能React组件

React的渲染机制 在React内部,存在着初始化渲染和更新渲染的概念。 初始化渲染会在组件第一次挂载时,渲染所有的节点 当我们希望改变某个子节点时 我们所期望React帮我们实现的渲染行为是这样的 我们希望当我们的props向下传递时,只有对应需要更新的节点进行更新并重新渲染,其余节点不需要更新和重新渲染。 但是事实上,在默认的情况下,结果却是这样的 所有的组件树都被重新渲染,因为对于React而言,只要有props或者state发生了改变,我的组件就要重新渲染,所以除了绿色的节点,所有的黄色节点也被渲染了。 例子: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 const Foo = ({foo}) => { console.log('Foo is rendering!'); return (<div>Foo {foo}</div>); } const Bar = ({bar}) => { console.log('Bar is rendering!'); return (<div>Bar {bar}</div>); } const FooBarGroup = ({foo, bar}) => { console.log('FooBar is rendering!'); return ( <div> <Foo foo={foo} /> <Bar bar={bar} /> </div> ) } class App extends React.Component { constructor(props) { super(props) this.state = { foo: 0, bar: 0 }; this.handleFooClick = this.handleFooClick.bind(this); this.handleBarClick = this.handleBarClick.bind(this); } handleFooClick (e) { e.preventDefault(); const newFoo = this.state.foo + 1; this.setState({foo: newFoo}); } handleBarClick(e) { e.preventDefault(); const newBar = this.state.bar + 1; this.setState({bar: newBar}); } render() { const {foo, bar} = this.state; return ( <div className="App"> <button onClick={this.handleFooClick}>Foo</button> <button onClick={this.handleBarClick}>Bar</button> <FooBarGroup foo={foo} bar={bar} /> </div> ); } } ...

2018年10月17日 · 4 分钟 · hateonion

使用Typescript给JavaScript做静态类型检查

从迁移TypeScript说起 笔者所说的这个项目,是一个运行了接近五年的老项目,代码横跨es3到es6。而且代码风格由于项目的人员流动也各异。由于项目人数越来越多,以前留下的技术债造成的危害也越来越大。而重构,在这样一个臃肿且混乱的项目中显得举步维艰。由于JS的动态性,有的时候你甚至不知道这个方法是某个类下的实例方法还是JavaScript原生或是JQuery所提供的方法。 这个时候,TypeScript由于其提供的强类型能很好的规范代码,方便开发人员进行日常开发和重构,开始进入了我们的视野。 但是马上我们面对上了另一个难题,那就是,我们只想享受TypeScript强类型带来的类型推导的优势,并不想花大功夫去把整个代码库全部重构成TypeScript。 不过好在,TypeScript从2.3版本后就开始支持使用JsDoc的语法,以comment的形式给JavaScript文件提供强类型的支持。Type Checking JavaScript Files · TypeScript 如何开始使用Type Checking 首先你需要在你需要检查的JavaScript的文件头部显式的加上如下comment 1 // @ts-check 对变量进行类型检查 如果你需要声名一个变量的类型,你可以这样 1 2 3 4 5 /** @type {number} */ let x; x = 0; // OK x = false; // Error: boolean is not assignable to number 当你尝试给x赋一个bool值时,编辑器会提示你类型冲突。 对函数进行类型检查 当然某些时候我们最需要的其实是对于函数签名和返回值的强类型声名,以便我们在其他地方使用的时候不会传入错误的参数或者使用类型错误的函数返回值。TypeScript提供了三种语法来声名函数的类型. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 // jsdoc standard syntax // 声明一个函数 /** * @param {string} foo * @param {string} bar * @returns {string} */ function test(foo, bar) { return `${foo} and ${bar}` } // closure syntax // 声明一个函数表达式 /** * @type {function(string, string): string} */ let test; // test 必须符合定义的函数签名 test = (foo, bar, foobar) => '123' // 报错 函数不符合定义的签名 test = (foo, bar) => 1 // 报错 函数返回值不符合 // typescript like syntax // 声明一个函数表达式 /** * @type {(foo:string, bar:string) => string} */ let test; // test 必须符合定义的函数签名 在日常使用中,如果你想声明一个函数,对于函数做类型检查,你需要使用 @params @returns这种declaration 语法。 如果你想确定一个函数表达式的签名,你需要使用 @type的语法。 ...

2018年10月14日 · 4 分钟 · hateonion

使用Yarn Workspace管理多project repo

背景 管理过在一个javascript repo下面有多个子project的同学可能都遇到过依赖管理困难的问题。对于一些公用的包(比如node-sass),我们想尽可能保证版本的一致,享受yarn的hoist特性,同时也不想下载多次。但是对于一些特殊的包,我们又想保持各个repo的独立性。 在Yarn 1.0版本以前,以上念想其实是很去实现的,我们不得不花费大量的人力去维护我们各个子project的依赖包关系。但是Yarn在1.0版本引入了workspace 这个特性之后,这些问题就不再存在了。 什么是workspace yarn官方对于workspace的定义是It allows you to setup multiple packages in such a way that you only need to run yarn install once to install all of them in a single pass. 。简而言之,workspace能帮助你更好的管理有多个子project的repo。你既可以在每个子project下使用独立的package.json管理你的依赖,又可以享受一条yarn命令安装或者升级所有依赖的都便利性。 workspace能帮你做什么 简而言之,workspace能帮你做这么两件事情 简化你的工作流 当你有多个子project的时候,你可能做的最多的事情就是分别进入每个文件夹,然后执行yarn install 或者 yarn upgrade foo。 但是引入workspace之后你需要的只是一条在根目录运行yarn install或者yarn upgrade foo命令,你的所有的依赖都会被安装或者更新。 降低包安装和包升级的成本 这里讲的成本主要指的是时间成本和管理的精力成本。 时间成本 在使用workspace之前,你想要安装整个repo的依赖时,你需要独立安装每个子project的依赖。这就意味着,如果你的repo下面同时以来于node-sass,你需要重复下载三次node-sass的安装包。当你的子project或者包依赖并不是特别多的时候,这些时间成本似乎影响并不是那么大。但是随着你的repo越来越大,依赖越来越复杂,消耗的时间也会变得越来越恐怖。 在引入workspace之后,yarn会帮助你分析各个子project里面的依赖关系,保证所有子project里面共同的依赖只会被下载和安装一次,大大缩减安装的时间。 精力成本 设想我们引用了一个有严重安全漏洞的包foo,我们想要将整个repo中的这个包都升级到最新版本来修复这个严重的安全漏洞。在没有使用workspace之前,你需要仔细且耐心的遍历每个子project的package.json文件,然后手动执行升级操作。 在引入workspace之后,你大可以在repo的根目录简单运行一条yarn upgrade foo命令,你的所有子project中依赖的foo包都会自动升级到package.json里面指定的版本。 当然,如果你想单独的升级某个子project的包,可以简单的使用yarn workspace <workspace-name> upgrade … 来做到。 怎么使用workspace yarn workspace并不需要安装什么其他的包,只需要简单的更改package.json便可以工作。 首先我们需要确定workspace root,一般来说workspace root都会是repo的根目录。 ...

2018年9月25日 · 1 分钟 · hateonion