├── docs ├── guide │ ├── babel.md │ ├── composite.md │ ├── dataStructure.md │ ├── designPatterns.md │ ├── node.md │ ├── preface.md │ ├── common.md │ ├── httpWritten.md │ ├── book.md │ ├── framework.md │ ├── dom.md │ ├── README.md │ ├── webpackPluginDesign.md │ ├── hr.md │ ├── string.md │ ├── hoisting.md │ ├── project.md │ ├── component.md │ ├── memory.md │ ├── reactivity.md │ ├── event.md │ ├── htmlBasic.md │ ├── componentCli.md │ ├── webpackMoudle.md │ ├── execute.md │ ├── deepclone.md │ ├── vue.md │ ├── webpack.md │ ├── mechanism.md │ ├── carousel.md │ ├── engineering.md │ ├── immutable.md │ └── resume.md ├── .DS_Store ├── .vuepress │ ├── styles │ │ ├── palette.styl │ │ └── index.styl │ ├── nav │ │ └── zh.js │ ├── components │ │ ├── OtherComponent.vue │ │ ├── Foo │ │ │ └── Bar.vue │ │ ├── demo-1.vue │ │ ├── svg-container.vue │ │ └── UpgradePath.vue │ ├── public │ │ ├── icons │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── mstile-150x150.png │ │ │ ├── apple-touch-icon.png │ │ │ ├── android-chrome-192x192.png │ │ │ ├── android-chrome-512x512.png │ │ │ ├── apple-touch-icon-120x120.png │ │ │ ├── apple-touch-icon-152x152.png │ │ │ ├── apple-touch-icon-180x180.png │ │ │ ├── apple-touch-icon-60x60.png │ │ │ ├── apple-touch-icon-76x76.png │ │ │ └── msapplication-icon-144x144.png │ │ ├── manifest.json │ │ └── logo.svg │ ├── enhanceApp.js │ ├── images │ │ └── logo.svg │ └── config.js ├── README.md └── logo.svg ├── .DS_Store ├── .temp ├── internal │ ├── global-ui.js │ ├── layout-components.js │ ├── root-mixins.js │ ├── app-enhancers.js │ └── page-components.js ├── app-enhancers │ ├── 0.js │ ├── 3.js │ ├── 4.js │ ├── 2.js │ ├── 5.js │ ├── 1.js │ ├── data-block.js │ └── global-components-1.js ├── palette.styl ├── style.styl └── transform │ └── ClientComputedMixin.js ├── deploy.sh ├── count.js ├── package.json ├── .gitignore └── README.md /docs/guide/babel.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/guide/composite.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/guide/dataStructure.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/.DS_Store -------------------------------------------------------------------------------- /.temp/internal/global-ui.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | "BackToTop", 3 | "SWUpdatePopup" 4 | ] -------------------------------------------------------------------------------- /docs/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.DS_Store -------------------------------------------------------------------------------- /docs/.vuepress/styles/palette.styl: -------------------------------------------------------------------------------- 1 | // placeholder for test, dont't remove it. 2 | 3 | //$accentColor = #f00 4 | -------------------------------------------------------------------------------- /docs/.vuepress/nav/zh.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | text: '指南', 4 | link: '/guide/', 5 | }, 6 | ] 7 | -------------------------------------------------------------------------------- /.temp/app-enhancers/0.js: -------------------------------------------------------------------------------- 1 | export { default } from "/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/.vuepress/enhanceApp.js" -------------------------------------------------------------------------------- /docs/.vuepress/components/OtherComponent.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /.temp/app-enhancers/3.js: -------------------------------------------------------------------------------- 1 | export { default } from "/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/plugin-back-to-top/enhanceAppFile.js" -------------------------------------------------------------------------------- /.temp/app-enhancers/4.js: -------------------------------------------------------------------------------- 1 | export { default } from "/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/plugin-pwa/lib/enhanceAppFile.js" -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.vuepress/public/icons/favicon-16x16.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.vuepress/public/icons/favicon-32x32.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.vuepress/public/icons/mstile-150x150.png -------------------------------------------------------------------------------- /.temp/app-enhancers/2.js: -------------------------------------------------------------------------------- 1 | import "/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/plugin-nprogress/enhanceAppFile.js" 2 | export default {} -------------------------------------------------------------------------------- /.temp/app-enhancers/5.js: -------------------------------------------------------------------------------- 1 | export { default } from "/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/plugin-google-analytics/enhanceAppFile.js" -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.vuepress/public/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /.temp/app-enhancers/1.js: -------------------------------------------------------------------------------- 1 | import "/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/core/lib/node/internal-plugins/style/client.js" 2 | export default {} -------------------------------------------------------------------------------- /.temp/palette.styl: -------------------------------------------------------------------------------- 1 | // Theme's Palette 2 | // null 3 | 4 | // User's Palette 5 | @import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/.vuepress/styles/palette.styl") -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.vuepress/public/icons/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.vuepress/public/icons/android-chrome-512x512.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.vuepress/public/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.vuepress/public/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.vuepress/public/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.vuepress/public/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.vuepress/public/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /docs/.vuepress/public/icons/msapplication-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elecrabbit/front-end-interview/HEAD/docs/.vuepress/public/icons/msapplication-icon-144x144.png -------------------------------------------------------------------------------- /.temp/app-enhancers/data-block.js: -------------------------------------------------------------------------------- 1 | export default ({ Vue }) => { Vue.mixin({ 2 | computed: { 3 | $dataBlock() { 4 | return this.$options.__data__block__ 5 | } 6 | } 7 | }) } -------------------------------------------------------------------------------- /docs/.vuepress/enhanceApp.js: -------------------------------------------------------------------------------- 1 | export default ({ Vue, isServer }) => { 2 | if (!isServer) { 3 | import('vue-toasted' /* webpackChunkName: "notification" */).then((module) => { 4 | Vue.use(module.default) 5 | }) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.temp/style.styl: -------------------------------------------------------------------------------- 1 | // Theme's Styles 2 | @import("/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/theme-default/styles/index.styl") 3 | 4 | // User's Styles 5 | @import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/.vuepress/styles/index.styl") -------------------------------------------------------------------------------- /docs/.vuepress/components/Foo/Bar.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /docs/.vuepress/components/demo-1.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /docs/.vuepress/components/svg-container.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | -------------------------------------------------------------------------------- /docs/guide/designPatterns.md: -------------------------------------------------------------------------------- 1 | # 设计模式 2 | 3 | 设计模式实际上是前人依据经验总结而成的,一套被 反复使用、多数人知晓、经过分类编目的、代码设计经验的总结。 4 | 5 | 通用的设计模式就是『四人帮』搞出的那经典的23套,而设计模式的重度用户就是传统的面向对象语言c++/java等,由于语言特性和使用场景的差异,很多设计模式并不适合JavaScript。 6 | 7 | 因此我们只总结了10个最常用的设计模式。 8 | 9 | > 更详细的JavaScript设计模式请阅读[JavaScript设计模式与开发实践](https://book.douban.com/subject/26382780/) 10 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroImage: logo.svg 4 | actionText: 开始 → 5 | actionLink: /guide/ 6 | features: 7 | - title: 简洁至上 8 | details: 追求重点和难点,剔除陈旧的知识。 9 | - title: 清晰易懂 10 | details: 将复杂的问题一一拆解。 11 | - title: 紧跟热点 12 | details: 当下热点技术一网打尽。 13 | footer: MIT Licensed | Copyright © 2019-present xiaomuzhu 14 | --- 15 | -------------------------------------------------------------------------------- /docs/guide/node.md: -------------------------------------------------------------------------------- 1 | # Node.js面试题 2 | 3 | Node.js专业的面试题在网上非常少,毕竟专职的Nodejs工程师依然不能跟Java PHP等传统后端工程师的数量去对比,目前唯一有含金量的面试题就是饿了么[Node interview of ElemeFE](https://github.com/ElemeFE/node-interview),我们的题目主要出自这里. 4 | 5 | 值得一提的是,由于Node.js与前端共用JavaScript一门语言,所以[Node interview of ElemeFE](https://github.com/ElemeFE/node-interview)中的面试题很多与本指南前端基础部分的面试题是类似的,我们仅列举了Node.js专属部分的面试题. 6 | 7 | -------------------------------------------------------------------------------- /.temp/internal/layout-components.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generated by "@vuepress/internal-layout-components" 3 | */ 4 | export default { 5 | "NotFound": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/theme-default/layouts/404.vue"), 6 | "Layout": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/theme-default/layouts/Layout.vue") 7 | } -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 终止一个错误 4 | set -e 5 | 6 | # 构建 7 | npm run build 8 | 9 | # 进入生成的构建文件夹 10 | cd docs/.vuepress/dist 11 | 12 | # 如果你是要部署到自定义域名 13 | # echo 'www.cxymsg.com/fed' > CNAME 14 | 15 | git init 16 | git add -A 17 | git commit -m 'deploy' 18 | 19 | # 如果你想要部署到 https://.github.io 20 | git push -f git@github.com:xiaomuzhu/xiaomuzhu.github.io.git master 21 | 22 | # 如果你想要部署到 https://.github.io/ 23 | # git push -f git@github.com:/.git master:gh-pages 24 | 25 | cd - -------------------------------------------------------------------------------- /docs/.vuepress/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "VuePress", 3 | "short_name": "VuePress", 4 | "icons": [ 5 | { 6 | "src": "/icons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/icons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "/index.html", 17 | "display": "standalone", 18 | "background_color": "#fff", 19 | "theme_color": "#3eaf7c" 20 | } 21 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.styl: -------------------------------------------------------------------------------- 1 | // placeholder for test, dont't remove it. 2 | 3 | //.content { 4 | // font-size 30px; 5 | //} 6 | 7 | pre.vue-container 8 | border-left-width: .5rem; 9 | border-left-style: solid; 10 | border-color: #42b983; 11 | border-radius: 0px; 12 | & > code 13 | font-size: 14px !important; 14 | & > p 15 | margin: -5px 0 -20px 0; 16 | code 17 | background-color: #42b983 !important; 18 | padding: 3px 5px; 19 | border-radius: 3px; 20 | color #000 21 | em 22 | color #808080 23 | font-weight light 24 | 25 | -------------------------------------------------------------------------------- /.temp/internal/root-mixins.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generated by "@vuepress/internal-root-mixins" 3 | */ 4 | import m0 from "/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/core/lib/client/root-mixins/updateMeta.js" 5 | import m1 from "/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/plugin-active-header-links/clientRootMixin.js" 6 | import m2 from "/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/plugin-nprogress/clientRootMixin.js" 7 | import m3 from "/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/plugin-medium-zoom/clientRootMixin.js" 8 | 9 | export default [ 10 | m0, 11 | m1, 12 | m2, 13 | m3 14 | ] -------------------------------------------------------------------------------- /docs/.vuepress/components/UpgradePath.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /.temp/app-enhancers/global-components-1.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | Vue.component("OtherComponent", () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/.vuepress/components/OtherComponent")) 3 | Vue.component("UpgradePath", () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/.vuepress/components/UpgradePath")) 4 | Vue.component("demo-1", () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/.vuepress/components/demo-1")) 5 | Vue.component("svg-container", () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/.vuepress/components/svg-container")) 6 | Vue.component("Foo-Bar", () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/.vuepress/components/Foo/Bar")) 7 | Vue.component("Badge", () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/node_modules/@vuepress/theme-default/global-components/Badge")) 8 | 9 | 10 | export default {} -------------------------------------------------------------------------------- /.temp/internal/app-enhancers.js: -------------------------------------------------------------------------------- 1 | import m0 from "/Users/dxy/Documents/dxy-gzh/front-end-interview/.temp/app-enhancers/0.js" 2 | import m1 from "/Users/dxy/Documents/dxy-gzh/front-end-interview/.temp/app-enhancers/1.js" 3 | import m2 from "/Users/dxy/Documents/dxy-gzh/front-end-interview/.temp/app-enhancers/data-block.js" 4 | import m3 from "/Users/dxy/Documents/dxy-gzh/front-end-interview/.temp/app-enhancers/global-components-2.js" 5 | import m4 from "/Users/dxy/Documents/dxy-gzh/front-end-interview/.temp/app-enhancers/2.js" 6 | import m5 from "/Users/dxy/Documents/dxy-gzh/front-end-interview/.temp/app-enhancers/3.js" 7 | import m6 from "/Users/dxy/Documents/dxy-gzh/front-end-interview/.temp/app-enhancers/4.js" 8 | import m7 from "/Users/dxy/Documents/dxy-gzh/front-end-interview/.temp/app-enhancers/5.js" 9 | 10 | export default [ 11 | m0, 12 | m1, 13 | m2, 14 | m3, 15 | m4, 16 | m5, 17 | m6, 18 | m7 19 | ] 20 | -------------------------------------------------------------------------------- /count.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | const guidePath = path.join(__dirname, 'docs/guide') 5 | 6 | 7 | let count = 0 8 | fs.readdir('./docs/guide', (err, files) => { 9 | files.forEach(file => { 10 | fs.readFile(path.join(guidePath, file), 'utf-8', (err, data) => { 11 | if (err) { 12 | console.log(err) 13 | } else { 14 | count = fnGetCpmisWords(data) + count 15 | console.log(count); 16 | 17 | } 18 | }) 19 | }) 20 | }) 21 | 22 | 23 | function fnGetCpmisWords(str){ 24 | sLen = 0; 25 | try{ 26 | //先将回车换行符做特殊处理 27 | str = str.replace(/(\r\n+|\s+| +)/g,"龘"); 28 | //处理英文字符数字,连续字母、数字、英文符号视为一个单词 29 | str = str.replace(/[\x00-\xff]/g,"m"); 30 | //合并字符m,连续字母、数字、英文符号视为一个单词 31 | str = str.replace(/m+/g,"*"); 32 | //去掉回车换行符 33 | str = str.replace(/龘+/g,""); 34 | //返回字数 35 | sLen = str.length; 36 | }catch(e){ 37 | console.log(e) 38 | } 39 | 40 | 41 | return sLen 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "front-end-interview", 3 | "version": "1.0.0", 4 | "description": "front-end-interview", 5 | "keywords": [ 6 | "css", 7 | "front-end", 8 | "html", 9 | "interview", 10 | "js", 11 | "questions" 12 | ], 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/" 16 | }, 17 | "main": "index.js", 18 | "scripts": { 19 | "dev": "vuepress dev docs --temp .temp", 20 | "build": "vuepress build docs --temp .temp", 21 | "view-info": "vuepress view-info docs --temp .temp", 22 | "show-help": "vuepress --help", 23 | "commit": "git-cz", 24 | "deploy": "bash deploy.sh" 25 | }, 26 | "author": "", 27 | "license": "ISC", 28 | "devDependencies": { 29 | "@vuepress/plugin-back-to-top": "^1.0.1", 30 | "@vuepress/plugin-google-analytics": "^1.0.1", 31 | "@vuepress/plugin-medium-zoom": "^1.0.1", 32 | "@vuepress/plugin-pwa": "^1.0.1", 33 | "commitizen": "^4.0.3", 34 | "cz-conventional-changelog": "^3.0.2", 35 | "vue-toasted": "^1.1.25", 36 | "vuepress": "^1.0.1", 37 | "vuepress-plugin-flowchart": "^1.4.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/guide/preface.md: -------------------------------------------------------------------------------- 1 | # 为什么会有这个项目 2 | 3 | 我经常在互联网上搜索一些面试题,也看过很多前端面试题相关的项目或者是文章集合,但是这些项目都存在一些问题. 4 | 5 | ## 大量的过时问题 6 | 7 | 现在搜索前端性能优化的处理方法,依然充斥着大量的**雅虎军规**,雅虎军规本身没问题,这是曾经的互联网巨头在大量实践中总结的宝贵经验。 8 | 9 | 问题是,雅虎都快没了,这些法则已经诞生超过十年了(我见过最早的中文版雅虎军规是2006年的),其中很多条"军规"早已经成为了日常操作,比如静态资源上 CDN,这在 10 年前算是时髦的事情,但是现在早已经司空见惯,再比如什么减少对 DOM 访问,css 放 head 标签,这种操作适用于史前前端,如今正常的项目基本都是上框架,操作 DOM 的机会本来就不多,而且工程化之后 webpack 自动打包,什么资源放哪都已经被集成了。 10 | 11 | 当然知道这些是有用的,问题是现在已经属于常识中的常识,试问,面试官问你前端性能优化,你上来背雅虎军规,什么少动 dom,上 cdn之类的,如果我是面试官给我的印象是,这个人没做过性能优化,只是网上搜了搜背了背而已. 12 | 13 | ## 深入浅出的不多 14 | 15 | 我看到很多项目整理了大量的面试题,但是解读部分非常草率,就比如**前端性能优化**这种问题,后面列了几条雅虎军规就了事了,这种解读的意义跟没有一样,单纯的罗列问题和罗列答案,不把东西讲清楚。 16 | 17 | 还有深入的部分,不得不说互联网上还是有非常多有深度的文章的,尤其是一些大厂的团队作品质量都有保证,但是很多时候讲的太深并不是好事,典型的就是各种源码解读文章,作者搞没搞清楚我不清楚,但是绝大多数读者肯定没有被“解读”清楚,除了源码晦涩难懂之外,文章大量罗列代码+一行注释这种写法不像是在解读,像是在做批注。 18 | 19 | ## 缺乏基础知识 20 | 21 | 现在的前端面试项目依然是 js+css+html 的三剑客模式,配上框架部分,这样一看是没有错,这就是"前端"面试,但是前端首先是程序员或者说是软件工程师,这里欠缺了计算机的基础知识. 22 | 23 | 如果放在几年前,一个熟练运用三剑客+一种框架的前端在市场上是香喷喷,现在只能算是一个合格水平了,前端野蛮生长的时代已经过去了,各个大厂也回归理性,除了前端的专业问题外,计算机的基础知识必不可少,尤其是算法部分,当然网络、操作系统、编译原理也都有涉及。 24 | 25 | ## 方向不够全面 26 | 27 | 前端一步步发展到今天,其实已经在发展过程中分化出了不同的方向。 28 | 29 | 所以,传统前端的面试知识已经不够了,我们的确需要一个更全面的项目,当然,我本人能力有限,只能尽可能先完善这些方向共同的知识点之后再对各个方向进行完善,还有,我的图形学不够看,欢迎图形学的朋友提交PR! 30 | -------------------------------------------------------------------------------- /docs/guide/common.md: -------------------------------------------------------------------------------- 1 | # 框架通用面试题 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | ## 为什么选择使用框架而不是原生? 6 | 7 | 框架的好处: 8 | 9 | 1. 组件化: 其中以 React 的组件化最为彻底,甚至可以到函数级别的原子组件,高度的组件化可以是我们的工程易于维护、易于组合拓展。 10 | 2. 天然分层: JQuery 时代的代码大部分情况下是面条代码,耦合严重,现代框架不管是 MVC、MVP还是MVVM 模式都能帮助我们进行分层,代码解耦更易于读写。 11 | 3. 生态: 现在主流前端框架都自带生态,不管是数据流管理架构还是 UI 库都有成熟的解决方案。 12 | 4. 开发效率: 现代前端框架都默认自动更新DOM,而非我们手动操作,解放了开发者的手动DOM成本,提高开发效率,从根本上解决了UI 与状态同步问题. 13 | 14 | ## 虚拟DOM的优劣如何? 15 | 16 | 优点: 17 | 18 | * 保证性能下限: 虚拟DOM可以经过diff找出最小差异,然后批量进行patch,这种操作虽然比不上手动优化,但是比起粗暴的DOM操作性能要好很多,因此虚拟DOM可以保证性能下限 19 | * 无需手动操作DOM: 虚拟DOM的diff和patch都是在一次更新中自动进行的,我们无需手动操作DOM,极大提高开发效率 20 | * 跨平台: 虚拟DOM本质上是JavaScript对象,而DOM与平台强相关,相比之下虚拟DOM可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等 21 | 22 | 缺点: 23 | 24 | * 无法进行极致优化: 在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化,比如VScode采用直接手动操作DOM的方式进行极端的性能优化 25 | 26 | ## 虚拟DOM实现原理? 27 | 28 | * 虚拟DOM本质上是JavaScript对象,是对真实DOM的抽象 29 | * 状态变更时,记录新树和旧树的差异 30 | * 最后把差异更新到真正的dom中 31 | 32 | > 详细实现见[虚拟DOM原理?](virtualDom.md) 33 | 34 | --- 35 | 36 | ## 公众号 37 | 38 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 39 | 40 | **简历模板:** 关注公众号回复「模板」获取 41 | 42 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 43 | 44 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | .env.test 68 | 69 | # parcel-bundler cache (https://parceljs.org/) 70 | .cache 71 | 72 | # next.js build output 73 | .next 74 | 75 | # nuxt.js build output 76 | .nuxt 77 | 78 | # vuepress build output 79 | .vuepress/dist 80 | 81 | # Serverless directories 82 | .serverless/ 83 | 84 | # FuseBox cache 85 | .fusebox/ 86 | 87 | # DynamoDB Local files 88 | .dynamodb/ 89 | 90 | .temp/ -------------------------------------------------------------------------------- /docs/guide/httpWritten.md: -------------------------------------------------------------------------------- 1 | # HTTP笔试部分 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | > 此部分是我早前在网上搜集的,已无法找出出处 6 | 7 | ## 缓存题 8 | 9 | 假设我们有一个HTML页面,如下: 10 | 11 | ```html 12 | 13 | 14 | http://www.w3.org/1999/xhtml"> 15 | 16 | 17 | page页 18 | 19 | 20 | 21 | 重新访问page页 22 | 23 | 24 | ``` 25 | 26 | 加载此页面后,会获取图片,图片请求返回的响应头为 27 | 28 | ```http 29 | HTTP/1.1 200 OK 30 | Cache-Control: no-cache 31 | Content-Type: image/png 32 | Last-Modified: Tue, 08 Nov 2016 06:59:00 GMT 33 | Accept-Ranges: bytes 34 | Date: Thu, 10 Nov 2016 02:48:50 GMT 35 | Content-Length: 3534 36 | ``` 37 | 38 | ### 问题一:当点击“重新访问 page 页”链接重新加载该页面后, head.png 如何二次加载? 39 | 40 | 响应头的`no-cache`表达的是可以缓存,但是每次都需要去服务器确认缓存资源的新鲜度,而不是不缓存,这是个坑。 41 | 42 | ```http 43 | Cache-Control: no-cache 44 | ``` 45 | 46 | 如果不跳这个坑的话,这个问题就简单了:图片会发出请求头带上`If-Modified-Since: Tue, 08 Nov 2016 06:59:00 GMT`,服务器确认新鲜度,如果客户端资源是新鲜资源则返回304,否则返回200并带上新的图片资源。 47 | 48 | ### 问题二:如果将上述信息中的 Cache-Control 设置为 private,那么结果又会如何呢? 49 | 50 | 当`Cache-Control: private`之后,说明一个问题,响应头没有给到任何缓存策略,这个时候客户端会怎么处理? 51 | 52 | 现在浏览器会有一个处理方法,当响应头没有任何缓存策略的时候有一套自己的处理机制,即 `Expires = 当前时间(Date - Last-Modified) * 10%`,简单理解就是响应头的Date时间与Last-Modified的时间差的十分之一作为缓存的过期时间。 53 | 54 | 按照这个处理流程,如果马上重新加载,则会直接读取本地缓存内容 55 | ,无需向服务器请求。 56 | 57 | --- 58 | 59 | ## 公众号 60 | 61 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 62 | 63 | **简历模板:** 关注公众号回复「模板」获取 64 | 65 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 66 | 67 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 68 | -------------------------------------------------------------------------------- /docs/guide/book.md: -------------------------------------------------------------------------------- 1 | # 书籍/课程推荐 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | 书单的推荐非常多,我在网络上也看过很多书单的推荐,往往时效性存在问题,像操作系统、编译原理这种变化不太快的书时效性不是问题,而前端作为应用层的技术变化太快,很可能两三年的书就过时了。 6 | 7 | 所以这里推荐的书,是个人认为依然没有过时的经典书籍,而且不会推荐框架、工具类的书,因为这些书太容易过时,框架一升级书的价值都大打折扣,而且类似于Vue这种框架文档本身已经足够好,没有必要看书,缺的是实战,再者,框架类的书(我看过的)真的没有几本是好书,除了『揭秘Angular』。 8 | 9 | ## 书单 10 | 11 | ### JavaScript 12 | 13 | [JavaScript高级程序设计(第3版)](https://book.douban.com/subject/10546125/): 红宝书,虽然是2012年的作品,还是ES5的内容,但是依然能让人深入浅出,缺点就是有一些内容比较陈旧,四星推荐。 14 | 15 | [你不知道的JavaScript(三部)](https://book.douban.com/subject/26351021/):这是一个三部曲系列,是让我深入理解JavaScript的启蒙书,上部是神作,中部是佳作,下部一般,整个系列四星推荐。 16 | 17 | [JavaScript面向对象精要](https://book.douban.com/subject/26352658/): 很薄的一本书,100页,针对js的对象的详细讲解,当然依然是ES5的内容,三天即可读完,三星半推荐。 18 | 19 | [Effective JavaScript](https://book.douban.com/subject/25786138/): 对于有一定经验的JavaScript开发者友好,教你如何写出并设计更好的代码,四星推荐。 20 | 21 | [深入理解ES6](https://book.douban.com/subject/27072230/): Nicholas C. Zakas 的力作,相比于阮老师的ES6标准入门,这本是更加深入浅出,称得上市面上最好的ES6教程,五星推荐。 22 | 23 | [JavaScript编程全解](https://book.douban.com/subject/25767719/):完全可以代替『权威指南』的大部头工具书,四星推荐。 24 | 25 | [JavaScript ES6函数式编程入门经典](https://book.douban.com/subject/30180100/): 函数式编程入门书,这本书虽然基础,但是市面上没有比他更好的入门书了,三星半推荐。 26 | 27 | [高性能JavaScript](https://book.douban.com/subject/5362856/): Nicholas C.Zakas的经典之作,2010年的作品确实有点显老,现在看其中很多优化的点已经成为了基本常识,但是算是市面上讲js性能优化的佳作,三星推荐。 28 | 29 | [JavaScript设计模式与开发实践](https://book.douban.com/subject/26382780/): 关于设计模式,js开发者看这本就够了,例子生动,解释详尽,算是js设计模式的集大成者,但是设计模式这种东西在前端或者说js世界有点水土不服,很多设计模式有点强行设计的意味,主要是领略思想,毕竟对于有经验的开发者设计模式不经意间就用在项目里了,他们的思想跟书中异曲同工,四星推荐。 30 | 31 | 32 | ### CSS: 33 | 34 | [精通 CSS(第3版)](https://book.douban.com/subject/30450258/): CSS的好书不多,这是为数不多的的能把css讲透彻的一本,四星半推荐。 35 | 36 | [CSS揭秘](https://book.douban.com/subject/26745943/):对于我这种css弱鸡而言,这是一本神书,作者是CSS的设计委员,全书彩色配图,这是一本既有术又有道的神作,对于普遍CSS短板的前端开发者,这是必读的css书籍,五星推荐。 37 | 38 | ### 算法: 39 | 40 | [学习JavaScript数据结构与算法(第3版)](https://book.douban.com/subject/33441631/): 只推荐这一本书,JavaScript工程师友好,这本书第一版还是非常基础的,随后逐渐扩充和修订,现在越来越成熟,再迭代几版怕是能和Java的橘书媲美,四星半推荐。 41 | 42 | ### 安全: 43 | 44 | [黑客攻防技术宝典 浏览器实战篇](https://book.douban.com/subject/26880889/): 前端安全相关的,这本是集大成者,看完之后有一种感觉,就是之前读的关于前端安全的文章和专栏跟闹着玩一样,所以我怀疑这本书不是给前端看到,受众应该是专业的白帽子,五星推荐。 45 | 46 | ### canvas: 47 | 48 | [HTML5 Canvas核心技术 图形、动画与游戏开发](https://book.douban.com/subject/24533314/): canvas入门与进阶必备,关于动画与基本应用部分适合前端看,后面要造一个游戏的部分没看下去,但是看前半部分足以让人入门canvas了,四星。 49 | 50 | ### Node 51 | [深入浅出Node.js](https://book.douban.com/subject/25768396/):六年过去了,他依然是国内最好的node书,真正的深入浅出,也是市面上为数不多的在讲node的书,而其他搭博客、聊天室的书没有必要读,四星。 52 | 53 | [Node.js开发实战](https://book.douban.com/subject/30373587/): 此书配上上一本堪称绝配,原理+实战的组合,node的书只推荐这两本,四星。 54 | 55 | ### 网络 56 | [HTTP权威指南](https://book.douban.com/subject/10746113/): 前端早晚要读的一本书,我是靠『图解HTTP』入门的,但是那本书讲的实在是太浅显了,最后你不得不还得拿起这个大部头,四星半推荐。 57 | 58 | --- 59 | 60 | ## 公众号 61 | 62 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 63 | 64 | **简历模板:** 关注公众号回复「模板」获取 65 | 66 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 67 | 68 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 69 | -------------------------------------------------------------------------------- /docs/guide/framework.md: -------------------------------------------------------------------------------- 1 | # 关于前端框架的面试须知 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | 目前,主流的招聘信息中都包含一项,就是至少会一种主流框架,如果只会JQuery可能在招聘市场就比较被动了. 6 | 7 | 本节没有具体的原理解析或者面试题目,而是一些偏"软"的一些学习技巧,但是个人认为会非常有用,希望你花点时间读一下. 8 | 9 | ## 准备框架相关知识的误区 10 | 11 | 笔者在学习框架和准备面试的过程中走过很多弯路,尤其是一些操作是很多人共通的,希望在这里分享出来,避免你再次掉入坑中. 12 | 13 | ### 不要为了读源码而读源码 14 | 15 | 不知道是从哪时起,读源码成为了一直时尚,甚至很多人以读过xx源码自居,仿佛读过xx源码就成为了某种意义上的认证一样. 16 | 17 | 笔者可以说读过非常多框架或者库的源码,曾经给自己的任务就是每天读一个库的源码,大概坚持了几个月,当然是从一些微型库开始阅读的. 18 | 19 | 也读过所谓的Vue、React、Redux、dva等著名库或者框架的源码,但是回过头来再审视自己的行为,其实性价比极低. 20 | 21 | 其实,很多情况下我是为了读源码而读源码,因为在社区里有一种很奇怪的气氛「读了xx源码就是牛x」,读源码成为了一种政治正确,仿佛说自己读了「xx源码」底气都足了一样. 22 | 23 | 而更多的时候我读源码,其实是为了『偷懒』,可能有人会问,读源码这么废力的行为,怎么还跟偷懒挂钩了?这里的偷懒是一种战术上的偷懒,我寄希望于用短期的战术上的勤奋,来到达长期的战略上的懒惰,我希望通过阅读React源码成为React专家,而不需要大量的长期的实践经验与思考。 24 | 25 | 很明显,读React源码根本不可能成为React专家,读源码才几斤几两,读了不代表理解了,更不代表理解了设计者背后的思想,更无法代表你能设计出一个React,就不要提改进React或者写React-like了,因为你读的目的就是为了『读』,所以读源码对你而言收益极低. 26 | 27 | 所以只推荐带着目的性的去阅读源码,比如公司有一个需求是需要一个mini版的React,而真得当你带有目的性的时候,阅读某一个库或者某一个框架的源码是远远不够的. 28 | 29 | 就比如,我认为Redux需要写太多了的模板代码,而当时市面上最著名的解决方案就是dva,但是dva并不是一个纯粹的数据流解决方案,它还加入了路由、异步等一些了库,算得上是一个框架集合了,我希望它是一个更加纯粹的数据流管理框架。 30 | 31 | 然后我找到了同样是阿里系的Mirror.js,但是Mirror.js由于是JavaScript编写的,即使加上了d.ts,也非常缺乏相关的类型支持和代码提示. 32 | 33 | 最后我找到了rematch,rematch既是纯粹的数据流管理框架,又是由TypeScript编写的,但是其类型编程运用的不够好,很多地方依然没有完整的类型提示. 34 | 35 | 于是我就通过阅读上述框架的源码,看看市面上主流的数据流管理框架的实现,然后取长补短,造了一个符合自己一点恶趣味的,但是完美支持TypeScript的数据流管理框架,丰富完整的类型提示让我的效率非常明显的提升了. 36 | 37 | 上述带着目的去阅读源码的过程才是正确的阅读源码的方式,你会通过阅读不同框架的源码发现其作者不同的设计理念,而哪些理念是与你相同的,哪些是没考虑到的,同样是设计插件系统,它们的实现为何会有这样或者那样的差异,哪个设计更好,这都是你在读源码过程中会思考到的地方,这样的好处就是你可以理解设计. 38 | 39 | 而当你只是单纯得为了读源码的时候,会陷入各种各样的细节中,目的就是为了搞懂作者这样写是什么意思,然后强行记忆下来,不久后你就会发现你早把自己读的源码忘记了,一方面是因为很多源码非常细节,你不可能记住这么多细节,另一方面,你根本没有理解这个设计,而只是记忆了源码细节而已. 40 | 41 | 我虽然读了很多库的源码,但是早已经忘记其具体细节和内容了,当初花费的时间其实算是白白流失了,而真正使我觉得自己读源码有价值且能提升能力的是有一些需求使我不得不去读一些源码,比如去设计一个虚拟滚动表格组件、比如写一个TypeScript友好的数据流框架、比如写一个前端Cli工具等等,我为了完成这些项目不得不阅读了非常多相关的库,不断思考与实践,我不仅沉淀下来了一些基础库,而且真正理解了背后的设计理念,它使得我有了设计上述组件或者库的能力,而这不是仅仅读一个库的源码就能做到的. 42 | 43 | ### 不建议读源码解读的文章 44 | 45 | 这里可能太过绝对了,有非常小的一部分文章还是值得读的,但是绝大多数所谓「源码解读」文章更像是「源码罗列」文章,还是要区分好什么叫解读,什么叫罗列. 46 | 47 | 源码罗列的典型就是贴了大段大段的源码,然后配上几个注释或者一段话,讲这个函数是干啥的,然后像流水账一样把一个库的某个模块或者某个库罗列下来,就成为了一个洋洋洒洒的大篇幅的所谓"源码解读". 48 | 49 | 这种囫囵吞枣式的「解读」方式是非常低效且不负责任的,一个库真正的核心部分其实非常小,典型的就是React本身代码有数万行,但是很多React-like库可以压缩到几千行,而且如果去掉防御式编程部分、特殊情况处理部分、一些非核模块部分等等,做一个微型React可能只需要1000多行代码。 50 | 51 | 真正的解读应该着重把这1000多行的React代码讲清楚,其设计理念、其设计取舍、其设计手法等等,而不是把数万行的React代码大片罗列加几行注释完事了。 52 | 53 | 很多时候读「源码解读」文章就跟看源码没太多区别了,耗时且低效。 54 | 55 | ### 不建议为了面试而读源码 56 | 57 | 现在的前端面试中确实流行问一些源码类的问题,比如双向绑定、虚拟DOM、脏检查、Fiber等等。 58 | 59 | 其实,面试官的目的是考察你对框架的一些基本原理理解,看一看是否有深入了解的意愿,把一些仅仅停留在API层面的框架小子筛出去,前端面试归根到底还是在招一个写业务代码的工程师,而不是招一个去造框架的专家,市面上到处抄点源码弄一个玩具框架的虽然很多,但是真正在生产环境中能运用的非常少,国内前端框架的设计专家掰着指头就能数出来几个。 60 | 61 | 所以不需要你去花非常大的精力去研究源码,因为当你并不想造一个可以在生产环境中运用的框架,只是为了面试而读的话,性价比太低,不如看几篇对应的面试文章,再进行一定程度的实战后理解其原理,在面试时讲清楚基本的原理即可,绝大多数时候面试官也不会刨根问底得问,因为面试官绝大多数也不知道更深层次的问题,比如如何优化Vue虚拟DOM的diff算法,除了专门的框架设计者和极少数求知欲非常强的开发者,没有人会考虑或者去探究这个问题。 62 | 63 | ## 公众号 64 | 65 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 66 | 67 | **简历模板:** 关注公众号回复「模板」获取 68 | 69 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 70 | 71 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 72 | -------------------------------------------------------------------------------- /.temp/transform/ClientComputedMixin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generated by "@vuepress/internal-transform-modules" 3 | */ 4 | 'use strict' 5 | 6 | /** 7 | * Get page data via path (permalink). 8 | * 9 | * @param {array} pages 10 | * @param {string} path 11 | * @returns {object} 12 | */ 13 | 14 | function findPageForPath (pages, path) { 15 | for (let i = 0; i < pages.length; i++) { 16 | const page = pages[i] 17 | if (page.path.toLowerCase() === path.toLowerCase()) { 18 | return page 19 | } 20 | } 21 | return { 22 | path: '', 23 | frontmatter: {} 24 | } 25 | } 26 | 27 | /** 28 | * Expose a function to get ClientComputedMixin constructor. 29 | * Note that this file will run in both server and client side. 30 | * 31 | * @param {object} siteData 32 | * @returns {ClientComputedMixin} 33 | */ 34 | 35 | export default siteData => { 36 | return class ClientComputedMixin { 37 | setPage (page) { 38 | this.__page = page 39 | } 40 | 41 | get $site () { 42 | return siteData 43 | } 44 | 45 | get $themeConfig () { 46 | return this.$site.themeConfig 47 | } 48 | 49 | get $frontmatter () { 50 | return this.$page.frontmatter 51 | } 52 | 53 | get $localeConfig () { 54 | const { locales = {}} = this.$site 55 | let targetLang 56 | let defaultLang 57 | for (const path in locales) { 58 | if (path === '/') { 59 | defaultLang = locales[path] 60 | } else if (this.$page.path.indexOf(path) === 0) { 61 | targetLang = locales[path] 62 | } 63 | } 64 | return targetLang || defaultLang || {} 65 | } 66 | 67 | get $siteTitle () { 68 | return this.$localeConfig.title || this.$site.title || '' 69 | } 70 | 71 | get $title () { 72 | const page = this.$page 73 | const { metaTitle } = this.$page.frontmatter 74 | if (typeof metaTitle === 'string') { 75 | return metaTitle 76 | } 77 | 78 | const siteTitle = this.$siteTitle 79 | const selfTitle = page.frontmatter.home ? null : ( 80 | page.frontmatter.title // explicit title 81 | || page.title // inferred title 82 | ) 83 | return siteTitle 84 | ? selfTitle 85 | ? (selfTitle + ' | ' + siteTitle) 86 | : siteTitle 87 | : selfTitle || 'VuePress' 88 | } 89 | 90 | get $description () { 91 | // #565 hoist description from meta 92 | const description = getMetaDescription(this.$page.frontmatter.meta) 93 | if (description) { 94 | return description 95 | } 96 | return this.$page.frontmatter.description || this.$localeConfig.description || this.$site.description || '' 97 | } 98 | 99 | get $lang () { 100 | return this.$page.frontmatter.lang || this.$localeConfig.lang || 'en-US' 101 | } 102 | 103 | get $localePath () { 104 | return this.$localeConfig.path || '/' 105 | } 106 | 107 | get $themeLocaleConfig () { 108 | return (this.$site.themeConfig.locales || {})[this.$localePath] || {} 109 | } 110 | 111 | get $page () { 112 | if (this.__page) { 113 | return this.__page 114 | } 115 | return findPageForPath( 116 | this.$site.pages, 117 | this.$route.path 118 | ) 119 | } 120 | } 121 | } 122 | 123 | function getMetaDescription (meta) { 124 | if (meta) { 125 | const descriptionMeta = meta.filter(item => item.name === 'description')[0] 126 | if (descriptionMeta) return descriptionMeta.content 127 | } 128 | } -------------------------------------------------------------------------------- /docs/guide/dom.md: -------------------------------------------------------------------------------- 1 | # DOM 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | * [DOM的事件模型是什么?](#dom的事件模型是什么?) 6 | * [DOM的事件流是什么?](#dom的事件流是什么?) 7 | * [什么是事件委托?](#什么是事件委托) 8 | 9 | 前端框架大行其道的今天,我们直接操作DOM的时候变得更少了,因此不妨复习一下DOM的[基本知识](http://luopq.com/2015/11/30/javascript-dom/) 10 | 11 | ## DOM的事件模型是什么? 12 | 13 | DOM之事件模型分脚本模型、内联模型(同类一个,后者覆盖)、动态绑定(同类多个) 14 | 15 | ```js 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 53 | 54 | ``` 55 | 56 | ## DOM的事件流是什么? 57 | 58 | 事件就是文档或浏览器窗口中发生的一些特定的交互瞬间,而事件流(又叫事件传播)描述的是从页面中接收事件的顺序。 59 | 60 | 61 | ### 事件冒泡 62 | 63 | 事件冒泡(event bubbling),即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点。 64 | 65 | 看如下例子: 66 | 67 | ```js 68 | 69 | 70 | 71 | 72 | Document 73 | 74 |
75 | 76 | 77 | ``` 78 | 79 | 如果单击了页面中的`
`元素,那么这个click事件沿DOM树向上传播,在每一级节点上都会发生,按照如下顺序传播: 80 | 81 | 1. div 82 | 2. body 83 | 3. html 84 | 4. document 85 | 86 | ### 事件捕获 87 | 88 | 事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前就捕获它。 89 | 90 | 还是以上一节的html结构为例: 91 | 92 | 在事件捕获过程中,document对象首先接收到click事件,然后事件沿DOM树依次向下,一直传播到事件的实际目标,即`
`元素 93 | 94 | 1. document 95 | 2. html 96 | 3. body 97 | 4. div 98 | 99 | ### 事件流 100 | 101 | 事件流又称为事件传播,DOM2级事件规定的事件流包括三个阶段:事件捕获阶段(capture phase)、处于目标阶段(target phase)和事件冒泡阶段(bubbling phase)。 102 | 103 | ![2019-06-23-03-06-09]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/276c91e03be37bc857446b7126428ea6.png) 104 | 105 | 触发顺序通常为 106 | 107 | 1. 进行事件捕获,为截获事件提供了机会 108 | 2. 实际的目标接收到事件 109 | 3. 冒泡阶段,可以在这个阶段对事件做出响应 110 | 111 | ## 什么是事件委托 112 | 113 | 事件委托就是利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件. 114 | 115 | 在绑定大量事件的时候往往选择事件委托。 116 | 117 | ```html 118 |
    119 |
  • one
  • 120 |
  • two
  • 121 |
  • three
  • 122 | ... 123 |
