2023年前端核心原理实现

385 阅读7分钟

下文总结了面试高频核心的原理题,更能看出前端工程师的素养,文章不会涉及过于小的知识点,以核心发散为导向来查漏补缺。

面试官想要的是什么?

我常常想面试官想要的是什么,基础扎实确实重要,但是深度对于高级前端更重要,在面试中考察的基础往往都是深度来扩散出来的。所以本文适合有一定基础的高级前端的学习和启发思考。本人水平有限错误难免,文章写出来只是互相交流,希望帮助广大掘友找到重点,提高面试能力。

一个优秀的求职者需要具备的能力有哪些?

  1. 你的能力亮点,必须一语击穿 !
  2. 项目的每个知识点都必须,了如指掌!
  3. 面试过程中一定要引导面试官,展示自己的技术深度和广度!

vue3 响应式

const isObject = val => val !== null && typeof val === 'object'
const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key)

/**
 * dep的结构  
 * WeakMap -> Set
 * 
 * 触发步骤
 * effect > track > trigger > effect()
 * 
 * track 
 * 根据activeEffect, 触发依赖的收集   key-----activeEffect
 * 
 * trigger
 * 触发key的activeEffect
 */
export function reactive (target) {
  if (!isObject(target)) return target

  const handler = {
    get (target, key, receiver) {
      track(target, key)

      const result = Reflect.get(target, key, receiver)
      if (isObject(result)) {
        return reactive(result)
      }
      return result
    },

    set (target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, reactive)

      let result = true
      if (oldValue !== value) {
        result = Reflect.set(target, key, value, receiver)
        trigger(target, key)
      }
      return result
    },

    deleteProperty (target, key) {
      const hadKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)

      if (hadKey && result) {
        target(target, key)
      }
      return result
    },
  }
  return new Proxy(target, handler)
}

let activeEffect = null
export function effect (callback) {
  activeEffect = callback
  callback()
  activeEffect = null
}

let targetMap = new WeakMap()

export function track (target, key) {
  if (!activeEffect) return
  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()))
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect)
  }
}

export function trigger (target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  const dep = depsMap.get(key)

  if (dep) {
    dep.forEach(effect => {
      effect()
    })
  }
}

const convert = val => (isObject(val) ? reactive(val) : val)

class RefImpl {
  constructor(_rawValue) {
    this._rawValue = _rawValue
    this.__v_isRef = true
    this._value = convert(_rawValue)
  }
  get value () {
    track(this, 'value')
    return this._value
  }
  set value (newValue) {
    if (newValue !== this._value) {
      this._rawValue = newValue
      this._value = convert(this._rawValue)
      trigger(this, 'value')
    }
  }
}

export function ref (rawValue) {
  if (isObject(rawValue) && rawValue.__v_isRef) return

  return new RefImpl(rawValue)
}

class ObjectRefImpl {
  constructor(proxy, _key) {
    this._proxy = proxy
    this._key = _key
    this.__v_isRef = true
  }
  get value () {
    return this._proxy[this._key]
  }
  set value (newVal) {
    this._proxy[this._key] = newVal
  }
}

export function toRef (proxy, key) {
  return new ObjectRefImpl(proxy, key)
}

export function toRefs (proxy) {
  const ret = proxy instanceof Array ? new Array(proxy.length) : {}
  for (const key in proxy) {
    ret[key] = toRef(proxy, key)
  }
  return ret
}

reactive原理: reactive将传入的对象被dep收集依赖,effect副作用函数调用会触发对应的Map中对应key的effectList,遍历执行。

ref原理:可以处理reactive的对象类型,核心可以多处理基础类型

toRef原理:通过对象的value属性访问返回对应的key的Proxy对象

toRefs原理: toRefs是toRef多个key的对象合并后可以解构的Proxy对象

核心流程: effect -> track -> trigger -> effect()

vue2 响应式

class Vue {
  // 参数为对象实例 这个对象用于告知vue需要挂载到哪个元素并挂载数据
  constructor(obj_instance) {
    // 给实例赋值对象的data属性
    this.$data = obj_instance.data;
    // 进行数据劫持 监听对象里属性的变化
    Observer(this.$data);
    Compile(obj_instance.el, this);
  }
}

