feng xiaohan
collection

deque

双端队列,可在两端快速进行插入和删除的序列容器。

  • append(x):在右侧添加一个元素。
  • appendleft(x):在左侧添加一个元素。
  • pop():移除并返回一个右侧元素。
  • popleft():移除并返回一个左侧元素。
  • extend(iterable):在右侧添加多个元素。
  • extendleft(iterable):在左侧添加多个元素(注意:添加的元素顺序会被反转)。
  • rotate(n):向右旋转 n 步(负数表示向左旋转)。
  • clear():清空所有元素。
queue = deque()
queue.append(1)
queue.popleft(2)
tips

本地启动服务测试前端打好的包

  • 打完包之后在 dist 目录下执行:
python -m http.server

注意:也可以在后面添加启动的端口号。

speration

复制

浅拷贝

  • 切片(:)
  • list()
  • copy()
org = [1, 2, 3, 4, 5]

o1 = org[:] # 切片
o2 = list(org) # list()
o3 = org.copy() # copy()

深拷贝

  • copy 模块中的 deepcopy()
org = [1, 2, 3, 4, 5]
od1 = copy.deepcopy(org)

反转 list

  • 切片(:)
  • reversed()
org = [1, 2, 3, 4, 5]

o1 = org[::-1] # 切片
o2 = reversed(org) # reversed()

换行

print(5 > /
2) # True
二叉树

根据不同的遍历顺序和对根节点的操作,可以分为前序遍历、中序遍历、后序遍历。其根本原理就是递归查询,直到找到叶子节点的子节点(null),然后返回 null。

思考:树节点的遍历有点类似于 js 的捕获冒泡,捕获阶段对应从根节点遍历到叶子节点,冒泡阶段对应从叶子节点处理逻辑回到根节点,这也被称为递归

相关问题只要掌握好二叉树的遍历顺序,就能解决。

对二叉树进行操作

  1. 确定遍历方法;
  2. 确定每一层节点的操作是什么;

剩下的就交给递归。

链表

由于链表的结构特殊,没有像数组那样方便的库方法,因此其很多操作都需要用到底层算法解决。(不过也因此可以巩固一下)

例如需要手写排序操作。

排序

根据链表的类型可选择不同的排序算法。

单链表

如果要保持时间复杂度 O(nlogn),则需要使用分治策略(归并排序),拆分链表。(详细算法实现参照排序一文)

其中 n 是链表长度。

双向链表

双向链表既可以从前遍历,又可以从后遍历,所以不只可以使用归并排序,还可以使用快速排序。(我猜的,暂未进行实践)

技巧

寻找中间节点:快慢指针,找到 mid 后再根据需求是否断链(前指空)。
dummy 节点:快速返回新的链表。
寻找倒数第 n 个节点:前后指针,前指针走 n 步,然后两个指针再一起走。

单调栈

已知单调栈使用场景:

寻找距离最近的最大/小元素

这种场景一般是从数组的尾部开始遍历,然后不停地更新栈中的元素,使得栈中的元素呈单调递减/增的序列。

寻找最长元素段

与普通单调栈场景不同,需要预先处理一段数据放入栈中,然后再从数组的尾部开始遍历,符合则将栈中数据弹出,比较下一个。

  • 一般栈中存储的是数组索引下标;
  • 遍历数组匹配栈顶元素时,一般只会做出栈操作,不会再入栈;
js 中 this 指向(绑定)

函数在运行时会自动生成一个内部对象(执行上下文),而 this 指向的就是这个对象(执行上下文),所以 this 的指向也是动态的。

this 指向(绑定)与函数调用的位置有关。

箭头函数是没有 this 的,其永远指向全局(window/undefined)。其实质是代码在编译的时候就已经定义好了。

默认绑定

非严格模式下,全局环境中 this 绑定是 window 对象,这也被称为默认绑定。

  • 直接在全局调用 this;
  • 在全局直接执行函数,函数中直接调用 this;

注意: esm 默认开始严格模式,去掉type="module就可以了。

注意在对象中某个属性的值是一个引用函数时,应该使用回调函数或者匿名函数将这个引用的函数包裹起来,避免在执行上下文的时候直接调用,而不是通过这个对象的属性访问之后才调用的。

全局(window)添加属性

  • 全局作用域下使用 var 声明变量;
  • 直接使用window.x
  • 在默认绑定的情况下直接使用 this(非严格模式);

隐式绑定

作为某个对象的方法(上下文对象)调用,this 绑定的是这个对象(上下文对象),这写也被称为隐式绑定。

