From 8af39a1e885a722f9fbf7c8f43c5e4f19d7ebcc9 Mon Sep 17 00:00:00 2001 From: funjia <13429193897@163.com> Date: Sat, 15 Mar 2025 15:50:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=A8=A1=E6=9D=BF?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=EF=BC=8C=E5=A2=9E=E5=8A=A0=E6=9C=8D=E5=8A=A1?= =?UTF-8?q?=E7=AB=AF=E6=A8=A1=E6=9D=BF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- components.d.ts | 2 + package.json | 3 +- public/logo.png | Bin 7140 -> 0 bytes server/.gitignore | 2 + server/config.example.yml | 22 + server/db/backupDB.json | 346 + server/db/db.json | 21146 +++++ server/db/logDB.json | 72195 ++++++++++++++++ server/db/resourceDB.json | 190 + server/index.mjs | 2 + server/package.json | 32 + server/routers/anki/dataLog.mjs | 15 + server/routers/anki/employee.mjs | 173 + server/routers/anki/notice.mjs | 154 + server/routers/anki/qa.mjs | 16 + server/routers/backupHistory.mjs | 108 + server/routers/dynamic.mjs | 157 + server/routers/upload.mjs | 167 + server/vite.config.js | 16 + server/yarn.lock | 1596 + src/App.vue | 5 + src/assets/logo.png | Bin 6849 -> 0 bytes src/captureObjectList.ts | 4 - src/common/api/index.ts | 67 +- src/components/SystemMenu.vue | 38 +- src/consts/dataDict.ts | 6 + src/containers/CaptureList.vue | 176 - src/containers/HomeHeader.vue | 11 +- src/containers/LicenseUpdate.vue | 117 - src/containers/LogInfo.vue | 152 - src/containers/SystemInfoFormDetail.vue | 154 - src/containers/UpServer.vue | 69 - src/containers/UpgradeSystem.vue | 371 - src/containers/viewModel/CaptureObjectList.ts | 100 - src/containers/viewModel/DataDict.ts | 149 + src/containers/viewModel/Ipc.ts | 216 - src/containers/viewModel/Plugin.ts | 145 + src/containers/viewModel/TravelInformation.ts | 214 - src/containers/viewModel/UpServer.ts | 54 - src/ipcList.ts | 4 - src/router/index.ts | 35 +- src/sett.ts | 4 - src/systemInfo.ts | 4 - src/systemLog.ts | 4 - src/test.js | 50 - src/upServer.ts | 4 - src/views/DataDict.vue | 65 + src/views/DeviceList.vue | 103 - src/views/DynamicTable.vue | 139 + src/views/Employee.vue | 334 +- src/views/Employee/A4.vue | 142 - src/views/Employee/Box1.vue | 118 - src/views/Employee/Box2.vue | 166 - src/views/Employee/Box3.vue | 163 - src/views/Employee/Box4.vue | 166 - src/views/Employee/Watermark.vue | 81 - src/views/Employee/boxType.ts | 36 - src/views/Employee/typs.ts | 23 - src/views/Login.vue | 11 +- .../{CaptureObjectList.vue => Plugin.vue} | 12 +- src/views/Sett.vue | 311 - src/views/SystemInfo.vue | 6 - src/views/SystemLog.vue | 17 - src/views/TravelInformation.vue | 109 - src/views/UpServer.vue | 6 - vite.config.ts | 4 + yarn.lock | 7 + 68 files changed, 96980 insertions(+), 3536 deletions(-) delete mode 100644 public/logo.png create mode 100644 server/.gitignore create mode 100644 server/config.example.yml create mode 100644 server/db/backupDB.json create mode 100644 server/db/db.json create mode 100644 server/db/logDB.json create mode 100644 server/db/resourceDB.json create mode 100644 server/index.mjs create mode 100644 server/package.json create mode 100644 server/routers/anki/dataLog.mjs create mode 100644 server/routers/anki/employee.mjs create mode 100644 server/routers/anki/notice.mjs create mode 100644 server/routers/anki/qa.mjs create mode 100644 server/routers/backupHistory.mjs create mode 100644 server/routers/dynamic.mjs create mode 100644 server/routers/upload.mjs create mode 100644 server/vite.config.js create mode 100644 server/yarn.lock delete mode 100644 src/assets/logo.png delete mode 100644 src/captureObjectList.ts create mode 100644 src/consts/dataDict.ts delete mode 100644 src/containers/CaptureList.vue delete mode 100644 src/containers/LicenseUpdate.vue delete mode 100644 src/containers/LogInfo.vue delete mode 100644 src/containers/SystemInfoFormDetail.vue delete mode 100644 src/containers/UpServer.vue delete mode 100644 src/containers/UpgradeSystem.vue delete mode 100644 src/containers/viewModel/CaptureObjectList.ts create mode 100644 src/containers/viewModel/DataDict.ts delete mode 100644 src/containers/viewModel/Ipc.ts create mode 100644 src/containers/viewModel/Plugin.ts delete mode 100644 src/containers/viewModel/TravelInformation.ts delete mode 100644 src/containers/viewModel/UpServer.ts delete mode 100644 src/ipcList.ts delete mode 100644 src/sett.ts delete mode 100644 src/systemInfo.ts delete mode 100644 src/systemLog.ts delete mode 100644 src/test.js delete mode 100644 src/upServer.ts create mode 100644 src/views/DataDict.vue delete mode 100644 src/views/DeviceList.vue create mode 100644 src/views/DynamicTable.vue delete mode 100644 src/views/Employee/A4.vue delete mode 100644 src/views/Employee/Box1.vue delete mode 100644 src/views/Employee/Box2.vue delete mode 100644 src/views/Employee/Box3.vue delete mode 100644 src/views/Employee/Box4.vue delete mode 100644 src/views/Employee/Watermark.vue delete mode 100644 src/views/Employee/boxType.ts delete mode 100644 src/views/Employee/typs.ts rename src/views/{CaptureObjectList.vue => Plugin.vue} (53%) delete mode 100644 src/views/Sett.vue delete mode 100644 src/views/SystemInfo.vue delete mode 100644 src/views/SystemLog.vue delete mode 100644 src/views/TravelInformation.vue delete mode 100644 src/views/UpServer.vue diff --git a/.gitignore b/.gitignore index a547bf3..54f07af 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,4 @@ dist-ssr *.ntvs* *.njsproj *.sln -*.sw? +*.sw? \ No newline at end of file diff --git a/components.d.ts b/components.d.ts index 3aa815c..096f020 100644 --- a/components.d.ts +++ b/components.d.ts @@ -23,6 +23,8 @@ declare module '@vue/runtime-core' { ElLink: typeof import('element-plus/es')['ElLink'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] + ElRadio: typeof import('element-plus/es')['ElRadio'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRow: typeof import('element-plus/es')['ElRow'] ElTag: typeof import('element-plus/es')['ElTag'] ImgPreview: typeof import('./src/components/ImgPreview.vue')['default'] diff --git a/package.json b/package.json index 7b2bee0..1309625 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "dayjs": "^1.10.7", "element-plus": "2.1.10", "funjialib": "git+http://git.funjia.top/funjia/FunJiaLib_Package.git", - "funjiaui": "git+http://git.funjia.top/funjia/FunjiaUI_Vue.git#v1.1.2", + "funjiaui": "git+http://git.funjia.top/funjia/FunjiaUI_Vue.git#v1.1.2", + "funjia-axios": "git+http://git.funjia.top/funjia/funjia-axios.git", "html-print-element": "^0.0.5", "html2pdf.js": "^0.10.2", "js-cookie": "^3.0.1", diff --git a/public/logo.png b/public/logo.png deleted file mode 100644 index fc51bf7b6abdca338ad64d1a6138a7c747b99d95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7140 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 a!2bh+Z@Tm1B?rL(0000 checkIfInstance(new Date(), Date)\n输出:true\n解释:根据定义,Date 构造函数返回的对象是 Date 的一个实例。\n示例 2:\n\n输入:func = () => { class Animal {}; class Dog extends Animal {}; return checkIfInstance(new Dog(), Animal); }\n输出:true\n解释:\nclass Animal {};\nclass Dog extends Animal {};\ncheckIfInstance(new Dog(), Animal); // true\n\nDog 是 Animal 的子类。因此,Dog 对象同时是 Dog 和 Animal 的实例。\n示例 3:\n\n输入:func = () => checkIfInstance(Date, Date)\n输出:false\n解释:日期的构造函数在逻辑上不能是其自身的实例。\n示例 4:\n\n输入:func = () => checkIfInstance(5, Number)\n输出:true\n解释:5 是一个 Number。注意,\"instanceof\" 关键字将返回 false。\n\n来源:力扣(LeetCode)\n链接:https://leetcode.cn/problems/check-if-object-instance-of-class\n著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处", + "tag": [ + "js" + ] + }, + { + "id": "clhcrtwcq000ckuij2wrsfvi2", + "question": "说说你对vue的理解?", + "answer": "二、vue是什么\nVue.js(/vjuː/,或简称为Vue)是一个用于创建用户界面的开源JavaScript框架,也是一个创建单页应用的Web应用框架。2016年一项针对JavaScript的调查表明,Vue有着89%的开发者满意度。在GitHub上,该项目平均每天能收获95颗星,为Github有史以来星标数第3多的项目同时也是一款流行的JavaScript前端框架,旨在更好地组织与简化Web开发。Vue所关注的核心是MVC模式中的视图层,同时,它也能方便地获取数据更新,并通过组件内部特定的方法实现视图与模型的交互PS: Vue作者尤雨溪是在为AngularJS工作之后开发出了这一框架。他声称自己的思路是提取Angular中为自己所喜欢的部分,构建出一款相当轻量的框架最早发布于2014年2月\n\n三、Vue核心特性\n数据驱动(MVVM)\nMVVM表示的是 Model-View-ViewModel\n\nModel:模型层,负责处理业务逻辑以及和服务器端进行交互\nView:视图层:负责将数据模型转化为UI展示出来,可以简单的理解为HTML页面\nViewModel:视图模型层,用来连接Model和View,是Model和View之间的通信桥梁\n这时候需要一张直观的关系图,如下\nimage.png\n\n组件化\n1.什么是组件化一句话来说就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件2.组件化的优势\n\n降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现\n调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单\n提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级\n指令系统\n解释:指令 (Directives) 是带有 v- 前缀的特殊属性作用:当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM\n\n常用的指令\n\n条件渲染指令 v-if\n列表渲染指令v-for\n属性绑定指令v-bind\n事件绑定指令v-on\n双向数据绑定指令v-model\n没有指令之前我们是怎么做的?是不是先要获取到DOM然后在....干点啥\n\n四、Vue跟传统开发的区别\n没有落地使用场景的革命不是好革命,就以一个高频的应用场景来示意吧注册账号这个需求大家应该很熟悉了,如下\n用jquery来实现大概的思路就是选择流程dom对象,点击按钮隐藏当前活动流程dom对象,显示下一流程dom对象如下图(代码就不上了,上了就篇文章就没了..)\n用vue来实现,我们知道vue基本不操作dom节点, 双向绑定使dom节点跟视图绑定后,通过修改变量的值控制dom节点的各类属性。所以其实现思路为:视图层使用一变量控制dom节点显示与否,点击按钮则改变该变量,如下图\n\n\n\n总结就是:\n\nVue所有的界面事件,都是只去操作数据的,Jquery操作DOM\nVue所有界面的变动,都是根据数据自动绑定出来的,Jquery操作DOM\n五、Vue和React对比\n这里就做几个简单的类比吧,当然没有好坏之分,只是使用场景不同\n\n相同点\n都有组件化思想\n都支持服务器端渲染\n都有Virtual DOM(虚拟dom)\n数据驱动视图\n都有支持native的方案:Vue的weex、React的React native\n都有自己的构建工具:Vue的vue-cli、React的Create React App\n区别\n数据变化的实现原理不同。react使用的是不可变数据,而Vue使用的是可变的数据\n组件化通信的不同。react中我们通过使用回调函数来进行通信的,而Vue中子组件向父组件传递消息有两种方式:事件和回调函数\ndiff算法不同。react主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM。Vue 使用双向指针,边对比,边更新DOM\n\n参考文献\nhttps://segmentfault.com/a/1190000016269636\nhttps://zh.wikipedia.org/zh-cn/Vue.js\nhttps://zhuanlan.zhihu.com/p/20197803\nhttps://zhuanlan.zhihu.com/p/38296857", + "createTime": "2023-05-07T02:04:44.426Z", + "tag": [ + "vue3" + ] + }, + { + "id": "clhcwpu19000ekuijexuf71yd", + "question": "说说你对双向绑定的理解?", + "answer": "一、什么是双向绑定\n我们先从单向绑定切入单向绑定非常简单,就是把Model绑定到View,当我们用JavaScript代码更新Model时,View就会自动更新双向绑定就很容易联想到了,在单向绑定的基础上,用户更新了View,Model的数据也自动被更新了,这种情况就是双向绑定举个栗子\n\n\n\n当用户填写表单时,View的状态就被更新了,如果此时可以自动更新Model的状态,那就相当于我们把Model和View做了双向绑定关系图如下\n\n\n\n二、双向绑定的原理是什么\n我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成\n\n数据层(Model):应用的数据及业务逻辑\n视图层(View):应用的展示效果,各类UI组件\n业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来\n而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理\n\n理解ViewModel\n它的主要职责就是:\n\n数据变化后更新视图\n视图变化后更新数据\n当然,它还有两个主要部分组成\n\n监听器(Observer):对所有数据的属性进行监听\n解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数\n三、实现双向绑定\n我们还是以Vue为例,先来看看Vue中的双向绑定流程是什么的\n\nnew Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe中\n同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在Compile中\n同时定义⼀个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数\n由于data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher\n将来data中数据⼀旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数\n流程图如下:\n\n\n\n实现\n先来一个构造函数:执行初始化,对data执行响应化处理\n\nclass Vue { \n constructor(options) { \n this.$options = options; \n this.$data = options.data; \n \n // 对data选项做响应式处理 \n observe(this.$data); \n \n // 代理data到vm上 \n proxy(this); \n \n // 执行编译 \n new Compile(options.el, this); \n } \n} \n对data选项执行响应化具体操作\n\nfunction observe(obj) { \n if (typeof obj !== \"object\" || obj == null) { \n return; \n } \n new Observer(obj); \n} \n \nclass Observer { \n constructor(value) { \n this.value = value; \n this.walk(value); \n } \n walk(obj) { \n Object.keys(obj).forEach((key) => { \n defineReactive(obj, key, obj[key]); \n }); \n } \n} \n编译Compile\n对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数\n\n\n\nclass Compile { \n constructor(el, vm) { \n this.$vm = vm; \n this.$el = document.querySelector(el); // 获取dom \n if (this.$el) { \n this.compile(this.$el); \n } \n } \n compile(el) { \n const childNodes = el.childNodes; \n Array.from(childNodes).forEach((node) => { // 遍历子元素 \n if (this.isElement(node)) { // 判断是否为节点 \n console.log(\"编译元素\" + node.nodeName); \n } else if (this.isInterpolation(node)) { \n console.log(\"编译插值⽂本\" + node.textContent); // 判断是否为插值文本 {{}} \n } \n if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素 \n this.compile(node); // 对子元素进行递归遍历 \n } \n }); \n } \n isElement(node) { \n return node.nodeType == 1; \n } \n isInterpolation(node) { \n return node.nodeType == 3 && /\\{\\{(.*)\\}\\}/.test(node.textContent); \n } \n} \n \n依赖收集\n视图中会用到data中某key,这称为依赖。同⼀个key可能出现多次,每次都需要收集出来用⼀个Watcher来维护它们,此过程称为依赖收集多个Watcher需要⼀个Dep来管理,需要更新时由Dep统⼀通知\n\n\n\n实现思路\n\ndefineReactive时为每⼀个key创建⼀个Dep实例\n初始化视图时读取某个key,例如name1,创建⼀个watcher1\n由于触发name1的getter方法,便将watcher1添加到name1对应的Dep中\n当name1更新,setter触发时,便可通过对应Dep通知其管理所有Watcher更新\n// 负责更新视图 \nclass Watcher { \n constructor(vm, key, updater) { \n this.vm = vm \n this.key = key \n this.updaterFn = updater \n \n // 创建实例时,把当前实例指定到Dep.target静态属性上 \n Dep.target = this \n // 读一下key,触发get \n vm[key] \n // 置空 \n Dep.target = null \n } \n \n // 未来执行dom更新函数,由dep调用的 \n update() { \n this.updaterFn.call(this.vm, this.vm[this.key]) \n } \n} \n声明Dep\n\nclass Dep { \n constructor() { \n this.deps = []; // 依赖管理 \n } \n addDep(dep) { \n this.deps.push(dep); \n } \n notify() { \n this.deps.forEach((dep) => dep.update()); \n } \n} \n创建watcher时触发getter\n\nclass Watcher { \n constructor(vm, key, updateFn) { \n Dep.target = this; \n this.vm[this.key]; \n Dep.target = null; \n } \n} \n \n依赖收集,创建Dep实例\n\nfunction defineReactive(obj, key, val) { \n this.observe(val); \n const dep = new Dep(); \n Object.defineProperty(obj, key, { \n get() { \n Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例 \n return val; \n }, \n set(newVal) { \n if (newVal === val) return; \n dep.notify(); // 通知dep执行更新方法 \n }, \n }); \n} \n参考文献\nhttps://www.liaoxuefeng.com/wiki/1022910821149312/1109527162256416\nhttps://juejin.cn/post/6844903942254510087#heading-9", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T04:21:32.880Z" + }, + { + "id": "clhcy4emq0002yxijclsg6xzx", + "question": "说说你对SPA(单页应用)的理解? ", + "answer": "一、什么是SPA\nSPA(single-page application),翻译过来就是单页应用SPA是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验在单页应用中,所有必要的代码(HTML、JavaScript和CSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面页面在任何时间点都不会重新加载,也不会将控制转移到其他页面举个例子来讲就是一个杯子,早上装的牛奶,中午装的是开水,晚上装的是茶,我们发现,变的始终是杯子里的内容,而杯子始终是那个杯子结构如下图\n\n\n\n我们熟知的JS框架如react,vue,angular,ember都属于SPA\n\n二、SPA和MPA的区别\n上面大家已经对单页面有所了解了,下面来讲讲多页应用MPA(MultiPage-page application),翻译过来就是多页应用在MPA中,每个页面都是一个主页面,都是独立的当我们在访问另一个页面的时候,都需要重新加载html、css、js文件,公共文件则根据需求按需加载如下图\n\n\n\n单页应用与多页应用的区别\n|\n| 单页面应用(SPA) | 多页面应用(MPA) |\n| :-- | :-- | :-- |\n| 组成 | 一个主页面和多个页面片段 | 多个主页面 |\n| 刷新方式 | 局部刷新 | 整页刷新 |\n| url模式 | 哈希模式 | 历史模式 |\n| SEO搜索引擎优化 | 难实现,可使用SSR方式改善 | 容易实现 |\n| 数据传递 | 容易 | 通过url、cookie、localStorage等传递 |\n| 页面切换 | 速度快,用户体验良好 | 切换加载资源,速度慢,用户体验差 |\n| 维护成本 | 相对容易 | 相对复杂 |\n\n单页应用优缺点\n优点:\n\n具有桌面应用的即时性、网站的可移植性和可访问性\n用户体验好、快,内容的改变不需要重新加载整个页面\n良好的前后端分离,分工更明确\n缺点:\n\n不利于搜索引擎的抓取\n首次渲染速度相对较慢\n三、实现一个SPA\n原理\n监听地址栏中hash变化驱动界面变化\n用pushsate记录浏览器的历史,驱动界面发送变化\n\n\n实现\nhash 模式\n核心通过监听url中的hash来进行路由跳转\n\n// 定义 Router \nclass Router { \n constructor () { \n this.routes = {}; // 存放路由path及callback \n this.currentUrl = ''; \n \n // 监听路由change调用相对应的路由回调 \n window.addEventListener('load', this.refresh, false); \n window.addEventListener('hashchange', this.refresh, false); \n } \n \n route(path, callback){ \n this.routes[path] = callback; \n } \n \n push(path) { \n this.routes[path] && this.routes[path]() \n } \n} \n \n// 使用 router \nwindow.miniRouter = new Router(); \nminiRouter.route('/', () => console.log('page1')) \nminiRouter.route('/page2', () => console.log('page2')) \n \nminiRouter.push('/') // page1 \nminiRouter.push('/page2') // page2 \nhistory模式\nhistory 模式核心借用 HTML5 history api,api 提供了丰富的 router 相关属性先了解一个几个相关的api\n\nhistory.pushState 浏览器历史纪录添加记录\nhistory.replaceState修改浏览器历史纪录中当前纪录\nhistory.popState 当 history 发生变化时触发\n// 定义 Router \nclass Router { \n constructor () { \n this.routes = {}; \n this.listerPopState() \n } \n \n init(path) { \n history.replaceState({path: path}, null, path); \n this.routes[path] && this.routes[path](); \n } \n \n route(path, callback){ \n this.routes[path] = callback; \n } \n \n push(path) { \n history.pushState({path: path}, null, path); \n this.routes[path] && this.routes[path](); \n } \n \n listerPopState () { \n window.addEventListener('popstate' , e => { \n const path = e.state && e.state.path; \n this.routers[path] && this.routers[path]() \n }) \n } \n} \n \n// 使用 Router \n \nwindow.miniRouter = new Router(); \nminiRouter.route('/', ()=> console.log('page1')) \nminiRouter.route('/page2', ()=> console.log('page2')) \n \n// 跳转 \nminiRouter.push('/page2') // page2 \n四、题外话:如何给SPA做SEO\n下面给出基于Vue的SPA如何实现SEO的三种方式\n\nSSR服务端渲染\n将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js\n\n静态化\n目前主流的静态化主要有两种:(1)一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中(2)另外一种是通过WEB服务器的 URL Rewrite的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效果\n\n使用Phantomjs针对爬虫处理\n原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。下面是大致流程图\n\n\n\n参考文献\nhttps://segmentfault.com/a/1190000019623624\nhttps://juejin.cn/post/6844903512107663368\nhttps://www.cnblogs.com/constantince/p/5586851.html", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T05:00:52.371Z" + }, + { + "id": "clhcy8off0004yxije2ox7qvm", + "question": "Vue中的v-show和v-if怎么理解?", + "answer": "一、v-show与v-if的共同点\n我们都知道在 vue 中 v-show 与 v-if 的作用效果是相同的(不含v-else),都能控制元素在页面是否显示\n\n在用法上也是相同的\n\n\n\n当表达式为true的时候,都会占据页面的位置\n当表达式都为false时,都不会占据页面位置\n二、v-show与v-if的区别\n控制手段不同\n编译过程不同\n编译条件不同\n控制手段:v-show隐藏则是为该元素添加css--display:none,dom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除\n\n编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换\n\n编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染\n\nv-show 由false变为true的时候不会触发组件的生命周期\n\nv-if由false变为true的时候,触发组件的beforeCreate、create、beforeMount、mounted钩子,由true变为false的时候触发组件的beforeDestory、destoryed方法\n\n性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;\n\n三、v-show与v-if原理分析\n具体解析流程这里不展开讲,大致流程如下\n\n将模板template转为ast结构的JS对象\n用ast得到的JS对象拼装render和staticRenderFns函数\nrender和staticRenderFns函数被调用后生成虚拟VNODE节点,该节点包含创建DOM节点所需信息\nvm.patch函数通过虚拟DOM算法利用VNODE节点创建真实DOM节点\nv-show原理\n不管初始条件是什么,元素总是会被渲染\n\n我们看一下在vue中是如何实现的\n\n代码很好理解,有transition就执行transition,没有就直接设置display属性\n\n// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts\nexport const vShow: ObjectDirective = {\n beforeMount(el, { value }, { transition }) {\n el._vod = el.style.display === 'none' ? '' : el.style.display\n if (transition && value) {\n transition.beforeEnter(el)\n } else {\n setDisplay(el, value)\n }\n },\n mounted(el, { value }, { transition }) {\n if (transition && value) {\n transition.enter(el)\n }\n },\n updated(el, { value, oldValue }, { transition }) {\n // ...\n },\n beforeUnmount(el, { value }) {\n setDisplay(el, value)\n }\n}\nv-if原理\nv-if在实现上比v-show要复杂的多,因为还有else else-if 等条件需要处理,这里我们也只摘抄源码中处理 v-if 的一小部分\n\n返回一个node节点,render函数通过表达式的值来决定是否生成DOM\n\n// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts\nexport const transformIf = createStructuralDirectiveTransform(\n /^(if|else|else-if)$/,\n (node, dir, context) => {\n return processIf(node, dir, context, (ifNode, branch, isRoot) => {\n // ...\n return () => {\n if (isRoot) {\n ifNode.codegenNode = createCodegenNodeForBranch(\n branch,\n key,\n context\n ) as IfConditionalExpression\n } else {\n // attach this branch's codegen node to the v-if root.\n const parentCondition = getParentCondition(ifNode.codegenNode!)\n parentCondition.alternate = createCodegenNodeForBranch(\n branch,\n key + ifNode.branches.length - 1,\n context\n )\n }\n }\n })\n }\n)\n四、v-show与v-if的使用场景\nv-if 与 v-show 都能控制dom元素在页面的显示\n\nv-if 相比 v-show 开销更大的(直接操作dom节点增加与删除)\n\n如果需要非常频繁地切换,则使用 v-show 较好\n\n如果在运行时条件很少改变,则使用 v-if 较好\n\n参考文献\nhttps://www.jianshu.com/p/7af8554d8f08\nhttps://juejin.cn/post/6897948855904501768\nhttps://vue3js/docs/zh", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T05:04:11.692Z" + }, + { + "id": "clhd1wdvg0006yxijc7sc40pn", + "question": "Vue实例挂载的过程中发生了什么?", + "answer": "在Vue实例挂载之前,Vue会将模板(template)编译成渲染函数(render function),然后创建一个虚拟DOM(Virtual DOM)节点树。接着会将Vue实例的数据(data)和虚拟DOM节点树进行关联,当数据变化时,Vue会通过响应式系统(Reactivity System)自动更新虚拟DOM。最后将虚拟DOM渲染到浏览器中,完成组件的挂载。\n\n具体来说,Vue实例挂载的过程如下:\n\n解析模板 Vue将template解析成AST(Abstract Syntax Tree)抽象语法树。\n编译渲染函数 将AST编译成渲染函数(render function)。\n数据劫持 Vue会对data对象进行数据劫持,以便在数据发生变化时可以及时更新视图。\n创建虚拟DOM 利用渲染函数和数据,Vue会创建虚拟DOM树,即VNode节点树。\n渲染虚拟DOM Vue会将VNode节点渲染成真实的DOM节点,并插入到文档中。此时,组件已经被挂载到页面上了。\n监听数据变化 Vue会通过响应式系统监听data对象的变化,并在数据发生变化时更新视图。\n销毁 当Vue实例被销毁时,Vue会自动注销所有事件监听器和定时器,以及从DOM树中移除组件节点。", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T06:46:36.607Z" + }, + { + "id": "clhd1y19g0008yxijg3gsad2b", + "question": "说说你对Vue生命周期的理解?", + "answer": "一、生命周期是什么\n生命周期(Life Cycle)的概念应用很广泛,特别是在政治、经济、环境、技术、社会等诸多领域经常出现,其基本涵义可以通俗地理解为“从摇篮到坟墓”(Cradle-to-Grave)的整个过程在Vue中实例从创建到销毁的过程就是生命周期,即指从创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程我们可以把组件比喻成工厂里面的一条流水线,每个工人(生命周期)站在各自的岗位,当任务流转到工人身边的时候,工人就开始工作PS:在Vue生命周期钩子会自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())\n\n二、生命周期有哪些\nVue生命周期总共可以分为8个阶段:创建前后, 载入前后,更新前后,销毁前销毁后,以及一些特殊场景的生命周期\n\n生命周期\t描述\nbeforeCreate\t组件实例被创建之初\ncreated\t组件实例已经完全创建\nbeforeMount\t组件挂载之前\nmounted\t组件挂载到实例上去之后\nbeforeUpdate\t组件数据发生变化,更新之前\nupdated\t数据数据更新之后\nbeforeDestroy\t组件实例销毁之前\ndestroyed\t组件实例销毁之后\nactivated\tkeep-alive 缓存的组件激活时\ndeactivated\tkeep-alive 缓存的组件停用时调用\nerrorCaptured\t捕获一个来自子孙组件的错误时被调用\n三、生命周期整体流程\nVue生命周期流程图\n\n\n\n具体分析\nbeforeCreate -> created\n\n初始化vue实例,进行数据观测\ncreated\n\n完成数据观测,属性与方法的运算,watch、event事件回调的配置\n可调用methods中的方法,访问和修改data数据触发响应式渲染dom,可通过computed和watch完成数据计算\n此时vm.$el 并没有被创建\ncreated -> beforeMount\n\n判断是否存在el选项,若不存在则停止编译,直到调用vm.$mount(el)才会继续编译\n优先级:render > template > outerHTML\nvm.el获取到的是挂载DOM的\nbeforeMount\n\n在此阶段可获取到vm.el\n此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项上\nbeforeMount -> mounted\n\n此阶段vm.el完成挂载,vm.$el生成的DOM替换了el选项所对应的DOM\nmounted\n\nvm.el已完成DOM的挂载与渲染,此刻打印vm.$el,发现之前的挂载点及内容已被替换成新的DOM\nbeforeUpdate\n\n更新的数据必须是被渲染在模板上的(el、template、render之一)\n此时view层还未更新\n若在beforeUpdate中再次修改数据,不会再次触发更新方法\nupdated\n\n完成view层的更新\n若在updated中再次修改数据,会再次触发更新方法(beforeUpdate、updated)\nbeforeDestroy\n\n实例被销毁前调用,此时实例属性与方法仍可访问\ndestroyed\n\n完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器\n并不能清除DOM,仅仅销毁实例\n使用场景分析\n\n生命周期\t描述\nbeforeCreate\t执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务\ncreated\t组件初始化完毕,各种数据可以使用,常用于异步数据获取\nbeforeMount\t未执行渲染、更新,dom未创建\nmounted\t初始化结束,dom已创建,可用于获取访问数据和dom元素\nbeforeUpdate\t更新前,可用于获取更新前各种状态\nupdated\t更新后,所有状态已是最新\nbeforeDestroy\t销毁前,可用于一些定时器或订阅的取消\ndestroyed\t组件已销毁,作用同上\n四、题外话:数据请求在created和mouted的区别\ncreated是在组件实例一旦创建完成的时候立刻调用,这时候页面dom节点并未生成mounted是在页面dom节点渲染完毕之后就立刻执行的触发时机上created是比mounted要更早的两者相同点:都能拿到实例对象的属性和方法讨论这个问题本质就是触发的时机,放在mounted请求有可能导致页面闪动(页面dom结构已经生成),但如果在页面加载前完成则不会出现此情况建议:放在create生命周期当中\n\n参考文献\nhttps://juejin.cn/post/6844903811094413320\nhttps://baike.baidu.com/\nhttp://cn.vuejs.org/", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T06:47:53.572Z" + }, + { + "id": "clhd1zfme000ayxijgs6yetbd", + "question": "为什么Vue中的v-if和v-for不建议一起用? ", + "answer": "一、作用\nv-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 true值的时候被渲染\n\nv-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in items 形式的特殊语法,其中 items 是源数据数组或者对象,而 item 则是被迭代的数组元素的别名\n\n在 v-for 的时候,建议设置key值,并且保证每个key值是独一无二的,这便于diff算法进行优化\n\n两者在用法上\n\n\n\n
  • \n {{ item.label }}\n
  • \n二、优先级\nv-if与v-for都是vue模板系统中的指令\n\n在vue模板编译的时候,会将指令系统转化成可执行的render函数\n\n示例\n编写一个p标签,同时使用v-if与 v-for\n\n
    \n

    \n {{ item.title }}\n

    \n
    \n创建vue实例,存放isShow与items数据\n\nconst app = new Vue({\n el: \"#app\",\n data() {\n return {\n items: [\n { title: \"foo\" },\n { title: \"baz\" }]\n }\n },\n computed: {\n isShow() {\n return this.items && this.items.length > 0\n }\n }\n})\n模板指令的代码都会生成在render函数中,通过app.$options.render就能得到渲染函数\n\nƒ anonymous() {\n with (this) { return \n _c('div', { attrs: { \"id\": \"app\" } }, \n _l((items), function (item) \n { return (isShow) ? _c('p', [_v(\"\\n\" + _s(item.title) + \"\\n\")]) : _e() }), 0) }\n}\n_l是vue的列表渲染函数,函数内部都会进行一次if判断\n\n初步得到结论:v-for优先级是比v-if高\n\n再将v-for与v-if置于不同标签\n\n
    \n \n
    \n再输出下render函数\n\nƒ anonymous() {\n with(this){return \n _c('div',{attrs:{\"id\":\"app\"}},\n [(isShow)?[_v(\"\\n\"),\n _l((items),function(item){return _c('p',[_v(_s(item.title))])})]:_e()],2)}\n}\n这时候我们可以看到,v-for与v-if作用在不同标签时候,是先进行判断,再进行列表的渲染\n\n我们再在查看下vue源码\n\n源码位置: \\vue-dev\\src\\compiler\\codegen\\index.js\n\nexport function genElement (el: ASTElement, state: CodegenState): string {\n if (el.parent) {\n el.pre = el.pre || el.parent.pre\n }\n if (el.staticRoot && !el.staticProcessed) {\n return genStatic(el, state)\n } else if (el.once && !el.onceProcessed) {\n return genOnce(el, state)\n } else if (el.for && !el.forProcessed) {\n return genFor(el, state)\n } else if (el.if && !el.ifProcessed) {\n return genIf(el, state)\n } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {\n return genChildren(el, state) || 'void 0'\n } else if (el.tag === 'slot') {\n return genSlot(el, state)\n } else {\n // component or element\n ...\n}\n在进行if判断的时候,v-for是比v-if先进行判断\n\n最终结论:v-for优先级比v-if高\n\n三、注意事项\n永远不要把 v-if 和 v-for 同时用在同一个元素上,带来性能方面的浪费(每次渲染都会先循环再进行条件判断)\n如果避免出现这种情况,则在外层嵌套template(页面渲染不生成dom节点),在这一层进行v-if判断,然后在内部进行v-for循环\n\n如果条件出现在循环内部,可通过计算属性computed提前过滤掉那些不需要显示的项\ncomputed: {\n items: function() {\n return this.list.filter(function (item) {\n return item.isShow\n })\n }\n}", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T06:48:58.838Z" + }, + { + "id": "clhd216a0000cyxij0dkj4lp9", + "question": "SPA(单页应用)首屏加载速度慢怎么解决 ", + "answer": "一、什么是首屏加载\n首屏时间(First Contentful Paint),指的是浏览器从响应用户输入网址地址,到首屏内容渲染完成的时间,此时整个网页不一定要全部渲染完成,但需要展示当前视窗需要的内容\n\n首屏加载可以说是用户体验中最重要的环节\n\n关于计算首屏时间\n利用performance.timing提供的数据:\n\nimage.png\n\n通过DOMContentLoad或者performance来计算出首屏时间\n\n// 方案一:\ndocument.addEventListener('DOMContentLoaded', (event) => {\n console.log('first contentful painting');\n});\n// 方案二:\nperformance.getEntriesByName(\"first-contentful-paint\")[0].startTime\n\n// performance.getEntriesByName(\"first-contentful-paint\")[0]\n// 会返回一个 PerformancePaintTiming的实例,结构如下:\n{\n name: \"first-contentful-paint\",\n entryType: \"paint\",\n startTime: 507.80000002123415,\n duration: 0,\n};\n二、加载慢的原因\n在页面渲染的过程,导致加载速度慢的因素可能如下:\n\n网络延时问题\n资源文件体积是否过大\n资源是否重复发送请求去加载了\n加载脚本的时候,渲染内容堵塞了\n三、解决方案\n常见的几种SPA首屏优化方式\n\n减小入口文件积\n静态资源本地缓存\nUI框架按需加载\n图片资源的压缩\n组件重复打包\n开启GZip压缩\n使用SSR\n减小入口文件体积\n常用的手段是路由懒加载,把不同路由对应的组件分割成不同的代码块,待路由被请求的时候会单独打包路由,使得入口文件变小,加载速度大大增加\n\nimage.png\n\n在vue-router配置路由的时候,采用动态加载路由的形式\n\nroutes:[ \n path: 'Blogs',\n name: 'ShowBlogs',\n component: () => import('./components/ShowBlogs.vue')\n]\n以函数的形式加载路由,这样就可以把各自的路由文件分别打包,只有在解析给定的路由时,才会加载路由组件\n\n静态资源本地缓存\n后端返回资源问题:\n\n采用HTTP缓存,设置Cache-Control,Last-Modified,Etag等响应头\n\n采用Service Worker离线缓存\n\n前端合理利用localStorage\n\nUI框架按需加载\n在日常使用UI框架,例如element-UI、或者antd,我们经常性直接饮用整个UI库\n\nimport ElementUI from 'element-ui'\nVue.use(ElementUI)\n但实际上我用到的组件只有按钮,分页,表格,输入与警告 所以我们要按需引用\n\nimport { Button, Input, Pagination, Table, TableColumn, MessageBox } from 'element-ui';\nVue.use(Button)\nVue.use(Input)\nVue.use(Pagination)\n组件重复打包\n假设A.js文件是一个常用的库,现在有多个路由使用了A.js文件,这就造成了重复下载\n\n解决方案:在webpack的config文件中,修改CommonsChunkPlugin的配置\n\nminChunks: 3\nminChunks为3表示会把使用3次及以上的包抽离出来,放进公共依赖文件,避免了重复加载组件\n\n图片资源的压缩\n图片资源虽然不在编码过程中,但它却是对页面性能影响最大的因素\n\n对于所有的图片资源,我们可以进行适当的压缩\n\n对页面上使用到的icon,可以使用在线字体图标,或者雪碧图,将众多小图标合并到同一张图上,用以减轻http请求压力。\n\n开启GZip压缩\n拆完包之后,我们再用gzip做一下压缩 安装compression-webpack-plugin\n\ncnmp i compression-webpack-plugin -D\n在vue.congig.js中引入并修改webpack配置\n\nconst CompressionPlugin = require('compression-webpack-plugin')\n\nconfigureWebpack: (config) => {\n if (process.env.NODE_ENV === 'production') {\n // 为生产环境修改配置...\n config.mode = 'production'\n return {\n plugins: [new CompressionPlugin({\n test: /\\.js$|\\.html$|\\.css/, //匹配文件名\n threshold: 10240, //对超过10k的数据进行压缩\n deleteOriginalAssets: false //是否删除原文件\n })]\n }\n }\n在服务器我们也要做相应的配置 如果发送请求的浏览器支持gzip,就发送给它gzip格式的文件 我的服务器是用express框架搭建的 只要安装一下compression就能使用\n\nconst compression = require('compression')\napp.use(compression()) // 在其他中间件使用之前调用\n使用SSR\nSSR(Server side ),也就是服务端渲染,组件或页面通过服务器生成html字符串,再发送到浏览器\n\n从头搭建一个服务端渲染是很复杂的,vue应用建议使用Nuxt.js实现服务端渲染\n\n小结:\n减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化 和 页面渲染优化\n\n下图是更为全面的首屏优化的方案\n\nimage.png\n\n大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化\n\n参考文献\nhttps://zhuanlan.zhihu.com/p/88639980?utm_source=wechat_session\nhttps://www.chengrang.com/how-browsers-work.html\nhttps://juejin.cn/post/6844904185264095246\nhttps://vue3js.cn/docs/zh", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T06:50:20.040Z" + }, + { + "id": "clhd3ibzr000eyxijac4xckgj", + "question": "为什么data属性是一个函数而不是一个对象?", + "answer": "面试官:为什么data属性是一个函数而不是一个对象?\nimage.png\n\n一、实例和组件定义data的区别\nvue实例的时候定义data属性既可以是一个对象,也可以是一个函数\n\nconst app = new Vue({\n el:\"#app\",\n // 对象格式\n data:{\n foo:\"foo\"\n },\n // 函数格式\n data(){\n return {\n foo:\"foo\"\n }\n }\n})\n组件中定义data属性,只能是一个函数\n\n如果为组件data直接定义为一个对象\n\nVue.component('component1',{\n template:`
    组件
    `,\n data:{\n foo:\"foo\"\n }\n})\n则会得到警告信息\n\nimage.png\n\n警告说明:返回的data应该是一个函数在每一个组件实例中\n\n二、组件data定义函数与对象的区别\n上面讲到组件data必须是一个函数,不知道大家有没有思考过这是为什么呢?\n\n在我们定义好一个组件的时候,vue最终都会通过Vue.extend()构成组件实例\n\n这里我们模仿组件构造函数,定义data属性,采用对象的形式\n\nfunction Component(){\n \n}\nComponent.prototype.data = {\n\tcount : 0\n}\n创建两个组件实例\n\nconst componentA = new Component()\nconst componentB = new Component()\n修改componentA组件data属性的值,componentB中的值也发生了改变\n\nconsole.log(componentB.data.count) // 0\ncomponentA.data.count = 1\nconsole.log(componentB.data.count) // 1\n产生这样的原因这是两者共用了同一个内存地址,componentA修改的内容,同样对componentB产生了影响\n\n如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同)\n\nfunction Component(){\n\tthis.data = this.data()\n}\nComponent.prototype.data = function (){\n return {\n \t\tcount : 0\n }\n}\n修改componentA组件data属性的值,componentB中的值不受影响\n\nconsole.log(componentB.data.count) // 0\ncomponentA.data.count = 1\nconsole.log(componentB.data.count) // 0\nvue组件可能会有很多个实例,采用函数返回一个全新data形式,使每个实例对象的数据不会受到其他实例对象数据的污染\n\n三、原理分析\n首先可以看看vue初始化data的代码,data的定义可以是函数也可以是对象\n\n源码位置:/vue-dev/src/core/instance/state.js\n\nfunction initData (vm: Component) {\n let data = vm.$options.data\n data = vm._data = typeof data === 'function'\n ? getData(data, vm)\n : data || {}\n ...\n}\ndata既能是object也能是function,那为什么还会出现上文警告呢?\n\n别急,继续看下文\n\n组件在创建的时候,会进行选项的合并\n\n源码位置:/vue-dev/src/core/util/options.js\n\n自定义组件会进入mergeOptions进行选项合并\n\nVue.prototype._init = function (options?: Object) {\n ...\n // merge options\n if (options && options._isComponent) {\n // optimize internal component instantiation\n // since dynamic options merging is pretty slow, and none of the\n // internal component options needs special treatment.\n initInternalComponent(vm, options)\n } else {\n vm.$options = mergeOptions(\n resolveConstructorOptions(vm.constructor),\n options || {},\n vm\n )\n }\n ...\n }\n定义data会进行数据校验\n\n源码位置:/vue-dev/src/core/instance/init.js\n\n这时候vm实例为undefined,进入if判断,若data类型不是function,则出现警告提示\n\nstrats.data = function (\n parentVal: any,\n childVal: any,\n vm?: Component\n): ?Function {\n if (!vm) {\n if (childVal && typeof childVal !== \"function\") {\n process.env.NODE_ENV !== \"production\" &&\n warn(\n 'The \"data\" option should be a function ' +\n \"that returns a per-instance value in component \" +\n \"definitions.\",\n vm\n );\n\n return parentVal;\n }\n return mergeDataOrFn(parentVal, childVal);\n }\n return mergeDataOrFn(parentVal, childVal, vm);\n};\n四、结论\n根实例对象data可以是对象也可以是函数(根实例是单例),不会产生数据污染情况\n组件实例对象data必须为函数,目的是为了防止多个组件实例对象之间共用一个data,产生数据污染。采用函数的形式,initData时会将其作为工厂函数都会返回全新data对象", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T07:31:40.216Z" + }, + { + "id": "clhd3m3qc000gyxij69cgg6zd", + "question": "Vue中给对象添加新属性界面不刷新?", + "answer": "一、直接添加属性的问题\n我们从一个例子开始\n\n定义一个p标签,通过v-for指令进行遍历\n\n然后给botton标签绑定点击事件,我们预期点击按钮时,数据新增一个属性,界面也 新增一行\n\n

    \n {{ value }}\n

    \n\n实例化一个vue实例,定义data属性和methods方法\n\nconst app = new Vue({\n el:\"#app\",\n \tdata:()=>{\n \titem:{\n oldProperty:\"旧属性\"\n }\n },\n methods:{\n addProperty(){\n this.items.newProperty = \"新属性\" // 为items添加新属性\n console.log(this.items) // 输出带有newProperty的items\n }\n }\n})\n点击按钮,发现结果不及预期,数据虽然更新了(console打印出了新属性),但页面并没有更新\n\n二、原理分析\n为什么产生上面的情况呢?\n\n下面来分析一下\n\nvue2是用过Object.defineProperty实现数据响应式\n\nconst obj = {}\nObject.defineProperty(obj, 'foo', {\n get() {\n console.log(`get foo:${val}`);\n return val\n },\n set(newVal) {\n if (newVal !== val) {\n console.log(`set foo:${newVal}`);\n val = newVal\n }\n }\n })\n}\n当我们访问foo属性或者设置foo值的时候都能够触发setter与getter\n\nobj.foo \nobj.foo = 'new'\n但是我们为obj添加新属性的时候,却无法触发事件属性的拦截\n\nobj.bar = '新属性'\n原因是一开始obj的foo属性被设成了响应式数据,而bar是后面新增的属性,并没有通过Object.defineProperty设置成响应式数据\n\n三、解决方案\nVue 不允许在已经创建的实例上动态添加新的响应式属性\n\n若想实现数据与视图同步更新,可采取下面三种解决方案:\n\nVue.set()\nObject.assign()\n$forcecUpdated()\nVue.set()\nVue.set( target, propertyName/index, value )\n\n参数\n\n{Object | Array} target\n{string | number} propertyName/index\n{any} value\n返回值:设置的值\n\n通过Vue.set向响应式对象中添加一个property,并确保这个新 property 同样是响应式的,且触发视图更新\n\n关于Vue.set源码(省略了很多与本节不相关的代码)\n\n源码位置:src\\core\\observer\\index.js\n\nfunction set (target: Array | Object, key: any, val: any): any {\n ...\n defineReactive(ob.value, key, val)\n ob.dep.notify()\n return val\n}\n这里无非再次调用defineReactive方法,实现新增属性的响应式\n\n关于defineReactive方法,内部还是通过Object.defineProperty实现属性拦截\n\n大致代码如下:\n\nfunction defineReactive(obj, key, val) {\n Object.defineProperty(obj, key, {\n get() {\n console.log(`get ${key}:${val}`);\n return val\n },\n set(newVal) {\n if (newVal !== val) {\n console.log(`set ${key}:${newVal}`);\n val = newVal\n }\n }\n })\n}\nObject.assign()\n直接使用Object.assign()添加到对象的新属性不会触发更新\n\n应创建一个新的对象,合并原对象和混入对象的属性\n\nthis.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})\n$forceUpdate\n如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事\n\n$forceUpdate迫使 Vue 实例重新渲染\n\nPS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。\n\n小结\n如果为对象添加少量的新属性,可以直接采用Vue.set()\n\n如果需要为新对象添加大量的新属性,则通过Object.assign()创建新对象\n\n如果你实在不知道怎么操作时,可采取$forceUpdate()进行强制刷新 (不建议)\n\nPS:vue3是用过proxy实现数据响应式的,直接动态添加新属性仍可以实现数据响应式\n\n参考文献\nhttps://cn.vuejs.org/v2/api/#Vue-set\nhttps://vue3js.cn/docs/zh", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T07:34:36.132Z" + }, + { + "id": "clhd3o24y000iyxij1gmt3hni", + "question": "Vue中组件和插件有什么区别?", + "answer": "一、组件是什么\n回顾以前对组件的定义:\n\n组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件\n\n组件的优势\n\n降低整个系统的耦合度,在保持接口不变的情况下,我们可以替换不同的组件快速完成需求,例如输入框,可以替换为日历、时间、范围等组件作具体的实现\n\n调试方便,由于整个系统是通过组件组合起来的,在出现问题的时候,可以用排除法直接移除组件,或者根据报错的组件快速定位问题,之所以能够快速定位,是因为每个组件之间低耦合,职责单一,所以逻辑会比分析整个系统要简单\n\n提高可维护性,由于每个组件的职责单一,并且组件在系统中是被复用的,所以对代码进行优化可获得系统的整体升级\n\n二、插件是什么\n插件通常用来为 Vue 添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:\n\n添加全局方法或者属性。如: vue-custom-element\n添加全局资源:指令/过滤器/过渡等。如 vue-touch\n通过全局混入来添加一些组件选项。如 vue-router\n添加 Vue 实例方法,通过把它们添加到 Vue.prototype 上实现。\n一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router\n三、两者的区别\n两者的区别主要表现在以下几个方面:\n\n编写形式\n注册形式\n使用场景\n编写形式\n编写组件\n编写一个组件,可以有很多方式,我们最常见的就是vue单文件的这种格式,每一个.vue文件我们都可以看成是一个组件\n\nvue文件标准格式\n\n\n\n\n我们还可以通过template属性来编写一个组件,如果组件内容多,我们可以在外部定义template组件内容,如果组件内容并不多,我们可直接写在template属性上\n\n\n\nVue.component('componentA',{ \n template: '#testComponent' \n template: `
    component
    ` // 组件内容少可以通过这种形式\n})\n编写插件\nvue插件的实现应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一个可选的选项对象\n\nMyPlugin.install = function (Vue, options) {\n // 1. 添加全局方法或 property\n Vue.myGlobalMethod = function () {\n // 逻辑...\n }\n\n // 2. 添加全局资源\n Vue.directive('my-directive', {\n bind (el, binding, vnode, oldVnode) {\n // 逻辑...\n }\n ...\n })\n\n // 3. 注入组件选项\n Vue.mixin({\n created: function () {\n // 逻辑...\n }\n ...\n })\n\n // 4. 添加实例方法\n Vue.prototype.$myMethod = function (methodOptions) {\n // 逻辑...\n }\n}\n注册形式\n组件注册\nvue组件注册主要分为全局注册与局部注册\n\n全局注册通过Vue.component方法,第一个参数为组件的名称,第二个参数为传入的配置项\n\nVue.component('my-component-name', { /* ... */ })\n局部注册只需在用到的地方通过components属性注册一个组件\n\nconst component1 = {...} // 定义一个组件\n\nexport default {\n\tcomponents:{\n\t\tcomponent1 // 局部注册\n\t}\n}\n插件注册\n插件的注册通过Vue.use()的方式进行注册(安装),第一个参数为插件的名字,第二个参数是可选择的配置项\n\nVue.use(插件名字,{ /* ... */} )\n注意的是:\n\n注册插件的时候,需要在调用 new Vue() 启动应用之前完成\n\nVue.use会自动阻止多次注册相同插件,只会注册一次\n\n使用场景\n具体的其实在插件是什么章节已经表述了,这里在总结一下\n\n组件 (Component) 是用来构成你的 App 的业务模块,它的目标是 App.vue\n\n插件 (Plugin) 是用来增强你的技术栈的功能模块,它的目标是 Vue 本身\n\n简单来说,插件就是指对Vue的功能的增强或补充\n\n参考文献\nhttps://vue3js.cn/docs/zh", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T07:36:07.378Z" + }, + { + "id": "clhd3soxx000kyxijctqoh8lq", + "question": "Vue组件间通信方式都有哪些?", + "answer": "一、组件间通信的概念\n开始之前,我们把组件间通信这个词进行拆分\n\n组件\n通信\n都知道组件是vue最强大的功能之一,vue中每一个.vue我们都可以视之为一个组件通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的。广义上,任何信息的交通都是通信组件间通信即指组件(.vue)通过某种方式来传递信息以达到某个目的举个栗子我们在使用UI框架中的table组件,可能会往table组件中传入某些数据,这个本质就形成了组件之间的通信\n\n二、组件间通信解决了什么\n在古代,人们通过驿站、飞鸽传书、烽火报警、符号、语言、眼神、触碰等方式进行信息传递,到了今天,随着科技水平的飞速发展,通信基本完全利用有线或无线电完成,相继出现了有线电话、固定电话、无线电话、手机、互联网甚至视频电话等各种通信方式从上面这段话,我们可以看到通信的本质是信息同步,共享回到vue中,每个组件之间的都有独自的作用域,组件间的数据是无法共享的但实际开发工作中我们常常需要让组件之间共享数据,这也是组件通信的目的要让它们互相之间能进行通讯,这样才能构成一个有机的完整系统\n\n二、组件间通信的分类\n组件间通信的分类可以分成以下\n\n父子组件之间的通信\n兄弟组件之间的通信\n祖孙与后代组件之间的通信\n非关系组件间之间的通信\n关系图:\n\n\n\n三、组件间通信的方案\n整理vue中8种常规的通信方案\n\n通过 props 传递\n通过 $emit 触发自定义事件\n使用 ref\nEventBus\n$parent 或$root\nattrs 与 listeners\nProvide 与 Inject\nVuex\nprops传递数据\n\n\n适用场景:父组件传递数据给子组件\n子组件设置props属性,定义接收父组件传递过来的参数\n父组件在使用子组件标签中通过字面量来传递值\nChildren.vue\n\nprops:{ \n // 字符串形式 \n name:String // 接收的类型参数 \n // 对象形式 \n age:{ \n type:Number, // 接收的类型为数值 \n defaule:18, // 默认值为18 \n require:true // age属性必须传递 \n } \n} \nFather.vue组件\n\n \n$emit 触发自定义事件\n适用场景:子组件传递数据给父组件\n子组件通过$emit触发自定义事件,$emit第二个参数为传递的数值\n父组件绑定监听器获取到子组件传递过来的参数\nChilfen.vue\n\nthis.$emit('add', good) \nFather.vue\n\n \nref\n父组件在使用子组件的时候设置ref\n父组件通过设置子组件ref来获取数据\n父组件\n\n \n \nthis.$refs.foo // 获取子组件实例,通过子组件实例我们就能拿到对应的数据 \nEventBus\n使用场景:兄弟组件传值\n创建一个中央时间总线EventBus\n兄弟组件通过$emit触发自定义事件,$emit第二个参数为传递的数值\n另一个兄弟组件通过$on监听自定义事件\nBus.js\n\n// 创建一个中央时间总线类 \nclass Bus { \n constructor() { \n this.callbacks = {}; // 存放事件的名字 \n } \n $on(name, fn) { \n this.callbacks[name] = this.callbacks[name] || []; \n this.callbacks[name].push(fn); \n } \n $emit(name, args) { \n if (this.callbacks[name]) { \n this.callbacks[name].forEach((cb) => cb(args)); \n } \n } \n} \n \n// main.js \nVue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上 \n// 另一种方式 \nVue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能 \nChildren1.vue\n\nthis.$bus.$emit('foo') \nChildren2.vue\n\nthis.$bus.$on('foo', this.handle) \n\nparent 或 root\n通过共同祖辈$parent或者$root搭建通信侨联\n兄弟组件\n\nthis.$parent.on('add',this.add) \n\n另一个兄弟组件\n\nthis.$parent.emit('add') \n\nattrs 与 listeners\n适用场景:祖先传递数据给子孙\n设置批量向下传属性$attrs和 $listeners\n包含了父级作用域中不作为 prop 被识别 (且获取) 的特性绑定 ( class 和 style 除外)。\n可以通过 v-bind=\"$attrs\" 传⼊内部组件\n// child:并未在props中声明foo \n

    {{$attrs.foo}}

    \n \n// parent \n \n// 给Grandson隔代传值,communication/index.vue \n \n \n// Child2做展开 \n \n \n// Grandson使⽤ \n
    \n{{msg}} \n
    \nprovide 与 inject\n在祖先组件定义provide属性,返回传递的值\n在后代组件通过inject接收组件传递过来的值\n祖先组件\n\nprovide(){ \n return { \n foo:'foo' \n } \n} \n后代组件\n\ninject:['foo'] // 获取到祖先组件传递过来的值 \nvuex\n适用场景: 复杂关系的组件数据传递\n\nVuex作用相当于一个用来存储共享变量的容器\n\n\nstate用来存放共享变量的地方\n\ngetter,可以增加一个getter派生状态,(相当于store中的计算属性),用来获得共享变量的值\n\nmutations用来存放修改state的方法。\n\nactions也是用来存放修改state的方法,不过action是在mutations的基础上进行。常用来做一些异步操作\n\n小结\n父子关系的组件数据传递选择 props 与 $emit进行传递,也可选择ref\n兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递\n祖先与后代组件数据传递可选择attrs与listeners或者 Provide与 Inject\n复杂关系的组件数据传递可以通过vuex存放共享的变量\n参考文献\nhttps://juejin.cn/post/6844903990052782094#heading-0\nhttps://zh.wikipedia.org/wiki/\\%E9\\%80\\%9A\\%E4\\%BF\\%A1\nhttps://vue3js.cn/docs/zh", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T07:39:43.558Z" + }, + { + "id": "clhd3u8fm000myxij401y7qsz", + "question": "Vue中的$nextTick有什么作用?", + "answer": "\n\n一、NextTick是什么\n官方对其的定义\n\n在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM\n\n什么意思呢?\n\n我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新\n\n举例一下\n\nHtml结构\n\n
    {{ message }}
    \n构建一个vue实例\n\nconst vm = new Vue({\n el: '#app',\n data: {\n message: '原始值'\n }\n})\n修改message\n\nthis.message = '修改后的值1'\nthis.message = '修改后的值2'\nthis.message = '修改后的值3'\n这时候想获取页面最新的DOM节点,却发现获取到的是旧值\n\nconsole.log(vm.$el.textContent) // 原始值\n这是因为message数据在发现变化的时候,vue并不会立刻去更新Dom,而是将修改数据的操作放在了一个异步操作队列中\n\n如果我们一直修改相同数据,异步操作队列还会进行去重\n\n等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行DOM的更新\n\n为什么要有nexttick\n举个例子\n\n{{num}}\nfor(let i=0; i<100000; i++){\n num = i\n}\n如果没有 nextTick 更新机制,那么 num 每次更新值都会触发视图更新(上面这段代码也就是会更新10万次视图),有了nextTick机制,只需要更新一次,所以nextTick本质是一种优化策略\n\n二、使用场景\n如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()\n\n第一个参数为:回调函数(可以获取最近的DOM结构)\n\n第二个参数为:执行函数上下文\n\n// 修改数据\nvm.message = '修改后的值'\n// DOM 还没有更新\nconsole.log(vm.$el.textContent) // 原始的值\nVue.nextTick(function () {\n // DOM 更新了\n console.log(vm.$el.textContent) // 修改后的值\n})\n组件内使用 vm.$nextTick() 实例方法只需要通过this.$nextTick(),并且回调函数中的 this 将自动绑定到当前的 Vue 实例上\n\nthis.message = '修改后的值'\nconsole.log(this.$el.textContent) // => '原始的值'\nthis.$nextTick(function () {\n console.log(this.$el.textContent) // => '修改后的值'\n})\n$nextTick() 会返回一个 Promise 对象,可以是用async/await完成相同作用的事情\n\nthis.message = '修改后的值'\nconsole.log(this.$el.textContent) // => '原始的值'\nawait this.$nextTick()\nconsole.log(this.$el.textContent) // => '修改后的值'\n三、实现原理\n源码位置:/src/core/util/next-tick.js\n\ncallbacks也就是异步操作队列\n\ncallbacks新增回调函数后又执行了timerFunc函数,pending是用来标识同一个时间只能执行一次\n\nexport function nextTick(cb?: Function, ctx?: Object) {\n let _resolve;\n\n // cb 回调函数会经统一处理压入 callbacks 数组\n callbacks.push(() => {\n if (cb) {\n // 给 cb 回调函数执行加上了 try-catch 错误处理\n try {\n cb.call(ctx);\n } catch (e) {\n handleError(e, ctx, 'nextTick');\n }\n } else if (_resolve) {\n _resolve(ctx);\n }\n });\n\n // 执行异步延迟函数 timerFunc\n if (!pending) {\n pending = true;\n timerFunc();\n }\n\n // 当 nextTick 没有传入函数参数的时候,返回一个 Promise 化的调用\n if (!cb && typeof Promise !== 'undefined') {\n return new Promise(resolve => {\n _resolve = resolve;\n });\n }\n}\ntimerFunc函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有:\n\nPromise.then、MutationObserver、setImmediate、setTimeout\n\n通过上面任意一种方法,进行降级操作\n\nexport let isUsingMicroTask = false\nif (typeof Promise !== 'undefined' && isNative(Promise)) {\n //判断1:是否原生支持Promise\n const p = Promise.resolve()\n timerFunc = () => {\n p.then(flushCallbacks)\n if (isIOS) setTimeout(noop)\n }\n isUsingMicroTask = true\n} else if (!isIE && typeof MutationObserver !== 'undefined' && (\n isNative(MutationObserver) ||\n MutationObserver.toString() === '[object MutationObserverConstructor]'\n)) {\n //判断2:是否原生支持MutationObserver\n let counter = 1\n const observer = new MutationObserver(flushCallbacks)\n const textNode = document.createTextNode(String(counter))\n observer.observe(textNode, {\n characterData: true\n })\n timerFunc = () => {\n counter = (counter + 1) % 2\n textNode.data = String(counter)\n }\n isUsingMicroTask = true\n} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {\n //判断3:是否原生支持setImmediate\n timerFunc = () => {\n setImmediate(flushCallbacks)\n }\n} else {\n //判断4:上面都不行,直接用setTimeout\n timerFunc = () => {\n setTimeout(flushCallbacks, 0)\n }\n}\n无论是微任务还是宏任务,都会放到flushCallbacks使用\n\n这里将callbacks里面的函数复制一份,同时callbacks置空\n\n依次执行callbacks里面的函数\n\nfunction flushCallbacks () {\n pending = false\n const copies = callbacks.slice(0)\n callbacks.length = 0\n for (let i = 0; i < copies.length; i++) {\n copies[i]()\n }\n}\n小结:\n\n把回调函数放入callbacks等待执行\n将执行函数放到微任务或者宏任务中\n事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调\n参考文献\nhttps://juejin.cn/post/6844904147804749832\n", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T07:40:55.475Z" + }, + { + "id": "clhd3vjrr000oyxijgeaogixo", + "question": "说说你对vue的mixin的理解,有什么应用场景?", + "answer": "一、mixin是什么\nMixin是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问mixin类的方法而不必成为其子类\n\nMixin类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承的复杂\n\nVue中的mixin\n先来看一下官方定义\n\nmixin(混入),提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。\n\n本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如data、components、methods 、created、computed等等\n\n我们只要将共用的功能以对象的方式传入 mixins选项中,当组件使用 mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来\n\n在Vue中我们可以局部混入跟全局混入\n\n局部混入\n定义一个mixin对象,有组件options的data、methods属性\n\nvar myMixin = {\n created: function () {\n this.hello()\n },\n methods: {\n hello: function () {\n console.log('hello from mixin!')\n }\n }\n}\n组件通过mixins属性调用mixin对象\n\nVue.component('componentA',{\n mixins: [myMixin]\n})\n该组件在使用的时候,混合了mixin里面的方法,在自动执行create生命钩子,执行hello方法\n\n全局混入\n通过Vue.mixin()进行全局的混入\n\nVue.mixin({\n created: function () {\n console.log(\"全局混入\")\n }\n})\n使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)\n\nPS:全局混入常用于插件的编写\n\n注意事项:\n当组件存在与mixin对象相同的选项的时候,进行递归合并的时候组件的选项会覆盖mixin的选项\n\n但是如果相同选项为生命周期钩子的时候,会合并成一个数组,先执行mixin的钩子,再执行组件的钩子\n\n二、使用场景\n在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立\n\n这时,可以通过Vue的mixin功能将相同或者相似的代码提出来\n\n举个例子\n\n定义一个modal弹窗组件,内部通过isShowing来控制显示\n\nconst Modal = {\n template: '#modal',\n data() {\n return {\n isShowing: false\n }\n },\n methods: {\n toggleShow() {\n this.isShowing = !this.isShowing;\n }\n }\n}\n定义一个tooltip提示框,内部通过isShowing来控制显示\n\nconst Tooltip = {\n template: '#tooltip',\n data() {\n return {\n isShowing: false\n }\n },\n methods: {\n toggleShow() {\n this.isShowing = !this.isShowing;\n }\n }\n}\n通过观察上面两个组件,发现两者的逻辑是相同,代码控制显示也是相同的,这时候mixin就派上用场了\n\n首先抽出共同代码,编写一个mixin\n\nconst toggle = {\n data() {\n return {\n isShowing: false\n }\n },\n methods: {\n toggleShow() {\n this.isShowing = !this.isShowing;\n }\n }\n}\n两个组件在使用上,只需要引入mixin\n\nconst Modal = {\n template: '#modal',\n mixins: [toggle]\n};\n \nconst Tooltip = {\n template: '#tooltip',\n mixins: [toggle]\n}\n通过上面小小的例子,让我们知道了Mixin对于封装一些可复用的功能如此有趣、方便、实用\n\n三、源码分析\n首先从Vue.mixin入手\n\n源码位置:/src/core/global-api/mixin.js\n\nexport function initMixin (Vue: GlobalAPI) {\n Vue.mixin = function (mixin: Object) {\n this.options = mergeOptions(this.options, mixin)\n return this\n }\n}\n主要是调用merOptions方法\n\n源码位置:/src/core/util/options.js\n\nexport function mergeOptions (\n parent: Object,\n child: Object,\n vm?: Component\n): Object {\n\nif (child.mixins) { // 判断有没有mixin 也就是mixin里面挂mixin的情况 有的话递归进行合并\n for (let i = 0, l = child.mixins.length; i < l; i++) {\n parent = mergeOptions(parent, child.mixins[i], vm)\n }\n}\n\n const options = {} \n let key\n for (key in parent) {\n mergeField(key) // 先遍历parent的key 调对应的strats[XXX]方法进行合并\n }\n for (key in child) {\n if (!hasOwn(parent, key)) { // 如果parent已经处理过某个key 就不处理了\n mergeField(key) // 处理child中的key 也就parent中没有处理过的key\n }\n }\n function mergeField (key) {\n const strat = strats[key] || defaultStrat\n options[key] = strat(parent[key], child[key], vm, key) // 根据不同类型的options调用strats中不同的方法进行合并\n }\n return options\n}\n从上面的源码,我们得到以下几点:\n\n优先递归处理 mixins\n先遍历合并parent 中的key,调用mergeField方法进行合并,然后保存在变量options\n再遍历 child,合并补上 parent 中没有的key,调用mergeField方法进行合并,保存在变量options\n通过 mergeField 函数进行了合并\n下面是关于Vue的几种类型的合并策略\n\n替换型\n合并型\n队列型\n叠加型\n替换型\n替换型合并有props、methods、inject、computed\n\nstrats.props =\nstrats.methods =\nstrats.inject =\nstrats.computed = function (\n parentVal: ?Object,\n childVal: ?Object,\n vm?: Component,\n key: string\n): ?Object {\n if (!parentVal) return childVal // 如果parentVal没有值,直接返回childVal\n const ret = Object.create(null) // 创建一个第三方对象 ret\n extend(ret, parentVal) // extend方法实际是把parentVal的属性复制到ret中\n if (childVal) extend(ret, childVal) // 把childVal的属性复制到ret中\n return ret\n}\nstrats.provide = mergeDataOrFn\n同名的props、methods、inject、computed会被后来者代替\n\n合并型\n和并型合并有:data\n\nstrats.data = function(parentVal, childVal, vm) { \n return mergeDataOrFn(\n parentVal, childVal, vm\n )\n};\n\nfunction mergeDataOrFn(parentVal, childVal, vm) { \n return function mergedInstanceDataFn() { \n var childData = childVal.call(vm, vm) // 执行data挂的函数得到对象\n var parentData = parentVal.call(vm, vm) \n if (childData) { \n return mergeData(childData, parentData) // 将2个对象进行合并 \n } else { \n return parentData // 如果没有childData 直接返回parentData\n }\n }\n}\n\nfunction mergeData(to, from) { \n if (!from) return to \n var key, toVal, fromVal; \n var keys = Object.keys(from); \n for (var i = 0; i < keys.length; i++) {\n key = keys[i];\n toVal = to[key];\n fromVal = from[key]; \n // 如果不存在这个属性,就重新设置\n if (!to.hasOwnProperty(key)) {\n set(to, key, fromVal);\n } \n // 存在相同属性,合并对象\n else if (typeof toVal ==\"object\" && typeof fromVal ==\"object\") {\n mergeData(toVal, fromVal);\n }\n } \n return to\n}\nmergeData函数遍历了要合并的 data 的所有属性,然后根据不同情况进行合并:\n\n当目标 data 对象不包含当前属性时,调用 set 方法进行合并(set方法其实就是一些合并重新赋值的方法)\n当目标 data 对象包含当前属性并且当前值为纯对象时,递归合并当前对象值,这样做是为了防止对象存在新增属性\n队列性\n队列性合并有:全部生命周期和watch\n\nfunction mergeHook (\n parentVal: ?Array,\n childVal: ?Function | ?Array\n): ?Array {\n return childVal\n ? parentVal\n ? parentVal.concat(childVal)\n : Array.isArray(childVal)\n ? childVal\n : [childVal]\n : parentVal\n}\n\nLIFECYCLE_HOOKS.forEach(hook => {\n strats[hook] = mergeHook\n})\n\n// watch\nstrats.watch = function (\n parentVal,\n childVal,\n vm,\n key\n) {\n // work around Firefox's Object.prototype.watch...\n if (parentVal === nativeWatch) { parentVal = undefined; }\n if (childVal === nativeWatch) { childVal = undefined; }\n /* istanbul ignore if */\n if (!childVal) { return Object.create(parentVal || null) }\n {\n assertObjectType(key, childVal, vm);\n }\n if (!parentVal) { return childVal }\n var ret = {};\n extend(ret, parentVal);\n for (var key$1 in childVal) {\n var parent = ret[key$1];\n var child = childVal[key$1];\n if (parent && !Array.isArray(parent)) {\n parent = [parent];\n }\n ret[key$1] = parent\n ? parent.concat(child)\n : Array.isArray(child) ? child : [child];\n }\n return ret\n};\n生命周期钩子和watch被合并为一个数组,然后正序遍历一次执行\n\n叠加型\n叠加型合并有:component、directives、filters\n\nstrats.components=\nstrats.directives=\n\nstrats.filters = function mergeAssets(\n parentVal, childVal, vm, key\n) { \n var res = Object.create(parentVal || null); \n if (childVal) { \n for (var key in childVal) {\n res[key] = childVal[key];\n } \n } \n return res\n}\n叠加型主要是通过原型链进行层层的叠加\n\n小结:\n替换型策略有props、methods、inject、computed,就是将新的同名参数替代旧的参数\n合并型策略是data, 通过set方法进行合并和重新赋值\n队列型策略有生命周期函数和watch,原理是将函数存入一个数组,然后正序遍历依次执行\n叠加型有component、directives、filters,通过原型链进行层层的叠加\n参考文献\nhttps://zhuanlan.zhihu.com/p/31018570\nhttps://juejin.cn/post/6844904015495446536#heading-1\nhttps://juejin.cn/post/6844903846775357453\nhttps://vue3js.cn/docs/zh", + "tag": [ + "vue3" + ], + "createTime": "2023-05-07T07:41:56.823Z" + }, + { + "id": "clhd3xdwu000qyxijam519a3m", + "question": "说说你对slot的理解?slot使用场景有哪些?", + "answer": "一、slot是什么\n在HTML中 slot 元素 ,作为 Web Components 技术套件的一部分,是Web组件内的一个占位符\n\n该占位符可以在后期使用自己的标记语言填充\n\n举个栗子\n\n\n\n 1\n\n\n 2\n\ntemplate不会展示到页面中,需要用先获取它的引用,然后添加到DOM中,\n\ncustomElements.define('element-details',\n class extends HTMLElement {\n constructor() {\n super();\n const template = document\n .getElementById('element-details-template')\n .content;\n const shadowRoot = this.attachShadow({mode: 'open'})\n .appendChild(template.cloneNode(true));\n }\n})\n在Vue中的概念也是如此\n\nSlot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出口\n\n可以将其类比为插卡式的FC游戏机,游戏机暴露卡槽(插槽)让用户插入不同的游戏磁条(自定义内容)\n\n放张图感受一下\n\n\n二、使用场景\n通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处理\n\n如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事情\n\n通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用\n\n比如布局组件、表格列、下拉选、弹框显示内容等\n\n三、分类\nslot可以分来以下三种:\n\n默认插槽\n具名插槽\n作用域插槽\n默认插槽\n子组件用标签来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页面\n\n父组件在使用的时候,直接在子组件的标签内写入内容即可\n\n子组件Child.vue\n\n\n父组件\n\n\n
    默认插槽
    \n
    \n具名插槽\n子组件用name属性来表示插槽的名字,不传为默认插槽\n\n父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性值\n\n子组件Child.vue\n\n\n父组件\n\n\n \n \n \n\n作用域插槽\n子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上\n\n父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用\n\n子组件Child.vue\n\n\n父组件\n\n \n \n \n \n\n小结:\nv-slot属性只能在