//数据劫持 —— 监听实例里的数据
function Observer (data_instance) {
  // 递归出口
  if (!data_instance || typeof data_instance !== "object") return;
  // 每次数据劫持一个对象时都创建Dependency实例 用于区分哪个对象对应哪个依赖实例和收集依赖
  const dependency = new Dependency();
  Object.keys(data_instance).forEach((key) => {
    // 使用defineProperty后属性里的值会被修改 需要提前保存属性的值
    let value = data_instance[key];
    // 递归劫持data里的子属性
    Observer(value);
    Object.defineProperty(data_instance, key, {
      enumerable: true,
      configurable: true,
      // 收集数据依赖
      get () {
        console.log(`获取了属性值 ${value}`);
        Dependency.temp && dependency.addSub(Dependency.temp);
        return value;
      },
      // 触发视图更新
      set (newVal) {
        console.log(`修改了属性值`);
        value = newVal;
        // 处理赋值是对象时的情况
        Observer(newVal);
        dependency.notify();
      },
    });
  });
}

//模板解析 —— 替换DOM内容 把vue实例上的数据解析到页面上
// 接收两个参数 1.vue实例挂载的元素<div id="app"> 2.vue实例
function Compile (element, vm) {
  vm.$el = document.querySelector(element);
  // 使用文档碎片来临时存放DOM元素 减少DOM更新
  const fragment = document.createDocumentFragment();
  let child;
  // 将页面里的子节点循环放入文档碎片
  while ((child = vm.$el.firstChild)) {
    fragment.appendChild(child);
  }
  fragment_compile(fragment);
  // 替换fragment里文本节点的内容
  function fragment_compile (node) {
    // 使用正则表达式去匹配并替换节点里的{{}}
    const pattern = /\{\{\s*(\S+)\s*\}\}/;
    if (node.nodeType === 3) {
      // 提前保存文本内容 否则文本在被替换一次后 后续的操作都会不生效
      // 打工人: {{name}}  => 打工人:西维 如果不保存后续修改name会匹配不到{{name}} 因为已经被替换
      const texts = node.nodeValue;
      // 获取正则表达式匹配文本字符串获得的所有结果
      const result_regex = pattern.exec(node.nodeValue);
      if (result_regex) {
        const arr = result_regex[1].split("."); // more.salary => ['more', 'salary']
        // 使用reduce归并获取属性对应的值 = vm.$data['more'] => vm.$data['more']['salary']
        const value = arr.reduce((total, current) => total[current], vm.$data);
        node.nodeValue = texts.replace(pattern, value);
        // 在节点值替换内容时 即模板解析的时候 添加订阅者
        // 在替换文档碎片内容时告诉订阅者如何更新 即告诉Watcher如何更新自己
        new Watcher(vm, result_regex[1], (newVal) => {
          node.nodeValue = texts.replace(pattern, newVal);
        });
      }
    }
    // 替换绑定了v-model属性的input节点的内容
    if (node.nodeType === 1 && node.nodeName === "INPUT") {
      const attr = Array.from(node.attributes);
      attr.forEach((item) => {
        if (item.nodeName === "v-model") {
          const value = item.nodeValue
            .split(".")
            .reduce((total, current) => total[current], vm.$data);
          node.value = value;
          new Watcher(vm, item.nodeValue, (newVal) => {
            node.value = newVal;
          });
          node.addEventListener("input", (e) => {
            // ['more', 'salary']
            const arr1 = item.nodeValue.split(".");
            // ['more']
            const arr2 = arr1.slice(0, arr1.length - 1);
            // vm.$data.more
            const final = arr2.reduce(
              (total, current) => total[current],
              vm.$data
            );
            // vm.$data.more['salary'] = e.target.value
            final[arr1[arr1.length - 1]] = e.target.value;
          });
        }
      });
    }
    // 对子节点的所有子节点也进行替换内容操作
    node.childNodes.forEach((child) => fragment_compile(child));
  }
  // 操作完成后将文档碎片添加到页面
  // 此时已经能将vm的数据渲染到页面上 但还未实现数据变动的及时更新
  vm.$el.appendChild(fragment);
}

