-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 77 KB
/
content.json
1
{"meta":{"title":"个人博客","subtitle":"","description":"","author":"ZYX&LZB","url":"https://nodisappear.github.io","root":"/"},"pages":[{"title":"","date":"2023-10-16T16:44:11.299Z","updated":"2023-10-16T16:22:44.783Z","comments":true,"path":"static/script.js","permalink":"https://nodisappear.github.io/static/script.js","excerpt":"","text":"const { readdir } = require('node:fs/promises'); async function listDir() { try { const files = await readdir(__dirname); const list = [] for (const file of files) { if(/.*[(jpg)|(png)|(jpeg)]+$/g.test(file.toLowerCase())) { list.push(\"static/\" + file) } } return list; } catch (err) { console.error(err); } } listDir().then((res) => { console.log(res); });"},{"title":"分类","date":"2021-09-06T06:00:15.000Z","updated":"2022-02-19T08:57:58.273Z","comments":true,"path":"categories/index.html","permalink":"https://nodisappear.github.io/categories/index.html","excerpt":"","text":""},{"title":"关于","date":"2021-09-07T12:51:56.000Z","updated":"2022-02-19T08:57:58.272Z","comments":true,"path":"about/index.html","permalink":"https://nodisappear.github.io/about/index.html","excerpt":"","text":""},{"title":"标签","date":"2021-09-06T06:01:08.000Z","updated":"2022-02-19T08:57:58.273Z","comments":true,"path":"tags/index.html","permalink":"https://nodisappear.github.io/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"js-log-report源码解析","slug":"js-log-report源码解析","date":"2022-06-12T15:07:00.000Z","updated":"2023-10-16T15:41:30.637Z","comments":true,"path":"4816ed5f.html","link":"","permalink":"https://nodisappear.github.io/4816ed5f.html","excerpt":"","text":"这个库的想法很简单,就是 window.onerror 监听浏览器控制台的报错事件,把报错信息【控制台输出错误信息,以及出错的文件,行号,堆栈信息】通过ajax直接上传到服务器端,解决项目现场,移动端调试比较难的问题。 github地址:【 https://github.com/ecitlm/js-log-report 】 1. js-log-report的使用方法1.1 包含哪些报错信息123456789101112var defaults = { ua: window.navigator.userAgent, browser: '', os: '', osVersion: '', errUrl: window.location.href, msg: '', // 错误的具体信息 url: '', // 错误所在的url line: '', // 错误所在的行 col: '', // 错误所在的列 error: '' // 具体的error对象} 1.2 上传接口形式上报错误日志的ajax接口形式: 接口的url自定义,post请求, 请求数据以 json 形式放在 body 中。 1.3 数据库设置服务端需要执行一下下面的SQL语句: 123456789101112131415161718DROP TABLE IF EXISTS `j_log`;CREATE TABLE `j_log` ( `id` int(10) NOT NULL AUTO_INCREMENT COMMENT 'id号', `os_version` char(10) DEFAULT NULL COMMENT '系统版本号', `msg` varchar(255) DEFAULT NULL COMMENT '错误信息', `error_url` varchar(255) DEFAULT NULL COMMENT '错误所在的url', `line` int(10) DEFAULT NULL COMMENT '错误所在的行', `col` int(10) DEFAULT NULL COMMENT '错误所在的列', `error` varchar(255) DEFAULT NULL COMMENT '具体的error对象', `url` varchar(255) DEFAULT NULL, `browser` varchar(255) DEFAULT NULL COMMENT '浏览器类型', `product_name` char(255) CHARACTER SET utf8 DEFAULT '' COMMENT '产品名称', `error_time` char(20) DEFAULT NULL COMMENT '时间戳', `os` char(10) DEFAULT NULL COMMENT '系统类型', `extend` varchar(255) DEFAULT NULL COMMENT '业务扩展字段、保存JSON字符串', `ua` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`)) ENGINE=MyISAM AUTO_INCREMENT=55 DEFAULT CHARSET=utf8; 2. js-log-report的主要流程(1) 取window.onerror中的报错信息, 这里会把3层堆栈报错信息上传 123456789101112131415161718192021222324252627282930window.onerror = function (msg, url, line, col, error) { // 采用异步的方式,避免阻塞 setTimeout(function () { // 不一定所有浏览器都支持col参数,如果不支持就用window.event来兼容 col = col || (window.event && window.event.errorCharacter) || 0 defaults.url = url defaults.line = line defaults.col = col if (error && error.stack) { // 如果浏览器有堆栈信息,直接使用 defaults.msg = error.stack.toString() } else if (arguments.callee) { // 尝试通过callee拿堆栈信息 var ext = [] var fn = arguments.callee.caller var floor = 3 // 这里只拿三层堆栈信息 while (fn && (--floor > 0)) { ext.push(fn.toString()) if (fn === fn.caller) { break// 如果有环 } fn = fn.caller } defaults.msg = ext.join(',') } // 合并上报的数据,包括默认上报的数据和自定义上报的数据 var reportData = extendObj(params.data || {}, defaults) console.log(reportData) ......} (2)用浏览器自带的 XMLHttpRequest 将报错数据上报, 不用依赖任何第三方库 2.1 自己封装的ajax库自己封装的ajax库, 在浏览器上运行,主要用到了浏览器的 XMLHttpRequest API,从中可以看到XMLHttpRequest的用法。 12345678910111213141516171819202122232425262728293031 function ajax (options) { options = options || {} options.type = (options.type || 'GET').toUpperCase() options.dataType = options.dataType || 'json' var params = formatParams(options.data) if (window.XMLHttpRequest) { var xhr = new XMLHttpRequest() } else { var xhr = new ActiveXObject('Microsoft.XMLHTTP') } xhr.onreadystatechange = function () { if (xhr.readyState == 4) { var status = xhr.status if (status >= 200 && status < 300) { options.success && options.success(xhr.responseText, xhr.responseXML) } else { options.fail && options.fail(status) } } } if (options.type == 'GET') { xhr.open('GET', options.url + '?' + params, true) xhr.send(null) } else if (options.type == 'POST') { xhr.open('POST', options.url, true) // 设置表单提交时的内容类型 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') xhr.send(params) } }","categories":[{"name":"技术","slug":"技术","permalink":"https://nodisappear.github.io/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"源码解析","slug":"源码解析","permalink":"https://nodisappear.github.io/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/"}],"author":"李泽滨"},{"title":"javascript事件循环机制","slug":"javascript事件循环机制","date":"2022-02-20T17:19:00.000Z","updated":"2023-10-16T15:41:04.512Z","comments":true,"path":"182c94d6.html","link":"","permalink":"https://nodisappear.github.io/182c94d6.html","excerpt":"","text":"1.JavaScript是单线程语言Javascript语言是一种单线程语言,所有任务都在一个线程上完成。单线程如果遇到某个任务比较耗时,比如涉及很多I/O操作:读取文件、HTTP请求、SQL查询等,线程的大部分运行时间都会在空等I/O操作的返回结果。Event Loop就是为了解决单线程语言的这个问题。 2.事件循环Event Loop为了解决JavaScript这种单线程语言带来的堵塞问题,Javascript程序会在程序中设置两个线程:一个负责程序本身的运行,称为主线程;另一个负责主线程与其他进程(主要是各种I/O操作)的通信,被称为”Event Loop线程”。Event Loop线程中主线程从任务队列中读取事件,这个过程是循环不断得,所以整个的这种运行机制又被称为事件循环。在事件循环中,异步事件并不会放到当前任务执行队列,而是会被挂起,放入另外一个回调队列。当前的任务队列执行结束以后,JavaScript引擎回去检查回调队列中是否有等待执行的任务【Perform a microtask checkpoint, 即执行微任务检查点】,若有会把第一个任务加入执行队列,然后不断的重复这个过程。JavaScrip是单线程,因此同一个执行队列产生的微任务总是会在宏任务之前被执行。 2.1 宏任务与微任务宏任务必然是在微任务之后才执行。宏任务: setTimeout setInterval I/O setImmediate[在浏览器中是微任务,在Node中是宏任务] requestAnimationFrame[在浏览器中是宏任务,在Node中是微任务] 微任务: Promise.then / catch / finally / async/await本质上还是基于Promise的一些封装 process.nextTick[在Node中是微任务] MutationObserver[在浏览器中是微任务] 3. javascript执行上下文和执行栈JavaScript 中有三种执行上下文类型: 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。 Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。 执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。 4. 示例12345678910111213141516171819202122console.log(1);setTimeout(() => {console.log(2)}, 1000)async function fn() { console.log(3) setTimeout(() => {console.log(4)}, 20) return Promise.resolve()}async function run() { console.log(5) await fn() console.log(6)}run()for(let i=0; i<50000000000; i++) {}setTimeout(() => { console.log(7) new Promise(resolve => { console.log(8) resolve() }).then(() => {console.log(9)})}, 0)console.log(10)","categories":[{"name":"技术","slug":"技术","permalink":"https://nodisappear.github.io/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"编程语言","slug":"编程语言","permalink":"https://nodisappear.github.io/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"}],"author":"李泽滨"},{"title":"core-decorator源码解析","slug":"core-decorator源码解析","date":"2021-12-15T03:21:00.000Z","updated":"2023-10-16T15:40:09.283Z","comments":true,"path":"b483b17d.html","link":"","permalink":"https://nodisappear.github.io/b483b17d.html","excerpt":"","text":"一. core-decorator装饰器库core-decorator是封装了一些常用功能的装饰器库,采用了es6的装饰器语法,可以装饰类和类的方法。 二. es6的装饰器语法装饰器是一种函数,写成 @ + 函数名,它可以放在类和类方法的定义前面。 1. 默认参数列表(1) 装饰类 默认参数列表中包含一个参数,即要装饰的目标类; (2) 装饰类的方法 默认参数列表中三个参数:第一个是要装饰的类的实例target【但这个时候实例还未生成,所以实际装饰的是类的原型】; 第二个key是要装饰的属性名;第三个参数descriptor是属性的描述对象【描述对象中包含的属性有:’value’, ‘initializer’, ‘get’, ‘set’, ‘writable’, ‘enumerable’】 三. 主要流程以防抖的装饰器debounce为例 1. 封装一个装饰器的功能函数handleDescriptor这里metaFor只是绑定在this对象上、保存定时器ID的对象 12345678910111213141516171819202122232425262728293031323334import { decorate, metaFor, internalDeprecation } from './private/utils';const DEFAULT_TIMEOUT = 300;function handleDescriptor(target, key, descriptor, [wait = DEFAULT_TIMEOUT, immediate = false]) { const callback = descriptor.value; if (typeof callback !== 'function') { throw new SyntaxError('Only functions can be debounced'); } return { ...descriptor, value() { const { debounceTimeoutIds } = metaFor(this); const timeout = debounceTimeoutIds[key]; const callNow = immediate && !timeout; const args = arguments; clearTimeout(timeout); debounceTimeoutIds[key] = setTimeout(() => { delete debounceTimeoutIds[key]; if (!immediate) { callback.apply(this, args); } }, wait); if (callNow) { callback.apply(this, args); } } };} 2. 对外暴露整个装饰器将功能函数handleDescriptor、默认参数列表作为参数传递给装饰器的工具函数decoratoe, 并对外暴露。 这里internalDeprecation只是一个在调用时显示是否deprecated的工具函数。 1234export default function debounce(...args) { internalDeprecation('@debounce is deprecated and will be removed shortly. Use @debounce from lodash-decorators.\\n\\n https://www.npmjs.com/package/lodash-decorators'); return decorate(handleDescriptor, args);} 3. 装饰器decorate函数的主要逻辑decorate主要对是否传递额外参数做了一下判断,其中怎么参数涉及的过程还是略微复杂的, 不明白可以参照 js-learning/decortor/core-decorator-test.js下的实际过程加深了解。 123456789101112131415161718192021222324252627export function isDescriptor(desc) { if (!desc || !desc.hasOwnProperty) { return false; } const keys = ['value', 'initializer', 'get', 'set']; for (let i = 0, l = keys.length; i < l; i++) { if (desc.hasOwnProperty(keys[i])) { return true; } } return false;}export function decorate(handleDescriptor, entryArgs) { if (isDescriptor(entryArgs[entryArgs.length - 1])) { // 没有额外参数的情况下,entryArgs就是默认参数列表 [target, key descriptor] return handleDescriptor(...entryArgs, []); } else { return function () { // Array.prototype.slice.call(arguments) 这个是编译时的最外层调用的默认参数列表 [target, key descriptor] return handleDescriptor(...Array.prototype.slice.call(arguments), entryArgs); }; }} 参考文献[1] core-decorator的github地址 [2] ECMAScript6入门","categories":[{"name":"技术","slug":"技术","permalink":"https://nodisappear.github.io/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"源码解析","slug":"源码解析","permalink":"https://nodisappear.github.io/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/"}],"author":"李泽滨"},{"title":"javascript深浅拷贝","slug":"javascript深浅拷贝","date":"2021-12-04T02:32:31.000Z","updated":"2023-10-16T15:38:58.718Z","comments":true,"path":"ad2c1b4b.html","link":"","permalink":"https://nodisappear.github.io/ad2c1b4b.html","excerpt":"","text":"生成指定深度和广度的对象12345678910111213141516function createData(deep, breadth) { var data = {}; var temp = data; for (var i = 0; i < deep; i++) { temp = temp["data"] = {}; for (var j = 0; j < breadth; j++) { temp[j] = j; } } return data;}// 2层深度,每层有3个数据createData(1, 3); 浅拷贝 —— 一层拷贝 遍历属性1234567891011121314function shallowClone(source) { var target = {}; // key in Obj:判断自身或原型链上是否存在某个属性 // for key in Obj: 遍历自身以及原型链上enumerable为true的可枚举属性,结合hasOwnProperty可以过滤掉原型链上的属性 // Object.keys(Obj): 遍历自身的可枚举属性 // Object.getOwnPropertyNames(Obj): 遍历自身的所有属性 for (var i in source) { if (source.hasOwnProperty(i)) { target[i] = source[i]; } } return target;} Object.assign(target, …sources) { …source } lodash.clone12let _ = require('lodash');_.clone(source); 深拷贝 —— 无限层级拷贝 扩展浅拷贝 [不推荐] 1234567891011121314151617181920212223// 存在问题:1.未直接检验参数是否为对象;2.判断属性是否为对象的逻辑不严谨,未考虑null;3.未兼容多种数据格式,如Map、Set等;...function deepClone(source) { var target = {}; for (var i in source) { if (source.hasOwnProperty(i)) { if (typeof source[i] === "object") { target[i] = deepClone(source[i]); } else { target[i] = source[i]; } } } return target;}// 递归,栈溢出deepClone(createData(10000)); // Maximum call stack size exceeded// 循环引用,栈溢出let data = {};data.data = data;deepClone(data); // Maximum call stack size exceeded JSON.parse(JSON.stringify(source)) [适用于JSON对象, 不保留引用,会爆栈] 构造函数 + 集合 [可保留引用,会爆栈]123456789101112131415function deepClone(source, hash = new WeakMap()) { if (source === null) return source; if (source instanceof Date) return new Date(source); if (source instanceof RegExp) return new RegExp(source); if (typeof source !== "object") return source; if (hash.get(source)) return hash.get(source); let target = new source.constructor(); hash.set(source, target); for (let key in source) { if (source.hasOwnProperty(key)) { target[key] = deepClone(source[key], hash); } } return target;} lodash.cloneDeep12let _ = require('lodash');_.cloneDeep(source); deepmerge12const merge = require('deepmerge');merge(target, source); 扩展内容1. 判断对象123456function isObject(x) { // 与 typeof x 相比,能够区分null、array: typeof null === 'object'; typeof [1,2,3] === 'object' // 与 x.toString() 相比,调用的不是Object对象实例重写的方法而是Object原型对象的方法: [1,2,3].toString() === '1,2,3'; Object.prototype.toString.call([1,2,3]) === '[object Array]' // 不能准确判断自定义对象: function func() {}; Object.prototype.toString.call(new func()) === '[object Object]'; return Object.prototype.toString.call(x) === "[object Object]";} 2. 判断类型1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889function type(x, strict = false) { // 将strict转换为布尔型 strict = !!strict; // 解决 typeof null === 'object' 无法判断的问题 if (x === null) { return "null"; } const t = typeof x; // typeof NaN === 'number' if (strict && t === Number && isNaN(x)) { return "NaN"; } // typeof 1 === 'number' // typeof '1' === 'string' // typeof false === 'boolean' // typeof undefined === 'undefined' // typeof Symbol() === 'symbol' // typeof function (){} === 'function' if (t !== "object") { return t; } let cls, clsLow; try { cls = Object.prototype.toString.call(x).slice(8, -1); clsLow = cls.toLowercase(); } catch (e) { // ie,new ActiveXObject(String)报错 return "object"; } if (clsLow !== "object") { if (strict) { // Object.prototype.toString.call(new Number(NaN)) === '[object Number]' if (clsLow === "number" && isNaN(clsLow)) { return "NaN"; } // Object.prototype.toString.call(new Number()) === '[object Number]'; // Object.prototype.toString.call(new Boolean()) === '[object Boolean]'; // Object.prototype.toString.call(new String()) === '[object String]'; if (clsLow === "number" || clsLow === "boolean" || clsLow === "string") { return cls; } } // Object.prototype.toString.call([]) === '[object Array]'; // Object.prototype.toString.call(new Array()) === '[object Array]' // Object.prototype.toString.call(new Set()) === '[object Set]' // Object.prototype.toString.call(new WeakSet()) === '[object WeakSet]' // Object.prototype.toString.call(new Map()) === '[object Map]' // Object.prototype.toString.call(new WeakMap()) === '[object WeakMap]' // Object.prototype.toString.call(new WeakRef({})) === '[object WeakRef]' return clsLow; } // Object.prototype.toString.call({}) === '[object Object]'; constructor: Object // Object.prototype.toString.call(new Object()) === '[object Object]'; constructor: Object if (x.constructor == Object) { return clsLow; } try { // Object.prototype.toString.call(Object.create(null)) === '[object Object]'; constructor: undefined // Object.getPrototypeOf(Object.create(null)) === null // x.__prototype__ === null 应用于早期firefox if (Object.getPrototypeOf(x) === null || x.__prototype__ === null) { return "object"; } } catch (e) { // ie,无Object.getPrototypeOf会报错 } try { // Object.prototype.toString.call(new function(){}) === '[object Object]'; constructor: f(){}() const cname = x.constructor.name; if(typeof cname === 'string') { return cname; } } catch (e) { // 无constructor } // function A() {}; A.prototype.constructor = null; new A // new A instanceof A === true return 'unknown';} 3. 通过 “循环+栈” 破解 “递归爆栈” (1) 深度优先遍历, 用栈做中间节点缓存 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465function deepLoopClone(obj) { if(obj === null || typeof obj !== 'object') { return obj } // 保留不同属性之间存在的引用关系 const uniqueList = []; const root = Array.isArray(obj) ? [] : {}; const stack = [ { parent: root, key: undefined, value: obj, } ]; while(stack.length) { const node = stack.pop(); const { parent, key, value } = node; let res = parent; if (typeof key !== 'undefined') { res = parent[key] = Array.isArray(value) ? [] : {}; } let uniqueData = find(uniqueList, value); if(uniqueData) { parent[key] = uniqueData.target; break; } uniqueList.push({ source: value, target: res }); for(let prop in value) { if (value.hasOwnProperty(prop)) { if (prop && typeof value[prop] === 'object') { stack.push({ parent: res, key: prop, value: value[prop], }); } else { res[prop] = value[prop]; } } } } return root;}// 查找对象function find(arr, item) { for(let i = 0; i < arr.length; i++) { if (arr[i].source === item) { return arr[i]; } } return null;}  (2) 广度优先遍历, 用队列做中间节点缓存 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364function rangeLoopClone(obj) { if(obj === null || typeof obj !== 'object') { return obj; } // 保留不同属性之间存在的引用关系 const uniqueList = []; const root = Array.isArray(obj) ? [] : {}; const queue = [ { parent: root, key: undefined, value: obj, } ]; while(queue.length) { const node = queue.shift(); const { parent, key, value } = node; if (typeof key !== 'undefined') { res = parent[key] = Array.isArray(value) ? [] : {}; } let uniqueData = find(uniqueList, value); if(uniqueData) { parent[key] = uniqueData.target; break; } uniqueList.push({ source: value, target: res }); for(let prop in value) { if (value.hasOwnProperty(prop)) { if (prop && typeof value[prop] === 'object') { queue.push({ parent: res, key: prop, value: value[prop], }); } else { res[prop] = value[prop]; } } } } return root;}// 查找对象function find(arr, item) { for(let i = 0; i < arr.length; i++) { if (arr[i].source === item) { return arr[i]; } } return null;} 4. 循环引用 (1) 常见类型 对象之间相互引用1234let obj1 = { name: '1' };let obj2 = { name: '2' };obj1.obj = obj2;obj2.obj = obj1; 对象的属性引用对象本身 1234567// 直接引用最外层的对象let obj = { name: '1' };obj.child = obj;// 引用对象的部分属性let obj = { name: '1', child: {} };obj.child.child = obj.child;  (2) 判断循环引用 123456789101112131415161718192021222324252627282930const isCyclic = (obj) => { // 用Set数据类型存储检测过的对象 let stackSet = new Set(); let detected = false; const detect = (obj) => { if(obj && typeof obj != 'object') { return; } if(stackSet.has(obj)) { return detected = true; } stackSet.add(obj); for(let key in obj) { if(obj.hasOwnProperty(key)) { detect(obj[key]); } } // 平级检测完成之后,将当前对象删除 stackSet.delete(obj); }; detect(obj); return detected;};  (3) 用JSON.stringify输出有循环引用的对象 12345678910111213141516let obj = { };obj.child = obj;// JSON.stringify()内部做了循环引用的检测JSON.stringify(obj); // Uncaught TypeError: Converting circular structure to JSONlet cache = [];JSON.stringify(obj, (key,value)=>{ if(value && typeof value === 'object') { if(cache.indexOf(value) !== -1) { return; } cache.push(value); } return value;});cache = null; 参考链接 深拷贝的终极探索(99%的人都不知道) 用 Object.prototype.toString.call(obj)检测对象类型原因分析 详解 forin,Object.keys 和 Object.getOwnPropertyNames 的区别 如何优雅地嗅探”对象“是否存在”环“? Javascript中的尾递归及其优化 尾调用和尾递归 利用深度/广度优先遍历手动实现JavaScript对象的深度拷贝","categories":[{"name":"编程语言","slug":"编程语言","permalink":"https://nodisappear.github.io/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"}],"tags":[{"name":"知识点","slug":"知识点","permalink":"https://nodisappear.github.io/tags/%E7%9F%A5%E8%AF%86%E7%82%B9/"}],"author":"张雅娴"},{"title":"读《JavaScript设计模式核心原理与应用实践》","slug":"读《JavaScript设计模式核心原理与应用实践》","date":"2021-11-03T08:18:10.000Z","updated":"2023-10-16T15:39:12.898Z","comments":true,"path":"806a46bc.html","link":"","permalink":"https://nodisappear.github.io/806a46bc.html","excerpt":"","text":"掘金小册——JavaScript 设计模式核心原理与应用实践 开篇:前端工程师的成长论一. 软件工程师的核心竞争力 驾驭技术的能力 能用健壮的代码解决具体问题 能用抽象的思维应对复杂系统 能用工程化思想规划大型业务 设计模式的“道”与“术”一. 设计模式的指导理论 SOLID 设计原则 *单一职责 [单一功能] *开放封闭 —— 对扩展开放,对修改封闭 类、模块、函数等软件实体可以扩展,但不可以修改 李式置换 [里式替换] 接口独立 [接口隔离] 依赖导致 [依赖反转] 二. 设计模式的核心思想 封装变化  观察整个逻辑里面的变与不变并分离它们,使变化的部分灵活而不变的部分稳定 三. 23 种设计模式 创建型 —— 封装创建对象过程中的变化 单例模式 原型模式 构造器模式 工厂模式 抽象工厂模式  结构型 —— 封装对象之间组合方式的变化 桥接模式 外观模式 组合模式 装饰器模式 适配器模式 代理模式 享元模式  行为型 —— 封装对象千变万化的行为 迭代器模式 解释器模式 观察者模式 中介者模式 访问者模式 状态模式 备忘录模式 策略模式 模板方法模式 职责链模式 命令模式 创建型:工厂模式 —— 区分“变与不变”一. 构造器模式 用构造函数初始化对象 —— 抽象不同对象实例之间的变与不变 123456789// 将赋值过程封装,确保每个对象具备 共性function User(name, age, career) { this.name = name; this.age = age; this.career = career;}// 将取值操作开放,确保每个对象具备 个性const user = new User("李雷", 25, "coder"); 二. 简单工厂模式 将创建对象的过程单独封装 —— 抽象不同构造函数之间的变与不变 1234567891011121314151617181920function User(name, age, career, work) { this.name = name; this.age = age; this.career = career; this.work = work;}// 将 承载共性的构造函数 和 承载个性的逻辑判断 写入同一函数function Factory(name, age, career) { let work; switch (career) { case "boss": work = ["喝茶", "看报", "见客户"]; break; case "coder": work = ["写代码", "写系分", "修Bug"]; break; } return new User("李雷", 25, "coder");}  重要提示: 在写了大量构造函数、调用了大量 new 的情况下,就应该思考是不是可以用工厂模式重构代码了! 三. 抽象工厂模式 围绕一个超级工厂创建其他工厂 —— 遵循“开放封闭”设计原则  将一个复杂场景中不同的类按性质划分为 4 个关键角色[1 - 4]: 0. 超级工厂:拥有多个抽象工厂的系统 【电子厂】 抽象工厂:抽象类,用于声明最终目标产品的共性,每个抽象工厂对应的一类产品称为“产品族” 【手机厂,电脑厂,…】 具体工厂:继承自抽象工厂,用于生成产品族里的一类具体产品 【智能手机厂,非智能手机厂,…】 抽象产品:抽象类,用于声明具体产品所依赖的细粒度产品的共性 【操作系统厂,硬件厂,…】 具体产品: 继承自抽象产品,用于生成具体产品所依赖的细粒度产品 【安卓操作系统厂/苹果操作系统厂,小米硬件厂/高通硬件厂,…】 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859// 抽象工厂,定义手机的共性class MobilePhoneFactory { createOS() { // 提供操作系统的接口 throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!"); } createHardWare() { // 提供硬件的接口 throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!"); }}// 具体工厂,用于生成手机实例class FakeStarFactory extends MobilePhoneFactory { createOS() { // 提供操作系统实例 return new AndroidOS(); } createHardWare() { // 提供硬件实例 return new QualcommHardWare(); }}// 抽象产品,定义手机操作系统的共性class OS { controlHardWare() { throw new Error("抽象产品方法不允许直接调用,你需要将我重写!"); }}// 具体产品,定义具体的手机操作系统class AndroidOS extends OS { controlHardWare() { console.log("操作系统:我会用安卓的方式去操作硬件"); }}// 抽象产品,定义手机硬件的共性class HardWare { operateByOrder() { throw new Error("抽象产品方法不允许直接调用,你需要将我重写!"); }}// 具体产品:定义具体的手机硬件class QualcommHardWare extends HardWare { operateByOrder() { console.log("硬件:我会用高通的方式去运转"); }}// 生产一台拥有安卓操作系统和高通硬件的手机const myMobilePhone = new FakeStarFactory(); // 创建手机实例const myOS = myMobilePhone.createOS(); // 添加操作系统const myHardWare = myMobilePhone.createHardWare(); // 添加硬件myOS.controlHardWare(); // 启动操作系统myHardWare.operateByOrder(); // 启动硬件// 扩展具体工厂, 生产一款新的手机class newStarFactory extends MobilePhoneFactory { createOS() {} createHardWare() {}}  重要提示: 1. 抽象工厂和简单工厂的共同之处是都基于“封装变化”的思想去分离一个系统中变与不变的部分,不同之处是应用的场景复杂度不同,简单工厂的处理对象是不苛求可扩展性的简单类,抽象工厂的处理对象是存在各种扩展可能性的能进一步划分的复杂类。 2. 抽象工厂目前在 JS 中应用得并不广泛,需要留意三点:(1) 学会用 ES6 模拟 Java 中的抽象类;(2) 了解抽象工厂模式中四个角色的定位和作用;(3) 理解“开放封闭”设计原则,知道它的好用之处和执行之必要性。 创建型:单例模式 —— Vuex 的数据管理哲学一. 单例模式的实现思路 保证一个类仅有一个实例,并提供一个访问它的全局访问点 —— 不管尝试创建多少次,都只返回第一次创建的实例  构造函数需要具备判断自己是否已经创建过一个实例的能力 12345678910111213141516171819202122232425262728293031// 1.判断逻辑写在静态方法中class SingleDog { constructor() { this.instance = null; } static getInstance() { if (!this.instance) { this.instance = new SingleDog(); } return this.instance; }}const s1 = SingleDog.getInstance();const s2 = SingleDog.getInstance();// 2.判断逻辑写在闭包中function SingleDog() {}const getInstance = (function () { let instance = null; return function () { if (!instance) { instance = new SingleDog(); } return instance; };})();const s1 = getInstance();const s2 = getInstance();// s1和s2都指向唯一的实例s1 === s2; // true 二. 生产实践:Vuex 中的单例模式理解 Vuex 中的 Store 引入 Vuex 插件 1234import Vue from "vue";import Vuex from "vuex";Vue.use(Vuex); Vue.use 源码 123456789101112131415161718192021222324252627282930313233// 截取参数function toArray(list: any, start?: number): Array<any> { start = start || 0; let i = list.length - start; const ret: Array<any> = new Array(i); while (i--) { ret[i] = list[i + start]; } return ret;}// 注册插件export function initUse(Vue: GlobalAPI) { Vue.use = function (plugin: Function | Object) { const installedPlugins = this._installedPlugins || (this._installedPlugins = []); // 已安装插件列表 if (installedPlugins.indexOf(plugin) > -1) { // 防止重复注册 return this; } const args = toArray(arguments, 1); args.unshift(this); if (typeof plugin.install === "function") { // 如果插件是一个对象,必须提供install方法 plugin.install.apply(plugin, args); } else if (typeof plugin === "function") { // 如果插件是一个函数,它会被直接当作install方法 plugin.apply(null, args); } installedPlugins.push(plugin); return this; };} Vuex install 源码 123456789101112131415161718192021222324252627let vue; // instancefunction install(_Vue) { if (Vue && _Vue === Vue) { // 判断传入的Vue实例对象是否已经被install过Vuex插件 if (__DEV__) { console.error( "[vuex] already installed. Vue.use(Vuex) should be called only once." ); } return; } Vue = _Vue; // 若没有,为该Vue实例对象install一个唯一的Vuex applyMixin(Vue); // 将Vuex的初始化逻辑写进Vue的钩子函数里}// applyMixin(Vue)function vuexInit() { const options = this.$options; // 当前Vue实例的初始化选项 if (options.store) { // 根实例有store this.$store = typeof options.store === "function" ? options.store() : options.store; } else if (options.parent && options.parent.$store) { // 根实例没有store,就找父节点的store this.$store = options.parent.$store; }}Vue.mixin({ beforeCreate: vuexInit }); // 全局混入 创建 store 123456const store = new Vuex.Store({ state: {}, mutations: {}, actions: {}, modules: {},}); 将 store 注入到 Vue 实例中 1234new Vue({ el: "#app", store,}); 创建型:原型模式 —— 谈 Prototype 无小事一. 以类为中心的语言和以原型为中心的语言 以类为中心的语言,原型模式不是必选项,它只用于特定场景  Java在大多数情况下以“实例化类”的方式来创建对象,虽然专门针对原型模式设计了一套接口和方法,但是只在必要场景下通过原型方法来应用原型模式,实现类型之间的解耦 123// 实例化类:通过传递相同的参数来创建相同的实例Dog dog = new Dog('旺财', 'male', 3, '柴犬')Dog dog_copy = new Dog('旺财', 'male', 3, '柴犬')  以原型为中心的语言,原型模式是根基,是一种编程范式  JavaScript本身类型比较模糊,不存在类型耦合的问题,使用原型模式不是为了得到一个副本,而是为了得到与构造函数(类)相对应类型的实例,实现数据和方法的共享 二. 谈原型模式,其实是谈原型范式 在JavaScript中,原型编程范式的体现就是基于原型链的继承 1. 原型 (1) 每个构造函数都有一个prototype属性指向其原型对象,而其原型对象又有一个constructor属性指回构造函数本身 (2) 每个实例都有一个__proto__属性,使用构造函数创建实例时,实例的__proto__属性就会指向构造函数的原型对象 1234567891011121314151617181920212223// 创建Dog构造函数// Dog.prototype.constructor === Dogfunction Dog(name, age) { this.name = name this.age = age}Dog.prototype.eat = function() { console.log('肉骨头真好吃')}// 使用Dog构造函数创建dog实例// dog.__proto__ === Dog.prototype// dog.hasOwnProperty('name') === true; dog.hasOwnProperty('age') === true// dog.eat === Dog.prototype.eat; dog.hasOwnProperty('eat') === falseconst dog = new Dog('旺财', 3)// 原型链: 实例 -> 实例的原型对象 -> 实例的原型对象的原型对象 ... -> Object// dog -> dog.__proto__(Dog.prototype) -> Dog.prototype.__proto__(Object.prototype)// Object.prototype.__proto__ === nulldog.toString() === '[object Object]'// 创建一个没有原型的对象Object.create(null).__proto__ === undefined 对象的深拷贝 深拷贝没有完美方案,每一种方案都有它的边界case,需要考虑不同数据结构(Array、Object、Map和Set等)的处理 用递归实现深拷贝的核心思路 1234567891011121314151617181920function deepClone(obj) { // 值类型或null if(obj === null || typeof obj !== 'object') { return obj } // 定义结果对象 let copy = {} if(obj.constructor === Array) { copy = [] } for(key in obj) { if(obj.hasOwnProperty(key)) { copy[key] = deepClone(obj[key]) } } return copy} 更多内容: 深浅拷贝 结构型:装饰器模式 —— 对象装上它,就像开了挂 在不改变原对象的基础上,对原对象进行包装拓展使其可以满足用户更复杂的需求 装饰器模式初相见 为了不被业务逻辑所干扰,应该将旧逻辑与新逻辑分离 12<!-- button --><button id='open'>点击</button> 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849// modalconst Modal = (function() { let modal = null return function() { if(!modal) { modal = document.createElement('div') modal.innerText = 'modal' modal.style.display = 'none' document.body.appendChild(modal) } return modal }})()// 封装旧逻辑-显示modalclass OpenButton { onClick() { const modal = new Modal() modal.style.display = 'block' }}// 装饰器-修改按钮的文字和状态class Decorator { constructor(open_button) { this.open_button = open_button // 传入内含旧逻辑的实例 } onClick() { this.open_button.onClick() // 执行-旧逻辑 this.changeButtonStatus() // 执行-新逻辑 } // 整合新逻辑 changeButtonStatus() { this.changeButtonText() this.disableButton() } // 拆分新逻辑 disableButton() { const btn = document.getElementById('open') btn.setAttribute("disabled", true) } changeButtonText() { const btn = document.getElementById('open') btn.innerText = '置灰' }}const openButton = new OpenButton()const decorator = new Decorator(openButton)document.getElementById('open').addEventListener('click', function() { decorator.onClick()}) 值得关注的细节 重要提示: 在日常开发中,当遇到两段各司其职的代码逻辑时,首先要有“尝试拆分”的敏感,其次要有“该不该拆”的判断,当逻辑粒度过小时,盲目拆分会导致代码中存在过多零碎的小方法,反而不会使代码变得更好 结构型:装饰器模式 —— 深入装饰器原理与优秀案例前置知识:ES7中的装饰器 在ES7中,可以用 @语法糖 去装饰一个类或一个类的方法 123456789101112131415161718192021222324252627// 装饰器函数function classDecorator(target) { target.hasDecorator = true return target}// 类装饰器@classDecoratorclass Button {} Button.hasDecorator // truefunction funDecorator(target, name, descriptor) { let originalMethod = descriptor.value descriptor.value = function() { console.log('装饰器逻辑') return originalMethod.apply(this, arguments) } return descriptor}// 方法装饰器Class Button { @funDecorator onClick() { console.log('原有逻辑') }} const button = new Button() button.onClick() // 装饰器逻辑 原有逻辑 装饰器语法糖背后的故事 装饰器最基本的操作是定义装饰器函数,将被装饰者“交给”装饰器 装饰器函数传参(1) 类修饰器:第一个参数是目标类(2) 方法装饰器:第一个参数是目标类的原型对象,第二个参数是目标属性名,第三个参数是属性描述对象 装饰器函数的调用时机装饰器函数在编译阶段执行,类的实例在代码运行时动态生成,为了让实例能正常调用被装饰好的类的方法,只能用装饰器去修饰目标类的原型对象 生产实践React中的装饰器:HOC HOC(Higher Order Component),高阶组件 编写一个高阶组件,把传入的组件丢进一个有红色边框的容器中 12345678910111213141516// 高阶组件是一个函数,接收一个组件作为参数,并返回一个新组件import React, { Component } from 'react'const BorderHoc = WrappedComponent => class extends Component { return() { return <div style="{{ border: 'solid 1px red' }}"> <WrappedComponent /> </div> }}// 装饰目标组件@BorderHocclass TargetComponent extends React.Component { render() {}} 用装饰器改写 Redux connect 123456789101112131415161718192021222324252627282930import React, { Component } from 'react'import { connect } from 'react-redux'import { bindActionCreators } from 'redux'import action from './action.js'// 建立组件和状态之间的映射关系function mapStateToProps(state) { return state.app }// 建立组件和store.dispatch的关系,使组件具备通过dispatch来派发状态的能力function mapDispatchToProps(dispatch) { return bindActionCreators(action, dispatch)}/* ------ 原本 ------- */class App extends Component { render() {}}// 调用connect可以返回一个具有装饰作用的函数,接收一个组件作为参数,传入组件与Redux结合,具备Redux提供的数据和能力connect(mapStateToProps, mapDispatchToProps)(App) /* ------ 改写 ------ */// 将调用connect的结果作为一个装饰器const _connect = connect(mapStateToProps, mapDispatchToProps)@_connect class App extends Component { render()} 结构型:适配器模式 —— 兼容代码就是一把梭 通过把一个类的接口变换成客户端所期待的另一种接口,解决一些兼容性问题 适配器的业务场景 用适配器承接旧接口的参数,实现新旧接口的无缝衔接 12345678910111213141516171819202122232425262728293031// 旧接口function Ajax(type, url, data, success, failed) { if(type === 'Get') {} else if(type === 'Post') {}}Ajax('get', url, data, function(res){}, function(err){})Ajax('post', url, data, function(res){}, function(err){})// 新接口class HttpUtils { static get(url) {} static post(url, data) {}}// 适配器 - 入参与旧接口保持一致function AjaxAdapter(type, url, data, success, failed) { let res try { if(type === 'Get') { res = HttpUtils.get(url) } else if(type === 'Post') { res = HttpUtils.post(url, data) } success(res) } catch(err) { failed(err) }}function Ajax(type, url, data, success, failed) { AjaxAdapter(type, url, data, success, failed)} 生产实践:axios中的适配器 用dispatchRequest方法派发请求 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657// 1. 统一接口Axios.prototype.request = function request(config) { return dispatchRequest(newConfig) }// 用getDefaultAdapter方法获取默认适配器var defaults = { adapter: getDefaultAdapter()}function getDefaultAdapter() { var adapter; if (typeof XMLHttpRequest !== 'undefined') { // 浏览器环境 adapter = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) {}) // 4. 统一规则 } } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { // Node环境 adapter = function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) {} // 4. 统一规则 } } return adapter; // 3. 统一出参,都是Promise}// 2. 统一入参,调用适配器function dispatchRequest(config) { // 转换请求体 config.data = transformData.call( config, config.data, config.headers, config.transformRequest ); var adapter = config.adapter || defaults.adapter; return adapter(config).then( function onAdapterResolution(response) { // 转换响应体 response.data = transformData.call( config, response.data, response.headers, config.transformResponse ); return response }, function onAdapterRejection(reason) { // 转换响应体 if (reason && reason.response) { reason.response.data = transformData.call( config, reason.response.data, reason.response.headers, config.transformResponse ) } return Promise.reject(reason) } );} 结构型:代理模式 —— 应用实践范例解析 一个对象不能直接访问另一个对象,需要第三者牵线搭桥而间接达到访问目的 前置知识 ES6中的Proxy1const proxy = new Proxy(target, handler) // target为目标对象,handler为拦截行为 事件代理 基于事件冒泡特性,将子元素的事件监听绑定到父元素上,操作不会直接触及目标子元素,而是由父元素进行处理分发后间接作用于子元素 1234<div id="father"> <a href="#">链接1号</a> <a href="#">链接2号</a> </div> 123456789101112131415// 未代理,监听每个子元素const aNodes = document.getElementById('father').getElementsByTagName('a')for(let i=0;i<aNodes.length;i++) { aNodes[i].addEventListener('click', function(e) { e.preventDefault() alert(`我是${aNodes[i].innerText}`) })}// 代理,只监听父元素document.getElementById('father').addEventListener('click', function(e) { if(e.target.tagName === 'A') { e.preventDefault() alert(`我是${e.target.innerText}`) }}) 虚拟代理 图片预加载时,页面img元素先展示占位图片,创建一个Image实例加载目标图片,然后将页面img元素的src指向目标图片(已缓存) 12345678910111213141516171819202122232425// 操作真实Imageclass PreLoadImage { constructor(imgNode) { this.imgNode = imgNode } setSrc(imgUrl) { this.imgNode.src = imgUrl }}// 接收真实Image,操作虚拟Imageclass ProxyImage { static LOADING_URL = 'xxxxxx' constructor(targetImage) { this.targetImage = targetImage } setSrc(targetUrl) { this.targetImage.setSrc(ProxyImage.LOADING_URL) // 真实Image初始化显示占位图片 const virtualImage = new Image() // 创建虚拟Image实例 virtualImage.onload = () => { // 虚拟Image加载完毕,真实Image显示真实图片 this.targetImage.setSrc(targetUrl) } virtualImage.src = targetUrl // 虚拟Image初始化显示真实图片 }} 缓存代理 对运算结果进行缓存 12345678910111213141516171819202122// 未代理,每次都重新计算const addAll = function() { let result = 0 const len = arguments.length for(let i = 0; i < len; i++) { result += arguments[i] } return result}// 代理,优先从缓存中读取计算结果const proxyAddAll = (function(){ const resultCache = {} return function() { const args = Array.prototype.join.call(arguments, ',') if(args in resultCache) { return resultCache[args] } return resultCache[args] = addAll(...arguments) }})()proxyAddAll(1,2) 保护代理 在访问层的getter和setter函数里添加校验和拦截,确保一部分变量是安全的 1234567891011121314151617181920212223const person = { age: 18, career: 'teacher', phone: 12345654321}const baseInfo = ['age', 'career']const privateInfo = ['phone']const user = {isValidated: true, isVIP: false}const accessProxy = new Proxy(person, { get: function(person, key) { if(!user.isValidated) { alert('您还没有完成验证哦') return } if(!user.isVIP && privateInfo.indexOf(key)!==-1) { alert('只有VIP才可以查看该信息哦') return } return person[key] }}) 行为型:策略模式 —— 重构小能手,拆分胖逻辑 定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换 12345678910111213141516171819202122232425262728293031// 对象映射:把相关算法收敛到一个对象里 // 单一职责原则:各行为函数相互独立,不依赖调用主体const priceProcessor = { processor1(originPrice) { if (originPrice >= 100) { return originPrice - 20 } return originPrice * 0.9 }, processor2(originPrice) { if (originPrice >= 100) { return originPrice - 30 } return originPrice * 0.8 }};// 通过委托实现行为分发function askPrice(tag, originPrice) { return priceProcessor[tag](originPrice)}// 扩展:开发封闭原则priceProcessor.processor3 = function(originPrice) { if (originPrice >= 100) { return originPrice - 50 } return originPrice}askPrice('processor3', 150) // 100 行为型:状态模式 —— 自主咖啡机背后的力量 允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类 1234567891011121314151617181920212223242526272829303132class CoffeeMaker { constructor() { this.state = 'init' this.excessMile = '1000ml' // 主体状态 } // 单一职责原则:各行为函数可能不会特别割裂,和状态主体之间存在着关联 stateToProcess = { that: this, american() { console.log('咖啡机现在的牛奶存储量是:', this.that.leftMilk) // 获取主体状态 console.log('我只吐黑咖啡') } latte() { this.american() console.log('加点奶') }, mocha() { this.latte(); console.log('加点巧克力') } changeState(state) { // 状态切换函数 this.state = state if (!this.stateToProcessor[state]) { return } this.stateToProcessor[state]() } }}const mk = new CoffeeMaker();mk.changeState('latte'); 行为型:观察者模式 —— 鬼故事:产品经理拉了一个钉钉群 观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个目标对象,当这个目标对象的状态发生变化时,会通知所有观察者对象,使它们能够自动更新,发布者可以直接触及到订阅者 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879/* ------ 角色划分 状态变化 发布者通知订阅者 ------ */// 抽象的发布者类class Publisher { constructor() { this.observers = [] // *维护订阅者的集合 console.log('Publisher created') } addObserver(observer) { // 增加订阅者 console.log('Publisher.addObserver invoked') this.observers.push(observer) } removeObserver(observer) { // 移除订阅者 console.log('Publisher.removeObserver invoked') this.observers.forEach((item, i) => { if (item === observer) { this.observers.splice(i, 1) } }) } notifyObserver() { // 通知所有订阅者 console.log('Publisher.notifyObserver invoked') this.observers.forEach((observer) => { observer.update(this) }) }}// 抽象的订阅者类class Observer { constructor() { console.log('Observer created') } update() { console.log('Observer.update invoked') // *提供统一方法供发布者调用 }}// 具体的发布者类class SpPublisher extends Publisher { constructor() { super() this.state = null console.log('SpPublisher created') } getState() { console.log('SpPublisher.getState invoked') return this.state } setState(state) { console.log('SpPublisher.setState invoked') this.state = state this.notifyObserver() }}// 具体的订阅者类class SpObserver extends Observer { constructor() { super() this.state = null console.log('SpObserver created') } update(publisher) { console.log('SpObserver.update invoked') this.state = publisher.getState() this.do() } do() { console.log('SpObserver.do invoked') console.log(this.state) }}const publisher = new SpPublisher()const observer1 = new SpObserver()const observer2 = new SpObserver()publisher.addObserver(observer1)publisher.addObserver(observer2)publisher.setState('go!!!') 行为型:观察者模式 —— 面试真题手把手教学Vue数据双向绑定(响应式系统)的实现原理 “发布-订阅”模式:发布者不直接触及到订阅者,而是由统一的第三方完成实际的通信操作,实现了完全地解耦 三个关键角色:监听器、订阅者、编译器(1) observer监听器:监听数据并转发给订阅者(发布者)(2) watcher订阅者:接收数据并更新视图(3) compile编译器:初始化视图,更新订阅者… 核心代码123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111// observer:在init阶段,对数据进行响应式化function observer(target) { if(target && typeof target === 'object') { Object.keys(target).forEach((key)=>{ defineReactive(target, key, target[key]) }) }}function defineReactive(target, key, val) { const dep = new Dep() observer(val) Object.defineProperty(target, key, { enumerable: true, configurable: false, get: function() { dep.addSub(Dep.target) // 依赖收集:将watcher对象存放到dep中 return val }, set: function(value) { // 通过setter -> Watcher -> update的流程来修改视图 if(val !== value) { val = value dep.notify() } } })}// Dep:订阅者收集器(发布者)class Dep { constructor() { this.subs = [] //存放watcher } addSub(sub) { this.subs.push(sub) } notify() { this.subs.forEach((sub)=>{ sub.update() }) }}// watcher: 订阅者(观察者)class Watcher { constructor() { Dep.target = this } update() { console.log("视图更新啦~") }}// Vue dataclass Vue { constructor(options) { this._data = options.data observer(this._data) new Watcher() console.log('render:', this._data.test) // 模拟渲染,触发get }}let obj = new Vue({ data: { test: 1 }})obj._data.test = 2// 全局事件总线 Event Bus:所有事件的发布/订阅操作必须经由事件中心class EventBus { constructor() { this.handlers = {} // 存储事件与回调的对应关系 } on(eventName, callback) { if(!this.handlers[eventName]) { this.handlers[eventName] = [] // 初始化监听函数队列 } this.handlers[eventName].push(callback) } emit(eventName, ...args) { if(this.handlers[eventName]) { const handlers = this.handlers[eventName].slice() // 浅拷贝,避免once移除监听器时弄乱顺序 handlers.forEach((callback) => { // 逐个调用监听函数队列里的回调函数 callback(...args) }) /* this.handlers[eventName].forEach((callback) => { callback(...args) }) */ } } off(eventName, callback) { // 移除监听函数队列里的指定回调函数 const callbacks = this.handlers[eventName] const index = callbacks.indexOf(callback) if (index !== -1) { callbacks.splice(index, 1) } } once(eventName, callback) { // 单次监听 const wrapper = (...args) => { callback(...args) this.off(eventName, wrapper) } this.on(eventName, wrapper) }}let bus = new EventBus()function getNum(num) { console.log('get:', num)}bus.on('event1', getNum)bus.emit('event1', 8)bus.once('event2', getNum)bus.emit('event2', 10) 行为型:迭代器模式 —— 真·遍历专家 迭代器模式提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象的内部表示 ES6对迭代器的实现 ES6约定,任何数据结构只要具备Symbol.iterator属性,就可以通过迭代器.next()和for(…of…)进行遍历 1234567891011121314const arr = [1, 2, 3]// 迭代器.next()const iterator = arr[Symbol.iterator]()let now = { done: false }while(!now.done) { now = iterator.next() if(!now.done) { console.log(`现在遍历到了${now.value}`) }}// for(...of...)for(item of arr) { // 实际是next方法的反复调用 console.log(`当前元素是${item}`)} 实现一个迭代器生成函数123456789101112131415// 通过闭包记录每次遍历的位置function iteratorGenerator(list) { var idx = 0 var len = list.length return { next: function() { var done = idx >= len var value = !done ? list[idx++] : undefined return { done: done, value: value } } }} 参考链接 vue.use()方法从源码到使用 vuex 实现原理 响应式系统的基本原理 响应式系统的依赖收集追踪原理","categories":[{"name":"技术","slug":"技术","permalink":"https://nodisappear.github.io/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"笔记","slug":"笔记","permalink":"https://nodisappear.github.io/tags/%E7%AC%94%E8%AE%B0/"}],"author":"张雅娴"},{"title":"deepmerge源码解析","slug":"deepmerge源码解析","date":"2021-10-08T08:45:41.000Z","updated":"2023-10-16T15:40:03.607Z","comments":true,"path":"a519f8f.html","link":"","permalink":"https://nodisappear.github.io/a519f8f.html","excerpt":"","text":"一. 介绍deepmerge: 以深拷贝的方式,合并两个或多个对象的可枚举属性。 二. 模块加载,CommonJs规范:module.exports = deepmerge; 三. 主要流程1234567891011121314151617181920function deepmerge(target, source, options) { options = options || {} options.arrayMerge = options.arrayMerge || defaultArrayMerge options.isMergeableObject = options.isMergeableObject || defaultIsMergeableObject // cloneUnlessOtherwiseSpecified is added to `options` so that custom arrayMerge() // implementations can use it. The caller may not replace it. options.cloneUnlessOtherwiseSpecified = cloneUnlessOtherwiseSpecified var sourceIsArray = Array.isArray(source) var targetIsArray = Array.isArray(target) var sourceAndTargetTypesMatch = sourceIsArray === targetIsArray if (!sourceAndTargetTypesMatch) { return cloneUnlessOtherwiseSpecified(source, options) } else if (sourceIsArray) { return options.arrayMerge(target, source, options) } else { return mergeObject(target, source, options) }} 1. 判断target和source的类型主要分为:(1)source和target是不同的类型,执行cloneUnlessOtherwiseSpecified(source, options);(2)source和target都是数组类型,执行options.arrayMerge(target, source, options);(3)sourc和target都是Object类型,执行mergeObject(target, source, options)。 1234567891011121314151617181920function mergeObject(target, source, options) { var destination = {} if (options.isMergeableObject(target)) { getKeys(target).forEach(function(key) { destination[key] = cloneUnlessOtherwiseSpecified(target[key], options) }) } getKeys(source).forEach(function(key) { if (propertyIsUnsafe(target, key)) { return } if (propertyIsOnObject(target, key) && options.isMergeableObject(source[key])) { destination[key] = getMergeFunction(key, options)(target[key], source[key], options) } else { destination[key] = cloneUnlessOtherwiseSpecified(source[key], options) } }) return destination} 2. mergeObject(target, source, options)(1)对target对象执行深拷贝操作;(2)遍历source对象,如果key值是在target原型链中存在,直接返回;如果key值是target中自有属性且可以合并,则执行 getMergeFunction(key, options)(target[key], source[key], options);否则直接将source属性深拷贝到target中,执行 cloneUnlessOtherwiseSpecified(source[key], options)。 12345function cloneUnlessOtherwiseSpecified(value, options) { return (options.clone !== false && options.isMergeableObject(value)) ? deepmerge(emptyTarget(value), value, options) : value} 3. cloneUnlessOtherwiseSpecified(value, options)将source中存在,而target中不存在的属性直接以一个空Object,进行深拷贝合并。 12345function defaultArrayMerge(target, source, options) { return target.concat(source).map(function(element) { return cloneUnlessOtherwiseSpecified(element, options) })} 4. 数组合并对target和source两个数组执行concat操作,然后对每个值执行深拷贝cloneUnlessOtherwiseSpecified(element, options) 123456789deepmerge.all = function deepmergeAll(array, options) { if (!Array.isArray(array)) { throw new Error('first argument should be an array') } return array.reduce(function(prev, next) { return deepmerge(prev, next, options) }, {})} 5. deepmerge.all对多个对象执行深拷贝操作,这里直接将all函数作为deepmerge函数的一个属性 四. 补充知识点deemerge 就是一个考虑全面、比较通用的深拷贝实现【比如source和target不同类型、能否merge、包含原型对象属性、预留自定义merge方法等】。代码本身比较精炼,值得学习参考。 1. getKeys考虑了Symbol属性1234567891011function getEnumerableOwnPropertySymbols(target) { return Object.getOwnPropertySymbols ? Object.getOwnPropertySymbols(target).filter(function(symbol) { return target.propertyIsEnumerable(symbol) }) : []}function getKeys(target) { return Object.keys(target).concat(getEnumerableOwnPropertySymbols(target))} 2. 属性是否位于原型链上,自由属性是否可枚举1234567891011121314function propertyIsOnObject(object, property) { try { return property in object } catch(_) { return false }}// Protects from prototype poisoning and unexpected merging up the prototype chain.function propertyIsUnsafe(target, key) { return propertyIsOnObject(target, key) // Properties are safe to merge if they don't exist in the target yet, && !(Object.hasOwnProperty.call(target, key) // unsafe if they exist up the prototype chain, && Object.propertyIsEnumerable.call(target, key)) // and also unsafe if they're nonenumerable.} 参考文献[1] deepmerge的npm官方文档","categories":[{"name":"技术","slug":"技术","permalink":"https://nodisappear.github.io/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"源码解析","slug":"源码解析","permalink":"https://nodisappear.github.io/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/"}],"author":"李泽滨"},{"title":"IDE常用快捷键","slug":"IDE常用快捷键","date":"2021-09-14T07:43:40.000Z","updated":"2023-10-16T15:39:48.640Z","comments":true,"path":"5028d3ef.html","link":"","permalink":"https://nodisappear.github.io/5028d3ef.html","excerpt":"","text":"VSCode折叠与展开区域内所有代码12Ctrl + K + 0 // 折叠 Ctrl + K + J // 展开 折叠与展开某块区域代码12Ctrl + K + [ // 折叠 Ctrl + K + ] // 展开 视图上下偏移12Ctrl + Up , Ctrl + Down // 行视图,幅度较小Alt + PageUp , Alt + PageDown // 屏视图,幅度较大 移动当前行的位置12Alt + Up // 向上移一行 Alt + Down // 向下移一行 复制当前行12Shift + Alt + Up // 复制到前一行Shift + Alt + Down // 复制到后一行 基于当前行插入一行12Ctrl + Shift + Enter // 之前插入Ctrl + Enter // 之后插入 搜索与跳转12Ctrl + P + "str" // 搜索名称包含str的文件Ctrl + P + ":n" // 跳转到第n行 查询与替换123/* 选中内容会默认填入框内 */Ctrl + F // 查询Ctrl + H // 替换","categories":[{"name":"技术","slug":"技术","permalink":"https://nodisappear.github.io/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"效率","slug":"效率","permalink":"https://nodisappear.github.io/tags/%E6%95%88%E7%8E%87/"}],"author":"张雅娴"},{"title":"前端性能分析","slug":"前端性能分析","date":"2021-09-11T08:07:41.000Z","updated":"2023-10-16T15:39:37.551Z","comments":true,"path":"dd85c073.html","link":"","permalink":"https://nodisappear.github.io/dd85c073.html","excerpt":"","text":"一些图1. Lighthouse界面 2. Performance界面 3. 指标解析 一些概念1. 长任务W3C性能工作组在LongTask规范中将超过50ms的任务定义为长任务。 2. FPSFrames Per Second,每秒帧率,表示每秒种画面的更新次数,大多数设备的屏幕刷新率都是60次/秒。 3. 时间切片(1) 核心思想: 如果一个任务不能在50ms内执行完,那么为了不阻塞主线程,它应该让出主线程的控制权(即停止执行这个任务),浏览器会先去处理其它任务,然后再回来继续执行未完成的任务。(2) 技术手段:将一个长任务拆分成很多个不超过50ms的小任务,分散在宏任务队列中执行。(3) 优、缺点:优点是可以避免卡死浏览器;缺点是任务运行的总时间变长(每处理完一个小任务,主线程会空闲出来,而且在下一个小任务开始处理之前有一小段延时)。(4) 代码实现: 123456789101112131415161718192021222324252627282930/* 用ES6的Generator函数 */ function someThing() { console.log('someThing');}function otherThing() { console.log('otherThing');}function ts (gen) { if (typeof gen === 'function') gen = gen(); if (!gen || typeof gen.next !== 'function') return; return function next() { const res = gen.next(); if (res.done) return; setTimeout(function () { next(); }, 1000); }}ts(function* doing() { const start = performance.now(); while (performance.now() - start < 10000) { someThing(); yield; // 暂停执行 otherThing(); } console.log('done!');})(); 一些方法1. 获取LCP12345new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { console.log("LCP candidate:", entry.startTime); }}).observe({ type: "largest-contentful-paint", buffered: true }); 2. 获取FID12345678910111213141516171819202122232425262728293031/* --- 方法一:通过PerformanceObserver --- */new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { const delay = entry.processingStart - entry.startTime; console.log("FID candidate:", delay); }}).observe({ type: "first-input", buffered: true });/* --- 方法二:通过<head></head>内监听用户交互事件 --- */['click','mousedown','touchstart','keydown', 'pointerdown'].forEach(eventType => { window.addEventListener(eventType, eventHandle);});function eventHandle(e){ /* --- 1. 通过performance.getEntriesByType('first-input') --- */ console.log(performance.getEntriesByType('first-input')[0].startTime); console.log(performance.getEntriesByType('first-input')[0].duration); /* --- 2. 通过timeStamp --- */ const eventTime = e.timeStamp; window.requestIdleCallback(onIdleCallback.bind(this, eventTime, e)); // 实验功能,用于在浏览器空闲时间执行回调函数 function onIdleCallback(eventTime, e) { const now = performance.now(); const duration = now - eventTime; return { timeStamp: eventTime, duration: duration } }}; 3. 获取CLS123456789let cls = 0;new PerformanceObserver((entryList) => { for (const entry of entryList.getEntries()) { if (!entry.hadRecentInput) { // 500ms内是否有用户数据 cls += entry.value; console.log("Current CLS value:", cls); } }}).observe({ type: "layout-shift", buffered: true }); 4. 获取FP1console.log(performance.getEntriesByType('paint')[0].startTime); 5. 获取FCP12345678910111213141516171819202122232425262728293031/* --- 方法一:通过performance.getEntriesByType('paint') --- */ console.log(performance.getEntriesByType('paint')[1].startTime);/* --- 方法二:通过Mutation Observer --- */let insertedNodes = [];let observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { for (let i = 0; i < mutation.addedNodes.length; i++) { mutation.addedNodes[i]['time'] = performance.now(); // 手动添加时间戳 insertedNodes.push(mutation.addedNodes[i]); } });});setTimeout(()=>{ observer.disconnect(); //停止监听});observer.observe(document.documentElement, { childList: true, subtree: true }); // 开始监听let timer = setInterval(()=>{ if(insertedNodes.length){ clearInterval(timer); for(let i=0;i<insertedNodes.length; i++){ if(insertedNodes[i].className === 'container'){ console.log(insertedNodes[i].time); // 获取某个元素的时间戳 break; } }; }}, 100); 6. 获取TTI1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950/* --- 方法一:通过安装npm包tti-polyfill或script标签引入tti-polyfill.js --- */<script> !function(){ // 创建PerformanceObserver实例观察longtask if('PerformanceLongTaskTiming' in window){ var g = window.__tti={ e:[] }; g.o = new PerformanceObserver(function(l){ g.e = g.e.concat(l.getEntries()); }); g.o.observe({ entryTypes:['longtask'] }) } }();</script>ttiPolyfill.getFirstConsistentlyInteractive().then((tti) => { console.log(tti);});/* --- 方法二:在页面加载的一段时间loadTime内,以(domContentLoadedEventStart-navigationStart)+basicTime为起始点进行循环查找,直到找到一个网络请求不超过2个且没有长任务的窗口,该窗口之前的最后一个长任务结束的时间点就是可稳定交互时间 --- */const basicTime = 5000, loadTime = 50000;function getTTItime(startTime, longTaskEntries, resourceEntries, domContentLoadedTime) { // 伪代码 let tti = startTime; while(startTime + basicTime <= loadTime) { tti = startTime; // 长任务 let longTasksInWindow = longTaskEntries.filter(task => { return task.startTime < startTime + basicTime && task.startTime + task.duration > startTime; }); if (longTasksInWindow.length) { const lastLongTask = longTasksInWindow[longTasksInWindow.length - 1]; startTime = lastLongTask.startTime + lastLongTask.duration; continue; } // 网络请求超过2个 let busyNetworkInWindow = resourceEntries.filter(request => { return !(request.startTime >= startTime + basicTime || request.startTime + request.duration <= startTime); }); if (busyNetworkInWindow.length > 2) { const firstRequest = busyNetworkInWindow[0]; startTime = firstRequest.startTime + firstRequest.duration; continue; } return Math.max(tti, domContentLoadedTime); } return Math.max(tti, domContentLoadedTime);} 参考链接Web性能领域常见的专业术语捕获FMP的原理时间切片 ( Time Slicing ) 使用 Lighthouse 分析前端性能前端性能优化之谈谈常见的性能指标及上报策略补齐Web前端性能分析的工具盲点Mutation Observer API解读新一代 Web 性能体验和质量指标2021 年 Web 核心性能指标是什么?谷歌工程师告诉你,FMP 过时啦!Chrome的Performance面板","categories":[{"name":"技术","slug":"技术","permalink":"https://nodisappear.github.io/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"性能","slug":"性能","permalink":"https://nodisappear.github.io/tags/%E6%80%A7%E8%83%BD/"}],"author":"张雅娴"},{"title":"hexo配置","slug":"hexo配置","date":"2021-09-06T06:09:41.000Z","updated":"2023-10-16T16:42:06.805Z","comments":true,"path":"b5a0661a.html","link":"","permalink":"https://nodisappear.github.io/b5a0661a.html","excerpt":"","text":"Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub. 一. 常用操作Create123$ hexo new post "source/_posts"$ hexo new page "source"$ hexo new draft "source/_drafts" Clean1$ hexo clean Publish1hexo publish draft "source/_drafts" Generate static files1$ hexo generate Run server1$ hexo server Deploy to remote sites1$ hexo deploy 二. hexo插件hexo添加永久链接之abbrlinkhexo添加搜索功能之hexo-generator-json-contenthexo添加评论功能之utterancessnippet主题下载snippet主题使用","categories":[{"name":"技术","slug":"技术","permalink":"https://nodisappear.github.io/categories/%E6%8A%80%E6%9C%AF/"}],"tags":[{"name":"配置","slug":"配置","permalink":"https://nodisappear.github.io/tags/%E9%85%8D%E7%BD%AE/"}],"author":"张雅娴"}],"categories":[{"name":"技术","slug":"技术","permalink":"https://nodisappear.github.io/categories/%E6%8A%80%E6%9C%AF/"},{"name":"编程语言","slug":"编程语言","permalink":"https://nodisappear.github.io/categories/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"}],"tags":[{"name":"源码解析","slug":"源码解析","permalink":"https://nodisappear.github.io/tags/%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/"},{"name":"编程语言","slug":"编程语言","permalink":"https://nodisappear.github.io/tags/%E7%BC%96%E7%A8%8B%E8%AF%AD%E8%A8%80/"},{"name":"知识点","slug":"知识点","permalink":"https://nodisappear.github.io/tags/%E7%9F%A5%E8%AF%86%E7%82%B9/"},{"name":"笔记","slug":"笔记","permalink":"https://nodisappear.github.io/tags/%E7%AC%94%E8%AE%B0/"},{"name":"效率","slug":"效率","permalink":"https://nodisappear.github.io/tags/%E6%95%88%E7%8E%87/"},{"name":"性能","slug":"性能","permalink":"https://nodisappear.github.io/tags/%E6%80%A7%E8%83%BD/"},{"name":"配置","slug":"配置","permalink":"https://nodisappear.github.io/tags/%E9%85%8D%E7%BD%AE/"}]}