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