124 | 125 | 139 | 140 | ``` 141 | 142 | 优点: 143 | 144 | * 节省内存占用,减少事件注册 145 | * 新增子对象时无需再次对其绑定事件,适合动态添加元素 146 | 147 | 局限性: 148 | 149 | * focus、blur 之类的事件本身没有事件冒泡机制,所以无法委托 150 | * mousemove、mouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,不适合事件委托 151 | 152 | --- 153 | 154 | ## 公众号 155 | 156 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 157 | 158 | **简历模板:** 关注公众号回复「模板」获取 159 | 160 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 161 | 162 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 163 | -------------------------------------------------------------------------------- /docs/guide/README.md: -------------------------------------------------------------------------------- 1 | # 指南使用手册 2 | 3 | ## 项目结构概览 4 | 5 | 本项目分为前言、推荐、面试技巧和七大模块四部分: 6 | 7 | 前言部分,主要介绍了项目的构成,以及为什么要写这个项目,算是本项目的概要,建议首先阅读。 8 | 9 | 推荐部分,这部分主要涉及一些书或者项目的推荐,作为学习。 10 | 11 | 面试技巧部分,这部分主要是从简历到hr面的一些技巧和建议。 12 | 13 | 七大模块,是主要内容,下一节会详细介绍。 14 | 15 | ## 模块概览 16 | 17 | 本项目会分为七大模块,分别为:前端基础、框架解读、工程化、性能优化、计算机基础、Typescript、细分领域。 18 | 19 | ### 前端基础 20 | 21 | 这一部分以JavaScript、CSS、HTML这前端传统的三剑客为主,以浏览器原理等为辅,这一模块的构成不是简单得采用罗列面试题+罗列对应答案的方式,我们首先会区分重难点和非重点知识,再进行相应的解读。 22 | 23 | 对于非重点或者简单的知识我们采用罗列答案的方式,这一部分主要是帮助开发者快速过一遍这些内容,比如『==和===的区别是什么?』这种问题属于简单型的问题,我们只会做简单的解答,并不会深入历史讲清楚『===』诞生的原因、背后原理之类的。 24 | 25 | 对于重点和难点知识,我们会会提供两个版本的解读,简略版本会直接罗列相关要点,如果对相关知识已经掌握,可以快速阅读简略版作为快速地复习,详细版会针对相关知识进行深度解读,如果对相关知识不够了解,那么建议阅读详细版。 26 | 27 | ### 框架解读 28 | 29 | 框架部分一般分为两种,一种是框架原理知识,一种为实战技巧。 30 | 31 | #### 框架原理 32 | 33 | 框架原理是必考的内容,因为没有公司希望招一个API小子过来,在这部分我们跟『前端基础』中的重点部分处理方法一样,通过详略两个版本的内容。 34 | 35 | 我们不会通过罗列源码的方式进行原理讲解,我们会尽量简化代码做到深入浅出。 36 | 37 | #### 实战技巧 38 | 39 | 实战技巧主要涉及基础组件实现,考察是否只会用现成组件库,有没有造组件轮子的能力。 40 | 41 | 我们会对常见的基础组件实现,创建一个对应的实战项目,毕竟这些实战技巧脱离了项目只靠罗列答案,稍微有较深入的追问就原形毕露。 42 | 43 | ### 工程化 44 | 45 | 前端工程化现在已经成为标配了,我们目前计划的内容有:webpack、Babel、Git、测试、Node工具。 46 | 47 | #### webpack 48 | 49 | webpack是前端项目的事实上的打包标准,这部分我们会从打包原理、插件loader编写和实战搭建三个部分进行讲解。 50 | 51 | webpack的知识是实战型的,所以会有一个实战源码。 52 | 53 | #### Babel 54 | 55 | Babel也是前端事实上的转译标准,不过这部分要考察的内容并不多,基本就是插件的编写和编译原理,我们也会有对应的实战源码。 56 | 57 | #### Git 58 | 59 | Git是任何软件工程师的必备技能(SVN瑟瑟发抖),这部分主要讲两个部分:工作流和使用技巧。 60 | 61 | #### 测试 62 | 63 | 测试是软件工程的必备,但是在前端领域至今的重视程度不够,我们看到招聘市场上对测试的要求也不多。 64 | 65 | 但是测试是软件工程的基本要求,我们还是增加了这个部分,主要是对测试工具的对比和入门,以及测试思想的解读。 66 | 67 | #### Node工具(todo) 68 | 69 | 实际上Node才是前端工程化的基石,现在前端对Node要求也越来越多,当然除了写中间层之外,一大用处就是造基于Node的工程工具,如下: 70 | ![2019-06-12-17-52-29](https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/998be8184d459d6592b2a2cacd10380b.png) 71 | 72 | 这个部分我们会造一个基于Node的Cli工具,并将各种代码美化工具、代码检测工具以及各种工程化的思想集成进去,除了学会造node工具之外,对前端工程化的知识算是一个总的梳理。 73 | 74 | ### Typescript(未完成) 75 | 76 | 考虑再三,我还是认为TS应该占据一席之地,这两年Typescript其实有成为前端大型项目的标准开发语言,Angular这种庞然大物早就用了TS自不必说,Vue已经抛弃flow用TS重写(用过flow就知道多难用),甚至连Facebook开源的jest也抛弃flow改用TS,在Node后端框架中最流行的Nestjs也是基于TS的,还有vscode、antd、rxjs等等都是基于TS的。 77 | 78 | 在招聘信息上我见到越来越多的要求是『有使用Typescript的经验者优先』这种条件,个人认为在Vue 3.0正式发布后TS会逐渐成为硬性要求,虽然尤小右声明Vue在使用层面不限制js或者ts,但是Angular团队也是这么说的。。。但是问题在于Vue的生态会逐渐被TS重写,Vue对TS的支持会非常好,会有越来越多的团队用TS开发Vue新项目,这个趋势是不可逆的,包括本身对TS支持良好的Angular和React,三大框架都被TS纳入版图之后,这个方向已经很明朗了。 79 | 80 | 所以,这部分会介绍TS的基本语法和高级类型编程,然后用TS重写一个库作为实战。 81 | 82 | ### 性能优化 83 | 84 | 性能优化是任何软件工程必备的步骤,我们不会罗列雅虎军规,而是进入到实战进行优化。 85 | 86 | 这个部分我们不会罗列任何东西,会直接拿一个项目进行一步步的优化,原因很简单,性能优化就是与实战强相关的,脱离实际谈优化都是空中楼阁,在面试过程中有没有进行过实际优化,如果没有实战作保证很容易翻车。 87 | 88 | ### 计算机基础 89 | 90 | 计算机基础是前端面试近两年的考察重点,也其中算法涉及的最多,所以这部分的重点在于算法。 91 | 92 | 另一个重点是网络部分,HTTP协议也是必不可少的。 93 | 94 | 这部分会分为这几个部分: 数据结构、算法、网络。 95 | 96 | 操作系统,数据库和编译原理部分暂时没有,前端涉及的编译原理主要是编译原理的前端部分,在『工程化』模块中的babel会讲到相关知识,而前端的操作系统其实是浏览器,这部分在『前端基础』模块会涉及,数据库的应用场景在前端很有限,基本以数据类的重型前端项目为主,这个部分暂时没有考虑加。 97 | 98 | ### 细分领域(todo) 99 | 100 | 前端的目前分化的领域大概有四类:移动前端、可视化、图形、Node。 101 | 102 | 移动web前端,他们大部分时间在为移动设备工作,除了传统前端知识外,还需要运用 hybrid、RN 甚至一些 native 技术 103 | ![移动前端招聘信息]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/5efdb218b01fbe5842a03131db64eddf.png) 104 | 105 | 可视化前端,他们大部分时间再跟数据、canvas、svg 等打交道,需要一定的可视化科学的基础和相关领域算法 106 | ![数据可视化招聘]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/af0f1a1d78a9216a177d8725825f17e0.png) 107 | 108 | 图形互动前端,他们离传统意义的前端更远,大部分时间在处理图形学范畴的东西 109 | ![图形互动]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/1935882f80633f14b9214fdaf026ede1.png) 110 | 111 | Node 前端,讲道理 node 工程师跟前端的关系已经很小了,但是也挂了前端的头衔,他们的技术要求基本就是个后端,除了也用 js 112 | ![Node 前端]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/bfe816f0325b3b466e1dea9aacd90b5a.png) 113 | 114 | 实际上,这四个领域的分化导致其对特定领域的知识要求更高,而相关的资料就相比于通用前端知识更少,这部分应该是本项目难度最大的地方,我会按照移动前端、Node前端、可视化、图形先后顺序进行更新,这也是我个人的熟悉程度,也是目前市面上相关需求的大小排序,毕竟市场对于找一个写shader的前端需求有限,一般是一些大厂或者游戏厂商。 115 | -------------------------------------------------------------------------------- /docs/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/guide/webpackPluginDesign.md: -------------------------------------------------------------------------------- 1 | # webpack 插件化设计 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | 本文来源于饿了么前端团队[从 Bundle 文件看 Webpack 模块机制](https://zhuanlan.zhihu.com/p/26955349) 6 | 7 | --- 8 | 9 | ## 前言 10 | 在专栏之前的一篇 [《从 Bundle 文件看 Webpack 模块机制》](webpackMoudle.md)中,我们了解到经过 Webpack 构建之后的代码是如何工作的,学习了其模块化的实现原理,今天将带大家走进 Webpack 的本体设计中去,从宏观的角度观察其内部的运行和实现。
Webpack 代码较为复杂,其在内部高度使用事件通信和插件化的机制来实现代码解耦及工作流程的控制。本文将以 Webpack 的 2.3.3 版本为例进行相关演示。 11 | 12 | ## 事件系统 13 | 说起事件,自然少不了发布/订阅模式,Webpack 的基础组件之一 [Tapable](https://link.zhihu.com/?target=https%3A//github.com/webpack/tapable) 是为其量身定做的 “EventEmitter”,但它不只是单纯的事件中枢,还相应补充了对事件流程的控制能力,增加如 waterfall/parallel 系列方法,实现了异步/并行等事件流的控制能力。
以下截选了实例的部分 API,具体可参阅源码 [[tapable/Tapable.js at master · webpack/tapable · GitHub]](https://link.zhihu.com/?target=https%3A//github.com/webpack/tapable/blob/master/lib/Tapable.js),仅三百余行。
![](https://cdn.nlark.com/yuque/0/2019/png/128853/1565282493767-291c8866-507e-4965-bb9a-5f78c6cd5cc9.png#align=left&display=inline&height=446&originHeight=446&originWidth=599&size=0&status=done&width=599)总体来说,可分为三类方法: 14 | 15 | 1. apply 提供给插件的注册使用。 16 | 2. plugins 注册事件监听,接受事件名称和对应的回调函数。 17 | 3. applyPlugins[xx] 系列方法用于 emit 事件。类似 applyPlugins0, applyPlugins1... 这样后面的数字用来限制对应事件函数形参个数,类似参数限制的声明保证了接口声明的一致性。 18 | 19 | 事件注册相关源码如下,注册的事件维护在 _plugins 对象中。 20 | ``` 21 | Tapable.prototype.plugin = function plugin(name, fn) { 22 | if(Array.isArray(name)) { 23 | name.forEach(function(name) { 24 | this.plugin(name, fn); 25 | }, this); 26 | return; 27 | } 28 | if(!this._plugins[name]) this._plugins[name] = [fn]; 29 | else this._plugins[name].push(fn); 30 | }; 31 | Tapable.prototype.apply = function apply() { 32 | for(var i = 0; i < arguments.length; i++) { 33 | arguments[i].apply(this); 34 | } 35 | }; 36 | ``` 37 | Webpack 的核心对象 Compiler/Compilation 都是 Tapable 的子类,各自分管自己的_plugins。由 Compiler 提供的 [Event Hook](https://link.zhihu.com/?target=https%3A//webpack.js.org/api/plugins/compiler/%23event-hooks) 往往也对应着 Bundle 过程的各生命周期,从中获可以取到对应阶段的 Compilation 对象引用,Compilation 是主要的执行者,在相应周期中负责各项子任务,并触发更细粒度的事件,同时保持着对处理结果的引用。对于开发者编码主要集中在 Compilation hook 的处理上,用于捕获事件结果进行二次改造。 38 | 39 | ## 插件化设计 40 | Webpack 的插件与事件是紧密相连的,插件的设计让代码高度职能化,事件如同丝线连接,完成插件与主体(主要为 Compiler 和 Ccompilation)间的流程和数据的协作。
**一、插件定义**
插件的接口也是非常简单,仅需要实现一个 apply 方法,这与 Tapable 的 apply 方法相对应,具体文档可参考官方的 [How to write a plugin](https://link.zhihu.com/?target=https%3A//webpack.js.org/development/how-to-write-a-plugin/)。
我们每天都在使用各种 Webpack Plugins 完成项目的构建,有时也需要自己为项目量身定制。Webpack 内部插件与这些日常使用插件完全相同,不仅遵循一致的 API 设计,也共享相同的事件发布者。也就是说,我们可以通过外部插件触及到 Bundle 过程的每个阶段,高度的拓展性也是 Webpack 社区繁荣的基础之一。
**二、插件注册**
插件注册的实质是插件内部事件的注册。日常使用中,我们将插件实例化以后声明在配置的 plugins 数组中,Webpack 接收此数组注册每个插件: 41 | ``` 42 | if(options.plugins && Array.isArray(options.plugins)) { 43 | compiler.apply.apply(compiler, options.plugins); 44 | } 45 | ``` 46 | Compiler.apply 顺序调用各插件的 apply 方法并传入 Compilation 运行时对象作为唯一的参数,方法内部调用 Compiler/Compilation 的 plugin 方法完成事件监听的注册。 47 | 48 | ## 执行实例 49 | 下面我们选取 Webpack 模块热更新这一过程为例,了解一下事件与插件系统在开发实践中的应用与表现。
要开启热更新,需要在配置对象中声明 webpack.HotModuleReplacementPlugin 的实例。可以看出,热更新功能同样作为一个内置插件拓展在本体中,相应文件位于 /lib/HotModuleReplacementPlugin.js。在此阶段所发生的事件调用流大致如下: 50 | 51 | - 判断是否输出 assets:applyPluginsBailResult - "should-generate-chunk-assets" 52 | - 输出前的 Before 事件:applyPlugins0 - "before-chunk-assets" 53 | - 输出结束:applyPlugins2 - "chunk-asset" 54 | - 附加的 assets 处理阶段:applyPlugins1 - "additional-chunk-assets" 55 | - 若附加阶段对需要对 assets 进行操作则再次触发:applyPlugins - "chunk-asset" 56 | 57 | Webpack 的 watch 文件变动后触发一轮新的 buildModule,生成 chunk 后再次调用 Compilation.createChunkAssets 方法更新 assets 对象,触发 “chunk-assets” 事件,紧跟着会触发“additional-chunk-assets”事件,目前源码中仅有 HotModuleReplacementPlugin 监听 “additional-chunk-assets” 事件,于是执行权转移给此插件。在插件内部,通过 Compilation 对象获取到本轮 build 的 chunk 信息,筛选出更新和移除的 module 交由 Template 对象生成 hot-update.js 的源代码作为新的 chunk 加入到 assets 中。
值得一提的是,在系统设计考量上,Webpack 并不只局限于满足自身的实现,还尽可能站在系统拓展的角度把控。如前文所述的 “chunk-assets” 事件其自身并没有注册相应的回调函数,但仍然保留这一事件的触发,传递当前阶段 chunk 对象的引用和对应的 chunk 文件名,有需求的开发者可以通过这个 hook 对 assets 进行二次开发, 类似细节还有很多。 58 | 59 | ## 总结 60 | 在事件机制驱动下,通过良好的 API 约定,简洁的插件系统设计,Webpack 在完成自身繁重的构建任务同时,还提供了良好的拓展性及可测试性。然而事件机制并不是万金油,如通过事件维系的代码缺乏明确的索引关系将增加代码跟踪和调试的复杂度。本文作为简明地分析,希望能抛砖引玉,如有不当之处还望指正。
祝各位拥有一份好心情:)。 61 | 62 | --- 63 | 64 | ## 公众号 65 | 66 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 67 | 68 | **简历模板:** 关注公众号回复「模板」获取 69 | 70 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 71 | 72 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 73 | -------------------------------------------------------------------------------- /docs/guide/hr.md: -------------------------------------------------------------------------------- 1 | # 如何通过HR面 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | HR通常是程序员面试的最后一面,讲道理刷人的几率不大,但是依然有人倒在了这最后一关上,我们会从HR的角度出发来分析如何应对HR面. 6 | 7 | ## HR面的目的 8 | 9 | HR面往往是把控人才质量的最后一关,与前面的技术面不同,HR面往往侧重员工风险的评估与基本的员工素质. 10 | 11 | 录用风险评估,这部分是评估候选人是否具备稳定性,是否会带来额外的管理风险,是否能马上胜任工作,比如频繁的跳槽会带了稳定性的风险,HR会慎重考虑这一点,比如在面试中候选人体现出了「杠精」潜质,HR会担心候选人在工作中会难以与他人协作或者不服从管理,带来管理风险,再比如,虽然国家明确规定在招聘中不得有性别、年龄等歧视,但是一个大龄已婚妇女会有近期产子的可能性,可能会有长期的产假,HR也会做出评估。 12 | 13 | 员工素质评估,这部分评估候选人是否具备职场的基本素质,是否有基本的沟通能力,是否有团队精神和合作意识等等,比如一个表现极为内向的候选人,HR可能会对其沟通能力产生怀疑. 14 | 15 | 所以在与HR交流中要尽量保持踏实稳重、积极乐观的态度,切忌暴露出夸夸其谈、负能量、浮躁等性格缺陷。 16 | 17 | ## HR面的常见问题 18 | 19 | ### 你对未来3-5年的职业规划 20 | 21 | 目的: 这个问题就是考察候选人对未来的规划能力,主要想通过候选人的规划来嗅出候选人对工作的态度、稳定性和对技术的追求. 22 | 23 | 分析: 一定要在你的回到中体现对技术的追求、对团队的贡献、对工作的态度,不要谈一些假大空的东西,或者薪资、职位这些太过于功利的东西,而且最好体现出你的稳定性,如果是校招生或者工作没几年的新人最好不要涉及创业这种话题,一方面职场新人计划没几年就创业,这种很不切实际,说明候选人没法按实际出发,另一方面说明候选人的稳定性不够. 24 | 25 | [还真有候选人因为谈创业被HR刷的](https://www.zhihu.com/question/29913345/answer/100210163) 26 | 27 | 建议分三部分谈: 28 | 29 | 1. 首先表示考虑过这个问题(有规划),如何谈一谈自己的现状(结合实际). 30 | 2. 接着从工作本身出发,谈谈自己会如何出色完成本职工作,如何对团队贡献、如何帮助带领团队其他成员创造更多的价值、如何帮助团队扩大影响力. 31 | 3. 最后从学习出发,谈谈自己会如何精进领域知识、如何通过提升自己专业能力,如何反哺团队. 32 | 33 | 至于想成为技术leader还是技术专家,就看自己的喜好了. 34 | 35 | ### 如何看待加班(996)? 36 | 37 | 目的: 考察候选人的抗压能力和责任心 38 | 39 | 分析: 这个问题几乎是必问的,虽然996ICU事件闹得沸沸扬扬,但是官方的态度很暧昧,只口头批评从没有实际行动,基本上是默许企业违反劳动法的,除了个别外企在国内基本没可能找到不加班的公司,所以在这个面试题中尽量体现出自己愿意牺牲自我时间来帮助团队和企业的意愿就行了,而且要强调自己的责任心,如果真的是碰到无意义加班,好好学习怎么用vscode刷LeetCode划水是正道. 40 | 41 | 建议: 42 | 43 | 1. 把加班分为紧急加班和长期加班 44 | 2. 对于紧急加班,表示这是每个公司都会遇到的情况,自己愿意牺牲时间帮助公司和团队 45 | 3. 对于长期加班,如果是自己长期加班那么会磨练自己的技能,提高自己的效率,如果是团队长期加班,自己会帮助团队找到问题,利用自动化工具或者更高效的协作流程来提高整个团队的效率,帮助大家摆脱加班 46 | 47 | 当然了,就算你提高了团队效率,还是会被安排更多的任务,加班很多时候仅仅是目的,但是你不能说出来啊,尤其是一些候选人很强硬得表示长期加班不接受,其实可以回答的更委婉,除非你是真的对这个公司没兴趣,如果以进入这个公司为第一目的,还是做个高姿态比较好. 48 | 49 | ### 面对大量超过自己承受能力且时间有限的工作时你会怎么办? 50 | 51 | 目的: 考察候选人时间管理和处理大量任务的能力,当然也会涉及一定的沟通能力 52 | 53 | 分析: 程序员的工作内容可能大部分时间并不在写代码上,而是要处理各种会议、需求和沟通,通常都属于工作超负荷的状态,面对上面这种问题不建议以加班的方式来解决,因为主要考察的是你的时间管理能力和沟通能力,这些要素要在回答中体现出来 54 | 55 | 建议: 56 | 57 | 1. 将大量任务分解为紧急且重要、重要但不紧急、紧急但不重要、不重要且不紧急,依次完成上述任务,在这里体现出时间管理的能力 58 | 2. 与自己的领导沟通将不重要的任务放缓执行或者砍掉,或者派给组内的新人处理,在这里体现出沟通能力 59 | 60 | ### 你之前在上海为什么现在来北京发展? 61 | 62 | 目的: 考察候选人的稳定性和职业选择 63 | 64 | 分析: 这个问题一般是上份工作在异地的情况下大概率出现,HR主要担心候选人异地换工作可能会不稳定,有短期内离职风险,这个时候不建议说"北京互联网公司多,机会多"这种话(合着觉得北京好跳槽?),回答最好要体现出自己的稳定性,比如"女朋友在北京,长期异地,准备来北京一起发展" "家在北京,回北京发展" 等等,潜台词就是以后会在北京发展,不会在多地之间来回摇摆. 65 | 66 | ## 为什么从上一家公司离职? 67 | 68 | 目的: 考察离职原因,候选人离职风险评估 69 | 70 | 分析: 这个问题经常会在跳槽的时候问到,这个时候切忌吐槽上一家公司或者自己的上一任老板,尽量从职业发展的角度来回答,凸显自己的稳定性和渴望学习上升的决心,至于一些敏感话题,比如加班太多、薪资太低这种问题也是可以谈的,毕竟你跳槽的诉求就是解决上家公司碰到的问题,但是不能触碰刚才提到的底线问题,切忌吐槽向. 71 | 72 | 建议: 73 | 74 | 1. 因为工资低、离家远、加班多、技术含量低等等原因离职 75 | 2. 因为离家远花费在路途上的时间过多,不如用来充电,因为加班多导致没有时间充电,无法提高等等 76 | 77 | 除了不要有负能量和吐槽向,这个部分可以坦诚得说出来 78 | 79 | ## 你还有其他公司的Offer吗? 80 | 81 | 目的: 评估候选人是否有短时间内入职其他公司的可能性 82 | 83 | 分析: 很多时候并不是候选人完美符合一个岗位的要求,HR当然想要一个技术更好、要钱更少、技术更匹配的候选人,但是候选人一般都会有这样或者那样的小问题。 84 | 85 | 比如,你的表现是可以胜任目前的岗位的,但是这个岗位不是很紧急,HR可能把你当做备胎,来找一个性价比更高的候选人. 86 | 87 | 比如,你的表现很好,履历优秀,HR不知道能不能100%拿下你. 88 | 89 | 所以如果你很希望加入这个公司,最好要做到「欲擒故纵」,既要体现自身的市场竞争力,又要给到HR一定的压力. 90 | 91 | 所以,即使你已经拿了全北京城互联网公司的offer了,也不要说自己offer多如牛毛,一副满不在乎的样子,这样会给HR造成他入职可能性不大的错觉,因为他的选择太多了. 92 | 93 | 当然,也不要跪在地上舔:"加入公司是我的梦想,我只等这一个offer",放心吧,一定被hr放到备胎人才库中. 94 | 95 | 建议: 96 | 97 | 1. 表明自己有三四个已经确认过的offer了(没有offer也要吹,但是不要透露具体公司) 98 | 2. 但是第一意向还是本公司,如果薪资差距不大,会优先考虑本公司 99 | 3. 再透露出,有一两个offer催得比较急,希望这边快点出结果 100 | 101 | ## 如何与HR谈薪资? 102 | 103 | HR与你谈论薪资经常有如下套路: 104 | 105 | * HR: 您期望的薪资是多少? 106 | * 你: 25K。 107 | 108 | OK,你已经被HR成功套路。这个时候你的最高价就是25K了,然后HR会顺着这个价往下砍,所以你最终的薪资一般都会低于25K。等你接到offer,你的心里肯定充满了各种“悔恨”:其实当时报价26、27甚至28、29也是可以的。 109 | 110 | 正确的回答可以这样,并且还能够反套路一下HR: 111 | 112 | * HR: 您期望的薪资是多少? 113 | * 你: 就我的面试表现,贵公司最高可以给多少薪水? 114 | 115 | 哈哈,如果经验不够老道的HR可能就真会说出一个报价(如25K)来,然后,你就可以很开心地顺着这个价慢慢地往上谈了。所以这种情况下,你最终的薪资肯定是大于25K的。当然,经验老道的HR会给你一句很官方的套话: 116 | 117 | * HR: 您期望的薪资是多少? 118 | * 你: 就我的面试表现,贵公司最高可以给多少薪水? 119 | * HR: 这个暂且没法确定,要结合您几轮面试结果和用人部门的意见来综合评定。 120 | 121 | 如果HR这么回答你,我的建议是这样的: 122 | 123 | 虽然薪资很重要,但是我个人觉得这不是最重要的。我有以下建议: 124 | 125 | * 如果你觉得你技术面试效果很好,可以报一个高一点的薪资,这样如果HR想要你,会找你商量的。 126 | * 如果你觉得技术面试效果一般,但是你比较想进这家公司,可以报一个折中的薪资。 127 | * 如果你觉得面试效果很好,但是你不想进这家公司,你可以适当“漫天要价”一下。 128 | * 如果你觉得面试效果不好,但是你想进这家公司,你可以开一个稍微低一点的工资。 129 | 130 | 需要注意的是,面试求职是一个双向选择的过程。面试应该做到不卑不亢,千万不要因为面试结果不好,就低声下气地乞求工作,每个人的工作经历和经验都是不一样的,技术面试不好,知道自己的短板针对性地补缺补差就行,而不是在人事关系上动歪脑筋。 131 | 132 | --- 133 | 134 | 参考: 135 | 136 | [面试技巧 | 技术岗位面试如何与HR谈薪](https://zhuanlan.zhihu.com/p/50657715) 137 | 138 | --- 139 | 140 | ## 公众号 141 | 142 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 143 | 144 | **简历模板:** 关注公众号回复「模板」获取 145 | 146 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 147 | 148 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 149 | -------------------------------------------------------------------------------- /docs/.vuepress/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.vuepress/public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.vuepress/config.js: -------------------------------------------------------------------------------- 1 | const { fs, path } = require('@vuepress/shared-utils') 2 | 3 | module.exports = ctx => ({ 4 | locales: { 5 | '/': { 6 | lang: 'zh-CN', 7 | title: '前端面试与进阶指南', 8 | description: '可能是全网最给力的前端面试项目' 9 | } 10 | }, 11 | head: [ 12 | ['link', { rel: 'icon', href: `/logo.svg` }], 13 | ['link', { rel: 'manifest', href: '/manifest.json' }], 14 | ['meta', { name: 'theme-color', content: '#3eaf7c' }], 15 | ['meta', { name: 'apple-mobile-web-app-capable', content: 'yes' }], 16 | ['meta', { name: 'apple-mobile-web-app-status-bar-style', content: 'black' }], 17 | ['link', { rel: 'apple-touch-icon', href: `/icons/apple-touch-icon-152x152.png` }], 18 | ['link', { rel: 'mask-icon', href: '/icons/safari-pinned-tab.svg', color: '#3eaf7c' }], 19 | ['meta', { name: 'msapplication-TileImage', content: '/icons/msapplication-icon-144x144.png' }], 20 | ['meta', { name: 'msapplication-TileColor', content: '#000000' }] 21 | ], 22 | themeConfig: { 23 | repo: 'xiaomuzhu/front-end-interview', 24 | editLinks: true, 25 | docsDir: 'packages/docs/docs', 26 | locales: { 27 | '/': { 28 | editLinkText: '在 GitHub 上编辑此页', 29 | nav: require('./nav/zh'), 30 | sidebar: { 31 | '/guide/': renderSiderBar() 32 | } 33 | } 34 | } 35 | }, 36 | plugins: [ 37 | ['@vuepress/back-to-top', true], 38 | ['@vuepress/pwa', { 39 | serviceWorker: true, 40 | updatePopup: true 41 | }], 42 | ['@vuepress/medium-zoom', true], 43 | ['@vuepress/google-analytics', { 44 | ga: '' 45 | }], 46 | ['container', { 47 | type: 'vue', 48 | before: '
',
 49 |       after: '
', 50 | }], 51 | ['container', { 52 | type: 'upgrade', 53 | before: info => ``, 54 | after: '', 55 | }], 56 | ], 57 | extraWatchFiles: [ 58 | '.vuepress/nav/zh.js', 59 | ] 60 | }) 61 | 62 | function renderSiderBar() { 63 | return ([{ 64 | title: '前言', 65 | collapsable: false, 66 | children: [ 67 | '', 68 | 'preface' 69 | ] 70 | }, 71 | { 72 | title: '面试技巧', 73 | collapsable: false, 74 | children: [ 75 | 'resume', 76 | 'project', 77 | 'hr', 78 | ] 79 | }, 80 | { 81 | title: '推荐', 82 | collapsable: false, 83 | children: [ 84 | 'book' 85 | ] 86 | }, 87 | { 88 | title: '前端基础', 89 | collapsable: false, 90 | children: [ 91 | 'htmlBasic', 92 | 'cssBasic', 93 | 'jsBasic', 94 | 'browser', 95 | 'dom', 96 | // 'designPatterns', 97 | ] 98 | }, 99 | { 100 | title: '前端基础笔试', 101 | collapsable: false, 102 | children: [ 103 | 'httpWritten', 104 | 'jsWritten', 105 | ] 106 | }, 107 | { 108 | title: '前端原理详解', 109 | collapsable: false, 110 | children: [ 111 | 'hoisting', 112 | 'eventLoop', 113 | 'immutable', 114 | 'memory', 115 | 'deepclone', 116 | 'event', 117 | 'mechanism', 118 | ] 119 | }, 120 | { 121 | title: '计算机基础', 122 | collapsable: true, 123 | children: [ 124 | 'http', 125 | 'tcp', 126 | ] 127 | }, 128 | { 129 | title: '数据结构与算法', 130 | collapsable: false, 131 | children: [ 132 | 'algorithm', 133 | 'string', 134 | ] 135 | }, 136 | { 137 | title: '前端框架', 138 | collapsable: false, 139 | children: [ 140 | 'framework', 141 | 'vue', 142 | 'react', 143 | ] 144 | }, 145 | { 146 | title: '框架原理详解', 147 | collapsable: true, 148 | children: [ 149 | 'virtualDom', 150 | 'devsProxy', 151 | 'setState', 152 | 'router', 153 | 'redux', 154 | 'fiber', 155 | 'abstract', 156 | 'reactHook', 157 | ] 158 | }, 159 | { 160 | title: '框架实战技巧', 161 | collapsable: true, 162 | children: [ 163 | 'componentCli', 164 | 'component', 165 | 'carousel', 166 | ] 167 | }, 168 | { 169 | title: '性能优化', 170 | collapsable: true, 171 | children: [ 172 | 'load', 173 | 'execute', 174 | ] 175 | }, 176 | { 177 | title: '工程化', 178 | collapsable: true, 179 | children: [ 180 | 'webpack', 181 | 'engineering', 182 | ] 183 | }, 184 | { 185 | title: '工程化原理', 186 | collapsable: true, 187 | children: [ 188 | 'ast', 189 | 'WebpackHMR', 190 | 'webpackPlugin', 191 | 'webpackPluginDesign', 192 | 'webpackMoudle', 193 | 'webpackLoader', 194 | 'babelPlugin', 195 | ] 196 | }, 197 | { 198 | title: '安全', 199 | collapsable: true, 200 | children: [ 201 | 'security', 202 | ] 203 | }, 204 | // { 205 | // title: 'Node', 206 | // collapsable: true, 207 | // children: [ 208 | // 'node', 209 | // ] 210 | // }, 211 | ]) 212 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 2 | 3 |

4 | 5 | 6 | 7 |

8 | 9 |

10 | 阅读 11 | 公众号 12 | 公众号 13 |

14 | 15 | 建议选择基于VuePress的在线文档阅读方式: https://www.cxymsg.com/ 16 | 17 | ## 目录 18 | 19 | - [前言](#前言) 20 | - [面试技巧](#面试技巧) 21 | - [推荐书籍](#推荐书籍) 22 | - [前端基础](#前端基础) 23 | - [前端笔试](#前端笔试) 24 | - [前端原理](#前端原理) 25 | - [计算机基础](#计算机基础) 26 | - [数据结构与算法](#数据结构与算法) 27 | - [前端框架](#前端框架) 28 | - [框架原理](#框架原理) 29 | - [UI组件](#UI组件) 30 | - [性能优化](#性能优化) 31 | - [工程化](#工程化) 32 | - [工程化原理](#工程化原理) 33 | - [前端安全](#安全) 34 | - [待办](#待办) 35 | - [说明](#说明) 36 | 37 | ### 前言 38 | 39 | * [项目阅读指南](docs/guide/README.md) 40 | * [为什么会有这个项目](docs/guide/preface.md) 41 | 42 | ### 面试技巧 43 | 44 | * [面试官到底想看什么样的简历?](docs/guide/resume.md) 45 | * [面试回答问题的技巧](docs/guide/project.md) 46 | * [如何通过HR面](docs/guide/hr.md) 47 | 48 | ### 推荐书籍 49 | 50 | * [书籍推荐](docs/guide/book.md) 51 | 52 | ### 前端基础 53 | 54 | * [前端基础](docs/guide/htmlBasic.md) 55 | * [CSS基础](docs/guide/cssBasic.md) 56 | * [JavaScript基础](docs/guide/jsBasic.md) 57 | * [浏览器面试题](docs/guide/browser.md) 58 | * [DOM面试题](docs/guide/dom.md) 59 | 60 | ### 前端笔试 61 | 62 | * [HTTP笔试部分](docs/guide/httpWritten.md) 63 | * [书籍推荐](docs/guide/book.md) 64 | 65 | ### 前端原理 66 | 67 | * [JavaScript的『预解释』与『变量提升』](docs/guide/hoisting.md) 68 | * [Event Loop详解](docs/guide/eventLoop.md) 69 | * [实现不可变数据](docs/guide/immutable.md) 70 | * [JavaScript内存管理](docs/guide/memory.md) 71 | * [实现深克隆](docs/guide/deepclone.md) 72 | * [如何实现一个Event](docs/guide/event.md) 73 | * [JavaScript的运行机制](docs/guide/mechanism.md) 74 | 75 | ### 计算机基础 76 | 77 | * [HTTP面试题](docs/guide/http.md) 78 | * [TCP面试题](docs/guide/tcp.md) 79 | 80 | ### 数据结构与算法 81 | 82 | * [算法面试题](docs/guide/algorithm.md) 83 | 84 | ### 前端框架 85 | 86 | * [关于前端框架的面试须知](docs/guide/framework.md) 87 | * [Vue面试题](docs/guide/vue.md) 88 | * [React面试题](docs/guide/react.md) 89 | 90 | ### 框架原理 91 | 92 | * [虚拟DOM原理](docs/guide/virtualDom.md) 93 | * [Proxy比defineproperty优劣对比?](docs/guide/devsProxy.md) 94 | * [setState到底是异步的还是同步的?](docs/guide/setState.md) 95 | * [前端路由的实现](docs/guide/router.md) 96 | * [redux原理全解](docs/guide/redux.md) 97 | * [React Fiber 架构解析](docs/guide/fiber.md) 98 | * [React组件复用指南](docs/guide/abstract.md) 99 | * [React-hooks 抽象组件](docs/guide/reactHook.md) 100 | 101 | ### UI组件 102 | 103 | * [如何搭建一个组件库的开发环境](docs/guide/componentCli.md) 104 | * [组件设计原则](docs/guide/component.md) 105 | * [实现轮播图组件](docs/guide/carousel.md) 106 | 107 | ### 性能优化 108 | 109 | * [前端性能优化-加载篇](docs/guide/load.md) 110 | * [前端性能优化-执行篇](docs/guide/execute.md) 111 | 112 | ### 工程化 113 | 114 | * [webpack面试题](docs/guide/webpack.md) 115 | * [前端工程化](docs/guide/engineering.md) 116 | 117 | ### 工程化原理 118 | 119 | * [如何写一个babel](docs/guide/ast.md) 120 | * [Webpack HMR 原理解析](docs/guide/WebpackHMR.md) 121 | * [webpack插件编写](docs/guide/webpackPlugin.md) 122 | * [webpack 插件化设计](docs/guide/webpackPluginDesign.md) 123 | * [Webpack 模块机制](docs/guide/webpackMoudle.md) 124 | * [webpack loader实现](docs/guide/webpackLoader.md) 125 | * [如何开发Babel插件](docs/guide/babelPlugin.md) 126 | 127 | ### 前端安全 128 | 129 | * [前端安全面试题](docs/guide/security.md) 130 | 131 | ## 说明 132 | 133 | ### 关于读者 134 | 135 | 本项目一开始并没有要做一个仅仅面向面试的项目,而是希望借助面试的形式进一步巩固、完善自身的知识,同时为初学者提供一个参考路径。 136 | 137 | * 对于前端新人: 本项目的面试题都是相对高频的题目,且有一定的难度,非常适合作为学习教程,相对复杂的面试题我们都有更详细的原理详解,总之,这是可以当做前端学习手册的项目. 138 | * 对于面试候选人: 本项目梳理了主流的面试题,并且有意去掉了一些相对简单的面试题,对于有一定难度的面试题也有更详细的详解部分,这是查漏补缺同时完善知识体系的「良药」,而如果你只想快速应对面试,我们也提供了纯面试化的《前端面试手册》关注[公众号](#公众号)即可获取。 139 | 140 | ### 关于内容 141 | 142 | 本项目的主要内容来源由三部分组成: 143 | 144 | * 作者手打: 主要搜集了近几年的主流面试题,并针对每个面试题进行回答 145 | * 作者之前的博客: 部分比较长篇的原理详解来源于作者之前的博客内容 146 | * 转载: 一些面试题在网上已经有非常好的对应文章,没有必要进行二次创作,因此作者按照转载规范进行了转载,如有侵权立即删除 147 | 148 | ### 关于后续 149 | 150 | 本项目的主体内容基本完成,但是依然缺乏校审和拓展内容,例如[JavaScript的运行机制](docs/guide/mechanism.md)这篇文章虽然是作者花费了大量精力完成的,主要参照的就是[ECMA规范](https://www.ecma-international.org/publications/standards/Ecma-262.htm),但是经过反复考证依然无法保证作者理解的规范是否正确,而且在网络上搜索不到基于最新规范的文章与作者的文章对应,所以需要更多的人参与进来来验证文章的正确性. 151 | 152 | 面试题部分作者分为了七大模块,虽然大体完成了,但是依然有几个重要的模块需要打磨: 153 | 154 | * 数据结构与算法: 这部分仅仅列举了一些非常常见的查找、排序算法,还没有更深入的讲解,也缺少数据结构部分的内容 155 | * TypeScript: 这部分我另开了一个项目,已经脱离面试的范畴了,更像是一本深入浅出的手册,但是只完成了一半 156 | * 细分领域: 这部分其实难度更大一些,如果说上述各部分属于前端范畴的通用知识的话,这部分就更加专业化也更加细分,也是我认为的前端岗位未来的进化方向,由于可视化和图形这两个领域相对更冷门、专业化程度更高,所以排期更靠后,我们会优先更新移动端和Node的内容,也欢迎各个领域的专业开发者贡献内容 157 | 158 | 当然,除此之外其他已经完成的内容难免有需要补充和拓展的地方. 159 | 160 | ## 公众号 161 | 162 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 163 | 164 | **简历模板:** 关注公众号回复「模板」获取 165 | 166 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 167 | 168 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 169 | -------------------------------------------------------------------------------- /docs/guide/string.md: -------------------------------------------------------------------------------- 1 | # 字符串类面试题 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | ## 解析 URL Params 为对象 6 | 7 | ```js 8 | let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled'; 9 | parseParam(url) 10 | /* 结果 11 | { user: 'anonymous', 12 | id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型 13 | city: '北京', // 中文需解码 14 | enabled: true, // 未指定值得 key 约定为 true 15 | } 16 | */ 17 | 18 | ``` 19 | 20 | ```js 21 | function parseParam(url) { 22 | const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来 23 | const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中 24 | let paramsObj = {}; 25 | // 将 params 存到对象中 26 | paramsArr.forEach(param => { 27 | if (/=/.test(param)) { // 处理有 value 的参数 28 | let [key, val] = param.split('='); // 分割 key 和 value 29 | val = decodeURIComponent(val); // 解码 30 | val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字 31 | 32 | if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值 33 | paramsObj[key] = [].concat(paramsObj[key], val); 34 | } else { // 如果对象没有这个 key,创建 key 并设置值 35 | paramsObj[key] = val; 36 | } 37 | } else { // 处理没有 value 的参数 38 | paramsObj[param] = true; 39 | } 40 | }) 41 | 42 | return paramsObj; 43 | } 44 | 45 | ``` 46 | 47 | ## 模板引擎实现 48 | 49 | ```js 50 | let template = '我是{{name}},年龄{{age}},性别{{sex}}'; 51 | let data = { 52 | name: '姓名', 53 | age: 18 54 | } 55 | render(template, data); // 我是姓名,年龄18,性别undefined 56 | 57 | ``` 58 | 59 | ```js 60 | function render(template, data) { 61 | const reg = /\{\{(\w+)\}\}/; // 模板字符串正则 62 | if (reg.test(template)) { // 判断模板里是否有模板字符串 63 | const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段 64 | template = template.replace(reg, data[name]); // 将第一个模板字符串渲染 65 | return render(template, data); // 递归的渲染并返回渲染后的结构 66 | } 67 | return template; // 如果模板没有模板字符串直接返回 68 | } 69 | 70 | ``` 71 | 72 | ## 转化为驼峰命名 73 | 74 | ```js 75 | var s1 = "get-element-by-id" 76 | 77 | // 转化为 getElementById 78 | ``` 79 | 80 | ```js 81 | var f = function(s) { 82 | return s.replace(/-\w/g, function(x) { 83 | return x.slice(1).toUpperCase(); 84 | }) 85 | } 86 | 87 | ``` 88 | 89 | ## 查找字符串中出现最多的字符和个数 90 | 91 | 例: abbcccddddd -> 字符最多的是d,出现了5次 92 | 93 | ```js 94 | let str = "abcabcabcbbccccc"; 95 | let num = 0; 96 | let char = ''; 97 | 98 | // 使其按照一定的次序排列 99 | str = str.split('').sort().join(''); 100 | // "aaabbbbbcccccccc" 101 | 102 | // 定义正则表达式 103 | let re = /(\w)\1+/g; 104 | str.replace(re,($0,$1) => { 105 | if(num < $0.length){ 106 | num = $0.length; 107 | char = $1; 108 | } 109 | }); 110 | console.log(`字符最多的是${char},出现了${num}次`); 111 | 112 | ``` 113 | 114 | ## 字符串查找 115 | 116 | 请使用最基本的遍历来实现判断字符串 a 是否被包含在字符串 b 中,并返回第一次出现的位置(找不到返回 -1)。 117 | 118 | ```js 119 | a='34';b='1234567'; // 返回 2 120 | a='35';b='1234567'; // 返回 -1 121 | a='355';b='12354355'; // 返回 5 122 | isContain(a,b); 123 | 124 | ``` 125 | 126 | ```js 127 | function isContain(a, b) { 128 | for (let i in b) { 129 | if (a[0] === b[i]) { 130 | let tmp = true; 131 | for (let j in a) { 132 | if (a[j] !== b[~~i + ~~j]) { 133 | tmp = false; 134 | } 135 | } 136 | if (tmp) { 137 | return i; 138 | } 139 | } 140 | } 141 | return -1; 142 | } 143 | 144 | ``` 145 | 146 | ## 实现千位分隔符 147 | 148 | ```js 149 | // 保留三位小数 150 | parseToMoney(1234.56); // return '1,234.56' 151 | parseToMoney(123456789); // return '123,456,789' 152 | parseToMoney(1087654.321); // return '1,087,654.321' 153 | 154 | ``` 155 | 156 | ```js 157 | function parseToMoney(num) { 158 | num = parseFloat(num.toFixed(3)); 159 | let [integer, decimal] = String.prototype.split.call(num, '.'); 160 | integer = integer.replace(/\d(?=(\d{3})+$)/g, '$&,'); 161 | return integer + '.' + (decimal ? decimal : ''); 162 | } 163 | 164 | ``` 165 | 166 | 正则表达式(运用了正则的前向声明和反前向声明): 167 | 168 | ```js 169 | function parseToMoney(str){ 170 | // 仅仅对位置进行匹配 171 | let re = /(?=(?!\b)(\d{3})+$)/g; 172 | return str.replace(re,','); 173 | } 174 | ``` 175 | 176 | ## 判断是否是电话号码 177 | 178 | ```js 179 | function isPhone(tel) { 180 | var regx = /^1[34578]\d{9}$/; 181 | return regx.test(tel); 182 | } 183 | 184 | ``` 185 | 186 | ## 验证是否是邮箱 187 | 188 | ```js 189 | function isEmail(email) { 190 | var regx = /^([a-zA-Z0-9_\-])+@([a-zA-Z0-9_\-])+(\.[a-zA-Z0-9_\-])+$/; 191 | return regx.test(email); 192 | } 193 | 194 | ``` 195 | 196 | ## 验证是否是身份证 197 | 198 | ```js 199 | function isCardNo(number) { 200 | var regx = /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/; 201 | return regx.test(number); 202 | } 203 | 204 | ``` 205 | 206 | --- 207 | 参考: 208 | 209 | * [前端面试遇到的算法题](https://juejin.im/post/5aa7c2306fb9a028c14a24b8) 210 | 211 | --- 212 | 213 | ## 公众号 214 | 215 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 216 | 217 | **简历模板:** 关注公众号回复「模板」获取 218 | 219 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 220 | 221 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 222 | 223 | -------------------------------------------------------------------------------- /docs/guide/hoisting.md: -------------------------------------------------------------------------------- 1 | # JavaScript的『预解释』与『变量提升』 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | ## 前言 6 | 7 | JavaScript的作用域一直是JavaScript比较让人头痛的一部分,也是面试中几乎必考的内容,因此,我们将从更深层次来讲述js作用域。 8 | 9 | ## 从一个实例开始 10 | 11 | 仔细阅读以下JavaScript代码,你觉得运行结果会是什么呢?是 `1` 还是`2`? 12 | 13 | ``` javascript 14 | var a= 1; 15 | function f() { 16 | console.log(a); 17 | var a = 2; 18 | } 19 | f(); 20 | ``` 21 | 22 | 答案是undefined. 23 | 24 | 那么到底是什么原因导致了这个让人意外的结果呢?这就要从JavaScript解释阶段说起。 25 | 26 | ## avaScript预解释 27 | 28 | 我们可以大致把JavaScript在浏览器中运行的过程分为两个阶段`预解释阶段`(有人说准确的说法是应该是Parser,我们以预解释方便理解) `执行阶段`,在JavaScript引擎对JavaScript代码进行执行之前,需要进行预先处理,然后再对处理后的代码进行执行。 29 | 30 | > 我们平时书写的JavaScript代码并不是JavaScript执行的代码(V8引擎读取一行执行一行这种理解是错误的),它需要预解释后,再由引擎进行执行. 31 | 32 | 具体的解释过程涉及到浏览器内核的技术不属于前端领域,不过我们可以浅显的理解一下V8在处理JavaScript的一般过程: 33 | 34 | 以上例中的`var a = 2;`为例,我们一般人的理解为**声明了一个值为2的变量a**,但是在JavaScript引擎处理时却分为了两个步骤: 35 | 36 | >1. 读取`var a`后,在当前作用域中查找是否有相同声明,如果没有就在当前作用域集合中创建一个名为`a`的变量,否则忽略此声明继续进行解析. 37 | >2. 接下来,V8引擎会处理`a = 2`的赋值操作,首先会询问当前作用域中是否有名为`a`的变量,如果有进行赋值,否则继续向上级作用域询问. 38 | 39 | ## JavaScript执行环境 40 | 41 | 我们上面提到的所谓javascript预解释正是创建函数的**执行环境**(又称“执行上下文”),只有搞定了javascript的执行环境我们才能搞清楚一段代码在执行过后为什么产生这样的结果。 42 | 43 | 我们用一段伪代码表示创立的**执行环境** 44 | 45 | ```javascript 46 | executionContextObj = { 47 | 'scopeChain': { /* 变量对象 + 所有父级执行上下文中的变量对象 */ }, 48 | 'variableObject': { /* 函数参数 / 参数, 内部变量以及函数声明 */ }, 49 | 'this': {} 50 | } 51 | ``` 52 | 53 | 作用域链(scopeChain)包括下面提到的变量对象(variableObject)和所有父级执行上下文中的变量对象. 54 | 55 | 变量对象(variableObject)是与执行上下文相关的数据作用域,一个与上下文相关的特殊对象,其中存储了在上下文中定义的变量和函数声明: 56 | 57 | * 变量 58 | * 函数声明 59 | * 函数的形参 60 | 61 | 在有了这些基板概念之后我们可以梳理一下js引擎创建执行的过程: 62 | 63 | * 创建阶段 64 | * 创建Scope chain 65 | * 创建variableObject 66 | * 设置this 67 | * 执行阶段 68 | * 变量的值、函数的引用 69 | * 执行代码 70 | 71 | 而变量对象的创建细节如下: 72 | 73 | * 根据函数的参数,创建并初始化arguments object 74 | * 扫描函数内部代码,查找函数声明(Function declaration) 75 | * 对于所有找到的函数声明,将函数名和函数引用存入变量对象中 76 | * 如果变量对象中已经有同名的函数,那么就进行覆盖 77 | * 扫描函数内部代码,查找变量声明(Variable declaration) 78 | * 对于所有找到的变量声明,将变量名存入变量对象中,并初始化为"undefined" 79 | * 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性 80 | 81 | ## 变量提升 82 | 83 | 正是由于以上的处理,产生了大家熟知的JavaScript中的**变量提升**,具体以上代码的执行过程如以下伪代码所示: 84 | 85 | ```javascript 86 | // global context 87 | executionContextObj = { 88 | 'scopeChain': { ... }, 89 | 'variableObject': { a: undefined, f: pointer to function f() }, 90 | 'this': {...} 91 | } 92 | ... 93 | }//首先在全局执行环境中声明了变量a以及函数f,此时a虽然被声明,但是尚未赋值 94 | x = 1; 95 | function f() { 96 | executionContextObj { 97 | 'scopeChain': { ... }, 98 | 'variableObject': { 99 | arguments: {}, 100 | a: undefined 101 | }, 102 | 'this': {...} 103 | } 104 | //内部词法环境中声明了变量a,此时a虽然被声明,但是尚未赋值 105 | console.log(a);//此时a需要被被打印出来,在作用域内寻找a变量赋值,于是被赋值undefined 106 | a = 2; 107 | } 108 | ``` 109 | 110 | 我们可以明显看到,`a`变量在预解释阶段已经被赋值`undefined`,在执行阶段js是自上而下单线执行,当`console.log(a)`执行之时,`a=2`还没有被执行,`a`变量的值便是预处理阶段被赋予的`undefined`, 111 | 112 | ## 函数声明与函数表达式 113 | 114 | 我们看到,在编译器处理阶段,除了被`var`声明的变量会有变量提升这一特性之外,函数也会产生这一特性,但是函数声明与函数表达式两种范式创建的函数却表现出不同的结果. 115 | 116 | 我们先看一个实例,运行以下代码 117 | 118 | ```javascript 119 | f(); 120 | g(); 121 | //函数声明 122 | function f() { 123 | console.log('f'); 124 | } 125 | //函数表达式 126 | var g = function() { 127 | console.log('g'); 128 | }; 129 | 130 | ``` 131 | 132 | `f`成功被打印出来,而`g函数`出现了类型错误,这是什么原因呢? 133 | 134 | 135 | ```javascript 136 | executionContextObj = { 137 | 'scopeChain': { ... }, 138 | 'variableObject': { f: pointer to function f(), g: undefined}, 139 | 'this': {...} 140 | } 141 | 142 | f(); 143 | g(); 144 | //函数声明 145 | function f() { 146 | console.log('f'); 147 | } 148 | //函数表达式 149 | var g = function() { 150 | console.log('g'); 151 | }; 152 | ``` 153 | 154 | 我们看到,在预解释阶段函数声明的`f`是被指向了正确的函数得以执行,而函数表达式`g`被赋予`undefined`,`undefined`无法被当作函数执行因此报错`g is not a function`. 155 | 156 | ## 冲突处理 157 | 158 | 通常情况下我们不会将同一变量变量重复声明,但是出现了类似情况后,编译器会如何处理这些冲突呢? 159 | 160 | 1. 变量之间冲突 161 | 162 | 执行以下函数: 163 | 164 | ```javascript 165 | var a = 3; 166 | var a = 4; 167 | console.log(a); 168 | ``` 169 | 170 | 结果显而易见,后声明变量值覆盖前者的值 171 | 2. 函数之间冲突 172 | 173 | ```javascript 174 | f(); 175 | function f() { 176 | console.log('f'); 177 | } 178 | 179 | function f () { 180 | console.log('g'); 181 | }; 182 | ``` 183 | 184 | 结果同变量冲突,后者覆盖前者. 185 | 186 | 3.函数与变量之间冲突 187 | 188 | ```javascript 189 | console.log(f); 190 | 191 | function f() { 192 | console.log('f'); 193 | } 194 | var f ='g'; 195 | ``` 196 | 197 | 结果如下,函数声明将覆盖变量声明 198 | 199 | `[Function: f]` 200 | 201 | ## ES6中的let 202 | 203 | 在ES6中出现了两个最新的声明语法`let`与`const`,我们以`let`为例,进行测试看看与`var`的区别. 204 | 205 | ```javascript 206 | function f() { 207 | console.log(a); 208 | let a = 2; 209 | } 210 | f(); // ReferenceError: a is not defined 211 | ``` 212 | 213 | 这段代码直接报错显示未定义,`let`与`const`拥有类似的特性,阻止了变量提升,当代码执行到`console.log(a)`时,执行换将中`a`还从未被定义,因此产生了错误. 214 | 215 | --- 216 | 217 | ## 公众号 218 | 219 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 220 | 221 | **简历模板:** 关注公众号回复「模板」获取 222 | 223 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 224 | 225 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 226 | -------------------------------------------------------------------------------- /docs/guide/project.md: -------------------------------------------------------------------------------- 1 | # 面试回答问题的技巧 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | 技术面试通常至少三轮: 6 | 7 | 1. 基础面试: 主要考察对岗位和简历中涉及到基础知识部分的提问,包括一部分算法和场景设计的面试题,这一面可能会涉及现场coding. 8 | 2. 项目面试: 主要考察简历中涉及的项目,会涉及你项目的相关业务知识、扮演角色、技术取舍、技术攻坚等等. 9 | 3. HR面试: 这一面通常是HR把关,主要涉及行为面试,考察候选人是否价值观符合公司要求、工作稳定性如何、沟通协作能力如何等等. 10 | 11 | 当然,对于初级岗或者校招生会涉及一轮笔试,相当多的公司会在现场面之前进行一轮电话面试,目的是最快速有效地把不符合要求的候选人筛除,对于个别需要跨部门协作的岗位会涉及交叉面试,比如前端候选人会被后端的面试官面试,一些有管理需求的岗位或者重要岗位可能会涉及总监面试或者vp面. 12 | 13 | 而一个正常的技术面试流程(以项目面为例)分为大致三个部分: 14 | 15 | 1. 自我介绍 16 | 2. 项目(技术)考察 17 | 3. 向面试官提问 18 | 19 | 那么该如何准备技术面试,如何在面试中掌握主动权呢? 20 | 21 | ## 自我介绍 22 | 23 | 几乎所有的面试都是从自我介绍这个环节开始的,所以我们得搞清楚为什么自我介绍通常作为一个面试的开头. 24 | 25 | ### 为什么需要自我介绍 26 | 27 | 首先,有一个很普遍的问题就是面试官很可能才刚拿到你的简历,他需要在你自我介绍的时候快速浏览你的简历,因为技术面试的面试官很多是一线的员工,面试候选人只是其工作中的一小部分,很多情况下是没有提前看过你的简历的. 28 | 29 | 其次,自我介绍其实是一个热身,面试官和候选人其实是陌生人,自我介绍不管是面试还是其他情况下,都是两个陌生人彼此交流的起点,也是缓解候选人与面试官之间尴尬的一种热身方式. 30 | 31 | 最后,自我介绍是展示自我、引出接下来技术面试的引子,是你自己指定技术面试方向的一次机会。 32 | 33 | 知道了以上原因,我们才能进行准备更好的自我介绍。 34 | 35 | ### 自我介绍的几个必备要素 36 | 37 | 自我介绍归根到底是一个热身运动,因此切忌占用大量的篇幅,上来就把自己从出生的经历到大学像流水账一样吐出来的,往往会被没耐心的面试官打断,而这也暴露了候选人讲话缺乏重点、沟通能力一般的缺点。 38 | 39 | 但是,一些关键信息是必须体现的,就我个人而言,以下信息是必备的: 40 | 41 | * 个人信息: 至少要体现出自己的姓名、岗位和工作年限,应届生则必须要介绍自己的教育背景,如果自己的前东家是个大厂(比如BAT)最好提及,自己的学历是亮点(985或者硕博或者类似于北邮这种CS强校)最好提及,其他的什么有没有女朋友、是不是独生子没人在意,不要占用篇幅。这个部分重点在于「你是谁?」。 42 | * 技术能力: 简要地介绍自己的技术栈,切忌把自己只是简单使用过,写过几个Demo或者看了看文档的所谓「技术栈」也说出来,一旦后面问到算是自找尴尬。这个部分的重点在于「你会什么?」。 43 | * 技能擅长: 重点介绍自己擅长的技术,比如性能优化、高并发、系统架构设计或者是沟通协调能力等等,切忌夸大其词,要实事求是,这是之后考察的重点。这个部分重点自在于「你擅长什么?」。 44 | 45 | ### 自我介绍要有目的性 46 | 47 | #### 要重点匹配当前岗位的技术栈 48 | 49 | 你的面试简历可能包含了各种各样的技术栈,但是在自我介绍过程中需要匹配当前岗位的技术要求. 50 | 51 | 就比如你目前面试的是移动端H5前端的开发岗位,就重点在自我介绍中突出自己在移动前端的经验,而此时大篇幅得讲述自己如何用Node支撑公司的web项目就显得很不明智. 52 | 53 | #### 要在自我介绍中做刻意引导 54 | 55 | 如果你的自我介绍跟流水账一样,没有任何重点,其实面试官也很难办,因为他都没法往下接话... 56 | 57 | 而只要你稍作引导,绝大部分面试官就会接你的话茬,比如「你在自我介绍中重点提及了一个项目,碰到了一些难点,然后被你攻克了,效果如何如何好等等」,如果我是面试官一定会问「你的xx项目的xx难点后来是怎么解决的?」。 58 | 59 | 面试官的目的是考察候选人的能力,对候选人做出评估,因此需要知道候选人擅长什么,是否匹配岗位,面试官绝大多数情况下很乐意你这种有意无意的引导,这样双方的沟通和评估会很顺利,而不是故意刁难候选人。 60 | 61 | ### 如何准备自我介绍 62 | 63 | 其实最好的方法也是最笨的方法就是把自我介绍写下来,这个自我介绍一定要体现上面提到的几大必备要素,在面试前简单过几遍,能把自我介绍的内容顺利得表达出来即可,切忌跟背课文一样. 64 | 65 | 自我介绍的时间最好控制在1-3分钟之间,这些时间足够面试官把你的简历过一遍了,面试官看完简历后正好接着你的自我介绍进行提问是最舒服的节奏,别上来开始10分钟的演讲,面试官等待的时候会很尴尬,这么长的篇幅说明你的自我介绍一定是流水账式的. 66 | 67 | ![2019-07-19-02-52-15]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/9ffe42ee1436a587375986e8fee79361.png) 68 | 69 | ## 技术考察 70 | 71 | 一个好的技术考察的开始,必须得有自我介绍部分好的铺垫和引导,有一种情况我们经常遇见: 72 | 73 | > 候选人说了一大堆非重点的自我介绍,面试官一时语塞,完全get不到候选人的重点,也不知道候选人擅长什么、有什么亮点项目,然后就在他简历的技术栈中选了本公司也在用的技术,候选人这个时候也开始冒汗,因为这个技术栈并不是他的擅长,回答的也磕磕绊绊,面试官的引导和深入追问也没有达到很好的效果,面试就在这种尴尬的气氛中展开了,面试结束后面试官对候选人的评价是技术不熟练、没有深入理解原理,候选人的感受是,面试官专挑自己不会的问。 74 | 75 | 所以在前面的部分,一定要做好引导,把面试官的问题引到我们擅长的领域,但是这样还不够,正所谓不打无准备之仗,我们依然需要针对可能出现的问题进行准备. 76 | 77 | 那么如何准备可能的面试题? 78 | 79 | 比如你擅长前端的性能优化,在自我介绍的部分已经做好了引导,接下来面试官一定会重点考察你性能优化的能力,很可能会涉及很有深度的问题,即使你擅长这方面的技术,但是如果没有准备也可能临场乱了阵脚. 80 | 81 | ### 多重提问 82 | 83 | 自我多重提问的意思是,当一个技术问题抛出的时候,你可能面对更深层次的追问 84 | 85 | ![2019-07-19-02-51-45]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/ba5273b261f66c8c3881fcf4a8ac6301.png) 86 | 87 | 依旧以前端性能优化为例,面试官可能的提问: 88 | 89 | 1. 你把这个手机端的白屏时间减少了150%以上,是从哪些方面入手优化的?这个问题即使你没做过前端性能优化也能回答个七七八八,无非是组件分割、缓存、tree shaking等等,这是第一重比较浅的问题。 90 | 91 | 2. 我看你用webpack中SplitChunksPlugin这个插件进行分chunk的,你分chunk的取舍是什么?哪些库分在同一个chunk,哪些应该分开你是如何考虑的?如果你提到了SplitChunksPlugin插件可能会有类似的追问,如果没有实际操作过的候选人这个时候就难以招架了,这个过程一定是需要一定的试错和取舍的. 92 | 93 | 3. 在分chunk的过程中有没有遇到什么坑?怎么解决的?其实SplitChunksPlugin这个插件有一个暗坑,那就是chunk的id自增性导致id不固定唯一,很可能一个新依赖就导致id全部打乱,使得http缓存失效. 94 | 95 | 以上只是针对SplitChunksPlugin插件相关的优化提问,当然也可能从你的性能测试角度、代码层面进行考察,但是思路是类似的。 96 | 97 | 因此不能把自己准备的问题答案停留在一个很浅显的层面,一方面无法展示自己的技术深度,另一方面在面试官的深度体情况下容易丢分,因此在自己的答案后面多进行自我的追问,看一看能不能把问题做的更深入。 98 | 99 | ### 答题法则 100 | 101 | 很多面试相关的宝典都推荐使用STAR法则进行问题的应答,我们不想引入这个额外的概念,基础技术面试的部分老老实实回答面试官的问题即可,通常需要问题运用到这个法则的是项目面,比如让你介绍一下你最得意的项目,回答问题的法则有这几个要点: 102 | 103 | * 项目背景: 简要说一下项目的背景,让面试官知道这个项目是做什么的 104 | * 个人角色: 让面试官知道你在这个项目中扮演的角色 105 | * 难点: 让面试官知道你在项目开发过程中碰到的难点 106 | * 解决方案: 针对上面的难点你有哪一些解决方案,是如何结合业务进行取舍的 107 | * 总结沉淀: 在攻克上述的难点后有没有沉淀出一套通用的解决方案,有没有将自己的方案在大部门进行推广等等 108 | 109 | 重点就在于后面三条,也是最体现你个人综合素质的一部分,我是面试官的话会非常欣赏那种可以发现问题、找到多种方案、能对多种方案进行比对取舍还可以总结沉淀出通用解决方案回馈团队的人。 110 | 111 | 从上述几点可以体现出一个人的技术热情、解决问题的能力和总结提高的能力。 112 | 113 | ### 刻意引导 114 | 115 | 是的,在回答面试官提问的时候也可以做到刻意引导。 116 | 117 | 我们就举几个简单的例子: 118 | 119 | * 除了Vue还用过Angular吗? 这个时候很多候选人就很实诚回答「没有」,其实我们可以回答的更好,把你知道的说出来展示自己的能力才是最重要的,你可以说「我虽然没用过,但是在学习双向绑定原理的时候了解了一下Angular脏检查的原理,在学习Nestjs的时候了解了依赖注入的原理,跟Angular也是类似的」,面试官一定会接着问你脏检查和依赖注入的问题,虽然你没有用过Angular,但是Angular的基本原理你都懂,这是很好的加分项,说明候选人有深入理解原理的意愿和触类旁通的能力 120 | 121 | * Vue如何实现双向绑定的? 很多候选人老老实实答了`object.defineproperty`如何如何操作,然后就没有了,其实你可以在回答完之后加上一嘴「Vue 3.0则选择了更好用的Proxy来替代object.defineproperty」或者「除了object.defineproperty这种数据劫持的方式,观察者模式和脏检查都可以实现双向绑定」,面试官大概率会问「Proxy好在哪?」或者「聊聊脏检查」等等,这样下一个问题就会依然在你的可控范围内 122 | 123 | ![2019-07-19-02-51-14]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/f089e61ebe8d34a2965c19aecbc93d52.png) 124 | 125 | 我们第一个例子把本来回答不上来的问题,转化为了成功展示自己能力的加分项,第二个例子让自己更多的展示了自己的能力,而且始终使面试官的问题在自己的可控范围内。 126 | 127 | ## 向面试官提问 128 | 129 | 这个部分基本到了面试尾声了,属于做好了不影响大局,但是可能加分,如果做不好很容易踩雷的区域. 130 | 131 | 首先我们声明几个雷区: 132 | 133 | * 切忌问结果: 问了也白问,绝大部分公司规定不会透露结果的,你这样让大家很尴尬 134 | * 切忌问工资: 除了HR跟你谈工资的时候,千万别跟技术面试官谈工资,工资是所有公司的高压线,没法谈论 135 | * 切忌问技术问题: 别拿自己不会的技术难题反问面试官,完全没意义,面试官答也不是不答也不是 136 | 137 | 有几个比较好的提问可供参考: 138 | 139 | * 如果我入职这个岗位的话,前三个月你希望我能做到些什么? 140 | * 我的这个岗位的前任是为什么离职的,我什么地方能做的更好? 141 | * 你对这个职位理想人选的要求是什么? 142 | 143 | 尽量围绕你的岗位进行提问,这可以使得你更快得熟悉你的工作内容,也让面试官看到你对此岗位的兴趣和热情,重要的是这些问题对于面试官而言既可以简略回答,也可以详细的给你讲解,如果他很热情得跟你介绍此岗位相关的情况,说明你可能表现得不错,否则的话,你可能不在他的备选名单里,这个时候就需要你早做打算了. 144 | 145 | ## 总结 146 | 147 | 我们用大量篇幅介绍了技术面试中的一些应试技巧,但是归根到底候选人的基本功和丰富的项目经验才是硬道理. 148 | 149 | 如果你看完了整篇文章,并进行了精心的准备,他是可以让你从75分到85分的实用技巧,而不是让你从55到85的什么秘籍. 150 | 151 | --- 152 | 153 | ## 公众号 154 | 155 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 156 | 157 | **简历模板:** 关注公众号回复「模板」获取 158 | 159 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 160 | 161 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 162 | -------------------------------------------------------------------------------- /docs/guide/component.md: -------------------------------------------------------------------------------- 1 | # 组件设计原则 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | 在校招和一些初中级岗位上会涉及一些组件的设计工作,可能是直接手写,也可能是讲设计思路,目前最常考的三种就是轮播图、级联选择组件和自动完成组件. 6 | 7 | 上述三种组件难度适中,而且发挥空间很大,不同水平的人写出的组件会有较大差异,因此也是常见题目. 8 | 9 | 而比较复杂的比如日历组件、虚拟滚动可能只涉及设计思路,现场写的可能性极低,太简单的组件考察起来价值不大,而且发挥空间也不多. 10 | 11 | ## 前言 12 | 13 | 设计前端组件是最能考验开发者基本功的测试之一,因为调用Material design、Antd、iView 等现成组件库的 API 每个人都可以做到,但是很多人并不知道很多常用组件的设计原理。 14 | 15 | 能否设计出通用前端组件也是区分前端工程师和前端api调用师的标准之一,那么应该如何设计出一个通用组件呢? 16 | 17 | > 下文中提到的**组件库**通常是指单个组件,而非集合的概念,集合概念的组件库是 Antd iView这种,我们所说的组件库是指集合中的单个组件,集合性质的组件库需要考虑的要更多. 18 | 19 | ## 细粒度的考量 20 | 21 | 我们在学习设计模式的时候会遇到很多种设计原则,其中一个设计原则就是**单一职责原则**,在组件库的开发中同样适用,我们原则上一个组件只专注一件事情,单一职责的组件的好处很明显,由于职责单一就可以最大可能性地复用组件,但是这也带来一个问题,过度单一职责的组件也可能会导致过度抽象,造成组件库的碎片化。 22 | 23 | 举个例子,一个自动完成组件(AutoComplete),他其实是由 Input 组件和 Select 组件组合而成的,因此我们完全可以复用之前的相关组件,就比如 Antd 的AutoComplete组件中就复用了Select组件,同时Calendar、 Form 等等一系列组件都复用了 Select 组件,那么Select 的细粒度就是合适的,因为 Select 保持的这种细粒度很容易被复用. 24 | 25 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167c7a0394422556?w=397&h=219&f=png&s=11734) 26 | 27 | 那么还有一个例子,一个徽章数组件(Badge),它的右上角会有红点提示,可能是数字也可能是 icon,他的职责当然也很单一,这个红点提示也理所当然也可以被单独抽象为一个独立组件,但是我们通常不会将他作为独立组件,因为在其他场景中这个组件是无法被复用的,因为没有类似的场景再需要小红点这个小组件了,所以作为独立组件就属于细粒度过小,因此我们往往将它作为 Badge 的内部组件,比如在 Antd 中它以ScrollNumber的名称作为Badge的内部组件存在。 28 | 29 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167c7a84a60bd61f?w=358&h=218&f=png&s=16277) 30 | 31 | 所以,所谓的单一职责组件要建立在可复用的基础上,对于不可复用的单一职责组件我们仅仅作为独立组件的内部组件即可。 32 | 33 | ## 通用性考量 34 | 35 | 我们要设计的本身就是通用组件库,不同于我们常见的业务组件,通用组件是与业务解耦但是又服务于业务开发的,那么问题来了,如何保证组件的通用性,通用性高一定是好事吗? 36 | 37 | 比如我们设计一个选择器(Select)组件,通常我们会设计成这样 38 | 39 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167c7b98eab9246b?w=426&h=268&f=png&s=11485) 40 | 这是一个我们最常见也最常用的选择器,但是问题是其通用性大打折扣 41 | 42 | 当我们有一个需求是长这样的时候,我们之前的选择器组件就不符合要求了,因为这个 Select 组件的最下部需要有一个可拓展的条目的按钮 43 | 44 | 45 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167c7c17dbdfc830?w=337&h=161&f=png&s=10466) 46 | 47 | 这个时候我们难道要重新修改之前的选择器组件,甚至再造一个符合要求的选择器组件吗?一旦有这种情况发生,那么只能说明之前的选择器组件通用性不够,需要我们重新设计. 48 | 49 | Antd 的 Select 组件预留了`dropdownRender`来进行自定义渲染,其依赖的 `rc-select`组件中的代码如下 50 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167c7c69dc878398?w=514&h=55&f=png&s=8389) 51 | 52 | > Antd 依赖了大量以`rc-`开头的底层组件,这些组件被[react-component](https://github.com/react-component)团队(同时也就是Antd 团队)维护,其主要实现组件的底层逻辑,Antd 则是在此基础上添加Ant Design设计语言而实现的 53 | 54 | 当然类似的设计还有很多,通用性设计其实是一定意义上放弃对 DOM 的掌控,而将 DOM 结构的决定权转移给开发者,`dropdownRender`其实就是放弃对 Select 下拉菜单中条目的掌控,Antd 的 Select 组件其实还有一个没有在文档中体现的方法`getInputElement`应该是对 Input 组件的自定义方法,Antd整个 Select 的组件设计非常复杂,基本将所有的 DOM 结构控制权全部暴露给了开发者,其本身只负责底层逻辑和最基本的 DOM 结构. 55 | 56 | 这是 Antd 所依赖的 re-select 最终 jsx 的结构,其 DOM 结构很简单,但是暴露了大量自定义渲染的接口给开发者. 57 | 58 | ```jsx 59 | return ( 60 | 94 |
105 |
117 | {ctrlNode} 118 | {this.renderClear()} 119 | {this.renderArrow(!!multiple)} 120 |
121 |
122 |
123 | ); 124 | ``` 125 | 126 | 那么这么多需要自定义的地方,这个 Select 组件岂不是很难用?因为好像所有地方都需要开发者自定义,通用性设计在将 DOM 结构决定权交给开发者的同时也保留了默认结构,在开发者没有显示自定义的时候默认使用默认渲染结构,其实 Select 的基本使用很方便,如下: 127 | 128 | ```jsx 129 | 132 | ``` 133 | 134 | > 组件的形态(DOM结构)永远是千变万化的,但是其行为(逻辑)是固定的,因此通用组件的秘诀之一就是将 DOM 结构的控制权交给开发者,组件只负责行为和最基本的 DOM 结构 135 | 136 | --- 137 | 138 | ## 公众号 139 | 140 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 141 | 142 | **简历模板:** 关注公众号回复「模板」获取 143 | 144 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 145 | 146 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 147 | -------------------------------------------------------------------------------- /docs/guide/memory.md: -------------------------------------------------------------------------------- 1 | # JavaScript内存管理 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | ## 前言 6 | 7 | 像C语言这样的底层语言一般都有底层的内存管理接口,比如 malloc()和free()。另一方面,JavaScript创建变量(对象,字符串等)时分配内存,并且在不再使用它们时“自动”释放。 后一个过程称为垃圾回收。这个“自动”是混乱的根源,并让JavaScript(和其他高级语言)开发者感觉他们可以不关心内存管理,这是错误的。 8 | 9 | > 本文主要参考了深入浅出nodejs中的内存章节 10 | 11 | ## 内存模型 12 | 13 | 平时我们使用的基本类型数据或者复杂类型数据都是如何存放的呢? 14 | 15 | 基本类型普遍被存放在『栈』中,而复杂类型是被存放在堆内存的。 16 | 17 | > 如果你不了解执行栈和内存堆的概念,请先阅读[JavaScript执行机制](#mechanism.md) 18 | 19 | 当你读完上述文章后,你会问,既然复杂类型被存放在内存堆中,执行栈的函数是如何使用内存堆的复杂类型? 20 | 21 | 实际上,执行栈的函数上下文会保存一个内存堆对应复杂类型对象的内存地址,通过引用来使用复杂类型对象。 22 | 23 | 一个例子: 24 | 25 | ```js 26 | function add() { 27 | const a = 1 28 | const b = { 29 | num: 2 30 | } 31 | 32 | const sum = a + b.num 33 | } 34 | ``` 35 | 36 | 示意图如下(我们暂时不考虑函数本身的内存) 37 | ![2019-06-20-12-38-57]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/8f09ef156288fd2c9ee9b0b0296fd154.png) 38 | 39 | 还有一个问题是否所有的基本类型都储存在栈中呢? 40 | 41 | 并不是,当一个基本类型被闭包引用之后,就可以长期存在于内存中,这个时候即使他是基本类型,也是会被存放在堆中的。 42 | 43 | ## 生命周期 44 | 45 | 不管什么程序语言,内存生命周期基本是一致的: 46 | 47 | 1. 分配你所需要的内存 48 | 2. 使用分配到的内存(读、写) 49 | 3. 不需要时将其释放\归还 50 | 51 | ![2019-06-20-12-18-16]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/b9f8c025986dee6a49599c985cd15f2e.png) 52 | 53 | 所有语言第二部分都是明确的。第一和第三部分在底层语言中是明确的,但在像JavaScript这些高级语言中,大部分都是隐含的。 54 | 55 | ## 内存回收 56 | 57 | V8的垃圾回收策略基于分代回收机制,该机制又基于**世代假说**,该假说有两个特点: 58 | 59 | * 大部分新生对象倾向于早死 60 | * 不死的对象,会活得更久 61 | 62 | 基于这个理论,现代垃圾回收算法根据对象的存活时间将内存进行了分代,并对不同分代的内存采用不同的高效算法进行垃圾回收 63 | 64 | ### V8的内存分代 65 | 66 | 在V8中,将内存分为了新生代(new space)和老生代(old space)。它们特点如下: 67 | 68 | * 新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象。 69 | * 老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象。 70 | 71 | ### Stop The World (全停顿) 72 | 73 | 在介绍垃圾回收算法之前,我们先了解一下「全停顿」。 74 | 75 | 为避免应用逻辑与垃圾回收器看到的情况不一致,垃圾回收算法在执行时,需要停止应用逻辑。垃圾回收算法在执行前,需要将应用逻辑暂停,执行完垃圾回收后再执行应用逻辑,这种行为称为 「全停顿」(Stop The World)。例如,如果一次GC需要50ms,应用逻辑就会暂停50ms。 76 | 77 | ### Scavenge 算法 78 | 79 | Scavenge 算法的缺点是,它的算法机制决定了只能利用一半的内存空间。但是新生代中的对象生存周期短、存活对象少,进行对象复制的成本不是很高,因而非常适合这种场景。 80 | 81 | 新生代中的对象主要通过 Scavenge 算法进行垃圾回收。Scavenge 的具体实现,主要采用了Cheney算法。 82 | 83 | ![2019-06-20-12-51-06]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/b883571872f75fcf0157377003f57cf2.png) 84 | 85 | Cheney算法采用复制的方式进行垃圾回收。它将堆内存一分为二,每一部分空间称为 semispace。这两个空间,只有一个空间处于使用中,另一个则处于闲置。使用中的 semispace 称为 「From 空间」,闲置的 semispace 称为 「To 空间」。 86 | 87 | 过程如下: 88 | 89 | * 从 From 空间分配对象,若 semispace 被分配满,则执行 Scavenge 算法进行垃圾回收。 90 | * 检查 From 空间的存活对象,若对象存活,则检查对象是否符合晋升条件,若符合条件则晋升到老生代,否则将对象从 From 空间复制到 To 空间。 91 | * 若对象不存活,则释放不存活对象的空间。 92 | * 完成复制后,将 From 空间与 To 空间进行角色翻转(flip)。 93 | 94 | ### 对象晋升 95 | 96 | 1. 对象是否经历过Scavenge回收。对象从 From 空间复制 To 空间时,会检查对象的内存地址来判断对象是否已经经过一次Scavenge回收。若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。 97 | 2. To 空间的内存使用占比是否超过限制。当对象从From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置为25%的比例的原因是,当完成 Scavenge 回收后,To 空间将翻转成From 空间,继续进行对象内存的分配。若占比过大,将影响后续内存分配。 98 | 99 | 对象晋升到老生代后,将接受新的垃圾回收算法处理。下图为Scavenge算法中,对象晋升流程图。 100 | 101 | ![2019-06-20-12-52-37]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/7d503b3c8b7619b0a4cceb34594fea03.png) 102 | 103 | ### Mark-Sweep & Mark-Compact 104 | 105 | 老生代中的对象有两个特点,第一是存活对象多,第二个存活时间长。若在老生代中使用 Scavenge 算法进行垃圾回收,将会导致复制存活对象的效率不高,且还会浪费一半的空间。因而,V8在老生代采用Mark-Sweep 和 Mark-Compact 算法进行垃圾回收。 106 | 107 | Mark-Sweep,是标记清除的意思。它主要分为标记和清除两个阶段。 108 | 109 | * 标记阶段,它将遍历堆中所有对象,并对存活的对象进行标记; 110 | * 清除阶段,对未标记对象的空间进行回收。 111 | 112 | 与 Scavenge 算法不同,Mark-Sweep 不会对内存一分为二,因此不会浪费空间。但是,经历过一次 Mark-Sweep 之后,内存的空间将会变得不连续,这样会对后续内存分配造成问题。比如,当需要分配一个比较大的对象时,没有任何一个碎片内支持分配,这将提前触发一次垃圾回收,尽管这次垃圾回收是没有必要的。 113 | 114 | ![2019-06-20-12-55-15]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/805b5b5cf48dc8299f7a8093fa2d4080.png) 115 | 116 | 为了解决内存碎片的问题,提高对内存的利用,引入了 Mark-Compact (标记整理)算法。Mark-Compact 是在 Mark-Sweep 算法上进行了改进,标记阶段与Mark-Sweep相同,但是对未标记的对象处理方式不同。与Mark-Sweep是对未标记的对象立即进行回收,Mark-Compact则是将存活的对象移动到一边,然后再清理端边界外的内存。 117 | 118 | ![2019-06-20-12-55-47]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/847849c83fe8b3d4fea20017b28ef89b.png) 119 | 120 | 由于Mark-Compact需要移动对象,所以执行速度上,比Mark-Sweep要慢。所以,V8主要使用Mark-Sweep算法,然后在当空间内存分配不足时,采用Mark-Compact算法。 121 | 122 | ### Incremental Marking(增量标记) 123 | 124 | 在新生代中,由于存活对象少,垃圾回收效率高,全停顿时间短,造成的影响小。但是老生代中,存活对象多,垃圾回收时间长,全停顿造成的影响大。为了减少全停顿的时间,V8对标记进行了优化,将一次停顿进行的标记过程,分成了很多小步。每执行完一小步就让应用逻辑执行一会儿,这样交替多次后完成标记。如下图所示: 125 | 126 | ![2019-06-20-12-56-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d42805a7a519dace93309411d32ccdb5.png) 127 | 128 | 长时间的GC,会导致应用暂停和无响应,将会导致糟糕的用户体验。从2011年起,v8就将「全暂停」标记换成了增量标记。改进后的标记方式,最大停顿时间减少到原来的1/6。 129 | 130 | ### lazy sweeping(延迟清理) 131 | 132 | * 发生在增量标记之后 133 | * 堆确切地知道有多少空间能被释放 134 | * 延迟清理是被允许的,因此页面的清理可以根据需要进行清理 135 | * 当延迟清理完成后,增量标记将重新开始 136 | 137 | ## 内存泄露 138 | 139 | ### 引起内存泄漏的几个禁忌 140 | 141 | * 滥用全局变量:直接用全局变量赋值,在函数中滥用this指向全局对象 142 | * 不销毁定时器和回调 143 | * DOM引用不规范,很多时候, 我们对 Dom 的操作, 会把 Dom 的引用保存在一个数组或者 Map 中,往往无法对其进行内存回收,ES6中引入 WeakSet 和 WeakMap 两个新的概念, 来解决引用造成的内存回收问题. WeakSet 和 WeakMap 对于值的引用可以忽略不计, 他们对于值的引用是弱引用,内存回收机制, 不会考虑这种引用. 当其他引用被消除后, 引用就会从内存中被释放. 144 | * 滥用闭包: 145 | 146 | ```js 147 | // 滥用闭包引起内存泄漏 148 | var theThing = null; 149 | var replaceThing = function () { 150 | var originalThing = theThing; 151 | var unused = function () { 152 | if (originalThing) // 对于 'originalThing'的引用 153 | console.log("hi"); 154 | }; 155 | theThing = { 156 | longStr: new Array(1000000).join('*'), 157 | someMethod: function () { 158 | console.log("message"); 159 | } 160 | }; 161 | }; 162 | setInterval(replaceThing, 1000); 163 | ``` 164 | 165 | ### 查看内存泄漏 166 | 167 | * 打开开发者工具,选择 Timeline 面板 168 | * 在顶部的Capture字段里面勾选 Memory 169 | * 点击左上角的录制按钮。 170 | * 在页面上进行各种操作,模拟用户的使用情况。 171 | * 一段时间后,点击对话框的 stop 按钮,面板上就会显示这段时间的内存占用情况。 172 | 173 | --- 174 | 175 | 参考: 176 | 177 | [深入浅出Node.js](https://book.douban.com/subject/25768396/) 178 | 179 | [MDN内存管理](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Memory_Management) 180 | 181 | --- 182 | 183 | ## 公众号 184 | 185 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 186 | 187 | **简历模板:** 关注公众号回复「模板」获取 188 | 189 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 190 | 191 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 192 | -------------------------------------------------------------------------------- /.temp/internal/page-components.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generated by "@vuepress/internal-page-components" 3 | */ 4 | export default { 5 | "v-7364f2b6": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/README.md"), 6 | "v-16db57dc": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/WebpackHMR.md"), 7 | "v-5db7e350": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/abstract.md"), 8 | "v-a6b0f6ea": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/README.md"), 9 | "v-5535cbb6": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/ast.md"), 10 | "v-dae534d4": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/algorithm.md"), 11 | "v-6ce1c4f6": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/babel.md"), 12 | "v-050a85d6": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/babelPlugin.md"), 13 | "v-38038a4a": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/book.md"), 14 | "v-08fc6914": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/browser.md"), 15 | "v-17888648": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/carousel.md"), 16 | "v-c64d1774": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/common.md"), 17 | "v-909d7754": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/component.md"), 18 | "v-9ec1af54": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/componentCli.md"), 19 | "v-45f5ac96": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/composite.md"), 20 | "v-26a54b74": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/cssBasic.md"), 21 | "v-48a310d6": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/dataStructure.md"), 22 | "v-d47e3e54": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/deepclone.md"), 23 | "v-1d743fcc": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/designPatterns.md"), 24 | "v-7b8fa194": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/devsProxy.md"), 25 | "v-d62d5894": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/dom.md"), 26 | "v-69018a14": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/event.md"), 27 | "v-26928c16": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/engineering.md"), 28 | "v-0f593414": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/eventLoop.md"), 29 | "v-09708bd6": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/execute.md"), 30 | "v-26cb8136": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/fiber.md"), 31 | "v-1aa489f6": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/framework.md"), 32 | "v-0527eb44": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/hoisting.md"), 33 | "v-26473748": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/hr.md"), 34 | "v-7636ce16": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/htmlBasic.md"), 35 | "v-2610bfe8": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/http.md"), 36 | "v-0dcf6a96": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/httpWritten.md"), 37 | "v-c6715f14": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/immutable.md"), 38 | "v-6faacdd6": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/jsBasic.md"), 39 | "v-95dc5814": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/jsWritten.md"), 40 | "v-1b672d60": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/load.md"), 41 | "v-561489d6": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/mechanism.md"), 42 | "v-2d28b59a": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/memory.md"), 43 | "v-13d67076": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/preface.md"), 44 | "v-5d0cbb18": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/node.md"), 45 | "v-571d1f56": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/project.md"), 46 | "v-7f195d96": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/react.md"), 47 | "v-1f0a8450": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/reactivity.md"), 48 | "v-612b2876": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/reactHook.md"), 49 | "v-07bf9b94": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/redux.md"), 50 | "v-0390a37c": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/resume.md"), 51 | "v-5ad4eb0a": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/router.md"), 52 | "v-92f0bdc8": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/security.md"), 53 | "v-00ee637a": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/string.md"), 54 | "v-6598957e": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/setState.md"), 55 | "v-b3cc06a4": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/virtualDom.md"), 56 | "v-eff5ea94": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/webpackLoader.md"), 57 | "v-14935594": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/vue.md"), 58 | "v-176f5554": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/tcp.md"), 59 | "v-87310654": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/webpack.md"), 60 | "v-4a243ed4": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/webpackMoudle.md"), 61 | "v-79883036": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/webpackPluginDesign.md"), 62 | "v-53678eb6": () => import("/Users/dxy/Documents/dxy-gzh/front-end-interview/docs/guide/webpackPlugin.md") 63 | } -------------------------------------------------------------------------------- /docs/guide/reactivity.md: -------------------------------------------------------------------------------- 1 | # Vue的响应式系统 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | ## 响应式系统 6 | 7 | UI 在 MVVM 中指的是 View,状态在 MVVM 中指的是 Modal,而保证 View 和 Modal 同步的是 View-Modal。 8 | 9 | Vue 通过一个[响应式系统](https://cn.vuejs.org/v2/guide/reactivity.html#ad)保证了View 与 Modal的同步,由于要兼容IE,Vue 选择了 `Object.defineProperty`作为响应式系统的实现,但是如果不考虑 IE 用户的话,` Object.defineProperty`并不是一个好的选择,具体请看[面试官系列(4): 基于Proxy 数据劫持的双向绑定优势所在](https://juejin.im/post/5acd0c8a6fb9a028da7cdfaf)。 10 | 11 | 我们将用 Proxy 实现一个响应式系统。 12 | 13 | ![](https://user-gold-cdn.xitu.io/2018/6/6/163d41869ea10f6d?w=750&h=390&f=png&s=33203) 14 | 15 | > 建议阅读之前看一下[面试官系列(4): 基于Proxy 数据劫持的双向绑定优势所在](https://juejin.im/post/5acd0c8a6fb9a028da7cdfaf)中基于`Object.defineProperty`的大致实现。 16 | 17 | ## 发布订阅中心 18 | 19 | 一个响应式系统离不开发布订阅模式,因为我们需要一个 `Dep`保存订阅者,并在 Observer 发生变化时通知保存在 Dep 中的订阅者,让订阅者得知变化并更新视图,这样才能保证视图与状态的同步。 20 | 21 | ```js 22 | /** 23 | * [subs description] 订阅器,储存订阅者,通知订阅者 24 | * @type {Map} 25 | */ 26 | export default class Dep { 27 | constructor() { 28 | // 我们用 hash 储存订阅者 29 | this.subs = new Map(); 30 | } 31 | // 添加订阅者 32 | addSub(key, sub) { 33 | // 取出键为 key 的订阅者 34 | const currentSub = this.subs.get(key); 35 | // 如果能取出说明有相同的 key 的订阅者已经存在,直接添加 36 | if (currentSub) { 37 | currentSub.add(sub); 38 | } else { 39 | // 用 Set 数据结构储存,保证唯一值 40 | this.subs.set(key, new Set([sub])); 41 | } 42 | } 43 | // 通知 44 | notify(key) { 45 | // 触发键为 key 的订阅者们 46 | if (this.subs.get(key)) { 47 | this.subs.get(key).forEach(sub => { 48 | sub.update(); 49 | }); 50 | } 51 | } 52 | } 53 | 54 | ``` 55 | 56 | ## 监听者的实现 57 | 58 | 我们在订阅器 `Dep` 中实现了一个`notify`方法来通知相应的订阅这们,然而`notify`方法到底什么时候被触发呢? 59 | 60 | 当然是当状态发生变化时,即 MVVM 中的 Modal 变化时触发通知,然而`Dep` 显然无法得知 Modal 是否发生了变化,因此我们需要创建一个监听者`Observer`来监听 Modal, 当 Modal 发生变化的时候我们就执行通知操作。 61 | 62 | vue 基于`Object.defineProperty`来实现了监听者,我们用 Proxy 来实现监听者. 63 | 64 | 与`Object.defineProperty`监听属性不同, Proxy 可以监听(实际是代理)整个对象,因此就不需要遍历对象的属性依次监听了,但是如果对象的属性依然是个对象,那么 Proxy 也无法监听,所以我们实现了一个`observify`进行递归监听即可。 65 | 66 | ```JavaScript 67 | /** 68 | * [Observer description] 监听器,监听对象,触发后通知订阅 69 | * @param {[type]} obj [description] 需要被监听的对象 70 | */ 71 | const Observer = obj => { 72 | const dep = new Dep(); 73 | return new Proxy(obj, { 74 | get: function(target, key, receiver) { 75 | // 如果订阅者存在,直接添加订阅 76 | if (Dep.target) { 77 | dep.addSub(key, Dep.target); 78 | } 79 | return Reflect.get(target, key, receiver); 80 | }, 81 | set: function(target, key, value, receiver) { 82 | // 如果对象值没有变,那么不触发下面的操作直接返回 83 | if (Reflect.get(receiver, key) === value) { 84 | return; 85 | } 86 | const res = Reflect.set(target, key, observify(value), receiver); 87 | // 当值被触发更改的时候,触发 Dep 的通知方法 88 | dep.notify(key); 89 | return res; 90 | }, 91 | }); 92 | }; 93 | 94 | /** 95 | * 将对象转为监听对象 96 | * @param {*} obj 要监听的对象 97 | */ 98 | export default function observify(obj) { 99 | if (!isObject(obj)) { 100 | return obj; 101 | } 102 | 103 | // 深度监听 104 | Object.keys(obj).forEach(key => { 105 | obj[key] = observify(obj[key]); 106 | }); 107 | 108 | return Observer(obj); 109 | } 110 | ``` 111 | 112 | ## 订阅者的实现 113 | 114 | 我们目前已经解决了两个问题,一个是如何得知 Modal 发生了改变(利用监听者 Observer 监听 Modal 对象),一个是如何收集订阅者并通知其变化(利用订阅器收集订阅者,并用notify通知订阅者)。 115 | 116 | 我们目前还差一个订阅者(Watcher) 117 | 118 | ```js 119 | // 订阅者 120 | export default class Watcher { 121 | constructor(vm, exp, cb) { 122 | this.vm = vm; // vm 是 vue 的实例 123 | this.exp = exp; // 被订阅的数据 124 | this.cb = cb; // 触发更新后的回调 125 | this.value = this.get(); // 获取老数据 126 | } 127 | get() { 128 | const exp = this.exp; 129 | let value; 130 | Dep.target = this; 131 | if (typeof exp === 'function') { 132 | value = exp.call(this.vm); 133 | } else if (typeof exp === 'string') { 134 | value = this.vm[exp]; 135 | } 136 | Dep.target = null; 137 | return value; 138 | } 139 | // 将订阅者放入待更新队列等待批量更新 140 | update() { 141 | pushQueue(this); 142 | } 143 | // 触发真正的更新操作 144 | run() { 145 | const val = this.get(); // 获取新数据 146 | this.cb.call(this.vm, val, this.value); 147 | this.value = val; 148 | } 149 | } 150 | 151 | ``` 152 | 153 | ## 批量更新的实现 154 | 155 | 我们在上一节中实现了订阅者( Watcher),但是其中的`update`方法是将订阅者放入了一个待更新的队列中,而不是直接触发,原因如下: 156 | ![](https://user-gold-cdn.xitu.io/2018/6/6/163d44b791377e87?w=693&h=380&f=png&s=139764) 157 | 158 | 因此这个队列需要做的是**异步**且**去重**,因此我们用 `Set`作为数据结构储存 Watcher 来去重,同时用`Promise`模拟异步更新。 159 | 160 | ```JavaScript 161 | // 创建异步更新队列 162 | let queue = new Set() 163 | 164 | // 用Promise模拟nextTick 165 | function nextTick(cb) { 166 | Promise.resolve().then(cb) 167 | } 168 | 169 | // 执行刷新队列 170 | function flushQueue(args) { 171 | queue.forEach(watcher => { 172 | watcher.run() 173 | }) 174 | // 清空 175 | queue = new Set() 176 | } 177 | 178 | // 添加到队列 179 | export default function pushQueue(watcher) { 180 | queue.add(watcher) 181 | // 下一个循环调用 182 | nextTick(flushQueue) 183 | } 184 | 185 | ``` 186 | 187 | ## 梳理 188 | 189 | 我们梳理一下流程, 一个响应式系统是如何做到 UI(View)与状态(Modal)同步的? 190 | 191 | 我们首先需要监听 Modal, 本文中我们用 Proxy 来监听了 Modal 对象,因此在 Modal 对象被修改的时候我们的 Observer 就可以得知。 192 | 193 | 我们得知Modal发生变化后如何通知 View 呢?要知道,一个 Modal 的改变可能触发多个 UI 的更新,比如一个用户的用户名改变了,它的个人信息组件、通知组件等等组件中的用户名都需要改变,对于这种情况我们很容易想到利用**发布订阅**模式来解决,我们需要一个订阅器(Dep)来储存订阅者(Watcher),当监听到 Modal 改变时,我们只需要通知相关的订阅者进行更新即可。 194 | 195 | 那么订阅者来自哪里呢?其实每一个组件实例对应着一个订阅者(正因为一个组件实例对应一个订阅者,才能利用 Dep 通知到相应组件,不然乱套了,通知订阅者就相当于间接通知了组件)。 196 | 197 | 当订阅者得知了具体变化后它会进行相应的更新,将更新体现在 UI(View)上,至此UI 与 Modal 的同步完成了。 198 | 199 | > [完整代码](https://github.com/xiaomuzhu/proxy-vue)已经在 github 上,目前只实现了一个响应式系统,接下来会逐步实现一个完整的迷你版 mvvm 框架,所以你可以 star 或者 watch 来关注进度. 200 | 201 | ## 响应式系统并不是全部 202 | 203 | 响应式系统虽然是 Vue 的核心概念,但是一个响应式系统并不够. 204 | 205 | 响应式系统虽然得知了数据值的变化,但是当值不能完整映射 UI 时,我们依然需要进行组件级别的 reRender,这种情况并不高效,因此 Vue 在2.0版本引入了虚拟 DOM, 虚拟 DOM进行进一步的 diff 操作可以进行细粒度更高的操作,可以保证 reReander 的下限(保证不那么慢)。 206 | 207 | 除此之外为了方便开发者,vue 内置了众多的指令,因此我们还需要一个 vue 模板解析器. 208 | 209 | --- 210 | 211 | ## 公众号 212 | 213 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 214 | 215 | **简历模板:** 关注公众号回复「模板」获取 216 | 217 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 218 | 219 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 220 | -------------------------------------------------------------------------------- /docs/guide/event.md: -------------------------------------------------------------------------------- 1 | # 如何实现一个Event 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | 6 | ## 前言 7 | 8 | 本文标题的题目是由其他问题延伸而来,面试中面试官的常用套路,揪住一个问题一直深挖,在产生这个问题之前一定是这个问题. 9 | 10 | > React/Vue不同组件之间是怎么通信的? 11 | 12 | **Vue** 13 | 14 | 1. 父子组件用Props通信 15 | 2. 非父子组件用Event Bus通信 16 | 3. 如果项目够复杂,可能需要Vuex等全局状态管理库通信 17 | 4. `$dispatch`(已经废除)和`$broadcast`(已经废除) 18 | 19 | **React** 20 | 21 | 1. 父子组件,父->子直接用Props,子->父用callback回调 22 | 2. 非父子组件,用发布订阅模式的Event模块 23 | 3. 项目复杂的话用Redux、Mobx等全局状态管理管库 24 | 4. 用新的[Context Api](https://juejin.im/post/5a7b41605188257a6310fbec) 25 | 26 | 我们大体上都会有以上回答,接下来很可能会问到如何实现`Event(Bus)`,因为这个东西太重要了,几乎所有的模块通信都是基于类似的模式,包括安卓开发中的`Event Bus`,Node.js中的`Event`模块(Node中几乎所有的模块都依赖于Event,包括不限于`http、stream、buffer、fs`等). 27 | 28 | 我们仿照Node中[Event API](http://nodejs.cn/api/events.html)实现一个简单的Event库,他是**发布订阅模式**的典型应用. 29 | 30 | > **提前声明:** 我们没有对传入的参数进行及时判断而规避错误,仅仅对核心方法进行了实现. 31 | 32 | ## 基本构造 33 | 34 | ### 初始化class 35 | 36 | 我们利用ES6的`class`关键字对`Event`进行初始化,包括`Event`的事件清单和监听者上限. 37 | 38 | 我们选择了`Map`作为储存事件的结构,因为作为键值对的储存方式`Map`比一般对象更加适合,我们操作起来也更加简洁,可以先看一下Map的[基本用法与特点](http://es6.ruanyifeng.com/#docs/set-map#Map). 39 | 40 | ```javascript 41 | class EventEmeitter { 42 | constructor() { 43 | this._events = this._events || new Map(); // 储存事件/回调键值对 44 | this._maxListeners = this._maxListeners || 10; // 设立监听上限 45 | } 46 | } 47 | ``` 48 | 49 | ### 监听与触发 50 | 51 | 触发监听函数我们可以用`apply`与`call`两种方法,在少数参数时`call`的性能更好,多个参数时`apply`性能更好,当年Node的Event模块就在三个参数以下用`call`否则用`apply`. 52 | 53 | 当然当Node全面拥抱ES6+之后,相应的`call/apply`操作用`Reflect`新关键字重写了,但是我们不想写的那么复杂,就做了一个简化版. 54 | 55 | ```javascript 56 | 57 | // 触发名为type的事件 58 | EventEmeitter.prototype.emit = function(type, ...args) { 59 | let handler; 60 | // 从储存事件键值对的this._events中获取对应事件回调函数 61 | handler = this._events.get(type); 62 | if (args.length > 0) { 63 | handler.apply(this, args); 64 | } else { 65 | handler.call(this); 66 | } 67 | return true; 68 | }; 69 | 70 | // 监听名为type的事件 71 | EventEmeitter.prototype.addListener = function(type, fn) { 72 | // 将type事件以及对应的fn函数放入this._events中储存 73 | if (!this._events.get(type)) { 74 | this._events.set(type, fn); 75 | } 76 | }; 77 | 78 | ``` 79 | 80 | 我们实现了触发事件的`emit`方法和监听事件的`addListener`方法,至此我们就可以进行简单的实践了. 81 | 82 | ```javascript 83 | // 实例化 84 | const emitter = new EventEmeitter(); 85 | 86 | // 监听一个名为arson的事件对应一个回调函数 87 | emitter.addListener('arson', man => { 88 | console.log(`expel ${man}`); 89 | }); 90 | 91 | // 我们触发arson事件,发现回调成功执行 92 | emitter.emit('arson', 'low-end'); // expel low-end 93 | ``` 94 | 95 | 似乎不错,我们实现了基本的触发/监听,但是如果有多个监听者呢? 96 | 97 | ```javascript 98 | // 重复监听同一个事件名 99 | emitter.addListener('arson', man => { 100 | console.log(`expel ${man}`); 101 | }); 102 | emitter.addListener('arson', man => { 103 | console.log(`save ${man}`); 104 | }); 105 | 106 | emitter.emit('arson', 'low-end'); // expel low-end 107 | ``` 108 | 109 | 是的,只会触发第一个,因此我们需要进行改造. 110 | 111 | ## 升级改造 112 | 113 | ### 监听/触发器升级 114 | 115 | 我们的`addListener`实现方法还不够健全,在绑定第一个监听者之后,我们就无法对后续监听者进行绑定了,因此我们需要将后续监听者与第一个监听者函数放到一个数组里. 116 | 117 | ```javascript 118 | 119 | // 触发名为type的事件 120 | EventEmeitter.prototype.emit = function(type, ...args) { 121 | let handler; 122 | handler = this._events.get(type); 123 | if (Array.isArray(handler)) { 124 | // 如果是一个数组说明有多个监听者,需要依次此触发里面的函数 125 | for (let i = 0; i < handler.length; i++) { 126 | if (args.length > 0) { 127 | handler[i].apply(this, args); 128 | } else { 129 | handler[i].call(this); 130 | } 131 | } 132 | } else { // 单个函数的情况我们直接触发即可 133 | if (args.length > 0) { 134 | handler.apply(this, args); 135 | } else { 136 | handler.call(this); 137 | } 138 | } 139 | 140 | return true; 141 | }; 142 | 143 | // 监听名为type的事件 144 | EventEmeitter.prototype.addListener = function(type, fn) { 145 | const handler = this._events.get(type); // 获取对应事件名称的函数清单 146 | if (!handler) { 147 | this._events.set(type, fn); 148 | } else if (handler && typeof handler === 'function') { 149 | // 如果handler是函数说明只有一个监听者 150 | this._events.set(type, [handler, fn]); // 多个监听者我们需要用数组储存 151 | } else { 152 | handler.push(fn); // 已经有多个监听者,那么直接往数组里push函数即可 153 | } 154 | }; 155 | ``` 156 | 157 | 是的,从此以后可以愉快的触发多个监听者的函数了. 158 | 159 | ```javascript 160 | // 监听同一个事件名 161 | emitter.addListener('arson', man => { 162 | console.log(`expel ${man}`); 163 | }); 164 | emitter.addListener('arson', man => { 165 | console.log(`save ${man}`); 166 | }); 167 | 168 | emitter.addListener('arson', man => { 169 | console.log(`kill ${man}`); 170 | }); 171 | 172 | // 触发事件 173 | emitter.emit('arson', 'low-end'); 174 | //expel low-end 175 | //save low-end 176 | //kill low-end 177 | ``` 178 | 179 | ### 移除监听 180 | 181 | 我们会用`removeListener`函数移除监听函数,但是匿名函数是无法移除的. 182 | 183 | ```javascript 184 | EventEmeitter.prototype.removeListener = function(type, fn) { 185 | const handler = this._events.get(type); // 获取对应事件名称的函数清单 186 | 187 | // 如果是函数,说明只被监听了一次 188 | if (handler && typeof handler === 'function') { 189 | this._events.delete(type, fn); 190 | } else { 191 | let postion; 192 | // 如果handler是数组,说明被监听多次要找到对应的函数 193 | for (let i = 0; i < handler.length; i++) { 194 | if (handler[i] === fn) { 195 | postion = i; 196 | } else { 197 | postion = -1; 198 | } 199 | } 200 | // 如果找到匹配的函数,从数组中清除 201 | if (postion !== -1) { 202 | // 找到数组对应的位置,直接清除此回调 203 | handler.splice(postion, 1); 204 | // 如果清除后只有一个函数,那么取消数组,以函数形式保存 205 | if (handler.length === 1) { 206 | this._events.set(type, handler[0]); 207 | } 208 | } else { 209 | return this; 210 | } 211 | } 212 | }; 213 | ``` 214 | 215 | ### 发现问题 216 | 217 | 我们已经基本完成了`Event`最重要的几个方法,也完成了升级改造,可以说一个`Event`的骨架是被我们开发出来了,但是它仍然有不足和需要补充的地方. 218 | 219 | > 1. 鲁棒性不足: 我们没有对参数进行充分的判断,没有完善的报错机制. 220 | > 2. 模拟不够充分: 除了`removeAllListeners`这些方法没有实现以外,例如监听时间后会触发`newListener`事件,我们也没有实现,另外最开始的监听者上限我们也没有利用到. 221 | 222 | 当然,这在面试中现场写一个Event已经是很够意思了,主要是体现出来对**发布-订阅**模式的理解,以及针对多个监听状况下的处理,不可能现场撸几百行写一个完整Event. 223 | 224 | 索性[Event](https://github.com/Gozala/events/blob/master/events.js)库帮我们实现了完整的特性,整个代码量有300多行,很适合阅读,你可以花十分钟的时间通读一下,见识一下完整的Event实现. 225 | 226 | --- 227 | 228 | ## 公众号 229 | 230 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 231 | 232 | **简历模板:** 关注公众号回复「模板」获取 233 | 234 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 235 | 236 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 237 | -------------------------------------------------------------------------------- /docs/guide/htmlBasic.md: -------------------------------------------------------------------------------- 1 | # HTML基础 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | * [doctype(文档类型) 的作用是什么?✨](#doctype的作用是什么?✨) 6 | * [这三种模式的区别是什么?(接上一问追问)](#这三种模式的区别是什么?) 7 | * [HTML、XML 和 XHTML 有什么区别?](#html、xhtml、xml有什么区别) 8 | * [什么是data-属性?](#什么是data-属性?) 9 | * [你对HTML语义化的理解?✨](#你对html语义化的理解?✨) 10 | * [HTML5与HTML4的不同之处](#html5与html4的不同之处) 11 | * [有哪些常用的meta标签?](#有哪些常用的meta标签?) 12 | * [src和href的区别?](#src和href的区别?) 13 | * [知道img的srcset的作用是什么?(追问)](#知道img的srcset的作用是什么?(追问)) 14 | * [还有哪一个标签能起到跟srcset相似作用?(追问)](#还有哪一个标签能起到跟srcset相似作用?(追问)) 15 | * [script标签中defer和async的区别?✨](#script标签中defer和async的区别?✨) 16 | * [有几种前端储存的方式?✨](#有几种前端储存的方式?✨) 17 | * [这些方式的区别是什么?(追问)✨](#这些方式的区别是什么?(追问)✨) 18 | 19 | 本章是HTML考点的非重难点,因此我们采用简略回答的方式进行撰写,所以不会有太多详细的解释。 20 | 21 | > 我们约定,每个问题后我们标记『✨』的为高频面试题 22 | 23 | ## doctype的作用是什么?✨ 24 | 25 | DOCTYPE是html5标准网页声明,且必须声明在HTML文档的第一行。来告知浏览器的解析器用什么文档标准解析这个文档,不同的渲染模式会影响到浏览器对于 CSS 代码甚至 JavaScript 脚本的解析 26 | 27 | 文档解析类型有: 28 | 29 | * BackCompat:怪异模式,浏览器使用自己的怪异模式解析渲染页面。(如果没有声明DOCTYPE,默认就是这个模式) 30 | * CSS1Compat:标准模式,浏览器使用W3C的标准解析渲染页面。 31 | 32 | > IE8还有一种介乎于上述两者之间的近乎标准的模式,但是基本淘汰了。 33 | 34 | ## 这三种模式的区别是什么? 35 | 36 | * 标准模式(standards mode):页面按照 HTML 与 CSS 的定义渲染 37 | * 怪异模式(quirks mode)模式: 会模拟更旧的浏览器的行为 38 | * 近乎标准(almost standards)模式: 会实施了一种表单元格尺寸的怪异行为(与IE7之前的单元格布局方式一致),除此之外符合标准定义 39 | 40 | ## HTML、XHTML、XML有什么区别 41 | 42 | * HTML(超文本标记语言): 在html4.0之前HTML先有实现再有标准,导致HTML非常混乱和松散 43 | * XML(可扩展标记语言): 主要用于存储数据和结构,可扩展,大家熟悉的JSON也是相似的作用,但是更加轻量高效,所以XML现在市场越来越小了 44 | * XHTML(可扩展超文本标记语言): 基于上面两者而来,W3C为了解决HTML混乱问题而生,并基于此诞生了HTML5,开头加入``的做法因此而来,如果不加就是兼容混乱的HTML,加了就是标准模式。 45 | 46 | ## 什么是data-属性? 47 | 48 | HTML的数据属性,用于将数据储存于标准的HTML元素中作为额外信息,我们可以通过js访问并操作它,来达到操作数据的目的。 49 | 50 | ```html 51 |
56 | ... 57 |
58 | ``` 59 | 60 | > 前端框架出现之后,这种方法已经不流行了 61 | 62 | ## 你对HTML语义化的理解?✨ 63 | 64 | 语义化是指使用恰当语义的html标签,让页面具有良好的结构与含义,比如`

