SegmentFault 前端学习之路最新的文章
2023-05-18T11:19:23+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
如何写好专栏?
https://segmentfault.com/a/1190000043801912
2023-05-18T11:19:23+08:00
2023-05-18T11:19:23+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<p>以下是一些,我对《如何写好专栏?》的一些思考笔迹,供爱写作的朋友进行参考。</p><p><strong>0️⃣生产好内容的不稳定性</strong></p><p>好娱乐内容,会给人带去情绪价值,无论是开心或情绪上的共鸣。而好的技术内容,给人带去是实打实的技术价值,它必须能帮助相关领域的技术同学获得成长。</p><p>无论是对于组织而言,还是对于个人而言,生产好内容却是不容易的,大红大卖更是不容易。那有没有一套这样的方法,它可以保证生产的都是好内容呢?能保证所有的好内容都能大卖呢?</p><p>对于后者,我是确定的,没有一个确定的方法论,能保证所有好内容都能大卖,无论哪个出版社或出版平台,大红的内容都是头部内容,是少数,而其他的大多数都是只能达到一个及格线。</p><p>对于前者,在某种程度上,我是确定的。是有一套方法论是能够保障内容底线,不至于太差,也能帮助原本就好的内容更上一层楼。极客时间出品的《工匠:内容生产管理手册》就对我的协作帮助良多。</p><p><strong>1️⃣生产好内容的核心方法论</strong></p><p>我们知道,写作是一个个性化的事情,然而个性化的东西不容易量产。如果专栏内容不能批量生产,极客时间也就难以大规模、持续的交付高品质的专栏内容了。为此,极客时间沉淀了“专栏流水线”的方法论,用于解决好内容不容易批量生产的问题。</p><p>书中内容讲述了如何通过管理动作来做好内容生产。类比于工业产品,工业产品是由一系列相互衔接、相互依存的科学工序生成的。</p><p>对于专栏生产,极客时间定制了一套专栏流水线的作业,其工序有四道:</p><ol><li>筹划期</li><li>打磨期</li><li>交付期</li><li>结课期</li></ol><p>不同的阶段,都有不同的三个核心关注目标。俗话说,好的开始是成功的一半。写一个专栏的核心,也是在第一个阶段——筹划期。极客时间教研部为筹划期制订了三个核心的关注点:</p><ol><li>确认老师是我们能力范围内的<strong>最佳人选</strong></li><li>确保自己充分理解目标用户的<strong>痛点问题</strong></li><li>确保课程设计可以抓住主要矛盾,为用户<strong>交付目标</strong></li></ol><p>无论是老师还是编辑都是围绕着这三个核心目标进行执行的,所以这三个目标至关重要。接下来我会围绕我是怎么理解这三个核心目标的。</p><p><strong>2️⃣找对好老师至关重要</strong></p><p>首先是第一条,确定老师是我们能力范围内的<strong>最佳人选。</strong></p><p>我们都知道,极客时间是从 InfoQ 孵化出来的 App,InfoQ、QCon、GMTC、架构师峰会这一系类的技术会议会帮助极客时间积累一大批优秀的讲师资源。</p><p>极客时间在确定好主题后,会联系老师确定初步意向,并会通过会通过 Google 搜索检查能够证明老师专业能力和写作能力的关键履历,包括且不限于已出版图书、个人博客、公开演讲。</p><p>所以和极客时间合作,本身要在某个技术领域中,努力达到集团、乃至行业的顶尖水平,才能够抓住合作的机遇。</p><p>这点对我们的思考价值在于,我们必须在日常工作中把每个技术项目做深了,才能有更大的平台。</p><p>我个人比较幸运,2016年就开始做 RN,在性能优化方向有一些沉淀,特别是在 RN Dynamic Import 上有一些创新,所以有机会参加了 GMTC,紧接着才有机会写 RN 专栏。</p><p>对于极客时间而言,它深刻地明白好老师的价值。没有技术沉淀,没有持续的内容输出,很难是好的老师。把项目做好很重要,会思考会总结也同样重要。</p><p><strong>3️⃣理解用户痛点问题</strong></p><p>极客时间生产专栏的第二核心的关注点是,确保自己充分理解目标用户的<strong>痛点问题。</strong></p><p>这一点,即使对极客时间编辑的要求,更是对专栏老师的要求。</p><p>在极客时间内部手册上提到:”保证最少列举4条关键用户的痛点问题”,并且表示“如果没有用户困惑,那就会把这门课暂停”。</p><p>并且还会在专栏立项之前,进行用户调研,对调研量级还有量化的要求。比如,调研环节需要至少与6位用户、3位专家和2位编辑一对一沟通。</p><p>此外,我和极客时间合作,通过演讲的形式吸引了第一批用户,做了一个社群。一共吸引了大约 50 人参与用户调研。</p><p>我自己也在写专栏前,做了一些很多调查。</p><p>调研:<a href="https://link.segmentfault.com/?enc=zqf2gzMJTzNfDqD6AEw80A%3D%3D.%2Be%2Bk6SCcTdY657ZwxzzagA6Z5f4t7vtyFsCQoOD5XjmkNCeGP2fGRvxGd%2Ba0wMZmC1yBP7j37KuUYMYpUaqrQA%3D%3D" rel="nofollow">https://docs.qq.com/sheet/DQWdsZ0RORkpFQmVj?tab=BB08J2</a></p><p><strong>4️⃣用户调研和用户画像</strong></p><p>我们把用户分成了三类,并对他们提的问题进行了深挖,分析他们学习的痛点。</p><ul><li><strong>初学者:非核心用户。</strong>他们没有前端或客户端的相关经验,直接学习本专栏难度较大。这类用户需要有强烈的学习意愿,边干边学进行大量实操,才能较好地完成学习任务 ;</li><li><strong>大前端开发者:核心用户。</strong>他们已经掌握了 Web、Android、iOS 任意一门技术,同时想学习 React Native。这时本专栏能够给他们带去的价值是实操、经验、原理方面的收获;</li><li><strong>资深跨端开发者:核心用户</strong>。他们本身就会 React Native,想更进一步。因此专栏能给他们带去的价值主要是,解决实践中遇到的业务痛点,并深入理解 React Native 的原理和思想精髓。</li></ul><p>另外,编辑和一些行业专家也提了他们领域建议,才最终将用户调研这一步做到位了。</p><p>可以说,如果没有这些前期的深入调研,后面我是很难写好专栏的。</p><p><strong>5️⃣交付获得感和信任感</strong></p><p>生产专栏的第三核心的关注点是:确保课程设计可以抓住主要矛盾,为用户<strong>交付目标。</strong></p><p>抓住了主要矛盾,就抓住了交付目标的核心。</p><p>那什么是主要矛盾?什么是交付目标呢?</p><p>先说主要矛盾,主要矛盾有两类:</p><ul><li>在读专栏之前,这个知识点你根本不知道,现在你读完之后就能彻底理解和应用这些知识点。</li><li>在读专栏之前,之前这个知识点你也知道,但是你读完之后,有了更深刻的理解。</li></ul><p>交付目标本质上,这是以内容作为载体,来传递价值。不仅让用户在公式、概念、应用、原理方面有实质性的收获,最好还能让用户能在学习方法、思维方式、价值观上与你产生共鸣。前者交付的是获得感,后者交付的是信任感。</p><p><strong>6️⃣专栏大纲的设计</strong></p><p>然而,生产文章和生产专栏是不一样的。</p><p>专栏是由一类列的文章组成的,除了要考虑文章内在的逻辑外,专栏更需要考虑每篇文章之间的逻辑关联。也就说,好的专栏,必须得有个好的大纲。</p><p>如何评估专栏大纲的专业性呢?主要包括 4 个方面:</p><ul><li>内容主线是否清楚?</li><li>是否涵盖必要知识点,是否有冗余?</li><li>是否有独特性?</li><li>是否能为交付目标服务?</li></ul><p>首先,对于不同的用户而言,他关注点是不一样的,新手期望的是一步一步的入门,老手关注的是底层原理,以及如何利用这些原理解决他的问题。因此,在大纲的设计上首先要准守的原则是,由易到难的原则。</p><p>其次,我在设计专栏大纲的时候,也大量的调研的国内外的书籍、课程和文档。这样也能较好弥补自身经验上盲区。以达到必要知识必讲,不重要的内容不讲的原则。</p><p>再次,我要考虑我的专栏大纲究竟有什么独特性,我会从供给侧上进行分析。比如,比如国内的同类书籍 《React Native入门与实战》是 16 年出的,就比较老了,这些年来,RN 迭代很快,很多知识都过时了。而官方文档缺乏一步一步指导和实战内容,不太适合新手入门和进一步深入实践。网络的技术博客内容零散,国外的畅销课水土不服。因此,写 RN 专栏是有机会的。这是我当时的初步判断。</p><p>最后,大纲经过三次以上反复打磨后,最终和编辑进行了定稿。专栏最终决定从核心基础、社区生态、基础设施,以及新架构前沿四个模块进行讲解,以已达到交付价值的目标。</p><p>可以看到,设计专栏和我们平时写博客是完全不一样的。大多数博客只是零散的记录,而专栏是完整产品,这是专栏知识完整性带来的知识结构化的价值。其核心在于大纲的设计。</p><p><strong>7️⃣专栏文章的叙事结构</strong></p><p>要交付一个好的专栏,专栏大纲很重要,具体到每篇文章也同样重要。</p><p>虽然专栏中每篇文章讲述的知识点各不相同,但每篇文章最好都遵循同一个叙事结构。这就好比《阿凡达2》和《阿凡达》两个片子的套路虽然相似,但正是遵循了这种套路的故事性,才让它大卖。</p><p>电影的故事背后是有套路的。希德·菲尔德的《电影剧本写作基础》这本书是电影编剧专业的必读教材,他在书中阐述了“三幕戏结构”理想剧本创作模式。</p><ol><li>第一幕:引出问题。先把主人公的日常模式给呈现出来,然后再打磨模式,构建冲突,推出问题。</li><li>第二幕:试着解决问题。主人公踏上征途,为了解决那个问题,他又遇上了一连串的问题。这就是线性递进的写作结构。</li><li>第三幕:彻底解决问题。经过前面的努力,最后主人公找到一个解决终极困境的方法。设计收尾时刻,让用户满意。</li></ol><p>在创造文章的时候,我们最好也要遵守这种套路。</p><p>比如,我在今天分享的套路就是。</p><p>首先,我先提出了问题:内容不是工业产品,内容是个性化的。因为它是否依靠人的参与,因此质量的不稳定性很大。那么,作为个人,我们应该如何写好专栏?而极客时间作为一个组织,又应该如何管理老师的专栏质量的?</p><p>然后,我开始试着解决问题。我提到了极客时间的《工匠:内容生产管理手册》。对于极客时间而言,它要找对老师。但光有老师不够,老师要能够理解用户才行。通过调研有了用户画像之后,你还得确保能给用户交付价值。那如何交付价值?专利大纲和文章结构是重点。这就是,为了解决上一个问题,又遇上了一连串的问题,然后逐一解决的过程。这个过程,就是线性递进的写作结构。</p><p>最后,“电影剧本”还要给出彻底解决方案。</p><p><strong>8️⃣总结</strong></p><p>回到我们最初的问题,如何写好专栏?</p><p>在《匠心:内容生成管理手册》中,它是这么给出答案的。</p><p>生成专栏一共分为4个阶段。</p><p>第一个阶段是,筹划期。筹划期最为关键。先要选对老师,然后开展用户调研,再围绕交付价值来进行课程设计。</p><p>第二个阶段是,打磨期。在这个时期,进行备稿,备稿到专栏总数的 1/2 的数量时,才能上线。上线的文章,要逐篇保障交付品控。</p><p>第三个阶段是,交付期。这个阶段一方面要关注打开率、学完率、收藏率、分享率,另一方面还需要对老师的状态进行跟进,做好老师写作压力大的心理疏导。</p><p>第四个阶段是,结课期。在最后,要为用户和老师交付足够的仪式感。并且进行阶段性的复盘,沉淀经验。</p><p>以上就是我从《匠心:内容生成管理手册》学到的写好专栏的方法论。</p><p>我的体会是:</p><ul><li>对于个人而言,要去理解用户,再利用“电影三幕戏结构”进行创作。</li><li>对于组织而言,找对人是核心,方法论是保障;</li></ul>
ITPUB的采访稿
https://segmentfault.com/a/1190000043251643
2023-01-05T16:38:08+08:00
2023-01-05T16:38:08+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<p>问题 1:您好!很荣幸有机会有机会采访您,请简单地做个自我介绍吧。</p><p>很荣幸接受你们ITPUB的采访。</p><p>我先自我介绍一下,我叫蒋宏伟,是《React Native 新架构实战课》专栏的作者。</p><p>我是2015年开始转行开始做前端工程师的;在2018年的时候,担任了58RN项目的负责人,负责58集团内部React Native技术基础设施建设;在2020年,从单兵作战变成了小组长,负责了更多技术方向和业务方向。</p><p>带团队和单兵作战很不一样,单从技术层面讲,最关键的是定技术方向,你要对得起大家,别把大家带错路了,因此对类似于React Native新架构这类前沿技术的探索,也成了我的必修课。</p><p>问题 2:您在开始做React Native架构之前主要负责的工作是?</p><p>在2018开始负责58RN基建之前,也就是2016年和2017年,那时我主要是负责的是 RN 业务的开发,也深度参与了一些58RN相关基建工作。</p><p>之所以,老板让我来负责58RN基建,我觉得和我自己好奇心很强有关系吧。其实2015年刚刚入行的时候,除了扎实基础之外,也花了很多时间调研了React。要知道,那时候团队同学用的都是JQuery,并且还对兼容IE6有一定要求,React放在那时候就是非常新的技术了。</p><p>也正因为,在2015年对React有了一定研究,2016年团队上 React Native 项目时,我就被派去参与了相关基建工作的开发了;由于参与了React Native基建的开发,在2017年启动第二个React Native业务时,我就被派去独立负责业务开发了;在2018年正好有个机会,也就顺理成章地负责起了58RN的基建了。</p><p>问题 3:现在React Native 架构对于您所在的58带来了那些价值?这些价值会一直持续增长嘛?</p><p>这个问题挺有意思的,我认为React Native的价值其实分为两个方面,一方面是提升了用户体验,另一方面是降低了企业的成本。</p><p>首先,React Native相对于 H5 来说,是能提升用户体验的。</p><p>举个例子,我们有一个短视频的业务用的就是React Native开发的,但这样的短视频业务用H5是开发不了的,业内也很少有用H5开发短视频的,因为用H5体验太差了,解决不了复杂手势冲突和视频功能定制的问题。</p><p>但我们在React Native上的沉淀比较多,开发出来的短视频业务性能不比Native差,甚至还有公司中有团队主动把 Native短视频下了,替换成React Native短视频。因为React Native不仅体验好而且能够热更新,产品需求一天迭代好几次都没问题,这能让用户始终享用我们最新的、最好用的功能。</p><p>其次,React Native相对于iOS/Androd,是能降低企业成本的。</p><p>降低成本很好理解,一套代码运行两端嘛。开发React Native只要一套代码就行了,而开发iOS/Android需要两套代码,开发一套代码肯定比两套代码的成本要低一些嘛。而且我们内部有着丰富的React Native生态,很多功能直接拿来用就行了,又能进一步的降低研发成本。</p><p>最后,我相信React Native能持续的给用户、给公司带去更多的价值。现在可以肯定,React Native新架构的出现会让用户的体验变得更好,另外在我们内部使用React Native的App也越来越多了,最近一年又有5、6个App接入了58RN,这自然给公司带去了更多的价值。</p><p>问题4: 今年在React Native新架构落地之前有没有遇到记忆深刻的问题?如何解决的?</p><p>我认为最难的问题,就是把集成了React Native老架构的App,给升级到新架构。</p><p>升级的主要问题不是技术上的问题,而是工程上的问题,关键是要做好风险、成本和收益的权衡。</p><p>我从去年9月份就开始研究React Native新架构了,React Native新架构几乎把整个React Native底层都重写了,现在React Native为了兼容,底层有两套代码,既有新架构代码又有老架构代码。既然底层改动量这么大,贸然升级风险肯定也不小,所以得控制升级的风险。</p><p>当然,现在谈论如何升级其实有点早了,因为React Native新架构正式版还没有出来。今年4月新架构预览版出来了,我估计新架构正式版的发布可能要到今年年底了。在今年年底或者明年年初,会有一些想尝鲜的团队开始小规模的尝试,比如在一些没有历史包袱的App,或者一些用户量小的App上先试试。等明年年底,或许会有更多的团队和项目进行升级跟进吧。</p><p>现在,我们对升级的风险、成本和收益的整体评估其实心里大致有了个数,就是得耐心的等待,等待一个 Ready 的时机。</p><p>问题 5:React Native架构与同系列架构的区别是什么?React Native架构日后会成为这个技术方向的主流吗?</p><p>可以换一种描述吗?比如,对比跨端架构之间的区别,优劣等等。</p><p>不太懂“同系列”指的是什么,也不太懂“这个技术方向”指代的是什么。</p><p>如果指的是跨端方向,那么 React Native、小程序、Flutter 之间的区别很大,而且架构一旦定型了想改难度很大,所以不同的跨端框架的架构上可能很难趋同。</p><p>问题6:没有计算机基础的前提下,是怎样的契机促使您走进了这个领域?</p><p>初生牛犊不怕虎吧。</p><p>2014年底的时候,我在北京的一个好朋友告诉我,他做前端工资12k,而我当时的工资才4k,年轻人谁不想多赚点钱呢,于是就有了转行的冲动。自学一个月HTML和CSS基础后,觉得自己还行是这块料,就下定决心辞职,拜师学艺三个月,把JavaScript学完了,再找了一个月工作,就完成了转行。</p><p>现在想想,除了初生牛犊不怕虎的精神外,更重要的原因是赶上了个好的时代,自己也抓住了机会。2015年移动互联网刚刚兴起,前端人才匮乏,企业对前端岗位需求量又大,我面试了40+家公司,拿到3个不错的 offer,就进入了前端的技术领域。</p><p>问题 7:最后,您对技术人员未来职业路线选择是否有一些好的意见和建议?</p><p>我在技术领域工作年限其实也不长,也就七年,谈不上什么建议,我分享一下我的观察和思考,不一定对。</p><p>我认为,技术人员未来职业线路大概有三种:</p><ul><li>第一种是,做了几年技术后,就对技术不感兴趣了。有的人选择回老家干份轻松的工作,有的人选择考公务员、做老师之类的。其实每个人都有自己喜欢的事情,回老家或者转行也是一件挺好的事。</li><li>第二种是,一直在某个技术方向上深耕,成为了技术专家。我身边的同龄人很多这样的例子,这些人手里有货,走哪都不慌。</li><li>第三种是,技术出身转去做产品、做管理和创业了。他们多是大我一轮的前辈,他们本身在技术领域就很厉害,又能把技术领域的成功经验复制到其他领域,创造更大的价值。</li></ul>
大家开发 RN 都用什么?
https://segmentfault.com/a/1190000041324009
2022-01-22T10:49:39+08:00
2022-01-22T10:49:39+08:00
fitfish
https://segmentfault.com/u/timetravel
2
<p>如果你觉得大家填写的这份数据对你有用,你也可以把你的经验分享给大家:<br><a href="https://link.segmentfault.com/?enc=XEngbgSB9eoxr6oavPupsA%3D%3D.FJQPlRLOh1uJKwYzTHHSmpWeCubYTphgXqsgF%2FQKsrYIrxIAG5c%2Bptz2v8Vy3DbQkWwzA0LVKbypJckIH6Hyig%3D%3D" rel="nofollow">https://docs.qq.com/sheet/DQWdsZ0RORkpFQmVj?tab=BB08J2</a><br><img src="/img/bVcXyp5" alt="1" title="1"><br><img src="/img/bVcXyp7" alt="2" title="2"><br><img src="/img/bVcXyp8" alt="3" title="3"><br><img src="/img/bVcXyp9" alt="4" title="4"><br><img src="/img/bVcXyqa" alt="5" title="5"><br><img src="/img/bVcXyqb" alt="6" title="6"><br><img src="/img/bVcXyqc" alt="7" title="7"><br><img src="/img/bVcXyqd" alt="8" title="8"><br><img src="/img/bVcXyqe" alt="9" title="9"><br><img src="/img/bVcXyqf" alt="10" title="10"><br><img src="/img/bVcXyqg" alt="11" title="11"><br><img src="/img/bVcXyqh" alt="12" title="12"><br><img src="/img/bVcXyqi" alt="17" title="17"><br><img src="/img/bVcXyqj" alt="18" title="18"></p>
React Native 新架构是怎么工作的?
https://segmentfault.com/a/1190000041182092
2021-12-26T19:22:50+08:00
2021-12-26T19:22:50+08:00
fitfish
https://segmentfault.com/u/timetravel
3
<p>原文地址:<a href="https://link.segmentfault.com/?enc=do7ggNtWew7yPNuuyoTiQg%3D%3D.VJC0Qsx0arjCgtXj7VPQsn%2FpPs6oCICk1QkPsuEzqfk4runyyOWOghszOfFJt1J5dPdolFQl15qmcxm3lorwsA%3D%3D" rel="nofollow">https://reactnative.dev/docs/...</a></p><blockquote><p>译者前言:</p><p>目前新架构所依赖的 React 18 已经发了 beta 版,React Native 新架构面向生态库和核心开发者的文档也正式发布,React Native 团队成员 Kevin Gozali 也在最近一次访谈中谈到新架构离正式发版还差最后一步延迟初始化,而最后一步大约会在 2022 年上半年完成。种种迹象表明,React Native 新架构真的要来了。</p><p>后续也会通过极客时间专栏的形式和大家介绍新架构的使用方法、剖析架构原理、讲解实践方案。</p><p>由于时间仓促,如果有翻译不当之处还请大家指出,以下是正文部分。</p></blockquote><hr><p>本文档还在更新持续中,会从概念上介绍 React Native 新架构是如何工作的。目标读者包括生态库的开发者、核心贡献者和特别有好奇心的人。</p><blockquote>文档介绍了即将发布的新渲染器 Fabric 的架构。</blockquote><h2>Fabric</h2><p>Fabric 是 React Native 新架构的渲染系统,是从老架构的渲染系统演变而来的。核心原理是在 C++ 层统一更多的渲染逻辑,提升与宿主平台(host platforms)互操作性,并为 React Native 解锁更多能力。研发始于 2018 年和 2021年,Facebook 应用中的 React Native 用的就是新的渲染器。</p><p>该文档简介了新渲染器(new renderer)及其核心概念,它不包括平台细节和任何代码细节,它介绍了核心概念、初衷、收益和不同场景的渲染流程。</p><blockquote><p>名词解释:</p><p>宿主平台(Host platform):React Native 嵌入的平台,比如 Android、iOS、Windows、macOS。</p><p>Fabric 渲染器(Fabric Renderer):React Native 执行的 React 框架代码,和 React 在 Web 中执行代码是同一份。但是,React Native 渲染的是通用平台视图(宿主视图)而不是 DOM 节点(可以认为 DOM 是 Web 的宿主视图)。 Fabric 渲染器使得渲染宿主视图变得可行。Fabric 让 React 与各个平台直接通信并管理其宿主视图实例。 Fabric 渲染器存在于 JavaScript 中,并且它调用的是由 C++ 代码暴露的接口。在这篇文章中有更多关于 React 渲染器的信息。</p></blockquote><h3>新渲染器的初衷和收益</h3><p>开发新的渲染架构的初衷是为了更好的用户体验,而这种新体验是在老架构上是不可能实现的。比如:</p><ul><li>为了提升宿主视图(host views)和 React 视图(React views)的互操作性,渲染器必须有能力同步地测量和渲染 React 界面。在老架构中,React Native 布局是异步的,这导致在宿主视图中渲染嵌套的 React Native 视图,会有布局“抖动”的问题。</li><li>借助多优先级和同步事件的能力,渲染器可以提高用户交互的优先级,来确保他们的操作得到及时的处理。</li><li>React Suspense 的集成,允许你在 React 中更符合直觉地写请求数据代码。</li><li>允许你在 React Native 使用 React Concurrent 可中断渲染功能。</li><li>更容易实现 React Native 的服务端渲染。</li></ul><p>新架构的收益还包括,代码质量、性能、可扩展性。</p><ul><li>类型安全:代码生成工具(code generation)确保了 JS 和宿主平台两方面的类型安全。代码生成工具使用 JavaScript 组件声明作为唯一事实源,生成 C++ 结构体来持有 props 属性。不会因为 JavaScript 和宿主组件 props 属性不匹配而出现构建错误。</li><li>共享 C++ core:渲染器是用 C++ 实现的,其核心 core 在平台之间是共享的。这增加了一致性并且使得新的平台能够更容易采用 React Native。(译注:例如 VR 新平台)</li><li>更好的宿主平台互操作性:当宿主组件集成到 React Native 时,同步和线程安全的布局计算提升了用户体验(译注:没有异步的抖动)。这意味着那些需要同步 API 的宿主平台库,变得更容易集成了。</li><li>性能提升:新的渲染系统的实现是跨平台的,每个平台都从那些原本只在某个特定平台的实现的性能优化中,得到了收益。比如拍平视图层级,原本只是 Android 上的性能优化方案,现在 Android 和 iOS 都直接有了。</li><li>一致性:新的渲染系统的实现是跨平台的,不同平台之间更容易保持一致。</li><li>更快的启动速度:默认情况下,宿主组件的初始化是懒执行的。</li><li>JS 和宿主平台之间的数据序列化更少:React 使用序列化 JSON 在 JavaScript 和宿主平台之间传递数据。新的渲染器用 JSI(JavaScript Interface)直接获取 JavaScript 数据。</li></ul><blockquote><p>名词解释</p><p>JavaScript Interfaces (JSI):一个轻量级的 API,给在 C++ 应用中嵌入的 JavaScript 引擎用的。Fabric 使用它在 Fabric 的 C++ 核心和 React 之间进行通信。</p></blockquote><h2>渲染、提交和挂载</h2><p>React Native 渲染器通过一系列加工处理,将 React 代码渲染到宿主平台。这一系列加工处理就是渲染流水线(pipeline),它的作用是初始化渲染和 UI 状态更新。 接下来介绍的是渲染流水线,及其在各种场景中的不同之处。</p><p>(译注:pipeline 的原义是将计算机指令处理过程拆分为多个步骤,并通过多个硬件处理单元并行执行来加快指令执行速度。其具体执行过程类似工厂中的流水线,并因此得名。)</p><p>渲染流水线可大致分为三个阶段:</p><ul><li>渲染(Render):在 JavaScript 中,React 执行那些产品逻辑代码创建 React 元素树(React Element Trees)。然后在 C++ 中,用 React 元素树创建 React 影子树(React Shadow Tree)。</li><li>提交(Commit):在 React 影子树完全创建后,渲染器会触发一次提交。这会将 React 元素树和新创建的 React 影子树的提升为“下一棵要挂载的树”。 这个过程中也包括了布局信息计算。</li><li>挂载(Mount):React 影子树有了布局计算结果后,它会被转化为一个宿主视图树(Host View Tree)。</li></ul><blockquote><p>名词解释</p><p>React 元素树(React Element Trees):React 元素树是通过 JavaScript 中的 React 创建的,该树由一系类 React 元素组成。一个 React 元素就是一个普通的 JavaScript 对象,它描述了应该在屏幕中展示什么。一个元素包括属性 props、样式 styles、子元素 children。React 元素分为两类:React 复合组件实例(React Composite Components)和 React 宿主组件(React Host Components)实例,并且它只存在于 JavaScript 中。</p><p>React 影子树(React Shadow Tree):React 影子树是通过 Fabric 渲染器创建的,树由一系列 React 影子节点组成。一个 React 影子节点是一个对象,代表一个已经挂载的 React 宿主组件,其包含的属性 props 来自 JavaScript。它也包括布局信息,比如坐标系 x、y,宽高 width、height。在新渲染器 Fabric 中,React 影子节点对象只存在于 C++ 中。而在老架构中,它存在于手机运行时的堆栈中,比如 Android 的 JVM。</p><p>宿主视图树(Host View Tree):宿主视图树就是一系列的宿主视图。宿主平台有 Android 平台、iOS 平台等等。在 Android 上,宿主视图就是 <code>android.view.ViewGroup</code>实例、 <code>android.widget.TextView</code>实例等等。宿主视图就像积木一样地构成了宿主视图树。每个宿主视图的大小和坐标位置基于的是 <code>LayoutMetrics</code>,而 <code>LayoutMetrics</code>是通过布局引擎 Yoga 计算出来的。宿主视图的样式和内容信息,是从 React 影子树中得到的。</p><p>渲染流水线的各个阶段可能发生在不同的线程中,更详细的信息可以参考线程模型部分。</p></blockquote><p><img src="/img/remote/1460000041182094" alt="React Native renderer Data flow" title="React Native renderer Data flow"></p><p>渲染流水线存在三种不同场景:</p><ol><li>初始化渲染</li><li>React 状态更新</li><li>React Native 渲染器的状态更新</li></ol><hr><h3>初始化渲染</h3><h4>渲染阶段</h4><p>想象一下你准备渲染一个组件:</p><pre><code class="jsx">function MyComponent() {
return (
<View>
<Text>Hello, World</Text>
</View>
);
}
// <MyComponent /></code></pre><p>在上面的例子中,<code> <MyComponent /></code>是 React 元素。React 会将 React 元素简化为最终的 React 宿主组件。每一次都会递归地调用函数组件 MyComponet ,或类组件的 render 方法,直至所有的组件都被调用过。现在,你拥有一棵 React 宿主组件的 React 元素树。</p><p><img src="/img/remote/1460000041182095" alt="Phase one: render" title="Phase one: render"></p><blockquote><p>名词解释:</p><p>React 组件(React Component):React 组件就是 JavaScript 函数或者类,描述如何创建 React 元素。</p><p>React 复合组件(React Composite Components):React 组件的 render 方法中,包括其他 React 复合组件和 React 宿主组件。(译注:复合组件就是开发者声明的组件)</p><p>React 宿主组件(React Host Components):React 组件的视图是通过宿主视图,比如 <code><View></code>、<code><Text></code>,实现的。在 Web 中,ReactDOM 的宿主组件就是 <code><p></code>标签、<code><div></code>标签代表的组件。</p></blockquote><p>在元素简化的过程中,每调用一个 React 元素,渲染器同时会同步地创建 React 影子节点。这个过程只发生在 React 宿主组件上,不会发生在 React 复合组件上。比如,一个 <code><View></code>会创建一个 <code>ViewShadowNode</code> 对象,一个<code><Text></code>会创建一个<code>TextShadowNode</code>对象。注意,<code><MyComponent></code>并没有直接对应的 React 影子节点。</p><p>在 React 为两个 React 元素节点创建一对父子关系的同时,渲染器也会为对应的 React 影子节点创建一样的父子关系。这就是影子节点的组装方式。</p><p><strong>其他细节</strong></p><ul><li>创建 React 影子节点、创建两个影子节点的父子关系的操作是同步的,也是线程安全的。该操作的执行是从 React(JavaScript)到渲染器(C++)的,大部分情况下是在 JavaScript 线程上执行的。(译注:后面线程模型有解释)</li><li>React 元素树和元素树中的元素并不是一直存在的,它只一个当前视图的描述,而最终是由 React “fiber” 来实现的。每一个 “fiber” 都代表一个宿主组件,存着一个 C++ 指针,指向 React 影子节点。这些都是因为有了 JSI 才有可能实现的。学习更多关于 “fibers” 的资料参考<a href="https://link.segmentfault.com/?enc=MwOEuyil7ylMJAf%2BObzoMQ%3D%3D.Nvq5GJIEbiYN%2FCSVzQUd41eIruHIVCCyTxaecP0pMiQ8NiXUpupzMrsR%2F7XHHX9yy0ZqVYGgWoTsdfrJUZcY9FuZIVQe8Uq%2Fs3Iivdhr7c0%3D" rel="nofollow">这篇文档</a>。</li><li>React 影子树是不可变的。为了更新任意的 React 影子节点,渲染器会创建了一棵新的 React 影子树。为了让状态更新更高效,渲染器提供了 clone 操作。更多细节可参考后面的 React 状态更新部分。</li></ul><p>在上面的示例中,各个渲染阶段的产物如图所示:</p><p><img src="/img/remote/1460000041182096" alt="Step one" title="Step one"></p><h4>提交阶段</h4><p>在 React 影子树创建完成后,渲染器触发了一次 React 元素树的提交。<img src="/img/remote/1460000041182097" alt="Phase two: commit" title="Phase two: commit"></p><p>提交阶段(Commit Phase)由两个操作组成:布局计算和树的提升。</p><ul><li><strong>布局计算(Layout Calculation)</strong>:这一步会计算每个 React 影子节点的位置和大小。在 React Native 中,每一个 React 影子节点的布局都是通过 Yoga 布局引擎来计算的。实际的计算需要考虑每一个 React 影子节点的样式,该样式来自于 JavaScript 中的 React 元素。计算还需要考虑 React 影子树的根节点的布局约束,这决定了最终节点能够拥有多少可用空间。</li></ul><p><img src="/img/remote/1460000041182098" alt="Step two" title="Step two"></p><ul><li><strong>树提升,从新树到下一棵树(Tree Promotion,New Tree → Next Tree)</strong>:这一步会将新的 React 影子树提升为要挂载的下一棵树。这次提升代表着新树拥有了所有要挂载的信息,并且能够代表 React 元素树的最新状态。下一棵树会在 UI 线程下一个“tick”进行挂载。(译注:tick 是 CUP 的最小时间单元)</li></ul><p><strong>更多细节</strong></p><ul><li>这些操作都是在后台线程中异步执行的。</li><li>绝大多数布局计算都是 C++ 中执行,只有某些组件,比如 Text、TextInput 组件等等,的布局计算是在宿主平台执行的。文字的大小和位置在每个宿主平台都是特别的,需要在宿主平台层进行计算。为此,Yoga 布局引擎调用了宿主平台的函数来计算这些组件的布局。</li></ul><h4>挂载阶段</h4><p><img src="/img/remote/1460000041182099" alt="Phase two: commit" title="Phase two: commit"></p><p>挂载阶段(Mount Phase)会将已经包含布局计算数据的 React 影子树,转换为以像素形式渲染在屏幕中的宿主视图树。请记住,这棵 React 元素树看起来是这样的:</p><pre><code class="jsx"><View>
<Text>Hello, World</Text>
</View></code></pre><p>站在更高的抽象层次上,React Native 渲染器为每个 React 影子节点创建了对应的宿主视图,并且将它们挂载在屏幕上。在上面的例子中,渲染器为<code><View></code> 创建了<code>android.view.ViewGroup</code> 实例,为 <code><Text></code> 创建了文字内容为“Hello World”的 <code>android.widget.TextView</code>实例 。iOS 也是类似的,创建了一个 <code>UIView</code> 并调用 <code>NSLayoutManager</code> 创建文本。然后会为宿主视图配置来自 React 影子节点上的属性,这些宿主视图的大小位置都是通过计算好的布局信息配置的。</p><p><img src="/img/remote/1460000041182100" alt="Step two" title="Step two"></p><p>更详细地说,挂载阶段由三个步骤组成:</p><ul><li><strong>树对比(Tree Diffing):</strong> 这个步骤完全用的是 C++ 计算的,会对比“已经渲染的树”(previously rendered tree)和”下一棵树”之间的差异。计算的结果是一系列宿主平台上的原子变更操作,比如 <code>createView</code>, <code>updateView</code>, <code>removeView</code>, <code>deleteView</code> 等等。在这个步骤中,还会将 React 影子树拍平,来避免不必要的宿主视图创建。关于视图拍平的算法细节可以在后文找到。</li><li><strong>树提升,从下一棵树到已渲染树(Tree Promotion,Next Tree → Rendered Tree):</strong>在这个步骤中,会自动将“下一棵树”提升为“先前渲染的树”,因此在下一个挂载阶段,树的对比计算用的是正确的树。</li><li><strong>视图挂载(View Mounting):</strong>这个步骤会在对应的原生视图上执行原子变更操作,该步骤是发生在原生平台的 UI 线程的。</li></ul><p><strong>更多细节</strong></p><ul><li>挂载阶段的所有操作都是在 UI 线程同步执行的。如果提交阶段是在后台线程执行,那么在挂载阶段会在 UI 线程的下一个“tick”执行。另外,如果提交阶段是在 UI 线程执行的,那么挂载阶段也是在 UI 线程执行。</li><li>挂载阶段的调度和执行很大程度取决于宿主平台。例如,当前 Android 和 iOS 挂载层的渲染架构是不一样的。</li><li>在初始化渲染时,“先前渲染的树”是空的。因此,树对比(tree diffing)步骤只会生成一系列仅包含创建视图、设置属性、添加视图的变更操作。而在接下来的 React 状态更新场景中,树对比的性能至关重要。</li><li>在当前生产环境的测试中,在视图拍平之前,React 影子树通常由大约 600-1000 个 React 影子节点组成。在视图拍平之后,树的节点数量会减少到大约 200 个。在 iPad 或桌面应用程序上,这个节点数量可能要乘个 10。</li></ul><h3>React 状态更新</h3><p>接下来,我们继续看 React 状态更新时,渲染流水线(render pipeline)的各个阶段是什么样的。假设你在初始化渲染时,渲染的是如下组件:</p><pre><code class="jsx">function MyComponent() {
return (
<View>
<View
style={{ backgroundColor: 'red', height: 20, width: 20 }}
/>
<View
style={{ backgroundColor: 'blue', height: 20, width: 20 }}
/>
</View>
);
}</code></pre><p>应用我们在初始化渲染部分学的知识,你可以得到如下的三棵树:</p><p><img src="/img/remote/1460000041182101" alt="Render pipeline 4" title="Render pipeline 4"></p><p>请注意,节点 3 对应的宿主视图背景是<strong>红的</strong>,而<strong>节点 4 </strong>对应的宿主视图背景是<strong>蓝的</strong>。假设 JavaScript 的产品逻辑是,将第一个内嵌的<code><View></code>的背景颜色由红色改为黄色。新的 React 元素树看起来大概是这样:</p><pre><code class="jsx"><View>
<View
style={{ backgroundColor: 'yellow', height: 20, width: 20 }}
/>
<View
style={{ backgroundColor: 'blue', height: 20, width: 20 }}
/>
</View></code></pre><p><strong>React Native 是如何处理这个更新的?</strong></p><p>从概念上讲,当发生状态更新时,为了更新已经挂载的宿主视图,渲染器需要直接更新 React 元素树。 但是为了线程的安全,React 元素树和 React 影子树都必须是不可变的(immutable)。这意味着 React 并不能直接改变当前的 React 元素树和 React 影子树,而是必须为每棵树创建一个包含新属性、新样式和新子节点的新副本。</p><p>让我们继续探究状态更新时,渲染流水线的各个阶段发生了什么。</p><h4>渲染阶段</h4><p><img src="/img/remote/1460000041182095" alt="Phase one: render" title="Phase one: render"></p><p>React 要创建了一个包含新状态的新的 React 元素树,它就要复制所有变更的 React 元素和 React 影子节点。 复制后,再提交新的 React 元素树。</p><p>React Native 渲染器利用结构共享的方式,将不可变特性的开销变得最小。为了更新 React 元素的新状态,从该元素到根元素路径上的所有元素都需要复制。 <strong>但 React 只会复制有新属性、新样式或新子元素的 React 元素</strong>,任何没有因状态更新发生变动的 React 元素都不会复制,而是由新树和旧树共享。</p><p>在上面的例子中,React 创建新树使用了这些操作:</p><ol><li>CloneNode(<strong>Node 3</strong>, {backgroundColor: 'yellow'}) → <strong>Node 3'</strong></li><li>CloneNode(<strong>Node 2</strong>) → <strong>Node 2'</strong></li><li>AppendChild(<strong>Node 2'</strong>, <strong>Node 3'</strong>)</li><li>AppendChild(<strong>Node 2'</strong>, <strong>Node 4</strong>)</li><li>CloneNode(<strong>Node 1</strong>) → <strong>Node 1'</strong></li><li>AppendChild(<strong>Node 1'</strong>, <strong>Node 2'</strong>)</li></ol><p>操作完成后,<strong>节点 1'(Node 1')</strong>就是新的 React 元素树的根节点。我们用 <strong>T</strong> 代表“先前渲染的树”,用 <strong>T'</strong> 代表“新树”。</p><p><img src="/img/remote/1460000041182102" alt="Render pipeline 5" title="Render pipeline 5"></p><p>注意节点 4 在 <strong>T</strong> and <strong>T'</strong> 之间是共享的。结构共享提升了性能并减少了内存的使用。</p><h4>提交阶段</h4><p><img src="/img/remote/1460000041182097" alt="Phase two: commit" title="Phase two: commit"></p><p>在 React 创建完新的 React 元素树和 React 影子树后,需要提交它们。</p><ul><li><strong>布局计算(Layout Calculation):</strong>状态更新时的布局计算,和初始化渲染的布局计算类似。一个重要的不同之处是布局计算可能会导致共享的 React 影子节点被复制。这是因为,如果共享的 React 影子节点的父节点引起了布局改变,共享的 React 影子节点的布局也可能发生改变。</li><li><strong>树提升(Tree Promotion ,New Tree → Next Tree):</strong> 和初始化渲染的树提升类似。</li><li><p><strong>树对比(Tree Diffing):</strong> 这个步骤会计算“先前渲染的树”(<strong>T</strong>)和“下一棵树”(<strong>T'</strong>)的区别。计算的结果是原生视图的变更操作。</p><ul><li>在上面的例子中,这些操作包括:<code>UpdateView(**'Node 3'**, {backgroundColor: 'yellow'})</code></li></ul></li></ul><h4>挂载阶段</h4><p><img src="/img/remote/1460000041182099" alt="Phase three: mount" title="Phase three: mount"></p><ul><li><strong>树提升(Tree Promotion ,Next Tree → Rendered Tree):</strong> 在这个步骤中,会自动将“下一棵树”提升为“先前渲染的树”,因此在下一个挂载阶段,树的对比计算用的是正确的树。</li><li><strong>视图挂载(View Mounting):</strong>这个步骤会在对应的原生视图上执行原子变更操作。在上面的例子中,只有<strong>视图3(View 3)</strong>的背景颜色会更新,变为黄色。</li></ul><p><img src="/img/remote/1460000041182103" alt="Render pipeline 6" title="Render pipeline 6"></p><h3>React Native 渲染器状态更新</h3><p>对于影子树中的大多数信息而言,React 是唯一所有方也是唯一事实源。并且所有来源于 React 的数据都是单向流动的。</p><p>但有一个例外。这个例外是一种非常重要的机制:C++ 组件可以拥有状态,且该状态可以不直接暴露给 JavaScript,这时候 JavaScript (或 React)就不是唯一事实源了。通常,只有复杂的宿主组件才会用到 C++ 状态,绝大多数宿主组件都不需要此功能。</p><p>例如,ScrollView 使用这种机制让渲染器知道当前的偏移量是多少。偏移量的更新是宿主平台的触发,具体地说是 ScrollView 组件。这些偏移量信息在 React Native 的 <a href="https://link.segmentfault.com/?enc=9QhYmginTGqMPxPprl2waQ%3D%3D.rO8IkbOM%2FIHocPsJLX06ylkSWZuigpbO1ZfRCdZFHJY%2BHc7FsG9BQdKBhxmFZ82AZXC10KaRT6MRZu9K2Z1nqlfQJk6%2BrNVJG8VGe3Fe%2BbE%3D" rel="nofollow">measure</a> 等 API 中有用到。 因为偏移量数据是由 C++ 状态持有的,所以源于宿主平台更新,不影响 React 元素树。</p><p>从概念上讲,C++ 状态更新类似于我们前面提到的 React 状态更新,但有两点不同:</p><ul><li>因为不涉及 React,所以跳过了“渲染阶段”(Render phase)。</li><li>更新可以源自和发生在任何线程,包括主线程。</li></ul><p><img src="/img/remote/1460000041182097" alt="Phase two: commit" title="Phase two: commit"></p><p>提交阶段(Commit Phase):在执行 C++ 状态更新时,会有一段代码把影子节点<strong>(N)</strong>的 C++ 状态设置为值 <strong>S</strong>。React Native 渲染器会反复尝试获取 <strong>N</strong> 的最新提交版本,并使用新状态 <strong>S</strong> 复制它 ,并将新的影子节点 <strong>N'</strong> 提交给影子树。如果 React 在此期间执行了另一次提交,或者其他 C++ 状态有了更新,本次 C++ 状态提交失败。这时渲染器将多次重试 C++ 状态更新,直到提交成功。这可以防止真实源的冲突和竞争。</p><p><img src="/img/remote/1460000041182099" alt="Phase three: mount" title="Phase three: mount"></p><p>挂载阶段(Mount Phase)实际上与 React 状态更新的挂载阶段相同。渲染器仍然需要重新计算布局、执行树对比等操作。详细步骤在前面已经讲过了。</p><h2>跨平台实现</h2><p><strong>React Native 渲染器使用 C++ core 渲染实现了跨平台共享。</strong></p><p>在上一代 React Native 渲染器中,React 影子树、布局逻辑、视图拍平算法是在各个平台单独实现的。当前的渲染器的设计上采用的是跨平台的解决方案,共享了核心的 C++ 实现。</p><p>React Native 团队计划将动画系统加入到渲染系统中,并将 React Native 的渲染系统扩展到新的平台,例如 Windows、游戏机、电视等等。</p><p>使用 C++ 作为核心渲染系统有几个有点。首先,单一实现降低了开发和维护成本。其次,它提升了创建 React 影子树的性能,同时在 Android 上,也因为不再使用 JNI for Yoga,降低了 Yoga 渲染引擎的开销,布局计算的性能也有所提升。最后,每个 React 影子节点在 C++ 中占用的内存,比在 Kotlin 或 Swift 中占用的要小。</p><blockquote><p>名词解释</p><p>Java Native Interface (JNI):一个用 Java 写的 API,用于在 Java 中写 native(译注:指调用 C++) 方法。作用是实现 Fabric 的 C++ 核心和 Android 的通信。</p></blockquote><p>React Native 团队还使用了强制不可变的 C++ 特性,来确保并发访问时共享资源即便不加锁保护,也不会有问题。</p><p>但在 Android 端还有两种例外,渲染器依然会有 JNI 的开销:</p><ul><li>复杂视图,比如 Text、TextInput 等,依然会使用 JNI 来传输属性 props。</li><li>在挂载阶段依然会使用 JNI 来发送变更操作。</li></ul><p>React Native 团队在探索使用 <code>ByteBuffer</code> 序列化数据这种新的机制,来替换 <code>ReadableMap</code>,减少 JNI 的开销。目标是将 JNI 的开销减少 35~50%。</p><p>渲染器提供了 C++ 与两边通信的 API:</p><ul><li><strong>(i)</strong> 与 React 通信</li><li><strong>(ii)</strong> 与宿主平台通信</li></ul><p>关于 <strong>(i) </strong>React 与渲染器的通信,包括<strong>渲染(render)</strong> React 树和监听<strong>事件(event)</strong>,比如 <code>onLayout</code>、<code>onKeyPress</code>、touch 等。</p><p>关于 <strong>(ii) </strong> React Native 渲染器与宿主平台的通信,包括在屏幕上<strong>挂载(mount)</strong>宿主视图,包括 create、insert、update、delete 宿主视图,和监听用户在宿主平台产生的<strong>事件(event)</strong>。</p><p><img src="/img/remote/1460000041182104" alt="Cross-platform implementation diagram" title="Cross-platform implementation diagram"></p><h2>视图拍平</h2><p><strong>视图拍平(View Flattening)是 React Native 渲染器避免布局嵌套太深的优化手段。</strong></p><p>React API 在设计上希望通过组合的方式,实现组件声明和重用,这为更简单的开发提供了一个很好的模型。但是在实现中,API 的这些特性会导致一些 React 元素会嵌套地很深,而其中大部分 React 元素节点只会影响视图布局,并不会在屏幕中渲染任何内容。这就是所谓的<strong>“只参与布局”</strong>类型节点。</p><p>从概念上讲,React 元素树的节点数量和屏幕上的视图数量应该是 1:1 的关系。但是,渲染一个很深的“只参与布局”的 React 元素会导致性能变慢。</p><p>举个很常见的例子,例子中“只参与布局”视图导致了性能损耗。</p><p>想象一下,你要渲染一个标题。你有一个应用,应用中拥有外边距 <code>ContainerComponent</code>的容器组件,容器组件的子组件是 <code>TitleComponent</code> 标题组件,标题组件包括一个图片和一行文字。React 代码示例如下:</p><pre><code class="jsx">function MyComponent() {
return (
<View> // ReactAppComponent
<View style={{margin: 10}} /> // ContainerComponent
<View style={{margin: 10}}> // TitleComponent
<Image {...} />
<Text {...}>This is a title</Text>
</View>
</View>
</View>
);
}</code></pre><p>React Native 在渲染时,会生成以下三棵树:</p><p><img src="/img/remote/1460000041182105" alt="Diagram one" title="Diagram one"></p><p>注意视图 2 和视图 3 是“只参与布局”的视图,因为它们在屏幕上渲染只是为了提供 10 像素的外边距。</p><p>为了提升 React 元素树中“只参与布局”类型的性能,渲染器实现了一种视图拍平的机制来合并或拍平这类节点,减少屏幕中宿主视图的层级深度。该算法考虑到了如下属性,比如 <code>margin</code>, <code>padding</code>, <code>backgroundColor</code>, <code>opacity</code>等等。</p><p>视图拍平算法是渲染器的对比(diffing)阶段的一部分,这样设计的好处是我们不需要额外的 CUP 耗时,来拍平 React 元素树中“只参与布局”的视图。此外,作为 C++ 核心的一部分,视图拍平算法默认是全平台共用的。</p><p>在前面的例子中,视图 2 和视图 3 会作为“对比算法”(diffing algorithm)的一部分被拍平,而它们的样式结果会被合并到视图 1 中。</p><p><img src="/img/remote/1460000041182106" alt="Diagram two" title="Diagram two"></p><p>虽然,这种优化让渲染器少创建和渲染两个宿主视图,但从用户的角度看屏幕内容没有任何区别。</p><h2>线程模型</h2><p><strong>React Native 渲染器在多个线程之间分配渲染流水线(render pipeline)任务。</strong></p><p>接下来我们会给线程模型下定义,并提供一些示例来说明渲染流水线的线程用法。</p><p>React Native 渲染器是线程安全的。从更高的视角看,在框架内部线程安全是通过不可变的数据结果保障的,其使用的是 C++ 的 const correctness 特性。这意味着,在渲染器中 React 的每次更新都会重新创建或复制新对象,而不是更新原有的数据结构。这是框架把线程安全和同步 API 暴露给 React 的前提。</p><p>渲染器使用三个不同的线程:</p><ul><li>UI 线程(主线程):唯一可以操作宿主视图的线程。</li><li>JavaScript 线程:这是执行 React 渲染阶段的地方。</li><li>后台线程:专门用于布局的线程。</li></ul><p>让我们回顾一下每个阶段支持的执行场景:</p><p><img src="/img/remote/1460000041182107" alt="Threading model symbols" title="Threading model symbols"></p><h4>渲染场景</h4><h5>在后台线程中渲染</h5><p>这是最常见的场景,大多数的渲染流水线发生在 JavaScript 线程和后台线程。</p><p><img src="/img/remote/1460000041182108" alt="Threading model use case one" title="Threading model use case one"></p><h5>在主线程中渲染</h5><p>当 UI 线程上有高优先级事件时,渲染器能够在 UI 线程上同步执行所有渲染流水线。</p><p><img src="/img/remote/1460000041182109" alt="Threading model use case two" title="Threading model use case two"></p><h5>默认或连续事件中断</h5><p>在这个场景中,UI 线程的低优先级事件中断了渲染步骤。React 和 React Native 渲染器能够中断渲染步骤,并把它的状态和一个在 UI 线程执行的低优先级事件合并。在这个例子中渲染过程会继续在后台线程中执行。</p><p><img src="/img/remote/1460000041182110" alt="Threading model use case three" title="Threading model use case three"></p><h5>不相干的事件中断</h5><p>渲染步骤是可中断的。在这个场景中, UI 线程的高优先级事件中断了渲染步骤。React 和渲染器是能够打断渲染步骤的,并把它的状态和 UI 线程执行的高优先级事件合并。在 UI 线程渲染步骤是同步执行的。</p><p><img src="/img/remote/1460000041182111" alt="Threading model use case four" title="Threading model use case four"></p><p><strong>来自 JavaScript 线程的后台线程批量更新</strong></p><p>在后台线程将更新分派给 UI 线程之前,它会检查是否有新的更新来自 JavaScript。 这样,当渲染器知道新的状态要到来时,它就不会直接渲染旧的状态。</p><p><img src="/img/remote/1460000041182112" alt="Threading model use case five" title="Threading model use case five"></p><h5>C++ 状态更新</h5><p>更新来自 UI 线程,并会跳过渲染步骤。更多细节请参考 React Native 渲染器状态更新。</p><p><img src="/img/remote/1460000041182113" alt="Threading model use case six" title="Threading model use case six"></p>
React Native迎来重大架构升级,性能将大幅提升
https://segmentfault.com/a/1190000040403414
2021-07-26T13:59:50+08:00
2021-07-26T13:59:50+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<p>7 月 14 日,React Native 核心团队的 Joshua Gross 在 Twitter 说,RN 的新架构已经在 Facebook 内部落地了,并且99%的代码已经开源。这次的架构升级“蓄谋已久”,Joshua 说他们从 2018 年 1 月就开始规划了。<br><img src="/img/bVcTGV3" alt="image.png" title="image.png"><br>Facebook 曾在 2018 年 6 月宣布了大规模 重构 RN 的计划和路线图,整个的重构目的是为了让 RN 更轻量化、更适应混合开发,接近甚至达到原生的体验。具体包括以下几个方面:<br>改变线程模型。UI 更新不再同时需要在三个不同的线程上触发执行,而是可以在任意线程上同步调用 JavaScript 进行优先更新,同时将低优先级工作推出主线程,以便保持对 UI 的响应。<br>引入异步渲染能力,允许多个渲染并简化异步数据处理。<br>简化 JSBridge,让它更快更轻量。<br>这次的架构升级对于 React Native 意义重大,按照官方的说法,这将大幅度提升 RN 的性能。自 RN 发布以来,它大致经历了如下几次的版本迭代(图片来自京东熊文源 GMTC PPT),这一次主要是解决诟病已久的性能问题。<br><img src="/img/bVcTGV6" alt="image.png" title="image.png"><br>为了进一步了解 RN 这次架构迭代的细节,InfoQ 记者采访了 58 同城资深前端工程师,58RN、Hybrid 框架的负责人蒋宏伟。以下为具体内容。<br>InfoQ:能给大家介绍下你理解的这次架构升级吗?新的 Fabric 渲染引擎是不是会大幅度提升 RN 的性能?<br>蒋宏伟:首先说明一下,Fabric 不是渲染引擎,是新架构 UI 渲染部分的代号。React Native 新架构包括三个关键部分:JSI 、TurboModules 和 Fabric。JSI 全称是 JavaScript Interface,代替的是原来的 Bridge。通过 JS 调用 C++,C ++ 调用 Java/OC 的方式,实现了 JS 和 Java/OC 之间的相互操作的。<br>TurboModules 是新架构 API 部分的代号。得益于 JSI,JavaScript 可以直接调用 Native 模块的方法。类似于在浏览器中,JavaScript 调用获取经纬度方法,实际调用的是 C++ 底层的获取方法。<br>Fabric 是新架构 UI 渲染部分的代号。得益于 JSI,JavaScript 可以直接操作 Native 的组件,渲染 UI。类似于,在浏览器中,JS 调用 createElement 创建 div 元素,并通过 C++ 底层渲染 UI。<br>根据现有的性能报告来看,新架构的性能大概提升了一个数量级。这里的性能指的是 API、UI 的操作性能,对首屏性能的影响还有待进一步评估。<br>InfoQ:新的架构代码全量放到 GitHub 了吗?你们团队计划升级吗?<br>蒋宏伟:Facebook 内部落地的新架构代码并未完全放到 GitHub。目前,JSI 和 TurboModules 部分已经可以使用,Fabric 部分还要等上一段时间。此外,在新架构的生态方面,比如导航、动画等,估计会有很多变化。<br>我们今年是有升级计划的,也非常期望能够快点用上 RN 新架构。根据我们以往的升级经验,最需要关心的是新、旧版本兼容性问题。我们内部有 9 个 App,300+ 的项目需要迁移,既需要自动化迁移工具,也需要业务开发和测试同学的配合,还需要一套逐步扩量的方案。<br>InfoQ:大家经常会拿 RN 和 Flutter 做对比,2019 年 GMTC 上,京东架构师熊文源说,在启动性能上,RN 稍微优于 Flutter,但渲染方面明显不如 Flutter,这是 RN 整个框架的瓶颈。这次升级过后,你会怎么评价两个框架呢?<br>蒋宏伟:这次升级过后,RN 在性能上能够追平 Flutter。首先,JavaScript 和 Dart 语言上都支持了 AOT 预编译,打个平手。其次,JavaScript 和 Dart 和底层交互都是通过 C++ 进行的,也是打个平手。最后,RN 原生组件绘制有平台的优化加成, 相对于 Flutter 自绘引擎绘制,可能还会好上一些。<br>其他方面,萝卜青菜各有所爱,前端同学会更喜欢 RN 一些,客户端同学更喜欢 Flutter 一些。<br>InfoQ:你们有调研过 Flutter 吗?<br>蒋宏伟:我们内部其实有很多 App 在用 Flutter,也开源了 Flutter Fair UI& 模板动态化框架,和 Magpie 开发的工具流。但 58 同城、安居客这种超级 App 没有用 Flutter,主要担心的还是包体积大小和启动内存。<br>InfoQ:从你视角看,决定跨端框架发展的关键因素是什么?跨端和原生的解决方案之间,未来会是一种怎么样的动态平衡?<br>蒋宏伟:跨端框架发展的关键因素是净收益的大小。从框架开发者的角度讲,Facebook 内部有 1000+ RN 页面,跨平台带来的净收益肯定很不错。Flutter 我有些不确定,这决定于 Google 的 Fuchsia 操作系统能否成功。从框架使用者的角度讲,生态起不来的,比如 Weex、NativeScript,开发成本太高,净收益可能为负的,这也会反过来制约框架的发展。<br>有原生就有跨端,二者会一直并存,但跨端方案的市场份额会变的更大。原生解决方案更多是在一些创新的、基础的场景中,比如短视频、VR 或者跨端基础设施。跨端解决方案,比如 Hybrid、小程序、RN、Flutter 等等,会更加成熟,使用的场景也会越多。又因为能够节约开发成本,在现有的场景中会被更多的使用。</p><p>京东架构师熊文源曾经在GMTC详细分享过RN的新架构,如果你想看他的Slides的话,可以在视频号给我私信,我单独发你。</p><p>来广营小盖<br>将在07月31日 09:00 直播</p><p>预约<br>视频号<br>如果你对这次架构升级有其他的增量信息,欢迎在评论区给我留言。</p>
React Native 无限列表的优化与实践
https://segmentfault.com/a/1190000040319964
2021-07-09T11:30:20+08:00
2021-07-09T11:30:20+08:00
fitfish
https://segmentfault.com/u/timetravel
2
<p>导语<br>本文介绍了在使用 React Native 开发过程中,如何对无限列表组件进行技术选型,如何使用RecyclerListView组件对无限列表进行性能优化,如何解决无限列表与标签页搭配使用时的内存优化与手势重叠的问题,希望对大家有所启发。</p><p>背景<br>对于分类信息流形态的产品,用户通过左右滑动切换分类,通过不断上滑来浏览更多的信息。</p><p><img src="/img/bVcTldu" alt="WeChata137821f0492646a08d5fd92e99479bf.png" title="WeChata137821f0492646a08d5fd92e99479bf.png"></p><p>用标签页(Tabs)实现切换分类,用无限列表(List)实现上滑浏览<br>手势上滑,页面向上滚动,展示更多列表项(List Item)<br>手势左滑,页面向左滚动,展示右边的列表(蓝色)</p><p>因为 React Native(RN) 可以用较低的成本,同时满足用户体验、快速迭代,和跨App开发上线的要求。所以,对于分类信息流形态的产品技术选型使用的是RN。在使用 RN 开发首页的过程中,我们填过很多坑,希望这些填坑经验,对读者有借鉴意义。<br>第一,RN 官方提供的无限列表(ListView/FlatList)性能太差,一直被业内吐槽。通过实践对比,我们选择了内存管理效率更优的第三方组件——RecyclerListView。</p><p>第二,RecyclerListView 需要知道每个列表项的高度,才能正确渲染。如果,列表项高度不确定,怎么处理?</p><p>第三,标签页和无限列表组合使用时,会遇到一些问题。首先,标签页中有多个无限列表,怎样有效管理内存?其次,标签页可以左右滑动,无限列表中也有左右滚动的内容组件,二者手势区域重叠时,如何指定组件优先处理?</p><p>列表的技术选型</p><ol><li>ListView</li></ol><p>在实践开发分类信息流形态的产品过程中,我们开始尝试过使用 RN,版本是 0.28。当时,无限列表用的是官方提供的 ListView 。ListView 的列表项始终不会被销毁,这会导致内存不断增加,导致卡顿。前100 条信息滚动非常流畅,200 条时就开始卡顿,到 1000 条时就基本就滑不动了。当时,也没有特别好的解决方案,只能在产品上进行妥协,将无限列表降级为有限列表。</p><ol start="2"><li>FlatList<br>FlatList 是在 RN 0.43 版本新增的,拥有内存回收的功能,可以用来实现无限列表。我们第一时间就跟进了,把 RN 版本进行升级。虽然,FlatList 可以实现无限列表,但体验上总归还是有所欠缺的。FlatList 在 iOS 表现很流畅,但在 Android 某些机型上会有略有卡顿。</li><li>RecyclerListView<br>在实践开发中,我们技术选型还尝试采用了 RecyclerListView。RecyclerListView 实现了内存的复用,性能也是更好。无论是iOS 还是 Android 都表现的很流畅。</li></ol><p>流畅度对比<br>衡量流畅度的关键指标是帧率,帧率越高越流畅,越低越卡顿。我们用 RecyclerListView 和 FlatList 分别实现了相同功能的无限列表,在Android 手机中进行了测试,滚动帧率如下。</p><p><img src="/img/bVcTldG" alt="WeChat647a25498ff0426b6f4a4229fad9b67b.png" title="WeChat647a25498ff0426b6f4a4229fad9b67b.png"></p><p>滚动帧率对比(以Android OPPO R9 为例) <br><img src="/img/bVcTldL" alt="WeChat5e918078079759f4856cb3cca5d94973.png" title="WeChat5e918078079759f4856cb3cca5d94973.png"></p><p>实现原理对比<br>ListView 、FlatList、RecyclerListView 都是 RN 的列表组件,为什么它们之间性能差距这么大?我们对其实现原理进行了一些研究。</p><ol><li>ListView <br>ListView的实现思路比较简单,当用户上滑加载新的列表内容时,会不断地新增列表项。每次新增,都会导致内存增加,增加到一定程度后,可使用的内存空间不足,页面就会出现卡顿。</li><li>FlatList <br>FlatList取了个巧,既然用户只能看到手机屏幕里的内容,那么只用将用户看到的(可视区域)和即将看到的(靠近可视区域)部分渲染出来就行了。而用户看不到的地方(远离可视区域),就删掉,用空白元素占位就行。这样,空白区域的内存就得到了释放。<br>要实现无限加载,必须要考虑如何高效利用内存。FlatList “删除一个,新增一个” 是一个思路。RecyclerListView “结构类似,改改再用” 是另一个思路。</li><li>RecyclerListView <br>RecyclerListView假设列表项的种类可枚举的。所有列表项可以分为若干类,比如,一张图片的图文布局是一类,两张图片的图文布局是一类,只要布局相似就是同一类列表项。开发者,需要对类型进行事先的声明。<br>const types = {<br>ONE_IMAGE: 'ONE_IMAGE', // 一张图片的图文布局<br>TWO_IMAGE: 'TWO_IMAGE' // 两张图片的图文布局<br>}</li></ol><p>如果,用户即将看见的列表项,和用户看不见的列表项,类型一样。就把用户看不见的列表,修改成用户即将看到的列表项。修改不涉及到组件的整体结构,只涉及组件的属性参数,通常包括,文本、图片地址,还有展示的位置。 <br> {/<em> 把用户看不见的列表项 </em>/} <br><View style={{position: 'absolute', top: disappeared}}></p><pre><code><Text>一行文本</Text>
<Image source={{uri: '1.png'}}/></code></pre><p><View> <br> {/<em> 修改成用户即将看见的列表项 </em>/} <br><View style={{position: 'absolute', top: visible}}></p><pre><code><Text>一行文本~~</Text>
<Image source={{uri: '2.png'}}/></code></pre><p><View></View></p><p>从三者原理上对比,我们可以发现,在内存使用效率方面,内存复用的 RecyclerListView 比内存回收的 FlatList 更好,FlatList又比内存不回收的 ListView 更好。</p><p>原理对比<br>手势上滑,页面向上滚动,加载更多列表项(深绿色)</p><p>RecyclerListView的实践<br>RecyclerListView 复用列表项的位置是需要经常变化的,因此用的是绝对定位 position: absolute 布局,而不是从上往下的flex 布局。使用了绝对定位,就需要知道列表项的位置(top)。为了使用者的方便,RecyclerListView 让开发者传入所有列表项的高度(height),内部自动推断出其位置(top)。</p><ol><li>高度确定的列表项<br>在最简单例子中,所有列表项的高度都是已知的。只需将将高度、类型数据,和 Server 的数据进行合并,就可以得到RecyclerListView 的状态(state)。<br>const types = {<br> ONE_IMAGE: 'ONE_IMAGE', // 一张图片的图文布局<br> TWO_IMAGE: 'TWO_IMAGE' // 两张图片的图文布局<br>}</li></ol><p>// server data<br>const serverData = [</p><pre><code>{ img: ['xx'], text: '' },
{ img: ['xx', 'xx'], text: '' },
{ img: ['xx', 'xx'], text: '' },
{ img: ['xx'], text: '' },</code></pre><p>]</p><p>// RecyclerListView state<br>const list = serverData.map(item => {</p><pre><code>switch (item.img.length) {
case 1:
// 高度确定,为 100px
return { ...item, height: 100, type: types.ONE_IMAGE, }
case 2:
return { ...item, height: 100, type: types.TWO_IMAGE, }
default:
return null
}</code></pre><p>})</p><ol start="2"><li>高度不确定的列表项<br>并不是所有列表项的高度,都是确定的。比如,上文下图的列表项,虽然图片高度是确定的,但是文本高度是由 Server 传过来的文本长度决定的。文字可能一行,可能两行,可能多行,文字有几行是不确定的,因此列表项的高度也不确定。那么,应该如何使用RecyclerListView 组件呢?</li></ol><p><img src="/img/bVcTldR" alt="WeChat046c9273fdb883dc918b2929dbd06fe0.png" title="WeChat046c9273fdb883dc918b2929dbd06fe0.png"></p><p>2.1 Native 异步获取高度<br>Native 端,实际上有提前计算文本高度的 API —— fontMetrics。将 Native fontMetrics API 暴露给JS,JS 不就具有了提前计算高度的能力了。此时,RecyclerListView 需要的 state 计算方法如下,其值是 promise 类型。<br>const list = serverData.map(async item => {</p><pre><code>switch (item.img.length) {
case 1:
return { ...item, height: await fontMetrics(item.text), type: types.ONE_IMAGE, }
case 2:
return { ...item, height: await fontMetrics(item.text), type: types.TWO_IMAGE, }
default:
return null
}</code></pre><p>})</p><p>每次调用 fontMetrics,都需要 oc/java 与 js 进行一次异步通讯。而异步通讯是非常耗时的,该方案会明显增加渲染耗时。此外,新增fontMetrics 接口的方案,依赖 Native 发版,只能在新版本中使用,老版本用不了。因此,我们没有采用。<br>2.2 位置修正<br>开启 RecyclerListView 的 forceNonDeterministicRendering=true 属性后,会自动进行布局位置纠正。其原理是,开发者事先估算出列表项的高度,RecyclerListView 先按估算高度把视图渲染出来。当视图渲染出来后,通过 onLayout 获取列表项真正的高度,再通过动画将视图位移到正确的位置。</p><p><img src="/img/bVcTldS" alt="WeChatc77458f6a6135fbe449d8f1d680f9d78.png" title="WeChatc77458f6a6135fbe449d8f1d680f9d78.png"></p><p>位置修正<br>该方案,在估计高度偏差小的场景下很适用,但在估算偏差大的场景下,会明观察到明显的重叠和位移的现象。那么,有没有一种估算偏差小,耗时又短的方法呢?<br>2.3 JS 估算高度 <br>大部分情况下,列表项高度不确定都是由文本长度的不确定导致的。因此,只要能大致估算文本的高度就行。<br>1 个 17px 字号 20px 行高的汉字,渲染出来的宽度为 17px,高度为 20px。如果,容器宽度足够宽,文字不折行, 30 个的汉字,渲染出来的宽度为30 <em> 17px = 510px,高度依旧为 20px。如果,容器宽度只有 414px,那么显然会折成 2 行,此时文字高度为 2 </em> 20px =40px。其通用公式为:<br>行数 = Math.ceil( 文字不折行宽度 / 容器宽度 )<br>文字高度 = 行数 * 行高<br>实际上,字符类型不仅有汉字,还有小写字母、大写字母、数字、空格等,此外,渲染字号也各有不同。因此,最终的文本行数算法也更为复杂。我们通过多种真机测试,得出了17px 下的各类字符类型的平局渲染宽度,比如大写字母 11px,小写字母 8.6px 等等,算法摘要如下:<br>/**</p><ul><li>@param str 字符串文本</li><li>@param fontSize 字号</li><li><p>@returns 不折行宽度<br>*/<br>function getStrWidth(str, fontSize) {<br> const scale = fontSize / 17;<br> const capitalWidth = 11 * scale; // 大写字母<br> const lowerWidth = 8.6 * scale; // 小写字母<br> const spaceWidth = 4 * scale; // 空格<br> const numberWidth = 9.9 * scale; // 数字<br> const chineseWidth = 17.3 * scale; // 中文和其他</p><p>const width = Array.from(str).reduce(</p><pre><code> (sum, char) =>
sum +
getCharWidth(char, {
capitalWidth,
lowerWidth,
spaceWidth,
numberWidth,
chineseWidth,
}),
0,</code></pre><p>);</p><p>return Math.floor(width / fontSize) * fontSize;<br>}</p></li></ul><p>/**</p><ul><li>@param string 字符串文本</li><li>@param fontSize 字体大小</li><li>@param width 渲染容器宽度</li><li>@returns 行数<br>*/<br>function getNumberOfLines(string, fontSize, width) {<br> return Math.ceil(getStrWidth(string, fontSize) / width);<br>}<br>上述纯 js 估算文字行数的算法,实测的准确率在 90% 左右,估算耗时为毫秒级别,能够很好的满足我们需求。<br>2.4 JS 估算高度 + 位置修正<br>因此,我们的最终方案为,通过 JS 估算出文本行数,并得出文本高度,再进一步地推断出列表项的布局高度。并开启forceNonDeterministicRendering=true,在估算有偏差时,自动动画修正列表项的位置。<br><img src="/img/bVcTld0" alt="WeChat6e4003adc60f86aa2ca9ecc5bf076c0b.png" title="WeChat6e4003adc60f86aa2ca9ecc5bf076c0b.png"><br>标签页中的无限列表<br>对于分类信息流形态的产品,有的会包含多样化标签,每个标签都有特定的内容,其中大部分标签页是无限列表。如果,所有标签页的内容都同时存在,内存得不释放,也会导致性能问题。</li></ul><ol><li>内存回收<br>沿用上面的处理列表内存的思路,我们可以选择内存回收,或内存复用思路。内存复用的前提是,复用内容的结构相同,只有数据有变化。实际业务中,产品已经将相似内容进行了分类,每个标签页各有各的特点,很难复用。因此,对于标签页而言,内存回收是更好的选择。 <br>整体思路是,可视区域内的标签页肯定要显示出来。最近在可视区域的显示过的内容,根据情况进行保留。远离可视区的内容,需要销毁。</li></ol><p><img src="/img/bVcTld3" alt="WeChate45fe1445de2151ad2b376d8568dfeac.png" title="WeChate45fe1445de2151ad2b376d8568dfeac.png"></p><p>销毁远离可视区的标签页</p><ol start="2"><li>手势重叠的处理<br>标签页 TabView 1.0 使用的是 RN 自带的手势系统,单独的左右滑动切换的标签页,自带的手势系统运行良好。如果可视区中,既有可以左右滑动切换的标签页,又有可以左右滚动的内容区域。用户向左滚动手势重叠区域时,是标签页响应滚动,还是内容区域响应,还是同时响应呢?<br><img src="/img/bVcTld4" alt="WeChatab2910f96f775ad392926a3410839f39.png" title="WeChatab2910f96f775ad392926a3410839f39.png"></li></ol><p>手势重叠区域,向左滚动,谁响应?<br>由于 RN 的手势识别,是同时在 oc/java 渲染主线程和 js 线程中同时进行的,这种奇怪的处理方式,使得手势很难得到精准的处理。这导致TabView 1.0 不能很好的处理手势重叠的业务场景。 <br>在 TabView 2.0 中,集成了新的手势系统 React Native Gesture Handler。新的手势系统,是声明式的,由纯oc/java 渲染主线程处理的手势系统。我们可以在 JS 代码中,对手势的响应方式进行提前声明,让标签页等待(waitFor) 内容区域的手势响应。也就是说,重叠的区域手势,只作用于内容区域。</p><p>总结<br>本文介绍了我们在使用 RN 开发分类信息流形态的产品的无限列表中,遇到的一些常见问题,以及如何进行技术考量、优化和选择的。希望能对大家有借鉴意义。</p>
北斗监控概述之告警篇(四)
https://segmentfault.com/a/1190000040319882
2021-07-09T11:26:06+08:00
2021-07-09T11:26:06+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<p>大家是否遇到过这样的场景,老板在群里 @ 你,向你反馈线上有 BUG。一般老板反馈的问题,都属于重要且紧急的需求,需要快速解决。这样的高优需求,即让你神经紧绷,又打破了你原有的计划,常常弄的人很累。靠充分的测试能够解决一部分问题,但是线上的场景永远比测试的场景更加丰富,难免会有遗漏 BUG 被带上线,造成业务损失。</p><p><img src="/img/remote/1460000040319884" alt="IMG_8755" title="IMG_8755"></p><h4>可靠地、实时地、准确地提前发现问题</h4><p>一个好的告警系统会提前发现问题,发出警告。你就有可能在老板 @ 你之前,就把问题给解决。</p><p>告警系统要提前发现问题的三个关键是:</p><ul><li>可靠性。告警系统本身要可靠,不能时灵时不灵。</li><li>实时性。要快,慢了就没有意义了。</li><li>准确性。狼没来,喊狼来了,喊了三次后就没人信了。</li></ul><p><img src="/img/remote/1460000040319885" alt="IMG_8756 2" title="IMG_8756 2"></p><p>接下来,我会先带大家看下我们的告警系统设计的全貌,再从可靠性、实时性、准确性三方面来详细介绍。</p><h3>实时告警:整体实现</h3><p>实时告警功能主要的工作量在 node.js 层,其整体实现如下:</p><ol><li>每分钟批量执行一次定时任务</li><li>向Druid发出请求,获取线上实时的聚合数据</li><li>判断聚合数据是否超出阈值,如果超出则执行下一步</li><li>发送短信、微信、邮件通知开发者</li></ol><p><img src="/img/remote/1460000040319886" alt="image-20210201164950636" title="image-20210201164950636"></p><h3>可靠性:每台机器启动一个定时任务,会执行多次</h3><p>一台机器运行定时任务,只有一个定时任务,指标超出阈值也只会发出一次告警。</p><p>但是,为了保障可靠性,我们就要避免单点风险,也就是至少要启动两台机器来运行定时任务,即使其中一台机器挂了,告警功能也能正常运行。但是,引入多台机器问题就来了。</p><p>二台机器运行定时任务,有两个定时任务,会发出两次告警。分布式 node.js 集群,会有多台机器运行定时任务,超出阈值就会发出多次告警,引入了重复告警的新问题。</p><h4>定时任务:Redis锁保障只执行一次</h4><p>我们需要的是多台机器,只执行一次定时任务,有啥办法来保障呢?我们想了两个方案。</p><ul><li>共识方案:一个集群中的多台机器之间,选出一台机器来执行定时任务。</li><li>抢锁方案:一个集群中的多台机器同时向 redis 发出请求,谁抢到 redis 锁谁执行定时任务。</li></ul><p>共识方案,在 Java 中有 zookeeper 之类的分布式调度工具可以使用,底层共识算法会选举出一台机器作为主(leader),其他机器作为从(follower)。可以只让主机器执行定时任务。但是在 node.js 中,我们并未找到类似于 zookeeper 的框架,所以就放弃了。</p><p>抢锁方案,是我们采用的方案。其原理是通过共享缓存的唯一性,来保障定时任务执行的唯一性,依赖的是 redis 的可靠性。在实际的业务中,我们认为 redis 比较稳定,单台就够了。如果以后发现问题,再改用多台 redis 也不迟。node.js 中有 bull、redlock 的分布式锁工具可以用。</p><ul><li>bull 的功能主要是分布式队列,用来实现分布式锁有些大材小用。</li><li>redlock 是 redis 官方提出的分布式锁算法,也有 node.js 的实现,正好符合我们的需求。</li></ul><p>因此,在告警功能的可靠性上,我们使用了 node.js 集群,避免了单点故障。同时,使用 redis 锁,保障一个告警定时任务,只会执行一次。</p><p><img src="/img/remote/1460000040319887" alt="image-20210201173000423" title="image-20210201173000423"></p><h4>实时性:关键是“快”</h4><p>实时性的关键词是“快”,从用户异常发生到开发者收到异常告警的时间越快越好。</p><p>其中关键点是:</p><ul><li>Web 发生异常后,马上上报,SDK 层不缓存。</li><li>使用时间序列数据库 Druid,减少海量数据聚合耗时。</li><li>每分钟 node.js 执行一次告警定时任务。</li></ul><p><img src="/img/remote/1460000040319888" alt="image-20210201220044515" title="image-20210201220044515"></p><p>大家可以看到,从发生时间到告警时间,中间有 8 个时间点,用哪个时间点好呢?</p><p>在 8 个时间点中,最为关键时间点有 2 个:</p><ul><li>发生时间(橙色):用户行为轨迹需要对用户异常日志、正常日志按时间进行排序。异常日志是立刻上报的,正常日志是用户主动上报的,这就只能用发生时间进行排序,其他时间肯定都不对。</li><li>接收时间(蓝色):开发者搜索某天某个用户发生的异常,如果用户本地时间是错的,可能就会搜不到。但我们可以相信,服务端的接收时间是准确的,按照接收时间搜索,肯定能搜到。</li></ul><p>那么告警指标的统计以那个时间点为准呢?具体的统计方式应该怎么设计呢?</p><h4>实时性:统计方式</h4><p>从两方面考虑,我们的统计时间点用的是接收时间。一方面,服务端时间的准确性是我们自己控制的,是可信的。另一方面,考虑有海外用户会跨时区,时区问题处理起来会比较麻烦,统一用服务端时间更好。</p><p>但是,这里还是有一些细节需要大家注意,从接收到可被读取之间,还有经过 kafka 消息队列和 druid 聚合计算的时间。理论上 kafka 只要不发生消息堆积,是能马上被 druid 进行消费的,druid 本身又有实时计算模块,聚合计算也是非常快的。但是,我们考虑到可能会有偶发的一些超时情况,可能会导致统计数据偏少,从而导致误报。因此,拍脑袋给 kafka 和 druid 预留了 1 分钟的缓冲时间,算作对实时性的一种妥协吧。</p><p><img src="/img/remote/1460000040319889" alt="image-20210201232622880" title="image-20210201232622880"></p><h4>准确性:线上异常的总量监控指标不可靠?</h4><p>现在,我们来聊聊最后一个特性,准确性,也就是告警指标是否可不可靠。</p><p>我们假设一个场景,有位开发者看到异常总数这个指标快速上升,他心中一紧,这线上是啥情况啊?</p><p><img src="/img/remote/1460000040319890" alt="image-20210202104744665" title="image-20210202104744665"></p><p>如果恰巧,前端页面刚刚有上线,大概率是前端问题。</p><p>如果恰巧,后端同时发出宕机告警,大概率是后端问题。</p><p>如果恰巧,业务刚刚在冲量,那么引起页面异常总数指标快速上升的原因,大概率只是因为 PV 涨了而已。</p><p>同理,在0~6点是业务范围量少,到了 8 点~10点,流量会有很大的上涨,同时也会影响异常总量上涨。</p><h4>准确性:异常PV比代表平均每个PV发生异常的数量</h4><p>在异常总数和PV同步增长的情况下,异常PV的比值其实会保持不变。其原因是平均用户每次访问,页面发生异常数量是一样的。因此,在我们的业务中,相对于异常总数,我们认为异常PV比的变化更重要。</p><p>当业务异常PV比大于预设的阈值,或者环比上升较快,那么就可以判断线上出现新的异常了。并向业务发出告警。</p><p><img src="/img/remote/1460000040319892" alt="image-20210202114133055" title="image-20210202114133055"></p><h4>准确性:全面的监控指标</h4><p>为了更全面的监控线上应用,我们设定了一系类的监控指标,其中深蓝色的是核心指标,它的准确性会更高一些。</p><p><img src="/img/remote/1460000040319893" alt="截屏2021-02-02 14.31.43" title="截屏2021-02-02 14.31.43"></p>
北斗监控概述之架构篇(三)
https://segmentfault.com/a/1190000040319844
2021-07-09T11:24:09+08:00
2021-07-09T11:24:09+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<p>用户浏览页面生产的数据,会被 SDK 整理成 3+1 类数据,再上报给后端。后端将这些数据进行处理、存储、再加工后,被展示出来被最终的开发者消费。</p><p><img src="/img/remote/1460000040319846" alt="image-20210506175903213" title="image-20210506175903213"></p><p>现在,虽然我们可以使用 node.js 来开发后端部分。但是,要 hold 住集团内部所有的前端监控请求,还是挺难的,要同时解决下面 3 个难点:</p><ul><li>流量大: 预计单日请求峰值可达10亿次</li><li>数据量大:预计每日产生数据峰值可达5TB</li><li>实时性强:需要提供分钟级别的告警、分析功能</li></ul><p>前端开发同学是最了解前端监控系统的需求的,但是对于海量数据处理我们的前端团队并没有经验,那么应该怎么应对大数据的挑战呢?</p><p>我们给出的答案是,合作。 <strong>JavaScript 和 Java 团队合作,发挥各自的优势,用 JavaScript 处理业务,用 Java 处理大数据。</strong> 团队整体上分为 3 个小组:</p><ul><li>SDK 小组。负责收集数据。</li><li>Java 小组。负责接收、预处理海量的数据(Java)。</li><li>全栈小组。负责数据再加工(node.js)和展示(web)。</li></ul><p><img src="/img/remote/1460000040319847" alt="image-20210416190637033" title="image-20210416190637033"></p><h2>整体架构</h2><p>通过 Java 和 Node 的结合,我们的监控平台既拥有高效的大数据处理能力,也拥有良好适应业务快速变化的能力,整体架构如下:</p><p><img src="/img/remote/1460000040319848" alt="image-20210129162333983" title="image-20210129162333983"></p><h3>数据存储:Java与Node的桥梁</h3><p><strong>Java 与 node.js 进行数据交换的桥梁就是,数据存储系统。</strong> 我们一共用到了 4 类数据存储系统,它们各有各的侧重。我们先简单介绍一下这 4 类数据存储系统。</p><table><thead><tr><th> </th><th><strong>MySQL</strong></th><th><strong>Redis</strong></th><th><strong>ElasticSearch</strong></th><th><strong>Apache Druid</strong></th></tr></thead><tbody><tr><td><strong>描述</strong></td><td>关系型数据库</td><td>基于内存的键值对存储数据库</td><td>基于Lucene搜索引擎构建的存储</td><td>高性能的实时分析数据库</td></tr><tr><td><strong>主数据模型</strong></td><td>Relational</td><td>Key-value</td><td>Search Engine</td><td>Time Series DBMS</td></tr><tr><td><strong>使用场景</strong></td><td>项目表、权限表、告警表</td><td>共享缓存、分布式锁</td><td>全文搜索</td><td>聚合分析、告警</td></tr></tbody></table><ul><li>MySQL 是大家最熟悉的,它是关系型数据库,类似于 Excel 的表。它的数据是一行行的存储,在我们的业务表中用的较多。</li><li><p>Redis 在我们的监控场景中,主要用于 Java node.js 的共享缓存,另外还有分布式锁。</p><ul><li>Redis 用的是内存来存储数据的,比 MySQL 用的磁盘存储数据的性能更好,因此更适合比较频繁的数据操作。</li></ul></li><li><p><strong>ElasticSearch (简称 ES) ,在我们的监控场景中主要用于日志的全文搜索。</strong></p><ul><li>ES 会通过分词构建的索引直接查找,比 MySQL 要一行一行的全表扫描性能更好,因此更适合通过错误关键字、堆栈关键字来搜索具体的报错信息这类场景。</li></ul></li><li><p><strong>Druid 在我们的监控场景中主要用于日志的聚合分析。</strong></p><ul><li>比如 count(*),Druid 是列式存储聚合,分析时可以直接读取一列数据, MySQL 要一行一行的全表扫描进行聚合分析,因此 Druid 更适合大数据的聚合分析。</li></ul></li></ul><p>当然具体的业务场景需要具体的分析。如果你们的业务数据量不大,不会遇到性能问题,那么 MySQL + Redis 也许就够了,不用着急上 ES 或 Druid 来优化性能。如果你们的业务数据量很大,PV 都破亿了,建议架构设计的时候,就需要考虑用上 ES 或 Druid。</p><h3>功能架构</h3><p>技术架构是整体项目的结构,其中设计细节必须结合功能才能更容易让大家理解,所以必须得先和大家介绍一下功能架构。</p><p><strong>SDK 会将通用数据、性能数据、正常数据、异常数据进行上报。这些数据上报后,经过处理形成了监控平台的 5 个功能:</strong></p><ul><li>明细查询:通过 ES 的搜索功能可以展示某条日志的明细。</li><li>轨迹查询:某个用户的浏览轨迹,包括性能、正常、异常日志记录(敏感信息需要用户授权才上报),通过 ES 将多种类型的日志按时间顺序排序,就能显示出某次用户的浏览轨迹了。</li><li>项目看板:一个项目会有多种聚合指标,如JS异常、接口异常、白屏时间等等,多种指标会一起请求回来,汇总一个看板中便于查看。</li><li>项目分析:所谓的分析就是异常、性能的具体分布情况,比如你可以对某个项目的 <code>xx is undefind</code> JS 错误和通用数据中的机型关联起来,就能知道它是在 <code>android </code> 还是 <code>ios</code> 分布的多了。将异常、性能与通用数据关联起来,就能对项目的某个异常、某个慢加载的机型、版本、地理位置等等通用数据进行分析了。</li><li>项目告警:当判断出某个指标超出阈值时,就要发出告警通知开发者。具体会在第四篇展开。</li></ul><p><img src="/img/remote/1460000040319849" alt="image-20210129162443424" title="image-20210129162443424"></p><h2>设计细节</h2><h3>数据归类:Node写Redis,Java读</h3><p>数据是以 projectId 为维度存储在 ES、Druid 中的。但 Java 只能拿到 url,而 url 与 projectId 的对应关系是在监控平台填写的,因此需要 node.js 把 projectId 和 url 怎么关联的数据传给 Java。</p><p>projectId 与 url 怎么关联的数据,会被高频读取,所以 node.js 会把它们存在 redis。日志数据被上报时,java 就会去 redis 读最新 projectId 与 url 关联关系。再进行数据归类,并存在 ES/Druid 中。 </p><p><img src="/img/remote/1460000040319850" alt="image-20210128145122626" title="image-20210128145122626"></p><p>具体的数据归类逻辑如下:</p><ol><li>错误日志 { error, url:"https://58.com/fang/list"}</li><li>先直接提取项目 ID</li><li>不存在则提取 URL "58.com/fang/list"</li><li>从 Redis HASH({"58.com/fang/list": 3}) 中查找 ID</li><li>按项目 ID 维度归类</li></ol><h3>日志查询:Java写ES,Node读</h3><p>Java 把日志明细数据给 node.js 是通过 ES 进行的。具体的是,在 Java 数据归类后,将数据写到 ES 中。开发者在监控平台查询日志明细时,就请求 node.js ,由 node.js 再请求 ES。</p><p><img src="/img/remote/1460000040319851" alt="image-20210129161231796" title="image-20210129161231796"></p><p>node.js 请求 ES 的关键参数如下:</p><pre><code class="json">"query": {
"bool": {
"filter": [
{"range": {"timestamp": {"lte": 1607322167607,"gt": 1607235767606}}},
{"term": {"projectId": 1}},
{"term": {"type": "exception" }},
{"wildcard": {"exception.content.keyword": "*TypeError*"}}
]
}
}</code></pre><p>在 ES 中,timestamp、projectId、exception 的索引事先就被创建了。比如, <code>TypeError: Parameter 'url' must be a string, not object</code> 这段文本,在 ES 内部会被分词为一个个单词,<code>TypeError</code> <code>Parameter </code> <code>url</code> <code> must</code> <code> be</code> <code> a</code> <code>string</code> <code>not</code> <code>object</code> ,每个单词都是一个索引。这些索引组合的搜索结果会有 0~N 条日志明细,都会被返回给 node.js。</p><p>node.js 再把数据返回给 web,在 web 中展开其中一条日志明细,并把它展示如下:</p><p><img src="/img/remote/1460000040319852" alt="image-20210128165245123" title="image-20210128165245123"></p><h3>日志聚合:Java写Druid,Node读</h3><p>Java 把日志聚合数据给 node.js 是通过 Druid 进行的。具体的就是,日志数据被 SDK 上报给 Java,在 Java 数据归类后,将数据写到 Druid 中。开发者在监控平台查询日志聚合时,就请求 node.js ,由 node.js 再请求 Druid。</p><p><img src="/img/remote/1460000040319853" alt="image-20210128165634947" title="image-20210128165634947"></p><p>node.js 请求 Druid 一般是通过 SQL 来请求的,一条请求 PV 的 SQL 如下:</p><pre><code class="sql">SELECT
TIME_BUCKET(__time,'P1D','Asia/Shanghai') timestamp,
pv
FROM hdp_ubu_tech_wei_beidou_data
WHERE
type='performance' AND
__time>='2021-02-14T00:00:00+08:00' AND
__time<='2021-02-14T23:59:59+08:00'
GROUP BY 1</code></pre><p>Druid 是为聚合分析专门设计的时间序列数据库。它可以对时间进行智能的分区,比如,在上面我们提到的 <code>SQL</code> 查询中, <code>P1D</code> 就是告诉 Druid 要以天为单位分区,查询 pv 返回的就是一天的 pv。你可以更改配置,以小时、分钟进行分区,查询 pv 返回的就是一小时或一分钟的 pv。</p><h3>日志聚合:聚合数据流转的整个流程</h3><p>Druid 会对原始数据做预聚合,加快查询的速度。示例如下:</p><p><img src="/img/remote/1460000040319854" alt="image-20210129105403021" title="image-20210129105403021"></p><p>node.js 把读取回来的数据返回给 web,由 web 展示如下(小时维度的 pv uv 数据):</p><p><img src="/img/remote/1460000040319855" alt="image-20210129162546196" title="image-20210129162546196"></p>
北斗监控概述之SDK篇(二)
https://segmentfault.com/a/1190000040319818
2021-07-09T11:22:29+08:00
2021-07-09T11:22:29+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<p>监控 SDK 的作用是收集客户端产生的日志,<strong>如何全面的收集日志是本篇讨论的重点</strong>。监控日志可以分为 3 类,异常日志、正常日志、性能日志,每次日志上报时还会上报 1 类通用的信息:</p><table><thead><tr><th>日志类型</th><th>二级分类</th><th>说明</th></tr></thead><tbody><tr><td>异常日志</td><td>JS错误、Server/Native 接口异常、资源下载异常</td><td>监控关键,需实时上报</td></tr><tr><td>正常日志</td><td>用户行为、log信息、路由信息</td><td>日志量大,由用户主动上报</td></tr><tr><td>性能日志</td><td>首屏时间、耗时明细、PV/UV</td><td>上报一次,同时统计PV/UV</td></tr><tr><td>通用信息</td><td>项目标识、用户标识、环境信息</td><td>其他 3 类日志附加的通用信息</td></tr></tbody></table><h2>异常日志</h2><p>JS 错误是最常见的异常日志,但并不是所有异常都是 JS 错误。接下来会介绍可能被大家忽略的但对线上问题定位很重要的<strong>三类异常的捕获方法,包括</strong></p><ol><li>Server 接口异常的捕获方法</li><li>Native 接口异常的捕获方法</li><li>资源下载异常的捕获方法</li></ol><p><img src="/img/remote/1460000040319820" alt="image-20210506170950355" title="image-20210506170950355"></p><h3>如何收集 Server 接口异常?</h3><p>在第一篇中,我们顺带介绍过通过 <code>onerror</code> 和 <code>unhandledrejection</code> 收集全局 JS 错误。但是在我们的实际业务中,其实是一些错误或异常不会抛到全局,也就不会被 <code>onerror</code> 和 <code>unhandledrejection</code> 收集到。比如,Server 接口异常,就不会被抛到全局。</p><pre><code class="js">const serverAPI = '58.com/api'
const response = await fetch(serverAPI)
if(response.status < 400) {
setState(() => ({data: response.json()}))
} else {
// 手动上报异常
errorReport({serverAPI, status: response.status})
}</code></pre><p><code>else</code> 这块代码,有些业务同学可能会不写。如果没有手动上报,这些 Server 异常就会被忽略,但是要求所有业务同学将每个 Server 异常都手动上报,也又不现实。怎么办呢?在监控 SDK 中统一上报。</p><h3>重写fetch方法</h3><p><strong>Server 接口异常的捕获方法,就是重写 <code>window.fetch</code> :</strong></p><pre><code class="js">const originalFetch = window.fetch
window.fetch = function wrapperFetch(...args) {
return originalFetch.apply(window, args).then(
(response) => {
// 自动上报异常
if (response.status >= 400) {
errorReport({ serverApi: args[0], status: response.status })
}
return response;
})
}</code></pre><p>使用类似的方法:</p><ul><li>我们也可以 wrapper XMLHttpRequest,捕获 status >= 400 的情况。</li><li>捕获 fetch 走到 catch 中的情况。</li><li>捕获 fetch 超时的情况。</li></ul><h3>绕过Hybrid SDK与Native交互</h3><p>我们再往前想一步,调用 Server 接口异常不会被抛到全局,Native 接口异常也不会被抛到全局。Native 接口异常,也可以由监控 SDK 专门捕获上传。思路和 <code>wrapperFetch</code> 方法思路类似,我们可以 <code>wrapperHybrid</code>。</p><p>这里先介绍一下业务怎么使用 Hybrid SDK 手动上报。接下来再说怎么自动上报。</p><pre><code class="js">const request = {action: 'getUser', params: 'useName'}
const response = await hybrid.action(request)
// 手动上报
if(response.code === 1) {
errorReport({ ...request, code: response.code })
}</code></pre><p>但是我们有些老页面,一些业务自定义的交互协议,业务直接通过协议就和 Native 进行交互了,并没有使用通用的 Hybrid SDK。通过重写 Hybrid SDK 的方式就不生效了。怎么办呢?直接监听最底层的 JSBridge。</p><pre><code class="js">jsbridge('wbmain://hybrid/jsbridge?action=getUser&parmas=userName&callback=windowGetUser')</code></pre><h3>监听 JSBridge</h3><p>JSBridge 包括请求和响应两个部分:</p><ul><li><p>Web 请求 Native</p><ul><li>请求方式有多种,这些方式是由 Native 和 Web 事先进行约定的,也基本不会改动,因此比上层接口更为稳定。</li><li>常用方法一:Web 调用全局方法,Native 重写该方法进行拦截。如重写拦截 <code>window.prompt()</code> 、 自定义全局函数。</li><li>常用方法二:Web 通过 iframe 发送请求,Native 拦截 iframe 请求。</li></ul></li><li><p>Native 响应 Web</p><ul><li>Web 事先自定义全局函数,并把全局函数名通过 <code>schema</code> 的 <code>callback</code> 参数传过去</li><li>Native 调用该全局函数,Web 就能拿到 Native 响应的参数了</li></ul></li></ul><p><img src="/img/remote/1460000040319821" alt="image-20210416190536263" title="image-20210416190536263"></p><p>以 iframe 方案为例,<strong>实现 Native 接口异常的捕获方法是监控 iframe src 的变化</strong>,如下:</p><pre><code class="js">// 创建监听
const observer = new MutationObserver((mutationList) => {
mutationList.forEach((mutation) => {
// 获取请求 的 schema
const schema = mutation.target.src
// 获取事先定义的全局函数名
const callback = getURLSearchParams(schema,'callback');
// 通过 wrapper 拦截响应,获取 native params ,当 params 异常时上报
// params 异常,由规范决定,如 errorCode = 1
window[callback] = wrapperCallback(window[callback])
})
});
// 开始监听 iframe src 变化
observer.observe(iframe, {attributes: true,attributeFilter: ['src']});</code></pre><h3>使用监听的方式获取更多信息</h3><p>另外 2 类异常的监听方式:</p><ul><li>跨域报错(左):使用 CDN 托管 JS 资源,JS 和 Web url 不在同一个域,跨域 JS 的报错浏览器只会给出 <code>script error</code> 的提示。<code>script error</code> 的提示并不是真的报错信息,也没有错误堆栈。通过重写监听函数,就能通过 try catch 捕获这些函数的报错了</li><li>资源报错(右):加载资源报错不会被 <code>window.onerror</code> 捕获到,但是<strong>资源异常可以被 <code>window.addEventListener('error')</code> 捕获到。</strong></li></ul><p><img src="/img/remote/1460000040319822" alt="image-20210127202810805" title="image-20210127202810805"></p><h2>性能收集</h2><p>性能收集常用的有两种方案,一种是业务自定义的性能标准,一种是业内通用的性能标准。</p><p>业务自定义的性能标准,需要入侵业务代码进行埋点收集,各个业务方案之间的都有一些差别,业务之间也不能横向对比,可用性较差。本篇重点讨论业内通用的性能标准,并<strong>创新性地实现了 FP、LCP 指标在 RN 端的收集方法。</strong></p><p><img src="/img/remote/1460000040319823" alt="image-20210506171943502" title="image-20210506171943502"></p><h3>性能标准</h3><p>另一种是 W3C 的性能标准,由于是浏览器的标准实现,可以不入侵业务代码,就能获取到页面加载耗时。W3C 的标准包括了 Navigation Timing 和 Navigation Timing Level 2,两份标准。第一份标准浏览器已经支持很好了,第二份新标准支持性差一些,但标准之间差别不大,收集的时候做下向下兼容即可。</p><p>性能收集包括两块</p><ul><li>耗时明细</li><li>白屏时间</li></ul><p>其中耗时明细的 Level 2 定义如下,所有耗时明细都需要收集:</p><p><img src="/img/remote/1460000040319824" alt="图片 1" title="图片 1"></p><h3>白屏时间统计</h3><p>相比耗时明细,大家可能更关注白屏时间,也就是页面整体渲染耗时。白屏时间大家定义各不相同,这是因为大家业务场景不一样导致的,大致可以分为两种定义。</p><ul><li>一种是后端直接生成模板,白屏时间的定义为 DOMContentLoaded(DCL) 或 Load(L) 这种老指标</li><li>一种是前端渲染页面,比如 React/Vue 为代表的 JS 生成页面,白屏时间的定义为 FP、FCP、LCP 这类新指标</li></ul><p><img src="/img/remote/1460000040319825" alt="图片 1" title="图片 1"></p><p><strong>获取 FP、FCP、LCP 新指标的方式是 <code>performance.getEntries()</code></strong> ,但是这些新指标并未纳入 W3C 规范,只是 Google 提出来的一种规范,目前兼容性比较差,可用性我们也在验证中。</p><p>FP (first paint)指的是第一个像素渲染到屏幕上所用的时间,FCP(first contentful paint)指的是第一个文字、图片等渲染到屏幕上所用的时间。在大多数场景下,二者基本相等。</p><p>LCP(largest contentful paint) 指的是显示面积最大的文字或图片渲染到屏幕上所用的时间。Google 提出的以 LCP 指标,已经可以在最新 Chrome 和 Android 机型上收集到了,大家设计 SDK 时不妨一起收集上来,作为一种辅助指标来衡量页面性能。</p><p><strong>RN 收集 FP LCP 指标</strong></p><p>FP、LCP 指标非常适合 React Web 页面,那么也同样适合 React Native 页面。业内并未有 FP、LCP 在 RN 上的实现,北斗 SDK 参考了 W3C 的 FP 和 LCP 标准,在 RN 中实现了 FP、LCP 指标的收集。</p><p><img src="/img/remote/1460000040319826" alt="image-20210506160747671" title="image-20210506160747671"></p><p>实现思路如下,<strong>在 RN 中监听所有 Text Image 组件的 <code>onLayout</code> 事件</strong>,就能获取该组件的渲染时间点。这样 FP 第一个像素的渲染耗时,就可以算出来。 <strong><code>onLayout</code> 中包含渲染元素的 <code>width</code> 和 <code>height</code> ,就可以知道那个是渲染面积最大的文字或图片,从而计算出 LCP 的耗时。</strong> 伪代码如下:</p><pre><code class="js">class RNVitals {
// 记录 FP
private fp;
// 记录 LCP
private lcp;
// 监听所有 Image 的 onLayout 事件
private setWrapperImage() {}
// 监听所有 Text 的 onLyaout 事件
private setWrapperText () {
const TextRender = Text.render
Text.render = (...args) => {
const originImage = TextRender.apply(this, args);
const { onLayout } = originImage.props ;
return React.cloneElement(originImage, {
onLayout: (event: LayoutChangeEvent) => {
this.track(event)
onLayout && onLayout(event)
}
});
}
}
// 计算 FP、LCP 指标
private track(event: LayoutChangeEvent): void {
const size = event.height * event.width
// 记录第一个元素渲染的时间戳
if( this.fp == null) {
this.fp = {
size,
layoutTime: Date.now()
}
}
// 记录&更新最大面积元素渲染的时间戳
if (this.lcp.size < size) {
this.lcp = {
size,
layoutTime: Date.now()
}
}
}
// 如果用户点击了页面或 lcp 2s都没有更新,则进行性能上报
publish report(){}
}</code></pre>
北斗监控概述之接入篇(一)
https://segmentfault.com/a/1190000040319781
2021-07-09T11:21:04+08:00
2021-07-09T11:21:04+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<p>作为一位前端开发者,你是否有过这样的困扰。某个用户反馈说,他的页面某个按钮点击不了,或者是根本打不开,但是你本地根本没法复现。因为,你不能通过不断调试的方法,来定位问题和修复问题,所以它非常难以解决。这时,你不妨先了解一下前端异常和性能监控,通过接入监控你可以较为轻松的找到线上问题线索。</p><p>接入前端监控系统,有两种方案,一种是接入外部监控系统,另外一种是自研。我们选择了自研,自研的优势是你可以根据你的需求来定制。但是,从头开始搭建一个大型的系统,并不是一个简单的事情。你需要站在产品角度,将前端工程师的需求翻译成功能,并且要站在架构师的角度来合理分工,解决高并发、实时性的难点,保障系统能够可靠性的运行。如果,你想要学习大型系统如何设计或者自己想搭建一个前端监控平台,我愿意把我的经验和你分享。</p><ol><li>北斗监控之接入篇:自研前端监控的快速接入方法</li><li>北斗监控之 SDK 篇:重写函数获取更多有用的线上信息</li><li>北斗监控之架构篇:分层架构搞定大数据的难题</li><li>北斗监控之告警篇:实现可靠、实时、准确告警的方法</li></ol><h3>北斗监控之接入篇:自研前端监控的快速接入方法</h3><p>我们来看一下一个最简单的监控 SDK 是什么样的。</p><pre><code class="js">window.onerror = (msg, source, line, column, error) => {
errorReport({ msg, source, line, column, stack: error.stack })
}
window.addEventListener('unhandledrejection', (event) => {
errorReport({ msg: event.reason })
})</code></pre><p>在前端应用中,我们通过 onerror 来监听 js 中普通错误,通过 unhandledRejection 来监听 Promise 错误。拿到错误信息后,再通过一个自定义的 errorReport 函数,把收集上来的错误上报到后端接口就行。</p><p>这样我们就完成了一个最简单的前端监控 SDK。实际上,我们一个后端接口,收集的肯定不止一个前端应用。因此我们需要项目 ID 来对前端应用进行区分。</p><h3>常用的接入方式:推广成本高</h3><p>以接入 Sentry 监控 SDK 为例。</p><p>第一步,你得需要在平台新建一个项目,拿到项目的 dsn,这个 dsn 本质是一个项目 ID。</p><p>第二步,你需要通过 script 或 npm 的方式,把 Sentry 的 SDK 引进来。</p><p>第三步,你需要初始化 Sentry。</p><pre><code class="js">import * as Sentry from "@sentry/browser";
Sentry.init({
dsn:"https://examplePublicKey@o0.ingest.sentry.io/0",
});</code></pre><p>你的应用 publicKey 是 1,他的应用 publicKey 2,Sentry 就是通过 dsn 中的 publicKey 来区分不同前端应用上报的数据的。所以,任何接 Sentry SDK 的前端应用都加上一个 dsn 的项目 ID,才能接 Sentry 监控。</p><p>大家可能觉得在业务代码中加个项目 ID ,再上次线成本也不高啊。但是大家站在监控平台的角度出发,公司内部有上千个项目要去推动接入,这看起来一点点的成本乘以一千倍,也会变得非常高。</p><h3>思路:上线平台向 Web 动态注入</h3><p>我们想到的一个思路是,针对 Web 前端应用场景,由上线平台或客户端向前端应用直接注入监控 SDK,省去业务接入的步骤。这样 App 中所有页面不就都能够得到监控了吗?</p><p>自动注入的方案很好,但怎么区分不同的 Web 前端应用?大家也肯定想到了,是 url。我们可以在 errorReport 错误上报的参数中,统一添加 url 作为项目统计标识。</p><pre><code class="js">function errorReport(event) {
// 用 URL 作为项目统计标识
const report = { ...event, url: location.href }
fetch('58.com/monitor/js', {
body: JSON.stringify(report),
})
}</code></pre><h3>以URL为维度统计,会引发三个问题</h3><p>首先是,现在我们有 url为维度,也有项目ID为维度的统计,两种方式要兼容,怎么办呢?</p><ul><li>如果一个项目,只有一个 URL,那么项目ID和URL实际是等价的。</li><li>如果一个项目,有两个或三个 URL 呢,怎么办?</li><li>但如果一个项目的 url 是动态变化的呢,比如文章详情页,url 通常是根据文章 ID 动态生成的。怎么办呢?</li></ul><h3>方案:兼容多种统计维度</h3><p>我们提出了一种兼容多种统计维度的方案。</p><p>第一种是,项目ID方案。业务先到监控平台申请项目ID,再到业务中手动填写项目ID的方案,这种方案适用于任意项目。</p><p>第二种是,固定 URL 方案。在前面提到的 Web 场景中,上线平台会帮我们自动注入监控 SDK,业务又只有一个固定的 URL,就无需入侵业务代码,业务方直接在平台填写 URL 即可把项目监控起来。</p><p>第三种是,动态 URL 改良的正则方案。正则方案容易造成错误匹配,比如业务就填了一个 58.com/* 就会把公司的所有 58 域名下的数据都统计到一个项目中。改良正则方案可以解决大部分错误匹配的问题。规则如下:</p><ol><li>URL N 个部分的 N,和匹配规则的 N 个部分的 N,要相等。</li><li>可以用 * 匹配 domain 和 path 中的任意部分</li><li><code>*</code>的数量 < N/2,也就是说一个 URL 有 6 部分,只能用 2 个 *。</li></ol><p><img src="/img/remote/1460000040319783" alt="image-20210416110902115" title="image-20210416110902115"></p><p>虽然兼容多种统计维度的接入方案,是以 Web 为基础进行思考的。但实际开发中,Web 动态注入方案,通过前端推动后端接入比较难,同时模板场景非常复杂,占比最多的老项目较多维护迭代少,整体看来推动成本高收益较小。而 RN 上线平台比较统一,也没有模板的困扰,方案上会比 Web 简单很多,因此该方案最终会先在 RN 先落地。</p><h3>小结</h3><p>本篇章对传统的以项目 ID 为维度的接入方式进行了思考,创新性地提出了一种无需入侵业务代码的接入方案,降低了业务方的接入成本,推广起来也会非常容易。该方案会先在 RN 场景下进行落地,业务只需在上线时勾选接入北斗监控,RN 打包平台会自动将北斗 RN SDK 注入到业务中,然后可以直接在监控平台看到 RN 项目的监控数据。</p>
58RN 页面秒开方案与实践
https://segmentfault.com/a/1190000040319692
2021-07-09T11:19:40+08:00
2021-07-09T11:19:40+08:00
fitfish
https://segmentfault.com/u/timetravel
13
<p>文中 metro-code-split 工具已开源,支持 RN 拆包和 dynamic import,欢迎大家 start。</p><p><a href="https://link.segmentfault.com/?enc=qDzBGH4M2WhYBP%2BdfKzJ%2Bg%3D%3D.gVl6Nmlo%2BCcFQW%2BUkWEr%2F%2B3Po1%2B2%2BHv%2BH0Mt5N%2BAb4n417UVCnKayZG7UkyrSZSk" rel="nofollow">https://github.com/wuba/metro...</a></p><p>以下是正文。</p><p>今天和大家分享的主题是《 58RN 页面秒开方案与实践》。先自我介绍一下,我叫蒋宏伟。我在 2015 年入职的 58,在 2016 年开始在 RN 方向上开始探索,这几年来,也推进不少 RN 性能方案的落地。在落地的过程中,一个被经常到的问题是:</p><p>做性能优化,耗时是降低了,但对业务来说有什么收益呢?</p><p>第一次,我确实被问住了。我就带着这个疑问,做了一个实验,去统计首屏时间和访问流失率的关系。我发现了一个有意思的规律。</p><p>首屏时间每降低 1 s,访问流失率降低 6.9%。</p><p><img src="/img/remote/1460000040319694" alt="GMTC 蒋宏伟 图片.003" title="GMTC 蒋宏伟 图片.003"></p><p>预测回头看,实际效果真有 6.9% 这么好吗?</p><p>我们拿着 6.9% 的收益数据,推动了几个业务进行了落地。整体来讲,预测的流失率收益跟实际的流失率收益其实是差不多的,但有分化。具体来讲,性能好的页面比预期收益差,性能差的页面比预期收益好。这也很好理解,性能好的页面,流失率已经很低了,进一步优化空间已经很小了。</p><p><img src="/img/remote/1460000040319695" alt="GMTC 蒋宏伟 图片.004" title="GMTC 蒋宏伟 图片.004"></p><p>当我们知道性能优化的收益会分化后,自然把更多的关注重点,落在了那些还没有实现秒开的页面上。我们就设计了几个指标,包括流失率、首屏时间、秒开收益。</p><p>流失率、首屏时间是事后指标。而秒开收益指标,是一个事前指标,它会告诉你,如果你的页面实现了秒开,你的流失率会降低多少?我们希望用这一系列的指标,来驱动业务进行性能优化。同时,我们也会给业务提供一些低成本、甚至无成本的优化方案,帮助业务的节约优化成本。</p><p>指标驱动业务,业务选择方案,方案提高收益,这就是我们设想的一个收益驱动的模型。</p><p><img src="/img/remote/1460000040319696" alt="GMTC 蒋宏伟 图片.005" title="GMTC 蒋宏伟 图片.005"></p><h2>首屏时间的采集方案</h2><p>本次的分享也会围绕着方案和指标这两块进行具体的展开。先说下指标这一块,最重要的指标是首屏时间,首屏时间算出来了,流失率和秒开收益其实就算出来了。因此,本次分享分为以下三个部分。</p><ul><li>第一部分讲的是,首屏时间的采集方案</li><li>第二部分再具体讲讲,性能优化的方案</li><li>最后跟大家总结和展望一下。</li></ul><p><img src="/img/remote/1460000040319697" alt="GMTC 蒋宏伟 图片.006" title="GMTC 蒋宏伟 图片.006"></p><p>我们先来看一个页面的加载流程,它大概有 5 个阶段:</p><ol><li>0 ms:用户进入</li><li>410 ms:首次内容绘制 FCP</li><li>668 ms:业务组件 DidUpdate</li><li>784 ms:最大内容绘制 LCP</li><li>928 ms:可视区加载完成</li></ol><p>第 2、3、4、5 个时间点,都可以定义为首屏时间。首屏时间定义不一样,耗时也不一样,而且差距非常大。因此,我们需要先选择一个指标,作为首屏时间的定义。</p><p><img src="/img/remote/1460000040319698" alt="GMTC 蒋宏伟 图片.007" title="GMTC 蒋宏伟 图片.007"></p><p>首屏时间我们选择的是 LCP 这个指标。为什么呢?</p><ul><li>第一,是因为 LCP 是最大内容绘制,这个时候页面中的主要元素其实已经展示出来。</li><li>第二,是因为 LCP 可以实现非侵入式的采集,不需要业务手动的去埋点。</li><li>第三,是因为 LCP 是 W3C 的草案,这是一个重要的原因。你告诉别人,你的首屏指标是 LCP,别人就懂了,不用过多解释。</li></ul><p><img src="/img/remote/1460000040319699" alt="image-20210706142415520" title="image-20210706142415520"></p><p>为了能让大家更好的理解 LCP 算法的实现,先给大家铺垫一下。</p><p>简单来讲,LCP 就是你看得到的最大元素,它渲染出来的时间。但是这里存在一个问题,比如我们第 2 张图片的最大元素,和第 5 张图片的最大元素,不是同一个元素。不同的元素,渲染出来的时间是不一样的,LCP 也不一样。也就是说,一个页面有多个 LCP,上报哪个 LCP 呢?应该上报最终收敛的 LCP 值,也就是可视区加载完成时的 LCP 值。</p><p><img src="/img/remote/1460000040319700" alt="GMTC 蒋宏伟 图片.009" title="GMTC 蒋宏伟 图片.009"></p><p>LCP 是 Web 的标准,在 RN 中并没有实现,应该怎么实现呢?</p><p>整体上讲,实现上大致分为 5 个步骤:</p><ol><li>在用户进入时,由 Native 线程记录 Start 时间戳。</li><li>由 Native 线程将 Start 时间戳注入到 JS Context 中。</li><li>由 JS 线程,监听页面中渲染元素的布局事件。</li><li>由 JS 线程,在页面渲染过程中进行计算,并不断更新 LCP 值。</li><li>由 JS 线程,计算得到 End 时间戳,并上报最终的 LCP 值。</li></ol><p>此时,最终上报的 LCP = End Time - Start Time。</p><p>其中难点是怎么收敛 LCP,也就是如何判断可视区完全加载。我们采用的规则是,当所有元素都加载完成,且底部的元素也已经加载完成时,可视区加载完成。元素有一个调用周期,先调用 render,再调用 layout。只调用了 render 的元素,是没有加载完成的元素。调用了 render 且调用了 layout 的元素,是加载完成的元素。能够判断一个元素是否加载完成了,也就能够判断可视区是否加载完成了。</p><p><img src="/img/remote/1460000040319701" alt="GMTC 蒋宏伟 图片.010" title="GMTC 蒋宏伟 图片.010"></p><h2>性能优化方案</h2><p>讲具体方案之前,先来讲一下,我们性能优化的整体思路。</p><p>做任何性能优化之前,我们要先分析性能的结构是什么,然后找到性能瓶颈,根据瓶颈来出具体的优化方案。</p><p>一个 RN 应用的性能结构,整体上看,分为 2 个部分, Native 部分和 JS 部分。再具体一点,又可以分为 6 个部分。以下是一个未优化的、比较复杂的、动态更新的 RN 应用的耗时结构:</p><ol><li>版本请求 200 ms</li><li>资源下载 470 ms</li><li>Native 初始化 350 ms</li><li>JS 初始化 380 ms</li><li>业务请求 420 ms</li><li>业务渲染 460 ms</li></ol><p>从大体上讲,上述 6 个结构,可以分为 3 个瓶颈。</p><ol><li>动态更新瓶颈,占比为 29%。</li><li>初始化瓶颈,占比为 32%。</li><li>业务耗时瓶颈,占比 39%。</li></ol><p><img src="/img/remote/1460000040319702" alt="GMTC 蒋宏伟 图片.013" title="GMTC 蒋宏伟 图片.013"></p><h3>瓶颈一:动态更新</h3><p>互联网产品有一个特征就是快速试错,这就要求业务能够快速迭代。为了支持业务快速迭代,这就要求应用能够动态更新。动态更新,肯定要发请求,要发请求就会拖慢性能,比如 Web。如果和 Native 一样,进行资源内置,性能会好很多,但又如何动态更新呢?</p><p>动态更新和性能似乎是一对矛盾体,有什么权衡之策吗?</p><p><img src="/img/remote/1460000040319703" alt="GMTC 蒋宏伟 图片.014" title="GMTC 蒋宏伟 图片.014"></p><p>我们开始想到的方案是,通过资源内置来提高页面性能,通过静默更新来动态更新。</p><p>当用户首次进来时,因为已经有内置资源了,所以不会有请求,页面可以直接渲染出来。与此同时,Native 线程会并行静默更新,询问服务端是否有最新版本,如果有就会下载 bundle,更新 cache。这样当用户下次进来的时候,可以使用到上次缓存的资源,直接渲染页面,同时并行静默更新。依此类推,用户每次进入时,都没有请求,可以直接渲染页面。</p><p>设计静默更新时有一个小细节需要注意。用户每次都使用的是上次缓存的资源,而不是线上最新的资源。因此存在一种风险,一个有严重 BUG 的版本被用户缓存下来了,且不能得到更新。为此,我们又设计了强制更新功能。在静默更新成功后,由 Native 线程通知 JS 线程,由业务根据具体的情况决定是否强制更新到最新版本。</p><p><img src="/img/remote/1460000040319704" alt="GMTC 蒋宏伟 图片.015" title="GMTC 蒋宏伟 图片.015"></p><p>资源内置 + 静默更新方案也有一些缺点:</p><ol><li>增加 App 体积。对于超级 App 而言,体积已经非常大了,要增加体积很难。</li><li>新版本覆盖率低。72 小时新版本覆盖率为 60% 左右,相对于 Web 方案来说比较低。</li><li>版本碎片化严重。多个内置版本和多次的动态更新,会导致版本碎片化的问题,推高了维护成本。</li></ol><p><img src="/img/remote/1460000040319705" alt="GMTC 蒋宏伟 图片.016" title="GMTC 蒋宏伟 图片.016"></p><p>因此,我们进行了一些改良。</p><p>用资源预加载,替代了资源内置。这就很大程度避免了包体积、覆盖率和碎片化的问题。静默更新依旧保留了,来更新可能出现的 BUG 版本。</p><p>资源预加载的话题其实已经讲烂了,我这里只从“权利”的角度,帮大家分析一下。</p><p>谁应该有预加载的权利?是 RN 框架,还是具体业务?把权限给框架,框架是可以把所有页面的资源都预加载了,但这样做明显效率很低,对于平台级 App 而言,一个 App 有几十个甚至上百个 RN 应用,大部分预加载的资源用户用不上,这就造成了浪费。把权限给业务,让具体业务一个个加载,又非常麻烦。</p><p>信息即权利,谁拥有信息,权利就给谁。最开始,框架没有任何有用信息,但业务可以根据业务数据,知道跳转到具体页面的比例,因此调用预加载的权利应该给业务。当用户已经使用过某个 RN 应用后,框架时知道这个信息的,这时权利应该给框架。框架可以在 App 启动后,进行版本预请求。</p><p><img src="/img/remote/1460000040319706" alt="GMTC 蒋宏伟 图片.017" title="GMTC 蒋宏伟 图片.017"></p><p>针对动态更新瓶颈,我们使用了资源预加载和静默更新的方案。耗时从未优化的 2280 ms,降低到了 1610 ms,降幅 29%。</p><p><img src="/img/remote/1460000040319707" alt="GMTC 蒋宏伟 图片.018" title="GMTC 蒋宏伟 图片.018"></p><h3>瓶颈二:框架初始化瓶颈</h3><p>首先,我们分析一下为什么框架初始化很慢。</p><p>JS 线程和 Native 线程是异步的通讯的,每次通讯都是通过 Bridge 进行序列化和反序列化完成的。在通讯之前,因为不在一个 Context 中,JS 线程和 Native 线程是相互不知道彼此存在的。因为 Native 不知道 JS 会使用哪个 NativeModule,所以 Native 需要初始化所有的 NativeModule,而不是按需初始化,这就是初始化性能慢的原因。</p><p>在 RN 新架构中,有计划把异步 Bridge 通讯替换成同步 JSI 通讯,从而实现按需初始化。但现在按需初始化功能还没有实现,因此,框架初始化的优化我们还是要做的。</p><p><img src="/img/remote/1460000040319708" alt="GMTC 蒋宏伟 图片.019" title="GMTC 蒋宏伟 图片.019"></p><p>我们给出的思路是,拆包内置和框架预执行。</p><p>我们的 App 是混合应用,首页用的不是 RN。因此,能在 App 启动后,先执行 RN 内置包,初始化所有的 NativeModules。在用户真正进入到 RN 页面时,性能自然会快上很多。</p><p>该方案最大的难点是拆包。如何把一个完整的 bundle 包,正确的拆成内置包和动态更新包呢?</p><p><img src="/img/remote/1460000040319709" alt="GMTC 蒋宏伟 图片.020" title="GMTC 蒋宏伟 图片.020"></p><p>刚开始我们踩了一个坑,希望能帮大家避免。</p><p>原来我们用的是 google 的 diff-match-patch 算法,该算法会对比新旧文本的区别,生成一个 patch 文件。同理,可以使用 diff-match-patch 算法,对比业务包和内置包的区别,生成一个 patch 动态更新包。</p><p>但是,patch 实际是一个“文本补丁”,“文本补丁” 是不能单独执行的。不能满足先执行内置包,再执行动态更新包的要求。</p><p><img src="/img/remote/1460000040319710" alt="GMTC 蒋宏伟 图片.021" title="GMTC 蒋宏伟 图片.021"></p><p>后来,我们改造了 metro 实现了正确拆包,从而实现了框架预加载。</p><p>一个完整的 bundle,由若干 module 组成,怎么区分某个 module 是属于内置包,还是动态更新包呢?内置 module 的路径或者说 ID,有一个特征,它是在 node_modules/react/xxx 或 node_modules/react-native/xxx 路径下的。可以先提前记录所有的内置 module 的 IDs,在打包时,将属于内置 module 都过滤掉,生成只包含业务 module 的动态更新包。</p><p>metro 拆包的动态更新包是“代码补丁”,可以直接执行,能够满足先执行内置包,再执行动态更新包的要求。</p><p>其中有一个细节是,内置包中要增加一行 require(InitializeCore) 的代码,来调用内置包中 defined 的 modules。增加这一行代码,首屏耗时大概可以多减少 90 ms。</p><p><img src="/img/remote/1460000040319711" alt="GMTC 蒋宏伟 图片.022" title="GMTC 蒋宏伟 图片.022"></p><p>针对框架初始化瓶颈,我们使用了拆包内置和框架预执行的方案。耗时从未优化的 1610 ms,降低到了 1300ms,整体降幅 43%。</p><p><img src="/img/remote/1460000040319712" alt="GMTC 蒋宏伟 图片.023" title="GMTC 蒋宏伟 图片.023"></p><h3>瓶颈三:业务请求瓶颈</h3><p>动态更新瓶颈、框架耗时瓶颈优化完后,再来看一下业务瓶颈。业务瓶颈主要由业务请求和业务渲染两部分组成,请求是比较好优化的,所以我们先针对业务请求瓶颈做优化。</p><p>业务请求的优化,其实有很多常用方案。</p><ul><li>业务数据缓存</li><li>在上一个页面预加载下一个页面的业务数据</li></ul><p>但是,不是每个应用它都适合做缓存,不是每个应用它的数据都适合在上个页面预加载。因此,我们需要一种更加通用的方案。仔细观察一下,Init 部分和业务请求部分是串行的,是不是可以改为并行?</p><p><img src="/img/remote/1460000040319713" alt="GMTC 蒋宏伟 图片.024" title="GMTC 蒋宏伟 图片.024"></p><p>我们的思路是,由 Native 代替 JS,在用户进入页面时直接并行的请求业务数据。</p><p>具体方案如下。</p><ol><li>在 Native 下载的资源文件中,会同时包含 Biz 业务包和原始的业务请求的 URL。</li><li>原始 URL 中会包含动态的业务参数,该变量会根据事先约定的规则进行转换。例如, <code>58.com/api?user=${user}</code>将会转换为 <code>58.com/api?user=GTMC</code>。</li><li>Native 并行执行 Biz 包渲染页面,和发起 URL 请求获取业务数据。</li><li>JS 侧直接调用 PreFetch(cb),即可获得 Native 侧请求的数据。</li></ol><p><img src="/img/remote/1460000040319714" alt="GMTC 蒋宏伟 图片.025" title="GMTC 蒋宏伟 图片.025"></p><p>针对业务请求瓶颈,我们使用了业务数据并行加载的方案。耗时从未优化的 1300 ms,降低到了 985 ms,整体降幅 57%。</p><p>应用上述方案,大部分页面都可以实现秒开。那还有性能优化的空间吗?</p><p><img src="/img/remote/1460000040319715" alt="GMTC 蒋宏伟 图片.026" title="GMTC 蒋宏伟 图片.026"></p><h3>代码执行瓶颈</h3><p>RN 页面渲染的慢的另外一个原因是,RN 需要执行完整的 JS 文件,即使 JS 中有不需要执行的代码。</p><p>我们来看一个案例。一个页面包含 3 个 tab,用户进来时只会看到 1 个 tab。理论上,只需要执行 1 个 tab 的代码即可。但实际上,另外 2 个看不见的 tab 的代码也会下载和执行,拖慢了性能。</p><p>RN 代码懒加载和懒执行的能力来提高性能,类似 Web 中的 Dynamic Import。</p><p><img src="/img/remote/1460000040319716" alt="GMTC 蒋宏伟 图片.028" title="GMTC 蒋宏伟 图片.028"></p><p>RN 官方并没有提供 dynamic import ,于是我们决定自己做。</p><p>目前,dynamic import demo 已经在 RN 0.64 版本中跑通了。业务初始化时,可以只执行 Biz 业务包,在跳转到 Foo、Bar 两个 dynamic 页面时,才会动态的下载对应的 chunk 动态包。退出已进入的 dynamic 页面再次进入,不会再下载,会利用原有的缓存直接渲染 dynamic 页面。</p><p><img src="/img/remote/1460000040319717" alt="GMTC 蒋宏伟 图片.030" title="GMTC 蒋宏伟 图片.030"></p><p>RN 的 dynamic import 实现,我们参考的是 TC39 的规范。</p><p>业务只需要写一行代码 <code>import("./Foo")</code> ,就可以实现代码懒加载和懒执行。剩下的所有工作,都在框架层和平台层做了。</p><p>在 runtime 运行时,业务执行 <code>import("./Foo")</code> 之后,框架层会去判断 <code>./Foo</code> 路径对应的 module 是否已经 install。如果没有 install,就会通过 <code>./Foo</code> 路径找到对应 chunk 包的 URL 地址,接着下载和执行 chunk,最后渲染 Foo Component。</p><p>Chunk 包的 URL 是一个 CDN 地址,显然上传 CDN 和记录 Path 和 URL 关系的工作,不是在 runtime 运行时做的,而是在 compile time 编译时做的。</p><p><img src="/img/remote/1460000040319718" alt="GMTC 蒋宏伟 图片.031" title="GMTC 蒋宏伟 图片.031"></p><p>在平台层的编译过程中,会将 Path 和 URL 的关系表存在 Biz 包中,这样 Runtime 才能通过 Path 找到对应的 URL。</p><p>完成这个过程,大致分为 5 个部分。</p><ol><li>Project:一个项目由若干个文件组成,文件之间会有相互依赖关系。</li><li>Graph:每个文件会生成一个对应的 module,所有 module 及其依赖关系组成了一个 graph。</li><li>Modules:给 dynamic module 的集合进行“着色”,进行区分。</li><li>Bundles:将多个 module 的集合都打包成多个 bundle。</li><li>CND:将 bundle 上传至 CDN。</li></ol><p>其中最为关键的步骤是,给 dynamic module 的集合进行着色。</p><ol><li>分解着色:一个 Graph 的着色的情况可以分解为若干个基础的 case,这些基础 case 的着色方案是已经确定下来的。</li><li>dynamic map:着色完成后,会将“绿色”“蓝色”这些 dynamic module 的根路径 Path 记录下来,并和其 bundle 的 CDN URL 地址组成一个 dynamic map。</li><li>Path to URL:Dynamic map 会打包到“白色”的 Biz 业务包中,因此在 runtime 调用 <code>import()</code> 时,可以通过 Path 找到对应的 URL。</li></ol><p><img src="/img/remote/1460000040319719" alt="GMTC 蒋宏伟 图片.032" title="GMTC 蒋宏伟 图片.032"></p><p>上述很多细节没有展开讲,关注实现细节实现的同学可以关注一下我们的开源工具 metro-code-split。</p><p>metro-code-split:<a href="https://link.segmentfault.com/?enc=B79N%2BlyOub66NEtaBB3%2BnA%3D%3D.uYRUqEvMLCgSoUGAA2QOIxJFrk2bfYx7z3nFTCyFl1tZhWW5I6RYLheK%2Bp6JGjg4" rel="nofollow">https://github.com/wuba/metro...</a></p><ul><li>基于 metro</li><li>支持 DLL 拆包</li><li>支持 RN Dynamic Import</li></ul><p><img src="/img/remote/1460000040319720" alt="GMTC 蒋宏伟 图片.036" title="GMTC 蒋宏伟 图片.036"></p><h2>总结与展望</h2><p>我们通过分析性能结构,找到了 3 类性能瓶颈,并产出了不同的优化方案。下图就是我们秒开方案的集合,图中列举(预期)收益、生效范围和生效场景,希望对大家的技术选型有所帮助。</p><p>在最新版本中, RN 新架构的很多功能已经成熟,我们也在进行积极探索。其中最让人惊喜的是 Hermes 引擎,已经可以同时在 iOS 和 Android 中使用了。Hermes 引擎相对于原来的 JSCore 引擎最大的区别是,Hermes 会进行预编译,在编译时将 JS 文件编译成 bytecode 文件,这样在运行时就能直接使用 bytecode 文件进行执行了,能够大幅减少 JS 执行耗时。经过测试我们发现,一个耗时 140 ms 的页面,能够降到 40 ms,降幅 80%。</p><p><img src="/img/remote/1460000040319721" alt="GMTC 蒋宏伟 图片.038" title="GMTC 蒋宏伟 图片.038"></p><p>在我们为业务提供性能优化方案的同时,我们也需要关注业务的落地情况。为了能让更多业务实现秒开,我们通过非侵入式收集的方式,收集了流失率、首屏时间、秒开收益等指标。在我们的实践中,这种将技术优化需求与业务收益挂钩的方式,更容易被业务接受,推动起来也更容易。</p><p>最后,希望我们的秒开方案和收益驱动的实践,能给大家带来启发,谢谢大家。</p><p><img src="/img/remote/1460000040319722" alt="GMTC 蒋宏伟 图片.039" title="GMTC 蒋宏伟 图片.039"></p>
React Native 无限列表的优化与实践
https://segmentfault.com/a/1190000021805800
2020-02-21T19:56:40+08:00
2020-02-21T19:56:40+08:00
fitfish
https://segmentfault.com/u/timetravel
2
<p>首发于《58技术》公众号</p>
<p><strong>背景</strong></p>
<p>对于分类信息流形态的产品,用户通过左右滑动切换分类,通过不断上滑来浏览更多的信息。</p>
<p>用标签页(Tabs)实现切换分类,用无限列表(List)实现上滑浏览 <br>手势上滑,页面向上滚动,展示更多列表项(List Item)</p>
<p>手势左滑,页面向左滚动,展示右边的列表(蓝色)</p>
<p>因为 React Native(RN) 可以用较低的成本,同时满足用户体验、快速迭代,和跨App 开发上线的要求。所以,对于分类信息流形态的产品技术选型使用的是 RN。在使用 RN 开发首页的过程中,我们填过很多坑,希望这些填坑经验,对读者有借鉴意义。</p>
<p>第一,RN 官方提供的无限列表(ListView/FlatList)性能太差,一直被业内吐槽。通过实践对比,我们选择了内存管理效率更优的第三方组件——RecyclerListView。</p>
<p>第二,RecyclerListView 需要知道每个列表项的高度,才能正确渲染。如果,列表项高度不确定,怎么处理?</p>
<p>第三,标签页和无限列表组合使用时,会遇到一些问题。首先,标签页中有多个无限列表,怎样有效管理内存?其次,标签页可以左右滑动,无限列表中也有左右滚动的内容组件,二者手势区域重叠时,如何指定组件优先处理?</p>
<p><strong>列表的技术选型</strong></p>
<p><strong>1<strong><em><em>、</em></em></strong>ListView</strong></p>
<p>在实践开发分类信息流形态的产品过程中,我们开始尝试过使用 RN,版本是 0.28。当时,无限列表用的是官方提供的 ListView 。ListView 的列表项始终不会被销毁,这会导致内存不断增加,导致卡顿。前 100 条信息滚动非常流畅,200 条时就开始卡顿,到 1000 条时就基本就滑不动了。当时,也没有特别好的解决方案,只能在产品上进行妥协,将无限列表降级为有限列表。</p>
<p><strong>2<strong><em><em>、</em></em></strong>FlatList</strong></p>
<p>FlatList 是在 RN 0.43 版本新增的,拥有内存回收的功能,可以用来实现无限列表。我们第一时间就跟进了,把 RN 版本进行升级。虽然, FlatList 可以实现无限列表,但体验上总归还是有所欠缺的。FlatList 在 iOS 表现很流畅,但在 Android 某些机型上会有略有卡顿。</p>
<p><strong>3<strong><em><em>、</em></em></strong>RecyclerListView</strong></p>
<p>在实践开发中,我们技术选型还尝试采用了 RecyclerListView。 RecyclerListView 实现了内存的复用,性能也是更好。无论是 iOS 还是 Android 都表现的很流畅。</p>
<p><strong>流畅度</strong><strong>对比</strong></p>
<p>衡量流畅度的关键指标是帧率,帧率越高越流畅,越低越卡顿。我们用 RecyclerListView 和 FlatList 分别实现了相同功能的无限列表,在 Android 手机中进行了测试,滚动帧率如下。</p>
<p>滚动帧率对比(以 Android OPPO R9 为例)</p>
<p><strong>Android</strong></p>
<p><strong>FlatList</strong></p>
<p><strong>RecyclerListView</strong></p>
<p><strong>< 20</strong> 帧占比</p>
<p>16%</p>
<p>3%</p>
<p>主观体验</p>
<p>一般卡</p>
<p>流畅</p>
<p><strong>实现原理</strong><strong>对比</strong></p>
<p>ListView 、FlatList、RecyclerListView 都是 RN 的列表组件,为什么它们之间性能差距这么大? 我们对其实现原理进行了一些研究。</p>
<p><strong>1. ListView</strong> 的实现思路比较简单,当用户上滑加载新的列表内容时,会不断地新增列表项。每次新增,都会导致内存增加,增加到一定程度后,可使用的内存空间不足,页面就会出现卡顿。</p>
<p><strong>2. FlatList</strong> 取了个巧,既然用户只能看到手机屏幕里的内容,那么只用将用户看到的(可视区域)和即将看到的(靠近可视区域)部分渲染出来就行了。而用户看不到的地方(远离可视区域),就删掉,用空白元素占位就行。这样,空白区域的内存就得到了释放。</p>
<p>要实现无限加载,必须要考虑如何高效利用内存。FlatList “删除一个,新增一个” 是一个思路。 RecyclerListView “结构类似,改改再用” 是另一个思路。</p>
<p><strong>3. RecyclerListView</strong> 假设列表项的种类可枚举的。所有列表项可以分为若干类,比如,一张图片的图文布局是一类,两张图片的图文布局是一类,只要布局相似就是同一类列表项。开发者,需要对类型进行事先的声明。</p>
<p>const types = {</p>
<p>ONE_IMAGE: 'ONE_IMAGE', // 一张图片的图文布局</p>
<p>TWO_IMAGE: 'TWO_IMAGE' // 两张图片的图文布局</p>
<p>}</p>
<p>如果,用户即将看见的列表项,和用户看不见的列表项,类型一样。就把用户看不见的列表,修改成用户即将看到的列表项。修改不涉及到组件的整体结构,只涉及组件的属性参数,通常包括,文本、图片地址,还有展示的位置。</p>
<p>{/* 把用户看不见的列表项 */}</p>
<p><View style={{position: 'absolute', top: disappeared}}></p>
<p><Text>一行文本</Text></p>
<p><Image source={{uri: '1.png'}}/></p>
<p><View> </p>
<p>{/* 修改成用户即将看见的列表项 */}</p>
<p><View style={{position: 'absolute', top: visible}}></p>
<p><Text>一行文本~~</Text></p>
<p><Image source={{uri: '2.png'}}/></p>
<p><View></View></p>
<p>从三者原理上对比,我们可以发现,在内存使用效率方面,内存复用的 RecyclerListView 比内存回收的 FlatList 更好,FlatList 又比内存不回收的ListView 更好。</p>
<p>原理对比 <br>手势上滑,页面向上滚动,加载更多列表项(深绿色)</p>
<p><strong>RecyclerListView</strong> <strong>的实践</strong></p>
<p>RecyclerListView 复用列表项的位置是需要经常变化的,因此用的是绝对定位 position: absolute 布局,而不是从上往下的 flex 布局。使用了绝对定位,就需要知道列表项的位置(top)。为了使用者的方便,RecyclerListView 让开发者传入所有列表项的高度(height),内部自动推断出其位置(top)。</p>
<p>高度确定的列表项</p>
<p>在最简单例子中,所有列表项的高度都是已知的。只需将将高度、类型数据,和 Server 的数据进行合并,就可以得到 RecyclerListView 的状态(state)。</p>
<p>const types = {</p>
<p>ONE_IMAGE: 'ONE_IMAGE', // 一张图片的图文布局</p>
<p>TWO_IMAGE: 'TWO_IMAGE' // 两张图片的图文布局</p>
<p>}</p>
<p>// server data</p>
<p>const serverData = [</p>
<p>{ img: ['xx'], text: '' },</p>
<p>{ img: ['xx', 'xx'], text: '' },</p>
<p>{ img: ['xx', 'xx'], text: '' },</p>
<p>{ img: ['xx'], text: '' },</p>
<p>]</p>
<p>// RecyclerListView state</p>
<p>const list = serverData.map(item => {</p>
<p>switch (item.img.length) {</p>
<p>case 1:</p>
<p>// 高度确定,为 100px</p>
<p>return { ...item, height: 100, type: types.ONE_IMAGE, }</p>
<p>case 2:</p>
<p>return { ...item, height: 100, type: types.TWO_IMAGE, }</p>
<p>default:</p>
<p>return null</p>
<p>}</p>
<p>})</p>
<p><strong>高度不确定的列表项</strong></p>
<p>并不是所有列表项的高度,都是确定的。比如,上文下图的列表项,虽然图片高度是确定的,但是文本高度是由 Server 传过来的文本长度决定的。文字可能一行,可能两行,可能多行,文字有几行是不确定的,因此列表项的高度也不确定。那么,应该如何使用 RecyclerListView 组件呢?</p>
<p><strong>1<strong><em><em>、</em></em></strong>Native</strong> <strong>异步获取高度</strong></p>
<p>Native 端,实际上有提前计算文本高度的 API —— fontMetrics。将 Native fontMetrics API 暴露给 JS,JS 不就具有了提前计算高度的能力了。此时,RecyclerListView 需要的 state 计算方法如下,其值是 promise 类型。</p>
<p>const list = serverData.map(async item => {</p>
<p>switch (item.img.length) {</p>
<p>case 1:</p>
<p>return { ...item, height: await fontMetrics(item.text), type: types.ONE_IMAGE, }</p>
<p>case 2:</p>
<p>return { ...item, height: await fontMetrics(item.text), type: types.TWO_IMAGE, }</p>
<p>default:</p>
<p>return null</p>
<p>}</p>
<p>})</p>
<p>每次调用 fontMetrics,都需要 oc/java 与 js 进行一次异步通讯。而异步通讯是非常耗时的,该方案会明显增加渲染耗时。此外,新增 fontMetrics 接口的方案,依赖 Native 发版,只能在新版本中使用,老版本用不了。因此,我们没有采用。</p>
<p><strong>2</strong><strong>、位置修正</strong></p>
<p>开启 RecyclerListView 的forceNonDeterministicRendering=true 属性后,会自动进行布局位置纠正。其原理是,开发者事先估算出列表项的高度, RecyclerListView 先按估算高度把视图渲染出来。当视图渲染出来后,通过onLayout 获取列表项真正的高度,再通过动画将视图位移到正确的位置。</p>
<p>位置修正</p>
<p>该方案,在估计高度偏差小的场景下很适用,但在估算偏差大的场景下,会明观察到明显的重叠和位移的现象。那么,有没有一种估算偏差小,耗时又短的方法呢?</p>
<p><strong>3<strong><em><em>、</em></em></strong>JS</strong> <strong>估算高度</strong></p>
<p>大部分情况下,列表项高度不确定都是由文本长度的不确定导致的。因此,只要能大致估算文本的高度就行。</p>
<p>1 个 17px 字号 20px 行高的汉字,渲染出来的宽度为 17px,高度为 20px。如果,容器宽度足够宽,文字不折行, 30 个的汉字,渲染出来的宽度为 30 * 17px = 510px,高度依旧为 20px。如果,容器宽度只有 414px,那么显然会折成 2 行,此时文字高度为 2 * 20px = 40px。其通用公式为:</p>
<p>行数 = Math.ceil( 文字不折行宽度 / 容器宽度 )</p>
<p>文字高度 = 行数 * 行高</p>
<p>实际上,字符类型不仅有汉字,还有小写字母、大写字母、数字、空格等,此外,渲染字号也各有不同。因此,最终的文本行数算法也更为复杂。我们通过多种真机测试,得出了 17px 下的各类字符类型的平局渲染宽度,比如大写字母 11px,小写字母 8.6px 等等,算法摘要如下:</p>
<p>/**</p>
<p>* @param str 字符串文本</p>
<p>* @param fontSize 字号</p>
<p>* @returns 不折行宽度</p>
<p>*/</p>
<p>function getStrWidth(str, fontSize) {</p>
<p>const scale = fontSize / 17;</p>
<p>const capitalWidth = 11 * scale; // 大写字母</p>
<p>const lowerWidth = 8.6 * scale;// 小写字母</p>
<p>const spaceWidth = 4 * scale; // 空格</p>
<p>const numberWidth = 9.9 * scale; // 数字</p>
<p>const chineseWidth = 17.3 * scale; // 中文和其他</p>
<p>const width = Array.from(str).reduce(</p>
<p>(sum, char) =></p>
<p>sum +</p>
<p>getCharWidth(char, {</p>
<p>capitalWidth,</p>
<p>lowerWidth,</p>
<p>spaceWidth,</p>
<p>numberWidth,</p>
<p>chineseWidth,</p>
<p>}),</p>
<p>0,</p>
<p>);</p>
<p>return Math.floor(width / fontSize) * fontSize;</p>
<p>}</p>
<p>/**</p>
<p>* @param string 字符串文本</p>
<p>* @param fontSize 字体大小</p>
<p>* @param width 渲染容器宽度</p>
<p>* @returns 行数</p>
<p>*/</p>
<p>function getNumberOfLines(string, fontSize, width) {</p>
<p>return Math.ceil(getStrWidth(string, fontSize) / width);</p>
<p>}</p>
<p>上述纯 js 估算文字行数的算法,实测的准确率在 90% 左右,估算耗时为毫秒级别,能够很好的满足我们需求。</p>
<p>4<strong>、</strong><strong>JS</strong> <strong>估算高度</strong> <strong>+</strong> <strong>位置修正</strong></p>
<p>因此,我们的最终方案为,通过 JS 估算出文本行数,并得出文本高度,再进一步地推断出列表项的布局高度。并开启 forceNonDeterministicRendering=true,在估算有偏差时,自动动画修正列表项的位置。</p>
<p><strong>方法</strong></p>
<p><strong>优点</strong></p>
<p><strong>缺点</strong></p>
<p><strong>Native</strong> <strong>异步获取高度</strong></p>
<p>获取正确高度,视图位置无偏差</p>
<p>异步获取,耗时长,渲染慢</p>
<p><strong>位置修正</strong></p>
<p>视图位置有偏差时,动画位移进行修正</p>
<p>前偏差大时,用户体验差</p>
<p><strong>JS</strong> <strong>估算高度</strong></p>
<p>估算高度准确在 90% 左右</p>
<p>有 10% 左右的视图,渲染不正确</p>
<p><strong>JS</strong> <strong>估算高度</strong> <strong>+</strong><strong>位置修正</strong></p>
<p>估算高度准确在 90% 左右。有大约 10% 偏差可以进行自动修正。</p>
<p>增加了 js 估算高度的步骤</p>
<p><strong>标签页中的无限列表</strong></p>
<p>对于分类信息流形态的产品,有的会包含多样化标签,每个标签都有特定的内容,其中大部分标签页是无限列表。如果,所有标签页的内容都同时存在,内存得不释放,也会导致性能问题。</p>
<p><strong>内存回收</strong></p>
<p>沿用上面的处理列表内存的思路,我们可以选择内存回收,或内存复用思路。内存复用的前提是,复用内容的结构相同,只有数据有变化。实际业务中,产品已经将相似内容进行了分类,每个标签页各有各的特点,很难复用。因此,对于标签页而言,内存回收是更好的选择。</p>
<p>整体思路是,可视区域内的标签页肯定要显示出来。最近在可视区域的显示过的内容,根据情况进行保留。远离可视区的内容,需要销毁。</p>
<p>销毁远离可视区的标签页</p>
<p><strong>手势重叠的处理</strong></p>
<p>标签页 TabView 1.0 使用的是 RN 自带的手势系统,单独的左右滑动切换的标签页,自带的手势系统运行良好。如果可视区中,既有可以左右滑动切换的标签页,又有可以左右滚动的内容区域。用户向左滚动手势重叠区域时,是标签页响应滚动,还是内容区域响应,还是同时响应呢?</p>
<p>手势重叠区域,向左滚动,谁响应?</p>
<p>由于 RN 的手势识别,是同时在 oc/java 渲染主线程和 js 线程中同时进行的,这种奇怪的处理方式,使得手势很难得到精准的处理。这导致 TabView 1.0 不能很好的处理手势重叠的业务场景。</p>
<p>在 TabView 2.0 中,集成了新的手势系统 React Native Gesture Handler。新的手势系统,是声明式的,由纯 oc/java 渲染主线程处理的手势系统。我们可以在 JS 代码中,对手势的响应方式进行提前声明,让标签页等待(waitFor) 内容区域的手势响应。也就是说,重叠的区域手势,只作用于内容区域。</p>
<p><strong>总结</strong></p>
<p>本文介绍了,我们在使用 RN 开发分类信息流形态的产品的无限列表中,遇到的一些常见问题,以及如何进行技术考量、优化和选择的。希望能对大家有借鉴意义。</p>
Facebook 专门推出的 Hermes 引擎性能并没有那么好
https://segmentfault.com/a/1190000019818195
2019-07-19T21:52:30+08:00
2019-07-19T21:52:30+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<p>调研 Facebook 为 React Native 专门推出的 JavaScript 引擎 Hermes。<br>结论:</p>
<ul>
<li>Hermes 可以替换 Android 默认的 JS 引擎。 Hermes 特点是通过将 JS 预编译成字节码,降低了白屏时间。</li>
<li>官方给出的数据是 Mattermo App 白屏时间,从 6.46s 减少到 3.90s,减少比例为 40%。</li>
<li>实际测试渲染包含 5000 个 Text 的 App的白屏时间(包括 Native 初始化),从 3.7s 减少到 3.4s,减少比例为 8%。</li>
<li>实际测试渲染包含 100 个初始化页面再次渲染的白屏时间(不包括 Native 初始化),从 2.52s 减少到 2.37s,减少比例为 5%。</li>
<li>在计算性能方面,以 V8 引擎的 benchmark 为得分标准,Hermes 得分比默认引擎低 79%。</li>
</ul>
Hooks + Context:状态管理的新选择
https://segmentfault.com/a/1190000019679398
2019-07-05T18:25:16+08:00
2019-07-05T18:25:16+08:00
fitfish
https://segmentfault.com/u/timetravel
10
<p>React 16.3 版本,正式推了出官方推荐的 context API —— 一种跨层级的数据传递方法。React 16.8 版本,推出了全新的 hooks 功能,将原本只有 class 组件才有的状态管理功能和生命周期函数功能,赋予了 function 组件。Hooks 配合 context 一起使用,为 react 状态管理提供了一种新的选择。这可能会减少开发者对 redux 等状态管理库的依赖。</p>
<p>本文首先会对官方的 context 作简单介绍,并搭建一个十分简单的使用全局状态的应用。然后再对 hooks 的基本 API <code>useState</code> <code>useEffect</code> 做基本介绍。接着使用 <code>useContext</code> hooks 对应用进行重构,让 context 的使用变得更优雅。再使用 <code>useReducer</code> hooks 来管理多个状态。最后,待充分理解 hooks 和 context 之后,我们将它们搭配起来用,对整个应用进行状态管理。</p>
<h2>Context 概述</h2>
<p>React 中存在一个众所周知的难题,那就是如何管理全局状态。即便是最基础的全局状态跨越层级传递,也是非常麻烦。此时,首选的解决方案就是使用状态管理库,如 redux。Redux 本身是一个 API 非常少的状态管理工具,其底层也是用 context 实现的。在一些状态管理不是那么复杂,但是又有跨越层级传递数据的需求时,不妨考虑使用 context 直接实现。</p>
<p>例如,一个 <code>Page</code> 组件包含全局状态 <code>user</code> ,需要经过多次<code>props</code>的传递,层级很深的 <code>Avatar</code> 组件才能使用它。</p>
<pre><code><Page user={user} />
// ... render ...
<PageLayout user={user}/>
// ... render ...
<NavigationBar user={user} />
// ... render ...
<Avatar user={user} /></code></pre>
<h3>Context :跨层级传递数据</h3>
<p>Context 提供了一种方法,解决了全局数据传递的问题,使得组件之间不用显式地通过 props 传递数据。</p>
<ul>
<li>React.createContext: 创建一个 <code>Context</code> 对象,该对象拥有 Provider 和 Consumer 属性。</li>
<li>Context.Provider: 接受一个 value 参数,在 value 参数更新的时候通知 Consumer。</li>
<li>Context.Consumer: 订阅 value 参数的改变。一旦 value 参数改变,就会触发它的回调函数。</li>
</ul>
<p>使用 context 重构的之后,跨层级传递数据就变得容易很多:</p>
<pre><code>// 创建一个 context
const UserContext = React.createContext();
class App extends React.Component {
state = { user: "崔然" };
setUser = user => {
this.setState({ user });
};
render() {
// 设置 context 当前值为 {user, setUser}
return (
<UserContext.Provider value={{
user:this.state.user,
setUser: this.setUser
}}>
<Page />
</UserContext.Provider>
);
}
}
// ... Page render ...
<PageLayout />
// ... PageLayout render ...
<NavigationBar />
// ... NavigationBar render ...
// 无论组件有多深,都可以**直接**读取 user 值
<UserContext.Consumer>
{ ({user, setUser}) => <Avatar user={user} setUser={setUser}/> }
</UserContext.Consumer></code></pre>
<h3>避免全局渲染</h3>
<p>但是,在使用 context 时,有些写代码的小技巧,需要特别注意。不然在全局状态改变时, Provider 的所有后代组件都会重新渲染。例如,用户点击 <code>Avatar</code> 组件后,将 <code>崔然</code> 更新为 <code>CuiRan</code>,这时会调用根组件的 <code>setUser</code> 方法。根组件 <code>setState({ user })</code> 更新状态,会导致整颗组件树重新渲染。</p>
<pre><code>const Avatar = ({ user, setUser }) => {
// 用户点击改变全局状态
return <div onClick={() => setUser("CuiRan")}>{user}</div>;
};
class App extends React.Component {
state = { user: "崔然" };
setUser = user => {
this.setState({ user });
};
// ... 渲染整颗组件树
}</code></pre>
<p>有没有解决方案呢?当然有!</p>
<p>创建一个只接收 <code>props.children</code>的新组件 <code>AppProvider</code> ,并将 <code>App</code> 组件中的逻辑都移到 <code>AppProvider</code>组件中。通过备注的 console 日志可以看到,该方式避免了不必要的渲染。</p>
<pre><code>const Avatar = ({ user, setUser }) => {
// 用户点击改变全局状态
return <div onClick={() => setUser("CuiRan")}>{user}</div>;
};
// 将 App 逻辑移到 AppProvider
const UserContext = React.createContext();
class AppProvider extends React.Component {
state = { user: "崔然" };
setUser = user => {
this.setState({ user });
};
render() {
return (
<UserContext.Provider
value={{
user: this.state.user,
setUser: this.setUser
}}
>
{this.props.children}
</UserContext.Provider>
);
}
}
// APP 只保留根组件最基本的 JSX 嵌套
const App = () => (
<AppProvider>
<Page />
</AppProvider>
);
// ... Page not render ...
<PageLayout />
// ... PageLayout not render ...
<NavigationBar />
// ... NavigationBar not render ...
// Consumer 监听到 Provider value 的改变
<UserContext.Consumer>
{/* **only** Avatar render */ }
{({user, setUser}) => <Avatar user={user} setUser={setUser}/>}
</UserContext.Consumer></code></pre>
<p>为什么?为什么把 <code>App</code> 上的全局状态及设置状态的方法移到 <code>AppProvider</code> 上,就能避免不必要的渲染?在 <code>props.children</code> 方案中:</p>
<pre><code>// 1. App 本身没有全局状态改变,因此 <Page/> 不会重渲染
const App = () => (
<AppProvider>
<Page />
</AppProvider>
);
// 2. Provider value 变化,因此会触发 Consumer 的监听函数。
<UserContext.Provider
value={{
user: this.state.user,
setUser: this.setUser
}}
>
{ /* 3. this.props.children 只是 <Page /> 的引用
但并不会调用 <Page />,即调用 createElement('Page') */ }
{this.props.children}
</UserContext.Provider></code></pre>
<p>虽然,context 解决了数据跨层级传输的问题,但是还遗留了一些问题:</p>
<ol>
<li>Consumer 的回调取值的写法 <code><Consumer>{ value => <></></Consumer></code> 不优雅。</li>
<li>单个状态和状态改变很好传递,但是多个状态和对应的状态改变传递依旧不方便。</li>
<li>多个全局状态,如何管理?</li>
</ol>
<p>没关系,且看 hooks 闪亮登场,将这些问题一一击破。</p>
<h2>Hooks 概述</h2>
<p>考虑到有些朋友不是很了解 hooks,本文先介绍一下 hooks 的基本用法 。Hooks 让我们可以在 function 组件中使用状态和生命周期函数,并赋予了一些更强大的功能。这也意味着,在 React 16.8 之后,我们再不需要写 class 组件。再强调一次,我们再不需要写 class 组件!</p>
<ol>
<li>
<code>useState</code>: 允许在 function 组件中,声明和改变状态。在此之前,只有 class 组件可以。</li>
<li>
<code>useEffect</code>:允许在 function 组件中,抽象地使用 React 的生命周期函数。开发者可以使用更函数式的、更清晰的 hooks 的方式。</li>
</ol>
<p>使用 hooks 对带有本地状态的 <code>Avatar</code> 组件进行重构说明:</p>
<pre><code>import React, { useState, useEffect } from 'react';
const Avatar = ({ user, setUser }) => {
// 创建 user 状态和修改状态的函数
const [user, setUser] = useState("崔然");
// 默认 componentDidMount/componentDidUpdate 时会触发回调
// 也可以使用第二个参数,指定触发时机
useEffect(() => {
document.title = `当前用户:${user}`;
});
// 使用 setUser 改变状态
return <div onClick={() => setUser("CuiRan")}>{user}</div>;
};</code></pre>
<p>接着,我们继续了解 context 的 hooks 用法 —— <code>userContext</code> 。</p>
<h3>useContext:更优雅的 context</h3>
<p>在 react 引入 hooks 后,使得 context 的消费更简单了,开发者可以很优雅地直接获取。下面我们使用 <code>useContext</code> 对 <code>User</code> 组件进行重构。</p>
<pre><code>// 重构前
const User = () => {
return (
<UserContext.Consumer>
{({ user, setUser }) => <Avatar user={user} setUser={setUser} />}
</UserContext.Consumer>
);
};
// 重构后
const User = () => {
// 直接获取,不用回调
const { user, setUser } = useContext(UserContext);
return <Avatar user={user} setUser={setUser} />;
};</code></pre>
<p>就是这么简单!无论 <code>context</code> 包含什么,是数字、字符串,还是对象、函数,都可以通过<code>useContext</code>访问它。</p>
<h3>useReducer:自带的状态管理</h3>
<p>当组件同时使用多个<code>useState</code>方法时,需要一个一个的声明。状态多了,就一大溜的声明。比如:</p>
<pre><code>const Avatar = ({ user, setUser }) => {
const [user, setUser] = useState("崔然");
const [age, setAge] = useState("18");
const [gender, setGender] = useState("女");
const [city, setCity] = useState("北京");
// more ...
};</code></pre>
<p><code>useReducer</code> 实际是 <code>useState</code> 的一个变种,解决了上述多个状态,需要多次使用 <code>useState</code> 的问题。</p>
<p>当你看到 <code>useReducer</code> 时,是不是非常熟悉?想起了redux 中的 <code>reducer</code> 函数。对!React 提供的 <code>useReducer</code> 函数,它就是使用 (use) reducer 函数作为参数。<code>useReducer</code> 接受的 <code>reducer</code> 参数,本质和 redux 的是一样的。然后 <code>useReducer</code> 会返回 <code>state</code> 和 <code>dispath</code> 方法,返回的 <code>dispath</code> ,本质上和 redux 的也是一样的。</p>
<p>让我们使用 <code>useReducer</code> 将带有本地状态的 <code>Avatar</code> 组件重构一下:</p>
<pre><code>const reducer = (state, action) => {
switch (action.type) {
case "CHANGE_USER":
return { ...state, user: action.user };
case "CHANGE_AGE":
return { ...state, age: action.age };
// more ...
default:
return state;
}};
const Avatar = ({ user, setUser }) => {
const [state, dispatch] = useReducer(
reducer,
{ user: "崔然", age: 18 }
);
return (
<>
<div onClick={() => dispatch({ type: "CHANGE_USER", user: "CuiRan" })}>
{state.user}
</div>
<div onClick={() => dispatch({ type: "CHANGE_AGE", age: 17 })}>
{state.age}
</div>
</>
)};
</code></pre>
<p>更进一步地,将 <code>useReducer</code>和直接对比 redux 试试,你会发现它们之间惊人的相似:</p>
<pre><code>// react hooks
const [state, dispatch] = useReducer(reducer, [initialArg]);
// redux
const store = createStore(reducer, [initialArg])
const state = store.getState()
const dispatch = store.dispatch
</code></pre>
<p>还记得我们再 context 中介绍的 provider 和 consumer 吗?再联想一下,它们的作用不就是和 react-redux 中的 provider 和 connect 一模一样 —— 将数据跨层级的进行传递!</p>
<pre><code>// react hooks
<GolbleContext.Provider value={{state,dispacth}} >
<App/>
</GolbleContext.Provider>
// ... 跨层级传递 ...
const { state, dispacth } = useContext(GolbleContext);
// react-redux
<Provider store={store}>
<App />
</Provider>
// ... 跨层级传递 ...
connect(mapStateToProps, actionCreators)(ConsumerComponent)</code></pre>
<p>到现在为止,react 可谓是自带了大半个 redux 的 API 了。那么我们不就可以把 redux 的状态管理思路直接搬过来即可。</p>
<p>最后,只需要将全局状态放到在 App 组件的顶层。最终的示例:</p>
<pre><code>const Avatar = ({{ state, dispatch }) => {// ...})
// 使用全局状态和 dispatch
const User = () => {
const { state, dispatch } = useContext(UserContext);
return <Avatar state={state} dispatch={dispatch} />;
};
// 生成全局状态和 dispatch
const reducer = (state, action) => {// ...};
const AppProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, { user: "崔然", age: 18 });
return (
<UserContext.Provider
value={{
state: state,
dispatch: dispatch
}}
>
{children}
</UserContext.Provider>
)};</code></pre>
<p>完整示例见:<a href="https://link.segmentfault.com/?enc=Ey9JXdbf57msNu0h1TBQ3w%3D%3D.hgRpgk3v%2Fs6u9inpNVNovB%2Bu4wJPfqdTyrC5oWjda52kv4zDhm6YXTfyysirpqXW" rel="nofollow">https://github.com/jiangleo/h...</a></p>
<h2>结论</h2>
<p>在 hooks 和 context 出现之前,react 缺乏自带的全局状态管理能力。即便很小的应用,一旦要用到全局状态,要么使用 <code>props</code> 多层级的进行传输,要么就只能引入 redux 等第三方状态管理工具。</p>
<p>在 hooks 和 context 出现之后,react 自身提供了一种简单的全局状态管理的能力。如果你的项目比较简单,只有少部分状态需要提升到全局,大部分组件依旧通过本地状态来进行管理。这时,使用 hooks + context 进行状态管理的是强烈推荐的。打苍蝇,用不着大炮。</p>
<p>此外,我们也观察到,社区中一些新型的基于 hooks + context 的状态管理库正在快速崛起,比如 easy-peasy、constate。另一方面,成熟的 redux 也在 7.x 版本,开始引入 hooks API 开始升级。我们也会持续保持关注,探索 hooks 时代状态管理的最佳实践。</p>
前端开发上线规范
https://segmentfault.com/a/1190000019134809
2019-05-10T11:27:56+08:00
2019-05-10T11:27:56+08:00
fitfish
https://segmentfault.com/u/timetravel
3
<p>开发个人项目时,不用准守所谓的开发上线规范,随意点也无所谓。而在开发公司项目时,我们不得不为设立一套规则和流程来进行规范。其目的有三个:</p>
<ul>
<li>用户能够正常访问</li>
<li>团队能够协同开发</li>
<li>个人能够成长进步</li>
</ul>
<p>为此,我们团队在逐步的去探索一套,适合我们的前端开发上线流程。探索的过程从两方面入手。</p>
<ul>
<li>工程化</li>
<li>流程化</li>
</ul>
<h2>工程化</h2>
<p>我们团队的 gitlab 仓库中已经存在近 200 项目,如果各自为政,没有文档、使用不同技术栈、代码风格也不进行代码校验。那么也就只有写这个项目的人才能进行维护了,换一个人肯定是一脸懵逼。为此我们团队在诸多的技术栈中,选择以 react、react-native 和小程序技术为核心,进行项目开发。</p>
<p>进一步地,使用工程化的手段,打造 react、react-native 和小程序的种子工程。新建项目时,所有的项目都是以种子工程为模板,在此之上进行开发。种子工程内,集成各种团队内事先约定的库和工具,这样就统一了团队的使用和核心库和工具。</p>
<ul>
<li>
<p>react、react-native 数据管理:</p>
<ul>
<li>✅ redux</li>
<li>mobx</li>
<li>graphQL</li>
<li>react hooks</li>
</ul>
</li>
<li>
<p>js 代码检查:(Formatting & Code-quality Rules)</p>
<ul>
<li>
<p>✅eslint</p>
<ul>
<li>✅ standard 规范(简单) <a href="https://link.segmentfault.com/?enc=N%2F0AgTvVkBjpmr0ws6x%2FLA%3D%3D.1Q5x4j1mMtscbjildt9NoEMU4ExNm%2BV1bA%2FqKaak9V51ZD%2FjhiWUisvlSQ2egajV" rel="nofollow">https://standardjs.com/readme...</a>
</li>
<li>airbnb 规范 (严格)</li>
</ul>
</li>
<li>jslint</li>
</ul>
</li>
<li>✅ html/css/js/md ... 代码风格:prettier(Only Formatting Rules)</li>
<li>
<p>git hooks</p>
<ul>
<li>
<p>✅ husky</p>
<ul><li>"pre-commit": "pretty-quick --staged && npx standard --fix"</li></ul>
</li>
<li>.git/hooks/pre-commit</li>
</ul>
</li>
<li>
<p>✅ 编辑器插件(以 VScode 为例)</p>
<ul>
<li>
<p>prettier</p>
<ul>
<li>"editor.formatOnSave": true,</li>
<li>"prettier.disableLanguages": ["javascript"] // 暂时没有研究透在 JS 这块 prettier 比 eslint 优势所在,因此只对 html/css 等做 Formatting,JS Formatting 依旧用的是 eslint standard 规范</li>
</ul>
</li>
<li>
<p>vscode-standardjs</p>
<ul><li>"standard.autoFixOnSave": true,</li></ul>
</li>
<li>es7-react-js-snippets 代码输入提示</li>
</ul>
</li>
</ul>
<p>示例 WubaRN 种子工程</p>
<h2>流程化</h2>
<p>流程化是提高产品需求持续迭代效率的关键,其背后的本质是分工。前端开发上线流程只是,信息产品(对应工业产品)生产其中一环。可以从三个维度进行分解:</p>
<ul>
<li>明确产品需求的生产步骤(产品需求拆解)</li>
<li>明确步骤边界和相关责任人(团队任务分工)</li>
<li>明确责任人需要专注的任务(个人时间安排)</li>
</ul>
<p>对于产品本身而言,拆解产品需求,分工提高效率。已经是在第一次工业革命时,就已经被发现的客观规律了。</p>
<p>扣针的制造就分成了 18 道工序。在有些工厂里,这 18 道工序分别由 18 个专门的工人负责完成。当然,也有些工厂会让一个工人完成 2~3 道工序。这个工厂每人每天可以制造出 4,800 枚针。如果工人们不是分别专习于一种特殊的业务,而是各自独立工作,那么任何人都不可能在一天之内制造出 20 枚针,甚至 1 枚也制造不出来——《国富论》</p>
<p>对于团队合作而言,存在人与人之间的沟通成本,如果不能明确开发步骤和责任边界,这将是巨大的灾难。</p>
<p>对于个人而言,即便缺失几个环节,只要完成任务,别人看起也是无所谓。但潜在的损失在于,个人状态不一定能始终保持高效,任何关键环节的缺失,可能导致事故。比如,缺乏沙箱验证,导致上线后页面奔溃。</p>
<p>当然,我讲的都没啥用,因为没踩过坑之前,都感觉无所谓。只有自己踩到坑,才会深有体会。</p>
<p>开发流程:</p>
<ul>
<li>✅ github flow(简单) <a href="https://link.segmentfault.com/?enc=C3vYBXQStrVBIcbko7KCag%3D%3D.d4JOpxNfK%2FHGhV9HvrPXqPqbNB2X5etLhXqfixzHHKTaOFGwx4KMd2EkgXpMmcCwZ7uT2v0C3neynKFfkTWcpQ%3D%3D" rel="nofollow">https://guides.github.com/pdf...</a>
</li>
<li><ul>
<li><ul><li>一句话介绍:任何人不得直接在 master 上推代码,master 的代码只能通过 merge/pull request 其他分支进行合并 。</li></ul></li>
<li>github flow 是 github flow /gitlab flow/ git flow 的最小集</li>
<li>介绍 <a href="https://link.segmentfault.com/?enc=mjlGZQiXId9zNzf4XD%2BfoQ%3D%3D.cz31wR10m0QcW7%2F52a%2FmgkZZEI%2FgJS52dYsBORJfsLep1qQs4kM3Gr7MvyLsYvkR" rel="nofollow">https://guides.github.com/int...</a>
</li>
<li>翻译 <a href="https://link.segmentfault.com/?enc=TbcoBeIT4CtlKHeKjM3ZIw%3D%3D.%2FEYOtQNL%2FLWms2Fi2%2FNRyXsMKWuwv4aLc%2FCIKrDcMPx5ptv3nrYqPNn76RHuN6ifHJdsJaYi3zuIDqVZvUXrOQ%3D%3D" rel="nofollow">https://blog.csdn.net/jeff_li...</a>
</li>
</ul></li>
<li>gitlab flow(一般) <a href="https://link.segmentfault.com/?enc=VwfrnwlB5gDXAOs8mZDkAA%3D%3D.I9cZoLAHQWLC8HoIbTqs1%2FMHKFpsaloNa1GR7iS%2B85UGlHPhmSzk02BhpzGZM2Fy5IXGA2ZWwE3vV0zAP%2FKC2A%3D%3D" rel="nofollow">https://blog.csdn.net/henryhu...</a>
</li>
<li>git flow(复杂) <a href="https://link.segmentfault.com/?enc=gQNzAZqYnEpSTvP2vqOlPQ%3D%3D.MtF1ArbHJHv6CVmjYbb45RTCsHjb%2BT6FlsKIPkMJCgAMRzJrxo2fCgjFcvT7CTLGmckVaoxnnLa6IJA%2Fh6dGOw%3D%3D" rel="nofollow">https://gitbook.tw/chapters/g...</a>
</li>
</ul>
<p>参考:隔壁安卓团队流程(安卓分支管理很复杂,用 github flow 简单介绍):</p>
<ol>
<li>从 master 拉取 feature/fix 分支进行开发</li>
<li>在分支上进行开发和自测(冒烟测试)</li>
<li>提交 merge request</li>
<li>
<p>告诉审核人 code review</p>
<ul>
<li>简单的 QQ 丢地址</li>
<li>复杂的开个会议室,审核</li>
<li>审核人 >= 2</li>
<li>MR 必须有评论 LGTM(look good to me)</li>
</ul>
</li>
<li>QA 测试通过</li>
<li>
<p>由合并人,合并代码</p>
<ul><li>安卓团队的合并人和审核人一般不是同一个人</li></ul>
</li>
</ol>
<p>上线流程:</p>
<ol>
<li>自测。RD 开发完毕后,通过静态资源或打包平台同步测试环境,并进行自测。</li>
<li>提测。RD 通过邮件或其他形式通知 QA 进行验收。</li>
<li>测试。QA 在测试环境进行验收。</li>
<li>沙箱。QA 将测试环境资源同步沙箱,再在沙箱进行验收。</li>
<li>上线。QA 将沙箱环境资源同步上线,最后在线上进行确认。</li>
</ol>
<p>参考:WubaRN 上线流程规范</p>
<p>团队规划</p>
<ol>
<li>根据前端开发上线规范,产出或完善 react/wuba-rn/小程序相关的种子工程。</li>
<li>针对核心项目,关闭所有人直接 push remote master 的权限,开发者都走 github flow 的上线流程。</li>
</ol>
动态规划解题思路
https://segmentfault.com/a/1190000018848420
2019-04-12T17:00:52+08:00
2019-04-12T17:00:52+08:00
fitfish
https://segmentfault.com/u/timetravel
9
<p>算法能力就是程序员的内力,内力强者对编程利剑的把控能力就更强。</p>
<h2>数键盘</h2>
<p>动态规划就是,通过递推的方式,由最基本的答案推导出更复杂答案的方法,直到找到最终问题的解。或者是,通过递归的方式,将复杂问题化解为更简单问题的方法,直到化解为有明确答案的最基础问题。</p>
<p>问:你现在用的键盘上有多少个键帽?</p>
<p><img src="/img/remote/1460000018848423?w=1200&h=630" alt="" title=""></p>
<p>当我问你这个问题时,你一定想到了解决方案,一个个数肯定能得到答案。</p>
<p>我们可以把这个简单的问题,用公式定义的更加清楚:设 F(n) 为键帽的总数,求 F(n) 的值。当你开始数第一个的键帽的时候,你得到了 F(1) = 1,这是一个最基本的答案。数数过程中,下一个答案等于上一个答案加 1。在状态规划中,我们通常把阶段性的答案,称作<strong>状态</strong>。复杂状态与简单状态之间存在的转化关系,叫做<strong>状态转移方程</strong>,状态转移方程是动态规范的核心,这这道题目中就是:</p>
<pre><code>F(i) = F(i - 1) + 1 ( 0<i≤N) </code></pre>
<p>当我们使用<strong>递推</strong>的方式,来求解动态规划时,我们会从 1 开始数起,一步步累加得到最终的状态:</p>
<pre><code>F(1) = 1
F(2) = F(1) + 1
...
F(N) = f(N-1) + 1</code></pre>
<p>当我们使用<strong>递归</strong>的方式,来求解动态规划时,我们会从把所有的键帽数量,记作状态 F(N),当我们数了一个键帽后,那么 剩下的状态就记作 F(N-1),因此:</p>
<pre><code>F(N) = F(N-1) + 1
F(N-1) = F(N-2) + 1
...
F(1) = 1</code></pre>
<p>无论是递推还是递归,都是得到的答案无疑都是一样的,只不过思维的方式有些不一样。递推是正向思维,先有基础答案后由复杂答案,最后得出最终问题的答案。递归是逆向思维,先有复杂的问题,然后把它化解为更简单的问题,直到分解为能一眼看出答案的基本问题。</p>
<p>数键盘虽然是一个很简单的游戏,但是解答的过程中已经包含了最基础的动态规划解题思路:</p>
<ol>
<li>定义状态</li>
<li>再重新定义问题</li>
<li>找到最基础的状态</li>
<li>找出状态转移方程</li>
<li>编程求解</li>
</ol>
<h2>最长上升子序列</h2>
<p>问:给定一个无序的整数数组,找到其中最长上升子序列的长度。</p>
<p>示例:</p>
<pre><code>输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。</code></pre>
<p>说明:</p>
<ul>
<li>可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。</li>
<li>你算法的时间复杂度应该为 O(<em>n2</em>) 。</li>
</ul>
<p>这道题目的问题是,求最长上升子序列的长度。直接拿到这个问题,肯定一脸懵逼,最长上升子序列的长度是什么?断词断句一个个解释,序列、子序列、上升序列、最长上升子序列的长度。</p>
<p>序列:这里指的是,一个无序的整数数组。</p>
<p>子序列:将原序列中的部分值,重新组合成一个新的序列,这个新的序列就是子序列。一个序列可以有多个子序列。如,原序列 [1, 5, 2, 3],那么 [1, 5] 和 [1, 2, 3] 都是原序列的子序列。</p>
<p>上升序列:从前往后看,序列中的前面的数字比后面的数字更小,序列呈递增规律,就是上升序列。[1, 2, 3] 就是上升序列,[1,2,0] 就不是上升序列。</p>
<p>最长上升子序列的长度:一个序列可能会有多个上升子序列,其中长度最长的叫做最长上升子序列,其长度叫做最长上升子序列的长度。</p>
<h2>动态规划方法一</h2>
<p>第一步:定义状态。定义状态为,以当前序列第 i 个数字结尾的最长上升子序列的长度,记作 L(i),0≤i≤N-1,N为序列长度。示例:序列[1,2,3],状态 L[1] = 2 ,表示第 1 个以 2 结尾的最长上升子序列的长度为 2。</p>
<p>第二步:重新定位问题。序列中的最长上升子序列,不一定是以最后一个数字结尾,而是所有状态中的最大值,即 Math.max(L[0],L[1],…,L(N-1))。示例:[1,2,0] 的最长上升子序列是 [1,2] ,是以第1个数字结尾的。</p>
<p>第三步:找到最基础的状态。当序列为空时,结尾的最长上升子序列的长度为0。但是我们发现,最初我们定义的状态,并不能表示该最基础的状态,因此需要对状态的定义稍作修正。</p>
<p>状态:以当前序列第 index 个数字结尾的最长上升子序列的长度,index 是序列的下标,记 i = index + 1 ,状态为 L(i),0≤i≤N,N为序列长度。此时 L[0] 表示空序列的最长上升子序列的长度 L[0] = 0,L[1] 表示以序列中第 0 位数字结尾的最长上升子序列的长度,L[1] = 1。</p>
<p>第四步:找到状态转移方程。若 L[i] 大于 1,则 L[i] 表示的子序列,去掉最后一位数,依旧是一个子序列,记该子序列为 L[j] 。其关系为 L[i] = L[j] + 1 。其中 L[j] 的最后一位 nums[j -1] < nums[i - 1],且 L[j] = Math.max( L[1],…,L[i-1]) ,0<j<i。</p>
<p>例如:序列A [1, 2, 6, 3, 4]</p>
<pre><code>1. L[0] = 0
2. L[1] = 1
3. L[2] = Math.max( L[1]) + 1 = L[1] + 1 = 2, 其中 nums[1-1] < nums[2-1]
4. L[3] = Math.max(L[1], L[2]) + 1 = L[2] + 1 = 2, 其中 nums[2-1] < nums[3-1]
5. L[4] = Math.max(L[1], L[2],L[3]) + 1 = L[2] + 1 = 2, 其中 nums[2-1] < nums[4-1]
6. L[5] = Math.max(L[1], L[2],L[3],L[4]) + 1 = L[4] + 1 = 2, 其中 nums[4-1] < nums[5-1]
</code></pre>
<p>变成为:</p>
<pre><code class="js">function lengthOfLIS(nums) {
const dp = [0]
for (let i = 1; i <= nums.length; i++) {
let max = 0
for (let j = 1; j < i; j++) {
if (nums[j - 1] < nums[i - 1]) {
max = Math.max(max, dp[j])
}
}
dp[i] = max + 1
}
return Math.max(...dp)
};</code></pre>
<h2>动态规划方法二</h2>
<p>第一步:定义状态。在序列前 index 项中,所有可能成为最长上升子序列的子序列。S[i]</p>
<pre><code>示例:A [10, 1, 12, 2, 3]
S[0] = [[10]]
S[1] = [[10], [1]]
S[2] = [[10, 12], [1, 12]]
S[3] = [[10, 12], [1, 12], [1, 2]]
S[4] = [[10, 12], [1, 12], [1, 2, 3]]</code></pre>
<p>当 S[1] = [[10], [1]] 时,A[2] 存在三种情况,①当 10 < A[2] 时, [10, A[2]] 和 [1, A[2]] 表示的长度等价;②当 1 < A[2] ≤ 10 时, [1, A[2]] 比 [10] 长;③当 A[2] ≤ 1 时,S[3] = [[10], [1], A[3]]。</p>
<p>因为题目只需要返回最终长度,所以 [10] 或 [1] 两种情况实际,可以简写为 [1] 这一种情况。A[3] 存在 3中情况,分别为①当 10 < A[2] 时, [1, A[2]] ;②当 1 < A[2] ≤ 10 时, [1, A[2]];③当 A[2] ≤ 1 时,S[3] = [A[3]]。因此可证明,只保留 [1] 一种情况,实际上已经代表了 [10] 或 [1] 两种情况。</p>
<p>对状态进行重新定义:在序列前 i 项中,长度为 k 的上升子序列中,最后一位的最小值。S[i]</p>
<pre><code>示例:A [10, 1, 12, 2, 3]
S[0] = [10]
S[1] = [1]
S[2] = [1,12]
S[3] = [1,2]
S[4] = [1,2,3]</code></pre>
<p>第二步:重新定位问题。 求 S[N-1] 的长度,其中 N 为序列的长度。</p>
<p>第三步:找到最基础的状态。当序列为空时,结尾的最长上升子序列的长度为0,因此对问题和状态进行重新修正。</p>
<p>状态:在序列前 i + 1 项中,长度为 k 的上升子序列中,最后一位的最小值。S[i]</p>
<p>问题:求 S[N] 的长度,其中 N 为序列的长度。</p>
<p>第四步:找到状态转移方程。如果 <code>A[i-1]</code> 比 <code>S[i]</code> 最后一位还要大,记作 <code>S[i][len -1] < A[i-1]</code> ,即可以组成一个更长子序列,<code>s[i] = [...s[i -1],A[i-1]]</code>。如果 <code>A[i-1]</code> 比 <code>S[i]</code> 中某一位 <code>S[i][j]</code>要小,但是比该位的前一位 <code>S[i][j-1]</code>要大,更具第一步中的推论,可以用 <code>A[i-1]</code> 替换掉 <code>S[i][j]</code> ,<code>S[i] = […,S[i][j-1],A[i-1] ,…]</code></p>
<pre><code>示例:A [10, 1, 12, 2, 3]
S[0] = [] // 初始化
S[1] = [10] // 在最后添加 A[1-1]=10
S[2] = [1] // A[2-1] < S[2][0],因此替换掉 S[2][0]
S[3] = [1,12] // 在最后添加 A[3-1]= 12
S[4] = [1,2] // S[2][0] < A[2-1] < S[2][1],因此替换掉 S[2][1]
S[5] = [1,2,3] // 在最后添加 A[5-1]= 3</code></pre>
<p>实现:</p>
<pre><code class="js">function lengthOfLIS(nums) {
const sequence = []
// 复杂度 n
for (let i = 1; i <= nums.length; i++) {
let len = sequence.length
// 增加
if (sequence[len - 1] < nums[i-1]) {
sequence[len] = nums[i-1]
// 替换
} else {
// sequence 具有单调性,可以使用 logn 复杂度的二分查找,查找到 S[i][j-1]<A[i]<=S[i][j] (0≤j≤i) 的位置,并对 S[i][j] 进行重新赋值。
let target = nums[i-1]
let start = 0
let end = len
let mid = parseInt(len / 2)
let x = 0
while (start <= end) {
if (target === sequence[mid]) {
x = mid
break
} else if (sequence[mid] < target) {
x = mid + 1
start = mid + 1
mid = parseInt((start + end) / 2)
} else {
x = mid
end = mid - 1
mid = parseInt((start + end) / 2)
}
}
sequence[x] = nums[i-1]
}
}
return sequence.length
};</code></pre>
GraphQL 和 Apollo 为什么能帮助你更快地完成开发需求?
https://segmentfault.com/a/1190000018706816
2019-03-29T19:10:25+08:00
2019-03-29T19:10:25+08:00
fitfish
https://segmentfault.com/u/timetravel
36
<p>在大前端应用的开发过程中,如何管理好数据是一件很有挑战的事情。后端工程师需要聚合来自多个数据源的数据,再分发到大前端的各个端中,而大前端工程师需要在实现用户体验好的视图 (optimistic UI<sup>1</sup>) 的基础上,需要考虑如何管理端上的状态。</p>
<p><img src="/img/bVbqEEe?w=932&h=732" alt="clipboard.png" title="clipboard.png"></p>
<p>在团队中使用 GraphQL 能够很好的解决数据管理的痛点。本文接下来会介绍 GraphQL 声明式(declarative)获取数据的方法,这将简化数据管理的难度,并且提升网络性能。还会介绍 Apollo<sup>2</sup> 如何通过一系列对开发者体验好的工具,提高工程师的开发效率。</p>
<p>译注1:optimistic UI 是一种 UI 设计模式。例如,你在微信上发送消息会直接显示,而不用等到消息的网络请求成功或失败后再显示。optimistic UI 的数据管理很复杂,需要先显示模拟数据,再等待网络请求成功或失败后,再显示真正的数据。通过 Apollo 可以轻易地实现 optimistic UI。<br>译注2:Apollo 是实现,GraphQL 是标准,和 JS/ECMA 的关系一样。</p>
<h2>开发者体验</h2>
<p>Apollo 可以帮助团队更快地实现功能上线,因为它对开发者的体验非常好。Apollo 目标是"让各端的数据管理变得简单"(simplify data management across the stack)。通过 Apollo Client、Apollo Server 和 Apollo Engine,以前需要花上一些功夫实现的功能,比如全栈缓存、数据规范化、optimistic UI,现在变得很简单。</p>
<p><img src="/img/bVbqEEV?w=668&h=568" alt="clipboard.png" title="clipboard.png"></p>
<h4>请求你所要的数据</h4>
<p>GraphQL 强类型查询语言的特性,使得开发者可以利用牛逼的工具来请求 GraphQL 接口。借助 GraphQL 内省系统(introspection system),开发者可以查询 GraphQL schema <sup>3</sup>信息,包括字段和类型。内省系统拥有一些非常炫酷的功能,比如自动生成的文档、代码自动补全等等。</p>
<p><br>译注3:schema 用于描述你所要数据的结构和字段,如:</p>
<pre><code> {
dogs {
id
breed
image {
url
}
activities {
name
}
}
}</code></pre>
<p><strong>GraphQL Playground</strong><br>Prisma 团队开发的 GraphQL Playground 工具是一款非常优秀的 IDE,它可以把自定义的 schema 和查询历史自动地生成文档。只要看一下,你就知道 GraphQL API 中有哪些能获取到的数据,而不用研究后端代码或了解数据来源。</p>
<p><img src="/img/remote/1460000018706819?w=1013&h=494" alt="GraphQL Playground" title="GraphQL Playground"></p>
<p>Apollo Server 2.0 内置了 GraphQL Playground,更方便你浏览 schema 和执行查询命令。</p>
<p><strong>Apollo DevTools</strong><br>Apollo DevTools 是 Chrome 的扩展程序,可以查询 Apollo 的前端缓存(Cache),记录查询(Queries)和变更(Mutations)。你还可以使用 Apollo DevTools 中的 GraphiQL 来方便地测试前端查询。</p>
<p><img src="/img/remote/1460000018706820?w=1616&h=666" alt="Apollo DevTools" title="Apollo DevTools"></p>
<h4>简化前端代码</h4>
<p>如果你使用过 REST 和状态管理库,如 Redux,为了发一个网络请求,你需要写 action creators、reducers、mapStateToProps 并集成中间件。使用 Apollo Client,你再也不用关系这些东西。Apollo Client 解决了一切,你只需要专注于查询,而不需要写一堆状态管理的代码。</p>
<pre><code class="js">import ApolloClient from "apollo-boost";
const client = new ApolloClient({
uri: "https://dog-graphql-api.glitch.me/graphql"
});</code></pre>
<p>有团队声称他们切换成 Apollo Client 后,删除了上千行状态管理代码和一堆复杂逻辑。这得益于 Apollo Client 不仅支持远程数据管理,还支持本地数据管理, Apollo 缓存就是当前应用全局状态的单一事实来源。</p>
<h4>现代化的工具</h4>
<p>Apollo platform<sup>4</sup> 可以让团队使用现代化的工具,帮忙他们快速发现错误、查看 API、开发具有挑战的缓存功能。</p>
<p>译注4:Apollo platform 是云平台。Apollo 在本文中有两层含义,首先 Apollo 是 GraphQL 的一个开源实现,其次 Apollo 是开发 Apollo platform 、Apollo Client 、Apollo Server 等产品的公司。</p>
<p>Apollo Engine 是 GraphQL 生态系统中唯一可以为你的 API 提供监控和分析的工具。Engine 可以显示每个 resolver<sup>5</sup> 的埋点指标,可以帮忙你定位错误, 可以分析 schema 中请求的每个字段的分布频率。 你还可以将这些数据传输到你正在用的其他分析平台,如 DataDog,并在某些数据超过报警阙值设置时进行报警。</p>
<p>译注5:resolver 处理返回字段的函数</p>
<p><img src="/img/remote/1460000018706821" alt="Apollo Engine" title="Apollo Engine"></p>
<h2>声明式数据获取</h2>
<p>使用 GraphQL 的一个主要优点是它有声明式数据获取的能力,不需要前端请求多个接口,不需要手动的聚合数据,只需要你精确地描述你所要的数据,然后 GraphQL 就会将你要的数据返回给你。而使用 REST ,你需要调用每一个接口,并过滤出你要的数据,然后将过滤后的数据构造成组件所需要的结构。</p>
<pre><code>GET /api/dogs/breeds
GET /api/dogs/images
GET /api/dogs/activities</code></pre>
<p>REST 的方法不仅不好使,而且容易出错,难以跨平台重用逻辑。对比一下 GraphQL 声明式的方式:</p>
<pre><code>const GET_DOGS = gql`
query {
dogs {
id
breed
image {
url
}
activities {
name
}
}
}
`;</code></pre>
<p>在上面,我们定义了我们想要从服务端获取的对象的结构。GraphQL 负责组合和过滤数据,同时返回我们想要的数据字段和结构。</p>
<p>如何使用 GraphQL 查询?Apollo Client 构建了 GraphQL 声明式请求数据的方法。在 React 应用中,获取数据、跟踪加载和错误状态以及更新 UI 的所有逻辑,都封装在一个 Query 组件中。这种封装方式使数据获取组件和展示组件很容易的组合在一起。让我们看看,如何在 React 应用中使用 Apollo Client 获取 GraphQL 数据:</p>
<pre><code class="jsx">const Feed = () => (
{/* 数据获取组件 Query*/}
<Query query={GET_DOGS}>
{/* 展示组件:由 Error、Fetching、DogList 等组成的函数组件 */}
{({ loading, error, data }) => {
if (error) return <Error />
if (loading || !data) return <Fetching />;
return <DogList dogs={data.dogs} />
}}
</Query>
);</code></pre>
<p>Apollo Client 管理整个请求的周期,从请求开始到请求结束,包括为你跟踪加载和错误状态。这里不用设置中间件,不用写模板代码,不用重构的数据结构,不用关心请求缓存。你所需要做的就是描述你组件要的数据,然后让 Apoolo Client 去完成那些繁重的工作。</p>
<p>当你使用 Apollo Client 时,你会发现你能删除很多不需要的数据管理方面的代码。具体能够删除多少行代码,要根据你项目的情况来判断,但有些团队声称他们删除了数千行代码。要了解更多 Apollo Client 的牛逼功能,例如 optimistic UI、重新获取、分页获取,请查看我们的状态管理指南。</p>
<h2>提升网络性能</h2>
<p>在许多情况下,在现有的 REST 接口层之上增加 GraphQL API 层,可以提高你 App 的网络性能,特别是在网络差的情况下。虽然,你应该通过网络性能监控来衡量 GraphQL 如何影响你的 App,但大家通常认为 GraphQL 通过避免客户端与服务端的往返通讯(round trips),和减少请求数据的大小来提升网络性能的。</p>
<h4>更少的请求数据</h4>
<p>因为从服务端返回的响应中只包含你指定的查询数据,所以 GraphQL 相对于 REST 可以显著地减少请求数据的大小。让我们看看前面文章中的例子:</p>
<pre><code class="js">const GET_DOGS = gql`
query {
dogs {
id
breed
image {
url
}
activities {
name
}
}
}
`;</code></pre>
<p>GraphQL 服务响应中只包括 dogs 对象的 id、breed、image、activities 字段,即便 REST 层的接口 dogs 是带有 100 个字段的对象也是如此,所有多余的字段都将在返回给客户端之前过滤掉。</p>
<h4>避免往返通讯(round trips)</h4>
<p>由于每个 GraphQL 请求只返回一个响应,使用 GraphQL 可以帮助你避免客户端到服务端的往返通讯。使用 REST,请求一个资源就是一次往返通讯,往返通讯会快速地增加。如果你请求要列表中的每一项,每一项都需要一次往返,每一项中的每个资源也需要一次往返,总次数就是二者的乘积<sup>6</sup>,这就导致了请求时间过长。</p>
<p> 译注6:极端 REST 例子,列表长度 N,每一项 3 个资源,请求次数就是 3*N</p>
<pre><code>GET /api/dogs/breeds
GET /api/dogs/images
GET /api/dogs/activities</code></pre>
<p>使用 GraphQL,每个查询代表一次往返通讯。如果你希望进一步的减少往返,你可以实现查询批处理(query batching),将多个查询封装到单个请求中。</p>
<h2>产品案例</h2>
<p>虽然 GraphQL 规范是由 Facebook 在 2015 年公布的,但是自 2012 年以来,GraphQL 就是 Facebook 移动应用开发的重要组成部分。</p>
<p>在 Apollo 团队,我们发现 GraphQL 为我们现有方案中遇到的很多问题提供了出色的解决方案,现在我们用它来优化我们的技术基础设施。几年来,我们和开源社区、客户、合作伙伴一起,为开源项目 Apollo 带了了诸多创新。我们很骄傲,Apollo 适用于各类公司,从创业公司到大型企业。</p>
<p>除了我们自己的经验,我们还收到了积极地在生产环境中使用 Apollo GraphQL 的企业客户的广泛反馈、贡献和支持。一些最值得借鉴的案例是:</p>
<ul>
<li>
<strong>The New York Times</strong>(<a href="https://link.segmentfault.com/?enc=OdDqrespAXcl%2FvqihNIcmQ%3D%3D.qaikyQGGN%2FvHRZ5EehgUk2zEJuKQxv2u1EMOu7rbv1UHr%2Fntaom6VuM1plBvBLmRaVqkjCBYKX5vnZewJmZ8qJNBapqb7jZ0CqjtUK1cjk0%3D" rel="nofollow">https://open.nytimes.com/the-...</a>: 学习 The New York Times 如何从 Relay 切换到 Apollo,实现他们 App 的 SSR 和持久化 queries。</li>
<li>
<strong>Airbnb</strong>(<a href="https://link.segmentfault.com/?enc=blVCCUy0oNNx1%2BhMrfmEZQ%3D%3D.7mkWTExWOXZtIfl3W3v2qHya3V8ux3j5yRCXWeCx5OwXWcxDxSdruNoS29ivziCg8%2BUkwTdWWJRpemIk%2Fx4uTkFqNNyf%2FMcuPEmO6HvclU4m5WQgJl3011gfXW9vG4eJ" rel="nofollow">https://medium.com/airbnb-eng...</a>: Airbnb 在 Apollo 平台上下了重注,去强化他们微服务的数据层。</li>
<li>
<strong>Express</strong>(<a href="https://link.segmentfault.com/?enc=t%2BTYXfjuOacPBx1TRvA4bw%3D%3D.tTJ4eMj%2BEfluh31CcH7BAa95nb%2FAuoYCMOrD2h8B9ER7hrw1I%2B1iRGQcQxyy4RvSaa6KnfukqEYR3SYX7C%2Brw98BzJVvp38Id56Oyx5Mp6x7OJJd2MEqqa66cSYSxqdK" rel="nofollow">https://dev-blog.apollodata.c...</a>:使用易上手的 Apollo 分页功能,优化我们团队的关键应用。</li>
<li>
<strong>Major League Soccer</strong>(<a href="https://link.segmentfault.com/?enc=skVnfk2ODjEhOC68gB6UAw%3D%3D.UCLIo4mUFov2AccPEKsugYMaLpgdGJCZaX6GveZ1%2BZ%2FQndTErdvP0u1IrWy0Bl0kbfgjnZXSU3IlFjNIUUTn4sPex1IugZWm1zJ5GKnDjxSSe674xVMrlOZX0%2BLcgoec" rel="nofollow">https://dev-blog.apollodata.c...</a>: 团队的数据管理工具从 Redux 切换到了 Apollo,帮忙他们删除了几乎所有的 Redux 代码。</li>
<li>
<strong>Expo</strong>(<a href="https://link.segmentfault.com/?enc=NRqmNcZnOvSJyVrHqmc%2Byg%3D%3D.Ji53J3qB0zKJW2uGt082HSBXuyhWxRWuDV9HkM66u9VokPGh86Osa0EF%2F1vcBF7lr%2B69oXmTT7V%2FcOxaKshtrIYpsxuYmFe3ksc0siQexv8%3D" rel="nofollow">https://dev-blog.apollodata.c...</a>: 使用 Apollo 开发 React Native App 使得团队可以专注于改善产品功能,而不是写数据抓取的逻辑。</li>
<li>
<strong>KLM</strong>(<a href="https://link.segmentfault.com/?enc=BcQX7Q89knwHVzuMl%2BfVlw%3D%3D.GMKcoFe86rm2VQMLZUynqEFt7dVMOHqRsoFaMSFj9%2Fo%3D" rel="nofollow">https://youtu.be/T2njjXHdKqw)</a>: 学习 KLM 团队如何使用 GraphQL 和 Apollo 扩展他们的 Angular app。</li>
</ul>
2019年前端的3个趋势
https://segmentfault.com/a/1190000017764337
2019-01-04T19:19:27+08:00
2019-01-04T19:19:27+08:00
fitfish
https://segmentfault.com/u/timetravel
26
<p>简介:</p>
<ol>
<li>JavaScript 应用范围广泛,静态类型语言 TypeScript 会继续得到更多开发者的青睐。</li>
<li>组件成为前端最基本的物料,CSS 融合在组件中(CSS in JS)的方案日趋成熟。</li>
<li>前端的“端”越来越多, API 查询语言 GraphQL 会继续保持高速增长 。</li>
</ol>
<h2>JavaScript 应用范围广泛,TypeScript 更受青睐</h2>
<p>在 github 2018 调查报告的中,JavaScript 连续多年稳居第一,成为最受欢迎的开发语言。从 Stack Overflow 的调查报告中,我们可以看到更详细的数据,任意两个开发者中至少有一个会 JavaScript,并且这个比例还在持续增长,从 2016年的 55.4%,到 2017年的 62.2% ,到 2018 年的 69.8%。在 npm 的调查报告中,JavaScript 生态圈也是非常繁荣,module 的数量继续保持高速增长,将其他语言远远的甩在了后面。</p>
<p>图一: npm 2018 调研报告 - Module Counts</p>
<p><img src="/img/remote/1460000017764340?w=500&h=361" alt="npm 报告" title="npm 报告"></p>
<p>从使用范围上看,JavaScript 可以写前端、服务端、移动端,甚至还可以写物联网应用。在 npm 2018 的调研报告中,大多数 JavaScript 开发者*写 web 前端应用(93%)和 node.js 服务端应用(70%)。在 stateofjs 2018 的调研报告中,还有相当数量的 JavaScript 开发者*写移动或桌面应用,例如 Electron(19.6%)、React Native(18.7%)、Native Apps(10.6%), Flutter 、Weex、PWA 都在 1% 以内。</p>
<p>备注:npm 和 stateofjs 的调研用户群体特征类似,统一归类为 JavaScript 开发者。</p>
<p>图二: npm 2018 调研报告 - The JavaScript I write runs on...</p>
<p><img src="/img/remote/1460000017764341" alt="image" title="image"></p>
<p>值得注意的是,TypeScript 在 2018 年得到更多开发者的青睐。在 github 语言排行版中,TypeScript 上升了 3 名,排到了第 7 的位置。在 stateofjs 2018 的调研报告中, JavaScript 开发者有 86.3% 愿意继续使用 ES6,有 46.7% 愿意继续使用 TypeScript。排在第三、四的是 Facebook 的 Flow 和 Reason 语言,但是占比都不高。</p>
<p>图三:stateofjs 2018 调研报告- JavaScript Flavors</p>
<p><img src="/img/remote/1460000017764342?w=1846&h=800" alt="image-20190103112825335" title="image-20190103112825335"></p>
<p>从互联网的发展历史的角度看,2010 年 3G (国内)开始普及,2014 年 4G 全面铺开,拉开移动互联网的序幕。互联网从传统的内容提供者,转变成了服务提供者。前端应用也发生的本质的转变,从传统互联网时代的内容展示,转变成了拥有复杂交互的逻辑的服务提供窗口。随着前端应用变得越来越复杂,和 JavaScript 应用的领域越来越广泛,传统 JavaScript(ES5) 已经适应复杂的开发需求,因此功能更加强大的 ES6 孕育而出。</p>
<p>在 JavaScript 应用复杂度不断增加的背景下,预计 2019 年,静态类型语言 TypeScript 会继续得到更多开发者的青睐。TypeScript 属于 ES6 的超集,一方面它可以很好的兼容 ES6 语法,另一方面它又提供了可选的静态类型检查和接口(interface)的功能。在开发复杂度高、需要大规模合作的 JavaScript 应用时,TypeScript 相对 ES6 不妨是一种更好的选择。</p>
<h2>组件成为最基本的前端物料,CSS in JS 让组件化更彻底</h2>
<p>在 stateofjs 2018 的调研报告中, JavaScript 开发者有 64.8% 愿意继续 React,有 28.8% 愿意继续 Vue。但根据个人观察,在国内 Vue 开发者会比 React 多一些,这可能是因为 Vue 上手简单并且有完善的中文文档。Angular 方面,有超过一半使用 Angular 框架的开发者表示,不愿意继续使用 Angular 进行开发了。而其他开发框架 Preact、Ember、Polymer、JQuery 的使用量都很少。现在,React 和 Vue 已经成为前端开发框架的双雄,不会 React 或 Vue 可能连工作都不好找。</p>
<p>图四:stateofjs 2018 调研报告 - Front-end Frameworks</p>
<p><img src="/img/remote/1460000017764343" alt="image-20190103130649735" title="image-20190103130649735"></p>
<p>组件是 React 和 Vue 最强大的功能之一。在 Vue 中一个 <code>.vue</code> 文件就是一个组件,包含 Template、JS、CSS 三个部分,其中 CSS 部分是可选的,开发者也可以将 CSS 独立出去。在 React 中一个 <code>.jsx</code> 文件就是一个组件,但是 JSX 只能包含 Template、JS 两个部分,组件的 CSS 部分必须<code> import from 'xxx.css'</code> 进来。</p>
<p>无论是 React 还是 Vue,都改变不了 CSS 全局作用域的问题。开发者可以在一个组件中,通过 Selector,如 <code>.class</code> <code>.id</code> ,取到本该属于其他组件的 CSS 样式。组件本应是一个独立的作用域,但是它的 CSS 竟然是全局的!在应用复杂度低、单人开发的情况下 CSS 全局作用域不算大问题。但是在多人合作开发的场景下,可能会因此导致样式冲突。比如,因为引入了 B 开发者的组件,A 开发者的组件样式错乱了,这就导致了较高的联调成本。</p>
<p>图五:CSS document level V.S. component level</p>
<p><img src="/img/remote/1460000017764344" alt="image-20190103171455162" title="image-20190103171455162"></p>
<p>解决的思路就是,使用 CSS in JS 的工具,使得 CSS 只对它归属的组件生效。CSS in JS 的方案有很多,主流的有:styled-components、emotion、css-modules、aphrodite、glamor、glamorous、radium、react-jss。styled-components 方案使用人数最多,emotion 方案排第二并且增长势头凶猛,而 css-modules 方案在两年前已经停止维护了,不再推荐。styled-components 的写法太反直觉,个人更喜欢 emotion。从下载量的增长势头来看 emotion 比 styled-components 更快。因此,如果有 CSS in JS 需求的项目,更加推荐 emotion。相信在 2019 年,CSS in JS 方案会更加成熟,我们不妨期待吧。</p>
<p>图六: npmtrends.com CSS in JS 方案下载量对比</p>
<p><img src="/img/remote/1460000017764345" alt="image-20190103175910752" title="image-20190103175910752"></p>
<h2>“端”越来越多,GraphQL 继续保持高速增长</h2>
<p>在移动互联网时代来临之前,传统意义上的前端只有浏览器的 PC 端。移动互联网兴起后,出现了浏览器的 H5 端、iOS 端、Android 端。再后来一些平台级 App ,比如微信、QQ,推出了自己的 JS-SDK,Hybird 也成为了新的端。近两年,微信、支付宝、百度、头条也推出了自己的小程序平台,小程序也成为了新的端。</p>
<p>每个端都有自个儿的个性,不存在一种大统一的方案,可以适配所有的端。这导致了同一个业务,需要在 6 个端,开发 6 次、联调 6 次。</p>
<p><img src="/img/remote/1460000017764346" alt="image-20190104142048117" title="image-20190104142048117"></p>
<p>我们假设有一个这样的 API,它包含了该业务在各个端上所有的数据,这不就解决了多次联调的问题了嘛。虽然还是需要开发 6 次,但是现在因为只有 1 个 API,所以联调次数变成了 1 次。但是该方案的背后的代价是,加载慢、维护成本高。任意 1 个端,都要获取其他 5 个端的上差异化的数据,加载能不慢嘛。如果 API 有改动,可能会影响到 6 个端的代码,维护起来也费劲。</p>
<p>稍作改变,现在我们假设,前端可以通过一种标准的 API 查询语法,精确地获取任意自定义的数据,在服务端通过解析前端查询语句,返回其自定义的查询数据。虽然还是 6 个端,1 个 API,但是每个端可以只获取自己的数据,不就解决了加载慢的问题了嘛。如果某个端需要增改获取的数据,只需要修改这个端的查询语句即可,这不就解决了维护成本高的问题了吗。通过定义一种标准的 API 查询语法,可以使得前端获取 API 数据,就像从数据库获取数据一样方便和灵活。</p>
<p>GraphQL 就定义一套标准的 API 查询语法,在保持灵活性和可维护性的前提下,极大的降低了联调成本。</p>
<p>备注:GraphQL 官方使用的例子是,一个业务要请求多个 REST 规范的 API 。但是,国内通常使用的不是准守标准的 REST API ,他们的痛点在国内不那么痛,所以改用多端多 API 联调成本高来举例。</p>
<p>图七:@helferjs 从REST到GraphQL</p>
<p><img src="/img/remote/1460000017764347?w=1932&h=1080" alt="image-20190103224317425" title="image-20190103224317425"></p>
<p>因为使用 API 查询语言 GraphQL 获取的方法太简单了,所以连数据管理的事省了。也就是说,使用 GraphQL 可以把 Redux、Mobx 干的活给省了。我们可以看到,在 stateofjs 2018 调研报告中, 把 GraphQL 和 Redux、Mobx 都归类为一类 —— 数据层(Data Layer)。报告中指出,有 47.2% 的 JavaScript 开发者表示会继续使用 Redux,20.4% 会继续使用 GraphQL, 5.6% 会继续使用 Mobx。需要留意的是,有 62.5% 表示对 GraphQL 感兴趣,因此 GraphQL 获得 stateofjs 的最感兴趣奖(Highest Interest)。</p>
<p>图八:stateofjs 2018 调研报告 - Data Layer</p>
<p><img src="/img/remote/1460000017764348?w=1792&h=796" alt="image-20190104110131962" title="image-20190104110131962"></p>
<p>预计 2019 年,GraphQL 会继续保持高速增长,被更多的开发者使用。在 npm 2018 调研报告中,特意指出了 GraphQL 的客户端库 Apollo 的下载量保持着高速的增长。</p>
<p>图九:npm 2018 调研报告-GraphQL continues hyper-growth</p>
<p><img src="/img/remote/1460000017764349" alt="image" title="image"></p>
以太坊的分片方案
https://segmentfault.com/a/1190000014668596
2018-04-29T23:34:03+08:00
2018-04-29T23:34:03+08:00
fitfish
https://segmentfault.com/u/timetravel
1
<h2>三难困境</h2>
<p>区块链的三难困境:去中心化、扩展性、安全性</p>
<h3>牺牲扩展性方案</h3>
<p>现在的比特币、以太坊都是通过牺牲扩展性来换取安全性的。</p>
<p>因为以太坊网络上的每笔交易,需要每个节点都计算、存储和广播一次。<br>这意味着以太坊网络的计算资源,不可能大于单个节点的计算资源。</p>
<p>将节点的计算、存储、宽带等资源记作, O(c) 。<br>将以太坊网络的计算、存储、宽带等资源记作,O(n)。<br><strong>不可扩展指</strong>的是,网络整体的计算能力不可能大于单个的节点的计算能力:</p>
<pre><code>O(n) < O(c)</code></pre>
<h3>牺牲去中心化</h3>
<p>有两种简单的提高扩展方案:区块的大小、使用超级节点代替普通节点。比如比特币的 8MB 的大区块扩容。</p>
<p>这类方案的特点是提高单个节点的计算、存储、宽带能力。<br>因为超级节点的计算资源比普通节点的计算资源更多。<br>所以网络的整体计算能力会因此而得到提高。</p>
<p>将超级节点的计算、存储、宽带等资源记作, O(c`) 。<br>将由超级节点组成以太坊网络的计算、存储、宽带等资源记作, O(n`) 。</p>
<p>牺牲去中心化的扩方案可以记作:</p>
<pre><code>O(c) < O(c`)
O(n) < O(n`)</code></pre>
<h3>牺牲安全性</h3>
<p>Google 的分布式网络。节点只属于 Google,不对外开放,需用专人维护和定时上下线。</p>
<pre><code>O(c) << O(n)</code></pre>
<h2>以太坊分片</h2>
<p>去中心化、扩展性、安全性都要。</p>
<h3>思路</h3>
<p>分片就是,将以太坊网络上的节点,分成 k 片,每片只处理 1/k 的交易。通过主网上的 validator manager contract(VMC)做统筹所有分片,具体的交易处理和账户信息保存都在分片进行,只将最后的交易结果保存主链上。</p>
<p>原来以太坊网络中的计算资源,都在做一件相同的事。现在通过分片,以太坊网络中的计算资源被分成 K 份,分片之间进行分工合作,大大提高了网络整体的计算效率。因此分片使得以太坊网络计算能力,突破了单个节点计算能力的限制。</p>
<p>将节点的计算、存储、宽带等资源记作, O(c) 。<br>将以太坊网络的计算、存储、宽带等资源记作,O(n)。<br><strong>可扩展</strong>指的就是网络整体的计算能力不局限于单个节点的计算能力。记作:</p>
<pre><code>O(c) < O(n)</code></pre>
<h3>收益</h3>
<p>分片可使以太坊网络计算能力得到 100~1000 倍的提升。但是安全性会有所降低,原来双花攻击需要控制以太坊网络 51% 的节点,但是分片之后只需要控制 33% 的节点。V 神认为牺牲一点安全性的,换来巨大的性能提升是非常值的。</p>
非对称加密及其应用
https://segmentfault.com/a/1190000014371502
2018-04-13T17:35:22+08:00
2018-04-13T17:35:22+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<h2>非对称加密及其应用</h2>
<hr>
<h3>怎么证明你是你?</h3>
<ul><li>
<p>去银行开户</p>
<ul>
<li>难点:KYC (Know Your Customer)</li>
<li>目的:可证明 + 不可抵赖</li>
<li>方法:人脸识别 + 身份证</li>
</ul>
</li></ul>
<hr>
<h3>怎么证明你的服务/应用是你的?</h3>
<ul>
<li>
<p>产商验证快应用</p>
<ul>
<li>难点:怎么证明你的快应用是你的</li>
<li>目的:可证明、完整性、不可抵赖</li>
</ul>
</li>
<li>
<p>https</p>
<ul>
<li>难点:通过不可信的网络,建立可信任的连接</li>
<li>目的:可证明、完整性</li>
</ul>
</li>
</ul>
<hr>
<h3>方法是什么?</h3>
<hr>
<h3>数学</h3>
<ul><li>
<p>RSA 加密算法</p>
<ul>
<li>互质数相乘容易 5 * 9 = 45</li>
<li>互质数因数分解很难 45: 1,5,3,9,15,和 45</li>
<li>越大的数越难</li>
</ul>
</li></ul>
<hr>
<ul><li>
<p>椭圆曲线加密算法</p>
<ul>
<li>从起始点做切线,找到切线与曲线相交点很容易</li>
<li>从相交点,找到起始点非常难</li>
<li>传言常规的椭圆曲线加密算法,被美国埋下了漏洞</li>
</ul>
</li></ul>
<p><img src="/img/remote/1460000014371507" alt="" title=""></p>
<hr>
<h3>密码学</h3>
<ul>
<li>非对称加密需要两把钥匙</li>
<li>用于其中一把进行加密,只能用另一把进行解密</li>
<li>留给自己的叫做私钥,暴露给外界的叫公钥</li>
</ul>
<p><img src="/img/remote/1460000014371508" alt="" title=""></p>
<hr>
<h4>演示</h4>
<ul>
<li>使用 <a href="https://link.segmentfault.com/?enc=TZAGFYftYJt8kWqRRPrQZg%3D%3D.Ld3CdaxO0cvypqJkPduzLxbpNhyRypYPW8%2FJ855iBulKybUaHsYmZnnSwNpOC80Xu%2F64dwGKMD2NvlxxzWhi%2FCjkCwF1EuwU%2FdudBfWhyXQ%3D" rel="nofollow">openssl</a> 生成 keys</li>
<li>公钥加密,私钥解密 Demo</li>
<li>私钥加密,公钥解密 Demo</li>
</ul>
<hr>
<h3>非对称加密</h3>
<hr>
<h4>方案:怎么证明你的快应用是你的?</h4>
<ol>
<li>快应用: 数学 (生成)=> 私钥 + 公钥</li>
<li>快应用: 私钥 + 程序 (非对称加密)=> 加密的快应用</li>
<li>
<p>厂商:加密的快应用 + 公钥 (非对称解密)=> 快应用</p>
<ul><li>如果公钥私钥不匹配,解密出来的是乱码</li></ul>
</li>
</ol>
<hr>
<h4>解决的问题</h4>
<ul>
<li>可证明: 你有办法证明,快应用是你的</li>
<li>完整性:快应用没有被第三方修改过</li>
<li>不可抵赖:快应用被查水表的,产商找你对账,你否认不了</li>
</ul>
<hr>
<h4>方案:怎么证明浏览器请求的服务器返回的消息是服务器的?</h4>
<ol>
<li>服务器:数学 (生成)=> 私钥 + 公钥</li>
<li>浏览器:公钥 + 临时通讯密码 (非对称加密)=> 加密的密码</li>
<li>服务端: 私钥 + 加密的密码 (非对称解密)=> 临时通讯密码</li>
<li>服务端:临时通讯密码 + 信息 (对称加密)=> 加密的信息</li>
<li>浏览器: 临时通讯密码 + 加密的信息 (对称解密) => 信息</li>
</ol>
<hr>
<h4>解决的问题</h4>
<ul>
<li>可证明 服务器有办法证明,response 是他发送的</li>
<li>完整性 response 被人修改后,可以查出来</li>
</ul>
<hr>
<h4>非对称加密表达式</h4>
<ul>
<li>
<p>Alice</p>
<ol>
<li>hash: x = hash(data)</li>
<li>send: c(x) and data</li>
</ol>
</li>
<li>
<p>Bob</p>
<ol>
<li>receive: c(x) and data</li>
<li>verify: d(c(x)) = x = hash(data)</li>
</ol>
</li>
</ul>
<hr>
<h4>非对称加密完整流程</h4>
<p><img src="/img/remote/1460000014371509?w=1838&h=990" alt="" title=""></p>
<hr>
<h3>问题</h3>
<ul>
<li>产商怎么知道你的快应用公钥是你的?</li>
<li>浏览器怎么知道你的服务器的公钥是你的?</li>
</ul>
<hr>
<h4>谁能证明?</h4>
<hr>
<h4>Alice 和 Bob 找一个第三方来证明</h4>
<hr>
<ul>
<li>Certificate Authority 数字证书认证机构担任可信的第三方角色</li>
<li>对比:电信服务商 担任不可信的第三方角色</li>
</ul>
<hr>
<p><img src="/img/remote/1460000006767313" alt="" title=""></p>
<hr>
<h4>数字签名获取实现(个人推理未验证)</h4>
<ol>
<li>Alice: 数学 (生成)=> 公钥 + 私钥</li>
<li>Alice : 公钥 (发送)=> CA</li>
<li>CA: 公钥 + 信息 (非对称加密)=> 加密的信息1</li>
<li>Alice: 私钥 + 加密的信息1 (非对称解密)=> 信息</li>
<li>Alice: 私钥 + 信息 (非对称加密) => 加密的信息2</li>
<li>CA: 公钥 + 加密的信息2 (非对称解密) => 信息</li>
<li>CA: 数学 (生成)=> 公钥2 + 私钥2</li>
<li>CA: 公钥2 + 信息(公钥、过期时间等) (非对称加密)=> 数字证书</li>
</ol>
<hr>
<h4>简化版获取证书</h4>
<ol>
<li><a href="https://link.segmentfault.com/?enc=eiy2Gl6wT2M0YIawIyijvw%3D%3D.fxJLvP9F2hYVKL1EUQXF1zGXACcrTWxeqC46RYOgG5wyVETqsJKNjkXMyHAGudJGSOykESnn2euFn9vp4K9THw%3D%3D" rel="nofollow">快应用获取证书</a></li>
<li>CA 的生成算法是公开的,Alice 可以验证 CA 不会保留自己的私钥</li>
<li>Alice 的公钥和私钥由 CA 生成,而不是 Alice 自己生成</li>
<li>CA 只需给将证书和私钥给 Alice 即可</li>
</ol>
<hr>
<h4>思考: CA 如何证明自己是 CA?</h4>
<hr>
<h4>再找个可信的第三方来证明</h4>
<h4>...</h4>
<hr>
<h4>最后的可信的第三方是操作系统的 CA</h4>
<hr>
<h3>Https 流程</h3>
<p><img src="/img/remote/1460000014371510?w=1512&h=932" alt="" title=""></p>
<hr>
<h3>我的一些错误操作</h3>
<ul>
<li>我把快应用的私钥和证书传到了 gitlab 上了</li>
<li>两次打包过程中,使用了两个不同的私钥和证书</li>
</ul>
等风来!区块链熊市,技术人就要做技术投资
https://segmentfault.com/a/1190000014308781
2018-04-11T13:57:01+08:00
2018-04-11T13:57:01+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<p>2017 年的区块链牛市已经过去了,目前看来,2018 年会是一个大熊市,投资抄币估计也捞不着什么。熊市只能囤囤币,囤囤技术,只能等下一轮风起了。<br>熊市囤技术,其实是技术人员很好的选择。等区块链牛市来了,说不定还可以加入区块链行业,获取区块链行业高速发展的红利。再不济,研究技术,对抄币也是有帮助的啊。于是自己,就在今年年初的时候下定决心,要学习一下区块链技术。</p>
<h2>囤技术的三个方向</h2>
<p>专门研究了一下“囤技术”的方向,大概可以分为三种:</p>
<ul>
<li>白皮书研究</li>
<li>区块链底层研发</li>
<li>智能合约研发</li>
</ul>
<p>白皮书研究,就是读大量的区块链白皮书,理解区块链项目的实现原理,然后从技术角度进行抄币分析。但摸摸自己的口袋,也没几个钱,投不了几个项目,所以这个方向不太适合我。</p>
<p>区块链底层研发,其职责是开发底层“链”,BAT 等一线互联网公司和区块链创业公司都有大量的招聘需求。该职位要求熟悉C++、GO、分布式网络、密码学、共识算法等。现在只有少量的程序员,能够满足该职位的要求,因此该类职位的月薪也是非常之高 —— 30k起。但是自己是前端工程师出身,要啃下这块硬骨头,难度有点大,想想就作罢了。</p>
<p>智能合约研发,更接近“应用”层 ,关注的是 DApp 的链上数据、逻辑和交互。智能合约工程师可以比作“区块链后端”,不过存数据的地方从数据库换成了区块链,写逻辑的语言从 Node.js 等换成了 Solidity,交互从 ajax 换成了 web3。现在 ETH、EOS、Fabric 等主流链的虚拟机,用的都是以太坊虚拟机 EVM。也就是说,通过一次学习,可以多个链上开发智能合约,这个性价比还不错的。目前来看,智能合约研发岗位非常少,只有区块链创业公司有招聘需求。但未来,区块链上的应用,肯定比链多很多。个人预测,短则一到两年,长则三到四年,区块链应用会有大爆发,到时候市场上的智能合约工程师的招聘需求也会大增,其薪资自然水涨船高。智能合约研发方向非常适合我,入手难度也不高。</p>
<h2>学习智能合约的三个途径</h2>
<p>我这两个月来的业余时间,基本都投入到了,智能合约学习中了。网上的智能合约开发资料中,很多是教你发币的,这类资料讲的太浅了,可以直接 pass。我认为,比较系统、且适合初学者的途径有三个:</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=C7ZCl%2FPxSc6I5b3xlHmtxA%3D%3D.YtpyzUZd0ZM1UmXsmm0wuEP9upDn2QDC2FNEt7CiwhrYDsJrM74k3vtAvHEgx63Y54Y4OKXuYOD9Qq4SkHbMk5JAyhSnRBdGt3tIYhe0t6fpkMtd%2Fk6WSYVpnToI3Ju%2B" rel="nofollow">Learning Solidity</a></li>
<li><a href="https://link.segmentfault.com/?enc=h5maE6Zb37ZDnE1m%2FwKO%2Bw%3D%3D.VgyUzKau8cGV3nuLKXEtfG7%2BINP%2F%2Bt2IAgLBa1749hE%3D" rel="nofollow">cryptozombies</a></li>
<li><a href="https://link.segmentfault.com/?enc=wN9P10%2BV9fLe8lYAuZoGbw%3D%3D.75g%2FVGY9mQHncj%2B4fYXY%2BYD6Og8vBHP%2Bb1zJ4BT5ikXM5daLJmUJ4PlYC%2BZjgMq%2BbYbcLfWFa%2FAX0rMyjDD7RQ%3D%3D" rel="nofollow">老董智能合约</a></li>
</ul>
<p>Learning Solidity ,是 YouTube 上的免费教学视频。现在更新到了第 27 课,每节课大概 10 分钟。不过全都是英语,听的我实在费劲,也就没有跟下去。</p>
<p>cryptozombies,是一个免费教学的智能合约编程网站。现在一共有 6 节课,从 Solidity 到智能合约部署都有涉及。每一课都有几个小节,每个小节会讲一个智能合约的基础知识,然后会出一个编程题,你要完成代码并提交通过,才能进行下一小节的学习。这门课程最大的好处是免费,但是内容深度不够,另外到现在也没有更新完。所以即使你学完后,也写不出一个完整的智能合约应用。我一开始是通过这个网站学习的,学完后发现不过瘾,就到知乎上去找找有没有其他学习途径,能够让我深入学习。这就让我发现了老董的智能合约课程,看知乎口碑不错,就花了 1699 大洋,报了第三期课程。</p>
<p>老董的智能合约课程,物超所值,非常值得购买。一共 7 课,包括 Solidity 语言,项目开发、调试和部署等流程,还有智能合约安全都有涉及。自己花了一个月的业余时间学完了,写了一个独立的应用,部署到了以太坊测试网络上了。</p>
<p><img src="/img/remote/1460000014308786" alt="智能合约项目代码" title="智能合约项目代码"></p>
<h2>提高学习效果的三个方法</h2>
<p>老董的智能合约课程,背后的提高学习效果的方法要重点说说。得益于课程的优秀的运营模式,初学者通过一个月的学习,基本可以把智能合约的知识体系搭建起来。我总结了其中三个提高学习效果的方法。</p>
<ul>
<li>找业内大咖当老师,带你快速上手</li>
<li>通过有效的奖励/惩罚机制,让你坚持学习下去</li>
<li>及时反馈,帮你建立正确的认知</li>
</ul>
<p>第一个方法就是,找业内大咖当老师,带你快速上手。初学者通过模仿业内大咖的思考方式和行为,可以把原先复杂的学习行为进行简化,从而快速上手。为什么要找大咖?不找大咖,学会了错误的习惯,以后被带到坑里去了怎么办?传统的 Mooc,在找大咖教学这点上做的很棒。但是 Mooc 最大的缺点就是,完成率太低,大概只有 5%。在 Mooc、得到买过课程的小伙伴会有这样的体验,当学习的新鲜劲过去后,也就学不动了。有些人认为,学不动的原因,在于自己毅力太差,或者时间不够。但是换个角度想,学习效果不好,课程的运营方难道没有责任吗?我在学习老董的课程中,找到了解决上述问题的答案,就是接下来我要讲的第二个方法。</p>
<p><img src="/img/remote/1460000014308787" alt="丹华资本对老董的评价" title="丹华资本对老董的评价"></p>
<p>第二个方法,通过有效的奖励/惩罚机制,让你坚持学习下去。第三期老董课程一共录取了 150 人,有 100 人完成了所有的作业,也就是老董课程的完成率为 67%。换句话说,老董的课程完成率比传统 Mooc 高 10 倍,这是一个质的提升!我把原因归结于,返还学费的机制和积分体系。返回学费机制,就是按时完成所有的课后作业,返回一半学费。积分机制是,通过每日打卡,完成作业,帮助同学答疑等可以获得积分,积分最高的同学可以返回全部学费。1699 元的学费,对于大部分人来说,是一笔失去后会心疼的钱。参与课程的同学,往往会抱有至少拿回一半原本属于自己的钱的心态参与进来。这种希望得到金钱奖励,或者害怕被失去自己学费的心态,会促使学员完成学业。</p>
<p><img src="/img/remote/1460000014308788" alt="班长雅珣退还一半学费" title="班长雅珣退还一半学费"></p>
<p>第三个方法,及时反馈,帮你建立正确的认知。要达到学习效果好的目的,不仅要有学习意愿,还要有即时反馈。通过即时反馈,肯定正确认知,纠正错误认知,可以很快的在大脑中建立正确知识模型。传统的 Mooc 交流是发生在论坛上的,基本很难有即时反馈这一环节。老董的课程,每堂课都有课后作业,并且有助教会帮忙批改作业。其次,微信群里,交流非常频繁,如果有疑问在群里提出,通常会立刻得到助教团队和热心同学的回答。最后,如果还有什么疑难杂症的问题,周末老师也会集中答疑。学生通过学习课程=>作业实践=>及时反馈三步,可以快速的建立正确知识模型。</p>
<p><a href="https://link.segmentfault.com/?enc=1XSYBDiMVTxfer7x5MqJdA%3D%3D.LZlEAHhh65%2FTwmqBReZGHs%2FY5jkSyNiZzOtoWxMKX86tEgYjEG1f8Y57m7IrxBRI" rel="nofollow">三期同学讨论积累资料的地址</a></p>
<p><img src="/img/remote/1460000014308789" alt="三期同学讨论积累资料" title="三期同学讨论积累资料"></p>
<p><img src="/img/remote/1460000014308790" alt="一共 15 个小组,这是我们组的 75 次作业提交记录" title="一共 15 个小组,这是我们组的 75 次作业提交记录"></p>
<p>等风来!区块链熊市,技术人就要做技术投资。风来了,一定有会大丰收的!</p>
详解区块链——从本质到实现原理
https://segmentfault.com/a/1190000013100081
2018-02-02T14:55:03+08:00
2018-02-02T14:55:03+08:00
fitfish
https://segmentfault.com/u/timetravel
4
<p>随着比特币、以太坊等数字货币的暴涨,数字货币的底层技术,区块链技术,开始进入大众的视野。姚劲波说:区块链有可能和互联网一样伟大。区块链技术比传统互联网技术好在哪里?它的实现原理优是什么呢?笔者希望通过本文,解答大家心中的疑问。</p>
<h2>信任问题</h2>
<p>信息在互联网上的复制和传播成本近乎为零,这让大家可以很轻易地发布和获取信息。但是资产和信息是不一样的,资产本应是不应该被随意复制的。如果人民币可以被随意复制,那么人人都是亿万富翁了 :-)。</p>
<p>现在的互联网和金融技术,通过搭建中心化服务器,解决了资产传播的问题,但是成本居高不下。原因在于,当今的金融市场是建立在不同的服务提供商组成的庞大网络上的。在这个庞大的网络中,存在着各种互相孤立的数据系统及运作体系,这极大地影响了金融市场效率的进一步提升。在 2012 年欧洲央行的一份报告中,估计除了每个人都支付的直接费用外,间接成本高达 GDP 的 1%,大概每年 1300 亿欧元。在世界银行的报告中,跨国汇款的成本接近 8%。[1]</p>
<p><strong>金融机构之间不能够无条件地信任对方,造成了金融领域数据孤岛的现象,进而导致了数字资产在互联网上流通成本居高不下。</strong>如果金融机构能够相互信任,那么所有的资产数据就可以在互联网上自由流通了,也就不存在数据孤岛的现象了,进而降低数字资产的流通费用。从道德上对信用的呼吁很难落到实处,有没有解决信任问题的技术手段呢?区块链技术的出现为解决信任问题带来了一丝曙光。现在区块链技术正在,除金融领域外的更多领域中进行应用,解决着这些领域内的信任问题。</p>
<h2>信任机器</h2>
<p>利用区块链技术,可以创造出一种信任机器[2]。比特币系统就是基于区块链的记账机器;以太坊就是基于区块链的智能合约机器等等。而<strong>信任秘诀在于区块链的加密、公开且不可篡改的特性</strong>:</p>
<h3>加密</h3>
<p>区块链技术是以密码学和数学为基础的,这是信任的根本。包括,椭圆曲线数字签名算法、非对称加密、哈希函数、梅克尔树等等。这些算法是密码学、数学上公认的穷宇宙之力都难以破解的算法。区块链上的数据是公开的,但这并不意味着你的秘密,可以被任何人知晓。利用这些加密算法可以保障你的数字权益,比如你的上网隐私,你的网络文章的所有权,还有你的数字钱包里的 Token。</p>
<h3>公开</h3>
<p>公开是赢得信任的最好手段。基于区块链技术的系统的规则(程序)和数据都是公开的。任何参与方都可以通过运行区块链的程序的方式加入进来,进而对数据进行验证。从程序可靠性的角度出发,信任既可以建立在一个黑盒程序之上,又可以建立在一个开源的程序之上。从交易双方的角度看,信任既可以建立在一个值得被信赖的第三方之上,又可以建立在自己亲自验证的基础之上。在自己能验证,也能依靠第三方进行验证情况下,自己验证更可靠;如果自己不能验证,那就只能选择一个值得被信赖的第三方。<strong>区块链提供了一个更公开、更透明且能够自己亲自验证的机制,因此基于区块链技术的系统比黑盒和第三方更容易赢得大家的信任。</strong></p>
<h3>不可篡改</h3>
<p>区块链技术不可篡改的特性,是数字资产不可复制的基础。技术上来说,我们可以对任何的数据进行:增、删、改、查。但是在基于区块链技术的系统上,删、改的操作的可行性几乎为零。</p>
<h2>核心原理</h2>
<p>加密技术是在区块链出现之前已有的技术,本文不详细展开。区块链的最大创新,在于公开且不可篡改。本文接下来会剖析区块链的核心原理,帮助大家理解为什么它是公开且不可篡改的。</p>
<h3>状态机</h3>
<p>我们先从最简单的区块链记账机器开始,比如以太坊系统(比特币系统的原理稍微绕一些,但本质一样),实际上就是多个节点维护同一个账本。记账机器会在账本上记录每笔交易的信息。通过初始时各个账户的余额和已记录的信息,就可以推断出任意时刻的各个账户的余额。也就是说区块链记账机器完成记账功能的基本原理是:状态机[3]。举个例子:在 state1 时,A B 都有 100;A 发起了一笔交易,支付 100 给 B,而这笔交易会被区块链系统记录下来。我们可以通过 state1 的账户余额和区块链上的交易记录,计算出 state2 时 A B 的账户余额:A 有 50,B 有 150。同理只要我们知道初始化的状态(Genesis),并将使用区块链系统记录每笔交易,就可以算出任意时刻的任意账户的余额了。</p>
<p><img src="/img/bV275g?w=1080&h=254" alt="clipboard.png" title="clipboard.png"></p>
<h3>双重支付</h3>
<p>在区块链技术出来之前,一直没有很好的方法解决去中心化记账过程中遇到的双重支付的问题。中心化记账指的是,一个节点(比如一台计算机)维护一个账本;去中心化记账指的是,多个节点维护同一套账本。</p>
<p>顾名思义,双重支付就是同一笔钱可用于两次支付。具体的说,就是在同一时间点,A 将 100 元,既支付给 B,又支付给 C。如果出现这种情况,A 就相当于把 100 元钱,当做 200 元钱来花了。但 B ,C 和记账机构三者之一就会因此遭受 100 元的损失。在中心化的记账系统中,双重支付的事情显然是不可能的。因为无论这两笔交易是否同时进行,中心化的记账系统处理这两笔交易一定会有一个先后顺序。中心化的记账系统会先处理第一笔交易,并在 A 的账户中扣除 100 元,再处理第二笔交易,如果此时 A 的账户中没有余额,第二笔交易就会失败。</p>
<p>在去中心化的记账系统中,会有多个记录交易信息的节点。在上述例子中,去中心化的记账系统中的一些节点会先收到 B 的交易信息;另一些节点会先收到 C 的这笔交易信息。在去中心化的记账系统中,所有节点都是平等的。不存在一个统筹的节点,来决定是先处理 B 的交易还是先处理 C 交易。这就产生了去中心化记账系统中双重支付的问题。</p>
<p>解决去中心化记账系统的双重支付问题,可以分成两步来讨论:第一步,确定交易信息的先后顺序;第二步,需要一个共识机制,来保证所有节点都认可这个顺序。</p>
<p>第一步,确认顺序的原理很简单,就是给交易排序。确定交易顺序的数据结构就是区块链。“区块链”中的“区块”,指的是在同一段时间内的交易信息及相关数据的集合。“链”就是把区块按产生的先后顺序连接在一起。</p>
<p><img src="/img/bV275r?w=1080&h=345" alt="clipboard.png" title="clipboard.png"></p>
<h3>共识机制</h3>
<p>共识机制是区块链系统上独立节点们通过遵守一套相同的规则,自发地对区块的先后顺序达成共识。这套规则可以简单的描述为以下 3 步[4]。</p>
<p>所有节点质押成本(如:算力)竞争记账权,由胜利者产出并广播区块(记账信息),并获得收益(如,比特币)奖励。</p>
<p>每个节点独立的对新区块进行验证,并组装进区块链。</p>
<p>每个节点对区块链进行独立选择,选择长度最长的区块链。</p>
<p>这个规则是如何让独立节点们自发地参与到区块链系统的记账之中呢?又是如何规避节点捣乱的问题呢?</p>
<p>第 1 步,保障了每个节点都会出于“自私”的目的,“诚实”地参与到区块链系统中来。“自私”指的是,节点都是为利润(收益 - 成本)而来。“诚实”指的是,遵循区块链的共识机制。但是有利润就会有作弊,如何防止作弊呢?这就要依靠后面两步。</p>
<p>第 2 步,保障了每个节点都可以对竞争胜出的节点的记账信息进行校验。即便有“捣乱”节点抢到了记账权,并记了假账,其他节点,包括你的节点,都可以通过验证得知是否作假。区块(账本)做不了假,那么有没有可能在链(顺序)上作假呢?也就是通过颠倒交易的先后顺序,进行双重支付。</p>
<p>第 3 步,保障了颠倒交易的先后顺序在经济上是不可行的。既然每个诚实的节点都选择的是最长的链,那么捣乱节点能不能制造一个最长的链呢?当捣乱链的长度,超过诚实链的长度的时候,整个交易的顺序就被颠倒过来了。在区块链上,父节点只能有一个,但是子节点可以有多个,多个子节点就会有分叉,称为 Fork。 捣乱节点可以在某个节点 Fork 原先的链,再用比诚实节点更快的速度,制造出一个最长捣乱链。这有没有可能呢?答案是,在技术上是有可能的,但是经济上是没有可能的。</p>
<p><img src="/img/bV275v?w=1080&h=564" alt="clipboard.png" title="clipboard.png"></p>
<h2>总结</h2>
<p>区块链技术的本质是通过公开的、加密的不可篡改的技术手段,为解决多方信任问题提供了一个方案。现在区块链技术离可大规模应用,还有很长的一段路要走。其中最关键原因是每秒确认交易的笔数太少、确认交易的时间又太长[5]。但是,换个角度思考一下,问题即机会。现在的区块链就像 98 年的互联网,未来充满挑战,也充满希望。</p>
<p>本文首发于 58无线技术 公众号</p>
<p>本文由【区块链研习社】优质内容计划支持,更多关于区块链的深度好文,请点击【区块链研习社】简书专栏:<a href="https://link.segmentfault.com/?enc=MOZEolDAW%2FkJ%2BYWc1W%2BH3w%3D%3D.1bk0B9sed5%2Bsy3vWC%2FemUa5x8RxLR%2BwFT2GmPsLbtfG9aRI8An9%2BBkUDn1s9G4dh" rel="nofollow">http://www.jianshu.com/c/b17f...</a></p>
<p>参考文章:</p>
<p>中国区块链技术和应用发展白皮书</p>
<p><a href="https://link.segmentfault.com/?enc=pJmeb7n2Ke%2B%2FxpB4F1%2FpGA%3D%3D.RCraIaBWj%2F%2FCxKnD4RE140NzaUapd61Ex7rV8XSYdBva39IbiiW%2FtsDzK63oezhb" rel="nofollow">http://www.199it.com/archives...</a></p>
<p>经济学人《The trust machine》</p>
<p><a href="https://link.segmentfault.com/?enc=RDaJaPzuAAsa7kvGnSFzQg%3D%3D.kHCf3rWfLwlYAopaYM8ftSafCOn1l6CSWfyhCtWrbjB%2F6eKmKFEDyvbugefa8tvdX8MKF7wZ7sqgGuK9hGCcpoXowEXQqVsH1nrxAUn9YU1r9uRCDQUVBxSgCisG4F1MsNnkF4ubyruG0D5IW1GqKlE%2FPN42KxytAGj1MerkFVU%3D" rel="nofollow">https://www.economist.com/new...</a></p>
<p>How does Ethereum work, anyway?</p>
<p><a href="https://link.segmentfault.com/?enc=sr1E8tvPmTiFansnXS8GGw%3D%3D.OvnvFVhcycsK7EwBnALIlGb9Hki4bwXPFACjoGU19ZAOVxLwdlV4NR%2BnERTGawjZTzuq%2BTIKjwTjXJ31ZVrQqPIFpeNbvPYNqPgHvPFP5SA%3D" rel="nofollow">https://medium.com/@preethika...</a></p>
<p>乔延宏译《精通比特币(第二版)》</p>
<p><a href="https://link.segmentfault.com/?enc=ExgFWQ0ZkDjtUlbuv3PCAQ%3D%3D.YhqSlthOYVlSTZovMI%2FIzyRMlVdiyW1XiZsY4QHRxYT6IC%2FMSYqJgOiEM5Lxbfd3IYlHC2qqDxVs8GXPJ6yFJy9B0K43EX70UuJEB6D813g%3D" rel="nofollow">http://book.8btc.com/books/6/...</a></p>
<p>Fundamental challenges with public blockchains</p>
<p><a href="https://link.segmentfault.com/?enc=VLnH8r3JoARdKtATDnidvQ%3D%3D.%2BLPBuXAofzKFjj4gt0z3bb6KFWuRRclEi0mkrTQwyXpQ3tiGol55POoaof%2BObxxjkzdEtn7NBxXdzje897drPYhhqLjXG%2FggqpyULTSa%2BLsE0aKA%2B4rwkRWZoTRHr6rKuhZmxLaryFoDyUpsdRc5Xw%3D%3D" rel="nofollow">https://medium.com/@preethika...</a></p>
在真机和模拟器中使用 devtools 调试(iOS Web版)
https://segmentfault.com/a/1190000012473189
2017-12-18T14:59:14+08:00
2017-12-18T14:59:14+08:00
fitfish
https://segmentfault.com/u/timetravel
4
<p>开发 Web 页面时,难免会碰到一些特定机型、特定版本或者是嵌到 App 中才会出现的问题。碰到这类问题时,如果不能使用开发者工具 devtools,这意味着,你只能使用 alert 或者 window.onerror 等手段。</p>
<p>谷歌和苹果分别提供了各自方案,来帮助开发者使用开发者工具 devtools 调试真机或模式器的 Web 页面。这极大的提高了调试效率。本文介绍了苹果提供的在真机和模拟器中使用 devtools 调试方法。</p>
<h2>真机调试</h2>
<p><strong>一 准备</strong></p>
<ol>
<li>需要一台 Mac。</li>
<li>需要一台在测试序列号中的 iPhone 手机。</li>
<li>hybrid 调试需要 iOS 同学帮忙安装一个测试版的 App。</li>
</ol>
<p><strong>二 开启 Safari Web 检查器的权限</strong></p>
<ol>
<li>打开<strong>设置</strong>
</li>
<li>依次进入 <strong>Safari > 高级</strong>
</li>
<li>开启 <strong>Web 检查器</strong>
</li>
</ol>
<p><img src="/img/remote/1460000012473192?w=370&h=683" alt="" title=""></p>
<p><strong>三 开启显示 Safari 开发菜单</strong></p>
<ol>
<li>打开<strong> Safari </strong>
</li>
<li>依次点开<strong> Safari 菜单 > 偏好设置 > 高级</strong>
</li>
<li>开启 <strong>在菜单栏中显示“开发”菜单</strong>
</li>
<li>这时就可以在 Safari 的菜单栏中看到 <strong>开发</strong> 选项了</li>
</ol>
<p><img src="/img/remote/1460000012473193?w=414&h=302" alt="" title=""></p>
<p><strong>四 使用 USB 连接 iPhone 和 Mac</strong></p>
<p><strong>五 打开App,进入页面</strong></p>
<p><strong>六 开启页面调试</strong></p>
<ol>
<li>打开 Safari</li>
<li>依次点开 <strong> 开发 > 某某的 Macbook > 具体页面</strong>
</li>
</ol>
<p><img src="/img/remote/1460000012473194?w=651&h=365" alt="" title=""></p>
<h2>模拟器中调试</h2>
<p><strong>一 准备</strong></p>
<ol>
<li>需要一台 Mac。</li>
<li>需要下载 Xcode 和 iOS Simulator。</li>
<li>hybrid 调试需要一个能在模拟器中安装的 App。</li>
</ol>
<p><strong>二 打开 Simulator</strong></p>
<ol>
<li>使用快捷键 <strong>command + 空格</strong>,打开 <strong>Spotlight</strong>
</li>
<li>输入 Simulator,并按回车打开。</li>
</ol>
<p><strong>三 打开App,进入页面</strong></p>
<p><strong>四 开启页面调试</strong></p>
<ol>
<li>打开 Safari</li>
<li>依次点开 <strong> 开发 > Simulator > 具体页面</strong>
</li>
</ol>
<p>注意事项: 必须先打开 Simulator,再打开 Safari。不然 Safari 检查不到 Simulator。</p>
<p><img src="/img/remote/1460000012473195?w=689&h=403" alt="" title=""></p>
使用 Portal 优雅实现“浮”在页面上的组件
https://segmentfault.com/a/1190000012325351
2017-12-07T14:37:02+08:00
2017-12-07T14:37:02+08:00
fitfish
https://segmentfault.com/u/timetravel
11
<h2>产品需求</h2>
<p>产品需求,实现一个选择器 Selector 组件,要求浮在页面上方。在网上随便找了个图,如下:</p>
<p><img src="/img/bVZSsk?w=720&h=1280" alt="clipboard.png" title="clipboard.png"></p>
<h3>实现方案</h3>
<p>实现这一的一个 Selector 组件并不难,不是本文的讨论内容。<br>本文讨论的主要是,在有类似于 Selector 组件一样,“浮”在页面的组件时,如何设计 React 组件树?</p>
<p>方案一:Seletor 组件是 App 组件的子组件。<br><img src="/img/bVZSu2?w=1122&h=1110" alt="clipboard.png" title="clipboard.png"></p>
<p>优势:Selector 属于 App 的子节点,子节点不受父节点的样式属性( <code>position</code> <code>overflow</code> )的干扰。</p>
<p>劣势:Selector 的显示状态属于 App 节点,跨分支传递状态成本太高。使用 Redux 或 Mobx 跨分支传递状态,依赖第三方组件,不利于复用;而手动传递,至少要 4 个步骤,如果 Button 节点更深,步骤会更多。并且这样写出的代码,耦合性太强,不利于维护。</p>
<p>方案二:Selector(fixed) 组件是 Button 组件的子组件。</p>
<p><img src="/img/bVZSuq?w=1094&h=1338" alt="clipboard.png" title="clipboard.png"></p>
<p>优势:Selector 的显示状态属于 Button 节点控制,状态管理成本低。</p>
<p>劣势:Selector 属于 Button 的子节点。而当父节点 Button 有文字超出隐藏的需求时(<code>overflow: hidden</code>),子节点 Selector 会被隐藏。</p>
<p>那么,有没有两全齐美的方案呢?有。</p>
<p>方案三:在 React 组件树设计上,Selector 是 Button 的子组件。但是在 DOM 树的角度 Selector 是 Body 的子节点。</p>
<p><img src="/img/bVZSuc?w=1248&h=1424" alt="clipboard.png" title="clipboard.png"></p>
<p>在这个方案中,Button 和 Selector 还是属于 React 组件树中的父子节点,享有父子组件状态传递方便的优势。<br>但是,Button 和 Selector 不再属于 DOM 树中的父子节点!Selector 被渲染到了 Body 节点下面,属于 Body 的子节点。这样 Selector 组件再也不会受到 Button 组件的样式干扰了。</p>
<p>在 React 中如何做到这一点呢?使用 React 16 的 <a href="https://link.segmentfault.com/?enc=9agZDO0Uk5JMF9uO5YWJ5w%3D%3D.DAs7dg%2F4La2N3BGlzvIO6nBr8mF4bxaniUz83rBU7%2F3q9ObU96%2BXqcmMMbKv3GcT" rel="nofollow">Portals</a>。<br>这个新属性的介绍文章很短,我就翻译下一吧。翻译只是意译,只为更好理解。</p>
<h2>Portals</h2>
<p>Portals 提供了一种超级棒的方法,可以将 react 子节点的 DOM 结构,渲染到 react 父节点之外的 DOM 中。</p>
<pre><code>ReactDOM.createPortal(child, container)</code></pre>
<p>第一个参数 child 是任何可以被渲染的 ReactChild,比如 element, string 或者 fragment. 第二个参数 container 是 一个 DOM 元素。</p>
<h3>使用方法</h3>
<p>一般来说,在 react 中是父子节点的关系,那么在 DOM 中也是父子节点的关系。</p>
<pre><code>render() {
// 在 react 中 div 和 children 是父子的关系,在 DOM 中 div 和 children 也是父子的关系。
return (
<div>
{this.props.children}
</div>
);
}</code></pre>
<p>然而,有时候打破了这种 react 父子节点和 DOM 父子节点的映射关系是非常有用的。使用 <code>createPortal</code> 可以将 react 的子节点插入到不同的 DOM 节点中。</p>
<pre><code>render() {
// React 并没有创建一个新的 div,来包裹 children。它将 children 渲染到了 domNode 中。
// domNode 可以是任意一个合法的 DOM 节点,无论它在 DOM 节点中的哪个位置。
return ReactDOM.createPortal(
this.props.children,
domNode,
);
}</code></pre>
<p><code>portal</code> 一个典型的用法是,当父组件有 <code>overflow: hidden</code> 或者 <code>z-index</code> 样式时,但是子组件需要“打破”父组件容器,显示在父组件之外。比如 dialogs,hovercards,tooltips 组件。</p>
<p>[在 CodePen 上尝试一下(<a href="https://codepen.io/gaearon/pen/yzMaBd)">https://codepen.io/gaearon/pe...</a></p>
<h3>Portals 的事件冒泡</h3>
<p>虽然 portal 可以在 DOM 树中的任意位置,但是它的行为依旧和普通的 React child 一样。比如上下文环境完全一样,无论 child 是不是 portal; portal 也一直存在于在 React 树上,无论它位于 DOM 树中的什么位置。</p>
<p>包括,事件冒泡。portal 节点的事件会冒泡到它的 React 树的祖先节点上,即使这些 React 树上的祖先节点并不是 DOM 树上的祖先节点。比如,有下面的 HTML 结构。</p>
<pre><code><html>
<body>
<div id="app-root"></div>
<div id="modal-root"></div>
</body>
</html></code></pre>
<p>在 DOM 树中是 portal 和它的 React 父组件兄弟节点,但是由于 React 的事件处理规则,让 portal 的 React 父组件有能力捕获 portal 的冒泡事件。</p>
<pre><code>// These two containers are siblings in the DOM
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');
class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}
componentDidMount() {
// The portal element is inserted in the DOM tree after
// the Modal's children are mounted, meaning that children
// will be mounted on a detached DOM node. If a child
// component requires to be attached to the DOM tree
// immediately when mounted, for example to measure a
// DOM node, or uses 'autoFocus' in a descendant, add
// state to Modal and only render the children when Modal
// is inserted in the DOM tree.
modalRoot.appendChild(this.el);
}
componentWillUnmount() {
modalRoot.removeChild(this.el);
}
render() {
return ReactDOM.createPortal(
this.props.children,
this.el,
);
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {clicks: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// This will fire when the button in Child is clicked,
// updating Parent's state, even though button
// is not direct descendant in the DOM.
this.setState(prevState => ({
clicks: prevState.clicks + 1
}));
}
render() {
return (
<div onClick={this.handleClick}>
<p>Number of clicks: {this.state.clicks}</p>
<p>
Open up the browser DevTools
to observe that the button
is not a child of the div
with the onClick handler.
</p>
<Modal>
<Child />
</Modal>
</div>
);
}
}
function Child() {
// The click event on this button will bubble up to parent,
// because there is no 'onClick' attribute defined
return (
<div className="modal">
<button>Click</button>
</div>
);
}
ReactDOM.render(<Parent />, appRoot);</code></pre>
<p>[在 CodePen 上尝试一下(<a href="https://codepen.io/gaearon/pen/jGBWpE)">https://codepen.io/gaearon/pe...</a></p>
<p>父组件能够捕获 portal 的冒泡事件的设计,允许开发者更加灵活的进行抽象,而这些抽象不依赖于 portal 。例如,如果你渲染一个 <code><Modal /></code> 组件,它的父组件能够捕获它的事件,无论使用的是不是 portal 实现的 (fixed 也能实现)。</p>
<h2>使用 portals 的实现 Selector</h2>
<pre><code>// 数据和选中的元素的状态由 Selector 自己控制
// 不要将 data、index 状态暴露给其他组件
// 暴露给父组件,越多和父组件耦合的就越重
class Selector extends Component {
componentDidMount(){
fetch('xxx')
.then(data => {
this.setState({
data,
})
})
}
handleSelect = index => {
this.setState({
index
})
}
render() {
return (
<List
data={this.state.data}
index={this.state.index}
onSelect={this.handleSelect}
/>
)
}
}
// 控制 Modal 显示状态都封装在 Button 中
class Button extends Component {
handleClick = () => {
this.setState( prevState => ({
show: !prevState.show
}))
}
render() {
return (
<div onClick={this.handleClick}>
<span>我是按钮</span>
// 为了保存 Selector 的状态,不要 unmount Modal,用 display: none 实现隐藏。
<Modal show={this.state.show}>
<Selector />
</Modal>
</div>
)
}
}
class App extends Component {
render() {
return (
<div>
<Button />
<Other />
</div>
)
}
}</code></pre>
<h3>讨论:属性暴露的越多越好,还是越少越好?</h3>
写一个轮播图,学会 React Native
https://segmentfault.com/a/1190000011830204
2017-11-02T16:38:16+08:00
2017-11-02T16:38:16+08:00
fitfish
https://segmentfault.com/u/timetravel
15
<p>我学习 Web 的第一课,就是学习写一个轮播图,在写轮播图时自然地将 html、css、js、DOM、组件设计等各方面简单的知识点给串起来了。学习 React Native 的时候,也自然用起了这个思路,挺好用的。本文通过写一个轮播图,希望帮助到那些对 React Native 有兴趣的同学。</p>
<p>本文会一步一步和带领大家实现一个轮播图组件,帮助大家将一个个单独的知识点给串。学习本文之前,最好对 React Native 有所了解。其中的一些单独的知识点,如果不是很了解,可以在学习过程中点击相关链接学习。这个单独的知识点包括:</p>
<ul>
<li>Components: View、Touchble*</li>
<li>APIs: Animated、PanResponder、StyleSheet</li>
</ul>
<p>配合 github 项目学习效果更佳:<br><a href="https://link.segmentfault.com/?enc=LAf%2FFuFV8j%2FyoAcnob%2B1cw%3D%3D.Jun38r315MJ80f355horx0BLtVxbvamB79N8UDS8wvhpYk8eikytAV00V3361uPU" rel="nofollow">https://github.com/jiangleo/l...</a></p>
<p>轮播图的最终效果图如下:</p>
<p><img src="/img/bVXNIB?w=480&h=324" alt="图片描述" title="图片描述"></p>
<h2>简单轮播图组件</h2>
<h3>接口设计</h3>
<p>一步实现最终效果图实现的效果是很难的,所以不如先把轮播图设计的简单点,然后一步一步地优化。<br>这个简单的轮播图组件,只拥有如下 3 个功能:</p>
<ul>
<li>在展现区域默认显示第 index 个项目的内容;</li>
<li>右滑,在展现区域显示上一个项目的内容;</li>
<li>左滑,在展现区域显示下一个项目的内容。</li>
</ul>
<p>轮播图的主要思想是,每次只显示一个个项目面,超出容器个项目面被隐藏,思路图如下:</p>
<p><img src="/img/bVXNIN?w=720&h=310" alt="图片描述" title="图片描述"><br><a href="https://link.segmentfault.com/?enc=a6p5hVzB0iUE%2Fqhkngh8kw%3D%3D.BwZjkNe%2FjrZKMRm3SJu1eZppBXmLXZkdxkc2MnSyK%2B3y6gcAF%2FsVarnPuGZg8DSG" rel="nofollow">图片来源</a></p>
<p>为了达到复用的效果,还需要将组件调用方和组件本身分离。即组件本身只有一个,但是可以被多次调用。</p>
<p>在明确简单轮播图组件的设计要求后,就很自然地设计出其调用方式:</p>
<ul>
<li>
<code>style</code>: 设置外部容器的样式。</li>
<li>
<code>index</code>: 控制组件展示第 <code>index</code> 项目。</li>
<li>
<code>onChange</code>: 当用户点击上一个按钮、点击下一个按钮触发,并通过回调参数通知调用方,<code>index</code> 应该怎么改变。</li>
<li>
<code>children</code>: 所有轮播项目。</li>
</ul>
<pre><code class="java">state={
index: 0,
}
render() {
return (
<Swiper
style={{with: 100}}
index={this.state.index}
onChange={(index)=> {
this.setState({
index: index
})
}}
>
<View />
<View />
<View />
</Swiper>
);
}</code></pre>
<h3>组件实现</h3>
<p>实现轮播的核心原理是,当 <code>index</code> 变化时,改变 Swiper 所有轮播项目的 <code>translateX</code> 值。超出 Swiper 容器的轮播项目会被隐藏,所以只会展现当前的第 <code>index</code> 个项目。其中有一个等式:</p>
<pre><code>轮播项目位移距离 = - 当前展示的项 * 外部容器宽度
translateX = - index * layoutWidth
</code></pre>
<p>在渲染之前,外部容器宽度 <code>layoutWidth</code> 是不知道的。因此只能在外部容器渲染后,通过 <code>onLayout</code> 函数,来获取外部容器宽度。在获取宽度后,再将正在的轮播项目渲染出来。但是这样做,需要两次渲染才能将轮播图显示出来。在一些对性能要求高的项目中,可以通过暴露一个外部容器初始化宽度 <code>initialWidth</code> 的接口来提前获取,避免两次渲染。</p>
<ul><li>新接口 <code>initialWidth</code>: 外部容器初始化宽度</li></ul>
<p>另外,我写代码的时候,有个小技巧,边写边测,通过小步迭代的方式,进行快速进行开发。因此,左滑、右滑切换的功能,不妨先用上一个、下一个按钮来代替。</p>
<p>其核心代码,如下:</p>
<pre><code class="java">_handleLayout = ({nativeEvent}) => {
this.setState({
layoutWidth: nativeEvent.layout.width,
})
}
render() {
const {children, style, index} = this.props;
const translateX = - index * this.state.layoutWidth;
const items = children.map((item, index) => React.cloneElement(
item,
{
key: index,
style: [
...item.props.style,
{
width: this.state.layoutWidth,
transform: [{translateX,}],
}
]
},
))
return (
<View
style={[styles.container,style]}
onLayout={this._handleLayout}
>
{items}
</View>
)
}</code></pre>
<p><a href="https://link.segmentfault.com/?enc=BQlZEASeGuAi3LXhzvULXA%3D%3D.8wCbiHci56CBCcDp3JFjG9UsrV9%2BXZTqCWXWgaV5%2Ft1oAAG3F%2ForCLXNtByPCiNvaUScoXefSECSa6lQ05BCO3936aeDaan9Fk8NsWCTX4vN1b6PlXgfPmgTa2R3KmQg" rel="nofollow">完整代码</a></p>
<h2>添加动画</h2>
<h3>
<code>Animated</code> 声明式动画</h3>
<p>动画功能会用到 <code>Animated</code> 这个 API。</p>
<p><code>Animated</code> 和 <code>state</code> 一样,都符合符合声明式编程的原理。由于 <code>Animated</code> 的动画值也可以看做页面的某种状态。在官网的示例代码中,直接将<code>Animated</code> 的动画值直接挂在了 <code>this.state</code> 上,也证明了这一点。<br>下面我们将 <code>Animated</code> 和 <code>state</code> 进行对比,帮助大家进行理解:</p>
<p># | Animated | state<br>声明 | <code>this.animKey = animValue}</code> | <code>this.state={stateKey: stateValue}</code><br>--| --| --<br>赋值 | <code><Animated.View props={this.animKey}></code> | <code><View props={this.state.stateKey}></code><br>改变状态 | this.animKey.setValue(newAnimValue) | <code>this.setState({stateKey: newStateValue})</code><br>改变状态_动画曲线形式 | <code>Animated.spring(this.animKey, {toValue: newAnimValue}).toStart()</code> | 无</p>
<h3>功能描述和接口实现</h3>
<p>在完成轮播图组件的基础切换功能的基础上,要给它添加动画功能:</p>
<ul>
<li>点击上一个按钮,从当前显示项目逐渐右移至上一个项目;</li>
<li>点击下一个按钮,从当前显示项目逐渐左移至下一个项目。</li>
</ul>
<p>一开始我们使用 <code>index</code> 这个属性来控制要展现的项目。因为动画会有中间值,比如介于 0 和 1 之间的值,所以我们需要一个新的值来表示项目的位置。</p>
<ul><li>positionAnimated:控制项目的位移位置</li></ul>
<p>为了组件接口的设计方便,不应该把这个底层状态 <code>positionAnimated</code> 暴露给组件调用方去处理。组件调用方依旧只需要控制 <code>index</code> 即可动画改变当前展示的项目。而在组件内部,监听 <code>index</code> 的更新,然后驱动 <code>positionAnimated</code> 的改变项目位置即可。</p>
<p>动画版轮播图的核心原理和最初的简单版类似:</p>
<pre><code>translateX = - index * layoutWidth
</code></pre>
<p>核心代码如下:</p>
<pre><code class="java">scrollTo = ( toIndex ) => {
Animated.spring(this.state.positionAnimated, {
toValue: - toIndex * this.state.layoutWidth,
friction: 12,
tension: 50,
}).start()
}
render() {
// ...
const items = children.map((item, index) => (
<Animated.View
style={{
width: layoutWidth,
transform: [{
translateX: this.state.positionAnimated
}],
}}
key={index}
>
{item}
</Animated.View>
));
// ...
}</code></pre>
<p><a href="https://link.segmentfault.com/?enc=Cbj4wfjd0FK5LfdZ%2Bt14Zw%3D%3D.96OyREiUIZPb2kFhqFSzur2CGBS2KFe59wcN4GY6JXlkuyLFR%2FeYwbkvl%2BKa7yj8i8OVC%2FqsL0YD4%2BAmDa89roX8HR%2BGm9TxgfhozxCxtFoXP%2FzVZ%2BtTJe%2BqbGCSMUSaBc7KBpicdRhbrMx4mxB%2FZg%3D%3D" rel="nofollow">完整代码</a></p>
<h2>支持手势控制</h2>
<h3>手势事件简介</h3>
<p>React Native 的手势事件类似于 Web,但 React Native 的手势事件更加强大和灵活。</p>
<p>两者相似点有:</p>
<p># | React Native | Web<br>--|--|--<br>开始触碰 | onPanResponderGrant | touchstart<br>开始移动 | onResponderMove | touchmove<br>结束触碰 | onResponderRelease | touchend<br>意外取消 | onResponderTerminate | touchcancel</p>
<p>两者不同点在于,React Native 可以针对具体元素绑定手势,而在 Web 中只能针对全局 <code>document</code> 进行手势监听。</p>
<p>在 React Native 手势接口设计上,大家可以先思考一个问题。因为 React Native 允许两个元素同时监听手势事件,如果两个元素都监听了手势,那么 React Native 应该响应那个元素呢?在 React Native 中设计了,成为响应者 <code>Responder</code> 的概念。大概可以描述为:如果没有响应者,任何元素都可以成为响应者;如果有元素是响应者,必须当前响应元素同意不再继续成为响应者后,其他元素才能变成响应者。总而言之,React Native 通过元素间的谈判,保障了手势响应者只有一个。谈判接口主要有:</p>
<p># | React Native | Web<br>--|--|--<br>开始触碰,是否成为响应者 | onStartShouldSetPanResponder => boolean | 无<br>开始移动,是否成为响应者 | onMoveShouldSetPanResponder => boolean | 无<br>有其他响应者,是否释放响应权 | onPanResponderTerminationRequest => boolean | 无</p>
<p>以上手势事件非常底层,写起来也很复杂。而一起简单的手势事件,如 click 事件,并不需要这么复杂。为此 React Native 基于以上手势事件,提供了 <code>TouchableHighlight</code> 等组件。该组件封装了一些常用的点击事件和点击相关的配置,如: <code>onPress</code>(click)、<code>underlayColor</code> 点击态背景色等。</p>
<p>在写简单轮播图时,用的是点击事件来代替滑动事件。点击事件的处理,用到的就是 <code>TouchableHighlight</code> 组件。</p>
<h3>实现</h3>
<p>手势轮播图在动画轮播图上进行了升级,它需要支持以下功能:</p>
<ul>
<li>滑动:用户滑动时,轮播图跟着手指移动;</li>
<li>右滑:用户向右滑动超过某个阙值后,触发右滑动作,轮播图位移至上一个项目;</li>
<li>左滑:用户向左滑动超过某个阙值后,触发左滑动作,轮播图位移至下一个项目。</li>
</ul>
<p>当用户滑动时,需要相应的改变 <code>positionAnimated</code> 的值,使轮播图跟着手指移动。这里有个等式:</p>
<pre><code>最终的位置 = 开始的位置 + 手势移动过的距离
position = startPosition + movePosition
</code></pre>
<p>开始的位置,需要在轮播图响应手势时 <code>onPanResponderGrant</code> 记录。手势移动过的距离可以在手势移动时 <code>onResponderMove</code> 获取,与此同时通过 <code>positionAnimated.setValue(position)</code> 改变轮播图的位置,让轮播图跟着手指移动。</p>
<p>左滑、右滑,是在用户抬起手指时 <code>onResponderRelease</code> 开始触发,触发的临界点我们可以简单的设置为外部容器一半的宽度。然后通过 <code>onChange</code> 事件告诉,调用方要改变的位置是什么,由调用方位移轮播图。</p>
<p>实现的核心代码如下:</p>
<pre><code class="java">onPanResponderEnd = () => {
// 超过 50% 的距离,触发左滑、右滑
const index = Math.round(-this.position / this.state.layoutWidth)
const safeIndex = this.getSafeIndex(index);
this.props.onChange(safeIndex)
};
responder = PanResponder.create({
onPanResponderGrant: (evt, gestureState) => {
// 用户手指触碰屏幕,停止动画
this.state.positionAnimated.stopAnimation();
// 记录手势响应时的位置
this.startPosition = this.position;
},
onPanResponderMove: (evt, { dx }) => {
// 要变化的位置 = 手势响应时的位置 + 移动的距离
const position = this.startPosition + dx
this.state.positionAnimated.setValue(position)
},
onPanResponderRelease: this.onPanResponderEnd,
onPanResponderTerminate: this.onPanResponderEnd,
});</code></pre>
<p><a href="https://link.segmentfault.com/?enc=jn5XJYpw9pdYPvSnZsOcPw%3D%3D.nfLJl0xd2hkAQipDf%2Fu2ieJoxkioEPHw5XWV2Bg57oOvV33gXT4SigL4rOnKItJYLT0RQTVkAOYCljs3nxy0Nwsv6oG2T%2F8j8Blo7PxLeyjTNFTpDpAMfH6PRBLS4axZ" rel="nofollow">完整代码</a></p>
<h2>总结</h2>
<p>到此一个 React Native 轮播图的也已经实现了,相信大家也应该对 React Native 有了大概的了解和认知。</p>
<p>在写这个轮播图的过程中,应用了 <code>View</code>、<code>Touchble*</code> 组件和 <code>Animated</code>、<code>PanResponder</code>、<code>StyleSheet</code> API。</p>
<p>在写轮播图的过程中,还应用了小步迭代的开发方式。即实现的过程中,将这个轮播图分为了三个阶段进行开发:简单轮播图、动画轮播图、手势轮播图。每个阶段,又可以分为三个步骤:准备要应用的知识(google)、实现功能描述、实现。通过小步迭代的方式,可以将一个大问题分解为几个小问题,再把小问题分解为最基本的知识点,再去设法实现。</p>
<p>最后,这还只是一个轮播图的雏形,还有很多优化点可以做,比如:</p>
<ul>
<li>isLoop: 是否头尾衔接的循环轮播</li>
<li>horizontal:是否水平轮播</li>
<li>renderPager:接受一个组件,该用于处理手势和动画。比如可以使用 ScrollView 和 ViewPagerAnder,在一些特定场景下处理手势和动画,达到更高的性能。</li>
<li>showsPagination:实现展现轮播提示的视图,比如小圆点提示当前播到第几个轮播项目了。</li>
</ul>
<p>大家可以参考代码中的 SwiperAndroid 进行完成。</p>
写 React 组件的最佳实践
https://segmentfault.com/a/1190000010835260
2017-08-24T17:16:29+08:00
2017-08-24T17:16:29+08:00
fitfish
https://segmentfault.com/u/timetravel
13
<p>本文为译文,已获得原作者允许,原文地址:<a href="https://link.segmentfault.com/?enc=d6wKQBDS0MNtEvGr92wPPw%3D%3D.fXqQsFUc2RSKXJRZZBf770UY5foTpyulSwp3MT8UIYNK7h8rSi0HkJNvc8inG95hCSIKTz6eTvkHhuibc34q9mWCtSDfTRl3zY%2BR1Mce3wg%3D" rel="nofollow">http://scottdomes.com/blog/ou...</a></p>
<p>当我第一次开始写 React 时,我发现多少个 React 教程,就有多少种写 React 组件方法。虽然如今,框架已经成熟,但是并没有一个 “正确” 写组件的方法。</p>
<p>在 MuseFind 的一年以来,我们的团队写了大量的 React 组件。我们精益求精,不断完善写 React 组件的方法。</p>
<p>本文介绍了,我们团队写 React 组件的最佳实践。<br>我们希望,无论你是初学者,还是经验丰富的人,这篇文章都会对你有用的。</p>
<p>在开始介绍之前,先说几个点:</p>
<ul>
<li><p>我们团队使用 ES6 和 ES7 的语法。</p></li>
<li><p>如果不清楚表现组件(presentational components)和容器组件(container components)之间的区别,我们建议先阅读 <a href="https://link.segmentfault.com/?enc=Grv6ZkL%2FccOEbMR0KuWIlQ%3D%3D.%2FG1jq6zu7Pr4T05YtAxdN6594wsT%2F1%2F0p%2FpMwwV34sbHxD65iEkNLEDom2kVSeoidkpoUVfDFe2%2B3q8xEyT3bDBoFM3d5aybe8SNHvLAhtj0ctQczOsoUFYHQbBU5CLK" rel="nofollow">这篇文章</a>。</p></li>
<li><p>如果有任何建议,问题或反馈意见,请在评论中通知我们。</p></li>
</ul>
<h2>基于类的组件</h2>
<p>基于类的组件(Class based components)是包含状态和方法的。<br>我们应该尽可能地使用基于函数的组件(Functional Components<br>)来代替它们。但是,现在让我们先来讲讲怎么写基于类的组件。</p>
<p>让我们逐行地构建我们的组件。</p>
<h3>引入 CSS</h3>
<pre><code class="js">import React, { Component } from 'react'
import { observer } from 'mobx-react'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'</code></pre>
<p>我认为最理想的 CSS 应该是 <a href="https://link.segmentfault.com/?enc=xdqsCdGMxpFsNxAsiK%2FZ7g%3D%3D.PyrTqvV%2FAwtMiVyWAEghayL%2F2CiF4Kh93NJwIdl%2Fe46cZ3PvKIi7YiaYjWv1J3g64FiJoDQHZeHKHoD4JHldKQ6UubWygj58BFyIpFEKf8lwkaZcjFWCKD0707hSPyIe" rel="nofollow"> CSS in JavaScript</a>。但是,这仍然是一个新的想法,还没有一个成熟的解决方案出现。<br>所以,现在我们还是使用将 CSS 文件引入到每个 React 组件中的方法。</p>
<p>我们团队会先引入依赖文件(node_modules 中的文件),然后空一行,再引入本地文件。</p>
<h3>初始化状态</h3>
<pre><code class="js">import React, { Component } from 'react'
import { observer } from 'mobx-react'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
export default class ProfileContainer extends Component {
state = { expanded: false }</code></pre>
<p>可以使用在 <code>constructor</code> 中初始化状态的老方法。<br>也可以使用 ES7 这种简单的初始化状态的新方法。<br>更多,请阅读 <a href="https://link.segmentfault.com/?enc=DA4hvNQyGnsw6EMcPKw%2FtA%3D%3D.GyVdlhqnzwISZ3zJ0%2FSN1%2FWCzTd2SCBMdBhYWy7g4eJ8NeHIuKta2pwDas0HrvMBrH4tFNV1jI1KPyklucFUDeFN%2FlHsdGsAkOBiz5H3DJI%3D" rel="nofollow">这里</a>。</p>
<h3>propTypes and defaultProps</h3>
<pre><code class="js">import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
export default class ProfileContainer extends Component {
state = { expanded: false }
static propTypes = {
model: object.isRequired,
title: string
}
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}</code></pre>
<p><code>propTypes</code> 和 <code>defaultProps</code> 是静态属性(static properties),在组件代码中,最好把它们写在组件靠前的位置。当其他开发人员查看这个组件的代码时,应该立即看到 <code>propTypes</code> 和 <code>defaultProps</code>,因为它们就好像这个组件的文档一样。(译注:关于组件书写的顺序,参考 <a href="https://link.segmentfault.com/?enc=bVbMx7RFusCrKumQcDAuYw%3D%3D.k74lH6bOc9Kt113FaYURzY1oRwS12deHgqJ%2FbFJ8R9PVXrA3YgmIlGmEOGi3HB%2B5AIF83IFMVKHvpQLnsTx0s7HdbnOaa2RvZ3EWFgntw1JxqvW16llKL2pONHMiBhx4" rel="nofollow">这篇文章</a>)</p>
<p>如果使用 React 15.3.0 或更高版本,请使用 <a href="https://link.segmentfault.com/?enc=kqQkYPZ0Cy6JfbXQ9ZVk3A%3D%3D.d2Nhgu%2F54rUXGQrUnFlBiSLxNhWiYnLWy02S%2BcOuKDRqJezek4k4XQi2vORHrUje" rel="nofollow">prop-types</a> 代替 React.PropTypes。使用 <code>prop-types</code> 时,应当将其解构。</p>
<p>所有组件都应该有 <code>propTypes</code>。</p>
<h3>Methods</h3>
<pre><code class="js">import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
export default class ProfileContainer extends Component {
state = { expanded: false }
static propTypes = {
model: object.isRequired,
title: string
}
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
handleSubmit = (e) => {
e.preventDefault()
this.props.model.save()
}
handleNameChange = (e) => {
this.props.model.changeName(e.target.value)
}
handleExpand = (e) => {
e.preventDefault()
this.setState({ expanded: !this.state.expanded })
}</code></pre>
<p>使用基于类的组件时,当你将方法传递给组件时,你必须保证方法在调用时具有正确的上下文 <code>this</code>。常见的方法是,通过将 <code>this.handleSubmit.bind(this)</code> 传递给子组件来实现。</p>
<p>我们认为,上述方法更简单,更直接。通过 ES6 箭头功能自动 <code>bind</code> 正确的上下文。</p>
<h3>给 <code>setState</code> 传递一个函数</h3>
<p>在上面的例子中,我们这样做:</p>
<pre><code class="js">this.setState({ expanded: !this.state.expanded })</code></pre>
<p>因为 <code>setState</code> 它实际上是异步的。<br>由于性能原因,所以 React 会批量的更新状态,因此调用 <code>setState</code> 后状态可能不会立即更改。</p>
<p>这意味着在调用 <code>setState</code> 时,不应该依赖当前状态,因为你不能确定该状态是什么!</p>
<p>解决方案是:给 <code>setState</code> 传递函数,而不是一个普通对象。函数的第一个参数是前一个状态。</p>
<pre><code class="js">this.setState(prevState => ({ expanded: !prevState.expanded }))</code></pre>
<h3>解构 Props</h3>
<pre><code class="js">import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
export default class ProfileContainer extends Component {
state = { expanded: false }
static propTypes = {
model: object.isRequired,
title: string
}
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
handleSubmit = (e) => {
e.preventDefault()
this.props.model.save()
}
handleNameChange = (e) => {
this.props.model.changeName(e.target.value)
}
handleExpand = (e) => {
e.preventDefault()
this.setState(prevState => ({ expanded: !prevState.expanded }))
}
render() {
const {
model,
title
} = this.props
return (
<ExpandableForm
onSubmit={this.handleSubmit}
expanded={this.state.expanded}
onExpand={this.handleExpand}>
<div>
<h1>{title}</h1>
<input
type="text"
value={model.name}
onChange={this.handleNameChange}
placeholder="Your Name"/>
</div>
</ExpandableForm>
)
}
} </code></pre>
<p>如上,当组件具有多个 <code>props</code> 值时,每个 <code>prop</code> 应当单独占据一行。</p>
<h3>装饰器</h3>
<pre><code class="js">@observer
export default class ProfileContainer extends Component {</code></pre>
<p>如果使用 <code>mobx</code>,那么应当是用装饰器(decorators)。其本质是将装饰器的组件传递到一个函数。</p>
<p>使用装饰器一种更加灵活和更加可读的方式。<br>我们团队在使用 <code>mobx</code> 和我们自己的 <code> mobx-models</code> 库时,使用了大量的装饰器。</p>
<p>如果您不想使用装饰器,也可以按照下面的方式做:</p>
<pre><code class="js">class ProfileContainer extends Component {
// Component code
}
export default observer(ProfileContainer)</code></pre>
<h3>闭包</h3>
<p>避免传递一个新闭包(Closures)给子组件,像下面这样:</p>
<pre><code class="js"><input
type="text"
value={model.name}
// onChange={(e) => { model.name = e.target.value }}
// ^ 上面是错误的. 使用下面的方法:
onChange={this.handleChange}
placeholder="Your Name"/></code></pre>
<p>为什么呢?因为每次父组件 <code>render</code> 时,都会创建一个新的函数(译注:通过 <code>(e) => { model.name = e.target.value }</code> 创建的新的函数也叫 <a href="https://link.segmentfault.com/?enc=e38OJxNwujjaBva8t8JcIw%3D%3D.o1D%2F6Pxi2sp33GPFWKHIS0w%2FVqCBqyMv3otdVQioJfDkPzkWW74C6yYReGSRK2CMRlFnSGwE7YXkHTb8bCe2ihriur9eotXzUwvZdM1o%2BBk%3D" rel="nofollow">闭包</a>)。 </p>
<p>如果将这个新函数传给一个 React 组件,无论这个组件的其他 <code>props</code> 有没有真正的改变,都就会导致它重新渲染。</p>
<p>调和(Reconciliation)是 React 中最耗费性能的一部分。因此,要避免传递新闭包的写法,不要让调和更加消耗性能!另外,传递类的方法的之中形式更容易阅读,调试和更改。</p>
<p>下面是我们整个组件:</p>
<pre><code class="js">import React, { Component } from 'react'
import { observer } from 'mobx-react'
import { string, object } from 'prop-types'
// Separate local imports from dependencies
import ExpandableForm from './ExpandableForm'
import './styles/ProfileContainer.css'
// Use decorators if needed
@observer
export default class ProfileContainer extends Component {
state = { expanded: false }
// Initialize state here (ES7) or in a constructor method (ES6)
// Declare propTypes as static properties as early as possible
static propTypes = {
model: object.isRequired,
title: string
}
// Default props below propTypes
static defaultProps = {
model: {
id: 0
},
title: 'Your Name'
}
// Use fat arrow functions for methods to preserve context (this will thus be the component instance)
handleSubmit = (e) => {
e.preventDefault()
this.props.model.save()
}
handleNameChange = (e) => {
this.props.model.name = e.target.value
}
handleExpand = (e) => {
e.preventDefault()
this.setState(prevState => ({ expanded: !prevState.expanded }))
}
render() {
// Destructure props for readability
const {
model,
title
} = this.props
return (
<ExpandableForm
onSubmit={this.handleSubmit}
expanded={this.state.expanded}
onExpand={this.handleExpand}>
// Newline props if there are more than two
<div>
<h1>{title}</h1>
<input
type="text"
value={model.name}
// onChange={(e) => { model.name = e.target.value }}
// Avoid creating new closures in the render method- use methods like below
onChange={this.handleNameChange}
placeholder="Your Name"/>
</div>
</ExpandableForm>
)
}
}</code></pre>
<h2>基于函数的组件</h2>
<p>基于函数的组件(Functional Components)是没有状态和方法的。它们是纯粹的、易读的。尽可能的使用它们。</p>
<h3>propTypes</h3>
<pre><code class="js">import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'
import './styles/Form.css'
ExpandableForm.propTypes = {
onSubmit: func.isRequired,
expanded: bool
}
// Component declaration</code></pre>
<p>在声明组件之前,给组件定义 <code>propTypes</code>,因为这样它们可以立即被看见。<br>我们可以这样做,因为 JavaScript 有函数提升(function hoisting)。</p>
<h3>解构 Props 和 defaultProps</h3>
<pre><code class="js">import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'
import './styles/Form.css'
ExpandableForm.propTypes = {
onSubmit: func.isRequired,
expanded: bool,
onExpand: func.isRequired
}
function ExpandableForm(props) {
const formStyle = props.expanded ? {height: 'auto'} : {height: 0}
return (
<form style={formStyle} onSubmit={props.onSubmit}>
{props.children}
<button onClick={props.onExpand}>Expand</button>
</form>
)
}</code></pre>
<p>我们的组件是一个函数,函数的参数就是组件的 <code>props</code>。我们可以使用解构参数的方式:</p>
<pre><code>import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'
import './styles/Form.css'
ExpandableForm.propTypes = {
onSubmit: func.isRequired,
expanded: bool,
onExpand: func.isRequired
}
function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
const formStyle = expanded ? {height: 'auto'} : {height: 0}
return (
<form style={formStyle} onSubmit={onSubmit}>
{children}
<button onClick={onExpand}>Expand</button>
</form>
)
}</code></pre>
<p>注意,我们还可以使用默认参数作为 <code>defaultProps</code>,这种方式可读性更强。<br>如果 <code>expanded</code> 未定义,则将其设置为false。(这样可以避免类似 ‘Cannot read <property> of undefined’ 之类的错误)</p>
<p>避免使用函数表达式的方式来定义组件,如下:</p>
<pre><code>const ExpandableForm = ({ onExpand, expanded, children }) => {</code></pre>
<p>这看起来非常酷,但是在这里,通过函数表达式定义的函数却是匿名函数。</p>
<p>如果 Bable 没有做相关的命名配置,那么报错时,错误堆栈中不会告诉具体是哪个组件出错了,只会显示 <<anonymous>> 。这使得调试变得非常糟糕。</p>
<p>匿名函数也可能会导致 React 测试库 Jest 出问题。由于这些潜在的隐患,我们推荐使用函数声明,而不是函数表达式。</p>
<h3>包裹函数</h3>
<p>因为基于函数的组件不能使用修饰器,所以你应当将基于函数的组件当做参数,传给修饰器对应的函数:</p>
<pre><code>import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'
import './styles/Form.css'
ExpandableForm.propTypes = {
onSubmit: func.isRequired,
expanded: bool,
onExpand: func.isRequired
}
function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
const formStyle = expanded ? {height: 'auto'} : {height: 0}
return (
<form style={formStyle} onSubmit={onSubmit}>
{children}
<button onClick={onExpand}>Expand</button>
</form>
)
}
export default observer(ExpandableForm)</code></pre>
<p>全部的代码如下:</p>
<pre><code class="js">import React from 'react'
import { observer } from 'mobx-react'
import { func, bool } from 'prop-types'
// Separate local imports from dependencies
import './styles/Form.css'
// Declare propTypes here, before the component (taking advantage of JS function hoisting)
// You want these to be as visible as possible
ExpandableForm.propTypes = {
onSubmit: func.isRequired,
expanded: bool,
onExpand: func.isRequired
}
// Destructure props like so, and use default arguments as a way of setting defaultProps
function ExpandableForm({ onExpand, expanded = false, children, onSubmit }) {
const formStyle = expanded ? { height: 'auto' } : { height: 0 }
return (
<form style={formStyle} onSubmit={onSubmit}>
{children}
<button onClick={onExpand}>Expand</button>
</form>
)
}
// Wrap the component instead of decorating it
export default observer(ExpandableForm)</code></pre>
<h2>JSX 中的条件表达式</h2>
<p>很可能你会做很多条件渲染。这是你想避免的:</p>
<p><img src="/img/bVTCRo?w=1513&h=487" alt="图片描述" title="图片描述"></p>
<p>不,三目嵌套不是一个好主意。</p>
<p>有一些库解决了这个问题(JSX-Control Statementments),但是为了引入另一个依赖库,我们使用复杂的条件表达式,解决了这个问题:</p>
<p><img src="/img/bVTCRw?w=825&h=352" alt="图片描述" title="图片描述"></p>
<p>使用大括号包裹一个立即执行函数(IIFE),然后把你的 <code>if</code> 语句放在里面,返回你想要渲染的任何东西。<br>请注意,像这样的 IIFE 可能会导致一些性能消耗,但在大多数情况下,可读性更加重要。</p>
<p>更新:许多评论者建议将此逻辑提取到子组件,由这些子组件返回的不同 <code>button</code>。这是对的,尽可能地拆分组件。</p>
<p>另外,当你有布尔判断渲染元素时,不应该这样做:</p>
<pre><code class="js">{
isTrue
? <p>True!</p>
: <none/>
}</code></pre>
<p>应该使用短路运算:</p>
<pre><code class="js">{
isTrue &&
<p>True!</p>
}</code></pre>
React 状态管理库: Mobx
https://segmentfault.com/a/1190000010084073
2017-07-07T08:36:28+08:00
2017-07-07T08:36:28+08:00
fitfish
https://segmentfault.com/u/timetravel
46
<p>React 是一个专注于视图层的库。React 维护了状态到视图的映射关系,开发者只需关心状态即可,由 React 来操控视图。</p>
<p>在小型应用中,单独使用 React 是没什么问题的。但在复杂应用中,容易碰到一些状态管理方面的问题,如:</p>
<ul>
<li><p>React 只提供了在内部组件修改状态的接口 <code>setState</code>。导致数据、业务逻辑和视图层耦合在组件内部,不利于扩展和维护。</p></li>
<li><p>React 应用即一颗组件树。兄弟节点,或者不在同一树杈的节点之间的状态同步是非常麻烦。</p></li>
<li><p>关心性能的情况下,需要手动设置 <code>shouldComponentUpdate</code></p></li>
</ul>
<p>这时就需要引入状态管理库。现在常用的状态管理库有 Mobx 和 Redux,本文会重点介绍 Mobx,然后会将 Mobx 和 Redux 进行对比,最后展望下未来的 React 状态管理方面趋势。</p>
<h2>Mobx 简介</h2>
<p>Mobx 的理念非常简单,可以用一个 demo 就把其核心原理说清楚。Mobx/MobxReact 中有三个核心概念,<code>observable</code>、<code>observer</code>、<code>action</code>。为了简单起见,本文没有提及 <code>computed</code> 等概念。</p>
<ul>
<li><p><code>observable</code>: 通过 <code>observable(state)</code> 定义组件的状态,包装后的状态是一个可观察数据(Observable Data)。</p></li>
<li><p><code>observer</code>: 通过 <code>observer(ReactComponent)</code> 定义组件。</p></li>
<li><p><code>action</code>: 通过 <code>action</code> 来修改状态。</p></li>
</ul>
<p>简化图如下:</p>
<p><img src="/img/bVQtuq?w=816&h=940" alt="图片描述" title="图片描述"></p>
<p>只讲概念还比较模糊,下面给大家举个例子。</p>
<p><a href="https://jsfiddle.net/jhwleo/1L5jcykr/9/">点击运行 https://jsfiddle.net/jhwleo/1L5jcykr/9/</a></p>
<pre><code class="javascript">// 通过 observable 定义组件的状态
const user = mobx.observable({
name: "Jay",
age: 22
})
// 通过 action 定义如何修改组件的状态
const changeName = mobx.action(name => user.name = name)
const changeAge = mobx.action(age => user.age = age)
// 通过 observer 定义 ReactComponent 组件。
const Hello = mobxReact.observer(class Hello extends React.Component {
componentDidMount(){
// 视图层通过事件触发 action
changeName('Wang') // render Wang
}
render() {
// 渲染
console.log('render',user.name);
return <div>Hello,{user.name}!</div>
}
})
ReactDOM.render(<Hello />, document.getElementById('mount'));
// 非视图层事件触发,外部直接触发 action
changeName('Wang2')// render Wang2
// 重点:没有触发重新渲染
// 原因:Hello 组件并没有用到 `user.age` 这个可观察数据
changeAge('18') // no console</code></pre>
<p>例子看完了,是不是非常简单。</p>
<p>使用 Mobx,组件状态可以在外部定义(也可以在组件内部),因此,数据、业务逻辑可以轻易地和视图层分离,提高应用的可扩展性和可维护性。另外,由于组件状态可以在外部定义,兄弟节点之间的状态同步也非常容易。最后一点, Mobx 知道什么时候应该渲染页面,因此基本不需要手动设置 <code>shouldComponentUpdate</code> 来提高应用性能。</p>
<p>接下来给大家介绍下 Mobx 中 <code>observable</code> <code>observer</code> <code>action</code> 的用法,并会简单介绍一下其原理。</p>
<h3>observable</h3>
<p>Mobx 如此简单的原因之一,就是使用了<strong>可观察数据</strong>(Observable Data)。简单说,可观察数据就是可以观察到数据的读取、写入,并进行拦截。</p>
<p>Mobx 提供了 <code>observable</code> 接口来定义可观察数据。定义的可观察数据,通常也是组件的状态。该方法接收一个参数,参数可以是原始数据类型、普通 Object、Array、或者 ES6 中的 Map 类型,返回一个 <code>observable</code> 类型的参数。</p>
<pre><code class="javascript">Array.isArray(mobx.observable([1,2,3])) === false // true
mobx.isObservable(mobx.observable([1,2,3])) === true // true</code></pre>
<p>注意,数组经过 <code>observable</code> 包装后,就不是 Array 类型了,而是 Mobx 定义的一个特殊类型 ———— <code>observable</code> 类型。<code>observable</code> 类型,可以通过 <code>mobx.isObservable</code> 来检查。</p>
<p>虽然数据类型不一样,但是使用方式基本和原来一致(原始数据类型除外)。</p>
<pre><code class="javascript">const observableArr = mobx.observable([1,2,3]);
const observableObj = mobx.observable({name: 'Jay'});
const observableMap = mobx.observable(new Map([['name','Wang']]));
console.log(observableArr[0]) // 1
console.log(observableObj.name) // Jay
console.log(observableMap.get('name')) // Wang</code></pre>
<p>可观察数据类型的原理是,在读取数据时,通过 <code>getter</code> 来拦截,在写入数据时,通过<code>setter</code> 来拦截。</p>
<pre><code class="javascript">Object.defineProperty(o, key, {
get : function(){
// 收集依赖的组件
return value;
},
set : function(newValue){
// 通知依赖的组件更新
value = newValue
},
});</code></pre>
<p>在可观察数据被组件读取时,Mobx 会进行拦截,并记录该组件和可观察数据的依赖关系。在可观察数据被写入时,Mobx 也会进行拦截,并通知依赖它的组件重新渲染。</p>
<h3>observer</h3>
<p><code>observer</code> 接收一个 React 组件作为参数,并将其转变成响应式(Reactive)组件。</p>
<pre><code class="javascript">// 普通组件
const Hello = mobxReact.observer(class Hello extends React.Component {
render() {
return <div>Hello,{user.name}!</div>
}
})
// 函数组件
const Hello = mobxReact.observer( () => (
<div>Hello,{user.name}!</div>
))</code></pre>
<p>响应式组件,即当且仅当组件依赖的可观察数据发生改变时,组件才会自动响应,并重新渲染。</p>
<p>在本文最开始的例子中,响应式组件依赖了 <code>user.name</code>,但是没有依赖 <code>user.age</code>。所以当<code>user.name</code> 发现变化时,组件更新。而 <code>user.age</code> 发生变化时,组件没有更新。</p>
<p>这里再详细分析本文中的第一个例子:</p>
<pre><code class="javascript">user.name = 'Wang2'// render Wang2
// 重点:没有触发重新渲染
// 原因:Hello 组件并没有用到 `user.age` 这个可观察数据
user.age = '18' // no console</code></pre>
<p>当可观察数据变化时,Mobx 会调用 <code>forceUpdate</code> 直接更新组件。</p>
<p><a href="https://link.segmentfault.com/?enc=eSszL1Slqwil%2BCdLsfyXXw%3D%3D.3nhPuNrxAtNSMysFaqz%2BmYNyciOvWtjB2xyNDCdaJXBC2qQuWg2ZHjGQbIfuQ%2F%2Bq%2BpQ8sCJk%2BP%2BpS%2BauIJzf722dxg%2BAD1iyMqs7WWH4KvdDWFklbikXmH%2F6WuK2LvHudaNy6TPTrIvuSBNQm8qm0Q%3D%3D" rel="nofollow">源码地址</a></p>
<p>而在传统 React 应用中,当状态、属性变化后会先调用 <code>shouldComponentUpdate</code>,该方法会深层对比前后状态和属性是否发生改变,再确定是否更新组件。</p>
<p><code>shouldComponentUpdate</code> 是很消耗性能的。Mobx 通过可观察数据,精确地知道组件是否需要更新,减少了调用 <code>shouldComponentUpdate</code> 这一步。这是 Mobx 性能好的原因之一。</p>
<p>另外需要注意的是 <code>observer</code> 并不是 <code>mobx</code> 的方法,而是 <code>mobx-react</code> 的方法。<code>mobx</code> 和 <code>mobx-react</code> 关系如同 <code>react</code> 与 <code>react-dom</code>。</p>
<h3>action</h3>
<p>在 Mobx 中是可以直接修改可观察数据,来进行更新组件的,但不建议这样做。如果在任何地方都修改可观察数据,将导致页面状态难以管理。</p>
<p>所有对可观察数据地修改,都应该在 <code>action</code> 中进行。</p>
<pre><code class="javascript">const changeName = mobx.action(name => user.name = name)</code></pre>
<p>使用 Mobx 可以将组件状态定义在组件外部,这样,组件逻辑和组件视图便很容易分离,兄弟组件之间的状态也很容易同步。另外,也不再需要手动使用 <code>shouldComponentUpdate</code> 进行性能优化了。</p>
<h2>Mobx 与 Redux 对比</h2>
<p>Mobx 的优势来源于<strong>可变数据</strong>(Mutable Data)和<strong>可观察数据</strong> (Observable Data) 。</p>
<p>Redux 的优势来源于<strong>不可变数据</strong>(Immutable data)。</p>
<p>可观察数据的优势,在前文已经介绍过了。现在再来聊聊可变数据和不可变数据。</p>
<p>顾名思义,可变数据和不可变数据的区别在于,可变数据创建后可以修改,不可变数据创建后不可以修改。</p>
<p>可变数据,可以直接修改,所以操作起来非常简单。这使得使用 mobx 改变状态,变得十分简单。</p>
<p>不可变数据并不一定要用到 Immutable 库。它完全可以是一种约定,只要创建后不修改即可。比如说,Redux 中的 <code>state</code>。每次修改都会重新生成一个 <code>newState</code> ,而不会对原来的值进行改变。所以说 Redux 中的 <code>state</code> 就是不可变数据。</p>
<pre><code class="javascript">reducer(state, action) => newState. </code></pre>
<p>不可变数据的优势在于,它可预测,可回溯。示例代码如下:</p>
<pre><code class="javascript">function foo(bar) {
let data = { key: 'value' };
bar(data);
console.log(data.key); // 猜猜会打印什么?
}</code></pre>
<p>如果是可变数据,<code>data.key</code> 的值可能会在 <code>bar</code> 函数中被改变,所以不能确定会打印什么值。但是如果是不可变数据,那么就可以肯定打印值是什么。这就是不可变数据的优势 ———— 可预测。不可变数据不会随着时间的变化(程序的运行)而发生改变。在需要回溯的时候,直接获取保存的值即可。</p>
<p>Mobx 与 Redux 技术选型的本质,是在可变数据与不可变数据之间选择。具体业务场景的技术选型,还需要根据实际情况进行分析,脱离业务场景讨论技术选型是没有意义的。但我个人在状态管理的技术选型上,还是倾向于 Mobx 的。原因是前端与副作用打交道非常频繁,有 Http 请求的副作用,Dom 操作的副作用等等。使用不可变数据,还必须得使用中间件对副作用封装;在 Redux 中修改一次状态,需要经过 Action、Dispatch、Reducer 三个步骤,代码写起来太啰嗦;而前端的程序以中小型程序为主,纯函数带来的可预测性的收益,远不及其带的代码复杂度所需要付出的成本。而 Mobx 使用起来更加简单,更适合现在以业务驱动、快速迭代的开发节奏。</p>
<h2>展望:Mobx 与不可变数据的融合</h2>
<p>不可变数据和可变数据,都是对状态的一种描述。那么有没有一种方案,能将一种状态,同时用可变数据和不可变数据来描述呢?这样就可以同时享有二者的优势了。(注意:当我们说可变数据时,通常它还是可观察数据,后文统一只说可变数据。)</p>
<p>答案是肯定的,它就是 MST(mobx-state-tree) <a href="https://link.segmentfault.com/?enc=bacYYJP0QbU1kyeb0cK2yA%3D%3D.QgPt2b0hVRvkS3MzpuA0kjdePia7ANtE6t7auYsxdm1wUfcawwvC2XrfMcBz6I6h" rel="nofollow">https://github.com/mobxjs/mob...</a>。</p>
<p>MST 是一个状态容器:一种状态,同时包含了可变数据、不可变数据两种不同的形式。</p>
<p>为了让状态可以在可变数据和不可变数据两种形式之间能够高效地相互转化,必须遵循 MST 定义状态的方法。</p>
<p>在 MST 中,定义状态必须先定义它的结构。状态的结构是一颗树(tree),树是由多层模型(model)组成,model 是由多个节点组成。</p>
<p>在下面的代码中,树只有一层 model,该 model 也只有一个节点:title。title 的类型是事先定好的,在这里是 <code>types.string</code>。树的结构定义好后,通过 <code>create</code> 方法传入数据,就生成树。</p>
<pre><code class="javascript">import {types} from "mobx-state-tree"
// declaring the shape of a node with the type `Todo`
const Todo = types.model({
title: types.string
})
// creating a tree based on the "Todo" type, with initial data:
const coffeeTodo = Todo.create({
title: "Get coffee"
})</code></pre>
<p>在一些稍微复杂的例子中,树的 model 可以有多层,每层可以有多个节点,有些节点定义的是数据类型(<code>types.xxx</code>),有些节点直接定义的是数据。下面的示例中,就是定义了一个多层多节点的树。除此之外,注意 <code>types.model</code> 函数的第一个参数定义的是 model 的名字,第二参数定义的是 model 的所有属性,第三个参数定义的是 action。</p>
<pre><code class="javascript">import { types, onSnapshot } from "mobx-state-tree"
const Todo = types.model("Todo", {
title: types.string,
done: false
}, {
toggle() {
this.done = !this.done
}
})
const Store = types.model("Store", {
todos: types.array(Todo)
})
// create an instance from a snapshot
const store = Store.create({ todos: [{
title: "Get coffee"
}]})</code></pre>
<p>最关键的来了,请看下面的代码。</p>
<pre><code class="javascript">// listen to new snapshots
onSnapshot(store, (snapshot) => {
console.dir(snapshot)
})
// invoke action that modifies the tree
store.todos[0].toggle()
// prints: `{ todos: [{ title: "Get coffee", done: true }]}`</code></pre>
<p>在上述代码的第一部分,使用 <code>onSnapshot</code> 监听状态的改变。第二部分,调用 <code>store.todos[0].toggle()</code> ,在这个 <code>action</code> 中通过使用<strong>可变数据</strong>的方式,直接修改了当前的状态。同时在 <code>onSnapshot</code> 生成了一个状态快照。这个状态快照就是状态的<strong>不可变数据</strong>的表现形式。</p>
<p>MST 这么神奇,那么具体怎么用呢?MST 只是一个状态容器,同时包含了可变数据和不可变数据。你可以用 MST 直接搭配 React 使用。可以 MST + Mobx + React 配合着用,还可以 MST + Redux + React 混搭着用。</p>
<p>MST 比较新,业内的实践非常少,如果不是急需,现在还可以先观望一下。</p>
React Native 的 ListView 性能问题已解决
https://segmentfault.com/a/1190000008589705
2017-03-06T19:50:34+08:00
2017-03-06T19:50:34+08:00
fitfish
https://segmentfault.com/u/timetravel
11
<p>长列表或者无限下拉列表是最常见的应用场景之一。RN 提供的 ListView 组件,在长列表这种数据量大的场景下,性能堪忧。而在最新的 0.43 版本中,提供了 FlatList 组件,或许就是你需要的高性能长列表解决方案。它足以应对大多数的长列表场景。</p>
<h2>测试数据</h2>
<p>FlatList 到底行不行,光说不行,先动手测试一下吧。</p>
<p>性能瓶颈主要体现在 Android 这边,所以就用魅族 MX5 测试机,测试无限下拉列表,列表为常见的左文右图的形式。</p>
<p>测试数据如下:</p>
<table>
<thead><tr>
<th>对比</th>
<th>ListView</th>
<th>FlatList</th>
</tr></thead>
<tbody>
<tr>
<td>1000条时内存</td>
<td>350M</td>
<td>180M</td>
</tr>
<tr>
<td>2000条时内存</td>
<td>/</td>
<td>230M</td>
</tr>
<tr>
<td>js-fps</td>
<td>4~6 fps</td>
<td>8~20 fps</td>
</tr>
</tbody>
</table>
<p><code>js-pfs</code> 类似于游戏的画面渲染的帧率,60 为最高。它用于判断 js 线程的繁忙程度,数值越大说明 js 线程运行状态越好,数值越小说明 js 线程运行状态越差。在快速滑动测试 ListView 的时候, <code>js-pfs</code> 的值一直在 4~6 范围波动,即使停止滑动,<code>js-pfs</code> 的值也不能很快恢复正常。而 FlatList 在快速滚动后停止,<code>js-pfs</code> 能够很快的恢复到正常。</p>
<p>内存方面,ListView 滑动到 1000 条时,已经涨到 350M。这时机器已经卡的不行了,所以没法滑到 2000 条并给出相关数据。而 FlatList 滑到 2000 条时的内存,也比 ListView 1000 条时的内存少不少。说明,FlatList 对内存的控制是很优秀的。</p>
<p>主观体验方面:FlatList 快速滑动至 2000 条的过程中全程体验流畅,没有出现卡顿或肉眼可见的掉帧现象。而ListView 滑动到 200 条开始卡顿,页面滑动变得不顺畅,到 500 条渲染极其缓慢,到 1000 条时已经滑不动了。</p>
<p>通过以上的简单的测试,可以看出,FlatList 已经能够应对简单的无限列表的场景。</p>
<h2>使用方法</h2>
<p>FlatList 有三个核心属性 <code>data</code> <code>renderItem</code> <code>getItemLayout</code>。它继承自 ScrollView 组件,所以拥有 ScrollView 的属性和方法。</p>
<p><strong> data </strong></p>
<p>和 ListView 不同,它没有特殊的 <code>DataSource</code> 数据类型作为传入参数。它接收的仅仅只是一个 <code>Array<object></code> 作为参数。<br>参数数组中的每一项,需要包含 <code>key</code> 值作为唯一标示。数据结构如下:</p>
<pre><code class="javascript">[{title: 'Title Text', key: 'item1'}]</code></pre>
<p><strong> renderItem </strong></p>
<p>和 ListView 的 <code>renderRow</code> 类似,它接收一个函数作为参数,该函数返回一个 ReactElement。函数的第一个参数的 <code>item</code> 是 <code>data</code>属性中的每个列表的数据( <code>Array<object></code> 中的 <code>object</code>) 。这样就将列表元素和数据结合在一起,生成了列表。</p>
<pre><code class="javascript"> _renderItem = ({item}) => (
<TouchableOpacity onPress={() => this._onPress(item)}>
<Text>{item.title}}</Text>
<TouchableOpacity/>
);
...
<FlatList data={[{title: 'Title Text', key: 'item1'}]} renderItem={this._renderItem} /></code></pre>
<p><strong> getItemLayout </strong></p>
<p>可选优化项。但是实际测试中,如果不做该项优化,性能会差很多。所以强烈建议做此项优化!<br>如果不做该项优化,每个列表都需要事先渲染一次,动态地取得其渲染尺寸,然后再真正地渲染到页面中。</p>
<p>如果预先知道列表中的每一项的高度(ITEM_HEIGHT)和其在父组件中的偏移量(offset)和位置(index),就能减少一次渲染。这是很关键的性能优化点。</p>
<pre><code class="javascript"> getItemLayout={(data, index) => (
{length: ITEM_HEIGHT, offset: ITEM_HEIGHT * index, index}
)}</code></pre>
<p>完整代码如下:</p>
<pre><code class="javascript">// 这里使用 getData 获取假数据
// 数据结构类似于 [{title: 'Title Text', key: 'item1'}]
import getData from './getData';
import TopicRow from './TopicRow';
// 引入 FlatList
import FlatList from 'react-native/Libraries/CustomComponents/Lists/FlatList';
export default class Wuba extends Component {
constructor(props) {
super(props);
this.state = {
listData: getData(),
};
}
renderItem({item,index}) {
return <TopicRow {...item} id={item.key} />;
}
render() {
return (
<FlatList
data = {this.state.listData}
renderItem={this.renderItem}
onEndReached={()=>{
// 到达底部,加载更多列表项
this.setState({
listData: this.state.listData.concat(getData())
});
}}
getItemLayout={(data, index) => (
// 120 是被渲染 item 的高度 ITEM_HEIGHT。
{length: 120, offset: 120 * index, index}
)}
/>
)
}
}</code></pre>
<h2>源码分析</h2>
<p>FlatList 之所以节约内存、渲染快,是因为它只将用户看到的(和即将看到的)部分真正渲染出来了。而用户看不到的地方,渲染的只是空白元素。渲染空白元素相比渲染真正的列表元素需要内存和计算量会大大减少,这就是性能好的原因。</p>
<p>FlatList 将页面分为 4 部分。初始化部分/上方空白部分/展现部分/下方空白部分。初始化部分,在每次都会渲染;当用户滚动时,根据需求动态的调整(上下)空白部分的高度,并将视窗中的列表元素正确渲染出来。</p>
<p><img src="/img/bVK9ux?w=628&h=862" alt="图片描述" title="图片描述"></p>
<pre><code class="javascript">_usedIndexForKey = false;
const lastInitialIndex = this.props.initialNumToRender - 1;
const {first, last} = this.state;
// 初始化时的 items (10个) ,被正确渲染出来
this._pushCells(cells, 0, lastInitialIndex);
// first 就是 在视图中(包括要即将在视图)的第一个 item
if (!disableVirtualization && first > lastInitialIndex) {
const initBlock = this._getFrameMetricsApprox(lastInitialIndex);
const firstSpace = this._getFrameMetricsApprox(first).offset -
(initBlock.offset + initBlock.length);
// 从第 11 个 items (除去初始化的 10个 items) 到 first 渲染空白元素
cells.push(
<View key="$lead_spacer" style={{[!horizontal ? 'height' : 'width']: firstSpace}} />
);
}
// last 是最后一个在视图(包括要即将在视图)中的元素。
// 从 first 到 last ,即用户看到的界面渲染真正的 item
this._pushCells(cells, Math.max(lastInitialIndex + 1, first), last);
if (!this._hasWarned.keys && _usedIndexForKey) {
console.warn(
'VirtualizedList: missing keys for items, make sure to specify a key property on each ' +
'item or provide a custom keyExtractor.'
);
this._hasWarned.keys = true;
}
if (!disableVirtualization && last < itemCount - 1) {
const lastFrame = this._getFrameMetricsApprox(last);
const end = this.props.getItemLayout ?
itemCount - 1 :
Math.min(itemCount - 1, this._highestMeasuredFrameIndex);
const endFrame = this._getFrameMetricsApprox(end);
const tailSpacerLength =
(endFrame.offset + endFrame.length) -
(lastFrame.offset + lastFrame.length);
// last 之后的元素,渲染空白
cells.push(
<View key="$tail_spacer" style={{[!horizontal ? 'height' : 'width']: tailSpacerLength}} />
);
}</code></pre>
<p>既然要使用空白元素去代替实际的列表元素,就需要预先知道实际展现元素的高度(或宽度)和相对位置。如果不知道,就需要先渲染出实际展现元素,在获取完展现元素的高度和相对位置后,再用相同(累计)高度空白元素去代替实际的列表元素。<code>_onCellLayout</code> 就是用于动态计算元素高度的方法,如果事先知道元素的高度和位置,就可以使用上面提到的 <code>getItemLayout</code> 方法,就能跳过 <code>_onCellLayout</code> 这一步,获得更好的性能。</p>
<pre><code class="javascript">return (
// _onCellLayout 就是这里的 _onLayout
// 先渲染一次展现元素,通过 onLayout 获取其尺寸等信息
<View onLayout={this._onLayout}>
{element}
</View>
);
...
_onCellLayout = (e, cellKey, index) => {
// 展现元素尺寸等相关计算
const layout = e.nativeEvent.layout;
const next = {
offset: this._selectOffset(layout),
length: this._selectLength(layout),
index,
inLayout: true,
};
const curr = this._frames[cellKey];
if (!curr ||
next.offset !== curr.offset ||
next.length !== curr.length ||
index !== curr.index
) {
this._totalCellLength += next.length - (curr ? curr.length : 0);
this._totalCellsMeasured += (curr ? 0 : 1);
this._averageCellLength = this._totalCellLength / this._totalCellsMeasured;
this._frames[cellKey] = next;
this._highestMeasuredFrameIndex = Math.max(this._highestMeasuredFrameIndex, index);
// 重新渲染一次。最终会调用一次上面分析的源码
this._updateCellsToRenderBatcher.schedule();
}
};</code></pre>
<p>简单分析 FlatList 的源码后,后发现它并没有和 native 端复用逻辑。而且如果有些机器性能极差,渲染过慢,那些假的列表——空白元素就会被用户看到!</p>
<p>那么为什么要基于 RN 的 ScrollView 组件进行性能优化,而不直接使用 Android 或 iOS 提供的列表组件呢?</p>
<p>最简单回答就是:太难了!</p>
<p>由于本人对 RN 底层原理实现只有简单理解。只能引用 Facebook 大神的解释,起一个抛砖引玉的作用。</p>
<p>以 iOS 的 <code>UITableView</code> 为例,所有即将在视窗中呈现的元素都必须同步渲染。这意味着如果渲染过程超过 16ms,就会掉帧。</p>
<blockquote><p>In UITableView, when an element comes on screen, you have to synchronously render it. This means that you've got less than 16ms to do it. If you don't, then you drop one or multiple frames.</p></blockquote>
<p>但是问题是,从 RN render 到真正调用 native 代码这个过程本身是<strong>异步</strong>的,过程中消耗的时间也并不能保证在 16ms 以内。</p>
<blockquote><p>The problem is in the RN render -> shadow -> yoga -> native loop. You have at least three runloop jumps (dispatch_async(dispatch_get_main_queue(), ...) as well as background thread work, which all work against the required goal.</p></blockquote>
<p>那么解决方案就是,在一些需要高性能的场景下,让 RN 能够<strong>同步</strong>的调用 native 代码。这个答案或许就是 ListView 性能问题的终极解决方案。</p>
<blockquote><p>We are actually starting to experiment more and more with synchronous method calls for other modules, which would allow us to build a native list component that could call <code>renderItem</code> on demand and choose whether to make the call synchronously on the UI thread if it's hi-pri (including the JS, react, and yoga/layout calcs), or on a background thread if it's a low-pri pre-render further off-screen. This native list component might also be able to do proper recycling and other optimizations.</p></blockquote>
深入理解 JavaScript 中的 class
https://segmentfault.com/a/1190000008338987
2017-02-13T19:49:53+08:00
2017-02-13T19:49:53+08:00
fitfish
https://segmentfault.com/u/timetravel
25
<p>在 ES6 规范中,引入了 <code>class</code> 的概念。使得 JS 开发者终于告别了,直接使用原型对象模仿面向对象中的类和类继承时代。</p>
<p>但是JS 中并没有一个真正的 <code>class</code> 原始类型, <code>class</code> 仅仅只是对原型对象运用语法糖。所以,只有理解如何使用原型对象实现类和类继承,才能真正地用好 <code>class</code>。</p>
<h2>ES6:<code>class</code>
</h2>
<p>通过类来创建对象,使得开发者不必写重复的代码,以达到代码复用的目的。它基于的逻辑是,两个或多个对象的结构功能类似,可以抽象出一个模板,依照模板复制出多个相似的对象。就像自行车制造商一遍一遍地复用相同的蓝图来制造大量的自行车。</p>
<p>使用 ES6 中的 <code>class</code> 声明一个类,是非常简单的事。它的语法如下:</p>
<pre><code class="js">class Person {
constructor(name){
this.name = name
}
hello(){
console.log('Hello, my name is ' + this.name + '.');
}
}
var xiaoMing = new Person('xiaoMing');
xiaoMing.hello() // Hello, my name is xiaoMing.</code></pre>
<p><code>xiaoMing</code> 是通过类 <code>Person</code> 实例化出来的对象。对象 <code>xiaoMing</code> 是按照类 <code>Person</code> 这个模板,实例化出来的对象。实例化出来的对象拥有类预先订制好的结构和功能。</p>
<p>ES6 的语法很简单,但是在实例化的背后,究竟是什么在起作用呢?</p>
<h2>
<code>class</code> 实例化的背后原理</h2>
<p>使用 <code>class</code> 的语法,让开发者告别了使用 <code>prototype</code> 模仿面向对象的时代。但是,<code>class</code> 并不是 ES6 引入的全新概念,它的原理依旧是原型继承。</p>
<h3>typeof <code>class</code> == "function"</h3>
<p>通过类型判断,我们可以得知,<code>class</code> 的并不是什么全新的数据类型,它实际只是 <code>function</code> (或者说 <code>object</code>)。</p>
<pre><code class="js">class Person {
// ...
}
typeof Person // function</code></pre>
<p>为了更加直观地了解 <code>Person</code> 的实质,可以将它在控制台打印出来,如下。</p>
<p><img src="/img/bVI9uQ?w=618&h=366" alt="图片描述" title="图片描述"></p>
<p><code>Person</code> 的属性并不多,除去用 <code>[[...]]</code> 包起来的内置属性外,大部分属性根据名字就能明白它的作用。需要我们重点关注的是 <code>prototype</code> 和 <code>__proto__</code> 两个属性。</p>
<p>(关于 <code>__proto__</code> 可以在<a href="https://segmentfault.com/a/1190000008293372#articleHeader2">本文的姊妹篇</a> 找到答案)</p>
<h3>实例化的原理: <code>prototype</code>
</h3>
<p>先来讲讲 <code>prototype</code> 属性,它指向一个特殊性对象:<strong>原型对象</strong>。</p>
<p>原型对象所以特殊,是因为它拥有一个普通对象没有的能力:将它的属性共享给其他对象。</p>
<p>在 ES6 规范 中,对 <strong>原型对象</strong> 是如下定义的:</p>
<pre><code>object that provides shared properties for other objects</code></pre>
<p>原型对象是如何将它的属性分享给其他对象的呢?</p>
<p>这里使用 ES5 创建一个类,并将它实例化,来看看它的实质。</p>
<pre><code class="js">function Person() {
this.name = name
}
// 1. 首先给 Person.prototype 原型对象添加了 describe 方法 。
Person.prototype.describe = function(){
console.log('Hello, my name is ' + this.name + '.');
}
// 2. 实例化对象的 __proto__ 指向 Person.prototype
var jane = new Person('jane');
jane.__proto__ === Person.prototype;
// 3. 读取 describe 方法时,实际会沿着原型链查找到 Person.prototype 原型对象上。
jane.describe() // Hello, my name is jane.</code></pre>
<p>上述使用 JS 模仿面向对象实例化的背后,实际有三个步骤:</p>
<ol>
<li><p>首先给 <code>Person.prototype</code> 属性所指的原型对象上添加了一个方法 <code>describe</code>。</p></li>
<li><p>在使用 <code>new</code> 关键字创建对象时,会默认给该对象添加一个原型属性 <code>__proto__</code>,该属性指向 <code>Person.prototype</code> 原型对象。</p></li>
<li><p>在读取 <code>describe</code> 方法时,首先会在 <code>jane</code> 对象查找该方法,但是 <code>jane</code> 对象并不直接拥有 <code>describe</code> 方法。所以会沿着原型链查找到 Person.prototype 原型对象上,最后返回该原型对象的 <code>describe</code> 方法。</p></li>
</ol>
<p>JS 中面向对象实例化的背后原理,实际上就是 <strong>原型对象</strong>。</p>
<p>为了方便大家理解,从网上扒了一张的图片,放到这来便于大家理解。</p>
<p><img src="/img/bVI9us?w=886&h=462" alt="图片描述" title="图片描述"></p>
<h3>
<code>prototype</code> 与 <code>__proto__</code> 区别</h3>
<p>理解上述原理后,还需要注意 <code>prototype</code> 与 <code>__proto__</code> 属性的区别。</p>
<p><code>__proto__</code> 所指的对象,真正将它的属性分享给它所属的对象。所有的对象都有 <code>__proto__</code> 属性,它是一个内置属性,被用于继承。</p>
<p><code>prototype</code> 是一个只属于 <code>function</code> 的属性。当使用 <code>new</code> 方法调用该构造函数的时候,它被用于构建新对象的 <code>__proto__</code>。另外它不可写,不可枚举,不可配置。</p>
<pre><code class="js">( new Foo() ).__proto__ === Foo.prototype
( new Foo() ).prototype === undefined</code></pre>
<h3>
<code>class</code> 定义属性</h3>
<p>当我们使用 <code>class</code> 定义属性(方法)的时候,实际上等于是在 <code>class</code> 的原型对象上定义属性。</p>
<pre><code class="js">class Foo {
constructor(){ /* constructor */ }
describe(){ /* describe */ }
}
// 等价于
function Foo (){
/* constructor */
}
Foo.prototype.describe = function(){ /* describe */ }</code></pre>
<p><code>constructor</code> 是一个比较特殊的属性,它指向构造函数(类)本身。可以通过以下代码验证。</p>
<pre><code class="js">Foo.prototype.constructor === Foo // true</code></pre>
<h2>类继承</h2>
<p>在传统面向对象中,类是可以继承类的。这样子类就可以复制父类的方法,达到代码复用的目的。</p>
<p>ES6 也提供了类继承的语法 <code>extends</code>,如下:</p>
<pre><code class="js">
class Foo {
constructor(who){
this.me = who;
}
identify(){
return "I am " + this.me;
}
}
class Bar extends Foo {
constructor(who){
// super() 指的是调用父类
// 调用的同时,会绑定 this 。
// 如:Foo.call(this, who)
super(who);
}
speak(){
alert( "Hello, " + this.identify() + "." );
}
}
var b1 = new Bar( "b1" );
b1.speak();</code></pre>
<p>当实例 <code>b1</code> 调用 <code>speak</code> 方法时,<code>b1</code> 本身没有 <code>speak</code>,所以会到 <code>Bar.prototype</code> 原型对象上查找,并且调用原型对象上的 <code>speak</code> 方法。调用 <code>identify</code> 方式时,由于 <code>this</code> 指向的是 <code>b1</code> 对象。所以也会先在 <code>b1</code> 本身查找,然后沿着原型链,查找 <code>Bar.prototype</code>,最后在 <code>Foo.prototype</code> 原型对象上找到 <code>identify</code> 方法,然后调用。</p>
<p>实际上,在 JavaScript 中,类继承的本质依旧是原型对象。</p>
<p>他们的关系如下图所示:</p>
<p><img src="/img/bVCzB8?w=659&h=577" alt="图片描述" title="图片描述"></p>
<h2>参考文章</h2>
<ul>
<li><p>(ES6 规范)[<a href="https://link.segmentfault.com/?enc=ZOv3qGL2HT0kx%2FX0R1J4wg%3D%3D.Y5s6fE8rNR2Hv7cBmcstKHtX3%2B3tOFIYjUnusFAy4UEGZPZuFvLDYhyESHRDtTJ9mz9%2BDoreYcz9GmlOI0xpOOwvmY%2FlLFuXiKBAZqBqZxoQDsnO4Seocm7o96bYFS2O" rel="nofollow">http://www.ecma-international...</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=DO1N1hQGwJplHNzkUmhhpg%3D%3D.Sqi5fDoAk9Uak88Ghp4HzVd1BsHW8DBdbRTqlEeCPZWC6CLy24MvAfHeX39sqe%2FP0R2UHLXVMuDxrtBOsY3zpagVVXrziGi7k6ByI0dhEUM%3D" rel="nofollow">MDN Classes</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=ZtknRJo5M3F3OzkuYrjziA%3D%3D.C1srxDoae2gu4JA8VfAxZQYl%2FXWG0APxscM9bKb82%2BkQM4iz2bJ5M3MExrB%2FqWBsvS4SpstcLcTNvPbrOfy%2BYcBK4zGcYeVzzVueeDCC2PS%2BKT0Zf%2BmQBY1L8ARMowK3" rel="nofollow">You-Dont-Know-JS</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=%2B3CkmAMPJTwcaeCaCu3JJw%3D%3D.4x4h21c4BvjuZHaCZpduiJu3xRKIby1xXJyxeXoZ0qYf5QC9DPNwWYB11XYFskQP%2FcD5H0gqZE7CJGleZucbM5BsXNODY5HleNZuOWFn2y8iR2QfmvUd4ImBz7QslI4H" rel="nofollow">JavaScript difference between <strong>proto</strong> and prototype</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=KMkliZlYBIKgyKrFjWdFRw%3D%3D.D4pP27aoP7lHKVLTGVTsnJOYXcsYJBkFA1jzTvhtLGXscavWBqu%2Bj7HBvrlwYRWi8Am%2BH9uYGA6W3SB%2BTB2rcHu1vum9lQiTDylVE%2BQRnXo%3D" rel="nofollow"><strong>proto</strong> VS. prototype in JavaScript</a></p></li>
</ul>
深入理解 JavaScript 原型继承
https://segmentfault.com/a/1190000008293372
2017-02-09T00:11:32+08:00
2017-02-09T00:11:32+08:00
fitfish
https://segmentfault.com/u/timetravel
12
<h2>继承的本质:重用</h2>
<p>在探讨 JavaScript 的原型继承之前,先不妨想想为什么要继承?</p>
<p>考虑一个场景,如果我们有两个对象,它们一部分属性相同,另一部属性不同。通常一个好的设计方案是将相同逻辑抽出来,实现重用。</p>
<p>以 <code>xiaoMing</code> <code>liLei</code> 两位同学举例。这两位同学有自己的名字,并且会介绍自己。抽象为程序对象,可以做如下表示。</p>
<pre><code class="js">var xiaoMing = {
name : "xiaoMing",
hello : function(){
console.log( 'Hello, my name is '+ this.name + '.');
}
}
var liLei = {
name : "liLei",
hello : function(){
console.log( 'Hello, my name is '+ this.name + '.');
}
}</code></pre>
<p>使用过 java 的同学,可能第一眼就想到了用面向对象来解决这个问题。创造一个 Person 的类,然后实例化 <code>xiaoMing</code> 和 <code>liLei</code> 两个对象。在 ES6 中也有类似于 java 中类的概念:<code>class</code>。</p>
<p>下面使用 ES6 的语法,用面向对象的思路来重构上面的代码。</p>
<pre><code class="js">class Person {
constructor(name){
this.name = name
}
hello(){
console.log(this.name);
}
}
var xiaoMing = new Person('xiaoMing');
var liLei = new Person('liLei');</code></pre>
<p>可以看到,使用类创建对象,达到了重用的目的。它基于的逻辑是,两个或多个对象的结构功能类似,可以抽象出一个模板,依照模板复制出多个相似的对象。</p>
<p>使用类创建对象,就像自行车制造商一遍一遍地重用相同的蓝图来制造大量的自行车。</p>
<p>然解决重用问题的方案,当然不止一种。传统面向对象的类,只是其中的一种方案。下面轮到我们的主角“原型继承”登场了,它从另一个角度解决了重用的问题。</p>
<h2>原型继承的原理</h2>
<h3>原型对象</h3>
<p>JavaScript 中的 <code>object</code> 由两部分组成,普通属性的集合,和原型属性。</p>
<pre><code class="js">var o = {
a : 'a',
...
__proto__: prototypeObj
}</code></pre>
<p>普通属性指的就是 <code>a</code>;<strong>原型属性</strong> 指的是 <code>__proto__</code>。这本不属于规范的一部分,后来 chrome 通过 <code>__proto__</code> 将这个语言底层属性给暴露出来了,慢慢的被大家所接受,也就添加到 ES6 规范中了。 <code>o.__proto__</code> 的值 <code>prototypeObj</code> 也就是 <strong>原型对象</strong> 。原型对象其实也就是一个普通对象,之所以叫原型对象的原因,只是因为它是原型属性所指的值。</p>
<p>原型对象所以特殊,是因为它拥有一个普通对象没有的能力:将它的属性共享给其他对象。</p>
<p>在 <a href="https://link.segmentfault.com/?enc=UvhVrYMuZe8VxhXY%2BJgWIg%3D%3D.GQMqlavTEJCqlXPNyTuogJ4b4ZoiEyIcZonRSwoJruPACrVGV6j4YbmKHrhrwz%2F0xTaiPb03%2FEzu2ND0CA0biLgJgM8jlErnhvE42FO%2BKc0pRgAgguixI8dkGIWA5Y5F" rel="nofollow">ES6 规范</a> 中,对它是如下定义的:</p>
<pre><code>object that provides shared properties for other objects</code></pre>
<h3>属性读操作</h3>
<p>回到最开始的例子,看看如何利用原型继承实现重用的目的。</p>
<pre><code class="js">var prototypeObj = {
hello: function(){
console.log( 'Hello, my name is '+ this.name + '.');
}
// ...
}
var xiaoMing = {
name : "xiaoMing",
__proto__ : prototypeObj
}
var liLei = {
name : "liLei",
__proto__ : prototypeObj
}
xiaoMing.hello(); // Hello, my name is xiaoMing.
liLei.hello(); // Hello, my name is liLei.</code></pre>
<p><code>xiaoMing</code> <code>liLei</code> 对象上,并没直接拥有 <code>hello</code> 属性(方法),但是却能读取该属性(执行该方法),这是为什么?</p>
<p>想象一个场景,你在做数学作业,遇到一个很难的题目,你不会做。而你有一个好兄弟,数学很厉害,你去请教他,把这道题做出来了。</p>
<p><code>xiaoMing</code> 对象上,没有 <code>hello</code> 属性,但是它有一个好兄弟,<code>prototypeObj</code>。属性读操作,在 <code>xiaoMing</code> 身上没有找到 <code>hello</code> 属性,就会去问它的兄弟 <code>prototypeObj</code>。所以 <code>hello</code> 方法会被执行。</p>
<h3>原型链</h3>
<p>还是做数学题的例子。你的数学题目很难,你的兄弟也没有答案,他推荐你去问另外一个同学。这样直到有了答案或者再也没有人可以问,你就不会再问下去。这样就好像有一条无形链条把你和同学们牵在了一起。</p>
<p>在 JS 中,读操作通过 <code>__proto__</code> 会一层一层链下去的结构,就叫 <code>原型链</code>。</p>
<pre><code class="js">var deepPrototypeObj = {
hello: function(){
console.log( 'Hello, my name is '+ this.name + '.');
}
__proto__ : null
}
var prototypeObj = {
__proto__ : deepPrototypeObj
}
var xiaoMing = {
name : "xiaoMing",
__proto__ : prototypeObj
}
var liLei = {
name : "liLei",
__proto__ : prototypeObj
}
xiaoMing.hello(); // Hello, my name is xiaoMing.
liLei.hello(); // Hello, my name is liLei.</code></pre>
<h3>原型继承的实现</h3>
<p>在上面的例子中,通过直接修改了 <code>__proto__</code> 属性值,实现了原型继承。但是在实际生产中,<br><a href="https://link.segmentfault.com/?enc=KFCnV5pDzxG6QxkjP%2B7hig%3D%3D.tMxSqSYwAgeIf6Eed7krSNuveJderkZ3SLSnpHjyepIUvAPNrwHdvyxbC4b5JtdHBvIpNhWpkDVX1HHy2c8UlYPrNVrvKYTpi4zUssRmd3QDVSKngZ4SH%2FVVztuyoDXU" rel="nofollow">用这种方式来改变和继承属性是对性能影响非常严重的</a>,所以并不推荐。</p>
<p>代替的方式是使用 <code>Object.create()</code> 方法。</p>
<p>调用 <code>Object.create()</code> 方法会创建一个新对象,同时指定该对象的原型对象为传入的第一个参数。</p>
<p>我们将上面的例子改一下。</p>
<pre><code class="js">var prototypeObj = {
hello: function(){
console.log( 'Hello, my name is '+ this.name + '.');
}
// ...
}
var xiaoMing = Object.create(prototypeObj);
var liLei = Object.create(prototypeObj);
xiaoMing.name = "xiaoMing";
liLei.name = "liLei";
xiaoMing.hello(); // Hello, my name is xiaoMing.
liLei.hello(); // Hello, my name is liLei.</code></pre>
<p><a href="https://link.segmentfault.com/?enc=8R88NvxNA08QabxZsCkpkw%3D%3D.Tsuw3fJm2hHlXNz43kjS0cLt1%2FCc1Y%2B1ogRticoncg%2FAIACSx%2F4WRDXqnEyK4xldu8I0Iz7lyzeOaqrCTuYlPlNocH0KyrC1nJqcuZyGMVQbhVRl%2BfsU6BK0VqipIDZp" rel="nofollow">You-Dont-Know-JS</a> 的作者,对这种原型继承的实现取了一个很好玩的名字 OLOO (objects-linked-to-other-objects) ,这种实现方式的优点是没有使用任何类的概念,只有 <code>object</code>,所以它是很符合 javaScript 的特性的。</p>
<p>因为JS 中本无类,只有 <code>object</code>。</p>
<p>无奈的是,喜欢类的程序员是在太多,所以在 ES6 新增了 <code>class</code> 概念。下一篇会讲 <code>class</code> 在 JS 中的实现原理</p>
<h2>小结</h2>
<p>通过类来创建对象,使得开发者不必写重复的代码,以达到代码重用的目的。它基于的逻辑是,两个或多个对象的结构功能类似,可以抽象出一个模板,依照模板<strong>复制</strong>出多个相似的对象。就像自行车制造商一遍一遍地重用相同的蓝图来制造大量的自行车。</p>
<p>使用原型继承,同样可以达到重用的目的。它基于的逻辑是,两个或多个对象的对象有一部分共用属性,可以将共用的属性抽象到另一个独立公共对象上,通过特殊的原型属性,将公共对象和普通对象链接起来,再利用属性读(写)规则进行遍历查找,实现属性<strong>共享</strong>。</p>
<h2>参考文章</h2>
<ul>
<li><p><a href="https://link.segmentfault.com/?enc=yFn2KqG8Q%2F%2Ft3nNpmCcH6Q%3D%3D.InwgL9DbDv0YjsbsKfgRM%2B1YludDIeb5fAqNBtnObgY%2BgQiW93Yclcqr%2BElcDhZ%2B3jYJi8pIamabC1nKi%2B9nApe0ezmH%2FPz0hg3lQF4uAGRqVktm5WwPd0CMDcYm5hV8" rel="nofollow">ES6 规范</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=w%2BnBZnHI0n%2FD6U%2BIw6dqZg%3D%3D.etlFsdMY3F38wxQyD1%2Bah0LslC4KQlCglruJLE76SqFgys7UP1fOGTxmqm%2FE16dW%2BIXTpYaJaSs1i15qDHW9ARF4GDOlTygVUH6%2FJBRnPdEhJkIpYowTSXZ7o6eVNxV5" rel="nofollow">You-Dont-Know-JS</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=m1bTAVvlrFCvPAufBruD5Q%3D%3D.mXSJy%2B2TRhhCYiKKB6zX%2BIQZkndIHa1a3Pwr2Gux%2F8FJNTNY9kgnrYfAEItg2KFQ3DOJIAl4w73FHZku7GgkHvyWQOBYUDNhEtiFlRhjJ5jPDNkuqM7Y7kqT%2BLZpGCgi" rel="nofollow">MDN Object.create()</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=L8DiJof3tVozd3kZIAyttg%3D%3D.uloD5G6HnH43trVCTzA3rzmduUHBtCY4tPP9nVtjgOFelngGsCkGOIA68kCUw9PbzsCfhFdebRecBOQ0MpY9QA%2FJAKbT8xP9NxnFpgMFibHeIEbF4ox%2Bl7KTiFA%2BWeU7" rel="nofollow">JavaScript difference between <strong>proto</strong> and prototype</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=71MHs8Eo%2FgccZH6YOvummQ%3D%3D.7vOWfYjraMj%2FWmj%2FVNfaV0us2%2BGI6QC81yQnGpIp7esFYwpJWHwCFlkotz2dBopaIratHufzFer4xJGexm2ub7tcnhUcOhun0zV13XdKRM0%3D" rel="nofollow"><strong>proto</strong> VS. prototype in JavaScript</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=WqOQ%2BeE%2F636orfHjGdN89w%3D%3D.UguvcKCfONRDEoquzjiico%2BQRvDE3qziu00nGW4XUza%2B233BwwfWkYmrbbi9uuzgGqkNKfBQZDApo3LFrndahdP5LZYWELbmrGUyXuiKLig%3D" rel="nofollow">JavaScript. The core</a></p></li>
<li><p><a href="https://link.segmentfault.com/?enc=7yXJH0Lm3KpKS266plRNrw%3D%3D.C%2FqOMSqu%2FDIACYT3eTQ1BnMU2DBY%2BWHzL%2FoTmSa%2F9Xj4yEWu6rbzXsEVhw8y9Xb0XCgUBv2BnHz6hgllcGwq%2BPnF7PqgwHY0EN5n1tT6gr0%3D" rel="nofollow">Understanding "Prototypes" in JavaScript</a></p></li>
</ul>
关于 this 你想知道的一切都在这里
https://segmentfault.com/a/1190000008156495
2017-01-18T14:32:36+08:00
2017-01-18T14:32:36+08:00
fitfish
https://segmentfault.com/u/timetravel
3
<p>无论在 <code>javascript</code> 的日常使用中还是前端面试过程中,<code>this</code> 的出镜率都极高。这无疑说明了,<code>this</code> 的重要性。但是 <code>this</code> 非常灵活,导致很多人觉得 <code>this</code> 的行为难以理解。本文从为什么要有 <code>this</code> 作为切入点,总结了 <code>this</code> 的六大规则,希望能帮助你解答困惑。</p>
<h2>简介</h2>
<p>this 实际上相当于一个参数,这个参数可能是开发中手动传入的,也可能是 JS 或者第三方传入的。<br>这个参数,通常指向的是函数执行时的“拥有者”。<code>this</code> 的机制,可以让函数设计的更加简洁,并且复用性更好。</p>
<p><code>this</code> 是在函数执行时进行绑定的,绑定规则一共六条,分别是:</p>
<ul>
<li><p><code>new</code> 绑定:使用 <code>new</code> 关键字创建对象时,<code>this</code> 会绑定到创建的对象上。</p></li>
<li><p>显式绑定:使用 <code>call</code>、<code>apply</code> 或 <code>bind</code> 方法显式绑定时, <code>this</code> 为其第一个参数。</p></li>
<li><p>隐式绑定:当函数挂在对象上执行时,系统会隐式地将 <code>this</code> 绑定到该对象上。</p></li>
<li><p>默认绑定:当函数独立执行时,严格模式 <code>this</code> 的默认绑定值为 <code>undefined</code>,否则为全局对象。</p></li>
<li><p>箭头函数绑定:使用箭头函数时,<code>this</code>的绑定值等于其外层的普通函数(或者全局对象本身)的<code>this</code>。</p></li>
<li><p>系统或第三方绑定:当函数作为参数,传入系统或者第三方提供的接口时,传入函数中的 <code>this</code> 是由系统或者第三方绑定的。</p></li>
</ul>
<h2>
<code>this</code> 的作用</h2>
<p>this 的机制提供了一个优雅的方式,隐式地传递一个对象,这可以让函数设计的更加简洁,并且复用性更好。</p>
<p>考虑下面一个例子,有两个按钮,点击后将其背景改为红色。</p>
<pre><code class="js">function changeBackgroundColor(ele) {
ele.style.backgroundColor = 'red';
}
btn1.addEventListener('click',function () {
changeBackgroundColor(btn1);
});
btn2.addEventListener('click',function () {
changeBackgroundColor(btn2);
});</code></pre>
<p>在这里,我们显式地将被点击的元素传递给了 <code>changeBackgroundColor</code> 函数。但实际上,这里可以利用 <code>this</code> 隐式传递上下文的特点,直接在函数获取当前被点击的元素。如下:</p>
<pre><code class="js">function changeBackgroundColor() {
this.style.backgroundColor = 'red';
}
btn1.addEventListener('click',changeBackgroundColor);
btn2.addEventListener('click',changeBackgroundColor);</code></pre>
<p>在第一个例子中,被点击元素是通过 <code>ele</code> ,这个形式参数来代替的。而在第二个例子中,是通过一个特殊的关键字 <code>this</code> 来代替。<code>this</code> 它的作用和形式参数类似,其本质上是一个对象的引用,它的特殊性在于不需要手动传值,所以使用起来会更加简单和方便。</p>
<h2>六大规则</h2>
<p>在实际使用中, <code>this</code> 究竟指向哪个对象是最令人困惑的。本文归类了六类情景,总结六条 <code>this</code> 的绑定规则。</p>
<h3>
<code>new</code> 绑定</h3>
<p>使用 <code>new</code> 创建对象的时候,类中的 <code>this</code> 指的是什么?</p>
<pre><code class="js">class Person {
constructor(name){
this.name = name;
}
getThis(){
return this
}
}
const xiaoMing = new Person("小明");
console.log(xiaoMing.getThis() === xiaoMing); // true
console.log(xiaoMing.getThis() === Person); // false
console.log(xiaoMing.name === "小明"); // true</code></pre>
<p>在上面例子中,使用了 ES6 的语法创建了 <code>Person</code> 类。在使用 <code>new</code> 关键字创建对象的过程中,<code>this</code> 会由系统自动绑定到创建的对象上,也就是 <code>xiaoMing</code>。</p>
<p>规则一:在使用 <code>new</code> 关键字创建对象时,<code>this</code> 会绑定到创建的对象上。</p>
<h3>显式绑定</h3>
<p>情景二,使用 <code>call</code>、<code>apply</code> 和 <code>bind</code> 方法,显式绑定 <code>this</code> 参数。</p>
<p>以 <code>call</code> 为例,<code>call</code> 方法的第一个传入的参数,是 <code>this</code> 引用的对象。</p>
<pre><code class="js">function foo() {
console.log( this === obj ); // true
console.log( this.a === 2 ); // true
}
const obj = {
a: 2
};
foo.call( obj );</code></pre>
<p>在显式传递的情况下,<code>this</code> 指向的对象很明显,就是 <code>call</code>、<code>apply</code> 或 <code>bind</code> 方法的第一个参数。</p>
<p>规则二:使用 <code>call</code>、<code>apply</code> 或 <code>bind</code> 方法显式绑定时, <code>this</code> 为其第一个参数。</p>
<h3>隐式绑定</h3>
<p>隐式绑定和显式绑定不同的地方在于,显式绑定由开发者来指定 <code>this</code>;而隐式绑定时,函数或方法都会有一个“拥有者”,这个“拥有者”指的是直接调用的函数或方法对象。</p>
<h4>例一</h4>
<p>先看一个最简单的例子。</p>
<pre><code class="js">function bar() {
console.log( this === obj );
}
const obj = {
foo: function () {
console.log( this === obj );
},
bar: bar
};
obj.foo(); // true
obj.bar(); // true</code></pre>
<p>函数 <code>foo</code> 是直接挂在对象 <code>obj</code> 里面的,函数 <code>bar</code> 是在外面定义的,然后挂在对象 <code>obj</code> 上的。无论函数是在何处定义,但最后<strong>函数调用</strong>时,它的“拥有者”是 <code>obj</code>。所以 <code>this</code> 指向的是函数调用时的“拥有者” <code>obj</code>。</p>
<h4>例二</h4>
<p>为了更加深入的理解,再考虑函数重新赋值到新的对象上的情况,来看看下面的例子。</p>
<pre><code class="js">function bar() {
console.log( this === obj1 ); // false
console.log( this === obj2 ); // true
}
const obj1 = {
foo: function () {
console.log( this === obj1 ); // false
console.log( this === obj2 ); // true
},
bar: bar
};
const obj2 = {
foo: obj1.foo,
bar: obj1.bar
};
obj2.foo();
obj2.bar();</code></pre>
<p>在该例子中,将 <code>obj1</code> 中的 <code>foo</code> 和 <code>bar</code> 方法赋值给了 <code>obj2</code>。函数调用时,“拥有者”是 <code>obj2</code>,而不是 <code>obj1</code>。所以 <code>this</code> 指向的是 <code>obj2</code>。</p>
<h4>例三</h4>
<p>对象可以多层嵌套,在这种情况下执行函数,函数的“拥有者”是谁呢?</p>
<pre><code class="js">const obj1 = {
obj2: {
foo: function foo() {
console.log( this === obj1 ); // false
console.log( this === obj1.obj2 ); // true
}
}
};
obj1.obj2.foo()</code></pre>
<p><code>foo</code> 方法/函数中的直接调用者是 <code>obj2</code>,而不是 <code>obj1</code>,所以函数的“拥有者”指向的是离它最近的直接调用者。</p>
<h4>例四</h4>
<p>如果一个方法/函数,在它的直接对象上调用执行,又同时执行了 <code>call</code> 方法,那么它是属于隐式绑定还是显式绑定呢?</p>
<pre><code class="js">const obj1 = {
a: 1,
foo: function () {
console.log(this === obj1); // false
console.log(this === obj2); // true
console.log(this.a === 2); // true
}
};
const obj2 = {
a: 2
};
obj1.foo.call(obj2); // true</code></pre>
<p>由上,可以看出,如果显式绑定存在,它就不可能属于隐式绑定。</p>
<p>规则三:如果函数是挂在对象上执行的,这个时候系统会隐式的将 <code>this</code> 绑定为函数执行时的“拥有者”。</p>
<h3>默认绑定</h3>
<p>前一小段,讨论了函数作为对象的方法执行时的情况。本小段,要讨论的是,函数独立执行的情况。</p>
<p>在函数直接调用的情况下,<code>this</code> 绑定的行为,称之为默认绑定。</p>
<h4>例一</h4>
<p>为了简单起见,先讨论在浏览器的非严格模式的下绑定行为。</p>
<pre><code class="js">function foo() {
console.log( this === window); // true
}
foo();</code></pre>
<p>在上面的例子中,系统将 <code>window</code> 默认地绑定到函数的 <code>this</code> 上。</p>
<h4>例二</h4>
<p>在这里,先介绍一种我们可能会在代码中见到的显式绑定 <code>null</code> 的写法。</p>
<pre><code class="js">function foo() {
console.log( this == window ); // true
}
foo.apply(null);</code></pre>
<p>将例一默认绑定的情况,改为了显式绑定 <code>null</code> 的情况。</p>
<p>在实际开发中,我们可能会用到 <code>apply</code> 方法,并在第一个参数传入 <code>null</code> 值,第二个参数传入数组的方式来传递数组类型的参数。这是一种传统的写法,当然现在可以用 <code>ES6</code> 的写法来代替,但是这不在本文的讨论范围内。</p>
<p>在本例最需要关注的是,<code>this</code> 竟然指向的 <code>window</code> 而不是 <code>null</code>。个人测试的结果是,在函数独立调用时,或者显式调用,传入的值为 <code>null</code> 和 <code>undefined</code> 的情况下,会将 <code>window</code> 默认绑定到 <code>this</code> 上。</p>
<p>在函数多次调用,形成了一个调用栈的情况下,默认绑定的规则也是成立的。</p>
<h4>例三</h4>
<p>接着,探讨下严格模式下,<code>this</code> 的默认绑定的值。</p>
<pre><code class="js">"use strict";
function foo() {
console.log( this === undefined );
}
foo(); // true
foo.call(undefined); // true
foo.call(null); // false</code></pre>
<p>在严格模式下,<code>this</code> 的默认绑定的值为 <code>undefined</code>。</p>
<p>规则四:在函数独立执行的情况下,严格模式 <code>this</code> 的默认绑定值为 <code>undefined</code>,否则默认绑定的值为 <code>window</code>。</p>
<h3>箭头函数绑定</h3>
<p>箭头函数实际上,只是一个语法糖,实际上箭头函数中的 <code>this</code> 实际上是其外层函数(或者 window/global 本身)中的 <code>this</code>。</p>
<pre><code class="js">// ES6
function foo() {
setTimeout(() => {
console.log(this === obj); // true
}, 100);
}
const obj = {
a : 1
}
foo.call(obj);
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log(_this === obj); // true
}, 100);
}
var obj = {
a : 1
}
foo.call(obj);</code></pre>
<p>规则五:使用箭头函数时,<code>this</code> 的绑定值和其外层的普通函数(或者 window/global 本身) <code>this</code> 绑定值相同。</p>
<h3>系统或第三方绑定</h3>
<p>在 JavaScript 中,函数是第一公民,可以将函数以值的方式,传入任何系统或者第三方提供的函数中。现在讨论,最后一种情况。当将函数作为值,传入系统函数或者第三方函数中时,<code>this</code> 究竟是如何绑定的。</p>
<p>我们在文章一开始提到的,两个按钮例子,系统自动将 <code>this</code> 绑定为点击的按钮。</p>
<pre><code class="js">function changeBackgroundColor() {
console.log(this === btn1); // true
}
btn1.addEventListener('click',changeBackgroundColor);</code></pre>
<p>接着测试系统提供的 <code>setTimeout</code> 接口在浏览器和 node 中绑定行为。</p>
<pre><code class="js">// 浏览器
setTimeout(function () {
console.log(this === window); // true
},0)
// node
setTimeout(function () {
console.log(this === global); // false
console.log(this); // Timeout
},0)</code></pre>
<p>很神奇的是,<code>setTimeout</code> 在 node 和浏览器中的绑定行为不一致。如果我们将 node 的中的 <code>this</code> 打印出来,会发现它绑定是一个 <code>Timeout</code> 对象。</p>
<p>如果是第三发提供的接口,情况会更加复杂。因为在其内部,会将什么值绑定到传入的函数的 <code>this</code> 上,事先是不知道的,除非查看文档或者源码。</p>
<p>系统或者第三方,在其内部,可能会使用前面的五种规则一种或多种规则,对传入函数的 <code>this</code> 进行绑定。所以,规则六,实际上一条在由前五条规则上衍生出来的规则。</p>
<p>规则六:调用系统或者第三方提供的接口时,传入函数中的 <code>this</code> 是由系统或者第三方绑定的。</p>
<p>参考文章:</p>
<p><a href="https://link.segmentfault.com/?enc=K4lXJfm2tLv9k5A6omEyyg%3D%3D.s63coQQJFG5gCEV97qT3AD6KIwqF2Vo%2F6SVsypMekCcQB9CGNzdfm3x5PGra%2FYbwYTfzBFKKw2wS%2B%2BVMe0oTgiZ9mLaoYbwQA7g4YAY6pPYIsr81H6%2BhRXxy8kIfYixH" rel="nofollow">You-Dont-Know-JS</a></p>
<p><a href="https://link.segmentfault.com/?enc=hfEecmMfc%2Bmt%2Ba9AXytPIg%3D%3D.yhHBUlUFDtEfpn7Ts3NbYtDJN4R2Qs867qgIu9x4GHplx1ZzNuTmOyS270fY6iyw" rel="nofollow">The this keyword</a></p>
<p><a href="https://link.segmentfault.com/?enc=Lb6hr7zTHXfWs0gXb6cmUA%3D%3D.aQZiXwSuQN0n30Xtdk%2BgUdB3RvjkTckTPlRPnTK8KZX2rrd%2BpSS5lsR5aM%2FmU7On7rCGH0KUlOANGq%2Fq%2F4QuPqcUhK7l7Y7tw0TRRUi9piVRsmKEgEmD1sO%2FBUaVrFNC" rel="nofollow">MDN this</a></p>
<hr>
<h2>后期补充</h2>
<p>查完规范后,用伪代码再总结一下。</p>
<p>规范地址:</p>
<p>Construct:<a href="https://link.segmentfault.com/?enc=IgEHWick6IOJtlMW9T%2FhBw%3D%3D.P09xqqDfn2Sg3JVtFWypY5Sg%2B25jtYZPUmPH%2BmlY8xvgbQalC1i41bDqayEOSIdJ519Qxr0okSOa0fVPImlxGZhXRiGlpg9MgO9dCoRQsWxm3pTc80zaRKpevzrHh3ZUUvHW%2BOnuKmtyKnU7Nqt7kK4%2B3VmxEa9EV2%2FjWrtu2rc%3D" rel="nofollow">http://www.ecma-international...</a><br>Function Objects:<a href="https://link.segmentfault.com/?enc=8hscnpuJMQbisGOXGKTitg%3D%3D.T3mgcEArr98WRG7CS4q7gmm%2BBef7yfVV4UE5a6oUyICobgxFuQjpll%2FnlHZVMt1ueKvxnYuN7TtovCxdoRBaNUxc3hZMGMVw7THPg4kgDrI%3D" rel="nofollow">http://www.ecma-international...</a><br>Function Calls:<a href="https://link.segmentfault.com/?enc=lWGy%2F8%2BFx2NA5OeFeQKP0A%3D%3D.fipWVVxF6NetUfhIakQs3Qge%2B17ceVnh2Pur6Z6QB3vhVOHFST6Te9ZWU%2BZI9kD6KzhFnq2fgN3RWCD3tvD%2Bi8Sz%2Fya%2FWNE5QkCKPBra%2FCW827wS0%2FCt0goxcJo7FMQP" rel="nofollow">http://www.ecma-international...</a><br>ArrowFunction:<a href="https://link.segmentfault.com/?enc=a81XYodCJIqnRDzjaEhAHA%3D%3D.u5JDY2dSEvO3y1GoPZOvq3SQ%2F1REJVtb%2BS1JHtRV%2FYiM3Ts5r9aGY6bgOe9Lbr4wYr4BPWmHr4fq2Ba4xbXVs4rSc7bo92%2BlKAbrjpoDc2UQBcN65sskSwy3kknlKfobYJjqmfgqf0D9M009mCI%2BGQ%3D%3D" rel="nofollow">http://www.ecma-international...</a></p>
<p>伪代码</p>
<pre><code class="javascript">
if (`newObj = new Object()`) {
this = newObj
} else if (`bind/call/apply(thisArgument,...)`) {
if (`use strict`) {
this = thisArgument
} else {
if (thisArgument == null || thisArgument == undefined) {
this = window || global
} else {
this = ToObject(thisArgument)
}
}
} else if (`Function Call`) {
if (`obj.foo()`) {
// base value . Reference = base value + reference name + strict reference
// 例外: super.render(obj). this = childObj ?
this = obj
} else if (`foo()`) {
// 例外: with statement. this = with object
this = `use strict` ? undefined : window || global
}
}</code></pre>
你的 css 也需要模块化
https://segmentfault.com/a/1190000008064468
2017-01-10T00:31:44+08:00
2017-01-10T00:31:44+08:00
fitfish
https://segmentfault.com/u/timetravel
3
<h2>css “局部”样式</h2>
<p>sass、less 通过 <code>@import</code> ,部分解决的 css 模块化的问题。</p>
<p>由于 css 是全局的,在被引入的文件和当前文件出现重名的情况下,前者样式就会被后者覆盖。<br>在引入一些公用组件,或者多人协作开发同一页面的时候,就需要考虑样式会不会被覆盖,这很麻烦。</p>
<pre><code>// file A
.name {
color: red
}
// file B
@import "A.scss";
.name {
color: green
}</code></pre>
<p>css 全局样式的特点,导致 css 难以维护,所以需要一种 css “局部”样式的解决方案。<br>也就是彻底的 css 模块化,<code>@import</code> 进来的 css 模块,需要隐藏自己的内部作用域。</p>
<h2>CSS Modules 原理</h2>
<p>通过在每个 class 名后带一个独一无二 hash 值,这样就不有存在全局命名冲突的问题了。这样就相当于伪造了“局部”样式。</p>
<pre><code>// 原始样式 styles.css
.title {
color: red;
}
// 原始模板 demo.html
import styles from 'styles.css';
<h1 class={styles.title}>
Hello World
</h1>
// 编译后的 styles.css
.title_3zyde {
color: red;
}
// 编译后的 demo.html
<h1 class="title_3zyde">
Hello World
</h1></code></pre>
<h2>webpack 与 CSS Modules</h2>
<p>webpack 自带的 <code>css-loader</code> 组件,自带了 CSS Modules,通过简单的配置即可使用。</p>
<pre><code>{
test: /\.css$/,
loader: "css?modules&localIdentName=[name]__[local]--[hash:base64:5]"
}</code></pre>
<p>命名规范是从 BEM 扩展而来。</p>
<ul>
<li><p>Block: 对应模块名 <code>[name]</code></p></li>
<li><p>Element: 对应节点名 <code>[local]</code></p></li>
<li><p>Modifier: 对应节点状态 <code>[hash:base64:5]</code></p></li>
</ul>
<p>使用 __ 和 -- 是为了区块内单词的分割节点区分开来。<br>最终 class 名为 <code>styles__title--3zyde</code>。</p>
<h2>在生产环境中使用</h2>
<p>在实际生产中,结合 sass 使用会更加便利。以下是结合 sass 使用的 webpack 的配置文件。</p>
<pre><code>{
test: /\.scss$/,
loader: "style!css?modules&importLoaders=1&localIdentName=[name]__[local]--[hash:base64:5]!sass?sourceMap=true&sourceMapContents=true"
}</code></pre>
<p>通常除了局部样式,还需要全局样式,比如 base.css 等基础文件。<br>将公用样式文件和组件样式文件分别放入到两个不同的目标下。如下。</p>
<pre><code>.
├── app
│ ├── styles # 公用样式
│ │ ├── app.scss
│ │ └── base.scss
│ │
│ └── components # 组件
├── Component.jsx # 组件模板
└── Component.scss # 组件样式</code></pre>
<p>然后通过 webpack 配置,将在 <code>app/styles</code> 文件夹的外的(exclude) scss 文件"局部"化。</p>
<pre><code>{
test: /\.scss$/,
exclude: path.resolve(__dirname, 'app/styles'),
loader: "style!css?modules&importLoaders=1&localIdentName=[name]__[local]--[hash:base64:5]!sass?sourceMap=true&sourceMapContents=true"
},
{
test: /\.scss$/,
include: path.resolve(__dirname, 'app/styles'),
loader: "style!css?sass?sourceMap=true&sourceMapContents=true"
}</code></pre>
<p>有时候,一个元素有多个 class 名,可以通过 <code>join(" ")</code> 或字符串模版的方式来给元素添加多个 class 名。</p>
<pre><code>// join-react.jsx
<h1 className={[styles.title,styles.bold].join(" ")}>
Hello World
</h1>
// stringTemp-react.jsx
<h1 className={`${styles.title} ${styles.bold}`}>
Hello World
</h1></code></pre>
<p>如果只写一个 class 就能把样式定义好,那么最好把所有样式写在一个 class 中。<br>所以,如果我们使用了多个 class 定义样式,通常会带一些一些逻辑判断。这个时候写起来就会麻烦不少。</p>
<p>引入 <a href="https://link.segmentfault.com/?enc=tPh1s55o0oN%2F5Irfd8dWcQ%3D%3D.yik%2FPMtt3FK4Gu1LFJiqNRnFNoaEtTD1i%2B4WNJuKQM%2Fq0S9umkiJGQdO7ypNeBBR" rel="nofollow">classnames</a> ,即可以解决给元素写多个 class 名的问题,也可以解决写逻辑判断的麻烦问题。</p>
<pre><code>classNames('foo', 'bar'); // => 'foo bar'
classNames('foo', { bar: true }); // => 'foo bar'
classNames({ 'foo-bar': true }); // => 'foo-bar'
classNames({ 'foo-bar': false }); // => ''
classNames({ foo: true }, { bar: true }); // => 'foo bar'
classNames({ foo: true, bar: true }); // => 'foo bar'
// lots of arguments of various types
classNames('foo', { bar: true, duck: false }, 'baz', { quux: true }); // => 'foo bar baz quux'
// other falsy values are just ignored
classNames(null, false, 'bar', undefined, 0, 1, { baz: null }, ''); // => 'bar 1'</code></pre>
<p>引入 CSS Modules 的样式模块,每个 class 每次都要写 <code>styles.xxx</code> 也是很麻烦,在《深入React技术栈》提到了 <code>react-css-modules</code> 的库,来减少代码的书写,感兴趣的同学可以研究下。</p>
<p>参考资料:<br>《深入React技术栈》<br><a href="https://link.segmentfault.com/?enc=B24oOPIdoVoQT682FtHUrw%3D%3D.XjYCaqbIXGsfJft3tfVg7IJKMgt6LftHF534c1R%2Btkx4wDcQJKXFyImtNaRIUQYF" rel="nofollow">css-modules</a><br><a href="https://link.segmentfault.com/?enc=bJKDIQGKSPUZPHkvrCBXYw%3D%3D.mS%2BYiCxUL5j%2BoXtti2Ni4fdN4Vb1IumWGDsfVvFqzvxFXhV29MCnZE4PszQPWWjkHy5EFCHdEG1PzB2pj6hJKQ%3D%3D" rel="nofollow">CSS Modules 用法教程</a></p>
mobx——rudex的简单替代品
https://segmentfault.com/a/1190000007938992
2016-12-27T22:29:43+08:00
2016-12-27T22:29:43+08:00
fitfish
https://segmentfault.com/u/timetravel
23
<h2>mobx 能干什么</h2>
<p>使用 react 写小型应用,数据、业务逻辑和视图的模块划分不是很细是没有问题的。在这个阶段,引入任何状态管理库,都算是奢侈的。但是随着页面逻辑的复杂度提升,在中大型应用中,数据、业务逻辑和视图,如果不能很好的划分,就很有可能出现维护难、性能低下的问题。</p>
<p>业内比较成熟的解决方案有 redux,但是 redux 使用过程中,给我的感觉是太复杂和繁琐。那么为什么不简单一点呢?mobx 的核心理念是 简单、可扩展的状态管理库。这可能正是你想要的。</p>
<p>react 关注的状态(state)到视图(view)的问题。而 mobx 关注的是状态仓库(store)到的状态(state)的问题。</p>
<h2>核心的概念1</h2>
<p>mobx 最最核心的概念只有2个。 <code>@observable</code> 和 <code>@observer</code> ,它们分别对应的是被观察者和观察者。这是大家常见的观察者模式,不过这里使用了,ES7 中的 <a href="https://link.segmentfault.com/?enc=n1E0d7XzQSfWeIabYMLe%2Fg%3D%3D.7ir26NX27OnCH1eFhcRp9oY5J%2FLbzLg8AI7x%2B%2FF4IKUiOAtPYkJWFRR7fwYxTlKS" rel="nofollow">装饰器</a>。</p>
<p>使用 <code>@observable</code> 可以观察类的值。</p>
<p>这里使用 <code>@observable</code> 将 Store 的 <code>todos</code> 变为一个被观察的值。</p>
<h3>observable</h3>
<p><strong>仓库</strong></p>
<pre><code class="javascript">// 这里引入的是 mobx
import {observable} from 'mobx';
class Store {
@observable todos = [{
title: "todo标题",
done: false,
}];
}</code></pre>
<h3>observer</h3>
<p><strong> mobx 组件</strong></p>
<p>然后再使用 <code>@observer</code> ,将组件变为观察者,响应 <code>todos</code> 状态变化。<br>当状态变化时,组件也会做相应的更新。</p>
<pre><code class="javascript">// 这里引入的是 mobx-react
import {observer} from 'mobx-react';
@observer
class TodoBox extends Component {
render() {
return (
<ul>
{this.props.store.todos.map(todo => <li>{todo.title}</li>)}
</ul>
)
}
}</code></pre>
<p>完整的 demo 如下。</p>
<pre><code class="javascript">import React, {Component} from 'react';
import { render } from 'react-dom';
import {observable} from 'mobx';
import {observer} from 'mobx-react';
// 最简单的 mobx 就是一个观察者模式
class Store {
// 被观察者
@observable todos = [{
title: "完成 Mobx 翻译",
done: false,
}];
}
// 观察者
@observer
class TodoBox extends Component {
render() {
return (
<ul>
{this.props.store.todos.map(todo => <li>{todo.title}</li>)}
</ul>
)
}
}
const store = new Store();
render(
<TodoBox store={store} />,
document.getElementById('root')
);</code></pre>
<p>通过以上的简单的例子,展现了 mobx 分离数据、视图的能力。</p>
<h2>核心概念2</h2>
<p>这一小节要介绍的两个概念虽然也是核心概念,但是是可选的。</p>
<p>前面例子,只讲了状态的读取,那么状态应该如何写入呢?</p>
<p>答案是直接写入!</p>
<pre><code class="javascript">@observer
class TodoBox extends Component {
render() {
return (
<div>
<ul>
{this.props.store.todos.map(
(todo,index) => <li key={index}>{todo.title}</li>
)}
</ul>
<div>
<input type="button" onClick={() => {
// 直接修改仓库中的状态值
this.props.store.todos[0].title = "修改后的todo标题"
}} value="点我"/>
</div>
</div>
)
}
}</code></pre>
<p>细心的朋友一定发现了奇怪的地方,react 官方说过 <code>props</code> 值不能直接修改,但是引入 mobx 后 <code>props</code> 可以直接修改了,这太奇怪了!</p>
<p>解决办法就是 mobx 的下一个概念 <code>action</code>。</p>
<h3>actions</h3>
<p>首先在 Store 中,定义一个 action。</p>
<pre><code class="javascript">class Store {
@observable todos = [{
title: "todo标题",
done: false,
}];
@action changeTodoTitle({index,title}){
this.todos[index].title = title
}
}</code></pre>
<p>在 Component 中调用,这样通过 action 的方法,就避免了直接修改 <code>props</code> 的问题。</p>
<pre><code class="js"><input type="button" onClick={() => {
this.props.store.changeTodoTitle({index:0,title:"修改后的todo标题"});
}} value="点我"/></code></pre>
<p>可以通过引入 mobx 定义的严格模式,强制使用 action 来修改状态。</p>
<pre><code class="javascript">import {useStrict} from 'mobx';
useStrict(true);</code></pre>
<h3>computed values</h3>
<p>在有些时候,state 并不一定是我们需要的最终数据。例如,所有的 todo 都放在 store.todos 中,而已经完成的 todos 的值(store.unfinishedTodos),可以由 store.todos 衍生而来。</p>
<p>对此,mobx 提供了 <code>computed</code> 装饰器,用于获取由基础 state 衍生出来的值。如果基础值没有变,获取衍生值时就会走缓存,这样就不会引起虚拟 DOM 的重新渲染。</p>
<p>通过 <code>@computed</code> + <code>getter</code> 函数来定义衍生值(computed values)。</p>
<pre><code class="javascript">import { computed } from 'mobx';
class Store {
@observable todos = [{
title: "todo标题",
done: false,
},{
title: "已经完成 todo 的标题",
done: true,
}];
@action changeTodoTitle({index,title}){
this.todos[index].title = title
}
@computed get finishedTodos () {
return this.todos.filter((todo) => todo.done)
}
}</code></pre>
<p>mobx 有一套机制,如果衍生值(computed values)所依赖的基础状态(state)没有发生改变,获取衍生值时,不会重新计算,而是走的缓存。因此 mobx 不会引起过度渲染,从而保障了性能。</p>
<p>当渲染的值为 finishedTodos ,点击修改标题,不会在控制台打印 "render";<br>换成 todos,就会打印 "render".<br>这是由于已完成的 todos 值没有改变,所以不会重新计算,而是走的缓存。因此不会调用 render 方法。</p>
<p>完整 demo 如下</p>
<pre><code class="javascript">import React, {Component} from 'react';
import { render } from 'react-dom';
import {observable, action, computed,useStrict} from 'mobx';
import {observer} from 'mobx-react';
useStrict(true);
class Store {
@observable todos = [{
title: "todo标题",
done: false,
},{
title: "已经完成 todo 的标题",
done: true,
}];
@action changeTodoTitle({index,title}){
this.todos[index].title = title
}
@computed get unfinishedTodos () {
return this.todos.filter((todo) => todo.done)
}
}
@observer
class TodoBox extends Component {
render() {
console.log('render');
return (
<div>
<ul>
{ /* 把 unfinishedTodos 换成 todos,点击修改标题就会在控制台打印 "render".*/ }
{this.props.store.unfinishedTodos.map(
(todo,index) => <li key={index}>{todo.title}</li>
)}
</ul>
<div>
<input type="button" onClick={() => {
this.props.store.changeTodoTitle({index:0,title:"修改后的todo标题"});
}} value="修改标题"/>
</div>
</div>
)
}
}
const store = new Store();
render(
<TodoBox store={store} />,
document.getElementById('root')
);
</code></pre>
<h2>小结</h2>
<p>翻译了官网的一段文章,就拿过来做小结了。</p>
<p>mobx 是一个的简单、可扩展的状态管理库。它背后的哲学非常简单:</p>
<pre><code>应用程序 state 是最基础的数据。任何可以从 state 中衍生出来的数据,都应该自动的被衍生出。</code></pre>
<p><img src="http://oi9t94i28.bkt.clouddn.com/mobx.png" alt="img" title="img"></p>
<p><strong>actions</strong> 是唯一能够改变 state 的方法。</p>
<p><strong>state</strong> 是最基础的数据,它不应该包含冗余的和派生的数据。</p>
<p><strong>computed values</strong> 派生值是通过纯函数从 state 中派生而来的。当派生值依赖的状态发生变化了,Mobx 将会自动更新派生值。如果依赖的状态没有改变,mobx 会做优化处理。</p>
<p><strong>reactions</strong> 也是派生数据,是从 state 中派生而来的。它的副作用是自动更新 UI。(注:mobx 有一个 reaction 接口,当 state 改变时,就会调用它的回调。UI 是通过 reaction 更新的。)</p>
<p>React 和 MobX 是非常强大的组合。React 提供了将应用状态映射为可渲染的组件树的机制。MobX 提供存储和更新应用状态的机制,供 React 使用。</p>
<p>React 和 MobX 提供了开发过程中常见问题的解决方案。 React 通过使用虚拟 DOM,减少了对浏览器 DOM 的操作。MobX 通过使用了响应式虚拟依赖状态图(reactive virtual dependency state graph) ,提供了应用程序状态与 React 组件同步的机制,这样 state 只会在需要时更新才会更新。(译者注:这段有点难理解,大概的意思是 Mobx 关注的是 store 到 state 的过程,React 关注的是 state 到 view 的过程)。</p>
<h2>辅助函数</h2>
<p>在实际开发中,需要用到不少 mobx 的辅助函数,这些辅助函数一共 14 个,挑了一些列举如下。</p>
<p><strong>autorun</strong><br>observable 的值初始化或改变时,自动运行。</p>
<p><strong>trasaction</strong><br>批量改变时,通过 trasaction 包装,只会触发一次 autorun。</p>
<p><strong>extendsObservable</strong><br>对类的属性或实例,进行监听。</p>
<p><strong>observable</strong><br>对普通对象进行监听。</p>
<p><strong>map</strong><br>使用 asMap 将对象转化为 map。</p>
<p><strong>action-strict</strong><br>在 mobx.usrStrict(true)时,只能通过 action 触发值的改变。</p>
<p><strong>when</strong><br>类似 autorun.</p>
<p>mobx.when 第一个参数是一个函数,初始化时也会自动执行。该函数返回一个 boolean 值,当返回值为 true 的时候,才会继续触发第一个函数。当返回值为 flase 时,不再继续监听。这时会执行 mobx.when 的第二个参数,这个参数也是一个函数。</p>
<p><strong>reaction</strong><br>类似 autorun.</p>
<p>reaction 不会在初始化时执行,只会在值改变的时候执行。</p>
<p>该函数有 2 个值,第一个参数是一个函数,返回监听的值.<br>第二个参数,也是一个函数,会在值改变的时候执行。</p>
<p><strong>spy</strong><br>类似 aoturun.</p>
<p>监听所有 mobx 的事件。</p>
<p>包含一个 type ,该值用来区分执行事件的类型。</p>
<p><strong>whyRun</strong><br>用于调试,打印 autorun 为什么会触发。</p>
React 核心思想之声明式渲染
https://segmentfault.com/a/1190000007463108
2016-11-12T18:18:01+08:00
2016-11-12T18:18:01+08:00
fitfish
https://segmentfault.com/u/timetravel
9
<p>React 发展很快,概念也多,本文目的在于帮助初学者理清 React 核心概念。</p>
<p>React 及 React 生态</p>
<p><img src="http://www.ruanyifeng.com/blogimg/asset/2016/bg2016092301.png" alt="" title=""></p>
<p>React 的核心概念只有 2 点:</p>
<ul>
<li><p>声明式渲染(Declarative)</p></li>
<li><p>基于组件(Component-Based)</p></li>
</ul>
<h2>声明式渲染</h2>
<h3>声明式与命令式</h3>
<ul>
<li><p>命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。</p></li>
<li><p>声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。</p></li>
</ul>
<p><img src="http://note.youdao.com/yws/public/resource/7a41fe2b3bf3998ade89bf3cdbfe8b55/WEBRESOURCE9d1a28a088c1183ed2f96c850f39272e" alt="" title=""></p>
<p>举例:</p>
<pre><code class="javascript">// 命令式关注如何做(how)
var numbers = [1,2,3,4,5]
var doubled = []
for(var i = 0; i < numbers.length; i++) {
var newNumber = numbers[i] * 2
doubled.push(newNumber)
}
console.log(doubled) //=> [2,4,6,8,10]</code></pre>
<p>遍历整个数组,取出每个元素,乘以二,然后把翻倍后的值放入新数组,每次都要操作这个双倍数组,直到计算完所有元素。</p>
<pre><code class="javascript">// 声明式关注做什么(what)
var numbers = [1,2,3,4,5]
var doubled = numbers.map(function(n) {
return n * 2
})
console.log(doubled) //=> [2,4,6,8,10]</code></pre>
<p>map 函数所作的事情是将直接遍历整个数组的过程归纳抽离出来,让我们专注于描述我们想要的是什么(what)。</p>
<h3>模板渲染</h3>
<p>渲染:模板 => HTML => 页面视图</p>
<p>发生在服务器的叫后端模板渲染,公司用的是<code>velocity</code>。</p>
<p>发生在客户端的叫前端模板渲染,常用的有 <a href="https://link.segmentfault.com/?enc=u9qp64kKZ%2ByJ5j9oeqtDPQ%3D%3D.Wakoo3firOOXIO7zMGoOQexZ1uOeggi3l8JmQCQN02pZC1wc6qrzGELTetSaU3X7" rel="nofollow">artTemplate</a>。</p>
<p>以 <code>artTemplate</code> 为例。</p>
<ul><li><p>模板</p></li></ul>
<pre><code class="HTML"><script id="test" type="text/html">
<div>
<h2>北京时间: {{ date.toLocaleTimeString() }}.</h2>
</div>
</script></code></pre>
<ul>
<li><p>数据</p></li>
<li><p>渲染</p></li>
</ul>
<pre><code class="javascript">setInterval(function() {
// 数据
var data = {
date: new Date()
};
// 渲染(将数据和模板绑定在)
var html = template('test', data);
// 渲染
document.getElementById('container').innerHTML = html;
},100)</code></pre>
<h3>React 声明式渲染</h3>
<p>和普通模板不同的是,React 模板写在 JS 文件中,而不是 html 的 <code><script></code> 标签中。能使用所有 JS 语法,而不只有模板语法,所以更加灵活。</p>
<pre><code class="javascript">function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
// 数据
const user = {
firstName: 'Harper',
lastName: 'Perez'
};
// 模板
const element = (
<h1>
Hello, {formatName(user)}!
</h1>
);
// 渲染
ReactDOM.render(
element,
document.getElementById('root')
);</code></pre>
<p>React 可局部渲染,且只渲染改变了的数据。纯模板只能整体渲染。</p>
<p>高效的局部渲染意味着,开发者 只需要维护可变的数据 <strong>state</strong> (what) ,让 react 框架帮助我们处理 DOM 操作(what)。</p>
<pre><code class="javascript">// React.createClass 创建模板容器(类)
class Clock extends Component {
render() {
return (
<div>
<h2>北京时间: { this.props.date.toLocaleTimeString() }</h2>
</div>
);
}
}
setInterval(function() {
// ReactDOM.render 渲染指令
ReactDOM.render(
// date 数据
<Clock date={new Date()} />,
document.getElementById('container')
);
}, 100);</code></pre>
<p>state 只用于存放可变的数据。</p>
<p>通过 setState 告诉 react 什么数据变了,React 会自动更新数据改变部分的视图</p>
<pre><code class="javascript">class Clock extends Component {
// 初始化
constructor(props) {
super(props);
// state 只用于存放可变的状态
this.state = {date: new Date()};
}
// 初始化完成后执行
componentDidMount() {
setInterval(() => {
// setState 在修改 state 参数后会自动调用 render 方法。
this.setState({
date: new Date()
})
},100)
}
render() {
return <h2>北京时间: { this.state.date.toLocaleTimeString() }</h2>
}
}
ReactDOM.render(
<Clock />,
document.getElementById('js-main')
);</code></pre>
<p><img src="http://image.slidesharecdn.com/reactredux-160310130354/95/react-redux-4-638.jpg?cb=1457615084" alt="" title=""></p>
<p>React 通过 diffing 算法计算如何更新视图。而 diffing 算法有个 的假设前提,开发人员会提供给长列表的每个子项一个 ID,帮助算法进行对比。</p>
<pre><code class="js">function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
<li key={number.toString()}>
{number}
</li>
);
return (
<ul>{listItems}</ul>
);
}
const numbers = [1, 2, 3, 4, 5];
ReactDOM.render(
<NumberList numbers={numbers} />,
document.getElementById('root')
);</code></pre>
<h3>完成的渲染流程</h3>
<p>初始化的渲染流程分为 3 步。</p>
<p>第一步,开发者使用 JSX 语法写 React,<code>babel</code> 会将 JSX 编译为浏览器能识别的 React JS 语法。这一步,一般配合 <code>webpack</code> 在本地进行。</p>
<p>第二步,执行 <code>ReactDOM.render</code> 函数,渲染出虚拟DOM。</p>
<p>第三步,react 将虚拟DOM,渲染成真实的DOM。</p>
<p>页面更新的流程同样也是 3 步。</p>
<p>第一步,当页面需要更新时,通过声明式的方法,调用 <code>setState</code> 告诉 react。</p>
<p>第二步,react 自动调用组件的 render 方法,渲染出虚拟 DOM。</p>
<p>第三步,react 会通过 <code>diffing</code> 算法,对比当前虚拟 DOM 和需要更新的虚拟 DOM 有什么区别。然后重新渲染区别部分的真实 DOM。</p>
<p><img src="http://note.youdao.com/yws/public/resource/7a41fe2b3bf3998ade89bf3cdbfe8b55/WEBRESOURCE5ffab2274cfce841fd64e08a6fc34789" alt="" title=""></p>
递归
https://segmentfault.com/a/1190000007462862
2016-11-12T17:44:02+08:00
2016-11-12T17:44:02+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<h2>递归概念</h2>
<p>递归是一种针对简单循环难以编程实现的问题,通过函数调用自身,提供优雅解决方案的技术。</p>
<p>递归都具有以下三个要点:</p>
<ul>
<li><p>使用 if-else 或 switch 语句来引导不同的情况。</p></li>
<li><p>拥有基础情况(base case)或终止条件(stopping condition)来停止递归。</p></li>
<li><p>每次递归调用都会简化原始问题,让它不断接近基础情况,所以可以用不同的参数调用自身方法,直到它变为这种基础情况。这个称之为递归调用(recursive call)。</p></li>
</ul>
<p>示例,计算阶乘</p>
<pre><code class="javascript">if(n == 0){ // base case
return 0;
}else { // recursive call
return n * factorial(n-1)
}</code></pre>
<h2>递归的优势--斐波那契数列</h2>
<p>计算阶乘很容易使用循环改写,某些情况下,用循环不容易解决的问题可以利用递归给出一个直观简单的解法。</p>
<p>斐波那契数列从 0 到 1 开始,之后的每个数都是序列中的前两个数之和,通过递归可以简单的实现出来。</p>
<pre><code class="javascript">var count = 0;
function fib(n) {
count++;
if(n == 0){
return 0;
}else if(n == 1){
return 1;
}else {
return fib(n-1) + fib(n-2);
}
}
const result = fib(10);
console.log('result',result);
console.log('count',count);
// result 55
// count 177</code></pre>
<p>程序中会出现很多重复调用,求第 10 个斐波那契数,就调用了 177 次自身函数,如果尝试求出更大的斐波那契数,那么相应的调用次数就会急剧的增加。</p>
<h3>优化递归调用</h3>
<p>将计算过的斐波那契值存起来,可以优化递归调用。通过改良,求第 10 个斐波那契数,只调用的 11 次自身函数。而且调用自身函数的次数永远是,n + 1 次,n 代表第 n 个需求的斐波那契数。</p>
<pre><code class="javascript">var count = 0;
const calculated = [];
function fib(n) {
count++;
if(n == 0){
return 0;
}else if(n == 1){
return 1;
}else {
if(!calculated[n-1]){
calculated[n-1] = fib(n-1);
}
if(!calculated[n-2]){
calculated[n-2] = fib(n-2);
}
return calculated[n-1] + calculated[n-2];
}
}
const result = fib(10);
console.log('result',result);
console.log('count',count);
// result 55
// count 11</code></pre>
<h3>递归辅助方法</h3>
<p>有时候可以通过找到一个要解决的初始问题的类似问题,来找到初始问题的解决方案。这个类似的方法称之为递归辅助方法。</p>
<p>举例,如果一个字符串从左读和从右读都是一样的,那么他就是一个回文串(palindrome)。可以通过下面的函数判断。</p>
<pre><code class="javascript">function palindrome(str) {
if(str.length <= 1 ){
return true;
}else if(str[0] !== str[str.length - 1]){
return false;
}else {
return palindrome(str.slice(1,-1));
}
}
const result = palindrome("dddddd");
console.log(result); // ture</code></pre>
<p>每次调用 palindrome 方法时,都会使用 str.slice 来创建一个新的字符串。<br>为了避免重新创建字符串,使用递归辅助方法 isPalindrome 来进行改良。</p>
<pre><code class="javascript">function isPalindrome(str) {
return palindrome(str,0,str.length-1);
}
function palindrome(str,low,high) {
if(low >= high){
return true;
}else if(str[low] !== str[high]){
return false;
}else {
low ++;
high --;
return palindrome(str,low,high);
}
}
const result = isPalindrome("dddaaaerddd");
console.log(result); // false</code></pre>
JavaScript 异步进化史
https://segmentfault.com/a/1190000006138882
2016-08-01T21:45:07+08:00
2016-08-01T21:45:07+08:00
fitfish
https://segmentfault.com/u/timetravel
15
<h2>同步与异步</h2>
<p>通常,代码是由上往下依次执行的。如果有多个任务,就必需排队,前一个任务完成,后一个任务才会执行。这种执行模式称之为: <strong>同步(synchronous)</strong> 。新手容易把计算机用语中的同步,和日常用语中的同步弄混淆。如,“把文件同步到云端”中的同步,指的是“使...保持一致”。而在计算机中,同步指的是任务从上往下依次执行的模式。比如:</p>
<p><strong> 例 1 </strong></p>
<pre><code class="javascript">A();
B();
C();</code></pre>
<p>在上述代码中,A、B、C 是三个不同的函数,每个函数都是一个不相关的任务。在同步模式下,计算机会先执行 A 任务,再执行 B 任务,最后执行 C 任务。在大部分情况,同步模式都没问题。但是如果 B 任务是一个耗时很长网络的请求,而 C 任务恰好是展现新页面,B 与 C 没有依赖关系。这就会导致网页卡顿的现象。有一种解决方案,将 B 放在 C 后面去执行,但唯一有些不足的是,B 的网络请求会迟一些再发送。</p>
<p>还有另一种更完美解决方案,将 B 任务分成的两个部分。一部分是,立即执行网络请求的任务;另一部分是,在请求数据回来后执行的任务。这种一部分在立即执行,另一部分在未来执行的模式称为 <strong>异步(asynchronous)</strong> 。伪代码如下:</p>
<p><strong> 例 2 </strong></p>
<pre><code class="javascript">A();
// 在现在发送请求
ajax('url1',function B() {
// 在未来某个时刻执行
})
C();
// 执行顺序 A => C => B</code></pre>
<p>实际上,JavaScript 引擎先执行了调用了浏览器的网络请求接口的任务(一部分任务),再由浏览器发送网络请求并监听请求返回(这个任务不由 JavaScript 引擎执行,而是浏览器);等请求放回后,浏览器再通知 JavaScript 引擎,开始执行回调函数中的任务(另一部分)。JavaScript 异步能力的本质是浏览器或 Node 的多线程能力。</p>
<h2>callback</h2>
<p>未来执行的函数通常也叫 callback。使用 callback 的异步模式,解决了阻塞的问题,但是也带了一些其他问题。在最开始,我们的函数是从上往下书写的,也是从上往下执行的,这非常符合我们的思维习惯,但是现在却被 callback 打断了!在上面一段代码中,它跳过 B 任务,先执行了 C任务!这种异步“非线性”的代码会比同步“线性”的代码,更难阅读,因此也更容易滋生 BUG。</p>
<p>试着判断下面这段代码的执行顺序,你会对“非线性”代码比“线性”代码更难以阅读,体会更深。</p>
<p><strong> 例 3 </strong></p>
<pre><code class="javascript">A();
ajax('url1', function(){
B();
ajax('url2', function(){
C();
}
D();
});
E();
// 下面是答案,你猜对了吗?
// A => E => B => D => C</code></pre>
<p>在例 3 中,我们的阅读代码视线是 <code>A => B => C => D => E</code> ,但是执行顺序却是 <code>A => E => B => D => C</code> 。从上往下执行的顺序被 Callback 打乱了,这就是非线性代码带来的糟糕之处。</p>
<p>上面的例子中,我们可以通过将 <code>ajax</code> 后面执行的任务 <code>E</code> 和 任务 <code>D</code> 提前,来进行代码优化。这种技巧在写多重嵌套的代码时,是非常有用的。改进后,如下。</p>
<p><strong> 例 4 </strong></p>
<pre><code class="javascript">A();
E();
ajax('url1', function(){
B();
D();
ajax('url2', function(){
C();
}
});
// 稍作优化,代码更容易看懂
// A => E => B => D => C</code></pre>
<p>在例 4 中,只有处理了成功回调,并没处理异常回调。接下来,把异常处理回调加上,再来讨论代码“线性”执行的问题。</p>
<p><strong> 例 5 </strong></p>
<pre><code class="javascript">A();
ajax('url1', function(){
B();
ajax('url2', function(){
C();
},function(){
D();
});
},function(){
E();
});</code></pre>
<p>例 5 中,加上异常处理回调后,<code>url1</code> 的成功回调函数 B 和异常回调函数 E,被分开了。这种“非线性”的情况又出现了。</p>
<p>在 node 中,为了解决的异常处理“非线性”的问题,制定了错误优先的策略。node 中 callback 的第一个参数,专门用于判断是否发生异常。</p>
<p><strong> 例 6 </strong></p>
<pre><code class="javascript">A();
get('url1', function(error){
if(error){
E();
}else {
B();
get('url2', function(error){
if(error){
D();
}else{
C();
}
});
}
});</code></pre>
<p>到此,callback 引起的“非线性”问题基本得到解决。遗憾的是,一旦嵌套层数多起来,阅读起来还不是很方便。此外,callback 一旦出现异常,只能在当前回调内部处理异常,并没有一个整体的异常触底方案。</p>
<h2>promise</h2>
<p>在 JavaScript 的异步进化史中,涌现出一系列解决 callback 弊端的库,而 Promise 成为了最终的胜者,并成功地被引入了 ES6 中。它将提供了一个更好的“线性”书写方式,并解决了异步异常只能在当前回调中捕获的问题。</p>
<p>Promise 就像一个中介,它承诺会将一个可信任的异步结果返回。签订协议的两方分别是异步接口和 callback。首先 Promise 和异步接口签订一个协议,成功时,调用 <code>resolve</code> 函数通知 Promise,异常时,调用 <code>reject</code> 通知 Promise。另一方面 Promise 和 callback 也签订一个协议,当异步接口的 <code>resolve</code> 或 <code>reject</code> 被调用时,由 Promise 返回可信任的值给 <code>then</code> 和 <code>catch</code> 中注册的 callback。</p>
<p>一个最简单的 promise 示例如下:</p>
<p><strong> 例 7 </strong></p>
<pre><code class="javascript">// 创建一个 Promise 实例(异步接口和 Promise 签订协议)
var promise = new Promise(function (resolve,reject) {
ajax('url',resolve,reject);
});
// 调用实例的 then catch 方法 (成功回调、异常回调与 Promise 签订协议)
promise.then(function(value) {
// success
}).catch(function (error) {
// error
})</code></pre>
<p>Promise 是个非常不错的中介,它只返回可信的信息给 callback。怎么理解可信的概念呢?准确的讲,就是 callback 一定会被<strong>异步调用</strong>,且<strong>只会调用一次</strong>。比如在使用第三方库的时候,由于某些原因,(假的)“异步”接口不可靠,它执行了同步代码,而没有进入异步逻辑,如例 8。</p>
<p><strong> 例 8 </strong></p>
<pre><code class="javascript">var promise1 = new Promise(function (resolve) {
// 由于某些原因导致“异步”接口,被同步执行了
if (true ){
// 同步代码
resolve('B');
} else {
// 异步代码
setTimeout(function(){
resolve('B');
},0)
}
});
// promise依旧会异步执行
promise1.then(function(value){
console.log(value)
});
console.log('A');
// A => B (先 A 后 B)</code></pre>
<p>再比如,由于某些原因,异步接口不可靠,<code>resolve</code> 或 <code>reject</code> 被执行了两次。但 Promise 只会通知 callback ,第一次异步接口返回的结果。如例 9:</p>
<p><strong> 例 9 </strong></p>
<pre><code class="javascript">
var promise2 = new Promise(function (resolve) {
// resolve 被执行了 2 次
setTimeout(function(){
resolve("第一次");
},0)
setTimeout(function(){
resolve("第二次");
},0)
});
// 但 callback 只会被调用一次,
promise2.then(function(msg){
console.log(msg) // "第一次"
console.log('A')
});
// A (只有一个)</code></pre>
<p>介绍完 Promise 的特性后,来看看它如何利用链式调用,解决 callback 模式下,异步代码可读性的问题。链式调用指的是:函数 <code>return</code> 一个可以继续执行的对象,该对象可以继续调用,并且 <code>return</code> 另一个可以继续执行的对象,如此反复达到不断调用的结果。如例 10:</p>
<p><strong> 例 10 </strong></p>
<pre><code class="javascript">// return 一个可以继续执行的 Promise 对象
var fetch = function(url){
return new Promise(function (resolve,reject) {
ajax(url,resolve,reject);
});
}
A();
fetch('url1').then(function(){
B();
// 返回一个新的 Promise 实例
return fetch('url2');
}).catch(function(){
C();
// 异常的时候也可以返回一个新的 Promise 实例
return fetch('url2');
// 使用链式写法调用这个新的 Promise 实例的 then 方法
}).then(function() {
// 可以继续 return,也可以不继续 return,结束链式调用
D();
})
// A B C D (顺序执行)</code></pre>
<p>如此反复,不断返回一个 Promise 对象,使 Promise 摆脱了 callback 层层嵌套的问题和异步代码“非线性”执行的问题。</p>
<p>另外,Promise 还解决了一个难点,callback 只能捕获当前错误异常。Promise 和 callback 不同,每个 callback 只能知道自己的报错情况,但 Promise 代理着所有的 callback,所有 callback 的报错,都可以由 Promise 统一处理。所以,可以通过在最后设置一个 <code>catch</code> 来捕获之前未捕获异常。</p>
<p>Promise 解决 callback 的异步调用问题,但 Promise 并没有摆脱 callback,它只是将 callback 放到一个可以信任的中间机构,这个中间机构去链接 callback 和异步接口。此外,链式调用的写法并不是非常优雅。接下来介绍的异步(async)函数方案,会给出一个更好的解决方案。</p>
<h2>异步(async)函数</h2>
<p>异步(async)函数是 ES7 的一个新的特性,它结合了 Promise,让我们摆脱 callback 的束缚,直接用“同步”方式,写异步函数。注意,这里的同步指的是写法同步,但实际依旧是异步执行的。</p>
<p>声明异步函数,只需在普通函数前添加一个关键字 <code>async</code> 即可,如:</p>
<p><code>async function main(){}</code></p>
<p>在异步函数中,可以使用 <code>await</code> 关键字,表示等待后面表达式的执行结果,再往下继续执行。表达式一般都是 Promise 实例。如,例 11:</p>
<p><strong> 例 11 </strong></p>
<pre><code class="javascript">var timer = function (delay) {
return new Promise(function create(resolve,reject) {
if(typeof delay !== 'number'){
reject(new Error('type error'));
}
setTimeout(resolve,delay,'done');
});
}
async function main{
var value = await timer(100);
// 不会立刻执行,等待 100ms 后才开始执行
console.log(value); // done
}
main();</code></pre>
<p>异步函数和普通函数的调用方式一样,最先执行 <code>main()</code> 函数。之后,会立即执行 <code>timer(100)</code> 函数。等到( <code>await</code> )后面的 promise 函数( <code>timer(100)</code> )返回结果后,程序才会执行下一行代码。</p>
<p>异步函数和普通函数写法基本类似,除了前面提到的声明方式类似和调用方式一样之外,它也可以使用 <code>try...catch</code> 来捕捉异常,也可以传入参数。但在异步函数中使用 <code>return</code> 是没有作用的,这和普通的 callback 函数 <code>return</code> 没有作用是一样原因。callback 或者异步函数是单独放在 JavaScript 栈(stack)中执行的,这时同步代码已经执行完毕。</p>
<p>在异步函数中,使用 <code>try...catch</code> 异常捕获的方案,代替了 Promise <code>catch</code> 的异常捕获的方案。示例如下:</p>
<p><strong> 例 12 </strong></p>
<pre><code class="javascript">async function main(delay){
try{
// timer 在例 11 中有过声明
var value1 = await timer(delay);
var value2 = await timer('');
var value3 = await timer(delay);
}catch(err){
console.error(err);
// Error: type error
// at create (<anonymous>:5:14)
// at timer (<anonymous>:3:10)
// at A (<anonymous>:12:10)
}
}
main(0);</code></pre>
<p>更神奇的是,异步函数也遵循,“函数是第一公民”的准则。也可以当作值,传入普通函数和异步函数中执行。需要注意的是,在异步函数中使异步函数用时要使用 <code>await</code>,不然异步函会被同步执行。例子如下:</p>
<p><strong> 例 12 </strong></p>
<pre><code class="javascript">async function doAsync(delay){
// timer 在例 11 中有过声明
var value1 = await timer(delay);
console.log('A')
}
async function main(main){
doAsync(0);
console.log('B')
}
main(main);
// B A</code></pre>
<p>这个时候打印出来的值是 <code>B A</code>。说明 <code>doAsync</code> 函数中的 <code>await timer(delay)</code> 并被同步执行了。如果要正确异步地执行 <code>doAsync</code> 函数,需要该函数之前添加 <code>await</code> 关键字,如下:</p>
<pre><code class="javascript">async function main(delay){
var value1 = await timer(delay);
console.log('A')
}
async function doAsync(main){
await main(0);
console.log('B')
}
doAsync(main);
// A B</code></pre>
<p>由于异步函数采用类同步的书写方法,所以在处理多个并发请求,新手可能会像下面一样书写:</p>
<p><strong> 例 13 </strong></p>
<pre><code class="javascript">var fetch = function (url) {
return new Promise(function (resolve,reject) {
ajax(url,resolve,reject);
});
}
async function main(){
try{
var value1 = await fetch('url1');
var value2 = await fetch('url2');
conosle.log(value1,value2);
}catch(err){
console.error(err)
}
}
main();</code></pre>
<p>但这样会导致 <code>url2</code> 的请求必需等到 <code>url1</code> 的请求回来后才会发送。如果 <code>url1</code> 与 <code>url2</code> 没有相互的依赖关系,将这两个请求同时发送实现的效果会更好。</p>
<p><code>Promise.all</code> 的方法,可以很好的处理并发请求。<code>Promise.all</code> 接受将多个 Promise 实例为参数,并将这些参数包装成一个新的 Promise 实例。这样,<code>Promise.all</code> 中所有的请求会第一时间发送出去;在所有的请求成功回来后才会触发 <code>Promise.all</code> 的 <code>resolve</code> 函数;当有一个请求失败,则立即调用 <code>Promise.all</code> 的 <code>reject</code> 函数。</p>
<pre><code class="javascript">var fetch = function (url) {
return new Promise(function (resolve, reject) {
ajax(url, resolve, reject);
});
}
async function main(){
try{
var arrValue = await Promise.all[fetch('url1'),fetch('url2')];
conosle.log(arrValue[0], arrValue[1]);
}catch(err){
console.error(err)
}
}
main();</code></pre>
<p>最后对异步函数的内容做个小结:</p>
<ul>
<li><p>声明: <code>async function main(){}</code></p></li>
<li><p>异步函数逻辑:可以使用 <code>await</code></p></li>
<li><p>调用: <code>main()</code></p></li>
<li><p>捕获异常: <code>try...catch</code></p></li>
<li><p>传入参数: <code>main('第一个参数')</code></p></li>
<li><p>return:不生效</p></li>
<li><p>异步函数作为参数传入其他函数:可以</p></li>
<li><p>处理并发逻辑:<code>Promise.all</code></p></li>
</ul>
<p>目前使用最新的 Chrome/node 已经支持 ES7 异步函数的写法了,另外也可以通过 Babel 以将异步函数转义为 ES5 的语法执行。大家可以自己动手试试,使用异步函数,用类同步的方式,书写异步代码。</p>
RN 0.26 引用方式中哪些属于React,哪些属于React Native
https://segmentfault.com/a/1190000005073313
2016-05-06T16:33:27+08:00
2016-05-06T16:33:27+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<p>以前引用方式,在0.26+版本将会报错<br>import React, { Component, View } from 'react-native';</p>
<p>现在<br>import React, { Component } from 'react';<br>import { View } from 'react-native';</p>
<p>英文原文如下</p>
<p>-- React Package Changes --<br>In React 0.14 for Web we started splitting up the React package into two packages <code>react</code> and <code>react-dom</code>. Now I'd like to make this consistent in React Native. The new package structure would be...<br>"react":<br>Children<br>Component<br>PropTypes<br>createElement<br>cloneElement<br>isValidElement<br>createClass<br>createFactory<br>createMixin<br>"react-native":<br>hasReactNativeInitialized<br>findNodeHandle<br>render<br>unmountComponentAtNode<br>unmountComponentAtNodeAndRemoveContainer<br>unstable_batchedUpdates<br>View<br>Text<br>ListView<br>...<br>and all the other native components.<br>So for a lot of components you actually have to import both packages.<br>var React = require('react');<br>var { View } = require('react-native');<br>var Foo = React.createClass({<br>render() { return <View />; }<br>});<br>However, for components that doesn't know anything about their rendering environment just need the <code>react</code> package as a dependency.<br>Currently a lot of these are accessible from both packages but we'd start issuing warnings if you use the wrong one.<br>This would be a little spammy so ideally we would have a simple codemod script that you can run on your imports to clean them up.<br>E.g. something that translates existing patterns like:<br>var React = require('react-native');<br>var { View } = React;<br>into:<br>var React = require('react');<br>var { View } = require('react-native');<br>If anyone wants to write and share that script with the community, that would be highly appreciated. We can start promoting it right now before we deprecate it.</p>
React Native 的默认单位和自适应布局方案
https://segmentfault.com/a/1190000004878644
2016-04-06T14:11:35+08:00
2016-04-06T14:11:35+08:00
fitfish
https://segmentfault.com/u/timetravel
6
<h2>默认单位 dp</h2>
<p>设置默认宽高的时,是不需要带单位的,如下所示:</p>
<pre><code><View style={{width:100,height:100}}></View>
</code></pre>
<p>那么,布局的默认单位是什么?在官方文档中有一段关于布局单位的描述。</p>
<pre><code>
static getPixelSizeForLayoutSize(layoutSize: number)
Converts a layout size (dp) to pixel size (px).
Guaranteed to return an integer number.
</code></pre>
<p><code>getPixelSizeForLayoutSize</code> 方式,是用于把默认以 <code>dp</code> 单位长度,转化为对应的 <code>px</code> 数值。那么很明显, 默认的布局单位是 <code>dp</code>。</p>
<h2>1dp = 1(css)px</h2>
<p><code>dp</code> 到底是个什么样单位?</p>
<p>关于 dp 官网给了我一个定义:</p>
<pre><code>A dp is equal to one physical pixel on a screen with a density of 160.To calculate dp:
dp = (width in pixels * 160) / screen density
</code></pre>
<p>很明显, <code>dp</code> 与 <code>物理px</code> 有一个关于 <code>(160/screen density)</code> 的正相关的关系:</p>
<pre><code>1dp = 1物理px (screen density = 160)
1dp = 2物理px (screen density = 320)
1dp = 3物理px (screen density = 480)
</code></pre>
<p>同理在 H5 页面,以下等式是成立的。</p>
<pre><code>1 (css)px = 1物理px (device pixel ratio = 1)
1 (css)px = 2物理px (device pixel ratio = 2)
1 (css)px = 3物理px (device pixel ratio = 3)
</code></pre>
<p>而实际上 <code>(160/screen density)</code> 就是 <code>pixelRatio</code>,也就是就是写 H5 页面时,像素比率 <code>window.devicePixelRatio</code>。 </p>
<p>也就是说,1dp = 1(css)px。</p>
<h2>屏幕的单位和概念对比</h2>
<p>在 Android 中,<code>screen density</code> 等于 <code>DPI</code>,表示像素密度。</p>
<p>在 google 布局单位文档中,关于 <code>screen density</code> 有过这样的描述,</p>
<p><img src="/img/bVuDiH" alt="图片描述" title="图片描述"></p>
<p><code>dpi</code> 有过这样的描述<br><img src="/img/bVuDiz" alt="图片描述" title="图片描述"></p>
<p>其中 240/160 两列,像素比同为1.5,可证明 <code>screen density</code> 和 <code>dpi</code>概念一样,只是换了个表达方式。</p>
<p>下面给出一些常见屏幕概念的对比表格</p>
<p><img src="/img/bVuDiR" alt="图片描述" title="图片描述"></p>
<p><code>dp</code> 与 <code>px</code> 的关系为:</p>
<pre><code>1dp = 1(css)px = 1px * pixelRatio
</code></pre>
<h2>自适应布局方案</h2>
<p>自适应布局方案采用了,将 UI 等比放大到 app 上的自适应布局。</p>
<p>UI 给默认 640 的图,采用 pxToDp 函数,即可将 UI 等比放大到 Android 机器上。</p>
<pre><code>import {Dimensions} from 'react-native';
// 58 app 只有竖屏模式,所以可以只获取一次 width
const deviceWidthDp = Dimensions.get('window').width;
// UI 默认给图是 640
const uiWidthPx = 640;
function pxToDp(uiElementPx) {
return uiElementPx * deviceWidthDp / uiWidthPx;
}
export default pxToDp;
</code></pre>
<p>调用方法</p>
<pre><code>import pxToDp from './pxToDp';
...
<View style={{width:pxToDp(100),height:pxToDp(100)}}></View>
...
</code></pre>
<p>如果 UI 图默认不是 640 宽,可以通过 PS 设置为 640 宽。</p>
<p><img src="/img/bVuDjh" alt="图片描述" title="图片描述"></p>
<p>参考链接: <br>RN官网 <a href="https://link.segmentfault.com/?enc=hy16bbW5k9J%2FxZl9YXJOjw%3D%3D.n9yb5JnnsztV89fAYJnZny4%2FOvjFpj1I%2FZkOmiEgtE%2Fh8zbQgtaK8A8rSsE9kY7A1dYUMfyB4t5T4Odcm%2BpoZq59og4zmbFoif17InXcBOU%3D" rel="nofollow">http://facebook.github.io/react-native/docs/pixelratio.html#content</a><br>px、dp、sp对比 <a href="https://link.segmentfault.com/?enc=20QKSZfXIfFaus16QtCrTA%3D%3D.hlU2M9eptBXsOoISP7cSmj3BU%2Bc7lmji3%2FmOizAAShz2kJV7W3%2BMCO6XnQLkb%2FlVmYgGJH%2BA9BLm8PhWcy%2Bo7t2fyUxOgQd%2FEE%2BcvswFbGUSaI1ycVbuo2luWTG%2BRgZUu9nioItZ419BhLd2jC0aTw%3D%3D" rel="nofollow">http://stackoverflow.com/questions/2025282/difference-between-px-dp-dip-and-sp-on-android/2025541#2025541</a><br>Android 布局方案(Google)<a href="https://link.segmentfault.com/?enc=3ToEoPuRoOvvmiMgLB0F3Q%3D%3D.gTZUQeFIH8gNleBr%2FQqJ5qjSKOJGzJgwBnQN2TOIGWVcPc0IJHdT5IxjWP3rdzhOl8Gg6k38qIhfeqC3A5%2BP91la0YLsvH2kCn%2FukgpX4kejrd8%2BE1ecLwJvnLuMqOcmUR3NHv3DnZ2nGXtN4grm6NoCMRuyucxNqYUZYEjvcfc%3D" rel="nofollow">https://www.google.com/design/spec/layout/units-measurements.html#units-measurements-density-independent-pixels-dp-</a><br>Android 布局方案(赵凯强)<a href="https://link.segmentfault.com/?enc=MCGlgycuyD69mABf4Ecz9g%3D%3D.FhKtpfd%2FJaSrTQiU5nIp6VpnEP0WxdoTZQPQakVqtPpWDqDNUVvj1Tgw8bB1VL9U4y2AaeeYpSLBmgKTh5Z3dg%3D%3D" rel="nofollow">http://blog.csdn.net/zhaokaiqiang1992/article/details/45419023</a></p>
React Native 入门
https://segmentfault.com/a/1190000004702101
2016-03-28T11:19:25+08:00
2016-03-28T11:19:25+08:00
fitfish
https://segmentfault.com/u/timetravel
0
<h2>理念</h2>
<h3>组件(components)</h3>
<p>工程师希望能像搭积木s 一样开发和维护系统,通过组装模块得到一个完整的系统。<br>在 RN 中,就是通过把 html、css 和 JS 放在一起维护,变成一个可以组合的单元,来搭建网页。</p>
<h3>数据-视图 state - render</h3>
<p>数据变化后,对应的视图部分就会变化。</p>
<h2>语法</h2>
<h3>ES6 vs ES5</h3>
<p>RN 官方文档的教程默认用的是 ES6 语法(但组件和API这块还夹杂着大量的ES5语法)。另外,RN 项目在本地 node_modules 有个 bable 的包,可以把 ES6 转换为 ES5,所以不用担心新语法不能被现有浏览器编译。</p>
<p>先简单介绍下默认项目中使用的几个 ES6 语法点,但不展开。</p>
<p><strong>let 和 const 与 val 类似,都是用来声明变量的。</strong><br><strong>箭头函数</strong></p>
<pre><code>(x) => 2*x
// 相当于
function(x){return 2*x}
</code></pre>
<p><strong>Class 类</strong></p>
<pre><code>//定义类
class Title {
// 构造函数
constructor(text) {
this.text= text;
}
// 原型链方法
render() {
return (
<header>this.text</header>
)
}
}
// ES5
function Title (text){
this.text= text;
}
Point.prototype.render= function () {
return '<header>'+this.text+'</header>';
}
</code></pre>
<p><strong>继承 用 extends 继承类</strong></p>
<pre><code>class SubHeader extends Header {}
</code></pre>
<p><strong>模块引用</strong></p>
<pre><code>// ES6
import { Component, StyleSheet, View } from 'react-native';
// 等同于
let _rn= require('react-native');
let Component= _rn.Component, StyleSheet= _rn.StyleSheet, View = _rn.View;
// 项目开始时,先引入默认模块和其他模块
import React, {
AppRegistry,
Component,
Image,
ListView,
StyleSheet,
Text,
View,
} from 'react-native';
</code></pre>
<h3>JSX vs HTML(模板) & CSS & JS</h3>
<p>JSX 是一种混合 HTML、CSS 和 JS 的语法,所以在 JSX 中忘了结构、样式、行为分离吧。在 React 中只有模块,JSX这种语法也是为组件服务的。</p>
<h4>最简单的组件</h4>
<p>render 方法用来渲染组件。每个组件由首先由最基本的 HTML 结构 或其他组件组成。<br>View 就是一个 RN 封装好的组件,它对应着 div,UIView,android.view,用于页面布局。 <br>Text 也是一个 RN 封装好的组件,它类似着 span 之类的标签,里面装的是文字。RN 是没有匿名文本节点的,所有文字必须装在 Text 中间。</p>
<pre><code>class TitleList extends Component{
render() {
return (
<View>
<Text>React Native</Text>
</View>
);
}
}
</code></pre>
<p>我们只要将数据 TitleList 输出到虚拟机的 app(MyProject)中,即可看到写好的文本。</p>
<pre><code>// 将 TitleList 注入到 app 中
AppRegistry.registerComponent('MyProject', () => TitleList);
</code></pre>
<h4>我们可以用变量代替文字。</h4>
<pre><code>let title = 'React Native';
class TitleList extends Component{
render() {
return (
<View>
<Text>{title}</Text>
</View>
);
}
}
</code></pre>
<h4>给组件加点样式</h4>
<p>直接在组件中写 style={} ,在中间使用对象的写法书写样式即可。</p>
<pre><code>let title = 'React Native';
class TitleList extends React.Component{
render() {
return (
<View style={{flexDirection: 'row', height: 100, padding: 20}}>
<Text>React Native</Text>
</View>
);
}
}
</code></pre>
<h4>当然也可用直接给个在 style 中 class 名</h4>
<pre><code>let title = 'React Native';
const styles = StyleSheet.create({
title : {
flexDirection: 'row',
height: 100,
padding: 20,
}
});
class TitleList extends React.Component{
render() {
return (
<View style={styles.title }>
<Text>React Native</Text>
</View>
);
}
}
</code></pre>
<h4>拼装组件</h4>
<p>有了单个 title 碎片后,希望把它拼装成一个真正的 list。我们需要引入一个原生组件 ListView ,并把定义的 title 组件和真实的数据拼装到 ListView 组件中去。</p>
<pre><code>// 首先将 title 碎片 拿出来
renderTitle (titles) {
return (
<View style={styles.title}>
<Text >{titles.description}</Text>
</View>
);
}
// 引入一个原生组件 ListView
// 用 renderRow 属性引用 renderTitle 组件。
// 再将用 dataSource 属性引用数据,这里用 this.state.dataSource 表示数据,后续会对其初始化
render() {
return (
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderTitle}
/>
);
}
</code></pre>
<p><strong>组件的数据流</strong></p>
<p>然后用 constructor 对 this.state.dataSource 初始化</p>
<pre><code> constructor (props) {
// 继承父类
super(props);
// 实例化一个 ListView.DataSource 对象。并且只修改改变数据,这可以保证只渲染改动的地方。
// RN 的 state 属性对应的数据变了,那么组件就会被重新渲染。只修改局部数据,那么直有组件的局部被重新渲染。
let ListDate = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
// 初始化 ListView数据,注意 state 一般用于可能被事件触发并改变视图的数据。
this.state = {
dataSource: ListDate.cloneWithRows([
{ description: 'RN1' },
{ description: 'RN2' },
{ description: 'RN2' },
{ description: 'RN2' },
{ description: 'RN2' },
])
};
}
</code></pre>
<h4>完整的组件</h4>
<pre><code>class TitleList extends React.Component{
// 初始化
constructor (props) {
// 不知道干嘛用,不加会报错。
super(props);
// 实例化一个 ListView.DataSource 对象。并且只修改改变数据,这可以保证只渲染改动的地方。
// RN 的 state 属性对应的数据变了,那么组件就会被重新渲染。只修改局部数据,那么直有组件的局部被重新渲染。
let ListDate = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2});
// 初始化 ListView 数据
this.state = {
dataSource: ListDate.cloneWithRows([
{ description: 'RN1' },
{ description: 'RN2' },
])
};
}
// 渲染
render() {
// ListView 是个原生组件
// dataSource 属性声明的是组件的数据
// renderRow 将 renderTitle 按排渲染
return (
<ListView
dataSource={this.state.dataSource}
renderRow={this.renderTitle}
/>
);
}
// title 碎片
renderTitle(titles){
return (
// styles 样式
<View style={styles.title}>
// dataSource 传来的数据
<Text >{titles.description}</Text>
</View>
);
}
}
</code></pre>
<p>工具<br>启动 命令行<br>编辑器 webstrom 支持 JSX<br>调试 chrome</p>
<p>参考文献<br>React 入门实例教程(阮一峰) <a href="https://link.segmentfault.com/?enc=9BLkbzRJmGV4SNyC%2B%2BgGVA%3D%3D.y%2Bm294wtQqRUw52dxed2oq%2Fco%2Bm52z%2FUFYgzel3STY6VZfjJj7w%2BB4rU%2Fa9LaQnb" rel="nofollow">http://www.ruanyifeng.com/blog/2015/03/react</a><br>ECMAScript 6 入门(阮一峰) <a href="https://link.segmentfault.com/?enc=fHCDwHCjp9zzthk3EvFDKw%3D%3D.cl6gafhpdJSTmK3bDKetMjCa%2BRs9uulYA4i%2BOcsWpQE%3D" rel="nofollow">http://es6.ruanyifeng.com/</a><br>es5-es6写法对照表(天地之灵) <a href="https://link.segmentfault.com/?enc=BzFlJ3AWXazIRjRBKvX9MQ%3D%3D.4GmdxxEBV7agXYxm3aAGX0YfUPhJqXRnWHwaffbhkAF5Vhs1f2fTkHHdQ19aSRJU6W64xcxpwGYT6PzQnedsyN%2FcBNcQz4jFh9KjYXYRHYcXmuIqcrxRB9k4hG3p8Kqm3yOuyX5OEhK8oVYetgZiAr1TeWoBY66vaU5e8%2FQPCwg%3D" rel="nofollow">http://bbs.reactnative.cn/topic/15/react-react-native-%E7%9A%84es5-es6%E5%86%99%E6%B3%95%E5%AF%B9%E7%85%A7%E8%A1%A8</a><br>React Native 官网 <a href="https://link.segmentfault.com/?enc=hTxL8o5zdyASFeH18anK4g%3D%3D.7xzMvcfe0EuWaTAXMyxnOzj4pv7aR7k0trIeGKJy1tyGDKuUsDSYt2zCHVC1mzjL" rel="nofollow">http://facebook.github.io/react-native/</a><br>React Native 中文 <a href="https://link.segmentfault.com/?enc=6c8p1o4grwk1etjLwDDqGQ%3D%3D.E1UcmXJzKm69Kr7D33c6%2BonC5%2FB9fQO7ynmDmc75pWg%3D" rel="nofollow">http://reactnative.cn/</a><br>React 官网 <a href="https://link.segmentfault.com/?enc=7l%2FdQho0f%2F9VO8yt0tRwQA%3D%3D.Qy2j4m2P6Q85EY9a%2BGljvieRsWjULA1ccVu1S%2FGqNMrwoahnhH0fB3RIyrOtgti%2F" rel="nofollow">https://facebook.github.io/react/index.html</a></p>
a标签是如何被触发跳转的
https://segmentfault.com/a/1190000004002147
2015-11-16T16:52:17+08:00
2015-11-16T16:52:17+08:00
fitfish
https://segmentfault.com/u/timetravel
4
<p>在统计按钮点击跳转次数时,给按钮绑定了touchstart事件,结果导致统计数据翻了近10倍。后改用click事件,数据才正常。固有此文。</p>
<h2>问题</h2>
<ol>
<li><p>当我们点击鼠标时,会触发一系列mouse/touch/click事件,a标签转跳是被什么事件触发的?</p></li>
<li><p>PC:在 div1 按下鼠标左键, 在div2 中释放鼠标左键,是否会触发click事件?</p></li>
<li><p>PC:在 div1 按下鼠标左键后,再在同一绑定事件的节点中移动鼠标,最后释放鼠标左键。 是否会触发click事件?</p></li>
<li><p>M: 在 div1 触按屏幕, 再在div2 中释放触按,是否会触发click事件?</p></li>
<li><p>M:在 div1 触按屏幕,再在同一绑定事件的节点中移动手指,最后移释放触按。是否会触发click事件?</p></li>
</ol>
<p>知道以上5个问题的结果的就可以不往下看了。</p>
<p>demo共用一段html</p>
<pre><code class="html"><a id="a" target="_blank" href="http://www.58.com">58</a>
<div class="div1"></div>
<div class="div2"></div></code></pre>
<h3>1. 当我们点击鼠标时,会触发一系列mouse/touch/pointer事件,a标签转跳是被什么事件触发的?</h3>
<pre><code class="js">var events = 'click touchstart touchend mousedown mouseup'.split(' ');
var n = 0;
var timer = setInterval(function(){
var event = new Event(events[n]);
document.getElementById('a').dispatchEvent(event); // 创建一系列事件,直接在元素节点上触发。
console.log(event.type);
n++;
if (n == events.length) {
clearInterval(timer);
}
},2000);</code></pre>
<p>测试结果: a标签跳转 只被 click 事件触发。</p>
<h3>2. PC:在 div1 按下鼠标左键, 在div2 中释放鼠标左键,是否会触发click事件?</h3>
<h3>3.PC:在 div1 按下鼠标左键后,再在同一绑定事件的节点中移动鼠标,最后释放鼠标左键。 是否会触发click事件?</h3>
<pre><code class="js">var events = 'click mousedown mouseup'.split(' ');
var divs = document.getElementsByTagName('div');
var handler = function(e){
e.preventDefault();
this.innerHTML += ' type:' + e.type + ' target:' + e.target.className + ' this:' + this.className;
};
for (var i=0; i <divs.length; i++) {
events.forEach(function(event){
divs[i].addEventListener(event, handler);
});
}</code></pre>
<p>测试结果:</p>
<p>问题2、问题3,在相同绑定事件元素中,按下鼠标左键,即使移动也会触发 click 事件,而在不同元素中不会触发 click 事件。</p>
<p>在测试问题3的过程中我们还发现,按下鼠标左键触发的是 div1 绑定的<code>mousedown</code>事件,释放鼠标左键触发的是 div2 绑定的<code>mouseup</code>事件。</p>
<p>我们可以这样认为,在点击鼠标左键时,只有在同一元素中先触发<code>mousedown</code>事件再触发<code>mouseup</code>事件,<code>click</code>事件才会被触发,a标签才会转跳</p>
<h3>4. M: 在 div1 触按屏幕, 再在div2 中释放触按,是否会触发click事件?</h3>
<h3>5. M:在 div1 触按屏幕,再在同一绑定事件的节点中移动手指,最后移释放触按。 是否会触发click事件?</h3>
<pre><code class="js">var events = 'click touchstart touchend mousedown mouseup'.split(' ');
var divs = document.getElementsByTagName('div');
var handler = function(e){
e.preventDefault();
this.innerHTML += ' type:' + e.type + ' target:' + e.target.className + ' this:' + this.className;
};
for (var i=0; i <divs.length; i++) {
events.forEach(function(event){
divs[i].addEventListener(event, handler); // type:mousedown target:div1 this:div1 | type:mouseup target:div2 this:div2
}); // type:touchstart target:div1 this:div1 | type:touchend target:div1 this:div1
}</code></pre>
<p>测试结果:</p>
<p>问题4,在 div1 触按屏幕, 再在div2 中释放触按,只会触发div1的 <code>touchstart</code> 和 <code>touchend</code> 事件,不会触发div2的 <code>touchend</code> 事件,并且不会触发<code>mousedown</code> <code>mouseup</code> <code>click</code>事件。</p>
<p>问题5,只要移动了手指,<code>mousedown</code> <code>mouseup</code> <code>click</code>事件就不被触发。</p>
<p>如果触摸屏幕,这些事件的触发顺序也是很有趣。 </p>
<p><code>touchstart</code> -> <code>touchend</code> -> <code>mousedown</code> -> <code>mouseup</code> -> <code>click</code>。</p>
<p>值得注意的是,<code>touchend</code>在<code>mousedown</code>之前被触发。</p>
<h2>总结</h2>
<p>a标签跳转只被<code>click</code>事件触发。<br>在PC端,点击鼠标左键,即使在a标签链接上移动鼠标,也会触发a标签跳转。<br>在M端,手指触摸屏幕,就会触发a标签跳转,但是如果过程中手指有移动,就触发不了a标签跳转。</p>
纯css无缝滚动
https://segmentfault.com/a/1190000003721884
2015-09-08T17:11:18+08:00
2015-09-08T17:11:18+08:00
fitfish
https://segmentfault.com/u/timetravel
13
<p>原理:<br><img src="/img/bVpMn3" alt="图片描述" title="图片描述"></p>
<p>复制一份需要无缝滚动的区域</p>
<pre><code><div class="page">
<ul class="box item1">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
<ul class="box item2">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
</ul>
</div> </code></pre>
<p>animation css3动画</p>
<pre><code>/*核心css*/
@keyframes moveup{
0% {
transform: translateY(0);
}
100% {
transform: translateY(-100%);
}
}
.box { animation: moveup 10s linear infinite; }
.page { overflow: hidden; }
/*样式*/
.page { margin-top: 200px; height: 100px; border-top: 2px solid lightblue; border-bottom: 2px solid red; }
.box {margin: 0;}
.box li{ line-height: 30px; }</code></pre>
<p>浏览器支持(需加css前缀):IE10+ Chrome FF android2.3+ safair5.1+</p>
Angular的模板与路由功能
https://segmentfault.com/a/1190000003118044
2015-08-20T18:11:37+08:00
2015-08-20T18:11:37+08:00
fitfish
https://segmentfault.com/u/timetravel
1
<h2>理解Angular的模板功能</h2>
<p>模板功能,是Angular的最核心的功能之一。<br>本节通过velocity模板与angular的模板功能对比,来说明angular的模板功能是如何工作的。</p>
<h3>传统的velocity服务端模板工作流程如下:</h3>
<p><strong>step:1</strong> Template 模板</p>
<ol>
<li><p><code>#</code>用来标识Velocity的脚本语句</p></li>
<li><p><code>$</code>用来标识一个对象(或理解为变量)</p></li>
</ol>
<pre><code><h1>$data.title</h1>
<ul>
#foreach($name in $data.names)
<li>$name</li>
#end
</ul></code></pre>
<p><strong>step:2</strong> Model 数据</p>
<p><code>$</code>变量的实际值存在服务器的 Model 中,这些数据结合<code>#</code>表示的velocity语法规则,在服务端转变成HTML也就是View 视图</p>
<pre><code>var data = {
"title": "分类信息网站",
"names": [
"58同城",
"赶集网"
]
};</code></pre>
<p><strong>step:3</strong> View 视图</p>
<p>在传统的服务端模板中,Template 模板和 Model 数据 组装成 View 视图 的过程发生在服务端,然后再把生成的HTML页面发送到游览器中。</p>
<pre><code><h1>分类信息网站</h1>
<ul>
<li>58同城</li>
<li>赶集网</ul>
</ul></code></pre>
<h3>Angular的模板的功能:</h3>
<p><strong>step:1</strong> Template 模板</p>
<ol>
<li><p>用<code>ng-app</code> <code>ng-controller</code> <code>ng-repeat</code>等标签属性表示Angular语法,官方术语叫做 <code>指令</code></p></li>
<li><p>用<code>{{}}</code>表示变量</p></li>
<li><p><code>ng-repeat</code>相当于velocity中的<code>#foreach</code>;<br><code>ng-app</code> 告诉Angular应该接页面中的哪一块,一般写在<code><html></code>标签中,让Angular接管整个页面;<br><code>ng-controller</code> 每个页面可能会有几个<code>controller</code>,它的作用是告诉Angular模板的对应的Angular数据由哪部分代码管理。</p></li>
</ol>
<pre><code><html ng-app="web">
<body ng-controller="mainController">
<h1>{{data.title}}</h1>
<ul>
<li ng-repeat="name in data.names">{{name}}</li>
</ul>
</body>
</html></code></pre>
<p><strong>step:2</strong> Model 数据</p>
<ol>
<li><p><code>$scope.data</code> 就相当于 上面velocity中模拟的 <code>data</code> 对象</p></li>
<li><p><code>angular.module('web',[])</code>对应的是模板中<code>ng-app="web"</code>;<code>controller('mainController',fn)</code>对应的是模板中的<code><body ng-controller="mainController"></code>。 表明<code>$scope.data</code>的作用域仅适用于<code><body></code>中的Angular模板。</p></li>
</ol>
<pre><code>angular.module('web', [])
.controller('mainController', function($scope){
$scope.data = {
"title": "分类信息网站",
"names": [
"58同城",
"赶集网"
]
};
});</code></pre>
<p><strong>step:3</strong> View 视图</p>
<p>在游览器中,Angular将Template 模板和Model 数据组装起来了,输出最终的HTML页面,也就是View 视图。</p>
<pre><code><h1>分类信息网站</h1>
<ul>
<li>58同城</li>
<li>赶集网</ul>
</ul></code></pre>
<p>Angular与velocity极其的相识,其功能都将Template 模板 和 Model 数据组装起来,输出成View 视图。其主要的不同在于,Angular的组装过程发生在游览器中,而velocity组装过程发生在服务器中。</p>
<p>我们可以通过下表将Angular模板功能和velocity进行一个简单对比。</p>
<table>
<thead><tr>
<th>对比</th>
<th>Angular</th>
<th>velocity</th>
</tr></thead>
<tbody>
<tr>
<td>模板语法</td>
<td>指令</td>
<td>
<code>#</code>开头的脚本语句</td>
</tr>
<tr>
<td>模板变量</td>
<td>{{}}</td>
<td>
<code>$</code>开头的模板变量</td>
</tr>
<tr>
<td>数据</td>
<td><code>$scope.data</code></td>
<td>模拟的data对象</td>
</tr>
<tr>
<td>工作原理</td>
<td>模板+数据=视图</td>
<td>模板+数据=视图</td>
</tr>
<tr>
<td>模板类型</td>
<td>客户端模板</td>
<td>服务端模板</td>
</tr>
</tbody>
</table>
<h2>Angular的路由功能及其原理</h2>
<h3>路由功能的实现原理</h3>
<p>在单页面应用中,View 视图会根据用户的操作进行切换,在传统页面中,视图的切换意味着页面跳转,用户会通过游览器中的后退,前进按钮来进行操作。而这正是单页面应用中,需要的功能。实现原理如下:</p>
<ol>
<li><p>点击某个按钮或链接</p></li>
<li><p>使用hash修改地址栏</p></li>
<li><p>加载相应视图</p></li>
<li><p>点击前进/后退</p></li>
<li><p>地址栏变化触发hashchange事件</p></li>
<li><p>监听到相应事件,加载对应视图</p></li>
</ol>
<p>如此一来,便形成了通过地址栏进行导航的深度链接,也就是我们所需要的路由机制。通过路由机制,一个单页应用的各个视图就可以很好的组织起来了。</p>
<p>html:</p>
<pre><code><div id="btns">
<input type="button" value="a">
<input type="button" value="b">
<input type="button" value="c">
</div>
<div id="page" style="display: none;">
<p>我是页面<span></span></p>
</div></code></pre>
<p>js:</p>
<pre><code>// 每次按钮点击的时候改变hash值。
$('input').click(function(event) {
var hash = $(this).val();
document.location.hash = '/' + hash;
});
// 用户点击游览器回退\前进或点击按钮都会引起hash值的改变。
// 当hash值改变时,改变 View 视图。
$(window).on('hashchange', function(e){
var hash = document.location.hash.replace(/#\//,'');
if (/(a|b|c)/.test(hash) ) {
$('#page span').text(hash);
$('#page').show();
$('#btns').hide();
} else {
$('#page').hide();
$('#btns').show();
}
});</code></pre>
<h3>Angular的路由功能</h3>
<p><strong>step1:引入依赖</strong></p>
<pre><code><script src="angular.min.js"></script>
<script src="angular-route.min.js"></script>
<script>
var app = angular.module('web', ['ngRoute']) //引入route模块,有点类似于require.js的引入
// ...
</script></code></pre>
<p>需要注意的是,以上文件的版本要一致,1.2.x的angular不能引用1.4.x版本的angular-ruote文件。</p>
<p><strong>step2:编写模板</strong></p>
<ol>
<li><p><code>ng-view</code>当前路由把对应的视图模板载入到该<code><div></code>中。</p></li>
<li><p><code>type="text/ng-template"</code>表明<code><script></code>标签中存的是Angular的模板文本。</p></li>
<li><p><code>href="#/"</code>Angular所控制的hash路径</p></li>
</ol>
<pre><code><div ng-view="" ></div>
<script type="text/ng-template" id="list.html">
<div id="btns" ng-controller="listContr">
<a ng-repeat="item in list" href="#/{{item}}" ><div>{{item}}</div></a>
</div>
</script>
<script type="text/ng-template" id="item.html">
<div id="page" ng-controller="itemContr">
<p>我是页面<span>{{item}}</span></p>
</div>
</script></code></pre>
<p><strong>step3:定义路由表</strong></p>
<p>为了给应用配置路由,需要使用configAPI,通过$routeProvider.whenAPI\otherwiseAPI来定义的路由规则。</p>
<ul>
<li><p>当URL为<code>/</code>时,AngularJS会使用<code>listContr</code>控制器和<code>list</code>模板</p></li>
<li><p>当URL为<code>/:item</code>时,这里的<code>:item</code>是变量,AngularJS会使用<code>itemContr</code>控制器和<code>item.html</code>模板。</p></li>
<li><p><code>otherwise</code>`redirectTo<code>表示,非以上路由,对页面进行重定向到</code>/`页面。</p></li>
</ul>
<pre><code>app.config(function($routeProvider){
$routeProvider.
when('/', {
controller: 'listContr', // 默认为全部职位
templateUrl: 'list'
}).
when('/:item',{ // 筛选职位
controller: 'itemContr',
templateUrl: 'item'
}).
otherwise({
redirectTo: '/'
});
});</code></pre>
<p><strong>step4:编写Controller</strong></p>
<p><code>$routeParams</code>对应的是路由的参数.</p>
<pre><code>app.controller('listContr', function($scope){
$scope.list = ['a','b','c'];
}).
controller('itemContr', function($scope, $routeParams){
$scope.item = $routeParams.item;
})</code></pre>