四、组件编译
组件的思想也是 Vue 核心,将组件编译为 vdom ,则也是一重难点!
你可以发现在 Vue 这一节有很多引用的图,其实它们有的相似,更多的是侧重点不同,建议都可按照流程图理解理解,做到融会贯通。
组件
官方示例:
// 定义一个名为 button-counter 的新组件 Vue.component('button-counter', { data: function () { return { count: 0 } }, template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>' }) <div id="components-demo"> <button-counter></button-counter> </div> new Vue({ el: '#components-demo' })
组件还涉及:组件之间的通信、插槽、动态组件等内容。后续再表(OS: 这是自己给自己挖了个多大的坑)。
parse
编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。这个过程会用到大量的正则表达式对字符串解析,源码很难读。
但是我们需要知道的是 start、end、comment、chars 四大函数。
对于普通标签的处理流程大致:
- 识别开始标签,生成匹配结构match。
- 处理attrs,将数组处理成 {name:'xxx',value:'xxx'}
- 生成astElement,处理for,if和once的标签。
- 识别结束标签,将没有闭合标签的元素一起处理。
- 建立父子关系,最后再对astElement做所有跟Vue 属性相关对处理。slot、component等等。
参考阅读:Vue parse之 从template到astElement 源码详解
optimize
当我们的模板 template 经过 parse 过程后,会输出生成 AST 树,那么接下来我们需要对这颗树做优化 —— optimize。
optimize 中最重要的是标记静态根(markStaticRoots )、静态节点(markStatic )。如果是静态节点则它们生成的DOM永远不需要改变,这让模板的更新更搞笑(不变的节点不用更新)。
问题:为什么子节点的元素类型是静态文本类型,就会给 optimize 过程加大成本呢? 首先来分析一下,之所以在 optimize 过程中做这个静态根节点的优化,目的是什么,成本是什么?
目的:在 patch 过程中,减少不必要的比对过程,加速更新。
成本:a. 需要维护静态模板的存储对象。b. 多层render函数调用。
codegen
编译的最后一步就是把优化后的 AST 树转换成可执行的代码,即在 codegen 环节。
主要步骤(调用函数):
- generate
- genIf
- genFor
- genData & genChildren
此节考的不多,仅做了解。了解更多,得看源码。
五、常用补充
keep-alive(常考)
keepalive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染 。也就是所谓的组件缓存。
v-if、v-show、v-for
三个高频子问题:
- v-if、v-show 区别?
答:v-if 相当于 display; v-show 相当于 visibility; 前者会控制是否创建,后者仅控制是否隐藏显示。
- v-if、v-for 为什么不能放一起用?
答:因为 v-for 优先级比 v-if 高,所以在每次重新渲染的时候会先遍历整个列表,再进行 if 判断是否展示,消耗性能。
- v-for 中能用 index 作 key 吗?
答:key 是 diff 算法中用来对比的,用 index 作为 key 并未唯一识别,当插入元素时,key 也会变化。index 作为 key,只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出(官网说明)。
本瓜这里不做细答,想了解更多请自行解决。
自定义指令
Vue 中的混入(Minxin)、自定义指令(directive)、过滤器(filter)有共通之处,在注册的时候,需要平衡局部注册和全局注册的优劣。
transition
本瓜读源码的过程中,发现源码中有较大的篇幅在描述关于transition。在官方文档中,transition 也是作为独立的重要一节来说明。进入/离开 & 列表过渡,Vue 动画&过渡,是容易忽视的点。
六、全家桶
自从用上了 Vue 全家桶,腿也不疼了,腰也不酸了。咦,一口气写五个页面,妈妈再也不用担心我的学习了。(好像有点串广告了......)
Vue-Router
分为两种模式:hash 和 history
- 默认 hash 模式,通过加锚点的方式
- history 利用 history.pushState API实现
原生: HTML5引入了 history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate 配合使用。详情链接
Vuex
由以下几部分核心组成:
- state:数据状态;
- mutations:更改状态(计算状态);
- getters:将state中的某个状态进行过滤然后获取新的状态;
- actions:执行多个mutation,它可以进行异步操作(async );
- modules:把状态和管理规则分类来装,让目录结构更清晰;
VueCLI
- VueCLI4中很重要的是 vue.config.js 这个文件的配置。
VuePress
- 采用 Vue + webpack,可以在 Markdown 中使用 Vue 组件,页面简洁大方,与 Vue 官网风格统一。
NuxtJS
七、webpack
只要你做前端有两年经验左右,那一样就得要求自己掌握一款打包工具了。
webpack 原理
官方解释:
webpack 是一个现代 JavaScript 应用程序的静态模块打包工具。当 webpack 处理应用程序时,它会在内部构建一个 依赖图(dependency graph),此依赖图会映射项目所需的每个模块,并生成一个或多个_bundle_。
核心概念:
- Entry:入口,Webpack 执行构建的第一步将从 Entry 开始,可抽象成输入。
- Module:模块,在 Webpack 里一切皆模块,一个模块对应着一个文件。Webpack 会从配置的 Entry 开始递归找出所有依赖的模块。
- Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割。
- Loader:模块转换器,用于把模块原内容按照需求转换成新内容。
- Plugin:扩展插件,在 Webpack 构建流程中的特定时机会广播出对应的事件,插件可以监听这些事件的发生,在特定时机做对应的事情。
功能:
代码转换、文件优化、代码分割、模块合并、自动刷新、代码校验、自动发布。
优化打包速度
- 搭载 webpack-parallel-uglify-plugin 插件,加速“压缩JS=>编译成 AST=>还原JS”的过程。
- 使用 HappyPack 提升 loader 解析速度。
- 使用 DLLPlugin 和 DLLReferencePlugin 插件,提前打包。
- tree-shaking 用来消除无用模块。
AMD、CMD
前端模块化有四种规范:CommonJS、AMD、CMD、ES6。
- AMD(异步模块定义)
- CMD(通用模块定义)
AMD(异步模块定义) | CMD(通用模块定义) |
速度快 | 性能较差 |
会浪费资源 | 只有真正需要才加载依赖 |
预先加载所有的依赖,直到使用的时候才执行 | 直到使用的时候才定义依赖 |
- Node.js是commonJS规范的主要实践者:module、module.exports(exports)、require、global。
- ES6 在语言标准的层面上,实现了模块功能,主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
问:比较 import 和 require 的区别?
import | require |
ES6标准中的模块化解决方案 | 是node中遵循CommonJS规范的模块化解决方案 |
不支持动态引入 | 支持动态引入 |
是关键词 | 不是关键词 |
编译时加载,必须放在模块顶部 | 运行时加载,理论上来说放在哪里都可以 |
性能较好 | 性能较差 |
实时绑定方式,即导入和导出的值都指向同一个内存地 | 导出时是值拷贝,就算导出的值变化了,导入的值也不会变化 |
会编译成require/exports来执行 | - |
实现plugin插件(腾讯WXG考点)
- 创建 plugins/demo-plugin.js 文件;
- 传递参数 Options;
- 通过 Compilation 写入文件;
- 管理 Warnings 和 Errors
串联四:
你最喜欢什么算法?
其实本瓜想回答:我最喜欢减法!因为幸福生活需要用减法。😄
算法这一个 part 也已久远,既然逃不掉,那就正面挑战它!其实也没那么难。
一图胜万言
- 原创脑图,转载请说明出处
串联知识点:数据结构、基础算法、排序算法、进阶算法。
串联记忆:
算法算法我不怕
数据结构打趴下
遍历排序我最溜
指针动态贪心刷
一、数据结构
队列
先入先出。
栈
先入后出。
堆
堆通常是一个可以被看做一棵树的数组对象。
堆总是满足下列性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
- 完全二叉树:在一颗二叉树中,若除最后一层外的其余层都是满的,并且最后一层要么是满的,要么在右边缺少连续若干节点,则此二叉树为完全二叉树(Complete Binary Tree)—— wiki。
(本瓜曾被拷问过这个点,大厂就是会考《数据结构》,别逃避,出来混迟早是要还的😭)。
链表
- 循环列表(考点)
// 循环链表 function Node(element){ this.element = element; this.prev = null; this.next = null; } function display(){ var current = this.head; //检查头节点当循环到头节点时退出循环 while(!(current.next == null) && !(current.next.element=='head')){ print(current.next.element); current = current.next; } } function Llist(){ this.head = new Node('head'); this.head.next = this.head; this.find = find; this.insert = insert; this.display = display; this.findPrevious = findPrevious; this.remove = remove; }
哈希
散列函数(英语:Hash function)又称散列算法、哈希函数。以 Key:Value 的方式存储数据。
哈希表最大的特点是可以快速定位到要查找的数据,查询的时间复杂度接近O(1)。本瓜建议大家可以把这里所有的数据结构的“增删改查”操作的时间复杂度都理一下,也是会被考的。
时间复杂度、空间复杂度
简单理解:
- 循环的次数写成 n 的表达式,就是时间复杂度。
- 申请的变量数量写成 n 的表达式,就是空间复杂度。
时间复杂度更多重要一点,常见的时间复杂度:O(1)、O(n)、O(logn)、O(n2)。本瓜小TIP:面试/笔试如果不知道怎么算,就在这里面猜吧。实在不行就答:O(n) ~ O(n2) 之间,大概率不会错🐶。
树的遍历(广度、深度)
广度优先遍历(BFS):
需要用到队列(Queue)来存储节点对象,队列的特点就是先进先出。示例
深度优先遍历(DFS):
- 前序(根结点 -> 左子树 -> 右子树)
- 中序(左子树 -> 根结点 -> 右子树)
- 后序(左子树 -> 右子树 -> 根结点)
二、基础算法
递归思想
- 著名的斐波那契数列,你要知道!
function result(){ if(n==1||n==2){ return 1 } return reslt(n-2)+result(n-1) }
- 函数柯里化,你也要知道!
函数柯里化:是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。—— wiki
// 普通方式 var add1 = function(a, b, c){ return a + b + c; } // 柯里化 var add2 = function(a) { return function(b) { return function(c) { return a + b + c; } } } // demo var foo = function(x) { return function(y) { return x + y } } foo(3)(4) // 7
这样处理参数,可以直接追加,而不需要初始传参就写完全。这里只是浅谈,更多请自行探索。
二分法
要求手写二分法,实在不行,能默写也可以啊!
// 二分法:先排序,再找目标 function binary_search(arr,target) { let min=0 let max=arr.length-1 while(min<=max){ let mid=Math.ceil((min+max)/2) if(arr[mid]==target){ return mid }else if(arr[mid]>target){ max=mid-1 }else if(arr[mid]<target){ min=mid+1 } } return "null" } console.log(binary_search([1,5,7,19,88],19))//3
三、排序算法
排序是比较常用也比较重要的一块,此处并未全列出。仅强调快排和冒泡,会用双循环也行啊。
快速排序
// 快排:选取基准,比基准大的放右边,比基准小的放左边,然后两边用递归 function quickSort(arr, i, j) { if(i < j) { let left = i; let right = j; let pivot = arr[left]; while(i < j) { while(arr[j] >= pivot && i < j) { // 从后往前找比基准小的数 j--; } if(i < j) { arr[i++] = arr[j]; } while(arr[i] <= pivot && i < j) { // 从前往后找比基准大的数 i++; } if(i < j) { arr[j--] = arr[i]; } } arr[i] = pivot; quickSort(arr, left, i-1); quickSort(arr, i+1, right); return arr; } }
冒泡排序
// 冒泡:双层循环 var arr=[10,20,50,100,40,200]; for(var i=0;i<arr.length-1;i++){ for(var j=0;j<arr.length-1-i;j++){ if(arr[j]>arr[j+1]){ var temp=arr[j] arr[j]=arr[j+1] arr[j+1]=temp } } } console.log(arr)
四、进阶算法
双指针
看到“有序”和“数组”。立刻把双指针法调度进你的大脑内存。普通双指针走不通,立刻想对撞指针!
示例:合并两个有序数组(双指针解法)
示例: 输入: nums1 = [1,2,3,0,0,0], m = 3 nums2 = [2,5,6], n = 3 输出: [1,2,2,3,5,6]
/** * @param {number[]} nums1 * @param {number} m * @param {number[]} nums2 * @param {number} n * @return {void} Do not return anything, modify nums1 in-place instead. */ const merge = function(nums1, m, nums2, n) { // 初始化两个指针的指向,初始化 nums1 尾部索引k let i = m - 1, j = n - 1, k = m + n - 1 // 当两个数组都没遍历完时,指针同步移动 while(i >= 0 && j >= 0) { // 取较大的值,从末尾往前填补 if(nums1[i] >= nums2[j]) { nums1[k] = nums1[i] i-- k-- } else { nums1[k] = nums2[j] j-- k-- } } // nums2 留下的情况,特殊处理一下 while(j>=0) { nums1[k] = nums2[j] k-- j-- } };
高级!
动态规划
动态规划的思想就是把一个大的问题进行拆分,细分成一个个小的子问题,且能够从这些小的子问题的解当中推导出原问题的解。
同时需要满足以下两个重要性质才能进行动态规划:
- 最优子结构性
- 子问题重叠性质
动态规划实例:斐波拉契数列(上文有提到)
斐波拉契数列:采用递归,虽然代码很简洁,但是明显随着次数的增加,导致递归树增长的非常庞大,耗时较久。
用动态规划实现斐波拉契数列,代码如下
function feiBoLaQie(n) { //创建一个数组,用于存放斐波拉契数组的数值 let val = []; //将数组初始化,将数组的每一项都置为0 for(let i =0 ;i<=n;i++){ val[i] = 0; } if (n==1 || n==2){ return 1; } else{ val[1] = 1; val[2] = 2; for (let j =3; j<=n;j++){ val[j] = val[j-1] + val[j-2]; } } return val[n-1]; } console.log(feiBoLaQie(40));//102334155
通过数组 val 中保存了中间结果, 如果要计算的斐波那契数是 1 或者 2, 那么 if 语句会返回 1。 否则,数值 1 和 2 将被保存在 val 数组中 1 和 2 的位置。
循环将会从 3 到输入的参数之间进行遍历, 将数组的每个元素赋值为前两个元素之和, 循环结束, 数组的最后一个元素值即为最终计算得到的斐波那契数值, 这个数值也将作为函数的返回值。
动态规划解决速度更快。
贪心算法
贪心算法遵循一种近似解决问题的技术,期盼通过每个阶段的局部最优选择(当前最好的解),从而达到全局的最优(全局最优解)。
注:贪心得到结果是一个可以接受的解,不一定总是得到最优的解。
示例:最少硬币找零问题
是给出要找零的钱数,以及可以用硬币的额度数量,找出有多少种找零方法。 如:美国面额硬币有:1,5,10,25 我们给36美分的零钱,看能得怎样的结果? 复制代码
function MinCoinChange(coins){ var coins = coins; var cache = {}; this.makeChange = function(amount){ var change = [], total = 0; for(var i = coins.length; i >= 0; i--){ var coin = coins[i]; while(total + coin <= amount){ change.push(coin); total += coin; } } return change; } } var minCoinChange = new MinCoinChange([1, 5, 10, 25]); minCoinChange.makeChange(36); //[25, 10, 1] 即一个25美分、一个10美分、一个1美分
串联五:
web 安全你知道那些?
一图胜万言
- 原创脑图,转载请说明出处
串联知识点:跨域、XSS(跨站脚本攻击)、CRFS(跨站请求伪造)、SQL 注入、DNS 劫持、HTTP 劫持。
串联记忆:三跨两劫持一注入
一、跨域
跨域定义
当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。
跨域限制 是浏览器的一种保护机制,若跨域,则:
- 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB。
- 无法接触非同源网页的 DOM。
- 无法向非同源地址发送 AJAX 请求
跨域解决
跨域解决:
- JSONP;
- CORS(跨域资源分享);
// 普通跨域请求:只需服务器端设置 Access-Control-Allow-Origin。 // 带cookie跨域请求:前后端都需要进行设置。 // 如前端在 axios 中设置 axios.defaults.withCredentials = true
- vue项目 设置 proxy 代理;
- nginx 代理;
- 设置document.domain解决无法读取非同源网页的 Cookie问题;
- 跨文档通信 API:window.postMessage();
二、XSS
XSS 原理
XSS的原理是WEB应用程序混淆了用户提交的数据和JS脚本的代码边界,导致浏览器把用户的输入当成了JS代码来执行。XSS的攻击对象是浏览器一端的普通用户。
示例:
<input type="text" value="<%= getParameter("keyword") %>"> <button>搜索</button> <div> 您搜索的关键词是:<%= getParameter("keyword") %> </div>
当浏览器请求
http://xxx/search?keyword="><script>alert('XSS');</script>
恶意代码,就会其执行
反射型 XSS
存储型 XSS 的攻击步骤:
- 攻击者将恶意代码提交到目标网站的数据库中。
- 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
存储型 XSS
反射型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
- 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
DOM型 XSS
DOM 型 XSS 的攻击步骤:
- 攻击者构造出特殊的 URL,其中包含恶意代码。
- 用户打开带有恶意代码的 URL。
- 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行。
- 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞。
XSS 防御
- 输入过滤,不要相信任何客户端的输入;
- 对 HTML 做充分转义;
- 设置 HTTP-only:禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
- 验证码:防止脚本冒充用户提交危险操作。
- 谨慎使用:.innerHTML、.outerHTML、document.write();
三、CSRF
CSRF 原理
跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求。利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目的。
曾在 09 年发生了著名的“谷歌邮箱窃取”事件,利用的就是 “CSRF”。
主要流程:
- 受害者登录a.com,并保留了登录凭证(Cookie)。
- 攻击者引诱受害者访问了b.com。
- b.com 向 a.com 发送了一个请求:a.com/act=xx。浏览器会默认携带a.com的Cookie。
- a.com接收到请求后,对请求进行验证,并确认是受害者的凭证,误以为是受害者自己发送的请求。
- a.com以受害者的名义执行了act=xx。
- 攻击完成,攻击者在受害者不知情的情况下,冒充受害者,让a.com执行了自己定义的操作。
注:攻击者无法直接窃取到用户的信息(Cookie,Header,网站内容等),仅仅是冒用 Cookie 中的信息。
这告诉我们冲浪的时候不能随便点链接,是有风险哒。
CSRF 防御
防御策略:
- 自动防御策略:同源检测(Origin 和 Referer 验证)。
- 主动防御措施:Token验证 或者 双重Cookie验证 以及配合Samesite Cookie。
- 保证页面的幂等性,后端接口不要在GET页面中做用户操作。
四、SQL 注入
SQL 注入原理
Sql 注入攻击是通过将恶意的 Sql 查询或添加语句插入到应用的输入参数中,再在后台 Sql 服务器上解析执行进行的攻击,它目前黑客对数据库进行攻击的最常用手段之一。
SQL 注入防御
防御:用sql语句预编译和绑定变量,是防御sql注入的最佳方法。还可以通过严格检查参数的数据类型的方式来防御。
五、DNS 劫持
DNS 劫持原理
DNS劫持又称域名劫持,是指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能访问或访问的是假网址。其实本质就是对DNS解析服务器做手脚
DNS 劫持防御
解决办法:
DNS的劫持过程是通过攻击运营商的解析服务器来达到目的。我们可以不用运营商的DNS解析而使用自己的解析服务器或者是提前在自己的App中将解析好的域名以IP的形式发出去就可以绕过运营商DNS解析,这样一来也避免了DNS劫持的问题。
六、HTTP 劫持
HTTP 劫持原理
在运营商的路由器节点上,设置协议检测,一旦发现是HTTP请求,而且是html类型请求,则拦截进行恶意处理。 常见有两种:
- 类似DNS劫持返回302让用户浏览器跳转到另外的地址。(钓鱼网站就是这么干)
- 在服务器返回的HTML数据中插入js或dom节点(广告)。(常见)
HTTP 劫持防御
防御方法:
- 使用HTTPS;
- 使用禁止转码申明;
- 在开发的网页中加入代码过滤:用 js 检查所有的外链是否属于白名单;
- 联系运营商;
这一 part 更多的是概念上的东西,不求完全记住,但是也要大致上能说一下。咱老话说的好:知之为知之 不知为不知,面试官在非关键性的问题上是不做严格要求的,千万别满嘴跑火车或者直接说不知道,这是两个极端,都不可取。
小结
显然,前端面试“串联”问题不仅局限以上,本瓜会继续按照这个“串联联想”的思路去整理,不断去打破体系、形成体系(自己给自己挖的天坑呐)。期待在这个反复的过程中找寻真理的痕迹!