`标签就代表段落,`

`代表正文内容等等。 65 | 66 | 语义化的好处主要有两点: 67 | 68 | * 开发者友好:使用语义类标签增强了可读性,开发者也能够清晰地看出网页的结构,也更为便于团队的开发和维护 69 | * 机器友好:带有语义的文字表现力丰富,更适合搜索引擎的爬虫爬取有效信息,语义类还可以支持读屏软件,根据文章可以自动生成目录 70 | 71 | 这对于简书、知乎这种富文本类的应用很重要,语义化对于其网站的内容传播有很大的帮助,但是对于功能性的web软件重要性大打折扣,比如一个按钮、Skeleton这种组件根本没有对应的语义,也不需要什么SEO。 72 | 73 | ## HTML5与HTML4的不同之处 74 | 75 | * 文件类型声明()仅有一型:。 76 | * 新的解析顺序:不再基于SGML。 77 | * 新的元素:section, video, progress, nav, meter, time, aside, canvas, command, datalist, details, embed, figcaption, figure, footer, header, hgroup, keygen, mark, output, rp, rt, ruby, source, summary, wbr。 78 | * input元素的新类型:date, email, url等等。 79 | * 新的属性:ping(用于a与area), charset(用于meta), async(用于script)。 80 | * 全域属性:id, tabindex, repeat。 81 | * 新的全域属性:contenteditable, contextmenu, draggable, dropzone, hidden, spellcheck。 82 | * 移除元素:acronym, applet, basefont, big, center, dir, font, frame, frameset, isindex, noframes, strike, tt 83 | 84 | ## 有哪些常用的meta标签? 85 | 86 | meta标签由name和content两个属性来定义,来描述一个HTML网页文档的属性,例如作者、日期和时间、网页描述、关键词、页面刷新等,除了一些http标准规定了一些name作为大家使用的共识,开发者也可以自定义name。 87 | 88 | * charset,用于描述HTML文档的编码形式 89 | 90 | ```html 91 | 92 | ``` 93 | 94 | * http-equiv,顾名思义,相当于http的文件头作用,比如下面的代码就可以设置http的缓存过期日期 95 | 96 | ```html 97 | <meta http-equiv="expires" content="Wed, 20 Jun 2019 22:33:00 GMT"> 98 | ``` 99 | 100 | * viewport,移动前端最熟悉不过,Web开发人员可以控制视口的大小和比例 101 | 102 | ```html 103 | 104 | ``` 105 | 106 | * apple-mobile-web-app-status-bar-style,开发过PWA应用的开发者应该很熟悉,为了自定义评估工具栏的颜色。 107 | 108 | ```html 109 | 110 | ``` 111 | 112 | ## src和href的区别? 113 | 114 | * src是指向外部资源的位置,指向的内容会嵌入到文档中当前标签所在的位置,在请求src资源时会将其指向的资源下载并应用到文档内,如js脚本,img图片和frame等元素。当浏览器解析到该元素时,会暂停其他资源的下载和处理,知道将该资源加载、编译、执行完毕,所以一般js脚本会放在底部而不是头部。 115 | 116 | * href是指向网络资源所在位置(的超链接),用来建立和当前元素或文档之间的连接,当浏览器识别到它他指向的文件时,就会并行下载资源,不会停止对当前文档的处理。 117 | 118 | ## 知道img的srcset的作用是什么?(追问) 119 | 120 | 可以设计响应式图片,我们可以使用两个新的属性srcset 和 sizes来提供更多额外的资源图像和提示,帮助浏览器选择正确的一个资源。 121 | 122 | srcset 定义了我们允许浏览器选择的图像集,以及每个图像的大小。 123 | 124 | sizes 定义了一组媒体条件(例如屏幕宽度)并且指明当某些媒体条件为真时,什么样的图片尺寸是最佳选择。 125 | 126 | 所以,有了这些属性,浏览器会: 127 | 128 | * 查看设备宽度 129 | * 检查 sizes 列表中哪个媒体条件是第一个为真 130 | * 查看给予该媒体查询的槽大小 131 | * 加载 srcset 列表中引用的最接近所选的槽大小的图像 132 | 133 | > srcset提供了根据屏幕条件选取图片的能力 134 | 135 | ```html 136 | Clock 141 | ``` 142 | 143 | ## 还有哪一个标签能起到跟srcset相似作用?(追问) 144 | 145 | ``元素通过包含零或多个 `` 元素和一个 ``元素来为不同的显示/设备场景提供图像版本。浏览器会选择最匹配的子 `` 元素,如果没有匹配的,就选择 `` 元素的 src 属性中的URL。然后,所选图像呈现在``元素占据的空间中 146 | 147 | > picture同样可以通过不同设备来匹配不同的图像资源 148 | 149 | ```html 150 | 151 | 153 | 154 | 155 | ``` 156 | 157 | ## script标签中defer和async的区别?✨ 158 | 159 | * defer:浏览器指示脚本在文档被解析后执行,script被异步加载后并不会立刻执行,而是等待文档被解析完毕后执行。 160 | * async:同样是异步加载脚本,区别是脚本加载完毕后立即执行,这导致async属性下的脚本是乱序的,对于script有先后依赖关系的情况,并不适用。 161 | ![2019-06-13-07-13-42]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/c84fdc0e47268832fa8914ab4d125002.png) 162 | 163 | > 蓝色线代表网络读取,红色线代表执行时间,这俩都是针对脚本的;绿色线代表 HTML 解析 164 | 165 | ## 有几种前端储存的方式?✨ 166 | 167 | cookies、localstorage、sessionstorage、Web SQL、IndexedDB 168 | 169 | ## 这些方式的区别是什么?(追问)✨ 170 | 171 | * cookies: 在HTML5标准前本地储存的主要方式,优点是兼容性好,请求头自带cookie方便,缺点是大小只有4k,自动请求头加入cookie浪费流量,每个domain限制20个cookie,使用起来麻烦需要自行封装 172 | 173 | * localStorage:HTML5加入的以键值对(Key-Value)为标准的方式,优点是操作方便,永久性储存(除非手动删除),大小为5M,兼容IE8+ 174 | 175 | * sessionStorage:与localStorage基本类似,区别是sessionStorage当页面关闭后会被清理,而且与cookie、localStorage不同,他不能在所有同源窗口中共享,是会话级别的储存方式 176 | 177 | * Web SQL:2010年被W3C废弃的本地数据库数据存储方案,但是主流浏览器(火狐除外)都已经有了相关的实现,web sql类似于SQLite,是真正意义上的关系型数据库,用sql进行操作,当我们用JavaScript时要进行转换,较为繁琐。 178 | 179 | * IndexedDB: 是被正式纳入HTML5标准的数据库储存方案,它是NoSQL数据库,用键值对进行储存,可以进行快速读取操作,非常适合web场景,同时用JavaScript进行操作会非常方便。 180 | 181 | --- 182 | 参考链接: 183 | 184 | 1. [src与href](https://blog.csdn.net/Panda_m/article/details/78456358) 185 | 2. [语义化](https://www.zhihu.com/question/20455165) 186 | 3. [defer和async的区别](https://segmentfault.com/q/1010000000640869) 187 | 4. [响应式图片MDN](https://developer.mozilla.org/zh-CN/docs/Learn/HTML/Multimedia_and_embedding/Responsive_images) 188 | 5. [张鑫旭-srcset释义](https://www.zhangxinxu.com/wordpress/2014/10/responsive-images-srcset-size-w-descriptor/) 189 | 6. [picture元素-MDN](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/picture) 190 | 191 | --- 192 | 193 | ## 公众号 194 | 195 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 196 | 197 | **简历模板:** 关注公众号回复「模板」获取 198 | 199 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 200 | 201 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 202 | -------------------------------------------------------------------------------- /docs/guide/componentCli.md: -------------------------------------------------------------------------------- 1 | # 如何搭建一个组件库的开发环境 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | ## 技术选型 6 | 7 | ### css 解决方案 8 | 9 | 由于CSS 本身的众多缺陷,如书写繁琐(不支持嵌套)、样式易冲突(没有作用域概念)、缺少变量(不便于一键换主题)等不一而足。为了解决这些问题,社区里的解决方案也是出了一茬又一茬,从最早的 CSS prepocessor(SASS、LESS、Stylus)到后来的后起之秀 PostCSS,再到 CSS Modules、Styled-Components 等。 10 | 11 | Antd 选择了 less 作为 css 的预处理方案,Bootstrap 选择了 Scss,这两种方案孰优孰劣已经争论了很多年了: 12 | 13 | [SCSS和LESS相比有什么优势?](https://www.zhihu.com/question/34606506) 14 | 15 | 但是不管是哪种方案都有一个很烦人的点,就是需要额外引入 css,比如 Antd 需要这样显示引入: 16 | ```js 17 | import Button from 'antd/lib/button'; 18 | import 'antd/lib/button/style'; 19 | ``` 20 | 为了解决这种尴尬的情况,Antd 用 [Babel 插件](https://github.com/ant-design/babel-plugin-import)将这种情况 Hack 掉了 21 | 22 | 而`material-ui`并不存在这种情况,他不需要显示引入 css,这个最流行的 React 前端组件库里面只有 js 和 ts 两种代码,并不存在 css 相关的代码,为什么呢? 23 | 24 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167c7ebf0abe55f9?w=983&h=292&f=png&s=56375) 25 | 26 | 他们用 `jss` 作为css-in-js 的解决方案,jsx 的引入已经将 js 和 html 耦合,css-in-js将 css 也耦合进去,此时组件便不需要显示引入 css,而是直接引用 js 即可. 27 | 28 | 这不是退化到史前前端那种写内联样式的时代了吗? 29 | 30 | 并不是,史前前端的内联样式是整个项目耦合的状态,当然要被抛弃到历史的垃圾堆中,后来的样式和逻辑分离,实际上是以页面为维度将 js css html 解耦的过程,如今的时代是组件化的时代了,jsx 已经将 js 和 html 框定到一个组件中,css 依然处于分离状态,这就导致了每次引用组件却还需要显示引入 css,css-in-js 正式彻底组件化的解决方案. 31 | 32 | 当然,我个人目前在用 styled-components,其优点[引用](https://zhuanlan.zhihu.com/p/26878157)如下: 33 | 34 | 35 | 1. 首先,styled-components 所有语法都是标准 css 语法,同时支持 scss 嵌套等常用语法,覆盖了所有 css 场景。 36 | 37 | 2. 在样式复写场景下,styled-components 支持在任何地方注入全局 css,就像写普通 css 一样 38 | 39 | 3. styled-components 支持自定义 className,两种方式,一种是用 [babel 插件](https://github.com/styled-components/babel-plugin-styled-components), 另一种方式是使用 styled.div.withConfig({ componentId: "prefix-button-container" }) 相当于添加 className="prefix-button-container" 40 | 41 | 4. className 语义化更轻松,这也是 class 起名的初衷 42 | 43 | 5. 更适合组件库使用,直接引用 import "module" 即可,否则你有三条路可以走:像 antd 一样,单独引用 css,你需要给 node_modules 添加 css-loader;组件内部直接 import css 文件,如果任何业务项目没有 css-loader 就会报错;组件使用 scss 引用,所有业务项目都要配置一份 scss-loader 给 node_modules;这三种对组件库来说,都没有直接引用来的友好 44 | 45 | 6. 当你写一套组件库,需要单独发包,又有统一样式的配置文件需求,如果这个配置文件是 js 的,所有组件直接引用,对外完全不用关注。否则,如果是 scss 配置文件,摆在面前还是三条路:每个组件单独引用 scss 文件,需要每个业务项目给 node_modules 添加 scss-loader(如果业务用了 less,还要装一份 scss 是不);或者业务方只要使用了你的组件库,就要在入口文件引用你的 scss 文件,比如你的组件叫 button,scss 可能叫 common-css,别人听都没听过,还要查文档;或者业务方在 webpack 配置中单独引用你的 common-css,这也不科学,如果用了3个组件库,天天改 webpack 配置也很不方便。 46 | 47 | 7. 当 css 设置了一半样式,另一半真的需要 js 动态传入,你不得不 css + css-in-js 混合使用,项目久了,维护的时候发现某些 css-in-js 不变了,可以固化在 css 里,css 里固定的值又因为新去求变得可变了,你又得拿出来放在 css-in-js 里,实践过就知道有多么烦心。 48 | 49 | ### js 解决方案 50 | 51 | 选 Typescript ,因为巨硬大法好... 52 | 53 | 可以看看知乎问题下我的回答[你为什么不用 Typescript](https://www.zhihu.com/question/273619114/answer/369180721) 54 | 55 | 或者看此文[TypeScript体系调研报告](https://juejin.im/post/59c46bc86fb9a00a4636f939) 56 | 57 | ## 如何快速启动一个组件库项目 58 | 59 | 组件的具体实现部分当然是组件库的核心,但是在现代前端库中其他部分也必不可少,我们需要一堆工具来辅助我们开发,例如编译工具、代码检测工具、打包工具等等。 60 | 61 | ### 打包工具(rollup vs webpack) 62 | 63 | 市面上打包工具数不胜数,最火爆的当然是需要*配置工程师*专门配置的webpack,但是在类库开发领域它有一个强大的对手就是 rollup。 64 | 65 | 现代市面上主流的库基本都选择了 rollup 作为打包工具,包括Angular React 和 Vue, 作为基础类库的打包工具 rollup 的优势如下: 66 | * Tree Shaking: 自动移除未使用的代码, 输出更小的文件 67 | * Scope Hoisting: 所有模块构建在一个函数内, 执行效率更高 68 | * Config 文件支持通过 ESM 模块格式书写 69 | 可以一次输出多种格式: 70 | * 模块规范: IIFE, AMD, CJS, UMD, ESM 71 | Development 与 production 版本: .js, .min.js 72 | 73 | 虽然上面部分功能已经被 webpack 实现了,但是 rollup 明显引入得更早,而Scope Hoisting更是杀手锏,由于 webpack 不得不在打包代码中构建模块系统来适应 app 开发(模块系统对于单一类库用处很小),Scope Hoisting将模块构建在一个函数内的做法更适合类库的打包. 74 | 75 | ### 代码检测 76 | 77 | 由于 JavaScript 各种诡异的特性和大型前端项目的出现,代码检测工具已经是前端开发者的标配了,Douglas Crockford最早于2002创造出了 JSLint,但是其无法拓展,具有极强的Douglas Crockford个人色彩,Anton Kovalyov由于无法忍受 JSLint 无法拓展的行为在2011年发布了可拓展的JSHint,一时之间JSHint成为了前端代码检测的流行解决方案. 78 | 79 | 随后的2013年,Nicholas C. Zakas鉴于JSHint拓展的灵活度不够的问题开发了全新的基于 AST 的 Lint 工具 ESLint,并随着 ES6的流行统治了前端界,ESLint 基于Esprima进行 JavaScript 解析的特性极易拓展,JSHint 在很长一段时间无法支持 ES6语法导致被 ESLint 超越. 80 | 81 | 但是在 Typescript 领域 ESLint 却处于弱势地位,TSLint 的出现要比 ESLint 正式支持 Typescript 早很多,~~目前 TSLint 似乎是 TS 的事实上的代码检测工具~~. 82 | 83 | > 注: 文章成文较早,我也没想到前阵子 TS 官方钦点了 ESLint,TSLint 失宠了,面向未来的官方标配的代码检测工具肯定是 ESLint 了,但是 TSLint 目前依然被大量使用,现在仍然可以放心使用 84 | 85 | 代码检测工具是一方面,代码检测风格也需要我们做选择,市面上最流行的代码检测风格应该是 Airbnb 出品的`eslint-config-airbnb`,其最大的特点就是极其严格,没有给开发者任何选择的余地,当然在大型前端项目的开发中这种严格的代码风格是有利于协作的,但是作为一个类库的代码检测工具而言并不适合,所以我们选择了`eslint-config-standard`这种相对更为宽松的代码检测风格. 86 | 87 | ### commit 规范 88 | 89 | 以下两种 commit 哪个更严谨且易于维护? 90 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167ca31603fcf26e?w=519&h=395&f=png&s=72712) 91 | 92 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167ca324293d58e5?w=348&h=271&f=png&s=14674) 93 | 94 | 最开始使用 commit 的时候我也经常犯下图的错误,直到看到很多明星类库的 commit 才意识到自己的错误,写好 commit message 不仅有助于他人 review, 还可以有效的输出 CHANGELOG, 对项目的管理实际至关重要. 95 | 96 | 目前流行的方案是 Angular 团队的[规范](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines),其关于 head 的大致规范如下: 97 | 98 | * type: commit 的类型 99 | * feat: 新特性 100 | * fix: 修改问题 101 | * refactor: 代码重构 102 | * docs: 文档修改 103 | * style: 代码格式修改, 注意不是 css 修改 104 | * test: 测试用例修改 105 | * chore: 其他修改, 比如构建流程, 依赖管理. 106 | * scope: commit 影响的范围, 比如: route, component, utils, build... 107 | * subject: commit 的概述, 建议符合 50/72 formatting 108 | * body: commit 具体修改内容, 可以分为多行, 建议符合 50/72 formatting 109 | * footer: 一些备注, 通常是 BREAKING CHANGE 或修复的 bug 的链接. 110 | 111 | 当然规范人们不一定会遵守,我最初知道此类规范的时候也并没有严格遵循,因为人总会偷懒,直到用`commitizen`将此规范集成到工具流中,每个 commit 就不得不遵循规范了. 112 | 113 | 我具体参考了这篇文章: [优雅的提交你的 Git Commit Message](https://juejin.im/post/5afc5242f265da0b7f44bee4) 114 | 115 | ### 测试工具 116 | 117 | 业务开发中由于前端需求变动频繁的特性,导致前端对测试的要求并没有后端那么高,后端业务逻辑一旦定型变动很少,比较适合测试. 118 | 119 | 但是基础类库作为被反复依赖的模块和较为稳定的需求是必须做测试的,前端测试库也可谓是种类繁多了,经过比对之后我还是选择了目前最流行也是被三大框架同时选择了的 Jest 作为测试工具,其优点很明显: 120 | 121 | 1. 开箱即用,内置断言、测试覆盖率工具,如果你用 MoCha 那可得自己手动配置 n 多了 122 | 2. 快照功能,Jest 可以利用其特有的快照测试功能,通过比对 UI 代码生成的快照文件 123 | 3. 速度优势,Jest 的测试用例是并行执行的,而且只执行发生改变的文件所对应的测试,提升了测试速度 124 | 125 | ### 其它 126 | 127 | 当然以上是主要工具的选择,还有一些比如: 128 | 129 | * 代码美化工具 prettier,解放人肉美化,同时利于不同人协作的风格一致 130 | * 持续集成工具 travis-ci,解放人肉测试 lint,利于保证每次 push 的可靠程度 131 | 132 | ### 快速启动脚手架 133 | 134 | 那么以上这么多配置难道要我们每次都自己写吗?组件的具体实现才是组件库的核心,我们为什么要花这么多时间在配置上面? 135 | 136 | 我们在建立 APP 项目时通常会用到框架官方提供的脚手架,比如 React 的 create-react-app,Angular 的 Angular-Cli 等等,那么能不能有一个专门用于组件开发的快速启动的脚手架呢? 137 | 138 | 有的,我最近开发了一款快速启动组件库开发的命令行工具--[create-component](https://github.com/xiaomuzhu/create-component) 139 | 140 | 利用 141 | 142 | ```shell 143 | create-component init 144 | ``` 145 | 146 | 来快速启动项目,我们提供了丰富的可选配置,只要你做好技术选型后,根据提示去选择配置即可,create-component 会自动根据配置生成脚手架,其灵感就来源于 vue-cli和 Angular-cli. 147 | 148 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167ca5470645ad89?w=421&h=242&f=png&s=104820) 149 | 150 | --- 151 | 152 | ### 参考链接 153 | 154 | * [react-slick](https://github.com/akiran/react-slick) 155 | * [复杂组件设计](https://zhuanlan.zhihu.com/p/29034015) 156 | * [精读《请停止 css-in-js 的行为》](https://zhuanlan.zhihu.com/p/26878157) 157 | * [使用 rollup](https://zhuanlan.zhihu.com/p/34218678) 158 | 159 | --- 160 | 161 | ## 公众号 162 | 163 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 164 | 165 | **简历模板:** 关注公众号回复「模板」获取 166 | 167 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 168 | 169 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 170 | -------------------------------------------------------------------------------- /docs/guide/webpackMoudle.md: -------------------------------------------------------------------------------- 1 | # Webpack 模块机制 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | 6 | 本文来源于饿了么前端团队[从 Bundle 文件看 Webpack 模块机制](https://zhuanlan.zhihu.com/p/25954788) 7 | 8 | --- 9 | 10 | 我们知道 Webpack 是一个模块打包工具,但是它打包后的 bundle 文件到底长什么样呢?本文将通过一个最简单的示例,分析 Webpack 打包后为我们生成的代码,来阐述项目代码运行时的模块机制。
**示例所用 Webpack 版本为 2.3.0** 11 | 12 | ### 准备点材料 13 | webpack.config.js 14 | 15 | ```js 16 | const path = require('path') 17 | module.exports = { 18 | entry: './a.js', 19 | output: { 20 | path: path.resolve(__dirname, 'dist'), 21 | filename: 'bundle.js' 22 | } 23 | } 24 | ``` 25 | b.js 26 | 27 | ```js 28 | console.log('module b runs') 29 | export default { 30 | name: 'b' 31 | } 32 | ``` 33 | c.js 34 | 35 | ```js 36 | import b from './b' 37 | export default { 38 | name: 'c' 39 | } 40 | ``` 41 | a.js 42 | 43 | ```js 44 | import b from './b' 45 | import c from './c' 46 | console.log(b.name) 47 | console.log(c.name) 48 | ``` 49 | 简单来说,就是以 a.js 为入口文件,将 a.js,b.js 和 c.js 打包到 dist/bundle.js 文件中。
完整的示例代码请戳 [how-webpack-modules-work](https://link.zhihu.com/?target=https%3A//github.com/ywwhack/how-webpack-modules-work/tree/00-setup)
进入正题前,先思考个问题:**b.js 中 的 'module b runs' 会输出几次?**
换句话说,a.js 和 c.js 都引用了 b.js, 那么console.log('module b runs') 到底执行了几次?
答案是 1 次。为什么?我们往下看。 50 | 51 | ### 模块初始化函数 52 | 53 | 在 [build.js](https://link.zhihu.com/?target=https%3A//github.com/ywwhack/how-webpack-modules-work/blob/00-setup/dist/bundle.js%23L1) 中,有一个 modules 变量 54 | 55 | ```js 56 | // modules 通过 IIFE 的方式传入 57 | (function (modules) { 58 | ... 59 | })([ 60 | (function (module, __webpack_exports__, __webpack_require__) { 61 | ... 62 | }), 63 | (function (module, __webpack_exports__, __webpack_require__) { 64 | ... 65 | }), 66 | (function (module, __webpack_exports__, __webpack_require__) { 67 | ... 68 | }) 69 | ]) 70 | ``` 71 | 可以看到 modules 变量存的是一个数组,里面每一项都是一个函数,我们把这些函数叫作**模块初始化函数**。Webpack 在打包的时候,会做以下几件事: 72 | 73 | - 将每个文件视为一个独立的模块 74 | - 分析模块之间的依赖关系,将模块中 import export 相关的内容做一次替换,比如:
75 | ```js 76 | // c.js 77 | import b from './b' 78 | export default { 79 | name: 'c' 80 | } 81 | // 最后被转化为 82 | var __WEBPACK_IMPORTED_MODULE_0__b__ = __webpack_require__(0) 83 | // 这里需要特别注意一点, Webpack 将 a 属性作为模块的 default 值 84 | __webpack_exports__["a"] = ({ 85 | name: 'c' 86 | }) 87 | ``` 88 | 89 | - 给所有模块外面加一层包装函数,使其成为模块初始化函数
90 | - 把所有模块初始化函数合成一个数组,赋值给 modules 变量
91 | 92 | 拿 c.js 做个例子,它最后会被包装成如下形式: 93 | 94 | ```js 95 | function (module, __webpack_exports__, __webpack_require__) { 96 | var __WEBPACK_IMPORTED_MODULE_0__b__ = __webpack_require__(0) 97 | __webpack_exports__["a"] = ({ 98 | name: 'c' 99 | }) 100 | } 101 | ``` 102 | 103 | ### modules && __webpack_exports**__** 104 | 上面提到的模块初始化函数中,第一个参数叫 module,第二个参数叫 __webpack_exports__,它们有什么联系和区别呢?
module 可以理解成模块的「元信息」,里面不仅存着我们真正用到的模块内容,也存了一些模块 id 等信息。
__webpack_exports__ 是我们真正「require」时得到的对象。
这两个对象之间有如下的关系:module.exports === __webpack_exports__
在模块初始化函数执行过程中,会对 __webpack_exports__(刚传入的时候为空对象)赋上新的属性,这也是为什么我叫这个函数为模块初始化函数的原因 -- 整个过程就是为了「构造」出一个新的 __webpack_exports__对象。
Webpack 中还有一个 installedModules 变量,通过它来保存这些已加载过的模块的引用。 105 | 106 | ### 模块 id 107 | 每个模块有一个唯一的 id,这个 id 从 0 开始,是个整数,modules 和 installedModules 变量通过这个 id 来保存相应的模块初始化函数和模块引用。 为了更好的梳理它们三个之间的关系,我们再拿上面的 c.js 做例子: 108 | 109 | ```js 110 | // 假设 c.js 打包后的模块 id 为 1 111 | // 那么对应的 modules 和 installedModules 如下 112 | // 存的是一个函数 113 | modules[1] = function (module, __webpack_exports__, __webpack_require__) { 114 | var __WEBPACK_IMPORTED_MODULE_0__b__ = __webpack_require__(0) 115 | __webpack_exports__["a"] = ({ 116 | name: 'c' 117 | }) 118 | } 119 | // 存的是一个对象 120 | installedModules[1] = { 121 | moduleId: 1, 122 | l: true, 123 | exports: { 124 | a: { 125 | name: 'c' 126 | } 127 | } 128 | } 129 | ``` 130 | 可能有小伙伴会问:为什么不直接用文件名来作为模块 id,而是使用从 0 开始的整数?
原因如下: 131 | 132 | - 模块之间的文件名可能会重复,比如 components 和 containers 目录下都有个文件叫 menu.js,这样模块 id 就会有冲突 133 | - 相比用文件名做 id,使用数字最后打包的体积更小 134 | 135 | ### require 函数 136 | ``` 137 | function __webpack_require__(moduleId) { 138 | // 检查 installedModules 中是否存在对应的 module 139 | // 如果存在就返回 module.exports 140 | if (installedModules[moduleId]) 141 | return installedModules[moduleId].exports; 142 | // 创建一个新的 module 对象,用于下面函数的调用 143 | var module = installedModules[moduleId] = { 144 | i: moduleId, 145 | l: false, 146 | exports: {} 147 | }; 148 | // 从 modules 中找到对应的模块初始化函数并执行 149 | modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 150 | // 标识 module 已被加载过 151 | module.l = true; 152 | return module.exports; 153 | } 154 | ``` 155 | __webpack_require__ 做了以下几件事: 156 | 157 | 1. 根据 moduleId 查看 installedModules 中是否存在相应的 module ,如果存在就返回对应的 module.exports 158 | 2. 如果 module 不存在,就创建一个新的 module 对象,并且使 installedModules[moduleId] 指向新建的 module 对象 159 | 3. 根据 moduleId 从 modules 对象中找到对应的模块初始化函数并执行,依次传入 module,module.exports,__webpack_require__。可以看到,__webpack_require__ 被当作参数传入,使得所有模块内部都可以通过调用该函数来引入其他模块 160 | 4. 最后一步,返回 module.exports 161 | 162 | ### 最后,我们来改造一下 bundle.js 163 | ```js 164 | (function (modules) { 165 | // 存放模块初始化函数 166 | const installedModules = {} 167 | 168 | function require(moduleId) { 169 | // 检查 installedModules 中是否存在该 module 170 | // 如果存在就返回 module.exports 171 | if (installedModules[moduleId]) 172 | return installedModules[moduleId].exports 173 | // 创建一个新的 module 对象,用于下面函数的调用 174 | var module = installedModules[moduleId] = { 175 | i: moduleId, 176 | l: false, 177 | exports: {} 178 | } 179 | // 从 modules 中找到对应的模块初始化函数并执行 180 | modules[moduleId].call(module.exports, module, module.exports, require) 181 | // 标识 module 已被加载过 182 | module.l = true; 183 | return module.exports 184 | } 185 | // 执行入口模块,a.js 的 moduleId 为 2 186 | require(2) 187 | })( 188 | [ /* b.js, moduleId = 0 */ 189 | (function (module, exports, require) { 190 | console.log('module b runs') 191 | exports['a'] = ({ 192 | name: 'b' 193 | }) 194 | }), 195 | /* c.js, moduleId = 1 */ 196 | (function (module, exports, require) { 197 | const module_b = require(0) 198 | 199 | exports['a'] = ({ 200 | name: 'c' 201 | }) 202 | }), 203 | /* a.js, moduleId = 2 */ 204 | (function (module, exports, require) { 205 | const module_b = require(0) 206 | const module_c = require(1) 207 | console.log(module_b['a'].name) 208 | console.log(module_c['a'].name) 209 | }) 210 | ] 211 | ) 212 | ``` 213 | 214 | ### 总结 215 | 通过上面的分析,可以看到,一个简单的模块机制由这几个部分构成: 216 | 217 | - 一个数组用于保存所有的模块初始化函数 -- modules 218 | - 一个对象用于保存加载过的模块 -- installedModules 219 | - 一个模块加载函数 -- require 220 | 221 | 了解这些「黑盒」,有助于我们更好的理解模块化。在此之上,还可以进一步去研究加了 Code Splitting 之后的代码的样子,以及思考如何生成这样一个 bundle 文件。这些内容也非常丰富,值得大家去探索。 222 | 223 | --- 224 | 225 | ## 公众号 226 | 227 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 228 | 229 | **简历模板:** 关注公众号回复「模板」获取 230 | 231 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 232 | 233 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 234 | -------------------------------------------------------------------------------- /docs/guide/execute.md: -------------------------------------------------------------------------------- 1 | # 前端性能优化-执行篇 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | 我们已经介绍了前端加载方面的优化操作,在实际开发中大部分情况下我们解决的性能优化问题就是加载问题,但是我们依然会碰到一些高性能要求的场景需要优化我们的代码执行速度. 6 | 7 | 我们不会去介绍用for循环或者forEach那种更快,一方面,这种所谓的快慢在前端场景中的差距几乎是可以忽略的,另一方面,随着JS引擎的迭代,这种差距也会发生变化,并不具有普适性,我们更愿意在更宏观的层面来探究这个问题. 8 | 9 | ## 动画性能优化 10 | 11 | 动画性能不仅在前端,在任何客户端技术中心都是性能的重灾区,归根到底是需要大量的计算和渲染工作,远超普通的静态UI. 12 | 13 | 在前端实现动画有三种主流的方式: 14 | 15 | * Canvas 16 | * CSS3 17 | * Dom 18 | 19 | 当然,DOM+js的这种方式由于极易引起浏览器重绘或者回流,有非常大的性能风险,对于这种动画的优化方法就是不用DOM进行动画操作. 20 | 21 | ### CSS3动画优化原理 22 | 23 | 要想进行CSS的动画优化必须了解一定的浏览器原理,我们会介绍浏览器原理的几个概念,图层、重绘、回流。 24 | 25 | #### 图层 26 | 27 | 浏览器在渲染一个页面时,会将页面分为很多个图层,图层有大有小,每个图层上有一个或多个节点。在渲染DOM的时候,浏览器所做的工作实际上是: 28 | 29 | * 获取DOM后分割为多个图层 30 | * 对每个图层的节点计算样式结果(Recalculate style--样式重计算) 31 | * 为每个节点生成图形和位置(Layout--回流和重布局) 32 | * 将每个节点绘制填充到图层位图中(Paint Setup和Paint--重绘) 33 | * 图层作为纹理上传至GPU 34 | * 符合多个图层到页面上生成最终屏幕图像(Composite Layers--图层重组) 35 | 36 | #### 回流 37 | 38 | 有些节点,当你改变他时,会需要重新布局(这也意味着需要重新计算其他被影响的节点的位置和大小)。 39 | 40 | 这种情况下,被影响的DOM树越大(可见节点),重绘所需要的时间就会越长,而渲染一帧动画的时间也相应变长。所以需要尽力避免这些属性 41 | 42 | 一些常用的改变时会触发重布局的属性: 43 | 44 | 盒子模型相关属性会触发重布局: 45 | 46 | * width 47 | * height 48 | * padding 49 | * margin 50 | * display 51 | * border-width 52 | * border 53 | * min-height 54 | 55 | 定位属性及浮动也会触发重布局: 56 | 57 | * top 58 | * bottom 59 | * left 60 | * right 61 | * position 62 | * float 63 | * clear 64 | 65 | 改变节点内部文字结构也会触发重布局: 66 | 67 | * text-align 68 | * overflow-y 69 | * font-weight 70 | * overflow 71 | * font-family 72 | * line-height 73 | * vertival-align 74 | * white-space 75 | * font-size 76 | 77 | #### 重绘 78 | 79 | 修改时只触发重绘的属性有: 80 | 81 | * color 82 | * border-style 83 | * border-radius 84 | * visibility 85 | * text-decoration 86 | * background 87 | * background-image 88 | * background-position 89 | * background-repeat 90 | * background-size 91 | * outline-color 92 | * outline 93 | * outline-style 94 | * outline-width 95 | * box-shadow 96 | 97 | 这些属性都不会修改节点的大小和位置,自然不会触发重布局,但是节点内部的渲染效果进行了改变,所以只需要重绘就可以了. 98 | 99 | ### CSS3动画优化 100 | 101 | 经过上面的介绍,我们大致了解了浏览器的绘制原理,那么想进行css动画优化就要遵循以下原则: 102 | 103 | 1. 尽量将动画放在一个独立图层,这样可以避免动画效果影响其他渲染层的元素 104 | 2. 尽量避免回流和重绘 105 | 3. 尽量使用GPU,速度更快 106 | 107 | 因此,我们需要创建独立的合成层. 108 | 109 | 那么如何才能创建合成层呢? 110 | 111 | 直接原因(direct reason): 112 | 113 | * 硬件加速的 iframe 元素(比如 iframe 嵌入的页面中有合成层)demo 114 | * video 元素 115 | * 覆盖在 video 元素上的视频控制栏 116 | * 3D 或者 硬件加速的 2D Canvas 元素 117 | - demo:普通 2D Canvas 不会提升为合成层 118 | - demo:3D Canvas 提升为合成层 119 | 120 | * 硬件加速的插件,比如 flash 等等 121 | * 在 DPI 较高的屏幕上,fix 定位的元素会自动地被提升到合成层中。但在 DPI 较低的设备上却并非如此,因为这个渲染层的提升会使得字体渲染方式由子像素变为灰阶 122 | * 有 3D transform 123 | * backface-visibility 为 hidden 124 | * 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition(需要是 active 的 animation 或者 transition,当 animation 或者 transition 效果未开始或结束后,提升合成层也会失效) 125 | * will-change 设置为 opacity、transform、top、left、bottom、right(其中 top、left 等需要设置明确的定位属性,如 relative 等)demo 126 | * 后代元素原因: 127 | - 有合成层后代同时本身有 transform、opactiy(小于 1)、mask、fliter、reflection 属性 demo 128 | - 有合成层后代同时本身 overflow 不为 visible(如果本身是因为明确的定位因素产生的 SelfPaintingLayer,则需要 z-index 不为 auto) demo 129 | - 有合成层后代同时本身 fixed 定位 demo 130 | - 有 3D transfrom 的合成层后代同时本身有 preserves-3d 属性 demo 131 | - 有 3D transfrom 的合成层后代同时本身有 perspective 属性 demo 132 | 133 | 提升合成层的最好方式是使用 CSS 的 will-change 属性。从上一节合成层产生原因中,可以知道 will-change 设置为 opacity、transform、top、left、bottom、right 可以将元素提升为合成层。 134 | 135 | 关于合成层的更多知识可以移步淘宝FED的[无线性能优化:Composite](https://fed.taobao.org/blog/2016/04/25/performance-composite/) 136 | 137 | **那么如何避免重绘和回流?** 138 | 139 | 具体而言,就是多使用transform 或者 opacity 来实现动画效果,上述方法在合成层使用不会引起重绘和回流. 140 | 141 | **那么如何利用GPU加速呢?** 142 | 143 | 以下几个属性会获得GPU加速 144 | 145 | * opacity 146 | * translate 147 | * rotate 148 | * scale 149 | 150 | ### Canvas动画优化 151 | 152 | CSS虽然更加简单也更加保证性能的下限,但是要想实现更加复杂可控的动画,那就必须用到Canvas+JavaScript这个组合了. 153 | 154 | Canvas作为浏览器提供的2D图形绘制API本身有一定的复杂度,优化的方法非常多,我们仅仅介绍几种比较主流的优化方式. 155 | 156 | #### 运用`requestAnimationFrame` 157 | 158 | 很多时候我们会使用`setInterval`这种定时器来完成js动画循环,但是定时器在单线程的js环境下并不可靠,并不是能保证严格按照开发者的设置来进行动画循环,因此很多时候`setInterval`会引起掉帧的情况. 159 | 160 | 因此requestAnimationFrame的优势就体现出来了: 161 | 162 | * 性能更好: 优点是它能够将所有的动画都放到一个浏览器重绘周期里去做,这样能保存你的CPU的循环次数,提高性能 163 | * 开销更小: requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销 164 | 165 | #### 离屏canvas 166 | 167 | 离屏渲染的原理是把离屏 canvas当成一个缓存区。把需要重复绘制的画面数据进行缓存起来,减少调用 canvas的 API的消耗: 168 | 169 | 1. 创建离屏canvas; 170 | 2. 设置离屏canvas的宽高; 171 | 3. 在离屏canvas中进行绘制; 172 | 4. 在离屏canvas的全部或部分绘制到正在显示的canvas上 173 | 174 | #### 避免浮点运算 175 | 176 | 利用 canvas进行动画绘制时,如果计算出来的坐标是浮点数,那么可能会出现 CSS Sub-pixel的问题,也就是会自动将浮点数值四舍五入转为整数,那么在动画的过程中,由于元素实际运动的轨迹并不是严格按照计算公式得到,那么就可能出现抖动的情况,同时也可能让元素的边缘出现抗锯齿失真 177 | 这也是可能影响性能的一方面,因为一直在做不必要的取证运算. 178 | 179 | #### 减少调用Canvas API 180 | 181 | canvas也是通过操纵 js来绘制的,但是相比于正常的 js操作,调用 canvas API将更加消耗资源,所以在绘制之前请做好规划,通过 适量 js原生计算减少 canvas API的调用是一件比较划算的事情. 182 | 183 | 比如,作粒子效果时,尽量少使用圆,最好使用方形,因为粒子太小,所以方形看上去也跟圆差不多。至于原因,很容易理解,我们画一个圆需要三个步骤:先beginPath,然后用arc画弧,再用fill进行填充才能产生一个圆。但是画方形,只需要一个fillRect就可以了。虽然只是差了两个调用,当粒子对象数量达到一定时,这性能差距就会显示出来了。 184 | 185 | #### web worker 186 | 187 | 在进行某些耗时操作,例如计算大量数据,一帧中包含了太多的绘制状态,大规模的 DOM操作等,可能会导致页面卡顿,影响用户体验. 188 | 189 | web worker最常用的场景就是大量的频繁计算,减轻主线程压力,如果遇到大规模的计算,可以通过此 API分担主线程压力,此 API兼容性已经很不错了,既然 canvas可以用,那 web worker也就完全可以考虑使用. 190 | 191 | ## 大量数据性能优化 192 | 193 | ### 虚拟列表 194 | 195 | 我们在实际开发过程中会碰到一种场景,前端需要渲染大量数据(几千行数万行数据不等),而且还不允许分页,这种情况下网页会出现掉帧、卡顿甚至假死的情况。 196 | 197 | 这是典型的大量数据渲染的场景,在不能使用分页的情况下通常采用虚拟列表的方式来解决此问题. 198 | 199 | 因为 DOM 元素的创建和渲染需要的时间成本很高,在大数据的情况下,完整渲染列表所需要的时间不可接收。其中一个解决思路就是在任何情况下只对「可见区域」进行渲染,可以达到极高的初次渲染性能。 200 | 201 | 虚拟列表指的就是「可视区域渲染」的列表,重要的基本就是两个概念: 202 | 203 | 可滚动区域:假设有 1000 条数据,每个列表项的高度是 30,那么可滚动的区域的高度就是 1000 * 30。当用户改变列表的滚动条的当前滚动值的时候,会造成可见区域的内容的变更。 204 | 205 | 可见区域:比如列表的高度是 300,右侧有纵向滚动条可以滚动,那么视觉可见的区域就是可见区域。 206 | 实现虚拟列表就是处理滚动条滚动后的可见区域的变更,其中具体步骤如下: 207 | 208 | * 计算当前可见区域起始数据的 startIndex 209 | * 计算当前可见区域结束数据的 endIndex 210 | * 计算当前可见区域的数据,并渲染到页面中 211 | * 计算 startIndex 对应的数据在整个列表中的偏移位置 startOffset,并设置到列表上 212 | 建议参考下图理解一下上面的步骤: 213 | 214 | ![2019-08-10-00-16-58]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/17fb2b15b40f4dcde54a42623e2ac67e.png) 215 | 216 | > 虚拟滚动的具体实现原理可以参看饿了么前端的文章[再谈前端虚拟列表的实现](https://zhuanlan.zhihu.com/p/34585166) 217 | 218 | 219 | ### Web Worker 220 | 221 | 大量数据的渲染环节我们可以采用虚拟列表或者虚拟表格的方式实现,但是大量数据的计算环节依然会产生浏览器假死或者卡顿的情况. 222 | 223 | 通常情况下我们CPU密集型的任务都是交给后端计算的,但是有些时候我们需要处理一些离线场景或者解放后端压力,这个时候此方法就不奏效了. 224 | 225 | 还有一种方法是计算切片,使用 setTimeout 拆分密集型任务,但是有些计算无法利用此方法拆解,同时还可能产生副作用,这个方法需要视具体场景而动. 226 | 227 | 最后一种方法也是目前比较奏效的方法就是利用Web Worker 进行多线程编程. 228 | 229 | Web Worker 是一个独立的线程(独立的执行环境),这就意味着它可以完全和 UI 线程(主线程)并行的执行 js 代码,从而不会阻塞 UI,它和主线程是通过 onmessage 和 postMessage 接口进行通信的。 230 | 231 | Web Worker 使得网页中进行多线程编程成为可能。当主线程在处理界面事件时,worker 可以在后台运行,帮你处理大量的数据计算,当计算完成,将计算结果返回给主线程,由主线程更新 DOM 元素。 232 | 233 | > Web Worker的具体实现原理可以参看石墨前端的文章[再谈前端虚拟列表的实现](https://zhuanlan.zhihu.com/p/29165800) 234 | 235 | --- 236 | 参考: 237 | 238 | [canvas优化](https://juejin.im/post/5ba478136fb9a05d151ca173#heading-11) 239 | 240 | --- 241 | 242 | ## 公众号 243 | 244 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 245 | 246 | **简历模板:** 关注公众号回复「模板」获取 247 | 248 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 249 | 250 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 251 | -------------------------------------------------------------------------------- /docs/guide/deepclone.md: -------------------------------------------------------------------------------- 1 | # 实现深克隆 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | ## 前言 6 | 7 | 实现一个深克隆是面试中常见的问题的,可是绝大多数面试者的答案都是不完整的,甚至是错误的,这个时候面试官会不断追问,看看你到底理解不理解深克隆的原理,很多情况下一些一知半解的面试者就原形毕漏了. 8 | 9 | 我们就来看一下如何实现一个深克隆,当然面试中没有让你完整实现的时候,但是你一定要搞清楚其中的坑在哪里,才可以轻松应对面试官的追问. 10 |    11 | * JavaScript原始类型: Undefined、Null、Boolean、Number、String、Symbol 12 | * JavaScript引用类型:Object 13 | 14 | ## 浅克隆 15 | 16 |   **浅克隆**之所以被称为**浅克隆**,是因为对象只会被克隆最外部的一层,至于更深层的对象,则依然是通过引用指向同一块堆内存. 17 | 18 | ```javascript 19 | // 浅克隆函数 20 | function shallowClone(o) { 21 | const obj = {}; 22 | for ( let i in o) { 23 | obj[i] = o[i]; 24 | } 25 | return obj; 26 | } 27 | // 被克隆对象 28 | const oldObj = { 29 | a: 1, 30 | b: [ 'e', 'f', 'g' ], 31 | c: { h: { i: 2 } } 32 | }; 33 | 34 | const newObj = shallowClone(oldObj); 35 | console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 } 36 | console.log(oldObj.c.h === newObj.c.h); // true 37 | 38 | ``` 39 | 40 | 我们可以看到,很明显虽然`oldObj.c.h`被克隆了,但是它还与`oldObj.c.h`相等,这表明他们依然指向同一段堆内存,这就造成了如果对`newObj.c.h`进行修改,也会影响`oldObj.c.h`,这就不是一版好的克隆. 41 | 42 | ```javascript 43 | newObj.c.h.i = 'change'; 44 | console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 'change' } 45 | ``` 46 | 47 | 我们改变了`newObj.c.h.i `的值,`oldObj.c.h.i`也被改变了,这就是浅克隆的问题所在. 48 | 49 | 当然有一个新的api`Object.assign()`也可以实现浅复制,但是效果跟上面没有差别,所以我们不再细说了. 50 | 51 | ## 深克隆 52 | 53 | ### JSON.parse方法 54 | 55 | 前几年微博上流传着一个传说中最便捷实现深克隆的方法, 56 | JSON对象parse方法可以将JSON字符串反序列化成JS对象,stringify方法可以将JS对象序列化成JSON字符串,这两个方法结合起来就能产生一个便捷的深克隆. 57 | 58 | ```javascript 59 | const newObj = JSON.parse(JSON.stringify(oldObj)); 60 | ``` 61 | 62 | 我们依然用上一节的例子进行测试 63 | 64 | ```javascript 65 | const oldObj = { 66 | a: 1, 67 | b: [ 'e', 'f', 'g' ], 68 | c: { h: { i: 2 } } 69 | }; 70 | 71 | const newObj = JSON.parse(JSON.stringify(oldObj)); 72 | console.log(newObj.c.h, oldObj.c.h); // { i: 2 } { i: 2 } 73 | console.log(oldObj.c.h === newObj.c.h); // false 74 | newObj.c.h.i = 'change'; 75 | console.log(newObj.c.h, oldObj.c.h); // { i: 'change' } { i: 2 } 76 | 77 | ``` 78 | 79 | 果然,这是一个实现深克隆的好方法,但是这个解决办法是不是太过简单了. 80 | 81 | 确实,这个方法虽然可以解决绝大部分是使用场景,但是却有很多坑. 82 | 83 | 1. 他无法实现对函数 、RegExp等特殊对象的克隆 84 | 2. 会抛弃对象的constructor,所有的构造函数会指向Object 85 | 3. 对象有循环引用,会报错 86 | 87 | 主要的坑就是以上几点,我们一一测试下: 88 | 89 | ```javascript 90 | // 构造函数 91 | function person(pname) { 92 | this.name = pname; 93 | } 94 | 95 | const Messi = new person('Messi'); 96 | 97 | // 函数 98 | function say() { 99 | console.log('hi'); 100 | }; 101 | 102 | const oldObj = { 103 | a: say, 104 | b: new Array(1), 105 | c: new RegExp('ab+c', 'i'), 106 | d: Messi 107 | }; 108 | 109 | const newObj = JSON.parse(JSON.stringify(oldObj)); 110 | 111 | // 无法复制函数 112 | console.log(newObj.a, oldObj.a); // undefined [Function: say] 113 | // 稀疏数组复制错误 114 | console.log(newObj.b[0], oldObj.b[0]); // null undefined 115 | // 无法复制正则对象 116 | console.log(newObj.c, oldObj.c); // {} /ab+c/i 117 | // 构造函数指向错误 118 | console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: Object] [Function: person] 119 | 120 | ``` 121 | 122 | 我们可以看到在对函数、正则对象、稀疏数组等对象克隆时会发生意外,构造函数指向也会发生错误。 123 | 124 | ```javascript 125 | const oldObj = {}; 126 | 127 | oldObj.a = oldObj; 128 | 129 | const newObj = JSON.parse(JSON.stringify(oldObj)); 130 | console.log(newObj.a, oldObj.a); // TypeError: Converting circular structure to JSON 131 | ``` 132 | 133 | 对象的循环引用会抛出错误. 134 | 135 | ### 2.2 构造一个深克隆函数 136 | 137 | 我们知道要想实现一个靠谱的深克隆方法,上一节提到的**序列/反序列**是不可能了,而通常教程里提到的方法也是不靠谱的,他们存在的问题跟上一届序列反序列操作中凸显的问题是一致的. 138 | ![](https://user-gold-cdn.xitu.io/2018/3/28/1626bc7a5caf947c?w=555&h=298&f=png&s=64444) 139 | *(这个方法也会出现上一节提到的问题)* 140 | 141 | 由于要面对不同的对象(正则、数组、Date等)要采用不同的处理方式,我们需要实现一个对象类型判断函数。 142 | 143 | ```javascript 144 | const isType = (obj, type) => { 145 | if (typeof obj !== 'object') return false; 146 | const typeString = Object.prototype.toString.call(obj); 147 | let flag; 148 | switch (type) { 149 | case 'Array': 150 | flag = typeString === '[object Array]'; 151 | break; 152 | case 'Date': 153 | flag = typeString === '[object Date]'; 154 | break; 155 | case 'RegExp': 156 | flag = typeString === '[object RegExp]'; 157 | break; 158 | default: 159 | flag = false; 160 | } 161 | return flag; 162 | }; 163 | ``` 164 | 165 | 这样我们就可以对特殊对象进行类型判断了,从而采用针对性的克隆策略. 166 | 167 | ```javascript 168 | const arr = Array.of(3, 4, 5, 2); 169 | 170 | console.log(isType(arr, 'Array')); // true 171 | ``` 172 | 173 | 对于正则对象,我们在处理之前要先补充一点新知识. 174 | 175 | 我们需要通过[正则的扩展](http://es6.ruanyifeng.com/#docs/regex#flags-%E5%B1%9E%E6%80%A7)了解到`flags`属性等等,因此我们需要实现一个提取flags的函数. 176 | 177 | ```javascript 178 | const getRegExp = re => { 179 | var flags = ''; 180 | if (re.global) flags += 'g'; 181 | if (re.ignoreCase) flags += 'i'; 182 | if (re.multiline) flags += 'm'; 183 | return flags; 184 | }; 185 | ``` 186 | 187 | 做好了这些准备工作,我们就可以进行深克隆的实现了. 188 | 189 | ```javascript 190 | /** 191 | * deep clone 192 | * @param {[type]} parent object 需要进行克隆的对象 193 | * @return {[type]} 深克隆后的对象 194 | */ 195 | const clone = parent => { 196 | // 维护两个储存循环引用的数组 197 | const parents = []; 198 | const children = []; 199 | 200 | const _clone = parent => { 201 | if (parent === null) return null; 202 | if (typeof parent !== 'object') return parent; 203 | 204 | let child, proto; 205 | 206 | if (isType(parent, 'Array')) { 207 | // 对数组做特殊处理 208 | child = []; 209 | } else if (isType(parent, 'RegExp')) { 210 | // 对正则对象做特殊处理 211 | child = new RegExp(parent.source, getRegExp(parent)); 212 | if (parent.lastIndex) child.lastIndex = parent.lastIndex; 213 | } else if (isType(parent, 'Date')) { 214 | // 对Date对象做特殊处理 215 | child = new Date(parent.getTime()); 216 | } else { 217 | // 处理对象原型 218 | proto = Object.getPrototypeOf(parent); 219 | // 利用Object.create切断原型链 220 | child = Object.create(proto); 221 | } 222 | 223 | // 处理循环引用 224 | const index = parents.indexOf(parent); 225 | 226 | if (index != -1) { 227 | // 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象 228 | return children[index]; 229 | } 230 | parents.push(parent); 231 | children.push(child); 232 | 233 | for (let i in parent) { 234 | // 递归 235 | child[i] = _clone(parent[i]); 236 | } 237 | 238 | return child; 239 | }; 240 | return _clone(parent); 241 | }; 242 | ``` 243 | 244 | 我们做一下测试 245 | 246 | ```javascript 247 | function person(pname) { 248 | this.name = pname; 249 | } 250 | 251 | const Messi = new person('Messi'); 252 | 253 | function say() { 254 | console.log('hi'); 255 | } 256 | 257 | const oldObj = { 258 | a: say, 259 | c: new RegExp('ab+c', 'i'), 260 | d: Messi, 261 | }; 262 | 263 | oldObj.b = oldObj; 264 | 265 | 266 | const newObj = clone(oldObj); 267 | console.log(newObj.a, oldObj.a); // [Function: say] [Function: say] 268 | console.log(newObj.b, oldObj.b); // { a: [Function: say], c: /ab+c/i, d: person { name: 'Messi' }, b: [Circular] } { a: [Function: say], c: /ab+c/i, d: person { name: 'Messi' }, b: [Circular] } 269 | console.log(newObj.c, oldObj.c); // /ab+c/i /ab+c/i 270 | console.log(newObj.d.constructor, oldObj.d.constructor); // [Function: person] [Function: person] 271 | ``` 272 | 273 | 当然,我们这个深克隆还不算完美,例如Buffer对象、Promise、Set、Map可能都需要我们做特殊处理,另外对于确保没有循环引用的对象,我们可以省去对循环引用的特殊处理,因为这很消耗时间,不过一个基本的深克隆函数我们已经实现了。 274 | 275 | --- 276 | 277 | ## 总结 278 | 279 | 实现一个完整的深克隆是由许多坑要踩的,npm上一些库的实现也不够完整,在生产环境中最好用`lodash`的深克隆实现. 280 | 281 | 在面试过程中,我们上面提到的众多坑是面试官很可能追问你的,要知道坑在哪里,能答出来才是你的加分项,在面试过程中必须要有一两个闪光点,如果只知道**序列/反序列**这种投机取巧的方法,在追问下不仅拿不到分,很可能造成只懂个皮毛的印象,毕竟,面试面得就是你知识的深度. 282 | 283 | --- 284 | 285 | ## 公众号 286 | 287 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 288 | 289 | **简历模板:** 关注公众号回复「模板」获取 290 | 291 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 292 | 293 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 294 | -------------------------------------------------------------------------------- /docs/guide/vue.md: -------------------------------------------------------------------------------- 1 | # Vue面试题 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | Vue框架部分我们会涉及一些高频且有一定探讨价值的面试题,我们不会涉及一些非常初级的在官方文档就能查看的纯记忆性质的面试题,比如: 6 | 7 | * vue常用的修饰符? 8 | * vue-cli 工程常用的 npm 命令有哪些? 9 | * vue中 keep-alive 组件的作用? 10 | 11 | 首先,上述类型的面试题在文档中可查,没有比官方文档更权威的答案了,其次这种问题没有太大价值,除了考察候选人的记忆力,最后,这种面试题只要用过vue的都知道,没有必要占用我们的篇幅. 12 | 13 | 我们的问题并不多,但是难度可能会高一些,如果你真的搞懂了这些问题,在绝大多数情况下会有举一反三的效果,可以说基本能拿下Vue相关的所有重要知识点了. 14 | 15 | ## 你对MVVM的理解? 16 | 17 | ### MVVM是什么? 18 | 19 | MVVM 模式,顾名思义即 Model-View-ViewModel 模式。它萌芽于2005年微软推出的基于 Windows 的用户界面框架 WPF ,前端最早的 MVVM 框架 knockout 在2010年发布。 20 | 21 | Model 层: 对应数据层的域模型,它主要做域模型的同步。通过 Ajax/fetch 等 API 完成客户端和服务端业务 Model 的同步。在层间关系里,它主要用于抽象出 ViewModel 中视图的 Model。 22 | 23 | View 层:作为视图模板存在,在 MVVM 里,整个 View 是一个动态模板。除了定义结构、布局外,它展示的是 ViewModel 层的数据和状态。View 层不负责处理状态,View 层做的是 数据绑定的声明、 指令的声明、 事件绑定的声明。 24 | 25 | ViewModel 层:把 View 需要的层数据暴露,并对 View 层的 数据绑定声明、 指令声明、 事件绑定声明 负责,也就是处理 View 层的具体业务逻辑。ViewModel 底层会做好绑定属性的监听。当 ViewModel 中数据变化,View 层会得到更新;而当 View 中声明了数据的双向绑定(通常是表单元素),框架也会监听 View 层(表单)值的变化。一旦值变化,View 层绑定的 ViewModel 中的数据也会得到自动更新。 26 | 27 | ![2019-07-16-21-47-05]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d55fe97b6ef63370645754e1d4a760b6.png) 28 | 29 | ### MVVM的优缺点? 30 | 31 | 优点: 32 | 33 | 1. 分离视图(View)和模型(Model),降低代码耦合,提高视图或者逻辑的重用性: 比如视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定不同的"View"上,当View变化的时候Model不可以不变,当Model变化的时候View也可以不变。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑 34 | 2. 提高可测试性: ViewModel的存在可以帮助开发者更好地编写测试代码 35 | 3. 自动更新dom: 利用双向绑定,数据更新后视图自动更新,让开发者从繁琐的手动dom中解放 36 | 37 | 缺点: 38 | 39 | 1. Bug很难被调试: 因为使用双向绑定的模式,当你看到界面异常了,有可能是你View的代码有Bug,也可能是Model的代码有问题。数据绑定使得一个位置的Bug被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。另外,数据绑定的声明是指令式地写在View的模版当中的,这些内容是没办法去打断点debug的 40 | 2. 一个大的模块中model也会很大,虽然使用方便了也很容易保证了数据的一致性,当时长期持有,不释放内存就造成了花费更多的内存 41 | 3. 对于大型的图形应用程序,视图状态较多,ViewModel的构建和维护的成本都会比较高 42 | 43 | ## 你对Vue生命周期的理解? 44 | 45 | ### 生命周期是什么 46 | 47 | Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是Vue的生命周期。 48 | 49 | ### 各个生命周期的作用 50 | 51 | | 生命周期 | 描述 | 52 | | -------- | ----- | 53 | | beforeCreate | 组件实例被创建之初,组件的属性生效之前 | 54 | | created | 组件实例已经完全创建,属性也绑定,但真实dom还没有生成,`$el`还不可用 | 55 | | beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 | 56 | | mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 | 57 | | beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 | 58 | | update | 组件数据更新之后 | 59 | | activited | keep-alive专属,组件被激活时调用 | 60 | | deadctivated | keep-alive专属,组件被销毁时调用 | 61 | | beforeDestory | 组件销毁前调用 | 62 | | destoryed | 组件销毁后调用 | 63 | 64 | ### 生命周期示意图 65 | 66 | ![2019-06-23-05-03-43]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d1279e6d6327d23f2e97bb0bf4950b47.png) 67 | 68 | ## 异步请求适合在哪个生命周期调用? 69 | 70 | 官方实例的异步请求是在mounted生命周期中调用的,而实际上也可以在created生命周期中调用。 71 | 72 | ## Vue组件如何通信? 73 | 74 | Vue组件通信的方法如下: 75 | 76 | * props/\$emit+v-on: 通过props将数据自上而下传递,而通过$emit和v-on来向上传递信息。 77 | * EventBus: 通过EventBus进行信息的发布与订阅 78 | * vuex: 是全局数据管理库,可以通过vuex管理全局的数据流 79 | * \$attrs/\$listeners: Vue2.4中加入的$attrs/$listeners可以进行跨级的组件通信 80 | * provide/inject:以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础 81 | 82 | 还有一些用solt插槽或者ref实例进行通信的,使用场景过于有限就不赘述了。 83 | 84 | > 详细可以参考这篇文章[vue中8种组件通信方式](https://juejin.im/post/5d267dcdf265da1b957081a3),不过太偏门的通信方式根本不会用到,单做知识点了解即可 85 | 86 | ## computed和watch有什么区别? 87 | 88 | computed: 89 | 90 | 1. `computed`是计算属性,也就是计算值,它更多用于计算值的场景 91 | 2. `computed`具有缓存性,computed的值在getter执行后是会缓存的,只有在它依赖的属性值改变之后,下一次获取computed的值时才会重新调用对应的getter来计算 92 | 3. `computed`适用于计算比较消耗性能的计算场景 93 | 94 | watch: 95 | 96 | 1. 更多的是「观察」的作用,类似于某些数据的监听回调,用于观察`props` `$emit`或者本组件的值,当数据变化时来执行回调进行后续操作 97 | 2. 无缓存性,页面重新渲染时值不变化也会执行 98 | 99 | 小结: 100 | 101 | 1. 当我们要进行数值计算,而且依赖于其他数据,那么把这个数据设计为computed 102 | 2. 如果你需要在某个数据变化时做一些事情,使用watch来观察这个数据变化 103 | 104 | ## Vue是如何实现双向绑定的? 105 | 106 | 利用`Object.defineProperty`劫持对象的访问器,在属性值发生变化时我们可以获取变化,然后根据变化进行后续响应,在vue3.0中通过Proxy代理对象进行类似的操作。 107 | 108 | ```JavaScript 109 | // 这是将要被劫持的对象 110 | const data = { 111 | name: '', 112 | }; 113 | 114 | function say(name) { 115 | if (name === '古天乐') { 116 | console.log('给大家推荐一款超好玩的游戏'); 117 | } else if (name === '渣渣辉') { 118 | console.log('戏我演过很多,可游戏我只玩贪玩懒月'); 119 | } else { 120 | console.log('来做我的兄弟'); 121 | } 122 | } 123 | 124 | // 遍历对象,对其属性值进行劫持 125 | Object.keys(data).forEach(function(key) { 126 | Object.defineProperty(data, key, { 127 | enumerable: true, 128 | configurable: true, 129 | get: function() { 130 | console.log('get'); 131 | }, 132 | set: function(newVal) { 133 | // 当属性值发生变化时我们可以进行额外操作 134 | console.log(`大家好,我系${newVal}`); 135 | say(newVal); 136 | }, 137 | }); 138 | }); 139 | 140 | data.name = '渣渣辉'; 141 | //大家好,我系渣渣辉 142 | //戏我演过很多,可游戏我只玩贪玩懒月 143 | ``` 144 | 145 | > 详细实现见[Proxy比defineproperty优劣对比?](devsProxy.md) 146 | 147 | ## Proxy与Object.defineProperty的优劣对比? 148 | 149 | Proxy的优势如下: 150 | 151 | * Proxy可以直接监听对象而非属性 152 | * Proxy可以直接监听数组的变化 153 | * Proxy有多达13种拦截方法,不限于apply、ownKeys、deleteProperty、has等等是`Object.defineProperty`不具备的 154 | * Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而`Object.defineProperty`只能遍历对象属性直接修改 155 | * Proxy作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利 156 | 157 | Object.defineProperty的优势如下: 158 | 159 | * 兼容性好,支持IE9 160 | 161 | > 详细实现见[Proxy比defineproperty优劣对比?](devsProxy.md) 162 | 163 | ## 你是如何理解Vue的响应式系统的? 164 | 165 | ![2019-07-22-16-29-59]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d5bfe6c9f35554783bd618edc15ec274.png) 166 | 167 | 响应式系统简述: 168 | 169 | * 任何一个 Vue Component 都有一个与之对应的 Watcher 实例。 170 | * Vue 的 data 上的属性会被添加 getter 和 setter 属性。 171 | * 当 Vue Component render 函数被执行的时候, data 上会被 触碰(touch), 即被读, getter 方法会被调用, 此时 Vue 会去记录此 Vue component 所依赖的所有 data。(这一过程被称为依赖收集) 172 | * data 被改动时(主要是用户操作), 即被写, setter 方法会被调用, 此时 Vue 会去通知所有依赖于此 data 的组件去调用他们的 render 函数进行更新。 173 | 174 | > [深入响应式系统](reactivity.md) 175 | 176 | ## 既然Vue通过数据劫持可以精准探测数据变化,为什么还需要虚拟DOM进行diff检测差异? 177 | 178 | 考点: Vue的变化侦测原理 179 | 180 | 前置知识: 依赖收集、虚拟DOM、响应式系统 181 | 182 | 现代前端框架有两种方式侦测变化,一种是pull一种是push 183 | 184 | pull: 其代表为React,我们可以回忆一下React是如何侦测到变化的,我们通常会用`setState`API显式更新,然后React会进行一层层的Virtual Dom Diff操作找出差异,然后Patch到DOM上,React从一开始就不知道到底是哪发生了变化,只是知道「有变化了」,然后再进行比较暴力的Diff操作查找「哪发生变化了」,另外一个代表就是Angular的脏检查操作。 185 | 186 | push: Vue的响应式系统则是push的代表,当Vue程序初始化的时候就会对数据data进行依赖的收集,一但数据发生变化,响应式系统就会立刻得知,因此Vue是一开始就知道是「在哪发生变化了」,但是这又会产生一个问题,如果你熟悉Vue的响应式系统就知道,通常一个绑定一个数据就需要一个Watcher,一但我们的绑定细粒度过高就会产生大量的Watcher,这会带来内存以及依赖追踪的开销,而细粒度过低会无法精准侦测变化,因此Vue的设计是选择中等细粒度的方案,在组件级别进行push侦测的方式,也就是那套响应式系统,通常我们会第一时间侦测到发生变化的组件,然后在组件内部进行Virtual Dom Diff获取更加具体的差异,而Virtual Dom Diff则是pull操作,Vue是push+pull结合的方式进行变化侦测的. 187 | 188 | ## Vue为什么没有类似于React中shouldComponentUpdate的生命周期? 189 | 190 | 考点: Vue的变化侦测原理 191 | 192 | 前置知识: 依赖收集、虚拟DOM、响应式系统 193 | 194 | 根本原因是Vue与React的变化侦测方式有所不同 195 | 196 | React是pull的方式侦测变化,当React知道发生变化后,会使用Virtual Dom Diff进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要用shouldComponentUpdate进行手动操作来减少diff,从而提高程序整体的性能. 197 | 198 | Vue是pull+push的方式侦测变化的,在一开始就知道那个组件发生了变化,因此在push的阶段并不需要手动控制diff,而组件内部采用的diff方式实际上是可以引入类似于shouldComponentUpdate相关生命周期的,但是通常合理大小的组件不会有过量的diff,手动优化的价值有限,因此目前Vue并没有考虑引入shouldComponentUpdate这种手动优化的生命周期. 199 | 200 | ## Vue中的key到底有什么用? 201 | 202 | `key`是为Vue中的vnode标记的唯一id,通过这个key,我们的diff操作可以更准确、更快速 203 | 204 | diff算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的`key`与旧节点进行比对,然后超出差异. 205 | 206 | > diff程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较,这四种比较方式就是首、尾、旧尾新头、旧头新尾. 207 | 208 | * 准确: 如果不加`key`,那么vue会选择复用节点(Vue的就地更新策略),导致之前节点的状态被保留下来,会产生一系列的bug. 209 | * 快速: key的唯一性可以被Map数据结构充分利用,相比于遍历查找的时间复杂度O(n),Map的时间复杂度仅仅为O(1). 210 | 211 | ![2019-07-26-14-52-57]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/8edce49381a9f6198faa60d7af73f74b.png) 212 | 213 | 218 | 219 | 220 | --- 221 | 222 | ## 公众号 223 | 224 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 225 | 226 | **简历模板:** 关注公众号回复「模板」获取 227 | 228 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 229 | 230 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 231 | -------------------------------------------------------------------------------- /docs/guide/webpack.md: -------------------------------------------------------------------------------- 1 | # webpack面试题 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | webpack是事实上的前端打包标准,相关的面试题也是面试的热点. 6 | 7 | ## webpack与grunt、gulp的不同? 8 | 9 | Grunt、Gulp是基于任务运行的工具: 10 | 11 | 它们会自动执行指定的任务,就像流水线,把资源放上去然后通过不同插件进行加工,它们包含活跃的社区,丰富的插件,能方便的打造各种工作流。 12 | 13 | Webpack是基于模块化打包的工具: 14 | 15 | 自动化处理模块,webpack把一切当成模块,当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。 16 | 17 | 因此这是完全不同的两类工具,而现在主流的方式是用npm script代替Grunt、Gulp,npm script同样可以打造任务流. 18 | 19 | ## webpack、rollup、parcel优劣? 20 | 21 | * webpack适用于大型复杂的前端站点构建: webpack有强大的loader和插件生态,打包后的文件实际上就是一个立即执行函数,这个立即执行函数接收一个参数,这个参数是模块对象,键为各个模块的路径,值为模块内容。立即执行函数内部则处理模块之间的引用,执行模块等,这种情况更适合文件依赖复杂的应用开发. 22 | * rollup适用于基础库的打包,如vue、d3等: Rollup 就是将各个模块打包进一个文件中,并且通过 Tree-shaking 来删除无用的代码,可以最大程度上降低代码体积,但是rollup没有webpack如此多的的如代码分割、按需加载等高级功能,其更聚焦于库的打包,因此更适合库的开发. 23 | * parcel适用于简单的实验性项目: 他可以满足低门槛的快速看到效果,但是生态差、报错信息不够全面都是他的硬伤,除了一些玩具项目或者实验项目不建议使用 24 | 25 | ![2019-08-03-02-53-46]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/14f023a939e7a89330629f4cffb70cfd.png) 26 | 27 | ![2019-08-03-02-53-34]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/e790108aaba2c7c7a2e7393de102b1b5.png) 28 | 29 | ## 有哪些常见的Loader? 30 | 31 | * file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件 32 | * url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去 33 | * source-map-loader:加载额外的 Source Map 文件,以方便断点调试 34 | * image-loader:加载并且压缩图片文件 35 | * babel-loader:把 ES6 转换成 ES5 36 | * css-loader:加载 CSS,支持模块化、压缩、文件导入等特性 37 | * style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSS。 38 | * eslint-loader:通过 ESLint 检查 JavaScript 代码 39 | 40 | ## 有哪些常见的Plugin? 41 | 42 | * define-plugin:定义环境变量 43 | * html-webpack-plugin:简化html文件创建 44 | * uglifyjs-webpack-plugin:通过`UglifyES`压缩`ES6`代码 45 | * webpack-parallel-uglify-plugin: 多核压缩,提高压缩速度 46 | * webpack-bundle-analyzer: 可视化webpack输出文件的体积 47 | * mini-css-extract-plugin: CSS提取到单独的文件中,支持按需加载 48 | 49 | ## 分别介绍bundle,chunk,module是什么 50 | 51 | * bundle:是由webpack打包出来的文件 52 | * chunk:代码块,一个chunk由多个模块组合而成,用于代码的合并和分割 53 | * module:是开发中的单个模块,在webpack的世界,一切皆模块,一个模块对应一个文件,webpack会从配置的entry中递归开始找出所有依赖的模块 54 | 55 | ## Loader和Plugin的不同? 56 | 57 | **不同的作用:** 58 | 59 | * **Loader**直译为"加载器"。Webpack将一切文件视为模块,但是webpack原生是只能解析js文件,如果想将其他文件也打包的话,就会用到`loader`。 所以Loader的作用是让webpack拥有了加载和解析_非JavaScript文件_的能力。 60 | * **Plugin**直译为"插件"。Plugin可以扩展webpack的功能,让webpack具有更多的灵活性。 在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。 61 | 62 | **不同的用法:** 63 | 64 | * **Loader**在`module.rules`中配置,也就是说他作为模块的解析规则而存在。 类型为数组,每一项都是一个`Object`,里面描述了对于什么类型的文件(`test`),使用什么加载(`loader`)和使用的参数(`options`) 65 | * **Plugin**在`plugins`中单独配置。 类型为数组,每一项是一个`plugin`的实例,参数都通过构造函数传入。 66 | 67 | ## webpack的构建流程是什么? 68 | 69 | Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程: 70 | 71 | 1. 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数; 72 | 2. 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译; 73 | 3. 确定入口:根据配置中的 entry 找出所有的入口文件; 74 | 4. 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理; 75 | 5. 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系; 76 | 6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会; 77 | 7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。 78 | 79 | 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。 80 | 81 | > 来源于[深入浅出webpack第五章](https://wangchong.tech/webpack/5%E5%8E%9F%E7%90%86/5-1%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E6%A6%82%E6%8B%AC.html) 82 | 83 | > 拓展阅读[细说 webpack 之流程篇](http://taobaofed.org/blog/2016/09/09/webpack-flow/) 84 | 85 | ## 是否写过Loader和Plugin?描述一下编写loader或plugin的思路? 86 | 87 | Loader像一个"翻译官"把读到的源文件内容转义成新的文件内容,并且每个Loader通过链式操作,将源文件一步步翻译成想要的样子。 88 | 89 | 编写Loader时要遵循单一原则,每个Loader只做一种"转义"工作。 每个Loader的拿到的是源文件内容(`source`),可以通过返回值的方式将处理后的内容输出,也可以调用`this.callback()`方法,将内容返回给webpack。 还可以通过 `this.async()`生成一个`callback`函数,再用这个callback将处理后的内容输出出去。 此外`webpack`还为开发者准备了开发loader的工具函数集——`loader-utils`。 90 | 91 | 相对于Loader而言,Plugin的编写就灵活了许多。 webpack在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。 92 | 93 | ## webpack的热更新是如何做到的?说明其原理? 94 | 95 | webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。 96 | 97 | **原理:** 98 | 99 | ![2019-08-03-15-45-12]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/c0863ad3d922fceccfc8290e39bb2474.png) 100 | 101 | 首先要知道server端和client端都做了处理工作 102 | 103 | 1. 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中。 104 | 2. 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中。 105 | 3. 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念。 106 | 4. 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换。 107 | 5. webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了。 108 | 6. HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码。这就是上图中 7、8、9 步骤。 109 | 7. 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用。 110 | 8. 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码。 111 | 112 | > 详细原理解析来源于知乎饿了么前端[Webpack HMR 原理解析](https://zhuanlan.zhihu.com/p/30669007) 113 | 114 | ## 如何用webpack来优化前端性能? 115 | 116 | 用webpack优化前端性能是指优化webpack的输出结果,让打包的最终结果在浏览器运行快速高效。 117 | 118 | * 压缩代码:删除多余的代码、注释、简化代码的写法等等方式。可以利用webpack的`UglifyJsPlugin`和`ParallelUglifyPlugin`来压缩JS文件, 利用`cssnano`(css-loader?minimize)来压缩css 119 | * 120 | * 利用CDN加速: 在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对于`output`参数和各loader的`publicPath`参数来修改资源路径 121 | * Tree Shaking: 将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数`--optimize-minimize`来实现 122 | * Code Splitting: 将代码按路由维度或者组件分块(chunk),这样做到按需加载,同时可以充分利用浏览器缓存 123 | * 提取公共第三方库:  SplitChunksPlugin插件来进行公共模块抽取,利用浏览器缓存可以长期缓存这些无需频繁变动的公共代码 124 | 125 | > 详解可以参照[前端性能优化-加载](load.md) 126 | 127 | ## 如何提高webpack的打包速度? 128 | 129 | * happypack: 利用进程并行编译loader,利用缓存来使得 rebuild 更快,遗憾的是作者表示已经不会继续开发此项目,类似的替代者是[thread-loader](https://github.com/webpack-contrib/thread-loader) 130 | * [外部扩展(externals)](https://webpack.docschina.org/configuration/externals/): 将不怎么需要更新的第三方库脱离webpack打包,不被打入bundle中,从而减少打包时间,比如jQuery用script标签引入 131 | * dll: 采用webpack的 DllPlugin 和 DllReferencePlugin 引入dll,让一些基本不会改动的代码先打包成静态资源,避免反复编译浪费时间 132 | * 利用缓存: `webpack.cache`、babel-loader.cacheDirectory、`HappyPack.cache`都可以利用缓存提高rebuild效率 133 | * 缩小文件搜索范围: 比如babel-loader插件,如果你的文件仅存在于src中,那么可以`include: path.resolve(__dirname, 'src')`,当然绝大多数情况下这种操作的提升有限,除非不小心build了node_modules文件 134 | 135 | > 实战文章推荐[使用webpack4提升180%编译速度 136 | Tool 137 | ](https://louiszhai.github.io/2019/01/04/webpack4/) 138 | 139 | ## 如何提高webpack的构建速度? 140 | 141 | 1. 多入口情况下,使用`CommonsChunkPlugin`来提取公共代码 142 | 2. 通过`externals`配置来提取常用库 143 | 3. 利用`DllPlugin`和`DllReferencePlugin`预编译资源模块 通过`DllPlugin`来对那些我们引用但是绝对不会修改的npm包来进行预编译,再通过`DllReferencePlugin`将预编译的模块加载进来。 144 | 4. 使用`Happypack` 实现多线程加速编译 145 | 5. 使用`webpack-uglify-parallel`来提升`uglifyPlugin`的压缩速度。 原理上`webpack-uglify-parallel`采用了多核并行压缩来提升压缩速度 146 | 6. 使用`Tree-shaking`和`Scope Hoisting`来剔除多余代码 147 | 148 | ## 怎么配置单页应用?怎么配置多页应用? 149 | 150 | 单页应用可以理解为webpack的标准模式,直接在`entry`中指定单页应用的入口即可,这里不再赘述 151 | 152 | 多页应用的话,可以使用webpack的 `AutoWebPlugin`来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范。 多页应用中要注意的是: 153 | 154 | - 每个页面都有公共的代码,可以将这些代码抽离出来,避免重复的加载。比如,每个页面都引用了同一套css样式表 155 | - 随着业务的不断扩展,页面可能会不断的追加,所以一定要让入口的配置足够灵活,避免每次添加新页面还需要修改构建配置 156 | 157 | 158 | --- 159 | 160 | ## 公众号 161 | 162 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 163 | 164 | **简历模板:** 关注公众号回复「模板」获取 165 | 166 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 167 | 168 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 169 | -------------------------------------------------------------------------------- /docs/guide/mechanism.md: -------------------------------------------------------------------------------- 1 | # JavaScript的运行机制 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | 了解JavaScript运行机制有助于我们避免bug,并写出高性能的代码,当然还有一大用处就是有助于我们通过造火箭环节的面试。 6 | 7 | 具体而言你会搞清楚以下问题: 8 | 9 | * 作用域链本质上是如何产生的 10 | * this是如何被绑定的 11 | * JavaScript代码到底运行原理是什么 12 | * 闭包产生的根本原因 13 | 14 | 而产生的『后果』是,你可以应对几乎所有的JavaScript作用域、闭包、执行等层面的面试题,还有一个可能的后果,就是面对复杂度不是那么高的代码时,你的脑子中会自己把执行过程像放动画一样过一遍(虽然这个动画也不非常准确)。 15 | 16 | ## JavaScript的执行环境 17 | 18 | 在了解JavaScript运行机制之前,我们需要搞清楚几个主要概念,这有助于我们接下来的理解。 19 | 20 | ### JavaScript引擎(JavaScript Engine) 21 | 22 | 赋予一段代码意义的正是JavaScript引擎,目前JavaScript引擎有许多种: 23 | 24 | * V8 — 开源,由 Google 开发,用 C ++ 编写 25 | * Rhino — 由 Mozilla 基金会管理,开源,完全用 Java 开发 26 | * SpiderMonkey — 是第一个支持 Netscape Navigator 的 JavaScript 引擎,目前正供 Firefox 使用 27 | * JavaScriptCore — 开源,以Nitro形式销售,由苹果为Safari开发 28 | * KJS — KDE 的引擎,最初由 Harri Porten 为 KDE 项目中的 Konqueror 网页浏览器开发 29 | * Chakra (JScript9) — Internet Explorer 30 | * Chakra (JavaScript) — Microsoft Edge 31 | * Nashorn, 作为 OpenJDK 的一部分,由 Oracle Java 语言和工具组编写 32 | * JerryScript —  物联网的轻量级引擎 33 | 34 | 而最为大家熟知的无疑是V8引擎,他用于Chrome浏览器和Node中。 35 | 36 | ![2019-06-19-13-00-37]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/27d902eae39383d1e92d05f4be51ce9b.png) 37 | 38 | V8引擎由两个主要部件组成: 39 | 40 | * emory Heap(内存堆) — 内存分配地址的地方 41 | * Call Stack(调用堆栈) — 代码执行的地方 42 | 43 | ### JavaScript运行时(JavaScript Runtime) 44 | 45 | 想让JavaScript真正运作起来,单单靠JavaScript Engine是不够的,JavaScript Engine的工作是**编译并执行 JavaScript 代码,完成内存分配、垃圾回收等**,但是缺乏与外部交互的能力。 46 | 47 | 比如单靠一个V8引擎是无法进行ajax请求、设置定时器、响应事件等操作的,这就需要JavaScript运行时(JavaScript Runtime)的帮助,它为 JavaScript 提供一些对象或机制,使它能够与外界交互。 48 | 49 | 比如,虽然Chrome和node都是用了V8引擎,但是他们的运行时却不同,比如process、fs浏览器都无法提供。 50 | 51 | ### 可执行代码 52 | 53 | 一段JavaScript代码的运行我们可以分为两个阶段: 54 | 55 | * 编译阶段: 56 | - 分词/词法分析(Tokenizing/Lexing) 57 | - 解析/语法分析(Parsing) 58 | - 预编译(解释) 59 | 60 | * 执行阶段 61 | 62 | 本文的重点在于执行阶段。 63 | 64 | JavaScript并非简单的一行行解释执行,而是将JavaScript代码分为一块块的可执行代码块进行执行,那么如何划分代码块? 65 | 66 | 目前有三类代码块: 67 | 68 | * 函数代码块(Function code) 69 | * 全局代码块(Global code) 70 | * eval代码块(Eval code) 71 | 72 | ## JavaScript执行 73 | 74 | 我们先看一个简单的例子: 75 | 76 | ![2019-06-20-08-15-59]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/2a165649e1648896c43cd0b5ce9f33d9.png) 77 | 78 | 看到这个例子思考一下JavaScript应该是如何执行它的? 79 | 80 | 如果你头脑里没有任何细节的概念,那么接下来的内容就很适用于你了。 81 | 82 | ### 堆 83 | 84 | 我们之前提到过JavaScript引擎两个重要部分: 85 | 86 | * emory Heap(内存堆) — 内存分配地址的地方 87 | * Call Stack(调用栈) — 代码执行的地方 88 | 89 | 而上面的代码声明正是被存放在『堆』中。 90 | 91 | ![2019-06-20-00-15-33](https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/65c06e0194c7f94e7af45e8fcb30e004.png) 92 | 93 | 此时虽然变量和函数都被声明了,但是函数还没有执行,我们现在执行`say`函数。 94 | 95 | ![2019-06-20-08-16-47]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/cb4772803d189080a33facfeecd11baa.png) 96 | 97 | 那么接下来又会发生什么呢? 98 | 99 | ### 调用栈 100 | 101 | 调用栈(Call Stack)这个概念对于经常调试JavaScript代码的同学应该不陌生。 102 | 103 | ![2019-06-20-00-22-23]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/7e4050faa0d3ed66965ad08bf2fec42e.png) 104 | 105 | 我们声明的函数与变量被储存在『内存堆』中,而当我们要执行的时候,就必须借助于『调用栈』来解决问题。 106 | 107 | 如果熟悉数据结构的同学应该知道,栈是一个基础的数据结构,它的特点就是先进后出。 108 | 109 | 我们仍然看这个例子,当`say`函数被调用的时候,他会被压入栈底。 110 | 111 | ![2019-06-20-00-29-02]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/14c76ec0f423e439cf0df59ad8548f8b.png) 112 | 113 | 那么是不是将函数压入栈内就结束了?肯定没有这么简单,这里需 114 | 要在引入一个概念,执行上下文(execution context)。 115 | 116 | ### 执行上下文(execution context) 117 | 118 | 执行上下文在代码块执行前创建,作为代码块运行的基本执行环境,那么执行上下文分为几种? 119 | 120 | 前面我们提到过,JavaScript中有三种可执行代码块,当然也对应着三种执行上下文。 121 | 122 | * 全局执行上下文 — 这是基础上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。 123 | * 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建。 124 | * Eval 执行上下文 — 执行在 eval 内部的代码也会有它属于自己的执行上下文,除非你想搞黑魔法,不然不要轻易使用它。 125 | 126 | 肯定会有人好奇,这个执行上下文到底包含哪些东西呢,他是如何运行的呢? 127 | 128 | 执行上下文分为两个阶段: 129 | 130 | * 创建阶段 131 | * 执行阶段 132 | 133 | 我们主要讨论创建阶段,执行阶段的主要工作就是分配变量 134 | 135 | #### 执行上下文的创建阶段 136 | 137 | 执行上下文的创建阶段主要解决以下三点: 138 | 139 | * 决定 this 的指向 140 | * 创建词法环境(LexicalEnvironment) 141 | * 创建变量环境(VariableEnvironment) 142 | 143 | > 你可能在一些过时的教材或者文章中见过变量对象(VO)这种说法,它的意思与词法环境类似,但是那是ES3的标准,现在早已经改了,改变的原因讨论如下[Why variable object was changed to lexical environment in ES5?](https://stackoverflow.com/questions/40544709/why-variable-object-was-changed-to-lexical-environment-in-es5) 144 | 145 | 伪代码如下: 146 | 147 | ![2019-06-20-08-17-34]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/58ff3a1b54232bf835e0eda470404691.png) 148 | 149 | ##### this指向 150 | 151 | 我们应该知道this的指向是在代码执行阶段确定的,所谓的『代码执行阶段』正是『执行上下文的创建阶段』。 152 | 153 | 默认情况下this指向全局对象,比如浏览器中的window. 154 | 155 | 此外可能存在隐式绑定的情况,比如通过对象调用函数: 156 | 157 | ![2019-06-20-08-18-09]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/17ac778f64d12da5c024b4fc310c2578.png) 158 | 159 | 这个时候this指向对象。 160 | 161 | 然后就是显示绑定对象(call apply bind)等,最后优先级最高的就是new调用构造函数生成一个对象。 162 | 163 | ##### 词法环境(LexicalEnvironment) 164 | 165 | 词法环境分为两大类: 166 | 167 | * 全局环境:全局环境的外部环境引用是 null,它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。 168 | * 函数环境:函数内部用户定义的变量存储在环境记录器中,外部引用既可以是其它函数的内部词法环境,也可以是全局词法环境 169 | 170 | 词法环境本身包括两个部分: 171 | 172 | * 『环境记录器(Environment Record)』是存储变量和函数声明的实际位置 173 | * 『外部环境的引用(outer Lexical Environment)』指它可以访问其父级词法环境(即作用域) 174 | 175 | 对于『环境记录器』而言,它又分为两个主要的词法环境类型: 176 | 177 | * 声明式环境(DecarativeEnvironment):函数定义,变量声明,try...catch等,此类型只对应函数的词法环境 178 | * 对象式环境(ObjectEnvironment):程序级别的(Program)对象、声明、with语句等,此类型只对应全局的词法环境 179 | 180 | 比如我们在全局声明一个函数: 181 | 182 | ![2019-06-20-08-18-42]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/90c3f805aeba811d2b75097a5b3fba48.png) 183 | 184 | 那么他的词法环境可以这样表示(下图我们省略了this绑定、变量环境等信息,便于理解): 185 | 186 | ![2019-06-20-03-49-33]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/f2fd3a92e2aa96c5005d525389834a57.png) 187 | 188 | ##### 变量环境(LexicalEnvironment) 189 | 190 | 变量环境的定义在es5标准和es6标准是略有不同的,我们采用[es6的标准](http://www.ecma-international.org/ecma-262/6.0/#sec-for-statement-runtime-semantics-labelledevaluation) 191 | 192 | 变量环境也是一个词法环境,但不同的是词法环境被用来存储函数声明和变量(let 和 const)绑定,而变量环境只用来存储 var 变量绑定。 193 | 194 | ### 执行过程 195 | 196 | 在了解了这么多概念之后,我们就可以把本节开头的例子再拓展一下: 197 | 198 | ![2019-06-20-08-19-15]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/8532b28d02cf78652a370c82a6c2d29a.png) 199 | 200 | 我们就一步步复盘一下上述代码是如何执行的(不考虑解析、预解释等操作,只考虑执行): 201 | 202 | 1. 变量`name`和函数声明`say`被白存在堆中。 203 | 204 | ![2019-06-20-05-23-40]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/e1f42e04400e14c49c32f51327f85789.png) 205 | 206 | 2. 创建全局可执行上下文: 207 | 208 | 全局上下文的伪代码如下: 209 | 210 | ![2019-06-20-08-19-53]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/2fd22918e0c60c3dacf7fdf3c2c28c3b.png) 211 | 212 | 示意图: 213 | 214 | ![2019-06-20-05-48-54]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/ac8d769de3c77bd724b0f98221c3f8d6.png) 215 | 216 | 3. 创建函数执行上下文 217 | 218 | say函数的执行上下文伪代码如下: 219 | 220 | ![2019-06-20-08-20-53]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/e3dd5ee7ef882c94d27ed55a546779d5.png) 221 | 222 | 4. 创建创建say函数体内的函数执行上下文 223 | 224 | play函数的执行上下文伪代码如下 225 | 226 | ![2019-06-20-08-21-45]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/885a56c1ebb11cfbc1588d5f51fbaee9.png) 227 | 228 | 示意图: 229 | 230 | ![2019-06-20-06-00-27]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/0f1701f3b7061942ae24a9357f28bc2e.png) 231 | 232 | 5. 开始执行 233 | 234 | 将上下文中的变量赋值,然后执行代码,执行完毕栈顶的play函数后弹出,接着执行say函数,完毕后弹出。 235 | 236 | ## 小结 237 | 238 | 我们通过本文了解了相关的JavaScript执行机制,现在可以回答这几个问题了。 239 | 240 | ### this是怎么被绑定的? 241 | 242 | 在创建可执行上下文的时候,根据代码的执行条件,来判断分别进行默认绑定、隐式绑定、显示绑定等。 243 | 244 | ### 作用域链是怎么形成的? 245 | 246 | 可执行上下文中的词法环境中含有外部词法环境的引用,我们可以通过这个引用获取外部词法环境的变量、声明等,这些引用串联起来一直指向全局的词法环境,因此形成了作用域链。 247 | 248 | ### 闭包是怎么形成的? 249 | 250 | 可执行上下文中的词法环境中含有外部词法环境的引用,我们可以通过这个引用获取外部词法环境的变量、声明等,因此形成了闭包。 251 | 252 | --- 253 | 254 | 参考 255 | 1. [ecma标准](http://www.ecma-international.org/ecma-262/6.0/#sec-for-statement-runtime-semantics-labelledevaluation) 256 | 2. [JavaScript调用栈到异步](https://www.valentinog.com/blog/engines/) 257 | 258 | --- 259 | 260 | ## 公众号 261 | 262 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 263 | 264 | **简历模板:** 关注公众号回复「模板」获取 265 | 266 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 267 | 268 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 269 | -------------------------------------------------------------------------------- /docs/guide/carousel.md: -------------------------------------------------------------------------------- 1 | # 实现轮播图组件 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | 6 | ## 轮播图基本原理 7 | 8 | 轮播图(Carousel),在 Antd 中被称为走马灯,可能是前端开发者最常见的组件之一了,不管是在 PC 端还是在移动端我们总能见到他的身影. 9 | 10 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167caa14649bedf2?w=342&h=182&f=png&s=2450) 11 | 12 | 那么我们通常是如何使用轮播图的呢?Antd 的代码如下 13 | 14 | ```html 15 | 16 | 17 |

