` `HTML5`属性,效果和`display:none;`相同,但这个属性用于记录一个元素的状态;
255 | - `height: 0;` 将元素高度设为 0 ,并消除边框;
256 | - `filter: blur(0);` `CSS3`属性,括号内的数值越大,图像高斯模糊的程度越大,到达一定程度可使图像消失`(此处感谢小伙伴支持)`;
257 |
258 | ### 21.对浏览器内核的理解?
259 |
260 | 浏览器内核主要分为两个部分: 渲染引擎和js引擎;
261 |
262 | - 渲染引擎: 负责取得页面的内容(`html`,`xml`, 图像等)、整理讯息(加入`css`等)、以及计算网页的显示方式,然后对输出至显示器或者打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不同。所有网页浏览器、电子邮件客户以及其他所需要编辑、显示网络的应用程序都需要内核。
263 | - `JS`引擎: 解析和执行`Javascript`来实现网页的动态效果。
264 |
265 | 最开始渲染引擎和`js`引擎没有明确的区分,后来`js`引擎越来越独立,内核就倾向于只渲染引擎。
266 |
267 | ### 22.制作一个访问量很大的网站,如何管理所有的css文件,js和图片?
268 |
269 | 从人手,分工和同步方面回答:
270 |
271 | - 前期团队必须确认好全局样式,编码模式;
272 | - 代码风格,编写习惯保持一致;
273 | - 标注样式编写人,各模块都要及时标注(标注关键样式调用的地方);
274 | - 对自己负责的页面进行标注;
275 | - `CSS`和`JS`分文件夹存并行存放,命名都要统一;
276 | - `JS`分文件夹存放,明明以该`JS`功能为准的英文翻译;
277 | - 图片采用整合的`.png`格式存放,尽量整合在一起,方便将来管理;
278 |
279 | ### 23.对BFC规范(块级格式化上下文:block formatting context)的理解
280 |
281 | `BFC`规定了内部的`Block Box`如何布局。一个页面是由很多个`Box`组成的,元素的类型和`display`属性,决定了这个`Box`的类型。不同类型的`box`,会参与不同的`Formatting Context`(决定如何渲染文档的容器),因此`Box`内的元素会以不用的方式渲染,也是就是说`BFC`内部的元素和外部的元素不会相互影响。
282 |
283 | 定位方案:
284 |
285 | - 内部的`box`会在垂直方向上一个接一个的放置;
286 | - `box`垂直方向的距离由`margin`决定,属于同一个`BFC`的两个相邻`Box`的`margin`会发生重叠;
287 | - 每个元素`margin box`的左边,与包含块`border box`的左边相接触;
288 | - `BFC`的区域不会与float box重叠;
289 | - `BFC`是页面上的一个隔离的独立容器,容器里面的元素不会影响到外面的元素;
290 | - 计算`BFC`的高度时,浮动元素也会参与计算。
291 |
292 | 满足下列条件之一就可以出发BFC:
293 |
294 | - 根元素变化,即`html`;
295 | - `float`的值不为`none`(默认);
296 | - `overflow`的值不为`visible`(默认);
297 | - `display`的值为`inline-block`, `tabke-cell`,`table-caption`;
298 | - `position`的值为`absolute`或`fixed`;
299 |
300 | ### 24.元素竖向的百分比设定是相对于容器的高度吗?
301 |
302 | 一般来说,子元素的百分比单位都是以父元素为依据。但是`margin`和`padding`例外。元素的`height`是相对于容器的高度,但是元素的`margin`和`padding`是相对于容器的宽度。
303 |
304 | ### 25.经常遇到的浏览器的兼容性有哪些?原因,解决方法是什么,常用hack的技巧 ?
305 |
306 | (1)、问题:`png24`位的图片在`ie`浏览器上出现背景。解决: 做成`png8`;
307 |
308 | (2)、问题:浏览器默认的`margin`和`padding`不同。 解决: 添加一个全局的`*{ margin: 0; padding: 0;}`;
309 |
310 | (3)、问题:`IE`下,可以使用获取常规属性的方法来获取自定义属性,也可以使用`getAttribute()`获取自定义属性,而`Firefox`下,只能使用`getAttribute()`获取自定义属性。 解决: 统一通过`getAttribute()`获取自定义属性;
311 |
312 | (4)、问题: `IE`下,`event`对象有`x`,`y`属性,但是没有`pageX`,`pageY`属性,而`Firefox`下,`event`对象有`pageX`,`pageY`属性,但是没有`x`,`y`属性。 解决: 使用`mX(mX = event.x ? event.x : event.pageX;)`来代替`IE`下的`event.x`或者`Firefox`下的`event.pageX`。
313 |
314 | ### 26.box-sizing 常用的属性有哪些?分别有什么作用?
315 |
316 | - `box-sizing: content-box;` // 默认的标准`(W3C)`盒模型元素效果;
317 | - `box-sizing: border-box;` // 触发怪异`(IE)`盒模型元素的效果;
318 | - `box-sizing: inherit;` // 继承父元素 `box-sizing` 属性的值;
319 |
320 | ### 27. margin和padding分别适合什么场景使用?
321 |
322 | (1)、需要在`border`外侧添加空白且空白处不需要背景(色),或上下相连的两个盒子之间的空白需要相互抵消时,可以使用`margin`;
323 |
324 | (2)、需要在`border`内侧添加空白且空白处需要背景(色),或上下相连的两个盒子之间的空白,希望等于两者之和时,可以使用`padding`。
325 |
326 | ### 28. 怎么让Chrome支持小于12px 的文字?
327 |
328 | ```css
329 | .shrink {
330 | -webkit-transform: scale(0.8);
331 | -o-transform: scale(1);
332 | display: inilne-block;
333 | }
334 | ```
335 |
336 | ### 29. 块级元素、行内元素和空元素定义?
337 |
338 | - **行内元素**:和有他元素都在一行上,高度、行高及外边距和内边距都不可改变,文字图片的宽度不可改变,只能容纳文本或者其他行内元素;
339 | - **块级元素**:总是在新行上开始,高度、行高及外边距和内边距都可控制,可以容纳内敛元素和其他元素;
340 | - **空元素**:在`HTML`元素中,没有内容的 `HTML` 元素被称为空元素。空元素是在开始标签中关闭的。`
` 就是没有关闭标签的空元素。
341 |
342 | ### 30. css sprites是什么?如何使用?
343 |
344 | `css`精灵图,把一堆小的图片整合到一张大的图片(png)上,利用`CSS`的`“background-image”`,`“background- repeat”``,“background-position”`的组合进行背景定位`background-position`可以用数字能精确的定位出背景图片的位置,减轻服务器对图片的请求数量。
345 |
346 | **优点:**
347 |
348 | - 利用`CSS Sprites`能很好地减少网页的http请求,从而大大提高了页面的性能,这也是`CSS Sprites`最大的优点;
349 | - `CSS Sprites`能减少图片的字节,曾经多次比较过,把3张图片合并成1张图片的字节总是小于这3张图片的字节总和。
350 |
351 | **缺点:**
352 |
353 | - 在图片合并时,要把多张图片有序的、合理的合并成一张图片,还要留好足够的空间,防止板块内出现不必要的背景。在宽屏及高分辨率下的自适应页面,如果背景不够宽,很容易出现背景断裂;
354 | - `CSSSprites`在开发的时候相对来说有点麻烦,需要借助`photoshop`或其他工具来对每个背景单元测量其准确的位置。
355 | - 维护方面:`CSS Sprites`在维护的时候比较麻烦,页面背景有少许改动时,就要改这张合并的图片,无需改的地方尽量不要动,这样避免改动更多的`CSS`,如果在原来的地方放不下,又只能(最好)往下加图片,这样图片的字节就增加了,还要改动`CSS`。
356 |
357 | **拓展:** 目前网站开发所用的精灵图(如字体库)一般都是直接用云端,而不是采用这种本地的了,如阿里图标库等
358 |
359 | ### 31. 绝对定位和浮动的区别和应用?
360 |
361 | **绝对定位**: 绝对定位脱离标准文档流,它的参考点是文档的左上角或者是右上角。如果有任何父元素有定位属性,此时就可以参考“**子绝父相**”定律来设置自己的定位参考元素。在网页制作过程中很灵活。制作覆盖效果的时候,会大量使用绝对定位。
362 |
363 | **浮动**: 浮动脱离标准文档流,通常用于制作并排显示的元素,通常用于大的布局,或者无序列表比如图片的并排。可以使用`clear:both`属性让标准流中的其他元素在此之后依次排列。
364 |
365 | ### 32. 简述什么是内容与表现分离?
366 |
367 | 首先对于`html`,`css`以及`javascript`,可以这样理解:
368 |
369 | 把网站理解成一个人,`html`就是构成人体的‘骨架’,`css`就是人体的‘装饰’,比如衣服,饰品等;而`javascript`就相当于人做出的‘动作’,这样就通俗易懂了。
370 |
371 | 对于内容和表现分离,是:尽量不要再`html`中插入行内样式,尽量将css抽成一个独立的模块,实现`html`‘骨架’和样式的分离,利于搜索引擎的同时,也便于后期维护。
372 |
373 | ### 33. CSS怎样判断不同分辨率显示不同宽度布局,从而实现自适应宽度?
374 |
375 | - 使用百分比布局,用百分比来写宽度、`marign`、`padding`;
376 | - 使用`rem`做单位,适当的写`js``让html`根元素的字号随着浏览器宽度的变化而等比例变化;
377 | - 使用媒体查询让不同宽度的浏览器使用不同的样式表。
378 |
379 | ### 34.rem为什么可以缩放,以什么为基准?其优缺点有哪些?
380 |
381 | `rem``以html`的字号为基准,比如`2rem`,而`html`的字号时`16px`,此时`rem`就是`32px`。可以写一段`js`让`html`根元素的字号随着浏览器宽度的变化而等比例变化,此时造成页面等比例缩放的现象。
382 |
383 | **优点**:
384 |
385 | 相对于`em`的好处来说,不会发生逐渐增大或者减小字体尺寸的情况,因为始终集成根元素的字体大小;`rem`单位不仅仅是可应用于字体大小,还可以用于设定高度等其它大小,使页面可以适配不同屏幕尺寸。
386 |
387 | 🍀**注意**: `rem` 一般只用于移动端。
388 |
389 | ### 35. 5条常见的Firefox和IE的脚本兼容的问题。
390 |
391 | - **绑定监听**: `IE`是`attatchEvent()` 、 `firefox`是`addEventListener()`;
392 | - **计算样式**:`IE`是`currentStyle`、 `firefox`是`getComputedSyle`;
393 | - **滚动事件**:`IE`是`MouseWheel`、 `firefox`是`onmousewheel`;
394 | - **事件对象**: `IE`是`window.event`属性, `firefox`必须给事件处理函数注入实参`event`;
395 |
396 | ### 36. :link、:visited、:hover、:active的执行顺序是怎么样的?
397 |
398 | `L-V-H-A`,`l(link)ov(visited)e h(hover)a(active)te`,即用喜欢和讨厌两个词来概括
399 |
400 | ### 37.为什么要语义化以及对于标签语义化的理解?
401 |
402 | 原因: **为了在没有css的情况下,页面也能呈现出很好的内筒结构和代码架构(可以理解为为了裸奔时好看哈哈哈)**。
403 |
404 | 理解:
405 |
406 | - 去掉或者丢失样式的时候能够让页面呈现清晰的结构;
407 | - 有利于`SEO`,可以和搜索引擎建立良好的沟通,有助于爬虫抓取更多的有效信息(爬虫依赖于标签来确定上下文和各个关键字的权重);
408 | - 方便其他设备解析(如屏幕阅读器,盲人阅读器,移动设备等),以意义的方式来渲染网页;
409 | - 便于团队的开发和维护,语义化更具有可读性,遵循`W3C`标准的团队都遵循这个标准,可以减少代码差异化;
410 |
411 | ### 38. 常见的CSS布局有几种?
412 |
413 | 常见的`CSS`布局有: **固定布局**、**流式布局**、**弹性布局**、**浮动布局**、**定位布局**、**margin和padding**。
414 |
415 | ### 39. position的absolute与fixed共同点与不同点?
416 |
417 | 相同点:
418 |
419 | - 改变行内元素的呈现方式,`display`被设置为`block`;
420 | - 让元素脱离普通流,不占据空间;
421 | - 默认会覆盖到非定位元素上;
422 |
423 | 不同点:
424 |
425 | - `absolute`的“根元素”是可以设置的,而`fixed`的“根元素”固定为浏览器窗口;
426 | - 当滚动网页时,`fixed`元素与浏览器窗口之间的距离是不变的。
427 |
428 | ### 40. CSS哪些属性可以继承?哪些属性不可以继承?
429 |
430 | - 可以继承的样式属性: `font-size`、`font-family`、`color`、`list-style`、`cursor`、`ul`, `li`, `dl`, `dd`, `dt`;
431 | - 不可继承的样式属性: `width`、`height`、 `border`、 `padding`、 `margin`、 `background`;
432 |
433 | 为了便于理解,觉得可以大致理解为**字体相关的样式可以继承,与尺寸相关的样式不可继承**。
434 |
435 | ### 41.box-sizing属性?
436 |
437 | 用来控制元素的盒子模型的解析模式,默认为`content-box`。
438 |
439 | - `content-box: W3C`的标准盒子模型,设置元素的`height/width`属性指的是`content`部分的宽/高;
440 | - `border-box`:IE传统盒子模型。设置元素的`height/width`属性指的是`border + padding + content`部分的高/宽;
441 |
442 | ### 42. CSS3动画比基于脚本的动画有哪些优势?
443 |
444 | 跟脚本动画相比,使用`CSS3`动画具有以下优势:
445 |
446 | - 易于使用,任何人都可以在不了解`javascript`的情况下创建它们;
447 | - 即使在合理的系统负载下也能很好的执行。
448 | - 由于简单的动画在`javascript`中的效果比较差,因此渲染引擎使用跳帧技术来使动画流畅进行;
449 | - 允许浏览器控制动画序列,通过在当前不可见的选项卡中执行的动画的更新频率来优化性能和效率;
450 |
451 | ### 43. style标签写在body后与body前有什么区别?
452 |
453 | 一般情况下,页面加载时自上而下的。将`style`标签至于`body`之前,为的是先加载样式。
454 |
455 | 若是写在`body`标签之后,由于浏览器以逐行方式对html文档进行解析,当解析到写在写在文档尾部的样式表时,会导致浏览器停止之前的渲染,等待加载且解析样式表完成之后会重新渲染,在`windows`的`IE`下可能会出现`FOUC`现象(页面闪烁)。
456 |
457 | ### 44. CSS属性overflow属性定义溢出元素内容区的内容会如何处理?
458 |
459 | - 参数是`scroll`的时候,一定会出滚动条;
460 | - 参数是`auto`的时候,子元素内容大于父元素时出现滚动条;
461 | - 参数是`visible`的时候,溢出的内容出现在父元素之外;
462 | - 参数是`hidden`的时候,溢出隐藏;
463 |
464 | ### 45. BFC、IFC、GFC、FFC是什么?
465 |
466 | - `Block formatting context(BFC)`--块级格式化上下文;
467 | - `Inline formatting context(IFC)`--内联格式化上下文;
468 | - `Grid formatting context(GFC)`--网格布局格式化上下文;
469 | - `Flex formatting context(FFC)`--自适应格式化上下文;
470 |
471 | ### 46. css样式引入方式的优缺点对比
472 |
473 | - 内嵌样式: 优点: 方便书写,权重高;缺点: 没有做到结构和样式分离;
474 | - 内联样式: 优点:结构样式相分离; 缺点:没有彻底分离;
475 | - 外联样式: 优点: 完全实现了结构和样式相分离; 缺点: 需要引入才能使用;
476 |
477 | ### 47. position 跟 display、overflow、float 这些特性相互叠加后会怎么样?
478 |
479 | - `display`属性规定元素应该生成的框的类型;
480 | - `position`属性规定元素的定位类型;
481 | - `float`属性是一种布局方式,定义元素往哪个方向浮动;
482 |
483 | **叠加结果**:有点类似于优先机制。`position`的值-- `absolute/fixed`优先级最高,有他们在时,`float`不起作用,`display`值需要调整。`float`或者`absolute`定位的元素,只能是块元素或者表格。
484 |
485 | ### 48.什么是critical CSS?
486 |
487 | `Critical CSS`是一种提取首屏中 `CSS` 的技术,以便尽快将内容呈现给用户。这是快速加载网页首屏的好方法。
488 |
489 | 核心思路:
490 |
491 | (1)、抽取出首页的`CSS`;
492 |
493 | (2)、用行内css样式,加载这部分的`css(critical CSS)`;
494 |
495 | (3)、等到页面加载完之后,再加载整个`css`,会有一部分`css`与`critical css`重叠;
496 |
497 | ### 49. 什么是回流(重排)和重绘以及其区别?
498 |
499 | - 回流(重排),`reflow`:当`render tree`中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变时而需要重新构建;
500 | - 重绘`(repaint`):当`render tree`中的一些元素需要更新属性,而这些属性只影响元素的外观,风格,而不会影响布局时,称其为**重绘**,例如颜色改变等。
501 |
502 | 🍀**注意**:**每个页面至少需要引发一次重排+重绘,而且重排(回流)一定会引发重绘**。
503 |
504 | 触发重排(回流)的条件:
505 |
506 | - 增加或者删除可见的`dom`元素;
507 | - 元素的位置发生了改变;
508 | - 元素的尺寸发生了改变,例如边距,宽高等几何属性改变;
509 | - 内容改变,例如图片大小,字体大小改变等;
510 | - 页面渲染初始化;
511 | - 浏览器窗口尺寸改变,例如`resize`事件发生时等;
512 |
--------------------------------------------------------------------------------
/vue3.x篇/vue3.md:
--------------------------------------------------------------------------------
1 | ## 1.Vue3.0 响应式方面做了哪些改变
2 |
3 | * vue2在初始化的时候,对data中的每个属性使用definepropery调用getter和setter使之变为响应式对象。如果属性值为对象,还会递归调用defineproperty使之变为响应式对象。
4 | * vue3使用proxy对象重写响应式。proxy的性能本来比defineproperty好,proxy可以拦截属性的访问、赋值、删除等操作,不需要初始化的时候遍历所有属性,另外有多层属性嵌套的话,只有访问某个属性的时候,才会递归处理下一级的属性。
5 |
6 | > 优势:
7 | > 可以监听动态新增的属性;
8 | > 可以监听删除的属性 ;
9 | > 可以监听数组的索引和 length 属性;
10 |
11 |
12 |
13 | 2.Vue 3.0 所采用的 Composition Api 与 Vue 2.x使用的Options Api 有什么区别?
14 | --------------------------------------------------------------
15 |
16 | ### Options Api
17 |
18 | > 包含一个描述组件选项(data、methods、props等)的对象 options;
19 | > API开发复杂组件,同一个功能逻辑的代码被拆分到不同选项 ;
20 | > 使用mixin重用公用代码,也有问题:命名冲突,数据来源不清晰;
21 |
22 | ### composition Api
23 |
24 | > vue3 新增的一组 api,它是基于函数的 api,可以更灵活的组织组件的逻辑。
25 | > 解决options api在大型项目中,options api不好拆分和重用的问题。
26 |
27 | 3.Proxy 相对于 Object.defineProperty 有哪些优点?
28 | -----------------------------------------
29 |
30 | proxy的性能本来比defineproperty好,proxy可以拦截属性的访问、赋值、删除等操作,不需要初始化的时候遍历所有属性,另外有多层属性嵌套的话,只有访问某个属性的时候,才会递归处理下一级的属性。
31 |
32 | > 可以监听数组变化
33 | > 可以劫持整个对象
34 | > 操作时不是对原对象操作,是 new Proxy 返回的一个新对象
35 | > 可以劫持的操作有 13 种
36 |
37 | 4.Vue3.0 在编译方面有哪些优化?
38 | ----------------------
39 |
40 | * vue.js 3.x中标记和提升所有的静态节点,diff的时候只需要对比动态节点内容;
41 | * Fragments(升级vetur插件): template中不需要唯一根节点,可以直接放文本或者同级标签
42 | * 静态提升(hoistStatic),当使用 hoistStatic 时,所有静态的节点都被提升到 render 方法之外.只会在应用启动的时候被创建一次,之后使用只需要应用提取的静态节点,随着每次的渲染被不停的复用。
43 | * patch flag, 在动态标签末尾加上相应的标记,只能带 patchFlag 的节点才被认为是动态的元素,会被追踪属性的修改,能快速的找到动态节点,而不用逐个逐层遍历,提高了虚拟dom diff的性能。
44 | * 缓存事件处理函数cacheHandler,避免每次触发都要重新生成全新的function去更新之前的函数
45 | * tree shaking 通过摇树优化核心库体积,减少不必要的代码量
46 |
47 | 5.Vue3.0 响应式api如何实现的
48 | -------------------------
49 |
50 | ### 1. reactive
51 |
52 | 设置对象为响应式对象。接收一个参数,判断这参数是否是对象。不是对象则直接返回这个参数,不做响应式处理。
53 | 创建拦截器handerler,设置get/set/deleteproperty。
54 | **get**
55 | 收集依赖(track);
56 | 如果当前 key 的值是对象,则为当前 key 的对象创建拦截器 handler, 设置 get/set/deleteProperty;
57 | 如果当前的 key 的值不是对象,则返回当前 key 的值。
58 | **set**
59 | 设置的新值和老值不相等时,更新为新值,并触发更新(trigger)。
60 | **deleteProperty**
61 | 当前对象有这个 key 的时候,删除这个 key 并触发更新(trigger)。
62 |
63 | ### 2. effect
64 |
65 | 接收一个函数作为参数。作用是:访问响应式对象属性时去收集依赖
66 |
67 | ### 3. track
68 |
69 | 接收两个参数:target 和 key
70 | -如果没有 activeEffect,则说明没有创建 effect 依赖
71 | -如果有 activeEffect,则去判断 WeakMap 集合中是否有 target 属性
72 | -WeakMap 集合中没有 target 属性,则 set(target, (depsMap = new Map()))
73 | -WeakMap 集合中有 target 属性,则判断 target 属性的 map 值的 depsMap 中是否有 key 属性
74 | -depsMap 中没有 key 属性,则 set(key, (dep = new Set()))
75 | -depsMap 中有 key 属性,则添加这个 activeEffect
76 |
77 | ### 4.trigger
78 |
79 | 判断 WeakMap 中是否有 target 属性,WeakMap 中有 target 属性,则判断 target 属性的 map 值中是否有 key 属性,有的话循环触发收集的 effect()。
80 |
81 | ## 6.Vue2.0 和 Vue3.0 有什么区别?
82 |
83 | 1. 响应式系统的重新配置,使用代理替换对象.define属性,使用代理优势:
84 |
85 | * 可直接监控阵列类型的数据变化
86 | * 监听的目标是对象本身,不需要像Object.defineProperty那样遍历每个属性,有一定的性能提升
87 | * 可拦截应用、拥有密钥、有等13种方法,以及Object.define属性没有办法
88 | * 直接添加对象属性/删除
89 |
90 | 1. 新增组合API,更好的逻辑重用和代码组织
91 | 2. 重构虚拟 DOM
92 |
93 | * 模板编译时间优化,将一些静态节点编译成常量
94 | * slot优化,采取槽编译成懒人功能,拿槽渲染的决定留给子组件
95 | * 在模板中提取和重用内联事件(最初,每次渲染时都会重新生成内联函数)
96 |
97 | 1. 代码结构调整,更方便树摇动,使其更小
98 | 2. 使用打字脚本替换流
99 |
100 | 7.Vue3带来了什么改变?
101 | --------------
102 |
103 | ### 1.性能的提升
104 |
105 | * 打包大小减少41%
106 |
107 | * 初次渲染快55%, 更新渲染快133%
108 |
109 | * 内存减少54%
110 |
111 | ......
112 |
113 | ### 2.源码的升级
114 |
115 | * 使用Proxy代替[defineProperty](https://so.csdn.net/so/search?q=defineProperty&spm=1001.2101.3001.7020)实现响应式
116 |
117 | * 重写[虚拟DOM](https://so.csdn.net/so/search?q=%E8%99%9A%E6%8B%9FDOM&spm=1001.2101.3001.7020)的实现和Tree-Shaking
118 |
119 | ......
120 |
121 | ### 3.拥抱TypeScript
122 |
123 | * Vue3可以更好的支持TypeScript
124 |
125 | ### 4.新的特性
126 |
127 | 1. Composition API(组合API)
128 |
129 | * setup配置
130 |
131 | * ref与reactive
132 |
133 | * watch与watchEffect
134 |
135 | * provide与inject
136 |
137 | * ......
138 |
139 | 2. 新的内置组件
140 |
141 | * Fragment
142 |
143 | * Teleport
144 |
145 | * Suspense
146 |
147 | 3. 其他改变
148 |
149 | * 新的生命周期钩子
150 |
151 | * data 选项应始终被声明为一个函数
152 |
153 | * 移除keyCode支持作为 v-on 的修饰符
154 |
155 | * ......
156 |
157 | 4.vue3还有哪些其他改变?
158 |
159 | * data选项应始终被声明为一个函数。
160 |
161 | * 过度类名的更改:
162 |
163 | * Vue2.x写法
164 |
165 | ```
166 | .v-enter,
167 | .v-leave-to {
168 | opacity: 0;
169 | }
170 | .v-leave,
171 | .v-enter-to {
172 | opacity: 1;
173 | }
174 | ```
175 |
176 | * Vue3.x写法
177 |
178 | ```
179 | .v-enter-from,
180 | .v-leave-to {
181 | opacity: 0;
182 | }
183 |
184 | .v-leave-from,
185 | .v-enter-to {
186 | opacity: 1;
187 | }
188 | ```
189 |
190 | * **移除**keyCode作为 v-on 的修饰符,同时也不再支持`config.keyCodes`
191 |
192 | * **移除**`v-on.native`修饰符
193 |
194 | * 父组件中绑定事件
195 |
196 | ```
197 |
201 | ```
202 |
203 | * 子组件中声明自定义事件
204 |
205 | ```
206 |
211 | ```
212 |
213 | * **移除**过滤器(filter)
214 |
215 | > 过滤器虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是 “只是 JavaScript” 的假设,这不仅有学习成本,而且有实现成本!建议用方法调用或计算属性去替换过滤器。
216 |
217 | * ......
218 |
219 | 8.Vue 生命周期
220 | ----------------------------
221 |
222 |
223 |
224 | **vue2.x的生命周期**
225 |
226 | 
227 |
228 | **vue3.0的生命周期**
229 |
230 | 
231 |
232 | * Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有有两个被更名:
233 |
234 | * `beforeDestroy`改名为 `beforeUnmount`
235 |
236 | * `destroyed`改名为 `unmounted`
237 |
238 | * Vue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:
239 |
240 | * `beforeCreate`===>`setup()`
241 |
242 | * `created`=======>`setup()`
243 |
244 | * `beforeMount` ===>`onBeforeMount`
245 |
246 | * `mounted`=======>`onMounted`
247 |
248 | * `beforeUpdate`===>`onBeforeUpdate`
249 |
250 | * `updated` =======>`onUpdated`
251 |
252 | * `beforeUnmount` ==>`onBeforeUnmount`
253 |
254 | * `unmounted` =====>`onUnmounted`
255 |
256 | 9.Vue3.0中的响应式原理是什么?vue2的响应式原理是什么?
257 | ------------------------------------------------------------------------------------------------------------------------
258 |
259 | ### vue2.x的响应式
260 |
261 | * 实现原理:
262 |
263 | * 对象类型:通过`Object.defineProperty()`对属性的读取、修改进行拦截(数据劫持)。
264 |
265 | * 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
266 |
267 | ```
268 | Object.defineProperty(data, 'count', {
269 | get () {},
270 | set () {}
271 | })
272 | ```
273 |
274 | * 存在问题:
275 |
276 | * 新增属性、删除属性, 界面不会更新。
277 |
278 | * 直接通过下标修改数组, 界面不会自动更新。
279 |
280 | ### Vue3.0的响应式
281 |
282 | * 实现原理:
283 |
284 | * 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
285 |
286 | * 通过Reflect(反射): 对源对象的属性进行操作。
287 |
288 | * MDN文档中描述的Proxy与Reflect:
289 |
290 | * Proxy:[Proxy - JavaScript | MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy "Proxy - JavaScript | MDN")
291 |
292 | * Reflect:[Reflect - JavaScript | MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect "Reflect - JavaScript | MDN")
293 |
294 | ```
295 | new Proxy(data, {
296 | // 拦截读取属性值
297 | get (target, prop) {
298 | return Reflect.get(target, prop)
299 | },
300 | // 拦截设置属性值或添加新属性
301 | set (target, prop, value) {
302 | return Reflect.set(target, prop, value)
303 | },
304 | // 拦截删除属性
305 | deleteProperty (target, prop) {
306 | return Reflect.deleteProperty(target, prop)
307 | }
308 | })
309 |
310 | proxy.name = 'tom' 
311 | ```
312 |
313 | 10.vue3响应式数据的判断?
314 | ---------------
315 |
316 | * isRef: 检查一个值是否为一个 ref 对象
317 |
318 | * isReactive: 检查一个对象是否是由 `reactive` 创建的响应式代理
319 |
320 | * isReadonly: 检查一个对象是否是由 `readonly` 创建的只读代理
321 |
322 | * isProxy: 检查一个对象是否是由 `reactive` 或者 `readonly` 方法创建的代理
323 |
324 | ## 11.vue3的常用 Composition API有哪些?
325 |
326 | 官方文档: [介绍 | Vue.js](https://v3.cn.vuejs.org/guide/composition-api-introduction.html "介绍 | Vue.js")
327 |
328 | 1.拉开序幕的setup
329 | ------------
330 |
331 | 1. 理解:Vue3.0中一个新的配置项,值为一个函数。
332 |
333 | 2. setup是所有**Composition API(组合API)**_“ 表演的舞台 ”_。
334 |
335 | 3. 组件中所用到的:数据、方法等等,均要配置在setup中。
336 |
337 | 4. setup函数的两种返回值:
338 |
339 | 1. 若返回一个对象,则对象中的属性、方法, 在模板中均可以直接使用。(重点关注!)
340 |
341 | 2. 若返回一个渲染函数:则可以自定义渲染内容。(了解)
342 |
343 | 5.setup的几个注意点
344 |
345 | * setup执行的时机
346 |
347 | * 在beforeCreate之前执行一次,this是undefined。
348 |
349 | * setup的参数
350 |
351 | * props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。
352 |
353 | * context:上下文对象
354 |
355 | * attrs: 值为对象,包含:组件外部传递过来,但没有在props配置中声明的属性, 相当于 `this.$attrs`。
356 |
357 | * slots: 收到的插槽内容, 相当于 `this.$slots`。
358 |
359 | * emit: 分发自定义事件的函数, 相当于 `this.$emit`。
360 |
361 | * 尽量不要与Vue2.x配置混用
362 |
363 | * Vue2.x配置(data、methos、computed...)中**可以访问到**setup中的属性、方法。
364 |
365 | * 但在setup中**不能访问到**Vue2.x配置(data、methos、computed...)。
366 |
367 | * 如果有重名, setup优先。
368 |
369 | * setup不能是一个async函数,因为返回值不再是return的对象, 而是promise, 模板看不到return对象中的属性。(后期也可以返回一个Promise实例,但需要Suspense和异步组件的配合)
370 |
371 | 2.ref函数
372 | -------
373 |
374 | * 作用: 定义一个响应式的数据
375 |
376 | * 语法: `const xxx = ref(initValue)`
377 |
378 | * 创建一个包含响应式数据的**引用对象(reference对象,简称ref对象)**。
379 |
380 | * JS中操作数据: `xxx.value`
381 |
382 | * 模板中读取数据: 不需要.value,直接:`
{{xxx}}
`
383 |
384 | * 备注:
385 |
386 | * 接收的数据可以是:基本类型、也可以是对象类型。
387 |
388 | * 基本类型的数据:响应式依然是靠`Object.defineProperty()`的`get`与`set`完成的。
389 |
390 | * 对象类型的数据:内部 _“ 求助 ”_ 了Vue3.0中的一个新函数—— `reactive`函数。
391 |
392 | 3.reactive函数
393 | ------------
394 |
395 | * 作用: 定义一个**对象类型**的响应式数据(基本类型不要用它,要用`ref`函数)
396 |
397 | * 语法:`const 代理对象= reactive(源对象)`接收一个对象(或数组),返回一个**代理对象(Proxy的实例对象,简称proxy对象)**
398 |
399 | * reactive定义的响应式数据是“深层次的”。
400 |
401 | * 内部基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据进行操作。
402 |
403 | 4.reactive对比ref
404 | ---------------
405 |
406 | * 从定义数据角度对比:
407 |
408 | * ref用来定义:**基本类型数据**。
409 |
410 | * reactive用来定义:**对象(或数组)类型数据**。
411 |
412 | * 备注:ref也可以用来定义**对象(或数组)类型数据**, 它内部会自动通过`reactive`转为**代理对象**。
413 |
414 | * 从原理角度对比:
415 |
416 | * ref通过`Object.defineProperty()`的`get`与`set`来实现响应式(数据劫持)。
417 |
418 | * reactive通过使用**Proxy**来实现响应式(数据劫持), 并通过**Reflect**操作**源对象**内部的数据。
419 |
420 | * 从使用角度对比:
421 |
422 | * ref定义的数据:操作数据**需要**`.value`,读取数据时模板中直接读取**不需要**`.value`。
423 |
424 | * reactive定义的数据:操作数据与读取数据:**均不需要**`.value`。
425 |
426 | 5.计算属性与监视
427 | ---------
428 |
429 | ### 1.computed函数
430 |
431 | * 与Vue2.x中computed配置功能一致
432 |
433 | * 写法
434 |
435 | ```
436 | import {computed} from 'vue'
437 |
438 | setup(){
439 | ...
440 | //计算属性——简写
441 | let fullName = computed(()=>{
442 | return person.firstName + '-' + person.lastName
443 | })
444 | //计算属性——完整
445 | let fullName = computed({
446 | get(){
447 | return person.firstName + '-' + person.lastName
448 | },
449 | set(value){
450 | const nameArr = value.split('-')
451 | person.firstName = nameArr[0]
452 | person.lastName = nameArr[1]
453 | }
454 | })
455 | }
456 | ```
457 |
458 | ### 2.watch函数
459 |
460 | * 与Vue2.x中watch配置功能一致
461 |
462 | * 两个小“坑”:
463 |
464 | * 监视reactive定义的响应式数据时:oldValue无法正确获取、强制开启了深度监视(deep配置失效)。
465 |
466 | * 监视reactive定义的响应式数据中某个属性时:deep配置有效。
467 |
468 | ```
469 | //情况一:监视ref定义的响应式数据
470 | watch(sum,(newValue,oldValue)=>{
471 | console.log('sum变化了',newValue,oldValue)
472 | },{immediate:true})
473 |
474 | //情况二:监视多个ref定义的响应式数据
475 | watch([sum,msg],(newValue,oldValue)=>{
476 | console.log('sum或msg变化了',newValue,oldValue)
477 | })
478 |
479 | /* 情况三:监视reactive定义的响应式数据
480 | 若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue!!
481 | 若watch监视的是reactive定义的响应式数据,则强制开启了深度监视
482 | */
483 | watch(person,(newValue,oldValue)=>{
484 | console.log('person变化了',newValue,oldValue)
485 | },{immediate:true,deep:false}) //此处的deep配置不再奏效
486 |
487 | //情况四:监视reactive定义的响应式数据中的某个属性
488 | watch(()=>person.job,(newValue,oldValue)=>{
489 | console.log('person的job变化了',newValue,oldValue)
490 | },{immediate:true,deep:true})
491 |
492 | //情况五:监视reactive定义的响应式数据中的某些属性
493 | watch([()=>person.job,()=>person.name],(newValue,oldValue)=>{
494 | console.log('person的job变化了',newValue,oldValue)
495 | },{immediate:true,deep:true})
496 |
497 | //特殊情况
498 | watch(()=>person.job,(newValue,oldValue)=>{
499 | console.log('person的job变化了',newValue,oldValue)
500 | },{deep:true}) //此处由于监视的是reactive素定义的对象中的某个属性,所以deep配置有效
501 | ```
502 |
503 | ### 3.watchEffect函数
504 |
505 | * watch的套路是:既要指明监视的属性,也要指明监视的回调。
506 |
507 | * watchEffect的套路是:不用指明监视哪个属性,监视的回调中用到哪个属性,那就监视哪个属性。
508 |
509 | * watchEffect有点像computed:
510 |
511 | * 但computed注重的计算出来的值(回调函数的返回值),所以必须要写返回值。
512 |
513 | * 而watchEffect更注重的是过程(回调函数的函数体),所以不用写返回值。
514 |
515 | ```
516 | //watchEffect所指定的回调中用到的数据只要发生变化,则直接重新执行回调。
517 | watchEffect(()=>{
518 | const x1 = sum.value
519 | const x2 = person.age
520 | console.log('watchEffect配置的回调执行了')
521 | })
522 | ```
523 |
524 | ## 12.toRef 代表什么意思
525 |
526 | * 作用:创建一个 ref 对象,其value值指向另一个对象中的某个属性。
527 |
528 | * 语法:`const name = toRef(person,'name')`
529 |
530 | * 应用: 要将响应式对象中的某个属性单独提供给外部使用时。
531 |
532 | * 扩展:`toRefs` 与`toRef`功能一致,但可以批量创建多个 ref 对象,语法:`toRefs(person)`
533 |
534 | ## 13.toRaw 代表什么意思
535 |
536 | * 作用:将一个由`reactive`生成的**响应式对象**转为**普通对象**。
537 |
538 | * 使用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新。
539 |
540 | ## 14.provide 与 inject
541 |
542 | 
543 |
544 | * 作用:实现**祖与后代组件间**通信
545 |
546 | * 套路:父组件有一个 `provide` 选项来提供数据,后代组件有一个 `inject` 选项来开始使用这些数据
547 |
548 | * 具体写法:
549 |
550 | 1. 祖组件中:
551 |
552 | ```
553 | setup(){
554 | ......
555 | let car = reactive({name:'奔驰',price:'40万'})
556 | provide('car',car)
557 | ......
558 | }
559 | ```
560 |
561 | 2. 后代组件中:
562 |
563 | ```
564 | setup(props,context){
565 | ......
566 | const car = inject('car')
567 | return {car}
568 | ......
569 | }
570 | ```
571 |
572 | 15、vue3为什么要添加新的Composition API,它可以解决哪些问题
573 | -----------------------------
574 |
575 | 在 Vue2.0 中,随着功能的增加,组件越来越复杂,维护起来也越来越难,而难以维护的根本原因是 Vue 的 API 设计迫使开发者使用监视、计算、方法 Option 组织代码,而不是实际的业务逻辑。
576 |
577 | 另外 Vue2.0 缺乏一个简单而低成本的机制来完成逻辑重用,虽然你可以 minxis 完全重用逻辑,但是当 mixin 更多的时候,就使得很难找到相应的数据,计算出来也许是从中 mixin 的方法,使得类型推断变得困难。
578 |
579 | 因此组合API外观,主要是解决选项API带来的问题,首先是代码组织,组合API开发者可以根据业务逻辑组织自己的代码,让代码更具可读性和可扩展性,也就是说,当下一个开发者接触到这段不是自己写的代码, 他可以更好地利用代码的组织来反转实际的业务逻辑,或者根据业务逻辑更好地理解代码。
580 |
581 | 二是实现代码的逻辑提取和重用,当然mixin逻辑提取和重用也可以实现,但就像我之前说的,多个mixin在作用于同一个组件时,很难看出mixin的属性,来源不明确,另外,多个mixin的属性存在变量命名冲突的风险。而 Composition API 恰恰解决了这两个问题。
582 |
583 | 16、什么是hook?什么是自定义hook函数?
584 | -----------------------
585 |
586 | * 什么是hook?—— 本质是一个函数,把setup函数中使用的Composition API进行了封装。
587 |
588 | * 类似于vue2.x中的mixin。
589 |
590 | * 自定义hook的优势: 复用代码, 让setup中的逻辑更清楚易懂。
591 |
592 | 17、都说 Composition API 和 React Hook 很像,请问他们的区别是什么?
593 | ------------------------------------------------
594 |
595 | 从 React Hook 从实现的角度来看,React Hook 是基于 useState 的调用顺序来确定下一个 re 渲染时间状态从哪个 useState 开始,所以有以下几个限制
596 |
597 | * 不在循环中、条件、调用嵌套函数 Hook
598 | * 你必须确保它总是在你这边 React Top level 调用函数 Hook
599 | * 使用效果、使用备忘录 依赖关系必须手动确定
600 |
601 | 和 Composition API 是基于 Vue 的响应系统,和 React Hook 相比
602 |
603 | * 在设置函数中,一个组件实例只调用一次设置,而 React Hook 每次重新渲染时,都需要调用 Hook,给 React 带来的 GC 比 Vue 更大的压力,性能也相对 Vue 对我来说也比较慢
604 | * Compositon API 你不必担心调用的顺序,它也可以在循环中、条件、在嵌套函数中使用
605 | * 响应式系统自动实现依赖关系收集,而且组件的性能优化是由 Vue 内部完成的,而 React Hook 的依赖关系需要手动传递,并且依赖关系的顺序必须得到保证,让路 useEffect、useMemo 等等,否则组件性能会因为依赖关系不正确而下降。
606 |
607 | 虽然Compoliton API看起来像React Hook来使用,但它的设计思路也是React Hook的参考。
608 |
609 | ## 18、Options API 存在的问题是什么?Composition API 的优势有哪些?
610 |
611 | ### 1.Options API 存在的问题
612 |
613 | 使用传统OptionsAPI中,新增或者修改一个需求,就需要分别在data,methods,computed里修改 。
614 |
615 |
616 |
617 | 
618 |
619 | 
620 |
621 | ### 2.Composition API 的优势
622 |
623 | 我们可以更加优雅的组织我们的代码,函数。让相关功能的代码更加有序的组织在一起。
624 |
625 |
626 |
627 | 
628 |
629 | 
630 |
631 | ## 19、vue3有哪些新的组件?
632 |
633 | ### 1.Fragment
634 |
635 | * 在Vue2中: 组件必须有一个根标签
636 |
637 | * 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
638 |
639 | * 好处: 减少标签层级, 减小内存占用
640 |
641 | ### 2.Teleport
642 |
643 | * 什么是Teleport?—— `Teleport` 是一种能够将我们的**组件html结构**移动到指定位置的技术。
644 |
645 | ```
646 |
647 |
648 |
649 |
我是一个弹窗
650 |
651 |
652 |
653 |
654 | ```
655 |
656 | ### 3.Suspense
657 |
658 | * 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
659 |
660 | * 使用步骤:
661 |
662 | * 异步引入组件
663 |
664 | ```
665 | import {defineAsyncComponent} from 'vue'
666 | const Child = defineAsyncComponent(()=>import('./components/Child.vue'))
667 | ```
668 |
669 | * 使用`Suspense`包裹组件,并配置好`default` 与 `fallback`
670 |
671 | ```
672 |
673 |
674 |
我是App组件
675 |
676 |
677 |
678 |
679 |
680 | 加载中.....
681 |
682 |
683 |
684 |
685 | ```
686 |
687 | 20.vue2和vue3的全局 API 和配置区别?
688 | --------------------------
689 |
690 | * Vue 2.x 有许多全局 API 和配置。
691 |
692 | * 例如:注册全局组件、注册全局指令等。
693 |
694 | ```
695 | //注册全局组件
696 | Vue.component('MyButton', {
697 | data: () => ({
698 | count: 0
699 | }),
700 | template: '
'
701 | })
702 |
703 | //注册全局指令
704 | Vue.directive('focus', {
705 | inserted: el => el.focus()
706 | }
707 | ```
708 |
709 | * Vue3.0中对这些API做出了调整:全局API的转移
710 |
711 | * 将全局的API,即:`Vue.xxx`调整到应用实例(`app`)上
712 |
713 |
2.x 全局 API(Vue) | 3.x 实例 API (app) |
|---|
| Vue.config.xxxx | app.config.xxxx |
| Vue.config.productionTip | 移除 |
| Vue.component | app.component |
| Vue.directive | app.directive |
| Vue.mixin | app.mixin |
| Vue.use | app.use |
| Vue.prototype | app.config.globalProperties |
714 |
715 |
--------------------------------------------------------------------------------
/vue2.x篇/vue2.md:
--------------------------------------------------------------------------------
1 | # vue篇
2 |
3 | 一、`MVVM`原理
4 | ----------
5 |
6 | 在`Vue2`官方文档中没有找到`Vue`是`MVVM`的直接证据,但文档有提到:虽然没有完全遵循`MVVM模型`,但是 Vue 的设计也受到了它的启发,因此在文档中经常会使用`vm`(ViewModel 的缩写) 这个变量名表示 Vue 实例。
7 |
8 | 为了感受`MVVM模型`的启发,我简单列举下其概念。
9 |
10 | MVVM是Model-View-ViewModel的简写,由三部分构成:
11 |
12 | * Model: 模型持有所有的数据、状态和程序逻辑
13 | * View: 负责界面的布局和显示
14 | * ViewModel:负责模型和界面之间的交互,是Model和View的桥梁
15 |
16 | 二、`SPA`单页面应用
17 | ------------
18 |
19 | 单页Web应用(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML页面并在用户与应用程序交互时动态更新该页面的Web应用程序。我们开发的`Vue`项目大多是借助个官方的`CLI`脚手架,快速搭建项目,直接通过`new Vue`构建一个实例,并将`el:'#app'`挂载参数传入,最后通过`npm run build`的方式打包后生成一个`index.html`,称这种只有一个`HTML`的页面为单页面应用。
20 |
21 | 当然,`vue`也可以像`jq`一样引入,作为多页面应用的基础框架。
22 |
23 | 三、`Vue`的特点
24 | ----------
25 |
26 | * 清晰的官方文档和好用的`api`,比较容易上手。
27 | * 是一套用于构建用户界面的**渐进式框架**,将注意力集中保持在核心库,而将其他功能如路由和全局状态管理交给相关的库。
28 | * 使用 **Virtual DOM**。
29 | * 提供了**响应式** (Reactive) 和**组件化** (Composable) 的视图组件。
30 |
31 | 四、`Vue`的构建入口
32 | ------------
33 |
34 | vue使用过程中可以采用以下两种方式:
35 |
36 | * 在vue脚手架中直接使用,参考文档:`https://cn.vuejs.org/v2/guide/installation.html`
37 | * 或者在html文件的头部通过静态文件的方式引入: ``
38 |
39 | 那么问题来了,使用的或者引入的到底是什么?
40 | 答:引入的是已经打包好的vue.js文件,通过rollup构建打包所得。
41 |
42 | 构建入口在哪里?
43 | 答:在`vue`源码的package.json文件中:
44 |
45 | ```
46 | "scripts": {
47 | // ...
48 | "build": "node scripts/build.js",
49 | "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
50 | "build:weex": "npm run build -- weex",
51 | // ...
52 | },
53 | 复制代码
54 | ```
55 |
56 | 通过执行npm run build的时候,会进行scripts/build.js文件的执行,npm run build:ssr和npm run build:weex的时候,将ssr和weex作为参数传入,按照参数构建出不一样的vue.js打包文件。
57 |
58 | 所以说,`vue`中的`package.json`文件就是构建的入口,具体构建流程可以参考[vue2入口:构建入口](https://juejin.cn/post/7128225931293884452 "https://juejin.cn/post/7128225931293884452")。
59 |
60 | 五、对`import Vue from "vue"`的理解
61 | -----------------------------
62 |
63 | 在使用脚手架开发项目时,会有一行代码`import Vue from "vue"`,那么这个`Vue`指的是什么。
64 | 答:一个构造函数。
65 |
66 | ```
67 | function Vue (options) {
68 | if (process.env.NODE_ENV !== 'production' &&
69 | !(this instanceof Vue)
70 | ) {
71 | warn('Vue is a constructor and should be called with the `new` keyword')
72 | }
73 | this._init(options)
74 | }
75 | initMixin(Vue)
76 | stateMixin(Vue)
77 | eventsMixin(Vue)
78 | lifecycleMixin(Vue)
79 | renderMixin(Vue)
80 | 复制代码
81 | ```
82 |
83 | 我们开发中引入的`Vue`其实就是这个构造函数,而且这个构造函数只能通过`new Vue`的方式进行使用,否则会在控制台打印警告信息。定义完后,还会通过`initMixin(Vue)`、`stateMixin(Vue)`、`eventsMixin(Vue)`、`lifecycleMixin(Vue)`和`renderMixin(Vue)`的方式为`Vue`原型中混入方法。我们通过`import Vue from "Vue"`引入的本质上就是一个原型上挂在了好多方法的构造函数。
84 |
85 | 六、对`new Vue`的理解
86 | ---------------
87 |
88 | ```
89 | // main.js文件
90 | import Vue from "vue";
91 | var app = new Vue({
92 | el: '#app',
93 | data() {
94 | return {
95 | msg: 'hello Vue~'
96 | }
97 | },
98 | template: `
{{msg}}
`,
99 | })
100 |
101 | console.log(app);
102 | 复制代码
103 | ```
104 |
105 | `new Vue`就是对构造函数`Vue`进行实例化,执行结果如下:
106 |
107 | 
108 |
109 | 可以看出实例化后的实例中包含了很多属性,用来对当前`app`进行描述,当然复杂的`Vue`项目这个`app`将会是一个树结构,通过`$parent`和`$children`维护父子关系。
110 |
111 | `new Vue`的过程中还会执行`this._init`方法进行初始化处理。
112 |
113 | 七、`编译`
114 | ------
115 |
116 | 虚拟`DOM`的生成必须通过`render`函数实现,`render`函数的产生是在编译阶段完成,核心代码如下:
117 |
118 | ```
119 | export const createCompiler = createCompilerCreator(function baseCompile (
120 | template: string,
121 | options: CompilerOptions
122 | ): CompiledResult {
123 | const ast = parse(template.trim(), options)
124 | if (options.optimize !== false) {
125 | optimize(ast, options)
126 | }
127 | const code = generate(ast, options)
128 | return {
129 | ast,
130 | render: code.render,
131 | staticRenderFns: code.staticRenderFns
132 | }
133 | })
134 | 复制代码
135 | ```
136 |
137 | 主要完成的功能是:
138 |
139 | * 通过`const ast = parse(template.trim(), options)`将`template`转换成`ast`树
140 | * 通过`optimize(ast, options)`对`ast`进行优化
141 | * 通过`const code = generate(ast, options)`将优化后的`ast`转换成包含`render`字符串的`code`对象,最终`render`字符串通过`new Function`转换为可执行的`render`函数
142 |
143 | `模板编译的真实入口`可以参考[vue2从template到render:模板编译入口](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7134253213871505415 "https://juejin.cn/post/7134253213871505415")
144 | `parse`可以参考[vue2从template到render:AST](https://juejin.cn/post/7134722260870365198 "https://juejin.cn/post/7134722260870365198")
145 | `optimize`可以参考[vue2从template到render:optimize](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7136206751602311175 "https://juejin.cn/post/7136206751602311175")
146 | `generate`可以参考[vue2从template到render:code](https://juejin.cn/post/7135783392133513252 "https://juejin.cn/post/7135783392133513252")
147 |
148 | 八、虚拟`DOM`
149 | ---------
150 |
151 | **先看浏览器对`HTML`的理解**:
152 |
153 | ```
154 |
155 |
My title
156 | Some text content
157 |
158 |
159 | 复制代码
160 | ```
161 |
162 | 当浏览器读到这些代码时,它会建立一个DOM树来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。 上述 HTML 对应的 DOM 节点树如下图所示:
163 |
164 |  每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。
165 |
166 | **再看`Vue`对`HTML template`的理解**
167 |
168 | Vue 通过建立一个**虚拟 DOM** 来追踪自己要如何改变真实 DOM。因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“**VNode**”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
169 |
170 | 简言之,浏览器对HTML的理解是DOM树,Vue对`HTML`的理解是虚拟DOM,最后在`patch`阶段通过DOM操作的api将其渲染成真实的DOM节点。
171 |
172 | 九、模板或者组件渲染
173 | ----------
174 |
175 | `Vue`中的编译会执行到逻辑`vm._update(vm._render(), hydrating)`,其中的`vm._render`执行会获取到`vNode`,`vm._update`就会对`vNode`进行`patch`的处理,又分为模板渲染和组件渲染。
176 |
177 | * 模板渲染,参考[vue2从数据到视图渲染:模板渲染](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7129095821261275167 "https://juejin.cn/post/7129095821261275167")
178 | * 组件渲染,参考[vue2从数据到视图渲染:组件渲染](https://juejin.cn/post/7129095821261275167 "https://juejin.cn/post/7129095821261275167")
179 |
180 | 十、数据响应式处理
181 | ---------
182 |
183 | `Vue`的数据响应式处理的核心是`Object.defineProperty`,在递归响应式处理对象的过程中,为每一个属性定义了一个发布者`dep`,当进行`_render`函数执行时会访问到当前值,在`get`中通过`dep.depend`进行当前`Watcher`的收集,当数据发生变化时会在`set`中通过`dep.notify`进行`Watcher`的更新。
184 |
185 | 数据响应式处理以及发布订阅者模式的关系请参考[vue2从数据变化到视图变化:发布订阅模式](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7130991439965585444 "https://juejin.cn/post/7130991439965585444")
186 |
187 | 十一、`this.$set`
188 | --------------
189 |
190 | ```
191 | const app = new Vue({
192 | el: "#app",
193 | data() {
194 | return {
195 | obj: {
196 | name: "name-1"
197 | }
198 | };
199 | },
200 | template: `
{{obj.name}}的年龄是{{obj.age}}
`,
201 | methods: {
202 | change() {
203 | this.obj.name = 'name-2';
204 | this.obj.age = 30;
205 | }
206 | }
207 | });
208 | 复制代码
209 | ```
210 |
211 | 以上例子执行的结果是:
212 | name-1的年龄是
213 | 当点击后依然是:
214 | name-2的年龄是
215 | 可以看出点击后,`obj`的`name`属性变化得到了视图更新,而`age`属性并未进行变化。
216 |
217 | `name`属性响应式的过程中锁定了一个发布者`dep`,在当前视图渲染时在发布者`dep`的`subs`中做了记录,一旦其发生改变,就会触发`set`方法中的`dep.notify`,继而执行视图的重新渲染。然而,`age`属性并未进行响应式的处理,当其改变时就不能进行视图渲染。
218 |
219 | 此时就需要通过`this.$set`的方式对其进行手动响应式的处理。具体细节请参考 [手动响应式处理和数组检测变化](https://juejin.cn/post/7131280408385159175 "https://juejin.cn/post/7131280408385159175")
220 |
221 | 十二、组件注册
222 | -------
223 |
224 | 组件的使用是先注册后使用,又分为:
225 |
226 | * 全局注册:可以直接在页面中使用
227 | * 局部注册:使用时需要通过`import xxx from xxx`的方式引入,并且在当前组件的选项`components`中增加局部组件的名称。
228 |
229 | 全局注册和局部注册实现原理可以参考 [vue2从数据到视图渲染:组件注册(全局组件/局部组件)](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7129338204448260109 "https://juejin.cn/post/7129338204448260109")
230 |
231 | 十三、异步组件
232 | -------
233 |
234 | Vue单页面应用中一个页面只有一个`
`承载所有节点,因此复杂项目可能会出现首屏加载白屏等问题,Vue异步组件就很好的处理了这问题。
235 |
236 | 异步组件的分类和实现原理请参考 [vue2从数据到视图渲染:异步组件](https://juejin.cn/post/7129784993663942693 "https://juejin.cn/post/7129784993663942693")
237 |
238 | 十四、`this.$nextTick`
239 | -------------------
240 |
241 | 因为通过`new`实例化构造函数`Vue`的时候会执行初始化方法`this._init`,其中涉及到的方法大多都是同步执行。`nextTick`在vue中是一个很重要的方法,在`new Vue`实例化的同步过程中将一些需要异步处理的函数推到异步队列中去,可以等`new Vue`所有的同步任务执行完后,再执行异步队列中的函数。
242 |
243 | `nextTick`的实现可以参考 [vue2从数据变化到视图变化:nextTick](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7129855050259628068 "https://juejin.cn/post/7129855050259628068"),
244 |
245 | 十五、`keep-alive`内置组件
246 | -------------------
247 |
248 | `vue`中支持组件化,并且也有用于缓存的内置组件`keep-alive`可直接使用,使用场景为`路由组件`和`动态组件`。
249 |
250 | * `activated`表示进入组件的生命周期,`deactivated`表示离开组件的生命周期
251 | * `include`表示匹配到的才缓存,`exclude`表示匹配到的都不缓存
252 | * `max`表示最多可以缓存多少组件
253 |
254 | `keep-alive`的具体实现请参考 [vue中的keep-alive(源码分析)](https://juejin.cn/post/7155828702356439076/ "https://juejin.cn/post/7155828702356439076/")
255 |
256 | 十六、生命周期
257 | -------
258 |
259 | `vue`中的生命周期有哪些?
260 | 答案:`11`个,分别为`beforeCreate`、`created`、`beforeMount`、`mounted`、`beforeUpdate`、`updated`、`activated`、`deactivated`、`beforeDestroy`、`destroyed`和`errorCaptured`。
261 |
262 | 具体实现请参考 [vue生命周期](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7140218856152236068 "https://juejin.cn/post/7140218856152236068")
263 |
264 | 十七、`v-show`和`v-if`的区别
265 | ---------------------
266 |
267 | 先看`v-if`和`v-show`的使用场景:
268 |
269 | (1)`v-if`更多的使用在需要考虑白屏时间或者切换次数很少的场景
270 | (2)`v-show`更多使用在`transition`控制的动画或者需要非常频繁地切换的场景
271 |
272 | 再从底层实现思路上分析:
273 |
274 | (1)`v-if`条件为`false`时,会生成空的占位注释节点,那么在考虑首页白屏时间时,选用`v-if`比较合适。条件从`false`变化为`true`的话会从空的注释节点变成真实节点,条件再变为`false`时真实节点又会变成注释节点,如果切换次数比较多,那么开销会比较大,频繁切换场景不建议使用`v-if`。
275 | (2)`v-show`条件为`false`时,会生成真实的节点,只是为当前节点增加了`display:none`来控制其隐藏,相比`v-if`生成空的注释节点其首次渲染开销是比较大的,所以不建议用在考虑首屏白屏时间的场景。如果我们频繁切换`v-show`的值,从`display:none`到`display:block`之间的切换比起空的注释节点和真实节点的开销要小很多,这种场景就建议使用`v-show`。
276 |
277 | 可以通过[vue中v-if和v-show的区别(源码分析)](https://juejin.cn/post/7139815983979757575 "https://juejin.cn/post/7139815983979757575")了解`v-if`和`v-show`详细过程。
278 |
279 | 十八、`v-for`中`key`的作用
280 | -------------------
281 |
282 | 在`v-for`进行循环展示过程中,当数据发生变化进行渲染的过程中,会进行新旧节点列表的比对。首先新旧`vnode`列表首先通过`首首`、`尾尾`、`首尾`和`尾首`的方式进行比对,如果`key`相同则采取原地复用的策略进行节点的移动。
283 |
284 | 如果首尾两两比对的方式找不到对应关系,继续通过`key`和`vnode`的对应关系进行寻找。
285 |
286 | 如果`key`和`vnode`对应关系中找不到,继续通过`sameVnode`的方式在未比对的节点中进行寻找。
287 |
288 | 如果都找不到,则将其按照新`vnode`进行`createElm`的方式进行创建,这种方式是比节点移动的方式计算量更大。
289 |
290 | 最后将旧的`vnode`列表中没有进行匹配的`vnode`中的`vnode.elm`在父节点中移除。
291 |
292 | 简单总结就是,新的`vnode`列表在旧的`vnode`列表中去寻找具有相同的`key`的节点进行原地复用,如果找不到则通过创建的方式`createElm`去创建一个,如果旧的`vnode`列表中没有进行匹配则在父节点中移除其`vnode.elm`。这就是原地复用逻辑的大体实现。
293 |
294 | 具体`key`和`diff`算法的关系可以参考[vue2从数据变化到视图变化:diff算法图解](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7131760211609518094 "https://juejin.cn/post/7131760211609518094")
295 |
296 | 十九、`v-for`和`v-if`能同时使用吗
297 | -----------------------
298 |
299 | 答案是:用了也能出来预期的效果,但是会有性能浪费。
300 |
301 | 同时包含`v-for`和`v-if`的`template`模板在编辑阶段会执行`v-for`比`v-if`优先级更高的编译流程;在生成`vnode`的阶段,会包含属性`isComment`为`true`的空白占位`vnode`;在`patch`阶段,会生成真实的占位节点。虽然一个空的占位节点无妨,但是如果数据量比较大的话,也是一个性能问题。
302 |
303 | 当然,可以在获取到数据(一般是在`beforeCreate`或者`created`阶段)时进行过滤处理,也可以通过计算属性对其进行处理。
304 |
305 | 可以通过[`v-for`和`v-if`可以一起使用吗?](https://juejin.cn/post/7137326610822201357 "https://juejin.cn/post/7137326610822201357")了解`v-for`和`v-if`的详细过程。
306 |
307 | 二十、`vue`中的`data`为什么是函数
308 | ----------------------
309 |
310 | 答案是:是不是一定是函数,得看场景。并且,也无需担心什么时候该将`data`写为函数还是对象,因为`vue`内部已经做了处理,并在控制台输出错误信息。
311 |
312 | **场景一**:`new Vue({data: ...})`
313 | 这种场景主要为项目入口或者多个`html`页面各实例化一个`Vue`时,这里的`data`即可用对象的形式,也可用工厂函数返回对象的形式。因为,这里的`data`只会出现一次,不存在重复引用而引起的数据污染问题。
314 |
315 | **场景二**:组件场景中的选项
316 | 在生成组件`vnode`的过程中,组件会在生成构造函数的过程中执行合并策略:
317 |
318 | ```
319 | // data合并策略
320 | strats.data = function (
321 | parentVal,
322 | childVal,
323 | vm
324 | ) {
325 | if (!vm) {
326 | if (childVal && typeof childVal !== 'function') {
327 | process.env.NODE_ENV !== 'production' && warn(
328 | 'The "data" option should be a function ' +
329 | 'that returns a per-instance value in component ' +
330 | 'definitions.',
331 | vm
332 | );
333 |
334 | return parentVal
335 | }
336 | return mergeDataOrFn(parentVal, childVal)
337 | }
338 |
339 | return mergeDataOrFn(parentVal, childVal, vm)
340 | };
341 | 复制代码
342 | ```
343 |
344 | 如果合并过程中发现子组件的数据不是函数,即`typeof childVal !== 'function'`成立,进而在开发环境会在控制台输出警告并且直接返回`parentVal`,说明这里压根就没有把`childVal`中的任何`data`信息合并到`options`中去。
345 |
346 | 可以通过[`vue`中的`data`为什么是函数?](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7139925047145316360 "https://juejin.cn/post/7139925047145316360")了解详细过程。
347 |
348 | 二十一、`this.$watch`
349 | -----------------
350 |
351 | 使用场景:用来监听数据的变化,当数据发生变化的时候,可以做一些业务逻辑的处理。
352 |
353 | 配置参数:
354 |
355 | * `deep`:监听数据的深层变化
356 | * `immediate`:立即触发回调函数
357 |
358 | 实现思路: `Vue`构造函数定义完成以后,在执行`stateMixin(Vue)`时为`Vue.prototype`上定义`$watch`。该方法通过`const watcher = new Watcher(vm, expOrFn, cb, options)`进行`Watcher`的实例化,将`options`中的`user`属性设置为`true`。并且,`$watch`逻辑结束的会返回函数`function unwatchFn () { watcher.teardown() }`,用来取消侦听的函数。
359 |
360 | 可以通过`watch`选项和`$watch`方法的区别[vue中的watch和$watch监听的事件,执行几次?](https://juejin.cn/post/7156172339414040584 "https://juejin.cn/post/7156172339414040584")来了解详细过程。
361 |
362 | 二十二、计算属性和侦听属性的区别
363 | ----------------
364 |
365 | **相同点:** 两者都是`Watcher`实例化过程中的产物
366 |
367 | **计算属性:**
368 |
369 | * 使用场景:模板内的表达式主要用于简单运算,对于复杂的计算逻辑可以用计算属性
370 | * 计算属性是基于它们的响应式依赖进行缓存的,当依赖的数据未发生变化时,多次调用无需重复执行函数
371 | * 计算属性计算结果依赖于`data`中的值
372 | * 同步操作,不支持异步
373 |
374 | **侦听属性:**
375 |
376 | * 使用场景:当需要在数据变化时执行异步或开销较大的操作时,可以用侦听属性
377 | * 可配置参数:可以通过配置`immediate`和`deep`来控制立即执行和深度监听的行为
378 | * 侦听属性侦听的是`data`中定义的
379 |
380 | 计算属性请参考[vue2从数据变化到视图变化:计算属性](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7132100391776452615 "https://juejin.cn/post/7132100391776452615")
381 | 侦听属性请参考[vue2从数据变化到视图变化:侦听器](https://juejin.cn/post/7132312215402577956 "https://juejin.cn/post/7132312215402577956")
382 |
383 | 二十三、`v-model`
384 | -------------
385 |
386 | ```
387 | // main.js
388 | new Vue({
389 | el: "#app",
390 | data() {
391 | return {
392 | msg: ""
393 | };
394 | },
395 | template: `
396 |
397 |
msg is: {{ msg }}
398 |
`
399 | });
400 | 复制代码
401 | ```
402 |
403 | **普通input:**`input`中的`v-model`,最终通过`target.addEventListener`处理成在节点上监听`input`事件`function($event){msg=$event.target.value}}`的形式,当`input`值变化时`msg`也跟着改变。
404 |
405 | ```
406 | // main.js
407 | const inputBox = {
408 | template: `
`,
409 | };
410 |
411 | new Vue({
412 | el: "#app",
413 | template: `
414 |
415 |
{{msg}}
416 |
`,
417 | components: {
418 | inputBox
419 | },
420 | data() {
421 | return {
422 | msg: 'hello world!'
423 | };
424 | },
425 | });
426 | 复制代码
427 | ```
428 |
429 | **组件**:`v-model`在组件中则通过给点击事件绑定原生事件,当触发到`$emit`的时候,再进行回调函数`ƒunction input($$v) {msg=$$v}`的执行,进而达到子组件修改父组件中数据`msg`的目的。
430 |
431 | 二十四、`v-slot`
432 | ------------
433 |
434 | `v-slot`产生的主要目的是,在组件的使用过程中可以让父组件有修改子组件内容的能力,就像在子组件里面放了个插槽,让父组件往插槽内塞入父组件中的楔子;并且,父组件在子组件中嵌入的楔子也可以访问子组件中的数据。`v-slot`的产生让组件的应用更加灵活。
435 |
436 | ### 1、具名插槽
437 |
438 | ```
439 | let baseLayout = {
440 | template: `
441 |
444 |
445 |
446 |
447 |
450 |
`,
451 | data() {
452 | return {
453 | url: ""
454 | };
455 | }
456 | };
457 |
458 | new Vue({
459 | el: "#app",
460 | template: `
461 |
462 | title-txt
463 |
464 | paragraph-1-txt
465 | paragraph-2-txt
466 |
467 | foot-txt
468 |
469 | `,
470 | components: {
471 | baseLayout
472 | }
473 | });
474 | 复制代码
475 | ```
476 |
477 | 引入的组件`baseLayout`中的`template`被添加了属性`v-slot:header`和`v-slot:footer`,子组件中定义了对应的插槽被添加了属性和,未被进行插槽标识的内容被插入到了匿名的`
`中。
478 |
479 | ### 2、作用域插槽
480 |
481 | ```
482 | let currentUser = {
483 | template: `
484 | {{childData.firstName}}
485 | `,
486 | data() {
487 | return {
488 | childData: {
489 | firstName: "first",
490 | lastName: "last"
491 | }
492 | };
493 | }
494 | };
495 |
496 | new Vue({
497 | el: "#app",
498 | template: `
499 | {{slotProps.userData.lastName}}
500 | `,
501 | components: {
502 | currentUser
503 | }
504 | });
505 | 复制代码
506 | ```
507 |
508 | 当前例子中作用域插槽通过`v-bind:userData="childData"`的方式,将`childData`作为参数,父组件中通过`v-slot:user="slotProps"`的方式进行接收,为父组件使用子组件中的数据提供了可能。
509 |
510 | `v-slot`的底层实现请参考[vue中的v-slot(源码分析)](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7144188805254709256 "https://juejin.cn/post/7144188805254709256")
511 |
512 | 二十五、`Vue.filters`
513 | -----------------
514 |
515 | `filters`类似于管道流可以将上一个过滤函数的结果作为下一个过滤函数的第一个参数,又可以在其中传递参数让过滤器更灵活。
516 |
517 | ```
518 | // main.js文件
519 | import Vue from "vue";
520 |
521 | Vue.filter("filterEmpty", function(val) {
522 | return val || "";
523 | });
524 |
525 | Vue.filter("filterA", function(val) {
526 | return val + "平时周末的";
527 | });
528 |
529 | Vue.filter("filterB", function(val, info, fn) {
530 | return val + info + fn;
531 | });
532 |
533 | new Vue({
534 | el: "#app",
535 | template: `
{{msg | filterEmpty | filterA | filterB('爱好是', transformHobby('chess'))}}
`,
536 | data() {
537 | return {
538 | msg: "张三"
539 | };
540 | },
541 | methods: {
542 | transformHobby(type) {
543 | const map = {
544 | bike: "骑行",
545 | chess: "象棋",
546 | game: "游戏",
547 | swimming: "游泳"
548 | };
549 | return map[type] || "未知";
550 | }
551 | }
552 | });
553 | 复制代码
554 | ```
555 |
556 | 其中我们对`msg`通过`filterEmpty`、`filterA`和`filterB('爱好是', transformHobby('chess'))}`进行三层过滤。
557 |
558 | `Vue.filters`的底层实现请查看[vue中的filters(源码分析)](https://juejin.cn/post/7146889304143298597 "https://juejin.cn/post/7146889304143298597")
559 |
560 | 二十六、`Vue.use`
561 | -------------
562 |
563 | * 作用:`Vue.use`被用来安装Vue.js插件,例如`vue-router`、`vuex`、`element-ui`。
564 | * `install`方法:如果插件是一个对象,必须提供 `install` 方法。如果插件是一个函数,它会被作为`install`方法。`install`方法调用时,会将`Vue`作为参数传入。
565 | * 调用时机:该方法需要在调用 `new Vue()` 之前被调用。
566 | * 特点:当 install 方法被同一个插件多次调用,插件将只会被安装一次。
567 |
568 | 二十七、`Vue.extend`和选项`extends`
569 | ----------------------------
570 |
571 | ### 1、`Vue.extend`
572 |
573 | `Vue.extend`使用基础`Vue`构造器创建一个“子类”,参数是一个包含组件选项的对象,实例化的过程中可以修改其中的选项,为实现功能的继承提供了思路。
574 |
575 | ```
576 | new Vue({
577 | el: "#app",
578 | template: `
`,
579 | mounted() {
580 | // 定义子类构造函数
581 | var Profile = Vue.extend({
582 | template: '
{{name}} 喜欢 {{fruit}}
',
583 | data: function () {
584 | return {
585 | name: '张三',
586 | fruit: '苹果'
587 | }
588 | },
589 | methods: {
590 | showInfo() {
591 | console.log(`${this.name}喜欢${this.fruit}`)
592 | }
593 | }
594 | })
595 | // 实例化1,挂载到`#person1`上
596 | new Profile().$mount('#person1')
597 | // 实例化2,并修改其`data`选项,挂载到`#person2`上
598 | new Profile({
599 | data: function () {
600 | return {
601 | name: '李四',
602 | fruit: '香蕉'
603 | }
604 | },
605 | }).$mount('#person2')
606 | },
607 | });
608 | 复制代码
609 | ```
610 |
611 | 在当前例子中,通过`Vue.extend`构建了子类构造函数`Profile`,可以通过`new Profile`的方式实例化无数个`vm`实例。我们定义初始的`template`、`data`和`methods`供`vm`进行使用,如果有变化,在实例的过程中传入新的选项参数即可,比如例子中实例化第二个`vm`的时候就对`data`进行了调整。
612 |
613 | ### 2、选项`extends`
614 |
615 | `extends`允许声明扩展另一个组件 (可以是一个简单的选项对象或构造函数),而无需使用 `Vue.extend`。这主要是为了便于扩展单文件组件,以实现组件继承的目的。
616 |
617 | ```
618 | const common = {
619 | template: `
{{name}}
`,
620 | data() {
621 | return {
622 | name: '表单'
623 | }
624 | }
625 | }
626 |
627 | const create = {
628 | extends: common,
629 | data() {
630 | return {
631 | name: '新增表单'
632 | }
633 | }
634 | }
635 |
636 | const edit = {
637 | extends: common,
638 | data() {
639 | return {
640 | name: '编辑表单'
641 | }
642 | }
643 | }
644 |
645 | new Vue({
646 | el: "#app",
647 | template: `
648 |
649 |
650 |
`,
651 | components: {
652 | create,
653 | edit,
654 | }
655 | });
656 | 复制代码
657 | ```
658 |
659 | 当前极简demo中定义了公共的表单`common`,然后又在新增表单组件`create`和编辑表单组件`edit`中扩展了`common`。
660 |
661 | 二十八、`Vue.mixin`和选项`mixins`
662 | --------------------------
663 |
664 | 全局混入和局部混入视情况而定,主要区别在全局混入是通过`Vue.mixin`的方式将选项混入到了`Vue.options`中,在所有获取子组件构建函数的时候都将其进行了合并,是一种影响全部组件的混入策略。
665 |
666 | 而局部混入是将选项通过配置`mixins`选项的方式合并到当前的子组件中,只有配置了`mixins`选项的组件才会受到混入影响,是一种局部的混入策略。
667 |
668 | 二十九、`Vue.directive`和`directives`
669 | --------------------------------
670 |
671 | ### 1、使用场景
672 |
673 | 主要用于对于DOM的操作,比如:文本框聚焦,节点位置控制、防抖节流、权限管理、复制操作等功能
674 |
675 | ### 2、钩子函数
676 |
677 | * `bind`:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
678 | * `inserted`:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
679 | * `update`:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新。
680 | * `componentUpdated`:指令所在组件的 VNode **及其子 VNode** 全部更新后调用。
681 | * `unbind`:只调用一次,指令与元素解绑时调用。
682 |
683 | ### 3、钩子函数参数
684 |
685 | * `el`:指令所绑定的元素,可以用来直接操作 DOM。
686 | * `binding`:一个对象,包含以下 property:
687 | * `name`:指令名,不包括 `v-` 前缀。
688 | * `value`:指令的绑定值,例如:`v-my-directive="1 + 1"` 中,绑定值为 `2`。
689 | * `oldValue`:指令绑定的前一个值,仅在 `update` 和 `componentUpdated` 钩子中可用。无论值是否改变都可用。
690 | * `expression`:字符串形式的指令表达式。例如 `v-my-directive="1 + 1"` 中,表达式为 `"1 + 1"`。
691 | * `arg`:传给指令的参数,可选。例如 `v-my-directive:foo` 中,参数为 `"foo"`。
692 | * `modifiers`:一个包含修饰符的对象。例如:`v-my-directive.foo.bar` 中,修饰符对象为 `{ foo: true, bar: true }`。
693 | * `vnode`:Vue 编译生成的虚拟节点。
694 | * `oldVnode`:上一个虚拟节点,仅在 `update` 和 `componentUpdated` 钩子中可用。
695 |
696 | ### 4、动态指令参数
697 |
698 | 指令的参数可以是动态的。例如,在 `v-mydirective:[argument]="value"` 中,`argument` 参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用。
699 |
700 | 三十、`vue`中的原生事件
701 | --------------
702 |
703 | `vue`中可以通过`@`或者`v-on`的方式绑定事件,也可为其添加修饰符。
704 |
705 | ```
706 | new Vue({
707 | el: '#app',
708 | template: `
`,
709 | methods: {
710 | divClick() {
711 | console.log('divClick')
712 | },
713 | aClick() {
714 | console.log('aClick')
715 | },
716 | }
717 | })
718 | 复制代码
719 | ```
720 |
721 | 以上例子如果点击`a`会触发其默认行为,如果`href`不为空还会进行跳转。除此之外,点击还会继续触发`div`上绑定的点击事件。
722 |
723 | 如果通过`@click.stop.prevent='aClick'`的方式为`a`标签的点击事件添加修饰符`stop`和`prevent`,那么就不会触发其`a`的默认行为,即使`href`不为空也不会进行跳转,同时,`div`上的点击事件也不会进行触发。
724 |
725 | 模板的渲染一般分为编译生成`render`函数、`render`函数执行生成`vNode`和`patch`进行渲染。下面按照这步骤进行简单分析。
726 |
727 | ### 1、`render`
728 |
729 | 通过编译生成的`render`函数:
730 |
731 | ```
732 | with(this) {
733 | return _c('div', {
734 | on: {
735 | "click": divClick
736 | }
737 | }, [_c('a', {
738 | attrs: {
739 | "href": "http://www.baidu.com"
740 | },
741 | on: {
742 | "click": function ($event) {
743 | $event.stopPropagation();
744 | $event.preventDefault();
745 | return aClick($event)
746 | }
747 | }
748 | }, [_v("点击")])])
749 | }
750 | 复制代码
751 | ```
752 |
753 | 其中`div`的`on`作为`div`事件描述。`a`标签的`attrs`作为属性描述,`on`作为事件描述,在描述中`.stop`被编译成了`$event.stopPropagation()`来阻止事件冒泡,`.prevent`被编译成了`$event.preventDefault()`用来阻止`a`标签的默认行为。
754 |
755 | ### 2、`vNode`
756 |
757 | 通过执行`Vue.prototype._render`将`render`函数转换成`vNode`。
758 |
759 | ### 3、`patch`
760 |
761 | `patch`的过程中,当完成`$el`节点的渲染后会执行`invokeCreateHooks(vnode, insertedVnodeQueue)`逻辑,其中,针对`attrs`会将其设置为`$el`的真实属性,当前例子中会为`a`标签设置`herf`属性。针对`on`会通过`target.addEventListener`的方式将其处理过的事件绑定到`$el`上,当前例子中会分别对`div`和`a`中的`click`进行处理,再通过`addEventListener`的方式进行绑定。
762 |
763 | ### 小结
764 |
765 | `vue`中的事件,从编译生成`render`再通过`Vue.prototype._render`函数执行`render`到生成`vNode`,主要是通过`on`作为描述。在`patch`渲染阶段,将`on`描述的事件进行处理再通过`addEventListener`的方式绑定到`$el`上。
766 |
767 | 三十一、常用修饰符
768 | ---------
769 |
770 | ### 1、表单修饰符
771 |
772 | #### (1)`.lazy`
773 |
774 | 在默认情况下,`v-model` 在每次 `input` 事件触发后将输入框的值与数据进行同步 ,可以添加 `lazy` 修饰符,从而转为在 `change` 事件之后进行同步:
775 |
776 | ```
777 |
778 | 复制代码
779 | ```
780 |
781 | #### (2)`.number`
782 |
783 | 如果想自动将用户的输入值转为数值类型,可以给 `v-model` 添加 `number` 修饰符:
784 |
785 | ```
786 |
787 | 复制代码
788 | ```
789 |
790 | #### (3)`.trim`
791 |
792 | 如果要自动过滤用户输入的首尾空白字符,可以给 `v-model` 添加 `trim` 修饰符:
793 |
794 | ```
795 |
796 | 复制代码
797 | ```
798 |
799 | ### 2、事件修饰符
800 |
801 | #### (1)`.stop`
802 |
803 | 阻止单击事件继续传播。
804 |
805 | ```
806 |
807 |
808 | 复制代码
809 | ```
810 |
811 | #### (2)`.prevent`
812 |
813 | 阻止标签的默认行为。
814 |
815 | ```
816 |
点击
817 | 复制代码
818 | ```
819 |
820 | #### (3)`.capture`
821 |
822 | 事件先在有`.capture`修饰符的节点上触发,然后在其包裹的内部节点中触发。
823 |
824 | ```
825 |
826 |
827 | 复制代码
828 | ```
829 |
830 | #### (4)`.self`
831 |
832 | 只当在 event.target 是当前元素自身时触发处理函数,即事件不是从内部元素触发的。
833 |
834 | ```
835 |
836 |
837 | 复制代码
838 | ```
839 |
840 | #### (5)`.once`
841 |
842 | 不像其它只能对原生的 DOM 事件起作用的修饰符,`.once` 修饰符还能被用到自定义的组件事件上,表示当前事件只触发一次。
843 |
844 | ```
845 |
点击
846 | 复制代码
847 | ```
848 |
849 | #### (6)`.passive`
850 |
851 | `.passive` 修饰符尤其能够提升移动端的性能
852 |
853 | ```
854 |
855 |
856 |
857 |
...
858 | 复制代码
859 | ```
860 |
861 | ### 3、其他修饰符
862 |
863 | 除了表单和事件的修饰符,`Vue`还提供了很多其他修饰符,在使用的时候可以查阅文档。
864 |
865 | ### 小结
866 |
867 | > `Vue`中提供了很多好用的功能和`api`,那么修饰符的出现就为功能和`api`提供了更为丰富的扩展属性和更大的灵活度。
868 |
869 | 三十二、`vue-router`
870 | ----------------
871 |
872 | `vue`路由是单页面中视图切换的方案,有三种`mode`:
873 |
874 | * hash,#后的仅仅作为参数,不属于url部分
875 | * history,路径作为请求url请求资源链接,如果找不到会出现404错误
876 | * abstract,服务端渲染场景 hash场景下,会出现`url`链接,再修改其view-router中对应的值。
877 |
878 | 了解`vue-router`的底层实现请参考[vue2视图切换:vue-router](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7141965586099077157 "https://juejin.cn/post/7141965586099077157")
879 |
880 | 三十三、`vuex`
881 | ----------
882 |
883 | `vuex`是状态管理仓库,一般使用的场景为:多个视图依赖于同一状态,来自不同视图的行为需要变更同一状态。其管理的状态是响应式的,修改也只能显式提交`mutation`的方式修改。`vuex`有`state`、`getter`、`mutation`、`action`和`module`五个核心,并且通过`module`实现了`vuex`树的管理。
884 |
885 | 了解`vuex`的底层实现请参考[vue2状态管理:vuex](https://juejin.cn/post/7141965586099077157 "https://juejin.cn/post/7141965586099077157")
886 |
887 | 三十四、`eventBus`
888 | --------------
889 |
890 | **使用场景**:兄弟组件传参
891 |
892 | ```
893 | const eventBus = new Vue();
894 |
895 | const A = {
896 | template: `
component-a
`,
897 | methods: {
898 | send() {
899 | eventBus.$emit('sendData', 'data from A')
900 | }
901 | },
902 | }
903 |
904 | const B = {
905 | template: `
component-b
`,
906 | created() {
907 | eventBus.$on('sendData', (args) => {
908 | console.log(args)
909 | })
910 | },
911 | }
912 |
913 | new Vue({
914 | el: '#app',
915 | components: {
916 | A,
917 | B,
918 | },
919 | template: `
`,
920 | })
921 | 复制代码
922 | ```
923 |
924 | 在当前例子中,`A`组件和`B`组件称为兄弟组件,`A`组件通过事件总线`eventBus`中的`$emit`分发事件,`B`组件则通过`$on`来监听事件。
925 |
926 | **实现原理:**`eventsMixin`
927 |
928 | ```
929 | export function eventsMixin (Vue: Class
) {
930 | const hookRE = /^hook:/
931 | Vue.prototype.$on = function (event: string | Array, fn: Function): Component {
932 | const vm: Component = this
933 | if (Array.isArray(event)) {
934 | for (let i = 0, l = event.length; i < l; i++) {
935 | vm.$on(event[i], fn)
936 | }
937 | } else {
938 | (vm._events[event] || (vm._events[event] = [])).push(fn)
939 | // optimize hook:event cost by using a boolean flag marked at registration
940 | // instead of a hash lookup
941 | if (hookRE.test(event)) {
942 | vm._hasHookEvent = true
943 | }
944 | }
945 | return vm
946 | }
947 |
948 | Vue.prototype.$once = function (event: string, fn: Function): Component {
949 | const vm: Component = this
950 | function on () {
951 | vm.$off(event, on)
952 | fn.apply(vm, arguments)
953 | }
954 | on.fn = fn
955 | vm.$on(event, on)
956 | return vm
957 | }
958 |
959 | Vue.prototype.$off = function (event?: string | Array, fn?: Function): Component {
960 | const vm: Component = this
961 | // all
962 | if (!arguments.length) {
963 | vm._events = Object.create(null)
964 | return vm
965 | }
966 | // array of events
967 | if (Array.isArray(event)) {
968 | for (let i = 0, l = event.length; i < l; i++) {
969 | vm.$off(event[i], fn)
970 | }
971 | return vm
972 | }
973 | // specific event
974 | const cbs = vm._events[event]
975 | if (!cbs) {
976 | return vm
977 | }
978 | if (!fn) {
979 | vm._events[event] = null
980 | return vm
981 | }
982 | // specific handler
983 | let cb
984 | let i = cbs.length
985 | while (i--) {
986 | cb = cbs[i]
987 | if (cb === fn || cb.fn === fn) {
988 | cbs.splice(i, 1)
989 | break
990 | }
991 | }
992 | return vm
993 | }
994 |
995 | Vue.prototype.$emit = function (event: string): Component {
996 | const vm: Component = this
997 | if (process.env.NODE_ENV !== 'production') {
998 | const lowerCaseEvent = event.toLowerCase()
999 | if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
1000 | tip(
1001 | `Event "${lowerCaseEvent}" is emitted in component ` +
1002 | `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
1003 | `Note that HTML attributes are case-insensitive and you cannot use ` +
1004 | `v-on to listen to camelCase events when using in-DOM templates. ` +
1005 | `You should probably use "${hyphenate(event)}" instead of "${event}".`
1006 | )
1007 | }
1008 | }
1009 | let cbs = vm._events[event]
1010 | if (cbs) {
1011 | cbs = cbs.length > 1 ? toArray(cbs) : cbs
1012 | const args = toArray(arguments, 1)
1013 | const info = `event handler for "${event}"`
1014 | for (let i = 0, l = cbs.length; i < l; i++) {
1015 | invokeWithErrorHandling(cbs[i], vm, args, vm, info)
1016 | }
1017 | }
1018 | return vm
1019 | }
1020 | }
1021 | 复制代码
1022 | ```
1023 |
1024 | 在`Vue`构造函数定义完执行的`eventsMixin`函数中,在`Vue.prototype`上分别定义了`$on`、`$emit`、`$off`和`$once`的方法易实现对事件的绑定、分发、取消和只执行一次的方法。`eventBus`就是利用了当`new Vue`实例化后实例上的`$on`、`$emit`、`$off`和`$once`进行数据传递。
1025 |
1026 | 三十五、`ref`
1027 | ---------
1028 |
1029 | **使用场景:** 父组件获取子组件数据或者执行子组件方法
1030 |
1031 | ```
1032 | const A = {
1033 | template: `{{childData.age}}
`,
1034 | data() {
1035 | return {
1036 | childData: {
1037 | name: 'qb',
1038 | age: 30
1039 | },
1040 | }
1041 | },
1042 | methods: {
1043 | increaseAge() {
1044 | this.childData.age++;
1045 | }
1046 | }
1047 | }
1048 |
1049 | new Vue({
1050 | el: '#app',
1051 | components: {
1052 | A,
1053 | },
1054 | template: ``,
1055 | methods: {
1056 | changeChildData() {
1057 | // 执行子组件的方法
1058 | this.$refs.childRef.increaseAge()
1059 | // 获取子组件的数据
1060 | console.log(this.$refs.childRef.childData);
1061 | },
1062 | }
1063 | })
1064 | 复制代码
1065 | ```
1066 |
1067 | 在当前例子中,通过`ref='childRef'`的方式在当前组件中定义一个`ref`,可以通过`this.$refs.childRef`的方式获取到子组件`A`。可以通过`this.$refs.childRef.increaseAge()`的方式执行子组件中`age`增加的方法,也可以通过`this.$refs.childRef.childData`的方式获取到子组件中的数据。
1068 |
1069 | 三十六、`props`
1070 | -----------
1071 |
1072 | **使用场景:** 父子传参
1073 |
1074 | ```
1075 | const A = {
1076 | template: `{{childData}}
`,
1077 | props: ['childData'],
1078 | methods: {
1079 | emitData() {
1080 | this.$emit('emitChildData', 'data from child')
1081 | }
1082 | },
1083 | }
1084 |
1085 | new Vue({
1086 | el: '#app',
1087 | components: {
1088 | A
1089 | },
1090 | template: ``,
1091 | data() {
1092 | return {
1093 | parentData: 'data from parent'
1094 | }
1095 | },
1096 | methods: {
1097 | getChildData(v) {
1098 | console.log(v);
1099 | }
1100 | }
1101 | })
1102 | 复制代码
1103 | ```
1104 |
1105 | 从当前例子中可以看出,数据父传子是通过`:childData='parentData'`的方式,数据子传父是通过`this.$emit('emitChildData', 'data from child')`的方式,然后,父组件通过`@emitChildData='getChildData'`的方式进行获取。
1106 |
1107 | ### 1、父组件`render`函数
1108 |
1109 | `new Vue`中传入的模板`template`经过遍历生成的`render`函数如下:
1110 |
1111 | ```
1112 | with(this) {
1113 | return _c('A', {
1114 | attrs: {
1115 | "childData": parentData
1116 | },
1117 | on: {
1118 | "emitChildData": getChildData
1119 | }
1120 | })
1121 | }
1122 | 复制代码
1123 | ```
1124 |
1125 | 其中`data`部分有`attrs`和`on`来描述属性和方法。
1126 |
1127 | 在通过`createComponent`创建组件`vnode`的过程中,会通过`const propsData = extractPropsFromVNodeData(data, Ctor, tag)`的方式获取`props`,通过`const listeners = data.on`的方式获取`listeners`,最后将其作为参数通过`new VNode(options)`的方式实例化组件`vnode`。
1128 |
1129 | ### 2、子组件渲染
1130 |
1131 | 在通过`const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance )`创建组件实例的过程中,会执行到组件继承自`Vue`的`._init`方法,通过`initEvents`将事件处理后存储到`vm._events`中,通过`initProps`将`childData`赋值到子组件`A`的`vm`实例上,并进行响应式处理,让其可以通过`vm.childData`的方式访问,并且数据发生变化时视图也可以发生改变。
1132 |
1133 | 组件模板编译后对应的`render`函数是:
1134 |
1135 | ```
1136 | with(this) {
1137 | return _c('div', {
1138 | on: {
1139 | "click": emitData
1140 | }
1141 | }, [_v(_s(childData))])
1142 | }
1143 | 复制代码
1144 | ```
1145 |
1146 | 在`createElm`完成节点的创建后,在`invokeCreateHooks(vnode, insertedVnodeQueue)`阶段,给`DOM`原生节点节点绑定`emitData`。
1147 |
1148 | ### 3、`this.$emit`
1149 |
1150 | 在点击执行`this.$emit`时,会通过`var cbs = vm._events[event]`取出`_events`中的事件进行执行。
1151 |
1152 | 至此,父组件中的传递的数据就在子组件中可以通过`this.xxx`的方式获得,也可以通过`this.$emit`的方式将子组件中的数据传递给父组件。
1153 |
1154 | `prop`数据发生改变引起视图变化的底层逻辑请参考[vue2从数据变化到视图变化:props引起视图变化详解](https://link.juejin.cn?target=https%3A%2F%2Fjuejin.cn%2Fpost%2F7132758292681457700 "https://juejin.cn/post/7132758292681457700")
1155 |
1156 | 三十七、`$attrs`和`$listeners`
1157 | -------------------------
1158 |
1159 | **使用场景:** 父子组件非`props`属性和非`native`方法传递
1160 |
1161 | ```
1162 | // main.js文件
1163 | import Vue from "vue";
1164 |
1165 | const B = {
1166 | template: `{{ formParentData }}
`,
1167 | data() {
1168 | return {
1169 | formParentData: ''
1170 | }
1171 | },
1172 | inheritAttrs: false,
1173 |
1174 | created() {
1175 | this.formParentData = this.$attrs;
1176 | console.log(this.$attrs, '--------------a-component-$attrs')
1177 | console.log(this.$listeners, '--------------b-component-$listeners')
1178 | },
1179 | methods: {
1180 | emitData() {
1181 | this.$emit('onFun', 'form B component')
1182 | }
1183 | },
1184 | }
1185 |
1186 | const A = {
1187 | template: ``,
1188 | components: {
1189 | B,
1190 | },
1191 | props: ['propData'],
1192 | inheritAttrs: false,
1193 | created() {
1194 | console.log(this.$attrs, '--------------b-component-$attrs')
1195 | console.log(this.$listeners, '--------------b-component-$listeners')
1196 | }
1197 | }
1198 |
1199 | new Vue({
1200 | el: '#app',
1201 | components: {
1202 | A,
1203 | },
1204 | template: ``,
1205 | data() {
1206 | return {
1207 | parentData: 'msg'
1208 | }
1209 | },
1210 | methods: {
1211 | nativeFun() {
1212 | console.log('方法A');
1213 | },
1214 | onFun(v) {
1215 | console.log('方法B', v);
1216 | },
1217 | }
1218 | })
1219 | 复制代码
1220 | ```
1221 |
1222 | 当前例子中,`new Vue`的`template`模板中有`attrData`、`propData`、`click.native`和`onFun`在进行传递。实际运行后,在`A`组件中`this.$attrs`为`{attrData: 'msg'}`,`this.$listeners`为`{onFun:f(...)}`。在`A`组件中通过`v-bind='$attrs'`和`v-on='$listeners'`的方式继续进行属性和方法的传递,在`B`组件中就可以获取到`A`组件中传入的`$attrs`和`$listeners`。
1223 |
1224 | 当前例子中完成了非`props`属性和非`native`方法的传递,并且通过`v-bind='$attrs'`和`v-on='$listeners'`的方式实现了属性和方法的跨层级传递。
1225 |
1226 | 同时通过`this.$emit`的方法触发了根节点中`onFun`事件。
1227 |
1228 | 关于例子中的`inheritAttrs: false`,默认情况下父作用域的不被认作`props`的`attribute`绑定将会“回退”且作为普通的`HTML`属性应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置`inheritAttrs`到`false`,这些默认行为将会被去掉。
1229 |
1230 | 三十八、`$parent`和`$children`
1231 | -------------------------
1232 |
1233 | **使用场景:** 利用父子关系进行数据的获取或者方法的调用
1234 |
1235 | ```
1236 | const A = {
1237 | template: `{{childRandom}}
`,
1238 | data() {
1239 | return {
1240 | childRandom: Math.random()
1241 | }
1242 | },
1243 | mounted() {
1244 | console.log(this.$parent.parentCount, '--child-created--'); // 获取父组件中的parentCount
1245 | },
1246 | methods: {
1247 | changeParentData() {
1248 | console.log(this.$parent); // 打印当前实例的$parent
1249 | this.$parent.changeParentData(); // 调用当前父级中的方法`changeParentData`
1250 | },
1251 | changeChildData() {
1252 | this.childRandom = Math.random();
1253 | }
1254 | }
1255 | }
1256 | const B = {
1257 | template: `b-component
`,
1258 | }
1259 |
1260 | new Vue({
1261 | el: '#app',
1262 | components: {
1263 | A,
1264 | B,
1265 | },
1266 | template: ``,
1267 | data() {
1268 | return {
1269 | parentCount: 1
1270 | }
1271 | },
1272 | mounted() {
1273 | console.log(this.$children[0].childRandom, '--parent-created--'); // 获取第一个子组件中的childRandom
1274 | },
1275 | methods: {
1276 | changeParentData() {
1277 | this.parentCount++;
1278 | },
1279 | changeChildrenData() {
1280 | console.log(this.$children); // 此时有两个子组件
1281 | this.$children[0].changeChildData(); // 调起第一个子组件中的'changeChildData'方法
1282 | }
1283 | }
1284 | })
1285 | 复制代码
1286 | ```
1287 |
1288 | 在当前例子中,父组件可以通过`this.$children`获取所有的子组件,这里有`A`组件和`B`组件,可以通过`this.$children[0].childRandom`的方式获取子组件`A`中的数据,也可以通过`this.$children[0].changeChildData()`的方式调起子组件`A`中的方法。
1289 |
1290 | 子组件可以通过`this.$parent`的方式获取父组件,可以通过`this.$parent.parentCount`获取父组件中的数据,也可以通过`this.$parent.changeParentData()`的方式修改父组件中的数据。
1291 |
1292 | `Vue`中`$parent`和`$children`父子关系的底层构建请参考[杂谈:parent/children的底层逻辑](https://juejin.cn/post/7162097166725414942/ "https://juejin.cn/post/7162097166725414942/")
1293 |
1294 | 三十九、`inject`和`provide`
1295 | ----------------------
1296 |
1297 | 使用场景:嵌套组件多层级传参
1298 |
1299 | ```
1300 | const B = {
1301 | template: `{{parentData1}}{{parentData2}}
`,
1302 | inject: ['parentData1', 'parentData2'],
1303 | }
1304 |
1305 | const A = {
1306 | template: ``,
1307 | components: {
1308 | B,
1309 | },
1310 | }
1311 |
1312 | new Vue({
1313 | el: '#app',
1314 | components: {
1315 | A,
1316 | },
1317 | template: ``,
1318 | provide: {
1319 | parentData1: {
1320 | name: 'name-2',
1321 | age: 30
1322 | },
1323 | parentData2: {
1324 | name: 'name-2',
1325 | age: 29
1326 | },
1327 | }
1328 | })
1329 | 复制代码
1330 | ```
1331 |
1332 | 例子中在`new Vue`的时候通过`provide`提供了两个数据来源`parentData1`和`parentData2`,然后跨了一个`A`组件在`B`组件中通过`inject`注入了这两个数据。
1333 |
1334 | ### 1、`initProvide`
1335 |
1336 | 在执行组件内部的`this._init`初始化方法时,会执行到`initProvide`逻辑:
1337 |
1338 | ```
1339 | export function initProvide (vm: Component) {
1340 | const provide = vm.$options.provide
1341 | if (provide) {
1342 | vm._provided = typeof provide === 'function'
1343 | ? provide.call(vm)
1344 | : provide
1345 | }
1346 | }
1347 | 复制代码
1348 | ```
1349 |
1350 | 如果在当前`vm.$options`中存在`provide`,会将其执行结果赋值给`vm._provided`。
1351 |
1352 | ### 2、`initInjections`
1353 |
1354 | ```
1355 | function initInjections (vm: Component) {
1356 | const result = resolveInject(vm.$options.inject, vm)
1357 | if (result) {
1358 | toggleObserving(false)
1359 | Object.keys(result).forEach(key => {
1360 | /* istanbul ignore else */
1361 | if (process.env.NODE_ENV !== 'production') {
1362 | defineReactive(vm, key, result[key], () => {
1363 | warn(
1364 | `Avoid mutating an injected value directly since the changes will be ` +
1365 | `overwritten whenever the provided component re-renders. ` +
1366 | `injection being mutated: "${key}"`,
1367 | vm
1368 | )
1369 | })
1370 | } else {
1371 | defineReactive(vm, key, result[key])
1372 | }
1373 | })
1374 | toggleObserving(true)
1375 | }
1376 | }
1377 | function resolveInject (inject: any, vm: Component): ?Object {
1378 | if (inject) {
1379 | // inject is :any because flow is not smart enough to figure out cached
1380 | const result = Object.create(null)
1381 | const keys = hasSymbol
1382 | ? Reflect.ownKeys(inject)
1383 | : Object.keys(inject)
1384 |
1385 | for (let i = 0; i < keys.length; i++) {
1386 | const key = keys[i]
1387 | // #6574 in case the inject object is observed...
1388 | if (key === '__ob__') continue
1389 | const provideKey = inject[key].from
1390 | let source = vm
1391 | while (source) {
1392 | if (source._provided && hasOwn(source._provided, provideKey)) {
1393 | result[key] = source._provided[provideKey]
1394 | break
1395 | }
1396 | source = source.$parent
1397 | }
1398 | if (!source) {
1399 | if ('default' in inject[key]) {
1400 | const provideDefault = inject[key].default
1401 | result[key] = typeof provideDefault === 'function'
1402 | ? provideDefault.call(vm)
1403 | : provideDefault
1404 | } else if (process.env.NODE_ENV !== 'production') {
1405 | warn(`Injection "${key}" not found`, vm)
1406 | }
1407 | }
1408 | }
1409 | return result
1410 | }
1411 | }
1412 | 复制代码
1413 | ```
1414 |
1415 | 如果当前组件中有选项`inject`,会以`while`循环的方式不断在`source = source.$parent`中寻找`_provided`,然后获取到祖先组件中提供的数据源,这是实现祖先组件向所有子孙后代注入依赖的核心。
1416 |
1417 | 四十、`Vue`项目能做的性能优化
1418 | -----------------
1419 |
1420 | ### 1、`v-if`和`v-show`
1421 |
1422 | * 频繁切换时使用`v-show`,利用其缓存特性
1423 | * 首屏渲染时使用`v-if`,如果为`false`则不进行渲染
1424 |
1425 | ### 2、`v-for`的`key`
1426 |
1427 | * 列表变化时,循环时使用唯一不变的`key`,借助其本地复用策略
1428 | * 列表只进行一次渲染时,`key`可以采用循环的`index`
1429 |
1430 | ### 3、侦听器和计算属性
1431 |
1432 | * 侦听器`watch`用于数据变化时引起其他行为
1433 | * 多使用`compouter`计算属性顾名思义就是新计算而来的属性,如果依赖的数据未发生变化,不会触发重新计算
1434 |
1435 | ### 4、合理使用生命周期
1436 |
1437 | * 在`destroyed`阶段进行绑定事件或者定时器的销毁
1438 | * 使用动态组件的时候通过`keep-alive`包裹进行缓存处理,相关的操作可以在`actived`阶段激活
1439 |
1440 | ### 5、数据响应式处理
1441 |
1442 | * 不需要响应式处理的数据可以通过`Object.freeze`处理,或者直接通过`this.xxx = xxx`的方式进行定义
1443 | * 需要响应式处理的属性可以通过`this.$set`的方式处理,而不是`JSON.parse(JSON.stringify(XXX))`的方式
1444 |
1445 | ### 6、路由加载方式
1446 |
1447 | * 页面组件可以采用异步加载的方式
1448 |
1449 | ### 7、插件引入
1450 |
1451 | * 第三方插件可以采用按需加载的方式,比如`element-ui`。
1452 |
1453 | ### 8、减少代码量
1454 |
1455 | * 采用`mixin`的方式抽离公共方法
1456 | * 抽离公共组件
1457 | * 定义公共方法至公共`js`中
1458 | * 抽离公共`css`
1459 |
1460 | ### 9、编译方式
1461 |
1462 | * 如果线上需要`template`的编译,可以采用完成版`vue.esm.js`
1463 | * 如果线上无需`template`的编译,可采用运行时版本`vue.runtime.esm.js`,相比完整版体积要小大约`30%`
1464 |
1465 | ### 10、渲染方式
1466 |
1467 | * 服务端渲染,如果是需要`SEO`的网站可以采用服务端渲染的方式
1468 | * 前端渲染,一些企业内部使用的后端管理系统可以采用前端渲染的方式
1469 |
1470 | ### 11、字体图标的使用
1471 |
1472 | * 有些图片图标尽可能使用字体图标
1473 |
1474 | 四十一、`Vue`项目白屏问题
1475 | ---------------
1476 |
1477 | * 1、开启`gzip`压缩减小文件体积。
1478 | * 2、`webpack`设置`productionSourceMap:false`,不在线上环境打包`.map`文件。
1479 | * 3、路由懒加载
1480 | * 4、异步组件的使用
1481 | * 5、静态资源使用`cdn`链接引入
1482 | * 6、采用`ssr`服务端渲染方案
1483 | * 7、骨架屏或者`loading`效果填充空白间隙
1484 | * 8、首次不渲染的隐藏采用`v-if`
1485 | * 9、注重代码规范:抽取公共组件,公共js,公共css样式,减小代码体积。删除无用代码,减少非必要注释。防止写出死循环等等
1486 | * 10、删除辅助开发的`console.log`
1487 | * 11、非`Vue`角度思考:非重要文件采用异步加载方式、css样式采用媒体查询、采用域名分片技术、http1升级成http2、如果是SSR项目考虑服务端渲染有没有可优化的点、请求头是否带了多余信息等思路
1488 |
1489 | 内容有些多,大体可以归类为从服务端拿到资源的速度、资源的体积和渲染是否阻塞的角度去作答。
1490 |
1491 | 四十二、从`0`到`1`构建一个`Vue`项目需要注意什么
1492 | -----------------------------
1493 |
1494 | * 架子:选用合适的初始化脚手架(`vue-cli2.0`或者`vue-cli3.0`)
1495 | * 请求:数据`axios`请求的配置
1496 | * 登录:登录注册系统
1497 | * 路由:路由管理页面
1498 | * 数据:`vuex`全局数据管理
1499 | * 权限:权限管理系统
1500 | * 埋点:埋点系统
1501 | * 插件:第三方插件的选取以及引入方式
1502 | * 错误:错误页面
1503 | * 入口:前端资源直接当静态资源,或者服务端模板拉取
1504 | * `SEO`:如果考虑`SEO`建议采用`SSR`方案
1505 | * 组件:基础组件/业务组件
1506 | * 样式:样式预处理起,公共样式抽取
1507 | * 方法:公共方法抽离
1508 |
1509 | 四十三、`SSR`
1510 | ---------
1511 |
1512 | ### 1、什么是服务端渲染(SSR)?
1513 |
1514 | Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序。
1515 |
1516 | ### 2、为什么使用服务端渲染(SSR)?
1517 |
1518 | 与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:
1519 |
1520 | * 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
1521 | * 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。
1522 |
1523 | ### 3、使用服务器端渲染 (SSR) 时需要考虑的问题?
1524 |
1525 | 使用服务器端渲染 (SSR) 时还需要有一些权衡之处
1526 |
1527 | * 开发条件所限。浏览器特定的代码,只能在某些生命周期钩子函数 (lifecycle hook) 中使用;一些外部扩展库 (external library) 可能需要特殊处理,才能在服务器渲染应用程序中运行。
1528 | * 涉及构建设置和部署的更多要求。与可以部署在任何静态文件服务器上的完全静态单页面应用程序 (SPA) 不同,服务器渲染应用程序,需要处于 Node.js server 运行环境。
1529 | * 更多的服务器端负载。在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用 CPU 资源 (CPU-intensive - CPU 密集),因此如果你预料在高流量环境 (high traffic) 下使用,请准备相应的服务器负载,并明智地采用缓存策略。
1530 |
1531 | 四十四、`scoped`
1532 | ------------
1533 |
1534 | 在`Vue`项目开发的项目中如果样式中未使用`scoped`,组件间的样式会出现覆盖的问题。
1535 |
1536 | **反例:**
1537 |
1538 | ```
1539 | // app.vue文件
1540 |
1541 |
1542 |
app-txt
1543 |
1544 |
1545 |
1546 |
1547 |
1553 |
1554 |
1559 | 复制代码
1560 | ```
1561 |
1562 | ```
1563 | // child.vue文件
1564 |
1565 | child-txt
1566 |
1567 |
1568 |
1573 | 复制代码
1574 | ```
1575 |
1576 | 父组件和子组件的样式颜色都为`green`,子组件中的样式覆盖了父组件的样式。
1577 |
1578 | **正例:**
1579 |
1580 | ```
1581 |
1582 | child-txt
1583 |
1584 |
1585 |
1590 | 复制代码
1591 | ```
1592 |
1593 | 此时,父组件中颜色为`red`,子组件中颜色为`green`。
1594 |
1595 | **主要原因:**
1596 |
1597 | 
1598 |
1599 |  例子中的DOM节点和CSS层叠样式中都被添加了`data-v-xxx`来表示唯一,所以`scoped`是给当前组件的节点和样式唯一标识为`data-v-xxx`,避免了样式覆盖。
1600 |
--------------------------------------------------------------------------------
/面试题精选集合/金三银四.md:
--------------------------------------------------------------------------------
1 | ### 1.Vue3.0性能提升主要是体现在哪些方面
2 |
3 | #### 1.响应式系统
4 |
5 | ```
6 | - Vue.js 2.x 中响应式系统的核心是 Object.definePropertry,劫持整个对象,然后进行深度遍历所有属性,给每个属性添加`getter`和`setter`,实现响应式
7 | - Vue.js 3.x 中使用 Proxy 对象重写响应式系统
8 | - 可以监听动态新增的属性
9 | - 可以监听删除的属性
10 | - 可以监听数组的索引和length属性
11 |
12 | * 实现原理:
13 |
14 | * 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
15 |
16 | * 通过Reflect(反射): 对源对象的属性进行操作。
17 |
18 | * MDN文档中描述的Proxy与Reflect:
19 |
20 | * Proxy:[Proxy - JavaScript | MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy "Proxy - JavaScript | MDN")
21 |
22 | * Reflect:[Reflect - JavaScript | MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect "Reflect - JavaScript | MDN")
23 |
24 | new Proxy(data, {
25 | // 拦截读取属性值
26 | get (target, prop) {
27 | return Reflect.get(target, prop)
28 | },
29 | // 拦截设置属性值或添加新属性
30 | set (target, prop, value) {
31 | return Reflect.set(target, prop, value)
32 | },
33 | // 拦截删除属性
34 | deleteProperty (target, prop) {
35 | return Reflect.deleteProperty(target, prop)
36 | }
37 | })
38 |
39 | proxy.name = 'tom' ![]
40 | ```
41 |
42 | #### 2.编译阶段
43 |
44 | ```
45 | - Vue.js 2.x 通过标记静态节点,优化 diff 的过程
46 | - Vue.js 3.x
47 | * vue.js 3.x中标记和提升所有的静态节点,diff的时候只需要对比动态节点内容;
48 | * Fragments(升级vetur插件): template中不需要唯一根节点,可以直接放文本或者同级标签
49 | * 静态提升(hoistStatic),当使用 hoistStatic 时,所有静态的节点都被提升到 render 方法之外.只会在应用启动的时候被创建一次,之后使用只需要应用提取的静态节点,随着每次的渲染被不停的复用。
50 | * patch flag, 在动态标签末尾加上相应的标记,只能带 patchFlag 的节点才被认为是动态的元素,会被追踪属性的修改,能快速的找到动态节点,而不用逐个逐层遍历,提高了虚拟dom diff的性能。
51 | * 缓存事件处理函数cacheHandler,避免每次触发都要重新生成全新的function去更新之前的函数
52 | ```
53 |
54 | #### 3.源码体积
55 |
56 | ```
57 | - 相比Vue2,Vue3整体体积变小了,除了移出一些不常用的AP
58 | - tree shanking
59 | - 任何一个函数,如ref、reavtived、computed等,仅仅在用到的时候才打包
60 | - 通过编译阶段的静态分析,找到没有引入的模块并打上标记,将这些模块都给摇掉
61 | ```
62 |
63 | ### 2.vue3有哪些新的组件
64 |
65 | #### 1.Fragment
66 |
67 | ```
68 | * 在Vue2中: 组件必须有一个根标签
69 |
70 | * 在Vue3中: 组件可以没有根标签, 内部会将多个标签包含在一个Fragment虚拟元素中
71 |
72 | * 好处: 减少标签层级, 减小内存占用
73 | ```
74 |
75 | #### 2.Teleport
76 |
77 | 什么是Teleport?—— `Teleport` 是一种能够将我们的**组件html结构**移动到指定位置的技术。
78 |
79 | ```
80 |
81 |
82 |
83 |
我是一个弹窗
84 |
85 |
86 |
87 |
88 | ```
89 |
90 | #### 3.Suspense
91 |
92 | - 等待异步组件时渲染一些额外内容,让应用有更好的用户体验
93 |
94 | - 使用步骤:
95 |
96 | - 异步引入组件
97 |
98 | ```
99 | import {defineAsyncComponent} from 'vue'
100 | const Child = defineAsyncComponent(()=>import('./components/Child.vue'))
101 | ```
102 |
103 | - 使用`Suspense`包裹组件,并配置好`default` 与 `fallback`
104 |
105 | ```
106 |
107 |
108 |
我是App组件
109 |
110 |
111 |
112 |
113 |
114 | 加载中.....
115 |
116 |
117 |
118 |
119 | ```
120 |
121 | ### 3.Vue2.0 和 Vue3.0 有什么区别
122 |
123 | ```
124 | 1. 响应式系统的重新配置,使用代理替换对象.define属性,使用代理优势:
125 |
126 | * 可直接监控阵列类型的数据变化
127 | * 监听的目标是对象本身,不需要像Object.defineProperty那样遍历每个属性,有一定的性能提升
128 | * 可拦截应用、拥有密钥、有等13种方法,以及Object.define属性没有办法
129 | * 直接添加对象属性/删除
130 |
131 | 1. 新增组合API,更好的逻辑重用和代码组织
132 | 2. 重构虚拟 DOM
133 |
134 | * 模板编译时间优化,将一些静态节点编译成常量
135 | * slot优化,采取槽编译成懒人功能,拿槽渲染的决定留给子组件
136 | * 在模板中提取和重用内联事件(最初,每次渲染时都会重新生成内联函数)
137 |
138 | 1. 代码结构调整,更方便树摇动,使其更小
139 | 2. 使用打字脚本替换流
140 | ```
141 |
142 | ### 4.Vue 生命周期
143 |
144 | #### 1.vue2.x的生命周期
145 |
146 | 
147 |
148 | #### 2.vue3.0的生命周期
149 |
150 | 
151 |
152 | ```
153 | * Vue3.0中可以继续使用Vue2.x中的生命周期钩子,但有有两个被更名:
154 |
155 | * `beforeDestroy`改名为 `beforeUnmount`
156 |
157 | * `destroyed`改名为 `unmounted`
158 |
159 | * Vue3.0也提供了 Composition API 形式的生命周期钩子,与Vue2.x中钩子对应关系如下:
160 |
161 | * `beforeCreate`===>`setup()`
162 |
163 | * `created`=======>`setup()`
164 |
165 | * `beforeMount` ===>`onBeforeMount`
166 |
167 | * `mounted`=======>`onMounted`
168 |
169 | * `beforeUpdate`===>`onBeforeUpdate`
170 |
171 | * `updated` =======>`onUpdated`
172 |
173 | * `beforeUnmount` ==>`onBeforeUnmount`
174 |
175 | * `unmounted` =====>`onUnmounted`
176 |
177 |
178 |
179 | Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。
180 | 1、beforeCreate(创建前) :数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。
181 | 2、created(创建后) :实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 `$el` 属性。
182 | 3、beforeMount(挂载前) :在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面上。
183 | 4、mounted(挂载后) :在el被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html 页面中。此过程中进行ajax交互。
184 | 5、beforeUpdate(更新前) :响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。
185 | 6、updated(更新后):在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
186 | 7、beforeDestroy(销毁前) :实例销毁之前调用。这一步,实例仍然完全可用,`this` 仍能获取到实例。
187 | 8、destroyed(销毁后) :实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。
188 | ```
189 |
190 |
191 |
192 | ### 5.都说 Composition API 和 React Hook 很像,请问他们的区别是什么
193 |
194 | ```
195 | 从 React Hook 从实现的角度来看,React Hook 是基于 useState 的调用顺序来确定下一个 re 渲染时间状态从哪个 useState 开始,所以有以下几个限制
196 |
197 | * 不在循环中、条件、调用嵌套函数 Hook
198 | * 你必须确保它总是在你这边 React Top level 调用函数 Hook
199 | * 使用效果、使用备忘录 依赖关系必须手动确定
200 |
201 | 和 Composition API 是基于 Vue 的响应系统,和 React Hook 相比
202 |
203 | * 在设置函数中,一个组件实例只调用一次设置,而 React Hook 每次重新渲染时,都需要调用 Hook,给 React 带来的 GC 比 Vue 更大的压力,性能也相对 Vue 对我来说也比较慢
204 | * Compositon API 你不必担心调用的顺序,它也可以在循环中、条件、在嵌套函数中使用
205 | * 响应式系统自动实现依赖关系收集,而且组件的性能优化是由 Vue 内部完成的,而 React Hook 的依赖关系需要手动传递,并且依赖关系的顺序必须得到保证,让路 useEffect、useMemo 等等,否则组件性能会因为依赖关系不正确而下降。
206 |
207 | 虽然Compoliton API看起来像React Hook来使用,但它的设计思路也是React Hook的参考。
208 | ```
209 |
210 | ### 6. Composition Api 与Options Api 有什么不同
211 |
212 | #### 1.Options Api
213 |
214 | `Options API`,即大家常说的选项API,即以`vue`为后缀的文件,通过定义`methods`,`computed`,`watch`,`data`等属性与方法,共同处理页面逻辑
215 |
216 | 如下图:
217 |
218 | 
219 |
220 | 可以看到`Options`代码编写方式,如果是组件状态,则写在`data`属性上,如果是方法,则写在`methods`属性上...
221 |
222 | 用组件的选项 (`data`、`computed`、`methods`、`watch`) 组织逻辑在大多数情况下都有效
223 |
224 | 然而,当组件变得复杂,导致对应属性的列表也会增长,这可能会导致组件难以阅读和理解
225 |
226 | #### 2.Composition Api
227 |
228 | 在 Vue3 Composition API 中,组件根据逻辑功能来组织的,一个功能所定义的所有 API 会放在一起(更加的高内聚,低耦合)
229 |
230 | 即使项目很大,功能很多,我们都能快速的定位到这个功能所用到的所有 API
231 |
232 | 
233 |
234 | #### 3.对比
235 |
236 | 下面对`Composition Api`与`Options Api`进行两大方面的比较
237 |
238 | - 逻辑组织
239 | - 逻辑复用
240 |
241 | ##### 逻辑组织
242 |
243 | ##### Options API
244 |
245 | 假设一个组件是一个大型组件,其内部有很多处理逻辑关注点(对应下图不用颜色)
246 |
247 | 
248 |
249 | 可以看到,这种碎片化使得理解和维护复杂组件变得困难
250 |
251 | 选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块
252 |
253 | ##### Compostion API
254 |
255 | 而`Compositon API`正是解决上述问题,将某个逻辑关注点相关的代码全都放在一个函数里,这样当需要修改一个功能时,就不再需要在文件中跳来跳去
256 |
257 | 下面举个简单例子,将处理`count`属性相关的代码放在同一个函数了
258 |
259 | ```
260 | function useCount() {
261 | let count = ref(10);
262 | let double = computed(() => {
263 | return count.value * 2;
264 | });
265 |
266 | const handleConut = () => {
267 | count.value = count.value * 2;
268 | };
269 |
270 | console.log(count);
271 |
272 | return {
273 | count,
274 | double,
275 | handleConut,
276 | };
277 | }
278 | ```
279 |
280 | 组件上中使用`count`
281 |
282 | ```
283 | export default defineComponent({
284 | setup() {
285 | const { count, double, handleConut } = useCount();
286 | return {
287 | count,
288 | double,
289 | handleConut
290 | }
291 | },
292 | });
293 | ```
294 |
295 | 再来一张图进行对比,可以很直观地感受到 `Composition API`在逻辑组织方面的优势,以后修改一个属性功能的时候,只需要跳到控制该属性的方法中即可
296 |
297 | 
298 |
299 | ##### 逻辑复用
300 |
301 | 在`Vue2`中,我们是用过`mixin`去复用相同的逻辑
302 |
303 | 下面举个例子,我们会另起一个`mixin.js`文件
304 |
305 | ```
306 | export const MoveMixin = {
307 | data() {
308 | return {
309 | x: 0,
310 | y: 0,
311 | };
312 | },
313 |
314 | methods: {
315 | handleKeyup(e) {
316 | console.log(e.code);
317 | // 上下左右 x y
318 | switch (e.code) {
319 | case "ArrowUp":
320 | this.y--;
321 | break;
322 | case "ArrowDown":
323 | this.y++;
324 | break;
325 | case "ArrowLeft":
326 | this.x--;
327 | break;
328 | case "ArrowRight":
329 | this.x++;
330 | break;
331 | }
332 | },
333 | },
334 |
335 | mounted() {
336 | window.addEventListener("keyup", this.handleKeyup);
337 | },
338 |
339 | unmounted() {
340 | window.removeEventListener("keyup", this.handleKeyup);
341 | },
342 | };
343 | ```
344 |
345 | 然后在组件中使用
346 |
347 | ```
348 |
349 |
350 | Mouse position: x {{ x }} / y {{ y }}
351 |
352 |
353 |
359 | ```
360 |
361 | 使用单个`mixin`似乎问题不大,但是当我们一个组件混入大量不同的 `mixins` 的时候
362 |
363 | ```
364 | mixins: [mousePositionMixin, fooMixin, barMixin, otherMixin]
365 | ```
366 |
367 | 会存在两个非常明显的问题:
368 |
369 | - 命名冲突
370 | - 数据来源不清晰
371 |
372 | 现在通过`Compositon API`这种方式改写上面的代码
373 |
374 | ```
375 | import { onMounted, onUnmounted, reactive } from "vue";
376 | export function useMove() {
377 | const position = reactive({
378 | x: 0,
379 | y: 0,
380 | });
381 |
382 | const handleKeyup = (e) => {
383 | console.log(e.code);
384 | // 上下左右 x y
385 | switch (e.code) {
386 | case "ArrowUp":
387 | // y.value--;
388 | position.y--;
389 | break;
390 | case "ArrowDown":
391 | // y.value++;
392 | position.y++;
393 | break;
394 | case "ArrowLeft":
395 | // x.value--;
396 | position.x--;
397 | break;
398 | case "ArrowRight":
399 | // x.value++;
400 | position.x++;
401 | break;
402 | }
403 | };
404 |
405 | onMounted(() => {
406 | window.addEventListener("keyup", handleKeyup);
407 | });
408 |
409 | onUnmounted(() => {
410 | window.removeEventListener("keyup", handleKeyup);
411 | });
412 |
413 | return { position };
414 | }
415 | ```
416 |
417 | 在组件中使用
418 |
419 | ```
420 |
421 |
422 | Mouse position: x {{ x }} / y {{ y }}
423 |
424 |
425 |
426 |
441 | ```
442 |
443 | 可以看到,整个数据来源清晰了,即使去编写更多的 hook 函数,也不会出现命名冲突的问题
444 |
445 | ##### 小结
446 |
447 | - 在逻辑组织和逻辑复用方面,`Composition API`是优于`Options API`
448 | - 因为`Composition API`几乎是函数,会有更好的类型推断。
449 | - `Composition API`对 `tree-shaking` 友好,代码也更容易压缩
450 | - `Composition API`中见不到`this`的使用,减少了`this`指向不明的情况
451 | - 如果是小型组件,可以继续使用`Options API`,也是十分友好的
452 |
453 | ### 7.什么是SPA单页面应用,首屏加载你是如何优化的
454 |
455 | ```
456 | 单页Web应用(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML页面并在用户与应用程序交互时动态更新该页面的Web应用程序。我们开发的`Vue`项目大多是借助个官方的`CLI`脚手架,快速搭建项目,直接通过`new Vue`构建一个实例,并将`el:'#app'`挂载参数传入,最后通过`npm run build`的方式打包后生成一个`index.html`,称这种只有一个`HTML`的页面为单页面应用。
457 |
458 | 当然,`vue`也可以像`jq`一样引入,作为多页面应用的基础框架。
459 |
460 |
461 | SPA首屏优化方式
462 |
463 | 减小入口文件积
464 | 静态资源本地缓存
465 | UI框架按需加载
466 | 图片资源的压缩
467 | 组件重复打包
468 | 开启GZip压缩
469 | 使用SSR
470 | ```
471 |
472 | ### 8.对Vue项目你做过哪些性能优化
473 |
474 | ```
475 | 1、`v-if`和`v-show`
476 |
477 | * 频繁切换时使用`v-show`,利用其缓存特性
478 | * 首屏渲染时使用`v-if`,如果为`false`则不进行渲染
479 |
480 | 2、`v-for`的`key`
481 |
482 | * 列表变化时,循环时使用唯一不变的`key`,借助其本地复用策略
483 | * 列表只进行一次渲染时,`key`可以采用循环的`index`
484 |
485 | 3、侦听器和计算属性
486 |
487 | * 侦听器`watch`用于数据变化时引起其他行为
488 | * 多使用`compouter`计算属性顾名思义就是新计算而来的属性,如果依赖的数据未发生变化,不会触发重新计算
489 |
490 | 4、合理使用生命周期
491 |
492 | * 在`destroyed`阶段进行绑定事件或者定时器的销毁
493 | * 使用动态组件的时候通过`keep-alive`包裹进行缓存处理,相关的操作可以在`actived`阶段激活
494 |
495 | 5、数据响应式处理
496 |
497 | * 不需要响应式处理的数据可以通过`Object.freeze`处理,或者直接通过`this.xxx = xxx`的方式进行定义
498 | * 需要响应式处理的属性可以通过`this.$set`的方式处理,而不是`JSON.parse(JSON.stringify(XXX))`的方式
499 |
500 | 6、路由加载方式
501 |
502 | * 页面组件可以采用异步加载的方式
503 |
504 | 7、插件引入
505 |
506 | * 第三方插件可以采用按需加载的方式,比如`element-ui`。
507 |
508 | 8、减少代码量
509 |
510 | * 采用`mixin`的方式抽离公共方法
511 | * 抽离公共组件
512 | * 定义公共方法至公共`js`中
513 | * 抽离公共`css`
514 |
515 | 9、编译方式
516 |
517 | * 如果线上需要`template`的编译,可以采用完成版`vue.esm.js`
518 | * 如果线上无需`template`的编译,可采用运行时版本`vue.runtime.esm.js`,相比完整版体积要小大约`30%`
519 |
520 | 10、渲染方式
521 |
522 | * 服务端渲染,如果是需要`SEO`的网站可以采用服务端渲染的方式
523 | * 前端渲染,一些企业内部使用的后端管理系统可以采用前端渲染的方式
524 |
525 | 11、字体图标的使用
526 |
527 | * 有些图片图标尽可能使用字体图标
528 | ```
529 |
530 | ### 9.Vue组件通信的方式有哪些
531 |
532 | ```
533 | vue中8种常规的通信方案
534 |
535 | 通过 props 传递
536 | 通过 $emit 触发自定义事件
537 | 使用 ref
538 | EventBus
539 | $parent 或$root
540 | attrs 与 listeners
541 | Provide 与 Inject
542 | Vuex
543 |
544 | 组件间通信的分类可以分成以下
545 |
546 | 父子关系的组件数据传递选择 props 与 $emit进行传递,也可选择ref
547 | 兄弟关系的组件数据传递可选择$bus,其次可以选择$parent进行传递
548 | 祖先与后代组件数据传递可选择attrs与listeners或者 Provide与 Inject
549 | 复杂关系的组件数据传递可以通过vuex存放共享的变量
550 | ```
551 |
552 | ### 10.Vue常用的修饰符有哪些
553 |
554 | ````
555 | 1、表单修饰符
556 |
557 | (1)`.lazy`
558 |
559 | 在默认情况下,`v-model` 在每次 `input` 事件触发后将输入框的值与数据进行同步 ,可以添加 `lazy` 修饰符,从而转为在 `change` 事件之后进行同步:
560 |
561 | ```
562 |
563 |
564 | ```
565 |
566 | (2)`.number`
567 |
568 | 如果想自动将用户的输入值转为数值类型,可以给 `v-model` 添加 `number` 修饰符:
569 |
570 | ```
571 |
572 |
573 | ```
574 |
575 | (3)`.trim`
576 |
577 | 如果要自动过滤用户输入的首尾空白字符,可以给 `v-model` 添加 `trim` 修饰符:
578 |
579 | ```
580 |
581 |
582 | ```
583 |
584 | 2、事件修饰符
585 |
586 | (1)`.stop`
587 |
588 | 阻止单击事件继续传播。
589 |
590 | ```
591 |
592 |
593 |
594 | ```
595 |
596 | (2)`.prevent`
597 |
598 | 阻止标签的默认行为。
599 |
600 | ```
601 | 点击
602 |
603 | ```
604 |
605 | (3)`.capture`
606 |
607 | 事件先在有`.capture`修饰符的节点上触发,然后在其包裹的内部节点中触发。
608 |
609 | ```
610 |
611 |
612 |
613 | ```
614 |
615 | (4)`.self`
616 |
617 | 只当在 event.target 是当前元素自身时触发处理函数,即事件不是从内部元素触发的。
618 |
619 | ```
620 |
621 |
622 |
623 | ```
624 |
625 | (5)`.once`
626 |
627 | 不像其它只能对原生的 DOM 事件起作用的修饰符,`.once` 修饰符还能被用到自定义的组件事件上,表示当前事件只触发一次。
628 |
629 | ```
630 | 点击
631 |
632 | ```
633 | (6)`.passive`
634 |
635 | `.passive` 修饰符尤其能够提升移动端的性能
636 |
637 | ```
638 |
639 |
640 |
641 | ...
642 | ```
643 |
644 | ````
645 |
646 | ### 11.Vue中的$nextTick有什么作用
647 |
648 | ```
649 | const callbacks = []
650 | let pending = false
651 |
652 | /**
653 | * 完成两件事:
654 | * 1、用 try catch 包装 flushSchedulerQueue 函数,然后将其放入 callbacks 数组
655 | * 2、如果 pending 为 false,表示现在浏览器的任务队列中没有 flushCallbacks 函数
656 | * 如果 pending 为 true,则表示浏览器的任务队列中已经被放入了 flushCallbacks 函数,
657 | * 待执行 flushCallbacks 函数时,pending 会被再次置为 false,表示下一个 flushCallbacks 函数可以进入
658 | * 浏览器的任务队列了
659 | * pending 的作用:保证在同一时刻,浏览器的任务队列中只有一个 flushCallbacks 函数
660 | * @param {*} cb 接收一个回调函数 => flushSchedulerQueue
661 | * @param {*} ctx 上下文
662 | * @returns
663 | */
664 | export function nextTick (cb?: Function, ctx?: Object) {
665 | let _resolve
666 | // 用 callbacks 数组存储经过包装的 cb 函数
667 | callbacks.push(() => {
668 | if (cb) {
669 | // 用 try catch 包装回调函数,便于错误捕获
670 | try {
671 | cb.call(ctx)
672 | } catch (e) {
673 | handleError(e, ctx, 'nextTick')
674 | }
675 | } else if (_resolve) {
676 | _resolve(ctx)
677 | }
678 | })
679 | if (!pending) {
680 | pending = true
681 | // 执行 timerFunc,在浏览器的任务队列中(首选微任务队列)放入 flushCallbacks 函数
682 | timerFunc()
683 | }
684 | // $flow-disable-line
685 | if (!cb && typeof Promise !== 'undefined') {
686 | return new Promise(resolve => {
687 | _resolve = resolve
688 | })
689 | }
690 | }
691 | ```
692 |
693 | ```
694 | 官方对其的定义
695 |
696 | 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM
697 |
698 | 什么意思呢?
699 |
700 | 我们可以理解成,Vue 在更新 DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新
701 |
702 | Vue 的异步更新机制的核心是利用了浏览器的异步任务队列来实现的,首选微任务队列,宏任务队列次之。
703 |
704 | 当响应式数据更新后,会调用 dep.notify 方法,通知 dep 中收集的 watcher 去执行 update 方法,watcher.update 将 watcher 自己放入一个 watcher 队列(全局的 queue 数组)。
705 |
706 | 然后通过 nextTick 方法将一个刷新 watcher 队列的方法(flushSchedulerQueue)放入一个全局的 callbacks 数组中。
707 |
708 | 如果此时浏览器的异步任务队列中没有一个叫 flushCallbacks 的函数,则执行 timerFunc 函数,将 flushCallbacks 函数放入异步任务队列。如果异步任务队列中已经存在 flushCallbacks 函数,等待其执行完成以后再放入下一个 flushCallbacks 函数。
709 |
710 | flushCallbacks 函数负责执行 callbacks 数组中的所有 flushSchedulerQueue 函数。
711 |
712 | flushSchedulerQueue 函数负责刷新 watcher 队列,即执行 queue 数组中每一个 watcher 的 run 方法,从而进入更新阶段,比如执行组件更新函数或者执行用户 watch 的回调函数。
713 | ```
714 |
715 | ### 12.如何理解双向数据绑定
716 |
717 | ```
718 | 我们都知道 Vue 是数据双向绑定的框架,双向绑定由三个重要部分构成
719 |
720 | 数据层(Model):应用的数据及业务逻辑
721 | 视图层(View):应用的展示效果,各类UI组件
722 | 业务逻辑层(ViewModel):框架封装的核心,它负责将数据与视图关联起来
723 | 而上面的这个分层的架构方案,可以用一个专业术语进行称呼:MVVM这里的控制层的核心功能便是 “数据双向绑定” 。自然,我们只需弄懂它是什么,便可以进一步了解数据绑定的原理
724 |
725 | 理解ViewModel
726 | 它的主要职责就是:
727 |
728 | 数据变化后更新视图
729 | 视图变化后更新数据
730 | 当然,它还有两个主要部分组成
731 |
732 | 监听器(Observer):对所有数据的属性进行监听
733 | 解析器(Compiler):对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
734 | ```
735 |
736 | ### 13.v-show和v-if有什么区别?你可以讲讲吗
737 |
738 | ```
739 | v-show 与 v-if 的作用效果是相同的(不含v-else),都能控制元素在页面是否显示,在用法上也是相同的
740 |
741 | - 区别
742 | 控制手段不同
743 | 编译过程不同
744 | 编译条件不同
745 |
746 | 控制手段:v-show隐藏则是为该元素添加css--display:none,dom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删除
747 |
748 | 编译过程:v-if切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换
749 |
750 | 编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染
751 |
752 | v-show 由false变为true的时候不会触发组件的生命周期
753 |
754 | v-if由false变为true的时候,触发组件的beforeCreate、create、beforeMount、mounted钩子,由true变为false的时候触发组件的beforeDestory、destoryed方法
755 |
756 | 性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗
757 | ```
758 |
759 | ### 14.有用过keep-alive吗?它有什么作用
760 |
761 | ```
762 | `vue`中支持组件化,并且也有用于缓存的内置组件`keep-alive`可直接使用,使用场景为`路由组件`和`动态组件`。
763 |
764 | * `activated`表示进入组件的生命周期,`deactivated`表示离开组件的生命周期
765 | * `include`表示匹配到的才缓存,`exclude`表示匹配到的都不缓存
766 | * `max`表示最多可以缓存多少组件
767 |
768 |
769 | 关于keep-alive的基本用法:
770 |
771 |
772 |
773 |
774 | 使用includes和exclude:
775 |
776 |
777 |
778 |
779 |
780 |
781 |
782 |
783 |
784 |
785 |
786 |
787 |
788 |
789 | 匹配首先检查组件自身的 name 选项,如果 name 选项不可用,则匹配它的局部注册名称 (父组件 components 选项的键值),匿名组件不能被匹配
790 |
791 | 设置了 keep-alive 缓存的组件,会多出两个生命周期钩子(activated与deactivated):
792 |
793 | 首次进入组件时:beforeRouteEnter > beforeCreate > created> mounted > activated > ... ... > beforeRouteLeave > deactivated
794 |
795 | 再次进入组件时:beforeRouteEnter >activated > ... ... > beforeRouteLeave > deactivated
796 | ```
797 |
798 | ### 15.你可以实现一个虚拟DOM吗
799 |
800 | **先看浏览器对`HTML`的理解**:
801 |
802 | ```
803 |
804 |
My title
805 | Some text content
806 |
807 |
808 | ```
809 |
810 | 当浏览器读到这些代码时,它会建立一个DOM树来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。 上述 HTML 对应的 DOM 节点树如下图所示:
811 |
812 | 
813 |
814 | ```
815 | 每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。
816 |
817 | **再看`Vue`对`HTML template`的理解**
818 |
819 | Vue 通过建立一个**虚拟 DOM** 来追踪自己要如何改变真实 DOM。因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“**VNode**”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
820 |
821 | 简言之,浏览器对HTML的理解是DOM树,Vue对`HTML`的理解是虚拟DOM,最后在`patch`阶段通过DOM操作的api将其渲染成真实的DOM节点。
822 | ```
823 |
824 | #### 如何实现虚拟DOM
825 |
826 | 首先可以看看`vue`中`VNode`的结构
827 |
828 | 源码位置:src/core/vdom/vnode.js
829 |
830 | ```
831 | export default class VNode {
832 | tag: string | void;
833 | data: VNodeData | void;
834 | children: ?Array;
835 | text: string | void;
836 | elm: Node | void;
837 | ns: string | void;
838 | context: Component | void; // rendered in this component's scope
839 | functionalContext: Component | void; // only for functional component root nodes
840 | key: string | number | void;
841 | componentOptions: VNodeComponentOptions | void;
842 | componentInstance: Component | void; // component instance
843 | parent: VNode | void; // component placeholder node
844 | raw: boolean; // contains raw HTML? (server only)
845 | isStatic: boolean; // hoisted static node
846 | isRootInsert: boolean; // necessary for enter transition check
847 | isComment: boolean; // empty comment placeholder?
848 | isCloned: boolean; // is a cloned node?
849 | isOnce: boolean; // is a v-once node?
850 |
851 | constructor (
852 | tag?: string,
853 | data?: VNodeData,
854 | children?: ?Array,
855 | text?: string,
856 | elm?: Node,
857 | context?: Component,
858 | componentOptions?: VNodeComponentOptions
859 | ) {
860 | /*当前节点的标签名*/
861 | this.tag = tag
862 | /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
863 | this.data = data
864 | /*当前节点的子节点,是一个数组*/
865 | this.children = children
866 | /*当前节点的文本*/
867 | this.text = text
868 | /*当前虚拟节点对应的真实dom节点*/
869 | this.elm = elm
870 | /*当前节点的名字空间*/
871 | this.ns = undefined
872 | /*编译作用域*/
873 | this.context = context
874 | /*函数化组件作用域*/
875 | this.functionalContext = undefined
876 | /*节点的key属性,被当作节点的标志,用以优化*/
877 | this.key = data && data.key
878 | /*组件的option选项*/
879 | this.componentOptions = componentOptions
880 | /*当前节点对应的组件的实例*/
881 | this.componentInstance = undefined
882 | /*当前节点的父节点*/
883 | this.parent = undefined
884 | /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
885 | this.raw = false
886 | /*静态节点标志*/
887 | this.isStatic = false
888 | /*是否作为跟节点插入*/
889 | this.isRootInsert = true
890 | /*是否为注释节点*/
891 | this.isComment = false
892 | /*是否为克隆节点*/
893 | this.isCloned = false
894 | /*是否有v-once指令*/
895 | this.isOnce = false
896 | }
897 |
898 | // DEPRECATED: alias for componentInstance for backwards compat.
899 | /* istanbul ignore next https://github.com/answershuto/learnVue*/
900 | get child (): Component | void {
901 | return this.componentInstance
902 | }
903 | }
904 | ```
905 |
906 | 这里对`VNode`进行稍微的说明:
907 |
908 | - 所有对象的 `context` 选项都指向了 `Vue` 实例
909 | - `elm` 属性则指向了其相对应的真实 `DOM` 节点
910 |
911 | ```
912 | vue`是通过`createElement`生成`VNode
913 | ```
914 |
915 | 源码位置:src/core/vdom/create-element.js
916 |
917 | ```
918 | export function createElement (
919 | context: Component,
920 | tag: any,
921 | data: any,
922 | children: any,
923 | normalizationType: any,
924 | alwaysNormalize: boolean
925 | ): VNode | Array {
926 | if (Array.isArray(data) || isPrimitive(data)) {
927 | normalizationType = children
928 | children = data
929 | data = undefined
930 | }
931 | if (isTrue(alwaysNormalize)) {
932 | normalizationType = ALWAYS_NORMALIZE
933 | }
934 | return _createElement(context, tag, data, children, normalizationType)
935 | }
936 | ```
937 |
938 | 上面可以看到`createElement` 方法实际上是对 `_createElement` 方法的封装,对参数的传入进行了判断
939 |
940 | ```
941 | export function _createElement(
942 | context: Component,
943 | tag?: string | Class | Function | Object,
944 | data?: VNodeData,
945 | children?: any,
946 | normalizationType?: number
947 | ): VNode | Array {
948 | if (isDef(data) && isDef((data: any).__ob__)) {
949 | process.env.NODE_ENV !== 'production' && warn(
950 | `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
951 | 'Always create fresh vnode data objects in each render!',
952 | context`
953 | )
954 | return createEmptyVNode()
955 | }
956 | // object syntax in v-bind
957 | if (isDef(data) && isDef(data.is)) {
958 | tag = data.is
959 | }
960 | if (!tag) {
961 | // in case of component :is set to falsy value
962 | return createEmptyVNode()
963 | }
964 | ...
965 | // support single function children as default scoped slot
966 | if (Array.isArray(children) &&
967 | typeof children[0] === 'function'
968 | ) {
969 | data = data || {}
970 | data.scopedSlots = { default: children[0] }
971 | children.length = 0
972 | }
973 | if (normalizationType === ALWAYS_NORMALIZE) {
974 | children = normalizeChildren(children)
975 | } else if ( === SIMPLE_NORMALIZE) {
976 | children = simpleNormalizeChildren(children)
977 | }
978 | // 创建VNode
979 | ...
980 | }
981 | ```
982 |
983 | 可以看到`_createElement`接收5个参数:
984 |
985 | - `context` 表示 `VNode` 的上下文环境,是 `Component` 类型
986 | - tag 表示标签,它可以是一个字符串,也可以是一个 `Component`
987 | - `data` 表示 `VNode` 的数据,它是一个 `VNodeData` 类型
988 | - `children` 表示当前 `VNode`的子节点,它是任意类型的
989 | - `normalizationType` 表示子节点规范的类型,类型不同规范的方法也就不一样,主要是参考 `render` 函数是编译生成的还是用户手写的
990 |
991 | 根据`normalizationType` 的类型,`children`会有不同的定义
992 |
993 | ```
994 | if (normalizationType === ALWAYS_NORMALIZE) {
995 | children = normalizeChildren(children)
996 | } else if ( === SIMPLE_NORMALIZE) {
997 | children = simpleNormalizeChildren(children)
998 | }
999 | ```
1000 |
1001 | `simpleNormalizeChildren`方法调用场景是 `render` 函数是编译生成的
1002 |
1003 | `normalizeChildren`方法调用场景分为下面两种:
1004 |
1005 | - `render` 函数是用户手写的
1006 | - 编译 `slot`、`v-for` 的时候会产生嵌套数组
1007 |
1008 | 无论是`simpleNormalizeChildren`还是`normalizeChildren`都是对`children`进行规范(使`children` 变成了一个类型为 `VNode` 的 `Array`),这里就不展开说了
1009 |
1010 | 规范化`children`的源码位置在:src/core/vdom/helpers/normalzie-children.js
1011 |
1012 | 在规范化`children`后,就去创建`VNode`
1013 |
1014 | ```
1015 | let vnode, ns
1016 | // 对tag进行判断
1017 | if (typeof tag === 'string') {
1018 | let Ctor
1019 | ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
1020 | if (config.isReservedTag(tag)) {
1021 | // 如果是内置的节点,则直接创建一个普通VNode
1022 | vnode = new VNode(
1023 | config.parsePlatformTagName(tag), data, children,
1024 | undefined, undefined, context
1025 | )
1026 | } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
1027 | // component
1028 | // 如果是component类型,则会通过createComponent创建VNode节点
1029 | vnode = createComponent(Ctor, data, context, children, tag)
1030 | } else {
1031 | vnode = new VNode(
1032 | tag, data, children,
1033 | undefined, undefined, context
1034 | )
1035 | }
1036 | } else {
1037 | // direct component options / constructor
1038 | vnode = createComponent(tag, data, context, children)
1039 | }
1040 | ```
1041 |
1042 | ```
1043 | createComponent`同样是创建`VNode
1044 | ```
1045 |
1046 | 源码位置:src/core/vdom/create-component.js
1047 |
1048 | ```
1049 | export function createComponent (
1050 | Ctor: Class | Function | Object | void,
1051 | data: ?VNodeData,
1052 | context: Component,
1053 | children: ?Array,
1054 | tag?: string
1055 | ): VNode | Array | void {
1056 | if (isUndef(Ctor)) {
1057 | return
1058 | }
1059 | // 构建子类构造函数
1060 | const baseCtor = context.$options._base
1061 |
1062 | // plain options object: turn it into a constructor
1063 | if (isObject(Ctor)) {
1064 | Ctor = baseCtor.extend(Ctor)
1065 | }
1066 |
1067 | // if at this stage it's not a constructor or an async component factory,
1068 | // reject.
1069 | if (typeof Ctor !== 'function') {
1070 | if (process.env.NODE_ENV !== 'production') {
1071 | warn(`Invalid Component definition: ${String(Ctor)}`, context)
1072 | }
1073 | return
1074 | }
1075 |
1076 | // async component
1077 | let asyncFactory
1078 | if (isUndef(Ctor.cid)) {
1079 | asyncFactory = Ctor
1080 | Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
1081 | if (Ctor === undefined) {
1082 | return createAsyncPlaceholder(
1083 | asyncFactory,
1084 | data,
1085 | context,
1086 | children,
1087 | tag
1088 | )
1089 | }
1090 | }
1091 |
1092 | data = data || {}
1093 |
1094 | // resolve constructor options in case global mixins are applied after
1095 | // component constructor creation
1096 | resolveConstructorOptions(Ctor)
1097 |
1098 | // transform component v-model data into props & events
1099 | if (isDef(data.model)) {
1100 | transformModel(Ctor.options, data)
1101 | }
1102 |
1103 | // extract props
1104 | const propsData = extractPropsFromVNodeData(data, Ctor, tag)
1105 |
1106 | // functional component
1107 | if (isTrue(Ctor.options.functional)) {
1108 | return createFunctionalComponent(Ctor, propsData, data, context, children)
1109 | }
1110 |
1111 | // extract listeners, since these needs to be treated as
1112 | // child component listeners instead of DOM listeners
1113 | const listeners = data.on
1114 | // replace with listeners with .native modifier
1115 | // so it gets processed during parent component patch.
1116 | data.on = data.nativeOn
1117 |
1118 | if (isTrue(Ctor.options.abstract)) {
1119 | const slot = data.slot
1120 | data = {}
1121 | if (slot) {
1122 | data.slot = slot
1123 | }
1124 | }
1125 |
1126 | // 安装组件钩子函数,把钩子函数合并到data.hook中
1127 | installComponentHooks(data)
1128 |
1129 | //实例化一个VNode返回。组件的VNode是没有children的
1130 | const name = Ctor.options.name || tag
1131 | const vnode = new VNode(
1132 | `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
1133 | data, undefined, undefined, undefined, context,
1134 | { Ctor, propsData, listeners, tag, children },
1135 | asyncFactory
1136 | )
1137 | if (__WEEX__ && isRecyclableComponent(vnode)) {
1138 | return renderRecyclableComponentTemplate(vnode)
1139 | }
1140 |
1141 | return vnode
1142 | }
1143 | ```
1144 |
1145 | 稍微提下`createComponent`生成`VNode`的三个关键流程:
1146 |
1147 | - 构造子类构造函数`Ctor`
1148 | - `installComponentHooks`安装组件钩子函数
1149 | - 实例化 `vnode`
1150 |
1151 | #### 小结
1152 |
1153 | `createElement` 创建 `VNode` 的过程,每个 `VNode` 有 `children`,`children` 每个元素也是一个`VNode`,这样就形成了一个虚拟树结构,用于描述真实的`DOM`树结构
1154 |
1155 | ### 16.为什么data属性是一个函数而不是一个对象,具体原因是什么
1156 |
1157 | ````
1158 | 是不是一定是函数,得看场景。并且,也无需担心什么时候该将`data`写为函数还是对象,因为`vue`内部已经做了处理,并在控制台输出错误信息。
1159 |
1160 | **场景一**:`new Vue({data: ...})`
1161 | 这种场景主要为项目入口或者多个`html`页面各实例化一个`Vue`时,这里的`data`即可用对象的形式,也可用工厂函数返回对象的形式。因为,这里的`data`只会出现一次,不存在重复引用而引起的数据污染问题。
1162 |
1163 | **场景二**:组件场景中的选项
1164 | 在生成组件`vnode`的过程中,组件会在生成构造函数的过程中执行合并策略:
1165 |
1166 | ```
1167 | // data合并策略
1168 | strats.data = function (
1169 | parentVal,
1170 | childVal,
1171 | vm
1172 | ) {
1173 | if (!vm) {
1174 | if (childVal && typeof childVal !== 'function') {
1175 | process.env.NODE_ENV !== 'production' && warn(
1176 | 'The "data" option should be a function ' +
1177 | 'that returns a per-instance value in component ' +
1178 | 'definitions.',
1179 | vm
1180 | );
1181 |
1182 | return parentVal
1183 | }
1184 | return mergeDataOrFn(parentVal, childVal)
1185 | }
1186 |
1187 | return mergeDataOrFn(parentVal, childVal, vm)
1188 | };
1189 | ```
1190 |
1191 | 如果合并过程中发现子组件的数据不是函数,即`typeof childVal !== 'function'`成立,进而在开发环境会在控制台输出警告并且直接返回`parentVal`,说明这里压根就没有把`childVal`中的任何`data`信息合并到`options`中去。
1192 |
1193 |
1194 | 上面讲到组件data必须是一个函数,不知道大家有没有思考过这是为什么呢?
1195 |
1196 | 在我们定义好一个组件的时候,vue最终都会通过Vue.extend()构成组件实例
1197 |
1198 | 这里我们模仿组件构造函数,定义data属性,采用对象的形式
1199 |
1200 | function Component(){
1201 |
1202 | }
1203 | Component.prototype.data = {
1204 | count : 0
1205 | }
1206 | 创建两个组件实例
1207 |
1208 | const componentA = new Component()
1209 | const componentB = new Component()
1210 | 修改componentA组件data属性的值,componentB中的值也发生了改变
1211 |
1212 | console.log(componentB.data.count) // 0
1213 | componentA.data.count = 1
1214 | console.log(componentB.data.count) // 1
1215 | 产生这样的原因这是两者共用了同一个内存地址,componentA修改的内容,同样对componentB产生了影响
1216 |
1217 | 如果我们采用函数的形式,则不会出现这种情况(函数返回的对象内存地址并不相同)
1218 |
1219 | function Component(){
1220 | this.data = this.data()
1221 | }
1222 | Component.prototype.data = function (){
1223 | return {
1224 | count : 0
1225 | }
1226 | }
1227 | 修改componentA组件data属性的值,componentB中的值不受影响
1228 |
1229 | console.log(componentB.data.count) // 0
1230 | componentA.data.count = 1
1231 | console.log(componentB.data.count) // 0
1232 | vue组件可能会有很多个实例,采用函数返回一个全新data形式,使每个实例对象的数据不会受到其他实例对象数据的污染
1233 | ````
1234 |
1235 | ### 17.Vue2的初始化过程你有过了解吗,做了哪些事情
1236 |
1237 | ```
1238 | new Vue走到了vue的构造函数中:`src\core\instance\index.js`文件。
1239 |
1240 | this._init(options)
1241 |
1242 | 然后从Mixin增加的原型方法看,initMixin(Vue),调用的是为Vue增加的原型方法_init
1243 |
1244 | // src/core/instance/init.js
1245 |
1246 | function initMixin (Vue) {
1247 | Vue.prototype._init = function (options) {
1248 | var vm = this; 创建vm,
1249 | ...
1250 | // 合并options 到 vm.$options
1251 | vm.$options = mergeOptions(
1252 | resolveConstructorOptions(vm.constructor),
1253 | options || {},
1254 | vm
1255 | );
1256 | }
1257 | ...
1258 | initLifecycle(vm); //初始生命周期
1259 | initEvents(vm); //初始化事件
1260 | initRender(vm); //初始render函数
1261 | callHook(vm, 'beforeCreate'); //执行 beforeCreate生命周期钩子
1262 | ...
1263 | initState(vm); //初始化data,props,methods computed,watch
1264 | ...
1265 | callHook(vm, 'created'); //执行 created 生命周期钩子
1266 |
1267 | if (vm.$options.el) {
1268 | vm.$mount(vm.$options.el); //这里也是重点,下面需要用到
1269 | }
1270 | }
1271 |
1272 | 总结
1273 |
1274 | 所以,从上面的函数看来,new vue所做的事情,就像一个流程图一样展开了,分别是
1275 |
1276 | - 合并配置
1277 | - 初始化生命周期
1278 | - 初始化事件
1279 | - 初始化渲染
1280 | - 调用 `beforeCreate` 钩子函数
1281 | - init injections and reactivity(这个阶段属性都已注入绑定,而且被 `$watch` 变成reactivity,但是 `$el` 还是没有生成,也就是DOM没有生成)
1282 | - 初始化state状态(初始化了data、props、computed、watcher)
1283 | - 调用created钩子函数。
1284 |
1285 | 在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM。
1286 | ```
1287 |
1288 | ### 18.Vue3初始化的一个大概流程
1289 |
1290 | ```
1291 | - 初始化的一个大概流程
1292 |
1293 | createApp() => mount() => render() => patch() => processComponent() => mountComponent()
1294 |
1295 | - 简易版流程编写
1296 |
1297 | 1.Vue.createApp() 实际执行的是renderer的createApp()
1298 |
1299 | 2.renderer是createRenderer这个方法创建
1300 |
1301 | 3.renderer的createApp()是createAppAPI()返回的
1302 |
1303 | 4.createAppApi接受到render之后,创建一个app实例,定义mount方法
1304 |
1305 | 5.mount会调用render函数。将vnode转换为真实dom
1306 |
1307 | createRenderer() => renderer => renderer.createApp() <= createAppApi()
1308 |
1309 |
1310 |
1311 |
1312 |
1401 | ```
1402 |
1403 | ### 19.vue3响应式api如何编写
1404 |
1405 | ```
1406 | var activeEffect = null;
1407 | function effect(fn) {
1408 | activeEffect = fn;
1409 | activeEffect();
1410 | activeEffect = null;
1411 | }
1412 | var depsMap = new WeakMap();
1413 | function gather(target, key) {
1414 | // 避免例如console.log(obj1.name)而触发gather
1415 | if (!activeEffect) return;
1416 | let depMap = depsMap.get(target);
1417 | if (!depMap) {
1418 | depsMap.set(target, (depMap = new Map()));
1419 | }
1420 | let dep = depMap.get(key);
1421 | if (!dep) {
1422 | depMap.set(key, (dep = new Set()));
1423 | }
1424 | dep.add(activeEffect)
1425 | }
1426 | function trigger(target, key) {
1427 | let depMap = depsMap.get(target);
1428 | if (depMap) {
1429 | const dep = depMap.get(key);
1430 | if (dep) {
1431 | dep.forEach((effect) => effect());
1432 | }
1433 | }
1434 | }
1435 | function reactive(target) {
1436 | const handle = {
1437 | set(target, key, value, receiver) {
1438 | Reflect.set(target, key, value, receiver);
1439 | trigger(receiver, key); // 设置值时触发自动更新
1440 | },
1441 | get(target, key, receiver) {
1442 | gather(receiver, key); // 访问时收集依赖
1443 | return Reflect.get(target, key, receiver);
1444 | },
1445 | };
1446 | return new Proxy(target, handle);
1447 | }
1448 |
1449 | function ref(name){
1450 | return reactive(
1451 | {
1452 | value: name
1453 | }
1454 | )
1455 | }
1456 | ```
1457 |
1458 | ### 20.在Vue项目中你是如何做的SSR渲染
1459 |
1460 | ```
1461 | 与传统 SPA (单页应用程序 (Single-Page Application)) 相比,服务器端渲染 (SSR) 的优势主要在于:
1462 |
1463 | * 更好的 SEO,由于搜索引擎爬虫抓取工具可以直接查看完全渲染的页面。
1464 | * 更快的内容到达时间 (time-to-content),特别是对于缓慢的网络情况或运行缓慢的设备。
1465 |
1466 | Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将这些静态标记"激活"为客户端上完全可交互的应用程序
1467 |
1468 | 服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行
1469 |
1470 | * Vue SSR是一个在SPA上进行改良的服务端渲染
1471 | * 通过Vue SSR渲染的页面,需要在客户端激活才能实现交互
1472 | * Vue SSR将包含两部分:服务端渲染的首屏,包含交互的SPA
1473 |
1474 | 使用ssr不存在单例模式,每次用户请求都会创建一个新的vue实例
1475 | 实现ssr需要实现服务端首屏渲染和客户端激活
1476 | 服务端异步获取数据asyncData可以分为首屏异步获取和切换组件获取
1477 | 首屏异步获取数据,在服务端预渲染的时候就应该已经完成
1478 | 切换组件通过mixin混入,在beforeMount钩子完成数据获取
1479 | ```
1480 |
1481 | ### 21.怎么看Vue的diff算法
1482 |
1483 | ```
1484 | diff 算法是一种通过同层的树节点进行比较的高效算法
1485 |
1486 | diff整体策略为:深度优先,同层比较
1487 | 比较只会在同层级进行, 不会跨层级比较
1488 | 比较的过程中,循环从两边向中间收拢
1489 |
1490 | - 当数据发生改变时,订阅者watcher就会调用patch给真实的DOM打补丁
1491 | - 通过isSameVnode进行判断,相同则调用patchVnode方法
1492 | - patchVnode做了以下操作:
1493 | - 找到对应的真实dom,称为el
1494 | - 如果都有都有文本节点且不相等,将el文本节点设置为Vnode的文本节点
1495 | - 如果oldVnode有子节点而VNode没有,则删除el子节点
1496 | - 如果oldVnode没有子节点而VNode有,则将VNode的子节点真实化后添加到el
1497 | - 如果两者都有子节点,则执行updateChildren函数比较子节点
1498 | - updateChildren主要做了以下操作:
1499 | - 设置新旧VNode的头尾指针
1500 | - 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode进行patch重复流程、调用createElem创建一个新节点,从哈希表寻找 key一致的VNode 节点再分情况操作
1501 | ```
1502 |
1503 | ### 22.从`0`到`1`构建一个`Vue`项目你需要做哪些内容
1504 |
1505 | ```
1506 | * 架子:选用合适的初始化脚手架(`vue-cli2.0`或者`vue-cli3.0`)
1507 | * 请求:数据`axios`请求的配置
1508 | * 登录:登录注册系统
1509 | * 路由:路由管理页面
1510 | * 数据:`vuex`全局数据管理
1511 | * 权限:权限管理系统
1512 | * 埋点:埋点系统
1513 | * 插件:第三方插件的选取以及引入方式
1514 | * 错误:错误页面
1515 | * 入口:前端资源直接当静态资源,或者服务端模板拉取
1516 | * `SEO`:如果考虑`SEO`建议采用`SSR`方案
1517 | * 组件:基础组件/业务组件
1518 | * 样式:样式预处理起,公共样式抽取
1519 | * 方法:公共方法抽离
1520 | ```
1521 |
1522 | ### 23. 介绍一下 js 的数据类型有哪些,值是如何存储的
1523 |
1524 | ```
1525 | JavaScript 一共有 8 种数据类型,其中有 7 种基本数据类型:Undefined、Null、Boolean、Number、String、Symbol(es6 新增,表示独一无二的值)和 BigInt(es10 新增);
1526 |
1527 | 1 种引用数据类型——Object(Object 本质上是由一组无序的名值对组成的)。里面包含 function、Array、Date 等。JavaScript 不支持任何创建自定义类型的机制,而所有值最终都将是上述 8 种数据类型之一。
1528 |
1529 | 原始数据类型:直接存储在**栈**(stack)中,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。
1530 |
1531 | 引用数据类型:同时存储在**栈**(stack)和**堆**(heap)中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
1532 | ```
1533 |
1534 | ### 24. JS 中Object.prototype.toString.call()判断数据类型
1535 |
1536 | ```
1537 | var a = Object.prototype.toString;
1538 | console.log(a.call(2));
1539 | console.log(a.call(true));
1540 | console.log(a.call('str'));
1541 | console.log(a.call([]));
1542 | console.log(a.call(function(){}));
1543 | console.log(a.call({}));
1544 | console.log(a.call(undefined));
1545 | console.log(a.call(null));https://link.juejin.cn?target=https%3A%2F%2Fsegmentfault.com%2Fa%2F1190000011467723%23articleHeader24 "https://segmentfault.com/a/1190000011467723#articleHeader24")
1546 | ```
1547 |
1548 | ### 25. null 和 undefined 的区别?
1549 |
1550 | ```
1551 | 首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
1552 |
1553 | undefined 代表的含义是未定义, null 代表的含义是空对象(其实不是真的对象,请看下面的**注意**!)。一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。
1554 |
1555 | 其实 null 不是对象,虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
1556 |
1557 | undefined 在 js 中不是一个保留字,这意味着我们可以使用 undefined 来作为一个变量名,这样的做法是非常危险的,它 会影响我们对 undefined 值的判断。但是我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
1558 |
1559 | 当我们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。当我们使用双等 号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
1560 | ```
1561 |
1562 | ### 26. {}和 [] 的 valueOf 和 toString 的结果是什么?
1563 |
1564 | ```
1565 | {} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"
1566 |
1567 | [] 的 valueOf 结果为 [] ,toString 的结果为 ""
1568 | ```
1569 |
1570 | ### 27. Javascript 的作用域和作用域链
1571 |
1572 | ```
1573 | **作用域:** 作用域是定义变量的区域,它有一套访问变量的规则,这套规则来管理浏览器引擎如何在当前作用域以及嵌套的作用域中根据变量(标识符)进行变量查找。
1574 |
1575 | **作用域链:** 作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和 函数。
1576 |
1577 | 作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前 端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
1578 |
1579 | 当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找。
1580 |
1581 | 作用域链的创建过程跟执行上下文的建立有关
1582 | ```
1583 |
1584 | ### 28. 谈谈你对 this、call、apply 和 bind 的理解
1585 |
1586 | ```
1587 | 1. 在浏览器里,在全局范围内 this 指向 window 对象;
1588 | 2. 在函数中,this 永远指向最后调用他的那个对象;
1589 | 3. 构造函数中,this 指向 new 出来的那个新的对象;
1590 | 4. call、apply、bind 中的 this 被强绑定在指定的那个对象上;
1591 | 5. 箭头函数中 this 比较特殊, 箭头函数 this 为父作用域的 this,不是调用时的 this. 要知道前四种方式, 都是调用时确定, 也就是动态的, 而箭头函数的 this 指向是静态的, 声明的时候就确定了下来;
1592 | 6. apply、call、bind 都是 js 给函数内置的一些 API,调用他们可以为函数指定 this 的执行, 同时也可以传参。
1593 | ```
1594 |
1595 | 
1596 |
1597 | ### 29. JavaScript 原型,原型链? 有什么特点?
1598 |
1599 | ```
1600 | 在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对 象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当我们使用构造函数新建一个对象后,在这个对象的内部 将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说我们 是不应该能够获取到这个值的,但是现在浏览器中都实现了 **proto** 属性来让我们访问这个属性,但是我们最好不要使用这 个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,我们可以通过这个方法来获取对 象的原型。
1601 |
1602 | 当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又 会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就 是我们新建的对象为什么能够使用 toString() 等方法的原因。
1603 |
1604 | 特点:
1605 |
1606 | JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与 之相关的对象也会继承这一改变。
1607 | ```
1608 |
1609 | 参考文章:
1610 | [《JavaScript 深入理解之原型与原型链》](http://cavszhouyou.top/JavaScript%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%E4%B9%8B%E5%8E%9F%E5%9E%8B%E4%B8%8E%E5%8E%9F%E5%9E%8B%E9%93%BE.html)
1611 |
1612 | ### 30. 什么是闭包,为什么要用它?
1613 |
1614 | ```
1615 | - 能够访问其它函数内部变量的函数,称为闭包
1616 | - 能够访问自由变量的函数,称为闭包
1617 |
1618 | 场景
1619 | 至于闭包的使用场景,其实在日常开发中使用到是非常频繁的
1620 |
1621 | - 防抖节流函数
1622 | - 定时器回调
1623 | - 等就不一一列举了
1624 |
1625 | 优点
1626 | 闭包帮我们解决了什么问题呢
1627 | **内部变量是私有的,可以做到隔离作用域,保持数据的不被污染性**
1628 |
1629 | 缺点
1630 | 同时闭包也带来了不小的坏处
1631 | **说到了它的优点`内部变量是私有的,可以做到隔离作用域`,那也就是说垃圾回收机制是无法清理闭包中内部变量的,那最后结果就是内存泄漏**
1632 | ```
1633 |
1634 | ### 31. 三种事件模型是什么?
1635 |
1636 | ```
1637 | **事件** 是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型。
1638 |
1639 | 1. **DOM0 级模型:** ,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过 js 属性来指定监听函数。这种方式是所有浏览器都兼容的。
1640 | 2. **IE 事件模型:** 在该事件模型中,一次事件共有两个过程,事件处理阶段,和事件冒泡阶段。事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。
1641 | 3. **DOM2 级事件模型:** 在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。
1642 | ```
1643 |
1644 | ### 32. js 数组和字符串有哪些原生方法, 列举一下
1645 |
1646 | 
1647 |
1648 | 
1649 |
1650 | ### 33. js 延迟加载的方式有哪些
1651 |
1652 | ```
1653 | js 的加载、解析和执行会阻塞页面的渲染过程,因此我们希望 js 脚本能够尽可能的延迟加载,提高页面的渲染速度。
1654 |
1655 | 1. 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。
1656 | 2. 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
1657 | 3. 给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
1658 | 4. 动态创建 DOM 标签的方式,我们可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
1659 | ```
1660 |
1661 | ### 34. js 的几种模块规范?
1662 |
1663 | ```
1664 | js 中现在比较成熟的有四种模块加载方案:
1665 |
1666 | * 第一种是 CommonJS 方案,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
1667 | * 第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
1668 | * 第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和 require.js 的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
1669 | * 第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。
1670 | ```
1671 |
1672 | ### 35. AMD 和 CMD 规范的区别?
1673 |
1674 | ```
1675 | 它们之间的主要区别有两个方面。
1676 |
1677 | 1. 第一个方面是在模块定义时对依赖的处理不同。AMD 推崇依赖前置,在定义模块的时候就要声明其依赖的模块。而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require。
1678 | 2. 第二个方面是对依赖模块的执行时机处理不同。首先 AMD 和 CMD 对于模块的加载方式都是异步加载,不过它们的区别在于 模块的执行时机,AMD 在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致。而 CMD 在依赖模块加载完成后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句 的时候才执行对应的模块,这样模块的执行顺序就和我们书写的顺序保持一致了。
1679 |
1680 | // CMD
1681 | define(function(require, exports, module) {
1682 | var a = require("./a");
1683 | a.doSomething();
1684 | // 此处略去 100 行
1685 | var b = require("./b"); // 依赖可以就近书写
1686 | b.doSomething();
1687 | // ...
1688 | });
1689 |
1690 | // AMD 默认推荐
1691 | define(["./a", "./b"], function(a, b) {
1692 | // 依赖必须一开始就写好
1693 | a.doSomething();
1694 | // 此处略去 100 行
1695 | b.doSomething();
1696 | // ...
1697 | });
1698 | ```
1699 |
1700 | ### 36. ES6 模块与 CommonJS 模块、AMD、CMD 的差异。
1701 |
1702 | ```
1703 | 1、语法上
1704 | CommonJS 使用的是 module.exports = {} 导出一个模块对象,require(‘file_path’) 引入模块对象;
1705 | ES6使用的是 export 导出指定数据, import 引入具体数据。
1706 |
1707 | 2、CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
1708 |
1709 | CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
1710 |
1711 | ES6 Modules 的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6的import 有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
1712 |
1713 | 3、CommonJS 模块是运行时加载,ES6 模块是编译时加载
1714 |
1715 | 运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
1716 |
1717 | 编译时加载: ES6 模块不是对象,而是通过 export 命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”
1718 |
1719 | PS:CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成
1720 | ```
1721 |
1722 | ### 37. JS 的运行机制
1723 |
1724 | ```
1725 | 异步任务分类:宏任务,微任务
1726 | 同步任务和异步任务分别进入不同的执行"场所"
1727 | 先执行主线程执行栈中的宏任务
1728 | 执行过程中如果遇到微任务,进入Event Table并注册函数,完成后移入到微任务的任务队列中
1729 | 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
1730 | 主线程会不断获取任务队列中的任务、执行任务、再获取、再执行任务也就是常说的Event Loop(事件循环)。
1731 | ```
1732 |
1733 | ### 38. 简单介绍一下 V8 引擎的垃圾回收机制
1734 |
1735 | ```
1736 | v8 的垃圾回收机制基于分代回收机制,这个机制又基于世代假说,这个假说有两个特点,一是新生的对象容易早死,另一个是不死的对象会活得更久。基于这个假说,v8 引擎将内存分为了新生代和老生代。
1737 |
1738 | 新创建的对象或者只经历过一次的垃圾回收的对象被称为新生代。经历过多次垃圾回收的对象被称为老生代。
1739 |
1740 | 新生代被分为 From 和 To 两个空间,To 一般是闲置的。当 From 空间满了的时候会执行 Scavenge 算法进行垃圾回收。当我们执行垃圾回收算法的时候应用逻辑将会停止,等垃圾回收结束后再继续执行。这个算法分为三步:
1741 |
1742 | (1)首先检查 From 空间的存活对象,如果对象存活则判断对象是否满足晋升到老生代的条件,如果满足条件则晋升到老生代。如果不满足条件则移动 To 空间。
1743 |
1744 | (2)如果对象不存活,则释放对象的空间。
1745 |
1746 | (3)最后将 From 空间和 To 空间角色进行交换。
1747 |
1748 | 新生代对象晋升到老生代有两个条件:
1749 |
1750 | (1)第一个是判断是对象否已经经过一次 Scavenge 回收。若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。
1751 |
1752 | (2)第二个是 To 空间的内存使用占比是否超过限制。当对象从 From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。设置 25% 的原因主要是因为算法结束后,两个空间结束后会交换位置,如果 To 空间的内存太小,会影响后续的内存分配。
1753 |
1754 | 老生代采用了标记清除法和标记压缩法。标记清除法首先会对内存中存活的对象进行标记,标记结束后清除掉那些没有标记的对象。由于标记清除后会造成很多的内存碎片,不便于后面的内存分配。所以了解决内存碎片的问题引入了标记压缩法。
1755 |
1756 | 由于在进行垃圾回收的时候会暂停应用的逻辑,对于新生代方法由于内存小,每次停顿的时间不会太长,但对于老生代来说每次垃圾回收的时间长,停顿会造成很大的影响。 为了解决这个问题 V8 引入了增量标记的方法,将一次停顿进行的过程分为了多步,每次执行完一小步就让运行逻辑执行一会,就这样交替运行。
1757 | ```
1758 |
1759 | 相关资料:
1760 |
1761 | [《深入理解 V8 的垃圾回收原理》](https://link.juejin.cn?target=https%3A%2F%2Fwww.jianshu.com%2Fp%2Fb8ed21e8a4fb "https://www.jianshu.com/p/b8ed21e8a4fb")
1762 |
1763 | [《JavaScript 中的垃圾回收》](https://link.juejin.cn?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F23992332 "https://zhuanlan.zhihu.com/p/23992332")
1764 |
1765 | ### 39. 哪些操作会造成内存泄漏?
1766 |
1767 | ```
1768 | * 1. 意外的全局变量
1769 | * 2. 被遗忘的计时器或回调函数
1770 | * 3. 脱离 DOM 的引用
1771 | * 4. 闭包
1772 | ```
1773 |
1774 | ### 40.ES6有哪些新特性?
1775 |
1776 | ```
1777 | * 块作用域
1778 | * 类
1779 | * 箭头函数
1780 | * 模板字符串
1781 | * 加强的对象字面
1782 | * 对象解构
1783 | * Promise
1784 | * 模块
1785 | * Symbol
1786 | * 代理(proxy)Set
1787 | * 函数默认参数
1788 | * 展开
1789 | ```
1790 |
1791 | ### 41. 什么是箭头函数?
1792 |
1793 | ```
1794 | //ES5 Version
1795 | var getCurrentDate = function (){
1796 | return new Date();
1797 | }
1798 |
1799 | //ES6 Version
1800 | const getCurrentDate = () => new Date();
1801 |
1802 | 箭头函数表达式的语法比函数表达式更简洁,并且没有自己的`this,arguments,super或new.target`。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
1803 |
1804 | 箭头函数没有自己的 this 值。它捕获词法作用域函数的 this 值,如果我们在全局作用域声明箭头函数,则 this 值为 window 对象。
1805 | ```
1806 |
1807 | ### 42. 什么是高阶函数?
1808 |
1809 | ```
1810 | 高阶函数只是将函数作为参数或返回值的函数。
1811 |
1812 | function higherOrderFunction(param,callback){
1813 | return callback(param);
1814 | }
1815 | ```
1816 |
1817 | ### 43. 手写 call、apply 及 bind 函数
1818 |
1819 | #### 1.实现call函数
1820 |
1821 | **实现步骤:**
1822 |
1823 | - 处理边界:
1824 |
1825 | - 对象不存在,this指向window;
1826 |
1827 | - 将「调用函数」*挂载到*「this指向的对象」的fn属性上。
1828 |
1829 | - 执行「this指向的对象」上的fn函数,并传入参数,返回结果。
1830 |
1831 | ```
1832 | Function.prototype.mu_call = function (context, ...args) {
1833 | //obj不存在指向window
1834 | if (!context || context === null) {
1835 | context = window;
1836 | }
1837 | // 创造唯一的key值 作为我们构造的context内部方法名
1838 | let fn = Symbol();
1839 |
1840 | //this指向调用call的函数
1841 | context[fn] = this;
1842 |
1843 | // 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了
1844 | return context[fn](...args);
1845 | };
1846 |
1847 | // 测试
1848 | var value = 2;
1849 | var obj1 = {
1850 | value: 1,
1851 | };
1852 | function bar(name, age) {
1853 | var myObj = {
1854 | name: name,
1855 | age: age,
1856 | value: this.value,
1857 | };
1858 | console.log(this.value, myObj);
1859 | }
1860 | bar.mu_call(null); //打印 2 {name: undefined, age: undefined, value: 2}
1861 | bar.mu_call(obj1, 'tom', '110'); // 打印 1 {name: "tom", age: "110", value: 1}
1862 | ```
1863 |
1864 | #### 2.实现apply函数
1865 |
1866 | **实现步骤:**
1867 |
1868 | - 与call一致
1869 | - 区别于参数的形式
1870 |
1871 | ```
1872 | Function.prototype.mu_apply = function (context, args) {
1873 | //obj不存在指向window
1874 | if (!context || context === null) {
1875 | context = Window;
1876 | }
1877 | // 创造唯一的key值 作为我们构造的context内部方法名
1878 | let fn = Symbol();
1879 |
1880 | //this指向调用call的函数
1881 | context[fn] = this;
1882 |
1883 | // 执行函数并返回结果 相当于把自身作为传入的context的方法进行调用了
1884 | return context[fn](...args);
1885 | };
1886 |
1887 | // 测试
1888 | var value = 2;
1889 | var obj1 = {
1890 | value: 1,
1891 | };
1892 | function bar(name, age) {
1893 | var myObj = {
1894 | name: name,
1895 | age: age,
1896 | value: this.value,
1897 | };
1898 | console.log(this.value, myObj);
1899 | }
1900 | bar.mu_apply(obj1, ["tom", "110"]); // 打印 1 {name: "tom", age: "110", value: 1}
1901 | ```
1902 |
1903 | #### 3.实现bind函数
1904 |
1905 | ```
1906 | Function.prototype.mu_bind = function (context, ...args) {
1907 | if (!context || context === null) {
1908 | context = window;
1909 | }
1910 | // 创造唯一的key值 作为我们构造的context内部方法名
1911 | let fn = Symbol();
1912 | context[fn] = this;
1913 | let _this = this;
1914 | // bind情况要复杂一点
1915 | const result = function (...innerArgs) {
1916 | // 第一种情况 :若是将 bind 绑定之后的函数当作构造函数,通过 new 操作符使用,则不绑定传入的 this,而是将 this 指向实例化出来的对象
1917 | // 此时由于new操作符作用 this指向result实例对象 而result又继承自传入的_this 根据原型链知识可得出以下结论
1918 | // this.__proto__ === result.prototype //this instanceof result =>true
1919 | // this.__proto__.__proto__ === result.prototype.__proto__ === _this.prototype; //this instanceof _this =>true
1920 | if (this instanceof _this === true) {
1921 | // 此时this指向指向result的实例 这时候不需要改变this指向
1922 | this[fn] = _this;
1923 | this[fn](...[...args, ...innerArgs]); //这里使用es6的方法让bind支持参数合并
1924 | delete this[fn];
1925 | } else {
1926 | // 如果只是作为普通函数调用 那就很简单了 直接改变this指向为传入的context
1927 | context[fn](...[...args, ...innerArgs]);
1928 | delete context[fn];
1929 | }
1930 | };
1931 | // 如果绑定的是构造函数 那么需要继承构造函数原型属性和方法
1932 | // 实现继承的方式: 使用Object.create
1933 | result.prototype = Object.create(this.prototype);
1934 | return result;
1935 | };
1936 | function Person(name, age) {
1937 | console.log(name); //'我是参数传进来的name'
1938 | console.log(age); //'我是参数传进来的age'
1939 | console.log(this); //构造函数this指向实例对象
1940 | }
1941 | // 构造函数原型的方法
1942 | Person.prototype.say = function () {
1943 | console.log(123);
1944 | };
1945 |
1946 | // 普通函数
1947 | function normalFun(name, age) {
1948 | console.log(name); //'我是参数传进来的name'
1949 | console.log(age); //'我是参数传进来的age'
1950 | console.log(this); //普通函数this指向绑定bind的第一个参数 也就是例子中的obj
1951 | console.log(this.objName); //'我是obj传进来的name'
1952 | console.log(this.objAge); //'我是obj传进来的age'
1953 | }
1954 |
1955 | let obj = {
1956 | objName: '我是obj传进来的name',
1957 | objAge: '我是obj传进来的age',
1958 | };
1959 |
1960 | // 先测试作为构造函数调用
1961 | // let bindFun = Person.mu_bind(obj, '我是参数传进来的name');
1962 | // let a = new bindFun('我是参数传进来的age');
1963 | // a.say(); //123
1964 |
1965 | // 再测试作为普通函数调用a;
1966 | let bindFun = normalFun.mu_bind(obj, '我是参数传进来的name');
1967 | bindFun('我是参数传进来的age');
1968 | ```
1969 |
1970 | 参考文章: [高频JavaScript手写面试题,你“行”吗](https://juejin.cn/post/7186557308883714085)
1971 |
1972 | ### 44. 函数柯里化的实现
1973 |
1974 | ```
1975 | // 函数柯里化指的是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。
1976 |
1977 | function curry(fn, args) {
1978 | // 获取函数需要的参数长度
1979 | let length = fn.length;
1980 |
1981 | args = args || [];
1982 |
1983 | return function() {
1984 | let subArgs = args.slice(0);
1985 |
1986 | // 拼接得到现有的所有参数
1987 | for (let i = 0; i < arguments.length; i++) {
1988 | subArgs.push(arguments[i]);
1989 | }
1990 |
1991 | // 判断参数的长度是否已经满足函数所需参数的长度
1992 | if (subArgs.length >= length) {
1993 | // 如果满足,执行函数
1994 | return fn.apply(this, subArgs);
1995 | } else {
1996 | // 如果不满足,递归返回科里化的函数,等待参数的传入
1997 | return curry.call(this, fn, subArgs);
1998 | }
1999 | };
2000 | }
2001 |
2002 | // es6 实现
2003 | function curry(fn, ...args) {
2004 | return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
2005 | }
2006 | ```
2007 |
2008 | 参考文章: [《JavaScript 专题之函数柯里化》](https://github.com/mqyqingfeng/Blog/issues/42)
2009 |
2010 | ### 45. 实现一个 new 操作符
2011 |
2012 | **首先需要了解new做了什么事情:**
2013 |
2014 | - 首先创建了一个空对象。
2015 | - 将空对象`proto`指向构造函数的原型`prototype`。
2016 | - 使`this`指向新创建的对象,并执行构造函数。
2017 | - 执行结果有返回值并且是一个对象, 返回执行的结果, 否则返回新创建的对象。
2018 |
2019 | ```
2020 | // 代码实现
2021 | function mu_new(fn,...arg){
2022 | // 首先创建空对象
2023 | const obj = {};
2024 | // 将空对象的原型proto指向构造函数的原型prototype
2025 | Object.setPrototypeOf(obj, fn.prototype)
2026 | // 将this指向新创建的对象,并且执行构造函数
2027 | const result = fn.apply(obj,arg);
2028 | // 执行结果有返回值并且是一个对象,返回执行的结果,否侧返回新创建的对象
2029 | return result instanceof Object ? result : obj;
2030 | }
2031 |
2032 | // 验证mu_new函数
2033 | function Dog(name){
2034 | this.name = name;
2035 | this.say = function(){
2036 | console.log('my name is' + this.name);
2037 | }
2038 | }
2039 |
2040 | const dog = mu_new(Dog, "傻🐶");
2041 | dog.say() //my name is傻🐶
2042 | ```
2043 |
2044 | ### 46. 可以讲讲Promise吗,可以手写实现一下吗?
2045 |
2046 | ```
2047 | Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了`Promise`对象。
2048 |
2049 | 所谓`Promise`,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
2050 |
2051 | **那我们来看看我们所熟知的`Promise`的基本原理**
2052 |
2053 | + 首先我们在调用Promise时,会返回一个Promise对象。
2054 | + 构建Promise对象时,需要传入一个executor函数,Promise的主要业务流程都在executor函数中执行。
2055 | + 如果运行在excutor函数中的业务执行成功了,会调用resolve函数;如果执行失败了,则调用reject函数。
2056 | + Promise的状态不可逆,同时调用resolve函数和reject函数,默认会采取第一次调用的结果。
2057 |
2058 | **结合Promise/A+规范,我们还可以分析出哪些基本特征**
2059 |
2060 | Promise/A+的规范比较多,在这列出一下核心的规范。[Promise/A+规范](https://link.juejin.cn/?target=https%3A%2F%2Fpromisesaplus.com%2F)
2061 |
2062 | + promise有三个状态:pending,fulfilled,rejected,默认状态是pending。
2063 | + promise有一个value保存成功状态的值,有一个reason保存失败状态的值,可以是undefined/thenable/promise。
2064 | + promise只能从pending到rejected, 或者从pending到fulfilled,状态一旦确认,就不会再改变。
2065 | + promise 必须有一个then方法,then接收两个参数,分别是promise成功的回调onFulfilled, 和promise失败的回调onRejected。
2066 | + 如果then中抛出了异常,那么就会把这个异常作为参数,传递给下一个then的失败的回调onRejected。
2067 |
2068 | 那`CustomPromise`,还实现不了基本原理的3,4两条,那我们来根据基本原理与Promise/A+分析下,还缺少什么
2069 |
2070 | - promise有三个状态:pending,fulfilled,rejected。
2071 | - executor执行器调用reject与resolve两个方法
2072 | - 还需要有保存成功或失败两个值的变量
2073 | - then接收两个参数,分别是成功的回调onFulfilled,失败的回调onRejected
2074 | ```
2075 |
2076 | [手写实现promise](https://juejin.cn/post/7194257890893365308)
2077 |
2078 | ### 47. 什么是 `async/await` 及其如何工作, 可以手写async吗
2079 |
2080 | ```
2081 | 1、Async—声明一个异步函数
2082 | - 自动将常规函数转换成Promise,返回值也是一个Promise对象
2083 | - 只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数
2084 | - 异步函数内部可以使用await
2085 | 2、Await—暂停异步的功能执行(var result = await someAsyncCall();)
2086 | - 放置在Promise调用之前,await强制其他代码等待,直到Promise完成并返回结果
2087 | - 只能与Promise一起使用,不适用与回调
2088 | - 只能在async函数内部使用
2089 | ```
2090 |
2091 | [手写实现async](https://juejin.cn/post/7195358576364224568)
2092 |
2093 | ### 48. instanceof 的优缺点是什么,如何实现
2094 |
2095 | **优缺点:**
2096 |
2097 | - **「优点」**:能够区分Array、Object和Function,适合用于判断自定义的类实例对象
2098 | - **「缺点」**:Number,Boolean,String基本数据类型不能判断
2099 |
2100 | **实现步骤:**
2101 |
2102 | - 传入参数为左侧的实例L,和右侧的构造函数R
2103 | - 处理边界,如果要检测对象为基本类型则返回false
2104 | - 分别取传入参数的原型
2105 | - 判断左侧的原型是否取到了null,如果是null返回false;如果两侧原型相等,返回true,否则继续取左侧原型的原型。
2106 |
2107 | ```
2108 | // 传入参数左侧为实例L, 右侧为构造函数R
2109 | function mu_instanceof(L,R){
2110 | // 处理边界:检测实例类型是否为原始类型
2111 | const baseTypes = ['string','number','boolean','symbol','undefined'];
2112 |
2113 | if(baseTypes.includes(typeof L) || L === null) return false;
2114 |
2115 | // 分别取传入参数的原型
2116 | let Lp = L.__proto__;
2117 | let Rp = R.prototype; // 函数才拥有prototype属性
2118 |
2119 | // 判断原型
2120 | while(true){
2121 | if(Lp === null) return false;
2122 | if(Lp === Rp) return true;
2123 | Lp = Lp.__proto__;
2124 | }
2125 | }
2126 |
2127 | // 验证
2128 | const isArray = mu_instanceof([],Array);
2129 | console.log(isArray); //true
2130 | const isDate = mu_instanceof('2023-01-09',Date);
2131 | console.log(isDate); // false
2132 | ```
2133 |
2134 | ### 49. js 的节流与防抖
2135 |
2136 | #### 1.防抖
2137 |
2138 | *函数防抖是在事件被触发n秒后再执行回调,如果在「n秒内又被触发」,则「重新计时」*
2139 |
2140 | ```
2141 | function debounce(fn, wait) {
2142 | let timer = null;
2143 | return function () {
2144 | if (timer != null) {
2145 | clearTimeout(timer);
2146 | }
2147 | timer = setTimeout(() => {
2148 | fn();
2149 | }, wait);
2150 | };
2151 | }
2152 | // 测试
2153 | function handle() {
2154 | console.log(Math.random());
2155 | }
2156 | // 窗口大小改变,触发防抖,执行handle
2157 | window.addEventListener('resize', debounce(handle, 1000));
2158 | ```
2159 |
2160 | #### 2.节流
2161 |
2162 | *当事件触发时,保证一定时间段内只**调用一次**函数。例如页面滚动的时候,每隔一段时间发一次请求*
2163 |
2164 | **实现步骤:**
2165 |
2166 | - 传入参数为执行函数fn,等待时间wait。
2167 | - 保存初始时间now。
2168 | - 返回一个函数,如果超过等待时间,执行函数,将now更新为当前时间。
2169 |
2170 | ```
2171 | function throttle(fn, wait, ...args) {
2172 | var pre = Date.now();
2173 | return function () {
2174 | // 函数可能会有入参
2175 | var context = this;
2176 | var now = Date.now();
2177 | if (now - pre >= wait) {
2178 | // 将执行函数的this指向当前作用域
2179 | fn.apply(context, args);
2180 | pre = Date.now();
2181 | }
2182 | };
2183 | }
2184 |
2185 | // 测试
2186 | var name = 'mu';
2187 | function handle(val) {
2188 | console.log(val + this.name);
2189 | }
2190 | // 滚动鼠标,触发防抖,执行handle
2191 | window.addEventListener('scroll', throttle(handle, 1000, '木由'));
2192 | ```
2193 |
2194 | ### 50.HTML、XML、XHTML 的区别
2195 |
2196 | ```
2197 | - `HTML`:超文本标记语言,是语法较为松散的、不严格的`Web`语言;
2198 | - `XML`:可扩展的标记语言,主要用于存储数据和结构,可扩展;
2199 | - `XHTML`:可扩展的超文本标记语言,基于`XML`,作用与`HTML`类似,但语法更严格。
2200 | ```
2201 |
2202 | ### 51. HTML、XHTML和HTML5区别以及有什么联系
2203 |
2204 | ```
2205 | XHTML与HTML的区别
2206 |
2207 | - `XHTML`标签名必须小写;
2208 | - `XHTML`元素必须被关闭;
2209 | - `XHTML`元素必须被正确的嵌套;
2210 | - `XHTML`元素必须要有根元素。
2211 |
2212 | XHTML与HTML5的区别
2213 |
2214 | - `HTML5`新增了`canvas`绘画元素;
2215 | - `HTML5`新增了用于绘媒介回放的`video`和`audio`元素;
2216 | - 更具语义化的标签,便于浏览器识别;
2217 | - 对本地离线存储有更好的支持;
2218 | - `MATHML`,`SVG`等,可以更好的`render`;
2219 | - 添加了新的表单控件:`calendar`、`date`、`time`、`email`等。
2220 |
2221 | HTML、XHTML、HTML5之间联系
2222 |
2223 | - `XHTML`是`HTML`规范版本;
2224 | - `HTML5`是`HTML`、`XHTML`以及`HTML DOM`的新标准。
2225 | ```
2226 |
2227 | ### 52.行内元素有哪些?块级元素有哪些? 空(void)元素有那些?
2228 |
2229 | ```
2230 | - 行内元素: `a`, `b`, `span`, `img`, `input`, `select`, `strong`;
2231 | - 块级元素: `div`, `ul`, `li`, `dl`, `dt`, `dd`, `h1-5`, `p`等;
2232 | - 空元素: `
`, `
`, `
`, ``, ``;
2233 | ```
2234 |
2235 | ### 53. 页面导入样式时,使用link和@import有什么区别
2236 |
2237 | ```
2238 | - `link`属于`HTML`标签,而`@import`是`css`提供的;
2239 | - 页面被加载时,`link`会同时被加载,而`@import`引用的css会等到页面被加载完再加载;
2240 | - `@import`只在`IE5`以上才能识别,而`link`是`XHTML`标签,无兼容问题;
2241 | - `link`方式的样式的权重高于`@import`的权重。
2242 | ```
2243 |
2244 | ### 54. 如何理解语义化标签
2245 |
2246 | ```
2247 | 概念
2248 |
2249 | 语义化是指根据内容的结构化(内容语义化),选择合适的标签(代码语义化),便于开发者阅读和写出更优雅的代码的同时,让浏览器的爬虫和机器很好的解析。
2250 |
2251 | 语义化的好处
2252 |
2253 | - 用正确的标签做正确的事情;
2254 | - 去掉或者丢失样式的时候能够让页面呈现出清晰的结构;
2255 | - 方便其他设备解析(如屏幕阅读器、盲人阅读器、移动设备)以意义的方式来渲染网页;
2256 | - 有利于`SEO`:和搜索引擎建立良好沟通,有助于爬虫抓取更多的有效信息:爬虫依赖于标签来确定上下文和各个关键字的权重;
2257 | - 便于团队开发和维护,语义化更具可读性,遵循W3C标准的团队都遵循这个标准,可以减少差异化。
2258 | ```
2259 |
2260 | ### 55. property和attribute的区别是什么
2261 |
2262 | ```
2263 | - `property`是`DOM`中的属性,是`JavaScript`里的对象;
2264 | - `attribute`是`HTML`标签上的特性,它的值只能够是字符串;
2265 |
2266 | 简单的理解就是:`Attribute`就是`DOM`节点自带的属性,例如`html`中常用的`id`、`class`、`title`、`align`等;而`Property`是这个`DOM`元素作为对象,其附加的内容,例如`childNodes`、`firstChild`等。
2267 | ```
2268 |
2269 | ### 56. html5有哪些新特性、移除了那些元素
2270 |
2271 | ```
2272 | 新特性
2273 |
2274 | **HTML5 现在已经不是 SGML 的子集,主要是关于图像,位置,存储,多任务等功能的增加。**
2275 |
2276 | - 拖拽释放`(Drag and drop)` `API`;
2277 | - 语义化更好的内容标签(`header`, `nav`, `footer`, `aside`, `article`, `section`);
2278 | - 音频、视频API(`audio`, `video`);
2279 | - 画布`(Canvas)` `API`;
2280 | - 地理`(Geolocation)` `API`;
2281 | - 本地离线存储 `localStorage` 长期存储数据,浏览器关闭后数据不丢失;
2282 | - `sessionStorage` 的数据在浏览器关闭后自动删除;
2283 | - 表单控件:`calendar`、`date`、`time`、`email`、`url`、`search` ;
2284 | - 新的技术`webworker`, `websocket`, `Geolocation`等;
2285 |
2286 | 移除元素
2287 |
2288 | **纯表现元素**:
2289 |
2290 | - `` 默认字体,不设置字体,以此渲染;
2291 | - `` 字体标签;
2292 | - `` 水平居中;
2293 | - `` 下划线;
2294 | - ``字体;
2295 | - ``中横字;
2296 | - ``文本等宽;
2297 |
2298 | **对可用性产生负面影响的元素**:
2299 |
2300 | `