Repository: febobo/web-interview Branch: master Commit: 7a688c8d941d Files: 272 Total size: 1.3 MB Directory structure: gitextract_tq4ln75s/ ├── .github/ │ └── workflows/ │ └── blank.yml ├── .gitignore ├── .vscode/ │ └── settings.json ├── docs/ │ ├── .vuepress/ │ │ ├── config.js │ │ └── theme/ │ │ ├── LICENSE │ │ ├── components/ │ │ │ ├── AlgoliaSearchBox.vue │ │ │ ├── DropdownLink.vue │ │ │ ├── DropdownTransition.vue │ │ │ ├── Home.vue │ │ │ ├── NavLink.vue │ │ │ ├── NavLinks.vue │ │ │ ├── Navbar.vue │ │ │ ├── Page.vue │ │ │ ├── PageEdit.vue │ │ │ ├── PageNav.vue │ │ │ ├── Sidebar.vue │ │ │ ├── SidebarButton.vue │ │ │ ├── SidebarGroup.vue │ │ │ ├── SidebarLink.vue │ │ │ └── SidebarLinks.vue │ │ ├── global-components/ │ │ │ └── Badge.vue │ │ ├── index.js │ │ ├── layouts/ │ │ │ ├── 404.vue │ │ │ └── Layout.vue │ │ ├── noopModule.js │ │ ├── styles/ │ │ │ ├── arrow.styl │ │ │ ├── code.styl │ │ │ ├── config.styl │ │ │ ├── custom-blocks.styl │ │ │ ├── index.styl │ │ │ ├── mobile.styl │ │ │ ├── toc.styl │ │ │ └── wrapper.styl │ │ └── util/ │ │ └── index.js │ ├── JavaScript/ │ │ ├── == _===.md │ │ ├── BOM.md │ │ ├── Dom.md │ │ ├── ajax.md │ │ ├── array_api.md │ │ ├── bind_call_apply.md │ │ ├── cache.md │ │ ├── closure.md │ │ ├── context_stack.md │ │ ├── continue_to_upload.md │ │ ├── copy.md │ │ ├── data_type.md │ │ ├── debounce_throttle.md │ │ ├── event_Model.md │ │ ├── event_agent.md │ │ ├── event_loop.md │ │ ├── function_cache.md │ │ ├── functional_programming.md │ │ ├── inherit.md │ │ ├── js_data_structure.md │ │ ├── loss_accuracy.md │ │ ├── memory_leak.md │ │ ├── new.md │ │ ├── prototype.md │ │ ├── pull_up_loading_pull_down_refresh.md │ │ ├── regexp.md │ │ ├── scope.md │ │ ├── security.md │ │ ├── single_sign.md │ │ ├── string_api.md │ │ ├── tail_recursion.md │ │ ├── this.md │ │ ├── type_conversion.md │ │ ├── typeof_instanceof.md │ │ └── visible.md │ ├── NodeJS/ │ │ ├── Buffer.md │ │ ├── EventEmitter.md │ │ ├── Stream.md │ │ ├── event_loop.md │ │ ├── file_upload.md │ │ ├── fs.md │ │ ├── global.md │ │ ├── jwt.md │ │ ├── middleware.md │ │ ├── nodejs.md │ │ ├── paging.md │ │ ├── performance.md │ │ ├── process.md │ │ └── require_order.md │ ├── README.md │ ├── React/ │ │ ├── Binding events.md │ │ ├── Building components.md │ │ ├── Fiber.md │ │ ├── High order components.md │ │ ├── Improve performance.md │ │ ├── JSX to DOM.md │ │ ├── React Hooks.md │ │ ├── React Router model.md │ │ ├── React Router.md │ │ ├── React refs.md │ │ ├── React.md │ │ ├── Real DOM_Virtual DOM.md │ │ ├── Redux Middleware.md │ │ ├── SyntheticEvent.md │ │ ├── animation.md │ │ ├── capture error.md │ │ ├── class_function component.md │ │ ├── communication.md │ │ ├── controlled_Uncontrolled.md │ │ ├── diff.md │ │ ├── how to use redux.md │ │ ├── immutable.md │ │ ├── import css.md │ │ ├── improve_render.md │ │ ├── key.md │ │ ├── life cycle.md │ │ ├── redux.md │ │ ├── render.md │ │ ├── server side rendering.md │ │ ├── setState.md │ │ ├── state_props.md │ │ ├── summary.md │ │ └── super()_super(props).md │ ├── algorithm/ │ │ ├── Algorithm.md │ │ ├── BinarySearch.md │ │ ├── Heap.md │ │ ├── Linked List.md │ │ ├── bubbleSort.md │ │ ├── design1.md │ │ ├── design2.md │ │ ├── graph.md │ │ ├── insertionSort.md │ │ ├── mergeSort.md │ │ ├── quickSort.md │ │ ├── selectionSort.md │ │ ├── set.md │ │ ├── sort.md │ │ ├── stack_queue.md │ │ ├── structure.md │ │ ├── time_space.md │ │ └── tree.md │ ├── applet/ │ │ ├── WebView_jscore.md │ │ ├── applet.md │ │ ├── lifecycle.md │ │ ├── login.md │ │ ├── navigate.md │ │ ├── optimization.md │ │ ├── publish.md │ │ └── requestPayment.md │ ├── css/ │ │ ├── BFC.md │ │ ├── animation.md │ │ ├── box.md │ │ ├── center.md │ │ ├── column_layout.md │ │ ├── css3_features.md │ │ ├── css_performance.md │ │ ├── dp_px_dpr_ppi.md │ │ ├── em_px_rem_vh_vw.md │ │ ├── flexbox.md │ │ ├── grid.md │ │ ├── hide_attributes.md │ │ ├── layout_painting.md │ │ ├── less_12px.md │ │ ├── responsive_layout.md │ │ ├── sass_less_stylus.md │ │ ├── selector.md │ │ ├── single_multi_line.md │ │ ├── triangle.md │ │ └── visual_scrolling.md │ ├── design/ │ │ ├── Factory Pattern.md │ │ ├── Observer Pattern.md │ │ ├── Proxy Pattern.md │ │ ├── Singleton Pattern.md │ │ ├── Strategy Pattern.md │ │ └── design.md │ ├── es6/ │ │ ├── array.md │ │ ├── decorator.md │ │ ├── function.md │ │ ├── generator.md │ │ ├── module.md │ │ ├── object.md │ │ ├── promise.md │ │ ├── proxy.md │ │ ├── set_map.md │ │ └── var_let_const.md │ ├── git/ │ │ ├── Git.md │ │ ├── HEAD_tree_index.md │ │ ├── Version control.md │ │ ├── command.md │ │ ├── conflict.md │ │ ├── fork_clone_branch.md │ │ ├── git pull _git fetch.md │ │ ├── git rebase_ git merge.md │ │ ├── git reset_ git revert.md │ │ └── git stash.md │ ├── http/ │ │ ├── 1.0_1.1_2.0.md │ │ ├── CDN.md │ │ ├── DNS.md │ │ ├── GET_POST.md │ │ ├── HTTPS.md │ │ ├── HTTP_HTTPS.md │ │ ├── OSI.md │ │ ├── TCP_IP.md │ │ ├── UDP_TCP.md │ │ ├── WebSocket.md │ │ ├── after_url.md │ │ ├── handshakes_waves.md │ │ ├── headers.md │ │ └── status.md │ ├── linux/ │ │ ├── file.md │ │ ├── linux users.md │ │ ├── linux.md │ │ ├── redirect_pipe.md │ │ ├── shell.md │ │ ├── thread_process.md │ │ └── vim.md │ ├── typescript/ │ │ ├── class.md │ │ ├── data_type.md │ │ ├── decorator.md │ │ ├── enum.md │ │ ├── function.md │ │ ├── generic.md │ │ ├── high type.md │ │ ├── interface.md │ │ ├── namespace_module.md │ │ ├── react.md │ │ ├── typescript_javascript.md │ │ └── vue.md │ ├── vue/ │ │ ├── 404.md │ │ ├── axios.md │ │ ├── axiosCode.md │ │ ├── bind.md │ │ ├── communication.md │ │ ├── components_plugin.md │ │ ├── cors.md │ │ ├── data.md │ │ ├── data_object_add_attrs.md │ │ ├── diff.md │ │ ├── directive.md │ │ ├── error.md │ │ ├── filter.md │ │ ├── first_page_time.md │ │ ├── if_for.md │ │ ├── keepalive.md │ │ ├── key.md │ │ ├── lifecycle.md │ │ ├── mixin.md │ │ ├── modifier.md │ │ ├── new_vue.md │ │ ├── nexttick.md │ │ ├── observable.md │ │ ├── permission.md │ │ ├── show_if.md │ │ ├── slot.md │ │ ├── spa.md │ │ ├── ssr.md │ │ ├── structure.md │ │ ├── vnode.md │ │ ├── vue.md │ │ └── vue3_vue2.md │ ├── vue3/ │ │ ├── composition.md │ │ ├── goal.md │ │ ├── modal_component.md │ │ ├── performance.md │ │ ├── proxy.md │ │ └── treeshaking.md │ └── webpack/ │ ├── HMR.md │ ├── Loader.md │ ├── Loader_Plugin.md │ ├── Plugin.md │ ├── Rollup_Parcel_snowpack_Vite.md │ ├── build_process.md │ ├── improve_build.md │ ├── performance.md │ ├── proxy.md │ └── webpack.md ├── package-lock.json.bakn └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/blank.yml ================================================ # This is a basic workflow to help you get started with Actions name: CI # Controls when the action will run. on: # Triggers the workflow on push or pull request events but only for the master branch push: branches: [ master ] pull_request: branches: [ master ] # Allows you to run this workflow manually from the Actions tab workflow_dispatch: # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: # This workflow contains a single job called "build" build: # The type of runner that the job will run on runs-on: ubuntu-latest # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - uses: actions/checkout@v2 - name: use Node.js # 使用action库 actions/setup-node安装node uses: actions/setup-node@v1 with: node-version: 10.x # 安装依赖 - name: npm install run: npm install # 打包 - name: npm build run: npm run build - name: deploy uses: easingthemes/ssh-deploy@v2.1.1 env: # 私钥 SSH_PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} SOURCE: "./dist" REMOTE_HOST: ${{ secrets.HOST }} REMOTE_USER: "root" TARGET: ${{ secrets.PATH }} ================================================ FILE: .gitignore ================================================ /coverage /docs/.vuepress/dist /examples/**/build.js /test/e2e/reports /test/e2e/screenshots /types/typings /types/test/*.js *.log .DS_Store node_modules ================================================ FILE: .vscode/settings.json ================================================ { "eggHelper.serverPort": 35684 } ================================================ FILE: docs/.vuepress/config.js ================================================ module.exports = { // title: "Vue3源码解析 - vue中文社区", title: "web前端面试 - 面试官系列", description: "web前端面试,vue面试题,react面试题,js面试题,大厂面试题,阿里面试题,京东面试题", base: '/interview/', head: [ ["link", { rel: "icon", href: "/onepunch.jpeg" }], [ "meta", { name: "keywords", content: "web前端面试,vue面试题,react面试题,js面试题,大厂面试题,阿里面试题,京东面试题", }, ], // [ // "script", // { src: "https://hm.baidu.com/hm.js?db1f163122162bcdb6d04f76b5c1df17" }, // ], ], themeConfig: { repo: "febobo/web-interview", // 自定义仓库链接文字。默认从 `themeConfig.repo` 中自动推断为 // "GitHub"/"GitLab"/"Bitbucket" 其中之一,或是 "Source"。 repoLabel: "Github", // 以下为可选的编辑链接选项 // 假如你的文档仓库和项目本身不在一个仓库: docsRepo: "febobo/web-interview", docsDir: "docs", docsBranch: "master", // 默认是 false, 设置为 true 来启用 editLinks: false, // 默认为 "Edit this page" editLinkText: "帮助我们改善此页面!", // displayAllHeaders: true, sidebar: [ { title: "Vue系列 ( 已完结..)", collapsable: false, children: [ ["vue/vue", "说说你对vue的理解?"], ["vue/spa", "说说你对SPA(单页应用)的理解?"], ["vue/show_if", "Vue中的v-show和v-if怎么理解?"], ["vue/new_vue", "Vue实例挂载的过程中发生了什么?"], ["vue/lifecycle", "说说你对Vue生命周期的理解?"], ["vue/if_for", "为什么Vue中的v-if和v-for不建议一起用?"], ["vue/first_page_time", "SPA(单页应用)首屏加载速度慢怎么解决?"], ["vue/data", "为什么data属性是一个函数而不是一个对象?"], ["vue/data_object_add_attrs", "Vue中给对象添加新属性界面不刷新?"], ["vue/components_plugin", "Vue中组件和插件有什么区别?"], ["vue/communication", "Vue组件间通信方式都有哪些?"], ["vue/bind", "说说你对双向绑定的理解?"], ["vue/nexttick", "说说你对nexttick的理解?"], ["vue/mixin", "说说你对vue的mixin的理解,有什么应用场景?"], ["vue/slot", "说说你对slot的理解?slot使用场景有哪些?"], ["vue/observable", "Vue.observable你有了解过吗?说说看"], ["vue/key", "你知道vue中key的原理吗?说说你对它的理解?"], ["vue/keepalive", "怎么缓存当前的组件?缓存后怎么更新?说说你对keep-alive的理解是什么?"], ["vue/modifier", "Vue常用的修饰符有哪些?有什么应用场景?"], ["vue/directive", "你有写过自定义指令吗?自定义指令的应用场景有哪些?"], ["vue/filter", "Vue中的过滤器了解吗?过滤器的应用场景有哪些?"], ["vue/vnode", "什么是虚拟DOM?如何实现一个虚拟DOM?说说你的思路"], ["vue/diff", "你了解vue的diff算法吗?说说看"], ["vue/axios", "Vue项目中有封装过axios吗?主要是封装哪方面的?"], ["vue/axiosCode", "你了解axios的原理吗?有看过它的源码吗?"], ["vue/ssr", "SSR解决了什么问题?有做过SSR吗?你是怎么做的?"], ["vue/structure", "说下你的vue项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢?"], ["vue/permission", "vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?"], ["vue/cors", "Vue项目中你是如何解决跨域的呢?"], ["vue/404", "vue项目本地开发完成后部署到服务器后报404是什么原因呢?"], ["vue/error", "你是怎么处理vue项目中的错误的?"], ["vue/vue3_vue2", "Vue3有了解过吗?能说说跟Vue2的区别吗?"] ], }, { title: "Vue3系列 ( 已完结..)", collapsable: false, children: [ ["vue3/goal", "Vue3.0的设计目标是什么?做了哪些优化?"], ["vue3/performance", "Vue3.0 性能提升主要是通过哪几方面体现的?"], ["vue3/proxy", "Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?"], ["vue3/composition", "Vue3.0 所采用的 Composition Api 与 Vue2.x 使用的 Options Api 有什么不同?"], ["vue3/treeshaking", "说说Vue 3.0中Treeshaking特性?举例说明一下?"], ["vue3/modal_component", "用Vue3.0 写过组件吗?如果想实现一个 Modal你会怎么设计?"], ], }, { title: "ES6系列 ( 已完结..)", collapsable: false, children: [ ["es6/var_let_const", "说说var、let、const之间的区别"], ["es6/array", "ES6中数组新增了哪些扩展?"], ["es6/object", "ES6中对象新增了哪些扩展?"], ["es6/function", "ES6中函数新增了哪些扩展?"], ["es6/set_map", "ES6中新增的Set、Map两种数据结构怎么理解?"], ["es6/promise", "你是怎么理解ES6中 Promise的?使用场景?"], ["es6/generator", "怎么理解ES6中 Generator的?使用场景?"], ["es6/proxy", "你是怎么理解ES6中Proxy的?使用场景?"], ["es6/module", "你是怎么理解ES6中Module的?使用场景?"], ["es6/decorator", "你是怎么理解ES6中 Decorator 的?使用场景?"], ], }, { title: "JavaScript系列 ( 已完结..)", collapsable: false, children: [ ["JavaScript/data_type", "说说JavaScript中的数据类型?存储上的差别?"], ["JavaScript/array_api", "数组的常用方法有哪些?"], ["JavaScript/string_api", "JavaScript字符串的常用方法有哪些?"], ["JavaScript/type_conversion", "谈谈 JavaScript 中的类型转换机制"], ["JavaScript/== _===", "== 和 ===区别,分别在什么情况使用"], ["JavaScript/copy", "深拷贝浅拷贝的区别?如何实现一个深拷贝?"], ["JavaScript/closure", "说说你对闭包的理解?闭包使用场景"], ["JavaScript/scope", "说说你对作用域链的理解"], ["JavaScript/prototype", "JavaScript原型,原型链 ? 有什么特点?"], ["JavaScript/inherit", "Javascript如何实现继承?"], ["JavaScript/this", "谈谈this对象的理解"], ["JavaScript/context_stack", "JavaScript中执行上下文和执行栈是什么?"], ["JavaScript/event_Model", "说说JavaScript中的事件模型"], ["JavaScript/typeof_instanceof", "typeof 与 instanceof 区别"], ["JavaScript/event_agent", "解释下什么是事件代理?应用场景?"], ["JavaScript/new", "说说new操作符具体干了什么?"], ["JavaScript/ajax", "ajax原理是什么?如何实现?"], ["JavaScript/bind_call_apply", "bind、call、apply 区别?如何实现一个bind?"], ["JavaScript/regexp", "说说你对正则表达式的理解?应用场景?"], ["JavaScript/event_loop", "说说你对事件循环的理解"], ["JavaScript/Dom", "DOM常见的操作有哪些?"], ["JavaScript/BOM", "说说你对BOM的理解,常见的BOM对象你了解哪些?"], ["JavaScript/tail_recursion", "举例说明你对尾递归的理解,有哪些应用场景"], ["JavaScript/memory_leak", "说说 JavaScript 中内存泄漏的几种情况?"], ["JavaScript/cache", "Javascript本地存储的方式有哪些?区别及应用场景?"], ["JavaScript/functional_programming", "说说你对函数式编程的理解?优缺点?"], ["JavaScript/function_cache", "Javascript中如何实现函数缓存?函数缓存有哪些应用场景?"], ["JavaScript/loss_accuracy", "说说 Javascript 数字精度丢失的问题,如何解决?"], ["JavaScript/debounce_throttle", "什么是防抖和节流?有什么区别?如何实现?"], ["JavaScript/visible", "如何判断一个元素是否在可视区域中?"], ["JavaScript/continue_to_upload", "大文件上传如何做断点续传?"], ["JavaScript/pull_up_loading_pull_down_refresh", "如何实现上拉加载,下拉刷新?"], ["JavaScript/single_sign", "什么是单点登录?如何实现?"], ["JavaScript/security", "web常见的攻击方式有哪些?如何防御?"], ], }, { title: "CSS系列 ( 已完结..)", collapsable: false, children: [ ["css/box", "说说你对盒子模型的理解?"], ["css/selector", "css选择器有哪些?优先级?哪些属性可以继承?"], ["css/em_px_rem_vh_vw", "说说em/px/rem/vh/vw区别?"], ["css/dp_px_dpr_ppi", "说说设备像素、css像素、设备独立像素、dpr、ppi 之间的区别?"], ["css/hide_attributes", "css中,有哪些方式可以隐藏页面元素?区别?"], ["css/BFC", "谈谈你对BFC的理解?"], ["css/center", "元素水平垂直居中的方法有哪些?如果元素不定宽高呢?"], ["css/column_layout", "如何实现两栏布局,右侧自适应?三栏布局中间自适应呢?"], ["css/flexbox", "说说flexbox(弹性盒布局模型),以及适用场景?"], ["css/grid", "介绍一下grid网格布局"], ["css/css3_features", "CSS3新增了哪些新特性?"], ["css/animation", "css3动画有哪些?"], ["css/layout_painting", "怎么理解回流跟重绘?什么场景下会触发?"], ["css/responsive_layout", "什么是响应式设计?响应式设计的基本原理是什么?如何做?"], ["css/css_performance", "如果要做优化,CSS提高性能的方法有哪些?"], ["css/single_multi_line", "如何实现单行/多行文本溢出的省略样式?"], ["css/visual_scrolling", "如何使用css完成视差滚动效果?"], ["css/triangle", "CSS如何画一个三角形?原理是什么?"], ["css/less_12px", "让Chrome支持小于12px 的文字方式有哪些?区别?"], ["css/sass_less_stylus", "说说对Css预编语言的理解?有哪些区别?"], ], }, { title: "Webpack系列 ( 已完结..)", collapsable: false, children:[ ["webpack/webpack", "说说你对webpack的理解?解决了什么问题?"], ["webpack/build_process", "说说webpack的构建流程?"], ["webpack/Loader", "说说webpack中常见的Loader?解决了什么问题?"], ["webpack/Plugin", "说说webpack中常见的Plugin?解决了什么问题?"], ["webpack/Loader_Plugin", "说说Loader和Plugin的区别?编写Loader,Plugin的思路?"], ["webpack/HMR", "说说webpack的热更新是如何做到的?原理是什么?"], ["webpack/proxy", "说说webpack proxy工作原理?为什么能解决跨域?"], ["webpack/performance", "说说如何借助webpack来优化前端性能?"], ["webpack/improve_build", "如何提高webpack的构建速度?"], ["webpack/Rollup_Parcel_snowpack_Vite", "与webpack类似的工具还有哪些?区别?"], ] }, { title: "HTTP系列 ( 已完结..)", collapsable: false, children:[ ["http/HTTP_HTTPS", "什么是HTTP? HTTP 和 HTTPS 的区别?"], ["http/HTTPS", "为什么说HTTPS比HTTP安全? HTTPS是如何保证安全的?"], ["http/UDP_TCP", "如何理解UDP 和 TCP? 区别? 应用场景?"], ["http/OSI", "如何理解OSI七层模型?"], ["http/TCP_IP", "如何理解TCP/IP协议?"], ["http/DNS", "DNS协议 是什么?说说DNS 完整的查询过程?"], ["http/CDN", "如何理解CDN?说说实现原理?"], ["http/1.0_1.1_2.0", "说说 HTTP1.0/1.1/2.0 的区别?"], ["http/status", "说说 HTTP 常见的状态码有哪些,适用场景?"], ["http/GET_POST", "说一下 GET 和 POST 的区别?"], ["http/headers", "说说 HTTP 常见的请求头有哪些? 作用?"], ["http/after_url", "说说地址栏输入 URL 敲下回车后发生了什么?"], ["http/handshakes_waves", "说说TCP为什么需要三次握手和四次挥手?"], ["http/WebSocket", "说说对WebSocket的理解?应用场景?"] ] }, { title: "NodeJS系列 ( 已完结..)", collapsable: false, children:[ ["NodeJS/nodejs", "说说你对 Node.js 的理解?优缺点?应用场景?"], ["NodeJS/global", "说说 Node.js 有哪些全局对象?"], ["NodeJS/process", "说说对 Node 中的 process 的理解?有哪些常用方法?"], ["NodeJS/fs", "说说对 Node 中的 fs模块的理解? 有哪些常用方法"], ["NodeJS/Buffer", "说说对 Node 中的 Buffer 的理解?应用场景?"], ["NodeJS/Stream", "说说对 Node 中的 Stream 的理解?应用场景?"], ["NodeJS/EventEmitter", "说说Node中的EventEmitter? 如何实现一个EventEmitter?"], ["NodeJS/event_loop", "说说对 Nodejs 中的事件循环机制理解?"], ["NodeJS/require_order", "说说 Node 文件查找的优先级以及 Require 方法的文件查找策略?"], ["NodeJS/middleware", "说说对中间件概念的理解,如何封装 node 中间件?"], ["NodeJS/jwt", "如何实现jwt鉴权机制?说说你的思路"], ["NodeJS/file_upload", "如何实现文件上传?说说你的思路"], ["NodeJS/paging", "如果让你来设计一个分页功能, 你会怎么设计? 前后端如何交互?"], ["NodeJS/performance", "Node性能如何进行监控以及优化?"], ] }, { title: "React系列 ( 已完结..)", collapsable: false, children:[ ["React/React", "说说对React的理解?有哪些特性?"], ["React/Real DOM_Virtual DOM", "说说 Real DOM和 Virtual DOM 的区别?优缺点?"], ["React/life cycle", "说说 React 生命周期有哪些不同阶段?每个阶段对应的方法是?"], ["React/state_props", "state 和 props有什么区别?"], ["React/super()_super(props)", "super()和super(props)有什么区别?"], ["React/setState", "说说 React中的setState执行机制"], ["React/SyntheticEvent", "说说React的事件机制?"], ["React/Binding events", "React事件绑定的方式有哪些?区别?"], ["React/Building components", "React构建组件的方式有哪些?区别?"], ["React/communication", "React中组件之间如何通信?"], ["React/key", "React中的key有什么作用?"], ["React/React refs", "说说对React refs 的理解?应用场景?"], ["React/class_function component", "说说对React中类组件和函数组件的理解?有什么区别?"], ["React/controlled_Uncontrolled", "说说对受控组件和非受控组件的理解?应用场景?"], ["React/High order components", "说说对高阶组件的理解?应用场景?"], ["React/React Hooks", "说说对React Hooks的理解?解决了什么问题?"], ["React/import css", "说说react中引入css的方式有哪几种?区别?"], ["React/animation", "在react中组件间过渡动画如何实现?"], ["React/redux", "说说你对Redux的理解?其工作原理?"], ["React/Redux Middleware", "说说对Redux中间件的理解?常用的中间件有哪些?实现原理?"], ["React/how to use redux", "你在React项目中是如何使用Redux的? 项目结构是如何划分的?"], ["React/React Router", "说说你对React Router的理解?常用的Router组件有哪些?"], ["React/React Router model", "说说React Router有几种模式?实现原理??"], ["React/immutable", "说说你对immutable的理解?如何应用在react项目中?"], ["React/render", "说说React render方法的原理?在什么时候会被触发?"], ["React/improve_render", "说说你是如何提高组件的渲染效率的?在React中如何避免不必要的render?"], ["React/diff", "说说React diff的原理是什么?"], ["React/Fiber", "说说对Fiber架构的理解?解决了什么问题?"], ["React/JSX to DOM", "说说React Jsx转换成真实DOM过程?"], ["React/Improve performance", "说说 React 性能优化的手段有哪些?"], ["React/capture error", "说说你在React项目是如何捕获错误的?"], ["React/server side rendering", "说说React服务端渲染怎么做?原理是什么?"], ["React/summary", "说说你在使用React 过程中遇到的常见问题?如何解决?"] ] }, { title: "版本控制系列 ( 已完结..)", collapsable: false, children:[ ["git/Version control", "说说你对版本管理的理解?常用的版本管理工具有哪些?"], ["git/Git", "说说你对Git的理解?"], ["git/fork_clone_branch", "说说Git中 fork, clone,branch这三个概念,有什么区别?"], ["git/command", "说说Git常用的命令有哪些?"], ["git/HEAD_tree_index", "说说Git 中 HEAD、工作树和索引之间的区别?"], ["git/git pull _git fetch", "说说对git pull 和 git fetch 的理解?有什么区别?"], ["git/git stash", "说说你对git stash 的理解?应用场景?"], ["git/git rebase_ git merge", "说说你对git rebase 和 git merge的理解?区别?"], ["git/conflict", "说说 git 发生冲突的场景?如何解决?"], ["git/git reset_ git revert", "说说你对git reset 和 git revert 的理解?区别?"], ] }, { title: "操作系统系列 ( 已完结..)", collapsable: false, children:[ ["linux/linux", "说说你对操作系统的理解?核心概念有哪些?"], ["linux/thread_process", "说说什么是进程?什么是线程?区别?"], ["linux/file", "说说 linux系统下 文件操作常用的命令有哪些?"], ["linux/vim", "说说 linux 系统下 文本编辑常用的命令有哪些?"], ["linux/linux users", "说说你对 linux 用户管理的理解?相关的命令有哪些?"], ["linux/redirect_pipe", "说说你对输入输出重定向和管道的理解?应用场景?"], ["linux/shell", "说说你对 shell 的理解?常见的命令?"], ] }, { title: "TypeScript 系列 ( 已完结..)", collapsable: false, children:[ ["typescript/typescript_javascript", "说说你对 TypeScript 的理解?与 JavaScript 的区别?"], ["typescript/data_type", "说说 typescript 的数据类型有哪些?"], ["typescript/enum", "说说你对 TypeScript 中枚举类型的理解?应用场景?"], ["typescript/interface", "说说你对 TypeScript 中接口的理解?应用场景?"], ["typescript/class", "说说你对 TypeScript 中类的理解?应用场景?"], ["typescript/function", "说说你对 TypeScript 中函数的理解?与 JavaScript 函数的区别?"], ["typescript/generic", "说说你对 TypeScript 中泛型的理解?应用场景?"], ["typescript/high type", "说说你对 TypeScript 中高级类型的理解?有哪些?"], ["typescript/decorator", "说说你对 TypeScript 装饰器的理解?应用场景?"], ["typescript/namespace_module", "说说对 TypeScript 中命名空间与模块的理解?区别?"], ["typescript/react", "说说如何在 React 项目中应用 TypeScript?"], ["typescript/vue", "说说如何在Vue项目中应用TypeScript?"] ] }, { title: "算法与数据结构系列 ( 已完结..)", collapsable: false, children:[ ["algorithm/Algorithm", "说说你对算法的理解?应用场景?"], ["algorithm/time_space", "说说你对算法中时间复杂度,空间复杂度的理解?如何计算?"], ["algorithm/structure", "说说你对数据结构的理解?有哪些?区别?"], ["algorithm/stack_queue", "说说你对栈、队列的理解?应用场景?"], ["algorithm/Linked List", "说说你对链表的理解?常见的操作有哪些?"], ["algorithm/set", "说说你对集合的理解?常见的操作有哪些?"], ["algorithm/tree", "说说你对树的理解?相关的操作有哪些?"], ["algorithm/Heap", "说说你对堆的理解?如何实现?应用场景?"], ["algorithm/graph", "说说你对图的理解?相关操作有哪些?"], ["algorithm/sort", "说说常见的排序算法有哪些?区别?"], ["algorithm/bubbleSort", "说说你对冒泡排序的理解?如何实现?应用场景?"], ["algorithm/selectionSort", "说说你对选择排序的理解?如何实现?应用场景?"], ["algorithm/insertionSort", "说说你对插入排序的理解?如何实现?应用场景?"], ["algorithm/mergeSort", "说说你对归并排序的理解?如何实现?应用场景?"], ["algorithm/quickSort", "说说你对快速排序的理解?如何实现?应用场景?"], ["algorithm/BinarySearch", "说说你对二分查找的理解?如何实现?应用场景?"], ["algorithm/design1", "说说说你对分而治之、动态规划的理解?区别?"], ["algorithm/design2", "说说你对贪心算法、回溯算法的理解?应用场景?"], ] }, { title: "小程序系列 ( 已完结..)", collapsable: false, children:[ ["applet/applet", "说说你对微信小程序的理解?优缺点?"], ["applet/lifecycle", "说说微信小程序的生命周期函数有哪些?"], ["applet/navigate", "说说微信小程序中路由跳转的方式有哪些?区别?"], ["applet/optimization", "说说提高微信小程序的应用速度的手段有哪些?"], ["applet/login", "说说微信小程序的登录流程?"], ["applet/publish", "说说微信小程序的发布流程?"], ["applet/requestPayment", "说说微信小程序的支付流程?"], ["applet/WebView_jscore", "说说微信小程序的实现原理?"], ] }, { title: "设计模式系列 ( 进行中..)", collapsable: false, children:[ ["design/design", "说说对设计模式的理解?常见的设计模式有哪些?"], ["design/Singleton Pattern", "说说你对单例模式的理解?如何实现?"], ["design/Factory Pattern", "说说你对工厂模式的理解?应用场景?"], ["design/Strategy Pattern", "说说你对策略模式的理解?应用场景?"], ["design/Proxy Pattern", "说说你对代理模式的理解?应用场景?"], ["design/Observer Pattern", "说说你对发布订阅、观察者模式的理解?区别?"], ] }, ], }, markdown: { lineNumbers: true, }, }; ================================================ FILE: docs/.vuepress/theme/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2018-present, Yuxi (Evan) You Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: docs/.vuepress/theme/components/AlgoliaSearchBox.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/DropdownLink.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/DropdownTransition.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/Home.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/NavLink.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/NavLinks.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/Navbar.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/Page.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/PageEdit.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/PageNav.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/Sidebar.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/SidebarButton.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/SidebarGroup.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/SidebarLink.vue ================================================ ================================================ FILE: docs/.vuepress/theme/components/SidebarLinks.vue ================================================ ================================================ FILE: docs/.vuepress/theme/global-components/Badge.vue ================================================ ================================================ FILE: docs/.vuepress/theme/index.js ================================================ const path = require('path') // Theme API. module.exports = (options, ctx) => { const { themeConfig, siteConfig } = ctx // resolve algolia const isAlgoliaSearch = ( themeConfig.algolia || Object .keys(siteConfig.locales && themeConfig.locales || {}) .some(base => themeConfig.locales[base].algolia) ) const enableSmoothScroll = themeConfig.smoothScroll === true return { alias () { return { '@AlgoliaSearchBox': isAlgoliaSearch ? path.resolve(__dirname, 'components/AlgoliaSearchBox.vue') : path.resolve(__dirname, 'noopModule.js') } }, plugins: [ ['@vuepress/active-header-links', options.activeHeaderLinks], '@vuepress/search', '@vuepress/plugin-nprogress', ['container', { type: 'tip', defaultTitle: { '/': 'TIP', '/zh/': '提示' } }], ['container', { type: 'warning', defaultTitle: { '/': 'WARNING', '/zh/': '注意' } }], ['container', { type: 'danger', defaultTitle: { '/': 'WARNING', '/zh/': '警告' } }], ['container', { type: 'details', before: info => `
${info ? `${info}` : ''}\n`, after: () => '
\n' }], ['smooth-scroll', enableSmoothScroll] ] } } ================================================ FILE: docs/.vuepress/theme/layouts/404.vue ================================================ ================================================ FILE: docs/.vuepress/theme/layouts/Layout.vue ================================================ ================================================ FILE: docs/.vuepress/theme/noopModule.js ================================================ export default {} ================================================ FILE: docs/.vuepress/theme/styles/arrow.styl ================================================ @require './config' .arrow display inline-block width 0 height 0 &.up border-left 4px solid transparent border-right 4px solid transparent border-bottom 6px solid $arrowBgColor &.down border-left 4px solid transparent border-right 4px solid transparent border-top 6px solid $arrowBgColor &.right border-top 4px solid transparent border-bottom 4px solid transparent border-left 6px solid $arrowBgColor &.left border-top 4px solid transparent border-bottom 4px solid transparent border-right 6px solid $arrowBgColor ================================================ FILE: docs/.vuepress/theme/styles/code.styl ================================================ {$contentClass} code color lighten($textColor, 20%) padding 0.25rem 0.5rem margin 0 font-size 0.85em background-color rgba(27,31,35,0.05) border-radius 3px .token &.deleted color #EC5975 &.inserted color $accentColor {$contentClass} pre, pre[class*="language-"] line-height 1.4 padding 1.25rem 1.5rem margin 0.85rem 0 background-color $codeBgColor border-radius 6px overflow auto code color #fff padding 0 background-color transparent border-radius 0 div[class*="language-"] position relative background-color $codeBgColor border-radius 6px .highlight-lines user-select none padding-top 1.3rem position absolute top 0 left 0 width 100% line-height 1.4 .highlighted background-color rgba(0, 0, 0, 66%) pre, pre[class*="language-"] background transparent position relative z-index 1 &::before position absolute z-index 3 top 0.8em right 1em font-size 0.75rem color rgba(255, 255, 255, 0.4) &:not(.line-numbers-mode) .line-numbers-wrapper display none &.line-numbers-mode .highlight-lines .highlighted position relative &:before content ' ' position absolute z-index 3 left 0 top 0 display block width $lineNumbersWrapperWidth height 100% background-color rgba(0, 0, 0, 66%) pre padding-left $lineNumbersWrapperWidth + 1 rem vertical-align middle .line-numbers-wrapper position absolute top 0 width $lineNumbersWrapperWidth text-align center color rgba(255, 255, 255, 0.3) padding 1.25rem 0 line-height 1.4 br user-select none .line-number position relative z-index 4 user-select none font-size 0.85em &::after content '' position absolute z-index 2 top 0 left 0 width $lineNumbersWrapperWidth height 100% border-radius 6px 0 0 6px border-right 1px solid rgba(0, 0, 0, 66%) background-color $codeBgColor for lang in $codeLang div{'[class~="language-' + lang + '"]'} &:before content ('' + lang) div[class~="language-javascript"] &:before content "js" div[class~="language-typescript"] &:before content "ts" div[class~="language-markup"] &:before content "html" div[class~="language-markdown"] &:before content "md" div[class~="language-json"]:before content "json" div[class~="language-ruby"]:before content "rb" div[class~="language-python"]:before content "py" div[class~="language-bash"]:before content "sh" div[class~="language-php"]:before content "php" @import '~prismjs/themes/prism-tomorrow.css' ================================================ FILE: docs/.vuepress/theme/styles/config.styl ================================================ $contentClass = '.theme-default-content' ================================================ FILE: docs/.vuepress/theme/styles/custom-blocks.styl ================================================ .custom-block .custom-block-title font-weight 600 margin-bottom -0.4rem &.tip, &.warning, &.danger padding .1rem 1.5rem border-left-width .5rem border-left-style solid margin 1rem 0 &.tip background-color #f3f5f7 border-color #42b983 &.warning background-color rgba(255,229,100,.3) border-color darken(#ffe564, 35%) color darken(#ffe564, 70%) .custom-block-title color darken(#ffe564, 50%) a color $textColor &.danger background-color #ffe6e6 border-color darken(red, 20%) color darken(red, 70%) .custom-block-title color darken(red, 40%) a color $textColor &.details display block position relative border-radius 2px margin 1.6em 0 padding 1.6em background-color #eee h4 margin-top 0 figure, p &:last-child margin-bottom 0 padding-bottom 0 summary outline none cursor pointer ================================================ FILE: docs/.vuepress/theme/styles/index.styl ================================================ @require './config' @require './code' @require './custom-blocks' @require './arrow' @require './wrapper' @require './toc' html, body padding 0 margin 0 background-color #fff body font-family -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif -webkit-font-smoothing antialiased -moz-osx-font-smoothing grayscale font-size 16px color $textColor #ad width: 125px; position: fixed; z-index: $z-header -1; right: 10px; top: 120px; padding: 10px; background-color: #fff; border-radius: 3px; font-size: 13px; text-align: center; img width: 100%; .page padding-left $sidebarWidth .navbar position fixed z-index 20 top 0 left 0 right 0 height $navbarHeight background-color #fff box-sizing border-box border-bottom 1px solid $borderColor .sidebar-mask position fixed z-index 9 top 0 left 0 width 100vw height 100vh display none .sidebar font-size 16px background-color #fff width $sidebarWidth position fixed z-index 10 margin 0 top $navbarHeight left 0 bottom 0 box-sizing border-box border-right 1px solid $borderColor overflow-y auto {$contentClass}:not(.custom) @extend $wrapper > *:first-child margin-top $navbarHeight a:hover text-decoration underline p.demo padding 1rem 1.5rem border 1px solid #ddd border-radius 4px img max-width 100% {$contentClass}.custom padding 0 margin 0 img max-width 100% a font-weight 500 color $accentColor text-decoration none p a code font-weight 400 color $accentColor kbd background #eee border solid 0.15rem #ddd border-bottom solid 0.25rem #ddd border-radius 0.15rem padding 0 0.15em blockquote font-size 1rem color #999; border-left .2rem solid #dfe2e5 margin 1rem 0 padding .25rem 0 .25rem 1rem & > p margin 0 ul, ol padding-left 1.2em strong font-weight 600 h1, h2, h3, h4, h5, h6 font-weight 600 line-height 1.25 {$contentClass}:not(.custom) > & margin-top (0.5rem - $navbarHeight) padding-top ($navbarHeight + 1rem) margin-bottom 0 &:first-child margin-top -1.5rem margin-bottom 1rem + p, + pre, + .custom-block margin-top 2rem &:hover .header-anchor opacity: 1 h1 font-size 2.2rem h2 font-size 1.65rem padding-bottom .3rem border-bottom 1px solid $borderColor h3 font-size 1.35rem a.header-anchor font-size 0.85em float left margin-left -0.87em padding-right 0.23em margin-top 0.125em opacity 0 &:hover text-decoration none code, kbd, .line-number font-family source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace p, ul, ol line-height 1.7 hr border 0 border-top 1px solid $borderColor table border-collapse collapse margin 1rem 0 display: block overflow-x: auto tr border-top 1px solid #dfe2e5 &:nth-child(2n) background-color #f6f8fa th, td border 1px solid #dfe2e5 padding .6em 1em .theme-container &.sidebar-open .sidebar-mask display: block &.no-navbar {$contentClass}:not(.custom) > h1, h2, h3, h4, h5, h6 margin-top 1.5rem padding-top 0 .sidebar top 0 @media (min-width: ($MQMobile + 1px)) .theme-container.no-sidebar .sidebar display none .page padding-left 0 @require 'mobile.styl' ================================================ FILE: docs/.vuepress/theme/styles/mobile.styl ================================================ @require './config' $mobileSidebarWidth = $sidebarWidth * 0.82 // narrow desktop / iPad @media (max-width: $MQNarrow) .sidebar font-size 15px width $mobileSidebarWidth .page padding-left $mobileSidebarWidth // wide mobile @media (max-width: $MQMobile) .sidebar top 0 padding-top $navbarHeight transform translateX(-100%) transition transform .2s ease .page padding-left 0 .theme-container &.sidebar-open .sidebar transform translateX(0) &.no-navbar .sidebar padding-top: 0 #ad display: none // narrow mobile @media (max-width: $MQMobileNarrow) h1 font-size 1.9rem {$contentClass} div[class*="language-"] margin 0.85rem -1.5rem border-radius 0 ================================================ FILE: docs/.vuepress/theme/styles/toc.styl ================================================ .table-of-contents .badge vertical-align middle ================================================ FILE: docs/.vuepress/theme/styles/wrapper.styl ================================================ $wrapper max-width $contentWidth margin 0 auto padding 2rem 2.5rem @media (max-width: $MQNarrow) padding 2rem @media (max-width: $MQMobileNarrow) padding 1.5rem ================================================ FILE: docs/.vuepress/theme/util/index.js ================================================ export const hashRE = /#.*$/ export const extRE = /\.(md|html)$/ export const endingSlashRE = /\/$/ export const outboundRE = /^[a-z]+:/i export function normalize (path) { return decodeURI(path) .replace(hashRE, '') .replace(extRE, '') } export function getHash (path) { const match = path.match(hashRE) if (match) { return match[0] } } export function isExternal (path) { return outboundRE.test(path) } export function isMailto (path) { return /^mailto:/.test(path) } export function isTel (path) { return /^tel:/.test(path) } export function ensureExt (path) { if (isExternal(path)) { return path } const hashMatch = path.match(hashRE) const hash = hashMatch ? hashMatch[0] : '' const normalized = normalize(path) if (endingSlashRE.test(normalized)) { return path } return normalized + '.html' + hash } export function isActive (route, path) { const routeHash = decodeURIComponent(route.hash) const linkHash = getHash(path) if (linkHash && routeHash !== linkHash) { return false } const routePath = normalize(route.path) const pagePath = normalize(path) return routePath === pagePath } export function resolvePage (pages, rawPath, base) { if (isExternal(rawPath)) { return { type: 'external', path: rawPath } } if (base) { rawPath = resolvePath(rawPath, base) } const path = normalize(rawPath) for (let i = 0; i < pages.length; i++) { if (normalize(pages[i].regularPath) === path) { return Object.assign({}, pages[i], { type: 'page', path: ensureExt(pages[i].path) }) } } console.error(`[vuepress] No matching page found for sidebar item "${rawPath}"`) return {} } function resolvePath (relative, base, append) { const firstChar = relative.charAt(0) if (firstChar === '/') { return relative } if (firstChar === '?' || firstChar === '#') { return base + relative } const stack = base.split('/') // remove trailing segment if: // - not appending // - appending to trailing slash (last segment is empty) if (!append || !stack[stack.length - 1]) { stack.pop() } // resolve relative path const segments = relative.replace(/^\//, '').split('/') for (let i = 0; i < segments.length; i++) { const segment = segments[i] if (segment === '..') { stack.pop() } else if (segment !== '.') { stack.push(segment) } } // ensure leading slash if (stack[0] !== '') { stack.unshift('') } return stack.join('/') } /** * @param { Page } page * @param { string } regularPath * @param { SiteData } site * @param { string } localePath * @returns { SidebarGroup } */ export function resolveSidebarItems (page, regularPath, site, localePath) { const { pages, themeConfig } = site const localeConfig = localePath && themeConfig.locales ? themeConfig.locales[localePath] || themeConfig : themeConfig const pageSidebarConfig = page.frontmatter.sidebar || localeConfig.sidebar || themeConfig.sidebar if (pageSidebarConfig === 'auto') { return resolveHeaders(page) } const sidebarConfig = localeConfig.sidebar || themeConfig.sidebar if (!sidebarConfig) { return [] } else { const { base, config } = resolveMatchingConfig(regularPath, sidebarConfig) return config ? config.map(item => resolveItem(item, pages, base)) : [] } } /** * @param { Page } page * @returns { SidebarGroup } */ function resolveHeaders (page) { const headers = groupHeaders(page.headers || []) return [{ type: 'group', collapsable: false, title: page.title, path: null, children: headers.map(h => ({ type: 'auto', title: h.title, basePath: page.path, path: page.path + '#' + h.slug, children: h.children || [] })) }] } export function groupHeaders (headers) { // group h3s under h2 headers = headers.map(h => Object.assign({}, h)) let lastH2 headers.forEach(h => { if (h.level === 2) { lastH2 = h } else if (lastH2) { (lastH2.children || (lastH2.children = [])).push(h) } }) return headers.filter(h => h.level === 2) } export function resolveNavLinkItem (linkItem) { return Object.assign(linkItem, { type: linkItem.items && linkItem.items.length ? 'links' : 'link' }) } /** * @param { Route } route * @param { Array | Array | [link: string]: SidebarConfig } config * @returns { base: string, config: SidebarConfig } */ export function resolveMatchingConfig (regularPath, config) { if (Array.isArray(config)) { return { base: '/', config: config } } for (const base in config) { if (ensureEndingSlash(regularPath).indexOf(encodeURI(base)) === 0) { return { base, config: config[base] } } } return {} } function ensureEndingSlash (path) { return /(\.html|\/)$/.test(path) ? path : path + '/' } function resolveItem (item, pages, base, groupDepth = 1) { if (typeof item === 'string') { return resolvePage(pages, item, base) } else if (Array.isArray(item)) { return Object.assign(resolvePage(pages, item[0], base), { title: item[1] }) } else { if (groupDepth > 3) { console.error( '[vuepress] detected a too deep nested sidebar group.' ) } const children = item.children || [] if (children.length === 0 && item.path) { return Object.assign(resolvePage(pages, item.path, base), { title: item.title }) } return { type: 'group', path: item.path, title: item.title, sidebarDepth: item.sidebarDepth, children: children.map(child => resolveItem(child, pages, base, groupDepth + 1)), collapsable: item.collapsable !== false } } } ================================================ FILE: docs/JavaScript/== _===.md ================================================ # 面试官:== 和 ===区别,分别在什么情况使用 ![](https://static.vue-js.com/51b208f0-68df-11eb-85f6-6fac77c0c9b3.png) ## 一、等于操作符 等于操作符用两个等于号( == )表示,如果操作数相等,则会返回 `true` 前面文章,我们提到在`JavaScript`中存在隐式转换。等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等 遵循以下规则: 如果任一操作数是布尔值,则将其转换为数值再比较是否相等 ```js let result1 = (true == 1); // true ``` 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等 ```js let result1 = ("55" == 55); // true ``` 如果一个操作数是对象,另一个操作数不是,则调用对象的 `valueOf() `方法取得其原始值,再根据前面的规则进行比较 ```js let obj = {valueOf:function(){return 1}} let result1 = (obj == 1); // true ``` `null `和` undefined `相等 ```js let result1 = (null == undefined ); // true ``` 如果有任一操作数是 `NaN` ,则相等操作符返回 `false` ```js let result1 = (NaN == NaN ); // false ``` 如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回` true ` ``` let obj1 = {name:"xxx"} let obj2 = {name:"xxx"} let result1 = (obj1 == obj2 ); // false ``` 下面进一步做个小结: - 两个都为简单类型,字符串和布尔值都会转换成数值,再比较 - 简单类型与引用类型比较,对象转化成其原始类型的值,再比较 - 两个都为引用类型,则比较它们是否指向同一个对象 - null 和 undefined 相等 - 存在 NaN 则返回 false ## 二、全等操作符 全等操作符由 3 个等于号( === )表示,只有两个操作数在不转换的前提下相等才返回 `true`。即类型相同,值也需相同 ```js let result1 = ("55" === 55); // false,不相等,因为数据类型不同 let result2 = (55 === 55); // true,相等,因为数据类型相同值也相同 ``` `undefined` 和 `null` 与自身严格相等 ```js let result1 = (null === null) //true let result2 = (undefined === undefined) //true ``` ## 三、区别 相等操作符(==)会做类型转换,再进行值的比较,全等运算符不会做类型转换 ```js let result1 = ("55" === 55); // false,不相等,因为数据类型不同 let result2 = (55 === 55); // true,相等,因为数据类型相同值也相同 ``` `null` 和 `undefined` 比较,相等操作符(==)为`true`,全等为`false` ```js let result1 = (null == undefined ); // true let result2 = (null === undefined); // false ``` ### 小结 相等运算符隐藏的类型转换,会带来一些违反直觉的结果 ```js '' == '0' // false 0 == '' // true 0 == '0' // true false == 'false' // false false == '0' // true false == undefined // false false == null // false null == undefined // true ' \t\r\n' == 0 // true ``` 但在比较`null`的情况的时候,我们一般使用相等操作符`==` ```js const obj = {}; if(obj.x == null){ console.log("1"); //执行 } ``` 等同于下面写法 ```js if(obj.x === null || obj.x === undefined) { ... } ``` 使用相等操作符(==)的写法明显更加简洁了 所以,除了在比较对象属性为`null`或者`undefined`的情况下,我们可以使用相等操作符(==),其他情况建议一律使用全等操作符(===) ================================================ FILE: docs/JavaScript/BOM.md ================================================ # 面试官:说说你对BOM的理解,常见的BOM对象你了解哪些? ![](https://static.vue-js.com/3e191c40-8089-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 `BOM` (Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象 其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退,前进,刷新,浏览器的窗口发生变化,滚动条的滚动,以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率 浏览器的全部内容可以看成`DOM`,整个浏览器可以看成`BOM`。区别如下: ![](https://static.vue-js.com/482f33e0-8089-11eb-85f6-6fac77c0c9b3.png) ## 二、window `Bom`的核心对象是`window`,它表示浏览器的一个实例 在浏览器中,`window`对象有双重角色,即是浏览器窗口的一个接口,又是全局对象 因此所有在全局作用域中声明的变量、函数都会变成`window`对象的属性和方法 ```js var name = 'js每日一题'; function lookName(){ alert(this.name); } console.log(window.name); //js每日一题 lookName(); //js每日一题 window.lookName(); //js每日一题 ``` 关于窗口控制方法如下: - `moveBy(x,y)`:从当前位置水平移动窗体x个像素,垂直移动窗体y个像素,x为负数,将向左移动窗体,y为负数,将向上移动窗体 - `moveTo(x,y)`:移动窗体左上角到相对于屏幕左上角的(x,y)点 - `resizeBy(w,h)`:相对窗体当前的大小,宽度调整w个像素,高度调整h个像素。如果参数为负值,将缩小窗体,反之扩大窗体 - `resizeTo(w,h)`:把窗体宽度调整为w个像素,高度调整为h个像素 - `scrollTo(x,y)`:如果有滚动条,将横向滚动条移动到相对于窗体宽度为x个像素的位置,将纵向滚动条移动到相对于窗体高度为y个像素的位置 - `scrollBy(x,y)`: 如果有滚动条,将横向滚动条向左移动x个像素,将纵向滚动条向下移动y个像素 `window.open()` 既可以导航到一个特定的`url`,也可以打开一个新的浏览器窗口 如果 `window.open()` 传递了第二个参数,且该参数是已有窗口或者框架的名称,那么就会在目标窗口加载第一个参数指定的URL ```js window.open('htttp://www.vue3js.cn','topFrame') ==> < a href=" " target="topFrame"> ``` `window.open()` 会返回新窗口的引用,也就是新窗口的 `window` 对象 ```js const myWin = window.open('http://www.vue3js.cn','myWin') ``` `window.close()` 仅用于通过 `window.open()` 打开的窗口 新创建的 `window` 对象有一个 `opener` 属性,该属性指向打开他的原始窗口对象 ## 三、location `url`地址如下: ```js http://foouser:barpassword@www.wrox.com:80/WileyCDA/?q=javascript#contents ``` `location`属性描述如下: | 属性名 | 例子 | 说明 | | -------- | ------------------------------------------------------ | ----------------------------------- | | hash | "#contents" | utl中#后面的字符,没有则返回空串 | | host | www.wrox.com:80 | 服务器名称和端口号 | | hostname | www.wrox.com | 域名,不带端口号 | | href | http://www.wrox.com:80/WileyCDA/?q=javascript#contents | 完整url | | pathname | "/WileyCDA/" | 服务器下面的文件路径 | | port | 80 | url的端口号,没有则为空 | | protocol | http: | 使用的协议 | | search | ?q=javascript | url的查询字符串,通常为?后面的内容 | 除了 `hash `之外,只要修改` location `的一个属性,就会导致页面重新加载新` URL` `location.reload()`,此方法可以重新刷新当前页面。这个方法会根据最有效的方式刷新页面,如果页面自上一次请求以来没有改变过,页面就会从浏览器缓存中重新加载 如果要强制从服务器中重新加载,传递一个参数`true`即可 ## 四、navigator `navigator` 对象主要用来获取浏览器的属性,区分浏览器类型。属性较多,且兼容性比较复杂 下表列出了`navigator`对象接口定义的属性和方法: ![](https://static.vue-js.com/6797ab40-8089-11eb-ab90-d9ae814b240d.png) ![](https://static.vue-js.com/74096620-8089-11eb-ab90-d9ae814b240d.png) ## 五、screen 保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度 ![](https://static.vue-js.com/7d6b21e0-8089-11eb-85f6-6fac77c0c9b3.png) ## 六、history `history`对象主要用来操作浏览器`URL`的历史记录,可以通过参数向前,向后,或者向指定`URL`跳转 常用的属性如下: - `history.go()` 接收一个整数数字或者字符串参数:向最近的一个记录中包含指定字符串的页面跳转, ```js history.go('maixaofei.com') ``` 当参数为整数数字的时候,正数表示向前跳转指定的页面,负数为向后跳转指定的页面 ```js history.go(3) //向前跳转三个记录 history.go(-1) //向后跳转一个记录 ``` - `history.forward()`:向前跳转一个页面 - `history.back()`:向后跳转一个页面 - `history.length`:获取历史记录数 ================================================ FILE: docs/JavaScript/Dom.md ================================================ # 面试官:DOM常见的操作有哪些? ![](https://static.vue-js.com/a89c99a0-7fdc-11eb-ab90-d9ae814b240d.png) ## 一、DOM 文档对象模型 (DOM) 是 `HTML` 和 `XML` 文档的编程接口 它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容 任何 `HTML `或` XML `文档都可以用 `DOM `表示为一个由节点构成的层级结构 节点分很多类型,每种类型对应着文档中不同的信息和(或)标记,也都有自己不同的特性、数据和方法,而且与其他类型有某种关系,如下所示: ```html Page

Hello World!

``` `DOM`像原子包含着亚原子微粒那样,也有很多类型的`DOM`节点包含着其他类型的节点。接下来我们先看看其中的三种: ```html

content

``` 上述结构中,`div`、`p`就是元素节点,`content`就是文本节点,`title`就是属性节点 ## 二、操作 日常前端开发,我们都离不开`DOM`操作 在以前,我们使用`Jquery`,`zepto`等库来操作`DOM`,之后在`vue`,`Angular`,`React`等框架出现后,我们通过操作数据来控制`DOM`(绝大多数时候),越来越少的去直接操作`DOM` 但这并不代表原生操作不重要。相反,`DOM`操作才能有助于我们理解框架深层的内容 下面就来分析`DOM`常见的操作,主要分为: - 创建节点 - 查询节点 - 更新节点 - 添加节点 - 删除节点 ### 创建节点 #### createElement 创建新元素,接受一个参数,即要创建元素的标签名 ```js const divEl = document.createElement("div"); ``` #### createTextNode 创建一个文本节点 ```js const textEl = document.createTextNode("content"); ``` #### createDocumentFragment 用来创建一个文档碎片,它表示一种轻量级的文档,主要是用来存储临时节点,然后把文档碎片的内容一次性添加到`DOM`中 ```js const fragment = document.createDocumentFragment(); ``` 当请求把一个`DocumentFragment` 节点插入文档树时,插入的不是 `DocumentFragment `自身,而是它的所有子孙节点 #### createAttribute 创建属性节点,可以是自定义属性 ```js const dataAttribute = document.createAttribute('custom'); consle.log(dataAttribute); ``` ### 获取节点 #### querySelector 传入任何有效的` css` 选择器,即可选中单个 `DOM `元素(首个): ```js document.querySelector('.element') document.querySelector('#element') document.querySelector('div') document.querySelector('[name="username"]') document.querySelector('div + p > span') ``` 如果页面上没有指定的元素时,返回 `null` #### querySelectorAll 返回一个包含节点子树内所有与之相匹配的`Element`节点列表,如果没有相匹配的,则返回一个空节点列表 ```js const notLive = document.querySelectorAll("p"); ``` 需要注意的是,该方法返回的是一个 `NodeList `的静态实例,它是一个静态的“快照”,而非“实时”的查询 关于获取`DOM`元素的方法还有如下,就不一一述说 ```js document.getElementById('id属性值');返回拥有指定id的对象的引用 document.getElementsByClassName('class属性值');返回拥有指定class的对象集合 document.getElementsByTagName('标签名');返回拥有指定标签名的对象集合 document.getElementsByName('name属性值'); 返回拥有指定名称的对象结合 document/element.querySelector('CSS选择器'); 仅返回第一个匹配的元素 document/element.querySelectorAll('CSS选择器'); 返回所有匹配的元素 document.documentElement; 获取页面中的HTML标签 document.body; 获取页面中的BODY标签 document.all['']; 获取页面中的所有元素节点的对象集合型 ``` 除此之外,每个`DOM`元素还有`parentNode`、`childNodes`、`firstChild`、`lastChild`、`nextSibling`、`previousSibling`属性,关系图如下图所示 ![](https://static.vue-js.com/c100f450-7fdc-11eb-ab90-d9ae814b240d.png) ### 更新节点 #### innerHTML 不但可以修改一个`DOM`节点的文本内容,还可以直接通过`HTML`片段修改`DOM`节点内部的子树 ```js // 获取

...

var p = document.getElementById('p'); // 设置文本为abc: p.innerHTML = 'ABC'; //

ABC

// 设置HTML: p.innerHTML = 'ABC RED XYZ'; //

...

的内部结构已修改 ``` #### innerText、textContent 自动对字符串进行`HTML`编码,保证无法设置任何`HTML`标签 ``` // 获取

...

var p = document.getElementById('p-id'); // 设置文本: p.innerText = ''; // HTML被自动编码,无法设置一个`,拼接到 HTML 中返回给浏览器。形成了如下的 HTML: ```html ">
您搜索的关键词是:">
``` 浏览器无法分辨出 `` 是恶意代码,因而将其执行,试想一下,如果是获取`cookie`发送对黑客服务器呢? 根据攻击的来源,`XSS`攻击可以分成: - 存储型 - 反射型 - DOM 型 ### 存储型 存储型 XSS 的攻击步骤: 1. 攻击者将恶意代码提交到目标网站的数据库中 2. 用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器 3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行 4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作 这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等 ### 反射型 XSS 反射型 XSS 的攻击步骤: 1. 攻击者构造出特殊的 URL,其中包含恶意代码 2. 用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器 3. 用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行 4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作 反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。 反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。 由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。 POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见 ### DOM 型 XSS DOM 型 XSS 的攻击步骤: 1. 攻击者构造出特殊的 URL,其中包含恶意代码 2. 用户打开带有恶意代码的 URL 3. 用户浏览器接收到响应后解析执行,前端 JavaScript 取出 URL 中的恶意代码并执行 4. 恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作 DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞 ### XSS的预防 通过前面介绍,看到`XSS`攻击的两大要素: - 攻击者提交而恶意代码 - 浏览器执行恶意代码 针对第一个要素,我们在用户输入的过程中,过滤掉用户输入的恶劣代码,然后提交给后端,但是如果攻击者绕开前端请求,直接构造请求就不能预防了 而如果在后端写入数据库前,对输入进行过滤,然后把内容给前端,但是这个内容在不同地方就会有不同显示 例如: 一个正常的用户输入了 `5 < 7` 这个内容,在写入数据库前,被转义,变成了 `5 < 7` 在客户端中,一旦经过了 `escapeHTML()`,客户端显示的内容就变成了乱码( `5 < 7` ) 在前端中,不同的位置所需的编码也不同。 - 当 `5 < 7` 作为 HTML 拼接页面时,可以正常显示: ```html
5 < 7
``` - 当 `5 < 7` 通过 Ajax 返回,然后赋值给 JavaScript 的变量时,前端得到的字符串就是转义后的字符。这个内容不能直接用于 Vue 等模板的展示,也不能直接用于内容长度计算。不能用于标题、alert 等 可以看到,过滤并非可靠的,下面就要通过防止浏览器执行恶意代码: 在使用 `.innerHTML`、`.outerHTML`、`document.write()` 时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 `.textContent`、`.setAttribute()` 等 如果用 `Vue/React` 技术栈,并且不使用 `v-html`/`dangerouslySetInnerHTML` 功能,就在前端 `render` 阶段避免 `innerHTML`、`outerHTML` 的 XSS 隐患 DOM 中的内联事件监听器,如 `location`、`onclick`、`onerror`、`onload`、`onmouseover` 等,`` 标签的 `href` 属性,JavaScript 的 `eval()`、`setTimeout()`、`setInterval()` 等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免 ```js < a href=" ">1 ``` 访问该页面后,表单会自动提交,相当于模拟用户完成了一次`POST`操作 还有一种为使用`a`标签的,需要用户点击链接才会触发 访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作 ```html < a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank"> 重磅消息!! ``` ### CSRF的特点 - 攻击一般发起在第三方网站,而不是被攻击的网站。被攻击的网站无法防止攻击发生 - 攻击利用受害者在被攻击网站的登录凭证,冒充受害者提交操作;而不是直接窃取数据 - 整个过程攻击者并不能获取到受害者的登录凭证,仅仅是“冒用” - 跨站请求可以用各种方式:图片URL、超链接、CORS、Form提交等等。部分请求方式可以直接嵌入在第三方论坛、文章中,难以进行追踪 ### CSRF的预防 CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性 防止`csrf`常用方案如下: - 阻止不明外域的访问 - 同源检测 - Samesite Cookie - 提交时要求附加本域才能获取的信息 - CSRF Token - 双重Cookie验证 这里主要讲讲`token`这种形式,流程如下: - 用户打开页面的时候,服务器需要给这个用户生成一个Token - 对于GET请求,Token将附在请求地址之后。对于 POST 请求来说,要在 form 的最后加上 ```html ``` - 当用户从客户端得到了Token,再次提交给服务器的时候,服务器需要判断Token的有效性 ## 四、SQL注入 Sql 注入攻击,是通过将恶意的 `Sql `查询或添加语句插入到应用的输入参数中,再在后台 `Sql `服务器上解析执行进行的攻击 ![](https://static.vue-js.com/ead52fa0-8d1d-11eb-85f6-6fac77c0c9b3.png) 流程如下所示: - 找出SQL漏洞的注入点 - 判断数据库的类型以及版本 - 猜解用户名和密码 - 利用工具查找Web后台管理入口 - 入侵和破坏 预防方式如下: - 严格检查输入变量的类型和格式 - 过滤和转义特殊字符 - 对访问数据库的Web应用程序采用Web应用防火墙 上述只是列举了常见的`web`攻击方式,实际开发过程中还会遇到很多安全问题,对于这些问题, 切记不可忽视 ## 参考文献 - https://tech.meituan.com/2018/09/27/fe-security.html - https://developer.mozilla.org/zh-CN/docs/learn/Server-side/First_steps/Website_security ================================================ FILE: docs/JavaScript/single_sign.md ================================================ # 面试官:什么是单点登录?如何实现? ![](https://static.vue-js.com/8a25a760-8c83-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一 SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统 SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过`passport`,子系统本身将不参与登录操作 当一个系统成功登录以后,`passport`将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被`passport`授权以后,会建立一个局部会话,在一定时间内可以无需再次向`passport`发起认证 ![](https://static.vue-js.com/2b9b0e70-8c4b-11eb-85f6-6fac77c0c9b3.png) 上图有四个系统,分别是`Application1`、`Application2`、`Application3`、和`SSO`,当`Application1`、`Application2`、`Application3`需要登录时,将跳到`SSO`系统,`SSO`系统完成登录,其他的应用系统也就随之登录了 #### 举个例子 淘宝、天猫都属于阿里旗下,当用户登录淘宝后,再打开天猫,系统便自动帮用户登录了天猫,这种现象就属于单点登录 ## 二、如何实现 ### 同域名下的单点登录 `cookie`的`domain`属性设置为当前域的父域,并且父域的`cookie`会被子域所共享。`path`属性默认为`web`应用的上下文路径 利用 `Cookie` 的这个特点,没错,我们只需要将` Cookie `的` domain`属性设置为父域的域名(主域名),同时将 `Cookie `的` path `属性设置为根路径,将 `Session ID`(或 `Token`)保存到父域中。这样所有的子域应用就都可以访问到这个` Cookie ` 不过这要求应用系统的域名需建立在一个共同的主域名之下,如 `tieba.baidu.com` 和 `map.baidu.com`,它们都建立在 `baidu.com `这个主域名之下,那么它们就可以通过这种方式来实现单点登录 ### 不同域名下的单点登录(一) 如果是不同域的情况下,`Cookie`是不共享的,这里我们可以部署一个认证中心,用于专门处理登录请求的独立的 `Web `服务 用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 `token` 写入 `Cookie`(注意这个 `Cookie `是认证中心的,应用系统是访问不到的) 应用系统检查当前请求有没有 `Token`,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心 由于这个操作会将认证中心的 `Cookie` 自动带过去,因此,认证中心能够根据 `Cookie` 知道用户是否已经登录过了 如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录 如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 `URL `,并在跳转前生成一个 `Token`,拼接在目标` URL` 的后面,回传给目标应用系统 应用系统拿到 `Token `之后,还需要向认证中心确认下 `Token` 的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 `Token `写入` Cookie`,然后给本次访问放行。(注意这个 `Cookie` 是当前应用系统的)当用户再次访问当前应用系统时,就会自动带上这个 `Token`,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了 此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法 ### 不同域名下的单点登录(二) 可以选择将 `Session ID` (或 `Token` )保存到浏览器的 `LocalStorage` 中,让前端在每次向后端发送请求时,主动将` LocalStorage `的数据传递给服务端 这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 `Session ID `(或 `Token `)放在响应体中传递给前端 单点登录完全可以在前端实现。前端拿到 `Session ID `(或 `Token` )后,除了将它写入自己的 `LocalStorage` 中之外,还可以通过特殊手段将它写入多个其他域下的 `LocalStorage` 中 关键代码如下: ```js // 获取 token var token = result.data.token; // 动态创建一个不可见的iframe,在iframe中加载一个跨域HTML var iframe = document.createElement("iframe"); iframe.src = "http://app1.com/localstorage.html"; document.body.append(iframe); // 使用postMessage()方法将token传递给iframe setTimeout(function () { iframe.contentWindow.postMessage(token, "http://app1.com"); }, 4000); setTimeout(function () { iframe.remove(); }, 6000); // 在这个iframe所加载的HTML中绑定一个事件监听器,当事件被触发时,把接收到的token数据写入localStorage window.addEventListener('message', function (event) { localStorage.setItem('token', event.data) }, false); ``` 前端通过 `iframe`+`postMessage()` 方式,将同一份 `Token` 写入到了多个域下的 `LocalStorage` 中,前端每次在向后端发送请求之前,都会主动从 `LocalStorage` 中读取` Token `并在请求中携带,这样就实现了同一份` Token` 被多个域所共享 此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域 ## 三、流程 单点登录的流程图如下所示: ![](https://static.vue-js.com/2422bc40-8c84-11eb-ab90-d9ae814b240d.png) - 用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数 - sso认证中心发现用户未登录,将用户引导至登录页面 - 用户输入用户名密码提交登录申请 - sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌 - sso认证中心带着令牌跳转会最初的请求地址(系统1) - 系统1拿到令牌,去sso认证中心校验令牌是否有效 - sso认证中心校验令牌,返回有效,注册系统1 - 系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源 - 用户访问系统2的受保护资源 - 系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数 - sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌 - 系统2拿到令牌,去sso认证中心校验令牌是否有效 - sso认证中心校验令牌,返回有效,注册系统2 - 系统2使用该令牌创建与用户的局部会话,返回受保护资源 用户登录成功之后,会与`sso`认证中心及各个子系统建立会话,用户与`sso`认证中心建立的会话称为全局会话 用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过`sso`认证中心 全局会话与局部会话有如下约束关系: - 局部会话存在,全局会话一定存在 - 全局会话存在,局部会话不一定存在 - 全局会话销毁,局部会话必须销毁 ## 参考文献 - https://blog.csdn.net/weixin_36380516/article/details/109006828 - https://baike.baidu.com/item/%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95 - https://juejin.cn/post/6844903664985866253 ================================================ FILE: docs/JavaScript/string_api.md ================================================ # 面试官:JavaScript字符串的常用方法有哪些? ![](https://static.vue-js.com/ceb6ebc0-65c1-11eb-ab90-d9ae814b240d.png) ## 一、操作方法 我们也可将字符串常用的操作方法归纳为增、删、改、查,需要知道字符串的特点是一旦创建了,就不可变 ### 增 这里增的意思并不是说直接增添内容,而是创建字符串的一个副本,再进行操作 除了常用`+`以及`${}`进行字符串拼接之外,还可通过`concat` #### concat 用于将一个或多个字符串拼接成一个新字符串 ```js let stringValue = "hello "; let result = stringValue.concat("world"); console.log(result); // "hello world" console.log(stringValue); // "hello" ``` ### 删 这里的删的意思并不是说删除原字符串的内容,而是创建字符串的一个副本,再进行操作 常见的有: - slice() - substr() - substring() 这三个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。 ```js let stringValue = "hello world"; console.log(stringValue.slice(3)); // "lo world" console.log(stringValue.substring(3)); // "lo world" console.log(stringValue.substr(3)); // "lo world" console.log(stringValue.slice(3, 7)); // "lo w" console.log(stringValue.substring(3,7)); // "lo w" console.log(stringValue.substr(3, 7)); // "lo worl" ``` ### 改 这里改的意思也不是改变原字符串,而是创建字符串的一个副本,再进行操作 常见的有: - trim()、trimLeft()、trimRight() - repeat() - padStart()、padEnd() - toLowerCase()、 toUpperCase() #### trim()、trimLeft()、trimRight() 删除前、后或前后所有空格符,再返回新的字符串 ```js let stringValue = " hello world "; let trimmedStringValue = stringValue.trim(); console.log(stringValue); // " hello world " console.log(trimmedStringValue); // "hello world" ``` #### repeat() 接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果 ```js let stringValue = "na "; let copyResult = stringValue.repeat(2) // na na ``` #### padEnd() 复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件 ```js let stringValue = "foo"; console.log(stringValue.padStart(6)); // " foo" console.log(stringValue.padStart(9, ".")); // "......foo" ``` ### toLowerCase()、 toUpperCase() 大小写转化 ```js let stringValue = "hello world"; console.log(stringValue.toUpperCase()); // "HELLO WORLD" console.log(stringValue.toLowerCase()); // "hello world" ``` ### 查 除了通过索引的方式获取字符串的值,还可通过: - chatAt() - indexOf() - startWith() - includes() #### charAt() 返回给定索引位置的字符,由传给方法的整数参数指定 ```js let message = "abcde"; console.log(message.charAt(2)); // "c" ``` #### indexOf() 从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 ) ```js let stringValue = "hello world"; console.log(stringValue.indexOf("o")); // 4 ``` #### startWith()、includes() 从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值 ```js let message = "foobarbaz"; console.log(message.startsWith("foo")); // true console.log(message.startsWith("bar")); // false console.log(message.includes("bar")); // true console.log(message.includes("qux")); // false ``` ## 二、转换方法 ### split 把字符串按照指定的分割符,拆分成数组中的每一项 ```js let str = "12+23+34" let arr = str.split("+") // [12,23,34] ``` ## 三、模板匹配方法 针对正则表达式,字符串设计了几个方法: - match() - search() - replace() ### match() 接收一个参数,可以是一个正则表达式字符串,也可以是一个` RegExp `对象,返回数组 ```js let text = "cat, bat, sat, fat"; let pattern = /.at/; let matches = text.match(pattern); console.log(matches[0]); // "cat" ``` ### search() 接收一个参数,可以是一个正则表达式字符串,也可以是一个` RegExp `对象,找到则返回匹配索引,否则返回 -1 ```js let text = "cat, bat, sat, fat"; let pos = text.search(/at/); console.log(pos); // 1 ``` ### replace() 接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数) ```js let text = "cat, bat, sat, fat"; let result = text.replace("at", "ond"); console.log(result); // "cond, bat, sat, fat" ``` ================================================ FILE: docs/JavaScript/tail_recursion.md ================================================ # 面试官:举例说明你对尾递归的理解,有哪些应用场景 ![](https://static.vue-js.com/74db8fe0-815d-11eb-85f6-6fac77c0c9b3.png) ## 一、递归 递归(英语:Recursion) 在数学与计算机科学中,是指在函数的定义中使用函数自身的方法 在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数 其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解 一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回 下面实现一个函数 `pow(x, n)`,它可以计算 `x` 的 `n` 次方 使用迭代的方式,如下: ```js function pow(x, n) { let result = 1; // 再循环中,用 x 乘以 result n 次 for (let i = 0; i < n; i++) { result *= x; } return result; } ``` 使用递归的方式,如下: ```js function pow(x, n) { if (n == 1) { return x; } else { return x * pow(x, n - 1); } } ``` `pow(x, n)` 被调用时,执行分为两个分支: ```js if n==1 = x / pow(x, n) = \ else = x * pow(x, n - 1) ``` 也就是说`pow` 递归地调用自身 直到 `n == 1` ![](https://static.vue-js.com/8002c960-815d-11eb-ab90-d9ae814b240d.png) 为了计算 `pow(2, 4)`,递归变体经过了下面几个步骤: 1. `pow(2, 4) = 2 * pow(2, 3)` 2. `pow(2, 3) = 2 * pow(2, 2)` 3. `pow(2, 2) = 2 * pow(2, 1)` 4. `pow(2, 1) = 2` 因此,递归将函数调用简化为一个更简单的函数调用,然后再将其简化为一个更简单的函数,以此类推,直到结果 ## 二、尾递归 尾递归,即在函数尾位置调用自身(或是一个尾调用本身的其他函数等等)。尾递归也是递归的一种特殊情形。尾递归是一种特殊的尾调用,即在尾部直接调用自身的递归函数 尾递归在普通尾调用的基础上,多出了2个特征: - 在尾部调用的是函数自身 - 可通过优化,使得计算仅占用常量栈空间 在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,递归次数过多容易造成栈溢出 这时候,我们就可以使用尾递归,即一个函数中所有递归形式的调用都出现在函数的末尾,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误 实现一下阶乘,如果用普通的递归,如下: ```js function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } factorial(5) // 120 ``` 如果`n`等于5,这个方法要执行5次,才返回最终的计算表达式,这样每次都要保存这个方法,就容易造成栈溢出,复杂度为`O(n)` 如果我们使用尾递归,则如下: ```js function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120 ``` 可以看到,每一次返回的就是一个新的函数,不带上一个函数的参数,也就不需要储存上一个函数了。尾递归只需要保存一个调用栈,复杂度 O(1) ## 二、应用场景 数组求和 ```js function sumArray(arr, total) { if(arr.length === 1) { return total } return sum(arr, total + arr.pop()) } ``` 使用尾递归优化求斐波那契数列 ```js function factorial2 (n, start = 1, total = 1) { if(n <= 2){ return total } return factorial2 (n -1, total, total + start) } ``` 数组扁平化 ```js let a = [1,2,3, [1,2,3, [1,2,3]]] // 变成 let a = [1,2,3,1,2,3,1,2,3] // 具体实现 function flat(arr = [], result = []) { arr.forEach(v => { if(Array.isArray(v)) { result = result.concat(flat(v, [])) }else { result.push(v) } }) return result } ``` 数组对象格式化 ```js let obj = { a: '1', b: { c: '2', D: { E: '3' } } } // 转化为如下: let obj = { a: '1', b: { c: '2', d: { e: '3' } } } // 代码实现 function keysLower(obj) { let reg = new RegExp("([A-Z]+)", "g"); for (let key in obj) { if (obj.hasOwnProperty(key)) { let temp = obj[key]; if (reg.test(key.toString())) { // 将修改后的属性名重新赋值给temp,并在对象obj内添加一个转换后的属性 temp = obj[key.replace(reg, function (result) { return result.toLowerCase() })] = obj[key]; // 将之前大写的键属性删除 delete obj[key]; } // 如果属性是对象或者数组,重新执行函数 if (typeof temp === 'object' || Object.prototype.toString.call(temp) === '[object Array]') { keysLower(temp); } } } return obj; }; ``` ## 参考文献 - https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8 ================================================ FILE: docs/JavaScript/this.md ================================================ # 面试官:谈谈this对象的理解 ![](https://static.vue-js.com/46c820d0-74b7-11eb-85f6-6fac77c0c9b3.png) ## 一、定义 函数的 `this` 关键字在 `JavaScript` 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别 在绝大多数情况下,函数的调用方式决定了 `this` 的值(运行时绑定) `this` 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象 举个例子: ```js function baz() { // 当前调用栈是:baz // 因此,当前调用位置是全局作用域 console.log( "baz" ); bar(); // <-- bar的调用位置 } function bar() { // 当前调用栈是:baz --> bar // 因此,当前调用位置在baz中 console.log( "bar" ); foo(); // <-- foo的调用位置 } function foo() { // 当前调用栈是:baz --> bar --> foo // 因此,当前调用位置在bar中 console.log( "foo" ); } baz(); // <-- baz的调用位置 ``` 同时,`this`在函数执行过程中,`this`一旦被确定了,就不可以再更改 ```js var a = 10; var obj = { a: 20 } function fn() { this = obj; // 修改this,运行后会报错 console.log(this.a); } fn(); ``` ## 二、绑定规则 根据不同的使用场合,`this`有不同的值,主要分为下面几种情况: - 默认绑定 - 隐式绑定 - new绑定 - 显示绑定 ### 默认绑定 全局环境中定义`person`函数,内部使用`this`关键字 ```js var name = 'Jenny'; function person() { return this.name; } console.log(person()); //Jenny ``` 上述代码输出`Jenny`,原因是调用函数的对象在游览器中位`window`,因此`this`指向`window`,所以输出`Jenny` 注意: 严格模式下,不能将全局对象用于默认绑定,this会绑定到`undefined`,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象 ### 隐式绑定 函数还可以作为某个对象的方法调用,这时`this`就指这个上级对象 ```js function test() { console.log(this.x); } var obj = {}; obj.x = 1; obj.m = test; obj.m(); // 1 ``` 这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,`this`指向的也只是它上一级的对象 ```js var o = { a:10, b:{ fn:function(){ console.log(this.a); //undefined } } } o.b.fn(); ``` 上述代码中,`this`的上一级对象为`b`,`b`内部并没有`a`变量的定义,所以输出`undefined` 这里再举一种特殊情况 ```js var o = { a:10, b:{ a:12, fn:function(){ console.log(this.a); //undefined console.log(this); //window } } } var j = o.b.fn; j(); ``` 此时`this`指向的是`window`,这里的大家需要记住,`this`永远指向的是最后调用它的对象,虽然`fn`是对象`b`的方法,但是`fn`赋值给`j`时候并没有执行,所以最终指向`window` ### new绑定 通过构建函数`new`关键字生成一个实例对象,此时`this`指向这个实例对象 ```js function test() {  this.x = 1; } var obj = new test(); obj.x // 1 ``` 上述代码之所以能过输出1,是因为`new`关键字改变了`this`的指向 这里再列举一些特殊情况: `new`过程遇到`return`一个对象,此时`this`指向为返回的对象 ```js function fn() { this.user = 'xxx'; return {}; } var a = new fn(); console.log(a.user); //undefined ``` 如果返回一个简单类型的时候,则`this`指向实例对象 ```js function fn() { this.user = 'xxx'; return 1; } var a = new fn; console.log(a.user); //xxx ``` 注意的是`null`虽然也是对象,但是此时`new`仍然指向实例对象 ```js function fn() { this.user = 'xxx'; return null; } var a = new fn; console.log(a.user); //xxx ``` ### 显示修改 `apply()、call()、bind()`是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时`this`指的就是这第一个参数 ```js var x = 0; function test() {  console.log(this.x); } var obj = {}; obj.x = 1; obj.m = test; obj.m.apply(obj) // 1 ``` 关于`apply、call、bind`三者的区别,我们后面再详细说 ## 三、箭头函数 在 ES6 的语法中还提供了箭头函语法,让我们在代码书写时就能确定 `this` 的指向(编译时绑定) 举个例子: ```js const obj = { sayThis: () => { console.log(this); } }; obj.sayThis(); // window 因为 JavaScript 没有块作用域,所以在定义 sayThis 的时候,里面的 this 就绑到 window 上去了 const globalSay = obj.sayThis; globalSay(); // window 浏览器中的 global 对象 ``` 虽然箭头函数的`this`能够在编译的时候就确定了`this`的指向,但也需要注意一些潜在的坑 下面举个例子: 绑定事件监听 ```js const button = document.getElementById('mngb'); button.addEventListener('click', ()=> { console.log(this === window) // true this.innerHTML = 'clicked button' }) ``` 上述可以看到,我们其实是想要`this`为点击的`button`,但此时`this`指向了`window` 包括在原型上添加方法时候,此时`this`指向`window` ```js Cat.prototype.sayName = () => { console.log(this === window) //true return this.name } const cat = new Cat('mm'); cat.sayName() ``` 同样的,箭头函数不能作为构建函数 ## 四、优先级 ### 隐式绑定 VS 显式绑定 ```js function foo() { console.log( this.a ); } var obj1 = { a: 2, foo: foo }; var obj2 = { a: 3, foo: foo }; obj1.foo(); // 2 obj2.foo(); // 3 obj1.foo.call( obj2 ); // 3 obj2.foo.call( obj1 ); // 2 ``` 显然,显示绑定的优先级更高 ### new绑定 VS 隐式绑定 ```js function foo(something) { this.a = something; } var obj1 = { foo: foo }; var obj2 = {}; obj1.foo( 2 ); console.log( obj1.a ); // 2 obj1.foo.call( obj2, 3 ); console.log( obj2.a ); // 3 var bar = new obj1.foo( 4 ); console.log( obj1.a ); // 2 console.log( bar.a ); // 4 ``` 可以看到,new绑定的优先级`>`隐式绑定 ### `new`绑定 VS 显式绑定 因为`new`和`apply、call`无法一起使用,但硬绑定也是显式绑定的一种,可以替换测试 ```js function foo(something) { this.a = something; } var obj1 = {}; var bar = foo.bind( obj1 ); bar( 2 ); console.log( obj1.a ); // 2 var baz = new bar( 3 ); console.log( obj1.a ); // 2 console.log( baz.a ); // 3 ``` `bar`被绑定到obj1上,但是`new bar(3)` 并没有像我们预计的那样把`obj1.a`修改为3。但是,`new`修改了绑定调用`bar()`中的`this` 我们可认为`new`绑定优先级`>`显式绑定 综上,new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级 ## 相关链接 - https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/this ================================================ FILE: docs/JavaScript/type_conversion.md ================================================ # 面试官:谈谈 JavaScript 中的类型转换机制 ![](https://static.vue-js.com/2abd00a0-6692-11eb-85f6-6fac77c0c9b3.png) ## 一、概述 前面我们讲到,`JS `中有六种简单数据类型:`undefined`、`null`、`boolean`、`string`、`number`、`symbol`,以及引用类型:`object` 但是我们在声明的时候只有一种数据类型,只有到运行期间才会确定当前类型 ```js let x = y ? 1 : a; ``` 上面代码中,`x`的值在编译阶段是无法获取的,只有等到程序运行时才能知道 虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的,如果运算子的类型与预期不符合,就会触发类型转换机制 常见的类型转换有: - 强制转换(显示转换) - 自动转换(隐式转换) ## 二、显示转换 显示转换,即我们很清楚可以看到这里发生了类型的转变,常见的方法有: - Number() - parseInt() - String() - Boolean() ### Number() 将任意类型的值转化为数值 先给出类型转换规则: ![](https://static.vue-js.com/915b7300-6692-11eb-ab90-d9ae814b240d.png) 实践一下: ```js Number(324) // 324 // 字符串:如果可以被解析为数值,则转换为相应的数值 Number('324') // 324 // 字符串:如果不可以被解析为数值,返回 NaN Number('324abc') // NaN // 空字符串转为0 Number('') // 0 // 布尔值:true 转成 1,false 转成 0 Number(true) // 1 Number(false) // 0 // undefined:转成 NaN Number(undefined) // NaN // null:转成0 Number(null) // 0 // 对象:通常转换成NaN(除了只包含单个数值的数组) Number({a: 1}) // NaN Number([1, 2, 3]) // NaN Number([5]) // 5 ``` 从上面可以看到,`Number`转换的时候是很严格的,只要有一个字符无法转成数值,整个字符串就会被转为`NaN` ### parseInt() `parseInt`相比`Number`,就没那么严格了,`parseInt`函数逐个解析字符,遇到不能转换的字符就停下来 ```js parseInt('32a3') //32 ``` ### String() 可以将任意类型的值转化成字符串 给出转换规则图: ![](https://static.vue-js.com/48dd8eb0-6692-11eb-85f6-6fac77c0c9b3.png) 实践一下: ```js // 数值:转为相应的字符串 String(1) // "1" //字符串:转换后还是原来的值 String("a") // "a" //布尔值:true转为字符串"true",false转为字符串"false" String(true) // "true" //undefined:转为字符串"undefined" String(undefined) // "undefined" //null:转为字符串"null" String(null) // "null" //对象 String({a: 1}) // "[object Object]" String([1, 2, 3]) // "1,2,3" ``` ### Boolean() 可以将任意类型的值转为布尔值,转换规则如下: ![](https://static.vue-js.com/53bdad10-6692-11eb-ab90-d9ae814b240d.png) 实践一下: ```js Boolean(undefined) // false Boolean(null) // false Boolean(0) // false Boolean(NaN) // false Boolean('') // false Boolean({}) // true Boolean([]) // true Boolean(new Boolean(false)) // true ``` ## 三、隐式转换 在隐式转换中,我们可能最大的疑惑是 :何时发生隐式转换? 我们这里可以归纳为两种情况发生隐式转换的场景: - 比较运算(`==`、`!=`、`>`、`<`)、`if`、`while`需要布尔值地方 - 算术运算(`+`、`-`、`*`、`/`、`%`) 除了上面的场景,还要求运算符两边的操作数不是同一类型 ### 自动转换为布尔值 在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用`Boolean`函数 可以得出个小结: - undefined - null - false - +0 - -0 - NaN - "" 除了上面几种会被转化成`false`,其他都换被转化成`true` ### 自动转换成字符串 遇到预期为字符串的地方,就会将非字符串的值自动转为字符串 具体规则是:先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串 常发生在`+`运算中,一旦存在字符串,则会进行字符串拼接操作 ```js '5' + 1 // '51' '5' + true // "5true" '5' + false // "5false" '5' + {} // "5[object Object]" '5' + [] // "5" '5' + function (){} // "5function (){}" '5' + undefined // "5undefined" '5' + null // "5null" ``` ### 自动转换成数值 除了`+`有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值 ```js '5' - '2' // 3 '5' * '2' // 10 true - 1 // 0 false - 1 // -1 '1' - 1 // 0 '5' * [] // 0 false / '5' // 0 'abc' - 1 // NaN null + 1 // 1 undefined + 1 // NaN ``` `null`转为数值时,值为`0` 。`undefined`转为数值时,值为`NaN` ================================================ FILE: docs/JavaScript/typeof_instanceof.md ================================================ # 面试官:typeof 与 instanceof 区别 ![](https://static.vue-js.com/3fc158f0-7710-11eb-ab90-d9ae814b240d.png) ## 一、typeof `typeof` 操作符返回一个字符串,表示未经计算的操作数的类型 使用方法如下: ```js typeof operand typeof(operand) ``` `operand`表示对象或原始值的表达式,其类型将被返回 举个例子 ```js typeof 1 // 'number' typeof '1' // 'string' typeof undefined // 'undefined' typeof true // 'boolean' typeof Symbol() // 'symbol' typeof null // 'object' typeof [] // 'object' typeof {} // 'object' typeof console // 'object' typeof console.log // 'function' ``` 从上面例子,前6个都是基础数据类型。虽然`typeof null`为`object`,但这只是` JavaScript` 存在的一个悠久 `Bug`,不代表`null `就是引用数据类型,并且`null `本身也不是对象 所以,`null `在 `typeof `之后返回的是有问题的结果,不能作为判断` null `的方法。如果你需要在 `if` 语句中判断是否为 `null`,直接通过`===null`来判断就好 同时,可以发现引用类型数据,用`typeof`来判断的话,除了`function`会被识别出来之外,其余的都输出`object` 如果我们想要判断一个变量是否存在,可以使用`typeof`:(不能使用`if(a)`, 若`a`未声明,则报错) ```js if(typeof a != 'undefined'){ //变量存在 } ``` ## 二、instanceof `instanceof` 运算符用于检测构造函数的 `prototype` 属性是否出现在某个实例对象的原型链上 使用如下: ```js object instanceof constructor ``` `object`为实例对象,`constructor`为构造函数 构造函数通过`new`可以实例对象,`instanceof `能判断这个对象是否是之前那个构造函数生成的对象 ```js // 定义构建函数 let Car = function() {} let benz = new Car() benz instanceof Car // true let car = new String('xxx') car instanceof String // true let str = 'xxx' str instanceof String // false ``` 关于`instanceof`的实现原理,可以参考下面: ```js function myInstanceof(left, right) { // 这里先用typeof来判断基础数据类型,如果是,直接返回false if(typeof left !== 'object' || left === null) return false; // getProtypeOf是Object对象自带的API,能够拿到参数的原型对象 let proto = Object.getPrototypeOf(left); while(true) { if(proto === null) return false; if(proto === right.prototype) return true;//找到相同原型对象,返回true proto = Object.getPrototypeof(proto); } } ``` 也就是顺着原型链去找,直到找到相同的原型对象,返回`true`,否则为`false` ## 三、区别 `typeof`与`instanceof`都是判断数据类型的方法,区别如下: - `typeof`会返回一个变量的基本类型,`instanceof`返回的是一个布尔值 - `instanceof` 可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型 - 而` typeof` 也存在弊端,它虽然可以判断基础数据类型(`null` 除外),但是引用数据类型中,除了` function` 类型以外,其他的也无法判断 可以看到,上述两种方法都有弊端,并不能满足所有场景的需求 如果需要通用检测数据类型,可以采用`Object.prototype.toString`,调用该方法,统一返回格式`“[object Xxx]” `的字符串 如下 ```js Object.prototype.toString({}) // "[object Object]" Object.prototype.toString.call({}) // 同上结果,加上call也ok Object.prototype.toString.call(1) // "[object Number]" Object.prototype.toString.call('1') // "[object String]" Object.prototype.toString.call(true) // "[object Boolean]" Object.prototype.toString.call(function(){}) // "[object Function]" Object.prototype.toString.call(null) //"[object Null]" Object.prototype.toString.call(undefined) //"[object Undefined]" Object.prototype.toString.call(/123/g) //"[object RegExp]" Object.prototype.toString.call(new Date()) //"[object Date]" Object.prototype.toString.call([]) //"[object Array]" Object.prototype.toString.call(document) //"[object HTMLDocument]" Object.prototype.toString.call(window) //"[object Window]" ``` 了解了`toString`的基本用法,下面就实现一个全局通用的数据类型判断方法 ```js function getType(obj){ let type = typeof obj; if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回 return type; } // 对于typeof返回结果是object的,再进行如下的判断,正则返回结果 return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1'); } ``` 使用如下 ```js getType([]) // "Array" typeof []是object,因此toString返回 getType('123') // "string" typeof 直接返回 getType(window) // "Window" toString返回 getType(null) // "Null"首字母大写,typeof null是object,需toString来判断 getType(undefined) // "undefined" typeof 直接返回 getType() // "undefined" typeof 直接返回 getType(function(){}) // "function" typeof能判断,因此首字母小写 getType(/123/g) //"RegExp" toString返回 ``` ================================================ FILE: docs/JavaScript/visible.md ================================================ # 面试官:如何判断一个元素是否在可视区域中? ![](https://static.vue-js.com/d848c790-8a05-11eb-85f6-6fac77c0c9b3.png) ## 一、用途 可视区域即我们浏览网页的设备肉眼可见的区域,如下图 ![](https://static.vue-js.com/9c5bbb10-8a56-11eb-85f6-6fac77c0c9b3.png) 在日常开发中,我们经常需要判断目标元素是否在视窗之内或者和视窗的距离小于一个值(例如 100 px),从而实现一些常用的功能,例如: - 图片的懒加载 - 列表的无限滚动 - 计算广告元素的曝光情况 - 可点击链接的预加载 ## 二、实现方式 判断一个元素是否在可视区域,我们常用的有三种办法: - offsetTop、scrollTop - getBoundingClientRect - Intersection Observer ### offsetTop、scrollTop `offsetTop`,元素的上外边框至包含元素的上内边框之间的像素距离,其他`offset`属性如下图所示: ![](https://static.vue-js.com/b4b63ca0-8a54-11eb-85f6-6fac77c0c9b3.png) 下面再来了解下`clientWidth`、`clientHeight`: - `clientWidth`:元素内容区宽度加上左右内边距宽度,即`clientWidth = content + padding` - `clientHeight`:元素内容区高度加上上下内边距高度,即`clientHeight = content + padding` 这里可以看到`client`元素都不包括外边距 最后,关于`scroll`系列的属性如下: - `scrollWidth` 和 `scrollHeight` 主要用于确定元素内容的实际大小 - `scrollLeft` 和 `scrollTop` 属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位置 - - 垂直滚动 `scrollTop > 0` - 水平滚动 `scrollLeft > 0` - 将元素的 `scrollLeft` 和 `scrollTop` 设置为 0,可以重置元素的滚动位置 #### 注意 - 上述属性都是只读的,每次访问都要重新开始 下面再看看如何实现判断: 公式如下: ```js el.offsetTop - document.documentElement.scrollTop <= viewPortHeight ``` 代码实现: ```js function isInViewPortOfOne (el) { // viewPortHeight 兼容所有浏览器写法 const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight const offsetTop = el.offsetTop const scrollTop = document.documentElement.scrollTop const top = offsetTop - scrollTop return top <= viewPortHeight } ``` ### getBoundingClientRect 返回值是一个 `DOMRect`对象,拥有`left`, `top`, `right`, `bottom`, `x`, `y`, `width`, 和 `height`属性 ```js const target = document.querySelector('.target'); const clientRect = target.getBoundingClientRect(); console.log(clientRect); // { // bottom: 556.21875, // height: 393.59375, // left: 333, // right: 1017, // top: 162.625, // width: 684 // } ``` 属性对应的关系图如下所示: ![](https://static.vue-js.com/e34ac5d0-8a05-11eb-85f6-6fac77c0c9b3.png) 当页面发生滚动的时候,`top`与`left`属性值都会随之改变 如果一个元素在视窗之内的话,那么它一定满足下面四个条件: - top 大于等于 0 - left 大于等于 0 - bottom 小于等于视窗高度 - right 小于等于视窗宽度 实现代码如下: ```js function isInViewPort(element) { const viewWidth = window.innerWidth || document.documentElement.clientWidth; const viewHeight = window.innerHeight || document.documentElement.clientHeight; const { top, right, bottom, left, } = element.getBoundingClientRect(); return ( top >= 0 && left >= 0 && right <= viewWidth && bottom <= viewHeight ); } ``` ### Intersection Observer `Intersection Observer` 即重叠观察者,从这个命名就可以看出它用于判断两个元素是否重叠,因为不用进行事件的监听,性能方面相比`getBoundingClientRect `会好很多 使用步骤主要分为两步:创建观察者和传入被观察者 #### 创建观察者 ```js const options = { // 表示重叠面积占被观察者的比例,从 0 - 1 取值, // 1 表示完全被包含 threshold: 1.0, root:document.querySelector('#scrollArea') // 必须是目标元素的父级元素 }; const callback = (entries, observer) => { ....} const observer = new IntersectionObserver(callback, options); ``` 通过`new IntersectionObserver`创建了观察者 `observer`,传入的参数 `callback` 在重叠比例超过 `threshold` 时会被执行` 关于`callback`回调函数常用属性如下: ```js // 上段代码中被省略的 callback const callback = function(entries, observer) { entries.forEach(entry => { entry.time; // 触发的时间 entry.rootBounds; // 根元素的位置矩形,这种情况下为视窗位置 entry.boundingClientRect; // 被观察者的位置举行 entry.intersectionRect; // 重叠区域的位置矩形 entry.intersectionRatio; // 重叠区域占被观察者面积的比例(被观察者不是矩形时也按照矩形计算) entry.target; // 被观察者 }); }; ``` #### 传入被观察者 通过 `observer.observe(target)` 这一行代码即可简单的注册被观察者 ```js const target = document.querySelector('.target'); observer.observe(target); ``` ### 三、案例分析 实现:创建了一个十万个节点的长列表,当节点滚入到视窗中时,背景就会从红色变为黄色 `Html`结构如下: ```js
``` `css`样式如下: ```css .container { display: flex; flex-wrap: wrap; } .target { margin: 5px; width: 20px; height: 20px; background: red; } ``` 往`container`插入1000个元素 ```js const $container = $(".container"); // 插入 100000 个
function createTargets() { const htmlString = new Array(100000) .fill('
') .join(""); $container.html(htmlString); } ``` 这里,首先使用`getBoundingClientRect `方法进行判断元素是否在可视区域 ```js function isInViewPort(element) { const viewWidth = window.innerWidth || document.documentElement.clientWidth; const viewHeight = window.innerHeight || document.documentElement.clientHeight; const { top, right, bottom, left } = element.getBoundingClientRect(); return top >= 0 && left >= 0 && right <= viewWidth && bottom <= viewHeight; } ``` 然后开始监听`scroll`事件,判断页面上哪些元素在可视区域中,如果在可视区域中则将背景颜色设置为`yellow` ```js $(window).on("scroll", () => { console.log("scroll !"); $targets.each((index, element) => { if (isInViewPort(element)) { $(element).css("background-color", "yellow"); } }); }); ``` 通过上述方式,可以看到可视区域颜色会变成黄色了,但是可以明显看到有卡顿的现象,原因在于我们绑定了`scroll`事件,`scroll`事件伴随了大量的计算,会造成资源方面的浪费 下面通过`Intersection Observer`的形式同样实现相同的功能 首先创建一个观察者 ```js const observer = new IntersectionObserver(getYellow, { threshold: 1.0 }); ``` `getYellow`回调函数实现对背景颜色改变,如下: ```js function getYellow(entries, observer) { entries.forEach(entry => { $(entry.target).css("background-color", "yellow"); }); } ``` 最后传入观察者,即`.target`元素 ```js $targets.each((index, element) => { observer.observe(element); }); ``` 可以看到功能同样完成,并且页面不会出现卡顿的情况 ## 参考文献 - https://developer.mozilla.org/zh-CN/docs/Web/API/Element/getBoundingClientRect - https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API ================================================ FILE: docs/NodeJS/Buffer.md ================================================ # 面试官:说说对 Node 中的 Buffer 的理解?应用场景? ![](https://static.vue-js.com/176d02b0-c69c-11eb-ab90-d9ae814b240d.png) ## 一、是什么 在`Node`应用中,需要处理网络协议、操作数据库、处理图片、接收上传文件等,在网络流和文件的操作中,要处理大量二进制数据,而`Buffer`就是在内存中开辟一片区域(初次初始化为8KB),用来存放二进制数据 在上述操作中都会存在数据流动,每个数据流动的过程中,都会有一个最小或最大数据量 如果数据到达的速度比进程消耗的速度快,那么少数早到达的数据会处于等待区等候被处理。反之,如果数据到达的速度比进程消耗的数据慢,那么早先到达的数据需要等待一定量的数据到达之后才能被处理 这里的等待区就指的缓冲区(Buffer),它是计算机中的一个小物理单位,通常位于计算机的 `RAM` 中 简单来讲,`Nodejs`不能控制数据传输的速度和到达时间,只能决定何时发送数据,如果还没到发送时间,则将数据放在`Buffer`中,即在`RAM`中,直至将它们发送完毕 上面讲到了`Buffer`是用来存储二进制数据,其的形式可以理解成一个数组,数组中的每一项,都可以保存8位二进制:`00000000`,也就是一个字节 例如: ```js const buffer = Buffer.from("why") ``` 其存储过程如下图所示: ![](https://static.vue-js.com/20371250-c69c-11eb-ab90-d9ae814b240d.png) ## 二、使用方法 `Buffer` 类在全局作用域中,无须`require`导入 创建`Buffer`的方法有很多种,我们讲讲下面的两种常见的形式: - Buffer.from() - Buffer.alloc() ### Buffer.from() ```js const b1 = Buffer.from('10'); const b2 = Buffer.from('10', 'utf8'); const b3 = Buffer.from([10]); const b4 = Buffer.from(b3); console.log(b1, b2, b3, b4); // ``` ### Buffer.alloc() ```js const bAlloc1 = Buffer.alloc(10); // 创建一个大小为 10 个字节的缓冲区 const bAlloc2 = Buffer.alloc(10, 1); // 建一个长度为 10 的 Buffer,其中全部填充了值为 `1` 的字节 console.log(bAlloc1); // console.log(bAlloc2); // ``` 在上面创建`buffer`后,则能够`toString`的形式进行交互,默认情况下采取`utf8`字符编码形式,如下 ```js const buffer = Buffer.from("你好"); console.log(buffer); // const str = buffer.toString(); console.log(str); // 你好 ``` 如果编码与解码不是相同的格式则会出现乱码的情况,如下: ```js const buffer = Buffer.from("你好","utf-8 "); console.log(buffer); // const str = buffer.toString("ascii"); console.log(str); // d= e%= ``` 当设定的范围导致字符串被截断的时候,也会存在乱码情况,如下: ```js const buf = Buffer.from('Node.js 技术栈', 'UTF-8'); console.log(buf) // console.log(buf.length) // 17 console.log(buf.toString('UTF-8', 0, 9)) // Node.js � console.log(buf.toString('UTF-8', 0, 11)) // Node.js 技 ``` 所支持的字符集有如下: - ascii:仅支持 7 位 ASCII 数据,如果设置去掉高位的话,这种编码是非常快的 - utf8:多字节编码的 Unicode 字符,许多网页和其他文档格式都使用 UTF-8 - utf16le:2 或 4 个字节,小字节序编码的 Unicode 字符,支持代理对(U+10000至 U+10FFFF) - ucs2,utf16le 的别名 - base64:Base64 编码 - latin:一种把 Buffer 编码成一字节编码的字符串的方式 - binary:latin1 的别名, - hex:将每个字节编码为两个十六进制字符 ## 三、应用场景 `Buffer`的应用场景常常与流的概念联系在一起,例如有如下: - I/O操作 - 加密解密 - zlib.js ### I/O操作 通过流的形式,将一个文件的内容读取到另外一个文件 ```js const fs = require('fs'); const inputStream = fs.createReadStream('input.txt'); // 创建可读流 const outputStream = fs.createWriteStream('output.txt'); // 创建可写流 inputStream.pipe(outputStream); // 管道读写 ``` ### 加解密 在一些加解密算法中会遇到使用 `Buffer`,例如 `crypto.createCipheriv` 的第二个参数 `key` 为 `string` 或 `Buffer` 类型 ### zlib.js `zlib.js` 为 `Node.js` 的核心库之一,其利用了缓冲区(`Buffer`)的功能来操作二进制数据流,提供了压缩或解压功能 ## 参考文献 - http://nodejs.cn/api/buffer.html - https://segmentfault.com/a/1190000019894714 ================================================ FILE: docs/NodeJS/EventEmitter.md ================================================ # 面试官:说说Node中的EventEmitter? 如何实现一个EventEmitter? ![](https://static.vue-js.com/16b10390-c83a-11eb-ab90-d9ae814b240d.png) ## 一、是什么 我们了解到,`Node `采用了事件驱动机制,而`EventEmitter `就是`Node`实现事件驱动的基础 在`EventEmitter`的基础上,`Node `几乎所有的模块都继承了这个类,这些模块拥有了自己的事件,可以绑定/触发监听器,实现了异步操作 `Node.js` 里面的许多对象都会分发事件,比如 fs.readStream 对象会在文件被打开的时候触发一个事件 这些产生事件的对象都是 events.EventEmitter 的实例,这些对象有一个 eventEmitter.on() 函数,用于将一个或多个函数绑定到命名事件上 ## 二、使用方法 `Node `的`events`模块只提供了一个`EventEmitter`类,这个类实现了`Node`异步事件驱动架构的基本模式——观察者模式 在这种模式中,被观察者(主体)维护着一组其他对象派来(注册)的观察者,有新的对象对主体感兴趣就注册观察者,不感兴趣就取消订阅,主体有更新的话就依次通知观察者们 基本代码如下所示: ```js const EventEmitter = require('events') class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter() function callback() { console.log('触发了event事件!') } myEmitter.on('event', callback) myEmitter.emit('event') myEmitter.removeListener('event', callback); ``` 通过实例对象的`on`方法注册一个名为`event`的事件,通过`emit`方法触发该事件,而`removeListener`用于取消事件的监听 关于其常见的方法如下: - emitter.addListener/on(eventName, listener) :添加类型为 eventName 的监听事件到事件数组尾部 - emitter.prependListener(eventName, listener):添加类型为 eventName 的监听事件到事件数组头部 - emitter.emit(eventName[, ...args]):触发类型为 eventName 的监听事件 - emitter.removeListener/off(eventName, listener):移除类型为 eventName 的监听事件 - emitter.once(eventName, listener):添加类型为 eventName 的监听事件,以后只能执行一次并删除 - emitter.removeAllListeners([eventName]): 移除全部类型为 eventName 的监听事件 ## 三、实现过程 通过上面的方法了解,`EventEmitter`是一个构造函数,内部存在一个包含所有事件的对象 ```js class EventEmitter { constructor() { this.events = {}; } } ``` 其中`events`存放的监听事件的函数的结构如下: ```js { "event1": [f1,f2,f3], "event2": [f4,f5], ... } ``` 然后开始一步步实现实例方法,首先是`emit`,第一个参数为事件的类型,第二个参数开始为触发事件函数的参数,实现如下: ```js emit(type, ...args) { this.events[type].forEach((item) => { Reflect.apply(item, this, args); }); } ``` 当实现了`emit`方法之后,然后实现`on`、`addListener`、`prependListener`这三个实例方法,都是添加事件监听触发函数,实现也是大同小异 ```js on(type, handler) { if (!this.events[type]) { this.events[type] = []; } this.events[type].push(handler); } addListener(type,handler){ this.on(type,handler) } prependListener(type, handler) { if (!this.events[type]) { this.events[type] = []; } this.events[type].unshift(handler); } ``` 紧接着就是实现事件监听的方法`removeListener/on` ```js removeListener(type, handler) { if (!this.events[type]) { return; } this.events[type] = this.events[type].filter(item => item !== handler); } off(type,handler){ this.removeListener(type,handler) } ``` 最后再来实现`once`方法, 再传入事件监听处理函数的时候进行封装,利用闭包的特性维护当前状态,通过`fired`属性值判断事件函数是否执行过 ```js once(type, handler) { this.on(type, this._onceWrap(type, handler, this)); } _onceWrap(type, handler, target) { const state = { fired: false, handler, type , target}; const wrapFn = this._onceWrapper.bind(state); state.wrapFn = wrapFn; return wrapFn; } _onceWrapper(...args) { if (!this.fired) { this.fired = true; Reflect.apply(this.handler, this.target, args); this.target.off(this.type, this.wrapFn); } } ``` 完整代码如下: ```js class EventEmitter { constructor() { this.events = {}; } on(type, handler) { if (!this.events[type]) { this.events[type] = []; } this.events[type].push(handler); } addListener(type,handler){ this.on(type,handler) } prependListener(type, handler) { if (!this.events[type]) { this.events[type] = []; } this.events[type].unshift(handler); } removeListener(type, handler) { if (!this.events[type]) { return; } this.events[type] = this.events[type].filter(item => item !== handler); } off(type,handler){ this.removeListener(type,handler) } emit(type, ...args) { this.events[type].forEach((item) => { Reflect.apply(item, this, args); }); } once(type, handler) { this.on(type, this._onceWrap(type, handler, this)); } _onceWrap(type, handler, target) { const state = { fired: false, handler, type , target}; const wrapFn = this._onceWrapper.bind(state); state.wrapFn = wrapFn; return wrapFn; } _onceWrapper(...args) { if (!this.fired) { this.fired = true; Reflect.apply(this.handler, this.target, args); this.target.off(this.type, this.wrapFn); } } } ``` 测试代码如下: ```js const ee = new EventEmitter(); // 注册所有事件 ee.once('wakeUp', (name) => { console.log(`${name} 1`); }); ee.on('eat', (name) => { console.log(`${name} 2`) }); ee.on('eat', (name) => { console.log(`${name} 3`) }); const meetingFn = (name) => { console.log(`${name} 4`) }; ee.on('work', meetingFn); ee.on('work', (name) => { console.log(`${name} 5`) }); ee.emit('wakeUp', 'xx'); ee.emit('wakeUp', 'xx'); // 第二次没有触发 ee.emit('eat', 'xx'); ee.emit('work', 'xx'); ee.off('work', meetingFn); // 移除事件 ee.emit('work', 'xx'); // 再次工作 ``` ## 参考文献 - http://nodejs.cn/api/events.html#events_class_eventemitter - https://segmentfault.com/a/1190000015762318 - https://juejin.cn/post/6844903781230968845 - https://vue3js.cn/interview ================================================ FILE: docs/NodeJS/Stream.md ================================================ # 面试官:说说对 Node 中的 Stream 的理解?应用场景? ![](https://static.vue-js.com/a5df3c60-c76f-11eb-ab90-d9ae814b240d.png) ## 一、是什么 流(Stream),是一个数据传输手段,是端到端信息交换的一种方式,而且是有顺序的,是逐块读取数据、处理内容,用于顺序读取输入或写入输出 `Node.js`中很多对象都实现了流,总之它是会冒数据(以 `Buffer` 为单位) 它的独特之处在于,它不像传统的程序那样一次将一个文件读入内存,而是逐块读取数据、处理其内容,而不是将其全部保存在内存中 流可以分成三部分:`source`、`dest`、`pipe` 在`source`和`dest`之间有一个连接的管道`pipe`,它的基本语法是`source.pipe(dest)`,`source`和`dest`就是通过pipe连接,让数据从`source`流向了`dest`,如下图所示: ![](https://static.vue-js.com/aec05670-c76f-11eb-ab90-d9ae814b240d.png) ## 二、种类 在`NodeJS`,几乎所有的地方都使用到了流的概念,分成四个种类: - 可写流:可写入数据的流。例如 fs.createWriteStream() 可以使用流将数据写入文件 - 可读流: 可读取数据的流。例如fs.createReadStream() 可以从文件读取内容 - 双工流: 既可读又可写的流。例如 net.Socket - 转换流: 可以在数据写入和读取时修改或转换数据的流。例如,在文件压缩操作中,可以向文件写入压缩数据,并从文件中读取解压数据 在`NodeJS`中`HTTP`服务器模块中,`request` 是可读流,`response` 是可写流。还有`fs` 模块,能同时处理可读和可写文件流 可读流和可写流都是单向的,比较容易理解,而另外两个是双向的 ### 双工流 之前了解过`websocket`通信,是一个全双工通信,发送方和接受方都是各自独立的方法,发送和接收都没有任何关系 如下图所示: ![](https://static.vue-js.com/b7ac6d00-c76f-11eb-ab90-d9ae814b240d.png) 基本代码如下: ```js const { Duplex } = require('stream'); const myDuplex = new Duplex({ read(size) { // ... }, write(chunk, encoding, callback) { // ... } }); ``` ### 双工流 双工流的演示图如下所示: ![](https://static.vue-js.com/c02883b0-c76f-11eb-ab90-d9ae814b240d.png) 除了上述压缩包的例子,还比如一个 `babel`,把`es6`转换为,我们在左边写入 `es6`,从右边读取 `es5` 基本代码如下所示: ```js const { Transform } = require('stream'); const myTransform = new Transform({ transform(chunk, encoding, callback) { // ... } }); ``` ## 三、应用场景 `stream`的应用场景主要就是处理`IO`操作,而`http`请求和文件操作都属于`IO`操作 试想一下,如果一次`IO`操作过大,硬件的开销就过大,而将此次大的`IO`操作进行分段操作,让数据像水管一样流动,直到流动完成 常见的场景有: - get请求返回文件给客户端 - 文件操作 - 一些打包工具的底层操作 ### get请求返回文件给客户端 使用`stream`流返回文件,`res`也是一个`stream`对象,通过`pipe`管道将文件数据返回 ```js const server = http.createServer(function (req, res) { const method = req.method; // 获取请求方法 if (method === 'GET') { // get 请求 const fileName = path.resolve(__dirname, 'data.txt'); let stream = fs.createReadStream(fileName); stream.pipe(res); // 将 res 作为 stream 的 dest } }); server.listen(8000); ``` ### 文件操作 创建一个可读数据流`readStream`,一个可写数据流`writeStream`,通过`pipe`管道把数据流转过去 ```js const fs = require('fs') const path = require('path') // 两个文件名 const fileName1 = path.resolve(__dirname, 'data.txt') const fileName2 = path.resolve(__dirname, 'data-bak.txt') // 读取文件的 stream 对象 const readStream = fs.createReadStream(fileName1) // 写入文件的 stream 对象 const writeStream = fs.createWriteStream(fileName2) // 通过 pipe执行拷贝,数据流转 readStream.pipe(writeStream) // 数据读取完成监听,即拷贝完成 readStream.on('end', function () { console.log('拷贝完成') }) ``` ### 一些打包工具的底层操作 目前一些比较火的前端打包构建工具,都是通过`node.js`编写的,打包和构建的过程肯定是文件频繁操作的过程,离不来`stream`,如`gulp` ## 参考文献 - https://xie.infoq.cn/article/1a9695020828460eb3c4ff1fa - https://juejin.cn/post/6844903891083984910 ================================================ FILE: docs/NodeJS/event_loop.md ================================================ # 面试官:说说对Nodejs中的事件循环机制理解? ![](https://static.vue-js.com/e0faf3c0-c90e-11eb-ab90-d9ae814b240d.png) ## 一、是什么 在[浏览器事件循环](https://github.com/febobo/web-interview/issues/73)中,我们了解到`javascript`在浏览器中的事件循环机制,其是根据`HTML5`定义的规范来实现 而在`NodeJS`中,事件循环是基于`libuv`实现,`libuv`是一个多平台的专注于异步IO的库,如下图最右侧所示: ![](https://static.vue-js.com/ea690b90-c90e-11eb-85f6-6fac77c0c9b3.png) 上图`EVENT_QUEUE` 给人看起来只有一个队列,但`EventLoop`存在6个阶段,每个阶段都有对应的一个先进先出的回调队列 ## 二、流程 上节讲到事件循环分成了六个阶段,对应如下: ![](https://static.vue-js.com/f2e34d80-c90e-11eb-ab90-d9ae814b240d.png) - timers阶段:这个阶段执行timer(setTimeout、setInterval)的回调 - 定时器检测阶段(timers):本阶段执行 timer 的回调,即 setTimeout、setInterval 里面的回调函数 - I/O事件回调阶段(I/O callbacks):执行延迟到下一个循环迭代的 I/O 回调,即上一轮循环中未被执行的一些I/O回调 - 闲置阶段(idle, prepare):仅系统内部使用 - 轮询阶段(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞 - 检查阶段(check):setImmediate() 回调函数在这里执行 - 关闭事件回调阶段(close callback):一些关闭的回调函数,如:socket.on('close', ...) 每个阶段对应一个队列,当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶段 除了上述6个阶段,还存在`process.nextTick`,其不属于事件循环的任何一个阶段,它属于该阶段与下阶段之间的过渡, 即本阶段执行结束, 进入下一个阶段前, 所要执行的回调,类似插队 流程图如下所示: ![](https://static.vue-js.com/fbe731d0-c90e-11eb-ab90-d9ae814b240d.png) 在`Node`中,同样存在宏任务和微任务,与浏览器中的事件循环相似 微任务对应有: - next tick queue:process.nextTick - other queue:Promise的then回调、queueMicrotask 宏任务对应有: - timer queue:setTimeout、setInterval - poll queue:IO事件 - check queue:setImmediate - close queue:close事件 其执行顺序为: - next tick microtask queue - other microtask queue - timer queue - poll queue - check queue - close queue ## 三、题目 通过上面的学习,下面开始看看题目 ```js async function async1() { console.log('async1 start') await async2() console.log('async1 end') } async function async2() { console.log('async2') } console.log('script start') setTimeout(function () { console.log('setTimeout0') }, 0) setTimeout(function () { console.log('setTimeout2') }, 300) setImmediate(() => console.log('setImmediate')); process.nextTick(() => console.log('nextTick1')); async1(); process.nextTick(() => console.log('nextTick2')); new Promise(function (resolve) { console.log('promise1') resolve(); console.log('promise2') }).then(function () { console.log('promise3') }) console.log('script end') ``` 分析过程: - 先找到同步任务,输出script start - 遇到第一个 setTimeout,将里面的回调函数放到 timer 队列中 - 遇到第二个 setTimeout,300ms后将里面的回调函数放到 timer 队列中 - 遇到第一个setImmediate,将里面的回调函数放到 check 队列中 - 遇到第一个 nextTick,将其里面的回调函数放到本轮同步任务执行完毕后执行 - 执行 async1函数,输出 async1 start - 执行 async2 函数,输出 async2,async2 后面的输出 async1 end进入微任务,等待下一轮的事件循环 - 遇到第二个,将其里面的回调函数放到本轮同步任务执行完毕后执行 - 遇到 new Promise,执行里面的立即执行函数,输出 promise1、promise2 - then里面的回调函数进入微任务队列 - 遇到同步任务,输出 script end - 执行下一轮回到函数,先依次输出 nextTick 的函数,分别是 nextTick1、nextTick2 - 然后执行微任务队列,依次输出 async1 end、promise3 - 执行timer 队列,依次输出 setTimeout0 - 接着执行 check 队列,依次输出 setImmediate - 300ms后,timer 队列存在任务,执行输出 setTimeout2 执行结果如下: ``` script start async1 start async2 promise1 promise2 script end nextTick1 nextTick2 async1 end promise3 setTimeout0 setImmediate setTimeout2 ``` 最后有一道是关于`setTimeout`与`setImmediate`的输出顺序 ```js setTimeout(() => { console.log("setTimeout"); }, 0); setImmediate(() => { console.log("setImmediate"); }); ``` 输出情况如下: ```js 情况一: setTimeout setImmediate 情况二: setImmediate setTimeout ``` 分析下流程: - 外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段 - 遇到`setTimeout`,虽然设置的是0毫秒触发,但实际上会被强制改成1ms,时间到了然后塞入`times`阶段 - 遇到`setImmediate`塞入`check`阶段 - 同步代码执行完毕,进入Event Loop - 先进入`times`阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足`setTimeout`条件,执行回调,如果没过1毫秒,跳过 - 跳过空的阶段,进入check阶段,执行`setImmediate`回调 这里的关键在于这1ms,如果同步代码执行时间较长,进入`Event Loop`的时候1毫秒已经过了,`setTimeout`先执行,如果1毫秒还没到,就先执行了`setImmediate` ## 参考文献 - https://segmentfault.com/a/1190000012258592 - https://juejin.cn/post/6844904100195205133 - https://vue3js.cn/interview/ ================================================ FILE: docs/NodeJS/file_upload.md ================================================ # 面试官:如何实现文件上传?说说你的思路 ![](https://static.vue-js.com/248a5580-ce60-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 文件上传在日常开发中应用很广泛,我们发微博、发微信朋友圈都会用到了图片上传功能 因为浏览器限制,浏览器不能直接操作文件系统的,需要通过浏览器所暴露出来的统一接口,由用户主动授权发起来访问文件动作,然后读取文件内容进指定内存里,最后执行提交请求操作,将内存里的文件内容数据上传到服务端,服务端解析前端传来的数据信息后存入文件里 对于文件上传,我们需要设置请求头为`content-type:multipart/form-data` > multipart互联网上的混合资源,就是资源由多种元素组成,form-data表示可以使用HTML Forms 和 POST 方法上传文件 结构如下: ```http POST /t2/upload.do HTTP/1.1 User-Agent: SOHUWapRebot Accept-Language: zh-cn,zh;q=0.5 Accept-Charset: GBK,utf-8;q=0.7,*;q=0.7 Connection: keep-alive Content-Length: 60408 Content-Type:multipart/form-data; boundary=ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC Host: w.sohu.com --ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC Content-Disposition: form-data; name="city" Santa colo --ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC Content-Disposition: form-data;name="desc" Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ... --ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC Content-Disposition: form-data;name="pic"; filename="photo.jpg" Content-Type: application/octet-stream Content-Transfer-Encoding: binary ... binary data of the jpg ... --ZnGpDtePMx0KrHh_G0X99Yef9r8JZsRJSXC-- ``` `boundary`表示分隔符,如果要上传多个表单项,就要使用`boundary`分割,每个表单项由`———XXX`开始,以`———XXX`结尾 而`xxx`是即时生成的字符串,用以确保整个分隔符不会在文件或表单项的内容中出现 每个表单项必须包含一个 `Content-Disposition` 头,其他的头信息则为可选项, 比如 `Content-Type` `Content-Disposition` 包含了 `type `和 一个名字为` name `的 `parameter`,`type` 是 `form-data`,`name `参数的值则为表单控件(也即 field)的名字,如果是文件,那么还有一个 `filename `参数,值就是文件名 ```kotlin Content-Disposition: form-data; name="user"; filename="logo.png" ``` 至于使用`multipart/form-data`,是因为文件是以二进制的形式存在,其作用是专门用于传输大型二进制数据,效率高 ### 二、如何实现 关于文件的上传的上传,我们可以分成两步骤: - 文件的上传 - 文件的解析 ### 文件上传 传统前端文件上传的表单结构如下: ```html
``` `action` 就是我们的提交到的接口,`enctype="multipart/form-data"` 就是指定上传文件格式,`input` 的 `name` 属性一定要等于`file` ### 文件解析 在服务器中,这里采用`koa2`中间件的形式解析上传的文件数据,分别有下面两种形式: - koa-body - koa-multer #### koa-body 安装依赖 ```cmd npm install koa-body ``` 引入`koa-body`中间件 ```js const koaBody = require('koa-body'); app.use(koaBody({ multipart: true, formidable: { maxFileSize: 200*1024*1024 // 设置上传文件大小最大限制,默认2M } })); ``` 获取上传的文件 ```js const file = ctx.request.files.file; // 获取上传文件 ``` 获取文件数据后,可以通过`fs`模块将文件保存到指定目录 ```js router.post('/uploadfile', async (ctx, next) => { // 上传单个文件 const file = ctx.request.files.file; // 获取上传文件 // 创建可读流 const reader = fs.createReadStream(file.path); let filePath = path.join(__dirname, 'public/upload/') + `/${file.name}`; // 创建可写流 const upStream = fs.createWriteStream(filePath); // 可读流通过管道写入可写流 reader.pipe(upStream); return ctx.body = "上传成功!"; }); ``` #### koa-multer 安装依赖: ```cmd npm install koa-multer ``` 使用 `multer` 中间件实现文件上传 ```js const storage = multer.diskStorage({ destination: (req, file, cb) => { cb(null, "./upload/") }, filename: (req, file, cb) => { cb(null, Date.now() + path.extname(file.originalname)) } }) const upload = multer({ storage }); const fileRouter = new Router(); fileRouter.post("/upload", upload.single('file'), (ctx, next) => { console.log(ctx.req.file); // 获取文件 }) app.use(fileRouter.routes()); ``` ## 参考文献 - https://segmentfault.com/a/1190000037411957 - https://www.jianshu.com/p/29e38bcc8a1d ================================================ FILE: docs/NodeJS/fs.md ================================================ # 面试官:说说对 Node 中的 fs模块的理解? 有哪些常用方法 ![](https://static.vue-js.com/a141e5c0-c46a-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 fs(filesystem),该模块提供本地文件的读写能力,基本上是`POSIX`文件操作命令的简单包装 可以说,所有与文件的操作都是通过`fs`核心模块实现 导入模块如下: ```js const fs = require('fs'); ``` 这个模块对所有文件系统操作提供异步(不具有`sync` 后缀)和同步(具有 `sync` 后缀)两种操作方式,而供开发者选择 ### 二、文件知识 在计算机中有关于文件的知识: - 权限位 mode - 标识位 flag - 文件描述为 fd ### 权限位 mode ![](https://static.vue-js.com/4f4d41a0-c46b-11eb-ab90-d9ae814b240d.png) 针对文件所有者、文件所属组、其他用户进行权限分配,其中类型又分成读、写和执行,具备权限位4、2、1,不具备权限为0 如在`linux`查看文件权限位: ```js drwxr-xr-x 1 PandaShen 197121 0 Jun 28 14:41 core -rw-r--r-- 1 PandaShen 197121 293 Jun 23 17:44 index.md ``` 在开头前十位中,`d`为文件夹,`-`为文件,后九位就代表当前用户、用户所属组和其他用户的权限位,按每三位划分,分别代表读(r)、写(w)和执行(x),- 代表没有当前位对应的权限 ### 标识位 标识位代表着对文件的操作方式,如可读、可写、即可读又可写等等,如下表所示: | 符号 | 含义 | | ---- | -------------------------------------------------------- | | r | 读取文件,如果文件不存在则抛出异常。 | | r+ | 读取并写入文件,如果文件不存在则抛出异常。 | | rs | 读取并写入文件,指示操作系统绕开本地文件系统缓存。 | | w | 写入文件,文件不存在会被创建,存在则清空后写入。 | | wx | 写入文件,排它方式打开。 | | w+ | 读取并写入文件,文件不存在则创建文件,存在则清空后写入。 | | wx+ | 和 w+ 类似,排他方式打开。 | | a | 追加写入,文件不存在则创建文件。 | | ax | 与 a 类似,排他方式打开。 | | a+ | 读取并追加写入,不存在则创建。 | | ax+ | 与 a+ 类似,排他方式打开。 | ### 文件描述为 fd 操作系统会为每个打开的文件分配一个名为文件描述符的数值标识,文件操作使用这些文件描述符来识别与追踪每个特定的文件 `Window `系统使用了一个不同但概念类似的机制来追踪资源,为方便用户,`NodeJS `抽象了不同操作系统间的差异,为所有打开的文件分配了数值的文件描述符 在 `NodeJS `中,每操作一个文件,文件描述符是递增的,文件描述符一般从 `3` 开始,因为前面有 `0`、`1`、`2`三个比较特殊的描述符,分别代表 `process.stdin`(标准输入)、`process.stdout`(标准输出)和 `process.stderr`(错误输出) ## 三、方法 下面针对`fs`模块常用的方法进行展开: - 文件读取 - 文件写入 - 文件追加写入 - 文件拷贝 - 创建目录 ### 文件读取 #### fs.readFileSync 同步读取,参数如下: - 第一个参数为读取文件的路径或文件描述符 - 第二个参数为 options,默认值为 null,其中有 encoding(编码,默认为 null)和 flag(标识位,默认为 r),也可直接传入 encoding 结果为返回文件的内容 ```js const fs = require("fs"); let buf = fs.readFileSync("1.txt"); let data = fs.readFileSync("1.txt", "utf8"); console.log(buf); // console.log(data); // Hello ``` #### fs.readFile 异步读取方法 `readFile` 与 `readFileSync` 的前两个参数相同,最后一个参数为回调函数,函数内有两个参数 `err`(错误)和 `data`(数据),该方法没有返回值,回调函数在读取文件成功后执行 ```js const fs = require("fs"); fs.readFile("1.txt", "utf8", (err, data) => { if(!err){ console.log(data); // Hello } }); ``` ### 文件写入 #### writeFileSync 同步写入,有三个参数: - 第一个参数为写入文件的路径或文件描述符 - 第二个参数为写入的数据,类型为 String 或 Buffer - 第三个参数为 options,默认值为 null,其中有 encoding(编码,默认为 utf8)、 flag(标识位,默认为 w)和 mode(权限位,默认为 0o666),也可直接传入 encoding ```js const fs = require("fs"); fs.writeFileSync("2.txt", "Hello world"); let data = fs.readFileSync("2.txt", "utf8"); console.log(data); // Hello world ``` #### writeFile 异步写入,`writeFile` 与 `writeFileSync` 的前三个参数相同,最后一个参数为回调函数,函数内有一个参数 `err`(错误),回调函数在文件写入数据成功后执行 ```js const fs = require("fs"); fs.writeFile("2.txt", "Hello world", err => { if (!err) { fs.readFile("2.txt", "utf8", (err, data) => { console.log(data); // Hello world }); } }); ``` ### 文件追加写入 #### appendFileSync 参数如下: - 第一个参数为写入文件的路径或文件描述符 - 第二个参数为写入的数据,类型为 String 或 Buffer - 第三个参数为 options,默认值为 null,其中有 encoding(编码,默认为 utf8)、 flag(标识位,默认为 a)和 mode(权限位,默认为 0o666),也可直接传入 encoding ```js const fs = require("fs"); fs.appendFileSync("3.txt", " world"); let data = fs.readFileSync("3.txt", "utf8"); ``` #### appendFile 异步追加写入方法 `appendFile` 与 `appendFileSync` 的前三个参数相同,最后一个参数为回调函数,函数内有一个参数 `err`(错误),回调函数在文件追加写入数据成功后执行 ```js const fs = require("fs"); fs.appendFile("3.txt", " world", err => { if (!err) { fs.readFile("3.txt", "utf8", (err, data) => { console.log(data); // Hello world }); } }); ``` ### 文件拷贝 #### copyFileSync 同步拷贝 ```js const fs = require("fs"); fs.copyFileSync("3.txt", "4.txt"); let data = fs.readFileSync("4.txt", "utf8"); console.log(data); // Hello world ``` #### copyFile 异步拷贝 ```js const fs = require("fs"); fs.copyFile("3.txt", "4.txt", () => { fs.readFile("4.txt", "utf8", (err, data) => { console.log(data); // Hello world }); }); ``` ### 创建目录 #### mkdirSync 同步创建,参数为一个目录的路径,没有返回值,在创建目录的过程中,必须保证传入的路径前面的文件目录都存在,否则会抛出异常 ```js // 假设已经有了 a 文件夹和 a 下的 b 文件夹 fs.mkdirSync("a/b/c") ``` #### mkdir 异步创建,第二个参数为回调函数 ```js fs.mkdir("a/b/c", err => { if (!err) console.log("创建成功"); }); ``` ## 参考文献 - http://nodejs.cn/api/fs.html - https://segmentfault.com/a/1190000019913303 ================================================ FILE: docs/NodeJS/global.md ================================================ # 面试官:说说 Node. js 有哪些全局对象? ![](https://static.vue-js.com/79c7b100-c2a3-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 在浏览器 `JavaScript` 中,通常` window` 是全局对象, 而 `Nodejs `中的全局对象是 `global` 在`NodeJS`里,是不可能在最外层定义一个变量,因为所有的用户代码都是当前模块的,只在当前模块里可用,但可以通过`exports`对象的使用将其传递给模块外部 所以,在`NodeJS`中,用`var`声明的变量并不属于全局的变量,只在当前模块生效 像上述的`global`全局对象则在全局作用域中,任何全局变量、函数、对象都是该对象的一个属性值 ## 二、有哪些 将全局对象分成两类: - 真正的全局对象 - 模块级别的全局变量 ### 真正的全局对象 下面给出一些常见的全局对象: - Class:Buffer - process - console - clearInterval、setInterval - clearTimeout、setTimeout - global #### Class:Buffer 可以处理二进制以及非`Unicode`编码的数据 在`Buffer`类实例化中存储了原始数据。`Buffer`类似于一个整数数组,在V8堆原始存储空间给它分配了内存 一旦创建了`Buffer`实例,则无法改变大小 #### process 进程对象,提供有关当前进程的信息和控制 包括在执行`node`程序进程时,如果需要传递参数,我们想要获取这个参数需要在`process`内置对象中 启动进程: ```cmd node index.js 参数1 参数2 参数3 ``` index.js文件如下: ```js process.argv.forEach((val, index) => { console.log(`${index}: ${val}`); }); ``` 输出如下: ```js /usr/local/bin/node /Users/mjr/work/node/process-args.js 参数1 参数2 参数3 ``` 除此之外,还包括一些其他信息如版本、操作系统等 ![](https://static.vue-js.com/85f473a0-c2a3-11eb-ab90-d9ae814b240d.png) #### console 用来打印`stdout`和`stderr` 最常用的输入内容的方式:console.log ```js console.log("hello"); ``` 清空控制台:console.clear ```js console.clear ``` 打印函数的调用栈:console.trace ```js function test() { demo(); } function demo() { foo(); } function foo() { console.trace(); } test(); ``` ![](https://static.vue-js.com/91b6dbb0-c2a3-11eb-85f6-6fac77c0c9b3.png) #### clearInterval、setInterval 设置定时器与清除定时器 ```js setInterval(callback, delay[, ...args]) ``` `callback`每`delay`毫秒重复执行一次 `clearInterval`则为对应发取消定时器的方法 #### clearTimeout、setTimeout 设置延时器与清除延时器 ```js setTimeout(callback,delay[,...args]) ``` `callback`在`delay`毫秒后执行一次 `clearTimeout`则为对应取消延时器的方法 #### global 全局命名空间对象,墙面讲到的`process`、`console`、`setTimeout`等都有放到`global`中 ```js console.log(process === global.process) // true ``` ### 模块级别的全局对象 这些全局对象是模块中的变量,只是每个模块都有,看起来就像全局变量,像在命令交互中是不可以使用,包括: - __dirname - __filename - exports - module - require #### __dirname 获取当前文件所在的路径,不包括后面的文件名 从 `/Users/mjr` 运行 `node example.js`: ```js console.log(__dirname); // 打印: /Users/mjr ``` #### __filename 获取当前文件所在的路径和文件名称,包括后面的文件名称 从 `/Users/mjr` 运行 `node example.js`: ```js console.log(__filename); // 打印: /Users/mjr/example.js ``` #### exports `module.exports` 用于指定一个模块所导出的内容,即可以通过 `require()` 访问的内容 ```js exports.name = name; exports.age = age; exports.sayHello = sayHello; ``` #### module 对当前模块的引用,通过`module.exports` 用于指定一个模块所导出的内容,即可以通过 `require()` 访问的内容 #### require 用于引入模块、 `JSON`、或本地文件。 可以从 `node_modules` 引入模块。 可以使用相对路径引入本地模块或` JSON `文件,路径会根据`__dirname`定义的目录名或当前工作目录进行处理 ## 参考文献 - http://nodejs.cn/api/globals.html - https://vue3js.cn/interview ================================================ FILE: docs/NodeJS/jwt.md ================================================ # 面试官:如何实现jwt鉴权机制?说说你的思路 ![](https://static.vue-js.com/efff62b0-cd88-11eb-ab90-d9ae814b240d.png) ## 一、是什么 JWT(JSON Web Token),本质就是一个字符串书写规范,如下图,作用是用来在用户和服务器之间传递安全可靠的信息 ![](https://static.vue-js.com/052904c0-cd89-11eb-ab90-d9ae814b240d.png) 在目前前后端分离的开发过程中,使用`token`鉴权机制用于身份验证是最常见的方案,流程如下: - 服务器当验证用户账号和密码正确的时候,给用户颁发一个令牌,这个令牌作为后续用户访问一些接口的凭证 - 后续访问会根据这个令牌判断用户时候有权限进行访问 `Token`,分成了三部分,头部(Header)、载荷(Payload)、签名(Signature),并以`.`进行拼接。其中头部和载荷都是以`JSON`格式存放数据,只是进行了编码 ![](https://static.vue-js.com/1175f990-cd89-11eb-85f6-6fac77c0c9b3.png) ### header 每个JWT都会带有头部信息,这里主要声明使用的算法。声明算法的字段名为`alg`,同时还有一个`typ`的字段,默认`JWT`即可。以下示例中算法为HS256 ```json { "alg": "HS256", "typ": "JWT" } ``` 因为JWT是字符串,所以我们还需要对以上内容进行Base64编码,编码后字符串如下: ```tex eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ``` ### payload 载荷即消息体,这里会存放实际的内容,也就是`Token`的数据声明,例如用户的`id`和`name`,默认情况下也会携带令牌的签发时间`iat`,通过还可以设置过期时间,如下: ```json { "sub": "1234567890", "name": "John Doe", "iat": 1516239022 } ``` 同样进行Base64编码后,字符串如下: ```tex eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ ``` ### Signature 签名是对头部和载荷内容进行签名,一般情况,设置一个`secretKey`,对前两个的结果进行`HMACSHA25`算法,公式如下: ```js Signature = HMACSHA256(base64Url(header)+.+base64Url(payload),secretKey) ``` 一旦前面两部分数据被篡改,只要服务器加密用的密钥没有泄露,得到的签名肯定和之前的签名不一致 ## 二、如何实现 `Token`的使用分成了两部分: - 生成token:登录成功的时候,颁发token - 验证token:访问某些资源或者接口时,验证token ### 生成 token 借助第三方库`jsonwebtoken`,通过`jsonwebtoken` 的 `sign` 方法生成一个 `token`: - 第一个参数指的是 Payload - 第二个是秘钥,服务端特有 - 第三个参数是 option,可以定义 token 过期时间 ```js const crypto = require("crypto"), jwt = require("jsonwebtoken"); // TODO:使用数据库 // 这里应该是用数据库存储,这里只是演示用 let userList = []; class UserController { // 用户登录 static async login(ctx) { const data = ctx.request.body; if (!data.name || !data.password) { return ctx.body = { code: "000002", message: "参数不合法" } } const result = userList.find(item => item.name === data.name && item.password === crypto.createHash('md5').update(data.password).digest('hex')) if (result) { // 生成token const token = jwt.sign( { name: result.name }, "test_token", // secret { expiresIn: 60 * 60 } // 过期时间:60 * 60 s ); return ctx.body = { code: "0", message: "登录成功", data: { token } }; } else { return ctx.body = { code: "000002", message: "用户名或密码错误" }; } } } module.exports = UserController; ``` 在前端接收到`token`后,一般情况会通过`localStorage`进行缓存,然后将`token`放到`HTTP `请求头`Authorization` 中,关于`Authorization` 的设置,前面要加上 Bearer ,注意后面带有空格 ```js axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); config.headers.common['Authorization'] = 'Bearer ' + token; // 留意这里的 Authorization return config; }) ``` ### 校验token 使用 `koa-jwt` 中间件进行验证,方式比较简单 ```js / 注意:放在路由前面 app.use(koajwt({ secret: 'test_token' }).unless({ // 配置白名单 path: [/\/api\/register/, /\/api\/login/] })) ``` - secret 必须和 sign 时候保持一致 - 可以通过 unless 配置接口白名单,也就是哪些 URL 可以不用经过校验,像登陆/注册都可以不用校验 - 校验的中间件需要放在需要校验的路由前面,无法对前面的 URL 进行校验 获取`token`用户的信息方法如下: ```js router.get('/api/userInfo',async (ctx,next) =>{ const authorization = ctx.header.authorization // 获取jwt const token = authorization.replace('Beraer ','') const result = jwt.verify(token,'test_token') ctx.body = result ``` 注意:上述的`HMA256`加密算法为单秘钥的形式,一旦泄露后果非常的危险 在分布式系统中,每个子系统都要获取到秘钥,那么这个子系统根据该秘钥可以发布和验证令牌,但有些服务器只需要验证令牌 这时候可以采用非对称加密,利用私钥发布令牌,公钥验证令牌,加密算法可以选择`RS256` ## 三、优缺点 优点: - json具有通用性,所以可以跨语言 - 组成简单,字节占用小,便于传输 - 服务端无需保存会话信息,很容易进行水平扩展 - 一处生成,多处使用,可以在分布式系统中,解决单点登录问题 - 可防护CSRF攻击 缺点: - payload部分仅仅是进行简单编码,所以只能用于存储逻辑必需的非敏感信息 - 需要保护好加密密钥,一旦泄露后果不堪设想 - 为避免token被劫持,最好使用https协议 ## 参考文献 - http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html - https://blog.wangjunfeng.com/post/golang-jwt/ - https://vue3js.cn/interview/ ================================================ FILE: docs/NodeJS/middleware.md ================================================ # 面试官:说说对中间件概念的理解,如何封装 node 中间件? ![](https://static.vue-js.com/614ae480-cce4-11eb-ab90-d9ae814b240d.png) ## 一、是什么 中间件(Middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的 在`NodeJS`中,中间件主要是指封装`http`请求细节处理的方法 例如在`express`、`koa`等`web`框架中,中间件的本质为一个回调函数,参数包含请求对象、响应对象和执行下一个中间件的函数 ![](https://static.vue-js.com/6a6ed3f0-cce4-11eb-85f6-6fac77c0c9b3.png) 在这些中间件函数中,我们可以执行业务逻辑代码,修改请求和响应对象、返回响应数据等操作 ## 二、封装 `koa`是基于`NodeJS`当前比较流行的`web`框架,本身支持的功能并不多,功能都可以通过中间件拓展实现。通过添加不同的中间件,实现不同的需求,从而构建一个 `Koa` 应用 `Koa` 中间件采用的是洋葱圈模型,每次执行下一个中间件传入两个参数: - ctx :封装了request 和 response 的变量 - next :进入下一个要执行的中间件的函数 ![](https://static.vue-js.com/7507b020-cce4-11eb-ab90-d9ae814b240d.png) 下面就针对`koa`进行中间件的封装: `Koa `的中间件就是函数,可以是` async` 函数,或是普通函数 ```js // async 函数 app.use(async (ctx, next) => { const start = Date.now(); await next(); const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); // 普通函数 app.use((ctx, next) => { const start = Date.now(); return next().then(() => { const ms = Date.now() - start; console.log(`${ctx.method} ${ctx.url} - ${ms}ms`); }); }); ``` 下面则通过中间件封装`http`请求过程中几个常用的功能: ### token校验 ```js module.exports = (options) => async (ctx, next) { try { // 获取 token const token = ctx.header.authorization if (token) { try { // verify 函数验证 token,并获取用户相关信息 await verify(token) } catch (err) { console.log(err) } } // 进入下一个中间件 await next() } catch (err) { console.log(err) } } ``` ### 日志模块 ```js const fs = require('fs') module.exports = (options) => async (ctx, next) => { const startTime = Date.now() const requestTime = new Date() await next() const ms = Date.now() - startTime; let logout = `${ctx.request.ip} -- ${requestTime} -- ${ctx.method} -- ${ctx.url} -- ${ms}ms`; // 输出日志文件 fs.appendFileSync('./log.txt', logout + '\n') } ``` `Koa`存在很多第三方的中间件,如`koa-bodyparser`、`koa-static`等 下面再来看看它们的大体的简单实现: ### koa-bodyparser `koa-bodyparser` 中间件是将我们的 `post` 请求和表单提交的查询字符串转换成对象,并挂在 `ctx.request.body` 上,方便我们在其他中间件或接口处取值 ```js // 文件:my-koa-bodyparser.js const querystring = require("querystring"); module.exports = function bodyParser() { return async (ctx, next) => { await new Promise((resolve, reject) => { // 存储数据的数组 let dataArr = []; // 接收数据 ctx.req.on("data", data => dataArr.push(data)); // 整合数据并使用 Promise 成功 ctx.req.on("end", () => { // 获取请求数据的类型 json 或表单 let contentType = ctx.get("Content-Type"); // 获取数据 Buffer 格式 let data = Buffer.concat(dataArr).toString(); if (contentType === "application/x-www-form-urlencoded") { // 如果是表单提交,则将查询字符串转换成对象赋值给 ctx.request.body ctx.request.body = querystring.parse(data); } else if (contentType === "applaction/json") { // 如果是 json,则将字符串格式的对象转换成对象赋值给 ctx.request.body ctx.request.body = JSON.parse(data); } // 执行成功的回调 resolve(); }); }); // 继续向下执行 await next(); }; }; ``` ### koa-static `koa-static` 中间件的作用是在服务器接到请求时,帮我们处理静态文件 ```js const fs = require("fs"); const path = require("path"); const mime = require("mime"); const { promisify } = require("util"); // 将 stat 和 access 转换成 Promise const stat = promisify(fs.stat); const access = promisify(fs.access) module.exports = function (dir) { return async (ctx, next) => { // 将访问的路由处理成绝对路径,这里要使用 join 因为有可能是 / let realPath = path.join(dir, ctx.path); try { // 获取 stat 对象 let statObj = await stat(realPath); // 如果是文件,则设置文件类型并直接响应内容,否则当作文件夹寻找 index.html if (statObj.isFile()) { ctx.set("Content-Type", `${mime.getType()};charset=utf8`); ctx.body = fs.createReadStream(realPath); } else { let filename = path.join(realPath, "index.html"); // 如果不存在该文件则执行 catch 中的 next 交给其他中间件处理 await access(filename); // 存在设置文件类型并响应内容 ctx.set("Content-Type", "text/html;charset=utf8"); ctx.body = fs.createReadStream(filename); } } catch (e) { await next(); } } } ``` ## 三、总结 在实现中间件时候,单个中间件应该足够简单,职责单一,中间件的代码编写应该高效,必要的时候通过缓存重复获取数据 `koa`本身比较简洁,但是通过中间件的机制能够实现各种所需要的功能,使得`web`应用具备良好的可拓展性和组合性 通过将公共逻辑的处理编写在中间件中,可以不用在每一个接口回调中做相同的代码编写,减少了冗杂代码,过程就如装饰者模式 ## 参考文献 - https://segmentfault.com/a/1190000017897279 - https://www.jianshu.com/p/81b6ebc0dd85 - https://baike.baidu.com/item/%E4%B8%AD%E9%97%B4%E4%BB%B6 ================================================ FILE: docs/NodeJS/nodejs.md ================================================ # 面试官:说说你对Node.js 的理解?优缺点?应用场景? ![](https://static.vue-js.com/b565d240-c1e6-11eb-ab90-d9ae814b240d.png) ## 一、是什么 `Node.js` 是一个开源与跨平台的 `JavaScript` 运行时环境 在浏览器外运行 V8 JavaScript 引擎(Google Chrome 的内核),利用事件驱动、非阻塞和异步输入输出模型等技术提高性能 可以理解为 `Node.js` 就是一个服务器端的、非阻塞式I/O的、事件驱动的`JavaScript`运行环境 ### 非阻塞异步 `Nodejs`采用了非阻塞型`I/O`机制,在做`I/O`操作的时候不会造成任何的阻塞,当完成之后,以时间的形式通知执行操作 例如在执行了访问数据库的代码之后,将立即转而执行其后面的代码,把数据库返回结果的处理代码放在回调函数中,从而提高了程序的执行效率 ### 事件驱动 事件驱动就是当进来一个新的请求的时,请求将会被压入一个事件队列中,然后通过一个循环来检测队列中的事件状态变化,如果检测到有状态变化的事件,那么就执行该事件对应的处理代码,一般都是回调函数 比如读取一个文件,文件读取完毕后,就会触发对应的状态,然后通过对应的回调函数来进行处理 ![](https://static.vue-js.com/a7729590-c1e8-11eb-ab90-d9ae814b240d.png) ## 二、优缺点 优点: - 处理高并发场景性能更佳 - 适合I/O密集型应用,值的是应用在运行极限时,CPU占用率仍然比较低,大部分时间是在做 I/O硬盘内存读写操作 因为`Nodejs`是单线程,带来的缺点有: - 不适合CPU密集型应用 - 只支持单核CPU,不能充分利用CPU - 可靠性低,一旦代码某个环节崩溃,整个系统都崩溃 ## 三、应用场景 借助`Nodejs`的特点和弊端,其应用场景分类如下: - 善于`I/O`,不善于计算。因为Nodejs是一个单线程,如果计算(同步)太多,则会阻塞这个线程 - 大量并发的I/O,应用程序内部并不需要进行非常复杂的处理 - 与 websocket 配合,开发长连接的实时交互应用程序 具体场景可以表现为如下: - 第一大类:用户表单收集系统、后台管理系统、实时交互系统、考试系统、联网软件、高并发量的web应用程序 - 第二大类:基于web、canvas等多人联网游戏 - 第三大类:基于web的多人实时聊天客户端、聊天室、图文直播 - 第四大类:单页面浏览器应用程序 - 第五大类:操作数据库、为前端和移动端提供基于`json`的API 其实,`Nodejs`能实现几乎一切的应用,只考虑适不适合使用它 ## 参考文献 - http://nodejs.cn/ - https://segmentfault.com/a/1190000019854308 - https://segmentfault.com/a/1190000005173218 ================================================ FILE: docs/NodeJS/paging.md ================================================ # 面试官:如果让你来设计一个分页功能, 你会怎么设计? 前后端如何交互? ![](https://static.vue-js.com/54b0a390-cf14-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 在我们做数据查询的时候,如果数据量很大,比如几万条数据,放在一个页面显示的话显然不友好,这时候就需要采用分页显示的形式,如每次只显示10条数据 ![](https://static.vue-js.com/6070e8c0-cf14-11eb-85f6-6fac77c0c9b3.png) 要实现分页功能,实际上就是从结果集中显示第1~10条记录作为第1页,显示第11~20条记录作为第2页,以此类推 因此,分页实际上就是从结果集中截取出第M~N条记录 ## 二、如何实现 前端实现分页功能,需要后端返回必要的数据,如总的页数,总的数据量,当前页,当前的数据 ```js { "totalCount": 1836, // 总的条数 "totalPages": 92, // 总页数 "currentPage": 1 // 当前页数 "data": [ // 当前页的数据 { ... } ] ``` 后端采用`mysql`作为数据的持久性存储 前端向后端发送目标的页码`page`以及每页显示数据的数量`pageSize`,默认情况每次取10条数据,则每一条数据的起始位置`start`为: ```js const start = (page - 1) * pageSize ``` 当确定了`limit`和`start`的值后,就能够确定`SQL`语句: ```JS const sql = `SELECT * FROM record limit ${pageSize} OFFSET ${start};` ``` 上诉`SQL`语句表达的意思为:截取从`start`到`start`+`pageSize`之间(左闭右开)的数据 关于查询数据总数的`SQL`语句为,`record`为表名: ```mysql SELECT COUNT(*) FROM record ``` 因此后端的处理逻辑为: - 获取用户参数页码数page和每页显示的数目 pageSize ,其中page 是必须传递的参数,pageSize为可选参数,默认为10 - 编写 SQL 语句,利用 limit 和 OFFSET 关键字进行分页查询 - 查询数据库,返回总数据量、总页数、当前页、当前页数据给前端 代码如下所示: ```js router.all('/api', function (req, res, next) { var param = ''; // 获取参数 if (req.method == "POST") { param = req.body; } else { param = req.query || req.params; } if (param.page == '' || param.page == null || param.page == undefined) { res.end(JSON.stringify({ msg: '请传入参数page', status: '102' })); return; } const pageSize = param.pageSize || 10; const start = (param.page - 1) * pageSize; const sql = `SELECT * FROM record limit ${pageSize} OFFSET ${start};` pool.getConnection(function (err, connection) { if (err) throw err; connection.query(sql, function (err, results) { connection.release(); if (err) { throw err } else { // 计算总页数 var allCount = results[0][0]['COUNT(*)']; var allPage = parseInt(allCount) / 20; var pageStr = allPage.toString(); // 不能被整除 if (pageStr.indexOf('.') > 0) { allPage = parseInt(pageStr.split('.')[0]) + 1; } var list = results[1]; res.end(JSON.stringify({ msg: '操作成功', status: '200', totalPages: allPage, currentPage: param.page, totalCount: allCount, data: list })); } }) }) }); ``` ## 三、总结 通过上面的分析,可以看到分页查询的关键在于,要首先确定每页显示的数量`pageSize`,然后根据当前页的索引`pageIndex`(从1开始),确定`LIMIT`和`OFFSET`应该设定的值: - LIMIT 总是设定为 pageSize - OFFSET 计算公式为 pageSize * (pageIndex - 1) 确定了这两个值,就能查询出第 `N`页的数据 ## 参考文献 - https://www.liaoxuefeng.com/wiki/1177760294764384/1217864791925600 - https://vue3js.cn/interview/ ================================================ FILE: docs/NodeJS/performance.md ================================================ # 面试官:Node性能如何进行监控以及优化? ![](https://static.vue-js.com/bb37dae0-d179-11eb-ab90-d9ae814b240d.png) ## 一、 是什么 `Node`作为一门服务端语言,性能方面尤为重要,其衡量指标一般有如下: - CPU - 内存 - I/O - 网络 ### CPU 主要分成了两部分: - CPU负载:在某个时间段内,占用以及等待CPU的进程总数 - CPU使用率:CPU时间占用状况,等于 1 - 空闲CPU时间(idle time) / CPU总时间 这两个指标都是用来评估系统当前CPU的繁忙程度的量化指标 `Node`应用一般不会消耗很多的`CPU`,如果`CPU`占用率高,则表明应用存在很多同步操作,导致异步任务回调被阻塞 ### 内存指标 内存是一个非常容易量化的指标。 内存占用率是评判一个系统的内存瓶颈的常见指标。 对于Node来说,内部内存堆栈的使用状态也是一个可以量化的指标 ```js // /app/lib/memory.js const os = require('os'); // 获取当前Node内存堆栈情况 const { rss, heapUsed, heapTotal } = process.memoryUsage(); // 获取系统空闲内存 const sysFree = os.freemem(); // 获取系统总内存 const sysTotal = os.totalmem(); module.exports = { memory: () => { return { sys: 1 - sysFree / sysTotal, // 系统内存占用率 heap: heapUsed / headTotal, // Node堆内存占用率 node: rss / sysTotal, // Node占用系统内存的比例 } } } ``` - rss:表示node进程占用的内存总量。 - heapTotal:表示堆内存的总量。 - heapUsed:实际堆内存的使用量。 - external :外部程序的内存使用量,包含Node核心的C++程序的内存使用量 在`Node`中,一个进程的最大内存容量为1.5GB。因此我们需要减少内存泄露 ### 磁盘 I/O 硬盘的` IO` 开销是非常昂贵的,硬盘 IO 花费的 CPU 时钟周期是内存的 164000 倍 内存 `IO `比磁盘` IO` 快非常多,所以使用内存缓存数据是有效的优化方法。常用的工具如 `redis`、`memcached `等 并不是所有数据都需要缓存,访问频率高,生成代价比较高的才考虑是否缓存,也就是说影响你性能瓶颈的考虑去缓存,并且而且缓存还有缓存雪崩、缓存穿透等问题要解决 ## 二、如何监控 关于性能方面的监控,一般情况都需要借助工具来实现 这里采用`Easy-Monitor 2.0`,其是轻量级的 `Node.js` 项目内核性能监控 + 分析工具,在默认模式下,只需要在项目入口文件 `require` 一次,无需改动任何业务代码即可开启内核级别的性能监控分析 使用方法如下: 在你的项目入口文件中按照如下方式引入,当然请传入你的项目名称: ```js const easyMonitor = require('easy-monitor'); easyMonitor('你的项目名称'); ``` 打开你的浏览器,访问 `http://localhost:12333` ,即可看到进程界面 关于定制化开发、通用配置项以及如何动态更新配置项详见官方文档 ## 三、如何优化 关于`Node`的性能优化的方式有: - 使用最新版本Node.js - 正确使用流 Stream - 代码层面优化 - 内存管理优化 ### 使用最新版本Node.js 每个版本的性能提升主要来自于两个方面: - V8 的版本更新 - Node.js 内部代码的更新优化 ### 正确使用流 Stream 在`Node`中,很多对象都实现了流,对于一个大文件可以通过流的形式发送,不需要将其完全读入内存 ```js const http = require('http'); const fs = require('fs'); // bad http.createServer(function (req, res) { fs.readFile(__dirname + '/data.txt', function (err, data) { res.end(data); }); }); // good http.createServer(function (req, res) { const stream = fs.createReadStream(__dirname + '/data.txt'); stream.pipe(res); }); ``` ### 代码层面优化 合并查询,将多次查询合并一次,减少数据库的查询次数 ```js // bad for user_id in userIds let account = user_account.findOne(user_id) // good const user_account_map = {} // 注意这个对象将会消耗大量内存。 user_account.find(user_id in user_ids).forEach(account){ user_account_map[account.user_id] = account } for user_id in userIds var account = user_account_map[user_id] ``` ### 内存管理优化 在 V8 中,主要将内存分为新生代和老生代两代: - 新生代:对象的存活时间较短。新生对象或只经过一次垃圾回收的对象 - 老生代:对象存活时间较长。经历过一次或多次垃圾回收的对象 若新生代内存空间不够,直接分配到老生代 通过减少内存占用,可以提高服务器的性能。如果有内存泄露,也会导致大量的对象存储到老生代中,服务器性能会大大降低 如下面情况: ```js const buffer = fs.readFileSync(__dirname + '/source/index.htm'); app.use( mount('/', async (ctx) => { ctx.status = 200; ctx.type = 'html'; ctx.body = buffer; leak.push(fs.readFileSync(__dirname + '/source/index.htm')); }) ); const leak = []; ``` `leak`的内存非常大,造成内存泄露,应当避免这样的操作,通过减少内存使用,是提高服务性能的手段之一 而节省内存最好的方式是使用池,其将频用、可复用对象存储起来,减少创建和销毁操作 例如有个图片请求接口,每次请求,都需要用到类。若每次都需要重新new这些类,并不是很合适,在大量请求时,频繁创建和销毁这些类,造成内存抖动 使用对象池的机制,对这种频繁需要创建和销毁的对象保存在一个对象池中。每次用到该对象时,就取对象池空闲的对象,并对它进行初始化操作,从而提高框架的性能 ## 参考文献 - https://segmentfault.com/a/1190000039327565 - https://zhuanlan.zhihu.com/p/50055740 - https://segmentfault.com/a/1190000010231628 ================================================ FILE: docs/NodeJS/process.md ================================================ # 面试官:说说对 Node 中的 process 的理解?有哪些常用方法? ![](https://static.vue-js.com/4f7866b0-c2b2-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 `process` 对象是一个全局变量,提供了有关当前 `Node.js `进程的信息并对其进行控制,作为一个全局变量 我们都知道,进程计算机系统进行资源分配和调度的基本单位,是操作系统结构的基础,是线程的容器 当我们启动一个`js`文件,实际就是开启了一个服务进程,每个进程都拥有自己的独立空间地址、数据栈,像另一个进程无法访问当前进程的变量、数据结构,只有数据通信后,进程之间才可以数据共享 由于`JavaScript`是一个单线程语言,所以通过`node xxx`启动一个文件后,只有一条主线程 ## 二、属性与方法 关于`process`常见的属性有如下: - process.env:环境变量,例如通过 `process.env.NODE_ENV 获取不同环境项目配置信息 - process.nextTick:这个在谈及 `EventLoop` 时经常为会提到 - process.pid:获取当前进程id - process.ppid:当前进程对应的父进程 - process.cwd():获取当前进程工作目录, - process.platform:获取当前进程运行的操作系统平台 - process.uptime():当前进程已运行时间,例如:pm2 守护进程的 uptime 值 - 进程事件: process.on(‘uncaughtException’,cb) 捕获异常信息、 process.on(‘exit’,cb)进程推出监听 - 三个标准流: process.stdout 标准输出、 process.stdin 标准输入、 process.stderr 标准错误输出 - process.title 指定进程名称,有的时候需要给进程指定一个名称 下面再稍微介绍下某些方法的使用: ### process.cwd() 返回当前 `Node `进程执行的目录 一个` Node` 模块 `A` 通过 NPM 发布,项目 `B` 中使用了模块 `A`。在 `A` 中需要操作 `B` 项目下的文件时,就可以用 `process.cwd()` 来获取 `B` 项目的路径 ### process.argv 在终端通过 Node 执行命令的时候,通过 `process.argv` 可以获取传入的命令行参数,返回值是一个数组: - 0: Node 路径(一般用不到,直接忽略) - 1: 被执行的 JS 文件路径(一般用不到,直接忽略) - 2~n: 真实传入命令的参数 所以,我们只要从 `process.argv[2]` 开始获取就好了 ```js const args = process.argv.slice(2); ``` ### process.env 返回一个对象,存储当前环境相关的所有信息,一般很少直接用到。 一般我们会在 `process.env` 上挂载一些变量标识当前的环境。比如最常见的用 `process.env.NODE_ENV` 区分 `development` 和 `production` 在 `vue-cli` 的源码中也经常会看到 `process.env.VUE_CLI_DEBUG` 标识当前是不是 `DEBUG` 模式 ### process.nextTick() 我们知道`NodeJs`是基于事件轮询,在这个过程中,同一时间只会处理一件事情 在这种处理模式下,`process.nextTick()`就是定义出一个动作,并且让这个动作在下一个事件轮询的时间点上执行 例如下面例子将一个`foo`函数在下一个时间点调用 ```js function foo() { console.error('foo'); } process.nextTick(foo); console.error('bar'); ``` 输出结果为`bar`、`foo` 虽然下述方式也能实现同样效果: ```js setTimeout(foo, 0); console.log('bar'); ``` 两者区别在于: - process.nextTick()会在这一次event loop的call stack清空后(下一次event loop开始前)再调用callback - setTimeout()是并不知道什么时候call stack清空的,所以何时调用callback函数是不确定的 ### 参考文献 - http://nodejs.cn/api/process.html - https://vue3js.cn/interview/ ================================================ FILE: docs/NodeJS/require_order.md ================================================ # 面试官:说说 Node 文件查找的优先级以及 Require 方法的文件查找策略? ![](https://static.vue-js.com/15913530-c9ba-11eb-ab90-d9ae814b240d.png) ## 一、模块规范 `NodeJS`对`CommonJS`进行了支持和实现,让我们在开发`node`的过程中可以方便的进行模块化开发: - 在Node中每一个js文件都是一个单独的模块 - 模块中包括CommonJS规范的核心变量:exports、module.exports、require - 通过上述变量进行模块化开发 而模块化的核心是导出与导入,在`Node`中通过`exports`与`module.exports`负责对模块中的内容进行导出,通过`require`函数导入其他模块(自定义模块、系统模块、第三方库模块)中的内容 ## 二、查找策略 `require`方法接收一下几种参数的传递: - 原生模块:http、fs、path等 - 相对路径的文件模块:./mod或../mod - 绝对路径的文件模块:/pathtomodule/mod - 目录作为模块:./dirname - 非原生模块的文件模块:mod `require`参数较为简单,但是内部的加载却是十分复杂的,其加载优先级也各自不同,如下图: ![](https://static.vue-js.com/33ae8ef0-c9ba-11eb-85f6-6fac77c0c9b3.png) 从上图可以看见,文件模块存在缓存区,寻找模块路径的时候都会优先从缓存中加载已经存在的模块 ### 原生模块 而像原生模块这些,通过`require `方法在解析文件名之后,优先检查模块是否在原生模块列表中,如果在则从原生模块中加载 ### 绝对路径、相对路径 如果`require`绝对路径的文件,则直接查找对应的路径,速度最快 相对路径的模块则相对于当前调用`require`的文件去查找 如果按确切的文件名没有找到模块,则 `NodeJs` 会尝试带上 `.js`、`.json `或 `.node `拓展名再加载 ### 目录作为模块 默认情况是根据根目录中`package.json`文件的`main`来指定目录模块,如: ```json { "name" : "some-library", "main" : "main.js" } ``` 如果这是在` ./some-library node_modules `目录中,则 `require('./some-library')` 会试图加载 `./some-library/main.js` 如果目录里没有 `package.json`文件,或者 `main`入口不存在或无法解析,则会试图加载目录下的 `index.js` 或 `index.node` 文件 ### 非原生模块 在每个文件中都存在`module.paths`,表示模块的搜索路径,`require`就是根据其来寻找文件 在`window`下输出如下: ```js [ 'c:\\nodejs\\node_modules', 'c:\\node_modules' ] ``` 可以看出`module path`的生成规则为:从当前文件目录开始查找`node_modules`目录;然后依次进入父目录,查找父目录下的`node_modules`目录,依次迭代,直到根目录下的`node_modules`目录 当都找不到的时候,则会从系统`NODE_PATH`环境变量查找 #### 举个例子: 如果在`/home/ry/projects/foo.js`文件里调用了 `require('bar.js')`,则 Node.js 会按以下顺序查找: - /home/ry/projects/node_modules/bar.js - /home/ry/node_modules/bar.js - /home/node_modules/bar.js - /node_modules/bar.js 这使得程序本地化它们的依赖,避免它们产生冲突 ## 三、总结 通过上面模块的文件查找策略之后,总结下文件查找的优先级: - 缓存的模块优先级最高 - 如果是内置模块,则直接返回,优先级仅次缓存的模块 - 如果是绝对路径 / 开头,则从根目录找 - 如果是相对路径 ./开头,则从当前require文件相对位置找 - 如果文件没有携带后缀,先从js、json、node按顺序查找 - 如果是目录,则根据 package.json的main属性值决定目录下入口文件,默认情况为 index.js - 如果文件为第三方模块,则会引入 node_modules 文件,如果不在当前仓库文件中,则自动从上级递归查找,直到根目录 ## 参考文献 - http://nodejs.cn/api/modules.html#modules_file_modules - https://blog.csdn.net/qq_36801250/article/details/106352686 - https://www.cnblogs.com/samve/p/10805908.html ================================================ FILE: docs/README.md ================================================

Statr vue es6 css javascript

本仓库为语音打卡社群(JS每日一题)维护的前端面试题库,包含不限于Vue面试题,React面试题,JS面试题,HTTP面试题,工程化面试题,CSS面试题,算法面试题,大厂面试题,高频面试题 同时我们提供更好阅读体验的在线版本,[点这里](https://vue3js.cn/interview) 点击[@AD](https://vue3js.cn/assets/img/18141744641588_.pic.jpg)添加好友,获取最新面试真题,加前端500人大群 避免失联可以先点star PS: 仓库每周一更,想要及时收到推送,交流想法的同学,可以点击[@公众号:JS每日一题](https://static.vue-js.com/b4b71a30-443b-11eb-85f6-6fac77c0c9b3.png)关注我们 ## 🤠 为什么要做这样的一个仓库 最开始是没有想要弄仓库的,我们通过微信群语音的形式进行每天一次的打卡,次日再通过[公众号](https://static.vue-js.com/b4b71a30-443b-11eb-85f6-6fac77c0c9b3.png)图文形式的题解推送至群内供大家复盘总结 随着时间的推移,题也积累的越来越多,再去通过公众号检索信息效率会明显降低 这个时候我们就想着通过开源的形式,将我们总结好的文章以多种形态的呈现方式帮助到更多需要的人 ## 🤡 这个仓库主要内容是? 仓库目前主题以前端面试题为主,后续可能会扩展更多的分支 在这个信息爆炸,前端生态百花齐放的时代,我们每找寻一个答案都要换一种形式,这简直太奢侈了 我们的目标是做最全最好最有质量前端面试仓库,用心收录大厂面试题,高频面试题,知识点面试题,用心做好每一道题值得参考的题解 ## 🤡 你们的本质就是面试题吗? 不是,一直在强调语音社群,这个仓库也仅且是一个附属品 我们只是以面试题的形式作为切入点,根据面试题去反推学习更具体的知识点,绝不仅限于应对面试,要真真切切在学习过程中得到蜕变 社群内每天的打卡都是一次思维整理输出的训练,将自己想要表达的内容放入框架中再进行输出,让我们的表达更简洁更系统 它的背后是思考,总结,更是坚持 在过程中看清自已,在结果中遇见自己,我们都值得有更美好的未来 再晒晒平常我们的样子

如果看到这里你也有兴趣,欢迎扫描下方二维码加入我们,持续成长

## 🤡 内容 点击[@公众号:JS每日一题](https://static.vue-js.com/b4b71a30-443b-11eb-85f6-6fac77c0c9b3.png)扫码关注,跟踪我们最新动态

Vue系列

- [面试官:说说你对vue的理解?](https://github.com/febobo/web-interview/issues/1) - [面试官:说说你对双向绑定的理解?](https://github.com/febobo/web-interview/issues/2) - [面试官:说说你对SPA(单页应用)的理解?](https://github.com/febobo/web-interview/issues/3) - [面试官:Vue中的v-show和v-if怎么理解?](https://github.com/febobo/web-interview/issues/4) - [面试官:Vue实例挂载的过程中发生了什么?](https://github.com/febobo/web-interview/issues/5) - [面试官:说说你对Vue生命周期的理解?](https://github.com/febobo/web-interview/issues/6) - [面试官:为什么Vue中的v-if和v-for不建议一起用?](https://github.com/febobo/web-interview/issues/7) - [面试官:SPA(单页应用)首屏加载速度慢怎么解决??](https://github.com/febobo/web-interview/issues/8) - [面试官:为什么data属性是一个函数而不是一个对象?](https://github.com/febobo/web-interview/issues/9) - [面试官:Vue中给对象添加新属性界面不刷新?](https://github.com/febobo/web-interview/issues/10) - [面试官:Vue中组件和插件有什么区别](https://github.com/febobo/web-interview/issues/11) - [面试官:Vue组件间通信方式都有哪些?](https://github.com/febobo/web-interview/issues/12) - [面试官:说说你对nexttick的理解?](https://github.com/febobo/web-interview/issues/14) - [面试官:说说你对vue的mixin的理解,有什么应用场景?](https://github.com/febobo/web-interview/issues/15) - [面试官:说说你对slot的理解?slot使用场景有哪些?](https://github.com/febobo/web-interview/issues/16) - [面试官:Vue.observable你有了解过吗?说说看](https://github.com/febobo/web-interview/issues/17) - [面试官:你知道vue中key的原理吗?说说你对它的理解?](https://github.com/febobo/web-interview/issues/18) - [面试官:怎么缓存当前的组件?缓存后怎么更新?说说你对keep-alive的理解是什么?](https://github.com/febobo/web-interview/issues/19) - [面试官:Vue常用的修饰符有哪些?有什么应用场景?](https://github.com/febobo/web-interview/issues/20) - [面试官:你有写过自定义指令吗?自定义指令的应用场景有哪些?](https://github.com/febobo/web-interview/issues/21) - [面试官:Vue中的过滤器了解吗?过滤器的应用场景有哪些?](https://github.com/febobo/web-interview/issues/22) - [面试官:什么是虚拟DOM?如何实现一个虚拟DOM?说说你的思路](https://github.com/febobo/web-interview/issues/23) - [面试官:了解过vue中的diff算法吗?说说看](https://github.com/febobo/web-interview/issues/24) - [面试官:Vue项目中有封装过axios吗?怎么封装的?](https://github.com/febobo/web-interview/issues/25) - [面试官:你了解Axios的原理吗?有看过它的源码吗?](https://github.com/febobo/web-interview/issues/26) - [面试官:SSR解决了什么问题?有做过SSR吗?你是怎么做的?](https://github.com/febobo/web-interview/issues/27) - [面试官:说下你的Vue项目的目录结构,如果是大型项目你该怎么划分结构和划分组件呢?](https://github.com/febobo/web-interview/issues/28) - [面试官:Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做?](https://github.com/febobo/web-interview/issues/29) - [面试官:跨域是什么?Vue项目中你是如何解决跨域的呢?](https://github.com/febobo/web-interview/issues/30) - [面试官:Vue项目如何部署?有遇到布署服务器后刷新404问题吗?](https://github.com/febobo/web-interview/issues/31) - [面试官:你是怎么处理vue项目中的错误的?](https://github.com/febobo/web-interview/issues/32) - [面试官:Vue3有了解过吗?能说说跟Vue2的区别吗?](https://github.com/febobo/web-interview/issues/33)

Vue3系列

- [面试官:Vue3.0的设计目标是什么?做了哪些优化?](https://github.com/febobo/web-interview/issues/45) - [面试官:Vue3.0 性能提升主要是通过哪几方面体现的?](https://github.com/febobo/web-interview/issues/46) - [面试官:Vue3.0里为什么要用 Proxy API 替代 defineProperty API ?](https://github.com/febobo/web-interview/issues/47) - [面试官:Vue3.0 所采用的 Composition Api 与 Vue2.x 使用的 Options Api 有什么不同?](https://github.com/febobo/web-interview/issues/48) - [面试官:说说Vue 3.0中Treeshaking特性?举例说明一下?](https://github.com/febobo/web-interview/issues/67) - [面试官:用Vue3.0 写过组件吗?如果想实现一个 Modal你会怎么设计?](https://github.com/febobo/web-interview/issues/50)

ES6系列

- [面试官:说说var、let、const之间的区别](https://github.com/febobo/web-interview/issues/34) - [面试官:ES6中数组新增了哪些扩展?](https://github.com/febobo/web-interview/issues/35) - [面试官:ES6中对象新增了哪些扩展?](https://github.com/febobo/web-interview/issues/36) - [面试官:ES6中函数新增了哪些扩展?](https://github.com/febobo/web-interview/issues/37) - [面试官:ES6中新增的Set、Map两种数据结构怎么理解?](https://github.com/febobo/web-interview/issues/38) - [面试官:你是怎么理解ES6中 Promise的?使用场景?](https://github.com/febobo/web-interview/issues/40) - [面试官:怎么理解ES6中 Generator的?使用场景?](https://github.com/febobo/web-interview/issues/41) - [面试官:你是怎么理解ES6中Proxy的?使用场景?](https://github.com/febobo/web-interview/issues/42) - [面试官:你是怎么理解ES6中Module的?使用场景?](https://github.com/febobo/web-interview/issues/43) - [面试官:你是怎么理解ES6中 Decorator 的?使用场景?](https://github.com/febobo/web-interview/issues/44)

Javascript系列

- [面试官:说说Javascript中的数据类型?区别?](https://github.com/febobo/web-interview/issues/51) - [面试官:Javscript数组的常用方法有哪些?](https://github.com/febobo/web-interview/issues/52) - [面试官:Javascript字符串的常用方法有哪些?](https://github.com/febobo/web-interview/issues/53) - [面试官:谈谈 Javascript 中的类型转换机制](https://github.com/febobo/web-interview/issues/54) - [面试官:== 和 ===区别,分别在什么情况使用](https://github.com/febobo/web-interview/issues/55) - [面试官:深拷贝浅拷贝的区别?如何实现一个深拷贝?](https://github.com/febobo/web-interview/issues/56) - [面试官:说说你对闭包的理解](https://github.com/febobo/web-interview/issues/57) - [面试官:说说你对作用域链的理解](https://github.com/febobo/web-interview/issues/58) - [面试官:JavaScript原型,原型链 ? 有什么特点?](https://github.com/febobo/web-interview/issues/59) - [面试官:Javascript如何实现继承?](https://github.com/febobo/web-interview/issues/60) - [面试官:谈谈this对象的理解](https://github.com/febobo/web-interview/issues/62) - [面试官:JavaScript中执行上下文和执行栈是什么?](https://github.com/febobo/web-interview/issues/63) - [面试官:说说JavaScript中的事件模型](https://github.com/febobo/web-interview/issues/64) - [面试官:typeof 与 instanceof 区别](https://github.com/febobo/web-interview/issues/65) - [面试官:解释下什么是事件代理?应用场景?](https://github.com/febobo/web-interview/issues/66) - [面试官:说说new操作符具体干了什么?](https://github.com/febobo/web-interview/issues/69) - [面试官:ajax原理是什么?如何实现?](https://github.com/febobo/web-interview/issues/70) - [面试官:bind、call、apply 区别?如何实现一个bind?](https://github.com/febobo/web-interview/issues/71) - [面试官:说说你对正则表达式的理解?应用场景?](https://github.com/febobo/web-interview/issues/72) - [面试官:说说你对事件循环的理解](https://github.com/febobo/web-interview/issues/73) - [面试官:DOM常见的操作有哪些?](https://github.com/febobo/web-interview/issues/75) - [面试官:说说你对BOM的理解,常见的BOM对象你了解哪些?](https://github.com/febobo/web-interview/issues/76) - [面试官:举例说明你对尾递归的理解,有哪些应用场景](https://github.com/febobo/web-interview/issues/77) - [面试官:说说 JavaScript 中内存泄漏的几种情况?](https://github.com/febobo/web-interview/issues/78) - [面试官:Javascript本地存储的方式有哪些?区别及应用场景?](https://github.com/febobo/web-interview/issues/79) - [面试官:说说你对函数式编程的理解?优缺点?](https://github.com/febobo/web-interview/issues/80) - [面试官:Javascript中如何实现函数缓存?函数缓存有哪些应用场景?](https://github.com/febobo/web-interview/issues/81) - [面试官:说说 Javascript 数字精度丢失的问题,如何解决?](https://github.com/febobo/web-interview/issues/82) - [面试官:什么是防抖和节流?有什么区别?如何实现?](https://github.com/febobo/web-interview/issues/83) - [面试官:如何判断一个元素是否在可视区域中?](https://github.com/febobo/web-interview/issues/84) - [面试官:大文件上传如何做断点续传?](https://github.com/febobo/web-interview/issues/89) - [面试官:如何实现上拉加载,下拉刷新?](https://github.com/febobo/web-interview/issues/90) - [面试官:什么是单点登录?如何实现?](https://github.com/febobo/web-interview/issues/91) - [面试官:web常见的攻击方式有哪些?如何防御?](https://github.com/febobo/web-interview/issues/92)

CSS系列

- [面试官:说说你对盒子模型的理解?](https://github.com/febobo/web-interview/issues/93) - [面试官:css选择器有哪些?优先级?哪些属性可以继承?](https://github.com/febobo/web-interview/issues/95) - [面试官:说说em/px/rem/vh/vw区别?](https://github.com/febobo/web-interview/issues/96) - [面试官:说说设备像素、css像素、设备独立像素、dpr、ppi 之间的区别?](https://github.com/febobo/web-interview/issues/97) - [面试官:css中,有哪些方式可以隐藏页面元素?区别?](https://github.com/febobo/web-interview/issues/98) - [面试官:谈谈你对BFC的理解?](https://github.com/febobo/web-interview/issues/99) - [面试官:元素水平垂直居中的方法有哪些?如果元素不定宽高呢?](https://github.com/febobo/web-interview/issues/102) - [面试官:如何实现两栏布局,右侧自适应?三栏布局中间自适应呢?](https://github.com/febobo/web-interview/issues/103) - [面试官:说说flexbox(弹性盒布局模型),以及适用场景?](https://github.com/febobo/web-interview/issues/104) - [面试官:介绍一下grid网格布局](https://github.com/febobo/web-interview/issues/105) - [面试官:CSS3新增了哪些新特性?](https://github.com/febobo/web-interview/issues/106) - [面试官:css3动画有哪些?](https://github.com/febobo/web-interview/issues/109) - [面试官:怎么理解回流跟重绘?什么场景下会触发?](https://github.com/febobo/web-interview/issues/107) - [面试官:什么是响应式设计?响应式设计的基本原理是什么?如何做?](https://github.com/febobo/web-interview/issues/108) - [面试官:如果要做优化,CSS提高性能的方法有哪些?](https://github.com/febobo/web-interview/issues/114) - [面试官:如何实现单行/多行文本溢出的省略样式?](https://github.com/febobo/web-interview/issues/115) - [面试官:如何使用css完成视差滚动效果?](https://github.com/febobo/web-interview/issues/116) - [面试官:CSS如何画一个三角形?原理是什么?](https://github.com/febobo/web-interview/issues/117) - [面试官:让Chrome支持小于12px 的文字方式有哪些?区别?](https://github.com/febobo/web-interview/issues/118) - [面试官:说说对Css预编语言的理解?有哪些区别?](https://github.com/febobo/web-interview/issues/119)

webpack系列

- [面试官:说说你对webpack的理解?解决了什么问题?](https://github.com/febobo/web-interview/issues/121) - [面试官:说说webpack的构建流程?](https://github.com/febobo/web-interview/issues/122) - [面试官:说说webpack中常见的Loader?解决了什么问题?](https://github.com/febobo/web-interview/issues/123) - [面试官:说说webpack中常见的Plugin?解决了什么问题?](https://github.com/febobo/web-interview/issues/124) - [面试官:说说Loader和Plugin的区别?编写Loader,Plugin的思路?](https://github.com/febobo/web-interview/issues/125) - [面试官:说说webpack的热更新是如何做到的?原理是什么?](https://github.com/febobo/web-interview/issues/126) - [面试官:说说webpack proxy工作原理?为什么能解决跨域?](https://github.com/febobo/web-interview/issues/130) - [面试官:说说如何借助webpack来优化前端性能?](https://github.com/febobo/web-interview/issues/131) - [面试官:如何提高webpack的构建速度?](https://github.com/febobo/web-interview/issues/132) - [面试官:与webpack类似的工具还有哪些?区别?](https://github.com/febobo/web-interview/issues/133)

HTTP系列

- [面试官:什么是HTTP? HTTP 和 HTTPS 的区别?](https://github.com/febobo/web-interview/issues/134) - [面试官:为什么说HTTPS比HTTP安全? HTTPS是如何保证安全的?](https://github.com/febobo/web-interview/issues/135) - [面试官:如何理解UDP 和 TCP? 区别? 应用场景?](https://github.com/febobo/web-interview/issues/136) - [面试官:如何理解OSI七层模型?](https://github.com/febobo/web-interview/issues/139) - [面试官:如何理解TCP/IP协议?](https://github.com/febobo/web-interview/issues/140) - [面试官:DNS协议 是什么?说说DNS 完整的查询过程?](https://github.com/febobo/web-interview/issues/141) - [面试官:如何理解CDN?说说实现原理?](https://github.com/febobo/web-interview/issues/142) - [面试官:说说 HTTP1.0/1.1/2.0 的区别?](https://github.com/febobo/web-interview/issues/143) - [面试官:说说HTTP 常见的状态码有哪些,适用场景?](https://github.com/febobo/web-interview/issues/144) - [面试官:说一下 GET 和 POST 的区别?](https://github.com/febobo/web-interview/issues/145) - [面试官:说说 HTTP 常见的请求头有哪些? 作用?](https://github.com/febobo/web-interview/issues/149) - [面试官:说说地址栏输入 URL 敲下回车后发生了什么?](https://github.com/febobo/web-interview/issues/150) - [面试官:说说TCP为什么需要三次握手和四次挥手?](https://github.com/febobo/web-interview/issues/151) - [面试官:说说对WebSocket的理解?应用场景?](https://github.com/febobo/web-interview/issues/152)

NodeJS系列

- [面试官:说说你对Node.js 的理解?优缺点?应用场景?](https://github.com/febobo/web-interview/issues/153) - [面试官:说说 Node. js 有哪些全局对象?](https://github.com/febobo/web-interview/issues/154) - [面试官:说说对 Node 中的 process 的理解?有哪些常用方法?](https://github.com/febobo/web-interview/issues/155) - [面试官:说说对 Node 中的 fs模块的理解? 有哪些常用方法](https://github.com/febobo/web-interview/issues/156) - [面试官:说说对 Node 中的 Buffer 的理解?应用场景?](https://github.com/febobo/web-interview/issues/164) - [面试官:说说对 Node 中的 Stream 的理解?应用场景?](https://github.com/febobo/web-interview/issues/165) - [面试官:说说Node中的EventEmitter? 如何实现一个EventEmitter?](https://github.com/febobo/web-interview/issues/166) - [面试官:说说对Nodejs中的事件循环机制理解?](https://github.com/febobo/web-interview/issues/167) - [面试官:说说 Node 文件查找的优先级以及 Require 方法的文件查找策略?](https://github.com/febobo/web-interview/issues/168) - [面试官:说说对中间件概念的理解,如何封装 node 中间件?](https://github.com/febobo/web-interview/issues/169) - [面试官:如何实现jwt鉴权机制?说说你的思路](https://github.com/febobo/web-interview/issues/170) - [面试官:如何实现文件上传?说说你的思路](https://github.com/febobo/web-interview/issues/171) - [面试官:如果让你来设计一个分页功能, 你会怎么设计? 前后端如何交互?](https://github.com/febobo/web-interview/issues/172) - [面试官:Node性能如何进行监控以及优化?](https://github.com/febobo/web-interview/issues/173)

React系列

- [面试官:说说对React的理解?有哪些特性?](https://github.com/febobo/web-interview/issues/180) - [面试官:说说 Real DOM和 Virtual DOM 的区别?优缺点?](https://github.com/febobo/web-interview/issues/181) - [面试官:说说 React 生命周期有哪些不同阶段?每个阶段对应的方法是?](https://github.com/febobo/web-interview/issues/182) - [面试官:state 和 props有什么区别?](https://github.com/febobo/web-interview/issues/183) - [面试官:super()和super(props)有什么区别?](https://github.com/febobo/web-interview/issues/184) - [面试官:说说React中的setState执行机制](https://github.com/febobo/web-interview/issues/185) - [面试官:说说React的事件机制?](https://github.com/febobo/web-interview/issues/186) - [面试官:React事件绑定的方式有哪些?区别?](https://github.com/febobo/web-interview/issues/187) - [面试官:React构建组件的方式有哪些?区别?](https://github.com/febobo/web-interview/issues/188) - [面试官:React中组件之间如何通信?](https://github.com/febobo/web-interview/issues/189) - [面试官:React中的key有什么作用?](https://github.com/febobo/web-interview/issues/191) - [面试官:说说对React refs 的理解?应用场景?](https://github.com/febobo/web-interview/issues/192) - [面试官:说说对React中类组件和函数组件的理解?有什么区别?](https://github.com/febobo/web-interview/issues/193) - [面试官:说说对受控组件和非受控组件的理解?应用场景?](https://github.com/febobo/web-interview/issues/207) - [面试官:说说对高阶组件的理解?应用场景?](https://github.com/febobo/web-interview/issues/194) - [面试官:说说对React Hooks的理解?解决了什么问题?](https://github.com/febobo/web-interview/issues/195) - [面试官:说说react中引入css的方式有哪几种?区别?](https://github.com/febobo/web-interview/issues/196) - [面试官:在react中组件间过渡动画如何实现?](https://github.com/febobo/web-interview/issues/197) - [面试官:说说你对Redux的理解?其工作原理?](https://github.com/febobo/web-interview/issues/198) - [面试官:说说对Redux中间件的理解?常用的中间件有哪些?实现原理?](https://github.com/febobo/web-interview/issues/199) - [面试官:你在React项目中是如何使用Redux的? 项目结构是如何划分的?](https://github.com/febobo/web-interview/issues/201) - [面试官:说说你对React Router的理解?常用的Router组件有哪些?](https://github.com/febobo/web-interview/issues/202) - [面试官:说说React Router有几种模式?实现原理?](https://github.com/febobo/web-interview/issues/203) - [面试官:说说你对immutable的理解?如何应用在react项目中?](https://github.com/febobo/web-interview/issues/204) - [面试官:说说React render方法的原理?在什么时候会被触发?](https://github.com/febobo/web-interview/issues/205) - [面试官:说说你是如何提高组件的渲染效率的?在React中如何避免不必要的render?](https://github.com/febobo/web-interview/issues/210) - [面试官:说说React diff的原理是什么?](https://github.com/febobo/web-interview/issues/208) - [面试官:说说对Fiber架构的理解?解决了什么问题?](https://github.com/febobo/web-interview/issues/209) - [面试官:说说React Jsx转换成真实DOM过程?](https://github.com/febobo/web-interview/issues/206) - [面试官:说说 React 性能优化的手段有哪些? ](https://github.com/febobo/web-interview/issues/211) - [面试官:说说你在React项目是如何捕获错误的?](https://github.com/febobo/web-interview/issues/216) - [面试官:说说React服务端渲染怎么做?原理是什么?](https://github.com/febobo/web-interview/issues/217) - [面试官:说说你在使用React 过程中遇到的常见问题?如何解决?](https://github.com/febobo/web-interview/issues/218)

版本控制系列

- [面试官:说说你对版本管理的理解?常用的版本管理工具有哪些?](https://github.com/febobo/web-interview/issues/219) - [面试官:说说你对Git的理解?](https://github.com/febobo/web-interview/issues/220) - [面试官:说说Git中 fork, clone,branch这三个概念,有什么区别?](https://github.com/febobo/web-interview/issues/221) - [面试官:说说Git常用的命令有哪些?](https://github.com/febobo/web-interview/issues/222) - [面试官:说说Git 中 HEAD、工作树和索引之间的区别?](https://github.com/febobo/web-interview/issues/223) - [面试官:说说对git pull 和 git fetch 的理解?有什么区别?](https://github.com/febobo/web-interview/issues/224) - [面试官:说说你对git stash 的理解?应用场景?](https://github.com/febobo/web-interview/issues/227) - [面试官:说说你对git rebase 和 git merge的理解?区别?](https://github.com/febobo/web-interview/issues/228) - [面试官:说说 git 发生冲突的场景?如何解决?](https://github.com/febobo/web-interview/issues/229) - [面试官:说说你对git reset 和 git revert 的理解?区别?](https://github.com/febobo/web-interview/issues/230)

操作系统系列

- [面试官:说说你对操作系统的理解?核心概念有哪些?](https://github.com/febobo/web-interview/issues/231) - [面试官:说说什么是进程?什么是线程?区别?](https://github.com/febobo/web-interview/issues/232) - [面试官:说说 linux系统下 文件操作常用的命令有哪些?](https://github.com/febobo/web-interview/issues/233) - [面试官:说说 linux 系统下 文本编辑常用的命令有哪些?](https://github.com/febobo/web-interview/issues/234) - [面试官:说说你对 linux 用户管理的理解?相关的命令有哪些?](https://github.com/febobo/web-interview/issues/235) - [面试官:说说你对输入输出重定向和管道的理解?应用场景?](https://github.com/febobo/web-interview/issues/236) - [面试官:说说你对 shell 的理解?常见的命令?](https://github.com/febobo/web-interview/issues/237)

typescript系列

- [面试官:说说你对 typescript 的理解?与 javascript 的区别?](https://github.com/febobo/web-interview/issues/245) - [面试官:说说 typescript 的数据类型有哪些?](https://github.com/febobo/web-interview/issues/246) - [面试官:说说你对 TypeScript 中枚举类型的理解?应用场景?](https://github.com/febobo/web-interview/issues/247) - [面试官:说说你对 TypeScript 中接口的理解?应用场景?](https://github.com/febobo/web-interview/issues/248) - [面试官:说说你对 TypeScript 中类的理解?应用场景?](https://github.com/febobo/web-interview/issues/249) - [面试官:说说你对 TypeScript 中函数的理解?与 JavaScript 函数的区别?](https://github.com/febobo/web-interview/issues/255) - [面试官:说说你对 TypeScript 中泛型的理解?应用场景?](https://github.com/febobo/web-interview/issues/250) - [面试官:说说你对 TypeScript 中高级类型的理解?有哪些?](https://github.com/febobo/web-interview/issues/251) - [面试官:说说你对 TypeScript 装饰器的理解?应用场景?](https://github.com/febobo/web-interview/issues/252) - [面试官:说说对 TypeScript 中命名空间与模块的理解?区别?](https://github.com/febobo/web-interview/issues/253) - [面试官:说说如何在React项目中应用TypeScript?](https://github.com/febobo/web-interview/issues/255) - [面试官:说说如何在Vue项目中应用TypeScript?](https://github.com/febobo/web-interview/issues/257)

算法系列

- [面试官:说说你对算法的理解?应用场景?](https://github.com/febobo/web-interview/issues/258) - [面试官:说说你对算法中时间复杂度,空间复杂度的理解?如何计算?](https://github.com/febobo/web-interview/issues/259) - [面试官:说说你对数据结构的理解?有哪些?区别?](https://github.com/febobo/web-interview/issues/260) - [面试官:说说你对栈、队列的理解?应用场景?](https://github.com/febobo/web-interview/issues/261) - [面试官:说说你对链表的理解?常见的操作有哪些?](https://github.com/febobo/web-interview/issues/262) - [面试官:说说你对集合的理解?常见的操作有哪些?](https://github.com/febobo/web-interview/issues/263) - [面试官:说说你对树的理解?相关的操作有哪些?](https://github.com/febobo/web-interview/issues/264) - [面试官:说说你对堆的理解?如何实现?应用场景?](https://github.com/febobo/web-interview/issues/265) - [面试官:说说你对图的理解?相关操作有哪些?](https://github.com/febobo/web-interview/issues/266) - [面试官:说说常见的排序算法有哪些?区别?](https://github.com/febobo/web-interview/issues/267) - [面试官:说说你对冒泡排序的理解?如何实现?应用场景?](https://github.com/febobo/web-interview/issues/271) - [面试官:说说你对选择排序的理解?如何实现?应用场景?](https://github.com/febobo/web-interview/issues/272) - [面试官:说说你对插入排序的理解?如何实现?应用场景?](https://github.com/febobo/web-interview/issues/273) - [面试官:说说你对归并排序的理解?如何实现?应用场景?](https://github.com/febobo/web-interview/issues/274) - [面试官:说说你对快速排序的理解?如何实现?应用场景?](https://github.com/febobo/web-interview/issues/275) - [面试官:说说你对二分查找的理解?如何实现?应用场景?](https://github.com/febobo/web-interview/issues/276) - [面试官:说说你对分而治之、动态规划的理解?区别?](https://github.com/febobo/web-interview/issues/277) - [面试官:说说你对贪心算法、回溯算法的理解?应用场景?](https://github.com/febobo/web-interview/issues/278)

小程序系列

- [面试官:说说你对微信小程序的理解?优缺点?](https://github.com/febobo/web-interview/issues/282) - [面试官:说说微信小程序的生命周期函数有哪些?](https://github.com/febobo/web-interview/issues/283) - [面试官:说说微信小程序中路由跳转的方式有哪些?区别?](https://github.com/febobo/web-interview/issues/284) - [面试官:说说提高微信小程序的应用速度的手段有哪些?](https://github.com/febobo/web-interview/issues/285) - [面试官:说说微信小程序的登录流程?](https://github.com/febobo/web-interview/issues/286) - [面试官:说说微信小程序的发布流程?](https://github.com/febobo/web-interview/issues/287) - [面试官:说说微信小程序的支付流程?](https://github.com/febobo/web-interview/issues/288) - [面试官:说说微信小程序的实现原理?](https://github.com/febobo/web-interview/issues/289)

设计模式系列

- [面试官:说说对设计模式的理解?常见的设计模式有哪些?](https://github.com/febobo/web-interview/issues/290) - [面试官:说说你对单例模式的理解?如何实现?](https://github.com/febobo/web-interview/issues/291) - [面试官:说说你对工厂模式的理解?应用场景?](https://github.com/febobo/web-interview/issues/292) - [面试官:说说你对策略模式的理解?应用场景?](https://github.com/febobo/web-interview/issues/293) - [面试官:说说你对代理模式的理解?应用场景?](https://github.com/febobo/web-interview/issues/294) - [面试官:说说你对发布订阅、观察者模式的理解?区别?](https://github.com/febobo/web-interview/issues/295) ## 👧 更多系列 ### 面试官手写系列/精选33道 筹备中.. ### 面试官浏览器系列/精选33道 筹备中.. ### 面试官前端构建系列/精选33道 筹备中.. ## 声明 - 添加本仓库地址可随意转载仓库内所有内容 - 本仓库永不收取任何费用,现在不会,未来也不会,也不会授权任何人/机构进行收费

================================================ FILE: docs/React/Binding events.md ================================================ # 面试官:React事件绑定的方式有哪些?区别? ![](https://static.vue-js.com/e21f5560-d8fa-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 在`react`应用中,事件名都是用小驼峰格式进行书写,例如`onclick`要改写成`onClick` 最简单的事件绑定如下: ```jsx class ShowAlert extends React.Component { showAlert() { console.log("Hi"); } render() { return ; } } ``` 从上面可以看到,事件绑定的方法需要使用`{}`包住 上述的代码看似没有问题,但是当将处理函数输出代码换成`console.log(this)`的时候,点击按钮,则会发现控制台输出`undefined` ## 二、如何绑定 为了解决上面正确输出`this`的问题,常见的绑定方式有如下: - render方法中使用bind - render方法中使用箭头函数 - constructor中bind - 定义阶段使用箭头函数绑定 ### render方法中使用bind 如果使用一个类组件,在其中给某个组件/元素一个`onClick`属性,它现在并会自定绑定其`this`到当前组件,解决这个问题的方法是在事件函数后使用`.bind(this)`将`this`绑定到当前组件中 ```jsx class App extends React.Component { handleClick() { console.log('this > ', this); } render() { return (
test
) } } ``` 这种方式在组件每次`render`渲染的时候,都会重新进行`bind`的操作,影响性能 ### render方法中使用箭头函数 通过`ES6`的上下文来将`this`的指向绑定给当前组件,同样再每一次`render`的时候都会生成新的方法,影响性能 ```jsx class App extends React.Component { handleClick() { console.log('this > ', this); } render() { return (
this.handleClick(e)}>test
) } } ``` ## constructor中bind 在`constructor`中预先`bind`当前组件,可以避免在`render`操作中重复绑定 ```jsx class App extends React.Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { console.log('this > ', this); } render() { return (
test
) } } ``` ### 定义阶段使用箭头函数绑定 跟上述方式三一样,能够避免在`render`操作中重复绑定,实现也非常的简单,如下: ```jsx class App extends React.Component { constructor(props) { super(props); } handleClick = () => { console.log('this > ', this); } render() { return (
test
) } } ``` ## 三、区别 上述四种方法的方式,区别主要如下: - 编写方面:方式一、方式二写法简单,方式三的编写过于冗杂 - 性能方面:方式一和方式二在每次组件render的时候都会生成新的方法实例,性能问题欠缺。若该函数作为属性值传给子组件的时候,都会导致额外的渲染。而方式三、方式四只会生成一个方法实例 综合上述,方式四是最优的事件绑定方式 ## 参考文献 - https://segmentfault.com/a/1190000011317515 - https://vue3js.cn/interview/ ================================================ FILE: docs/React/Building components.md ================================================ # 面试官:React构建组件的方式有哪些?区别? ![](https://static.vue-js.com/04355cb0-da10-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式 在`React`中,一个类、一个函数都可以视为一个组件 在[之前文章](https://mp.weixin.qq.com/s/Wi0r38LBopsyQ9HesMID0g)中,我们了解到组件所存在的优势: - 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现 - 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单 - 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级 ## 二、如何构建 在`React`目前来讲,组件的创建主要分成了三种方式: - 函数式创建 - 通过 React.createClass 方法创建 - 继承 React.Component 创建 ### 函数式创建 在`React Hooks`出来之前,函数式组件可以视为无状态组件,只负责根据传入的`props`来展示视图,不涉及对`state`状态的操作 大多数组件可以写为无状态组件,通过简单组合构建其他组件 在`React`中,通过函数简单创建组件的示例如下: ```jsx function HelloComponent(props, /* context */) { return
Hello {props.name}
} ``` ### 通过 React.createClass 方法创建 `React.createClass`是react刚开始推荐的创建组件的方式,目前这种创建方式已经不怎么用了 像上述通过函数式创建的组件的方式,最终会通过`babel`转化成`React.createClass`这种形式,转化成如下: ```jsx function HelloComponent(props) /* context */{ return React.createElement( "div", null, "Hello ", props.name ); } ``` 由于上述的编写方式过于冗杂,目前基本上不使用上 ### 继承 React.Component 创建 同样在`react hooks`出来之前,有状态的组件只能通过继承`React.Component`这种形式进行创建 有状态的组件也就是组件内部存在维护的数据,在类创建的方式中通过`this.state`进行访问 当调用`this.setState`修改组件的状态时,组价会再次会调用`render()`方法进行重新渲染 通过继承`React.Component`创建一个时钟示例如下: ```jsx class Timer extends React.Component { constructor(props) { super(props); this.state = { seconds: 0 }; } tick() { this.setState(state => ({ seconds: state.seconds + 1 })); } componentDidMount() { this.interval = setInterval(() => this.tick(), 1000); } componentWillUnmount() { clearInterval(this.interval); } render() { return (
Seconds: {this.state.seconds}
); } } ``` ## 三、区别 由于`React.createClass `创建的方式过于冗杂,并不建议使用 而像函数式创建和类组件创建的区别主要在于需要创建的组件是否需要为有状态组件: - 对于一些无状态的组件创建,建议使用函数式创建的方式 - 由于`react hooks`的出现,函数式组件创建的组件通过使用`hooks`方法也能使之成为有状态组件,再加上目前推崇函数式编程,所以这里建议都使用函数式的方式来创建组件 在考虑组件的选择原则上,能用无状态组件则用无状态组件 ## 参考文献 - https://react.docschina.org/ ================================================ FILE: docs/React/Fiber.md ================================================ # 面试官:说说对Fiber架构的理解?解决了什么问题? ![](https://static.vue-js.com/554da6d0-ed24-11eb-85f6-6fac77c0c9b3.png) ## 一、问题 `JavaScript `引擎和页面渲染引擎两个线程是互斥的,当其中一个线程执行时,另一个线程只能挂起等待 如果 `JavaScript` 线程长时间地占用了主线程,那么渲染层面的更新就不得不长时间地等待,界面长时间不更新,会导致页面响应度变差,用户可能会感觉到卡顿 而这也正是 `React 15` 的 `Stack Reconciler `所面临的问题,当 `React `在渲染组件时,从开始到渲染完成整个过程是一气呵成的,无法中断 如果组件较大,那么`js`线程会一直执行,然后等到整棵`VDOM`树计算完成后,才会交给渲染的线程 这就会导致一些用户交互、动画等任务无法立即得到处理,导致卡顿的情况 ![](https://static.vue-js.com/5eb3a850-ed24-11eb-ab90-d9ae814b240d.png) ## 二、是什么 React Fiber 是 Facebook 花费两年余时间对 React 做出的一个重大改变与优化,是对 React 核心算法的一次重新实现。从Facebook在 React Conf 2017 会议上确认,React Fiber 在React 16 版本发布 在`react`中,主要做了以下的操作: - 为每个增加了优先级,优先级高的任务可以中断低优先级的任务。然后再重新,注意是重新执行优先级低的任务 - 增加了异步任务,调用requestIdleCallback api,浏览器空闲的时候执行 - dom diff树变成了链表,一个dom对应两个fiber(一个链表),对应两个队列,这都是为找到被中断的任务,重新执行 从架构角度来看,`Fiber` 是对 `React `核心算法(即调和过程)的重写 从编码角度来看,`Fiber `是 `React `内部所定义的一种数据结构,它是 `Fiber `树结构的节点单位,也就是 `React 16` 新架构下的虚拟`DOM` 一个 `fiber `就是一个 `JavaScript `对象,包含了元素的信息、该元素的更新操作队列、类型,其数据结构如下: ```js type Fiber = { // 用于标记fiber的WorkTag类型,主要表示当前fiber代表的组件类型如FunctionComponent、ClassComponent等 tag: WorkTag, // ReactElement里面的key key: null | string, // ReactElement.type,调用`createElement`的第一个参数 elementType: any, // The resolved function/class/ associated with this fiber. // 表示当前代表的节点类型 type: any, // 表示当前FiberNode对应的element组件实例 stateNode: any, // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回 return: Fiber | null, // 指向自己的第一个子节点 child: Fiber | null, // 指向自己的兄弟结构,兄弟节点的return指向同一个父节点 sibling: Fiber | null, index: number, ref: null | (((handle: mixed) => void) & { _stringRef: ?string }) | RefObject, // 当前处理过程中的组件props对象 pendingProps: any, // 上一次渲染完成之后的props memoizedProps: any, // 该Fiber对应的组件产生的Update会存放在这个队列里面 updateQueue: UpdateQueue | null, // 上一次渲染的时候的state memoizedState: any, // 一个列表,存放这个Fiber依赖的context firstContextDependency: ContextDependency | null, mode: TypeOfMode, // Effect // 用来记录Side Effect effectTag: SideEffectTag, // 单链表用来快速查找下一个side effect nextEffect: Fiber | null, // 子树中第一个side effect firstEffect: Fiber | null, // 子树中最后一个side effect lastEffect: Fiber | null, // 代表任务在未来的哪个时间点应该被完成,之后版本改名为 lanes expirationTime: ExpirationTime, // 快速确定子树中是否有不在等待的变化 childExpirationTime: ExpirationTime, // fiber的版本池,即记录fiber更新过程,便于恢复 alternate: Fiber | null, } ``` ## 三、如何解决 `Fiber`把渲染更新过程拆分成多个子任务,每次只做一小部分,做完看是否还有剩余时间,如果有继续下一个任务;如果没有,挂起当前任务,将时间控制权交给主线程,等主线程不忙的时候在继续执行 即可以中断与恢复,恢复后也可以复用之前的中间状态,并给不同的任务赋予不同的优先级,其中每个任务更新单元为 `React Element` 对应的 `Fiber `节点 实现的上述方式的是`requestIdleCallback`方法 `window.requestIdleCallback()`方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应 首先 React 中任务切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间再进行页面的渲染。等浏览器忙完之后有剩余时间,再继续之前 React 未完成的任务,是一种合作式调度。 该实现过程是基于 `Fiber `节点实现,作为静态的数据结构来说,每个 `Fiber` 节点对应一个 `React element`,保存了该组件的类型(函数组件/类组件/原生组件等等)、对应的 DOM 节点等信息。 作为动态的工作单元来说,每个 `Fiber` 节点保存了本次更新中该组件改变的状态、要执行的工作。 每个 Fiber 节点有个对应的 `React element`,多个 `Fiber `节点根据如下三个属性构建一颗树: ```javascript // 指向父级Fiber节点 this.return = null // 指向子Fiber节点 this.child = null // 指向右边第一个兄弟Fiber节点 this.sibling = null ``` 通过这些属性就能找到下一个执行目标 ## 参考文献 - https://juejin.cn/post/6926432527980691470 - https://zhuanlan.zhihu.com/p/137234573 - https://vue3js.cn/interview ================================================ FILE: docs/React/High order components.md ================================================ # 面试官:说说对高阶组件的理解?应用场景? ![](https://static.vue-js.com/c8901850-e197-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 高阶函数(Higher-order function),至少满足下列一个条件的函数 - 接受一个或多个函数作为输入 - 输出一个函数 在`React`中,高阶组件即接受一个或多个组件作为参数并且返回一个组件,本质也就是一个函数,并不是一个组件 ```jsx const EnhancedComponent = highOrderComponent(WrappedComponent); ``` 上述代码中,该函数接受一个组件`WrappedComponent`作为参数,返回加工过的新组件`EnhancedComponent` 高阶组件的这种实现方式,本质上是一个装饰者设计模式 ## 二、如何编写 最基本的高阶组件的编写模板如下: ```jsx import React, { Component } from 'react'; export default (WrappedComponent) => { return class EnhancedComponent extends Component { // do something render() { return ; } } } ``` 通过对传入的原始组件 `WrappedComponent` 做一些你想要的操作(比如操作 props,提取 state,给原始组件包裹其他元素等),从而加工出想要的组件 `EnhancedComponent` 把通用的逻辑放在高阶组件中,对组件实现一致的处理,从而实现代码的复用 所以,高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用 但在使用高阶组件的同时,一般遵循一些约定,如下: - props 保持一致 - 你不能在函数式(无状态)组件上使用 ref 属性,因为它没有实例 - 不要以任何方式改变原始组件 WrappedComponent - 透传不相关 props 属性给被包裹的组件 WrappedComponent - 不要再 render() 方法中使用高阶组件 - 使用 compose 组合高阶组件 - 包装显示名字以便于调试 这里需要注意的是,高阶组件可以传递所有的`props`,但是不能传递`ref` 如果向一个高阶组件添加`refe`引用,那么`ref` 指向的是最外层容器组件实例的,而不是被包裹的组件,如果需要传递`refs`的话,则使用`React.forwardRef`,如下: ```jsx function withLogging(WrappedComponent) { class Enhance extends WrappedComponent { componentWillReceiveProps() { console.log('Current props', this.props); console.log('Next props', nextProps); } render() { const {forwardedRef, ...rest} = this.props; // 把 forwardedRef 赋值给 ref return ; } }; // React.forwardRef 方法会传入 props 和 ref 两个参数给其回调函数 // 所以这边的 ref 是由 React.forwardRef 提供的 function forwardRef(props, ref) { return } return React.forwardRef(forwardRef); } const EnhancedComponent = withLogging(SomeComponent); ``` ## 三、应用场景 通过上面的了解,高阶组件能够提高代码的复用性和灵活性,在实际应用中,常常用于与核心业务无关但又在多个模块使用的功能,如权限控制、日志记录、数据校验、异常处理、统计上报等 举个例子,存在一个组件,需要从缓存中获取数据,然后渲染。一般情况,我们会如下编写: ```jsx import React, { Component } from 'react' class MyComponent extends Component { componentWillMount() { let data = localStorage.getItem('data'); this.setState({data}); } render() { return
{this.state.data}
} } ``` 上述代码当然可以实现该功能,但是如果还有其他组件也有类似功能的时候,每个组件都需要重复写`componentWillMount`中的代码,这明显是冗杂的 下面就可以通过高价组件来进行改写,如下: ```jsx import React, { Component } from 'react' function withPersistentData(WrappedComponent) { return class extends Component { componentWillMount() { let data = localStorage.getItem('data'); this.setState({data}); } render() { // 通过{...this.props} 把传递给当前组件的属性继续传递给被包装的组件WrappedComponent return } } } class MyComponent2 extends Component { render() { return
{this.props.data}
} } const MyComponentWithPersistentData = withPersistentData(MyComponent2) ``` 再比如组件渲染性能监控,如下: ```jsx class Home extends React.Component { render() { return (

Hello World.

); } } function withTiming(WrappedComponent) { return class extends WrappedComponent { constructor(props) { super(props); this.start = 0; this.end = 0; } componentWillMount() { super.componentWillMount && super.componentWillMount(); this.start = Date.now(); } componentDidMount() { super.componentDidMount && super.componentDidMount(); this.end = Date.now(); console.log(`${WrappedComponent.name} 组件渲染时间为 ${this.end - this.start} ms`); } render() { return super.render(); } }; } export default withTiming(Home); ``` ## 参考文献 - https://zh-hans.reactjs.org/docs/higher-order-components.html#gatsby-focus-wrapper - https://zh.wikipedia.org/wiki/%E9%AB%98%E9%98%B6%E5%87%BD%E6%95%B0 - https://segmentfault.com/a/1190000010307650 - https://zhuanlan.zhihu.com/p/61711492 ================================================ FILE: docs/React/Improve performance.md ================================================ # 面试官:说说 React 性能优化的手段有哪些? ![](https://static.vue-js.com/a9e83b00-f270-11eb-ab90-d9ae814b240d.png) ## 一、是什么 `React`凭借`virtual DOM`和`diff`算法拥有高效的性能,但是某些情况下,性能明显可以进一步提高 在前面文章中,我们了解到类组件通过调用`setState`方法, 就会导致`render`,父组件一旦发生`render`渲染,子组件一定也会执行`render`渲染 当我们想要更新一个子组件的时候,如下图绿色部分: ![](https://static.vue-js.com/b41f6f30-f270-11eb-ab90-d9ae814b240d.png) 理想状态只调用该路径下的组件`render`: ![](https://static.vue-js.com/bc0f2460-f270-11eb-85f6-6fac77c0c9b3.png) 但是`react`的默认做法是调用所有组件的`render`,再对生成的虚拟`DOM`进行对比(黄色部分),如不变则不进行更新 ![](https://static.vue-js.com/c2f0c4f0-f270-11eb-85f6-6fac77c0c9b3.png) 从上图可见,黄色部分`diff`算法对比是明显的性能浪费的情况 ## 二、如何做 在[React中如何避免不必要的render](https://mp.weixin.qq.com/s/h4NX4Plr6TCjoIhlawiJTg)中,我们了解到如何避免不必要的`render`来应付上面的问题,主要手段是通过`shouldComponentUpdate`、`PureComponent`、`React.memo`,这三种形式这里就不再复述 除此之外, 常见性能优化常见的手段有如下: - 避免使用内联函数 - 使用 React Fragments 避免额外标记 - 使用 Immutable - 懒加载组件 - 事件绑定方式 - 服务端渲染 #### 避免使用内联函数 如果我们使用内联函数,则每次调用`render`函数时都会创建一个新的函数实例,如下: ```jsx import React from "react"; export default class InlineFunctionComponent extends React.Component { render() { return (

Welcome Guest

{ this.setState({inputValue: e.target.value}) }} value="Click For Inline Function" />
) } } ``` 我们应该在组件内部创建一个函数,并将事件绑定到该函数本身。这样每次调用 `render` 时就不会创建单独的函数实例,如下: ```jsx import React from "react"; export default class InlineFunctionComponent extends React.Component { setNewStateData = (event) => { this.setState({ inputValue: e.target.value }) } render() { return (

Welcome Guest

) } } ``` #### 使用 React Fragments 避免额外标记 用户创建新组件时,每个组件应具有单个父标签。父级不能有两个标签,所以顶部要有一个公共标签,所以我们经常在组件顶部添加额外标签`div` 这个额外标签除了充当父标签之外,并没有其他作用,这时候则可以使用`fragement` 其不会向组件引入任何额外标记,但它可以作为父级标签的作用,如下所示: ```jsx export default class NestedRoutingComponent extends React.Component { render() { return ( <>

This is the Header Component

Welcome To Demo Page

) } } ``` ### 事件绑定方式 在[事件绑定方式](https://mp.weixin.qq.com/s/VfQ34ZEPXUXsimzMaJ_41A)中,我们了解到四种事假绑定的方式 从性能方面考虑,在`render`方法中使用`bind`和`render`方法中使用箭头函数这两种形式在每次组件`render`的时候都会生成新的方法实例,性能欠缺 而`constructor`中`bind`事件与定义阶段使用箭头函数绑定这两种形式只会生成一个方法实例,性能方面会有所改善 ### 使用 Immutable 在[理解Immutable中](https://mp.weixin.qq.com/s/laYJ_KNa8M5JNBnIolMDAA),我们了解到使用 `Immutable`可以给 `React` 应用带来性能的优化,主要体现在减少渲染的次数 在做`react`性能优化的时候,为了避免重复渲染,我们会在`shouldComponentUpdate()`中做对比,当返回`true`执行`render`方法 `Immutable`通过`is`方法则可以完成对比,而无需像一样通过深度比较的方式比较 ### 懒加载组件 从工程方面考虑,`webpack`存在代码拆分能力,可以为应用创建多个包,并在运行时动态加载,减少初始包的大小 而在`react`中使用到了`Suspense `和 `lazy`组件实现代码拆分功能,基本使用如下: ```jsx const johanComponent = React.lazy(() => import(/* webpackChunkName: "johanComponent" */ './myAwesome.component')); export const johanAsyncComponent = props => ( }> ); ``` ### 服务端渲染 采用服务端渲染端方式,可以使用户更快的看到渲染完成的页面 服务端渲染,需要起一个`node`服务,可以使用`express`、`koa`等,调用`react`的`renderToString`方法,将根组件渲染成字符串,再输出到响应中 例如: ```js import { renderToString } from "react-dom/server"; import MyPage from "./MyPage"; app.get("/", (req, res) => { res.write("My Page"); res.write("
"); res.write(renderToString()); res.write("
"); res.end(); }); ``` 客户端使用render方法来生成HTML ```jsx import ReactDOM from 'react-dom'; import MyPage from "./MyPage"; ReactDOM.render(, document.getElementById('app')); ``` ### 其他 除此之外,还存在的优化手段有组件拆分、合理使用`hooks`等性能优化手段... ### 三、总结 通过上面初步学习,我们了解到`react`常见的性能优化可以分成三个层面: - 代码层面 - 工程层面 - 框架机制层面 通过这三个层面的优化结合,能够使基于`react`项目的性能更上一层楼 ## 参考文献 - https://zhuanlan.zhihu.com/p/108666350 - https://segmentfault.com/a/1190000007811296 ================================================ FILE: docs/React/JSX to DOM.md ================================================ # 面试官:说说React Jsx转换成真实DOM过程? ![](https://static.vue-js.com/1d340620-f00a-11eb-ab90-d9ae814b240d.png) ## 一、是什么 `react`通过将组件编写的`JSX`映射到屏幕,以及组件中的状态发生了变化之后 `React`会将这些「变化」更新到屏幕上 在前面文章了解中,`JSX`通过`babel`最终转化成`React.createElement`这种形式,例如: ```jsx
< img src="avatar.png" className="profile" />
``` 会被`bebel`转化成如下: ```jsx React.createElement( "div", null, React.createElement("img", { src: "avatar.png", className: "profile" }), React.createElement(Hello, null) ); ``` 在转化过程中,`babel`在编译时会判断 JSX 中组件的首字母: - 当首字母为小写时,其被认定为原生 `DOM` 标签,`createElement` 的第一个变量被编译为字符串 - 当首字母为大写时,其被认定为自定义组件,createElement 的第一个变量被编译为对象 最终都会通过`RenderDOM.render(...)`方法进行挂载,如下: ```jsx ReactDOM.render(, document.getElementById("root")); ``` ## 二、过程 在`react`中,节点大致可以分成四个类别: - 原生标签节点 - 文本节点 - 函数组件 - 类组件 如下所示: ```jsx class ClassComponent extends Component { static defaultProps = { color: "pink" }; render() { return (

ClassComponent

{this.props.name}

); } } function FunctionComponent(props) { return (
FunctionComponent

{props.name}

); } const jsx = (

xx

< a href=" ">xxx
); ``` 这些类别最终都会被转化成`React.createElement`这种形式 `React.createElement`其被调用时会传⼊标签类型`type`,标签属性`props`及若干子元素`children`,作用是生成一个虚拟`Dom`对象,如下所示: ```js function createElement(type, config, ...children) { if (config) { delete config.__self; delete config.__source; } // ! 源码中做了详细处理,⽐如过滤掉key、ref等 const props = { ...config, children: children.map(child => typeof child === "object" ? child : createTextNode(child) ) }; return { type, props }; } function createTextNode(text) { return { type: TEXT, props: { children: [], nodeValue: text } }; } export default { createElement }; ``` `createElement`会根据传入的节点信息进行一个判断: - 如果是原生标签节点, type 是字符串,如div、span - 如果是文本节点, type就没有,这里是 TEXT - 如果是函数组件,type 是函数名 - 如果是类组件,type 是类名 虚拟`DOM`会通过`ReactDOM.render`进行渲染成真实`DOM`,使用方法如下: ```jsx ReactDOM.render(element, container[, callback]) ``` 当首次调用时,容器节点里的所有 `DOM` 元素都会被替换,后续的调用则会使用 `React` 的 `diff`算法进行高效的更新 如果提供了可选的回调函数`callback`,该回调将在组件被渲染或更新之后被执行 `render`大致实现方法如下: ```js function render(vnode, container) { console.log("vnode", vnode); // 虚拟DOM对象 // vnode _> node const node = createNode(vnode, container); container.appendChild(node); } // 创建真实DOM节点 function createNode(vnode, parentNode) { let node = null; const {type, props} = vnode; if (type === TEXT) { node = document.createTextNode(""); } else if (typeof type === "string") { node = document.createElement(type); } else if (typeof type === "function") { node = type.isReactComponent ? updateClassComponent(vnode, parentNode) : updateFunctionComponent(vnode, parentNode); } else { node = document.createDocumentFragment(); } reconcileChildren(props.children, node); updateNode(node, props); return node; } // 遍历下子vnode,然后把子vnode->真实DOM节点,再插入父node中 function reconcileChildren(children, node) { for (let i = 0; i < children.length; i++) { let child = children[i]; if (Array.isArray(child)) { for (let j = 0; j < child.length; j++) { render(child[j], node); } } else { render(child, node); } } } function updateNode(node, nextVal) { Object.keys(nextVal) .filter(k => k !== "children") .forEach(k => { if (k.slice(0, 2) === "on") { let eventName = k.slice(2).toLocaleLowerCase(); node.addEventListener(eventName, nextVal[k]); } else { node[k] = nextVal[k]; } }); } // 返回真实dom节点 // 执行函数 function updateFunctionComponent(vnode, parentNode) { const {type, props} = vnode; let vvnode = type(props); const node = createNode(vvnode, parentNode); return node; } // 返回真实dom节点 // 先实例化,再执行render函数 function updateClassComponent(vnode, parentNode) { const {type, props} = vnode; let cmp = new type(props); const vvnode = cmp.render(); const node = createNode(vvnode, parentNode); return node; } export default { render }; ``` ## 三、总结 在`react`源码中,虚拟`Dom`转化成真实`Dom`整体流程如下图所示: ![](https://static.vue-js.com/28824fa0-f00a-11eb-ab90-d9ae814b240d.png) 其渲染流程如下所示: - 使用React.createElement或JSX编写React组件,实际上所有的 JSX 代码最后都会转换成React.createElement(...) ,Babel帮助我们完成了这个转换的过程。 - createElement函数对key和ref等特殊的props进行处理,并获取defaultProps对默认props进行赋值,并且对传入的孩子节点进行处理,最终构造成一个虚拟DOM对象 - ReactDOM.render将生成好的虚拟DOM渲染到指定容器上,其中采用了批处理、事务等机制并且对特定浏览器进行了性能优化,最终转换为真实DOM ## 参考文献 - https://bbs.huaweicloud.com/blogs/265503) - https://huang-qing.github.io/react/2019/05/29/React-VirDom/ - https://segmentfault.com/a/1190000018891454 ================================================ FILE: docs/React/React Hooks.md ================================================ # 面试官:说说对React Hooks的理解?解决了什么问题? ![](https://static.vue-js.com/8d357c50-e12e-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 `Hook` 是 React 16.8 的新增特性。它可以让你在不编写 `class` 的情况下使用 `state` 以及其他的 `React` 特性 至于为什么引入`hook`,官方给出的动机是解决长时间使用和维护`react`过程中常遇到的问题,例如: - 难以重用和共享组件中的与状态相关的逻辑 - 逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 local state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面 - 类组件中的this增加学习成本,类组件在基于现有工具的优化上存在些许问题 - 由于业务变动,函数组件不得不改为类组件等等 在以前,函数组件也被称为无状态的组件,只负责渲染的一些工作 因此,现在的函数组件也可以是有状态的组件,内部也可以维护自身的状态以及做一些逻辑方面的处理 ## 二、有哪些 上面讲到,`Hooks`让我们的函数组件拥有了类组件的特性,例如组件内的状态、生命周期 最常见的`hooks`有如下: - useState - useEffect - 其他 ### useState 首先给出一个例子,如下: ```js import React, { useState } from 'react'; function Example() { // 声明一个叫 "count" 的 state 变量 const [count, setCount] = useState(0); return (

You clicked {count} times

); } ``` 在函数组件中通过`useState`实现函数内部维护`state`,参数为`state`默认的值,返回值是一个数组,第一个值为当前的`state`,第二个值为更新`state`的函数 该函数组件等价于的类组件如下: ```js class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } render() { return (

You clicked {this.state.count} times

); } } ``` 从上述两种代码分析,可以看出两者区别: - state声明方式:在函数组件中通过 useState 直接获取,类组件通过constructor 构造函数中设置 - state读取方式:在函数组件中直接使用变量,类组件通过`this.state.count`的方式获取 - state更新方式:在函数组件中通过 setCount 更新,类组件通过this.setState() 总的来讲,useState 使用起来更为简洁,减少了`this`指向不明确的情况 ### useEffect `useEffect`可以让我们在函数组件中进行一些带有副作用的操作 同样给出一个计时器示例: ```js class Example extends React.Component { constructor(props) { super(props); this.state = { count: 0 }; } componentDidMount() { document.title = `You clicked ${this.state.count} times`; } componentDidUpdate() { document.title = `You clicked ${this.state.count} times`; } render() { return (

You clicked {this.state.count} times

); } } ``` 从上面可以看见,组件在加载和更新阶段都执行同样操作 而如果使用`useEffect`后,则能够将相同的逻辑抽离出来,这是类组件不具备的方法 对应的`useEffect`示例如下: ```jsx import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); useEffect(() => { document.title = `You clicked ${count} times`; }); return (

You clicked {count} times

); } ``` `useEffect`第一个参数接受一个回调函数,默认情况下,`useEffect`会在第一次渲染和更新之后都会执行,相当于在`componentDidMount`和`componentDidUpdate`两个生命周期函数中执行回调 如果某些特定值在两次重渲染之间没有发生变化,你可以跳过对 effect 的调用,这时候只需要传入第二个参数,如下: ```js useEffect(() => { document.title = `You clicked ${count} times`; }, [count]); // 仅在 count 更改时更新 ``` 上述传入第二个参数后,如果 `count` 的值是 `5`,而且我们的组件重渲染的时候 `count` 还是等于 `5`,React 将对前一次渲染的 `[5]` 和后一次渲染的 `[5]` 进行比较,如果是相等则跳过`effects`执行 回调函数中可以返回一个清除函数,这是`effect`可选的清除机制,相当于类组件中`componentwillUnmount`生命周期函数,可做一些清除副作用的操作,如下: ```jsx useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); ``` 所以, `useEffect`相当于`componentDidMount`,`componentDidUpdate` 和 `componentWillUnmount` 这三个生命周期函数的组合 ### 其它 hooks 在组件通信过程中可以使用`useContext`,`refs`学习中我们也用到了`useRef`获取`DOM`结构...... 还有很多额外的`hooks`,如: - useReducer - useCallback - useMemo - useRef ## 三、解决什么 通过对上面的初步认识,可以看到`hooks`能够更容易解决状态相关的重用的问题: - 每调用useHook一次都会生成一份独立的状态 - 通过自定义hook能够更好的封装我们的功能 编写`hooks`为函数式编程,每个功能都包裹在函数中,整体风格更清爽,更优雅 `hooks`的出现,使函数组件的功能得到了扩充,拥有了类组件相似的功能,在我们日常使用中,使用`hooks`能够解决大多数问题,并且还拥有代码复用机制,因此优先考虑`hooks` ## 参考文献 - https://zh-hans.reactjs.org/docs/hooks-state.html - https://zh-hans.reactjs.org/docs/hooks-effect.html - https://www.cnblogs.com/lalalagq/p/9898531.html ================================================ FILE: docs/React/React Router model.md ================================================ # 面试官:说说React Router有几种模式?实现原理? ![](https://static.vue-js.com/065f7a80-e978-11eb-ab90-d9ae814b240d.png) ## 一、是什么 在单页应用中,一个`web`项目只有一个`html`页面,一旦页面加载完成之后,就不用因为用户的操作而进行页面的重新加载或者跳转,其特性如下: - 改变 url 且不让浏览器像服务器发送请求 - 在不刷新页面的前提下动态改变浏览器地址栏中的URL地址 其中主要分成了两种模式: - hash 模式:在url后面加上#,如http://127.0.0.1:5500/home/#/page1 - history 模式:允许操作浏览器的曾经在标签页或者框架里访问的会话历史记录 ## 二、使用 `React Router`对应的`hash`模式和`history`模式对应的组件为: - HashRouter - BrowserRouter 这两个组件的使用都十分的简单,作为最顶层组件包裹其他组件,如下所示 ```jsx // 1.import { BrowserRouter as Router } from "react-router-dom"; // 2.import { HashRouter as Router } from "react-router-dom"; import React from 'react'; import { BrowserRouter as Router, // HashRouter as Router Switch, Route, } from "react-router-dom"; import Home from './pages/Home'; import Login from './pages/Login'; import Backend from './pages/Backend'; import Admin from './pages/Admin'; function App() { return ( ); } export default App; ``` ## 三、实现原理 路由描述了 `URL` 与 `UI `之间的映射关系,这种映射是单向的,即 URL 变化引起 UI 更新(无需刷新页面) 下面以`hash`模式为例子,改变`hash`值并不会导致浏览器向服务器发送请求,浏览器不发出请求,也就不会刷新页面 `hash` 值改变,触发全局 `window` 对象上的 `hashchange` 事件。所以 `hash` 模式路由就是利用 `hashchange` 事件监听 `URL` 的变化,从而进行 `DOM` 操作来模拟页面跳转 `react-router`也是基于这个特性实现路由的跳转 下面以`HashRouter`组件分析进行展开: ## HashRouter `HashRouter`包裹了整应用, 通过`window.addEventListener('hashChange',callback)`监听`hash`值的变化,并传递给其嵌套的组件 然后通过`context`将`location`数据往后代组件传递,如下: ```jsx import React, { Component } from 'react'; import { Provider } from './context' // 该组件下Api提供给子组件使用 class HashRouter extends Component { constructor() { super() this.state = { location: { pathname: window.location.hash.slice(1) || '/' } } } // url路径变化 改变location componentDidMount() { window.location.hash = window.location.hash || '/' window.addEventListener('hashchange', () => { this.setState({ location: { ...this.state.location, pathname: window.location.hash.slice(1) || '/' } }, () => console.log(this.state.location)) }) } render() { let value = { location: this.state.location } return ( { this.props.children } ); } } export default HashRouter; ``` ### Router `Router`组件主要做的是通过`BrowserRouter`传过来的当前值,通过`props`传进来的`path`与`context`传进来的`pathname`进行匹配,然后决定是否执行渲染组件 ```js import React, { Component } from 'react'; import { Consumer } from './context' const { pathToRegexp } = require("path-to-regexp"); class Route extends Component { render() { return ( { state => { console.log(state) let {path, component: Component} = this.props let pathname = state.location.pathname let reg = pathToRegexp(path, [], {end: false}) // 判断当前path是否包含pathname if(pathname.match(reg)) { return } return null } } ); } } export default Route; ``` ## 参考文献 - https://juejin.cn/post/6870376090297171975#heading-9 - https://segmentfault.com/a/1190000023560665 ================================================ FILE: docs/React/React Router.md ================================================ # 面试官:说说你对React Router的理解?常用的Router组件有哪些? ![](https://static.vue-js.com/c6635670-e8ac-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 `react-router`等前端路由的原理大致相同,可以实现无刷新的条件下切换显示不同的页面 路由的本质就是页面的`URL`发生改变时,页面的显示结果可以根据`URL`的变化而变化,但是页面不会刷新 因此,可以通过前端路由可以实现单页(SPA)应用 `react-router`主要分成了几个不同的包: - react-router: 实现了路由的核心功能 - react-router-dom: 基于 react-router,加入了在浏览器运行环境下的一些功能 - react-router-native:基于 react-router,加入了 react-native 运行环境下的一些功能 - react-router-config: 用于配置静态路由的工具库 ## 二、有哪些 这里主要讲述的是`react-router-dom`的常用`API`,主要是提供了一些组件: - BrowserRouter、HashRouter - Route - Link、NavLink - switch - redirect ### BrowserRouter、HashRouter `Router`中包含了对路径改变的监听,并且会将相应的路径传递给子组件 `BrowserRouter`是`history`模式,`HashRouter`模式 使用两者作为最顶层组件包裹其他组件 ```jsx import { BrowserRouter as Router } from "react-router-dom"; export default function App() { return (
); } ``` ### Route `Route`用于路径的匹配,然后进行组件的渲染,对应的属性如下: - path 属性:用于设置匹配到的路径 - component 属性:设置匹配到路径后,渲染的组件 - render 属性:设置匹配到路径后,渲染的内容 - exact 属性:开启精准匹配,只有精准匹配到完全一致的路径,才会渲染对应的组件 ```jsx import { BrowserRouter as Router, Route } from "react-router-dom"; export default function App() { return (

Welcome!

} />
); } ``` ### Link、NavLink 通常路径的跳转是使用`Link`组件,最终会被渲染成`a`元素,其中属性`to`代替`a`标题的`href`属性 `NavLink`是在`Link`基础之上增加了一些样式属性,例如组件被选中时,发生样式变化,则可以设置`NavLink`的一下属性: - activeStyle:活跃时(匹配时)的样式 - activeClassName:活跃时添加的class 如下: ```js 首页 关于 我的 ``` 如果需要实现`js`实现页面的跳转,那么可以通过下面的形式: 通过`Route`作为顶层组件包裹其他组件后,页面组件就可以接收到一些路由相关的东西,比如`props.history` ```jsx const Contact = ({ history }) => (

Contact

); ``` `props `中接收到的`history`对象具有一些方便的方法,如`goBack`,`goForward`,`push` ### redirect 用于路由的重定向,当这个组件出现时,就会执行跳转到对应的`to`路径中,如下例子: ```js const About = ({ match: { params: { name }, }, }) => ( // props.match.params.name {name !== "tom" ? : null}

About {name}

) ``` 上述组件当接收到的路由参数`name` 不等于 `tom` 的时候,将会自动重定向到首页 ### switch `swich`组件的作用适用于当匹配到第一个组件的时候,后面的组件就不应该继续匹配 如下例子: ```jsx ``` 如果不使用`switch`组件进行包裹 除了一些路由相关的组件之外,`react-router`还提供一些`hooks`,如下: - useHistory - useParams - useLocation ### useHistory `useHistory`可以让组件内部直接访问`history`,无须通过`props`获取 ```js import { useHistory } from "react-router-dom"; const Contact = () => { const history = useHistory(); return (

Contact

); }; ``` ### useParams ```jsx const About = () => { const { name } = useParams(); return ( // props.match.params.name {name !== "John Doe" ? : null}

About {name}

); }; ``` ### useLocation `useLocation` 会返回当前 `URL `的 `location `对象 ```jsx import { useLocation } from "react-router-dom"; const Contact = () => { const { pathname } = useLocation(); return (

Contact

Current URL: {pathname}

); }; ``` ## 三、参数传递 这些路由传递参数主要分成了三种形式: - 动态路由的方式 - search传递参数 - to传入对象 ### 动态路由 动态路由的概念指的是路由中的路径并不会固定 例如将`path`在`Route`匹配时写成`/detail/:id`,那么 `/detail/abc`、`/detail/123`都可以匹配到该`Route` ```jsx 详情 ... 其他Route ``` 获取参数方式如下: ```jsx console.log(props.match.params.xxx) ``` ### search传递参数 在跳转的路径中添加了一些query参数; ```jsx 详情2 ``` 获取形式如下: ```js console.log(props.location.search) ``` ### to传入对象 传递方式如下: ```jsx 详情2 ``` 获取参数的形式如下: ```js console.log(props.location) ``` ## 参考文献 - http://react-guide.github.io/react-router-cn/docs/API.html#route ================================================ FILE: docs/React/React refs.md ================================================ # 面试官:说说对React refs 的理解?应用场景? ![](https://static.vue-js.com/25162040-de02-11eb-ab90-d9ae814b240d.png) ## 一、是什么 `Refs` 在计算机中称为弹性文件系统(英语:Resilient File System,简称ReFS) `React` 中的 `Refs`提供了一种方式,允许我们访问 `DOM `节点或在 `render `方法中创建的 `React `元素 本质为`ReactDOM.render()`返回的组件实例,如果是渲染组件则返回的是组件实例,如果渲染`dom`则返回的是具体的`dom`节点 ## 二、如何使用 创建`ref`的形式有三种: - 传入字符串,使用时通过 this.refs.传入的字符串的格式获取对应的元素 - 传入对象,对象是通过 React.createRef() 方式创建出来,使用时获取到创建的对象中存在 current 属性就是对应的元素 - 传入函数,该函数会在 DOM 被挂载时进行回调,这个函数会传入一个 元素对象,可以自己保存,使用时,直接拿到之前保存的元素对象即可 - 传入hook,hook是通过 useRef() 方式创建,使用时通过生成hook对象的 current 属性就是对应的元素 ### 传入字符串 只需要在对应元素或组件中`ref`属性 ```jsx class MyComponent extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); } render() { return
; } } ``` 访问当前节点的方式如下: ```js this.refs.myref.innerHTML = "hello"; ``` ### 传入对象 `refs`通过`React.createRef()`创建,然后将`ref`属性添加到`React`元素中,如下: ```jsx class MyComponent extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); } render() { return
; } } ``` 当 `ref` 被传递给 `render` 中的元素时,对该节点的引用可以在 `ref` 的 `current` 属性中访问 ```js const node = this.myRef.current; ``` ### 传入函数 当`ref`传入为一个函数的时候,在渲染过程中,回调函数参数会传入一个元素对象,然后通过实例将对象进行保存 ```jsx class MyComponent extends React.Component { constructor(props) { super(props); this.myRef = React.createRef(); } render() { return
this.myref = element} />; } } ``` 获取`ref`对象只需要通过先前存储的对象即可 ```js const node = this.myref ``` ### 传入hook 通过`useRef`创建一个`ref`,整体使用方式与`React.createRef`一致 ```jsx function App(props) { const myref = useRef() return ( <>
) } ``` 获取`ref`属性也是通过`hook`对象的`current`属性 ```js const node = myref.current; ``` 上述三种情况都是`ref`属性用于原生`HTML`元素上,如果`ref`设置的组件为一个类组件的时候,`ref`对象接收到的是组件的挂载实例 注意的是,不能在函数组件上使用`ref`属性,因为他们并没有实例 ## 三、应用场景 在某些情况下,我们会通过使用`refs`来更新组件,但这种方式并不推荐,更多情况我们是通过`props`与`state`的方式进行去重新渲染子元素 过多使用`refs`,会使组件的实例或者是`DOM`结构暴露,违反组件封装的原则 例如,避免在 `Dialog` 组件里暴露 `open()` 和 `close()` 方法,最好传递 `isOpen` 属性 但下面的场景使用`refs`非常有用: - 对Dom元素的焦点控制、内容选择、控制 - 对Dom元素的内容设置及媒体播放 - 对Dom元素的操作和对组件实例的操作 - 集成第三方 DOM 库 ## 参考文献 - https://zh-hans.reactjs.org/docs/refs-and-the-dom.html - https://segmentfault.com/a/1190000020842342 - https://vue3js.cn/interview ================================================ FILE: docs/React/React.md ================================================ # 面试官:说说对 React 的理解?有哪些特性? ![](https://static.vue-js.com/671f5a90-d265-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 React,用于构建用户界面的 JavaScript 库,只提供了 UI 层面的解决方案 遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效 使用虚拟 `DOM` 来有效地操作 `DOM`,遵循从高阶组件到低阶组件的单向数据流 帮助我们将界面成了各个独立的小块,每一个块就是组件,这些组件之间可以组合、嵌套,构成整体页面 `react` 类组件使用一个名为 `render()` 的方法或者函数组件`return`,接收输入的数据并返回需要展示的内容 ```jsx class HelloMessage extends React.Component { render() { return
Hello {this.props.name}
; } } ReactDOM.render( , document.getElementById("hello-example") ); ``` 上述这种类似 `XML` 形式就是 `JSX`,最终会被 `babel` 编译为合法的 `JS` 语句调用 被传入的数据可在组件中通过 `this.props` 在 `render()` 访问 ## 二、特性 `React` 特性有很多,如: - JSX 语法 - 单向数据绑定 - 虚拟 DOM - 声明式编程 - Component 着重介绍下声明式编程及 Component ### 声明式编程 声明式编程是一种编程范式,它关注的是你要做什么,而不是如何做 它表达逻辑而不显式地定义步骤。这意味着我们需要根据逻辑的计算来声明要显示的组件 如实现一个标记的地图: 通过命令式创建地图、创建标记、以及在地图上添加的标记的步骤如下: ```js // 创建地图 const map = new Map.map(document.getElementById("map"), { zoom: 4, center: { lat, lng }, }); // 创建标记 const marker = new Map.marker({ position: { lat, lng }, title: "Hello Marker", }); // 地图上添加标记 marker.setMap(map); ``` 而用 `React` 实现上述功能则如下: ```jsx ``` 声明式编程方式使得 `React` 组件很容易使用,最终的代码简单易于维护 ### Component 在 `React` 中,一切皆为组件。通常将应用程序的整个逻辑分解为小的单个部分。 我们将每个单独的部分称为组件 组件可以是一个函数或者是一个类,接受数据输入,处理它并返回在 `UI` 中呈现的 `React` 元素 函数式组件如下: ```jsx const Header = () => { return (

TODO App

); }; ``` 类组件(有状态组件)如下: ```jsx class Dashboard extends React.Component { constructor(props) { super(props); this.state = {}; } render() { return (
); } } ``` 一个组件该有的特点如下: - 可组合:每个组件易于和其它组件一起使用,或者嵌套在另一个组件内部 - 可重用:每个组件都是具有独立功能的,它可以被使用在多个 UI 场景 - 可维护:每个小的组件仅仅包含自身的逻辑,更容易被理解和维护 ## 三、优势 通过上面的初步了解,可以感受到 `React` 存在的优势: - 高效灵活 - 声明式的设计,简单使用 - 组件式开发,提高代码复用率 - 单向响应的数据流会比双向绑定的更安全,速度更快 ## 参考文献 - [https://segmentfault.com/a/1190000015924762](https://segmentfault.com/a/1190000015924762) - [https://react.docschina.org/](https://react.docschina.org/) ================================================ FILE: docs/React/Real DOM_Virtual DOM.md ================================================ # 面试官:说说 Real DOM 和 Virtual DOM 的区别?优缺点? ![](https://static.vue-js.com/f1d36350-d302-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 Real DOM,真实 `DOM`,意思为文档对象模型,是一个结构化文本的抽象,在页面渲染出的每一个结点都是一个真实 `DOM` 结构,如下: ![](https://static.vue-js.com/fc7ba8d0-d302-11eb-85f6-6fac77c0c9b3.png) `Virtual Dom`,本质上是以 `JavaScript` 对象形式存在的对 `DOM` 的描述 创建虚拟 `DOM` 目的就是为了更好将虚拟的节点渲染到页面视图中,虚拟 `DOM` 对象的节点与真实 `DOM` 的属性一一照应 在 `React` 中,`JSX` 是其一大特性,可以让你在 `JS` 中通过使用 `XML` 的方式去直接声明界面的 `DOM` 结构 ```jsx // 创建 h1 标签,右边千万不能加引号 const vDom =

Hello World

; // 找到
节点 const root = document.getElementById("root"); // 把创建的 h1 标签渲染到 root 节点上 ReactDOM.render(vDom, root); ``` 上述中,`ReactDOM.render()` 用于将你创建好的虚拟 `DOM` 节点插入到某个真实节点上,并渲染到页面上 `JSX` 实际是一种语法糖,在使用过程中会被 `babel` 进行编译转化成 `JS` 代码,上述 `VDOM` 转化为如下: ```jsx const vDom = React.createElement( 'h1', { className: 'hClass', id: 'hId' }, 'hello world' ) ``` 可以看到,`JSX` 就是为了简化直接调用 `React.createElement()` 方法: - 第一个参数是标签名,例如 h1、span、table... - 第二个参数是个对象,里面存着标签的一些属性,例如 id、class 等 - 第三个参数是节点中的文本 通过 `console.log(VDOM)`,则能够得到虚拟 `VDOM` 消息 ![](https://static.vue-js.com/1716b9a0-d303-11eb-ab90-d9ae814b240d.png) 所以可以得到,`JSX` 通过 `babel` 的方式转化成 `React.createElement` 执行,返回值是一个对象,也就是虚拟 `DOM` ## 二、区别 两者的区别如下: - 虚拟 DOM 不会进行排版与重绘操作,而真实 DOM 会频繁重排与重绘 - 虚拟 DOM 的总损耗是“虚拟 DOM 增删改+真实 DOM 差异增删改+排版与重绘”,真实 DOM 的总损耗是“真实 DOM 完全增删改+排版与重绘” 拿[以前文章](https://mp.weixin.qq.com/s?__biz=MzU1OTgxNDQ1Nw==&mid=2247484516&idx=1&sn=965a4ce32bf93adb9ed112922c5cb8f5&chksm=fc10c632cb674f2484fdf914d76fba55afcefca3b5adcbe6cf4b0c7fd36e29d0292e8cefceb5&scene=178&cur_album_id=1711105826272116736#rd)举过的例子: 传统的原生 `api` 或 `jQuery` 去操作 `DOM` 时,浏览器会从构建 `DOM` 树开始从头到尾执行一遍流程 当你在一次操作时,需要更新 10 个 `DOM` 节点,浏览器没这么智能,收到第一个更新 `DOM` 请求后,并不知道后续还有 9 次更新操作,因此会马上执行流程,最终执行 10 次流程 而通过 `VNode`,同样更新 10 个 `DOM` 节点,虚拟 `DOM` 不会立即操作 `DOM`,而是将这 10 次更新的 `diff` 内容保存到本地的一个 `js` 对象中,最终将这个 `js` 对象一次性 `attach` 到 `DOM` 树上,避免大量的无谓计算 ## 三、优缺点 真实 `DOM` 的优势: - 易用 缺点: - 效率低,解析速度慢,内存占用量过高 - 性能差:频繁操作真实 DOM,易于导致重绘与回流 使用虚拟 `DOM` 的优势如下: - 简单方便:如果使用手动操作真实 `DOM` 来完成页面,繁琐又容易出错,在大规模应用下维护起来也很困难 - 性能方面:使用 Virtual DOM,能够有效避免真实 DOM 数频繁更新,减少多次引起重绘与回流,提高性能 - 跨平台:React 借助虚拟 DOM,带来了跨平台的能力,一套代码多端运行 缺点: - 在一些性能要求极高的应用中虚拟 DOM 无法进行针对性的极致优化 - 首次渲染大量 DOM 时,由于多了一层虚拟 DOM 的计算,速度比正常稍慢 ## 参考文献 - [https://juejin.cn/post/6844904052971536391](https://juejin.cn/post/6844904052971536391) - [https://www.html.cn/qa/other/22832.html](https://www.html.cn/qa/other/22832.html) ================================================ FILE: docs/React/Redux Middleware.md ================================================ # 面试官:说说对Redux中间件的理解?常用的中间件有哪些?实现原理? ![](https://static.vue-js.com/4520bbd0-e699-11eb-ab90-d9ae814b240d.png) ## 一、是什么 中间件(Middleware)是介于应用系统和系统软件之间的一类软件,它使用系统软件所提供的基础服务(功能),衔接网络上应用系统的各个部分或不同的应用,能够达到资源共享、功能共享的目的 在上篇文章中,了解到了`Redux`整个工作流程,当`action`发出之后,`reducer`立即算出`state`,整个过程是一个同步的操作 那么如果需要支持异步操作,或者支持错误处理、日志监控,这个过程就可以用上中间件 `Redux`中,中间件就是放在就是在`dispatch`过程,在分发`action`进行拦截处理,如下图: ![](https://static.vue-js.com/57edf750-e699-11eb-ab90-d9ae814b240d.png) 其本质上一个函数,对`store.dispatch`方法进行了改造,在发出 `Action `和执行 `Reducer `这两步之间,添加了其他功能 ## 二、常用的中间件 有很多优秀的`redux`中间件,如: - redux-thunk:用于异步操作 - redux-logger:用于日志记录 上述的中间件都需要通过`applyMiddlewares`进行注册,作用是将所有的中间件组成一个数组,依次执行 然后作为第二个参数传入到`createStore`中 ```js const store = createStore( reducer, applyMiddleware(thunk, logger) ); ``` ### redux-thunk `redux-thunk`是官网推荐的异步处理中间件 默认情况下的`dispatch(action)`,`action`需要是一个`JavaScript`的对象 `redux-thunk`中间件会判断你当前传进来的数据类型,如果是一个函数,将会给函数传入参数值(dispatch,getState) - dispatch函数用于我们之后再次派发action - getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态 所以`dispatch`可以写成下述函数的形式: ```js const getHomeMultidataAction = () => { return (dispatch) => { axios.get("http://xxx.xx.xx.xx/test").then(res => { const data = res.data.data; dispatch(changeBannersAction(data.banner.list)); dispatch(changeRecommendsAction(data.recommend.list)); }) } } ``` ### redux-logger 如果想要实现一个日志功能,则可以使用现成的`redux-logger` ```js import { applyMiddleware, createStore } from 'redux'; import createLogger from 'redux-logger'; const logger = createLogger(); const store = createStore( reducer, applyMiddleware(logger) ); ``` 这样我们就能简单通过中间件函数实现日志记录的信息 ## 三、实现原理 首先看看`applyMiddlewares`的源码 ```js export default function applyMiddleware(...middlewares) { return (createStore) => (reducer, preloadedState, enhancer) => { var store = createStore(reducer, preloadedState, enhancer); var dispatch = store.dispatch; var chain = []; var middlewareAPI = { getState: store.getState, dispatch: (action) => dispatch(action) }; chain = middlewares.map(middleware => middleware(middlewareAPI)); dispatch = compose(...chain)(store.dispatch); return {...store, dispatch} } } ``` 所有中间件被放进了一个数组`chain`,然后嵌套执行,最后执行`store.dispatch`。可以看到,中间件内部(`middlewareAPI`)可以拿到`getState`和`dispatch`这两个方法 在上面的学习中,我们了解到了`redux-thunk`的基本使用 内部会将`dispatch`进行一个判断,然后执行对应操作,原理如下: ```js function patchThunk(store) { let next = store.dispatch; function dispatchAndThunk(action) { if (typeof action === "function") { action(store.dispatch, store.getState); } else { next(action); } } store.dispatch = dispatchAndThunk; } ``` 实现一个日志输出的原理也非常简单,如下: ```js let next = store.dispatch; function dispatchAndLog(action) { console.log("dispatching:", addAction(10)); next(addAction(5)); console.log("新的state:", store.getState()); } store.dispatch = dispatchAndLog; ``` ## 参考文献 - http://www.ruanyifeng.com/blog/2016/09/redux_tutorial_part_two_async_operations.html ================================================ FILE: docs/React/SyntheticEvent.md ================================================ # 面试官:说说React的事件机制? ![](https://static.vue-js.com/f054f080-d86f-11eb-ab90-d9ae814b240d.png) ## 一、是什么 `React`基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等 在`React`中这套事件机制被称之为合成事件 #### 合成事件(SyntheticEvent) 合成事件是 `React `模拟原生 `DOM `事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器 根据 `W3C `规范来定义合成事件,兼容所有浏览器,拥有与浏览器原生事件相同的接口,例如: ```jsx const button = ``` 如果想要获得原生`DOM`事件,可以通过`e.nativeEvent`属性获取 ```js const handleClick = (e) => console.log(e.nativeEvent);; const button = ``` 从上面可以看到`React`事件和原生事件也非常的相似,但也有一定的区别: - 事件名称命名方式不同 ```jsx // 原生事件绑定方式 // React 合成事件绑定方式 const button = ``` - 事件处理函数书写不同 ```jsx // 原生事件 事件处理函数写法 // React 合成事件 事件处理函数写法 const button = ``` 虽然`onclick`看似绑定到`DOM`元素上,但实际并不会把事件代理函数直接绑定到真实的节点上,而是把所有的事件绑定到结构的最外层,使用一个统一的事件去监听 这个事件监听器上维持了一个映射来保存所有组件内部的事件监听和处理函数。当组件挂载或卸载时,只是在这个统一的事件监听器上插入或删除一些对象 当事件发生时,首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。这样做简化了事件处理和回收机制,效率也有很大提升 ## 二、执行顺序 关于`React `合成事件与原生事件执行顺序,可以看看下面一个例子: ```jsx import React from 'react'; class App extends React.Component{ constructor(props) { super(props); this.parentRef = React.createRef(); this.childRef = React.createRef(); } componentDidMount() { console.log("React componentDidMount!"); this.parentRef.current?.addEventListener("click", () => { console.log("原生事件:父元素 DOM 事件监听!"); }); this.childRef.current?.addEventListener("click", () => { console.log("原生事件:子元素 DOM 事件监听!"); }); document.addEventListener("click", (e) => { console.log("原生事件:document DOM 事件监听!"); }); } parentClickFun = () => { console.log("React 事件:父元素事件监听!"); }; childClickFun = () => { console.log("React 事件:子元素事件监听!"); }; render() { return (
分析事件执行顺序
); } } export default App; ``` 输出顺序为: ```tex 原生事件:子元素 DOM 事件监听! 原生事件:父元素 DOM 事件监听! React 事件:子元素事件监听! React 事件:父元素事件监听! 原生事件:document DOM 事件监听! ``` 可以得出以下结论: - React 所有事件都挂载在 document 对象上 - 当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件 - 所以会先执行原生事件,然后处理 React 事件 - 最后真正执行 document 上挂载的事件 对应过程如图所示: ![](https://static.vue-js.com/08e22ff0-d870-11eb-ab90-d9ae814b240d.png) 所以想要阻止不同时间段的冒泡行为,对应使用不同的方法,对应如下: - 阻止合成事件间的冒泡,用e.stopPropagation() - 阻止合成事件与最外层 document 上的事件间的冒泡,用e.nativeEvent.stopImmediatePropagation() - 阻止合成事件与除最外层document上的原生事件上的冒泡,通过判断e.target来避免 ```js document.body.addEventListener('click', e => { if (e.target && e.target.matches('div.code')) { return; } this.setState({ active: false, }); }); } ``` ## 三、总结 `React`事件机制总结如下: - React 上注册的事件最终会绑定在document这个 DOM 上,而不是 React 组件对应的 DOM(减少内存开销就是因为所有的事件都绑定在 document 上,其他节点没有绑定事件) - React 自身实现了一套事件冒泡机制,所以这也就是为什么我们 event.stopPropagation()无效的原因。 - React 通过队列的形式,从触发的组件向父组件回溯,然后调用他们 JSX 中定义的 callback - React 有一套自己的合成事件 SyntheticEvent ## 参考文献 - https://zh-hans.reactjs.org/docs/events.html - https://segmentfault.com/a/1190000015725214?utm_source=sf-similar-article - https://segmentfault.com/a/1190000038251163 ================================================ FILE: docs/React/animation.md ================================================ # 面试官:在react中组件间过渡动画如何实现? ![](https://static.vue-js.com/294f1e00-e4b0-11eb-ab90-d9ae814b240d.png) ## 一、是什么 在日常开发中,页面切换时的转场动画是比较基础的一个场景 当一个组件在显示与消失过程中存在过渡动画,可以很好的增加用户的体验 在`react`中实现过渡动画效果会有很多种选择,如`react-transition-group`,`react-motion`,`Animated`,以及原生的`CSS`都能完成切换动画 ## 二、如何实现 在`react`中,`react-transition-group`是一种很好的解决方案,其为元素添加`enter`,`enter-active`,`exit`,`exit-active`这一系列勾子 可以帮助我们方便的实现组件的入场和离场动画 其主要提供了三个主要的组件: - CSSTransition:在前端开发中,结合 CSS 来完成过渡动画效果 - SwitchTransition:两个组件显示和隐藏切换时,使用该组件 - TransitionGroup:将多个动画组件包裹在其中,一般用于列表中元素的动画 ### CSSTransition 其实现动画的原理在于,当`CSSTransition`的`in`属性置为`true`时,`CSSTransition`首先会给其子组件加上`xxx-enter`、`xxx-enter-active`的`class`执行动画 当动画执行结束后,会移除两个`class`,并且添加`-enter-done`的`class` 所以可以利用这一点,通过`css`的`transition`属性,让元素在两个状态之间平滑过渡,从而得到相应的动画效果 当`in`属性置为`false`时,`CSSTransition`会给子组件加上`xxx-exit`和`xxx-exit-active`的`class`,然后开始执行动画,当动画结束后,移除两个`class`,然后添加`-enter-done`的`class` 如下例子: ```jsx export default class App2 extends React.PureComponent { state = {show: true}; onToggle = () => this.setState({show: !this.state.show}); render() { const {show} = this.state; return (
); } } ``` 对应`css`样式如下: ```css .fade-enter { opacity: 0; transform: translateX(100%); } .fade-enter-active { opacity: 1; transform: translateX(0); transition: all 500ms; } .fade-exit { opacity: 1; transform: translateX(0); } .fade-exit-active { opacity: 0; transform: translateX(-100%); transition: all 500ms; } ``` ### SwitchTransition `SwitchTransition`可以完成两个组件之间切换的炫酷动画 比如有一个按钮需要在`on`和`off`之间切换,我们希望看到`on`先从左侧退出,`off`再从右侧进入 `SwitchTransition`中主要有一个属性`mode`,对应两个值: - in-out:表示新组件先进入,旧组件再移除; - out-in:表示就组件先移除,新组建再进入 `SwitchTransition`组件里面要有`CSSTransition`,不能直接包裹你想要切换的组件 里面的`CSSTransition`组件不再像以前那样接受`in`属性来判断元素是何种状态,取而代之的是`key`属性 下面给出一个按钮入场和出场的示例,如下: ```jsx import { SwitchTransition, CSSTransition } from "react-transition-group"; export default class SwitchAnimation extends PureComponent { constructor(props) { super(props); this.state = { isOn: true } } render() { const {isOn} = this.state; return ( { } ) } btnClick() { this.setState({isOn: !this.state.isOn}) } } ``` `css`文件对应如下: ```css .btn-enter { transform: translate(100%, 0); opacity: 0; } .btn-enter-active { transform: translate(0, 0); opacity: 1; transition: all 500ms; } .btn-exit { transform: translate(0, 0); opacity: 1; } .btn-exit-active { transform: translate(-100%, 0); opacity: 0; transition: all 500ms; } ``` ### TransitionGroup 当有一组动画的时候,就可将这些`CSSTransition`放入到一个`TransitionGroup`中来完成动画 同样`CSSTransition`里面没有`in`属性,用到了`key`属性 `TransitionGroup`在感知`children`发生变化的时候,先保存移除的节点,当动画结束后才真正移除 其处理方式如下: - 插入的节点,先渲染dom,然后再做动画 - 删除的节点,先做动画,然后再删除dom 如下: ```jsx import React, { PureComponent } from 'react' import { CSSTransition, TransitionGroup } from 'react-transition-group'; export default class GroupAnimation extends PureComponent { constructor(props) { super(props); this.state = { friends: [] } } render() { return (
{ this.state.friends.map((item, index) => { return (
{item}
) }) }
) } addFriend() { this.setState({ friends: [...this.state.friends, "coderwhy"] }) } } ``` 对应`css`如下: ```css .friend-enter { transform: translate(100%, 0); opacity: 0; } .friend-enter-active { transform: translate(0, 0); opacity: 1; transition: all 500ms; } .friend-exit { transform: translate(0, 0); opacity: 1; } .friend-exit-active { transform: translate(-100%, 0); opacity: 0; transition: all 500ms; } ``` ## 参考文献 - https://segmentfault.com/a/1190000018861018 - https://mp.weixin.qq.com/s/14HneI7SpfrRHKtqgosIiA ================================================ FILE: docs/React/capture error.md ================================================ # 面试官:说说你在React项目是如何捕获错误的? ![](https://static.vue-js.com/8db1b5c0-f288-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 错误在我们日常编写代码是非常常见的 举个例子,在`react`项目中去编写组件内`JavaScript`代码错误会导致 `React` 的内部状态被破坏,导致整个应用崩溃,这是不应该出现的现象 作为一个框架,`react`也有自身对于错误的处理的解决方案 ## 二、如何做 为了解决出现的错误导致整个应用崩溃的问题,`react16`引用了**错误边界**新的概念 错误边界是一种 `React` 组件,这种组件可以捕获发生在其子组件树任何位置的 `JavaScript` 错误,并打印这些错误,同时展示降级 `UI`,而并不会渲染那些发生崩溃的子组件树 错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误 形成错误边界组件的两个条件: - 使用了 static getDerivedStateFromError() - 使用了 componentDidCatch() 抛出错误后,请使用 `static getDerivedStateFromError()` 渲染备用 UI ,使用 `componentDidCatch()` 打印错误信息,如下: ```jsx class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false }; } static getDerivedStateFromError(error) { // 更新 state 使下一次渲染能够显示降级后的 UI return { hasError: true }; } componentDidCatch(error, errorInfo) { // 你同样可以将错误日志上报给服务器 logErrorToMyService(error, errorInfo); } render() { if (this.state.hasError) { // 你可以自定义降级后的 UI 并渲染 return

Something went wrong.

; } return this.props.children; } } ``` 然后就可以把自身组件的作为错误边界的子组件,如下: ```jsx ``` 下面这些情况无法捕获到异常: - 事件处理 - 异步代码 - 服务端渲染 - 自身抛出来的错误 在`react 16`版本之后,会把渲染期间发生的所有错误打印到控制台 除了错误信息和 JavaScript 栈外,React 16 还提供了组件栈追踪。现在你可以准确地查看发生在组件树内的错误信息: ![](https://static.vue-js.com/7b2b51d0-f289-11eb-ab90-d9ae814b240d.png) 可以看到在错误信息下方文字中存在一个组件栈,便于我们追踪错误 对于错误边界无法捕获的异常,如事件处理过程中发生问题并不会捕获到,是因为其不会在渲染期间触发,并不会导致渲染时候问题 这种情况可以使用`js`的`try...catch...`语法,如下: ```jsx class MyComponent extends React.Component { constructor(props) { super(props); this.state = { error: null }; this.handleClick = this.handleClick.bind(this); } handleClick() { try { // 执行操作,如有错误则会抛出 } catch (error) { this.setState({ error }); } } render() { if (this.state.error) { return

Caught an error.

} return } } ``` 除此之外还可以通过监听`onerror`事件 ```js window.addEventListener('error', function(event) { ... }) ``` ## 参考文献 - https://zh-hans.reactjs.org/docs/error-boundaries.html ================================================ FILE: docs/React/class_function component.md ================================================ # 面试官:说说对React中类组件和函数组件的理解?有什么区别? ![](https://static.vue-js.com/6c196d80-de39-11eb-85f6-6fac77c0c9b3.png) ## 一、类组件 类组件,顾名思义,也就是通过使用`ES6`类的编写形式去编写组件,该类必须继承`React.Component` 如果想要访问父组件传递过来的参数,可通过`this.props`的方式去访问 在组件中必须实现`render`方法,在`return`中返回`React`对象,如下: ```jsx class Welcome extends React.Component { constructor(props) { super(props) } render() { return

Hello, {this.props.name}

} } ``` ## 二、函数组件 函数组件,顾名思义,就是通过函数编写的形式去实现一个`React`组件,是`React`中定义组件最简单的方式 ```jsx function Welcome(props) { return

Hello, {props.name}

; } ``` 函数第一个参数为`props`用于接收父组件传递过来的参数 ## 三、区别 针对两种`React`组件,其区别主要分成以下几大方向: - 编写形式 - 状态管理 - 生命周期 - 调用方式 - 获取渲染的值 ### 编写形式 两者最明显的区别在于编写形式的不同,同一种功能的实现可以分别对应类组件和函数组件的编写形式 函数组件: ```jsx function Welcome(props) { return

Hello, {props.name}

; } ``` 类组件: ```jsx class Welcome extends React.Component { constructor(props) { super(props) } render() { return

Hello, {this.props.name}

} } ``` ### 状态管理 在`hooks`出来之前,函数组件就是无状态组件,不能保管组件的状态,不像类组件中调用`setState` 如果想要管理`state`状态,可以使用`useState`,如下: ```jsx const FunctionalComponent = () => { const [count, setCount] = React.useState(0); return (

count: {count}

); }; ``` 在使用`hooks`情况下,一般如果函数组件调用`state`,则需要创建一个类组件或者`state`提升到你的父组件中,然后通过`props`对象传递到子组件 ### 生命周期 在函数组件中,并不存在生命周期,这是因为这些生命周期钩子都来自于继承的`React.Component` 所以,如果用到生命周期,就只能使用类组件 但是函数组件使用`useEffect`也能够完成替代生命周期的作用,这里给出一个简单的例子: ```jsx const FunctionalComponent = () => { useEffect(() => { console.log("Hello"); }, []); return

Hello, World

; }; ``` 上述简单的例子对应类组件中的`componentDidMount`生命周期 如果在`useEffect`回调函数中`return `一个函数,则`return`函数会在组件卸载的时候执行,正如`componentWillUnmount` ```jsx const FunctionalComponent = () => { React.useEffect(() => { return () => { console.log("Bye"); }; }, []); return

Bye, World

; }; ``` ### 调用方式 如果是一个函数组件,调用则是执行函数即可: ```jsx // 你的代码 function SayHi() { return

Hello, React

} // React内部 const result = SayHi(props) // »

Hello, React

``` 如果是一个类组件,则需要将组件进行实例化,然后调用实例对象的`render`方法: ```jsx // 你的代码 class SayHi extends React.Component { render() { return

Hello, React

} } // React内部 const instance = new SayHi(props) // » SayHi {} const result = instance.render() // »

Hello, React

``` ### 获取渲染的值 首先给出一个示例 函数组件对应如下: ```jsx function ProfilePage(props) { const showMessage = () => { alert('Followed ' + props.user); } const handleClick = () => { setTimeout(showMessage, 3000); } return ( ) } ``` 类组件对应如下: ```jsx class ProfilePage extends React.Component { showMessage() { alert('Followed ' + this.props.user); } handleClick() { setTimeout(this.showMessage.bind(this), 3000); } render() { return } } ``` 两者看起来实现功能是一致的,但是在类组件中,输出`this.props.user`,`Props `在 `React `中是不可变的所以它永远不会改变,但是 `this` 总是可变的,以便您可以在 `render` 和生命周期函数中读取新版本 因此,如果我们的组件在请求运行时更新。`this.props` 将会改变。`showMessage `方法从“最新”的 `props` 中读取 `user` 而函数组件,本身就不存在`this`,`props`并不发生改变,因此同样是点击,`alert`的内容仍旧是之前的内容 ### 小结 两种组件都有各自的优缺点 函数组件语法更短、更简单,这使得它更容易开发、理解和测试 而类组件也会因大量使用 `this `而让人感到困惑 ## 参考文献 - https://zh-hans.reactjs.org/docs/components-and-props.html#function-and-class-components - https://juejin.cn/post/6844903806140973069 ================================================ FILE: docs/React/communication.md ================================================ # 面试官:React中组件之间如何通信? ![](https://static.vue-js.com/767a2800-dc9f-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 我们将组件间通信可以拆分为两个词: - 组件 - 通信 回顾[Vue系列](https://mp.weixin.qq.com/s/uFjMz6BByA5eknBgkvgdeQ)的文章,组件是`vue`中最强大的功能之一,同样组件化是`React`的核心思想 相比`vue`,`React`的组件更加灵活和多样,按照不同的方式可以分成很多类型的组件 而通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的,广义上,任何信息的交通都是通信 组件间通信即指组件通过某种方式来传递信息以达到某个目的 ## 二、如何通信 组件传递的方式有很多种,根据传送者和接收者可以分为如下: - 父组件向子组件传递 - 子组件向父组件传递 - 兄弟组件之间的通信 - 父组件向后代组件传递 - 非关系组件传递 ### 父组件向子组件传递 由于`React`的数据流动为单向的,父组件向子组件传递是最常见的方式 父组件在调用子组件的时候,只需要在子组件标签内传递参数,子组件通过`props`属性就能接收父组件传递过来的参数 ```jsx function EmailInput(props) { return ( ); } const element = ; ``` ### 子组件向父组件传递 子组件向父组件通信的基本思路是,父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值 父组件对应代码如下: ```jsx class Parents extends Component { constructor() { super(); this.state = { price: 0 }; } getItemPrice(e) { this.setState({ price: e }); } render() { return (
price: {this.state.price}
{/* 向子组件中传入一个函数 */}
); } } ``` 子组件对应代码如下: ```jsx class Child extends Component { clickGoods(e) { // 在此函数中传入值 this.props.getPrice(e); } render() { return (
); } } ``` ### 兄弟组件之间的通信 如果是兄弟组件之间的传递,则父组件作为中间层来实现数据的互通,通过使用父组件传递 ```jsx class Parent extends React.Component { constructor(props) { super(props) this.state = {count: 0} } setCount = () => { this.setState({count: this.state.count + 1}) } render() { return (
); } } ``` ### 父组件向后代组件传递 父组件向后代组件传递数据是一件最普通的事情,就像全局数据一样 使用`context`提供了组件之间通讯的一种方式,可以共享数据,其他数据都能读取对应的数据 通过使用`React.createContext`创建一个`context` ```js const PriceContext = React.createContext('price') ``` `context`创建成功后,其下存在`Provider`组件用于创建数据源,`Consumer`组件用于接收数据,使用实例如下: `Provider`组件通过`value`属性用于给后代组件传递数据: ```jsx ``` 如果想要获取`Provider`传递的数据,可以通过`Consumer`组件或者或者使用`contextType`属性接收,对应分别如下: ```jsx class MyClass extends React.Component { static contextType = PriceContext; render() { let price = this.context; /* 基于这个值进行渲染工作 */ } } ``` `Consumer`组件: ````jsx { /*这里是一个函数*/ } { price =>
price:{price}
}
```` ### 非关系组件传递 如果组件之间关系类型比较复杂的情况,建议将数据进行一个全局资源管理,从而实现通信,例如`redux`。关于`redux`的使用后续再详细介绍 ## 三、总结 由于`React`是单向数据流,主要思想是组件不会改变接收的数据,只会监听数据的变化,当数据发生变化时它们会使用接收到的新值,而不是去修改已有的值 因此,可以看到通信过程中,数据的存储位置都是存放在上级位置中 ## 参考文献 - https://react.docschina.org/docs/context.html ================================================ FILE: docs/React/controlled_Uncontrolled.md ================================================ # 面试官:说说对受控组件和非受控组件的理解?应用场景? ![](https://static.vue-js.com/12990fd0-df2f-11eb-ab90-d9ae814b240d.png) ## 一、受控组件 受控组件,简单来讲,就是受我们控制的组件,组件的状态全程响应外部数据 举个简单的例子: ```jsx class TestComponent extends React.Component { constructor (props) { super(props); this.state = { username: 'lindaidai' }; } render () { return } } ``` 这时候当我们在输入框输入内容的时候,会发现输入的内容并无法显示出来,也就是`input`标签是一个可读的状态 这是因为`value`被`this.state.username`所控制住。当用户输入新的内容时,`this.state.username`并不会自动更新,这样的话`input`内的内容也就不会变了 如果想要解除被控制,可以为`input`标签设置`onChange`事件,输入的时候触发事件函数,在函数内部实现`state`的更新,从而导致`input`框的内容页发现改变 因此,受控组件我们一般需要初始状态和一个状态更新事件函数 ## 二、非受控组件 非受控组件,简单来讲,就是不受我们控制的组件 一般情况是在初始化的时候接受外部数据,然后自己在内部存储其自身状态 当需要时,可以使用` ref ` 查询 `DOM `并查找其当前值,如下: ```jsx import React, { Component } from 'react'; export class UnControll extends Component { constructor (props) { super(props); this.inputRef = React.createRef(); } handleSubmit = (e) => { console.log('我们可以获得input内的值为', this.inputRef.current.value); e.preventDefault(); } render () { return (
this.handleSubmit(e)}>
) } } ``` 关于`refs`的详情使用可以参考[之前文章](https://mp.weixin.qq.com/s/ZBKWcslVBi0IKQgz7lYzbA) ## 三、应用场景 大部分时候推荐使用受控组件来实现表单,因为在受控组件中,表单数据由`React`组件负责处理 如果选择非受控组件的话,控制能力较弱,表单数据就由`DOM`本身处理,但更加方便快捷,代码量少 针对两者的区别,其应用场景如下图所示: ![](https://static.vue-js.com/f28aed20-df2f-11eb-ab90-d9ae814b240d.png) ## 参考文献 - http://meloguo.com/2018/10/08/受控与非受控组件/ - https://zhuanlan.zhihu.com/p/37579677 ================================================ FILE: docs/React/diff.md ================================================ # 面试官:说说React diff的原理是什么? ![](https://static.vue-js.com/967e6150-ec91-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 跟`Vue`一致,`React`通过引入`Virtual DOM`的概念,极大地避免无效的`Dom`操作,使我们的页面的构建效率提到了极大的提升 而`diff`算法就是更高效地通过对比新旧`Virtual DOM`来找出真正的`Dom`变化之处 传统diff算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),`react`将算法进行一个优化,复杂度姜维`O(n)`,两者效率差距如下图: ![](https://static.vue-js.com/a43c9960-ec91-11eb-ab90-d9ae814b240d.png) ## 二、原理 `react`中`diff`算法主要遵循三个层级的策略: - tree层级 - conponent 层级 - element 层级 ### tree层级 `DOM`节点跨层级的操作不做优化,只会对相同层级的节点进行比较 ![](https://static.vue-js.com/ae71d1c0-ec91-11eb-85f6-6fac77c0c9b3.png) 只有删除、创建操作,没有移动操作,如下图: ![](https://static.vue-js.com/b85f2bb0-ec91-11eb-ab90-d9ae814b240d.png) `react`发现新树中,R节点下没有了A,那么直接删除A,在D节点下创建A以及下属节点 上述操作中,只有删除和创建操作 ### conponent层级 如果是同一个类的组件,则会继续往下`diff`运算,如果不是一个类的组件,那么直接删除这个组件下的所有子节点,创建新的 ![](https://static.vue-js.com/c1fcdf00-ec91-11eb-ab90-d9ae814b240d.png) 当`component D `换成了`component G` 后,即使两者的结构非常类似,也会将`D`删除再重新创建`G` ### element层级 对于比较同一层级的节点们,每个节点在对应的层级用唯一的`key`作为标识 提供了 3 种节点操作,分别为 `INSERT_MARKUP `(插入)、`MOVE_EXISTING` (移动)和 `REMOVE_NODE` (删除) 如下场景: ![](https://static.vue-js.com/cae1c9a0-ec91-11eb-ab90-d9ae814b240d.png) 通过`key`可以准确地发现新旧集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将旧集合中节点的位置进行移动,更新为新集合中节点的位置 流程如下表: ![](https://static.vue-js.com/d34c5420-ec91-11eb-85f6-6fac77c0c9b3.png) - index: 新集合的遍历下标。 - oldIndex:当前节点在老集合中的下标 - maxIndex:在新集合访问过的节点中,其在老集合的最大下标 如果当前节点在新集合中的位置比老集合中的位置靠前的话,是不会影响后续节点操作的,这里这时候被动字节不用动 操作过程中只比较oldIndex和maxIndex,规则如下: - 当oldIndex>maxIndex时,将oldIndex的值赋值给maxIndex - 当oldIndex=maxIndex时,不操作 - 当oldIndex1
2
3
4
5
``` 后续更改成[1,3,2,5,4],使用`key`与不使用`key`作用如下: ```html 1.加key
1
1
2
3
3
========>
2
4
5
5
4
操作:节点2移动至下标为2的位置,节点4移动至下标为4的位置。 2.不加key
1
1
2
3
3
========>
2
4
5
5
4
操作:修改第1个到第5个节点的innerText ``` 如果我们对这个集合进行增删的操作改成[1,3,2,5,6] ```html 1.加key
1
1
2
3
3
========>
2
4
5
5
6
操作:节点2移动至下标为2的位置,新增节点6至下标为4的位置,删除节点4。 2.不加key
1
1
2
3
3
========>
2
4
5
5
6
操作:修改第1个到第5个节点的innerText ``` 由于`dom`节点的移动操作开销是比较昂贵的,没有`key`的情况下要比有`key`的性能更好 ## 参考文献 - https://zhuanlan.zhihu.com/p/140489744 - https://zhuanlan.zhihu.com/p/20346379 ================================================ FILE: docs/React/how to use redux.md ================================================ # 面试官:你在React项目中是如何使用Redux的? 项目结构是如何划分的? ![](https://static.vue-js.com/31a4aff0-e7dc-11eb-ab90-d9ae814b240d.png) ## 一、背景 在前面文章了解中,我们了解到`redux`是用于数据状态管理,而`react`是一个视图层面的库 如果将两者连接在一起,可以使用官方推荐`react-redux`库,其具有高效且灵活的特性 `react-redux`将组件分成: - 容器组件:存在逻辑处理 - UI 组件:只负责现显示和交互,内部不处理逻辑,状态由外部控制 通过`redux`将整个应用状态存储到`store`中,组件可以派发`dispatch`行为`action`给`store` 其他组件通过订阅`store`中的状态`state`来更新自身的视图 ## 二、如何做 使用`react-redux`分成了两大核心: - Provider - connection ### Provider 在`redux`中存在一个`store`用于存储`state`,如果将这个`store`存放在顶层元素中,其他组件都被包裹在顶层元素之上 那么所有的组件都能够受到`redux`的控制,都能够获取到`redux`中的数据 使用方式如下: ```js ``` ### connection `connect`方法将`store`上的`getState `和 `dispatch `包装成组件的`props` 导入`conect`如下: ```js import { connect } from "react-redux"; ``` 用法如下: ```js connect(mapStateToProps, mapDispatchToProps)(MyComponent) ``` 可以传递两个参数: - mapStateToProps - mapDispatchToProps ### mapStateToProps 把`redux`中的数据映射到`react`中的`props`中去 如下: ```jsx const mapStateToProps = (state) => { return { // prop : state.xxx | 意思是将state中的某个数据映射到props中 foo: state.bar } } ``` 组件内部就能够通过`props`获取到`store`中的数据 ```cons class Foo extends Component { constructor(props){ super(props); } render(){ return( // 这样子渲染的其实就是state.bar的数据了
this.props.foo
) } } Foo = connect()(Foo) export default Foo ``` ### mapDispatchToProps 将`redux`中的`dispatch`映射到组件内部的`props`中 ```jsx const mapDispatchToProps = (dispatch) => { // 默认传递参数就是dispatch return { onClick: () => { dispatch({ type: 'increatment' }); } }; } ``` ```js class Foo extends Component { constructor(props){ super(props); } render(){ return( ) } } Foo = connect()(Foo); export default Foo; ``` ### 小结 整体流程图大致如下所示: ![](https://static.vue-js.com/3e47db10-e7dc-11eb-85f6-6fac77c0c9b3.png) ## 三、项目结构 可以根据项目具体情况进行选择,以下列出两种常见的组织结构 #### 按角色组织(MVC) 角色如下: - reducers - actions - components - containers 参考如下: ```js reducers/ todoReducer.js filterReducer.js actions/ todoAction.js filterActions.js components/ todoList.js todoItem.js filter.js containers/ todoListContainer.js todoItemContainer.js filterContainer.js ``` #### 按功能组织 使用`redux`使用功能组织项目,也就是把完成同一应用功能的代码放在一个目录下,一个应用功能包含多个角色的代码 `Redux`中,不同的角色就是`reducer`、`actions`和视图,而应用功能对应的就是用户界面的交互模块 参考如下: ```js todoList/ actions.js actionTypes.js index.js reducer.js views/ components.js containers.js filter/ actions.js actionTypes.js index.js reducer.js views/ components.js container.js ``` 每个功能模块对应一个目录,每个目录下包含同样的角色文件: - actionTypes.js 定义action类型 - actions.js 定义action构造函数 - reducer.js 定义这个功能模块如果响应actions.js定义的动作 - views 包含功能模块中所有的React组件,包括展示组件和容器组件 - index.js 把所有的角色导入,统一导出 其中`index`模块用于导出对外的接口 ```js import * as actions from './actions.js'; import reducer from './reducer.js'; import view from './views/container.js'; export { actions, reducer, view }; ``` 导入方法如下: ```js import { actions, reducer, view as TodoList } from './xxxx' ``` ## 参考文献 - https://www.redux.org.cn/docs/basics/UsageWithReact.html - https://segmentfault.com/a/1190000010384268 ================================================ FILE: docs/React/immutable.md ================================================ # 面试官:说说你对immutable的理解?如何应用在react项目中? ![](https://static.vue-js.com/797e9470-ea3f-11eb-ab90-d9ae814b240d.png) ## 一、是什么 Immutable,不可改变的,在计算机中,即指一旦创建,就不能再被更改的数据 对 `Immutable `对象的任何修改或添加删除操作都会返回一个新的 `Immutable `对象 `Immutable` 实现的原理是 `Persistent Data Structure`(持久化数据结构): - 用一种数据结构来保存数据 - 当数据被修改时,会返回一个对象,但是新的对象会尽可能的利用之前的数据结构而不会对内存造成浪费 也就是使用旧数据创建新数据时,要保证旧数据同时可用且不变,同时为了避免 `deepCopy `把所有节点都复制一遍带来的性能损耗,`Immutable` 使用了 `Structural Sharing`(结构共享) 如果对象树中一个节点发生变化,只修改这个节点和受它影响的父节点,其它节点则进行共享 如下图所示: ![](https://pic4.zhimg.com/80/2b4c801a7b40eefcd4ee6767fb984fdf_720w.gif) ## 二、如何使用 使用`Immutable`对象最主要的库是`immutable.js` immutable.js 是一个完全独立的库,无论基于什么框架都可以用它 其出现场景在于弥补 Javascript 没有不可变数据结构的问题,通过 structural sharing来解决的性能问题 内部提供了一套完整的 Persistent Data Structure,还有很多易用的数据类型,如`Collection`、`List`、`Map`、`Set`、`Record`、`Seq`,其中: - List: 有序索引集,类似 JavaScript 中的 Array - Map: 无序索引集,类似 JavaScript 中的 Object - Set: 没有重复值的集合 主要的方法如下: - fromJS():将一个js数据转换为Immutable类型的数据 ```js const obj = Immutable.fromJS({a:'123',b:'234'}) ``` - toJS():将一个Immutable数据转换为JS类型的数据 - is():对两个对象进行比较 ```js import { Map, is } from 'immutable' const map1 = Map({ a: 1, b: 1, c: 1 }) const map2 = Map({ a: 1, b: 1, c: 1 }) map1 === map2 //false Object.is(map1, map2) // false is(map1, map2) // true ``` - get(key):对数据或对象取值 - getIn([]) :对嵌套对象或数组取值,传参为数组,表示位置 ```js let abs = Immutable.fromJS({a: {b:2}}); abs.getIn(['a', 'b']) // 2 abs.getIn(['a', 'c']) // 子级没有值 let arr = Immutable.fromJS([1 ,2, 3, {a: 5}]); arr.getIn([3, 'a']); // 5 arr.getIn([3, 'c']); // 子级没有值 ``` - 如下例子:使用方法如下: ```js import Immutable from 'immutable'; foo = Immutable.fromJS({a: {b: 1}}); bar = foo.setIn(['a', 'b'], 2); // 使用 setIn 赋值 console.log(foo.getIn(['a', 'b'])); // 使用 getIn 取值,打印 1 console.log(foo === bar); // 打印 false ``` 如果换到原生的`js`,则对应如下: ```js let foo = {a: {b: 1}}; let bar = foo; bar.a.b = 2; console.log(foo.a.b); // 打印 2 console.log(foo === bar); // 打印 true ``` ## 三、在React中应用 使用 `Immutable `可以给 `React` 应用带来性能的优化,主要体现在减少渲染的次数 在做`react`性能优化的时候,为了避免重复渲染,我们会在`shouldComponentUpdate()`中做对比,当返回`true`执行`render`方法 `Immutable`通过`is`方法则可以完成对比,而无需像一样通过深度比较的方式比较 在使用`redux`过程中也可以结合`Immutable`,不使用`Immutable`前修改一个数据需要做一个深拷贝 ```jsx import '_' from 'lodash'; const Component = React.createClass({ getInitialState() { return { data: { times: 0 } } }, handleAdd() { let data = _.cloneDeep(this.state.data); data.times = data.times + 1; this.setState({ data: data }); } } ``` 使用 Immutable 后: ```jsx getInitialState() { return { data: Map({ times: 0 }) } }, handleAdd() { this.setState({ data: this.state.data.update('times', v => v + 1) }); // 这时的 times 并不会改变 console.log(this.state.data.get('times')); } ``` 同理,在`redux`中也可以将数据进行`fromJS`处理 ```js import * as constants from './constants' import {fromJS} from 'immutable' const defaultState = fromJS({ //将数据转化成immutable数据 home:true, focused:false, mouseIn:false, list:[], page:1, totalPage:1 }) export default(state=defaultState,action)=>{ switch(action.type){ case constants.SEARCH_FOCUS: return state.set('focused',true) //更改immutable数据 case constants.CHANGE_HOME_ACTIVE: return state.set('home',action.value) case constants.SEARCH_BLUR: return state.set('focused',false) case constants.CHANGE_LIST: // return state.set('list',action.data).set('totalPage',action.totalPage) //merge效率更高,执行一次改变多个数据 return state.merge({ list:action.data, totalPage:action.totalPage }) case constants.MOUSE_ENTER: return state.set('mouseIn',true) case constants.MOUSE_LEAVE: return state.set('mouseIn',false) case constants.CHANGE_PAGE: return state.set('page',action.page) default: return state } } ``` ## 参考文献 - https://zhuanlan.zhihu.com/p/20295971?spm=a2c4e.11153940.blogcont69516.18.4f275a00EzBHjr&columnSlug=purerender - https://www.jianshu.com/p/7bf04638e82a ================================================ FILE: docs/React/import css.md ================================================ # 面试官:说说react中引入css的方式有哪几种?区别? ![](https://static.vue-js.com/7d825230-e217-11eb-ab90-d9ae814b240d.png) ## 一、是什么 组件式开发选择合适的`css`解决方案尤为重要 通常会遵循以下规则: - 可以编写局部css,不会随意污染其他组件内的原生; - 可以编写动态的css,可以获取当前组件的一些状态,根据状态的变化生成不同的css样式; - 支持所有的css特性:伪类、动画、媒体查询等; - 编写起来简洁方便、最好符合一贯的css风格特点 在这一方面,`vue`使用`css`起来更为简洁: - 通过 style 标签编写样式 - scoped 属性决定编写的样式是否局部有效 - lang 属性设置预处理器 - 内联样式风格的方式来根据最新状态设置和改变css 而在`react`中,引入`CSS`就不如`Vue`方便简洁,其引入`css`的方式有很多种,各有利弊 ## 二、方式 常见的`CSS`引入方式有以下: - 在组件内直接使用 - 组件中引入 .css 文件 - 组件中引入 .module.css 文件 - CSS in JS ### 在组件内直接使用 直接在组件中书写`css`样式,通过`style`属性直接引入,如下: ```js import React, { Component } from "react"; const div1 = { width: "300px", margin: "30px auto", backgroundColor: "#44014C", //驼峰法 minHeight: "200px", boxSizing: "border-box" }; class Test extends Component { constructor(props, context) { super(props); } render() { return (
123
); } } export default Test; ``` 上面可以看到,`css`属性需要转换成驼峰写法 这种方式优点: - 内联样式, 样式之间不会有冲突 - 可以动态获取当前state中的状态 缺点: - 写法上都需要使用驼峰标识 - 某些样式没有提示 - 大量的样式, 代码混乱 - 某些样式无法编写(比如伪类/伪元素) ### 组件中引入css文件 将`css`单独写在一个`css`文件中,然后在组件中直接引入 `App.css`文件: ```css .title { color: red; font-size: 20px; } .desc { color: green; text-decoration: underline; } ``` 组件中引入: ```js import React, { PureComponent } from 'react'; import Home from './Home'; import './App.css'; export default class App extends PureComponent { render() { return (

我是App的标题

我是App中的一段文字描述

) } } ``` 这种方式存在不好的地方在于样式是全局生效,样式之间会互相影响 ### 组件中引入 .module.css 文件 将`css`文件作为一个模块引入,这个模块中的所有`css`,只作用于当前组件。不会影响当前组件的后代组件 这种方式是`webpack`特工的方案,只需要配置`webpack`配置文件中`modules:true`即可 ```jsx import React, { PureComponent } from 'react'; import Home from './Home'; import './App.module.css'; export default class App extends PureComponent { render() { return (

我是App的标题

我是App中的一段文字描述

) } } ``` 这种方式能够解决局部作用域问题,但也有一定的缺陷: - 引用的类名,不能使用连接符(.xxx-xx),在 JavaScript 中是不识别的 - 所有的 className 都必须使用 {style.className} 的形式来编写 - 不方便动态来修改某些样式,依然需要使用内联样式的方式; ### CSS in JS CSS-in-JS, 是指一种模式,其中` CSS `由 `JavaScript `生成而不是在外部文件中定义 此功能并不是 React 的一部分,而是由第三方库提供,例如: - styled-components - emotion - glamorous 下面主要看看`styled-components`的基本使用 本质是通过函数的调用,最终创建出一个组件: - 这个组件会被自动添加上一个不重复的class - styled-components会给该class添加相关的样式 基本使用如下: 创建一个`style.js`文件用于存放样式组件: ```js export const SelfLink = styled.div` height: 50px; border: 1px solid red; color: yellow; `; export const SelfButton = styled.div` height: 150px; width: 150px; color: ${props => props.color}; background-image: url(${props => props.src}); background-size: 150px 150px; `; ``` 引入样式组件也很简单: ```jsx import React, { Component } from "react"; import { SelfLink, SelfButton } from "./style"; class Test extends Component { constructor(props, context) { super(props); } render() { return (
app.js SelfButton
); } } export default Test; ``` ## 三、区别 通过上面四种样式的引入,可以看到: - 在组件内直接使用`css`该方式编写方便,容易能够根据状态修改样式属性,但是大量的演示编写容易导致代码混乱 - 组件中引入 .css 文件符合我们日常的编写习惯,但是作用域是全局的,样式之间会层叠 - 引入.module.css 文件能够解决局部作用域问题,但是不方便动态修改样式,需要使用内联的方式进行样式的编写 - 通过css in js 这种方法,可以满足大部分场景的应用,可以类似于预处理器一样样式嵌套、定义、修改状态等 至于使用`react`用哪种方案引入`css`,并没有一个绝对的答案,可以根据各自情况选择合适的方案 ## 参考文献 - https://zh-hans.reactjs.org/docs/faq-styling.html#gatsby-focus-wrapper - https://mp.weixin.qq.com/s/oywTpNKEikMXn8QTBgITow ================================================ FILE: docs/React/improve_render.md ================================================ # 面试官:说说你是如何提高组件的渲染效率的?在React中如何避免不必要的render? ![](https://static.vue-js.com/de2d7e20-ecf8-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 `react` 基于虚拟 `DOM` 和高效 `Diff `算法的完美配合,实现了对 `DOM `最小粒度的更新,大多数情况下,`React `对 `DOM `的渲染效率足以我们的业务日常 复杂业务场景下,性能问题依然会困扰我们。此时需要采取一些措施来提升运行性能,避免不必要的渲染则是业务中常见的优化手段之一 ## 二、如何做 在之前文章中,我们了解到`render`的触发时机,简单来讲就是类组件通过调用`setState`方法, 就会导致`render`,父组件一旦发生`render`渲染,子组件一定也会执行`render`渲染 从上面可以看到,父组件渲染导致子组件渲染,子组件并没有发生任何改变,这时候就可以从避免无谓的渲染,具体实现的方式有如下: - shouldComponentUpdate - PureComponent - React.memo ### shouldComponentUpdate 通过`shouldComponentUpdate`生命周期函数来比对 `state `和 `props`,确定是否要重新渲染 默认情况下返回`true`表示重新渲染,如果不希望组件重新渲染,返回 `false` 即可 ### PureComponent 跟`shouldComponentUpdate `原理基本一致,通过对 `props` 和 `state`的浅比较结果来实现 `shouldComponentUpdate`,源码大致如下: ```js if (this._compositeType === CompositeTypes.PureClass) { shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState); } ``` `shallowEqual`对应方法大致如下: ```js const hasOwnProperty = Object.prototype.hasOwnProperty; /** * is 方法来判断两个值是否是相等的值,为何这么写可以移步 MDN 的文档 * https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/is */ function is(x: mixed, y: mixed): boolean { if (x === y) { return x !== 0 || y !== 0 || 1 / x === 1 / y; } else { return x !== x && y !== y; } } function shallowEqual(objA: mixed, objB: mixed): boolean { // 首先对基本类型进行比较 if (is(objA, objB)) { return true; } if (typeof objA !== 'object' || objA === null || typeof objB !== 'object' || objB === null) { return false; } const keysA = Object.keys(objA); const keysB = Object.keys(objB); // 长度不相等直接返回false if (keysA.length !== keysB.length) { return false; } // key相等的情况下,再去循环比较 for (let i = 0; i < keysA.length; i++) { if ( !hasOwnProperty.call(objB, keysA[i]) || !is(objA[keysA[i]], objB[keysA[i]]) ) { return false; } } return true; } ``` 当对象包含复杂的数据结构时,对象深层的数据已改变却没有触发 `render` 注意:在`react`中,是不建议使用深层次结构的数据 ### React.memo `React.memo`用来缓存组件的渲染,避免不必要的更新,其实也是一个高阶组件,与 `PureComponent` 十分类似。但不同的是, `React.memo` 只能用于函数组件 ```jsx import { memo } from 'react'; function Button(props) { // Component code } export default memo(Button); ``` 如果需要深层次比较,这时候可以给`memo`第二个参数传递比较函数 ```jsx function arePropsEqual(prevProps, nextProps) { // your code return prevProps === nextProps; } export default memo(Button, arePropsEqual); ``` ## 三、总结 在实际开发过程中,前端性能问题是一个必须考虑的问题,随着业务的复杂,遇到性能问题的概率也在增高 除此之外,建议将页面进行更小的颗粒化,如果一个过大,当状态发生修改的时候,就会导致整个大组件的渲染,而对组件进行拆分后,粒度变小了,也能够减少子组件不必要的渲染 ## 参考文献 - https://juejin.cn/post/6844903781679759367#heading-12 ================================================ FILE: docs/React/key.md ================================================ # 面试官:React中的key有什么作用? ![](https://static.vue-js.com/31677360-dd69-11eb-ab90-d9ae814b240d.png) ## 一、是什么 首先,先给出`react`组件中进行列表渲染的一个示例: ```jsx const data = [ { id: 0, name: 'abc' }, { id: 1, name: 'def' }, { id: 2, name: 'ghi' }, { id: 3, name: 'jkl' } ]; const ListItem = (props) => { return
  • {props.name}
  • ; }; const List = () => { return (
      {data.map((item) => ( ))}
    ); }; ``` 然后在输出就可以看到`react`所提示的警告信息: ```tex Each child in a list should have a unique "key" prop. ``` 根据意思就可以得到渲染列表的每一个子元素都应该需要一个唯一的`key`值 在这里可以使用列表的`id`属性作为`key`值以解决上面这个警告 ```jsx const List = () => { return (
      {data.map((item) => ( ))}
    ); }; ``` ## 二、作用 跟`Vue`一样,`React` 也存在 `Diff`算法,而元素`key`属性的作用是用于判断元素是新创建的还是被移动的元素,从而减少不必要的元素渲染 因此`key`的值需要为每一个元素赋予一个确定的标识 如果列表数据渲染中,在数据后面插入一条数据,`key`作用并不大,如下: ```jsx this.state = { numbers:[111,222,333] } insertMovie() { const newMovies = [...this.state.numbers, 444]; this.setState({ movies: newMovies }) }
      { this.state.movies.map((item, index) => { return
    • {item}
    • }) }
    ``` 前面的元素在`diff`算法中,前面的元素由于是完全相同的,并不会产生删除创建操作,在最后一个比较的时候,则需要插入到新的`DOM`树中 因此,在这种情况下,元素有无`key`属性意义并不大 下面再来看看在前面插入数据时,使用`key`与不使用`key`的区别: ```js insertMovie() { const newMovies = [000 ,...this.state.numbers]; this.setState({ movies: newMovies }) } ``` 当拥有`key`的时候,`react`根据`key`属性匹配原有树上的子元素以及最新树上的子元素,像上述情况只需要将000元素插入到最前面位置 当没有`key`的时候,所有的`li`标签都需要进行修改 同样,并不是拥有`key`值代表性能越高,如果说只是文本内容改变了,不写`key`反而性能和效率更高 主要是因为不写`key`是将所有的文本内容替换一下,节点不会发生变化 而写`key`则涉及到了节点的增和删,发现旧`key`不存在了,则将其删除,新`key`在之前没有,则插入,这就增加性能的开销 ## 三、总结 良好使用`key`属性是性能优化的非常关键的一步,注意事项为: - key 应该是唯一的 - key不要使用随机值(随机数在下一次 render 时,会重新生成一个数字) - 使用 index 作为 key值,对性能没有优化 `react`判断`key`的流程具体如下图: ![](https://static.vue-js.com/3b9afe10-dd69-11eb-ab90-d9ae814b240d.png) ## 参考文献 - https://zh-hans.reactjs.org/docs/lists-and-keys.html#gatsby-focus-wrapper - https://segmentfault.com/a/1190000017511836 ================================================ FILE: docs/React/life cycle.md ================================================ # 面试官:说说 React 生命周期有哪些不同阶段?每个阶段对应的方法是? ![](https://static.vue-js.com/5c717010-d373-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 在[以前文章](https://mp.weixin.qq.com/s?__biz=MzU1OTgxNDQ1Nw==&mid=2247484176&idx=1&sn=5623421ed2678046ed9e438aadf6e26f&chksm=fc10c146cb67485015f24f7e9f5862c4c685fc33485fe30e1b375a534b4031978439c554e0c0&scene=178&cur_album_id=1711105826272116736#rd)中,我们了解到生命周期定义 生命周期`(Life Cycle)`的概念应用很广泛,特别是在经济、环境、技术、社会等诸多领域经常出现,其基本涵义可以通俗地理解为“从摇篮到坟墓”`(Cradle-to-Grave)`的整个过程 跟`Vue`一样,`React`整个组件生命周期包括从创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程 ## 二、流程 这里主要讲述`react16.4`之后的生命周期,可以分成三个阶段: - 创建阶段 - 更新阶段 - 卸载阶段 ### 创建阶段 创建阶段主要分成了以下几个生命周期方法: - constructor - getDerivedStateFromProps - render - componentDidMount #### constructor 实例过程中自动调用的方法,在方法内部通过`super`关键字获取来自父组件的`props` 在该方法中,通常的操作为初始化`state`状态或者在`this`上挂载方法 ### getDerivedStateFromProps 该方法是新增的生命周期方法,是一个静态的方法,因此不能访问到组件的实例 执行时机:组件创建和更新阶段,不论是`props`变化还是`state`变化,也会调用 在每次`render`方法前调用,第一个参数为即将更新的`props`,第二个参数为上一个状态的`state`,可以比较`props` 和 `state`来加一些限制条件,防止无用的state更新 该方法需要返回一个新的对象作为新的`state`或者返回`null`表示`state`状态不需要更新 ### render 类组件必须实现的方法,用于渲染`DOM`结构,可以访问组件`state`与`prop`属性 注意: 不要在 `render` 里面 `setState`, 否则会触发死循环导致内存崩溃 ### componentDidMount 组件挂载到真实`DOM`节点后执行,其在`render`方法之后执行 此方法多用于执行一些数据获取,事件监听等操作 ### 更新阶段 该阶段的函数主要为如下方法: - getDerivedStateFromProps - shouldComponentUpdate - render - getSnapshotBeforeUpdate - componentDidUpdate ### getDerivedStateFromProps 该方法介绍同上 ## shouldComponentUpdate 用于告知组件本身基于当前的`props`和`state`是否需要重新渲染组件,默认情况返回`true` 执行时机:到新的props或者state时都会调用,通过返回true或者false告知组件更新与否 一般情况,不建议在该周期方法中进行深层比较,会影响效率 同时也不能调用`setState`,否则会导致无限循环调用更新 ### render 介绍如上 ### getSnapshotBeforeUpdate 该周期函数在`render`后执行,执行之时`DOM`元素还没有被更新 该方法返回的一个`Snapshot`值,作为`componentDidUpdate`第三个参数传入 ```jsx getSnapshotBeforeUpdate(prevProps, prevState) { console.log('#enter getSnapshotBeforeUpdate'); return 'foo'; } componentDidUpdate(prevProps, prevState, snapshot) { console.log('#enter componentDidUpdate snapshot = ', snapshot); } ``` 此方法的目的在于获取组件更新前的一些信息,比如组件的滚动位置之类的,在组件更新后可以根据这些信息恢复一些UI视觉上的状态 ### componentDidUpdate 执行时机:组件更新结束后触发 在该方法中,可以根据前后的`props`和`state`的变化做相应的操作,如获取数据,修改`DOM`样式等 ### 卸载阶段 ## componentWillUnmount 此方法用于组件卸载前,清理一些注册是监听事件,或者取消订阅的网络请求等 一旦一个组件实例被卸载,其不会被再次挂载,而只可能是被重新创建 ## 三、总结 新版生命周期整体流程如下图所示: ![](https://static.vue-js.com/66c999c0-d373-11eb-85f6-6fac77c0c9b3.png) 旧的生命周期流程图如下: ![](https://static.vue-js.com/d379e420-d374-11eb-ab90-d9ae814b240d.png) 通过两个图的对比,可以发现新版的生命周期减少了以下三种方法: - componentWillMount - componentWillReceiveProps - componentWillUpdate 其实这三个方法仍然存在,只是在前者加上了`UNSAFE_`前缀,如`UNSAFE_componentWillMount`,并不像字面意思那样表示不安全,而是表示这些生命周期的代码可能在未来的 `react `版本可能废除 同时也新增了两个生命周期函数: - getDerivedStateFromProps - getSnapshotBeforeUpdate ## 参考文献 - https://github.com/pomelovico/keep/issues/23 - https://segmentfault.com/a/1190000020268993 ================================================ FILE: docs/React/redux.md ================================================ # 说说你对Redux的理解?其工作原理? ![](https://static.vue-js.com/52394be0-e2a5-11eb-ab90-d9ae814b240d.png) ## 一、是什么 `React`是用于构建用户界面的,帮助我们解决渲染`DOM`的过程 而在整个应用中会存在很多个组件,每个组件的`state`是由自身进行管理,包括组件定义自身的`state`、组件之间的通信通过`props`传递、使用`Context`实现数据共享 如果让每个组件都存储自身相关的状态,理论上来讲不会影响应用的运行,但在开发及后续维护阶段,我们将花费大量精力去查询状态的变化过程 这种情况下,如果将所有的状态进行集中管理,当需要更新状态的时候,仅需要对这个管理集中处理,而不用去关心状态是如何分发到每一个组件内部的 `redux`就是一个实现上述集中管理的容器,遵循三大基本原则: - 单一数据源 - state 是只读的 - 使用纯函数来执行修改 注意的是,`redux`并不是只应用在`react`中,还与其他界面库一起使用,如`Vue` ## 二、工作原理 `redux `要求我们把数据都放在 `store `公共存储空间 一个组件改变了 `store` 里的数据内容,其他组件就能感知到 `store `的变化,再来取数据,从而间接的实现了这些数据传递的功能 工作流程图如下所示: ![](https://static.vue-js.com/27b2e930-e56b-11eb-85f6-6fac77c0c9b3.png) 根据流程图,可以想象,`React Components` 是借书的用户, `Action Creactor` 是借书时说的话(借什么书), `Store` 是图书馆管理员,`Reducer` 是记录本(借什么书,还什么书,在哪儿,需要查一下), `state` 是书籍信息 整个流程就是借书的用户需要先存在,然后需要借书,需要一句话来描述借什么书,图书馆管理员听到后需要查一下记录本,了解图书的位置,最后图书馆管理员会把这本书给到这个借书人 转换为代码是,`React Components` 需要获取一些数据, 然后它就告知 `Store` 需要获取数据,这就是就是 `Action Creactor` , `Store` 接收到之后去 `Reducer` 查一下, `Reducer` 会告诉 `Store` 应该给这个组件什么数据 ## 三、如何使用 创建一个`store`的公共数据区域 ```js import { createStore } from 'redux' // 引入一个第三方的方法 const store = createStore() // 创建数据的公共存储区域(管理员) ``` 还需要创建一个记录本去辅助管理数据,也就是`reduecer`,本质就是一个函数,接收两个参数`state`,`action`,返回`state` ```js // 设置默认值 const initialState = { counter: 0 } const reducer = (state = initialState, action) => { } ``` 然后就可以将记录本传递给`store`,两者建立连接。如下: ```js const store = createStore(reducer) ``` 如果想要获取`store`里面的数据,则通过`store.getState()`来获取当前`state` ```js console.log(store.getState()); ``` 下面再看看如何更改`store`里面数据,是通过`dispatch`来派发`action`,通常`action`中都会有`type`属性,也可以携带其他的数据 ```js store.dispatch({ type: "INCREMENT" }) store.dispath({ type: "DECREMENT" }) store.dispatch({ type: "ADD_NUMBER", number: 5 }) ``` 下面再来看看修改`reducer`中的处理逻辑: ```js const reducer = (state = initialState, action) => { switch (action.type) { case "INCREMENT": return {...state, counter: state.counter + 1}; case "DECREMENT": return {...state, counter: state.counter - 1}; case "ADD_NUMBER": return {...state, counter: state.counter + action.number} default: return state; } } ``` 注意,`reducer`是一个纯函数,不需要直接修改`state` 这样派发`action`之后,既可以通过`store.subscribe`监听`store`的变化,如下: ```js store.subscribe(() => { console.log(store.getState()); }) ``` 在`React`项目中,会搭配`react-redux`进行使用 完整代码如下: ```js const redux = require('redux'); const initialState = { counter: 0 } // 创建reducer const reducer = (state = initialState, action) => { switch (action.type) { case "INCREMENT": return {...state, counter: state.counter + 1}; case "DECREMENT": return {...state, counter: state.counter - 1}; case "ADD_NUMBER": return {...state, counter: state.counter + action.number} default: return state; } } // 根据reducer创建store const store = redux.createStore(reducer); store.subscribe(() => { console.log(store.getState()); }) // 修改store中的state store.dispatch({ type: "INCREMENT" }) // console.log(store.getState()); store.dispatch({ type: "DECREMENT" }) // console.log(store.getState()); store.dispatch({ type: "ADD_NUMBER", number: 5 }) // console.log(store.getState()); ``` ### 小结 - createStore可以帮助创建 store - store.dispatch 帮助派发 action , action 会传递给 store - store.getState 这个方法可以帮助获取 store 里边所有的数据内容 - store.subscrible 方法订阅 store 的改变,只要 store 发生改变, store.subscrible 这个函数接收的这个回调函数就会被执行 ## 参考文献 - https://cn.redux.js.org/docs/introduction/ - https://www.redux.org.cn/docs/basics/Actions.html - https://lulujianglab.com/posts/大白话解析 Redux 、 redux-thunk 、redux-saga 和 react-redux ================================================ FILE: docs/React/render.md ================================================ # 面试官:说说React render方法的原理?在什么时候会被触发? ![](https://static.vue-js.com/3d855230-ec6d-11eb-ab90-d9ae814b240d.png) ## 一、原理 首先,`render`函数在`react`中有两种形式: 在类组件中,指的是`render`方法: ```jsx class Foo extends React.Component { render() { return

    Foo

    ; } } ``` 在函数组件中,指的是函数组件本身: ```js function Foo() { return

    Foo

    ; } ``` 在`render`中,我们会编写`jsx`,`jsx`通过`babel`编译后就会转化成我们熟悉的`js`格式,如下: ```jsx return (
    hello
    start
    Right Reserve
    ) ``` `babel`编译后: ```js return ( React.createElement( 'div', { className : 'cn' }, React.createElement( Header, null, 'hello' ), React.createElement( 'div', null, 'start' ), 'Right Reserve' ) ) ``` 从名字上来看,`createElement`方法用来元素的 在`react`中,这个元素就是虚拟`DOM`树的节点,接收三个参数: - type:标签 - attributes:标签属性,若无则为null - children:标签的子节点 这些虚拟`DOM`树最终会渲染成真实`DOM` 在`render`过程中,`React` 将新调用的 `render `函数返回的树与旧版本的树进行比较,这一步是决定如何更新 `DOM` 的必要步骤,然后进行 `diff` 比较,更新 `DOM `树 ## 二、触发时机 `render`的执行时机主要分成了两部分: - 类组件调用 setState 修改状态 ```jsx class Foo extends React.Component { state = { count: 0 }; increment = () => { const { count } = this.state; const newCount = count < 10 ? count + 1 : count; this.setState({ count: newCount }); }; render() { const { count } = this.state; console.log("Foo render"); return (

    {count}

    ); } } ``` 点击按钮,则调用`setState`方法,无论`count`发生变化辩护,控制台都会输出`Foo render`,证明`render`执行了 - 函数组件通过`useState hook`修改状态 ```jsx function Foo() { const [count, setCount] = useState(0); function increment() { const newCount = count < 10 ? count + 1 : count; setCount(newCount); } console.log("Foo render"); return (

    {count}

    ); } ``` 函数组件通过`useState`这种形式更新数据,当数组的值不发生改变了,就不会触发`render` - 类组件重新渲染 ```js class App extends React.Component { state = { name: "App" }; render() { return (
    ); } } function Foo() { console.log("Foo render"); return (

    Foo

    ); } ``` 只要点击了 `App` 组件内的 `Change name` 按钮,不管 `Foo` 具体实现是什么,都会被重新`render`渲染 - 函数组件重新渲染 ```jsx function App(){ const [name,setName] = useState('App') return (
    ) } function Foo() { console.log("Foo render"); return (

    Foo

    ); } ``` 可以发现,使用`useState`来更新状态的时候,只有首次会触发`Foo render`,后面并不会导致`Foo render` ## 三、总结 `render`函数里面可以编写`JSX`,转化成`createElement`这种形式,用于生成虚拟`DOM`,最终转化成真实`DOM` 在` React` 中,类组件只要执行了 `setState` 方法,就一定会触发 `render` 函数执行,函数组件使用`useState`更改状态不一定导致重新`render` 组件的` props` 改变了,不一定触发 `render` 函数的执行,但是如果 `props` 的值来自于父组件或者祖先组件的 `state` 在这种情况下,父组件或者祖先组件的 `state` 发生了改变,就会导致子组件的重新渲染 所以,一旦执行了`setState`就会执行`render`方法,`useState` 会判断当前值有无发生改变确定是否执行`render`方法,一旦父组件发生渲染,子组件也会渲染 ![](https://static.vue-js.com/229784b0-ecf5-11eb-ab90-d9ae814b240d.png) ## 参考文献 - https://zhuanlan.zhihu.com/p/45091185 - https://juejin.cn/post/6844904181493415950 ================================================ FILE: docs/React/server side rendering.md ================================================ # 面试官:说说React服务端渲染怎么做?原理是什么? ![](https://static.vue-js.com/8c93cbe0-f3f7-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 在[SSR中](https://mp.weixin.qq.com/s/vvUtC_aAprUjoJRnfFjA1A),我们了解到`Server-Side Rendering` ,简称`SSR`,意为服务端渲染 指由服务侧完成页面的 `HTML` 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程 ![](https://static.vue-js.com/96dc3e20-f3f7-11eb-85f6-6fac77c0c9b3.png) 其解决的问题主要有两个: - SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面 - 加速首屏加载,解决首屏白屏问题 ## 二、如何做 在`react`中,实现`SSR`主要有两种形式: - 手动搭建一个 SSR 框架 - 使用成熟的SSR 框架,如 Next.JS 这里主要以手动搭建一个`SSR`框架进行实现 首先通过`express`启动一个`app.js`文件,用于监听3000端口的请求,当请求根目录时,返回`HTML`,如下: ```js const express = require('express') const app = express() app.get('/', (req,res) => res.send(` ssr demo Hello world `)) app.listen(3000, () => console.log('Exampleapp listening on port 3000!')) ``` 然后再服务器中编写`react`代码,在`app.js`中进行应引用 ```jsx import React from 'react' const Home = () =>{ return
    home
    } export default Home ``` 为了让服务器能够识别`JSX`,这里需要使用`webpakc`对项目进行打包转换,创建一个配置文件`webpack.server.js`并进行相关配置,如下: ```js const path = require('path') //node的path模块 const nodeExternals = require('webpack-node-externals') module.exports = { target:'node', mode:'development', //开发模式 entry:'./app.js', //入口 output: { //打包出口 filename:'bundle.js', //打包后的文件名 path:path.resolve(__dirname,'build') //存放到根目录的build文件夹 }, externals: [nodeExternals()], //保持node中require的引用方式 module: { rules: [{ //打包规则 test: /\.js?$/, //对所有js文件进行打包 loader:'babel-loader', //使用babel-loader进行打包 exclude: /node_modules/,//不打包node_modules中的js文件 options: { presets: ['react','stage-0',['env', { //loader时额外的打包规则,对react,JSX,ES6进行转换 targets: { browsers: ['last 2versions'] //对主流浏览器最近两个版本进行兼容 } }]] } }] } } ``` 接着借助`react-dom`提供了服务端渲染的 `renderToString`方法,负责把`React`组件解析成`html` ```js import express from 'express' import React from 'react'//引入React以支持JSX的语法 import { renderToString } from 'react-dom/server'//引入renderToString方法 import Home from'./src/containers/Home' const app= express() const content = renderToString() app.get('/',(req,res) => res.send(` ssr demo ${content} `)) app.listen(3001, () => console.log('Exampleapp listening on port 3001!')) ``` 上面的过程中,已经能够成功将组件渲染到了页面上 但是像一些事件处理的方法,是无法在服务端完成,因此需要将组件代码在浏览器中再执行一遍,这种服务器端和客户端共用一套代码的方式就称之为**同构** 重构通俗讲就是一套React代码在服务器上运行一遍,到达浏览器又运行一遍: - 服务端渲染完成页面结构 - 浏览器端渲染完成事件绑定 浏览器实现事件绑定的方式为让浏览器去拉取`JS`文件执行,让`JS`代码来控制,因此需要引入`script`标签 通过`script`标签为页面引入客户端执行的`react`代码,并通过`express`的`static`中间件为`js`文件配置路由,修改如下: ```js import express from 'express' import React from 'react'//引入React以支持JSX的语法 import { renderToString } from'react-dom/server'//引入renderToString方法 import Home from './src/containers/Home' const app = express() app.use(express.static('public')); //使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹 const content = renderToString() app.get('/',(req,res)=>res.send(` ssr demo ${content} `)) app.listen(3001, () =>console.log('Example app listening on port 3001!')) ``` 然后再客户端执行以下`react`代码,新建`webpack.client.js`作为客户端React代码的`webpack`配置文件如下: ```js const path = require('path') //node的path模块 module.exports = { mode:'development', //开发模式 entry:'./src/client/index.js', //入口 output: { //打包出口 filename:'index.js', //打包后的文件名 path:path.resolve(__dirname,'public') //存放到根目录的build文件夹 }, module: { rules: [{ //打包规则 test: /\.js?$/, //对所有js文件进行打包 loader:'babel-loader', //使用babel-loader进行打包 exclude: /node_modules/, //不打包node_modules中的js文件 options: { presets: ['react','stage-0',['env', { //loader时额外的打包规则,这里对react,JSX进行转换 targets: { browsers: ['last 2versions'] //对主流浏览器最近两个版本进行兼容 } }]] } }] } } ``` 这种方法就能够简单实现首页的`react`服务端渲染,过程对应如下图: ![](https://static.vue-js.com/a2894970-f3f7-11eb-85f6-6fac77c0c9b3.png) 在做完初始渲染的时候,一个应用会存在路由的情况,配置信息如下: ```js import React from 'react' //引入React以支持JSX import { Route } from 'react-router-dom' //引入路由 import Home from './containers/Home' //引入Home组件 export default (
    ) ``` 然后可以通过`index.js`引用路由信息,如下: ```js import React from 'react' import ReactDom from 'react-dom' import { BrowserRouter } from'react-router-dom' import Router from'../Routers' const App= () => { return ( {Router} ) } ReactDom.hydrate(, document.getElementById('root')) ``` 这时候控制台会存在报错信息,原因在于每个`Route`组件外面包裹着一层`div`,但服务端返回的代码中并没有这个`div` 解决方法只需要将路由信息在服务端执行一遍,使用使用`StaticRouter`来替代`BrowserRouter`,通过`context`进行参数传递 ```js import express from 'express' import React from 'react'//引入React以支持JSX的语法 import { renderToString } from 'react-dom/server'//引入renderToString方法 import { StaticRouter } from 'react-router-dom' import Router from '../Routers' const app = express() app.use(express.static('public')); //使用express提供的static中间件,中间件会将所有静态文件的路由指向public文件夹 app.get('/',(req,res)=>{ const content = renderToString(( //传入当前path //context为必填参数,用于服务端渲染参数传递 {Router} )) res.send(` ssr demo
    ${content}
    `) }) app.listen(3001, () => console.log('Exampleapp listening on port 3001!')) ``` 这样也就完成了路由的服务端渲染 ## 三、原理 整体`react`服务端渲染原理并不复杂,具体如下: `node server` 接收客户端请求,得到当前的请求`url` 路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 `props`、`context`或者`store` 形式传入组件 然后基于 `react` 内置的服务端渲染方法 `renderToString()`把组件渲染为 `html`字符串在把最终的 `html `进行输出前需要将数据注入到浏览器端 浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 `html` 节点,整个流程结束 ## 参考文献 - https://zhuanlan.zhihu.com/p/52693113 - https://segmentfault.com/a/1190000020417285 - https://juejin.cn/post/6844904000387563533#heading-14 ================================================ FILE: docs/React/setState.md ================================================ # 面试官:说说 React中的setState执行机制 ![](https://static.vue-js.com/3acb8ca0-d825-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 一个组件的显示形态可以由数据状态和外部参数所决定,而数据状态就是`state` 当需要修改里面的值的状态需要通过调用`setState`来改变,从而达到更新组件内部数据的作用 如下例子: ```jsx import React, { Component } from 'react' export default class App extends Component { constructor(props) { super(props); this.state = { message: "Hello World" } } render() { return (

    {this.state.message}

    ) } changeText() { this.setState({ message: "JS每日一题" }) } } ``` 通过点击按钮触发`onclick`事件,执行`this.setState`方法更新`state`状态,然后重新执行`render`函数,从而导致页面的视图更新 如果直接修改`state`的状态,如下: ```jsx changeText() { this.state.message = "你好啊,李银河"; } ``` 我们会发现页面并不会有任何反应,但是`state`的状态是已经发生了改变 这是因为`React`并不像`vue2`中调用`Object.defineProperty`数据响应式或者`Vue3`调用`Proxy`监听数据的变化 必须通过`setState`方法来告知`react`组件`state`已经发生了改变 关于`state`方法的定义是从`React.Component`中继承,定义的源码如下: ```js Component.prototype.setState = function(partialState, callback) { invariant( typeof partialState === 'object' || typeof partialState === 'function' || partialState == null, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.', ); this.updater.enqueueSetState(this, partialState, callback, 'setState'); }; ``` 从上面可以看到`setState`第一个参数可以是一个对象,或者是一个函数,而第二个参数是一个回调函数,用于可以实时的获取到更新之后的数据 ## 二、更新类型 在使用`setState`更新数据的时候,`setState`的更新类型分成: - 异步更新 - 同步更新 ### 异步更新 先举出一个例子: ```jsx changeText() { this.setState({ message: "你好啊" }) console.log(this.state.message); // Hello World } ``` 从上面可以看到,最终打印结果为`Hello world`,并不能在执行完`setState`之后立马拿到最新的`state`的结果 如果想要立刻获取更新后的值,在第二个参数的回调中更新后会执行 ```jsx changeText() { this.setState({ message: "你好啊" }, () => { console.log(this.state.message); // 你好啊 }); } ``` ### 同步更新 同样先给出一个在`setTimeout`中更新的例子: ```jsx changeText() { setTimeout(() => { this.setState({ message: "你好啊 }); console.log(this.state.message); // 你好啊 }, 0); } ``` 上面的例子中,可以看到更新是同步 再来举一个原生`DOM`事件的例子: ```jsx componentDidMount() { const btnEl = document.getElementById("btn"); btnEl.addEventListener('click', () => { this.setState({ message: "你好啊,李银河" }); console.log(this.state.message); // 你好啊,李银河 }) } ``` ### 小结 - 在组件生命周期或React合成事件中,setState是异步 - 在setTimeout或者原生dom事件中,setState是同步 ### 三、批量更新 同样先给出一个例子: ```jsx handleClick = () => { this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 this.setState({ count: this.state.count + 1, }) console.log(this.state.count) // 1 } ``` 点击按钮触发事件,打印的都是 1,页面显示 `count` 的值为 2 对同一个值进行多次 `setState `, `setState` 的批量更新策略会对其进行覆盖,取最后一次的执行结果 上述的例子,实际等价于如下: ```js Object.assign( previousState, {index: state.count+ 1}, {index: state.count+ 1}, ... ) ``` 由于后面的数据会覆盖前面的更改,所以最终只加了一次 如果是下一个`state`依赖前一个`state`的话,推荐给`setState`一个参数传入一个`function`,如下: ```jsx onClick = () => { this.setState((prevState, props) => { return {count: prevState.count + 1}; }); this.setState((prevState, props) => { return {count: prevState.count + 1}; }); } ``` 而在`setTimeout`或者原生`dom`事件中,由于是同步的操作,所以并不会进行覆盖现象 ## 参考文献 - https://juejin.cn/post/6844903667426918408 - https://juejin.cn/post/6844903636749778958 - https://segmentfault.com/a/1190000039077904 ================================================ FILE: docs/React/state_props.md ================================================ # 面试官:state 和 props 有什么区别? ![](https://static.vue-js.com/7f272780-d440-11eb-ab90-d9ae814b240d.png) ## 一、state 一个组件的显示形态可以由数据状态和外部参数所决定,而数据状态就是 `state`,一般在 `constructor` 中初始化 当需要修改里面的值的状态需要通过调用 `setState` 来改变,从而达到更新组件内部数据的作用,并且重新调用组件 `render` 方法,如下面的例子: ```jsx class Button extends React.Component { constructor() { super(); this.state = { count: 0, }; } updateCount() { this.setState((prevState, props) => { return { count: prevState.count + 1 }; }); } render() { return ( ); } } ``` `setState` 还可以接受第二个参数,它是一个函数,会在 `setState` 调用完成并且组件开始重新渲染时被调用,可以用来监听渲染是否完成 ```js this.setState( { name: "JS每日一题", }, () => console.log("setState finished") ); ``` ## 二、props `React` 的核心思想就是组件化思想,页面会被切分成一些独立的、可复用的组件 组件从概念上看就是一个函数,可以接受一个参数作为输入值,这个参数就是 `props`,所以可以把 `props` 理解为从外部传入组件内部的数据 `react` 具有单向数据流的特性,所以他的主要作用是从父组件向子组件中传递数据 `props` 除了可以传字符串,数字,还可以传递对象,数组甚至是回调函数,如下: ```jsx class Welcome extends React.Component { render() { return

    Hello {this.props.name}

    ; } } const element = ; ``` 上述 `name` 属性与 `onNameChanged` 方法都能在子组件的 `props` 变量中访问 在子组件中,`props` 在内部不可变的,如果想要改变它看,只能通过外部组件传入新的 `props` 来重新渲染子组件,否则子组件的 `props` 和展示形式不会改变 ## 三、区别 相同点: - 两者都是 JavaScript 对象 - 两者都是用于保存信息 - props 和 state 都能触发渲染更新 区别: - props 是外部传递给组件的,而 state 是在组件内被组件自己管理的,一般在 constructor 中初始化 - props 在组件内部是不可修改的,但 state 在组件内部可以进行修改 - state 是多变的、可以修改 ## 参考文献 - [https://lucybain.com/blog/2016/react-state-vs-pros/](https://lucybain.com/blog/2016/react-state-vs-pros/) - [https://juejin.cn/post/6844904009203974158](https://juejin.cn/post/6844904009203974158) ================================================ FILE: docs/React/summary.md ================================================ # 面试官:说说你在使用React 过程中遇到的常见问题?如何解决? ![](https://static.vue-js.com/7efcd400-f47d-11eb-ab90-d9ae814b240d.png) ## 一、前言 在使用`react`开发项目过程中,每个人或多或少都会遇到一些"奇怪"的问题,本质上都是我们对其理解的不够透彻 `react` 系列,33个工作日,33次凌晨还在亮起的台灯,到今天就圆满画上句号了,比心 在系列中我们列出了很多比较经典的考题,工作中遇到的问题也往往就藏中其中,只是以不同的表现形式存在罢了 今天的题解不算题解,准确来说是对整个系列的一次贯穿,总结 目录: - react 有什么特性 - 生命周期有哪些不同阶段?每个阶段对应的方法是? - state 和 props有什么区别? - super()和super(props)有什么区别? - setState执行机制? - React的事件机制? - 事件绑定的方式有哪些? - 构建组件的方式有哪些?区别? - 组件之间如何通信? - key有什么作用? - refs 的理解?应用场景? - Hooks的理解?解决了什么问题? - 如何引入css? - redux工作原理? - redux中间件有哪些? - react-router组件有哪些? - render触发时机? - 如何减少render? - JSX转化DOM过程? - 性能优化手段有哪些 - 如何做服务端渲染? ### react 有什么特性 主要的特性分为: - JSX语法 - 单向数据绑定 - 虚拟DOM - 声明式编程 - Component 借助这些特性,`react`整体使用起来更加简单高效,组件式开发提高了代码的复用率 ### 生命周期有哪些不同阶段?每个阶段对应的方法是? 主要分成了新的生命周期和旧的生命周期: - 新版生命周期整体流程如下图所示: ![](https://static.vue-js.com/66c999c0-d373-11eb-85f6-6fac77c0c9b3.png) 旧的生命周期流程图如下: ![](https://static.vue-js.com/d379e420-d374-11eb-ab90-d9ae814b240d.png) ### state 和 props有什么区别? 两者相同点: - 两者都是 JavaScript 对象 - 两者都是用于保存信息 - props 和 state 都能触发渲染更新 区别: - props 是外部传递给组件的,而 state 是在组件内被组件自己管理的,一般在 constructor 中初始化 - props 在组件内部是不可修改的,但 state 在组件内部可以进行修改 - state 是多变的、可以修改 ### super()和super(props)有什么区别? 在`React`中,类组件基于`ES6`,所以在`constructor`中必须使用`super` 在调用`super`过程,无论是否传入`props`,`React`内部都会将`porps`赋值给组件实例`porps`属性中 如果只调用了`super()`,那么`this.props`在`super()`和构造函数结束之间仍是`undefined` ### setState执行机制? 在`react`类组件的状态需要通过`setState`进行更改,在不同场景下对应不同的执行顺序: - 在组件生命周期或React合成事件中,setState是异步 - 在setTimeout或者原生dom事件中,setState是同步 当我们批量更改`state`的值的时候,`react`内部会将其进行覆盖,只取最后一次的执行结果 当需要下一个`state`依赖当前`state`的时候,则可以在`setState`中传递一个回调函数进行下次更新 ### React的事件机制? `React`基于浏览器的事件机制自身实现了一套事件机制,包括事件注册、事件的合成、事件冒泡、事件派发等 组件注册的事件最终会绑定在`document`这个 `DOM `上,而不是 `React `组件对应的 `DOM`,从而节省内存开销 自身实现了一套事件冒泡机制,阻止不同时间段的冒泡行为,需要对应使用不同的方法 ### 事件绑定的方式有哪些? `react`常见的绑定方式有如下: - render方法中使用bind - render方法中使用箭头函数 - constructor中bind - 定义阶段使用箭头函数绑定 前两种方式在每次组件`render`的时候都会生成新的方法实例,性能问题欠缺 ### 构建组件的方式有哪些?区别? 组件的创建主要分成了三种方式: - 函数式创建 - 继承 React.Component 创建 - 通过 React.createClass 方法创建 如今一般都是前两种方式,对于一些无状态的组件创建,建议使用函数式创建的方式,再比如`hooks`的机制下,函数式组件能做类组件对应的事情,所以建议都使用函数式的方式来创建组件 ### 组件之间如何通信? 组件间通信可以通过`props`、传递回调函数、`context`、`redux`等形式进行组件之间通讯 ### key有什么作用? 使用`key`是`react`性能优化的手段,在一系列数据最前面插入元素,如果没有`key`的值,则所有的元素都需要进行更换,而有`key`的情况只需要将最新元素插入到前面,不涉及删除操作 在使用`key`的时候应保证: - key 应该是唯一的 - key不要使用随机值(随机数在下一次 render 时,会重新生成一个数字) - 避免使用 index 作为 key ### refs 的理解?应用场景? `Refs`允许我们访问 `DOM `节点或在 `render `方法中创建的 `React `元素 下面的场景使用`refs`非常有用: - 对Dom元素的焦点控制、内容选择、控制 - 对Dom元素的内容设置及媒体播放 - 对Dom元素的操作和对组件实例的操作 - 集成第三方 DOM 库 ### Hooks的理解?解决了什么问题? `Hook` 是 React 16.8 的新增特性。它可以让你在不编写 `class` 的情况下使用 `state` 以及其他的 `React` 特性 解决问题如下: - 难以重用和共享组件中的与状态相关的逻辑 - 逻辑复杂的组件难以开发与维护,当我们的组件需要处理多个互不相关的 local state 时,每个生命周期函数中可能会包含着各种互不相关的逻辑在里面 - 类组件中的this增加学习成本,类组件在基于现有工具的优化上存在些许问题 - 由于业务变动,函数组件不得不改为类组件等等 ### 如何引入css? 常见的`CSS`引入方式有以下: - 在组件内直接使用 - 组件中引入 .css 文件 - 组件中引入 .module.css 文件 - CSS in JS 组件内直接使用`css`会导致大量的代码,而文件中直接引入`css`文件是全局作用域,发生层叠 引入`.module.css `文件能够解决局部作用域问题,但是不方便动态修改样式,需要使用内联的方式进行样式的编写 `css in js `这种方法,可以满足大部分场景的应用,可以类似于预处理器一样样式嵌套、定义、修改状态等 ### redux工作原理? `redux `要求我们把数据都放在 `store `公共存储空间 一个组件改变了 `store` 里的数据内容,其他组件就能感知到 `store `的变化,再来取数据,从而间接的实现了这些数据传递的功能 工作流程图如下所示: ![](https://static.vue-js.com/27b2e930-e56b-11eb-85f6-6fac77c0c9b3.png) ### redux中间件有哪些? 市面上有很多优秀的`redux`中间件,如: - redux-thunk:用于异步操作 - redux-logger:用于日志记录 ### react-router组件有哪些? 常见的组件有: - BrowserRouter、HashRouter - Route - Link、NavLink - switch - redirect ### render触发时机? 在` React` 中,类组件只要执行了 `setState` 方法,就一定会触发 `render` 函数执行 函数组件`useState` 会判断当前值有无发生改变确定是否执行`render`方法,一旦父组件发生渲染,子组件也会渲染 ### 如何减少render? 父组件渲染导致子组件渲染,子组件并没有发生任何改变,这时候就可以从避免无谓的渲染,具体实现的方式有如下: - shouldComponentUpdate - PureComponent - React.memo ### JSX转化DOM过程? `jsx`首先会转化成`React.createElement`这种形式,`React.createElement`作用是生成一个虚拟`Dom`对象,然后会通过`ReactDOM.render`进行渲染成真实`DOM` ### 性能优化手段有哪些 除了减少`render`的渲染之外,还可以通过以下手段进行优化: 除此之外, 常见性能优化常见的手段有如下: - 避免使用内联函数 - 使用 React Fragments 避免额外标记 - 使用 Immutable - 懒加载组件 - 事件绑定方式 - 服务端渲染 ### 如何做服务端渲染? `node server` 接收客户端请求,得到当前的请求`url` 路径,然后在已有的路由表内查找到对应的组件,拿到需要请求的数据,将数据作为 `props`、`context`或者`store` 形式传入组件 然后基于 `react` 内置的服务端渲染方法 `renderToString()`把组件渲染为 `html`字符串在把最终的 `html `进行输出前需要将数据注入到浏览器端 浏览器开始进行渲染和节点对比,然后执行完成组件内事件绑定和一些交互,浏览器重用了服务端输出的 `html` 节点,整个流程结束 ![](https://static.vue-js.com/a2894970-f3f7-11eb-85f6-6fac77c0c9b3.png) ================================================ FILE: docs/React/super()_super(props).md ================================================ # 面试官:super() 和 super(props) 有什么区别? ![](https://static.vue-js.com/618abaf0-d71c-11eb-85f6-6fac77c0c9b3.png) ## 一、ES6 类 在 `ES6` 中,通过 `extends` 关键字实现类的继承,方式如下: ```js class sup { constructor(name) { this.name = name; } printName() { console.log(this.name); } } class sub extends sup { constructor(name, age) { super(name); // super代表的事父类的构造函数 this.age = age; } printAge() { console.log(this.age); } } let jack = new sub("jack", 20); jack.printName(); //输出 : jack jack.printAge(); //输出 : 20 ``` 在上面的例子中,可以看到通过 `super` 关键字实现调用父类,`super` 代替的是父类的构建函数,使用 `super(name)` 相当于调用 `sup.prototype.constructor.call(this,name)` 如果在子类中不使用 `super`,关键字,则会引发报错,如下: ![](https://static.vue-js.com/6ab40190-d71c-11eb-85f6-6fac77c0c9b3.png) 报错的原因是 子类是没有自己的 `this` 对象的,它只能继承父类的 `this` 对象,然后对其进行加工 而 `super()` 就是将父类中的 `this` 对象继承给子类的,没有 `super()` 子类就得不到 `this` 对象 如果先调用 `this`,再初始化 `super()`,同样是禁止的行为 ```js class sub extends sup { constructor(name, age) { this.age = age; super(name); // super代表的事父类的构造函数 } } ``` 所以在子类 `constructor` 中,必须先代用 `super` 才能引用 `this` ## 二、类组件 在 `React` 中,类组件是基于 `ES6` 的规范实现的,继承 `React.Component`,因此如果用到 `constructor` 就必须写 `super()` 才初始化 `this` 这时候,在调用 `super()` 的时候,我们一般都需要传入 `props` 作为参数,如果不传进去,`React` 内部也会将其定义在组件实例中 ```js // React 内部 const instance = new YourComponent(props); instance.props = props; ``` 所以无论有没有 `constructor`,在 `render` 中 `this.props` 都是可以使用的,这是 `React` 自动附带的,是可以不写的: ```jsx class HelloMessage extends React.Component { render() { return
    nice to meet you! {this.props.name}
    ; } } ``` 但是也不建议使用 `super()` 代替 `super(props)` 因为在 `React` 会在类组件构造函数生成实例后再给 `this.props` 赋值,所以在不传递 `props` 在 `super` 的情况下,调用 `this.props` 为 `undefined`,如下情况: ```jsx class Button extends React.Component { constructor(props) { super(); // 没传入 props console.log(props); // {} console.log(this.props); // undefined // ... } } ``` 而传入 `props` 的则都能正常访问,确保了 `this.props` 在构造函数执行完毕之前已被赋值,更符合逻辑,如下: ```jsx class Button extends React.Component { constructor(props) { super(props); // 没传入 props console.log(props); // {} console.log(this.props); // {} // ... } } ``` ## 三、总结 在 `React` 中,类组件基于 `ES6`,所以在 `constructor` 中必须使用 `super` 在调用 `super` 过程,无论是否传入 `props`,`React` 内部都会将 `porps` 赋值给组件实例 `porps` 属性中 如果只调用了 `super()`,那么 `this.props` 在 `super()` 和构造函数结束之间仍是 `undefined` ## 参考文献 - [https://overreacted.io/zh-hans/why-do-we-write-super-props/](https://overreacted.io/zh-hans/why-do-we-write-super-props/) - [https://segmentfault.com/q/1010000008340434](https://segmentfault.com/q/1010000008340434) ================================================ FILE: docs/algorithm/Algorithm.md ================================================ # 面试官:说说你对算法的理解?应用场景? ![](https://static.vue-js.com/eca03690-1620-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 算法(Algorithm)是指解题方案的准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制 也就是说,能够对一定规范的输入,在有限时间内获得所要求的输出 如果一个算法有缺陷,或不适合于某个问题,执行这个算法将不会解决这个问题 一个程序=算法+数据结构,数据结构是算法实现的基础,算法总是要依赖于某种数据结构来实现的,两者不可分割 因此,算法的设计和选择要同时结合数据结构,简单地说数据结构的设计就是选择存储方式,如确定问题中的信息是用数组存储还是用普通的变量存储或其他更加复杂的数据结构 针对上述,可以得出一个总结:不同的算法可能用不同的时间、空间或效率来完成同样的任务 ## 二、特性 关于算法的五大特性,有如下: - 有限性(Finiteness):一个算法必须保证执行有限步之后结束 - 确切性(Definiteness): 一个算法的每一步骤必须有确切的定义 - 输入(Input):一个算法有零个或多个输入,以刻画运算对象的初始情况,所谓零个输入是指算法本身给定了初始条件 - 输出(Output):一个算法有一个或多个输出。没有输出的算法毫无意义 - 可行性(Effectiveness):算法中执行的任何计算步骤都是可以被分解为基本的可执行的操作步骤,即每个计算步骤都可以在有限时间内完成(也称之为有效性) ## 三、应用场景 在前端领域中,数据结构与算法无法不在,例如现在的`vue`或者`react`项目,实现虚拟`DOM`或者`Fiber`结构,本质就是一种数据结构,如下一个简单的虚拟`DOM`: ```js { type: 'div', props: { name: 'lucifer' }, children: [{ type: 'span', props: {}, children: [] }] } ``` `vue`与`react`都能基于基于对应的数据结构实现`diff`算法,提高了整个框架的性能以及拓展性 包括在前端`javascript`编译的时候,都会生成对应的抽象语法树`AST`,其本身不涉及到任何语法,因此你只要编写相应的转义规则,就可以将任何语法转义到任何语法,也是`babel`, `PostCSS`, `prettier`, `typescript` 除了这些框架或者工具底层用到算法与数据结构之外,日常业务也无处不在,例如实现一个输入框携带联想功能,如下: ![](https://static.vue-js.com/682d16c0-1621-11ec-8e64-91fdec0f05a1.png) 如果我们要实现这个功能, 则可以使用前缀树,如下: ![](https://static.vue-js.com/55a1ed50-1621-11ec-8e64-91fdec0f05a1.png) 包括前端可能会做一些对字符串进行相似度检测,例如"每日一题"和"js每日一题"两个字符串进行相似度对比,这种情况可以通过“最小编辑距离”算法,如果`a`和`b`的编辑距离越小,我们认为越相似 日常在编写任何代码的都需要一个良好的算法思维,选择好的算法或者数据结构,能让整个程序效率更高 ## 参考文献 - https://baike.baidu.com/item/%E7%AE%97%E6%B3%95/209025 - https://lucifer.ren/blog/2019/09/18/algorthimn-fe-1/ ================================================ FILE: docs/algorithm/BinarySearch.md ================================================ # 面试官:说说你对二分查找的理解?如何实现?应用场景? ![](https://static.vue-js.com/d43ca230-2987-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 在计算机科学中,二分查找算法,也称折半搜索算法,是一种在有序数组中查找某一特定元素的搜索算法 想要应用二分查找法,则这一堆数应有如下特性: - 存储在数组中 - 有序排序 搜索过程从数组的中间元素开始,如果中间元素正好是要查找的元素,则搜索过程结束 如果某一特定元素大于或者小于中间元素,则在数组大于或小于中间元素的那一半中查找,而且跟开始一样从中间元素开始比较 如果在某一步骤数组为空,则代表找不到 这种搜索算法每一次比较都使搜索范围缩小一半 如下图所示: ![](https://static.vue-js.com/e2108520-2987-11ec-a752-75723a64e8f5.png) 相比普通的顺序查找,除了数据量很少的情况下,二分查找会比顺序查找更快,区别如下所示: ![](https://pic2.zhimg.com/v2-43339b963db63b33107b56503ad6b1b5_b.gif) ## 二、如何实现 基于二分查找的实现,如果数据是有序的,并且不存在重复项,实现代码如下: ```js function BinarySearch(arr, target) { if (arr.length <= 1) return -1 // 低位下标 let lowIndex = 0 // 高位下标 let highIndex = arr.length - 1 while (lowIndex <= highIndex) { // 中间下标 const midIndex = Math.floor((lowIndex + highIndex) / 2) if (target < arr[midIndex]) { highIndex = midIndex - 1 } else if (target > arr[midIndex]) { lowIndex = midIndex + 1 } else { // target === arr[midIndex] return midIndex } } return -1 } ``` 如果数组中存在重复项,而我们需要找出第一个制定的值,实现则如下: ```js function BinarySearchFirst(arr, target) { if (arr.length <= 1) return -1 // 低位下标 let lowIndex = 0 // 高位下标 let highIndex = arr.length - 1 while (lowIndex <= highIndex) { // 中间下标 const midIndex = Math.floor((lowIndex + highIndex) / 2) if (target < arr[midIndex]) { highIndex = midIndex - 1 } else if (target > arr[midIndex]) { lowIndex = midIndex + 1 } else { // 当 target 与 arr[midIndex] 相等的时候,如果 midIndex 为0或者前一个数比 target 小那么就找到了第一个等于给定值的元素,直接返回 if (midIndex === 0 || arr[midIndex - 1] < target) return midIndex // 否则高位下标为中间下标减1,继续查找 highIndex = midIndex - 1 } } return -1 } ``` 实际上,除了有序的数组可以使用,还有一种特殊的数组可以应用,那就是轮转后的有序数组 有序数组即一个有序数字以某一个数为轴,将其之前的所有数都轮转到数组的末尾所得 例如,[4, 5, 6, 7, 0, 1, 2]就是一个轮转后的有序数组 该数组的特性是存在一个分界点用来分界两个有序数组,如下: ![](https://static.vue-js.com/eeee2130-2987-11ec-8e64-91fdec0f05a1.png) 分界点有如下特性: - 分界点元素 >= 第一个元素 - 分界点元素 < 第一个元素 代码实现如下: ```js function search (nums, target) { // 如果为空或者是空数组的情况 if (nums == null || !nums.length) { return -1; } // 搜索区间是前闭后闭 let begin = 0, end = nums.length - 1; while (begin <= end) { // 下面这样写是考虑大数情况下避免溢出 let mid = begin + ((end - begin) >> 1); if (nums[mid] == target) { return mid; } // 如果左边是有序的 if (nums[begin] <= nums[mid]) { //同时target在[ nums[begin],nums[mid] ]中,那么就在这段有序区间查找 if (nums[begin] <= target && target <= nums[mid]) { end = mid - 1; } else { //否则去反方向查找 begin = mid + 1; } //如果右侧是有序的 } else { //同时target在[ nums[mid],nums[end] ]中,那么就在这段有序区间查找 if (nums[mid] <= target && target <= nums[end]) { begin = mid + 1; } else { end = mid - 1; } } } return -1; }; ``` 对比普通的二分查找法,为了确定目标数会落在二分后的哪个部分,我们需要更多的判定条件 ## 三、应用场景 二分查找法的`O(logn)`让它成为十分高效的算法。不过它的缺陷却也是比较明显,就在它的限定之上: - 有序:我们很难保证我们的数组都是有序的 - 数组:数组读取效率是O(1),可是它的插入和删除某个元素的效率却是O(n),并且数组的存储是需要连续的内存空间,不适合大数据的情况 关于二分查找的应用场景,主要如下: - 不适合数据量太小的数列;数列太小,直接顺序遍历说不定更快,也更简单 - 每次元素与元素的比较是比较耗时的,这个比较操作耗时占整个遍历算法时间的大部分,那么使用二分查找就能有效减少元素比较的次数 - 不适合数据量太大的数列,二分查找作用的数据结构是顺序表,也就是数组,数组是需要连续的内存空间的,系统并不一定有这么大的连续内存空间可以使用 ## 参考文献 - https://zh.wikipedia.org/wiki/%E4%BA%8C%E5%88%86%E6%90%9C%E5%B0%8B%E6%BC%94%E7%AE%97%E6%B3%95#javascript_%E7%89%88%E6%9C%AC - https://www.cnblogs.com/ider/archive/2012/04/01/binary_search.html ================================================ FILE: docs/algorithm/Heap.md ================================================ # 面试官:说说你对堆的理解?如何实现?应用场景? ![](https://static.vue-js.com/dd12c700-1ed7-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 堆(Heap)是计算机科学中一类特殊的数据结构的统称 堆通常是一个可以被看做一棵完全二叉树的数组对象,如下图: ![](https://static.vue-js.com/ea0fd1f0-1ed7-11ec-8e64-91fdec0f05a1.png) 总是满足下列性质: - 堆中某个结点的值总是不大于或不小于其父结点的值 - 堆总是一棵完全二叉树 堆又可以分成最大堆和最小堆: - 最大堆:每个根结点,都有根结点的值大于两个孩子结点的值 - 最小堆:每个根结点,都有根结点的值小于孩子结点的值 ## 二、操作 堆的元素存储方式,按照完全二叉树的顺序存储方式存储在一个一维数组中,如下图: ![](https://static.vue-js.com/ea0fd1f0-1ed7-11ec-8e64-91fdec0f05a1.png) 用一维数组存储则如下: ```js [0, 1, 2, 3, 4, 5, 6, 7, 8] ``` 根据完全二叉树的特性,可以得到如下特性: - 数组零坐标代码的是堆顶元素 - 一个节点的父亲节点的坐标等于其坐标除以2整数部分 - 一个节点的左节点等于其本身节点坐标 * 2 + 1 - 一个节点的右节点等于其本身节点坐标 * 2 + 2 根据上述堆的特性,下面构建最小堆的构造函数和对应的属性方法: ```js class MinHeap { constructor() { // 存储堆元素 this.heap = [] } // 获取父元素坐标 getParentIndex(i) { return (i - 1) >> 1 } // 获取左节点元素坐标 getLeftIndex(i) { return i * 2 + 1 } // 获取右节点元素坐标 getRightIndex(i) { return i * 2 + 2 } // 交换元素 swap(i1, i2) { const temp = this.heap[i1] this.heap[i1] = this.heap[i2] this.heap[i2] = temp } // 查看堆顶元素 peek() { return this.heap[0] } // 获取堆元素的大小 size() { return this.heap.length } } ``` 涉及到堆的操作有: - 插入 - 删除 ### 插入 将值插入堆的底部,即数组的尾部,当插入一个新的元素之后,堆的结构就会被破坏,因此需要堆中一个元素做上移操作 将这个值和它父节点进行交换,直到父节点小于等于这个插入的值,大小为`k`的堆中插入元素的时间复杂度为`O(logk)` 如下图所示,22节点是新插入的元素,然后进行上移操作: ![](https://static.vue-js.com/06893fb0-1ed8-11ec-8e64-91fdec0f05a1.png) 相关代码如下: ```js // 插入元素 insert(value) { this.heap.push(value) this.shifUp(this.heap.length - 1) } // 上移操作 shiftUp(index) { if (index === 0) { return } const parentIndex = this.getParentIndex(index) if(this.heap[parentIndex] > this.heap[index]){ this.swap(parentIndex, index) this.shiftUp(parentIndex) } } ``` ### 删除 常见操作是用数组尾部元素替换堆顶,这里不直接删除堆顶,因为所有的元素会向前移动一位,会破坏了堆的结构 然后进行下移操作,将新的堆顶和它的子节点进行交换,直到子节点大于等于这个新的堆顶,删除堆顶的时间复杂度为`O(logk)` 整体如下图操作: ![](https://static.vue-js.com/12a2a160-1ed8-11ec-a752-75723a64e8f5.png) 相关代码如下: ```js // 删除元素 pop() { this.heap[0] = this.heap.pop() this.shiftDown(0) } // 下移操作 shiftDown(index) { const leftIndex = this.getLeftIndex(index) const rightIndex = this.getRightIndex(index) if (this.heap[leftIndex] < this.heap[index]){ this.swap(leftIndex, index) this.shiftDown(leftIndex) } if (this.heap[rightIndex] < this.heap[index]){ this.swap(rightIndex, index) this.shiftDown(rightIndex) } } ``` ### 时间复杂度 关于堆的插入和删除时间复杂度都是`Olog(n)`,原因在于包含n个节点的完全二叉树,树的高度不会超过`log2n` 堆化的过程是顺着节点所在路径比较交换的,所以堆化的时间复杂度跟树的高度成正比,也就是`Olog(n)`,插入数据和删除堆顶元素的主要逻辑就是堆化 ### 三、总结 - 堆是一个完全二叉树 - 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值 - 对于每个节点的值都大于等于子树中每个节点值的堆,叫作“大顶堆” - 对于每个节点的值都小于等于子树中每个节点值的堆,叫作“小顶堆” - 根据堆的特性,我们可以使用堆来进行排序操作,也可以使用其来求第几大或者第几小的值 ## 参考文献 - https://baike.baidu.com/item/%E5%A0%86/20606834 - https://xlbpowder.cn/2021/02/26/%E5%A0%86%E5%92%8C%E5%A0%86%E6%8E%92%E5%BA%8F/ ================================================ FILE: docs/algorithm/Linked List.md ================================================ # 面试官:说说你对链表的理解?常见的操作有哪些? ![](https://static.vue-js.com/d6638dd0-1c76-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 链表(Linked List)是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的,由一系列结点(链表中每一个元素称为结点)组成 每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域 ![](https://static.vue-js.com/e4e93490-1c76-11ec-8e64-91fdec0f05a1.png) 节点用代码表示,则如下: ```js class Node { constructor(val) { this.val = val; this.next = null; } } ``` - data 表示节点存放的数据 - next 表示下一个节点指向的内存空间 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到`O(1)`的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是`O(logn)`和`O(1)` 链表的结构也十分多,常见的有四种形式: - 单链表:除了头节点和尾节点,其他节点只包含一个后继指针 - 循环链表:跟单链表唯一的区别就在于它的尾结点又指回了链表的头结点,首尾相连,形成了一个环 - 双向链表:每个结点具有两个方向指针,后继指针(next)指向后面的结点,前驱指针(prev)指向前面的结点,其中节点的前驱指针和尾结点的后继指针均指向空地址NULL - 双向循环链表:跟双向链表基本一致,不过头节点前驱指针指向尾迹诶单和尾节点的后继指针指向头节点 ## 二、操作 关于链表的操作可以主要分成如下: - 遍历 - 插入 - 删除 ### 遍历 遍历很好理解,就是根据`next`指针遍历下去,直到为`null`,如下: ```js let current = head while(current){ console.log(current.val) current = current.next } ``` ### 插入 向链表中间插入一个元素,可以如下图所示: ![](https://static.vue-js.com/f5fe5fd0-1c76-11ec-8e64-91fdec0f05a1.png) 可以看到,插入节点可以分成如下步骤: - 存储插入位置的前一个节点 - 存储插入位置的后一个节点 - 将插入位置的前一个节点的 next 指向插入节点 - 将插入节点的 next 指向前面存储的 next 节点 相关代码如下所示: ```js let current = head while (current < position){ pervious = current; current = current.next; } pervious.next = node; node.next = current; ``` 如果在头节点进行插入操作的时候,会实现`previousNode`节点为`undefined`,不适合上述方式 解放方式可以是在头节点前面添加一个虚拟头节点,保证插入行为一致 ### 删除 向链表任意位置删除节点,如下图操作: ![](https://static.vue-js.com/0160cd90-1c77-11ec-a752-75723a64e8f5.png) 从上图可以看到删除节点的步骤为如下: - 获取删除节点的前一个节点 - 获取删除节点的后一个节点 - 将前一个节点的 next 指向后一个节点 - 向删除节点的 next 指向为null 如果想要删除制定的节点,示意代码如下: ```js while (current != node){ pervious = current; current = current.next; nextNode = current.next; } pervious.next = nextNode ``` 同样如何希望删除节点处理行为一致,可以在头节点前面添加一个虚拟头节点 ## 三、应用场景 缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非常广泛的应用,比如常见的`CPU`缓存、数据库缓存、浏览器缓存等等 当缓存空间被用满时,我们可能会使用`LRU`最近最好使用策略去清楚,而实现`LRU`算法的数据结构是链表,思路如下: 维护一个有序单链表,越靠近链表尾部的结点是越早之前访问的。当有一个新的数据被访问时,我们从链表头部开始顺序遍历链表 - 如果此数据之前已经被缓存在链表中了,我们遍历得到这个数据的对应结点,并将其从原来的位置删除,并插入到链表头部 - 如果此数据没在缓存链表中 - 如果此时缓存未满,可直接在链表头部插入新节点存储此数据 - 如果此时缓存已满,则删除链表尾部节点,再在链表头部插入新节点 由于链表插入删除效率极高,达到O(1)。对于不需要搜索但变动频繁且无法预知数量上限的数据的情况的时候,都可以使用链表 ## 参考文献 - https://zh.wikipedia.org/zh-hans/%E9%93%BE%E8%A1%A8 - https://mah93.github.io/2019/07/19/js-linked/ ================================================ FILE: docs/algorithm/bubbleSort.md ================================================ # 面试官:说说你对冒泡排序的理解?如何实现?应用场景? ![](https://static.vue-js.com/6f5e0850-2652-11ec-a752-75723a64e8f5.png) ## 一、是什么 冒泡排序(Bubble Sort),是一种计算机科学领域的较简单的排序算法 冒泡排序的思想就是在每次遍历一遍未排序的数列之后,将一个数据元素浮上去(也就是排好了一个数据) 如同碳酸饮料中二氧化碳的气泡最终会上浮到顶端一样,故名“冒泡排序” 假如我们要把 12、35、99、18、76 这 5 个数从大到小进行排序,那么数越大,越需要把它放在前面 思路如下: - 从后开始遍历,首先比较 18 和 76,发现 76 比 18 大,就把两个数交换顺序,得到 12、35、99、76、18 - 接着比较 76 和 99,发现 76 比 99 小,所以不用交换顺序 - 接着比较 99 和 35,发现 99 比 35 大,交换顺序 - 接着比较 99 和 12,发现 99 比 12 大,交换顺序 最终第 1 趟排序的结果变成了 99、12、35、76、18,如下图所示: ![](https://static.vue-js.com/7a363770-2652-11ec-8e64-91fdec0f05a1.png) 上述可以看到,经过第一趟的排序,可以得到最大的元素,接下来第二趟排序则对剩下的的4个元素进行排序,如下图所示: ![](https://static.vue-js.com/84b9ddf0-2652-11ec-a752-75723a64e8f5.png) 经过第 2 趟排序,结果为 99、76、12、35、18 然后开始第3趟的排序,结果为99、76、35、12、18 然后第四趟排序结果为99、76、35、18、12 经过 4 趟排序之后,只剩一个 12 需要排序了,这时已经没有可比较的元素了,这时排序完成 ## 二、如何实现 如果要实现一个从小到大的排序,算法原理如下: - 首先比较相邻的元素,如果第一个元素比第二个元素大,则交换它们 - 针对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对,这样,最后的元素回事最大的数 - 针对所有的元素重复以上的步骤,除了最后一个 - 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较 ![](https://www.runoob.com/wp-content/uploads/2019/03/bubbleSort.gif) 用代码表示则如下: ```js function bubbleSort(arr) { const len = arr.length; for (let i = 0; i < len - 1; i++) { for (let j = 0; j < len - 1 - i; j++) { if (arr[j] > arr[j+1]) { // 相邻元素两两对比 var temp = arr[j+1]; // 元素交换 arr[j+1] = arr[j]; arr[j] = temp; } } } return arr; } ``` 可以看到:冒泡排序在每一轮排序中都会使一个元素排到一趟, 也就是最终需要 n-1 轮这样的排序 而在每轮排序中都需要对相邻的两个元素进行比较,在最坏的情况下,每次比较之后都需要交换位置,此时时间复杂度为`O(n^2)` ### 优化 对冒泡排序常见的改进方法是加入一标志性变量`exchange`,用于标志某一趟排序过程中是否有数据交换 如果进行某一趟排序时并没有进行数据交换,则说明数据已经按要求排列好,可立即结束排序,避免不必要的比较过程 可以设置一标志性变量`pos`,用于记录每趟排序中最后一次进行交换的位置,由于`pos`位置之后的记录均已交换到位,故在进行下一趟排序时只要扫描到`pos`位置即可,如下: ```js function bubbleSort1(arr){ const i=arr.length-1;//初始时,最后位置保持不变 while(i>0){ let pos = 0;//每趟开始时,无记录交换 for(let j = 0; j < i; j++){ if(arr[j] > arr[j+1]){ let tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1] = tmp; pos = j;//记录最后交换的位置 } } i = pos;//为下一趟排序作准备 } return arr; } ``` 在待排序的数列有序的情况下,只需要一轮排序并且不用交换,此时情况最好,时间复杂度为`O(n)` 并且从上述比较中看到,只有后一个元素比前面的元素大(小)时才会对它们交换位置并向上冒出,对于同样大小的元素,是不需要交换位置的,所以对于同样大小的元素来说,相对位置是不会改变的,因此, 冒泡排序是稳定的 ## 三、应用场景 冒泡排的核心部分是双重嵌套循环, 时间复杂度是 O(N 2 ),相比其它排序算法,这是一个相对较高的时间复杂度,一般情况不推荐使用,由于冒泡排序的简洁性,通常被用来对于程序设计入门的学生介绍算法的概念 ## 参考文献 - https://baike.baidu.com/item/%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F/4602306 - https://www.runoob.com/w3cnote/bubble-sort.html - http://data.biancheng.net/view/116.html - https://dsb123dsb.github.io/2017/03/07/js%E5%AE%9E%E7%8E%B0%E5%86%92%E6%B3%A1%E6%8E%92%E5%BA%8F%E4%BB%A5%E5%8F%8A%E4%BC%98%E5%8C%96/ ================================================ FILE: docs/algorithm/design1.md ================================================ # 面试官:说说你对分而治之、动态规划的理解?区别? ![](https://static.vue-js.com/298437b0-29d0-11ec-a752-75723a64e8f5.png) ## 一、分而治之 分而治之是算法设计中的一种方法,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并 关于分而治之的实现,都会经历三个步骤: - 分解:将原问题分解为若干个规模较小,相对独立,与原问题形式相同的子问题 - 解决:若子问题规模较小且易于解决时,则直接解。否则,递归地解决各子问题 - 合并:将各子问题的解合并为原问题的解 实际上,关于分而治之的思想,我们在前面已经使用,例如归并排序的实现,同样经历了实现分而治之的三个步骤: - 分解:把数组从中间一分为二 - 解决:递归地对两个子数组进行归并排序 - 合并:将两个字数组合并称有序数组 同样关于快速排序的实现,亦如此: - 分:选基准,按基准把数组分成两个字数组 - 解:递归地对两个字数组进行快速排序 - 合:对两个字数组进行合并 同样二分搜索也能使用分而治之的思想去实现,代码如下: ```js function binarySearch(arr,l,r,target){ if(l> r){ return -1; } let mid = l + Math.floor((r-l)/2) if(arr[mid] === target){ return mid; }else if(arr[mid] < target ){ return binarySearch(arr,mid + 1,r,target) }else{ return binarySearch(arr,l,mid - 1,target) } } ``` ## 二、动态规划 动态规划,同样是算法设计中的一种方法,是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法 常常适用于有重叠子问题和最优子结构性质的问题 简单来说,动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决 然后呢,把子问题答案保存起来,以减少重复计算。再根据子问题答案反推,得出原问题解的一种方法。 一般这些子问题很相似,可以通过函数关系式递推出来,例如斐波那契数列,我们可以得到公式:当 n 大于 2的时候,F(n) = F(n-1) + F(n-2) , f(10)= f(9)+f(8),f(9) = f(8) + f(7)...是重叠子问题,当n = 1、2的时候,对应的值为2,这时候就通过可以使用一个数组记录每一步计算的结果,以此类推,减少不必要的重复计算 ### 适用场景 如果一个问题,可以把所有可能的答案穷举出来,并且穷举出来后,发现存在重叠子问题,就可以考虑使用动态规划 比如一些求最值的场景,如最长递增子序列、最小编辑距离、背包问题、凑零钱问题等等,都是动态规划的经典应用场景 关于动态规划题目解决的步骤,一般如下: - 描述最优解的结构 - 递归定义最优解的值 - 按自底向上的方式计算最优解的值 - 由计算出的结果构造一个最优解 ## 三、区别 动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解 与分治法不同的是,适合于用动态规划求解的问题,经分解得到子问题往往**不是互相独立**的,而分而治之的子问题是相互独立的 若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次 如果我们能够保存已解决的子问题的答案,而在需要时再找出已求得的答案,这样就可以避免大量的重复计算,节省时间 综上,可得: - 动态规划:有最优子结构和重叠子问题 - 分而治之:各子问题独立 ## 参考文献 - https://baike.baidu.com/item/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92/529408 - https://juejin.cn/post/6951922898638471181 ================================================ FILE: docs/algorithm/design2.md ================================================ # 面试官:说说你对贪心算法、回溯算法的理解?应用场景? ![](https://static.vue-js.com/1d49eae0-2e8e-11ec-a752-75723a64e8f5.png) ## 一、贪心算法 贪心算法,又称贪婪算法,是算法设计中的一种思想 其期待每一个阶段都是局部最优的选择,从而达到全局最优,但是结果并不一定是最优的 举个零钱兑换的例子,如果你有1元、2元、5元的钱币数张,用于兑换一定的金额,但是要求兑换的钱币张数最少 如果现在你要兑换11元,按照贪心算法的思想,先选择面额最大的5元钱币进行兑换,那么就得到11 = 5 + 5 + 1 的选择,这种情况是最优的 但是如果你手上钱币的面额为1、3、4,想要兑换6元,按照贪心算法的思路,我们会 6 = 4 + 1 + 1这样选择,这种情况结果就不是最优的选择 从上面例子可以看到,贪心算法存在一些弊端,使用到贪心算法的场景,都会存在一个特性: 一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法 至于是否选择贪心算法,主要看是否有如下两大特性: - 贪心选择:当某一个问题的整体最优解可通过一系列局部的最优解的选择达到,并且每次做出的选择可以依赖以前做出的选择,但不需要依赖后面需要做出的选择 - 最优子结构:如果一个问题的最优解包含其子问题的最优解,则此问题具备最优子结构的性质。问题的最优子结构性质是该问题是否可以用贪心算法求解的关键所在 ## 二、回溯算法 回溯算法,也是算法设计中的一种思想,是一种渐进式寻找并构建问题解决方式的策略 回溯算法会先从一个可能的工作开始解决问题,如果不行,就回溯并选择另一个动作,知道将问题解决 使用回溯算法的问题,有如下特性: - 有很多路,例如一个矩阵的方向或者树的路径 - 在这些的路里面,有死路也有生路,思路即不符合题目要求的路,生路则符合 - 通常使用递归来模拟所有的路 常见的伪代码如下: ```js result = [] function backtrack(路径, 选择列表): if 满足结束条件: result.add(路径) return for 选择 of 选择列表: 做选择 backtrack(路径, 选择列表) 撤销选择 ``` 重点解决三个问题: - 路径:也就是已经做出的选择 - 选择列表 - 结束条件 例如经典使用回溯算法为解决全排列的问题,如下: 一个不含重复数字的数组 `nums` ,我们要返回其所有可能的全排列,解决这个问题的思路是: - 用递归模拟所有的情况 - 遇到包含重复元素的情况则回溯 - 收集到所有到达递归终点的情况,并返回、 ![](https://static.vue-js.com/2a030f00-2e8e-11ec-8e64-91fdec0f05a1.png) 用代码表示则如下: ```js var permute = function(nums) { const res = [], path = []; backtracking(nums, nums.length, []); return res; function backtracking(n, k, used) { if(path.length === k) { res.push(Array.from(path)); return; } for (let i = 0; i < k; i++ ) { if(used[i]) continue; path.push(n[i]); used[i] = true; // 同支 backtracking(n, k, used); path.pop(); used[i] = false; } } }; ``` ## 三、总结 前面也初步了解到分而治之、动态规划,现在再了解到贪心算法、回溯算法 其中关于分而治之、动态规划、贪心策略三者的求解思路如下: ![](https://static.vue-js.com/504b5230-2e8e-11ec-8e64-91fdec0f05a1.png) 其中三者对应的经典问题如下图: ![](https://static.vue-js.com/62cdc910-2e8e-11ec-8e64-91fdec0f05a1.png) ## 参考文献 - https://zh.wikipedia.org/wiki/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95 - https://leetcode-cn.com/problems/permutations/solution/dai-ma-sui-xiang-lu-dai-ni-xue-tou-hui-s-mfrp/ - https://cloud.tencent.com/developer/article/1767046 ================================================ FILE: docs/algorithm/graph.md ================================================ # 面试官:说说你对图的理解?相关操作有哪些? ![](https://static.vue-js.com/7876c2f0-2059-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 在计算机科学中,图是一种抽象的数据类型,在图中的数据元素通常称为结点,`V`是所有顶点的集合,`E`是所有边的集合 如果两个顶点`v`,` w`,只能由`v`向`w`,而不能由`w`向`v`,那么我们就把这种情况叫做一个从 `v` 到 `w` 的有向边。`v `也被称做初始点,`w`也被称为终点。这种图就被称做有向图 如果`v`和`w`是没有顺序的,从`v`到达`w`和从`w`到达`v`是完全相同的,这种图就被称为无向图 图的结构比较复杂,任意两个顶点之间都可能存在联系,因此无法以数据元素在存储区中的物理位置来表示元素之间的关系 常见表达图的方式有如下: - 邻接矩阵 - 邻接表 ### 邻接矩阵 通过使用一个二维数组`G[N][N]`进行表示`N`个点到`N-1`编号,通过邻接矩阵可以立刻看出两顶点之间是否存在一条边,只需要检查邻接矩阵行`i`和列`j`是否是非零值,对于无向图,邻接矩阵是对称的 ![](https://static.vue-js.com/881d4300-2059-11ec-a752-75723a64e8f5.png) ### 邻接表 存储方式如下图所示: ![](https://static.vue-js.com/949fedd0-2059-11ec-a752-75723a64e8f5.png) 在`javascript`中,可以使用`Object`进行表示,如下: ```js const graph = { A: [2, 3, 5], B: [2], C: [0, 1, 3], D: [0, 2], E: [6], F: [0, 6], G: [4, 5] } ``` 图的数据结构还可能包含和每条边相关联的数值(edge value),例如一个标号或一个数值(即权重,weight;表示花费、容量、长度等) ## 二、操作 关于的图的操作常见的有: - 深度优先遍历 - 广度优先遍历 首先构建一个图的邻接矩阵表示,如下面的图: ![](https://static.vue-js.com/a1311790-2059-11ec-8e64-91fdec0f05a1.png) 用代码表示则如下: ```js const graph = { 0: [1, 4], 1: [2, 4], 2: [2, 3], 3: [], 4: [3], } ``` ### 深度优先遍历 也就是尽可能的往深处的搜索图的分支 实现思路是,首先应该确定一个根节点,然后对根节点的没访问过的相邻节点进行深度优先遍历 确定以 0 为根节点,然后进行深度遍历,然后遍历1,接着遍历 2,然后3,此时完成一条分支`0 - 1- 2- 3`的遍历,换一条分支,也就是4,4后面因为3已经遍历过了,所以就不访问了 用代码表示则如下: ```js const visited = new Set() const dfs = (n) => { console.log(n) visited.add(n) // 访问过添加记录 graph[n].forEach(c => { if(!visited.has(c)){ // 判断是否访问呢过 dfs(c) } }) } ``` ### 广度优先遍历 先访问离根节点最近的节点,然后进行入队操作,解决思路如下: - 新建一个队列,把根节点入队 - 把队头出队并访问 - 把队头的没访问过的相邻节点入队 - 重复二、三步骤,知道队列为空 用代码标识则如下: ```js const visited = new Set() const dfs = (n) => { visited.add(n) const q = [n] while(q.length){ const n = q.shift() console.log(n) graph[n].forEach(c => { if(!visited.has(c)){ q.push(c) visited.add(c) } }) } } ``` ## 三、总结 通过上面的初步了解,可以看到图就是由顶点的有穷非空集合和顶点之间的边组成的集合,分成了无向图与有向图 图的表达形式可以分成邻接矩阵和邻接表两种形式,在`javascript`中,则可以通过二维数组和对象的形式进行表达 图实际是很复杂的,后续还可以延伸出无向图和带权图,对应如下图所示: ![](https://static.vue-js.com/b0d88200-2059-11ec-8e64-91fdec0f05a1.png) ## 参考文献 - https://zh.wikipedia.org/wiki/%E5%9B%BE_(%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84) - https://www.kancloud.cn/imnotdown1019/java_core_full/2159607 ================================================ FILE: docs/algorithm/insertionSort.md ================================================ # 面试官:说说你对插入排序的理解?如何实现?应用场景? ![](https://static.vue-js.com/912adc10-267f-11ec-a752-75723a64e8f5.png) ## 一、是什么 插入排序(Insertion Sort),一般也被称为直接插入排序。对于少量元素的排序,它是一个有效、简单的算法 其主要的实现思想是将数据按照一定的顺序一个一个的插入到有序的表中,最终得到的序列就是已经排序好的数据 插入排序的工作方式像许多人排序一手扑克牌,开始时,我们的左手为空并且桌子上的牌面向下 然后,我们每次从桌子上拿走一张牌并将它插入左手中正确的位置,该正确位置需要从右到左将它与已在手中的每张牌进行比较 例如一个无序数组 3、1、7、5、2、4、9、6,将其升序的结果则如下: 一开始有序表中无数据,直接插入3 从第二个数开始,插入一个元素1,然后和有序表中记录3比较,1<3,所以插入到记录 3 的左侧 ![](https://static.vue-js.com/9d24f5f0-267f-11ec-a752-75723a64e8f5.png) 向有序表插入记录 7 时,同有序表中记录 3 进行比较,3<7,所以插入到记录 3 的右侧 ![](https://static.vue-js.com/a6a954e0-267f-11ec-8e64-91fdec0f05a1.png) 向有序表中插入记录 5 时,同有序表中记录 7 进行比较,5<7,同时 5>3,所以插入到 3 和 7 中间 ![](https://static.vue-js.com/b1981940-267f-11ec-8e64-91fdec0f05a1.png) 照此规律,依次将无序表中的记录 4,9 和 6插入到有序表中 ![](https://static.vue-js.com/bc2ed290-267f-11ec-a752-75723a64e8f5.png) ## 二、如何实现 将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。 从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置 如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面 ![](https://www.runoob.com/wp-content/uploads/2019/03/insertionSort.gif) 用代码表示则如下: ```js function insertionSort(arr) { const len = arr.length; let preIndex, current; for (let i = 1; i < len; i++) { preIndex = i - 1; current = arr[i]; while(preIndex >= 0 && arr[preIndex] > current) { arr[preIndex+1] = arr[preIndex]; preIndex--; } arr[preIndex+1] = current; } return arr; } ``` 在插入排序中,当待排序数组是有序时,是最优的情况,只需当前数跟前一个数比较一下就可以了,这时一共需要比较`N- 1`次,时间复杂度为`O(n)` 最坏的情况是待排序数组是逆序的,此时需要比较次数最多,总次数记为:1+2+3+…+N-1,所以,插入排序最坏情况下的时间复杂度为`O(n^2)` 通过上面了解,可以看到插入排序是一种稳定的排序方式 ## 三、应用场景 插入排序时间复杂度是 O(n2),适用于数据量不大,算法稳定性要求高,且数据局部或整体有序的数列排序 ## 参考文献 - https://baike.baidu.com/item/%E6%8F%92%E5%85%A5%E6%8E%92%E5%BA%8F/7214992 - http://data.biancheng.net/view/65.html ================================================ FILE: docs/algorithm/mergeSort.md ================================================ # 面试官:说说你对归并排序的理解?如何实现?应用场景? ![](https://static.vue-js.com/fa1d5720-26ac-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 归并排序(Merge Sort)是建立归并操作上的一种有效,稳定的排序算法,该算法是采用分治法的一个非常典型的应用 将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序 例如对于含有 `n` 个记录的无序表,首先默认表中每个记录各为一个有序表(只不过表的长度都为 1) 然后进行两两合并,使 `n` 个有序表变为`n/2` 个长度为 2 或者 1 的有序表(例如 4 个小有序表合并为 2 个大的有序表) 通过不断地进行两两合并,直到得到一个长度为 `n` 的有序表为止 例如对无序表{49,38,65,97,76,13,27}进行归并排序分成了分、合两部分: 如下图所示: ![](https://static.vue-js.com/05f14b60-26ad-11ec-a752-75723a64e8f5.png) 归并合过程中,每次得到的新的子表本身有序,所以最终得到有序表 上述分成两部分,则称为二路归并,如果分成三个部分则称为三路归并,以此类推 ## 二、如何实现 关于归并排序的算法思路如下: - 分:把数组分成两半,再递归对子数组进行分操作,直至到一个个单独数字 - 合:把两个数合成有序数组,再对有序数组进行合并操作,直到全部子数组合成一个完整的数组 - 合并操作可以新建一个数组,用于存放排序后的数组 - 比较两个有序数组的头部,较小者出队并且推入到上述新建的数组中 - 如果两个数组还有值,则重复上述第二步 - 如果只有一个数组有值,则将该数组的值出队并推入到上述新建的数组中 ![](https://www.runoob.com/wp-content/uploads/2019/03/mergeSort.gif) 用代码表示则如下图所示: ```js function mergeSort(arr) { // 采用自上而下的递归方法 const len = arr.length; if(len < 2) { return arr; } let middle = Math.floor(len / 2), left = arr.slice(0, middle), right = arr.slice(middle); return merge(mergeSort(left), mergeSort(right)); } function merge(left, right) { const result = []; while (left.length && right.length) { if (left[0] <= right[0]) { result.push(left.shift()); } else { result.push(right.shift()); } } while (left.length) result.push(left.shift()); while (right.length) result.push(right.shift()); return result; } ``` 上述归并分成了分、合两部分,在处理分过程中递归调用两个分的操作,所花费的时间为2乘`T(n/2)`,合的操作时间复杂度则为`O(n)`,因此可以得到以下公式: 总的执行时间 = 2 × 输入长度为`n/2`的`sort`函数的执行时间 + `merge`函数的执行时间`O(n)` 当只有一个元素时,`T(1) = O(1)` 如果对`T(n) = 2 * T(n/2) + O(n) `进行左右 / n的操作,得到 `T(n) / n = (n / 2) * T(n/2) + O(1)` 现在令 `S(n) = T(n)/n`,则`S(1) = O(1)`,然后利用表达式带入得到`S(n) = S(n/2) + O(1)` 所以可以得到:`S(n) = S(n/2) + O(1) = S(n/4) + O(2) = S(n/8) + O(3) = S(n/2^k) + O(k) = S(1) + O(logn) = O(logn)` 综上可得,`T(n) = n * log(n) = nlogn` 关于归并排序的稳定性,在进行合并过程,在1个或2个元素时,1个元素不会交换,2个元素如果大小相等也不会交换,由此可见归并排序是稳定的排序算法 ## 三、应用场景 在外排序中通常使用排序-归并的策略,外排序是指处理超过内存限度的数据的排序算法,通常将中间结果放在读写较慢的外存储器,如下分成两个阶段: - 排序阶段:读入能够放进内存中的数据量,将其排序输出到临时文件,一次进行,将带排序数据组织为多个有序的临时文件 - 归并阶段:将这些临时文件组合为大的有序文件 例如,使用100m内存对900m的数据进行排序,过程如下: - 读入100m数据内存,用常规方式排序 - 将排序后的数据写入磁盘 - 重复前两个步骤,得到9个100m的临时文件 - 将100m的内存划分为10份,将9份为输入缓冲区,第10份为输出缓冲区 - 进行九路归并排序,将结果输出到缓冲区 - 若输出缓冲区满,将数据写到目标文件,清空缓冲区 - 若缓冲区空,读入相应文件的下一份数据 ## 参考文献 - https://baike.baidu.com/item/%E5%BD%92%E5%B9%B6%E6%8E%92%E5%BA%8F/1639015 - https://chowdera.com/2021/09/20210920201630258d.html#_127 - https://juejin.cn/post/6844904007899561998 ================================================ FILE: docs/algorithm/quickSort.md ================================================ # 面试官:说说你对快速排序的理解?如何实现?应用场景? ![](https://static.vue-js.com/bafae570-268a-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 快速排序(Quick Sort)算法是在冒泡排序的基础上进行改进的一种算法,从名字上看就知道该排序算法的特点是快、效率高,是处理大数据最快的排序算法之一 实现的基本思想是:通过一次排序将整个无序表分成相互独立的两部分,其中一部分中的数据都比另一部分中包含的数据的值小 然后继续沿用此方法分别对两部分进行同样的操作,直到每一个小部分不可再分,所得到的整个序列就变成有序序列 例如,对无序表49,38,65,97,76,13,27,49进行快速排序,大致过程为: - 首先从表中选取一个记录的关键字作为分割点(称为“枢轴”或者支点,一般选择第一个关键字),例如选取 49 - 将表格中大于 49 个放置于 49 的右侧,小于 49 的放置于 49 的左侧,假设完成后的无序表为:{27,38,13,49,65,97,76,49} - 以 49 为支点,将整个无序表分割成了两个部分,分别为{27,38,13}和{65,97,76,49},继续采用此种方法分别对两个子表进行排序 - 前部分子表以 27 为支点,排序后的子表为{13,27,38},此部分已经有序;后部分子表以 65 为支点,排序后的子表为{49,65,97,76} - 此时前半部分子表中的数据已完成排序;后部分子表继续以 65 为支点,将其分割为{49}和{97,76},前者不需排序,后者排序后的结果为{76, 97} - 通过以上几步的排序,最后由子表{13,27,38}、{49}、{49}、{65}、{76,97}构成有序表:{13,27,38,49,49,65,76,97} ## 二、如何实现 可以分成以下步骤: - 分区:从数组中选择任意一个基准,所有比基准小的元素放在基准的左边,比基准大的元素放到基准的右边 - 递归:递归地对基准前后的子数组进行分区 ![](https://www.runoob.com/wp-content/uploads/2019/03/quickSort.gif) 用代码表示则如下: ```js function quickSort (arr) { const rec = (arr) => { if (arr.length <= 1) { return arr; } const left = []; const right = []; const mid = arr[0]; // 基准元素 for (let i = 1; i < arr.length; i++){ if (arr[i] < mid) { left.push(arr[i]); } else { right.push(arr[i]); } } return [...rec(left), mid, ...rec(right)] } return res(arr) }; ``` 快速排序是冒泡排序的升级版,最坏情况下每一次基准元素都是数组中最小或者最大的元素,则快速排序就是冒泡排序 这种情况时间复杂度就是冒泡排序的时间复杂度:`T[n] = n * (n-1) = n^2 + n`,也就是`O(n^2)` 最好情况下是`O(nlogn)`,其中递归算法的时间复杂度公式:`T[n] = aT[n/b] + f(n)`,推导如下所示: ![](https://static.vue-js.com/b6019540-2b5e-11ec-8e64-91fdec0f05a1.png) 关于上述代码实现的快速排序,可以看到是稳定的 ## 三、应用场景 快速排序时间复杂度为`O(nlogn)`,是目前基于比较的内部排序中被认为最好的方法,当数据过大且数据杂乱无章时,则适合采用快速排序 ## 参考文献 - https://baike.baidu.com/item/%E5%BF%AB%E9%80%9F%E6%8E%92%E5%BA%8F%E7%AE%97%E6%B3%95/369842 - https://www.cnblogs.com/l199616j/p/10597245.html ================================================ FILE: docs/algorithm/selectionSort.md ================================================ # 面试官:说说你对选择排序的理解?如何实现?应用场景? ![](https://static.vue-js.com/50a05ed0-2671-11ec-a752-75723a64e8f5.png) ## 一、是什么 选择排序(Selection sort)是一种简单直观的排序算法,无论什么数据进去都是 `O(n²) `的时间复杂度,所以用到它的时候,数据规模越小越好 其基本思想是:首先在未排序的数列中找到最小(or最大)元素,然后将其存放到数列的起始位置 然后再从剩余未排序的元素中继续寻找最小(or最大)元素,然后放到已排序序列的末尾 以此类推,直到所有元素均排序完毕 举个例子,一个数组为 56、12、80、91、29,其排序过程如下: - 第一次遍历时,从下标为 1 的位置即 56 开始,找出关键字值最小的记录 12,同下标为 0 的关键字 56 交换位置。此时数组为 12、56、80、91、20 ![](https://static.vue-js.com/60bd2050-2671-11ec-a752-75723a64e8f5.png) - 第二次遍历时,从下标为 2 的位置即 56 开始,找出最小值 20,同下标为 2 的关键字 56 互换位置,此时数组为12、20、80、91、56 ![](https://static.vue-js.com/6b04cf40-2671-11ec-8e64-91fdec0f05a1.png) - 第三次遍历时,从下标为 3 的位置即 80 开始,找出最小值 56,同下标为 3 的关键字 80 互换位置,此时数组为 12、20、56、91、80 ![](https://static.vue-js.com/757f4e00-2671-11ec-a752-75723a64e8f5.png) - 第四次遍历时,从下标为 4 的位置即 91 开始,找出最小是 80,同下标为 4 的关键字 91 互换位置,此时排序完成,变成有序数组 ![](https://static.vue-js.com/757f4e00-2671-11ec-a752-75723a64e8f5.png) ## 二、如何实现 从上面可以看到,对于具有 `n` 个记录的无序表遍历 `n-1` 次,第` i` 次从无序表中第 `i` 个记录开始,找出后序关键字中最小的记录,然后放置在第 `i` 的位置上 直至到从第`n`个和第`n-1`个元素中选出最小的放在第`n-1`个位置 如下动画所示: ![](https://www.runoob.com/wp-content/uploads/2019/03/selectionSort.gif) 用代码表示则如下: ```js function selectionSort(arr) { var len = arr.length; var minIndex, temp; for (var i = 0; i < len - 1; i++) { minIndex = i; for (var j = i + 1; j < len; j++) { if (arr[j] < arr[minIndex]) { // 寻找最小的数 minIndex = j; // 将最小数的索引保存 } } temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } return arr; } ``` 第一次内循环比较`N - 1`次,然后是`N-2`次,`N-3`次,……,最后一次内循环比较1次 共比较的次数是 `(N - 1) + (N - 2) + ... + 1`,求等差数列和,得 `(N - 1 + 1)* N / 2 = N^2 / 2`,舍去最高项系数,其时间复杂度为 `O(N^2)` 从上述也可以看到,选择排序是一种稳定的排序 ## 三、应用场景 和冒泡排序一致,相比其它排序算法,这也是一个相对较高的时间复杂度,一般情况不推荐使用 但是我们还是要掌握冒泡排序的思想及实现,这对于我们的算法思维是有很大帮助的 ## 参考文献 - https://baike.baidu.com/item/%E9%80%89%E6%8B%A9%E6%8E%92%E5%BA%8F/9762418 - https://zhuanlan.zhihu.com/p/29889599 - http://data.biancheng.net/view/72.html ================================================ FILE: docs/algorithm/set.md ================================================ # 面试官:说说你对集合的理解?常见的操作有哪些? ![](https://static.vue-js.com/e3de7810-1d36-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 集合(Set),指具有某种特定性质的事物的总体,里面的每一项内容称作元素 在数学中,我们经常会遇到集合的概念: - 有限集合:例如一个班集所有的同学构成的集合 - 无限集合:例如全体自然数集合 在计算机中集合道理也基本一致,具有三大特性: - 确定性:于一个给定的集合,集合中的元素是确定的。即一个元素,或者属于该集合,或者不属于该集合,两者必居其一 - 无序性:在一个集合中,不考虑元素之间的顺序,只要元素完全相同,就认为是同一个集合 - 互异性:集合中任意两个元素都是不同的 ## 二、操作 在`ES6`中,集合本身是一个构建函数`Set`,用来生成 `Set` 数据结构,如下: ```js const s = new Set(); ``` 关于集合常见的方法有: - add():增 - delete():删 - has():改 - clear():查 ### add() 添加某个值,返回 `Set` 结构本身 当添加实例中已经存在的元素,`set`不会进行处理添加 ```js s.add(1).add(2).add(2); // 2只被添加了一次 ``` 体现了集合的互异性特性 ### delete() 删除某个值,返回一个布尔值,表示删除是否成功 ```js s.delete(1) ``` ### has() 返回一个布尔值,判断该值是否为`Set`的成员 ```js s.has(2) ``` ### clear() 清除所有成员,没有返回值 ```js s.clear() ``` 关于多个集合常见的操作有: - 并集 - 交集 - 差集 ### 并集 两个集合的共同元素,如下图所示: ![](https://static.vue-js.com/ed96df50-1d36-11ec-a752-75723a64e8f5.png) 代码实现方式如下: ```js let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); // 并集 let union = new Set([...a, ...b]); // Set {1, 2, 3, 4} ``` ### 交集 两个集合`A` 和 `B`,即属于`A`又属于`B`的元素,如下图所示: ![](https://static.vue-js.com/f8a9cd80-1d36-11ec-a752-75723a64e8f5.png) 用代码标识则如下: ```js let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); // 交集 let intersect = new Set([...a].filter(x => b.has(x))); // set {2, 3} ``` ### 差集 两个集合`A` 和 `B`,属于`A`的元素但不属于`B`的元素称为`A`相对于`B`的差集,如下图所示: ![](https://static.vue-js.com/0191c560-1d37-11ec-8e64-91fdec0f05a1.png) 代码标识则如下: ```js let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); // (a 相对于 b 的)差集 let difference = new Set([...a].filter(x => !b.has(x))); // Set {1} ``` ## 三、应用场景 一般情况下,使用数组的概率会比集合概率高很多 使用`set`集合的场景一般是借助其确定性,其本身只包含不同的元素 所以,可以利用`Set`的一些原生方法轻松的完成数组去重,查找数组公共元素及不同元素等操作 ## 参考文献 - https://zh.wikipedia.org/wiki/%E5%B9%B6%E9%9B%86 - https://zh.wikipedia.org/wiki/%E8%A1%A5%E9%9B%86 ================================================ FILE: docs/algorithm/sort.md ================================================ # 面试官:说说常见的排序算法有哪些?区别? ![](https://static.vue-js.com/63eb7920-211c-11ec-a752-75723a64e8f5.png) ## 一、是什么 排序是程序开发中非常常见的操作,对一组任意的数据元素经过排序操作后,就可以把他们变成一组一定规则排序的有序序列 排序算法属于算法中的一种,而且是覆盖范围极小的一种,彻底掌握排序算法对程序开发是有很大的帮助的 对与排序算法的好坏衡量,主要是从时间复杂度、空间复杂度、稳定性 时间复杂度、空间复杂度前面已经讲过,这里主要看看稳定性的定义 稳定性指的是假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变 即在原序列中,r[i] = r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的 ## 二、有哪些 常见的算法排序算法有: - 冒泡排序 - 选择排序 - 插入排序 - 归并排序 - 快速排序 ### 冒泡排序 一种简单直观的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来 思路如下: - 比较相邻的元素,如果第一个比第二个大,就交换它们两个 - 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数 - 针对所有的元素重复以上的步骤,除了最后一个 - 重复上述步骤,直到没有任何一堆数字需要比较 ![](https://pic4.zhimg.com/v2-33a947c71ad62b254cab62e5364d2813_b.webp) ### 选择排序 选择排序是一种简单直观的排序算法,它也是一种交换排序算法 无论什么数据进去都是 `O(n²) `的时间复杂度。所以用到它的时候,数据规模越小越好 唯一的好处是不占用额外的内存存储空间 思路如下: - 在未排序序列中找到最小(大)元素,存放到排序序列的起始位置 - 从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。 - 重复第二步,直到所有元素均排序完毕 ![](https://pic1.zhimg.com/v2-1c7e20f306ddc02eb4e3a50fa7817ff4_b.webp) ### 插入排序 插入排序是一种简单直观的排序算法 它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入 解决思路如下: - 把待排序的数组分成已排序和未排序两部分,初始的时候把第一个元素认为是已排好序的 - 从第二个元素开始,在已排好序的子数组中寻找到该元素合适的位置并插入该位置(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。) - 重复上述过程直到最后一个元素被插入有序子数组中 ![](https://pic3.zhimg.com/v2-91b76e8e4dab9b0cad9a017d7dd431e2_b.webp) ### 归并排序 归并排序是建立在归并操作上的一种有效的排序算法 该算法是采用分治法的一个非常典型的应用 将已有序的子序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序 解决思路如下: - 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列 - 设定两个指针,最初位置分别为两个已经排序序列的起始位置 - 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置 - 重复步骤3直到某一指针到达序列尾 - 将另一序列剩下的所有元素直接复制到合并序列尾 ![](https://pic3.zhimg.com/v2-cdda3f11c6efbc01577f5c29a9066772_b.jpg) ### 快速排序 快速排序是对冒泡排序算法的一种改进,基本思想是通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小 再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列 解决思路如下: - 从数列中挑出一个元素,称为"基准"(pivot) - 重新排序数列,所有比基准值小的元素摆放在基准前面,所有比基准值大的元素摆在基准后面(相同的数可以到任何一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作 - 递归地(recursively)把小于基准值元素的子数列和大于基准值元素的子数列排序 ![](https://pic1.zhimg.com/v2-c411339b79f92499dcb7b5f304c826f4_b.jpg) ## 三、区别 除了上述的排序算法之外,还存在其他的排序算法,例如希尔排序、堆排序等等...... 区别如下图所示: ![](https://static.vue-js.com/5c3d7b50-2131-11ec-a752-75723a64e8f5.png) ## 参考文献 - https://www.runoob.com/w3cnote/bubble-sort.html - http://www.x-lab.info/post/sort-algorithm/ - https://zhuanlan.zhihu.com/p/42586566 ================================================ FILE: docs/algorithm/stack_queue.md ================================================ # 面试官:说说你对栈、队列的理解?应用场景? ![](https://static.vue-js.com/bc57f530-1b99-11ec-a752-75723a64e8f5.png) ## 一、栈 栈(stack)又名堆栈,它是一种运算受限的线性表,限定仅在表尾进行插入和删除操作的线性表 表尾这一端被称为栈顶,相反地另一端被称为栈底,向栈顶插入元素被称为进栈、入栈、压栈,从栈顶删除元素又称作出栈 所以其按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据,具有记忆作用 关于栈的简单实现,如下: ```js class Stack { constructor() { this.items = []; } /** * 添加一个(或几个)新元素到栈顶 * @param {*} element 新元素 */ push(element) { this.items.push(element) } /** * 移除栈顶的元素,同时返回被移除的元素 */ pop() { return this.items.pop() } /** * 返回栈顶的元素,不对栈做任何修改(这个方法不会移除栈顶的元素,仅仅返回它) */ peek() { return this.items[this.items.length - 1] } /** * 如果栈里没有任何元素就返回true,否则返回false */ isEmpty() { return this.items.length === 0 } /** * 移除栈里的所有元素 */ clear() { this.items = [] } /** * 返回栈里的元素个数。这个方法和数组的length属性很类似 */ size() { return this.items.length } } ``` 关于栈的操作主要的方法如下: - push:入栈操作 - pop:出栈操作 ## 二、队列 跟栈十分相似,队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作 进行插入操作的端称为队尾,进行删除操作的端称为队头,当队列中没有元素时,称为空队列 在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出 简单实现一个队列的方式,如下: ```js class Queue { constructor() { this.list = [] this.frontIndex = 0 this.tailIndex = 0 } enqueue(item) { this.list[this.tailIndex++] = item } unqueue() { const item = this.list[this.frontIndex] this.frontIndex++ return item } } ``` 上述这种入队和出队操作中,头尾指针只增加不减小,致使被删元素的空间永远无法重新利用 当队列中实际的元素个数远远小于向量空间的规模时,也可能由于尾指针已超越向量空间的上界而不能做入队操作,出该现象称为"假溢" 在实际使用队列时,为了使队列空间能重复使用,往往对队列的使用方法稍加改进: 无论插入或删除,一旦`rear`指针增1或`front`指针增1 时超出了所分配的队列空间,就让它指向这片连续空间的起始位置,这种队列也就是循环队列 下面实现一个循环队列,如下: ```js class Queue { constructor(size) { this.size = size; // 长度需要限制, 来达到空间的利用, 代表空间的长度 this.list = []; this.font = 0; // 指向首元素 this.rear = 0; // 指向准备插入元素的位置 } enQueue() { if (this.isFull() == true) { return false } this.rear = this.rear % this.k; this._data[this.rear++] = value; return true } deQueue() { if(this.isEmpty()){ return false; } this.font++; this.font = this.font % this.k; return true; } isEmpty() { return this.font == this.rear - 1; } isFull() { return this.rear % this.k == this.font; } } ``` 上述通过求余的形式代表首尾指针增1 时超出了所分配的队列空间 ## 三、应用场景 ### 栈 借助栈的先进后出的特性,可以简单实现一个逆序数处的功能,首先把所有元素依次入栈,然后把所有元素出栈并输出 包括编译器的在对输入的语法进行分析的时候,例如`"()"`、`"{}"`、`"[]"`这些成对出现的符号,借助栈的特性,凡是遇到括号的前半部分,即把这个元素入栈,凡是遇到括号的后半部分就比对栈顶元素是否该元素相匹配,如果匹配,则前半部分出栈,否则就是匹配出错 包括函数调用和递归的时候,每调用一个函数,底层都会进行入栈操作,出栈则返回函数的返回值 生活中的例子,可以把乒乓球盒比喻成一个堆栈,球一个一个放进去(入栈),最先放进去的要等其后面的全部拿出来后才能出来(出栈),这种就是典型的先进后出模型 ### 队列 当我们需要按照一定的顺序来处理数据,而该数据的数据量在不断地变化的时候,则需要队列来帮助解题 队列的使用广泛应用在广度优先搜索种,例如层次遍历一个二叉树的节点值(后续将到) 生活中的例子,排队买票,排在队头的永远先处理,后面的必须等到前面的全部处理完毕再进行处理,这也是典型的先进先出模型 ## 参考文献 - https://baike.baidu.com/item/%E6%A0%88/12808149 - https://baike.baidu.com/item/%E9%98%9F%E5%88%97/14580481 ================================================ FILE: docs/algorithm/structure.md ================================================ # 面试官:说说你对数据结构的理解?有哪些?区别? ![](https://static.vue-js.com/3d87b540-1aa6-11ec-a752-75723a64e8f5.png) ## 一、是什么 数据结构是计算机存储、组织数据的方式,是指相互之间存在一种或多种特定关系的数据元素的集合 前面讲到,一个程序 = 算法 + 数据结构,数据结构是实现算法的基础,选择合适的数据结构可以带来更高的运行或者存储效率 数据元素相互之间的关系称为结构,根据数据元素之间关系的不同特性,通常有如下四类基本的结构: - 集合结构:该结构的数据元素间的关系是“属于同一个集合” - 线性结构:该结构的数据元素之间存在着一对一的关系 - 树型结构:该结构的数据元素之间存在着一对多的关系 - 图形结构:该结构的数据元素之间存在着多对多的关系,也称网状结构 由于数据结构种类太多,逻辑结构可以再分成为: - 线性结构:有序数据元素的集合,其中数据元素之间的关系是一对一的关系,除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的 - 非线性结构:各个数据元素不再保持在一个线性序列中,每个数据元素可能与零个或者多个其他数据元素发生关联 ![](https://static.vue-js.com/9aedc5d0-1aa6-11ec-8e64-91fdec0f05a1.png) ## 二、有哪些 常见的数据结构有如下: - 数组 - 栈 - 队列 - 链表 - 树 - 图 - 堆 - 散列表 ### 数组 在程序设计中,为了处理方便, 一般情况把具有相同类型的若干变量按有序的形式组织起来,这些按序排列的同类数据元素的集合称为数组 ### 栈 一种特殊的线性表,只能在某一端插入和删除的特殊线性表,按照先进后出的特性存储数据 先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据 ### 队列 跟栈基本一致,也是一种特殊的线性表,其特性是先进先出,只允许在表的前端进行删除操作,而在表的后端进行插入操作 ### 链表 是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成 一般情况,每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域 ### 树 树是典型的非线性结构,在树的结构中,有且仅有一个根结点,该结点没有前驱结点。在树结构中的其他结点都有且仅有一个前驱结点,而且可以有两个以上的后继结点 ### 图 一种非线性结构。在图结结构中,数据结点一般称为顶点,而边是顶点的有序偶对。如果两个顶点之间存在一条边,那么就表示这两个顶点具有相邻关系 ### 堆 堆是一种特殊的树形数据结构,每个结点都有一个值,特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆 ### 散列表 若结构中存在关键字和`K`相等的记录,则必定在`f(K)`的存储位置上,不需比较便可直接取得所查记录 ## 三、区别 上述的数据结构,之前的区别可以分成线性结构和非线性结构: - 线性结构有:数组、栈、队列、链表等 - 非线性结构有:树、图、堆等 ## 参考文献 - https://zh.wikipedia.org/wiki/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84 - https://baike.baidu.com/item/数据结构/1450 ================================================ FILE: docs/algorithm/time_space.md ================================================ # 面试官:说说你对算法中时间复杂度,空间复杂度的理解?如何计算? ![](https://static.vue-js.com/07fd4050-16fc-11ec-a752-75723a64e8f5.png) ## 一、前言 算法(Algorithm)是指用来操作数据、解决程序问题的一组方法。对于同一个问题,使用不同的算法,也许最终得到的结果是一样的,但在过程中消耗的资源和时间却会有很大的区别 衡量不同算法之间的优劣主要是通过**时间**和**空间**两个维度去考量: - 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。 - 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述 通常会遇到一种情况,时间和空间维度不能够兼顾,需要在两者之间取得一个平衡点是我们需要考虑的 一个算法通常存在最好、平均、最坏三种情况,我们一般关注的是最坏情况 最坏情况是算法运行时间的上界,对于某些算法来说,最坏情况出现的比较频繁,也意味着平均情况和最坏情况一样差 ## 二、时间复杂度 时间复杂度是指执行这个算法所需要的计算工作量,其复杂度反映了程序执行时间「随输入规模增长而增长的量级」,在很大程度上能很好地反映出算法的优劣与否 一个算法花费的时间与算法中语句的「执行次数成正比」,执行次数越多,花费的时间就越多 算法的复杂度通常用大O符号表述,定义为`T(n) = O(f(n))`,常见的时间复杂度有:O(1)常数型、O(log n)对数型、O(n)线性型、O(nlogn)线性对数型、O(n^2)平方型、O(n^3)立方型、O(n^k)k次方型、O(2^n)指数型,如下图所示: ![](https://static.vue-js.com/33d5ebf0-16fc-11ec-8e64-91fdec0f05a1.png) 从上述可以看到,随着问题规模`n`的不断增大,上述时间复杂度不断增大,算法的执行效率越低,由小到大排序如下: ```js Ο(1)<Ο(log n)<Ο(n)<Ο(nlog n)<Ο(n2)<Ο(n3)<…<Ο(2^n)<Ο(n!) ``` 注意的是,算法复杂度只是描述算法的增长趋势,并不能说一个算法一定比另外一个算法高效,如果常数项过大的时候也会导致算法的执行时间变长 关于如何计算时间复杂度,可以看看如下简单例子: ```js function process(n) { let a = 1 let b = 2 let sum = a + b for(let i = 0; i < n; i++) { sum += i } return sum } ``` 该函数算法需要执行的运算次数用输入大小`n`的函数表示,即 `T(n) = 2 + n + 1`,那么时间复杂度为`O(n + 3)`,又因为时间复杂度只关注最高数量级,且与之系数也没有关系,因此上述的时间复杂度为`O(n)` 又比如下面的例子: ```js function process(n) { let count = 0 for(let i = 0; i < n; i++){ for(let i = 0; i < n; i++){ count += 1 } } } ``` 循环里面嵌套循环,外面的循环执行一次,里面的循环执行`n`次,因此时间复杂度为 `O(n*n*1 + 2) = O(n^2)` 对于顺序执行的语句,总的时间复杂度等于其中最大的时间复杂度,如下: ```js function process(n) { let sum = 0 for(let i = 0; i < n; i++) { sum += i } for(let i = 0; i < n; i++){ for(let i = 0; i < n; i++){ sum += 1 } } return sum } ``` 上述第一部分复杂度为`O(n)`,第二部分复杂度为`O(n^2)`,总复杂度为`max(O(n^2), O(n)) = O(n^2)` 又如下一个例子: ```js function process(n) { let i = 1; // ① while (i <= n) { i = i * 2; // ② } } ``` 循环语句中以2的倍数来逼近`n`,每次都乘以2。如果用公式表示就是1 * 2 * 2 * 2 … * 2 <=n,也就是说2的`x`次方小于等于`n`时会执行循环体,记作`2^x <= n`,于是得出`x<=logn` 因此循环在执行`logn`次之后,便结束,因此时间复杂度为`O(logn)` 同理,如果一个`O(n)`循环里面嵌套`O(logn)`的循环,则时间复杂度为`O(nlogn)`,像`O(n^3)`无非也就是嵌套了三层`O(n)`循环 ## 三、空间复杂度 空间复杂度主要指执行算法所需内存的大小,用于对程序运行过程中所需要的临时存储空间的度量 除了需要存储空间、指令、常数、变量和输入数据外,还包括对数据进行操作的工作单元和存储计算所需信息的辅助空间 下面给出空间复杂度为`O(1)`的示例,如下 ```js let a = 1 let b = 2 let c = 3 ``` 上述代码的临时空间不会随着`n`的变化而变化,因此空间复杂度为`O(1)` ```js let arr [] for(i=1; i<=n; ++i){ arr.push(i) } ``` 上述可以看到,随着`n`的增加,数组的占用的内存空间越大 通常来说,只要算法不涉及到动态分配的空间,以及递归、栈所需的空间,空间复杂度通常为`O(1)`,一个一维数组`a[n]`,空间复杂度`O(n)`,二维数组为`O(n^2)` ## 参考文献 - https://juejin.cn/post/6844904167824162823#heading-7 - https://zhuanlan.zhihu.com/p/50479555 - https://cloud.tencent.com/developer/article/1769988 ================================================ FILE: docs/algorithm/tree.md ================================================ # 面试官:说说你对树的理解?相关的操作有哪些? ![](https://static.vue-js.com/5a7616f0-1dfe-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 在计算机领域,树形数据结构是一类重要的非线性数据结构,可以表示数据之间一对多的关系。以树与二叉树最为常用,直观看来,树是以分支关系定义的层次结构 二叉树满足以下两个条件: - 本身是有序树 - 树中包含的各个结点的不能超过 2,即只能是 0、1 或者 2 如下图,左侧的为二叉树,而右侧的因为头结点的子结点超过2,因此不属于二叉树: ![](https://static.vue-js.com/66758800-1dfe-11ec-a752-75723a64e8f5.png) 同时,二叉树可以继续进行分类,分成了满二叉树和完成二叉树: - 满二叉树:如果二叉树中除了叶子结点,每个结点的度都为 2 ![](https://static.vue-js.com/759db050-1dfe-11ec-a752-75723a64e8f5.png) - 完成二叉树:如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布 ![](https://static.vue-js.com/84ae31f0-1dfe-11ec-8e64-91fdec0f05a1.png) ## 二、操作 关于二叉树的遍历,常见的有: - 前序遍历 - 中序遍历 - 后序遍历 - 层序遍历 ### 前序遍历 前序遍历的实现思想是: - 访问根节点 - 访问当前节点的左子树 - 若当前节点无左子树,则访问当前节点的右子 根据遍历特性,递归版本用代码表示则如下: ```js const preOrder = (root) => { if(!root){ return } console.log(root) preOrder(root.left) preOrder(root.right) } ``` 如果不使用递归版本,可以借助栈先进后出的特性实现,先将根节点压入栈,再分别压入右节点和左节点,直到栈中没有元素,如下: ```js const preOrder = (root) => { if(!root){ return } const stack = [root] while (stack.length) { const n = stack.pop() console.log(n.val) if (n.right) { stack.push(n.right) } if (n.left) { stack.push(n.left) } } } ``` ### 中序遍历 前序遍历的实现思想是: - 访问当前节点的左子树 - 访问根节点 - 访问当前节点的右子 递归版本很好理解,用代码表示则如下: ```js const inOrder = (root) => { if (!root) { return } inOrder(root.left) console.log(root.val) inOrder(root.right) } ``` 非递归版本也是借助栈先进后出的特性,可以一直首先一直压入节点的左元素,当左节点没有后,才开始进行出栈操作,压入右节点,然后有依次压入左节点,如下: ```js const inOrder = (root) => { if (!root) { return } const stack = [root] let p = root while(stack.length || p){ while (p) { stack.push(p) p = p.left } const n = stack.pop() console.log(n.val) p = n.right } } ``` ### 后序遍历 前序遍历的实现思想是: - 访问当前节点的左子树 - 访问当前节点的右子 - 访问根节点 递归版本,用代码表示则如下: ```js const postOrder = (root) => { if (!root) { return } postOrder(root.left) postOrder(root.right) console.log(n.val) } ``` 后序遍历非递归版本实际根全序遍历是逆序关系,可以再多创建一个栈用来进行输出,如下: ```js const preOrder = (root) => { if(!root){ return } const stack = [root] const outPut = [] while (stack.length) { const n = stack.pop() outPut.push(n.val) if (n.right) { stack.push(n.right) } if (n.left) { stack.push(n.left) } } while (outPut.length) { const n = outPut.pop() console.log(n.val) } } ``` ### 层序遍历 按照二叉树中的层次从左到右依次遍历每层中的结点 借助队列先进先出的特性,从树的根结点开始,依次将其左孩子和右孩子入队。而后每次队列中一个结点出队,都将其左孩子和右孩子入队,直到树中所有结点都出队,出队结点的先后顺序就是层次遍历的最终结果 用代码表示则如下: ```js const levelOrder = (root) => { if (!root) { return [] } const queue = [[root, 0]] const res = [] while (queue.length) { const n = queue.shift() const [node, leval] = n if (!res[leval]) { res[leval] = [node.val] } else { res[leval].push(node.val) } if (node.left) { queue.push([node.left, leval + 1]) } if (node.right) { queue.push([node.right, leval + 1]) } } return res }; ``` ## 三、总结 树是一个非常重要的非线性结构,其中二叉树以二叉树最常见,二叉树的遍历方式可以分成前序遍历、中序遍历、后序遍历 同时,二叉树又分成了完成二叉树和满二叉树 ## 参考文献 - https://baike.baidu.com/item/%E4%BA%8C%E5%8F%89%E6%A0%91 - http://data.biancheng.net/view/27.html ================================================ FILE: docs/applet/WebView_jscore.md ================================================ - # 面试官:说说微信小程序的实现原理? ![](https://static.vue-js.com/4407cb60-3722-11ec-a752-75723a64e8f5.png) ## 一、背景 网页开发,渲染线程和脚本是互斥的,这也是为什么长时间的脚本运行可能会导致页面失去响应的原因,本质就是我们常说的 `JS` 是单线程的 而在小程序中,选择了 `Hybrid` 的渲染方式,将视图层和逻辑层是分开的,双线程同时运行,视图层的界面使用 `WebView` 进行渲染,逻辑层运行在 `JSCore` 中 ![](https://static.vue-js.com/4e322e50-3722-11ec-8e64-91fdec0f05a1.png) - 渲染层:界面渲染相关的任务全都在 WebView 线程里执行。一个小程序存在多个界面,所以渲染层存在多个 WebView 线程 - 逻辑层:采用 JsCore 线程运行 JS 脚本,在这个环境下执行的都是有关小程序业务逻辑的代码 ## 二、通信 小程序在渲染层,宿主环境会把`wxml`转化成对应的`JS`对象 在逻辑层发生数据变更的时候,通过宿主环境提供的`setData`方法把数据从逻辑层传递到渲染层,再经过对比前后差异,把差异应用在原来的`Dom`树上,渲染出正确的视图 ![](https://static.vue-js.com/5948ed10-3722-11ec-a752-75723a64e8f5.png) 当视图存在交互的时候,例如用户点击你界面上某个按钮,这类反馈应该通知给开发者的逻辑层,需要将对应的处理状态呈现给用户 对于事件的分发处理,微信进行了特殊的处理,将所有的事件拦截后,丢到逻辑层交给`JavaScript`进行处理 ![](https://static.vue-js.com/61f9f670-3722-11ec-a752-75723a64e8f5.png) 由于小程序是基于双线程的,也就是任何在视图层和逻辑层之间的数据传递都是线程间的通信,会有一定的延时,因此在小程序中,页面更新成了异步操作 异步会使得各部分的运行时序变得复杂一些,比如在渲染首屏的时候,逻辑层与渲染层会同时开始初始化工作,但是渲染层需要有逻辑层的数据才能把界面渲染出来 如果渲染层初始化工作较快完成,就要等逻辑层的指令才能进行下一步工作 因此逻辑层与渲染层需要有一定的机制保证时序正确,在每个小程序页面的生命周期中,存在着若干次页面数据通信 ![](https://static.vue-js.com/6cb798b0-3722-11ec-a752-75723a64e8f5.png) ## 三、运行机制 小程序启动运行两种情况: - 冷启动(重新开始):用户首次打开或者小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动,即为冷启动 - 热启动:用户已经打开过小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需要将后台态的小程序切换到前台,这个过程就是热启动 #### 需要注意: > 1.小程序没有重启的概念 > 2.当小程序进入后台,客户端会维持一段时间的运行状态,超过一定时间后会被微信主动销毁 > 3.短时间内收到系统两次以上内存警告,也会对小程序进行销毁,这也就为什么一旦页面内存溢出,页面会奔溃的本质原因了 ![](https://static.vue-js.com/968c8510-3722-11ec-a752-75723a64e8f5.png) 开发者在后台发布新版本之后,无法立刻影响到所有现网用户,但最差情况下,也在发布之后 24 小时之内下发新版本信息到用户 每次冷启动时,都会检查是否有更新版本,如果发现有新版本,将会异步下载新版本的代码包,并同时用客户端本地的包进行启动,即新版本的小程序需要等下一次冷启动才会应用上 ## 参考文献 - https://developers.weixin.qq.com/community/develop/article/doc/0008a4c4f28f30fe3eb863b2750813 - https://juejin.cn/post/6976805521407868958#heading-5 - https://juejin.cn/post/6844903805675388942 - https://juejin.cn/post/6844903999863259144#heading-1 ================================================ FILE: docs/applet/applet.md ================================================ # 面试官:说说你对微信小程序的理解?优缺点? ![](https://static.vue-js.com/be367c80-300e-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 2017年,微信正式推出了小程序,允许外部开发者在微信内部运行自己的代码,开展业务 截至目前,小程序已经成为国内前端的一个重要业务,跟 `Web` 和手机 `App` 有着同等的重要性 ![](https://static.vue-js.com/ce751de0-300e-11ec-8e64-91fdec0f05a1.png) 小程序是一种不需要下载安装即可使用的应用,它实现了应用“触手可及”的梦想,用户扫一扫或者搜一下即可打开应用 也体现了“用完即走”的理念,用户不用关心是否安装太多应用的问题。应用将无处不在,随时可用,但又无需安装卸载 注意的是,除了微信小程序,还有百度小程序、微信小程序、支付宝小程序、抖音小程序,都是每个平台自己开发的,都是有针对性平台的应用程序 ## 二、背景 ⼩程序并⾮凭空冒出来的⼀个概念,当微信中的 `WebView` 逐渐成为移动 `Web`的⼀个重要⼊⼝时,微信就有相关的 `JS-SDK` `JS-SDK` 解决了移动⽹⻚能⼒不⾜的问题,通过暴露微信的接⼝使得 `Web` 开发者能够拥有更多的能⼒,然⽽在更多的能⼒之外,`JS-SDK`的模式并没有解决使⽤移动⽹⻚遇到的体验不良的问题 因此需要设计⼀个⽐较好的系统,使得所有开发者在微信中都能获得⽐较好的体验: - 快速的加载 - 更强⼤的能⼒ - 原⽣的体验 - 易⽤且安全的微信数据开放 - ⾼效和简单的开发 这些是`JS-SDK`做不到的,需要设计一个全新的小程序系统 对于小程序的开发,提供一个简单、高效的应用开发框架和丰富的组件及`API`,帮助开发者开发出具有原生体验的服务 其中相比`H5`,小程序与其的区别有如下: - 运⾏环境:⼩程序基于浏览器内核重构的内置解析器 - 系统权限:⼩程序能获得更多的系统权限,如⽹络通信状态、数据缓存能⼒等 - 渲染机制:⼩程序的逻辑层和渲染层是分开的 小程序可以视为只能用微信打开和浏览的`H5`,小程序和网页的技术模型是一样的,用到的 `JavaScript` 语言和 `CSS` 样式也是一样的,只是网页的 `HTML` 标签被稍微修改成了 `WXML` 标签 因此可以说,小程序页面本质上就是网页 其中关于微信小程序的实现原理,我们在后面的文章讲到 ## 三、优缺点 优点: - 随搜随用,用完即走:使得小程序可以代替许多APP,或是做APP的整体嫁接,或是作为阉割版功能的承载体 - 流量大,易接受:小程序借助自身平台更加容易引入更多的流量 - 安全 - 开发门槛低 - 降低兼容性限制 缺点: - 用户留存:及相关数据显示,小程序的平均次日留存在13%左右,但是双周留存骤降到仅有1% - 体积限制:微信小程序只有2M的大小,这样导致无法开发大型一些的小程序 - 受控微信:比起APP,尤其是安卓版的高自由度,小程序要面对很多来自微信的限制,从功能接口,甚至到类别内容,都要接受微信的管控 ## 参考文献 - https://developers.weixin.qq.com/miniprogram/dev/framework/ - https://www.zhihu.com/question/263816362 ================================================ FILE: docs/applet/lifecycle.md ================================================ # 面试官:说说微信小程序的生命周期函数有哪些? ![](https://static.vue-js.com/1df64890-30e0-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 跟`vue`、`react`框架一样,微信小程序框架也存在生命周期,实质也是一堆会在特定时期执行的函数 小程序中,生命周期主要分成了三部分: - 应用的生命周期 - 页面的生命周期 - 组件的生命周期 ### 应用的生命周期 小程序的生命周期函数是在`app.js`里面调用的,通过`App(Object)`函数用来注册一个小程序,指定其小程序的生命周期回调 ### 页面的生命周期 页面生命周期函数就是当你每进入/切换到一个新的页面的时候,就会调用的生命周期函数,同样通过`App(Object)`函数用来注册一个页面 ### 组件的生命周期 组件的生命周期,指的是组件自身的一些函数,这些函数在特殊的时间点或遇到一些特殊的框架事件时被自动触发,通过`Component(Object)`进行注册组件 ## 二、有哪些 ### 应用的生命周期 | 生命周期 | 说明 | | ---------------------- | --------------------------------------- | | onLaunch | 小程序初始化完成时触发,全局只触发一次 | | onShow | 小程序启动,或从后台进入前台显示时触发 | | onHide | 小程序从前台进入后台时触发 | | onError | 小程序发生脚本错误或 API 调用报错时触发 | | onPageNotFound | 小程序要打开的页面不存在时触发 | | onUnhandledRejection() | 小程序有未处理的 Promise 拒绝时触发 | | onThemeChange | 系统切换主题时触发 | ### 页面的生命周期 | 生命周期 | 说明 | 作用 | | -------- | --------------------------------- | ------------------------------ | | onLoad | 生命周期回调—监听页面加载 | 发送请求获取数据 | | onShow | 生命周期回调—监听页面显示 | 请求数据 | | onReady | 生命周期回调—监听页面初次渲染完成 | 获取页面元素(少用) | | onHide | 生命周期回调—监听页面隐藏 | 终止任务,如定时器或者播放音乐 | | onUnload | 生命周期回调—监听页面卸载 | 终止任务 | ### 组件的生命周期 | 生命周期 | 说明 | | -------- | --------------------------------- | | created | 生命周期回调—监听页面加载 | | attached | 生命周期回调—监听页面显示 | | ready | 生命周期回调—监听页面初次渲染完成 | | moved | 生命周期回调—监听页面隐藏 | | detached | 生命周期回调—监听页面卸载 | | error | 每当组件方法抛出错误时执行 | 注意的是: - 组件实例刚刚被创建好时, created 生命周期被触发,此时,组件数据 this.data 就是在 Component 构造器中定义的数据 data , 此时不能调用 setData - 在组件完全初始化完毕、进入页面节点树后, attached 生命周期被触发。此时, this.data 已被初始化为组件的当前值。这个生命周期很有用,绝大多数初始化工作可以在这个时机进行 - 在组件离开页面节点树后, detached 生命周期被触发。退出一个页面时,如果组件还在页面节点树中,则 detached 会被触发 还有一些特殊的生命周期,它们并非与组件有很强的关联,但有时组件需要获知,以便组件内部处理,这样的生命周期称为“组件所在页面的生命周期”,在 `pageLifetimes` 定义段中定义,如下: | 生命周期 | 说明 | | -------- | -------------------------- | | show | 组件所在的页面被展示时执行 | | hide | 组件所在的页面被隐藏时执行 | 代码如下: ```js Component({ pageLifetimes: { show: function() { // 页面被展示 }, hide: function() { // 页面被隐藏 }, } }) ``` ## 三、执行过程 ### 应⽤的⽣命周期执行过程: - ⽤户⾸次打开⼩程序,触发 onLaunch(全局只触发⼀次) - ⼩程序初始化完成后,触发onShow⽅法,监听⼩程序显示 - ⼩程序从前台进⼊后台,触发 onHide⽅法 - ⼩程序从后台进⼊前台显示,触发 onShow⽅法 - ⼩程序后台运⾏⼀定时间,或系统资源占⽤过⾼,会被销毁 ### ⻚⾯⽣命周期的执行过程: - ⼩程序注册完成后,加载⻚⾯,触发onLoad⽅法 - ⻚⾯载⼊后触发onShow⽅法,显示⻚⾯ - ⾸次显示⻚⾯,会触发onReady⽅法,渲染⻚⾯元素和样式,⼀个⻚⾯只会调⽤⼀次 - 当⼩程序后台运⾏或跳转到其他⻚⾯时,触发onHide⽅法 - 当⼩程序有后台进⼊到前台运⾏或重新进⼊⻚⾯时,触发onShow⽅法 - 当使⽤重定向⽅法 wx.redirectTo() 或关闭当前⻚返回上⼀⻚wx.navigateBack(),触发onUnload 当存在也应用生命周期和页面周期的时候,相关的执行顺序如下: - 打开小程序:(App)onLaunch --> (App)onShow --> (Pages)onLoad --> (Pages)onShow --> (pages)onRead - 进入下一个页面:(Pages)onHide --> (Next)onLoad --> (Next)onShow --> (Next)onReady - 返回上一个页面:(curr)onUnload --> (pre)onShow - 离开小程序:(App)onHide - 再次进入:小程序未销毁 --> (App)onShow(执行上面的顺序),小程序被销毁,(App)onLaunch重新开始执行. ## 参考文献 - https://developers.weixin.qq.com/miniprogram/dev/reference/api/App.html#onLaunch-Object-object - https://developers.weixin.qq.com/miniprogram/dev/reference/api/Page.html#onLoad-Object-query - https://developers.weixin.qq.com/miniprogram/dev/reference/api/App.html#onLaunch-Object-object ================================================ FILE: docs/applet/login.md ================================================ # 面试官:说说微信小程序的登录流程? ![](https://static.vue-js.com/aa3ccbd0-3428-11ec-8e64-91fdec0f05a1.png) ## 一、背景 传统的`web`开发实现登陆功能,一般的做法是输入账号密码、或者输入手机号及短信验证码进行登录 服务端校验用户信息通过之后,下发一个代表登录态的 `token` 给客户端,以便进行后续的交互,每当`token`过期,用户都需要重新登录 而在微信小程序中,可以通过微信官方提供的登录能力方便地获取微信提供的用户身份标识,快速建立小程序内的用户体系,从而实现登陆功能 实现小程序用户体系主要涉及到`openid`和`code`的概念: - 调用`wx.login()`方法会生成`code`,将`code`作为参数传递给微信服务器指定接口,就可以获取用户的`openid` 对于每个小程序,微信都会将用户的微信`ID`映射出一个小程序 `openid`,作为这个用户在这个小程序的唯一标识 ## 二、流程 微信小程序登陆具体实现的逻辑如下图所示: ![](https://static.vue-js.com/b60638c0-3428-11ec-a752-75723a64e8f5.png) - 通过 wx.login() 获取到用户的code判断用户是否授权读取用户信息,调用wx.getUserInfo 读取用户数据 - 由于小程序后台授权域名无法授权微信的域名,所以需要自身后端调用微信服务器获取用户信息 - 通过 wx.request() 方法请求业务方服务器,后端把 appid , appsecret 和 code 一起发送到微信服务器。 appid 和 appsecret 都是微信提供的,可以在管理员后台找到 - 微信服务器返回了 openid 及本次登录的会话密钥 session_key - 后端从数据库中查找 openid ,如果没有查到记录,说明该用户没有注册,如果有记录,则继续往下走 - session_key 是对用户数据进行加密签名的密钥。为了自身应用安全,session_key 不应该在网络上传输 - 然后生成 session并返回给小程序 - 小程序把 session 存到 storage 里面 - 下次请求时,先从 storage 里面读取,然后带给服务端 - 服务端对比 session 对应的记录,然后校验有效期 更加详细的功能图如下所示: ![](https://static.vue-js.com/c3cfbb70-3428-11ec-8e64-91fdec0f05a1.png) ## 三、扩展 实际业务中,我们还需要登录态是否过期,通常的做法是在登录态(临时令牌)中保存有效期数据,该有效期数据应该在服务端校验登录态时和约定的时间(如服务端本地的系统时间或时间服务器上的标准时间)做对比 这种方法需要将本地存储的登录态发送到小程序的服务端,服务端判断为无效登录态时再返回需重新执行登录过程的消息给小程 另一种方式可以通过调用`wx.checkSession`检查微信登陆态是否过期: - 如果过期,则发起完整的登录流程 - 如果不过期,则继续使用本地保存的自定义登录态 这种方式的好处是不需要小程序服务端来参与校验,而是在小程序端调用AP,流程如下所示: ![](https://static.vue-js.com/8b446d30-349d-11ec-a752-75723a64e8f5.png) ## 参考文献 - https://segmentfault.com/a/1190000016750340 - https://juejin.cn/post/6955754095860776973 - https://www.cnblogs.com/zwh0910/p/13977278.html ================================================ FILE: docs/applet/navigate.md ================================================ # 面试官:说说微信小程序中路由跳转的方式有哪些?区别? ![](https://static.vue-js.com/52bd3820-31a5-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 微信小程序拥有`web`网页和`Application`共同的特征,我们的页面都不是孤立存在的,而是通过和其他页面进行交互,来共同完成系统的功能 在微信小程序中,每个页面可以看成是一个` pageModel`,`pageModel `全部以栈的形式进行管理 ## 二、有哪些 常见的微信小程序页面跳转方式有如下: - wx.navigateTo(Object) - wx.redirectTo(Object) - wx.switchTab(Object) - wx.navigateBack(Object) - wx.reLaunch(Object) ### wx.navigateTo(Object) `wx.navigateTo()`用于保留当前页面、跳转到应用内的某个页面,使用 `wx.navigateBack`可以返回到原页面 对于页面不是特别多的小程序,通常推荐使用 `wx.navigateTo`进行跳转, 以便返回原页面,以提高加载速度。当页面特别多时,则不推荐使用 参数表如下所示: ![](https://static.vue-js.com/5e524ea0-31a5-11ec-8e64-91fdec0f05a1.png) 流程图如下: ![](https://static.vue-js.com/68f033e0-31a5-11ec-8e64-91fdec0f05a1.png) ### wx.redirectTo(Object) 重定向,当页面过多时,被保留页面会挤占微信分配给小程序的内存,或是达到微信所限制的 10 层页面栈的情况下,我们应该考虑选择 `wx.redirectTo` `wx.redirectTo()`用于关闭当前页面,跳转到应用内的某个页面 这样的跳转,可以避免跳转前页面占据运行内存,但返回时页面需要重新加载,增加了返回页面的显示时间 参数表如下所示: ![](https://static.vue-js.com/76066c20-31a5-11ec-8e64-91fdec0f05a1.png) 流程图如下所示: ![](https://static.vue-js.com/828c4b40-31a5-11ec-a752-75723a64e8f5.png) ### wx.switchTab(Object) 跳转到 `tabBar `页面,并关闭其他所有非 `tabBar` 页面 参数表如下所示: ![](https://static.vue-js.com/968869d0-31a5-11ec-a752-75723a64e8f5.png) ### wx.navigateBack(Object) `wx.navigateBack()` 用于关闭当前页面,并返回上一页面或多级页面,开发者可通过 `getCurrentPages()` 获取当前的页面栈,决定需要返回几层则设置对象的`delta`属性即可 参数表如下: ![](https://static.vue-js.com/a28d8030-31a5-11ec-a752-75723a64e8f5.png) ### wx.reLaunch(Object) 关闭所有页面,打开到应用内的某个页面,返回的时候跳到首页 流程图如下所示: ![](https://static.vue-js.com/accca3a0-31a5-11ec-8e64-91fdec0f05a1.png) 参数表如下所示: ![](https://static.vue-js.com/b98c7e80-31a5-11ec-8e64-91fdec0f05a1.png) ## 三、总结 关于上述五种跳转方式,做下总结: - navigateTo 保留当前页面,跳转到应用内的某个页面,使用 wx.navigateBack 可以返回到原页 - redirectTo 关闭当前页面,跳转到应用内的某个页面 - switchTab 跳转到 tabBar 页面,同时关闭其他非 tabBar 页面 - navigateBack 返回上一页面 - reLanch 关闭所有页面,打开到应用内的某个页面 其中关于它们的页面栈的关系如下: - avigateTo 新页面入栈 - redirectTo 当前页面出栈,新页面入栈 - navigateBack 页面不断出栈,直到目标返回页,新页面入栈 - switchTab 页面全部出栈,只留下新的 Tab 页面 - reLanch 页面全部出栈,只留下新的页面 ## 参考文献 - https://developers.weixin.qq.com/miniprogram/dev/api/route/wx.navigateBack.html ================================================ FILE: docs/applet/optimization.md ================================================ # 面试官:说说提高微信小程序的应用速度的手段有哪些? ![](https://static.vue-js.com/f606d530-3278-11ec-a752-75723a64e8f5.png) ## 一、是什么 小程序启动会常常遇到如下图场景: ![](https://static.vue-js.com/03941230-3279-11ec-8e64-91fdec0f05a1.png) 这是因为,小程序首次启动前,微信会在小程序启动前为小程序准备好通用的运行环境,如运行中的线程和一些基础库的初始化 然后才开始进入启动状态,展示一个固定的启动界面,界面内包含小程序的图标、名称和加载提示图标。此时,微信会在背后完成几项工作: - 下载小程序代码包 - 加载小程序代码包 - 初始化小程序首页 下载到的小程序代码包不是小程序的源代码,而是编译、压缩、打包之后的代码包 整体流程如下图: ![](https://static.vue-js.com/11c0ea90-3279-11ec-a752-75723a64e8f5.png) ## 二、手段 围绕上图小程序的启动流程, 我们可以从加载、渲染两个纬度进行切入: ### 加载 提升体验最直接的方法是控制小程序包的大小,常见手段有如下: - 代码包的体积压缩可以通过勾选开发者工具中“上传代码时,压缩代码”选项 - 及时清理无用的代码和资源文件 - 减少资源包中的图片等资源的数量和大小(理论上除了小icon,其他图片资源从网络下载),图片资源压缩率有限 并且可以采取分包加载的操作,将用户访问率高的页面放在主包里,将访问率低的页面放入子包里,按需加载 当用户点击到子包的目录时,还是有一个代码包下载的过程,这会感觉到明显的卡顿,所以子包也不建议拆的太大,当然我们可以采用子包预加载技术,并不需要等到用户点击到子包页面后在下载子包 ![](https://static.vue-js.com/2034de10-3279-11ec-8e64-91fdec0f05a1.png) ### 渲染 关于微信小程序首屏渲染优化的手段如下: - 请求可以在页面onLoad就加载,不需要等页面ready后在异步请求数据 - 尽量减少不必要的https请求,可使用 getStorageSync() 及 setStorageSync() 方法将数据存储在本地 - 可以在前置页面将一些有用的字段带到当前页,进行首次渲染(列表页的某些数据--> 详情页),没有数据的模块可以进行骨架屏的占位 在微信小程序中,提高页面的多次渲染效率主要在于正确使用`setData`: - 不要过于频繁调用setData,应考虑将多次setData合并成一次setData调用 - 数据通信的性能与数据量正相关,因而如果有一些数据字段不在界面中展示且数据结构比较复杂或包含长字符串,则不应使用`setData`来设置这些数据 - 与界面渲染无关的数据最好不要设置在data中,可以考虑设置在page对象的其他字段下 除此之外,对于一些独立的模块我们尽可能抽离出来,这是因为自定义组件的更新并不会影响页面上其他元素的更新 各个组件也将具有各自独立的逻辑空间。每个组件都分别拥有自己的独立的数据、`setData`调用 ## 三、总结 **小程序启动加载性能**: - 控制代码包的大小 - 分包加载 - 首屏体验(预请求,利用缓存,避免白屏,及时反馈 **小程序渲染性能**: - 避免不当的使用setData - 使用自定义组件 ### 参考文献 - https://juejin.cn/post/6969779451177484296 - https://segmentfault.com/a/1190000008925450 - https://juejin.cn/post/6844903638226173965 - https://juejin.cn/post/6844903726939897869 ================================================ FILE: docs/applet/publish.md ================================================ # 面试官:说说微信小程序的发布流程? ![](https://static.vue-js.com/d5cccdf0-3652-11ec-8e64-91fdec0f05a1.png) ## 一、背景 在中大型的公司里,人员的分工非常仔细,一般会有不同岗位角色的员工同时参与同一个小程序项目。为此,小程序平台设计了不同的权限管理使得项目管理者可以更加高效管理整个团队的协同工作 ![](https://static.vue-js.com/e76aff50-3652-11ec-8e64-91fdec0f05a1.png) 以往我们在开发完网页之后,需要把网页的代码和资源放在服务器上,让用户通过互联网来访问 在小程序的平台里,开发者完成开发之后,需要在开发者工具提交小程序的代码包,然后在小程序后台发布小程序 ![](https://static.vue-js.com/fe5da190-3652-11ec-8e64-91fdec0f05a1.png) ## 二、流程 关于发布的流程,主要分成了三个部分: - 上传代码 - 提交审核 - 发布版本 ### 上传代码 在开发者工具中,可以点击代码上传功能: ![](https://static.vue-js.com/08f19bc0-3653-11ec-a752-75723a64e8f5.png) 然后就可以填写版本信息: ![](https://static.vue-js.com/1d02c8f0-3653-11ec-a752-75723a64e8f5.png) 然后点击上传,编译器则会提示上传代码成功 ### 提交审核 代码上传完毕,就可以登陆微信公众号的官网首页,点击【开发管理】,查看应用详情: ![](https://static.vue-js.com/281038e0-3653-11ec-8e64-91fdec0f05a1.png) 提交审核过程需要填写审核信息,如下图: ![](https://static.vue-js.com/33d97ec0-3653-11ec-a752-75723a64e8f5.png) 提交审核成功之后如下图: ![](https://static.vue-js.com/3e4c3550-3653-11ec-a752-75723a64e8f5.png) ### 发布版本 当审核通过之后,即可提交发布 ![](https://static.vue-js.com/495140d0-3653-11ec-8e64-91fdec0f05a1.png) 发布成功之后则如下: ![](https://static.vue-js.com/5293b4c0-3653-11ec-8e64-91fdec0f05a1.png) ## 三、扩展 上述是最简单的小程序代码发布的流程,通常的流程如下: - 代码管理服务器上新建分支 - 开发测试新需求 - 测试完成后,将本地分支合并到 master 分支 - 拉取 master 分支最新代码,执行 build 命令生成小程序可执行文件 - 开发者工具点击“上传” - 提审 - 发布 但是面对多人协调开发的时候,有可能出现已经上线的代码还没合并到`master`的情况 因此可以考虑自动化构建部署,就是将从开发到部署的一系列流程变成自动化,衔接连贯,在构建失败时能够告知开发者,构建成功后能够告知测试和实施人员,可参考如下流程图: ![](https://static.vue-js.com/602d9bf0-3653-11ec-a752-75723a64e8f5.png) ## 参考文献 - https://juejin.cn/post/6994414162700927012 - https://www.leapcloud.cn/website/docs/doc_config/xiaochengxu/xiaochengxu.html ================================================ FILE: docs/applet/requestPayment.md ================================================ # 面试官:说说微信小程序的支付流程? ![](https://static.vue-js.com/2266fff0-34a0-11ec-8e64-91fdec0f05a1.png) ## 一、前言 微信小程序为电商类小程序,提供了非常完善、优秀、安全的支付功能 在小程序内可调用微信的`API`完成支付功能,方便、快捷 场景如下图所示: ![](https://static.vue-js.com/6e0cff40-34a0-11ec-a752-75723a64e8f5.png) ![](https://static.vue-js.com/34864830-34a0-11ec-8e64-91fdec0f05a1.png) - 用户通过分享或扫描二维码进入商户小程序,用户选择购买,完成选购流程 - 调起微信支付控件,用户开始输入支付密码 - 密码验证通过,支付成功。商户后台得到支付成功的通知 - 返回商户小程序,显示购买成功 - 微信支付公众号下发支付凭证 ## 二、流程 以电商小程序为例 支付流程图如下所示: ![](https://static.vue-js.com/76b66780-34a0-11ec-8e64-91fdec0f05a1.png) 具体的做法: - 打开某小程序,点击直接下单 - wx.login获取用户临时登录凭证code,发送到后端服务器换取openId - 在下单时,小程序需要将购买的商品Id,商品数量,以及用户的openId传送到服务器 - 服务器在接收到商品Id、商品数量、openId后,生成服务期订单数据,同时经过一定的签名算法,向微信支付发送请求,获取预付单信息(prepay_id),同时将获取的数据再次进行相应规则的签名,向小程序端响应必要的信息 - 小程序端在获取对应的参数后,调用wx.requestPayment()发起微信支付,唤醒支付工作台,进行支付 - 接下来的一些列操作都是由用户来操作的包括了微信支付密码,指纹等验证,确认支付之后执行鉴权调起支付 - 鉴权调起支付:在微信后台进行鉴权,微信后台直接返回给前端支付的结果,前端收到返回数据后对支付结果进行展示 - 推送支付结果:微信后台在给前端返回支付的结果后,也会向后台也返回一个支付结果,后台通过这个支付结果来更新订单的状态 其中后端响应数据必要的信息则是`wx.requestPayment`方法所需要的参数,大致如下: ```JS wx.requestPayment({ // 时间戳 timeStamp: '', // 随机字符串 nonceStr: '', // 统一下单接口返回的 prepay_id 参数值 package: '', // 签名类型 signType: '', // 签名 paySign: '', // 调用成功回调 success () {}, // 失败回调 fail () {}, // 接口调用结束回调 complete () {} }) ``` 参数表如下所示: ![](https://files.mdnice.com/user/155/48efed1f-d67f-45a7-ab2c-89a6424fafa0.png) ## 三、结束 小程序支付和以往的网页、APP微信支付大同小异,可以说小程序的支付变得更加简洁,不需要设置支付目录、域名授权等操作 ## 参考文献 - https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_8_0.shtml - https://juejin.cn/post/6844903895970349064 ================================================ FILE: docs/css/BFC.md ================================================ # 面试官:谈谈你对BFC的理解? ![](https://static.vue-js.com/c3d68290-9511-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 我们在页面布局的时候,经常出现以下情况: - 这个元素高度怎么没了? - 这两栏布局怎么没法自适应? - 这两个元素的间距怎么有点奇怪的样子? - ...... 原因是元素之间相互的影响,导致了意料之外的情况,这里就涉及到`BFC`概念 `BFC`(Block Formatting Context),即块级格式化上下文,它是页面中的一块渲染区域,并且有一套属于自己的渲染规则: - 内部的盒子会在垂直方向上一个接一个的放置 - 对于同一个BFC的俩个相邻的盒子的margin会发生重叠,与方向无关。 - 每个元素的左外边距与包含块的左边界相接触(从左到右),即使浮动元素也是如此 - BFC的区域不会与float的元素区域重叠 - 计算BFC的高度时,浮动子元素也参与计算 - BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然 `BFC`目的是形成一个相对于外界完全独立的空间,让内部的子元素不会影响到外部的元素 ## 二、触发条件 触发`BFC`的条件包含不限于: - 根元素,即HTML元素 - 浮动元素:float值为left、right - overflow值不为 visible,为 auto、scroll、hidden - display的值为inline-block、inltable-cell、table-caption、table、inline-table、flex、inline-flex、grid、inline-grid - position的值为absolute或fixed ## 三、应用场景 利用`BFC`的特性,我们将`BFC`应用在以下场景: #### 防止margin重叠(塌陷) ```html

    Haha

    Hehe

    ``` 页面显示如下: ![](https://static.vue-js.com/d0ce3650-9511-11eb-85f6-6fac77c0c9b3.png) 两个`p`元素之间的距离为`100px`,发生了`margin`重叠(塌陷),以最大的为准,如果第一个P的`margin`为80的话,两个P之间的距离还是100,以最大的为准。 前面讲到,同一个`BFC`的俩个相邻的盒子的`margin`会发生重叠 可以在`p`外面包裹一层容器,并触发这个容器生成一个`BFC`,那么两个`p`就不属于同一个`BFC`,则不会出现`margin`重叠 ```html

    Haha

    Hehe

    ``` 这时候,边距则不会重叠: ![](https://static.vue-js.com/dec44740-9511-11eb-85f6-6fac77c0c9b3.png) #### 清除内部浮动 ```html
    ``` 页面显示如下: ![](https://static.vue-js.com/ec5d4410-9511-11eb-85f6-6fac77c0c9b3.png) 而`BFC`在计算高度时,浮动元素也会参与,所以我们可以触发`.par`元素生成`BFC`,则内部浮动元素计算高度时候也会计算 ```css .par { overflow: hidden; } ``` 实现效果如下: ![](https://static.vue-js.com/f6487b20-9511-11eb-ab90-d9ae814b240d.png) #### 自适应多栏布局 这里举个两栏的布局 ```html
    ``` 效果图如下: ![](https://static.vue-js.com/ffb95210-9511-11eb-ab90-d9ae814b240d.png) 前面讲到,每个元素的左外边距与包含块的左边界相接触 因此,虽然`.aslide`为浮动元素,但是`main`的左边依然会与包含块的左边相接触 而`BFC`的区域不会与浮动盒子重叠 所以我们可以通过触发`main`生成`BFC`,以此适应两栏布局 ```css .main { overflow: hidden; } ``` 这时候,新的`BFC`不会与浮动的`.aside`元素重叠。因此会根据包含块的宽度,和`.aside`的宽度,自动变窄 效果如下: ![](https://static.vue-js.com/0a5f2690-9512-11eb-ab90-d9ae814b240d.png) ### 小结 可以看到上面几个案例,都体现了`BFC`实际就是页面一个独立的容器,里面的子元素不影响外面的元素 ## 参考文献 - https://developer.mozilla.org/zh-CN/docs/Web/Guide/CSS/Block_formatting_context - https://github.com/zuopf769/notebook/blob/master/fe/BFC%E5%8E%9F%E7%90%86%E5%89%96%E6%9E%90/README.md ================================================ FILE: docs/css/animation.md ================================================ # 面试官:css3动画有哪些? ![](https://static.vue-js.com/d12e2380-9c0a-11eb-ab90-d9ae814b240d.png) ## 一、是什么 CSS动画(CSS Animations)是为层叠样式表建议的允许可扩展标记语言(XML)元素使用CSS的动画的模块 即指元素从一种样式逐渐过渡为另一种样式的过程 常见的动画效果有很多,如平移、旋转、缩放等等,复杂动画则是多个简单动画的组合 `css`实现动画的方式,有如下几种: - transition 实现渐变动画 - transform 转变动画 - animation 实现自定义动画 ## 二、实现方式 ### transition 实现渐变动画 `transition`的属性如下: - property:填写需要变化的css属性 - duration:完成过渡效果需要的时间单位(s或者ms) - timing-function:完成效果的速度曲线 - delay: 动画效果的延迟触发时间 其中`timing-function`的值有如下: | 值 | 描述 | | ----------------------------- | ------------------------------------------------------------ | | linear | 匀速(等于 cubic-bezier(0,0,1,1)) | | ease | 从慢到快再到慢(cubic-bezier(0.25,0.1,0.25,1)) | | ease-in | 慢慢变快(等于 cubic-bezier(0.42,0,1,1)) | | ease-out | 慢慢变慢(等于 cubic-bezier(0,0,0.58,1)) | | ease-in-out | 先变快再到慢(等于 cubic-bezier(0.42,0,0.58,1)),渐显渐隐效果 | | cubic-bezier(*n*,*n*,*n*,*n*) | 在 cubic-bezier 函数中定义自己的值。可能的值是 0 至 1 之间的数值 | 注意:并不是所有的属性都能使用过渡的,如`display:none<->display:block` 举个例子,实现鼠标移动上去发生变化动画效果 ```html
    ``` ### transform 转变动画 包含四个常用的功能: - translate:位移 - scale:缩放 - rotate:旋转 - skew:倾斜 一般配合`transition`过度使用 注意的是,`transform`不支持`inline`元素,使用前把它变成`block` 举个例子 ```html
    ``` 可以看到盒子发生了旋转,倾斜,平移,放大 ### animation 实现自定义动画 `animation`是由 8 个属性的简写,分别如下: | 属性 | 描述 | 属性值 | | -------------------------------------- | ------------------------------------------------------------ | --------------------------------------------- | | animation-duration | 指定动画完成一个周期所需要时间,单位秒(s)或毫秒(ms),默认是 0 | | | animation-timing-function | 指定动画计时函数,即动画的速度曲线,默认是 "ease" | linear、ease、ease-in、ease-out、ease-in-out | | animation-delay | 指定动画延迟时间,即动画何时开始,默认是 0 | | | animation-iteration-count | 指定动画播放的次数,默认是 1 | | | animation-direction 指定动画播放的方向 | 默认是 normal | normal、reverse、alternate、alternate-reverse | | animation-fill-mode | 指定动画填充模式。默认是 none | forwards、backwards、both | | animation-play-state | 指定动画播放状态,正在运行或暂停。默认是 running | running、pauser | | animation-name | 指定 @keyframes 动画的名称 | | `CSS` 动画只需要定义一些关键的帧,而其余的帧,浏览器会根据计时函数插值计算出来, 通过 `@keyframes` 来定义关键帧 因此,如果我们想要让元素旋转一圈,只需要定义开始和结束两帧即可: ```css @keyframes rotate{ from{ transform: rotate(0deg); } to{ transform: rotate(360deg); } } ``` `from` 表示最开始的那一帧,`to` 表示结束时的那一帧 也可以使用百分比刻画生命周期 ```css @keyframes rotate{ 0%{ transform: rotate(0deg); } 50%{ transform: rotate(180deg); } 100%{ transform: rotate(360deg); } } ``` 定义好了关键帧后,下来就可以直接用它了: ```css animation: rotate 2s; ``` ## 三、总结 | 属性 | 含义 | | ------------------ | ------------------------------------------------------------ | | transition(过度) | 用于设置元素的样式过度,和animation有着类似的效果,但细节上有很大的不同 | | transform(变形) | 用于元素进行旋转、缩放、移动或倾斜,和设置样式的动画并没有什么关系,就相当于color一样用来设置元素的“外表” | | translate(移动) | 只是transform的一个属性值,即移动 | | animation(动画) | 用于设置动画属性,他是一个简写的属性,包含6个属性 | ## 参考文献 - https://segmentfault.com/a/1190000022540857 - https://zh.m.wikipedia.org/wiki/CSS%E5%8A%A8%E7%94%BB - https://vue3js.cn/interview ================================================ FILE: docs/css/box.md ================================================ # 面试官:说说你对盒子模型的理解? ![](https://static.vue-js.com/8d0e9ca0-8f9b-11eb-ab90-d9ae814b240d.png) ## 一、是什么 当对一个文档进行布局(layout)的时候,浏览器的渲染引擎会根据标准之一的 CSS 基础框盒模型(CSS basic box model),将所有元素表示为一个个矩形的盒子(box) 一个盒子由四个部分组成:`content`、`padding`、`border`、`margin` ![](https://static.vue-js.com/976789a0-8f9b-11eb-85f6-6fac77c0c9b3.png) `content`,即实际内容,显示文本和图像 `boreder`,即边框,围绕元素内容的内边距的一条或多条线,由粗细、样式、颜色三部分组成 `padding`,即内边距,清除内容周围的区域,内边距是透明的,取值不能为负,受盒子的`background`属性影响 `margin`,即外边距,在元素外创建额外的空白,空白通常指不能放其他元素的区域 上述是一个从二维的角度观察盒子,下面再看看看三维图: ![](https://static.vue-js.com/b2548b00-8f9b-11eb-ab90-d9ae814b240d.png) 下面来段代码: ```html
    盒子模型
    ``` 当我们在浏览器查看元素时,却发现元素的大小变成了`240px` 这是因为,在`CSS`中,盒子模型可以分成: - W3C 标准盒子模型 - IE 怪异盒子模型 默认情况下,盒子模型为`W3C` 标准盒子模型 ## 二、标准盒子模型 标准盒子模型,是浏览器默认的盒子模型 下面看看标准盒子模型的模型图: ![](https://static.vue-js.com/c0e1d2e0-8f9b-11eb-85f6-6fac77c0c9b3.png) 从上图可以看到: - 盒子总宽度 = width + padding + border + margin; - 盒子总高度 = height + padding + border + margin 也就是,`width/height` 只是内容高度,不包含 `padding` 和 `border `值 所以上面问题中,设置`width`为200px,但由于存在`padding`,但实际上盒子的宽度有240px ## 三、IE 怪异盒子模型 同样看看IE 怪异盒子模型的模型图: ![](https://static.vue-js.com/cfbb3ef0-8f9b-11eb-ab90-d9ae814b240d.png) 从上图可以看到: - 盒子总宽度 = width + margin; - 盒子总高度 = height + margin; 也就是,`width/height` 包含了 `padding `和 `border `值 ## Box-sizing CSS 中的 box-sizing 属性定义了引擎应该如何计算一个元素的总宽度和总高度 语法: ```css box-sizing: content-box|border-box|inherit: ``` - content-box 默认值,元素的 width/height 不包含padding,border,与标准盒子模型表现一致 - border-box 元素的 width/height 包含 padding,border,与怪异盒子模型表现一致 - inherit 指定 box-sizing 属性的值,应该从父元素继承 回到上面的例子里,设置盒子为 border-box 模型 ```html
    盒子模型
    ``` 这时候,就可以发现盒子的所占据的宽度为200px ## 参考文献 - https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Box_Model/Introduction_to_the_CSS_box_model - https://developer.mozilla.org/zh-CN/docs/Web/CSS/box-sizing ================================================ FILE: docs/css/center.md ================================================ # 面试官:元素水平垂直居中的方法有哪些?如果元素不定宽高呢? ![](https://static.vue-js.com/7b64c8d0-95f9-11eb-ab90-d9ae814b240d.png) ## 一、背景 在开发中经常遇到这个问题,即让某个元素的内容在水平和垂直方向上都居中,内容不仅限于文字,可能是图片或其他元素 居中是一个非常基础但又是非常重要的应用场景,实现居中的方法存在很多,可以将这些方法分成两个大类: - 居中元素(子元素)的宽高已知 - 居中元素宽高未知 ## 二、实现方式 实现元素水平垂直居中的方式: - 利用定位+margin:auto - 利用定位+margin:负值 - 利用定位+transform - table布局 - flex布局 - grid布局 ### 利用定位+margin:auto 先上代码: ```html
    ``` 父级设置为相对定位,子级绝对定位 ,并且四个定位属性的值都设置了0,那么这时候如果子级没有设置宽高,则会被拉开到和父级一样宽高 这里子元素设置了宽高,所以宽高会按照我们的设置来显示,但是实际上子级的虚拟占位已经撑满了整个父级,这时候再给它一个`margin:auto`它就可以上下左右都居中了 ### 利用定位+margin:负值 绝大多数情况下,设置父元素为相对定位, 子元素移动自身50%实现水平垂直居中 ```html
    ``` 整个实现思路如下图所示: ![](https://static.vue-js.com/922dc300-95f9-11eb-ab90-d9ae814b240d.png) - 初始位置为方块1的位置 - 当设置left、top为50%的时候,内部子元素为方块2的位置 - 设置margin为负数时,使内部子元素到方块3的位置,即中间位置 这种方案不要求父元素的高度,也就是即使父元素的高度变化了,仍然可以保持在父元素的垂直居中位置,水平方向上是一样的操作 但是该方案需要知道子元素自身的宽高,但是我们可以通过下面`transform`属性进行移动 ### 利用定位+transform 实现代码如下: ```css
    ``` `translate(-50%, -50%)`将会将元素位移自己宽度和高度的-50% 这种方法其实和最上面被否定掉的margin负值用法一样,可以说是`margin`负值的替代方案,并不需要知道自身元素的宽高 ### table布局 设置父元素为`display:table-cell`,子元素设置 `display: inline-block`。利用`vertical`和`text-align`可以让所有的行内块级元素水平垂直居中 ```html
    ``` ### flex弹性布局 还是看看实现的整体代码: ```html
    ``` `css3`中了`flex`布局,可以非常简单实现垂直水平居中 这里可以简单看看`flex`布局的关键属性作用: - display: flex时,表示该容器内部的元素将按照flex进行布局 - align-items: center表示这些元素将相对于本容器水平居中 - justify-content: center也是同样的道理垂直居中 ### grid网格布局 ```html
    ``` 这里看到,`gird`网格布局和`flex`弹性布局都简单粗暴 ### 小结 上述方法中,不知道元素宽高大小仍能实现水平垂直居中的方法有: - 利用定位+margin:auto - 利用定位+transform - flex布局 - grid布局 ## 三、总结 根据元素标签的性质,可以分为: - 内联元素居中布局 - 块级元素居中布局 ### 内联元素居中布局 水平居中 - 行内元素可设置:text-align: center - flex布局设置父元素:display: flex; justify-content: center 垂直居中 - 单行文本父元素确认高度:height === line-height - 多行文本父元素确认高度:display: table-cell; vertical-align: middle ### 块级元素居中布局 水平居中 - 定宽: margin: 0 auto - 绝对定位+left:50%+margin:负自身一半 垂直居中 - position: absolute设置left、top、margin-left、margin-top(定高) - display: table-cell - transform: translate(x, y) - flex(不定高,不定宽) - grid(不定高,不定宽),兼容性相对比较差 ## 参考文献 - https://juejin.cn/post/6844903982960214029#heading-10 ================================================ FILE: docs/css/column_layout.md ================================================ # 面试官:如何实现两栏布局,右侧自适应?三栏布局中间自适应呢? ![](https://static.vue-js.com/f335d400-976e-11eb-85f6-6fac77c0c9b3.png) ## 一、背景 在日常布局中,无论是两栏布局还是三栏布局,使用的频率都非常高 ### 两栏布局 两栏布局实现效果就是将页面分割成左右宽度不等的两列,宽度较小的列设置为固定宽度,剩余宽度由另一列撑满, 比如 `Ant Design` 文档,蓝色区域为主要内容布局容器,侧边栏为次要内容布局容器 > 这里称宽度较小的列父元素为次要布局容器,宽度较大的列父元素为主要布局容器 ![](https://static.vue-js.com/fcb8ac50-976e-11eb-85f6-6fac77c0c9b3.png) 这种布局适用于内容上具有明显主次关系的网页 ### 三栏布局 三栏布局按照左中右的顺序进行排列,通常中间列最宽,左右两列次之 大家最常见的就是`github`: ![](https://static.vue-js.com/0bf016e0-976f-11eb-ab90-d9ae814b240d.png) ## 二、两栏布局 两栏布局非常常见,往往是以一个定宽栏和一个自适应的栏并排展示存在 实现思路也非常的简单: - 使用 float 左浮左边栏 - 右边模块使用 margin-left 撑出内容块做内容展示 - 为父级元素添加BFC,防止下方元素飞到上方内容 代码如下: ```html
    左边
    右边
    ``` 还有一种更为简单的使用则是采取:flex弹性布局 ### flex弹性布局 ```html
    左边
    右边
    ``` `flex`可以说是最好的方案了,代码少,使用简单 注意的是,`flex`容器的一个默认属性值:`align-items: stretch;` 这个属性导致了列等高的效果。 为了让两个盒子高度自动,需要设置: `align-items: flex-start` ## 三、三栏布局 实现三栏布局中间自适应的布局方式有: - 两边使用 float,中间使用 margin - 两边使用 absolute,中间使用 margin - 两边使用 float 和负 margin - display: table 实现 - flex实现 - grid网格布局 ### 两边使用 float,中间使用 margin 需要将中间的内容放在`html`结构最后,否则右侧会臣在中间内容的下方 实现代码如下: ```html
    左侧
    右侧
    中间
    ``` 原理如下: - 两边固定宽度,中间宽度自适应。 - 利用中间元素的margin值控制两边的间距 - 宽度小于左右部分宽度之和时,右侧部分会被挤下去 这种实现方式存在缺陷: - 主体内容是最后加载的。 - 右边在主体内容之前,如果是响应式设计,不能简单的换行展示 ### 两边使用 absolute,中间使用 margin 基于绝对定位的三栏布局:注意绝对定位的元素脱离文档流,相对于最近的已经定位的祖先元素进行定位。无需考虑HTML中结构的顺序 ```html
    左边固定宽度
    右边固定宽度
    中间自适应
    ``` 实现流程: - 左右两边使用绝对定位,固定在两侧。 - 中间占满一行,但通过 margin和左右两边留出10px的间隔 ### 两边使用 float 和负 margin ```html
    中间自适应
    左边固定宽度
    右边固定宽度
    ``` 实现过程: - 中间使用了双层标签,外层是浮动的,以便左中右能在同一行展示 - 左边通过使用负 margin-left:-100%,相当于中间的宽度,所以向上偏移到左侧 - 右边通过使用负 margin-left:-100px,相当于自身宽度,所以向上偏移到最右侧 缺点: - 增加了 .main-wrapper 一层,结构变复杂 - 使用负 margin,调试也相对麻烦 ### 使用 display: table 实现 `` 标签用于展示行列数据,不适合用于布局。但是可以使用 `display: table` 来实现布局的效果 ```html
    左边固定宽度
    中间自适应
    右边固定宽度
    ``` 实现原理: - 层通过 display: table设置为表格,设置 table-layout: fixed`表示列宽自身宽度决定,而不是自动计算。 - 内层的左中右通过 display: table-cell设置为表格单元。 - 左右设置固定宽度,中间设置 width: 100% 填充剩下的宽度 ### 使用flex实现 利用`flex`弹性布局,可以简单实现中间自适应 代码如下: ```html
    左侧
    中间
    右侧
    ``` 实现过程: - 仅需将容器设置为`display:flex;`, - 盒内元素两端对其,将中间元素设置为`100%`宽度,或者设为`flex:1`,即可填充空白 - 盒内元素的高度撑开容器的高度 优点: - 结构简单直观 - 可以结合 flex的其他功能实现更多效果,例如使用 order属性调整显示顺序,让主体内容优先加载,但展示在中间 ### grid网格布局 代码如下: ```html
    左侧
    中间
    右侧
    ``` 跟`flex`弹性布局一样的简单 ## 参考文献 - https://zhuqingguang.github.io/2017/08/16/adapting-two-layout/ - https://segmentfault.com/a/1190000008705541 ================================================ FILE: docs/css/css3_features.md ================================================ # 面试官:CSS3新增了哪些新特性? ![](https://static.vue-js.com/d58f6df0-9b5e-11eb-ab90-d9ae814b240d.png) ## 一、是什么 `css`,即层叠样式表(Cascading Style Sheets)的简称,是一种标记语言,由浏览器解释执行用来使页面变得更美观 `css3`是`css`的最新标准,是向后兼容的,`CSS1/2 `的特性在` CSS3` 里都是可以使用的 而` CSS3` 也增加了很多新特性,为开发带来了更佳的开发体验 ## 二、选择器 `css3`中新增了一些选择器,主要为如下图所示: ![](https://static.vue-js.com/e368cf20-9b5e-11eb-85f6-6fac77c0c9b3.png) ## 三、新样式 ### 边框 `css3`新增了三个边框属性,分别是: - border-radius:创建圆角边框 - box-shadow:为元素添加阴影 - border-image:使用图片来绘制边框 #### box-shadow 设置元素阴影,设置属性如下: - 水平阴影 - 垂直阴影 - 模糊距离(虚实) - 阴影尺寸(影子大小) - 阴影颜色 - 内/外阴影 其中水平阴影和垂直阴影是必须设置的 ### 背景 新增了几个关于背景的属性,分别是`background-clip`、`background-origin`、`background-size`和`background-break` #### background-clip 用于确定背景画区,有以下几种可能的属性: - background-clip: border-box; 背景从border开始显示 - background-clip: padding-box; 背景从padding开始显示 - background-clip: content-box; 背景显content区域开始显示 - background-clip: no-clip; 默认属性,等同于border-box 通常情况,背景都是覆盖整个元素的,利用这个属性可以设定背景颜色或图片的覆盖范围 #### background-origin 当我们设置背景图片时,图片是会以左上角对齐,但是是以`border`的左上角对齐还是以`padding`的左上角或者`content`的左上角对齐? `border-origin`正是用来设置这个的 - background-origin: border-box; 从border开始计算background-position - background-origin: padding-box; 从padding开始计算background-position - background-origin: content-box; 从content开始计算background-position 默认情况是`padding-box`,即以`padding`的左上角为原点 #### background-size background-size属性常用来调整背景图片的大小,主要用于设定图片本身。有以下可能的属性: - background-size: contain; 缩小图片以适合元素(维持像素长宽比) - background-size: cover; 扩展元素以填补元素(维持像素长宽比) - background-size: 100px 100px; 缩小图片至指定的大小 - background-size: 50% 100%; 缩小图片至指定的大小,百分比是相对包 含元素的尺寸 ### background-break 元素可以被分成几个独立的盒子(如使内联元素span跨越多行),`background-break` 属性用来控制背景怎样在这些不同的盒子中显示 - background-break: continuous; 默认值。忽略盒之间的距离(也就是像元素没有分成多个盒子,依然是一个整体一样) - background-break: bounding-box; 把盒之间的距离计算在内; - background-break: each-box; 为每个盒子单独重绘背景 ### 文字 ### word-wrap 语法:`word-wrap: normal|break-word` - normal:使用浏览器默认的换行 - break-all:允许在单词内换行 ### text-overflow ` text-overflow`设置或检索当当前行超过指定容器的边界时如何显示,属性有两个值选择: - clip:修剪文本 - ellipsis:显示省略符号来代表被修剪的文本 ### text-shadow `text-shadow`可向文本应用阴影。能够规定水平阴影、垂直阴影、模糊距离,以及阴影的颜色 ### text-decoration CSS3里面开始支持对文字的更深层次的渲染,具体有三个属性可供设置: - text-fill-color: 设置文字内部填充颜色 - text-stroke-color: 设置文字边界填充颜色 - text-stroke-width: 设置文字边界宽度 ### 颜色 `css3`新增了新的颜色表示方式`rgba`与`hsla` - rgba分为两部分,rgb为颜色值,a为透明度 - hala分为四部分,h为色相,s为饱和度,l为亮度,a为透明度 ## 四、transition 过渡 `transition`属性可以被指定为一个或多个` CSS `属性的过渡效果,多个属性之间用逗号进行分隔,必须规定两项内容: - 过度效果 - 持续时间 语法如下: ```css transition: CSS属性,花费时间,效果曲线(默认ease),延迟时间(默认0) ``` 上面为简写模式,也可以分开写各个属性 ```css transition-property: width; transition-duration: 1s; transition-timing-function: linear; transition-delay: 2s; ``` ### 五、transform 转换 `transform`属性允许你旋转,缩放,倾斜或平移给定元素 `transform-origin`:转换元素的位置(围绕那个点进行转换),默认值为`(x,y,z):(50%,50%,0)` 使用方式: - transform: translate(120px, 50%):位移 - transform: scale(2, 0.5):缩放 - transform: rotate(0.5turn):旋转 - transform: skew(30deg, 20deg):倾斜 ### 六、animation 动画 动画这个平常用的也很多,主要是做一个预设的动画。和一些页面交互的动画效果,结果和过渡应该一样,让页面不会那么生硬 animation也有很多的属性 - animation-name:动画名称 - animation-duration:动画持续时间 - animation-timing-function:动画时间函数 - animation-delay:动画延迟时间 - animation-iteration-count:动画执行次数,可以设置为一个整数,也可以设置为infinite,意思是无限循环 - animation-direction:动画执行方向 - animation-paly-state:动画播放状态 - animation-fill-mode:动画填充模式 ## 七、渐变 颜色渐变是指在两个颜色之间平稳的过渡,`css3`渐变包括 - linear-gradient:线性渐变 > background-image: linear-gradient(direction, color-stop1, color-stop2, ...); - radial-gradient:径向渐变 > linear-gradient(0deg, red, green); ## 八、其他 关于`css3`其他的新特性还包括`flex`弹性布局、`Grid`栅格布局,这两个布局在以前就已经讲过,这里就不再展示 除此之外,还包括多列布局、媒体查询、混合模式等等...... ## 参考文献 - https://juejin.cn/post/6844903518520901639#heading-1 - https://www.w3school.com.cn/css/index.asp ================================================ FILE: docs/css/css_performance.md ================================================ # 面试官:如果要做优化,CSS提高性能的方法有哪些? ![](https://static.vue-js.com/c071c820-9fa3-11eb-ab90-d9ae814b240d.png) ## 一、前言 每一个网页都离不开`css`,但是很多人又认为,`css`主要是用来完成页面布局的,像一些细节或者优化,就不需要怎么考虑,实际上这种想法是不正确的 作为页面渲染和内容展现的重要环节,`css`影响着用户对整个网站的第一体验 因此,在整个产品研发过程中,`css`性能优化同样需要贯穿全程 ## 二、实现方式 实现方式有很多种,主要有如下: - 内联首屏关键CSS - 异步加载CSS - 资源压缩 - 合理使用选择器 - 减少使用昂贵的属性 - 不要使用@import ### 内联首屏关键CSS 在打开一个页面,页面首要内容出现在屏幕的时间影响着用户的体验,而通过内联`css`关键代码能够使浏览器在下载完`html`后就能立刻渲染 而如果外部引用`css`代码,在解析`html`结构过程中遇到外部`css`文件,才会开始下载`css`代码,再渲染 所以,`CSS`内联使用使渲染时间提前 注意:但是较大的`css`代码并不合适内联(初始拥塞窗口、没有缓存),而其余代码则采取外部引用方式 ### 异步加载CSS 在`CSS`文件请求、下载、解析完成之前,`CSS`会阻塞渲染,浏览器将不会渲染任何已处理的内容 前面加载内联代码后,后面的外部引用`css`则没必要阻塞浏览器渲染。这时候就可以采取异步加载的方案,主要有如下: - 使用javascript将link标签插到head标签最后 ```js // 创建link标签 const myCSS = document.createElement( "link" ); myCSS.rel = "stylesheet"; myCSS.href = "mystyles.css"; // 插入到header的最后位置 document.head.insertBefore( myCSS, document.head.childNodes[ document.head.childNodes.length - 1 ].nextSibling ); ``` - 设置link标签media属性为noexis,浏览器会认为当前样式表不适用当前类型,会在不阻塞页面渲染的情况下再进行下载。加载完成后,将`media`的值设为`screen`或`all`,从而让浏览器开始解析CSS ```html ``` - 通过rel属性将link元素标记为alternate可选样式表,也能实现浏览器异步加载。同样别忘了加载完成之后,将rel设回stylesheet ```html ``` ### 资源压缩 利用`webpack`、`gulp/grunt`、`rollup`等模块化工具,将`css`代码进行压缩,使文件变小,大大降低了浏览器的加载时间 ### 合理使用选择器 `css`匹配的规则是从右往左开始匹配,例如`#markdown .content h3`匹配规则如下: - 先找到h3标签元素 - 然后去除祖先不是.content的元素 - 最后去除祖先不是#markdown的元素 如果嵌套的层级更多,页面中的元素更多,那么匹配所要花费的时间代价自然更高 所以我们在编写选择器的时候,可以遵循以下规则: - 不要嵌套使用过多复杂选择器,最好不要三层以上 - 使用id选择器就没必要再进行嵌套 - 通配符和属性选择器效率最低,避免使用 ### 减少使用昂贵的属性 在页面发生重绘的时候,昂贵属性如`box-shadow`/`border-radius`/`filter`/透明度/`:nth-child`等,会降低浏览器的渲染性能 ### 不要使用@import css样式文件有两种引入方式,一种是`link`元素,另一种是`@import` `@import`会影响浏览器的并行下载,使得页面在加载时增加额外的延迟,增添了额外的往返耗时 而且多个`@import`可能会导致下载顺序紊乱 比如一个css文件`index.css`包含了以下内容:`@import url("reset.css")` 那么浏览器就必须先把`index.css`下载、解析和执行后,才下载、解析和执行第二个文件`reset.css` ### 其他 - 减少重排操作,以及减少不必要的重绘 - 了解哪些属性可以继承而来,避免对这些属性重复编写 - cssSprite,合成所有icon图片,用宽高加上backgroud-position的背景图方式显现出我们要的icon图,减少了http请求 - 把小的icon图片转成base64编码 - CSS3动画或者过渡尽量使用transform和opacity来实现动画,不要使用left和top属性 ## 三、总结 `css`实现性能的方式可以从选择器嵌套、属性特性、减少`http`这三面考虑,同时还要注意`css`代码的加载顺序 ## 参考文献 - https://www.zhihu.com/question/19886806 - https://juejin.cn/post/6844903649605320711#heading-1 - https://vue3js.cn/interview/ ================================================ FILE: docs/css/dp_px_dpr_ppi.md ================================================ # 面试官:说说设备像素、css像素、设备独立像素、dpr、ppi 之间的区别? ![](https://static.vue-js.com/c4d9bfd0-91f2-11eb-85f6-6fac77c0c9b3.png) ## 一、背景 在`css`中我们通常使用px作为单位,在PC浏览器中`css`的1个像素都是对应着电脑屏幕的1个物理像素 这会造成一种错觉,我们会认为`css`中的像素就是设备的物理像素 但实际情况却并非如此,`css`中的像素只是一个抽象的单位,在不同的设备或不同的环境中,`css`中的1px所代表的设备物理像素是不同的 当我们做移动端开发时,同为1px的设置,在不同分辨率的移动设备上显示效果却有很大差异 这背后就涉及了css像素、设备像素、设备独立像素、dpr、ppi的概念 ## 二、介绍 ### CSS像素 CSS像素(css pixel, px): 适用于web编程,在 CSS 中以 px 为后缀,是一个长度单位 在 CSS 规范中,长度单位可以分为两类,绝对单位以及相对单位 px是一个相对单位,相对的是设备像素(device pixel) 一般情况,页面缩放比为1,1个CSS像素等于1个设备独立像素 `CSS`像素又具有两个方面的相对性: - 在同一个设备上,每1个 CSS 像素所代表的设备像素是可以变化的(比如调整屏幕的分辨率) - 在不同的设备之间,每1个 CSS 像素所代表的设备像素是可以变化的(比如两个不同型号的手机) 在页面进行缩放操作也会 引起`css`中`px`的变化,假设页面放大一倍,原来的 1px 的东西变成 2px,在实际宽度不变的情况下1px 变得跟原来的 2px 的长度(长宽)一样了(元素会占据更多的设备像素) 假设原来需要 320px 才能填满的宽度现在只需要 160px px会受到下面的因素的影响而变化: - 每英寸像素(PPI) - 设备像素比(DPR) ### 设备像素 设备像素(device pixels),又称为物理像素 指设备能控制显示的最小物理单位,不一定是一个小正方形区块,也没有标准的宽高,只是用于显示丰富色彩的一个“点”而已 可以参考公园里的景观变色彩灯,一个彩灯(物理像素)由红、蓝、绿小灯组成,三盏小灯不同的亮度混合出各种色彩 ![](https://static.vue-js.com/cffc6570-91f2-11eb-ab90-d9ae814b240d.png) 从屏幕在工厂生产出的那天起,它上面设备像素点就固定不变了,单位为`pt` ### 设备独立像素 设备独立像素(Device Independent Pixel):与设备无关的逻辑像素,代表可以通过程序控制使用的虚拟像素,是一个总体概念,包括了CSS像素 在`javaScript`中可以通过`window.screen.width/ window.screen.height` 查看 比如我们会说“电脑屏幕在 2560x1600分辨率下不适合玩游戏,我们把它调为 1440x900”,这里的“分辨率”(非严谨说法)指的就是设备独立像素 一个设备独立像素里可能包含1个或者多个物理像素点,包含的越多则屏幕看起来越清晰 至于为什么出现设备独立像素这种虚拟像素单位概念,下面举个例子: iPhone 3GS 和 iPhone 4/4s 的尺寸都是 3.5 寸,但 iPhone 3GS 的分辨率是 320x480,iPhone 4/4s 的分辨率是 640x960 这意味着,iPhone 3GS 有 320 个物理像素,iPhone 4/4s 有 640 个物理像素 如果我们按照真实的物理像素进行布局,比如说我们按照 320 物理像素进行布局,到了 640 物理像素的手机上就会有一半的空白,为了避免这种问题,就产生了虚拟像素单位 我们统一 iPhone 3GS 和 iPhone 4/4s 都是 320 个虚拟像素,只是在 iPhone 3GS 上,最终 1 个虚拟像素换算成 1 个物理像素,在 iphone 4s 中,1 个虚拟像素最终换算成 2 个物理像素 至于 1 个虚拟像素被换算成几个物理像素,这个数值我们称之为设备像素比,也就是下面介绍的`dpr` ### dpr dpr(device pixel ratio),设备像素比,代表设备独立像素到设备像素的转换关系,在`JavaScript`中可以通过 `window.devicePixelRatio` 获取 计算公式如下: ![](https://static.vue-js.com/dd45e2b0-91f2-11eb-ab90-d9ae814b240d.png) 当设备像素比为1:1时,使用1(1×1)个设备像素显示1个CSS像素 当设备像素比为2:1时,使用4(2×2)个设备像素显示1个CSS像素 当设备像素比为3:1时,使用9(3×3)个设备像素显示1个CSS像素 如下图所示: ![](https://static.vue-js.com/e63cceb0-91f2-11eb-ab90-d9ae814b240d.png) 当`dpr`为3,那么`1px`的`CSS`像素宽度对应`3px`的物理像素的宽度,1px的`CSS`像素高度对应`3px`的物理像素高度 ### ppi ppi (pixel per inch),每英寸像素,表示每英寸所包含的像素点数目,更确切的说法应该是像素密度。数值越高,说明屏幕能以更高密度显示图像 计算公式如下: ![](https://static.vue-js.com/f734adf0-91f2-11eb-ab90-d9ae814b240d.png) ## 三、总结 无缩放情况下,1个CSS像素等于1个设备独立像素 设备像素由屏幕生产之后就不发生改变,而设备独立像素是一个虚拟单位会发生改变 PC端中,1个设备独立像素 = 1个设备像素 (在100%,未缩放的情况下) 在移动端中,标准屏幕(160ppi)下 1个设备独立像素 = 1个设备像素 设备像素比(dpr) = 设备像素 / 设备独立像素 每英寸像素(ppi),值越大,图像越清晰 ## 参考文献 - https://developer.mozilla.org/zh-CN/docs/Glossary/CSS_pixel - https://hijiangtao.github.io/2017/07/09/Device-Viewport-and-Pixel-Introduction/ ================================================ FILE: docs/css/em_px_rem_vh_vw.md ================================================ # 面试官:说说em/px/rem/vh/vw区别? ![](https://static.vue-js.com/51b036e0-9131-11eb-85f6-6fac77c0c9b3.png) ## 一、介绍 传统的项目开发中,我们只会用到`px`、`%`、`em`这几个单位,它可以适用于大部分的项目开发,且拥有比较良好的兼容性 从`CSS3`开始,浏览器对计量单位的支持又提升到了另外一个境界,新增了`rem`、`vh`、`vw`、`vm`等一些新的计量单位 利用这些新的单位开发出比较良好的响应式页面,适应多种不同分辨率的终端,包括移动设备等 ## 二、单位 在`css`单位中,可以分为长度单位、绝对单位,如下表所指示 | CSS单位 | | | ------------ | -------------------------------------- | | 相对长度单位 | em、ex、ch、rem、vw、vh、vmin、vmax、% | | 绝对长度单位 | cm、mm、in、px、pt、pc | 这里我们主要讲述px、em、rem、vh、vw ### px px,表示像素,所谓像素就是呈现在我们显示器上的一个个小点,每个像素点都是大小等同的,所以像素为计量单位被分在了绝对长度单位中 有些人会把`px`认为是相对长度,原因在于在移动端中存在设备像素比,`px`实际显示的大小是不确定的 这里之所以认为`px`为绝对单位,在于`px`的大小和元素的其他属性无关 ### em em是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸(`1em = 16px`) 为了简化 `font-size` 的换算,我们需要在` css `中的 `body` 选择器中声明` font-size `= `62.5%`,这就使 em 值变为 `16px*62.5% = 10px` 这样 `12px = 1.2em`, `10px = 1em`, 也就是说只需要将你的原来的` px` 数值除以 10,然后换上 `em `作为单位就行了 特点: - em 的值并不是固定的 - em 会继承父级元素的字体大小 - em 是相对长度单位。相对于当前对象内文本的字体尺寸。如当前对行内文本的字体尺寸未被人为设置,则相对于浏览器的默认字体尺寸 - 任意浏览器的默认字体高都是 16px 举个例子 ```html
    我是14px=1.4rem
    我是12px=1.2rem
    ``` 样式为 ```css ``` 这时候`.big`元素的`font-size`为14px,而`.small`元素的`font-size`为12px ### rem rem,相对单位,相对的只是HTML根元素`font-size`的值 同理,如果想要简化`font-size`的转化,我们可以在根元素`html`中加入`font-size: 62.5%` ```css html {font-size: 62.5%; } /* 公式16px*62.5%=10px */ ``` 这样页面中1rem=10px、1.2rem=12px、1.4rem=14px、1.6rem=16px;使得视觉、使用、书写都得到了极大的帮助 特点: - rem单位可谓集相对大小和绝对大小的优点于一身 - 和em不同的是rem总是相对于根元素,而不像em一样使用级联的方式来计算尺寸 ### vh、vw vw ,就是根据窗口的宽度,分成100等份,100vw就表示满宽,50vw就表示一半宽。(vw 始终是针对窗口的宽),同理,`vh`则为窗口的高度 这里的窗口分成几种情况: - 在桌面端,指的是浏览器的可视区域 - 移动端指的就是布局视口 像`vw`、`vh`,比较容易混淆的一个单位是`%`,不过百分比宽泛的讲是相对于父元素: - 对于普通定位元素就是我们理解的父元素 - 对于position: absolute;的元素是相对于已定位的父元素 - 对于position: fixed;的元素是相对于 ViewPort(可视窗口) ## 三、总结 **px**:绝对单位,页面按精确像素展示 **em**:相对单位,基准点为父节点字体的大小,如果自身定义了`font-size`按自身来计算,整个页面内`1em`不是一个固定的值 **rem**:相对单位,可理解为`root em`, 相对根节点`html`的字体大小来计算 **vh、vw**:主要用于页面视口大小布局,在页面布局上更加方便简单 ================================================ FILE: docs/css/flexbox.md ================================================ # 面试官:说说flexbox(弹性盒布局模型),以及适用场景? ![](https://static.vue-js.com/ef25b0a0-9837-11eb-ab90-d9ae814b240d.png) ## 一、是什么 `Flexible Box` 简称 `flex`,意为”弹性布局”,可以简便、完整、响应式地实现各种页面布局 采用Flex布局的元素,称为`flex`容器`container` 它的所有子元素自动成为容器成员,称为`flex`项目`item` ![](https://static.vue-js.com/fbc5f590-9837-11eb-ab90-d9ae814b240d.png) 容器中默认存在两条轴,主轴和交叉轴,呈90度关系。项目默认沿主轴排列,通过`flex-direction`来决定主轴的方向 每根轴都有起点和终点,这对于元素的对齐非常重要 ## 二、属性 关于`flex`常用的属性,我们可以划分为容器属性和容器成员属性 容器属性有: - flex-direction - flex-wrap - flex-flow - justify-content - align-items - align-content ### flex-direction 决定主轴的方向(即项目的排列方向) ```css .container { flex-direction: row | row-reverse | column | column-reverse; } ``` 属性对应如下: - row(默认值):主轴为水平方向,起点在左端 - row-reverse:主轴为水平方向,起点在右端 - column:主轴为垂直方向,起点在上沿。 - column-reverse:主轴为垂直方向,起点在下沿 如下图所示: ![](https://static.vue-js.com/0c9abc70-9838-11eb-ab90-d9ae814b240d.png) ### flex-wrap 弹性元素永远沿主轴排列,那么如果主轴排不下,通过`flex-wrap`决定容器内项目是否可换行 ```css .container { flex-wrap: nowrap | wrap | wrap-reverse; } ``` 属性对应如下: - nowrap(默认值):不换行 - wrap:换行,第一行在下方 - wrap-reverse:换行,第一行在上方 默认情况是不换行,但这里也不会任由元素直接溢出容器,会涉及到元素的弹性伸缩 ### flex-flow 是`flex-direction`属性和`flex-wrap`属性的简写形式,默认值为`row nowrap` ```css .box { flex-flow: || ; } ``` ### justify-content 定义了项目在主轴上的对齐方式 ```css .box { justify-content: flex-start | flex-end | center | space-between | space-around; } ``` 属性对应如下: - flex-start(默认值):左对齐 - flex-end:右对齐 - center:居中 - space-between:两端对齐,项目之间的间隔都相等 - space-around:两个项目两侧间隔相等 效果图如下: ![](https://static.vue-js.com/2d5ca950-9838-11eb-85f6-6fac77c0c9b3.png) ### align-items 定义项目在交叉轴上如何对齐 ```css .box { align-items: flex-start | flex-end | center | baseline | stretch; } ``` 属性对应如下: - flex-start:交叉轴的起点对齐 - flex-end:交叉轴的终点对齐 - center:交叉轴的中点对齐 - baseline: 项目的第一行文字的基线对齐 - stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度 ### align-content 定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用 ```css .box { align-content: flex-start | flex-end | center | space-between | space-around | stretch; } ``` 属性对应如吓: - flex-start:与交叉轴的起点对齐 - flex-end:与交叉轴的终点对齐 - center:与交叉轴的中点对齐 - space-between:与交叉轴两端对齐,轴线之间的间隔平均分布 - space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍 - stretch(默认值):轴线占满整个交叉轴 效果图如下: ![](https://static.vue-js.com/39bcb0f0-9838-11eb-ab90-d9ae814b240d.png) 容器成员属性如下: - `order` - `flex-grow` - `flex-shrink` - `flex-basis` - `flex` - `align-self` ### order 定义项目的排列顺序。数值越小,排列越靠前,默认为0 ```css .item { order: ; } ``` ### flex-grow 上面讲到当容器设为`flex-wrap: nowrap;`不换行的时候,容器宽度有不够分的情况,弹性元素会根据`flex-grow`来决定 定义项目的放大比例(容器宽度>元素总宽度时如何伸展) 默认为`0`,即如果存在剩余空间,也不放大 ```css .item { flex-grow: ; } ``` 如果所有项目的`flex-grow`属性都为1,则它们将等分剩余空间(如果有的话) ![](https://static.vue-js.com/48c8c5c0-9838-11eb-ab90-d9ae814b240d.png) 如果一个项目的`flex-grow`属性为2,其他项目都为1,则前者占据的剩余空间将比其他项多一倍 ![](https://static.vue-js.com/5b822b20-9838-11eb-ab90-d9ae814b240d.png) 弹性容器的宽度正好等于元素宽度总和,无多余宽度,此时无论`flex-grow`是什么值都不会生效 ### flex-shrink 定义了项目的缩小比例(容器宽度<元素总宽度时如何收缩),默认为1,即如果空间不足,该项目将缩小 ```css .item { flex-shrink: ; /* default 1 */ } ``` 如果所有项目的`flex-shrink`属性都为1,当空间不足时,都将等比例缩小 如果一个项目的`flex-shrink`属性为0,其他项目都为1,则空间不足时,前者不缩小 ![](https://static.vue-js.com/658c5be0-9838-11eb-85f6-6fac77c0c9b3.png) 在容器宽度有剩余时,`flex-shrink`也是不会生效的 ### flex-basis 设置的是元素在主轴上的初始尺寸,所谓的初始尺寸就是元素在`flex-grow`和`flex-shrink`生效前的尺寸 浏览器根据这个属性,计算主轴是否有多余空间,默认值为`auto`,即项目的本来大小,如设置了`width`则元素尺寸由`width/height`决定(主轴方向),没有设置则由内容决定 ```css .item { flex-basis: | auto; /* default auto */ } ``` 当设置为0的是,会根据内容撑开 它可以设为跟`width`或`height`属性一样的值(比如350px),则项目将占据固定空间 ### flex `flex`属性是`flex-grow`, `flex-shrink` 和 `flex-basis`的简写,默认值为`0 1 auto`,也是比较难懂的一个复合属性 ```css .item { flex: none | [ <'flex-grow'> <'flex-shrink'>? || <'flex-basis'> ] } ``` 一些属性有: - flex: 1 = flex: 1 1 0% - flex: 2 = flex: 2 1 0% - flex: auto = flex: 1 1 auto - flex: none = flex: 0 0 auto,常用于固定尺寸不伸缩 `flex:1` 和 `flex:auto` 的区别,可以归结于`flex-basis:0`和`flex-basis:auto`的区别 当设置为0时(绝对弹性元素),此时相当于告诉`flex-grow`和`flex-shrink`在伸缩的时候不需要考虑我的尺寸 当设置为`auto`时(相对弹性元素),此时则需要在伸缩时将元素尺寸纳入考虑 注意:建议优先使用这个属性,而不是单独写三个分离的属性,因为浏览器会推算相关值 ### align-self 允许单个项目有与其他项目不一样的对齐方式,可覆盖`align-items`属性 默认值为`auto`,表示继承父元素的`align-items`属性,如果没有父元素,则等同于`stretch` ```css .item { align-self: auto | flex-start | flex-end | center | baseline | stretch; } ``` 效果图如下: ![](https://static.vue-js.com/6f8304a0-9838-11eb-85f6-6fac77c0c9b3.png) ## 三、应用场景 在以前的文章中,我们能够通过`flex`简单粗暴的实现元素水平垂直方向的居中,以及在两栏三栏自适应布局中通过`flex`完成,这里就不再展开代码的演示 包括现在在移动端、小程序这边的开发,都建议使用`flex`进行布局 ## 参考文献 - https://developer.mozilla.org/zh-CN/docs/Web/CSS/flex - http://www.ruanyifeng.com/blog/2015/07/flex-grammar.html ================================================ FILE: docs/css/grid.md ================================================ # 面试官:介绍一下grid网格布局 ![](https://static.vue-js.com/4d73e3d0-9a94-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 `Grid` 布局即网格布局,是一个二维的布局方式,由纵横相交的两组网格线形成的框架性布局结构,能够同时处理行与列 擅长将一个页面划分为几个主要区域,以及定义这些区域的大小、位置、层次等关系 ![](https://static.vue-js.com/59680a40-9a94-11eb-85f6-6fac77c0c9b3.png) 这与之前讲到的`flex`一维布局不相同 设置`display:grid/inline-grid`的元素就是网格布局容器,这样就能出发浏览器渲染引擎的网格布局算法 ```js

    ``` 上述代码实例中,`.container`元素就是网格布局容器,`.item`元素就是网格的项目,由于网格元素只能是容器的顶层子元素,所以`p`元素并不是网格元素 这里提一下,网格线概念,有助于下面对`grid-column`系列属性的理解 网格线,即划分网格的线,如下图所示: ![](https://static.vue-js.com/61be7080-9a94-11eb-ab90-d9ae814b240d.png) 上图是一个 2 x 3 的网格,共有3根水平网格线和4根垂直网格线 ## 二、属性 同样,`Grid` 布局属性可以分为两大类: - 容器属性, - 项目属性 关于容器属性有如下: ### display 属性 文章开头讲到,在元素上设置`display:grid` 或 `display:inline-grid` 来创建一个网格容器 - display:grid 则该容器是一个块级元素 - display: inline-grid 则容器元素为行内元素 ### grid-template-columns 属性,grid-template-rows 属性 `grid-template-columns` 属性设置列宽,`grid-template-rows` 属性设置行高 ```css .wrapper { display: grid; /* 声明了三列,宽度分别为 200px 200px 200px */ grid-template-columns: 200px 200px 200px; grid-gap: 5px; /* 声明了两行,行高分别为 50px 50px */ grid-template-rows: 50px 50px; } ``` 以上表示固定列宽为 200px 200px 200px,行高为 50px 50px 上述代码可以看到重复写单元格宽高,通过使用`repeat()`函数,可以简写重复的值 - 第一个参数是重复的次数 - 第二个参数是重复的值 所以上述代码可以简写成 ```css .wrapper { display: grid; grid-template-columns: repeat(3,200px); grid-gap: 5px; grid-template-rows:repeat(2,50px); } ``` 除了上述的`repeact`关键字,还有: - auto-fill:示自动填充,让一行(或者一列)中尽可能的容纳更多的单元格 >`grid-template-columns: repeat(auto-fill, 200px)` 表示列宽是 200 px,但列的数量是不固定的,只要浏览器能够容纳得下,就可以放置元素 - fr:片段,为了方便表示比例关系 >`grid-template-columns: 200px 1fr 2fr` 表示第一个列宽设置为 200px,后面剩余的宽度分为两部分,宽度分别为剩余宽度的 1/3 和 2/3 - minmax:产生一个长度范围,表示长度就在这个范围之中都可以应用到网格项目中。第一个参数就是最小值,第二个参数就是最大值 >`minmax(100px, 1fr)`表示列宽不小于`100px`,不大于`1fr` - auto:由浏览器自己决定长度 >`grid-template-columns: 100px auto 100px` 表示第一第三列为 100px,中间由浏览器决定长度 ### grid-row-gap 属性, grid-column-gap 属性, grid-gap 属性 `grid-row-gap` 属性、`grid-column-gap` 属性分别设置行间距和列间距。`grid-gap` 属性是两者的简写形式 `grid-row-gap: 10px` 表示行间距是 10px `grid-column-gap: 20px` 表示列间距是 20px `grid-gap: 10px 20px` 等同上述两个属性 ### grid-template-areas 属性 用于定义区域,一个区域由一个或者多个单元格组成 ```css .container { display: grid; grid-template-columns: 100px 100px 100px; grid-template-rows: 100px 100px 100px; grid-template-areas: 'a b c' 'd e f' 'g h i'; } ``` 上面代码先划分出9个单元格,然后将其定名为`a`到`i`的九个区域,分别对应这九个单元格。 多个单元格合并成一个区域的写法如下 ```css grid-template-areas: 'a a a' 'b b b' 'c c c'; ``` 上面代码将9个单元格分成`a`、`b`、`c`三个区域 如果某些区域不需要利用,则使用"点"(`.`)表示 ### grid-auto-flow 属性 划分网格以后,容器的子元素会按照顺序,自动放置在每一个网格。 顺序就是由`grid-auto-flow`决定,默认为行,代表"先行后列",即先填满第一行,再开始放入第二行 ![](https://static.vue-js.com/70fb3240-9a94-11eb-ab90-d9ae814b240d.png) 当修改成`column`后,放置变为如下: ![](https://static.vue-js.com/7c26ffa0-9a94-11eb-ab90-d9ae814b240d.png) ### justify-items 属性, align-items 属性, place-items 属性 `justify-items` 属性设置单元格内容的水平位置(左中右),`align-items` 属性设置单元格的垂直位置(上中下) 两者属性的值完成相同 ```css .container { justify-items: start | end | center | stretch; align-items: start | end | center | stretch; } ``` 属性对应如下: - start:对齐单元格的起始边缘 - end:对齐单元格的结束边缘 - center:单元格内部居中 - stretch:拉伸,占满单元格的整个宽度(默认值) `place-items`属性是`align-items`属性和`justify-items`属性的合并简写形式 ### justify-content 属性, align-content 属性, place-content 属性 `justify-content`属性是整个内容区域在容器里面的水平位置(左中右),`align-content`属性是整个内容区域的垂直位置(上中下) ```css .container { justify-content: start | end | center | stretch | space-around | space-between | space-evenly; align-content: start | end | center | stretch | space-around | space-between | space-evenly; } ``` 两个属性的写法完全相同,都可以取下面这些值: - start - 对齐容器的起始边框 - end - 对齐容器的结束边框 - center - 容器内部居中 ![](https://static.vue-js.com/9d1ec990-9a94-11eb-ab90-d9ae814b240d.png) - space-around - 每个项目两侧的间隔相等。所以,项目之间的间隔比项目与容器边框的间隔大一倍 - space-between - 项目与项目的间隔相等,项目与容器边框之间没有间隔 - space-evenly - 项目与项目的间隔相等,项目与容器边框之间也是同样长度的间隔 - stretch - 项目大小没有指定时,拉伸占据整个网格容器 ![](https://static.vue-js.com/a620b210-9a94-11eb-85f6-6fac77c0c9b3.png) ### grid-auto-columns 属性和 grid-auto-rows 属性 有时候,一些项目的指定位置,在现有网格的外部,就会产生显示网格和隐式网格 比如网格只有3列,但是某一个项目指定在第5行。这时,浏览器会自动生成多余的网格,以便放置项目。超出的部分就是隐式网格 而`grid-auto-rows`与`grid-auto-columns`就是专门用于指定隐式网格的宽高 关于项目属性,有如下: ### grid-column-start 属性、grid-column-end 属性、grid-row-start 属性以及grid-row-end 属性 指定网格项目所在的四个边框,分别定位在哪根网格线,从而指定项目的位置 - grid-column-start 属性:左边框所在的垂直网格线 - grid-column-end 属性:右边框所在的垂直网格线 - grid-row-start 属性:上边框所在的水平网格线 - grid-row-end 属性:下边框所在的水平网格线 举个例子: ```html
    1
    2
    3
    ``` 通过设置`grid-column`属性,指定1号项目的左边框是第二根垂直网格线,右边框是第四根垂直网格线 ![](https://static.vue-js.com/b7925530-9a94-11eb-ab90-d9ae814b240d.png) ### grid-area 属性 `grid-area` 属性指定项目放在哪一个区域 ```css .item-1 { grid-area: e; } ``` 意思为将1号项目位于`e`区域 与上述讲到的`grid-template-areas`搭配使用 ### justify-self 属性、align-self 属性以及 place-self 属性 `justify-self`属性设置单元格内容的水平位置(左中右),跟`justify-items`属性的用法完全一致,但只作用于单个项目。 `align-self`属性设置单元格内容的垂直位置(上中下),跟`align-items`属性的用法完全一致,也是只作用于单个项目 ```css .item { justify-self: start | end | center | stretch; align-self: start | end | center | stretch; } ``` 这两个属性都可以取下面四个值。 - start:对齐单元格的起始边缘。 - end:对齐单元格的结束边缘。 - center:单元格内部居中。 - stretch:拉伸,占满单元格的整个宽度(默认值) ## 三、应用场景 文章开头就讲到,`Grid`是一个强大的布局,如一些常见的 CSS 布局,如居中,两列布局,三列布局等等是很容易实现的,在以前的文章中,也有使用`Grid`布局完成对应的功能 关于兼容性问题,结果如下: ![](https://static.vue-js.com/c24a2b10-9a94-11eb-85f6-6fac77c0c9b3.png) 总体兼容性还不错,但在 IE 10 以下不支持 目前,`Grid`布局在手机端支持还不算太友好 ## 参考文献 - https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Grid_Layout - https://www.ruanyifeng.com/blog/2019/03/grid-layout-tutorial.html - https://juejin.cn/post/6854573220306255880#heading-2 ================================================ FILE: docs/css/hide_attributes.md ================================================ # 面试官:css中,有哪些方式可以隐藏页面元素?区别? ![](https://static.vue-js.com/ccf96f50-929a-11eb-ab90-d9ae814b240d.png) ## 一、前言 在平常的样式排版中,我们经常遇到将某个模块隐藏的场景 通过`css`隐藏元素的方法有很多种,它们看起来实现的效果是一致的 但实际上每一种方法都有一丝轻微的不同,这些不同决定了在一些特定场合下使用哪一种方法 ## 二、实现方式 通过`css`实现隐藏元素方法有如下: - display:none - visibility:hidden - opacity:0 - 设置height、width模型属性为0 - position:absolute - clip-path ### display:none 设置元素的`display`为`none`是最常用的隐藏元素的方法 ```css .hide { display:none; } ``` 将元素设置为`display:none`后,元素在页面上将彻底消失 元素本身占有的空间就会被其他元素占有,也就是说它会导致浏览器的重排和重绘 消失后,自身绑定的事件不会触发,也不会有过渡效果 特点:元素不可见,不占据空间,无法响应点击事件 ### visibility:hidden 设置元素的`visibility`为`hidden`也是一种常用的隐藏元素的方法 从页面上仅仅是隐藏该元素,DOM结果均会存在,只是当时在一个不可见的状态,不会触发重排,但是会触发重绘 ```css .hidden{ visibility:hidden } ``` 给人的效果是隐藏了,所以他自身的事件不会触发 特点:元素不可见,占据页面空间,无法响应点击事件 ### opacity:0 `opacity`属性表示元素的透明度,将元素的透明度设置为0后,在我们用户眼中,元素也是隐藏的 不会引发重排,一般情况下也会引发重绘 > 如果利用 animation 动画,对 opacity 做变化(animation会默认触发GPU加速),则只会触发 GPU 层面的 composite,不会触发重绘 ```css .transparent { opacity:0; } ``` 由于其仍然是存在于页面上的,所以他自身的的事件仍然是可以触发的,但被他遮挡的元素是不能触发其事件的 需要注意的是:其子元素不能设置opacity来达到显示的效果 特点:改变元素透明度,元素不可见,占据页面空间,可以响应点击事件 ### 设置height、width属性为0 将元素的`margin`,`border`,`padding`,`height`和`width`等影响元素盒模型的属性设置成0,如果元素内有子元素或内容,还应该设置其`overflow:hidden`来隐藏其子元素 ```css .hiddenBox { margin:0; border:0; padding:0; height:0; width:0; overflow:hidden; } ``` 特点:元素不可见,不占据页面空间,无法响应点击事件 ### position:absolute 将元素移出可视区域 ```css .hide { position: absolute; top: -9999px; left: -9999px; } ``` 特点:元素不可见,不影响页面布局 ### clip-path 通过裁剪的形式 ```css .hide { clip-path: polygon(0px 0px,0px 0px,0px 0px,0px 0px); } ``` 特点:元素不可见,占据页面空间,无法响应点击事件 ### 小结 最常用的还是`display:none`和`visibility:hidden`,其他的方式只能认为是奇招,它们的真正用途并不是用于隐藏元素,所以并不推荐使用它们 ## 三、区别 关于`display: none`、` visibility: hidden`、`opacity: 0`的区别,如下表所示: | | display: none | visibility: hidden | opacity: 0 | | :--------------------- | :------------ | :----------------- | ---------- | | 页面中 | 不存在 | 存在 | 存在 | | 重排 | 会 | 不会 | 不会 | | 重绘 | 会 | 会 | 不一定 | | 自身绑定事件 | 不触发 | 不触发 | 可触发 | | transition | 不支持 | 支持 | 支持 | | 子元素可复原 | 不能 | 能 | 不能 | | 被遮挡的元素可触发事件 | 能 | 能 | 不能 | ## 参考文献 - https://www.cnblogs.com/a-cat/p/9039962.html ================================================ FILE: docs/css/layout_painting.md ================================================ # 面试官:怎么理解回流跟重绘?什么场景下会触发? ![](https://static.vue-js.com/1ed5d340-9cdc-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 在`HTML`中,每个元素都可以理解成一个盒子,在浏览器解析过程中,会涉及到回流与重绘: - 回流:布局引擎会根据各种样式计算每个盒子在页面上的大小与位置 - 重绘:当计算好盒模型的位置、大小及其他属性后,浏览器根据每个盒子特性进行绘制 具体的浏览器解析渲染机制如下所示: ![](https://static.vue-js.com/2b56a950-9cdc-11eb-ab90-d9ae814b240d.png) - 解析HTML,生成DOM树,解析CSS,生成CSSOM树 - 将DOM树和CSSOM树结合,生成渲染树(Render Tree) - Layout(回流):根据生成的渲染树,进行回流(Layout),得到节点的几何信息(位置,大小) - Painting(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素 - Display:将像素发送给GPU,展示在页面上 在页面初始渲染阶段,回流不可避免的触发,可以理解成页面一开始是空白的元素,后面添加了新的元素使页面布局发生改变 当我们对 `DOM` 的修改引发了 `DOM `几何尺寸的变化(比如修改元素的宽、高或隐藏元素等)时,浏览器需要重新计算元素的几何属性,然后再将计算的结果绘制出来 当我们对 `DOM `的修改导致了样式的变化(`color`或`background-color`),却并未影响其几何属性时,浏览器不需重新计算元素的几何属性、直接为该元素绘制新的样式,这里就仅仅触发了重绘 ## 二、如何触发 要想减少回流和重绘的次数,首先要了解回流和重绘是如何触发的 ### 回流触发时机 回流这一阶段主要是计算节点的位置和几何信息,那么当页面布局和几何信息发生变化的时候,就需要回流,如下面情况: - 添加或删除可见的DOM元素 - 元素的位置发生变化 - 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等) - 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代 - 页面一开始渲染的时候(这避免不了) - 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的) 还有一些容易被忽略的操作:获取一些特定属性的值 > offsetTop、offsetLeft、 offsetWidth、offsetHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、clientTop、clientLeft、clientWidth、clientHeight 这些属性有一个共性,就是需要通过即时计算得到。因此浏览器为了获取这些值,也会进行回流 除此还包括`getComputedStyle `方法,原理是一样的 ### 重绘触发时机 触发回流一定会触发重绘 可以把页面理解为一个黑板,黑板上有一朵画好的小花。现在我们要把这朵从左边移到了右边,那我们要先确定好右边的具体位置,画好形状(回流),再画上它原有的颜色(重绘) 除此之外还有一些其他引起重绘行为: - 颜色的修改 - 文本方向的修改 - 阴影的修改 ### 浏览器优化机制 由于每次重排都会造成额外的计算消耗,因此大多数浏览器都会通过队列化修改并批量执行来优化重排过程。浏览器会将修改操作放入到队列里,直到过了一段时间或者操作达到了一个阈值,才清空队列 当你获取布局信息的操作的时候,会强制队列刷新,包括前面讲到的`offsetTop`等方法都会返回最新的数据 因此浏览器不得不清空队列,触发回流重绘来返回正确的值 ## 三、如何减少 我们了解了如何触发回流和重绘的场景,下面给出避免回流的经验: - 如果想设定元素的样式,通过改变元素的 `class` 类名 (尽可能在 DOM 树的最里层) - 避免设置多项内联样式 - 应用元素的动画,使用 `position` 属性的 `fixed` 值或 `absolute` 值(如前文示例所提) - 避免使用 `table` 布局,`table` 中每个元素的大小以及内容的改动,都会导致整个 `table` 的重新计算 - 对于那些复杂的动画,对其设置 `position: fixed/absolute`,尽可能地使元素脱离文档流,从而减少对其他元素的影响 - 使用css3硬件加速,可以让`transform`、`opacity`、`filters`这些动画不会引起回流重绘 - 避免使用 CSS 的 `JavaScript` 表达式 在使用 `JavaScript` 动态插入多个节点时, 可以使用`DocumentFragment`. 创建后一次插入. 就能避免多次的渲染性能 但有时候,我们会无可避免地进行回流或者重绘,我们可以更好使用它们 例如,多次修改一个把元素布局的时候,我们很可能会如下操作 ```js const el = document.getElementById('el') for(let i=0;i<10;i++) { el.style.top = el.offsetTop + 10 + "px"; el.style.left = el.offsetLeft + 10 + "px"; } ``` 每次循环都需要获取多次`offset`属性,比较糟糕,可以使用变量的形式缓存起来,待计算完毕再提交给浏览器发出重计算请求 ```js // 缓存offsetLeft与offsetTop的值 const el = document.getElementById('el') let offLeft = el.offsetLeft, offTop = el.offsetTop // 在JS层面进行计算 for(let i=0;i<10;i++) { offLeft += 10 offTop += 10 } // 一次性将计算结果应用到DOM上 el.style.left = offLeft + "px" el.style.top = offTop + "px" ``` 我们还可避免改变样式,使用类名去合并样式 ```js const container = document.getElementById('container') container.style.width = '100px' container.style.height = '200px' container.style.border = '10px solid red' container.style.color = 'red' ``` 使用类名去合并样式 ```html ``` 前者每次单独操作,都去触发一次渲染树更改(新浏览器不会), 都去触发一次渲染树更改,从而导致相应的回流与重绘过程 合并之后,等于我们将所有的更改一次性发出 我们还可以通过通过设置元素属性`display: none`,将其从页面上去掉,然后再进行后续操作,这些后续操作也不会触发回流与重绘,这个过程称为离线操作 ```js const container = document.getElementById('container') container.style.width = '100px' container.style.height = '200px' container.style.border = '10px solid red' container.style.color = 'red' ``` 离线操作后 ```js let container = document.getElementById('container') container.style.display = 'none' container.style.width = '100px' container.style.height = '200px' container.style.border = '10px solid red' container.style.color = 'red' ...(省略了许多类似的后续操作) container.style.display = 'block' ``` ## 参考文献 - https://juejin.cn/post/6844903942137053192 - https://segmentfault.com/a/1190000017329980 ================================================ FILE: docs/css/less_12px.md ================================================ # 面试官:让Chrome支持小于12px 的文字方式有哪些?区别? ![](https://static.vue-js.com/62945fd0-a334-11eb-85f6-6fac77c0c9b3.png) ## 一、背景 Chrome 中文版浏览器会默认设定页面的最小字号是12px,英文版没有限制 原由 Chrome 团队认为汉字小于12px就会增加识别难度 - 中文版浏览器 与网页语言无关,取决于用户在Chrome的设置里(chrome://settings/languages)把哪种语言设置为默认显示语言 - 系统级最小字号 浏览器默认设定页面的最小字号,用户可以前往 chrome://settings/fonts 根据需求更改 而我们在实际项目中,不能奢求用户更改浏览器设置 对于文本需要以更小的字号来显示,就需要用到一些小技巧 ## 二、解决方案 常见的解决方案有: - zoom - -webkit-transform:scale() - -webkit-text-size-adjust:none ### Zoom `zoom` 的字面意思是“变焦”,可以改变页面上元素的尺寸,属于真实尺寸 其支持的值类型有: - zoom:50%,表示缩小到原来的一半 - zoom:0.5,表示缩小到原来的一半 使用 `zoom` 来”支持“ 12px 以下的字体 代码如下: ```html 测试10px 测试12px ``` 效果如下: ![](https://static.vue-js.com/d5243980-a334-11eb-ab90-d9ae814b240d.png) > 需要注意的是,`Zoom` 并不是标准属性,需要考虑其兼容性 ![image.png](https://static.vue-js.com/3defe3c0-a343-11eb-85f6-6fac77c0c9b3.png) ### -webkit-transform:scale() 针对`chrome`浏览器,加`webkit`前缀,用`transform:scale()`这个属性进行放缩 注意的是,使用`scale`属性只对可以定义宽高的元素生效,所以,下面代码中将`span`元素转为行内块元素 实现代码如下: ```html 测试10px 测试12px ``` 效果如下: ![](https://static.vue-js.com/d5243980-a334-11eb-ab90-d9ae814b240d.png) ### -webkit-text-size-adjust:none 该属性用来设定文字大小是否根据设备(浏览器)来自动调整显示大小 属性值: - percentage:字体显示的大小; - auto:默认,字体大小会根据设备/浏览器来自动调整; - none:字体大小不会自动调整 ```css html { -webkit-text-size-adjust: none; } ``` 这样设置之后会有一个问题,就是当你放大网页时,一般情况下字体也会随着变大,而设置了以上代码后,字体只会显示你当前设置的字体大小,不会随着网页放大而变大了 所以,我们不建议全局应用该属性,而是单独对某一属性使用 > 需要注意的是,自从`chrome 27`之后,就取消了对这个属性的支持。同时,该属性只对英文、数字生效,对中文不生效 ## 三、总结 `Zoom` 非标属性,有兼容问题,缩放会改变了元素占据的空间大小,触发重排 `-webkit-transform:scale()` 大部分现代浏览器支持,并且对英文、数字、中文也能够生效,缩放不会改变了元素占据的空间大小,页面布局不会发生变化 `-webkit-text-size-adjust`对谷歌浏览器有版本要求,在27之后,就取消了该属性的支持,并且只对英文、数字生效 ## 参考文献 - https://developer.mozilla.org/zh-CN/docs/Web/CSS/text-size-adjust - https://vue3js.cn/interview ================================================ FILE: docs/css/responsive_layout.md ================================================ # 面试官:什么是响应式设计?响应式设计的基本原理是什么?如何做? ![](https://static.vue-js.com/a57e2e40-9dba-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 响应式网站设计(Responsive Web design)是一种网络页面设计布局,页面的设计与开发应当根据用户行为以及设备环境(系统平台、屏幕尺寸、屏幕定向等)进行相应的响应和调整 描述响应式界面最著名的一句话就是“Content is like water” 大白话便是“如果将屏幕看作容器,那么内容就像水一样” 响应式网站常见特点: - 同时适配PC + 平板 + 手机等 - 标签导航在接近手持终端设备时改变为经典的抽屉式导航 - 网站的布局会根据视口来调整模块的大小和位置 ![](https://static.vue-js.com/ae68be30-9dba-11eb-85f6-6fac77c0c9b3.png) ## 二、实现方式 响应式设计的基本原理是通过媒体查询检测不同的设备屏幕尺寸做处理,为了处理移动端,页面头部必须有`meta`声明`viewport` ```html

    ``` 关于`css`属性选择器常用的有: - id选择器(#box),选择id为box的元素 - 类选择器(.one),选择类名为one的所有元素 - 标签选择器(div),选择标签为div的所有元素 - 后代选择器(#box div),选择id为box元素内部所有的div元素 - 子选择器(.one>one_1),选择父元素为.one的所有.one_1的元素 - 相邻同胞选择器(.one+.two),选择紧接在.one之后的所有.two元素 - 群组选择器(div,p),选择div、p的所有元素 还有一些使用频率相对没那么多的选择器: - 伪类选择器 ```css :link :选择未被访问的链接 :visited:选取已被访问的链接 :active:选择活动链接 :hover :鼠标指针浮动在上面的元素 :focus :选择具有焦点的 :first-child:父元素的首个子元素 ``` - 伪元素选择器 ```css :first-letter :用于选取指定选择器的首字母 :first-line :选取指定选择器的首行 :before : 选择器在被选元素的内容前面插入内容 :after : 选择器在被选元素的内容后面插入内容 ``` - 属性选择器 ```css [attribute] 选择带有attribute属性的元素 [attribute=value] 选择所有使用attribute=value的元素 [attribute~=value] 选择attribute属性包含value的元素 [attribute|=value]:选择attribute属性以value开头的元素 ``` 在`CSS3`中新增的选择器有如下: - 层次选择器(p~ul),选择前面有p元素的每个ul元素 - 伪类选择器 ```css :first-of-type 表示一组同级元素中其类型的第一个元素 :last-of-type 表示一组同级元素中其类型的最后一个元素 :only-of-type 表示没有同类型兄弟元素的元素 :only-child 表示没有任何兄弟的元素 :nth-child(n) 根据元素在一组同级中的位置匹配元素 :nth-last-of-type(n) 匹配给定类型的元素,基于它们在一组兄弟元素中的位置,从末尾开始计数 :last-child 表示一组兄弟元素中的最后一个元素 :root 设置HTML文档 :empty 指定空的元素 :enabled 选择可用元素 :disabled 选择被禁用元素 :checked 选择选中的元素 :not(selector) 选择与 不匹配的所有元素 ``` - 属性选择器 ```css [attribute*=value]:选择attribute属性值包含value的所有元素 [attribute^=value]:选择attribute属性开头为value的所有元素 [attribute$=value]:选择attribute属性结尾为value的所有元素 ``` ## 二、优先级 相信大家对`CSS`选择器的优先级都不陌生: > 内联 > ID选择器 > 类选择器 > 标签选择器 到具体的计算层⾯,优先级是由 A 、B、C、D 的值来决定的,其中它们的值计算规则如下: - 如果存在内联样式,那么 A = 1, 否则 A = 0 - B的值等于 ID选择器出现的次数 - C的值等于 类选择器 和 属性选择器 和 伪类 出现的总次数 - D 的值等于 标签选择器 和 伪元素 出现的总次数 这里举个例子: ```css #nav-global > ul > li > a.nav-link ``` 套用上面的算法,依次求出 `A` `B` `C` `D` 的值: - 因为没有内联样式 ,所以 A = 0 - ID选择器总共出现了1次, B = 1 - 类选择器出现了1次, 属性选择器出现了0次,伪类选择器出现0次,所以 C = (1 + 0 + 0) = 1 - 标签选择器出现了3次, 伪元素出现了0次,所以 D = (3 + 0) = 3 上面算出的`A` 、 `B`、`C`、`D` 可以简记作:`(0, 1, 1, 3)` 知道了优先级是如何计算之后,就来看看比较规则: - 从左往右依次进行比较 ,较大者优先级更高 - 如果相等,则继续往右移动一位进行比较 - 如果4位全部相等,则后面的会覆盖前面的 经过上面的优先级计算规则,我们知道内联样式的优先级最高,如果外部样式需要覆盖内联样式,就需要使用`!important` ## 三、继承属性 在`css`中,继承是指的是给父元素设置一些属性,后代元素会自动拥有这些属性 关于继承属性,可以分成: - 字体系列属性 ```css font:组合字体 font-family:规定元素的字体系列 font-weight:设置字体的粗细 font-size:设置字体的尺寸 font-style:定义字体的风格 font-variant:偏大或偏小的字体 ``` - 文本系列属性 ```css text-indent:文本缩进 text-align:文本水平对刘 line-height:行高 word-spacing:增加或减少单词间的空白 letter-spacing:增加或减少字符间的空白 text-transform:控制文本大小写 direction:规定文本的书写方向 color:文本颜色 ``` - 元素可见性 ```css visibility ``` - 表格布局属性 ```css caption-side:定位表格标题位置 border-collapse:合并表格边框 border-spacing:设置相邻单元格的边框间的距离 empty-cells:单元格的边框的出现与消失 table-layout:表格的宽度由什么决定 ``` - 列表属性 ```css list-style-type:文字前面的小点点样式 list-style-position:小点点位置 list-style:以上的属性可通过这属性集合 ``` - 引用 ```css quotes:设置嵌套引用的引号类型 ``` - 光标属性 ```css cursor:箭头可以变成需要的形状 ``` 继承中比较特殊的几点: - a 标签的字体颜色不能被继承 - h1-h6标签字体的大下也是不能被继承的 ### 无继承的属性 - display - 文本属性:vertical-align、text-decoration - 盒子模型的属性:宽度、高度、内外边距、边框等 - 背景属性:背景图片、颜色、位置等 - 定位属性:浮动、清除浮动、定位position等 - 生成内容属性:content、counter-reset、counter-increment - 轮廓样式属性:outline-style、outline-width、outline-color、outline - 页面样式属性:size、page-break-before、page-break-after ## 参考文献 - https://www.html.cn/qa/css3/13444.html - https://developer.mozilla.org/zh-CN/docs/Learn/CSS/Building_blocks/Selectors ================================================ FILE: docs/css/single_multi_line.md ================================================ # 面试官:如何实现单行/多行文本溢出的省略样式? ![](https://static.vue-js.com/ada8d840-a0e9-11eb-ab90-d9ae814b240d.png) ## 一、前言 在日常开发展示页面,如果一段文本的数量过长,受制于元素宽度的因素,有可能不能完全显示,为了提高用户的使用体验,这个时候就需要我们把溢出的文本显示成省略号 对于文本的溢出,我们可以分成两种形式: - 单行文本溢出 - 多行文本溢出 ## 二、实现方式 ### 单行文本溢出省略 理解也很简单,即文本在一行内显示,超出部分以省略号的形式展现 实现方式也很简单,涉及的`css`属性有: - text-overflow:规定当文本溢出时,显示省略符号来代表被修剪的文本 - white-space:设置文字在一行显示,不能换行 - overflow:文字长度超出限定宽度,则隐藏超出的内容 `overflow`设为`hidden`,普通情况用在块级元素的外层隐藏内部溢出元素,或者配合下面两个属性实现文本溢出省略 `white-space:nowrap`,作用是设置文本不换行,是`overflow:hidden`和`text-overflow:ellipsis`生效的基础 `text-overflow`属性值有如下: - clip:当对象内文本溢出部分裁切掉 - ellipsis:当对象内文本溢出时显示省略标记(...) `text-overflow`只有在设置了`overflow:hidden`和`white-space:nowrap`才能够生效的 举个例子 ```html

    ``` 效果如下: ![](https://static.vue-js.com/bb3048e0-a0e9-11eb-85f6-6fac77c0c9b3.png) 可以看到,设置单行文本溢出较为简单,并且省略号显示的位置较好 ### 多行文本溢出省略 多行文本溢出的时候,我们可以分为两种情况: - 基于高度截断 - 基于行数截断 #### 基于高度截断 #### 伪元素 + 定位 核心的`css`代码结构如下: - position: relative:为伪元素绝对定位 - overflow: hidden:文本溢出限定的宽度就隐藏内容) - position: absolute:给省略号绝对定位 - line-height: 20px:结合元素高度,高度固定的情况下,设定行高, 控制显示行数 - height: 40px:设定当前元素高度 - ::after {} :设置省略号样式 代码如下所示: ```html

    这是一段很长的文本
    ``` 实现原理很好理解,就是通过伪元素绝对定位到行尾并遮住文字,再通过 `overflow: hidden` 隐藏多余文字 这种实现具有以下优点: - 兼容性好,对各大主流浏览器有好的支持 - 响应式截断,根据不同宽度做出调整 一般文本存在英文的时候,可以设置`word-break: break-all`使一个单词能够在换行时进行拆分 #### 基于行数截断 纯`css`实现也非常简单,核心的`css`代码如下: - -webkit-line-clamp: 2:用来限制在一个块元素显示的文本的行数,为了实现该效果,它需要组合其他的WebKit属性) - display: -webkit-box:和1结合使用,将对象作为弹性伸缩盒子模型显示 - -webkit-box-orient: vertical:和1结合使用 ,设置或检索伸缩盒对象的子元素的排列方式 - overflow: hidden:文本溢出限定的宽度就隐藏内容 - text-overflow: ellipsis:多行文本的情况下,用省略号“…”隐藏溢出范围的文本 ```html

    这是一些文本这是一些文本这是一些文本这是一些文本这是一些文本 这是一些文本这是一些文本这是一些文本这是一些文本这是一些文本

    ``` 可以看到,上述使用了`webkit`的`CSS`属性扩展,所以兼容浏览器范围是`PC`端的`webkit`内核的浏览器,由于移动端大多数是使用`webkit`,所以移动端常用该形式 需要注意的是,如果文本为一段很长的英文或者数字,则需要添加`word-wrap: break-word`属性 还能通过使用`javascript`实现配合`css`,实现代码如下所示: css结构如下: ```css p { position: relative; width: 400px; line-height: 20px; overflow: hidden; } .p-after:after{ content: "..."; position: absolute; bottom: 0; right: 0; padding-left: 40px; background: -webkit-linear-gradient(left, transparent, #fff 55%); background: -moz-linear-gradient(left, transparent, #fff 55%); background: -o-linear-gradient(left, transparent, #fff 55%); background: linear-gradient(to right, transparent, #fff 55%); } ``` javascript代码如下: ```js $(function(){ //获取文本的行高,并获取文本的高度,假设我们规定的行数是五行,那么对超过行数的部分进行限制高度,并加上省略号 $('p').each(function(i, obj){ var lineHeight = parseInt($(this).css("line-height")); var height = parseInt($(this).height()); if((height / lineHeight) >3 ){ $(this).addClass("p-after") $(this).css("height","60px"); }else{ $(this).removeClass("p-after"); } }); }) ``` ## 参考文献 - https://www.zoo.team/article/text-overflow - https://segmentfault.com/a/1190000017078153 ================================================ FILE: docs/css/triangle.md ================================================ # 面试官:CSS如何画一个三角形?原理是什么? ![](https://static.vue-js.com/bd310120-a279-11eb-85f6-6fac77c0c9b3.png) ## 一、前言 在前端开发的时候,我们有时候会需要用到一个三角形的形状,比如地址选择或者播放器里面播放按钮 ![](https://static.vue-js.com/d6d8ff60-a279-11eb-85f6-6fac77c0c9b3.png) 通常情况下,我们会使用图片或者`svg`去完成三角形效果图,但如果单纯使用`css`如何完成一个三角形呢? 实现过程似乎也并不困难,通过边框就可完成 ## 二、实现过程 在以前也讲过盒子模型,默认情况下是一个矩形,实现也很简单 ```html
    ``` 效果如下图所示: ![](https://static.vue-js.com/e3f244e0-a279-11eb-ab90-d9ae814b240d.png) 将`border`设置`50px`,效果图如下所示: ![](https://static.vue-js.com/ee0b42b0-a279-11eb-ab90-d9ae814b240d.png) 白色区域则为`width`、`height`,这时候只需要你将白色区域部分宽高逐渐变小,最终变为0,则变成如下图所示: ![](https://static.vue-js.com/2afaa030-a27a-11eb-85f6-6fac77c0c9b3.png) 这时候就已经能够看到4个不同颜色的三角形,如果需要下方三角形,只需要将上、左、右边框设置为0就可以得到下方的红色三角形 ![](https://static.vue-js.com/2afaa030-a27a-11eb-85f6-6fac77c0c9b3.png) 但这种方式,虽然视觉上是实现了三角形,但实际上,隐藏的部分任然占据部分高度,需要将上方的宽度去掉 最终实现代码如下: ```css .border { width: 0; height: 0; border-style:solid; border-width: 0 50px 50px; border-color: transparent transparent #d9534f; } ``` 如果想要实现一个只有边框是空心的三角形,由于这里不能再使用`border`属性,所以最直接的方法是利用伪类新建一个小一点的三角形定位上去 ```css .border { width: 0; height: 0; border-style:solid; border-width: 0 50px 50px; border-color: transparent transparent #d9534f; position: relative; } .border:after{ content: ''; border-style:solid; border-width: 0 40px 40px; border-color: transparent transparent #96ceb4; position: absolute; top: 0; left: 0; } ``` 效果图如下所示: ![i](https://static.vue-js.com/59f4d720-a27a-11eb-85f6-6fac77c0c9b3.png) 伪类元素定位参照对象的内容区域宽高都为0,则内容区域即可以理解成中心一点,所以伪元素相对中心这点定位 将元素定位进行微调以及改变颜色,就能够完成下方效果图: ![](https://static.vue-js.com/653a6e10-a27a-11eb-85f6-6fac77c0c9b3.png) 最终代码如下: ```css .border:after { content: ''; border-style: solid; border-width: 0 40px 40px; border-color: transparent transparent #96ceb4; position: absolute; top: 6px; left: -40px; } ``` ## 三、原理分析 可以看到,边框是实现三角形的部分,边框实际上并不是一个直线,如果我们将四条边设置不同的颜色,将边框逐渐放大,可以得到每条边框都是一个梯形 ![](https://static.vue-js.com/78d4bd90-a27a-11eb-85f6-6fac77c0c9b3.png) 当分别取消边框的时候,发现下面几种情况: - 取消一条边的时候,与这条边相邻的两条边的接触部分会变成直的 - 当仅有邻边时, 两个边会变成对分的三角 - 当保留边没有其他接触时,极限情况所有东西都会消失 ![](https://static.vue-js.com/84586ef0-a27a-11eb-85f6-6fac77c0c9b3.png) 通过上图的变化规则,利用旋转、隐藏,以及设置内容宽高等属性,就能够实现其他类型的三角形 如设置直角三角形,如上图倒数第三行实现过程,我们就能知道整个实现原理 实现代码如下: ```css .box { /* 内部大小 */ width: 0px; height: 0px; /* 边框大小 只设置两条边*/ border-top: #4285f4 solid; border-right: transparent solid; border-width: 85px; /* 其他设置 */ margin: 50px; } ``` ## 参考文献 - https://www.cnblogs.com/echolun/p/11888612.html - https://juejin.cn/post/6844903567795421197 - https://vue3js.cn/interview ================================================ FILE: docs/css/visual_scrolling.md ================================================ # 面试官:如何使用css完成视差滚动效果? ![](https://static.vue-js.com/1b2d33e0-a18d-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 视差滚动(Parallax Scrolling)是指多层背景以不同的速度移动,形成立体的运动效果,带来非常出色的视觉体验 我们可以把网页解刨成:背景层、内容层、悬浮层 ![](https://static.vue-js.com/57c942a0-a1cc-11eb-85f6-6fac77c0c9b3.png) 当滚动鼠标滑轮的时候,各个图层以不同的速度移动,形成视觉差的效果 ![image.png](https://static.vue-js.com/e57ab280-a1dd-11eb-ab90-d9ae814b240d.png) ## 二、实现方式 使用`css`形式实现视觉差滚动效果的方式有: - background-attachment - transform:translate3D ### background-attachment 作用是设置背景图像是否固定或者随着页面的其余部分滚动 值分别有如下: - scroll:默认值,背景图像会随着页面其余部分的滚动而移动 - fixed:当页面的其余部分滚动时,背景图像不会移动 - inherit:继承父元素background-attachment属性的值 完成滚动视觉差就需要将`background-attachment`属性设置为`fixed`,让背景相对于视口固定。及时一个元素有滚动机制,背景也不会随着元素的内容而滚动 也就是说,背景一开始就已经被固定在初始的位置 核心的`css`代码如下: ```css section { height: 100vh; } .g-img { background-image: url(...); background-attachment: fixed; background-size: cover; background-position: center center; } ``` 整体例子如下: ```html
    1
    2
    3
    4
    5
    6
    7
    ``` ### transform:translate3D 同样,让我们先来看一下两个概念`transform`和`perspective`: - transform: css3 属性,可以对元素进行变换(2d/3d),包括平移 translate,旋转 rotate,缩放 scale,等等 - perspective: css3 属性,当元素涉及 3d 变换时,perspective 可以定义我们眼睛看到的 3d 立体效果,即空间感 `3D`视角示意图如下所示: ![](https://static.vue-js.com/24f37dd0-a18d-11eb-85f6-6fac77c0c9b3.png) 举个例子: ```html
    one
    two
    three
    ``` 而这种方式实现视觉差动的原理如下: - 容器设置上 transform-style: preserve-3d 和 perspective: xpx,那么处于这个容器的子元素就将位于3D空间中, - 子元素设置不同的 transform: translateZ(),这个时候,不同元素在 3D Z轴方向距离屏幕(我们的眼睛)的距离也就不一样 - 滚动滚动条,由于子元素设置了不同的 transform: translateZ(),那么他们滚动的上下距离 translateY 相对屏幕(我们的眼睛),也是不一样的,这就达到了滚动视差的效果 ## 参考文献 - https://imweb.io/topic/5b73ef73a56e07401e48729d - https://juejin.cn/post/6844903654458146823#heading-5 ================================================ FILE: docs/design/Factory Pattern.md ================================================ # 面试官:说说你对工厂模式的理解?应用场景? ![](https://static.vue-js.com/27a84d10-3bea-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 工厂模式是用来创建对象的一种最常用的设计模式,不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂 其就像工厂一样重复的产生类似的产品,工厂模式只需要我们传入正确的参数,就能生产类似的产品 举个例子: - 编程中,在一个 A 类中通过 new 的方式实例化了类 B,那么 A 类和 B 类之间就存在关联(耦合) - 后期因为需要修改了 B 类的代码和使用方式,比如构造函数中传入参数,那么 A 类也要跟着修改,一个类的依赖可能影响不大,但若有多个类依赖了 B 类,那么这个工作量将会相当的大,容易出现修改错误,也会产生很多的重复代码,这无疑是件非常痛苦的事; - 这种情况下,就需要将创建实例的工作从调用方(A类)中分离,与调用方**解耦**,也就是使用工厂方法创建实例的工作封装起来(**减少代码重复**),由工厂管理对象的创建逻辑,调用方不需要知道具体的创建过程,只管使用,**而降低调用者因为创建逻辑导致的错误**; ## 二、实现 工厂模式根据抽象程度的不同可以分为: - 简单工厂模式(Simple Factory) - 工厂方法模式(Factory Method) - 抽象工厂模式(Abstract Factory) ### 简单工厂模式 简单工厂模式也叫静态工厂模式,用一个工厂对象创建同一类对象类的实例 假设我们要开发一个公司岗位及其工作内容的录入信息,不同岗位的工作内容不一致 代码如下: ```js function Factory(career) { function User(career, work) { this.career = career this.work = work } let work switch(career) { case 'coder': work = ['写代码', '修Bug'] return new User(career, work) break case 'hr': work = ['招聘', '员工信息管理'] return new User(career, work) break case 'driver': work = ['开车'] return new User(career, work) break case 'boss': work = ['喝茶', '开会', '审批文件'] return new User(career, work) break } } let coder = new Factory('coder') console.log(coder) let boss = new Factory('boss') console.log(boss) ``` `Factory`就是一个简单工厂。当我们调用工厂函数时,只需要传递name、age、career就可以获取到包含用户工作内容的实例对象 ### 工厂方法模式 工厂方法模式跟简单工厂模式差不多,但是把具体的产品放到了工厂函数的`prototype`中 这样一来,扩展产品种类就不必修改工厂函数了,和心累就变成抽象类,也可以随时重写某种具体的产品 也就是相当于工厂总部不生产产品了,交给下辖分工厂进行生产;但是进入工厂之前,需要有个判断来验证你要生产的东西是否是属于我们工厂所生产范围,如果是,就丢给下辖工厂来进行生产 如下代码: ```js // 工厂方法 function Factory(career){ if(this instanceof Factory){ var a = new this[career](); return a; }else{ return new Factory(career); } } // 工厂方法函数的原型中设置所有对象的构造函数 Factory.prototype={ 'coder': function(){ this.careerName = '程序员' this.work = ['写代码', '修Bug'] }, 'hr': function(){ this.careerName = 'HR' this.work = ['招聘', '员工信息管理'] }, 'driver': function () { this.careerName = '司机' this.work = ['开车'] }, 'boss': function(){ this.careerName = '老板' this.work = ['喝茶', '开会', '审批文件'] } } let coder = new Factory('coder') console.log(coder) let hr = new Factory('hr') console.log(hr) ``` 工厂方法关键核心代码是工厂里面的判断this是否属于工厂,也就是做了分支判断,这个工厂只做我能做的产品 ### 抽象工厂模式 上述简单工厂模式和工厂方法模式都是直接生成实例,但是抽象工厂模式不同,抽象工厂模式并不直接生成实例, 而是用于对产品类簇的创建 通俗点来讲就是:简单工厂和工厂方法模式的工作是生产产品,那么抽象工厂模式的工作就是生产工厂的 由于`JavaScript`中并没有抽象类的概念,只能模拟,可以分成四部分: - 用于创建抽象类的函数 - 抽象类 - 具体类 - 实例化具体类 上面的例子中有`coder`、`hr`、`boss`、`driver`四种岗位,其中`coder`可能使用不同的开发语言进行开发,比如`JavaScript`、`Java`等等。那么这两种语言就是对应的类簇 示例代码如下: ```js let CareerAbstractFactory = function(subType, superType) { // 判断抽象工厂中是否有该抽象类 if (typeof CareerAbstractFactory[superType] === 'function') { // 缓存类 function F() {} // 继承父类属性和方法 F.prototype = new CareerAbstractFactory[superType]() // 将子类的constructor指向父类 subType.constructor = subType; // 子类原型继承父类 subType.prototype = new F() } else { throw new Error('抽象类不存在') } } ``` 上面代码中`CareerAbstractFactory`就是一个抽象工厂方法,该方法在参数中传递子类和父类,在方法体内部实现了子类对父类的继承 ## 三、应用场景 从上面可看到,简单简单工厂的优点就是我们只要传递正确的参数,就能获得所需的对象,而不需要关心其创建的具体细节 应用场景也容易识别,有构造函数的地方,就应该考虑简单工厂,但是如果函数构建函数太多与复杂,会导致工厂函数变得复杂,所以不适合复杂的情况 抽象工厂模式一般用于严格要求以面向对象思想进行开发的超大型项目中,我们一般常规的开发的话一般就是简单工厂和工厂方法模式会用的比较多一些 综上,工厂模式适用场景如下: - 如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择 - 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式; - 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性 ## 参考文献 - https://www.runoob.com/design-pattern/factory-pattern.html - https://juejin.cn/post/6844903653774458888 - https://zhuanlan.zhihu.com/p/344119981 ================================================ FILE: docs/design/Observer Pattern.md ================================================ # 面试官:说说你对发布订阅、观察者模式的理解?区别? ![](https://static.vue-js.com/342739f0-3fb1-11ec-8e64-91fdec0f05a1.png) ## 一、观察者模式 观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新 观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯 ![](https://static.vue-js.com/d3a80020-3f7c-11ec-a752-75723a64e8f5.png) 例如生活中,我们可以用报纸期刊的订阅来形象的说明,当你订阅了一份报纸,每天都会有一份最新的报纸送到你手上,有多少人订阅报纸,报社就会发多少份报纸 报社和订报纸的客户就形成了一对多的依赖关系 实现代码如下: 被观察者模式 ```js class Subject { constructor() { this.observerList = []; } addObserver(observer) { this.observerList.push(observer); } removeObserver(observer) { const index = this.observerList.findIndex(o => o.name === observer.name); this.observerList.splice(index, 1); } notifyObservers(message) { const observers = this.observeList; observers.forEach(observer => observer.notified(message)); } } ``` 观察者: ```h' class Observer { constructor(name, subject) { this.name = name; if (subject) { subject.addObserver(this); } } notified(message) { console.log(this.name, 'got message', message); } } ``` 使用代码如下: ```js const subject = new Subject(); const observerA = new Observer('observerA', subject); const observerB = new Observer('observerB'); subject.addObserver(observerB); subject.notifyObservers('Hello from subject'); subject.removeObserver(observerA); subject.notifyObservers('Hello again'); ``` 上述代码中,观察者主动申请加入被观察者的列表,被观察者主动将观察者加入列表 ## 二、发布订阅模式 发布-订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不同的类别,无需了解哪些订阅者(如果有的话)可能存在 同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在 ![](https://static.vue-js.com/e24d3cd0-3f7c-11ec-8e64-91fdec0f05a1.png) 实现代码如下: ```js class PubSub { constructor() { this.messages = {}; this.listeners = {}; } // 添加发布者 publish(type, content) { const existContent = this.messages[type]; if (!existContent) { this.messages[type] = []; } this.messages[type].push(content); } // 添加订阅者 subscribe(type, cb) { const existListener = this.listeners[type]; if (!existListener) { this.listeners[type] = []; } this.listeners[type].push(cb); } // 通知 notify(type) { const messages = this.messages[type]; const subscribers = this.listeners[type] || []; subscribers.forEach((cb, index) => cb(messages[index])); } } ``` 发布者代码如下: ```js class Publisher { constructor(name, context) { this.name = name; this.context = context; } publish(type, content) { this.context.publish(type, content); } } ``` 订阅者代码如下: ```js class Subscriber { constructor(name, context) { this.name = name; this.context = context; } subscribe(type, cb) { this.context.subscribe(type, cb); } } ``` 使用代码如下: ```js const TYPE_A = 'music'; const TYPE_B = 'movie'; const TYPE_C = 'novel'; const pubsub = new PubSub(); const publisherA = new Publisher('publisherA', pubsub); publisherA.publish(TYPE_A, 'we are young'); publisherA.publish(TYPE_B, 'the silicon valley'); const publisherB = new Publisher('publisherB', pubsub); publisherB.publish(TYPE_A, 'stronger'); const publisherC = new Publisher('publisherC', pubsub); publisherC.publish(TYPE_C, 'a brief history of time'); const subscriberA = new Subscriber('subscriberA', pubsub); subscriberA.subscribe(TYPE_A, res => { console.log('subscriberA received', res) }); const subscriberB = new Subscriber('subscriberB', pubsub); subscriberB.subscribe(TYPE_C, res => { console.log('subscriberB received', res) }); const subscriberC = new Subscriber('subscriberC', pubsub); subscriberC.subscribe(TYPE_B, res => { console.log('subscriberC received', res) }); pubsub.notify(TYPE_A); pubsub.notify(TYPE_B); pubsub.notify(TYPE_C); ``` 上述代码,发布者和订阅者需要通过发布订阅中心进行关联,发布者的发布动作和订阅者的订阅动作相互独立,无需关注对方,消息派发由发布订阅中心负责 ## 三、区别 两种设计模式思路是一样的,举个生活例子: - 观察者模式:某公司给自己员工发月饼发粽子,是由公司的行政部门发送的,这件事不适合交给第三方,原因是“公司”和“员工”是一个整体 - 发布-订阅模式:某公司要给其他人发各种快递,因为“公司”和“其他人”是独立的,其唯一的桥梁是“快递”,所以这件事适合交给第三方快递公司解决 上述过程中,如果公司自己去管理快递的配送,那公司就会变成一个快递公司,业务繁杂难以管理,影响公司自身的主营业务,因此使用何种模式需要考虑什么情况两者是需要耦合的 两者区别如下图: ![](https://files.mdnice.com/user/155/9141682c-7386-4f12-8412-fb17a1cd4bf6.png) - 在观察者模式中,观察者是知道Subject的,Subject一直保持对观察者进行记录。然而,在发布订阅模式中,发布者和订阅者不知道对方的存在。它们只有通过消息代理进行通信。 - 在发布订阅模式中,组件是松散耦合的,正好和观察者模式相反。 - 观察者模式大多数时候是同步的,比如当事件触发,Subject就会去调用观察者的方法。而发布-订阅模式大多数时候是异步的(使用消息队列) ## 参考文献 - https://zh.wikipedia.org/zh-hans/%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F - https://zh.wikipedia.org/wiki/%E5%8F%91%E5%B8%83/%E8%AE%A2%E9%98%85 - https://www.cnblogs.com/onepixel/p/10806891.html - https://juejin.cn/post/6978728619782701087 ================================================ FILE: docs/design/Proxy Pattern.md ================================================ # 面试官:说说你对代理模式的理解?应用场景? ![](https://static.vue-js.com/899a6ef0-3d6a-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 代理模式(Proxy Pattern)是为一个对象提供一个代用品或占位符,以便控制对它的访问 代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要时,提供一个替身对象来控制这个对象的访问,客户实际上访问的是替身对象 ![](https://static.vue-js.com/951c99b0-3d6a-11ec-a752-75723a64e8f5.png) 在生活中,代理模式的场景是十分常见的,例如我们现在如果有租房、买房的需求,更多的是去找链家等房屋中介机构,而不是直接寻找想卖房或出租房的人谈。此时,链家起到的作用就是代理的作用 ## 二、使用 在`ES6`中,存在`proxy`构建函数能够让我们轻松使用代理模式: ```js const proxy = new Proxy(target, handler); ``` 关于`Proxy`的使用可以翻看以前的文章 而按照功能来划分,`javascript`代理模式常用的有: - 缓存代理 - 虚拟代理 ### 缓存代理 缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果 如实现一个求积乘的函数,如下: ```js var muti = function () { console.log("开始计算乘积"); var a = 1; for (var i = 0, l = arguments.length; i < l; i++) { a = a * arguments[i]; } return a; }; ``` 现在加入缓存代理,如下: ```js var proxyMult = (function () { var cache = {}; return function () { var args = Array.prototype.join.call(arguments, ","); if (args in cache) { return cache[args]; } return (cache[args] = mult.apply(this, arguments)); }; })(); proxyMult(1, 2, 3, 4); // 输出:24 proxyMult(1, 2, 3, 4); // 输出:24 ``` 当第二次调用 `proxyMult(1, 2, 3, 4)` 时,本体 `mult` 函数并没有被计算,`proxyMult` 直接返回了之前缓存好的计算结果 ### 虚拟代理 虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建 常见的就是图片预加载功能: 未使用代理模式如下: ```js let MyImage = (function(){ let imgNode = document.createElement( 'img' ); document.body.appendChild( imgNode ); // 创建一个Image对象,用于加载需要设置的图片 let img = new Image; img.onload = function(){ // 监听到图片加载完成后,设置src为加载完成后的图片 imgNode.src = img.src; }; return { setSrc: function( src ){ // 设置图片的时候,设置为默认的loading图 imgNode.src = 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif'; // 把真正需要设置的图片传给Image对象的src属性 img.src = src; } } })(); MyImage.setSrc( 'https://xxx.jpg' ); ``` `MyImage`对象除了负责给`img`节点设置`src`外,还要负责预加载图片,违反了面向对象设计的原则——单一职责原则 上述过程`loding`则是耦合进`MyImage`对象里的,如果以后某个时候,我们不需要预加载显示loading这个功能了,就只能在`MyImage`对象里面改动代码 使用代理模式,代码则如下: ```js // 图片本地对象,负责往页面中创建一个img标签,并且提供一个对外的setSrc接口 let myImage = (function(){ let imgNode = document.createElement( 'img' ); document.body.appendChild( imgNode ); return { //setSrc接口,外界调用这个接口,便可以给该img标签设置src属性 setSrc: function( src ){ imgNode.src = src; } } })(); // 代理对象,负责图片预加载功能 let proxyImage = (function(){ // 创建一个Image对象,用于加载需要设置的图片 let img = new Image; img.onload = function(){ // 监听到图片加载完成后,给被代理的图片本地对象设置src为加载完成后的图片 myImage.setSrc( this.src ); } return { setSrc: function( src ){ // 设置图片时,在图片未被真正加载好时,以这张图作为loading,提示用户图片正在加载 myImage.setSrc( 'https://img.zcool.cn/community/01deed576019060000018c1bd2352d.gif' ); img.src = src; } } })(); proxyImage.setSrc( 'https://xxx.jpg' ); ``` 使用代理模式后,图片本地对象负责往页面中创建一个`img`标签,并且提供一个对外的`setSrc`接口; 代理对象负责在图片未加载完成之前,引入预加载的`loading`图,负责了图片预加载的功能 上述并没有改变或者增加`MyImage`的接口,但是通过代理对象,实际上给系统添加了新的行为 并且上述代理模式可以发现,代理和本体接口的一致性,如果有一天不需要预加载,那么就不需要代理对象,可以选择直接请求本体。其中关键是代理对象和本体都对外提供了 `setSrc` 方法 ‘ ## 三、应用场景 现在的很多前端框架或者状态管理框架都使用代理模式,用与监听变量的变化 使用代理模式代理对象的访问的方式,一般又被称为拦截器,比如我们在项目中经常使用 `Axios` 的实例来进行 HTTP 的请求,使用拦截器 `interceptor` 可以提前对 请求前的数据 服务器返回的数据进行一些预处理 以及上述应用到的缓存代理和虚拟代理 ## 参考文献 - https://juejin.cn/post/6844903555036364814#heading-2 - https://juejin.cn/post/6992510837403418654#heading-7 - https://sothx.com/2021/06/26/proxy/ ================================================ FILE: docs/design/Singleton Pattern.md ================================================ # 面试官:说说你对单例模式的理解?如何实现? ![](https://static.vue-js.com/7df7d830-3b2b-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 单例模式(Singleton Pattern):创建型模式,提供了一种创建对象的最佳方式,这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建 在应用程序运行期间,单例模式只会在全局作用域下创建一次实例对象,让所有需要调用的地方都共享这一单例对象,如下图所示: ![](https://static.vue-js.com/fa7898d0-3b2c-11ec-8e64-91fdec0f05a1.png) 从定义上来看,全局变量好像就是单例模式,但是一般情况我们不认为全局变量是一个单例模式,原因是: - 全局命名污染 - 不易维护,容易被重写覆盖 ## 二、实现 在`javascript`中,实现一个单例模式可以用一个变量来标志当前的类已经创建过对象,如果下次获取当前类的实例时,直接返回之前创建的对象即可,如下: ```JS // 定义一个类 function Singleton(name) { this.name = name; this.instance = null; } // 原型扩展类的一个方法getName() Singleton.prototype.getName = function() { console.log(this.name) }; // 获取类的实例 Singleton.getInstance = function(name) { if(!this.instance) { this.instance = new Singleton(name); } return this.instance }; // 获取对象1 const a = Singleton.getInstance('a'); // 获取对象2 const b = Singleton.getInstance('b'); // 进行比较 console.log(a === b); ``` 使用闭包也能够实现,如下: ```js function Singleton(name) { this.name = name; } // 原型扩展类的一个方法getName() Singleton.prototype.getName = function() { console.log(this.name) }; // 获取类的实例 Singleton.getInstance = (function() { var instance = null; return function(name) { if(!this.instance) { this.instance = new Singleton(name); } return this.instance } })(); // 获取对象1 const a = Singleton.getInstance('a'); // 获取对象2 const b = Singleton.getInstance('b'); // 进行比较 console.log(a === b); ``` 也可以将上述的方法稍作修改,变成构造函数的形式,如下: ```js // 单例构造函数 function CreateSingleton (name) { this.name = name; this.getName(); }; // 获取实例的名字 CreateSingleton.prototype.getName = function() { console.log(this.name) }; // 单例对象 const Singleton = (function(){ var instance; return function (name) { if(!instance) { instance = new CreateSingleton(name); } return instance; } })(); // 创建实例对象1 const a = new Singleton('a'); // 创建实例对象2 const b = new Singleton('b'); console.log(a===b); // true ``` ## 三、使用场景 在前端中,很多情况都是用到单例模式,例如页面存在一个模态框的时候,只有用户点击的时候才会创建,而不是加载完成之后再创建弹窗和隐藏,并且保证弹窗全局只有一个 可以先创建一个通常的获取对象的方法,如下: ```js const getSingle = function( fn ){ let result; return function(){ return result || ( result = fn .apply(this, arguments ) ); } }; ``` 创建弹窗的代码如下: ```js const createLoginLayer = function(){ var div = document.createElement( 'div' ); div.innerHTML = '我是浮窗'; div.style.display = 'none'; document.body.appendChild( div ); return div; }; const createSingleLoginLayer = getSingle( createLoginLayer ); document.getElementById( 'loginBtn' ).onclick = function(){ var loginLayer = createSingleLoginLayer(); loginLayer.style.display = 'block'; }; ``` 上述这种实现称为惰性单例,意图解决需要时才创建类实例对象 并且`Vuex`、`redux`全局态管理库也应用单例模式的思想,如下图: ![](https://static.vue-js.com/8be50f80-3b2b-11ec-a752-75723a64e8f5.png) 现在很多第三方库都是单例模式,多次引用只会使用同一个对象,如`jquery`、`lodash`、`moment`... ## 参考文献 - https://zh.wikipedia.org/zh-hans/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F - https://www.runoob.com/design-pattern/singleton-pattern.html - https://juejin.cn/post/6844903874210299912#heading-5 ================================================ FILE: docs/design/Strategy Pattern.md ================================================ # 面试官:说说你对策略模式的理解?应用场景? ![](https://static.vue-js.com/e4aad950-3cb2-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 策略模式(Strategy Pattern)指的是定义一系列的算法,把它们一个个封装起来,目的就是将算法的使用与算法的实现分离开来 一个基于策略模式的程序至少由两部分组成: - 策略类,策略类封装了具体的算法,并负责具体的计算过程 - 环境类Context,Context 接受客户的请求,随后 把请求委托给某一个策略类 ## 二、使用 举个例子,公司的年终奖是根据员工的工资和绩效来考核的,绩效为A的人,年终奖为工资的4倍,绩效为B的人,年终奖为工资的3倍,绩效为C的人,年终奖为工资的2倍 若使用`if`来实现,代码则如下: ```js var calculateBouns = function(salary,level) { if(level === 'A') { return salary * 4; } if(level === 'B') { return salary * 3; } if(level === 'C') { return salary * 2; } }; // 调用如下: console.log(calculateBouns(4000,'A')); // 16000 console.log(calculateBouns(2500,'B')); // 7500 ``` 从上述可有看到,函数内部包含过多`if...else`,并且后续改正的时候,需要在函数内部添加逻辑,违反了开放封闭原则 而如果使用策略模式,就是先定义一系列算法,把它们一个个封装起来,将不变的部分和变化的部分隔开,如下: ```js var obj = { "A": function(salary) { return salary * 4; }, "B" : function(salary) { return salary * 3; }, "C" : function(salary) { return salary * 2; } }; var calculateBouns =function(level,salary) { return obj[level](salary); }; console.log(calculateBouns('A',10000)); // 40000 ``` 上述代码中,`obj`对应的是策略类,而`calculateBouns`对应上下通信类 又比如实现一个表单校验的代码,常常会像如下写法: ```js var registerForm = document.getElementById("registerForm"); registerForm.onsubmit = function(){ if(registerForm.userName.value === '') { alert('用户名不能为空'); return; } if(registerForm.password.value.length < 6) { alert("密码的长度不能小于6位"); return; } if(!/(^1[3|5|8][0-9]{9}$)/.test(registerForm.phoneNumber.value)) { alert("手机号码格式不正确"); return; } } ``` 上述代码包含多处`if`语句,并且违反了开放封闭原则,如果应用中还有其他的表单,需要重复编写代码 此处也可以使用策略模式进行重构校验,第一步确定不变的内容,即策略规则对象,如下: ```js var strategy = { isNotEmpty: function(value,errorMsg) { if(value === '') { return errorMsg; } }, // 限制最小长度 minLength: function(value,length,errorMsg) { if(value.length < length) { return errorMsg; } }, // 手机号码格式 mobileFormat: function(value,errorMsg) { if(!/(^1[3|5|8][0-9]{9}$)/.test(value)) { return errorMsg; } } }; ``` 然后找出变的地方,作为环境类`context`,负责接收用户的要求并委托给策略规则对象,如下`Validator`类: ```js var Validator = function(){ this.cache = []; // 保存效验规则 }; Validator.prototype.add = function(dom,rule,errorMsg) { var str = rule.split(":"); this.cache.push(function(){ // str 返回的是 minLength:6 var strategy = str.shift(); str.unshift(dom.value); // 把input的value添加进参数列表 str.push(errorMsg); // 把errorMsg添加进参数列表 return strategys[strategy].apply(dom,str); }); }; Validator.prototype.start = function(){ for(var i = 0, validatorFunc; validatorFunc = this.cache[i++]; ) { var msg = validatorFunc(); // 开始效验 并取得效验后的返回信息 if(msg) { return msg; } } }; ``` 通过`validator.add`方法添加校验规则和错误信息提示,使用如下: ```js var validateFunc = function(){ var validator = new Validator(); // 创建一个Validator对象 /* 添加一些效验规则 */ validator.add(registerForm.userName,'isNotEmpty','用户名不能为空'); validator.add(registerForm.password,'minLength:6','密码长度不能小于6位'); validator.add(registerForm.userName,'mobileFormat','手机号码格式不正确'); var errorMsg = validator.start(); // 获得效验结果 return errorMsg; // 返回效验结果 }; var registerForm = document.getElementById("registerForm"); registerForm.onsubmit = function(){ var errorMsg = validateFunc(); if(errorMsg){ alert(errorMsg); return false; } } ``` 上述通过策略模式完成表单的验证,并且可以随时调用,在修改表单验证规则的时候,也非常方便,通过传递参数即可调用 ## 三、应用场景 从上面可以看到,使用策略模式的优点有如下: - 策略模式利用组合,委托等技术和思想,有效的避免很多if条件语句 - 策略模式提供了开放-封闭原则,使代码更容易理解和扩展 - 策略模式中的代码可以复用 策略模式不仅仅用来封装算法,在实际开发中,通常会把算法的含义扩散开来,使策略模式也可以用来封装 一系列的“业务规则” 只要这些业务规则指向的目标一致,并且可以被替换使用,我们就可以用策略模式来封装它们 ## 参考文献 - https://segmentfault.com/a/1190000021883055 - https://juejin.cn/post/6844903504109109262 - https://juejin.cn/post/6844903751225081864 ================================================ FILE: docs/design/design.md ================================================ # 面试官:说说对设计模式的理解?常见的设计模式有哪些? ![](https://static.vue-js.com/065bc170-37ce-11ec-a752-75723a64e8f5.png) ## 一、是什么 在软件工程中,设计模式是对软件设计中普遍存在的各种问题所提出的解决方案 设计模式并不直接用来完成代码的编写,而是描述在各种不同情况下,要怎么解决问题的一种方案 设计模式能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免会引起麻烦的紧耦合,以增强软件设计面对并适应变化的能力 因此,当我们遇到合适的场景时,我们可能会条件反射一样自然而然想到符合这种场景的设计模式 比如,当系统中某个接口的结构已经无法满足我们现在的业务需求,但又不能改动这个接口,因为可能原来的系统很多功能都依赖于这个接口,改动接口会牵扯到太多文件 因此应对这种场景,我们可以很快地想到可以用适配器模式来解决这个问题 ## 二、有哪些 常见的设计模式有: - 单例模式 - 工厂模式 - 策略模式 - 代理模式 - 中介者模式 - 装饰者模式 - ...... ### 单例模式 保证一个类仅有一个实例,并提供一个访问它的全局访问点。实现的方法为先判断实例存在与否,如果存在则直接返回,如果不存在就创建了再返回,这就确保了一个类只有一个实例对象 如下图的车,只有一辆,一旦借出去则不能再借给别人: ![](https://static.vue-js.com/ea527aa0-37cd-11ec-8e64-91fdec0f05a1.png) ### 工厂模式 工厂模式通常会分成3个角色: - 工厂角色-负责实现创建所有实例的内部逻辑. - 抽象产品角色-是所创建的所有对象的父类,负责描述所有实例所共有的公共接口 - 具体产品角色-是创建目标,所有创建的对象都充当这个角色的某个具体类的实例 ![](https://static.vue-js.com/fadd1920-37cd-11ec-8e64-91fdec0f05a1.png) ### 策略模式 策略模式,就是定义一系列的算法,把他们一个个封装起来,并且使他们可以相互替换 至少分成两部分: - 策略类(可变),策略类封装了具体的算法,并负责具体的计算过程 - 环境类(不变),接受客户的请求,随后将请求委托给某一个策略类 ### 代理模式 代理模式:为对象提供一个代用品或占位符,以便控制对它的访问 例如实现图片懒加载的功能,先通过一张`loading`图占位,然后通过异步的方式加载图片,等图片加载好了再把完成的图片加载到`img`标签里面 ### 中介者模式 中介者模式的定义:通过一个中介者对象,其他所有的相关对象都通过该中介者对象来通信,而不是相互引用,当其中的一个对象发生改变时,只需要通知中介者对象即可 通过中介者模式可以解除对象与对象之间的紧耦合关系 ### 装饰者模式 装饰者模式的定义:在不改变对象自身的基础上,在程序运行期间给对象动态地添加方法 通常运用在原有方法维持不变,在原有方法上再挂载其他方法来满足现有需求 ## 三、总结 不断去学习设计模式,会对我们有着极大的帮助,主要如下: - 从许多优秀的软件系统中总结出的成功的、能够实现可维护性、复用的设计方案,使用这些方案将可以让我们避免做一些重复性的工作 - 设计模式提供了一套通用的设计词汇和一种通用的形式来方便开发人员之间沟通和交流,使得设计方案更加通俗易懂 - 大部分设计模式都兼顾了系统的可重用性和可扩展性,这使得我们可以更好地重用一些已有的设计方案、功能模块甚至一个完整的软件系统,避免我们经常做一些重复的设计、编写一些重复的代码 - 合理使用设计模式并对设计模式的使用情况进行文档化,将有助于别人更快地理解系统 - 学习设计模式将有助于初学者更加深入地理解面向对象思想 ## 参考文献 - https://zh.wikipedia.org/wiki/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F_(%E8%AE%A1%E7%AE%97%E6%9C%BA) - https://juejin.cn/post/6844903795017646094 - https://segmentfault.com/a/1190000030850326 ================================================ FILE: docs/es6/array.md ================================================ # 面试官:ES6中数组新增了哪些扩展? ![](https://static.vue-js.com/a156b8d0-53c5-11eb-85f6-6fac77c0c9b3.png) ## 一、扩展运算符的应用 ES6通过扩展元素符`...`,好比 `rest` 参数的逆运算,将一个数组转为用逗号分隔的参数序列 ```js console.log(...[1, 2, 3]) // 1 2 3 console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5 [...document.querySelectorAll('div')] // [
    ,
    ,
    ] ``` 主要用于函数调用的时候,将一个数组变为参数序列 ```js function push(array, ...items) { array.push(...items); } function add(x, y) { return x + y; } const numbers = [4, 38]; add(...numbers) // 42 ``` 可以将某些数据结构转为数组 ```js [...document.querySelectorAll('div')] ``` 能够更简单实现数组复制 ```js const a1 = [1, 2]; const [...a2] = a1; // [1,2] ``` 数组的合并也更为简洁了 ```js const arr1 = ['a', 'b']; const arr2 = ['c']; const arr3 = ['d', 'e']; [...arr1, ...arr2, ...arr3] // [ 'a', 'b', 'c', 'd', 'e' ] ``` 注意:通过扩展运算符实现的是浅拷贝,修改了引用指向的值,会同步反映到新数组 下面看个例子就清楚多了 ```js const arr1 = ['a', 'b',[1,2]]; const arr2 = ['c']; const arr3 = [...arr1,...arr2] arr[1][0] = 9999 // 修改arr1里面数组成员值 console.log(arr[3]) // 影响到arr3,['a','b',[9999,2],'c'] ``` 扩展运算符可以与解构赋值结合起来,用于生成数组 ```js const [first, ...rest] = [1, 2, 3, 4, 5]; first // 1 rest // [2, 3, 4, 5] const [first, ...rest] = []; first // undefined rest // [] const [first, ...rest] = ["foo"]; first // "foo" rest // [] ``` 如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错 ```js const [...butLast, last] = [1, 2, 3, 4, 5]; // 报错 const [first, ...middle, last] = [1, 2, 3, 4, 5]; // 报错 ``` 可以将字符串转为真正的数组 ```javascript [...'hello'] // [ "h", "e", "l", "l", "o" ] ``` 定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组 ```js let nodeList = document.querySelectorAll('div'); let array = [...nodeList]; let map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); let arr = [...map.keys()]; // [1, 2, 3] ``` 如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错 ```javascript const obj = {a: 1, b: 2}; let arr = [...obj]; // TypeError: Cannot spread non-iterable object ``` ## 二、构造函数新增的方法 关于构造函数,数组新增的方法有如下: - Array.from() - Array.of() ### Array.from() 将两类对象转为真正的数组:类似数组的对象和可遍历`(iterable)`的对象(包括 `ES6` 新增的数据结构 `Set` 和 `Map`) ```js let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 }; let arr2 = Array.from(arrayLike); // ['a', 'b', 'c'] ``` 还可以接受第二个参数,用来对每个元素进行处理,将处理后的值放入返回的数组 ```js Array.from([1, 2, 3], (x) => x * x) // [1, 4, 9] ``` ### Array.of() 用于将一组值,转换为数组 ```js Array.of(3, 11, 8) // [3,11,8] ``` 没有参数的时候,返回一个空数组 当参数只有一个的时候,实际上是指定数组的长度 参数个数不少于 2 个时,`Array()`才会返回由参数组成的新数组 ```js Array() // [] Array(3) // [, , ,] Array(3, 11, 8) // [3, 11, 8] ``` ### 三、实例对象新增的方法 关于数组实例对象新增的方法有如下: - copyWithin() - find()、findIndex() - fill() - entries(),keys(),values() - includes() - flat(),flatMap() ### copyWithin() 将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组 参数如下: - target(必需):从该位置开始替换数据。如果为负值,表示倒数。 - start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。 - end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。 ```js [1, 2, 3, 4, 5].copyWithin(0, 3) // 将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2 // [4, 5, 3, 4, 5] ``` ### find()、findIndex() `find()`用于找出第一个符合条件的数组成员 参数是一个回调函数,接受三个参数依次为当前的值、当前的位置和原数组 ```js [1, 5, 10, 15].find(function(value, index, arr) { return value > 9; }) // 10 ``` `findIndex`返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回`-1` ```javascript [1, 5, 10, 15].findIndex(function(value, index, arr) { return value > 9; }) // 2 ``` 这两个方法都可以接受第二个参数,用来绑定回调函数的`this`对象。 ```js function f(v){ return v > this.age; } let person = {name: 'John', age: 20}; [10, 12, 26, 15].find(f, person); // 26 ``` ### fill() 使用给定值,填充一个数组 ```javascript ['a', 'b', 'c'].fill(7) // [7, 7, 7] new Array(3).fill(7) // [7, 7, 7] ``` 还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置 ```js ['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c'] ``` 注意,如果填充的类型为对象,则是浅拷贝 ### entries(),keys(),values() `keys()`是对键名的遍历、`values()`是对键值的遍历,`entries()`是对键值对的遍历 ```js or (let index of ['a', 'b'].keys()) { console.log(index); } // 0 // 1 for (let elem of ['a', 'b'].values()) { console.log(elem); } // 'a' // 'b' for (let [index, elem] of ['a', 'b'].entries()) { console.log(index, elem); } // 0 "a" ``` ### includes() 用于判断数组是否包含给定的值 ```js [1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false [1, 2, NaN].includes(NaN) // true ``` 方法的第二个参数表示搜索的起始位置,默认为`0` 参数为负数则表示倒数的位置 ```js [1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true ``` ### flat(),flatMap() 将数组扁平化处理,返回一个新数组,对原数据没有影响 ```js [1, 2, [3, 4]].flat() // [1, 2, 3, 4] ``` `flat()`默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将`flat()`方法的参数写成一个整数,表示想要拉平的层数,默认为1 ```js [1, 2, [3, [4, 5]]].flat() // [1, 2, 3, [4, 5]] [1, 2, [3, [4, 5]]].flat(2) // [1, 2, 3, 4, 5] ``` `flatMap()`方法对原数组的每个成员执行一个函数相当于执行`Array.prototype.map()`,然后对返回值组成的数组执行`flat()`方法。该方法返回一个新数组,不改变原数组 ```js // 相当于 [[2, 4], [3, 6], [4, 8]].flat() [2, 3, 4].flatMap((x) => [x, x * 2]) // [2, 4, 3, 6, 4, 8] ``` `flatMap()`方法还可以有第二个参数,用来绑定遍历函数里面的`this` ### 四、数组的空位 数组的空位指,数组的某一个位置没有任何值 ES6 则是明确将空位转为`undefined`,包括`Array.from`、扩展运算符、`copyWithin()`、`fill()`、`entries()`、`keys()`、`values()`、`find()`和`findIndex()` 建议大家在日常书写中,避免出现空位 ### 五、排序稳定性 将`sort()`默认设置为稳定的排序算法 ```js const arr = [ 'peach', 'straw', 'apple', 'spork' ]; const stableSorting = (s1, s2) => { if (s1[0] < s2[0]) return -1; return 1; }; arr.sort(stableSorting) // ["apple", "peach", "straw", "spork"] ``` 排序结果中,`straw`在`spork`的前面,跟原始顺序一致 ## 参考文献 - https://es6.ruanyifeng.com/#docs/array ================================================ FILE: docs/es6/decorator.md ================================================ # 面试官:你是怎么理解ES6中 Decorator 的?使用场景? ![](https://static.vue-js.com/7df43560-5ba5-11eb-85f6-6fac77c0c9b3.png) ## 一、介绍 Decorator,即装饰器,从名字上很容易让我们联想到装饰者模式 简单来讲,装饰者模式就是一种在不改变原类和使用继承的情况下,动态地扩展对象功能的设计理论。 `ES6`中`Decorator`功能亦如此,其本质也不是什么高大上的结构,就是一个普通的函数,用于扩展类属性和类方法 这里定义一个士兵,这时候他什么装备都没有 ```js class soldier{ } ``` 定义一个得到 AK 装备的函数,即装饰器 ```js function strong(target){ target.AK = true } ``` 使用该装饰器对士兵进行增强 ```js @strong class soldier{ } ``` 这时候士兵就有武器了 ```js soldier.AK // true ``` 上述代码虽然简单,但也能够清晰看到了使用`Decorator`两大优点: - 代码可读性变强了,装饰器命名相当于一个注释 - 在不改变原有代码情况下,对原来功能进行扩展 ## 二、用法 `Docorator`修饰对象为下面两种: - 类的装饰 - 类属性的装饰 ### 类的装饰 当对类本身进行装饰的时候,能够接受一个参数,即类本身 将装饰器行为进行分解,大家能够有个更深入的了解 ```js @decorator class A {} // 等同于 class A {} A = decorator(A) || A; ``` 下面`@testable`就是一个装饰器,`target`就是传入的类,即`MyTestableClass`,实现了为类添加静态属性 ```js @testable class MyTestableClass { // ... } function testable(target) { target.isTestable = true; } MyTestableClass.isTestable // true ``` 如果想要传递参数,可以在装饰器外层再封装一层函数 ```js function testable(isTestable) { return function(target) { target.isTestable = isTestable; } } @testable(true) class MyTestableClass {} MyTestableClass.isTestable // true @testable(false) class MyClass {} MyClass.isTestable // false ``` ### 类属性的装饰 当对类属性进行装饰的时候,能够接受三个参数: - 类的原型对象 - 需要装饰的属性名 - 装饰属性名的描述对象 首先定义一个`readonly`装饰器 ```js function readonly(target, name, descriptor){ descriptor.writable = false; // 将可写属性设为false return descriptor; } ``` 使用`readonly`装饰类的`name`方法 ```javascript class Person { @readonly name() { return `${this.first} ${this.last}` } } ``` 相当于以下调用 ```js readonly(Person.prototype, 'name', descriptor); ``` 如果一个方法有多个装饰器,就像洋葱一样,先从外到内进入,再由内到外执行 ```javascript function dec(id){ console.log('evaluated', id); return (target, property, descriptor) =>console.log('executed', id); } class Example { @dec(1) @dec(2) method(){} } // evaluated 1 // evaluated 2 // executed 2 // executed 1 ``` 外层装饰器`@dec(1)`先进入,但是内层装饰器`@dec(2)`先执行 ### 注意 装饰器不能用于修饰函数,因为函数存在变量声明情况 ```js var counter = 0; var add = function () { counter++; }; @add function foo() { } ``` 编译阶段,变成下面 ```js var counter; var add; @add function foo() { } counter = 0; add = function () { counter++; }; ``` 意图是执行后`counter`等于 1,但是实际上结果是`counter`等于 0 ## 三、使用场景 基于`Decorator`强大的作用,我们能够完成各种场景的需求,下面简单列举几种: 使用`react-redux`的时候,如果写成下面这种形式,既不雅观也很麻烦 ```js class MyReactComponent extends React.Component {} export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent); ``` 通过装饰器就变得简洁多了 ```js @connect(mapStateToProps, mapDispatchToProps) export default class MyReactComponent extends React.Component {} ``` 将`mixins`,也可以写成装饰器,让使用更为简洁了 ```js function mixins(...list) { return function (target) { Object.assign(target.prototype, ...list); }; } // 使用 const Foo = { foo() { console.log('foo') } }; @mixins(Foo) class MyClass {} let obj = new MyClass(); obj.foo() // "foo" ``` 下面再讲讲`core-decorators.js`几个常见的装饰器 #### @antobind `autobind`装饰器使得方法中的`this`对象,绑定原始对象 ```javascript import { autobind } from 'core-decorators'; class Person { @autobind getPerson() { return this; } } let person = new Person(); let getPerson = person.getPerson; getPerson() === person; // true ``` #### @readonly `readonly`装饰器使得属性或方法不可写 ```javascript import { readonly } from 'core-decorators'; class Meal { @readonly entree = 'steak'; } var dinner = new Meal(); dinner.entree = 'salmon'; // Cannot assign to read only property 'entree' of [object Object] ``` #### @deprecate `deprecate`或`deprecated`装饰器在控制台显示一条警告,表示该方法将废除 ```javascript import { deprecate } from 'core-decorators'; class Person { @deprecate facepalm() {} @deprecate('功能废除了') facepalmHard() {} } let person = new Person(); person.facepalm(); // DEPRECATION Person#facepalm: This function will be removed in future versions. person.facepalmHard(); // DEPRECATION Person#facepalmHard: 功能废除了 ``` ## 参考文献 - https://es6.ruanyifeng.com/#docs/decorator ================================================ FILE: docs/es6/function.md ================================================ # 面试官:对象新增了哪些扩展? ![](https://static.vue-js.com/54a04a10-5569-11eb-85f6-6fac77c0c9b3.png) ## 一、参数 `ES6`允许为函数的参数设置默认值 ```js function log(x, y = 'World') { console.log(x, y); } console.log('Hello') // Hello World console.log('Hello', 'China') // Hello China console.log('Hello', '') // Hello ``` 函数的形参是默认声明的,不能使用`let`或`const`再次声明 ```js function foo(x = 5) { let x = 1; // error const x = 2; // error } ``` 参数默认值可以与解构赋值的默认值结合起来使用 ```js function foo({x, y = 5}) { console.log(x, y); } foo({}) // undefined 5 foo({x: 1}) // 1 5 foo({x: 1, y: 2}) // 1 2 foo() // TypeError: Cannot read property 'x' of undefined ``` 上面的`foo`函数,当参数为对象的时候才能进行解构,如果没有提供参数的时候,变量`x`和`y`就不会生成,从而报错,这里设置默认值避免 ```js function foo({x, y = 5} = {}) { console.log(x, y); } foo() // undefined 5 ``` 参数默认值应该是函数的尾参数,如果不是非尾部的参数设置默认值,实际上这个参数是没发省略的 ```javascript function f(x = 1, y) { return [x, y]; } f() // [1, undefined] f(2) // [2, undefined] f(, 1) // 报错 f(undefined, 1) // [1, 1] ``` ## 二、属性 ### 函数的length属性 `length`将返回没有指定默认值的参数个数 ```js (function (a) {}).length // 1 (function (a = 5) {}).length // 0 (function (a, b, c = 5) {}).length // 2 ``` `rest` 参数也不会计入`length`属性 ```js (function(...args) {}).length // 0 ``` 如果设置了默认值的参数不是尾参数,那么`length`属性也不再计入后面的参数了 ```js (function (a = 0, b, c) {}).length // 0 (function (a, b = 1, c) {}).length // 1 ``` ### name属性 返回该函数的函数名 ```js var f = function () {}; // ES5 f.name // "" // ES6 f.name // "f" ``` 如果将一个具名函数赋值给一个变量,则 `name`属性都返回这个具名函数原本的名字 ```js const bar = function baz() {}; bar.name // "baz" ``` `Function`构造函数返回的函数实例,`name`属性的值为`anonymous` ```javascript (new Function).name // "anonymous" ``` `bind`返回的函数,`name`属性值会加上`bound`前缀 ```javascript function foo() {}; foo.bind({}).name // "bound foo" (function(){}).bind({}).name // "bound " ``` ## 三、作用域 一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域 等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的 下面例子中,`y=x`会形成一个单独作用域,`x`没有被定义,所以指向全局变量`x` ```js let x = 1; function f(y = x) { // 等同于 let y = x let x = 2; console.log(y); } f() // 1 ``` ## 四、严格模式 只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错 ```js // 报错 function doSomething(a, b = a) { 'use strict'; // code } // 报错 const doSomething = function ({a, b}) { 'use strict'; // code }; // 报错 const doSomething = (...a) => { 'use strict'; // code }; const obj = { // 报错 doSomething({a, b}) { 'use strict'; // code } }; ``` ## 五、箭头函数 使用“箭头”(`=>`)定义函数 ```js var f = v => v; // 等同于 var f = function (v) { return v; }; ``` 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分 ```js var f = () => 5; // 等同于 var f = function () { return 5 }; var sum = (num1, num2) => num1 + num2; // 等同于 var sum = function(num1, num2) { return num1 + num2; }; ``` 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用`return`语句返回 ```js var sum = (num1, num2) => { return num1 + num2; } ``` 如果返回对象,需要加括号将对象包裹 ```js let getTempItem = id => ({ id: id, name: "Temp" }); ``` 注意点: - 函数体内的`this`对象,就是定义时所在的对象,而不是使用时所在的对象 - 不可以当作构造函数,也就是说,不可以使用`new`命令,否则会抛出一个错误 - 不可以使用`arguments`对象,该对象在函数体内不存在。如果要用,可以用 `rest` 参数代替 - 不可以使用`yield`命令,因此箭头函数不能用作 Generator 函数 ## 参考文献 - https://es6.ruanyifeng.com/#docs/function ================================================ FILE: docs/es6/generator.md ================================================ # 面试官:你是怎么理解ES6中 Generator的?使用场景? ![](https://static.vue-js.com/7db499b0-5947-11eb-ab90-d9ae814b240d.png) ## 一、介绍 Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同 回顾下上文提到的解决异步的手段: - 回调函数 - promise 那么,上文我们提到`promsie`已经是一种比较流行的解决异步方案,那么为什么还出现`Generator`?甚至`async/await`呢? 该问题我们留在后面再进行分析,下面先认识下`Generator` ### Generator函数 执行 `Generator` 函数会返回一个遍历器对象,可以依次遍历 `Generator` 函数内部的每一个状态 形式上,`Generator `函数是一个普通函数,但是有两个特征: - `function`关键字与函数名之间有一个星号 - 函数体内部使用`yield`表达式,定义不同的内部状态 ```javascript function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } ``` ## 二、使用 `Generator` 函数会返回一个遍历器对象,即具有`Symbol.iterator`属性,并且返回给自己 ```javascript function* gen(){ // some code } var g = gen(); g[Symbol.iterator]() === g // true ``` 通过`yield`关键字可以暂停`generator`函数返回的遍历器对象的状态 ```javascript function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator(); ``` 上述存在三个状态:`hello`、`world`、`return` 通过`next`方法才会遍历到下一个内部状态,其运行逻辑如下: - 遇到`yield`表达式,就暂停执行后面的操作,并将紧跟在`yield`后面的那个表达式的值,作为返回的对象的`value`属性值。 - 下一次调用`next`方法时,再继续往下执行,直到遇到下一个`yield`表达式 - 如果没有再遇到新的`yield`表达式,就一直运行到函数结束,直到`return`语句为止,并将`return`语句后面的表达式的值,作为返回的对象的`value`属性值。 - 如果该函数没有`return`语句,则返回的对象的`value`属性值为`undefined` ```javascript hw.next() // { value: 'hello', done: false } hw.next() // { value: 'world', done: false } hw.next() // { value: 'ending', done: true } hw.next() // { value: undefined, done: true } ``` `done`用来判断是否存在下个状态,`value`对应状态值 `yield`表达式本身没有返回值,或者说总是返回`undefined` 通过调用`next`方法可以带一个参数,该参数就会被当作上一个`yield`表达式的返回值 ```javascript function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); } var a = foo(5); a.next() // Object{value:6, done:false} a.next() // Object{value:NaN, done:false} a.next() // Object{value:NaN, done:true} var b = foo(5); b.next() // { value:6, done:false } b.next(12) // { value:8, done:false } b.next(13) // { value:42, done:true } ``` 正因为`Generator `函数返回`Iterator`对象,因此我们还可以通过`for...of`进行遍历 ```javascript function* foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6; } for (let v of foo()) { console.log(v); } // 1 2 3 4 5 ``` 原生对象没有遍历接口,通过`Generator `函数为它加上这个接口,就能使用`for...of`进行遍历了 ```javascript function* objectEntries(obj) { let propKeys = Reflect.ownKeys(obj); for (let propKey of propKeys) { yield [propKey, obj[propKey]]; } } let jane = { first: 'Jane', last: 'Doe' }; for (let [key, value] of objectEntries(jane)) { console.log(`${key}: ${value}`); } // first: Jane // last: Doe ``` ## 三、异步解决方案 回顾之前展开异步解决的方案: - 回调函数 - Promise 对象 - generator 函数 - async/await 这里通过文件读取案例,将几种解决异步的方案进行一个比较: ### 回调函数 所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,再调用这个函数 ```javascript fs.readFile('/etc/fstab', function (err, data) { if (err) throw err; console.log(data); fs.readFile('/etc/shells', function (err, data) { if (err) throw err; console.log(data); }); }); ``` `readFile`函数的第三个参数,就是回调函数,等到操作系统返回了`/etc/passwd`这个文件以后,回调函数才会执行 ### Promise `Promise`就是为了解决回调地狱而产生的,将回调函数的嵌套,改成链式调用 ```js const fs = require('fs'); const readFile = function (fileName) { return new Promise(function (resolve, reject) { fs.readFile(fileName, function(error, data) { if (error) return reject(error); resolve(data); }); }); }; readFile('/etc/fstab').then(data =>{ console.log(data) return readFile('/etc/shells') }).then(data => { console.log(data) }) ``` 这种链式操作形式,使异步任务的两段执行更清楚了,但是也存在了很明显的问题,代码变得冗杂了,语义化并不强 ### generator `yield`表达式可以暂停函数执行,`next`方法用于恢复函数执行,这使得`Generator`函数非常适合将异步任务同步化 ```javascript const gen = function* () { const f1 = yield readFile('/etc/fstab'); const f2 = yield readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; ``` ### async/await 将上面`Generator`函数改成`async/await`形式,更为简洁,语义化更强了 ```js const asyncReadFile = async function () { const f1 = await readFile('/etc/fstab'); const f2 = await readFile('/etc/shells'); console.log(f1.toString()); console.log(f2.toString()); }; ``` ### 区别: 通过上述代码进行分析,将`promise`、`Generator`、`async/await`进行比较: - `promise`和`async/await`是专门用于处理异步操作的 - `Generator`并不是为异步而设计出来的,它还有其他功能(对象迭代、控制输出、部署`Interator`接口...) - `promise`编写代码相比`Generator`、`async`更为复杂化,且可读性也稍差 - `Generator`、`async`需要与`promise`对象搭配处理异步情况 - `async`实质是`Generator`的语法糖,相当于会自动执行`Generator`函数 - `async`使用上更为简洁,将异步代码以同步的形式进行编写,是处理异步编程的最终方案 ## 四、使用场景 `Generator`是异步解决的一种方案,最大特点则是将异步操作同步化表达出来 ```js function* loadUI() { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen(); } var loader = loadUI(); // 加载UI loader.next() // 卸载UI loader.next() ``` 包括`redux-saga `中间件也充分利用了`Generator`特性 ```js import { call, put, takeEvery, takeLatest } from 'redux-saga/effects' import Api from '...' function* fetchUser(action) { try { const user = yield call(Api.fetchUser, action.payload.userId); yield put({type: "USER_FETCH_SUCCEEDED", user: user}); } catch (e) { yield put({type: "USER_FETCH_FAILED", message: e.message}); } } function* mySaga() { yield takeEvery("USER_FETCH_REQUESTED", fetchUser); } function* mySaga() { yield takeLatest("USER_FETCH_REQUESTED", fetchUser); } export default mySaga; ``` 还能利用`Generator`函数,在对象上实现`Iterator`接口 ```js function* iterEntries(obj) { let keys = Object.keys(obj); for (let i=0; i < keys.length; i++) { let key = keys[i]; yield [key, obj[key]]; } } let myObj = { foo: 3, bar: 7 }; for (let [key, value] of iterEntries(myObj)) { console.log(key, value); } // foo 3 // bar 7 ``` ## 参考文献 - https://es6.ruanyifeng.com/#docs/generator-async ================================================ FILE: docs/es6/module.md ================================================ # 面试官:你是怎么理解ES6中Module的?使用场景? ![](https://static.vue-js.com/b6d19be0-5adb-11eb-ab90-d9ae814b240d.png) ## 一、介绍 模块,(Module),是能够单独命名并独立地完成一定功能的程序语句的**集合(即程序代码和数据结构的集合体)**。 两个基本的特征:外部特征和内部特征 - 外部特征是指模块跟外部环境联系的接口(即其他模块或程序调用该模块的方式,包括有输入输出参数、引用的全局变量)和模块的功能 - 内部特征是指模块的内部环境具有的特点(即该模块的局部数据和程序代码) ### 为什么需要模块化 - 代码抽象 - 代码封装 - 代码复用 - 依赖管理 如果没有模块化,我们代码会怎样? - 变量和方法不容易维护,容易污染全局作用域 - 加载资源的方式通过script标签从上到下。 - 依赖的环境主观逻辑偏重,代码较多就会比较复杂。 - 大型项目资源难以维护,特别是多人合作的情况下,资源的引入会让人奔溃 因此,需要一种将` JavaScript `程序模块化的机制,如 - CommonJs (典型代表:node.js早期) - AMD (典型代表:require.js) - CMD (典型代表:sea.js) ### AMD `Asynchronous ModuleDefinition`(AMD),异步模块定义,采用异步方式加载模块。所有依赖模块的语句,都定义在一个回调函数中,等到模块加载完成之后,这个回调函数才会运行 代表库为`require.js` ```js /** main.js 入口文件/主模块 **/ // 首先用config()指定各模块路径和引用名 require.config({ baseUrl: "js/lib", paths: { "jquery": "jquery.min", //实际路径为js/lib/jquery.min.js "underscore": "underscore.min", } }); // 执行基本操作 require(["jquery","underscore"],function($,_){ // some code here }); ``` ### CommonJs `CommonJS` 是一套 `Javascript` 模块规范,用于服务端 ```js // a.js module.exports={ foo , bar} // b.js const { foo,bar } = require('./a.js') ``` 其有如下特点: - 所有代码都运行在模块作用域,不会污染全局作用域 - 模块是同步加载的,即只有加载完成,才能执行后面的操作 - 模块在首次执行后就会缓存,再次加载只返回缓存结果,如果想要再次执行,可清除缓存 - `require`返回的值是被输出的值的拷贝,模块内部的变化也不会影响这个值 既然存在了`AMD`以及`CommonJs`机制,`ES6`的`Module`又有什么不一样? ES6 在语言标准的层面上,实现了`Module`,即模块功能,完全可以取代 `CommonJS `和 `AMD `规范,成为浏览器和服务器通用的模块解决方案 `CommonJS` 和` AMD` 模块,都只能在运行时确定这些东西。比如,`CommonJS `模块就是对象,输入时必须查找对象属性 ```javascript // CommonJS模块 let { stat, exists, readfile } = require('fs'); // 等同于 let _fs = require('fs'); let stat = _fs.stat; let exists = _fs.exists; let readfile = _fs.readfile; ``` `ES6`设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量 ```js // ES6模块 import { stat, exists, readFile } from 'fs'; ``` 上述代码,只加载3个方法,其他方法不加载,即 `ES6` 可以在编译时就完成模块加载 由于编译加载,使得静态分析成为可能。包括现在流行的`typeScript`也是依靠静态分析实现功能 ## 二、使用 `ES6`模块内部自动采用了严格模式,这里就不展开严格模式的限制,毕竟这是`ES5`之前就已经规定好 模块功能主要由两个命令构成: - `export`:用于规定模块的对外接口 - `import`:用于输入其他模块提供的功能 ### export 一个模块就是一个独立的文件,该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用`export`关键字输出该变量 ```javascript // profile.js export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958; 或 // 建议使用下面写法,这样能瞬间确定输出了哪些变量 var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958; export { firstName, lastName, year }; ``` 输出函数或类 ```js export function multiply(x, y) { return x * y; }; ``` 通过`as`可以进行输出变量的重命名 ```js function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion }; ``` ### import 使用`export`命令定义了模块的对外接口以后,其他 JS 文件就可以通过`import`命令加载这个模块 ```javascript // main.js import { firstName, lastName, year } from './profile.js'; function setName(element) { element.textContent = firstName + ' ' + lastName; } ``` 同样如果想要输入变量起别名,通过`as`关键字 ```javascript import { lastName as surname } from './profile.js'; ``` 当加载整个模块的时候,需要用到星号`*` ```js // circle.js export function area(radius) { return Math.PI * radius * radius; } export function circumference(radius) { return 2 * Math.PI * radius; } // main.js import * as circle from './circle'; console.log(circle) // {area:area,circumference:circumference} ``` 输入的变量都是只读的,不允许修改,但是如果是对象,允许修改属性 ```js import {a} from './xxx.js' a.foo = 'hello'; // 合法操作 a = {}; // Syntax Error : 'a' is read-only; ``` 不过建议即使能修改,但我们不建议。因为修改之后,我们很难差错 `import`后面我们常接着`from`关键字,`from`指定模块文件的位置,可以是相对路径,也可以是绝对路径 ```js import { a } from './a'; ``` 如果只有一个模块名,需要有配置文件,告诉引擎模块的位置 ```javascript import { myMethod } from 'util'; ``` 在编译阶段,`import`会提升到整个模块的头部,首先执行 ```javascript foo(); import { foo } from 'my_module'; ``` 多次重复执行同样的导入,只会执行一次 ```js import 'lodash'; import 'lodash'; ``` 上面的情况,大家都能看到用户在导入模块的时候,需要知道加载的变量名和函数,否则无法加载 如果不需要知道变量名或函数就完成加载,就要用到`export default`命令,为模块指定默认输出 ```js // export-default.js export default function () { console.log('foo'); } ``` 加载该模块的时候,`import`命令可以为该函数指定任意名字 ```js // import-default.js import customName from './export-default'; customName(); // 'foo' ``` ### 动态加载 允许您仅在需要时动态加载模块,而不必预先加载所有模块,这存在明显的性能优势 这个新功能允许您将`import()`作为函数调用,将其作为参数传递给模块的路径。 它返回一个 `promise`,它用一个模块对象来实现,让你可以访问该对象的导出 ```js import('/modules/myModule.mjs') .then((module) => { // Do something with the module. }); ``` ### 复合写法 如果在一个模块之中,先输入后输出同一个模块,`import`语句可以与`export`语句写在一起 ```javascript export { foo, bar } from 'my_module'; // 可以简单理解为 import { foo, bar } from 'my_module'; export { foo, bar }; ``` 同理能够搭配`as`、`*`搭配使用 ## 三、使用场景 如今,`ES6`模块化已经深入我们日常项目开发中,像`vue`、`react`项目搭建项目,组件化开发处处可见,其也是依赖模块化实现 `vue`组件 ```js ``` `react`组件 ```js function App() { return (
    组件化开发 ---- 模块化
    ); } export default App; ``` 包括完成一些复杂应用的时候,我们也可以拆分成各个模块 ## 参考文献 - https://macsalvation.net/the-history-of-js-module/ - https://es6.ruanyifeng.com/#docs/module ================================================ FILE: docs/es6/object.md ================================================ # 面试官:对象新增了哪些扩展? ![](https://static.vue-js.com/4da4dd40-5427-11eb-ab90-d9ae814b240d.png) ## 一、属性的简写 ES6中,当对象键名与对应值名相等的时候,可以进行简写 ```js const baz = {foo:foo} // 等同于 const baz = {foo} ``` 方法也能够进行简写 ```js const o = { method() { return "Hello!"; } }; // 等同于 const o = { method: function() { return "Hello!"; } } ``` 在函数内作为返回值,也会变得方便很多 ```js function getPoint() { const x = 1; const y = 10; return {x, y}; } getPoint() // {x:1, y:10} ``` 注意:简写的对象方法不能用作构造函数,否则会报错 ```js const obj = { f() { this.foo = 'bar'; } }; new obj.f() // 报错 ``` ## 二、属性名表达式 ES6 允许字面量定义对象时,将表达式放在括号内 ```js let lastWord = 'last word'; const a = { 'first word': 'hello', [lastWord]: 'world' }; a['first word'] // "hello" a[lastWord] // "world" a['last word'] // "world" ``` 表达式还可以用于定义方法名 ```js let obj = { ['h' + 'ello']() { return 'hi'; } }; obj.hello() // hi ``` 注意,属性名表达式与简洁表示法,不能同时使用,会报错 ```js // 报错 const foo = 'bar'; const bar = 'abc'; const baz = { [foo] }; // 正确 const foo = 'bar'; const baz = { [foo]: 'abc'}; ``` 注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串`[object Object]` ```js const keyA = {a: 1}; const keyB = {b: 2}; const myObject = { [keyA]: 'valueA', [keyB]: 'valueB' }; myObject // Object {[object Object]: "valueB"} ``` ## 三、super关键字 `this`关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字`super`,指向当前对象的原型对象 ```javascript const proto = { foo: 'hello' }; const obj = { foo: 'world', find() { return super.foo; } }; Object.setPrototypeOf(obj, proto); // 为obj设置原型对象 obj.find() // "hello" ``` ## 四、扩展运算符的应用 在解构赋值中,未被读取的可遍历的属性,分配到指定的对象上面 ```js let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; x // 1 y // 2 z // { a: 3, b: 4 } ``` 注意:解构赋值必须是最后一个参数,否则会报错 解构赋值是浅拷贝 ```js let obj = { a: { b: 1 } }; let { ...x } = obj; obj.a.b = 2; // 修改obj里面a属性中键值 x.a.b // 2,影响到了结构出来x的值 ``` 对象的扩展运算符等同于使用`Object.assign()`方法 ## 五、属性的遍历 ES6 一共有 5 种方法可以遍历对象的属性。 - for...in:循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性) - Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名 - Object.getOwnPropertyNames(obj):回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名 - Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有 Symbol 属性的键名 - Reflect.ownKeys(obj):返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举 上述遍历,都遵守同样的属性遍历的次序规则: - 首先遍历所有数值键,按照数值升序排列 - 其次遍历所有字符串键,按照加入时间升序排列 - 最后遍历所有 Symbol 键,按照加入时间升序排 ```js Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 }) // ['2', '10', 'b', 'a', Symbol()] ``` ## 六、对象新增的方法 关于对象新增的方法,分别有以下: - Object.is() - Object.assign() - Object.getOwnPropertyDescriptors() - Object.setPrototypeOf(),Object.getPrototypeOf() - Object.keys(),Object.values(),Object.entries() - Object.fromEntries() ### Object.is() 严格判断两个值是否相等,与严格比较运算符(===)的行为基本一致,不同之处只有两个:一是`+0`不等于`-0`,二是`NaN`等于自身 ```js +0 === -0 //true NaN === NaN // false Object.is(+0, -0) // false Object.is(NaN, NaN) // true ``` ### Object.assign() `Object.assign()`方法用于对象的合并,将源对象`source`的所有可枚举属性,复制到目标对象`target` `Object.assign()`方法的第一个参数是目标对象,后面的参数都是源对象 ```javascript const target = { a: 1, b: 1 }; const source1 = { b: 2, c: 2 }; const source2 = { c: 3 }; Object.assign(target, source1, source2); target // {a:1, b:2, c:3} ``` 注意:`Object.assign()`方法是浅拷贝,遇到同名属性会进行替换 ### Object.getOwnPropertyDescriptors() 返回指定对象所有自身属性(非继承属性)的描述对象 ```js const obj = { foo: 123, get bar() { return 'abc' } }; Object.getOwnPropertyDescriptors(obj) // { foo: // { value: 123, // writable: true, // enumerable: true, // configurable: true }, // bar: // { get: [Function: get bar], // set: undefined, // enumerable: true, // configurable: true } } ``` ### Object.setPrototypeOf() `Object.setPrototypeOf`方法用来设置一个对象的原型对象 ```js Object.setPrototypeOf(object, prototype) // 用法 const o = Object.setPrototypeOf({}, null); ``` ### Object.getPrototypeOf() 用于读取一个对象的原型对象 ```js Object.getPrototypeOf(obj); ``` ### Object.keys() 返回自身的(不含继承的)所有可遍历(enumerable)属性的键名的数组 ```js var obj = { foo: 'bar', baz: 42 }; Object.keys(obj) // ["foo", "baz"] ``` ### Object.values() 返回自身的(不含继承的)所有可遍历(enumerable)属性的键对应值的数组 ```js const obj = { foo: 'bar', baz: 42 }; Object.values(obj) // ["bar", 42] ``` ### Object.entries() 返回一个对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对的数组 ```js const obj = { foo: 'bar', baz: 42 }; Object.entries(obj) // [ ["foo", "bar"], ["baz", 42] ] ``` ### Object.fromEntries() 用于将一个键值对数组转为对象 ```js Object.fromEntries([ ['foo', 'bar'], ['baz', 42] ]) // { foo: "bar", baz: 42 } ``` ## 参考文献 - https://es6.ruanyifeng.com/#docs/object ================================================ FILE: docs/es6/promise.md ================================================ # 面试官:你是怎么理解ES6中 Promise的?使用场景? ![](https://static.vue-js.com/f033b160-5811-11eb-85f6-6fac77c0c9b3.png) ## 一、介绍 `Promise `,译为承诺,是异步编程的一种解决方案,比传统的解决方案(回调函数)更加合理和更加强大 在以往我们如果处理多层异步操作,我们往往会像下面那样编写我们的代码 ```js doSomething(function(result) { doSomethingElse(result, function(newResult) { doThirdThing(newResult, function(finalResult) { console.log('得到最终结果: ' + finalResult); }, failureCallback); }, failureCallback); }, failureCallback); ``` 阅读上面代码,是不是很难受,上述形成了经典的回调地狱 现在通过`Promise`的改写上面的代码 ```js doSomething().then(function(result) { return doSomethingElse(result); }) .then(function(newResult) { return doThirdThing(newResult); }) .then(function(finalResult) { console.log('得到最终结果: ' + finalResult); }) .catch(failureCallback); ``` 瞬间感受到`promise`解决异步操作的优点: - 链式操作减低了编码难度 - 代码可读性明显增强 下面我们正式来认识`promise`: ### 状态 `promise`对象仅有三种状态 - `pending`(进行中) - `fulfilled`(已成功) - `rejected`(已失败) ### 特点 - 对象的状态不受外界影响,只有异步操作的结果,可以决定当前是哪一种状态 - 一旦状态改变(从`pending`变为`fulfilled`和从`pending`变为`rejected`),就不会再变,任何时候都可以得到这个结果 ### 流程 认真阅读下图,我们能够轻松了解`promise`整个流程 ![](https://static.vue-js.com/1b02ae90-58a9-11eb-85f6-6fac77c0c9b3.png) ## 二、用法 `Promise`对象是一个构造函数,用来生成`Promise`实例 ```javascript const promise = new Promise(function(resolve, reject) {}); ``` `Promise`构造函数接受一个函数作为参数,该函数的两个参数分别是`resolve`和`reject` - `resolve`函数的作用是,将`Promise`对象的状态从“未完成”变为“成功” - `reject`函数的作用是,将`Promise`对象的状态从“未完成”变为“失败” ### 实例方法 `Promise`构建出来的实例存在以下方法: - then() - catch() - finally() #### then() `then`是实例状态发生改变时的回调函数,第一个参数是`resolved`状态的回调函数,第二个参数是`rejected`状态的回调函数 `then`方法返回的是一个新的`Promise`实例,也就是`promise`能链式书写的原因 ```javascript getJSON("/posts.json").then(function(json) { return json.post; }).then(function(post) { // ... }); ``` #### catch `catch()`方法是`.then(null, rejection)`或`.then(undefined, rejection)`的别名,用于指定发生错误时的回调函数 ```javascript getJSON('/posts.json').then(function(posts) { // ... }).catch(function(error) { // 处理 getJSON 和 前一个回调函数运行时发生的错误 console.log('发生错误!', error); }); ``` `Promise `对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止 ```javascript getJSON('/post/1.json').then(function(post) { return getJSON(post.commentURL); }).then(function(comments) { // some code }).catch(function(error) { // 处理前面三个Promise产生的错误 }); ``` 一般来说,使用`catch`方法代替`then()`第二个参数 `Promise `对象抛出的错误不会传递到外层代码,即不会有任何反应 ```js const someAsyncThing = function() { return new Promise(function(resolve, reject) { // 下面一行会报错,因为x没有声明 resolve(x + 2); }); }; ``` 浏览器运行到这一行,会打印出错误提示`ReferenceError: x is not defined`,但是不会退出进程 `catch()`方法之中,还能再抛出错误,通过后面`catch`方法捕获到 #### finally() `finally()`方法用于指定不管 Promise 对象最后状态如何,都会执行的操作 ```javascript promise .then(result => {···}) .catch(error => {···}) .finally(() => {···}); ``` ### 构造函数方法 `Promise`构造函数存在以下方法: - all() - race() - allSettled() - resolve() - reject() - try() ### all() `Promise.all()`方法用于将多个 `Promise `实例,包装成一个新的 `Promise `实例 ```javascript const p = Promise.all([p1, p2, p3]); ``` 接受一个数组(迭代对象)作为参数,数组成员都应为`Promise`实例 实例`p`的状态由`p1`、`p2`、`p3`决定,分为两种: - 只有`p1`、`p2`、`p3`的状态都变成`fulfilled`,`p`的状态才会变成`fulfilled`,此时`p1`、`p2`、`p3`的返回值组成一个数组,传递给`p`的回调函数 - 只要`p1`、`p2`、`p3`之中有一个被`rejected`,`p`的状态就变成`rejected`,此时第一个被`reject`的实例的返回值,会传递给`p`的回调函数 注意,如果作为参数的 `Promise` 实例,自己定义了`catch`方法,那么它一旦被`rejected`,并不会触发`Promise.all()`的`catch`方法 ```javascript const p1 = new Promise((resolve, reject) => { resolve('hello'); }) .then(result => result) .catch(e => e); const p2 = new Promise((resolve, reject) => { throw new Error('报错了'); }) .then(result => result) .catch(e => e); Promise.all([p1, p2]) .then(result => console.log(result)) .catch(e => console.log(e)); // ["hello", Error: 报错了] ``` 如果`p2`没有自己的`catch`方法,就会调用`Promise.all()`的`catch`方法 ```javascript const p1 = new Promise((resolve, reject) => { resolve('hello'); }) .then(result => result); const p2 = new Promise((resolve, reject) => { throw new Error('报错了'); }) .then(result => result); Promise.all([p1, p2]) .then(result => console.log(result)) .catch(e => console.log(e)); // Error: 报错了 ``` ### race() `Promise.race()`方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例 ```javascript const p = Promise.race([p1, p2, p3]); ``` 只要`p1`、`p2`、`p3`之中有一个实例率先改变状态,`p`的状态就跟着改变 率先改变的 Promise 实例的返回值则传递给`p`的回调函数 ```javascript const p = Promise.race([ fetch('/resource-that-may-take-a-while'), new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('request timeout')), 5000) }) ]); p .then(console.log) .catch(console.error); ``` ### allSettled() `Promise.allSettled()`方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例 只有等到所有这些参数实例都返回结果,不管是`fulfilled`还是`rejected`,包装实例才会结束 ```javascript const promises = [ fetch('/api-1'), fetch('/api-2'), fetch('/api-3'), ]; await Promise.allSettled(promises); removeLoadingIndicator(); ``` #### resolve() 将现有对象转为 `Promise `对象 ```javascript Promise.resolve('foo') // 等价于 new Promise(resolve => resolve('foo')) ``` 参数可以分成四种情况,分别如下: - 参数是一个 Promise 实例,`promise.resolve`将不做任何修改、原封不动地返回这个实例 - 参数是一个`thenable`对象,`promise.resolve`会将这个对象转为 `Promise `对象,然后就立即执行`thenable`对象的`then()`方法 - 参数不是具有`then()`方法的对象,或根本就不是对象,`Promise.resolve()`会返回一个新的 Promise 对象,状态为`resolved` - 没有参数时,直接返回一个`resolved`状态的 Promise 对象 #### reject() `Promise.reject(reason)`方法也会返回一个新的 Promise 实例,该实例的状态为`rejected` ```javascript const p = Promise.reject('出错了'); // 等同于 const p = new Promise((resolve, reject) => reject('出错了')) p.then(null, function (s) { console.log(s) }); // 出错了 ``` `Promise.reject()`方法的参数,会原封不动地变成后续方法的参数 ```javascript Promise.reject('出错了') .catch(e => { console.log(e === '出错了') }) // true ``` ## 三、使用场景 将图片的加载写成一个`Promise`,一旦加载完成,`Promise`的状态就发生变化 ```javascript const preloadImage = function (path) { return new Promise(function (resolve, reject) { const image = new Image(); image.onload = resolve; image.onerror = reject; image.src = path; }); }; ``` 通过链式操作,将多个渲染数据分别给个`then`,让其各司其职。或当下个异步请求依赖上个请求结果的时候,我们也能够通过链式操作友好解决问题 ```js // 各司其职 getInfo().then(res=>{ let { bannerList } = res //渲染轮播图 console.log(bannerList) return res }).then(res=>{ let { storeList } = res //渲染店铺列表 console.log(storeList) return res }).then(res=>{ let { categoryList } = res console.log(categoryList) //渲染分类列表 return res }) ``` 通过`all()`实现多个请求合并在一起,汇总所有请求结果,只需设置一个`loading`即可 ```js function initLoad(){ // loading.show() //加载loading Promise.all([getBannerList(),getStoreList(),getCategoryList()]).then(res=>{ console.log(res) loading.hide() //关闭loading }).catch(err=>{ console.log(err) loading.hide()//关闭loading }) } //数据初始化 initLoad() ``` 通过`race`可以设置图片请求超时 ```js //请求某个图片资源 function requestImg(){ var p = new Promise(function(resolve, reject){ var img = new Image(); img.onload = function(){ resolve(img); } //img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg"; 正确的 img.src = "https://b-gold-cdn.xitu.io/v3/static/img/logo.a7995ad.svg1"; }); return p; } //延时函数,用于给请求计时 function timeout(){ var p = new Promise(function(resolve, reject){ setTimeout(function(){ reject('图片请求超时'); }, 5000); }); return p; } Promise .race([requestImg(), timeout()]) .then(function(results){ console.log(results); }) .catch(function(reason){ console.log(reason); }); ``` ## 参考文献 - https://es6.ruanyifeng.com/#docs/promise ================================================ FILE: docs/es6/proxy.md ================================================ # 面试官:你是怎么理解ES6中Proxy的?使用场景? ![](https://static.vue-js.com/6f656e30-59f5-11eb-85f6-6fac77c0c9b3.png) ## 一、介绍 **定义:** 用于定义基本操作的自定义行为 **本质:** 修改的是程序默认形为,就形同于在编程语言层面上做修改,属于元编程`(meta programming)` 元编程(Metaprogramming,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作 一段代码来理解 ```bash #!/bin/bash # metaprogram echo '#!/bin/bash' >program for ((I=1; I<=1024; I++)) do echo "echo $I" >>program done chmod +x program ``` 这段程序每执行一次能帮我们生成一个名为`program`的文件,文件内容为1024行`echo`,如果我们手动来写1024行代码,效率显然低效 - 元编程优点:与手工编写全部代码相比,程序员可以获得更高的工作效率,或者给与程序更大的灵活度去处理新的情形而无需重新编译 `Proxy` 亦是如此,用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等) ## 二、用法 `Proxy`为 构造函数,用来生成 `Proxy `实例 ```javascript var proxy = new Proxy(target, handler) ``` ### 参数 `target`表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理)) `handler`通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 `p` 的行为 ### handler解析 关于`handler`拦截属性,有如下: - get(target,propKey,receiver):拦截对象属性的读取 - set(target,propKey,value,receiver):拦截对象属性的设置 - has(target,propKey):拦截`propKey in proxy`的操作,返回一个布尔值 - deleteProperty(target,propKey):拦截`delete proxy[propKey]`的操作,返回一个布尔值 - ownKeys(target):拦截`Object.keys(proxy)`、`for...in`等循环,返回一个数组 - getOwnPropertyDescriptor(target, propKey):拦截`Object.getOwnPropertyDescriptor(proxy, propKey)`,返回属性的描述对象 - defineProperty(target, propKey, propDesc):拦截`Object.defineProperty(proxy, propKey, propDesc)`,返回一个布尔值 - preventExtensions(target):拦截`Object.preventExtensions(proxy)`,返回一个布尔值 - getPrototypeOf(target):拦截`Object.getPrototypeOf(proxy)`,返回一个对象 - isExtensible(target):拦截`Object.isExtensible(proxy)`,返回一个布尔值 - setPrototypeOf(target, proto):拦截`Object.setPrototypeOf(proxy, proto)`,返回一个布尔值 - apply(target, object, args):拦截 Proxy 实例作为函数调用的操作 - construct(target, args):拦截 Proxy 实例作为构造函数调用的操作 ### Reflect 若需要在`Proxy`内部调用对象的默认行为,建议使用`Reflect`,其是`ES6`中操作对象而提供的新 `API` 基本特点: - 只要`Proxy`对象具有的代理方法,`Reflect`对象全部具有,以静态方法的形式存在 - 修改某些`Object`方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回`false`) - 让`Object`操作都变成函数行为 下面我们介绍`proxy`几种用法: ### get() `get`接受三个参数,依次为目标对象、属性名和 `proxy` 实例本身,最后一个参数可选 ```javascript var person = { name: "张三" }; var proxy = new Proxy(person, { get: function(target, propKey) { return Reflect.get(target,propKey) } }); proxy.name // "张三" ``` `get`能够对数组增删改查进行拦截,下面是试下你数组读取负数的索引 ```js function createArray(...elements) { let handler = { get(target, propKey, receiver) { let index = Number(propKey); if (index < 0) { propKey = String(target.length + index); } return Reflect.get(target, propKey, receiver); } }; let target = []; target.push(...elements); return new Proxy(target, handler); } let arr = createArray('a', 'b', 'c'); arr[-1] // c ``` 注意:如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则会报错 ```js const target = Object.defineProperties({}, { foo: { value: 123, writable: false, configurable: false }, }); const handler = { get(target, propKey) { return 'abc'; } }; const proxy = new Proxy(target, handler); proxy.foo // TypeError: Invariant check failed ``` ### set() `set`方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 `Proxy` 实例本身 假定`Person`对象有一个`age`属性,该属性应该是一个不大于 200 的整数,那么可以使用`Proxy`保证`age`的属性值符合要求 ```js let validator = { set: function(obj, prop, value) { if (prop === 'age') { if (!Number.isInteger(value)) { throw new TypeError('The age is not an integer'); } if (value > 200) { throw new RangeError('The age seems invalid'); } } // 对于满足条件的 age 属性以及其他属性,直接保存 obj[prop] = value; } }; let person = new Proxy({}, validator); person.age = 100; person.age // 100 person.age = 'young' // 报错 person.age = 300 // 报错 ``` 如果目标对象自身的某个属性,不可写且不可配置,那么`set`方法将不起作用 ```javascript const obj = {}; Object.defineProperty(obj, 'foo', { value: 'bar', writable: false, }); const handler = { set: function(obj, prop, value, receiver) { obj[prop] = 'baz'; } }; const proxy = new Proxy(obj, handler); proxy.foo = 'baz'; proxy.foo // "bar" ``` 注意,严格模式下,`set`代理如果没有返回`true`,就会报错 ```javascript 'use strict'; const handler = { set: function(obj, prop, value, receiver) { obj[prop] = receiver; // 无论有没有下面这一行,都会报错 return false; } }; const proxy = new Proxy({}, handler); proxy.foo = 'bar'; // TypeError: 'set' on proxy: trap returned falsish for property 'foo' ``` ### deleteProperty() `deleteProperty`方法用于拦截`delete`操作,如果这个方法抛出错误或者返回`false`,当前属性就无法被`delete`命令删除 ```javascript var handler = { deleteProperty (target, key) { invariant(key, 'delete'); Reflect.deleteProperty(target,key) return true; } }; function invariant (key, action) { if (key[0] === '_') { throw new Error(`无法删除私有属性`); } } var target = { _prop: 'foo' }; var proxy = new Proxy(target, handler); delete proxy._prop // Error: 无法删除私有属性 ``` 注意,目标对象自身的不可配置(configurable)的属性,不能被`deleteProperty`方法删除,否则报错 ### 取消代理 ``` Proxy.revocable(target, handler); ``` ## 三、使用场景 `Proxy`其功能非常类似于设计模式中的代理模式,常用功能如下: - 拦截和监视外部对对象的访问 - 降低函数或类的复杂度 - 在复杂操作前对操作进行校验或对所需资源进行管理 使用 `Proxy` 保障数据类型的准确性 ```js let numericDataStore = { count: 0, amount: 1234, total: 14 }; numericDataStore = new Proxy(numericDataStore, { set(target, key, value, proxy) { if (typeof value !== 'number') { throw Error("属性只能是number类型"); } return Reflect.set(target, key, value, proxy); } }); numericDataStore.count = "foo" // Error: 属性只能是number类型 numericDataStore.count = 333 // 赋值成功 ``` 声明了一个私有的 `apiKey`,便于 `api` 这个对象内部的方法调用,但不希望从外部也能够访问 `api._apiKey` ```js let api = { _apiKey: '123abc456def', getUsers: function(){ }, getUser: function(userId){ }, setUser: function(userId, config){ } }; const RESTRICTED = ['_apiKey']; api = new Proxy(api, { get(target, key, proxy) { if(RESTRICTED.indexOf(key) > -1) { throw Error(`${key} 不可访问.`); } return Reflect.get(target, key, proxy); }, set(target, key, value, proxy) { if(RESTRICTED.indexOf(key) > -1) { throw Error(`${key} 不可修改`); } return Reflect.get(target, key, value, proxy); } }); console.log(api._apiKey) api._apiKey = '987654321' // 上述都抛出错误 ``` 还能通过使用`Proxy`实现观察者模式 观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行 `observable`函数返回一个原始对象的 `Proxy` 代理,拦截赋值操作,触发充当观察者的各个函数 ```javascript const queuedObservers = new Set(); const observe = fn => queuedObservers.add(fn); const observable = obj => new Proxy(obj, {set}); function set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); queuedObservers.forEach(observer => observer()); return result; } ``` 观察者函数都放进`Set`集合,当修改`obj`的值,在会`set`函数中拦截,自动执行`Set`所有的观察者 ## 参考文献 - https://es6.ruanyifeng.com/#docs/proxy - https://vue3js.cn/es6 ================================================ FILE: docs/es6/set_map.md ================================================ # 面试官:你是怎么理解ES6新增Set、Map两种数据结构的? ![](https://static.vue-js.com/2b947d00-560c-11eb-85f6-6fac77c0c9b3.png) 如果要用一句来描述,我们可以说 `Set`是一种叫做集合的数据结构,`Map`是一种叫做字典的数据结构 什么是集合?什么又是字典? - 集合 是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合 - 字典 是一些元素的集合。每个元素有一个称作key 的域,不同元素的key 各不相同 区别? - 共同点:集合、字典都可以存储不重复的值 - 不同点:集合是以[值,值]的形式存储元素,字典是以[键,值]的形式存储 ## 一、Set ` Set`是`es6`新增的数据结构,类似于数组,但是成员的值都是唯一的,没有重复的值,我们一般称为集合 `Set`本身是一个构造函数,用来生成 Set 数据结构 ```js const s = new Set(); ``` ### 增删改查 `Set`的实例关于增删改查的方法: - add() - delete() - has() - clear() ### add() 添加某个值,返回 `Set` 结构本身 当添加实例中已经存在的元素,`set`不会进行处理添加 ```js s.add(1).add(2).add(2); // 2只被添加了一次 ``` ### delete() 删除某个值,返回一个布尔值,表示删除是否成功 ```js s.delete(1) ``` ### has() 返回一个布尔值,判断该值是否为`Set`的成员 ```js s.has(2) ``` ### clear() 清除所有成员,没有返回值 ```js s.clear() ``` ### 遍历 `Set`实例遍历的方法有如下: 关于遍历的方法,有如下: - keys():返回键名的遍历器 - values():返回键值的遍历器 - entries():返回键值对的遍历器 - forEach():使用回调函数遍历每个成员 `Set`的遍历顺序就是插入顺序 `keys`方法、`values`方法、`entries`方法返回的都是遍历器对象 ```javascript let set = new Set(['red', 'green', 'blue']); for (let item of set.keys()) { console.log(item); } // red // green // blue for (let item of set.values()) { console.log(item); } // red // green // blue for (let item of set.entries()) { console.log(item); } // ["red", "red"] // ["green", "green"] // ["blue", "blue"] ``` `forEach()`用于对每个成员执行某种操作,没有返回值,键值、键名都相等,同样的`forEach`方法有第二个参数,用于绑定处理函数的`this` ```javascript let set = new Set([1, 4, 9]); set.forEach((value, key) => console.log(key + ' : ' + value)) // 1 : 1 // 4 : 4 // 9 : 9 ``` 扩展运算符和` Set` 结构相结合实现数组或字符串去重 ```javascript // 数组 let arr = [3, 5, 2, 2, 5, 5]; let unique = [...new Set(arr)]; // [3, 5, 2] // 字符串 let str = "352255"; let unique = [...new Set(str)].join(""); // "352" ``` 实现并集、交集、和差集 ```javascript let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); // 并集 let union = new Set([...a, ...b]); // Set {1, 2, 3, 4} // 交集 let intersect = new Set([...a].filter(x => b.has(x))); // set {2, 3} // (a 相对于 b 的)差集 let difference = new Set([...a].filter(x => !b.has(x))); // Set {1} ``` ## 二、Map `Map`类型是键值对的有序列表,而键和值都可以是任意类型 `Map`本身是一个构造函数,用来生成 `Map` 数据结构 ```js const m = new Map() ``` ### 增删改查 `Map` 结构的实例针对增删改查有以下属性和操作方法: - size 属性 - set() - get() - has() - delete() - clear() ### size `size`属性返回 Map 结构的成员总数。 ```javascript const map = new Map(); map.set('foo', true); map.set('bar', false); map.size // 2 ``` ### set() 设置键名`key`对应的键值为`value`,然后返回整个 Map 结构 如果`key`已经有值,则键值会被更新,否则就新生成该键 同时返回的是当前`Map`对象,可采用链式写法 ```javascript const m = new Map(); m.set('edition', 6) // 键是字符串 m.set(262, 'standard') // 键是数值 m.set(undefined, 'nah') // 键是 undefined m.set(1, 'a').set(2, 'b').set(3, 'c') // 链式操作 ``` ### get() `get`方法读取`key`对应的键值,如果找不到`key`,返回`undefined` ```javascript const m = new Map(); const hello = function() {console.log('hello');}; m.set(hello, 'Hello ES6!') // 键是函数 m.get(hello) // Hello ES6! ``` ### has() `has`方法返回一个布尔值,表示某个键是否在当前 Map 对象之中 ```javascript const m = new Map(); m.set('edition', 6); m.set(262, 'standard'); m.set(undefined, 'nah'); m.has('edition') // true m.has('years') // false m.has(262) // true m.has(undefined) // true ``` ### delete() `delete`方法删除某个键,返回`true`。如果删除失败,返回`false` ```javascript const m = new Map(); m.set(undefined, 'nah'); m.has(undefined) // true m.delete(undefined) m.has(undefined) // false ``` ### clear() `clear`方法清除所有成员,没有返回值 ```javascript let map = new Map(); map.set('foo', true); map.set('bar', false); map.size // 2 map.clear() map.size // 0 ``` ### 遍历 `Map `结构原生提供三个遍历器生成函数和一个遍历方法: - keys():返回键名的遍历器 - values():返回键值的遍历器 - entries():返回所有成员的遍历器 - forEach():遍历 Map 的所有成员 遍历顺序就是插入顺序 ```javascript const map = new Map([ ['F', 'no'], ['T', 'yes'], ]); for (let key of map.keys()) { console.log(key); } // "F" // "T" for (let value of map.values()) { console.log(value); } // "no" // "yes" for (let item of map.entries()) { console.log(item[0], item[1]); } // "F" "no" // "T" "yes" // 或者 for (let [key, value] of map.entries()) { console.log(key, value); } // "F" "no" // "T" "yes" // 等同于使用map.entries() for (let [key, value] of map) { console.log(key, value); } // "F" "no" // "T" "yes" map.forEach(function(value, key, map) { console.log("Key: %s, Value: %s", key, value); }); ``` ## 三、WeakSet 和 WeakMap ### WeakSet 创建`WeakSet`实例 ```js const ws = new WeakSet(); ``` `WeakSet `可以接受一个具有 `Iterable `接口的对象作为参数 ```js const a = [[1, 2], [3, 4]]; const ws = new WeakSet(a); // WeakSet {[1, 2], [3, 4]} ``` 在`API`中`WeakSet`与`Set`有两个区别: - 没有遍历操作的`API` - 没有`size`属性 `WeakSet`只能成员只能是引用类型,而不能是其他类型的值 ```js let ws=new WeakSet(); // 成员不是引用类型 let weakSet=new WeakSet([2,3]); console.log(weakSet) // 报错 // 成员为引用类型 let obj1={name:1} let obj2={name:1} let ws=new WeakSet([obj1,obj2]); console.log(ws) //WeakSet {{…}, {…}} ``` `WeakSet `里面的引用只要在外部消失,它在 `WeakSet `里面的引用就会自动消失 ### WeakMap `WeakMap`结构与`Map`结构类似,也是用于生成键值对的集合 在`API`中`WeakMap`与`Map`有两个区别: - 没有遍历操作的`API` - 没有`clear`清空方法 ```javascript // WeakMap 可以使用 set 方法添加成员 const wm1 = new WeakMap(); const key = {foo: 1}; wm1.set(key, 2); wm1.get(key) // 2 // WeakMap 也可以接受一个数组, // 作为构造函数的参数 const k1 = [1, 2, 3]; const k2 = [4, 5, 6]; const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]); wm2.get(k2) // "bar" ``` `WeakMap`只接受对象作为键名(`null`除外),不接受其他类型的值作为键名 ```javascript const map = new WeakMap(); map.set(1, 2) // TypeError: 1 is not an object! map.set(Symbol(), 2) // TypeError: Invalid value used as weak map key map.set(null, 2) // TypeError: Invalid value used as weak map key ``` `WeakMap`的键名所指向的对象,一旦不再需要,里面的键名对象和所对应的键值对会自动消失,不用手动删除引用 举个场景例子: 在网页的 DOM 元素上添加数据,就可以使用`WeakMap`结构,当该 DOM 元素被清除,其所对应的`WeakMap`记录就会自动被移除 ```javascript const wm = new WeakMap(); const element = document.getElementById('example'); wm.set(element, 'some information'); wm.get(element) // "some information" ``` 注意:`WeakMap` 弱引用的只是键名,而不是键值。键值依然是正常引用 下面代码中,键值`obj`会在`WeakMap`产生新的引用,当你修改`obj`不会影响到内部 ```js const wm = new WeakMap(); let key = {}; let obj = {foo: 1}; wm.set(key, obj); obj = null; wm.get(key) // Object {foo: 1} ``` ## 参考文献 - https://es6.ruanyifeng.com/#docs/set-map ================================================ FILE: docs/es6/var_let_const.md ================================================ # 面试官:说说var、let、const之间的区别 ![](https://static.vue-js.com/d2aba2e0-50f7-11eb-85f6-6fac77c0c9b3.png) ## 一、var 在ES5中,顶层对象的属性和全局变量是等价的,用`var`声明的变量既是全局变量,也是顶层变量 注意:顶层对象,在浏览器环境指的是`window`对象,在 `Node` 指的是`global`对象 ```js var a = 10; console.log(window.a) // 10 ``` 使用`var`声明的变量存在变量提升的情况 ```js console.log(a) // undefined var a = 20 ``` 在编译阶段,编译器会将其变成以下执行 ```js var a console.log(a) a = 20 ``` 使用`var`,我们能够对一个变量进行多次声明,后面声明的变量会覆盖前面的变量声明 ```js var a = 20 var a = 30 console.log(a) // 30 ``` 在函数中使用使用`var`声明变量时候,该变量是局部的 ```js var a = 20 function change(){ var a = 30 } change() console.log(a) // 20 ``` 而如果在函数内不使用`var`,该变量是全局的 ```js var a = 20 function change(){ a = 30 } change() console.log(a) // 30 ``` ## 二、let `let`是`ES6`新增的命令,用来声明变量 用法类似于`var`,但是所声明的变量,只在`let`命令所在的代码块内有效 ```js { let a = 20 } console.log(a) // ReferenceError: a is not defined. ``` 不存在变量提升 ```js console.log(a) // 报错ReferenceError let a = 2 ``` 这表示在声明它之前,变量`a`是不存在的,这时如果用到它,就会抛出一个错误 只要块级作用域内存在`let`命令,这个区域就不再受外部影响 ```js var a = 123 if (true) { a = 'abc' // ReferenceError let a; } ``` 使用`let`声明变量前,该变量都不可用,也就是大家常说的“暂时性死区” 最后,`let`不允许在相同作用域中重复声明 ```js let a = 20 let a = 30 // Uncaught SyntaxError: Identifier 'a' has already been declared ``` 注意的是相同作用域,下面这种情况是不会报错的 ```js let a = 20 { let a = 30 } ``` 因此,我们不能在函数内部重新声明参数 ```js function func(arg) { let arg; } func() // Uncaught SyntaxError: Identifier 'arg' has already been declared ``` ## 三、const `const`声明一个只读的常量,一旦声明,常量的值就不能改变 ```js const a = 1 a = 3 // TypeError: Assignment to constant variable. ``` 这意味着,`const`一旦声明变量,就必须立即初始化,不能留到以后赋值 ```js const a; // SyntaxError: Missing initializer in const declaration ``` 如果之前用`var`或`let`声明过变量,再用`const`声明同样会报错 ```js var a = 20 let b = 20 const a = 30 const b = 30 // 都会报错 ``` `const`实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动 对于简单类型的数据,值就保存在变量指向的那个内存地址,因此等同于常量 对于复杂类型的数据,变量指向的内存地址,保存的只是一个指向实际数据的指针,`const`只能保证这个指针是固定的,并不能确保改变量的结构不变 ```js const foo = {}; // 为 foo 添加一个属性,可以成功 foo.prop = 123; foo.prop // 123 // 将 foo 指向另一个对象,就会报错 foo = {}; // TypeError: "foo" is read-only ``` 其它情况,`const`与`let`一致 ## 四、区别 `var`、`let`、`const`三者区别可以围绕下面五点展开: - 变量提升 - 暂时性死区 - 块级作用域 - 重复声明 - 修改声明的变量 - 使用 ### 变量提升 `var `声明的变量存在变量提升,即变量可以在声明之前调用,值为`undefined` `let`和`const`不存在变量提升,即它们所声明的变量一定要在声明后使用,否则报错 ```js // var console.log(a) // undefined var a = 10 // let console.log(b) // Cannot access 'b' before initialization let b = 10 // const console.log(c) // Cannot access 'c' before initialization const c = 10 ``` ### 暂时性死区 `var`不存在暂时性死区 `let`和`const`存在暂时性死区,只有等到声明变量的那一行代码出现,才可以获取和使用该变量 ```js // var console.log(a) // undefined var a = 10 // let console.log(b) // Cannot access 'b' before initialization let b = 10 // const console.log(c) // Cannot access 'c' before initialization const c = 10 ``` ### 块级作用域 `var`不存在块级作用域 `let`和`const`存在块级作用域 ```js // var { var a = 20 } console.log(a) // 20 // let { let b = 20 } console.log(b) // Uncaught ReferenceError: b is not defined // const { const c = 20 } console.log(c) // Uncaught ReferenceError: c is not defined ``` ### 重复声明 `var`允许重复声明变量 `let`和`const`在同一作用域不允许重复声明变量 ```js // var var a = 10 var a = 20 // 20 // let let b = 10 let b = 20 // Identifier 'b' has already been declared // const const c = 10 const c = 20 // Identifier 'c' has already been declared ``` ### 修改声明的变量 `var`和`let`可以 `const`声明一个只读的常量。一旦声明,常量的值就不能改变 ```js // var var a = 10 a = 20 console.log(a) // 20 //let let b = 10 b = 20 console.log(b) // 20 // const const c = 10 c = 20 console.log(c) // Uncaught TypeError: Assignment to constant variable ``` ### 使用 能用`const`的情况尽量使用`const`,其他情况下大多数使用`let`,避免使用`var` ## 参考文献 - https://es6.ruanyifeng.com/ ================================================ FILE: docs/git/Git.md ================================================ # 面试官:说说你对Git的理解? ![](https://static.vue-js.com/213eba50-f79c-11eb-bc6f-3f06e1491664.png) ## 一、是什么 git,是一个分布式版本控制软件,最初目的是为更好地管理`Linux`内核开发而设计 分布式版本控制系统的客户端并不只提取最新版本的文件快照,而是把代码仓库完整地镜像下来。这么一来,任何一处协同工作用的服务器发生故障,事后都可以用任何一个镜像出来的本地仓库恢复 ![](https://static.vue-js.com/29240f40-f79c-11eb-991d-334fd31f0201.png) 项目开始,只有一个原始版仓库,别的机器可以`clone`这个原始版本库,那么所有`clone`的机器,它们的版本库其实都是一样的,并没有主次之分 所以在实现团队协作的时候,只要有一台电脑充当服务器的角色,其他每个人都从这个“服务器”仓库`clone`一份到自己的电脑上,并且各自把各自的提交推送到服务器仓库里,也从服务器仓库中拉取别人的提交 `github`实际就可以充当这个服务器角色,其是一个开源协作社区,提供`Git`仓库托管服务,既可以让别人参与你的开源项目,也可以参与别人的开源项目 ## 二、工作原理 当我们通过`git init`创建或者`git clone`一个项目的时候,项目目录会隐藏一个`.git`子目录,其作用是用来跟踪管理版本库的 `Git` 中所有数据在存储前都计算校验和,然后以校验和来引用,所以在我们修改或者删除文件的时候,`git`能够知道 `Git `用以计算校验和的机制叫做 SHA-1 散列(hash,哈希), 这是一个由 40 个十六进制字符(0-9 和 a-f)组成字符串,基于 Git 中文件的内容或目录结构计算出来,如下: ```text 24b9da6552252987aa493b52f8696cd6d3b00373 ``` 当我们修改文件的时候,`git`就会修改文件的状态,可以通过`git status`进行查询,状态情况如下: - 已修改(modified):表示修改了文件,但还没保存到数据库中。 - 已暂存(staged):表示对一个已修改文件的当前版本做了标记,使之包含在下次提交的快照中。 - 已提交(committed):表示数据已经安全的保存在本地数据库中。 文件状态对应的,不同状态的文件在` Git `中处于不同的工作区域,主要分成了四部分: - 工作区:相当于本地写代码的区域,如 git clone 一个项目到本地,相当于本地克隆了远程仓库项目的一个副本 - 暂存区:暂存区是一个文件,保存了下次将提交的文件列表信息,一般在 Git 仓库目录中 - 本地仓库:提交更新,找到暂存区域的文件,将快照永久性存储到 Git 本地仓库 - 远程仓库:远程的仓库,如 github ![](https://static.vue-js.com/3273c9a0-f79c-11eb-bc6f-3f06e1491664.png) ## 三、命令 从上图可以看到,`git`日常简单的使用就只有上图6个命令: - add - commit - push - pull - clone - checkout 但实际上还有很多命令,如果想要熟练使用,还有60个多命令,通过这些命令的配合使用,能够提高个人工作效率和团队协助能力 ## 参考文献 - https://zh.wikipedia.org/wiki/Git - https://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html ================================================ FILE: docs/git/HEAD_tree_index.md ================================================ # 面试官:说说Git 中 HEAD、工作树和索引之间的区别? ![](https://static.vue-js.com/2de056a0-fa40-11eb-991d-334fd31f0201.png) ## 一、HEAD 在`git`中,可以存在很多分支,其本质上是一个指向`commit`对象的可变指针,而`Head`是一个特别的指针,是一个指向你正在工作中的本地分支的指针 简单来讲,就是你现在在哪儿,HEAD 就指向哪儿 例如当前我们处于`master`分支,所以`HEAD`这个指针指向了`master`分支指针 ![](https://static.vue-js.com/36cb0da0-fa40-11eb-991d-334fd31f0201.png) 然后通过调用`git checkout test`切换到`test`分支,那么`HEAD`则指向`test`分支,如下图: ![](https://static.vue-js.com/3e86ba80-fa40-11eb-991d-334fd31f0201.png) 但我们在`test`分支再一次`commit`信息的时候,`HEAD`指针仍然指向了`test`分支指针,而`test`分支指针已经指向了最新创建的提交,如下图: ![](https://static.vue-js.com/439839b0-fa66-11eb-991d-334fd31f0201.png) 这个`HEAD`存储的位置就在`.git/HEAD`目录中,查看信息可以看到`HEAD`指向了另一个文件 ```cmd $ cat .git/HEAD ref: refs/heads/master $ cat .git/refs/heads/master 7406a10efcc169bbab17827aeda189aa20376f7f ``` 这个文件的内容是一串哈希码,而这个哈希码正是`master`分支上最新的提交所对应的哈希码 所以,当我们切换分支的时候,`HEAD`指针通常指向我们所在的分支,当我们在某个分支上创建新的提交时,分支指针总是会指向当前分支的最新提交 所以,HEAD指针 ——–> 分支指针 ——–> 最新提交 ## 二、工作树和索引 在`Git`管理下,大家实际操作的目录被称为工作树,也就是工作区域 在数据库和工作树之间有索引,索引是为了向数据库提交作准备的区域,也被称为暂存区域 ![](https://static.vue-js.com/46e5ac40-fa40-11eb-bc6f-3f06e1491664.png) `Git`在执行提交的时候,不是直接将工作树的状态保存到数据库,而是将设置在中间索引区域的状态保存到数据库 因此,要提交文件,首先需要把文件加入到索引区域中。 所以,凭借中间的索引,可以避免工作树中不必要的文件提交,还可以将文件修改内容的一部分加入索引区域并提交 ## 三、区别 从所在的位置来看: - HEAD 指针通常指向我们所在的分支,当我们在某个分支上创建新的提交时,分支指针总是会指向当前分支的最新提交 - 工作树是查看和编辑的(源)文件的实际内容 - 索引是放置你想要提交给 git仓库文件的地方,如工作树的代码通过 git add 则添加到 git 索引中,通过git commit 则将索引区域的文件提交到 git 仓库中 ## 参考文献 - https://backlog.com/git-tutorial/cn/intro/intro1_4.html - https://juejin.cn/post/6844903598522908686 - https://www.zsythink.net/archives/3412 ================================================ FILE: docs/git/Version control.md ================================================ # 面试官:说说你对版本管理的理解?常用的版本管理工具有哪些? ![](https://static.vue-js.com/f0e8a2d0-f5ac-11eb-ab90-d9ae814b240d.png) ## 一、是什么 版本控制(Version control),是维护工程蓝图的标准作法,能追踪工程蓝图从诞生一直到定案的过程。此外,版本控制也是一种软件工程技巧,借此能在软件开发的过程中,确保由不同人所编辑的同一程序文件都得到同步 透过文档控制,能记录任何工程项目内各个模块的改动历程,并为每次改动编上序号 一种简单的版本控制形式如下:赋给图的初版一个版本等级“A”。当做了第一次改变后,版本等级改为“B”,以此类推 版本控制能提供项目的设计者,将设计恢复到之前任一状态的选择权 简言之,你的修改只要提到到版本控制系统,基本都可以找回,版本控制系统就像一台时光机器,可以让你回到任何一个时间点 ## 二、有哪些 版本控制系统在当今的软件开发中,被认为是理所当然的配备工具之一,根据类别可以分成: - 本地版本控制系统 - 集中式版本控制系统 - 分布式版本控制系统 ### 本地版本控制系统 结构如下图所示: ![](https://static.vue-js.com/c545ded0-f5ad-11eb-ab90-d9ae814b240d.png) 优点: - 简单,很多系统中都有内置 - 适合管理文本,如系统配置 缺点: - 其不支持远程操作,因此并不适合多人版本开发 ### 集中式版本控制系统 结构如下图所示: ![](https://static.vue-js.com/8b4b3040-f5ad-11eb-85f6-6fac77c0c9b3.png) 优点: - 适合多人团队协作开发 - 代码集中化管理 缺点: - 单点故障 - 必须联网,无法单机工作 代表工具有`SVN`、`CVS`: ### SVN `TortoiseSVN`是一款非常易于使用的跨平台的 版本控制/版本控制/源代码控制软件 ### CVS `CVS`是版本控制系统,是源配置管理(SCM)的重要组成部分。使用它,您可以记录源文件和文档的历史记录 老牌的版本控制系统,它是基于客户端/服务器的行为使得其可容纳多用户,构成网络也很方便 这一特性使得`CVS`成为位于不同地点的人同时处理数据文件(特别是程序的源代码)时的首选 #### 分布式版本控制系统 结构如下图: ![](https://static.vue-js.com/4301a260-f5ad-11eb-85f6-6fac77c0c9b3.png) 优点: - 适合多人团队协作开发 - 代码集中化管理 - 可以离线工作 - 每个计算机都是一个完整仓库 分布式版本管理系统每个计算机都有一个完整的仓库,可本地提交,可以做到离线工作,则不用像集中管理那样因为断网情况而无法工作 代表工具为`Git`、`HG`: ### Git `Git`是目前世界上最先进的分布式版本控制系统,旨在快速高效地处理从小型到大型项目的所有事务 特性:易于学习,占用内存小,具有闪电般快速的性能 使用`Git`和`Gitlab`搭建版本控制环境是现在互联网公司最流行的版本控制方式 ### HG `Mercurial`是一个免费的分布式源代码管理工具。它可以有效地处理任何规模的项目,并提供简单直观的界面 `Mercurial `是一种轻量级分布式版本控制系统,采用 `Python `语言实现,易于学习和使用,扩展性强 ## 三、总结 版本控制系统的优点如下: - 记录文件所有历史变化,这是版本控制系统的基本能力 - 随时恢复到任意时间点,历史记录功能使我们不怕改错代码了 - 支持多功能并行开发,通常版本控制系统都支持分支,保证了并行开发的可行 - 多人协作并行开发,对于多人协作项目,支持多人协作开发的版本管理将事半功倍 ## 参考文献 - https://pm.readthedocs.io/vcs/understanding.html - https://zh.wikipedia.org/wiki/%E7%89%88%E6%9C%AC%E6%8E%A7%E5%88%B6 ================================================ FILE: docs/git/command.md ================================================ # 面试官:说说Git常用的命令有哪些? ![](https://static.vue-js.com/f66b3290-f7af-11eb-bc6f-3f06e1491664.png) ## 一、前言 `git `的操作可以通过命令的形式如执行,日常使用就如下图6个命令即可 ![](https://static.vue-js.com/fe150520-f7af-11eb-991d-334fd31f0201.png) 实际上,如果想要熟练使用,超过60多个命令需要了解,下面则介绍下常见的的`git `命令 ## 二、有哪些 ## 配置 `Git `自带一个 `git config` 的工具来帮助设置控制 `Git `外观和行为的配置变量,在我们安装完`git`之后,第一件事就是设置你的用户名和邮件地址 后续每一个提交都会使用这些信息,它们会写入到你的每一次提交中,不可更改 设置提交代码时的用户信息命令如下: - git config [--global] user.name "[name]" - git config [--global] user.email "[email address]" ### 启动 一个`git`项目的初始有两个途径,分别是: - git init [project-name]:创建或在当前目录初始化一个git代码库 - git clone url:下载一个项目和它的整个代码历史 ### 日常基本操作 在日常工作中,代码常用的基本操作如下: - git init 初始化仓库,默认为 master 分支 - git add . 提交全部文件修改到缓存区 - git add <具体某个文件路径+全名> 提交某些文件到缓存区 - git diff 查看当前代码 add后,会 add 哪些内容 - git diff --staged查看现在 commit 提交后,会提交哪些内容 - git status 查看当前分支状态 - git pull <远程仓库名> <远程分支名> 拉取远程仓库的分支与本地当前分支合并 - git pull <远程仓库名> <远程分支名>:<本地分支名> 拉取远程仓库的分支与本地某个分支合并 - git commit -m "<注释>" 提交代码到本地仓库,并写提交注释 - git commit -v 提交时显示所有diff信息 - git commit --amend [file1] [file2] 重做上一次commit,并包括指定文件的新变化 关于提交信息的格式,可以遵循以下的规则: - feat: 新特性,添加功能 - fix: 修改 bug - refactor: 代码重构 - docs: 文档修改 - style: 代码格式修改, 注意不是 css 修改 - test: 测试用例修改 - chore: 其他修改, 比如构建流程, 依赖管理 ### 分支操作 - git branch 查看本地所有分支 - git branch -r 查看远程所有分支 - git branch -a 查看本地和远程所有分支 - git merge <分支名> 合并分支 - git merge --abort 合并分支出现冲突时,取消合并,一切回到合并前的状态 - git branch <新分支名> 基于当前分支,新建一个分支 - git checkout --orphan <新分支名> 新建一个空分支(会保留之前分支的所有文件) - git branch -D <分支名> 删除本地某个分支 - git push <远程库名> :<分支名> 删除远程某个分支 - git branch <新分支名称> <提交ID> 从提交历史恢复某个删掉的某个分支 - git branch -m <原分支名> <新分支名> 分支更名 - git checkout <分支名> 切换到本地某个分支 - git checkout <远程库名>/<分支名> 切换到线上某个分支 - git checkout -b <新分支名> 把基于当前分支新建分支,并切换为这个分支 ### 远程同步 远程操作常见的命令: - git fetch [remote] 下载远程仓库的所有变动 - git remote -v 显示所有远程仓库 - git pull [remote] [branch] 拉取远程仓库的分支与本地当前分支合并 - git fetch 获取线上最新版信息记录,不合并 - git push [remote] [branch] 上传本地指定分支到远程仓库 - git push [remote] --force 强行推送当前分支到远程仓库,即使有冲突 - git push [remote] --all 推送所有分支到远程仓库 ### 撤销 - git checkout [file] 恢复暂存区的指定文件到工作区 - git checkout [commit] [file] 恢复某个commit的指定文件到暂存区和工作区 - git checkout . 恢复暂存区的所有文件到工作区 - git reset [commit] 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变 - git reset --hard 重置暂存区与工作区,与上一次commit保持一致 - git reset [file] 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变 - git revert [commit] 后者的所有变化都将被前者抵消,并且应用到当前分支 > `reset`:真实硬性回滚,目标版本后面的提交记录全部丢失了 > > `revert`:同样回滚,这个回滚操作相当于一个提价,目标版本后面的提交记录也全部都有 ### 存储操作 你正在进行项目中某一部分的工作,里面的东西处于一个比较杂乱的状态,而你想转到其他分支上进行一些工作,但又不想提交这些杂乱的代码,这时候可以将代码进行存储 - git stash 暂时将未提交的变化移除 - git stash pop 取出储藏中最后存入的工作状态进行恢复,会删除储藏 - git stash list 查看所有储藏中的工作 - git stash apply <储藏的名称> 取出储藏中对应的工作状态进行恢复,不会删除储藏 - git stash clear 清空所有储藏中的工作 - git stash drop <储藏的名称> 删除对应的某个储藏 ## 三、总结 `git`常用命令速查表如下所示: ![](https://static.vue-js.com/0a10f3c0-f7b0-11eb-991d-334fd31f0201.png) ## 参考文献 - https://www.ruanyifeng.com/blog/2015/12/git-cheat-sheet.html - https://segmentfault.com/a/1190000017875714 ================================================ FILE: docs/git/conflict.md ================================================ # 面试官:说说 git 发生冲突的场景?如何解决? ![](https://static.vue-js.com/8aeccc40-fdb3-11eb-bc6f-3f06e1491664.png) ## 一、是什么 一般情况下,出现分支的场景有如下: - 多个分支代码合并到一个分支时 - 多个分支向同一个远端分支推送 具体情况就是,多个分支修改了同一个文件(任何地方)或者多个分支修改了同一个文件的名称 如果两个分支中分别修改了不同文件中的部分,是不会产生冲突,直接合并即可 应用在命令中,就是`push`、`pull`、`stash`、`rebase`等命令下都有可能产生冲突情况,从本质上来讲,都是`merge`和`patch`(应用补丁)时产生冲突 ## 二、分析 在本地主分值`master`创建一个`a.txt`文件,文件起始位置写上`master commit`,如下: ![](https://static.vue-js.com/959ade20-fdb3-11eb-991d-334fd31f0201.png) 然后提交到仓库: - git add a.txt - git commit -m 'master first commit' 创建一个新的分支`featurel1`分支,并进行切换,如下: ```cmd git checkout -b featurel1 ``` 然后修改`a.txt`文件首行文字为 `featurel commit`,然后添加到暂存区,并开始进行提交到仓库: - git add a.txt - git commit -m 'featurel first change' 然后通过`git checkout master`切换到主分支,通过`git merge`进行合并,发现不会冲突 此时`a.txt`文件的内容变成`featurel commit`,没有出现冲突情况,这是因为`git`在内部发生了快速合并 > 如果当前分支的每一个提交(commit)都已经存在另一个分支里了,git 就会执行一个“快速向前”(fast forward)操作 > > git 不创建任何新的提交(commit),只是将当前分支指向合并进来的分支 如果此时切换到`featurel`分支,将文件的内容修改成`featrue second commit`,然后提交到本地仓库 然后切换到主分支,如果此时在`a.txt`文件再次修改,修改成`mastet second commit`,然后再次提交到本地仓库 此时,`master`分支和`feature1`分支各自都分别有新的提交,变成了下图所示: ![](https://static.vue-js.com/a05488c0-fdb3-11eb-991d-334fd31f0201.png) 这种情况下,无法执行快速合并,只能试图把各自的修改合并起来,但这种合并就可能会有冲突 现在通过`git merge featurel`进行分支合并,如下所示: ![](https://static.vue-js.com/b0991d90-fdb3-11eb-bc6f-3f06e1491664.png) 从冲突信息可以看到,`a.txt`发生冲突,必须手动解决冲突之后再提交 而`git status`同样可以告知我们冲突的文件: ![](https://static.vue-js.com/c5823430-fdb3-11eb-991d-334fd31f0201.png) 打开`a.txt`文件,可以看到如下内容: ![](https://static.vue-js.com/ce7a0a90-fdb3-11eb-bc6f-3f06e1491664.png) `git`用`<<<<<<<`,`=======`,`>>>>>>>`标记出不同分支的内容: - <<<<<<< 和 ======= 之间的区域就是当前更改的内容 - ======= 和 >>>>>>> 之间的区域就是传入进来更改的内容 现在要做的事情就是将冲突的内容进行更改,对每个文件使用 `git add` 命令来将其标记为冲突已解决。 一旦暂存这些原本有冲突的文件,`Git `就会将它们标记为冲突已解决然后再提交: - git add a.txt - git commit -m "conflict fixed" 此时`master`分支和`feature1`分支变成了下图所示: ![](https://static.vue-js.com/d7421e60-fdb3-11eb-bc6f-3f06e1491664.png) 使用`git log`命令可以看到合并的信息: ![](https://static.vue-js.com/e0dfd1b0-fdb3-11eb-991d-334fd31f0201.png) ## 三、总结 当`Git`无法自动合并分支时,就必须首先解决冲突,解决冲突后,再提交,合并完成 解决冲突就是把`Git`合并失败的文件手动编辑为我们希望的内容,再提交 ## 参考文献 - https://www.liaoxuefeng.com/wiki/896043488029600/900004111093344 ================================================ FILE: docs/git/fork_clone_branch.md ================================================ # 面试官:说说Git中 fork, clone,branch这三个概念,有什么区别? ![](https://static.vue-js.com/9c4eb9a0-f7ad-11eb-bc6f-3f06e1491664.png) ## 一、是什么 ### fork `fork`,英语翻译过来就是叉子,动词形式则是分叉,如下图,从左到右,一条直线变成多条直线 ![](https://static.vue-js.com/ad04ade0-f7ad-11eb-991d-334fd31f0201.png) 转到`git`仓库中,`fork`则可以代表分叉、克隆 出一个(仓库的)新拷贝 ![](https://static.vue-js.com/b4b31450-f7ad-11eb-991d-334fd31f0201.png) 包含了原来的仓库(即upstream repository,上游仓库)所有内容,如分支、Tag、提交 如果想将你的修改合并到原项目中时,可以通过的 Pull Request 把你的提交贡献回 原仓库 ### clone `clone`,译为克隆,它的作用是将文件从远程代码仓下载到本地,从而形成一个本地代码仓 执行`clone`命令后,会在当前目录下创建一个名为`xxx`的目录,并在这个目录下初始化一个 `.git` 文件夹,然后从中读取最新版本的文件的拷贝 默认配置下远程 `Git` 仓库中的每一个文件的每一个版本都将被拉取下来 ### branch `branch`,译为分支,其作用简单而言就是开启另一个分支, 使用分支意味着你可以把你的工作从开发主线上分离开来,以免影响开发主线 ` Git` 处理分支的方式十分轻量,创建新分支这一操作几乎能在瞬间完成,并且在不同分支之间的切换操作也是一样便捷 在我们开发中,默认只有一条`master`分支,如下图所示: ![](https://static.vue-js.com/7fa8e9c0-f923-11eb-991d-334fd31f0201.png) 通过`git branch `可以创建一个分支,但并不会自动切换到新分支中去 ![](https://static.vue-js.com/89efd560-f923-11eb-bc6f-3f06e1491664.png) 通过`git checkout`可以切换到另一个`testing`分支 ![](https://static.vue-js.com/91d1cef0-f923-11eb-bc6f-3f06e1491664.png) ## 二、如何使用 ### fork 当你在`github`发现感兴趣开源项目的时候,可以通过点击`github`仓库中右上角`fork`标识的按钮,如下图: ![](https://static.vue-js.com/bc4c4510-f7ad-11eb-991d-334fd31f0201.png) 点击这个操作后会将这个仓库的文件、提交历史、issues和其余东西的仓库复制到自己的`github`仓库中,而你本地仓库是不会存在任何更改 然后你就可以通过`git clone`对你这个复制的远程仓库进行克隆 后续更改任何东西都可以在本地完成,如`git add`、`git commit`一系列的操作,然后通过`push`命令推到自己的远程仓库 如果希望对方接受你的修改,可以通过发送`pull requests`给对方,如果对方接受。则会将你的修改内容更新到仓库中 ![](https://static.vue-js.com/c5265a40-f7ad-11eb-991d-334fd31f0201.png) 整体流程如下图: ![](https://static.vue-js.com/ced8ce10-f7ad-11eb-bc6f-3f06e1491664.png) ### clone 在`github`中,开源项目右侧存在`code`按钮,点击后则会显示开源项目`url`信息,如下图所示: ![](https://static.vue-js.com/d8685090-f7ad-11eb-bc6f-3f06e1491664.png) 通过`git clone xxx`则能完成远程项目的下载 ### branch 可通过`git branch`进行查看当前的分支状态, 如果给了`--list`,或者没有非选项参数,现有的分支将被列出;当前的分支将以绿色突出显示,并标有星号 以及通过`git branch`创建一个新的分支出来 ## 三、区别 其三者区别如下: - fork 只能对代码仓进行操作,且 fork 不属于 git 的命令,通常用于代码仓托管平台的一种“操作” - clone 是 git 的一种命令,它的作用是将文件从远程代码仓下载到本地,从而形成一个本地代码仓 - branch 特征与 fork 很类似,fork 得到的是一个新的、自己的代码仓,而 branch 得到的是一个代码仓的一个新分支 ## 参考文献 - https://git-scm.com/book/zh/v2/Git-基础-获取-Git-仓库 - https://git-scm.com/book/zh/v2/Git-分支-分支简介 ================================================ FILE: docs/git/git pull _git fetch.md ================================================ # 说说对git pull 和 git fetch 的理解?有什么区别? ![](https://static.vue-js.com/cc90c050-fac2-11eb-991d-334fd31f0201.png) ## 一、是什么 先回顾两个命令的定义 - git fetch 命令用于从另一个存储库下载对象和引用 - git pull 命令用于从另一个存储库或本地分支获取并集成(整合) 再来看一次`git`的工作流程图,如下所示: ![](https://static.vue-js.com/d523ba60-fac2-11eb-991d-334fd31f0201.png) 可以看到,`git fetch`是将远程主机的最新内容拉到本地,用户在检查了以后决定是否合并到工作本机分支中 而`git pull` 则是将远程主机的最新内容拉下来后直接合并,即:`git pull = git fetch + git merge`,这样可能会产生冲突,需要手动解决 在我们本地的`git`文件中对应也存储了`git`本地仓库分支的`commit ID `和 跟踪的远程分支的`commit ID`,对应文件如下: - .git/refs/head/[本地分支] - .git/refs/remotes/[正在跟踪的分支] 使用 `git fetch`更新代码,本地的库中`master`的`commitID`不变 但是与`git`上面关联的那个`orign/master`的`commit ID`发生改变 这时候我们本地相当于存储了两个代码的版本号,我们还要通过`merge`去合并这两个不同的代码版本 ![](https://static.vue-js.com/fd23ff70-fb12-11eb-bc6f-3f06e1491664.png) 也就是`fetch`的时候本地的`master`没有变化,但是与远程仓关联的那个版本号被更新了,接下来就是在本地`merge`合并这两个版本号的代码 相比之下,使用`git pull`就更加简单粗暴,会将本地的代码更新至远程仓库里面最新的代码版本,如下图: ![](https://static.vue-js.com/091b8140-fb13-11eb-bc6f-3f06e1491664.png) ## 二、用法 一般远端仓库里有新的内容更新,当我们需要把新内容下载的时候,就使用到`git pull`或者`git fetch`命令 ### fetch 用法如下: ```cmd git fetch <远程主机名> <远程分支名>:<本地分支名> ``` 例如从远程的`origin`仓库的`master`分支下载代码到本地并新建一个`temp`分支 ```cmd git fetch origin master:temp ``` 如果上述没有冒号,则表示将远程`origin`仓库的`master`分支拉取下来到本地当前分支 这里`git fetch`不会进行合并,执行后需要手动执行`git merge`合并,如下: ```cmd git merge temp ``` ### pull 两者的用法十分相似,`pull`用法如下: ```cmd git pull <远程主机名> <远程分支名>:<本地分支名> ``` 例如将远程主机`origin`的`master`分支拉取过来,与本地的`branchtest`分支合并,命令如下: ```cmd git pull origin master:branchtest ``` 同样如果上述没有冒号,则表示将远程`origin`仓库的`master`分支拉取下来与本地当前分支合并 ## 三、区别 相同点: - 在作用上他们的功能是大致相同的,都是起到了更新代码的作用 不同点: - git pull是相当于从远程仓库获取最新版本,然后再与本地分支merge,即git pull = git fetch + git merge - 相比起来,git fetch 更安全也更符合实际要求,在 merge 前,我们可以查看更新情况,根据实际情况再决定是否合并 ## 参考文献 - https://zhuanlan.zhihu.com/p/123370920 - https://segmentfault.com/a/1190000017030384 - https://juejin.cn/post/6844903921794859021 ================================================ FILE: docs/git/git rebase_ git merge.md ================================================ # 面试官:说说你对git rebase 和 git merge的理解?区别? ![](https://static.vue-js.com/77590970-fdd4-11eb-bc6f-3f06e1491664.png) ## 一、是什么 在使用 `git` 进行版本管理的项目中,当完成一个特性的开发并将其合并到 `master` 分支时,会有两种方式: - git merge - git rebase `git rebase` 与 `git merge`都有相同的作用,都是将一个分支的提交合并到另一分支上,但是在原理上却不相同 用法上两者也十分的简单: ### git merge 将当前分支合并到指定分支,命令用法如下: ```cmd git merge xxx ``` ### git rebase 将当前分支移植到指定分支或指定`commit`之上,用法如下: ```cmd git rebase -i ``` 常见的参数有`--continue`,用于解决冲突之后,继续执行`rebase` ```cmd git rebase --continue ``` ## 二、分析 ### git merge 通过`git merge`将当前分支与`xxx`分支合并,产生的新的`commit`对象有两个父节点 如果“指定分支”本身是当前分支的一个直接子节点,则会产生快照合并 举个例子,`bugfix`分支是从`master`分支分叉出来的,如下所示: ![](https://static.vue-js.com/88410a30-fdd4-11eb-991d-334fd31f0201.png) 合并` bugfix`分支到`master`分支时,如果`master`分支的状态没有被更改过,即 `bugfix`分支的历史记录包含`master`分支所有的历史记录 所以通过把`master`分支的位置移动到`bugfix`的最新分支上,就完成合并 如果`master`分支的历史记录在创建`bugfix`分支后又有新的提交,如下情况: ![](https://static.vue-js.com/929eb220-fdd4-11eb-991d-334fd31f0201.png) 这时候使用`git merge`的时候,会生成一个新的提交,并且`master`分支的`HEAD`会移动到新的分支上,如下: ![](https://static.vue-js.com/9fdfa3e0-fdd4-11eb-991d-334fd31f0201.png) 从上面可以看到,会把两个分支的最新快照以及二者最近的共同祖先进行三方合并,合并的结果是生成一个新的快照 ### git rebase 同样,`master`分支的历史记录在创建`bugfix`分支后又有新的提交,如下情况: ![](https://static.vue-js.com/ab2d5120-fdd4-11eb-bc6f-3f06e1491664.png) 通过`git rebase`,会变成如下情况: ![](https://static.vue-js.com/b72aed70-fdd4-11eb-991d-334fd31f0201.png) 在移交过程中,如果发生冲突,需要修改各自的冲突,如下: ![](https://static.vue-js.com/c9ba0e80-fdd4-11eb-bc6f-3f06e1491664.png) `rebase`之后,`master`的`HEAD`位置不变。因此,要合并`master`分支和`bugfix`分支 ![](https://static.vue-js.com/dc660660-fdd4-11eb-991d-334fd31f0201.png) 从上面可以看到,`rebase`会找到不同的分支的最近共同祖先,如上图的`B` 然后对比当前分支相对于该祖先的历次提交,提取相应的修改并存为临时文件(老的提交`X`和`Y`也没有被销毁,只是简单地不能再被访问或者使用) 然后将当前分支指向目标最新位置`D`, 然后将之前另存为临时文件的修改依序应用 ## 三、区别 从上面可以看到,`merge`和`rebasea`都是合并历史记录,但是各自特性不同: ### merge 通过`merge`合并分支会新增一个`merge commit`,然后将两个分支的历史联系起来 其实是一种非破坏性的操作,对现有分支不会以任何方式被更改,但是会导致历史记录相对复杂 ### rebase `rebase `会将整个分支移动到另一个分支上,有效地整合了所有分支上的提交 主要的好处是历史记录更加清晰,是在原有提交的基础上将差异内容反映进去,消除了 ` git merge `所需的不必要的合并提交 ## 参考文献 - https://zhuanlan.zhihu.com/p/361182707 - https://yuweijun.github.io/git-zh/1-git-branching.html#_rebasing - https://backlog.com/git-tutorial/cn/stepup/stepup1_4.html ================================================ FILE: docs/git/git reset_ git revert.md ================================================ # 面试官:说说你对git reset 和 git revert 的理解?区别? ![](https://static.vue-js.com/046b4440-ff74-11eb-bc6f-3f06e1491664.png) ## 一、是什么 ### git reset `reset`用于回退版本,可以遗弃不再使用的提交 执行遗弃时,需要根据影响的范围而指定不同的参数,可以指定是否复原索引或工作树内容 ![](https://static.vue-js.com/ab4d0c00-ff72-11eb-bc6f-3f06e1491664.png) ### git revert 在当前提交后面,新增一次提交,抵消掉上一次提交导致的所有变化,不会改变过去的历史,主要是用于安全地取消过去发布的提交 ![](https://static.vue-js.com/bd12c290-ff72-11eb-991d-334fd31f0201.png) ## 二、如何用 ### git reset 当没有指定`ID`的时候,默认使用`HEAD`,如果指定`ID`,那么就是基于指向`ID`去变动暂存区或工作区的内容 ```cmd // 没有指定ID, 暂存区的内容会被当前ID版本号的内容覆盖,工作区不变 git reset // 指定ID,暂存区的内容会被指定ID版本号的内容覆盖,工作区不变 git reset ``` 日志`ID`可以通过查询,可以`git log`进行查询,如下: ```cmd commit a7700083ace1204ccdff9f71631fb34c9913f7c5 (HEAD -> master) Author: linguanghui Date: Tue Aug 17 22:34:40 2021 +0800 second commit commit e31118663ce66717edd8a179688a7f3dde5a9393 Author: linguanghui Date: Tue Aug 17 22:20:01 2021 +0800 first commit ``` 常见命令如下: - --mixed(默认):默认的时候,只有暂存区变化 - --hard参数:如果使用 --hard 参数,那么工作区也会变化 - --soft:如果使用 --soft 参数,那么暂存区和工作区都不会变化 ![](https://static.vue-js.com/225b41e0-ff73-11eb-bc6f-3f06e1491664.png) ### git revert 跟`git reset`用法基本一致,`git revert` 撤销某次操作,此次操作之前和之后的 `commit`和`history`都会保留,并且把这次撤销,作为一次最新的提交,如下: ```cmd git revert ``` 如果撤销前一个版本,可以通过如下命令: ```cmd git revert HEAD ``` 撤销前前一次,如下: ```cmd git revert HEAD^ ``` ## 三、区别 撤销(revert)被设计为撤销公开的提交(比如已经push)的安全方式,`git reset`被设计为重设本地更改 因为两个命令的目的不同,它们的实现也不一样:重设完全地移除了一堆更改,而撤销保留了原来的更改,用一个新的提交来实现撤销 两者主要区别如下: - git revert是用一次新的commit来回滚之前的commit,git reset是直接删除指定的commit - git reset 是把HEAD向后移动了一下,而git revert是HEAD继续前进,只是新的commit的内容和要revert的内容正好相反,能够抵消要被revert的内容 - 在回滚这一操作上看,效果差不多。但是在日后继续 merge 以前的老版本时有区别 > git revert是用一次逆向的commit“中和”之前的提交,因此日后合并老的branch时,之前提交合并的代码仍然存在,导致不能够重新合并 > > 但是git reset是之间把某些commit在某个branch上删除,因而和老的branch再次merge时,这些被回滚的commit应该还会被引入 - 如果回退分支的代码以后还需要的情况则使用`git revert`, 如果分支是提错了没用的并且不想让别人发现这些错误代码,则使用`git reset` ## 参考文献 - https://juejin.cn/post/6844903542931587086 - https://marklodato.github.io/visual-git-guide/index-zh-cn.html#reset ================================================ FILE: docs/git/git stash.md ================================================ # 面试官:说说你对git stash 的理解?应用场景? ![](https://static.vue-js.com/83ddf210-fd6f-11eb-bc6f-3f06e1491664.png) ## 一、是什么 stash,译为存放,在 git 中,可以理解为保存当前工作进度,会把暂存区和工作区的改动进行保存,这些修改会保存在一个栈上 后续你可以在任何时候任何分支重新将某次的修改推出来,重新应用这些更改的代码 默认情况下,`git stash`会缓存下列状态的文件: - 添加到暂存区的修改(staged changes) - Git跟踪的但并未添加到暂存区的修改(unstaged changes) 但以下状态的文件不会缓存: - 在工作目录中新的文件(untracked files) - 被忽略的文件(ignored files) 如果想要上述的文件都被缓存,可以使用`-u`或者`--include-untracked`可以工作目录新的文件,使用`-a`或者`--all`命令可以当前目录下的所有修改 ## 二、如何使用 关于`git stash`常见的命令如下: - git stash - git stash save - git stash list - git stash pop - git stash apply - git stash show - git stash drop - git stash clear ### git stash 保存当前工作进度,会把暂存区和工作区的改动保存起来 ### git stash save `git stash save`可以用于存储修改.并且将`git`的工作状态切回到`HEAD`也就是上一次合法提交上 如果给定具体的文件路径,`git stash`只会处理路径下的文件.其他的文件不会被存储,其存在一些参数: - --keep-index 或者 -k 只会存储为加入 git 管理的文件 - --include-untracked 为追踪的文件也会被缓存,当前的工作空间会被恢复为完全清空的状态 - -a 或者 --all 命令可以当前目录下的所有修改,包括被 git 忽略的文件 ### git stash list 显示保存进度的列表。也就意味着,`git stash`命令可以多次执行,当多次使用`git stash`命令后,栈里会充满未提交的代码,如下: ![](https://static.vue-js.com/50216dd0-fccf-11eb-bc6f-3f06e1491664.png) 其中,`stash@{0}`、`stash@{1}`就是当前`stash`的名称 ### git stash pop `git stash pop` 从栈中读取最近一次保存的内容,也就是栈顶的`stash`会恢复到工作区 也可以通过 `git stash pop` + `stash`名字执行恢复哪个`stash`恢复到当前目录 如果从`stash`中恢复的内容和当前目录中的内容发生了冲突,则需要手动修复冲突或者创建新的分支来解决冲突 ### git stash apply 将堆栈中的内容应用到当前目录,不同于`git stash pop`,该命令不会将内容从堆栈中删除 也就说该命令能够将堆栈的内容多次应用到工作目录中,适应于多个分支的情况 同样,可以通过`git stash apply` + `stash`名字执行恢复哪个`stash`恢复到当前目录 ### git stash show 查看堆栈中最新保存的`stash`和当前目录的差异 通过使用`git stash show -p`查看详细的不同 通过使用`git stash show stash@{1}`查看指定的`stash`和当前目录差异 ![](https://static.vue-js.com/458620a0-fccf-11eb-bc6f-3f06e1491664.png) ### git stash drop `git stash drop` + `stash`名称表示从堆栈中移除某个指定的stash ### git stash clear 删除所有存储的进度 ## 三、应用场景 当你在项目的一部分上已经工作一段时间后,所有东西都进入了混乱的状态, 而这时你想要切换到另一个分支或者拉下远端的代码去做一点别的事情 但是你创建一次未完成的代码的`commit`提交,这时候就可以使用`git stash` 例如以下场景: 当你的开发进行到一半,但是代码还不想进行提交 ,然后需要同步去关联远端代码时.如果你本地的代码和远端代码没有冲突时,可以直接通过`git pull`解决 但是如果可能发生冲突怎么办.直接`git pull`会拒绝覆盖当前的修改,这时候就可以依次使用下述的命令: - git stash - git pull - git stash pop 或者当你开发到一半,现在要修改别的分支问题的时候,你也可以使用`git stash`缓存当前区域的代码 - git stash:保存开发到一半的代码 - git commit -m '修改问题' - git stash pop:将代码追加到最新的提交之后 ================================================ FILE: docs/http/1.0_1.1_2.0.md ================================================ # 面试官:说说 HTTP1.0/1.1/2.0 的区别? ![](https://static.vue-js.com/e167a580-b93a-11eb-ab90-d9ae814b240d.png) ## 一、HTTP1.0 `HTTP`协议的第二个版本,第一个在通讯中指定版本号的HTTP协议版本 `HTTP 1.0` 浏览器与服务器只保持短暂的连接,每次请求都需要与服务器建立一个`TCP`连接 服务器完成请求处理后立即断开`TCP`连接,服务器不跟踪每个客户也不记录过去的请求 简单来讲,每次与服务器交互,都需要新开一个连接 ![](https://static.vue-js.com/efff4da0-b93a-11eb-85f6-6fac77c0c9b3.png) 例如,解析`html`文件,当发现文件中存在资源文件的时候,这时候又创建单独的链接 最终导致,一个`html`文件的访问包含了多次的请求和响应,每次请求都需要创建连接、关系连接 这种形式明显造成了性能上的缺陷 如果需要建立长连接,需要设置一个非标准的Connection字段 `Connection: keep-alive` ## 二、HTTP1.1 在`HTTP1.1`中,默认支持长连接(`Connection: keep-alive`),即在一个TCP连接上可以传送多个`HTTP`请求和响应,减少了建立和关闭连接的消耗和延迟 建立一次连接,多次请求均由这个连接完成 ![](https://static.vue-js.com/22db2b90-b93b-11eb-ab90-d9ae814b240d.png) 这样,在加载`html`文件的时候,文件中多个请求和响应就可以在一个连接中传输 同时,`HTTP 1.1`还允许客户端不用等待上一次请求结果返回,就可以发出下一次请求,但服务器端必须按照接收到客户端请求的先后顺序依次回送响应结果,以保证客户端能够区分出每次请求的响应内容,这样也显著地减少了整个下载过程所需要的时间 同时,`HTTP1.1`在`HTTP1.0`的基础上,增加更多的请求头和响应头来完善的功能,如下: - 引入了更多的缓存控制策略,如If-Unmodified-Since, If-Match, If-None-Match等缓存头来控制缓存策略 - 引入range,允许值请求资源某个部分 - 引入host,实现了在一台WEB服务器上可以在同一个IP地址和端口号上使用不同的主机名来创建多个虚拟WEB站点 并且还添加了其他的请求方法:`put`、`delete`、`options`... ## 三、HTTP2.0 而`HTTP2.0`在相比之前版本,性能上有很大的提升,如添加了一个特性: - 多路复用 - 二进制分帧 - 首部压缩 - 服务器推送 ### 多路复用 `HTTP/2` 复用`TCP`连接,在一个连接里,客户端和浏览器都可以**同时**发送多个请求或回应,而且不用按照顺序一一对应,这样就避免了”队头堵塞” ![](https://static.vue-js.com/313f1980-b93b-11eb-85f6-6fac77c0c9b3.png) 上图中,可以看到第四步中`css`、`js`资源是同时发送到服务端 ### 二进制分帧 帧是`HTTP2`通信中最小单位信息 `HTTP/2` 采用二进制格式传输数据,而非 `HTTP 1.x `的文本格式,解析起来更高效 将请求和响应数据分割为更小的帧,并且它们采用二进制编码 `HTTP2 `中,同域名下所有通信都在单个连接上完成,该连接可以承载任意数量的双向数据流 每个数据流都以消息的形式发送,而消息又由一个或多个帧组成。多个帧之间可以乱序发送,根据帧首部的流标识可以重新组装,这也是多路复用同时发送数据的实现条件 ### 首部压缩 `HTTP/2`在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键值对,对于相同的数据,不再通过每次请求和响应发送 首部表在`HTTP/2`的连接存续期内始终存在,由客户端和服务器共同渐进地更新 例如:下图中的两个请求, 请求一发送了所有的头部字段,第二个请求则只需要发送差异数据,这样可以减少冗余数据,降低开销 ![](https://static.vue-js.com/3c536740-b93b-11eb-ab90-d9ae814b240d.png) ### 服务器推送 `HTTP2`引入服务器推送,允许服务端推送资源给客户端 服务器会顺便把一些客户端需要的资源一起推送到客户端,如在响应一个页面请求中,就可以随同页面的其它资源 免得客户端再次创建连接发送请求到服务器端获取 这种方式非常合适加载静态资源 ![](https://static.vue-js.com/47130550-b93b-11eb-85f6-6fac77c0c9b3.png) ## 四、总结 HTTP1.0: - 浏览器与服务器只保持短暂的连接,浏览器的每次请求都需要与服务器建立一个TCP连接 HTTP1.1: - 引入了持久连接,即TCP连接默认不关闭,可以被多个请求复用 - 在同一个TCP连接里面,客户端可以同时发送多个请求 - 虽然允许复用TCP连接,但是同一个TCP连接里面,所有的数据通信是按次序进行的,服务器只有处理完一个请求,才会接着处理下一个请求。如果前面的处理特别慢,后面就会有许多请求排队等着 - 新增了一些请求方法 - 新增了一些请求头和响应头 HTTP2.0: - 采用二进制格式而非文本格式 - 完全多路复用,而非有序并阻塞的、只需一个连接即可实现并行 - 使用报头压缩,降低开销 - 服务器推送 ## 参考文献 - https://zh.wikipedia.org/wiki/%E8%B6%85%E6%96%87%E6%9C%AC%E4%BC%A0%E8%BE%93%E5%8D%8F%E8%AE%AE#HTTP/1.0 - https://www.jianshu.com/p/52d86558ca57 - https://segmentfault.com/a/1190000016496448 - https://zhuanlan.zhihu.com/p/26559480 ================================================ FILE: docs/http/CDN.md ================================================ # 面试官:如何理解CDN?说说实现原理? ![](https://static.vue-js.com/437ae0f0-b86b-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 CDN (全称 Content Delivery Network),即内容分发网络 构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。`CDN` 的关键技术主要有内容存储和分发技术 简单来讲,`CDN`就是根据用户位置分配最近的资源 于是,用户在上网的时候不用直接访问源站,而是访问离他“最近的”一个 CDN 节点,术语叫**边缘节点**,其实就是缓存了源站内容的代理服务器。如下图: ![](https://static.vue-js.com/4f0289f0-b86b-11eb-85f6-6fac77c0c9b3.png) ## 二、原理分析 在没有应用`CDN`时,我们使用域名访问某一个站点时的路径为 > 用户提交域名→浏览器对域名进行解释→`DNS` 解析得到目的主机的IP地址→根据IP地址访问发出请求→得到请求数据并回复 应用`CDN`后,`DNS` 返回的不再是 `IP` 地址,而是一个`CNAME`(Canonical Name ) 别名记录,指向`CDN`的全局负载均衡 `CNAME`实际上在域名解析的过程中承担了中间人(或者说代理)的角色,这是`CDN`实现的关键 #### 负载均衡系统 由于没有返回`IP`地址,于是本地`DNS`会向负载均衡系统再发送请求 ,则进入到`CDN`的全局负载均衡系统进行智能调度: - 看用户的 IP 地址,查表得知地理位置,找相对最近的边缘节点 - 看用户所在的运营商网络,找相同网络的边缘节点 - 检查边缘节点的负载情况,找负载较轻的节点 - 其他,比如节点的“健康状况”、服务能力、带宽、响应时间等 结合上面的因素,得到最合适的边缘节点,然后把这个节点返回给用户,用户就能够就近访问`CDN`的缓存代理 整体流程如下图: ![](https://static.vue-js.com/588d7890-b86b-11eb-85f6-6fac77c0c9b3.png) #### 缓存代理 缓存系统是 `CDN `的另一个关键组成部分,缓存系统会有选择地缓存那些最常用的那些资源 其中有两个衡量`CDN`服务质量的指标: - 命中率:用户访问的资源恰好在缓存系统里,可以直接返回给用户,命中次数与所有访问次数之比 - 回源率:缓存里没有,必须用代理的方式回源站取,回源次数与所有访问次数之比 缓存系统也可以划分出层次,分成一级缓存节点和二级缓存节点。一级缓存配置高一些,直连源站,二级缓存配置低一些,直连用户 回源的时候二级缓存只找一级缓存,一级缓存没有才回源站,可以有效地减少真正的回源 现在的商业 `CDN`命中率都在 90% 以上,相当于把源站的服务能力放大了 10 倍以上 ## 三、总结 `CDN` 目的是为了改善互联网的服务质量,通俗一点说其实就是提高访问速度 `CDN` 构建了全国、全球级别的专网,让用户就近访问专网里的边缘节点,降低了传输延迟,实现了网站加速 通过`CDN`的负载均衡系统,智能调度边缘节点提供服务,相当于`CDN`服务的大脑,而缓存系统相当于`CDN`的心脏,缓存命中直接返回给用户,否则回源 ## 参考文献 - https://zh.wikipedia.org/wiki/內容傳遞網路 - https://juejin.cn/post/6844903890706661389#heading-5 - https://blog.csdn.net/lxx309707872/article/details/109078783 ================================================ FILE: docs/http/DNS.md ================================================ # 面试官:DNS协议 是什么?说说DNS 完整的查询过程? ![](https://static.vue-js.com/88081710-b78f-11eb-ab90-d9ae814b240d.png) ## 一、是什么 DNS(Domain Names System),域名系统,是互联网一项服务,是进行域名和与之相对应的 IP 地址进行转换的服务器 简单来讲,`DNS`相当于一个翻译官,负责将域名翻译成`ip`地址 - IP 地址:一长串能够唯一地标记网络上的计算机的数字 - 域名:是由一串用点分隔的名字组成的 Internet 上某一台计算机或计算机组的名称,用于在数据传输时对计算机的定位标识 ![](https://static.vue-js.com/965a03a0-b78f-11eb-ab90-d9ae814b240d.png) ## 二、域名 域名是一个具有层次的结构,从上到下一次为根域名、顶级域名、二级域名、三级域名... ![](https://static.vue-js.com/9f112780-b78f-11eb-85f6-6fac77c0c9b3.png) 例如`www.xxx.com`,`www`为三级域名、`xxx`为二级域名、`com`为顶级域名,系统为用户做了兼容,域名末尾的根域名`.`一般不需要输入 在域名的每一层都会有一个域名服务器,如下图: ![](https://static.vue-js.com/f40e0090-b7a4-11eb-85f6-6fac77c0c9b3.png) 除此之外,还有电脑默认的本地域名服务器 ## 三、查询方式 DNS 查询的方式有两种: - 递归查询:如果 A 请求 B,那么 B 作为请求的接收者一定要给 A 想要的答案 ![](https://static.vue-js.com/a73be9e0-b78f-11eb-85f6-6fac77c0c9b3.png) - 迭代查询:如果接收者 B 没有请求者 A 所需要的准确内容,接收者 B 将告诉请求者 A,如何去获得这个内容,但是自己并不去发出请求 ![](https://static.vue-js.com/b023e1c0-b78f-11eb-85f6-6fac77c0c9b3.png) ## 四、域名缓存 在域名服务器解析的时候,使用缓存保存域名和`IP`地址的映射 计算机中`DNS`的记录也分成了两种缓存方式: - 浏览器缓存:浏览器在获取网站域名的实际 IP 地址后会对其进行缓存,减少网络请求的损耗 - 操作系统缓存:操作系统的缓存其实是用户自己配置的 `hosts` 文件 ## 五、查询过程 解析域名的过程如下: - 首先搜索浏览器的 DNS 缓存,缓存中维护一张域名与 IP 地址的对应表 - 若没有命中,则继续搜索操作系统的 DNS 缓存 - 若仍然没有命中,则操作系统将域名发送至本地域名服务器,本地域名服务器采用递归查询自己的 DNS 缓存,查找成功则返回结果 - 若本地域名服务器的 DNS 缓存没有命中,则本地域名服务器向上级域名服务器进行迭代查询 - 首先本地域名服务器向根域名服务器发起请求,根域名服务器返回顶级域名服务器的地址给本地服务器 - 本地域名服务器拿到这个顶级域名服务器的地址后,就向其发起请求,获取权限域名服务器的地址 - 本地域名服务器根据权限域名服务器的地址向其发起请求,最终得到该域名对应的 IP 地址 - 本地域名服务器将得到的 IP 地址返回给操作系统,同时自己将 IP 地址缓存起来 - 操作系统将 IP 地址返回给浏览器,同时自己也将 IP 地址缓存起 - 至此,浏览器就得到了域名对应的 IP 地址,并将 IP 地址缓存起 流程如下图所示: ![](https://static.vue-js.com/bec3c740-b78f-11eb-ab90-d9ae814b240d.png) ## 参考文献 - https://zh.wikipedia.org/wiki/%E5%9F%9F%E5%90%8D%E7%B3%BB%E7%BB%9F - https://www.cnblogs.com/jmilkfan-fanguiju/p/12789677.html - https://segmentfault.com/a/1190000039039275 - https://vue3js.cn/interview ================================================ FILE: docs/http/GET_POST.md ================================================ # 面试官:说一下 GET 和 POST 的区别? ![](https://static.vue-js.com/6e8d19e0-bc3d-11eb-ab90-d9ae814b240d.png) ## 一、是什么 `GET`和`POST`,两者是`HTTP`协议中发送请求的方法 #### GET `GET`方法请求一个指定资源的表示形式,使用GET的请求应该只被用于获取数据 #### POST `POST`方法用于将实体提交到指定的资源,通常导致在服务器上的状态变化或**副作用** 本质上都是`TCP`链接,并无差别 但是由于`HTTP`的规定和浏览器/服务器的限制,导致他们在应用过程中会体现出一些区别 ## 二、区别 从`w3schools`得到的标准答案的区别如下: - GET在浏览器回退时是无害的,而POST会再次提交请求。 - GET产生的URL地址可以被Bookmark,而POST不可以。 - GET请求会被浏览器主动cache,而POST不会,除非手动设置。 - GET请求只能进行url编码,而POST支持多种编码方式。 - GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。 - GET请求在URL中传送的参数是有长度限制的,而POST没有。 - 对参数的数据类型,GET只接受ASCII字符,而POST没有限制。 - GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。 - GET参数通过URL传递,POST放在Request body中 ### 参数位置 貌似从上面看到`GET`与`POST`请求区别非常大,但两者实质并没有区别 无论 `GET `还是 `POST`,用的都是同一个传输层协议,所以在传输上没有区别 当不携带参数的时候,两者最大的区别为第一行方法名不同 > POST /uri HTTP/1.1 \r\n > > GET /uri HTTP/1.1 \r\n 当携带参数的时候,我们都知道`GET`请求是放在`url`中,`POST`则放在`body`中 `GET` 方法简约版报文是这样的 ``` GET /index.html?name=qiming.c&age=22 HTTP/1.1 Host: localhost ``` `POST `方法简约版报文是这样的 ``` POST /index.html HTTP/1.1 Host: localhost Content-Type: application/x-www-form-urlencoded name=qiming.c&age=22 ``` 注意:这里只是约定,并不属于`HTTP`规范,相反的,我们可以在`POST`请求中`url`中写入参数,或者`GET`请求中的`body`携带参数 ### 参数长度 `HTTP `协议没有` Body `和 `URL` 的长度限制,对 `URL `限制的大多是浏览器和服务器的原因 `IE`对`URL`长度的限制是2083字节(2K+35)。对于其他浏览器,如Netscape、FireFox等,理论上没有长度限制,其限制取决于操作系统的支持 这里限制的是整个`URL`长度,而不仅仅是参数值的长度 服务器处理长` URL` 要消耗比较多的资源,为了性能和安全考虑,会给 `URL` 长度加限制 ### 安全 `POST `比` GET` 安全,因为数据在地址栏上不可见 然而,从传输的角度来说,他们都是不安全的,因为` HTTP` 在网络上是明文传输的,只要在网络节点上捉包,就能完整地获取数据报文 只有使用`HTTPS`才能加密安全 ### 数据包 对于`GET`方式的请求,浏览器会把`http header`和`data`一并发送出去,服务器响应200(返回数据) 对于`POST`,浏览器先发送`header`,服务器响应100 `continue`,浏览器再发送`data`,服务器响应200 ok 并不是所有浏览器都会在`POST`中发送两次包,`Firefox`就只发送一次 ## 参考文献 - https://mp.weixin.qq.com/s?__biz=MzI3NzIzMzg3Mw==&mid=100000054&idx=1&sn=71f6c214f3833d9ca20b9f7dcd9d33e4#rd - https://blog.fundebug.com/2019/02/22/compare-http-method-get-and-post/ - https://www.w3school.com.cn/tags/html_ref_httpmethods.asp - https://vue3js.cn/interview ================================================ FILE: docs/http/HTTPS.md ================================================ # 面试官:为什么说HTTPS比HTTP安全? HTTPS是如何保证安全的? ![](https://static.vue-js.com/b5512250-b2ff-11eb-ab90-d9ae814b240d.png) ## 一、安全特性 在上篇文章中,我们了解到`HTTP`在通信过程中,存在以下问题: - 通信使用明文(不加密),内容可能被窃听 - 不验证通信方的身份,因此有可能遭遇伪装 而`HTTPS`的出现正是解决这些问题,`HTTPS`是建立在`SSL`之上,其安全性由`SSL`来保证 在采用`SSL`后,`HTTP`就拥有了`HTTPS`的加密、证书和完整性保护这些功能 > SSL(Secure Sockets Layer 安全套接字协议),及其继任者传输层安全(Transport Layer Security,TLS)是为网络通信提供安全及数据完整性的一种安全协议 ![](https://static.vue-js.com/cb559400-b2ff-11eb-85f6-6fac77c0c9b3.png) ## 二、如何做 `SSL `的实现这些功能主要依赖于三种手段: - 对称加密:采用协商的密钥对数据加密 - 非对称加密:实现身份认证和密钥协商 - 摘要算法:验证信息的完整性 - 数字签名:身份验证 ### 对称加密 对称加密指的是加密和解密使用的秘钥都是同一个,是对称的。只要保证了密钥的安全,那整个通信过程就可以说具有了机密性 ![](https://static.vue-js.com/e3f040f0-b2ff-11eb-ab90-d9ae814b240d.png) ### 非对称加密 非对称加密,存在两个秘钥,一个叫公钥,一个叫私钥。两个秘钥是不同的,公钥可以公开给任何人使用,私钥则需要保密 公钥和私钥都可以用来加密解密,但公钥加密后只能用私钥解 密,反过来,私钥加密后也只能用公钥解密 ![](https://static.vue-js.com/d9603e60-b2ff-11eb-ab90-d9ae814b240d.png) ### 混合加密 在`HTTPS`通信过程中,采用的是对称加密+非对称加密,也就是混合加密 在对称加密中讲到,如果能够保证了密钥的安全,那整个通信过程就可以说具有了机密性 而`HTTPS`采用非对称加密解决秘钥交换的问题 具体做法是发送密文的一方使用对方的公钥进行加密处理“对称的密钥”,然后对方用自己的私钥解密拿到“对称的密钥” ![](https://static.vue-js.com/f375f290-b2ff-11eb-85f6-6fac77c0c9b3.png) 这样可以确保交换的密钥是安全的前提下,使用对称加密方式进行通信 #### 举个例子: 网站秘密保管私钥,在网上任意分发公钥,你想要登录网站只要用公钥加密就行了,密文只能由私钥持有者才能解密。而黑客因为没有私钥,所以就无法破解密文 上述的方法解决了数据加密,在网络传输过程中,数据有可能被篡改,并且黑客可以伪造身份发布公钥,如果你获取到假的公钥,那么混合加密也并无多大用处,你的数据扔被黑客解决 因此,在上述加密的基础上仍需加上完整性、身份验证的特性,来实现真正的安全,实现这一功能则是摘要算法 ### 摘要算法 实现完整性的手段主要是摘要算法,也就是常说的散列函数、哈希函数 可以理解成一种特殊的压缩算法,它能够把任意长度的数据“压缩”成固定长度、而且独一无二的“摘要”字符串,就好像是给这段数据生成了一个数字“指纹” ![](https://static.vue-js.com/12798da0-b300-11eb-85f6-6fac77c0c9b3.png) 摘要算法保证了“数字摘要”和原文是完全等价的。所以,我们只要在原文后附上它的摘要,就能够保证数据的完整性 比如,你发了条消息:“转账 1000 元”,然后再加上一个 SHA-2 的摘要。网站收到后也计算一下消息的摘要,把这两份“指纹”做个对比,如果一致,就说明消息是完整可信的,没有被修改 ![](https://static.vue-js.com/023790e0-b300-11eb-ab90-d9ae814b240d.png) ### 数字签名 数字签名能确定消息确实是由发送方签名并发出来的,因为别人假冒不了发送方的签名 原理其实很简单,就是用私钥加密,公钥解密 签名和公钥一样完全公开,任何人都可以获取。但这个签名只有用私钥对应的公钥才能解开,拿到摘要后,再比对原文验证完整性,就可以像签署文件一样证明消息确实是你发的 ![](https://static.vue-js.com/21aa6880-b300-11eb-85f6-6fac77c0c9b3.png) 和消息本身一样,因为谁都可以发布公钥,我们还缺少防止黑客伪造公钥的手段,也就是说,怎么判断这个公钥就是你的公钥 这时候就需要一个第三方,就是证书验证机构 ### CA验证机构 数字证书认证机构处于客户端与服务器双方都可信赖的第三方机构的立场 CA 对公钥的签名认证要求包括序列号、用途、颁发者、有效时间等等,把这些打成一个包再签名,完整地证明公钥关联的各种信息,形成“数字证书” 流程如下图: ![](https://static.vue-js.com/395648a0-b300-11eb-85f6-6fac77c0c9b3.png) - 服务器的运营人员向数字证书认证机构提出公开密钥的申请 - 数字证书认证机构在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名 - 然后分配这个已签名的公开密钥,并将该公开密钥放入公钥证书后绑定在一起 - 服务器会将这份由数字证书认证机构颁发的数字证书发送给客户端,以进行非对称加密方式通信 接到证书的客户端可使用数字证书认证机构的公开密钥,对那张证书上的数字签名进行验证,一旦验证通过,则证明: - 认证服务器的公开密钥的是真实有效的数字证书认证机构 - 服务器的公开密钥是值得信赖的 ## 三、总结 可以看到,`HTTPS`与`HTTP`虽然只差一个`SSL`,但是通信安全得到了大大的保障,通信的四大特性都以解决,解决方式如下: - 机密性:混合算法 - 完整性:摘要算法 - 身份认证:数字签名 - 不可否定:数字签名 同时引入第三方证书机构,确保公开秘钥的安全性 ## 参考文献 - https://zhuanlan.zhihu.com/p/100657391 - https://juejin.cn/post/6844903830987997197#heading-7 - https://cloud.tencent.com/developer/article/1748862 ================================================ FILE: docs/http/HTTP_HTTPS.md ================================================ # 面试官:什么是HTTP? HTTP 和 HTTPS 的区别? ![](https://static.vue-js.com/f50c71f0-b20b-11eb-ab90-d9ae814b240d.png) ## 一、HTTP `HTTP` (HyperText Transfer Protocol),即超文本运输协议,是实现网络通信的一种规范 ![](https://static.vue-js.com/fda119b0-b20b-11eb-85f6-6fac77c0c9b3.png) 在计算机和网络世界有,存在不同的协议,如广播协议、寻址协议、路由协议等等...... 而`HTTP`是一个传输协议,即将数据由A传到B或将B传输到A,并且 A 与 B 之间能够存放很多第三方,如: A<=>X<=>Y<=>Z<=>B 传输的数据并不是计算机底层中的二进制包,而是完整的、有意义的数据,如HTML 文件, 图片文件, 查询结果等超文本,能够被上层应用识别 在实际应用中,`HTTP`常被用于在`Web`浏览器和网站服务器之间传递信息,以明文方式发送内容,不提供任何方式的数据加密 特点如下: - 支持客户/服务器模式 - 简单快速:客户向服务器请求服务时,只需传送请求方法和路径。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快 - 灵活:HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type加以标记 - 无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间 - 无状态:HTTP协议无法根据之前的状态进行本次的请求处理 ## 二、HTTPS 在上述介绍`HTTP`中,了解到`HTTP`传递信息是以明文的形式发送内容,这并不安全。而`HTTPS`出现正是为了解决`HTTP`不安全的特性 为了保证这些隐私数据能加密传输,让`HTTP`运行安全的`SSL/TLS`协议上,即 HTTPS = HTTP + SSL/TLS,通过 `SSL`证书来验证服务器的身份,并为浏览器和服务器之间的通信进行加密 `SSL` 协议位于` TCP/IP` 协议与各种应用层协议之间,浏览器和服务器在使用 `SSL` 建立连接时需要选择一组恰当的加密算法来实现安全通信,为数据通讯提供安全支持 ![](https://static.vue-js.com/078c50c0-b20c-11eb-ab90-d9ae814b240d.png) 流程图如下所示: ![](https://static.vue-js.com/0e409fc0-b20c-11eb-85f6-6fac77c0c9b3.png) - 首先客户端通过URL访问服务器建立SSL连接 - 服务端收到客户端请求后,会将网站支持的证书信息(证书中包含公钥)传送一份给客户端 - 客户端的服务器开始协商SSL连接的安全等级,也就是信息加密的等级 - 客户端的浏览器根据双方同意的安全等级,建立会话密钥,然后利用网站的公钥将会话密钥加密,并传送给网站 - 服务器利用自己的私钥解密出会话密钥 - 服务器利用会话密钥加密与客户端之间的通信 ## 三、区别 - HTTPS是HTTP协议的安全版本,HTTP协议的数据传输是明文的,是不安全的,HTTPS使用了SSL/TLS协议进行了加密处理,相对更安全 - HTTP 和 HTTPS 使用连接方式不同,默认端口也不一样,HTTP是80,HTTPS是443 - HTTPS 由于需要设计加密以及多次握手,性能方面不如 HTTP - HTTPS需要SSL,SSL 证书需要钱,功能越强大的证书费用越高 ## 参考文献 - https://www.cnblogs.com/klb561/p/10289199.html - https://www.jianshu.com/p/205c0fc51c97 - https://vue3js.cn/interview ================================================ FILE: docs/http/OSI.md ================================================ # 面试官:如何理解OSI七层模型? ![](https://static.vue-js.com/e2e1b910-b61e-11eb-ab90-d9ae814b240d.png) ## 一、是什么 OSI (Open System Interconnect)模型全称为开放式通信系统互连参考模型,是国际标准化组织 ( ISO ) 提出的一个试图使各种计算机在世界范围内互连为网络的标准框架 `OSI `将计算机网络体系结构划分为七层,每一层实现各自的功能和协议,并完成与相邻层的接口通信。即每一层扮演固定的角色,互不打扰 ## 二、划分 `OSI`主要划分了七层,如下图所示: ![](https://static.vue-js.com/eb1b2170-b61e-11eb-ab90-d9ae814b240d.png) ### 应用层 应用层位于 OSI 参考模型的第七层,其作用是通过应用程序间的交互来完成特定的网络应用 该层协议定义了应用进程之间的交互规则,通过不同的应用层协议为不同的网络应用提供服务。例如域名系统 `DNS`,支持万维网应用的 `HTTP` 协议,电子邮件系统采用的 `SMTP `协议等 在应用层交互的数据单元我们称之为报文 ### 表示层 表示层的作用是使通信的应用程序能够解释交换数据的含义,其位于 `OSI `参考模型的第六层,向上为应用层提供服务,向下接收来自会话层的服务 该层提供的服务主要包括数据压缩,数据加密以及数据描述,使应用程序不必担心在各台计算机中表示和存储的内部格式差异 ### 会话层 会话层就是负责建立、管理和终止表示层实体之间的通信会话 该层提供了数据交换的定界和同步功能,包括了建立检查点和恢复方案的方法 ### 传输层 传输层的主要任务是为两台主机进程之间的通信提供服务,处理数据包错误、数据包次序,以及其他一些关键传输问题 传输层向高层屏蔽了下层数据通信的细节。因此,它是计算机通信体系结构中关键的一层 其中,主要的传输层协议是`TCP`和`UDP` ### 网络层 两台计算机之间传送数据时其通信链路往往不止一条,所传输的信息甚至可能经过很多通信子网 网络层的主要任务就是选择合适的网间路由和交换节点,确保数据按时成功传送 在发送数据时,网络层把传输层产生的报文或用户数据报封装成分组和包,向下传输到数据链路层 在网络层使用的协议是无连接的网际协议(Internet Protocol)和许多路由协议,因此我们通常把该层简单地称为 IP 层 ### 数据链路层 数据链路层通常也叫做链路层,在物理层和网络层之间。两台主机之间的数据传输,总是在一段一段的链路上传送的,这就需要使用专门的链路层协议 在两个相邻节点之间传送数据时,数据链路层将网络层交下来的 `IP `数据报组装成帧,在两个相邻节点间的链路上传送帧 每一帧的数据可以分成:报头`head`和数据`data`两部分: - head 标明数据发送者、接受者、数据类型,如 MAC地址 - data 存储了计算机之间交互的数据 通过控制信息我们可以知道一个帧的起止比特位置,此外,也能使接收端检测出所收到的帧有无差错,如果发现差错,数据链路层能够简单的丢弃掉这个帧,以避免继续占用网络资源 ### 物理层 作为` OSI` 参考模型中最低的一层,物理层的作用是实现计算机节点之间比特流的透明传送 该层的主要任务是确定与传输媒体的接口的一些特性(机械特性、电气特性、功能特性,过程特性) 该层主要是和硬件有关,与软件关系不大 ## 三、传输过程 数据在各层之间的传输如下图所示: ![](https://static.vue-js.com/f3a89d40-b61e-11eb-85f6-6fac77c0c9b3.png) - 应用层报文被传送到运输层 - 在最简单的情况下,运输层收取到报文并附上附加信息,该首部将被接收端的运输层使用 - 应用层报文和运输层首部信息一道构成了运输层报文段。附加的信息可能包括:允许接收端运输层向上向适当的应用程序交付报文的信息以及差错检测位信息。该信息让接收端能够判断报文中的比特是否在途中已被改变 - 运输层则向网络层传递该报文段,网络层增加了如源和目的端系统地址等网络层首部信息,生成了网络层数据报 - 网络层数据报接下来被传递给链路层,在数据链路层数据包添加发送端 MAC 地址和接收端 MAC 地址后被封装成数据帧 - 在物理层数据帧被封装成比特流,之后通过传输介质传送到对端 - 对端再一步步解开封装,获取到传送的数据 ## 参考文献 - https://zh.wikipedia.org/wiki/OSI%E6%A8%A1%E5%9E%8B - https://zhuanlan.zhihu.com/p/32059190 - https://leetcode-cn.com/leetbook/detail/networks-interview-highlights/ - https://vue3js.cn/interview ================================================ FILE: docs/http/TCP_IP.md ================================================ # 面试官:如何理解TCP/IP协议? ![](https://static.vue-js.com/4f69a930-b647-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 TCP/IP,**传输控制协议**/**网际协议**,是指能够在多个不同网络间实现信息传输的协议簇 - TCP(传输控制协议) 一种面向连接的、可靠的、基于字节流的传输层通信协议 - IP(网际协议) 用于封包交换数据网络的协议 TCP/IP协议不仅仅指的是`TCP `和`IP`两个协议,而是指一个由`FTP`、`SMTP`、`TCP`、`UDP`、`IP`等协议构成的协议簇, 只是因为在`TCP/IP`协议中`TCP`协议和`IP`协议最具代表性,所以通称为TCP/IP协议族(英语:TCP/IP Protocol Suite,或TCP/IP Protocols) ## 二、划分 TCP/IP协议族按层次分别了五层体系或者四层体系 五层体系的协议结构是综合了 OSI 和 TCP/IP 优点的一种协议,包括应用层、传输层、网络层、数据链路层和物理层 五层协议的体系结构只是为介绍网络原理而设计的,实际应用还是 TCP/IP 四层体系结构,包括应用层、传输层、网络层(网际互联层)、网络接口层 如下图所示: ![](https://static.vue-js.com/5bb93610-b647-11eb-85f6-6fac77c0c9b3.png) ### 五层体系 #### 应用层 `TCP/IP` 模型将 `OSI `参考模型中的会话层、表示层和应用层的功能合并到一个应用层实现,通过不同的应用层协议为不同的应用提供服务 如:`FTP`、`Telnet`、`DNS`、`SMTP` 等 #### 传输层 该层对应于 OSI 参考模型的传输层,为上层实体提供源端到对端主机的通信功能 传输层定义了两个主要协议:传输控制协议(TCP)和用户数据报协议(UDP) 其中面向连接的 TCP 协议保证了数据的传输可靠性,面向无连接的 UDP 协议能够实现数据包简单、快速地传输 #### 网络层 负责为分组网络中的不同主机提供通信服务,并通过选择合适的路由将数据传递到目标主机 在发送数据时,网络层把运输层产生的报文段或用户数据封装成分组或包进行传送 #### 数据链路层 数据链路层在两个相邻节点传输数据时,将网络层交下来的IP数据报组装成帧,在两个相邻节点之间的链路上传送帧 #### 物理层 保数据可以在各种物理媒介上进行传输,为数据的传输提供可靠的环境 ### 四层体系 TCP/IP 的四层结构则如下表所示: | 层次名称 | 单位 | 功 能 | 协 议 | | ---------- | ------ | --------------------------------------------------------- | ------------------------------------------------------------ | | 网络接口层 | 帧 | 负责实际数据的传输,对应OSI参考模型的下两层 | HDLC(高级链路控制协议)PPP(点对点协议) SLIP(串行线路接口协议) | | 网络层 | 数据报 | 负责网络间的寻址数据传输,对应OSI参考模型的第三层 | IP(网际协议) ICMP(网际控制消息协议)ARP(地址解析协议) RARP(反向地址解析协议) | | 传输层 | 报文段 | 负责提供可靠的传输服务,对应OSI参考模型的第四层 | TCP(控制传输协议) UDP(用户数据报协议) | | 应用层 | | 负责实现一切与应用程序相关的功能,对应OSI参考模型的上三层 | FTP(文件传输协议) HTTP(超文本传输协议) DNS(域名服务器协议)SMTP(简单邮件传输协议)NFS(网络文件系统协议) | ## 三、总结 OSI 参考模型与 TCP/IP 参考模型区别如下: 相同点: - OSI 参考模型与 TCP/IP 参考模型都采用了层次结构 - 都能够提供面向连接和无连接两种通信服务机制 不同点: - OSI 采用的七层模型; TCP/IP 是四层或五层结构 - TCP/IP 参考模型没有对网络接口层进行细分,只是一些概念性的描述; OSI 参考模型对服务和协议做了明确的区分 - OSI 参考模型虽然网络划分为七层,但实现起来较困难。TCP/IP 参考模型作为一种简化的分层结构是可以的 - TCP/IP协议去掉表示层和会话层的原因在于会话层、表示层、应用层都是在应用程序内部实现的,最终产出的是一个应用数据包,而应用程序之间是几乎无法实现代码的抽象共享的,这也就造成 `OSI` 设想中的应用程序维度的分层是无法实现的 三种模型对应关系如下图所示: ![](https://static.vue-js.com/3fbff4d0-b647-11eb-ab90-d9ae814b240d.png) ## 参考文献 - https://zh.wikipedia.org/wiki/TCP/IP%E5%8D%8F%E8%AE%AE%E6%97%8F - https://zhuanlan.zhihu.com/p/103162095 - https://segmentfault.com/a/1190000039204681 - https://leetcode-cn.com/leetbook/detail/networks-interview-highlights/ - https://vue3js.cn/interview ================================================ FILE: docs/http/UDP_TCP.md ================================================ # 面试官:如何理解UDP 和 TCP? 区别? 应用场景? ![](https://static.vue-js.com/85ad65b0-b393-11eb-ab90-d9ae814b240d.png) ## 一、UDP UDP(User Datagram Protocol),用户数据包协议,是一个简单的**面向数据报的通信协议**,即对应用层交下来的报文,不合并,不拆分,只是在其上面加上首部后就交给了下面的网络层 也就是说无论应用层交给`UDP`多长的报文,它统统发送,一次发送一个报文 而对接收方,接到后直接去除首部,交给上面的应用层就完成任务 `UDP`报头包括4个字段,每个字段占用2个字节(即16个二进制位),标题短,开销小 ![](https://static.vue-js.com/928e5d20-b393-11eb-ab90-d9ae814b240d.png) 特点如下: - UDP 不提供复杂的控制机制,利用 IP 提供面向无连接的通信服务 - 传输途中出现丢包,UDP 也不负责重发 - 当包的到达顺序出现乱序时,UDP没有纠正的功能。 - 并且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的一种机制。即使是出现网络拥堵的情况,UDP 也无法进行流量控制等避免网络拥塞行为 ## 二、TCP TCP(Transmission Control Protocol),传输控制协议,是一种可靠、**面向字节流的通信协议**,把上面应用层交下来的数据看成无结构的字节流来发送 可以想象成流水形式的,发送方TCP会将数据放入“蓄水池”(缓存区),等到可以发送的时候就发送,不能发送就等着,TCP会根据当前网络的拥塞状态来确定每个报文段的大小 `TCP`报文首部有20个字节,额外开销大 ![](https://static.vue-js.com/a0010d40-b393-11eb-ab90-d9ae814b240d.png) 特点如下: - TCP充分地实现了数据传输时各种控制功能,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。而这些在 UDP 中都没有。 - 此外,TCP 作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而可以控制通信流量的浪费。 - 根据 TCP 的这些机制,在 IP 这种无连接的网络上也能够实现高可靠性的通信( 主要通过检验和、序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现) ## 三、区别 `UDP`与`TCP`两者的都位于传输层,如下图所示: ![](https://static.vue-js.com/a92bda80-b393-11eb-ab90-d9ae814b240d.png) 两者区别如下表所示: | | TCP | UDP | | -------- | -------------------------------- | ------------------------------ | | 可靠性 | 可靠 | 不可靠 | | 连接性 | 面向连接 | 无连接 | | 报文 | 面向字节流 | 面向报文 | | 效率 | 传输效率低 | 传输效率高 | | 双共性 | 全双工 | 一对一、一对多、多对一、多对多 | | 流量控制 | 滑动窗口 | 无 | | 拥塞控制 | 慢开始、拥塞避免、快重传、快恢复 | 无 | | 传输效率 | 慢 | 快 | - TCP 是面向连接的协议,建立连接3次握手、断开连接四次挥手,UDP是面向无连接,数据传输前后不连接连接,发送端只负责将数据发送到网络,接收端从消息队列读取 - TCP 提供可靠的服务,传输过程采用流量控制、编号与确认、计时器等手段确保数据无差错,不丢失。UDP 则尽可能传递数据,但不保证传递交付给对方 - TCP 面向字节流,将应用层报文看成一串无结构的字节流,分解为多个TCP报文段传输后,在目的站重新装配。UDP协议面向报文,不拆分应用层报文,只保留报文边界,一次发送一个报文,接收方去除报文首部后,原封不动将报文交给上层应用 - TCP 只能点对点全双工通信。UDP 支持一对一、一对多、多对一和多对多的交互通信 两者应用场景如下图: ![](https://static.vue-js.com/b6cdd800-b393-11eb-ab90-d9ae814b240d.png) 可以看到,TCP 应用场景适用于对效率要求低,对准确性要求高或者要求有链接的场景,而UDP 适用场景为对效率要求高,对准确性要求低的场景 ## 参考文献 - https://zh.wikipedia.org - https://www.shangmayuan.com/a/a1e3ceb218284cefb95de7fd.html - https://segmentfault.com/a/1190000021815671 - https://vue3js.cn/interview ================================================ FILE: docs/http/WebSocket.md ================================================ # 面试官:说说对WebSocket的理解?应用场景? ![](https://static.vue-js.com/a358a8c0-c0f1-11eb-ab90-d9ae814b240d.png) ## 一、是什么 WebSocket,是一种网络传输协议,位于`OSI`模型的应用层。可在单个`TCP`连接上进行全双工通信,能更好的节省服务器资源和带宽并达到实时通迅 客户端和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输 ![](https://static.vue-js.com/ad386e20-c0f1-11eb-85f6-6fac77c0c9b3.png) 从上图可见,`websocket`服务器与客户端通过握手连接,连接成功后,两者都能主动的向对方发送或接受数据 而在`websocket`出现之前,开发实时`web`应用的方式为轮询 不停地向服务器发送 HTTP 请求,问有没有数据,有数据的话服务器就用响应报文回应。如果轮询的频率比较高,那么就可以近似地实现“实时通信”的效果 轮询的缺点也很明显,反复发送无效查询请求耗费了大量的带宽和 `CPU `资源 ## 二、特点 ### 全双工 通信允许数据在两个方向上同时传输,它在能力上相当于两个单工通信方式的结合 例如指 A→B 的同时 B→A ,是瞬时同步的 ### 二进制帧 采用了二进制帧结构,语法、语义与 HTTP 完全不兼容,相比`http/2`,`WebSocket `更侧重于“实时通信”,而`HTTP/2` 更侧重于提高传输效率,所以两者的帧结构也有很大的区别 不像 `HTTP/2` 那样定义流,也就不存在多路复用、优先级等特性 自身就是全双工,也不需要服务器推送 ### 协议名 引入`ws`和`wss`分别代表明文和密文的`websocket`协议,且默认端口使用80或443,几乎与`http`一致 ```http ws://www.chrono.com ws://www.chrono.com:8080/srv wss://www.chrono.com:445/im?user_id=xxx ``` ### 握手 `WebSocket `也要有一个握手过程,然后才能正式收发数据 客户端发送数据格式如下: ```http GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Origin: http://example.com Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 ``` - Connection:必须设置Upgrade,表示客户端希望连接升级 - Upgrade:必须设置Websocket,表示希望升级到Websocket协议 - Sec-WebSocket-Key:客户端发送的一个 base64 编码的密文,用于简单的认证秘钥。要求服务端必须返回一个对应加密的“Sec-WebSocket-Accept应答,否则客户端会抛出错误,并关闭连接 - Sec-WebSocket-Version :表示支持的Websocket版本 服务端返回的数据格式: ```http HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Sec-WebSocket-Protocol: chat ``` - HTTP/1.1 101 Switching Protocols:表示服务端接受 WebSocket 协议的客户端连接 - Sec-WebSocket-Accep:验证客户端请求报文,同样也是为了防止误连接。具体做法是把请求头里“Sec-WebSocket-Key”的值,加上一个专用的 UUID,再计算摘要 ### 优点 - 较少的控制开销:数据包头部协议较小,不同于http每次请求需要携带完整的头部 - 更强的实时性:相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少 - 保持创连接状态:创建通信后,可省略状态信息,不同于HTTP每次请求需要携带身份验证 - 更好的二进制支持:定义了二进制帧,更好处理二进制内容 - 支持扩展:用户可以扩展websocket协议、实现部分自定义的子协议 - 更好的压缩效果:Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率 ## 二、应用场景 基于`websocket`的事实通信的特点,其存在的应用场景大概有: - 弹幕 - 媒体聊天 - 协同编辑 - 基于位置的应用 - 体育实况更新 - 股票基金报价实时更新 ## 参考文献 - https://zh.wikipedia.org/wiki/WebSocket - https://www.oschina.net/translate/9-killer-uses-for-websockets - https://vue3js.cn/interview ================================================ FILE: docs/http/after_url.md ================================================ # 面试官:说说地址栏输入 URL 敲下回车后发生了什么? ![](https://static.vue-js.com/11bf1f20-bdf4-11eb-85f6-6fac77c0c9b3.png) ## 一、简单分析 简单的分析,从输入 `URL`到回车后发生的行为如下: - URL解析 - DNS 查询 - TCP 连接 - HTTP 请求 - 响应请求 - 页面渲染 ## 二、详细分析 ### URL解析 首先判断你输入的是一个合法的` URL` 还是一个待搜索的关键词,并且根据你输入的内容进行对应操作 `URL`的解析第过程中的第一步,一个`url`的结构解析如下: ![](https://static.vue-js.com/27a0c690-bdf4-11eb-ab90-d9ae814b240d.png) ### DNS查询 在之前文章中讲过`DNS`的查询,这里就不再讲述了 整个查询过程如下图所示: ![](https://static.vue-js.com/330fb770-bdf4-11eb-85f6-6fac77c0c9b3.png) 最终,获取到了域名对应的目标服务器`IP`地址 ### TCP连接 在之前文章中,了解到`tcp`是一种面向有连接的传输层协议 在确定目标服务器服务器的`IP`地址后,则经历三次握手建立`TCP`连接,流程如下: ![](https://static.vue-js.com/ad750790-bdf4-11eb-85f6-6fac77c0c9b3.png) ### 发送 http 请求 当建立`tcp`连接之后,就可以在这基础上进行通信,浏览器发送 `http` 请求到目标服务器 请求的内容包括: - 请求行 - 请求头 - 请求主体 ![](https://static.vue-js.com/bbcb60f0-bdf4-11eb-ab90-d9ae814b240d.png) ### 响应请求 当服务器接收到浏览器的请求之后,就会进行逻辑操作,处理完成之后返回一个`HTTP`响应消息,包括: - 状态行 - 响应头 - 响应正文 ![](https://static.vue-js.com/c5fe0140-bdf4-11eb-ab90-d9ae814b240d.png) 在服务器响应之后,由于现在`http`默认开始长连接`keep-alive`,当页面关闭之后,`tcp`链接则会经过四次挥手完成断开 ### 页面渲染 当浏览器接收到服务器响应的资源后,首先会对资源进行解析: - 查看响应头的信息,根据不同的指示做对应处理,比如重定向,存储cookie,解压gzip,缓存资源等等 - 查看响应头的 Content-Type的值,根据不同的资源类型采用不同的解析方式 关于页面的渲染过程如下: - 解析HTML,构建 DOM 树 - 解析 CSS ,生成 CSS 规则树 - 合并 DOM 树和 CSS 规则,生成 render 树 - 布局 render 树( Layout / reflow ),负责各元素尺寸、位置的计算 - 绘制 render 树( paint ),绘制页面像素信息 - 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成( composite ),显示在屏幕上 ![](https://static.vue-js.com/db7bddd0-bdf4-11eb-85f6-6fac77c0c9b3.png) ## 参考文献 - https://github.com/febobo/web-interview/issues/141 - https://zhuanlan.zhihu.com/p/80551769 ================================================ FILE: docs/http/handshakes_waves.md ================================================ # 面试官:说说TCP为什么需要三次握手和四次挥手? ![](https://static.vue-js.com/ef4696a0-beb9-11eb-ab90-d9ae814b240d.png) ## 一、三次握手 三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包 主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备 过程如下: - 第一次握手:客户端给服务端发一个 SYN 报文,并指明客户端的初始化序列号 ISN(c),此时客户端处于 SYN_SENT 状态 - 第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,为了确认客户端的 SYN,将客户端的 ISN+1作为ACK的值,此时服务器处于 SYN_RCVD 的状态 - 第三次握手:客户端收到 SYN 报文之后,会发送一个 ACK 报文,值为服务器的ISN+1。此时客户端处于 ESTABLISHED 状态。服务器收到 ACK 报文之后,也处于 ESTABLISHED 状态,此时,双方已建立起了连接 ![](https://static.vue-js.com/fb489fc0-beb9-11eb-85f6-6fac77c0c9b3.png) 上述每一次握手的作用如下: - 第一次握手:客户端发送网络包,服务端收到了 这样服务端就能得出结论:客户端的发送能力、服务端的接收能力是正常的。 - 第二次握手:服务端发包,客户端收到了 这样客户端就能得出结论:服务端的接收、发送能力,客户端的接收、发送能力是正常的。不过此时服务器并不能确认客户端的接收能力是否正常 - 第三次握手:客户端发包,服务端收到了。 这样服务端就能得出结论:客户端的接收、发送能力正常,服务器自己的发送、接收能力也正常 通过三次握手,就能确定双方的接收和发送能力是正常的。之后就可以正常通信了 ### 为什么不是两次握手? 如果是两次握手,发送端可以确定自己发送的信息能对方能收到,也能确定对方发的包自己能收到,但接收端只能确定对方发的包自己能收到 无法确定自己发的包对方能收到 并且两次握手的话, 客户端有可能因为网络阻塞等原因会发送多个请求报文,延时到达的请求又会与服务器建立连接,浪费掉许多服务器的资源 ## 二、四次挥手 `tcp`终止一个连接,需要经过四次挥手 过程如下: - 第一次挥手:客户端发送一个 FIN 报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1 状态,停止发送数据,等待服务端的确认 - 第二次挥手:服务端收到 FIN 之后,会发送 ACK 报文,且把客户端的序列号值 +1 作为 ACK 报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT状态 - 第三次挥手:如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN 报文,且指定一个序列号。此时服务端处于 `LAST_ACK` 的状态 - 第四次挥手:客户端收到 FIN 之后,一样发送一个 ACK 报文作为应答,且把服务端的序列号值 +1 作为自己 ACK 报文的序列号值,此时客户端处于 TIME_WAIT状态。需要过一阵子以确保服务端收到自己的 ACK 报文之后才会进入 CLOSED 状态,服务端收到 ACK 报文之后,就处于关闭连接了,处于 CLOSED 状态 ![](https://static.vue-js.com/0a3ebb90-beba-11eb-85f6-6fac77c0c9b3.png) ### 四次挥手原因 服务端在收到客户端断开连接`Fin`报文后,并不会立即关闭连接,而是先发送一个`ACK`包先告诉客户端收到关闭连接的请求,只有当服务器的所有报文发送完毕之后,才发送`FIN`报文断开连接,因此需要四次挥手 ## 三、总结 一个完整的三次握手四次挥手如下图所示: ![](https://static.vue-js.com/65941490-beba-11eb-85f6-6fac77c0c9b3.png) ## 参考文献 - https://zhuanlan.zhihu.com/p/53374516 - https://segmentfault.com/a/1190000020610336 ================================================ FILE: docs/http/headers.md ================================================ # 面试官:说说 HTTP 常见的请求头有哪些? 作用? ![](https://static.vue-js.com/964abb00-bc69-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 HTTP头字段(HTTP header fields),是指在超文本传输协议(HTTP)的请求和响应消息中的消息头部分 它们定义了一个超文本传输协议事务中的操作参数 HTTP头部字段可以自己根据需要定义,因此可能在 `Web `服务器和浏览器上发现非标准的头字段 下面是一个`HTTP`请求的请求头: ```http GET /home.html HTTP/1.1 Host: developer.mozilla.org User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:50.0) Gecko/20100101 Firefox/50.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.5 Accept-Encoding: gzip, deflate, br Referer: https://developer.mozilla.org/testpage.html Connection: keep-alive Upgrade-Insecure-Requests: 1 If-Modified-Since: Mon, 18 Jul 2016 02:36:04 GMT If-None-Match: "c561c68d0ba92bbeb8b0fff2a9199f722e3a621a" Cache-Control: max-age=0 ``` ## 二、分类 常见的请求字段如下表所示: | 字段名 | 说明 | 示例 | | ----------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | Accept | 能够接受的回应内容类型(Content-Types) | Accept: text/plain | | Accept-Charset | 能够接受的字符集 | Accept-Charset: utf-8 | | Accept-Encoding | 能够接受的编码方式列表 | Accept-Encoding: gzip, deflate | | Accept-Language | 能够接受的回应内容的自然语言列表 | Accept-Language: en-US | | Authorization | 用于超文本传输协议的认证的认证信息 | Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== | | Cache-Control | 用来指定在这次的请求/响应链中的所有缓存机制 都必须 遵守的指令 | Cache-Control: no-cache | | Connection | 该浏览器想要优先使用的连接类型 | Connection: keep-alive Connection: Upgrade | | Cookie | 服务器通过 Set- Cookie (下文详述)发送的一个 超文本传输协议Cookie | Cookie: $Version=1; Skin=new; | | Content-Length | 以 八位字节数组 (8位的字节)表示的请求体的长度 | Content-Length: 348 | | Content-Type | 请求体的 多媒体类型 | Content-Type: application/x-www-form-urlencoded | | Date | 发送该消息的日期和时间 | Date: Tue, 15 Nov 1994 08:12:31 GMT | | Expect | 表明客户端要求服务器做出特定的行为 | Expect: 100-continue | | Host | 服务器的域名(用于虚拟主机 ),以及服务器所监听的传输控制协议端口号 | Host: en.wikipedia.org:80 Host: en.wikipedia.org | | If-Match | 仅当客户端提供的实体与服务器上对应的实体相匹配时,才进行对应的操作。主要作用时,用作像 PUT 这样的方法中,仅当从用户上次更新某个资源以来,该资源未被修改的情况下,才更新该资源 | If-Match: "737060cd8c284d8af7ad3082f209582d" | | If-Modified-Since | 允许在对应的内容未被修改的情况下返回304未修改 | If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT | | If-None-Match | 允许在对应的内容未被修改的情况下返回304未修改 | If-None-Match: "737060cd8c284d8af7ad3082f209582d" | | If-Range | 如果该实体未被修改过,则向我发送我所缺少的那一个或多个部分;否则,发送整个新的实体 | If-Range: "737060cd8c284d8af7ad3082f209582d" | | Range | 仅请求某个实体的一部分 | Range: bytes=500-999 | | User-Agent | 浏览器的浏览器身份标识字符串 | User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/21.0 | | Origin | 发起一个针对 跨来源资源共享 的请求 | Origin: http://www.example-social-network.com | ## 三、使用场景 通过配合请求头和响应头,可以满足一些场景的功能实现: ### 协商缓存 协商缓存是利用的是`【Last-Modified,If-Modified-Since】`和`【ETag、If-None-Match】`这两对请求头响应头来管理的 `Last-Modified` 表示本地文件最后修改日期,浏览器会在request header加上`If-Modified-Since`(上次返回的`Last-Modified`的值),询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来 `Etag`就像一个指纹,资源变化都会导致`ETag`变化,跟最后修改时间没有关系,`ETag`可以保证每一个资源是唯一的 `If-None-Match`的header会将上次返回的`Etag`发送给服务器,询问该资源的`Etag`是否有更新,有变动就会发送新的资源回来 而强制缓存不需要发送请求到服务端,根据请求头`expires`和`cache-control`判断是否命中强缓存 强制缓存与协商缓存的流程图如下所示: ![](https://static.vue-js.com/a4065b00-bc69-11eb-85f6-6fac77c0c9b3.png) ### 会话状态 `cookie`,类型为「小型文本文件」,指某些网站为了辨别用户身份而储存在用户本地终端上的数据,通过响应头`set-cookie`决定 作为一段一般不超过 4KB 的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制 `Cookie `有效期、安全性、使用范围的可选属性组成 `Cookie` 主要用于以下三个方面: - 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) - 个性化设置(如用户自定义设置、主题等) - 浏览器行为跟踪(如跟踪分析用户行为等 ## 参考文献 - https://zh.wikipedia.org/wiki/HTTP头字段 - https://github.com/amandakelake/blog/issues/41 ================================================ FILE: docs/http/status.md ================================================ # 面试官:说说HTTP 常见的状态码有哪些,适用场景? ![](https://static.vue-js.com/038831d0-bbc9-11eb-ab90-d9ae814b240d.png) ## 一、是什么 HTTP状态码(英语:HTTP Status Code),用以表示网页服务器超文本传输协议响应状态的3位数字代码 它由 RFC 2616规范定义的,并得到 `RFC 2518`、`RFC 2817`、`RFC 2295`、`RFC 2774`与 `RFC 4918`等规范扩展 简单来讲,`http`状态码的作用是服务器告诉客户端当前请求响应的状态,通过状态码就能判断和分析服务器的运行状态 ## 二、分类 状态码第一位数字决定了不同的响应状态,有如下: - 1 表示消息 - 2 表示成功 - 3 表示重定向 - 4 表示请求错误 - 5 表示服务器错误 ### 1xx 代表请求已被接受,需要继续处理。这类响应是临时响应,只包含状态行和某些可选的响应头信息,并以空行结束 常见的有: - 100(客户端继续发送请求,这是临时响应):这个临时响应是用来通知客户端它的部分请求已经被服务器接收,且仍未被拒绝。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。服务器必须在请求完成后向客户端发送一个最终响应 - 101:服务器根据客户端的请求切换协议,主要用于websocket或http2升级 ### 2xx 代表请求已成功被服务器接收、理解、并接受 常见的有: - 200(成功):请求已成功,请求所希望的响应头或数据体将随此响应返回 - 201(已创建):请求成功并且服务器创建了新的资源 - 202(已创建):服务器已经接收请求,但尚未处理 - 203(非授权信息):服务器已成功处理请求,但返回的信息可能来自另一来源 - 204(无内容):服务器成功处理请求,但没有返回任何内容 - 205(重置内容):服务器成功处理请求,但没有返回任何内容 - 206(部分内容):服务器成功处理了部分请求 ### 3xx 表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向 常见的有: - 300(多种选择):针对请求,服务器可执行多种操作。 服务器可根据请求者 (user agent) 选择一项操作,或提供操作列表供请求者选择 - 301(永久移动):请求的网页已永久移动到新位置。 服务器返回此响应(对 GET 或 HEAD 请求的响应)时,会自动将请求者转到新位置 - 302(临时移动): 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求 - 303(查看其他位置):请求者应当对不同的位置使用单独的 GET 请求来检索响应时,服务器返回此代码 - 305 (使用代理): 请求者只能使用代理访问请求的网页。 如果服务器返回此响应,还表示请求者应使用代理 - 307 (临时重定向): 服务器目前从不同位置的网页响应请求,但请求者应继续使用原有位置来进行以后的请求 ### 4xx 代表了客户端看起来可能发生了错误,妨碍了服务器的处理 常见的有: - 400(错误请求): 服务器不理解请求的语法 - 401(未授权): 请求要求身份验证。 对于需要登录的网页,服务器可能返回此响应。 - 403(禁止): 服务器拒绝请求 - 404(未找到): 服务器找不到请求的网页 - 405(方法禁用): 禁用请求中指定的方法 - 406(不接受): 无法使用请求的内容特性响应请求的网页 - 407(需要代理授权): 此状态代码与 401(未授权)类似,但指定请求者应当授权使用代理 - 408(请求超时): 服务器等候请求时发生超时 ### 5xx 表示服务器无法完成明显有效的请求。这类状态码代表了服务器在处理请求的过程中有错误或者异常状态发生 常见的有: - 500(服务器内部错误):服务器遇到错误,无法完成请求 - 501(尚未实施):服务器不具备完成请求的功能。 例如,服务器无法识别请求方法时可能会返回此代码 - 502(错误网关): 服务器作为网关或代理,从上游服务器收到无效响应 - 503(服务不可用): 服务器目前无法使用(由于超载或停机维护) - 504(网关超时): 服务器作为网关或代理,但是没有及时从上游服务器收到请求 - 505(HTTP 版本不受支持): 服务器不支持请求中所用的 HTTP 协议版本 ## 三、适用场景 下面给出一些状态码的适用场景: - 100:客户端在发送POST数据给服务器前,征询服务器情况,看服务器是否处理POST的数据,如果不处理,客户端则不上传POST数据,如果处理,则POST上传数据。常用于POST大数据传输 - 206:一般用来做断点续传,或者是视频文件等大文件的加载 - 301:永久重定向会缓存。新域名替换旧域名,旧的域名不再使用时,用户访问旧域名时用301就重定向到新的域名 - 302:临时重定向不会缓存,常用 于未登陆的用户访问用户中心重定向到登录页面 - 304:协商缓存,告诉客户端有缓存,直接使用缓存中的数据,返回页面的只有头部信息,是没有内容部分 - 400:参数有误,请求无法被服务器识别 - 403:告诉客户端进制访问该站点或者资源,如在外网环境下,然后访问只有内网IP才能访问的时候则返回 - 404:服务器找不到资源时,或者服务器拒绝请求又不想说明理由时 - 503:服务器停机维护时,主动用503响应请求或 nginx 设置限速,超过限速,会返回503 - 504:网关超时 ## 参考文献 - https://zh.wikipedia.org/wiki/HTTP状态码 - https://kebingzao.com/2018/10/05/http-status-code/ - https://vue3js.cn/interview ================================================ FILE: docs/linux/file.md ================================================ # 面试官:说说 linux系统下 文件操作常用的命令有哪些? ![](https://static.vue-js.com/6cb38ac0-03c1-11ec-a752-75723a64e8f5.png) ## 一、是什么 `Linux` 是一个开源的操作系统(OS),是一系列Linux内核基础上开发的操作系统的总称(常见的有Ubuntu、centos) 系统通常会包含以下4个主要部分 - 内核 - shell - 文件系统 - 应用程序 文件系统是一个**目录树的结构**,文件系统结构从一个根目录开始,根目录下可以有任意多个文件和子目录,子目录中又可以有任意多个文件和子目录 ![](https://static.vue-js.com/b71b64c0-03c1-11ec-a752-75723a64e8f5.png) ## 二、文件操作 常见处理目录的命令如下: - ls(英文全拼:list files): 列出目录及文件名 - cd(英文全拼:change directory):切换目录 - pwd(英文全拼:print work directory):显示目前的目录 - mkdir(英文全拼:make directory):创建一个新的目录 - rmdir(英文全拼:remove directory):删除一个空的目录 - cp(英文全拼:copy file): 复制文件或目录 - rm(英文全拼:remove): 删除文件或目录 - mv(英文全拼:move file): 移动文件与目录,或修改文件与目录的名称 ### ls 列出目录文件,选项与参数: - -a :全部的文件,连同隐藏文件( 开头为 . 的文件) 一起列出来(常用) - -d :仅列出目录本身,而不是列出目录内的文件数据(常用) - -l :长数据串列出,包含文件的属性与权限等等数据;(常用) 例如将家目录下的所有文件列出来(含属性与隐藏档) ```cmd [root@www ~]# ls -al ~ ``` ### cd 切换工作目录 语法: ```cmd cd [相对路径或绝对路径] ``` ```cmd # 表示回到自己的家目录,亦即是 /root 这个目录 [root@www runoob]# cd ~ # 表示去到目前的上一级目录,亦即是 /root 的上一级目录的意思; [root@www ~]# cd .. ``` ### pwd `pwd` 是 `Print Working Directory` 的缩写,也就是显示目前所在目录的命令。 ``` [root@www ~]# pwd [-P] ``` 选项与参数: - -P :显示出确实的路径,而非使用连结 (link) 路径 ### mkdir 创建新目录 语法: ``` mkdir [-mp] 目录名称 ``` 选项与参数: - -m :配置文件的权限 - -p :帮助你直接将所需要的目录(包含上一级目录)递归创建起来 ### rmdir (删除空的目录) 语法: ``` rmdir [-p] 目录名称 ``` 选项与参数: - -p :连同上一级『空的』目录也一起删除 ### cp 即拷贝文件和目录 语法: ```cmd cp 目标文件 拷贝文件 ``` 用法如下: ```cmd cp file file_copy --> file 是目标文件,file_copy 是拷贝出来的文件 cp file one --> 把 file 文件拷贝到 one 目录下,并且文件名依然为 file cp file one/file_copy --> 把 file 文件拷贝到 one 目录下,文件名为file_copy cp *.txt folder --> 把当前目录下所有 txt 文件拷贝到 folder 目录下 复制代码 ``` 常用参数如下: - `-r` 递归的拷贝,常用来拷贝一整个目录 ### rm (移除文件或目录) 语法: ``` rm [-fir] 文件或目录 ``` 选项与参数: - -f :就是 force 的意思,忽略不存在的文件,不会出现警告信息; - -i :互动模式,在删除前会询问使用者是否动作 - -r :递归删除啊!最常用在目录的删除了!这是非常危险的选项!! ### mv (移动文件与目录,或修改名称) 语法: ``` [root@www ~]# mv [-fiu] source destination [root@www ~]# mv [options] source1 source2 source3 .... directory ``` 选项与参数: - -f :force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖; - -i :若目标文件 (destination) 已经存在时,就会询问是否覆盖! - -u :若目标文件已经存在,且 source 比较新,才会升级 (update) ### ln `Linux` 文件的存储方式分为3个部分,文件名、文件内容以及权限,其中文件名的列表是存储在硬盘的其它地方和文件内容是分开存放的,每个文件名通过 `inode` 标识绑定到文件内容 `Linux` 下有两种链接类型:硬链接和软链接 #### 硬链接 使链接的两个文件共享同样文件内容,就是同样的 `inode` ,一旦文件1和文件2之间有了硬链接,那么修改任何一个文件,修改的都是同一块内容 语法: ```cmd # 创建 file2 为 file1 的硬链接 ln file1 file2 ``` ![](https://static.vue-js.com/c92e7800-03c1-11ec-8e64-91fdec0f05a1.png) 删除文件1不会影响删除文件2,对于硬链接来说,删除任意一方的文件,共同指向的文件内容并不会从硬盘上删除 只有同时删除了两个文件后后,它们共同指向的文件内容才会消失。 #### 软链接 类似`window`系统的快捷方式 使用方式: ```cmd ln -s file1 file2 ``` ![](https://static.vue-js.com/d5a22eb0-03c1-11ec-8e64-91fdec0f05a1.png)其实 `file2` 只是 `file1` 的一个快捷方式,它指向的是 `file1` ,所以显示的是 `file1` 的内容,但其实 `file2` 的 `inode` 与 `file1` 并不相同 如果 删除了 `file2` 的话, `file1` 是不会受影响的,但如果删除 `file1` 的话, `file2` 就会变成死链接,因为指向的文件不见了 ## 三、文件查看 常见的文件内容查看有如下: - cat 由第一行开始显示文件内容 - less 一页一页的显示文件内容 - head 只看头几行 - tail 只看尾巴几行 ### cat 由第一行开始显示文件内容 语法: ``` cat [-AbEnTv] ``` 常见的选项与参数如下: - -b :列出行号,仅针对非空白行做行号显示,空白行不标行号! - -n :列印出行号,连同空白行也会有行号,与 -b 的选项不同 ### less 一页一页翻动,以下实例输出/etc/man.config文件的内容: ```cmd [root@www ~]# less /etc/man.config # # Generated automatically from man.conf.in by the # configure script. # # man.conf from man-1.6d ....(中间省略).... : <== 这里可以等待你输入命令! ``` less运行时可以输入的命令有: - 空白键 :向下翻动一页; - [pagedown]:向下翻动一页; - [pageup] :向上翻动一页; - /字串 :向下搜寻『字串』的功能; - ?字串 :向上搜寻『字串』的功能; - n :重复前一个搜寻 (与 / 或 ? 有关!) - N :反向的重复前一个搜寻 (与 / 或 ? 有关!) - q :离开 less 这个程序 ### head 取出文件前面几行 语法: ``` head [-n number] 文件 ``` 选项与参数: - -n :后面接数字,代表显示几行的意思 ```cmd [root@www ~]# head /etc/man.config ``` ### tail 取出文件后面几行 语法: ``` tail [-n number] 文件 ``` 选项与参数: - -n :后面接数字,代表显示几行的意思 - -f :表示持续侦测后面所接的档名,要等到按下[ctrl]-c才会结束tail的侦测 ## 参考文献 - https://www.runoob.com/linux/linux-file-content-manage.html - https://juejin.cn/post/6938385978004340744#heading-35 - https://zh.wikipedia.org/wiki/Linux ================================================ FILE: docs/linux/linux users.md ================================================ # 面试官:说说你对 linux 用户管理的理解?相关的命令有哪些? ![](https://static.vue-js.com/8d8d9d70-0417-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 Linux是一个多用户的系统,允许使用者在系统上通过规划不同类型、不同层级的用户,并公平地分配系统资源与工作环境 而与 `Windows` 系统最大的不同, `Linux` 允许不同的用户同时登录主机,同时使用主机的资源 既然是多用户的系统,那么最常见的问题就是权限,不同的用户对于不同的文件都应该有各自的权限 例如,小 A 希望个人文件不被其他用户读取,而如果不对文件进行权限设置,共享了主机资源的小 B 也可以读取小 A 的个人文件,这是不合理的 这里面涉及到用户与用户组的概念 ## 二、用户与用户组 `Linux `以 “用户与用户组” 的概念,建立用户与文件权限之间的联系,保证系统能够充分考虑每个用户的隐私保护,很大程度上保障了 `Linux` 作为多用户系统的可行性 从文件权限的角度出发,“用户与用户组” 引申为三个具体的对象: - **文件所有者** - **用户组成员** - **其他人** 每一个对象对某一个文件的持有权限是不同的 ### 文件所有者 当一个用户创建了一个文件,这个用户就是这个文件的文件所有者。文件所有者对文件拥有最高权限,同时排他性地拥有该文件 除非文件所有者开放权限,否则其他人无法对文件执行查看、修改等操作 ### 用户组 将 “其他用户” 区分为用户组成员和其他人后,若文件所有者希望对部分用户开放权限,而对其他人继续保持私有,则只需要将这部分用户与文件所有者划入一个用户组 这样,这部分用户就成了与文件所有者同组的用户组成员。用户可以对用户组成员开放文件权限,用户组成员则具备了查看、修改文件的权限,而对其他无关用户保持私有 例如,团队成员之间保持文件资源共享,但对非团队成员保持私有,这就需要将文件所有者与团队成员用户划分为同一个用户组,再对用户组成员开放权限即可 ### 其他人 既与文件所有者没有任何联系的其他用户 ### 小结 户和用户组的对应关系是:一对一、多对一、一对多或多对多: - 一对一:某个用户可以是某个组的唯一成员 - 多对一:多个用户可以是某个唯一的组的成员,不归属其它用户组 - 一对多:某个用户可以是多个用户组的成员 - 多对多:多个用户对应多个用户组,并且几个用户可以是归属相同的组 ### 拓展 当我们使用`ls -l`的时候,会列出当前目录的文件信息,如下: ```cmd drwxr-xr-x 3 osmond osmond 4096 05-16 13:32 nobp ``` - d:文件类型 - rwxr-xr-x:文件权限 - 3 硬链接数或目录包含的文件数 - osmond:文件所有者 - 4096:文件长度 - 05-16 13:32:文件上次修改的事件和日期 - nobp:文件名 下面主要看看文件权限分析,实际上是由9个字符组成,每3个一组: - 第一组控制文件**所有者**的访问权限 - 第二组控制所有者**所在用户组**的其他成员的访问权限 - 第三组控制**系统其他用户**的访问权限 `-`代表当前没有,`rwx`对应代表的意思如下: ![](https://static.vue-js.com/9ac2cf60-0417-11ec-8e64-91fdec0f05a1.png) ### 三、用户操作 用户相关的操作有如下: ### 新增用户 `useradd` 可以用来创建新用户,简要语法为: ```text useradd [options] [username] ``` 例如: 添加一个一般用户 ``` # useradd kk //添加用户kk ``` 为添加的用户指定相应的用户组 ``` # useradd -g root kk //添加用户kk,并指定用户所在的组为root用户组 ``` 创建一个系统用户 ``` # useradd -r kk //创建一个系统用户kk ``` 为新添加的用户指定/home目录 ``` # useradd-d /home/myf kk //新添加用户kk,其home目录为/home/myf //当用户名kk登录主机时,系统进入的默认目录为/home/myf ``` ## 设置密码 创建的用户还没有设置登录密码,需要利用`passwd`进行密码设置 ```text asswd [options] [username] ``` `option` 参数有如下: - -d 删除密码 - -f 强迫用户下次登录时必须修改口令 - -w 口令要到期提前警告的天数 - -k 更新只能发送在过期之后 - -l 停止账号使用 - -S 显示密码信息 - -u 启用已被停止的账户 - -x 指定口令最长存活期 - -g 修改群组密码 - 指定口令最短存活期 - -i 口令过期后多少天停用账户 例如,修改用户密码 ``` # passwd runoob //设置runoob用户的密码 Enter new UNIX password: //输入新密码,输入的密码无回显 Retype new UNIX password: //确认密码 passwd: password updated successfully # ``` 显示账号密码信息 ``` # passwd -S runoob runoob P 05/13/2010 0 99999 7 -1 ``` 删除用户密码 ``` # passwd -d lx138 passwd: password expiry information changed. ``` ### 修改用户 `chage` 命令用来修改与用户密码相关的过期信息,如密码失效日、密码最短保留天数、失效前警告天数等 ```text chage [option] [username] ``` 常见的参数有: - -d:指定密码最后修改日期 - -E:密码到期的日期 - -l:列出用户以及密码的有效期 - -m:密码能够更改的最小天数 - -M:密码保持有效的最大天数 ### 删除用户 userdel 命令用来删除用户的相关的所有数据。 ```text userdel [options] [username] ``` 常见的参数有: - -r:删除用户登入目录以及目录中所有文件 例如删除用户账号 ``` # userdel hnlinux ``` 用户组相关的操作如下: ### 新增用户组 `groupadd`用于创建一个新的工作组,新工作组的信息将被添加到系统文件中 ```text groupadd [options] [groupname] ``` 常见的参数有如下: - -g:指定新建工作组的 id; - -r:创建系统工作组,系统工作组的组ID小于 500 - -K:覆盖配置文件 "/ect/login.defs" - -o:允许添加组 ID 号不唯一的工作组 - -f,--force: 如果指定的组已经存在,此选项将失明了仅以成功状态退出 例如创建一个新的组,并添加组 ID。 ``` #groupadd -g 344 runoob ``` ### 修改用户 `groupmod `命令用来修改 `group `相关的参数,例如群组识别码或者名称 ```text groupmod [options] [groupname] ``` 常见的参数有: - -g <群组识别码>  设置欲使用的群组识别码 - -o  重复使用群组识别码 - -n <新群组名称>  设置欲使用的群组名 例如修改组名: ``` # groupmod -n linux linuxso ``` ### 删除用户组 `groupdel` 用于删除用户组,如果该群组中仍包括某些用户,则必须先删除这些用户后,方能删除群组 ```text groupdel [groupname] ``` 日常工作通常会碰到只有` root `用户才有权限执行的操作,这就需要使用用户身份切换的命令: ### su 用于变更为其他使用者的身份,除 `root` 外,需要键入该使用者的密码 ### sudo `sudo`命令以系统管理者的身份执行指令,也就是说,经由 sudo 所执行的指令就好像是 root 亲自执行 不是所有的用户都能执行 `sudo` 命令的,而是在 `/etc/sudoers` 文件内的用户才能执行这个命令 例如`sudo`命令使用`ls`: ``` $ sudo ls ``` ## 参考文献 - https://zhuanlan.zhihu.com/p/37964411 - https://zhuanlan.zhihu.com/p/105482468 ================================================ FILE: docs/linux/linux.md ================================================ # 面试官:说说你对操作系统的理解?核心概念有哪些? ![](https://static.vue-js.com/0f06bf30-008a-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 操作系统(Operating System,缩写:OS)是一组主管并控制计算机操作、运用和运行硬件、软件资源和提供公共服务来组织用户交互的相互关联的系统软件程序,同时也是计算机系统的内核与基石 简单来讲,操作系统就是一种复杂的软件,相当于软件管家 操作系统需要处理如管理与配置内存、决定系统资源供需的优先次序、控制输入与输出设备、操作网络与管理文件系统等基本事务, 操作系统的类型非常多样,不同机器安装的操作系统可从简单到复杂,可从移动电话的嵌入式系统到超级电脑的大型操作系统,在计算机与用户之间起接口的作用,如下图: ![](https://static.vue-js.com/0ad1b850-009b-11ec-8e64-91fdec0f05a1.png) 许多操作系统制造者对它涵盖范畴的定义也不尽一致,例如有些操作系统集成了图形用户界面,而有些仅使用命令行界面,将图形用户界面视为一种非必要的应用程序 ## 二、核心概念 操作系统的核心概念都是对具体物理硬件的抽象,主要有如下: - 进程(线程):进程(线程)是操作系统对CPU的抽象 - 虚拟内存(地址空间):虚拟内存是操作系统对物理内存的抽象 - 文件:文件是操作系统对物理磁盘的抽象 - shell:它是一个程序,可从键盘获取命令并将其提供给操作系统以执行。 - GUI :是一种用户界面,允许用户通过图形图标和音频指示符与电子设备进行交互 - 计算机架构(computer architecture): 在计算机工程中,计算机体系结构是描述计算机系统功能,组织和实现的一组规则和方法。它主要包括指令集、内存管理、I/O 和总线结构 - 多处理系统(Computer multitasking):是指计算机同时运行多个程序的能力 - 程序计数器(Program counter):程序计数器 是一个 CPU 中的寄存器,用于指示计算机在其程序序列中的位置 - 多线程(multithreading):是指从软件或者硬件上实现多个线程并发执行的技术 - CPU 核心(core):它是 CPU 的大脑,它接收指令,并执行计算或运算以满足这些指令。一个 CPU 可以有多个内核 - 图形处理器(Graphics Processing Unit):又称显示核心、视觉处理器、显示芯片或绘图芯片 - 缓存命中(cache hit):当应用程序或软件请求数据时,会首先发生缓存命中 - RAM((Random Access Memory):随机存取存储器,也叫主存,是与 CPU 直接交换数据的内部存储器 - ROM (Read Only Memory):只读存储器是一种半导体存储器,其特性是一旦存储数据就无法改变或删除 - 虚拟地址(virtual memory): 虚拟内存是计算机系统内存管理的一种机制 - 驱动程序(device driver):设备驱动程序,简称驱动程序(driver),是一个允许高级别电脑软件与硬件交互的程序 - USB(Universal Serial Bus):是连接计算机系统与外部设备的一种串口总线标准,也是一种输入输出接口的技术规范 - 地址空间(address space):地址空间是内存中可供程序或进程使用的有效地址范 - 进程间通信(interprocess communication): 指至少两个进程或线程间传送数据或信号的一些技术或方法 - 目录(directory): 在计算机或相关设备中,一个目录或文件夹就是一个装有数字文件系统的虚拟容器 - 路径(path name): 路径是一种电脑文件或目录的名称的通用表现形式,它指向文件系统上的一个唯一位置。 - 根目录(root directory):根目录指的就是计算机系统中的顶层目录,比如 Windows 中的 C 盘和 D 盘,Linux 中的 / - 工作目录(Working directory):它是一个计算机用语。用户在操作系统内所在的目录,用户可在此目录之下,用相对文件名访问文件。 - 文件描述符(file descriptor): 文件描述符是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念 - 客户端(clients):客户端是访问服务器提供的服务的计算机硬件或软件。 - 服务端(servers): 在计算中,服务器是为其他程序或设备提供功能的计算机程序或设备 ## 三、总结 - 操作系统是管理计算机硬件与软件资源的程序,是计算机的基石 - 操作系统本质上是一个运行在计算机上的软件程序 ,用于管理计算机硬件和软件资源 - 操作系统存在屏蔽了硬件层的复杂性。 操作系统就像是硬件使用的负责人,统筹着各种相关事项 - 操作系统的内核(Kernel)是操作系统的核心部分,它负责系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理。 内核是连接应用程序和硬件的桥梁,决定着系统的性能和稳定性 ## 参考文献 - https://www.cnblogs.com/cxuanBlog/p/13297199.html - https://www.cnblogs.com/cxuanblog/p/12607608.html - https://www.anvilliu.com/2021/03/06/%E8%AE%A1%E7%AE%97%E6%9C%BA%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E2%80%94%E2%80%94%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/ ================================================ FILE: docs/linux/redirect_pipe.md ================================================ # 面试官:说说你对输入输出重定向和管道的理解?应用场景? ![](https://static.vue-js.com/1036dde0-0634-11ec-a752-75723a64e8f5.png) ## 一、是什么 `linux`中有三种标准输入输出,分别是`STDIN`,`STDOUT`,`STDERR`,对应的数字是0、1、2: - STDIN 是标准输入,默认从键盘读取信息 - STDOUT 是标准输出,默认将输出结果输出至终端 - STDERR 是标准错误,默认将输出结果输出至终端 对于任何`linux`命令的执行会有下面的过程: ![](https://static.vue-js.com/1a57caf0-0634-11ec-8e64-91fdec0f05a1.png) 一条命令的执行需要键盘等的标准输入,命令的执行和正确或错误,其中的每一个双向箭头就是一个通道,所以数据流可以流入到文件端(**重定向或管道**) 简单来讲,重定向就是把本来要显示在终端的命令结果,输送到别的地方,分成: - 输入重定向:流出到屏幕如果命令所需的输入不是来自键盘,而是来自指定的文件 - 输出重定向:命令的输出可以不显示在屏幕,而是写在指定的文件中 管道就是把两个命令连接起来使用,一个命令的输出作为另一个命令的输入 两者的区别在于: - 管道触发两个子进程,执行 | 两边的程序;而重定向是在一个进程内执行。 - 管道两边都是shell命令 - 重定向符号的右边只能是Linux文件 - 重定向符号的优先级大于管道 ## 二、命令 重定向常见的命令符号有: - \> : 输出重定向到一个文件或设备 覆盖原来的文件 > 如果该文件不存在,则新建一个文件 > > 如果该文件已经存在,会把文件内容覆盖 > > 这些操纵不会征用用户的确认 - \>> :输出重定向到一个文件或设备,但是是 追加原来的文件的末尾 - < :用于制定命令的输入 - << :从键盘的输入重定向为某个命令的输入 > 以逐行输入的模式(回车键进行换行) > > 所有输入的行都将在输入结束字符串之后发送给命令 - 2> 将一个标准错误输出重定向到一个文件或设备,会覆盖原来的文件 - 2>> 将一个标准错误输出重定向到一个文件或设备,是追加到原来的文件 - 2>&1:组合符号,将标准错误输出重定向到标准输出相同的地方 > 1就是代表标准输出 - \>& 将一个标准错误输出重定向到一个文件或设备覆盖原来的文件 - |& 将一个标准错误管道输出到另一个命令作为输入 ## 三、应用场景 将当前目录的文件输出重定向到`1.txt`文件中,并且会清空原有的`1.txt`的内容 ```cmd ls -a > 1.txt ``` 或者以追加的形式,重定向输入到`1.txt`中 ```cmd ls -a >> 1.txt ``` 将标准错误输出到某个文件,可以如下: ```cmd $ touch 2> 2.txt $ cat 2.txt touch: 缺少了文件操作数 请尝试执行 "touch --help" 来获取更多信息。 ``` 通过组合符号将两者结合一起,无论进程输出的信息是正确还是错误的信息,都会重定向到指定的文件里 ```cmd [root@linguanghui home]# abc &> file.txt [root@linguanghui home]# cat file.txt -bash: abc: command not found ``` 再者通过管道查询文件内容是否包含想要的信息: ```cmd cat test.txt | grep -n 'xxx' ``` 上述`cat test.txt`会将`test.txt`的内容作为标准输出,然后利用管道,将其作为`grep -n 'xxx'`命令的标准输入。 ### 参考文献 - https://segmentfault.com/a/1190000020519335 - https://murphypei.github.io/blog/2018/04/linux-redirect-pipe - https://www.huaweicloud.com/articles/0fb70e8c724ae79f4fc8d676cd6160d3.html ================================================ FILE: docs/linux/shell.md ================================================ # 面试官:说说你对 shell 的理解?常见的命令? ![](https://static.vue-js.com/71003620-0883-11ec-a752-75723a64e8f5.png) ## 一、是什么 `Shell `是一个由`c`语言编写的应用程序,它是用户使用 Linux 的桥梁。Shell 既是一种命令语言,又是一种程序设计语言 它连接了用户和` Linux `内核,让用户能够更加高效、安全、低成本地使用 `Linux` 内核 其本身并不是内核的一部分,它只是站在内核的基础上编写的一个应用程序,它和 QQ、微信等其它软件没有什么区别,特殊的地方就是开机立马启动,并呈现在用户面前 主要作用是接收用户输入的命令,并对命令进行处理,处理完毕后再将结果反馈给用户,比如输出到显示器、写入到文件等,同样能够调用和组织其他的应用程序,相当于一个领导者的身份,如下图: ![](https://static.vue-js.com/80db0ca0-0883-11ec-8e64-91fdec0f05a1.png) 那么`shell`脚本就是多个 `Shell` 命令的组合并通过 `if` 条件分支控制或循环来组合运算,实现一些复杂功能,文件后缀名为`.sh` 常用的 `ls` 命令,它本身也是一个 `Shell` 脚本,通过执行这个 `Shell` 脚本可以列举当前目录下的文件列表,如下创建一个`hello.sh`脚本 ```shell #!/bin/bash # 执行的命令主体 ls echo "hello world" ``` - #!/bin/bash :指定脚本要使用的 Shell 类型为 Bash - ls、echo: 脚本文件的内容,表明我们执行 hello.sh 脚本时会列举出当前目录的文件列表并且会向控制台打印 `hello world 执行方式为`.hello.zsh` ## 二、种类 `Linux` 的 `Shell` 种类众多,只要能给用户提供命令行环境的程序,常见的有: - Bourne Shell(sh),是目前所有 Shell 的祖先,被安装在几乎所有发源于 Unix 的操作系统上 - Bourne Again shell(bash) ,是 sh 的一个进阶版本,比 sh 更优秀, bash 是目前大多数 Linux 发行版以及 macOS 操作系统的默认 Shell - C Shell(csh) ,它的语法类似 C 语言 - TENEX C Shell(tcsh) ,它是 csh 的优化版本 - Korn shell(ksh) ,一般在收费的 Unix 版本上比较多见 - Z Shell(zsh) ,它是一种比较新近的 Shell ,集 bash 、 ksh 和 tcsh 各家之大成 ![](https://static.vue-js.com/8e739440-0883-11ec-a752-75723a64e8f5.png) 关于 `Shell` 的几个常见命令: - ls:查看文件 - cd:切换工作目录 - pwd:显示用户当前目录 - mkdir:创建目录 - cp:拷贝 - rm:删除 - mv:移动 - du:显示目录所占用的磁盘空间 ## 三、命令 `Shell` 并不是简单的堆砌命令,我们还可以在 `Shell` 中编程,这和使用 `C++`、`C#`、`Java`、`Python` 等常见的编程语言并没有什么两样。 Shell 虽然没有 C++、Java、Python 等强大,但也支持了基本的编程元素,例如: - if...else 选择结构,case...in 开关语句,for、while、until 循环; - 变量、数组、字符串、注释、加减乘除、逻辑运算等概念; - 函数,包括用户自定义的函数和内置函数(例如 printf、export、eval 等) 下面以`bash`为例简单了解一下`shell`的基本使用 ### 变量 `Bash` 没有数据类型的概念,所有的变量值都是字符串,可以保存一个数字、一个字符、一个字符串等等 同时无需提前声明变量,给变量赋值会直接创建变量 访问变量的语法形式为:`${var}` 和 `$var` 。 变量名外面的花括号是可选的,加不加都行,加花括号是为了帮助解释器识别变量的边界,所以推荐加花括号。 ```bash word="hello" echo ${word} # Output: hello ``` ### 条件控制 跟其它程序设计语言一样,Bash 中的条件语句让我们可以决定一个操作是否被执行。结果取决于一个包在`[[ ]]`里的表达式 跟其他语言一样,使用`if...else`进行表达,如果中括号里的表达式为真,那么`then`和`fi`之间的代码会被执行,如果则`else`和`fi`之间的代码会被执行 ```shell if [[ 2 -ne 1 ]]; then echo "true" else echo "false" fi # Output: true ``` `fi`标志着条件代码块的结束 ### 函数 bash 函数定义语法如下: ```bash [ function ] funname [()] { action; [return int;] } ``` - 函数定义时,function 关键字可有可无 - 函数返回值 - return 返回函数返回值,返回值类型只能为整数(0-255)。如果不加 return 语句,shell 默认将以最后一条命令的运行结果,作为函数返回值 - 函数返回值在调用该函数后通过 $? 来获得 - 所有函数在使用前必须定义。这意味着必须将函数放在脚本开始部分,直至 shell 解释器首次发现它时,才可以使用。调用函数仅使用其函数名即可 ## 参考文献 - http://c.biancheng.net/view/706.html - https://juejin.cn/post/6930013333454061575 ================================================ FILE: docs/linux/thread_process.md ================================================ # 面试官:说说什么是进程?什么是线程?区别? ![](https://static.vue-js.com/f414d8a0-02f6-11ec-a752-75723a64e8f5.png) ## 一、进程 操作系统中最核心的概念就是进程,进程是对正在运行中的程序的一个抽象,是系统进行资源分配和调度的基本单位 操作系统的其他所有内容都是围绕着进程展开的,负责执行这些任务的是`CPU` ![](https://static.vue-js.com/3ff146b0-02f6-11ec-8e64-91fdec0f05a1.png) 进程是一种抽象的概念,从来没有统一的标准定义看,一般由程序、数据集合和进程控制块三部分组成: - 程序用于描述进程要完成的功能,是控制进程执行的指令集 - 数据集合是程序在执行时所需要的数据和工作区 - 程序控制块,包含进程的描述信息和控制信息,是进程存在的唯一标志 ## 二、线程 **线程**(thread)是操作系统能够进行**运算调度**的最小单位,其是进程中的一个执行任务(控制单元),负责当前进程中程序的执行 一个进程至少有一个线程,一个进程可以运行多个线程,这些线程共享同一块内存,线程之间可以共享对象、资源,如果有冲突或需要协同,还可以随时沟通以解决冲突或保持同步 举个例子,假设你经营着一家物业管理公司。最初,业务量很小,事事都需要你亲力亲为。给老张家修完暖气管道,立马再去老李家换电灯泡——这叫单线程,所有的工作都得顺序执行 后来业务拓展了,你雇佣了几个工人,这样,你的物业公司就可以同时为多户人家提供服务了——这叫多线程,你是主线程 ![](https://static.vue-js.com/63de34c0-02f6-11ec-a752-75723a64e8f5.png) 但实际上,并不是线程越多,进程的工作效率越高,这是因为在一个进程内,不管你创建了多少线程,它们总是被限定在一颗`CPU`内,或者多核`CPU`的一个核内 这意味着,多线程在宏观上是并行的,在微观上则是分时切换串行的,多线程编程无法充分发挥多核计算资源的优势 这导致使用多线程做任务并行处理时,线程数量超过一定数值后,线程越多速度反倒越慢的原因 ## 三、区别 - **本质区别**:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位 - **在开销方面**:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小 - **所处环境**:在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行) - **内存分配方面**:系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源 - **包含关系**:没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程 举个例子:进程=火车,线程=车厢 - 线程在进程下行进(单纯的车厢无法运行) - 一个进程可以包含多个线程(一辆火车可以有多个车厢) - 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘) - 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易) - 进程要比线程消耗更多的计算机资源(采用多列火车相比多个车厢更耗资源) - 进程间不会相互影响,一个线程挂掉将导致整个进程挂掉(一列火车不会影响到另外一列火车,但是如果一列火车上中间的一节车厢着火了,将影响到所有车厢) ## 参考文献 - https://zhuanlan.zhihu.com/p/106283969 - https://blog.csdn.net/ThinkWAon/article/details/102021274 - https://www.zhihu.com/question/25532384 ================================================ FILE: docs/linux/vim.md ================================================ # 面试官:说说 linux 系统下 文本编辑常用的命令有哪些? ![](https://static.vue-js.com/1062b8b0-049b-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 `Vim`是从 `vi` 发展出来的一个文本编辑器,代码补全、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用。 简单的来说, `vi` 是老式的字处理器,不过功能已经很齐全了,但是还是有可以进步的地方 而`vim `可以说是程序开发者的一项很好用的工具 ## 二、使用 基本上 vi/vim 共分为三种模式,分别是: - 命令模式(Command mode) - 输入模式(Insert mode) - 底线命令模式(Last line mode) ![](https://static.vue-js.com/265a0080-03d6-11ec-a752-75723a64e8f5.png) ### 命令模式 `Vim` 的默认模式,在这个模式下,你不能输入文本,但是可以让我们在文本间移动,删除一行文本,复制黏贴文本,跳转到指定行,撤销操作,等等 #### 移动光标 常用的命令如下: - h 向左移动一个字符 - j 向下移动一个字符 - k 向上移动一个字符 - i 向右移动一个字符 或者使用方向键进行控制 如果想要向下移动`n`行,可通过使用 "nj" 或 "n↓" 的组合按键 #### 搜索 常见的命令如下: - /word:向光标之下寻找一个名称为 word 的字符 - ?word:向光标之上寻找一个字符串名称为 word 的字符串 - n:代表重复前一个搜寻的动作,即再次执行上一次的操作 - N:反向进行前一个搜索动作 #### 删除、复制、粘贴 常用的命令如下: - x:向后删除一个字符 - X:向前删除一个字符 - nc:n 为数字,连续向后删除 n 个字符 - dd:删除游标所在的那一整行 - d0:删除游标所在处,到该行的最前面一个字符 - d$删除游标所在处,到该行的最后一个字符 - ndd:除光标所在的向下 n 行 - yy:复制游标所在的那一行 - y0:复制光标所在的那个字符到该行行首的所有数据 - y$:复制光标所在的那个字符到该行行尾的所有数据 - p:已复制的数据在光标下一行贴上 - P:已复制的数据在光标上一行贴上 - nc:重复删除n行数据 ### 输入模式 命令模式通过输入大小写`i`、`a`、`o`可以切换到输入模式,如下: - i:从目前光标所在处输入 - I:在目前所在行的第一个非空格符处开始输入 - a:从目前光标所在的下一个字符处开始输入 - A:从光标所在行的最后一个字符处开始输入 - o:在目前光标所在的下一行处输入新的一行 - O:目前光标所在的上一行处输入新的一行 输入模式我们熟悉的文本编辑器的模式,就是可以输入任何你想输入的内容 如果想从插入模式回到命令模式,使用按下键盘左上角的`ESC`键 ### 底线命令模式 这个模式下可以运行一些命令例如“退出”,“保存”,等动作,为了进入底线命令模式,首先要进入命令模式,再按下冒号键: 常见的命令如下: - w:将编辑的数据写入硬盘档案中 - w!:若文件属性为『只读』时,强制写入该档案 - q:未修改,直接退出 - q!:修改过但不存储 - wq:储存后离开 ## 参考文献 - https://www.runoob.com/linux/linux-vim.html ================================================ FILE: docs/typescript/class.md ================================================ # 面试官:说说你对 TypeScript 中类的理解?应用场景? ![](https://static.vue-js.com/e4c19060-0cb4-11ec-a752-75723a64e8f5.png) ## 一、是什么 类(Class)是面向对象程序设计(OOP,Object-Oriented Programming)实现信息封装的基础 > 类是一种用户定义的引用数据类型,也称类类型 传统的面向对象语言基本都是基于类的,`JavaScript` 基于原型的方式让开发者多了很多理解成本 在 `ES6` 之后,`JavaScript` 拥有了 `class` 关键字,虽然本质依然是构造函数,但是使用起来已经方便了许多 但是` JavaScript` 的` class `依然有一些特性还没有加入,比如修饰符和抽象类 `TypeScript` 的 `class` 支持面向对象的所有特性,比如 类、接口等 ## 二、使用方式 定义类的关键字为 `class`,后面紧跟类名,类可以包含以下几个模块(类的数据成员): - **字段** : 字段是类里面声明的变量。字段表示对象的有关数据。 - **构造函数**: 类实例化时调用,可以为类的对象分配内存。 - **方法**: 方法为对象要执行的操作 如下例子: ```ts class Car { // 字段 engine:string; // 构造函数 constructor(engine:string) { this.engine = engine } // 方法 disp():void { console.log("发动机为 : "+this.engine) } } ``` ### 继承 类的继承使用过`extends`的关键字 ```ts class Animal { move(distanceInMeters: number = 0) { console.log(`Animal moved ${distanceInMeters}m.`); } } class Dog extends Animal { bark() { console.log('Woof! Woof!'); } } const dog = new Dog(); dog.bark(); dog.move(10); dog.bark(); ``` `Dog`是一个 派生类,它派生自 `Animal` 基类,派生类通常被称作子类,基类通常被称作 超类 `Dog`类继承了`Animal`类,因此实例`dog`也能够使用`Animal`类`move`方法 同样,类继承后,子类可以对父类的方法重新定义,这个过程称之为方法的重写,通过`super`关键字是对父类的直接引用,该关键字可以引用父类的属性和方法,如下: ```ts class PrinterClass { doPrint():void { console.log("父类的 doPrint() 方法。") } } class StringPrinter extends PrinterClass { doPrint():void { super.doPrint() // 调用父类的函数 console.log("子类的 doPrint()方法。") } } ``` ### 修饰符 可以看到,上述的形式跟`ES6`十分的相似,`typescript`在此基础上添加了三种修饰符: - 公共 public:可以自由的访问类程序里定义的成员 - 私有 private:只能够在该类的内部进行访问 - 受保护 protect:除了在该类的内部可以访问,还可以在子类中仍然可以访问 ### 私有修饰符 只能够在该类的内部进行访问,实例对象并不能够访问 ![](https://static.vue-js.com/f57365f0-0cb4-11ec-a752-75723a64e8f5.png) 并且继承该类的子类并不能访问,如下图所示: ![](https://static.vue-js.com/0072cc20-0cb5-11ec-8e64-91fdec0f05a1.png) ### 受保护修饰符 跟私有修饰符很相似,实例对象同样不能访问受保护的属性,如下: ![](https://static.vue-js.com/09e72580-0cb5-11ec-a752-75723a64e8f5.png) 有一点不同的是 `protected` 成员在子类中仍然可以访问 ![](https://static.vue-js.com/137f81a0-0cb5-11ec-8e64-91fdec0f05a1.png) 除了上述修饰符之外,还有只读**修饰符** #### 只读修饰符 通过`readonly`关键字进行声明,只读属性必须在声明时或构造函数里被初始化,如下: ![](https://static.vue-js.com/1e848d20-0cb5-11ec-8e64-91fdec0f05a1.png) 除了实例属性之外,同样存在静态属性 ### 静态属性 这些属性存在于类本身上面而不是类的实例上,通过`static`进行定义,访问这些属性需要通过 类型.静态属性 的这种形式访问,如下所示: ```ts class Square { static width = '100px' } console.log(Square.width) // 100px ``` 上述的类都能发现一个特点就是,都能够被实例化,在 `typescript`中,还存在一种抽象类 ### 抽象类 抽象类做为其它派生类的基类使用,它们一般不会直接被实例化,不同于接口,抽象类可以包含成员的实现细节 `abstract `关键字是用于定义抽象类和在抽象类内部定义抽象方法,如下所示: ```ts abstract class Animal { abstract makeSound(): void; move(): void { console.log('roaming the earch...'); } } ``` 这种类并不能被实例化,通常需要我们创建子类去继承,如下: ```ts class Cat extends Animal { makeSound() { console.log('miao miao') } } const cat = new Cat() cat.makeSound() // miao miao cat.move() // roaming the earch... ``` ## 三、应用场景 除了日常借助类的特性完成日常业务代码,还可以将类(class)也可以作为接口,尤其在 `React` 工程中是很常用的,如下: ```ts export default class Carousel extends React.Component {} ``` 由于组件需要传入 `props` 的类型 `Props` ,同时有需要设置默认 `props` 即 `defaultProps`,这时候更加适合使用`class`作为接口 先声明一个类,这个类包含组件 `props` 所需的类型和初始值: ```ts // props的类型 export default class Props { public children: Array> | React.ReactElement | never[] = [] public speed: number = 500 public height: number = 160 public animation: string = 'easeInOutQuad' public isAuto: boolean = true public autoPlayInterval: number = 4500 public afterChange: () => {} public beforeChange: () => {} public selesctedColor: string public showDots: boolean = true } ``` 当我们需要传入 `props` 类型的时候直接将 `Props` 作为接口传入,此时 `Props` 的作用就是接口,而当需要我们设置`defaultProps`初始值的时候,我们只需要: ```ts public static defaultProps = new Props() ``` `Props` 的实例就是 `defaultProps` 的初始值,这就是 `class `作为接口的实际应用,我们用一个 `class` 起到了接口和设置初始值两个作用,方便统一管理,减少了代码量 ## 参考文献 - https://www.tslang.cn/docs/handbook/classes.html - https://www.runoob.com/typescript/ts-class.html ================================================ FILE: docs/typescript/data_type.md ================================================ # 面试官:说说 typescript 的数据类型有哪些? ![](https://static.vue-js.com/d88f9450-0998-11ec-a752-75723a64e8f5.png) ## 一、是什么 `typescript` 和 `javascript`几乎一样,拥有相同的数据类型,另外在`javascript`基础上提供了更加实用的类型供开发使用 在开发阶段,可以为明确的变量定义为某种类型,这样`typescript`就能在编译阶段进行类型检查,当类型不合符预期结果的时候则会出现错误提示 ## 二、有哪些 `typescript` 的数据类型主要有如下: - boolean(布尔类型) - number(数字类型) - string(字符串类型) - array(数组类型) - tuple(元组类型) - enum(枚举类型) - any(任意类型) - null 和 undefined 类型 - void 类型 - never 类型 - object 对象类型 ### boolean 布尔类型 ```tsx let flag:boolean = true; // flag = 123; // 错误 flag = false; //正确 ``` ### number 数字类型,和`javascript`一样,`typescript`的数值类型都是浮点数,可支持二进制、八进制、十进制和十六进制 ```tsx let num:number = 123; // num = '456'; // 错误 num = 456; //正确 ``` 进制表示: ```tsx let decLiteral: number = 6; // 十进制 let hexLiteral: number = 0xf00d; // 十六进制 let binaryLiteral: number = 0b1010; // 二进制 let octalLiteral: number = 0o744; // 八进制 ``` ### string 字符串类型,和`JavaScript`一样,可以使用双引号(`"`)或单引号(`'`)表示字符串 ```tsx let str:string = 'this is ts'; str = 'test'; ``` 作为超集,当然也可以使用模版字符串``进行包裹,通过 ${} 嵌入变量 ```tsx let name: string = `Gene`; let age: number = 37; let sentence: string = `Hello, my name is ${ name } ``` ### array 数组类型,跟`javascript`一致,通过`[]`进行包裹,有两种写法: 方式一:元素类型后面接上 `[]` ```tsx let arr:string[] = ['12', '23']; arr = ['45', '56']; ``` 方式二:使用数组泛型,`Array<元素类型>`: ```tsx let arr:Array = [1, 2]; arr = ['45', '56']; ``` ### tuple 元祖类型,允许表示一个已知元素数量和类型的数组,各元素的类型不必相同 ```tsx let tupleArr:[number, string, boolean]; tupleArr = [12, '34', true]; //ok typleArr = [12, '34'] // no ok ``` 赋值的类型、位置、个数需要和定义(生明)的类型、位置、个数一致 ### enum `enum`类型是对JavaScript标准数据类型的一个补充,使用枚举类型可以为一组数值赋予友好的名字 ```tsx enum Color {Red, Green, Blue} let c: Color = Color.Green; ``` ### any 可以指定任何类型的值,在编程阶段还不清楚类型的变量指定一个类型,不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查,这时候可以使用`any`类型 使用`any`类型允许被赋值为任意类型,甚至可以调用其属性、方法 ```tsx let num:any = 123; num = 'str'; num = true; ``` 定义存储各种类型数据的数组时,示例代码如下: ```tsx let arrayList: any[] = [1, false, 'fine']; arrayList[1] = 100; ``` ### null 和 和 undefined 在` JavaScript` 中 `null `表示 "什么都没有",是一个只有一个值的特殊类型,表示一个空对象引用,而`undefined`表示一个没有设置值的变量 默认情况下`null`和`undefined`是所有类型的子类型, 就是说你可以把 `null `和 `undefined `赋值给 `number `类型的变量 ```tsx let num:number | undefined; // 数值类型 或者 undefined console.log(num); // 正确 num = 123; console.log(num); // 正确 ``` 但是`ts`配置了`--strictNullChecks`标记,`null`和`undefined`只能赋值给`void`和它们各自 ### void 用于标识方法返回值的类型,表示该方法没有返回值。 ```tsx function hello(): void { alert("Hello Runoob"); } ``` ### never `never`是其他类型 (包括` null `和 `undefined`)的子类型,可以赋值给任何类型,代表从不会出现的值 但是没有类型是 never 的子类型,这意味着声明 `never` 的变量只能被 `never` 类型所赋值。 `never` 类型一般用来指定那些总是会抛出异常、无限循环 ```tsx let a:never; a = 123; // 错误的写法 a = (() => { // 正确的写法 throw new Error('错误'); })() // 返回never的函数必须存在无法达到的终点 function error(message: string): never { throw new Error(message); } ``` ### object 对象类型,非原始类型,常见的形式通过`{}`进行包裹 ```tsx let obj:object; obj = {name: 'Wang', age: 25}; ``` ## 三、总结 和`javascript`基本一致,也分成: - 基本类型 - 引用类型 在基础类型上,`typescript`增添了`void`、`any`、`emum`等原始类型 ## 参考文献 - https://www.tslang.cn/docs/handbook/basic-types.html ================================================ FILE: docs/typescript/decorator.md ================================================ # 面试官:说说你对 TypeScript 装饰器的理解?应用场景? ![](https://static.vue-js.com/f8905dd0-111c-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上 是一种在不改变原类和使用继承的情况下,动态地扩展对象功能 同样的,本质也不是什么高大上的结构,就是一个普通的函数,`@expression` 的形式其实是`Object.defineProperty`的语法糖 `expression `求值后必须也是一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入 ## 二、使用方式 由于`typescript`是一个实验性特性,若要使用,需要在`tsconfig.json`文件启动,如下: ```ts { "compilerOptions": { "target": "ES5", "experimentalDecorators": true } } ``` `typescript`装饰器的使用和`javascript`基本一致 类的装饰器可以装饰: - 类 - 方法/属性 - 参数 - 访问器 ### 类装饰 例如声明一个函数 `addAge` 去给 Class 的属性 `age` 添加年龄. ```ts function addAge(constructor: Function) { constructor.prototype.age = 18; } @addAge class Person{ name: string; age!: number; constructor() { this.name = 'huihui'; } } let person = new Person(); console.log(person.age); // 18 ``` 上述代码,实际等同于以下形式: ```ts Person = addAge(function Person() { ... }); ``` 上述可以看到,当装饰器作为修饰类的时候,会把构造器传递进去。 `constructor.prototype.age` 就是在每一个实例化对象上面添加一个 `age` 属性 ### 方法/属性装饰 同样,装饰器可以用于修饰类的方法,这时候装饰器函数接收的参数变成了: - target:对象的原型 - propertyKey:方法的名称 - descriptor:方法的属性描述符 可以看到,这三个属性实际就是`Object.defineProperty`的三个参数,如果是类的属性,则没有传递第三个参数 如下例子: ```ts // 声明装饰器修饰方法/属性 function method(target: any, propertyKey: string, descriptor: PropertyDescriptor) { console.log(target); console.log("prop " + propertyKey); console.log("desc " + JSON.stringify(descriptor) + "\n\n"); descriptor.writable = false; }; function property(target: any, propertyKey: string) { console.log("target", target) console.log("propertyKey", propertyKey) } class Person{ @property name: string; constructor() { this.name = 'huihui'; } @method say(){ return 'instance method'; } @method static run(){ return 'static method'; } } const xmz = new Person(); // 修改实例方法say xmz.say = function() { return 'edit' } ``` 输出如下图所示: ![](https://static.vue-js.com/e96bc1b0-114d-11ec-8e64-91fdec0f05a1.png) ### 参数装饰 接收3个参数,分别是: - target :当前对象的原型 - propertyKey :参数的名称 - index:参数数组中的位置 ```ts function logParameter(target: Object, propertyName: string, index: number) { console.log(target); console.log(propertyName); console.log(index); } class Employee { greet(@logParameter message: string): string { return `hello ${message}`; } } const emp = new Employee(); emp.greet('hello'); ``` 输入如下图: ![](https://static.vue-js.com/f2f32de0-114d-11ec-a752-75723a64e8f5.png) ### 访问器装饰 使用起来方式与方法装饰一致,如下: ```ts function modification(target: Object, propertyKey: string, descriptor: PropertyDescriptor) { console.log(target); console.log("prop " + propertyKey); console.log("desc " + JSON.stringify(descriptor) + "\n\n"); }; class Person{ _name: string; constructor() { this._name = 'huihui'; } @modification get name() { return this._name } } ``` ### 装饰器工厂 如果想要传递参数,使装饰器变成类似工厂函数,只需要在装饰器函数内部再函数一个函数即可,如下: ```ts function addAge(age: number) { return function(constructor: Function) { constructor.prototype.age = age } } @addAge(10) class Person{ name: string; age!: number; constructor() { this.name = 'huihui'; } } let person = new Person(); ``` ### 执行顺序 当多个装饰器应用于一个声明上,将由上至下依次对装饰器表达式求值,求值的结果会被当作函数,由下至上依次调用,例如如下: ```ts function f() { console.log("f(): evaluated"); return function (target, propertyKey: string, descriptor: PropertyDescriptor) { console.log("f(): called"); } } function g() { console.log("g(): evaluated"); return function (target, propertyKey: string, descriptor: PropertyDescriptor) { console.log("g(): called"); } } class C { @f() @g() method() {} } // 输出 f(): evaluated g(): evaluated g(): called f(): called ``` ## 三、应用场景 可以看到,使用装饰器存在两个显著的优点: - 代码可读性变强了,装饰器命名相当于一个注释 - 在不改变原有代码情况下,对原来功能进行扩展 后面的使用场景中,借助装饰器的特性,除了提高可读性之后,针对已经存在的类,可以通过装饰器的特性,在不改变原有代码情况下,对原来功能进行扩展 ## 参考文献 - https://www.tslang.cn/docs/handbook/decorators.html - https://juejin.cn/post/6844903876605280269#heading-5 ================================================ FILE: docs/typescript/enum.md ================================================ # 面试官:说说你对 TypeScript 中枚举类型的理解?应用场景? ![](https://static.vue-js.com/76173bf0-0b0c-11ec-a752-75723a64e8f5.png) ## 一、是什么 枚举是一个被命名的整型常数的集合,用于声明一组命名的常数,当一个变量有几种可能的取值时,可以将它定义为枚举类型 通俗来说,枚举就是一个对象的所有可能取值的集合 在日常生活中也很常见,例如表示星期的SUNDAY、MONDAY、TUESDAY、WEDNESDAY、THURSDAY、FRIDAY、SATURDAY就可以看成是一个枚举 枚举的说明与结构和联合相似,其形式为: ```txt enum 枚举名{ 标识符①[=整型常数], 标识符②[=整型常数], ... 标识符N[=整型常数], }枚举变量; ``` ## 二、使用 枚举的使用是通过`enum`关键字进行定义,形式如下: ```ts enum xxx { ... } ``` 声明关键字为枚举类型的方式如下: ```ts // 声明d为枚举类型Direction let d: Direction; ``` 类型可以分成: - 数字枚举 - 字符串枚举 - 异构枚举 ### 数字枚举 当我们声明一个枚举类型是,虽然没有给它们赋值,但是它们的值其实是默认的数字类型,而且默认从0开始依次累加: ```ts enum Direction { Up, // 值默认为 0 Down, // 值默认为 1 Left, // 值默认为 2 Right // 值默认为 3 } console.log(Direction.Up === 0); // true console.log(Direction.Down === 1); // true console.log(Direction.Left === 2); // true console.log(Direction.Right === 3); // true ``` 如果我们将第一个值进行赋值后,后面的值也会根据前一个值进行累加1: ```ts enum Direction { Up = 10, Down, Left, Right } console.log(Direction.Up, Direction.Down, Direction.Left, Direction.Right); // 10 11 12 13 ``` ### 字符串枚举 ```ts 枚举类型的值其实也可以是字符串类型: enum Direction { Up = 'Up', Down = 'Down', Left = 'Left', Right = 'Right' } console.log(Direction['Right'], Direction.Up); // Right Up ``` 如果设定了一个变量为字符串之后,后续的字段也需要赋值字符串,否则报错: ```ts enum Direction { Up = 'UP', Down, // error TS1061: Enum member must have initializer Left, // error TS1061: Enum member must have initializer Right // error TS1061: Enum member must have initializer } ``` ### 异构枚举 即将数字枚举和字符串枚举结合起来混合起来使用,如下: ```ts enum BooleanLikeHeterogeneousEnum { No = 0, Yes = "YES", } ``` 通常情况下我们很少会使用异构枚举 ### 本质 现在一个枚举的案例如下: ```ts enum Direction { Up, Down, Left, Right } ``` 通过编译后,`javascript`如下: ```ts var Direction; (function (Direction) { Direction[Direction["Up"] = 0] = "Up"; Direction[Direction["Down"] = 1] = "Down"; Direction[Direction["Left"] = 2] = "Left"; Direction[Direction["Right"] = 3] = "Right"; })(Direction || (Direction = {})); ``` 上述代码可以看到, `Direction[Direction["Up"] = 0] = "Up"`可以分成 - Direction["Up"] = 0 - Direction[0] = "Up" 所以定义枚举类型后,可以通过正反映射拿到对应的值,如下: ```ts enum Direction { Up, Down, Left, Right } console.log(Direction.Up === 0); // true console.log(Direction[0]); // Up ``` 并且多处定义的枚举是可以进行合并操作,如下: ```ts enum Direction { Up = 'Up', Down = 'Down', Left = 'Left', Right = 'Right' } enum Direction { Center = 1 } ``` 编译后,`js`代码如下: ```js var Direction; (function (Direction) { Direction["Up"] = "Up"; Direction["Down"] = "Down"; Direction["Left"] = "Left"; Direction["Right"] = "Right"; })(Direction || (Direction = {})); (function (Direction) { Direction[Direction["Center"] = 1] = "Center"; })(Direction || (Direction = {})); ``` 可以看到,`Direction`对象属性回叠加 ## 三、应用场景 就拿回生活的例子,后端返回的字段使用 0 - 6 标记对应的日期,这时候就可以使用枚举可提高代码可读性,如下: ```ts enum Days {Sun, Mon, Tue, Wed, Thu, Fri, Sat}; console.log(Days["Sun"] === 0); // true console.log(Days["Mon"] === 1); // true console.log(Days["Tue"] === 2); // true console.log(Days["Sat"] === 6); // true ``` 包括后端日常返回0、1 等等状态的时候,我们都可以通过枚举去定义,这样可以提高代码的可读性,便于后续的维护 ## 参考文献 - https://zh.wikipedia.org/wiki/%E6%9E%9A%E4%B8%BE - https://www.jianshu.com/p/b9e1caa4dd98 - https://juejin.cn/post/6844904112669065224#heading-30 ================================================ FILE: docs/typescript/function.md ================================================ # 面试官:说说你对 TypeScript 中函数的理解?与 JavaScript 函数的区别? ![](https://static.vue-js.com/3f1c1390-0d42-11ec-a752-75723a64e8f5.png) ## 一、是什么 函数是` JavaScript` 应用程序的基础,帮助我们实现抽象层、模拟类、信息隐藏和模块 在` TypeScript` 里,虽然已经支持类、命名空间和模块,但函数仍然是主要定义行为的方式,`TypeScript` 为 `JavaScript` 函数添加了额外的功能,丰富了更多的应用场景 函数类型在 `TypeScript` 类型系统中扮演着非常重要的角色,它们是可组合系统的核心构建块 ## 二、使用方式 跟`javascript` 定义函数十分相似,可以通过`funciton` 关键字、箭头函数等形式去定义,例如下面一个简单的加法函数: ```ts const add = (a: number, b: number) => a + b ``` 上述只定义了函数的两个参数类型,这个时候整个函数虽然没有被显式定义,但是实际上` TypeScript` 编译器是能够通过类型推断到这个函数的类型,如下图所示: ![](https://static.vue-js.com/4b3415b0-0d42-11ec-8e64-91fdec0f05a1.png) 当鼠标放置在第三行`add`函数名的时候,会出现完整的函数定义类型,通过`:` 的形式来定于参数类型,通过 `=>` 连接参数和返回值类型 当我们没有提供函数实现的情况下,有两种声明函数类型的方式,如下所示: ```ts // 方式一 type LongHand = { (a: number): number; }; // 方式二 type ShortHand = (a: number) => number; ``` 当存在函数重载时,只能使用方式一的形式 ### 可选参数 当函数的参数可能是不存在的,只需要在参数后面加上 `?` 代表参数可能不存在,如下: ```ts const add = (a: number, b?: number) => a + (b ? b : 0) ``` 这时候参数`b`可以是`number`类型或者`undefined`类型,即可以传一个`number`类型或者不传都可以 ### 剩余类型 剩余参数与`JavaScript`的语法类似,需要用 `...` 来表示剩余参数 如果剩余参数 `rest` 是一个由`number`类型组成的数组,则如下表示: ```ts const add = (a: number, ...rest: number[]) => rest.reduce(((a, b) => a + b), a) ``` ### 函数重载 允许创建数项名称相同但输入输出类型或个数不同的子程序,它可以简单地称为一个单独功能可以执行多项任务的能力 关于`typescript`函数重载,必须要把精确的定义放在前面,最后函数实现时,需要使用 `|`操作符或者`?`操作符,把所有可能的输入类型全部包含进去,用于具体实现 这里的函数重载也只是多个函数的声明,具体的逻辑还需要自己去写,`typescript`并不会真的将你的多个重名 `function `的函数体进行合并 例如我们有一个add函数,它可以接收 `string`类型的参数进行拼接,也可以接收 `number` 类型的参数进行相加,如下: ```ts // 上边是声明 function add (arg1: string, arg2: string): string function add (arg1: number, arg2: number): number // 因为我们在下边有具体函数的实现,所以这里并不需要添加 declare 关键字 // 下边是实现 function add (arg1: string | number, arg2: string | number) { // 在实现上我们要注意严格判断两个参数的类型是否相等,而不能简单的写一个 arg1 + arg2 if (typeof arg1 === 'string' && typeof arg2 === 'string') { return arg1 + arg2 } else if (typeof arg1 === 'number' && typeof arg2 === 'number') { return arg1 + arg2 } } ``` ## 三、区别 从上面可以看到: - 从定义的方式而言,typescript 声明函数需要定义参数类型或者声明返回值类型 - typescript 在参数中,添加可选参数供使用者选择 - typescript 增添函数重载功能,使用者只需要通过查看函数声明的方式,即可知道函数传递的参数个数以及类型 ## 参考文献 - https://www.tslang.cn/docs/handbook/functions.html - https://zh.wikipedia.org/wiki/%E5%87%BD%E6%95%B0%E9%87%8D%E8%BD%BD - https://jkchao.github.io/typescript-book-chinese/typings/functions.html#%E9%87%8D%E8%BD%BD ================================================ FILE: docs/typescript/generic.md ================================================ # 面试官:说说你对 TypeScript 中泛型的理解?应用场景? ![](https://static.vue-js.com/5bb5f1d0-0e17-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 泛型程序设计(generic programming)是程序设计语言的一种风格或范式 泛型允许我们在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型 在`typescript`中,定义函数,接口或者类的时候,不预先定义好具体的类型,而在使用的时候在指定类型的一种特性 假设我们用一个函数,它可接受一个 `number` 参数并返回一个` number` 参数,如下写法: ```ts function returnItem (para: number): number { return para } ``` 如果我们打算接受一个 `string` 类型,然后再返回 `string`类型,则如下写法: ```ts function returnItem (para: string): string { return para } ``` 上述两种编写方式,存在一个最明显的问题在于,代码重复度比较高 虽然可以使用 `any`类型去替代,但这也并不是很好的方案,因为我们的目的是接收什么类型的参数返回什么类型的参数,即在运行时传入参数我们才能确定类型 这种情况就可以使用泛型,如下所示: ```ts function returnItem(para: T): T { return para } ``` 可以看到,泛型给予开发者创造灵活、可重用代码的能力 ## 二、使用方式 泛型通过`<>`的形式进行表述,可以声明: - 函数 - 接口 - 类 ### 函数声明 声明函数的形式如下: ```ts function returnItem(para: T): T { return para } ``` 定义泛型的时候,可以一次定义**多个类型参数**,比如我们可以同时定义泛型 `T` 和 泛型 `U`: ```ts function swap(tuple: [T, U]): [U, T] { return [tuple[1], tuple[0]]; } swap([7, 'seven']); // ['seven', 7] ``` ### 接口声明 声明接口的形式如下: ```ts interface ReturnItemFn { (para: T): T } ``` 那么当我们想传入一个number作为参数的时候,就可以这样声明函数: ```ts const returnItem: ReturnItemFn = para => para ``` ### ### 类声明 使用泛型声明类的时候,既可以作用于类本身,也可以作用与类的成员函数 下面简单实现一个元素同类型的栈结构,如下所示: ```ts class Stack { private arr: T[] = [] public push(item: T) { this.arr.push(item) } public pop() { this.arr.pop() } } ``` 使用方式如下: ```ts const stack = new Stacn() ``` 如果上述只能传递 `string` 和 `number` 类型,这时候就可以使用 `` 的方式猜实现**约束泛型**,如下所示: ![](https://static.vue-js.com/67d212a0-0e17-11ec-8e64-91fdec0f05a1.png) 除了上述的形式,泛型更高级的使用如下: 例如要设计一个函数,这个函数接受两个参数,一个参数为对象,另一个参数为对象上的属性,我们通过这两个参数返回这个属性的值 这时候就设计到泛型的索引类型和约束类型共同实现 ### 索引类型、约束类型 索引类型 `keyof T` 把传入的对象的属性类型取出生成一个联合类型,这里的泛型 U 被约束在这个联合类型中,如下所示: ```ts function getValue(obj: T, key: U) { return obj[key] // ok } ``` 上述为什么需要使用泛型约束,而不是直接定义第一个参数为 `object`类型,是因为默认情况 `object` 指的是`{}`,而我们接收的对象是各种各样的,一个泛型来表示传入的对象类型,比如 `T extends object` 使用如下图所示: ![](https://static.vue-js.com/74fcbd40-0e17-11ec-a752-75723a64e8f5.png) ### 多类型约束 例如如下需要实现两个接口的类型约束: ```ts interface FirstInterface { doSomething(): number } interface SecondInterface { doSomethingElse(): string } ``` 可以创建一个接口继承上述两个接口,如下: ```ts interface ChildInterface extends FirstInterface, SecondInterface { } ``` 正确使用如下: ```ts class Demo { private genericProperty: T constructor(genericProperty: T) { this.genericProperty = genericProperty } useT() { this.genericProperty.doSomething() this.genericProperty.doSomethingElse() } } ``` 通过泛型约束就可以达到多类型约束的目的 ## 三、应用场景 通过上面初步的了解,后述在编写 `typescript` 的时候,定义函数,接口或者类的时候,不预先定义好具体的类型,而在使用的时候在指定类型的一种特性的时候,这种情况下就可以使用泛型 灵活的使用泛型定义类型,是掌握`typescript` 必经之路 ## 参考文献 - https://www.tslang.cn/docs/handbook/generics.html ================================================ FILE: docs/typescript/high type.md ================================================ # 面试官:说说你对 TypeScript 中高级类型的理解?有哪些? ![](https://static.vue-js.com/bda521e0-1065-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 除了`string`、`number`、`boolean` 这种基础类型外,在 `typescript` 类型声明中还存在一些高级的类型应用 这些高级类型,是`typescript`为了保证语言的灵活性,所使用的一些语言特性。这些特性有助于我们应对复杂多变的开发场景 ## 二、有哪些 常见的高级类型有如下: - 交叉类型 - 联合类型 - 类型别名 - 类型索引 - 类型约束 - 映射类型 - 条件类型 ### 交叉类型 通过 `&` 将多个类型合并为一个类型,包含了所需的所有类型的特性,本质上是一种并的操作 语法如下: ```ts T & U ``` 适用于对象合并场景,如下将声明一个函数,将两个对象合并成一个对象并返回: ```ts function extend(first: T, second: U) : T & U { let result: = {} for (let key in first) { result[key] = first[key] } for (let key in second) { if(!result.hasOwnProperty(key)) { result[key] = second[key] } } return result } ``` ### 联合类型 联合类型的语法规则和逻辑 “或” 的符号一致,表示其类型为连接的多个类型中的任意一个,本质上是一个交的关系 语法如下: ```ts T | U ``` 例如 `number` | `string` | `boolean` 的类型只能是这三个的一种,不能共存 如下所示: ```ts function formatCommandline(command: string[] | string) { let line = ''; if (typeof command === 'string') { line = command.trim(); } else { line = command.join(' ').trim(); } } ``` ### 类型别名 类型别名会给一个类型起个新名字,类型别名有时和接口很像,但是可以作用于原始值、联合类型、元组以及其它任何你需要手写的类型 可以使用 `type SomeName = someValidTypeAnnotation`的语法来创建类型别名: ```ts type some = boolean | string const b: some = true // ok const c: some = 'hello' // ok const d: some = 123 // 不能将类型“123”分配给类型“some” ``` 此外类型别名可以是泛型: ```ts type Container = { value: T }; ``` 也可以使用类型别名来在属性里引用自己: ```ts type Tree = { value: T; left: Tree; right: Tree; } ``` 可以看到,类型别名和接口使用十分相似,都可以描述一个对象或者函数 两者最大的区别在于,`interface `只能用于定义对象类型,而 `type` 的声明方式除了对象之外还可以定义交叉、联合、原始类型等,类型声明的方式适用范围显然更加广泛 ### 类型索引 `keyof` 类似于 `Object.keys` ,用于获取一个接口中 Key 的联合类型。 ```ts interface Button { type: string text: string } type ButtonKeys = keyof Button // 等效于 type ButtonKeys = "type" | "text" ``` ### 类型约束 通过关键字 `extend` 进行约束,不同于在 `class` 后使用 `extends` 的继承作用,泛型内使用的主要作用是对泛型加以约束 ```ts type BaseType = string | number | boolean // 这里表示 copy 的参数 // 只能是字符串、数字、布尔这几种基础类型 function copy(arg: T): T { return arg } ``` 类型约束通常和类型索引一起使用,例如我们有一个方法专门用来获取对象的值,但是这个对象并不确定,我们就可以使用 `extends` 和 `keyof` 进行约束。 ```ts function getValue(obj: T, key: K) { return obj[key] } const obj = { a: 1 } const a = getValue(obj, 'a') ``` ### 映射类型 通过 `in` 关键字做类型的映射,遍历已有接口的 `key` 或者是遍历联合类型,如下例子: ```ts type Readonly = { readonly [P in keyof T]: T[P]; }; interface Obj { a: string b: string } type ReadOnlyObj = Readonly ``` 上述的结构,可以分成这些步骤: - keyof T:通过类型索引 keyof 的得到联合类型 'a' | 'b' - P in keyof T 等同于 p in 'a' | 'b',相当于执行了一次 forEach 的逻辑,遍历 'a' | 'b' 所以最终`ReadOnlyObj`的接口为下述: ```ts interface ReadOnlyObj { readonly a: string; readonly b: string; } ``` ### 条件类型 条件类型的语法规则和三元表达式一致,经常用于一些类型不确定的情况。 ```ts T extends U ? X : Y ``` 上面的意思就是,如果 T 是 U 的子集,就是类型 X,否则为类型 Y ## 三、总结 可以看到,如果只是掌握了 `typeScript` 的一些基础类型,可能很难游刃有余的去使用 `typeScript`,需要了解一些`typescript`的高阶用法 并且`typescript`在版本的迭代中新增了很多功能,需要不断学习与掌握 ## 参考文献 - https://www.tslang.cn/docs/handbook/advanced-types.html - https://juejin.cn/post/6844904003604578312 - https://zhuanlan.zhihu.com/p/103846208 ================================================ FILE: docs/typescript/interface.md ================================================ # 面试官:说说你对 TypeScript 中接口的理解?应用场景? ![](https://static.vue-js.com/193389b0-0b2b-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 **接口**是一系列抽象方法的声明,是一些方法特征的集合,这些方法都应该是抽象的,需要由具体的**类**去实现,然后第三方就可以通过这组抽象方法调用,让具体的类执行具体的方法 简单来讲,一个接口所描述的是一个对象相关的属性和方法,但并不提供具体创建此对象实例的方法 `typescript`的核心功能之一就是对类型做检测,虽然这种检测方式是“鸭式辨型法”,而接口的作用就是为为这些类型命名和为你的代码或第三方代码定义一个约定 ## 二、使用方式 接口定义如下: ```ts interface interface_name { } ``` 例如有一个函数,这个函数接受一个 `User` 对象,然后返回这个 `User` 对象的 `name` 属性: ```ts const getUserName = (user) => user.name ``` 可以看到,参数需要有一个`user`的`name`属性,可以通过接口描述`user`参数的结构 ```ts interface User { name: string age: number } const getUserName = (user: User) => user.name ``` 这些属性并不一定全部实现,上述传入的对象必须拥有`name`和`age`属性,否则`typescript`在编译阶段会报错,如下图: ![](https://static.vue-js.com/25d3a790-0b2b-11ec-a752-75723a64e8f5.png) 如果不想要`age`属性的话,这时候可以采用**可选属性**,如下表示: ```ts interface User { name: string age?: number } ``` 这时候`age`属性则可以是`number`类型或者`undefined`类型 有些时候,我们想要一个属性变成只读属性,在`typescript`只需要使用`readonly`声明,如下: ```ts interface User { name: string age?: number readonly isMale: boolean } ``` 当我们修改属性的时候,就会出现警告,如下所示: ![](https://static.vue-js.com/2f6d3c30-0b2b-11ec-8e64-91fdec0f05a1.png) 这是属性中有一个函数,可以如下表示: ```ts interface User { name: string age?: number readonly isMale: boolean say: (words: string) => string } ``` 如果传递的对象不仅仅是上述的属性,这时候可以使用: - 类型推断 ``` interface User { name: string age: number } const getUserName = (user: User) => user.name getUserName({color: 'yellow'} as User) ``` - 给接口添加字符串**索引签名** ```ts interface User { name: string age: number [propName: string]: any; } ``` 接口还能实现继承,如下图: ![](https://static.vue-js.com/38a41760-0b2b-11ec-8e64-91fdec0f05a1.png) 也可以继承多个,父类通过逗号隔开,如下: ```ts interface Father { color: String } interface Mother { height: Number } interface Son extends Father,Mother{ name: string age: Number } ``` ## 三、应用场景 例如在`javascript`中定义一个函数,用来获取用户的姓名和年龄: ```js const getUserInfo = function(user) { // ... return name: ${user.name}, age: ${user.age} } ``` 如果多人开发的都需要用到这个函数的时候,如果没有注释,则可能出现各种运行时的错误,这时候就可以使用接口定义参数变量: ```ts // 先定义一个接口 interface IUser { name: string; age: number; } const getUserInfo = (user: IUser): string => { return `name: ${user.name}, age: ${user.age}`; }; // 正确的调用 getUserInfo({name: "koala", age: 18}); ``` 包括后面讲到类的时候也会应用到接口 ## 参考文献 - https://www.tslang.cn/docs/handbook/interfaces.html ================================================ FILE: docs/typescript/namespace_module.md ================================================ # 面试官:说说对 TypeScript 中命名空间与模块的理解?区别? ![](https://static.vue-js.com/9378d760-137e-11ec-8e64-91fdec0f05a1.png) ## 一、模块 `TypeScript` 与` ECMAScript` 2015 一样,任何包含顶级 `import` 或者 `export` 的文件都被当成一个模块 相反地,如果一个文件不带有顶级的`import`或者`export`声明,那么它的内容被视为全局可见的 例如我们在在一个 `TypeScript` 工程下建立一个文件 `1.ts`,声明一个变量`a`,如下: ```ts const a = 1 ``` 然后在另一个文件同样声明一个变量`a`,这时候会出现错误信息 ![](https://static.vue-js.com/a239d970-137e-11ec-a752-75723a64e8f5.png) 提示重复声明`a`变量,但是所处的空间是全局的 如果需要解决这个问题,则通过`import`或者`export`引入模块系统即可,如下: ```ts const a = 10; export default a ``` 在`typescript`中,`export`关键字可以导出变量或者类型,用法与`es6`模块一致,如下: ```ts export const a = 1 export type Person = { name: String } ``` 通过`import` 引入模块,如下: ```ts import { a, Person } from './export'; ``` ## 二、命名空间 命名空间一个最明确的目的就是解决重名问题 命名空间定义了标识符的可见范围,一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的 这样,在一个新的名字空间中可定义任何标识符,它们不会与任何已有的标识符发生冲突,因为已有的定义都处于其他名字空间中 `TypeScript` 中命名空间使用 `namespace` 来定义,语法格式如下: ```ts namespace SomeNameSpaceName { export interface ISomeInterfaceName { } export class SomeClassName { } } ``` 以上定义了一个命名空间 `SomeNameSpaceName`,如果我们需要在外部可以调用 `SomeNameSpaceName` 中的类和接口,则需要在类和接口添加 `export` 关键字 使用方式如下: ```ts SomeNameSpaceName.SomeClassName ``` 命名空间本质上是一个对象,作用是将一系列相关的全局变量组织到一个对象的属性,如下: ```ts namespace Letter { export let a = 1; export let b = 2; export let c = 3; // ... export let z = 26; } ``` 编译成`js`如下: ```js var Letter; (function (Letter) { Letter.a = 1; Letter.b = 2; Letter.c = 3; // ... Letter.z = 26; })(Letter || (Letter = {})); ``` ## 三、区别 - 命名空间是位于全局命名空间下的一个普通的带有名字的 JavaScript 对象,使用起来十分容易。但就像其它的全局命名空间污染一样,它很难去识别组件之间的依赖关系,尤其是在大型的应用中 - 像命名空间一样,模块可以包含代码和声明。 不同的是模块可以声明它的依赖 - 在正常的TS项目开发过程中并不建议用命名空间,但通常在通过 d.ts 文件标记 js 库类型的时候使用命名空间,主要作用是给编译器编写代码的时候参考使用 ## 参考文献 - https://www.tslang.cn/docs/handbook/modules.html - https://www.tslang.cn/docs/handbook/namespaces.html - https://www.tslang.cn/docs/handbook/namespaces-and-modules.html ================================================ FILE: docs/typescript/react.md ================================================ # 面试官:说说如何在 React 项目中应用 TypeScript? ![](https://static.vue-js.com/a98974e0-13bc-11ec-a752-75723a64e8f5.png) ## 一、前言 单独的使用 `TypeScript` 并不会导致学习成本很高,但是绝大部分前端开发者的项目都是依赖于框架的 例如与 `Vue`、`React` 这些框架结合使用的时候,会有一定的门槛 使用 `TypeScript` 编写 `React` 代码,除了需要 `TypeScript` 这个库之外,还需要安装 `@types/react`、`@types/react-dom` ```bash npm i @types/react -s npm i @types/react-dom -s ``` 至于上述使用 `@types` 的库的原因在于,目前非常多的 `JavaScript` 库并没有提供自己关于 `TypeScript` 的声明文件 所以,`ts` 并不知道这些库的类型以及对应导出的内容,这里 `@types` 实际就是社区中的 `DefinitelyTyped` 库,定义了目前市面上绝大多数的 `JavaScript` 库的声明 所以下载相关的 `JavaScript` 对应的 `@types` 声明时,就能够使用使用该库对应的类型定义 ## 二、使用方式 在编写 `React` 项目的时候,最常见的使用的组件就是: - 无状态组件 - 有状态组件 - 受控组件 ### 无状态组件 主要作用是用于展示 `UI`,如果使用 `js` 声明,则如下所示: ```jsx import * as React from "React"; export const Logo = (props) => { const { logo, className, alt } = props; return {alt}; }; ``` 但这时候 `ts` 会出现报错提示,原因在于没有定义 `porps` 类型,这时候就可以使用 `interface` 接口去定义 `porps` 即可,如下: ```tsx import * as React from "React"; interface IProps { logo?: string; className?: string; alt?: string; } export const Logo = (props: IProps) => { const { logo, className, alt } = props; return {alt}; }; ``` 但是我们都知道 `props` 里面存在 `children` 属性,我们不可能每个 `porps` 接口里面定义多一个 `children`,如下: ```ts interface IProps { logo?: string; className?: string; alt?: string; children?: ReactNode; } ``` 更加规范的写法是使用 `React` 里面定义好的 `FC` 属性,里面已经定义好 `children` 类型,如下: ```tsx export const Logo: React.FC = (props) => { const { logo, className, alt } = props; return {alt}; }; ``` - React.FC 显式地定义了返回类型,其他方式是隐式推导的 - React.FC 对静态属性:displayName、propTypes、defaultProps 提供了类型检查和自动补全 - React.FC 为 children 提供了隐式的类型(ReactElement | null) ### 有状态组件 可以是一个类组件且存在 `props` 和 `state` 属性 如果使用 `TypeScript` 声明则如下所示: ```tsx import * as React from "React"; interface IProps { color: string; size?: string; } interface IState { count: number; } class App extends React.Component { public state = { count: 1, }; public render() { return
    Hello world
    ; } } ``` 上述通过泛型对 `props`、`state` 进行类型定义,然后在使用的时候就可以在编译器中获取更好的智能提示 关于 `Component` 泛型类的定义,可以参考下 React 的类型定义文件 `node_modules/@types/React/index.d.ts`,如下所示: ```ts class Component { readonly props: Readonly<{ children?: ReactNode }> & Readonly

    ; state: Readonly; } ``` 从上述可以看到,`state` 属性也定义了可读类型,目的是为了防止直接调用 `this.state` 更新状态 ### 受控组件 受控组件的特性在于元素的内容通过组件的状态 `state` 进行控制 由于组件内部的事件是合成事件,不等同于原生事件, 例如一个 `input` 组件修改内部的状态,常见的定义的时候如下所示: ```ts private updateValue(e: React.ChangeEvent) { this.setState({ itemText: e.target.value }) } ``` 常用 `Event` 事件对象类型: - ClipboardEvent 剪贴板事件对象 - DragEvent 拖拽事件对象 - ChangeEvent Change 事件对象 - KeyboardEvent 键盘事件对象 - MouseEvent 鼠标事件对象 - TouchEvent 触摸事件对象 - WheelEvent 滚轮事件对象 - AnimationEvent 动画事件对象 - TransitionEvent 过渡事件对象 `T` 接收一个 `DOM` 元素类型 ## 三、总结 上述只是简单的在 `React` 项目使用 `TypeScript`,但在编写 `React` 项目的时候,还存在 `hooks`、默认参数、以及 `store` 等等...... `TypeScript` 在框架中使用的学习成本相对会更高,需要不断编写才能熟练 ## 参考文献 - [https://juejin.cn/post/6952696734078369828](https://juejin.cn/post/6952696734078369828) - [https://juejin.cn/post/6844903684422254606](https://juejin.cn/post/6844903684422254606) ================================================ FILE: docs/typescript/typescript_javascript.md ================================================ # 面试官:说说你对 TypeScript 的理解?与 JavaScript 的区别? ![](https://static.vue-js.com/58cd3580-0950-11ec-8e64-91fdec0f05a1.png) ## 一、是什么 `TypeScript` 是 `JavaScript` 的类型的超集,支持`ES6`语法,支持面向对象编程的概念,如类、接口、继承、泛型等 > 超集,不得不说另外一个概念,子集,怎么理解这两个呢,举个例子,如果一个集合 A 里面的的所有元素集合 B 里面都存在,那么我们可以理解集合 B 是集合 A 的超集,集合 A 为集合 B 的子集 ![](https://static.vue-js.com/61c2c1f0-0950-11ec-a752-75723a64e8f5.png) 其是一种静态类型检查的语言,提供了类型注解,在代码编译阶段就可以检查出数据类型的错误 同时扩展了` JavaScript` 的语法,所以任何现有的` JavaScript` 程序可以不加改变的在 `TypeScript` 下工作 为了保证兼容性,`TypeScript` 在编译阶段需要编译器编译成纯 `JavaScript` 来运行,是为大型应用之开发而设计的语言,如下: `ts` 文件如下: ```ts const hello: string = "Hello World!"; console.log(hello); ``` 编译文件后: ```js const hello = "Hello World!"; console.log(hello); ``` ## 二、特性 `TypeScript` 的特性主要有如下: - **类型批注和编译时类型检查** :在编译时批注变量类型 - **类型推断**:ts 中没有批注变量类型会自动推断变量的类型 - **类型擦除**:在编译过程中批注的内容和接口会在运行时利用工具擦除 - **接口**:ts 中用接口来定义对象类型 - **枚举**:用于取值被限定在一定范围内的场景 - **Mixin**:可以接受任意类型的值 - **泛型编程**:写代码时使用一些以后才指定的类型 - **名字空间**:名字只在该区域内有效,其他区域可重复使用该名字而不冲突 - **元组**:元组合并了不同类型的对象,相当于一个可以装不同类型数据的数组 - ... ### 类型批注 通过类型批注提供在编译时启动类型检查的静态类型,这是可选的,而且可以忽略而使用 `JavaScript` 常规的动态类型 ```tsx function Add(left: number, right: number): number { return left + right; } ``` 对于基本类型的批注是 `number`、`bool` 和 `string`,而弱或动态类型的结构则是 `any` 类型 ### 类型推断 当类型没有给出时,TypeScript 编译器利用类型推断来推断类型,如下: ```ts let str = "string"; ``` 变量 `str` 被推断为字符串类型,这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时 如果缺乏声明而不能推断出类型,那么它的类型被视作默认的动态 `any` 类型 ### 接口 接口简单来说就是用来描述对象的类型 数据的类型有 `number`、`null`、` string` 等数据格式,对象的类型就是用接口来描述的 ```tsx interface Person { name: string; age: number; } let tom: Person = { name: "Tom", age: 25, }; ``` ## 三、区别 - TypeScript 是 JavaScript 的超集,扩展了 JavaScript 的语法 - TypeScript 可处理已有的 JavaScript 代码,并只对其中的 TypeScript 代码进行编译 - TypeScript 文件的后缀名 .ts (.ts,.tsx,.dts),JavaScript 文件是 .js - 在编写 TypeScript 的文件的时候就会自动编译成 js 文件 更多的区别如下图所示: ![](https://static.vue-js.com/6b544040-0950-11ec-8e64-91fdec0f05a1.png) ## 参考文献 - [https://zhuanlan.zhihu.com/p/140012915](https://zhuanlan.zhihu.com/p/140012915) - [https://www.jianshu.com/p/c8aaba6e8ce0](https://www.jianshu.com/p/c8aaba6e8ce0) - [https://www.cnblogs.com/powertoolsteam/p/13500668.html](https://www.cnblogs.com/powertoolsteam/p/13500668.html) ================================================ FILE: docs/typescript/vue.md ================================================ # 面试官:说说如何在Vue项目中应用TypeScript? ![](https://static.vue-js.com/cc658c10-1565-11ec-8e64-91fdec0f05a1.png) ## 一、前言 与link类似 在`VUE`项目中应用`typescript`,我们需要引入一个库`vue-property-decorator`, 其是基于`vue-class-component`库而来,这个库`vue`官方推出的一个支持使用`class`方式来开发`vue`单文件组件的库 主要的功能如下: - methods 可以直接声明为类的成员方法 - 计算属性可以被声明为类的属性访问器 - 初始化的 data 可以被声明为类属性 - data、render 以及所有的 Vue 生命周期钩子可以直接作为类的成员方法 - 所有其他属性,需要放在装饰器中 ## 二、使用 vue-property-decorator 主要提供了多个装饰器和一个函数: - @Prop - @PropSync - @Model - @Watch - @Provide - @Inject - @ProvideReactive - @InjectReactive - @Emit - @Ref - @Component (由 vue-class-component 提供) - Mixins (由 vue-class-component 提供) ### @Component `Component`装饰器它注明了此类为一个`Vue`组件,因此即使没有设置选项也不能省略 如果需要定义比如 `name`、`components`、`filters`、`directives`以及自定义属性,就可以在`Component`装饰器中定义,如下: ```vue import {Component,Vue} from 'vue-property-decorator'; import {componentA,componentB} from '@/components'; @Component({ components:{ componentA, componentB, }, directives: { focus: { // 指令的定义 inserted: function (el) { el.focus() } } } }) export default class YourCompoent extends Vue{ } ``` ### computed、data、methods 这里取消了组件的data和methods属性,以往data返回对象中的属性、methods中的方法需要直接定义在Class中,当做类的属性和方法 ```ts @Component export default class HelloDecorator extends Vue { count: number = 123 // 类属性相当于以前的 data add(): number { // 类方法就是以前的方法 this.count + 1 } // 获取计算属性 get total(): number { return this.count + 1 } // 设置计算属性 set total(param:number): void { this.count = param } } ``` ### @props 组件接收属性的装饰器,如下使用: ```js import {Component,Vue,Prop} from vue-property-decorator; @Component export default class YourComponent extends Vue { @Prop(String) propA:string; @Prop([String,Number]) propB:string|number; @Prop({ type: String, // type: [String , Number] default: 'default value', // 一般为String或Number //如果是对象或数组的话。默认值从一个工厂函数中返回 // defatult: () => { // return ['a','b'] // } required: true, validator: (value) => { return [ 'InProcess', 'Settled' ].indexOf(value) !== -1 } }) propC:string; } ``` ### @watch 实际就是`Vue`中的监听器,如下: ```vue import { Vue, Component, Watch } from 'vue-property-decorator' @Component export default class YourComponent extends Vue { @Watch('child') onChildChanged(val: string, oldVal: string) {} @Watch('person', { immediate: true, deep: true }) onPersonChanged1(val: Person, oldVal: Person) {} @Watch('person') onPersonChanged2(val: Person, oldVal: Person) {} } ``` ### @emit `vue-property-decorator` 提供的 `@Emit` 装饰器就是代替`Vue `中的事件的触发`$emit`,如下: ````TS import {Vue, Component, Emit} from 'vue-property-decorator'; @Component({}) export default class Some extends Vue{ mounted(){ this.$on('emit-todo', function(n) { console.log(n) }) this.emitTodo('world'); } @Emit() emitTodo(n: string){ console.log('hello'); } } ```` ## 三 、总结 可以看到上述`typescript`版本的`vue class`的语法与平时`javascript`版本使用起来还是有很大的不同,多处用到`class`与装饰器,但实际上本质是一致的,只有不断编写才会得心应手 ================================================ FILE: docs/vue/404.md ================================================ # 面试官:vue项目本地开发完成后部署到服务器后报404是什么原因呢? ![image.png](https://static.vue-js.com/002c9320-4f3e-11eb-ab90-d9ae814b240d.png) ## 一、如何部署 前后端分离开发模式下,前后端是独立布署的,前端只需要将最后的构建物上传至目标服务器的`web`容器指定的静态目录下即可 我们知道`vue`项目在构建后,是生成一系列的静态文件 ![](https://imgkr2.cn-bj.ufileos.com/b9d13e56-f859-4b4b-a9da-a703a34c2f5d.png?UCloudPublicKey=TOKEN_8d8b72be-579a-4e83-bfd0-5f6ce1546f13&Signature=m1qDXRSFHrfXlnAtAlVhjoLKP70%253D&Expires=1609927181) 常规布署我们只需要将这个目录上传至目标服务器即可 ```bash // scp 上传 user为主机登录用户,host为主机外网ip, xx为web容器静态资源路径 scp dist.zip user@host:/xx/xx/xx ``` 让`web`容器跑起来,以`nginx`为例 ```bash server { listen 80; server_name www.xxx.com; location / { index /data/dist/index.html; } } ``` 配置完成记得重启`nginx` ```bash // 检查配置是否正确 nginx -t // 平滑重启 nginx -s reload ``` 操作完后就可以在浏览器输入域名进行访问了 当然上面只是提到最简单也是最直接的一种布署方式 什么自动化,镜像,容器,流水线布署,本质也是将这套逻辑抽象,隔离,用程序来代替重复性的劳动,本文不展开 ## 二、404问题 这是一个经典的问题,相信很多同学都有遇到过,那么你知道其真正的原因吗? 我们先还原一下场景: - `vue`项目在本地时运行正常,但部署到服务器中,刷新页面,出现了404错误 先定位一下,HTTP 404 错误意味着链接指向的资源不存在 问题在于为什么不存在?且为什么只有`history`模式下会出现这个问题? ### 为什么history模式下有问题 `Vue`是属于单页应用(single-page application) 而`SPA`是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个`index.html` 现在,我们回头来看一下我们的`nginx`配置 ```js server { listen 80; server_name www.xxx.com; location / { index /data/dist/index.html; } } ``` 可以根据 `nginx` 配置得出,当我们在地址栏输入 `www.xxx.com` 时,这时会打开我们 `dist` 目录下的 `index.html` 文件,然后我们在跳转路由进入到 `www.xxx.com/login` 关键在这里,当我们在 `website.com/login` 页执行刷新操作,`nginx location` 是没有相关配置的,所以就会出现 404 的情况 ### 为什么hash模式下没有问题 `router hash` 模式我们都知道是用符号#表示的,如 `website.com/#/login`, `hash` 的值为 `#/login` 它的特点在于:`hash` 虽然出现在 `URL` 中,但不会被包括在 `HTTP` 请求中,对服务端完全没有影响,因此改变 `hash` 不会重新加载页面 `hash` 模式下,仅 `hash` 符号之前的内容会被包含在请求中,如 `website.com/#/login` 只有 `website.com` 会被包含在请求中 ,因此对于服务端来说,即使没有配置`location`,也不会返回404错误 ## 解决方案 看到这里我相信大部分同学都能想到怎么解决问题了, 产生问题的本质是因为我们的路由是通过JS来执行视图切换的, 当我们进入到子路由时刷新页面,`web`容器没有相对应的页面此时会出现404 所以我们只需要配置将任意页面都重定向到 `index.html`,把路由交由前端处理 对`nginx`配置文件`.conf`修改,添加`try_files $uri $uri/ /index.html;` ```bash server { listen 80; server_name www.xxx.com; location / { index /data/dist/index.html; try_files $uri $uri/ /index.html; } } ``` 修改完配置文件后记得配置的更新 ```bash nginx -s reload ``` 这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 `index.html` 文件 为了避免这种情况,你应该在 `Vue` 应用里面覆盖所有的路由情况,然后在给出一个 404 页面 ```js const router = new VueRouter({ mode: 'history', routes: [ { path: '*', component: NotFoundComponent } ] }) ``` 关于后端配置方案还有:`Apache`、`nodejs`等,思想是一致的,这里就不展开述说了 ## 参考文献 - https://juejin.cn/post/6844903872637632525 - https://vue-js.com/topic/5f8cf91d96b2cb0032c385c0 ================================================ FILE: docs/vue/axios.md ================================================ # 面试官:Vue项目中有封装过axios吗?主要是封装哪方面的? ![](https://static.vue-js.com/2bf1e460-45a7-11eb-85f6-6fac77c0c9b3.png) ## 一、axios是什么 `axios` 是一个轻量的 `HTTP`客户端 基于 `XMLHttpRequest` 服务来执行 `HTTP` 请求,支持丰富的配置,支持 `Promise`,支持浏览器端和 `Node.js` 端。自`Vue`2.0起,尤大宣布取消对 `vue-resource` 的官方推荐,转而推荐 `axios`。现在 `axios` 已经成为大部分 `Vue` 开发者的首选 ### 特性 - 从浏览器中创建 `XMLHttpRequests` - 从 `node.js` 创建 `http`请求 - 支持 `Promise` API - 拦截请求和响应 - 转换请求数据和响应数据 - 取消请求 - 自动转换` JSON` 数据 - 客户端支持防御`XSRF` ### 基本使用 安装 ```js // 项目中安装 npm install axios --S // cdn 引入 ``` 导入 ```js import axios from 'axios' ``` 发送请求 ```js axios({ url:'xxx', // 设置请求的地址 method:"GET", // 设置请求方法 params:{ // get请求使用params进行参数凭借,如果是post请求用data type: '', page: 1 } }).then(res => { // res为后端返回的数据 console.log(res); }) ``` 并发请求`axios.all([])` ```js function getUserAccount() { return axios.get('/user/12345'); } function getUserPermissions() { return axios.get('/user/12345/permissions'); } axios.all([getUserAccount(), getUserPermissions()]) .then(axios.spread(function (res1, res2) { // res1第一个请求的返回的内容,res2第二个请求返回的内容 // 两个请求都执行完成才会执行 })); ``` ## 二、为什么要封装 `axios` 的 API 很友好,你完全可以很轻松地在项目中直接使用。 不过随着项目规模增大,如果每发起一次`HTTP`请求,就要把这些比如设置超时时间、设置请求头、根据项目环境判断使用哪个请求地址、错误处理等等操作,都需要写一遍 这种重复劳动不仅浪费时间,而且让代码变得冗余不堪,难以维护。为了提高我们的代码质量,我们应该在项目中二次封装一下 `axios` 再使用 举个例子: ```js axios('http://localhost:3000/data', { // 配置代码 method: 'GET', timeout: 1000, withCredentials: true, headers: { 'Content-Type': 'application/json', Authorization: 'xxx', }, transformRequest: [function (data, headers) { return data; }], // 其他请求配置... }) .then((data) => { // todo: 真正业务逻辑代码 console.log(data); }, (err) => { // 错误处理代码 if (err.response.status === 401) { // handle authorization error } if (err.response.status === 403) { // handle server forbidden error } // 其他错误处理..... console.log(err); }); ``` 如果每个页面都发送类似的请求,都要写一堆的配置与错误处理,就显得过于繁琐了 这时候我们就需要对`axios`进行二次封装,让使用更为便利 ## 三、如何封装 封装的同时,你需要和 后端协商好一些约定,请求头,状态码,请求超时时间....... 设置接口请求前缀:根据开发、测试、生产环境的不同,前缀需要加以区分 请求头 : 来实现一些具体的业务,必须携带一些参数才可以请求(例如:会员业务) 状态码: 根据接口返回的不同`status` , 来执行不同的业务,这块需要和后端约定好 请求方法:根据`get`、`post`等方法进行一个再次封装,使用起来更为方便 请求拦截器: 根据请求的请求头设定,来决定哪些请求可以访问 响应拦截器: 这块就是根据 后端`返回来的状态码判定执行不同业务 ### 设置接口请求前缀 利用`node`环境变量来作判断,用来区分开发、测试、生产环境 ```js if (process.env.NODE_ENV === 'development') { axios.defaults.baseURL = 'http://dev.xxx.com' } else if (process.env.NODE_ENV === 'production') { axios.defaults.baseURL = 'http://prod.xxx.com' } ``` 在本地调试的时候,还需要在`vue.config.js`文件中配置`devServer`实现代理转发,从而实现跨域 ```js devServer: { proxy: { '/proxyApi': { target: 'http://dev.xxx.com', changeOrigin: true, pathRewrite: { '/proxyApi': '' } } } } ``` ### 设置请求头与超时时间 大部分情况下,请求头都是固定的,只有少部分情况下,会需要一些特殊的请求头,这里将普适性的请求头作为基础配置。当需要特殊请求头时,将特殊请求头作为参数传入,覆盖基础配置 ```js const service = axios.create({ ... timeout: 30000, // 请求 30s 超时 headers: { get: { 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' // 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来 }, post: { 'Content-Type': 'application/json;charset=utf-8' // 在开发中,一般还需要单点登录或者其他功能的通用请求头,可以一并配置进来 } }, }) ``` ### 封装请求方法 先引入封装好的方法,在要调用的接口重新封装成一个方法暴露出去 ```js // get 请求 export function httpGet({ url, params = {} }) { return new Promise((resolve, reject) => { axios.get(url, { params }).then((res) => { resolve(res.data) }).catch(err => { reject(err) }) }) } // post // post请求 export function httpPost({ url, data = {}, params = {} }) { return new Promise((resolve, reject) => { axios({ url, method: 'post', transformRequest: [function (data) { let ret = '' for (let it in data) { ret += encodeURIComponent(it) + '=' + encodeURIComponent(data[it]) + '&' } return ret }], // 发送的数据 data, // url参数 params }).then(res => { resolve(res.data) }) }) } ``` 把封装的方法放在一个`api.js`文件中 ```js import { httpGet, httpPost } from './http' export const getorglist = (params = {}) => httpGet({ url: 'apps/api/org/list', params }) ``` 页面中就能直接调用 ```js // .vue import { getorglist } from '@/assets/js/api' getorglist({ id: 200 }).then(res => { console.log(res) }) ``` 这样可以把`api`统一管理起来,以后维护修改只需要在`api.js`文件操作即可 ### 请求拦截器 请求拦截器可以在每个请求里加上token,做了统一处理后维护起来也方便 ```js // 请求拦截器 axios.interceptors.request.use( config => { // 每次发送请求之前判断是否存在token // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况,此处token一般是用户完成登录后储存到localstorage里的 token && (config.headers.Authorization = token) return config }, error => { return Promise.error(error) }) ``` ### 响应拦截器 响应拦截器可以在接收到响应后先做一层操作,如根据状态码判断登录状态、授权 ```js // 响应拦截器 axios.interceptors.response.use(response => { // 如果返回的状态码为200,说明接口请求成功,可以正常拿到数据 // 否则的话抛出错误 if (response.status === 200) { if (response.data.code === 511) { // 未授权调取授权接口 } else if (response.data.code === 510) { // 未登录跳转登录页 } else { return Promise.resolve(response) } } else { return Promise.reject(response) } }, error => { // 我们可以在这里对异常状态作统一处理 if (error.response.status) { // 处理请求失败的情况 // 对不同返回码对相应处理 return Promise.reject(error.response) } }) ``` ### 小结 - 封装是编程中很有意义的手段,简单的`axios`封装,就可以让我们可以领略到它的魅力 - 封装 `axios` 没有一个绝对的标准,只要你的封装可以满足你的项目需求,并且用起来方便,那就是一个好的封装方案 ## 参考文献 - https://www.html.cn/qa/vue-js/20544.html - https://juejin.cn/post/6844904033782611976 - https://juejin.cn/post/6844903801451708429 ================================================ FILE: docs/vue/axiosCode.md ================================================ # 面试官:你了解axios的原理吗?有看过它的源码吗? ![](https://static.vue-js.com/1564f7d0-4662-11eb-ab90-d9ae814b240d.png) ## 一、axios的使用 关于`axios`的基本使用,上篇文章已经有所涉及,这里再稍微回顾下: **发送请求** ```js import axios from 'axios'; axios(config) // 直接传入配置 axios(url[, config]) // 传入url和配置 axios[method](url[, option]) // 直接调用请求方式方法,传入url和配置 axios[method](url[, data[, option]]) // 直接调用请求方式方法,传入data、url和配置 axios.request(option) // 调用 request 方法 const axiosInstance = axios.create(config) // axiosInstance 也具有以上 axios 的能力 axios.all([axiosInstance1, axiosInstance2]).then(axios.spread(response1, response2)) // 调用 all 和传入 spread 回调 ``` **请求拦截器** ```js axios.interceptors.request.use(function (config) { // 这里写发送请求前处理的代码 return config; }, function (error) { // 这里写发送请求错误相关的代码 return Promise.reject(error); }); ``` **响应拦截器** ```js axios.interceptors.response.use(function (response) { // 这里写得到响应数据后处理的代码 return response; }, function (error) { // 这里写得到错误响应处理的代码 return Promise.reject(error); }); ``` **取消请求** ```js // 方式一 const CancelToken = axios.CancelToken; const source = CancelToken.source(); axios.get('xxxx', { cancelToken: source.token }) // 取消请求 (请求原因是可选的) source.cancel('主动取消请求'); // 方式二 const CancelToken = axios.CancelToken; let cancel; axios.get('xxxx', { cancelToken: new CancelToken(function executor(c) { cancel = c; }) }); cancel('主动取消请求'); ``` ## 二、实现一个简易版axios 构建一个`Axios`构造函数,核心代码为`request` ```js class Axios { constructor() { } request(config) { return new Promise(resolve => { const {url = '', method = 'get', data = {}} = config; // 发送ajax请求 const xhr = new XMLHttpRequest(); xhr.open(method, url, true); xhr.onload = function() { console.log(xhr.responseText) resolve(xhr.responseText); } xhr.send(data); }) } } ``` 导出`axios`实例 ```js // 最终导出axios的方法,即实例的request方法 function CreateAxiosFn() { let axios = new Axios(); let req = axios.request.bind(axios); return req; } // 得到最后的全局变量axios let axios = CreateAxiosFn(); ``` 上述就已经能够实现`axios({ })`这种方式的请求 下面是来实现下`axios.method()`这种形式的请求 ```js // 定义get,post...方法,挂在到Axios原型上 const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post']; methodsArr.forEach(met => { Axios.prototype[met] = function() { console.log('执行'+met+'方法'); // 处理单个方法 if (['get', 'delete', 'head', 'options'].includes(met)) { // 2个参数(url[, config]) return this.request({ method: met, url: arguments[0], ...arguments[1] || {} }) } else { // 3个参数(url[,data[,config]]) return this.request({ method: met, url: arguments[0], data: arguments[1] || {}, ...arguments[2] || {} }) } } }) ``` 将`Axios.prototype`上的方法搬运到`request`上 首先实现个工具类,实现将`b`方法混入到`a`,并且修改`this`指向 ```js const utils = { extend(a,b, context) { for(let key in b) { if (b.hasOwnProperty(key)) { if (typeof b[key] === 'function') { a[key] = b[key].bind(context); } else { a[key] = b[key] } } } } } ``` 修改导出的方法 ```js function CreateAxiosFn() { let axios = new Axios(); let req = axios.request.bind(axios); // 增加代码 utils.extend(req, Axios.prototype, axios) return req; } ``` 构建拦截器的构造函数 ```js class InterceptorsManage { constructor() { this.handlers = []; } use(fullfield, rejected) { this.handlers.push({ fullfield, rejected }) } } ``` 实现`axios.interceptors.response.use`和`axios.interceptors.request.use` ```js class Axios { constructor() { // 新增代码 this.interceptors = { request: new InterceptorsManage, response: new InterceptorsManage } } request(config) { ... } } ``` 执行语句`axios.interceptors.response.use`和`axios.interceptors.request.use`的时候,实现获取`axios`实例上的`interceptors`对象,然后再获取`response`或`request`拦截器,再执行对应的拦截器的`use`方法 把`Axios`上的方法和属性搬到`request`过去 ```js function CreateAxiosFn() { let axios = new Axios(); let req = axios.request.bind(axios); // 混入方法, 处理axios的request方法,使之拥有get,post...方法 utils.extend(req, Axios.prototype, axios) // 新增代码 utils.extend(req, axios) return req; } ``` 现在`request`也有了`interceptors`对象,在发送请求的时候,会先获取`request`拦截器的`handlers`的方法来执行 首先将执行`ajax`的请求封装成一个方法 ```js request(config) { this.sendAjax(config) } sendAjax(config){ return new Promise(resolve => { const {url = '', method = 'get', data = {}} = config; // 发送ajax请求 console.log(config); const xhr = new XMLHttpRequest(); xhr.open(method, url, true); xhr.onload = function() { console.log(xhr.responseText) resolve(xhr.responseText); }; xhr.send(data); }) } ``` 获得`handlers`中的回调 ```js request(config) { // 拦截器和请求组装队列 let chain = [this.sendAjax.bind(this), undefined] // 成对出现的,失败回调暂时不处理 // 请求拦截 this.interceptors.request.handlers.forEach(interceptor => { chain.unshift(interceptor.fullfield, interceptor.rejected) }) // 响应拦截 this.interceptors.response.handlers.forEach(interceptor => { chain.push(interceptor.fullfield, interceptor.rejected) }) // 执行队列,每次执行一对,并给promise赋最新的值 let promise = Promise.resolve(config); while(chain.length > 0) { promise = promise.then(chain.shift(), chain.shift()) } return promise; } ``` `chains`大概是`['fulfilled1','reject1','fulfilled2','reject2','this.sendAjax','undefined','fulfilled2','reject2','fulfilled1','reject1']`这种形式 这样就能够成功实现一个简易版`axios` ## 三、源码分析 首先看看目录结构 ![](https://static.vue-js.com/9d90eaa0-48b6-11eb-85f6-6fac77c0c9b3.png) `axios`发送请求有很多实现的方法,实现入口文件为`axios.js ` ```js function createInstance(defaultConfig) { var context = new Axios(defaultConfig); // instance指向了request方法,且上下文指向context,所以可以直接以 instance(option) 方式调用 // Axios.prototype.request 内对第一个参数的数据类型判断,使我们能够以 instance(url, option) 方式调用 var instance = bind(Axios.prototype.request, context); // 把Axios.prototype上的方法扩展到instance对象上, // 并指定上下文为context,这样执行Axios原型链上的方法时,this会指向context utils.extend(instance, Axios.prototype, context); // Copy context to instance // 把context对象上的自身属性和方法扩展到instance上 // 注:因为extend内部使用的forEach方法对对象做for in 遍历时,只遍历对象本身的属性,而不会遍历原型链上的属性 // 这样,instance 就有了 defaults、interceptors 属性。 utils.extend(instance, context); return instance; } // Create the default instance to be exported 创建一个由默认配置生成的axios实例 var axios = createInstance(defaults); // Factory for creating new instances 扩展axios.create工厂函数,内部也是 createInstance axios.create = function create(instanceConfig) { return createInstance(mergeConfig(axios.defaults, instanceConfig)); }; // Expose all/spread axios.all = function all(promises) { return Promise.all(promises); }; axios.spread = function spread(callback) { return function wrap(arr) { return callback.apply(null, arr); }; }; module.exports = axios; ``` 主要核心是 `Axios.prototype.request`,各种请求方式的调用实现都是在 `request` 内部实现的, 简单看下 `request` 的逻辑 ```js Axios.prototype.request = function request(config) { // Allow for axios('example/url'[, config]) a la fetch API // 判断 config 参数是否是 字符串,如果是则认为第一个参数是 URL,第二个参数是真正的config if (typeof config === 'string') { config = arguments[1] || {}; // 把 url 放置到 config 对象中,便于之后的 mergeConfig config.url = arguments[0]; } else { // 如果 config 参数是否是 字符串,则整体都当做config config = config || {}; } // 合并默认配置和传入的配置 config = mergeConfig(this.defaults, config); // 设置请求方法 config.method = config.method ? config.method.toLowerCase() : 'get'; /* something... 此部分会在后续拦截器单独讲述 */ }; // 在 Axios 原型上挂载 'delete', 'get', 'head', 'options' 且不传参的请求方法,实现内部也是 request utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) { Axios.prototype[method] = function(url, config) { return this.request(utils.merge(config || {}, { method: method, url: url })); }; }); // 在 Axios 原型上挂载 'post', 'put', 'patch' 且传参的请求方法,实现内部同样也是 request utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) { Axios.prototype[method] = function(url, data, config) { return this.request(utils.merge(config || {}, { method: method, url: url, data: data })); }; }); ``` `request`入口参数为`config`,可以说`config`贯彻了`axios`的一生 `axios` 中的 `config `主要分布在这几个地方: - 默认配置 `defaults.js` - `config.method`默认为 `get` - 调用 `createInstance` 方法创建 `axios `实例,传入的`config` - 直接或间接调用 `request` 方法,传入的 `config` ```js // axios.js // 创建一个由默认配置生成的axios实例 var axios = createInstance(defaults); // 扩展axios.create工厂函数,内部也是 createInstance axios.create = function create(instanceConfig) { return createInstance(mergeConfig(axios.defaults, instanceConfig)); }; // Axios.js // 合并默认配置和传入的配置 config = mergeConfig(this.defaults, config); // 设置请求方法 config.method = config.method ? config.method.toLowerCase() : 'get'; ``` 从源码中,可以看到优先级:默认配置对象`default` < `method:get` < `Axios`的实例属性`this.default` < `request`参数 下面重点看看`request`方法 ```js Axios.prototype.request = function request(config) { /* 先是 mergeConfig ... 等,不再阐述 */ // Hook up interceptors middleware 创建拦截器链. dispatchRequest 是重中之重,后续重点 var chain = [dispatchRequest, undefined]; // push各个拦截器方法 注意:interceptor.fulfilled 或 interceptor.rejected 是可能为undefined this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) { // 请求拦截器逆序 注意此处的 forEach 是自定义的拦截器的forEach方法 chain.unshift(interceptor.fulfilled, interceptor.rejected); }); this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) { // 响应拦截器顺序 注意此处的 forEach 是自定义的拦截器的forEach方法 chain.push(interceptor.fulfilled, interceptor.rejected); }); // 初始化一个promise对象,状态为resolved,接收到的参数为已经处理合并过的config对象 var promise = Promise.resolve(config); // 循环拦截器的链 while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); // 每一次向外弹出拦截器 } // 返回 promise return promise; }; ``` 拦截器`interceptors`是在构建`axios`实例化的属性 ```js function Axios(instanceConfig) { this.defaults = instanceConfig; this.interceptors = { request: new InterceptorManager(), // 请求拦截 response: new InterceptorManager() // 响应拦截 }; } ``` `InterceptorManager`构造函数 ```js // 拦截器的初始化 其实就是一组钩子函数 function InterceptorManager() { this.handlers = []; } // 调用拦截器实例的use时就是往钩子函数中push方法 InterceptorManager.prototype.use = function use(fulfilled, rejected) { this.handlers.push({ fulfilled: fulfilled, rejected: rejected }); return this.handlers.length - 1; }; // 拦截器是可以取消的,根据use的时候返回的ID,把某一个拦截器方法置为null // 不能用 splice 或者 slice 的原因是 删除之后 id 就会变化,导致之后的顺序或者是操作不可控 InterceptorManager.prototype.eject = function eject(id) { if (this.handlers[id]) { this.handlers[id] = null; } }; // 这就是在 Axios的request方法中 中循环拦截器的方法 forEach 循环执行钩子函数 InterceptorManager.prototype.forEach = function forEach(fn) { utils.forEach(this.handlers, function forEachHandler(h) { if (h !== null) { fn(h); } }); } ``` 请求拦截器方法是被 `unshift`到拦截器中,响应拦截器是被`push`到拦截器中的。最终它们会拼接上一个叫`dispatchRequest`的方法被后续的 `promise` 顺序执行 ```js var utils = require('./../utils'); var transformData = require('./transformData'); var isCancel = require('../cancel/isCancel'); var defaults = require('../defaults'); var isAbsoluteURL = require('./../helpers/isAbsoluteURL'); var combineURLs = require('./../helpers/combineURLs'); // 判断请求是否已被取消,如果已经被取消,抛出已取消 function throwIfCancellationRequested(config) { if (config.cancelToken) { config.cancelToken.throwIfRequested(); } } module.exports = function dispatchRequest(config) { throwIfCancellationRequested(config); // 如果包含baseUrl, 并且不是config.url绝对路径,组合baseUrl以及config.url if (config.baseURL && !isAbsoluteURL(config.url)) { // 组合baseURL与url形成完整的请求路径 config.url = combineURLs(config.baseURL, config.url); } config.headers = config.headers || {}; // 使用/lib/defaults.js中的transformRequest方法,对config.headers和config.data进行格式化 // 比如将headers中的Accept,Content-Type统一处理成大写 // 比如如果请求正文是一个Object会格式化为JSON字符串,并添加application/json;charset=utf-8的Content-Type // 等一系列操作 config.data = transformData( config.data, config.headers, config.transformRequest ); // 合并不同配置的headers,config.headers的配置优先级更高 config.headers = utils.merge( config.headers.common || {}, config.headers[config.method] || {}, config.headers || {} ); // 删除headers中的method属性 utils.forEach( ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'], function cleanHeaderConfig(method) { delete config.headers[method]; } ); // 如果config配置了adapter,使用config中配置adapter的替代默认的请求方法 var adapter = config.adapter || defaults.adapter; // 使用adapter方法发起请求(adapter根据浏览器环境或者Node环境会有不同) return adapter(config).then( // 请求正确返回的回调 function onAdapterResolution(response) { // 判断是否以及取消了请求,如果取消了请求抛出以取消 throwIfCancellationRequested(config); // 使用/lib/defaults.js中的transformResponse方法,对服务器返回的数据进行格式化 // 例如,使用JSON.parse对响应正文进行解析 response.data = transformData( response.data, response.headers, config.transformResponse ); return response; }, // 请求失败的回调 function onAdapterRejection(reason) { if (!isCancel(reason)) { throwIfCancellationRequested(config); if (reason && reason.response) { reason.response.data = transformData( reason.response.data, reason.response.headers, config.transformResponse ); } } return Promise.reject(reason); } ); }; ``` 再来看看`axios`是如何实现取消请求的,实现文件在`CancelToken.js` ```js function CancelToken(executor) { if (typeof executor !== 'function') { throw new TypeError('executor must be a function.'); } // 在 CancelToken 上定义一个 pending 状态的 promise ,将 resolve 回调赋值给外部变量 resolvePromise var resolvePromise; this.promise = new Promise(function promiseExecutor(resolve) { resolvePromise = resolve; }); var token = this; // 立即执行 传入的 executor函数,将真实的 cancel 方法通过参数传递出去。 // 一旦调用就执行 resolvePromise 即前面的 promise 的 resolve,就更改promise的状态为 resolve。 // 那么xhr中定义的 CancelToken.promise.then方法就会执行, 从而xhr内部会取消请求 executor(function cancel(message) { // 判断请求是否已经取消过,避免多次执行 if (token.reason) { return; } token.reason = new Cancel(message); resolvePromise(token.reason); }); } CancelToken.source = function source() { // source 方法就是返回了一个 CancelToken 实例,与直接使用 new CancelToken 是一样的操作 var cancel; var token = new CancelToken(function executor(c) { cancel = c; }); // 返回创建的 CancelToken 实例以及取消方法 return { token: token, cancel: cancel }; }; ``` 实际上取消请求的操作是在 `xhr.js` 中也有响应的配合的 ```js if (config.cancelToken) { config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } // 取消请求 request.abort(); reject(cancel); }); } ``` 巧妙的地方在 `CancelToken`中 `executor` 函数,通过`resolve`函数的传递与执行,控制`promise`的状态 ### 小结 ![](https://static.vue-js.com/b1d2ebd0-48b6-11eb-ab90-d9ae814b240d.png) ## 参考文献 - https://juejin.cn/post/6856706569263677447#heading-4 - https://juejin.cn/post/6844903907500490766 - https://github.com/axios/axios ================================================ FILE: docs/vue/bind.md ================================================ # 面试官:双向数据绑定是什么 ![](https://static.vue-js.com/cef7dcc0-3ac9-11eb-85f6-6fac77c0c9b3.png) ## 一、什么是双向绑定 我们先从单向绑定切入单向绑定非常简单,就是把`Model`绑定到`View`,当我们用`JavaScript`代码更新`Model`时,`View`就会自动更新双向绑定就很容易联想到了,在单向绑定的基础上,用户更新了`View`,`Model`的数据也自动被更新了,这种情况就是双向绑定举个栗子 ![](https://static.vue-js.com/d65738d0-3ac9-11eb-ab90-d9ae814b240d.png) 当用户填写表单时,`View`的状态就被更新了,如果此时可以自动更新`Model`的状态,那就相当于我们把`Model`和`View`做了双向绑定关系图如下 ![](https://static.vue-js.com/dcc1d4a0-3ac9-11eb-ab90-d9ae814b240d.png) ## 二、双向绑定的原理是什么 我们都知道 `Vue` 是数据双向绑定的框架,双向绑定由三个重要部分构成 - 数据层(Model):应用的数据及业务逻辑 - 视图层(View):应用的展示效果,各类UI组件 - 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来 而上面的这个分层的架构方案,可以用一个专业术语进行称呼:`MVVM`这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理 ### 理解ViewModel 它的主要职责就是: - 数据变化后更新视图 - 视图变化后更新数据 当然,它还有两个主要部分组成 - 监听器(Observer):对所有数据的属性进行监听 - 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数 ### 三、实现双向绑定 我们还是以`Vue`为例,先来看看`Vue`中的双向绑定流程是什么的 1. `new Vue()`首先执行初始化,对`data`执行响应化处理,这个过程发生`Observe`中 2. 同时对模板执行编译,找到其中动态绑定的数据,从`data`中获取并初始化视图,这个过程发生在`Compile`中 3. 同时定义⼀个更新函数和`Watcher`,将来对应数据变化时`Watcher`会调用更新函数 4. 由于`data`的某个`key`在⼀个视图中可能出现多次,所以每个`key`都需要⼀个管家`Dep`来管理多个`Watcher` 5. 将来data中数据⼀旦发生变化,会首先找到对应的`Dep`,通知所有`Watcher`执行更新函数 流程图如下: ![](https://static.vue-js.com/e5369850-3ac9-11eb-85f6-6fac77c0c9b3.png) ### 实现 先来一个构造函数:执行初始化,对`data`执行响应化处理 ```js class Vue {   constructor(options) {     this.$options = options;     this.$data = options.data;            // 对data选项做响应式处理     observe(this.$data);            // 代理data到vm上     proxy(this);            // 执行编译     new Compile(options.el, this);   } } ``` 对`data`选项执行响应化具体操作 ```js function observe(obj) {   if (typeof obj !== "object" || obj == null) {     return;   }   new Observer(obj); } class Observer {   constructor(value) {     this.value = value;     this.walk(value);   }   walk(obj) {     Object.keys(obj).forEach((key) => {       defineReactive(obj, key, obj[key]);     });   } } ``` #### 编译`Compile` 对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数 ![](https://static.vue-js.com/f27e19c0-3ac9-11eb-85f6-6fac77c0c9b3.png) ```js class Compile {   constructor(el, vm) {     this.$vm = vm;     this.$el = document.querySelector(el);  // 获取dom     if (this.$el) {       this.compile(this.$el);     }   }   compile(el) {     const childNodes = el.childNodes;      Array.from(childNodes).forEach((node) => { // 遍历子元素       if (this.isElement(node)) {   // 判断是否为节点         console.log("编译元素" + node.nodeName);       } else if (this.isInterpolation(node)) {         console.log("编译插值⽂本" + node.textContent);  // 判断是否为插值文本 {{}}       }       if (node.childNodes && node.childNodes.length > 0) {  // 判断是否有子元素         this.compile(node);  // 对子元素进行递归遍历       }     });   }   isElement(node) {     return node.nodeType == 1;   }   isInterpolation(node) {     return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);   } } ``` #### 依赖收集 视图中会用到`data`中某`key`,这称为依赖。同⼀个`key`可能出现多次,每次都需要收集出来用⼀个`Watcher`来维护它们,此过程称为依赖收集多个`Watcher`需要⼀个`Dep`来管理,需要更新时由`Dep`统⼀通知 ![](https://static.vue-js.com/fa191f40-3ac9-11eb-ab90-d9ae814b240d.png) 实现思路 1. `defineReactive`时为每⼀个`key`创建⼀个`Dep`实例 2. 初始化视图时读取某个`key`,例如`name1`,创建⼀个`watcher1` 3. 由于触发`name1`的`getter`方法,便将`watcher1`添加到`name1`对应的Dep中 4. 当`name1`更新,`setter`触发时,便可通过对应`Dep`通知其管理所有`Watcher`更新 ```js // 负责更新视图 class Watcher {   constructor(vm, key, updater) {     this.vm = vm     this.key = key     this.updaterFn = updater     // 创建实例时,把当前实例指定到Dep.target静态属性上     Dep.target = this     // 读一下key,触发get     vm[key]     // 置空     Dep.target = null   }   // 未来执行dom更新函数,由dep调用的   update() {     this.updaterFn.call(this.vm, this.vm[this.key])   } } ``` 声明`Dep` ```js class Dep {   constructor() {     this.deps = [];  // 依赖管理   }   addDep(dep) {     this.deps.push(dep);   }   notify() {      this.deps.forEach((dep) => dep.update());   } } ``` 创建`watcher`时触发`getter` ```js class Watcher {   constructor(vm, key, updateFn) {     Dep.target = this;     this.vm[this.key];     Dep.target = null;   } } ``` 依赖收集,创建`Dep`实例 ```js function defineReactive(obj, key, val) {   this.observe(val);   const dep = new Dep();   Object.defineProperty(obj, key, {     get() {       Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例       return val;     },     set(newVal) {       if (newVal === val) return;       dep.notify(); // 通知dep执行更新方法     },   }); } ``` ## 参考文献 - https://www.liaoxuefeng.com/wiki/1022910821149312/1109527162256416 - https://juejin.cn/post/6844903942254510087#heading-9 面试官VUE系列总进度:3/33 [面试官:说说你对vue的理解\?](http://mp.weixin.qq.com/s?__biz=MzU1OTgxNDQ1Nw==&mid=2247484101&idx=1&sn=83b0983f0fca7d7c556e4cb0bff8c9b8&chksm=fc10c093cb674985ef3bd2966f66fc28c5eb70b0037e4be1af4bf54fb6fa9571985abd31d52f&scene=21#wechat_redirect) [面试官:说说你对SPA(单页应用)的理解\?](http://mp.weixin.qq.com/s?__biz=MzU1OTgxNDQ1Nw==&mid=2247484119&idx=1&sn=d171b28a00d42549d279498944a98519&chksm=fc10c081cb6749976814aaeda6a6433db418223cec57edda7e15b9e5a0ca69ad549655639c61&scene=21#wechat_redirect) ![](https://static.vue-js.com/821b87b0-3ac6-11eb-ab90-d9ae814b240d.png) ================================================ FILE: docs/vue/communication.md ================================================ # 面试官:Vue组件之间的通信方式都有哪些? ![](https://static.vue-js.com/7de50d20-3aca-11eb-85f6-6fac77c0c9b3.png) ## 一、组件间通信的概念 开始之前,我们把**组件间通信**这个词进行拆分 - 组件 - 通信 都知道组件是`vue`最强大的功能之一,`vue`中每一个`.vue`我们都可以视之为一个组件通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的。广义上,任何信息的交通都是通信**组件间通信**即指组件\(`.vue`\)通过某种方式来传递信息以达到某个目的举个栗子我们在使用`UI`框架中的`table`组件,可能会往`table`组件中传入某些数据,这个本质就形成了组件之间的通信 ## 二、组件间通信解决了什么 在古代,人们通过驿站、飞鸽传书、烽火报警、符号、语言、眼神、触碰等方式进行信息传递,到了今天,随着科技水平的飞速发展,通信基本完全利用有线或无线电完成,相继出现了有线电话、固定电话、无线电话、手机、互联网甚至视频电话等各种通信方式从上面这段话,我们可以看到通信的本质是信息同步,共享回到`vue`中,每个组件之间的都有独自的作用域,组件间的数据是无法共享的但实际开发工作中我们常常需要让组件之间共享数据,这也是组件通信的目的要让它们互相之间能进行通讯,这样才能构成一个有机的完整系统 ## 二、组件间通信的分类 组件间通信的分类可以分成以下 - 父子组件之间的通信 - 兄弟组件之间的通信 - 祖孙与后代组件之间的通信 - 非关系组件间之间的通信 关系图: ![](https://static.vue-js.com/85b92400-3aca-11eb-ab90-d9ae814b240d.png) ## 三、组件间通信的方案 整理`vue`中8种常规的通信方案 1. 通过 props 传递 2. 通过 \$emit 触发自定义事件 3. 使用 ref 4. EventBus 5. $parent 或$root 6. attrs 与 listeners 7. Provide 与 Inject 8. Vuex ### props传递数据 ![](https://static.vue-js.com/8f80a670-3aca-11eb-ab90-d9ae814b240d.png) - 适用场景:父组件传递数据给子组件 - 子组件设置`props`属性,定义接收父组件传递过来的参数 - 父组件在使用子组件标签中通过字面量来传递值 `Children.vue` ```js props:{     // 字符串形式  name:String // 接收的类型参数     // 对象形式     age:{           type:Number, // 接收的类型为数值         defaule:18,  // 默认值为18        require:true // age属性必须传递     } } ``` `Father.vue`组件 ```js ``` ### \$emit 触发自定义事件 - 适用场景:子组件传递数据给父组件 - 子组件通过`$emit触发`自定义事件,`$emit`第二个参数为传递的数值 - 父组件绑定监听器获取到子组件传递过来的参数 `Chilfen.vue` ```js this.$emit('add', good) ``` `Father.vue` ```js ``` ### ref - 父组件在使用子组件的时候设置`ref` - 父组件通过设置子组件`ref`来获取数据 父组件 ```js this.$refs.foo  // 获取子组件实例,通过子组件实例我们就能拿到对应的数据 ``` ### EventBus - 使用场景:兄弟组件传值 - 创建一个中央事件总线`EventBus` - 兄弟组件通过`$emit`触发自定义事件,`$emit`第二个参数为传递的数值 - 另一个兄弟组件通过`$on`监听自定义事件 `Bus.js` ```js // 创建一个中央时间总线类 class Bus {   constructor() {     this.callbacks = {};   // 存放事件的名字   }   $on(name, fn) {     this.callbacks[name] = this.callbacks[name] || [];     this.callbacks[name].push(fn);   }   $emit(name, args) {     if (this.callbacks[name]) {       this.callbacks[name].forEach((cb) => cb(args));     }   } } // main.js Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上 // 另一种方式 Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能 ``` `Children1.vue` ```js this.$bus.$emit('foo') ``` `Children2.vue` ```js this.$bus.$on('foo', this.handle) ``` ### $parent 或$ root - 通过共同祖辈`$parent`或者`$root`搭建通信桥连 兄弟组件 `this.$parent.on('add',this.add) ` 另一个兄弟组件 `this.$parent.emit('add') ` ### $attrs 与$ listeners - 适用场景:祖先传递数据给子孙 - 设置批量向下传属性`$attrs`和 `$listeners` - 包含了父级作用域中不作为 `prop` 被识别 \(且获取\) 的特性绑定 \( class 和 style 除外\)。 - 可以通过 `v-bind="$attrs"` 传⼊内部组件 ```js // child:并未在props中声明foo

    {{$attrs.foo}}

    // parent ``` ```js // 给Grandson隔代传值,communication/index.vue // Child2做展开 // Grandson使⽤ {{msg}}
    ``` ### provide 与 inject - 在祖先组件定义`provide`属性,返回传递的值 - 在后代组件通过`inject`接收组件传递过来的值 祖先组件 ```js provide(){     return {         foo:'foo'     } } ``` 后代组件 ```js inject:['foo'] // 获取到祖先组件传递过来的值 ``` ### `vuex` - 适用场景: 复杂关系的组件数据传递 - `Vuex`作用相当于一个用来存储共享变量的容器 ![](https://static.vue-js.com/fa207cd0-3aca-11eb-ab90-d9ae814b240d.png) - `state`用来存放共享变量的地方 - `getter`,可以增加一个`getter`派生状态,\(相当于`store`中的计算属性),用来获得共享变量的值 - `mutations`用来存放修改`state`的方法。 - `actions`也是用来存放修改state的方法,不过`action`是在`mutations`的基础上进行。常用来做一些异步操作 ### 小结 - 父子关系的组件数据传递选择 `props`  与 `$emit`进行传递,也可选择`ref` - 兄弟关系的组件数据传递可选择`$bus`,其次可以选择`$parent`进行传递 - 祖先与后代组件数据传递可选择`attrs`与`listeners`或者 `Provide`与 `Inject` - 复杂关系的组件数据传递可以通过`vuex`存放共享的变量 ## 参考文献 - https://juejin.cn/post/6844903990052782094#heading-0 - https://zh.wikipedia.org/wiki/\%E9\%80\%9A\%E4\%BF\%A1 - https://vue3js.cn/docs/zh 面试官VUE系列总进度:5/33 [面试官:说说你对vue的理解\?](http://mp.weixin.qq.com/s?__biz=MzU1OTgxNDQ1Nw==&mid=2247484101&idx=1&sn=83b0983f0fca7d7c556e4cb0bff8c9b8&chksm=fc10c093cb674985ef3bd2966f66fc28c5eb70b0037e4be1af4bf54fb6fa9571985abd31d52f&scene=21#wechat_redirect) [面试官:说说你对SPA(单页应用)的理解\?](http://mp.weixin.qq.com/s?__biz=MzU1OTgxNDQ1Nw==&mid=2247484119&idx=1&sn=d171b28a00d42549d279498944a98519&chksm=fc10c081cb6749976814aaeda6a6433db418223cec57edda7e15b9e5a0ca69ad549655639c61&scene=21#wechat_redirect) [面试官:说说你对双向绑定的理解\?](http://mp.weixin.qq.com/s?__biz=MzU1OTgxNDQ1Nw==&mid=2247484167&idx=1&sn=7b00b4333ab2722f25f12586b70667ca&chksm=fc10c151cb6748476008dab2f4e6c6264f5d19678305955c85cec1b619e56e8f7457b7357fb9&scene=21#wechat_redirect) [面试官:说说你对Vue生命周期的理解\?](http://mp.weixin.qq.com/s?__biz=MzU1OTgxNDQ1Nw==&mid=2247484176&idx=1&sn=5623421ed2678046ed9e438aadf6e26f&chksm=fc10c146cb67485015f24f7e9f5862c4c685fc33485fe30e1b375a534b4031978439c554e0c0&scene=21#wechat_redirect) ![](https://static.vue-js.com/821b87b0-3ac6-11eb-ab90-d9ae814b240d.png) ================================================ FILE: docs/vue/components_plugin.md ================================================ # 面试官:Vue中组件和插件有什么区别? ![image.png](https://static.vue-js.com/683475e0-3acc-11eb-ab90-d9ae814b240d.png) ## 一、组件是什么 回顾以前对组件的定义: 组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在`Vue`中每一个`.vue`文件都可以视为一个组件 组件的优势 - 降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现 - 调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单 - 提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级 ## 二、插件是什么 插件通常用来为 `Vue` 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种: - 添加全局方法或者属性。如: `vue-custom-element` - 添加全局资源:指令/过滤器/过渡等。如 `vue-touch` - 通过全局混入来添加一些组件选项。如` vue-router` - 添加 `Vue` 实例方法,通过把它们添加到 `Vue.prototype` 上实现。 - 一个库,提供自己的 `API`,同时提供上面提到的一个或多个功能。如` vue-router` ## 三、两者的区别 两者的区别主要表现在以下几个方面: - 编写形式 - 注册形式 - 使用场景 ### 编写形式 #### 编写组件 编写一个组件,可以有很多方式,我们最常见的就是`vue`单文件的这种格式,每一个`.vue`文件我们都可以看成是一个组件 `vue`文件标准格式 ```vue ``` 我们还可以通过`template`属性来编写一个组件,如果组件内容多,我们可以在外部定义`template`组件内容,如果组件内容并不多,我们可直接写在`template`属性上 ```js Vue.component('componentA',{ template: '#testComponent' template: `
    component
    ` // 组件内容少可以通过这种形式 }) ``` #### 编写插件 `vue`插件的实现应该暴露一个 `install` 方法。这个方法的第一个参数是 `Vue` 构造器,第二个参数是一个可选的选项对象 ```js MyPlugin.install = function (Vue, options) { // 1. 添加全局方法或 property Vue.myGlobalMethod = function () { // 逻辑... } // 2. 添加全局资源 Vue.directive('my-directive', { bind (el, binding, vnode, oldVnode) { // 逻辑... } ... }) // 3. 注入组件选项 Vue.mixin({ created: function () { // 逻辑... } ... }) // 4. 添加实例方法 Vue.prototype.$myMethod = function (methodOptions) { // 逻辑... } } ``` ### 注册形式 #### 组件注册 `vue`组件注册主要分为全局注册与局部注册 全局注册通过`Vue.component`方法,第一个参数为组件的名称,第二个参数为传入的配置项 ```js Vue.component('my-component-name', { /* ... */ }) ``` 局部注册只需在用到的地方通过`components`属性注册一个组件 ```js const component1 = {...} // 定义一个组件 export default { components:{ component1 // 局部注册 } } ``` #### 插件注册 插件的注册通过`Vue.use()`的方式进行注册(安装),第一个参数为插件的名字,第二个参数是可选择的配置项 ```js Vue.use(插件名字,{ /* ... */} ) ``` 注意的是: 注册插件的时候,需要在调用 `new Vue()` 启动应用之前完成 `Vue.use`会自动阻止多次注册相同插件,只会注册一次 ### 使用场景 具体的其实在插件是什么章节已经表述了,这里在总结一下 组件 `(Component)` 是用来构成你的 `App` 的业务模块,它的目标是 `App.vue` 插件 `(Plugin)` 是用来增强你的技术栈的功能模块,它的目标是 `Vue` 本身 简单来说,插件就是指对`Vue`的功能的增强或补充 ## 参考文献 - https://vue3js.cn/docs/zh ================================================ FILE: docs/vue/cors.md ================================================ # 面试官:Vue项目中你是如何解决跨域的呢? ![](https://static.vue-js.com/db3045b0-4e31-11eb-85f6-6fac77c0c9b3.png) ## 一、跨域是什么 跨域本质是浏览器基于**同源策略**的一种安全手段 同源策略(Sameoriginpolicy),是一种约定,它是浏览器最核心也最基本的安全功能 所谓同源(即指在同一个域)具有以下三个相同点 - 协议相同(protocol) - 主机相同(host) - 端口相同(port) 反之非同源请求,也就是协议、端口、主机其中一项不相同的时候,这时候就会产生跨域 >一定要注意跨域是浏览器的限制,你用抓包工具抓取接口数据,是可以看到接口已经把数据返回回来了,只是浏览器的限制,你获取不到数据。用postman请求接口能够请求到数据。这些再次印证了跨域是浏览器的限制。 ## 二、如何解决 解决跨域的方法有很多,下面列举了三种: - JSONP - CORS - Proxy 而在`vue`项目中,我们主要针对`CORS`或`Proxy`这两种方案进行展开 ### CORS CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应 `CORS` 实现起来非常方便,只需要增加一些 `HTTP` 头,让服务器能声明允许的访问来源 只要后端实现了 `CORS`,就实现了跨域 ![](https://static.vue-js.com/140deb80-4e32-11eb-ab90-d9ae814b240d.png) 以` koa`框架举例 添加中间件,直接设置`Access-Control-Allow-Origin`响应头 ```js app.use(async (ctx, next)=> { ctx.set('Access-Control-Allow-Origin', '*'); ctx.set('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild'); ctx.set('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS'); if (ctx.method == 'OPTIONS') { ctx.body = 200; } else { await next(); } }) ``` ps: `Access-Control-Allow-Origin` 设置为*其实意义不大,可以说是形同虚设,实际应用中,上线前我们会将`Access-Control-Allow-Origin` 值设为我们目标`host` ### Proxy 代理(Proxy)也称网络代理,是一种特殊的网络服务,允许一个(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击 **方案一** 如果是通过`vue-cli`脚手架工具搭建项目,我们可以通过`webpack`为我们起一个本地服务器作为请求的代理对象 通过该服务器转发请求至目标服务器,得到结果再转发给前端,但是最终发布上线时如果web应用和接口服务器不在一起仍会跨域 在`vue.config.js`文件,新增以下代码 ```js amodule.exports = { devServer: { host: '127.0.0.1', port: 8084, open: true,// vue项目启动时自动打开浏览器 proxy: { '/api': { // '/api'是代理标识,用于告诉node,url前面是/api的就是使用代理的 target: "http://xxx.xxx.xx.xx:8080", //目标地址,一般是指后台服务器地址 changeOrigin: true, //是否跨域 pathRewrite: { // pathRewrite 的作用是把实际Request Url中的'/api'用""代替 '^/api': "" } } } } } ``` 通过`axios`发送请求中,配置请求的根路径 ```js axios.defaults.baseURL = '/api' ``` **方案二** 此外,还可通过服务端实现代理请求转发 以`express`框架为例 ```js var express = require('express'); const proxy = require('http-proxy-middleware') const app = express() app.use(express.static(__dirname + '/')) app.use('/api', proxy({ target: 'http://localhost:4000', changeOrigin: false })); module.exports = app ``` **方案三** 通过配置`nginx`实现代理 ```js server { listen 80; # server_name www.josephxia.com; location / { root /var/www/html; index index.html index.htm; try_files $uri $uri/ /index.html; } location /api { proxy_pass http://127.0.0.1:3000; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } ``` ================================================ FILE: docs/vue/data.md ================================================ # 面试官:为什么data属性是一个函数而不是一个对象? ![](https://static.vue-js.com/83e51560-3acc-11eb-85f6-6fac77c0c9b3.png) ## 一、实例和组件定义data的区别 `vue`实例的时候定义`data`属性既可以是一个对象,也可以是一个函数 ```js const app = new Vue({ el:"#app", // 对象格式 data:{ foo:"foo" }, // 函数格式 data(){ return { foo:"foo" } } }) ``` 组件中定义`data`属性,只能是一个函数 如果为组件`data`直接定义为一个对象 ```js Vue.component('component1',{ template:`
    组件
    `, data:{ foo:"foo" } }) ``` 则会得到警告信息 ![](https://static.vue-js.com/8e6fc0c0-3acc-11eb-ab90-d9ae814b240d.png) 警告说明:返回的`data`应该是一个函数在每一个组件实例中 ## 二、组件data定义函数与对象的区别 上面讲到组件`data`必须是一个函数,不知道大家有没有思考过这是为什么呢? 在我们定义好一个组件的时候,`vue`最终都会通过`Vue.extend()`构成组件实例 这里我们模仿组件构造函数,定义`data`属性,采用对象的形式 ```js function Component(){ } Component.prototype.data = { count : 0 } ``` 创建两个组件实例 ``` const componentA = new Component() const componentB = new Component() ``` 修改`componentA`组件`data`属性的值,`componentB`中的值也发生了改变 ```js console.log(componentB.data.count) // 0 componentA.data.count = 1 console.log(componentB.data.count) // 1 ``` 产生这样的原因这是两者共用了同一个内存地址,`componentA`修改的内容,同样对`componentB`产生了影响 如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同) ```js function Component(){ this.data = this.data() } Component.prototype.data = function (){ return { count : 0 } } ``` 修改`componentA`组件`data`属性的值,`componentB`中的值不受影响 ```js console.log(componentB.data.count) // 0 componentA.data.count = 1 console.log(componentB.data.count) // 0 ``` `vue`组件可能会有很多个实例,采用函数返回一个全新`data`形式,使每个实例对象的数据不会受到其他实例对象数据的污染 ## 三、原理分析 首先可以看看`vue`初始化`data`的代码,`data`的定义可以是函数也可以是对象 源码位置:`/vue-dev/src/core/instance/state.js` ```js function initData (vm: Component) { let data = vm.$options.data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} ... } ``` `data`既能是`object`也能是`function`,那为什么还会出现上文警告呢? 别急,继续看下文 组件在创建的时候,会进行选项的合并 源码位置:`/vue-dev/src/core/util/options.js` 自定义组件会进入`mergeOptions`进行选项合并 ```js Vue.prototype._init = function (options?: Object) { ... // merge options if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } ... } ``` 定义`data`会进行数据校验 源码位置:`/vue-dev/src/core/instance/init.js` 这时候`vm`实例为`undefined`,进入`if`判断,若`data`类型不是`function`,则出现警告提示 ```js strats.data = function ( parentVal: any, childVal: any, vm?: Component ): ?Function { if (!vm) { if (childVal && typeof childVal !== "function") { process.env.NODE_ENV !== "production" && warn( 'The "data" option should be a function ' + "that returns a per-instance value in component " + "definitions.", vm ); return parentVal; } return mergeDataOrFn(parentVal, childVal); } return mergeDataOrFn(parentVal, childVal, vm); }; ``` ### 四、结论 - 根实例对象`data`可以是对象也可以是函数(根实例是单例),不会产生数据污染情况 - 组件实例对象`data`必须为函数,目的是为了防止多个组件实例对象之间共用一个`data`,产生数据污染。采用函数的形式,`initData`时会将其作为工厂函数都会返回全新`data`对象 ================================================ FILE: docs/vue/data_object_add_attrs.md ================================================ # 面试官:动态给vue的data添加一个新的属性时会发生什么?怎样解决? ![image.png](https://static.vue-js.com/a502dde0-3acc-11eb-ab90-d9ae814b240d.png) ## 一、直接添加属性的问题 我们从一个例子开始 定义一个`p`标签,通过`v-for`指令进行遍历 然后给`botton`标签绑定点击事件,我们预期点击按钮时,数据新增一个属性,界面也 新增一行 ```html

    {{ value }}

    ``` 实例化一个`vue`实例,定义`data`属性和`methods`方法 ```js const app = new Vue({ el:"#app", data:()=>{ item:{ oldProperty:"旧属性" } }, methods:{ addProperty(){ this.items.newProperty = "新属性" // 为items添加新属性 console.log(this.items) // 输出带有newProperty的items } } }) ``` 点击按钮,发现结果不及预期,数据虽然更新了(`console`打印出了新属性),但页面并没有更新 ## 二、原理分析 为什么产生上面的情况呢? 下面来分析一下 `vue2`是用过`Object.defineProperty`实现数据响应式 ```js const obj = {} Object.defineProperty(obj, 'foo', { get() { console.log(`get foo:${val}`); return val }, set(newVal) { if (newVal !== val) { console.log(`set foo:${newVal}`); val = newVal } } }) } ``` 当我们访问`foo`属性或者设置`foo`值的时候都能够触发`setter`与`getter` ```js obj.foo obj.foo = 'new' ``` 但是我们为`obj`添加新属性的时候,却无法触发事件属性的拦截 ```js obj.bar = '新属性' ``` 原因是一开始`obj`的`foo`属性被设成了响应式数据,而`bar`是后面新增的属性,并没有通过`Object.defineProperty`设置成响应式数据 ## 三、解决方案 `Vue` 不允许在已经创建的实例上动态添加新的响应式属性 若想实现数据与视图同步更新,可采取下面三种解决方案: - Vue.set() - Object.assign() - $forcecUpdated() ### Vue.set() Vue.set( target, propertyName/index, value ) 参数 - `{Object | Array} target` - `{string | number} propertyName/index` - `{any} value` 返回值:设置的值 通过`Vue.set`向响应式对象中添加一个`property`,并确保这个新 `property `同样是响应式的,且触发视图更新 关于`Vue.set`源码(省略了很多与本节不相关的代码) 源码位置:`src\core\observer\index.js` ```js function set (target: Array | Object, key: any, val: any): any { ... defineReactive(ob.value, key, val) ob.dep.notify() return val } ``` 这里无非再次调用`defineReactive`方法,实现新增属性的响应式 关于`defineReactive`方法,内部还是通过`Object.defineProperty`实现属性拦截 大致代码如下: ```js function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { console.log(`get ${key}:${val}`); return val }, set(newVal) { if (newVal !== val) { console.log(`set ${key}:${newVal}`); val = newVal } } }) } ``` ### Object.assign() 直接使用`Object.assign()`添加到对象的新属性不会触发更新 应创建一个新的对象,合并原对象和混入对象的属性 ```js this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...}) ``` ### $forceUpdate 如果你发现你自己需要在 `Vue `中做一次强制更新,99.9% 的情况,是你在某个地方做错了事 `$forceUpdate`迫使` Vue` 实例重新渲染 PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。 ### 小结 - 如果为对象添加少量的新属性,可以直接采用`Vue.set()` - 如果需要为新对象添加大量的新属性,则通过`Object.assign()`创建新对象 - 如果你实在不知道怎么操作时,可采取`$forceUpdate()`进行强制刷新 (不建议) PS:`vue3`是用过`proxy`实现数据响应式的,直接动态添加新属性仍可以实现数据响应式 ## 参考文献 - https://cn.vuejs.org/v2/api/#Vue-set - https://vue3js.cn/docs/zh ================================================ FILE: docs/vue/diff.md ================================================ # 面试官:你了解vue的diff算法吗?说说看 ![](https://static.vue-js.com/5e858e30-4585-11eb-85f6-6fac77c0c9b3.png) ## 一、是什么 `diff` 算法是一种通过同层的树节点进行比较的高效算法 其有两个特点: - 比较只会在同层级进行, 不会跨层级比较 - 在diff比较的过程中,循环从两边向中间比较 `diff` 算法在很多场景下都有应用,在 `vue` 中,作用于虚拟 `dom` 渲染成真实 `dom` 的新旧 `VNode` 节点比较 ## 二、比较方式 `diff`整体策略为:深度优先,同层比较 1. 比较只会在同层级进行, 不会跨层级比较 img 2. 比较的过程中,循环从两边向中间收拢 img 下面举个`vue`通过`diff`算法更新的例子: 新旧`VNode`节点如下图所示: ![](https://static001.infoq.cn/resource/image/80/6d/80dc339f73b186479e6d1fc18bfbf66d.png) 第一次循环后,发现旧节点D与新节点D相同,直接复用旧节点D作为`diff`后的第一个真实节点,同时旧节点`endIndex`移动到C,新节点的 `startIndex` 移动到了 C ![](https://static001.infoq.cn/resource/image/76/54/76032c78c8ef74047efd42c070e48854.png) 第二次循环后,同样是旧节点的末尾和新节点的开头(都是 C)相同,同理,`diff` 后创建了 C 的真实节点插入到第一次创建的 D 节点后面。同时旧节点的 `endIndex` 移动到了 B,新节点的 `startIndex` 移动到了 E ![](https://static001.infoq.cn/resource/image/1c/d7/1c76e7489660188d35f0a38ea8c8ecd7.png) 第三次循环中,发现E没有找到,这时候只能直接创建新的真实节点 E,插入到第二次创建的 C 节点之后。同时新节点的 `startIndex` 移动到了 A。旧节点的 `startIndex` 和 `endIndex` 都保持不动 ![](https://static001.infoq.cn/resource/image/4b/08/4b622c0d61673ec5474465d82305d308.png) 第四次循环中,发现了新旧节点的开头(都是 A)相同,于是 `diff` 后创建了 A 的真实节点,插入到前一次创建的 E 节点后面。同时旧节点的 `startIndex` 移动到了 B,新节点的` startIndex` 移动到了 B ![](https://static001.infoq.cn/resource/image/59/b4/5982417c3e0b2fa9ae940354a0e67ab4.png) 第五次循环中,情形同第四次循环一样,因此 `diff` 后创建了 B 真实节点 插入到前一次创建的 A 节点后面。同时旧节点的 `startIndex `移动到了 C,新节点的 startIndex 移动到了 F ![](https://static001.infoq.cn/resource/image/16/86/16cf0ef90f6e19d26c0ddffeca067e86.png) 新节点的 `startIndex` 已经大于 `endIndex` 了,需要创建 `newStartIdx` 和 `newEndIdx` 之间的所有节点,也就是节点F,直接创建 F 节点对应的真实节点放到 B 节点后面 ![](https://static001.infoq.cn/resource/image/dc/ad/dc215b45682cf6c9cc4700a5425673ad.png) ## 三、原理分析 当数据发生改变时,`set`方法会调用`Dep.notify`通知所有订阅者`Watcher`,订阅者就会调用`patch`给真实的`DOM`打补丁,更新相应的视图 源码位置:src/core/vdom/patch.js ```js function patch(oldVnode, vnode, hydrating, removeOnly) { if (isUndef(vnode)) { // 没有新节点,直接执行destory钩子函数 if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] if (isUndef(oldVnode)) { isInitialPatch = true createElm(vnode, insertedVnodeQueue) // 没有旧节点,直接用新节点生成dom元素 } else { const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // 判断旧节点和新节点自身一样,一致执行patchVnode patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { // 否则直接销毁及旧节点,根据新节点生成dom元素 if (isRealElement) { if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } } oldVnode = emptyNodeAt(oldVnode) } return vnode.elm } } } ``` `patch`函数前两个参数位为`oldVnode` 和 `Vnode` ,分别代表新的节点和之前的旧节点,主要做了四个判断: - 没有新节点,直接触发旧节点的`destory`钩子 - 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较了,直接全是新建,所以只调用 `createElm` - 旧节点和新节点自身一样,通过 `sameVnode` 判断节点是否一样,一样时,直接调用 `patchVnode `去处理这两个节点 - 旧节点和新节点自身不一样,当两个节点不一样的时候,直接创建新节点,删除旧节点 下面主要讲的是`patchVnode`部分 ```js function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { // 如果新旧节点一致,什么都不做 if (oldVnode === vnode) { return } // 让vnode.el引用到现在的真实dom,当el修改时,vnode.el会同步变化 const elm = vnode.elm = oldVnode.elm // 异步占位符 if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } // 如果新旧都是静态节点,并且具有相同的key // 当vnode是克隆节点或是v-once指令控制的节点时,只需要把oldVnode.elm和oldVnode.child都复制到vnode上 // 也不用再有其他操作 if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } // 如果vnode不是文本节点或者注释节点 if (isUndef(vnode.text)) { // 并且都有子节点 if (isDef(oldCh) && isDef(ch)) { // 并且子节点不完全一致,则调用updateChildren if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) // 如果只有新的vnode有子节点 } else if (isDef(ch)) { if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // elm已经引用了老的dom节点,在老的dom节点上添加子节点 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) // 如果新vnode没有子节点,而vnode有子节点,直接删除老的oldCh } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1) // 如果老节点是文本节点 } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') } // 如果新vnode和老vnode是文本节点或注释节点 // 但是vnode.text != oldVnode.text时,只需要更新vnode.elm的文本内容就可以 } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } } ``` `patchVnode`主要做了几个判断: - 新节点是否是文本节点,如果是,则直接更新`dom`的文本内容为新节点的文本内容 - 新节点和旧节点如果都有子节点,则处理比较更新子节点 - 只有新节点有子节点,旧节点没有,那么不用比较了,所有节点都是全新的,所以直接全部新建就好了,新建是指创建出所有新`DOM`,并且添加进父节点 - 只有旧节点有子节点而新节点没有,说明更新后的页面,旧节点全部都不见了,那么要做的,就是把所有的旧节点删除,也就是直接把`DOM` 删除 子节点不完全一致,则调用`updateChildren` ```js function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { let oldStartIdx = 0 // 旧头索引 let newStartIdx = 0 // 新头索引 let oldEndIdx = oldCh.length - 1 // 旧尾索引 let newEndIdx = newCh.length - 1 // 新尾索引 let oldStartVnode = oldCh[0] // oldVnode的第一个child let oldEndVnode = oldCh[oldEndIdx] // oldVnode的最后一个child let newStartVnode = newCh[0] // newVnode的第一个child let newEndVnode = newCh[newEndIdx] // newVnode的最后一个child let oldKeyToIdx, idxInOld, vnodeToMove, refElm // removeOnly is a special flag used only by // to ensure removed elements stay in correct relative positions // during leaving transitions const canMove = !removeOnly // 如果oldStartVnode和oldEndVnode重合,并且新的也都重合了,证明diff完了,循环结束 while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 如果oldVnode的第一个child不存在 if (isUndef(oldStartVnode)) { // oldStart索引右移 oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left // 如果oldVnode的最后一个child不存在 } else if (isUndef(oldEndVnode)) { // oldEnd索引左移 oldEndVnode = oldCh[--oldEndIdx] // oldStartVnode和newStartVnode是同一个节点 } else if (sameVnode(oldStartVnode, newStartVnode)) { // patch oldStartVnode和newStartVnode, 索引左移,继续循环 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] // oldEndVnode和newEndVnode是同一个节点 } else if (sameVnode(oldEndVnode, newEndVnode)) { // patch oldEndVnode和newEndVnode,索引右移,继续循环 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] // oldStartVnode和newEndVnode是同一个节点 } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // patch oldStartVnode和newEndVnode patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue) // 如果removeOnly是false,则将oldStartVnode.eml移动到oldEndVnode.elm之后 canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) // oldStart索引右移,newEnd索引左移 oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] // 如果oldEndVnode和newStartVnode是同一个节点 } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // patch oldEndVnode和newStartVnode patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue) // 如果removeOnly是false,则将oldEndVnode.elm移动到oldStartVnode.elm之前 canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) // oldEnd索引左移,newStart索引右移 oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] // 如果都不匹配 } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 尝试在oldChildren中寻找和newStartVnode的具有相同的key的Vnode idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 如果未找到,说明newStartVnode是一个新的节点 if (isUndef(idxInOld)) { // New element // 创建一个新Vnode createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) // 如果找到了和newStartVnodej具有相同的key的Vnode,叫vnodeToMove } else { vnodeToMove = oldCh[idxInOld] /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !vnodeToMove) { warn( 'It seems there are duplicate keys that is causing an update error. ' + 'Make sure each v-for item has a unique key.' ) } // 比较两个具有相同的key的新节点是否是同一个节点 //不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,所以为节点设置key可以更高效的利用dom。 if (sameVnode(vnodeToMove, newStartVnode)) { // patch vnodeToMove和newStartVnode patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue) // 清除 oldCh[idxInOld] = undefined // 如果removeOnly是false,则将找到的和newStartVnodej具有相同的key的Vnode,叫vnodeToMove.elm // 移动到oldStartVnode.elm之前 canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) // 如果key相同,但是节点不相同,则创建一个新的节点 } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm) } } // 右移 newStartVnode = newCh[++newStartIdx] } } ``` `while`循环主要处理了以下五种情景: - 当新老 `VNode` 节点的 `start` 相同时,直接 `patchVnode` ,同时新老 `VNode` 节点的开始索引都加 1 - 当新老 `VNode` 节点的 `end`相同时,同样直接 `patchVnode` ,同时新老 `VNode` 节点的结束索引都减 1 - 当老 `VNode` 节点的 `start` 和新 `VNode` 节点的 `end` 相同时,这时候在 `patchVnode` 后,还需要将当前真实 `dom` 节点移动到 `oldEndVnode` 的后面,同时老 `VNode` 节点开始索引加 1,新 `VNode` 节点的结束索引减 1 - 当老 `VNode` 节点的 `end` 和新 `VNode` 节点的 `start` 相同时,这时候在 `patchVnode` 后,还需要将当前真实 `dom` 节点移动到 `oldStartVnode` 的前面,同时老 `VNode` 节点结束索引减 1,新 `VNode` 节点的开始索引加 1 - 如果都不满足以上四种情形,那说明没有相同的节点可以复用,则会分为以下两种情况: - 从旧的 `VNode` 为 `key` 值,对应 `index` 序列为 `value` 值的哈希表中找到与 `newStartVnode` 一致 `key` 的旧的 `VNode` 节点,再进行`patchVnode `,同时将这个真实 `dom `移动到 `oldStartVnode` 对应的真实 `dom` 的前面 - 调用 `createElm` 创建一个新的 `dom` 节点放到当前 `newStartIdx` 的位置 ### 小结 - 当数据发生改变时,订阅者`watcher`就会调用`patch`给真实的`DOM`打补丁 - 通过`isSameVnode`进行判断,相同则调用`patchVnode`方法 - `patchVnode`做了以下操作: - 找到对应的真实`dom`,称为`el` - 如果都有都有文本节点且不相等,将`el`文本节点设置为`Vnode`的文本节点 - 如果`oldVnode`有子节点而`VNode`没有,则删除`el`子节点 - 如果`oldVnode`没有子节点而`VNode`有,则将`VNode`的子节点真实化后添加到`el` - 如果两者都有子节点,则执行`updateChildren`函数比较子节点 - `updateChildren`主要做了以下操作: - 设置新旧`VNode`的头尾指针 - 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用`patchVnode`进行`patch`重复流程、调用`createElem`创建一个新节点,从哈希表寻找 `key`一致的`VNode` 节点再分情况操作 ## 参考文献 - https://juejin.cn/post/6881907432541552648#heading-1 - https://www.infoq.cn/article/udlcpkh4iqb0cr5wgy7f ================================================ FILE: docs/vue/directive.md ================================================ # 面试官:你有写过自定义指令吗?自定义指令的应用场景有哪些? ![](https://static.vue-js.com/bd85a970-4345-11eb-85f6-6fac77c0c9b3.png) ## 一、什么是指令 开始之前我们先学习一下指令系统这个词 **指令系统**是计算机硬件的语言系统,也叫机器语言,它是系统程序员看到的计算机的主要属性。因此指令系统表征了计算机的基本功能决定了机器所要求的能力 在`vue`中提供了一套为数据驱动视图更为方便的操作,这些操作被称为指令系统 我们看到的`v- `开头的行内属性,都是指令,不同的指令可以完成或实现不同的功能 除了核心功能默认内置的指令 (`v-model` 和 `v-show`),`Vue` 也允许注册自定义指令 指令使用的几种方式: ```js //会实例化一个指令,但这个指令没有参数 `v-xxx` // -- 将值传到指令中 `v-xxx="value"` // -- 将字符串传入到指令中,如`v-html="'

    内容

    '"` `v-xxx="'string'"` // -- 传参数(`arg`),如`v-bind:class="className"` `v-xxx:arg="value"` // -- 使用修饰符(`modifier`) `v-xxx:arg.modifier="value"` ``` ### 二、如何实现 注册一个自定义指令有全局注册与局部注册 全局注册主要是通过`Vue.directive`方法进行注册 `Vue.directive`第一个参数是指令的名字(不需要写上`v-`前缀),第二个参数可以是对象数据,也可以是一个指令函数 ```js // 注册一个全局自定义指令 `v-focus` Vue.directive('focus', { // 当被绑定的元素插入到 DOM 中时…… inserted: function (el) { // 聚焦元素 el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能 } }) ``` 局部注册通过在组件`options`选项中设置`directive`属性 ```js directives: { focus: { // 指令的定义 inserted: function (el) { el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能 } } } ``` 然后你可以在模板中任何元素上使用新的 `v-focus` property,如下: ```js ``` 自定义指令也像组件那样存在钩子函数: - `bind`:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置 - `inserted`:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中) - `update`:所在组件的 `VNode` 更新时调用,但是可能发生在其子 `VNode` 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 - `componentUpdated`:指令所在组件的 `VNode` 及其子 `VNode` 全部更新后调用 - `unbind`:只调用一次,指令与元素解绑时调用 所有的钩子函数的参数都有以下: - `el`:指令所绑定的元素,可以用来直接操作 `DOM` - `binding`:一个对象,包含以下 `property`: - `name`:指令名,不包括 `v-` 前缀。 - `value`:指令的绑定值,例如:`v-my-directive="1 + 1"` 中,绑定值为 `2`。 - `oldValue`:指令绑定的前一个值,仅在 `update` 和 `componentUpdated` 钩子中可用。无论值是否改变都可用。 - `expression`:字符串形式的指令表达式。例如 `v-my-directive="1 + 1"` 中,表达式为 `"1 + 1"`。 - `arg`:传给指令的参数,可选。例如 `v-my-directive:foo` 中,参数为 `"foo"`。 - `modifiers`:一个包含修饰符的对象。例如:`v-my-directive.foo.bar` 中,修饰符对象为 `{ foo: true, bar: true }` - `vnode`:`Vue` 编译生成的虚拟节点 - `oldVnode`:上一个虚拟节点,仅在 `update` 和 `componentUpdated` 钩子中可用 > 除了 `el` 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 `dataset` 来进行 举个例子: ```html
    ``` ## 三、应用场景 使用自定义指令可以满足我们日常一些场景,这里给出几个自定义指令的案例: - 表单防止重复提交 - 图片懒加载 - 一键 Copy的功能 ### 表单防止重复提交 表单防止重复提交这种情况设置一个`v-throttle`自定义指令来实现 举个例子: ```js // 1.设置v-throttle自定义指令 Vue.directive('throttle', { bind: (el, binding) => { let throttleTime = binding.value; // 节流时间 if (!throttleTime) { // 用户若不设置节流时间,则默认2s throttleTime = 2000; } let cbFun; el.addEventListener('click', event => { if (!cbFun) { // 第一次执行 cbFun = setTimeout(() => { cbFun = null; }, throttleTime); } else { event && event.stopImmediatePropagation(); } }, true); }, }); // 2.为button标签设置v-throttle自定义指令 ``` ### 图片懒加载 设置一个`v-lazy`自定义指令完成图片懒加载 ```js const LazyLoad = { // install方法 install(Vue,options){ // 代替图片的loading图 let defaultSrc = options.default; Vue.directive('lazy',{ bind(el,binding){ LazyLoad.init(el,binding.value,defaultSrc); }, inserted(el){ // 兼容处理 if('IntersectionObserver' in window){ LazyLoad.observe(el); }else{ LazyLoad.listenerScroll(el); } }, }) }, // 初始化 init(el,val,def){ // data-src 储存真实src el.setAttribute('data-src',val); // 设置src为loading图 el.setAttribute('src',def); }, // 利用IntersectionObserver监听el observe(el){ let io = new IntersectionObserver(entries => { let realSrc = el.dataset.src; if(entries[0].isIntersecting){ if(realSrc){ el.src = realSrc; el.removeAttribute('data-src'); } } }); io.observe(el); }, // 监听scroll事件 listenerScroll(el){ let handler = LazyLoad.throttle(LazyLoad.load,300); LazyLoad.load(el); window.addEventListener('scroll',() => { handler(el); }); }, // 加载真实图片 load(el){ let windowHeight = document.documentElement.clientHeight let elTop = el.getBoundingClientRect().top; let elBtm = el.getBoundingClientRect().bottom; let realSrc = el.dataset.src; if(elTop - windowHeight<0&&elBtm > 0){ if(realSrc){ el.src = realSrc; el.removeAttribute('data-src'); } } }, // 节流 throttle(fn,delay){ let timer; let prevTime; return function(...args){ let currTime = Date.now(); let context = this; if(!prevTime) prevTime = currTime; clearTimeout(timer); if(currTime - prevTime > delay){ prevTime = currTime; fn.apply(context,args); clearTimeout(timer); return; } timer = setTimeout(function(){ prevTime = Date.now(); timer = null; fn.apply(context,args); },delay); } } } export default LazyLoad; ``` ### 一键 Copy的功能 ```js import { Message } from 'ant-design-vue'; const vCopy = { // /* bind 钩子函数,第一次绑定时调用,可以在这里做初始化设置 el: 作用的 dom 对象 value: 传给指令的值,也就是我们要 copy 的值 */ bind(el, { value }) { el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到 el.handler = () => { if (!el.$value) { // 值为空的时候,给出提示,我这里的提示是用的 ant-design-vue 的提示,你们随意 Message.warning('无复制内容'); return; } // 动态创建 textarea 标签 const textarea = document.createElement('textarea'); // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域 textarea.readOnly = 'readonly'; textarea.style.position = 'absolute'; textarea.style.left = '-9999px'; // 将要 copy 的值赋给 textarea 标签的 value 属性 textarea.value = el.$value; // 将 textarea 插入到 body 中 document.body.appendChild(textarea); // 选中值并复制 textarea.select(); // textarea.setSelectionRange(0, textarea.value.length); const result = document.execCommand('Copy'); if (result) { Message.success('复制成功'); } document.body.removeChild(textarea); }; // 绑定点击事件,就是所谓的一键 copy 啦 el.addEventListener('click', el.handler); }, // 当传进来的值更新的时候触发 componentUpdated(el, { value }) { el.$value = value; }, // 指令与元素解绑的时候,移除事件绑定 unbind(el) { el.removeEventListener('click', el.handler); }, }; export default vCopy; ``` 关于自定义指令还有很多应用场景,如:拖拽指令、页面水印、权限校验等等应用场景 ## 参考文献 - https://vue3js.cn/docs/zh - https://juejin.cn/post/6844904197448531975#heading-5 - https://www.cnblogs.com/chenwenhao/p/11924161.html#_label2 ================================================ FILE: docs/vue/error.md ================================================ # 面试官:你是怎么处理vue项目中的错误的? ![](https://static.vue-js.com/3cafe4f0-4fd9-11eb-ab90-d9ae814b240d.png) ## 一、错误类型 任何一个框架,对于错误的处理都是一种必备的能力 在`Vue` 中,则是定义了一套对应的错误处理规则给到使用者,且在源代码级别,对部分必要的过程做了一定的错误处理。 主要的错误来源包括: - 后端接口错误 - 代码中本身逻辑错误 ## 二、如何处理 ### 后端接口错误 通过`axios`的`interceptor`实现网络请求的`response`先进行一层拦截 ```js apiClient.interceptors.response.use( response => { return response; }, error => { if (error.response.status == 401) { router.push({ name: "Login" }); } else { message.error("出错了"); return Promise.reject(error); } } ); ``` ### 代码逻辑问题 #### 全局设置错误处理 设置全局错误处理函数 ```js Vue.config.errorHandler = function (err, vm, info) { // handle error // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子 // 只在 2.2.0+ 可用 } ``` `errorHandler`指定组件的渲染和观察期间未捕获错误的处理函数。这个处理函数被调用时,可获取错误信息和 `Vue` 实例 不过值得注意的是,在不同` Vue` 版本中,该全局 `API` 作用的范围会有所不同: > 从 2.2.0 起,这个钩子也会捕获组件生命周期钩子里的错误。同样的,当这个钩子是 `undefined` 时,被捕获的错误会通过 `console.error` 输出而避免应用崩 > 从 2.4.0 起,这个钩子也会捕获 Vue 自定义事件处理函数内部的错误了 > 从 2.6.0 起,这个钩子也会捕获 `v-on` DOM 监听器内部抛出的错误。另外,如果任何被覆盖的钩子或处理函数返回一个 Promise 链 (例如 async 函数),则来自其 Promise 链的错误也会被处理 #### 生命周期钩子 `errorCaptured`是 2.5.0 新增的一个生命钩子函数,当捕获到一个来自子孙组件的错误时被调用 基本类型 ```js (err: Error, vm: Component, info: string) => ?boolean ``` 此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 `false` 以阻止该错误继续向上传播 参考官网,错误传播规则如下: - 默认情况下,如果全局的 `config.errorHandler` 被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报 - 如果一个组件的继承或父级从属链路中存在多个 `errorCaptured` 钩子,则它们将会被相同的错误逐个唤起。 - 如果此 `errorCaptured` 钩子自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局的 `config.errorHandler` - 一个 `errorCaptured` 钩子能够返回 `false` 以阻止错误继续向上传播。本质上是说“这个错误已经被搞定了且应该被忽略”。它会阻止其它任何会被这个错误唤起的 `errorCaptured` 钩子和全局的 `config.errorHandler` 下面来看个例子 定义一个父组件`cat` ```js Vue.component('cat', { template:`

    Cat:

    `, props:{ name:{ required:true, type:String } }, errorCaptured(err,vm,info) { console.log(`cat EC: ${err.toString()}\ninfo: ${info}`); return false; } }); ``` 定义一个子组件`kitten`,其中`dontexist()`并没有定义,存在错误 ```js Vue.component('kitten', { template:'

    Kitten: {{ dontexist() }}

    ', props:{ name:{ required:true, type:String } } }); ``` 页面中使用组件 ```html
    ``` 在父组件的`errorCaptured`则能够捕获到信息 ```js cat EC: TypeError: dontexist is not a function info: render ``` ### 三、源码分析 异常处理源码 源码位置:/src/core/util/error.js ```js // Vue 全局配置,也就是上面的Vue.config import config from '../config' import { warn } from './debug' // 判断环境 import { inBrowser, inWeex } from './env' // 判断是否是Promise,通过val.then === 'function' && val.catch === 'function', val !=== null && val !== undefined import { isPromise } from 'shared/util' // 当错误函数处理错误时,停用deps跟踪以避免可能出现的infinite rendering // 解决以下出现的问题https://github.com/vuejs/vuex/issues/1505的问题 import { pushTarget, popTarget } from '../observer/dep' export function handleError (err: Error, vm: any, info: string) { // Deactivate deps tracking while processing error handler to avoid possible infinite rendering. pushTarget() try { // vm指当前报错的组件实例 if (vm) { let cur = vm // 首先获取到报错的组件,之后递归查找当前组件的父组件,依次调用errorCaptured 方法。 // 在遍历调用完所有 errorCaptured 方法、或 errorCaptured 方法有报错时,调用 globalHandleError 方法 while ((cur = cur.$parent)) { const hooks = cur.$options.errorCaptured // 判断是否存在errorCaptured钩子函数 if (hooks) { // 选项合并的策略,钩子函数会被保存在一个数组中 for (let i = 0; i < hooks.length; i++) { // 如果errorCaptured 钩子执行自身抛出了错误, // 则用try{}catch{}捕获错误,将这个新错误和原本被捕获的错误都会发送给全局的config.errorHandler // 调用globalHandleError方法 try { // 当前errorCaptured执行,根据返回是否是false值 // 是false,capture = true,阻止其它任何会被这个错误唤起的 errorCaptured 钩子和全局的 config.errorHandler // 是true capture = fale,组件的继承或父级从属链路中存在的多个 errorCaptured 钩子,会被相同的错误逐个唤起 // 调用对应的钩子函数,处理错误 const capture = hooks[i].call(cur, err, vm, info) === false if (capture) return } catch (e) { globalHandleError(e, cur, 'errorCaptured hook') } } } } } // 除非禁止错误向上传播,否则都会调用全局的错误处理函数 globalHandleError(err, vm, info) } finally { popTarget() } } // 异步错误处理函数 export function invokeWithErrorHandling ( handler: Function, context: any, args: null | any[], vm: any, info: string ) { let res try { // 根据参数选择不同的handle执行方式 res = args ? handler.apply(context, args) : handler.call(context) // handle返回结果存在 // res._isVue an flag to avoid this being observed,如果传入值的_isVue为ture时(即传入的值是Vue实例本身)不会新建observer实例 // isPromise(res) 判断val.then === 'function' && val.catch === 'function', val !=== null && val !== undefined // !res._handled _handle是Promise 实例的内部变量之一,默认是false,代表onFulfilled,onRejected是否被处理 if (res && !res._isVue && isPromise(res) && !res._handled) { res.catch(e => handleError(e, vm, info + ` (Promise/async)`)) // avoid catch triggering multiple times when nested calls // 避免嵌套调用时catch多次的触发 res._handled = true } } catch (e) { // 处理执行错误 handleError(e, vm, info) } return res } //全局错误处理 function globalHandleError (err, vm, info) { // 获取全局配置,判断是否设置处理函数,默认undefined // 已配置 if (config.errorHandler) { // try{}catch{} 住全局错误处理函数 try { // 执行设置的全局错误处理函数,handle error 想干啥就干啥💗 return config.errorHandler.call(null, err, vm, info) } catch (e) { // 如果开发者在errorHandler函数中手动抛出同样错误信息throw err // 判断err信息是否相等,避免log两次 // 如果抛出新的错误信息throw err Error('你好毒'),将会一起log输出 if (e !== err) { logError(e, null, 'config.errorHandler') } } } // 未配置常规log输出 logError(err, vm, info) } // 错误输出函数 function logError (err, vm, info) { if (process.env.NODE_ENV !== 'production') { warn(`Error in ${info}: "${err.toString()}"`, vm) } /* istanbul ignore else */ if ((inBrowser || inWeex) && typeof console !== 'undefined') { console.error(err) } else { throw err } } ``` ### 小结 - `handleError`在需要捕获异常的地方调用,首先获取到报错的组件,之后递归查找当前组件的父组件,依次调用`errorCaptured` 方法,在遍历调用完所有 `errorCaptured` 方法或 `errorCaptured` 方法有报错时,调用 `globalHandleError` 方法 - `globalHandleError `调用全局的 `errorHandler` 方法,再通过`logError`判断环境输出错误信息 - `invokeWithErrorHandling`更好的处理异步错误信息 - `logError`判断环境,选择不同的抛错方式。非生产环境下,调用`warn`方法处理错误 ## 参考文献 - https://juejin.cn/post/6844904096936230925 - https://segmentfault.com/a/1190000018606181 ================================================ FILE: docs/vue/filter.md ================================================ # 面试官:Vue中的过滤器了解吗?过滤器的应用场景有哪些? ![](https://static.vue-js.com/fe68eea0-440f-11eb-ab90-d9ae814b240d.png) ## 一、是什么 过滤器(`filter`)是输送介质管道上不可缺少的一种装置 大白话,就是把一些不必要的东西过滤掉 过滤器实质不改变原始数据,只是对数据进行加工处理后返回过滤后的数据再进行调用处理,我们也可以理解其为一个纯函数 `Vue` 允许你自定义过滤器,可被用于一些常见的文本格式化 ps: `Vue3`中已废弃`filter` ## 二、如何用 `vue`中的过滤器可以用在两个地方:双花括号插值和 `v-bind` 表达式,过滤器应该被添加在 `JavaScript `表达式的尾部,由“管道”符号指示: ```js {{ message | capitalize }}
    ``` ### 定义filter 在组件的选项中定义本地的过滤器 ```js filters: { capitalize: function (value) { if (!value) return '' value = value.toString() return value.charAt(0).toUpperCase() + value.slice(1) } } ``` 定义全局过滤器: ```js Vue.filter('capitalize', function (value) { if (!value) return '' value = value.toString() return value.charAt(0).toUpperCase() + value.slice(1) }) new Vue({ // ... }) ``` 注意:当全局过滤器和局部过滤器重名时,会采用局部过滤器 过滤器函数总接收表达式的值 (之前的操作链的结果) 作为第一个参数。在上述例子中,`capitalize` 过滤器函数将会收到 `message` 的值作为第一个参数 过滤器可以串联: ``` {{ message | filterA | filterB }} ``` 在这个例子中,`filterA` 被定义为接收单个参数的过滤器函数,表达式 `message` 的值将作为参数传入到函数中。然后继续调用同样被定义为接收单个参数的过滤器函数 `filterB`,将 `filterA` 的结果传递到 `filterB` 中。 过滤器是 `JavaScript `函数,因此可以接收参数: ``` {{ message | filterA('arg1', arg2) }} ``` 这里,`filterA` 被定义为接收三个参数的过滤器函数。 其中 `message` 的值作为第一个参数,普通字符串 `'arg1'` 作为第二个参数,表达式 `arg2` 的值作为第三个参数 举个例子: ```html

    {{ msg | msgFormat('疯狂','--')}}

    ``` ### 小结: - 部过滤器优先于全局过滤器被调用 - 一个表达式可以使用多个过滤器。过滤器之间需要用管道符“|”隔开。其执行顺序从左往右 ## 三、应用场景 平时开发中,需要用到过滤器的地方有很多,比如单位转换、数字打点、文本格式化、时间格式化之类的等 比如我们要实现将30000 => 30,000,这时候我们就需要使用过滤器 ```js Vue.filter('toThousandFilter', function (value) { if (!value) return '' value = value.toString() return .replace(str.indexOf('.') > -1 ? /(\d)(?=(\d{3})+\.)/g : /(\d)(?=(?:\d{3})+$)/g, '$1,') }) ``` ## 四、原理分析 使用过滤器 ```js {{ message | capitalize }} ``` 在模板编译阶段过滤器表达式将会被编译为过滤器函数,主要是用过`parseFilters`,我们放到最后讲 ```js _s(_f('filterFormat')(message)) ``` 首先分析一下`_f`: _f 函数全名是:`resolveFilter`,这个函数的作用是从`this.$options.filters`中找出注册的过滤器并返回 ```js // 变为 this.$options.filters['filterFormat'](message) // message为参数 ``` 关于`resolveFilter` ```js import { indentity,resolveAsset } from 'core/util/index' export function resolveFilter(id){ return resolveAsset(this.$options,'filters',id,true) || identity } ``` 内部直接调用`resolveAsset`,将`option`对象,类型,过滤器`id`,以及一个触发警告的标志作为参数传递,如果找到,则返回过滤器; `resolveAsset`的代码如下: ```js export function resolveAsset(options,type,id,warnMissing){ // 因为我们找的是过滤器,所以在 resolveFilter函数中调用时 type 的值直接给的 'filters',实际这个函数还可以拿到其他很多东西 if(typeof id !== 'string'){ // 判断传递的过滤器id 是不是字符串,不是则直接返回 return } const assets = options[type] // 将我们注册的所有过滤器保存在变量中 // 接下来的逻辑便是判断id是否在assets中存在,即进行匹配 if(hasOwn(assets,id)) return assets[id] // 如找到,直接返回过滤器 // 没有找到,代码继续执行 const camelizedId = camelize(id) // 万一你是驼峰的呢 if(hasOwn(assets,camelizedId)) return assets[camelizedId] // 没找到,继续执行 const PascalCaseId = capitalize(camelizedId) // 万一你是首字母大写的驼峰呢 if(hasOwn(assets,PascalCaseId)) return assets[PascalCaseId] // 如果还是没找到,则检查原型链(即访问属性) const result = assets[id] || assets[camelizedId] || assets[PascalCaseId] // 如果依然没找到,则在非生产环境的控制台打印警告 if(process.env.NODE_ENV !== 'production' && warnMissing && !result){ warn('Failed to resolve ' + type.slice(0,-1) + ': ' + id, options) } // 无论是否找到,都返回查找结果 return result } ``` 下面再来分析一下`_s`: `_s` 函数的全称是 `toString`,过滤器处理后的结果会当作参数传递给 `toString`函数,最终 `toString`函数执行后的结果会保存到`Vnode`中的text属性中,渲染到视图中 ```js function toString(value){ return value == null ? '' : typeof value === 'object' ? JSON.stringify(value,null,2)// JSON.stringify()第三个参数可用来控制字符串里面的间距 : String(value) } ``` 最后,在分析下`parseFilters`,在模板编译阶段使用该函数阶段将模板过滤器解析为过滤器函数调用表达式 ```js function parseFilters (filter) { let filters = filter.split('|') let expression = filters.shift().trim() // shift()删除数组第一个元素并将其返回,该方法会更改原数组 let i if (filters) { for(i = 0;i < filters.length;i++){ experssion = warpFilter(expression,filters[i].trim()) // 这里传进去的expression实际上是管道符号前面的字符串,即过滤器的第一个参数 } } return expression } // warpFilter函数实现 function warpFilter(exp,filter){ // 首先判断过滤器是否有其他参数 const i = filter.indexof('(') if(i<0){ // 不含其他参数,直接进行过滤器表达式字符串的拼接 return `_f("${filter}")(${exp})` }else{ const name = filter.slice(0,i) // 过滤器名称 const args = filter.slice(i+1) // 参数,但还多了 ‘)’ return `_f('${name}')(${exp},${args}` // 注意这一步少给了一个 ')' } } ``` ### 小结: - 在编译阶段通过`parseFilters`将过滤器编译成函数调用(串联过滤器则是一个嵌套的函数调用,前一个过滤器执行的结果是后一个过滤器函数的参数) - 编译后通过调用`resolveFilter`函数找到对应过滤器并返回结果 - 执行结果作为参数传递给`toString`函数,而`toString`执行后,其结果会保存在`Vnode`的`text`属性中,渲染到视图 ## 参考文献 - https://cn.vuejs.org/v2/guide/filters.html#ad - https://blog.csdn.net/weixin_42724176/article/details/105546684 - https://vue3js.cn ================================================ FILE: docs/vue/first_page_time.md ================================================ # 面试官:SPA首屏加载速度慢的怎么解决? ![image.png](https://static.vue-js.com/24617c00-3acc-11eb-ab90-d9ae814b240d.png) ## 一、什么是首屏加载 首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容 首屏加载可以说是用户体验中**最重要**的环节 ### 关于计算首屏时间 利用`performance.timing`提供的数据: ![image.png](https://static.vue-js.com/2e2491a0-3acc-11eb-85f6-6fac77c0c9b3.png) 通过`DOMContentLoad`或者`performance`来计算出首屏时间 ```js // 方案一: document.addEventListener('DOMContentLoaded', (event) => { console.log('first contentful painting'); }); // 方案二: performance.getEntriesByName("first-contentful-paint")[0].startTime // performance.getEntriesByName("first-contentful-paint")[0] // 会返回一个 PerformancePaintTiming的实例,结构如下: { name: "first-contentful-paint", entryType: "paint", startTime: 507.80000002123415, duration: 0, }; ``` ## 二、加载慢的原因 在页面渲染的过程,导致加载速度慢的因素可能如下: - 网络延时问题 - 资源文件体积是否过大 - 资源是否重复发送请求去加载了 - 加载脚本的时候,渲染内容堵塞了 ## 三、解决方案 常见的几种SPA首屏优化方式 - 减小入口文件积 - 静态资源本地缓存 - UI框架按需加载 - 图片资源的压缩 - 组件重复打包 - 开启GZip压缩 - 使用SSR ### 减小入口文件体积 常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加 ![image.png](https://static.vue-js.com/486cee90-3acc-11eb-ab90-d9ae814b240d.png) 在`vue-router`配置路由的时候,采用动态加载路由的形式 ```js routes:[ path: 'Blogs', name: 'ShowBlogs', component: () => import('./components/ShowBlogs.vue') ] ``` 以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件 ### 静态资源本地缓存 后端返回资源问题: - 采用`HTTP`缓存,设置`Cache-Control`,`Last-Modified`,`Etag`等响应头 - 采用`Service Worker`离线缓存 前端合理利用`localStorage` ### UI框架按需加载 在日常使用`UI`框架,例如`element-UI`、或者`antd`,我们经常性直接引用整个`UI`库 ```js import ElementUI from 'element-ui' Vue.use(ElementUI) ``` 但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用 ```js import { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui'; Vue.use(Button) Vue.use(Input) Vue.use(Pagination) ``` ### 组件重复打包 假设`A.js`文件是一个常用的库,现在有多个路由使用了`A.js`文件,这就造成了重复下载 解决方案:在`webpack`的`config`文件中,修改`CommonsChunkPlugin`的配置 ```js minChunks: 3 ``` `minChunks`为3表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件 ### 图片资源的压缩 图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素 对于所有的图片资源,我们可以进行适当的压缩 对页面上使用到的`icon`,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻`http`请求压力。 ### 开启GZip压缩 拆完包之后,我们再用`gzip`做一下压缩 安装`compression-webpack-plugin` ```js cnmp i compression-webpack-plugin -D ``` 在`vue.congig.js`中引入并修改`webpack`配置 ```js const CompressionPlugin = require('compression-webpack-plugin') configureWebpack: (config) => { if (process.env.NODE_ENV === 'production') { // 为生产环境修改配置... config.mode = 'production' return { plugins: [new CompressionPlugin({ test: /\.js$|\.html$|\.css/, //匹配文件名 threshold: 10240, //对超过10k的数据进行压缩 deleteOriginalAssets: false //是否删除原文件 })] } } ``` 在服务器我们也要做相应的配置 如果发送请求的浏览器支持`gzip`,就发送给它`gzip`格式的文件 我的服务器是用`express`框架搭建的 只要安装一下`compression`就能使用 ``` const compression = require('compression') app.use(compression()) // 在其他中间件使用之前调用 ``` ### 使用SSR SSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器 从头搭建一个服务端渲染是很复杂的,`vue`应用建议使用`Nuxt.js`实现服务端渲染 ### 小结: 减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化 和 页面渲染优化 下图是更为全面的首屏优化的方案 ![image.png](https://static.vue-js.com/4fafe900-3acc-11eb-85f6-6fac77c0c9b3.png) 大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化 ## 参考文献 - https://zhuanlan.zhihu.com/p/88639980?utm_source=wechat_session - https://www.chengrang.com/how-browsers-work.html - https://juejin.cn/post/6844904185264095246 - https://vue3js.cn/docs/zh ================================================ FILE: docs/vue/if_for.md ================================================ # 面试官:v-if和v-for的优先级是什么? ![](https://static.vue-js.com/e8764810-3acb-11eb-85f6-6fac77c0c9b3.png) ## 一、作用 `v-if` 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 `true`值的时候被渲染 `v-for` 指令基于一个数组来渲染一个列表。`v-for` 指令需要使用 `item in items` 形式的特殊语法,其中 `items` 是源数据数组或者对象,而 `item` 则是被迭代的数组元素的别名 在 `v-for` 的时候,建议设置`key`值,并且保证每个`key`值是独一无二的,这便于`diff`算法进行优化 两者在用法上 ```js
  • {{ item.label }}
  • ``` ## 二、优先级 `v-if`与`v-for`都是`vue`模板系统中的指令 在`vue`模板编译的时候,会将指令系统转化成可执行的`render`函数 ### 示例 编写一个`p`标签,同时使用`v-if`与 `v-for` ```html

    {{ item.title }}

    ``` 创建`vue`实例,存放`isShow`与`items`数据 ```js const app = new Vue({ el: "#app", data() { return { items: [ { title: "foo" }, { title: "baz" }] } }, computed: { isShow() { return this.items && this.items.length > 0 } } }) ``` 模板指令的代码都会生成在`render`函数中,通过`app.$options.render`就能得到渲染函数 ```js ƒ anonymous() { with (this) { return _c('div', { attrs: { "id": "app" } }, _l((items), function (item) { return (isShow) ? _c('p', [_v("\n" + _s(item.title) + "\n")]) : _e() }), 0) } } ``` `_l`是`vue`的列表渲染函数,函数内部都会进行一次`if`判断 初步得到结论:`v-for`优先级是比`v-if`高 再将`v-for`与`v-if`置于不同标签 ```html
    ``` 再输出下`render`函数 ```js ƒ anonymous() { with(this){return _c('div',{attrs:{"id":"app"}}, [(isShow)?[_v("\n"), _l((items),function(item){return _c('p',[_v(_s(item.title))])})]:_e()],2)} } ``` 这时候我们可以看到,`v-for`与`v-if`作用在不同标签时候,是先进行判断,再进行列表的渲染 我们再在查看下`vue`源码 源码位置:` \vue-dev\src\compiler\codegen\index.js` ```js export function genElement (el: ASTElement, state: CodegenState): string { if (el.parent) { el.pre = el.pre || el.parent.pre } if (el.staticRoot && !el.staticProcessed) { return genStatic(el, state) } else if (el.once && !el.onceProcessed) { return genOnce(el, state) } else if (el.for && !el.forProcessed) { return genFor(el, state) } else if (el.if && !el.ifProcessed) { return genIf(el, state) } else if (el.tag === 'template' && !el.slotTarget && !state.pre) { return genChildren(el, state) || 'void 0' } else if (el.tag === 'slot') { return genSlot(el, state) } else { // component or element ... } ``` 在进行`if`判断的时候,`v-for`是比`v-if`先进行判断 最终结论:`v-for`优先级比`v-if`高 ## 三、注意事项 1. 永远不要把 `v-if` 和 `v-for` 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断) 2. 如果避免出现这种情况,则在外层嵌套`template`(页面渲染不生成`dom`节点),在这一层进行v-if判断,然后在内部进行v-for循环 ```js ``` 3. 如果条件出现在循环内部,可通过计算属性`computed`提前过滤掉那些不需要显示的项 ```js computed: { items: function() { return this.list.filter(function (item) { return item.isShow }) } } ``` ================================================ FILE: docs/vue/keepalive.md ================================================ # 面试官:说说你对keep-alive的理解是什么? ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9nSDMxdUY5VklpYlRaSXdpY3ZmUkR3STRiamRBVGlhVEpFZDNzamRoeTd3MDlVM0k5ZERjNUVVSUNFVk1WSVE2aDFYMjVpY1NRT3lraWFwWEpEUFM0VGJST0l3LzY0MA?x-oss-process=image/format,png) ## 一、Keep-alive 是什么 `keep-alive`是`vue`中的内置组件,能在组件切换过程中将状态保留在内存中,防止重复渲染`DOM` `keep-alive` 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们 `keep-alive`可以设置以下`props`属性: - `include` \- 字符串或正则表达式。只有名称匹配的组件会被缓存 - `exclude` \- 字符串或正则表达式。任何名称匹配的组件都不会被缓存 - `max` \- 数字。最多可以缓存多少组件实例 关于`keep-alive`的基本用法: ```go ``` 使用`includes`和`exclude`: ```go ``` 匹配首先检查组件自身的 `name` 选项,如果 `name` 选项不可用,则匹配它的局部注册名称 \(父组件 `components` 选项的键值\),匿名组件不能被匹配 设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(`activated`与`deactivated`): - 首次进入组件时:`beforeRouteEnter` > `beforeCreate` > `created`\> `mounted` > `activated` > ... ... > `beforeRouteLeave` > `deactivated` - 再次进入组件时:`beforeRouteEnter` >`activated` > ... ... > `beforeRouteLeave` > `deactivated` ## 二、使用场景 使用原则:当我们在某些场景下不需要让页面重新加载时我们可以使用`keepalive` 举个栗子: 当我们从`首页`–>`列表页`–>`商详页`–>`再返回`,这时候列表页应该是需要`keep-alive` 从`首页`–>`列表页`–>`商详页`–>`返回到列表页(需要缓存)`–>`返回到首页(需要缓存)`–>`再次进入列表页(不需要缓存)`,这时候可以按需来控制页面的`keep-alive` 在路由中设置`keepAlive`属性判断是否需要缓存 ```go { path: 'list', name: 'itemList', // 列表页 component (resolve) { require(['@/pages/item/list'], resolve) }, meta: { keepAlive: true, title: '列表页' } } ``` 使用`` ```go
    ``` ## 三、原理分析 `keep-alive`是`vue`中内置的一个组件 源码位置:src/core/components/keep-alive.js ```go export default { name: 'keep-alive', abstract: true, props: { include: [String, RegExp, Array], exclude: [String, RegExp, Array], max: [String, Number] }, created () { this.cache = Object.create(null) this.keys = [] }, destroyed () { for (const key in this.cache) { pruneCacheEntry(this.cache, key, this.keys) } }, mounted () { this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) }) }, render() { /* 获取默认插槽中的第一个组件节点 */ const slot = this.$slots.default const vnode = getFirstComponentChild(slot) /* 获取该组件节点的componentOptions */ const componentOptions = vnode && vnode.componentOptions if (componentOptions) { /* 获取该组件节点的名称,优先获取组件的name字段,如果name不存在则获取组件的tag */ const name = getComponentName(componentOptions) const { include, exclude } = this /* 如果name不在inlcude中或者存在于exlude中则表示不缓存,直接返回vnode */ if ( (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode } const { cache, keys } = this /* 获取组件的key值 */ const key = vnode.key == null // same constructor may get registered as different local components // so cid alone is not enough (#3269) ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key /* 拿到key值后去this.cache对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存 */ if (cache[key]) { vnode.componentInstance = cache[key].componentInstance // make current key freshest remove(keys, key) keys.push(key) } /* 如果没有命中缓存,则将其设置进缓存 */ else { cache[key] = vnode keys.push(key) // prune oldest entry /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */ if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } vnode.data.keepAlive = true } return vnode || (slot && slot[0]) } } ``` 可以看到该组件没有`template`,而是用了`render`,在组件渲染的时候会自动执行`render`函数 `this.cache`是一个对象,用来存储需要缓存的组件,它将以如下形式存储: ```go this.cache = { 'key1':'组件1', 'key2':'组件2', // ... } ``` 在组件销毁的时候执行`pruneCacheEntry`函数 ```go function pruneCacheEntry ( cache: VNodeCache, key: string, keys: Array, current?: VNode ) { const cached = cache[key] /* 判断当前没有处于被渲染状态的组件,将其销毁*/ if (cached && (!current || cached.tag !== current.tag)) { cached.componentInstance.$destroy() } cache[key] = null remove(keys, key) } ``` 在`mounted`钩子函数中观测 `include` 和 `exclude` 的变化,如下: ```go mounted () { this.$watch('include', val => { pruneCache(this, name => matches(val, name)) }) this.$watch('exclude', val => { pruneCache(this, name => !matches(val, name)) }) } ``` 如果`include` 或`exclude` 发生了变化,即表示定义需要缓存的组件的规则或者不需要缓存的组件的规则发生了变化,那么就执行`pruneCache`函数,函数如下: ```go function pruneCache (keepAliveInstance, filter) { const { cache, keys, _vnode } = keepAliveInstance for (const key in cache) { const cachedNode = cache[key] if (cachedNode) { const name = getComponentName(cachedNode.componentOptions) if (name && !filter(name)) { pruneCacheEntry(cache, key, keys, _vnode) } } } } ``` 在该函数内对`this.cache`对象进行遍历,取出每一项的`name`值,用其与新的缓存规则进行匹配,如果匹配不上,则表示在新的缓存规则下该组件已经不需要被缓存,则调用`pruneCacheEntry`函数将其从`this.cache`对象剔除即可 关于`keep-alive`的最强大缓存功能是在`render`函数中实现 首先获取组件的`key`值: ```go const key = vnode.key == null? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '') : vnode.key ``` 拿到`key`值后去`this.cache`对象中去寻找是否有该值,如果有则表示该组件有缓存,即命中缓存,如下: ```go /* 如果命中缓存,则直接从缓存中拿 vnode 的组件实例 */ if (cache[key]) { vnode.componentInstance = cache[key].componentInstance /* 调整该组件key的顺序,将其从原来的地方删掉并重新放在最后一个 */ remove(keys, key) keys.push(key) } ``` 直接从缓存中拿 `vnode` 的组件实例,此时重新调整该组件`key`的顺序,将其从原来的地方删掉并重新放在`this.keys`中最后一个 `this.cache`对象中没有该`key`值的情况,如下: ```go /* 如果没有命中缓存,则将其设置进缓存 */ else { cache[key] = vnode keys.push(key) /* 如果配置了max并且缓存的长度超过了this.max,则从缓存中删除第一个 */ if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode) } } ``` 表明该组件还没有被缓存过,则以该组件的`key`为键,组件`vnode`为值,将其存入`this.cache`中,并且把`key`存入`this.keys`中 此时再判断`this.keys`中缓存组件的数量是否超过了设置的最大缓存数量值`this.max`,如果超过了,则把第一个缓存组件删掉 ## 四、思考题:缓存后如何获取数据 解决方案可以有以下两种: - beforeRouteEnter - actived ### beforeRouteEnter 每次组件渲染的时候,都会执行`beforeRouteEnter` ```go beforeRouteEnter(to, from, next){ next(vm=>{ console.log(vm) // 每次进入路由执行 vm.getData() // 获取数据 }) }, ``` ### actived 在`keep-alive`缓存的组件被激活的时候,都会执行`actived`钩子 ```go activated(){ this.getData() // 获取数据 }, ``` 注意:服务器端渲染期间`avtived`不被调用 ## 参考文献 - https://www.cnblogs.com/dhui/p/13589401.html - https://www.cnblogs.com/wangjiachen666/p/11497200.html - https://vue3js.cn/docs/zh ================================================ FILE: docs/vue/key.md ================================================ # 面试官:你知道vue中key的原理吗?说说你对它的理解 ![](https://static.vue-js.com/bc6e9540-3f41-11eb-85f6-6fac77c0c9b3.png) ## 一、Key是什么 开始之前,我们先还原两个实际工作场景 1. 当我们在使用`v-for`时,需要给单元加上`key` ```js
    • ...
    ``` 2. 用`+new Date()`生成的时间戳作为`key`,手动强制触发重新渲染 ```js ``` 那么这背后的逻辑是什么,`key`的作用又是什么? 一句话来讲 > key是给每一个vnode的唯一id,也是diff的一种优化策略,可以根据key,更准确, 更快的找到对应的vnode节点 ### 场景背后的逻辑 当我们在使用`v-for`时,需要给单元加上`key` - 如果不用key,Vue会采用就地复地原则:最小化element的移动,并且会尝试尽最大程度在同适当的地方对相同类型的element,做patch或者reuse。 - 如果使用了key,Vue会根据keys的顺序记录element,曾经拥有了key的element如果不再出现的话,会被直接remove或者destoryed 用`+new Date()`生成的时间戳作为`key`,手动强制触发重新渲染 - 当拥有新值的rerender作为key时,拥有了新key的Comp出现了,那么旧key Comp会被移除,新key Comp触发渲染 ## 二、设置key与不设置key区别 举个例子: 创建一个实例,2秒后往`items`数组插入数据 ```html

    {{item}}

    ``` 在不使用`key`的情况,`vue`会进行这样的操作: ![](https://static.vue-js.com/c9da6790-3f41-11eb-85f6-6fac77c0c9b3.png) 分析下整体流程: - 比较A,A,相同类型的节点,进行`patch`,但数据相同,不发生`dom`操作 - 比较B,B,相同类型的节点,进行`patch`,但数据相同,不发生`dom`操作 - 比较C,F,相同类型的节点,进行`patch`,数据不同,发生`dom`操作 - 比较D,C,相同类型的节点,进行`patch`,数据不同,发生`dom`操作 - 比较E,D,相同类型的节点,进行`patch`,数据不同,发生`dom`操作 - 循环结束,将E插入到`DOM`中 一共发生了3次更新,1次插入操作 在使用`key`的情况:`vue`会进行这样的操作: - 比较A,A,相同类型的节点,进行`patch`,但数据相同,不发生`dom`操作 - 比较B,B,相同类型的节点,进行`patch`,但数据相同,不发生`dom`操作 - 比较C,F,不相同类型的节点 - 比较E、E,相同类型的节点,进行`patch`,但数据相同,不发生`dom`操作 - 比较D、D,相同类型的节点,进行`patch`,但数据相同,不发生`dom`操作 - 比较C、C,相同类型的节点,进行`patch`,但数据相同,不发生`dom`操作 - 循环结束,将F插入到C之前 一共发生了0次更新,1次插入操作 通过上面两个小例子,可见设置`key`能够大大减少对页面的`DOM`操作,提高了`diff`效率 ### 设置key值一定能提高diff效率吗? 其实不然,文档中也明确表示 > 当 Vue.js 用 v-for 正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素 这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出 建议尽可能在使用 `v-for` 时提供 `key`,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升 ## 三、原理分析 源码位置:core/vdom/patch.js 这里判断是否为同一个`key`,首先判断的是`key`值是否相等如果没有设置`key`,那么`key`为`undefined`,这时候`undefined`是恒等于`undefined` ```js function sameVnode (a, b) { return ( a.key === b.key && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || ( isTrue(a.isAsyncPlaceholder) && a.asyncFactory === b.asyncFactory && isUndef(b.asyncFactory.error) ) ) ) } ``` `updateChildren`方法中会对新旧`vnode`进行`diff`,然后将比对出的结果用来更新真实的`DOM` ```js function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { ... while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { ... } else if (isUndef(oldEndVnode)) { ... } else if (sameVnode(oldStartVnode, newStartVnode)) { ... } else if (sameVnode(oldEndVnode, newEndVnode)) { ... } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right ... } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left ... } else { if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // same key but different element. treat as new element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } ... } ``` ## 参考文献 - https://juejin.cn/post/6844903826693029895 - https://juejin.cn/post/6844903985397104648 - https://vue3js.cn/docs/zh ================================================ FILE: docs/vue/lifecycle.md ================================================ # 面试官:请描述下你对vue生命周期的理解?在created和mounted这两个生命周期中请求数据有什么区别呢? ![](https://static.vue-js.com/3a119e10-3aca-11eb-85f6-6fac77c0c9b3.png) ## 一、生命周期是什么 生命周期`(Life Cycle)`的概念应用很广泛,特别是在政治、经济、环境、技术、社会等诸多领域经常出现,其基本涵义可以通俗地理解为“从摇篮到坟墓”`(Cradle-to-Grave)`的整个过程在`Vue`中实例从创建到销毁的过程就是生命周期,即指从创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程我们可以把组件比喻成工厂里面的一条流水线,每个工人(生命周期)站在各自的岗位,当任务流转到工人身边的时候,工人就开始工作PS:在`Vue`生命周期钩子会自动绑定 `this` 上下文到实例中,因此你可以访问数据,对 `property` 和方法进行运算这意味着**你不能使用箭头函数来定义一个生命周期方法** \(例如 `created: () => this.fetchTodos()`\) ## 二、生命周期有哪些 Vue生命周期总共可以分为8个阶段:创建前后, 载入前后,更新前后,销毁前销毁后,以及一些特殊场景的生命周期 | 生命周期 | 描述 | | :-- | :-- | | beforeCreate | 组件实例被创建之初 | | created | 组件实例已经完全创建 | | beforeMount | 组件挂载之前 | | mounted | 组件挂载到实例上去之后 | | beforeUpdate | 组件数据发生变化,更新之前 | | updated | 组件数据更新之后 | | beforeDestroy | 组件实例销毁之前 | | destroyed | 组件实例销毁之后 | | activated | keep-alive 缓存的组件激活时 | | deactivated | keep-alive 缓存的组件停用时调用 | | errorCaptured | 捕获一个来自子孙组件的错误时被调用 | ## 三、生命周期整体流程 `Vue`生命周期流程图 ![](https://static.vue-js.com/44114780-3aca-11eb-85f6-6fac77c0c9b3.png) #### 具体分析 **beforeCreate -> created** - 初始化`vue`实例,进行数据观测 **created** - 完成数据观测,属性与方法的运算,`watch`、`event`事件回调的配置 - 可调用`methods`中的方法,访问和修改data数据触发响应式渲染`dom`,可通过`computed`和`watch`完成数据计算 - 此时`vm.$el` 并没有被创建 **created -> beforeMount** - 判断是否存在`el`选项,若不存在则停止编译,直到调用`vm.$mount(el)`才会继续编译 - 优先级:`render` > `template` > `outerHTML` - `vm.el`获取到的是挂载`DOM`的 **beforeMount** - 在此阶段可获取到`vm.el` - 此阶段`vm.el`虽已完成DOM初始化,但并未挂载在`el`选项上 **beforeMount -> mounted** - 此阶段`vm.el`完成挂载,`vm.$el`生成的`DOM`替换了`el`选项所对应的`DOM` **mounted** - `vm.el`已完成`DOM`的挂载与渲染,此刻打印`vm.$el`,发现之前的挂载点及内容已被替换成新的DOM **beforeUpdate** - 更新的数据必须是被渲染在模板上的(`el`、`template`、`render`之一) - 此时`view`层还未更新 - 若在`beforeUpdate`中再次修改数据,不会再次触发更新方法 **updated** - 完成`view`层的更新 - 若在`updated`中再次修改数据,会再次触发更新方法(`beforeUpdate`、`updated`) **beforeDestroy** - 实例被销毁前调用,此时实例属性与方法仍可访问 **destroyed** - 完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器 - 并不能清除DOM,仅仅销毁实例 **使用场景分析** | 生命周期 | 描述 | | :-- | :-- | | beforeCreate | 执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务 | | created | 组件初始化完毕,各种数据可以使用,常用于异步数据获取 | | beforeMount | 未执行渲染、更新,dom未创建 | | mounted | 初始化结束,dom已创建,可用于获取访问数据和dom元素 | | beforeUpdate | 更新前,可用于获取更新前各种状态 | | updated | 更新后,所有状态已是最新 | | beforeDestroy | 销毁前,可用于一些定时器或订阅的取消 | | destroyed | 组件已销毁,作用同上 | ## 四、题外话:数据请求在created和mouted的区别 `created`是在组件实例一旦创建完成的时候立刻调用,这时候页面`dom`节点并未生成;`mounted`是在页面`dom`节点渲染完毕之后就立刻执行的。触发时机上`created`是比`mounted`要更早的,两者的相同点:都能拿到实例对象的属性和方法。 讨论这个问题本质就是触发的时机,放在`mounted`中的请求有可能导致页面闪动(因为此时页面`dom`结构已经生成),但如果在页面加载前完成请求,则不会出现此情况。建议对页面内容的改动放在`created`生命周期当中。 ## 参考文献 - https://juejin.cn/post/6844903811094413320 - https://baike.baidu.com/ - http://cn.vuejs.org/ 面试官VUE系列总进度:4/33 [面试官:说说你对vue的理解\?](http://mp.weixin.qq.com/s?__biz=MzU1OTgxNDQ1Nw==&mid=2247484101&idx=1&sn=83b0983f0fca7d7c556e4cb0bff8c9b8&chksm=fc10c093cb674985ef3bd2966f66fc28c5eb70b0037e4be1af4bf54fb6fa9571985abd31d52f&scene=21#wechat_redirect) [面试官:说说你对SPA(单页应用)的理解\?](http://mp.weixin.qq.com/s?__biz=MzU1OTgxNDQ1Nw==&mid=2247484119&idx=1&sn=d171b28a00d42549d279498944a98519&chksm=fc10c081cb6749976814aaeda6a6433db418223cec57edda7e15b9e5a0ca69ad549655639c61&scene=21#wechat_redirect) [面试官:说说你对双向绑定的理解\?](http://mp.weixin.qq.com/s?__biz=MzU1OTgxNDQ1Nw==&mid=2247484167&idx=1&sn=7b00b4333ab2722f25f12586b70667ca&chksm=fc10c151cb6748476008dab2f4e6c6264f5d19678305955c85cec1b619e56e8f7457b7357fb9&scene=21#wechat_redirect) ![](https://static.vue-js.com/821b87b0-3ac6-11eb-ab90-d9ae814b240d.png) ================================================ FILE: docs/vue/mixin.md ================================================ # 面试官:说说你对vue的mixin的理解,有什么应用场景? ![](https://static.vue-js.com/8a739c90-3b7f-11eb-85f6-6fac77c0c9b3.png) ## 一、mixin是什么 `Mixin`是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问`mixin`类的方法而不必成为其子类 `Mixin`类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承的复杂 ### Vue中的mixin 先来看一下官方定义 > `mixin`(混入),提供了一种非常灵活的方式,来分发 `Vue` 组件中的可复用功能。 本质其实就是一个`js`对象,它可以包含我们组件中任意功能选项,如`data`、`components`、`methods `、`created`、`computed`等等 我们只要将共用的功能以对象的方式传入 `mixins`选项中,当组件使用 `mixins`对象时所有`mixins`对象的选项都将被混入该组件本身的选项中来 在`Vue`中我们可以**局部混入**跟**全局混入** ### 局部混入 定义一个`mixin`对象,有组件`options`的`data`、`methods`属性 ```js var myMixin = { created: function () { this.hello() }, methods: { hello: function () { console.log('hello from mixin!') } } } ``` 组件通过`mixins`属性调用`mixin`对象 ```js Vue.component('componentA',{ mixins: [myMixin] }) ``` 该组件在使用的时候,混合了`mixin`里面的方法,在自动执行`created`生命钩子,执行`hello`方法 ### 全局混入 通过`Vue.mixin()`进行全局的混入 ```js Vue.mixin({ created: function () { console.log("全局混入") } }) ``` 使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件) PS:全局混入常用于插件的编写 ### 注意事项: 当组件存在与`mixin`对象相同的选项的时候,进行递归合并的时候组件的选项会覆盖`mixin`的选项 但是如果相同选项为生命周期钩子的时候,会合并成一个数组,先执行`mixin`的钩子,再执行组件的钩子 ## 二、使用场景 在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立 这时,可以通过`Vue`的`mixin`功能将相同或者相似的代码提出来 举个例子 定义一个`modal`弹窗组件,内部通过`isShowing`来控制显示 ```js const Modal = { template: '#modal', data() { return { isShowing: false } }, methods: { toggleShow() { this.isShowing = !this.isShowing; } } } ``` 定义一个`tooltip`提示框,内部通过`isShowing`来控制显示 ```js const Tooltip = { template: '#tooltip', data() { return { isShowing: false } }, methods: { toggleShow() { this.isShowing = !this.isShowing; } } } ``` 通过观察上面两个组件,发现两者的逻辑是相同,代码控制显示也是相同的,这时候`mixin`就派上用场了 首先抽出共同代码,编写一个`mixin` ```js const toggle = { data() { return { isShowing: false } }, methods: { toggleShow() { this.isShowing = !this.isShowing; } } } ``` 两个组件在使用上,只需要引入`mixin` ```js const Modal = { template: '#modal', mixins: [toggle] }; const Tooltip = { template: '#tooltip', mixins: [toggle] } ``` 通过上面小小的例子,让我们知道了`Mixin`对于封装一些可复用的功能如此有趣、方便、实用 ## 三、源码分析 首先从`Vue.mixin`入手 源码位置:/src/core/global-api/mixin.js ```js export function initMixin (Vue: GlobalAPI) { Vue.mixin = function (mixin: Object) { this.options = mergeOptions(this.options, mixin) return this } } ``` 主要是调用`merOptions`方法 源码位置:/src/core/util/options.js ```js export function mergeOptions ( parent: Object, child: Object, vm?: Component ): Object { if (child.mixins) { // 判断有没有mixin 也就是mixin里面挂mixin的情况 有的话递归进行合并 for (let i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm) } } const options = {} let key for (key in parent) { mergeField(key) // 先遍历parent的key 调对应的strats[XXX]方法进行合并 } for (key in child) { if (!hasOwn(parent, key)) { // 如果parent已经处理过某个key 就不处理了 mergeField(key) // 处理child中的key 也就parent中没有处理过的key } } function mergeField (key) { const strat = strats[key] || defaultStrat options[key] = strat(parent[key], child[key], vm, key) // 根据不同类型的options调用strats中不同的方法进行合并 } return options } ``` 从上面的源码,我们得到以下几点: - 优先递归处理 `mixins` - 先遍历合并`parent` 中的`key`,调用`mergeField`方法进行合并,然后保存在变量`options` - 再遍历 `child`,合并补上 `parent` 中没有的`key`,调用`mergeField`方法进行合并,保存在变量`options` - 通过 `mergeField` 函数进行了合并 下面是关于`Vue`的几种类型的合并策略 - 替换型 - 合并型 - 队列型 - 叠加型 ### 替换型 替换型合并有`props`、`methods`、`inject`、`computed` ```js strats.props = strats.methods = strats.inject = strats.computed = function ( parentVal: ?Object, childVal: ?Object, vm?: Component, key: string ): ?Object { if (!parentVal) return childVal // 如果parentVal没有值,直接返回childVal const ret = Object.create(null) // 创建一个第三方对象 ret extend(ret, parentVal) // extend方法实际是把parentVal的属性复制到ret中 if (childVal) extend(ret, childVal) // 把childVal的属性复制到ret中 return ret } strats.provide = mergeDataOrFn ``` 同名的`props`、`methods`、`inject`、`computed`会被后来者代替 ### 合并型 和并型合并有:`data` ```js strats.data = function(parentVal, childVal, vm) { return mergeDataOrFn( parentVal, childVal, vm ) }; function mergeDataOrFn(parentVal, childVal, vm) { return function mergedInstanceDataFn() { var childData = childVal.call(vm, vm) // 执行data挂的函数得到对象 var parentData = parentVal.call(vm, vm) if (childData) { return mergeData(childData, parentData) // 将2个对象进行合并 } else { return parentData // 如果没有childData 直接返回parentData } } } function mergeData(to, from) { if (!from) return to var key, toVal, fromVal; var keys = Object.keys(from); for (var i = 0; i < keys.length; i++) { key = keys[i]; toVal = to[key]; fromVal = from[key]; // 如果不存在这个属性,就重新设置 if (!to.hasOwnProperty(key)) { set(to, key, fromVal); } // 存在相同属性,合并对象 else if (typeof toVal =="object" && typeof fromVal =="object") { mergeData(toVal, fromVal); } } return to } ``` `mergeData`函数遍历了要合并的 data 的所有属性,然后根据不同情况进行合并: - 当目标 data 对象不包含当前属性时,调用 `set` 方法进行合并(set方法其实就是一些合并重新赋值的方法) - 当目标 data 对象包含当前属性并且当前值为纯对象时,递归合并当前对象值,这样做是为了防止对象存在新增属性 ### 队列性 队列性合并有:全部生命周期和`watch` ```js function mergeHook ( parentVal: ?Array, childVal: ?Function | ?Array ): ?Array { return childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal } LIFECYCLE_HOOKS.forEach(hook => { strats[hook] = mergeHook }) // watch strats.watch = function ( parentVal, childVal, vm, key ) { // work around Firefox's Object.prototype.watch... if (parentVal === nativeWatch) { parentVal = undefined; } if (childVal === nativeWatch) { childVal = undefined; } /* istanbul ignore if */ if (!childVal) { return Object.create(parentVal || null) } { assertObjectType(key, childVal, vm); } if (!parentVal) { return childVal } var ret = {}; extend(ret, parentVal); for (var key$1 in childVal) { var parent = ret[key$1]; var child = childVal[key$1]; if (parent && !Array.isArray(parent)) { parent = [parent]; } ret[key$1] = parent ? parent.concat(child) : Array.isArray(child) ? child : [child]; } return ret }; ``` 生命周期钩子和`watch`被合并为一个数组,然后正序遍历一次执行 ### 叠加型 叠加型合并有:`component`、`directives`、`filters` ```js strats.components= strats.directives= strats.filters = function mergeAssets( parentVal, childVal, vm, key ) { var res = Object.create(parentVal || null); if (childVal) { for (var key in childVal) { res[key] = childVal[key]; } } return res } ``` 叠加型主要是通过原型链进行层层的叠加 ### 小结: - 替换型策略有`props`、`methods`、`inject`、`computed`,就是将新的同名参数替代旧的参数 - 合并型策略是`data`, 通过`set`方法进行合并和重新赋值 - 队列型策略有生命周期函数和`watch`,原理是将函数存入一个数组,然后正序遍历依次执行 - 叠加型有`component`、`directives`、`filters`,通过原型链进行层层的叠加 ## 参考文献 - https://zhuanlan.zhihu.com/p/31018570 - https://juejin.cn/post/6844904015495446536#heading-1 - https://juejin.cn/post/6844903846775357453 - https://vue3js.cn/docs/zh ================================================ FILE: docs/vue/modifier.md ================================================ # 面试官:Vue常用的修饰符有哪些有什么应用场景 ![](https://static.vue-js.com/8f718e30-42c0-11eb-ab90-d9ae814b240d.png) ## 一、修饰符是什么 在程序世界里,修饰符是用于限定类型以及类型成员的声明的一种符号 在`Vue`中,修饰符处理了许多`DOM`事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理 `vue`中修饰符分为以下五种: - 表单修饰符 - 事件修饰符 - 鼠标按键修饰符 - 键值修饰符 - v-bind修饰符 ## 二、修饰符的作用 ### 表单修饰符 在我们填写表单的时候用得最多的是`input`标签,指令用得最多的是`v-model` 关于表单的修饰符有如下: - lazy - trim - number #### lazy 在我们填完信息,光标离开标签的时候,才会将值赋予给`value`,也就是在`change`事件之后再进行信息同步 ```js

    {{value}}

    ``` #### trim 自动过滤用户输入的首空格字符,而中间的空格不会过滤 ```js ``` #### number 自动将用户的输入值转为数值类型,但如果这个值无法被`parseFloat`解析,则会返回原来的值 ```js ``` ### 事件修饰符 事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符: - stop - prevent - self - once - capture - passive - native #### stop 阻止了事件冒泡,相当于调用了`event.stopPropagation`方法 ```js
    //只输出1 ``` #### prevent 阻止了事件的默认行为,相当于调用了`event.preventDefault`方法 ```js
    ``` #### self 只当在 `event.target` 是当前元素自身时触发处理函数 ```js
    ...
    ``` > 使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 `v-on:click.prevent.self` 会阻止**所有的点击**,而 `v-on:click.self.prevent` 只会阻止对元素自身的点击 #### once 绑定了事件以后只能触发一次,第二次就不会触发 ```js ``` #### capture 使事件触发从包含这个元素的顶层开始往下触发 ```js
    obj1
    obj2
    obj3
    obj4
    // 输出结构: 1 2 4 3 ``` #### passive 在移动端,当我们在监听元素滚动事件的时候,会一直触发`onscroll`事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给`onscroll`事件整了一个`.lazy`修饰符 ```js
    ...
    ``` > 不要把 `.passive` 和 `.prevent` 一起使用,因为 `.prevent` 将会被忽略,同时浏览器可能会向你展示一个警告。 > > `passive` 会告诉浏览器你不想阻止事件的默认行为 #### native 让组件变成像`html`内置标签那样监听根元素的原生事件,否则组件上使用 `v-on` 只会监听自定义事件 ```js ``` > 使用.native修饰符来操作普通HTML标签是会令事件失效的 ### 鼠标按钮修饰符 鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下: - left 左键点击 - right 右键点击 - middle 中键点击 ```js ``` ### 键盘修饰符 键盘修饰符是用来修饰键盘事件(`onkeyup`,`onkeydown`)的,有如下: `keyCode`存在很多,但`vue`为我们提供了别名,分为以下两种: - 普通键(enter、tab、delete、space、esc、up...) - 系统修饰键(ctrl、alt、meta、shift...) ```js // 只有按键为keyCode的时候才触发 ``` 还可以通过以下方式自定义一些全局的键盘码别名 ```js Vue.config.keyCodes.f2 = 113 ``` ### v-bind修饰符 v-bind修饰符主要是为属性进行操作,用来分别有如下: - async - prop - camel #### async 能对`props`进行一个双向绑定 ```js //父组件 //子组件 this.$emit('update:myMessage',params); ``` 以上这种方法相当于以下的简写 ```js //父亲组件 func(e){ this.bar = e; } //子组件js func2(){ this.$emit('update:myMessage',params); } ``` 使用`async`需要注意以下两点: - 使用`sync`的时候,子组件传递的事件名格式必须为`update:value`,其中`value`必须与子组件中`props`中声明的名称完全一致 - 注意带有 `.sync` 修饰符的 `v-bind` 不能和表达式一起使用 - 将 `v-bind.sync` 用在一个字面量的对象上,例如 `v-bind.sync=”{ title: doc.title }”`,是无法正常工作的 #### props 设置自定义标签属性,避免暴露数据,防止污染HTML结构 ```js ``` #### camel 将命名变为驼峰命名法,如将` view-Box`属性名转换为 `viewBox` ```js ``` ## 三、应用场景 根据每一个修饰符的功能,我们可以得到以下修饰符的应用场景: - .stop:阻止事件冒泡 - .native:绑定原生事件 - .once:事件只执行一次 - .self :将事件绑定在自身身上,相当于阻止事件冒泡 - .prevent:阻止默认事件 - .caption:用于事件捕获 - .once:只触发一次 - .keyCode:监听特定键盘按下 - .right:右键 ## 参考文献 - https://segmentfault.com/a/1190000016786254 - https://vue3js.cn/docs/zh ================================================ FILE: docs/vue/new_vue.md ================================================ # 面试官:Vue实例挂载的过程 ![](https://static.vue-js.com/63194810-3a09-11eb-85f6-6fac77c0c9b3.png) ## 一、思考 我们都听过知其然知其所以然这句话 那么不知道大家是否思考过`new Vue()`这个过程中究竟做了些什么? 过程中是如何完成数据的绑定,又是如何将数据渲染到视图的等等 ## 一、分析 首先找到`vue`的构造函数 源码位置:src\core\instance\index.js ```js function Vue (options) { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init(options) } ``` `options`是用户传递过来的配置项,如`data、methods`等常用的方法 `vue`构建函数调用`_init`方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方法 ```js initMixin(Vue); // 定义 _init stateMixin(Vue); // 定义 $set $get $delete $watch 等 eventsMixin(Vue); // 定义事件 $on $once $off $emit lifecycleMixin(Vue);// 定义 _update $forceUpdate $destroy renderMixin(Vue); // 定义 _render 返回虚拟dom ``` 首先可以看`initMixin`方法,发现该方法在`Vue`原型上定义了`_init`方法 源码位置:src\core\instance\init.js ```js Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { startTag = `vue-perf-start:${vm._uid}` endTag = `vue-perf-end:${vm._uid}` mark(startTag) } // a flag to avoid this being observed vm._isVue = true // merge options // 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法 if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { // 合并vue属性 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { // 初始化proxy拦截器 initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm // 初始化组件生命周期标志位 initLifecycle(vm) // 初始化组件事件侦听 initEvents(vm) // 初始化渲染方法 initRender(vm) callHook(vm, 'beforeCreate') // 初始化依赖注入内容,在初始化data、props之前 initInjections(vm) // resolve injections before data/props // 初始化props/data/method/watch/methods initState(vm) initProvide(vm) // resolve provide after data/props callHook(vm, 'created') /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { vm._name = formatComponentName(vm, false) mark(endTag) measure(`vue ${vm._name} init`, startTag, endTag) } // 挂载元素 if (vm.$options.el) { vm.$mount(vm.$options.el) } } ``` 仔细阅读上面的代码,我们得到以下结论: - 在调用`beforeCreate`之前,数据初始化并未完成,像`data`、`props`这些属性无法访问到 - 到了`created`的时候,数据已经初始化完成,能够访问`data`、`props`这些属性,但这时候并未完成`dom`的挂载,因此无法访问到`dom`元素 - 挂载方法是调用`vm.$mount`方法 `initState`方法是完成`props/data/method/watch/methods`的初始化 源码位置:src\core\instance\state.js ```js export function initState (vm: Component) { // 初始化组件的watcher列表 vm._watchers = [] const opts = vm.$options // 初始化props if (opts.props) initProps(vm, opts.props) // 初始化methods方法 if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { // 初始化data initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) if (opts.watch && opts.watch !== nativeWatch) { initWatch(vm, opts.watch) } } ``` 我们和这里主要看初始化`data`的方法为`initData`,它与`initState`在同一文件上 ```js function initData (vm: Component) { let data = vm.$options.data // 获取到组件上的data data = vm._data = typeof data === 'function' ? getData(data, vm) : data || {} if (!isPlainObject(data)) { data = {} process.env.NODE_ENV !== 'production' && warn( 'data functions should return an object:\n' + 'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function', vm ) } // proxy data on instance const keys = Object.keys(data) const props = vm.$options.props const methods = vm.$options.methods let i = keys.length while (i--) { const key = keys[i] if (process.env.NODE_ENV !== 'production') { // 属性名不能与方法名重复 if (methods && hasOwn(methods, key)) { warn( `Method "${key}" has already been defined as a data property.`, vm ) } } // 属性名不能与state名称重复 if (props && hasOwn(props, key)) { process.env.NODE_ENV !== 'production' && warn( `The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`, vm ) } else if (!isReserved(key)) { // 验证key值的合法性 // 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据 proxy(vm, `_data`, key) } } // observe data // 响应式监听data是数据的变化 observe(data, true /* asRootData */) } ``` 仔细阅读上面的代码,我们可以得到以下结论: - 初始化顺序:`props`、`methods`、`data` - `data`定义的时候可选择函数形式或者对象形式(组件只能为函数形式) 关于数据响应式在这就不展开详细说明 上文提到挂载方法是调用`vm.$mount`方法 源码位置: ```js Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { // 获取或查询元素 el = el && query(el) /* istanbul ignore if */ // vue 不允许直接挂载到body或页面文档上 if (el === document.body || el === document.documentElement) { process.env.NODE_ENV !== 'production' && warn( `Do not mount Vue to or - mount to normal elements instead.` ) return this } const options = this.$options // resolve template/el and convert to render function if (!options.render) { let template = options.template // 存在template模板,解析vue模板文件 if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template) /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && !template) { warn( `Template element not found or is empty: ${options.template}`, this ) } } } else if (template.nodeType) { template = template.innerHTML } else { if (process.env.NODE_ENV !== 'production') { warn('invalid template option:' + template, this) } return this } } else if (el) { // 通过选择器获取元素内容 template = getOuterHTML(el) } if (template) { /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile') } /** * 1.将temmplate解析ast tree * 2.将ast tree转换成render语法字符串 * 3.生成render方法 */ const { render, staticRenderFns } = compileToFunctions(template, { outputSourceRange: process.env.NODE_ENV !== 'production', shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end') measure(`vue ${this._name} compile`, 'compile', 'compile end') } } } return mount.call(this, el, hydrating) } ``` 阅读上面代码,我们能得到以下结论: - 不要将根元素放到`body`或者`html`上 - 可以在对象中定义`template/render`或者直接使用`template`、`el`表示元素选择器 - 最终都会解析成`render`函数,调用`compileToFunctions`,会将`template`解析成`render`函数 对`template`的解析步骤大致分为以下几步: - 将`html`文档片段解析成`ast`描述符 - 将`ast`描述符解析成字符串 - 生成`render`函数 生成`render`函数,挂载到`vm`上后,会再次调用`mount`方法 源码位置:src\platforms\web\runtime\index.js ```js // public mount method Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined // 渲染组件 return mountComponent(this, el, hydrating) } ``` 调用`mountComponent`渲染组件 ```js export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el // 如果没有获取解析的render函数,则会抛出警告 // render是解析模板文件生成的 if (!vm.$options.render) { vm.$options.render = createEmptyVNode if (process.env.NODE_ENV !== 'production') { /* istanbul ignore if */ if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || vm.$options.el || el) { warn( 'You are using the runtime-only build of Vue where the template ' + 'compiler is not available. Either pre-compile the templates into ' + 'render functions, or use the compiler-included build.', vm ) } else { // 没有获取到vue的模板文件 warn( 'Failed to mount component: template or render function not defined.', vm ) } } } // 执行beforeMount钩子 callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { const name = vm._name const id = vm._uid const startTag = `vue-perf-start:${id}` const endTag = `vue-perf-end:${id}` mark(startTag) const vnode = vm._render() mark(endTag) measure(`vue ${name} render`, startTag, endTag) mark(startTag) vm._update(vnode, hydrating) mark(endTag) measure(`vue ${name} patch`, startTag, endTag) } } else { // 定义更新函数 updateComponent = () => { // 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render vm._update(vm._render(), hydrating) } } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined // 监听当前组件状态,当有数据变化时,更新组件 new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted && !vm._isDestroyed) { // 数据更新引发的组件更新 callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm } ``` 阅读上面代码,我们得到以下结论: - 会触发`beforeCreate`钩子 - 定义`updateComponent`渲染页面视图的方法 - 监听组件数据,一旦发生变化,触发`beforeUpdate`生命钩子 `updateComponent`方法主要执行在`vue`初始化时声明的`render`,`update`方法 `render`的作用主要是生成`vnode` 源码位置:src\core\instance\render.js ```js // 定义vue 原型上的render方法 Vue.prototype._render = function (): VNode { const vm: Component = this // render函数来自于组件的option const { render, _parentVnode } = vm.$options if (_parentVnode) { vm.$scopedSlots = normalizeScopedSlots( _parentVnode.data.scopedSlots, vm.$slots, vm.$scopedSlots ) } // set parent vnode. this allows render functions to have access // to the data on the placeholder node. vm.$vnode = _parentVnode // render self let vnode try { // There's no need to maintain a stack because all render fns are called // separately from one another. Nested component's render fns are called // when parent component is patched. currentRenderingInstance = vm // 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) { try { vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e) } catch (e) { handleError(e, vm, `renderError`) vnode = vm._vnode } } else { vnode = vm._vnode } } finally { currentRenderingInstance = null } // if the returned array contains only a single node, allow it if (Array.isArray(vnode) && vnode.length === 1) { vnode = vnode[0] } // return empty vnode in case the render function errored out if (!(vnode instanceof VNode)) { if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) { warn( 'Multiple root nodes returned from render function. Render function ' + 'should return a single root node.', vm ) } vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode } ``` `_update`主要功能是调用`patch`,将`vnode`转换为真实`DOM`,并且更新到页面中 源码位置:src\core\instance\lifecycle.js ```js Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode // 设置当前激活的作用域 const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render // 执行具体的挂载逻辑 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. } ``` ## 三、结论 - `new Vue`的时候调用会调用`_init`方法 - 定义 `$set`、` $get` 、`$delete`、`$watch` 等方法 - 定义 `$on`、`$off`、`$emit`、`$off `等事件 - 定义 `_update`、`$forceUpdate`、`$destroy`生命周期 - 调用`$mount`进行页面的挂载 - 挂载的时候主要是通过`mountComponent`方法 - 定义`updateComponent`更新函数 - 执行`render`生成虚拟`DOM` - `_update`将虚拟`DOM`生成真实`DOM`结构,并且渲染到页面中 ## 参考文献 - https://www.cnblogs.com/gerry2019/p/12001661.html - https://github.com/vuejs/vue/tree/dev/src/core/instance - https://vue3js.cn ================================================ FILE: docs/vue/nexttick.md ================================================ # 面试官:Vue中的$nextTick有什么作用? ![](https://static.vue-js.com/76484d30-3aba-11eb-85f6-6fac77c0c9b3.png) ## 一、NextTick是什么 官方对其的定义 > 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM 什么意思呢? 我们可以理解成,`Vue` 在更新 `DOM` 时是异步执行的。当数据发生变化,`Vue`将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新 举例一下 `Html`结构 ```html
    {{ message }}
    ``` 构建一个`vue`实例 ```js const vm = new Vue({ el: '#app', data: { message: '原始值' } }) ``` 修改`message` ```js this.message = '修改后的值1' this.message = '修改后的值2' this.message = '修改后的值3' ``` 这时候想获取页面最新的`DOM`节点,却发现获取到的是旧值 ```js console.log(vm.$el.textContent) // 原始值 ``` 这是因为`message`数据在发现变化的时候,`vue`并不会立刻去更新`Dom`,而是将修改数据的操作放在了一个异步操作队列中 如果我们一直修改相同数据,异步操作队列还会进行去重 等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行`DOM`的更新 #### 为什么要有nexttick 举个例子 ```js {{num}} for(let i=0; i<100000; i++){ num = i } ``` 如果没有 `nextTick` 更新机制,那么 `num` 每次更新值都会触发视图更新(上面这段代码也就是会更新10万次视图),有了`nextTick`机制,只需要更新一次,所以`nextTick`本质是一种优化策略 ## 二、使用场景 如果想要在修改数据后立刻得到更新后的`DOM`结构,可以使用`Vue.nextTick()` 第一个参数为:回调函数(可以获取最近的`DOM`结构) 第二个参数为:执行函数上下文 ```js // 修改数据 vm.message = '修改后的值' // DOM 还没有更新 console.log(vm.$el.textContent) // 原始的值 Vue.nextTick(function () { // DOM 更新了 console.log(vm.$el.textContent) // 修改后的值 }) ``` 组件内使用 `vm.$nextTick()` 实例方法只需要通过`this.$nextTick()`,并且回调函数中的 `this` 将自动绑定到当前的 `Vue` 实例上 ```js this.message = '修改后的值' console.log(this.$el.textContent) // => '原始的值' this.$nextTick(function () { console.log(this.$el.textContent) // => '修改后的值' }) ``` `$nextTick()` 会返回一个 `Promise` 对象,可以是用`async/await`完成相同作用的事情 ```js this.message = '修改后的值' console.log(this.$el.textContent) // => '原始的值' await this.$nextTick() console.log(this.$el.textContent) // => '修改后的值' ``` ## 三、实现原理 源码位置:`/src/core/util/next-tick.js` `callbacks`也就是异步操作队列 `callbacks`新增回调函数后又执行了`timerFunc`函数,`pending`是用来标识同一个时间只能执行一次 ```js export function nextTick(cb?: Function, ctx?: Object) { let _resolve; // cb 回调函数会经统一处理压入 callbacks 数组 callbacks.push(() => { if (cb) { // 给 cb 回调函数执行加上了 try-catch 错误处理 try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); // 执行异步延迟函数 timerFunc if (!pending) { pending = true; timerFunc(); } // 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用 if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve; }); } } ``` `timerFunc`函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有: `Promise.then`、`MutationObserver`、`setImmediate`、`setTimeout` 通过上面任意一种方法,进行降级操作 ```js export let isUsingMicroTask = false if (typeof Promise !== 'undefined' && isNative(Promise)) { //判断1:是否原生支持Promise const p = Promise.resolve() timerFunc = () => { p.then(flushCallbacks) if (isIOS) setTimeout(noop) } isUsingMicroTask = true } else if (!isIE && typeof MutationObserver !== 'undefined' && ( isNative(MutationObserver) || MutationObserver.toString() === '[object MutationObserverConstructor]' )) { //判断2:是否原生支持MutationObserver let counter = 1 const observer = new MutationObserver(flushCallbacks) const textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } isUsingMicroTask = true } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { //判断3:是否原生支持setImmediate timerFunc = () => { setImmediate(flushCallbacks) } } else { //判断4:上面都不行,直接用setTimeout timerFunc = () => { setTimeout(flushCallbacks, 0) } } ``` 无论是微任务还是宏任务,都会放到`flushCallbacks`使用 这里将`callbacks`里面的函数复制一份,同时`callbacks`置空 依次执行`callbacks`里面的函数 ```js function flushCallbacks () { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } ``` **小结:** 1. 把回调函数放入callbacks等待执行 2. 将执行函数放到微任务或者宏任务中 3. 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调 ## 参考文献 - https://juejin.cn/post/6844904147804749832 ================================================ FILE: docs/vue/observable.md ================================================ # 面试官:Vue.observable你有了解过吗?说说看 ![](https://static.vue-js.com/193782e0-3e7b-11eb-ab90-d9ae814b240d.png) ## 一、Observable 是什么 `Observable` 翻译过来我们可以理解成**可观察的** 我们先来看一下其在`Vue`中的定义 > `Vue.observable`,让一个对象变成响应式数据。`Vue` 内部会用它来处理 `data` 函数返回的对象 返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器 ```js Vue.observable({ count : 1}) ``` 其作用等同于 ```js new vue({ count : 1}) ``` 在 `Vue 2.x` 中,被传入的对象会直接被 `Vue.observable` 变更,它和被返回的对象是同一个对象 在 `Vue 3.x` 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应的 ## 二、使用场景 在非父子组件通信时,可以使用通常的`bus`或者使用`vuex`,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,`observable`就是一个很好的选择 创建一个`js`文件 ```js // 引入vue import Vue from 'vue // 创建state对象,使用observable让state对象可响应 export let state = Vue.observable({ name: '张三', 'age': 38 }) // 创建对应的方法 export let mutations = { changeName(name) { state.name = name }, setAge(age) { state.age = age } } ``` 在`.vue`文件中直接使用即可 ```js import { state, mutations } from '@/store export default { // 在计算属性中拿到值 computed: { name() { return state.name }, age() { return state.age } }, // 调用mutations里面的方法,更新数据 methods: { changeName: mutations.changeName, setAge: mutations.setAge } } ``` ## 三、原理分析 源码位置:src\core\observer\index.js ```js export function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void // 判断是否存在__ob__响应式属性 if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) { ob = value.__ob__ } else if ( shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { // 实例化Observer响应式对象 ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob } ``` `Observer`类 ```js export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data constructor (value: any) { this.value = value this.dep = new Dep() this.vmCount = 0 def(value, '__ob__', this) if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } this.observeArray(value) } else { // 实例化对象是一个对象,进入walk方法 this.walk(value) } } ``` `walk`函数 ```js walk (obj: Object) { const keys = Object.keys(obj) // 遍历key,通过defineReactive创建响应式对象 for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } ``` `defineReactive`方法 ```js export function defineReactive ( obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } // cater for pre-defined getter/setters const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = !shallow && observe(val) // 接下来调用Object.defineProperty()给对象定义响应式属性 Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter () { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter (newVal) { const value = getter ? getter.call(obj) : val /* eslint-disable no-self-compare */ if (newVal === value || (newVal !== newVal && value !== value)) { return } /* eslint-enable no-self-compare */ if (process.env.NODE_ENV !== 'production' && customSetter) { customSetter() } // #7981: for accessor properties without setter if (getter && !setter) return if (setter) { setter.call(obj, newVal) } else { val = newVal } childOb = !shallow && observe(newVal) // 对观察者watchers进行通知,state就成了全局响应式对象 dep.notify() } }) } ``` ## 参考文献 - https://blog.csdn.net/qq_32682301/article/details/105419673 - https://wbbyouzi.com/archives/343 ================================================ FILE: docs/vue/permission.md ================================================ # 面试官:vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做? ![](https://static.vue-js.com/397e1fa0-4dad-11eb-ab90-d9ae814b240d.png) ## 一、是什么 权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源 而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式触发 - 页面加载触发 - 页面上的按钮点击触发 总的来说,所有的请求发起都触发自前端路由或视图 所以我们可以从这两方面入手,对触发权限的源头进行控制,最终要实现的目标是: - 路由方面,用户登录后只能看到自己有权访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转 `4xx` 提示页 - 视图方面,用户只能看到自己有权浏览的内容和有权操作的控件 - 最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦截 ## 二、如何做 前端权限控制可以分为四个方面: - 接口权限 - 按钮权限 - 菜单权限 - 路由权限 ### 接口权限 接口权限目前一般采用`jwt`的形式来验证,没有通过的话一般返回`401`,跳转到登录页面重新进行登录 登录完拿到`token`,将`token`存起来,通过`axios`请求拦截器进行拦截,每次请求的时候头部携带`token` ```js axios.interceptors.request.use(config => { config.headers['token'] = cookie.get('token') return config }) axios.interceptors.response.use(res=>{},{response}=>{ if (response.data.code === 40099 || response.data.code === 40098) { //token过期或者错误 router.push('/login') } }) ``` ### 路由权限控制 **方案一** 初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校验 ```js const routerMap = [ { path: '/permission', component: Layout, redirect: '/permission/index', alwaysShow: true, // will always show the root menu meta: { title: 'permission', icon: 'lock', roles: ['admin', 'editor'] // you can set roles in root nav }, children: [{ path: 'page', component: () => import('@/views/permission/page'), name: 'pagePermission', meta: { title: 'pagePermission', roles: ['admin'] // or you can only set roles in sub nav } }, { path: 'directive', component: () => import('@/views/permission/directive'), name: 'directivePermission', meta: { title: 'directivePermission' // if do not set roles, means: this page does not require permission } }] }] ``` 这种方式存在以下四种缺点: - 加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限访问,对性能会有影响。 - 全局路由守卫里,每次路由跳转都要做权限判断。 - 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译 - 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识 **方案二** 初始化的时候先挂载不需要权限控制的路由,比如登录页,404等错误页。如果用户通过URL进行强制访问,则会直接进入404,相当于从源头上做了控制 登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用`addRoutes`添加路由 ```js import router from './router' import store from './store' import { Message } from 'element-ui' import NProgress from 'nprogress' // progress bar import 'nprogress/nprogress.css'// progress bar style import { getToken } from '@/utils/auth' // getToken from cookie NProgress.configure({ showSpinner: false })// NProgress Configuration // permission judge function function hasPermission(roles, permissionRoles) { if (roles.indexOf('admin') >= 0) return true // admin permission passed directly if (!permissionRoles) return true return roles.some(role => permissionRoles.indexOf(role) >= 0) } const whiteList = ['/login', '/authredirect']// no redirect whitelist router.beforeEach((to, from, next) => { NProgress.start() // start progress bar if (getToken()) { // determine if there has token /* has token*/ if (to.path === '/login') { next({ path: '/' }) NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it } else { if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息 store.dispatch('GetUserInfo').then(res => { // 拉取user_info const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop'] store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由表 router.addRoutes(store.getters.addRouters) // 动态添加可访问路由表 next({ ...to, replace: true }) // hack方法 确保addRoutes已完成 ,set the replace: true so the navigation will not leave a history record }) }).catch((err) => { store.dispatch('FedLogOut').then(() => { Message.error(err || 'Verification failed, please login again') next({ path: '/' }) }) }) } else { // 没有动态改变权限的需求可直接next() 删除下方权限判断 ↓ if (hasPermission(store.getters.roles, to.meta.roles)) { next()// } else { next({ path: '/401', replace: true, query: { noGoBack: true }}) } // 可删 ↑ } } } else { /* has no token*/ if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 next() } else { next('/login') // 否则全部重定向到登录页 NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it } } }) router.afterEach(() => { NProgress.done() // finish progress bar }) ``` 按需挂载,路由就需要知道用户的路由权限,也就是在用户登录进来的时候就要知道当前用户拥有哪些路由权限 这种方式也存在了以下的缺点: - 全局路由守卫里,每次路由跳转都要做判断 - 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编译 - 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识 ### 菜单权限 菜单权限可以理解成将页面与理由进行解耦 #### 方案一 菜单与路由分离,菜单由后端返回 前端定义路由信息 ```js { name: "login", path: "/login", component: () => import("@/pages/Login.vue") } ``` `name`字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有`name`对应的字段,并且做唯一性校验 全局路由守卫里做判断 ```js function hasPermission(router, accessMenu) { if (whiteList.indexOf(router.path) !== -1) { return true; } let menu = Util.getMenuByName(router.name, accessMenu); if (menu.name) { return true; } return false; } Router.beforeEach(async (to, from, next) => { if (getToken()) { let userInfo = store.state.user.userInfo; if (!userInfo.name) { try { await store.dispatch("GetUserInfo") await store.dispatch('updateAccessMenu') if (to.path === '/login') { next({ name: 'home_index' }) } else { //Util.toDefaultPage([...routers], to.name, router, next); next({ ...to, replace: true })//菜单权限更新完成,重新进一次当前路由 } } catch (e) { if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 next() } else { next('/login') } } } else { if (to.path === '/login') { next({ name: 'home_index' }) } else { if (hasPermission(to, store.getters.accessMenu)) { Util.toDefaultPage(store.getters.accessMenu,to, routes, next); } else { next({ path: '/403',replace:true }) } } } } else { if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入 next() } else { next('/login') } } let menu = Util.getMenuByName(to.name, store.getters.accessMenu); Util.title(menu.title); }); Router.afterEach((to) => { window.scrollTo(0, 0); }); ``` 每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的`name`与路由的`name`是一一对应的,而后端返回的菜单就已经是经过权限过滤的 如果根据路由`name`找不到对应的菜单,就表示用户有没权限访问 如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过`addRoutes`动态挂载 这种方式的缺点: - 菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使用 - 全局路由守卫里,每次路由跳转都要做判断 #### 方案二 菜单和路由都由后端返回 前端统一定义路由组件 ```js const Home = () => import("../pages/Home.vue"); const UserInfo = () => import("../pages/UserInfo.vue"); export default { home: Home, userInfo: UserInfo }; ``` 后端路由组件返回以下格式 ```js [ { name: "home", path: "/", component: "home" }, { name: "home", path: "/userinfo", component: "userInfo" } ] ``` 在将后端返回路由通过`addRoutes`动态挂载之间,需要将数据处理一下,将`component`字段换为真正的组件 如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处理 这种方法也会存在缺点: - 全局路由守卫里,每次路由跳转都要做判断 - 前后端的配合要求更高 ### 按钮权限 #### 方案一 按钮权限也可以用`v-if`判断 但是如果页面过多,每个页面页面都要获取用户权限`role`和路由表里的`meta.btnPermissions`,然后再做判断 这种方式就不展开举例了 #### 方案二 通过自定义指令进行按钮权限的判断 首先配置路由 ```js { path: '/permission', component: Layout, name: '权限测试', meta: { btnPermissions: ['admin', 'supper', 'normal'] }, //页面需要的权限 children: [{ path: 'supper', component: _import('system/supper'), name: '权限测试页', meta: { btnPermissions: ['admin', 'supper'] } //页面需要的权限 }, { path: 'normal', component: _import('system/normal'), name: '权限测试页', meta: { btnPermissions: ['admin'] } //页面需要的权限 }] } ``` 自定义权限鉴定指令 ```js import Vue from 'vue' /**权限指令**/ const has = Vue.directive('has', { bind: function (el, binding, vnode) { // 获取页面按钮权限 let btnPermissionsArr = []; if(binding.value){ // 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较。 btnPermissionsArr = Array.of(binding.value); }else{ // 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较。 btnPermissionsArr = vnode.context.$route.meta.btnPermissions; } if (!Vue.prototype.$_has(btnPermissionsArr)) { el.parentNode.removeChild(el); } } }); // 权限检查方法 Vue.prototype.$_has = function (value) { let isExist = false; // 获取用户按钮权限 let btnPermissionsStr = sessionStorage.getItem("btnPermissions"); if (btnPermissionsStr == undefined || btnPermissionsStr == null) { return false; } if (value.indexOf(btnPermissionsStr) > -1) { isExist = true; } return isExist; }; export {has} ``` 在使用的按钮中只需要引用`v-has`指令 ```js 编辑 ``` ### 小结 关于权限如何选择哪种合适的方案,可以根据自己项目的方案项目,如考虑路由与菜单是否分离 权限需要前后端结合,前端尽可能的去控制,更多的需要后台判断 ## 参考文献 - https://mp.weixin.qq.com/s/b-D2eH1mLwL_FkaZwjueSw - https://segmentfault.com/a/1190000020887109 - https://juejin.cn/post/6844903648057622536#heading-6 ================================================ FILE: docs/vue/show_if.md ================================================ # 面试官:v-show和v-if有什么区别?使用场景分别是什么? ![](https://static.vue-js.com/d21c3c50-3acb-11eb-85f6-6fac77c0c9b3.png) ## 一、v-show与v-if的共同点 我们都知道在 `vue` 中 `v-show ` 与 `v-if` 的作用效果是相同的(不含v-else),都能控制元素在页面是否显示 在用法上也是相同的 ```js ``` - 当表达式为`true`的时候,都会占据页面的位置 - 当表达式都为`false`时,都不会占据页面位置 ## 二、v-show与v-if的区别 - 控制手段不同 - 编译过程不同 - 编译条件不同 控制手段:`v-show`隐藏则是为该元素添加`css--display:none`,`dom`元素依旧还在。`v-if`显示隐藏是将`dom`元素整个添加或删除 编译过程:`v-if`切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;`v-show`只是简单的基于css切换 编译条件:`v-if`是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染 - `v-show` 由`false`变为`true`的时候不会触发组件的生命周期 - `v-if`由`false`变为`true`的时候,触发组件的`beforeCreate`、`create`、`beforeMount`、`mounted`钩子,由`true`变为`false`的时候触发组件的`beforeDestory`、`destoryed`方法 性能消耗:`v-if`有更高的切换消耗;`v-show`有更高的初始渲染消耗; ## 三、v-show与v-if原理分析 具体解析流程这里不展开讲,大致流程如下 - 将模板`template`转为`ast`结构的`JS`对象 - 用`ast`得到的`JS`对象拼装`render`和`staticRenderFns`函数 - `render`和`staticRenderFns`函数被调用后生成虚拟`VNODE`节点,该节点包含创建`DOM`节点所需信息 - `vm.patch`函数通过虚拟`DOM`算法利用`VNODE`节点创建真实`DOM`节点 ### v-show原理 不管初始条件是什么,元素总是会被渲染 我们看一下在`vue`中是如何实现的 代码很好理解,有`transition`就执行`transition`,没有就直接设置`display`属性 ```js // https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts export const vShow: ObjectDirective = { beforeMount(el, { value }, { transition }) { el._vod = el.style.display === 'none' ? '' : el.style.display if (transition && value) { transition.beforeEnter(el) } else { setDisplay(el, value) } }, mounted(el, { value }, { transition }) { if (transition && value) { transition.enter(el) } }, updated(el, { value, oldValue }, { transition }) { // ... }, beforeUnmount(el, { value }) { setDisplay(el, value) } } ``` ### v-if原理 `v-if`在实现上比`v-show`要复杂的多,因为还有`else` `else-if` 等条件需要处理,这里我们也只摘抄源码中处理 `v-if` 的一小部分 返回一个`node`节点,`render`函数通过表达式的值来决定是否生成`DOM` ```js // https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts export const transformIf = createStructuralDirectiveTransform( /^(if|else|else-if)$/, (node, dir, context) => { return processIf(node, dir, context, (ifNode, branch, isRoot) => { // ... return () => { if (isRoot) { ifNode.codegenNode = createCodegenNodeForBranch( branch, key, context ) as IfConditionalExpression } else { // attach this branch's codegen node to the v-if root. const parentCondition = getParentCondition(ifNode.codegenNode!) parentCondition.alternate = createCodegenNodeForBranch( branch, key + ifNode.branches.length - 1, context ) } } }) } ) ``` ## 四、v-show与v-if的使用场景 `v-if` 与 `v-show` 都能控制`dom`元素在页面的显示 `v-if` 相比 `v-show` 开销更大的(直接操作`dom`节点增加与删除) 如果需要非常频繁地切换,则使用 v-show 较好 如果在运行时条件很少改变,则使用 v-if 较好 ## 参考文献 - https://www.jianshu.com/p/7af8554d8f08 - https://juejin.cn/post/6897948855904501768 - https://vue3js/docs/zh ================================================ FILE: docs/vue/slot.md ================================================ # 面试官:说说你对slot的理解?slot使用场景有哪些? ![](https://static.vue-js.com/141ca660-3dbc-11eb-85f6-6fac77c0c9b3.png) ## 一、slot是什么 在HTML中 `slot` 元素 ,作为 `Web Components` 技术套件的一部分,是Web组件内的一个占位符 该占位符可以在后期使用自己的标记语言填充 举个栗子 ```html 1 2 ``` `template`不会展示到页面中,需要用先获取它的引用,然后添加到`DOM`中, ```js customElements.define('element-details', class extends HTMLElement { constructor() { super(); const template = document .getElementById('element-details-template') .content; const shadowRoot = this.attachShadow({mode: 'open'}) .appendChild(template.cloneNode(true)); } }) ``` 在`Vue`中的概念也是如此 `Slot` 艺名插槽,花名“占坑”,我们可以理解为`solt`在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中`slot`位置),作为承载分发内容的出口 可以将其类比为插卡式的FC游戏机,游戏机暴露卡槽(插槽)让用户插入不同的游戏磁条(自定义内容) 放张图感受一下 ![](https://static.vue-js.com/63c0dff0-3dbd-11eb-85f6-6fac77c0c9b3.png) ## 二、使用场景 通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理 如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情 通过`slot`插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用 比如布局组件、表格列、下拉选、弹框显示内容等 ## 三、分类 `slot`可以分来以下三种: - 默认插槽 - 具名插槽 - 作用域插槽 ### 默认插槽 子组件用``标签来确定渲染的位置,标签里面可以放`DOM`结构,当父组件使用的时候没有往插槽传入内容,标签内`DOM`结构就会显示在页面 父组件在使用的时候,直接在子组件的标签内写入内容即可 子组件`Child.vue` ```html ``` 父组件 ```html
    默认插槽
    ``` ### 具名插槽 子组件用`name`属性来表示插槽的名字,不传为默认插槽 父组件中在使用时在默认插槽的基础上加上`slot`属性,值为子组件插槽`name`属性值 子组件`Child.vue` ```html ``` 父组件 ```html ``` ### 作用域插槽 子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件`v-slot`接受的对象上 父组件中在使用时通过`v-slot:`(简写:#)获取子组件的信息,在内容中使用 子组件`Child.vue` ```html ``` 父组件 ```html ``` ### 小结: - `v-slot`属性只能在`