下文总结了面试高频核心的原理题,更能看出前端工程师的素养,文章不会涉及过于小的知识点,以核心发散为导向来查漏补缺。
面试官想要的是什么?
我常常想面试官想要的是什么,基础扎实确实重要,但是深度对于高级前端更重要,在面试中考察的基础往往都是深度来扩散出来的。所以本文适合有一定基础的高级前端的学习和启发思考。本人水平有限错误难免,文章写出来只是互相交流,希望帮助广大掘友找到重点,提高面试能力。
一个优秀的求职者需要具备的能力有哪些?
- 你的能力亮点,必须一语击穿 !
- 项目的每个知识点都必须,了如指掌!
- 面试过程中一定要引导面试官,展示自己的技术深度和广度!
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
}