面经-前端常见手写

手写路由

hash

利用了url的hash值变化但是页面不刷新的原理。我们想要一个路由实例,在实例中注册对应的url可以进行不同的组件的显示。 因此我们首先有个类创建路由实例。 有个注册方法,将不同的hash值与不同的页面操作连接起来,页面操作封装到callback函数里,我们可以用一个map封装。

js 复制代码
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变化可能来自三个不同的来源:

  1. 用户点击浏览器的前进/后退按钮(只会改变url,并不会存储页面)
  • 这会触发popstate事件
  • 我们必须监听这个事件来响应这类变化
  1. 通过history.pushState/replaceState编程方式改变URL
  • 这些方法不会触发任何事件!
  • 调用后需要手动更新页面内容
  1. 用户点击应用内的链接
  • 需要拦截点击事件并阻止默认行为
  • 然后手动处理路由变化

我们的history路由刷新可能出现404,这时候服务器重新返回一下HTML即可

js 复制代码
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

js 复制代码
// 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;
}
js 复制代码
//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
}
js 复制代码
//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();
  })
}

总结:我们的复杂对象使用Proxy来进行拦截。 proxy是es6引入的一个新语法。

  • 当我们的数据类型为复杂对象时,我们无需一个个的将每个属性遍历的用defineProperty进行设置getter和setter,所以他的性能在这种复杂对象上会比defineProterty好很多。
  • 并且他支持13种底层操作的拦截(in,delete,函数调用等,所以可以拦截数组索引和length,对象添加删除)。
  • 对于嵌套的对象,可以实现惰性监听(只有被访问才递归监听)(defineproperty必须知道拦截的属性进行设置) 所以vue3选择了proxy进行响应式的拦截。

我们会创建一个WeakMap(当我们的对象在组件销毁,没有引用指向时候,会自动回收)来存储对象的响应式属性以及需要重新执行的响应式方法 例如Effect()。

当我们使用了Reactive,我们首先会查看一下这个对象是否是响应式对象。他有一个专门的map存储我们的原始对象和代理对象。假如已经是响应式对象,就把我们的响应式对象返回。假如不是,会根据选项将对象放入map或者sharrowmap中:防止出现这种情况:第一次浅度,第二次深度,直接返回浅度对象。

假如不是,他会创建我们的一个代理拦截对象。 当我们的用户第一次访问属性的时候,他会去调用track方法去搜集依赖。假如调用的不是响应式方法,就会直接返回,是响应式方法就加入到依赖map之中。

当用户设置属性的时候,他会拦截去调用 我们的trigger方法。接着遍历我们的每一个方法,重新执行一遍。

ref

js 复制代码
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)

js 复制代码
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

js 复制代码
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调用。

防抖 节流

// 相同间隔内多次取消前一次执行本次

js 复制代码
function debounce(fn,wait){
let timeout;
reutrn function(...args){
if(timeout) clearTimeout(timeout);
timeout = setTimeout() => {(fn.call(this,args)},wait);
}
}
js 复制代码
// 节流 规定时间间隔内只触发一次

function throtle(time,callback){
 let lt = 0;
 var args,context;
 return function(){//剩余参数
 let date = new Date();
 args = arguments;
 context = this;
 if(date - lt > time){
 callback.apply(context,args);
 lt = date;
 }
 }
}

这里再给出一个高级的节流,支持leading,trailing,remaining和cancle,通过判断选项配置当前时间来设置前置执行,通过一个定时器来设置后置执行,通过取消定时器取消后置执行。

js 复制代码
/**
 * 节流函数 - 限制函数在一定时间内只执行一次
 * @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;
}

发布订阅模式

promise并发控制池

js 复制代码
function request(urls=[],limit = 5,done = () => {}){

}

数组扁平化

函数柯里化

深拷贝

相关推荐
Hyyy16 分钟前
ElementPlus按需加载 + 配置中文避坑(干掉1MB冗余代码)
前端·javascript·面试
Summer_Xu28 分钟前
模拟 Koa 中间件机制与洋葱模型
前端·设计模式·node.js
李鸿耀30 分钟前
📦 Rollup
前端·rollup.js
小kian32 分钟前
vite安全漏洞deny解决方案
前端·vite
时物留影34 分钟前
不写代码也能开发 API?试试这个组合!
前端·ai编程
试图感化富婆36 分钟前
【uni-app】市面上的模板一堆?打开源码一看乱的一匹?教你如何定制适合自己的模板
前端
卖报的小行家_36 分钟前
Vue3源码,响应式原理-数组
前端
牛马喜喜36 分钟前
如何从零实现一个todo list (2)
前端
小old弟40 分钟前
jQuery写油猴脚本报错eslint:no-undef - '$' is not defined
前端
Paramita40 分钟前
实战:使用Ollama + Node搭建本地AI问答应用
前端