//依赖 —— 实现发布-订阅模式 用于存放订阅者和通知订阅者更新
class Dependency {
  constructor() {
    this.subscribers = [];  // 用于收集依赖data的订阅者信息
  }
  addSub (sub) {
    this.subscribers.push(sub);
  }
  notify () {
    this.subscribers.forEach((sub) => sub.update());
  }
}

// 订阅者
class Watcher {
  // 需要vue实例上的属性 以获取更新什么数据
  constructor(vm, key, callback) {
    this.vm = vm;
    this.key = key;
    this.callback = callback;
    //临时属性 —— 触发getter 把订阅者实例存储到Dependency实例的subscribers里面
    Dependency.temp = this;
    key.split(".").reduce((total, current) => total[current], vm.$data);
    Dependency.temp = null; // 防止订阅者多次加入到依赖实例数组里
  }
  update () {
    const value = this.key
      .split(".")
      .reduce((total, current) => total[current], this.vm.$data);
    this.callback(value);
  }
}

流程: new Vue()定义了全局vue实例的data、el、render、methods等配置,会先通过Observer递归收集依赖, get会触发Dep的addSub的方法,将Watcher推入订阅的队列。然后Compile编译解析模板,将data内的数据渲染到模板,然后生成Vdom的描述,根据是否更新做diff,在解析模板时会触发get将Watcher内部定义了如何更新渲染模板的方法传入Dep订阅中心,最后挂载到el的根节点。在set方法中会重新递归依赖并触发Dep.notify、执行所有的Watcher的update重新渲染更新到视图。

核心原理: 发布订阅 + 数据劫持(Object.defineProperty)

v-model原理: 核心相同,区别是找到对应的nodeType做触发更新,view更新到model,需要根据解析的v-model字符串然后监听input的事件修改对应的data数据

vue的Compile原理

核心compileToFunctions 函数 主要有三个步骤

1.生成 ast
2.优化静态节点
3.根据 ast 生成 render 函数

import { parse } from "./parse";
import { generate } from "./codegen";
export function compileToFunctions(template) {
  let ast = parse(template);
  let code = generate(ast);
  let renderFn = new Function(`with(this){return ${code}}`);
  return renderFn;
}

前端路由原理

class MyRouter {
  constructor(config) {
    // 路由配置列表
    this._routes = config.routes;
    // 路由历史栈
    this.routeHistory = [];
    this.currentUrl = '';
    this.currentIndex = -1;

    // 跳转中间变量
    this.changeFlag = false;

    this.init();
  }
  init () {
    window.addEventListener(
      'hashchange',
      this.refresh.bind(this),
      false
    );

    window.addEventListener(
      'load',
      this.refresh.bind(this),
      false
    );
  }
  // 单页更新
  refresh () {
    // 1. 路由参数处理
    if (this.changeFlag) {
      this.changeFlag = false;
    } else {
      this.currentUrl = location.hash.slice(1) || '/';
      // 去除分叉路径
      this.routeHistory = this.routeHistory.slice(0, this.currentIndex + 1);
      this.routeHistory.push(this.currentUrl);
      this.currentIndex++;
    }

    // 2. 切换模块
    let path = MyRouter.getPath();
    let currentComponentName = '';
    let nodeList = document.querySelectorAll('[data-component-name]');

    // 查找当前路由名称对应
    // find()
    for (let i = 0; i < this._routes.length; i++) {
      if (this._routes[i].path === path) {
        currentComponentName = this._routes[i].name;
        break;
      }
    }

    // 遍历控制节点模块展示
    nodeList.forEach(item => {
      if (item.dataset.componentName === currentComponentName) {
        item.style.display = 'block';
      } else {
        item.style.display = 'none';
      }
    })
  }

