手写路由
hash
利用了url的hash值变化但是页面不刷新的原理。我们想要一个路由实例,在实例中注册对应的url可以进行不同的组件的显示。 因此我们首先有个类创建路由实例。 有个注册方法,将不同的hash值与不同的页面操作连接起来,页面操作封装到callback函数里,我们可以用一个map封装。
class HashRouter(){
constructor() {
this.routess = [];
window.addEventListener('hashChange',this.load().bind(this));//监听变化
load(){
let hash = window.location.hash.splice(1);
let callback = this.routes[hash];
callback&&callback.call(this);
}
}
}
history
history是HTML5新增的api,简单来说能够控制用户的会话和携带数据但是不刷新页面。
注意: hash路由通过监听hashchange来改变页面内容
但是浏览器没有提供类似onurlchange这样的事件。您可能想到监听window.location的变化,但在JavaScript中这也没有直接的方法(location对象没有变化事件) 在history中URL变化可能来自三个不同的来源:
- 用户点击浏览器的前进/后退按钮(只会改变url,并不会存储页面)
- 这会触发popstate事件
- 我们必须监听这个事件来响应这类变化
- 通过history.pushState/replaceState编程方式改变URL
- 这些方法不会触发任何事件!
- 调用后需要手动更新页面内容
- 用户点击应用内的链接
- 需要拦截点击事件并阻止默认行为
- 然后手动处理路由变化
我们的history路由刷新可能出现404,这时候服务器重新返回一下HTML即可
class HistoryRouter {
constructor() {
// 存储路由映射
this.routes = {};
// 内容容器
this.container = document.getElementById('app');
// 绑定方法的this
this.handlePopState = this.handlePopState.bind(this);
this.handleLink = this.handleLink.bind(this);
// 初始化
this.init();
}
init() {
// 监听链接点击事件
document.addEventListener('click', this.handleLink);
// 监听浏览器前进/后退按钮
window.addEventListener('popstate', this.handlePopState);
// 加载当前页面
this.loadRoute(location.pathname);
}
// 注册路由
register(path, callback) {
this.routes[path] = callback;
return this; // 允许链式调用
}
// 处理链接点击
handleLink(e) {
// 只处理带有data-link属性的链接
if (e.target.matches('[data-link]')) {
e.preventDefault();
const url = e.target.getAttribute('href');
this.navigate(url);
}
}
// 导航到指定路径
navigate(path) {
// 更新历史记录和URL
history.pushState({path}, '', path);
// 加载对应的路由内容
this.loadRoute(path);
}
// 处理浏览器前进/后退
handlePopState(e) {
const path = location.pathname;
this.loadRoute(path);
}
// 加载路由对应的内容
loadRoute(path) {
const route = this.routes[path] || this.routes['*']; // 尝试获取路由或404路由
if (route && typeof route === 'function') {
const content = route();
this.renderContent(content);
} else {
this.renderContent(`<h2>404 未找到</h2><p>路径 "${path}" 不存在</p>`);
}
}
// 渲染内容到容器
renderContent(content) {
this.container.innerHTML = content;
}
}
手写响应式
reactive
// reactive.js
import {
mutableHandlers
} from './baseHandles';
export const reactiveMap = new WeakMap();
export const shallowReactiveMap = new WeakMap();// 浅响应式
// 大型项目 响应式对象很多,但是reactiveMap 只有一个 性能?
// 垃圾回收 弱引用
// router-view
export const reactive = (target ) => {
return createReactiveObject(target,reactiveMap,mutableHandlers);
}
export const shallowReactive = (target ) => {
return createReactiveObject(target,shallowReactiveMap,shallowReactiveHandlers);
}
function createReactiveObject(target, proxyMap, proxyHandlers) {
if (typeof target !== 'object') {
console.warn('reactive 必须是一个对象')
return target;
}
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target,mutableHandlers); // 被代理对象,拦截对象方法
proxyMap.set(target,proxy);
return proxy;
}
//baseHandle.js
import { track } from './effect';
import { trigger } from './effect';
import { reactive } from './reactive';
import { isObject } from '../shared';
// 代理对象的拦截操作
const get = createGetter();
const set = createSetter();
const shallowReactiveGet = createGetter(true);
function has(target,key){
const res = Reflect.has(target,key);
track(target,'has',key);
return res;
}
// 代理对象get
function createGetter(shallow = false) {
return function get(target, key, receiver) {
// 收集依赖
track(target,'get',key);
let res = target[key];
if(shallow){
return res;
}
if(isObject(res)){
return reactive(res);
}
return res;
}
}
// 代理对象set
function createSetter() {
return function set(target, key, value, receiver) {
target[key] = value;
trigger(target,'set',key);
return true;
}
}
export const mutableHandlers = {
get,
set,
//has,
}
export const shallowReactiveHandlers = {
get: shallowReactiveGet,
set
}
//effect.js
let activeEffect = null;
let targetMap = new WeakMap();// 弱引用
export function effect(fn) {
// 返回一个函数 立即执行一次
const effectFn = () => {
try{
activeEffect = effectFn;
let res = fn();
return res;
}finally{
activeEffect = null;
}
}
console.log(fn,'fn')
effectFn();
return effectFn;
}
// 拦截到get请求进行的操作
export function track(target,type,key) { //<obj,<obj.key,set>>
console.log('触发track -> target type key')
let depsMap = targetMap.get(target);
if(!depsMap){
targetMap.set(target,depsMap = new Map());
}
let dep = depsMap.get(key);
if(!dep){
depsMap.set(key,dep = new Set());
}
dep.add(activeEffect);
}
// 拦截到set请求进行的操作
export function trigger(target,type,key) {
console.log('触发trigger -> target type key')
let depsMap = targetMap.get(target);
if(!depsMap){
return;
}
let dep = depsMap.get(key);
if(!dep){
return;
}
dep.forEach(effectFn => {
console.log(effectFn,'effectFn')
effectFn();
})
}
简单响应式:
let effectList = []
let activeEffect = null
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
// 依赖收集
if (activeEffect) {
effectList.push(activeEffect)
}
return target[key]
},
set(target, key, value) {
target[key] = value
// 触发更新
effectList.forEach(effect => effect())
return true
}
})
}
function effect(callback) {
activeEffect = callback
callback() // 执行一次进行依赖收集
activeEffect = null
}
const data = reactive({count:0});
effect(()=>{
console.log('count changed:', data.count);
})
总结:我们的复杂对象使用Proxy来进行拦截。 proxy是es6引入的一个新语法。
- 当我们的数据类型为复杂对象时,我们无需一个个的将每个属性遍历的用defineProperty进行设置getter和setter,所以他的性能在这种复杂对象上会比defineProterty好很多。
- 并且他支持13种底层操作的拦截(in,delete,函数调用等,所以可以拦截数组索引和length,对象添加删除)。
- 对于嵌套的对象,可以实现惰性监听(只有被访问才递归监听)(defineproperty必须知道拦截的属性进行设置) 所以vue3选择了proxy进行响应式的拦截。
我们会创建一个WeakMap(当我们的对象在组件销毁,没有引用指向时候,会自动回收)来存储对象的响应式属性以及需要重新执行的响应式方法 例如Effect()。
当我们使用了Reactive,我们首先会查看一下这个对象是否是响应式对象。他有一个专门的map存储我们的原始对象和代理对象。假如已经是响应式对象,就把我们的响应式对象返回。假如不是,会根据选项将对象放入map或者sharrowmap中:防止出现这种情况:第一次浅度,第二次深度,直接返回浅度对象。
假如不是,他会创建我们的一个代理拦截对象。 当我们的用户第一次访问属性的时候,他会去调用track方法去搜集依赖。假如调用的不是响应式方法,就会直接返回,是响应式方法就加入到依赖map之中。
当用户设置属性的时候,他会拦截去调用 我们的trigger方法。接着遍历我们的每一个方法,重新执行一遍。
ref
import { reactive } from './reactive';
import {track, trigger} from './effect'
export function ref(value) {
if (isRef(value)) {
return value;
}
return new RefImpl(value);
}
// 最轻量的拦截器
class RefImpl {
constructor(val){
// 私有
this.__isRef = true;
this._val = convert(val);
}
get value(){
track(this,'get','value');
return this._val;
}
set value(val){
if(val !== this._val){
this._val = convert(val);
trigger(this,'set','value');
}
}
}
function convert(val){
return typeof val === 'object' ? reactive(val) : val;
}
function isRef(value) {
return !!value.__isRef;
}
总结: 简单属性的响应式采用了class关键字的getter和setter来实现,它可以看作是defineProper的语法糖。 我们通过将属性包装成一个对象,同样去使用track和trigger方法去进行进行处理。假如传入的是一个对象,他会用reacctive将他的对象进行响应式处理,接着包装成一个value的二级对象进行返回。
简单diff 算法
首先是模板编译,编译成render函数。render函数中包括我们的js代码。
当我们的响应式数据发生变化时,他不可能说直接追踪更新我们每个依赖发生变化的DOM部分。他会重新执行我们的render函数进行渲染。描述出我们的新虚拟DOM树,他是在内存中的一个DOM副本。接着将新旧虚拟DOM树来进行对比。计算出最优差异变更法,更新差异,这个算法,就叫diff算法。
首先他是是同层的节点之间进行比较。当找到类型相同节点时(key,),则会调用patch方法,patch方法主要干两件事:递归遍历比较子节点,查看新旧节点的不同地方(比如文本)进行更新。
假如在新DOM树中相同节点的位置不同,则会进行节点移动。主要通过一个lastIndex来记录已经处理好的节点在旧节点中的索引值,假如找到的旧DOM节点index j在lastIndex之前,则会将VNode往后调,否则不动。
假如没在旧树中找到新节点,那就找到他应该插入的位置,进行插入。 最后再到旧树中找新树中没有的节点,调用DOM方法进行删除。
但是这种算法有时候很耗费性能的,例如(abcde,edcba)完全逆序,你要移动4次DOM。于是改进了算法,采用双端比较法,头头,尾尾,头尾,尾头依次比较。
在vue3中使用的是一个最长连续字串的动态规划算法。
有没有想过为什么我们的每次响应式数据变化render函数都会重新执行?不会很耗费性能吗?为什么不能像我们操控dom一样对特定的依赖数据的DOM进行原子化的更新呢? 现在已经有类似的前端框架出现(svelet)
const oldChildren = n1.children;
const newChildren = n2.children;
let lastIndex = 0;
// 遍历新的 children
for (let i = 0; i < newChildren.length; i++) {
const newVNode = newChildren[i];
let j = 0;
let find = false;
// 遍历旧的 children
for (; j < oldChildren.length; j++) {
const oldVNode = oldChildren[j];
// 如果找到了具有相同 key 值的两个节点,则调用 patch 函数更新
if (newVNode.key === oldVNode.key) {
find = true;
patch(oldVNode, newVNode, container);// 更新当前节点标签,子节点递归更新
if (j < lastIndex) {
// 需要移动
const prevVNode = newChildren[i - 1];
if (prevVNode) {
const anchor = prevVNode.el.nextSibling;
insert(newVNode.el, container, anchor);
}
} else {
// 更新 lastIndex
lastIndex = j;
}
break;
}
}
if (!find) {
const prevVNode = newChildren[i - 1];
let anchor = null;
if (prevVNode) {
anchor = prevVNode.el.nextSibling;
} else {
anchor = container.firstChild;
}
patch(null, newVNode, container, anchor);
}
}
// 遍历旧的节点
for (let i = 0; i < oldChildren.length; i++) {
const oldVNode = oldChildren[i];
// 拿着旧 VNode 去新 children 中寻找相同的节点
const has = newChildren.find(
vnode => vnode.key === oldVNode.key
);
if (!has) {
// 如果没有找到相同的节点,则移除
unmount(oldVNode);
}
}
手写简单axios
function simpleAxios({baseURL = ''}){
// 拦截器
const interceptors = {
request: [],
response: []
}
// 推入拦截器
function useRequestInterceptor(interceptor){
interceptors.request.push(interceptor);
}
// 拦截器注册执行
function executeInterceptors(interceptors, config){
return interceptors.reduce((promise, interceptor) => {
return promise.then(interceptor);
}, Promise.resolve(config));
}
function sendRequest(method, url, data) {
return executeInterceptors(interceptors.request, {method, url, data})
.then(({method, url, data}) => {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open(method, url);//异步 || 同步
if(method === 'POST'){
xhr.setRequestHeader('Content-Type', 'application/json');
}
xhr.onreadystatechange = function(){
if(xhr.readyState === 4 && xhr.status === 200){
resolve(xhr.responseText);
}
else{
reject(xhr.statusText);
}
}
xhr.send(JSON.stringify(data));
});
});
}
return {
get(url){
return sendRequest('GET', `${baseURL}${url}`);
},
post(url, data){
return sendRequest('POST', `${baseURL}${url}`, data);
},
useRequestInterceptor(interceptor){
interceptors.request.push(interceptor);
}
}
}
export default simpleAxios;
总结: axios底层使用了XMLHttpRequest进行发送消息。我的axios主要实现了baseURL配置,请求相应拦截,连续的tehnable调用。 首先我的axios中有一个baseURL设置,我们可以在初始化的时候传入这个baseURL,接着在发送请求的时候进行模板字符串的拼接。baseURL能很好的进行一些切换,比如我们开发环境和上线的baseurl肯定是不一样的,我们可以通过process.env判断是否是dev选择不同的baseurl。
axios中需要一个拦截器,我在函数中设置了一个拦截器对象,里面有请求拦截器数组和响应拦截器数组。当我们调用对应的拦截器注册方法可以进行注册,也就是将回调函数推入数组中。
因为拦截器是依次执行的,我们可以通过让拦截器函数依次执行即可。我们使用promise的thenable调用,这样能够很好的处理链式错误捕获和值传递。
axios中使用的是reduce函数,他可以对我们的数组进行一个连续的操作,我们设置从初始值为我们的一个promise,因为promise.then返回值一定是一个promise,所以可以进行连续的thenable调用。
接着就可以使用XMLHttpRequest对象进行发送。我们可以把他封装到promise里,方便发送之后的thenable调用。
防抖 节流
// 相同间隔内多次取消前一次执行本次
function debounce(callback, wait) {//防抖:一定时间内取消前一次
let timeout;
return function (...args) {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
callback.apply(this, args);
}, wait);
}
}
//节流:没到时间不执行这一次
function throttle(callback, wait) {
let time = 0;
return function (...args) {
let now = new Date();
if (now - time > wait) {
time = now;
callback.apply(this, args);
}
}
}
这里再给出一个高级的节流,支持leading,trailing,remaining和cancle,通过判断选项配置当前时间来设置前置执行,通过一个定时器来设置后置执行,通过取消定时器取消后置执行。
/**
* 节流函数 - 限制函数在一定时间内只执行一次
* @param {Function} func - 需要节流的函数
* @param {Number} wait - 等待时间(毫秒)
* @param {Object} options - 配置选项
* @param {Boolean} options.leading - 是否在延迟开始前执行函数(默认: true)
* @param {Boolean} options.trailing - 是否在延迟结束后执行函数(默认: true)
* @returns {Function} - 返回节流后的函数
*/
function throttle(func, wait, options={}) {
// 声明函数内部变量
let timeout; // 定时器引用
let context; // 执行上下文
let args; // 函数参数
let result; // 函数返回结果
let previous = 0; // 上次执行时间点
// options对象已在参数中默认初始化为空对象
/**
* 延迟执行函数(在wait时间后执行)
* 作为setTimeout的回调使用
*/
const later = function() {
// 若leading为false,重置为0;否则更新为当前时间戳
previous = options.leading === false ? 0 : new Date().getTime();
// 清除定时器标识
timeout = null;
// 执行原函数
func.apply(context, args);
// 如果没有定时器了,清除上下文和参数引用
if (!timeout) context = args = null;
};
/**
* 节流化后返回的函数
* 每次事件触发时会执行此函数
*/
var throttled = function() {
// 获取当前时间戳
var now = new Date().getTime();
// 第一次执行且不希望立即执行时,将previous设为当前时间
if (!previous && options.leading === false) previous = now;
// 计算距离下次执行func的剩余时间
var remaining = wait - (now - previous);
// 保存调用时的上下文和参数
context = this;
args = arguments;
// 如果已经到了执行时间点或者时钟回拨了(remaining > wait)
if (remaining <= 0 || remaining > wait) {
// 如果有定时器,清除它
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
// 更新上次执行时间点
previous = now;
// 执行函数
func.apply(context, args);
// 执行完毕后清除上下文和参数引用
if (!timeout) context = args = null;
}
// 如果还没到执行时间点,且允许trailing执行
else if (!timeout && options.trailing !== false) {
// 设置定时器,在剩余时间后执行later
timeout = setTimeout(later, remaining);
}
};
/**
* 取消节流
* 用于停止计时器并重置状态
*/
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
// 返回节流化后的函数
return throttled;
}
发布订阅模式
class EventEmitter {
constructor() {
this.cache = {};// 发布者
}
on(name,fn){
// 建立订阅关系的
if (this.cache[name]){
this.cache[name].push(fn)
}else{
this.cache[name] = [fn]
}
}
emit(name,...args){
// 触发事件
if(this.cache[name]){
let tasks = this.cache[name].slice()
tasks.forEach(fn=>{
fn(...args)
})
}
}
off(name,fn){
let tasks = this.cache[name]
if(tasks) {
const index = tasks.findIndex(f => f === fn)
if(index >= 0){
tasks.splice(index,1)
}
}
}
}
- 维护一个事件中心(cache对象),用于存储事件与对应的回调函数
- 提供三个核心方法:
- on:订阅事件,将回调函数存入对应事件名的数组中
- emit:发布事件,触发特定事件名下所有回调函数的执行
- off:取消订阅,从事件数组中移除特定的回调函数
数组扁平化
arr.flat() 可以实现扁平化
//1
function flatten(arr) {
let result = [];
for (let i = 0; i < arr.length; i++) {
// 判断当前元素是否为数组
if (Array.isArray(arr[i])) {
// 递归扁平化子数组,并合并到结果中
result = result.concat(flatten(arr[i]));
} else {
// 非数组元素直接添加
result.push(arr[i]);
}
}
return result;
}
//2
const arr = [1, [2, [3, [4]], 5], 6];
// 扁平化任意深度
console.log(arr.flat(Infinity)); // [1, 2, 3, 4, 5, 6]
函数柯里化
function currying(fn,...args){
if(fn.length <= args.length){
return fn(...args)
}
return function(...args1){
return currying(fn,...args,...args1) }
}
大数相加
function addBigIntegers(a, b) {
// 初始化指针(从末尾开始)和进位
let i = a.length - 1;
let j = b.length - 1;
let carry = 0;
const result = []; // 遍历两个数字字符串,处理每一位相加
while (i >= 0 || j >= 0 || carry > 0) {
// 获取当前位的数字(超出长度则视为0)
const digitA = i >= 0 ? parseInt(a[i], 10) : 0;
const digitB = j >= 0 ? parseInt(b[j], 10) : 0;
// 计算当前位总和(包含进位)
const sum = digitA + digitB + carry;
// 取当前位结果(sum % 10)
result.push(sum % 10);
// 更新进位(Math.floor(sum / 10))
carry = Math.floor(sum / 10);
// 移动指针
i--; j--; } // 结果数组反转后拼接成字符串
return result.reverse().join('');
}
instanceof
function myInstanceof(obj, constructor) {
// 处理基本类型和null/undefined的情况
if (obj === null || typeof obj !== 'object') {
return false;
} // 获取对象的原型
let proto = Object.getPrototypeOf(obj);
// 遍历原型链
while (proto !== null) {
// 如果找到匹配的原型,返回true
if (proto === constructor.prototype) {
return true; }
// 继续向上查找原型链
proto = Object.getPrototypeOf(proto);
} //
遍历完原型链都没找到匹配,返回false
return false;
}
数值千分位
function format(num) {
const numStrArr = String(num).split('.');
let res = []
let numStr = numStrArr[0];
for (let i = numStr.length; i > 0; i -= 3) {
res.unshift(numStr.slice(Math.max(i - 3, 0), i));
}
if (numStrArr.length == 1) {//每隔三位加,
return res.join(',');
}
else {
return res.join(',') + '.' + numStrArr[1]
}
}
手写new
function myNew(Consructor,...args){
let obj = Object.create(Consructor.prototype);
let res = Consructor.apply(obj,args);
return (typeof res === 'object' &&res!==null )? res:obj;
}
手写call apply bind
//call
Function.prototype.myCall = function(obj,...args){
let context = obj?obj:window;
const fn = Symbol('fn')
context[fn] = this;
const res = context[fn](...args);
delete context[fn];
return res;
}
//apply
Function.prototype.myApply = function(obj,args){
let context = obj?obj:window;
const fn = Symbol('fn')
context[fn] = this;
const res = context[fn](...args);
delete context[fn];
return res;
}
//bind
Function.prototype.myBind = function(obj){
let context = obj?obj:window;
let fun = this;
const bindFun = function(...args){
if(this instanceof bindFun){
return new fun(...args);
}
return fun.apply(context,args);
}
return bindFun
}
手写map filter reduce foreach
//map
Array.prototype.sx_map = function (callback) {
const res = []
for (let i = 0; i < this.length; i++) {
res.push(callback(this[i], i, this))
}
return res
}
//filter
Array.prototype.sx_filter = function (callback) {
const res = []
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this) && res.push(this[i])
}
return res
}
// reduce
Array.prototype.myReduce = function(callback, init){
let res = init;
this.forEach((item, index) => {
res = callback(res, item);
})
return res;
}
// 计算所有num相加
const sum = players.sx_reduce((pre, next) => {
return pre + next.num
}, 0)
console.log(sum) // 85
//forEach
Array.prototype.sx_forEach = function (callback) {
for (let i = 0; i < this.length; i++) {
callback(this[i], i, this)
}
}
players.sx_forEach((item, index, arr) => {
console.log(item, index)
})
setTimeout实现setTimeInterval
function mySetTimout(fn, delay) {
let timer = null
const interval = () => {
fn()
timer = setTimeout(interval, delay)
}
setTimeout(interval, delay)
return {
cancel: () => {
clearTimeout(timer)
}
}
}
实现compose函数
function compose(...fn) {
if (fn.length === 0) return (num) => num
if (fn.length === 1) return fn[0]
return fn.reduce((pre, next) => {
return (num) => {
return next(pre(num))
}
})
}
function fn1(x) {
return x + 1;
}
function fn2(x) {
return x + 2;
}
function fn3(x) {
return x + 3;
}
function fn4(x) {
return x + 4;
}
const a = compose(fn1, fn2, fn3, fn4);
console.log(a)
console.log(a(1)); // 1+2+3+4=11
DOM和树
//dom转树
function dom2tree(dom) {
const obj = {}
obj.tag = dom.tagName
obj.children = []
dom.childNodes.forEach(child => obj.children.push(dom2tree(child)))
return obj
}
//树转dom render函数
function _render(vnode) {
// 如果是数字类型转化为字符串
if (typeof vnode === "number") {
vnode = String(vnode);
}
// 字符串类型直接就是文本节点
if (typeof vnode === "string") {
return document.createTextNode(vnode);
}
// 普通DOM
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
// 遍历属性
Object.keys(vnode.attrs).forEach((key) => {
const value = vnode.attrs[key];
dom.setAttribute(key, value);
});
}
// 子数组进行递归操作
vnode.children.forEach((child) => dom.appendChild(_render(child)));
return dom;
}
浅拷贝 & 深拷贝
// 浅拷贝
const a = { name: 'sunshine_lin', age: 23, arr: [] }
const b = {}
for (let key in a){
b[key] = a[key]
}
const b = {...a};
//深拷贝
// 1
function deepClone(target) {
return JSON.parse(JSON.stringify(target))
}
//缺点:
- 1、对象中有字段值为`undefined`,转换后则会直接字段消失
- 2、对象如果有字段值为`RegExp`对象,转换后则字段值会变成{}
- 3、对象如果有字段值为`NaN、+-Infinity`,转换后则字段值变成null
- 4、对象如果有`环引用`,转换直接报错
//2
function deepClone(target) { // 基本数据类型直接返回
if (typeof target !== 'object') {
return target
}
const temp = Array.isArray(target) ? [] : {}
for (const key in target) { // 递归
temp[key] = deepClone(target[key])
}
return temp
}
//缺点:
没解决循环引用问题,
//3
- 每次遍历到有引用数据类型,
就把他当做`key`放到`Map`中,
对应的`value`是新创建的`对象temp`
- 每次遍历到有引用数据类型,
就去Map中找找有没有对应的`key`,
如果有,就说明这个对象之前已经注册过,
现在又遇到第二次,那肯定就是环引用了,
直接根据`key`获取`value`,并返回`value`
function deepClone(target, map = new Map()) {
// 基本数据类型直接返回
if (typeof target !== 'object') {
return target
}
// 引用数据类型特殊处理
// 判断数组还是对象
const temp = Array.isArray(target) ? [] : {}
+ if (map.get(target)) {
+ // 已存在则直接返回
+ return map.get(target)
+ }
+ // 不存在则第一次设置
+ map.set(target, temp)
for (const key in target) {
// 递归
temp[key] = deepClone(target[key], map)
}
return temp
}
版本号排序
function sortVersions(versions) {
// 复制原数组避免修改原始数据
return [...versions].sort((a, b) => {
// 分割版本号为数组
const aParts = a.split('.');
const bParts = b.split('.');
// 取最长的版本号长度进行比较
const maxLength = Math.max(aParts.length, bParts.length);
for (let i = 0; i < maxLength; i++) {
// 获取当前位置的版本号部分,不存在则视为0
const aPart = parseInt(aParts[i] || 0, 10);
const bPart = parseInt(bParts[i] || 0, 10);
// 比较当前部分
if (aPart > bPart) return 1;
if (aPart < bPart) return -1;
}
// 所有部分都相等
return 0;
});
}
对象key转驼峰 驼峰转下滑线
function strTransform(str) {
return String(str).replace(/_([a-z])/g, (match, letter) => {
console.log(letter);
return letter.toUpperCase();
});
}
function transform(target) {//传入对象,返回key更改后的新对象
if (target === null || typeof target !== 'object') {
return target;
}
if (Array.isArray(target)) {
return target.map((item) => {
return transform(item)
})
}
let result = {};
for (key in target) {
let obj = transform(target[key]);
result[strTransform(key)] = obj;
}
return result;
}
//驼峰转下滑
function toSnakeCase(str) {
// 处理驼峰命名:在大写字母前添加下划线,并转为小写
return str.replace(/[A-Z]/g, (match) => {
return '_' + match.toLowerCase();
});
}
// 递归将对象的所有驼峰key转换为下划线命名
function convertCamelToSnakeCase(obj) {
// 如果不是对象或为null,直接返回
if (obj === null || typeof obj !== 'object') {
return obj;
}
// 如果是数组,递归处理每个元素
if (Array.isArray(obj)) {
return obj.map(item => convertCamelToSnakeCase(item));
}
// 处理对象
const result = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// 将驼峰key转换为下划线命名
const snakeCaseKey = toSnakeCase(key);
// 递归处理值(可能是嵌套对象)
result[snakeCaseKey] = convertCamelToSnakeCase(obj[key]);
}
}
return result;
}
数组转树 子树添加父id
function arrayToTree(arrayData) {
// 创建一个映射,存储所有节点
const map = {};
const tree = [];
// 第一次遍历,将所有节点存入映射
arrayData.forEach(item => {
map[item.id] = { ...item, children: [] };
});
// 第二次遍历,构建树结构
arrayData.forEach(item => {
const node = map[item.id];
if (item.parentId === 0) {
// 根节点
tree.push(node);
} else {
// 子节点,添加到父节点的children数组中
const parent = map[item.parentId];
if (parent) {
parent.children.push(node);
}
}
});
return tree[0] || null;
}
// 2. 树转数组(子树添加父id)
function treeToArray(node, parentId = 0, result = []) {
// 复制节点并添加父ID,删除children属性
const item = { ...node, parentId };
delete item.children;
result.push(item);
// 递归处理子节点
if (node.children && node.children.length) {
node.children.forEach(child =>
treeToArray(child, node.id, result)
);
}
return result;
}
// 测试数据
const arrayData = [
{ id: 1, name: '根节点', parentId: 0 },
{ id: 2, name: '子节点1', parentId: 1 },
{ id: 3, name: '子节点2', parentId: 1 },
{ id: 4, name: '孙节点1', parentId: 2 },
{ id: 5, name: '孙节点2', parentId: 3 },
{ id: 6, name: '曾孙节点', parentId: 4 }
];
// 数组转树测试
const treeData = arrayToTree(arrayData);
console.log('数组转树结果:', JSON.stringify(treeData, null, 2));
// 树转数组测试
const convertedArray = treeToArray(treeData);
解析URL
function parseURL(url) {
const res = {};
const paramsRUL = url.split('?')[1];
const arr = paramsRUL.split('&');
arr.forEach((item) => {
const [key, value] = item.split('=');
if (res[key]) {
if (!Array.isArray(res[key])) {
let a = res[key];
res[key] = [];
res[key].push(a, value)
} else {
res[key].push(value);
}
}
else {
res[key] = value;
}
})
return res;
}
const testUrl = 'https://example.com?name=张三&age=25&hobby=篮球&hobby=音乐&active';
console.log(parseURL(testUrl));
// 输出:
// {
// name: "张三",
// age: "25",
// hobby: ["篮球", "音乐"],
// active: ""
// }
手写promise静态方法
resolve,rejected
Promise.myResolve = function(value) {
return new Promise((resolve, reject) => {
resolve(value);
})
}
//Promise.reject:返回一个带有拒绝原因的Promise对象
Promise.myReject = function(reason) {
return new Promise((resolve, reject) => {
reject(reason);
})
}
all allSettled
//all
Promise.myAll=function(promises){
let res = [];
let count = 0;
return new Promise((resolve,reject)=>{
promises.forEach((item,index) => {
item.then((resolveStr)=>{
res[index] = resolveStr;
count++;
if(count==promises.length){
resolve(res);
}
}).catch((rejectStr)=>reject(rejectStr));
});
})
}
// allSettled
Promise.myAllSettled=function(promises){
let res = [];
let count = 0;
return new Promise((resolve,reject)=>{
promises.forEach((item,index) => {
item.then((resolveStr)=>{
res[index] = {status:'fulfilled',value:resolveStr};
count++;
if(count==promises.length){
resolve(res);
}
}).catch((rejectStr)=>{
res[index] = {status:'rejected',reason:rejectStr};
count++;
if(count==promises.length){
resolve(res);
}
});
});
race any
//race
Promise.myRace = function (promises) {
return new Promise((resolve, reject) => {
promises.forEach((item, index) => {
item.then((resolveStr) => {
resolve(resolveStr);
}).catch((rejectStr) => reject(rejectStr));
});
})
}
//any
Promise.myAny = function (promises) {
return new Promise((resolve, reject) => {
let rejectNum = 0;
let rejectReason = [];
promises.forEach((item, index) => {
item.then((resolveStr) => {
resolve(resolveStr);
}).catch((rejectStr) => {
rejectNum++;
rejectReason[index] = rejectStr;
if(rejectNum == promises.length){
reject(rejectReason);
}
});
});
})
}
promise并发控制池
function promisePool(promises, maxConcurrent) {
let count = 0; // 记录正在执行的 Promise 数量
const results = []; // 存储所有 Promise 的执行结果
const queue = [...promises]; // 克隆任务队列
const enqueue = () => {
while (count < maxConcurrent && queue.length > 0) {
const promise = queue.shift();
count++;
promise()
.then((res) => {
results.push(res);
})
.catch((err) => {
results.push(err);
})
.finally(() => {
count--;
if (queue.length > 0) {
enqueue();
}
});
}
if (count === 0 && queue.length === 0) {
return Promise.resolve(results);
}
};
return enqueue();
}
记录当前正在执行的 Promise 数量(如示例代码中的 count 变量),并与最大并发数进行比较,以此决定是否可以从任务队列中取出新的任务来执行。
当一个 Promise 执行完成( fulfilled 或 rejected )时,通过 finally 回调来减少正在执行的 Promise 数量,并检查是否还有未执行的任务,如果有则继续执行。
使用一个任务队列(如示例代码中的 queue 数组)来存储所有待执行的任务,并在合适的时机从中取出任务执行。
红绿灯
//async await
const task = (light, timeout) => {
return new Promise((resolve) => {
setTimeout(() => resolve(console.log(light))
, timeout)
})
}
const taskRunner = async () => {
await task('red', 1000)
await task('green', 2000)
await task('yellow', 3000)
taskRunner()
}
taskRunner()
//promise
const task = (timer, light) =>
new Promise((resolve, reject) => {
setTimeout(() => {
if (light === 'red') {
red()
} else if (light === 'green') {
green()
} else if (light === 'yellow') {
yellow()
}
resolve()
}, timer)
})
const step = () => {
task(1000, 'red')
.then(() => task(2000, 'green'))
.then(() => task(3000, 'yellow'))
.then(step)
}
step()
lazyman
class _LazyMan {
constructor(name) {
this.tasks = []
const task = () => {
console.log(`Hi! This is ${name}`)
this.next()
}
this.tasks.push(task)
setTimeout(() => {
this.next()
}, 0)
}
next() {
const task = this.tasks.shift()
task && task()
}
sleep(time) {
this.sleepWrapper(time, false)
return this
}
sleepFirst(time) {
this.sleepWrapper(time, true)
return this
}
sleepWrapper(time, first) {
const task = () => {
setTimeout(() => {
console.log(`Wake up after ${time}`)
this.next()
}, time * 1000)
}
if (first) {
this.tasks.unshift(task)
} else {
this.tasks.push(task)
}
}
eat(food) {
const task = () => {
console.log(`Eat ${food}`);
this.next();
};
this.tasks.push(task);
return this;
}
}
// 测试
const lazyMan = (name) => new _LazyMan(name)
lazyMan('Hank').sleep(1).eat('dinner')
lazyMan('Hank').eat('dinner').eat('supper')
lazyMan('Hank').eat('supper').sleepFirst(5)