IndexedDB浏览器数据库 IndexedDB 入门教程 - 阮一峰的网络日志 (ruanyifeng.com)概述随着浏览器的功能不断增强，越来越多的网站开始考虑，将大量数据储存在客户端，这样可以减少从服务器获取数据，直接从本地获取数据。现有的浏览器数据储存方案，都不适合储存大量数据：Cookie 的大小不超过4KB，且每次请求都会发送回服务器；LocalStorage 在 2.5MB 到 10MB 之间（各家浏览器不同），而且不提供搜索功能，不能建立自定义的索引。所以，需要一种新的解决方案，这就是IndexedDB诞生的背景。通俗的说，IndexedDB就是浏览器提供的本地数据库，它可以被网页脚本创建和操作。IndexedDB允许存储大量数据，提供查找接口，还能建立索引。这些都是LocalStorage所不具备的。就数据库类型而言，IndexedDB不属于关系型数据库（不支持SQL查询），更接近NoSQL数据库。IndexedDB具有以下特点。键值对存储：IndexedDB内部采用对象仓库（object store）存放数据。所有类型的数据可以直接存入，包括javascript对象。对象仓库中，数据以键值对的形式保存，每一个数据记录都有一个对应的主键，主键是独一无二的，不能有重复，负责会抛出一个错误。异步。IndexedDB操作不会锁死浏览器，用户依然可以进行其他操作，这与LocalStorage形成对比，后者的操作是同步的。异步是为了防止大量数据的读写拖慢网页的表现。同源支持：IndexedDB受到同源限制，每一个数据库对应创建它的域名，网页只能访问自身名下的数据库，而不能访问跨域的数据库。存储空间大：IndexedDB的存储空间比LocalStorage大得多，一般来说不少于250MB，甚至没有上限。支持二进制存储：IndexedDB不仅可以存储字符串，还可以存储二进制数据（ArrayBuffer对象和Blob对象）基本概念IndexedDB是一个比较复杂的API，涉及不少概念。它把不同实体，抽象成一个个对象接口。学习这个API，就是学习它的各种对象接口。数据库：IDBDatabase 对象对象仓库：IDBObjectStore 对象索引： IDBIndex 对象事务： IDBTransaction 对象操作请求：IDBRequest 对象指针： IDBCursor 对象主键集合：IDBKeyRange 对象下面是一些主要的概念。数据库数据库是一系列相关数据的容器。每个域名（严格的说，是协议+域名+端口）都可以新建任意多个数据库。IndexedDB数据库有版本的概念。同一时刻，只能有一个版本的数据库存在。如果要修改数据库结构（新增或删除表、索引或者主键），只能通过升级数据库版本完成。对象仓库每个数据库包含若干个对象仓库（object store)。它类似于关系型数据库的表格。数据记录对象仓库保存的是数据记录。每条记录类似于关系型数据库里的行，但只有主键和数据体俩部分。主键用来建立默认的索引，必须是不同的，否则会报错。主键可以是数据记录里面的一个属性，也可以指定为一个递增的整数编号{ id: 1, text: 'foo' }上述对象中，id属性可以当做主键。数据体可以是任意数据类型，不限于对象。索引为了加速数据的检索，可以再对象仓库里面，为不同的属性建立索引。事务数据记录的读写和删改都要通过事务完成。事务对象提供error、abort和complete三个事件，用来监听结果操作流程IndexedDB 数据库的各种操作，一般是按照下面的流程进行的。这个部分只给出简单的代码示例，用于快速上手，详细的各个对象的 API 请看这里。var request = window.indexedDB.open(databaseName, version);这个方法接受俩个这个方法接受两个参数，第一个参数是字符串，表示数据库的名字。如果指定的数据库不存在，就会新建数据库。第二个参数是整数，表示数据库的版本。如果省略，打开已有数据库时，默认为当前版本；新建数据库时，默认为1。indexedDB.open()方法返回一个 IDBRequest 对象。这个对象通过三种事件error、success、upgradeneeded，处理打开数据库的操作结果。（1）error 事件error事件表示打开数据库失败。request.onerror = function (event) { console.log('数据库打开报错'); };（2）success 事件success事件表示成功打开数据库。var db; request.onsuccess = function (event) { db = request.result; console.log('数据库打开成功'); };这时，通过request对象的result属性拿到数据库对象。（3）upgradeneeded 事件如果指定的版本号，大于数据库的实际版本号，就会发生数据库升级事件upgradeneeded。var db; request.onupgradeneeded = function (event) { db = event.target.result; }这时通过事件对象的target.result属性，3.2 新建数据库新建数据库与打开数据库是同一个操作。如果指定的数据库不存在，就会新建。不同之处在于，后续的操作主要在upgradeneeded事件的监听函数里面完成，因为这时版本从无到有，所以会触发这个事件。通常，新建数据库以后，第一件事是新建对象仓库（即新建表）。request.onupgradeneeded = function(event) { db = event.target.result; var objectStore = db.createObjectStore('person', { keyPath: 'id' }); }上面代码中，数据库新建成功以后，新增一张叫做person的表格，主键是id。更好的写法是先判断一下，这张表格是否存在，如果不存在再新建。request.onupgradeneeded = function (event) { db = event.target.result; var objectStore; if (!db.objectStoreNames.contains('person')) { objectStore = db.createObjectStore('person', { keyPath: 'id' }); } }主键（key）是默认建立索引的属性。比如，数据记录是{ id: 1, name: '张三' }，那么id属性可以作为主键。主键也可以指定为下一层对象的属性，比如{ foo: { bar: 'baz' } }的foo.bar也可以指定为主键。如果数据记录里面没有合适作为主键的属性，那么可以让 IndexedDB 自动生成主键。var objectStore = db.createObjectStore( 'person', { autoIncrement: true } );上面代码中，指定主键为一个递增的整数。新建对象仓库以后，下一步可以新建索引。request.onupgradeneeded = function(event) { db = event.target.result; var objectStore = db.createObjectStore('person', { keyPath: 'id' }); objectStore.createIndex('name', 'name', { unique: false }); objectStore.createIndex('email', 'email', { unique: true }); }上面代码中，IDBObject.createIndex()的三个参数分别为索引名称、索引所在的属性、配置对象（说明该属性是否包含重复的值）。3.3 新增数据新增数据指的是向对象仓库写入数据记录。这需要通过事务完成。function add() { var request = db.transaction(['person'], 'readwrite') .objectStore('person') .add({ id: 1, name: '张三', age: 24, email: 'zhangsan@example.com' }); request.onsuccess = function (event) { console.log('数据写入成功'); }; request.onerror = function (event) { console.log('数据写入失败'); } } add();上面代码中，写入数据需要新建一个事务。新建时必须指定表格名称和操作模式（"只读"或"读写"）。新建事务以后，通过IDBTransaction.objectStore(name)方法，拿到 IDBObjectStore 对象，再通过表格对象的add()方法，向表格写入一条记录。写入操作是一个异步操作，通过监听连接对象的success事件和error事件，了解是否写入成功。3.4 读取数据读取数据也是通过事务完成。function read() { var transaction = db.transaction(['person']); var objectStore = transaction.objectStore('person'); var request = objectStore.get(1); request.onerror = function(event) { console.log('事务失败'); }; request.onsuccess = function( event) { if (request.result) { console.log('Name: ' + request.result.name); console.log('Age: ' + request.result.age); console.log('Email: ' + request.result.email); } else { console.log('未获得数据记录'); } }; } read();上面代码中，objectStore.get()方法用于读取数据，参数是主键的值。3.5 遍历数据遍历数据表格的所有记录，要使用指针对象 IDBCursor。function readAll() { var objectStore = db.transaction('person').objectStore('person'); objectStore.openCursor().onsuccess = function (event) { var cursor = event.target.result; if (cursor) { console.log('Id: ' + cursor.key); console.log('Name: ' + cursor.value.name); console.log('Age: ' + cursor.value.age); console.log('Email: ' + cursor.value.email); cursor.continue(); } else { console.log('没有更多数据了！'); } }; } readAll();上面代码中，新建指针对象的openCursor()方法是一个异步操作，所以要监听success事件。3.6 更新数据更新数据要使用IDBObject.put()方法。function update() { var request = db.transaction(['person'], 'readwrite') .objectStore('person') .put({ id: 1, name: '李四', age: 35, email: 'lisi@example.com' }); request.onsuccess = function (event) { console.log('数据更新成功'); }; request.onerror = function (event) { console.log('数据更新失败'); } } update();上面代码中，put()方法自动更新了主键为1的记录。3.7 删除数据IDBObjectStore.delete()方法用于删除记录。function remove() { var request = db.transaction(['person'], 'readwrite') .objectStore('person') .delete(1); request.onsuccess = function (event) { console.log('数据删除成功'); }; } remove();3.8 使用索引索引的意义在于，可以让你搜索任意字段，也就是说从任意字段拿到数据记录。如果不建立索引，默认只能搜索主键（即从主键取值）。假定新建表格的时候，对name字段建立了索引。objectStore.createIndex('name', 'name', { unique: false });现在，就可以从name找到对应的数据记录了。var transaction = db.transaction(['person'], 'readonly'); var store = transaction.objectStore('person'); var index = store.index('name'); var request = index.get('李四'); request.onsuccess = function (e) { var result = e.target.result; if (result) { // ... } else { // ... } }使用场景所有的场景都基于客户端需要存储大量数据的前提下：数据可视化等界面，大量数据，每次请求会消耗很大性能。即时聊天工具，大量消息需要存在本地。其它存储方式容量不满足时，不得已使用IndexedDB
Web Worker简介Web Worker（工作线程）是HTML5中提出的概念，分为俩汇总类型，专用线程（Dedicated Web Worker）：专用线程仅能被创建它的脚本所使用（一个专用线程独对应一个主线程）。共享线程（Shared Web Worker）：而共享线程能够在不同的脚本下进行使用（一共共享线程对应多个主线程）。专用线路可以看做是默认情况的WebWorker，加上修饰词的目的是为了与主线程进行区分。用途Web Worker 的意义在于可以将一些耗时的数据处理操作从主线程中剥离，使主线程更加专注于页面渲染和交互。懒加载文本分析流媒体数据处理canvas 图形绘制图像处理注意有同源限制无法访问DOM节点运行在一个上下文中，无法使用Window对象WebWorker的运行不会影响主线程，但主线程交互时仍收到主线程单线程的瓶颈制约。换言之，如果Worker线程频繁与主线程进行交互，主线程由于需要处理交互，仍有可能使页面发生阻塞共享线程可以被多个浏览上下文调用（Browsing context）调用，但所有这些浏览上下文必须同源（相同协议，主机和端口号）Worker上下文 Worker执行的上下文，与主页面执行时的上下文并不相同，最顶层的对象并不是window，而是WorkerGlobalScope，所以无法访问window、以及与window相关的DOM API，但是可以与setTimeout、setInterval等协作。WorkerGlobalScope作用域下的常用属性、方法：1、self： WorkerGlobalScope 的 self 属性对对象本身的引用；2、location：location 属性返回当线程被创建出来的时候与之关联的 WorkerLocation对象，它表示用于初始化这个工作线程的脚步资源的绝对 URL，即使页面被多次重定向后，这个 URL 资源位置也不会改变；3、close：关闭当前线程；4、importScripts：通过importScripts()方法通过url在worker中加载库函数；5、XMLHttpRequest：能发出Ajax请求，但是XMLHttpRequest的responseXML和channel属性总会返回null；6、能调用setTimeout/setInterval以及addEventListener/postMessage等；具体分类为为：时间相关clearInterval()clearTimeout()setInterval()setTimeoutWorker 相关importScripts()close()postMessage()存储相关CacheIndexedDB网络相关FetchWebSocketXMLHttpRequest支持根据 CanI Use 网站的统计，目前约有98.01% 的浏览器支持专用线程。而对于共享线程，则仅有大约 41.66% 的浏览器支持。if (window.Worker) { // ... }if (window.SharedWorker) { // ... }专用线程线程创建专用线程由Worker()方法创建，可以接收俩个参数，第一个参数是必填的脚本的URL，第二个参数是可选的配置对象options，可以指定type、credentials、name三个属性URL是一个DOMString表示worker将执行的脚本的URL。它必须遵循同源策略。options包含可以再创建对象实例时设置的选项的属性的对象。可用属性如下type：用以指定 worker 类型的 DOMString 值. 该值可以是 classic 或 module. 如果未指定，将使用默认值 classic.credentials：用以指定 worker 凭证的 DOMString 值.该值可以是 omit, same-origin，或 include.。如果未指定，或者 type 是 classic，将使用默认值 omit (不要求凭证)。name：在 DedicatedWorkerGlobalScope 的情况下，用来表示 worker 的 scope 的一个 DOMString 值，主要用于调试目的。通常形式创建worker线程var worker = new Worker('worker.js') //var worker = new Worker('worker.js', { name: 'workerName'})另外，通过URL.createObjectURL()创建URL对象，可以实现创建内嵌的worker：var myTask = ` //js脚本执行代码 `; //创建Blob对象 var blob = new Blob([myTask]); //创建worker线程 var worker = new Worker(window.URL.createObjectURL(blob));线程数据通讯Worker与其主页面之间通讯是通过onmessage事件和postMessage()方法实现的。注意：在主页面与Woker之间传递的数据是通过复制实现的，而不是在内存中共享完成的：传给Worker的对象需要经过序列化，接下来在另一端还需要反序列化。页面与Woker不会共享一个实例，最终的结果就是在每次通信结束时生成了数据的一个副本。简而言之，Woker与其主页面之间只能单纯的传递数据，不能传递复杂的引用类型：如果通过构造函数创建的对象等。并且，传递的数据也是经过拷贝生成的一个副本，在一段对数据修改不会影响另一端// 主线程 var worker = new Worker('worker.js') //主线程传递数据给worker线程 worker.postMessage("hello worker") //主线程接受worker线程传递过来的数据 worker.onmessage = function(e) { console.log(e.data) }// Worker 线程 //接受主线程传递过来的数据 onmessage = function (e) { if (e.data) { //worker向主线程传递数据 postMessage("hello mainjs") } } 关闭Worker 关闭 Worker的方式有两种：1、在主线程中使用 terminate()方法；2、在 Worker 线程中使用close()方法// 主线程 worker.terminate()// Dedicated Worker 线程中 self.close()错误处理 通过在主线程或Worker 线程中设置 onerror和 onmessageerror的回调函数对错误进行处理:// 主线程 worker.onerror = function () { // 捕捉错误 } // 主线程使用专用线程 worker.onmessageerror = function () { // 捕捉错误 } // worker 线程 onerror = function () { // 捕捉错误 } onmessageerror = function () { // 捕捉错误 } 加载外部脚本importScriptsWeb Worker 提供了 importScripts()方法，能够将外部脚本文件加载到 Worker 中:importScripts('script1.js') importScripts('script2.js') // 以上写法等价于 importScripts('script1.js', 'script2.js')共享线程共享线程Can I Use上浏览器支持率仅为36.12%，所以不推荐大家使用：如果非要使用不可，先判断浏览器是否支持：if (window.SharedWorker) { //操作 } 共享线程的创建和使用方式与专用线程的基本一致，但是要注意以下几点：//主线程 //使用SharedWorker创建共享线程 var sharedWorker = new SharedWorker('shared-worker.js') //使用sharedWorker.port调用postMessage、onmessage等方法 sharedWorker.port.postMessage([10, 24]) sharedWorker.port.onmessage = function (e) { console.log(e.data) } //捕捉错误 sharedWorker.port.onerror = function () { // 捕捉错误 } sharedWorker.port.onmessageerror = function () { // 捕捉错误 } // Worker 线程 onconnect = function (e) { //获取port let port = e.ports[0] //使用port调用postMessage、onmessage等方法 port.onmessage = function (e) { if (e.data.length > 1) { port.postMessage() } } } // Shared Worker 线程中关闭线程 self.port.close() 完整实例由于Web Worker无法加载本地文件，所以要在本地文件创建worker,要通过Blob()方式本地创建： worker 2秒面接受worker线程数据 页面一开始显示2秒面接受worker线程数据，刷新页面，等待两秒钟，显示文本修改为hello mainjs，同时观察到控制台输出信息，表明worker和主线程进行数据接受和传递：
SSR谈谈我对服务端渲染(SSR)的理解 - 掘金 (juejin.cn)什么是SSRCSR是client Side Render简称；页面上的内容是我们JS文件渲染出来的，js文件运行在浏览器上面，服务端只返回一个HTML模板。SSR是Server Side Render简称；页面上的内容是通过服务端渲染生成的，浏览器直接显式服务端返回的HTML就可以了。SSR解决的问题：优点SEO：搜索引擎优先取页面HTML结构，使用SSR时，服务端已经生成了和业务关联的HTML，有利于SEO首屏呈现渲染：用户无需等待页面所有js加载完成就可以看到页面视图（压力来到了服务器，所以需要权衡哪些用服务端渲染，哪些交给客户端）缺点需要特殊库的兼容，提升了整体项目的复杂度由于没有动态更新，（拿vue举例子）所有生命周期钩子函数中，只有beforeCreate和created会在服务端渲染SSR过程中被调用。也就是说任何其他生命周期钩子函数中的代码（beforeMount或mounted，）只会在客户端执行同时渲染对服务端的压力也是相对较大的，现在主流的无论是vue还是react框架都提供了完善的对SSR的支持，但是这里仍然奉劝大家，开始做项目选型的时候就要考虑好前端部分是否需要做SSR，将一个已经开发了很长一段时间的项目改成SSR的模式是非常费时费力的。SSR渲染流程和传统那么我们来首先看看前后端不分离下的请求流程，也就是是传统服务端渲染前后端职责不清前后端代码杂揉在一起项目难以管理和维护 尽管如此，这种渲染方式还是有一些好处：客户端能够快速呈现服务器端渲染好的页面，减少白屏时间，这能够提供很好的用户体验SEO 友好，服务端渲染从服务器发出的html带有页面内容，可提高搜索排名。下面是主流的前后端分离架构客户端渲染指使用 JS 来渲染页面大部分内容。客户端在请求时，服务端不做任何处理，直接将前端资源打包后生成的html返回给客户端，此时的html中无任何网页内容，需要客户端去加载执行js代码才能渲染生成页面内容，同时完成事件绑定，然后客户端再去通过ajax请求后端api获取数据更新视图。这样一来，前后端代码解耦，两端使用 ajax 进行交互实现局部刷新。但这种方式也有一些不足：首屏加载缓慢，因为要等 js 加载完毕后，才能进行页面渲染SEO 不友好，SPA模式的客户端渲染从服务器发出的只是一个没有内容的空壳，大部分页面内容通过js渲染，搜索引擎自然爬不到东西客户端渲染至少发起http请求三次，第一次是请求页面，第二次是请求页面里的JS脚本（渲染dom和进行事件绑定），第三次是动态数据ajax请求。而服务端渲染将传统的3次串行http请求简化成一次http请求，客户端只需要请求页面，解析服务端返回的html即可。需要重复渲染，因为发送ajax请求向后台获取数据，一般在componentDidMounted生命周期，页面已经渲染过一次，等到获取数据后更新视图，页面又需要再渲染一次SSR渲染其实就是将一套JS代码在服务端执行一遍渲染静态页面，又在客户端执行一遍完成事件绑定。SSR的优劣势刚才已经提到过了不再赘述，那么看起来是不是很简单呢？那什么情况下应该使用SSR进行渲染呢？主要取决于首屏渲染速度是否真的重要SEO是否真的需要其核心的变动就是增加了一个Node层中间层作为对一些主要元素进行与渲染，因为Node服务器和后台的API服务器在内网之中，因此它们俩之间交互会非常的快，在这里直接实现API的调用很显然比客户端到服务端的代销来的小，当然也相应增加了服务端的压力。同时增加了代码的复杂性，因为写代码的时候还需要考虑哪些代码是在Node服务器就执行了，哪些会延迟到客户端执行。选型无论如何还是明确一点，SSR大部分情况下都是为了SEO做的，但是做SER不止SSR一种方式也可以用SSG，他也是一种服务端渲染技术。如果你调研 SSR 只是为了优化为数不多的营销页面的 SEO (例如 /、/about 和 /contact 等)，那么你可能需要 SSG 而不是 SSR。SSG 也非常适合构建基于内容的网站，比如文档站点或者博客。静态站点生成 (Static-Site Generation，缩写为 SSG)，也被称为预渲染，是另一种流行的构建快速网站的技术。如果用服务端渲染一个页面所需的数据对每个用户来说都是相同的，那么我们可以只渲染一次，提前在构建过程中完成，而不是每次请求进来都重新渲染页面。预渲染的页面生成后作为静态 HTML 文件被服务器托管。
AST abstract syntax Tree简介抽象语法树是一种用编程语言编写的源代码的抽象语法结构的树表示。树的每个节点表示源代码中出现的一个构造。AST全称为Abstract Syntax Tree，抽象语法树。AST在编译器的应用非常重要，因为抽象语法树是编译器中广泛用于表示程序代码结构的数据结构。AST通常是编译器语法分析阶段的结果。它通常过编译器所需的几个阶段作为程序的中间表示，并且对编译器的最终输出有很大影响。在进一步讨论实现部分之前，让我们先讨论一下AST的使用。AST主要用于编译器一检查代码的准确性。如果生成的语法树有错误，编译器会打印一条错误信息。使用抽象语法树（AST）是因为某些构造无法用上下文无关语法表示，列如隐式类型。它们高度特定于编程语言，但对通用语法树的研究正在进行中。在java中AST通常由一个解析器创建，该解析器读取原地阿妈并创建一个树形结构，代表代码的语法和结构。树中的每个节点都代表一个不同的语法结构，如类、方法或语句。一般来说，AST是编译器和其他需要分析和处理编程语言语法的工具的重要工具。作用AST有什么作用了？用过lombok的人一定能感觉到那种简洁舒服的代码风格，lombok的原理也就是在代码编译的时候根据相应的注解动态的去修改AST这个树，为它增加新的节点（也就是对于的代码比如get、set）。所以如果想动态的修改代码，或者直接生成java代码的话，AST是一个不错的选择。什么是AST抽象语法树我们都知道,在传统的编译语言的流程中,程序的一段源代码在执行之前会经历三个步骤,统称为"编译":分词/词法分析 这个过程会将由字符组成的字符串分解成有意义的代码块,这些代码块统称为词法单元(token).举个例子: let a = 1, 这段程序通常会被分解成为下面这些词法单元: let 、a、=、1、 ；空格是否被当成此法单元，取决于空格在这门语言中的意义。解析/语法分析 这个过程是将词法单元流转换成一个由元素嵌套所组成的代表了程序语法结构的树,这个树被称为"抽象语法树"（abstract syntax code，AST）代码生成 将AST转换成可执行代码的过程被称为代码生成.抽象语法树（abstract syntax code ，AST）是源代码的抽象语法结构的树状表示，树上的每个节点都表示源代码中的一种结构，之所以说是抽象的，抽象表示把js代码进行了结构化的转化，转化为一种数据结构。这种数据结构其实就是一个大的json对象，json我们都熟悉，他就像一颗枝繁叶茂的树。有树根，有树干，有树枝，有树叶，无论多小多大，都是一棵完整的树。简单理解,就是把我们写的代码按照一定的规则转换成一种树形结构。用途AST不仅仅是用来Js引擎的编译上，我们实际开发过程中也是经常使用的，比如我们常用的babel插件自动将ES6的语法转换成ES5、使用 UglifyJS来压缩代码 、css预处理器、开发WebPack插件、Vue-cli前端自动化工具等等，这些底层原理都是基于AST来实现的，AST能力十分强大， 能够帮助开发者理解JavaScript这门语言的精髓。结构我们先来简单看一组AST树状结构const team = '大转转FE'经过转换，输出如下AST树桩结构{ "type": "Program", "start": 0, "end": 18, "body": [ { "type": "VariableDeclaration", "start": 0, "end": 18, "declarations": [ { "type": "VariableDeclarator", "start": 6, "end": 18, "id": { "type": "Identifier", "start": 6, "end": 8, "name": "team" }, "init": { "type": "Literal", "start": 11, "end": 18, "value": "大转转FE", "raw": "'大转转FE'" } } ], "kind": "const" } ], "sourceType": "module" }我们可以看到，一个标准的AST结构可以理解为一个json对象，那我们就可以通过一些方法去解析和操作它Esprima: Parser测试网址AST编译过程词法分析var company = 'zhuanzhuan'假如有以上代码，在词法分析阶段，回显对整个代码进行扫描，生成tokens流，扫描过程如下我们会通过条件判断这个字符是字母， "/" , "数字" , 空格 , "(" , ")" , ";" 等等。如果是字母会继续往下看如果还是字母或者数字，会继续这一过程直到不是为止，这个时候发现找到的这个字符串是一个var，是一个关键字，并且下一个字符是一个空格，就会生成{ "type" : "Keyword" , "value" : "var" }放入数组中它继续向下找发现了一个字母company（因为找到的上一个值是var这个时候如果它发现下一个字符不是字母就可能直接会报错并返回）并且后面是空格生成{ "type" : "Identifier" , "value" : "company" }放到数组中syntax{ "type": "Program", "body": [ { "type": "VariableDeclaration", "declarations": [ { "type": "VariableDeclarator", "id": { "type": "Identifier", "name": "company" }, "init": { "type": "Literal", "value": "zhuanzhuan", "raw": "'zhuanzhuan'" } } ], "kind": "var" } ], "sourceType": "script" }parser生成AST树这里我们使用esprima去生成, 安装相关依赖 npm i esprima --save以如下代码为例:const company = 'zhuanzhuan'要得到其对应的AST,我们对其进行如下操作:const esprima = require('esprima'); let code = 'const company = "zhuanzhuan" '; const ast = esprima.parseScript(code); console.log(ast);运行结果如下:$ node test.js Script { type: 'Program', body: [ VariableDeclaration { type: 'VariableDeclaration', declarations: [Array], kind: 'const' } ], sourceType: 'script' }这样我们就得到了一棵AST树traverse对AST树遍历,进行增删改查这里我们使用estraverse去完成, 安装相关依赖 npm i estraverse --save还是上面的代码, 我们更改为 const team = '大转转FE'const esprima = require('esprima'); const estraverse = require('estraverse'); let code = 'const company = "zhuanzhuan" '; const ast = esprima.parseScript(code); estraverse.traverse(ast, { enter: function (node) { node.name = 'team'; node.value = "大转转FE"; } }); console.log(ast);运行结果如下:$ node test.js Script { type: 'Program', body: [ VariableDeclaration { type: 'VariableDeclaration', declarations: [Array], kind: 'const', name: 'team', value: '大转转FE' } ], sourceType: 'script', name: 'team', value: '大转转FE' }这样一来,我们就完成了对AST的遍历更新。generator将更新后的AST转化成代码这里我们使用escodegen去生成, 安装相关依赖 npm i escodegen --save整体代码结构如下:const esprima = require('esprima'); const estraverse = require('estraverse'); const escodegen = require('escodegen'); let code = 'const company = "zhuanzhuan" '; const ast = esprima.parseScript(code); estraverse.traverse(ast, { enter: function (node) { node.name = 'team'; node.value = "大转转FE"; } }); const transformCode = escodegen.generate(ast); console.log(transformCode);会得到如下结果:$ node test.js const team = '大转转FE';这样一来,我们就完成了对一段简单代码的AST编译过程。babel原理浅析babel的三个主要处理步骤分别是解析（parse）转换（transform），生成（generate）解析 将代码解析成抽象语法树（AST），每个js引擎（比如Chrome浏览器中的V8引擎）都有自己的AST解析器，而Babel是通过Babylon(https://github.com/babel/babylon)实现的。解析过程有两个阶段：词法分析和语法分析，词法分析阶段把字符串形式的代码转换为词元（tokens）流，令牌词元类似于AST中节点；而语法分析阶段则会把一个词元流转换成 AST的形式，同时这个阶段会把词元中的信息转换成AST的表述结构。转换 转换步骤接收 AST 并对其进行遍历，在此过程中对节点进行添加、更新及移除等操作。 Babel通过babel-traverse对其进行深度优先遍历，维护AST树的整体状态，并且可完成对其的替换，删除或者增加节点，这个方法的参数为原始AST和自定义的转换规则，返回结果为转换后的AST。生成 代码生成)步骤把最终（经过一系列转换之后）的 AST 转换成字符串形式的代码，同时还会创建源码映射（source maps）(http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/)。.代码生成其实很简单：深度优先遍历整个 AST，然后构建可以表示转换后代码的字符串。 Babel通过babel-generator再转换成js代码，过程就是深度优先遍历整个AST，然后构建可以表示转换后代码的字符串。Vue中AST抽象语法树的运用Vue中AST主要用在模板编译过程我们先来看看vue模板编译的整体流程图：vue的模板编译主要分为三个步骤解析器阶段：将template里面的代码解析成AST抽象语法树优化器阶段：将AST抽象语法树静态表情打上tag，防止重复渲染（优化了diff算法）代码生成阶段：优化后的AST抽象语法树通过generate函数生成rander函数字符串我们来看看vue源代码整体实现过程export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { //生成ast的过程 const ast = parse(template.trim(), options) //优化ast的过程,给ast抽象语法树静态标签打上tag,防止重复渲染 if (options.optimize !== false) { optimize(ast, options) } //通过generate函数生成render函数字符串 const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } })解析器要实现的功能就是将模板解析成AST，我们这里主要来分析一下代码解析阶段，这里主要运用的是parse()这个函数，事实上，函数内部也分为好几个解析器，文本解析器以及过滤解析器，其中最主要的就是HTML解析器。HTML解析器的作用是解析HTML，它在解析HTML的过程中会触发各种钩子函数，来看看代码实现parseHTML(template, { //解析开始标签 start (tag, attrs, unary, start, end) { }, //解析结束标签 end (tag, start, end) { }, //解析文本 chars (text: string, start: number, end: number) { }, //解析注释 comment (text: string, start, end){ } })举个例子: 我们是大转转FE 当上面这个模板被HTML解析器解析时,所触发的钩子函数依次是: start、chars、end。所以HTML解析器在实现上是一个函数，它有两个参数----模板和选项，我们的模板是一小段一小段去截取与解析的，所以需要不断循环截取，我们来看看vue内部实现原理：function parseHTML (html, options) { while (html) { //判断父元素为正常标签的元素的逻辑 if (!lastTag || !isPlainTextElement(lastTag)) { //vue中要判断是 文本、注释、条件注释、DOCTYPE、结束、开始标签 //除了文本标签， 其他的都是以 < 开头, 所以区分处理 var textEnd = html.indexOf('<'); if (textEnd === 0) { //注释的处理逻辑 if (comment.test(html)) {} //条件注释的处理逻辑 if (conditionalComment.test(html)) {} //doctype的处理逻辑 var doctypeMatch = html.match(doctype); if (doctypeMatch) {} //结束标签的处理逻辑 var endTagMatch = html.match(endTag); if (endTagMatch) {} //开始标签的处理逻辑 var startTagMatch = parseStartTag(); if (startTagMatch) {} } var text = (void 0), rest = (void 0), next = (void 0); //解析文本 if (textEnd >= 0) {} // "<" 字符在当前 html 字符串中不存在 if (textEnd < 0) { text = html html = '' } // 如果存在 text 文本 // 调用 options.chars 回调，传入 text 文本 if (options.chars && text) { // 字符相关回调 options.chars(text) } }else{ // 父元素为script、style、textarea的处理逻辑 } } }以上就是vue解析器生成AST语法树的主流程了，代码细节的地方还需要自己去解读源码，源码位置:vuepackagesweex-template-compilerbuild.jsAST抽象语法树的知识点作为JavaScript中(任何编程语言中都有ast这个概念,这里就不过多赘述)相对基础的，也是最不可忽略的知识，带给我们的启发是无限可能的，它就像一把螺丝刀，能够拆解javascript这台庞大的机器，让我们能够看到一些本质的东西，同时也能通过它批量构建任何javascript代码。ShadingSphere的SQL解析引擎Shadingsphere作为一款分库分表的中间件，分片是其核心功能。分片流程包括SQL解析、SQL路由、SQL改写、SQL执行，结果归并。分片流程的这些流程都对应响应的引擎这篇文章只分析SQL解析流程。SQL解析是分片流程中的第一步，是由SQL解析引擎完成SQL解析的。SQL解析分为词法分析和语法解析，首先通过词法解析将SQL语句作为一个不可再分的单词，然后再使用语法解析对SQL进行理解，提炼出解析上下文。解析上下文包括表，选择项，排序项，分组项，聚合函数，分页信息，查询条件以及可能需要修改的占位符的标记SELECT id, name FROM t_user WHERE status = 'ACTIVE' AND age > 18抽象语法树如图shadingsphere中SQL解析流程最终会将SQL语句解析为SQLStatement对象，SQLStatement包括SQL片段对象SQLSegment，SQLStatement对象是解析好的SQL对象。如InsertStatement表示SQL插入语句被shardingsphereSQL解析后的对象，该类如下：public final class InsertStatement extends DMLStatement { //表片段 private SimpleTableSegment table; //插入的字段片段 private InsertColumnsSegment insertColumns; //set 片段 private SetAssignmentSegment setAssignment; //On Duplicate Key片段 private OnDuplicateKeyColumnsSegment onDuplicateKeyColumns; //插入的值片段 private final Collection<InsertValuesSegment> values = new LinkedList<>(); //省略代码 }SQLSegment 对象包含了位置信息，如InsertColumnsSegment包含了插入字段在sql语句中开始位置和结束位置。如：public final class InsertColumnsSegment implements SQLSegment { private final int startIndex; private final int stopIndex; private final Collection<ColumnSegment> columns; }有了上述的了解，那么来看看SQL解析引擎SQLParserEngine的入口。在分析《shardingsphere源码阅读-ShardingRuntimeContext分析》中，分析了AbstractRuntimeContext，在该类的构造方法中，会初始化SQLParserEngine，如下：protected AbstractRuntimeContext(final T rule, final Properties props, final DatabaseType databaseType) { //省略代码 sqlParserEngine = SQLParserEngineFactory.getSQLParserEngine(DatabaseTypes.getTrunkDatabaseTypeName(databaseType)); //省略代码 }使用SQL引擎工程SQLParserEngineFactory创建了SQLParserEngine，SQLParserEngineFactory类如下：public final class SQLParserEngineFactory { //缓存SQLParserEngine private static final Map<String, SQLParserEngine> ENGINES = new ConcurrentHashMap<>(); public static SQLParserEngine getSQLParserEngine(final String databaseTypeName) { //根据数据库类型从缓存中获取SQL解析引擎 if (ENGINES.containsKey(databaseTypeName)) { return ENGINES.get(databaseTypeName); } //加锁，再次从缓存中获取，如果没有获取到，直接new一个SQLParserEngine synchronized (ENGINES) { if (ENGINES.containsKey(databaseTypeName)) { return ENGINES.get(databaseTypeName); } SQLParserEngine result = new SQLParserEngine(databaseTypeName); ENGINES.put(databaseTypeName, result); return result; } } }SQLParserEngineFactory类有一个ENGINES属性，保存着不同数据库类型的SQL解析引擎。getSQLParserEngine方法获取SQL解析引擎。首先根据数据库类型从缓存中获取SQL解析引擎，获取不到，则加锁再次从缓存中获取，如果还获取不到，则直接创建SQLParserEngine并添加到缓存中。获取到SQLParserEngine，就可以进行解析SQL了，SQLParserEngine类如下：public final class SQLParserEngine { //数据库类型 private final String databaseTypeName; //SQL解析结果缓存 private final SQLParseResultCache cache = new SQLParseResultCache(); public SQLStatement parse(final String sql, final boolean useCache) { //SPI解析钩子 ParsingHook parsingHook = new SPIParsingHook(); //解析钩子启动，将解析前sql作为参数传入 parsingHook.start(sql); try { //解析SQL SQLStatement result = parse0(sql, useCache); //解析SQL成功后，调用解析钩子的成功方法，将解析结果传入 parsingHook.finishSuccess(result); return result; // CHECKSTYLE:OFF } catch (final Exception ex) { // CHECKSTYLE:ON //如果解析失败，将异常作为参数传递给钩子的finishFailure方法 parsingHook.finishFailure(ex); throw ex; } } private SQLStatement parse0(final String sql, final boolean useCache) { //如果使用缓存，则从缓存中直接获取SQL解析的结果 if (useCache) { Optional<SQLStatement> cachedSQLStatement = cache.getSQLStatement(sql); if (cachedSQLStatement.isPresent()) { return cachedSQLStatement.get(); } } //使用SQL解析执行器，获取解析后的解析树ParseTree ParseTree parseTree = new SQLParserExecutor(databaseTypeName, sql).execute().getRootNode(); //委托ParseTreeVisitor创建SQLStatement SQLStatement result = (SQLStatement) ParseTreeVisitorFactory.newInstance(databaseTypeName, VisitorRule.valueOf(parseTree.getClass())).visit(parseTree); //如果使用缓存，则将解析的SQLStatement加入缓存中 if (useCache) { cache.put(sql, result); } return result; } }SQLParserEngine的parse方法，首先创建ParsingHook，用于在SQL解析的跟踪和监控。在SQL解析前，调用parsingHook的start方法，将解析前的SQL作为参数传入。然后调用parse0方法进行解析，解析SQL成功后，调用解析钩子的finishSuccess方法，将解析结果作为参数传入。如果解析过程中发生异常，则调用parsingHook的finishFailure方法，将异常作为参数传入。ParsingHook是一个接口，使用者可以实现ParsingHook的三个方法，可以监控和跟踪解析的前后过程。parse0方法是真正解析SQL的方法，如果使用缓存，则从缓存中直接获取SQL解析的结果，如果缓存中不存在，则使用SQL解析执行器，获取解析后的解析树ParseTree，然后在委托ParseTreeVisitor创建SQLStatement。ShardingSphere使用ANTLR作为SQL解析的引擎。SQLParserExecutor类将SQL解析为抽象语法树，代码如下：public final class SQLParserExecutor { //数据库类型名称 private final String databaseTypeName; //SQL语句 private final String sql; public ParseASTNode execute() { ParseASTNode result = towPhaseParse(); if (result.getRootNode() instanceof ErrorNode) { throw new SQLParsingException(String.format("Unsupported SQL of `%s`", sql)); } return result; } private ParseASTNode towPhaseParse() { //根据数据库类型和sql加载SQL解析器 SQLParser sqlParser = SQLParserFactory.newInstance(databaseTypeName, sql); try { ((Parser) sqlParser).setErrorHandler(new BailErrorStrategy()); ((Parser) sqlParser).getInterpreter().setPredictionMode(PredictionMode.SLL); //解析 return (ParseASTNode) sqlParser.parse(); } catch (final ParseCancellationException ex) { ((Parser) sqlParser).reset(); ((Parser) sqlParser).setErrorHandler(new DefaultErrorStrategy()); ((Parser) sqlParser).getInterpreter().setPredictionMode(PredictionMode.LL); return (ParseASTNode) sqlParser.parse(); } } }SQLParserExecutor类采用工厂模式很快数据库类型和sql得到不同数据库的SQL解析器。SQLParserFactory类负责获取SQL解析器：public final class SQLParserFactory { static { //注册SQL解析器的配置 NewInstanceServiceLoader.register(SQLParserConfiguration.class); } //根据SQL解析器配置获取SQL解析器 public static SQLParser newInstance(final String databaseTypeName, final String sql) { for (SQLParserConfiguration each : NewInstanceServiceLoader.newServiceInstances(SQLParserConfiguration.class)) { if (each.getDatabaseTypeName().equals(databaseTypeName)) { return createSQLParser(sql, each); } } throw new UnsupportedOperationException(String.format("Cannot support database type '%s'", databaseTypeName)); } @SneakyThrows //根据SQL解析器获取解析器 private static SQLParser createSQLParser(final String sql, final SQLParserConfiguration configuration) { Lexer lexer = (Lexer) configuration.getLexerClass().getConstructor(CharStream.class).newInstance(CharStreams.fromString(sql)); return configuration.getParserClass().getConstructor(TokenStream.class).newInstance(new CommonTokenStream(lexer)); } }SQLParserFactory类根据不同的SQL解析器配置获取不同数据库的SQL解析器，最终获取到ANTLR的SQL解析器Lexer。通过SQLParserExecutor获取到ParseTree，就可以通过ParseTreeVisitorFactory类获取不同的SQLStatement了，到这里SQLParserEngine解析就分析完了。SQLParserEngine解析器使用的入口在哪里呢？对于分库分表，ShardingStatement是执行SQL的类，对应JDBC中的Statement类，在《shardingsphere源码阅读-兼容jdbc规范》中分析到，shardingsphere兼容了原生的JDBC的规范，使用使用shardingsphere操作数据库跟原生JDBC操作数据库几乎完全一样。ShardingStatement的prepare方法如下：private ExecutionContext prepare(final String sql) throws SQLException { statementExecutor.clear(); //获取分库分表的运行时上下文 ShardingRuntimeContext runtimeContext = connection.getRuntimeContext(); //从ShardingRuntimeContext获取SQL执行引擎SQLParserEngine作为参数创建SimpleQueryPrepareEngine BasePrepareEngine prepareEngine = new SimpleQueryPrepareEngine( runtimeContext.getRule().toRules(), runtimeContext.getProperties(), runtimeContext.getMetaData(), runtimeContext.getSqlParserEngine()); ExecutionContext result = prepareEngine.prepare(sql, Collections.emptyList()); statementExecutor.init(result); statementExecutor.getStatements().forEach(this::replayMethodsInvocation); return result; }ShardingStatement类的prepare方法中，使用SQLParserEngine作为参数创建SimpleQueryPrepareEngine，SimpleQueryPrepareEngine是简单查询准备引擎，这个SimpleQueryPrepareEngine的作用是在真正执行SQL执行之前，先准备好SQL执行所需要的东西。如SQL路由的结果，SQL重写的结果。这里就不具体进行展开分析SimpleQueryPrepareEngine，后面在分析SQL路由，SQL重写会进行展开分析。
Vue双向数据绑定原理vue2defineProperty第一个参数为需要定义属性的对象第二参数为需要定义的属性名第三个参数为属性描述符使用definePropery定义的属性不能对其进行修改，删除，以及枚举。怎么解决呢？在属性描述符中设置writable:true 可修改enumerable:true 可枚举configurable:true 可操作/删除var obj={} Object.defineProperty(obj,"name",{ value:"lisa", writable:true, enumerable:true, configurable:true })defineProperty的set和get属性用于实现数据劫持var obj={} var a=1; Object.defineProperties(obj,{ a:{ // 注意：value，writable属性与get,set互斥，如果同时存在则会报错 //所以，设置初始值只能在外部设置 //在a被访问时返回a的值 get(){ console.log("a被访问了，值为"+a) return a; }, //在a被修改时，将a的值修改为传入的新值 set(newVal){ a=newVal; console.log("a被修改了，值为"+a) } } }) obj.a //访问a,值为1 obj.a=5 //设置a的值，值为5 obj.a//再次访问a，值为5将操作的元素放入数组function DataArr(){ var Arrobj={} //数组的属性 var arr=[] //this指向new DataArr() Object.defineProperty(this,"val",{ get(){ return Arrobj; }, set(newVal){ Arrobj=newVal //向数组中添加新的属性值 arr.push({val:Arrobj}) } }) this.getArr=function(){ return arr } } var dataArr=new DataArr() dataArr.val=123 dataArr.val=456 console.log(dataArr.getArr()) //[{val:123},{val:456}] 问题关于对象： Vue 无法检测 property 的添加或移除。关于数组：不能利用索引直接设置一个数组项,也不能修改数组的长度。vue3原理：Vue3.0中的响应式采用了ES6中的 Proxy 方法。Proxy 对象用于定义基本操作的自定义行为（如属性查找、赋值、枚举、函数调用等）据阮一峰文章介绍：Proxy可以理解成，在目标对象之前架设一层 "拦截"，当外界对该对象访问的时候，都必须经过这层拦截，而Proxy就充当了这种机制，类似于代理的含义，它可以对外界访问对象之前进行过滤和改写该对象。监听数组的方法不能触发Object.defineProperty方法中的set操作(如果要监听的到话，需要重新编写数组的方法)。必须遍历每个对象的每个属性，如果对象嵌套很深的话，需要使用递归调用。const obj = new Proxy(target, handler);target: 被代理对象。handler: 是一个对象，声明了代理target的一些操作。obj: 是被代理完成之后返回的对象。但是当外界每次对obj进行操作时，就会执行handler对象上的一些方法。handler中常用的对象方法如下：1. get(target, propKey, receiver)2. set(target, propKey, value, receiver)3. has(target, propKey)4. construct(target, args):5. apply(target, object, args)const target = { name: 'kongzhi' }; const handler = { get: function(target, key) { console.log("${key} 被读取"); return target[key]; }, set: function(target, key, value) { console.log("${key} 被设置为 ${value}"); target[key] = value; } }; const testObj = new Proxy(target, handler); /* 获取testObj中name属性值 会自动执行 get函数后 打印信息：name 被读取 及输出名字 kongzhi */ console.log(testObj.name); /* 改变target中的name属性值 打印信息如下： name 被设置为 111 */ testObj.name = 111; console.log(target.name); // 输出 111如上代码所示：也就是说 target是被代理的对象，handler是代理target的，那么handler上面有set和get方法，当每次打印target中的name属性值的时候会自动执行handler中get函数方法，当每次设置 target.name 属性值的时候，会自动调用 handler中的set方法，因此target对象对应的属性值会发生改变，同时改变后的 testObj对象也会发生改变。同理改变返回后 testObj对象中的属性也会改变原对象target的属性的，因为对象是引用类型的，是同一个引用的。如果这样还是不好理解的话，可以简单的看如下代码应该可以理解了：const target = { name: 'kongzhi' }; const testA = target; testA.name = 'xxx'; console.log(testA.name); // 打印 xxx console.log(target.name); // 打印 xxxget该方法的含义是：用于拦截某个属性的读取操作。它有三个参数，如下解析：target: 目标对象。propKey: 目标对象的属性。receiver: (可选)，该参数为上下文this对象const obj = { name: 'kongzhi' }; const handler = { get: function(target, propKey) { // 使用 Reflect来判断该目标对象是否有该属性 if (Reflect.has(target, propKey)) { // 使用Reflect 来读取该对象的属性 return Reflect.get(target, propKey); } else { throw new ReferenceError('该目标对象没有该属性'); } } }; const testObj = new Proxy(obj, handler); /* Proxy中读取某个对象的属性值的话， 就会使用get方法进行拦截，然后返回该值。 */ console.log(testObj.name); // kongzhi /* 如果对象没有该属性的话，就会进入else语句，就会报错： Uncaught ReferenceError: 该目标对象没有该属性 */ // console.log(testObj.name2); /* 其实Proxy中拦截的操作是在原型上的，因此我们也可以使用 Object.create(obj) 来实现对象的继承的。 如下代码演示： */ const testObj2 = Object.create(testObj); console.log(testObj2.name); // 看看他们的原型是否相等 console.log(testObj2.__proto__ === testObj.__proto__); // 返回true该方法是用来拦截某个属性的赋值操作，它可以接受四个参数，参数解析分别如下：target: 目标对象。propKey: 目标对象的属性名value: 属性值receiver(可选): 一般情况下是Proxy实列const obj = { 'name': 'kongzhi' }; const handler = { set: function(obj, prop, value) { return Reflect.set(obj, prop, value); } }; const proxy = new Proxy(obj, handler); proxy.name = '我是空智'; console.log(proxy.name); // 输出： 我是空智 console.log(obj); // 输出： {name: '我是空智'}当然如果设置该对象的属性是不可写的，那么set方法就不起作用了，如下代码演示：const obj = { 'name': 'kongzhi' }; Object.defineProperty(obj, 'name', { writable: false }); const handler = { set: function(obj, prop, value, receiver) { Reflect.set(obj, prop, value); } }; const proxy = new Proxy(obj, handler); proxy.name = '我是空智'; console.log(proxy.name); // 打印的是 kongzhi注意：proxy对数组也是可以监听的；如下代码演示，数组中的 push方法监听：const obj = [{ 'name': 'kongzhi' }]; const handler = { set: function(obj, prop, value) { return Reflect.set(obj, prop, value); } }; const proxy = new Proxy(obj, handler); proxy.push({'name': 'kongzhi222'}); proxy.forEach(function(item) { console.log(item.name); // 打印出 kongzhi kongzhi222 });该方法是判断某个目标对象是否有该属性名。接收二个参数，分别为目标对象和属性名。返回的是一个布尔型。如下代码演示：const obj = { 'name': 'kongzhi' }; const handler = { has: function(target, key) { if (Reflect.has(target, key)) { return true; } else { return false; } } }; const proxy = new Proxy(obj, handler); console.log(Reflect.has(obj, 'name')); // true console.log(Reflect.has(obj, 'age')); // falseconstruct(target, args, newTarget):该方法是用来拦截new命令的，它接收三个参数，分别为 目标对象，构造函数的参数对象及创造实列的对象。第三个参数是可选的。它的作用是拦截对象属性。如下代码演示：function A(name) { this.name = name; } const handler = { construct: function(target, args, newTarget) { /* 输出： function A(name) { this.name = name; } */ console.log(target); // 输出： ['kongzhi', {age: 30}] console.log(args); return args } }; const Test = new Proxy(A, handler); const obj = new Test('kongzhi', {age: 30}); console.log(obj); // 输出： ['kongzhi', {age: 30}]其他转载自深入理解 ES6中的 Reflect - 龙恩0707 - 博客园 (cnblogs.com)vue2 和vue3的区别监听数据原理vue2的双向数据绑定是利用了es5 的一个API Object.definepropert() 对数据进行劫持 结合发布订阅模式来实现的。vue3中使用了es6的proxyAPI对数据进行处理。相比与vue2，使用proxy API 优势有：defineProperty只能监听某个属性，不能对全对象进行监听；可以省去for in 、闭包等内容来提升效率（直接绑定整个对象即可）；可以监听数组，不用再去单独的对数组做特异性操作，vue3可以检测到数组内部数据的变化。选项式API和组合式APIoptionsAPIvue2中组织代码：我们会在一个vue文件中data，methods，computed，watch中定义属性和方法，共同处理页面逻辑上图解释了optionsAPI的缺点，一个功能往往需要在不同的vue配置项中定义属性和方法，比较分散，项目小还好，清晰明了，但项目大了之后，一个methods中可能包含多个方法，往往分不清哪个方法对应着哪个功能，而且当你想要新增一个功能的时候，你可能需要在data，methods，computed，watch中都要写一些东西。但是这个时候选项里面的内容很多你需要上下来回的翻滚，特别影响效率Composition APIcompositionAPI是根据逻辑相关组织代码的，提高可读性和可维护性，基于函数组合的API更好的重用代码逻辑（在vue2 Options API中通过Mixins重用逻辑代码，容易发生命名冲突且关系不清）Composition API 最大的优点通俗的讲就是把跟一个功能相关的东西放在一个地方，它是目前最合理也最容易维护的。你可以随时将功能的一部分拆分出去。可以将每一个功能相关所有的东西比如methods、computed都放在如上图的function中，这个function可以独立存在，可以放在一个TS文件中，也可以在NPM独立发布，最终CompositionAPI把他们组合起来代码下面的例子，将处理count属性相关的代码放在同一个函数了function useCount() { let count = ref(10); let double = computed(() => { return count.value * 2; }); const handleConut = () => { count.value = count.value * 2; }; console.log(count); return { count, double, handleConut, }; }组件上使用countexport default defineComponent({ setup() { const { count, double, handleConut } = useCount(); return { count, double, handleConut } }, });可以很清楚感受到CompositionAPI在逻辑组织方面的优势，以后修改属性功能的时候，只需要调到控制该属性的方法中即可vue2中，我们是用过mixin去复用相同的逻辑，export const MoveMixin = { data() { return { x: 0, y: 0, }; }, methods: { handleKeyup(e) { console.log(e.code); // 上下左右 x y switch (e.code) { case "ArrowUp": this.y--; break; case "ArrowDown": this.y++; break; case "ArrowLeft": this.x--; break; case "ArrowRight": this.x++; break; } }, }, mounted() { window.addEventListener("keyup", this.handleKeyup); }, unmounted() { window.removeEventListener("keyup", this.handleKeyup); }, }; 然后在组建中使用 Mouse position: x {{ x }} / y {{ y }} 使用单个mixin似乎问题不大，但是当我们一个组件混入大量不同的 mixins 的时候mixins: [mousePositionMixin, fooMixin, barMixin, otherMixin]会存在两个非常明显的问题：命名冲突数据来源不清楚 现在通过Compositon API这种方式改写上面的代码import { onMounted, onUnmounted, reactive } from "vue"; export function useMove() { const position = reactive({ x: 0, y: 0, }); const handleKeyup = (e) => { console.log(e.code); // 上下左右 x y switch (e.code) { case "ArrowUp": // y.value--; position.y--; break; case "ArrowDown": // y.value++; position.y++; break; case "ArrowLeft": // x.value--; position.x--; break; case "ArrowRight": // x.value++; position.x++; break; } }; onMounted(() => { window.addEventListener("keyup", handleKeyup); }); onUnmounted(() => { window.removeEventListener("keyup", handleKeyup); }); return { position }; } 在组件中使用 Mouse position: x {{ x }} / y {{ y }} 可以看到，整个数据来源清晰了，即使去编写更多的 hook 函数，也不会出现命名冲突的问题在逻辑组织和逻辑复用方面，Composition API是优于Options API因为Composition API几乎是函数，会有更好的类型推断。Composition API对 tree-shaking 友好，代码也更容易压缩Composition API中见不到this的使用，减少了this指向不明的情况如果是小型组件，可以继续使用Options API，也是十分友好的
牛客刷题记录2Hash和History的区别Hash路由hash模式的工作原理是hash change事件，可以在windows监听hash变化。我们在url后面随便添加一个#xx触发这个事件vue-router默认的是hash模式使用URL的hash来模拟完整的一个URL，当URL改变的时候，页面不会重新加载，也就是单页应用了，当后面hash发生变化，不会导致浏览器向服务器发出请求，浏览器不发出请求，就不会刷新页面，并且会触发hashChange这个事件，通过监听hash值的变化来实现更新页面部分内容的操作。对于hash模式会创建hashhistory对象在访问不同路由的时候会发生俩件事HashHistory.push()将新的路由添加到浏览器访问的历史栈顶，和HashHistory.replce()替换到当前栈顶路由history路由通过history.push使它做页面跳转不会出发页面刷新，使用window.onpopstate监听浏览器的前进和后退，再做其他处理每当激活同一文档中不同的历史记录条目时，popstate事件就会在对应的window对象上触发。如果当前处于激活状态的历史记录条目是由history.pushState()方法创建的或是history.replaceState()方法进行修改的，则popstate事件的state属性包含了记录条目的state对象的一个拷贝调用 history.pushState() 或者 history.replaceState() 不会触发 popstate 事件。popstate 事件只会在浏览器某些行为下触发，比如点击后退按钮（或者在 JavaScript 中调用 history.back() 方法）。即，在同一文档的两个历史记录条目之间导航会触发该事件。hash模式下url会带有#，需要url更优雅时，可以使用history模式。需要兼容低版本的浏览器时，建议使用hash模式。需要添加任意类型数据到记录时，可以使用history模式。延伸前端路由的出现要从 ajax 开始，有了 Ajax 后，用户交互就不用每次都刷新页面，体验带来了极大的提升。随着技术的发展，简单的异步已经不能满足需求，所以异步的更高级体验出现了——SPA(单页应用)。 SPA 的出现大大提高了 WEB 应用的交互体验。在与用户的交互过程中，不再需要重新刷新页面，获取数据也是通过 Ajax 异步获取，页面显示变的更加流畅。 但由于 SPA 中用户的交互是通过 JS 改变 HTML 内容来实现的，页面本身的 url 并没有变化，这导致了两个问题：• SPA 无法记住用户的操作记录，无论是刷新、前进还是后退，都无法展示用户真实的期望内容。• SPA 中虽然由于业务的不同会有多种页面展示形式，但只有一个 url，对 SEO 不友好，不方便搜索引擎进行收录。前端路由就是为了解决上述问题而出现。圣杯布局和双飞翼布局神隔壁布局和双飞翼布局是一回事，它们实现的都是三栏布局，俩边的盒子宽度固定，中间盒子自适应，也就是我们常说的固比固布局。他们的实现效果是一样的双飞翼布局如果把三栏布局比作一只鸟，可以把main看成是鸟的身体，left和right则是鸟的翅膀。这个布局的思路是先把重要的身体布局放好，然后再把翅膀移动到适当的地方圣杯布局圣杯布局是俩便固定宽度，中间自适应的三栏布局中间栏放到文档流前面，先行渲染目的是为了解决三栏布局实现方式很多可以使用定位也可以使用浮动，弹性盒子实现比较简单和双飞翼布局大同小异，区别于在堆遮挡中间栏的处理方式，圣杯为父元素设置padiding，然后相对定位把左右俩部分拉会原位双飞翼则是直接对中间部分设置margin，把左右俩边拉会原位浏览器输入URL后发生了什么用户输入url会使用浏览器默认的搜索引擎加上搜索内容合成url，如果是域名会加上协议合成完整的URL（如https）合成完整的URL然后按下回车。浏览器进程通过进程间通信把url传给网络进程（网络进程接收到url才发起真正的网络请求）网络进程接受到url后，先查找有没有缓存，有缓存直接返回缓存的资源地址，没有缓存直接发起真正的网络请求首先获得域名IP地址和服务器简历TCP连接（三次握手）简历连接后，浏览器构建数据包（请求行，请求头，请求正文，并把改域名相关的cookie等数据附加到请求头）然后想服务器发送请求消息服务器接受得到请求消息后根据请求信息构建响应数据（响应行，响应头，响应正文）然后发送回网络进程网络进程接收到数据后进行解析，如果发现状态码为301,302说明服务器要进行重定向数据传输完成，tcp四次挥手断开连接。网络进程将获取到的数据包进行解析，根据响应头中的content-type来判断响应数据类型（如果是字节类型，就将请求交给下载管理器，流程结束，不是就通知浏览器进程进行渲染）如果有数据一切正常，当浏览器拿到服务器的数据后，开始渲染页面的同时获取html中的图片音频，视频，css，js等文件这期间获取到js会直接执行js代码，阻塞浏览器进行渲染，因为渲染引擎和js引擎互斥，通常把script标签放在body的底部渲染的过程就是先将html转换成dom树，再将css样式转换为stylesheet，根据dom树和stylesheet创建布局树对布局树进行分层。为每个图层生成绘制列表，再将图层分成图块，紧接着光栅化将图块转成位图，最后合成绘制生成页面数组和伪数组对象与数组的关系在说区别之前，需要先提到另外一个知识，就是JavaScript的原型继承。所有JavaScript的内置构造函数都是继承自 Object.prototype。在这个前提下，可以理解为使用 new Array() 或 [] 创建出来的数组对象，都会拥有 Object.prototype 的属性值。var obj = {}; var arr = []; obj[2] = 'a'; arr[2] = 'a'; console.log(obj[2]); // 输出 a console.log(arr[2]); // 输出 a console.log(obj.length); // 输出 undefined console.log(arr.length); // 输出 3通过上面这个测试，可以看到，虽然 obj[2]与arr[2] 都输出'a'，但是，在输出length上有明显的差异，这是为什么呢？obj[2]输出'a'，是因为对象就是普通的键值对存取数据而arr[2]输出'a' 则不同，数组是通过索引来存取数据，arr[2]之所以输出'a'，是因为数组arr索引2的位置已经存储了数据obj.length并不具有数组的特性，并且obj没有保存属性length，那么自然就会输出undefined而对于数组来说，length是数组的一个内置属性，数组会根据索引长度来更改length的值。伪数组1、拥有length属性，其它属性（索引）为非负整数(对象中的索引会被当做字符串来处理，这里你可以当做是个非负整数串来理解)2、不具有数组所具有的方法伪数组，就是像数组一样有 length 属性，也有 0、1、2、3 等属性的对象，看起来就像数组一样，但不是数组，比如var fakeArray = { length: 3, "0": "first", "1": "second", "2": "third" }; for (var i = 0; i < fakeArray.length; i++) { console.log(fakeArray[i]); } Array.prototype.join.call(fakeArray,'+');常见的参数的参数 arguments，DOM 对象列表（比如通过 document.getElementsByTags 得到的列表），jQuery 对象（比如 $("div")）。伪数组是一个 Object，而真实的数组是一个 ArrayfakeArray instanceof Array === false; Object.prototype.toString.call(fakeArray) === "[object Object]"; var arr = [1,2,3,4,6]; arr instanceof Array === true; Object.prototype.toString.call(arr) === "[object Array]"js的import和export在理想情况下，开发者只需要实现核心的业务逻辑，其他都可以加载别人已经写好的模块。但是，javascript不是一种模块化的编程语言，在ES6以前，它是不支持类，所以也就没有模块的。在ES6标准发布后，module成为标准，标准都是以export指令导出接口，以import引入模块，但是在node模块中，我们依然采用的是CommonJS模块哦规范，使用require引入模块，使用module.exports导出接口export导出模块export语法声明用于导出函数，对象，指定文件（或模块）的原始值export有俩种模块导出方式：命名式导出和默认导出命名式每个模块可导出多个，而默认每个模块只有一个importimport 语法用于从已导出的模块、脚本中导入函数、对象、指定文件（或模块）的原始值。import 模块导入与 export 模块导出功能相对应，也存在两种模块导入方式：命名式导入（名称导入）和默认导入（定义式导入）。import 的语法跟 require 不同，而且 import 必须放在文件的最开始，且前面不允许有其他逻辑代码，这和其他所有编程语言风格一致。import defaultMember from "module-name"; import * as name from "module-name"; import { member1 } from "module-name"; import { member as alias } from "module-name"; import { member1 , member2 } from "module-name"; import { member1 , member2 as alias2 , [...] } from "module-name"; import defaultMember, { member1 [ , [...] ] } from "module-name"; import defaultMember, * as name from "module-name"; import "module-name";defaultMember 导入默认导出成员name 从将要导入模块中收到的导出值的名称member1，member1 导入指定名称的多个成员alias 别名，对指定导入成员进行的重命名module-name 要导入的模块，是一个文件名as 重命名导入成员名称from 从已经存在的模块、脚本文件等导入原生对象，宿主对象的区别原生对象原生对象包括内置对象和本地对象，内置对象是包括了一些在运行过程中动态创建的对象本地对象定义为独立于宿主环境的ECMAscropt提供的对象，包括Object、Function、Array、String、Boolean、Number、Date、RegExp 内置对象是独立于宿主环境的对象，ECMA262之定义了两个内置对象Global和MathGlobal(全局)对象是ECMAScript中一个特别的对象，因为这个对象是不存在的。在ECMAScript中不属于任何其他对象的属性和方法，都属于它的属性和方法。所以，事实上，并不存在全局变量和全局函数；所有在全局作用域定义的变量和函数，都是Global对象的属性和方法。PS：因为ECMAScript没有定义怎么调用Global对象，所以，Global.属性或者Global.方法()都是无效的。(Web浏览器将Global作为window对象的一部分加以实现)Global对象是ECMAScript中最特别的对象，因为实际上它根本不存在！在ECMAScript中，不存在独立的函数，所有函数都必须是某个对象的方法。类似于isNaN()、parseInt()和parseFloat()方法等，看起来都是函数，而实际上，它们都是Global对象的方法。而且Global对象的方法还不止这些。宿主对象ECMAscript主要在这个宿主的概念上，ECMAScript中的宿主当然就是我们网页的运行环境，何为“宿主对象”？ ECMAScript中的“宿主”当然就是我们网页的运行环境，即“操作系统”和“浏览器”。所有非原生对象都是宿主对象（host object），即由 ECMAScript 实现的宿主环境提供的对象。所有的 BOM 和 DOM 对象都是宿主对象。因为其对于不同的“宿主”环境所展示的内容不同。其实说白了就是，ECMAScript官方未定义的对象都属于宿主对象，因为其未定义的对象大多数是自己通过ECMAScript程序创建的对象。TML DOM 是 W3C 标准（是 HTML 文档对象模型的英文缩写，Document Object Model for HTML）。HTML DOM 定义了用于 HTML 的一系列标准的对象，以及访问和处理 HTML 文档的标准方法。通过 DOM，可以访问所有的 HTML 元素，连同它们所包含的文本和属性。可以对其中的内容进行修改和删除，同时也可以创建新的元素。js数据的拷贝方法通过jsona暴力转换使用扩展运算符+解构赋值使用对象合并Object.assign()遍历和递归对象什么是V8引擎V8是google开源的javascript和webAssembly引擎，用c++编写。用于Node.js和chrome等，V8实现了ECMAscropt，进而WebAssembly。V8可以独立运行，也可以嵌入到任何C++应用程序中V8使用后C++开发，并在谷歌浏览器中使用。在运行javascript，之前，相比于掐的javascript的引擎转换成字节码或解释执行，V8将其编译成原生机器码。并使用了内联缓存（inline caching），等方法来提高性能。编译执行V8引擎的性能保证来源于其内置的JavaScript编译器，V8采用的编译执行策略是JavaScript从服务端完成加载后，预先对所有的JavaScript代码进行编译如果分析V8的源代码，可以发现V8仍然有类似java字节码的中间过程，但是从外部看，V8是直接将JavaScript编译成本地机器语言的。执行过程中编译器不再对过程进行干预。V8的解释与执行是俩个不同的部分但是像JavaScript这种脚本语言有一个不同于编译语言的特点，就是它的弱类型，换句话说，某个变量属于哪种类型，只有在JavaScript语言运行过程中才能确定V8的编译器为解释这种问题，引入了内联缓存技术来加速这类在编译期不能被优化的代码举一个简单的例子，下面是一个 JavaScript 读取属性的例子：function f(v) { return v.x; }对于V8的编译器来说，仅仅通过静态编译过程无从得知，是要读取一个对象自己的属性，还是原型对象的属性，还是一个getter方法，或者浏览器的某些自定义回调，这个属性还可能根本不存在如果V8在编译的代码中处理这些所有情况，即时一个简单的操作也会引发上百条指令。为了优化这种情况，V8引擎需要对这个操作进行猜测。V8引擎引入了内联缓存机制，将猜测最可能的几个方法，放到内联缓存中（类似处理器使用高速缓存来提高指令命中率）如果猜对了V8就能大大提高访问速度，如果如果没猜对，那么继续寻找，执行并发现正确方法，这样的访问代价也不是很高。V8引擎详解（五）——内联缓存 - 掘金 (juejin.cn)垃圾回收V8 - Orinoco(垃圾回收) - 知乎 (zhihu.com)Vue和MVVMM模型（Model）数据模型，负责数据存储。泛指后端进行的各种业余逻辑处理和数据操控，主要围绕数据库系统展开V就是view视图，负责页面展示，也就是用户页面。主要又HTML和CSS来构建VM就是视图模型（View-Model）负责处理业务逻辑，对数据加工后交给视图进行展示，通过vue类创建的对象叫vue实例化对象，这个对象就是MVVM模式中的VM层，模型通过它可以将数据绑定到页面上，视图可以通过它将数据映射到模型上为什么说VUE是一个渐进式的javascript框架, 渐进式是什么意思？渐进式的概念是分层设计，每层可选，不同层可灵活介入其他方案VUE允许你将一个网页分割成可复用的组件，每个组件都包含属于自己的HTML、CSS、JAVASCRIPT以用来渲染网页中相应的地方。对于VUE的使用可大可小，它都会有相应的方式来整合到你的项目中。所以说它是一个渐进式的框架。举个例子，我们要买一台电脑，店家给我们提供了一个IBM。官方可能会提供windows作为可选，我们也可以在电脑上安装我们自己喜欢的Ubuntu。declarative rendering（声明式渲染）component system（组件系统）client-side routing（前端路由）state management（状态管理）build system（构建系统）
js牛客刷题记录1Cookie，sessionStroage，localstorage之间的区别Cookie前后端都可以写入，sessionStorage以及localStorage都是由前端写入cookie的生命周期由写入时就设置好的，localstorage是写入时就一直存在，除非手动清除，sessionStroage是由页面关闭时自动删除cookie存储空间大小约4k，sessionStroage以及localStroage空间比较大，大约5MB。前端给后端发送请求时自动携带cookie，session以及local都不携带cookie一般存储登录验证信息，localstroage常用于存储不宜变动的数据，减轻服务器压力sessionStroage可以用来检测用户是否刷新进入页面，如音乐恢复进度条功能JS基础数据类型number包括整数和浮点数JS进行浮点元素运算可能得到一个不精确的值0.1+0.2=0.300000004，不能进行精确度要求比较高的运算；NAN表示not A Number 检查时返回number数值number类型，用来表示任何类型的数字：整数或者浮点数都可以；实际上，JS中的数值，是一个64位的浮点数，这与Java中的double类型的浮点数是一致的；但是它有表示的范围，在范围内，JS可以准确表示，超出范围，JS不会报错，但是数值不准：整数范围：±253-1，闭区间，即±9007,1992,5474,0992，那么超出16位的整数，必然表示不准；浮点数范围：±1.7976,9313,4862,3157308，JS有内置常量用于表示这个数值，即Number.MAX_VALUE；能表示的最小的小数：5-324，JS有内置常量用于表示这个数值，即Number.MIN_VALUE;位运算范围：JS只支持32位整数，即[-231, 231-1]；String类型String类型（字符串）是由 Unicode 字符、数字、标点符号等组成的序列，在 JavaScript 代码中用于表示 JavaScript 文本的数据类型，字符串型数据通常由单引号或双引号包裹，由双引号定界的字符串中可以再包含有单引号，单引号定界的字符串中也可以再包含有双引号。在js中字符串需要使用引号引起来，单引号和双引号不能混用；引号不能嵌套，可以使用进行转义。Boolean与数字类型的值不同，Boolean类型（布尔型）变量的值只有固定的两种表示方式，一种是 true，另一种是 false，前者表示真，后者表示假，如果用数字表示，那么，true 可以使用 1 来表示，false 可以使用 0 来表示，布尔型变量的值来源于逻辑性运算符，常用于控制结构流程。布尔值只有两个true/false，主要做逻辑判断；使用typeof检查一个布尔值时返回boolean。Null类型在 JavaScript 代码中，Null类型（空值型）是一个比较特殊的类型，它只有一个值，就是 null，当引用一个未定义的对象时，则将返回一个这个 null 值， 从严格意义上来说，null 值本质上是一个对象类型，是一个空指针的对象类型。Null（空值）类型的值只有一个，null，用来表示一个为空的对象；使用typeof检查时值时返回object，因为null表示空对象NaN、infanity、undefined。Undefined类型与 Null 型相同，Undefined 型（未定义）也是只有一个 undefined 值，当在编写 JavaScript 代码时，如果定义了一个变量，但没有给它赋值，那么，这个变量将返回 undefined 值，这也是变量默认的值，与 Null 型不同之处在于，Null 型是一个空值，而 Undefined 型表示无值。js中输出未被赋值的被定义变量结果是Undefined；Undefined类型的值只有一个。使用typeof检查时值时返回undifineBitIntBigInt 类型是 JavaScript 中的一个基础的数值类型，可以表示任意精度的整数。使用 BigInt，您可以安全地存储和操作大整数，甚至可以超过数字类型的安全整数限制。BigInt 是通过在整数末尾附加字母 n 或调用构造函数来创建的。通过使用常量 Number.MAX_SAFE_INTEGER，您可以获得可以用数字递增的最安全的值。通过引入 BigInt，您可以操作超过 Number.MAX_SAFE_INTEGER 的数字。StringJavaScript 的字符串类型用于表示文本数据。它是一组 16 位的无符号整数值的“元素”。在字符串中的每个元素占据了字符串的位置。第一个元素的索引为 0，下一个是索引 1，依此类推。字符串的长度是它的元素的数量。不同于一些编程语言（例如，C 语言），JavaScript 的字符串是不可更改的。这意味着字符串一旦被创建，就不能被修改。但是，可以基于对原始字符串的操作来创建新的字符串。例如：获取一个字符串的子串可通过选择个别字母或者使用 String.substr()。两个字符串的连接使用连接运算符（+）或者 [String.concat()。符号类型符号（Symbols）类型是唯一且不可修改的原始值，并且可以用来作为对象的key，在某些语言中也有与之相似的类型js作用域{}和函数外的区域为全局作用域全局作用域中的声明的变量是全局变量，在页面的任意的部分都可以访问。全局作用域中无法访问函数作用域的变量全局作用域在页面打开时创建，在页面关闭时销毁。全局作用域中有一个全局对象window，它代表的是一个浏览器的窗口，由浏览器创建，可以直接使用，全局变量是window对象的属性，函数是window对象的方法。什么叫闭包闭包（closure）是js语言的特色闭包的作用：通过一系列的方法js的变量作用域有俩种，全局变量和局部变量var n = 999; function f1() { alert(n); } f1(); // 999另一方面，在函数外部无法读取函数内的局部变量。function f1() { var n = 999; } alert(n); // error这里有一个地方需要注意， 函数内部声明变量的时候，一定要使用var命令。如果不用的话，你实际上声明了一个全局变量！function f1() { n = 999; } f1(); alert(n); // 999如果一个函数访问了次函数的父级以及父级以上的作用域变量，那么这个函数就是一个闭包 var a = 1; // 匿名的立即执行函数，因访问了全局变量a，所以也是一个闭包 (function test (){ alert(a); })()本质上，JS中的每个函数都是一个闭包，因为每个函数都可以访问全局变量。js的Promise概念Promise 对象用于一个异步操作的最终完成（包括成功和失败）及结果值的表示。简而言之，就是处理异步请求的。之所以叫promise，就是承诺做这件事情，如果成功则怎么处理失败怎么处理// 语法 new Promise( // 下面定义的函数是 executor function(resolve, reject) { ... } )executorexecutor是一个带有resolve和reject俩个参数的函数executor函数在Promise构造函数执行时立即执行，被传递resolve和reject函数（executor函数在promise构造函数返回新对象前被调用）executor内部通常汇之星一些异步操作，一旦完成，可以调用resolve函数来promise状态改成fulfilled（即完成），或在发生错误时将它的状态改为rejected如果在 executor 函数中抛出一个错误，那么该 Promise 状态为 rejected 。executor 函数的返回值被忽略executor 中，reslove 或 reject 只能执行其中一个函数pending： 初始状态，不是成功或失败状态fulfilled： 意味着操作成功完成rejected： 意味着操作失败setInterval# 间隔多少毫秒就执行函数一次，循环执行 # function 延时到的时候执行的函数 # delay 延时，缺省0，立即执行 setInterval(function[,delay]); setInterval(() => { console.log('I am working!') }, 1000)可以理解为是一个定时循环的任务setTimeout# 等待多少毫秒就执行函数一次，结束 # function 延时到的时候执行的函数 # delay 延时，缺省0，立即执行 setTimeout(function[,delay]);setTimeout(() => { console.log('I am working!') }, 2000)示例new Promise(function (resolve, reject) { var a = 0; var b = 1; if (b == 0) reject("Divide zero"); else resolve(a / b); }).then(function (value) { console.log("a / b = " + value); }).catch(function (err) { console.log(err); }).finally(function () { console.log("End"); });俩个方法Promise.resolve(value) 返回 状态为 fulfilled 的 Promise 对象Promise.reject(reason) 返回 状态为 rejected 的 Promise 对象promise是异步微任务，解决了异步多层嵌套回调的问题，让代码可读性更高更容易维护。promise是Es6提供的一个构造函数，可以使用promise构造函数new一个实例，promise构造函数接受一个函数作为参数，这个函数有俩个参数，分别是resolve和reject，resolve将promise的状态由等待变为成功，将异步操作的参数传递过去，reject则将状态由等待转变为失败，在异步操作失败时调用function runAsync() { return new Promise (function(resolve, reject){ setTimeout(function(){ console.log('Do Sth...'); // resolve('OK...') reject('NOT OK...'); }, 3000); }); } runAsync().then(res=>{ console.log(res) }).catch(res=>{ console.log(res) })BCF是什么B代表box，是CSS中的概念：一个页面是由很多box组成的，元素的类型和display属性，决定了这个box的类型CF的全程是Formatting Context是W3C规范中的一个概念，它是页面中的一块渲染区域，并且有一套渲染规则，它决定了其子元素将如何定位，以及其他元素的关系和相互作用。最常见的formatting context有Block fomating Context（BFC）和Inline formatin context（简称IFC）区域:根元素float属性不为noneposition为absolute或fixed display为inline-block, table-cell, table-caption, flex, inline-flexoverflow不为visible布局：计算BFC的高度时，浮动元素也参与计算每个元素的margin box的左边， 与包含块border box的左边相接触(对于从左往右的格式化，否则相反)。即使存在浮动也是如此。内部的Box会在垂直方向，一个接一个地放置Box垂直方向的距离由margin决定。属于同一个BFC的两个相邻Box的margin会发生重叠BFC就是页面上的一个隔离的独立容器，容器里面的子元素不会影响到外面的元素。反之也如此。BFC的区域不会与float box重叠样式优先级的规则是什么第一优先级：无条件优先的属性只需要在属性后面使用！important。它会覆盖页面内任何位置定义的元素样式。ie6不支持该属性。第二优先级：在html中给元素标签加style，即内联样式。该方法会造成css难以管理，所以不推荐使用。第三优先级：由一个或多个id选择器来定义。例如，#id{margin:0;}会覆盖.classname{margin:3pxl}第四优先级：由一个或多个类选择器，属性选择器，伪类选择器定义如.classname{margin:3px}会覆盖div{margin:6px;}第五优先级：由一个或多个类型选择器定义，div{marigin:6px;}覆盖*{margin:10px；}第六优先级：通配选择器，如*{marigin:6px;}举例内联样式：写在标签属性style的样式，如 ID选择器，如#id{…}类选择器，如 .class{…}属性选择器，如 input[type="email"]{…}伪类选择器，如a:hover{…}伪元素选择器，如 p::before{…}标签选择器，如 input{…}通配选择器，如 *{…}async和await的用法async表示函数里有异步操作await表示紧跟在后面的表达式需要等待结果async函数返回一个Promise对象，可以使用then方法添加回调函数。当函数执行时，一旦遇到await方法就会先返回，等到出发的异步操作完成，再接着执行函数体后面的语句特点1）方便级联调用：即调用依次发生的场景；2）同步代码编写方式： Promise使用then函数进行链式调用，是一种从左向右的横向写法；async/await从上到下，顺序执行，就像写同步代码一样，更符合代码编写习惯；3）多个参数传递： Promise的then函数只能传递一个参数，虽然可以通过包装成对象来传递多个参数，但是会导致传递冗余信息，频繁的解析又重新组合参数，比较麻烦；async/await没有这个限制，可以当做普通的局部变量来处理，用let或者const定义的块级变量想怎么用就怎么用，想定义几个就定义几个，完全没有限制，也没有冗余工作；4）同步代码和异步代码可以一起编写： 使用Promise的时候最好将同步代码和异步代码放在不同的then节点中，这样结构更加清晰；async/await整个书写习惯都是同步的，不需要纠结同步和异步的区别，当然，异步过程需要包装成一个Promise对象放在await关键字后面；5）基于协程： Promise是根据函数式编程的范式，对异步过程进行了一层封装，async/await基于协程的机制，是真正的“保存上下文，控制权切换……控制权恢复，取回上下文”这种机制，是对异步过程更精确的一种描述；6）async/await是对Promise的优化： async/await是基于Promise的，是进一步的一种优化，不过在写代码时，Promise本身的API出现得很少，很接近同步代码的写法；async关键字表明程序里面有可能有异步的过程：表明程序里面有可能有异步的过程，里面可以有await关键字；当然全部是同步代码也没关系，但这样async关键字就多余了非阻塞： async函数里面如果有异步过程会等待，但是async函数本身会马上返回，不会阻塞当前线程，可以简单认为，async函数工作在主线程，同步执行，不会阻塞界面渲染，async函数内部由await关键字修饰的异步过程，工作在相应的协程上，会阻塞等待异步任务的完成再返回；async函数返回类型为Promise对象： 这是和普通函数本质上不同的地方，也是使用时重点注意的地方；（1）return newPromise();这个符合async函数本意；（2）return data;这个是同步函数的写法，这里是要特别注意的，这个时候，其实就相当于Promise.resolve(data);还是一个Promise对象，但是在调用async函数的地方通过简单的=是拿不到这个data的，因为返回值是一个Promise对象，所以需要用.then(data => { })函数才可以拿到这个data；（3）如果没有返回值，相当于返回了Promise.resolve(undefined);无等待 联想到Promise的特点，在没有await的情况下执行async函数，它会立即执行，返回一个Promise对象，并且绝对不会阻塞后面的语句，这和普通返回Promise对象的函数并无二致；await关键字await只能在async函数内部使用：不能放在普通函数里，否则会报错await关键字后面跟Promise对象：在Pending状态时，相应的协程会交出控制权，进入等待状态，这是协程的本质；await 仅在异步函数和模块的顶级主体中有效await是async wait的意思： wait的是resolve(data)的消息，并把数据data返回，比如下面代码中，当Promise对象由Pending变为Resolved的时候，变量a就等于data，然后再顺序执行下面的语句console.log(a)，这真的是等待，真的是顺序执行，表现和同步代码几乎一模一样；const a = await new Promise((resolve, reject) => { // async process ... return resolve(data); }); console.log(a);await后面也可以跟同步代码： 不过系统会自动将其转化成一个Promsie对象，比如：const a = await 'hello world' // 相当于 const a = await Promise.resolve('hello world'); // 跟同步代码是一样的，还不如省事点，直接去掉await关键字 const a = 'hello world';await对于失败消息的处理： await只关心异步过程成功的消息resolve(data)，拿到相应的数据data，至于失败消息reject(error)，不关心不处理；对于错误的处理有以下几种方法供选择：（1）让await后面的Promise对象自己catch；（2）也可以让外面的async函数返回的Promise对象统一catch；（3）像同步代码一样，放在一个try...catch结构中； async componentDidMount() { // 这是React Native的回调函数，加个async关键字，没有任何影响，但是可以用await关键字 // 将异步和同步的代码放在一个try..catch中，异常都能抓到 try { let array = null; let data = await asyncFunction(); // 这里用await关键字，就能拿到结果值；否则，没有await的话，只能拿到Promise对象 if (array.length > 0) { // 这里会抛出异常，下面的catch也能抓到 array.push(data); } } catch (error) { alert(JSON.stringify(error)) } } await对于结果的处理： await是个运算符，用于组成表达式，await表达式的运算结果取决于它等的东西，如果它等到的不是一个Promise对象，那么await表达式的运算结果就是它等到的东西；如果它等到的是一个Promise对象，await就忙起来了，它会阻塞其后面的代码，等着Promise对象resolve，然后得到resolve的值，作为await表达式的运算结果；虽然是阻塞，但async函数调用并不会造成阻塞，它内部所有的阻塞都被封装在一个Promise对象中异步执行，这也正是await必须用在async函数中的原因；Flex布局简介传统布局：兼容性好，布局繁琐，局限性flex布局：操作方便，布局简单，移动端广泛，IE11以上才能支持flex是flexble box的缩写，意为弹性布局，用来为盒模型提供最大的灵活性，任何一个容器都可以指定为flex布局当我们为父盒子设置为flex布局后，子元素的float，clear和vertical-align（这个是设置元素的垂直排列的.用来定义行内元素的基线相对于该元素所在行的基线的垂直对齐）属性将失效flex也叫弹性布局，伸缩布局，弹性盒布局概念采用flex布局的元素，称为flex容器，它的所有子元素自动成为容器成员，称为flex项目（flex item）就是通过给父盒子添加flex属性，来控制子盒子的位置和排列方式父项常见属性flex-direction设置主轴的方向，在flex布局中，是分为主轴和侧轴俩个方向的，也叫行和列，x轴和y轴默认主轴方向就是x轴，水平向右默认侧轴方向就是y轴方向，水平向下主轴和侧轴方向是会变化的，就看flex-direction设置谁为主轴，剩下的就是侧轴。而我们的子元素是跟着主轴来排列的row：默认值，从左到右row-reverse：从右到左column：从上到下column-reverse：从下到上justify-content设置主轴上子元素的排列方式使用这个属性之前一定要确定好主轴是哪个flex-start：默认值，从头部开始，如果主轴是x轴，则从右到左flex-end：从尾部开始排列center：在主轴居中对齐space-around：平分剩余空间space-between：先俩边贴边，再平分剩余空间flex-wrap设置子元素换行默认情况下，项目都排在一条线上。flex-wrap属性定义，flex布局中默认是不换行的，如果装不下会缩小子元素宽度nowrap：默认值，不换行wrap：换行align-itemsalign-items是设置侧轴上子元素排列方式，在子项为单项时使用flex-start默认值，从上到下flex-end：从下到上center：垂直居中stretch：拉伸align-content设置子项侧轴上的排列方式，并且只能用于子项出现换行的情况，在单行下是没有效果的flex-start：默认值在侧轴的头部开始排列flex-end：在侧轴的尾部开始排列center：在侧轴中间显示space-around：在周抽平分剩余空间space-between：子项在侧轴先分布在俩头，再平分剩余空间stretch：设置子项元素高度平分父元素高度flex-flow是flex-direction和flex-warp属性的复核属性第一个参数flex-direction，第二个参数是flex-warpflex-flow:column warp;子项常见属性flex属性flex:1flex属性定义子项目分配的剩余空间，用flex来表示占多少份数align-self控制子项在侧轴上的排列方式align-self属性允许有单个项目有与其他项目不一样的对其方式，可覆盖align-items属性默认值为auto，表示继承父元素的align-items属性，如果没有父元素，则等同于stretchorder数值越小，排列越靠前，默认为0注意和z-index不一样js的变量提升变量提升console.log(a); var a=3;按理说，按照js单线程的逻辑，这里输出的应该是报错，即a is not defined因为我们想的是在没有定义a之前打印输出了a变量，理应报这个我们还未定义a的错误，可事实真的如此吗，我们来看实际输出的结果：结果竟然输出的是undefined，也就是js认为现在a已经定义过了，但是没赋值。这是怎么回事？我们明明把定义和赋值写在了第二行。这，就是本文介绍的，提升。简单来说，就是js代码执行前引擎会进行预编译，预编译期间会将函数变量声明提升至变量的最前端，然后再去执行剩下的代码函数提升function chifan(){ console.log('我要吃米饭') } chifan() function chifan(){ console.log('我要吃面') } chifan()可以看到，定义了chifan函数，分别输出两个字符串，按照我们对js单线程的逻辑理解，执行的代码结果应该是：‘我要吃米饭’‘我要吃面’可实际上呢：这就是js所谓的函数提升，我们使用的是函数声明的方式，所以js会有类似变量提升的函数提升，与变量提升不同，函数提升不仅仅提升函数声明，而是提升函数整体var game=function (){ console.log('玩英雄联盟') } game() var game=function (){ console.log('玩CSGO') } game()这里并没有用函数声明的方式，而用的是函数表达式的方式，所以并不存在函数整体提升，仅仅也只是声明提升，具体执行过程如下补充let，cost不会存在变量提升的问题，函数声明提示，都是在js编一阶段提升的，当前变量函数提升至当前作用域的最顶端，并没有开始执行代码，所以都会被赋初值undefined。js的指向，箭头函数和普通函数js中=>是箭头函数，是ES6标准中的新函数。箭头函数表达式的语法比函数表达式更加简介，并且没有自己的this比如 x => x * x相当于function (x) { return x * x; }箭头函数可以有一个“简写体”或常见的“块体”。在一个简写体中，只需要一个表达式，并附加一个隐式的返回值。在块体中，必须使用明确的return语句。var func = x => x * x; // 简写函数 省略return(简写体) var func = (x, y) => { return x + y; }; //常规编写 明确的返回值(块体)递归var fact = (x) => ( x==0 ? 1 : x*fact(x-1) ); fact(5); // 120JS的重绘和重排（回流）重排一定会导致重绘，重绘不一定导致重排，如DOM变化不影响几何属性，元素布局没有改变，则只发生一次重绘（不需要重排）如何减少重绘和重拍尽可能在DOM树的最末端改变class避免设置多层内联样式动画效果应用到position属性为absolute或fixed的元素上避免使用table布局使用css3硬件加速，可以让transform、opacity、filters等动画效果不会引起回流重绘js操作中避免重绘避免使用JS一个样式修改完接着改下一个样式，最好一次性更改CSS样式，或者将样式列表定义为class的名称避免频繁操作DOM，使用文档片段创建一个子树，然后再拷贝到文档中先隐藏元素，进行修改后再显示该元素，因为display:none上的DOM操作不会引发回流和重绘避免循环读取offsetLeft等属性，在循环之前把它们存起来对于复杂动画效果,使用绝对定位让其脱离文档流，否则会引起父元素及后续元素大量的回流JS的六种继承方式prototype原型对象构造js的构造器function person(firstname,lastname,age,eyecolor) { this.firstname=firstname; this.lastname=lastname; this.age=age; this.eyecolor=eyecolor; }有了构造器就可以创建新的对象实例var myFather=new person("John","Doe",50,"blue"); var myMother=new person("Sally","Rally",48,"green");所有的js对象都会从一个prototype原型中集成属性和方法我们也知道在一个存在构造器的对象中是不能添加属性操作的Person.nationality = "English";需要添加的时候需要在构造器函数中加function Person(first, last, age, eyecolor) { this.firstName = first; this.lastName = last; this.age = age; this.eyeColor = eyecolor; this.nationality = "English"; }prototype继承所有js对象都是一个prototype原型对象中集成属性和方法Date 对象从 Date.prototype 继承。Array 对象从 Array.prototype 继承。Person 对象从 Person.prototype 继承。所有js中的对象都是位于原型链顶端的Object实例js对象有一个指向一个原型对象的链。当试图访问一个对象的属性时，它不仅仅在该对象上搜寻该对象的原型，依次层层向上搜索，知道匹配到一个名字匹配的或找到一个名字匹配的属性或原型链的末尾有的时候我们想要在所有已经存在的对象添加新的属性或方法。另外，有时候我们想要在对象的构造函数中添加属性或方法。使用 prototype 属性就可以给对象的构造函数添加新的属性：function Person(first, last, age, eyecolor) { this.firstName = first; this.lastName = last; this.age = age; this.eyeColor = eyecolor; } Person.prototype.nationality = "English";当然我们也可以使用 prototype 属性就可以给对象的构造函数添加新的方法：function Person(first, last, age, eyecolor) { this.firstName = first; this.lastName = last; this.age = age; this.eyeColor = eyecolor; } Person.prototype.name = function() { return this.firstName + " " + this.lastName; };原型链继承function Parent() { this.isShow = true this.info = { name: "mjy", age: 18, }; } Parent.prototype.getInfo = function() { console.log(this.info); console.log(this.isShow); } function Child() {}; Child.prototype = new Parent(); let Child1 = new Child(); Child1.info.gender = "男"; Child1.getInfo(); // {name: 'mjy', age: 18, gender: '男'} ture let child2 = new Child(); child2.isShow = false console.log(child2.info.gender) // 男 child2.getInfo(); // {name: 'mjy', age: 18, gender: '男'} false优点：写法方便简洁，容易理解缺点：对象实例沟通下所有继承的属性和方法。不能传参数，因为这个对象是一次性创建的借用构造函数继承call方法call传入的参数数量不固定，第一个参数是代表函数体内的this指向，第二个参数开始往后，每个参数传入函数apply方法apply方法接受俩个参数，第一个参数制定了函数体内的this对象的指向，两个方法都使用了对象本身作为第一个参数。 两者的区别在于第二个参数： apply传入的是一个参数数组，也就是将多个参数组合成为一个数组传入，而call则作为call的参数传入（从第二个参数开始）。call传递的参数是序列1，2，3，4apply传递的参数是集合型[1，2，3，4]例子 var obj={ name:"张三", age:20 }; function method(){ console.log(this);//window } method(); this指向是window，那么怎么让他指向当前对象呢var obj={ name:"张三", age:20 }; function method(a,b,c){ console.log(this,a,b,c);//{name: "张三", age: 20} 1 2 3 } method.call(obj,1,2,3);applyvar obj={ name:"张三", age:20 }; function method(a,b,c){ console.log(this,a,b,c);//{name: "张三", age: 20} 1 2 3 } method.apply(obj,[1,2,3]); 组合继承（经典继承）将原型链和借用构造函数组合到一块。使用原型链实现对原型属性和方法的继承，而通过借用构造函数来实现Udine实例属性的继承。这样通过在原型上定义实现了函数复用，又保证了每个实例都有自己的属性function Person(gender) { console.log('执行次数'); this.info = { name: "mjy", age: 19, gender: gender } } Person.prototype.getInfo = function () { // 使用原型链继承原型上的属性和方法 console.log(this.info.name, this.info.age) } function Child(gender) { Person.call(this, gender) // 使用构造函数法传递参数 } Child.prototype = new Person() let child1 = new Child('男'); child1.info.nickname = 'xiaoma' child1.getInfo() console.log(child1.info); let child2 = new Child('女'); console.log(child2.info);优点: 解决了原型链继承和借用构造函数继承造成的影响。缺点: 无论在什么情况下，都会调用两次超类型构造函数：一次是在创建子类型原型的时候，另一次是在子类型构造函数内部原型式继承借用构造函数A内部创建一个临时性的构造函数，然后将传入的对象最为这个构造函数的原型，最后返回这个临时类型的一个新实例本质上函数A是对传入的对象进行了一次复刻function createObject(obj) { function Fun() {} Fun.prototype = obj return new Fun() } let person = { name: 'mjy', age: 18, hoby: ['唱', '跳'], showName() { console.log('my name is:', this.name) } } let child1 = createObject(person) child1.name = 'xxxy' child1.hoby.push('rap') let child2 = createObject(person) console.log(child1) console.log(child2) console.log(person.hoby) // ['唱', '跳', 'rap']Object.create()Object.create() 是把现有对象的属性，挂到新建对象的原型上，新建对象为空对象ECMAScript 5通过增加Object.create()方法将原型式继承的概念规范化了。这个方法接收两个参数：作为新对象原型的对象，以及给新对象定义额外属性的对象（第二个可选）。在只有一个参数时，Object.create()与这里的函数A方法效果相同。let person = { name: 'mjy', age: 19, hoby: ['唱', '跳'], showName() { console.log('my name is: ', this.name) } } let child1 = Object.create(person) child1.name = 'xxt' child1.hoby.push('rap') let child2 = Object.create(person) console.log(child1) console.log(child2) console.log(person.hoby) // ['唱', '跳', 'rap']优点是：不需要单独创建构造函数。缺点是：属性中包含的引用值始终会在相关对象间共享，子类实例不能向父类传参寄生式继承寄生式继承的思路与(寄生) 原型式继承 和 [工厂模式](https://so.csdn.net/so/search?q=工厂模式&spm=1001.2101.3001.7020) 似, 即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象。function objectCopy(obj) { function Fun() { }; Fun.prototype = obj; return new Fun(); } function createAnother(obj) { let clone = objectCopy(obj); clone.showName = function () { console.log('my name is：', this.name); }; return clone; } let person = { name: "mjy", age: 18, hoby: ['唱', '跳'] } let child1 = createAnother(person); child1.hoby.push("rap"); console.log(child1.hoby); // ['唱', '跳', 'rap'] child1.showName(); // my name is： mjy let child2 = createAnother(person); console.log(child2.hoby); // ['唱', '跳', 'rap']优点：写法简单，不需要单独创建构造函数。缺点：通过寄生式继承给对象添加函数会导致函数难以重用。使用寄生式继承来为对象添加函数, 会由于不能做到函数复用而降低效率;这一点与构造函数模式类似.寄生组合式继承前面讲过，组合继承是常用的经典继承模式，不过，组合继承最大的问题就是无论什么情况下，都会调用两次父类构造函数；一次是在创建子类型的时候，一次是在子类型的构造函数内部。寄生组合继承就是为了降低父类构造函数的开销而实现的。通过借用构造函数来继承属性，通过原型链的混成形式来继承方法。本质上，就是使用寄生式继承来继承超类型的原型，然后再将结果指定给子类型的原型。function objectCopy(obj) { function Fun() { }; Fun.prototype = obj; return new Fun(); } function inheritPrototype(child, parent) { let prototype = objectCopy(parent.prototype); prototype.constructor = child; Child.prototype = prototype; } function Parent(name) { this.name = name; this.hoby = ['唱', '跳'] } Parent.prototype.showName = function () { console.log('my name is：', this.name); } function Child(name, age) { Parent.call(this, name); this.age = age; } inheritPrototype(Child, Parent); Child.prototype.showAge = function () { console.log('my age is：', this.age); } let child1 = new Child("mjy", 18); child1.showAge(); // 18 child1.showName(); // mjy child1.hoby.push("rap"); console.log(child1.hoby); // ['唱', '跳', 'rap'] let child2 = new Child("yl", 18); child2.showAge(); // 18 child2.showName(); // yl console.log(child2.hoby); // ['唱', '跳']优点是：高效率只调用一次父构造函数，并且因此避免了在子原型上面创建不必要，多余的属性。与此同时，原型链还能保持不变；缺点是：代码复杂class继承// 继承Person类中的sayHi方法 class Person { sayHi(){ console.log("hello"); } } class Chinese extends Person {} // 继承Person类中的属性和方法 class Person { constructor(name, age, gender){ this.name = name; this.age = age; this.gender = gender; } sayHi(){ console.log("hello"); } } // 子类 class Chinese extends Person { constructor(name, age, gender, skin){ // 在子类中，必须在constructor函数中，首先调用super() super(name, age, gender); // 相当于Person.call(this,name,age,gender) // 调用super之后才可以去写其他代码 this.skin = skin; } } let xm = new Chinese("xm", 20, "male", "黄"); console.log(xm); xm.sayHi();前端页面性能指标首屏绘制First Paint fp是时间线上的第一个时间点是至浏览器从响应用户输入网址地址，到浏览器开始显示内容的时间，简而言之就是浏览器第一次发生变化的时间首屏内容绘制First Contentful Paint FCP 翻译为首次内容绘制，指浏览器从响应用户输入网络地址，在页面首次绘制文本，图片，非白色的canvas或者SVG才能算作FCP。可交互时间time to interactive TTI,首屏交互时间，表示网页第一次完全达到可交互状态的时间点。可交互状态指的是页面上的 UI 组件是可以交互的（可以响应按钮的点击或在文本框输入文字等），不仅如此，此时主线程已经达到“流畅”的程度，主线程的任务均不超过50毫秒。在一般的管理系统中，TTI 是一个很重要的指标。首次有效绘制First Meaning Paint FMP，表示页面的“主要内容”开始出现在屏幕上的时间点，它以前是我们测量用户加载体验的主要指标。本质上是通过一个算法来猜测某个时间点可能是 FMP，但是最好的情况也只有77%的准确率，在lighthouse6.0 的时候废弃掉了这个指标，取而代之的是 LCP 这个指标。LCP（全称“Largest Contentful Paint”）表示可视区“内容”最大的可见元素开始出现在屏幕上的时间点。vue的生命周期钩子函数beforCreate() 实例创建前触发created() 实例创建完成beforMount()模板渲染前,可以访问数据,模板编译完成,虚拟dom已经存在mounted()模板渲染完成,可以拿到dom节点的数据beforeUpdate()更新前updated()更新完成activated()激活前deactivated()激活后beforDestory()销毁前destroyed()销毁后组件之间如何通信父子：props，emit，refs兄弟：eventbus，emit隔代：inject，provide，attr，listener不传火了：vuex，piniaDiff算法简介diff算法可以看做是一种对比算法，对比的对象是新旧虚拟DOM。顾名思义，diff算法可以找到新旧Dom之间的差异，但diff算法中其实并不是只有对比虚拟Dom，还有根据对比用户的结果更新真实DOM它有六个属性，sel表示当前节点的属性，children表示当前节点的其他子标签节点，elm表示当前虚拟节点对应的真实节点（这里暂时没有），key纪委当前节点的key，text表示节点下的文本let vnode = { sel: 'ul', data: {}, children: [ { sel: 'li', data: { class: 'item' }, text: 'son1' }, { sel: 'li', data: { class: 'item' }, text: 'son2' }, ], elm: undefined, key: undefined, text: undefined }那么虚拟Dom有什么用呢。我们其实可以把虚拟dom理解成对应真实Dom的一种状态。当真实Dom发生变化后，虚拟Dom可以为我们提供这个真实Dom变化之前和变化之后的状态，我们通过对比这俩个状态，即可得出Dom真实需要更新的部分，即可实现最小量更新。在一些比较复杂的Dom变化场景中，通过对比虚拟Dom后更新真实Dom会比直接更新真实Dom效率高，这就是虚拟Dom和Diff算法真正存在的含义H函数在 介绍diff算法原理之前还需要简单让大家了解一下h函数，因为我们要靠它称为我们生成虚拟Dom，这个render函数里面传入的那个h函数h函数可以接受多种类型的参数，但其实它内部只干了一件事，就是执行vnode函数。根据传入h函数的参数来决定执行vnode函数时传入的参数。那么vnode函数又是干什么的呢？vnode函数其实也只干了一件事，就是把传入h函数的参数转化为一个对象，即虚拟Dom。// vnode.js export default function (sel, data, children, text, elm) { const key = data.key return {sel, data, children, text, elm, key} } 执行h函数后，内部会通过vnode函数生成虚拟Dom，h函数在把这个虚拟return出去算法总结由于diff算法对比的是虚拟Dom，而虚拟Dom是呈树状的，所以我们可以发现，diff算法中充满了递归。总结起来，其实diff算法就是一个 patch —> patchVnode —> updateChildren —> patchVnode —> updateChildren —> patchVnode这样的一个循环递归的过程。这里再提一嘴key，我们面试中经常会被问到vue中key的作用。根据上面我们分析的，key的主要作用其实就是对比两个虚拟节点时，判断其是否为相同节点。加了key以后，我们可以更为明确的判断两个节点是否为同一个虚拟节点，是的话判断子节点是否有变更（有变更更新真实Dom），不是的话继续比。如果不加key的话，如果两个不同节点的标签名恰好相同，那么就会被判定为同一个节点（key都为undefined），结果一对比这两个节点的子节点发现不一样，这样会凭空增加很多对真实Dom的操作，从而导致页面更频繁得进行重绘和回流。所以我认为合理利用key可以有效减少真实Dom的变动，从而减少页面重绘和回流的频率，进而提高页面更新的效率。VNode在刀耕火种的年代，我们需要在各个事件方法中直接操作DOM来达到修改视图的目的。但是当应用一大就会变得难以维护。那我们是不是可以把真实DOM树抽象成一棵以JavaScript对象构成的抽象树，在修改抽象树数据后将抽象树转化成真实DOM重绘到页面上呢？于是虚拟DOM出现了，它是真实DOM的一层抽象，用属性描述真实DOM的各个特性。当它发生变化的时候，就会去修改视图。可以想象，最简单粗暴的方法就是将整个DOM结构用innerHTML修改到页面上，但是这样进行重绘整个视图层是相当消耗性能的，我们是不是可以每次只更新它的修改呢？所以Vue.js将DOM抽象成一个以JavaScript对象为节点的虚拟DOM树，以VNode节点模拟真实DOM，可以对这颗抽象树进行创建节点、删除节点以及修改节点等操作，在这过程中都不需要操作真实DOM，只需要操作JavaScript对象后只对差异修改，相对于整块的innerHTML的粗暴式修改，大大提升了性能。修改以后经过diff算法得出一些需要修改的最小单位，再将这些小单位的视图进行更新。这样做减少了很多不需要的DOM操作，大大提高了性能。Vue就使用了这样的抽象节点VNode，它是对真实DOM的一层抽象，而不依赖某个平台，它可以是浏览器平台，也可以是weex，甚至是node平台也可以对这样一棵抽象DOM树进行创建删除修改等操作，这也为前后端同构提供了可能。这是js源码中对vnode的定义export default class VNode { tag: string | void; data: VNodeData | void; children: ?Array<VNode>; text: string | void; elm: Node | void; ns: string | void; context: Component | void; // rendered in this component's scope functionalContext: Component | void; // only for functional component root nodes key: string | number | void; componentOptions: VNodeComponentOptions | void; componentInstance: Component | void; // component instance parent: VNode | void; // component placeholder node raw: boolean; // contains raw HTML? (server only) isStatic: boolean; // hoisted static node isRootInsert: boolean; // necessary for enter transition check isComment: boolean; // empty comment placeholder? isCloned: boolean; // is a cloned node? isOnce: boolean; // is a v-once node? constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions ) { /*当前节点的标签名*/ this.tag = tag /*当前节点对应的对象，包含了具体的一些数据信息，是一个VNodeData类型，可以参考VNodeData类型中的数据信息*/ this.data = data /*当前节点的子节点，是一个数组*/ this.children = children /*当前节点的文本*/ this.text = text /*当前虚拟节点对应的真实dom节点*/ this.elm = elm /*当前节点的名字空间*/ this.ns = undefined /*编译作用域*/ this.context = context /*函数化组件作用域*/ this.functionalContext = undefined /*节点的key属性，被当作节点的标志，用以优化*/ this.key = data && data.key /*组件的option选项*/ this.componentOptions = componentOptions /*当前节点对应的组件的实例*/ this.componentInstance = undefined /*当前节点的父节点*/ this.parent = undefined /*简而言之就是是否为原生HTML或只是普通文本，innerHTML的时候为true，textContent的时候为false*/ this.raw = false /*静态节点标志*/ this.isStatic = false /*是否作为根节点插入*/ this.isRootInsert = true /*是否为注释节点*/ this.isComment = false /*是否为克隆节点*/ this.isCloned = false /*是否有v-once指令*/ this.isOnce = false } // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ get child (): Component | void { return this.componentInstance } }这是一个最基础的VNode节点，作为其他派生VNode类的基类，里面定义了下面这些数据。tag: 当前节点的标签名data: 当前节点对应的对象，包含了具体的一些数据信息，是一个VNodeData类型，可以参考VNodeData类型中的数据信息children: 当前节点的子节点，是一个数组text: 当前节点的文本elm: 当前虚拟节点对应的真实dom节点ns: 当前节点的名字空间context: 当前节点的编译作用域functionalContext: 函数化组件作用域key: 节点的key属性，被当作节点的标志，用以优化componentOptions: 组件的option选项componentInstance: 当前节点对应的组件的实例parent: 当前节点的父节点raw: 简而言之就是是否为原生HTML或只是普通文本，innerHTML的时候为true，textContent的时候为falseisStatic: 是否为静态节点isRootInsert: 是否作为跟节点插入isComment: 是否为注释节点isCloned: 是否为克隆节点isOnce: 是否有v-once指令打个比方，比如说我现在有这么一个VNode树{ tag: 'div' data: { class: 'test' }, children: [ { tag: 'span', data: { class: 'demo' } text: 'hello,VNode' } ] }渲染之后就是这样的 hello,VNode 下面是一些创建vnode的方法/*创建一个空VNode节点*/ export const createEmptyVNode = () => { const node = new VNode() node.text = '' node.isComment = true return node } /*创建一个文本节点*/ export function createTextVNode (val: string | number) { return new VNode(undefined, undefined, undefined, String(val)) }vue中的keep-alivekeep-alive是vue的内置组件，会缓存不活动的组件实例而不是销毁他们。和transition相似，keep-alive是一个抽象组件：它自身不会渲染成一个DOM元素，也不会出现在父组件中作用：在组件切换过程中将状态保留在内存中，防止重复渲染DOM，减少加载时间即性能消耗，提高用户体验性原理：在created函数调用时将需要缓存的Vnode节点保存在this.cache中，在render页面渲染时，如果VNode的缓存的name符合缓存条件（即可以用include以及exclude控制），则会从this.catch中取出之前缓存的Vnode实例进行渲染参数名值描述include字符串或正则表达式只有名称匹配的组件会被缓存。exclude字符串或正则表达式任何名称匹配的组件都不会被缓存。max数字最多可以缓存多少组件实例。// router.js { path: '/home', name: 'home', component: () => import('../views/home.vue') }, { path: '/test', name: 'test', component: () => import('../views/test.vue') },// App.vue <keep-alive include="test"> <router-view/> </keep-alive> // 1. 将缓存 name 为 test 的组件(基本） <keep-alive include='test'> <router-view/> </keep-alive> // 2. 将缓存 name 为 a 或者 b 的组件，结合动态组件使用 <keep-alive include='a,b'> <router-view/> </keep-alive> // 3. 使用正则表达式，需使用 v-bind <keep-alive :include='/a|b/'> <router-view/> </keep-alive> // 4.动态判断 <keep-alive :include='includedComponents'> <router-view/> </keep-alive> // 5. 将不缓存 name 为 test 的组件 <keep-alive exclude='test'> <router-view/> </keep-alive> // 6. 和 `<transition>` 一起使用 <transition> <keep-alive> <router-view/> </keep-alive> </transition> // 7. 数组 (使用 `v-bind`) <keep-alive :include="['a', 'b']"> <component :is="view"></component> </keep-alive> // test.vue 此时切换路由，我们就会发现 test 文件内的 inputVal 会被缓存了，而 home 内的值没有被缓存。此外，我们还可以通过路由中的 meta 属性来控制，是否需要缓存。将 test 路由中的 meta 添加 keepAlive 属性为 true，表示当前路由组件要进行缓存。// router.js { path: '/home', name: 'home', component: () => import('../views/home.vue') }, { path: '/test', name: 'test', meta:{ keepAlive:true }, component: () => import('../views/test.vue') },keep-alive 代码可以结合 v-if 进行包裹，如果 meta 中的 keepAlive 为 true 进行缓存，否侧不进行缓存。<keep-alive> <router-view v-if="$route.meta.keepAlive" /> </keep-alive> <router-view v-if="!$route.meta.keepAlive" />实际开发中，我们可以结合路由守卫来实现需要缓存组件的缓存。export default { beforeRouteLeave(to, from, next) { to.meta.keepAlive = true; next(); } }名称描述activated在 keep-alive 组件激活时调用， 该钩子函数在服务器端渲染期间不被调用。deactivated在 keep-alive 组件停用时调用，该钩子在服务器端渲染期间不被调用。使用< keep-alive > 会将数据保留在内存中，如果要在每次进入页面的时候获取最新的数据，需要在 activated 阶段获取数据，承担原来created钩子中获取数据的任务。被包含在 < keep-alive > 中创建的组件，会多出两个生命周期的钩子: activated 与 deactivatedactivated：在组件被激活时调用，在组件第一次渲染时也会被调用，之后每次keep-alive激活时被调用。deactivated：在组件被停用时调用。注意： 只有组件被 keep-alive 包裹时，这两个生命周期才会被调用，如果作为正常组件使用，是不会被调用，以及在 2.1.0 版本之后，使用 exclude 排除之后，就算被包裹在 keep-alive 中，这两个钩子依然不会被调用！另外在服务端渲染时此钩子也不会被调用的。什么时候获取数据？当引入keep-alive 的时候，页面第一次进入，钩子的触发顺序 created -> mounted -> activated，退出时触发 deactivated。当再次进入（前进或者后退）时，只触发 activated。我们知道 keep-alive 之后页面模板第一次初始化解析变成 HTML 片段后，再次进入就不在重新解析而是读取内存中的数据。只有当数据变化时，才使用 VirtualDOM 进行 diff 更新。所以，页面进入的数据获取应该在 activated 中也放一份。数据下载完毕手动操作 DOM 的部分也应该在activated中执行才会生效。所以，应该 activated 中留一份数据获取的代码，或者不要 created 部分，直接将 created 中的代码转移到 activated 中。应用场景如果未使用 keep-alive 组件，则在页面回退时仍然会重新渲染页面，触发 created 钩子，使用体验不好。在以下场景中使用 keep-alive 组件会显著提高用户体验，菜单存在多级关系（如：主页 -> 列表页 -> 详情页）的场景：Render函数简介Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中，真的需要 JavaScript 的完全编程的能力。这时可以用渲染函数，它比模板更接近编译器。render 函数和 template 一样都是创建 html 模板的，但是有些场景中用 template 实现起来代码冗长繁琐而且有大量重复，这时候就可以用 render 函数。createElement 函数讲解这个函数的作用就是生成一个VNode节点，render函数得到这个VNode节点之后，返回给Vue.js的mount函数，渲染成真实DOM节点并挂在到根节点上createElement 函数的返回值是 VNode（即：虚拟节点）参数一个HTML标签字符串，组件选项对象，或者解析上述任何一种的async异步函数一个包含模板属性的数据对象，可以在template中使用这些特性子虚拟节点，由createElemet构建组成，也就可以使用字符串来生成文本虚拟节点// main.js文件 new Vue({ el: '#app', render:function (createElement) { //1.普通用法 // createElement(标签，{属性}，[内容]) return createElement("h2",{class:"box"},['hello',createElement("button",["按钮"])]) } })我们也可以自定义一个组件，传递给render函数// ... let Cpn = { template:' {{message}} ', data(){ return { message:"我是组件" } } } new Vue({ el: '#app', render:function (createElement) { //2.使用组件 return createElement(Cpn) } })效果如图nextTick原理js运行机制首先我们要了解JavaScript是一个单线程的脚本语言，因为如果同时对同一个Dom节点进行添加和删除操作，那么就会出现问题，我该听谁的，具体怎么操作。了解了js是一门单线程的脚本语言，我们来认识几个概念主线程: 可以理解成js的首要任务，也可以方便理解成同步任务的先进先出。调用了，使用了同步任务就执行。 执行栈: 因为执行栈是一个先入后出的一个数据结构，为了便于理解，我们姑且先浅显的理解成有一个同步任务来了进入栈底马上执行它，然后释放。任务队列: 任务队列的存在就是为了应对一些异步任务的存在，我们在执行一个任务如果迟迟等不到总不可能一直等待着，程序不接着往下进行。 宏任务和微任务 在这么我们都将其看作是异步任务，因为通俗理解主线程是在执行同步任务完成之后才会来执行宏任务和微任务。在这里我们通俗理解微任务的优先级比宏任务要高，先于宏任务执行。所有同步任务都在主线程上执行，形成一个执行栈（execution context stack）。主线程之外，还存在一个"任务队列"（task queue）。只要异步任务有了运行结果，就在"任务队列"之中放置一个事件。一旦"执行栈"中的所有同步任务执行完毕，系统就会读取"任务队列"，看看里面有哪些事件。那些对应的异步任务，于是结束等待状态，进入执行栈，开始执行。主线程不断重复上面的第三步。宏任务包含：执行script标签内部代码、setTimeout/setInterval、ajax请、postMessageMessageChannel、setImmediate，I/O（Node.js） 微任务包含：Promise、MutonObserver、Object.observe、process.nextTick（Node.js）主线程的执行过程就是一个 tick，而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务（task）。 规范中规定 task 分为两大类，分别是 macro task 和 micro task，并且每个 macro task 结束后，都要清空所有的 micro task。异步： 指的是不进入主线程，某个异步任务可以执行了，该任务才会进入主线程执行。执行完函数或方法后，不必阻塞性地等待返回值或消息，只需要向系统委托一个异步过程，那么当系统接收到返回值或消息时，系统会自动触发委托的异步过程，从而完成一个完整的流程。异步更新队列vue在更新DOM时是异步执行的。只要侦听到数据变化，Vue将开启一个队列，并缓冲在同一事件循环中发生的所有数据变更如同一个watcher被多次触发，只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和DOM是非常重要的然后，在下一个的事件循环“tick”中，Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate，如果执行环境不支持，则会采用 setTimeout(fn, 0) 代替。在 vue2.5 的源码中，macrotask 降级的方案依次是：setImmediate、MessageChannel、setTimeoutvue的nextTick实现原理vue异步队列的方式来控制Dom更新和nextTick回调后先执行microtask 因为其高优先级特性，能确保队列中的微任务在一次事件循环前被执行完毕考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案$nextTick的原理：$nextTick本质是返回一个Promise 加分回答 应用场景：在钩子函数created()里面想要获取操作Dom，把操作DOM的方法放在$nextTick中vue组件data为什么必须是函数因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。CSS3硬件加速GPU 硬件加速是指应用 GPU 的图形性能对浏览器中的一些图形操作交给 GPU 来完成，因为 GPU 是专门为处理图形而设计，所以它在速度和能耗上更有效率 左边元素的动画通过 left/top 操作位置实现，右边元素的动画通过 transform: translate 实现，你可以打开 chrome 的 “Paint flashing” 查看，绿色部分是正在 repaint 的内容。查看地址从 demo 中可以看到左边的图形在运动时外层有一圈绿色的边框，表示元素不停地 repaint，并且可以看到其运动过程中有丢帧现象，具体表现为运动不连贯，有轻微闪动。那么transform是如何让动画不会导致重绘的呢？最直接的答案就是transform会直接使用硬件加速，在GPU中运行，绕开了软件渲染。之前学习 flash 的时候，就知道动画是由一帧一帧的图片组成，在浏览器中也是如此。我们首先看一下，浏览器每一帧都做了什么。JavaScript：JavaScript 实现动画效果，DOM 元素操作等。Style（计算样式）：确定每个 DOM 元素应该应用什么 CSS 规则。Layout（布局）：计算每个 DOM 元素在最终屏幕上显示的大小和位置。由于 web 页面的元素布局是相对的，所以其中任意一个元素的位置发生变化，都会联动的引起其他元素发生变化，这个过程叫 reflow。Paint（绘制）：在多个层上绘制 DOM 元素的的文字、颜色、图像、边框和阴影等。Composite（渲染层合并）：按照合理的顺序合并图层然后显示到屏幕上。浏览器在获取 render tree（详细知识可以查看深入了解浏览器重排和重绘）后，渲染树中包含了大量的渲染元素，每一个渲染元素会被分到一个图层中，每个图层又会被加载到 GPU 形成渲染纹理。这里的秘诀就在于通过transform的层会使用GPU渲染，因此不需要重绘，这一点非常类似 3D 绘图功能，最终这些使用 transform 的图层都会由独立的合成器进程进行处理。过程如下：render tree -> 渲染元素 -> 图层 -> GPU 渲染 -> 浏览器复合图层 -> 生成最终的屏幕图像。注意：chrome devtools 中可以开启 Rendering 中的 Layer borders 查看图层纹理。其中黄色边框表示该元素有 3d 变换，表示放到一个新的复合层（composited layer）中渲染，蓝色栅格表示正常的 render layer。在文章开始给出的例子中，CSS的transform在GPU直接创建一个新的层。我们也可以开启 Layer borders，（这个选项可以帮助我们查看哪些是单独的层，开启这个选项以后单独的层会具有一个橙色的边框。）可以观察到，使用 transform: translate 动画的元素，外围有一个黄色的边框，可知其为复合层。在 GPU 渲染的过程中，一些元素会因为符合了某些规则，而被提升为独立的层（黄色边框部分），一旦独立出来，就不会影响其它 DOM 的布局，所以我们可以利用这些规则，将经常变换的 DOM 主动提升到独立的层，那么在浏览器的一帧运行中，就可以减少 Layout 和 Paint 的时间了。源代码 css3 animation 创建独立图层哪些规则能让浏览器主动帮我们创建独立的层呢？3D 或者透视变换（perspective，transform） 的 CSS 属性。使用加速视频解码的 video 元素。拥有 3D（WebGL） 上下文或者加速 2D 上下文的 canvas 元素。混合插件（Flash)。对自己的 opacity 做 CSS 动画或使用一个动画 webkit 变换的元素。拥有加速 CSS 过滤器的元素。元素有一个包含复合层的后代节点(换句话说，就是一个元素拥有一个子元素，该子元素在自己的层里)。元素有一个兄弟元素在复合图层渲染，并且该兄弟元素的 z-index 较小，那这个元素也会被应用到复合图层。开启 GPU 加速CSS的animation、tranform、transition并不会自动开启GPU加速，而是通过浏览器的缓慢的软件渲染引擎来实现执行，那么我们怎么才能实现GPU加速呢，很多浏览器提供了某些触发该模式的规则。比如使用 translate3d() rotate3d() scale3d() 这几个方法，我们就可以使用ＧＰＵ加速了。如下几个css属性可以触发硬件加速：transform（ translate3d、translateZ(0)等）opacityfilter（滤镜：drop-shadow()、opacity()，函数与已有的box-shadow、opacity属性很相似；不同之处在于，通过滤镜，一些浏览器为了更好的性能会提供硬件加速）will-change：哪一个属性即将发生变化，进而进行优化。因此为了页面更加流畅，高性能的动画，我们可以使用GPU来处理。如果有一些元素不需要用到上述属性，但是需要触发硬件加速效果，例如：某些情况下，我们并不想要对元素应用３Ｄ变换的效果，却还想要实现ＧＰＵ加速，可以使用一些小技巧来诱导浏览器开启硬件加速。transform: translateZ(0)这个声明就是可以触发桌面端和移动端的GPU加速，这是一个非常有效的方式（包含所有的浏览器前缀）：.element { -webkit-transform: translateZ(0); -moz-transform: translateZ(0); -ms-transform: translateZ(0); -o-transform: translateZ(0); transform: translateZ(0); /**或者**/ transform: rotateZ(360deg); transform: translate3d(0, 0, 0); } 使用硬件加速需要注意的地方Memory大部分重要的问题都是关于内存。GPU处理过多的内容会导致内存问题。这在移动端和移动端浏览器会导致崩溃。因此，通常不会对所有的元素使用硬件加速。（过多地开启硬件加速可能会耗费较多的内存，因此什么时候开启硬件加速，给多少元素开启硬件加速，需要用测试结果说话。）Font rendering在GPU渲染字体会导致抗锯齿无效。这是因为GPU和CPU的算法不同。因此如果你不在动画结束的时候关闭硬件加速，会产生字体模糊。The Near Future有必要使用transform hack的地方是提高性能。浏览器自身也提供了优化的功能，这也就是will-change属性。这个功能允许你告诉浏览器这个属性会发生变化，因此浏览器会在开始之前对其进行优化。这里有一个例子：.example { will-change: transform; }遗憾的是，并不是所有浏览器都支持这个功能。GPU优点GPU渲染可以提高动画性能GPU渲染会提高动画的渲染帧数
Webpack和ViteWebpack和vite都是现代化打包工具为什么vite快vite是基于esbuild预构建依赖,而esbuild是go语言编写,因为go语言的效率更高,所以vite比用js编写的打包器快10倍webpack原理vite原理图webpack过程分析依赖=>编译打包=>交给本地服务器进行渲染.首先分析各个模块之间的依赖,然后进行打包,在启动webpack-dev-server请求时,直接显式打包结果.webpack打包之后存在的问题随着模块的增多，会造成打出的 bundle 体积过大，进而会造成热更新速度明显拖慢。vite启动服务器=>请求模块时按需动态编译显示,是先启动开发服务器,请求某个模块时,再对该模块进行实时编译,因为现代浏览器本身支持es-Module,所以会自动向依赖的Module发出请求.所以vite就将环境下的模块文件作为浏览器执行文件,而不是像webpack进行打包后交给本地服务器为什么快分析了webpack和vite的打包方式后,就明白了了vite为什么比webpack打包快,因为在确定时候不需要打包,所以不用分析与模块之间的依赖关系,不用进行编译,这种方式就类似于我们在请求某个ui框架的时候,可以对其进行按需加载,当浏览器请求某个模块时,在根据需要对模块的内容进行编译.按需动态编译可缩短编译时间,当项目越复杂,模块越多的情况下,vite明显优于webpakc热更新方面,效率更高,当改动了某个模块时,也只用让浏览器重新请求该模块,不需要像webpack那样将模块依赖的模块全部编译一次
1.安装nvmhttps://github.com/coreybutler/nvm-windows/releases下载exe程序即可管理员权限打开cmdnvm ls 命令查看本地已有版本 nvm ls // 查看已安装node版本 nvm install vXX // 安装对应vXX版本的node nvm uninstall vXX // 卸载对应vXX版本的node nvm use xxx // 选择使用XXX版本
性能双向响应原理由Object.defindProperty改为基于ES6的proxy使其粒度更大,速度更快,且消除了之前存在的警告重写了Vdom,突破了Vdom的性能瓶颈进行了模板编译的优化进行了更加高效的组件初始化tree-shaking的支持支持了tree-shaking(剪枝):像修建输液一样把不需要的东西给修剪掉,使vue3的体积更小需要的模块才会打入到包里,优化后的Vue3.0的打包体积只有原来的一半.像keep-alive,transition甚至v-for等功能都可以按需引入Composition APIcomposition-api是一个vue3中新增的功能,是比mixin更强大的存在composition-api可以提高代码逻辑的可复用性,从而实现与模板的无关性;同时使代码的可压缩性更强,另外把Reactivity模块独立开来,意味着vue3.0的响应式模块可以与其他框架相组合Fragments不再限制teplate只有一个根节点render函数也可以返回数组了Better TypeScript Support更好的类型推导,使得Vue3吧TypeScript支持的更好Custom Renderer API实现用DOM的方式进行WebGL编程