1

18 |

2

19 |

3

20 |

4

21 |
22 | 23 | ``` 24 | 25 | 问题是我们在`Carousel`中放入了四组`div`为什么一次只显示一组呢? 26 | 27 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167caaa4c8a34613?w=292&h=183&f=png&s=19509) 28 | 29 | 图中被红框圈住的为可视区域,可视区域的位置是固定的,我们只需要移动后面`div`的位置就可以做到1 2 3 4四个子组件轮播的效果,那么子组件2目前在可视区域是可以被看到的,1 3 4应该被隐藏,这就需要我们设置overflow 属性为 hidden来隐藏非可视区域的子组件. 30 | 31 | ![](https://images2015.cnblogs.com/blog/979044/201707/979044-20170710105934040-1007626405.gif) 32 | 33 | 因此就比较明显了,我们设计一个可视窗口组件`Frame`,然后将四个 `div`共同放入幻灯片组合组件`SlideList`中,并用`SlideItem`分别将 `div`包裹起来,实际代码应该是这样的: 34 | 35 | ```html 36 | 37 | 38 | 39 |

1

40 |
41 | 42 |

2

43 |
44 | 45 |

3

46 |
47 | 48 |

4

49 |
50 |
51 | 52 | ``` 53 | 54 | 我们不断利用`translateX`来改变`SlideList`的位置来达到轮播效果,如下图所示,每次轮播的触发都是通过改变`transform: translateX()`来操作的 55 | 56 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167cab805b1bb385?w=1061&h=336&f=png&s=206423) 57 | 58 | ## 轮播图基础实现 59 | 60 | 搞清楚基本原理那么实现起来相对容易了,我们以移动端的实现为例,来实现一个基础的移动端轮播图. 61 | 62 | 首先我们要确定**可视窗口**的宽度,因为我们需要这个宽度来计算出`SlideList`的长度(`SlideList`的长度通常是可视窗口的倍数,比如要放三张图片,那么`SlideList`应该为可视窗口的至少3倍),不然我们无法通过`translateX`来移动它. 63 | 64 | 我们通过`getBoundingClientRect`来获取可视区域真实的长度,`SlideList`的长度那么为: 65 | 66 | `slideListWidth = (len + 2) * width`(len 为传入子组件的数量,width 为可视区域宽度) 67 | 68 | 至于为什么要`+2`后面会提到. 69 | 70 | ```js 71 | /** 72 | * 设置轮播区域尺寸 73 | * @param x 74 | */ 75 | private setSize(x?: number) { 76 | const { width } = this.frameRef.current!.getBoundingClientRect() 77 | const len = React.Children.count(this.props.children) 78 | const total = len + 2 79 | 80 | this.setState({ 81 | slideItemWidth: width, 82 | slideListWidth: total * width, 83 | total, 84 | translateX: -width * this.state.currentIndex, 85 | startPositionX: x !== undefined ? x : 0, 86 | }) 87 | } 88 | ``` 89 | 90 | 获取到了总长度之后如何实现轮播呢?我们需要根据用户反馈来触发轮播,在移动端通常是通过手指滑动来触发轮播,这就需要三个事件`onTouchStart` `onTouchMove` `onTouchEnd`. 91 | 92 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167cac7f4ecc8312?w=342&h=148&f=png&s=108464) 93 | 94 | `onTouchStart`顾名思义是在手指触摸到屏幕时触发的事件,在这个事件里我们只需要记录下手指触摸屏幕的横轴坐标 x 即可,因为我们会通过其横向滑动的距离大小来判断是否触发轮播 95 | 96 | ```js 97 | /** 98 | * 处理触摸起始时的事件 99 | * 100 | * @private 101 | * @param {React.TouchEvent} e 102 | * @memberof Carousel 103 | */ 104 | private onTouchStart(e: React.TouchEvent) { 105 | clearInterval(this.autoPlayTimer) 106 | // 获取起始的横轴坐标 107 | const { x } = getPosition(e) 108 | this.setSize(x) 109 | this.setState({ 110 | startPositionX: x, 111 | }) 112 | } 113 | ``` 114 | 115 | `onTouchMove`顾名思义是处于滑动状态下的事件,此事件在`onTouchStart`触发后,`onTouchEnd`触发前,在这个事件中我们主要做两件事,一件事是判断滑动方向,因为用户可能向左或者向右滑动,另一件事是让轮播图跟随手指移动,这是必要的用户反馈. 116 | 117 | ```js 118 | /** 119 | * 当触摸滑动时处理事件 120 | * 121 | * @private 122 | * @param {React.TouchEvent} e 123 | * @memberof Carousel 124 | */ 125 | private onTouchMove(e: React.TouchEvent) { 126 | const { slideItemWidth, currentIndex, startPositionX } = this.state 127 | const { x } = getPosition(e) 128 | 129 | const deltaX = x - startPositionX 130 | // 判断滑动方向 131 | const direction = deltaX > 0 ? 'right' : 'left' 132 | 133 | this.setState({ 134 | direction, 135 | moveDeltaX: deltaX, 136 | // 改变translateX来达到轮播组件跟随手指移动的效果 137 | translateX: -(slideItemWidth * currentIndex) + deltaX, 138 | }) 139 | } 140 | ``` 141 | 142 | `onTouchEnd`顾名思义是滑动完毕时触发的事件,在此事件中我们主要做一个件事情,就是判断是否触发轮播,我们会设置一个阈值`threshold`,当滑动距离超过这个阈值时才会触发轮播,毕竟没有阈值的话用户稍微触碰轮播图就造成轮播,误操作会造成很差的用户体验. 143 | 144 | ```js 145 | /** 146 | * 滑动结束处理的事件 147 | * 148 | * @private 149 | * @memberof Carousel 150 | */ 151 | private onTouchEnd() { 152 | this.autoPlay() 153 | const { moveDeltaX, slideItemWidth, direction } = this.state 154 | const threshold = slideItemWidth * THRESHOLD_PERCENTAGE 155 | // 判断是否轮播 156 | const moveToNext = Math.abs(moveDeltaX) > threshold 157 | 158 | if (moveToNext) { 159 | // 如果轮播触发那么进行轮播操作 160 | this.handleSwipe(direction!) 161 | } else { 162 | // 轮播不触发,那么轮播图回到原位 163 | this.handleMisoperation() 164 | } 165 | } 166 | ``` 167 | 168 | ## 轮播图的动画效果 169 | 170 | 我们常见的轮播图肯定不是生硬的切换,一般在轮播中会有一个渐变或者缓动的动画,这就需要我们加入动画效果. 171 | 172 | 我们制作动画通常有两个选择,一个是用 css3自带的动画效果,另一个是用浏览器提供的[requestAnimationFrame API](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestAnimationFrame) 173 | 174 | 孰优孰劣?css3简单易用上手快,兼容性好,`requestAnimationFrame` 灵活性更高,能实现 css3实现不了的动画,比如众多缓动动画 css3都束手无策,因此我们毫无疑问地选择了`requestAnimationFrame`. 175 | 176 | > 双方对比请看张鑫旭大神的[CSS3动画那么强,requestAnimationFrame还有毛线用?](https://www.zhangxinxu.com/wordpress/2013/09/css3-animation-requestanimationframe-tween-%E5%8A%A8%E7%94%BB%E7%AE%97%E6%B3%95/) 177 | 178 | 想用`requestAnimationFrame`实现缓动效果就需要特定的缓动函数,下面就是典型的缓动函数 179 | 180 | ```js 181 | type tweenFunction = (t: number, b: number, _c: number, d: number) => number 182 | const easeInOutQuad: tweenFunction = (t, b, _c, d) => { 183 | const c = _c - b; 184 | if ((t /= d / 2) < 1) { 185 | return c / 2 * t * t + b; 186 | } else { 187 | return -c / 2 * ((--t) * (t - 2) - 1) + b; 188 | } 189 | } 190 | ``` 191 | 192 | 缓动函数接收四个参数,分别是: 193 | 194 | * t: 时间 195 | * b:初始位置 196 | * _c:结束的位置 197 | * d:速度 198 | 199 | 通过这个函数我们能算出每一帧轮播图所在的位置, 如下: 200 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167caed22ca506a3?w=424&h=179&f=png&s=27194) 201 | 202 | 在获取每一帧对应的位置后,我们需要用`requestAnimationFrame`不断递归调用依次移动位置,我们不断调用`animation`函数是其触发函数体内的` this.setState({ 203 | translateX: tweenQueue[0], 204 | })`来达到移动轮播图位置的目的,此时将这数组内的30个位置依次快速执行就是一个缓动动画效果. 205 | ```js 206 | /** 207 | * 递归调用,根据轨迹运动 208 | * 209 | * @private 210 | * @param {number[]} tweenQueue 211 | * @param {number} newIndex 212 | * @memberof Carousel 213 | */ 214 | private animation(tweenQueue: number[], newIndex: number) { 215 | if (tweenQueue.length < 1) { 216 | this.handleOperationEnd(newIndex) 217 | return 218 | } 219 | this.setState({ 220 | translateX: tweenQueue[0], 221 | }) 222 | tweenQueue.shift() 223 | this.rafId = requestAnimationFrame(() => this.animation(tweenQueue, newIndex)) 224 | } 225 | ``` 226 | 227 | 但是我们发现了一个问题,当我们移动轮播图到最后的时候,动画出现了问题,*当我们向左滑动最后一个轮播图`div4`时,这种情况下应该是图片向左滑动,然后第一张轮播图`div1`进入可视区域,但是反常的是图片快速向右滑动`div1`出现在可是区域...* 228 | 229 | 因为我们此时将位置4设置为了位置1,这样才能达到不断循环的目的,但是也造成了这个副作用,图片行为与用户行为产生了相悖的情况(用户向左划动,图片向右走). 230 | 231 | 目前业界的普遍做法是将图片首尾相连,例如图片1前面连接一个图片4,图片4后跟着一个图片1,这就是为什么之前计算长度时要`+2` 232 | 233 | `slideListWidth = (len + 2) * width`(len 为传入子组件的数量,width 为可视区域宽度) 234 | 235 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167caf9df54367bd?w=515&h=172&f=png&s=24733) 236 | 237 | 当我们移动图片4时就不会出现上述向左滑图片却向右滑的情况,因为真实情况是: 238 | 239 | `图片4 -- 滑动为 -> 伪图片1` 也就是位置 5 变成了位置 6 240 | 241 | 当动画结束之后,我们迅速把`伪图片1`的位置设置为`真图片1`,这其实是个障眼法,也就是说动画执行过程中实际上是`图片4`到`伪图片1`的过程,当结束后我们偷偷把`伪图片1`换成`真图片1`,因为两个图一模一样,所以这个转换的过程用户根本看不出来... 242 | 243 | 如此一来我们就可以实现无缝切换的轮播图了 244 | 245 | ## 改进方向 246 | 247 | 我们实现了轮播图的基本功能,但是其通用性依然存在缺陷: 248 | 1. 提示点的自定义: 我的实现是一个小点,而 antd 是用的条,这个地方完全可以将 dom 结构的决定权交给开发者. 249 | 250 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167cb035bb7c4896?w=324&h=140&f=png&s=97640) 251 | 252 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167cb03bdb4f1dcd?w=315&h=148&f=png&s=1849) 253 | 254 | 2. 方向的自定义: 本轮播图只有水平方向的实现,其实也可以有纵向轮播 255 | 256 | 257 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167cb052530fbaee?w=329&h=174&f=png&s=2242) 258 | 259 | 3. 多张轮播:除了单张轮播也可以多张轮播 260 | 261 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167cb07b7c8f848a?w=965&h=147&f=png&s=6838) 262 | 263 | 264 | 以上都是可以对轮播图进行拓展的方向,相关的还有性能优化方面 265 | 266 | 我们的具体代码中有一个相关实现,我们的轮播图其实是有自动轮播功能的,但是很多时候页面并不在用户的可视页面中,我们可以根据是否页面被隐藏来取消定时器终止自动播放. 267 | 268 | 269 | ![](https://user-gold-cdn.xitu.io/2018/12/20/167cb0a069892023?w=538&h=206&f=png&s=35737) 270 | 271 | 272 | github[项目地址](https://github.com/xiaomuzhu/rc-carousel) 273 | > 以上 demo 仅供参考,实际项目开发中最好还是使用成熟的开源组件,要有造轮子的能力和不造轮子的觉悟 274 | 275 | --- 276 | 277 | ## 公众号 278 | 279 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 280 | 281 | **简历模板:** 关注公众号回复「模板」获取 282 | 283 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 284 | 285 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 286 | -------------------------------------------------------------------------------- /docs/guide/engineering.md: -------------------------------------------------------------------------------- 1 | # 前端工程化 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | ## Babel的原理是什么? 6 | 7 | babel 的转译过程也分为三个阶段,这三步具体是: 8 | 9 | * 解析 Parse: 将代码解析生成抽象语法树( 即AST ),即词法分析与语法分析的过程 10 | * 转换 Transform: 对于 AST 进行变换一系列的操作,babel 接受得到 AST 并通过 babel-traverse 对其进行遍历,在此过程中进行添加、更新及移除等操作 11 | * 生成 Generate: 将变换后的 AST 再转换为 JS 代码, 使用到的模块是 babel-generator 12 | 13 | ![2019-08-03-23-32-34]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/566a1ac865cfc0b1bf5511f377ff6828.png) 14 | 15 | > 更具体的原理可以移步[如何写一个babel](ast.md) 16 | 17 | ## 如何写一个babel插件? 18 | 19 | **Babel解析成AST,然后插件更改AST,最后由Babel输出代码** 20 | 21 | 那么Babel的插件模块需要你暴露一个function,function内返回visitor 22 | ```javascript 23 | module.export = function(babel){ 24 | return { 25 | visitor:{ 26 | } 27 | } 28 | } 29 | ``` 30 | visitor是对各类型的AST节点做处理的地方,那么我们怎么知道Babel生成了的AST有哪些节点呢?
很简单,你可以把Babel转换的结果打印出来,或者这里有传送门: [AST explorer](https://astexplorer.net/)
![](https://cdn.nlark.com/yuque/0/2019/jpeg/128853/1564847220298-abb4a365-0d09-4e06-8903-2d267b8af354.jpeg#align=left&display=inline&height=956&originHeight=956&originWidth=947&size=0&status=done&width=947)
这里我们看到 `const result = 1 + 2`中的`1 + 1`是一个`BinaryExpression`节点,那么在visitor中,我们就处理这个节点 31 | ```javascript 32 | var babel = require('babel-core'); 33 | var t = require('babel-types'); 34 | const visitor = { 35 | BinaryExpression(path) { 36 | const node = path.node; 37 | let result; 38 | // 判断表达式两边,是否都是数字 39 | if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) { 40 | // 根据不同的操作符作运算 41 | switch (node.operator) { 42 | case "+": 43 | result = node.left.value + node.right.value; 44 | break 45 | case "-": 46 | result = node.left.value - node.right.value; 47 | break; 48 | case "*": 49 | result = node.left.value * node.right.value; 50 | break; 51 | case "/": 52 | result = node.left.value / node.right.value; 53 | break; 54 | case "**": 55 | let i = node.right.value; 56 | while (--i) { 57 | result = result || node.left.value; 58 | result = result * node.left.value; 59 | } 60 | break; 61 | default: 62 | } 63 | } 64 | // 如果上面的运算有结果的话 65 | if (result !== undefined) { 66 | // 把表达式节点替换成number字面量 67 | path.replaceWith(t.numericLiteral(result)); 68 | } 69 | } 70 | }; 71 | module.exports = function (babel) { 72 | return { 73 | visitor 74 | }; 75 | } 76 | ``` 77 | 插件写好了,我们运行下插件试试 78 | ``` 79 | const babel = require("babel-core"); 80 | const result = babel.transform("const result = 1 + 2;",{ 81 | plugins:[ 82 | require("./index") 83 | ] 84 | }); 85 | console.log(result.code); // const result = 3; 86 | ``` 87 | 与预期一致,那么转换 `const result = 1 + 2 + 3 + 4 + 5;`呢?
结果是: `const result = 3 + 3 + 4 + 5;`
这就奇怪了,为什么只计算了`1 + 2`之后,就没有继续往下运算了?
我们看一下这个表达式的AST树
![](https://cdn.nlark.com/yuque/0/2019/jpeg/128853/1564847220485-a7c07a30-c4d4-453e-8fd3-9c3092ed5d91.jpeg#align=left&display=inline&height=957&originHeight=957&originWidth=1020&size=0&status=done&width=1020)
你会发现Babel解析成表达式里面再嵌套表达式。 88 | ``` 89 | 表达式( 表达式( 表达式( 表达式(1 + 2) + 3) + 4) + 5) 90 | ``` 91 | 而我们的判断条件并不符合所有的,只符合`1 + 2` 92 | ```javascript 93 | // 判断表达式两边,是否都是数字 94 | if (t.isNumericLiteral(node.left) && t.isNumericLiteral(node.right)) {} 95 | ``` 96 | 那么我们得改一改
第一次计算`1 + 2`之后,我们会得到这样的表达式 97 | ``` 98 | 表达式( 表达式( 表达式(3 + 3) + 4) + 5) 99 | ``` 100 | 其中 `3 + 3`又符合了我们的条件, 我们通过向上递归的方式遍历父级节点
又转换成这样: 101 | ``` 102 | 表达式( 表达式(6 + 4) + 5) 103 | 表达式(10 + 5) 104 | 15 105 | ``` 106 | ```javascript 107 | // 如果上面的运算有结果的话 108 | if (result !== undefined) { 109 | // 把表达式节点替换成number字面量 110 | path.replaceWith(t.numericLiteral(result)); 111 | let parentPath = path.parentPath; 112 | // 向上遍历父级节点 113 | parentPath && visitor.BinaryExpression.call(this, parentPath); 114 | } 115 | ``` 116 | 到这里,我们就得出了结果 `const result = 15;`
那么其他运算呢:
`const result = 100 + 10 - 50` >>> `const result = 60;`
`const result = (100 / 2) + 50` >>> `const result = 100;`
`const result = (((100 / 2) + 50 * 2) / 50) ** 2` >>> `const result = 9;` 117 | 118 | > [项目地址](https://github.com/axetroy/babel-plugin-pre-calculate-number) 119 | 120 | 上述答案来源于cnode[帖子](https://cnodejs.org/topic/5a9317d38d6e16e56bb808d1) 121 | 122 | > 更详实的教程移步[官方的插件教程](https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md#builders) 123 | 124 | --- 125 | 126 | ## 你的git工作流是怎样的? 127 | 128 | GitFlow 是由 Vincent Driessen 提出的一个 git操作流程标准。包含如下几个关键分支: 129 | 130 | | 名称 | 说明 | 131 | | --- | --- | 132 | | master | 主分支 | 133 | | develop | 主开发分支,包含确定即将发布的代码 | 134 | | feature | 新功能分支,一般一个新功能对应一个分支,对于功能的拆分需要比较合理,以避免一些后面不必要的代码冲突 | 135 | | release | 发布分支,发布时候用的分支,一般测试时候发现的 bug 在这个分支进行修复 | 136 | | hotfix | hotfix 分支,紧急修 bug 的时候用 | 137 | 138 | GitFlow 的优势有如下几点: 139 | 140 | - 并行开发:GitFlow 可以很方便的实现并行开发:每个新功能都会建立一个新的 `feature` 分支,从而和已经完成的功能隔离开来,而且只有在新功能完成开发的情况下,其对应的 `feature` 分支才会合并到主开发分支上(也就是我们经常说的 `develop` 分支)。另外,如果你正在开发某个功能,同时又有一个新的功能需要开发,你只需要提交当前 `feature` 的代码,然后创建另外一个 `feature` 分支并完成新功能开发。然后再切回之前的 `feature` 分支即可继续完成之前功能的开发。 141 | - 协作开发:GitFlow 还支持多人协同开发,因为每个 `feature` 分支上改动的代码都只是为了让某个新的 `feature` 可以独立运行。同时我们也很容易知道每个人都在干啥。 142 | - 发布阶段:当一个新 `feature` 开发完成的时候,它会被合并到 `develop` 分支,这个分支主要用来暂时保存那些还没有发布的内容,所以如果需要再开发新的 `feature`,我们只需要从 `develop` 分支创建新分支,即可包含所有已经完成的 `feature` 。 143 | - 支持紧急修复:GitFlow 还包含了 `hotfix` 分支。这种类型的分支是从某个已经发布的 tag 上创建出来并做一个紧急的修复,而且这个紧急修复只影响这个已经发布的 tag,而不会影响到你正在开发的新 `feature`。 144 | 145 | 然后就是 GitFlow 最经典的几张流程图,一定要理解:
![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1564853230881-000559e1-bbcf-453e-b6a0-41b9a94e6937.webp#align=left&display=inline&height=715&originHeight=715&originWidth=483&size=0&status=done&width=483)
`feature` 分支都是从 `develop` 分支创建,完成后再合并到 `develop` 分支上,等待发布。
![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1564853230910-0cefe6ee-ee05-4332-b02b-44c65428d81a.webp#align=left&display=inline&height=715&originHeight=715&originWidth=483&size=0&status=done&width=483)
当需要发布时,我们从 `develop` 分支创建一个 `release` 分支
![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1564853230917-d7ab323c-06f5-4365-ae2d-f65a78c5faa0.webp#align=left&display=inline&height=715&originHeight=715&originWidth=494&size=0&status=done&width=494)
然后这个 `release` 分支会发布到测试环境进行测试,如果发现问题就在这个分支直接进行修复。在所有问题修复之前,我们会不停的重复**发布->测试->修复->重新发布->重新测试**这个流程。
发布结束后,这个 `release` 分支会合并到 `develop` 和 `master` 分支,从而保证不会有代码丢失。
![](https://cdn.nlark.com/yuque/0/2019/webp/128853/1564853230859-855003ad-8d60-4c86-9803-a852c9d95b65.webp#align=left&display=inline&height=717&originHeight=717&originWidth=494&size=0&status=done&width=494)
`master` 分支只跟踪已经发布的代码,合并到 `master` 上的 commit 只能来自 `release` 分支和 `hotfix` 分支。
`hotfix` 分支的作用是紧急修复一些 Bug。
它们都是从 `master` 分支上的某个 tag 建立,修复结束后再合并到 `develop` 和 `master` 分支上。
146 | 147 | > 更多工作流可以参考阮老师的[Git 工作流程](https://www.ruanyifeng.com/blog/2015/12/git-workflow.html) 148 | 149 | ## rebase 与 merge的区别? 150 | 151 | git rebase 和 git merge 一样都是用于从一个分支获取并且合并到当前分支. 152 | 153 | 假设一个场景,就是我们开发的[feature/todo]分支要合并到master主分支,那么用rebase或者merge有什么不同呢? 154 | 155 | ![2019-08-04-01-20-09]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/4f2db1d2289e18c7b874a9015a20b38d.png) 156 | 157 | * marge 特点:自动创建一个新的commit 158 | 如果合并的时候遇到冲突,仅需要修改后重新commit 159 | * 优点:记录了真实的commit情况,包括每个分支的详情 160 | * 缺点:因为每次merge会自动产生一个merge commit,所以在使用一些git 的GUI tools,特别是commit比较频繁时,看到分支很杂乱。 161 | 162 | ![2019-08-04-01-22-04]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/ea8107df9a1650ed19b0b68ab3da4567.png) 163 | 164 | * rebase 特点:会合并之前的commit历史 165 | * 优点:得到更简洁的项目历史,去掉了merge commit 166 | * 缺点:如果合并出现代码问题不容易定位,因为re-write了history 167 | 168 | 因此,当需要保留详细的合并信息的时候建议使用git merge,特别是需要将分支合并进入master分支时;当发现自己修改某个功能时,频繁进行了git commit提交时,发现其实过多的提交信息没有必要时,可以尝试git rebase. 169 | 170 | ## git reset、git revert 和 git checkout 有什么区别 171 | 172 | 这个问题同样也需要先了解 git 仓库的三个组成部分:工作区(Working Directory)、暂存区(Stage)和历史记录区(History)。 173 | 174 | * 工作区:在 git 管理下的正常目录都算是工作区,我们平时的编辑工作都是在工作区完成 175 | * 暂存区:临时区域。里面存放将要提交文件的快照 176 | * 历史记录区:git commit 后的记录区 177 | 178 | 三个区的转换关系以及转换所使用的命令: 179 | 180 | ![2019-08-04-01-51-29]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/403ff092b287ae192b376ac93fb69816.png) 181 | 182 | git reset、git revert 和 git checkout的共同点:用来撤销代码仓库中的某些更改。 183 | 184 | 然后是不同点: 185 | 186 | 首先,从 commit 层面来说: 187 | 188 | * git reset 可以将一个分支的末端指向之前的一个 commit。然后再下次 git 执行垃圾回收的时候,会把这个 commit 之后的 commit 都扔掉。git reset 还支持三种标记,用来标记 reset 指令影响的范围: 189 | - --mixed:会影响到暂存区和历史记录区。也是默认选项 190 | - --soft:只影响历史记录区 191 | - --hard:影响工作区、暂存区和历史记录区 192 | 193 | > 注意:因为 git reset 是直接删除 commit 记录,从而会影响到其他开发人员的分支,所以不要在公共分支(比如 develop)做这个操作。 194 | 195 | * git checkout 可以将 HEAD 移到一个新的分支,并更新工作目录。因为可能会覆盖本地的修改,所以执行这个指令之前,你需要 stash 或者 commit 暂存区和工作区的更改。 196 | * git revert 和 git reset 的目的是一样的,但是做法不同,它会以创建新的 commit 的方式来撤销 commit,这样能保留之前的 commit 历史,比较安全。另外,同样因为可能会覆盖本地的修改,所以执行这个指令之前,你需要 stash 或者 commit 暂存区和工作区的更改。 197 | 198 | 然后,从文件层面来说: 199 | 200 | * git reset 只是把文件从历史记录区拿到暂存区,不影响工作区的内容,而且不支持 --mixed、--soft 和 --hard。 201 | * git checkout 则是把文件从历史记录拿到工作区,不影响暂存区的内容。 202 | * git revert 不支持文件层面的操作。 203 | 204 | --- 205 | 206 | ## 公众号 207 | 208 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 209 | 210 | **简历模板:** 关注公众号回复「模板」获取 211 | 212 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 213 | 214 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 215 | -------------------------------------------------------------------------------- /docs/guide/immutable.md: -------------------------------------------------------------------------------- 1 | # 实现不可变数据 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | ## 前言 6 | 7 | 我们在学习 React 的过程中经常会碰到一个概念,那就是数据的不可变性(immutable),不可变数据是函数式编程里的重要概念,因为可变数据在提供方便的时候会带了很多棘手的副作用,那么我们应该如何处理这些棘手的问题,如何实现不可变数据呢? 8 | 9 | ## 1.可变数据的副作用 10 | 11 |  我们应该都知道的基本知识,在JavaScript中分为原始类型和引用类型. 12 |    13 | > JavaScript原始类型:Undefined、Null、Boolean、Number、String、Symbol 14 | > JavaScript引用类型:Object 15 | 16 | 同时引用类型在使用过程中经常会产生副作用. 17 | 18 | ```js 19 | const person = {player: {name: 'Messi'}}; 20 | const person1 = person; 21 | console.log(person, person1); 22 | //[ { name: 'Messi' } ] [ { name: 'Messi' } ] 23 | person.player.name = 'Kane'; 24 | console.log(person, person1); 25 | //[ { name: 'Kane' } ] [ { name: 'Kane' } ] 26 | 27 | ``` 28 | 29 | 我们看到,当修改了`person`中属性后,`person1`的属性值也随之改变,因为这两个变量的指针指向了同一块内存,当一个变量被修改后,内存随之变动,而另一个变量由于指向同一块内存,自然也随之变化了,这就是引用类型的副作用.
可是绝大多数情况下我们并不希望`person1`的属性值也发生改变,我们应该如何解决这个问题? 30 | 31 | ## 2.不可变数据的解决方案 32 | 33 | #### 2.1 浅复制 34 | 35 |   在ES6中我们可以用`Object.assign` 或者 `...`对引用类型进行浅复制. 36 | 37 | ```js 38 | const person = [{name: 'Messi'}]; 39 | const person1 = person.map(item => 40 | ({...item, name: 'Kane'}) 41 | ) 42 | console.log(person, person1); 43 | // [{name: 'Messi'}] [{name: 'Kane'}] 44 | 45 | ``` 46 | 47 | `person`的确被成功复制了,但是之所以我们称它为浅复制,是因为这种复制只能复制一层,在多层嵌套的情况下依然会出现副作用. 48 | 49 | ```js 50 | const person = [{name: 'Messi', info: {age: 30}}]; 51 | const person1 = person.map(item => 52 | ({...item, name: 'Kane'}) 53 | ) 54 | console.log(person[0].info === person1[0].info); // true 55 | 56 | ``` 57 | 58 | 上述代码表明当利用浅复制产生新的`person1`后其中嵌套的`info`属性依然与原始的`person`的`info`属性指向同一个堆内存对象,这种情况依然会产生副作用.
我们可以发现浅复制虽然可以解决浅层嵌套的问题,但是依然对多层嵌套的引用类型无能为力. 59 | 60 | #### 2.2 深克隆 61 | 62 | 既然浅复制(克隆)无法解决这个问题,我们自然会想到利用深克隆的方法来实现多层嵌套复制的问题.
我们之前已经讨论过如何实现一个深克隆,在此我们不做深究,深克隆毫无疑问可以解决引用类型产生的副作用. 63 | > [面试官系列(1): 如何实现深克隆](https://juejin.im/post/5abb55ee6fb9a028e33b7e0a) 64 | 65 | 实现一个在生产环境中可以用的深克隆是非常繁琐的事情,我们不仅要考虑到_正则_、_Symbol_、_Date_等特殊类型,还要考虑到_原型链_和_循环引用_的处理,当然我们可以选择使用成熟的[开源库](https://link.juejin.im?target=https%3A%2F%2Fgithub.com%2Flodash%2Flodash%2Fblob%2Fmaster%2FcloneDeep.js)进行深克隆处理.
可是问题就在于我们实现一次深克隆的开销太昂贵了,[如何实现深克隆](https://link.juejin.im?target=https%3A%2F%2Fgithub.com%2Fxiaomuzhu%2FElemeFE-node-interview%2Fblob%2Fmaster%2FJavaScript%25E5%259F%25BA%25E7%25A1%2580%2Fjavascript%25E5%25AE%259E%25E7%258E%25B0%25E6%25B7%25B1%25E5%2585%258B%25E9%259A%2586.md)中我们展示了一个勉强可以使用的深克隆函数已经处理了相当多的逻辑,如果我们每使用一次深克隆就需要一次如此昂贵的开销,程序的性能是会大打折扣. 66 | 67 | ```js 68 | const person = [{name: 'Messi', info: {age: 30}}]; 69 | for (let i=0; i< 100000;i++) { 70 | person.push({name: 'Messi', info: {age: 30}}); 71 | } 72 | console.time('clone'); 73 | const person1 = person.map(item => 74 | ({...item, name: 'Kane'}) 75 | ) 76 | console.timeEnd('clone'); 77 | console.time('cloneDeep'); 78 | const person2 = lodash.cloneDeep(person) 79 | console.timeEnd('cloneDeep'); 80 | // clone : 105.520ms 81 | // cloneDeep : 372.839ms 82 | 83 | ``` 84 | 85 | 我们可以看到深克隆的的性能相比于浅克隆大打折扣,但是浅克隆又不能从根本上杜绝引用类型的副作用,我们需要找到一个兼具性能和效果的方案. 86 | 87 | #### 2.3 immutable.js 88 | 89 | immutable.js是正是兼顾了使用效果和性能的解决方案
原理如下: 90 | **Immutable**实现的原理是**Persistent Data Structur**(持久化数据结构),对**Immutable**对象的任何修改或添加删除操作都会返回一个新的**Immutable**对象, 同时使用旧数据创建新数据时,要保证旧数据同时可用且不变。
为了避免像 `deepCopy`一样 把所有节点都复制一遍带来的性能损耗,**Immutable** 使用了 **Structural Sharing**(结构共享),即如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享。请看下面动画
![](https://cdn.nlark.com/yuque/0/2019/gif/128853/1561002048739-a6e8c916-b387-4ebc-856c-b7564d9b2489.gif#align=left&display=inline&height=575&originHeight=575&originWidth=613&size=0&status=done&width=613)
我们看到动画中右侧的子节点由于发生变化,相关父节点进行了重建,但是左侧树没有发生变化,最后形成的新的树依然复用了左侧树的节点,看起来真的是无懈可击.
immutable.js 的实现方法确实很高明,毕竟是花了 Facebook 工程师三年打造的全新数据结构,相比于深克隆,带来的 cpu 消耗很低,同时内存占用也很小.
但是 immutable.js 就没有弊端吗?
在使用过程中,immutable.js也存在很多问题.
我目前碰到的坑有: 91 | 92 | 1. 由于实现了完整的不可变数据,immutable.js的体积过于庞大,尤其在移动端这个情况被凸显出来. 93 | 2. 全新的api+不友好的文档,immutable.js使用的是自己的一套api,因此我们对js原生数组、对象的操作统统需要抛弃重新学习,但是官方文档不友好,很多情况下需要自己去试api. 94 | 3. 调试错误困难,immutable.js自成一体的数据结构,我们无法像读原生js一样读它的数据结构,很多情况下需要`toJS()`转化为原生数据结构再进行调试,这让人很崩溃. 95 | 4. 极易引起滥用,immutable.js 在 react 项目中本来是可以大幅提高软件性能,通过深度对比避免大量重复渲染的,但是很多开发者习惯在 react-redux 的 connect 函数中将 immutable.js 数据通过 `toJS`转化为正常的 js 数据结构,这个时候新旧 props 就永远不会相等了,就导致了大量重复渲染,严重降低性能. 96 | 5. 版本更新卡壳,immutable.js 在4.0.0-rc.x 上大概卡了一年了,在3.x 版本中对 typescript 支持极差,而新版本一直卡壳 97 | 98 | immutable.js在某种程度上来说,更适合于对数据可靠度要求颇高的大型前端应用(需要引入庞大的包、额外的学习成本甚至类型检测工具对付immutable.js与原生js类似的api),中小型的项目引入immutable.js的代价有点高昂了,可是我们有时候不得不利用immutable的特性,那么如何保证性能和效果的情况下减少immutable相关库的体积和提高api友好度呢? 99 | 100 | ## 3.实现更简单的immutable 101 | 102 | 我们的原则已经提到了,要尽可能得减小体积,这就注定了我们不能像immutable.js那样自己定义各种数据结构,而且要减小使用成本,所以要用原生js的方式,而不是自定义数据结构中的api.
_这个时候需要我们思考如何实现上述要求呢?_
我们要通过原生js的api来实现immutable,很显然我们需要对引用对象的set、get、delete等一系列操作的特性进行修改,这就需要`defineProperty`或者`Proxy`进行元编程.
我们就以`Proxy`为例来进行编码,当然,我们需要事先了解一下`Proxy`的[使用方法](https://link.juejin.im?target=http%3A%2F%2Fes6.ruanyifeng.com%2F%23docs%2Fproxy%23Proxy-revocable).
我们先定义一个目标对象 103 | 104 | ```js 105 | const target = {name: 'Messi', age: 29}; 106 | 107 | ``` 108 | 109 | 我们如果想每访问一次这个对象的`age`属性,`age`属性的值就增加`1`. 110 | 111 | ```js 112 | const target = {name: 'Messi', age: 29}; 113 | const handler = { 114 | get: function(target, key, receiver) { 115 | console.log(`getting ${key}!`); 116 | if (key === 'age') { 117 | const age = Reflect.get(target, key, receiver) 118 | Reflect.set(target, key, age+1, receiver); 119 | return age+1 120 | } 121 | return Reflect.get(target, key, receiver); 122 | } 123 | }; 124 | const a = new Proxy(target, handler); 125 | console.log(a.age, a.age); 126 | //getting age! 127 | //getting age! 128 | //30 31 129 | 130 | ``` 131 | 是的`Proxy`就像一个代理器,当有人对目标对象进行处理(set、has、get等等操作)的时候它会拦截操作,并用我们提供的代码进行处理,此时`Proxy`相当于一个中介或者叫代理人,当然`Proxy`的名字也说明了这一点,它经常被用于代理模式中,例如字段验证、缓存代理、访问控制等等。
我们的目的很简单,就是利用`Proxy`的特性,在外部对目标对象进行修改的时候来进行额外操作保证数据的不可变。
在外部对目标对象进行修改的时候,我们可以将被修改的引用的那部分进行拷贝,这样既能保证效率又能保证可靠性. 132 | 133 | 1. 那么如何判断目标对象是否被修改过,最好的方法是维护一个状态 134 | 135 | ```js 136 | function createState(target) { 137 | this.modified = false; // 是否被修改 138 | this.target = target; // 目标对象 139 | this.copy = undefined; // 拷贝的对象 140 | } 141 | 142 | ``` 143 | 144 | 2. 此时我们就可以通过状态判断来进行不同的操作了 145 | 146 | ```js 147 | createState.prototype = { 148 | // 对于get操作,如果目标对象没有被修改直接返回原对象,否则返回拷贝对象 149 | get: function(key) { 150 | if (!this.modified) return this.target[key]; 151 | return this.copy[key]; 152 | }, 153 | // 对于set操作,如果目标对象没被修改那么进行修改操作,否则修改拷贝对象 154 | set: function(key, value) { 155 | if (!this.modified) this.markChanged(); 156 | return (this.copy[key] = value); 157 | }, 158 | // 标记状态为已修改,并拷贝 159 | markChanged: function() { 160 | if (!this.modified) { 161 | this.modified = true; 162 | this.copy = shallowCopy(this.target); 163 | } 164 | }, 165 | }; 166 | // 拷贝函数 167 | function shallowCopy(value) { 168 | if (Array.isArray(value)) return value.slice(); 169 | if (value.__proto__ === undefined) 170 | return Object.assign(Object.create(null), value); 171 | return Object.assign({}, value); 172 | } 173 | 174 | ``` 175 | 176 | 3. 最后我们就可以利用构造函数`createState`接受目标对象`state`生成对象`store`,然后我们就可以用`Proxy`代理`store`,`producer`是外部传进来的操作函数,当`producer`对代理对象进行操作的时候我们就可以通过事先设定好的`handler`进行代理操作了. 177 | 178 | ```js 179 | const PROXY_STATE = Symbol('proxy-state'); 180 | const handler = { 181 | get(target, key) { 182 | if (key === PROXY_STATE) return target; 183 | return target.get(key); 184 | }, 185 | set(target, key, value) { 186 | return target.set(key, value); 187 | }, 188 | }; 189 | // 接受一个目标对象和一个操作目标对象的函数 190 | function produce(state, producer) { 191 | const store = new createState(state); 192 | const proxy = new Proxy(store, handler); 193 | producer(proxy); 194 | const newState = proxy[PROXY_STATE]; 195 | if (newState.modified) return newState.copy; 196 | return newState.target; 197 | } 198 | 199 | ``` 200 | 201 | 4. 我们可以验证一下,我们看到`producer`并没有干扰到之前的目标函数. 202 | 203 | ```js 204 | const baseState = [ 205 | { 206 | todo: 'Learn typescript', 207 | done: true, 208 | }, 209 | { 210 | todo: 'Try immer', 211 | done: false, 212 | }, 213 | ]; 214 | const nextState = produce(baseState, draftState => { 215 | draftState.push({todo: 'Tweet about it', done: false}); 216 | draftState[1].done = true; 217 | }); 218 | console.log(baseState, nextState); 219 | /* 220 | [ { todo: 'Learn typescript', done: true }, 221 | { todo: 'Try immer', done: true } ] 222 | [ { todo: 'Learn typescript', done: true , 223 | { todo: 'Try immer', done: true }, 224 | { todo: 'Tweet about it', done: false } ] 225 | */ 226 | 227 | ``` 228 | 229 | 没问题,我们成功实现了轻量级的 immutable.js,在保证 api友好的同时,做到了比 immutable.js 更小的体积和不错的性能. 230 | 231 | ## 总结 232 | 233 | 实际上这个实现就是不可变数据库[immer](https://link.juejin.im?target=https%3A%2F%2Fgithub.com%2Fmweststrate%2Fimmer) 234 | 的迷你版,我们阉割了大量的代码才缩小到了60行左右来实现这个基本功能,实际上除了`get/set`操作,这个库本身有`has/getOwnPropertyDescriptor/deleteProperty`等一系列的实现,我们由于篇幅的原因很多代码也十分粗糙,深入了解可以移步完整源码.
在不可变数据的技术选型上,我查阅了很多资料,也进行过实践,immutable.js 的确十分难用,尽管我用他开发过一个完整的项目,因为任何来源的数据都需要通过 fromJS()将他转化为 Immutable 本身的结构,而我们在组件内用数据驱动视图的时候,组件又不能直接用 Immutable 的数据结构,这个时候又需要进行数据转换,只要你的项目沾染上了 Immutable.js 就不得不将整个项目全部的数据结构用Immutable.js 重构(否则就是到处可见的 fromjs 和 tojs 转换,一方面影响性能一方面影响代码可读性),这个解决方案的侵入性极强,不建议大家轻易尝试. 235 | 236 | 237 | --- 238 | 239 | ## 公众号 240 | 241 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 242 | 243 | **简历模板:** 关注公众号回复「模板」获取 244 | 245 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 246 | 247 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 248 | -------------------------------------------------------------------------------- /docs/guide/resume.md: -------------------------------------------------------------------------------- 1 | # 面试官到底想看什么样的简历? 2 | 3 | 点击关注本[公众号](#公众号)获取文档最新更新,并可以领取配套于本指南的 **《前端面试手册》** 以及**最标准的简历模板**. 4 | 5 | ## 简历准备 6 | 7 | 简历是你进入面试的敲门砖,也是留给意向公司的第一印象,所以这个很重要,必须在这上面做足了文章,一份优秀的面试简历是整个面试成败的重中之重,我们会详细分析如何准备简历才能保证简历不被刷掉。 8 | 9 | 简历通常有这几部分构成: 10 | 11 | 1. 基本资料 12 | 2. 专业技能 13 | 3. 工作经历 14 | 4. 项目经历 15 | 5. 教育背景 16 | 17 | 我们会逐一进行分析。 18 | 19 | ## 准备简历模板 20 | 21 | 万事开头难,简历的编写如果从头开始需要浪费很多时间,其实最快速也最聪明的办法就是先找一份还不错的简历模板,之后我们只需要填写信息即可。 22 | 23 | 简历模板的选择很讲究,有些简历基本不看内容就会被刷掉,这些简历一般会对面试官进行视觉攻击,让简历给面试官的第一印象就是反感。 24 | 25 | 有两种坑爹的简历模板: 26 | 27 | 一种是经典简历模板,真是堪称『经典』,这种简历模板在我上小学的时候就有了,以现在的眼光看有点不够看了,配色也比较『魔幻』,加上表格类的简历属于low到底端的简历类型,基本上扫一眼就扔了,这种简历只需要3秒钟就能被面试官扔到垃圾堆。 28 | 29 | ![2019-07-02-15-46-35]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8af2d9398b1?w=488&h=449&f=png&s=116106) 30 | 31 | 另一种是设计感十足的简历模板,这种简历设计感十足,这五颜六色的配色一定能亮瞎面试官的双眼,这种花里胡哨的简历同样也是3秒钟沉到垃圾堆底部的简历。 32 | 33 | ![2019-07-02-14-27-48]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8af2ddd751a?w=650&h=919&f=png&s=568202) 34 | 35 | 以上两类简历模板堪称面试官杀手,我相信只要你用了上述两类模板,绝对连让面试官看第二眼的兴趣都没有。 36 | 37 | 面试官筛简历要的是高效、清晰、内容突出,不管是HR还是技术面试官都想在最快速的情况下看到有效信息,你眼中所谓的『视觉效果』在别人眼里就是『视觉噪音』或者『视觉垃圾』,严重影响看简历的心情和寻找有效信息的速度。 38 | 39 | 其实我发现不仅仅是在互联网技术招聘这个领域,大部分企业招聘的简历要求都很简单,清晰、简洁即可,最重要的是要内容清晰,突出主题。 40 | 41 | 就像这样,颜色不超过黑白灰三色,把强调的内容讲清楚,让面试官一眼就看到重点即可: 42 | 43 | ![2019-07-03-07-24-26]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8af2dd96194?w=750&h=1067&f=png&s=300790) 44 | 45 | 简历模板可以去公众号『程序员面试官』后台回复『模板』二字领取。 46 | 47 | ## 准备个人信息 48 | 49 | 个人信息部分主要包括姓名、电话、点子邮箱、求职意向,当然这四个是必填的,其它的都是选填,填好了是加分项,否则很可能减分。 50 | 51 | 接下来才是重点: 52 | 53 | 1. github:如果准备一个基本没有更新的博客或者没有任何贡献的github,那么给面试官一种为了放上去而放上去的感觉,这基本上就是在跟面试官说『这个候选人平时根本没有总结提炼的习惯』,所以如果有长期维护的github或者博客一定要放上去,质量好的话会非常有用,如果没有千万别放。 54 | 55 | 2. 学历:如果你的学历是专科、高中毕业之类的,还写在简历头部强调一遍,这就造成了你是『学渣』的印象,没有公司喜欢学渣的,这又增加了简历被刷的几率,如果是研究生以上学历可以写,突出一下学历优势,本科学历在技术面试领域基本上敲门砖级别的,没必要写。 56 | 57 | 3. 年龄:如果你是大龄程序员,尤其是你还在求一份低端岗位的时候千万别写,一个大龄程序员在求职一个中低端岗位,说明这些年基本原地踏步,还不能加班,到这里基本上此简历就凉了一半了。 58 | 59 | 4. 照片:形象优秀的可以贴,尤其是形象优秀的女程序媛,其它的最好不要贴,如果要贴的话,最好是贴那种PS过的非常职业的证件照,那种平时搞怪的、光着膀子的生活照,基本就是自杀行为。 60 | 61 | 如果你没有特别之处,直接按下面这种最简单的个人信息填写方式即可,切勿给自己加戏: 62 | 63 | ![2019-07-03-07-39-48]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8af2fcc44d3?w=469&h=79&f=png&s=12401) 64 | 65 | ## 准备专业技能 66 | 67 | 对于程序员的专业技能其实就是技术栈,对于自己的技术栈如何描述是个很难的问题,比如什么算是精通?什么算是了解?什么是熟悉? 68 | 69 | 关于对技术技能的描述有很多种,有五种的也有三种的,而且每个人对词汇的理解都不一样,我结合相关专家的理解和自己的理解来简单阐述下描述词汇的区别,我们这里只讲三种的了解、熟悉、精通。 70 | 71 | * 了解:使用过某一项技术,能在别人指导下完成工作,但不能胜任复杂工作,也不能独立解决问题。 72 | * 熟悉:大量运用过的某一项技术,能独立完成工作,且能独立完成有一定复杂度的工作,在技术的应用层面不会有太大问题,甚至理解一点原理。 73 | * 精通:不仅可以运用某一门技术完成复杂项目,而且理解这项技术背后的原理,可以对此技术进行二次开发,甚至本身就是技术源码的贡献者。 74 | 75 | 我们就以Vue这个框架为例,如果你可以用vue写一些简单的页面,单独完成某几个页面的开发,但是无法脱离公司脚手架工作,也无法独立从0完成一个有一定复杂度的项目,只能称之为了解。 76 | 77 | 如果你有大量运用vue的经验,有从0独立完成一定复杂度项目的能力,可以完全脱离脚手架进行开发,且对vue的原理有一定的了解,可以称之为熟悉。 78 | 79 | 如果你用vue完成过复杂度很高的项目,而且非常熟悉vue的原理,是vue源码的主要贡献者,亦或者根据vue源码进行过魔改(比如mpvue),你可以称得上精通。 80 | 81 | ![2019-07-03-16-49-28]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8af2dc6a2d2?w=719&h=322&f=png&s=65731) 82 | 83 | 那么有两个坑是候选人经常犯的,『杂』和『精』,这种两个坑大量集中在应届生和刚毕业每两年的新手身上,其主要特点是『急于表现自我』、『对技术深度与广度出现无知而导致的过度自信』。 84 | 85 | 首先说说杂,比如你要应聘一个Java后端,老老实实把自己的java技术栈写好就行了,强调一下自己擅长什么即可,最好专精某领域比如『高并发』、『高可用』等等,这个时候一些简历非要给自己加戏,自己会的不会的一股脑往上堆,什么逆向、密码学、图形、驱动、AI都要体现出来,越杂越好,这种简历给人的印象就是个什么都不懂的半吊子。 86 | 87 | 再说说精,一个刚毕业的应届生,出来简历就各种精通,精通Java、精通Java虚拟机、精通spring全家桶、精通kafka等等,请放心,这种简历是不会没头没脑投过来了,这种在大学里就精通各种的天才早被他的各种学长介绍进了大厂或者外企做某某Star重点培养了,往往看到的这种也是半吊子。 88 | 89 | 再给大家一个技术栈模板: 90 | 91 | ![2019-07-02-17-46-10]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8af3021141d?w=641&h=131&f=png&s=29120) 92 | 93 | 这样写的后果就在于让面试官一眼就看出你是个吹牛的半吊子,那些各种精通的全才在业界早就出名了,根本不可能还在投简历。 94 | 95 | ## 准备工作经历 96 | 97 | 工作经历本身不用花太多笔墨去写,面试官主要想看的就是每段工作经历的持续时间、在不同公司担任的职责如何、是否有大厂的工作经验等等。 98 | 99 | 那么什么简历在这里给面试官减分呢? 100 | 101 | * 频繁跳槽:比如三年换了四家公司,每个公司呆的时长不要超过一年 102 | * 常年初级岗:比如工作五六年之后依然在完成一些简单的项目开发 103 | * 末流公司经历:在技术招聘届,大厂的优先级最高比如BAT、TMD甚至微软、谷歌等外企,知名度独角兽其次,比如商汤、旷视等等,一般的互联网公司排在第三,就是工作中小型的互联网公司一般大家叫不上名字,排在最后的就是外包和传统企业的经历 104 | 105 | 所以,如果你有频繁跳槽的经历怎么办?在本公司老老实实等到满一年再跳槽。 106 | 107 | 如果常年初级岗怎么办?想办法晋升或者参与一些业界知名项目,再或者写一个有一定复杂度的私人项目。 108 | 109 | 如果有末流公司经历怎么办?如果是很久以前的末流公司经验可以直接不写,也没人在乎你很早之前的工作经历,如果你现在就在末流公司,赶紧想办法跳槽,去不了大厂,去非知名的互联网公司也算是胜利大逃亡了。 110 | 111 | > 不建议任何形式的简历造假,如果去一些大厂,分分钟背调出来,与其简历造假,不如现在就行动起来,比如从现在到年底跳槽季,深度参与一个知名开源项目或者做一个有一定复杂度的私人项目绰绰有余,除非996. 112 | 113 | ## 准备项目经历 114 | 115 | 项目经历不管对于社招还是校招都是重中之重,很多时候成败就在于项目经历这块,一个普通本科可以通过优秀的项目经历逆袭985,一个小厂的员工也可以获得大厂的面试机会。 116 | 117 | 但是必须要说一下项目经历的编写很讲究,这是为后面面试部分铺路的绝佳机会,也是直接让你的简历扑街的重点沦陷区域。 118 | 119 | 先说容易让简历扑街的几个坑位。 120 | 121 | ### 切忌流水账写法 122 | 123 | 项目经历流水账写法是绝大多数简历的通病,通篇下来就讲了一件事『我干了啥』。 124 | 125 | 大部分简历却是这样的: 126 | 127 | > 用Vue、Vuex、Vue-router、axios等技术开发电商网站的前端部分,主要负责首页、店铺详情、商品详情、商品列表、订单详情、订单中心等相关页面的开发工作,与设计师与后端配合,可要高度还原设计稿。 128 | 129 | 这个描述有什么问题? 130 | 131 | 其实看似也没啥问题,但是这种流水账写法太多了,完全无法突出自己的优势展现自己的能力。 132 | 133 | 项目经历是考察重点,面试官想知道候选人在一次项目经历中扮演的角色、负责的模块、碰到的问题、解决的思路、达成的效果以及最后的总结与沉淀。 134 | 135 | 而上面的描述只显示了『我干了啥』,所以这种项目描述几乎是没意义的,因为对于面试官而言他看不到有效信息,没有有效信息的项目描述基本就没价值了,如果这个时候你还没有大厂经历或者名校背书,基本上也就凉了。 136 | 137 | ### 切忌堆积项目 138 | 139 | 堆积项目这种现象往往出现在没有什么优秀项目经历的简历身上,候选人企图以数量优势掩盖质量的劣势,其实往往适得其反,项目经历的一栏最好放2-3个项目,非常优秀的项目可能放一个就足够了,举个极端例子如果有一天尤雨溪写简历,其实只需要在项目经历那些一行『Vue.js作者』就行了,当然,他并不需要投简历。 140 | 141 | 有一些项目切忌放上去: 142 | 143 | * demo级项目:很多简历居然还在放一些仿xx官网的demo,这是十足的减分项,有一些则是东拼西凑抄了一些框架的源码搞了个玩具项目,也没有任何价值。 144 | * 烂大街的项目:这种以vue技术栈的为最,由于视频网站的某门课程流行,导致大量的仿饿了么、仿qq音乐、仿美团、仿去哪儿,同样Java的同学也是仿电商网站、仿大众点评等等,十份简历5份一模一样的项目,你是面试官会怎么想。 145 | * 低质量的开源项目:一个大原则就是低star的尽量别放(除非是高质量代码的冷门项目),长期弃坑的也不要放,不要为了凑数量把低质量的项目暴露出来,好好藏着。 146 | 147 | 如果只放两个项目,最好的搭配是一个公司内部挑大梁的项目和一个社区内的开源项目,后者之所以可以占据一席之地,是因为通过你的开源项目,面试官可以通过commit完整看到你的创造过程,比如工程化建设、commit规范、代码规范、协作方式、代码能力、沟通能力等等,这甚至比面试都有用,没有比开源项目更能展示你综合素质的东西了。 148 | 149 | ### 切忌放虚假项目 150 | 151 | 一个项目做没做过只要是有经验的面试官一问便知,如果你真的靠假项目忽悠过了面试,那这个公司八成也有问题,人才把关不过硬,你可以想象你的队友都是什么水平,在这种公司大成长价值也不大。 152 | 153 | 好,如果你说实在没项目可写了,我只能造假了,那么你应该想一下这多层追问。 154 | 155 | 比如你说你优化了一个前端项目的首屏性能,降低了白屏时间,那么面试官对这个性能优化问题会进行深挖,来考察候选人的实际水平: 156 | 157 | 1. 你的性能优化指标是怎么确定的?平均下来时间减短了多少? 158 | 2. 你的性能是如何测试的?有两种主流的性能测试方法你是怎么选的? 159 | 3. 你是根据哪些指标进行针对性优化的? 160 | 4. 除了你说的这些优化方法还有没有想过通过xx来解决? 161 | 5. 你的这个优化方法在实际操作中碰到过什么问题吗?有没有进一步做过测试? 162 | 6. 我们假设这么一种情况,比如xxxx,你会这么进行优化? 163 | 164 | 面试官多层追问的逻辑是这样的: 165 | 166 | 了解背景 -> 了解方案 -> 深挖方案 -> 模拟场景 167 | 168 | 首先得了解你性能优化的指标如何,接着需要了解你是这么测试的指标、再怎么进行针对性优化的,再接着提出一些其它解决方案考察你对优化场景的知识储备和方案决策能力,最后再模拟一个其它的业务场景,来考察你的技能迁移能力,看看是否是对某块领域有一定的了解,而不是只针对某个项目。 169 | 170 | 如果要真的在面试现场对答如流,那么一定是在某一块领域有一定知识储备的人,不是随随便便搞个项目就能蒙混过关的。 171 | 172 | ### 合格的项目经历如何写 173 | 174 | 合格的项目经历必须要有以下几点: 175 | 176 | * 项目概述 177 | * 个人职责 178 | * 项目难点 179 | * 工作成果 180 | 181 | 如果你不怕字太多,还可以选择性加入解决方案、选型思路等等,但是由于篇幅限制和为面试铺垫就不太建议写得太多。 182 | 183 | **项目概述**的目的是让面试官理解项目,不是每个人面试官都做过你的那种项目,所以需一个简述方便面试官理解。 184 | 185 | **个人职责**就是告诉面试官你在本项目中扮演的角色,是领导者?主导者?还是跟随者,你负责了哪些模块,承担了多大的工作量,以此来评估你在团队中的作用。 186 | 187 | **项目难点**的目的在于让面试官看到你碰到的技术难题,方便后续面试对项目进行一系列讨论。 188 | 189 | **工作成果**就很明显了,面试官需要看到你在做了上述工作到底达成了什么成绩,这个时候最好以数据说话,比如访问量、白屏时间等等。 190 | 191 | 像这种项目经历描述就比较合适: 192 | 193 | ![2019-07-03-09-47-50]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8af641250b4?w=748&h=413&f=png&s=108025) 194 | 195 | 这个时候也切忌展开长篇大论,把技术细节一个个写上去,甚至还写了心路历程的都是大忌,一方面篇幅太大会造成视觉混乱,另一方面面试官想看到的是『简』历,不是技术总结,面试官要面对上百份简历没那么时间来看你长篇大论,长篇大论大可以在面试中展开。 196 | 197 | 最好的方法就是一行文字简单得说清楚即可,反正项目面的时候一定会问到,到时候好好把你准备的内容讲给面试官,掌握面试的主动权就是从项目经历这一栏中开始。 198 | 199 | ## 教育背景 200 | 201 | 应届生可以写得更详细一点,比如绩点排名怎么样,有没有突出的科目,社招就不要写太多了,简单的入学时间、学校、专业即可,而且写你的最高学历即可,没必要从初中就开始写学历流水账,没有人看的。 202 | 203 | ![2019-07-03-10-01-30]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8af669dff4a?w=776&h=117&f=png&s=18095) 204 | 205 | ## 几点注意事项 206 | 207 | * 自我评价不建议写:技术面试几乎没人看你的自我评价,连面试技术问题都嫌『talk is cheap show me the code』,你的自我评价除了占篇幅没啥用处,充其量算是面试官的干扰信息。 208 | 209 | * 简历封面千万别搞:这都是一些简历制作网站骗用户付费的伎俩,不仅是互联网行业,其它行业我也没见过要简历封面这种无用操作的。 210 | 211 | ![2019-07-03-10-02-33]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8af7311195c?w=324&h=419&f=png&s=44715) 212 | 213 | * 证书不建议写:应届生可以酌情考虑弄个六级证书什么的,对于社招而言,列一堆证书甚至是减分项,国内的各种证你也懂的,是有多不自信才沦落到靠一堆证书来证明自己的价值。 214 | 215 | * 千万别用技能图表:首先用90分、80分来评价自己的技术本身就没有什么说服力,也不可能这么精准,而且什么是90分、什么是80根本就没有一个公论,所以用一般的比较通用的熟悉、精通描述即可,千万别加戏,面试官或者HR没那么多闲工夫去理解你的图表,老老实实按最通用高效的方式描述自己的技术栈。 216 | 217 | ![2019-07-03-15-09-28]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8af6e38d7b4?w=269&h=125&f=png&s=10791) 218 | 219 | * 简历最好一页:程序员又不是设计师有时候需要作品呈现,如果你的简历超过一页那么一定是出问题了,要么项目、技术栈描述太多太杂占据大量篇幅,要么加了一堆图表或者图画来加戏,当然往往是犯前一个错误的更多。 220 | 221 | 这是我在网上找到的一个例子很能说明问题: 222 | 223 | ![2019-07-03-16-00-28]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8af920e3f2c?w=689&h=347&f=png&s=165515) 224 | 225 | ![2019-07-03-16-00-38]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8af9384742f?w=667&h=294&f=png&s=100556) 226 | 227 | 简历的版面寸土寸金,别说话跟裹脚布一样,精炼的一句话即可描述你的问题。 228 | 229 | * 不建议用任何简历制作网站或者开源的简历制作器:我之前不仅用过上述的东西,还付过费,完全是浪费时间和浪费金钱,先说简历制作网站基本上都是那种花里胡哨的简历,看起来炫但是基本是面试官最讨厌的那种形式,开源的简历制作器也是类似的,我甚至还为了自己的简历魔改过这种制作器,到头来也是浪费时间,记住简历『黑白灰』三个配色,简洁即可,切勿让简历形式喧宾夺主。 230 | 231 | 这是我整理的简历范本(项目经历可以多写一个): 232 | 233 | ![2019-07-03-07-20-54]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8afa094ef23?w=506&h=610&f=png&s=97246) 234 | 235 | 简历范本可以去公众号『程序员面试官』后台回复『模板』二字领取。 236 | 237 | ## 你可能的疑问 238 | 239 | 如果你读到这里,谢谢你的耐心,可能你也会有疑问--『你这篇文章,这不让写,那不让写,我的简历填都填不满,怎么办?』。 240 | 241 | 实际上一份简历很多部分是已经固定了的,比如个人信息、教育背景、工作经历等等,其实能做文章的部分也只有技术栈和项目经历,也就是说后面两个部分是可以靠当下努力来改变的。 242 | 243 | 举个简单的例子,比如你做了3年的Java开发,公司还是用很老旧的SSM技术栈,自己其实有点沦为框架小子的意思,只能做一些增删改查这种类型的工作,虽然工作内容都能胜任,但是根本做不了更有挑战性的事情,而外面对Java工程师的要求已经越来越高了. 244 | 245 | ![2019-07-03-16-16-49]( https://user-gold-cdn.xitu.io/2019/7/4/16bba8afad3c463a?w=638&h=176&f=png&s=57416) 246 | 247 | 我们完全可以花半年到一年的时间对某个细分领域进行专门的学习和实践,我们可以通过写私人项目、参与开源项目的方式增加自己的项目经验和项目履历,一段时间后你肯定在某个细分领域至少处于一个进阶水平,你的简历也不可能填都填不满。 248 | 249 | 对于前端工程师也是一样,如果你觉得你逐渐沦为页面仔,自己也没有拿得出手的项目,也不妨多思考之前的项目是不是有的性能部分可以优化,是不是平时的工作有很多重复性的,能不能通过node工具或者vscode插件来提高效率,又或者公司的框架用起来太繁琐,可不可以进行改造升级提高生产力。 250 | 251 | 这个时候可能有人又问,『我自己工作都多的不行,凭什么还想为公司写什么工具框架?公司会额外付钱吗?』 252 | 253 | 你写的框架和工具是你未来跳槽中的简历的重要部分,即使它现在不会变现,在你跳槽过程中一定会变现,总之这些额外工作是为你自己打工的,你的现任公司只是因此额外受益了而已。 254 | 255 | ## 总结 256 | 257 | 我知道现在并不是跳槽的旺季,可能很多人不会看这篇文章,但是当真正跳槽季来临的时候,往往很多人又开始为填满自己的简历而发愁,当自己的简历石沉大海,又会冒出这种言论: 258 | 259 | * 哎呀,还是自己学历不够好,我能力没问题就是吃了学历的亏 260 | * 自己没有大厂的履历真是吃亏,自己能力没问题,就是没大厂背书 261 | * 所在的公司都是一些老技术栈,我的简历就太吃亏了,都怪公司 262 | 263 | 实际情况是,大厂履历、名校经历、出色项目只要有一项拿得出手,就会成为抢手货,更何况随着时间的推移,教育背景就越发不重要,更重要的还是工作履历和项目经历。 264 | 265 | 与其今后发愁如何填满简历,不如现在行动为自己的简历『打工』。 266 | 267 | 268 | --- 269 | 270 | ## 公众号 271 | 272 | 想要实时关注笔者最新的文章和最新的文档更新请关注公众号**程序员面试官**,后续的文章会优先在公众号更新. 273 | 274 | **简历模板:** 关注公众号回复「模板」获取 275 | 276 | **《前端面试手册》:** 配套于本指南的突击手册,关注公众号回复「fed」获取 277 | 278 | ![2019-08-12-03-18-41]( https://xiaomuzhu-image.oss-cn-beijing.aliyuncs.com/d846f65d5025c4b6c4619662a0669503.png) 279 | --------------------------------------------------------------------------------