<?xml version="1.0" encoding="UTF-8" ?>
  <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
   <atom:link href="http://godlanbo.com/rss/index.xml" rel="self" type="application/rss+xml" />
   <atom:icon>http://godlanbo.com/favicon.ico</atom:icon>
   <title>GodLanbo的个人博客</title>
   <description>GodLanbo的个人博客</description>
   <link>http://godlanbo.com</link>
   <lastBuildDate>Thu, 25 Jul 2024 13:12:00 GMT</lastBuildDate>
   <pubDate>Thu, 25 Jul 2024 13:12:00 GMT</pubDate>
   <ttl>60</ttl>
  <item>
        <title>关于React StrictMode引发的思考</title>
        <link>http://godlanbo.com/blogs/48</link>
        <guid>48</guid>
        <pubDate>Thu, 21 Dec 2023 13:35:14 GMT</pubDate>
        <description><![CDATA[
          <h2>背景</h2>
<p>事情是这样的，我在开发一个富文本编辑器的”提及“功能，就是常见的 @xxx 高亮这么一个功能。在编辑器检测到 ”@“ 符号输入的时候，需要在合适的一个位置，出现一个<code>SelectList</code> 给我选择需要提及的人。类似下面这种：</p>
<p><img src="https://godlanbo.com/images/1703165639691.jpg" alt="截图"></p>
<p>显然这个合适的位置是基于当前输入的光标位置出现的。同时我还应该自动focus到这个<code>SelectList</code>上。</p>
<p>显然，思路是清晰的，不需要的时候，可以将提及组件卸载，然后当”@“输入的时候，触发提及组件的挂载，在 <code>useEffect dep = []</code>中获取当前光标的位置，然后用它的<code>Range</code>节点获取关于位置坐标的信息，再拿这个信息对提及组件做一个定位就OK了。</p>
<p>逻辑是简单的，但是实际出现的问题是挠头的。</p>
<p><img src="https://godlanbo.com/images/1703165639699.jpg" alt="v2_73657041-7a0f-4067-9ad5-676324a7640g.jpg"></p>
<h2>问题</h2>
<p>下面简单来一段<strong>伪代码</strong>，好方便的描述问题，让你更清楚的明白问题：</p>
<pre><code class="language-jsx">function MentionComp() {
  useEffect(() =&amp;amp;gt; {
    // 获取光标位置
    const position = getPointorPosition();
    // 利用光标位置，给组件计算一个合适的位置
    setCompPosition(position);
    // 聚焦到选框上
    SelectList.focus()
  }, [])
  // ...render SelectList use CompPosition
}
</code></pre>
<p>获取光标位置这里，用的 Selection 和 Range API获取，当用户在编辑器中输入”@“的时候，很显然，就会立即触发<code>MentionComp</code>的挂载。这个时候我们就会获取到当前位置的光标，就是 ”@“ 字符的位置，然后再相对于这个定位稍微调整即可。</p>
<p>但是实际上，诡异的事情发生了，我无论在富文本编辑器的哪个地方键入 ”@“，<code>MentionComp</code> 都会出现在相对于富文本编辑器最开始的位置，就好像光标一直在最开始没有移动一样。</p>
<h2>排查</h2>
<h3>排查光标位置获取</h3>
<p>最开始排查的，肯定是自己写的获取光标位置的代码有问题，直接上 <code>log</code> 进 <code>debuger</code>，调试对应的值。一开始在用断点调试的时候，发现走到对应逻辑的光标位置值， 一直是OK的，我就更加疑惑了。直到看控制台日志，才发现 <code>useEffect</code> 里获取光标逻辑的代码执行了两次，第一次是正确的，第二次直接变成 0 了！，就像是光标丢了一样。</p>
<p><img src="https://godlanbo.com/images/1703165639694.jpg" alt="v3_006a_2bea9e21-7ca4-4c2e-a19f-f2685bf41bfg.png"></p>
<p>这直接给我CPU烧了，先不说第二次值为什么不对，<code>useEffect dep = []</code>这个逻辑能在组件周期内执行两次？我马上怀疑提及组件可能被卸载重挂载了，于是我立马在 <code>useEffect dep = []</code>里面写了个 return 来验证我的想法，果然通过日志发现组件发生了一次卸载。</p>
<p>那么第二次光标丢了的原因也就很好解释了，因为在第一次挂载的时候，页面焦点已经被 focus 到了 <code>SelectList</code> 上，然后由于被重挂载，上一次渲染的节点已经丢了，自然，这个时候获取光标位置就取不到了。</p>
<h3>排查重挂载</h3>
<p>然后就到了重头戏，为什么提及组件被卸载了一次？</p>
<p>最开始我以为是富文本组件挂载提及组件的问题，可能是一些不可名状的底层逻辑（笔者没读过react源码，认为react底层的逻辑就像深黑幻想一样神秘，出现任何不可预料的情况都是有可能的）。</p>
<p>但是看了下富文本编辑器关于提及组件挂载这里，逻辑非常干净简单，简单的不能再简单了，即使是深黑幻想也不可能出问题：</p>
<p><img src="https://godlanbo.com/images/1703165639781.jpg" alt="截图"></p>
<p>一个简单的条件渲染，不可能搞出这么多幺蛾子，那我显然怀疑到另一个深黑幻想上，就是逻辑复杂的富文本编辑器（虽然这里我直觉觉得不是它的问题，但是没思路，就只能删代码排除法了）。于是我直接把编辑器干掉，用一个<code>button click</code>来代替触发 <code>showMention</code> 的逻辑。</p>
<p>果然啊，不是富文本编辑器的问题。到这时，这里的逻辑已经简单成这样了：</p>
<p><img src="https://godlanbo.com/images/1703165639764.jpg" alt="截图"></p>
<p>在我看来，这是不可能出问题的逻辑了，不然有点颠覆我的 react 认识。虽然短短几段文字，但我当时排查已经快一个小时过去了，很可惜，没什么进展。</p>
<h2>解决</h2>
<p>正在我脑子里绞着<em>react</em>，<em>useEffect</em>，<em>重渲染</em>这些词语，来回想着下一步排查思路的时候，脑海里记忆的深处突然回响：</p>
<p>&amp;gt; 嘿，这个项目是最新的 React 18吧，它从17升级过来可是多了不少东西。
&amp;gt;
&amp;gt; 有没有想到什么？比如关于 ”输出两次“ 相关的？</p>
<p>我突然想到在 React 18 发版，在前端生态引起讨论的时候，看过一些文章，但是因为我还在用 React17，没有在意那些文章的内容。不过有个内容印象很深，好像在React 18的什么情况下，<code>useEffect</code> 中的内容会输出两次。</p>
<p>”就是这个！“，我虽然还没有上手验证，但是内心的直觉告诉我，这恐怕就是原因。</p>
<p>这个内容看来引起过很多开发者的困惑，网上短短几分钟就搜到了相关的原因，是因为根组件被包裹 <code>StrictMode</code>导致的。我找到我的项目的根App组件，果然被包了这个东西，我把它删掉然后测试，逻辑恢复正常。</p>
<h2>什么是 StrictMode？</h2>
<p><code>StrictMode</code> 组件是React 18新增的一个特性，它会把它包裹下的组件行为开启严格模式。那严格模式下，组件会有什么特殊行为吗，这里直接引用官方文档：</p>
<p>&amp;gt; Your components will re-render an extra time to find bugs caused by impure rendering.
&amp;gt;
&amp;gt; 你的组件将会在一小会儿之后被重新调用渲染，用来找出那些不正确的渲染导致的BUG
&amp;gt;
&amp;gt; Your components will re-run Effects an extra time to find bugs caused by missing Effect cleanup.
&amp;gt;
&amp;gt; 你的组件将会重新运行副作用在一小会儿后，用来找到那些引起问题的副作用缺失清除
&amp;gt;
&amp;gt; Your components will be checked for usage of deprecated APIs.
&amp;gt;
&amp;gt; 你的组件将会被检查是否使用了过期的API</p>
<p>其实当我刚看到这里的时候，其实气愤占更多数，尤其是看到官网文档上，React对于你组件的假设：</p>
<p>&amp;gt; React assumes that every component you write is a pure function.
&amp;gt;
&amp;gt; React 假设每一个你的组件都应该是一个纯函数</p>
<p>什么纯函数这里不展开，这玩意儿又能写（水）一篇，大概意思就是组件会改变外部状态。这里React 的说法开始让我觉得很无语，你认为应该怎么怎么样，就给我搞一个这种意外行为，真的是非常坑爹啊，在页面上经常会有操作DOM，BOM之类的，组件难免会有副作用产生。</p>
<p>但是回过头我开始思考，严格模式，真的是如我想的那样蠢吗？</p>
<h2>思考</h2>
<p>首先是看完了官方文档关于严格模式的详细描述，以及这些行为能够检出哪些BUG的例子。<a href="https://react.dev/reference/react/StrictMode#fixing-bugs-found-by-double-rendering-in-development">文档指路</a></p>
<p>看完之后其实非常深刻啊，官方的举例都是非常典型的那种，让我觉得这个严格模式好像也没有那么不合理。</p>
<p>反过来回顾我这次遇到的问题，其实也是属于严格模式想要检出的BUG第二种，副作用未清除那类。我在 <code>useEffect</code> 中，操作了DOM，改变了外部的聚焦状态，从而导致了这个问题。其实富文本编辑器有提供API给我重置光标位置，我如果这么写：</p>
<pre><code class="language-jsx">function MentionComp() {
  useEffect(() =&amp;amp;gt; {
    // ...
    // 聚焦到选框上
    SelectList.focus();
    return () =&amp;amp;gt; {
      // 把光标恢复到最近一次输入上
      editor.resetPointor();
    }
  }, [])
  // ...render SelectList use CompPosition
}
</code></pre>
<p>就不会有问题，其实它也确实帮我检出了，副作用未清除的问题。要是在线上（严格模式自身不会在生产环境生效）真的遇到了，因为意外导致的组件重新挂载一次，就会因为这个出问题。从严格模式诞生的意义来看：</p>
<p>&amp;gt; &amp;lt;StrictMode&amp;gt; lets you find common bugs in your components early during development.</p>
<p>它确实做到了。</p>

        ]]></description>
      </item><item>
        <title>WebGL学习笔记（一）</title>
        <link>http://godlanbo.com/blogs/45</link>
        <guid>45</guid>
        <pubDate>Tue, 22 Mar 2022 09:52:31 GMT</pubDate>
        <description><![CDATA[
          <h2>1、清空canvas绘图区</h2>
<p>在绘制canvas之前，我们可能需要将canvas的背景清空，以绘制下一帧的内容。</p>
<h3>1.1、获取绘图上下文</h3>
<pre><code class="language-javascript">const canvas = document.querySelector('.canvas')
const context = canvas.getContext('webgl')
</code></pre>
<p>我们想要操作canvas之前，首先都需要获取到画布的上下文。JS给canvas标签对象提供了一个方法<code>getContext</code>用以获取画布上下文。</p>
<p><code>getContext</code>方法返回一个上下文对象，这个对象上提供了很多有用的用于操作canvas画布的API。</p>
<h3>1.2、设置用来清空canvas的背景色</h3>
<p>我们通过上一节获取的绘图上下文对象上的<code>clearColor</code>方法来设置背景色。</p>
<pre><code class="language-typescript">// 句法
void gl.clearColor(red, green, blue, alpha);
</code></pre>
<p><code>clearColor</code>方法接收四个值，分别对应RGBA的四个值，不过这个API使用的颜色分量值不是 0 - 255，而是 0 - 1。这个方法用于设置清空颜色缓冲时的颜色值。一旦指定了背景色之后，背景色就会驻存在WebGL系统中，在下一次调用<code>clearColor</code>方法前不会改变。</p>
<p>换句话说，如果将来什么时候你想用同样的颜色再清空一次绘图区，没有必要再调用<code>clearColor</code>。</p>
<h3>1.3、清空canvas</h3>
<p>我们使用在绘图上下文对象上的<code>clear</code>方法使用预设值来清空对应缓冲。这里使用的预设值就是上一节中使用<code>claerColor</code>设置的值。</p>
<p><code>clear</code>方法接收一个参数来指定需要清空的缓冲区，这些<strong>缓冲区预存值</strong>在绘图上下文对象上可以获得：</p>
<ul>
<li>gl.COLOR_BUFFER_BIT：颜色缓冲区，也就是我们需要用到来清空背景色的缓冲区值</li>
<li>gl.DEPTH_BUFFER_BIT：深度缓冲区</li>
<li>gl.STENCIL_BUFFER_BIT：模板缓冲区</li>
</ul>
<pre><code class="language-javascript">gl.clear(gl.COLOR_BUFFER_BIT)
</code></pre>
<p><img src="https://godlanbo.com/images/1647942704905.jpg" alt="clear清空缓冲区方法"></p>
<h2>2、着色器</h2>
<p>着色器是一种绘图机制，所有WebGL程序依赖它进行绘制。它提供了灵活且强大的绘制二维或者三维图形的方法，同样的，也带来了较为复杂的操作方法。</p>
<p>通过给<strong>顶点着色器</strong>和<strong>片元着色器</strong>赋值，然后将其传入WebGL系统中进行渲染，已达到我们想要的绘制图形。</p>
<h2>3、WebGL坐标系统</h2>
<p>着色器使用的是WebGL坐标系统。在WebGL坐标系统中，使用的是三维的坐标系统，因为会有绘制3D图形的需求，所以有Z轴是很容易理解的。WebGL坐标系统可以简单的概括为<strong>右手坐标系</strong>：</p>
<p><img src="https://godlanbo.com/images/1647942701955.jpg" alt="右手坐标系"></p>
<p>然后我们将WebGL坐标系统映射到我们的Web端的画布，也就是canvas中，是如下显示，原点在画布中间，上下左右边缘为一个单位：</p>
<p><img src="https://godlanbo.com/images/1647942702502.jpg" alt="canvas中WebGL坐标系统"></p>
<h2>4、使用着色器绘制</h2>
<p>当我们使用着色器进行绘制的时候，代码就不仅跑在JS环境里了，因为着色器是WebGL系统内的东西，我们需要将参数初始化完成，然后将其放入WebGL系统中进行渲染：</p>
<p><img src="https://godlanbo.com/images/1647942704058.jpg" alt="从JS到WebGL再到浏览器群"></p>
<p>着色器程序是底层是OpenGL，也就是更偏C语言形式，我们在JS中需要以字符串的形式编写它们，然后传入WebGL系统中运行：</p>
<pre><code class="language-javascript">// 顶点着色器（GLSL ES语言）
const VSHADER_SOURCE = `
  void main() {
    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
    gl_PointSize = 10.0;
  }
`
// 片元着色器（GLSL ES语言）
const FSHADER_SOURCE = `
  void main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
  }
`
</code></pre>
<h3>4.1、顶点着色器参数</h3>
<ul>
<li><strong>gl_Position</strong>：标识顶点的位置。类型：vec4向量</li>
<li><strong>gl_PointSize</strong>：点的尺寸（像素数）。类型：float</li>
</ul>
<p>关于其中的vec4向量和它的参数：</p>
<p><img src="https://godlanbo.com/images/1647942705216.jpg" alt="vec4向量和它的参数"></p>
<h3>4.2、片元着色器参数</h3>
<ul>
<li><strong>gl_FragColor</strong>：指定片元颜色（RGBA格式）</li>
</ul>
<p>在Web世界里面，我们的颜色分量值通常在0-255之间。但是WebGL是继承自OpenGL的，所以遵循传统的OpenGL颜色分量的取值范围，从0.0 - 1.0。</p>
<h3>4.3、绘制操作</h3>
<p>使用绘图上下文对象中的<code>drawArrays</code>方法来绘制图像。它用于执行顶点着色器。</p>
<pre><code class="language-typescript">// 语法
void gl.drawArrays(mode, first, count);
</code></pre>
<p><strong>mode</strong> 参数用于指定绘制的方式：</p>
<ul>
<li>gl.POINTS: 绘制一系列点。</li>
<li>gl.LINE_STRIP: 绘制一个线条。即，绘制一系列线段，上一点连接下一点。</li>
<li>gl.LINE_LOOP: 绘制一个线圈。即，绘制一系列线段，上一点连接下一点，并且最后一点与第一个点相连。</li>
<li>gl.LINES: 绘制一系列单独线段。每两个点作为端点，线段之间不连接。</li>
<li>gl.TRIANGLE_STRIP：绘制一个三角带。</li>
<li>gl.TRIANGLE_FAN：绘制一个三角扇。</li>
<li>gl.TRIANGLES: 绘制一系列三角形。每三个点作为顶点。</li>
</ul>
<p><strong>first</strong> 参数用于指定从哪个顶点开始绘制。</p>
<p><strong>count</strong> 参数用于指定绘制需要用到多少个点。</p>
<h2>5、外部指定顶点着色器参数</h2>
<p>前面顶点着色器的位置是固定写死在字符串中的，缺乏灵活性，既然在Web上进行绘制，通过JS控制着色器行为是非常必要的。</p>
<p>先试着将位置信息从外面JS动态传入，我们有两种方式从JS向着色器传入数据：<strong>attribute变量</strong>和<strong>uniform变量</strong>。而attribute变量传入的通常是与<strong>某个</strong>顶点着色器相关的数据，而uniform变量通常用来传递与顶点无关而普适的数据（例如所有顶点都有的属性）。所以我们这里从外部传入顶点着色器的位置使用attribute变量。</p>
<h3>5.1、attribute变量</h3>
<p>attribute 变量是一中GLSL ES变量，只有顶点着色器能够使用它。我们需要使用它，要经过三个步骤：</p>
<ul>
<li>在顶点着色器程序中声明attribute变量（<strong>声明</strong></li>
<li>将attribute变量赋值给 gl_Position 变量（<strong>赋值</strong></li>
<li>向attribute变量传输数据（<strong>传值</strong></li>
</ul>
<h3>5.2、声明attribute变量</h3>
<p>声明attribute变量，显然是需要在WebGL系统中进行，所以我们修改顶点着色器的字符串程序如下：</p>
<pre><code class="language-javascript">const VSHADER_SOURCE = `
  attribute vec4 a_Position;
  void main() {
    gl_Position = a_Position;
    gl_PointSize = 10.0;
  }
`
</code></pre>
<p>我们在全局作用域对attribute变量进行声明，它的声明格式必须按照以下格式：</p>
<p>&amp;lt;<strong>存储限定符</strong>&amp;gt;&amp;lt;<strong>类型</strong>&amp;gt;&amp;lt;<strong>变量名</strong>&amp;gt;</p>
<p><img src="https://godlanbo.com/images/1647942701837.jpg" alt="声明格式"></p>
<h3>5.3、将attribute变量赋值给gl_Position</h3>
<p>然后我们把这个变量赋值给 gl_Position ，可以看到 attribute 的类型，和我们一开始使用vec4函数的类型一样，都是vec4向量类型。</p>
<p>这样写好之后，着色器程序就已经准备好从外部接收值了。</p>
<h3>5.4、向attribute变量传输数据</h3>
<p>想要从JS中向着色器中的attribute变量赋值，需要通过获取到 attribute 变量的地址，通过地址向 attribute 变量进行传值。</p>
<p>我们通过绘制上下文对象提供的API，可以获取到attribute变量的存储位置：</p>
<pre><code class="language-javascript">// program 是着色器绘制时创建的程序对象
// 'a_Position' 是要获取存储位置的变量名称
const a_Position = gl.getAttribLocation(program, 'a_Position')
</code></pre>
<p><img src="https://godlanbo.com/images/1647942704077.jpg" alt="getAttribLocation函数参数"></p>
<p>获取到 attribute 存储位置之后，我们就可以调用绘制上下文对象提供的API: <code>vertexAttrib3f</code>向存储位置进行传值：</p>
<pre><code class="language-javascript">gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)
</code></pre>
<p><code>vertexAttrib3f</code>是用来向attribute变量赋值的API，它还有一系列同族函数：</p>
<pre><code class="language-typescript">// 同族函数及其语法
void gl.vertexAttrib1f(index, v0);
void gl.vertexAttrib2f(index, v0, v1);
void gl.vertexAttrib3f(index, v0, v1, v2);
void gl.vertexAttrib4f(index, v0, v1, v2, v3);
</code></pre>
<p>&amp;gt; 这些同族函数都是用来向attribute变量进行传值用的，只不过传递的变量个数不同，从函数名我们就能看出来。
&amp;gt;
&amp;gt; 它们都是遵守OpenGL中的函数命名规范：
&amp;gt;
&amp;gt; &amp;lt;基础函数名&amp;gt;&amp;lt;参数个数&amp;gt;&amp;lt;参数类型&amp;gt;
&amp;gt;
&amp;gt; vertexAttrib          3                   f
&amp;gt;
&amp;gt; &amp;quot; f &amp;quot; 表示参数类型是浮点数，对应其它的还有整数 &amp;quot; i &amp;quot;</p>
<p>当调用<code>vertexAttrib3f</code>方法后，传入的三个值一起被传给顶点着色器中的 a_Position 变量。你可以发现， a_Position 变量的类型是vec4，需要4个分量，但是我们调用的传值函数只传入了3个。这里第四个值会被默认设置为 1.0 ，也就是我们前面的齐次坐标值。</p>
<h2>6、坐标系变换</h2>
<p>我们知道，在浏览器上的坐标系和WebGL系统中的坐标系是完全不同的。浏览器的坐标系和canvas的坐标系是一样的，<strong>左上角是原点</strong>，然后<strong>横向向右是X轴正方向</strong>；竖向<strong>向下是Y轴正方向</strong>：</p>
<p><img src="https://godlanbo.com/images/1647942702267.jpg" alt="不同的坐标系"></p>
<p>由于上一节讲到了，我们可以通过JS外部传入一些参数，来影响WebGL的渲染，那么坐标系变换就显得很重要；我们需要把在浏览器坐标系里得到的坐标数据，转换成WebGL坐标系中的坐标数据之后，再传入WebGL系统进行渲染才不会出错。下面用一个**“在点击位置渲染一个点”**的实例来计算坐标系变换。</p>
<p>首先我们有一个点击事件，我们需要拿到当前点击的坐标：</p>
<pre><code class="language-javascript">function handleClick(event) {
  let dx = event.clientX
  let dy = event.clientY
}
</code></pre>
<p>由于canvas画布的原点不一定是和浏览器窗口的原点重合的，因为canvas画布不总在左上角的位置，它可能被放到屏幕中间。</p>
<p><img src="https://godlanbo.com/images/1647942701940.jpg" alt="canvas画布的位置"></p>
<p>所以我们要得到点击位置在canvas中的坐标，需要减去canvas画布距离浏览器左上边框的距离：</p>
<pre><code class="language-javascript">function handleClick(event) {
  let dx = event.clientX
  let dy = event.clientY
  const rect = event.target.getBoundingClientRect()
  dx = x - rect.left
  dy = y - rect.top
}
</code></pre>
<p>到这里我们得到了在canvas坐标系中，点击位置的坐标（dx，dy）。然后我们要把它转换到WebGL坐标系中。</p>
<p>在WebGL坐标系中，原点在中心位置，所以我们将点击的点横向移动<code>dx - canvas.width / 2</code>那么多，竖向移动<code>canvas.height / 2 - dy</code>这么多，就可以将点击的点移动到WebGL坐标系的原点处。所以可以得到在原点是canvas中心的坐标系里，点击位置的坐标是：<strong>（dx - canvas.width/2，canvas.height/2 - dy）</strong></p>
<p><img src="https://godlanbo.com/images/1647942702113.jpg" alt="坐标演示"></p>
<p>这样我们就得到了在WebGL坐标系中，点击位置的坐标值，然后由于WebGL坐标系统中，坐标系范围是从0到1，我们需要再把坐标值的范围映射的0到1的范围中来：</p>
<pre><code class="language-javascript">function handleClick(event, canvas) { // 追加一个canvas画布节点参数
  let dx = event.clientX
  let dy = event.clientY
  const rect = event.target.getBoundingClientRect()
  dx = x - rect.left
  dy = y - rect.top
  let dx_webgl = (dx - canvas.width / 2) / (canvas.width / 2)
  let dy_webgl = (canvas.height / 2 - dy) / (canvas.height / 2)
  // 通过 (dx_webgl, dy_webgl) 进行渲染
}
</code></pre>
<h2>7、外部指定片元着色器的参数</h2>
<p>前面通过 attribute 变量向顶点着色器传入了数据控制了它的渲染行为。只有顶点着色器才可以使用 attribute 变量，所以这里使用 uniform 变量向片元着色器传值。</p>
<h3>7.1、uniform变量</h3>
<p>和使用 attribute 变量一样，使用 uniform 变量同样需要经过声明，赋值，传递数据三个步骤。</p>
<h3>7.2、声明uniform变量</h3>
<p>在传递给WebGL系统的字符串里，我们来声明 uniform 变量：</p>
<pre><code class="language-javascript">const FSHADER_SOURCE = `
  precision mediump float;
  uniform vec4 u_FragColor;
  void main() {
    gl_FragColor = u_FragColor;
  }
`
</code></pre>
<p><code>precision mediump float</code>是在规定GPU使用的浮点数精度，这个后面会学到。</p>
<p>声明 uniform 变量的方式和 attribute 一样：</p>
<p><img src="https://godlanbo.com/images/1647942701835.jpg" alt="声明uniform变量"></p>
<h3>7.3、将uniform变量赋值给着色器</h3>
<p>将 uniform 变量在WebGL程序中赋值给管理颜色的变量：<code>gl_FragColor</code>。这样后面我们传入的值就可以修改着色器的行为了。</p>
<h3>7.4、向uniform变量传输数据</h3>
<p>同样通过地址来向 uniform 变量传值：</p>
<pre><code class="language-javascript">const u_FragColor = gl.getUniformLocation(program, 'u_FragColor')
</code></pre>
<p><img src="https://godlanbo.com/images/1647942702516.jpg" alt="获取uniform地址接口"></p>
<p>然后调用<code>uniform4f</code>向着色器注入数据：</p>
<pre><code class="language-javascript">gl.uniform4f(u_FragColor, 0.0, 0.0, 0.0, 1.0)
</code></pre>
<p><img src="https://godlanbo.com/images/1647942703377.jpg" alt="截图"></p>
<p>通过这个API，我们可以把想传递的值从JS传入WebGL系统。它和 attribute 传值的API类似，也可以通过最后两个字母来判断函数参数个数和参数类型，也有一系列同族函数：</p>
<p><img src="https://godlanbo.com/images/1647942703547.jpg" alt="同族函数"></p>
<p>和 attribute 的同族API一样，不传的值都会有默认值，在这里前三个参数的默认值是 0.0，最后一个参数的默认值是 1.0。</p>

        ]]></description>
      </item><item>
        <title>【TS类型体操学习】(二) - 联合类型的分配艺术</title>
        <link>http://godlanbo.com/blogs/43</link>
        <guid>43</guid>
        <pubDate>Thu, 03 Mar 2022 11:23:17 GMT</pubDate>
        <description><![CDATA[
          <h2>前言</h2>
<p>对于联合类型在类型系统中的一些特性和行为，开始时一直不甚清楚，模模糊糊的用着。初见联合类型的分配特性是在写<code>MyExclude</code>这个题的时候，题目要求是从前一个联合类型中，剔除后一个联合类型的值：</p>
<pre><code class="language-typescript">Equal&amp;amp;lt;MyExclude&amp;amp;lt;&amp;quot;a&amp;quot; | &amp;quot;b&amp;quot; | &amp;quot;c&amp;quot;, &amp;quot;a&amp;quot;&amp;amp;gt;, &amp;quot;b&amp;quot; | &amp;quot;c&amp;quot;&amp;amp;gt;
</code></pre>
<p>初见的时候，没什么思路，当时已经马马虎虎会用 <code>extends</code>条件表达式了，但是不太清楚联合类型在条件表达式中的处理过程，经过一番折腾，算是渐渐理解了联合类型在遇到条件表达式时候的行为。</p>
<p>下面我用几个题目，来联合类型分配的特性。</p>
<h2>分配行为的初理解：43-easy-exclude</h2>
<h3>题目简述</h3>
<ul>
<li><a href="https://github.com/type-challenges/type-challenges/blob/master/questions/43-easy-exclude/README.md">题目链接</a></li>
</ul>
<p>就如前言中描述，从前一个联合类型中，删除掉后面联合类型中的值。</p>
<h3>思路</h3>
<p>我们和上一篇文章一样，首先还是从JS的思路来考虑，基本解决的办法就是两个循环，依次去匹配对应的值来进行筛选：</p>
<pre><code class="language-javascript">function exclude(arr1, arr2) {
  let res = []
  for(let val of arr1) {
    if (!arr2.includes(val)) {
      res.push(val)
    }
  }
  return res
}
</code></pre>
<p>所以我们转换成TS的大概思路就是，循环第一个联合类型中的值，然后依次去和第二个联合类型匹配，如果有，就不要，没有，就保留。</p>
<p>那么这里需要解决几个问题：</p>
<ul>
<li>遍历联合类型</li>
<li>判断一个值是否在联合类型里</li>
<li>累积保存结果</li>
</ul>
<h4>遍历联合类型</h4>
<p>这里就要提到官方文档上面对于联合类型在条件语句中的行为描述：</p>
<p>&amp;gt;  <strong>Distributive Conditional Types</strong>
&amp;gt;
&amp;gt; When conditional types act on a generic type, they become distributive when given a union type.</p>
<p>大致意思就是，当在条件语句中的泛型的值，被赋予联合类型时，这时就会发生分配行为，举个例子：</p>
<pre><code class="language-typescript">type ToArray&amp;amp;lt;Type&amp;amp;gt; = Type extends any ? Type[] : never;

type StrArrOrNumArr = ToArray&amp;amp;lt;string | number&amp;amp;gt;;
// type StrArrOrNumArr = string[] | number[]
</code></pre>
<p>从上面的例子可以看到，联合类型在面对<code>extends</code>时，没有被当做一个整体（如果是的话，那么结果应该是 <code>(string | number)[]</code>），而是被分别拆开，就像<strong>遍历</strong>一样，依次取出值进行运算，所以上面的例子运算的时候实际可以类比：</p>
<pre><code class="language-typescript">type StrArrorNumArr =
(string extends any ? string[] : never) |
(number extends any ? number[] : never)
</code></pre>
<p>从这里也可以知道最后一点<strong>累积保存结果</strong>的解决方案，联合类型的分配行为之后，会把所有的结果最后做一个联合，正好解决了这个问题。</p>
<p>所以我们循环第一个联合类型：</p>
<pre><code class="language-typescript">type MyExclude&amp;amp;lt;T, U&amp;amp;gt; = T extends any ? xxx : xxx
</code></pre>
<h4>判断一个值是否在联合类型内</h4>
<p>涉及判断，这里我们自然还是使用条件语句<code>extends</code>进行操作，<code>A extends B</code>是在判断A是否可以赋值给B，简单理解就是A是子类，B是父类，子类可以赋值给父类，就能进肯定分支。</p>
<p>这里父类其实是更宽泛于子类的，所以套到联合类型上来也是一样，更加宽泛的联合类型做B，可以用来判断被它包含的元素：</p>
<pre><code class="language-typescript">'a' extends 'a' | 'b' // true
'a' | 'c' extends 'a' | 'c' | 'b' // true
'c' extends 'a' | 'b' // false
</code></pre>
<p>所以我们只要能循环出第一个联合类型中的元素，就可以很轻松的判断它是否在第二个联合类型中。结合第一小节中循环：</p>
<pre><code class="language-typescript">type MyExclude&amp;amp;lt;T, U&amp;amp;gt; = T extends U ? never : T
</code></pre>
<p>这里由于T泛型是联合类型，发生分配，分别对第二个联合类型进行条件判断，如果存在，结果就是never，不是（意味着不在第二个联合类型中），所以得到 T。</p>
<p>最后得到类似 T1 | T2 | never = T1 | T2 的结果。</p>
<pre><code class="language-typescript">type MyExclude&amp;amp;lt;'a' | 'b', 'a'&amp;amp;gt;
=
('a' extends 'a' ? never : 'a') | // never
('b' extends 'a' ? never : 'b')   // 'b'
= never | 'b'
= 'b'
</code></pre>
<h2>分配发生的条件</h2>
<p><img src="https://godlanbo.com/images/1646306541199.jpg" alt="90fea252d1ef5b33d55f1580188a5b7f.png"></p>
<p>我们来看下这个例子，通过上面例题的描述，我们知道联合类型在遇到<code>extends</code>条件语句的时候，会发生分配行为。但是在上图的例子中，可以明显看到，<code>temp</code>的结果，符合我们对于联合类型分配行为的预期；而<code>temp1</code>，在表达式完全和 <code>Exclude$</code>等同的情况下，结果却完全不符合联合类型分配行为之后的结果：</p>
<pre><code class="language-typescript"> // 预期的分配行为
type res = 'name' | 'age' | 'gender' extends 'name' | 'age' ? never : T
= ('name' extends 'name' | 'age' ? never : 'name') |
('age' extends 'name' | 'age' ? never : 'age') |
('gender' extends 'name' | 'age' ? never : 'gender')
= never | never |'gender'
= 'gender'
</code></pre>
<p>这是为什么呢？当时我也很疑惑，其实这里是因为错误的判读了文档的内容，让我们再回头看下TS官方文档对于分配行为的原文描述：</p>
<p>&amp;gt; When conditional types act on a <strong>generic type</strong>, they become distributive when given a union type.</p>
<p>我们可以注意到，在文档中描述联合类型分配行为的时候，不仅提到了在条件语句中，还有一句**“When conditioncal types act on a generic type”**，这里的意思是，当条件作用于泛型的时候。</p>
<p>所以这里文档描述的是，当<code>extends</code>两头是泛型进行比较的时候，给泛型的值如果是联合类型，才会发生分配行为。反之，如果<code>extends</code>两头不是泛型，那么即使是联合类型，也不会发生分配，就当做一整个值来使用了，所以上面的例子中，<code>temp1</code>的结果才会是那样。</p>
<h2>空的联合类型never：1042-medium-isnever</h2>
<h3>题目简述</h3>
<ul>
<li><a href="https://github.com/type-challenges/type-challenges/blob/master/questions/1042-medium-isnever/README.md">题目链接</a></li>
</ul>
<p>判断输入值是否是<code>never</code></p>
<pre><code class="language-typescript">type res = IsNever&amp;amp;lt;1&amp;amp;gt; // false
type res1 = IsNever&amp;amp;lt;never&amp;amp;gt; // true
</code></pre>
<h3>思路</h3>
<p>看起来似乎非常简单，由于<code>never</code>的特性，只有<code>never</code>值，才可以赋值给<code>never</code>类型；结合我们常用的条件判断语句<code>extends</code>的本质：<code>T1 extends T2</code> 其实是在判断 <code>T1</code> 能否赋值给 <code>T2</code>。</p>
<p>所以很容易写下解法：</p>
<pre><code class="language-typescript">type IsNever&amp;amp;lt;T&amp;amp;gt; = T extends never ? true : false
</code></pre>
<p>但是，写完之后，你会发现，它并不如你想的那般工作：</p>
<pre><code class="language-typescript">type res = IsNever&amp;amp;lt;never&amp;amp;gt; // never
</code></pre>
<p>你发现本应该结果为<code>true</code>的输入值得到了<code>never</code>的结果。这是非常奇怪的结果，也就是说当输入值是never的时候，extends既没有进肯定分支（true），也没有进否定分支（false）。</p>
<p>这是为什么呢，在github上找到了讨论类似问题的issue，下面有官方的回答：</p>
<p>&amp;gt; never is essentially an empty union type, and using union types with conditional type inference results in distributive behaviour. Distributing over an empty union results in an empty union, as there's nothing to distribute over.</p>
<p>大致意思就是，<code>never</code>本质上是一个空的联合类型，所以当它赋值给泛型，就会在条件语句中发生分配行为，此时空联合类型自然是无法分配的，所以结果是<code>never</code>。</p>
<p>这也解释了分配行为后，结果联合在一起的时候，<code>never</code>会被忽视的现象：</p>
<pre><code class="language-typescript">'a' | never = 'a'
</code></pre>
<p>所以，为了避免这种不可预期的分配行为，我们将泛型比较的时候放入数组进行规避，这种写法也是在<a href="https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types">官方文档</a>中的写法：</p>
<pre><code class="language-typescript">type IsNever&amp;amp;lt;T&amp;amp;gt; = [T] extends [never] ? true : false
</code></pre>
<h2>结语</h2>
<p>通过这篇文章，应该是总结了我目前学习TS到现在，遇到的所有关于联合类型的特性。希望我在后面能够更加熟练运用以解决其它问题。</p>
<p>&amp;gt; 永远进步</p>

        ]]></description>
      </item><item>
        <title>【TS类型体操学习】(一) - 关于数组/元组，字符串类型的常见遍历</title>
        <link>http://godlanbo.com/blogs/42</link>
        <guid>42</guid>
        <pubDate>Fri, 11 Feb 2022 10:06:09 GMT</pubDate>
        <description><![CDATA[
          <h2>背景</h2>
<p>TypeScript作为前端领域中越来越火热的一门技术，一直想花费一些功夫去精进一下它。原来只是简单的阅读文档学习了一些基本语法，在工作和日常学习中也只是用来当做普通的类型描述，有点类似CSS的使用方式在使用。</p>
<p>实际上TS作为有泛型的类型描述语言，具有<a href="https://zh.wikipedia.org/wiki/%E5%9C%96%E9%9D%88%E5%AE%8C%E5%82%99%E6%80%A7">图灵完备</a>性，不仅仅只是可以做到一些简单的类型描述；所以学习TS的高级用法能够更加深入的了解TS类型系统。</p>
<p>正好趁着寒假有时间，来过一下TS的类型训练题集 ——— <a href="https://github.com/type-challenges/type-challenges/blob/master/README.zh-CN.md">ts-challenges</a>，中文常翻译为类型体操。下面就用里面的几个例题来总结目前我学习到的关于遍历相关的写法。</p>
<h2>字符串的遍历：531-medium-string-to-union</h2>
<h3>题目简述</h3>
<ul>
<li><a href="https://github.com/type-challenges/type-challenges/blob/master/questions/531-medium-string-to-union/README.md">题目链接</a></li>
</ul>
<p>将<code>string</code>类型转化为<code>union</code>类型。</p>
<pre><code class="language-typescript">type Test = '123';
type Result = StringToUnion&amp;amp;lt;Test&amp;amp;gt;; // expected to be &amp;quot;1&amp;quot; | &amp;quot;2&amp;quot; | &amp;quot;3&amp;quot;
</code></pre>
<h3>思路</h3>
<p>我们先从JS的思路上来考虑，这里其实就类似于要去遍历这个传入的String字面量类型的值。这里我们用模板字符串 + 类型推断就可以简单的实现：</p>
<pre><code class="language-typescript">type StringToUnion&amp;amp;lt;T extends string&amp;amp;gt; = T extends `${infer F}${infer R}`
  ? F | StringToUnion&amp;amp;lt;R&amp;amp;gt;
  : never
</code></pre>
<p>下面来简单分析一下这种写法，首先我们使用<code>extends</code>关键字约束输入的泛型值，是一个字符串；然后我们用TS中常见的条件判断语句的写法<code>T extends X ? 1 : 2</code>，对输入值进行判断，然后这里使用模板字符串，构造：</p>
<pre><code class="language-typescript">T extends `${infer F}${infer R}` ? true : false
</code></pre>
<p>这样一种写法，这里使用了<code>infer</code>关键字，这是TS类型系统中非常有用的一个工具，它可以帮助你进行类型推断，并且将推断的类型赋值出来，上面的例子里，就像是<code>var F, var R</code>一样，声明了两个类型变量。然后TS这里就会对符合条件的输入值就行推断，比如这里我们的输入值T，是一个字符串字面量类型 <code>123</code>，为了满足模板字符串<code>${F}${R}</code>的形式，TS推断出</p>
<ul>
<li>F的类型是<code>'1'</code>的字符串字面量类型（相当于 var F = '1'）</li>
<li>R的类型是<code>'23'</code>的字符串字面量类型（相当于 var R = '23'）</li>
</ul>
<p>注意上面这些，都是<strong>类型</strong>，而不是<strong>值</strong>；<code>'1'</code>不是JS的中的值，而是一个字符串字面量类型，也就是TS系统中的值，在我们书写过程中，一定要分清JS中的值，和TS类型系统中的值，在TS中我们只能使用对于TS来说是变量的值，也就是类型值。</p>
<p>经过上面的操作，我们就成功利用<code>infer</code>关键字，将字符串字面量的第一个字符当做单独类型拆了出来，那么后续的字符串拆分，就直接递归就好了。</p>
<h2>数组的遍历：459-medium-flatten</h2>
<h3>题目简述</h3>
<ul>
<li><a href="https://github.com/type-challenges/type-challenges/blob/master/questions/459-medium-flatten/README.md">题目链接</a></li>
</ul>
<p>将一个数组深度打平。</p>
<pre><code class="language-typescript">type flatten = Flatten&amp;amp;lt;[1, 2, [3, 4], [[[5]]]]&amp;amp;gt; // [1, 2, 3, 4, 5]
</code></pre>
<h3>思路</h3>
<p>数组打平，JS中一个老生常谈的问题，我们先用JS简单实现一下，在对换到TS中来实现，这样写的时候思路会清晰一些：</p>
<pre><code class="language-javascript">function myFlatten(arr) {
  let res = []
  for(let val of arr) {
    res = res.concat(Array.isArray(val) ? myFlatten(val) : val)
  }
  return res
}
</code></pre>
<p>写好JS版本之后，我们简单分析一下需要哪些操作：<strong>一个用来累积结果的数组</strong>，<strong>遍历输入值</strong>，<strong>递归打平</strong>。</p>
<h4>累积结果</h4>
<p>首先是累积结果的数组。在JS里面我们可以简单的声明一个变量来存放，但是在TS中，能在Type中声明新变量的地方，就只能使用上面我们提到的<code>infer</code>关键字，但是这个关键字并不是自由声明的，它是用来推断的，所以我们否定这个写法。</p>
<p>所以这里我们唯一可以利用的地方是Flatten类型的输入值，由于我们这里肯定递归的去调用这个类型做处理，可以考虑把累积的结果一起通过递归传递下去，比如这样：</p>
<pre><code class="language-typescript">type Flatten&amp;amp;lt;T extends unkonw[], Temp extends unkonw[] = []&amp;amp;gt; = ...
</code></pre>
<p>这样，当我们直接传一个值就去的时候，就相当于声明了一个 Temp 数组，这里就可以用它来做值的积累。具体如何做，可以往后看。</p>
<h4>遍历输入值</h4>
<p>对照JS方案，解决了累积值的数组之后，我们就需要开始遍历输入的数组值。这里遍历的方案和上一个例题中遍历String类型的方案差不多，也是利用<code>infer</code>关键字，对输入值进行拆分，然后递归后续的值，达到一个循环的效果（因为递归本来就可以被优化成循环，我们这里其实就是把循环劣化成了递归）：</p>
<pre><code class="language-typescript">type Flatten&amp;amp;lt;T extends unkonw[], Temp extends unkonw[] = []&amp;amp;gt;
= T extends [infer First, ...infer Rest] ? /* 递归处理 */ : Temp
</code></pre>
<p>这里我们这个题目大概的模型就快出来了，这里<code>extends</code>语句如果走到<code>false</code>分支，就说明输入值是一个空数组，那么我们直接返回累积数组就好了（对应JS中直接返回res）。</p>
<h4>递归处理</h4>
<p>遍历和累积值的问题都解决了，我们来写最重要的，循环中的处理部分。我们看JS方案做了什么处理：</p>
<p>判断当前值是不是数组：</p>
<ul>
<li>
<p>不是：直接放入累积数组</p>
</li>
<li>
<p>是：对当前值递归调用打平函数，把结果放入累积数组</p>
</li>
</ul>
<p>那么我们这里TS也走相同的处理就好了：</p>
<pre><code class="language-typescript">type Flatten&amp;amp;lt;T extends unkonw[], Temp extends unkonw[] = []&amp;amp;gt;
= T extends [infer First, ...infer Rest] 
  ?
  /* 递归处理，这里的当前值就是First */
  Frist extends unkonw[] // 当前值是数组吗？
    // 是-剩下的继续递归，累积数组放打平后的结果
    ? Flatten&amp;amp;lt;Rest, [...Temp, ...Flatten&amp;amp;lt;Frist&amp;amp;gt;]&amp;amp;gt;
    // 不是-剩下的继续递归，直接将当前值放入累积数组
    : Flatten&amp;amp;lt;Rest, [...Temp, Frist]&amp;amp;gt;
  : Temp
</code></pre>
<p>我们可以直接通过扩展运算符，在TS中拼接我们需要的数组，就代替JS中push进数组的操作：</p>
<pre><code class="language-javascript">// temp.push(val) =&amp;amp;gt; [...temp, val]
// temp.push(myFlatten(val)) =&amp;amp;gt; [...temp, ...Flatten&amp;amp;lt;val&amp;amp;gt;]
</code></pre>
<h2>总结</h2>
<p>简单的学习TS能够带来的收益非常有限，我原来长时间处于这种状态，也没有找到更好的TS徐徐渐进的学习方法，因为TS官方手册是全英文的，对于我来说看起来非常困难。</p>
<p>但是这个TS类型体操是一个学习TS很好的工具，它能从简单到复杂，逐步让你感受和学习TS类型系统中的各种特性，这种目标明确（做题），用到什么学习什么（题目涉及知识点）的路线，非常适合我，刷的过程中学习到了很多，后续也会更新更多笔记。</p>
<p>&amp;gt; 永远进步</p>

        ]]></description>
      </item><item>
        <title>🚀前端首屏性能优化篇 - 非SSR方案提升</title>
        <link>http://godlanbo.com/blogs/41</link>
        <guid>41</guid>
        <pubDate>Tue, 28 Dec 2021 05:50:02 GMT</pubDate>
        <description><![CDATA[
          <h2>1、前言</h2>
<p>网站的性能一直是前端工程师努力的方向之一，更加流畅的体验，更加快速的页面呈现，都是好的web网站的指标之一。</p>
<p>如今随着网站能包含的元素和内容越来越丰富和多元，在访问网站的时候需要加载的资源变得更多，我们不能再放手不管让网站自由加载所有内容，这样会让用户等待资源加载的时间过长，体验下降。</p>
<p>这次，就以我的个人博客为例子，从最开始的荒芜状态，向业界网站性能标准“秒开”做一次系统性的性能优化。</p>
<p>以下为<strong>初始数据</strong>，数据来源使用腾讯云RUM性能监控。</p>
<p><img src="https://godlanbo.com/images/1640670682448.jpg" alt="image20211223153100648.png"></p>
<h2>2、文件压缩</h2>
<p>网站打开的时候需要加载许多的资源，当加载的资源很多很大的时候，我们网站的打开速度就会变慢。在网络中传输大文件是一种非常耗时的行为，所以我们需要将我们的网站资源文件（JS，CSS，图片等）进行压缩，减小它们的体积，这样我们的网站就可以更加快速的下载到本地呈现给用户。</p>
<h3>2.1、资源文件的压缩（CSS，JS，html等）</h3>
<p>这些文件，我们通常采用<code>gzip</code>压缩之后再进行传输，这是一个全部浏览器都支持的压缩算法。浏览器接收到gzip方式压缩的资源后，会自动把它解压缩使用。下图可以看到<code>gzip</code>压缩能带来很大的收益。</p>
<p><img src="https://godlanbo.com/images/1640670693467.jpg" alt="image20211223221003387.png"></p>
<p>如何对这些资源开启<code>gzip</code>压缩，可以参考我的另一篇文章<a href="https://godlanbo.com/blogs/22">开启gzip压缩，让你的资源下载更快</a>。</p>
<h3>2.2、图片压缩</h3>
<p>现在原图的质量越来越好，动辄几mb甚至十几mb，要知道对于网页加载来说，几百kb的网络加载都是昂贵的，所以我们要尽可能减小图片的大小，而减小图片大小的方式就是压缩图片。</p>
<p>高清的图片和经过一定压缩后的图片呈现出来往往肉眼很难分辨他们的质量，所以我们大多情况不用担心压缩导致的图片模糊等情况。但是在尺寸上，压缩却可以带来非常大的收益，这种收益在<strong>大图</strong>上尤为明显：</p>
<ul>
<li>压缩前</li>
</ul>
<p><img src="https://godlanbo.com/images/1640670709329.jpg" alt="image2021122322284333616402697364252.png"></p>
<ul>
<li>压缩后</li>
</ul>
<p><img src="https://godlanbo.com/images/1640670720570.jpg" alt="image20211223222950199.png"></p>
<p>将所有图片压缩后，我们将会得到一个几乎和原来没有差别的网页呈现，但是现在你的网页需要加载的资源就被大大的减少了。压缩图片的网站推荐 <a href="https://tinify.cn/">https://tinify.cn/</a>，方便好用，也可以接入API自动化。</p>
<h3>2.3、字体文件压缩</h3>
<p>一个完整的字体文件（<strong>这里指ttf后缀的字体文件</strong>）往往非常大，几百kb甚至几mb。一个业界比较常见的字体文件压缩方案是通过字蛛，将字体文件拆分。因为一个完整的字体文件之所以大，是因为它内部包含了这种字体的所有的文字，但是往往网站不需要用到全部的字，所以只把需要的拆出来，这样就大大减少了字体文件的体积。</p>
<p>但是上述的压缩场景对于SPA项目来说不太可能实现，因为字蛛是通过扫描HTML来获取页面需要哪些字的，SPA项目的HTML没有经过加载空空如也，啥也没有；而且对于含有输入场景的网页，由于用户的输入有不确定性，也不能很好的去拆分字体文件，所以这种方案使用场景有限。</p>
<p>我们常用的字体文件格式是<strong>TTF（TrueType Font）</strong>，由苹果和微软为 PostScript 而开发的字体格式，它被开发时就没有考虑为web使用，所以它们没有经过压缩，体积往往较大。</p>
<p>所以我们采用另一种字体文件格式来代替它，那就是<strong>WOFF（Web Open Font Format）</strong>。该格式<strong>完全是为了 Web 而创建</strong>，由 Mozilla 基金会、微软和 Opera 软件公司合作推出。WOFF 字体均经过 WOFF 的编码工具压缩，<strong>文件大小一般比 TTF 小 40%</strong>，加载速度更快，可以更好的嵌入网页中。目前所有的主流浏览器都支持这个更加棒的字体，可以放心使用：</p>
<p><img src="https://godlanbo.com/images/1640670734023.jpg" alt="image20211224153150887.png"></p>
<p>除此之外还有更先进的WOFF2，这是WOFF的下一代，在WOFF原有的基础进一步压缩了30%的大小，不过毕竟是下一代，目前它的支持程度并没有WOFF那般普及：</p>
<p><img src="https://godlanbo.com/images/1640670743745.jpg" alt="image20211224153247995.png"></p>
<p>所以考虑兼容性的情况下，我们对字体的书写往往采用如下的兼容写法，从上到下依次从最新的排到最老的，这样用户代理会从上到下依次尝试加载解析对应的字体，尝试成功后就会停止加载。</p>
<pre><code class="language-css">@font-face {
  font-family: 'Fira Sans';
  src: url('./fonts/FiraSans-ExtraBold.woff2') format('woff2'),
    url('./fonts/FiraSans-ExtraBold.woff') format('woff'),
    url('./fonts/FiraSans-ExtraBold.ttf') format('truetype');
  font-weight: 800;
}
</code></pre>
<h2>3 、异步加载，按需引入</h2>
<p>对于我们的网站来说，用户刚进来看到的页面，往往是不需要加载全部资源的，当用户浏览其它页面的时候，才会用到那些资源，自然这些资源我们就可以把他们的加载往后放一放，优先加载首屏需要的资源，这就是分包策略和<strong>异步加载</strong>。当然复杂的分包策略和异步加载的代码，我们现在基本不用担心，项目一般都是通过webpack配置好即可。</p>
<p>说完异步加载，我们再来说说<strong>按需引入</strong>。前面说了这么多，其实都是在减小我们网站需要加载的内容体积，那么就有一个非常值得关注的地方，<strong>资源利用率</strong>。你加载100kb的文件，结果只使用了其中10kb的内容，剩下90kb就被浪费掉了，这些网络资源本可以去加载其它内容，让你的网站更快的呈现，而不是去加载这无效的90kb内容。所以我们在打包项目的时候，要尽可能的减少不必要的内容引入。</p>
<p>用<a href="https://element.eleme.cn/#/zh-CN/component/quickstart">Element-ui</a>举个例子，你的项目应该很少会用到整个UI库的所有组件，所以我们就需要把没有用到的组件的相关代码从最后的产物中剔除来减小我们的产物体积。详细的操作方法可以看官方文档，这里不再赘述，主要是为了说明按需引入的重要性，整个Element-ui打包出来足足有好几百kb的JS文件和一两百kb的CSS文件，这些可能只是因为你用了其中那么2 3个组件，这明显是划不来的，所以按需引入的重要性非常高，引入第三方库的时候因随时注意，是否因为使用一个小功能而大大增加打包后的产物体积。</p>
<h2>4、CDN加速</h2>
<p>我们的网站资源都需要从服务器上加载，通常我们都把所有的资源放在自己的服务器上，包括HTML和HTML引用的CSS，JS还有图片等。但是我们自己的服务器从各方面来说，都不适合去承载大量的资源请求。所以这个时候就需要用到<strong>CDN（Content delivery network）</strong>。</p>
<p>&amp;gt; <strong>内容分发网络</strong>（英语：<strong>C</strong>ontent <strong>D</strong>elivery <strong>N</strong>etwork或<strong>C</strong>ontent <strong>D</strong>istribution <strong>N</strong>etwork，缩写：<strong>CDN</strong>）是指一种透过<a href="https://zh.wikipedia.org/wiki/%2525E4%2525BA%252592%2525E8%252581%2525AF%2525E7%2525B6%2525B2">互联网</a>互相连接的电脑网络系统，利用最靠近每位用户的服务器，更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户，来提供高性能、可扩展性及低成本的网络内容传递给用户。 --维基百科</p>
<p>简单的理解就是把你需要加载的资源不是放在你自己的服务器上，而是放在一个托管的服务器上，这个托管的服务器有着更好的性能，更稳定的服务，可以为用户提供更快的访达。这种情况下，我们把HTML放在自己的服务器上，然后把HTML所链接的资源放在CDN上，这样，对于我们自己的服务器来说，就只需承担HTML文档的流量，这是比较小的，然后HTML文档在客户端被解析之后，去对应的CDN上获取各种资源（JS，CSS，图片等）。这样网站所有加载的资源，都能获得一个不错的速度。</p>
<p><img src="https://godlanbo.com/images/1640670757791.jpg" alt="image20211227144006470.png"></p>
<h2>5、离线缓存（Service Worker）</h2>
<h3>5.1、简介</h3>
<p>除了上述说的手段，减小资源体积，提高资源传输速度之外，我们还可以有一些优化方式，那就是缓存。我们的资源不总是在更新，所以我们没必要让用户每次访问都重新去拉取一遍资源，我们可以让这些资源缓存在用户本地，等待用户再次访问的时候，可以直接拿出来用。从本地读取肯定是要比网络请求快的。</p>
<p>那么我们如何缓存呢？这里就不讲什么协商缓存和强缓存了，这种网上太多了，不再赘述，这次讲另一种缓存，使用Service Worker。</p>
<p>&amp;gt; Service workers 本质上充当 Web 应用程序、浏览器与网络（可用时）之间的代理服务器。这个 API 旨在创建有效的离线体验，它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源。它还提供入口以推送通知和访问后台同步 API。 -- MDN</p>
<p>SW（以下Service Worker都简称SW）是一个比较新的API，它主要是用来解决离线情况下，使用本地缓存的资源来加载web程序。对于SW的介绍、基础用法和常见API，可以参考<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API/Using_Service_Workers">MDN上的SW的使用教程</a>。本文这里直接从使用说起，如何接入项目进行使用。</p>
<p>SW的API并不简单，从0开始去规划一个项目的本地资源缓存的SW代码是一个相当大的工程，好在Google已经有完善的解决方案，那就是<a href="https://developers.google.com/web/tools/workbox/guides/get-started">workbox</a>，它提供了很多工具来帮助我们对请求的资源进行管理和缓存。</p>
<h3>5.2、项目引入（vue-cli项目例子）</h3>
<p>下面使用vue-cli项目进行示范，如何在项目中接入SW和workbox：</p>
<pre><code class="language-js">// vue.config.js
// 首先需要安装 serviceworker-webpack-plugin 插件，它会帮助我们引入SW
const ServiceWorkerWebpackPlugin = require('serviceworker-webpack-plugin')

module.exports = {
  chainWebpack: config =&amp;amp;gt; {
    config.plugin('sw').use(ServiceWorkerWebpackPlugin, [
      {
        // 你 sw 代码文件所在的位置
        entry: path.join(__dirname, 'src/sw.js')
      }
    ])
  }
}
</code></pre>
<p>首先我们需要安装 <code>serviceworker-webpack-plugin</code>插件，他可以帮助我们往项目中引入SW文件，因为现在项目都是会经过webpack打包的项目，你不可能像单HTML文件那样，直接相对路径引用SW文件。</p>
<pre><code class="language-js">// main.js
import runtime from 'serviceworker-webpack-plugin/lib/runtime'

if ('serviceWorker' in navigator) {
  runtime.register()
}
</code></pre>
<p>然后我们在<code>main.js</code> 中，检测浏览器是否支持SW，如果支持就注册SW，这里直接调用注册函数就好，插件会根据你在config中的配置自动读取对应的文件。</p>
<pre><code class="language-js">// sw.js
import { registerRoute } from 'workbox-routing'
import { StaleWhileRevalidate, CacheFirst } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'
import { CacheableResponsePlugin } from 'workbox-cacheable-response'

// ...
// 图片字体等静态资源缓存
registerRoute(
  /.+\.(?:png|gif|jpg|jpeg|svg|ico|ttf|woff|woff2)$/,
  new CacheFirst({
    cacheName: 'assets',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60 // 30 Days
      })
    ]
  })
)
// css js 资源缓存
registerRoute(
  /.+\.(?:js|css)$/,
  new StaleWhileRevalidate({
    cacheName: 'resource',
    plugins: [
      new CacheableResponsePlugin({
        statuses: [200]
      })
    ]
  })
)
</code></pre>
<p>然后就是sw.js，这里就是写SW代码的地方。这里我们需要安装一些<code>workbox</code>相关的工具，<code>workbox</code>提供了很多工具，可以根据需要多安装或者少安装，下面几个是我推荐的：</p>
<pre><code class="language-bash">npm i workbox-routing workbox-strategies workbox-expiration workbox-cacheable-response -D
</code></pre>
<h3>5.3、workbox系列工具使用说明</h3>
<p>下面先大致介绍一下刚刚安装的那些包，和里面用到的一些东西。</p>
<h4>5.3.1、workbox-routing</h4>
<p>这是最重要的工具，是一定要安装的，它负责拦截我们发出去的请求，然后对这些个请求，进行相应的缓存处理。</p>
<pre><code class="language-js">// registerRoute 是最重要的方法，用来拦截请求，第一个参数可以是正则也可以是一个函数
// 是正则的话，当请求的URL匹配的时候，就会对这个请求执行对应得 CacheHandler
registerRoute(
  /.+\.(?:png|gif|jpg|jpeg|svg|ico|ttf|woff|woff2)$/,
  // 这里暂时用 handler 代替
  cacheHandler
)
// 是函数的话，会传入一些请求相关的参数，你可以在这里做一些判断，然后返回Boolean，返回是
// true 的话就适用对应得 CacheHandler
registerRoute(
  ({ request, url }) =&amp;amp;gt; {
    return request.destination.length === 0 &amp;amp;&amp;amp; url.href.match(/.+\/api\//)
  },
  cacheHandler
)
</code></pre>
<h4>5.3.2、workbox-strategies</h4>
<p>这个工具包里面提供了一些预设的缓存处理策略，能够满足我们绝大多数要求</p>
<h5>缓存策略介绍</h5>
<ul>
<li>
<p>Stale-While-Revalidate：这个策略的工作路线如下图，它会优先从缓存中读取数据，同时每次请求也会在后台去服务器请求来更新数据。当缓存中没有数据的时候，就会把服务器的请求返回给客户端使用，并且更新缓存。</p>
<p>它非常适合用来缓存一些可变的资源，比如CSS和JS，你可以享受到缓存的速度，即使远端资源更新之后，客户端也只需要刷新下页面就能更新缓存，不用担心读到旧缓存。</p>
</li>
</ul>
<p>​</p>
<p><img src="https://godlanbo.com/images/1640670795938.jpg" alt="image20211227184911526.png"></p>
<p>​</p>
<ul>
<li>
<p>Cache First：这个策略工作路线如下图，它同样优先去读取缓存中的内容，但是如果能读到，就不会发起网络请求，只有在读不到缓存的时候，才会发起网络请求，将请求结果返回客户端并更新缓存。</p>
<p>也就是说这种策略，只要缓存中有对应数据，就不会发起网络请求去更新缓存中的内容，这适合一些不常变化的资源缓存。比如图片，字体资源。</p>
</li>
</ul>
<p><img src="https://godlanbo.com/images/1640670812085.jpg" alt="image20211227183540519.png"></p>
<ul>
<li>
<p>Network First：这个策略工作路线如下图，它总是优先去发起网络请求，然后把拿到的请求返回给客户端的同时，塞到缓存里。当某次网络请求失败之后，就会从缓存里去读取数据。</p>
<p>从上述我们可以看出，这种策略多是一种应对网络错误时的兜底策略，当发生错误时，我们采取上一次成功的数据返回给用户。这种策略一般用在数据接口的请求上，用来应对网络错误。</p>
</li>
</ul>
<p><img src="https://godlanbo.com/images/1640670825001.jpg" alt="image20211227183820394.png"></p>
<ul>
<li>Network Only：这个策略工作路线如下图，它就很简单了，就和它的名字一样，只走网络请求，自我感觉这个策略用处不大，并不清楚应用场景在哪儿。</li>
</ul>
<p><img src="https://godlanbo.com/images/1640670845647.jpg" alt="image20211227184849536.png"></p>
<ul>
<li>Cache Only：这个策略工作路线如下图，和上面<code>Network Only</code>同理，就是只走缓存。这个更少用到了，它要求你提前缓存里就有对应的资源才行，需要配合<code>workbox</code>的另一个功能 <code>precache</code> 使用，这里不做展开。</li>
</ul>
<p><img src="https://godlanbo.com/images/1640670856099.jpg" alt="image20211227185052837.png"></p>
<h5>如何使用</h5>
<pre><code class="language-js">// css js 资源缓存
// 这里用缓存 css js举例
registerRoute(
  /.+\.(?:js|css)$/,
  new StaleWhileRevalidate({
    cacheName: 'resource',
    plugins: [
      new CacheableResponsePlugin({
        statuses: [200]
      })
    ]
  })
)
</code></pre>
<p>这里我们用缓存css 和 js举例，通过上面一节的说明，对应这种远端可能会更新的资源，建议采用第一种<code>Stale-While-Revalidate</code>的缓存策略。使用方法就是把 <code>workbox-routing</code> 那一节例子中的 <code>CacheHandler</code> 占位，改成对应策略的实例即可。</p>
<p>在实例的时候还可以填入一些 options，比如cacheName和plugins，cacheName后面会反映到本地cache Api中存储的地方，plugins可以提供一些额外的功能，比如这里，我们希望每次缓存的资源都是200请求的，而不是404或者500状态码的资源。所以从 <code>workbox-cacheable-response</code>中导入一个插件，他可以帮我们过滤相应的状态码。</p>
<h4>5.3.3、workbox-expiration</h4>
<p>这里面提供了一些供缓存策略实例使用的插件（就如上一节里提到的，缓存策略实例化的时候，传入的options里可以有plugins选项），使用例子如下：</p>
<pre><code class="language-js">import { registerRoute } from 'workbox-routing'
import { CacheFirst } from 'workbox-strategies'
import { ExpirationPlugin } from 'workbox-expiration'

// 图片字体等静态资源缓存
registerRoute(
  /.+\.(?:png|gif|jpg|jpeg|svg|ico|ttf|woff|woff2)$/,
  new CacheFirst({
    cacheName: 'assets',
    plugins: [
      new ExpirationPlugin({
        // 最多缓存 60 个项目
        maxEntries: 60,
        // 缓存时间最长 30 天
        maxAgeSeconds: 30 * 24 * 60 * 60 // 30 Days
      })
    ]
  })
)
</code></pre>
<p>我们在<code>CacheFirst</code>策略中，实例化<code>ExpirationPlugin</code>插件，这个插件可以帮助我们限制缓存项目上限和缓存的时间。因为<code>CacheFirst</code>策略在有缓存的时候会始终读取缓存，虽然这里存的是不常变化的内容，但是我们仍然不希望无限的增加缓存的内容并且无限期的保留缓存，这个插件就可以很好的帮我们实现这些功能。</p>
<h4>5.3.4、workbox-cacheable-response</h4>
<p>这个里面提供的插件的使用已经在 5.3.2 那一节提到，就是帮助我们根据状态码，选择性的将网络请求的资源包塞到缓存里。</p>
<h3>5.4、Service Worker小结</h3>
<p>SW这个东西呢，我觉得属于是缓存方面功能的天花板，它可以灵活的定义缓存的项目，缓存的时机等，也可以操作缓存的内容而达到一些其他的目的。但是毕竟是一个比较新的功能，兼容性上的障碍使它只是在缓存这块处于一个锦上添花的位置，不能全盘依靠SW来做缓存。</p>
<h2>6、结尾</h2>
<p>经过好几天的优化，在没有上终极首屏优化方案（SSR）之前，网站已经达到了一个还不错的速度：</p>
<p><img src="https://godlanbo.com/images/1640670874738.jpg" alt="image20211223153335103.png"></p>
<p>这里的数据都只是取得首页的数据，不同页面打开的速度可能会有些差别，这里只取我认为最重要的首页的打开数据作为前后对比。后续还可以使用SSR方案进行进一步的优化，不过这个方案需要项目整体重构，一时半会儿搞不出来。</p>
<h2>参考文献</h2>
<p><a href="https://zhuanlan.zhihu.com/p/28179203">Web 字体简介: TTF, OTF, WOFF, EOT &amp;amp; SVG</a></p>
<p><a href="https://segmentfault.com/a/1190000037449332">按需引入element-ui</a></p>
<p><a href="https://dev.to/voodu/vue-3-pwa-service-worker-12di">Vue 3, PWA &amp;amp; service worker</a></p>
<p><a href="https://developers.google.com/web/tools/workbox/guides/get-started">Workbox get started</a></p>

        ]]></description>
      </item><item>
        <title>Chrome插件开发小记 </title>
        <link>http://godlanbo.com/blogs/40</link>
        <guid>40</guid>
        <pubDate>Wed, 15 Dec 2021 06:36:25 GMT</pubDate>
        <description><![CDATA[
          <h2><strong>背景</strong></h2>
<p>作为一个前端，MDN是一个非常重要的文档网站，各种CSS，JS的属性功能都可以在MDN上找到不错的解释。对于我来说MDN更是非常重要，因为记性不是很好，所以在开发的过程中，在网页浏览的时候，看到一个关键点，就想马上去MDN回忆或者查看一下相关的使用介绍，每次都需要复制关键字，再到MDN去搜索，感觉比较麻烦，为了可以方便的在网页中跳转MDN搜索，加上正好以前就一直想学下Chrome的插件开发，就拿这个小功能做一个练手。</p>
<p>最后的实现效果如下图，右键菜单中可以直接搜索选中的关键词。</p>
<p><img src="https://godlanbo.com/images/1639549725634.jpg" alt="0.png"></p>
<h2><strong>插件目录</strong></h2>
<p>chrome插件对于插件目录结构没有任何要求，只要求你的插件目录下有个manifest.json文件，这个是chrome解析插件信息的关键配置文件。</p>
<h2><strong>manifest.json</strong></h2>
<pre><code class="language-JSON">{
  // 清单文件的版本，这个必须写，而且必须是2或者3
  &amp;quot;manifest_version&amp;quot;: 3,
  // 插件的名称
  &amp;quot;name&amp;quot;: &amp;quot;MDN-search-plugin&amp;quot;,
  // 插件的版本
  &amp;quot;version&amp;quot;: &amp;quot;1.0.0&amp;quot;,
  // 插件描述
  &amp;quot;description&amp;quot;: &amp;quot;MDN搜索包&amp;quot;,
  // 图标，全部用一个图片也没什么问题
  &amp;quot;icons&amp;quot;: {
    &amp;quot;16&amp;quot;: &amp;quot;img/dino.png&amp;quot;,
    &amp;quot;48&amp;quot;: &amp;quot;img/dino.png&amp;quot;,
    &amp;quot;128&amp;quot;: &amp;quot;img/dino.png&amp;quot;
  },
  // 会一直常驻的后台JS或后台页面
  &amp;quot;background&amp;quot;: {
    // 这是必须的
    &amp;quot;service_worker&amp;quot;: &amp;quot;js/background.js&amp;quot;
  },
  &amp;quot;action&amp;quot;: {
    &amp;quot;default_icon&amp;quot;: &amp;quot;img/dino.png&amp;quot;,
    // 图标悬停时的标题，可选
    &amp;quot;default_title&amp;quot;: &amp;quot;MDN search&amp;quot;,
    &amp;quot;default_popup&amp;quot;: &amp;quot;html/popup.html&amp;quot;
  },
  // 需要直接注入页面的JS
  &amp;quot;content_scripts&amp;quot;: [
    {
      //&amp;quot;matches&amp;quot;: [&amp;quot;http://*/*&amp;quot;, &amp;quot;https://*/*&amp;quot;],
      // &amp;quot;&amp;amp;lt;all_urls&amp;amp;gt;&amp;quot; 表示匹配所有地址
      &amp;quot;matches&amp;quot;: [&amp;quot;&amp;amp;lt;all_urls&amp;amp;gt;&amp;quot;],
      // 多个JS按顺序注入
      &amp;quot;js&amp;quot;: [&amp;quot;content-script.js&amp;quot;],
      &amp;quot;css&amp;quot;: [&amp;quot;css/index.css&amp;quot;],
      // &amp;quot;css&amp;quot;: [&amp;quot;css/custom.css&amp;quot;],
      &amp;quot;run_at&amp;quot;: &amp;quot;document_start&amp;quot;
    }
  ],
  // 点击插件跳转的主页
  &amp;quot;homepage_url&amp;quot;: &amp;quot;https://www.baidu.com&amp;quot;,
  // 所需权限
  &amp;quot;permissions&amp;quot;: [
    &amp;quot;contextMenus&amp;quot;, // 右键菜单
  ],
  // 可选权限
  &amp;quot;optional_permissions&amp;quot;: [&amp;quot;...&amp;quot;]
  // 普通页面能够直接访问的插件资源列表，如果不设置是无法直接访问的
  &amp;quot;web_accessible_resources&amp;quot;: [{
    &amp;quot;resources&amp;quot;: [&amp;quot;js/content-script.js&amp;quot;],
    &amp;quot;matches&amp;quot;: [&amp;quot;&amp;amp;lt;all_urls&amp;amp;gt;&amp;quot;]
  }],
  &amp;quot;options_ui&amp;quot;: {
    &amp;quot;page&amp;quot;: &amp;quot;html/options.html&amp;quot;
  }
}
</code></pre>
<p>上面简单列出了manifest.json的配置，当然这里只列出了一部分常用的，还有很多配置，可以在<a href="https://developer.chrome.com/docs/extensions/mv3/manifest/">这里</a>查看完整配置。下面详细讲一下上面的这些配置</p>
<h3><em><strong>manifest_version</strong></em></h3>
<p>配置文件的版本，这个是必须的，chrome插件的配置文件依赖这个版本进行不同的解析，只能填2或者3，表示你使用v2版本的配置项还是v3版本的配置项，建议开发的时候使用v3的配置项，它能享受更好的安全服务等，并且后续肯定会废弃v2版本的配置文件，到时候还是要迁移的，所以本文档后面所有的配置项都是v3版本的。</p>
<h3><em><strong>name，version，description</strong></em></h3>
<p>插件的名字，版本，描述</p>
<p><img src="https://godlanbo.com/images/1639549725570.jpg" alt="1.png"></p>
<h3><em><strong>Icons</strong></em></h3>
<p>就是插件的图标，多个尺寸只是为了保证清晰，其实你用个清晰的图片全部顶上也没有大问题，推荐使用png格式，但是其它图片格式也能够解析。</p>
<h3><em><strong>background</strong></em></h3>
<p>运行在后台的脚本，非常重要，它会在整个浏览器开启中一直运行在后台，所以你应该把插件所需要的常驻逻辑脚本放在这里。</p>
<p>在v2版本中可以通过引入page或者脚本的方式注入后台。在v3版本中使用<em>service_worker</em>运行后台脚本，这意味着你不能在这里直接访问dom，需要配合后面的内容脚本。</p>
<p>由于是使用service worker，v3版本的后台脚本编写方式是通过事件监听和响应才能够正常运行，类似v2版本的回调方式不被允许，原因引用MDN对于service worker的解释：</p>
<p>&amp;gt; Service worker运行在worker上下文，因此它不能访问DOM。相对于驱动应用的主JavaScript线程，它运行在其他线程中，所以不会造成阻塞。它设计为完全异步，同步API（如<a href="https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest">XHR</a>和<a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API">localStorage (en-US)</a>）不能在service worker中使用。</p>
<p>所以在编写的时候需要注意：</p>
<pre><code class="language-JavaScript">// 如果你使用的v2版本的配置文件，这样可以生效

chrome.contextMenus.create({

    onclick() {} // 这里会报错

})

// v3版本

chrome.contextMenus.onClicked.addListener(() =&amp;amp;gt; {})
</code></pre>
<p>后台脚本里你几乎可以访问到所有的chrome插件扩展api，是一个权限非常高的脚本，你可以利用它做很多想做的事情。更多关于后台脚本查看<a href="https://developer.chrome.com/docs/extensions/mv3/service_workers/">官方文档中关于后台脚本</a>的解释。</p>
<h3><em><strong>action</strong></em></h3>
<p>这个配置是控制插件在浏览器右上角显示的小图标的行为。</p>
<ul>
<li><strong>default_icon</strong>：图标，和上面的icons是一样的，可以多尺寸，也可以一个，推荐png其它也行。</li>
<li><strong>default_title</strong>：鼠标hover后显示的文案。</li>
</ul>
<p><img src="https://godlanbo.com/images/1639549725578.jpg" alt="4.png"></p>
<ul>
<li><strong>default_popup</strong>：点击之后弹出来的弹窗，是一个可以自定义的html文件。</li>
</ul>
<p>&amp;gt; 当用户单击工具栏中扩展的操作按钮时，将显示操作的弹出窗口。弹出窗口可以包含您喜欢的任何 HTML 内容，并且会自动调整大小以适合其内容。弹出窗口不能小于 25x25，不能大于 800x600。</p>
<p><img src="https://godlanbo.com/images/1639549725572.jpg" alt="3.png"></p>
<h3><em><strong>content_scripts</strong></em></h3>
<p>又是一个非常重要的配置项目。</p>
<p>&amp;gt; 内容脚本是在网页上下文中运行的文件。通过使用标准的<a href="https://www.w3.org/TR/DOM-Level-2-HTML/">文档对象模型</a>(DOM)，他们能够读取浏览器访问的网页的详细信息，对其进行更改，并将信息传递给其父扩展。</p>
<p>他接受一个数组，每一个数组项表示一个注入项目：</p>
<ul>
<li>
<p><strong>matches</strong>：表示这个项目需要在什么url中注入，命中匹配就会运行这个项目中注入的js和css，&amp;lt;all_urls&amp;gt;表示全部url</p>
</li>
<li>
<p><strong>Js</strong>：一个js项目数组，按照数组顺序依次注入</p>
</li>
<li>
<p><strong>Css</strong>：虽然名字是内容脚本，但是还可以注入css。但是css注入要小心一点，因为可能影响全局样式</p>
</li>
<li>
<p>run_at</p>
<p>：内容注入的时机</p>
<ul>
<li><strong>document_idle</strong>（默认）：首先会在document_end之后，window.onload事件之前。注入的确切时刻取决于文档的复杂程度和加载所需的时间，其实就和requestIdleCallback差不多，请求一个空闲时刻立即注入。</li>
<li><strong>document_start</strong>：脚本在任何css文件之后，任何其他DOM构造之前或任何其他脚本运行之前注入。</li>
<li><strong>document_end</strong>：脚本在DOM完成后，图像和帧等子资源加载之前注入。</li>
</ul>
</li>
</ul>
<p>在内容脚本里面，我们和页面共享文档对象，这让我们可以在这个脚本里对网页进行灵活的操作。诸如拦截广告或者定制化一些内容等。但是内容脚本对于chrome扩展api的访问就有一定的限制，不像后台脚本那样拥有很高的权限，只能访问一部分扩展api，不过大多数情况下足够用了。</p>
<p>内容脚本不能和网页共享JS，也就是说不能访问到页面的JS变量，如果需要互动的话，我们需要做一些额外处理。上面提到内容脚本是可以访问DOM的，所以我们可以将JS资源通过script标签，向页面内添加，这样就可以让JS资源的运行环境和网页的JS环境相同了：</p>
<pre><code class="language-JavaScript">/**
 * 向页面注入脚本
 * @param {String} jsPath 脚本在你插件目录下的路径
 */
function injectCustomJs(jsPath) {
  let script = document.createElement('script')
  script.setAttribute('type', 'text/javascript')
  // 获得的地址类似：chrome-extension://ihcokhadfjfchaeagdoclpnjdiokfakg/js/inject.js
  // 因为这个JS资源是在你的扩展插件的存放位置，这样相当于就是让网页去访问你的扩展脚本资源
  script.src = chrome.extension.getURL(jsPath)
  document.head.appendChild(script)
}
</code></pre>
<p>但是，但是，页面能够访问你扩展插件里的JS资源是需要权限的，不能随便网页去访问插件的资源。在***<a href="https://bytedance.feishu.cn/docs/doccn8ZIJDCZqZwoWONDfYgZd2f#O5EoTc">web_accessible_resources</a>***中设置网页可以访问的插件资源。</p>
<h3><em><strong>homepage_url</strong></em></h3>
<p>插件的主页页面，右键浏览器右上角插件的icon，第一个就跳转到你的插件主页：</p>
<p><img src="https://godlanbo.com/images/1639549725571.jpg" alt="2.png"></p>
<p>这里填入一个你喜欢的URL就好了。</p>
<h3><em><strong>permissions</strong></em></h3>
<p>插件所需的权限列表，在这里写的权限，会在插件安装的时候申请的，因为我们这个插件用不到什么复杂的权限，简单的列一些常见的：</p>
<ul>
<li>contextMenus：右键菜单</li>
<li>notifications：通知</li>
<li>webRequest：web请求</li>
<li>storage：插件可以使用本地存储</li>
</ul>
<p>这个字段申请的权限，要求用户一次性接受这里面声明的全部权限，而且你可以确保在你插件运行的过程中，这其中声明的权限一定是有的。</p>
<h3><em><strong>optional_permissions</strong></em></h3>
<p>插件声明的可选权限，可以使用扩展API在运行的时候申请，书写格式和值同上一个permission相同，只不过这里的权限需要使用api申请，这会给用户友好的提示。<a href="https://developer.chrome.com/docs/extensions/reference/permissions/">了解更多关于权限的配置</a>。</p>
<h3><em><strong>web_accessible_resources</strong></em></h3>
<p>指定插件的哪些资源公开给外面访问。因为有安全隐患，所以需要限制资源的公开访问程度。</p>
<p>&amp;gt; 在 Manifest V2 之前，可以从 Web 上的任何页面访问扩展中的所有资源。这允许恶意网站对用户已安装的扩展程序进行<a href="https://en.wikipedia.org/wiki/Device_fingerprint">指纹识别</a>或利用已安装扩展程序中的漏洞（例如<a href="https://en.wikipedia.org/wiki/Cross-site_scripting">XSS 漏洞</a>）。</p>
<p>&amp;gt; 从 Manifest V2 开始，对这些资源的访问受到限制，以保护用户的隐私。MV2 扩展仅公开那些明确指定为 Web 可访问的资源。</p>
<p>&amp;gt; Manifest V3 提供更细粒度的控制，让您可以向指定的页面、域或扩展公开单个资源。</p>
<p>v3版本提供了非常细粒度的资源公开控制，由一个列表组成，每个列表项拥有以下属性：</p>
<ul>
<li>resources：涉及的资源列表</li>
<li>matches：匹配的url列表，只有匹配到的url才能访问resources公开的资源。</li>
<li>extensions：允许访问公开资源的扩展ID</li>
</ul>
<p>每个元素必须包含一个<code>&amp;quot;resources&amp;quot;</code>元素和一个<code>&amp;quot;matches&amp;quot;</code>或<code>&amp;quot;extensions&amp;quot;</code>元素。这建立了一个映射，将指定的资源暴露给与模式匹配的网页或指定的扩展。</p>
<h3><em><strong>options_ui</strong></em></h3>
<p>配置页面的配置，你可以配置一个html在这里，右键点击右上角的icon，然后里面的选项就可以跳过去：</p>
<p><img src="https://godlanbo.com/images/1639549725668.jpg" alt="5.png"></p>
<h2><strong>实践一个小demo</strong></h2>
<p>我们大致浏览了配置文件之后，就可以进行开发了。</p>
<h3><strong>文件准备</strong></h3>
<p>其实工作量非常小，首先我们需要一个插件图标，我直接打开MDN网页的控制台，把它的图标牛过来，原滋原味。</p>
<p>然后我们创建一个目录，放入一个配置文件，再把必须的几个脚本也放进去，大概长这个样子：</p>
<p><img src="https://godlanbo.com/images/1639549725669.jpg" alt="6.png"></p>
<p>哪些文件对应哪些配置项也很清楚，background.html可以不要了，因为v3版本的配置用不到它，这是我原来用v2版本配置开发时留下的。</p>
<h3>调试开发</h3>
<p>我们打开chrome浏览器，在网址栏输入<a href="http://chrome://extensions/">chrome://extensions/</a>，就可以跳转到你的扩展管理页面，然后右上角开启开发者模式，点击加载已解压的扩展程序，加载我们的扩展：</p>
<p><img src="https://godlanbo.com/images/1639549725864.jpg" alt="7.png"></p>
<p>这个时候如果你的manifest文件写的有问题，它会提示你加载失败，有非常详细的原因提示，你照着改好，再点击重新加载即可，举个例子：</p>
<p><img src="https://godlanbo.com/images/1639549725720.jpg" alt="10.png"></p>
<p>这里告诉我们，后台脚本配置的page属性，已经不能在v3版本的配置中使用了，请用service_worker代替它。或者你也可以选择使用v2版本的配置项。</p>
<p>一切完成，它就跑起来了。点击service worker会新开一个后台脚本的窗口，你可以在这个里面看到后台脚本的输出和报错。</p>
<p><img src="https://godlanbo.com/images/1639549725764.jpg" alt="13.png"></p>
<p>准备就绪之后，开始开发，这个插件并用不到内容脚本，只会用到后台脚本，首先我们需要右键菜单的权限，在permission中添加对应的权限。</p>
<p>然后编写后台脚本：</p>
<pre><code class="language-JavaScript">// 记住这是在service woker的上下文中，使用监听回调的形式来响应事件
// 扩展运行时 - 下载时触发
chrome.runtime.onInstalled.addListener(() =&amp;amp;gt; {
  // 创建一个右键菜单
  chrome.contextMenus.create(
    {
      // item的类型
      type: 'normal',
      // 显示的文字，%s占位符会显示你选中的字
      title: '使用MDN搜索 %s',
      // 这个菜单的id
      id: 'MDN-search',
      // 可以出现这个菜单项的上下文
      contexts: ['all']
    },
    // 创建后的 回调
    function () {
      console.log('contextMenus are create.')
    }
  )
})
// 右键菜单点击的时候触发
chrome.contextMenus.onClicked.addListener(info =&amp;amp;gt; {
  // 新建一个tab
  chrome.tabs.create({
    url:
      'https://developer.mozilla.org/zh-CN/search?q=' +
      encodeURI(info.selectionText) // info. selectionText是选中的文字
  })
})
</code></pre>
<p>扩展APi就没什么讲的了，这个太多了也讲不完，详细的API可以参考<a href="https://developer.chrome.com/docs/extensions/reference/">这里</a>，非常详细，也不用看完，需要的时候找相关的API查看就行了。</p>
<h3>开发小技巧</h3>
<p>因为开发的时候需要在内容脚本和后台脚本中使用扩展API，很明显这些API是没有提示的，容易写错，体验也不好，可以在插件的上层目录，初始化一下npm，然后安装一下chrome的类型，我没有试过全局安装是否可以，如果可以的话也行：</p>
<pre><code class="language-bash">cd ../plugin_dir
npm init -y
npm i @types/chrome
</code></pre>
<p><img src="https://godlanbo.com/images/1639549725671.jpg" alt="8.png"></p>
<p>这样开发起来就会舒服很多，还能随时点进去看看配置项。</p>
<p><img src="https://godlanbo.com/images/1639549725747.jpg" alt="11.png"></p>
<h2>总结</h2>
<p>因为一些懒懒的需求，学习了一下chrome插件的开发，小小尝试了一把收获还是很多的，虽然扩展API不知道几个，配置也还有很多不知道，但是从0到1这个最难得阶段已经过去了，后面还需要什么开发就可以轻车熟路的翻文档找对应的描述就行了。</p>
<p>扩展现在就是在本地上使用，如果你要发布的话，还需要打包：</p>
<p><img src="https://godlanbo.com/images/1639549725680.jpg" alt="9.png"></p>
<p>打包出一个.crx结尾的文件，然后去Google注册成为开发者，才可以把打包的插件发布到市场。据说注册开发者还要花钱，如果没有特别需要发布的插件，可以直接就这样用了，在本地还是非常方便的，改了代码更新重新读取一下就好了，小范围分享直接发文件就好了。</p>
<p><img src="https://godlanbo.com/images/1639549725765.jpg" alt="12.png"></p>

        ]]></description>
      </item><item>
        <title>关于useReducer的理解</title>
        <link>http://godlanbo.com/blogs/39</link>
        <guid>39</guid>
        <pubDate>Thu, 09 Dec 2021 12:36:54 GMT</pubDate>
        <description><![CDATA[
          <p>这个东西是useState的高级产物，React依赖状态进行更新，我们可以在函数组件中利用useState钩入状态，利用其返回的set函数来变更状态，但是，useState这种变更数据的方式太过单一，不能做到复杂的状态变化，类比vuex，useState每次能做的变更，大概就像下面这样：</p>
<pre><code class="language-jsx">module.exports = {
  mutations: {
    setState(state) {
    // 每次的变更只能简单的进行值得变更，无法运行逻辑
    state.xxx = 'xxx'
    }
  }
}
</code></pre>
<p>这显然是不够的，有不少情况下状态的变更是需要逻辑判断的，并不是简单的赋值。所以React官方引入了useReducer，我理解它就像是useState的高级版本，其本质，也是去修改state。</p>
<p>它的大概运行逻辑如下：</p>
<pre><code class="language-js">(state, action) =&amp;amp;gt; newState
</code></pre>
<p>我们根据触发的action和当前的state，计算得到下一次的newState。参照官网的例子，使用useReducer大致如下：</p>
<pre><code class="language-jsx">const [state, dispatch] = useReducer(reducer, initialArg)
// 在这里改变state，返回新的state值
function reducer(state, action) {/* ...  */}
function App() {
	return (
  	&amp;amp;lt;button onClick={() =&amp;amp;gt; dispatch(/* action */)}&amp;amp;gt;&amp;amp;lt;/button&amp;amp;gt;
  )
}
</code></pre>
<p>它接受一个处理函数，和一个初始值，然后返回一个状态，和一个更改状态的触发器。</p>
<p>然后这个处理函数reducer，这是你编写的部分，你想要怎么样的处理函数，这个函数会被传入两个参数：当前的state和触发状态改变的时候传入的值，也就是dispatch被调用时传入的值，然后在这个函数里运行你想要改变state的逻辑，最后返回新的state值，流程就是这样。</p>
<p>我一开始没有理解这其中的流程，大致是因为受vuex影响太深，我将上面改成如下形式：</p>
<pre><code class="language-jsx">const [state, dispatch] = useReducer(handler, initialArg)
// 在这里改变state，返回新的state值
function handler(state, payload) {/* ...  */}
function App() {
	return (
  	&amp;amp;lt;button onClick={() =&amp;amp;gt; dispatch(/* payload */)}&amp;amp;gt;&amp;amp;lt;/button&amp;amp;gt;
  )
}
</code></pre>
<p>改一下变量名字其实就好理解多了，可以理解useReducer返回的dispatch就是一个改变state动作的专属触发器，也就是说，我可以通过dispatch来触发一个改变state的动作，然后我可以通过dispatch触发的同时传入一些参数来辅助state处理函数改变state。</p>
<p>还是类比到vuex上：</p>
<pre><code class="language-js">// store.js
module.exports = {
  actions: {
    handler({ state }, payload) {
      // 在这里改变state，payload就是触发的时候传入的参数
    }
  }
}
// 组件中
this.$store.dispatch('handler', payload)
</code></pre>
<p>唯一不同的就是vue中处理函数没有一个专属的触发器，大家统一用dispatch触发，传入第一个参数来分别，其它的对比一看就好理解了。</p>

        ]]></description>
      </item><item>
        <title>关于useEffect的理解</title>
        <link>http://godlanbo.com/blogs/38</link>
        <guid>38</guid>
        <pubDate>Thu, 09 Dec 2021 12:35:32 GMT</pubDate>
        <description><![CDATA[
          <h3>Render每次独立存在，钩子数据持续保存</h3>
<p>首先我们需要了解一下React中钩子函数的机制，我们知道，在React中，每次重新渲染组件，其实就是重新调用组件的Render函数，对于函数组件来说，就是每次重新调用那个函数：</p>
<pre><code class="language-jsx">function Timer () {
  let [timer, setTimer] = useState(0)
  return &amp;lt;div&amp;gt; setTimer(pre =&amp;amp;gt; pre += 1)}&amp;amp;gt;Timer: {timer}&amp;lt;/div&amp;gt;
}
</code></pre>
<p>每次timer的更新，都会重新调用Timer函数进行渲染。</p>
<p>那么，就有个问题，每次都会调用Timer函数的话，也就是说每次都会执行useState，肯定不是每次都初始化一个state，要不然每次都是初始值，显然不是，我们只有第一次useState是拿到初始值，后续重新渲染的时候，useState返回的都是第一次我们定义的值；也就是说React会在组件中保存钩子的数据，重新调用的时候来判断执行的逻辑。</p>
<p>所以React才要求钩子的执行顺序不改变，因为React内部没有钩子的标识来判断每次调用对应哪一个钩子，它是通过调用顺序来判断的，按照调用顺序，默认的把钩子存储的数据依序返回，如果在组件函数中重新渲染时调用钩子的顺序和初始化时一样，那么每个钩子就能够拿到正确的数据。</p>
<p>所以，看似在函数组件里，我们每次都声明了一个useState返回的timer变量，其实它的值是根据组件内部存储的值返回得到的，只有第一次会初始化。</p>
<h3>useEffect依赖的状态</h3>
<p>说了这么多，我们再回到useEffect，为什么官网上会说建议不要提供空数组给用到组件数据流的useEffect。是因为useEffect在每次组件重新渲染都会执行，但是这大多数时候并不符合我们的预期，所以react提供了第二个参数，来告诉它，这个effect函数依赖哪些数据流。</p>
<p>我们总应该准确告诉React，Effect依赖了数据流中的哪些状态，除非你真的希望这个Effect只运行一次，因为如果你传入空数组，这个Effect就只会在组件挂载和卸载的时候分别执行一次，即使其中依赖的数据流变化了，也不会重新激活。</p>
<p>然后我们再来理解官网上的错误范例：</p>
<pre><code class="language-jsx">function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() =&amp;amp;gt; {
    const id = setInterval(() =&amp;amp;gt; {
      setCount(count + 1); // 这个 effect 依赖于 `count` state
    }, 1000);
    return () =&amp;amp;gt; clearInterval(id);
  }, []); // 🔴 Bug: `count` 没有被指定为依赖

  return &amp;lt;h1&amp;gt;{count}&amp;lt;/h1&amp;gt;;
}
</code></pre>
<p>我们上面提到了，每次Render都是独立的，但是钩子数据根据运行顺序取到第一次初始化的值。这个例子的想法是，useEffect在挂载组件的时候执行一次，然后每过一秒count增加1。</p>
<p>但是实际上，由于我们给useEffect传入的第二个参数是空数组，导致这个钩子只会在组件挂载的时候被执行一次。那么根据闭包，内部的函数拿到外面的count值，当组件重新渲染的时候，第二次执行Counter，useEffect的钩子不会重新激活执行，那么就是继续执行上一次Render中的钩子函数，上次的钩子函数中的定时器执行的时候访问count值，是访问到闭包（第一次Render环境）中的值，这个值是不会变的，React变化的视图是由一系列Render组成的，抛开这个本质，count就是一个普通值，并不是一个包装的，可变化的动态值，所以每次定时器执行都是在count = 1的作用域中执行，达不到预期的功能。</p>
<h3>useEffect使用的函数声明的位置</h3>
<p>如果useEffec用到了函数，直觉告诉我们，这些函数统一声明在外面更加符合复用的特性。当你的函数没有用到数据流的时候，这样没什么问题，但是如果函数依赖组件的数据流，React推荐将函数放在useEffect中，以帮助依赖数组的编写。官网上大概就是两种写法：</p>
<pre><code class="language-jsx">// 在useEffect中声明函数
useEffect(() =&amp;amp;gt; {
  function fetchProduct() {
    console.log(state)
  }
  fetchProduct();
}, [state])
// =========================&amp;amp;gt;
// 如果不能使用上面的方法，使用useCallback
let fn = useCallback(() =&amp;amp;gt; {
  console.log(state)
}, [state])
// useCallback能保证fn函数不会随组件渲染改变
// 这能保证useEffect不会因为函数引用不同导致意外的渲染
useEffect(() =&amp;amp;gt; {
  fn()
}, [fn])
</code></pre>
<h3>理解参考</h3>
<p><a href="https://juejin.cn/post/6844903806090608647#heading-4">精读《useEffect 完全指南》</a></p>
<p><a href="https://heptaluan.github.io/2020/11/07/React/17/">深入useEffect</a></p>

        ]]></description>
      </item><item>
        <title>React JSX 事件的编写方式与问题</title>
        <link>http://godlanbo.com/blogs/37</link>
        <guid>37</guid>
        <pubDate>Thu, 09 Dec 2021 12:33:52 GMT</pubDate>
        <description><![CDATA[
          <h3>编写方式</h3>
<p>在继承式的普通组件中：</p>
<pre><code class="language-jsx">class T extends React.Component {
  render() {
    return (
    	&amp;amp;lt;button onClick={() =&amp;amp;gt; alert('hello')}&amp;amp;gt;&amp;amp;lt;/button&amp;amp;gt;
    )
  }
}
</code></pre>
<p>在组件中编写事件的时候（比如click事件），不能像vue那样，传入一个函数调用，要传入一个函数；如果是需要传入参数调用的函数，应该重新包裹为一个函数，在这个函数被调用的时候调用。</p>
<p>其实就是，react在渲染节点的时候会去调用render函数，然后就会运行其中的内容，比如onClick属性，对于react来说，就是节点的一个属性，当它去取值的时候，就会去运行后面的大括号，所以，后面如果是一个函数<strong>调用</strong>，每次渲染这个组件，都会被调用，而onClick得到的值，当然不是一个响应函数，而是一个函数运行后的值，所以就会出问题。</p>
<p>但是当这个函数不需要入参的时候，就可以像平常写Vue那样，直接写函数名字就可以了，因为此时这里传给onClick属性的就是一个响应函数，可以正常调用，在渲染组件的时候也不会出问题：</p>
<pre><code class="language-jsx">class T extends React.Component {
  sayHi() {
    console.log('hi')
  }
  render() {
    // 此时这个响应函数不需要参数，所以直接传入函数即可
    return (
      &amp;amp;lt;button onClick={this.sayHi}&amp;amp;gt;&amp;amp;lt;/button&amp;amp;gt;
    )
  }
}
</code></pre>
<p>我们可以再对比一下在Vue中的写法：</p>
<pre><code class="language-html">&amp;amp;lt;template&amp;amp;gt;
	&amp;amp;lt;button @click=&amp;quot;alert('hello')&amp;quot;&amp;amp;gt;&amp;amp;lt;/button&amp;amp;gt;
&amp;amp;lt;/template&amp;amp;gt;
</code></pre>
<p>为什么在Vue中不管函数是否传入参数，都可以直接写呢？因为你编写的是模板，最后Vue会把模板编译为render函数，在编译这个过程，就把这个问题处理掉了，你传入的字符串会被解析，解析到是一个传入参数的函数调用的时候，就会包裹一个函数，是一样的效果，只是Vue利用模板的优势简化了这个步骤。</p>
<h3>出现的问题</h3>
<p>上面的讨论只是我对于react官网上，即使可以直接函数名字的时候，还是写箭头函数的疑惑。最后得出了上面的结论，但是，在官网上对于这种写法还有一种考虑：</p>
<p>![截屏2021-06-20 上午11.27.30](/Users/bytedance/Documents/文档工作区/React学习笔记/关于React JSX中事件的编写.assets/截屏2021-06-20 上午11.27.30.png)</p>
<p>避免this造成的困扰，我们知道，在JS中，this的指向有时候非常让人困惑，一切就源于this的指向是运行时动态的，而不是像词法作用域一样静态的。所以才有了箭头函数来让this的行为和词法作用域行为相似。</p>
<p>那么react官网上提到的这个this造成的困扰具体是指什么呢？当时我就是因为它这简单的提到而没有重视，导致了后面的问题，我们来看下面这个例子：</p>
<pre><code class="language-jsx">class C extends React.Component {
  render() {
    return (
      &amp;amp;lt;button onClick={() =&amp;amp;gt; this.props.handleClick('world')}&amp;amp;gt;&amp;amp;lt;/button&amp;amp;gt;
    )
  }
}
class T extends React.Component {
  constructor(props) {
    super(props)
    this.state = {
      message: 'hi '
    }
  }
  sayHi(msg) {
    console.log(this.state.message + msg)
  }
  render() {
    return (
      &amp;amp;lt;C handleClick={this.sayHi}&amp;amp;gt;&amp;amp;lt;/C&amp;amp;gt;
    )
  }
}
</code></pre>
<p>T组件中调用了C组件，向它传递了一个函数，按照上一节的思路，我们这里在T组件中并不需要向函数传入参数，所以直接传入handleClick，然后在C，子组件这里调用handleClick的时候，就需要传递参数了，所以我们这了包裹箭头函数。</p>
<p>所以上面点击button之后的结果是？打印”hi world“吗？可能直觉上是，但实际上会报错--”TypeError: Cannot read property 'message' of undefined“。为什么？</p>
<p>因为在sayHi函数调用的时候，其中this的指向已经不是T组件了，所以去寻找state的时候会是undefined。那这时this指向哪儿？让我们看看sayHi实际被调用在哪儿，是在C组件的onClick的箭头函数中。遵循谁调用指向谁的简单原理，显然这里的this指向props，就是T组件传值给C组件的对象，这不是我们想要的。</p>
<p>通过上面的例子，就能明白官网上说的，this指向带来的问题了，我们对T组件做如下修改再来看看：</p>
<pre><code class="language-jsx">class T extend React.Component {
  // ...
  render() {
    // ...
    return (
      &amp;amp;lt;C handleClick={(msg) =&amp;amp;gt; this.sayHi(msg)}&amp;amp;gt;&amp;amp;lt;/C&amp;amp;gt;
    )
  }
}
</code></pre>
<p>这个时候就可以正常显示”hi world“了，因为此时sayHi的实际调用点在T组件传入属性handleClick的箭头函数中，这里调用sayHi，根据谁调用指向谁，sayHi里的this指向这里箭头函数中的this，这里的this由于箭头函数的特性指向词法作用域的上层，也就是T组件实例，所以sayHi如我们预期的取到了T组件上的属性值。</p>
<h3>总结</h3>
<p>所以，在编写关于事件传值的时候一定要注意是否可以省略直接函数名字，要只传函数名，要满足几个条件，首先就是不需要传参数，其次就是这个函数是否有用到this，否则只能用箭头函数来规避这个this指向的问题。</p>

        ]]></description>
      </item><item>
        <title>Mobx</title>
        <link>http://godlanbo.com/blogs/36</link>
        <guid>36</guid>
        <pubDate>Thu, 09 Dec 2021 12:31:37 GMT</pubDate>
        <description><![CDATA[
          <h3>简记</h3>
<p>状态管理工具，React所有组件的更新和渲染都是依赖状态的改变，状态是React中的一个重要概念，React的组件只有在自身state或是props改变的时候才会重新渲染。</p>
<p>所以为了能像Vuex那样进行跨组件的状态传递，我们肯定需要一个全局的状态管理，那么这个全局的状态管理，他不仅仅需要所有组件能够访问，它还需要有能让React响应状态变化的能力。</p>
<h3>相关点</h3>
<ul>
<li>
<p>state状态，Mobx中的一个属性可以简单的看做一个状态，当组件用到这个状态，且这个属性（状态）发生改变，组件就可以更新视图。在Mobx中定义这种状态需要它是响应式的。</p>
</li>
<li>
<p>action动作，和vuex一样，我们最好不要轻易通过直接修改的方式去改变全局的state，这样不利于记录，代码传达的意思也更加难以理解。所以Mobx提供一种action来改变状态state，它可以包裹一个函数，这个函数就变成了动作函数，我们在这个里面修改state。</p>
</li>
<li>
<p>computed计算值，可以简单的理解为vue中的计算属性，只不过这只存在于Mobx中，而不是像vue中在每个组件都存在。Mobx中依赖响应式的state，可以定义计算属性，当依赖的值更新时它会更新。</p>
</li>
<li>
<p>reaction反应，我简单的理解为和vue中一样的watcher，它可以在依赖发生变化的时候自动执行一些副作用（回调函数），通常是将React中的组件变成reaction的，我理解为内部观测这个组件，收集它依赖的state，然后将重新渲染这个组件作为副作用。当然也可以定义其他的reaction，利用autorun，这里挪用官方例子：</p>
<pre><code class="language-js">// A function that automatically observes the state.
// 这个reaction自动的监测了todos.unfinishedTodoCount这个状态，当他更新的时候，
// 触发注册的这个副作用，很像vue中的watcher
autorun(() =&amp;amp;gt; {
    console.log(&amp;quot;Tasks left: &amp;quot; + todos.unfinishedTodoCount)
})
</code></pre>
</li>
</ul>

        ]]></description>
      </item><item>
        <title>页面中的选择&amp;范围</title>
        <link>http://godlanbo.com/blogs/35</link>
        <guid>35</guid>
        <pubDate>Sun, 04 Jul 2021 13:25:35 GMT</pubDate>
        <description><![CDATA[
          <h3>一、起因</h3>
<p>最近在阅读学习next.js的文档，学习文档肯定首先跟着它的教程走，在这个过程中，文档有时会让你创建一些文件，然后贴出准备好的代码让你粘贴进去。在我反反复复这个过程几次后，我突然发现一个有趣点，文档中贴出的代码段，轻轻一点就可以选中全部：</p>
<p><img src="https://godlanbo.com/images/1625405057745.jpg" alt="截屏20210704 下午6.20.48.png"></p>
<p>不用像选择其它文本一样去拖动鼠标选中全部，这让我非常好奇，它是怎么做到的，如何控制用户的光标和选中内容。</p>
<h3>二、选择&amp;amp;范围</h3>
<p>其实对于这种操作我不是完全没有谱，我记得红宝书上有提到关于一个页面范围相关的操作，和我看到的这种操作有点类似，但是当时由于我觉得这东西作用很小，我可能永远不会用到，就没有细看，所以现在并不知道这是怎么搞起的。</p>
<p>顺着我的记忆，我很快找到了相关的内容：Selection和Range对象。简单的概述下这两个对象：</p>
<p><strong>Selection</strong>：</p>
<p>&amp;gt; <code>Selection</code> 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区，可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。  —— <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Selection">MDN</a></p>
<p><strong>Range</strong>:</p>
<p>&amp;gt; <strong><code>Range</code></strong> 接口表示一个包含节点与文本节点的一部分的文档片段。 —— <a href="https://developer.mozilla.org/zh-CN/docs/Web/API/Range">MDN</a></p>
<p>range是一个很底层的接口，它可以表示用户选择的区域，也可以通过方法去设置用户选择的区域，但是它的作用并不是选中，就相当于在页面文档中划出了一片范围，但是并不会触发变蓝的选中，触发变蓝选中的Selection对象。</p>
<p>Selection对象相当于就是来消费Range的，Range给它划定一块区域，Selection上去就把这块区域选中了。同样，你也可以手动选中页面后从Selection对象中提取range实例：</p>
<pre><code class="language-js">let sel = window.getSelection() // 获得页面的selection实例
sel.getRange(0) // 获取当前的range，除了FireFox，均不可能支持多个选区（range）
</code></pre>
<p>range和selection有很多的方法去修改选择的区域，具体参考<a href="https://zh.javascript.info/selection-range">现代JavaScript教程-选择和范围</a>，这里写的足够详细，我就不多赘述了，我们下面来实现最开始那个效果，期间用到的的关于range和selection的方法我会简单介绍一下，详细请看上面链接教程中的内容。</p>
<h3>三、动手实现</h3>
<p>我们直接取next.js中的这块区域作为我们的目标：</p>
<p><img src="https://godlanbo.com/images/1625405101820.jpg" alt="截屏20210704 下午8.13.07.png"></p>
<pre><code class="language-js">let node = $0 // 在浏览器中选中元素可以在控制台用$0方便的取出，我们把它保留在node变量中
</code></pre>
<p>然后开始，首先我们要选中这块区域，我们需要一个range来划定这块区域：</p>
<pre><code class="language-js">let range = document.createRange() // 使用document.createRange()来创建一个range对象
</code></pre>
<p>我们有了range对象，就可以调用上面的方法来划定这块区域，我们下面采用几种方法来划定：</p>
<pre><code class="language-js">// 1. 使用selectNode，直接将range划定为传入的节点包含的整个范围
range.selectNode(node)
// 2. 使用setStart设定划定区域起点，两个参数，第一个参数为起点node，第二参
// 数为偏移量，这个偏移量在node的子节点中数，setEnd同理（左闭右开），为设置划定区
// 域终点这里的意思就是，指定node的第一个子节点作为起点，最后一个子节点作为终点，相
// 当于就是选择了node下全部内容
range.setStart(node, 0)
range.setEnd(node, node.children.length)
</code></pre>
<p>主要是就是这两种形式，其它还有些更加细致的和功能化的一些方法，请自行了解。</p>
<p>选取好区域之后，最后一步就是选定它，我们首先获取selection实例，然后将range传给selection实例消费就好了：</p>
<pre><code class="language-js">let sel = window.getSelection()
// 要先移除selection实例上原有的全部range，否则会静默失败
sel.removeAllRanges()
// 然后我们用addRange方法传入我们构造好的range
sel.addRange(range)
</code></pre>
<p>然后就可以当当当，看到整块区域被选中变蓝了。</p>
<h3>四、最后</h3>
<p>闲暇的时间总是很零碎又很少，但是这不是停下的理由，不断的重复疑惑-查找-实践-解开，才能够一直学习新的知识，不断进步。</p>
<h3>参考</h3>
<p><a href="https://zh.javascript.info/selection-range">现代JavaScript教程-选择&amp;amp;范围</a></p>

        ]]></description>
      </item><item>
        <title>vue2中computed的实现-源码流程分析</title>
        <link>http://godlanbo.com/blogs/34</link>
        <guid>34</guid>
        <pubDate>Wed, 17 Feb 2021 16:31:07 GMT</pubDate>
        <description><![CDATA[
          <h2>前言</h2>
<p>这次我们来阅读关于vue2中计算属性的实现细节，本文不会写过多的关于代码实现的细枝末节，重点盘一盘计算属性在源码中的实现思路。</p>
<h2>初始化</h2>
<p>在vue实例初始化的时候，会执行到初始化相关数据方面的方法进行data，computed，props等的初始化：</p>
<pre><code class="language-js">Vue.prototype._init = function(options) {
  // ...
  // 在这个函数里进行各种数据相关的初始化
  initState(vm)
  // ...
}
</code></pre>
<p>我们直接在initState中找到关于计算属性的实现，省略无关的部分：</p>
<pre><code class="language-js">const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  const watchers = vm._computedWatchers = Object.create(null)
  // 遍历传入的computed对象
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' &amp;amp;&amp;amp; getter == null) {
      warn(
        `Getter is missing for computed property &amp;quot;${key}&amp;quot;.`,
        vm
      )
    }
    watchers[key] = new Watcher(
      vm,
      getter || noop,
      noop,
      computedWatcherOptions
    )
		// 判断props或者data上是否已经有重名属性
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property &amp;quot;${key}&amp;quot; is already defined in data.`, vm)
      } else if (vm.$options.props &amp;amp;&amp;amp; key in vm.$options.props) {
        warn(`The computed property &amp;quot;${key}&amp;quot; is already defined as a prop.`, vm)
      }
    }
  }
}
</code></pre>
<p>initCompuetd首先在vm实例上初始化一个_computedWatchers对象来存放每个计算属性对应的watcher，遍历传入的computed对象，判断每一个key对应的value是否是函数；我们写计算属性的时候往往是写一个函数，其实还可以写一个带有get和set属性的对象，不过用的很少，这里就不过多分析了，只要知道这里拿到了我们写的计算属性的getter就好了；然后对应key实例化一个watcher，这个watcher是一个lazy watcher；最后判断props或者data上是否有重名的属性key，没有的话就对每一个key执行defineComputed方法：</p>
<pre><code class="language-js">const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
// 省略关于SSR的代码
export function defineComputed (
  target: any,
  key: string,
  userDef: Object | Function
) {
  // 分情况调用createComputedGetter构建计算属性的属性描述
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = createComputedGetter(key)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get ? createComputedGetter(key) : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  // 使用defineProperty往实例上添加计算属性
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers &amp;amp;&amp;amp; this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
</code></pre>
<p>defineComputed其实就是处理了一下get和set，然后利用defineProperty声明一个属性在vm实例上。重点是createComputedGetter这个函数，它作为计算属性的get函数，每次取值都会被调用；这个函数返回一个函数computedGetter，computedGetter首先拿到这个key计算属性对应的watcher，如果watcher.dirty为true，就进行求值（<code>watcher.evaluate()</code>就是调用watcher的getter去求值），然后判断当前是否有watcher在工作，有的话，就调用<code>watcher.depend()</code>方法，最后返回值。</p>
<h2>订阅、计算、更新</h2>
<p>这上面就是全部计算属性的初始化流程，然后我们来看下watcher的相关代码，这里我们只关注lazy watcher和普通watcher不一致的地方：</p>
<pre><code class="language-js">// 已省略无关代码
class Watcher {
  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
  ) {
    this.vm = vm
    vm._watchers.push(this)
    // options
    this.lazy = !!options.lazy
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  update () {
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }
}
</code></pre>
<p>首先可以看到lazy watcher一开始并没有像其它watcher一样在构造的时候就求一次值，它的初始值一开始为undefined，这是为什么呢？我们知道watcher有几种，这种lazy watcher，还有用户定义的user watcher也就是侦听属性，还有渲染watcher，后两个都是在实例化的时候触发get去订阅需要的依赖，这是因为无论是在初始化监听器，还是渲染的时候，依赖（比如data中的变量）已经初始化完毕了，可以接受订阅，我们看下initState就知道顺序：</p>
<pre><code class="language-js">export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch &amp;amp;&amp;amp; opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
</code></pre>
<p>可以看到整个的初始化顺序，计算属性和侦听属性是在之后的；然后我们知道，计算属性是可以依赖另一个计算属性的是吧？说到这里，原因也就不言而喻，计算属性是按顺序初始化，但是没有规定你写的顺序一定是后面依赖前面的对不对？如果一开始的计算属性就依赖后面未初始化的计算属性，一开始就求值那不就出事儿了吗。所以lazy watcher一开始是不求值的。</p>
<p>继续，lazy watcher有一个dirty属性，一开始为true，从刚刚computedGetter方法中我们可以知道，只有dirty为true的时候，访问计算属性才会触发求值，这个是用来起缓存作用的，计算属性依赖的值不更新，就不用重新计算；我们可以看到求值函数evaluate会把dirty置为false，而只有在update方法中才会将dirty重新置为true。</p>
<p>在求值函数evaluate中，还会去调用getter，这里计算属性才真正的求值，这个过程中就会订阅它依赖的变量，它依赖的变量的dep也会收集这个计算属性的watcher作为依赖，这里很重要。</p>
<p>执行完求值后，接着判断当前工作watcher，有的话就执行depend的方法。这个当前watcher是什么？大概率是渲染watcher，为什么，这里是计算属性的get函数，什么时候会进来？访问计算属性的时候；那什么时候会访问计算属性呢？渲染模板的时候是吧，否则其他时候大概都没有（侦听属性侦听计算属性的时候也有，不过这种情况少见），你在method里面访问计算属性可没有工作watcher；所以当这里的Dep.target是渲染watcher的时候，执行depend方法，我们来看看depend方法，其实就是依次触发当前这个计算watcher订阅了的依赖的depend方法，什么意思呢？就是相当于把渲染watcher收集到了每个计算属性订阅的变量的依赖中去，也就是让渲染watcher一并订阅和计算属性有关的依赖，为什么呢？举个简单的例子：</p>
<pre><code class="language-vue">&amp;amp;lt;template&amp;amp;gt;
  &amp;lt;div&amp;gt;
    {{a}}
  &amp;lt;/div&amp;gt;
&amp;amp;lt;/template&amp;amp;gt;
&amp;amp;lt;script&amp;amp;gt;
export default {
  data() {
    return {
      b: 1,
      c: 0
    }
  },
  computed: {
    a() {
      return this.b + this.c
    }
  }
}
&amp;amp;lt;/script&amp;amp;gt;
</code></pre>
<p>上面这个例子的模板里面，只有a这个计算属性，并没有b、c这两变量，所以在进行模板渲染的时候，渲染watcher不会订阅b、c；那渲染watcher要更新渲染的话，就要订阅a这个依赖，那能订阅吗？在getter中收集依赖，在setter中触发依赖，你这计算属性又不能this.a = xxx这样用，那这就不能触发依赖啊，明显，这个计算属性依赖了b、c，所以间接地，渲染watcher应该订阅b、c来响应更新对不对？所以watcher.depend方法就是相当于将自己的订阅列表，拷一份儿给渲染watcher，让依赖这个计算属性渲染模板的渲染watcher知道，哪些依赖变动之后自己就会变，就要触发更新。</p>
<p>当计算属性第一次被求值后，dirty就被置为false，后续的访问都可以不求值直接返回值，直到触发update方法，我们走一遍这个流程，就用上面那个demo，我们触发this.b++，这个时候b被那几个watcher订阅呢？首先就是计算watcher，然后是渲染watcher，然后遍历这些watcher，触发update；这里需要注意的是，vue会在触发watcher更新的时候先按照它们的id从小到大排序，由于计算属性的初始化时肯定排在渲染模板之前的，所以计算watcher必然排在渲染watcher之前被触发udpate，这个时候就会把dirty置为true，随后渲染watcher重新渲染模板去访问计算属性，正好dirty为true，重新求值，完美的逻辑，vue源码的优美令人舒畅。</p>
<h2>最后</h2>
<p>vue2中的计算属性源码流程分析就看完了，整个流程下来非常清晰，逻辑没有一丝丝累赘，计算属性的强大功能通过一个lazy watcher和一个watcher排序通知完美的展现。希望本篇文章能够帮助到你更好的理解计算属性和vue的内部执行流程，我是Godlanbo，我们下篇文章见。</p>

        ]]></description>
      </item><item>
        <title>vue2响应式中Dep类位置的问题-补充</title>
        <link>http://godlanbo.com/blogs/33</link>
        <guid>33</guid>
        <pubDate>Wed, 17 Feb 2021 09:58:26 GMT</pubDate>
        <description><![CDATA[
          <h2>前言</h2>
<p>接着<a href="https://godlanbo.com/blogs/32">上文</a>的话题，我们这次再来讨论一下闭包中保存的dep和响应式对象身上observer类中保存的dep类他们分别的作用。</p>
<h2>源码分析</h2>
<p>上一篇文章分析了observer类中保存的dep类主要是用来给数组使用的，对于对象来说，把他删了问题也不大，因为对象每一个key都有自己的闭包dep来收集依赖。</p>
<p>不过上次的分析仅限于对于响应式核心实现代码的分析。我们都知道对于vue2来说，直接访问数组下标和给对象赋值一个不存在的属性是不会触发响应式更新的，我们需要用到vue给我们提供的api，Vue.set来设置，这次的分析就是基于这个set方法的源码进行的，我们先来看一下这个set方法的实现：</p>
<pre><code class="language-js">// vue-2.6.11/src/core/observer/index.js
export function set (target: Array&amp;amp;lt;any&amp;amp;gt; | Object, key: any, val: any): any {
  // 检测第一个参数是否是undefined或者基础类型，是就报错
  if (process.env.NODE_ENV !== 'production' &amp;amp;&amp;amp;
    (isUndef(target) || isPrimitive(target))
  ) {
    warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
  }
  // 处理是数组的情况
  if (Array.isArray(target) &amp;amp;&amp;amp; isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // 如果这个key本来就存在，那就直接赋值返回
  if (key in target &amp;amp;&amp;amp; !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // 尝试获取对象身上的observer对象
  const ob = (target: any).__ob__
  // 如果尝试对vue实例或者根data对象操作，会报一个警告
  if (target._isVue || (ob &amp;amp;&amp;amp; ob.vmCount)) {
    process.env.NODE_ENV !== 'production' &amp;amp;&amp;amp; warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    )
    return val
  }
  // 如果没有拿到observer对象，说明target不是一个响应式对象，直接赋值返回即可
  if (!ob) {
    target[key] = val
    return val
  }
  // 将新的key加入到target上并且变成响应式的
  defineReactive(ob.value, key, val)
  // 触发更新，返回值
  ob.dep.notify()
  return val
}
</code></pre>
<p>分析一下这个方法，首先参数校验，没什么好说的；之后判断<code>target</code>是不是数组，是数组的话就用<code>splice</code>方法对值进行修改，由于数组的响应式是使用拦截7个原生方法的方式实现的，所以这样就可以触发数组的响应；如果不是数组的话那就是一个对象，尝试去拿对象身上的<code>observer</code>对象，然后校验一下<code>target</code>；之后判断刚刚是否有拿到<code>observer</code>对象，如果没拿到，那么说明这个对象不是一个响应式对象，那么直接赋值返回就行，如果能拿到，那就用<code>defineReactive</code>往<code>target</code>身上添加一个新的响应式<code>key</code>，在<code>defineReactive</code>的时候并不会触发更新，所以我们手动触发一下更新。</p>
<p>整个流程就是这样，需要注意的点在这里，最后手动触发更新是使用的对象身上的<code>observer</code>对象中的dep去通知更新的，因为新的属性刚刚添加上去，我们无法去触发闭包中的<code>dep</code>去通知更新，并且它自己的闭包<code>dep</code>类中也还没有依赖。那为什么用它所在对象的<code>dep</code>去触发更新呢？我举个例子：</p>
<pre><code class="language-js">let obj = {
  a: {
    b: 12,
    c: 34
  }
}
</code></pre>
<p>对于这个对象，我们尝试往<code>obj.a</code>上添加一个新的属性<code>d</code>，这时我们就需要触发与<code>obj.a.d</code>有关的更新，但是这是个新添加的属性，也没有收集过它的依赖，vue不知道这个新属性和哪些地方有关联，需要更新哪些地方，那么怎么做呢？</p>
<p>vue就尝试去猜测，你想啊，最可能与<code>obj.a.d</code>有关的地方是哪里呢？没错，就是使用了<code>obj.a</code>的地方是吧？无论是短路表达式<code>obj.a &amp;amp;&amp;amp; obj.a.d</code>也好，或是<code>v-if=&amp;quot;obj.a.d&amp;quot;</code>也好，我们访问d属性，都会经过obj.a这个对象，恰好，我们在<code>defineReactive</code>对<code>key</code>为<code>a</code>做响应式处理的时候，会拿到一个<code>childOb</code>，这个<code>childOb</code>就是<code>obj.a</code>这个对象的<code>observer</code>对象，在上一篇文章中我提到，这个<code>childOb.dep.denpend()</code>删了之后，对于对象键的响应式是没有影响的，它是用来给数组用的，所以这个<code>observer</code>对象的<code>dep</code>类收集的是什么依赖呢？其实就是所有关于<code>obj.a</code>的依赖，所以只要访问到了<code>a</code>这个键的地方，<code>{b:12, c:34}</code>这个对象的<code>observer</code>下的<code>dep</code>就会收集起来。</p>
<p>所以我们拿到这个<code>observer</code>对象中的<code>dep</code>去通知更新，就是通知所有与<code>obj.a</code>有关的地方更新，然后就会进入patch流程重新计算vnode，在重新渲染的过程中，用到<code>obj.a.d</code>的地方就会触发get，然后添加<code>d</code>响应式的<code>defineReactive</code>中的闭包<code>dep</code>就会收集这些依赖，最后重新渲染完之后，闭包dep就收集好了所有关于<code>obj.a.d</code>的依赖。</p>
<p>这样vue就完成了添加一个新属性，从所有可能用到它的地方，筛选出了全部真正用到它的地方作为依赖保存了起来，看懂之后简直妙的不行。分析完set方法我们也知道了，<code>childOb.dep.denpend()</code>和<code>observer</code>类上的<code>dep</code>，不仅给数组使用，还用于给<code>set</code>方法等api用来便利的通知更新。</p>
<h2>最后</h2>
<p>在阅读vue源码的过程中，常常遇到一些完全不知道是拿来干嘛的实现，比如这两篇文章中讨论的childOb、闭包dep和observer中的dep实例等等，往往需要大量的时间去思考和写demo去调试，看代码的执行流程才能够看懂某段代码，甚至某一行代码的作用。vue作为现在最热门的三大框架之一，它的代码精炼程度自不必说，值的我反复去咀嚼。</p>
<p>我是Godlanbo，我们下篇文章见。</p>

        ]]></description>
      </item><item>
        <title>vue2响应式中Dep类位置的问题</title>
        <link>http://godlanbo.com/blogs/32</link>
        <guid>32</guid>
        <pubDate>Mon, 08 Feb 2021 15:53:01 GMT</pubDate>
        <description><![CDATA[
          <h2>前言</h2>
<p>最近在学习vue2中的响应式原理，照着教程手写代码，大部分都可以理解，这里记录一下遇到的几个关于Dep类很难理解的点，很少有教程谈到这个地方。本文不提供vue2响应式原理任何教学，仅作为难点补充，毕竟这方面的文章网上简直多如牛毛，我的学习源码地址在<a href="https://github.com/godlanbo/vue2reactive">这里</a>，如果需要可以拉下来研究。</p>
<h2>对Dep实例化的位置和操作的疑惑</h2>
<p>首先我们知道Dep类的作用是存放依赖，它负责将收集依赖，并且当变量值发生变化后通知Watcher更新，所以我们每一个响应式的值都需要一个Dep类；这里把相关的代码贴一下，是相关defineReactive函数和Observer类的：</p>
<pre><code class="language-js">// 下面的实现均省略了无关代码
function defineReactive(data, key, value) {
  if (arguments.length === 2) {
    value = data[key]
  }
  // 闭包保存的Dep类
  let dep = new Dep()
  let childOb = observe(value)
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    set(newValue) {
      // ...
    },
    get() {
      // 收集依赖，这里的依赖是和这个key相关的值的依赖
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
      }
      return value
    }
  })
}
// Obsever类
class Observer {
  constructor(value) {
    this.dep = new Dep()
    def(value, '__ob__', this, false)
    if (Array.isArray(value)) {
      // ...
    } else {
      // ...
    }
  }
  // ...
}
</code></pre>
<p>我们在每一次执行defineReactive将一个键值变成响应式的时候，都利用闭包保存了它的Dep，也就是<code>let dep = new Dep()</code>这里，然后在下面触发get的时候进行依赖收集。<strong>这里是第一个疑惑的问题</strong>，我们可以看到在Observer类上会实例化一个dep对象来保存依赖，那为什么还要在闭包中再实例一个dep对象来保存依赖呢，究竟两个都起作用还是什么？</p>
<p>继续看defineReactive，我们可以看到在这里有一个操作，我们拿到当前这个key的值后对它进行响应式处理，这没什么问题，然后将它的observer对象存了下来（childOb），在触发依赖收集的时候，不仅对当前key的依赖列表进行了收集，还要触发这个key对应值的依赖列表进行收集；<strong>这里就是第二个疑惑的问题</strong>，这里为什么要再利用childOb对象上的dep去收集一次依赖？</p>
<p>可能这里不太能明白第二个问题，我举个例子：</p>
<pre><code class="language-js">let obj = {
  a: {
    b: 1
  }
}
</code></pre>
<p>我们将这个对象响应式处理后的相关逻辑做成一张图：</p>
<p><img src="https://godlanbo.com/images/1612799503511.jpg" alt="image20210208224627867.png"></p>
<p>还是图片方便理解一点，可以看到对于{b：1}来说，有两个dep实例能收集到对应的依赖，一个是dep实例3，保存在闭包中，触发b的get的时候收集；一个是dep实例2，由于访问b需要访问a，所以a属性的闭包中的childOb就是{b：1}的observer实例，触发a的get后会触发childOb.dep的依赖收集。</p>
<p>这就可以明显的看到，对象{b：1}的observer中的dep实例2是多余的，dep实例3已经能够对b的变化做出感知，而且如果这个对象有多个键，比如：</p>
<pre><code class="language-js">let obj = {
  a: {
    b: 1,
    c: 2
  }
}
</code></pre>
<p>那么{b：1，c：2}对象上的observer实例中dep类就无效了，因为我们知道对象的每一个键都需要一个dep实例来保存依赖。这就是第二个问题，这里的<code>childOb.dep.denpend()</code>的作用让我迷惑。</p>
<h2>为什么要这样设置Dep</h2>
<p>其实两个问题的答案可以归结到一个东西身上，那就是数组。</p>
<p>我们知道数组的响应式是比较特殊的在vue2中，在vue2中是利用了原型链的特性拦截了数组的7个方法进行了改造。当使用了数组的这七个方法之一，我们就需要通过dep类去通知Watcher，那么数组的dep放在哪里的呢？在上面对于对象的dep，我们是放在defineReactive的闭包中，因为这里有对应key的get和set，他们都能访问到。但是数组的拦截器并不能访问到这里的dep，所以我们需要一个其他地方来放置数组的dep，让它既可以被拦截器访问，也可以在数组的get中访问。所以，才有了observer类上的dep实例，它是为数组服务的，在拦截器中可以通过<code>this.__ob__.dep</code>拿到他。</p>
<p>看到这里，其实第二个问题的答案也浮出水面了，对于对象来说，只能有一个observer实例，当然不能用它上面的唯一dep来存放依赖，因为对象的每个key都需要一个依赖列表。但是对于数组来说，一个数组无论多长，只需要一个dep来存放依赖就可以了，即使这个数组保存了很复杂的对象，但是那个对象自己会有自己的dep，所以数组只需要一个dep来响应拦截器上的对数组的操作方法就好了，举个例子：</p>
<pre><code class="language-js">let obj = {
  a: ['1']
}
</code></pre>
<p>这样，在key为a的defineReactive闭包中，childOb就是['1']的observer对象，我们直接访问a就可以拿到这个数组，此时触发a的get，就可以触发childOb.dep.depend收集依赖，是不是很巧妙？</p>
<h2>总结</h2>
<p>所以总得来说，不管是Observer上看似多余的dep实例也好还是childOb对于对象那多余的依赖收集也罢，这些东西都是服务于数组这个特殊的情况的，我已经测试过了，将childOb部分的代码删除，完全不影响对象形式的Watcher通知，但是数组就歇菜了，一加上它就可以让数组正常工作。</p>
<h2>写在最后</h2>
<p>可能行文逻辑有点乱，因为这个东西比较抽象，确实不太好讲，我也就是记录一下这个困惑了我很久的问题，看过两遍响应式相关的教程了，自己也写过一遍，但是这里一直不太懂为什么，这里弄懂了写篇博客记录一下。</p>
<p>如果你也有相同的疑惑，并且看了本文还是不懂文章中提到的问题，欢迎在下方评论或者直接在留言板给我留言。</p>

        ]]></description>
      </item><item>
        <title>axios源码学习（四）</title>
        <link>http://godlanbo.com/blogs/31</link>
        <guid>31</guid>
        <pubDate>Wed, 03 Feb 2021 10:02:53 GMT</pubDate>
        <description><![CDATA[
          <h2>前言</h2>
<p>在前面的axios源码学习中，我们了解了axios的创建过程，请求发送和拦截器的构造，这次我们来学习axios是如何做到取消请求的。</p>
<h2>实例</h2>
<p>还是一样，在看源码之前，我们需要先看一下axios是如何使用取消请求的，然后再对应去看使用部分的源码：</p>
<pre><code class="language-html">&amp;amp;lt;body&amp;amp;gt;
  &amp;amp;lt;button onclick=&amp;quot;request()&amp;quot; style=&amp;quot;margin-right: 10px;&amp;quot;&amp;amp;gt;发送请求&amp;amp;lt;/button&amp;amp;gt;
  &amp;amp;lt;button onclick=&amp;quot;cancleRequest()&amp;quot;&amp;amp;gt;取消&amp;amp;lt;/button&amp;amp;gt;
  &amp;amp;lt;script&amp;amp;gt;
    let cancel = null
    function cancleRequest() {
      cancel('我不想发送了')
      cancel = null
    }
    function request() {
      axios({
        method: 'get',
        url: 'http://localhost:3000/posts',
        cancelToken: new axios.CancelToken(function (c) {
          cancel = c
        })
      }).then(res =&amp;amp;gt; {
        console.log(res)
      }).catch(err =&amp;amp;gt; {
        console.log(err)
      })
    }
  &amp;amp;lt;/script&amp;amp;gt;
&amp;amp;lt;/body&amp;amp;gt;
</code></pre>
<p>我们将json-server服务的响应延迟调高一点，具体设置请查看<a href="https://github.com/typicode/json-server#cli-usage">json-server的配置</a>。然后我们就可以发送请求，利用创建CancelToken时传给我们的<code>c</code>函数，调用它就可以取消请求，而且还能传入取消理由，我们可以在catch中拿到这个理由：</p>
<p><img src="https://godlanbo.com/images/1612344497529.jpg" alt="image20210203154445321.png"></p>
<p>如果要达到这个效果，需要给axios发送时的配置对象传入一个属性<code>cancelToken</code>，他接受一个<code>CancelToken</code>对象，这个对象是axios内置的。</p>
<h2>源码分析</h2>
<p>知道了如何配置axios才能发送请求后，我们就可以开始对应去看那部分的源码了；首先可以看到我们需要调用axios上的一个属性：CancelToken，这个在属性是在axios.js这个入口文件中被挂载到实例上的：</p>
<pre><code class="language-js">// axios.js
axios.CancelToken = require('./cancel/CancelToken');
</code></pre>
<p>在第一篇文章中说道了axios的目录结构中有个cancel目录，下面存放的就是取消功能相关的文件，我们在这里可以找到<code>CancelToken</code>的实现：</p>
<pre><code class="language-js">function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }
	// 这里是关键，声明了一个promise，将变化它状态的resolve函数赋值
  // 给了一个外面的变量resolvePromise，也就是说resolvePromise这个变量
  // 现在有了改变这个promise的状态的能力
  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      return;
    }
    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}
</code></pre>
<p>抛开细枝末节的代码，<code>CancelToken</code>的代码非常少，短短20+行。首先它判断了一下参数类型，这个没什么好说的，下面才是关键，<code>CancelToken</code>在自己身上声明了一个<code>promise</code>，它是未解决的，改变它状态的<code>resolve</code>函数被赋值给了外面的<code>resolvePromise</code>变量，也就是说我们运行resolvePromise()，CancelToken身上的promise状态就会变为解决。</p>
<p>然后继续往下看，<code>executor</code>是我们<code>new CancelToken</code>时传入的函数，它往我们这个函数中传入一个参数，这个参数就是<code>cancel</code>函数，也就是我们上面实例里收到的<code>c</code>参数，这个函数接受一个<code>message</code>作为参数，然后看它的内容，<code>token</code>就是<code>CancelToken</code>本身；如果<code>token</code>身上已经有<code>renson</code>属性，就直接返回。这里是为了防止你反复调用取消函数出错，看下面如果没有这个<code>reason</code>属性，就往<code>token</code>上加一个reason属性，值是用<code>message</code>创建的一个取消对象，其实就是个消息对象，在cancel目录下的Cancel.js中可以看到他的构造函数：</p>
<pre><code class="language-js">// Cancel.js
function Cancel(message) {
  this.message = message;
}
</code></pre>
<p>所以如果你取消成功了，<code>token</code>身上就会有这个<code>reason</code>属性，你再去调用取消函数就直接返回了。函数最后调用<code>resolvePromise</code>传入这个取消对象，我们上面说了<code>resolvePromise</code>这个函数，就是<code>CancelToken.promise</code>的<code>resolve</code>函数，也就是说这里把取消对象作为解决值改变了<code>CancelToken.promise</code>的状态，让他变为了解决状态。</p>
<p>好，到这里CancelToken对象的面貌我们就已经熟知了，然后我们知道，axios本身自己，是没有取消请求的功能的，最终需要取消请求还是要依托xhr对象的abort函数，调用它才能够真正做到取消函数，它是在真正产生请求对象的地方被调用的，也就是适配器那里，我们去到适配器文件中，找到对应的代码：</p>
<pre><code class="language-js">// xhr.js
// ...
if (config.cancelToken) {
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }
    request.abort();
    reject(cancel);
    // 清理请求对象
    request = null;
  });
}
// ...
</code></pre>
<p>我们直接找到关于请求取消功能的核心代码，这次更少了，只有10+行，但是实现很巧妙，我们来分析一下。</p>
<p><code>config</code>对象就是配置对象，他由我们的传入的配置对象和默认配置对象合并得到，首先判断上面是否有<code>cancelToken</code>属性，如果有才走取消的逻辑，也就是我们只有配置对象中<code>cancelToken</code>不为空才走这里，这个结合实例很好理解；我们传入的<code>cancelToken</code>属性上实例化了一个<code>CancelToken</code>对象，<code>CancelToken</code>对象上有一个promise属性，这里调用它的<code>then</code>方法，往里面传入了一个函数，由于一开始这个<code>promise</code>是等待状态，<code>then</code>方法中的函数不会被执行。</p>
<p>看到这里其实已经能够和前面<code>CancelToken</code>的代码联系起来了，是的，当我们在外面调用CancelToken得取消函数时，就会使得<code>CancelToken.promise</code>的状态变为解决，所以这里的then方法就会执行，我们来看这个函数的作用，首先是判断请求是否还在，<code>request</code>就是<code>xhr</code>对象创建的请求，当请求结束或者已经被取消都会把它置为<code>null</code>，所以这里判断是否还满足取消条件，不满足就直接返回；满足的话就执行<code>request.abort()</code>，也就是前面说的，真正起取消作用的函数；然后清理<code>request</code>，以传入的<code>cancel</code>对象作为拒绝理由将整个适配器返回的<code>promise</code>状态置为拒绝，这样用户在外面就可以通过<code>catch</code>拿到这个取消对象和里面的消息。</p>
<h2>写在最后</h2>
<p>axios的取消功能利用promise实现了巧妙的联动，将改变状态的resolve以函数形式（cancel函数执行resolve）暴露给用户，然后利用这个promise的then方法中的函数执行相应的动作，实现了一种触发式的控制。</p>
<p>至此，axios源码学习系列就完结了，关于axios的主要流程的源码都已经看过了，其他什么帮助函数和一些参数规则化的代码就不再做文详解了，希望这个系列能够帮助你学习到axios的源码设计，更加了解axios。我们下篇文章再见。</p>

        ]]></description>
      </item><item>
        <title>axios源码学习（三）</title>
        <link>http://godlanbo.com/blogs/30</link>
        <guid>30</guid>
        <pubDate>Mon, 01 Feb 2021 08:17:05 GMT</pubDate>
        <description><![CDATA[
          <h2>前言</h2>
<p>本篇我们来阅读学习axios关于拦截器相关的源码部分，事先说一下，阅读学习这部分代码最好熟悉Promise的相关知识，否则可能会看不懂axios对于拦截器的链式调用设计。</p>
<h2>实例</h2>
<p>在阅读源码之前，我们先来看一个axios拦截器的使用实例：</p>
<pre><code class="language-js">// 请求拦截器
axios.interceptors.request.use(function requestOne(config) {
  console.log('request - 1')
  return config
}, err =&amp;amp;gt; {})
axios.interceptors.request.use(function requestTwo(config) {
  console.log('request - 2')
  return config
}, err =&amp;amp;gt; {})
// 响应拦截器
axios.interceptors.response.use(function responseOne(response) {
  console.log('response - 1')
  return response
}, err =&amp;amp;gt; {})
axios.interceptors.response.use(function responseTwo(response) {
  console.log('response - 2')
  return response
}, err =&amp;amp;gt; {})
axios({
  method: 'get',
  url: 'http://localhost:3000/posts'
}).then(res =&amp;amp;gt; {
  console.log(res)
})
</code></pre>
<p>得到的结果是：</p>
<p><img src="https://godlanbo.com/images/1612167383884.jpg" alt="image20210201141625038.png"></p>
<p>可以看到对于请求拦截器，先执行了后挂载的拦截器函数，顺序是倒着的，而响应拦截器的执行顺序是和挂载顺序一致的，这是什么原因呢？看了这部分的源码实现，我们就可以明白这里的原因了。</p>
<h2>源码解析</h2>
<p>首先，调用拦截器进行挂载是在<code>axios.interceptors</code>上，那<code>interceptors</code>是从哪里来的呢，回想我们在第一篇文章中所学习的axios实例的创建过程，关于请求相关的方法是从<code>Axios.prototype</code>复制过来的，<code>defaults</code>和interceptors属性是从一个<code>Axios</code>实例本身上复制过来的；所以这里<code>interceptors</code>属性就是在Axios对象上的属性：</p>
<pre><code class="language-js">function Axios(instanceConfig) {
  this.defaults = instanceConfig;
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
</code></pre>
<p>可以看到<code>interceptors</code>是一个对象，上面保存了两个<code>InterceptorManager</code>对象，这个对象就是拦截器管理对象，<code>request</code>和<code>response</code>就分别代表着请求拦截器和响应拦截器，我们看看<code>InterceptorManager</code>对象是怎么实现的：</p>
<pre><code class="language-js">function InterceptorManager() {
  this.handlers = []; // 存放拦截器的数组
}
// 添加拦截器
InterceptorManager.prototype.use = function use(fulfilled, rejected) {
  this.handlers.push({
    fulfilled: fulfilled,
    rejected: rejected
  });
  return this.handlers.length - 1;
};
// 移除对应id的拦截器
InterceptorManager.prototype.eject = function eject(id) {
	// ...
};
// 遍历拦截器的方法
InterceptorManager.prototype.forEach = function forEach(fn) {
  // ...
};
</code></pre>
<p>这个对象的实现其实很简单，一个数组负责存放拦截器，有一个use方法，也就是我们使用<code>axios.interceptors.request.use</code>时调用的方法，它的作用就是把你传入的拦截器函数push进自己的handlers数组里，当你看到这里的形参名称：<code>fulfilled</code>，<code>rejected</code>，也许你已经猜到这和Promise的调用脱不开关系。</p>
<p>好了，介绍完拦截器的创建，我们再回来看看发送请求的函数<code>Axios.prototype.request</code>中，是如何使用这个<code>InterceptorManager</code>来达到拦截的效果的：</p>
<pre><code class="language-js">Axios.prototype.request = function request(config) {
	// 这里省略各种config的解析设置，我们这次着重关注拦截相关代码
  // ...
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
	// 构造请求拦截器
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
	// 构造响应拦截器
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
};
</code></pre>
<p>我们省略对于config的设置，直接看拦截器的相关代码；首先声明了一个chain数组，第一项是我们的派发请求函数，第二项是一个undefined，在第二篇文章中我们知道<code>dispatchRequest</code>函数会返回一个Promise，它解决之后的结果就是响应体。</p>
<p>然后往下看，开始循环请求拦截器，这个<code>forEach</code>是它自己封装的，其实就是和我们平时使用的差不多，这里你就把它当做普通的遍历就好了，这里遍历的是<code>InterceptorManager</code>中的handlers，取到里面的每一个拦截器<code>interceptor</code>，将它的<code>fulfilled</code>和rejected函数插入到<code>chain</code>的<strong>前面</strong>。没错，你看这里是使用的<code>chain.unshift</code>方法，这个方法往数组前面插入传入的参数，也就是说chain会变成这个样子：</p>
<pre><code class="language-js">[interceptor.fulfilled, interceptor.rejected, dispatchRequest, undefined]
</code></pre>
<p>所以，每次遍历一对请求拦截器，就会往chain前面添加一对fulfilled、rejected函数，我们就拿实例中写的demo做测试，看清楚demo中的函数名字，我们debug看一下遍历完请求拦截器之后，chain变成什么样子了：</p>
<p><img src="https://godlanbo.com/images/1612167398586.jpg" alt="image20210201152332432.png"></p>
<p>可以看到，由于是往前面插入，先挂载的<code>requestOne</code>被先插入了进去，后挂载的<code>requestTwo</code>后插入；到这里结合下面的promise循环，我们大概已经能够想到为什么请求拦截器的执行顺序会与挂载相反，因为它是被插入到了chain数组前面，导致了倒序。</p>
<p>接着看响应拦截器的循环，和请求拦截器是一样的做法，只不过换成了<code>chain.push</code>往里面加入拦截器函数，整个拦截器添加流程走完，我们再来看一下chain：</p>
<p><img src="https://godlanbo.com/images/1612167409916.jpg" alt="image20210201152812874.png"></p>
<p>由于所有的响应拦截器函数是被依照遍历顺序push进去的，所以他们的相对顺序没有变化，在后面执行的时候，就和挂载的顺序一致。</p>
<p>至此，请求链数组chain已经被构造完毕，我们往下看：</p>
<pre><code class="language-js">while (chain.length) {
  promise = promise.then(chain.shift(), chain.shift());
}
</code></pre>
<p>初始的promise是用config构造的解决状态的promise，然后一直循环直到chain数组为空；接着看循环内容，每次从chain的前端拿出两个函数分别作为当前Promise的解决回调和拒绝回调。由于Promise的特性，每个请求拦截器函数返回的config会被封装为解决状态的Promise，这样下一次循坏的时候就可以直接用then方法拿到config，我们简单展开这里：</p>
<pre><code class="language-js">let config = {}
let promise = Promise.resolve(config)
promise
.then(function requestTwo(config) {
  console.log('request - 2')
  return config
}, err =&amp;amp;gt; {})
.then(function requestOne(config) { // 这里其实就是在链式调用
  console.log('request - 1')
  return config
}, err =&amp;amp;gt; {})
// 由于拦截器是未知长度的，你可以添加很多个，所以直接全部then链式调用不现实，但是我们可以利用循环
// 下面这种实现和上面的效果完全一致，这样我们就可以利用循环加一个中间变量将整个链串起来
promise = promise
.then(function requestTwo(config) {
  console.log('request - 2')
  return config
}, err =&amp;amp;gt; {})
// 这个promise其实就是上一次返回的结果，解决状态的值就是config，所以下一个then里面仍然可以拿到
// config对象
promise.then(/* ... */)
</code></pre>
<p>所以我们就可以这样每次一对按着处理这样的请求拦截器，直到到<code>dispatchRequest</code>函数，这里你就清楚为什么后面需要跟一个<code>undefined</code>来占位了，因为每次都是两个函数被取出分别作为Promise的解决回调和拒绝回调。（其实<code>undefined</code>还起到传递错误的作用，前面的请求拦截器如果出了错，就会进入拒绝回调，当拒绝原因传到<code>undefined</code>这里 时，Promise的特性会导致这里发生透传，就直接传到后面响应拦截器的拒绝回调里去了）</p>
<p>由于Promise链式调用的特性，当<code>dispatchRequest</code>未返回的时候，后面的响应拦截器也不会被执行，直到<code>dispatchRequest</code>返回结果，状态变更为解决，就会将<code>response</code>传递给后面的响应拦截器中，然后就是和前面一样的工作方式了，依次执行拦截器将结果一个一个往后面传，直到<code>chain</code>为空，说明已经处理完所有的拦截器处理函数，就把最后一次的结果返回出去，给到用户，这也是一个Promise，所以我们调用then方法就可以拿到<code>response</code>。</p>
<h2>写在最后</h2>
<p>这次拦截器源码的阅读可能需要相当的Promise功力才能够理解，其实只要你了解过Promise的链式调用和参数传递的特性，当看到chain数组被构建出来的时候，你就应该能够理解axios拦截器是怎么运作的了，最后那个循环其实是最简单的部分，最需要学习的，是那个chain的构造思想，这是拦截器的精髓。</p>
<p>希望这边文章能够帮助你更了解axios拦截器的内容，下一篇文章将阅读学习axios是如何做到可以取消请求发送的，我们下篇文章见。</p>

        ]]></description>
      </item><item>
        <title>axios源码学习（二）</title>
        <link>http://godlanbo.com/blogs/29</link>
        <guid>29</guid>
        <pubDate>Sun, 31 Jan 2021 07:10:38 GMT</pubDate>
        <description><![CDATA[
          <h2>前言</h2>
<p>在axios源码学习（一）中我们阅读了关于axios创建流程的源码，了解了axios的创建过程以及它为什么既可以当做函数调用发送请求，也可以调用它的方法去发送请求。这次我们就来阅读关于axios发送请求相关的源码。</p>
<h2>Request</h2>
<p>这里先写一个axios发送请求的小demo，关于请求的url，可以使用json-server来在本地起一个小的服务来提供数据，关于如何使用json-server请查看<a href="https://github.com/typicode/json-server">它的github</a>：</p>
<pre><code class="language-js">axios({
  method: 'get',
  url: 'http://localhost:3000/posts'
}).then(res =&amp;amp;gt; {
  console.log(res)
})
</code></pre>
<p>我们在上一篇文章中了解到，axios当做函数使用，就是调用的request方法，所以我们来细看<code>Axios.prototype.request</code>方法：</p>
<pre><code class="language-js">Axios.prototype.request = function request(config) {
  // 允许axios使用 axios(url, config) 这种形式来发送请求
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
	// 将你传入的配置对象和默认的配置对象进行合并
  config = mergeConfig(this.defaults, config);
  // 设置请求的方法
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  // 拦截器相关的构造
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
  // 拦截器的相关代码已被省略
  // ...
  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }
  return promise;
};
</code></pre>
<p>首先就是对传入的config对象进行一些判断，若没有传递config则设置为空对象。之后用axios封装的<code>mergeConfig</code>方法对传入的配置和默认的配置进行合并，合并的时候，传入的配置具有更高优先级，可以覆盖默认配置。然后设置请求方法。</p>
<p>关键地方来了，这里声明了一个chain数组，存放了一个函数和一个undefined，第一个<code>dispatchRequest</code>就是真正的去做派发请求的函数，那么这里为什么不直接调用<code>dispatchRequest</code>而是创建一个数组，而创建一个数组为什么第二个位置需要放一个<code>undefined</code>的呢？这些都是为了后面拦截器功能做准备的，现在我们先可以不用看懂它的作用，之后在阅读了拦截器代码之后刚刚的问题就解决了。</p>
<p>接下来将配置对象转换为一个解决状态的Promise，所以在最后的<code>promise = promise.then(chain.shift(), chain.shift());</code>这里，一定会进入promise的成功回调，也就是第一个函数，<code>dispatchRequest</code>函数中，我们来看看这个函数：</p>
<pre><code class="language-js">function dispatchRequest(config) {
	// ...
  config.headers = config.headers || {};
  // 转换data数据
	// ...
  // 打扁header
	// ...
	// 清理headers上的其他方法配置
  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );
  // 获取适配器
  var adapter = config.adapter || defaults.adapter;
  return adapter(config).then(function onAdapterResolution(response) {
    // 省略转换response...
    return response;
  }, function onAdapterRejection(reason) {
		// 省略错误处理...
    return Promise.reject(reason);
  });
};
</code></pre>
<p>在<code>dispatchRequest</code>中，首先拿到config对象就是一通转换，转换data，处理header；然后headers上面对于每种请求方法（get、post...）都事先有准备好一些header的配置，现在确认请求方法后，就可以取出对应方法的配置然后把这些预先的准备都删掉了。</p>
<p>然后拿到适配器，适配器就是用来适配在node环境和浏览器环境是使用<code>http</code>模块还是<code>XMLHttpRequest</code>对象来发送请求的。然后就把请求发送出去，适配器返回一个promise，在解决状态的promise中就可以拿到响应结果<code>response</code>。</p>
<p>所以<code>dispatchRequest</code>函数将适配器（是一个Promise）返回，然后在<code>request</code>方法再返回出去，我们就可以利用Promise的特性直接从<code>request</code>的结果中使用<code>then</code>方法拿到由适配器<code>then</code>结果中返回的<code>response</code>了。</p>
<p>我简单的写一个模拟demo，梳理一下这个流程：</p>
<pre><code class="language-js">function Axios() {}
// 模拟适配器
function adapter(config) {
  return new Promise(resolve =&amp;amp;gt; {
    resolve({
      config,
      data: { name: 'godlanbo' }
    })
  })
}
// 分发函数
dispatchRequest = function(config) {
  return adapter(config).then(response =&amp;amp;gt; {
    return response
  })
}
Axios.prototype.request = function(config) {
  promise = Promise.resolve(config)
  // 这里源码中的写法其实就是这里的一种简写，这里可能需要你有promise基础
  // 就能懂 promise.then(dispatchRequest)其实和下面这种写法一样的
  promise = promise.then(config =&amp;amp;gt; {
    return dispatchRequest(config)
  })
  return promise
}
axios = Axios.prototype.request
axios({
  method: 'get',
  url: 'http://localhost:3000/posts'
}).then(res =&amp;amp;gt; {
  console.log(res)
})
</code></pre>
<p>这个demo是可以运行的，你可以自己试一试，最后在console中输出的，就是在适配器中传入的响应内容，如果这里的promise传递你不能够理解，可以参考<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise">MDN上的Promise教程</a>学习相关内容后再来看就能看懂了。</p>
<h2>写在最后</h2>
<p>本文我们阅读了axios关于发送请求流程的源码，知道了axios是怎么把config对象一级一级往下传最后发送请求的；清楚了axios是如何发送请求的之后，下一篇将阅读和学习axios最精髓的部分，拦截器相关的源码。那么，我们下一篇文章见。</p>

        ]]></description>
      </item><item>
        <title>axios源码学习（一）</title>
        <link>http://godlanbo.com/blogs/28</link>
        <guid>28</guid>
        <pubDate>Sat, 30 Jan 2021 09:13:08 GMT</pubDate>
        <description><![CDATA[
          <h2>前言</h2>
<p>看源码学习，着重看脉络，学习思想，所以在学习和记录的过程中，我不想对每一个细节都展开讲，不影响我们理解主要流程的模块就简单提及一下它的功能。</p>
<h2>目录结构</h2>
<p>先来查看一下axios的目录结构</p>
<p>&amp;gt; ├─lib
&amp;gt; │  │  axios.js
&amp;gt; │  │  defaults.js
&amp;gt; │  │  utils.js
&amp;gt; │  ├─adapters
&amp;gt; │  │      http.js
&amp;gt; │  │      xhr.js
&amp;gt; │  ├─cancel
&amp;gt; │  ├─core
&amp;gt; │  └─helpers</p>
<p>axios为入口文件，defaults是存放默认配置的文件，utils是工具函数文件。</p>
<p>adapters文件夹是用来存放适配器的目录，因为axios是可以在浏览器和node环境两种情况下运行的，这个适配器就是在不同的环境下运行的，浏览器环境就使用xhr（也就是<em>XMLHttpRequest</em>对象来发送请求），node环境就使用http模块来发送请求。</p>
<p>cancel文件夹是存放取消请求功能的目录，core是核心实现代码的目录，helpers是axios自己实现的各种帮助函数的存放目录。</p>
<h2>既是对象也是函数</h2>
<p>先从axios的创建看起，我们平时用axios的时候常常有下面这些写法：</p>
<pre><code class="language-js">// 设置axios的默认配置
axios.defaults.timeout = 5000
axios.defaults.method = 'post'
// 当做对象调用方法
axios.get({})
axios.post({})
axios.interceptors.request.use(() =&amp;amp;gt; {}, () =&amp;amp;gt; {})
axios.interceptors.response.use(() =&amp;amp;gt; {}, () =&amp;amp;gt; {})
// 当做函数直接使用
axios({})
</code></pre>
<p>这里可以看到一个<code>axios</code>使用的一个特点，就是<code>axios</code>这个变量，既可以当做函数使用，也可以当做对象调用上面的方法，它是怎么做到的呢，我们从它的创建开始看起：</p>
<pre><code class="language-js">// 创建实例
function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  var instance = bind(Axios.prototype.request, context);
  // 扩展Axios.prototype到实例instance上
  utils.extend(instance, Axios.prototype, context);
  // 将上下文扩展到instance上
  utils.extend(instance, context);
  return instance;
}
// 这个就是axios暴露给我们使用的变量
var axios = createInstance(defaults);
</code></pre>
<p>首先<code>axios</code>通过一个函数<code>craeteInstance()</code>来创建一个实例，它接受一个配置对象，一开始传进去的这个配置对象，就是从defaults文件中拿到的默认配置对象。然后走到第一步，利用传进来的默认配置创建一个上下文<code>context</code>，它是一个<code>Axios</code>对象，我们来看一下这个<code>Axios</code>对象的实现（与现在我们的目的无关的实现已经被省略）：</p>
<pre><code class="language-js">function Axios(instanceConfig) {
  // 将默认配置设置为自己的属性
  this.defaults = instanceConfig;
  // 这里是拦截器
  this.interceptors = {
    request: new InterceptorManager(),
    response: new InterceptorManager()
  };
}
// 以下是往原型上添加各种方法
// 这个request方法很重要，就是它负责派发请求，后面的get post方法的实现也是调用了这个方法
Axios.prototype.request = function request(config) {};
Axios.prototype.getUri = function getUri(config) {};
// utils.forEach是axios自己封装的遍历函数，类似原生的forEach
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {};
});
utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {};
});
</code></pre>
<p><code>Axios</code>对象有两个属性，<code>defaults</code>和<code>interceptors</code>，这里我们是不是想起了把axios当做对象使用的时候调用的<code>defaults</code>和<code>interceptors</code>，其实就是这里挂载的属性，<code>defautls</code>就是<code>axios</code>的默认配置，而<code>interceptors</code>就是拦截器。</p>
<p>然后<code>Axios</code>对象挂载了很多方法，像get、post、request啊之类的，这其中request负责真正的发送请求，其他的get、post实现里其实都是调用了request方法。</p>
<p>明白了<code>Axios</code>对象之后，我们就知道了<code>context</code>是什么了，然后继续创建的下一步，将<code>Axios.prototypt.request</code>函数作为实例，<code>this</code>绑定到<code>context</code>上（<code>bind</code>函数是axios自己封装的函数，功能就是原生的<code>function.bind</code>的功能)；所以现在我们的实例<code>instance</code>，其实就是<code>Axios.prototypt.request</code>这个函数，这里就做到了第一步，<code>axios</code>实例可以作为函数使用。</p>
<p>上面说到了这个<code>request</code>函数就是用来发送请求的，所以<code>axios</code>实例作为这个函数的话，就能够这样使用<code>axios</code>实例来发送请求：</p>
<pre><code class="language-js">axios({}).then(res =&amp;amp;gt; {})
</code></pre>
<p>所以<strong>axios实例本身，其实就是<code>Axios.prototypt.request</code>这个函数</strong>，但是axios还能作为对象调用这么多属性和方法，request这个函数可做不到这么多，所以继续往下看：</p>
<pre><code class="language-js">utils.extend(instance, Axios.prototype, context);
utils.extend(instance, context);
</code></pre>
<p>这个<code>utils.extend</code>是axios自己封装的函数，作用是将第二个参数上的所有属性复制到第一个参数上，然后<code>context</code>作为执行时的<code>this</code>。我们都知道JavaScript中，函数也是一种特殊的对象，所以这里就是在<code>insrance</code>这个特殊对象上挂载了<code>Axios.prototypt</code>上的所有方法（上面<code>Axios</code>对象的<code>prototype</code>上的各种请求方法），还挂载了context上的属性，而<code>context</code>是什么呢？就是<code>Axios</code>对象，这个对象上有<code>defaults</code>属性和<code>interceptors</code>属性。所以到了这里，我们就明白了，为什么axios实例可以调用那些<code>Axios</code>的方法和属性，是因为这里往axios实例上复制了<code>Axios</code>原型上的所有方法和<code>Axios</code>对象本身的属性：</p>
<p><img src="https://godlanbo.com/images/1611996379874.jpg" alt="image20210130163338986.png"></p>
<p>在控制台debug看一下<code>instance</code>实例，本身是一个函数，身上挂着许多方法，这就是axios实例既可以用作对象调用方法，本身自己也可以当做函数发送请求（因为它自己是request函数）的原因。</p>
<h2>写在最后</h2>
<p>本篇文章简单讲解了axios的创建过程，通过源码分析了为什么axios既可以通过函数调用发送请求，也可以当做对象调用方法，希望有帮助到你更加理解axios这个工具，下一axios源码学习将学习发送请求相关的源码逻辑，我们下篇文章见。</p>

        ]]></description>
      </item><item>
        <title>CSS实现多列等高布局</title>
        <link>http://godlanbo.com/blogs/27</link>
        <guid>27</guid>
        <pubDate>Fri, 29 Jan 2021 10:33:35 GMT</pubDate>
        <description><![CDATA[
          <h2>前言</h2>
<p>实现的场景，多列内容并排在容器里，容器高度靠最高的那一列撑开，其余比较矮的列自动在高度上撑满父容器。</p>
<p><img src="https://godlanbo.com/images/1611916189681.jpg" alt="image20210129164753928.png"></p>
<p>最终想要实现的效果：</p>
<p><img src="https://godlanbo.com/images/1611916199623.jpg" alt="image20210129164839959.png"></p>
<h2>解决方法</h2>
<ul>
<li>flex布局</li>
</ul>
<p>这种是最简单的方法，使用flex布局的情况下，利用<code>align-items：stretch</code>属性可以轻松办到多列等高布局，flex属性的兼容支持已经很好了（除了IE11以下），所以可以放心的使用。</p>
<pre><code class="language-html">&amp;amp;lt;!DOCTYPE html&amp;amp;gt;
&amp;amp;lt;html lang=&amp;quot;en&amp;quot;&amp;amp;gt;
&amp;amp;lt;head&amp;amp;gt;
  &amp;amp;lt;title&amp;amp;gt;Document&amp;amp;lt;/title&amp;amp;gt;
  &amp;amp;lt;style&amp;amp;gt;
    .container {
      display: flex;
      /* 
      	 这里也可以不设置 align-items：stretch，align-items的默认值为normal
      	 在普通的项目上，normal表现为和stretch一样的行为
      */
      width: 100%;
      overflow: hidden;
      background-color: #ccc;
    }
    .box {
      width: 33%;
      background-color: aqua;
      color: black;
      float: left;
      margin-right: 0.3%;
    }
  &amp;amp;lt;/style&amp;amp;gt;
&amp;amp;lt;/head&amp;amp;gt;
&amp;amp;lt;body&amp;amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;4444444444444444&amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
      首推使用vscode进行学习开发前端，轻量的使用s法的支持自然是相当不行的，而且没有针对JS，html等的格式化，如果有代码洁癖者还是建议使用vscode；后者是最重型的前端集成开发环境，功能非常强大，补全，格式化，自动索引都是吊打vscode，当然代价就是体积大，启动慢。
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
      首推使用vscode进行学习开发前端，轻量的同时有着良好对JS的语法支持，还有多种插件可以自行安装，高效的进行代码的编写。备选项是使用sublime或者是webstorm，前者是类似于笔记本的超轻量编辑器，启动速度快，当然对于语法的支持自然是相当不行的，而且没有针对JS，html等的格式化，如果有代码洁癖者还是建议使用vscode；后者是最重型的前端集成开发环境，功能非常强大，补全，格式化，自动索引都是吊打vscode，当然代价就是体积大，启动慢。
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;amp;lt;/body&amp;amp;gt;
&amp;amp;lt;/html&amp;amp;gt;
</code></pre>
<ul>
<li>margin负值</li>
</ul>
<p>首先简单解释一下margin负值带来的效果，对于margin-top和left采用负值的情况，是自己本身朝着那个方向移动过去，比如margin-top：-50px的话，就是朝着上面相邻元素的外边距移动50px，margin-left同理。然后对于margin-bottom和right则是本身元素右边（下边）的border向左（上）移动。举个例子就是，现在两个元素并排，左边元素设置margin-right：-50px，那么右边的元素就会向左移动50px，因为左边元素的border向左移动了，右边的元素自然是紧邻它的border的，于是也就跟着向左移动50px。</p>
<p>了解了margin负值之后，我们来看以下步骤，首先是开始的时候，三个黑色框表示内容块区域，红色边框表示父容器：</p>
<p><img src="https://godlanbo.com/images/1611916216222.jpg" alt="image20210129180131391.png"></p>
<p>然后我们给里面每一个盒子加一个同样大小的padding-bottom（青色区域），此时父容器被撑开：</p>
<p><img src="https://godlanbo.com/images/1611916226238.jpg" alt="image20210129180341999.png"></p>
<p>然后设置一个和上一步中同样大小的margin-bottom的负值，此时由于负值的影响，每一个内容块的border都向上移动和padding-bottom一样数值的距离，根据包含块的定义，现在父容器的底部其实也向上移动了相同的距离：</p>
<p><img src="https://godlanbo.com/images/1611916235615.jpg" alt="image20210129181716907.png"></p>
<p>然后我们在再父容器上加上overflow：hidden，把溢出的部分隐藏掉，就可以达到多列等高的效果，将padding-bottom和margin-bottom的绝对值设置为很大（比如10000px）还可以做到假的自适应，只要最高列的元素与最低列的元素差值不超过这个，就一直可以达到这个效果。</p>
<p>由于背景图片和背景颜色都可以覆盖padding区域和content区域，所以第二种方法也能解决大多数的情况，但是缺点就是设置边框，如果给内部元素设置下边框，是看不见的，因为border区域已经顶到外面去了。所以如果需要使用边框的话，还是建议使用flex布局。</p>
<h2>写在最后</h2>
<p>当我第一次遇到这个场景的时候，我是不知道flex的align-items有stretch这个属性的（flex布局用了快一年了都不知道，捂脸），当时是使用的第二种方法去尝试，但是我当时是需要设置border的，还正好就是border-bottom，弄了很久，后来才知道align-items的stretch属性，一行代码就搞定了。。正好就在这里记录一下，同样可以实现的方法还有table布局和grid布局，这两个编者不熟悉，就不过多介绍了。</p>
<p>希望能够帮到你，我们下篇文章见。</p>

        ]]></description>
      </item><item>
        <title>css实现自适应方形图片展示</title>
        <link>http://godlanbo.com/blogs/26</link>
        <guid>26</guid>
        <pubDate>Fri, 29 Jan 2021 07:53:48 GMT</pubDate>
        <description><![CDATA[
          <h2>前言</h2>
<p>最近做项目的时候遇到一个场景，需要在一个宽度未定的容器下面摆放三张方形的图片。我们都知道，如果需要图片显示为正方形的话，一般需要已知确定的宽高数值，然后去限定图片包裹块的大小，然后设置图片宽高100%去撑满这个图片包裹块。但是在这个场景下，未知宽度的容器，就代表了我们不知道图片需要展示的确切宽度数值，只知道它相对于父容器应该是1/3的宽度比例，那么这就代表我们无法给予图片包裹块高度等同于宽度的确切数值去让图片变成方形的。</p>
<pre><code class="language-html">&amp;amp;lt;!DOCTYPE html&amp;amp;gt;
&amp;amp;lt;html lang=&amp;quot;en&amp;quot;&amp;amp;gt;
&amp;amp;lt;head&amp;amp;gt;
  &amp;amp;lt;title&amp;amp;gt;Document&amp;amp;lt;/title&amp;amp;gt;
  &amp;amp;lt;style&amp;amp;gt;
    /* 未知宽度容器 */
    .container {
      display: flex;
      width: 100%;
      background-color: #ccc;
    }
    .img-wrapper {
      width: 33%;
      /* 这里无法设置与宽度大小相同的值的高度 */
    }
    .img-wrapper img {
      height: 100%;
      width: 100%;
      object-fit: cover;
    }
  &amp;amp;lt;/style&amp;amp;gt;
&amp;amp;lt;/head&amp;amp;gt;
&amp;amp;lt;body&amp;amp;gt;
  &amp;lt;div&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;img src=&amp;quot;./demo.png&amp;quot; alt=&amp;quot;demo&amp;quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;img src=&amp;quot;./demo.png&amp;quot; alt=&amp;quot;demo&amp;quot;&amp;gt;
    &amp;lt;/div&amp;gt;
    &amp;lt;div&amp;gt;
      &amp;lt;img src=&amp;quot;./demo.png&amp;quot; alt=&amp;quot;demo&amp;quot;&amp;gt;
    &amp;lt;/div&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;amp;lt;/body&amp;amp;gt;
&amp;amp;lt;/html&amp;amp;gt;
</code></pre>
<h2>在高度中拿到宽度</h2>
<p>这个场景的难点在于，宽度不是固定的像素，是未知的，但是我们又需要在高度上设置和宽度相同的值才能使得图片变成方形；也就是说我们需要在高度上拿到CSS自己随时算出来的宽度值。</p>
<p>这个时候我想到一个很少使用的情况——margin/padding的百分比值。</p>
<p>我想大多数时候，大家使用margin/padding的时候，都是设置固定的像素值而非使用百分比的值，我们都知道在CSS中百分比值都是一个相对的值（比如width：100%是取父元素的宽度的100%），那么margin/padding的百分比值是取什么的相对值呢？没错，就是取的父元素的宽度值。</p>
<p>可能有人会有疑问，啊，你说padding/margin left/right这种横向的值取宽度的相对值还有的说，为什么纵向的margin/padding top/bottom是取宽度的相对值而不是取高度的相对值呢？</p>
<p>这个其实在CSS权威指南中有说明，如果取高度的相对值，可能会引起死循环：</p>
<p>&amp;gt; 我们认为，正常流中的大多数元素都会足够高以包含其后代元素（包括外边距），如果一个元素的上下外边距是父元素的height的百分数，就可能导致一个无限循环，父元素的height会增加，以适应后代元素上下外边距的增加，而相应的，上下外边距因为父元素height的增加也会增加，形成无限循环。</p>
<p>利用这个我们就可以在高度上拿到和宽度一样的值了。</p>
<h2>绝对定位元素的百分比高度</h2>
<p>这个时候，我们已经可以利用margin/padding top/bottom来获取相同的宽度值了，但是，此时图片包裹块的大小应该是 1/3的父元素宽度，然后自己有个高度，在加上我们设置的 margin/padding top/bottom（值为1/3的父元素宽度，也就是等于现在自己的宽度）。很明显，我们要让他变成正方形，只需要将高度设置为0即可。</p>
<pre><code class="language-css">.img-wrapper {
  width: 33%;
  padding-bottom: 33%;
  height: 0;
}
</code></pre>
<p>至于为什么这里使用padding而不使用margin，马上你就知道了，至于是bottom还是top，这个影响不大。</p>
<p>好，此时图片的包裹块已经是完美的正方形，但是这里出现一个问题，包裹块的高度设置为0之后，内容块的高度就为0了，图片撑满包裹块的height：100%是相对父元素内容块的高度，此时也就为0了，图片就显示不出来了。</p>
<p>那么现在应该想办法让图片元素height：100%取到包裹块的内容块+内边距的高度，这个时候绝对定位元素的特性就起作用了。</p>
<p>当元素绝对定位的时候，height百分比值是相对于离他最近的具有定位特性（position不是static）的祖先元素的padding box进行计算，也就是计算的时候还需要将padding值加进来一起计算，而不是只计算内容块的值。</p>
<p>利用这个特性，我们将代码做出如下修改：</p>
<pre><code class="language-css">.img-wrapper {
  position: relative;
  width: 33%;
  padding-bottom: 33%;
  height: 0;
}
.img-wrapper img {
  position: absolute;
  height: 100%;
  width: 100%;
  object-fit: cover;
}
</code></pre>
<p>然后我们就可以看到图片如你想的一样变成正方形了，完美！这里也就说明了我们上面为什么不使用margin而使用padding将包裹块撑开成正方形了。至于是选择使用padding-bottom还是padding-top，只是包裹块的内容块位置不同而已，如果使用padding-top的话，需要在img中添加top：0重新定位一下。</p>
<h2>最后</h2>
<p>利用这个场景，我知道了padding/margin设置百分比值的特殊性和实际的应用场景，而且还了解到绝对定位元素和普通元素计算height百分比值的不同，可以算是收获满满了。这篇文章作为我学习的笔记的同时也希望能帮到你理解更多的CSS特性。我是Godlanbo，我们下篇文章见。</p>

        ]]></description>
      </item><item>
        <title>🚀 tailwindcss在Vue中的简单上手</title>
        <link>http://godlanbo.com/blogs/25</link>
        <guid>25</guid>
        <pubDate>Sun, 10 Jan 2021 14:11:48 GMT</pubDate>
        <description><![CDATA[
          <p>对于前端工程师来说，每天在写页面的同时接触最多的除了JavaScript就是CSS了，为了实现一些常见或者不常见的样式或者效果，我们每天都会编写许多CSS代码，有时候你会发现你似乎写了不少相似或者重复的规则，比如圆角啊，定位啊等等，但是当你想把他们组合起来的时候，它们在不同的地方表现出的不一致又让你难以下手。</p>
<h2>介绍</h2>
<p>今天带来一个非常热门的CSS工具库来帮我们解决这个问题——tailwindcss。它不同于OOCSS和BEM的那种追求模块化CSS的理念，它的理念是功能至上（Utility-First）。它提供了一个极其庞大的样式类声明，让我们在编写CSS的时候可以不用写那么多行style，而只需要在class中添加它提供好的样式声明即可。</p>
<p>比如说我们现在要实现一个上下左右居中的功能，以往会这样写：</p>
<pre><code class="language-css"> .flex-center {
   display: flex;
   align-items: center;
   justify-content: center;
 }
</code></pre>
<p>用了tailwindcss之后就会变成这样：</p>
<pre><code class="language-html"> &amp;amp;lt;element class=&amp;quot;flex items-center justify-center&amp;quot;&amp;amp;gt;&amp;amp;lt;/element&amp;amp;gt;
</code></pre>
<p>这样只需要在html中添加类样式声明，就可以获得想要的样式。</p>
<p>你可能会说这样失去了模块化抽象的能力，而且当需要的样式变多的时候，html中的class就会变得又臭又长，让人难以忍受；好在tailwindcss给我们提供了<code>@apply</code>指令，让我们同样可以根据自己的需要，在CSS中使用它提供的样式声明抽象功能模块：</p>
<pre><code class="language-css"> .flex-center {
   @apply flex items-center justify-center;
 }
 .btn {
   @apply px-5 bg-blue-500 text-white;
 }
// ...
</code></pre>
<p>tailwindcss不同于bootstrap，bootstrap会提供给你一组已经预设好的样式声明，你给元素加上去，就能获得对应的效果，但是如果你想在这个基础上修改，就需要自己写CSS去覆盖对应的样式。tailwindcss相反，它不会提供给你任何打包好的样式，而是更加零散的提供给你大量的基础样式声明，让你能够轻松自由的组合和扩展。更多关于tailwindcss请查看它们的<a href="https://tailwindcss.com/">官网</a>。</p>
<h2>在项目中使用</h2>
<p>项目技术栈为Vue，以使用vue-cli创建的vue2.x项目为例。</p>
<h3>安装</h3>
<p>tailwindcss最新版本为2.0.2（截止编写时间），使用它要依赖于PostCss8，vue-cli内置的PostCss只有7，开始我以为可以自己通过安装PostCss和postcss-loader来升级，最后发现vue-cli依赖的postcss7内置在它的cli-server包内，外部安装的postcss8无法被读取到，我暂不知道如何将vue-cli内置的postcss升级到8版本，所以只能依照官网的教程，安装兼容版：</p>
<pre><code class="language-shell"> npm install tailwindcss@npm:@tailwindcss/postcss7-compat @tailwindcss/postcss7-compat
</code></pre>
<p>官网教程还会安装postcss7和autoprefixer，这两个东西vue-cli已经内置了，所以不用安装。不用担心的是，官网上有说兼容版有全部的功能和模块，不用担心因为兼容”缺斤少两“。</p>
<h3>配置</h3>
<p>官方建议将tailwindcss配置为postcss插件进行使用，在vue中配置postcss插件有两种方法：</p>
<ul>
<li>在vue.config.js文件中配置</li>
</ul>
<pre><code class="language-js">  module.exports = {
    css: {
      loaderOptions: {
        postcss: {
          plugins: [require('tailwindcss'), require('autoprefixer')]
        }
      }
    }
  }
</code></pre>
<ul>
<li>创建独立的postcss.config.js文件配置</li>
</ul>
<pre><code class="language-js">  // postcss.config.js
  module.exports = {
    plugins: {
      tailwindcss: {},
      autoprefixer: {}
    }
  }
</code></pre>
<p>两种方式任选其一即可，但是需要注意的是，vue项目默认是配置了autoprefixer的，但是这里在引入tailwindcss的时候也需要将autoprefixer加入到postcss配置中去，因为你的插件配置会覆盖vue-cli的默认配置，如果你只配置tailwindcss的话，autoprefixer就会失效从而失去css前缀补全的功能。</p>
<p>然后运行<code>npx tailwindcss init</code>命令，得到一个最简版的tailwindcss的配置文件：</p>
<pre><code class="language-js">  // tailwind.config.js
  module.exports = {
    purge: [],
    darkMode: false, // or 'media' or 'class'
    theme: {
      extend: {},
    },
    variants: {},
    plugins: [],
  }
</code></pre>
<p>在这里你可以定制、扩展和修改tailwindcss默认提供的样式声明来让你的开发更加高效和个性化，详情请看<a href="https://tailwindcss.com/docs/configuration">定制化</a></p>
<h3>在编辑器中使用</h3>
<p>这里我推荐使用vscode，上面有专门针对tailwindcss补全的插件：</p>
<p><img src="https://godlanbo.com/images/1610287840212.jpg" alt="image20210110212340930.png"></p>
<p>结合tailwindcss的开发体验非常nice。不过需要让它在实际开发中能够帮助我们提示和补全，还需要进行一些相关vscode的配置。</p>
<ul>
<li>开启string检查：vscode中，当你在编辑字符串的时候，默认是不会有补全的，想要tailwindcss辅助插件在class编辑的时候提供补全的话就需要开启这个功能，让vscode在你编写字符串的也检查补全。</li>
</ul>
<pre><code class="language-json">    // setting.json
    {
+     &amp;quot;editor.quickSuggestions&amp;quot;: {
+       &amp;quot;strings&amp;quot;: true
+     }
    }
</code></pre>
<ul>
<li>关闭vscode自带的关于css，scss，less等的代码检查：因为tailwindcss的语法有别于正常的样式代码，直接写上去的vscode会报警告，或者错误，看着很不舒服；这个时候我们需要关闭它。</li>
</ul>
<pre><code class="language-json">    // setting.json
    {
      &amp;quot;editor.quickSuggestions&amp;quot;: {
        &amp;quot;strings&amp;quot;: true
      },
+     &amp;quot;css.validate&amp;quot;: false,
+     &amp;quot;scss.validate&amp;quot;: false,
+     &amp;quot;less.validate&amp;quot;: false
    }
</code></pre>
<ul>
<li>关闭vue对于文件内样式的检测：我们开发vue一般都会预装vetur插件，这个插件对vue文件内部编写样式有自己的检测，我们同样将其关闭，避免不认识tailwindcss语法报错。</li>
</ul>
<pre><code class="language-json">    // setting.json
    {
      &amp;quot;editor.quickSuggestions&amp;quot;: {
        &amp;quot;strings&amp;quot;: true
      },
      &amp;quot;css.validate&amp;quot;: false,
      &amp;quot;scss.validate&amp;quot;: false,
      &amp;quot;less.validate&amp;quot;: false,
+     &amp;quot;vetur.validation.style&amp;quot;: false
    }
</code></pre>
<ul>
<li>
<p>配置stylelint自定义检查（可选）：我们关闭了自带的css代码检查之后，虽然tailwindcss语法不会报错了，但是普通的css代码写错了写不会报错了，如果你对自己编写css有信心的话，这一步也可以不做，不过一般还是需要一个最基本的语法检测。</p>
<p>首先安装插件stylelint，在项目目录下创建stylelint.config.js：</p>
<pre><code class="language-js">  module.exports = {
    rules: {
      // 这一项标识需要检测不认识的@规则，但是忽略ignoreAtRules中的项
      // 这样会忽略@apply等，但是当使用scss的时候，@include等会报错，
      // 你可以将其添加到ignoreAtRules中，也可以将这一整项删除不检测
      // 注意：这个规则没有false选项，不开启删除即可
      // 更多stylelint规则配置见 https://stylelint.docschina.org/user-guide/rules/
      'at-rule-no-unknown': [
        true,
        {
          ignoreAtRules: [
            'tailwind',
            'apply',
            'variants',
            'responsive',
            'screen'
          ]
        }
      ],
      'declaration-block-trailing-semicolon': 'always',
      'no-descending-specificity': null
    }
  }
</code></pre>
<p>配置好了之后，插件stylelint就会读取项目下的配置文件，帮我们检测css的语法错误。</p>
</li>
</ul>
<h2>结束</h2>
<p>当我们完成这一系列操作，就可以愉快的使用tailwindcss来编写我们的样式了：</p>
<p><img src="https://godlanbo.com/images/1610287863893.jpg" alt="image20210110215538706.png"></p>
<p>还需要注意的是，当前这个tailwindcss补全插件只有在工作区就是项目根目录的时候才会起效，多根工作区尚且不能获得补全效果。目前我还没有找到如何配置这个插件的工作目录，如果有人找到的话，可以在右下角的留言板中给我留言哦😀；那么好的，我们下次再见。</p>
<h2>参考</h2>
<p><a href="https://tailwindcss.com/docs/configuration">tailwindcss官方文档</a></p>
<p><a href="https://www.xlbd.me/posts/2020-06-01-jamstack-blog-theme.html">VuePress + TailwindCss + Netlify 重写个人独立博客</a></p>

        ]]></description>
      </item><item>
        <title>深入理解浏览器的渲染原理</title>
        <link>http://godlanbo.com/blogs/24</link>
        <guid>24</guid>
        <pubDate>Thu, 24 Dec 2020 07:08:37 GMT</pubDate>
        <description><![CDATA[
          <h2>前言</h2>
<p>页面内容快速加载和流畅的交互是用户希望得到的Web体验，因此，我们前端开发者应力争实现这两个目标。了解浏览器的工作原理有助于我们更好的优化网页性能。本文会深入讲解关于浏览器渲染网页中的各个步骤和细节。</p>
<h2>1、浏览器环境中的JS引擎</h2>
<p>我们拿Chrome举例子，Chrome浏览器内置了解析JavaScript代码的V8引擎，我们每打开一个网页，就是打开了一个浏览器进程，进程内部就可以有很多的线程，JS解析引擎就是其中一个，这也正是众所周知的JavaScript的解析器特点——单线程。也就是说同一时间它只能做一件事，如果当前做的这件事被堵住了，后面的事情就做不了了。</p>
<h2>2、阻塞与异步任务</h2>
<p>在浏览器里面，我们的JS代码的顺利执行非常重要，单线程的JS代码一旦被花费很长时间的任务阻塞，网页就会出大问题，所以在JavaScript中有着异步任务的设计，当你需要完成一些过一会儿才需要结果的任务时，你可以将它添加到异步队列中去，这样它就不会阻塞你的JavaScript代码的后续执行，有关于JavaScript的运行机制，可以看我的<a href="https://godlanbo.com/blogs/20">JS事件循环机制</a>这篇文章。</p>
<h2>3、浏览器解析网页的相关进程</h2>
<p>我们知道浏览器作为一个进程，其中运行着很多线程，什么管插件的啊，管UI的啊，管用户数据、书签的等等，我们不需要考虑那么多其它额外的线程，在渲染网页的过程中，比较核心的是以下几个：</p>
<ul>
<li>
<p>浏览器GUI渲染线程</p>
<p>这个线程的主要功能是解析HTML文档，解析CSS文档，构建DOM树和CSSOM树，也负责调用它们两个合成的Render Tree去渲染内容。它还负责页面的回流（Layout）与重绘（Painting），使像素真正渲染在页面上。</p>
</li>
<li>
<p>JS解析线程</p>
<p>主要负责解析JavaScript脚本，它会一直不断的等待就绪任务队列里新任务的到来；一个浏览器进程里只有一个Js解析线程。</p>
</li>
<li>
<p>网络线程</p>
<p>负责去网络上请求需要的资源，比如外链的JS，CSS文件，还有图片字体资源等。每一个新开的网络请求都会打开一个网络线程。</p>
</li>
</ul>
<p>我们都知道JS的运行是可以改变DOM结构和样式的，为了避免刚渲染完DOM就被JS修改了又要重新渲染这种浪费性能的情况发生，GUI渲染进程在JS解析器执行JavaScript脚本的时候，会被阻塞，直到JS引擎任务队列空闲才会继续解析渲染工作。也就是说<strong>GUI渲染线程</strong>和JS解析线程是<strong>互斥</strong>的。</p>
<h2>4、关键渲染路径</h2>
<p>现在我们来看一下，当浏览器拿到HTML，CSS，JavaScript之后，需要经过哪些步骤，把代码渲染成页面，整个渲染流程如下图：</p>
<p><img src="https://godlanbo.com/images/1608792988458.jpg" alt="浏览器渲染原理"></p>
<h3>4.1、构建DOM树</h3>
<p>从服务器拿到返回的HTML文件，浏览器就开始解析HTML代码，并将其转化成DOM树，DOM树上的每一个节点都保存着我们需要的信息。</p>
<h3>4.2、构建CSSOM树</h3>
<p>解析CSS样式文件，构建生成CSSOM树，这上面的节点里保存了对应节点解析CSS规则后最后计算出来的最终样式信息，也就是Computed Style。</p>
<h3>4.3、Render Tree合成</h3>
<p>当我们得到DOM树和CSSOM树之后，把他们结合就能得到渲染树。</p>
<p><img src="https://godlanbo.com/images/1608793017329.jpg" alt="生成渲染树"></p>
<h3>4.4、回流（layout）</h3>
<p>一旦渲染树被构建，布局变成了可能。布局取决于屏幕的尺寸。布局这个步骤决定了在哪里和如何在页面上放置元素，决定了每个元素的宽和高，以及他们之间的相关性，于是根据渲染树，进行回流（layout），得到节点在屏幕上的几何信息。</p>
<h3>4.5、重绘（painting）</h3>
<p>最后一步是将像素绘制在屏幕上。一旦渲染树创建并且布局完成，像素就可以被绘制在屏幕上。</p>
<h2>5、解析渲染中的阻塞</h2>
<p>在正常的网页中，上面的关键渲染路径一般不太可能直接顺利的一次执行完毕，因为在上面的过程中，我们还有JS没有考虑进去，前面说了，JS解析线程会与GUI渲染线程互斥，当解析JS的时候，关键渲染路径就会被停下。所以，接下来我就讲解关于渲染中的阻塞问题。</p>
<h3>5.1、JS执行阻塞DOM构建渲染</h3>
<p>由于浏览器会对JS的执行做最坏的打算，于是当我们JS解析线程工作时，GUI渲染线程就会被阻塞，等待JS执行完毕再继续进行；前面说到了HTLM的解析也是GUI渲染线程的工作，这就意味着当JS执行的时候，浏览器将暂停HTML的解析与DOM树的构建。</p>
<p>这也是为什么我们常常建议把JS放在body底部，其中一个原因就是为了不阻塞DOM树的构建，这也是为什么放在body上面的JS常常取不到想要的DOM节点，因为这个时候DOM树的构建还没有完成就被阻塞了。</p>
<p>但是把JS直接放在底部也是会有一些问题的，放在底部也就表明最后被解析到，最后被加载，如果你的页面依赖JS来渲染一些内容，会导致页面的完整渲染被延后，我后面preload那里会讲到如何解决这个问题。</p>
<h3>5.2、JS加载阻塞全部</h3>
<p>不同于加载外部的CSS文件，加载外部的CSS文件并不会阻塞HTML的解析和DOM树的构建（加载文件交给网络线程），顶多延缓首次渲染（拿不到CSS就不会进行渲染），但是加载外部的JS文件不一样，浏览器不清楚这里面的代码是否会修改已经构建的DOM节点，于是会做出最坏的打算，等待JS文件加载完毕-&amp;gt;运行完毕，才会继续解析HTML。</p>
<h3>5.3、CSS阻塞DOM树渲染</h3>
<p>在4中我们学习到关键渲染路径中CSSOM的构建是非常关键的一步，也就是说当CSSOM构建没有完成的时候，即使DOM树已经构建完毕，浏览器也不会进行DOM树的渲染，但是从4中的图可以看出，CSS和HTML的解析构建是可以并行的，所以CSS的解析构建不会阻塞DOM树的构建，只是如5.2中所说的那样，会延缓DOM树的首次渲染。</p>
<p>所以为了尽可能快点让页面进行渲染，我们应该尽快提供所需要的CSS。</p>
<h3>5.4、CSS解析构建阻塞JS执行</h3>
<p>我们都知道JavaScript是非常强大的，它们不仅可以读取和修改 DOM 属性，还可以读取和修改 CSSOM 属性。那么当CSS构建CSSOM的同时又出现了JS去修改CSSOM的情况呢？显然，我们现在遇到了竞态问题。</p>
<p>如果浏览器尚未完成 CSSOM 的下载和构建，而我们却想在此时运行脚本，会怎样？答案很简单，对性能不利：<strong>浏览器将延迟脚本执行，直至其完成 CSSOM 的下载和构建。</strong></p>
<p>我举一个简单的例子，看下面这段代码：</p>
<pre><code class="language-html">&amp;amp;lt;!DOCTYPE html&amp;amp;gt;
&amp;amp;lt;html&amp;amp;gt;
&amp;amp;lt;head&amp;amp;gt;
  &amp;amp;lt;title&amp;amp;gt;test&amp;amp;lt;/title&amp;amp;gt;
  &amp;amp;lt;script&amp;amp;gt;
    let startDate = new Date()
    document.addEventListener('DOMContentLoaded', function() {
      console.log('DOMContentLoaded');
    })
  &amp;amp;lt;/script&amp;amp;gt;
  &amp;amp;lt;link href=&amp;quot;https://cdn.bootcss.com/bootstrap/4.0.0-alpha.6/css/bootstrap.css&amp;quot; rel=&amp;quot;stylesheet&amp;quot;&amp;amp;gt;
  &amp;amp;lt;script&amp;amp;gt;
    let endDate = new Date()
    console.log('run time:' + (endDate -startDate) + 'ms')
  &amp;amp;lt;/script&amp;amp;gt;
&amp;amp;lt;/head&amp;amp;gt;
&amp;amp;lt;body&amp;amp;gt;
  &amp;lt;h1&amp;gt;test&amp;lt;/h1&amp;gt;
&amp;amp;lt;/body&amp;amp;gt;
&amp;amp;lt;/html&amp;amp;gt;
</code></pre>
<p>我在这里补充一个知识，在浏览器渲染页面的过程中，会触发两个个事件，<code>DOMContentLoaded</code>，它代表着DOM树已经构建完成，另一个是，<code>onLoad</code>，它代表页面已经全部加载完毕，包括css，js，图片等。</p>
<p>我们在浏览器上运行这个网页（记得开启不使用缓存），结果如下：</p>
<p><img src="https://godlanbo.com/images/1608793057328.jpg" alt="image20201224133311190.png"></p>
<p><img src="https://godlanbo.com/images/1608793067653.jpg" alt="image20201224133325624.png"></p>
<p>可以看到，位于css资源其后的JS脚本，等到css资源加载完毕之后JS才恢复执行，JS的执行又会阻塞DOM树的构建，所以等到JS执行完毕之后，DOM树才构建完成，触发<code>DOMContentLoaded</code>事件。</p>
<h2>6、preload 预加载</h2>
<p>在上面我介绍了页面加载资源的时候，关于浏览器出于性能考虑作出的一些顺序的调整（利用阻塞，来达到一定顺序执行的目的）。但是有些时候，有些资源就是我们网页即刻需要的，考虑下面这个例子：</p>
<pre><code class="language-html">&amp;amp;lt;!DOCTYPE html&amp;amp;gt;
&amp;amp;lt;html&amp;amp;gt;
&amp;amp;lt;head&amp;amp;gt;
  &amp;amp;lt;script src=&amp;quot;/js/index.js&amp;quot;&amp;amp;gt;&amp;amp;lt;/script&amp;amp;gt;
  &amp;amp;lt;script src=&amp;quot;/js/menu.js&amp;quot;&amp;amp;gt;&amp;amp;lt;/script&amp;amp;gt;
  &amp;amp;lt;script src=&amp;quot;/js/main.js&amp;quot;&amp;amp;gt;&amp;amp;lt;/script&amp;amp;gt;
&amp;amp;lt;/head&amp;amp;gt;
&amp;amp;lt;body&amp;amp;gt;
  &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
&amp;amp;lt;/body&amp;amp;gt;
&amp;amp;lt;/html&amp;amp;gt;
</code></pre>
<p>当我们解析到 index.js d的时候，我们会去加载它，这是5.2的情况，JS的加载会停止HTML的解析和DOM的构建，于是这个时候浏览器需要等待index.js 加载执行完毕之后，才能继续往后面解析。我们现在来分析一下，当JS加载完毕开始执行后，三个主要线程的情况：</p>
<ul>
<li>GUI：被阻塞</li>
<li>JS解析线程：刚刚加载过来的代码正在被执行</li>
<li>网络线程：空闲</li>
</ul>
<p>很明显在解析执行JS的时候，网络是空闲的，这就有了发挥的空间：我们能不能一边解析执行 js/css，一边去请求下一个(或下一批)资源呢？</p>
<p>对于这种情况，浏览器提供了一个预加载的机制，preload，引用MDN的解释：</p>
<p>&amp;gt; link元素的 <code>rel</code> 属性的属性值<code>preload</code>能够让你在你的HTML页面中 head元素内部书写一些声明式的资源获取请求，可以指明哪些资源是在页面加载完成后即刻需要的。对于这种即刻需要的资源，你可能希望在页面加载的生命周期的早期阶段就开始获取，在浏览器的主渲染机制介入前就进行预加载。这一机制使得资源可以更早的得到加载并可用，且更不易阻塞页面的初步渲染，进而提升性能。</p>
<p>我们把上面的代码改成这样：</p>
<pre><code class="language-html">&amp;amp;lt;!DOCTYPE html&amp;amp;gt;
&amp;amp;lt;html&amp;amp;gt;
&amp;amp;lt;head&amp;amp;gt;
  &amp;amp;lt;script src=&amp;quot;/js/index.js&amp;quot;&amp;amp;gt;&amp;amp;lt;/script&amp;amp;gt;
  &amp;amp;lt;link src=&amp;quot;/js/menu.js&amp;quot; rel=preload as=script&amp;amp;gt;&amp;amp;lt;/link&amp;amp;gt;
  &amp;amp;lt;link src=&amp;quot;/js/main.js&amp;quot; rel=preload as=script&amp;amp;gt;&amp;amp;lt;/link&amp;amp;gt;
&amp;amp;lt;/head&amp;amp;gt;
&amp;amp;lt;body&amp;amp;gt;
  &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
&amp;amp;lt;/body&amp;amp;gt;
&amp;amp;lt;/html&amp;amp;gt;
</code></pre>
<p>现在页面就可以在解析index.js的同时，继续加载后面两个JS资源，而且这种途径加载JS是不会执行的，也就是说不会阻塞HTML的解析和DOM树的构建，它会把加载的资源放到内存中，然后我们配合5.1中提到的方法，把JS执行放在body底部：</p>
<pre><code class="language-html">&amp;amp;lt;!DOCTYPE html&amp;amp;gt;
&amp;amp;lt;html&amp;amp;gt;
&amp;amp;lt;head&amp;amp;gt;
  &amp;amp;lt;script src=&amp;quot;/js/index.js&amp;quot;&amp;amp;gt;&amp;amp;lt;/script&amp;amp;gt;
  &amp;amp;lt;link src=&amp;quot;/js/menu.js&amp;quot; rel=preload as=script&amp;amp;gt;&amp;amp;lt;/link&amp;amp;gt;
  &amp;amp;lt;link src=&amp;quot;/js/main.js&amp;quot; rel=preload as=script&amp;amp;gt;&amp;amp;lt;/link&amp;amp;gt;
&amp;amp;lt;/head&amp;amp;gt;
&amp;amp;lt;body&amp;amp;gt;
  &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
  &amp;amp;lt;script src=&amp;quot;/js/menu.js&amp;quot;&amp;amp;gt;&amp;amp;lt;/script&amp;amp;gt;
  &amp;amp;lt;script src=&amp;quot;/js/main.js&amp;quot;&amp;amp;gt;&amp;amp;lt;/script&amp;amp;gt;
&amp;amp;lt;/body&amp;amp;gt;
&amp;amp;lt;/html&amp;amp;gt;
</code></pre>
<p>这样当DOM树构建完成，解析到底部的JS时，命中早已经加载好的缓存内容，直接就可以开始执行，不用等待JS的加载，这样就可以有效解决在5.1中提到的把JS放在body底部的问题。</p>
<h2>7、prefetch 资源预取</h2>
<p>浏览器中还为<code>link</code>标签的<code>rel</code>属性提供了一个值，叫做<code>prefetch</code>。还是取来自MDN上的解释：</p>
<p>&amp;gt; 预取是一种浏览器机制，其利用浏览器空闲时间来下载或预取用户在不久的将来可能访问的文档。网页向浏览器提供一组预取提示，并在浏览器完成当前页面的加载后开始静默地拉取指定的文档并将其存储在缓存中。当用户访问其中一个预取文档时，便可以快速的从浏览器缓存中得到。</p>
<p>我举一个简单的实际场景作为例子来说明——图片懒加载。</p>
<p>当我们浏览网页的时候，没有进入我们视窗的图片往往会做懒加载，以减小首次加载的压力。当我们的页面加载完成之后，还未向下滚动，这个时候其实浏览器就已经是空闲了。正常来说，我们需要等到下面的图片进入我们的视野才开始加载，但是其实我们可以等到页面加载完空闲的时候，就去加载下面的图片。这样当用户滚动下来的时候，懒加载的图片很快就可以从缓存中读出来，大大提高体验。</p>
<p>这就是资源预取，它并不会占用首次加载时的网络资源，也就是如MDN中所说，在浏览器空闲的时候用来加载一些用户将来可能会用到的资源。所以资源预取不会影响首屏渲染，它触发的时机是在<code>onLoad</code>事件触发前一点。</p>
<h2>8、预加载扫描器</h2>
<p>大家看了上面的所有流程之后，会不会对preload产生一些疑问，preload的提前加载条件是建立在浏览器知道这个link资源是有preload属性的，也就是说，这一行被解析过了。</p>
<p>但是我们在5.2中提到，js的加载会阻塞HTML的解析。让我们回到preload那一节中的例子：</p>
<pre><code class="language-html">&amp;amp;lt;!DOCTYPE html&amp;amp;gt;
&amp;amp;lt;html&amp;amp;gt;
&amp;amp;lt;head&amp;amp;gt;
  &amp;amp;lt;script src=&amp;quot;/js/index.js&amp;quot;&amp;amp;gt;&amp;amp;lt;/script&amp;amp;gt;
  &amp;amp;lt;link src=&amp;quot;/js/menu.js&amp;quot; rel=preload as=script&amp;amp;gt;&amp;amp;lt;/link&amp;amp;gt;
  &amp;amp;lt;link src=&amp;quot;/js/main.js&amp;quot; rel=preload as=script&amp;amp;gt;&amp;amp;lt;/link&amp;amp;gt;
&amp;amp;lt;/head&amp;amp;gt;
&amp;amp;lt;body&amp;amp;gt;
  &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
&amp;amp;lt;/body&amp;amp;gt;
&amp;amp;lt;/html&amp;amp;gt;
</code></pre>
<p>我在preload那一节说在解析index.js的时候，后面的preload资源可以被加载，但是考虑到JS解析阻塞HTML解析，浏览器应该不知道后面的link标签是什么情况才对。</p>
<p>在实际的网页中我们打开network观察资源加载情况也可以发现，JS的加载后面的一些资源加载，也是可以被浏览器解析到同时发出请求的，这似乎和我们在5.2中说的有一些矛盾。</p>
<p>但是其实这是浏览器自身的优化机制，并不是如我们想的那样，html文件一被浏览器从服务器下载下来就开始解析的：</p>
<p><img src="https://godlanbo.com/images/1608793085602.jpg" alt="image20201224143513666.png"></p>
<p>可以从时间线上明显看到，从html文件被下载，到第一个资源开始加载，中间还经过了一段时间，这段时间就是<strong>预加载扫描器</strong>造成的。</p>
<p>引用MDN的解释：</p>
<p>&amp;gt; 浏览器构建DOM树时，这个过程占用了主线程。当这种情况发生时，预加载扫描仪将解析可用的内容并请求高优先级资源，如CSS、JavaScript和web字体。多亏了预加载扫描器，我们不必等到解析器找到对外部资源的引用来请求它。它将在后台检索资源，以便在主HTML解析器到达请求的资源时，它们可能已经在运行，或者已经被下载。预加载扫描仪提供的优化减少了阻塞。</p>
<p>知道了这个我们就能很好的理解为什么JS的加载会阻塞HTML的解析，但其后的资源请求还是可以发出去了，预加载器帮我们提前扫描了外部资源的请求，所以上面那个例子index.js后面的preload早就被预加载器解析了，当然可以正常的提前加载。</p>
<h2>结语</h2>
<p>本文是我多方面总结浏览器渲染原理之后集合出来的，网上大概很多文章讲述各种方面，但是没有统一在一起讲的，而且很少有讲到预加载器这个东西的文章，中间很长一段时间导致我不能理解为什么浏览器可以提前解析到位于JS加载后面的外部资源。希望这篇文章能帮到你提高网页性能。</p>
<h2>参考</h2>
<p><a href="https://juejin.cn/post/6844903667733118983">https://juejin.cn/post/6844903667733118983</a></p>
<p><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML/Preloading_content">https://developer.mozilla.org/zh-CN/docs/Web/HTML/Preloading_content</a></p>
<p><a href="https://developer.mozilla.org/zh-CN/docs/Web/Performance/%E6%B5%8F%E8%A7%88%E5%99%A8%E6%B8%B2%E6%9F%93%E9%A1%B5%E9%9D%A2%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86">https://developer.mozilla.org/zh-CN/docs/Web/Performce/浏览器渲染页面工作原理</a></p>
<p><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Link_prefetching_FAQ">https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Link_prefetching_FAQ</a></p>
<p><a href="https://developers.google.com/web/fundamentals/performance/critical-rendering-path/adding-interactivity-with-javascript?hl=zh-cn">https://developers.google.com/web/fundamentals/performance/critical-rendering-path/adding-interactivity-with-javascript?hl=zh-cn</a></p>

        ]]></description>
      </item><item>
        <title>本站后续维护计划</title>
        <link>http://godlanbo.com/blogs/23</link>
        <guid>23</guid>
        <pubDate>Mon, 21 Dec 2020 06:19:31 GMT</pubDate>
        <description><![CDATA[
          <p>本网站建成以来也有快3个月了，大大小小提交了百来个commits，比起一开始来说，已经加了不少功能，也修复了不少bug。然后这里写下来以后可能会做还有我想做的功能计划，一方面方便查看，二来也是记录一下维护的历史。</p>
<p><img src="https://godlanbo.com/images/1608531280699.jpg" alt="image20201221140318295.png"></p>
<h2>未来想做计划列表</h2>
<ul>
<li>[x] Rss订阅</li>
<li>[x] 邮件订阅功能</li>
<li>[ ] 全站搜索</li>
<li>[ ] 订阅功能后台管理</li>
<li>[ ] 评论记忆（至少想不要每次都输入那么多信息）</li>
<li>[ ] 意见箱</li>
<li>[ ] footer Bar（我快看不下去我那简陋的设计了=。=|||）</li>
<li>[ ] 移动端体验和样式优化（一直进行中）</li>
<li>[ ] 网站更新历史</li>
<li>[ ] about me</li>
<li>[ ] life 页面想做的更好一点</li>
</ul>
<p>暂时就想到这么多，如果我想到新的，我都会加在上面的列表后面。</p>
<p>希望未来有一天可以给上面列表全部打上 √</p>

        ]]></description>
      </item><item>
        <title>开启gzip压缩，让你的资源下载更快</title>
        <link>http://godlanbo.com/blogs/22</link>
        <guid>22</guid>
        <pubDate>Fri, 18 Dec 2020 09:11:22 GMT</pubDate>
        <description><![CDATA[
          <p>对于大部分使用云服务器搭建自己网站的学生来说，大部分都不会拥有很快的服务器下载速度，这样当网站资源文件稍微大一点之后，就会导致访问网站加载很长时间。下面详解如何开启gzip和其相关的配置。</p>
<h2>开启gzip</h2>
<p>开启gzip压缩有两种方式，两种方式都需要用到nginx。</p>
<h3>一、服务器实时压缩</h3>
<p>在 nginx 中实时用 gzip 压缩文件输出，利用 nginx 中的模块 ngx_http_gzip_module， 消耗服务器的 CPU 性能来做压缩。这种方式的nginx 配置如下，各配置的详细说明见注释：</p>
<pre><code class="language-nginx">http{
  // 开启gzip压缩
  gzip on;
  // 当返回内容大于此值时才会使用gzip进行压缩,以K为单位,当值为0时，所有页面都进行压缩。
  gzip_min_length 1k;
  // 设置用于处理请求压缩的缓冲区数量和大小。比如4 16K表示按照内存页大小以16K为单位，申请4倍的内存空间。
  gzip_buffers 4 16k;
  // 压缩等级，1 - 10，越大压缩效果越好，越消耗CPU性能。
  gzip_comp_level 8;
  // 设置需要压缩的MIME类型,如果不在设置类型范围内的请求不进行压缩
  gzip_types text/plain application/javascript application/x-javascript text/css application/xml text/javascript;
  // 增加响应头”Vary: Accept-Encoding”，这个响应头用于识别浏览器是否支持gzip缓存进而缓存不同的资源副本，避免
  // 出现有gzip能力的浏览器请求命中没有压缩的缓存副本
  gzip_vary on;
  // 通过表达式，表明哪些UA头不使用gzip压缩
  gzip_disable &amp;quot;MSIE [1-6]\.&amp;quot;;
  // 用于识别http协议的版本，早期的浏览器不支持gzip压缩，用户会看到乱码，所以为了支持前期版本加了此选项。默认在http/1.0的协议下不开启gzip压缩。
  gzip_http_version 1.1;
  // Nginx做为反向代理的时候启用
  gzip_proxied any;
}
</code></pre>
<p>对于上面的配置中的<code>gzip_proxied</code>字段补充说明一下相关取值：</p>
<ul>
<li>off – 关闭所有的代理结果数据压缩</li>
<li>expired – 如果header中包含”Expires”头信息，启用压缩</li>
<li>no-cache – 如果header中包含”Cache-Control:no-cache”头信息，启用压缩</li>
<li>no-store – 如果header中包含”Cache-Control:no-store”头信息，启用压缩</li>
<li>private – 如果header中包含”Cache-Control:private”头信息，启用压缩</li>
<li>no_last_modified – 启用压缩，如果header中包含”Last_Modified”头信息，启用压缩</li>
<li>no_etag – 启用压缩，如果header中包含“ETag”头信息，启用压缩</li>
<li>auth – 启用压缩，如果header中包含“Authorization”头信息，启用压缩</li>
<li>any – 无条件压缩所有结果数据</li>
</ul>
<h3>二、使用事先准备好的gzip文件</h3>
<p>事先用 gzip 压缩好文件（.gz）让 nginx 根据请求来自己选择 .gz 文件输出，利用 nginx 中的模块 <code>http_gzip_static_module</code>，不消耗 CPU 资源，nginx配置只需要在上述里面加入一行即可，如下：</p>
<pre><code class="language-nginx">gzip_static on;
</code></pre>
<p>添加这个配置后，nginx会先去寻找 .gz 文件返回给浏览器，如果找不到才会使用自带的gzip模块去压缩。</p>
<p>配置完成后重启 nginx 可能会报错，因为 nginx 默认不携带 <code>http_gzip_static_module</code> 模块。需要在 nginx 的编译参数 <code>configure arguments</code> 中加上 <code>--with-http_gzip_static_module</code> 重新编译。</p>
<p>然后是准备gzip文件，以vue项目为例子，我们需要安装webpack打包插件 <code>compression-webpack-plugin</code>，然后在<code>vue.config.js</code> 中加入：</p>
<pre><code class="language-js">const CompressionPlugin = require('compression-webpack-plugin')

module.exports = {
  configureWebpack: config =&amp;amp;gt; {
    if (process.env.NODE_ENV === 'production') {
      return {
        plugins: [
          new CompressionPlugin({
            // 这里有个坑，虽然写的和默认值一样，但如果不写，你的gz文件会打包到js，css文件夹外面
            // 写了之后才会放在同一个文件夹下面，我也不知道为什么
            filename: &amp;quot;[path][base].gz&amp;quot;,
            algorithm: &amp;quot;gzip&amp;quot;,
            test: /\.(js|css|json|txt|html|ico|svg)(\?.*)?$/i, //匹配文件名
            threshold: 10240, //对10K以上的数据进行压缩
            minRatio: 0.8,
            deleteOriginalAssets: false //是否删除源文件,删除的话不会有js文件，都是gz文件
          })
        ]
      }
    }
  }
}
</code></pre>
<p>对于 <code>compression-webpack-plugin</code> 插件，需要你自己安装一下 <code>webpack</code> ，不然会报错<code>Cannot read property 'tapPromise' of undefined</code>，因为他不能使用<code>vue-cli</code>自带的<code>webpack</code>，建议安装 <code>compression-webpack-plugin 6.0.0</code>版本，因为最新的 7.0.0 版本需要依赖 <code>webpack5</code>，但是如果你安装 <code>webpack5</code>之后会发现根本就打包不了了，因为<code>vue-cli</code>还不支持 <code>webpack5</code>。</p>
<p>所以建议的组合是安装 <code>webpack4.x.x</code>版本加上 <code>compression-webpack-plugin 6.0.0</code>版本，这样才可以正常打包成功。等到<code>vue-cli</code>支持<code>webpack5</code>之后，也可以将<code>webpack</code>升级到5，然后安装<code>compression-webpack-plugin 7.0.0</code>版本。</p>
<h2>最后</h2>
<p>网上的教程虽然繁多，但是坑还得自己踩，知识还是得自己总结。第二种关于<code>compression-webpack-plugin</code>插件那里，我折腾了好久才打包成功，网上往往就是把代码一挂，就打包成功了，一样的代码拿下来一跑，本地老是报错，不得不感叹前端生态更新之快，稍微一些版本的变动，就能让人在兼容上折腾好久。</p>
<h2>参考</h2>
<p><a href="http://blog.itpub.net/31541037/viewspace-2156996/">http://blog.itpub.net/31541037/viewspace-2156996/</a></p>
<p><a href="https://github.com/vuejs/vue-cli/issues/5986">https://github.com/vuejs/vue-cli/issues/5986</a></p>
<p><a href="https://github.com/webpack-contrib/compression-webpack-plugin/issues/229">https://github.com/webpack-contrib/compression-webpack-plugin/issues/229</a></p>

        ]]></description>
      </item><item>
        <title>forEach异步不阻塞，1s输出一个结果</title>
        <link>http://godlanbo.com/blogs/21</link>
        <guid>21</guid>
        <pubDate>Tue, 01 Dec 2020 17:32:49 GMT</pubDate>
        <description><![CDATA[
          <h2>题目</h2>
<p>&amp;gt; 输出以下代码运行结果，为什么？如果希望每隔 1s 输出一个结果，应该如何改造？注意不可改动 square 方法</p>
<pre><code class="language-js">const list = [1, 2, 3]
const square = num =&amp;amp;gt; {
  return new Promise((resolve, reject) =&amp;amp;gt; {
    setTimeout(() =&amp;amp;gt; {
      resolve(num * num)
    }, 1000)
  })
}

function test() {
  list.forEach(async x=&amp;amp;gt; {
    const res = await square(x)
    console.log(res)
  })
}
test()
</code></pre>
<h2>解答</h2>
<p>由于forEach异步不阻塞，所以是1s之后输出全部结果。</p>
<h3>一、为什么forEach异步不阻塞</h3>
<p>对于这里forEach不阻塞的解释，因为forEach是对每个遍历项进行传入的函数处理，对于上面的代码实现相当于下面这种情况：</p>
<pre><code class="language-js">function test() {
  (aysnc function(x){
    const res = await square(x)
    console.log(res)
  })(1)
  (aysnc function(x){
    const res = await square(x)
    console.log(res)
  })(2)
  (aysnc function(x){
    const res = await square(x)
    console.log(res)
  })(3)
}
</code></pre>
<p>对于aysnc函数执行的上下文来说，它们执行的时候和普通函数并无两样，所以这里直接向执行队列里推进三个函数。</p>
<p>由于aysnc和await其实就是Promise的语法糖，我们把这里还原就非常好理解了：</p>
<pre><code class="language-js">function test() {
  (aysnc function(x){
    square(x).then(res =&amp;amp;gt; console.log(res))
  })(1)
  (aysnc function(x){
    square(x).then(res =&amp;amp;gt; console.log(res))
  })(2)
  (aysnc function(x){
    square(x).then(res =&amp;amp;gt; console.log(res))
  })(3)
}
</code></pre>
<p>因为Promise异步的实现，这里不会阻塞主线程的运行，所以当执行队列里的三个函数同步的执行完毕后，相当于往异步等待队列里添加了三个then后面的函数；由于它们几乎被同时添加（因为添加它们的三个函数同步执行完毕），定时器同时启动，1s后均变为可执行状态同时被添加到执行队列，最后主线程空闲，一次性输出它们（因为都已经准备就绪，关于事件循环，可以看看我的<a href="http://godlanbo.com/blogs/20">JS事件循环机制</a>）。</p>
<h3>二、如何1s输出一个结果</h3>
<p>有三种方式：</p>
<ul>
<li>简单的使用for循环即可，for循环是异步阻塞的</li>
</ul>
<pre><code class="language-js">async function test() {
  for(let i = 0; i &amp;amp;lt; list.length; i++) {
    const res = await square(list[i])
    console.log(res)
  }
}
</code></pre>
<ul>
<li>使用ES6新加的for of循环或者使用for in循环也可以</li>
</ul>
<pre><code class="language-js">async function test() {
  for(let x of list) {
    const res = await square(x)
    console.log(res)
  }
}
async function test() {
  for(let i in list) { // for in 遍历出来是数组的索引
    const res = await square(list[i])
    console.log(res)
  }
}
</code></pre>
<ul>
<li>利用Promise自己的链式调用</li>
</ul>
<pre><code class="language-js">let promise = Promise.resolve()
function test(i = 0) {
  if (i === list.length) return
  promise = promise.then(() =&amp;amp;gt; {
    return square(list[i]).then(res =&amp;amp;gt; {
      console.log(res)
    })
  })
  test(i + 1)
}
</code></pre>
<h2>参考</h2>
<p><a href="https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/389">https://github.com/Advanced-Frontend/Daily-Interview-Question/issues/389</a></p>

        ]]></description>
      </item><item>
        <title>JS事件循环之异步任务</title>
        <link>http://godlanbo.com/blogs/20</link>
        <guid>20</guid>
        <pubDate>Tue, 01 Dec 2020 17:26:56 GMT</pubDate>
        <description><![CDATA[
          <h3>什么是异步任务</h3>
<p>首先我们知道，js是单线程语言，事件循环是js的事件执行机制。因为js是单线程的所以就导致，如果一个任务执行时间太长，会导致后面的任务无法执行，进而卡住执行栈。所以在js中将任务分为两类：</p>
<ul>
<li>同步任务</li>
<li>异步任务</li>
</ul>
<p>同步任务直接添加到执行栈中，异步任务则在自己需要的准备工作完成后添加在执行栈中，这样就不会因为异步任务所需要的准备时间过长而导致执行阻塞。</p>
<h3>异步任务的执行</h3>
<p>异步任务在创建的时候，不会立即执行，而是会被注册到事件表（Event Table）中，然后继续执行主线程的同步任务，当异步任务准备就绪的时候，会将注册的回调函数放入任务队列（Event Queue）。当主线程当下的任务结束后，会去任务队列里面取新的任务，这个时候异步任务的回调函数就进入主线程执行。</p>
<p>上面这个过程会一直循环，也就是我们说的事件循环机制。</p>
<pre><code class="language-js">setTimeout(() =&amp;amp;gt; {
  console.log('回调函数执行')
}, 100)
console.log('主线程任务')
</code></pre>
<p>上面代码执行后打印顺序是：“主线程任务”，等待100ms后打印 “回调函数执行”。</p>
<p>有时候我们还能看到<code>setTimeout(callBack, 0)</code>,<code>setTimeout(callBack)</code>这样的写法，但是这种延迟为0的注册异步任务的形式并不代表它就会如同同步任务一样立即执行。</p>
<pre><code class="language-js">setTimeout(() =&amp;amp;gt; {
  console.log('回调函数执行')
}, 0)
console.log('主线程任务')
</code></pre>
<p>上面的代码的执行结果仍然是：“主线程任务”， “回调函数执行”。setTimeout第二参数的意思只是最少的等待时间，这个时间一到，就会把回调函数放入任务队列，但是是否立即执行，还需要看主线程是否为空，如果当前的主线程还有其他任务在执行，那么就要等待主线程空闲后，再去任务队列拿新的任务执行。所以<code>setTimeout(callBack, 0)</code>只是将callBack立即加入到任务队列，此时主线程的执行栈中已经有任务在执行，所以要等到主线程空闲后再执行。</p>
<h3>异步任务的分类</h3>
<p>异步任务分为两类：</p>
<ul>
<li>宏任务（setTimeout，setInterval，整段代码的script等）</li>
<li>微任务（promise的then，process.nextTick等）</li>
</ul>
<p>这两类任务都属于异步任务，他们的区别在于执行顺序的不同（此图转引自<a href="https://juejin.im/post/5b498d245188251b193d4059">这里</a>）：</p>
<p><img src="https://godlanbo.com/images/1606843708445.jpg" alt="164974fa4b42e4af.jpg"></p>
<p>对于上面的执行顺序，我们看个简单的例子：</p>
<pre><code class="language-js">console.log('start')
setTimeout(() =&amp;amp;gt; {
  console.log('第一個回调')
}, 0)

Promise.resolve().then(() =&amp;amp;gt; {
  console.log('promise1')
}).then(() =&amp;amp;gt; {
  console.log('promise2')
})

setTimeout(() =&amp;amp;gt; {
  console.log('第二个回调')
}, 0)

console.log('end')
</code></pre>
<p>首先整块代码当做一个宏任务执行，先执行同步任务，输出start，然后将第一个setTimeout添加到宏任务队列记做<code>setTimeout1</code>，然后填加promise的两个then到微任务队列我们记做<code>then1</code>和<code>then2</code>，再将第二个setTimeout添加到宏任务记做<code>setTimeout2</code>，最后输出end，此时的两类异步任务的队列情况如下：</p>
<table>
<thead>
<tr>
<th>宏任务队列（macrotasks queues）</th>
<th>微任务队列（microtasks queues）</th>
</tr>
</thead>
<tbody>
<tr>
<td>setTimeout1</td>
<td>then1</td>
</tr>
<tr>
<td>setTimeout2</td>
<td>then2</td>
</tr>
</tbody>
</table>
<p>这个时候整个代码宏任务完成，去微任务队列清空所有的微任务，输出promise1,promise2，然后拿出一个宏任务执行。如此重复上面的步骤。所以最后输出结果是：start, end, promise1, promise2, 第一個回调，第二个回调。</p>
<h3>结尾</h3>
<p>以上就是关于js异步任务的简单介绍。</p>
<h3>参考文献</h3>
<p>https://juejin.im/post/5b498d245188251b193d4059</p>
<p>https://juejin.im/post/59c25c936fb9a00a3f24e114</p>
<p>http://www.ruanyifeng.com/blog/2014/10/event-loop.html</p>

        ]]></description>
      </item><item>
        <title>虚假的回调函数： addEventListener</title>
        <link>http://godlanbo.com/blogs/19</link>
        <guid>19</guid>
        <pubDate>Mon, 30 Nov 2020 07:23:05 GMT</pubDate>
        <description><![CDATA[
          <h2>背景</h2>
<p>一个阳光明媚（才怪）的下午，我一个同样和我学习前端的朋友来问我关于<code>this</code>指向的问题，首先就是关于箭头函数和函数声明中关于<code>this</code>指向的问题，这个是老生常谈的话题了，没有问题；然后就着这个话题谈到了回调函数中的<code>this</code>，他举了个例子：</p>
<pre><code class="language-js">setTimeout(() =&amp;amp;gt; {
  console.log(this)
})
setTimeout(function() {
  console.log(this)
})
</code></pre>
<p>我就说这种回调函数形式的情况下，如果不使用箭头函数的话，里面的<code>this</code>都是指向的<code>window</code>，因为函数中的this指向调用者，回调函数没有调用者，或者说调用者就是顶部作用域。然后他追问到下面这种情况为什么不使用箭头函数内部的this也没有指向<code>window</code>：</p>
<pre><code class="language-js">element.onclick = function(){}
</code></pre>
<p>我说这种情况根本不是回调函数的形式，这是在元素的属性上赋值了一个函数，等到触发事件的时候，元素会去调用这个属性上的函数，自然其中的this就会指向调用者本身也就是元素自己了。然后我顺势就说到了另一种绑定事件的方法<code>addEventListener</code>，我说像这种明显的回调函数式注册的事件处理，不使用箭头函数的话，函数内部的<code>this</code>就会指向<code>window</code>。</p>
<p>当时我非常自信啊，<code>addEventListener</code>这种如此明显的<code>callBack</code>形式注册处理函数的情况，我断定它使用函数声明的情况下，处理函数中的<code>this</code>是指向<code>window</code>的。</p>
<p>然后，晚上他在qq上找到我，给我发了一张图：</p>
<p><img src="http://godlanbo.com/images/1606720834098.jpg" alt="B3XVL`LY2DL4M_9TU7JI.png"></p>
<p>啪的一下，很快啊！一巴掌打我脸上。</p>
<p><img src="http://godlanbo.com/images/1606720848986.jpg" alt="G0AKMIKH559ZUH5YI8`1.jpg"></p>
<h2>解决</h2>
<p>我们经常见到的回调函数形式一般都是这种样子：</p>
<pre><code class="language-js">function todoSomething(args, callback) {
  // do something
  if (callback) callback()
}
</code></pre>
<p><code>addEventListener</code>调用的写法简直就是经典的回调函数形式，传一个处理函数进去，导致我直接误判了它this的指向；但这其实是很简单的内容，在MDN上能够轻松找到相关的解释：</p>
<p>&amp;gt; ### 处理过程中 <code>this</code> 的值的问题
&amp;gt;
&amp;gt; 通常来说this的值是触发事件的元素的引用，这种特性在多个相似的元素使用同一个通用事件监听器时非常让人满意。
&amp;gt;
&amp;gt; 当使用 <code>addEventListener()</code> 为一个元素注册事件的时候，句柄里的 this 值是该元素的引用。其与传递给句柄的 event 参数的 <code>currentTarget 属性的值一样。</code></p>
<p>对于这种特殊情况下的<code>this</code>指向问题，只能在踩坑之后记住（就像我这次），因为是和平时认知（回调函数中的this）不一样的结果，我在网上搜寻为什么会出现这种特殊情况，也没有得到结果，最后只找到了一段对于<code>addEventListener</code>的<code>polyfill</code>：</p>
<pre><code class="language-js">if (!Element.prototype.addEventListener) {
  var eventListeners = []

  var addEventListener = function(
    type,
    listener /*, useCapture (will be ignored) */
  ) {
    var self = this
    var wrapper = function(e) {
      e.target = e.srcElement
      e.currentTarget = self
      if (typeof listener.handleEvent != 'undefined') {
        listener.handleEvent(e)
      } else {
        listener.call(self, e) // 正常的回调函数在这里被绑定了上下文
      }
    }
    if (type == 'DOMContentLoaded') {
      var wrapper2 = function(e) {
        if (document.readyState == 'complete') {
          wrapper(e)
        }
      }
      document.attachEvent('onreadystatechange', wrapper2)
      eventListeners.push({
        object: this,
        type: type,
        listener: listener,
        wrapper: wrapper2
      })

      if (document.readyState == 'complete') {
        var e = new Event()
        e.srcElement = window
        wrapper2(e)
      }
    } else {
      this.attachEvent('on' + type, wrapper)
      eventListeners.push({
        object: this,
        type: type,
        listener: listener,
        wrapper: wrapper
      })
    }
  }
}
</code></pre>
<p>在<code>polyfill</code>明显的看到了对于回调函数的上下文绑定，所以我只能猜测web api中的<code>addEventListener</code>底层实现也是对回调函数就行了处理的。</p>
<h2>参考</h2>
<p><a href="https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener">https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener</a></p>

        ]]></description>
      </item><item>
        <title>Node模块循环引用问题</title>
        <link>http://godlanbo.com/blogs/18</link>
        <guid>18</guid>
        <pubDate>Wed, 18 Nov 2020 07:29:47 GMT</pubDate>
        <description><![CDATA[
          <h2>背景</h2>
<p>下午我正在为我的博客添加评论系统，由于只是想用一个简单的功能，所以是自己在开发，在开发到添加评论的时候，自然的从blog服务的文件中引入了一个函数<code>updateBlogCommentNum</code>，用于在添加了评论后博客的数据库中评论数量也更新一下。</p>
<p>这时候考虑到博客删除的时候需要清空对应的评论，于是在blog服务的文件里引用了<code>clearComments</code>函数来清除对应的评论。</p>
<p>然后上面的两个函数，我秉着模块化的思想，<code>updateBlogCommentNum</code>这个关于blog相关的函数我放在了blog service的文件里，<code>clearComments</code>函数我放在了comment service的文件里。</p>
<h2>困惑</h2>
<p>做完了开发后，自然就是调试，打开postman，开始对刚刚写的接口进行调试，当时主要是想调试<code>jwt</code>的鉴权设置是否正确，因为对于业务代码我基本没什么担心的，很少出现导致不能运行的bug。</p>
<p>当调试到添加评论的时候，我发现报错了，正常嘛，因为我对于<code>jwt</code>也只是简单配置来用，出点问题还是在接受范围内，结果我一查报错信息：<code>updateBlogCommentNum is not a function</code>，嗯？</p>
<p><img src="http://godlanbo.com/images/1605684483603.jpg" alt="~5E3H9VJE50VO5MT.jpg"></p>
<p>还能有这种错误，这种一看就是函数引用错误的东西不是直接在运行阶段就会报错吗？</p>
<p>立即打开编辑器查看，发现并没有出现写错名字这种低级错误，编辑器也正常的显示了定义：</p>
<p><img src="http://godlanbo.com/images/1605684462283.jpg" alt="image20201117181517160.png"></p>
<p>有点诡异，连续测试了几次，还是一样的错误信息：<code>updateBlogCommentNum is not a function</code>。但是编辑器这边一点苗头都看不出来，<code>console</code>去打印这个函数确实发现是<code>undefined</code>。</p>
<h2>很难发现的问题</h2>
<p>后来我怀疑不是这个函数的问题，而是引用出了问题，我把另一个以前能运行的函数从blog service文件中引入到comment service文件中打印发现也是<code>undefined</code>，这就确定了问题不是出在这个函数这里，而是引用哪里出了问题。但是一直不能定位到底是出了什么问题，本着死马当活马医的想法，把node is not a function 拿到google去搜，我想着这不过是再常见不过的type error了，应该不会有什么结果，但令我惊喜的是居然搜到了！神奇。。。</p>
<p>最后发现问题是一个互相引用引起的，我的blog service文件引用了comment service模块中的函数，comment service文件又引用了blog service中的模块，导致了一个循环引用。</p>
<h2>循环引用</h2>
<p>在Node的官网上就有关于<a href="https://nodejs.org/api/modules.html#modules_cycles">这方面的描述</a>。</p>
<p>在官网给出的例子中，有 3 个模块：<code>main.js</code>、<code>a.js</code>、<code>b.js</code>。其中 <code>main.js</code> 有对 <code>a.js</code> 和 <code>b.js</code> 的引用，而 <code>a.js</code> 和 <code>b.js</code> 又是相互引用的关系（详细情况请参阅上段末的超链接）。</p>
<p>官网上点出了这种模块循环的情况，并且解释清楚了原因：</p>
<p>&amp;gt; When <code>main.js</code> loads <code>a.js</code>, then <code>a.js</code> in turn loads <code>b.js</code>. At that point, <code>b.js</code> tries to load <code>a.js</code>. In order to prevent an infinite loop, an <strong>unfinished copy</strong> of the <code>a.js</code> exports object is returned to the <code>b.js</code> module. <code>b.js</code> then finishes loading, and its exports object is provided to the <code>a.js</code> module.</p>
<p>所以简单来说，就是Node为了防止循环引用导致出现死循环这种情况的发生，在第一次载入模块的时候会对模块进行缓存，当下一次再进行载入的时候，会直接使用缓存中的结果，这样就避免了死循环，但是由于缓存中是一个未完成的副本（<code>unfinished copy</code>），所以会导致一些东西不能如我们预想的那样被导出。</p>
<p>回到我的问题就非常清晰了，blog和comment两个模块发生循环引用，导致blog中导出不完整，所以comment中引用blog中的函数才会出现<code>undefined</code>的情况。</p>
<h2>解决</h2>
<p>解决的办法有很多，可以重新安排一下函数的位置，比如我把<code>clearComments</code>函数放到了blog service模块中，这样blog service模块就不用引用comment service模块了，自然问题也就解决了。</p>
<p>还有种方法是在每个模块中优先把需要导出的内容提到文件顶部，像这样：</p>
<pre><code class="language-js">module.exports = {
  /* ... */
  updateBlogCommentNum
}
/* 下面是其它代码 */
/* ... */
</code></pre>
<p>这样模块一加载就把所有需要导出的内容优先导出了，所以即使后面出现循环引用，用到了缓存，缓存中也是有需要导出的部分内容的。就是可能看起来有点反常规（一般都是把导出写在文件最下面）</p>
<h2>参考</h2>
<p><a href="https://stackoverflow.com/questions/33865068/typeerror-is-not-a-function-in-node-js/37886523">https://stackoverflow.com/questions/33865068/typeerror-is-not-a-function-in-node-js/37886523</a></p>
<p><a href="https://nodejs.org/api/modules.html#modules_cycles">https://nodejs.org/api/modules.html#modules_cycles</a></p>
<p><a href="http://maples7.com/2016/08/17/cyclic-dependencies-in-node-and-its-solution/">http://maples7.com/2016/08/17/cyclic-dependencies-in-node-and-its-solution/</a></p>

        ]]></description>
      </item><item>
        <title>一次修nginx路径匹配的经历</title>
        <link>http://godlanbo.com/blogs/17</link>
        <guid>17</guid>
        <pubDate>Tue, 22 Sep 2020 14:09:09 GMT</pubDate>
        <description><![CDATA[
          <p>这次经历告诉我，多学习一些常用的基础知识是多么的重要。</p>
<h2>一、没有配置root</h2>
<p>今天在修改我的网站的时候，在服务器上想用nginx做一下图片和字体的缓存设置，于是上网搜了一下相关的配置，很多，一下子就配置好了：</p>
<pre><code class="language-nginx">location ~ .*\.(png|jpg|jpeg|ttf|woff)$ {
  expires 30d;
}
</code></pre>
<p>然后开心的打开网站准备看看缓存的效果，结果迎接我的却是全部资源的404。令人无法接受，我自然认为应该是location的路径匹配出了点问题，于是认真去网上又找了一遍，确认了匹配路径没有问题，行，再试一次，仍然404。</p>
<p>我觉得直接搜索图片缓存配置可能已经解决不了这个问题，因为明显这不是路径配置出了问题。我开始搜索nginx的location匹配规则，不一会儿看到了一个代码块：</p>
<pre><code class="language-nginx">location ~* \/img {
  root D:/nginx/reg/;
  index test.png
}
</code></pre>
<p>我看到这个配置的结果匹配到了<code>D:/nginx/reg/img/text.png</code>，我一下子似乎明白了问题出在哪里。于是我将我的配置中加了一行我自己的root：</p>
<pre><code class="language-nginx">location ~ .*\.(png|jpg|jpeg|ttf|woff)$ {
  root /root/web-dist;
  expires 30d;
}
</code></pre>
<p>一下子我就能够访问资源了，开心之余去搜索了一些配置样例，基本都是把root写在server里面的，我一直把root写在了location里面，以前没有出过问题，这次也就是写到匹配<code>/</code>路径的location里面的，直到今天出现这个情况。</p>
<p>所有以后记得一定把root，也就是基地址写在server里面，再用location去匹配。</p>
<h2>二、匹配优先级问题</h2>
<p>当我搞定了缓存图片的配置之后，打开网页，嗯，图片资源和字体都加载出来了，但是我发现控制台还有一个报错，是我自己服务端静态资源服务上的图片挂了，报404。我直接？？？，修一个bug，造一个bug， 行吧，有了刚刚查的那些资料，我大概明白了问题应该也是出现在nginx配置上，检查了下配置：</p>
<pre><code class="language-nginx">location /images/ {
  proxy_pass http://godlanbo.com:3000;
}
location ~ .*\.(jpg|jpeg|png)$ {
  root  /root/web-dist;
  expires 30d;
}
</code></pre>
<p>猜测应该是nginx匹配优先级的问题，我一开始以为是从上往下解析的，所以把静态资源服务的匹配放在图片缓存的上面，但似乎不是这样的。去网上查了一下：</p>
<p>&amp;gt; 第一优先级：等号类型（=）的优先级最高。一旦匹配成功，则不再查找其他匹配项。
&amp;gt; 第二优先级：^~类型表达式。一旦匹配成功，则不再查找其他匹配项。
&amp;gt; 第三优先级：正则表达式类型（~ ~*）的优先级次之。如果有多个location的正则能匹配的话，则使用正则表达式最长的那个。
&amp;gt; 第四优先级：常规字符串匹配类型。按前缀匹配。</p>
<p>这一下就懂了，我的<code>/images/</code>是前缀匹配，最低优先级，而图片缓存是正则匹配，比前缀匹配高一个等级，所以静态资源服务的图片访问被匹配到下面去了，自然404。看了一下第三优先级的描述，同级优先匹配最长的正则表达式，那就直接：</p>
<pre><code class="language-nginx">location ~ /images/.*\.(jpg|png|jpeg) {
  proxy_pass http://godlanbo.com:3000;
}
</code></pre>
<p>哈，这下静态服务的匹配路径就比图片缓存的长了，就能够正常访问了XD。</p>

        ]]></description>
      </item><item>
        <title>个人博客搭建路线指南</title>
        <link>http://godlanbo.com/blogs/16</link>
        <guid>16</guid>
        <pubDate>Tue, 22 Sep 2020 08:18:50 GMT</pubDate>
        <description><![CDATA[
          <p>首先声明本教程旨在自己动手，通过学习简单的前后端开发技术搭建自己的个人博客，而不是直接使用hexo，ghost等博客框架进行博客的快速搭建。</p>
<h2>1、准备工作</h2>
<p>在进行开发之前，我们首先需要做好一些准备工作，比如环境的搭建，开发工具的选择等，这些都可以在过程中帮助我们更好的进行开发。</p>
<h3>1.1、前端</h3>
<p>首推使用vscode进行学习开发前端，轻量的同时有着良好对JS的语法支持，还有多种插件可以自行安装，高效的进行代码的编写。备选项是使用sublime或者是webstorm，前者是类似于笔记本的超轻量编辑器，启动速度快，当然对于语法的支持自然是相当不行的，而且没有针对JS，html等的格式化，如果有代码洁癖者还是建议使用vscode；后者是最重型的前端集成开发环境，功能非常强大，补全，格式化，自动索引都是吊打vscode，当然代价就是体积大，启动慢。</p>
<p>如果你后期会考虑到使用工具或者框架进行辅助开发，还需要在本地预先安装好<a href="https://nodejs.org/zh-cn/">node</a>环境，我们并不会使用node的语法进行什么开发，而是需要node的包管理器和node运行工具来进行一些框架的安装和本地运行。</p>
<h3>1.2、后端</h3>
<p>后端在语言上有很多种选择，根据不同的开发语言选择合适的工具。</p>
<p>如果你是使用node进行开发，那么推荐策略和前端一样，因为node就是js的一种形式，对前端代码支持良好的工具都有着很好的JS语法支持。</p>
<p>如果你是使用java进行开发，推荐使用<a href="https://www.jetbrains.com/idea/">IDEA</a>集成开发环境，他有对java非常良好的支持，虽然是重型工具，但是对于java这种构建起来比较复杂的情况，好的工具能够帮助免去很多麻烦的事情。</p>
<p>如果你是使用python进行开发，可以使用vscode或者是<a href="https://www.jetbrains.com/pycharm/">PYCHARM</a>集成开发环境，前者可以安装插件后对python进行支持，后者是专门的python开发工具，当然后者的功能会强大很多，缺点也是上面说的一样，体积大，启动慢。</p>
<h3>1.3、服务器</h3>
<p>当我们开发好了所有项目文件之后，我们就需要开始部署，以让我们的项目可以在公网被访问到。这个时候首先就需要一台服务器，这个可以在<a href="https://cn.aliyun.com/">阿里云</a>或者是<a href="https://cloud.tencent.com/">腾讯云</a>上去购买一台学生机，有学生优惠，非常便宜，一年只需要100+元。购买的时候可以选择你的服务器的操作系统，可以选择Ubuntu或是Centos。</p>
<h2>2、学习路线</h2>
<p>准备好开发环境和趁手的工具，就可以开始进行学习了。</p>
<h3>2.1、前端基础</h3>
<p>前端学习，万变不离其宗，三驾马车<a href="https://developer.mozilla.org/zh-CN/docs/Web/HTML">HTML</a>，<a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS">CSS</a>，<a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript">JS</a>。</p>
<h4>2.1.1、HTML</h4>
<p>基础的学习推荐在<a href="https://www.w3school.com.cn/html/index.asp">w3cshool</a>或者是<a href="https://www.runoob.com/html/html-tutorial.html">菜鸟教程</a>上进行学习，建议学习html全部内容，至少要看一遍，记不住到是无所谓，有点映象，在开发的时候用到再进行查阅学习即可。对于html5内容，重点需要学习语义标签和一些html5的新规范，其余的知识有兴趣可以看一看，在前期用到的机会较少。</p>
<h4>2.1.2、CSS</h4>
<p>仍然推荐在<a href="https://www.w3school.com.cn/html/index.asp">w3cshool</a>或者是<a href="https://www.runoob.com/html/html-tutorial.html">菜鸟教程</a>上进行简单的学习，对于css，属性众多，对于基本的属性（颜色，宽高等）进行掌握和运用，对于一些较复杂属性，看一遍，能记住最好，有个印象也行，当使用到或者看到的时候，想不起来作用和用法，推荐到<a href="https://developer.mozilla.org/zh-CN/">MDN</a>查找相关内容，MDN上对于属性的解释和赋值规则和作用都有非常详细的介绍。</p>
<p>布局方面注意的是，grid布局如果觉得不方便可以不用看了。</p>
<p>当你学习完css属性和值以后，就可以开始学习<a href="https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Selectors">CSS选择器</a>，这个是比较重要的一步，你需要学习这个才能将学习到的css属性作用到你想作用到的元素上去。刚开始可以只学习元素选择器，类选择器，ID选择器。</p>
<h3>2.1.3、JS</h3>
<p>推荐<a href="https://wangdoc.com/javascript/basic/grammar.html">阮一峰的JS文档</a>，是一个很好的入门文档，从基础语法到内置对象都有比较详细的说明，遇到没看懂的也可以去MDN上查找相关内容，总的来说，MDN是前端开发中最重要的网站之一。再者，可以学习红宝书（JavaScript高级编程设计），这也是学习JS路上的一本比读书，虽然名字是高级编程，但是是从基础语法开始讲起，也是入门JS的一本好书。</p>
<h3>2.2、开发选择</h3>
<p>当学习完基础知识之后，就可以考虑开始进行开发了，这时候有两种选择，直接使用原生进行开发，如果是这样的话，上面的知识足够支撑进行开发了，只不过会稍微慢一点，需要手写的逻辑比较多。第二种选择就是使用框架进行开发，这可以大大加快开发进程，减轻编码的负担，只是需要多花一些时间去学习一下框架的使用。</p>
<h3>2.3、前端进阶</h3>
<h4>2.3.1、JQ&amp;amp;BootStrap</h4>
<p>如果你觉得原生写起来太过不便，框架入门又太过复杂，可以考虑JQuery和Bootstrap的组合进行开发。jq是一种js库，bootstrap是一套前端UI框架。两者都可以在网上找到对应的文档和教程（<a href="https://www.jquery123.com/">jq</a>,<a href="https://v3.bootcss.com/">bootstrap</a>）。由于编者两个都没用过，这里不做评价，但是两者都有很好的口碑，但是似乎有过时的趋势。</p>
<h4>2.3.2、响应式框架</h4>
<p>前端三大响应式框架，<a href="https://angular.cn/">Angular</a>，<a href="https://cn.vuejs.org/v2/guide/installation.html">Vue</a>，<a href="https://zh-hans.reactjs.org/">React</a>，都可以快速方便的进行前端项目的开发，具体学习参看官方文档，从下载赞安装到构建，我觉得官方文档讲的足够透彻，就看你自己选择哪一个框架进行学习使用。（编者使用的是Vue，入门成本其实相对低一点）</p>
<h3>2.4、后端</h3>
<p>后端的开发不像前端一样有固定的基础，它有多样化的技术选型。</p>
<h4>2.4.1、Node.js</h4>
<p>如果在学习了前端的js后觉得这门语言还不错，可以考虑使用Node.js进行后端的开发，因为是Js的变种，所以语法可以平滑过渡，使用node的包管理器可以使用到各种各样的方便工具。</p>
<p>使用node进行后端开发，不建议使用原生编写服务接口，解析和数据处理都要写不少逻辑，显得有点复杂，推荐使用轻量框架<a href="https://expressjs.com/zh-cn/">express</a>，轻量简便，快速上手，做一个博客的服务端已经绰绰有余。同样的，官网上有详细的安装使用教程，直接进行学习，高级用法基本不会用到，查看基本的用法即可。</p>
<h4>2.4.2、Python</h4>
<p>python后端主要是<a href="https://dormousehole.readthedocs.io/en/latest/">flask</a>，<a href="https://docs.djangoproject.com/zh-hans/2.2/">django</a>，flask是一种微框架，学习成本比较低，如果会python的话，很快就能上手。django看官方文档。</p>
<h4>2.4.3、Java</h4>
<p>java 的后端开发主要框架主要就是<a href="https://spring.io/">spring</a>家族，spring写代码简单，但是配置很繁琐，后面就spring就推出了spring boot，采用约定大于配置的思想，就是在spring基础上加了自动配置的功能，所有配置都是有默认值，只用改你想改的就好了。这样就只用关注代码的逻辑就好了。</p>
<p>学习的话，先学一下java的反射，泛型，注解，然后就是ide推荐使用idea，包管理工具maven，gradle得学会一种，然后就是spring了，框架可以直接从spring boot 开始学，学起来比较快，有时间的话，也可以深入了解spring，看一看源码。</p>
<h3>2.5、博客存储</h3>
<h4>2.5.1、数据库</h4>
<p>我们写博客的时候可能会用到数据库，将博客的markdown字符串存在数据库里面方便读取修改和添加。比较推荐使用Mysql，方便简单。如何安装看<a href="https://zhuanlan.zhihu.com/p/112765207">Windows版</a> <a href="https://juejin.im/post/6844903831298375693">Mac版</a>。</p>
<p>学习简单的数据库操作，可以在<a href="https://www.runoob.com/sql/sql-syntax.html">菜鸟教程</a>学习，只需要学习简单的创建表，增删查改功能即可，因为博客不会用到很复杂的查询语句。</p>
<p>如果需要在GUI界面上操作数据库可以下载<a href="https://dev.mysql.com/downloads/workbench/">Mysql workbench</a>，选择对应版本下载安装即可。或者使用其它的GUI软件也可以，请自行进行选择学习。（编者没有使用其它GUI的经验）</p>
<h4>2.5.2、不使用数据库</h4>
<p>也可以不使用数据库，直接在后端将接受到的markdown字符串转成文件存储，然后读取的时候直接读取对应的文件，将markdown字符串读出来传回前端渲染。</p>
<h3>2.6、部署</h3>
<p>首先我们需要登录上服务器，配置所需要的环境，因为前端的代码大多是静态文件，访问即可运行，即使是三大框架那种需要npm构建的项目，也有对应的打包方案，无需在服务器上再部署前端环境。但是后端代码需要服务器又对应运行环境才可以运行在后台提供服务。所以根据你后端的技术选型，配置相应环境即可。</p>
<p>然后是安装nginx，nginx是一个轻量的http和反向代理服务器，有了它我们就能在浏览器上访问我们的相关文件。<a href="https://segmentfault.com/a/1190000015797789">Ubuntu下安装nginx</a>、<a href="https://zhuanlan.zhihu.com/p/59481778">Centos下安装nginx</a>。</p>
<p>如果你没有使用数据库，下面一段教程可以跳过。</p>
<p>在服务器上安装mysql，<a href="%5Bhttps://wiki.ubuntu.org.cn/MySQL%E5%AE%89%E8%A3%85%E6%8C%87%E5%8D%97%5D(https://wiki.ubuntu.org.cn/MySQL%E5%AE%89%E8%A3%85%E6%8C%87%E5%8D%97)">Ubuntu安装mysql</a>，<a href="https://juejin.im/post/6844903870053761037">Centos安装mysql</a>。若你没有使用GUI进行数据库的操作，请查看<a href="https://www.cnblogs.com/yuwensong/p/3955834.html">Mysql导入导出sql文件</a>，在本地导出你的数据库文件，上传到服务器，再在服务器上导入文件运行。若你使用了GUI，给出<a href="https://blog.csdn.net/wangjun5159/article/details/52387007">Mysql workbench的导出教程</a>，其它GUI工具的导出教程请自行搜索。然后使用GUI工具远程链接上服务器上的mysql服务，再把sql文件导入执行即可。</p>
<h2>3、注意事项</h2>
<p>由于编者的服务器是Ubuntu，所以对于Centos部分的所有教程均未验证有效性，如果出现问题，具体情况具体分析，实在不行就找康神。</p>

        ]]></description>
      </item><item>
        <title>前端面试经典for+setTimeout多解法</title>
        <link>http://godlanbo.com/blogs/15</link>
        <guid>15</guid>
        <pubDate>Thu, 10 Sep 2020 14:40:45 GMT</pubDate>
        <description><![CDATA[
          <p>话不多说，直接先上原题：</p>
<pre><code class="language-js">for(var i = 0; i &amp;amp;lt; 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 600)
}
</code></pre>
<p>由于<code>console</code>开始执行的时候 i 已经被变成了5，所以这个代码会直接输出5个5，然后就会问你，如何将其改成依次输出1 2 3 4。</p>
<h2>一、ES6</h2>
<p>由于ES6标准引入了let关键字，let关键字声明的变量拥有私有作用域，只在声明所在的代码块有效（以<code>{}</code>为界），所以我们直接把声明 i 的时候改成使用let声明，在每一次for执行的时候，i的值就会被固定到当前块中，这样<code>console</code>开始执行的时候就能取到当时 i 的值：</p>
<pre><code class="language-js">for(let i = 0; i &amp;amp;lt; 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 600)
}
</code></pre>
<h2>二、闭包</h2>
<p>闭包的概念引用MDN上的原话：</p>
<p>&amp;gt; 函数和对其周围状态（<strong>lexical environment，词法环境</strong>）的引用捆绑在一起构成<strong>闭包</strong>（<strong>closure</strong>）。也就是说，闭包可以让你从内部函数访问外部函数作用域。在 JavaScript 中，每当函数被创建，就会在函数生成时生成闭包。</p>
<p>我们直接将代码改成下面的样子：</p>
<pre><code class="language-js">for (var i = 0; i &amp;amp;lt; 5; i++) {
  (function (i) {
    setTimeout(function () {
      console.log(i)
    }, 200)
  })(i)
}
</code></pre>
<p>上述代码中我们利用一个立即执行函数包裹了定时器，而定时器中的回调函数引用了外层自调函数的形参 i ，所以此时定时器的回调函数是一个闭包。在循环的时候， i 就被绑定到了这个立即执行函数的作用域中，然后函数里又有对形参 i 的引用，所以立即执行函数在执行完后不会被销毁，会暂时保留在内存中，在它作用域中的 i 的值自然也被保留下来，当<code>console</code>执行时，就可以访问到当时传进来的值。</p>
<h2>三、setTimeout的第三个参数</h2>
<p>我估计可能有人不知道<code>setTimeout</code>还有第三个参数，我也是最近才知道的：</p>
<p><img src="http://47.99.136.41:3000/images/1599789282670.jpg" alt="settimeout.png"></p>
<p>可以看到第三个参数是<code>arguments</code>，对没错，就是作为<code>setTimeout</code>第一个参数<code>timer</code>函数的参数传入，我们将代码改成：</p>
<pre><code class="language-js">for (var i = 0; i &amp;amp;lt; 5; i++) {
  setTimeout(function(j) {
    console.log(j)
  }, 200, i)
}
</code></pre>
<p>i 的值被传入<code>timer</code>函数中与其作用域绑定，在<code>console</code>执行的时候就能访问到<code>timer</code>函数作用域保存的当时那个 i 的值。</p>
<h2>四、catch使作用域链延长</h2>
<p>关于catch使作用域链延长造成的现象可以看我的另一篇文章。</p>
<p>我们将代码改成下面的样子：</p>
<pre><code class="language-js">var i = 0
for (; i &amp;amp;lt; 5; ++i) {
  try {
    throw i
  } catch(i) {
    setTimeout(function () {
      console.log(i)
    }, 200)
  }
}
</code></pre>
<p><code>catch</code>会延长作用域链创建一个新的变量对象（作用域），其中只包含了对抛出错误对象的声明，我们将 i 抛出，进入到<code>catch</code>块中，这样 i  的值就被传递到<code>catch</code>声明的作用域中被绑定，i 的值就只能在<code>catch</code>块中被使用（类似于模拟了一个块级作用域），当<code>console</code>执行的时候，便能访问到<code>catch</code>块中绑定的 i 的值。</p>

        ]]></description>
      </item><item>
        <title>catch块延长作用域链</title>
        <link>http://godlanbo.com/blogs/14</link>
        <guid>14</guid>
        <pubDate>Thu, 10 Sep 2020 11:23:18 GMT</pubDate>
        <description><![CDATA[
          <h2>背景</h2>
<p>今天在群里偶然看见一个有趣的代码块：</p>
<pre><code class="language-js">(function() {
  try {
    throw new Error()
  } catch(x) {
    var x = 1, y = 2
    console.log(x) // 1
  }
  console.log(x) // undefined
  console.log(y) // 2
})()
</code></pre>
<p>看到里面注释所标注的输出内容，一下子就把我打到了未知领域，赶紧把代码复制到本地跑了一下，结果当然就和注释的一样。顿时感觉自己的基础稀烂，赶紧翻书上网学习相关内容。</p>
<h2>一、作用域链</h2>
<p>​	在js中<strong>执行环境</strong>决定了变量或者函数有权访问的其他数据，每个环境都有一个关联的<strong>变量对象</strong>，环境中定义的变量和函数都保存在里面。函数也有自己的执行环境；当代码在一个环境执行的时候，会创建变量对象的一个<strong>作用域链</strong>。作用域链可以保证对执行环境有权访问的变量和函数的有序访问，作用域链最前端就是当前的执行环境关联的变量对象，往后面走就是包含（外部）环境的变量的对象，一直往后走到末尾就是全局环境，这是作用域链中的最后一个对象。</p>
<p>​	以下面的代码为例子：</p>
<pre><code class="language-js">var a = 1
function one() {
  var b = 1
  function two() {
    var c = 1
  }
}
</code></pre>
<p>这个例子里涉及了三个执行环境，全局环境，one的局部环境，two的局部环境；最里面的two局部环境就可以顺着作用域链访问外面的a和b，但是one局部环境和全局环境就不能访问到two里面的c。</p>
<h2>二、延长作用域链</h2>
<p>​	在js高程4.2.1里说到了延长作用域链这个东西，有两种情况，一种是with，另一种是try-catch语句中的catch块。本篇文章重点说明catch对作用域链的延长作用。引用高程里话：</p>
<p>&amp;gt; 对于catch语句来说，会创建一个新的变量对象，其中包含的是被抛出的错误对象的声明。</p>
<p>我们刚刚说到作用域链上就是一个接一个的变量对象，最前端就是当前执行环境的变量对象，这里的catch就相当于在当前执行环境的变量对象前面再加一个变量对象，其中定义了被抛出错误对象的声明。</p>
<h2>最后</h2>
<p>​	我们回到最开始提到的代码，现在在catch块中，我们以x的标识声明了错误对象，然后用var的形式又一次初始化了x，这里看起来有点不容易看清楚，我们知道var对变量是有提升的，可以将代码看成下面这样：</p>
<pre><code class="language-js">(function() {
  var x, y
  try {
    throw new Error()
  } catch(x) {
    x = 1, y = 2
    console.log(x) // 1
  }
  console.log(x) // undefined
  console.log(y) // 2
})()
</code></pre>
<p>这样问题的答案几乎呼之欲出，由于我们前面讲到了catch会创建单独的变量对象来延长作用域链，所以现在的作用域链并不是看上去的</p>
<ul>
<li>Global
<ul>
<li>function</li>
<li>x, y</li>
</ul>
</li>
</ul>
<p>而是被catch延长后的</p>
<ul>
<li>Global
<ul>
<li>function</li>
<li>x, y
<ul>
<li>catch</li>
<li>x （对错误对象的声明）</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>这样就非常好理解了，在catch块中执行的对x的赋值，最先在catch块中找到了相应的标识，于是停止往作用域链的后面去找的操作，直接将catch块中对于错误对象声明的变量x赋值为了1，然后y在当前作用域中找不到，沿着作用域链找到了function作用域下的y，赋值为了1，所以到最后，func中输出的x为undefined，因为在catch块中的赋值没有赋到它上面，而y则是输出2。</p>

        ]]></description>
      </item><item>
        <title>Promise实现-异步队列</title>
        <link>http://godlanbo.com/blogs/13</link>
        <guid>13</guid>
        <pubDate>Tue, 08 Sep 2020 11:53:42 GMT</pubDate>
        <description><![CDATA[
          <p>我们都知道Promise可以进行then的链式调用，当Promise中的异步过程执行完毕后，我们将结果resolve，之后Promise的状态改变，执行then方法，然后返回新的Promise以供链式调用，那Promise是如何做到then等待Promise的状态改变的呢？下面我们来实现一个简单的链式调用，了解一下Promise内部是如何实现异步队列的等待的。</p>
<p>首先我们定义我们的Promise框架，Promise需要传入一个处理函数，处理函数执行一个resolve方法：</p>
<pre><code class="language-js">class myPromise {
  callbacks = [] // 这是Promise要处理的回调函数集合
  constructor (handler) {
    handler(this.resolve.bind(this))
  }
  resolve(value) {
    ...
  }
}
</code></pre>
<p>我们这里只实现链式调用即可，所以resolve只实现存值和执行回调函数：</p>
<pre><code class="language-js">class myPromise {
  callbacks = [] // 这是Promise要处理的回调函数集合
  constructor (handler) {
    handler(this.resolve.bind(this))
  }
  resolve(value) {
    // 如果不包裹一层setTimeout，在resolve的时候就会立即执行回调函数，如果此时handler的内容是
    // 一个同步函数，那么就会导致then中的回调函数来不及注册到callBacks中（因为then是异步执行的），由
    // 于resolve只能被调用一次，所以此时的回调函数就永远不会触发
    setTimeout(() =&amp;amp;gt; {
      this.data = value
      this.callbacks.forEach((callback) =&amp;amp;gt; callback(value))
    })
  }
}
</code></pre>
<p>然后是Promise最重要的链式调用的实现方法then：</p>
<pre><code class="language-js">class myPromise {
  ...
	// 其实调用then就是往Promise中注册一个回调
  then(onResolved) {
    // then需要返回一个新的Promise
    return new myPromise((resolve) =&amp;amp;gt; {
      // 往回调函数集中注册一个函数（其中会执行我们传入的回调函数）
      this.callbacks.push(() =&amp;amp;gt; {
        // 执行我们传入的回调函数拿到结果
        const result = onResolved(this.data)
        // 如果结果仍然是一个Promise，那么现在返回的这个Promise的状态继承这个结果的状态
        if (result instanceof myPromise) {
          // 这里把返回的Promise的处理权（resolve函数）交给结果的Promise（result）
          // 这样当结果的Promise产生结果的时候，这里返回的Promise的resolve也被触发
          // 则执行当前返回的Promise所注册的回调函数（就是这个then后面then中注册的函数）
          result.then(resolve)
        } else {
          // 如果不是，则执行resolve，将这个Promise的回调函数集全部执行
          resolve(result)
        }
      })
    })
  } 
}
</code></pre>
<p>我们用一个包含全部情况 的简单例子来描述这个过程：</p>
<pre><code class="language-js">new myPromise(resolve =&amp;amp;gt; { // fn1
  setTimeout(() =&amp;amp;gt; {
    resolve(1)
  }, 500)
}).then(res =&amp;amp;gt; { // then1
  console.log(res)
  return new myPromise(resolve =&amp;amp;gt; { // fn2
    setTimeout(() =&amp;amp;gt; {
      resolve(2)
    }, 500)
  })
}).then(console.log) // then2
</code></pre>
<p>首先我们传入fn1创建Promise（下面简称promise1），创建时会执行这个函数，此时注册一个定时器，过一会儿执行这个promise1的resolve函数。</p>
<p>然后调用promise1的then方法，传入then1作为回调，在then方法里，我们创建一个promise2，promise2创建后，会为promise1的回调集中注册一个回调函数（push的那个，现在还没有执行）。</p>
<p>然后再调用promise2的then方法（这里是链式调用的第二个then方法），传入then2作为回调，在里面我们会在创建一个promise3，为promise2的回调集中注册一个回调函数。</p>
<p>到这里，我们就跑完了主线的代码，等待第一个定时器触发，执行promise1的resolve(1)，触发promise1的回调函数，首先执行then1，将1传入，打印出来后，用fn2创建一个user promise并返回。</p>
<p>这个时候判断拿到then1的返回值result，发现是个Promise，于是执行user promise的then方法，这个时候传入的创建函数是promise2的resolve函数，这里就是把promise2的状态改变权交给了user promise。</p>
<p>然后进入user promise的then方法，同样创建一个返回promise4，往user promise的回调里面注册回调函数，等待第二个定时器触发，执行user promise中的回调函数，结合上面，我们知道，user promise的回调集中正是promise2的resolve函数，所以执行后直接触发promise2的回调集，也就是console.log那里，打印2，结束。</p>
<h2>结束</h2>
<p>上文只是对于Promise链式调用的简单实现和解析，但是我们看完之后可以发现，Promise的三个状态中的前两个状态（pending，fulfilled），其实就是靠的resolve去转换，一个Promise执行resolve后，就从pending变到了fulfilled，然后返回结果的终值。Promise就是用resolve这个类似触发器的东西当做链式调用的钥匙来使用。</p>
<h2>参考文献</h2>
<p>https://www.ituring.com.cn/article/66566</p>
<p>https://juejin.im/post/5e6f4579f265da576429a907</p>

        ]]></description>
      </item><item>
        <title>Node.js连接MySql的正确姿势</title>
        <link>http://godlanbo.com/blogs/12</link>
        <guid>12</guid>
        <pubDate>Tue, 08 Sep 2020 11:52:18 GMT</pubDate>
        <description><![CDATA[
          <p>node写后台的时候，连接数据库是基本操作，当然为了方便开发中的使用，自然要对连接数据库的方式进行一定的封装，导出之后再使用。下面使用MySql来讲讲链接数据库的各种姿势。</p>
<p>以下是开发中的常规操作：</p>
<pre><code class="language-js">// db.js
const mysql = require('mysql')
const con = mysql.createConnection({
  host : &amp;quot;hostName&amp;quot;,
  user : &amp;quot;username&amp;quot;,
  password: &amp;quot;password&amp;quot;
})
// 开始连接数据库
con.connect()
function querySql(sql, callback){
  con.query(sql, (err, result) =&amp;amp;gt; {
    if (err) {
      callback(err, null)
    } else {
      callback(err, result)
    }
  })
}
module.exports = {
  querySql
}
// js 在这里使用导出的查库函数
const { querySql } = require('db.js')
querySql('select * from database', (err, resutl) =&amp;amp;gt; {
  // do some thing
})
</code></pre>
<p>在开发中我们只需要能够查询到数据库的数据就可以了，所以不用考虑数据库连接消耗的资源，一直开启一个连接使用就可以了，上面这种写法能够满足开发用。当然线上是不能这样写的，因为数据库每个连接有一个最大连接时间，约8个小时，时间过了就会自动断开连接，这样是肯定不行的，而且考虑到数据库一直处于连接状态会消耗大量资源，还可能导致内存泄漏，我们可以每次查询创建一个连接，使用完之后关闭。</p>
<p>在改造上面的代码之前我们先看看，这个代码里出现了callback，也就是回调函数。在我的<a href="https://blog.crazyforcode.org/js-deal-callbackhell/">上一篇文章</a>里提到，遇到这种出现回调函数的情况，我们可以用promise来改造回调过程，避免可能出现的回调地狱，使用起来也更加的合理。</p>
<p>改造一下上面的querySql函数</p>
<pre><code class="language-js">const MYSQL_CONF = {
  host : &amp;quot;hostName&amp;quot;,
  user : &amp;quot;username&amp;quot;,
  password: &amp;quot;password&amp;quot;
}
function querySql(sql) {
  // 创建连接对象
  const con = mysql.createConnection(MYSQL_CONF)
  // 开始连接数据库
  con.connect()
  return new Promise((resolve, reject) =&amp;amp;gt; {
    con.query(sql, (err, result) =&amp;amp;gt; {
      if (err) {
        reject(err)
        return
      }
      resolve(result)
    })
    // 关闭连接
    con.end()
  })
}
// js
const { querySql } = require('db.js')
querySql('select * from database').then(resutl =&amp;amp;gt; {
  // do some thing
}).catch(err =&amp;amp;gt; {
  // do some thing
})
</code></pre>
<p>这样似乎不错了，每次查询完毕都会关闭连接，不会导致数据库长期连接出现内存泄漏。</p>
<p>当然，这样只能应对访问量不大，并发执行数据库操作不多的时候。像上面这种写法，每一个用户的每次数据库操作都会创建一个连接，为每个用户打开和维护数据库连接，特别是对动态数据库驱动的网站应用程序的请求，代价高昂，十分浪费资源，并且访问量一大，就有可能挂掉。所以一般使用数据库连接池的方式来连接数据库。</p>
<p>数据库连接池负责分配、管理和释放数据库连接，它允许应用程序重复使用一个现有的数据库连接，而不是再重新建立一个；释放空闲时间（未使用的链接会被放到连接池中处于空闲状态）超过最大空闲时间（就是上文提到的8h后自动断开连接的时间）的数据库连接来避免因为没有释放数据库连接而引起的数据库连接遗漏。这项技术能明显提高对数据库操作的性能。</p>
<p>连接池基本的思想是在系统初始化的时候，将数据库连接作为对象存储在内存中，当用户需要访问数据库时，并非建立一个新的连接，而是从连接池中取出一个已建立的空闲连接对象。使用完毕后，用户也并非将连接关闭，而是将连接放回连接池中，以供下一个请求访问使用。而连接的建立、断开都由连接池自身来管理。</p>
<p>先来写一下mysql连接池的基本写法</p>
<pre><code class="language-js">const pool = mysql.createPool(MYSQL_CONF)
pool.getConnection((err, connetion) =&amp;amp;gt; {
  // connetion就是从连接池里拿到的连接
  connection.query( &amp;quot;select * from table1&amp;quot;, (err, result) =&amp;amp;gt; {
    // do some thing
  })
  // 注意这里不是连接end掉了，而是释放掉，归还回连接池里
  connection.release()
})
</code></pre>
<p>好的，连接池也会写了，之后封装成promise，再导出就完成了</p>
<pre><code class="language-js">// 创建mysql连接池
const pool = mysql.createPool(MYSQL_CONF)
// 统一处理sql语句
function querySql(sql) {
  // 使用promise对象处理查询的数据
  const promise = new Promise((resolve, reject) =&amp;amp;gt; {
    // 从连接池里获取一个连接
    pool.getConnection((err, connection) =&amp;amp;gt; {
      if (err) {
        reject(err)
        return
      } else {
        connection.query(sql, (err, result) =&amp;amp;gt; {
          if (err) {
            reject(err)
            return
          } else {
            resolve(result)
          }
        })
      }
      // 释放本次连接
      connection.release()
    })
  })
  return promise
}
module.exports = {
  querySql
}
</code></pre>
<h2>参考</h2>
<p>https://zhuanlan.zhihu.com/p/30172660</p>
<p>https://blog.csdn.net/bob_baobao/article/details/82260541</p>

        ]]></description>
      </item><item>
        <title>JS事件循环之异步任务</title>
        <link>http://godlanbo.com/blogs/11</link>
        <guid>11</guid>
        <pubDate>Tue, 08 Sep 2020 11:49:12 GMT</pubDate>
        <description><![CDATA[
          <h3>什么是异步任务</h3>
<p>首先我们知道，js是单线程语言，事件循环是js的事件执行机制。因为js是单线程的所以就导致，如果一个任务执行时间太长，会导致后面的任务无法执行，进而卡住执行栈。所以在js中将任务分为两类：</p>
<ul>
<li>同步任务</li>
<li>异步任务</li>
</ul>
<p>同步任务直接添加到执行栈中，异步任务则在自己需要的准备工作完成后添加在执行栈中，这样就不会因为异步任务所需要的准备时间过长而导致执行阻塞。</p>
<h3>异步任务的执行</h3>
<p>异步任务在创建的时候，不会立即执行，而是会被注册到事件表（Event Table）中，然后继续执行主线程的同步任务，当异步任务准备就绪的时候，会将注册的回调函数放入任务队列（Event Queue）。当主线程当下的任务结束后，会去任务队列里面取新的任务，这个时候异步任务的回调函数就进入主线程执行。</p>
<p>上面这个过程会一直循环，也就是我们说的事件循环机制。</p>
<pre><code class="language-js">setTimeout(() =&amp;amp;gt; {
  console.log('回调函数执行')
}, 100)
console.log('主线程任务')
</code></pre>
<p>上面代码执行后打印顺序是：“主线程任务”，等待100ms后打印 “回调函数执行”。</p>
<p>有时候我们还能看到<code>setTimeout(callBack, 0)</code>,<code>setTimeout(callBack)</code>这样的写法，但是这种延迟为0的注册异步任务的形式并不代表它就会如同同步任务一样立即执行。</p>
<pre><code class="language-js">setTimeout(() =&amp;amp;gt; {
  console.log('回调函数执行')
}, 0)
console.log('主线程任务')
</code></pre>
<p>上面的代码的执行结果仍然是：“主线程任务”， “回调函数执行”。setTimeout第二参数的意思只是最少的等待时间，这个时间一到，就会把回调函数放入任务队列，但是是否立即执行，还需要看主线程是否为空，如果当前的主线程还有其他任务在执行，那么就要等待主线程空闲后，再去任务队列拿新的任务执行。所以<code>setTimeout(callBack, 0)</code>只是将callBack立即加入到任务队列，此时主线程的执行栈中已经有任务在执行，所以要等到主线程空闲后再执行。</p>
<h3>异步任务的分类</h3>
<p>异步任务分为两类：</p>
<ul>
<li>宏任务（setTimeout，setInterval，整段代码的script等）</li>
<li>微任务（promise的then，process.nextTick等）</li>
</ul>
<p>这两类任务都属于异步任务，他们的区别在于执行顺序的不同（此图转引自<a href="https://juejin.im/post/5b498d245188251b193d4059">这里</a>）：</p>
<p><img src="http://47.99.136.41:3000/images/1599565717665.jpg" alt="164974fa4b42e4af.jpg"></p>
<p>对于上面的执行顺序，我们看个简单的例子：</p>
<pre><code>console.log('start')
setTimeout(() =&amp;amp;gt; {
  console.log('第一個回调')
}, 0)

Promise.resolve().then(() =&amp;amp;gt; {
  console.log('promise1')
}).then(() =&amp;amp;gt; {
  console.log('promise2')
})

setTimeout(() =&amp;amp;gt; {
  console.log('第二个回调')
}, 0)

console.log('end')
</code></pre>
<p>首先整块代码当做一个宏任务执行，先执行同步任务，输出start，然后将第一个setTimeout添加到宏任务队列记做<code>setTimeout1</code>，然后填加promise的两个then到微任务队列我们记做<code>then1</code>和<code>then2</code>，再将第二个setTimeout添加到宏任务记做<code>setTimeout2</code>，最后输出end，此时的两类异步任务的队列情况如下：</p>
<table>
<thead>
<tr>
<th>宏任务队列（macrotasks queues）</th>
<th>微任务队列（microtasks queues）</th>
</tr>
</thead>
<tbody>
<tr>
<td>setTimeout1</td>
<td>then1</td>
</tr>
<tr>
<td>setTimeout2</td>
<td>then2</td>
</tr>
</tbody>
</table>
<p>这个时候整个代码宏任务完成，去微任务队列清空所有的微任务，输出promise1,promise2，然后拿出一个宏任务执行。如此重复上面的步骤。所以最后输出结果是：start, end, promise1, promise2, 第一個回调，第二个回调。</p>
<h3>结尾</h3>
<p>以上就是关于js异步任务的简单介绍。</p>
<h3>参考文献</h3>
<p>https://juejin.im/post/5b498d245188251b193d4059</p>
<p>https://juejin.im/post/59c25c936fb9a00a3f24e114</p>
<p>http://www.ruanyifeng.com/blog/2014/10/event-loop.html</p>

        ]]></description>
      </item><item>
        <title>JS-解决回调地狱</title>
        <link>http://godlanbo.com/blogs/10</link>
        <guid>10</guid>
        <pubDate>Tue, 08 Sep 2020 11:45:42 GMT</pubDate>
        <description><![CDATA[
          <p>JavaScript是单线程语言，但是js中有很多任务耗时比较长，比如ajax请求，如果都按照顺序进行，往往会出现浏览器无响应的情况，所以就需要异步的形式。我下面用Node.js中的读取文件来举例，node和JavaScript原理是差不多的。</p>
<p>先看看下面这个例子:</p>
<pre><code class="language-js">// a.json
// {
//   &amp;quot;next&amp;quot;: &amp;quot;b.json&amp;quot;,
//   &amp;quot;msg&amp;quot;: &amp;quot;this is A&amp;quot;
// }
const fs = require('fs')
fs.readFile('a.json', (err, data) =&amp;amp;gt; {
  if (err) {
    console.error(err)
    return
  }
  console.log(data.toString())
})

</code></pre>
<p>读取文件内容是个非常简单的异步操作，你得在文件内容读出来之后才能对文件内容进行操作。这是读取一个文件内容的异步过程，只有一个异步函数，回调函数读出的内容也没有拿出来给其他函数使用。</p>
<p>如果是几个异步函数连在一起呢？比如某一个读取文件的操作需要用到上一个读取文件读出来的内容。看下面这个例子：</p>
<pre><code class="language-js">// a.json
{
  &amp;quot;next&amp;quot;: &amp;quot;b.json&amp;quot;,
  &amp;quot;msg&amp;quot;: &amp;quot;this is A&amp;quot;
}
// b.json
{
  &amp;quot;next&amp;quot;: &amp;quot;c.json&amp;quot;,
  &amp;quot;msg&amp;quot;: &amp;quot;this is B&amp;quot;
}
// c.json
{
  &amp;quot;next&amp;quot;: null,
  &amp;quot;msg&amp;quot;: &amp;quot;this is C&amp;quot;
}

const fs = require('fs')
// 定义一个读取文件内容的函数
function getFileContent (filename, callback) {
  fs.readFile(filename, (err, data) =&amp;amp;gt; {
    if (err) {
      console.error(err)
      return
    }
    // callback是一个函数，参数是文件的内容
    callback(JSON.parse(data.toString()))
  })
}

getFileContent('a.json', aData =&amp;amp;gt; { // aData中拿到a文件的内容，在这个函数里
  console.log('a data is', aData)   // 才能调用aData.next访问b.json
  getFileContent(aData.next, bData =&amp;amp;gt; {  // 用前面拿到的a文件内容里的b文件名拿到b文件内容，下面继续
    console.log('b data is', bData)
    getFileContent(bData.next, cData =&amp;amp;gt; {
      console.log('c data is', cData)
    })
  })
})
</code></pre>
<p>看这里的异步过程产生的回调嵌套已经有好几层了，如果再多几个文件，再多几个异步过程，如山一般的回调就会在代码里形成回调地狱。</p>
<p>这个时候就轮到Promise来了。Promise 是异步编程的一种解决方案，它本身是一个对象，它代表了一个异步操作的最终完成的结果或者失败结果。</p>
<p>promise本身是一个构造函数，下面代码创造了一个<code>Promise</code>实例：</p>
<pre><code class="language-js">const promise = new Promise((resolve, reject) =&amp;amp;gt; {
	// some code
  if(/* 异步操作成功 */) {
  	resolve(value)
  } else { /* 异步操作失败（不是抛出错误，是异步条件不满足的情况下reject） */
    reject(error)
  }
})
</code></pre>
<p>promise接受两个参数，resolve, reject，这是两个js自带的函数，不用自己写。resolve的作用是将异步成功的内容传递出去，reject的作用相反，将异步失败时候的内容作为参数传递出去。</p>
<p>promise实例生成后，就可以调用它的then和catch方法，在then中，我们可以拿到resolve和reject中返回的内容，而then又返回一个新的promise实例，这就是它的强大之处，举个例子就好懂了，下面用promise对上面读文件进行重写：</p>
<pre><code class="language-js">const fs = require('fs')
// 获取文件的函数返回一个处理文件异步的promise
function getFileContent (filename) {
  const promise = new Promise((resolve, reject) =&amp;amp;gt; {
    fs.readFile(filename, (err, data) =&amp;amp;gt; {
      if (err) {
        reject(err)
        return
      }
      resolve(JSON.parse(data.toString())) // 将读取出来的文件内容用resolve传递出去
    })
  })
  return promise
}
// 调用这个promise，在then里面拿到了a文件的内容
getFileContent('a.json').then(aData =&amp;amp;gt; {
  console.log('a Data ', aData)
  // 返回一个新的promise，这个promise处理读取b文件的异步
  return getFileContent(aData.next)
}).then(bData =&amp;amp;gt; {  // 返回的是一个promise，所以可以直接链式调用then，这个then会拿到b的内容
  console.log('b Data ', bData)
  // 返回一个新的promise，这个promise处理读取c文件的异步
  return getFileContent(bData.next)
}).then(cData =&amp;amp;gt; {  // 链式调用
  console.log('c Data ', cData)
})
</code></pre>
<p>使用promise进行重写后，我们发现，再多的文件只不过是多几个链式调用而已，嵌套控制在一层，解决了回调地狱这个问题。</p>
<p>在上面的链式调用里，后一个then会等待前一个promise的结果产生后调用，如果进行中报错，链式调用就会断掉，这时候就用到promise的另一个方法，catch：</p>
<pre><code class="language-js">promise
  .then(...)
  .then(...)
  .then(...)
  .catch(...) // catch可以处理前面所有then的报错,处理之后可以继续链式调用
  .then(...)
</code></pre>
<p>一遇到异常抛出，Promise 链就会停下来，直接调用链式中的 <code>catch</code> 处理程序来继续当前执行。这上面的代码看起来或许有点像下面这样：</p>
<pre><code class="language-js">try {
  let result1 = syncDoSomething1();
  let result2 = syncDoSomething2(result1);
  let result3 = syncDoSomething3(result2);
} catch(error) {
  handle(error) // 处理error
}
</code></pre>
<p>哦，这样似乎有点同步任务的意思了，正好，在新的规定里可以用新的<code>async/await</code> 语法将这种类似同步编程处理异步逻辑的代码体现出来：</p>
<pre><code class="language-js">async function foo() {
  try {
    let result1 = await syncDoSomething1();
    let result2 = await syncDoSomething2(result1);
    let result3 = await syncDoSomething3(result2);
  } catch(error) {
    handle(error) // 处理error
  }
}
</code></pre>
<p>在async定义的函数中，你可以使用await语法来处理promise，await执行到这里之后，会等待promise中的异步处理完成之后直接取出promise的值，然后再继续执行。我们用这种方法再来优化一下我们开头说的读取文件的例子：</p>
<pre><code class="language-js">async function readFileData() {
  const aData = await getFileContent('a.json')
  console.log('a Data ', aData)
  const bData = await getFileContent(aData.next)
  console.log('b Data ', bData)
  const cData = await getFileContent(bData.next)
  console.log('c Data ', cData)
}
</code></pre>
<p>是不是一下子就感觉这个代码非常简单了，就像是在写同步任务一样没有任何回调的顾虑，await会帮我们拿到promise的内容，直接用就可以了。到最后，什么回调地狱完全就没有了。</p>
<h3>参考:</h3>
<p>https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Using_promises</p>

        ]]></description>
      </item><item>
        <title>CSS中动画的交替间隔</title>
        <link>http://godlanbo.com/blogs/9</link>
        <guid>9</guid>
        <pubDate>Tue, 08 Sep 2020 11:43:28 GMT</pubDate>
        <description><![CDATA[
          <p>一开始想做一个动画，交替循环播放，但是每次交替想让它间隔一点进行下一次交替，我以为animation中有这个属性，结果在MDN找了一圈发现没有，不想去做复杂的js逻辑，于是在网上翻找了一下，有个可行的解决办法。</p>
<h2>一、循环的间隔</h2>
<p>我们可以像如下这样定义动画：</p>
<pre><code class="language-css">@keyframes demo {
  0%, 75% { /* 原状态 */ }
  100% { /* 目标状态 */ }
}
</code></pre>
<p>然后将动画时间设定为4s，这样前75%的时间（3s）将没有动作，最后1s才将动画执行，效果相当于在1s内执行动画动作，但是每次执行都有3s的前摇，把这个动画连在一起，就可以组成有3s循环间隔的动画。但是这样写第一次也会有前摇，当然你可以把间隔时间写到后面去，然后动画执行在一开始，这样循环起来效果都是一样的，你也可以用animation-delay的负数值将第一次动画提前来解决。</p>
<h2>二、交替循环的间隔</h2>
<p>像以上那样将间隔写在动画周期的一端，动画写在一端的写法，只能达到循环的间隔，因为每次循环，动画周期重新开始，所以每次都有这个你设置好的间隔，但是我们有时也会交替循环一起用（alternate infinite），交替的时候是将动画周期<strong>倒着</strong>回来，这样如果是像上面那样间隔和动画动作各写一端，在交替循环的时候就会出现间隔翻倍，动画动作无间隔执行两次的情况。</p>
<p>&amp;gt;就像把火柴棍末端对末端，头对头连在一起，火柴梗长度就会翻倍，两个火柴头会直接相邻（+——++——+）</p>
<p>为了解决这种情况的间隔，可以把动画周期拆成三部分，将动画动作放在中间，两边放间隔时间，注意这里两边的间隔长度必须一样，否则在交替的时候还是会出现间隔时间不相等的问题，因为交替的时候是将动画周期<strong>倒着</strong>回来的。两侧的间隔加在一起就是每次交替循环的间隔。举个例子，我现在想要0.5s完成一个动画，1s的交替间隔，于是我可以设置动画执行时间为1.5s，然后定义动画：</p>
<pre><code class="language-css">@keyframes demo {
  0%, 33% { /* 原状态 */ }
  66%, 100% { /* 目标状态 */ }
}
</code></pre>
<p>这样前1/3时间没变化，中间1/3时间执行动画，后1/3时间也没有变化。当然这种写法，就必须要在第一次执行的时候使用负值的animation-delay来抵消一边的间隔。</p>
<h2>三、如何计算时间</h2>
<p>对于第一种循环的延迟，我们动画的执行时间就等于 &amp;lt;u&amp;gt;你想要的间隔时间 + 你的动画动作执行时间&amp;lt;/u&amp;gt;。然后动画设定就按照相应的时间百分比去设定就好了。动画动作和间隔的前后顺序看你心情，当然如果间隔在前，可能你会需要用animation-delay去抵消第一次的间隔好让动画立即执行。</p>
<p>对于第二种交替循环的间隔，动画的执行之间依然等于 &amp;lt;u&amp;gt;你想要的间隔时间 + 你的动画动作执行时间&amp;lt;/u&amp;gt;。然后将间隔的一半分别设在动画周期的两侧，取相应百分比，然后第一次用animation-delay取间隔一半的负数去抵消第一次的动画的起始侧间隔。</p>
<h2>参考</h2>
<p>https://segmentfault.com/q/1010000000321090</p>

        ]]></description>
      </item><item>
        <title>CSS中文本过多省略显示</title>
        <link>http://godlanbo.com/blogs/8</link>
        <guid>8</guid>
        <pubDate>Sat, 05 Sep 2020 08:45:17 GMT</pubDate>
        <description><![CDATA[
          <p>有两种，一种是一行的省略：</p>
<p>&amp;gt; 这是一行文字...</p>
<p>一种是多行的省略：</p>
<p>&amp;gt; 这是很多行文字
&amp;gt; 很多行文字...</p>
<p>下面介绍两种的css写法</p>
<h2>一、一行式的</h2>
<pre><code class="language-css">.element {
  text-overflow: ellipsis;
  white-space: nowrap;
  overflow: hidden;
}
</code></pre>
<p><code>text-overflow</code>表示文本超出后的显示样式，设置为ellipsis表示文本超出后显示三个点，然后是<code>white-space</code>表示如何处理文本的空格，设置为nowrap后将不再换行，达到一行内显示的效果，最后是<code>overflow</code>，表示内容物超出容器后的处理方式，设置为hidden表示隐藏。</p>
<p>这里要注意的是，要设置这种省略方式，元素必须设置宽度，不然整个文本将不换行的一排显示，甚至超出你的视窗。只有设置了定宽上面的css才会生效，不换行的文本遇到定宽容器，一定超出，然后超出隐藏，文本超出后显示三个点，这就是这个过程。</p>
<h2>二、多行式的</h2>
<pre><code class="language-css">.element {
  overflow : hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
}
</code></pre>
<p>想要实现复数行的文本超出省略需要用到旧版的flex布局：display： box，由于已经是旧版了，所以使用的时候需要加上对应前缀。</p>
<p>首先需要对容器设置overflow : hidden，这样才会在line-clamp的限制下换行；line-clamp可以容器中的内容限制为指定的行数。当文本超出这个行数后，会自动的显示三个小点的省略，所以不需要设置text-overflow。box-orient: vertical 表示文本行垂直排布。</p>

        ]]></description>
      </item></channel>
      </rss>