适用于所有编码员。无论您是刚刚开始并想要备忘单还是想了解更高级的内容,这些针对 Web 开发的最佳 ChatGPT 提示都能满足您的需求。
我希望你表现得像一个 Python 解释器。我会给你Python代码,你来执行它。不提供任何解释。除了代码的输出之外,不要回复任何内容。第一个代码是:“print('hello world!')”
我希望你充当 Linux 终端。我将输入命令,您将回复终端应显示的内容。我希望您仅使用一个唯一代码块内的终端输出进行回复,而不是其他任何内容。不要写解释。除非我指示您这样做,否则请勿键入命令。当我需要用英语告诉你一些事情时,我会将文本放在大括号内{就像这样}。我的第一个命令是 pwd
我希望你充当 JavaScript 控制台。我将输入命令,您将回复 javascript 控制台应显示的内容。我希望您仅使用一个唯一代码块内的终端输出进行回复,而不是其他任何内容。不要写解释。除非我指示您这样做,否则请勿键入命令。当我需要用英语告诉你一些事情时,我会将文本放在大括号内{就像这样}。我的第一个命令是 console.log(“Hello World”);
编写 Markdown 格式备忘单。
在项目符号列表中,提供使用 Apple 或 Windows 设备进行 React 的快捷方式。
我想让你调试这段代码。该代码应该执行[提供目的][在此处插入代码]
我希望你担任网页设计顾问。我将为您提供与需要帮助设计或重新开发其网站的组织相关的详细信息,您的角色是建议最合适的界面和功能,以增强用户体验,同时满足公司的业务目标。您应该利用您对 UX/UI 设计原则、编码语言、网站开发工具等的知识,为项目制定全面的计划。我的第一个请求是“我需要帮助创建一个销售珠宝的电子商务网站。”
我希望您在示例数据库前充当 SQL 终端。该数据库包含名为“产品”、“用户”、“订单”和“供应商”的表。我将输入查询,您将用终端显示的内容进行回复。我希望您在单个代码块中回复查询结果表,而不是其他任何内容。不要写解释。除非我指示您这样做,否则请勿键入命令。当我需要用英语告诉你一些事情时,我会在大括号中这样做(就像这样)。我的第一个命令是“SELECT TOP 10 * FROM Products ORDER BY Id DESC”
您能为我的网站推荐一个合适的前端框架吗?我正在制作一个电子商务网站。
创建一个 TypeScript 函数,使用 Black-Scholes 模型计算隐含波动率。输入为标的价格、执行价格、无风险利率和期权价格。一步步写下来,并为每个步骤提供解释。
你现在是MetaGPT,你的工作就是用一个创造性的、直观的类比向我解释一堆代码。每当我在这里发布代码片段或问题时,您都会用一个非常有创意的类比与现实世界的对象进行比较来说明问题。然后,您可以引导我了解如何解决问题,或者当前代码如何解决问题,使用您的类比元素来帮助您进行解释。每当您认为可以为解释增加价值时,请不要忘记用易于理解的类比来说明您的解释。确保像世界上最伟大的老师那样教授这些东西。假设你所教的人不是那么聪明,那么,找到一种说明性的方式向他们解释。如果您明白,请立即回复:“MetaGPT:嘿,我是 MetaGPT!
您是一名专业软件工程师,掌握了 Python 和 Rust 编程语言。在这种情况下,您将帮助我将用 Python 编写的代码库翻译为 Rust。
为我编写以下 Python 函数的单元测试
源:CSDN;
链接:https://mp.weixin.qq.com/s/7orC7SMDzLHpZCqhy24BwA
@开发者,在后端仅提供原始数据集的情况下,如何让所有搜索逻辑都在前端完成?不仅如此,还要能够实现预测用户输入、忽略错别字输入、快速自动补齐等智能功能?本文的作者就深入 JavaScript 技术特性,谈一谈 React、性能优化以及多线程那些事儿。
作者 | Leo Fabrikant
译者 | 弯月,责编 | 郭芮
出品 | CSDN(ID:CSDNnews)
以下为译文:
我有一个看似很简单的任务:“有一个从后端检索的数据集,其中包含13,000个数据项,每一项都是冗长罗嗦的名称(科学组织)。使用这些数据创建一个带自动补齐功能的搜索栏。”
你也不觉得很难,对不对?
难点1:
不能使用后端。后端只能提供原始的数据集,所有搜索逻辑都必须在前端完成。
难点2:
开发人员(我):“这些组织名称这么长,需要花点心思。如果我们只是运行简单的字符串匹配,而且用户可能会输错或出现拼写错误,那么就很难搜索到任何结果。”
客户:“你说的对,我们必须加入一些智能的功能,预测用户的输入,忽略输入的错别字。”
注意:一般情况下,我不建议你在未经项目经理同意下,提示客户没有提到的复杂功能!这种情况称为特征蔓延(feature creep)。在上述例子中,只是恰巧我一个人负责这个合同,我有足够的精力,所以我认为这是一个有趣的挑战。
难点3:
这是最大的难点。我选择的智能(也称为“模糊”)搜索引擎非常慢……
随着搜索词的长度加长,这个搜索算法库的搜索时间会迅速增加。此外,看看下面这个庞大的列表,里面的数据项极其冗长,用户需要输入一个很长的搜索词才能出现自动补齐提示。别无他法,搜索引擎跟不上用户的打字速度,这个UI可能会废掉。
我不知道是不是因为我选择的这个搜索算法库太过糟糕,我也不知道是不是因为所有“模糊”搜索算法都是这样的情形。但幸运的是,我没有费心去寻找其他选择。
尽管上述难点大多数是我自己强加上去的,但我依然决定坚持到底,努力优化这种实现。虽然我知道这可能不是最佳策略,但是这个项目的情况允许我这么做,结果将说明一切,最重要的是,对我来说这是一次绝佳的学习体验和成长的机会。
在第一个实现中,我使用了UI的react-autocomplete和react-virtualized库。
render () { return ( <Autocomplete value={this.state.searchTerm} items={this.getSearchResults()} renderMenu={this.reactVirtualizedList} renderItem={this.renderItem} getItemValue={ item=> item.name } onChange={(e, value)=> this.setState({searchTerm: value})} /> ) }
Autocomplete组件需要传递以下几项:value属性,需要传入输入框中的searchTeam;items属性,传入搜索结果;以及renderMenu函数,该函数将搜索结果列表传递给react-vertualized。
react-virtualized能够很好地处理大型列表(它只会渲染列表显示在屏幕上的一小部分,只有滚动列表时才会更新)。考虑到我们需要渲染的组件并不多,我认为应该不会有太严重的性能问题。
更新操作的声明周期也很简单:
getSearchResults=()=> { const {searchTerm}=this.state; return searchTerm ? this.searchEngine.search(searchTerm) : [] // searchEngine.search is the expensive search algorithm };
我们来看看结果如何:
哎呀……很糟。在按住删除键时的的确确能感觉到UI的停顿,因为键盘触发delete事件太快了。
不过至少模糊搜索好用了:'anerican'正确地解释成了'American'。但随着搜索关键字的加长,两个元素(输入框和搜索结果)的渲染过程完全跟不上用户输入的速度,延迟非常大。
尽管我们的搜索算法的确很慢,但如此大的延迟并不是由于单次搜索时间太长导致的。这里还有另外一个现象需要理解:我们管它叫UI阻塞。理解这个现象需要深入了解Chrome的DevTools性能评测工具。
性能评测
可能你不熟悉这个工具,但是你要知道,熟练使用性能评测工具是深入理解JavaScript的最好方式。这不仅因为它能提供有用的信息帮你解决各种问题,而且按照时间显示出JavaScript的执行过程能够帮助你更好地理解好的UI、React、JavaScript事件循环、同步异步执行等概念。
在下面每一节,我都会给出性能分析过程,以及从这些过程中推断出的有趣的结论。这些数据可能会让你眼花缭乱,但我会尽力给出合理的解释!
首先评测一下在Autocomplete中按下两个键时的情况:
X轴:时间,Y轴:按照类型排列的事件(用户输入、主线程函数调用)
理解基本的序列非常重要:
首先是用户的键盘输入(Key Characer)。这些输入在JavaScript的主线程上触发了Event(keypress)事件,该事件又触发了我们的onChange处理函数,该函数会调用setState(图上看不见,因为它太小了,但它的位置在整个栈的最开头附近)。这一系列动作标志着重新计算的开始,计算setState对组件会产生何种影响。这一过程称为更新,或者叫做重新渲染,它会调用几个React生命周期方法,其中就包括render。这一切都发生在一个执行栈(有时称为“调用栈”或简称为“栈”),在图中由每个Event (keypress)下方竖直排列的框表示。每个框都是执行栈中的一次函数调用。
这里不要被render这个词误导了。React的渲染并不仅仅是在屏幕上绘制。渲染只是React用来计算被更新的元素应当如何显示的过程。如果仔细查看第二个Event (keypress),就会发现在Event (keypress)框的外面有个小得几乎看不见的绿条。放大一些就能看到这是对浏览器绘制的调用:
这才是UI更新被真正绘制到屏幕上,并在显示器上显示新帧的过程。而且,第一个Event (keypress)之后没有绘制,只有第二个后面才有。
这说明,浏览器绘制(外观上的更新)并不一定会在Event (keypress)事件发生并且React完成更新之后发生。
原因是JavaScript的事件循环和JavaScript对于任务队列的优先级处理。在React结束计算并将更新写入DOM之后(称为提交阶段,发生在每个执行栈末尾的地方),你可能会以为浏览器应该开始绘制,将DOM更新显示在屏幕上。但是在绘制之前,浏览器会检查JavaScript事件队列中是否还有其他任务,有的任务会比绘制更优先执行。
当JavaScript线程忙于渲染第一个keypress时,产生第二个用户输入(如键盘按下事件)就会出现这种情况(你可以看到第二个Key Character输入发生在第一个keypress执行栈依然在运行时)。这就是前面提到的UI阻塞。第二个Event (keypress)阻塞了UI更新第一个keypress。
不幸的是,这会导致巨大的延迟。因为渲染本身非常慢(部分原因是因为渲染中包含了昂贵的搜索算法),如果用户输入非常快,那么很大可能会在前一个执行栈结束之前输入新的字符。这会产生新的Event (keypress),并且它的优先级比浏览器绘制要高。所以绘制会不断被用户的输入拖延。
不仅如此,甚至在用户停止输入后,队列中依然滞留了许多keypress时间,React需要依次对每个keypress进行计算。所以,即使在输入结束后,也不得不等待这些针对早已不需要的searchTeams的搜索!
注意最后一个Key Character发生后,还有4个Event (keypress)滞留,浏览器需要处理完所有事件才能重绘。
改进方法
为了解决这个问题,重要的是要理解好的UI的要素是什么。实际上,对于每次键盘输入,用户期待的视觉反馈包括两个独立的要素:
理解用户的期望才能找到解决方案。尽管Google搜索快得像闪电一样,但用户无法立即收到搜索结果的事情也屡见不鲜。一些UI甚至会在请求搜索结果时显示加载进度条。
重要的是要理解,对于反应迅速的UI而言,第一种反馈(按下的键显示在输入框中)扮演了非常重要的角色。如果UI无法做到这一点,就会有严重的问题。
查看性能评测是解决问题的第一个提示。仔细观察这些长长的执行栈和render中包含的昂贵的search方法,我们会发现,更新输入框和搜索结果的一切操作都发生在同一个执行栈内。所以,两者的UI更新都会被搜索算法阻塞。
但是,输入框的更新不需要等待结果!它只需要知道用户按下了哪个键,而不需要知道搜索结果是什么。如果我们有办法控制事件执行的顺序,输入框就有机会先更新UI,再去渲染搜索结果,这样就能减少一些延迟。因此,我们的第一个优化措施就是将输入框的渲染从搜索结果的渲染中分离出来。
注意:熟悉Dan Abramov在React JSConf 2018上的演讲的人应该能回忆起这个场景。在他的幻灯片中,他设计了一个昂贵的更新操作,随着输入值的增加,屏幕上需要渲染的组件也越来越多。这里我们遇到的困难非常相似,只不过是随着搜索关键字长度的增加,单个搜索函数的复杂度会增加而已。在Dan的演讲中,他演示了时间切片(Time Slicing),这个React团队在开发中的功能也许可以解决这个问题!我们的尝试会以相似的方案解决问题:找到一个方法来改变渲染的顺序,防止昂贵的计算再次阻塞主线程的UI更新。
注意:本篇讨论的优化最后以失败告终了,但我认为值得讲一讲,因为它能帮助我们更好地理解React的componentDidUpdate生命周期。如果你非常熟悉React,或者只想看看怎样改善性能问题,那么可以直接跳到下一篇。
拆分组件
由于我们想把昂贵的搜索结果更新从输入框更新中拆分出来,所以我们应该自己设计一个组件,并放弃使用react-autocomplete库提供的一站式解决方案Autocomplete:
//autocomplete.js render () { return ( <div> <input onChange={ e=> this.setState({searchTerm: e.target.value})} value={this.state.searchTerm}/> <SearchResults searchEngine={this.props.searchEngine} searchTerm={this.state.searchTerm}/> </div> ) }
然后在SearchResults中,我们需要异步触发searchEngine.search(searchTerm),而不应该在更新searchTerm的同一个渲染中进行。
我最初的想法是利用SearchResults的componentDidUpdate,让searchEngine异步工作,因为听上去这个方法似乎是在更新之后异步执行的。
//searchResults.js componentDidUpdate(prevProps) { const {searchTerm, searchEngine}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { this.setState({ searchResults: searchEngine.search(searchTerm) }) } } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> }
我们将昂贵的searchEngine移动到了componentDidUpdate中,这样就不用在render方法中调用,而是等到更新之后再执行。我希望在输入框更新之后的另一个执行栈中执行render,并两者之间执行一次绘制。我想象的新的更新生命周期如下:
很不幸,认为componentDidUpdate会在浏览器更新之后运行是一个常见的误解。我们来看看这个方法的性能评测:
看到问题了吗?componentDidUpdate跟最初的keypress事件是在同一个执行栈上执行的
componentDidUpdate并不会在绘制结束后执行,因此执行栈跟上一篇一样昂贵,我们只不过是将昂贵的search方法移动到了不同位置而已。尽管这个解决方案并不能改善性能,但我们可以借此机会理解React的生命周期componentDidUpdate的具体行为。
虽然componentDidUpdate不会在另一个执行栈上运行,但它确实是在React更新完组件状态并将更新后的DOM值提交之后才执行的。尽管这些更新后的DOM值还没有被浏览器绘制,它们依然反映了更新后的UI应有的样子。所以,任何componentDidupdate内执行的DOM查询都能访问到更新后的值。所以,组件确实更新了,只是浏览器中看不见而已。
所以,如果想要做DOM计算,一般都应该在componentDidUpdate中进行。在这里很方便根据布局改变进行更新,比如根据新的布局方式计算元素的位置或大小,然后更新状态等。
如果componentDidUpdate每次触发改变布局的更新时都要等待实际的浏览器绘制,那么用户体验会非常糟糕,因为用户可能会在布局改变时看到两次屏幕闪烁。
注(React Hooks):这个差异也有助于理解新的useEffect和useLayoutEffect钩子。useEffect就是我们在这里尝试实现的效果。它会让代码在另一个执行栈中运行,从而在执行之前浏览器可以进行绘制。而useLayoutEffect更像是componentDidUpdate,允许你在DOM更新之后、浏览器绘制之前执行代码。
上一篇我们拆分了组件:
//autocomplete.js render () { return ( <div> <input onChange={ e=> this.setState({searchTerm: e.target.value})} value={this.state.searchTerm}/> <SearchResults searchEngine={this.props.searchEngine} searchTerm={this.state.searchTerm}/> </div> ) }
但我们没能让昂贵的searchEngine在另一个执行栈上运行。那么,还有什么办法能实现这一点呢?
有两个常见的方法可以设置异步调用:Promise和setTimeout。
Promise
//searchResults.js componentDidUpdate(prevProps) { const {searchTerm, searchEngine}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { /* stick the update with the expensive search method into a promise callback: */ Promise.resolve().then(()=> { this.setState({ searchResults: searchEngine.search(searchTerm) }) }) } } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> }
我们来看看性能评测:
(anonymous)是Promise.then()回调函数
又失败了!
理论上Promsie的回调函数是异步的,因为它们不会同步执行,但实际上还是在同一个执行栈中运行的。
仔细看看性能评测就会发现,回调函数被放在了Run Microtasks下,因为Promise的回调函数被当作了微任务。浏览器通常会在完成正常的栈之后检查并运行微任务。
更多信息:Jake Archibald有一篇非常好的演讲(https://medium.com/r/?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DcCOL7MC4Pl0),解释了JavaScript事件循环在微任务方面的处理,还深入讨论了许多我涉及到的话题。
尽管了解这一点很好,但并没有解决问题。我们需要新的执行栈,这样浏览器才有机会在新的执行栈开始之前进行绘制。
setTimeout
//searchResults.js componentDidUpdate(prevProps) { const {searchTerm, searchEngine}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { /* stick the update with the expensive search method into a setTimeout callback: */ const setTimeoutCallback=()=> { this.setState({ searchResults: searchEngine.search(searchTerm) }) } setTimeout(setTimeoutCallback) } } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> }
性能评测:
绘制很难看到,它太小了,但蓝线的位置的确发生了绘制
哈哈!成功了!注意到这里有两个执行栈:Event (keypress)和Timer Fired (searchResults.js:49)。两个栈之间发生了一次绘制(蓝线的位置)。这正是我们想要的!来看看漂亮的UI!
按住删除键时依然有明显的延迟
有很大的改进,但依然很令人失望……这个页面的确变好了,但依然能感觉到UI的延迟。我们来仔细看看性能测试。
我们需要一些技巧才能分析这个性能测试报告并找出延迟的原因。使用性能报告有几个小技巧:
性能评测工具着重显示了两个特别有用的Key Input Interactions,每个都有不同的性能度量:
理想状态下,Key Down交互应该非常短,因为如果JavaScript主线程没有被阻塞的话,事件应该在键盘按下之后立即出发。所以,看到长长的Key Down就意味着发生了UI阻塞问题。
在本例中,长长的Key Down的原因是它们触发时,主线程还在被昂贵的setTimeoutCallback阻塞,比如最后的Key Down。很不幸,Key Down发生时,运行昂贵的search的setTimeoutCallback刚刚开始,也就是说,Key Down只有等待search的计算结束才能触发事件。这个评测中的search方法大约花了330毫秒,也就是1/3秒。从优秀的UI角度而言,1/3秒的主线程阻塞实在太长了。
特别引起我注意的是最后一个Key Character。尽管它关联了Key Down的结束并且触发了Event (keypress),浏览器并没有绘制新的searchTerm,而是执行了另一个setTimeoutCallback。这就是Key Character交互花了两倍时间的原因。
这一点着实让我大跌眼镜。将search移动到setTimeoutCallback的目的,就是让浏览器能够在调用setTimeoutCallback之前进行绘制。但是在最后的Event (keypress)之前依然没有绘制(蓝线的位置)。
结论是,我们不能依赖于浏览器的队列机制。显然浏览器并不一定将绘制排在超时回调之前。如果超时回调需要占用主线程330毫秒,那么也会阻碍主线程,导致延迟。
在上一篇中,我们拆分了组件,并成功地使用setTimeout将昂贵的search移动到了另一个执行栈中,因此浏览器无需等待 search完成,就可以绘制输入框的更新:
//autocomplete.js render () { return ( <div> <input onChange={ e=> this.setState({searchTerm: e.target.value})} value={this.state.searchTerm}/> <SearchResults searchEngine={this.props.searchEngine} searchTerm={this.state.searchTerm}/> </div> ) } //searchResults.js componentDidUpdate(prevProps) { const {searchTerm, searchEngine}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { const setTimeoutCallback=()=> { this.setState({ searchResults: searchEngine.search(searchTerm) }) } setTimeout(setTimeoutCallback) } } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> }
不幸的是,这依然没有解决每个searchTerm导致search阻塞主线程330毫秒并导致UI延迟的问题。
JavaScript的单线程实在太糟糕了……突然我想到了一个方法。
最近我在阅读渐进式Web应用,其中有人使用Service Worker来实现一些过程,比如在另一个线程中进行缓存。于是我开始学习Service Worker。这是一个全新的API,我需要花点时间来学习。
但在学习之前我想先通过实验来验证一下增加额外的线程是否真的能够提高性能。
用服务器端来模拟第二个线程
我以前就做过搜索和自动补齐提示的功能,但这次特别困难的原因是需要完全在前端实现。以前我做过用API来获取搜索结果。其中一种思路是,API和运行API的服务器实际上相当于前端应用利用的另一个线程。
于是,我想到可以做一个简单的node服务器让searchTerm访问,从而实现在另一个线程中运行昂贵的搜索。这个功能非常容易实现,因为这个项目中已经设置过开发用的服务器了。所以我只需添加一个新的路径:
app.route('/prep-staging/testSearch') .post(bodyParser.json(), (req, res)=> { const {data, searchTerm}=req.body const engine=new SearchEngine(data) const searchResults=engine.search(searchTerm) res.send({searchResult}) })
然后将SearchResults中的setTimeout改成fetch:
//searchResults.js componentDidUpdate(prevProps) { const {searchTerm, searchEngine, data}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { /* ping the search route with the searchTerm and update state with the results when they return: */ fetch(`testSearch`, { method: 'POST', body: JSON.stringify({data, searchTerm}), headers: { 'content-type': 'application/json' } }) .then(r=> r.json()) .then(resp=> this.setState({searchResults: resp.searchResults})) } } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> }
现在是见证奇迹的时刻!
注意看删除!
太棒了!输入框的更新几乎非常完美。而另一方面,搜索结果的更新依然非常缓慢,但没关系!别忘了我们的首要任务就是让用户在输入时尽快获得反馈。我们来看看性能测试报告:
注意主线程中间漂亮的空白,它不再是之前层层叠叠的样子了。性能优化中经常会遇到这种情况。主线程中的空白越多越好!
从顶端可以看到testSearch花费的时间。最长的一个实际上花了800毫秒,让我很吃惊,但仔细想想就会发现,并不是search花了这么长时间,而是我们的Node服务器也是单线程的。它在第一个搜索完成之前无法开始另一个搜索。由于输入比搜索快得多,所以搜索会进入队列然后被延迟。搜索函数实际花费的时间是前一个搜索完成之后的部分,大约315毫秒。
总的来说,将昂贵的任务放到服务器线程中,就可以将堆叠的栈移动到服务器上。所以,尽管依然有改进的空间,但主线程看起来非常好,UI的响应速度也更快了!
我们已经证明这个思路是正确的,现在来实现吧!
做了一点研究后我发现,Server Worker并不是正确的选择,因为它不兼容Internet Explorer。幸运的是,它还有个近亲,叫做Web Worker,能兼容所有主流服务器,API更简单,而且能完成我们需要的功能!
Web worker能够在JavaScript的主线程之外的另一个线程上运行代码,每个Web worker都由一个脚本文件启动。启动方式非常简单:
//searchResults.js export default class SearchResults extends React.Component { constructor (props) { super(); this.state={ searchResults: [], } //initiate the webworker: this.webWorker=new Worker('...path to webWorker.js') //pass it the 13,000 item search data to initialize the searchEngine with: this.webWorker.postMessage({data: props.data}) //assign the handler that will accept the searchResults when it sends them back: this.webWorker.onmessage=this.handleResults } componentDidUpdate(prevProps) { const {searchTerm}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { //change our async search request to a .postMessage, the messaging API of webWorkers: this.webWorker.postMessage({searchTerm}) } } handleResults=(e)=> { const {searchResults}=e.data this.setState({ searchResults }) } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> } }
下面是webWorker.js脚本,在SearchResults的构造函数中进行初始化:
//webWorker.js self.importScripts('...the search engine script, provides the SearchEngine constructor'); let searchEngine; let cache={} //thought I would add a simple cache... Wait till you see those deletes now :) function initiateSearchEngine (data) { //initiate the search engine with the 13,000 item data set searchEngine=new SearchEngine(data); //reset the cache on initiate just in case cache={}; } function search (searchTerm) { const cachedResult=cache[searchTerm] if(cachedResult) { self.postMessage(cachedResult) return } const message={ searchResults: searchEngine.search(searchTerm) }; cache[searchTerm]=message; //self.postMessage is the api for sending messages to main thread self.postMessage(message) } /*self.onmessage is where we define the handler for messages recieved from the main thread*/ self.onmessage=function(e) { const {data, searchTerm}=e.data; /*We can determine how to respond to the .postMessage from SearchResults.js based on which data properties it has:*/ if(data) { initiateSearchEngine(data) } else if(searchTerm) { search(searchTerm) } }
可以看到,我还加了些额外的代码。这里我加了缓存,这样之前搜索过的searchTerms就可以立即返回结果了。如此一来,最耗性能的用户交互(常按删除键)的效率就提高了。
我们来看看运行情况:
太棒了……非常快!这看起来很不错啊,实话说,做到这个样子就可以直接发布了!
但是,现在就发布多没劲啊……
从用户体验的角度来说,这个UI已经非常好了。如果仔细观察,其实依然能看到搜索结果的延迟,但几乎察觉不到……不过幸运的是,从性能测试报告中可以看到低效率的地方:
请无视灰色的条,我也不知道它们是怎么来的。
报告中最左侧的线程是我们关注的线程。Main线程已经折叠了,因为里面只有大量的空白,意味着主线程的性能非常好,不会成为任何主要的性能瓶颈。
可以看到,Worker线程里堆满了search调用。从最后一个Key Character输入(蓝线位置)之后就会看到其后果。Worker线程中的最后一个search实际上推迟了3个search才返回。测量一下会发现,延迟大约有850毫秒。而且大部分都是不必要的,因为那三个search都没用,我们不再需要它们返回的结果了。
你也许在想:“这已经很好了!再优化下去性价比不高啊!”
我不这样认为。首先,不要低估尝试新事物和探索带来的价值。如果你从来没做过探索,就很可能无法评价是否值得,因为你不知道你能有哪些收获,以及你将投入多少时间和努力。所以,我认为这种探索带来的经验和知识是无价的。随着知识的积累,你能更好地评价投入的时间是否值得,以及是否应该考虑这些问题。你可以做出更好的决定!
其次,别忘了这并不是过早优化。这些优化都是根据性能评价做出的,我们可以测量出效果。不管怎样评价,如果能改善850毫秒的延迟,那都是非常重大的改进。
最后(但不是唯一),别忘了移动端!虽然我不会在本文中介绍,但我在研究这个问题时,我也跟踪了移动端的性能,现在的条件下依然有能察觉得到的性能延迟。
不管怎么说,我们来解决这个问题!
前面的性能评测揭示的最明显的问题就是,即使对于无用的searchTerm也会运行昂贵的搜索。所以目前的解决方案之一就是在执行昂贵的搜索之前确保searchTerm是最新的。只要在webWorker脚本中加入confirmSearchTerm就可以非常容易地实现:
//webWorker.js self.importScripts('...the search engine script, provides the SearchEngine constructor'); let searchEngine; let cache={} function initiateSearchEngine (data) { searchEngine=new SearchEngine(data); cache={}; } function search (searchTerm) { const cachedResult=cache[searchTerm] if(cachedResult) { self.postMessage(cachedResult) return } const message={ searchResults: searchEngine.search(searchTerm) }; cache[searchTerm]=message; self.postMessage(message) } function confirmSearchTerm (searchTerm) { self.postMessage({confirmSearchTerm: searchTerm}) } self.onmessage=function(e) { const {data, searchTerm, confirmed}=e.data; if(data) { initiateSearchEngine(data) } else if(searchTerm) { /*check if the searchTerm is confirmed, if not, send a confirmSearchTerm message to compare the searchTerm with the latest value on the main thread */ confirmed ? search(searchTerm) : confirmSearchTerm(searchTerm) } }
这里还给SearchResults handleResults加了个额外的条件,监听confirmSearchTerm的请求:
//searchResults.js export default class SearchResults extends React.Component { constructor (props) { super(); this.state={ searchResults: [], } this.webWorker=new Worker('...path to webWorker.js') this.webWorker.postMessage({data: props.data}) this.webWorker.onmessage=this.handleResults } componentDidUpdate(prevProps) { const {searchTerm}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { this.webWorker.postMessage({searchTerm}) } } handleResults=(e)=> { const {searchResults, confirmSearchTerm}=e.data; /* check if confirmSearchTerm property was sent, if so compare it to the latest searchTerm and send back a confirmed searchTerm message */ if (confirmSearchTerm && confirmSearchTerm===this.props.searchTerm) { this.webWorker.postMessage({ searchTerm: this.props.searchTerm, confirmed: true }) } else if (searchResults) { this.setState({ searchResults }) } } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> } }
我们来看看性能评测,看看有没有改进:
很难看出结果,因为它们运行得太快了,但在每次Worker的执行栈之前都会执行confirmSearchTerm(见蓝线)。
说明的确管用了:Worker在每次运行昂贵的search方法之前都会确认搜索是否必要。而且可以看到,顶部的橙色部分有4个Key Character输入,但只运行了三个搜索。这里我们成功地去掉了一个不必要的搜索,节约了最多330毫秒。但之前我们看到,多个额外的搜索会进入队列然后再不必要地运行,而现在我们完全避免了这个问题。所以,节约的时间非常显著,特别是在移动端上。
但仔细观察就会发现我们依然在浪费时间:
最后一个搜索使用了最新的searchTerm,但依然要至少等待当前的搜索完成后才能开始。浪费了84毫秒(蓝色高亮部分)!据我所知,执行栈一旦开始就无法取消。那么我们是不是无计可施了呢?
如果多加一个线程的效果很好,那么加4个会怎样?
实话实说,现在这些只是出于兴趣……但说真的,最后这次优化确实在移动端上带来了人眼能察觉到的改善……
无论怎样,现在我打算用Web Worker阵列!
为了出这份报告,我用人类最快的速度输入的!
图中可以看到,顶端每个橙色的Key Character都有一个ww worker线程,能立即开始搜索(橙色条)。不再需要等待前一个搜索结束。而且每个ww在结束后就能用于下一次搜索。
这是因为我们设置了workerArray.js这个web worker作为分发器。图中看不到,因为它执行得太快了,但对于每个Key Character,workerArray都会执行一个微笑的执行栈,用来处理主线程传来的搜索请求消息,然后分发给第一个可用的ww worker。
我们成功地解决了“搜索堆”的问题。可以认为用增加车道的方式解决了交通拥堵。
为什么用4个搜索worker呢?因为我在测试时的输入速度从来没能到过需要第五个worker的速度(增加worker带来的改进非常微小)。
结果发现,从Key Character输入到search执行之间没有任何延迟。除非找到另一个更有效的搜索算法,否则我们已经成功地移除了所有性能瓶颈。
别忘了,我并没有说这是最佳的解决方案。是否每台手机都能处理4个web worker?性能改善是否值得付出这些额外的代码复杂度?是否还有其他安全方面的考量,导致代码更加复杂?
这些问题都非常重要,正是这些问题会最终引出这样的解决方案。
但至少现在,这个Web worker阵列非常优秀!我们来看看实际效果:
感谢你耐心地阅读完所有的篇节!
如果说这一系列优化有什么感想的话,那就是:
如果你有兴趣,可以看看下面Web worker阵列的代码。
下面的代码中有个小问题。需要一个非常微小的修改才能使它更强壮,最多两行代码。你能找到问题所在并修复吗?
//searchResults.js export default class SearchResults extends React.Component { constructor (props) { super(); this.state={ searchResults: [], } //initiate the worker array: this.workerArray=new WorkerArrayController({ data: props.data, handleResults: this.handleResults, arraySize: 4 }); } componentDidUpdate(prevProps) { const {searchTerm}=this.props; if(searchTerm && searchTerm !==prevProps.searchTerm) { this.workerArray.search({searchTerm}) } } handleResults=(e)=> { const {searchResults}=e.data this.setState({ searchResults }) } componentWillUnmount () { this.workerArray.terminate(); } render () { return <ReactVirtualizedList searchResults={this.state.searchResults}/> } }
SearchResults组件初始化WorkerArrayController。
//workerArrayController.js export default class WorkerArrayController { constructor ({data, handleResults, arraySize}) { this.workerArray=new Worker('... path to workerArray.js'); let i=1; this.webWorkers={}; while (i <=arraySize) { const workerName=`ww${i}`; this.webWorkers[workerName]=new Worker(`...path to ww1.js`); /* Creates a MessageChannel for each worker and passes that channel's ports to both workerArray dispatcher and the worker so they can communicate with each other */ const channel=new MessageChannel(); this.workerArray.postMessage({workerName}, [channel.port1]); this.webWorkers[workerName].postMessage({data}, [channel.port2]); i++; } this.workerArray.onmessage=handleResults; } search=(searchTerm)=> { this.workerArray.postMessage({searchTerm}); } terminate() { this.workerArray.terminate(); for (const workerName in this.webWorkers) { this.webWorkers[workerName].terminate(); } } }
WorkerArrayController用4个ww初始化workerArray web worker,并传递MessageChannel端口给它们,这样它们能够互相通信。
//workerArray.js const ports={}; let cache={}; let queue; function initiatePort (workerName, port) { ports[workerName]=port; const webWorker=ports[workerName]; webWorker.inUse=false; webWorker.onmessage=function handleResults (e) { const {searchTerm, searchResults}=e.data; const message={searchTerm, searchResults}; /* If all workers happen to be inUse, the message gets saved to the the queue and passed to the first worker that finishes */ if(queue) { webWorker.postMessage(queue); webWorker.inUse=true; queue=null; } else { webWorker.inUse=false; } cache[searchTerm]=message; self.postMessage(message); } } function dispatchSearchRequest (searchTerm) { const cachedResult=cache[searchTerm]; if(cachedResult) { self.postMessage(cachedResult); return } const message={searchTerm}; for (const workerName in ports) { const webWorker=ports[workerName]; if(!webWorker.inUse) { webWorker.postMessage(message); webWorker.inUse=true; return } } queue=message; } self.onmessage=function (e) { const {workerName, searchTerm}=e.data; if(workerName) { initiatePort(workerName, e.ports[0]); } else if(searchTerm) { dispatchSearchRequest(searchTerm); } }
workerArray初始化端口对象用于通信,并跟踪每个ww worker。它还初始化了缓存和队列,万一所有端口都被占用的情况下用来跟踪最新的searchTerm请求。
//ww1.js self.importScripts('...the search engine script, provides the SearchEngine constructor'); let searchEngine; let port; function initiate (data, port) { searchEngine=new SearchEngine(data); port=port; port.onmessage=search; } /* search is attached to the port as the message handler so it runs when communicating with the workerArray only */ function search (e) { const {searchTerm}=e.data; const message={ searchResults: searchEngine.search(searchTerm) }; port.postMessage(message) } /* self.onmessage is the handler that responds to messages from the main thread, which only fires during initiation */ self.onmessage=function(e) { const {data}=e.data; initiate(data, e.ports[0]); }
原文:Secrets of JavaScript: A tale of React, performance optimization and multi-threading本文为 CSDN 翻译,转载请注明来源出处。
应该如何以及在何处学习编程?
软件开发是具有挑战性和利润丰厚的职业选择。我们的日常实用项目-灯泡,电视,汽车,银行,购物-一切都是由智能代码驱动的。
如果您想学习编程但不知道从哪里开始,那么您来对地方了。我已编写了分步指南,可帮助您开始进行软件开发,并消除您的忧虑。
如何选择编程语言?
如您所知,当今有许多流行的编程语言。容易被诱惑并尝试一次学习所有内容。但是,这只会导致沮丧和失望。
我强烈建议您选择一种语言(当然,仅在开始时,稍后您应该以获取多种语言为目标)并坚持使用,而不是在太多船上一起航行。选择语言之前,请注意以下几点:
难度级别-您先前对编程语言(如果有)的经验是成功的关键。如果您是编程的新手,那么Java和C#将是一个不错的起点。但是我强烈建议那些愿意花更多时间并寻找更多有价值的选择的人使用Java。它具有广泛的应用程序,收费很高,并且有大量的开发者社区可以寻求支持,因此值得付出额外的努力。
这是突出显示最容易学习的编程语言的信息图:
这张照片很好地总结了所有这些。Java处于中间位置,因为它提供了努力与回报之间的完美平衡。还有许多其他可供选择的选项-从常绿的HTML到新兴的数据科学/人工智能收藏夹(R,Python)。
流行度-这是选择语言时要考虑的另一个重要参数。花数百小时?学习没有很多应用程序的东西将不会有回报。根据TIOBE,排名前五位的流行语言是:
C
Java
Python
C ++
C#
Java和C总是在流行方面竞争,请记住,这仅是一个指标,您的选择不应该仅取决于语言的流行程度。
而且,JavaScript在许多评级中处于领先地位,这并不奇怪。JavaScript是前端开发的主要语言,也经常用于在各种情况下编写简短的脚本。因此,几乎所有开发人员至少在基本水平上都知道这种语言。
资源可用性-自己学习编程,您将反复引用可用资源来消除疑虑。选择一种已经存在多年的语言,意味着有大量的教育和参考资料可以帮助您适应学习过程。无论您是想获得扎实的理论知识,还是想立即开始编写编码,都只需要找到适合自己风格的正确路线即可。此外,有活跃的在线论坛可以发布和回答查询-很有可能您的下一个查询已被某人回答!
长期前景—职业发展可能是您决定学习编程的重要因素。正如您所了解的需求技能,顶级公司使用的技术通常是一个方便的参考。
该信息图显示,排名前25位的公司中有超过15家使用Python,Java,Javascript和C / C ++。尽管C已有数十年的历史并仍被广泛使用,但Python,Javascript和Java的年龄却相同,并且年龄稍小。Java很快流行起来,而Python则花了一些时间。
编程语言的范围
编程是不同的。该词用于创建功能丰富的网站或Web应用程序。此外,对于Web编程,有一个前端(创建功能接口,浏览器可以读取,显示或启动的所有内容)和一个后端(在服务器上运行的所有内容)。编程也是移动应用程序和桌面程序的创建。对于这些领域中的每个领域,都有一组工作工具,包括编程语言。
科学编程,视频游戏,大数据,贸易,金融-不同的语言也可以用于不同的行业。
某些语言更适合编写小型脚本,而其他语言则适用于繁重的企业应用程序。
下面,我将简要列出最常用的位置和位置:
企业级服务器端应用程序(主要是Java)可能是C#,有时是C ++
科学编程-Python,Java,C#,R
大数据-Java,Python,R
机器学习-Python,R
“大型”视频游戏-主要是C
++(具有不同的引擎,尤其是虚幻引擎),C#(具有Unity引擎)
移动应用程序(包括视频游戏)-Java,Kotlin,Swift
操作系统,驱动程序,高性能桌面应用程序-C,C ++
前端(在网络浏览器内创建交互效果)— JavaScript,打字稿
实际上,大多数语言是相对通用的。假设Java可以应用于上述任何领域。在特定区域使用特定语言的主要原因有两个:
1,语言的体系结构特征。C和C ++比同一个Java或C#的底层略高一些,它们更接近硬件并且工作更快。因此,在需要高性能的地方,使用这些语言。而且,如果安全性更为重要,那么在Java中构建安全性要比在C和C ++中容易得多。
2,工具的历史和可用性。假设Java和C#在架构和语法上非常相似。但是Java较早进入Enterprise,因此Enterprise多年来已经支持了许多应用程序。
如果您已经对未来的工作领域有任何计划,请仔细查看相应的语言。例如,如果您确定要成为前端开发人员,则可以选择JavaScript。但是,对于所有其他情况,我对您的建议-选择一种通用语言和流行语言,对于初学者来说相对容易。可以是Java,Python或C。在学习了基础知识之后,您以后可以加深对所选语言的了解,或切换到另一种语言,例如C ++。
步骤1:选择要学习的最佳编程语言
既然您已经知道了指导决策的重要参数,那么现在该该大踏步了。根据上面列出的条件,以下是我建议初学者使用的编程语言:
Java
它提供了最佳的奖励-努力比率。这是初学者友好的。它是学习位于复杂应用程序基础上的面向对象编程的理想选择。它是一种开源语言。Java的最重要优势之一是JVM(Java虚拟机),JVM是使计算机能够运行Java应用程序的虚拟机。这意味着您只需编写一次代码,它就可以在从服务器到手机的任何平台上运行(当然,要保留一些内容)。
各种规模的组织都使用Java,这是一种有利可图的职业选择。Java程序员有一个动态的工作市场,始终欢迎高素质的程序员。
Java用于大型企业级服务器端应用程序。它也广泛用于Android编程中。银行,金融,保险,电子商务,旅行,运输,社交网络等以技术为主导的行业都部署了Java代码以建立引人入胜的用户体验。Java驱动的Web应用程序在大数据技术,软件工具和嵌入式空间中很流行。
C语言
它是开始编程之旅的另一种可靠语言。C可以称为“中级”语言。这意味着与高级Java或Python不同,它更接近硬件。因此,它可以帮助您更深入地了解计算基础。它具有较少的标准库,这意味着您将要从头开始编写大量代码,这对初学者来说非常有用。
在实际应用中,C是最古老但用途广泛的语言之一。几十年来,它一直是用于创建驱动程序,操作系统和脚本的语言。Linux,Unix,Windows —它们全都是用C编写的。考虑到其在物联网(IoT)和数据库中的应用,它仍然是未来的安全选择。
Python
Python长期以来一直处于编程世界的边缘。由于数据科学应用的突然激增,它已成为最近几年的主流。Python的独特之处在于可以快速构建原型。它具有许多补充工具,例如NumPy,SciPy等,这些工具简单易学,但具有在数据科学和科学计算领域构建复杂系统的能力。
Python拥有可在线使用的庞大资源库,并且开发人员社区每天都在增长,从而为庞大的库增加了更多知识。由于它具有内置的测试框架,因此它是进行跨平台测试的首选。
Python对诸如数据科学,机器学习和人工智能之类的未来技术有很高的需求。它可以用于开发Web应用程序或软件。
步骤2.创建学习计划
学习编程语言应被视为一个旅程,而不是一个目的地。您不能在几周内成为并掌握编程语言的所有方面。符合您的期望。列出您希望在课程结束时实现的目标,然后选择与您的目标,学习速度和学习方式相匹配的课程。
不同的语言具有不同的学习方法,您必须系统地遵循这些方法,并关注更大的前景。将您的总体目标分解为每日目标,并将其纳入您的计划中。
步骤3:选择学习资源
既然您已经知道要做什么,那么下一步就是研究资源并将其入围。它们有不同的类型。
以实践为导向的在线课程
有在线资源提供动手教程。这些采用实用的方法进行教学,并使您养成早期编码的习惯。最好的学习方法是通过练习。
所有后续步骤。继续!
如果您只问我一个有关学习编程的建议,就是这样。如果编程很容易,那么每个人都会这样做。学习新事物需要时间和耐心。您需要诚实地努力,并朝着计划的最终结果不断前进。在此过程中需要大步走小路。
即使是经验丰富的程序员,也会编写错误的代码,但他们会继续对其进行完善,直到其成功为止。从您的错误中学习(但从他人的错误中学习更好),并尽量避免重蹈覆辙-有很多新的错误待发!
结论
学习编程语言需要时间,耐心,努力和持久。您选择的资源对于确定最终编写的程序员的质量有很大帮助。由于在线学习革命,许多知名的大学和程序员都在网上发布了他们的资料。您需要做的就是找到合适的人,并发掘您内在的巨大潜力。
我是Java程序员,目前全职Java线上一对一指导学习,为期一年,根据学员的学习基础,学习能力,学习进度,学习时间去制定适合的学习路线和指导,学习Java的小伙伴可以关注或者私信!
*请认真填写需求信息,我们会在24小时内与您取得联系。