注意:

  • 如果该函数(方法)是箭头函数无论有几层嵌套对象,this 会进行默认绑定(window/undefined)。

    匿名函数和具名函数则不受其影响。

  • 如果将该对象的函数赋值给了另一个变量,在全局中直接调用该变量对应的函数,也是相当于默认绑定。这也被称为绑定丢失现象
const obj = {
  a: 10,
  b: function () {
    console.log(this);
  },
};
const copy_obj = obj.b;
copy_obj(); // undefined / window

obj.b(); // {a: 10, b: ƒ}

new 绑定

通过 new 关键字调用的函数会生成一个实例对象,this 绑定的就是这个实例对象

注意:如果在这个构造函数中返回了一个引用数据类型(对象、数组、函数),this 指向的就是这个引用数据类型。

实例对象可以使用构造函数中this绑定的值,因为this绑定的就是整个实例对象。如果在实例对象(this)上找不到所访问的属性,就会去它的原型对象上找,遍历整条原型链条直到找到这个值或者 null 为止(null 返回 undefined)。

对象原型

js 中每一个函数都有一个原型对象(prototype),通过 new 关键字(构造函数)生成的实例对象都可以访问这个原型对象上的属性和方法

实例对象可以通过__proto__属性访问这个原型对象
原型对象可通通过constructor属性访问这个实例对象的构造函数

原型对象也有它的原型对象,直到获取到 Object 的原型的原型(null)为止,就这样组成了一条原型链。

模式

使用 this 和原型对象可以实现类似继承的效果。

todo… which parttern

显示绑定

使用call()apply()bind()时,this 绑定的是指定对象

  • call():一个一个传参;
  • apply():传递一个数组;
  • bind():直接返回一个新函数;

其他

  • 可以使用Object.create(null)创建一个没有 this 绑定的对象;
  • 箭头函数会继承外层函数调用的 this 绑定(window/undefined);
  • es6 之前的 self 同 this 机制;
requestAnimationFrame

浏览器每次刷新页面之前执行其中的回调函数,宏任务。返回一个整数编号,可用于传递给cancelAnimationFrame取消动画帧。

  • requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。

    保证其回调函数在屏幕每一次的绘制间隔中执行一次,不会出现丢帧和动画卡顿的情况。

  • 隐藏的元素不会触发回调。

    保证更少的 CPU、GPU 消耗。

  • 浏览器专门为动画提供的 API,浏览器在运行时会针对该 API 进行优化,如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销。

setTimeout 和 setInterval

  • setTimeout 和 setInterval 定时器是不精准的,设置的间隔时间其实是在线程队列中等待的时间。由于 js 的单线程机制,在插入队列前有大量任务或者其中某个任务的执行时间过长,都会影响最终的执行时间。
  • 并且浏览器为了防止过多任务的堆积,对其最小间隔时间做了限制(4ms/10ms),即使你设置 setTimeout(fn, 0),回调函数也不会立即执行,而是至少在 4 毫秒后执行。
  • setInterval 因为其重复执行,其误差值可能会越来越大。

小结
js 事件循环前序队列事件的阻塞、延迟,浏览器的资源调度策略(最小间隔)等问题都会影响 setTimeout 和 setInterval 的精度。

requestAnimationFrame

执行回调与系统的绘制帧率同步(保持最佳绘制效率),不会因为间隔时间过短导致过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅。让网页动画有一个统一的刷新机制,节约系统资源,提高系统性能,改善视觉体验

应用

页面滚动事件(*)

平滑回滚到页面顶部(在 requestAnimationFrame 事件中每次回滚一小段,直到回到顶部):

const scrollToTop = () => {
  const c = document.documentElement.scrollTop || document.body.scrollTop;
  console.log(c);
  if (c > 0) {
    window.requestAnimationFrame(scrollToTop); // 平滑回滚
    window.scrollTo(0, c - c / 8);
    // window.scrollTo(0, c - c);
  }
};
document.addEventListener("scroll", () => {
  window.requestAnimationFrame(() => {
    console.log("scroll");
  });
});

大量数据渲染

一般做法:使用定时器(setTimeout/setInterval)控制循环 loop 函数,在 loop 函数中遍历添加限制长度的数据并添加到页面中。

或者使用分页请求,前端设置 pageSize,每次点击不同的 pageCount 就重新请求一次。

优化:使用 requestAnimationFrame 代替 setTimeout。