  push (option) {
    if (option.path) {
      MyRouter.changeHash(option.path, option.query);
    } else if (option.name) {
      let path = '';

      for (let i = 0; i < this._routes.length; i++) {
        if (this._routes[i].name === option.name) {
          path = this._routes[i].path;
          break;
        }
      }

      if (path) {
        MyRouter.changeHash(path, option.query);
      }
    }
  }

  back () {
    this.changeFlag = true;
    // ……
  }

  front () {
    this.changeFlag = true;
  }

  static getPath () {
    let href = window.location.href;
    let index = href.indexOf('#');
    if (index < 0) {
      return '';
    }
    href = href.slice(index + 1);

    let searchIndex = href.indexOf("?");
    if (searchIndex < 0) {
      return href;
    } else {
      return href.slice(0, searchIndex);
    }
  }

  static changeHash (path, query) {
    if (query) {
      let str = '';
      for (let i in query) {
        str += '&' + i + '=' + query[i];
      }

      window.location.hash
        = str
          ? path + '?' + str.slice(1)
          : path;
    } else {
      window.location.hash = path;
    }
  }
}

hash路由的实现: 核心监听onhashChange事件,通过一个队列存储路由,一旦改变找到对应dom节点将其展示, router.push根据对应的name修改location.hash的值实现跳转,并处理query参数。

react的fiber

react18的concurrent mode正式实现了从同步更新到可中断的异步更新

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

核心解读: shouldYield代表浏览器是否空闲,react实现了替代的requestIdleCallback的方法, WorkInProgress和current fiber用于双缓存树的替换和复用。performUnitOfWork用于构建fiber树。就是在不断执行只要空闲就构建fiber节点。

流程: 为了不让js执行阻塞ui渲染,将这个js执行过程分片可中断执行,让人一种感觉不卡顿的感受。fiber本质是一个链表,有三个指针sibing、return、child来实现中断。会先通过react jsx通过babel转为createElement的节点,然后通过reconcile的三个阶段处理fiber节点,beginWork转为fiber节点,并生成指针。 completeWork会对节点加上props、事件等属性,并做diff算法做effectTag标记。在CommitWork会根据标记一次性渲染dom,同时在dom更新分为3个阶段(beforeMutation mutation layout)。在这个过程中会根据lanes的模型做Schedule的调度,执行优先级的渲染。通过为了复用节点,在渲染更新的过程会有双缓存树的替换和复用

react的hooks的原理

hooks基于fiber、会在 fiber 节点上放一个链表,每个节点的 memorizedState 属性上存放了对应的不同hooks依赖的数据,通过next指向下一个memorizedState,注意这里会区分mount还是update阶段,同时为了顺序执行hooks不能出现在条件、函数、循环中执行。useCallback、useMemo、useRef无非处理不同阶段的缓存处理。重点是useState和useEffect的调度实现。

useEffect核心:通过pushEffect生成effect对象的环状的单向链表、并会存在fiber.updateQueue中, 根据依赖项做更新,对上次的effect对象做比对做异步更新。相比useLayoutEffect是异步,在commit阶段的beforeMutation不会阻塞渲染。

function pushEffect(tag, create, destroy, deps) {
  // 创建 effect 对象
  var effect = {
    tag: tag,	// effect的类型,区分是 useEffect 还是 useLayoutEffect
    create: create,	// 传入use(Layout)Effect函数的第一个参数,即回调函数
    destroy: destroy,	// 销毁函数
    deps: deps,	// 依赖项
    // Circular
    next: null
  };
  // 获取 fiber 的 updateQueue
  var componentUpdateQueue = currentlyRenderingFiber$1.updateQueue;

  if (componentUpdateQueue === null) {
    componentUpdateQueue = createFunctionComponentUpdateQueue();
    currentlyRenderingFiber$1.updateQueue = componentUpdateQueue;
    // 如果前面没有 effect,则将componentUpdateQueue.lastEffect指针指向effect环状链表的最后一个
    componentUpdateQueue.lastEffect = effect.next = effect;
  } else {
    var lastEffect = componentUpdateQueue.lastEffect;

    if (lastEffect === null) {
      componentUpdateQueue.lastEffect = effect.next = effect;
    } else {
      // 如果前面已经有 effect,将当前生成的 effect 插入链表尾部
      var firstEffect = lastEffect.next;
      lastEffect.next = effect;
      effect.next = firstEffect;
      // 把最后收集到的 effect 放到 lastEffect 上面
      componentUpdateQueue.lastEffect = effect;
    }
  }

  return effect;
}

