SegmentFault 苏溪云原创博客最新的文章
2021-05-19T18:08:55+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
状态管理本应如此简单
https://segmentfault.com/a/1190000040028847
2021-05-19T18:08:55+08:00
2021-05-19T18:08:55+08:00
苏溪云
https://segmentfault.com/u/terry_su
2
<p><img src="/img/remote/1460000040028849" alt="" title=""></p><p>2021年520前夕。近一年来主要都在做业务,不过对不断提升Coding质量与速度的追求和思考一直没停过。机缘巧合之下,有幸着重花时间完善了一套稳定、易用的状态管理方案。</p><p>作者在工作历程中,由最初用redux,然后到用vuex,再到实验mobx、dva、react hooks甚至是自研工具,最后回归redux。</p><p>在此期间,一直在探寻简单易用,并且有完整类型提示的方案。就是当我们输入一两个关键字,IDE自动弹出模型对应dispatch方法,再输入空字符串'',自动提示可能要调用的方法等。日常要写太多状态管理代码,完全没必要敲很多重复内容。</p><p>那么重新写一个状态管理库?</p><p>redux已经很好用,只是代码略微繁琐,为何要重新写?基于redux封装是否可行?</p><p>那是否要集成其他路由、模块化加载和网络请求等模块,让用户通过这个工具做大多数事?</p><p>这其实是很多集成式框架做的事。但状态管理本身就是单独一个模块,没必要和其他模块耦合在一起。模块化对于项目未来局部升级和改造要远方便于集成式。</p><p>既然讲到了类型推导提示,那么是否只有TypeScript项目才能完美支持?</p><p>当然很多类型都是借助TypeScript定义,但如今利用<a href="https://link.segmentfault.com/?enc=XhtbsE7dDX0HQWROr2UtIQ%3D%3D.0o8uRxesF8ai1g5UzZ6xhKGXWEYQOaXWEM6xHjKbSBUhi61sJCOhrm%2Be0wfm3YPrIftmSNDf1JCi2CNnuF4yT6Rxu8s12F%2FP9cgsNTT%2F%2Bik%3D" rel="nofollow">JSDoc注释类型写法</a>也能让JS项目支持类型提示和推导。</p><p>是否能兼容已有项目?</p><p>只要遵循相应规则,正常情况下都能兼容。</p><h2>方案</h2><p>现在的状态管理方案概念是:</p><ol><li>使用状态模型<code>model</code>管理状态</li><li>状态模型包含原始状态<code>state</code>和更新状态方法<code>methods</code></li><li>所有状态和方法都支持类型推导提示</li></ol><h3>状态模型model</h3><p>使用状态模型的好处是既可以集中式管理状态,同时能够让不同组件实现跨组件共用状态。<br>同时,每一个状态模型的常用方法可在该模型暴露,以供外界使用。</p><h3>状态模型包含原始状态<code>state</code>和更新状态方法<code>methods</code></h3><p>简化模型的构成元素,只需要状态和更新状态方法。更新状态方法用于处理复杂状态更新,也就是所谓的副作用。</p><pre><code class="js">{
name: 'counter',
state: {
count: 0
},
methods: {
add({ state, update }, number) {
update({ count: state.count + number })
}
async delayAdd({ dispatch }, duration, number) {
await new Promise(resolve => setTimeout( () => resolve(), duration ))
dispatch('add', 1)
}
async run({ state, dispatch, getState }) {
console.log('current count', state.count)
await dispatch('delayAdd', 1000, 1)
console.log('count after update', getState().count)
}
}
}</code></pre><p>而直接更新某个状态只需通过方案库暴露一个特有更新方法即可。比如:</p><pre><code class="js">updateModel('counter', { count: 100 })</code></pre><h3>所有状态和方法都支持类型推导提示</h3><p>这个太重要了,极大加快研发效率。<br><img src="/img/remote/1460000040028849" alt="" title=""></p><h2>代码实现</h2><p>代码实现地址:<br>Tredux: github.com/tredux-org/tredux</p><p>项目只是给个参考,最重要的是这套状态管理方案概念(代码层面可以有各种实现)。</p><p>其实封装难度并不大,最难的是如何让它在最简单的用法下,能够覆盖日常业务开发场景。</p><p>如果各位大佬觉得这套方案还可以,想用到自己项目甚至是公司项目中,但又不太放心依赖外部库,大可以将源码(一个TS文件)放到项目中单独引用。</p><p>现有方案已在业务项目中稳定运行,欢迎各位试用。</p><h2>结尾</h2><p>当然,这一套<strong>简单</strong>的状态管理方案只是抛砖引玉,也希望市场上能涌现出更多轻量易用的解决方案,让状态管理更轻松。</p><p><strong>感谢你花时间阅读这篇文章。如果你喜欢这篇文章,欢迎点赞、收藏和分享,让更多的人看到这篇文章,这也是对我最大的鼓励和支持!</strong> </p><p><strong>欢迎通过微信(搜索“苏溪云的博客”或者扫描下方二维码)或<a href="https://link.segmentfault.com/?enc=O01DohkRn6NDidkH8HcrTA%3D%3D.vWNJe9MpxELXln3Y%2FO%2BE4TAOMqBuVyWwCtwMu3sGR1O5ihOn5p0n4ActnRWSNFMD" rel="nofollow">Github</a>订阅我的博客。</strong></p><p><img src="/img/remote/1460000022198778" alt="微信公众号:苏溪云的博客" title="微信公众号:苏溪云的博客"></p>
轻松掌握React Hooks底层实现原理
https://segmentfault.com/a/1190000038768433
2020-12-31T23:45:04+08:00
2020-12-31T23:45:04+08:00
苏溪云
https://segmentfault.com/u/terry_su
26
<p>由于最近业务较忙,2020年搞懂React原理系列文章最终篇直到现在才在业余时间抽空完成。之前在公司内部已有过一次PPT形式的分享,但经过一段时间对hooks的深度使用,对其又有了更深一些了解,故本次加上新内容并以文章形式再分享一次。</p><p>持续一年阅读React源码和总结其核心原理,慢慢也有了一些心得:</p><p>读懂源码只是第一步,弄懂其功能的代码实现方式。而再进一步是彻底搞懂其实现原理、思想,它通过什么方式实现了什么功能,带来了什么价值。</p><p>不管它的底层代码如何改写,最终的目的都是为了实现某个功能。只要我们把其功能实现原理掌握,便可活学活用,结合业务让业务开发效率更高,或围绕业务做一些生产力工具。</p><blockquote><p>React使用当前最新版本:<code>17.0.1</code></p><p>今年写了一个“搞懂React源码系列”,把React最核心的内容用最易懂的方式讲清楚。2020年搞懂React源码系列:</p><ul><li><a href="https://link.segmentfault.com/?enc=11gI89xx%2FH%2Fs7mAubmixXw%3D%3D.MCViDBkShAVnwN0HyG7pYfV6nIu%2BpST1A9vYKF6nyRLrnHJp%2FHbmm%2BQyoNX2IOj0ipn7N5MXwtpJ5QIDoqWUKHpppaBspy0iJMqqWth81hc%3D" rel="nofollow">React Diff原理</a></li><li><a href="https://link.segmentfault.com/?enc=4q8JEjSZraDrYsAoM5%2FapA%3D%3D.vMgPeANvMjNPSB1k%2FLBAzrpy1gBkZpMudER0aEfAqGOTP6Oq3GHh52bTIPdmwWuxYsc1HWCKZoH%2BBtaYsaey9hcLEsc%2Fq2JhPoY2QqMDxjkGXKgPCxNDccxImglbS1uiNdd9r1pXSF0fO%2F0ZJJdXCg%3D%3D" rel="nofollow">React 调度原理</a></li><li><a href="https://link.segmentfault.com/?enc=4h49JOQM6%2FfbkDSZP3IXBQ%3D%3D.y0tRtKWR1cIypiF9O7mGuxUN%2BXaNjdJkiIyF%2FB0AxR7Y3pgmObjMAwjkbgJnFWVIfyqfjxDVdNR0TeAoXX0LqE057KXS8eIiX4AHN8%2FU%2Bn4%3D" rel="nofollow">搭建阅读React源码环境-支持React所有版本断点调试细分文件</a></li><li>(当前)React Hooks原理</li></ul><p>少了React Fiber更新原理?那是因为<a href="https://link.segmentfault.com/?enc=HnukMdOCKQRz6yHaOJHWEA%3D%3D.EE8sx5AiQPA%2F6ksHWgt%2FFTyHKrNBFrhPEdkYuRMhNzeFWn%2F8TxHi5Pcq%2FkGYZEJrZNQfn7SR%2BI1xODwEk%2FnaIB8mkcwK%2BDV9ZpDkMiLt49b2fOazCxSJ0IWyHVbnsDiGEDjJnUg08FCO4kyTqJn3zYtj8aJNwEne8hrWKfph01E%3D" rel="nofollow">国外大佬的一篇文章</a>写得太好,没有必要再重复写一次。或许明年可以找个时间写个简明概要的React所有原理汇总文章。</p></blockquote><p>本文将重点讲<code>useMemo</code>、<code>useEffect</code>和<code>useState</code>3个api,因为它们在日常开发中最常用。后面讲其他几个api。本次主要描述每个hook的功能和原理。</p><h2>基础知识</h2><p>任何一个hook的调用方式都是:</p><pre><code class="js">输出 = hook函数(输入)</code></pre><p>一定会有输入和hook函数和输出。<br>而被调用的过程一般是2种:组件初始化和组件更新。</p><h2>UseMemo实现原理</h2><p>useMemo的功能是记忆某个结果,只有依赖项发生改变时才更新输出结果。</p><pre><code class="js">输出结果 = useMemo(计算函数,依赖项)</code></pre><p>下方展示其在不同过程中useMemo内部实现原理。</p><table><thead><tr><th>输入</th><th>hook函数</th><th>输出</th></tr></thead><tbody><tr><td>计算函数,依赖项</td><td>useMemo</td><td>计算结果</td></tr></tbody></table><p><strong>组件初始化</strong>:</p><ol><li>执行计算函数,获取计算结果</li><li>缓存结果结果和依赖项</li><li>返回计算结果</li></ol><p><strong>组件更新</strong>:</p><pre><code>if (依赖项和已缓存依赖项相同) {
返回已缓存计算结果
} else {
执行计算函数,获取新计算结果
缓存新计算结果和新依赖项
返回新计算结果
}</code></pre><blockquote>其中一个问题值得注意,依赖项是如何比较的?深比较或浅比较?因为依赖项一般是一个数组,而数组中的每个元素是具体的依赖变量,那么React是如何比较的?<p>翻看源码,发现若两个依赖项都是数组,则React会使用Object.is对其每一个元素进行强比较。</p></blockquote><pre><code class="js">Object.is('foo', 'foo'); // true
Object.is(window, window); // true
Object.is('foo', 'bar'); // false
Object.is([], []); // false
var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo); // true
Object.is(foo, bar); // false
Object.is(null, null); // true
// Special Cases
Object.is(0, -0); // false
Object.is(-0, -0); // true
Object.is(NaN, 0/0); // true</code></pre><blockquote>转念一想,其实就应这样比较。</blockquote><h2>UseEffect实现原理</h2><pre><code class="js">useEffect(创建函数,依赖项)</code></pre><p>useEffect的主要功能是:</p><p>组件加载后执行创建函数,创建函数执行后会返回一个销毁函数,在组件销毁前执行。</p><p>若依赖项为数组且不为空,则依赖项改变时,会执行上一个销毁函数和重新执行创建函数。</p><table><thead><tr><th>输入</th><th>hook函数</th></tr></thead><tbody><tr><td>创建函数,依赖项</td><td>useEffect</td></tr></tbody></table><p>useEffect直接被调用的过程是组件初始化和组件更新,其销毁函数被调用的过程是组件销毁。</p><p><strong>组件初始化</strong>:</p><ol><li>生成一个effect对象,包含创建函数</li><li>缓存effect和依赖项</li><li>当React进入提交阶段,执行effect中的创建函数,获取销毁函数。若销毁函数不为空,则将其放入effect。</li></ol><p><strong>组件更新</strong>:</p><ol><li>生成一个effect对象, 包含创建函数</li><li>检查已缓存effect中是否有销毁函数,有的话则放入新effect对象</li><li>缓存effect</li><li>若依赖项和已缓存依赖项不同,则将hasEffect标记添加到effect,并缓存新依赖项</li><li>当React进入提交阶段:</li></ol><pre><code>if (effect有hasEffect标记) {
若effect中有销毁函数,则先执行销毁函数
执行effect中的创建函数,获取销毁函数。若销毁函数不为空,则将其放入effect
} </code></pre><p><strong>组件销毁</strong>:</p><ol><li>若effect中有销毁函数,则执行。</li></ol><h2>UseState实现原理</h2><p>useState的功能是设置一个状态的初始值,并返回当前状态和设置状态的函数。</p><pre><code class="js">[状态,设置状态函数] = useState(初始状态)</code></pre><table><thead><tr><th>输入</th><th>hook函数</th><th>输出</th></tr></thead><tbody><tr><td>初始状态</td><td>useState</td><td>状态,设置状态函数</td></tr></tbody></table><p>useState直接被调用的过程也是组件初始化和组件更新,其还有一个调用设置状态函数的过程。</p><p><strong>组件初始化</strong>:</p><ol><li>若初始状态为函数,则将函数执行结果设为当前状态。否则将初始状态设为当前状态。</li><li>生成设置状态函数</li><li>缓存当前状态和设置状态函数</li><li>返回当前状态</li></ol><p><strong>组件更新</strong>:</p><ol><li>读取缓存状态和设置状态函数</li><li>返回缓存状态</li></ol><p><strong>执行设置状态函数</strong>:</p><ol><li>更新缓存状态</li><li>触发React组件树更新</li><li>在下一次组件更新时,将返回已被更新的缓存状态</li></ol><h2>useReducer</h2><p>useReducer的功能和原理与useState一致,区别在于useReducer使用函数管理状态,使用派发动作指令函数作为设置状态函数。Reducer概念可参看redux。</p><pre><code class="js">[状态,派发动作指令函数]=useReducer(reducer函数,初始状态)</code></pre><h2>UseCallback实现原理</h2><pre><code>已缓存函数 = useCallback(待缓存函数,依赖项)</code></pre><p>useCallback的功能就是useMemo记忆函数一个封装,相比useMemo只是少套了一层函数:</p><pre><code>已缓存函数 = useMemo( () => 待缓存函数, 依赖项)</code></pre><p>不过React内部并没有用useMemo直接实现useCallback,而是用一套类似useMemo的代码实现。</p><h2>UseRef实现原理</h2><pre><code>{current: 当前值} = useRef(初始当前值)</code></pre><p>useRef的功能是生成一个对象,结构是:<code>{current: 当前值}</code>, 对象一旦初始化,不会因为组件更新而改变。</p><p>虽然对象初始化后不会因组件更新改变,但我们可以通过更改其current属性,当前值就相当于一个组件的变量,类似class组件的实例属性。</p><p>useRef最常用的场景莫过于设置组件的ref。</p><pre><code class="jsx">const container = useRef(null)
return <div ref={container}></div></code></pre><p>其实此处官网也有特别讲,div上的ref属性将触发设置<code>container.current</code>为dom对象。</p><p>但我们也可以把useRef作为生成组件变量的方法灵活应用。</p><table><thead><tr><th>输入</th><th>hook函数</th><th>输出</th></tr></thead><tbody><tr><td>初始当前值</td><td>useRef</td><td>{current: 当前值}</td></tr></tbody></table><p><strong>组件初始化</strong>:</p><ol><li>生成对象: { current: 初始当前值 }</li><li>缓存对象</li><li>返回缓存对象</li></ol><p><strong>组件更新</strong>:</p><ol><li>获取缓存对象</li><li>返回缓存对象</li></ol><h2>UseImperativeHandle</h2><p>useImperativeHandle的功能被子组件使用,实现父组件调用子组件内部方法,一般与forwardRef一起使用。</p><p>UseImperativeHandle实现原理与useEffect类似。</p><h2>UseLayoutEffect</h2><p>useLayoutEffect和useLayout的功能区别:</p><table><thead><tr><th>useLayoutEffect</th><th>useLayout</th></tr></thead><tbody><tr><td>渲染到屏幕前执行</td><td>渲染到屏幕后执行</td></tr></tbody></table><pre><code class="js">useLayoutEffect(() => {
// 组件初始化或更新在渲染到屏幕前执行
return () => {
// 1. 组件卸载前执行 2. 执行下一个effect前执行
}
}, )</code></pre><hr><p>在跨年前完成2020搞懂React原理系列文章最后一篇,也是为了迎接即将到来的2021。</p><p>在2021年,新的系列即将启航。不过在写新系列前,下一篇将先写微前端框架实现原理。</p><p>祝大家新年快乐!</p>
另辟蹊径搭建阅读React源码调试环境-支持所有React版本细分文件断点调试
https://segmentfault.com/a/1190000022990223
2020-06-21T23:08:00+08:00
2020-06-21T23:08:00+08:00
苏溪云
https://segmentfault.com/u/terry_su
9
<p><img src="/img/remote/1460000022990226" alt="" title=""></p><h2>引言(为什么写这篇文章)</h2><p>若要高效阅读和理解React源码,搭建调试环境是必不可少的一步。而常规方法:使用<code>react.development.js</code>和<code>react-dom.development.js</code>调试,虽然方便,但无法知道每段代码属于哪个细分文件,所以本文将介绍一种取巧的方法搭建便于调试React源码的环境,支持断点调试细分文件,并且此方法理论上可应用于所有Reat版本。</p><blockquote><p>最近一个月忙着换工作,这周终于有时间继续写发文章。写文章耗时短,搭建工具耗时较长,读者们可主要看工具使用模块。</p><p>React使用当前最新版本:<code>16.13.1</code> (2022年5月9日更新:已支持18.1.0)</p><p>今年会写一个“搞懂React源码系列”,把React最核心的内容用最易懂的方式讲清楚。2020年搞懂React源码系列:</p><ul><li>React Diff原理</li><li>React 调度原理</li><li>(当前)搭建阅读React源码环境-支持React所有版本断点调试细分文件</li><li>React Hooks原理</li></ul><p>欢迎Star和订阅<a href="https://link.segmentfault.com/?enc=CHav5WPxZznoTW%2FLVGlLUA%3D%3D.rugCy%2BrI06P88lZogHXaWNLFOZ%2FWOHhD0pNpgRUH7PtsDX%2F5vdOo0IVF8Xfx679h" rel="nofollow">我的博客</a>。</p></blockquote><h2>快速使用</h2><p>就像用手机并不一定要知道它的生产过程,使用源码调试环境也不一定要知道它的构建方法。</p><h3>方法1: 线上调试</h3><p><img src="/img/remote/1460000022990230" alt="" title=""><br>访问地址:<a href="https://link.segmentfault.com/?enc=qPVubQ1ThOWohMfCoEwuZw%3D%3D.QRCnsYSRsv6VlrT9A%2B3qQfy1mF9m%2FRppEVIcRFH%2FsCCcykZ6fc%2FwL0OSBpnk3Dhlqh9U4%2Boj8KwiNc1CX%2FDy69lx5nMEFSV2UjNRCZtYlZE%3D" rel="nofollow">https://terry-su.github.io/de...</a></p><h3>(推荐)方法2:下载对应直接调试源码文件</h3><p>此方法优势是可修改源码,比如在源码中添加注释。</p><p>使用步骤:</p><p>1 . 访问项目<a href="https://link.segmentfault.com/?enc=tKVlsZfvJTOCHWqXf3hMwg%3D%3D.gos5bc92qmjg5Sqs9BJtyArukyLbB3Gx4fVd7tqI96zmINj%2BFOHlBKVDUKH0S9K52ZMffzLCKBazD00qQ3sE0g%3D%3D" rel="nofollow">debug-react-source-code</a>,选择要调试React版本对应分支,然后点击下载压缩包。</p><p><img src="/img/remote/1460000022990227" alt="" title=""><br><img src="/img/remote/1460000022990229" alt="" title=""></p><p>当前(2020/6/21)版本列表:</p><ul><li><a href="https://link.segmentfault.com/?enc=6LvNF4CHp4ZGlyHLJPUBgw%3D%3D.6iNCLWav40wSWQJol6WI%2F3%2Bfy7PBGQwWRAAT0Mfw%2FSnNBq8DyuJAM8sN0kEtnWKrC30jnLTNC1LuNzi9dC6ZMA7kGS9uR8TKOsuDJSY2oBU%3D" rel="nofollow">debug-react-16.13.1</a></li><li><a href="https://link.segmentfault.com/?enc=uI%2FfkSchxX2DfZIj7Um1eA%3D%3D.YmQwQCrNEyc%2BoNxvwitFkiliJ9lLTXQHZbQqIjmnIEiscyZlTpaPpYrxMxFdL4WU%2F1A%2B40pvGJJHAj%2Fra22b9i7SidArBu1VbjzMPFxnCmU%3D" rel="nofollow">debug-react-16.6.0</a></li></ul><p>2 . 将压缩包解压后,用vscode打开该文件夹。vscode需安装<a href="https://link.segmentfault.com/?enc=nW3zsinMuikmym3R7kf31Q%3D%3D.ulF6ZEMqtIg%2FkXVXqcpw3lvBXIC0JHoGj4KdvuOftkqKFV2kDqV4tKWmJP9d3FmlICNagxTfh5zt%2BNJz6fc733BFi%2B5iQK0b5T002Ptq7Uryt9YaGxu8S9ijPDSunt%2B5" rel="nofollow">Debugger for Chrome</a>拓展,用于在vscode对源码添加断点</p><p>3 . 安装依赖后,开启服务</p><pre><code>npm install</code></pre><pre><code>npm start</code></pre><p>4 . 在源码中添加断点,按F5启动调试即可<br><img src="/img/remote/1460000022990228" alt="" title=""></p><p>~ ~ ~ ~ ~ ~</p><p>~ ~ ~ ~ ~ ~</p><h2>背景</h2><p>正片现在开始。</p><p>接下来讲讲搭建该调试环境背景。React官方建议直接使用源码项目中创建生成的<code>react.development.js</code>和<code>react-dom.development.js</code>。</p><p>但此方法无法看到每段代码所在具体源文件。最好的方案是能够直接调试源码中的细分文件。</p><p>其实之前网上可以找到实现此方案的方法,用webpack新建一个项目,然后想办法引入React源码中的各个模块,再添加各种配置,修改源文件以解决各种特殊情况。我之前也尝试过,但发现随着版本更新,该方法已逐渐不再适用,因为会遇到无法解决的特殊情况。</p><p>根本原因是因为React源码的打包配置较多,且含有自定义配置,所以即使给<code>rollup</code>配置了<code>sourcemap</code>也不会生效。</p><blockquote>有人给react提过<a href="https://link.segmentfault.com/?enc=q4cqYy7iiLPAmgfAYnQuSA%3D%3D.OXNxLwHNmhoYw11vsI4ecyl8tDJagZdocMIclhed25yniIMhQF24L8ytmfpVRDA6" rel="nofollow">一个编译react生成sourcemap的issue</a>,但Dan的回复是:"你应该具备不依赖sourcemap调试开发模式下源码的能力"。HaHa</blockquote><p>但是,能够调试源码文件的确是刚需,怎么办呢?</p><h2>实现方法</h2><p>于是,开始从其他突破口寻找解决方案。生成sourcemap的方案走不通,那通过<code>react.development.js</code>反过来生成各种细分文件是否可行?</p><p>如果要生成各个细分文件,就需要它们的路径信息,在哪里添加?</p><p>既然react源码是通过<code>rollup</code>打包生成,那么,能否在<code>rollup</code>配置中添加一个自定义插件,往<code>react.development.js</code>中注入代码对应路径?</p><p>顺着这个思路,一番尝试后,发现有一两种特殊情况要处理,但最终目的成功实现!是的,目前所搭建调试环境就是使用此方法。</p><p>具体实现细节较复杂,这里先简单说下主要流程:</p><p>1 . 生成注入细分源码文件路径信息的<code>react.development.js</code>和<code>react-dom.development.js</code></p><p>2 . 从<code>react.development.js</code>和<code>react-dom.development.js</code>中提取核心数据,反向生成源码文件</p><p>3 . 根据核心数据,新建react,react-dom对应html和主要html,使用iframe实现隔离react、react-dom代码作用域和使不同html能够通信。</p><h2>衡量利弊</h2><p>此方法优点:</p><ul><li>简化(移除flow类型代码和生产环境相关代码)</li><li>方便使用,可放在线上调试(体验地址:<a href="https://link.segmentfault.com/?enc=LABRw7ljbqRlPeWLxtfR8A%3D%3D.KyQ65Jo9YYSne4OxQQO6QDmetl5HtaPaKq8RlNtDU8ycwLsuyeGkjObAj1z3jDYPBlVbp%2FQppbcggTddkLG5FUQxAKSKjNPsLec%2Bmycq748%3D" rel="nofollow">https://terry-su.github.io/de...</a>)</li></ul><p>此方法缺点:</p><ul><li>没有Flow类型代码</li><li>没有生产环境相关代码</li></ul><p>如何解决缺点?方法就是配合原始源码细分文件一起阅读,比如查看一个对象的类型结构。<br>但大多数情况下,此方法都适用。</p><h2>具体实现</h2><p>本小节仅建议想了解此工具构建原理的同学阅读, 对于只需要获取调试环境的读者,可跳过此小节。</p><p>1 . 下载react源码,安装依赖项。</p><p>2 . 创建自定义<code>rollup</code>插件,生成新的<code>build.js </code>:在新<code>build.js</code>中引入自定义<code>rollup</code>插件,该插件的作用是给每个文件的头部和尾部添加特殊起始标记和结束标记,每个标记都包含该文件的路径信息<br>运行新的<code>build.js</code>,从而生成注入了细分文件路径信息的<code>react.development.js</code>和<code>react-dom.development.js</code></p><p>3 . 处理<code>react-development.js</code>和<code>react-dom.development.js</code>,生成核心数据:基于两个JS文件,生成对应核心数据,类型结构为:<code>{outputFile: string, text: string}[]</code>。<code>outputFile</code>是反向输出的文件路径,<code>text</code>为文件内容。</p><p>4 . 基于核心数据,创建源码调试环境: 创建<code>dependency-react.html</code>和<code>dependency-react-dom.html</code>和其他所需文件。目录结构为:</p><pre><code>/react.development/
/react-dom.development/
/babel.js
/dependency-main.html
/dependency-react.html
/dependency-react-dom.html
/index.html
/index.js</code></pre><p>其中,<code>index.js</code>即为调试入口文件。</p><p>完整内容建议感兴趣的同学直接阅读源码:<a href="https://link.segmentfault.com/?enc=ag1aR9rPigfMUFsH%2F2mHgA%3D%3D.glQfy2ACDvl%2B54I1sjVrN9vAdbkLXuRegiqkMMc%2FE%2FBXqoejJPDDpmRKd0ICWnNNigZsCdc1RUkkFPmU10NCQQ%3D%3D" rel="nofollow">debug-react-source-code</a>。</p><h2>预告</h2><p>下一篇将写React hooks的原理。Hooks的精髓往往不是的它的实现原理,而是设计理念。但弄懂React hooks原理,能让我们进一步加深对hooks思想和设计方式的理解。</p><blockquote><p><strong>感谢你花时间阅读这篇文章。如果你喜欢这篇文章,欢迎点赞、收藏和分享,让更多的人看到这篇文章,这也是对我最大的鼓励和支持!</strong> </p><p><strong>欢迎通过微信(搜索“苏溪云的博客”或者扫描下方二维码)或<a href="https://link.segmentfault.com/?enc=j74w25fvZRLwQiym66I7PQ%3D%3D.nhgqpoAojLv1bsu4SSgCNtwj68SppJ3HiWqwktipvSTS5ZlUNF6LUTtswMFJJHoE" rel="nofollow">Github</a>订阅我的博客。</strong></p><p><img src="/img/remote/1460000022198778" alt="微信公众号:苏溪云的博客" title="微信公众号:苏溪云的博客"></p></blockquote>
彻底搞懂React源码调度原理(Concurrent模式)
https://segmentfault.com/a/1190000022606323
2020-05-11T13:48:48+08:00
2020-05-11T13:48:48+08:00
苏溪云
https://segmentfault.com/u/terry_su
24
<p><img src="/img/remote/1460000022605861" alt="" title=""></p>
<p>自上一篇写关于diff的文章到现在已经过了二十天多,利用业余时间和10天婚假的闲暇,终于搞懂了React源码中的调度原理。当费劲一番周折终于调试到将更新与调度任务连接在一起的核心逻辑那一刻,忧愁的嘴角终于露出欣慰的微笑。</p>
<p>最早之前,React还没有用fiber重写,那个时候对React调度模块就有好奇。而现在的调度模块对于之前没研究过它的我来说更是带有一层神秘的色彩,色彩中朦胧浮现出两个字:“困难”。</p>
<p>截至目前react的Concurrent(同时)调度模式依然处在实验阶段(期待中),还未正式发布,但官网已有相关简单介绍的文档,相信不久之后就会发布(参考hooks)。</p>
<p>在研究的时候也查阅了网上的相关资料,但可参考的不多。原因一个是调度模块源码变动较大,之前的一些文章和现在的源码实现对不上(不过很多文章对时间切片和优先级安排的概念讲解很到位),另一个是现在可参考的列出调度流程相应源码的文章几乎没有。</p>
<p>所以本文主要是通过自己对源码的阅读,推理和验证,加上大量时间作为催化剂,将React源码中的调度原理展现给各位读者。</p>
<blockquote>
<p>React使用当前最新版本:<code>16.13.1</code></p>
<p>今年会写一个“搞懂React源码系列”,把React最核心的内容用最易懂的方式讲清楚。2020年搞懂React源码系列:</p>
<ul>
<li>React Diff原理</li>
<li>(当前)React 调度原理</li>
<li>搭建阅读React源码环境-支持React所有版本断点调试细分文件</li>
<li>React Hooks原理</li>
</ul>
<p>欢迎Star和订阅<a href="https://link.segmentfault.com/?enc=ni%2BJQdQOKMKfUU6DFq15Gw%3D%3D.pQ31eST33AVVffCbxvU7RxEf8RI819YH3p9OtYYUjW27WWBuuiXxmj8Q8tCTC4XR" rel="nofollow">我的博客</a>。</p>
</blockquote>
<h3>同步调度模式</h3>
<p>React目前只有一种调度模式:同步模式。只有等Concurrent调度模式正式发布,才能使用第两种模式。</p>
<p>没有案例的讲解是没有灵魂的。我们先来看一个此处和后续讲优先级都将用到的案例:</p>
<p>假设有一个按钮和有8000个包含同样数字的文本标签,点击按钮后数字会加2。(使用8000个文本标签是为了加长react单次更新任务的计算时间,以便直观观察react如何执行多任务)</p>
<p>我们用类组件实现案例。</p>
<p>渲染内容:</p>
<pre><code class="html"><div>
<button ref={this.buttonRef} onClick={this.handleButtonClick}>增加2</button>
<div>
{Array.from(new Array(8000)).map( (v,index) =>
<span key={index}>{this.state.count}</span>
)}
</div>
</div></code></pre>
<p>添加按钮点击事件:</p>
<pre><code class="js">handleButtonClick = () => {
this.setState( prevState => ({ count: prevState.count + 2 }) )
}</code></pre>
<p>并在<code>componentDidMount</code>中添加如下代码:</p>
<pre><code class="js">const button = this.buttonRef.current
setTimeout( () => this.setState( { count: 1 } ), 500 )
setTimeout( () => button.click(), 500 )</code></pre>
<p>ReactDOM初始化组件:</p>
<pre><code class="js">ReactDOM.render(<SyncSchedulingExample />, document.getElementById("container"));</code></pre>
<p>添加2个setTimeout是为了展示同步模式的精髓: 500毫秒后有两个异步的setState的任务,由于react要计算和渲染8000个文本标签,那么任何一个任务光计算的时间都要几百毫秒,那么react会如何处理这两个任务?</p>
<p>运行案例后,查看Chrome性能分析图:</p>
<p><img src="/img/remote/1460000022605254" alt="" title=""></p>
<p>从结果可知,尽管两个任务理应“同时”运行,但react会先把第一个任务执行完后再执行第二个任务,这就是react同步模式:</p>
<p>多个任务时,react都会按照任务顺序一个一个执行,它无法保证后面的任务能在本应执行的时间执行。(其实就是JS本身特性EventLoop的展现。比如只要一个while循环足够久,理应在某个时刻执行的方法就会被延迟到while循环结束后才运行。)</p>
<h3>Concurrent(同时)调度模式</h3>
<p>Concurrent调度模式是一种支持<strong>同时执行多个更新任务</strong>的调度模式。</p>
<p>它的特点是任何一个更新任务都可以被更高优先级中断插队,在高优先级任务执行之后再执行。</p>
<p>很重要的一点,"同时执行多个更新任务"指的是同时将多个更新任务添加到React调度的任务队列中,然后React会一个个执行,而不是类似多线程同时工作那种方式。</p>
<h4>如何理解模式名字:Concurrent(同时)?</h4>
<p>React官网用了一个很形象的版本管理案例来形容“同时”模式。</p>
<p>当我们没有版本管理软件的时候,若一个人要修改某个文件,需要通知其他人不要修改这个文件,只有等他修改完之后才能去修改。无法做到多个人同时修改一个文件。</p>
<p>但有了版本管理软件,我们每个人都可以拉一个分支,修改同一个文件,然后将自己修改的内容合并到主分支上,做到多人“同时”修改一个文件。</p>
<p>所以,如果React也能做到“同时”执行多个更新任务,做到每一个更新任务的执行不会阻塞其他更新任务的加入,岂不是很方便。</p>
<p>这可以看作是“同时”模式名字的由来。</p>
<h4>同时调度模式的应用场景</h4>
<p>下方为React团队成员Dan在做同时模式分享时用的DEMO。同样的快速输入几个数字,在同步模式和同时模式可发现明显区别。</p>
<p><a href="https://link.segmentfault.com/?enc=E6QLCbLbvEQMdSzPMFMW8g%3D%3D.R68S%2FUrWPWx0vH75fzfUdcDAH1BPtJuHj7X8lh8ATMEi8BQjWUviABSxHxDby9Oi" rel="nofollow">Dan-Concurrent Mode Demo</a>:</p>
<p><img src="/img/remote/1460000022605255" alt="" title=""></p>
<p>同步模式下,卡顿现象明显,并且会出现UI阻塞状态:Input中的光标不再闪烁,而是卡住。</p>
<p>同时模式下,只有输入内容较长才会出现稍微的卡顿情况和UI阻塞。性能得到明显改善。</p>
<p>同时模式很好的解决了连续频繁更新状态场景下的卡顿和UI阻塞问题。当然,同时模式下还有其他实用功能,比如Suspense,因为本文主要讲调度原理和源码实现,所以就不展开讲Suspense了。</p>
<h4>同时调度模式如何实现</h4>
<p>React是如何实现同时调度模式的?这也是本文的核心。接下来将先讲时间切片模式,以及React如何实现时间切片模式,然后再讲调度中的优先级,以及如何实现优先级插队,最后讲调度的核心参数:expirationTime(过期时间)。</p>
<h3>时间切片</h3>
<p><img src="/img/remote/1460000022605257" alt="" title=""></p>
<h4>什么是时间切片</h4>
<p>最早是从Lin Clark分享的<a href="https://link.segmentfault.com/?enc=L8HRvZioSo7hM1mFrzzeJA%3D%3D.eNd%2Bj6O68Q9K1yPeiub7NbfDYZFTTL3L6sdsBl%2BhS%2FAgCLLr2QlhPDe2S99Es82r" rel="nofollow">经典Fiber演讲</a>中了解到的时间切片。时间切片指的是一种将多个粒度小的任务放入一个个时间切片中执行的一种方法。</p>
<h4>时间切片的作用</h4>
<p>在刚执行完一个时间切片准备执行下一个时间切片前,React能够:</p>
<ul>
<li>判断是否有用户界面交互事件和其他需要执行的代码,比如点击事件,有的话则执行该事件</li>
<li>判断是否有优先级更高的任务需要执行,如果有,则中断当前任务,执行更高的优先级任务。也就是利用时间前片来实现高优先级任务插队。</li>
</ul>
<p>即时间切片有两个作用:</p>
<ol>
<li>在执行任务过程中,不阻塞用户与页面交互,立即响应交互事件和需要执行的代码</li>
<li>实现高优先级插队</li>
</ol>
<h4>React源码如何实现时间切片</h4>
<p>1 . 首先在这里引入当前React版本中的一段注释说明:</p>
<blockquote>// Scheduler periodically yields in case there is other work on the main<br>// thread, like user events. By default, it yields multiple times per frame.<br>// <strong>It does not attempt to align with frame boundaries, since most tasks don't</strong><br>// <strong>need to be frame aligned</strong>; for <strong>those that do, use requestAnimationFrame</strong>.<br>let yieldInterval = 5;</blockquote>
<p>注释对象是声明<code>yieldInterval</code>变量的表达式,值为5,即5毫秒。其实这就是React目前的单位时间切片长度。</p>
<p>注释中说一个帧中会有多个时间切片(显而易见,一帧~=16.67ms,包含3个时间切片还多),切片时间不会与帧对齐,如果要与帧对齐,则使用<code>requestAnimationFrame</code>。</p>
<p>从2019年2月27号开始,React调度模块<a href="https://link.segmentfault.com/?enc=YMkwH6o0Y8bv%2BZ0pWoukMQ%3D%3D.6ujATQa2juOm0d%2F%2BC5Kh%2BMvHYQL6%2FUVHF3EMLl01pBbwFHypc%2BgRBnbuJN9I67fJRLvJb1fJ4PqR%2FxFvoZnYswYaKnGcIZS0uP6UFV6z5vF97RReVCTyhvBG8OsedXoXMBOSCAxaEr124YGwtuc3fNmkqwf5ddd%2Fe6jDpN6DQ8lnt%2FiEpRBv8%2FcR%2FTBgDcRu" rel="nofollow">移除了之前的requestIdleCallback腻子脚本相关代码</a>。</p>
<p><img src="/img/remote/1460000022605256" alt="" title=""></p>
<p>所以在一些之前的调度相关文章中,会提到React如何使用<code>requestAnimationFrame</code>实现<code>requestIdleCallback</code>腻子脚本,以及计算帧的边界时间等。因为当时的调度源码的确使用了这些来实现时间切片。不过现在的调度模块代码已精简许多,并且用新的方式实现了时间切片。</p>
<p>2 . 了解时间切片实现方法前需掌握的知识点:</p>
<ul><li>
<code>Message Channel</code>:浏览器提供的一种数据通信接口,可用来实现订阅发布。其特点是其两个端口属性支持双向通信和异步发布事件(<code>port.postMessage(...)</code>)。</li></ul>
<pre><code class="js">const channel = new MessageChannel()
const port1 = channel.port1
const port2 = channel.port2
port1.onmessage = e => { console.log( e.data ) }
port2.postMessage('from port2')
console.log( 'after port2 postMessage' )
port2.onmessage = e => { console.log( e.data ) }
port1.postMessage('from port1')
console.log( 'after port1 postMessage' )
// 控制台输出:
// after port2 postMessage
// after port1 postMessage
// from port2
// from port1</code></pre>
<ul><li>
<code>Fiber</code>: Fiber是一个的节点对象,React使用链表的形式将所有Fiber节点连接,形成链表树,即虚拟DOM树。</li></ul>
<p>当有更新出现,React会生成一个工作中的Fiber树,并对工作中Fiber树上每一个Fiber节点进行计算和<a href="https://link.segmentfault.com/?enc=tT8wwwFTeGLqw1BO5Cn2Cg%3D%3D.Y8NmO30rctEw7raizqdUwj2yCBYQBPUtrPFsJoA7HwSxhSSLOPB3ai2fppe6AH28dsFZzSOzKo6rfQzQmys9exBF4GWEd2f0upjNiveDjFA%3D" rel="nofollow">diff</a>,完成计算工作(React称之为渲染步骤)之后,再更新DOM(提交步骤)。</p>
<p>3 . 下面让我们来看React究竟如何实现时间切片。</p>
<p>首先React会默认有许多微小任务,即所有的工作中fiber节点。</p>
<p>在执行调度工作循环和计算工作循环时,执行每一个工作中Fiber。但是,有一个条件是每隔5毫秒,会跳出工作循环,运行一次<strong>异步的<code>MessageChannel</code>的<code>port.postMessage(...)</code>方法,检查是否存在事件响应、更高优先级任务或其他代码需要执行</strong>,如果有则执行,如果没有则重新创建工作循环,执行剩下的工作中Fiber。</p>
<p><img src="/img/remote/1460000022605257" alt="" title=""></p>
<p>但是,为什么性能图上显示的切片不是精确的5毫秒?</p>
<p>因为一个时间切片中有多个工作中fiber执行,每执行完一个工作中Fiber,都会检查开始计时时间至当前时间的间隔是否已超过或等于5毫秒,如果是则跳出工作循环,但算上检查的最后一个工作中fiber本身执行也有一段时间,所以最终一个时间切片时间一定大于或等于5毫秒。</p>
<p>时间切片和其他模块的实现原理对应源码位于本文倒数第二章节“源码实探”。</p>
<p>将描述和实际源码分开,是为了方便阅读。先用大白话把原理实现流程讲出来,不放难懂的源码,最后再贴出对应源码。</p>
<h2>如何调度一个任务</h2>
<p>讲完时间切片,就可以了解React如何真正的调度一个任务了。<br><img src="/img/remote/1460000022605259" alt="" title=""></p>
<p><code>requestIdleCallback(callback, { timeout: number })</code>是浏览器提供的一种可以让回调函数执行在每帧(上图2个<code>vsync</code>之间即为1帧)末尾的空闲阶段的方法,配置timeout后,若多帧持续没有空闲时间,超过timeout时长后,该回调函数将立即被执行。 </p>
<p>现在的React调度模块虽没有使用<code>requestIdleCallback</code>,但充分吸收了<code>requestIdleCallback</code>的理念。其<code>unstable_scheduleCallback(priorityLevel, callback, { timeout: number })</code>就是类似的实现,不过是针对不同优先级封装的一种调度任务的方法。</p>
<p>在讲调度流程前先简单介绍调度中用到的相关参数:</p>
<ul>
<li>当前Fiber树的root:拥有属性“回调函数”</li>
<li>React中的调度模块的任务: 拥有属性 “优先级,回调函数,过期时间”</li>
<li>过期时间标记:源码中expirationTime有两种类型,一种是标记类型:一个极大值,大小与时长成反比,可以用来作优先级标记,值越大,优先级越高,比如:<code>1073741551</code>;另一种是从网页加载开始计时的具体过期时间:比如8000毫秒)。具体内容详见后面的expirationTime章节</li>
<li>DOM调度配置: 因为react同时支持web端dom和移动端native两种,核心算法一致,但有些内容是两端独有的,所以有的模块有专门的DOM配置和Native配置。我们这里将用到调度模块的DOM配置</li>
<li>
<code>requestHostCallback</code>:DOM调度配置中使用<code>Message Channel</code>异步执行回调函数的方法</li>
</ul>
<p>接下来看React如何调度一个任务。</p>
<h4>初始化</h4>
<p>1 . 当出现新的更新,React会运行一个确保root被安排任务的函数。</p>
<p>2 . 当root的回调函数为空值且新的更新对应的过期时间标记是异步类型,根据当前时间和过期时间标记推断出优先级和计算出timeout,然后根据优先级、timeout, 结合执行工作的回调函数,新建一个任务(这里就是<code>scheduleCallback</code>),将该任务放入任务队列中,调用DOM调度配置文件中的<code>requestHostCallback</code>,回调函数为调度中心的清空任务方法。</p>
<h4>运行任务</h4>
<p>1 . <code>requestHostCallback</code>调用MessageChannel中的异步函数:<code>port.postMessage(...)</code>,从而异步执行之前另一个端口<code>port1</code>订阅的方法,在该方法中,执行<code>requestHostCallback</code>的回调函数,即调度中心的清空任务方法。</p>
<p>2 . 清空任务方法中,会执行调度中心的工作循环,循环执行任务队列中的任务。</p>
<p>有趣的是,工作循环并不是执行完一次任务中的回调函数就继续执行下一个任务的回调函数,而是执行完一个任务中的回调函数后,检测其是否返回函数。若返回,则将其作为任务新的回调函数,继续进行工作循环;若未返回,则执行下一个任务的回调函数。</p>
<p>并且工作循环中也在检查5毫秒时间切片是否到期,到期则重新调<code>port.postMessage(...)</code>。</p>
<p>3 . 任务的回调函数是一个执行同时模式下root工作的方法。执行该方法时将循环执行工作中fiber,同样使用5毫秒左右的时间切片进行计算和diff,5毫秒时间切片过期后就会返回其自身。</p>
<h4>完成任务</h4>
<p>1 . 在执行完所有工作中fiber后,React进入提交步骤,更新DOM。</p>
<p>2 . 任务的回调函数返回空值,调度工作循环因此(运行任务步骤中第二点:若任务的回调函数执行后返回为空,则执行下一个任务)完成此任务,并将此任务从任务队列中删除。</p>
<h3>如何实现优先级</h3>
<p>目前有6种优先级(从高到低排序):</p>
<table>
<thead><tr>
<th>优先级类型</th>
<th>使用场景</th>
</tr></thead>
<tbody>
<tr>
<td>立即执行ImmediatePriority</td>
<td>React内部使用:过期任务立即同步执行;用户自定义使用</td>
</tr>
<tr>
<td>用户与页面交互UserBlockingPriority</td>
<td>React内部使用:用户交互事件生成此优先级任务;用户自定义使用</td>
</tr>
<tr>
<td>普通NormalPriority</td>
<td>React内部使用:默认优先级;用户自定义使用</td>
</tr>
<tr>
<td>低LowPriority</td>
<td>用户自定义使用</td>
</tr>
<tr>
<td>空闲IdlePriority</td>
<td>用户自定义使用</td>
</tr>
<tr>
<td>无NoPriority</td>
<td>React内部使用:初始化和重置root;用户自定义使用</td>
</tr>
</tbody>
</table>
<p>表格中列出了优先级类型和使用场景。React内部用到了除低优先级和空闲优先级以外的优先级。理论上,用户可以自定义使用所有优先级,使用方法:</p>
<pre><code class="js">React.unstable_scheduleCallback(priorityLevel, callback, { timeout: <number> })</code></pre>
<p>不同优先级的作用就是让高优先级任务优先于低优先级任务执行,并且由于时间切片的特性(每5毫秒执行一次异步的<code>port.postMessage(...)</code>,在执行相应回调函数前会执行检测到的需要执行的代码)高优先级任务的加入可以中断正在运行的低优先级任务,先执行完高优先级任务,再重新执行被中断的低优先级让任务。</p>
<p>高优先级插队也是同时调度模式的核心功能之一。</p>
<h4>高优先级插队</h4>
<p>接下来,使用类似同步模式代码的插队案例。<br>渲染内容:</p>
<pre><code class="html"><div>
<button ref={this.buttonRef} onClick={this.handleButtonClick}>增加2</button>
<div>
{Array.from(new Array(8000)).map( (v,index) =>
<span key={index}>{this.state.count}</span>
)}
</div>
</div></code></pre>
<p>添加按钮点击事件:</p>
<pre><code class="js">handleButtonClick = () => {
this.setState( prevState => ({ count: prevState.count + 2 }) )
}</code></pre>
<p>并在<code>componentDidMount</code>中添加如下代码(不同之处,第二次setTimeout的时间由500改为600):</p>
<pre><code class="js">const button = this.buttonRef.current
setTimeout( () => this.setState( { count: 1 } ), 500 )
setTimeout( () => button.click(), 600)</code></pre>
<p>ReactDOM初始化组件(不同之处,使用React.createRoot开启Concurrent模式):</p>
<pre><code class="js">ReactDOM.createRoot( document.getElementById('container') ).render( <ConcurrentSchedulingExample /> )</code></pre>
<p>为什么第二次setTimeout的时间由500改为600?</p>
<p>因为是为了展示高优先级插队。第二次setTimeout使用的用户交互优先级更新,晚100毫秒,可保证第一次setTimeout对应的普通更新正在执行中,还没有完成,这个时候最能体现插队效果。</p>
<p><img src="/img/remote/1460000022605261" alt="" title=""></p>
<p>运行案例后,页面默认显示8000个0,然后0变为2(而不是变为1),再变为3。</p>
<p>通过DOM内容的变化已经可以看出:第二次setTimeout执行的按钮点击事件对应的更新插了第一次setTimeout对应更新的队。</p>
<p>接下来,观察性能图。<br>总览:<br><img src="/img/remote/1460000022605260" alt="" title=""><br>被中断细节:只执行了3个时间切片就被中断:<br><img src="/img/remote/1460000022605262" alt="" title=""></p>
<h4>如何实现高优先级插队</h4>
<p>1 . 延用上面的高优先级插队案例,从触发高优先级点击事件(准备插队)开始。</p>
<p>触发点击事件后,React会运行内部的合成事件相关代码,然后执行一个执行优先级的方法,优先级参数为“用户交互UserBlockingPriority”,接着进行<code>setState</code>操作。</p>
<p><code>setState</code>的关联方法新建一个更新,计算当前的过期时间标记,然后开始安排工作。</p>
<p>2 . 在安排工作方法中,运行确保root被安排任务的方法。因为现在的优先级更高且过期时间标记不同,调度中心取消对之前低优先级任务的安排,并将之前低优先级任务的回调置空,确保它之后不会被执行(调度中心工作循环根据当前的任务的回调函数是否为空决定是否继续执行该任务)。</p>
<p>然后调度中心根据高优先级更新对应的优先级、过期时间标记、timeout等创建新的任务。</p>
<p>3 . 执行高优先级任务,当执行到开始计算工作中类Fiber(<code>class ConcurrentSchedulingExample</code>),执行更新队列方法时,React将循环遍历工作中类fiber的更新环状链表。</p>
<p><strong>当循环到之前低优先级任务对应更新时,因为低优先级过期时间标记小于当前渲染过期时间标记,故将该低优先级过期时间标记设为工作中类fiber的过期时间标记(其他情况会将工作中类fiber的过期时间标记设为0)</strong>。此处是之后恢复低优先级的关键所在。</p>
<p>4 . 在完成优先级任务过程的提交渲染DOM步骤中,渲染DOM后,会将root的callbackNode(其名字容易误导其功能,其实就是调度任务,用callbackTask或许更合适)设为空值。</p>
<p>在接下来执行确保root被安排任务的方法中,因为下一次过期时间标记不为空(根本原因就是上面第二点提到工作中类fiber的过期时间标记被设置为低优先级过期时间标记)且root的callbackNode为空值,所以创建新的任务,即重新创建一个新的低优先级任务。并将任务放入任务列表中。</p>
<p>5 . 重新执行低优先级任务。此处需要注意是重新执行而不是从之前中断的地方继续执行。毕竟React计算过程中只有当前fiber树和工作中fiber树,执行高优先级时,工作中fiber树已经被更新,所以恢复低优先级任务一定是重新完整执行一遍。</p>
<h3>过期时间ExpirationTime</h3>
<p>作为贯穿整个调度流程的参数,过期时间ExpirationTime的重要性不言而喻。</p>
<p>但在调试过程中,发现expirationTime却不止一种类型。它的值有时是<code>1073741121</code>,有时又是<code>6500</code>,两个值显示对应不同类型。为什么会出现这种情况?</p>
<blockquote>事实上,当前Reac正在重写ExpirationTime的功能,如果后续看到这篇文章发现跟源码差别较大,欢迎阅读我之后写的解读新ExpirationTime功能的文章(立个FLAG先,主要后面expirationTime一块变化应该不小,值得研究)。</blockquote>
<h4>ExpirationTime的变化过程</h4>
<p>以上方优先级插队为例,观察expirationTime值及其相关值的变化。</p>
<ul><li>更新低优先级</li></ul>
<p><img src="/img/remote/1460000022635360" alt="" title=""></p>
<p>在设置更新时,会根据当前优先级和当前时间标记生成对应过期时间标记。</p>
<p>而此后,在确保和安排任务时,会将过期时间标记转换为实际过期时间。</p>
<p>表格的第二第三过程转了一圈,最后还是回到第一次计算的过期时间(因为js同步执行少量代码过程中,performance.now()的变化几乎可以忽略)。</p>
<ul><li>中断低优先级更新,更新高优先级</li></ul>
<p><img src="/img/remote/1460000022635362" alt="" title=""></p>
<p>执行高优先级时,低优先级被中断。而能够让低优先级被恢复的核心逻辑就是最后一个过程(执行更新队列)中对<code>updateExpirationTime</code>(低优先级更新的过期时间标记)和<code>renderExpirationTime</code>(高优先级更新的过期时间标记)的判断。</p>
<p>因为低优先级过期时间标记小于高优先级过期时间标记,即低优先级过期时间大于高优先级过期时间(过期时间标记与过期时间成反比,下面会讲到),表明低优先级更新已经被插队,需要重新执行。所以低优先级更新过期时间标记设为工作中类fiber的过期时间标记。</p>
<ul><li>重新更新低优先级</li></ul>
<p><img src="/img/remote/1460000022635361" alt="" title=""></p>
<h3>过期时间的两种类型</h3>
<p>通过观察expirationTime值的变化过程,可知在设置更新时,计算的expiraionTime为一种标记形式,而到安排任务的时候,任务的expirationTime已变为实际过期时间。</p>
<p><code>expirationTime</code>的2种类型:</p>
<ol>
<li>时间标记:一个极大值,如<code>1073741121</code>
</li>
<li>过期时间:从网页加载开始计时的实际过期时间,单位为毫秒</li>
</ol>
<h4>过期时间标记</h4>
<p>React成员Andrew Clark在"<a href="https://link.segmentfault.com/?enc=G0ft82UBhfs5Z4ZpBtEUEw%3D%3D.SPCYoYOKuQfGr3OIxlV74bJUd5djAk%2Fx945Kc3K%2Bgv9UawIQZ7NxfCE499rgS5c9uaDfu6pgsHG8XIGrNl0%2BDPsrg8T13xSJK2Nl9jdsw1ZJQDVkiVRKudt3zXbRRuPa" rel="nofollow">Make ExpirationTime an opaque type</a> "中提到了expirationTime作为标记的计算方法和作用:</p>
<blockquote>In the old reconciler, **expiration times are computed by applying an<br>offset to the current system time<strong>. </strong>This has the effect of increasing<br>the priority of updates as time progresses**.</blockquote>
<p>他说ExpirationTime是通过给当前系统时间添加一个偏移量来计算,这样的作用是随着时间运行能够提升更新的优先级。</p>
<p>而源码中,expirationTime的确是根据一个最大整数值偏移量来计算:</p>
<p>MAGIC_NUMBER_OFFSET - ceiling(MAGIC_NUMBER_OFFSET - currentTime + expirationInMs / UNIT_SIZE, bucketSizeMs / UNIT_SIZE)</p>
<p>其中:</p>
<ul>
<li>
<code>MAGIC_NUMBER_OFFSET </code>是一个极大常量: <code>1073741 821</code>
</li>
<li>UNIT_SIZE也是常量:<code>10</code>,用来将毫秒值除以10,比如1000毫秒转为<code>1000/10=100</code>,便于展示时间标记</li>
<li>
<code>ceiling(num, unit)</code>的作用是根据单位长度进行特殊向上取整(对基础值也向上取整,比如1.1特殊向上取整后为2,而1特殊向上取整后也为2, 可以理解为 Math.floor( num + 1 ) )</li>
</ul>
<pre><code class="js">function ceiling(num, unit) {
return ((num / unit | 0) + 1) * unit;
}</code></pre>
<p><code>num | 0</code>的作用类似<code>Math.floor(num)</code>, 向下取整,并且加1可以放入括号,所以代码可转换为:</p>
<pre><code class="js">function ceiling(num, unit) {
return Math.floor( num / unit + 1 ) * unit;
}</code></pre>
<p>比如,若单位<code>unit</code>为10,若数值<code>num</code>为:</p>
<ul>
<li><ul><li>10,则返回20</li></ul></li>
<li><ul><li>11,也返回20</li></ul></li>
</ul>
<p>为什么要React要使用特殊向上取整方法?</p>
<p>因为这样可以实现”更新节流“:在单位时间(比如100毫秒)内,保证多个同等优先级更新计算出的<code>expirationTime</code>相同,只执行第一个更新对应的任务(但计算更新时会用到所有更新)。</p>
<p>在确保root被安排好任务的函数中,会判断新的更新<code>expirationTime</code>和正在执行的更新<code>expirationTime</code>是否相同,以及它们的优先级是否相同,若相同,则直接<code>return</code>。从而不会执行第一个更新之后更新对应的任务。</p>
<p>但这并不是说之后的更新都不会执行。由于第一个更新对应任务的执行是异步的(<code>post.postMessage</code>),在第一个更新执行更新队列时,其他更新早已被加入更新队列,所以能确保计所有更新参与计算。</p>
<ul>
<li>
<code>MAGIC_NUMBER_OFFSET - currentTime</code>的值为<code>performance.now()/10</code>
</li>
<li>
<code>expirationInMs </code>表示不同优先级对应的过期时长:</li>
<li><ul><li>普通/低优先级:5秒</li></ul></li>
<li><ul><li>高优先级(用户交互优先级):生产环境下为150毫秒,开发环境下为500毫秒</li></ul></li>
<li><ul><li>立即优先级、空闲优先级不通过上面的公式计算,它们的过期时间标记值分别为<code>1</code>和<code>2</code>,一个表示立即过期,另一个表示永不过期。</li></ul></li>
<li>
<code>bucketSizeMs</code>: 即<code>ceiling(num, unit)</code>中的<code>unit</code>,作为特殊向上取整的单位长度。高优先级为100毫秒,普通/低优先级为250毫秒。</li>
</ul>
<p>为了便于理解,不考虑更新节流,则:</p>
<pre><code class="js">过期时间标记值 = 极大数值 - ( 当前时间 + 优先级对应过期时长 ) / 10</code></pre>
<p>而<code>当前时间 + 优先级对应过期时长</code>就是实际过期时间,所以:</p>
<pre><code class="js">过期时间标记值 = 极大数值 - 过期时间 / 10</code></pre>
<h4>过期时间</h4>
<p>过期时间就是:</p>
<pre><code class="js">当前时间 + 优先级对应过期时长 </code></pre>
<p>过期时间标记转换为过期时间:</p>
<pre><code class="js">function expirationTimeToMs(expirationTime) {
return (MAGIC_NUMBER_OFFSET - expirationTime) * UNIT_SIZE;
}</code></pre>
<h3>源码实探</h3>
<p>写到此处,不知不觉已经过了好几天。对于源码展现这一块,也有了不同的打算。之前计划纯用流程图展现。但因为涉及关键代码量大,流程图不是很适用。所以这次直接用流程叙述+相关源码,直观的实现原理对应源码。</p>
<h4>时间切片源码</h4>
<p>在执行调度工作循环和计算工作循环时,执行每一个工作中Fiber。但是,有一个条件是每隔5毫秒,会跳出工作循环,</p>
<pre><code class="js">function workLoop(...) {
...
while (currentTask !== null && ...) {
....
}
...
}</code></pre>
<blockquote>调度工作循环</blockquote>
<pre><code class="js"> function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}</code></pre>
<blockquote>计算工作循环中,<code>shouldYield()</code>即为检查5毫秒是否到期的条件</blockquote>
<pre><code class="js">shouldYield(...) --> Scheduler_shouldYield(...) --> unstable_shouldYield(...)
--> shouldYieldToHost(...)
--> getCurrentTime() >= deadline
-->
var yieldInterval = 5; var deadline = 0;
var performWorkUntilDeadline = function() {
...
var currentTime = getCurrentTime()
deadline = currentTime + yieldInterval
...
}</code></pre>
<blockquote>
<code>var yieldInterval = 5</code>为每隔5毫秒的体现</blockquote>
<p>运行一次<strong>异步的<code>MessageChannel</code>的<code>port.postMessage(...)</code>方法,检查是否存在事件响应、更高优先级任务或其他代码需要执行</strong>,如果有则执行,如果没有则重新创建工作循环,执行剩下的工作中Fiber。</p>
<pre><code class="js">var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
requestHostCallback = function(callback) {
...
if (...) {
...
port.postMessage(null);
}
}</code></pre>
<p>在执行调度任务过程中,会执行<code>requestHostCallback(...) </code>, 从而调用<code>port.postMessage(...)</code></p>
<h4>调度一个任务源码</h4>
<h5>初始化</h5>
<p>1 . 当出现新的更新,React会运行一个确保root被安排任务的函数。</p>
<pre><code class="js">setState(...) --> enqueueSetState(...)
--> scheduleWork(...) --> ensureRootIsScheduled(...)</code></pre>
<p>2 . 当root的回调函数为空值且新的更新对应的过期时间标记是异步类型,根据当前时间和过期时间标记推断出优先级和计算出timeout,</p>
<pre><code class="js">var currentTime = requestCurrentTimeForUpdate();
var priorityLevel = inferPriorityFromExpirationTime(currentTime, expirationTime);
if (expirationTime === Sync) {
...
} else {
callbackNode = scheduleCallback(priorityLevel, performConcurrentWorkOnRoot.bind(null, root),
{
timeout: expirationTimeToMs(expirationTime) - now()
});
}</code></pre>
<p>然后根据优先级、timeout, 结合执行工作的回调函数,新建一个任务(这里就是<code>scheduleCallback</code>),</p>
<pre><code class="js">function unstable_scheduleCallback(priorityLevel, callback, options) {
...
var expirationTime = startTime + timeout;
var newTask = {
id: taskIdCounter++,
callback: callback,
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
sortIndex: -1
};
...
}</code></pre>
<p>将该任务放入任务队列中,调用DOM调度配置文件中的<code>requestHostCallback</code>,回调函数为调度中心的清空任务方法。</p>
<pre><code class="js">push(taskQueue, newTask);
...
if (...) {
...
requestHostCallback(flushWork);
}</code></pre>
<p><code>flushWork</code>为调度中心的清空任务方法,即将任务队列中的任务执行后然后移除</p>
<h5>运行任务</h5>
<p>1 . <code>requestHostCallback</code>调用MessageChannel中的异步函数:<code>port.postMessage(...)</code>,</p>
<pre><code class="js">var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
requestHostCallback = function (callback) {
scheduledHostCallback = callback;
if (...) {
...
port.postMessage(null);
}
};</code></pre>
<p>从而异步执行之前另一个端口<code>port1</code>订阅的方法,在该方法中,执行<code>requestHostCallback</code>的回调函数,即调度中心的清空任务方法。</p>
<pre><code class="js">var performWorkUntilDeadline = function () {
...
var hasMoreWork = scheduledHostCallback(...);
}</code></pre>
<p>2 .清空任务方法中,会执行调度中心的工作循环,循环执行任务队列中的任务。</p>
<pre><code class="js">function flushWork(...) {
...
return workLoop(...);
...
}</code></pre>
<p>有趣的是,工作循环并不是执行完一次任务中的回调函数就继续执行下一个任务的回调函数,而是执行完一个任务中的回调函数后,检测其是否返回函数。若返回,则将其作为任务新的回调函数,继续进行工作循环;若未返回,则执行下一个任务的回调函数。</p>
<pre><code class="js">function workLoop(...) {
...
while (currentTask !== null && ...) {
var callback = currentTask.callback;
if (callback !== null) {
currentTask.callback = null;
...
var continuationCallback = callback(didUserCallbackTimeout)
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
...
}
} else {
pop(taskQueue)
}
currentTask = peek(taskQueue);
}
...
}</code></pre>
<p>并且工作循环中也在检查5毫秒时间切片是否到期,到期则重新调<code>port.postMessage(...)</code>。</p>
<pre><code class="js">while(currentTask !== null && ...) {
...
if (... && (... || shouldYieldToHost())) {
break;
}
...
}
if (currentTask !== null) {
return true;
}</code></pre>
<pre><code class="js">var hasMoreWork = scheduledHostCallback(...);
if (!hasMoreWork) {
...
} else {
port.postMessage(null);
}</code></pre>
<p>3 . 任务的回调函数是一个执行同时模式下root工作的方法。执行该方法时将循环执行工作中fiber,同样使用5毫秒左右的时间切片进行计算和diff,5毫秒时间切片过期后就会返回其自身。</p>
<pre><code class="js">function performConcurrentWorkOnRoot(...) {
...
do {
try {
workLoopConcurrent();
break;
} catch (...) {
...
}
} while (true);
...
return performConcurrentWorkOnRoot.bind(...);
}</code></pre>
<h5>完成任务</h5>
<p>1 . 在执行完所有工作中fiber后,React进入提交步骤,更新DOM。</p>
<pre><code class="js">finishConcurrentRender(...)-->commitRoot(...)-->commitRootImpl(...)</code></pre>
<p>2 . 任务的回调函数返回空值,调度工作循环因此(运行任务步骤中第二点:若任务的回调函数执行后返回为空,则执行下一个任务)完成此任务,并将此任务从任务队列中删除。</p>
<pre><code class="js">function performConcurrentWorkOnRoot() {
...
if (workInProgress !== null) { ... }
else {
...
finishConcurrentRender(root, finishedWork, workInProgressRootExitStatus, expirationTime);
}
...
return null;
}</code></pre>
<pre><code class="js">function workLoop(...) {
...
while (currentTask !== null && ...) {
var callback = currentTask.callback;
if (callback !== null) {
currentTask.callback = null;
...
var continuationCallback = callback(didUserCallbackTimeout)
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
...
}
} else {
pop(taskQueue)
}
currentTask = peek(taskQueue);
}
...
}</code></pre>
<h4>高优先级插队</h4>
<p>1 . 延用上面的高优先级插队案例,从触发高优先级点击事件(准备插队)开始。</p>
<p>触发点击事件后,React会运行内部的合成事件相关代码,然后执行一个执行优先级的方法,优先级参数为“用户交互UserBlockingPriority”,接着进行<code>setState</code>操作。</p>
<pre><code class="js">onClick --> discreteUpdates
--> runWithPriority(UserBlockingPriority, ...)
-->setState</code></pre>
<p><code>setState</code>的关联方法新建一个更新,计算当前的过期时间标记,然后开始安排工作。</p>
<pre><code class="js">enqueueSetState: function (...) {
...
var expirationTime = computeExpirationForFiber(...);
var update = createUpdate(...);
...
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
}</code></pre>
<p>2 . 在安排工作方法中,运行确保root被安排任务的方法。因为现在的优先级更高且过期时间标记不同,调度中心取消对之前低优先级任务的安排,并将之前低优先级任务的回调置空,确保它之后不会被执行(调度中心工作循环根据当前的任务的回调函数是否为空决定是否继续执行该任务)。</p>
<pre><code class="js">function ensureRootIsScheduled(...) {
if (existingCallbackNode !== null) {
...
cancelCallback(existingCallbackNode);
}
...
}</code></pre>
<pre><code class="js">function unstable_cancelCallback(task) {
...
task.callback = null;
}</code></pre>
<p>然后调度中心根据高优先级更新对应的优先级、过期时间标记、timeout等创建新的任务。</p>
<pre><code class="js">var expirationTime = startTime + timeout;
var newTask = {
...
callback: callback,
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
...
};</code></pre>
<p>3 . 执行高优先级任务,当执行到开始计算工作中类Fiber(<code>class ConcurrentSchedulingExample</code>),执行更新队列方法时,React将循环遍历工作中类fiber的更新环状链表。<strong>当循环到之前低优先级任务对应更新时,因为低优先级过期时间标记小于当前渲染过期时间标记,故将该低优先级过期时间标记设为工作中类fiber的过期时间标记(其他情况会将工作中类fiber的过期时间标记设为0)</strong>。此处是之后恢复低优先级的关键所在。</p>
<pre><code class="js">function processUpdateQueue(...) {
...
var newExpirationTime = NoWork;
...
if (updateExpirationTime < renderExpirationTime) {
if (updateExpirationTime > newExpirationTime) {
newExpirationTime = updateExpirationTime;
}
} else { ... }
...
workInProgress.expirationTime = newExpirationTime
...
}</code></pre>
<blockquote>
<code>NoWork</code>为<code>0</code>
</blockquote>
<p>4 . 在完成优先级任务过程的提交渲染DOM步骤中,渲染DOM后,会将root的callbackNode(其名字容易误导其功能,其实就是调度任务,用callbackTask或许更合适)设为空值。</p>
<pre><code class="js">function commitRootImpl(...) {
...
root.callbackNode = null;
...
}</code></pre>
<p>在接下来执行确保root被安排任务的方法中,因为下一次过期时间标记不为空(根本原因就是上面第二点提到工作中类fiber的过期时间标记被设置为低优先级过期时间标记)且root的callbackNode为空值,所以创建新的任务,即重新创建一个新的低优先级任务。并将任务放入任务列表中。</p>
<pre><code class="js">function ensureRootIsScheduled(...) {
var expirationTime = getNextRootExpirationTimeToWorkOn(...);
if (expirationTime === NoWork) { ... return }
if (expirationTime === Sync) { ... }
else {
callbackNode = scheduleCallback(priorityLevel, performConcurrentWorkOnRoot.bind(null, root),
{
timeout: expirationTimeToMs(expirationTime) - now()
});
}
}</code></pre>
<pre><code class="js">function unstable_scheduleCallback(priorityLevel, callback, options) {
...
var expirationTime = startTime + timeout;
var newTask = {
...
callback: callback,
priorityLevel: priorityLevel,
startTime: startTime,
expirationTime: expirationTime,
...
};
...
push(taskQueue, newTask);
...
}</code></pre>
<p>5 . 重新执行低优先级任务。此处需要注意是重新执行而不是从之前中断的地方继续执行。毕竟React计算过程中只有当前fiber树和工作中fiber树,执行高优先级时,工作中fiber树已经被更新,所以恢复低优先级任务一定是重新完整执行一遍。</p>
<h3>最后写点什么</h3>
<p>此次阅读源码的一些心得:</p>
<p>1 . 先自上而下,再自下而上。</p>
<p>自上而下是先了解源码的整体结构,总的执行流程是怎样,再一层一层往下研究。而自下而上是着重研究某个功能的细节,弄懂细节之后再研究其上层。</p>
<p>2 . 面向问题看源码。</p>
<p>在研究某个功能时,先提出问题,再研究源码解决问题。不过若有问题尝试很久都无法解决,可以先放下,继续研究其他问题,之后再回来解决。</p>
<p>3 . 调试源码。</p>
<p>对于非常简单的功能,一般只看源码就能弄懂。但其他功能,往往只有经过调试才能验证和推理,从而真正弄懂。下一篇会写如何搭建<strong>支持所有React版本断点调试细分文件</strong>的React源码调试环境。</p>
<blockquote>
<strong>感谢你花时间阅读这篇文章。如果你喜欢这篇文章,欢迎点赞、收藏和分享,让更多的人看到这篇文章,这也是对我最大的鼓励和支持!</strong> <p><strong>欢迎通过微信(搜索“苏溪云的博客”或者扫描下方二维码)或<a href="https://link.segmentfault.com/?enc=rWN8g%2BwU6%2FX3zxOkQSM%2B1Q%3D%3D.BAtdpeEmfGlztNO8VCMIA0caOKH0pe%2Bl1vXaHXyeZYhaM7NCOLTf%2FuEkQpVOZSxI" rel="nofollow">Github</a>订阅我的博客。</strong></p>
<p><img src="/img/remote/1460000022198778" alt="微信公众号:苏溪云的博客" title="微信公众号:苏溪云的博客"></p>
</blockquote>
搞懂React源码系列-React Diff原理
https://segmentfault.com/a/1190000022311760
2020-04-09T10:26:06+08:00
2020-04-09T10:26:06+08:00
苏溪云
https://segmentfault.com/u/terry_su
13
<p><img src="/img/remote/1460000022312575" alt="" title=""></p>
<p>时隔2年,重新看React源码,很多以前不理解的内容现在都懂了。本文将用实际案例结合相关React源码,集中讨论React Diff原理。使用当前最新React版本:<code>16.13.1</code>。</p>
<blockquote>
<p>另外,今年将写一个“搞懂React源码系列”,把React<strong>最核心内容</strong>用最通俗易懂地方式讲清楚。2020年搞懂React源码系列:</p>
<ul>
<li>React Diff原理</li>
<li>React 调度原理</li>
<li>搭建阅读React源码环境-支持所有版本断点调试</li>
<li>React Hooks原理</li>
</ul>
<p>欢迎Star和订阅<a href="https://link.segmentfault.com/?enc=PxBlZ%2F%2F9OODFb2tmXyzGAA%3D%3D.bV3sjIVst4w2xaOuAmMONmbdQEHetMYV3CMMG5BYqmbmEErSaxfOpK4qHszYaPjz" rel="nofollow">我的博客</a>。</p>
</blockquote>
<p>在讨论Diff算法前,有必要先介绍React Fiber,因为React源码中各种实现都是基于Fiber,包括Diff算法。当然,熟悉React Fiber的朋友可跳过Fiber介绍。</p>
<h2>Fiber简介</h2>
<p>Fiber并不复杂,但如果要<a href="https://link.segmentfault.com/?enc=hKF5jVMK4URpV9mxM%2BuOPA%3D%3D.6qAf167ML%2FJ7l1u8HKjmQ9g0c0OdJNQ72KmOy6hsCC%2FGPVWWXGsdlykvi88GnB0C" rel="nofollow">全面理解</a>,还是得花好一段时间。本文主题是diff原理,所以这里仅简单介绍下Fiber。</p>
<p><img src="/img/remote/1460000022311763" alt="" title=""></p>
<p>Fiber是一个抽象的节点对象,每个对象可能有子Fiber(child)和相邻Fiber(child)和父Fiber(return),React使用链表的形式将所有Fiber节点连接,形成链表树。</p>
<p>Fiber还有副作用标签(effectTag),比如替换Placement(替换)和Deletion(删除),用于之后更新DOM。</p>
<p>值得注意的是,React diff中,除了fiber,还用到了基础的<a href="https://link.segmentfault.com/?enc=OyLMSioG48NlBrlGnyuDKg%3D%3D.zPazM8xx0%2FnLQRz5NLu4V%2FXF2iFPwsX%2BH9F9mVMwBBcRnehE95WBqkwxSnein7oy" rel="nofollow">React元素对象</a>(如: 将<code><div>foo</div></code>编译后生成的对象: <code>{ type: 'div', props: { children: 'foo' } }</code> )。</p>
<h2>Diff 过程</h2>
<p>React源码中,关于diff要从<code>reconcileChildren(...)</code>说起。</p>
<p>总流程:</p>
<p><img src="/img/remote/1460000022311766" alt="" title=""></p>
<p>流程图中, 显示源码中用到的函数名,省略复杂参数。“新内容”即被比较的新内容,它可能是三种类型:</p>
<ul>
<li>对象: React元素</li>
<li>字符串或数字: 文本</li>
<li>数组:数组元素可能是React元素或文本</li>
</ul>
<h2>新内容为React元素</h2>
<p>我们先以新内容为React元素为例,全面的调试一遍代码,将之后会重复用到的方法在此步骤中讲解,同时以一张流程图作为总结。</p>
<p>案例:</p>
<pre><code class="js">
function SingleElementDifferentTypeChildA() { return <h1>A</h1> }
function SingleElementDifferentTypeChildB() { return <h2>B</h2> }
function SingleElementDifferentType() {
const [ showingA, setShowingA ] = useState( true )
useEffect( () => {
setTimeout( () => setShowingA( false ), 1000 )
} )
return showingA ? <SingleElementDifferentTypeChildA/> : <SingleElementDifferentTypeChildB/>
}
ReactDOM.render( <SingleElementDifferentType/>, document.getElementById('container') )
</code></pre>
<p>从第一步<code>reconcileChildren(...)</code>开始调试代码,无需关注与diff不相关的内容,比如<code>renderExpirationTime</code>。左侧调试面板可看到对应变量的类型。</p>
<p><img src="/img/remote/1460000022311764" alt="" title=""></p>
<p>此处:</p>
<ul>
<li>
<code>workInProgress</code>: 父级Fiber</li>
<li>
<code>current.child</code>: 处于比较中的旧内容对应fiber</li>
<li>
<code>nextChildren</code>: 即处于比较中的新内容, 为React元素,其类型为对象。</li>
</ul>
<p><strong>在Diff时</strong>,比较中的<strong>旧内容为Fiber</strong>,而比较中的<strong>新内容为React元素、文本或数组</strong>。其实从这一步已经可以看出,React官网的<a href="https://link.segmentfault.com/?enc=Ycgh1VoFWu%2FIti%2BPb%2BfQUg%3D%3D.88Tm52DetC08wGJL7MAJ9wWpYTaEAdoap7A6MuOcczmk3WxQQuq%2BkqwoAvq4ynMrSdXT%2FDkEDuHASYuJIaLlbCwxf9lWegMhhyw2cILMF8k%3D" rel="nofollow">diff算法说明</a>和实际代码是实现差别较大。</p>
<p><img src="/img/remote/1460000022311765" alt="" title=""></p>
<p>因为新内容为对象,所以继续执行<code>reconcileSingleElement(...)</code>和<code>placeSingleChild(...)</code>。</p>
<p>我们先看<code>placeSingleChild(...)</code>:</p>
<p><img src="/img/remote/1460000022311767" alt="" title=""></p>
<p><code>placeSingleChild(...)</code>的作用很简单,给differ后的Fiber添加副作用标签:Placement(替换),表明在之后需要将旧Fiber对应的DOM元素进行替换。</p>
<p>继续看 <code>reconcileSingleElement(...)</code>:</p>
<p><img src="/img/remote/1460000022311768" alt="" title=""></p>
<p><strong>此处正式开始diff(比较)</strong>,child为旧内容fiber,element为新内容,它们的<strong>元素类型</strong>不同。</p>
<p><img src="/img/remote/1460000022311770" alt="" title=""></p>
<p><img src="/img/remote/1460000022311769" alt="" title=""></p>
<p>因为类型不同,React将“删除”旧内容fiber以及其所有相邻Fiber(即给这些fiber添加副作用标签 Deletion(删除)), 并基于新内容生成新的Fiber。然后将新的Fiber设置为父Fiber的child。</p>
<p>到此,一个新内容为React元素的且新旧内容的元素类型不同的Diff过程已经完成。</p>
<p>那如果新旧内容的元素类型相同呢?</p>
<p>编写类似案例,我们可以得到结果</p>
<p><img src="/img/remote/1460000022311771" alt="" title=""></p>
<p><code>userFiber(...)</code>:</p>
<p><img src="/img/remote/1460000022311772" alt="" title=""></p>
<p><code>userFiber(...)</code>的主要作用是基于旧内容fiber和新内容的属性(props)克隆生成一个新内容fiber,这也是所谓的fiber复用。</p>
<p>所以当新旧内容的元素类容相同,React会复用旧内容fiber,结合新内容属性,生成一个新的fiber。同样,将新的fiber设置位父fiber的child。</p>
<p>新内容为React元素的diff流程总结:</p>
<p><img src="/img/remote/1460000022311782" alt="" title=""></p>
<h2>新内容为文本</h2>
<p>当新内容为文本时,逻辑与新内容为React元素时类似:</p>
<p><img src="/img/remote/1460000022311778" alt="" title=""></p>
<h2>新内容为数组</h2>
<p>使用案例:</p>
<pre><code class="js">
function ArrayComponent() {
const [ showingA, setShowingA ] = useState( true )
useEffect( () => {
setTimeout( () => setShowingA( false ), 1000 )
} )
return showingA ? <div>
<span>A</span>
<span>B</span>
</div> : <div>
<span>C</span>
D
</div>
}
ReactDOM.render( <ArrayComponent/>, document.getElementById('container') )
</code></pre>
<p><img src="/img/remote/1460000022311779" alt="" title=""></p>
<p>若新内容为数组,需<code>reconcileChildrenArray(...)</code>:</p>
<p><img src="/img/remote/1460000022311773" alt="" title=""></p>
<p>for循环遍历新内容数组,伪代码(用于理解):</p>
<pre><code class="js">for ( let i = 0, oldFiber; i < newArray.length; ) {
...
i++
oldFiber = oldFiber.sibling
}</code></pre>
<p>遍历每个新内容数组元素时:</p>
<p><img src="/img/remote/1460000022311774" alt="" title=""></p>
<p><code>updateSlot(...)</code>:</p>
<p><img src="/img/remote/1460000022311775" alt="" title=""></p>
<p>因为<code>newChild</code>的类型为<code>object</code>, 所以:</p>
<p><img src="/img/remote/1460000022311776" alt="" title=""></p>
<p><code>updateElement(...)</code>:</p>
<p><img src="/img/remote/1460000022311785" alt="" title=""></p>
<p><code>updateElement(...)</code>与<code>reconcileSingleElement(...)</code>核心逻辑一致:</p>
<ul>
<li>若新旧内容元素类型一致,则克隆旧fiber,结合新内容生成新的fiber</li>
<li>若不一致,则基于新内容创建新的fiber。</li>
</ul>
<p>同理,<code>updateTextNode(...)</code>:</p>
<p><img src="/img/remote/1460000022311780" alt="" title=""></p>
<p><code>updateTextNode(...)</code>与<code>reconcileSingleTextNode(...)</code>核心逻辑一致:</p>
<ul>
<li>若旧内容fiber的标签不是<code>HostText</code>,则基于新内容文本创建新的fiber</li>
<li>若是<code>HostText</code>, 则克隆旧fiber,结合新内容文本生成新的fiber</li>
</ul>
<p>在本案例中,新内容数组for循环完成后:</p>
<p><img src="/img/remote/1460000022311777" alt="" title=""></p>
<p>因为新旧内容数组的长度一致,所以直接返回第一个新的fiber。然后同上,React将新的fiber设为父fiber的child。</p>
<p>不过若新内容数组长度与旧内容fiber及其相邻fiber的总个数不一致,React如何处理?</p>
<p>编写类似案例。</p>
<p>若新内容数组长度更短:</p>
<p><img src="/img/remote/1460000022311783" alt="" title=""></p>
<p>React将删除多余的旧内容fiber的相邻fiber。</p>
<p>若新内容数组长度更长:</p>
<p><img src="/img/remote/1460000022311781" alt="" title=""></p>
<p>React将遍历多余的新内容数组元素,基于新内容数组元素创建的新的fiber,并添加副作用标签 Placement(替换)。</p>
<p>新内容为数组时的diff流程总结:</p>
<p><img src="/img/remote/1460000022311784" alt="" title=""></p>
<h2>总结</h2>
<p>通过React源码研究diff算法时,仅调试分析<strong>相关代码</strong>,能比较容易的得出答案。</p>
<p>Diff的三种情况:</p>
<ol>
<li>新内容为React元素</li>
<li>新内容为文本</li>
<li>新内容为数组</li>
</ol>
<p>Diff时若比较结果相同,则复用旧内容Fiber,结合新内容生成新Fiber;若不同,仅通过新内容创建新fiber。</p>
<p>然后给旧内容fiber添加副作用替换标签,或者给旧内容fiber及其所有相邻元素添加副作用删除标签。</p>
<p>最后将新的(第一个)fiber设为父fiber的child。</p>
<h2>参考资料</h2>
<ul>
<li>The how and why on React’s usage of linked list in Fiber to walk the component’s tree: <a href="https://link.segmentfault.com/?enc=bTwPiNd%2BoOmvMF8bsZVT1w%3D%3D.g0iLmEs%2FmcAcOkoEdkLontX1m7FfcxAvu6VeIq1vt37mkbHjT%2FX96hKrNNjhGx%2FAbhl49ksg2P%2FtkjAW1bg0M54dhSJbxkHC7%2FVNA3Cxs9GkHhgRj1YWAhrJIKKlNlXgUZoMBTyiPSb8TEpwXePKtA%3D%3D" rel="nofollow">https://medium.com/react-in-d...</a>
</li>
<li>[译]深入React fiber架构及源码: <a href="https://link.segmentfault.com/?enc=M%2FDWpmFzUPo2GLOXbESTXA%3D%3D.dnumC3r3bUWOJeS5Un75e9xNPb71zFzulU5f03JEmF3w2zijtG9ILMUnOJq%2FajqV" rel="nofollow">https://zhuanlan.zhihu.com/p/...</a>
</li>
<li>Inside Fiber: in-depth overview of the new reconciliation algorithm in React: <a href="https://link.segmentfault.com/?enc=lU1c50IDPdPqpgfjXdu35A%3D%3D.slQh3xKiVTqO9%2B0e0E4mCN99mCdfZPosZ0bjHnDtxgA%2BMYw8oWBOgG09CwbDBADRsaCdzXFrxPIybsE5iqmbfEtwvuPP1TG9lymbVUgYlZsBz%2Fhdxd7RFE27zSf3M8VdTjzZDPjFF7czkS0fSRV4CCDqoq2yu5f9ZY5r6eKBJdo%3D" rel="nofollow">https://medium.com/react-in-d...</a>
</li>
</ul>
<p><strong>感谢你花时间阅读这篇文章。如果你喜欢这篇文章,欢迎点赞、收藏和分享,让更多的人看到这篇文章,这也是对我最大的鼓励和支持!</strong> </p>
<p><strong>欢迎通过微信(扫描下方二维码)或<a href="https://link.segmentfault.com/?enc=mwCb45U5mAY15lrUq1xrVQ%3D%3D.l9Se7oXBVYaV%2FKRflZ9oWeE4qrpbAhiOGjXl8t2a96Oj62IHXMocBIFKqC1fyHJ3" rel="nofollow">Github</a>订阅我的博客。</strong></p>
<p><img src="/img/remote/1460000022198778" alt="微信公众号:苏溪云的博客" title="微信公众号:苏溪云的博客"></p>
轻松学会HTTP缓存(强缓存,协商缓存)
https://segmentfault.com/a/1190000022198770
2020-03-30T11:05:18+08:00
2020-03-30T11:05:18+08:00
苏溪云
https://segmentfault.com/u/terry_su
18
<p>若读者对“强缓存”,“协商缓存”字眼非常熟悉,但又不知道他们具体是什么,亦或有读者还不了解HTTP缓存,那么本文将为读者一一讲解。</p>
<blockquote>欢迎Star和订阅<a href="https://link.segmentfault.com/?enc=zHafCMzCNMWDGJSI2hu79w%3D%3D.iZsaqOF1gCtqGPr8Ys8yIm9JXTK6ix6aCGhCPNphdYKj7eFLAm4nDWaKOA%2F9S4CB" rel="nofollow">我的博客</a>。</blockquote>
<h2>HTTP缓存流程</h2>
<p>在介绍什么是强缓存、协商缓存前,让我们先了解HTTP缓存的流程,因为强缓存、协商缓存只是其中2步。</p>
<p><img src="/img/remote/1460000022198774" alt="image" title="image"></p>
<h2>强缓存</h2>
<p>“检查缓存是否过期”一步即强缓存。若缓存未过期,直接使用浏览器本地缓存,不用请求服务器。 </p>
<p>检查缓存是否过期依据请求报文中的2种首部:过期时间<code>Expires</code>和有效时间<code>Cache-Control:max-age</code>。例子:</p>
<ul>
<li><code>Expires: Fri, 05, Jul, 2020, 05:00:00 GMT</code></li>
<li><code>Cache-Control: max-age=60000</code></li>
</ul>
<p>前者为缓存具体的过期时间,后者为缓存有效期。<code>Cache-Control: max-age</code>的优先级高于<code>Expires</code>。</p>
<h2>协商缓存</h2>
<p>“协商缓存”可以理解为一个动作:“与服务器协商是否更新缓存”。</p>
<p>当检查到缓存已过期,缓存端需要与服务器协商是否更新缓存。在请求报文中,用于协商的条件类首部也有2种,时间再验证<code>If-Modified-Since</code>和实体标签再验证<code>If-No-Matched</code>。若条件为真,服务器会返回新文档给缓存。否则,服务器返回304(Not Modified)。它们的格式为:</p>
<ul>
<li><code>If-Modified-Since: <date></code></li>
<li><code>If-None-Matched: <tags></code></li>
</ul>
<p>日期再验证<code>If-Modified-Since</code>从字面即可理解:如何从某个时间之后文档被修改过。</p>
<p>实体标签再验证<code>If-None-Matched</code>同样可理解为:若缓存端的实体标签Etag(Entity Tag)与服务器不匹配。</p>
<p>实体标签是什么? 这里要从既然有了日期再验证为何还需要实体标签验证说起。</p>
<p>考虑一种特殊情况,若验证时,发现服务器上的文档被重写过文件修改时间,但内容不变,那这个时候日期再验证不通过,但实际并没有必要更新文档。所以引入了实体标签验证。实体标签Etag是为文档提供的特殊标签,格式为字符串,可看作唯一id。</p>
<p>若实体标签再验证不通过,服务器会返回新文档和新的Etag给缓存。</p>
<p>实体标签再验证的优先级高于日期再验证。</p>
<h2>客户端刷新和重载</h2>
<p>那么客户端的刷新和重载如何影响HTTP缓存?事实上,每个浏览器都由自己的一套处理机制。一般来说,普通刷新不会影响缓存,但强制刷新(重载)会让缓存失效,重新向请求服务器文档。</p>
<h2>实践</h2>
<p>光有理论没有实践验证肯定不够。此处使用一个案例体验协商缓存。</p>
<p>新建一个文件夹,新建index.html, 内容为“Test Cache”。使用<a href="https://link.segmentfault.com/?enc=yMInwybTz%2BCnSnrYiuLpGw%3D%3D.NOl%2BBfLpUG2oHYWbcn%2B%2FLJd5ChhQFnIgyWgDGTrqOpM%3D" rel="nofollow">serve</a>将该文件夹静态服务化。打开Chrome,新建标签页,打开开发人员工具,切换到网络模块,然后打开服务化后的地址: <code>http://localhost:5000</code>。<br><img src="/img/remote/1460000022198773" alt="image" title="image"><br>可看到服务返回状态为200。<br>接下来刷新页面。</p>
<p><img src="/img/remote/1460000022198776" alt="image" title="image"><br>服务器返回状态变为304(Not Modified)。</p>
<p><img src="/img/remote/1460000022198775" alt="image" title="image"><br>请求首部用的是实体标签再验证<code>If-None-Match:<tag></code>。</p>
<p><img src="/img/remote/1460000022198777" alt="image" title="image"><br>响应首部返回的Etag与请求中的Etag相同。</p>
<h2>总结</h2>
<p>HTTP缓存的2个要点就是:</p>
<ol>
<li>检查缓存是否过期(强缓存)</li>
<li>若缓存过期,与服务器协商是否更新缓存(协商缓存)。</li>
</ol>
<p>而这2点每个都包含相关的2个报文请求首部:</p>
<ul>
<li>强缓存:过期时间<code>Expires</code> 和有效期<code>Cache-Control: max-age</code>
</li>
<li>协商缓存:日期再验证<code>If-Modified-Since</code>和实体标签再验证<code>If-Not-Matched</code>
</li>
</ul>
<h2>参考资料</h2>
<ul>
<li>《HTTP权威指南》 > 第7章 > 缓存</li>
<li><a href="https://link.segmentfault.com/?enc=3%2BJmhMntg%2BZFZ0vx3v0p5g%3D%3D.P2U8YG8dyRfKaiKEFuTmIpGugjfIxTDn8%2BrHXtmWzy23uP%2F8vw4QDQSQzmvoR%2FJj" rel="nofollow">强缓存和协商缓存区别和过程</a></li>
</ul>
<p><strong>感谢你花时间阅读这篇文章。如果你喜欢这篇文章,欢迎点赞、收藏和分享,让更多的人看到这篇文章,这也是对我最大的鼓励和支持!</strong> </p>
<p><strong>欢迎通过微信(扫描下方二维码)或<a href="https://link.segmentfault.com/?enc=MvTRZDouN6wJrO6FmHO1pw%3D%3D.dORh52OqSQ8TKeGyqY0m07dwkGijW0kmu%2Fdc1gwnzCLglZb711H0wfokrSxdMoBQ" rel="nofollow">Github</a>订阅我的博客。</strong></p>
<p><img src="/img/remote/1460000022198778" alt="微信公众号:苏溪云的博客" title="微信公众号:苏溪云的博客"></p>
看完Webpack源码,我学到了这些
https://segmentfault.com/a/1190000021585435
2020-01-15T09:20:14+08:00
2020-01-15T09:20:14+08:00
苏溪云
https://segmentfault.com/u/terry_su
31
<p><img src="/img/remote/1460000021585439" alt="" title=""></p>
<p>继React,Vue,这是第三个着重阅读源码的前端项目-Webpack。本文主要以:</p>
<ul>
<li>WHY: 为何要看Webpack源码</li>
<li>HOW: 如何阅读Webpack源码</li>
<li>WHAT: 看完源码后学到了什么</li>
</ul>
<p>三个方向展开。</p>
<blockquote>欢迎Star和订阅<a href="https://link.segmentfault.com/?enc=jdIv0AKC7mLalX2vX4TpQw%3D%3D.2DVwmmsNQFB43CgSdtqTUVvfh9q3SwhQyKio0RpnEf5xprWkVqKICQlb9Fq%2FCKvZ" rel="nofollow">我的博客</a>。</blockquote>
<h2>WHY</h2>
<p>诚然Webpack这是一个前端工程化工具,理解容易, 使用简单,似乎没有深入研究的必要。那为什么还要费心费力阅读其源码?这,把正在写此篇文章的我也问住了。理提纲时,认为WHY最好写,几句话就可带过,但事实证明真要较真这一块还值得一说。 <br>擅自揣测下会阅读Webpack源码伙伴可能的动机:</p>
<ol>
<li>丰富工作经验</li>
<li>技术真爱粉,知其然亦须知其所以然,同时学习方法</li>
<li>与工作或个人项目相关,参考学习</li>
<li>看有人写相关文章,也看看了解下</li>
</ol>
<p>作者最先是原因是4,然后是1,2。当然,1,2应该是大多数人看项目源码的动机。</p>
<h2>How</h2>
<h3>搭建源码调试环境</h3>
<p>要阅读源码,首先拿到源码,然后最后能边调试边阅读。当然,如果智力和推理能力惊人,大可以直接在Github上在线阅读。<br>有2种方法下载源码。一种是最常见的git clone,将Github上webpack项目clone到本地,pull后与webpack官方最新代码保持一致,一劳永逸。不过作者尝试第一种方法时,总是clone不下来,很大可能是由于webpack源文件过大且github服务器clone一直很慢。<br>于是退而求其次,使用第二种方法:下载Webpack源码release版本。选择一个打算阅读的webpack源码版本,直接下载"Source code(zip)"即可。速度非常快,因为不包含.git。<br><img src="/img/remote/1460000021585441" alt="image" title="image"></p>
<p>IDE作者使用VSCode,调试node很方便。拿到源码后,在目录新建一个文件夹,写一个简单的webpack案例,然后使用VSCode进行调试。 <br>不过,在实际操作中,直接使用下载源码中的webpack.js调试可能会出现报错<code>Cannot find module 'json-parse-better-errors'</code>或<code>Cannot find module 'webpack/lib/RequestShortener'</code>,只需运行<code>npm install webpack webpack-cli --save-dev</code>,即可解决报错,且不影响调试源码。</p>
<p><img src="/img/remote/1460000021585440" alt="image" title="image"></p>
<p>此附作者在调试时<a href="https://link.segmentfault.com/?enc=o%2FXbEDVZKWs0tbTzBboQAw%3D%3D.aUuRyHPN9hd9b%2B0uph%2BQQTTmv%2Bzm7pwPxqQw4ClQaYH1npv%2FUo%2BUOjgM3OfmA1qvs0Lixc2JaLgOTc0oVB535Q%3D%3D" rel="nofollow">使用版本</a>参考,下载后使用VSCode打开webpack-4.41.4(modified),安装依赖,安装webpack和webpack-cli,按F5即可启动调试。</p>
<h2>调试,理清大致脉路走向</h2>
<p>Webpack源码量庞大,把每一行代码都读懂确实没有必要,但是我们至少要知道它的整体运行流程,知道它反复用到的核心代码,以及各个模块的生命周期如何运转。</p>
<h3>找核心功能源码</h3>
<p>代码量大,想要在走整体流程时恰好找核心功能的源码,困难重重,至少对于webpack源码是这样,因为其独特的插件和回调结构。<br>不过,我们可以根据每一个想要了解的核心功能,单独去寻找和阅读相关源码。比如,如果我们想看webpack如何打包生成bundle.js,可通过webpack一定会调用NodeJS文件系统输出文件方法,全局搜索"writeFile"找到相关代码,或通过bundle.js中的关键字"// Check if module is in cache"进行搜索。</p>
<p><img src="/img/remote/1460000021585442" alt="image" title="image"></p>
<h2>What</h2>
<p>通过边调试边阅读代码,了解代码整体走向以及webpack如何打包生成bundle.js,作者学到了以下内容:</p>
<ul>
<li>tapable插件机制</li>
<li>简化版Webpack运行流程</li>
<li>bundle.js内容如何生成</li>
</ul>
<h3>Tapable</h3>
<p><a href="https://link.segmentfault.com/?enc=%2BAQtoS8KzpHS5mTTApFl3w%3D%3D.Ldrt9haI5iqP9mhk1fSUw25C9ho2Pr%2Brlzgc7EETv7Lay7Qa8ujOyZ%2BHGIxieFVW" rel="nofollow">Tapable</a>在源码中应用随处可见,要了解源码,首先得学习tapable机制。其实它并不复杂,并且我们只需要知道它的基本作用和用法即可。<br>Tapable 可理解为一套钩子回调函数机制,每一个钩子可订阅多个函数,发布钩子时会运行该钩子订阅该的多个函数。<br>用一个简单案例说明。</p>
<pre><code class="js">const { SyncHook } = require('tapable')
class Car {
constructor() {
this.hooks = {
// # 添加一个钩子
start: new SyncHook()
}
}
}
const car = new Car()
// start钩子订阅一个函数
car.hooks.start.tap( 'run slowly', () => console.log('start running slowly') )
// start钩子订阅另一个函数
car.hooks.start.tap( 'run mediumly', () => console.log('start running mediumly') )
// 发布钩子
car.hooks.start.call() // 输出: run slowly run mediumly</code></pre>
<h3>简化版Webpack运行流程</h3>
<p><img src="/img/remote/1460000021585438" alt="4" title="4"></p>
<h3>bundle.js内容如何生成</h3>
<p>未压缩的bundle.js文件结构一般如下:</p>
<pre><code class="js">/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
....</code></pre>
<p>那么Webpack如何生成这些内容?<br>其实Webpack对于内容分两步处理,第一步先通过loader(默认为babel-loader)生成组合JS代码。第二步将组合JS代码放入webpack默认函数中,从而避免变量泄露。<br>如打包前: <br><code>foo.js</code></p>
<pre><code class="js">export const foo = () => 'hello foo!'</code></pre>
<p><code>bar.js</code></p>
<pre><code class="js">import { foo } from './foo.js'
foo()
console.log( 'hello bar!' )</code></pre>
<p>打包第一步,通过loader(默认为babel-loader)生成组合JS代码:</p>
<pre><code class="js">...
const foo = () => 'hello foo!'
...
\r\n__WEBPACK_MODULE_REFERENCE__0_666f6f_call__()\r\nconsole.log( 'hello bar!' )
...</code></pre>
<p>打包第二步,组合JS代码放入webpack默认函数中。</p>
<pre><code class="js">/******/ (function(modules) { // webpackBootstrap\n
...
const foo = () => 'hello foo!'
...
foo()
console.log( 'hello bar!' )
...</code></pre>
<p>值得注意的是,核心文件为<code>ConcatenatedModule.js</code>, 通过遍历<code>modulesWithInfo</code>从而生成打包代码。</p>
<h2>常见问题</h2>
<p>1 . <code>runtime</code>是什么? </p>
<p>不管在webpack源码,还是Vue源码和其他地方,runtime经常出现。runtime究竟是什么?<br>经过反复查阅资料和推敲,runtime代码可以理解为编译后生成的代码。比如,对于React,runtime代码就是编译JSX代码后生成的JS代码。对于Vue,runtime代码则是编译template,script,style后生成的JS代码。</p>
<p>2 . 热更新,Code Splitting, Tree-shaking等是如何实现? </p>
<p>Webpack内容较多,核心模块原理也不少,比如loader如何运转,Code Splitting如何实现,Tree-Shaking和热加载又是怎么做到的。但毕竟时间有限,此次阅读源码的目标不是大而全的弄懂所有内容,而是掌握Webpack的主要运转流程以及了解较为感兴趣的几个模块。所以其他模块原理以后有机再加入此文。对相应模块模块感兴趣的伙伴可网上先自行搜索相关内容。</p>
<h2>阅读源码资源推荐</h2>
<ul>
<li><a href="https://link.segmentfault.com/?enc=5%2BWLbYPOTbGEKt7U4meTgw%3D%3D.dhSOO1qudmHPDRubTQsqwjpe6YyatYta5Li%2FOPeFVu5N6OLvNvk8jXBaY81QM40qGQ4i1Pyqd4Im5cn1f5uk0Pp%2FN%2Fb51vi0QjLm%2F9dYPWtHrrg828mLKQSTgK8iuF8D" rel="nofollow">how-react-works.pdf</a></li>
<li><a href="https://link.segmentfault.com/?enc=Ec75UqdkAuBc2wKGhV2LfQ%3D%3D.4RMZMDlIEzEzmuHcS1vO2K5UEzzQtCu0McZxtdySNuM%3D" rel="nofollow">深入浅出webpack-吴浩麟</a></li>
<li><a href="https://link.segmentfault.com/?enc=JjW0Ag6kVCkJ%2BnRiDcRlXA%3D%3D.gPxZrouTtjHeu%2BO9LvXeW8CxYsULamm5O0rzTk8oOxO8q0gBMaW44ZxbI66SKGWM7VkDq2vzLSZ%2Fbcie0rOfLQ%3D%3D" rel="nofollow">help developers better understand how webpack works: artsy-webpack-tour</a></li>
<li><a href="https://link.segmentfault.com/?enc=sgYmwaj0tv%2Fb98RBiTrn1g%3D%3D.ElrfUA1PKg6XJKv9cWQ1xjRXnj%2BEo0vgDItSwcPvEsS3a33BjkD%2B3gYN%2Ftcf5jEt" rel="nofollow">build your own webpack</a></li>
</ul>
<p><strong>感谢你花时间阅读这篇文章。如果你喜欢这篇文章,欢迎点赞、收藏和分享,让更多的人看到这篇文章,这也是对我最大的鼓励和支持!</strong> <br><strong>欢迎Star和订阅<a href="https://link.segmentfault.com/?enc=IwswXfp4F4pvfFaPhz3p%2Bg%3D%3D.5wve0VimvfIeI2J2f37raxKCSWQ2gHEOHHiR2pKAx8DWcoqNltszH6PGOdCFCCUS" rel="nofollow">我的原创前端技术博客</a>。</strong></p>
记一次前端项目重构要点总结
https://segmentfault.com/a/1190000019660077
2019-07-04T08:58:42+08:00
2019-07-04T08:58:42+08:00
苏溪云
https://segmentfault.com/u/terry_su
14
<p><img src="/img/remote/1460000019660483" alt="" title=""></p>
<p>不知不觉已是2019年的7月,恍惚之间已工作四年。懵懵懂懂的成长,间歇性努力,实话说,对现在自己取得的成果不大满意。不过,好在时不时顿悟,知道适时作出改变。</p>
<p>此后发文会适当记录一些心路历程,与君共勉。</p>
<blockquote>欢迎Star和订阅<a href="https://link.segmentfault.com/?enc=iLFa6DLn725VQa7xLMcTpg%3D%3D.8oYNIOrxvJsPcjDCkyN1o97%2B2HFMt1FxBMzBtLPt9T6FONXL1GmTTuQ6fL7hz3i8" rel="nofollow">我的博客</a>。</blockquote>
<p>本文要点:</p>
<ol>
<li>什么项目,为何会重构?</li>
<li>怎么重构的?</li>
<li>重构前后对比</li>
</ol>
<h2>什么项目,为何会重构?</h2>
<p>项目是公司主打业务产品之一的可视化子项目,与其他子项目几乎没有耦合,所以可以单独拎出来重构。具体业务不作过多描述,主要重构内容为系统组件和一个<a href="https://link.segmentfault.com/?enc=XwIeuOnHHvFf7kFCDRKo0w%3D%3D.GPPU7LQobV4CVcGh0iTDg%2FMLyXFHAVEmDvcaTdUnrWTd8FmddMrmNlV1tKnKs16s" rel="nofollow">自研可视化图形库</a>。重构总共耗时30个工作日。</p>
<p>技术主要用Vue2系列和JavaScript。第一个重构原因就是没有引入静态类型,导致查看一个对象结构需要翻来覆去在多个文件中查找。第二是因为之前新增代码模式一般为:“来一个需求加一段代码”,长期积累导致代码结构混乱,可读性差。第三是各个状态模块耦合度高,加大了代码维护难度。</p>
<h2>怎么重构的?</h2>
<p><strong>一、在JavaScript中使用TypeScript</strong>。“什么?在JS中使用TS? 闻所未闻。 ” 在看到TS官网手册最后一条<a href="https://link.segmentfault.com/?enc=l3X%2F7BRlQXKlM5bVNDbnOw%3D%3D.AlQJTEHVNgmyCtXl6N9ayY4EpiHaIXR8V0OFFR8ETx8tfXE4cs%2FPFjzGk1%2Be9tdC5DrijewRa26edQzMD1rxlygOwur8NoaxqZrz1d87L%2B8Q43Z4kXOaQbQCdZOT%2B5Ot" rel="nofollow">"Type Checking JavaScript File"</a>之前,我也这样想。其实,TS和VSCode(一款IDE)结合,也可以实现静态类型检测,只不过使用注释形式,一样支持<code>tsconfig.json</code>和自定义Typing。</p>
<pre><code class="ts">type TypeApple = { name: string, count: number }</code></pre>
<pre><code class="js">/** @type {TypeApple} */
const apple = { name: 'foo', count: 100 }</code></pre>
<p><strong>二、细化模块分类</strong>。一般情况下,模块都会有耦合。但如果耦合度过高,往往是因为模块没有细分到位。如果细化模块?举例,假如有一个模块叫<code>Operation</code>,里面既包含操作相关逻辑,也有操作面板逻辑。随着业务发展,操作面板逻辑越来越多。我们完全可以将操作面板逻辑单独抽成一个模块<code>OperationPanel</code>。</p>
<p><strong>三、解耦可视化库和Vue/Vuex</strong>。写业务的时候,很容易因为方便,在Vue组件或Vuex模块中代码越写越长,越来越难维护。这个项目也不列外。所以重构的时候,单独将可视化库相关逻辑抽成模块,并使用类Vuex写法(state, getters, mutations, actions)进行管理。</p>
<pre><code class="js">class Counter {
// # state
/** @type {number} */
count = 0
// # getters
get countText() { return `Count is: ${ this.count }` }
// # mutations
/** @param {number} count*/
SET_COUNT = count => { this.count = count }
// # actions
/** @param {number} count*/
logCount = ( count ) => {
this.SET_COUNT( count )
console.log( this.countText )
}
}</code></pre>
<p><strong>四、最后一条,编写可维护性高的代码</strong>。这里说两个方法。</p>
<p><strong>第一个是“使用<code>Map</code>”</strong>。处理“一个有多类型的数据”需要使用判断,常见有3种方法:<code>If</code>, <code>Switch</code>, <code>Map</code>。<code>If</code>的使用简单粗暴,容易理解。</p>
<pre><code class="js">if ( animalType === 'dog' ) {
console.log( 'Wang!' )
} else if ( animalType === 'cat' ) {
console.log( 'Miao!' )
} else if ( animalType === 'bird' ) {
console.log( 'Jiu!' )
}</code></pre>
<p><code>Switch</code>可以看做是<code>If</code>的简化。</p>
<pre><code class="js">switch ( animalType ) {
case 'dog':
console.log( 'Wang!' )
break
case 'cat':
console.log( 'Miao!' )
break
case 'bird':
console.log( 'Jiu!' )
break
}</code></pre>
<p>而<code>Map</code>针对性最强,并且最简洁、最易于维护。</p>
<pre><code class="js">const logMap = {
dog: () => console.log( 'Wang!' ),
cat: () => console.log( 'Miao!' ),
bird: () => console.log( 'Jiu!' ),
}
logMap[ animalType ]()</code></pre>
<p>具体使用也哪一种因场景而异,但多数场景下,使用<code>Map</code>可读性更强。</p>
<p><strong>第二个是“使用getters和mutations”</strong>。比如定义一个模块的<code>`:</code>operationGetters.js`, 里面提供各种用来获取与操作有关的常量和方法。</p>
<pre><code class="js">export const OPERATION_TYPE_A = 0
export const OPERATION_TYPE_B = 1
export const OPERATION_TITLE_MAP = {
[ OPERATION_TYPE_A ]: 'Title A',
[ OPERATION_TYPE_B ]: 'Title B',
}
export const getOperationTitleByType = type => OPERATION_TITLE_MAP[ type ]</code></pre>
<p>定义<code>mutations</code>则是定义一个提供相关各种变更数据方法的文件。在维护代码的时候,查找变更方法名即可直接找到更改数据的出处。</p>
<pre><code class="js">export const SET_OPERATION_TITLE = ( operation, title ) => { operation.title = title }</code></pre>
<h2>重构前后对比</h2>
<table>
<thead><tr>
<th>代码类型</th>
<th><strong>重构前</strong></th>
<th><strong>重构后</strong></th>
</tr></thead>
<tbody>
<tr>
<td>可视化工具库</td>
<td>245kb</td>
<td>214kb</td>
</tr>
<tr>
<td>组件</td>
<td>171kb</td>
<td>157kb</td>
</tr>
<tr>
<td>状态管理</td>
<td>62kb</td>
<td>60kb</td>
</tr>
<tr>
<td>模块模型</td>
<td>15kb</td>
<td>19kb</td>
</tr>
<tr>
<td>服务请求</td>
<td>12kb</td>
<td>4kb</td>
</tr>
<tr>
<td>常量</td>
<td>18kb</td>
<td>22kb</td>
</tr>
<tr>
<td>工具(Utils)</td>
<td>15kb</td>
<td>19kb</td>
</tr>
<tr>
<td>静态类型(TypeScript)</td>
<td>0kb</td>
<td>9kb</td>
</tr>
<tr>
<td><strong>合计</strong></td>
<td><strong>521kb</strong></td>
<td><strong>509 kb</strong></td>
</tr>
</tbody>
</table>
<p>因为边重构边在加新功能,且添加了定义TypeScript静态类型的代码,所以总代码量减少并不多。 </p>
<p>不过性能显著提升,最重要的是代码可读性、可维护性大大增强,从而能够更从容地应对新需求。</p>
<p><strong>感谢你花时间阅读这篇文章。如果你喜欢这篇文章,欢迎点赞、收藏和分享,让更多的人看到这篇文章,这也是对我最大的鼓励和支持!</strong> <br><strong>欢迎Star和订阅<a href="https://link.segmentfault.com/?enc=gmbZuGHIVyELNuNP49RVLQ%3D%3D.VuthT9GMCT9tmIx7tJO48c4UIdZybta46YW1PdfqlO6vqINpRmZAtLiMdkM0gfpi" rel="nofollow">我的原创前端技术博客</a>。</strong></p>
使用React手写一个对话框或模态框
https://segmentfault.com/a/1190000018967814
2019-04-24T09:33:30+08:00
2019-04-24T09:33:30+08:00
苏溪云
https://segmentfault.com/u/terry_su
6
<p><img src="/img/remote/1460000018967817" alt="" title=""></p>
<p>打算用React写对话框已经很长一段时间,现在是时候兑现承诺了。实际上,写起来相当简单。</p>
<p>核心在于使用React的接口<code>React.createPortal(element, domContainer)</code>。该接口将<code>element</code>渲染后的DOM节点嵌入<code>domContainer</code>(通常是<code>document.body</code>),并保证只嵌入一次。</p>
<blockquote>欢迎订阅<a href="https://link.segmentfault.com/?enc=hUrpTc7S9AUMqeiT27Cumw%3D%3D.9oI6SnNJQYaE8thY7wsB3EB1Y%2BhJ1TXMDqQJrK9MftTTxyZyznonRC6v3DwKpXFT" rel="nofollow">我的博客</a>。</blockquote>
<p>所以,我们可以这样写一个对话框或模态框:</p>
<pre><code class="jsx">function Dialog() {
return React.createPortal( <div>Dialog contents</div>, document.body )
}</code></pre>
<p>一个新的<code>div</code>会出现在<code>body</code>内部:<br><img src="/img/remote/1460000018967818" alt="image" title="image"></p>
<p>一个完整DEMO:</p>
<p><img src="/img/bVbrKvj?w=246&h=110" alt="clipboard.png" title="clipboard.png"><br><a href="https://link.segmentfault.com/?enc=%2FbiNnuy4%2BDWzyNrpEIJsGQ%3D%3D.B4PDE1oGNs4K4Or9lUxFvE3yVcjIL0Km5BTN8Zk4fAjLrqRP%2FJ0bRgZmdgUXu7CCkqukPFyjsxsfrAgBwnigL6eAZ8Cd5K%2FTmiHlqzQspKI%3D" rel="nofollow"><em>点击运行DEMO</em></a></p>
<pre><code class="jsx">
class Modal extends React.Component {
render() {
const {
visible,
onClose
} = this.props
return visible && ReactDOM.createPortal(<StyledModalRoot>
<div className="box">
Content
<br/>
<button onClick={onClose}>Close</button>
</div>
</StyledModalRoot>, document.body)
}
}
class App extends React.Component {
state = {
visibleModal: false
}
showModal = () => this.setState( { visibleModal: true } )
handleCloseModal = () => this.setState( { visibleModal: false } )
render() {
const { visibleModal } = this.state
return <div style={{padding: '20px'}}>
<button onClick={ this.showModal }>Show Modal</button>
<Modal visible={visibleModal} onClose={ this.handleCloseModal } />
</div>
}
}
const StyledModalRoot = styled.div`
position: fixed;
z-index: 1001;
left: 0;
top: 0;
display: grid;
place-items: center;
width: 100%;
height: 100%;
background: rgba( 0, 0, 0, 0.2 );
>.box {
position: relative;
display: grid;
place-items: center;
width: 80%;
height: 80%;
background: white;
border-radius: 10px;
box-shadow: 0px 3px 5px -1px rgba(0,0,0,0.2), 0px 5px 8px 0px rgba(0,0,0,0.14), 0px 1px 14px 0px rgba(0,0,0,0.12);
}
`</code></pre>
<p><strong>感谢你花时间阅读这篇文章。如果你喜欢这篇文章,欢迎点赞、收藏和分享,让更多的人看到这篇文章,这也是对我最大的鼓励和支持!</strong> <br><strong>欢迎在Star和订阅<a href="https://link.segmentfault.com/?enc=P4H2g5xCHKHR0zYyqBTBoQ%3D%3D.lgdByuAgJNUPlbpWAnY0CMyiGH84Up9Y4RK%2BnawbmnLNpDl8prf%2FvV%2FS7h6%2F805G" rel="nofollow">我的原创前端技术博客</a>。</strong></p>
节流 - 理解,实践与实现
https://segmentfault.com/a/1190000018736908
2019-04-02T10:48:21+08:00
2019-04-02T10:48:21+08:00
苏溪云
https://segmentfault.com/u/terry_su
14
<p><img src="/img/bVbqMwN?w=1280&h=720" alt="图片描述" title="图片描述"></p>
<p>节流(分流),与防抖(去抖)实现原理相似。本文主要讨论节流,镜像文章:<a href="https://link.segmentfault.com/?enc=ijjnZy4Ny1Bay%2FYs2pMPNg%3D%3D.fAT%2F9YeTeftbq0Lakc7IZ2vZKcQEroEuU1uQ1yz1otqA%2F9LzlSpCQuddZ1k5wlt%2Bm2HX3Z60Styesxyh3THx4A%3D%3D" rel="nofollow">防抖 - 理解,实践与实现</a>。分开讨论防抖和节流,主要是为了让一些还不太了解节流防抖的读者能够有针对性地,逐一掌握它们。 <br>如何用代码实现节流也是一个要点。本文采用循序渐进地方式,先绘制一个案例的流程图,再根据流程图的逻辑编写节流功能代码。</p>
<blockquote>
<strong>文章包含多个可交互案例,可通过<a href="https://link.segmentfault.com/?enc=HxQhF3Ne69cukZZcQ00FAA%3D%3D.CGvaW6DOY3gUOYkRx5VKPbkh8xSFR7BcL4QvR1uA0Aqj3hFHCp2rV%2FVbGlbL%2BrNb4eGrmAnd1zP1J2CVzkC3HA%3D%3D" rel="nofollow">博客原文</a>实时查看案例</strong><br><strong>同时欢迎订阅<a href="https://link.segmentfault.com/?enc=pi3wSLyAkrZScDLke8FpUQ%3D%3D.3Krd9RubN18HyVAY5XBZn0GNl36xWu4n6AEoMn5NxemelFW9W3Wlpa9wk5KinC%2Fp" rel="nofollow">我的博客</a></strong>
</blockquote>
<h2>节流案例</h2>
<p><img src="/img/bVbqMwb?w=1386&h=530" alt="clipboard.png" title="clipboard.png"></p>
<p><a href="https://link.segmentfault.com/?enc=C7wG92Y3mTa8HdihdGTlZg%3D%3D.kuhdBmIk16C7BzlneVXDTErC8NxMN7P0W1nmbBD4EoZFyqQPr%2FMPYln6AkN4ykSeeMvC2gZ0SYL%2BjqcMKMjXdGXqITfrlm0bhX6Gxtw6cPkE%2FAPRfDm%2BAMPTaWNzr8%2Bc" rel="nofollow">点击运行案例</a></p>
<p>当鼠标移动时,mousemove事件频繁被触发。上方为未节流模式,每一次mousemove触发都会绘制一个圆点。而下方为节流模式,尽管mosuemove在鼠标移动时被多次触发,但只有在限定时间间隔才会绘制圆点。</p>
<h2>理解和实现节流</h2>
<p>通过上方案例,可以基本了解节流的作用: 频繁触发的事件,事件处理函数在一定的时间间隔内只执行一次。</p>
<p>不过节流函数是如何做到的? 以上方案例为例,绘制其流程图如下。 </p>
<p>核心参数:</p>
<ol>
<li>间隔时长</li>
<li>计时器</li>
</ol>
<p><img src="/img/bVbqMlC?w=1386&h=1226" alt="clipboard.png" title="clipboard.png"></p>
<p>根据流程图的思路实现分流函数:</p>
<pre><code class="js">function throttle( func, wait ) {
let timer
function throttled( ...args ) {
const self = this
if ( timer == null ) {
invokeFunc()
addTimer()
}
function addTimer() {
timer = setTimeout( () => {
clearTimer()
}, wait )
}
function invokeFunc() {
func.apply( self, args )
}
}
return throttled
function clearTimer() {
clearTimeout( timer )
timer = null
}
}</code></pre>
<p>接下来,用编写的节流函数实现上方案例</p>
<p><img src="/img/bVbqMwb?w=1386&h=530" alt="clipboard.png" title="clipboard.png"></p>
<p><a href="https://link.segmentfault.com/?enc=CRMpGfYMp9IrRcJsFcSusw%3D%3D.J8HxVt4qmYm8WbgjpnxeNs3J1VoTs5N4LK%2B86YGbSCIBJvm9TYiECt2%2FXo2ZOrIasZxqTCm9DJZzzfSJlU6iFPiZ3XR1ilrRpsVmviSybUArh7eg5v0kZyr2gMggTb8k" rel="nofollow">点击运行案例</a></p>
<h2>应用场景</h2>
<p>无限的滚动条</p>
<p><img src="/img/bVbqMwD?w=688&h=260" alt="clipboard.png" title="clipboard.png"></p>
<p><a href="https://link.segmentfault.com/?enc=Wk4hmpWTXqXgHLuexNhQVQ%3D%3D.zni%2FnS9VkxE5oedEvBsQ%2FDNZkGBaMPSvXhdnHdb%2BlZ6kBHoeGDxefNG7dFouDuGn7W5lNfj3uAGl%2FM95AyYPnnTxQ%2BFcRQJr3bKahNWIQgct4a82co%2B1nk2K9yqwLI8DZynJSDEsbeGn6x5KDkdGrg%3D%3D" rel="nofollow">点击运行案例</a></p>
<h2>总结</h2>
<p>节流和防抖类似,都能有效优化系统性能,不过使用业务场景有所区别:</p>
<ul>
<li>防抖既可用于在多次触发的事件(如文本框逐个输入文字),也可用于在频繁触发的事件(如调整窗口尺寸)。</li>
<li>节流多只用在频繁触发的事件(如滚动滚动条)上。</li>
</ul>
<p><strong>感谢你花时间阅读这篇文章。如果你喜欢这篇文章,欢迎点赞、收藏和分享,让更多的人看到这篇文章,这也是对我最大的鼓励和支持!</strong> <br><strong>同时欢迎订阅<a href="https://link.segmentfault.com/?enc=V5bgX7FmjXXF3OL%2Fmua2FQ%3D%3D.YE5%2FIjAJRWhsfLqHPFWGPzYBS7ABhBVUZ8kLm9Ms8x762WykNPT6YVl888QnU3fP" rel="nofollow">我的博客</a></strong></p>
前端动画演绎排序算法
https://segmentfault.com/a/1190000018682836
2019-03-28T09:03:21+08:00
2019-03-28T09:03:21+08:00
苏溪云
https://segmentfault.com/u/terry_su
33
<blockquote><strong>文章包含多个可交互案例,可通过<a href="https://link.segmentfault.com/?enc=gI%2BcLEiQuSD39YwG%2FCvw3Q%3D%3D.QAVhrZHauweXFMmGDD7gYMp9yqt4qh7VTvw%2FoeJ%2F98VlFn9KXVSWVrDsmXFda0UCy1OomhibaQBrMEH8tDpfWUoQ%2Fgvn28d2nUwNyMUPTIgaOxM4IFhiohzbtANRD72W" rel="nofollow">博客原文</a>实时查看案例</strong></blockquote>
<p><img src="https://terry-su.github.io/BlogCDN/images/simpson-evolution.jpg" alt="" title=""></p>
<p>在学习了常用的排序算法之后,打算用动画Demo来生动形象的展现它们。</p>
<p>这里包含6种排序算法,其中一半是简单算法,另一半是高级算法:</p>
<ul>
<li>冒泡排序</li>
<li>选择排序</li>
<li>插入排序</li>
<li>~</li>
<li>归并排序</li>
<li>希尔排序</li>
<li>快速排序</li>
</ul>
<h2>冒泡排序</h2>
<p>这可能是最简单的一种,但是速度非常慢。 <br>假设我们按照棒球运动员的身高来排列队列。从最左边开始。</p>
<ol>
<li>比较两个球员</li>
<li>如果左边的高一些,就换掉。否则,不做任何操作。</li>
<li>向右移动一个位置</li>
</ol>
<p><img src="/img/bVbqBz6" alt="clipboard.png" title="clipboard.png"></p>
<p><a href="https://link.segmentfault.com/?enc=Dg49p7UzcoSAH7WVfrZRSA%3D%3D.lMEC5L2Scrohpl9KBdOo3yHBy5qdLhvWWKR9auEeyvxIBVVi57kTkiuUgS8sFuck68ed9ccum%2BBfCjxcbOU9dv2LXVovufZ63DCPQ3waMAN4YZp2NdzoVdF4fFeV9iLk" rel="nofollow">点击运行案例</a></p>
<h2>选择排序</h2>
<p>也从最左边开始。</p>
<ol>
<li>寻找从当前位置到右边的最矮球员</li>
<li>将最矮球员与当前位置的球员交换</li>
<li>向右移动一个位置</li>
</ol>
<p><img src="/img/bVbqBz6" alt="clipboard.png" title="clipboard.png"></p>
<p><a href="https://link.segmentfault.com/?enc=dSZQu1kJT5IWly29owvXZw%3D%3D.Cih7E48CYKyC%2BYrJDsvJWU4mVwFiNEdaJaKJyJcYS6NuYoeIGcAEMtdeM6xru0hWXpPHrTHIQ3I27rrCEpnHYyrxrYm97Ge4f270Bl4SB1ilY48ix9u3jUzP0fMslPPc" rel="nofollow">点击运行案例</a></p>
<h2>插入排序</h2>
<p>在大多数情况下,这是基础排序方法中的最佳方法。它的速度是冒泡排序的两倍。 <br>而具体步骤比上面的排序稍微复杂一些。从左边的开始。</p>
<ol>
<li>部分排序左球员</li>
<li>选择第一个未排序的球员作为标记球员</li>
<li>将比标记球员矮的球员移到右边</li>
<li>将标记的球员插入到第一个移动过位置的球员的前一个位置。</li>
</ol>
<p><img src="/img/bVbqBz6" alt="clipboard.png" title="clipboard.png"></p>
<p><a href="https://link.segmentfault.com/?enc=eArUR9zItpT1Y3ubw%2BJvqw%3D%3D.jOYgDwU4k13OKPWYzMTagv%2BGZ0njzN5amm3c4jWmwqkdwHasLo9RodVro2JS7VrfnHqkzDN%2BsDCoTVjc3agUnGOqzkRWcNmb%2BDyqO459moCGbnnhys9R%2F29DbElEPuGG" rel="nofollow">点击运行案例</a></p>
<h2>合并排序</h2>
<p>合并排序算法的核心是两个已经排序的数组的合并和递归。 <br><img src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/e6/Merge_sort_algorithm_diagram.svg/800px-Merge_sort_algorithm_diagram.svg.png" alt="" title=""></p>
<p>如图所示,主要步骤如下:</p>
<ol>
<li>将数字分成两部分</li>
<li>合并两部分</li>
</ol>
<p><img src="/img/bVbqBz6" alt="clipboard.png" title="clipboard.png"></p>
<p><a href="https://link.segmentfault.com/?enc=CS8I%2BEuE%2FlWQApYsZNgnwQ%3D%3D.NkyzzK45Rrhfv0I4%2F2dTyO2PO9qCPUdYAe5mFUtNI%2BAHRvbGD7vYTf7V3HGdcLUrj2o6UCUL4QrUo2YgzLU6fNfqHFubcO%2BWDL1FqapFB6nNAJ9GBoN7%2F06QODzViW2s" rel="nofollow">点击运行案例</a></p>
<h2>希尔排序</h2>
<p>“Shell排序”的名称是以发现它的Donald Shell命名的。它基于<strong>插入排序</strong>,但是增加了一个新特性,从而极大地提高了插入排序的性能。 </p>
<p>主要步骤</p>
<ol>
<li>将数组按区间(例如3)划分为若干组,并对它们进行一直排序,直到所有元素都被划分和排序为止。</li>
<li>缩小区间,继续进行分割和排序,直到区间变为1。</li>
</ol>
<p><img src="/img/bVbqBz6" alt="clipboard.png" title="clipboard.png"></p>
<p><a href="https://link.segmentfault.com/?enc=aonG4mBL8hDKIiNKnHGG%2FQ%3D%3D.U1ChM8wlkbHUthlcoSmgAyMDEBqj1919nmumiEXwEMtVRSYyPhU6fTg8z8x3nI%2FtPUTQWrYGHZ%2B76pn%2FiwtP1Y4St5Uo30pGt1dN%2BQwSUR5hYSHizpRvPvACqyBqBFpQ" rel="nofollow">点击运行案例</a></p>
<h2>快速排序</h2>
<p>在大多数情况下,这是最快的排序。</p>
<ol>
<li>选择一个参考元素(最右边的元素)</li>
<li>将数组划分为左子数组(比参考元素小的所有元素)和右子数组(比参考元素大的所有元素)</li>
<li>对左子数组和右子数组重复步骤2</li>
</ol>
<p><img src="/img/bVbqBz6" alt="clipboard.png" title="clipboard.png"></p>
<p><a href="https://link.segmentfault.com/?enc=cJNqzjIjfkm4S%2B2jpnzZgQ%3D%3D.281G86MTxIROEDTLwfGzRIAV4e1TL7s%2FwtjbXIQGKNhSgOpHqU%2B5Bl2s0%2FUP0Cklrwl6db2aLp%2FtBBP86KBHyBqxE771zQSmP4niiIb9BC6Ki4qIRdLEYdnGMYioLYRE" rel="nofollow">点击运行案例</a></p>
<p><strong>感谢你花时间阅读这篇文章。如果你喜欢这篇文章,欢迎点赞、收藏和分享,让更多的人看到这篇文章,这也是对我最大的鼓励和支持!</strong> <br><strong>欢迎Star和订阅<a href="https://link.segmentfault.com/?enc=v4A8QMN1RvnNiDsPWE4K2g%3D%3D.sklFuLySj2QNAhcLVkNdIyhE7jFEiAK9UkgR5WgMVcQeQUTXoJ1cX7hs9vWXu7If" rel="nofollow">我的博客</a>。</strong></p>
揭开redux,react-redux的神秘面纱
https://segmentfault.com/a/1190000017410874
2018-12-18T10:04:31+08:00
2018-12-18T10:04:31+08:00
苏溪云
https://segmentfault.com/u/terry_su
17
<p>16年开始使用react-redux,迄今也已两年多。这时候再来阅读和读懂redux/react-redux源码,虽已没有当初的新鲜感,但依然觉得略有收获。把要点简单写下来,一方面供感兴趣的读者参考,另一方面也是自己做下总结。</p>
<p>为了完整阅读体验,欢迎移步到我的<a href="https://link.segmentfault.com/?enc=Ks%2FZucCNFjXMd42dQ12%2FGA%3D%3D.0hRzrhgaRUbADHO6bETNjSdJ7mdxwxeS%2FmQiEQHTTpfzY1wM8SxsFgq3z6g9Z7rG%2Bw7pTc89aGIPMonOBbrLhElFT5%2FsVyCwKKeSUw735so%3D" rel="nofollow">博客原文</a>。</p>
<h2>redux</h2>
<p>react-redux最核心的内容就是redux。内带redux,react-redux只提供了几个API来关联redux与react的组件以及react state的更新。</p>
<p>首先,看下如何使用redux。 redux老司机可以直接滑动滚轮至下一章。 <br>简单来说,redux有三个概念,action, reducer 和 dispatch。 action和dispatch比较好理解:动作指令和提交动作指令方法。而reducer,个人在字面上没有理解,但抽象层面上可以理解为用来生成state的函数。用一个简单案例体现这三个概念:</p>
<pre><code class="js">// action
const INCREMENT = { type: 'INCREMENT' }
// reducer
function count( state = 0, action ) {
switch( action.type ) {
case 'INCREMENT':
return state + 1
default:
return state
}
}
// dispatch
// 此处开始使用redux
const store = redux.createStore( count )
console.log( store.getState() ) // 0
store.dispatch( INCREMENT )
console.log( store.getState() ) // 1</code></pre>
<p>接下来说说redux中的两大模块:</p>
<ul>
<li>
<code>store</code>对象</li>
<li>中间件</li>
</ul>
<h2>store对象</h2>
<p>API<code>createStore</code>会创建了一个<code>store</code>对象,创建的过程中它主要做了下面两件事:</p>
<ol>
<li>初始化state</li>
<li>暴露相关接口:<code>getState()</code>, <code>dispatch( action )</code>, <code>subscribe( listener )</code>等。其中<code>getState()</code>用来获取store中的实时state, <code>dispatch(action)</code>根据传入的action更新state, <code>subscribe( listener)</code>可以监听state的变化。</li>
</ol>
<h2>中间件</h2>
<p>中间件可以用来debug或提交异步动作指令. 在初始化store的时候,我们通过<code>createStore( reducer, state, applyMiddleware( middleware1, middleware2 ) )</code>添加多个中间件。 <br>为了实现多个中间件,redux专门引入了函数式编程的<code>compose()</code>方法,简单来说,<code>compose</code>将多层函数调用的写法变得优雅:</p>
<pre><code class="js">// 未使用compose方法
a( b( c( 'd' ) ) )
// 用compose方法
compose( a, b, c )('d')</code></pre>
<p>而中间件的写法比较奇特,是多级函数,在阅读源码的时候有点绕。显然中间件的写法还可以优化,尽管现在的写法方便在源码中使用,但对redux用户来说稍显复杂,可以用单层函数。</p>
<pre><code class="js">function logMiddleware({ getState }) {
return nextDispatch => action => {
console.log( 'before dispatch', getState() )
const res = nextDispatch( action )
console.log( 'after dispatch', getState() )
return res
}
}</code></pre>
<h2>react-redux</h2>
<p>了解了redux运作原理,就可以知道react-redux的大部分使用场景是如何运作。react-redux提供了几个API将redux与react相互关联。</p>
<p>基于上一个案例展示react-redux的用法:</p>
<pre><code class="jsx">// action
const increment = () => ({ type: 'INCREMENT' })
// reducer
function count( state = 0, action ) {
switch( action.type ) {
case 'INCREMENT':
return state + 1
default:
return state
}
}
// redux
const store = Redux.createStore( count )
// react-redux
const { Provider, connect } = ReactRedux
const mapStateToProps = state => ( { count: state } )
const mapDispatchToProps = dispatch => ( { increment : () => dispatch( increment() ) } )
const App = connect( mapStateToProps, mapDispatchToProps )( class extends React.Component {
onClick = () => {
this.props.increment()
}
render() {
return <div>
<p>Count: { this.props.count }</p>
<button onClick={ this.onClick }>+</button>
</div>
}
} )
ReactDOM.render( <Provider store={ store }>
<App />
</Provider>, document.getElementById( 'app' ) )</code></pre>
<p><a href="https://link.segmentfault.com/?enc=RZ4TQFb%2BkxYppvt8T43now%3D%3D.CtX%2BO3rjQ5OSZk0ogqQy4xaACGI03wi5Uxw6aaJYqfv3cIr0%2FDD0eBa9t0PMEwbBBAPPGhU6SKuNiKHcSOkyeKGhGSlUcXG%2B%2FdFdl%2FuutqU%3D" rel="nofollow"><em>点击运行案例</em></a></p>
<p>react-redux提供最常用的两个API是:</p>
<ul>
<li><code>Provider</code></li>
<li><code>connect</code></li>
</ul>
<h2><code>Provider</code></h2>
<p>Provider本质上是一个react组件,通过react的<a href="https://link.segmentfault.com/?enc=EGLtU4CoG5fSx%2FSQ51aJ%2FA%3D%3D.cndcF9t3dWVfTJ9y1c3O2z6Nr83I6%2F6G5XUjIl6Pk09Vb%2Fb6suRaz3bnn88saQvHVYv6b1UYq0nn%2B%2Bgyys%2FvCg%3D%3D" rel="nofollow">context api</a>(使一个组件可以跨多级组件传递props)挂载redux store中的state,并且当组件初始化后开始监听state。<br>当监听到state改变,Provider会重新<code>setState</code>在context上的<code>storeState</code>,简要实现代码如下:</p>
<pre><code class="jsx">class Provider extends Component {
constructor(props) {
super(props)
const { store } = props
this.state = {
storeState: Redux.store.getState(),
}
}
componentDidMount() {
this.subscribe()
}
subscribe() {
const { store } = this.props
store.subscribe(() => {
const newStoreState = store.getState()
this.setState(providerState => {
return { storeState: newStoreState }
})
})
}
render() {
const Context = React.createContext(null)
<Context.Provider value={this.state}>
{this.props.children}
</Context.Provider>
}
}</code></pre>
<h2><code>connect()</code></h2>
<p><code>connect</code>方法通过<code>connectHOC</code>(HOC: react高阶组件)将部分或所有state以及提交动作指令方法赋值给react组件的props。</p>
<h2>小结</h2>
<p>写react不用redux就像写代码不用git, 我们需要用redux来更好地管理react应用中的state。了解redux/react-redux的运作原理会消除我们在使用redux开发时的未知和疑惑,并且在脑中有一个完整的代码执行回路,让开发流程变得透明,直观。 </p>
<p>如果本文帮助到了你,我也十分荣幸, 欢迎点赞和收藏。如果有任何疑问或者建议,都欢迎在下方评论区提出。</p>
防抖 - 理解,实践与实现
https://segmentfault.com/a/1190000016480079
2018-09-21T10:38:07+08:00
2018-09-21T10:38:07+08:00
苏溪云
https://segmentfault.com/u/terry_su
16
<p>为了完整阅读体验,欢迎移步到我的<a href="https://link.segmentfault.com/?enc=9b%2BB%2BEdJJlBa%2FIx7FM792Q%3D%3D.1LCq1MFT%2FlFmX%2BEmtq54nGIe%2Bavb9NeWJdou%2FcJFaTcy0ftawBAZ%2FfDf9hQTezuZmDBKrNE1r1r9JqKhMhzNcA%3D%3D" rel="nofollow">博客原文</a>。</p>
<p>防抖(去抖),以及节流(分流)在日常开发中可能用的不多,但在特定场景,却十分有用。本文主要讨论防抖,镜像文章:<a href="https://link.segmentfault.com/?enc=pVekPTwrtUMfGf4wFvTUAg%3D%3D.bMdjtYhVAbB9%2BAPtmmniWrWuJ%2FptGIH%2BqP%2BA53nwjLdj4TlX5j%2B7e0WBjIfDsO9Afji0K5rrQNc3el%2BM3qnvfA%3D%3D" rel="nofollow">节流 - 理解,实践与实现</a>。分开讨论防抖和节流,主要是为了让一些还不太了解防抖节流的读者能够有针对性地,逐一掌握它们。 <br>防抖有<strong>两种模式</strong>(容易让人迷惑):延时执行和直接执行。后续详细讨论。 <br>防抖还有一个关键点是如果用代码实现。本文以循序渐进地方式,先以实现一个简单案例为例,绘制流程图,再根据流程图逻辑编写防抖功能代码。</p>
<h2>典型案例</h2>
<p>以日常开发中常用的搜索按钮为例,若用户点击一次搜索按钮后,不小心“手抖”很快又点了一次按钮,防抖可以规避第二次甚至更多次搜索。</p>
<p><img src="https://image-static.segmentfault.com/799/107/799107495-5ba456951f8cf_articlex" alt="clipboard.png" title="clipboard.png"><br><a href="https://link.segmentfault.com/?enc=AJtRpzD6p7rtLBMQ8DHLVQ%3D%3D.LH78Z3RRp%2FYI7vr1GFxggBJY4g3ZGCbZPCnR7HBlYcas%2F2%2BrXZDHCYsHdyfu3cbG9bEBj5MVSY3WZ6pPDQmV1zwszLXeWVfykteBXYvM8uHMKL74UGUeQ1oFaPVlcT5g" rel="nofollow">点击查看案例</a></p>
<p>第一个搜索按钮未做任何防抖处理。 <br>搜索按钮A为第一种防抖模式:延时执行。若用户连续快速点击多次,只有最后一次点击结束,延时一段时间后才执行搜索。 <br>搜索按钮B为第二种防抖模式:直接执行。若用户连续快速点击多次,只有第一次点击会执行搜索。</p>
<h2>防抖是什么</h2>
<p>结合上方案例,防抖可以理解为:多次触发事件后,事件处理函数只执行一次。 <br>而防抖的两种模式可以根据实际使用场景分别应用。</p>
<h2>应用场景</h2>
<p>在搜索框中实时键入文本搜索</p>
<p><img src="https://image-static.segmentfault.com/171/372/1713720488-5ba4a1fe1a837_articlex" alt="clipboard.png" title="clipboard.png"></p>
<p><a href="https://link.segmentfault.com/?enc=PBeEZyM01vvzeFaXiIInqw%3D%3D.IbWIKZdu6wTe9mbSsoUu5hnPnBLcxi46X%2F8FXtFHBur%2FP7UhbLcwpPfIa0Qsm86nyHaC4O%2FIw01XogvFJV5B%2BcuTRCex%2FeXzb0DJTsQqAt10SPQi1PS5MhLPIR58pJqF" rel="nofollow">点击查看案例</a></p>
<p>防止频繁点击搜索按钮触发搜索请求<br><img src="https://image-static.segmentfault.com/863/628/863628280-5ba454b03225b_articlex" alt="clipboard.png" title="clipboard.png"><br><a href="https://link.segmentfault.com/?enc=kNh5h6MIEToXLVk2uFltTA%3D%3D.lydUvKqlKPPX8zjpw5I5QaLLhBiFWn9QJxxYd08nQEc3mW3N%2Ba5JRYu72%2ByKpTBrpW1I6lEz%2F4il6UIhNpVNELrmHBXoKGzgGg7Rzs7X1v9294uhT14msMxDrix1Mr2B" rel="nofollow">点击查看案例</a></p>
<h2>一步步手写防抖</h2>
<p>接下来我们通过一个案例梳理实现防抖的思路。 <br>假设我们要实现本文第一个案例中搜索按钮A的功能。首先整理需求:</p>
<ol>
<li>点击搜索按钮后,函数并不马上执行,而是等一段时间再执行。</li>
<li>若在这段时间内,按钮再次被点击,则重新开始计时,等待同样一段时间后再执行。</li>
</ol>
<p>实现的方法有两种,推荐第一种,用计时器(setTimeout)简化代码,将重心放在实现防抖的逻辑上。</p>
<p>方法一核心参数:</p>
<ol>
<li>等待时长</li>
<li>计时器</li>
</ol>
<p>绘制方法一的流程图:</p>
<p><img src="https://image-static.segmentfault.com/516/529/516529209-5ba45519d422d_articlex" alt="clipboard.png" title="clipboard.png"></p>
<p>根据流程图思路实现方法一的防抖代码:</p>
<pre><code class="js">function debounce( func, wait = 0 ) {
let timer
function debounced( ...args ) {
const self = this
if ( timer == null ) {
addTimer()
return
}
if ( timer != null ) {
clearTimer()
addTimer()
return
}
function addTimer() {
timer = setTimeout( () => {
invokeFunc()
clearTimer()
}, wait )
}
function invokeFunc() {
func.apply( self, args )
}
}
return debounced
function clearTimer() {
clearTimeout( timer )
timer = null
}
}</code></pre>
<p>方法二核心参数:</p>
<ol>
<li>等待时长</li>
<li>最早可执行时间</li>
</ol>
<p>绘制方法二的流程图:</p>
<p><img src="https://image-static.segmentfault.com/231/759/2317592402-5ba4554e5b0b6_articlex" alt="clipboard.png" title="clipboard.png"></p>
<p>根据流程图实现方法二的防抖代码:</p>
<pre><code class="js">function debounce( func, wait = 0 ) {
// Earliest time when func can be invoked
let earliest
function debounced( ...args ) {
const self = this
if ( typeof earliest === 'undefined' ) {
setEarliset()
}
if ( typeof earliest !== 'undefined' ) {
if ( now() >= earliest ) {
invokeFun()
} else {
setEarliset()
}
}
function setEarliset() {
earliest = now() + wait
}
function invokeFun() {
func.apply( self, args )
}
}
return debounced
function now() {
return +new Date()
}
}</code></pre>
<p>同样,我们可以使用类似方法实现搜索按钮B的功能。<br>需求描述:</p>
<ol>
<li>点击搜索按钮后,函数马上执行。只有等待一段时间后被点击才能执行函数。</li>
<li>若在这段时间内按钮被点击,则重新计时。</li>
</ol>
<p>核心参数:</p>
<ol>
<li>等待时长</li>
<li>计时器</li>
</ol>
<p><img src="https://image-static.segmentfault.com/102/103/1021037037-5ba4556820c94_articlex" alt="clipboard.png" title="clipboard.png"></p>
<pre><code class="js">function debounce( func, wait = 0 ) {
let timer
function debounced( ...args ) {
const self = this
timer == null && invokeFunc()
timer != null && clearTimer()
timer = setTimeout( clearTimer, wait )
function invokeFunc() {
func.apply( self, args )
}
}
return debounced
function clearTimer() {
clearTimeout( timer )
timer = null
}
}</code></pre>
<p>接下来我们使用刚才编写的debounce函数来测试第一个案例<br><img src="https://image-static.segmentfault.com/637/017/637017202-5ba45573ab410_articlex" alt="clipboard.png" title="clipboard.png"><br><a href="https://link.segmentfault.com/?enc=wugZipxXjZq%2F5bRcZ0o0Hg%3D%3D.HcsMbK7p7zyQM%2BqK1v5tr85K4cnHhW7LuKLu6LuVaSCXcHwQRWO%2FHu%2B%2BI1b667Hlso%2BZnT3XcUUQL46VZq2Z5Zuv2EeoioxFBdtQzCnK6Ge%2B0C%2FIJSB7biuIJRJl1bJg" rel="nofollow">点击查看案例</a></p>
<h2>总结</h2>
<p>防抖是一个高阶函数,能够将多个事件函数合并为一个,在“调整window尺寸”,“在搜索框中实时搜索键入文本”, “滚动滚动条”和“防止搜索按钮频繁点击触发多余请求”等案例中,十分有用。</p>
<h2>链接</h2>
<ul>
<li>Lodash推荐:<a href="https://link.segmentfault.com/?enc=ydi8XYwTVPOUwyuVrMVgZA%3D%3D.FAZwBi%2BYLO4eBxFBBIH3tTj74rXYcYQBLuVRZX3dwR7546m1KbCwnd1Tlt9ucuC5YbVWtItC5rMNWA12ADRrOw%3D%3D" rel="nofollow">https://css-tricks.com/deboun...</a>
</li>
<li>简单理解防抖定义:<a href="https://link.segmentfault.com/?enc=AxK%2BypfVaoLT4rjlNHDvgQ%3D%3D.UY0K1%2BbkexI4WNU5n%2BdaFdOftBoXSX%2Bn5ZIJq2hStOP3QOoRB5DhQIRSJPaEifEaOpWich1NN58bMqgLQMHPGQ%3D%3D" rel="nofollow">https://www.cnblogs.com/woody...</a>
</li>
</ul>
<p><strong>感谢你花时间阅读这篇文章。如果你喜欢这篇文章,欢迎点赞、收藏和分享,让更多的人看到这篇文章,这也是对我最大的鼓励和支持!</strong> <br><strong>同时欢迎阅读我的更多原创前端技术博客: <a href="https://link.segmentfault.com/?enc=K3A4r6VbAi5JTSIvlimN7g%3D%3D.mxXb338FJM4%2FvdmJBU4n%2FK2y%2BWmx3klABaD8f%2BBDBi0%3D" rel="nofollow">苏溪云的博客</a>。</strong></p>
不再手写import - VSCode自动引入Vue组件和Js模块
https://segmentfault.com/a/1190000015690038
2018-07-19T11:07:46+08:00
2018-07-19T11:07:46+08:00
苏溪云
https://segmentfault.com/u/terry_su
11
<p>如要自动引入Vue组件,首先需安装VSCode拓展: <a href="https://link.segmentfault.com/?enc=xoyWpoYeRlN2vUNTWPujEA%3D%3D.fXacvrRbcWU%2Bcf87OisaBHZC5ryxWMXZt0A9EnWbdwncfDdWUPer2XDDPYgnXORVwuHZAZQQ2l0WEVnmrjDJ%2FsoL7KPT6hu0nFLRTdWAjwk%3D" rel="nofollow"><code>Vetur</code></a></p>
<h2>自动引入Vue组件和普通Js模块</h2>
<p>在根目录添加 <code>jsconfig.json</code> 。 <br>每次修改<code>jsconfig.json</code>后需<strong>重启该VSCode窗口</strong></p>
<pre><code class="json">{
"include": [
"./src/**/*"
],
}</code></pre>
<p><img src="/img/remote/1460000015690041?w=800&h=350" alt="" title=""><br><img src="/img/remote/1460000015690042?w=568&h=238" alt="" title=""></p>
<h2>支持Webpack别名路径</h2>
<p>同上,需更新 <code>jsconfig.json</code></p>
<pre><code class="js">{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
},
"include": [
"./src/**/*",
],
}</code></pre>
<p><img src="/img/remote/1460000015690043?w=788&h=328" alt="" title=""><br><img src="/img/remote/1460000015690044?w=662&h=230" alt="" title=""></p>
<h2>在JS中自动引入node_modules中的JS</h2>
<ol>
<li>以<code>lodahs</code>为例,安装 <code>lodash</code>: <code>npm install lodash</code>
</li>
<li>在根目录添加 <code>jsconfig.json</code>
</li>
</ol>
<pre><code class="js">{
"compilerOptions": {
"checkJs": true,
},
"include": [
"node_modules/lodash/*"
],
}</code></pre>
<ol><li>输入关键字后,点击提示灯泡(快键键: <code>Ctrl/Command + .</code>),选择JS模块</li></ol>
<blockquote>注意: <code>checkJs</code>可能会引起部分项目语法报错,如有报错可使用下面的方法作为替代方案。</blockquote>
<p><img src="/img/remote/1460000015690045?w=694&h=256" alt="" title=""></p>
<h2>使用<code>Npm Intellisense</code>自动引入node_modules中的JS</h2>
<ol>
<li>安装VSCode拓展: <a href="https://link.segmentfault.com/?enc=aKadqSmT4JUYspRiYRABpw%3D%3D.oDbb2O8c0CtSUaszFgy2lvxdYtKkhrFr5PwzR7GLxgC52p7tOmkQI9ax2nCmhLvyWWJxDzdyEKvMRWDlM3zGFwXkkVUyrIayEwNKRE9KD3Swj%2F%2FcD0BuObNQyWLbZ7cI" rel="nofollow"><code>Npm Intellisense</code></a>
</li>
<li>配置 <code>Npm Intellisense</code>
</li>
</ol>
<pre><code class="js">{
"npm-intellisense.scanDevDependencies": true,
"npm-intellisense.importES6": true,
"npm-intellisense.importQuotes": "'",
"npm-intellisense.importLinebreak": ";\r\n",
"npm-intellisense.importDeclarationType": "const",
}</code></pre>
<ol><li>VSCode输入命令(<code>Ctrl/Command + Shift + P</code>): <code>Npm Intellisense: Import module</code>后, 选择引入包</li></ol>
<p><img src="/img/remote/1460000015690046?w=1008&h=316" alt="" title=""></p>
<h2>其他</h2>
<p>自动引入Vue组件和JS模块后,按住<code>Ctrl/Command</code>点击路径可直接跳到该文件<br><img src="/img/remote/1460000015690047?w=1000&h=414" alt="" title=""><br><img src="/img/remote/1460000015690048?w=1000&h=414" alt="" title=""></p>
<p>文章原始发布于:<a href="https://link.segmentfault.com/?enc=kdC%2FC8QWe3amlXMaC0pA3A%3D%3D.b8ihGovJ2XOp6lDCp%2BU0TuGYbOVh2XhGkJH%2FTDna5rYUC8lEBLB36jzBItnSJ%2BchRsOtHQglwxp233pw9NnURhXW%2F0Pe7sLuNDmBmJw0PKo%3D" rel="nofollow">我的博客</a></p>