//需要插入的容器
const ul = document.getElementById("container");
// 插入十万条数据
const total = 100000;
// 一次插入 20 条
const once = 20;
//总页数
const page = total / once;
//每条记录的索引
const index = 0;
//循环加载数据
function loop(curTotal, curIndex) {
  if (curTotal <= 0) {
    return false;
  }
  //每页多少条
  const pageCount = Math.min(curTotal, once);
  function addData() {
    for (let i = 0; i < pageCount; i++) {
      let li = document.createElement("li");
      li.innerText = curIndex + i + " : " + ~~(Math.random() * total);
      ul.appendChild(li);
    }
    loop(curTotal - pageCount, curIndex + pageCount);
  }
  window.requestAnimationFrame(addData);
  // setTimeout(addData, 0);
}
loop(total, index);

监控页面卡顿(监测性能)

定义一些边界值来检测网页的帧率(fps),通过在 requestAnimationFrame 中执行一些代码,并计算执行时间来判断是否页面卡顿。(requestAnimationFrame 的绘制率是 60HZ,大多数人的视觉系统能够识别的最大帧率为 60 帧每秒)

1s 中 60 帧,1 帧 16.7ms,如果超过 16.7ms,则表示页面卡顿。

水印(*)

使用 requestAnimationFrame 添加页面水印(背景图片)。

防抖和节流

防抖(Debounce)和节流(Throttle)是两种常用的优化前端性能的技术,用于控制事件触发频率,避免过多的事件触发导致性能问题。

防抖

事件触发之后等待一定时间再执行,如果这个时间内该事件又被触发了则重新计时。

function debounce(func, delay) {
  let timer;

  return function () {
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(this, arguments);
    }, delay);
  };
}

适用场景:

  • 输入框搜索建议:用户在输入时,只有在停止输入一段时间后才触发搜索请求。;
  • 窗口大小调整:窗口大小调整时,只在停止调整一段时间后触发重新计算布局。

节流

一定时间间隔内,无论事件触发多少次,只会执行一次。事件只会按照一定时间间隔一次一次执行。

function throttle(func, delay) {
  let isRun = true;
  return function () {
    if (!isRun) return;
    isRun = false;
    setTimeout(() => {
      func.apply(this, arguments);
      isRun = true;
    }, delay);
  };
}

setTimeout 不精准,详情见 setTimeout 一章。

适用场景:

  • 滚动加载:滚动页面时,只在一定时间间隔内加载更多内容,避免频繁加载。
  • 防止按钮重复点击:按钮点击后在一定时间内禁用,防止用户多次点击。
Wujie 使用

基本使用

  • 安装 wujie

    npm i wujie
    
  • 在主应用中引入(以 Vue 应用为例)

    index.js

    import { createApp } from "vue";
    import App from "./App.vue";
    
    import { startApp } from "wujie"; // 引入wujie,通过其中的startApp来设置子应用
    
    const app = createApp(App);
    setupApp({
      name: "唯一id",
      url: "子应用地址",
      exec: true,
      el: "容器",
      sync: true,
    });
    
    app.mount("#app");
    

Vue 组件封装

如果使用的是 vue2 或 vue3 的应用,可以直接使用封装好的组件。

  • 安装 wujie-vue2/ wujie-vue3

    # vue2 框架
    npm i wujie-vue2 -S
    # vue3 框架
    npm i wujie-vue3 -S
    
  • 在主应用中引入并挂载

    import { createApp } from "vue";
    import App from "./App.vue";
    import Wujie from "wujie-vue3"; // 引入wujie-vue3
    const app = createApp(App);
    
    app.use(Wujie); // 注册
    
    app.mount("#app");
    
  • 在主应用中使用,接入子应用

    在此之前要先启动子应用。

    <WujieVue url="http://127.0.0.1:5147" name="vue3"></WujieVue> <!--子应用vue3-->
    ...
    <WujieVue
      width="100%"
      height="100%"
      name="xxx"
      :url="xxx"
      :sync="true"
      :fetch="fetch"
      :props="props"
      :beforeLoad="beforeLoad"
      :beforeMount="beforeMount"
      :afterMount="afterMount"
      :beforeUnmount="beforeUnmount"
      :afterUnmount="afterUnmount"
    </WujieVue>
    ...
    

封装原理

wujie-vue3是按照一个组件的原理去实现的。

import {
  defineComponent,
  h,
  getCurrentInstance,
  onMounted,
  watch,
  onBeforeUnmount,
} from "vue";
import { startApp, bus } from "wujie";
import type { PropType } from "vue";
import { Props } from "./type";