function createFunctionComponentUpdateQueue() {
  return {
    lastEffect: null,
    stores: null
  };
}

redux原理

流程:createStore传入reducer,返回的store的dispatch触发action执行reducer对应type的纯函数,更新store内的state,渲染更新视图。

function createStore(reducer, enhancer) {
  if(enhancer && typeof enhancer === 'function') {
    const newCreateStore = enhancer(createStore)  //这里用了中间件对其做增强
    const newStore = newCreateStore(reducer)
    return newStore
  }
  let state;             
  let listeners = []; 

  function subscribe(callback) {
    listeners.push(callback);       
  }

  // 1. reducer 执行   2. 将subscribe执行
  function dispatch(action) {
    state = reducer(state, action)

    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
  }

  // getState直接返回state
  function getState() {
    return state;
  }

  // store包装一下前面的方法直接返回
  const store = {
    subscribe,
    dispatch,
    getState
  }

  return store;
}

combineReducers原理: 合并reducer,只维护一个state

thunk中间件原理: 如果action是函数就会调用,并传入dispatch在下次执行

const store = createStore(
  reducer,
  applyMiddleware(logger)
)

const thunk = store => next => action => {
  return typeof action === 'function' ? action(store.dispatch, store.getState) : next(action)
}
const action = function(dispatch) {
  return fetchUsers().then(
    (users) => dispatch({type:'updateUsers', payload: users}),
    (error) => dispatch({type:'updateUsersError'}),
  );
};

dispatch(action)  //异步的dispatch

redux中间件原理: applyMiddleware内部enhancer会在createStore增强内部的store和dispatch。本质compose函数实现。logger中间件会依次执行获得 store -> next(下一个dispatch) -> action -> state