const WujieVue = defineComponent({
  props: {
    width: { type: String, default: "" },
    height: { type: String, default: "" },
    name: { type: String, default: "", required: true },
    loading: { type: HTMLElement, default: undefined },
    url: { type: String, default: "", required: true },
    sync: { type: Boolean, default: undefined },
    prefix: { type: Object, default: undefined },
    alive: { type: Boolean, default: undefined },
    props: { type: Object, default: undefined },
    attrs: { type: Object, default: undefined },
    replace: {
      type: Function as PropType<Props["replace"]>,
      default: undefined,
    },
    fetch: { type: Function as PropType<Props["fetch"]>, default: undefined },
    fiber: { type: Boolean, default: undefined },
    degrade: { type: Boolean, default: undefined },
    plugins: { type: Array as PropType<Props["plugins"]>, default: null },
    beforeLoad: {
      type: Function as PropType<Props["beforeLoad"]>,
      default: null,
    },
    beforeMount: {
      type: Function as PropType<Props["beforeMount"]>,
      default: null,
    },
    afterMount: {
      type: Function as PropType<Props["afterMount"]>,
      default: null,
    },
    beforeUnmount: {
      type: Function as PropType<Props["beforeUnmount"]>,
      default: null,
    },
    afterUnmount: {
      type: Function as PropType<Props["afterUnmount"]>,
      default: null,
    },
    activated: {
      type: Function as PropType<Props["activated"]>,
      default: null,
    },
    deactivated: {
      type: Function as PropType<Props["deactivated"]>,
      default: null,
    },
  },
  setup(props, { emit }) {
    const init = () => {
      const instance = getCurrentInstance();
      startApp({
        // 微前端初始化所需方法
        name: props.name,
        url: props.url,
        el: instance?.refs.wujie as HTMLElement,
        loading: props.loading,
        alive: props.alive,
        fetch: props.fetch,
        props: props.props,
        attrs: props.attrs,
        replace: props.replace,
        sync: props.sync,
        prefix: props.prefix,
        fiber: props.fiber,
        degrade: props.degrade,
        plugins: props.plugins,
        beforeLoad: props.beforeLoad,
        beforeMount: props.beforeMount,
        afterMount: props.afterMount,
        beforeUnmount: props.beforeUnmount,
        afterUnmount: props.afterUnmount,
        activated: props.activated,
        deactivated: props.deactivated,
      });
    };
    // name和url可能是动态的,所以需要监听它们的变化,然后再初始化
    watch([props.name, props.url], () => {
      init();
    });

    // 传参函数
    const hanlderEmit = (event: string, ...args: any[]) => {
      emit(event, ...args); // 暴露给父组件(给bus做了一个中转)
    };

    onMounted(() => {
      bus.$onAll(hanlderEmit); // 监听所有事件
      init();
    });

    onBeforeUnmount(() => {
      bus.$offAll(hanlderEmit); // 销毁
    });

    return () =>
      h("div", {
        style: {
          width: props.width,
          height: props.height,
        },
        ref: "wujie", // 便于之后获取到这个dom
      });
  },
});
WujieVue.install = function (app) {
  app.component("WujieVue", WujieVue); // 组件注册
};

export default WujieVue;

注意:

  • PropType:有很多复杂的数据结构是办法没有标注类型的(在 ts 中类型与接口不对应会报错),所以 vue3 提供一个 PropType 来与定义的结构相对应。(此处的类型对应的就是导出的 Props 里的类型,见下文)
  • getCurrentInstance:在setup()中的this还是 undefined,所以没有办法通过this.$refs来读取。在 vue3 中可以使用getCurrentInstance()来获取组件的实例。
  • bus:wujie 封装的用于传参的对象。

定义所需参数(wujie 的参数)

import type { plugin } from "wujie";
type lifecycle = (appWindow: Window) => any;
interface Props {
  /** 唯一性用户必须保证 */
  name: string;
  /** 需要渲染的url */
  url: string;
  /** 需要渲染的html, 如果用户已有则无需从url请求 */
  html?: string;
  /** 渲染的容器 */
  loading?: HTMLElement;
  /** 路由同步开关, false刷新无效,但是前进后退依然有效 */
  sync?: boolean;
  /** 子应用短路径替换,路由同步时生效 */
  prefix?: { [key: string]: string };
  /** 子应用保活模式,state不会丢失 */
  alive?: boolean;
  /** 注入给子应用的数据 */
  props?: { [key: string]: any };
  /** js采用fiber模式执行 */
  fiber?: boolean;
  /** 子应用采用降级iframe方案 */
  degrade?: boolean;
  /** 自定义运行iframe的属性 */
  attrs?: { [key: string]: any };
  /** 自定义降级渲染iframe的属性 */
  degradeAttrs?: { [key: string]: any };
  /** 代码替换钩子 */
  replace?: (codeText: string) => string;
  /** 自定义fetch,资源和接口 */
  fetch?: (input: RequestInfo, init?: RequestInit) => Promise<Response>;
  /** 子应插件 */
  plugins: Array<plugin>;
  /** 子应用生命周期 */
  beforeLoad?: lifecycle;
  /** 没有做生命周期改造的子应用不会调用 */
  beforeMount?: lifecycle;
  afterMount?: lifecycle;
  beforeUnmount?: lifecycle;
  afterUnmount?: lifecycle;
  /** 非保活应用不会调用 */
  activated?: lifecycle;
  deactivated?: lifecycle;
}