//logger中间件
function logger(store) {
  return function(next) {
    return function(action) {
      console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}
function applyMiddleware(...middlewares) {
  function enhancer(createStore) {
    function newCreateStore(reducer) {
      const store = createStore(reducer);
      const chain = middlewares.map(middleware => middleware(store));
      const { dispatch } = store;
      const newDispatchGen = compose(...chain);
      const newDispatch = newDispatchGen(dispatch);
      return {...store, dispatch: newDispatch}
    }
    return newCreateStore;
  }
  return enhancer;
}

react-redux的connect原理:通过顶层的context的provide和consumer将map和dispatch传入connect的组件,组件可以得到最新state, dispatch方法,一旦store变化,必然会触发forceUpdate,就会立刻更新组件重渲染


export const connect = (mapStateToProps, mapDispatchToProps) => Component => {
  function Connect (props) {
    const store = useContext(ReduxContext)
    const [count, setCount] = useState(true)
    const forceUpdate = () => setCount(val => !val)
    useEffect(() => store.subscribe(forceUpdate, []))

    return (
      <ReduxContext.Consumer>
        {
          store => <>
            <Component {...props} {...mapStateToProps(state.getState())} {...mapDispatchToProps(store.dispatch)}>
            </Component>
          </>
        }
      </ReduxContext.Consumer>
    )
  }
  return Connect
}

koa洋葱模型

/**
 * 递归嵌套、通过next去流转到别的中间件,并一层层执行完后,再从当前next没有执行完的部分执行,一层层向外执行。
 */

const middlewares = [];
let mw1 = async function (ctx, next) {
  console.log("next前,第一个中间件", ctx.name++)
  await next()
  console.log("next后,第一个中间件", ctx.name++)
}
let mw2 = async function (ctx, next) {
  console.log("next前,第二个中间件", ctx.name++)
  await next()
  console.log("next后,第二个中间件", ctx.name++)
}
let mw3 = async function (ctx, next) {
  console.log("第三个中间件,没有next了", ctx.name++)
}

const use = (fn) => {
  middlewares.push(fn);
}

use(mw1);
use(mw2);
use(mw3);

const compose = (middlewares) => {
  return (ctx, next) => {
    function dispatch (i) {
      const fn = middlewares[i];
      if (!fn) return null;
      // 重点:next就是执行dispatch(i)
      return fn(ctx, dispatch.bind(null, i + 1));
    }
    return dispatch(0);
  }
}
const fn = compose(middlewares);
fn({ name: 0 });

发布订阅和观察者模式

本质上,观察者模式和发布订阅模式都是对回调函数的松(解)耦合。回调函数是:事件A结束后,执行事件B。观察者模式实现的是:定义好事件A,事件B,通过“观察”这一行为,将事件A和B的因果先后关系关联起来。发布订阅模式实现的是:事件A结束后,发布到事件中心;事件B订阅A,连同后续回调托管到事件中心。事件中心将A和B关联起来。这一过程中,事件A和事件B完全不会受到对方是否存在的影响,是完全解耦合的。

发布订阅

//发布订阅
class EventEmitter {
  constructor() {
    this.cache = {} // 中介
  }

  on (name, fn) {
    if (this.cache[name]) {
      this.cache[name].push(fn)
    } else {
      this.cache[name] = []
    }
  }

  off (name, fn) {
    let tasks = this.cache[name]
    if (tasks) {
      let index = tasks.findIndex(task => task === fn)
      if (index > -1) {
        tasks.splice(index, 1)
      }
    }
  }

  emit (name, once = false, ...args) {
    let tasks = this.cache[name]
    if (tasks) {
      for (const fn of tasks) {
        fn(...args)
      }
    }
    if (once) {
      delete this.cache[name]
    }
  }
}

let eventBus = new EventEmitter()
let fn1 = function (name, age) {
  console.log(`${name} ${age}`)
}
let fn2 = function (name, age) {
  console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布兰', 12)

观察者模式

//被观察者
class Subject {
  constructor(name) {
    this.state = '开心'
    this.observers = []  //收集
  }
  add (o) {
    this.observers.push(o)
  }
  notify (newState) {
    this.state = newState
    this.observers.forEach(item => item.update(this))
  }
}
// 观察者
class Observer {
  constructor(name) {
    this.name = name;
  }
  update (student) {
    console.log('观察者:' + this.name + '被观察者现在的状态是:' + student.state);
  }
}

let student = new Subject('学生'); //被观察者

let parent = new Observer('父母');  //观察者1
let teacher = new Observer('老师'); //观察者2

student.add(parent);  //订阅观察者1通知
student.add(teacher); //订阅观察者2通知
student.notify('正在好好学习~');  //数据修改,通知所有观察者

cmd和umd的原理

//umd规范,兼容commonjs、amd、cjs
!(function (root, factory) {
  if (typeof module === 'object' && typeof module.exports === 'object') {
    // console.log('是commonjs模块规范,nodejs环境')
    module.exports = factory();
  } else if (typeof define === 'function' && define.amd) {
    // console.log('是AMD模块规范,如require.js')
    define(factory)
  } else if (typeof define === 'function' && define.cmd) {
    // console.log('是CMD模块规范,如sea.js')
    define(function (require, exports, module) {
      module.exports = factory()
    })
  } else {
    // console.log('没有模块环境,直接挂载在全局对象上')
    root.umdModule = factory();
  }
})(this, function () {
  return {
    name: '我是umd模块'
  }
})

//cmd模块
const fs = require('fs')
const { resolve } = require('path')
const { Script } = require('vm')

function commonjsModule (filename) {
  const fileContent = fs.readFileSync(resolve(__dirname, filename))
  const warpped = `(
    function(require, module, exports) {
      ${fileContent}
    }
  )`

  const scripts = new Script(warpped, {
    filename: 'index.js'
  })

  const module = {
    exports: {}
  }

  const func = scripts.runInThisContext()
  func(my_require, module, module.exports)

  return module.exports
}