export { Props };

React 组件封装

wujie 基于react的组件封装,使用参数基本和 Vue 的封装类似,只是换了 react 的写法。

预加载

  • 解构出 preloadApp()
  • 使用 preloadApp(),将exec设置为 true 开启预加载
import { createApp } from "vue";
import App from "./App.vue";

import Wujie from "wujie-vue3";
const { preloadApp } = Wujie; // 解构出preloadApp()
const app = createApp(App);

app.use(Wujie);
app.mount("#app");

preloadApp({ name: "react", url: "http://127.0.0.1:5147", exec: true }); // 开启预加载(参数和setupApp相同,多了个exec)

应用通信

wujie 提供三种通信方式:

window 通信

由于子应用运行的iframesrc和主应用是同域的,所以相互可以直接通信。

  • 主应用调用子应用的全局数据

    window.document.querySelector("iframe[name=子应用id]").contentWindow.xxx;
    
  • 子应用调用主应用的全局数据

    window.parent.xxx;
    

例子:

主应用定义一个全局变量:

var a = "abc";

子应用通过window.parent.a访问:

const getParentA = () => {
  console.log(alter(window.parent.a));
};

注意:由于同源策略的限制,如果 iframe 的源与主页面的源不同,那么在控制 iframe 中的元素时可能会受到限制。(如果需要跨域控制 iframe 中的元素,可以考虑使用 postMessage API 来进行通信)

props 通信

  • 主应用可以通过props注入数据和方法

    <WujieVue
      name="xxx"
      url="xxx"
      :props="{ data: xxx, methods: xxx }"
    ></WujieVue>
    

    例子:

    <template>
      <WujieVue
        url="http://127.0.0.1:5147"
        name="vue3"
        :props="{age: 23, name: '张三'}"
      ></WujieVue>
    </template>
    
  • 子应用可以通过$wujie来获取:

    无界对子应用注入了$wujie对象,可以通过$wujie或者window.$wujie获取。

    const props = window.$wujie?.props; // {data: xxx, methods: xxx}
    

    注意:直接写window.$wujie.props可能能用,但是会报错,这是因为没有写$wujie的声明文件,在全局声明一下就好了:

    declare global {
      interface Window {
        $wujie: {
          props: Record<string, any>;
        };
      }
    }
    

eventBus 通信

无界提供一套去中心化的通信方案,主应用和子应用、子应用和子应用都可以通过这种方式进行通信。

通过$on监听,$emit触发双向传递消息。

  • 主应用使用

    // 如果使用wujie
    import { bus } from "wujie";
    
    // 如果使用wujie-vue
    import WujieVue from "wujie-vue";
    const { bus } = WujieVue;
    
    // 如果使用wujie-react
    import WujieReact from "wujie-react";
    const { bus } = WujieReact;
    
    // 主应用监听事件
    bus.$on("事件名字", function (arg1, arg2, ...) {});
    // 主应用发送事件
    bus.$emit("事件名字", arg1, arg2, ...);
    // 主应用取消事件监听
    bus.$off("事件名字", function (arg1, arg2, ...) {});
    

    例子:

    bus.$on("vue3", (data: any) => {
      console.log(date, "我是主应用");
    });
    
  • 子应用使用

    // 子应用监听事件
    window.$wujie?.bus.$on("事件名字", function (arg1, arg2, ...) {});
    // 子应用发送事件
    window.$wujie?.bus.$emit("事件名字", arg1, arg2, ...);
    // 子应用取消事件监听
    window.$wujie?.bus.$off("件名字", function (arg1, arg2, ...) {});
    

    例子:

    const send = () => {
      window.$wujie.bus.$emit("vue3", "我是子应用");
    };